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:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-13 11:11:59 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-13 11:11:59 -0300
commitd2b86473a267c960a1f0c012a983176e5d962187 (patch)
tree002423e5987a3c544f469befae83519ccb29fffb /src
parent851811b43be38bea48ba5d04d16987bd541dca53 (diff)
net: add signed direct onion peer endpoints
Diffstat (limited to 'src')
-rw-r--r--src/btfaddr.cpp102
-rw-r--r--src/btfaddr.h8
-rw-r--r--src/main_gui.cpp15
-rw-r--r--src/net.cpp102
-rw-r--r--src/net.h3
-rw-r--r--src/nostr.cpp76
-rw-r--r--src/nostr.h12
-rw-r--r--src/readme.txt8
-rw-r--r--src/selftest.cpp9
-rw-r--r--src/sockcount.h1
-rw-r--r--src/test_btfaddr.cpp22
11 files changed, 311 insertions, 47 deletions
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp
index 6edcc91..ce2ac86 100644
--- a/src/btfaddr.cpp
+++ b/src/btfaddr.cpp
@@ -19,6 +19,8 @@
19#include <cstring> 19#include <cstring>
20#include <algorithm> 20#include <algorithm>
21#include <cctype> 21#include <cctype>
22#include <cerrno>
23#include <cstdlib>
22 24
23using json = nlohmann::json; 25using json = nlohmann::json;
24 26
@@ -166,10 +168,47 @@ static bool HexDecode(const std::string& s, unsigned char* out, size_t n)
166 return true; 168 return true;
167} 169}
168 170
171bool NormalizeOnionEndpoint(const std::string& in, std::string& out)
172{
173 out.clear();
174 std::string s = ToLowerTrim(in);
175 size_t colon = s.rfind(':');
176 if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size())
177 return false;
178
179 std::string host = s.substr(0, colon);
180 std::string port = s.substr(colon + 1);
181 while (!host.empty() && host.back() == '.') host.pop_back();
182 static const char* suffix = ".onion";
183 const size_t suffixLen = strlen(suffix);
184 if (host.size() != 56 + suffixLen)
185 return false;
186 if (host.compare(host.size() - suffixLen, suffixLen, suffix) != 0)
187 return false;
188 for (size_t i = 0; i < 56; i++)
189 {
190 char c = host[i];
191 if (!((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7')))
192 return false;
193 }
194 for (size_t i = 0; i < port.size(); i++)
195 if (!isdigit((unsigned char)port[i]))
196 return false;
197
198 errno = 0;
199 char* end = NULL;
200 long nPort = strtol(port.c_str(), &end, 10);
201 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
202 return false;
203
204 out = host + ":" + port;
205 return true;
206}
207
169// Digest signed over the descriptor fields (domain-separated). 208// Digest signed over the descriptor fields (domain-separated).
170static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, 209static void DescriptorDigestV2(const unsigned char pubkey[32], const std::string& enc,
171 const std::string& meeting_node, uint64_t created, 210 const std::string& meeting_node, uint64_t created,
172 unsigned char out[32]) 211 unsigned char out[32])
173{ 212{
174 static const char* DS = ".btf-descriptor-v2"; 213 static const char* DS = ".btf-descriptor-v2";
175 SHA256_CTX ctx; 214 SHA256_CTX ctx;
@@ -184,9 +223,27 @@ static void DescriptorDigest(const unsigned char pubkey[32], const std::string&
184 SHA256_Final(out, &ctx); 223 SHA256_Final(out, &ctx);
185} 224}
186 225
226static void DescriptorDigestV3(const unsigned char pubkey[32], const std::string& enc,
227 const std::string& meeting_node, const std::string& onion,
228 uint64_t created, unsigned char out[32])
229{
230 static const char* DS = ".btf-descriptor-v3-onion";
231 SHA256_CTX ctx;
232 SHA256_Init(&ctx);
233 SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS));
234 SHA256_Update(&ctx, pubkey, 32);
235 SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size());
236 SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size());
237 SHA256_Update(&ctx, (const unsigned char*)onion.data(), onion.size());
238 unsigned char be[8];
239 for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff);
240 SHA256_Update(&ctx, be, 8);
241 SHA256_Final(out, &ctx);
242}
243
187std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], 244std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
188 const std::string& enc, const std::string& meeting_node, 245 const std::string& enc, const std::string& meeting_node,
189 uint64_t created) 246 uint64_t created, const std::string& onion)
190{ 247{
191 secp256k1_context* ctx = (secp256k1_context*)ctxv; 248 secp256k1_context* ctx = (secp256k1_context*)ctxv;
192 secp256k1_keypair kp; 249 secp256k1_keypair kp;
@@ -197,17 +254,28 @@ std::string SignDescriptor(void* ctxv, const unsigned char seckey[32],
197 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); 254 secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub);
198 255
199 unsigned char digest[32]; 256 unsigned char digest[32];
200 DescriptorDigest(pubkey, enc, meeting_node, created, digest); 257 DescriptorDigestV2(pubkey, enc, meeting_node, created, digest);
201 258
202 unsigned char sig[64]; 259 unsigned char sig[64];
203 // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. 260 // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing.
204 if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); 261 if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string();
205 262
206 json v = json::object(); 263 json v = json::object();
207 v["v"] = 2; 264 v["v"] = onion.empty() ? 2 : 3;
208 v["pubkey"] = HexEncode(pubkey, 32); 265 v["pubkey"] = HexEncode(pubkey, 32);
209 v["enc"] = enc; 266 v["enc"] = enc;
210 v["meeting_node"] = meeting_node; 267 v["meeting_node"] = meeting_node;
268 if (!onion.empty())
269 {
270 std::string onionNorm;
271 if (!NormalizeOnionEndpoint(onion, onionNorm)) return std::string();
272 unsigned char digest3[32];
273 DescriptorDigestV3(pubkey, enc, meeting_node, onionNorm, created, digest3);
274 unsigned char sig3[64];
275 if (!secp256k1_schnorrsig_sign32(ctx, sig3, digest3, &kp, NULL)) return std::string();
276 v["onion"] = onionNorm;
277 v["sig3"] = HexEncode(sig3, 64);
278 }
211 v["created"] = created; 279 v["created"] = created;
212 v["sig"] = HexEncode(sig, 64); 280 v["sig"] = HexEncode(sig, 64);
213 return v.dump(); 281 return v.dump();
@@ -242,15 +310,35 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr,
242 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; 310 if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false;
243 311
244 unsigned char digest[32]; 312 unsigned char digest[32];
245 DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest); 313 DescriptorDigestV2(expect_pubkey, enc, meeting_node, created, digest);
246 314
247 secp256k1_xonly_pubkey xpub; 315 secp256k1_xonly_pubkey xpub;
248 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; 316 if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false;
249 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; 317 if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false;
250 318
319 std::string onion;
320 if (v.contains("onion"))
321 {
322 if (v["onion"].is_string() && v.contains("sig3") && v["sig3"].is_string())
323 {
324 std::string onionNorm;
325 std::string onionRaw = v["onion"].get<std::string>();
326 if (NormalizeOnionEndpoint(onionRaw, onionNorm) && onionNorm == onionRaw)
327 {
328 unsigned char sig3[64];
329 unsigned char digest3[32];
330 DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3);
331 if (HexDecode(v["sig3"].get<std::string>(), sig3, 64) &&
332 secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub))
333 onion = onionNorm;
334 }
335 }
336 }
337
251 memcpy(out.pubkey, expect_pubkey, 32); 338 memcpy(out.pubkey, expect_pubkey, 32);
252 out.enc = enc; 339 out.enc = enc;
253 out.meeting_node = meeting_node; 340 out.meeting_node = meeting_node;
341 out.onion = onion;
254 out.created = created; 342 out.created = created;
255 return true; 343 return true;
256 } 344 }
diff --git a/src/btfaddr.h b/src/btfaddr.h
index 1addf8f..55a3ebd 100644
--- a/src/btfaddr.h
+++ b/src/btfaddr.h
@@ -52,15 +52,21 @@ struct Descriptor
52 unsigned char pubkey[32]; 52 unsigned char pubkey[32];
53 std::string enc; // service x25519 public key (hex) for the E2E channel 53 std::string enc; // service x25519 public key (hex) for the E2E channel
54 std::string meeting_node; // rendezvous node the service is registered at 54 std::string meeting_node; // rendezvous node the service is registered at
55 std::string onion; // optional direct Tor hidden-service endpoint (host.onion:port)
55 uint64_t created; 56 uint64_t created;
56}; 57};
57 58
59// Normalize and validate a direct Tor hidden-service endpoint. Only v3 onion
60// names are accepted: 56 base32 chars plus ".onion", followed by ":PORT".
61bool NormalizeOnionEndpoint(const std::string& in, std::string& out);
62
58// Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this 63// Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this
59// header free of secp headers); `seckey` is the 32-byte service secret key. 64// header free of secp headers); `seckey` is the 32-byte service secret key.
60// Returns the descriptor JSON as a string, or "" on failure. 65// Returns the descriptor JSON as a string, or "" on failure.
61std::string SignDescriptor(void* ctx, const unsigned char seckey[32], 66std::string SignDescriptor(void* ctx, const unsigned char seckey[32],
62 const std::string& enc, const std::string& meeting_node, 67 const std::string& enc, const std::string& meeting_node,
63 uint64_t created); 68 uint64_t created,
69 const std::string& onion = std::string());
64 70
65// Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved 71// Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved
66// `.btf` address encodes). Returns true and fills `out` on success. 72// `.btf` address encodes). Returns true and fills `out` on success.
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index bef8503..239fb60 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -1,6 +1,7 @@
1// Bitflash entry point -- starts node threads then runs GUI (or headless). 1// Bitflash entry point -- starts node threads then runs GUI (or headless).
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include "nostr.h"
4#include "proxy.h" 5#include "proxy.h"
5#include "selftest.h" 6#include "selftest.h"
6#include "walletcmd.h" 7#include "walletcmd.h"
@@ -134,10 +135,11 @@ static void PrintUsage()
134 printf(" /connectbtf=PEER_BTF_ADDRESS\n"); 135 printf(" /connectbtf=PEER_BTF_ADDRESS\n");
135 printf(" /rvrelay=HOST:PORT\n"); 136 printf(" /rvrelay=HOST:PORT\n");
136 printf(" /announcerelay=HOST:PORT\n"); 137 printf(" /announcerelay=HOST:PORT\n");
138 printf(" /onionservice=HOST.onion:PORT (advertise this node's Tor hidden service)\n");
137 printf("\n"); 139 printf("\n");
138 printf("Network:\n"); 140 printf("Network:\n");
139 printf(" /port=N (P2P listen port, default 8433)\n"); 141 printf(" /port=N (P2P listen port, default 8433)\n");
140 printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n"); 142 printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n");
141 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); 143 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
142 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 144 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
143 printf("\n"); 145 printf("\n");
@@ -286,6 +288,17 @@ static void ParseStartupArguments(int argc, char* argv[])
286 if (!announceRelay.empty()) 288 if (!announceRelay.empty())
287 strBtfAnnounceRelay = announceRelay; 289 strBtfAnnounceRelay = announceRelay;
288 290
291 string onionService = argval2(argc, argv, "/onionservice", "-onionservice");
292 if (!onionService.empty())
293 {
294 string err;
295 if (!BtfSetLocalOnionEndpoint(onionService, err))
296 fprintf(stderr, "Ignoring /onionservice=%s: %s\n", onionService.c_str(), err.c_str());
297 else
298 fprintf(stderr, "Direct onion peer endpoint advertised: %s\n",
299 BtfLocalOnionEndpoint().c_str());
300 }
301
289 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); 302 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
290 if (fTorMode) 303 if (fTorMode)
291 { 304 {
diff --git a/src/net.cpp b/src/net.cpp
index c24547e..ab17332 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -17,6 +17,8 @@
17#include <poll.h> 17#include <poll.h>
18#endif 18#endif
19#include <openssl/rand.h> 19#include <openssl/rand.h>
20#include <cerrno>
21#include <cstdlib>
20#include "btfaddr.h" 22#include "btfaddr.h"
21#include "btftunnel.h" 23#include "btftunnel.h"
22#include "proxy.h" 24#include "proxy.h"
@@ -170,7 +172,7 @@ static const char* pszSockSite[SOCK_SITES] = {
170 "external-ip probe", "listen socket", "inbound accept", 172 "external-ip probe", "listen socket", "inbound accept",
171 "rendezvous dial", "rendezvous listen", "rendezvous accept", 173 "rendezvous dial", "rendezvous listen", "rendezvous accept",
172 "loopback pair listener", "loopback pair app end", "loopback pair pump end", 174 "loopback pair listener", "loopback pair app end", "loopback pair pump end",
173 "nostr relay", "stratum bridge" 175 "nostr relay", "direct onion peer", "stratum bridge"
174}; 176};
175 177
176static string SockAccountingText() 178static string SockAccountingText()
@@ -627,6 +629,7 @@ struct CachedBtfPeer
627{ 629{
628 string btfAddr; 630 string btfAddr;
629 string meeting; 631 string meeting;
632 string onion;
630 string encHex; 633 string encHex;
631 int64 lastSeen; 634 int64 lastSeen;
632 // The peer's own signed descriptor, as it announced itself. Kept verbatim 635 // The peer's own signed descriptor, as it announced itself. Kept verbatim
@@ -685,6 +688,8 @@ void LoadCachedBtfPeers(vector<CachedBtfPeer>& out)
685 CachedBtfPeer p; 688 CachedBtfPeer p;
686 p.btfAddr = o["btf"].get<string>(); 689 p.btfAddr = o["btf"].get<string>();
687 p.meeting = o["meeting"].get<string>(); 690 p.meeting = o["meeting"].get<string>();
691 if (o.contains("onion") && o["onion"].is_string())
692 btf::NormalizeOnionEndpoint(o["onion"].get<string>(), p.onion);
688 p.encHex = o["enc"].get<string>(); 693 p.encHex = o["enc"].get<string>();
689 p.lastSeen = o.value("seen", (int64)0); 694 p.lastSeen = o.value("seen", (int64)0);
690 if (o.contains("desc") && o["desc"].is_string()) 695 if (o.contains("desc") && o["desc"].is_string())
@@ -708,7 +713,8 @@ static CCriticalSection cs_btfPeerCache;
708// learned about by exchange must not cost us the ability to pass it on. 713// learned about by exchange must not cost us the ability to pass it on.
709static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, 714static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
710 const unsigned char enc_pub[32], 715 const unsigned char enc_pub[32],
711 const string& strDesc = string()) 716 const string& strDesc = string(),
717 const string& strOnion = string())
712{ 718{
713 CRITICAL_BLOCK(cs_btfPeerCache) 719 CRITICAL_BLOCK(cs_btfPeerCache)
714 { 720 {
@@ -716,11 +722,15 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
716 LoadCachedBtfPeers(peers); 722 LoadCachedBtfPeers(peers);
717 723
718 string strKeepDesc = strDesc; 724 string strKeepDesc = strDesc;
725 string strKeepOnion;
726 btf::NormalizeOnionEndpoint(strOnion, strKeepOnion);
719 for (size_t i = 0; i < peers.size(); i++) 727 for (size_t i = 0; i < peers.size(); i++)
720 if (peers[i].btfAddr == strBtfAddr) 728 if (peers[i].btfAddr == strBtfAddr)
721 { 729 {
722 if (strKeepDesc.empty()) 730 if (strKeepDesc.empty())
723 strKeepDesc = peers[i].desc; 731 strKeepDesc = peers[i].desc;
732 if (strKeepOnion.empty())
733 strKeepOnion = peers[i].onion;
724 peers.erase(peers.begin() + i); 734 peers.erase(peers.begin() + i);
725 break; 735 break;
726 } 736 }
@@ -728,6 +738,7 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
728 CachedBtfPeer p; 738 CachedBtfPeer p;
729 p.btfAddr = strBtfAddr; 739 p.btfAddr = strBtfAddr;
730 p.meeting = strMeeting; 740 p.meeting = strMeeting;
741 p.onion = strKeepOnion;
731 p.encHex = BytesToHex(enc_pub, 32); 742 p.encHex = BytesToHex(enc_pub, 32);
732 p.lastSeen = GetTime(); 743 p.lastSeen = GetTime();
733 p.desc = strKeepDesc; 744 p.desc = strKeepDesc;
@@ -742,6 +753,8 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting,
742 nlohmann::json o; 753 nlohmann::json o;
743 o["btf"] = q.btfAddr; 754 o["btf"] = q.btfAddr;
744 o["meeting"] = q.meeting; 755 o["meeting"] = q.meeting;
756 if (!q.onion.empty())
757 o["onion"] = q.onion;
745 o["enc"] = q.encHex; 758 o["enc"] = q.encHex;
746 o["seen"] = q.lastSeen; 759 o["seen"] = q.lastSeen;
747 if (!q.desc.empty()) 760 if (!q.desc.empty())
@@ -830,7 +843,7 @@ static int TryBtfSeeds()
830 foreach(const string& strRelay, relays) 843 foreach(const string& strRelay, relays)
831 { 844 {
832 if (fShutdown) return nConnected; 845 if (fShutdown) return nConnected;
833 if (ConnectNodeBtfResolved(s.first, strRelay, enc)) 846 if (ConnectNodeBtfResolved(s.first, strRelay, "", enc))
834 { 847 {
835 LogPrint("net", "btfseed: reached %s via %s\n", 848 LogPrint("net", "btfseed: reached %s via %s\n",
836 s.first.c_str(), strRelay.c_str()); 849 s.first.c_str(), strRelay.c_str());
@@ -868,7 +881,7 @@ void ThreadReconnectCachedBtfPeers(void* parg)
868 if (fShutdown) return; 881 if (fShutdown) return;
869 unsigned char enc[32]; 882 unsigned char enc[32];
870 if (!HexToBytes(p.encHex, enc, 32)) continue; 883 if (!HexToBytes(p.encHex, enc, 32)) continue;
871 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, enc)) 884 if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, p.onion, enc))
872 { 885 {
873 nConnected++; 886 nConnected++;
874 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait 887 if (nConnected >= 8) break; // enough to bootstrap; the rest can wait
@@ -959,14 +972,37 @@ int BtfPexAccept(const vector<string>& vDesc)
959 if (!HexToBytes(d.enc, enc, 32)) 972 if (!HexToBytes(d.enc, enc, 32))
960 continue; 973 continue;
961 974
962 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc); 975 RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc, d.onion);
963 nKept++; 976 nKept++;
964 } 977 }
965 return nKept; 978 return nKept;
966} 979}
967 980
981static bool SplitHostPort(const string& strHostPort, string& strHost, unsigned short& nPort)
982{
983 size_t colon = strHostPort.rfind(':');
984 if (colon == string::npos || colon == 0 || colon + 1 >= strHostPort.size())
985 return false;
986 string port = strHostPort.substr(colon + 1);
987 for (size_t i = 0; i < port.size(); i++)
988 if (!isdigit((unsigned char)port[i]))
989 return false;
990
991 errno = 0;
992 char* end = NULL;
993 long n = strtol(port.c_str(), &end, 10);
994 if (errno == ERANGE || end == NULL || *end != '\0' || n <= 0 || n > 65535)
995 return false;
996
997 strHost = strHostPort.substr(0, colon);
998 nPort = (unsigned short)n;
999 return true;
1000}
1001
968static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], 1002static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32],
969 const string& strMeeting, const unsigned char enc_pub[32]) 1003 const string& strMeeting, const string& strOnion,
1004 const unsigned char enc_pub[32],
1005 const string& strDesc = string())
970{ 1006{
971 CAddress addr = BtfMarkerAddr(pk); 1007 CAddress addr = BtfMarkerAddr(pk);
972 CNode* pnode = FindNode(addr.ip); 1008 CNode* pnode = FindNode(addr.ip);
@@ -976,16 +1012,42 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
976 return pnode; 1012 return pnode;
977 } 1013 }
978 1014
979 BtfChurnNoteDialAttempt(strBtfAddr, strMeeting); 1015 string strOnionNorm;
980 size_t colon = strMeeting.rfind(':'); 1016 if (BtfSocks5ProxyEnabled() && btf::NormalizeOnionEndpoint(strOnion, strOnionNorm))
981 if (colon == string::npos)
982 { 1017 {
983 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); 1018 string strOnionHost;
984 return NULL; 1019 unsigned short nOnionPort = 0;
1020 if (SplitHostPort(strOnionNorm, strOnionHost, nOnionPort))
1021 {
1022 BtfChurnNoteDialAttempt(strBtfAddr, strOnionNorm);
1023 SOCKET hOnionSocket = BtfConnectSocket(strOnionHost, nOnionPort, SOCK_ONION_PEER, 20);
1024 if (hOnionSocket != INVALID_SOCKET)
1025 {
1026 if (fDebug)
1027 LogPrint("net", "connected %s via direct onion %s\n",
1028 strBtfAddr.c_str(), strOnionNorm.c_str());
1029 BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, true);
1030 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm);
1031
1032 pnode = new CNode(hOnionSocket, addr, false);
1033 pnode->strBtfAddr = strBtfAddr;
1034 pnode->strBtfMeeting = string("onion ") + strOnionNorm;
1035 pnode->AddRef();
1036 CRITICAL_BLOCK(cs_vNodes)
1037 vNodes.push_back(pnode);
1038 return pnode;
1039 }
1040 if (fDebug)
1041 LogPrint("net", "ConnectNodeBtf: direct onion to %s at %s failed, falling back to rendezvous\n",
1042 strBtfAddr.c_str(), strOnionNorm.c_str());
1043 BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, false);
1044 }
985 } 1045 }
986 string strHost = strMeeting.substr(0, colon); 1046
987 int nPort = atoi(strMeeting.substr(colon + 1).c_str()); 1047 BtfChurnNoteDialAttempt(strBtfAddr, strMeeting);
988 if (nPort <= 0 || nPort > 65535) 1048 string strHost;
1049 unsigned short nPort = 0;
1050 if (!SplitHostPort(strMeeting, strHost, nPort))
989 { 1051 {
990 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); 1052 BtfChurnNoteDialResult(strBtfAddr, strMeeting, false);
991 return NULL; 1053 return NULL;
@@ -1005,7 +1067,7 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p
1005 BtfChurnNoteDialResult(strBtfAddr, strMeeting, true); 1067 BtfChurnNoteDialResult(strBtfAddr, strMeeting, true);
1006 1068
1007 // This one answered -- worth trying first next time we start. 1069 // This one answered -- worth trying first next time we start.
1008 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub); 1070 RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm);
1009 1071
1010 // Add node 1072 // Add node
1011 pnode = new CNode(hSocket, addr, false); 1073 pnode = new CNode(hSocket, addr, false);
@@ -1035,9 +1097,10 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
1035 LogPrint("net", "trying %s\n", strBtfAddr.c_str()); 1097 LogPrint("net", "trying %s\n", strBtfAddr.c_str());
1036 1098
1037 string strMeeting; 1099 string strMeeting;
1100 string strOnion;
1038 unsigned char enc_pub[32]; 1101 unsigned char enc_pub[32];
1039 BtfChurnNoteResolveAttempt(); 1102 BtfChurnNoteResolveAttempt();
1040 if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) 1103 if (!BtfResolve(strBtfAddr, strMeeting, enc_pub, &strOnion))
1041 { 1104 {
1042 BtfChurnNoteResolveResult(false); 1105 BtfChurnNoteResolveResult(false);
1043 if (fDebug) 1106 if (fDebug)
@@ -1045,7 +1108,7 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
1045 return NULL; 1108 return NULL;
1046 } 1109 }
1047 BtfChurnNoteResolveResult(true); 1110 BtfChurnNoteResolveResult(true);
1048 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); 1111 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub);
1049} 1112}
1050 1113
1051// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller 1114// Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller
@@ -1054,7 +1117,8 @@ CNode* ConnectNodeBtf(const string& strBtfAddr)
1054// address). Makes zero relay connections -- only talks to the peer's 1117// address). Makes zero relay connections -- only talks to the peer's
1055// rendezvous meeting node. 1118// rendezvous meeting node.
1056CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, 1119CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting,
1057 const unsigned char enc_pub[32]) 1120 const string& strOnion, const unsigned char enc_pub[32],
1121 const string& strDesc)
1058{ 1122{
1059 unsigned char pk[32]; 1123 unsigned char pk[32];
1060 if (!btf::ParseAddress(strBtfAddr, pk)) 1124 if (!btf::ParseAddress(strBtfAddr, pk))
@@ -1064,7 +1128,7 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting
1064 } 1128 }
1065 if (BtfLocalAddress() == strBtfAddr) 1129 if (BtfLocalAddress() == strBtfAddr)
1066 return NULL; // ourselves 1130 return NULL; // ourselves
1067 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); 1131 return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub, strDesc);
1068} 1132}
1069 1133
1070// Anonymous inbound listener (this node's `.btf` hidden service). Registers at 1134// Anonymous inbound listener (this node's `.btf` hidden service). Registers at
diff --git a/src/net.h b/src/net.h
index feecf96..9300169 100644
--- a/src/net.h
+++ b/src/net.h
@@ -28,7 +28,8 @@ enum
28bool GetMyExternalIP(unsigned int& ipRet); 28bool GetMyExternalIP(unsigned int& ipRet);
29CNode* ConnectNodeBtf(const string& strBtfAddr); 29CNode* ConnectNodeBtf(const string& strBtfAddr);
30CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, 30CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting,
31 const unsigned char enc_pub[32]); 31 const string& strOnion, const unsigned char enc_pub[32],
32 const string& strDesc=string());
32void ThreadBtfAccept(void* parg); 33void ThreadBtfAccept(void* parg);
33void ThreadBtfConnect(void* parg); 34void ThreadBtfConnect(void* parg);
34extern string strBtfConnect; 35extern string strBtfConnect;
diff --git a/src/nostr.cpp b/src/nostr.cpp
index d286767..0c1b4b6 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -112,6 +112,8 @@ vector<string> vBtfMeetingRelays = {
112}; 112};
113string strBtfActiveRelay; 113string strBtfActiveRelay;
114static CCriticalSection cs_activeRelay; 114static CCriticalSection cs_activeRelay;
115static string strBtfOnionEndpoint;
116static CCriticalSection cs_onionEndpoint;
115 117
116// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from 118// Only the rendezvous we actually registered at (BtfSetActiveRelay, called from
117// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it 119// ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it
@@ -133,6 +135,26 @@ void BtfSetActiveRelay(const string& relay)
133 strBtfActiveRelay = relay; 135 strBtfActiveRelay = relay;
134} 136}
135 137
138bool BtfSetLocalOnionEndpoint(const string& endpoint, string& errOut)
139{
140 string norm;
141 if (!btf::NormalizeOnionEndpoint(endpoint, norm))
142 {
143 errOut = "expected v3 onion endpoint HOST.onion:PORT";
144 return false;
145 }
146 CRITICAL_BLOCK(cs_onionEndpoint)
147 strBtfOnionEndpoint = norm;
148 return true;
149}
150
151string BtfLocalOnionEndpoint()
152{
153 CRITICAL_BLOCK(cs_onionEndpoint)
154 return strBtfOnionEndpoint;
155 return "";
156}
157
136// A relay this node advertises on Nostr for others to discover (/announcerelay). 158// A relay this node advertises on Nostr for others to discover (/announcerelay).
137string strBtfAnnounceRelay; 159string strBtfAnnounceRelay;
138// Relays learned from other operators' Nostr announcements. 160// Relays learned from other operators' Nostr announcements.
@@ -370,10 +392,16 @@ std::string BtfLocalDescriptor()
370 string meeting; 392 string meeting;
371 CRITICAL_BLOCK(cs_activeRelay) 393 CRITICAL_BLOCK(cs_activeRelay)
372 meeting = strBtfActiveRelay; 394 meeting = strBtfActiveRelay;
395 string onion = BtfLocalOnionEndpoint();
373 if (meeting.empty()) 396 if (meeting.empty())
374 return ""; 397 {
398 if (onion.empty())
399 return "";
400 meeting = "rendezvous-pending";
401 }
375 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, 402 return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey,
376 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime()); 403 g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime(),
404 onion);
377} 405}
378 406
379// The shared secp256k1 context, so net.cpp can verify descriptors that arrive 407// The shared secp256k1 context, so net.cpp can verify descriptors that arrive
@@ -806,17 +834,21 @@ static void PublishDescriptor(CWebSocket& ws, CNostrKey& key)
806 // meeting_node is the rendezvous relay this node's hidden service 834 // meeting_node is the rendezvous relay this node's hidden service
807 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us. 835 // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us.
808 string meeting_node = BtfActiveRelay(); 836 string meeting_node = BtfActiveRelay();
837 string onion = BtfLocalOnionEndpoint();
809 if (meeting_node.empty()) 838 if (meeting_node.empty())
810 { 839 {
811 // Not registered anywhere yet. Publishing now would advertise a meeting 840 if (onion.empty())
812 // node we can't be reached at, and peers would waste dials on it. The 841 {
813 // ThreadNostrSeed loop republishes on every pass, so skipping costs 842 // Not registered anywhere yet, and no direct onion endpoint exists.
814 // nothing but the delay until ThreadBtfAccept registers. 843 // Publishing now would advertise a meeting node we can't be reached at.
815 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); 844 LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n");
816 return; 845 return;
846 }
847 meeting_node = "rendezvous-pending";
817 } 848 }
818 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(), 849 string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(),
819 meeting_node, (uint64_t)GetTime()); 850 meeting_node, (uint64_t)GetTime(),
851 onion);
820 if (desc.empty()) 852 if (desc.empty())
821 return; 853 return;
822 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) }); 854 json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) });
@@ -1018,7 +1050,7 @@ static void HandlePoolAnnouncement(const json& ev)
1018// Resolve a `.btf` address via a relay: fetch the descriptor published by that 1050// Resolve a `.btf` address via a relay: fetch the descriptor published by that
1019// address's key and verify it self-certifies under the decoded pubkey. 1051// address's key and verify it self-certifies under the decoded pubkey.
1020static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, 1052static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1021 btf::Descriptor& out) 1053 btf::Descriptor& out, string* rawOut = NULL)
1022{ 1054{
1023 unsigned char pk[32]; 1055 unsigned char pk[32];
1024 if (!btf::ParseAddress(btfAddr, pk)) 1056 if (!btf::ParseAddress(btfAddr, pk))
@@ -1045,7 +1077,11 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1045 const json& ev = j[2]; 1077 const json& ev = j[2];
1046 if (ev.contains("content") && 1078 if (ev.contains("content") &&
1047 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out)) 1079 btf::VerifyDescriptor(ctx, ev["content"].get<string>(), pk, out))
1080 {
1081 if (rawOut)
1082 *rawOut = ev["content"].get<string>();
1048 return true; 1083 return true;
1084 }
1049 } 1085 }
1050 else if (t == "EOSE") 1086 else if (t == "EOSE")
1051 break; 1087 break;
@@ -1057,8 +1093,10 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr,
1057// public discovery relays for the owner's self-certified descriptor. Used by 1093// public discovery relays for the owner's self-certified descriptor. Used by
1058// ConnectNodeBtf in net.cpp. 1094// ConnectNodeBtf in net.cpp.
1059bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, 1095bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1060 unsigned char enc_pub[32]) 1096 unsigned char enc_pub[32], std::string* onionHostPort)
1061{ 1097{
1098 if (onionHostPort)
1099 onionHostPort->clear();
1062 if (!EnsureNostrKey()) 1100 if (!EnsureNostrKey())
1063 return false; 1101 return false;
1064 for (int i = 0; i < nNostrRelays && !fShutdown; i++) 1102 for (int i = 0; i < nNostrRelays && !fShutdown; i++)
@@ -1076,10 +1114,15 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
1076 } 1114 }
1077 lease.fOk = true; 1115 lease.fOk = true;
1078 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1116 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending")
1079 continue; // descriptor predates the owner's relay config 1117 {
1118 if (!onionHostPort || d.onion.empty())
1119 continue; // caller needs rendezvous, or descriptor has no direct path
1120 }
1080 if (!HexDecode(d.enc, enc_pub, 32)) 1121 if (!HexDecode(d.enc, enc_pub, 32))
1081 continue; 1122 continue;
1082 meetingHostPort = d.meeting_node; 1123 meetingHostPort = d.meeting_node;
1124 if (onionHostPort)
1125 *onionHostPort = d.onion;
1083 return true; 1126 return true;
1084 } 1127 }
1085 CATCH_PRINT_EXCEPTION("BtfResolve") 1128 CATCH_PRINT_EXCEPTION("BtfResolve")
@@ -1168,12 +1211,15 @@ bool BtfResolveMany(const std::vector<std::string>& btfAddrs,
1168 btf::Descriptor d; 1211 btf::Descriptor d;
1169 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d)) 1212 if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get<string>(), pk, d))
1170 continue; 1213 continue;
1171 if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") 1214 if ((d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") &&
1215 d.onion.empty())
1172 continue; 1216 continue;
1173 BtfResolvedPeer rp; 1217 BtfResolvedPeer rp;
1174 if (!HexDecode(d.enc, rp.enc_pub, 32)) 1218 if (!HexDecode(d.enc, rp.enc_pub, 32))
1175 continue; 1219 continue;
1176 rp.meetingHostPort = d.meeting_node; 1220 rp.meetingHostPort = d.meeting_node;
1221 rp.onionHostPort = d.onion;
1222 rp.descriptor = ev["content"].get<string>();
1177 out[ai->second] = rp; 1223 out[ai->second] = rp;
1178 pending.erase(ai->second); 1224 pending.erase(ai->second);
1179 } 1225 }
@@ -1650,11 +1696,13 @@ static void ConnectDiscoveredBtfPeers()
1650 DialCtx* ctx = (DialCtx*)arg; 1696 DialCtx* ctx = (DialCtx*)arg;
1651 string addr = ctx->addr; 1697 string addr = ctx->addr;
1652 string meeting = ctx->rp.meetingHostPort; 1698 string meeting = ctx->rp.meetingHostPort;
1699 string onion = ctx->rp.onionHostPort;
1700 string desc = ctx->rp.descriptor;
1653 unsigned char enc_pub[32]; 1701 unsigned char enc_pub[32];
1654 memcpy(enc_pub, ctx->rp.enc_pub, 32); 1702 memcpy(enc_pub, ctx->rp.enc_pub, 32);
1655 delete ctx; 1703 delete ctx;
1656 1704
1657 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); 1705 CNode* pnode = ConnectNodeBtfResolved(addr, meeting, onion, enc_pub, desc);
1658 CRITICAL_BLOCK(cs_btfPeerState) 1706 CRITICAL_BLOCK(cs_btfPeerState)
1659 { 1707 {
1660 if (pnode) 1708 if (pnode)
diff --git a/src/nostr.h b/src/nostr.h
index b5eb5e3..dc4288c 100644
--- a/src/nostr.h
+++ b/src/nostr.h
@@ -40,6 +40,13 @@ std::string BtfActiveRelay();
40// Record the relay this node just registered its service at (thread-safe). 40// Record the relay this node just registered its service at (thread-safe).
41void BtfSetActiveRelay(const std::string& relay); 41void BtfSetActiveRelay(const std::string& relay);
42 42
43// Optional Tor hidden-service endpoint for this node's normal P2P listener.
44// Set by /onionservice=HOST.onion:PORT. The endpoint is signed into this node's
45// .btf descriptor, so peers with -tor can dial it directly before falling back
46// to rendezvous relays.
47bool BtfSetLocalOnionEndpoint(const std::string& endpoint, std::string& errOut);
48std::string BtfLocalOnionEndpoint();
49
43// If set (via /announcerelay=host:port), this node announces that relay on 50// If set (via /announcerelay=host:port), this node announces that relay on
44// Nostr so other nodes discover it automatically -- no manual seed-list edit. 51// Nostr so other nodes discover it automatically -- no manual seed-list edit.
45extern std::string strBtfAnnounceRelay; 52extern std::string strBtfAnnounceRelay;
@@ -94,12 +101,15 @@ void* BtfSecpContext();
94// self-certified descriptor and return its meeting node ("host:port") and 101// self-certified descriptor and return its meeting node ("host:port") and
95// x25519 public key for the end-to-end channel. 102// x25519 public key for the end-to-end channel.
96bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, 103bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort,
97 unsigned char enc_pub[32]); 104 unsigned char enc_pub[32],
105 std::string* onionHostPort = NULL);
98 106
99// Result of resolving one .btf address via BtfResolveMany. 107// Result of resolving one .btf address via BtfResolveMany.
100struct BtfResolvedPeer 108struct BtfResolvedPeer
101{ 109{
102 std::string meetingHostPort; 110 std::string meetingHostPort;
111 std::string onionHostPort;
112 std::string descriptor;
103 unsigned char enc_pub[32]; 113 unsigned char enc_pub[32];
104}; 114};
105 115
diff --git a/src/readme.txt b/src/readme.txt
index 794fe20..1e9cffc 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -43,11 +43,11 @@ Command-line options (in addition to the original ones)
43------------------------------------------------------- 43-------------------------------------------------------
44 /port=N listen on P2P port N (default 8433) 44 /port=N listen on P2P port N (default 8433)
45 /socks=HOST:PORT 45 /socks=HOST:PORT
46 route outbound Nostr and .btf rendezvous dials through a 46 route outbound Nostr, .btf rendezvous, and direct onion peer
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor 47 dials through a SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
48 or /socks=[::1]:9050 for an IPv6 loopback proxy 48 or /socks=[::1]:9050 for an IPv6 loopback proxy
49 /tor[=HOST:PORT] 49 /tor[=HOST:PORT]
50 Tor mode for outbound Nostr and .btf rendezvous dials; 50 Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer dials;
51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050 51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050
52 /solomine mine without requiring a connected peer (local testing) 52 /solomine mine without requiring a connected peer (local testing)
53 /operator operator mode (run pool server) 53 /operator operator mode (run pool server)
@@ -72,6 +72,8 @@ Command-line options (in addition to the original ones)
72 override meeting relay list with one relay 72 override meeting relay list with one relay
73 /announcerelay=HOST:PORT 73 /announcerelay=HOST:PORT
74 announce this relay on Nostr for discovery 74 announce this relay on Nostr for discovery
75 /onionservice=HOST.onion:PORT
76 advertise this node's Tor hidden service in its signed .btf descriptor
75 77
76 78
77Roadmap 79Roadmap
diff --git a/src/selftest.cpp b/src/selftest.cpp
index bcbf9d3..a0d9edd 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -6,6 +6,7 @@
6// and against a temporary data directory. 6// and against a temporary data directory.
7 7
8#include "headers_core.h" 8#include "headers_core.h"
9#include "btfaddr.h"
9#include "bip32.h" 10#include "bip32.h"
10#include "proxy.h" 11#include "proxy.h"
11#include "selftest.h" 12#include "selftest.h"
@@ -1507,6 +1508,14 @@ static int RunSocks5ProxySelfTest()
1507 !BtfIsTorOnionHost("relay.example") && 1508 !BtfIsTorOnionHost("relay.example") &&
1508 !BtfIsTorOnionHost(".onion"), 1509 !BtfIsTorOnionHost(".onion"),
1509 "detects .onion hosts without accepting lookalikes") ? 0 : 1; 1510 "detects .onion hosts without accepting lookalikes") ? 0 : 1;
1511 std::string onion;
1512 nFail += Check(btf::NormalizeOnionEndpoint("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD.onion.:8433", onion) &&
1513 onion == "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433",
1514 "normalizes v3 onion peer endpoint") ? 0 : 1;
1515 nFail += Check(!btf::NormalizeOnionEndpoint("short.onion:8433", onion) &&
1516 !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:0", onion) &&
1517 !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc1.onion:8433", onion),
1518 "rejects malformed onion peer endpoints") ? 0 : 1;
1510 1519
1511 BtfClearSocks5Proxy(); 1520 BtfClearSocks5Proxy();
1512 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; 1521 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
diff --git a/src/sockcount.h b/src/sockcount.h
index 5147f55..a63c6be 100644
--- a/src/sockcount.h
+++ b/src/sockcount.h
@@ -63,6 +63,7 @@ enum
63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode 63 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode
64 SOCK_PAIR_PUMP, // loopback pair: the pump end 64 SOCK_PAIR_PUMP, // loopback pair: the pump end
65 SOCK_NOSTR, // outbound to a Nostr relay 65 SOCK_NOSTR, // outbound to a Nostr relay
66 SOCK_ONION_PEER, // direct outbound to a peer's Tor hidden service
66 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets 67 SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets
67 SOCK_SITES 68 SOCK_SITES
68}; 69};
diff --git a/src/test_btfaddr.cpp b/src/test_btfaddr.cpp
index b40dcc1..8de426e 100644
--- a/src/test_btfaddr.cpp
+++ b/src/test_btfaddr.cpp
@@ -86,6 +86,7 @@ int main()
86 } 86 }
87 87
88 const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"; 88 const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899";
89 const std::string ONION = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433";
89 90
90 // helper: fresh keypair -> (seckey, xonly pubkey) 91 // helper: fresh keypair -> (seckey, xonly pubkey)
91 auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) { 92 auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) {
@@ -108,6 +109,17 @@ int main()
108 CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected"); 109 CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected");
109 } 110 }
110 111
112 printf("descriptor_signs_optional_onion_endpoint\n");
113 {
114 unsigned char sk[32], pk[32]; newKey(sk, pk);
115 std::string d = btf::SignDescriptor(ctx, sk, ENC_HEX, "meeting-node-abc", 1000, ONION);
116 CHECK(!d.empty(), "sign produced onion descriptor");
117 btf::Descriptor got;
118 CHECK(btf::VerifyDescriptor(ctx, d, pk, got), "onion descriptor verifies");
119 CHECK(got.meeting_node == "meeting-node-abc" && got.enc == ENC_HEX &&
120 got.created == 1000 && got.onion == ONION, "onion fields match");
121 }
122
111 printf("descriptor_rejects_tampering\n"); 123 printf("descriptor_rejects_tampering\n");
112 { 124 {
113 unsigned char sk[32], pk[32]; newKey(sk, pk); 125 unsigned char sk[32], pk[32]; newKey(sk, pk);
@@ -117,6 +129,16 @@ int main()
117 CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected"); 129 CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected");
118 json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef"; 130 json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef";
119 CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected"); 131 CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected");
132
133 std::string d3 = btf::SignDescriptor(ctx, sk, ENC_HEX, "node-1", 5, ONION);
134 json j3 = json::parse(d3);
135 j3["onion"] = "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde.onion:8433";
136 CHECK(btf::VerifyDescriptor(ctx, j3.dump(), pk, o) && o.onion.empty(),
137 "tampered onion ignored rather than trusted");
138 json j4 = json::parse(d3);
139 j4["sig3"] = std::string(128, '0');
140 CHECK(btf::VerifyDescriptor(ctx, j4.dump(), pk, o) && o.onion.empty(),
141 "bad onion signature ignored rather than trusted");
120 } 142 }
121 143
122 printf("full_resolve_chain_from_address\n"); 144 printf("full_resolve_chain_from_address\n");