From 4d6168dd0b6c79cc485183484912853f81f4b1a5 Mon Sep 17 00:00:00 2001 From: Flyinghead Date: Tue, 1 Apr 2025 17:07:54 +0200 Subject: [PATCH] dcnet: find best access point --- core/network/dcnet.cpp | 270 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 251 insertions(+), 19 deletions(-) diff --git a/core/network/dcnet.cpp b/core/network/dcnet.cpp index f500413a2..6630121d8 100644 --- a/core/network/dcnet.cpp +++ b/core/network/dcnet.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #ifndef __ANDROID__ //#define WIRESHARK_DUMP 1 #endif @@ -58,14 +59,15 @@ template class PPPSocket { public: - PPPSocket(asio::io_context& io_context, const typename SocketT::endpoint_type& endpoint) + PPPSocket(asio::io_context& io_context, const typename SocketT::endpoint_type& 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); + os_notify("Connected to DCNet with modem", 5000, endpointName.c_str()); receive(); } @@ -185,14 +187,15 @@ using PPPTcpSocket = PPPSocket; class EthSocket { public: - EthSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint) + 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); + os_notify("Connected to DCNet with Ethernet", 5000, endpointName.c_str()); receive(); u8 prolog[] = { 'D', 'C', 'N', 'E', 'T', 1 }; send(prolog, sizeof(prolog)); @@ -356,6 +359,204 @@ private: 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] < (ssize_t)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: @@ -404,11 +605,15 @@ public: 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; friend DCNetService; }; static DCNetThread thread; @@ -472,32 +677,59 @@ void DCNetService::receiveEthFrame(u8 const *frame, unsigned int len) thread.sendEthFrame(frame, len); } -void DCNetThread::run() +void DCNetThread::connect(const asio::ip::address& address, const std::string& apname) { - try { - std::string port; - if (config::EmulateBBA) - port = "7655"; - else - port = "7654"; + 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 + 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()); - asio::ip::tcp::endpoint endpoint = *it.begin(); + if (it.empty()) + throw FlycastException("Host not found"); + endpoint = *it.begin(); + } + else { + endpoint.address(address); + endpoint.port(config::EmulateBBA ? TAP_PORT : PPP_PORT); + } + if (config::EmulateBBA) + ethSocket = 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); + }); - if (config::EmulateBBA) { - ethSocket = std::make_unique(*io_context, endpoint); - } - else { - toModem.clear(); - pppSocket = std::make_unique(*io_context, endpoint); - } io_context->run(); } catch (const FlycastException& e) { ERROR_LOG(NETWORK, "DCNet connection error: %s", e.what());