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-12 10:22:23 -0300
committerBitflash <admin@bitflash.network>2026-08-12 10:22:23 -0300
commitd0ebe648f333def99fababd65f6b929448616d56 (patch)
treec7ccaa28b9f83abd077ba90c031632deda83c80a
parentc330b8e5e1c057049b5b361908c94f41344dff80 (diff)
parenta0a300c240c91fa0c1cd1dc60ff5420a89cc33f8 (diff)
Merge pull request 'net: add Tor mode for outbound discovery' (#28) from tor-mode into main
-rw-r--r--README.md9
-rw-r--r--docs/tor.md84
-rw-r--r--src/main_gui.cpp30
-rw-r--r--src/proxy.cpp50
-rw-r--r--src/proxy.h3
-rw-r--r--src/readme.txt3
-rw-r--r--src/selftest.cpp25
7 files changed, 197 insertions, 7 deletions
diff --git a/README.md b/README.md
index c2fd6b8..978aba5 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,8 @@ Worth being precise, because the difference matters if you are relying on it.
36 36
37**The node still listens on 8433.** Nothing dials by IP any more, but the listener is still there, so anyone who already knows your address and can reach that port may connect directly. If that matters to you, firewall it. 37**The node still listens on 8433.** Nothing dials by IP any more, but the listener is still there, so anyone who already knows your address and can reach that port may connect directly. If that matters to you, firewall it.
38 38
39This is unlinkability between peers, not anonymity against a network observer. It is not Tor. 39This is unlinkability between peers, not anonymity against a network observer.
40For Tor routing, start the node with `-tor`; see [Tor mode](docs/tor.md).
40 41
41--- 42---
42 43
@@ -319,6 +320,7 @@ Other options worth knowing:
319-datadir=PATH # wallet and chain data elsewhere 320-datadir=PATH # wallet and chain data elsewhere
320-port=N # P2P listen port, default 8433 321-port=N # P2P listen port, default 8433
321-socks=HOST:PORT # SOCKS5 for outbound Nostr and .btf rendezvous dials 322-socks=HOST:PORT # SOCKS5 for outbound Nostr and .btf rendezvous dials
323-tor[=HOST:PORT] # Tor mode; default local Tor SOCKS5 proxy is 127.0.0.1:9050
322-debug # verbose log; without it debug.log is nearly silent 324-debug # verbose log; without it debug.log is nearly silent
323-help # full list 325-help # full list
324``` 326```
@@ -332,6 +334,11 @@ dials through a local SOCKS5 proxy such as Tor. When it is set, the node skips
332plain HTTP external-IP probes instead of leaking a direct request outside the 334plain HTTP external-IP probes instead of leaking a direct request outside the
333proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`. 335proxy. IPv6 proxy endpoints use brackets, for example `-socks=[::1]:9050`.
334 336
337`-tor` is a privacy shorthand for the usual local Tor SOCKS5 listener at
338`127.0.0.1:9050`. It routes outbound Nostr discovery and `.btf` rendezvous
339dials through Tor, refuses direct `.onion` resolution, and keeps external-IP
340probes disabled. Use `-tor=HOST:PORT` when Tor listens somewhere else.
341
335### When something looks wrong 342### When something looks wrong
336 343
337**Menu > Diagnostics**, and the same report in `debug.log` every ten minutes: 344**Menu > Diagnostics**, and the same report in `debug.log` every ten minutes:
diff --git a/docs/tor.md b/docs/tor.md
new file mode 100644
index 0000000..087c822
--- /dev/null
+++ b/docs/tor.md
@@ -0,0 +1,84 @@
1# Tor mode
2
3Bitflash can route outbound discovery traffic through a local Tor SOCKS5 proxy.
4This is transport plumbing only: it does not change consensus, wallet keys,
5mining, `.btf` addresses, or the encrypted rendezvous protocol.
6
7## Client mode
8
9Start Tor locally, then start Bitflash with:
10
11```bash
12bitflash -tor
13```
14
15`-tor` is shorthand for:
16
17```bash
18bitflash -socks=127.0.0.1:9050
19```
20
21If Tor listens somewhere else:
22
23```bash
24bitflash -tor=127.0.0.1:9150
25bitflash -tor=[::1]:9050
26```
27
28When Tor mode is enabled:
29
30- outbound Nostr relay dials use Tor;
31- outbound `.btf` rendezvous relay dials use Tor;
32- destination names are sent to Tor as SOCKS5 domain-name CONNECT requests, so
33 the local DNS resolver does not see them;
34- direct `.onion` dials are refused unless a SOCKS5/Tor proxy is enabled;
35- plain HTTP external-IP probes are skipped.
36
37## Onion rendezvous relay
38
39An operator can expose a Bitflash rendezvous relay as a Tor hidden service. The
40node does not create hidden services itself; Tor owns that configuration.
41
42Example `torrc`:
43
44```text
45HiddenServiceDir /var/lib/tor/bitflash-rendezvous/
46HiddenServicePort 8434 127.0.0.1:8434
47```
48
49After restarting Tor, read the generated onion name:
50
51```bash
52sudo cat /var/lib/tor/bitflash-rendezvous/hostname
53```
54
55Then run the relay on localhost/VPS as usual and announce the onion endpoint from
56nodes that use Tor:
57
58```bash
59bitflash -tor -rvrelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434
60bitflash -tor -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434
61```
62
63For a pool operator that wants the pool descriptor to point at an onion
64rendezvous relay, use both:
65
66```bash
67bitflash -operator -tor \
68 -rvrelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434 \
69 -announcerelay=exampleexampleexampleexampleexampleexampleexampleexample.onion:8434
70```
71
72## Full-node hidden service
73
74The legacy P2P listener still binds to port `8433`. You can expose it through
75Tor manually:
76
77```text
78HiddenServiceDir /var/lib/tor/bitflash-node/
79HiddenServicePort 8433 127.0.0.1:8433
80```
81
82Current automatic peer discovery is `.btf`/rendezvous based, not onion-address
83gossip. Treat direct full-node onion access as an operator/admin tool unless a
84future release adds first-class onion peer advertisements.
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index 90a6050..bef8503 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -138,6 +138,7 @@ static void PrintUsage()
138 printf("Network:\n"); 138 printf("Network:\n");
139 printf(" /port=N (P2P listen port, default 8433)\n"); 139 printf(" /port=N (P2P listen port, default 8433)\n");
140 printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n"); 140 printf(" /socks=HOST:PORT (SOCKS5 proxy for outbound Nostr and .btf relay dials)\n");
141 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
141 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 142 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
142 printf("\n"); 143 printf("\n");
143 printf("Wallet:\n"); 144 printf("Wallet:\n");
@@ -285,16 +286,35 @@ static void ParseStartupArguments(int argc, char* argv[])
285 if (!announceRelay.empty()) 286 if (!announceRelay.empty())
286 strBtfAnnounceRelay = announceRelay; 287 strBtfAnnounceRelay = announceRelay;
287 288
288 string socksProxy = argval2(argc, argv, "/socks", "-socks"); 289 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
289 if (!socksProxy.empty()) 290 if (fTorMode)
290 { 291 {
292 string socksProxy = argval2(argc, argv, "/socks", "-socks");
293 if (!socksProxy.empty())
294 fprintf(stderr, "Warning: /tor takes precedence over /socks=%s\n",
295 socksProxy.c_str());
296
297 string torProxy = argval2(argc, argv, "/tor", "-tor");
291 string err; 298 string err;
292 if (!BtfSetSocks5Proxy(socksProxy, err)) 299 if (!BtfEnableTorProxy(torProxy, err))
293 fprintf(stderr, "Ignoring /socks=%s: %s\n", socksProxy.c_str(), err.c_str()); 300 fprintf(stderr, "Ignoring /tor=%s: %s\n", torProxy.c_str(), err.c_str());
294 else 301 else
295 fprintf(stderr, "SOCKS5 proxy enabled for outbound discovery/relay dials: %s\n", 302 fprintf(stderr, "Tor mode enabled through SOCKS5 proxy %s\n",
296 BtfSocks5ProxyName().c_str()); 303 BtfSocks5ProxyName().c_str());
297 } 304 }
305 else
306 {
307 string socksProxy = argval2(argc, argv, "/socks", "-socks");
308 if (!socksProxy.empty())
309 {
310 string err;
311 if (!BtfSetSocks5Proxy(socksProxy, err))
312 fprintf(stderr, "Ignoring /socks=%s: %s\n", socksProxy.c_str(), err.c_str());
313 else
314 fprintf(stderr, "SOCKS5 proxy enabled for outbound discovery/relay dials: %s\n",
315 BtfSocks5ProxyName().c_str());
316 }
317 }
298 318
299 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for 319 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for
300 // testing the seed path and for private networks that ship no compiled list. 320 // testing the seed path and for private networks that ship no compiled list.
diff --git a/src/proxy.cpp b/src/proxy.cpp
index d7dc324..7d30911 100644
--- a/src/proxy.cpp
+++ b/src/proxy.cpp
@@ -9,8 +9,10 @@
9 9
10static CCriticalSection cs_socks5; 10static CCriticalSection cs_socks5;
11static bool g_fSocks5Proxy = false; 11static bool g_fSocks5Proxy = false;
12static bool g_fTorProxy = false;
12static std::string g_socks5Host; 13static std::string g_socks5Host;
13static unsigned short g_socks5Port = 0; 14static unsigned short g_socks5Port = 0;
15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050";
14 16
15static void SetSocketTimeout(SOCKET s, int nTimeoutSecs) 17static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
16{ 18{
@@ -67,6 +69,24 @@ static bool ValidProxyIpv6HostChar(char c)
67 return isxdigit((unsigned char)c) || c == ':' || c == '.'; 69 return isxdigit((unsigned char)c) || c == ':' || c == '.';
68} 70}
69 71
72static std::string LowerProxyHost(std::string host)
73{
74 if (!host.empty() && host[host.size() - 1] == '.')
75 host.erase(host.size() - 1);
76 for (size_t i = 0; i < host.size(); i++)
77 host[i] = (char)tolower((unsigned char)host[i]);
78 return host;
79}
80
81bool BtfIsTorOnionHost(const std::string& host)
82{
83 std::string lower = LowerProxyHost(host);
84 static const char* suffix = ".onion";
85 size_t suffixLen = strlen(suffix);
86 return lower.size() > suffixLen &&
87 lower.compare(lower.size() - suffixLen, suffixLen, suffix) == 0;
88}
89
70static bool ParsePort(const std::string& port, unsigned short& portOut, 90static bool ParsePort(const std::string& port, unsigned short& portOut,
71 std::string& errOut) 91 std::string& errOut)
72{ 92{
@@ -176,7 +196,8 @@ bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
176 return true; 196 return true;
177} 197}
178 198
179bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut) 199static bool SetSocks5Proxy(const std::string& spec, bool fTorMode,
200 std::string& errOut)
180{ 201{
181 std::string host; 202 std::string host;
182 unsigned short port = 0; 203 unsigned short port = 0;
@@ -187,15 +208,27 @@ bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut)
187 g_socks5Host = host; 208 g_socks5Host = host;
188 g_socks5Port = port; 209 g_socks5Port = port;
189 g_fSocks5Proxy = true; 210 g_fSocks5Proxy = true;
211 g_fTorProxy = fTorMode;
190 } 212 }
191 return true; 213 return true;
192} 214}
193 215
216bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut)
217{
218 return SetSocks5Proxy(spec, false, errOut);
219}
220
221bool BtfEnableTorProxy(const std::string& spec, std::string& errOut)
222{
223 return SetSocks5Proxy(spec.empty() ? DEFAULT_TOR_SOCKS5_PROXY : spec, true, errOut);
224}
225
194void BtfClearSocks5Proxy() 226void BtfClearSocks5Proxy()
195{ 227{
196 CRITICAL_BLOCK(cs_socks5) 228 CRITICAL_BLOCK(cs_socks5)
197 { 229 {
198 g_fSocks5Proxy = false; 230 g_fSocks5Proxy = false;
231 g_fTorProxy = false;
199 g_socks5Host.clear(); 232 g_socks5Host.clear();
200 g_socks5Port = 0; 233 g_socks5Port = 0;
201 } 234 }
@@ -208,6 +241,13 @@ bool BtfSocks5ProxyEnabled()
208 return false; 241 return false;
209} 242}
210 243
244bool BtfTorProxyEnabled()
245{
246 CRITICAL_BLOCK(cs_socks5)
247 return g_fSocks5Proxy && g_fTorProxy;
248 return false;
249}
250
211std::string BtfSocks5ProxyName() 251std::string BtfSocks5ProxyName()
212{ 252{
213 CRITICAL_BLOCK(cs_socks5) 253 CRITICAL_BLOCK(cs_socks5)
@@ -315,7 +355,15 @@ SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
315 } 355 }
316 356
317 if (!fProxy) 357 if (!fProxy)
358 {
359 if (BtfIsTorOnionHost(destHost))
360 {
361 error("Refusing direct .onion connection to %s:%u; use /tor or /socks\n",
362 destHost.c_str(), (unsigned)destPort);
363 return INVALID_SOCKET;
364 }
318 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs); 365 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
366 }
319 367
320 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs); 368 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
321 if (s == INVALID_SOCKET) 369 if (s == INVALID_SOCKET)
diff --git a/src/proxy.h b/src/proxy.h
index b5e69c8..6429cf2 100644
--- a/src/proxy.h
+++ b/src/proxy.h
@@ -21,8 +21,11 @@
21bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut, 21bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
22 unsigned short& portOut, std::string& errOut); 22 unsigned short& portOut, std::string& errOut);
23bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut); 23bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut);
24bool BtfEnableTorProxy(const std::string& spec, std::string& errOut);
24void BtfClearSocks5Proxy(); 25void BtfClearSocks5Proxy();
25bool BtfSocks5ProxyEnabled(); 26bool BtfSocks5ProxyEnabled();
27bool BtfTorProxyEnabled();
28bool BtfIsTorOnionHost(const std::string& host);
26std::string BtfSocks5ProxyName(); 29std::string BtfSocks5ProxyName();
27 30
28SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort, 31SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
diff --git a/src/readme.txt b/src/readme.txt
index 1fa77df..794fe20 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -46,6 +46,9 @@ Command-line options (in addition to the original ones)
46 route outbound Nostr and .btf rendezvous dials through a 46 route outbound Nostr and .btf rendezvous dials through a
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor 47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
48 or /socks=[::1]:9050 for an IPv6 loopback proxy 48 or /socks=[::1]:9050 for an IPv6 loopback proxy
49 /tor[=HOST:PORT]
50 Tor mode for outbound Nostr and .btf rendezvous dials;
51 defaults to the local Tor SOCKS5 proxy at 127.0.0.1:9050
49 /solomine mine without requiring a connected peer (local testing) 52 /solomine mine without requiring a connected peer (local testing)
50 /operator operator mode (run pool server) 53 /operator operator mode (run pool server)
51 /participant=ADDR 54 /participant=ADDR
diff --git a/src/selftest.cpp b/src/selftest.cpp
index aa4e528..cca5335 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -1474,6 +1474,11 @@ static int RunSocks5ProxySelfTest()
1474 "rejects overflowing port") ? 0 : 1; 1474 "rejects overflowing port") ? 0 : 1;
1475 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err), 1475 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err),
1476 "rejects unsupported authenticated proxy syntax") ? 0 : 1; 1476 "rejects unsupported authenticated proxy syntax") ? 0 : 1;
1477 nFail += Check(BtfIsTorOnionHost("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd.onion") &&
1478 BtfIsTorOnionHost("Relay.Example.ONION.") &&
1479 !BtfIsTorOnionHost("relay.example") &&
1480 !BtfIsTorOnionHost(".onion"),
1481 "detects .onion hosts without accepting lookalikes") ? 0 : 1;
1477 1482
1478 BtfClearSocks5Proxy(); 1483 BtfClearSocks5Proxy();
1479 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1; 1484 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
@@ -1490,6 +1495,26 @@ static int RunSocks5ProxySelfTest()
1490 "enables bracketed IPv6 proxy endpoint") ? 0 : 1; 1495 "enables bracketed IPv6 proxy endpoint") ? 0 : 1;
1491 BtfClearSocks5Proxy(); 1496 BtfClearSocks5Proxy();
1492 1497
1498 nFail += Check(BtfEnableTorProxy("", err) &&
1499 BtfSocks5ProxyEnabled() &&
1500 BtfTorProxyEnabled() &&
1501 BtfSocks5ProxyName() == "127.0.0.1:9050",
1502 "enables Tor mode on the default local SOCKS5 endpoint") ? 0 : 1;
1503 BtfClearSocks5Proxy();
1504
1505 nFail += Check(BtfEnableTorProxy("[::1]:9050", err) &&
1506 BtfSocks5ProxyEnabled() &&
1507 BtfTorProxyEnabled() &&
1508 BtfSocks5ProxyName() == "[::1]:9050",
1509 "enables Tor mode on a custom IPv6 endpoint") ? 0 : 1;
1510 BtfClearSocks5Proxy();
1511
1512 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9051", err) &&
1513 BtfSocks5ProxyEnabled() &&
1514 !BtfTorProxyEnabled(),
1515 "plain SOCKS5 mode is distinct from Tor mode") ? 0 : 1;
1516 BtfClearSocks5Proxy();
1517
1493 std::string handshakeErr; 1518 std::string handshakeErr;
1494 bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr); 1519 bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr);
1495 nFail += Check(fHandshakeOk, handshakeErr.empty() ? 1520 nFail += Check(fHandshakeOk, handshakeErr.empty() ?