diff options
| author | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-13 11:11:59 -0300 |
|---|---|---|
| committer | Nsable-sh <nsable-sh@noreply.git.bitflash.network> | 2026-08-13 11:11:59 -0300 |
| commit | d2b86473a267c960a1f0c012a983176e5d962187 (patch) | |
| tree | 002423e5987a3c544f469befae83519ccb29fffb | |
| parent | 851811b43be38bea48ba5d04d16987bd541dca53 (diff) | |
net: add signed direct onion peer endpoints
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | docs/rendezvous.md | 70 | ||||
| -rw-r--r-- | docs/tor.md | 39 | ||||
| -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 |
14 files changed, 420 insertions, 73 deletions
| @@ -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 | ||
| 27 | No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks. | 27 | No 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 | ||
| 31 | Worth being precise, because the difference matters if you are relying on it. | 31 | Worth 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 | ||
| 39 | This is unlinkability between peers, not anonymity against a network observer. | 39 | This is unlinkability between peers, not anonymity against a network observer. |
| 40 | For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md). | 40 | For Tor routing and direct onion peer endpoints, start the node with `-tor`; |
| 41 | see [Tor mode](docs/tor.md). | ||
| 41 | For a plain SOCKS5 proxy without Tor-specific defaults, use | 42 | For 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: | |||
| 346 | needed — the data directory takes an exclusive lock, so a second node pointed at | 348 | needed — the data directory takes an exclusive lock, so a second node pointed at |
| 347 | the same one will refuse to start. | 349 | the 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 |
| 350 | dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips | 352 | dials, and direct `.onion` peer dials through a local SOCKS5 proxy such as Tor. |
| 353 | When it is set, the node skips | ||
| 351 | plain HTTP external-IP probes instead of leaking a direct request outside the | 354 | plain HTTP external-IP probes instead of leaking a direct request outside the |
| 352 | proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. | 355 | proxy. 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, |
| 356 | dials through Tor, refuses direct `.onion` resolution, and keeps external-IP | 359 | and signed direct `.onion` peer dials through Tor, and keeps external-IP probes |
| 357 | probes disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. | 360 | disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. To make your own |
| 361 | node reachable without a rendezvous relay, expose port `8433` as a Tor hidden | ||
| 362 | service and pass `-onionservice=HOST.onion:8433`; peers with `-tor` will try it | ||
| 363 | before 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: | |||
| 18 | 1. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr | 18 | 1. **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. | ||
| 22 | 2. **Rendezvous (TCP).** Caller and service both connect out to the same relay | 23 | 2. **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. |
| 25 | 3. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the | 26 | 3. **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. | ||
| 30 | 4. **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`. | |||
| 107 | The descriptor is a JSON object, signed with the identity key over a domain | 112 | The descriptor is a JSON object, signed with the identity key over a domain |
| 108 | separated digest of its own fields. | 113 | separated digest of its own fields. |
| 109 | 114 | ||
| 115 | Legacy 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 | ||
| 121 | The signed digest is: | 128 | Onion-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 | |||
| 143 | The legacy signed digest is: | ||
| 122 | 144 | ||
| 123 | ``` | 145 | ``` |
| 124 | digest = SHA256( ".btf-descriptor-v2" | 146 | digest = 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 | ||
| 131 | The 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 |
| 132 | randomness) over `digest` by the identity key. | 154 | the rendezvous relay. The onion-capable digest is: |
| 155 | |||
| 156 | ``` | ||
| 157 | digest3 = 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 | ||
| 166 | randomness) 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 |
| 135 | fields with the right types, confirm `pubkey` equals the address being resolved, | 169 | fields with the right types, confirm `pubkey` equals the address being resolved, |
| 136 | recompute the digest, and verify the Schnorr signature. Any failure rejects the | 170 | recompute the digest, and verify the Schnorr signature. Any failure rejects the |
| 137 | descriptor. | 171 | descriptor. |
| 138 | 172 | ||
| 173 | If `onion` is present, readers accept it only when it is a normalized v3 onion | ||
| 174 | endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2 | ||
| 175 | descriptor 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 |
| 140 | seen. A node only publishes a descriptor once it has actually registered at a | 178 | seen. 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. | 179 | has actually registered at a `meeting_node`, so it never advertises a relay it |
| 180 | cannot be reached at. A node with a direct onion endpoint may publish before | ||
| 181 | rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers | ||
| 182 | can still dial the signed onion endpoint, and rendezvous becomes fallback once | ||
| 183 | registration 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 | ||
| 103 | The legacy P2P listener still binds to port `8433`. You can expose it through | 105 | The legacy P2P listener still binds to port `8433`. You can expose it through |
| 104 | Tor manually: | 106 | Tor manually: |
| @@ -108,6 +110,35 @@ HiddenServiceDir /var/lib/tor/bitflash-node/ | |||
| 108 | HiddenServicePort 8433 127.0.0.1:8433 | 110 | HiddenServicePort 8433 127.0.0.1:8433 |
| 109 | ``` | 111 | ``` |
| 110 | 112 | ||
| 111 | Current automatic peer discovery is `.btf`/rendezvous based, not onion-address | 113 | After restarting Tor, read the generated onion name: |
| 112 | gossip. Treat direct full-node onion access as an operator/admin tool unless a | 114 | |
| 113 | future release adds first-class onion peer advertisements. | 115 | ```bash |
| 116 | sudo cat /var/lib/tor/bitflash-node/hostname | ||
| 117 | ``` | ||
| 118 | |||
| 119 | Then start Bitflash with Tor outbound enabled and advertise that hidden service: | ||
| 120 | |||
| 121 | ```bash | ||
| 122 | bitflash -tor \ | ||
| 123 | -onionservice=exampleexampleexampleexampleexampleexampleexampleexample.onion:8433 | ||
| 124 | ``` | ||
| 125 | |||
| 126 | That onion endpoint is signed into this node's self-certifying `.btf` | ||
| 127 | descriptor. New peers that resolve the descriptor and also run with `-tor` try | ||
| 128 | the `.onion:8433` P2P connection first. If it fails, they fall back to the | ||
| 129 | ordinary encrypted rendezvous path. Older nodes ignore the onion field and still | ||
| 130 | use the rendezvous relay because the descriptor keeps the legacy signature too. | ||
| 131 | |||
| 132 | If the node has `-onionservice` but has not registered at any rendezvous relay | ||
| 133 | yet, it can still publish an onion-capable descriptor with | ||
| 134 | `meeting_node = rendezvous-pending`. Tor-capable peers can dial the hidden | ||
| 135 | service directly from that descriptor. Rendezvous becomes a fallback, not a | ||
| 136 | precondition for being discoverable. | ||
| 137 | |||
| 138 | This means rendezvous relays stop being the only way to reach a node that has | ||
| 139 | published a hidden service. They remain useful bootstrap/fallback infrastructure, | ||
| 140 | but an already-discovered onion-capable peer can be reached directly over Tor. | ||
| 141 | |||
| 142 | The node does not create or rotate Tor hidden services itself. Tor owns the | ||
| 143 | private key under `HiddenServiceDir`; back it up if you want the onion name to | ||
| 144 | stay 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 | ||
| 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"); |