From a109dcd23fd387324e63f265a237723ba1d6c4fa Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Wed, 12 Aug 2026 14:44:43 -0300 Subject: wallet: collapse restored legacy hdnext --- src/selftest.cpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) (limited to 'src/selftest.cpp') diff --git a/src/selftest.cpp b/src/selftest.cpp index cca5335..bcbf9d3 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -586,6 +586,34 @@ static int RunWalletHDSelfTest() "the recovery audit finds wallet.dat-only immature mining rewards") ? 0 : 1; nFail += Check(audit.nLegacyImmatureTx == 1, "the recovery audit counts wallet.dat-only immature mining rewards") ? 0 : 1; + + CKey keyLegacyHD11; + nSchemaForLegacyAudit = nHDKeySchema; + nHDKeySchema = HD_SCHEMA_LEGACY; + if (!DeriveHDKey(11, keyLegacyHD11, strError)) + throw std::runtime_error("legacy compatibility index 11 derivation failed: " + strError); + nHDKeySchema = nSchemaForLegacyAudit; + if (!AddKey(keyLegacyHD11)) + throw std::runtime_error("could not store the legacy compatibility index 11 key"); + + std::map > mapLegacyProbe; + mapLegacyProbe[0] = keyAuditLegacyHD.GetPubKey(); + mapLegacyProbe[11] = keyLegacyHD11.GetPubKey(); + + CWalletTx wtxLegacyHD11; + wtxLegacyHD11.vout.push_back(CTxOut(19 * COIN, CScript() << keyLegacyHD11.GetPubKey() << OP_CHECKSIG)); + CRITICAL_BLOCK(cs_mapWallet) + { + mapWallet.clear(); + mapWallet[wtxLegacyHD11.GetHash()] = wtxLegacyHD11; + } + nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 12, + "restore collapses legacy hdnext to the last used compatibility index plus one") ? 0 : 1; + + CRITICAL_BLOCK(cs_mapWallet) + mapWallet.clear(); + nFail += Check(WalletLastUsedPubKeyIndexNext(mapLegacyProbe) == 0, + "restore does not persist legacy hdnext at the scan depth when no compatibility key was used") ? 0 : 1; } catch (const std::exception& e) { -- cgit v1.2.3 From d2b86473a267c960a1f0c012a983176e5d962187 Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 11:11:59 -0300 Subject: net: add signed direct onion peer endpoints --- README.md | 26 ++++++++----- docs/rendezvous.md | 70 +++++++++++++++++++++++++++++------ docs/tor.md | 39 ++++++++++++++++++-- src/btfaddr.cpp | 102 +++++++++++++++++++++++++++++++++++++++++++++++---- src/btfaddr.h | 8 +++- src/main_gui.cpp | 15 +++++++- src/net.cpp | 102 +++++++++++++++++++++++++++++++++++++++++---------- src/net.h | 3 +- src/nostr.cpp | 76 +++++++++++++++++++++++++++++++------- src/nostr.h | 12 +++++- src/readme.txt | 8 ++-- src/selftest.cpp | 9 +++++ src/sockcount.h | 1 + src/test_btfaddr.cpp | 22 +++++++++++ 14 files changed, 420 insertions(+), 73 deletions(-) (limited to 'src/selftest.cpp') 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: **RandomX proof of work.** Memory-hard algorithm used by Monero. A laptop competes equally with a server. ASICs and GPUs have no advantage. -**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. +**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. No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute blocks. @@ -30,14 +30,15 @@ No premine. No ICO. 50 BTF per block, halving on schedule, 21M cap, ~2 minute bl Worth being precise, because the difference matters if you are relying on it. -**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. +**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. -**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. +**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. **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. This is unlinkability between peers, not anonymity against a network observer. -For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md). +For Tor routing and direct onion peer endpoints, start the node with `-tor`; +see [Tor mode](docs/tor.md). For a plain SOCKS5 proxy without Tor-specific defaults, use `-socks=HOST:PORT`. @@ -336,8 +337,9 @@ Other options worth knowing: ```bash -datadir=PATH # wallet and chain data elsewhere -port=N # P2P listen port, default 8433 --socks=HOST:PORT # SOCKS5 for outbound Nostr and .btf rendezvous dials +-socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials -tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050 +-onionservice=HOST.onion:PORT # advertise this node's Tor hidden service -debug # verbose log; without it debug.log is nearly silent -help # full list ``` @@ -346,15 +348,19 @@ Other options worth knowing: needed — the data directory takes an exclusive lock, so a second node pointed at the same one will refuse to start. -`-socks=127.0.0.1:9050` routes outbound Nostr discovery and `.btf` rendezvous -dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips +`-socks=127.0.0.1:9050` routes outbound Nostr discovery, `.btf` rendezvous +dials, and direct `.onion` peer dials through a local SOCKS5 proxy such as Tor. +When it is set, the node skips plain HTTP external-IP probes instead of leaking a direct request outside the proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. `-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at -`127.0.0.1:9050`. It routes outbound Nostr discovery and `.btf` rendezvous -dials through Tor, refuses direct `.onion` resolution, and keeps external-IP -probes disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. +`127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials, +and signed direct `.onion` peer dials through Tor, and keeps external-IP probes +disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. To make your own +node reachable without a rendezvous relay, expose port `8433` as a Tor hidden +service and pass `-onionservice=HOST.onion:8433`; peers with `-tor` will try it +before falling back to rendezvous. ### When something looks wrong 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: 1. **Discovery (Nostr).** The service publishes a signed descriptor as a Nostr event. A caller fetches it by the service public key and verifies the signature. The descriptor says which rendezvous relay the service is waiting - at and which encryption key to use. + at, which encryption key to use, and optionally which Tor hidden service can + be dialed directly. 2. **Rendezvous (TCP).** Caller and service both connect out to the same relay and are paired by public key. Neither needs an inbound port or a public IP. After pairing the relay is a blind byte pipe. -3. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the +3. **Direct onion fallback bypass.** If the descriptor carries a valid signed + `.onion:port` endpoint and the caller is running with Tor mode, the caller + tries that endpoint before using rendezvous. This removes the rendezvous + relay from the data path for peers that choose to run hidden services. +4. **Encrypted channel (X25519 + XChaCha20-Poly1305).** Over the paired pipe the caller and service run an ephemeral to static handshake and then exchange authenticated frames. The relay sees only ciphertext. @@ -107,6 +112,8 @@ The default rendezvous port is `8434`. The descriptor is a JSON object, signed with the identity key over a domain separated digest of its own fields. +Legacy descriptor: + ```json { "v": 2, @@ -118,7 +125,22 @@ separated digest of its own fields. } ``` -The signed digest is: +Onion-capable descriptor: + +```json +{ + "v": 3, + "pubkey": "", + "enc": "", + "meeting_node": "host:port", + "onion": "<56-char-v3-name>.onion:8433", + "created": , + "sig": "", + "sig3": "" +} +``` + +The legacy signed digest is: ``` digest = SHA256( ".btf-descriptor-v2" @@ -128,17 +150,37 @@ digest = SHA256( ".btf-descriptor-v2" || created (8 bytes, big endian) ) ``` -The signature is a deterministic BIP340 Schnorr signature (no auxiliary -randomness) over `digest` by the identity key. +`sig` is kept on v3 descriptors so old nodes can still resolve and dial through +the rendezvous relay. The onion-capable digest is: + +``` +digest3 = SHA256( ".btf-descriptor-v3-onion" + || pubkey[32] + || enc (raw bytes of the string) + || meeting_node (raw bytes of the string) + || onion (raw bytes of the normalized string) + || created (8 bytes, big endian) ) +``` + +`sig` and `sig3` are deterministic BIP340 Schnorr signatures (no auxiliary +randomness) over their respective digests by the identity key. **Verification** (untrusted input, must never throw): parse the JSON, require all fields with the right types, confirm `pubkey` equals the address being resolved, recompute the digest, and verify the Schnorr signature. Any failure rejects the descriptor. +If `onion` is present, readers accept it only when it is a normalized v3 onion +endpoint and `sig3` verifies. A bad or tampered onion field is ignored; the v2 +descriptor can still be used for rendezvous fallback if `sig` verifies. + `created` lets a reader prefer the freshest descriptor when more than one is -seen. A node only publishes a descriptor once it has actually registered at a -`meeting_node`, so it never advertises a relay it cannot be reached at. +seen. A node without a direct onion endpoint only publishes a descriptor once it +has actually registered at a `meeting_node`, so it never advertises a relay it +cannot be reached at. A node with a direct onion endpoint may publish before +rendezvous registration using `meeting_node = "rendezvous-pending"`; Tor peers +can still dial the signed onion endpoint, and rendezvous becomes fallback once +registration succeeds. ## 6. Rendezvous transport @@ -243,8 +285,9 @@ tunnel. not published anywhere. - **Forward secrecy.** The caller's per connection ephemeral key means a later compromise of the service encryption key does not decrypt past sessions. -- **IP privacy.** Neither side needs a public IP or an inbound port. The observable - network fact is that each side has a TCP connection to a relay. +- **IP privacy.** Neither side needs a public IP or an inbound port for + rendezvous. With a signed `.onion` endpoint, the reachable side exposes only a + Tor hidden service and the relay is not in the peer connection path. ## 10. Threat model and limitations @@ -265,6 +308,9 @@ tunnel. unreachable, discovery stalls until one is reachable again. Relay TLS is verified, which closes a man in the middle path on discovery, but availability still rests on the relay set. +- **Direct onion is not automatic Tor configuration.** Bitflash signs and + advertises a hidden-service endpoint, and Tor-mode peers can dial it. The Tor + daemon still owns hidden-service creation, private keys, rotation and uptime. ## 11. Wire reference @@ -273,9 +319,9 @@ tunnel. | `.btf` Base32 alphabet | `abcdefghijklmnopqrstuvwxyz234567`, no padding | | Address payload | `pubkey[32] || checksum[2]`, 55 Base32 chars | | Address checksum | `SHA256(".btf-checksum-v1" || pubkey)[0..2]` | -| Descriptor domain sep | `.btf-descriptor-v2` | -| Descriptor version | `2` | -| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand) | +| Descriptor domain sep | `.btf-descriptor-v2`; optional onion sep `.btf-descriptor-v3-onion` | +| Descriptor version | `2`; `3` when a signed onion endpoint is present | +| Descriptor signature | BIP340 Schnorr, deterministic (no aux rand), v3 keeps `sig` plus `sig3` | | Nostr descriptor event | kind `38501`, `d = btf-descriptor-2` | | Nostr relay event | kind `38502`, `d = btf-relay-2` | | 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: - outbound Nostr relay dials use Tor; - outbound `.btf` rendezvous relay dials use Tor; +- outbound direct peer dials prefer signed `.onion` endpoints when a peer + advertises one; - destination names are sent to Tor as SOCKS5 domain-name CONNECT requests, so the local DNS resolver does not see them; - direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled; @@ -98,7 +100,7 @@ bitflash -operator -tor \ -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 ``` -## Full-node hidden service +## Direct full-node hidden service The legacy P2P listener still binds to port `8433`. You can expose it through Tor manually: @@ -108,6 +110,35 @@ HiddenServiceDir /var/lib/tor/bitflash-node/ HiddenServicePort 8433 127.0.0.1:8433 ``` -Current automatic peer discovery is `.btf`/rendezvous based, not onion-address -gossip. Treat direct full-node onion access as an operator/admin tool unless a -future release adds first-class onion peer advertisements. +After restarting Tor, read the generated onion name: + +```bash +sudo cat /var/lib/tor/bitflash-node/hostname +``` + +Then start Bitflash with Tor outbound enabled and advertise that hidden service: + +```bash +bitflash -tor \ + -onionservice=exampleexampleexampleexampleexampleexampleexampleexample.onion:8433 +``` + +That onion endpoint is signed into this node's self-certifying `.btf` +descriptor. New peers that resolve the descriptor and also run with `-tor` try +the `.onion:8433` P2P connection first. If it fails, they fall back to the +ordinary encrypted rendezvous path. Older nodes ignore the onion field and still +use the rendezvous relay because the descriptor keeps the legacy signature too. + +If the node has `-onionservice` but has not registered at any rendezvous relay +yet, it can still publish an onion-capable descriptor with +`meeting_node = rendezvous-pending`. Tor-capable peers can dial the hidden +service directly from that descriptor. Rendezvous becomes a fallback, not a +precondition for being discoverable. + +This means rendezvous relays stop being the only way to reach a node that has +published a hidden service. They remain useful bootstrap/fallback infrastructure, +but an already-discovered onion-capable peer can be reached directly over Tor. + +The node does not create or rotate Tor hidden services itself. Tor owns the +private key under `HiddenServiceDir`; back it up if you want the onion name to +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 @@ #include #include #include +#include +#include using json = nlohmann::json; @@ -166,10 +168,47 @@ static bool HexDecode(const std::string& s, unsigned char* out, size_t n) return true; } +bool NormalizeOnionEndpoint(const std::string& in, std::string& out) +{ + out.clear(); + std::string s = ToLowerTrim(in); + size_t colon = s.rfind(':'); + if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size()) + return false; + + std::string host = s.substr(0, colon); + std::string port = s.substr(colon + 1); + while (!host.empty() && host.back() == '.') host.pop_back(); + static const char* suffix = ".onion"; + const size_t suffixLen = strlen(suffix); + if (host.size() != 56 + suffixLen) + return false; + if (host.compare(host.size() - suffixLen, suffixLen, suffix) != 0) + return false; + for (size_t i = 0; i < 56; i++) + { + char c = host[i]; + if (!((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7'))) + return false; + } + for (size_t i = 0; i < port.size(); i++) + if (!isdigit((unsigned char)port[i])) + return false; + + errno = 0; + char* end = NULL; + long nPort = strtol(port.c_str(), &end, 10); + if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535) + return false; + + out = host + ":" + port; + return true; +} + // Digest signed over the descriptor fields (domain-separated). -static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, - const std::string& meeting_node, uint64_t created, - unsigned char out[32]) +static void DescriptorDigestV2(const unsigned char pubkey[32], const std::string& enc, + const std::string& meeting_node, uint64_t created, + unsigned char out[32]) { static const char* DS = ".btf-descriptor-v2"; SHA256_CTX ctx; @@ -184,9 +223,27 @@ static void DescriptorDigest(const unsigned char pubkey[32], const std::string& SHA256_Final(out, &ctx); } +static void DescriptorDigestV3(const unsigned char pubkey[32], const std::string& enc, + const std::string& meeting_node, const std::string& onion, + uint64_t created, unsigned char out[32]) +{ + static const char* DS = ".btf-descriptor-v3-onion"; + SHA256_CTX ctx; + SHA256_Init(&ctx); + SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS)); + SHA256_Update(&ctx, pubkey, 32); + SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size()); + SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size()); + SHA256_Update(&ctx, (const unsigned char*)onion.data(), onion.size()); + unsigned char be[8]; + for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff); + SHA256_Update(&ctx, be, 8); + SHA256_Final(out, &ctx); +} + std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], const std::string& enc, const std::string& meeting_node, - uint64_t created) + uint64_t created, const std::string& onion) { secp256k1_context* ctx = (secp256k1_context*)ctxv; secp256k1_keypair kp; @@ -197,17 +254,28 @@ std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); unsigned char digest[32]; - DescriptorDigest(pubkey, enc, meeting_node, created, digest); + DescriptorDigestV2(pubkey, enc, meeting_node, created, digest); unsigned char sig[64]; // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); json v = json::object(); - v["v"] = 2; + v["v"] = onion.empty() ? 2 : 3; v["pubkey"] = HexEncode(pubkey, 32); v["enc"] = enc; v["meeting_node"] = meeting_node; + if (!onion.empty()) + { + std::string onionNorm; + if (!NormalizeOnionEndpoint(onion, onionNorm)) return std::string(); + unsigned char digest3[32]; + DescriptorDigestV3(pubkey, enc, meeting_node, onionNorm, created, digest3); + unsigned char sig3[64]; + if (!secp256k1_schnorrsig_sign32(ctx, sig3, digest3, &kp, NULL)) return std::string(); + v["onion"] = onionNorm; + v["sig3"] = HexEncode(sig3, 64); + } v["created"] = created; v["sig"] = HexEncode(sig, 64); return v.dump(); @@ -242,15 +310,35 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr, if (!HexDecode(v["sig"].get(), sig, 64)) return false; unsigned char digest[32]; - DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest); + DescriptorDigestV2(expect_pubkey, enc, meeting_node, created, digest); secp256k1_xonly_pubkey xpub; if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; + std::string onion; + if (v.contains("onion")) + { + if (v["onion"].is_string() && v.contains("sig3") && v["sig3"].is_string()) + { + std::string onionNorm; + std::string onionRaw = v["onion"].get(); + if (NormalizeOnionEndpoint(onionRaw, onionNorm) && onionNorm == onionRaw) + { + unsigned char sig3[64]; + unsigned char digest3[32]; + DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3); + if (HexDecode(v["sig3"].get(), sig3, 64) && + secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub)) + onion = onionNorm; + } + } + } + memcpy(out.pubkey, expect_pubkey, 32); out.enc = enc; out.meeting_node = meeting_node; + out.onion = onion; out.created = created; return true; } 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 unsigned char pubkey[32]; std::string enc; // service x25519 public key (hex) for the E2E channel std::string meeting_node; // rendezvous node the service is registered at + std::string onion; // optional direct Tor hidden-service endpoint (host.onion:port) uint64_t created; }; +// Normalize and validate a direct Tor hidden-service endpoint. Only v3 onion +// names are accepted: 56 base32 chars plus ".onion", followed by ":PORT". +bool NormalizeOnionEndpoint(const std::string& in, std::string& out); + // Sign a descriptor. `ctx` is a secp256k1_context* (passed as void* to keep this // header free of secp headers); `seckey` is the 32-byte service secret key. // Returns the descriptor JSON as a string, or "" on failure. std::string SignDescriptor(void* ctx, const unsigned char seckey[32], const std::string& enc, const std::string& meeting_node, - uint64_t created); + uint64_t created, + const std::string& onion = std::string()); // Verify a descriptor is validly signed by `expect_pubkey` (the key the resolved // `.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 @@ // Bitflash entry point -- starts node threads then runs GUI (or headless). #include "headers_core.h" +#include "nostr.h" #include "proxy.h" #include "selftest.h" #include "walletcmd.h" @@ -134,10 +135,11 @@ static void PrintUsage() printf(" /connectbtf=PEER_BTF_ADDRESS\n"); printf(" /rvrelay=HOST:PORT\n"); printf(" /announcerelay=HOST:PORT\n"); + printf(" /onionservice=HOST.onion:PORT (advertise this node's Tor hidden service)\n"); printf("\n"); printf("Network:\n"); printf(" /port=N (P2P listen port, default 8433)\n"); - printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n"); + printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n"); printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); printf("\n"); @@ -286,6 +288,17 @@ static void ParseStartupArguments(int argc, char* argv[]) if (!announceRelay.empty()) strBtfAnnounceRelay = announceRelay; + string onionService = argval2(argc, argv, "/onionservice", "-onionservice"); + if (!onionService.empty()) + { + string err; + if (!BtfSetLocalOnionEndpoint(onionService, err)) + fprintf(stderr, "Ignoring /onionservice=%s: %s\n", onionService.c_str(), err.c_str()); + else + fprintf(stderr, "Direct onion peer endpoint advertised: %s\n", + BtfLocalOnionEndpoint().c_str()); + } + bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); if (fTorMode) { 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 @@ #include #endif #include +#include +#include #include "btfaddr.h" #include "btftunnel.h" #include "proxy.h" @@ -170,7 +172,7 @@ static const char* pszSockSite[SOCK_SITES] = { "external-ip probe", "listen socket", "inbound accept", "rendezvous dial", "rendezvous listen", "rendezvous accept", "loopback pair listener", "loopback pair app end", "loopback pair pump end", - "nostr relay", "stratum bridge" + "nostr relay", "direct onion peer", "stratum bridge" }; static string SockAccountingText() @@ -627,6 +629,7 @@ struct CachedBtfPeer { string btfAddr; string meeting; + string onion; string encHex; int64 lastSeen; // The peer's own signed descriptor, as it announced itself. Kept verbatim @@ -685,6 +688,8 @@ void LoadCachedBtfPeers(vector& out) CachedBtfPeer p; p.btfAddr = o["btf"].get(); p.meeting = o["meeting"].get(); + if (o.contains("onion") && o["onion"].is_string()) + btf::NormalizeOnionEndpoint(o["onion"].get(), p.onion); p.encHex = o["enc"].get(); p.lastSeen = o.value("seen", (int64)0); if (o.contains("desc") && o["desc"].is_string()) @@ -708,7 +713,8 @@ static CCriticalSection cs_btfPeerCache; // learned about by exchange must not cost us the ability to pass it on. static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, const unsigned char enc_pub[32], - const string& strDesc = string()) + const string& strDesc = string(), + const string& strOnion = string()) { CRITICAL_BLOCK(cs_btfPeerCache) { @@ -716,11 +722,15 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, LoadCachedBtfPeers(peers); string strKeepDesc = strDesc; + string strKeepOnion; + btf::NormalizeOnionEndpoint(strOnion, strKeepOnion); for (size_t i = 0; i < peers.size(); i++) if (peers[i].btfAddr == strBtfAddr) { if (strKeepDesc.empty()) strKeepDesc = peers[i].desc; + if (strKeepOnion.empty()) + strKeepOnion = peers[i].onion; peers.erase(peers.begin() + i); break; } @@ -728,6 +738,7 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, CachedBtfPeer p; p.btfAddr = strBtfAddr; p.meeting = strMeeting; + p.onion = strKeepOnion; p.encHex = BytesToHex(enc_pub, 32); p.lastSeen = GetTime(); p.desc = strKeepDesc; @@ -742,6 +753,8 @@ static void RememberBtfPeer(const string& strBtfAddr, const string& strMeeting, nlohmann::json o; o["btf"] = q.btfAddr; o["meeting"] = q.meeting; + if (!q.onion.empty()) + o["onion"] = q.onion; o["enc"] = q.encHex; o["seen"] = q.lastSeen; if (!q.desc.empty()) @@ -830,7 +843,7 @@ static int TryBtfSeeds() foreach(const string& strRelay, relays) { if (fShutdown) return nConnected; - if (ConnectNodeBtfResolved(s.first, strRelay, enc)) + if (ConnectNodeBtfResolved(s.first, strRelay, "", enc)) { LogPrint("net", "btfseed: reached %s via %s\n", s.first.c_str(), strRelay.c_str()); @@ -868,7 +881,7 @@ void ThreadReconnectCachedBtfPeers(void* parg) if (fShutdown) return; unsigned char enc[32]; if (!HexToBytes(p.encHex, enc, 32)) continue; - if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, enc)) + if (ConnectNodeBtfResolved(p.btfAddr, p.meeting, p.onion, enc)) { nConnected++; if (nConnected >= 8) break; // enough to bootstrap; the rest can wait @@ -959,14 +972,37 @@ int BtfPexAccept(const vector& vDesc) if (!HexToBytes(d.enc, enc, 32)) continue; - RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc); + RememberBtfPeer(strAddr, d.meeting_node, enc, strDesc, d.onion); nKept++; } return nKept; } +static bool SplitHostPort(const string& strHostPort, string& strHost, unsigned short& nPort) +{ + size_t colon = strHostPort.rfind(':'); + if (colon == string::npos || colon == 0 || colon + 1 >= strHostPort.size()) + return false; + string port = strHostPort.substr(colon + 1); + for (size_t i = 0; i < port.size(); i++) + if (!isdigit((unsigned char)port[i])) + return false; + + errno = 0; + char* end = NULL; + long n = strtol(port.c_str(), &end, 10); + if (errno == ERANGE || end == NULL || *end != '\0' || n <= 0 || n > 65535) + return false; + + strHost = strHostPort.substr(0, colon); + nPort = (unsigned short)n; + return true; +} + static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char pk[32], - const string& strMeeting, const unsigned char enc_pub[32]) + const string& strMeeting, const string& strOnion, + const unsigned char enc_pub[32], + const string& strDesc = string()) { CAddress addr = BtfMarkerAddr(pk); CNode* pnode = FindNode(addr.ip); @@ -976,16 +1012,42 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p return pnode; } - BtfChurnNoteDialAttempt(strBtfAddr, strMeeting); - size_t colon = strMeeting.rfind(':'); - if (colon == string::npos) + string strOnionNorm; + if (BtfSocks5ProxyEnabled() && btf::NormalizeOnionEndpoint(strOnion, strOnionNorm)) { - BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); - return NULL; + string strOnionHost; + unsigned short nOnionPort = 0; + if (SplitHostPort(strOnionNorm, strOnionHost, nOnionPort)) + { + BtfChurnNoteDialAttempt(strBtfAddr, strOnionNorm); + SOCKET hOnionSocket = BtfConnectSocket(strOnionHost, nOnionPort, SOCK_ONION_PEER, 20); + if (hOnionSocket != INVALID_SOCKET) + { + if (fDebug) + LogPrint("net", "connected %s via direct onion %s\n", + strBtfAddr.c_str(), strOnionNorm.c_str()); + BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, true); + RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm); + + pnode = new CNode(hOnionSocket, addr, false); + pnode->strBtfAddr = strBtfAddr; + pnode->strBtfMeeting = string("onion ") + strOnionNorm; + pnode->AddRef(); + CRITICAL_BLOCK(cs_vNodes) + vNodes.push_back(pnode); + return pnode; + } + if (fDebug) + LogPrint("net", "ConnectNodeBtf: direct onion to %s at %s failed, falling back to rendezvous\n", + strBtfAddr.c_str(), strOnionNorm.c_str()); + BtfChurnNoteDialResult(strBtfAddr, strOnionNorm, false); + } } - string strHost = strMeeting.substr(0, colon); - int nPort = atoi(strMeeting.substr(colon + 1).c_str()); - if (nPort <= 0 || nPort > 65535) + + BtfChurnNoteDialAttempt(strBtfAddr, strMeeting); + string strHost; + unsigned short nPort = 0; + if (!SplitHostPort(strMeeting, strHost, nPort)) { BtfChurnNoteDialResult(strBtfAddr, strMeeting, false); return NULL; @@ -1005,7 +1067,7 @@ static CNode* ConnectNodeBtfTail(const string& strBtfAddr, const unsigned char p BtfChurnNoteDialResult(strBtfAddr, strMeeting, true); // This one answered -- worth trying first next time we start. - RememberBtfPeer(strBtfAddr, strMeeting, enc_pub); + RememberBtfPeer(strBtfAddr, strMeeting, enc_pub, strDesc, strOnionNorm); // Add node pnode = new CNode(hSocket, addr, false); @@ -1035,9 +1097,10 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) LogPrint("net", "trying %s\n", strBtfAddr.c_str()); string strMeeting; + string strOnion; unsigned char enc_pub[32]; BtfChurnNoteResolveAttempt(); - if (!BtfResolve(strBtfAddr, strMeeting, enc_pub)) + if (!BtfResolve(strBtfAddr, strMeeting, enc_pub, &strOnion)) { BtfChurnNoteResolveResult(false); if (fDebug) @@ -1045,7 +1108,7 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) return NULL; } BtfChurnNoteResolveResult(true); - return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); + return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub); } // Same as ConnectNodeBtf, but skips the Nostr resolve step because the caller @@ -1054,7 +1117,8 @@ CNode* ConnectNodeBtf(const string& strBtfAddr) // address). Makes zero relay connections -- only talks to the peer's // rendezvous meeting node. CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, - const unsigned char enc_pub[32]) + const string& strOnion, const unsigned char enc_pub[32], + const string& strDesc) { unsigned char pk[32]; if (!btf::ParseAddress(strBtfAddr, pk)) @@ -1064,7 +1128,7 @@ CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting } if (BtfLocalAddress() == strBtfAddr) return NULL; // ourselves - return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, enc_pub); + return ConnectNodeBtfTail(strBtfAddr, pk, strMeeting, strOnion, enc_pub, strDesc); } // 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 bool GetMyExternalIP(unsigned int& ipRet); CNode* ConnectNodeBtf(const string& strBtfAddr); CNode* ConnectNodeBtfResolved(const string& strBtfAddr, const string& strMeeting, - const unsigned char enc_pub[32]); + const string& strOnion, const unsigned char enc_pub[32], + const string& strDesc=string()); void ThreadBtfAccept(void* parg); void ThreadBtfConnect(void* parg); 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 vBtfMeetingRelays = { }; string strBtfActiveRelay; static CCriticalSection cs_activeRelay; +static string strBtfOnionEndpoint; +static CCriticalSection cs_onionEndpoint; // Only the rendezvous we actually registered at (BtfSetActiveRelay, called from // ThreadBtfAccept once registration succeeds). Empty until then, on purpose: it @@ -133,6 +135,26 @@ void BtfSetActiveRelay(const string& relay) strBtfActiveRelay = relay; } +bool BtfSetLocalOnionEndpoint(const string& endpoint, string& errOut) +{ + string norm; + if (!btf::NormalizeOnionEndpoint(endpoint, norm)) + { + errOut = "expected v3 onion endpoint HOST.onion:PORT"; + return false; + } + CRITICAL_BLOCK(cs_onionEndpoint) + strBtfOnionEndpoint = norm; + return true; +} + +string BtfLocalOnionEndpoint() +{ + CRITICAL_BLOCK(cs_onionEndpoint) + return strBtfOnionEndpoint; + return ""; +} + // A relay this node advertises on Nostr for others to discover (/announcerelay). string strBtfAnnounceRelay; // Relays learned from other operators' Nostr announcements. @@ -370,10 +392,16 @@ std::string BtfLocalDescriptor() string meeting; CRITICAL_BLOCK(cs_activeRelay) meeting = strBtfActiveRelay; + string onion = BtfLocalOnionEndpoint(); if (meeting.empty()) - return ""; + { + if (onion.empty()) + return ""; + meeting = "rendezvous-pending"; + } return btf::SignDescriptor(g_nostrKey.ctx, g_nostrKey.seckey, - g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime()); + g_nostrKey.EncPubHex(), meeting, (uint64_t)GetTime(), + onion); } // The shared secp256k1 context, so net.cpp can verify descriptors that arrive @@ -806,17 +834,21 @@ static void PublishDescriptor(CWebSocket& ws, CNostrKey& key) // meeting_node is the rendezvous relay this node's hidden service // (ThreadBtfAccept in net.cpp) is registered at -- where clients dial us. string meeting_node = BtfActiveRelay(); + string onion = BtfLocalOnionEndpoint(); if (meeting_node.empty()) { - // Not registered anywhere yet. Publishing now would advertise a meeting - // node we can't be reached at, and peers would waste dials on it. The - // ThreadNostrSeed loop republishes on every pass, so skipping costs - // nothing but the delay until ThreadBtfAccept registers. - LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); - return; + if (onion.empty()) + { + // Not registered anywhere yet, and no direct onion endpoint exists. + // Publishing now would advertise a meeting node we can't be reached at. + LogPrint("nostr", "Nostr: skipping descriptor publish, no rendezvous registered yet\n"); + return; + } + meeting_node = "rendezvous-pending"; } string desc = btf::SignDescriptor(key.ctx, key.seckey, key.EncPubHex(), - meeting_node, (uint64_t)GetTime()); + meeting_node, (uint64_t)GetTime(), + onion); if (desc.empty()) return; json tags = json::array({ json::array({ "d", BTF_DESC_DTAG }) }); @@ -1018,7 +1050,7 @@ static void HandlePoolAnnouncement(const json& ev) // Resolve a `.btf` address via a relay: fetch the descriptor published by that // address's key and verify it self-certifies under the decoded pubkey. static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, - btf::Descriptor& out) + btf::Descriptor& out, string* rawOut = NULL) { unsigned char pk[32]; if (!btf::ParseAddress(btfAddr, pk)) @@ -1045,7 +1077,11 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, const json& ev = j[2]; if (ev.contains("content") && btf::VerifyDescriptor(ctx, ev["content"].get(), pk, out)) + { + if (rawOut) + *rawOut = ev["content"].get(); return true; + } } else if (t == "EOSE") break; @@ -1057,8 +1093,10 @@ static bool ResolveDescriptor(CWebSocket& ws, void* ctx, const string& btfAddr, // public discovery relays for the owner's self-certified descriptor. Used by // ConnectNodeBtf in net.cpp. bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, - unsigned char enc_pub[32]) + unsigned char enc_pub[32], std::string* onionHostPort) { + if (onionHostPort) + onionHostPort->clear(); if (!EnsureNostrKey()) return false; for (int i = 0; i < nNostrRelays && !fShutdown; i++) @@ -1076,10 +1114,15 @@ bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, } lease.fOk = true; if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") - continue; // descriptor predates the owner's relay config + { + if (!onionHostPort || d.onion.empty()) + continue; // caller needs rendezvous, or descriptor has no direct path + } if (!HexDecode(d.enc, enc_pub, 32)) continue; meetingHostPort = d.meeting_node; + if (onionHostPort) + *onionHostPort = d.onion; return true; } CATCH_PRINT_EXCEPTION("BtfResolve") @@ -1168,12 +1211,15 @@ bool BtfResolveMany(const std::vector& btfAddrs, btf::Descriptor d; if (!btf::VerifyDescriptor(g_nostrKey.ctx, ev["content"].get(), pk, d)) continue; - if (d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") + if ((d.meeting_node.empty() || d.meeting_node == "rendezvous-pending") && + d.onion.empty()) continue; BtfResolvedPeer rp; if (!HexDecode(d.enc, rp.enc_pub, 32)) continue; rp.meetingHostPort = d.meeting_node; + rp.onionHostPort = d.onion; + rp.descriptor = ev["content"].get(); out[ai->second] = rp; pending.erase(ai->second); } @@ -1650,11 +1696,13 @@ static void ConnectDiscoveredBtfPeers() DialCtx* ctx = (DialCtx*)arg; string addr = ctx->addr; string meeting = ctx->rp.meetingHostPort; + string onion = ctx->rp.onionHostPort; + string desc = ctx->rp.descriptor; unsigned char enc_pub[32]; memcpy(enc_pub, ctx->rp.enc_pub, 32); delete ctx; - CNode* pnode = ConnectNodeBtfResolved(addr, meeting, enc_pub); + CNode* pnode = ConnectNodeBtfResolved(addr, meeting, onion, enc_pub, desc); CRITICAL_BLOCK(cs_btfPeerState) { 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(); // Record the relay this node just registered its service at (thread-safe). void BtfSetActiveRelay(const std::string& relay); +// Optional Tor hidden-service endpoint for this node's normal P2P listener. +// Set by /onionservice=HOST.onion:PORT. The endpoint is signed into this node's +// .btf descriptor, so peers with -tor can dial it directly before falling back +// to rendezvous relays. +bool BtfSetLocalOnionEndpoint(const std::string& endpoint, std::string& errOut); +std::string BtfLocalOnionEndpoint(); + // If set (via /announcerelay=host:port), this node announces that relay on // Nostr so other nodes discover it automatically -- no manual seed-list edit. extern std::string strBtfAnnounceRelay; @@ -94,12 +101,15 @@ void* BtfSecpContext(); // self-certified descriptor and return its meeting node ("host:port") and // x25519 public key for the end-to-end channel. bool BtfResolve(const std::string& btfAddr, std::string& meetingHostPort, - unsigned char enc_pub[32]); + unsigned char enc_pub[32], + std::string* onionHostPort = NULL); // Result of resolving one .btf address via BtfResolveMany. struct BtfResolvedPeer { std::string meetingHostPort; + std::string onionHostPort; + std::string descriptor; unsigned char enc_pub[32]; }; 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) ------------------------------------------------------- /port=N listen on P2P port N (default 8433) /socks=HOST:PORT - route outbound Nostr and .btf rendezvous dials through a - SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor + route outbound Nostr, .btf rendezvous, and direct onion peer + dials through a SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor or /socks=[::1]:9050 for an IPv6 loopback proxy /tor[=HOST:PORT] - Tor mode for outbound Nostr and .btf rendezvous dials; + Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer dials; defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050 /solomine mine without requiring a connected peer (local testing) /operator operator mode (run pool server) @@ -72,6 +72,8 @@ Command-line options (in addition to the original ones) override meeting relay list with one relay /announcerelay=HOST:PORT announce this relay on Nostr for discovery + /onionservice=HOST.onion:PORT + advertise this node's Tor hidden service in its signed .btf descriptor 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 @@ // and against a temporary data directory. #include "headers_core.h" +#include "btfaddr.h" #include "bip32.h" #include "proxy.h" #include "selftest.h" @@ -1507,6 +1508,14 @@ static int RunSocks5ProxySelfTest() !BtfIsTorOnionHost("relay.example") && !BtfIsTorOnionHost(".onion"), "detects .onion hosts without accepting lookalikes") ? 0 : 1; + std::string onion; + nFail += Check(btf::NormalizeOnionEndpoint("ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD.onion.:8433", onion) && + onion == "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433", + "normalizes v3 onion peer endpoint") ? 0 : 1; + nFail += Check(!btf::NormalizeOnionEndpoint("short.onion:8433", onion) && + !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:0", onion) && + !btf::NormalizeOnionEndpoint("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc1.onion:8433", onion), + "rejects malformed onion peer endpoints") ? 0 : 1; BtfClearSocks5Proxy(); 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 SOCK_PAIR_APP, // loopback pair: the app end handed to CNode SOCK_PAIR_PUMP, // loopback pair: the pump end SOCK_NOSTR, // outbound to a Nostr relay + SOCK_ONION_PEER, // direct outbound to a peer's Tor hidden service SOCK_STRATUM_BRIDGE, // local Stratum bridge listener/miner sockets SOCK_SITES }; 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() } const std::string ENC_HEX = "aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899"; + const std::string ONION = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion:8433"; // helper: fresh keypair -> (seckey, xonly pubkey) auto newKey = [&](unsigned char sk[32], unsigned char pk[32]) { @@ -108,6 +109,17 @@ int main() CHECK(!btf::VerifyDescriptor(ctx, d, pk2, bad), "different key rejected"); } + printf("descriptor_signs_optional_onion_endpoint\n"); + { + unsigned char sk[32], pk[32]; newKey(sk, pk); + std::string d = btf::SignDescriptor(ctx, sk, ENC_HEX, "meeting-node-abc", 1000, ONION); + CHECK(!d.empty(), "sign produced onion descriptor"); + btf::Descriptor got; + CHECK(btf::VerifyDescriptor(ctx, d, pk, got), "onion descriptor verifies"); + CHECK(got.meeting_node == "meeting-node-abc" && got.enc == ENC_HEX && + got.created == 1000 && got.onion == ONION, "onion fields match"); + } + printf("descriptor_rejects_tampering\n"); { unsigned char sk[32], pk[32]; newKey(sk, pk); @@ -117,6 +129,16 @@ int main() CHECK(!btf::VerifyDescriptor(ctx, j.dump(), pk, o), "tampered meeting_node rejected"); json j2 = json::parse(d); j2["enc"] = "00000000000000000000000000000000000000000000000000000000deadbeef"; CHECK(!btf::VerifyDescriptor(ctx, j2.dump(), pk, o), "tampered enc rejected"); + + std::string d3 = btf::SignDescriptor(ctx, sk, ENC_HEX, "node-1", 5, ONION); + json j3 = json::parse(d3); + j3["onion"] = "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcde.onion:8433"; + CHECK(btf::VerifyDescriptor(ctx, j3.dump(), pk, o) && o.onion.empty(), + "tampered onion ignored rather than trusted"); + json j4 = json::parse(d3); + j4["sig3"] = std::string(128, '0'); + CHECK(btf::VerifyDescriptor(ctx, j4.dump(), pk, o) && o.onion.empty(), + "bad onion signature ignored rather than trusted"); } printf("full_resolve_chain_from_address\n"); -- cgit v1.2.3 From 53cc17d621f55b8904812fae5df90ef089bf5aaf Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 11:54:35 -0300 Subject: net: add managed Tor hidden service bootstrap --- README.md | 13 +- docs/rendezvous.md | 8 +- docs/tor.md | 52 +++++- src/Makefile | 5 +- src/main_gui.cpp | 65 ++++--- src/makefile.mingw | 5 +- src/net.cpp | 2 + src/readme.txt | 3 + src/selftest.cpp | 37 +++- src/tor.cpp | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tor.h | 22 +++ 11 files changed, 665 insertions(+), 39 deletions(-) create mode 100644 src/tor.cpp create mode 100644 src/tor.h (limited to 'src/selftest.cpp') diff --git a/README.md b/README.md index 181b6e8..a7ce2b2 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,7 @@ Other options worth knowing: -port=N # P2P listen port, default 8433 -socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials -tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050 +-managedtor[=PATH] # start Tor, create a hidden service, advertise its onion -onionservice=HOST.onion:PORT # advertise this node's Tor hidden service -debug # verbose log; without it debug.log is nearly silent -help # full list @@ -357,10 +358,14 @@ proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. `-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at `127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials, and signed direct `.onion` peer dials through Tor, and keeps external-IP probes -disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. To make your own -node reachable without a rendezvous relay, expose port `8433` as a Tor hidden -service and pass `-onionservice=HOST.onion:8433`; peers with `-tor` will try it -before falling back to rendezvous. +disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. + +`-managedtor[=PATH]` starts a Tor process for this node, writes a local `torrc`, +creates a v3 hidden service for port `8433`, routes outbound discovery through +that Tor instance, and signs the generated `.onion:8433` endpoint into the +node's `.btf` descriptor. Without `PATH`, Bitflash looks for `tor/tor.exe`, +`tor.exe`, or `tor` beside the binary and then on `PATH`. The hidden-service key +is stored under the Bitflash data directory in `managed-tor/onion-service/`. ### When something looks wrong diff --git a/docs/rendezvous.md b/docs/rendezvous.md index e1888b9..68b5df3 100644 --- a/docs/rendezvous.md +++ b/docs/rendezvous.md @@ -308,9 +308,11 @@ tunnel. unreachable, discovery stalls until one is reachable again. Relay TLS is verified, which closes a man in the middle path on discovery, but availability still rests on the relay set. -- **Direct onion is not automatic Tor configuration.** Bitflash signs and - advertises a hidden-service endpoint, and Tor-mode peers can dial it. The Tor - daemon still owns hidden-service creation, private keys, rotation and uptime. +- **Direct onion needs Tor.** With `-managedtor`, Bitflash starts a Tor process, + writes a local hidden-service config, and advertises the generated onion + endpoint. With `-onionservice`, an operator can still run Tor manually and + provide the endpoint. In both cases Tor owns the hidden-service private key and + uptime. ## 11. Wire reference diff --git a/docs/tor.md b/docs/tor.md index b4f88f9..1c0f589 100644 --- a/docs/tor.md +++ b/docs/tor.md @@ -36,6 +36,47 @@ If both `-tor` and `-socks` are present, Tor mode wins and the node prints a warning. That keeps a command line with `-tor` from silently becoming a custom proxy route. +## Managed Tor mode + +`-managedtor` starts a Tor process for this node, writes a local `torrc`, creates +a v3 hidden service for the normal Bitflash P2P listener, routes outbound +discovery through that Tor instance, and signs the generated `.onion:8433` +endpoint into the node's `.btf` descriptor. + +```bash +bitflash -managedtor +``` + +Without a value, Bitflash looks for Tor in this order: + +1. `tor/tor.exe` or `tor/tor` beside the Bitflash binary; +2. `tor.exe` or `tor` beside the Bitflash binary; +3. `tor.exe` or `tor` on `PATH`. + +To point at a specific Tor executable: + +```bash +bitflash -managedtor=/opt/tor/bin/tor +bitflash -managedtor=C:\Tor Browser\Browser\TorBrowser\Tor\tor.exe +``` + +Managed Tor writes its state under the Bitflash data directory: + +```text +managed-tor/ + torrc + data/ + onion-service/ +``` + +The private key for the onion address lives in `managed-tor/onion-service/`. +Back up that directory if you want the same onion address after moving the node. +Delete it only if you intentionally want a fresh onion endpoint. + +`-managedtor` takes precedence over `-tor` and `-socks`, because it must choose +the local SOCKS port it starts. Diagnostics report the current managed Tor +state, including the SOCKS port and onion endpoint once Tor has generated it. + When Tor mode is enabled: - outbound Nostr relay dials use Tor; @@ -100,10 +141,10 @@ bitflash -operator -tor \ -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 ``` -## Direct full-node hidden service +## Manual direct full-node hidden service -The legacy P2P listener still binds to port `8433`. You can expose it through -Tor manually: +Managed Tor is the preferred path for ordinary nodes. Operators can still expose +the P2P listener through a separately managed Tor service: ```text HiddenServiceDir /var/lib/tor/bitflash-node/ @@ -139,6 +180,5 @@ This means rendezvous relays stop being the only way to reach a node that has published a hidden service. They remain useful bootstrap/fallback infrastructure, but an already-discovered onion-capable peer can be reached directly over Tor. -The node does not create or rotate Tor hidden services itself. Tor owns the -private key under `HiddenServiceDir`; back it up if you want the onion name to -stay stable. +Tor owns the private key under `HiddenServiceDir`; back it up if you want the +onion name to stay stable. diff --git a/src/Makefile b/src/Makefile index 511fe79..19ad33a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -53,7 +53,7 @@ LIBS = -L$(SECP_LIB) -lsecp256k1 \ HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h + selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o @@ -61,7 +61,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o @@ -194,5 +194,6 @@ tests: test_bip32 bitflash-node ./bitflash-node -selftest=pool-stratum ./bitflash-node -selftest=parse-money ./bitflash-node -selftest=socks5-proxy + ./bitflash-node -selftest=managed-tor .PHONY: tests fuzz-net-message-smoke fuzz-script-smoke diff --git a/src/main_gui.cpp b/src/main_gui.cpp index 239fb60..deed471 100644 --- a/src/main_gui.cpp +++ b/src/main_gui.cpp @@ -4,6 +4,7 @@ #include "nostr.h" #include "proxy.h" #include "selftest.h" +#include "tor.h" #include "walletcmd.h" #include // hardware_concurrency, to sanity-check /genproclimit #ifndef _WIN32 @@ -113,7 +114,7 @@ static void PrintUsage() printf(" /gen\n"); printf(" /nogui or /daemon\n"); printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); - printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n"); + printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n"); printf("\n"); printf("Mining mode:\n"); printf(" /operator\n"); @@ -141,6 +142,7 @@ static void PrintUsage() printf(" /port=N (P2P listen port, default 8433)\n"); printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n"); printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); + printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n"); printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); printf("\n"); printf("Wallet:\n"); @@ -284,6 +286,24 @@ static void ParseStartupArguments(int argc, char* argv[]) vBtfMeetingRelays.push_back(rvRelay); } + // net.cpp has described nListenPort as "tunable via /port" since it was + // written, but nothing ever read the option, so the port was fixed at 8433 + // and a second node could not start on a machine already running one. Parse + // it before Tor setup so a managed hidden service maps the real listener. + string strPort = argval2(argc, argv, "/port", "-port"); + if (!strPort.empty()) + { + int nPort = atoi(strPort.c_str()); + if (nPort <= 0 || nPort > 65535) + fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str()); + else + { + nListenPort = htons((unsigned short)nPort); + // Or we would announce a port we never bound. + addrLocalHost.port = nListenPort; + } + } + string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); if (!announceRelay.empty()) strBtfAnnounceRelay = announceRelay; @@ -299,10 +319,23 @@ static void ParseStartupArguments(int argc, char* argv[]) BtfLocalOnionEndpoint().c_str()); } + bool fManagedTor = arg(argc, argv, "/managedtor") || arg(argc, argv, "-managedtor"); bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); - if (fTorMode) + string socksProxy = argval2(argc, argv, "/socks", "-socks"); + if (fManagedTor) + { + if (fTorMode || !socksProxy.empty()) + fprintf(stderr, "Warning: /managedtor takes precedence over /tor and /socks\n"); + + string torPath = argval2(argc, argv, "/managedtor", "-managedtor"); + string err; + if (!BtfStartManagedTor(torPath, err)) + fprintf(stderr, "Ignoring /managedtor=%s: %s\n", torPath.c_str(), err.c_str()); + else + fprintf(stderr, "Managed Tor enabled: %s\n", BtfManagedTorStatus().c_str()); + } + else if (fTorMode) { - string socksProxy = argval2(argc, argv, "/socks", "-socks"); if (!socksProxy.empty()) fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n", socksProxy.c_str()); @@ -317,7 +350,6 @@ static void ParseStartupArguments(int argc, char* argv[]) } else { - string socksProxy = argval2(argc, argv, "/socks", "-socks"); if (!socksProxy.empty()) { string err; @@ -348,22 +380,6 @@ static void ParseStartupArguments(int argc, char* argv[]) vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1))); } - // net.cpp has described nListenPort as "tunable via /port" since it was - // written, but nothing ever read the option, so the port was fixed at 8433 - // and a second node could not start on a machine already running one. - string strPort = argval2(argc, argv, "/port", "-port"); - if (!strPort.empty()) - { - int nPort = atoi(strPort.c_str()); - if (nPort <= 0 || nPort > 65535) - fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str()); - else - { - nListenPort = htons((unsigned short)nPort); - // Or we would announce a port we never bound. - addrLocalHost.port = nListenPort; - } - } } // Report a startup failure somewhere the user will actually see it. @@ -715,7 +731,12 @@ int main(int argc, char* argv[]) ReacceptWalletTransactions(); - if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } + if (!StartNode(strErrors)) + { + fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); + BtfStopManagedTor(); + return 1; + } if (nMineMode == MINE_OPERATOR) { gPoolServerRunning = true; @@ -748,6 +769,7 @@ int main(int argc, char* argv[]) printf("Running headless. Ctrl-C to stop.\n"); while (!fShutdown) Sleep(500); StopNode(); + BtfStopManagedTor(); // Checkpoints, releases wallet.dat from this directory's Berkeley DB // environment, and closes it. Declared since 2009 and never once // called here, which is why a wallet.dat copied elsewhere would not @@ -767,6 +789,7 @@ int main(int argc, char* argv[]) int ret = RunGUI(argc, argv); fShutdown = true; StopNode(); + BtfStopManagedTor(); DBFlush(true); return ret; #endif diff --git a/src/makefile.mingw b/src/makefile.mingw index f82247a..333be96 100644 --- a/src/makefile.mingw +++ b/src/makefile.mingw @@ -37,7 +37,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ - obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o + obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o @@ -51,7 +51,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ key.h bignum.h script.h db.h base58.h net.h nostr.h \ randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ - selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h + selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h all: bitflash.exe @@ -113,3 +113,4 @@ tests: test_bip32.exe bitflash.exe ./bitflash.exe -selftest=consensus-limits ./bitflash.exe -selftest=pool-stratum ./bitflash.exe -selftest=socks5-proxy + ./bitflash.exe -selftest=managed-tor diff --git a/src/net.cpp b/src/net.cpp index ab17332..6bc3c1c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -22,6 +22,7 @@ #include "btfaddr.h" #include "btftunnel.h" #include "proxy.h" +#include "tor.h" void ThreadReconnectCachedBtfPeers(void* parg); void ThreadMessageHandler2(void* parg); @@ -296,6 +297,7 @@ string GetDiagnosticsText() str += strprintf(" peers watched %d of %d held by poll%s\n", nPeersWatched, nHeld, nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); + str += strprintf(" managed Tor %s\n", BtfManagedTorStatus().c_str()); if (nBlocksReceived > 0) str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", diff --git a/src/readme.txt b/src/readme.txt index 1e9cffc..9ddbc24 100644 --- a/src/readme.txt +++ b/src/readme.txt @@ -49,6 +49,9 @@ Command-line options (in addition to the original ones) /tor[=HOST:PORT] Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer dials; defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050 + /managedtor[=PATH] + start Tor, create a v3 hidden service for this node, route outbound + discovery through it, and advertise the generated onion endpoint /solomine mine without requiring a connected peer (local testing) /operator operator mode (run pool server) /participant=ADDR diff --git a/src/selftest.cpp b/src/selftest.cpp index a0d9edd..4cb872e 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -11,6 +11,7 @@ #include "proxy.h" #include "selftest.h" #include "sockcount.h" +#include "tor.h" #include "walletcmd.h" extern int RunPoolStratumSelfTest(); @@ -1565,6 +1566,38 @@ static int RunSocks5ProxySelfTest() return nFail == 0 ? 0 : 1; } +static int RunManagedTorSelfTest() +{ + printf("managed-tor self-test\n"); + int nFail = 0; + + std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data", + "C:/Bitflash Managed Tor/onion-service", + 19050, 8433); + nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos, + "quotes the Tor data directory") ? 0 : 1; + nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos, + "binds SOCKS5 to localhost only") ? 0 : 1; + nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos && + torrc.find("IsolateClientAddr") != std::string::npos && + torrc.find("IsolateDestAddr") != std::string::npos && + torrc.find("IsolateDestPort") != std::string::npos, + "enables strict Tor stream isolation") ? 0 : 1; + nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos, + "writes a hidden service directory") ? 0 : 1; + nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos, + "requests a v3 onion service") ? 0 : 1; + nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos, + "maps the onion service to the Bitflash P2P listener") ? 0 : 1; + nFail += Check(BtfManagedTorStatus() == "disabled", + "managed Tor starts disabled") ? 0 : 1; + + printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", + nFail, nFail == 1 ? "" : "s"); + fflush(stdout); + return nFail == 0 ? 0 : 1; +} + int RunSelfTest(const std::string& name) { AttachTerminal(); @@ -1589,8 +1622,10 @@ int RunSelfTest(const std::string& name) return RunParseMoneySelfTest(); if (name == "socks5-proxy") return RunSocks5ProxySelfTest(); + if (name == "managed-tor") + return RunManagedTorSelfTest(); printf("Unknown self-test '%s'\n", name.c_str()); - printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy\n"); + printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, managed-tor\n"); return 1; } diff --git a/src/tor.cpp b/src/tor.cpp new file mode 100644 index 0000000..30a6f0b --- /dev/null +++ b/src/tor.cpp @@ -0,0 +1,492 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. + +#include "headers_core.h" +#include "tor.h" +#include "nostr.h" +#include "proxy.h" + +#include + +#ifndef _WIN32 +#include +#include +#endif + +static CCriticalSection cs_managedTor; +static bool g_fManagedTor = false; +static bool g_fManagedTorStopping = false; +static unsigned short g_managedTorSocksPort = 0; +static unsigned short g_managedTorP2PPort = 0; +static std::string g_managedTorPath; +static std::string g_managedTorDataDir; +static std::string g_managedTorHiddenServiceDir; +static std::string g_managedTorOnion; +static std::string g_managedTorStatus; +#ifdef _WIN32 +static PROCESS_INFORMATION g_managedTorProcess; +#else +static pid_t g_managedTorPid = -1; +#endif + +static std::string PathJoin(const std::string& a, const std::string& b) +{ + if (a.empty()) + return b; + char last = a[a.size() - 1]; + if (last == '/' || last == '\\') + return a + b; + return a + "/" + b; +} + +static std::string NormalizeTorrcPath(std::string path) +{ + for (size_t i = 0; i < path.size(); i++) + if (path[i] == '\\') + path[i] = '/'; + return path; +} + +static std::string QuoteTorrcPath(const std::string& path) +{ + std::string out = "\""; + for (size_t i = 0; i < path.size(); i++) + { + if (path[i] == '"' || path[i] == '\\') + out += '\\'; + out += path[i]; + } + out += "\""; + return out; +} + +static std::string QuoteCommandArg(const std::string& arg) +{ + std::string out = "\""; + for (size_t i = 0; i < arg.size(); i++) + { + if (arg[i] == '"') + out += '\\'; + out += arg[i]; + } + out += "\""; + return out; +} + +static bool EnsureDir(const std::string& path, std::string& errOut) +{ + if (path.empty()) + { + errOut = "directory path is empty"; + return false; + } +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path.c_str()); + if (attr != INVALID_FILE_ATTRIBUTES) + { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return true; + errOut = strprintf("%s exists and is not a directory", path.c_str()); + return false; + } + if (CreateDirectoryA(path.c_str(), NULL)) + return true; + DWORD err = GetLastError(); + if (err == ERROR_ALREADY_EXISTS) + return true; + errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err); + return false; +#else + struct stat st; + if (stat(path.c_str(), &st) == 0) + { + if (S_ISDIR(st.st_mode)) + return true; + errOut = strprintf("%s exists and is not a directory", path.c_str()); + return false; + } + if (mkdir(path.c_str(), 0700) == 0 || errno == EEXIST) + return true; + errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno)); + return false; +#endif +} + +static bool WriteTextFile(const std::string& path, const std::string& data, + std::string& errOut) +{ + FILE* f = fopen(path.c_str(), "wb"); + if (!f) + { + errOut = strprintf("could not write %s", path.c_str()); + return false; + } + bool ok = fwrite(data.data(), 1, data.size(), f) == data.size(); + if (fclose(f) != 0) + ok = false; + if (!ok) + { + errOut = strprintf("could not finish writing %s", path.c_str()); + return false; + } + return true; +} + +static bool ReadOneLine(const std::string& path, std::string& out) +{ + out.clear(); + FILE* f = fopen(path.c_str(), "rb"); + if (!f) + return false; + char buf[256] = {0}; + if (!fgets(buf, sizeof(buf), f)) + { + fclose(f); + return false; + } + fclose(f); + out = buf; + while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' || + out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t')) + out.erase(out.size() - 1); + return !out.empty(); +} + +static bool PickLocalPort(unsigned short& portOut, std::string& errOut) +{ + portOut = 0; + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == INVALID_SOCKET) + { + errOut = "could not create local port probe socket"; + return false; + } + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0) + { + closesocket(s); + errOut = "could not bind local port probe socket"; + return false; + } + socklen_t len = sizeof(addr); + if (getsockname(s, (struct sockaddr*)&addr, &len) != 0) + { + closesocket(s); + errOut = "could not read local port probe socket"; + return false; + } + portOut = ntohs(addr.sin_port); + closesocket(s); + if (portOut == 0) + { + errOut = "local port probe returned port zero"; + return false; + } + return true; +} + +static std::string ExecutableDir() +{ +#ifdef _WIN32 + char path[MAX_PATH + 1] = {0}; + DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH); + if (n == 0 || n >= MAX_PATH) + return "."; + std::string s = path; + size_t slash = s.find_last_of("\\/"); + if (slash == std::string::npos) + return "."; + return s.substr(0, slash); +#else + char path[4096] = {0}; + ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (n <= 0) + return "."; + path[n] = 0; + std::string s = path; + size_t slash = s.find_last_of('/'); + if (slash == std::string::npos) + return "."; + return s.substr(0, slash); +#endif +} + +static bool FileIsExecutableCandidate(const std::string& path) +{ +#ifdef _WIN32 + DWORD attr = GetFileAttributesA(path.c_str()); + return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY); +#else + return access(path.c_str(), X_OK) == 0; +#endif +} + +static std::string ResolveTorPath(const std::string& torPathOpt) +{ + if (!torPathOpt.empty()) + return torPathOpt; + + std::vector candidates; + std::string exeDir = ExecutableDir(); +#ifdef _WIN32 + candidates.push_back(PathJoin(exeDir, "tor/tor.exe")); + candidates.push_back(PathJoin(exeDir, "tor.exe")); + candidates.push_back("tor.exe"); +#else + candidates.push_back(PathJoin(exeDir, "tor/tor")); + candidates.push_back(PathJoin(exeDir, "tor")); + candidates.push_back("tor"); +#endif + for (size_t i = 0; i < candidates.size(); i++) + { + if (candidates[i].find('/') == std::string::npos && + candidates[i].find('\\') == std::string::npos) + return candidates[i]; // Let the OS search PATH. + if (FileIsExecutableCandidate(candidates[i])) + return candidates[i]; + } +#ifdef _WIN32 + return "tor.exe"; +#else + return "tor"; +#endif +} + +std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, + const std::string& hiddenServiceDir, + unsigned short socksPort, + unsigned short p2pPort) +{ + std::string torData = NormalizeTorrcPath(dataDir); + std::string hsDir = NormalizeTorrcPath(hiddenServiceDir); + std::string logFile = NormalizeTorrcPath(PathJoin(dataDir, "tor.log")); + + std::string s; + s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n"; + s += "DataDirectory " + QuoteTorrcPath(torData) + "\n"; + s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n", + (unsigned)socksPort); + s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n"; + s += "HiddenServiceVersion 3\n"; + s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n", + (unsigned)p2pPort, (unsigned)p2pPort); + s += "Log notice file " + QuoteTorrcPath(logFile) + "\n"; + return s; +} + +static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath, + std::string& errOut) +{ +#ifdef _WIN32 + memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess)); + STARTUPINFOA si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath); + std::vector mutableCmd(cmd.begin(), cmd.end()); + mutableCmd.push_back(0); + if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW, + NULL, NULL, &si, &g_managedTorProcess)) + { + errOut = strprintf("could not start Tor process %s (error %lu)", + torPath.c_str(), (unsigned long)GetLastError()); + return false; + } + return true; +#else + g_managedTorPid = fork(); + if (g_managedTorPid < 0) + { + errOut = strprintf("could not fork Tor process: %s", strerror(errno)); + return false; + } + if (g_managedTorPid == 0) + { + execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL); + _exit(127); + } + return true; +#endif +} + +static void ThreadManagedTorHostname(void*) +{ + std::string hostnamePath; + unsigned short p2pPort = 0; + CRITICAL_BLOCK(cs_managedTor) + { + hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname"); + p2pPort = g_managedTorP2PPort; + } + + for (int i = 0; i < 90 && !fShutdown; i++) + { + std::string onion; + if (ReadOneLine(hostnamePath, onion)) + { + std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort); + std::string err; + if (BtfSetLocalOnionEndpoint(endpoint, err)) + { + CRITICAL_BLOCK(cs_managedTor) + { + g_managedTorOnion = BtfLocalOnionEndpoint(); + g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s", + (unsigned)g_managedTorSocksPort, + g_managedTorOnion.c_str()); + } + printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str()); + return; + } + CRITICAL_BLOCK(cs_managedTor) + g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s", + err.c_str()); + return; + } + Sleep(1000); + } + + CRITICAL_BLOCK(cs_managedTor) + { + if (!g_fManagedTorStopping) + g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, waiting for hidden service hostname", + (unsigned)g_managedTorSocksPort); + } +} + +bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut) +{ + CRITICAL_BLOCK(cs_managedTor) + { + if (g_fManagedTor) + { + errOut = "managed Tor is already running"; + return false; + } + } + + std::string root = PathJoin(GetAppDir(), "managed-tor"); + std::string dataDir = PathJoin(root, "data"); + std::string hsDir = PathJoin(root, "onion-service"); + std::string torrcPath = PathJoin(root, "torrc"); + if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut)) + return false; + + unsigned short socksPort = 0; + if (!PickLocalPort(socksPort, errOut)) + return false; + + unsigned short p2pPort = ntohs(nListenPort); + std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort, p2pPort); + if (!WriteTextFile(torrcPath, torrc, errOut)) + return false; + + std::string torPath = ResolveTorPath(torPathOpt); + { + CRITICAL_BLOCK(cs_managedTor) + { + g_fManagedTor = true; + g_fManagedTorStopping = false; + g_managedTorPath = torPath; + g_managedTorDataDir = dataDir; + g_managedTorHiddenServiceDir = hsDir; + g_managedTorSocksPort = socksPort; + g_managedTorP2PPort = p2pPort; + g_managedTorOnion.clear(); + g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u", + torPath.c_str(), (unsigned)socksPort); + } + } + + if (!StartTorProcess(torPath, torrcPath, errOut)) + { + CRITICAL_BLOCK(cs_managedTor) + { + g_fManagedTor = false; + g_managedTorStatus = strprintf("failed: %s", errOut.c_str()); + } + return false; + } + + std::string proxyErr; + if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr)) + { + BtfStopManagedTor(); + errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str()); + return false; + } + + if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1) + { + BtfStopManagedTor(); + errOut = "could not start managed Tor hostname watcher"; + return false; + } + + return true; +} + +void BtfStopManagedTor() +{ + bool fWasRunning = false; + CRITICAL_BLOCK(cs_managedTor) + { + fWasRunning = g_fManagedTor; + if (!fWasRunning) + return; + g_fManagedTorStopping = true; + g_fManagedTor = false; + g_managedTorStatus = "stopping"; + } +#ifdef _WIN32 + if (g_managedTorProcess.hProcess) + { + TerminateProcess(g_managedTorProcess.hProcess, 0); + WaitForSingleObject(g_managedTorProcess.hProcess, 5000); + CloseHandle(g_managedTorProcess.hProcess); + CloseHandle(g_managedTorProcess.hThread); + memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess)); + } +#else + if (g_managedTorPid > 0) + { + kill(g_managedTorPid, SIGTERM); + for (int i = 0; i < 50; i++) + { + int status = 0; + pid_t r = waitpid(g_managedTorPid, &status, WNOHANG); + if (r == g_managedTorPid) + break; + Sleep(100); + } + g_managedTorPid = -1; + } +#endif + CRITICAL_BLOCK(cs_managedTor) + { + g_managedTorStatus = "stopped"; + g_managedTorOnion.clear(); + } +} + +bool BtfManagedTorEnabled() +{ + CRITICAL_BLOCK(cs_managedTor) + return g_fManagedTor; + return false; +} + +std::string BtfManagedTorStatus() +{ + CRITICAL_BLOCK(cs_managedTor) + { + if (!g_fManagedTor && g_managedTorStatus.empty()) + return "disabled"; + return g_managedTorStatus; + } + return "disabled"; +} diff --git a/src/tor.h b/src/tor.h new file mode 100644 index 0000000..482e773 --- /dev/null +++ b/src/tor.h @@ -0,0 +1,22 @@ +// Copyright (c) 2026 Bitflash developers +// Distributed under the MIT/X11 software license. +// +// Managed Tor process support. This is transport bootstrapping only: it does +// not touch consensus, wallet keys, mining, or the .btf identity. + +#ifndef BITFLASH_TOR_H +#define BITFLASH_TOR_H + +#include + +bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut); +void BtfStopManagedTor(); +bool BtfManagedTorEnabled(); +std::string BtfManagedTorStatus(); + +std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, + const std::string& hiddenServiceDir, + unsigned short socksPort, + unsigned short p2pPort); + +#endif -- cgit v1.2.3 From 1bf78c53404d83638177889590ce199c87af63bc Mon Sep 17 00:00:00 2001 From: Nsable-sh Date: Thu, 13 Aug 2026 14:09:58 -0300 Subject: net: stop managed Tor through control port --- src/selftest.cpp | 5 +- src/tor.cpp | 249 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- src/tor.h | 1 + 3 files changed, 248 insertions(+), 7 deletions(-) (limited to 'src/selftest.cpp') diff --git a/src/selftest.cpp b/src/selftest.cpp index 4cb872e..3c93927 100644 --- a/src/selftest.cpp +++ b/src/selftest.cpp @@ -1573,11 +1573,14 @@ static int RunManagedTorSelfTest() std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data", "C:/Bitflash Managed Tor/onion-service", - 19050, 8433); + 19050, 19051, 8433); nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos, "quotes the Tor data directory") ? 0 : 1; nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos, "binds SOCKS5 to localhost only") ? 0 : 1; + nFail += Check(torrc.find("ControlPort 127.0.0.1:19051") != std::string::npos && + torrc.find("CookieAuthentication 1") != std::string::npos, + "binds an authenticated Tor control port to localhost only") ? 0 : 1; nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos && torrc.find("IsolateClientAddr") != std::string::npos && torrc.find("IsolateDestAddr") != std::string::npos && diff --git a/src/tor.cpp b/src/tor.cpp index 180cd2b..e47ee44 100644 --- a/src/tor.cpp +++ b/src/tor.cpp @@ -17,6 +17,7 @@ static CCriticalSection cs_managedTor; static bool g_fManagedTor = false; static bool g_fManagedTorStopping = false; static unsigned short g_managedTorSocksPort = 0; +static unsigned short g_managedTorControlPort = 0; static unsigned short g_managedTorP2PPort = 0; static std::string g_managedTorPath; static std::string g_managedTorDataDir; @@ -108,8 +109,16 @@ static bool EnsureDir(const std::string& path, std::string& errOut) errOut = strprintf("%s exists and is not a directory", path.c_str()); return false; } - if (mkdir(path.c_str(), 0700) == 0 || errno == EEXIST) + if (mkdir(path.c_str(), 0700) == 0) return true; + if (errno == EEXIST) + { + if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) + { + chmod(path.c_str(), 0700); + return true; + } + } errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno)); return false; #endif @@ -154,6 +163,205 @@ static bool WriteTextFile(const std::string& path, const std::string& data, return true; } +static bool ReadBinaryFile(const std::string& path, std::vector& data, + std::string& errOut) +{ + data.clear(); + FILE* f = fopen(path.c_str(), "rb"); + if (!f) + { + errOut = strprintf("could not read %s", path.c_str()); + return false; + } + unsigned char buf[256]; + for (;;) + { + size_t n = fread(buf, 1, sizeof(buf), f); + if (n > 0) + data.insert(data.end(), buf, buf + n); + if (n < sizeof(buf)) + { + if (ferror(f)) + { + fclose(f); + errOut = strprintf("could not finish reading %s", path.c_str()); + return false; + } + break; + } + if (data.size() > 1024) + { + fclose(f); + errOut = strprintf("%s is unexpectedly large", path.c_str()); + return false; + } + } + fclose(f); + if (data.empty()) + { + errOut = strprintf("%s is empty", path.c_str()); + return false; + } + return true; +} + +static void SetSocketTimeouts(SOCKET s, int millis) +{ +#ifdef _WIN32 + DWORD timeout = millis; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout)); + setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout)); +#else + struct timeval tv; + tv.tv_sec = millis / 1000; + tv.tv_usec = (millis % 1000) * 1000; + setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); +#endif +} + +static bool SendAll(SOCKET s, const std::string& data) +{ + const char* p = data.data(); + int left = (int)data.size(); + while (left > 0) + { + int n = send(s, p, left, 0); + if (n <= 0) + return false; + p += n; + left -= n; + } + return true; +} + +static bool RecvControlLine(SOCKET s, std::string& line) +{ + line.clear(); + while (line.size() < 4096) + { + char ch = 0; + int n = recv(s, &ch, 1, 0); + if (n <= 0) + return false; + if (ch == '\n') + return true; + if (ch != '\r') + line += ch; + } + return false; +} + +static SOCKET ConnectLoopback(unsigned short port, std::string& errOut) +{ +#ifdef _WIN32 + WSADATA wsadata; + int ret = WSAStartup(MAKEWORD(2,2), &wsadata); + if (ret != NO_ERROR) + { + errOut = strprintf("could not start Winsock for Tor control shutdown (WSAStartup returned %d)", ret); + return INVALID_SOCKET; + } +#endif + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (s == INVALID_SOCKET) + { + errOut = "could not create Tor control socket"; +#ifdef _WIN32 + WSACleanup(); +#endif + return INVALID_SOCKET; + } + SetSocketTimeouts(s, 3000); + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = htons(port); + if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) != 0) + { + closesocket(s); + errOut = strprintf("could not connect to Tor control port 127.0.0.1:%u", (unsigned)port); +#ifdef _WIN32 + WSACleanup(); +#endif + return INVALID_SOCKET; + } + return s; +} + +static void CloseLoopback(SOCKET s) +{ + if (s != INVALID_SOCKET) + closesocket(s); +#ifdef _WIN32 + WSACleanup(); +#endif +} + +static bool ReadPositiveTorControlReply(SOCKET s, std::string& errOut) +{ + std::string line; + if (!RecvControlLine(s, line)) + { + errOut = "Tor control port closed before replying"; + return false; + } + if (line.size() >= 3 && line.substr(0, 3) == "250") + return true; + errOut = strprintf("Tor control command failed: %s", line.c_str()); + return false; +} + +static bool RequestManagedTorShutdown(std::string& errOut) +{ + std::string dataDir; + unsigned short controlPort = 0; + CRITICAL_BLOCK(cs_managedTor) + { + dataDir = g_managedTorDataDir; + controlPort = g_managedTorControlPort; + } + if (controlPort == 0 || dataDir.empty()) + { + errOut = "Tor control port is not available"; + return false; + } + + std::vector cookie; + if (!ReadBinaryFile(PathJoin(dataDir, "control_auth_cookie"), cookie, errOut)) + return false; + + SOCKET s = ConnectLoopback(controlPort, errOut); + if (s == INVALID_SOCKET) + return false; + + bool ok = false; + std::string auth = "AUTHENTICATE " + HexStr(cookie.begin(), cookie.end(), false) + "\r\n"; + if (!SendAll(s, auth) || !ReadPositiveTorControlReply(s, errOut)) + goto done; + + if (!SendAll(s, "SIGNAL SHUTDOWN\r\n")) + { + errOut = "could not send Tor shutdown signal"; + goto done; + } + + { + std::string reply; + if (RecvControlLine(s, reply) && !(reply.size() >= 3 && reply.substr(0, 3) == "250")) + { + errOut = strprintf("Tor rejected shutdown signal: %s", reply.c_str()); + goto done; + } + } + ok = true; + +done: + CloseLoopback(s); + return ok; +} + static bool ReadOneLine(const std::string& path, std::string& out) { out.clear(); @@ -295,6 +503,7 @@ static std::string ResolveTorPath(const std::string& torPathOpt) std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, const std::string& hiddenServiceDir, unsigned short socksPort, + unsigned short controlPort, unsigned short p2pPort) { std::string torData = NormalizeTorrcPath(dataDir); @@ -304,6 +513,8 @@ std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, s += "DataDirectory " + QuoteTorrcPath(torData) + "\n"; s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n", (unsigned)socksPort); + s += strprintf("ControlPort 127.0.0.1:%u\n", (unsigned)controlPort); + s += "CookieAuthentication 1\n"; s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n"; s += "HiddenServiceVersion 3\n"; s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n", @@ -503,9 +714,13 @@ bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut) unsigned short socksPort = 0; if (!PickLocalPort(socksPort, errOut)) return false; + unsigned short controlPort = 0; + if (!PickLocalPort(controlPort, errOut)) + return false; unsigned short p2pPort = ntohs(nListenPort); - std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort, p2pPort); + std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort, + controlPort, p2pPort); if (!WriteTextFile(torrcPath, torrc, errOut)) return false; @@ -519,6 +734,7 @@ bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut) g_managedTorDataDir = dataDir; g_managedTorHiddenServiceDir = hsDir; g_managedTorSocksPort = socksPort; + g_managedTorControlPort = controlPort; g_managedTorP2PPort = p2pPort; g_managedTorOnion.clear(); g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u", @@ -582,8 +798,15 @@ void BtfStopManagedTor() #ifdef _WIN32 if (g_managedTorProcess.hProcess) { - TerminateProcess(g_managedTorProcess.hProcess, 0); - WaitForSingleObject(g_managedTorProcess.hProcess, 5000); + std::string shutdownErr; + bool fGraceful = RequestManagedTorShutdown(shutdownErr); + DWORD waitResult = WaitForSingleObject(g_managedTorProcess.hProcess, + fGraceful ? 10000 : 1000); + if (waitResult != WAIT_OBJECT_0) + { + TerminateProcess(g_managedTorProcess.hProcess, 0); + WaitForSingleObject(g_managedTorProcess.hProcess, 5000); + } CloseHandle(g_managedTorProcess.hProcess); CloseHandle(g_managedTorProcess.hThread); memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess)); @@ -591,15 +814,26 @@ void BtfStopManagedTor() #else if (g_managedTorPid > 0) { - kill(g_managedTorPid, SIGTERM); - for (int i = 0; i < 50; i++) + std::string shutdownErr; + if (!RequestManagedTorShutdown(shutdownErr)) + kill(g_managedTorPid, SIGTERM); + bool fExited = false; + for (int i = 0; i < 100; i++) { int status = 0; pid_t r = waitpid(g_managedTorPid, &status, WNOHANG); if (r == g_managedTorPid) + { + fExited = true; break; + } Sleep(100); } + if (!fExited) + { + kill(g_managedTorPid, SIGKILL); + waitpid(g_managedTorPid, NULL, 0); + } g_managedTorPid = -1; } #endif @@ -607,6 +841,9 @@ void BtfStopManagedTor() { g_managedTorStatus = "stopped"; g_managedTorOnion.clear(); + g_managedTorSocksPort = 0; + g_managedTorControlPort = 0; + g_managedTorP2PPort = 0; } } diff --git a/src/tor.h b/src/tor.h index 482e773..7c5c603 100644 --- a/src/tor.h +++ b/src/tor.h @@ -17,6 +17,7 @@ std::string BtfManagedTorStatus(); std::string BtfBuildManagedTorrcForTest(const std::string& dataDir, const std::string& hiddenServiceDir, unsigned short socksPort, + unsigned short controlPort, unsigned short p2pPort); #endif -- cgit v1.2.3