upleb.uk

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

summaryrefslogtreecommitdiff
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
parent851811b43be38bea48ba5d04d16987bd541dca53 (diff)
net: add signed direct onion peer endpoints
-rw-r--r--README.md26
-rw-r--r--docs/rendezvous.md70
-rw-r--r--docs/tor.md39
-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
14 files changed, 420 insertions, 73 deletions
diff --git a/README.md b/README.md
index e671302..181b6e8 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Bitflash keeps Satoshi's original consensus rules and replaces two things:
22 22
23**RandomX proof of work.** Memory-hard algorithm used by Monero. A laptop competes equally with a server. ASICs and GPUs have no advantage. 23**RandomX proof of work.** Memory-hard algorithm used by Monero. A laptop competes equally with a server. ASICs and GPUs have no advantage.
24 24
25**Anonymous addressing.** Every node has a `.btf` address derived from its public key, similar to a Tor `.onion`. Nodes reach each other through encrypted rendezvous tunnels, so no port forwarding is needed and a node behind CGNAT works normally. 25**Anonymous addressing.** Every node has a `.btf` address derived from its public key, similar to a Tor `.onion`. Nodes reach each other through encrypted rendezvous tunnels by default, and can advertise a signed direct `.onion` endpoint for Tor peers.
26 26
27No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks. 27No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks.
28 28
@@ -30,14 +30,15 @@ No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute bl
30 30
31Worth being precise, because the difference matters if you are relying on it. 31Worth being precise, because the difference matters if you are relying on it.
32 32
33**A peer you reach over `.btf` does not learn your IP.** All outbound connections go through a rendezvous tunnel; there is no IP-based peer dialling left in the node. The relay forwards encrypted bytes and cannot read or alter them. 33**A peer you reach over `.btf` does not learn your IP.** Outbound peer discovery uses rendezvous tunnels by default. If both sides use Tor and the peer advertises a signed `.onion` endpoint, the node connects directly to that hidden service instead. In both cases the peer does not see your clearnet IP.
34 34
35**The rendezvous relay does see your IP.** It has to — it is the thing your TCP connection terminates on. Relays are run by volunteers, so treat that as a party who knows you are on the network. 35**The rendezvous relay sees your IP unless you use Tor.** It has to — it is the thing your TCP connection terminates on. With `-tor`, the relay sees Tor instead. With a signed direct `.onion` peer endpoint, the relay is not on that connection path at all.
36 36
37**The node still listens on 8433.** Nothing dials by IP any more, but the listener is still there, so anyone who already knows your address and can reach that port may connect directly. If that matters to you, firewall it. 37**The node still listens on 8433.** Nothing dials by IP any more, but the listener is still there, so anyone who already knows your address and can reach that port may connect directly. If that matters to you, firewall it.
38 38
39This is unlinkability between peers, not anonymity against a network observer. 39This is unlinkability between peers, not anonymity against a network observer.
40For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md). 40For Tor routing and direct onion peer endpoints, start the node with `-tor`;
41see [Tor mode](docs/tor.md).
41For a plain SOCKS5 proxy without Tor-specific defaults, use 42For a plain SOCKS5 proxy without Tor-specific defaults, use
42`-socks=HOST:PORT`. 43`-socks=HOST:PORT`.
43 44
@@ -336,8 +337,9 @@ Other options worth knowing:
336```bash 337```bash
337-datadir=PATH # wallet and chain data elsewhere 338-datadir=PATH # wallet and chain data elsewhere
338-port=N # P2P listen port, default 8433 339-port=N # P2P listen port, default 8433
339-socks=HOST:PORT # SOCKS5 for outbound Nostr and .btf rendezvous dials 340-socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials
340-tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050 341-tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050
342-onionservice=HOST.onion:PORT # advertise this node's Tor hidden service
341-debug # verbose log; without it debug.log is nearly silent 343-debug # verbose log; without it debug.log is nearly silent
342-help # full list 344-help # full list
343``` 345```
@@ -346,15 +348,19 @@ Other options worth knowing:
346needed — the data directory takes an exclusive lock, so a second node pointed at 348needed — the data directory takes an exclusive lock, so a second node pointed at
347the same one will refuse to start. 349the same one will refuse to start.
348 350
349`-socks=127.0.0.1:9050` routes outbound Nostr discovery and `.btf` rendezvous 351`-socks=127.0.0.1:9050` routes outbound Nostr discovery, `.btf` rendezvous
350dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips 352dials, and direct `.onion` peer dials through a local SOCKS5 proxy such as Tor.
353When it is set, the node skips
351plain HTTP external-IP probes instead of leaking a direct request outside the 354plain HTTP external-IP probes instead of leaking a direct request outside the
352proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. 355proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`.
353 356
354`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at 357`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at
355`127.0.0.1:9050`. It routes outbound Nostr discovery and `.btf` rendezvous 358`127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials,
356dials through Tor, refuses direct `.onion` resolution, and keeps external-IP 359and signed direct `.onion` peer dials through Tor, and keeps external-IP probes
357probes disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. 360disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. To make your own
361node reachable without a rendezvous relay, expose port `8433` as a Tor hidden
362service and pass `-onionservice=HOST.onion:8433`; peers with `-tor` will try it
363before falling back to rendezvous.
358 364
359### When something looks wrong 365### When something looks wrong
360 366
diff --git a/docs/rendezvous.md b/docs/rendezvous.md
index ac8e598..e1888b9 100644
--- a/docs/rendezvous.md
+++ b/docs/rendezvous.md
@@ -18,11 +18,16 @@ A connection to a `.btf` service has three layers:
181. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr 181. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr
19 event. A caller fetches it by the service public key and verifies the 19 event. A caller fetches it by the service public key and verifies the
20 signature. The descriptor says which rendezvous relay the service is waiting 20 signature. The descriptor says which rendezvous relay the service is waiting
21 at and which encryption key to use. 21 at, which encryption key to use, and optionally which Tor hidden service can
22 be dialed directly.
222. **Rendezvous (TCP).** Caller and service both connect out to the same relay 232. **Rendezvous (TCP).** Caller and service both connect out to the same relay
23 and are paired by public key. Neither needs an inbound port or a public IP. 24 and are paired by public key. Neither needs an inbound port or a public IP.
24 After pairing the relay is a blind byte pipe. 25 After pairing the relay is a blind byte pipe.
253. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the 263. **Direct onion fallback bypass.** If the descriptor carries a valid signed
27 `.onion:port` endpoint and the caller is running with Tor mode, the caller
28 tries that endpoint before using rendezvous. This removes the rendezvous
29 relay from the data path for peers that choose to run hidden services.
304. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the
26 caller and service run an ephemeral to static handshake and then exchange 31 caller and service run an ephemeral to static handshake and then exchange
27 authenticated frames. The relay sees only ciphertext. 32 authenticated frames. The relay sees only ciphertext.
28 33
@@ -107,6 +112,8 @@ The default rendezvous port is `8434`.
107The descriptor is a JSON object, signed with the identity key over a domain 112The descriptor is a JSON object, signed with the identity key over a domain
108separated digest of its own fields. 113separated digest of its own fields.
109 114
115Legacy descriptor:
116
110```json 117```json
111{ 118{
112 "v": 2, 119 "v": 2,
@@ -118,7 +125,22 @@ separated digest of its own fields.
118} 125}
119``` 126```
120 127
121The signed digest is: 128Onion-capable descriptor:
129
130```json
131{
132 "v": 3,
133 "pubkey": "<hex 32-byte x-only identity key>",
134 "enc": "<hex 32-byte X25519 public key>",
135 "meeting_node": "host:port",
136 "onion": "<56-char-v3-name>.onion:8433",
137 "created": <unix seconds>,
138 "sig": "<legacy v2 signature>",
139 "sig3": "<signature over the onion-capable digest>"
140}
141```
142
143The legacy signed digest is:
122 144
123``` 145```
124digest = SHA256( ".btf-descriptor-v2" 146digest = SHA256( ".btf-descriptor-v2"
@@ -128,17 +150,37 @@ digest = SHA256( ".btf-descriptor-v2"
128 || created (8 bytes, big endian) ) 150 || created (8 bytes, big endian) )
129``` 151```
130 152
131The signature is a deterministic BIP340 Schnorr signature (no auxiliary 153`sig` is kept on v3 descriptors so old nodes can still resolve and dial through
132randomness) over `digest` by the identity key. 154the rendezvous relay. The onion-capable digest is:
155
156```
157digest3 = SHA256( ".btf-descriptor-v3-onion"
158 || pubkey[32]
159 || enc (raw bytes of the string)
160 || meeting_node (raw bytes of the string)
161 || onion (raw bytes of the normalized string)
162 || created (8 bytes, big endian) )
163```
164
165`sig` and `sig3` are deterministic BIP340 Schnorr signatures (no auxiliary
166randomness) over their respective digests by the identity key.
133 167
134**Verification** (untrusted input, must never throw): parse the JSON, require all 168**Verification** (untrusted input, must never throw): parse the JSON, require all
135fields with the right types, confirm `pubkey` equals the address being resolved, 169fields with the right types, confirm `pubkey` equals the address being resolved,
136recompute the digest, and verify the Schnorr signature. Any failure rejects the 170recompute the digest, and verify the Schnorr signature. Any failure rejects the
137descriptor. 171descriptor.
138 172
173If `onion` is present, readers accept it only when it is a normalized v3 onion
174endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2
175descriptor can still be used for rendezvous fallback if `sig` verifies.
176
139`created` lets a reader prefer the freshest descriptor when more than one is 177`created` lets a reader prefer the freshest descriptor when more than one is
140seen. A node only publishes a descriptor once it has actually registered at a 178seen. A node without a direct onion endpoint only publishes a descriptor once it
141`meeting_node`, so it never advertises a relay it cannot be reached at. 179has actually registered at a `meeting_node`, so it never advertises a relay it
180cannot be reached at. A node with a direct onion endpoint may publish before
181rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers
182can still dial the signed onion endpoint, and rendezvous becomes fallback once
183registration succeeds.
142 184
143## 6. Rendezvous transport 185## 6. Rendezvous transport
144 186
@@ -243,8 +285,9 @@ tunnel.
243 not published anywhere. 285 not published anywhere.
244- **Forward secrecy.** The caller's per connection ephemeral key means a later 286- **Forward secrecy.** The caller's per connection ephemeral key means a later
245 compromise of the service encryption key does not decrypt past sessions. 287 compromise of the service encryption key does not decrypt past sessions.
246- **IP privacy.** Neither side needs a public IP or an inbound port. The observable 288- **IP privacy.** Neither side needs a public IP or an inbound port for
247 network fact is that each side has a TCP connection to a relay. 289 rendezvous. With a signed `.onion` endpoint, the reachable side exposes only a
290 Tor hidden service and the relay is not in the peer connection path.
248 291
249## 10. Threat model and limitations 292## 10. Threat model and limitations
250 293
@@ -265,6 +308,9 @@ tunnel.
265 unreachable, discovery stalls until one is reachable again. Relay TLS is 308 unreachable, discovery stalls until one is reachable again. Relay TLS is
266 verified, which closes a man in the middle path on discovery, but availability 309 verified, which closes a man in the middle path on discovery, but availability
267 still rests on the relay set. 310 still rests on the relay set.
311- **Direct onion is not automatic Tor configuration.** Bitflash signs and
312 advertises a hidden-service endpoint, and Tor-mode peers can dial it. The Tor
313 daemon still owns hidden-service creation, private keys, rotation and uptime.
268 314
269## 11. Wire reference 315## 11. Wire reference
270 316
@@ -273,9 +319,9 @@ tunnel.
273| `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding | 319| `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding |
274| Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars | 320| Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars |
275| Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` | 321| Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` |
276| Descriptor domain sep | `.btf-descriptor-v2` | 322| Descriptor domain sep | `.btf-descriptor-v2`; optional onion sep `.btf-descriptor-v3-onion` |
277| Descriptor version | `2` | 323| Descriptor version | `2`; `3` when a signed onion endpoint is present |
278| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand) | 324| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand), v3 keeps `sig` plus `sig3` |
279| Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` | 325| Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` |
280| Nostr relay event | kind `38502`, `d = btf-relay-2` | 326| Nostr relay event | kind `38502`, `d = btf-relay-2` |
281| Nostr pool event | kind `38503`, `d = btf-pool-2` | 327| Nostr pool event | kind `38503`, `d = btf-pool-2` |
diff --git a/docs/tor.md b/docs/tor.md
index 69b29f1..b4f88f9 100644
--- a/docs/tor.md
+++ b/docs/tor.md
@@ -40,6 +40,8 @@ When Tor mode is enabled:
40 40
41- outbound Nostr relay dials use Tor; 41- outbound Nostr relay dials use Tor;
42- outbound `.btf` rendezvous relay dials use Tor; 42- outbound `.btf` rendezvous relay dials use Tor;
43- outbound direct peer dials prefer signed `.onion` endpoints when a peer
44 advertises one;
43- destination names are sent to Tor as SOCKS5 domain-name CONNECT requests, so 45- destination names are sent to Tor as SOCKS5 domain-name CONNECT requests, so
44 the local DNS resolver does not see them; 46 the local DNS resolver does not see them;
45- direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled; 47- direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled;
@@ -98,7 +100,7 @@ bitflash -operator -tor \
98 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 100 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434
99``` 101```
100 102
101## Full-node hidden service 103## Direct full-node hidden service
102 104
103The legacy P2P listener still binds to port `8433`. You can expose it through 105The legacy P2P listener still binds to port `8433`. You can expose it through
104Tor manually: 106Tor manually:
@@ -108,6 +110,35 @@ HiddenServiceDir /var/lib/tor/bitflash-node/
108HiddenServicePort 8433 127.0.0.1:8433 110HiddenServicePort 8433 127.0.0.1:8433
109``` 111```
110 112
111Current automatic peer discovery is `.btf`/rendezvous based, not onion-address 113After restarting Tor, read the generated onion name:
112gossip. Treat direct full-node onion access as an operator/admin tool unless a 114
113future release adds first-class onion peer advertisements. 115```bash
116sudo cat /var/lib/tor/bitflash-node/hostname
117```
118
119Then start Bitflash with Tor outbound enabled and advertise that hidden service:
120
121```bash
122bitflash -tor \
123 -onionservice=exampleexampleexampleexampleexampleexampleexampleexample.onion:8433
124```
125
126That onion endpoint is signed into this node's self-certifying `.btf`
127descriptor. New peers that resolve the descriptor and also run with `-tor` try
128the `.onion:8433` P2P connection first. If it fails, they fall back to the
129ordinary encrypted rendezvous path. Older nodes ignore the onion field and still
130use the rendezvous relay because the descriptor keeps the legacy signature too.
131
132If the node has `-onionservice` but has not registered at any rendezvous relay
133yet, it can still publish an onion-capable descriptor with
134`meeting_node = rendezvous-pending`. Tor-capable peers can dial the hidden
135service directly from that descriptor. Rendezvous becomes a fallback, not a
136precondition for being discoverable.
137
138This means rendezvous relays stop being the only way to reach a node that has
139published a hidden service. They remain useful bootstrap/fallback infrastructure,
140but an already-discovered onion-capable peer can be reached directly over Tor.
141
142The node does not create or rotate Tor hidden services itself. Tor owns the
143private key under `HiddenServiceDir`; back it up if you want the onion name to
144stay stable.
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");