upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-30 13:38:12 -0300
committerGitHub <noreply@github.com>2026-07-30 13:38:12 -0300
commit9f7141b28e2da9db7d7cb099d1f13f28742ff0fa (patch)
treea934b2a786639046298b23eef02a4d6468b1d629 /src
parentb539d776bd86df9224d97048f26f33d2750d1c92 (diff)
rendezvous: bound the wait for a dial, so a dead path is noticed (#51)
A node registers at a relay and waits to be dialled. The wait had no bound, and ConnectTo() clears the socket timeout on purpose -- a registration is idle by design and may sit for a long time. What that cannot survive is the path dying with neither end saying so, which is the ordinary case and not the exotic one: a NAT drops the idle mapping, a relay restarts, a route changes. No FIN arrives, nothing becomes readable, and recv() never returns. The thread stays alive and asleep, so ThreadBtfAccept never reaches the `continue` that would register again. The node is unreachable and every outward sign says it is fine. Caught on the bootstrap seed: 27 minutes without a block, eight behind the network, relay reachable the whole time, and a restart fixing it instantly. Nothing in the log marked the moment it went deaf. I tried TCP keepalive first and it does not work here. With probes every 5s and a 2-probe limit, a socket whose replies were being dropped was still ESTAB after four minutes with the probe counter stuck at zero. It is measured, not assumed, and that approach is not in this change. So the bound is ours to enforce, at the one place that knows what waiting means. RvServiceWaitPaired takes a timeout; expiring is not an error but "nobody came, go round again", which the loop above already handles. Cost is one reconnect per relay per five minutes. Verified by killing a registration's path silently -- dropping only the relay's replies, so no RST or FIN is ever seen. With a 60s bound for the test: rendezvous: no dial at 92.246.128.180:8434 within 60s (or it dropped us), re-registering Registrations went 1 -> 2 and the node carried on. Before this, the same treatment left it blocked past four minutes with no log line at all. The socket goes back to blocking once a dial lands, because from that point it is a data tunnel and a tunnel is allowed to sit quiet between messages.
Diffstat (limited to 'src')
-rw-r--r--src/btfrv.cpp51
-rw-r--r--src/btfrv.h7
-rw-r--r--src/net.cpp13
-rw-r--r--src/net.h8
4 files changed, 72 insertions, 7 deletions
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index 63e092e..7efd38b 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -258,10 +258,57 @@ RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
258 return (RvSocket)s; 258 return (RvSocket)s;
259} 259}
260 260
261bool RvServiceWaitPaired(RvSocket s) 261bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs)
262{ 262{
263 // A registration socket is idle on purpose -- we announce ourselves and
264 // wait, possibly for a long time, for someone to dial. ConnectTo() clears
265 // the socket timeout for exactly that reason, and with no timeout at all
266 // this recv() has no way to end when the path dies without either side
267 // saying so: a NAT drops the idle mapping, a relay restarts, a route
268 // changes. No FIN arrives, nothing is readable, and the call never
269 // returns. The thread stays alive and asleep, so ThreadBtfAccept never
270 // reaches the `continue` that would register again, and the node goes deaf
271 // while every outward sign says it is healthy.
272 //
273 // Seen in production on the bootstrap seed: 27 minutes without a block,
274 // eight behind the network, relay reachable throughout, fixed instantly by
275 // a restart.
276 //
277 // TCP keepalive is the obvious answer and it does not work here -- I
278 // measured it. With probes every 5s and a 2-probe limit, a socket whose
279 // replies were being dropped was still ESTAB after four minutes, the probe
280 // counter stuck at zero. So the timeout is ours to enforce, at the one
281 // place that knows what waiting means.
282 //
283 // Expiring is not a failure. It costs one reconnect per interval and
284 // returns the loop to a known state.
285 if (nTimeoutSecs > 0)
286 {
287#ifdef _WIN32
288 DWORD tv = (DWORD)nTimeoutSecs * 1000;
289 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
290#else
291 struct timeval tv; tv.tv_sec = nTimeoutSecs; tv.tv_usec = 0;
292 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
293#endif
294 }
295
263 unsigned char paired = 0; 296 unsigned char paired = 0;
264 return ReadN((SOCKET)s, &paired, 1) && paired == 0x01; 297 bool fOk = ReadN((SOCKET)s, &paired, 1) && paired == 0x01;
298
299 // Hand back a socket that blocks again: from here it is a data tunnel, and
300 // a tunnel legitimately sits quiet between messages.
301 if (fOk && nTimeoutSecs > 0)
302 {
303#ifdef _WIN32
304 DWORD tvOff = 0;
305 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tvOff, sizeof(tvOff));
306#else
307 struct timeval tvOff; tvOff.tv_sec = 0; tvOff.tv_usec = 0;
308 setsockopt((SOCKET)s, SOL_SOCKET, SO_RCVTIMEO, &tvOff, sizeof(tvOff));
309#endif
310 }
311 return fOk;
265} 312}
266 313
267RvSocket RvClientConnect(const char* relay_host, unsigned short port, 314RvSocket RvClientConnect(const char* relay_host, unsigned short port,
diff --git a/src/btfrv.h b/src/btfrv.h
index 9d6756e..9cd7476 100644
--- a/src/btfrv.h
+++ b/src/btfrv.h
@@ -49,7 +49,12 @@ RvSocket RvServiceRegister(const char* relay_host, unsigned short port,
49// Block on a registered service socket until a client is paired to us. Returns 49// Block on a registered service socket until a client is paired to us. Returns
50// false if the relay drops us or errors, in which case the caller should close 50// false if the relay drops us or errors, in which case the caller should close
51// the socket and register again. 51// the socket and register again.
52bool RvServiceWaitPaired(RvSocket s); 52// Blocks until a client is paired with us. Give it a timeout, in seconds, and
53// it returns false once that passes with nobody arriving -- which the caller
54// should treat as "register again", not as an error. Zero means wait forever,
55// which is what this used to do unconditionally and must not be used for a
56// registration socket: see the note on the implementation.
57bool RvServiceWaitPaired(RvSocket s, int nTimeoutSecs = 0);
53 58
54// ---- client side (the node dialing a .btf address) ---- 59// ---- client side (the node dialing a .btf address) ----
55// Connect through the relay to the service registered under `target_pubkey`. 60// Connect through the relay to the service registered under `target_pubkey`.
diff --git a/src/net.cpp b/src/net.cpp
index 395092d..2581578 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -783,12 +783,17 @@ void ThreadBtfAccept(void* parg)
783 LogPrint("net", "rendezvous: registered at %s, waiting for a dial\n", 783 LogPrint("net", "rendezvous: registered at %s, waiting for a dial\n",
784 strMeeting.c_str()); 784 strMeeting.c_str());
785 785
786 // Now block for someone to arrive. 786 // Now wait for someone to arrive -- but not forever. This used to have
787 if (!btf::RvServiceWaitPaired(rv)) 787 // no bound, and a registration whose path died quietly left the thread
788 // parked in recv() with nothing to wake it: the loop never came back
789 // here, the node stopped being reachable, and nothing in the log said
790 // so. Re-registering every few minutes when nobody has dialled costs
791 // one reconnect and removes the whole failure mode.
792 if (!btf::RvServiceWaitPaired(rv, BTF_RENDEZVOUS_WAIT_SECS))
788 { 793 {
789 btf::RvClose(rv); 794 btf::RvClose(rv);
790 LogPrint("net", "rendezvous: %s dropped us before any dial, re-registering\n", 795 LogPrint("net", "rendezvous: no dial at %s within %ds (or it dropped us), "
791 strMeeting.c_str()); 796 "re-registering\n", strMeeting.c_str(), BTF_RENDEZVOUS_WAIT_SECS);
792 continue; 797 continue;
793 } 798 }
794 799
diff --git a/src/net.h b/src/net.h
index 364fbfb..4379d04 100644
--- a/src/net.h
+++ b/src/net.h
@@ -43,6 +43,14 @@ static const unsigned int MAX_PEX_DESCRIPTOR_BYTES = 1024;
43// connection is the intended traffic; anything faster is someone else's idea. 43// connection is the intended traffic; anything faster is someone else's idea.
44static const int64 PEX_MIN_INTERVAL = 60; 44static const int64 PEX_MIN_INTERVAL = 60;
45 45
46// How long to sit registered at a rendezvous relay before giving up on this
47// attempt and registering again. Bounded because a registration socket that
48// goes quiet is indistinguishable, from inside recv(), from one whose path has
49// died -- and the second kind never wakes up. Five minutes is short enough
50// that a node is unreachable only briefly and long enough that the reconnect
51// traffic is nothing: one per relay per five minutes.
52static const int BTF_RENDEZVOUS_WAIT_SECS = 300;
53
46// Descriptors to hand a peer: ours first, then peers that answered us. 54// Descriptors to hand a peer: ours first, then peers that answered us.
47void BtfPexCollect(std::vector<std::string>& vDescOut); 55void BtfPexCollect(std::vector<std::string>& vDescOut);
48// Verify descriptors a peer sent and remember the good ones. Returns how many. 56// Verify descriptors a peer sent and remember the good ones. Returns how many.