diff options
| author | Bitflash <btf01@proton.me> | 2026-07-31 09:38:10 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-31 09:38:10 -0300 |
| commit | 375415e2fb5af01eeea045dd9aa0522844d9b218 (patch) | |
| tree | 8567d0d98e8e42aba0a48eb4a2735ba8e69a2207 /relay/btfrv.cpp | |
| parent | f2ea483df4b2277abe13cc19bcc0bc5834c2e3e6 (diff) | |
relay: drop registrations whose far end is gone (#65)
A service registers at the relay and its socket sits in the waiting map
until a client dials it. Nothing ever checked whether that service was
still there. A node that went away left its registration behind, and the
next client to ask for that key got paired with a corpse: both sides were
told the pairing succeeded, then the tunnel failed with nothing to explain
it. That is the "tunnel to X failed" line the dialling node reports.
A reaper walks the waiting map every 60 seconds and drops registrations
whose far end is provably gone, and the pairing path checks the same thing
before handing a registration over, so a client gets a clean refusal
instead of a broken tunnel.
Provably gone means: the socket became readable and recv returned 0 or an
error. A registered service sends nothing while it waits, so readable is
already abnormal; recv distinguishes a closed peer from anything else.
Sockets that are simply idle are left alone. select() rather than
MSG_DONTWAIT so this compiles on Windows and never touches the socket's
blocking mode.
Deliberately no expiry by age. Nodes older than v1.2.7 register once and
block forever waiting to be dialled -- they never re-register, so a clock
based rule would quietly make every one of them unreachable, and being
blocked in recv they would never find out. Only provable death counts.
Known limit: this catches a peer that closed or reset, not one whose path
died silently with no FIN. Detecting that needs a probe, and there is no
byte the relay can send to a waiting service without corrupting the
protocol it is waiting on.
Verified on the VPS on a spare port: a node registered, was killed with
SIGKILL, and the next reaper pass logged
"reaper: dropped 1 dead registration(s), 0 waiting".
Diffstat (limited to 'relay/btfrv.cpp')
| -rw-r--r-- | relay/btfrv.cpp | 95 |
1 files changed, 94 insertions, 1 deletions
diff --git a/relay/btfrv.cpp b/relay/btfrv.cpp index ffda08c..b1eee55 100644 --- a/relay/btfrv.cpp +++ b/relay/btfrv.cpp | |||
| @@ -35,6 +35,7 @@ typedef int SOCKET; | |||
| 35 | #include <thread> | 35 | #include <thread> |
| 36 | #include <mutex> | 36 | #include <mutex> |
| 37 | #include <atomic> | 37 | #include <atomic> |
| 38 | #include <chrono> | ||
| 38 | 39 | ||
| 39 | namespace btf | 40 | namespace btf |
| 40 | { | 41 | { |
| @@ -46,6 +47,9 @@ static SOCKET g_listen = INVALID_SOCKET; | |||
| 46 | static std::map<std::string, SOCKET> g_waiting; | 47 | static std::map<std::string, SOCKET> g_waiting; |
| 47 | static std::mutex g_mtx; | 48 | static std::mutex g_mtx; |
| 48 | 49 | ||
| 50 | // How often the reaper walks the waiting map. | ||
| 51 | static const int REAP_INTERVAL_SECS = 60; | ||
| 52 | |||
| 49 | bool RvInit() | 53 | bool RvInit() |
| 50 | { | 54 | { |
| 51 | static bool done = false; | 55 | static bool done = false; |
| @@ -106,6 +110,77 @@ static SOCKET ConnectTo(const char* host, unsigned short port) | |||
| 106 | return s; | 110 | return s; |
| 107 | } | 111 | } |
| 108 | 112 | ||
| 113 | // ---- liveness of a waiting registration ---- | ||
| 114 | |||
| 115 | // True when the far end of a registration socket is provably gone. | ||
| 116 | // | ||
| 117 | // A registered service sends nothing while it waits, so the socket should never | ||
| 118 | // be readable. Readable therefore means one of two things, and recv tells them | ||
| 119 | // apart: 0 is a clean close, negative is an error, and either way the | ||
| 120 | // registration is worthless. Anything else -- including a service that somehow | ||
| 121 | // has data pending -- is left alone, because guessing wrong here unregisters a | ||
| 122 | // node that is perfectly fine. | ||
| 123 | // | ||
| 124 | // select() rather than MSG_DONTWAIT so the same code compiles on Windows, and | ||
| 125 | // so the socket's blocking mode is never touched. | ||
| 126 | static bool RegistrationDead(SOCKET s) | ||
| 127 | { | ||
| 128 | fd_set rd; | ||
| 129 | FD_ZERO(&rd); | ||
| 130 | FD_SET(s, &rd); | ||
| 131 | struct timeval tv; | ||
| 132 | tv.tv_sec = 0; | ||
| 133 | tv.tv_usec = 0; | ||
| 134 | int r = select((int)(s + 1), &rd, NULL, NULL, &tv); | ||
| 135 | if (r <= 0) | ||
| 136 | return false; // not readable: still waiting, healthy | ||
| 137 | char c; | ||
| 138 | int n = recv(s, &c, 1, MSG_PEEK); | ||
| 139 | return (n <= 0); | ||
| 140 | } | ||
| 141 | |||
| 142 | // Drop registrations whose far end has gone away. | ||
| 143 | // | ||
| 144 | // Deliberately no expiry by age. Nodes older than v1.2.7 register once and then | ||
| 145 | // block forever waiting to be dialled -- they never re-register, so a clock | ||
| 146 | // based rule would quietly make every one of them unreachable, and being | ||
| 147 | // blocked in recv they would never find out. Only provable death counts. | ||
| 148 | static void ReapDeadRegistrations() | ||
| 149 | { | ||
| 150 | int nBefore = 0, nReaped = 0; | ||
| 151 | { | ||
| 152 | std::lock_guard<std::mutex> lk(g_mtx); | ||
| 153 | nBefore = (int)g_waiting.size(); | ||
| 154 | std::map<std::string, SOCKET>::iterator it = g_waiting.begin(); | ||
| 155 | while (it != g_waiting.end()) | ||
| 156 | { | ||
| 157 | if (RegistrationDead(it->second)) | ||
| 158 | { | ||
| 159 | CLOSESOCK(it->second); | ||
| 160 | g_waiting.erase(it++); | ||
| 161 | nReaped++; | ||
| 162 | } | ||
| 163 | else | ||
| 164 | ++it; | ||
| 165 | } | ||
| 166 | } | ||
| 167 | if (nReaped > 0) | ||
| 168 | printf("reaper: dropped %d dead registration(s), %d waiting\n", | ||
| 169 | nReaped, nBefore - nReaped); | ||
| 170 | fflush(stdout); | ||
| 171 | } | ||
| 172 | |||
| 173 | static void ReaperThread() | ||
| 174 | { | ||
| 175 | while (!g_stop) | ||
| 176 | { | ||
| 177 | for (int i = 0; i < REAP_INTERVAL_SECS && !g_stop; i++) | ||
| 178 | std::this_thread::sleep_for(std::chrono::seconds(1)); | ||
| 179 | if (g_stop) break; | ||
| 180 | ReapDeadRegistrations(); | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 109 | // ---- relay side ---- | 184 | // ---- relay side ---- |
| 110 | 185 | ||
| 111 | // Pump bytes from `from` to `to`. Owns closing `from`; shuts down `to` on exit | 186 | // Pump bytes from `from` to `to`. Owns closing `from`; shuts down `to` on exit |
| @@ -146,7 +221,23 @@ static void HandleIncoming(SOCKET s) | |||
| 146 | { | 221 | { |
| 147 | std::lock_guard<std::mutex> lk(g_mtx); | 222 | std::lock_guard<std::mutex> lk(g_mtx); |
| 148 | std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey); | 223 | std::map<std::string, SOCKET>::iterator it = g_waiting.find(pubkey); |
| 149 | if (it != g_waiting.end()) { svc = it->second; g_waiting.erase(it); } | 224 | if (it != g_waiting.end()) |
| 225 | { | ||
| 226 | // Check before handing it over. Pairing a client with a dead | ||
| 227 | // registration produces a tunnel that fails after both sides | ||
| 228 | // think they succeeded, which is the failure the dialling node | ||
| 229 | // reports as "tunnel to X failed" with nothing to explain it. | ||
| 230 | if (RegistrationDead(it->second)) | ||
| 231 | { | ||
| 232 | CLOSESOCK(it->second); | ||
| 233 | g_waiting.erase(it); | ||
| 234 | } | ||
| 235 | else | ||
| 236 | { | ||
| 237 | svc = it->second; | ||
| 238 | g_waiting.erase(it); | ||
| 239 | } | ||
| 240 | } | ||
| 150 | } | 241 | } |
| 151 | if (svc == INVALID_SOCKET) | 242 | if (svc == INVALID_SOCKET) |
| 152 | { | 243 | { |
| @@ -187,6 +278,8 @@ bool RvRelayRun(unsigned short port) | |||
| 187 | if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; } | 278 | if (bind(g_listen, (struct sockaddr*)&addr, sizeof(addr)) != 0) { CLOSESOCK(g_listen); return false; } |
| 188 | if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; } | 279 | if (listen(g_listen, 16) != 0) { CLOSESOCK(g_listen); return false; } |
| 189 | 280 | ||
| 281 | std::thread(ReaperThread).detach(); | ||
| 282 | |||
| 190 | while (!g_stop) | 283 | while (!g_stop) |
| 191 | { | 284 | { |
| 192 | struct sockaddr_in cli; | 285 | struct sockaddr_in cli; |