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:
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.cpp37
-rw-r--r--src/tor.cpp492
-rw-r--r--src/tor.h22
8 files changed, 605 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..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