1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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 <map>
#include <mutex>
#ifndef _WIN32
#include <unistd.h>
#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<SOCKET, int> 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<std::mutex> 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<std::mutex> lock(a.mtx);
std::map<SOCKET, int>::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<std::mutex> 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<std::mutex> 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
|