diff options
| author | Bitflash <admin@bitflash.network> | 2026-08-13 11:38:50 -0300 |
|---|---|---|
| committer | Bitflash <admin@bitflash.network> | 2026-08-13 11:38:50 -0300 |
| commit | c009e7cc0c5093f4c4add741cb9d131d19956871 (patch) | |
| tree | 002423e5987a3c544f469befae83519ccb29fffb /src | |
| parent | 851811b43be38bea48ba5d04d16987bd541dca53 (diff) | |
| parent | d2b86473a267c960a1f0c012a983176e5d962187 (diff) | |
Merge pull request 'net: add signed direct onion peer endpoints' (#32) from onion-direct-peers into main
Diffstat (limited to 'src')
| -rw-r--r-- | src/btfaddr.cpp | 102 | ||||
| -rw-r--r-- | src/btfaddr.h | 8 | ||||
| -rw-r--r-- | src/main_gui.cpp | 15 | ||||
| -rw-r--r-- | src/net.cpp | 102 | ||||
| -rw-r--r-- | src/net.h | 3 | ||||
| -rw-r--r-- | src/nostr.cpp | 76 | ||||
| -rw-r--r-- | src/nostr.h | 12 | ||||
| -rw-r--r-- | src/readme.txt | 8 | ||||
| -rw-r--r-- | src/selftest.cpp | 9 | ||||
| -rw-r--r-- | src/sockcount.h | 1 | ||||
| -rw-r--r-- | src/test_btfaddr.cpp | 22 |
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 | ||
| 23 | using json = nlohmann::json; | 25 | using 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 | ||
| 171 | bool 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). |
| 170 | static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, | 209 | static 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 | ||
| 226 | static 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 | |||
| 187 | std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], | 244 | std::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". | ||
| 61 | bool 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. |
| 61 | std::string SignDescriptor(void* ctx, const unsigned char seckey[32], | 66 | std::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 | ||
| 176 | static string SockAccountingText() | 178 | static 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. |
| 709 | static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, | 714 | static 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 | ||
| 981 | static 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 | |||
| 968 | static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], | 1002 | static 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. |
| 1056 | CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, | 1119 | CNode* 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 |
| @@ -28,7 +28,8 @@ enum | |||
| 28 | bool GetMyExternalIP(unsigned int& ipRet); | 28 | bool GetMyExternalIP(unsigned int& ipRet); |
| 29 | CNode* ConnectNodeBtf(const string& strBtfAddr); | 29 | CNode* ConnectNodeBtf(const string& strBtfAddr); |
| 30 | CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, | 30 | CNode* 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()); | ||
| 32 | void ThreadBtfAccept(void* parg); | 33 | void ThreadBtfAccept(void* parg); |
| 33 | void ThreadBtfConnect(void* parg); | 34 | void ThreadBtfConnect(void* parg); |
| 34 | extern string strBtfConnect; | 35 | extern 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 | }; |
| 113 | string strBtfActiveRelay; | 113 | string strBtfActiveRelay; |
| 114 | static CCriticalSection cs_activeRelay; | 114 | static CCriticalSection cs_activeRelay; |
| 115 | static string strBtfOnionEndpoint; | ||
| 116 | static 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 | ||
| 138 | bool 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 | |||
| 151 | string 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). |
| 137 | string strBtfAnnounceRelay; | 159 | string 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. |
| 1020 | static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, | 1052 | static 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. |
| 1059 | bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | 1095 | bool 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). |
| 41 | void BtfSetActiveRelay(const std::string& relay); | 41 | void 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. | ||
| 47 | bool BtfSetLocalOnionEndpoint(const std::string& endpoint, std::string& errOut); | ||
| 48 | std::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. |
| 45 | extern std::string strBtfAnnounceRelay; | 52 | extern 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. |
| 96 | bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, | 103 | bool 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. |
| 100 | struct BtfResolvedPeer | 108 | struct 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 | ||
| 77 | Roadmap | 79 | Roadmap |
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"); |