/*
Copyright 2025 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 .
*/
/* DCNet: the Dreamcast's line goes over the Internet to a relay of the
* community's servers, which answers the modem's PPP or the adapter's
* Ethernet itself, so that nothing of the network is emulated here.
*
* It runs on the network thread in place of picoTCP, with the same rings
* to the emulated modem and adapter, and waits the same way: in select(),
* on its sockets and on the emulation thread's wake socket. Its sockets
* do not block. */
#include "types.h"
#include "netthread.h"
#include "picoppp.h"
#include "reios/reios.h"
#include "stdclass.h"
#include
#include
#include
#include
#include
#if !defined(_MSC_VER) && !defined(TARGET_NO_THREADS)
static const char DCNET_HOST[] = "dcnet.flyca.st";
static const u16 PPP_PORT = 7654;
static const u16 TAP_PORT = 7655;
static const u16 POWER_SMASH_PPP_PORT = 7656;
// Access points: the main server lists them, and the one that answers fastest is used
static const u16 AP_PORT = 7655;
static const u8 AP_MAGIC[4] = { 0x01, 0xc0, 0x15, 0xdc }; // 0xDC15C001, as the other clients send it
enum { AP_PING = 1, AP_PONG = 2, AP_DISCOVER = 3 };
static const u32 MAX_ETH_FRAME_SIZE = 1514;
// What waits for the socket, or for the modem or the adapter, at most
static const size_t MAX_PENDING = 64 * 1024;
static u64 now_ms()
{
return (u64)cpu_features_get_time_usec() / 1000;
}
static void close_socket(sock_t& s)
{
if (VALID(s))
closesocket(s);
s = INVALID_SOCKET;
}
struct AccessPoint
{
u32 addr; // network order
std::string name;
int ping;
int count;
};
static void send_ping(sock_t s, u32 addr)
{
u8 buf[13];
sockaddr_in to;
const u64 now = now_ms();
memcpy(&buf[0], AP_MAGIC, 4);
buf[4] = AP_PING;
memcpy(&buf[5], &now, sizeof(now)); // given back as it is
memset(&to, 0, sizeof(to));
to.sin_family = AF_INET;
to.sin_port = htons(AP_PORT);
to.sin_addr.s_addr = addr;
sendto(s, (const char *)buf, sizeof(buf), L_MSG_NOSIGNAL, (const sockaddr *)&to, sizeof(to));
}
/* The address to connect to (network order): the access point that answers
* fastest, the main server if it lists none, 0 if there is no server. A
* second at most. */
static u32 find_access_point(std::string& apName)
{
const u32 mainAddr = netthread_resolve(DCNET_HOST);
if (mainAddr == 0)
{
WARN_LOG(MODEM, "DCNet: can't resolve %s", DCNET_HOST);
return 0;
}
sock_t s = selectable_socket(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
if (!VALID(s))
return mainAddr;
set_non_blocking(s);
u8 discover[5];
memcpy(discover, AP_MAGIC, 4);
discover[4] = AP_DISCOVER;
sockaddr_in to;
memset(&to, 0, sizeof(to));
to.sin_family = AF_INET;
to.sin_port = htons(AP_PORT);
to.sin_addr.s_addr = mainAddr;
sendto(s, (const char *)discover, sizeof(discover), L_MSG_NOSIGNAL, (const sockaddr *)&to, sizeof(to));
std::vector aps;
bool listed = false;
bool pingedAgain = false;
bool done = false;
const u64 start = now_ms();
while (!done && netthread_running())
{
const int kicks = netthread_kicks();
const u64 elapsed = now_ms() - start;
if (elapsed >= 1000)
break;
if (elapsed >= 500 && !pingedAgain)
{
// Again those that have not answered
pingedAgain = true;
for (const AccessPoint& ap : aps)
if (ap.count == 0)
send_ping(s, ap.addr);
}
u8 buf[512];
sockaddr_in from;
socklen_t fromLen = sizeof(from);
const int len = (int)recvfrom(s, (char *)buf, sizeof(buf), 0, (sockaddr *)&from, &fromLen);
if (len < 0)
{
fd_set readable, writable;
FD_ZERO(&readable);
FD_ZERO(&writable);
FD_SET(s, &readable);
const u64 phaseEnd = pingedAgain ? 1000 : 500;
netthread_wait(kicks, &readable, &writable, (int)s, (int)(phaseEnd - elapsed));
continue;
}
if (len < 5 || from.sin_port != htons(AP_PORT))
continue;
if (!listed && buf[4] == AP_DISCOVER && from.sin_addr.s_addr == mainAddr
&& memcmp(buf, AP_MAGIC, 4) == 0)
{
// address (4), name length (1), name: as many as the datagram holds
listed = true;
for (int p = 5; p + 5 <= len; )
{
AccessPoint ap;
memcpy(&ap.addr, &buf[p], 4);
const int l = buf[p + 4];
if (p + 5 + l > len)
break;
ap.name.assign((const char *)&buf[p + 5], l);
ap.ping = 0;
ap.count = 0;
aps.push_back(ap);
p += 5 + l;
}
if (aps.size() > 1)
{
for (const AccessPoint& ap : aps)
send_ping(s, ap.addr);
}
else
done = true;
}
else if (len == 13 && buf[4] == AP_PONG)
{
u64 ts;
memcpy(&ts, &buf[5], sizeof(ts));
const int ping = (int)(now_ms() - ts);
for (AccessPoint& ap : aps)
{
if (ap.addr != from.sin_addr.s_addr)
continue;
ap.ping += ping;
ap.count++;
if (ap.count < 5)
send_ping(s, ap.addr);
else
// five answers from one access point: enough
done = true;
break;
}
}
}
closesocket(s);
if (aps.empty())
return mainAddr;
const AccessPoint *best = nullptr;
int bestPing = 1000000;
for (const AccessPoint& ap : aps)
{
if (ap.count == 0) {
INFO_LOG(MODEM, "DCNet AP %s: no answer", ap.name.c_str());
continue;
}
const int ping = ap.ping / ap.count;
INFO_LOG(MODEM, "DCNet AP %s: ping %d ms (%d pings)", ap.name.c_str(), ping, ap.count);
if (ping < bestPing) {
bestPing = ping;
best = ≈
}
}
if (best == nullptr)
best = &aps[0];
apName = best->name;
return best->addr;
}
// A connection to @port of the access point, INVALID_SOCKET if none can be had
static sock_t dcnet_connect(u16 port)
{
std::string apName;
const u32 addr = find_access_point(apName);
if (addr == 0 || !netthread_running())
return INVALID_SOCKET;
sock_t s = selectable_socket(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
if (!VALID(s))
return INVALID_SOCKET;
set_non_blocking(s);
set_no_sigpipe(s);
sockaddr_in to;
memset(&to, 0, sizeof(to));
to.sin_family = AF_INET;
to.sin_port = htons(port);
to.sin_addr.s_addr = addr;
if (connect(s, (const sockaddr *)&to, sizeof(to)) < 0
&& get_last_error() != EINPROGRESS && get_last_error() != L_EWOULDBLOCK)
{
perror("DCNet connect");
closesocket(s);
return INVALID_SOCKET;
}
// Ten seconds at most, and no longer than the thread is wanted
const u64 start = now_ms();
while (true)
{
const int kicks = netthread_kicks();
if (!netthread_running() || now_ms() - start >= 10000)
{
if (netthread_running())
WARN_LOG(MODEM, "DCNet: no connection after 10 s");
closesocket(s);
return INVALID_SOCKET;
}
fd_set readable, writable, failed;
FD_ZERO(&readable);
FD_ZERO(&writable);
FD_ZERO(&failed);
FD_SET(s, &writable);
FD_SET(s, &failed);
timeval tv = { 0, 0 };
if (select((int)s + 1, NULL, &writable, &failed, &tv) > 0)
break;
FD_ZERO(&writable);
FD_SET(s, &writable);
netthread_wait(kicks, &readable, &writable, (int)s, 100);
}
#ifdef _WIN32
char value = 0;
#else
int value = 0;
#endif
socklen_t l = sizeof(value);
if (getsockopt(s, SOL_SOCKET, SO_ERROR, &value, &l) < 0 || value != 0)
{
WARN_LOG(MODEM, "DCNet: connection to %s refused", apName.empty() ? DCNET_HOST : apName.c_str());
closesocket(s);
return INVALID_SOCKET;
}
set_tcp_nodelay(s);
NOTICE_LOG(MODEM, "Connected to DCNet%s%s", apName.empty() ? "" : " via ", apName.c_str());
return s;
}
/* A DHCP release from the Dreamcast: the game is done with the network
* (the connection is closed; its next frame opens another) */
static bool is_dhcp_release(const u8 *f, u32 len)
{
if (len < 0x11d
|| f[0xc] != 0x08 || f[0xd] != 0x00 // type: IPv4
|| f[0x17] != 0x11 // UDP
|| ((f[0x22] << 8) | f[0x23]) != 68 // src port: dhcpc
|| ((f[0x24] << 8) | f[0x25]) != 67) // dest port: dhcps
return false;
// The options, after the magic cookie
for (u32 i = 0x11a; i < len && f[i] != 0xff; )
{
if (f[i] == 0)
{
// pad
i++;
continue;
}
if (i + 2 >= len)
break;
if (f[i] == 53 && f[i + 2] == 7) // message type: release
return true;
i += f[i + 1] + 2;
}
return false;
}
void dcnet_run(bool bba)
{
std::string gameId(ip_meta.product_number, sizeof(ip_meta.product_number));
gameId = trim_trailing_ws(gameId);
/* These games want their PPP frames each with a flag at both ends, which
* a server on its own port sees to */
const bool powerSmash = !bba
&& (gameId == "HDR-0113" // Power Smash
|| gameId == "HDR-0091" // Pro Yakyuu Team de Asobou Net!
|| gameId == "HDR-0040"); // Virtual-On Oratorio Tangram
const u16 port = bba ? TAP_PORT : powerSmash ? POWER_SMASH_PPP_PORT : PPP_PORT;
sock_t sock = INVALID_SOCKET;
// The modem's line is made once: when it is lost, it is lost until the game dials again
bool lineLost = false;
// The adapter's connection is opened by a frame of the game's, kept here meanwhile
static u8 heldFrame[NET_ETH_FRAME_MAX];
u32 heldSize = 0;
u64 lastAttempt = 0;
bool attempted = false;
std::vector toNet; // for the socket
std::vector fromNet; // from the socket: frames (adapter) or bytes to frame (Power Smash)
std::vector toModem; // for the modem, when its ring is full
size_t fromNetPos = 0;
while (netthread_running())
{
const int kicks = netthread_kicks();
if (!VALID(sock))
{
u8 drain[512];
if (!bba)
{
if (lineLost)
{
// What the modem sends goes nowhere
while (netthread_modem_take(drain, sizeof(drain)) != 0)
;
}
else
{
sock = dcnet_connect(port);
if (!VALID(sock))
{
lineLost = true;
continue;
}
}
}
else
{
if (heldSize == 0)
heldSize = netthread_eth_take(heldFrame);
if (heldSize != 0 && is_dhcp_release(heldFrame, heldSize))
heldSize = 0;
// One attempt each five seconds at most: the frames in between are lost
if (heldSize != 0 && attempted && now_ms() - lastAttempt < 5000)
heldSize = 0;
if (heldSize != 0)
{
attempted = true;
lastAttempt = now_ms();
sock = dcnet_connect(port);
if (VALID(sock))
{
static const u8 prolog[] = { 'D', 'C', 'N', 'E', 'T', 1 };
toNet.clear();
fromNet.clear();
fromNetPos = 0;
toNet.push_back(sizeof(prolog));
toNet.push_back(0);
toNet.insert(toNet.end(), prolog, prolog + sizeof(prolog));
if (heldSize <= MAX_ETH_FRAME_SIZE)
{
toNet.push_back((u8)heldSize);
toNet.push_back((u8)(heldSize >> 8));
toNet.insert(toNet.end(), heldFrame, heldFrame + heldSize);
}
}
heldSize = 0;
continue;
}
}
if (!VALID(sock))
{
fd_set readable, writable;
FD_ZERO(&readable);
FD_ZERO(&writable);
netthread_wait(kicks, &readable, &writable, -1, 1000);
continue;
}
}
// To the network
bool closing = false;
if (bba)
{
u8 frame[NET_ETH_FRAME_MAX];
while (toNet.size() + 2 + NET_ETH_FRAME_MAX <= MAX_PENDING)
{
const u32 size = netthread_eth_take(frame);
if (size == 0)
break;
if (size > MAX_ETH_FRAME_SIZE)
continue;
if (is_dhcp_release(frame, size))
{
INFO_LOG(MODEM, "DCNet: DHCP release");
closing = true;
break;
}
toNet.push_back((u8)size);
toNet.push_back((u8)(size >> 8));
toNet.insert(toNet.end(), frame, frame + size);
}
}
else
{
u8 buf[2048];
while (toNet.size() < MAX_PENDING)
{
const size_t n = netthread_modem_take(buf, std::min(sizeof(buf), MAX_PENDING - toNet.size()));
if (n == 0)
break;
toNet.insert(toNet.end(), buf, buf + n);
}
}
if (!toNet.empty())
{
const int r = (int)send(sock, (const char *)&toNet[0], toNet.size(), L_MSG_NOSIGNAL);
if (r < 0)
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
perror("DCNet send");
closing = true;
}
}
else
toNet.erase(toNet.begin(), toNet.begin() + r);
}
// From the network: what was received before first
if (!bba)
{
if (!toModem.empty())
{
const size_t n = netthread_modem_give(&toModem[0], toModem.size());
toModem.erase(toModem.begin(), toModem.begin() + n);
}
}
else
{
while (fromNet.size() - fromNetPos >= 2)
{
const u32 size = fromNet[fromNetPos] | (fromNet[fromNetPos + 1] << 8);
if (size > MAX_ETH_FRAME_SIZE)
{
ERROR_LOG(MODEM, "DCNet: invalid Ethernet frame size: %d", size);
closing = true;
break;
}
if (fromNet.size() - fromNetPos < 2 + size)
break;
if (!netthread_eth_give(&fromNet[fromNetPos + 2], size))
break;
fromNetPos += 2 + size;
}
if (fromNetPos != 0)
{
fromNet.erase(fromNet.begin(), fromNet.begin() + fromNetPos);
fromNetPos = 0;
}
}
// Room for more: everything received has gone, but a part of a frame
const bool room = !closing && toModem.empty()
&& (!bba || fromNet.size() < 2 + MAX_ETH_FRAME_SIZE);
if (room)
{
u8 buf[2048];
const int r = (int)recv(sock, (char *)buf, sizeof(buf), 0);
if (r == 0)
{
INFO_LOG(MODEM, "DCNet: connection closed by the server");
closing = true;
}
else if (r < 0)
{
if (get_last_error() != L_EAGAIN && get_last_error() != L_EWOULDBLOCK)
{
perror("DCNet recv");
closing = true;
}
}
else if (bba)
fromNet.insert(fromNet.end(), buf, buf + r);
else if (!powerSmash)
{
const size_t n = netthread_modem_give(buf, r);
toModem.insert(toModem.end(), buf + n, buf + r);
}
else
{
// Each frame ends with its flag: one is put before it too, if it has none
fromNet.insert(fromNet.end(), buf, buf + r);
size_t start = 0;
for (size_t i = 1; i < fromNet.size(); i++)
{
if (fromNet[i] != '~')
continue;
if (fromNet[start] != '~')
toModem.push_back('~');
toModem.insert(toModem.end(), fromNet.begin() + start, fromNet.begin() + i + 1);
start = i + 1;
i++;
}
fromNet.erase(fromNet.begin(), fromNet.begin() + start);
if (fromNet.size() >= 1542)
{
// no flag in sight: as it is
toModem.insert(toModem.end(), fromNet.begin(), fromNet.end());
fromNet.clear();
}
const size_t n = toModem.empty() ? 0 : netthread_modem_give(&toModem[0], toModem.size());
toModem.erase(toModem.begin(), toModem.begin() + n);
}
if (r > 0)
// there may be more
continue;
}
if (closing)
{
close_socket(sock);
toNet.clear();
fromNet.clear();
fromNetPos = 0;
toModem.clear();
if (!bba)
lineLost = true;
continue;
}
fd_set readable, writable;
FD_ZERO(&readable);
FD_ZERO(&writable);
if (room)
FD_SET(sock, &readable);
if (!toNet.empty())
FD_SET(sock, &writable);
netthread_wait(kicks, &readable, &writable, (int)sock, 1000);
}
close_socket(sock);
}
#endif