/*
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);
}
}