diff options
Diffstat (limited to 'src/sockcount.h')
| -rw-r--r-- | src/sockcount.h | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/src/sockcount.h b/src/sockcount.h new file mode 100644 index 0000000..96b6efb --- /dev/null +++ b/src/sockcount.h | |||
| @@ -0,0 +1,219 @@ | |||
| 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, each apparently holding a distinct ephemeral port. Reading the code | ||
| 8 | // did not find it: every connect() failure path in this tree closes its socket. | ||
| 9 | // So count instead -- tag each socket where it is created, untag it where it is | ||
| 10 | // closed, report the difference per site. | ||
| 11 | // | ||
| 12 | // The counting is what showed the counting was wrong. Windows lists a socket | ||
| 13 | // bound to the wildcard address TWICE in MSFT_NetTCPConnection: once for the | ||
| 14 | // binding (0.0.0.0:port, state "Bound") and once for the connection itself | ||
| 15 | // (state "Established"). Measured: 35 of a node's 38 "Bound" rows had the same | ||
| 16 | // local port as one of its established connections. The bound count therefore | ||
| 17 | // rises because the node is talking to more peers, not because it is leaking, | ||
| 18 | // which is why a fix that plainly worked looked like it had done nothing. | ||
| 19 | // | ||
| 20 | // The honest metric is the ORPHAN: a "Bound" row whose port appears nowhere | ||
| 21 | // else in that process's table. | ||
| 22 | // | ||
| 23 | // $b = $c | ? State -eq 'Bound' | ||
| 24 | // $rest = ($c | ? State -ne 'Bound').LocalPort | ||
| 25 | // ($b | ? { $rest -notcontains $_.LocalPort }).Count | ||
| 26 | // | ||
| 27 | // By that metric, measured the same evening on three machines: a node on 1.2.10 | ||
| 28 | // with 26h of uptime had 1262 orphans and was still taking on 30-90 an hour; a | ||
| 29 | // node on 1.2.11 with 4h40m had 3; a node on 1.2.12 had 3, all from its first | ||
| 30 | // half minute. At the 1.2.10 rate the 1.2.11 node would have had ~230. | ||
| 31 | // | ||
| 32 | // So the leak was real and 1.2.11 ended it -- most likely the 60-second reaper | ||
| 33 | // for undeliverable nodes, which was written for a different problem. The | ||
| 34 | // counters below stay: they are how the question got answered, and they are how | ||
| 35 | // it would be answered again. | ||
| 36 | // | ||
| 37 | // Header-only on purpose. btfrv.cpp is compiled a second time, on its own, into | ||
| 38 | // the standalone rendezvous relay -- a separate tree that does not link net.cpp. | ||
| 39 | // Anything here that needed a .cpp would break that build, and it would break | ||
| 40 | // it on the VPS rather than on this machine. Include after the platform socket | ||
| 41 | // headers; each of those files defines SOCKET for itself. | ||
| 42 | |||
| 43 | #ifndef BITFLASH_SOCKCOUNT_H | ||
| 44 | #define BITFLASH_SOCKCOUNT_H | ||
| 45 | |||
| 46 | #include <cerrno> | ||
| 47 | #include <map> | ||
| 48 | #include <mutex> | ||
| 49 | |||
| 50 | #ifndef _WIN32 | ||
| 51 | #include <unistd.h> | ||
| 52 | #endif | ||
| 53 | |||
| 54 | enum | ||
| 55 | { | ||
| 56 | SOCK_EXTIP, // GetMyExternalIP probe | ||
| 57 | SOCK_LISTEN, // the node's own listening socket | ||
| 58 | SOCK_ACCEPT, // inbound connections | ||
| 59 | SOCK_RV_DIAL, // outbound to a rendezvous relay | ||
| 60 | SOCK_RV_LISTEN, // rendezvous listener | ||
| 61 | SOCK_RV_ACCEPT, // inbound at the rendezvous | ||
| 62 | SOCK_PAIR_LISTEN, // loopback pair: the temporary listener | ||
| 63 | SOCK_PAIR_APP, // loopback pair: the app end handed to CNode | ||
| 64 | SOCK_PAIR_PUMP, // loopback pair: the pump end | ||
| 65 | SOCK_NOSTR, // outbound to a Nostr relay | ||
| 66 | SOCK_SITES | ||
| 67 | }; | ||
| 68 | |||
| 69 | struct BtfSockAccount | ||
| 70 | { | ||
| 71 | std::mutex mtx; | ||
| 72 | std::map<SOCKET, int> mapSite; | ||
| 73 | long long nOpened[SOCK_SITES]; | ||
| 74 | long long nClosed[SOCK_SITES]; | ||
| 75 | long long nCloseFailed[SOCK_SITES]; | ||
| 76 | // Why the close failed, counted per code per site. Kept because nothing in | ||
| 77 | // this codebase had ever looked at what closesocket returns, and a close | ||
| 78 | // that fails leaves the descriptor and its port in place. It is not the | ||
| 79 | // leak -- see the note at the top of this file -- but it is the one number | ||
| 80 | // that would say so if it ever became the leak. | ||
| 81 | std::map<int, long long> mapCloseErr[SOCK_SITES]; | ||
| 82 | // A handle number arriving from socket()/accept() while some site still | ||
| 83 | // claims it. The operating system only hands a number back out after the | ||
| 84 | // last close of it, so a collision means somebody's bookkeeping outlived | ||
| 85 | // its socket -- and whoever still believes they own that number will | ||
| 86 | // eventually close it, taking down a connection that now belongs to | ||
| 87 | // someone else. Counted against the site that was still holding it, which | ||
| 88 | // is the one to go and read. | ||
| 89 | long long nCollided[SOCK_SITES]; | ||
| 90 | long long nClosedUntagged; | ||
| 91 | BtfSockAccount() : nClosedUntagged(0) | ||
| 92 | { | ||
| 93 | for (int i = 0; i < SOCK_SITES; i++) { nOpened[i] = 0; nClosed[i] = 0; nCloseFailed[i] = 0; nCollided[i] = 0; } | ||
| 94 | } | ||
| 95 | }; | ||
| 96 | |||
| 97 | // One instance across every translation unit: a function-local static in an | ||
| 98 | // inline function is the same object everywhere it is used. | ||
| 99 | inline BtfSockAccount& BtfSockAccounting() | ||
| 100 | { | ||
| 101 | static BtfSockAccount account; | ||
| 102 | return account; | ||
| 103 | } | ||
| 104 | |||
| 105 | // Record a freshly created socket and return it unchanged, so call sites read | ||
| 106 | // as they did before. INVALID_SOCKET passes through untouched. | ||
| 107 | inline SOCKET BtfSocketTag(SOCKET hSocket, int nSite) | ||
| 108 | { | ||
| 109 | if (hSocket == INVALID_SOCKET || nSite < 0 || nSite >= SOCK_SITES) | ||
| 110 | return hSocket; | ||
| 111 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 112 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 113 | std::map<SOCKET, int>::iterator mi = a.mapSite.find(hSocket); | ||
| 114 | if (mi != a.mapSite.end()) | ||
| 115 | a.nCollided[mi->second]++; | ||
| 116 | a.mapSite[hSocket] = nSite; | ||
| 117 | a.nOpened[nSite]++; | ||
| 118 | return hSocket; | ||
| 119 | } | ||
| 120 | |||
| 121 | // Close a socket and forget it. Closing one nobody tagged is counted too -- | ||
| 122 | // that number staying at zero is what says the accounting is complete. | ||
| 123 | inline int BtfCloseSocket(SOCKET hSocket) | ||
| 124 | { | ||
| 125 | if (hSocket == INVALID_SOCKET) | ||
| 126 | return 0; | ||
| 127 | int nSite = -1; | ||
| 128 | { | ||
| 129 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 130 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 131 | std::map<SOCKET, int>::iterator mi = a.mapSite.find(hSocket); | ||
| 132 | if (mi == a.mapSite.end()) | ||
| 133 | a.nClosedUntagged++; | ||
| 134 | else | ||
| 135 | { | ||
| 136 | nSite = mi->second; | ||
| 137 | a.nClosed[nSite]++; | ||
| 138 | a.mapSite.erase(mi); | ||
| 139 | } | ||
| 140 | } | ||
| 141 | #ifdef _WIN32 | ||
| 142 | int nRet = ::closesocket(hSocket); | ||
| 143 | int nErr = (nRet != 0) ? WSAGetLastError() : 0; | ||
| 144 | #else | ||
| 145 | int nRet = ::close(hSocket); | ||
| 146 | int nErr = (nRet != 0) ? errno : 0; | ||
| 147 | #endif | ||
| 148 | // A close that fails leaves the descriptor -- and on Windows the ephemeral | ||
| 149 | // port it holds -- in place. Counted because a socket the program believes | ||
| 150 | // it released and the operating system still shows is exactly the shape of | ||
| 151 | // the leak being chased, and nothing in this codebase has ever looked at | ||
| 152 | // what closesocket returns. | ||
| 153 | if (nRet != 0 && nSite >= 0) | ||
| 154 | { | ||
| 155 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 156 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 157 | a.nCloseFailed[nSite]++; | ||
| 158 | a.mapCloseErr[nSite][nErr]++; | ||
| 159 | } | ||
| 160 | return nRet; | ||
| 161 | } | ||
| 162 | |||
| 163 | // Copy the counters out. Formatting lives in net.cpp, which has strprintf -- | ||
| 164 | // and which the relay build does not compile. | ||
| 165 | inline void BtfSockSnapshot(long long* pOpened, long long* pClosed, long long* pFailed, | ||
| 166 | long long* pCollided, long long* pUntagged) | ||
| 167 | { | ||
| 168 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 169 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 170 | for (int i = 0; i < SOCK_SITES; i++) | ||
| 171 | { | ||
| 172 | pOpened[i] = a.nOpened[i]; | ||
| 173 | pClosed[i] = a.nClosed[i]; | ||
| 174 | pFailed[i] = a.nCloseFailed[i]; | ||
| 175 | pCollided[i] = a.nCollided[i]; | ||
| 176 | } | ||
| 177 | *pUntagged = a.nClosedUntagged; | ||
| 178 | } | ||
| 179 | |||
| 180 | // Of the sockets still tagged live, how many have no peer on the other end. | ||
| 181 | // | ||
| 182 | // This is the check that ended the socket hunt, by answering it with a zero. | ||
| 183 | // The suspicion was that some site created sockets, never connected them and | ||
| 184 | // never let go. Run against a live node it reports 1 -- the listening socket, | ||
| 185 | // which has no peer by definition. Every other tagged socket is connected. | ||
| 186 | // | ||
| 187 | // See the note at the top of this file for what the pile actually was. | ||
| 188 | inline void BtfSockLiveUnconnected(long long* pUnconnected) | ||
| 189 | { | ||
| 190 | for (int i = 0; i < SOCK_SITES; i++) | ||
| 191 | pUnconnected[i] = 0; | ||
| 192 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 193 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 194 | for (std::map<SOCKET, int>::const_iterator mi = a.mapSite.begin(); mi != a.mapSite.end(); ++mi) | ||
| 195 | { | ||
| 196 | struct sockaddr_storage ss; | ||
| 197 | #ifdef _WIN32 | ||
| 198 | int len = sizeof(ss); | ||
| 199 | #else | ||
| 200 | socklen_t len = sizeof(ss); | ||
| 201 | #endif | ||
| 202 | if (getpeername(mi->first, (struct sockaddr*)&ss, &len) != 0) | ||
| 203 | pUnconnected[mi->second]++; | ||
| 204 | } | ||
| 205 | } | ||
| 206 | |||
| 207 | // The close-failure codes for one site, newest counts included. Separate call | ||
| 208 | // because the table above is fixed-width and this is a list of unknown length. | ||
| 209 | inline void BtfSockCloseErrors(int nSite, std::map<int, long long>& mapOut) | ||
| 210 | { | ||
| 211 | mapOut.clear(); | ||
| 212 | if (nSite < 0 || nSite >= SOCK_SITES) | ||
| 213 | return; | ||
| 214 | BtfSockAccount& a = BtfSockAccounting(); | ||
| 215 | std::lock_guard<std::mutex> lock(a.mtx); | ||
| 216 | mapOut = a.mapCloseErr[nSite]; | ||
| 217 | } | ||
| 218 | |||
| 219 | #endif | ||