upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/net.cpp
diff options
context:
space:
mode:
authorBitflash <btf01@proton.me>2026-07-29 11:52:12 -0300
committerGitHub <noreply@github.com>2026-07-29 11:52:12 -0300
commit96a8e42bbc313f96d54377ae5886da84298e61a3 (patch)
tree36bcd61016ee03c90140b407d4e5863a282e78cf /src/net.cpp
parent7f6fdbab6c90360ab3b2d391a7b5aedc6f9721ac (diff)
Remember .btf peers that answered, and dial them first on start (#25)
* net: remember .btf peers that answered, and dial them first on start Discovery depends entirely on the Nostr relays. A node that has been up for hours knows exactly which peers respond, and throws all of it away on exit; the next start reaches for the relays again and walks the same descriptor list, of which roughly 96% is dead. That is what makes a restart take minutes before the first tunnel opens -- measured today at about seven. Every peer that completes a tunnel is now written to btfpeers.json with its meeting node and encryption key, which is everything needed to redial it. On start a thread tries those before the relays have answered anything, stopping once eight are connected. Entries older than a week are dropped, and the file holds at most fifty. Cache only: nothing here changes how a first-ever run bootstraps, and a missing or corrupt file just falls through to relay discovery. * net: exchange signed .btf descriptors between peers Discovery has depended entirely on the Nostr relays. Two connected nodes never told each other who else existed, so a relay outage left a running network unable to grow, and a node that found one stale peer had no way to learn there was anything better. The old `addr` gossip went out with the IRC seed. Peers now trade signed descriptors over a new "btfpeers" message: our own first, then peers that actually answered us. The receiver checks each Schnorr signature against the key the `.btf` address decodes to, so the sender is trusted for nothing -- forging an entry needs somebody else's secret key. That leaves flooding with self-generated keys, which the per-message caps and the "peers that answered" ordering are there to blunt. Unknown commands are ignored (main.cpp), so nodes without this just drop the message; nothing here is a protocol break. The cache from the peer-cache change now keeps each peer's descriptor verbatim alongside the parsed fields, because that signature is what makes a peer relayable. Entries without one -- resolved through Nostr, or written before this -- stay dialable, just not passed on. A dial never clears a descriptor already on file. * main_gui: implement /port, which net.cpp already documented net.cpp has said nListenPort is "tunable via /port" since it was written, but nothing ever parsed the option. Two nodes could not share a machine, which is exactly what testing peer exchange needs. Sets addrLocalHost.port too, so we do not advertise a port we never bound.
Diffstat (limited to 'src/net.cpp')
-rw-r--r--src/net.cpp280
1 files changed, 280 insertions, 0 deletions
diff --git a/src/net.cpp b/src/net.cpp
index ab07f94..dfeb4a2 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -2,6 +2,13 @@
2// Distributed under the MIT/X11 software license, see the accompanying 2// Distributed under the MIT/X11 software license, see the accompanying
3// file license.txt or http://www.opensource.org/licenses/mit-license.php. 3// file license.txt or http://www.opensource.org/licenses/mit-license.php.
4 4
5#pragma push_macro("snprintf")
6#undef snprintf
7#include <nlohmann/json.hpp>
8#pragma pop_macro("snprintf")
9#ifdef snprintf
10#undef snprintf
11#endif
5#include "headers.h" 12#include "headers.h"
6#ifdef _WIN32 13#ifdef _WIN32
7#include <winsock2.h> 14#include <winsock2.h>
@@ -10,6 +17,7 @@
10#include "btfaddr.h" 17#include "btfaddr.h"
11#include "btftunnel.h" 18#include "btftunnel.h"
12 19
20void ThreadReconnectCachedBtfPeers(void* parg);
13void ThreadMessageHandler2(void* parg); 21void ThreadMessageHandler2(void* parg);
14void ThreadSocketHandler2(void* parg); 22void ThreadSocketHandler2(void* parg);
15 23
@@ -259,6 +267,269 @@ static CAddress BtfMarkerAddr(const unsigned char b5[5])
259// ConnectNodeBtfResolved (which takes an already-resolved descriptor, so it 267// ConnectNodeBtfResolved (which takes an already-resolved descriptor, so it
260// makes no Nostr relay calls at all -- see nostr.cpp's BtfResolveMany for why 268// makes no Nostr relay calls at all -- see nostr.cpp's BtfResolveMany for why
261// that matters when dialing many candidates in parallel). 269// that matters when dialing many candidates in parallel).
270//
271// Peers we have actually reached, remembered across restarts.
272//
273// Discovery is otherwise entirely dependent on the Nostr relays: a node that
274// has been running for hours and knows exactly which peers answer throws all
275// of it away on exit, and the next start walks the same ~96%-dead descriptor
276// list again. Reconnecting from this file skips the relays completely --
277// address, meeting node and encryption key are all we need.
278//
279static const size_t MAX_CACHED_BTF_PEERS = 50;
280static const int64 CACHED_BTF_PEER_TTL = 7 * 24 * 60 * 60; // a week
281
282struct CachedBtfPeer
283{
284 string btfAddr;
285 string meeting;
286 string encHex;
287 int64 lastSeen;
288 // The peer's own signed descriptor, as it announced itself. Kept verbatim
289 // because that signature is what lets us hand this peer on to somebody
290 // else: the receiver checks it against the key the address decodes to and
291 // never has to take our word for anything. Empty for entries learned
292 // before peer exchange existed, or resolved through Nostr -- those are
293 // still dialable, just not relayable.
294 string desc;
295};
296
297static string BtfPeerCachePath() { return GetAppDir() + "/btfpeers.json"; }
298
299static string BytesToHex(const unsigned char* p, size_t n)
300{
301 static const char* h = "0123456789abcdef";
302 string s;
303 s.reserve(n * 2);
304 for (size_t i = 0; i < n; i++) { s += h[p[i] >> 4]; s += h[p[i] & 0xf]; }
305 return s;
306}
307
308static bool HexToBytes(const string& s, unsigned char* out, size_t n)
309{
310 if (s.size() != n * 2) return false;
311 for (size_t i = 0; i < n; i++)
312 {
313 unsigned int b;
314 if (sscanf(s.c_str() + i * 2, "%2x", &b) != 1) return false;
315 out[i] = (unsigned char)b;
316 }
317 return true;
318}
319
320void LoadCachedBtfPeers(vector<CachedBtfPeer>& out)
321{
322 out.clear();
323 FILE* f = fopen(BtfPeerCachePath().c_str(), "r");
324 if (!f) return;
325 string body;
326 char buf[4096];
327 size_t r;
328 while ((r = fread(buf, 1, sizeof(buf), f)) > 0) body.append(buf, r);
329 fclose(f);
330 try
331 {
332 nlohmann::json arr = nlohmann::json::parse(body);
333 if (!arr.is_array()) return;
334 int64 nNow = GetTime();
335 for (const auto& o : arr)
336 {
337 if (!o.is_object()) continue;
338 if (!o.contains("btf") || !o["btf"].is_string()) continue;
339 if (!o.contains("meeting") || !o["meeting"].is_string()) continue;
340 if (!o.contains("enc") || !o["enc"].is_string()) continue;
341 CachedBtfPeer p;
342 p.btfAddr = o["btf"].get<string>();
343 p.meeting = o["meeting"].get<string>();
344 p.encHex = o["enc"].get<string>();
345 p.lastSeen = o.value("seen", (int64)0);
346 if (o.contains("desc") && o["desc"].is_string())
347 p.desc = o["desc"].get<string>();
348 if (nNow - p.lastSeen > CACHED_BTF_PEER_TTL) continue; // long gone
349 out.push_back(p);
350 }
351 }
352 catch (...)
353 {
354 LogPrint("net", "btfpeers: cache unreadable, starting empty\n");
355 out.clear();
356 }
357}
358
359static CCriticalSection cs_btfPeerCache;
360
361// strDesc is the peer's own signed descriptor when we have it (it announced
362// itself over peer exchange), "" when we only resolved it through Nostr. An
363// empty one never clears a descriptor already on file: dialing a peer we first
364// learned about by exchange must not cost us the ability to pass it on.
365static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
366 const unsigned char enc_pub[32],
367 const string& strDesc = string())
368{
369 CRITICAL_BLOCK(cs_btfPeerCache)
370 {
371 vector<CachedBtfPeer> peers;
372 LoadCachedBtfPeers(peers);
373
374 string strKeepDesc = strDesc;
375 for (size_t i = 0; i < peers.size(); i++)
376 if (peers[i].btfAddr == strBtfAddr)
377 {
378 if (strKeepDesc.empty())
379 strKeepDesc = peers[i].desc;
380 peers.erase(peers.begin() + i);
381 break;
382 }
383
384 CachedBtfPeer p;
385 p.btfAddr = strBtfAddr;
386 p.meeting = strMeeting;
387 p.encHex = BytesToHex(enc_pub, 32);
388 p.lastSeen = GetTime();
389 p.desc = strKeepDesc;
390 peers.insert(peers.begin(), p); // most recent first
391
392 if (peers.size() > MAX_CACHED_BTF_PEERS)
393 peers.resize(MAX_CACHED_BTF_PEERS);
394
395 nlohmann::json arr = nlohmann::json::array();
396 foreach(const CachedBtfPeer& q, peers)
397 {
398 nlohmann::json o;
399 o["btf"] = q.btfAddr;
400 o["meeting"] = q.meeting;
401 o["enc"] = q.encHex;
402 o["seen"] = q.lastSeen;
403 if (!q.desc.empty())
404 o["desc"] = q.desc;
405 arr.push_back(o);
406 }
407 FILE* f = fopen(BtfPeerCachePath().c_str(), "w");
408 if (f)
409 {
410 string s = arr.dump(2);
411 fwrite(s.c_str(), 1, s.size(), f);
412 fclose(f);
413 }
414 }
415}
416
417// Dial everything we reached last time, before the relays have said anything.
418void ThreadReconnectCachedBtfPeers(void* parg)
419{
420 vector<CachedBtfPeer> peers;
421 LoadCachedBtfPeers(peers);
422 if (peers.empty())
423 {
424 LogPrint("net", "btfpeers: no cache yet, waiting on relay discovery\n");
425 return;
426 }
427 LogPrint("net", "btfpeers: trying %zu remembered peer(s) before discovery\n",
428 peers.size());
429
430 int nConnected = 0;
431 foreach(const CachedBtfPeer& p, peers)
432 {
433 if (fShutdown) return;
434 unsigned char enc[32];
435 if (!HexToBytes(p.encHex, enc, 32)) continue;
436 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, enc))
437 {
438 nConnected++;
439 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait
440 }
441 }
442 LogPrint("net", "btfpeers: %d of %zu remembered peer(s) answered\n",
443 nConnected, peers.size());
444}
445
446//
447// .btf peer exchange
448//
449// Until now discovery depended entirely on the Nostr relays: two connected
450// nodes never told each other who else existed, so a relay outage left a
451// running network unable to grow, and a node that found one stale peer had no
452// way to learn there was anything better.
453//
454// What travels here is each peer's own signed descriptor. The receiver checks
455// the Schnorr signature against the key the `.btf` address decodes to, so the
456// sender is trusted for nothing -- forging an entry would need somebody else's
457// secret key. That leaves flooding: valid descriptors for keys the sender
458// generated itself. The caps below bound one message, and the cache prefers
459// peers that actually answered, so a flood costs a Sybil more than it costs us.
460//
461
462void BtfPexCollect(vector<string>& vDescOut)
463{
464 vDescOut.clear();
465
466 // Ours first -- the node on the other end may know nobody but us.
467 string strMine = BtfLocalDescriptor();
468 if (!strMine.empty())
469 vDescOut.push_back(strMine);
470
471 vector<CachedBtfPeer> peers;
472 CRITICAL_BLOCK(cs_btfPeerCache)
473 LoadCachedBtfPeers(peers);
474
475 // Most recently seen first: these answered us, they are not names copied
476 // off a relay listing.
477 foreach(const CachedBtfPeer& p, peers)
478 {
479 if (vDescOut.size() >= MAX_PEX_DESCRIPTORS) break;
480 if (p.desc.empty()) continue; // nothing provable to pass on
481 if (p.desc.size() > MAX_PEX_DESCRIPTOR_BYTES) continue;
482 vDescOut.push_back(p.desc);
483 }
484}
485
486int BtfPexAccept(const vector<string>& vDesc)
487{
488 void* ctx = BtfSecpContext();
489 if (!ctx)
490 return 0;
491
492 string strSelf = BtfLocalAddress();
493 int nKept = 0;
494 unsigned int nSeen = 0;
495
496 foreach(const string& strDesc, vDesc)
497 {
498 if (++nSeen > MAX_PEX_DESCRIPTORS) break;
499 if (strDesc.empty() || strDesc.size() > MAX_PEX_DESCRIPTOR_BYTES) continue;
500
501 // The descriptor names the key it belongs to, and taking that key from
502 // the blob is safe precisely because the signature must verify under
503 // it: nobody can produce a valid descriptor for a key they don't hold.
504 unsigned char pubkey[32];
505 try
506 {
507 nlohmann::json v = nlohmann::json::parse(strDesc);
508 if (!v.is_object() || !v.contains("pubkey") || !v["pubkey"].is_string())
509 continue;
510 if (!HexToBytes(v["pubkey"].get<string>(), pubkey, 32))
511 continue;
512 }
513 catch (...) { continue; } // malformed JSON from an untrusted peer
514
515 btf::Descriptor d;
516 if (!btf::VerifyDescriptor(ctx, strDesc, pubkey, d))
517 continue;
518
519 string strAddr = btf::Address(pubkey);
520 if (strAddr.empty() || strAddr == strSelf)
521 continue; // ourselves, nothing to learn
522
523 unsigned char enc[32];
524 if (!HexToBytes(d.enc, enc, 32))
525 continue;
526
527 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc);
528 nKept++;
529 }
530 return nKept;
531}
532
262static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], 533static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32],
263 const string& strMeeting, const unsigned char enc_pub[32]) 534 const string& strMeeting, const unsigned char enc_pub[32])
264{ 535{
@@ -289,6 +560,9 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
289 if (fDebug) 560 if (fDebug)
290 LogPrint("net", "connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str()); 561 LogPrint("net", "connected %s via rendezvous %s\n", strBtfAddr.c_str(), strMeeting.c_str());
291 562
563 // This one answered -- worth trying first next time we start.
564 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub);
565
292 // Add node 566 // Add node
293 pnode = new CNode(hSocket, addr, false); 567 pnode = new CNode(hSocket, addr, false);
294 pnode->AddRef(); 568 pnode->AddRef();
@@ -951,6 +1225,12 @@ bool StartNode(string& strError)
951 } 1225 }
952 }, 0, NULL); 1226 }, 0, NULL);
953 1227
1228 // Peers that answered last time, dialled straight away. The relays are the
1229 // only way to find anyone otherwise, and reaching them plus walking their
1230 // descriptor list is what makes a restart take minutes.
1231 if (_beginthread(ThreadReconnectCachedBtfPeers, 0, NULL) == -1)
1232 printf("Error: _beginthread(ThreadReconnectCachedBtfPeers) failed\n");
1233
954 // Peer discovery over Nostr relays (replaces the old IRC seed) 1234 // Peer discovery over Nostr relays (replaces the old IRC seed)
955 if (_beginthread(ThreadNostrSeed, 0, NULL) == -1) 1235 if (_beginthread(ThreadNostrSeed, 0, NULL) == -1)
956 printf("Error: _beginthread(ThreadNostrSeed) failed\n"); 1236 printf("Error: _beginthread(ThreadNostrSeed) failed\n");