1500 lines
39 KiB
C++
1500 lines
39 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 "hw/bba/bba.h"
|
|
|
|
#include <unordered_map>
|
|
#include <mutex>
|
|
#include <future>
|
|
|
|
#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 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<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());
|
|
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> 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);
|
|
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> 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;
|
|
}
|
|
}
|
|
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<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");
|
|
// 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();
|
|
|
|
// 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);
|
|
}
|
|
|
|
}
|
|
|