upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/sockcount.h
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-08-01 13:10:23 -0300
committerGitHub <noreply@github.com>2026-08-01 13:10:23 -0300
commit21c8bb63cb85f3001ddee9237b1d7e689675d293 (patch)
tree9668f6bf39635424a6180c7a7edb6c7931ea7545 /src/sockcount.h
parent7131c30c33c980f0ba1d37dfc6a3fbe8c319813e (diff)
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.
Diffstat (limited to 'src/sockcount.h')
-rw-r--r--src/sockcount.h137
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
33enum
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
48struct 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.
64inline 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.
72inline 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.
85inline 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.
124inline 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