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:
Diffstat (limited to 'src/sockcount.h')
-rw-r--r--src/sockcount.h219
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
54enum
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
69struct 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.
99inline 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.
107inline 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.
123inline 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.
165inline 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.
188inline 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.
209inline 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