/* 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 . */ #include "types.h" #include #include "netservice.h" #include "util/tsqueue.h" #include "oslib/oslib.h" #include "emulator.h" #include "hw/bba/bba.h" #include "cfg/option.h" #include "stdclass.h" #ifndef LIBRETRO #include "cfg/cfg.h" #endif #include #include #include #include #ifndef __ANDROID__ //#define WIRESHARK_DUMP 1 #endif namespace net::modbba { static TsQueue toModem; class DCNetService : public Service { public: bool start() override; void stop() override; void writeModem(u8 b) override; int readModem() override; int modemAvailable() override; void receiveEthFrame(const u8 *frame, u32 size) override; }; class PPPSocket { public: PPPSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint, const std::string& endpointName = "") : socket(io_context) { asio::error_code ec; socket.connect(endpoint, ec); if (ec) throw FlycastException(ec.message().c_str()); os_notify("Connected to DCNet with modem", 5000, endpointName.c_str()); receive(); } virtual ~PPPSocket() { if (dumpfp != nullptr) fclose(dumpfp); } void send(u8 b) { if (sendBufSize == sendBuffer.size()) { WARN_LOG(NETWORK, "PPP output buffer overflow"); return; } sendBuffer[sendBufSize++] = b; doSend(); } protected: virtual void receive() { socket.async_read_some(asio::buffer(recvBuffer), [this](const std::error_code& ec, size_t len) { if (ec || len == 0) { if (ec) ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str()); close(); return; } pppdump(recvBuffer.data(), len, false); for (size_t i = 0; i < len; i++) toModem.push(recvBuffer[i]); receive(); }); } void doSend() { if (sending) return; pppdump(sendBuffer.data(), sendBufSize, true); sending = true; asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize), [this](const std::error_code& ec, size_t len) { if (ec) { ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str()); close(); return; } sending = false; sendBufSize -= len; if (sendBufSize > 0) { memmove(&sendBuffer[0], &sendBuffer[len], sendBufSize); doSend(); } }); } void close() { std::error_code ignored; socket.close(ignored); } void pppdump(uint8_t *buf, int len, bool egress) { #ifdef WIRESHARK_DUMP if (!len) return; if (dumpfp == nullptr) { dumpfp = fopen("ppp.dump", "a"); if (dumpfp == nullptr) return; time_t now; time(&now); u32 reset_time = ntohl((u32)now); fputc(7, dumpfp); // Reset time fwrite(&reset_time, sizeof(reset_time), 1, dumpfp); dump_last_time_ms = getTimeMs(); } u32 delta = getTimeMs() / 100 - dump_last_time_ms / 100; if (delta < 256) { fputc(6, dumpfp); // Time step (short) fwrite(&delta, 1, 1, dumpfp); } else { delta = ntohl(delta); fputc(5, dumpfp); // Time step (long) fwrite(&delta, sizeof(delta), 1, dumpfp); } dump_last_time_ms = getTimeMs(); fputc(egress ? 1 : 2, dumpfp); // Sent/received data uint16_t slen = htons(len); fwrite(&slen, 2, 1, dumpfp); fwrite(buf, 1, len, dumpfp); #endif } asio::ip::tcp::socket socket; std::array recvBuffer; std::array sendBuffer; u32 sendBufSize = 0; bool sending = false; FILE *dumpfp = nullptr; u64 dump_last_time_ms; }; class PowerSmashPPPSocket : public PPPSocket { public: PowerSmashPPPSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint, const std::string& endpointName = "") : PPPSocket(io_context, endpoint, endpointName) {} private: void receive() override { socket.async_read_some(asio::buffer(&recvBuffer[recvBufSize], recvBuffer.size() - recvBufSize), [this](const std::error_code& ec, size_t len) { if (ec || len == 0) { if (ec) ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str()); close(); return; } recvBufSize += len; while (recvBufSize != 0) { u32 frameSize = 0; for (u32 i = 1; i < recvBufSize; i++) { if (recvBuffer[i] == '~') { frameSize = i + 1; break; } } if (frameSize == 0) break; pppdump(recvBuffer.data(), frameSize, false); // Power Smash requires both start and end Flag Sequences if (recvBuffer[0] != '~') toModem.push('~'); for (size_t i = 0; i < frameSize; i++) toModem.push(recvBuffer[i]); recvBufSize -= frameSize; if (recvBufSize != 0) memmove(&recvBuffer[0], &recvBuffer[frameSize], recvBufSize); } receive(); }); } u32 recvBufSize = 0; }; class EthSocket { public: EthSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint, const std::string& endpointName = "") : socket(io_context) { asio::error_code ec; socket.connect(endpoint, ec); if (ec) throw FlycastException(ec.message().c_str()); os_notify("Connected to DCNet with Ethernet", 5000, endpointName.c_str()); receive(); u8 prolog[] = { 'D', 'C', 'N', 'E', 'T', 1 }; send(prolog, sizeof(prolog)); } ~EthSocket() { if (dumpfp != nullptr) fclose(dumpfp); } void send(const u8 *frame, u32 size) { if (sendBufferIdx + size >= sendBuffer.size()) { WARN_LOG(NETWORK, "Dropped out frame (buffer:%d + %d bytes). Increase send buffer size\n", sendBufferIdx, size); return; } if (size >= 32) // skip prolog ethdump(frame, size); *(u16 *)&sendBuffer[sendBufferIdx] = size; sendBufferIdx += 2; memcpy(&sendBuffer[sendBufferIdx], frame, size); sendBufferIdx += size; doSend(); } private: using iterator = asio::buffers_iterator; std::pair static packetMatcher(iterator begin, iterator end) { if (end - begin < 3) return std::make_pair(begin, false); iterator i = begin; uint16_t len = (uint8_t)*i++; len |= uint8_t(*i++) << 8; len += 2; if (end - begin < len) return std::make_pair(begin, false); return std::make_pair(begin + len, true); } void receive() { asio::async_read_until(socket, asio::dynamic_vector_buffer(recvBuffer), packetMatcher, [this](const std::error_code& ec, size_t len) { if (ec || len == 0) { if (ec) ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str()); std::error_code ignored; socket.close(ignored); return; } /* verify(len - 2 == *(u16 *)&recvBuffer[0]); printf("In frame: dest %02x:%02x:%02x:%02x:%02x:%02x " "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes\n", recvBuffer[2], recvBuffer[3], recvBuffer[4], recvBuffer[5], recvBuffer[6], recvBuffer[7], recvBuffer[8], recvBuffer[9], recvBuffer[10], recvBuffer[11], recvBuffer[12], recvBuffer[13], *(u16 *)&recvBuffer[14], (int)len - 2); */ ethdump(&recvBuffer[2], len - 2); bba_recv_frame(&recvBuffer[2], len - 2); if (len < recvBuffer.size()) recvBuffer.erase(recvBuffer.begin(), recvBuffer.begin() + len); else recvBuffer.clear(); receive(); }); } void doSend() { if (sending) return; sending = true; asio::async_write(socket, asio::buffer(sendBuffer, sendBufferIdx), [this](const std::error_code& ec, size_t len) { sending = false; if (ec) { ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str()); std::error_code ignored; socket.close(ignored); return; } sendBufferIdx -= len; if (sendBufferIdx != 0) { memmove(sendBuffer.data(), sendBuffer.data() + len, sendBufferIdx); doSend(); } }); } void ethdump(const uint8_t *frame, int size) { #ifdef WIRESHARK_DUMP if (dumpfp == nullptr) { dumpfp = fopen("bba.pcapng", "wb"); if (dumpfp == nullptr) { const char *home = getenv("HOME"); if (home != nullptr) { std::string path = home + std::string("/bba.pcapng"); dumpfp = fopen(path.c_str(), "wb"); } if (dumpfp == nullptr) return; } u32 blockType = 0x0A0D0D0A; // Section Header Block fwrite(&blockType, sizeof(blockType), 1, dumpfp); u32 blockLen = 28; fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); u32 magic = 0x1A2B3C4D; fwrite(&magic, sizeof(magic), 1, dumpfp); u32 version = 1; // 1.0 fwrite(&version, sizeof(version), 1, dumpfp); u64 sectionLength = ~0; // unspecified fwrite(§ionLength, sizeof(sectionLength), 1, dumpfp); fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); blockType = 1; // Interface Description Block fwrite(&blockType, sizeof(blockType), 1, dumpfp); blockLen = 20; fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); const u32 linkType = 1; // Ethernet fwrite(&linkType, sizeof(linkType), 1, dumpfp); const u32 snapLen = 0; // no limit fwrite(&snapLen, sizeof(snapLen), 1, dumpfp); // TODO options? if name, ip/mac address fwrite(&blockLen, sizeof(blockLen), 1, dumpfp); } const u32 blockType = 6; // Extended Packet Block fwrite(&blockType, sizeof(blockType), 1, dumpfp); u32 roundedSize = ((size + 3) & ~3) + 32; fwrite(&roundedSize, sizeof(roundedSize), 1, dumpfp); u32 ifId = 0; fwrite(&ifId, sizeof(ifId), 1, dumpfp); u64 now = getTimeMs() * 1000; fwrite((u32 *)&now + 1, 4, 1, dumpfp); fwrite(&now, 4, 1, dumpfp); fwrite(&size, sizeof(size), 1, dumpfp); fwrite(&size, sizeof(size), 1, dumpfp); fwrite(frame, 1, size, dumpfp); fwrite(frame, 1, roundedSize - size - 32, dumpfp); fwrite(&roundedSize, sizeof(roundedSize), 1, dumpfp); #endif } asio::ip::tcp::socket socket; std::vector recvBuffer; std::array sendBuffer; u32 sendBufferIdx = 0; bool sending = false; FILE *dumpfp = nullptr; }; class AccessPointFinder { public: AccessPointFinder(asio::io_context& io_context) : io_context(io_context), socket(io_context, asio::ip::udp::endpoint()), timer(io_context) { } template void find(const Handler& handler) { this->handler = std::function(handler); try { asio::ip::udp::resolver resolver(io_context); auto it = resolver.resolve("dcnet.flyca.st", std::to_string(PORT)); if (it.empty()) { finish(); return; } mainEndpoint = *it.begin(); std::array buf; memcpy(&buf[0], &MAGIC, sizeof(MAGIC)); buf[4] = DISCOVER; // discover access points socket.send_to(asio::buffer(buf), mainEndpoint); timer.expires_after(asio::chrono::milliseconds(500)); timer.async_wait([this](const std::error_code& ec) { if (ec) return; // Re-ping access points that didn't answer after 500 ms for (const auto& ap : accessPoints) { if (ap.count == 0) sendPing(ap.endpoint); } timer.expires_after(asio::chrono::milliseconds(500)); timer.async_wait([this](const std::error_code& ec) { // 1 sec final timeout if (ec) return; std::error_code err; socket.close(err); finish(); }); }); receiveAccessPoints(); } catch (const std::system_error& e) { finish(e.code()); } } private: void receiveAccessPoints() { socket.async_receive_from(asio::buffer(recvbuf), recvEndpoint, [this](const std::error_code& ec, size_t len) { if (recvEndpoint != mainEndpoint || len < 5 || memcmp(&recvbuf[0], &MAGIC, sizeof(MAGIC)) || recvbuf[4] != DISCOVER) { // Unexpected or invalid packet receiveAccessPoints(); return; } const uint8_t *p = &recvbuf[5]; while (p - &recvbuf[0] < (int)len) { accessPoints.emplace_back(); uint32_t addr; memcpy(&addr, p, sizeof(uint32_t)); accessPoints.back().endpoint = asio::ip::udp::endpoint(asio::ip::address_v4(htonl(addr)), PORT); p += 4; size_t l = *p++; accessPoints.back().name = std::string((const char *)p, (const char *)(p + l)); p += l; } if (accessPoints.size() > 1) { // Need to ping for (const auto& ap : accessPoints) sendPing(ap.endpoint); } else { finish(); } }); } void sendPing(const asio::ip::udp::endpoint& endpoint) { std::array buf; memcpy(&buf[0], &MAGIC, sizeof(MAGIC)); buf[4] = PING; u64 now = (u64)getTimeMs(); memcpy(&buf[5], &now, sizeof(u64)); socket.send_to(asio::buffer(buf), endpoint); receivePing(); } void receivePing() { if (receiving) return; receiving = true; socket.async_receive_from(asio::buffer(recvbuf), recvEndpoint, [this](const std::error_code& ec, size_t len) { receiving = false; if (ec) { if (ec != asio::error::operation_aborted && ec != asio::error::bad_descriptor) INFO_LOG(NETWORK, "receivePing error: %s", ec.message().c_str()); return; } if (len != 13 || recvbuf[4] != PONG) { receivePing(); return; } u64 ts; memcpy(&ts, &recvbuf[5], sizeof(ts)); int ping = getTimeMs() - (time_t)ts; for (auto& ap : accessPoints) { if (ap.endpoint == recvEndpoint) { ap.ping += ping; ap.count++; if (ap.count < 3) sendPing(ap.endpoint); else // we have 3 answers from one AP so let's stop here finish(); return; } } receivePing(); }); } void finish(const std::error_code& ec = {}) { std::error_code e; socket.close(e); timer.cancel(e); if (ec) { handler(ec, {}, {}); } else if (accessPoints.empty()) { handler({}, mainEndpoint.address(), {}); } else { int bestPing = 1000000; const AccessPoint *bestAP = nullptr; for (const AccessPoint& ap : accessPoints) { if (ap.count == 0) { INFO_LOG(NETWORK, "AP %s (%s): no answer", ap.name.c_str(), ap.endpoint.address().to_string().c_str()); continue; } const int ping = ap.ping / ap.count; INFO_LOG(NETWORK, "AP %s (%s): ping %d ms", ap.name.c_str(), ap.endpoint.address().to_string().c_str(), ping); if (ping < bestPing) { bestPing = ping; bestAP = ≈ } } if (bestAP == nullptr) bestAP = &accessPoints[0]; handler({}, bestAP->endpoint.address(), bestAP->name); } } struct AccessPoint { asio::ip::udp::endpoint endpoint; std::string name; int ping = 0; int count = 0; }; asio::io_context& io_context; asio::ip::udp::socket socket; std::array recvbuf; asio::ip::udp::endpoint recvEndpoint; asio::ip::udp::endpoint mainEndpoint; std::vector accessPoints; bool receiving = false; asio::steady_timer timer; std::function handler; static constexpr uint16_t PORT = 7655; static constexpr uint32_t MAGIC = 0xDC15C001; static constexpr uint8_t PING = 1; static constexpr uint8_t PONG = 2; static constexpr uint8_t DISCOVER = 3; }; class DCNetThread { public: void start() { if (thread.joinable()) return; io_context = std::make_unique(); thread = std::thread(&DCNetThread::run, this); } void stop() { if (!thread.joinable()) return; io_context->stop(); thread.join(); pppSocket.reset(); ethSocket.reset(); io_context.reset(); os_notify("DCNet disconnected", 3000); } void sendModem(u8 v) { if (io_context == nullptr || pppSocket == nullptr) return; io_context->post([this, v]() { pppSocket->send(v); }); } void sendEthFrame(const u8 *frame, u32 len) { if (io_context != nullptr && ethSocket != nullptr) { std::vector vbuf(frame, frame + len); io_context->post([this, vbuf]() { ethSocket->send(vbuf.data(), vbuf.size()); }); } else { // restart the thread if previously stopped start(); } } private: void run(); void connect(const asio::ip::address& address = {}, const std::string& apname = {}); std::thread thread; std::unique_ptr io_context; std::unique_ptr pppSocket; std::unique_ptr ethSocket; static constexpr uint16_t PPP_PORT = 7654; static constexpr uint16_t TAP_PORT = 7655; static constexpr uint16_t POWER_SMASH_PPP_PORT = 7656; friend DCNetService; }; static DCNetThread thread; bool DCNetService::start() { emu.setNetworkState(true); thread.start(); return true; } void DCNetService::stop() { thread.stop(); emu.setNetworkState(false); } void DCNetService::writeModem(u8 b) { thread.sendModem(b); } int DCNetService::readModem() { if (toModem.empty()) return -1; else return toModem.pop(); } int DCNetService::modemAvailable() { return toModem.size(); } void DCNetService::receiveEthFrame(u8 const *frame, unsigned int len) { /* printf("Out frame: dest %02x:%02x:%02x:%02x:%02x:%02x " "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes %s\n", frame[0], frame[1], frame[2], frame[3], frame[4], frame[5], frame[6], frame[7], frame[8], frame[9], frame[10], frame[11], *(u16 *)&frame[12], len, EthSocket::Instance == nullptr ? "LOST" : ""); */ // Stop DCNet on DHCP Release if (len >= 0x11d && *(u16 *)&frame[0xc] == 0x0008 // type: IPv4 && frame[0x17] == 0x11 // UDP && ntohs(*(u16 *)&frame[0x22]) == 68 // src port: dhcpc && ntohs(*(u16 *)&frame[0x24]) == 67) // dest port: dhcps { const u8 *options = &frame[0x11a]; while (options - frame < len && *options != 0xff) { if (*options == 53 // message type && options[2] == 7) // release { stop(); return; } options += options[1] + 2; } } thread.sendEthFrame(frame, len); } void DCNetThread::connect(const asio::ip::address& address, const std::string& apname) { const bool powerSmash = settings.content.gameId == "HDR-0113" // Power Smash || settings.content.gameId == "HDR-0091"; // Pro Yakyuu Team de Asobou Net! asio::ip::tcp::endpoint endpoint; if (address.is_unspecified()) { std::string hostname = "dcnet.flyca.st"; #ifndef LIBRETRO hostname = cfgLoadStr("network", "DCNetServer", hostname); #endif std::string port; if (config::EmulateBBA) port = std::to_string(TAP_PORT); else if (powerSmash) port = std::to_string(POWER_SMASH_PPP_PORT); else port = std::to_string(PPP_PORT); asio::ip::tcp::resolver resolver(*io_context); asio::error_code ec; auto it = resolver.resolve(hostname, port, ec); if (ec) throw FlycastException(ec.message()); if (it.empty()) throw FlycastException("Host not found"); endpoint = *it.begin(); } else { endpoint.address(address); if (config::EmulateBBA) endpoint.port(TAP_PORT); else if (powerSmash) endpoint.port(POWER_SMASH_PPP_PORT); else endpoint.port(PPP_PORT); } if (config::EmulateBBA) ethSocket = std::make_unique(*io_context, endpoint, apname); else if (powerSmash) pppSocket = std::make_unique(*io_context, endpoint, apname); else pppSocket = std::make_unique(*io_context, endpoint, apname); } void DCNetThread::run() { toModem.clear(); try { std::string hostname; #ifndef LIBRETRO hostname = cfgLoadStr("network", "DCNetServer", ""); if (!hostname.empty()) connect(); #endif AccessPointFinder finder(*io_context); if (hostname.empty()) finder.find([this](const std::error_code& ec, const asio::ip::address& address, const std::string& apname) { if (ec) WARN_LOG(NETWORK, "AP discovery failed: %s", ec.message().c_str()); this->connect(address, apname); }); io_context->run(); } catch (const FlycastException& e) { ERROR_LOG(NETWORK, "DCNet connection error: %s", e.what()); os_notify("Can't connect to DCNet", 8000, e.what()); } catch (const std::runtime_error& e) { ERROR_LOG(NETWORK, "DCNetThread::run error: %s", e.what()); } } }