upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src
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 /src
parentc009e7cc0c5093f4c4add741cb9d131d19956871 (diff)
parent1bf78c53404d83638177889590ce199c87af63bc (diff)
Merge pull request 'net: add managed Tor hidden service bootstrap' (#33) from embedded-tor-manager into main
Diffstat (limited to 'src')
-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
8 files changed, 983 insertions, 26 deletions
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