upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-13 11:54:35 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-13 11:54:35 -0300
commit53cc17d621f55b8904812fae5df90ef089bf5aaf (patch)
tree794d251d6bdf51f0d488469ff050ece1846cdbea
parentc009e7cc0c5093f4c4add741cb9d131d19956871 (diff)
net: add managed Tor hidden service bootstrap
-rw-r--r--README.md13
-rw-r--r--docs/rendezvous.md8
-rw-r--r--docs/tor.md52
-rw-r--r--src/Makefile5
-rw-r--r--src/main_gui.cpp65
-rw-r--r--src/makefile.mingw5
-rw-r--r--src/net.cpp2
-rw-r--r--src/readme.txt3
-rw-r--r--src/selftest.cpp37
-rw-r--r--src/tor.cpp492
-rw-r--r--src/tor.h22
11 files changed, 665 insertions, 39 deletions
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:
339-port=N # P2P listen port, default 8433 339-port=N # P2P listen port, default 8433
340-socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials 340-socks=HOST:PORT # SOCKS5 for outbound Nostr, .btf relay, and onion peer dials
341-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-managedtor[=PATH] # start Tor, create a hidden service, advertise its onion
342-onionservice=HOST.onion:PORT # advertise this node's Tor hidden service 343-onionservice=HOST.onion:PORT # advertise this node's Tor hidden service
343-debug # verbose log; without it debug.log is nearly silent 344-debug # verbose log; without it debug.log is nearly silent
344-help # full list 345-help # full list
@@ -357,10 +358,14 @@ proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`.
357`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at 358`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at
358`127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials, 359`127.0.0.1:9050`. It routes outbound Nostr discovery, `.btf` rendezvous dials,
359and signed direct `.onion` peer dials through Tor, and keeps external-IP probes 360and signed direct `.onion` peer dials through Tor, and keeps external-IP probes
360disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else. To make your own 361disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else.
361node reachable without a rendezvous relay, expose port `8433` as a Tor hidden 362
362service and pass `-onionservice=HOST.onion:8433`; peers with `-tor` will try it 363`-managedtor[=PATH]` starts a Tor process for this node, writes a local `torrc`,
363before falling back to rendezvous. 364creates a v3 hidden service for port `8433`, routes outbound discovery through
365that Tor instance, and signs the generated `.onion:8433` endpoint into the
366node's `.btf` descriptor. Without `PATH`, Bitflash looks for `tor/tor.exe`,
367`tor.exe`, or `tor` beside the binary and then on `PATH`. The hidden-service key
368is stored under the Bitflash data directory in `managed-tor/onion-service/`.
364 369
365### When something looks wrong 370### When something looks wrong
366 371
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.
308 unreachable, discovery stalls until one is reachable again. Relay TLS is 308 unreachable, discovery stalls until one is reachable again. Relay TLS is
309 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
310 still rests on the relay set. 310 still rests on the relay set.
311- **Direct onion is not automatic Tor configuration.** Bitflash signs and 311- **Direct onion needs Tor.** With `-managedtor`, Bitflash starts a Tor process,
312 advertises a hidden-service endpoint, and Tor-mode peers can dial it. The Tor 312 writes a local hidden-service config, and advertises the generated onion
313 daemon still owns hidden-service creation, private keys, rotation and uptime. 313 endpoint. With `-onionservice`, an operator can still run Tor manually and
314 provide the endpoint. In both cases Tor owns the hidden-service private key and
315 uptime.
314 316
315## 11. Wire reference 317## 11. Wire reference
316 318
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
36warning. That keeps a command line with `-tor` from silently becoming a custom 36warning. That keeps a command line with `-tor` from silently becoming a custom
37proxy route. 37proxy route.
38 38
39## Managed Tor mode
40
41`-managedtor` starts a Tor process for this node, writes a local `torrc`, creates
42a v3 hidden service for the normal Bitflash P2P listener, routes outbound
43discovery through that Tor instance, and signs the generated `.onion:8433`
44endpoint into the node's `.btf` descriptor.
45
46```bash
47bitflash -managedtor
48```
49
50Without a value, Bitflash looks for Tor in this order:
51
521. `tor/tor.exe` or `tor/tor` beside the Bitflash binary;
532. `tor.exe` or `tor` beside the Bitflash binary;
543. `tor.exe` or `tor` on `PATH`.
55
56To point at a specific Tor executable:
57
58```bash
59bitflash -managedtor=/opt/tor/bin/tor
60bitflash -managedtor=C:\Tor Browser\Browser\TorBrowser\Tor\tor.exe
61```
62
63Managed Tor writes its state under the Bitflash data directory:
64
65```text
66managed-tor/
67 torrc
68 data/
69 onion-service/
70```
71
72The private key for the onion address lives in `managed-tor/onion-service/`.
73Back up that directory if you want the same onion address after moving the node.
74Delete it only if you intentionally want a fresh onion endpoint.
75
76`-managedtor` takes precedence over `-tor` and `-socks`, because it must choose
77the local SOCKS port it starts. Diagnostics report the current managed Tor
78state, including the SOCKS port and onion endpoint once Tor has generated it.
79
39When Tor mode is enabled: 80When Tor mode is enabled:
40 81
41- outbound Nostr relay dials use Tor; 82- outbound Nostr relay dials use Tor;
@@ -100,10 +141,10 @@ bitflash -operator -tor \
100 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 141 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434
101``` 142```
102 143
103## Direct full-node hidden service 144## Manual direct full-node hidden service
104 145
105The legacy P2P listener still binds to port `8433`. You can expose it through 146Managed Tor is the preferred path for ordinary nodes. Operators can still expose
106Tor manually: 147the P2P listener through a separately managed Tor service:
107 148
108```text 149```text
109HiddenServiceDir /var/lib/tor/bitflash-node/ 150HiddenServiceDir /var/lib/tor/bitflash-node/
@@ -139,6 +180,5 @@ This means rendezvous relays stop being the only way to reach a node that has
139published a hidden service. They remain useful bootstrap/fallback infrastructure, 180published a hidden service. They remain useful bootstrap/fallback infrastructure,
140but an already-discovered onion-capable peer can be reached directly over Tor. 181but an already-discovered onion-capable peer can be reached directly over Tor.
141 182
142The node does not create or rotate Tor hidden services itself. Tor owns the 183Tor owns the private key under `HiddenServiceDir`; back it up if you want the
143private key under `HiddenServiceDir`; back it up if you want the onion name to 184onion name to stay stable.
144stay 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 \
53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 53HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
54 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 54 key.h bignum.h script.h db.h base58.h net.h nostr.h \
55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ 55 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \
56 selftest.h walletcmd.h compat.h sockcount.h proxy.h font_roboto.h 56 selftest.h walletcmd.h compat.h sockcount.h proxy.h tor.h font_roboto.h
57 57
58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \ 58IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
59 obj/imgui_widgets.o obj/imgui_impl_glfw.o obj/imgui_impl_opengl3.o 59 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 \
61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 61NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
62 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ 62 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \
63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 63 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o
65 65
66GUI_OBJS = obj/gui.o obj/main_gui.o 66GUI_OBJS = obj/gui.o obj/main_gui.o
67 67
@@ -194,5 +194,6 @@ tests: test_bip32 bitflash-node
194 ./bitflash-node -selftest=pool-stratum 194 ./bitflash-node -selftest=pool-stratum
195 ./bitflash-node -selftest=parse-money 195 ./bitflash-node -selftest=parse-money
196 ./bitflash-node -selftest=socks5-proxy 196 ./bitflash-node -selftest=socks5-proxy
197 ./bitflash-node -selftest=managed-tor
197 198
198.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke 199.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 @@
4#include "nostr.h" 4#include "nostr.h"
5#include "proxy.h" 5#include "proxy.h"
6#include "selftest.h" 6#include "selftest.h"
7#include "tor.h"
7#include "walletcmd.h" 8#include "walletcmd.h"
8#include <thread> // hardware_concurrency, to sanity-check /genproclimit 9#include <thread> // hardware_concurrency, to sanity-check /genproclimit
9#ifndef _WIN32 10#ifndef _WIN32
@@ -113,7 +114,7 @@ static void PrintUsage()
113 printf(" /gen\n"); 114 printf(" /gen\n");
114 printf(" /nogui or /daemon\n"); 115 printf(" /nogui or /daemon\n");
115 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n"); 116 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n");
116 printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n"); 117 printf(" net-message, consensus-limits, pool-stratum, parse-money, socks5-proxy, or managed-tor\n");
117 printf("\n"); 118 printf("\n");
118 printf("Mining mode:\n"); 119 printf("Mining mode:\n");
119 printf(" /operator\n"); 120 printf(" /operator\n");
@@ -141,6 +142,7 @@ static void PrintUsage()
141 printf(" /port=N (P2P listen port, default 8433)\n"); 142 printf(" /port=N (P2P listen port, default 8433)\n");
142 printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n"); 143 printf(" /socks=HOST:PORT (SOCKS5 proxy for Nostr, .btf relay, and onion dials)\n");
143 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n"); 144 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
145 printf(" /managedtor[=PATH] (start Tor, create a hidden service, advertise its onion)\n");
144 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 146 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
145 printf("\n"); 147 printf("\n");
146 printf("Wallet:\n"); 148 printf("Wallet:\n");
@@ -284,6 +286,24 @@ static void ParseStartupArguments(int argc, char* argv[])
284 vBtfMeetingRelays.push_back(rvRelay); 286 vBtfMeetingRelays.push_back(rvRelay);
285 } 287 }
286 288
289 // net.cpp has described nListenPort as "tunable via /port" since it was
290 // written, but nothing ever read the option, so the port was fixed at 8433
291 // and a second node could not start on a machine already running one. Parse
292 // it before Tor setup so a managed hidden service maps the real listener.
293 string strPort = argval2(argc, argv, "/port", "-port");
294 if (!strPort.empty())
295 {
296 int nPort = atoi(strPort.c_str());
297 if (nPort <= 0 || nPort > 65535)
298 fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str());
299 else
300 {
301 nListenPort = htons((unsigned short)nPort);
302 // Or we would announce a port we never bound.
303 addrLocalHost.port = nListenPort;
304 }
305 }
306
287 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay"); 307 string announceRelay = argval2(argc, argv, "/announcerelay", "-announcerelay");
288 if (!announceRelay.empty()) 308 if (!announceRelay.empty())
289 strBtfAnnounceRelay = announceRelay; 309 strBtfAnnounceRelay = announceRelay;
@@ -299,10 +319,23 @@ static void ParseStartupArguments(int argc, char* argv[])
299 BtfLocalOnionEndpoint().c_str()); 319 BtfLocalOnionEndpoint().c_str());
300 } 320 }
301 321
322 bool fManagedTor = arg(argc, argv, "/managedtor") || arg(argc, argv, "-managedtor");
302 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor"); 323 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
303 if (fTorMode) 324 string socksProxy = argval2(argc, argv, "/socks", "-socks");
325 if (fManagedTor)
326 {
327 if (fTorMode || !socksProxy.empty())
328 fprintf(stderr, "Warning: /managedtor takes precedence over /tor and /socks\n");
329
330 string torPath = argval2(argc, argv, "/managedtor", "-managedtor");
331 string err;
332 if (!BtfStartManagedTor(torPath, err))
333 fprintf(stderr, "Ignoring /managedtor=%s: %s\n", torPath.c_str(), err.c_str());
334 else
335 fprintf(stderr, "Managed Tor enabled: %s\n", BtfManagedTorStatus().c_str());
336 }
337 else if (fTorMode)
304 { 338 {
305 string socksProxy = argval2(argc, argv, "/socks", "-socks");
306 if (!socksProxy.empty()) 339 if (!socksProxy.empty())
307 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n", 340 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n",
308 socksProxy.c_str()); 341 socksProxy.c_str());
@@ -317,7 +350,6 @@ static void ParseStartupArguments(int argc, char* argv[])
317 } 350 }
318 else 351 else
319 { 352 {
320 string socksProxy = argval2(argc, argv, "/socks", "-socks");
321 if (!socksProxy.empty()) 353 if (!socksProxy.empty())
322 { 354 {
323 string err; 355 string err;
@@ -348,22 +380,6 @@ static void ParseStartupArguments(int argc, char* argv[])
348 vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1))); 380 vBtfExtraSeeds.push_back(make_pair(val.substr(0, colon), val.substr(colon + 1)));
349 } 381 }
350 382
351 // net.cpp has described nListenPort as "tunable via /port" since it was
352 // written, but nothing ever read the option, so the port was fixed at 8433
353 // and a second node could not start on a machine already running one.
354 string strPort = argval2(argc, argv, "/port", "-port");
355 if (!strPort.empty())
356 {
357 int nPort = atoi(strPort.c_str());
358 if (nPort <= 0 || nPort > 65535)
359 fprintf(stderr, "Ignoring /port=%s: not a port number\n", strPort.c_str());
360 else
361 {
362 nListenPort = htons((unsigned short)nPort);
363 // Or we would announce a port we never bound.
364 addrLocalHost.port = nListenPort;
365 }
366 }
367} 383}
368 384
369// Report a startup failure somewhere the user will actually see it. 385// Report a startup failure somewhere the user will actually see it.
@@ -715,7 +731,12 @@ int main(int argc, char* argv[])
715 731
716 ReacceptWalletTransactions(); 732 ReacceptWalletTransactions();
717 733
718 if (!StartNode(strErrors)) { fprintf(stderr,"StartNode: %s\n",strErrors.c_str()); return 1; } 734 if (!StartNode(strErrors))
735 {
736 fprintf(stderr,"StartNode: %s\n",strErrors.c_str());
737 BtfStopManagedTor();
738 return 1;
739 }
719 740
720 if (nMineMode == MINE_OPERATOR) { 741 if (nMineMode == MINE_OPERATOR) {
721 gPoolServerRunning = true; 742 gPoolServerRunning = true;
@@ -748,6 +769,7 @@ int main(int argc, char* argv[])
748 printf("Running headless. Ctrl-C to stop.\n"); 769 printf("Running headless. Ctrl-C to stop.\n");
749 while (!fShutdown) Sleep(500); 770 while (!fShutdown) Sleep(500);
750 StopNode(); 771 StopNode();
772 BtfStopManagedTor();
751 // Checkpoints, releases wallet.dat from this directory's Berkeley DB 773 // Checkpoints, releases wallet.dat from this directory's Berkeley DB
752 // environment, and closes it. Declared since 2009 and never once 774 // environment, and closes it. Declared since 2009 and never once
753 // called here, which is why a wallet.dat copied elsewhere would not 775 // called here, which is why a wallet.dat copied elsewhere would not
@@ -767,6 +789,7 @@ int main(int argc, char* argv[])
767 int ret = RunGUI(argc, argv); 789 int ret = RunGUI(argc, argv);
768 fShutdown = true; 790 fShutdown = true;
769 StopNode(); 791 StopNode();
792 BtfStopManagedTor();
770 DBFlush(true); 793 DBFlush(true);
771 return ret; 794 return ret;
772#endif 795#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 \
37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \ 37NODE_OBJS = obj/util.o obj/script.o obj/db.o obj/net.o obj/main.o \
38 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \ 38 obj/market.o obj/sha.o obj/nostr.o obj/randomx_pow.o \
39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \ 39 obj/btfaddr.o obj/btfchan.o obj/btfrv.o obj/btftunnel.o obj/rpc.o \
40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o 40 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o obj/tor.o
41 41
42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
43 43
@@ -51,7 +51,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \ 51HEADERS = headers_core.h headers.h util.h main.h serialize.h uint256.h \
52 key.h bignum.h script.h db.h base58.h net.h nostr.h \ 52 key.h bignum.h script.h db.h base58.h net.h nostr.h \
53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \ 53 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \
54 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h 54 selftest.h walletcmd.h sockcount.h proxy.h tor.h compat.h font_roboto.h
55 55
56all: bitflash.exe 56all: bitflash.exe
57 57
@@ -113,3 +113,4 @@ tests: test_bip32.exe bitflash.exe
113 ./bitflash.exe -selftest=consensus-limits 113 ./bitflash.exe -selftest=consensus-limits
114 ./bitflash.exe -selftest=pool-stratum 114 ./bitflash.exe -selftest=pool-stratum
115 ./bitflash.exe -selftest=socks5-proxy 115 ./bitflash.exe -selftest=socks5-proxy
116 ./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 @@
22#include "btfaddr.h" 22#include "btfaddr.h"
23#include "btftunnel.h" 23#include "btftunnel.h"
24#include "proxy.h" 24#include "proxy.h"
25#include "tor.h"
25 26
26void ThreadReconnectCachedBtfPeers(void* parg); 27void ThreadReconnectCachedBtfPeers(void* parg);
27void ThreadMessageHandler2(void* parg); 28void ThreadMessageHandler2(void* parg);
@@ -296,6 +297,7 @@ string GetDiagnosticsText()
296 str += strprintf(" peers watched %d of %d held by poll%s\n", 297 str += strprintf(" peers watched %d of %d held by poll%s\n",
297 nPeersWatched, nHeld, 298 nPeersWatched, nHeld,
298 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : ""); 299 nHeld > nPeersWatched ? " <-- NOT ALL PEERS ARE BEING READ" : "");
300 str += strprintf(" managed Tor %s\n", BtfManagedTorStatus().c_str());
299 301
300 if (nBlocksReceived > 0) 302 if (nBlocksReceived > 0)
301 str += strprintf(" blocks received %lld (%lld arrived without a parent, %.1f%%)\n", 303 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)
49 /tor[=HOST:PORT] 49 /tor[=HOST:PORT]
50 Tor mode for outbound Nostr, .btf rendezvous, and direct onion peer 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 /managedtor[=PATH]
53 start Tor, create a v3 hidden service for this node, route outbound
54 discovery through it, and advertise the generated onion endpoint
52 /solomine mine without requiring a connected peer (local testing) 55 /solomine mine without requiring a connected peer (local testing)
53 /operator operator mode (run pool server) 56 /operator operator mode (run pool server)
54 /participant=ADDR 57 /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 @@
11#include "proxy.h" 11#include "proxy.h"
12#include "selftest.h" 12#include "selftest.h"
13#include "sockcount.h" 13#include "sockcount.h"
14#include "tor.h"
14#include "walletcmd.h" 15#include "walletcmd.h"
15 16
16extern int RunPoolStratumSelfTest(); 17extern int RunPoolStratumSelfTest();
@@ -1565,6 +1566,38 @@ static int RunSocks5ProxySelfTest()
1565 return nFail == 0 ? 0 : 1; 1566 return nFail == 0 ? 0 : 1;
1566} 1567}
1567 1568
1569static int RunManagedTorSelfTest()
1570{
1571 printf("managed-tor self-test\n");
1572 int nFail = 0;
1573
1574 std::string torrc = BtfBuildManagedTorrcForTest("C:/Bitflash Managed Tor/data",
1575 "C:/Bitflash Managed Tor/onion-service",
1576 19050, 8433);
1577 nFail += Check(torrc.find("DataDirectory \"C:/Bitflash Managed Tor/data\"") != std::string::npos,
1578 "quotes the Tor data directory") ? 0 : 1;
1579 nFail += Check(torrc.find("SocksPort 127.0.0.1:19050") != std::string::npos,
1580 "binds SOCKS5 to localhost only") ? 0 : 1;
1581 nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos &&
1582 torrc.find("IsolateClientAddr") != std::string::npos &&
1583 torrc.find("IsolateDestAddr") != std::string::npos &&
1584 torrc.find("IsolateDestPort") != std::string::npos,
1585 "enables strict Tor stream isolation") ? 0 : 1;
1586 nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos,
1587 "writes a hidden service directory") ? 0 : 1;
1588 nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos,
1589 "requests a v3 onion service") ? 0 : 1;
1590 nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos,
1591 "maps the onion service to the Bitflash P2P listener") ? 0 : 1;
1592 nFail += Check(BtfManagedTorStatus() == "disabled",
1593 "managed Tor starts disabled") ? 0 : 1;
1594
1595 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1596 nFail, nFail == 1 ? "" : "s");
1597 fflush(stdout);
1598 return nFail == 0 ? 0 : 1;
1599}
1600
1568int RunSelfTest(const std::string& name) 1601int RunSelfTest(const std::string& name)
1569{ 1602{
1570 AttachTerminal(); 1603 AttachTerminal();
@@ -1589,8 +1622,10 @@ int RunSelfTest(const std::string& name)
1589 return RunParseMoneySelfTest(); 1622 return RunParseMoneySelfTest();
1590 if (name == "socks5-proxy") 1623 if (name == "socks5-proxy")
1591 return RunSocks5ProxySelfTest(); 1624 return RunSocks5ProxySelfTest();
1625 if (name == "managed-tor")
1626 return RunManagedTorSelfTest();
1592 1627
1593 printf("Unknown self-test '%s'\n", name.c_str()); 1628 printf("Unknown self-test '%s'\n", name.c_str());
1594 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"); 1629 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");
1595 return 1; 1630 return 1;
1596} 1631}
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 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers_core.h"
5#include "tor.h"
6#include "nostr.h"
7#include "proxy.h"
8
9#include <errno.h>
10
11#ifndef _WIN32
12#include <signal.h>
13#include <sys/wait.h>
14#endif
15
16static CCriticalSection cs_managedTor;
17static bool g_fManagedTor = false;
18static bool g_fManagedTorStopping = false;
19static unsigned short g_managedTorSocksPort = 0;
20static unsigned short g_managedTorP2PPort = 0;
21static std::string g_managedTorPath;
22static std::string g_managedTorDataDir;
23static std::string g_managedTorHiddenServiceDir;
24static std::string g_managedTorOnion;
25static std::string g_managedTorStatus;
26#ifdef _WIN32
27static PROCESS_INFORMATION g_managedTorProcess;
28#else
29static pid_t g_managedTorPid = -1;
30#endif
31
32static std::string PathJoin(const std::string& a, const std::string& b)
33{
34 if (a.empty())
35 return b;
36 char last = a[a.size() - 1];
37 if (last == '/' || last == '\\')
38 return a + b;
39 return a + "/" + b;
40}
41
42static std::string NormalizeTorrcPath(std::string path)
43{
44 for (size_t i = 0; i < path.size(); i++)
45 if (path[i] == '\\')
46 path[i] = '/';
47 return path;
48}
49
50static std::string QuoteTorrcPath(const std::string& path)
51{
52 std::string out = "\"";
53 for (size_t i = 0; i < path.size(); i++)
54 {
55 if (path[i] == '"' || path[i] == '\\')
56 out += '\\';
57 out += path[i];
58 }
59 out += "\"";
60 return out;
61}
62
63static std::string QuoteCommandArg(const std::string& arg)
64{
65 std::string out = "\"";
66 for (size_t i = 0; i < arg.size(); i++)
67 {
68 if (arg[i] == '"')
69 out += '\\';
70 out += arg[i];
71 }
72 out += "\"";
73 return out;
74}
75
76static bool EnsureDir(const std::string& path, std::string& errOut)
77{
78 if (path.empty())
79 {
80 errOut = "directory path is empty";
81 return false;
82 }
83#ifdef _WIN32
84 DWORD attr = GetFileAttributesA(path.c_str());
85 if (attr != INVALID_FILE_ATTRIBUTES)
86 {
87 if (attr & FILE_ATTRIBUTE_DIRECTORY)
88 return true;
89 errOut = strprintf("%s exists and is not a directory", path.c_str());
90 return false;
91 }
92 if (CreateDirectoryA(path.c_str(), NULL))
93 return true;
94 DWORD err = GetLastError();
95 if (err == ERROR_ALREADY_EXISTS)
96 return true;
97 errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err);
98 return false;
99#else
100 struct stat st;
101 if (stat(path.c_str(), &st) == 0)
102 {
103 if (S_ISDIR(st.st_mode))
104 return true;
105 errOut = strprintf("%s exists and is not a directory", path.c_str());
106 return false;
107 }
108 if (mkdir(path.c_str(), 0700) == 0 || errno == EEXIST)
109 return true;
110 errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno));
111 return false;
112#endif
113}
114
115static bool WriteTextFile(const std::string& path, const std::string& data,
116 std::string& errOut)
117{
118 FILE* f = fopen(path.c_str(), "wb");
119 if (!f)
120 {
121 errOut = strprintf("could not write %s", path.c_str());
122 return false;
123 }
124 bool ok = fwrite(data.data(), 1, data.size(), f) == data.size();
125 if (fclose(f) != 0)
126 ok = false;
127 if (!ok)
128 {
129 errOut = strprintf("could not finish writing %s", path.c_str());
130 return false;
131 }
132 return true;
133}
134
135static bool ReadOneLine(const std::string& path, std::string& out)
136{
137 out.clear();
138 FILE* f = fopen(path.c_str(), "rb");
139 if (!f)
140 return false;
141 char buf[256] = {0};
142 if (!fgets(buf, sizeof(buf), f))
143 {
144 fclose(f);
145 return false;
146 }
147 fclose(f);
148 out = buf;
149 while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' ||
150 out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t'))
151 out.erase(out.size() - 1);
152 return !out.empty();
153}
154
155static bool PickLocalPort(unsigned short& portOut, std::string& errOut)
156{
157 portOut = 0;
158 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
159 if (s == INVALID_SOCKET)
160 {
161 errOut = "could not create local port probe socket";
162 return false;
163 }
164 struct sockaddr_in addr;
165 memset(&addr, 0, sizeof(addr));
166 addr.sin_family = AF_INET;
167 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
168 addr.sin_port = 0;
169 if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
170 {
171 closesocket(s);
172 errOut = "could not bind local port probe socket";
173 return false;
174 }
175 socklen_t len = sizeof(addr);
176 if (getsockname(s, (struct sockaddr*)&addr, &len) != 0)
177 {
178 closesocket(s);
179 errOut = "could not read local port probe socket";
180 return false;
181 }
182 portOut = ntohs(addr.sin_port);
183 closesocket(s);
184 if (portOut == 0)
185 {
186 errOut = "local port probe returned port zero";
187 return false;
188 }
189 return true;
190}
191
192static std::string ExecutableDir()
193{
194#ifdef _WIN32
195 char path[MAX_PATH + 1] = {0};
196 DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
197 if (n == 0 || n >= MAX_PATH)
198 return ".";
199 std::string s = path;
200 size_t slash = s.find_last_of("\\/");
201 if (slash == std::string::npos)
202 return ".";
203 return s.substr(0, slash);
204#else
205 char path[4096] = {0};
206 ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
207 if (n <= 0)
208 return ".";
209 path[n] = 0;
210 std::string s = path;
211 size_t slash = s.find_last_of('/');
212 if (slash == std::string::npos)
213 return ".";
214 return s.substr(0, slash);
215#endif
216}
217
218static bool FileIsExecutableCandidate(const std::string& path)
219{
220#ifdef _WIN32
221 DWORD attr = GetFileAttributesA(path.c_str());
222 return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
223#else
224 return access(path.c_str(), X_OK) == 0;
225#endif
226}
227
228static std::string ResolveTorPath(const std::string& torPathOpt)
229{
230 if (!torPathOpt.empty())
231 return torPathOpt;
232
233 std::vector<std::string> candidates;
234 std::string exeDir = ExecutableDir();
235#ifdef _WIN32
236 candidates.push_back(PathJoin(exeDir, "tor/tor.exe"));
237 candidates.push_back(PathJoin(exeDir, "tor.exe"));
238 candidates.push_back("tor.exe");
239#else
240 candidates.push_back(PathJoin(exeDir, "tor/tor"));
241 candidates.push_back(PathJoin(exeDir, "tor"));
242 candidates.push_back("tor");
243#endif
244 for (size_t i = 0; i < candidates.size(); i++)
245 {
246 if (candidates[i].find('/') == std::string::npos &&
247 candidates[i].find('\\') == std::string::npos)
248 return candidates[i]; // Let the OS search PATH.
249 if (FileIsExecutableCandidate(candidates[i]))
250 return candidates[i];
251 }
252#ifdef _WIN32
253 return "tor.exe";
254#else
255 return "tor";
256#endif
257}
258
259std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
260 const std::string& hiddenServiceDir,
261 unsigned short socksPort,
262 unsigned short p2pPort)
263{
264 std::string torData = NormalizeTorrcPath(dataDir);
265 std::string hsDir = NormalizeTorrcPath(hiddenServiceDir);
266 std::string logFile = NormalizeTorrcPath(PathJoin(dataDir, "tor.log"));
267
268 std::string s;
269 s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n";
270 s += "DataDirectory " + QuoteTorrcPath(torData) + "\n";
271 s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n",
272 (unsigned)socksPort);
273 s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n";
274 s += "HiddenServiceVersion 3\n";
275 s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n",
276 (unsigned)p2pPort, (unsigned)p2pPort);
277 s += "Log notice file " + QuoteTorrcPath(logFile) + "\n";
278 return s;
279}
280
281static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath,
282 std::string& errOut)
283{
284#ifdef _WIN32
285 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
286 STARTUPINFOA si;
287 memset(&si, 0, sizeof(si));
288 si.cb = sizeof(si);
289 std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath);
290 std::vector<char> mutableCmd(cmd.begin(), cmd.end());
291 mutableCmd.push_back(0);
292 if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW,
293 NULL, NULL, &si, &g_managedTorProcess))
294 {
295 errOut = strprintf("could not start Tor process %s (error %lu)",
296 torPath.c_str(), (unsigned long)GetLastError());
297 return false;
298 }
299 return true;
300#else
301 g_managedTorPid = fork();
302 if (g_managedTorPid < 0)
303 {
304 errOut = strprintf("could not fork Tor process: %s", strerror(errno));
305 return false;
306 }
307 if (g_managedTorPid == 0)
308 {
309 execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL);
310 _exit(127);
311 }
312 return true;
313#endif
314}
315
316static void ThreadManagedTorHostname(void*)
317{
318 std::string hostnamePath;
319 unsigned short p2pPort = 0;
320 CRITICAL_BLOCK(cs_managedTor)
321 {
322 hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname");
323 p2pPort = g_managedTorP2PPort;
324 }
325
326 for (int i = 0; i < 90 && !fShutdown; i++)
327 {
328 std::string onion;
329 if (ReadOneLine(hostnamePath, onion))
330 {
331 std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort);
332 std::string err;
333 if (BtfSetLocalOnionEndpoint(endpoint, err))
334 {
335 CRITICAL_BLOCK(cs_managedTor)
336 {
337 g_managedTorOnion = BtfLocalOnionEndpoint();
338 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s",
339 (unsigned)g_managedTorSocksPort,
340 g_managedTorOnion.c_str());
341 }
342 printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str());
343 return;
344 }
345 CRITICAL_BLOCK(cs_managedTor)
346 g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s",
347 err.c_str());
348 return;
349 }
350 Sleep(1000);
351 }
352
353 CRITICAL_BLOCK(cs_managedTor)
354 {
355 if (!g_fManagedTorStopping)
356 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, waiting for hidden service hostname",
357 (unsigned)g_managedTorSocksPort);
358 }
359}
360
361bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut)
362{
363 CRITICAL_BLOCK(cs_managedTor)
364 {
365 if (g_fManagedTor)
366 {
367 errOut = "managed Tor is already running";
368 return false;
369 }
370 }
371
372 std::string root = PathJoin(GetAppDir(), "managed-tor");
373 std::string dataDir = PathJoin(root, "data");
374 std::string hsDir = PathJoin(root, "onion-service");
375 std::string torrcPath = PathJoin(root, "torrc");
376 if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut))
377 return false;
378
379 unsigned short socksPort = 0;
380 if (!PickLocalPort(socksPort, errOut))
381 return false;
382
383 unsigned short p2pPort = ntohs(nListenPort);
384 std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort, p2pPort);
385 if (!WriteTextFile(torrcPath, torrc, errOut))
386 return false;
387
388 std::string torPath = ResolveTorPath(torPathOpt);
389 {
390 CRITICAL_BLOCK(cs_managedTor)
391 {
392 g_fManagedTor = true;
393 g_fManagedTorStopping = false;
394 g_managedTorPath = torPath;
395 g_managedTorDataDir = dataDir;
396 g_managedTorHiddenServiceDir = hsDir;
397 g_managedTorSocksPort = socksPort;
398 g_managedTorP2PPort = p2pPort;
399 g_managedTorOnion.clear();
400 g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u",
401 torPath.c_str(), (unsigned)socksPort);
402 }
403 }
404
405 if (!StartTorProcess(torPath, torrcPath, errOut))
406 {
407 CRITICAL_BLOCK(cs_managedTor)
408 {
409 g_fManagedTor = false;
410 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
411 }
412 return false;
413 }
414
415 std::string proxyErr;
416 if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr))
417 {
418 BtfStopManagedTor();
419 errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str());
420 return false;
421 }
422
423 if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1)
424 {
425 BtfStopManagedTor();
426 errOut = "could not start managed Tor hostname watcher";
427 return false;
428 }
429
430 return true;
431}
432
433void BtfStopManagedTor()
434{
435 bool fWasRunning = false;
436 CRITICAL_BLOCK(cs_managedTor)
437 {
438 fWasRunning = g_fManagedTor;
439 if (!fWasRunning)
440 return;
441 g_fManagedTorStopping = true;
442 g_fManagedTor = false;
443 g_managedTorStatus = "stopping";
444 }
445#ifdef _WIN32
446 if (g_managedTorProcess.hProcess)
447 {
448 TerminateProcess(g_managedTorProcess.hProcess, 0);
449 WaitForSingleObject(g_managedTorProcess.hProcess, 5000);
450 CloseHandle(g_managedTorProcess.hProcess);
451 CloseHandle(g_managedTorProcess.hThread);
452 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
453 }
454#else
455 if (g_managedTorPid > 0)
456 {
457 kill(g_managedTorPid, SIGTERM);
458 for (int i = 0; i < 50; i++)
459 {
460 int status = 0;
461 pid_t r = waitpid(g_managedTorPid, &status, WNOHANG);
462 if (r == g_managedTorPid)
463 break;
464 Sleep(100);
465 }
466 g_managedTorPid = -1;
467 }
468#endif
469 CRITICAL_BLOCK(cs_managedTor)
470 {
471 g_managedTorStatus = "stopped";
472 g_managedTorOnion.clear();
473 }
474}
475
476bool BtfManagedTorEnabled()
477{
478 CRITICAL_BLOCK(cs_managedTor)
479 return g_fManagedTor;
480 return false;
481}
482
483std::string BtfManagedTorStatus()
484{
485 CRITICAL_BLOCK(cs_managedTor)
486 {
487 if (!g_fManagedTor && g_managedTorStatus.empty())
488 return "disabled";
489 return g_managedTorStatus;
490 }
491 return "disabled";
492}
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 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Managed Tor process support. This is transport bootstrapping only: it does
5// not touch consensus, wallet keys, mining, or the .btf identity.
6
7#ifndef BITFLASH_TOR_H
8#define BITFLASH_TOR_H
9
10#include <string>
11
12bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut);
13void BtfStopManagedTor();
14bool BtfManagedTorEnabled();
15std::string BtfManagedTorStatus();
16
17std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
18 const std::string& hiddenServiceDir,
19 unsigned short socksPort,
20 unsigned short p2pPort);
21
22#endif