upleb.uk

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

summaryrefslogtreecommitdiff
path: root/relay/btfrv.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-31 09:38:10 -0300
committerGitHub <noreply@github.com>2026-07-31 09:38:10 -0300
commit375415e2fb5af01eeea045dd9aa0522844d9b218 (patch)
tree8567d0d98e8e42aba0a48eb4a2735ba8e69a2207 /relay/btfrv.cpp
parentf2ea483df4b2277abe13cc19bcc0bc5834c2e3e6 (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.cpp95
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
39namespace btf 40namespace btf
40{ 41{
@@ -46,6 +47,9 @@ static SOCKET g_listen = INVALID_SOCKET;
46static std::map<std::string, SOCKET> g_waiting; 47static std::map<std::string, SOCKET> g_waiting;
47static std::mutex g_mtx; 48static std::mutex g_mtx;
48 49
50// How often the reaper walks the waiting map.
51static const int REAP_INTERVAL_SECS = 60;
52
49bool RvInit() 53bool 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.
126static 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.
148static 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
173static 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;