#include "stdafx.h" #ifdef _WINDOWS64 // Winsock2 must come after stdafx.h (which defines WIN32_LEAN_AND_MEAN before // windows.h, so the old winsock.h v1 is never pulled in — no conflict). #include #include // UPnP IGD support via Windows native COM interfaces (no third-party lib needed). // natupnp.h is in the Windows 8.1+ SDK; comdef.h provides _bstr_t/_variant_t helpers. #include #include #include "Windows64_LceLiveP2P.h" #include "Windows64_Log.h" #include "Network/WinsockNetLayer.h" #include #include #include #include #pragma comment(lib, "Ws2_32.lib") #pragma comment(lib, "Ole32.lib") #pragma comment(lib, "OleAut32.lib") // ============================================================================ // Internal implementation // ============================================================================ namespace { // ------------------------------------------------------------------------- // STUN constants (RFC 5389) // ------------------------------------------------------------------------- static const unsigned long STUN_MAGIC_COOKIE = 0x2112A442UL; static const unsigned short STUN_BINDING_REQUEST = 0x0001; static const unsigned short STUN_BINDING_RESPONSE = 0x0101; static const unsigned short STUN_ATTR_XOR_MAPPED_ADDR = 0x0020; static const unsigned short STUN_ATTR_MAPPED_ADDR = 0x0001; static const char* STUN_HOST_PRIMARY = "stun.l.google.com"; static const char* STUN_HOST_FALLBACK = "stun1.l.google.com"; static const int STUN_PORT = 19302; static const DWORD STUN_TIMEOUT_MS = 5000; // Re-send a keepalive STUN binding request every N ms to keep the NAT // mapping alive. static const ULONGLONG KEEPALIVE_INTERVAL_MS = 20000ULL; // Bring the public EConnMethod into this anonymous namespace for convenience. using EConnMethod = Win64LceLiveP2P::EConnMethod; // ------------------------------------------------------------------------- // STUN message helpers // ------------------------------------------------------------------------- struct StunRequest { unsigned char bytes[20]; // Full 20-byte binding request unsigned char txId[12]; // Transaction ID (last 12 bytes) }; struct StunResult { bool success; std::string externalIp; unsigned short externalPort; std::string errorMessage; }; // Build a 20-byte STUN Binding Request with a fresh random transaction ID. StunRequest BuildStunRequest() { StunRequest req = {}; for (int i = 0; i < 12; ++i) req.txId[i] = static_cast(rand() & 0xFF); unsigned char* p = req.bytes; // Message Type: Binding Request (big-endian) p[0] = 0x00; p[1] = 0x01; // Message Length: 0 (no attributes in a request) p[2] = 0x00; p[3] = 0x00; // Magic Cookie: 0x2112A442 p[4] = 0x21; p[5] = 0x12; p[6] = 0xA4; p[7] = 0x42; // Transaction ID memcpy(p + 8, req.txId, 12); return req; } // Parse a STUN Binding Success Response. // Looks for XOR-MAPPED-ADDRESS first, falls back to MAPPED-ADDRESS. // Returns true and fills outIp/outPort on success. bool ParseStunResponse( const unsigned char* data, int length, std::string* outIp, unsigned short* outPort) { if (length < 20) return false; // Magic cookie check if (data[4] != 0x21 || data[5] != 0x12 || data[6] != 0xA4 || data[7] != 0x42) return false; // Must be a Binding Success Response const unsigned short msgType = (static_cast(data[0]) << 8) | data[1]; if (msgType != STUN_BINDING_RESPONSE) return false; const unsigned short msgLen = (static_cast(data[2]) << 8) | data[3]; if (static_cast(msgLen) + 20 > length) return false; // Walk the attribute list int offset = 20; const int end = 20 + static_cast(msgLen); while (offset + 4 <= end) { const unsigned short attrType = (static_cast(data[offset]) << 8) | data[offset + 1]; const unsigned short attrLen = (static_cast(data[offset + 2]) << 8) | data[offset + 3]; const int valueOffset = offset + 4; const int attrEnd = valueOffset + static_cast(attrLen); // Bounds check before reading the attribute value if (attrEnd > length) break; if ((attrType == STUN_ATTR_XOR_MAPPED_ADDR || attrType == STUN_ATTR_MAPPED_ADDR) && attrLen >= 8) { const unsigned char family = data[valueOffset + 1]; if (family != 0x01) // IPv4 only { // Skip — IPv6 not supported here } else if (attrType == STUN_ATTR_XOR_MAPPED_ADDR) { // X-Port = port XOR (magic_cookie >> 16) // X-Address = address XOR magic_cookie // Both the packet value and the mask are in network byte order, // so we can XOR the big-endian values read directly from the bytes. const unsigned short xport = (static_cast(data[valueOffset + 2]) << 8) | data[valueOffset + 3]; const unsigned short port = xport ^ static_cast(STUN_MAGIC_COOKIE >> 16); const unsigned long xaddr = (static_cast(data[valueOffset + 4]) << 24) | (static_cast(data[valueOffset + 5]) << 16) | (static_cast(data[valueOffset + 6]) << 8) | data[valueOffset + 7]; const unsigned long addr = xaddr ^ STUN_MAGIC_COOKIE; // addr is in host byte order; convert to network for inet_ntop. const unsigned long addrNet = htonl(addr); char ipStr[INET_ADDRSTRLEN] = {}; inet_ntop(AF_INET, &addrNet, ipStr, sizeof(ipStr)); *outIp = ipStr; *outPort = port; return true; } else // STUN_ATTR_MAPPED_ADDR (no XOR) { const unsigned short port = (static_cast(data[valueOffset + 2]) << 8) | data[valueOffset + 3]; const unsigned long addr = (static_cast(data[valueOffset + 4]) << 24) | (static_cast(data[valueOffset + 5]) << 16) | (static_cast(data[valueOffset + 6]) << 8) | data[valueOffset + 7]; const unsigned long addrNet = htonl(addr); char ipStr[INET_ADDRSTRLEN] = {}; inet_ntop(AF_INET, &addrNet, ipStr, sizeof(ipStr)); *outIp = ipStr; *outPort = port; return true; } } // Advance past this attribute (padded to 4-byte boundary) const int padded = (static_cast(attrLen) + 3) & ~3; offset += 4 + padded; } return false; } // ------------------------------------------------------------------------- // IPv4 routability classification // ------------------------------------------------------------------------- // Returns true iff 'ip' is a globally-routable IPv4 address. // Bit-mask comparisons on the 32-bit host-order value; no string scanning. static bool IsPublicRoutableIPv4Impl(const std::string& ip) { if (ip.empty()) return false; struct in_addr addr = {}; if (inet_pton(AF_INET, ip.c_str(), &addr) != 1) return false; // Convert to host byte order for straight numeric range tests. const unsigned long h = ntohl(addr.s_addr); if ((h & 0xFF000000u) == 0x00000000u) return false; // 0.0.0.0/8 "this" network if ((h & 0xFF000000u) == 0x0A000000u) return false; // 10.0.0.0/8 RFC 1918 if ((h & 0xFFC00000u) == 0x64400000u) return false; // 100.64.0.0/10 RFC 6598 CGNAT if ((h & 0xFF000000u) == 0x7F000000u) return false; // 127.0.0.0/8 loopback if ((h & 0xFFFF0000u) == 0xA9FE0000u) return false; // 169.254.0.0/16 link-local if ((h & 0xFFF00000u) == 0xAC100000u) return false; // 172.16.0.0/12 RFC 1918 if ((h & 0xFFFF0000u) == 0xC0A80000u) return false; // 192.168.0.0/16 RFC 1918 if ((h & 0xFFFE0000u) == 0xC6120000u) return false; // 198.18.0.0/15 benchmarking if ((h & 0xF0000000u) == 0xE0000000u) return false; // 224.0.0.0/4 multicast if ((h & 0xF0000000u) == 0xF0000000u) return false; // 240.0.0.0/4 reserved return true; } // ------------------------------------------------------------------------- // UPnP IGD helpers // ------------------------------------------------------------------------- struct UPnPResult { bool success; std::string externalIp; int externalPort; // same as localPort we passed in int localPort; // what we registered std::string error; }; // Attempts to add a UPnP port mapping (UDP or TCP) via the Windows IUPnPNAT // COM interface. Handles its own CoInitializeEx/CoUninitialize. // Blocks for up to a few seconds while UPnP discovery runs. // Set tcp=false for UDP (P2P socket), tcp=true for TCP (game server port). UPnPResult TryUPnPMapping(int localPort, bool tcp) { UPnPResult result = {}; result.localPort = localPort; // COM must be initialised on this thread (STA for UPnP callbacks). HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); const bool coInitialized = SUCCEEDED(hr) || (hr == RPC_E_CHANGED_MODE); IUPnPNAT* pNAT = nullptr; hr = CoCreateInstance(__uuidof(UPnPNAT), nullptr, CLSCTX_ALL, __uuidof(IUPnPNAT), reinterpret_cast(&pNAT)); if (FAILED(hr)) { result.error = "UPnP: CoCreateInstance(UPnPNAT) failed hr=0x" + std::to_string(static_cast(hr)); if (coInitialized) CoUninitialize(); return result; } // get_StaticPortMappingCollection blocks until UPnP discovery completes // (~1-3 s on a cooperative router, full timeout if no IGD present). IStaticPortMappingCollection* pMappings = nullptr; hr = pNAT->get_StaticPortMappingCollection(&pMappings); pNAT->Release(); if (FAILED(hr) || pMappings == nullptr) { result.error = "UPnP: no IGD found (router may not support UPnP)"; if (coInitialized) CoUninitialize(); return result; } // Determine our local IP address (pick first IPv4 from hostname resolution). char localIp[INET_ADDRSTRLEN] = {}; { char hostname[256] = {}; gethostname(hostname, sizeof(hostname)); addrinfo hints = {}; hints.ai_family = AF_INET; addrinfo* info = nullptr; if (getaddrinfo(hostname, nullptr, &hints, &info) == 0 && info != nullptr) { inet_ntop(AF_INET, &reinterpret_cast(info->ai_addr)->sin_addr, localIp, sizeof(localIp)); freeaddrinfo(info); } } if (localIp[0] == '\0') { pMappings->Release(); result.error = "UPnP: could not determine local IPv4 address"; if (coInitialized) CoUninitialize(); return result; } const wchar_t* protocol = tcp ? L"TCP" : L"UDP"; const wchar_t* description = tcp ? L"LceLive Game" : L"LceLive P2P"; // Remove any stale LceLive mapping on this port (best-effort). pMappings->Remove(localPort, _bstr_t(protocol)); // Convert local IP to wide for the COM API. wchar_t localIpW[INET_ADDRSTRLEN] = {}; MultiByteToWideChar(CP_ACP, 0, localIp, -1, localIpW, INET_ADDRSTRLEN); IStaticPortMapping* pMapping = nullptr; hr = pMappings->Add( localPort, // external port _bstr_t(protocol), // protocol localPort, // internal port _bstr_t(localIpW), // internal client (our LAN IP) VARIANT_TRUE, // enabled _bstr_t(description), // description &pMapping ); pMappings->Release(); if (FAILED(hr) || pMapping == nullptr) { result.error = "UPnP: AddPortMapping failed hr=0x" + std::to_string(static_cast(hr)); if (coInitialized) CoUninitialize(); return result; } // Read back the external IP the router assigned. BSTR extIpBstr = nullptr; pMapping->get_ExternalIPAddress(&extIpBstr); if (extIpBstr != nullptr) { char buf[64] = {}; WideCharToMultiByte(CP_UTF8, 0, extIpBstr, -1, buf, sizeof(buf), nullptr, nullptr); result.externalIp = buf; SysFreeString(extIpBstr); } pMapping->Release(); if (coInitialized) CoUninitialize(); // Sanity-check: router must return a publicly routable external IP. // IsPublicRoutableIPv4Impl rejects 0.0.0.0, all RFC 1918 private ranges, // 100.64.0.0/10 CGNAT, loopback, link-local, multicast, and reserved blocks. if (!IsPublicRoutableIPv4Impl(result.externalIp)) { // Identify CGNAT specifically — it is the most common surprise failure // (carrier assigns a 100.64/10 address; UPnP succeeds but the mapped // port is invisible to the public internet). struct in_addr cgnatCheck = {}; const bool isCgnat = !result.externalIp.empty() && inet_pton(AF_INET, result.externalIp.c_str(), &cgnatCheck) == 1 && (ntohl(cgnatCheck.s_addr) & 0xFFC00000u) == 0x64400000u; result.error = isCgnat ? "UPnP: host is behind CGNAT (" + result.externalIp + " in 100.64.0.0/10) — not publicly routable; relay will be used" : "UPnP: router returned non-routable external IP (" + result.externalIp + ")"; return result; } result.success = true; result.externalPort = localPort; return result; } // Remove a previously added UPnP port mapping. // Call from HostClose() on any thread (handles its own CoInit/Uninit). // tcp=false removes a UDP mapping, tcp=true removes a TCP mapping. void RemoveUPnPMapping(int port, bool tcp) { HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); const bool coInitialized = SUCCEEDED(hr) || (hr == RPC_E_CHANGED_MODE); IUPnPNAT* pNAT = nullptr; hr = CoCreateInstance(__uuidof(UPnPNAT), nullptr, CLSCTX_ALL, __uuidof(IUPnPNAT), reinterpret_cast(&pNAT)); if (SUCCEEDED(hr) && pNAT != nullptr) { IStaticPortMappingCollection* pMappings = nullptr; if (SUCCEEDED(pNAT->get_StaticPortMappingCollection(&pMappings)) && pMappings != nullptr) { pMappings->Remove(port, _bstr_t(tcp ? L"TCP" : L"UDP")); pMappings->Release(); } pNAT->Release(); } if (coInitialized) CoUninitialize(); } // ------------------------------------------------------------------------- // Runtime state // ------------------------------------------------------------------------- struct WorkerResult { bool success; EConnMethod method; std::string externalIp; int externalPort; std::string errorMessage; }; struct P2PState { bool initialized; CRITICAL_SECTION lock; Win64LceLiveP2P::EP2PState state; EConnMethod connMethod; // set once worker succeeds // The long-lived UDP socket. INVALID_SOCKET when not open. // Owned by the main thread once discovery completes. // During discovery, owned by StunWorkerProc. SOCKET udpSocket; int localPort; // Local port we bound (host order) // UPnP: we keep track of mapped ports so HostClose can remove them. bool upnpMappingActive; // UDP P2P port int upnpMappedPort; bool tcpUpnpMappingActive; // TCP game port int tcpUpnpMappedPort; // Resolved STUN server address — cached to avoid re-DNS on keepalives. sockaddr_in stunServerAddr; bool stunServerAddrValid; // Discovery results (written by worker, read by main thread after join) WorkerResult workerResult; std::string externalIp; int externalPort; // Worker thread HANDLE workerThread; bool workerDone; std::string lastError; ULONGLONG nextKeepaliveAt; }; static P2PState g_p2p = {}; static INIT_ONCE g_initOnce = INIT_ONCE_STATIC_INIT; BOOL CALLBACK InitP2PState(PINIT_ONCE, PVOID, PVOID*) { // Initialize Winsock once for the lifetime of the process. // WinHTTP already does this internally, but we do it explicitly so raw // Winsock calls (WSASocket, getaddrinfo, etc.) are available. WSADATA wsaData = {}; WSAStartup(MAKEWORD(2, 2), &wsaData); InitializeCriticalSection(&g_p2p.lock); g_p2p.state = Win64LceLiveP2P::EP2PState::Idle; g_p2p.udpSocket = INVALID_SOCKET; g_p2p.initialized = true; return TRUE; } void EnsureInitialized() { InitOnceExecuteOnce(&g_initOnce, &InitP2PState, nullptr, nullptr); } // ------------------------------------------------------------------------- // Resolve the STUN server address. Returns true on success. // ------------------------------------------------------------------------- bool ResolveStunServer(const char* host, sockaddr_in* outAddr) { addrinfo hints = {}; hints.ai_family = AF_INET; hints.ai_socktype = SOCK_DGRAM; char portStr[16] = {}; sprintf_s(portStr, "%d", STUN_PORT); addrinfo* info = nullptr; if (getaddrinfo(host, portStr, &hints, &info) != 0 || info == nullptr) return false; *outAddr = *reinterpret_cast(info->ai_addr); freeaddrinfo(info); return true; } // ------------------------------------------------------------------------- // Discovery worker thread // // Attempt order: // 1. UPnP IGD port mapping — no hole-punching needed, works on ~60% of home routers // 2. STUN external endpoint — requires hole-punching, works on ~95% of the rest // // In both cases the long-lived UDP socket stays open for the life of the // host session (keepalives via STUN, actual data via KCP later). // ------------------------------------------------------------------------- DWORD WINAPI DiscoveryWorkerProc(LPVOID) { // ---- Open and bind the long-lived UDP socket ---- SOCKET sock = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, nullptr, 0, 0); if (sock == INVALID_SOCKET) { WorkerResult r = {}; r.errorMessage = "P2P: failed to create UDP socket (WSA " + std::to_string(WSAGetLastError()) + ")"; EnterCriticalSection(&g_p2p.lock); g_p2p.workerResult = r; g_p2p.workerDone = true; LeaveCriticalSection(&g_p2p.lock); return 0; } sockaddr_in bindAddr = {}; bindAddr.sin_family = AF_INET; bindAddr.sin_addr.s_addr = INADDR_ANY; bindAddr.sin_port = 0; if (::bind(sock, reinterpret_cast(&bindAddr), sizeof(bindAddr)) != 0) { closesocket(sock); WorkerResult r = {}; r.errorMessage = "P2P: failed to bind UDP socket"; EnterCriticalSection(&g_p2p.lock); g_p2p.workerResult = r; g_p2p.workerDone = true; LeaveCriticalSection(&g_p2p.lock); return 0; } sockaddr_in local = {}; int localLen = sizeof(local); getsockname(sock, reinterpret_cast(&local), &localLen); const int localPort = ntohs(local.sin_port); setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&STUN_TIMEOUT_MS), sizeof(STUN_TIMEOUT_MS)); // ---- Step 1a: UPnP IGD — UDP (P2P socket) ---- LCELOG("P2P", "trying UPnP IGD UDP (local port %d)", localPort); const UPnPResult upnp = TryUPnPMapping(localPort, false /*udp*/); // ---- Step 1b: UPnP IGD — TCP (game server port) ---- // Always attempt regardless of UDP outcome: the router maps UDP and TCP // independently. A successful TCP mapping means joiners can reach the game // server directly over the internet without port-forwarding. const int tcpGamePort = WinsockNetLayer::GetHostPort(); UPnPResult tcpUpnp = {}; if (tcpGamePort > 0) { LCELOG("P2P", "trying UPnP IGD TCP (game port %d)", tcpGamePort); tcpUpnp = TryUPnPMapping(tcpGamePort, true /*tcp*/); if (tcpUpnp.success) LCELOG("P2P", "UPnP TCP game port %d mapped", tcpGamePort); else LCELOG("P2P", "UPnP TCP failed (%s)", tcpUpnp.error.c_str()); } if (upnp.success) { LCELOG("P2P", "UPnP UDP mapped — external %s:%d local port %d", upnp.externalIp.c_str(), upnp.externalPort, localPort); WorkerResult r = {}; r.success = true; r.method = EConnMethod::UPnP; r.externalIp = upnp.externalIp; r.externalPort = upnp.externalPort; EnterCriticalSection(&g_p2p.lock); g_p2p.udpSocket = sock; g_p2p.localPort = localPort; g_p2p.stunServerAddrValid = false; // keepalives still done via STUN below g_p2p.upnpMappingActive = true; g_p2p.upnpMappedPort = localPort; g_p2p.tcpUpnpMappingActive = tcpUpnp.success; g_p2p.tcpUpnpMappedPort = tcpUpnp.success ? tcpGamePort : 0; g_p2p.workerResult = r; g_p2p.workerDone = true; LeaveCriticalSection(&g_p2p.lock); return 0; } LCELOG("P2P", "UPnP UDP failed (%s) — falling back to STUN", upnp.error.c_str()); // ---- Step 2: STUN ---- const char* hosts[2] = { STUN_HOST_PRIMARY, STUN_HOST_FALLBACK }; WorkerResult result = {}; sockaddr_in serverAddr = {}; bool serverAddrValid = false; for (int h = 0; h < 2 && !result.success; ++h) { const char* stunHost = hosts[h]; if (!ResolveStunServer(stunHost, &serverAddr)) { result.errorMessage = std::string("P2P: DNS failed for ") + stunHost; LCELOG("P2P", "STUN DNS lookup failed for %s", stunHost); continue; } serverAddrValid = true; // Try up to 2 transmissions per server for (int attempt = 0; attempt < 2 && !result.success; ++attempt) { const StunRequest req = BuildStunRequest(); const int sent = sendto( sock, reinterpret_cast(req.bytes), sizeof(req.bytes), 0, reinterpret_cast(&serverAddr), sizeof(serverAddr)); if (sent != static_cast(sizeof(req.bytes))) { result.errorMessage = "P2P: STUN sendto failed"; continue; } unsigned char buf[512] = {}; sockaddr_in fromAddr = {}; int fromLen = sizeof(fromAddr); const int received = recvfrom( sock, reinterpret_cast(buf), sizeof(buf), 0, reinterpret_cast(&fromAddr), &fromLen); if (received < 20) { result.errorMessage = "P2P: STUN timeout or empty response"; continue; } std::string ip = {}; unsigned short port = 0; if (ParseStunResponse(buf, received, &ip, &port)) { result.success = true; result.method = EConnMethod::STUN; result.externalIp = ip; result.externalPort = port; } else { result.errorMessage = "P2P: STUN response parse failed"; } } if (!result.success) LCELOG("P2P", "STUN failed for %s — %s", stunHost, result.errorMessage.c_str()); } EnterCriticalSection(&g_p2p.lock); if (result.success) { g_p2p.udpSocket = sock; g_p2p.localPort = localPort; g_p2p.stunServerAddr = serverAddr; g_p2p.stunServerAddrValid = serverAddrValid; // TCP UPnP was already attempted at the top (Step 1b) — record result. g_p2p.tcpUpnpMappingActive = tcpUpnp.success; g_p2p.tcpUpnpMappedPort = tcpUpnp.success ? tcpGamePort : 0; } else { closesocket(sock); } g_p2p.workerResult = result; g_p2p.workerDone = true; LeaveCriticalSection(&g_p2p.lock); return 0; } // ------------------------------------------------------------------------- // Keepalive — sends a STUN binding request to keep the NAT mapping warm. // Called from P2PTick() outside the critical section. // Uses the cached server address; no DNS lookup. // ------------------------------------------------------------------------- void SendKeepalive(SOCKET sock, const sockaddr_in& serverAddr) { const StunRequest req = BuildStunRequest(); sendto( sock, reinterpret_cast(req.bytes), sizeof(req.bytes), 0, reinterpret_cast(&serverAddr), sizeof(serverAddr)); LCELOG("P2P", "keepalive sent"); } } // anonymous namespace // ============================================================================ // Public API // ============================================================================ namespace Win64LceLiveP2P { bool IsPublicRoutableIPv4(const std::string& ip) { return IsPublicRoutableIPv4Impl(ip); } bool HostOpen() { EnsureInitialized(); EnterCriticalSection(&g_p2p.lock); if (g_p2p.state != EP2PState::Idle) { LeaveCriticalSection(&g_p2p.lock); return false; // Already running — caller should HostClose() first. } g_p2p.state = EP2PState::Discovering; g_p2p.workerDone = false; g_p2p.connMethod = EConnMethod::None; g_p2p.lastError.clear(); g_p2p.externalIp.clear(); g_p2p.externalPort = 0; g_p2p.localPort = 0; g_p2p.upnpMappingActive = false; g_p2p.upnpMappedPort = 0; g_p2p.tcpUpnpMappingActive = false; g_p2p.tcpUpnpMappedPort = 0; g_p2p.workerThread = CreateThread(nullptr, 0, &DiscoveryWorkerProc, nullptr, 0, nullptr); if (g_p2p.workerThread == nullptr) { g_p2p.state = EP2PState::Failed; g_p2p.lastError = "P2P: failed to create discovery thread."; LeaveCriticalSection(&g_p2p.lock); return false; } LeaveCriticalSection(&g_p2p.lock); LCELOG("P2P", "discovery started (UPnP -> STUN fallback)"); return true; } void HostClose() { EnsureInitialized(); // Read state we need without blocking the worker. HANDLE threadToWait = nullptr; bool removeUpnp = false; int upnpPortToRemove = 0; bool removeTcpUpnp = false; int tcpUpnpPortToRemove = 0; EnterCriticalSection(&g_p2p.lock); threadToWait = g_p2p.workerThread; removeUpnp = g_p2p.upnpMappingActive; upnpPortToRemove = g_p2p.upnpMappedPort; removeTcpUpnp = g_p2p.tcpUpnpMappingActive; tcpUpnpPortToRemove = g_p2p.tcpUpnpMappedPort; LeaveCriticalSection(&g_p2p.lock); // Wait for in-flight discovery thread BEFORE taking the lock again, to // avoid deadlocking (the worker also takes the lock at the end of its run). if (threadToWait != nullptr) { WaitForSingleObject(threadToWait, 8000); CloseHandle(threadToWait); } // Remove UPnP mappings while we still have the info (before we clear state). if (removeUpnp && upnpPortToRemove != 0) { LCELOG("P2P", "removing UPnP UDP mapping for port %d", upnpPortToRemove); RemoveUPnPMapping(upnpPortToRemove, false /*udp*/); } if (removeTcpUpnp && tcpUpnpPortToRemove != 0) { LCELOG("P2P", "removing UPnP TCP mapping for port %d", tcpUpnpPortToRemove); RemoveUPnPMapping(tcpUpnpPortToRemove, true /*tcp*/); } EnterCriticalSection(&g_p2p.lock); g_p2p.workerThread = nullptr; if (g_p2p.udpSocket != INVALID_SOCKET) { closesocket(g_p2p.udpSocket); g_p2p.udpSocket = INVALID_SOCKET; } g_p2p.state = EP2PState::Idle; g_p2p.connMethod = EConnMethod::None; g_p2p.localPort = 0; g_p2p.externalIp.clear(); g_p2p.externalPort = 0; g_p2p.stunServerAddrValid = false; g_p2p.lastError.clear(); g_p2p.workerDone = false; g_p2p.upnpMappingActive = false; g_p2p.upnpMappedPort = 0; g_p2p.tcpUpnpMappingActive = false; g_p2p.tcpUpnpMappedPort = 0; LeaveCriticalSection(&g_p2p.lock); LCELOG("P2P", "host socket closed"); } void P2PTick() { EnsureInitialized(); EnterCriticalSection(&g_p2p.lock); // Integrate a completed discovery if (g_p2p.state == EP2PState::Discovering && g_p2p.workerDone) { HANDLE t = g_p2p.workerThread; g_p2p.workerThread = nullptr; LeaveCriticalSection(&g_p2p.lock); if (t != nullptr) { WaitForSingleObject(t, INFINITE); CloseHandle(t); } EnterCriticalSection(&g_p2p.lock); const WorkerResult& r = g_p2p.workerResult; if (r.success) { g_p2p.state = EP2PState::Ready; g_p2p.connMethod = r.method; g_p2p.externalIp = r.externalIp; g_p2p.externalPort = r.externalPort; g_p2p.nextKeepaliveAt = GetTickCount64() + KEEPALIVE_INTERVAL_MS; const char* methodName = (r.method == EConnMethod::UPnP) ? "UPnP" : "STUN"; LCELOG("P2P", "ready via %s — external %s:%d local port %d", methodName, r.externalIp.c_str(), r.externalPort, g_p2p.localPort); } else { g_p2p.state = EP2PState::Failed; g_p2p.lastError = r.errorMessage; LCELOG("P2P", "all discovery methods failed — %s", r.errorMessage.c_str()); } } // Check whether a STUN keepalive is due. // UPnP sessions also send keepalives to keep the STUN mapping warm for // potential fallback, but only if we have a STUN server address cached. bool doKeepalive = false; SOCKET keepaliveSock = INVALID_SOCKET; sockaddr_in keepaliveAddr = {}; if (g_p2p.state == EP2PState::Ready && g_p2p.stunServerAddrValid && GetTickCount64() >= g_p2p.nextKeepaliveAt) { doKeepalive = true; keepaliveSock = g_p2p.udpSocket; keepaliveAddr = g_p2p.stunServerAddr; g_p2p.nextKeepaliveAt = GetTickCount64() + KEEPALIVE_INTERVAL_MS; } LeaveCriticalSection(&g_p2p.lock); if (doKeepalive && keepaliveSock != INVALID_SOCKET) SendKeepalive(keepaliveSock, keepaliveAddr); } P2PSnapshot GetP2PSnapshot() { EnsureInitialized(); P2PTick(); P2PSnapshot snap = {}; EnterCriticalSection(&g_p2p.lock); snap.state = g_p2p.state; snap.connMethod = g_p2p.connMethod; snap.externalIp = g_p2p.externalIp; snap.externalPort = g_p2p.externalPort; snap.localPort = g_p2p.localPort; snap.tcpPortMapped = g_p2p.tcpUpnpMappingActive; switch (g_p2p.state) { case EP2PState::Idle: snap.statusMessage = L"P2P: idle."; break; case EP2PState::Discovering: snap.statusMessage = L"P2P: discovering external endpoint (UPnP \u2192 STUN)..."; break; case EP2PState::Ready: { const wchar_t* method = (g_p2p.connMethod == EConnMethod::UPnP) ? L"UPnP" : L"STUN"; wchar_t buf[256] = {}; swprintf_s(buf, L"P2P ready via %s. External %hs:%d (local port %d)", method, g_p2p.externalIp.c_str(), g_p2p.externalPort, g_p2p.localPort); snap.statusMessage = buf; break; } case EP2PState::Failed: snap.statusMessage = L"P2P: discovery failed (UPnP + STUN). " L"Manual port forwarding may be required."; snap.errorMessage = std::wstring( g_p2p.lastError.begin(), g_p2p.lastError.end()); break; } LeaveCriticalSection(&g_p2p.lock); return snap; } } // namespace Win64LceLiveP2P #endif // _WINDOWS64