upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBitflash <admin@bitflash.network>2026-08-13 14:21:15 -0300
committerBitflash <admin@bitflash.network>2026-08-13 14:21:15 -0300
commit5dec9b16a5b98ed0e24ec7113b867f22be2d0026 (patch)
treee06544e9e7516203bc4309460438a0486b0328e4
parentc009e7cc0c5093f4c4add741cb9d131d19956871 (diff)
parent1bf78c53404d83638177889590ce199c87af63bc (diff)
Merge pull request 'net: add managed Tor hidden service bootstrap' (#33) from embedded-tor-manager into main
-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.cpp40
-rw-r--r--src/tor.cpp866
-rw-r--r--src/tor.h23
11 files changed, 1043 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..3c93927 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,41 @@ 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, 19051, 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("ControlPort 127.0.0.1:19051") != std::string::npos &&
1582 torrc.find("CookieAuthentication 1") != std::string::npos,
1583 "binds an authenticated Tor control port to localhost only") ? 0 : 1;
1584 nFail += Check(torrc.find("IsolateSOCKSAuth") != std::string::npos &&
1585 torrc.find("IsolateClientAddr") != std::string::npos &&
1586 torrc.find("IsolateDestAddr") != std::string::npos &&
1587 torrc.find("IsolateDestPort") != std::string::npos,
1588 "enables strict Tor stream isolation") ? 0 : 1;
1589 nFail += Check(torrc.find("HiddenServiceDir \"C:/Bitflash Managed Tor/onion-service\"") != std::string::npos,
1590 "writes a hidden service directory") ? 0 : 1;
1591 nFail += Check(torrc.find("HiddenServiceVersion 3") != std::string::npos,
1592 "requests a v3 onion service") ? 0 : 1;
1593 nFail += Check(torrc.find("HiddenServicePort 8433 127.0.0.1:8433") != std::string::npos,
1594 "maps the onion service to the Bitflash P2P listener") ? 0 : 1;
1595 nFail += Check(BtfManagedTorStatus() == "disabled",
1596 "managed Tor starts disabled") ? 0 : 1;
1597
1598 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1599 nFail, nFail == 1 ? "" : "s");
1600 fflush(stdout);
1601 return nFail == 0 ? 0 : 1;
1602}
1603
1568int RunSelfTest(const std::string& name) 1604int RunSelfTest(const std::string& name)
1569{ 1605{
1570 AttachTerminal(); 1606 AttachTerminal();
@@ -1589,8 +1625,10 @@ int RunSelfTest(const std::string& name)
1589 return RunParseMoneySelfTest(); 1625 return RunParseMoneySelfTest();
1590 if (name == "socks5-proxy") 1626 if (name == "socks5-proxy")
1591 return RunSocks5ProxySelfTest(); 1627 return RunSocks5ProxySelfTest();
1628 if (name == "managed-tor")
1629 return RunManagedTorSelfTest();
1592 1630
1593 printf("Unknown self-test '%s'\n", name.c_str()); 1631 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"); 1632 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; 1633 return 1;
1596} 1634}
diff --git a/src/tor.cpp b/src/tor.cpp
new file mode 100644
index 0000000..e47ee44
--- /dev/null
+++ b/src/tor.cpp
@@ -0,0 +1,866 @@
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_managedTorControlPort = 0;
21static unsigned short g_managedTorP2PPort = 0;
22static std::string g_managedTorPath;
23static std::string g_managedTorDataDir;
24static std::string g_managedTorHiddenServiceDir;
25static std::string g_managedTorOnion;
26static std::string g_managedTorStatus;
27#ifdef _WIN32
28static PROCESS_INFORMATION g_managedTorProcess;
29#else
30static pid_t g_managedTorPid = -1;
31#endif
32
33static std::string PathJoin(const std::string& a, const std::string& b)
34{
35 if (a.empty())
36 return b;
37 char last = a[a.size() - 1];
38 if (last == '/' || last == '\\')
39 return a + b;
40 return a + "/" + b;
41}
42
43static std::string NormalizeTorrcPath(std::string path)
44{
45 for (size_t i = 0; i < path.size(); i++)
46 if (path[i] == '\\')
47 path[i] = '/';
48 return path;
49}
50
51static std::string QuoteTorrcPath(const std::string& path)
52{
53 std::string out = "\"";
54 for (size_t i = 0; i < path.size(); i++)
55 {
56 if (path[i] == '"' || path[i] == '\\')
57 out += '\\';
58 out += path[i];
59 }
60 out += "\"";
61 return out;
62}
63
64static std::string QuoteCommandArg(const std::string& arg)
65{
66 std::string out = "\"";
67 for (size_t i = 0; i < arg.size(); i++)
68 {
69 if (arg[i] == '"')
70 out += '\\';
71 out += arg[i];
72 }
73 out += "\"";
74 return out;
75}
76
77static bool EnsureDir(const std::string& path, std::string& errOut)
78{
79 if (path.empty())
80 {
81 errOut = "directory path is empty";
82 return false;
83 }
84#ifdef _WIN32
85 DWORD attr = GetFileAttributesA(path.c_str());
86 if (attr != INVALID_FILE_ATTRIBUTES)
87 {
88 if (attr & FILE_ATTRIBUTE_DIRECTORY)
89 return true;
90 errOut = strprintf("%s exists and is not a directory", path.c_str());
91 return false;
92 }
93 if (CreateDirectoryA(path.c_str(), NULL))
94 return true;
95 DWORD err = GetLastError();
96 if (err == ERROR_ALREADY_EXISTS)
97 return true;
98 errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err);
99 return false;
100#else
101 struct stat st;
102 if (stat(path.c_str(), &st) == 0)
103 {
104 if (S_ISDIR(st.st_mode))
105 {
106 chmod(path.c_str(), 0700);
107 return true;
108 }
109 errOut = strprintf("%s exists and is not a directory", path.c_str());
110 return false;
111 }
112 if (mkdir(path.c_str(), 0700) == 0)
113 return true;
114 if (errno == EEXIST)
115 {
116 if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode))
117 {
118 chmod(path.c_str(), 0700);
119 return true;
120 }
121 }
122 errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno));
123 return false;
124#endif
125}
126
127static bool WriteTextFile(const std::string& path, const std::string& data,
128 std::string& errOut)
129{
130 std::string tmp = path + ".tmp";
131 FILE* f = fopen(tmp.c_str(), "wb");
132 if (!f)
133 {
134 errOut = strprintf("could not write %s", tmp.c_str());
135 return false;
136 }
137 bool ok = fwrite(data.data(), 1, data.size(), f) == data.size();
138 if (fclose(f) != 0)
139 ok = false;
140 if (!ok)
141 {
142 remove(tmp.c_str());
143 errOut = strprintf("could not finish writing %s", tmp.c_str());
144 return false;
145 }
146#ifdef _WIN32
147 if (!MoveFileExA(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
148 {
149 remove(tmp.c_str());
150 errOut = strprintf("could not install %s atomically (error %lu)",
151 path.c_str(), (unsigned long)GetLastError());
152 return false;
153 }
154#else
155 if (rename(tmp.c_str(), path.c_str()) != 0)
156 {
157 remove(tmp.c_str());
158 errOut = strprintf("could not install %s atomically: %s",
159 path.c_str(), strerror(errno));
160 return false;
161 }
162#endif
163 return true;
164}
165
166static bool ReadBinaryFile(const std::string& path, std::vector<unsigned char>& data,
167 std::string& errOut)
168{
169 data.clear();
170 FILE* f = fopen(path.c_str(), "rb");
171 if (!f)
172 {
173 errOut = strprintf("could not read %s", path.c_str());
174 return false;
175 }
176 unsigned char buf[256];
177 for (;;)
178 {
179 size_t n = fread(buf, 1, sizeof(buf), f);
180 if (n > 0)
181 data.insert(data.end(), buf, buf + n);
182 if (n < sizeof(buf))
183 {
184 if (ferror(f))
185 {
186 fclose(f);
187 errOut = strprintf("could not finish reading %s", path.c_str());
188 return false;
189 }
190 break;
191 }
192 if (data.size() > 1024)
193 {
194 fclose(f);
195 errOut = strprintf("%s is unexpectedly large", path.c_str());
196 return false;
197 }
198 }
199 fclose(f);
200 if (data.empty())
201 {
202 errOut = strprintf("%s is empty", path.c_str());
203 return false;
204 }
205 return true;
206}
207
208static void SetSocketTimeouts(SOCKET s, int millis)
209{
210#ifdef _WIN32
211 DWORD timeout = millis;
212 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
213 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
214#else
215 struct timeval tv;
216 tv.tv_sec = millis / 1000;
217 tv.tv_usec = (millis % 1000) * 1000;
218 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
219 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
220#endif
221}
222
223static bool SendAll(SOCKET s, const std::string& data)
224{
225 const char* p = data.data();
226 int left = (int)data.size();
227 while (left > 0)
228 {
229 int n = send(s, p, left, 0);
230 if (n <= 0)
231 return false;
232 p += n;
233 left -= n;
234 }
235 return true;
236}
237
238static bool RecvControlLine(SOCKET s, std::string& line)
239{
240 line.clear();
241 while (line.size() < 4096)
242 {
243 char ch = 0;
244 int n = recv(s, &ch, 1, 0);
245 if (n <= 0)
246 return false;
247 if (ch == '\n')
248 return true;
249 if (ch != '\r')
250 line += ch;
251 }
252 return false;
253}
254
255static SOCKET ConnectLoopback(unsigned short port, std::string& errOut)
256{
257#ifdef _WIN32
258 WSADATA wsadata;
259 int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
260 if (ret != NO_ERROR)
261 {
262 errOut = strprintf("could not start Winsock for Tor control shutdown (WSAStartup returned %d)", ret);
263 return INVALID_SOCKET;
264 }
265#endif
266 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
267 if (s == INVALID_SOCKET)
268 {
269 errOut = "could not create Tor control socket";
270#ifdef _WIN32
271 WSACleanup();
272#endif
273 return INVALID_SOCKET;
274 }
275 SetSocketTimeouts(s, 3000);
276 struct sockaddr_in addr;
277 memset(&addr, 0, sizeof(addr));
278 addr.sin_family = AF_INET;
279 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
280 addr.sin_port = htons(port);
281 if (connect(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
282 {
283 closesocket(s);
284 errOut = strprintf("could not connect to Tor control port 127.0.0.1:%u", (unsigned)port);
285#ifdef _WIN32
286 WSACleanup();
287#endif
288 return INVALID_SOCKET;
289 }
290 return s;
291}
292
293static void CloseLoopback(SOCKET s)
294{
295 if (s != INVALID_SOCKET)
296 closesocket(s);
297#ifdef _WIN32
298 WSACleanup();
299#endif
300}
301
302static bool ReadPositiveTorControlReply(SOCKET s, std::string& errOut)
303{
304 std::string line;
305 if (!RecvControlLine(s, line))
306 {
307 errOut = "Tor control port closed before replying";
308 return false;
309 }
310 if (line.size() >= 3 && line.substr(0, 3) == "250")
311 return true;
312 errOut = strprintf("Tor control command failed: %s", line.c_str());
313 return false;
314}
315
316static bool RequestManagedTorShutdown(std::string& errOut)
317{
318 std::string dataDir;
319 unsigned short controlPort = 0;
320 CRITICAL_BLOCK(cs_managedTor)
321 {
322 dataDir = g_managedTorDataDir;
323 controlPort = g_managedTorControlPort;
324 }
325 if (controlPort == 0 || dataDir.empty())
326 {
327 errOut = "Tor control port is not available";
328 return false;
329 }
330
331 std::vector<unsigned char> cookie;
332 if (!ReadBinaryFile(PathJoin(dataDir, "control_auth_cookie"), cookie, errOut))
333 return false;
334
335 SOCKET s = ConnectLoopback(controlPort, errOut);
336 if (s == INVALID_SOCKET)
337 return false;
338
339 bool ok = false;
340 std::string auth = "AUTHENTICATE " + HexStr(cookie.begin(), cookie.end(), false) + "\r\n";
341 if (!SendAll(s, auth) || !ReadPositiveTorControlReply(s, errOut))
342 goto done;
343
344 if (!SendAll(s, "SIGNAL SHUTDOWN\r\n"))
345 {
346 errOut = "could not send Tor shutdown signal";
347 goto done;
348 }
349
350 {
351 std::string reply;
352 if (RecvControlLine(s, reply) && !(reply.size() >= 3 && reply.substr(0, 3) == "250"))
353 {
354 errOut = strprintf("Tor rejected shutdown signal: %s", reply.c_str());
355 goto done;
356 }
357 }
358 ok = true;
359
360done:
361 CloseLoopback(s);
362 return ok;
363}
364
365static bool ReadOneLine(const std::string& path, std::string& out)
366{
367 out.clear();
368 FILE* f = fopen(path.c_str(), "rb");
369 if (!f)
370 return false;
371 char buf[256] = {0};
372 if (!fgets(buf, sizeof(buf), f))
373 {
374 fclose(f);
375 return false;
376 }
377 fclose(f);
378 out = buf;
379 while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' ||
380 out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t'))
381 out.erase(out.size() - 1);
382 return !out.empty();
383}
384
385static bool PickLocalPort(unsigned short& portOut, std::string& errOut)
386{
387 portOut = 0;
388 bool ok = false;
389#ifdef _WIN32
390 WSADATA wsadata;
391 int ret = WSAStartup(MAKEWORD(2,2), &wsadata);
392 if (ret != NO_ERROR)
393 {
394 errOut = strprintf("could not start Winsock for local port probe (WSAStartup returned %d)", ret);
395 return false;
396 }
397#endif
398 struct sockaddr_in addr;
399 socklen_t len = sizeof(addr);
400 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
401 if (s == INVALID_SOCKET)
402 {
403 errOut = "could not create local port probe socket";
404 goto done;
405 }
406 memset(&addr, 0, sizeof(addr));
407 addr.sin_family = AF_INET;
408 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
409 addr.sin_port = 0;
410 if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
411 {
412 errOut = "could not bind local port probe socket";
413 goto done;
414 }
415 if (getsockname(s, (struct sockaddr*)&addr, &len) != 0)
416 {
417 errOut = "could not read local port probe socket";
418 goto done;
419 }
420 portOut = ntohs(addr.sin_port);
421 if (portOut == 0)
422 {
423 errOut = "local port probe returned port zero";
424 goto done;
425 }
426 ok = true;
427done:
428 if (s != INVALID_SOCKET)
429 closesocket(s);
430#ifdef _WIN32
431 WSACleanup();
432#endif
433 return ok;
434}
435
436static std::string ExecutableDir()
437{
438#ifdef _WIN32
439 char path[MAX_PATH + 1] = {0};
440 DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
441 if (n == 0 || n >= MAX_PATH)
442 return ".";
443 std::string s = path;
444 size_t slash = s.find_last_of("\\/");
445 if (slash == std::string::npos)
446 return ".";
447 return s.substr(0, slash);
448#else
449 char path[4096] = {0};
450 ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
451 if (n <= 0)
452 return ".";
453 path[n] = 0;
454 std::string s = path;
455 size_t slash = s.find_last_of('/');
456 if (slash == std::string::npos)
457 return ".";
458 return s.substr(0, slash);
459#endif
460}
461
462static bool FileIsExecutableCandidate(const std::string& path)
463{
464#ifdef _WIN32
465 DWORD attr = GetFileAttributesA(path.c_str());
466 return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
467#else
468 return access(path.c_str(), X_OK) == 0;
469#endif
470}
471
472static std::string ResolveTorPath(const std::string& torPathOpt)
473{
474 if (!torPathOpt.empty())
475 return torPathOpt;
476
477 std::vector<std::string> candidates;
478 std::string exeDir = ExecutableDir();
479#ifdef _WIN32
480 candidates.push_back(PathJoin(exeDir, "tor/tor.exe"));
481 candidates.push_back(PathJoin(exeDir, "tor.exe"));
482 candidates.push_back("tor.exe");
483#else
484 candidates.push_back(PathJoin(exeDir, "tor/tor"));
485 candidates.push_back(PathJoin(exeDir, "tor"));
486 candidates.push_back("tor");
487#endif
488 for (size_t i = 0; i < candidates.size(); i++)
489 {
490 if (candidates[i].find('/') == std::string::npos &&
491 candidates[i].find('\\') == std::string::npos)
492 return candidates[i]; // Let the OS search PATH.
493 if (FileIsExecutableCandidate(candidates[i]))
494 return candidates[i];
495 }
496#ifdef _WIN32
497 return "tor.exe";
498#else
499 return "tor";
500#endif
501}
502
503std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
504 const std::string& hiddenServiceDir,
505 unsigned short socksPort,
506 unsigned short controlPort,
507 unsigned short p2pPort)
508{
509 std::string torData = NormalizeTorrcPath(dataDir);
510 std::string hsDir = NormalizeTorrcPath(hiddenServiceDir);
511 std::string s;
512 s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n";
513 s += "DataDirectory " + QuoteTorrcPath(torData) + "\n";
514 s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n",
515 (unsigned)socksPort);
516 s += strprintf("ControlPort 127.0.0.1:%u\n", (unsigned)controlPort);
517 s += "CookieAuthentication 1\n";
518 s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n";
519 s += "HiddenServiceVersion 3\n";
520 s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n",
521 (unsigned)p2pPort, (unsigned)p2pPort);
522 return s;
523}
524
525static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath,
526 std::string& errOut)
527{
528#ifdef _WIN32
529 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
530 STARTUPINFOA si;
531 memset(&si, 0, sizeof(si));
532 si.cb = sizeof(si);
533 std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath);
534 std::vector<char> mutableCmd(cmd.begin(), cmd.end());
535 mutableCmd.push_back(0);
536 if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW,
537 NULL, NULL, &si, &g_managedTorProcess))
538 {
539 errOut = strprintf("could not start Tor process %s (error %lu)",
540 torPath.c_str(), (unsigned long)GetLastError());
541 return false;
542 }
543 return true;
544#else
545 g_managedTorPid = fork();
546 if (g_managedTorPid < 0)
547 {
548 errOut = strprintf("could not fork Tor process: %s", strerror(errno));
549 return false;
550 }
551 if (g_managedTorPid == 0)
552 {
553 execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL);
554 _exit(127);
555 }
556 return true;
557#endif
558}
559
560static bool ManagedTorProcessAlive(bool& fExited, int& nExitCode)
561{
562 fExited = false;
563 nExitCode = 0;
564#ifdef _WIN32
565 HANDLE hProcess = NULL;
566 CRITICAL_BLOCK(cs_managedTor)
567 hProcess = g_managedTorProcess.hProcess;
568 if (!hProcess)
569 return false;
570 DWORD code = 0;
571 if (!GetExitCodeProcess(hProcess, &code))
572 return false;
573 if (code != STILL_ACTIVE)
574 {
575 fExited = true;
576 nExitCode = (int)code;
577 return false;
578 }
579 return true;
580#else
581 pid_t pid = -1;
582 CRITICAL_BLOCK(cs_managedTor)
583 pid = g_managedTorPid;
584 if (pid <= 0)
585 return false;
586 int status = 0;
587 pid_t r = waitpid(pid, &status, WNOHANG);
588 if (r == 0)
589 return true;
590 if (r == pid)
591 {
592 fExited = true;
593 if (WIFEXITED(status))
594 nExitCode = WEXITSTATUS(status);
595 else if (WIFSIGNALED(status))
596 nExitCode = 128 + WTERMSIG(status);
597 CRITICAL_BLOCK(cs_managedTor)
598 if (g_managedTorPid == pid)
599 g_managedTorPid = -1;
600 }
601 return false;
602#endif
603}
604
605static bool ValidateManagedTorStartup(std::string& errOut)
606{
607 Sleep(500);
608 bool fExited = false;
609 int nExitCode = 0;
610 if (ManagedTorProcessAlive(fExited, nExitCode))
611 return true;
612 if (fExited)
613 errOut = strprintf("Tor exited during startup with code %d", nExitCode);
614 else
615 errOut = "Tor process is not running after startup";
616 return false;
617}
618
619static void MarkManagedTorDead(int nExitCode)
620{
621 BtfStopManagedTor();
622 CRITICAL_BLOCK(cs_managedTor)
623 g_managedTorStatus = strprintf("stopped unexpectedly, Tor exit code %d", nExitCode);
624}
625
626static void ThreadManagedTorWatchdog(void*)
627{
628 for (;;)
629 {
630 Sleep(5000);
631 CRITICAL_BLOCK(cs_managedTor)
632 {
633 if (!g_fManagedTor || g_fManagedTorStopping || fShutdown)
634 return;
635 }
636
637 bool fExited = false;
638 int nExitCode = 0;
639 if (!ManagedTorProcessAlive(fExited, nExitCode))
640 {
641 if (fExited)
642 MarkManagedTorDead(nExitCode);
643 else
644 {
645 BtfStopManagedTor();
646 CRITICAL_BLOCK(cs_managedTor)
647 g_managedTorStatus = "stopped unexpectedly, Tor process handle is gone";
648 }
649 return;
650 }
651 }
652}
653
654static void ThreadManagedTorHostname(void*)
655{
656 std::string hostnamePath;
657 unsigned short p2pPort = 0;
658 CRITICAL_BLOCK(cs_managedTor)
659 {
660 hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname");
661 p2pPort = g_managedTorP2PPort;
662 }
663
664 for (int i = 0; i < 90 && !fShutdown; i++)
665 {
666 std::string onion;
667 if (ReadOneLine(hostnamePath, onion))
668 {
669 std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort);
670 std::string err;
671 if (BtfSetLocalOnionEndpoint(endpoint, err))
672 {
673 CRITICAL_BLOCK(cs_managedTor)
674 {
675 g_managedTorOnion = BtfLocalOnionEndpoint();
676 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s",
677 (unsigned)g_managedTorSocksPort,
678 g_managedTorOnion.c_str());
679 }
680 printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str());
681 return;
682 }
683 CRITICAL_BLOCK(cs_managedTor)
684 g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s",
685 err.c_str());
686 return;
687 }
688 Sleep(1000);
689 }
690
691 BtfStopManagedTor();
692 CRITICAL_BLOCK(cs_managedTor)
693 g_managedTorStatus = "failed: Tor did not create a hidden service hostname within 90s";
694}
695
696bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut)
697{
698 CRITICAL_BLOCK(cs_managedTor)
699 {
700 if (g_fManagedTor)
701 {
702 errOut = "managed Tor is already running";
703 return false;
704 }
705 }
706
707 std::string root = PathJoin(GetAppDir(), "managed-tor");
708 std::string dataDir = PathJoin(root, "data");
709 std::string hsDir = PathJoin(root, "onion-service");
710 std::string torrcPath = PathJoin(root, "torrc");
711 if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut))
712 return false;
713
714 unsigned short socksPort = 0;
715 if (!PickLocalPort(socksPort, errOut))
716 return false;
717 unsigned short controlPort = 0;
718 if (!PickLocalPort(controlPort, errOut))
719 return false;
720
721 unsigned short p2pPort = ntohs(nListenPort);
722 std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort,
723 controlPort, p2pPort);
724 if (!WriteTextFile(torrcPath, torrc, errOut))
725 return false;
726
727 std::string torPath = ResolveTorPath(torPathOpt);
728 {
729 CRITICAL_BLOCK(cs_managedTor)
730 {
731 g_fManagedTor = true;
732 g_fManagedTorStopping = false;
733 g_managedTorPath = torPath;
734 g_managedTorDataDir = dataDir;
735 g_managedTorHiddenServiceDir = hsDir;
736 g_managedTorSocksPort = socksPort;
737 g_managedTorControlPort = controlPort;
738 g_managedTorP2PPort = p2pPort;
739 g_managedTorOnion.clear();
740 g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u",
741 torPath.c_str(), (unsigned)socksPort);
742 }
743 }
744
745 if (!StartTorProcess(torPath, torrcPath, errOut))
746 {
747 CRITICAL_BLOCK(cs_managedTor)
748 {
749 g_fManagedTor = false;
750 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
751 }
752 return false;
753 }
754 if (!ValidateManagedTorStartup(errOut))
755 {
756 BtfStopManagedTor();
757 CRITICAL_BLOCK(cs_managedTor)
758 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
759 return false;
760 }
761
762 std::string proxyErr;
763 if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr))
764 {
765 BtfStopManagedTor();
766 errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str());
767 return false;
768 }
769
770 if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1)
771 {
772 BtfStopManagedTor();
773 errOut = "could not start managed Tor hostname watcher";
774 return false;
775 }
776 if (_beginthread(ThreadManagedTorWatchdog, 0, NULL) == (uintptr_t)-1)
777 {
778 BtfStopManagedTor();
779 errOut = "could not start managed Tor watchdog";
780 return false;
781 }
782
783 return true;
784}
785
786void BtfStopManagedTor()
787{
788 bool fWasRunning = false;
789 CRITICAL_BLOCK(cs_managedTor)
790 {
791 fWasRunning = g_fManagedTor;
792 if (!fWasRunning)
793 return;
794 g_fManagedTorStopping = true;
795 g_fManagedTor = false;
796 g_managedTorStatus = "stopping";
797 }
798#ifdef _WIN32
799 if (g_managedTorProcess.hProcess)
800 {
801 std::string shutdownErr;
802 bool fGraceful = RequestManagedTorShutdown(shutdownErr);
803 DWORD waitResult = WaitForSingleObject(g_managedTorProcess.hProcess,
804 fGraceful ? 10000 : 1000);
805 if (waitResult != WAIT_OBJECT_0)
806 {
807 TerminateProcess(g_managedTorProcess.hProcess, 0);
808 WaitForSingleObject(g_managedTorProcess.hProcess, 5000);
809 }
810 CloseHandle(g_managedTorProcess.hProcess);
811 CloseHandle(g_managedTorProcess.hThread);
812 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
813 }
814#else
815 if (g_managedTorPid > 0)
816 {
817 std::string shutdownErr;
818 if (!RequestManagedTorShutdown(shutdownErr))
819 kill(g_managedTorPid, SIGTERM);
820 bool fExited = false;
821 for (int i = 0; i < 100; i++)
822 {
823 int status = 0;
824 pid_t r = waitpid(g_managedTorPid, &status, WNOHANG);
825 if (r == g_managedTorPid)
826 {
827 fExited = true;
828 break;
829 }
830 Sleep(100);
831 }
832 if (!fExited)
833 {
834 kill(g_managedTorPid, SIGKILL);
835 waitpid(g_managedTorPid, NULL, 0);
836 }
837 g_managedTorPid = -1;
838 }
839#endif
840 CRITICAL_BLOCK(cs_managedTor)
841 {
842 g_managedTorStatus = "stopped";
843 g_managedTorOnion.clear();
844 g_managedTorSocksPort = 0;
845 g_managedTorControlPort = 0;
846 g_managedTorP2PPort = 0;
847 }
848}
849
850bool BtfManagedTorEnabled()
851{
852 CRITICAL_BLOCK(cs_managedTor)
853 return g_fManagedTor;
854 return false;
855}
856
857std::string BtfManagedTorStatus()
858{
859 CRITICAL_BLOCK(cs_managedTor)
860 {
861 if (!g_fManagedTor && g_managedTorStatus.empty())
862 return "disabled";
863 return g_managedTorStatus;
864 }
865 return "disabled";
866}
diff --git a/src/tor.h b/src/tor.h
new file mode 100644
index 0000000..7c5c603
--- /dev/null
+++ b/src/tor.h
@@ -0,0 +1,23 @@
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 controlPort,
21 unsigned short p2pPort);
22
23#endif