/* Copyright 2024 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #ifdef _WIN32 #include #endif #include "types.h" //#define BBA_PCAPNG_DUMP #ifdef __MINGW32__ #define _POSIX_SOURCE #endif extern "C" { #include #include #include #include #include #include #include #ifdef _MSC_VER #pragma pack(pop) #endif } #include #include "net_platform.h" #include "picoppp.h" #include "miniupnp.h" #include "cfg/option.h" #include "emulator.h" #include "oslib/oslib.h" #include "util/tsqueue.h" #include "util/shared_this.h" #include "hw/bba/bba.h" #include #include #include #define RESOLVER1_OPENDNS_COM "208.67.222.222" #define AFO_ORIG_IP 0x83f2fb3f // 63.251.242.131 in network order #define IGP_ORIG_IP 0xef2bd2cc // 204.210.43.239 in network order constexpr int PICO_TICK_MS = 5; static pico_device *pico_dev; static TsQueue in_buffer; static TsQueue out_buffer; static pico_ip4 dcaddr; static pico_ip4 dnsaddr; struct pico_ip4 public_ip; static pico_ip4 afo_ip; struct GamePortList { const char *gameId[10]; uint16_t udpPorts[10]; uint16_t tcpPorts[10]; }; static const GamePortList GamesPorts[] = { { // Alien Front Online { "MK-51171" }, { 7980 }, { }, }, { // ChuChu Rocket { "MK-51049", "HDR-0039", "MK-5104950" }, { 9789 }, { }, }, { { "MK-51037", "HDR-0106" // Daytona USA "HDR-0073" // Sega Tetris "GENERIC", "T44501M" // Golf Shiyouyo 2 // (the dreamcastlive patched versions are id'ed as GENERIC) }, { 12079, 20675 }, }, { // Dee Dee Planet { "HDR-0041" }, { 9879 }, }, { // Driving Strikers online demo { "IND-161053" }, { 30099 }, }, { // Floigan Bros { "MK-51114" }, {}, { 37001 }, }, { // Internet Game Pack { "MK-51138" }, { 5656 }, { 5011, 10500, 10501, 10502, 10503 }, }, { // NBA 2K1,2K2 / NFL 2K1,2K2 / NCAA 2K2 { "MK-51063", "HDR-0150", // NBA 2K1 "MK-51178", "HDR-0197", "MK-5117850", // NBA 2K2 "MK-51062", "HDR-0144", // NFL 2K1 "MK-51168", "HDR-0196", // NFL 2K2 "MK-51176" }, // NCAA 2K2 { 5502, 5503, 5656 }, { 5011, 6666 }, }, { // The Next Tetris { "T40214N", "T17717D 50" }, { 3512 }, { 3512 }, }, { // Ooga Booga { "MK-51140" }, { 6001 }, { }, }, { // PBA Tour Bowling 2001 { "T26702N" }, { 6500, 47624, 13139 }, // +dynamic DirectPlay port 2300-2400 { 47624 }, // +dynamic DirectPlay port 2300-2400 }, { // Planet Ring { "MK-5114864", "MK-5112550" }, { 7648, 1285, 1028 }, { }, }, { // StarLancer { "T40209N", "T17723D 05" }, { 6500, 47624 }, // +dynamic DirectPlay port 2300-2400 { 47624 }, // +dynamic DirectPlay port 2300-2400 }, { // World Series Baseball 2K2 { "MK-51152", "HDR-0198" }, { 37171, 13713 }, { }, }, { // Worms World Party { "T22904N", "T7016D 50" }, { }, { 17219 }, }, { // Atomiswave { "FASTER THAN SPEED" }, { 8888 }, { }, }, }; static bool pico_thread_running = false; extern "C" int dont_reject_opt_vj_hack; static bool start_pico(); u32 makeDnsQueryPacket(void *buf, const char *host); pico_ip4 parseDnsResponsePacket(const void *buf, size_t len); static int modem_read(pico_device *dev, void *data, int len) { u8 *p = (u8 *)data; int count = 0; for (; !out_buffer.empty() && count < len; count++) *p++ = out_buffer.pop(); return count; } static int modem_write(pico_device *dev, const void *data, int len) { u8 *p = (u8 *)data; for (int i = 0; i < len; i++) { while (in_buffer.size() > 1024) { if (!pico_thread_running) return 0; PICO_IDLE(); } in_buffer.push(*p++); } return len; } static void write_pico(u8 b) { out_buffer.push(b); } static int read_pico() { if (in_buffer.empty()) return -1; else return in_buffer.pop(); } static int pico_available() { return in_buffer.size(); } class DirectPlay { public: virtual ~DirectPlay() = default; virtual void processOutPacket(const u8 *data, int len) = 0; }; class TcpSocket : public SharedThis { public: void connect(pico_socket *pico_sock) { this->pico_sock = pico_sock; attachPicoSocket(); u32 remoteIp = pico_sock->local_addr.ip4.addr; if (remoteIp == AFO_ORIG_IP // Alien Front Online || remoteIp == IGP_ORIG_IP) // Internet Game Pack { remoteIp = afo_ip.addr; // same ip for both for now } pico.state = Established; asio::ip::address_v4 addrv4(*(std::array *)&remoteIp); asio::ip::tcp::endpoint endpoint(addrv4, htons(pico_sock->local_port)); setName(endpoint); socket.async_connect(endpoint, std::bind(&TcpSocket::onConnect, shared_from_this(), asio::placeholders::error)); } void start() { pico_sock = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, nullptr); if (pico_sock == nullptr) { INFO_LOG(NETWORK, "pico_socket_open failed: error %d", pico_err); return; } attachPicoSocket(); const auto& endpoint = socket.remote_endpoint(); setName(endpoint); memcpy(&pico_sock->local_addr.ip4.addr, endpoint.address().to_v4().to_bytes().data(), 4); pico_sock->local_port = htons(endpoint.port()); if (pico_socket_connect(pico_sock, &dcaddr.addr, htons(socket.local_endpoint().port())) != 0) { INFO_LOG(NETWORK, "pico_socket_connect failed: error %d", pico_err); pico_socket_close(pico_sock); return; } asio.state = Established; socket.set_option(asio::ip::tcp::no_delay(true)); } asio::ip::tcp::socket& getSocket() { return socket; } void close() { closeAll(); directPlay.reset(); } private: TcpSocket(asio::io_context& io_context, std::shared_ptr directPlay) : io_context(io_context), socket(io_context), directPlay(directPlay) { } void setName(const asio::ip::tcp::endpoint& endpoint) { // for logging if (socket.is_open()) name = std::to_string(socket.local_endpoint().port()) + " -> " + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); else name = "? -> " + endpoint.address().to_string() + ":" + std::to_string(endpoint.port()); } void attachPicoSocket() { pico_sock->wakeup = [](uint16_t ev, pico_socket *picoSock) { if (picoSock == nullptr || picoSock->priv == nullptr) ERROR_LOG(NETWORK, "Pico callback with null tcp socket"); else static_cast(picoSock->priv)->get()->picoCallback(ev); }; pico_sock->priv = new Ptr(shared_from_this()); } void detachPicoSocket() { pico.state = Closed; if (pico_sock != nullptr) { pico_sock->wakeup = nullptr; void *priv = pico_sock->priv; pico_sock = nullptr; delete static_cast(priv); // Note: 'this' might have been deleted at this point } } void closeAll() { asio.state = Closed; asio::error_code ec; socket.close(ec); pico.state = Closed; if (pico_sock != nullptr) pico_socket_close(pico_sock); } void onConnect(const std::error_code& ec) { if (ec) { INFO_LOG(NETWORK, "TcpSocket[%s] outbound_connect failed: %s", name.c_str(), ec.message().c_str()); closeAll(); } else { asio.state = Established; socket.set_option(asio::ip::tcp::no_delay(true)); setName(socket.remote_endpoint()); DEBUG_LOG(NETWORK, "TcpSocket[%s] outbound connected", name.c_str()); readAsync(); picoCallback(0); } } void readAsync() { if (asio.readInProgress || asio.state != Established) return; verify(pico.pendingWrite == 0); asio.readInProgress = true; socket.async_read_some(asio::buffer(in_buffer), std::bind(&TcpSocket::onRead, shared_from_this(), asio::placeholders::error, asio::placeholders::bytes_transferred)); } void onRead(const std::error_code& ec, size_t len) { asio.readInProgress = false; if (ec || len == 0) { if (ec && ec != asio::error::eof && ec != asio::error::operation_aborted) INFO_LOG(NETWORK, "TcpSocket[%s] read error %s", name.c_str(), ec.message().c_str()); else DEBUG_LOG(NETWORK, "TcpSocket[%s] asio EOF", name.c_str()); if (pico_sock != nullptr) { if (pico.state == Established) pico_socket_shutdown(pico_sock, PICO_SHUT_WR); else if (pico.state == Closed) pico_socket_close(pico_sock); } asio.state = Closed; return; } if (pico_sock == nullptr) return; DEBUG_LOG(NETWORK, "TcpSocket[%s] inbound %d bytes", name.c_str(), (int)len); if (pico_sock->remote_port == short_be(5011) && len >= 5 && in_buffer[0] == 1) // Visual Concepts sport games memcpy((void *)&in_buffer[1], &pico_sock->local_addr.ip4.addr, 4); pico.pendingWrite = len; picoCallback(PICO_SOCK_EV_WR); } void onWritten(const std::error_code& ec, size_t len) { asio.writeInProgress = false; if (ec) { INFO_LOG(NETWORK, "TcpSocket[%s] write error: %s", name.c_str(), ec.message().c_str()); closeAll(); } else { DEBUG_LOG(NETWORK, "TcpSocket[%s] outbound %d bytes", name.c_str(), (int)len); picoCallback(0); } } void picoCallback(u16 ev) { ev |= pico.pendingEvent; pico.pendingEvent = 0; if (!socket.is_open()) { if (ev & PICO_SOCK_EV_DEL) { detachPicoSocket(); } else { if (ev != PICO_SOCK_EV_FIN) INFO_LOG(NETWORK, "TcpSocket[%s] asio socket is closed (ev %x, pendingW %d)", name.c_str(), ev, pico.pendingWrite); pico_socket_close(pico_sock); } return; } if (ev & PICO_SOCK_EV_RD) { verify(pico.state != Closed); if (asio.state == Connecting || asio.writeInProgress) { pico.pendingEvent |= PICO_SOCK_EV_RD; } else { // This callback might be called recursively if FIN is received pico.readInProgress = true; int r = pico_socket_read(pico_sock, sendbuf, sizeof(sendbuf)); pico.readInProgress = false; DEBUG_LOG(NETWORK, "TcpSocket[%s] read event: pico.state %d, %d bytes", name.c_str(), pico.state, r); if (r > 0) { if (pico_sock->local_port == short_be(5011) && r >= 5 && sendbuf[0] == 1) // Visual Concepts sport games memcpy(&sendbuf[1], &public_ip.addr, 4); else directPlay->processOutPacket((const u8 *)&sendbuf[0], r); asio::async_write(socket, asio::buffer(sendbuf, r), std::bind(&TcpSocket::onWritten, shared_from_this(), asio::placeholders::error, asio::placeholders::bytes_transferred)); asio.writeInProgress = true; } else if (r < 0) { INFO_LOG(NETWORK, "TcpSocket[%s] pico read error: %s", name.c_str(), strerror(pico_err)); if (socket.is_open()) { if (asio.state == Closed) socket.close(); else socket.shutdown(asio::socket_base::shutdown_send); } pico_socket_close(pico_sock); pico.state = Closed; } } } if (ev & PICO_SOCK_EV_WR) { if (pico.pendingWrite > 0) { DEBUG_LOG(NETWORK, "TcpSocket[%s] write event: pico.state %d, %d bytes", name.c_str(), pico.state, pico.pendingWrite); if (pico.state == Connecting) { pico.pendingEvent |= PICO_SOCK_EV_WR; } else { int sent = pico_socket_write(pico_sock, &in_buffer[0], (int)pico.pendingWrite); if (sent < 0) { INFO_LOG(NETWORK, "TcpSocket[%s] pico send error: %s", name.c_str(), strerror(pico_err)); pico.pendingWrite = 0; closeAll(); } else if (sent < (int)pico.pendingWrite) { if (sent > 0) { // FIXME how to handle partial pico writes if any? PICO_SOCK_EV_WR? WARN_LOG(NETWORK, "TcpSocket[%s] Partial pico send: %d -> %d", name.c_str(), (int)pico.pendingWrite, sent); asio.state = Closed; } } else { pico.pendingWrite = 0; readAsync(); } } } else { readAsync(); } } if (ev & PICO_SOCK_EV_CONN) { DEBUG_LOG(NETWORK, "TcpSocket[%s] connect event", name.c_str()); verify(pico.state == Connecting); pico.state = Established; readAsync(); } if (ev & PICO_SOCK_EV_CLOSE) // FIN received { DEBUG_LOG(NETWORK, "TcpSocket[%s] close event (pending ev %x, pico.reading %d, asio.writing %d)", name.c_str(), pico.pendingEvent, pico.readInProgress, asio.writeInProgress); if (pico.pendingEvent == 0 && !pico.readInProgress && !asio.writeInProgress) { pico.state = Closed; if (socket.is_open()) { pico_socket_shutdown(pico_sock, PICO_SHUT_RD); socket.shutdown(asio::socket_base::shutdown_send); } else { pico_socket_close(pico_sock); } } else { pico.pendingEvent |= PICO_SOCK_EV_CLOSE; } } if (ev & PICO_SOCK_EV_FIN) // Socket is in the closed state { DEBUG_LOG(NETWORK, "TcpSocket[%s] FIN event (pending ev %x, asio.writing %d, pico.reading %d)", name.c_str(), pico.pendingEvent, asio.writeInProgress, pico.readInProgress); if (pico.pendingEvent == 0 && !asio.writeInProgress && !pico.readInProgress) closeAll(); else pico.pendingEvent |= PICO_SOCK_EV_FIN; } if (ev & PICO_SOCK_EV_ERR) { INFO_LOG(NETWORK, "TcpSocket[%s] Pico socket error received: %s", name.c_str(), strerror(pico_err)); closeAll(); } if (ev & PICO_SOCK_EV_DEL) detachPicoSocket(); } asio::io_context& io_context; asio::ip::tcp::socket socket; std::shared_ptr directPlay; pico_socket *pico_sock = nullptr; std::array in_buffer; char sendbuf[1510]; enum State { Connecting, Established, Closed }; struct { State state = Connecting; bool readInProgress = false; bool writeInProgress = false; } asio; struct { State state = Connecting; u16 pendingEvent = 0; u32 pendingWrite = 0; bool readInProgress = false; } pico; std::string name; friend super; }; // Handles inbound tcp connections class TcpAcceptor : public SharedThis { public: void start() { TcpSocket::Ptr newSock = TcpSocket::create(io_context, directPlay); sockets.push_back(newSock); acceptor.async_accept(newSock->getSocket(), std::bind(&TcpAcceptor::onAccept, shared_from_this(), newSock, asio::placeholders::error)); } void stop() { acceptor.close(); for (auto& socket : sockets) socket->close(); sockets.clear(); directPlay.reset(); } private: TcpAcceptor(asio::io_context& io_context, u16 port, std::shared_ptr directPlay) : io_context(io_context), acceptor(asio::ip::tcp::acceptor(io_context, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port))), directPlay(directPlay) { } void onAccept(TcpSocket::Ptr newSock, const std::error_code& ec) { if (ec) { if (ec != asio::error::operation_aborted) INFO_LOG(NETWORK, "accept failed: %s", ec.message().c_str()); } else { DEBUG_LOG(NETWORK, "Inbound TCP connection to port %d from %s:%d", acceptor.local_endpoint().port(), newSock->getSocket().remote_endpoint().address().to_string().c_str(), newSock->getSocket().remote_endpoint().port()); newSock->start(); start(); } } asio::io_context& io_context; asio::ip::tcp::acceptor acceptor; std::shared_ptr directPlay; std::vector sockets; friend super; }; // Handles outbound dc tcp sockets class TcpSink { public: TcpSink(asio::io_context& io_context, std::shared_ptr directPlay) : io_context(io_context), directPlay(directPlay) { pico_sock = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_TCP, [](uint16_t ev, pico_socket *picoSock) { if (picoSock == nullptr || picoSock->priv == nullptr) WARN_LOG(NETWORK, "Pico callback with null tcp socket"); else static_cast(picoSock->priv)->picoCallback(ev); }); if (pico_sock == nullptr) ERROR_LOG(NETWORK, "error opening TCP socket: %s", strerror(pico_err)); pico_sock->priv = this; int yes = 1; pico_socket_setoption(pico_sock, PICO_TCP_NODELAY, &yes); pico_ip4 inaddr_any = {}; uint16_t listen_port = 0; int ret = pico_socket_bind(pico_sock, &inaddr_any, &listen_port); if (ret < 0) ERROR_LOG(NETWORK, "error binding TCP socket to port %u: %s", short_be(listen_port), strerror(pico_err)); else if (pico_socket_listen(pico_sock, 10) != 0) ERROR_LOG(NETWORK, "error listening on port %u", short_be(listen_port)); } ~TcpSink() { if (pico_sock != nullptr) pico_sock->wakeup = nullptr; } void stop() { if (pico_sock != nullptr) pico_socket_close(pico_sock); directPlay.reset(); for (auto& socket : sockets) socket->close(); sockets.clear(); } private: void picoCallback(uint16_t ev) { if (ev & PICO_SOCK_EV_CONN) { pico_ip4 orig; uint16_t port; pico_socket *sock_a = pico_socket_accept(pico_sock, &orig, &port); if (sock_a == nullptr) { // Also called for child sockets INFO_LOG(NETWORK, "pico_socket_accept error: %s", strerror(pico_err)); } else { char peer[30]; int yes = 1; pico_ipv4_to_string(peer, sock_a->local_addr.ip4.addr); DEBUG_LOG(NETWORK, "TcpSink: Outbound from port %d to %s:%d", short_be(port), peer, short_be(sock_a->local_port)); pico_socket_setoption(sock_a, PICO_TCP_NODELAY, &yes); pico_tcp_set_linger(sock_a, 10000); TcpSocket::Ptr psock = TcpSocket::create(io_context, directPlay); psock->connect(sock_a); sockets.push_back(psock); } } if (ev & PICO_SOCK_EV_ERR) { INFO_LOG(NETWORK, "TcpSink error: %s", strerror(pico_err)); pico_socket_close(pico_sock); } if (ev & PICO_SOCK_EV_FIN) pico_socket_close(pico_sock); if (ev & (PICO_SOCK_EV_RD | PICO_SOCK_EV_WR)) WARN_LOG(NETWORK, "TcpSink: R/W event %x", ev); if (ev & PICO_SOCK_EV_DEL) { pico_sock->priv = nullptr; pico_sock = nullptr; } } asio::io_context& io_context; std::shared_ptr directPlay; pico_socket *pico_sock; std::vector sockets; }; // Handles inbound datagram to a given port class UdpSocket : public SharedThis { public: void start() { readAsync(); } void sendto(const char *buf, size_t len, u32 addr, u16 port) { asio::ip::udp::endpoint destination(asio::ip::address_v4(addr), port); DEBUG_LOG(NETWORK, "UdpSocket: outbound %d bytes from %d to %s:%d", (int)len, socket.local_endpoint().port(), destination.address().to_string().c_str(), destination.port()); std::error_code ec; socket.send_to(asio::buffer(buf, len), destination, 0, ec); if (ec && ec != asio::error::would_block) INFO_LOG(NETWORK, "UDP sendto failed: %s", ec.message().c_str()); } void close() { asio::error_code ec; socket.close(ec); } private: UdpSocket(asio::io_context& io_context, u16 port, pico_socket *pico_sock, u16 dcport) : io_context(io_context), socket(io_context, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)), pico_sock(pico_sock), dcport(dcport) { asio::socket_base::broadcast option(true); socket.set_option(option); socket.non_blocking(true); } void readAsync() { socket.async_receive_from(asio::buffer(recvbuf), source, [this](const std::error_code& ec, size_t len) { if (ec) { INFO_LOG(NETWORK, "UDP recv_from failed: %s", ec.message().c_str()); return; } DEBUG_LOG(NETWORK, "UdpSocket: received %d bytes to port %d from %s:%d", (int)len, dcport, source.address().to_string().c_str(), source.port()); if (len == 0) WARN_LOG(NETWORK, "Received empty datagram"); // filter out messages coming from ourselves (happens for broadcasts) u32 srcAddr = htonl(source.address().to_v4().to_uint()); if (socket.local_endpoint().port() != source.port() || !is_local_address(srcAddr)) { pico_msginfo msginfo; msginfo.dev = pico_dev; msginfo.tos = 0; msginfo.ttl = 0; msginfo.local_addr.ip4.addr = srcAddr; msginfo.local_port = htons(source.port()); int r = pico_socket_sendto_extended(pico_sock, &recvbuf[0], len, &dcaddr, htons(dcport), &msginfo); if (r < (int)len) INFO_LOG(NETWORK, "error UDP sending to port %d: %s", dcport, strerror(pico_err)); } readAsync(); }); } asio::io_context& io_context; asio::ip::udp::socket socket; pico_socket *pico_sock; std::array recvbuf; asio::ip::udp::endpoint source; // source endpoint when receiving packets u16 dcport; friend super; }; // Handles all outbound datagrams class UdpSink { public: UdpSink(asio::io_context& io_context) : io_context(io_context) { pico_sock = pico_socket_open(PICO_PROTO_IPV4, PICO_PROTO_UDP, [](u16 ev, pico_socket *picoSock) { if (picoSock == nullptr || picoSock->priv == nullptr) ERROR_LOG(NETWORK, "Pico callback with null udp sink"); else static_cast(picoSock->priv)->picoCallback(ev); }); if (pico_sock == nullptr) { ERROR_LOG(NETWORK, "error opening UDP socket: %s", strerror(pico_err)); return; } pico_sock->priv = this; pico_ip4 inaddr_any = {0}; uint16_t listen_port = 0; int ret = pico_socket_bind(pico_sock, &inaddr_any, &listen_port); if (ret < 0) ERROR_LOG(NETWORK, "error binding UDP socket to port %u: %s", short_be(listen_port), strerror(pico_err)); } ~UdpSink() { if (pico_sock != nullptr) pico_sock->wakeup = nullptr; } void setDirectPlay(std::shared_ptr directPlay) { this->directPlay = directPlay; } UdpSocket::Ptr findSocket(u16 port) { auto it = sockets.find(port); if (it != sockets.end()) return it->second; try { UdpSocket::Ptr sock; try { sock = UdpSocket::create(io_context, port, pico_sock, port); } catch (const std::system_error& e) { if (e.code() != asio::error::address_in_use) throw; // Use a random local port WARN_LOG(NETWORK, "Server UDP socket on port %d: address in use, using random port instead", port); sock = UdpSocket::create(io_context, 0, pico_sock, port); } sock->start(); sockets[port] = sock; return sock; } catch (const std::system_error& e) { WARN_LOG(NETWORK, "Server UDP socket on port %d: %s", port, e.what()); return nullptr; } } void stop() { for (auto& [port,sock] : sockets) sock->close(); sockets.clear(); if (pico_sock != nullptr) pico_socket_close(pico_sock); directPlay.reset(); } private: void picoCallback(u16 ev) { if (ev & PICO_SOCK_EV_RD) { char buf[1510]; pico_ip4 src_addr; uint16_t src_port; pico_msginfo msginfo; int r = 0; while (true) { src_port = 0; src_addr = {}; r = pico_socket_recvfrom_extended(pico_sock, buf, sizeof(buf), &src_addr.addr, &src_port, &msginfo); if (r < 0) { INFO_LOG(NETWORK, "error UDP recv: %s", strerror(pico_err)); break; } if (r == 0 && src_port == 0 && src_addr.addr == 0) // No more packets break; if (r == 0) WARN_LOG(NETWORK, "Sending empty datagram"); // Daytona USA if (msginfo.local_port == 0x2F2F && r >= 3 && buf[0] == 0x20 && buf[2] == 0x42) { if (buf[1] == 0x2b && r >= 37 + (int)sizeof(public_ip.addr)) { // Start session packet char *p = &buf[37]; if (memcmp(p, &dcaddr.addr, sizeof(dcaddr.addr)) == 0) memcpy(p, &public_ip.addr, sizeof(public_ip.addr)); } else if (buf[1] == 0x15 && r >= 14 + (int)sizeof(public_ip.addr)) { char *p = &buf[5]; if (memcmp(p, &dcaddr.addr, sizeof(dcaddr.addr)) == 0) memcpy(p, &public_ip.addr, sizeof(public_ip.addr)); p = &buf[14]; if (memcmp(p, &dcaddr.addr, sizeof(dcaddr.addr)) == 0) memcpy(p, &public_ip.addr, sizeof(public_ip.addr)); } } else if (msginfo.local_port == htons(47624)) directPlay->processOutPacket((const u8 *)buf, r); UdpSocket::Ptr sock = findSocket(htons(src_port)); if (sock) sock->sendto(buf, r, htonl(msginfo.local_addr.ip4.addr), htons(msginfo.local_port)); } } if (ev & PICO_SOCK_EV_DEL) { pico_sock->wakeup = nullptr; pico_sock = nullptr; } } asio::io_context& io_context; pico_socket *pico_sock = nullptr; std::unordered_map sockets; std::shared_ptr directPlay; }; class DirectPlayImpl : public DirectPlay, public SharedThis { public: void processOutPacket(const u8 *data, int len) override { if (!isDirectPlay(data, len)) return; u16 port = htons(*(u16 *)&data[6]); if (port >= 2300 && port <= 2400 && port != this->port) { NOTICE_LOG(NETWORK, "DirectPlay4 local port is %d", port); if (acceptor) { acceptor->stop(); acceptor.reset(); } forwardPorts(port, false); this->port = port; udpSink.findSocket(port); try { acceptor = TcpAcceptor::create(io_context, port, shared_from_this()); acceptor->start(); } catch (const std::system_error& e) { WARN_LOG(NETWORK, "DirectPlay TCP socket on port %d: %s", port, e.what()); } } if (*(u16 *)&data[24] == 0x13) // Add Forward Request { // This one is the guest game port, only UDP is used u16 port = htons(*(u16 *)&data[0x72]); if (port >= 2300 && port <= 2400 && port != this->gamePort) { if (*(u16 *)&data[0x62] == this->port) WARN_LOG(NETWORK, "DirectPlay4 AddForwardRequest expected port %d got %d", this->port, *(u16 *)&data[0x62]); NOTICE_LOG(NETWORK, "DirectPlay4 game port is %d", port); forwardPorts(port, true); this->gamePort = port; udpSink.findSocket(port); } } } ~DirectPlayImpl() { stop(); if (upnpCmd.valid()) upnpCmd.get(); } void stop() { if (acceptor) acceptor->stop(); acceptor.reset(); } private: DirectPlayImpl(asio::io_context& io_context, UdpSink& udpSink, std::shared_ptr upnp) : io_context(io_context), udpSink(udpSink), upnp(upnp) { } bool isDirectPlay(const u8 *data, int len) { return len >= 24 && (data[2] & 0xf0) == 0xb0 && data[3] == 0xfa // DirectPlay4 signature && !memcmp(&data[20], "play", 4); } void forwardPorts(u16 port, bool udpOnly) { if (upnp && upnp->isInitialized()) { if (upnpCmd.valid()) upnpCmd.get(); upnpCmd = std::async(std::launch::async, [this, port, udpOnly]() { if (!upnp->AddPortMapping(port, false)) WARN_LOG(NETWORK, "UPNP AddPortMapping UDP %d failed", port); if (!udpOnly && !upnp->AddPortMapping(port, true)) WARN_LOG(NETWORK, "UPNP AddPortMapping TCP %d failed", port); }); } } u16 port = 0; u16 gamePort = 0; TcpAcceptor::Ptr acceptor; asio::io_context& io_context; UdpSink& udpSink; std::shared_ptr upnp; std::future upnpCmd; friend super; }; class DnsResolver : public SharedThis { public: void resolve(const char *host, pico_ip4 *result) { // need to introduce a dns query object if concurrency is needed verify(!busy); busy = true; u32 len = makeDnsQueryPacket(buf, host); socket.async_send_to(asio::buffer(buf, len), nsEndpoint, std::bind(&DnsResolver::querySent, shared_from_this(), result, asio::placeholders::error, asio::placeholders::bytes_transferred)); } private: DnsResolver(asio::io_context& io_context, const char *nameServer) : io_context(io_context), socket(io_context) { using namespace asio::ip; udp::resolver resolver(io_context); nsEndpoint = *resolver.resolve(udp::v4(), nameServer, "53").begin(); socket.open(udp::v4()); } void querySent(pico_ip4 *result, const std::error_code& ec, size_t len) { if (!ec) { socket.async_receive_from(asio::mutable_buffer(buf, sizeof(buf)), nsEndpoint, std::bind(&DnsResolver::responseReceived, shared_from_this(), result, asio::placeholders::error, asio::placeholders::bytes_transferred)); } else { busy = false; } } void responseReceived(pico_ip4 *result, const std::error_code& ec, size_t len) { if (!ec) { *result = parseDnsResponsePacket(buf, len); DEBUG_LOG(NETWORK, "dns resolved: %s (using %s)", asio::ip::address_v4(*(std::array *)result).to_string().c_str(), nsEndpoint.address().to_string().c_str()); } busy = false; } asio::io_context& io_context; asio::ip::udp::endpoint nsEndpoint; asio::ip::udp::socket socket; char buf[1024]; bool busy = false; friend super; }; static void resolveDns(asio::io_context& io_context) { public_ip.addr = 0; afo_ip.addr = 0; DnsResolver::Ptr resolver = DnsResolver::create(io_context, RESOLVER1_OPENDNS_COM); resolver->resolve("myip.opendns.com", &public_ip); char str[16]; pico_ipv4_to_string(str, dnsaddr.addr); resolver = DnsResolver::create(io_context, str); resolver->resolve("auriga.segasoft.com", &afo_ip); } static pico_device *pico_eth_create() { pico_device *eth = (pico_device *)PICO_ZALLOC(sizeof(pico_device)); if (!eth) return nullptr; const u8 mac_addr[6] = { 0xc, 0xa, 0xf, 0xe, 0, 1 }; if (0 != pico_device_init(eth, "ETHPEER", mac_addr)) return nullptr; DEBUG_LOG(NETWORK, "Device %s created", eth->name); return eth; } static FILE *pcapngDump; static void dumpFrame(const u8 *frame, u32 size) { #ifdef BBA_PCAPNG_DUMP if (pcapngDump == nullptr) { pcapngDump = fopen("bba.pcapng", "wb"); if (pcapngDump == nullptr) { const char *home = getenv("HOME"); if (home != nullptr) { std::string path = home + std::string("/bba.pcapng"); pcapngDump = fopen(path.c_str(), "wb"); } if (pcapngDump == nullptr) return; } u32 blockType = 0x0A0D0D0A; // Section Header Block fwrite(&blockType, sizeof(blockType), 1, pcapngDump); u32 blockLen = 28; fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump); u32 magic = 0x1A2B3C4D; fwrite(&magic, sizeof(magic), 1, pcapngDump); u32 version = 1; // 1.0 fwrite(&version, sizeof(version), 1, pcapngDump); u64 sectionLength = ~0; // unspecified fwrite(§ionLength, sizeof(sectionLength), 1, pcapngDump); fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump); blockType = 1; // Interface Description Block fwrite(&blockType, sizeof(blockType), 1, pcapngDump); blockLen = 20; fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump); const u32 linkType = 1; // Ethernet fwrite(&linkType, sizeof(linkType), 1, pcapngDump); const u32 snapLen = 0; // no limit fwrite(&snapLen, sizeof(snapLen), 1, pcapngDump); // TODO options? if name, ip/mac address fwrite(&blockLen, sizeof(blockLen), 1, pcapngDump); } const u32 blockType = 6; // Extended Packet Block fwrite(&blockType, sizeof(blockType), 1, pcapngDump); u32 roundedSize = ((size + 3) & ~3) + 32; fwrite(&roundedSize, sizeof(roundedSize), 1, pcapngDump); u32 ifId = 0; fwrite(&ifId, sizeof(ifId), 1, pcapngDump); u64 now = getTimeMs() * 1000; fwrite((u32 *)&now + 1, 4, 1, pcapngDump); fwrite(&now, 4, 1, pcapngDump); fwrite(&size, sizeof(size), 1, pcapngDump); fwrite(&size, sizeof(size), 1, pcapngDump); fwrite(frame, 1, size, pcapngDump); fwrite(frame, 1, roundedSize - size - 32, pcapngDump); fwrite(&roundedSize, sizeof(roundedSize), 1, pcapngDump); #endif } static void closeDumpFile() { if (pcapngDump != nullptr) { fclose(pcapngDump); pcapngDump = nullptr; } } static void pico_receive_eth_frame(const u8 *frame, u32 size) { if (pico_dev == nullptr) { start_pico(); } else { dumpFrame(frame, size); pico_stack_recv(pico_dev, (u8 *)frame, size); } } static int send_eth_frame(pico_device *dev, void *data, int len) { dumpFrame((const u8 *)data, len); return bba_recv_frame((const u8 *)data, len); } static void picoTick(const std::error_code& ec, asio::steady_timer *timer) { if (ec) { ERROR_LOG(NETWORK, "picoTick timer error: %s", ec.message().c_str()); return; } pico_stack_tick(); timer->expires_at(timer->expiry() + asio::chrono::milliseconds(PICO_TICK_MS)); timer->async_wait(std::bind(picoTick, asio::placeholders::error, timer)); } class PicoThread { public: void start() { verify(!thread.joinable()); io_context = std::make_unique(); thread = std::thread(&PicoThread::run, this); } void stop() { if (!thread.joinable()) return; io_context->stop(); thread.join(); io_context.reset(); } private: void run(); const GamePortList *ports = nullptr; std::shared_ptr upnp; bool usingPPP = false; std::thread thread; std::unique_ptr io_context; }; void PicoThread::run() { ThreadName _("PicoTCP"); // Find the network ports for the current game ports = nullptr; for (u32 i = 0; i < std::size(GamesPorts) && ports == nullptr; i++) { const auto& game = GamesPorts[i]; for (u32 j = 0; j < std::size(game.gameId) && game.gameId[j] != nullptr; j++) { if (settings.content.gameId == game.gameId[j]) { NOTICE_LOG(NETWORK, "Found network ports for game %s", settings.content.gameId.c_str()); ports = &game; break; } } } // Web TV requires the VJ compression option, which picotcp doesn't support. // This hack allows WebTV to connect although the correct fix would // be to implement VJ compression. dont_reject_opt_vj_hack = settings.content.gameId == "6107117" || settings.content.gameId == "610-7390" || settings.content.gameId == "610-7391" ? 1 : 0; std::future pnpFuture; if (ports != nullptr && config::EnableUPnP) { upnp = std::make_shared(); pnpFuture = std::move( std::async(std::launch::async, [this]() { // Initialize miniupnpc and map network ports ThreadName _("UPNP-init"); if (!upnp->Init()) WARN_LOG(NETWORK, "UPNP Init failed"); else { for (u32 i = 0; i < std::size(ports->udpPorts) && ports->udpPorts[i] != 0; i++) if (!upnp->AddPortMapping(ports->udpPorts[i], false)) WARN_LOG(NETWORK, "UPNP AddPortMapping UDP %d failed", ports->udpPorts[i]); for (u32 i = 0; i < std::size(ports->tcpPorts) && ports->tcpPorts[i] != 0; i++) if (!upnp->AddPortMapping(ports->tcpPorts[i], true)) WARN_LOG(NETWORK, "UPNP AddPortMapping TCP %d failed", ports->tcpPorts[i]); } })); } // Empty queues in_buffer.clear(); out_buffer.clear(); // Find DNS ip address { std::string dnsName = config::DNS; if (dnsName == "46.101.91.123") // override legacy default with current one dnsName = "dns.flyca.st"; asio::ip::udp::resolver resolver(*io_context); std::error_code ec; auto it = resolver.resolve(asio::ip::udp::v4(), dnsName, "53", ec); if (ec) WARN_LOG(NETWORK, "%s: %s", dnsName.c_str(), ec.message().c_str()); if (!ec && !it.empty()) { asio::ip::udp::endpoint endpoint = *it.begin(); memcpy(&dnsaddr.addr, &endpoint.address().to_v4().to_bytes()[0], sizeof(dnsaddr.addr)); char s[17]; pico_ipv4_to_string(s, dnsaddr.addr); NOTICE_LOG(NETWORK, "%s IP is %s", dnsName.c_str(), s); } else { u32 addr; pico_string_to_ipv4("46.101.91.123", &addr); dnsaddr.addr = addr; WARN_LOG(NETWORK, "Can't resolve dns.flyca.st. Using default 46.101.91.123"); } } resolveDns(*io_context); pico_stack_init(); // Create ppp/eth device usingPPP = !config::EmulateBBA; u32 addr; if (usingPPP) { // PPP pico_dev = pico_ppp_create(); if (!pico_dev) throw FlycastException("PicoTCP ppp creation failed"); pico_string_to_ipv4("192.168.167.2", &addr); memcpy(&dcaddr.addr, &addr, sizeof(addr)); pico_ppp_set_peer_ip(pico_dev, dcaddr); pico_string_to_ipv4("192.168.167.1", &addr); pico_ip4 ipaddr; memcpy(&ipaddr.addr, &addr, sizeof(addr)); pico_ppp_set_ip(pico_dev, ipaddr); pico_ppp_set_dns1(pico_dev, dnsaddr); pico_ppp_set_serial_read(pico_dev, modem_read); pico_ppp_set_serial_write(pico_dev, modem_write); pico_ppp_set_serial_set_speed(pico_dev, [](pico_device *dev, uint32_t speed) { return 0; }); pico_dev->proxied = 1; pico_ppp_connect(pico_dev); } else { // Ethernet pico_dev = pico_eth_create(); if (pico_dev == nullptr) throw FlycastException("PicoTCP eth creation failed"); pico_dev->send = &send_eth_frame; pico_dev->proxied = 1; pico_queue_protect(pico_dev->q_in); pico_string_to_ipv4("192.168.169.1", &addr); pico_ip4 ipaddr; memcpy(&ipaddr.addr, &addr, sizeof(addr)); pico_string_to_ipv4("255.255.255.0", &addr); pico_ip4 netmask; memcpy(&netmask.addr, &addr, sizeof(addr)); pico_ipv4_link_add(pico_dev, ipaddr, netmask); // dreamcast IP pico_string_to_ipv4("192.168.169.2", &addr); memcpy(&dcaddr.addr, &addr, sizeof(addr)); pico_dhcp_server_setting dhcpSettings{ 0 }; dhcpSettings.dev = pico_dev; dhcpSettings.server_ip = ipaddr; dhcpSettings.lease_time = long_be(24 * 60 * 60); // seconds dhcpSettings.netmask = netmask; dhcpSettings.pool_start = addr; dhcpSettings.pool_end = addr; dhcpSettings.pool_next = addr; dhcpSettings.dns_server.addr = dnsaddr.addr; if (pico_dhcp_server_initiate(&dhcpSettings) != 0) WARN_LOG(NETWORK, "DHCP server init failed"); } // Create sinks UdpSink udpSink(*io_context); DirectPlayImpl::Ptr directPlay = DirectPlayImpl::create(*io_context, udpSink, upnp); udpSink.setDirectPlay(directPlay); TcpSink tcpSink(*io_context, directPlay); // Open listening sockets std::vector acceptors; if (ports != nullptr) { for (u32 i = 0; i < std::size(ports->udpPorts) && ports->udpPorts[i] != 0; i++) udpSink.findSocket(ports->udpPorts[i]); for (u32 i = 0; i < std::size(ports->tcpPorts) && ports->tcpPorts[i] != 0; i++) try { auto acceptor = TcpAcceptor::create(*io_context, ports->tcpPorts[i], directPlay); acceptor->start(); acceptors.push_back(std::move(acceptor)); } catch (const std::system_error& e) { WARN_LOG(NETWORK, "Server TCP socket on port %d: %s", ports->tcpPorts[i], e.what()); } } // pico stack timer asio::steady_timer timer(*io_context); picoTick({}, &timer); // main loop io_context->run(); for (auto& acceptor : acceptors) acceptor->stop(); acceptors.clear(); tcpSink.stop(); udpSink.stop(); directPlay->stop(); directPlay.reset(); pico_stack_tick(); pico_stack_tick(); pico_stack_tick(); if (pico_dev) { if (usingPPP) { pico_ppp_destroy(pico_dev); } else { closeDumpFile(); pico_dhcp_server_destroy(pico_dev); pico_device_destroy(pico_dev); } pico_dev = nullptr; } pico_stack_deinit(); if (upnp) { std::thread pnpTerm([upnp = this->upnp]() { upnp->Term(); }); pnpTerm.detach(); upnp.reset(); } } static PicoThread pico_thread; static bool start_pico() { emu.setNetworkState(true); if (pico_thread_running) return false; pico_thread_running = true; pico_thread.start(); return true; } static void stop_pico() { emu.setNetworkState(false); pico_thread_running = false; pico_thread.stop(); } // picotcp mutex implementation extern "C" { void *pico_mutex_init(void) { return new std::mutex(); } void pico_mutex_lock(void *mux) { ((std::mutex *)mux)->lock(); } void pico_mutex_unlock(void *mux) { ((std::mutex *)mux)->unlock(); } void pico_mutex_deinit(void *mux) { delete (std::mutex *)mux; } } namespace net::modbba { bool PicoTcpService::start() { return start_pico(); } void PicoTcpService::stop() { stop_pico(); } void PicoTcpService::writeModem(u8 b) { write_pico(b); } int PicoTcpService::readModem() { return read_pico(); } int PicoTcpService::modemAvailable() { return pico_available(); } void PicoTcpService::receiveEthFrame(const u8 *frame, u32 size) { pico_receive_eth_frame(frame, size); } }