diff options
Diffstat (limited to 'src/sockcount.h')
| -rw-r--r-- | src/sockcount.h | 137 |
1 files changed, 137 insertions, 0 deletions
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 @@ | |||
| 1 | // Copyright (c) 2026 Bitflash developers | ||
| 2 | // Distributed under the MIT/X11 software license. | ||
| 3 | // | ||
| 4 | // Socket accounting -- which part of the program is holding the sockets. | ||
| 5 | // | ||
| 6 | // A node on a 32-core machine was found with 1502 sockets in Windows' "bound" | ||
| 7 | // state: created, never connected, never closed, each holding a distinct | ||
| 8 | // ephemeral port. That is 9% of the machine's whole dynamic port range gone in | ||
| 9 | // nineteen hours, and when the range runs out nothing on that machine can open | ||
| 10 | // an outbound connection -- not just this program. | ||
| 11 | // | ||
| 12 | // Reading the code did not find it. Every connect() failure path in this tree | ||
| 13 | // closes its socket. So count instead: tag each socket where it is created, | ||
| 14 | // untag it where it is closed, and report the difference per site. Whichever | ||
| 15 | // site's live count tracks the leak is the answer. | ||
| 16 | // | ||
| 17 | // Header-only on purpose. btfrv.cpp is compiled a second time, on its own, into | ||
| 18 | // the standalone rendezvous relay -- a separate tree that does not link net.cpp. | ||
| 19 | // Anything here that needed a .cpp would break that build, and it would break | ||
| 20 | // it on the VPS rather than on this machine. Include after the platform socket | ||
| 21 | // headers; each of those files defines SOCKET for itself. | ||
| 22 | |||
| 23 | #ifndef BITFLASH_SOCKCOUNT_H | ||
| 24 | #define BITFLASH_SOCKCOUNT_H | ||
| 25 | |||
| 26 | #include <map> | ||
| 27 | #include <mutex> | ||
| 28 | |||
| 29 | #ifndef _WIN32 | ||
| 30 | #include <unistd.h> | ||
| 31 | #endif | ||
| 32 | |||
| 33 | enum | ||
| 34 | { | ||
| 35 | SOCK_EXTIP, // GetMyExternalIP probe | ||
| 36 | SOCK_LISTEN, // the node's own listening socket | ||
| 37 | SOCK_ACCEPT, // inbound connections | ||
| 38 | SOCK_RV_DIAL, // outbound to a rendezvous relay | ||
| 39 | SOCK_RV_LISTEN, // rendezvous listener | ||
| 40 | SOCK_RV_ACCEPT, // inbound at the rendezvous | ||
| 41 | SOCK_PAIR_LISTEN, // loopback pair: the temporary listener | ||
| 42 | SOCK_PAIR_APP, // loopback pair: the app end handed to CNode | ||
| 43 | SOCK_PAIR_PUMP, // loopback pair: the pump end | ||
| 44 | SOCK_NOSTR, // outbound to a Nostr relay | ||
| 45 | SOCK_SITES | ||
| 46 | }; | ||
| 47 | |||
| 48 | struct BtfSockAccount | ||
| 49 | { | ||
| 50 | std::mutex mtx; | ||
| 51 | std::map<SOCKET, int> mapSite; | ||
| 52 | long long nOpened[SOCK_SITES]; | ||
| 53 | long long nClosed[SOCK_SITES]; | ||
| 54 | long long nCloseFailed[SOCK_SITES]; | ||
| 55 | long long nClosedUntagged; | ||
| 56 | BtfSockAccount() : nClosedUntagged(0) | ||
| 57 | { | ||
| 58 | for (int i = 0; i < SOCK_SITES; i++) { nOpened[i] = 0; nClosed[i] = 0; nCloseFailed[i] = 0; } | ||
| 59 | } | ||
| 60 | }; | ||
| 61 | |||
| 62 | // One instance across every translation unit: a function-local static in an | ||
| 63 | // inline function is the same object everywhere it is used. | ||
| 64 | inline BtfSockAccount& BtfSockAccounting() | ||
| 65 | { | ||
| 66 | static BtfSockAccount account; | ||
| 67 | return account; | ||
| 68 | } | ||
| 69 | |||
| 70 | // Record a freshly created socket and return it unchanged, so call sites read | ||
| 71 | // as they did before. INVALID_SOCKET passes through untouched. | ||
| 72 | inline SOCKET BtfSocketTag(SOCKET hSocket, int nSite) | ||
| 73 | { | ||
| 74 | if (hSocket == INVALID_SOCKET || nSite < 0 || nSite >= SOCK_SITES) | ||
| 75 | return hSocket; | ||
| 76 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 77 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 78 | a.mapSite[hSocket] = nSite; | ||
| 79 | a.nOpened[nSite]++; | ||
| 80 | return hSocket; | ||
| 81 | } | ||
| 82 | |||
| 83 | // Close a socket and forget it. Closing one nobody tagged is counted too -- | ||
| 84 | // that number staying at zero is what says the accounting is complete. | ||
| 85 | inline int BtfCloseSocket(SOCKET hSocket) | ||
| 86 | { | ||
| 87 | if (hSocket == INVALID_SOCKET) | ||
| 88 | return 0; | ||
| 89 | int nSite = -1; | ||
| 90 | { | ||
| 91 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 92 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 93 | std::map<SOCKET, int>::iterator mi = a.mapSite.find(hSocket); | ||
| 94 | if (mi == a.mapSite.end()) | ||
| 95 | a.nClosedUntagged++; | ||
| 96 | else | ||
| 97 | { | ||
| 98 | nSite = mi->second; | ||
| 99 | a.nClosed[nSite]++; | ||
| 100 | a.mapSite.erase(mi); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | #ifdef _WIN32 | ||
| 104 | int nRet = ::closesocket(hSocket); | ||
| 105 | #else | ||
| 106 | int nRet = ::close(hSocket); | ||
| 107 | #endif | ||
| 108 | // A close that fails leaves the descriptor -- and on Windows the ephemeral | ||
| 109 | // port it holds -- in place. Counted because a socket the program believes | ||
| 110 | // it released and the operating system still shows is exactly the shape of | ||
| 111 | // the leak being chased, and nothing in this codebase has ever looked at | ||
| 112 | // what closesocket returns. | ||
| 113 | if (nRet != 0 && nSite >= 0) | ||
| 114 | { | ||
| 115 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 116 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 117 | a.nCloseFailed[nSite]++; | ||
| 118 | } | ||
| 119 | return nRet; | ||
| 120 | } | ||
| 121 | |||
| 122 | // Copy the counters out. Formatting lives in net.cpp, which has strprintf -- | ||
| 123 | // and which the relay build does not compile. | ||
| 124 | inline void BtfSockSnapshot(long long* pOpened, long long* pClosed, long long* pFailed, long long* pUntagged) | ||
| 125 | { | ||
| 126 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 127 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 128 | for (int i = 0; i < SOCK_SITES; i++) | ||
| 129 | { | ||
| 130 | pOpened[i] = a.nOpened[i]; | ||
| 131 | pClosed[i] = a.nClosed[i]; | ||
| 132 | pFailed[i] = a.nCloseFailed[i]; | ||
| 133 | } | ||
| 134 | *pUntagged = a.nClosedUntagged; | ||
| 135 | } | ||
| 136 | |||
| 137 | #endif | ||