From 21c8bb63cb85f3001ddee9237b1d7e689675d293 Mon Sep 17 00:00:00 2001 From: Bitflash Date: Sat, 1 Aug 2026 13:10:23 -0300 Subject: diag: count sockets by where they were created, and whether the close took (#86) A node was found holding 1502 sockets in Windows bound state, one ephemeral port each, 9% of the machine range in nineteen hours. Reading the code did not find it: every connect() failure path here closes its socket. So count. Every socket is tagged at creation and untagged at close, across all ten sites, with a counter for closes that fail. Already rules out three explanations: nothing is created or closed behind the counter (untagged closes are zero), no close is failing, and the leaked sockets are not in the live set -- tracked live matches what the OS reports as established, while the bound pile is something the program already closed successfully. --- src/sockcount.h | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/sockcount.h (limited to 'src/sockcount.h') diff --git a/src/sockcount.h b/src/sockcount.h new file mode 100644 index 0000000..19f0827 --- /dev/null +++ b/src/sockcount.h @@ -0,0 +1,137 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Socket accounting -- which part of the program is holding the sockets. +// +// A node on a 32-core machine was found with 1502 sockets in Windows' "bound" +// state: created, never connected, never closed, each holding a distinct +// ephemeral port. That is 9% of the machine's whole dynamic port range gone in +// nineteen hours, and when the range runs out nothing on that machine can open +// an outbound connection -- not just this program. +// +// Reading the code did not find it. Every connect() failure path in this tree +// closes its socket. So count instead: tag each socket where it is created, +// untag it where it is closed, and report the difference per site. Whichever +// site's live count tracks the leak is the answer. +// +// Header-only on purpose. btfrv.cpp is compiled a second time, on its own, into +// the standalone rendezvous relay -- a separate tree that does not link net.cpp. +// Anything here that needed a .cpp would break that build, and it would break +// it on the VPS rather than on this machine. Include after the platform socket +// headers; each of those files defines SOCKET for itself. + +#ifndef BITFLASH_SOCKCOUNT_H +#define BITFLASH_SOCKCOUNT_H + +#include +#include + +#ifndef _WIN32 +#include +#endif + +enum +{ + SOCK_EXTIP, // GetMyExternalIP probe + SOCK_LISTEN, // the node's own listening socket + SOCK_ACCEPT, // inbound connections + SOCK_RV_DIAL, // outbound to a rendezvous relay + SOCK_RV_LISTEN, // rendezvous listener + SOCK_RV_ACCEPT, // inbound at the rendezvous + SOCK_PAIR_LISTEN, // loopback pair: the temporary listener + SOCK_PAIR_APP, // loopback pair: the app end handed to CNode + SOCK_PAIR_PUMP, // loopback pair: the pump end + SOCK_NOSTR, // outbound to a Nostr relay + SOCK_SITES +}; + +struct BtfSockAccount +{ + std::mutex mtx; + std::map mapSite; + long long nOpened[SOCK_SITES]; + long long nClosed[SOCK_SITES]; + long long nCloseFailed[SOCK_SITES]; + long long nClosedUntagged; + BtfSockAccount() : nClosedUntagged(0) + { + for (int i = 0; i < SOCK_SITES; i++) { nOpened[i] = 0; nClosed[i] = 0; nCloseFailed[i] = 0; } + } +}; + +// One instance across every translation unit: a function-local static in an +// inline function is the same object everywhere it is used. +inline BtfSockAccount& BtfSockAccounting() +{ + static BtfSockAccount account; + return account; +} + +// Record a freshly created socket and return it unchanged, so call sites read +// as they did before. INVALID_SOCKET passes through untouched. +inline SOCKET BtfSocketTag(SOCKET hSocket, int nSite) +{ + if (hSocket == INVALID_SOCKET || nSite < 0 || nSite >= SOCK_SITES) + return hSocket; + BtfSockAccount& a = BtfSockAccounting(); + std::lock_guard lock(a.mtx); + a.mapSite[hSocket] = nSite; + a.nOpened[nSite]++; + return hSocket; +} + +// Close a socket and forget it. Closing one nobody tagged is counted too -- +// that number staying at zero is what says the accounting is complete. +inline int BtfCloseSocket(SOCKET hSocket) +{ + if (hSocket == INVALID_SOCKET) + return 0; + int nSite = -1; + { + BtfSockAccount& a = BtfSockAccounting(); + std::lock_guard lock(a.mtx); + std::map::iterator mi = a.mapSite.find(hSocket); + if (mi == a.mapSite.end()) + a.nClosedUntagged++; + else + { + nSite = mi->second; + a.nClosed[nSite]++; + a.mapSite.erase(mi); + } + } +#ifdef _WIN32 + int nRet = ::closesocket(hSocket); +#else + int nRet = ::close(hSocket); +#endif + // A close that fails leaves the descriptor -- and on Windows the ephemeral + // port it holds -- in place. Counted because a socket the program believes + // it released and the operating system still shows is exactly the shape of + // the leak being chased, and nothing in this codebase has ever looked at + // what closesocket returns. + if (nRet != 0 && nSite >= 0) + { + BtfSockAccount& a = BtfSockAccounting(); + std::lock_guard lock(a.mtx); + a.nCloseFailed[nSite]++; + } + return nRet; +} + +// Copy the counters out. Formatting lives in net.cpp, which has strprintf -- +// and which the relay build does not compile. +inline void BtfSockSnapshot(long long* pOpened, long long* pClosed, long long* pFailed, long long* pUntagged) +{ + BtfSockAccount& a = BtfSockAccounting(); + std::lock_guard lock(a.mtx); + for (int i = 0; i < SOCK_SITES; i++) + { + pOpened[i] = a.nOpened[i]; + pClosed[i] = a.nClosed[i]; + pFailed[i] = a.nCloseFailed[i]; + } + *pUntagged = a.nClosedUntagged; +} + +#endif -- cgit v1.2.3