mirror of
https://github.com/flyinghead/flycast.git
synced 2026-11-08 17:12:25 -10:00
pico_socket_connect() ignored the result of pico_socket_alter_state(), which fails when a socket with the same local/remote address and port is already in the socket tree. The new socket then carried on untracked, and deleting it later found and unlinked the other socket instead, which was then freed while still in use: a double free or heap corruption. picoppp hits this when it forwards an inbound connection whose client reused a source port while picotcp still holds the previous connection (Linux reuses loopback source ports at once). Fail the connect with EADDRINUSE, and have picoppp retry with a port picotcp picks. Found by forwarding a few hundred connections a second to the Broadband Adapter; the crash went away with this. [ci skip]
1545 lines
40 KiB
C++
1545 lines
40 KiB
C++
/*
|
|
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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef _WIN32
|
|
#include <ws2tcpip.h>
|
|
#endif
|
|
|
|
#include "types.h"
|
|
|
|
//#define BBA_PCAPNG_DUMP
|
|
|
|
#ifdef __MINGW32__
|
|
#define _POSIX_SOURCE
|
|
#endif
|
|
|
|
extern "C" {
|
|
#include <pico_stack.h>
|
|
#include <pico_dev_ppp.h>
|
|
#include <pico_socket.h>
|
|
#include <pico_socket_tcp.h>
|
|
#include <pico_ipv4.h>
|
|
#include <pico_tcp.h>
|
|
#include <pico_dhcp_server.h>
|
|
#ifdef _MSC_VER
|
|
#pragma pack(pop)
|
|
#endif
|
|
}
|
|
#include <asio.hpp>
|
|
|
|
#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 <unordered_map>
|
|
#include <deque>
|
|
#include <mutex>
|
|
#include <future>
|
|
#include <vector>
|
|
|
|
#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<u8> in_buffer;
|
|
static TsQueue<u8> out_buffer;
|
|
|
|
static TsQueue<std::vector<u8>> bbaBuffer;
|
|
|
|
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, Alien Front (naomi)
|
|
{ "MK-51171", "ALIEN FRONT" },
|
|
{ 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;
|
|
// ppp: wait until the game sends some data before responding. Required for WinCE games.
|
|
static bool passiveMode = true;
|
|
|
|
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)
|
|
{
|
|
if (passiveMode) {
|
|
in_buffer.clear();
|
|
passiveMode = false;
|
|
}
|
|
out_buffer.push(b);
|
|
}
|
|
|
|
static int read_pico()
|
|
{
|
|
if (in_buffer.empty() || passiveMode)
|
|
return -1;
|
|
else
|
|
return in_buffer.pop();
|
|
}
|
|
|
|
static int pico_available() {
|
|
return passiveMode ? 0 : in_buffer.size();
|
|
}
|
|
|
|
class DirectPlay
|
|
{
|
|
public:
|
|
virtual ~DirectPlay() = default;
|
|
virtual void processOutPacket(const u8 *data, int len) = 0;
|
|
};
|
|
|
|
class TcpSocket : public SharedThis<TcpSocket>
|
|
{
|
|
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<u8, 4> *)&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());
|
|
int rc = pico_socket_connect(pico_sock, &dcaddr.addr, htons(socket.local_endpoint().port()));
|
|
if (rc != 0 && pico_err == PICO_ERR_EADDRINUSE)
|
|
{
|
|
// The client reused a source port while picotcp still holds its previous
|
|
// connection: let picotcp pick a free port instead.
|
|
pico_sock->local_port = 0;
|
|
rc = pico_socket_connect(pico_sock, &dcaddr.addr, htons(socket.local_endpoint().port()));
|
|
}
|
|
if (rc != 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();
|
|
detachPicoSocket();
|
|
directPlay.reset();
|
|
}
|
|
|
|
private:
|
|
TcpSocket(asio::io_context& io_context, std::shared_ptr<DirectPlay> 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<Ptr *>(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<Ptr *>(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);
|
|
// A read notification can cover more data than fits in sendbuf.
|
|
// Resume draining after the asynchronous write releases that buffer,
|
|
// even if no new packet (and therefore no new read event) arrives.
|
|
picoCallback(pico_sock != nullptr && pico.state != Closed ? PICO_SOCK_EV_RD : 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())
|
|
{
|
|
// The host peer may already be gone: don't throw (and terminate) over it
|
|
asio::error_code ec;
|
|
if (asio.state == Closed)
|
|
socket.close(ec);
|
|
else
|
|
socket.shutdown(asio::socket_base::shutdown_send, ec);
|
|
}
|
|
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);
|
|
asio::error_code ec;
|
|
socket.shutdown(asio::socket_base::shutdown_send, ec);
|
|
}
|
|
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> directPlay;
|
|
pico_socket *pico_sock = nullptr;
|
|
std::array<char, 536> 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<TcpAcceptor>
|
|
{
|
|
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> 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> directPlay;
|
|
std::vector<TcpSocket::Ptr> sockets;
|
|
friend super;
|
|
};
|
|
|
|
// Handles outbound dc tcp sockets
|
|
class TcpSink
|
|
{
|
|
public:
|
|
TcpSink(asio::io_context& io_context, std::shared_ptr<DirectPlay> 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<TcpSink *>(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> directPlay;
|
|
pico_socket *pico_sock;
|
|
std::vector<TcpSocket::Ptr> sockets;
|
|
};
|
|
|
|
// Handles inbound datagram to a given port
|
|
class UdpSocket : public SharedThis<UdpSocket>
|
|
{
|
|
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<u8, 1510> 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<UdpSink *>(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> 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<u16, UdpSocket::Ptr> sockets;
|
|
std::shared_ptr<DirectPlay> directPlay;
|
|
};
|
|
|
|
class DirectPlayImpl : public DirectPlay, public SharedThis<DirectPlayImpl>
|
|
{
|
|
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<MiniUPnP> 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<MiniUPnP> upnp;
|
|
std::future<void> upnpCmd;
|
|
friend super;
|
|
};
|
|
|
|
class DnsResolver : public SharedThis<DnsResolver>
|
|
{
|
|
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<u8, 4> *)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;
|
|
}
|
|
}
|
|
|
|
// BBA -> pico stack
|
|
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);
|
|
}
|
|
}
|
|
|
|
// pico stack -> BBA
|
|
static int send_eth_frame(pico_device *dev, void *data, int len)
|
|
{
|
|
dumpFrame((const u8 *)data, len);
|
|
if (len < 6 || len > 1514)
|
|
// discard
|
|
return len;
|
|
bbaBuffer.push(std::vector<u8>((const u8 *)data, (const u8 *)data + len));
|
|
return 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<asio::io_context>();
|
|
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<MiniUPnP> upnp;
|
|
bool usingPPP = false;
|
|
std::thread thread;
|
|
std::unique_ptr<asio::io_context> io_context;
|
|
};
|
|
|
|
void PicoThread::run()
|
|
{
|
|
ThreadName _("PicoTCP");
|
|
passiveMode = true;
|
|
// 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<void> pnpFuture;
|
|
if (ports != nullptr && config::EnableUPnP)
|
|
{
|
|
upnp = std::make_shared<MiniUPnP>();
|
|
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();
|
|
bbaBuffer.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<TcpAcceptor::Ptr> 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);
|
|
}
|
|
|
|
u32 PicoTcpService::sendEthFrame(u8 *frame)
|
|
{
|
|
if (bbaBuffer.empty())
|
|
return 0;
|
|
const std::vector<u8> v = bbaBuffer.pop();
|
|
memcpy(frame, v.data(), v.size());
|
|
return v.size();
|
|
}
|
|
|
|
}
|
|
|