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/Makefile7
-rw-r--r--src/btfrv.cpp9
-rw-r--r--src/main_gui.cpp36
-rw-r--r--src/makefile.mingw10
-rw-r--r--src/net.cpp4
-rw-r--r--src/nostr.cpp50
-rw-r--r--src/proxy.cpp377
-rw-r--r--src/proxy.h34
-rw-r--r--src/readme.txt7
-rw-r--r--src/selftest.cpp279
10 files changed, 759 insertions, 54 deletions
diff --git a/src/Makefile b/src/Makefile
index 00a8182..511fe79 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -30,7 +30,7 @@ WARNFLAGS = -Wformat -Werror=format \
30 -Wno-narrowing -Wno-unused-result 30 -Wno-narrowing -Wno-unused-result
31 31
32CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ 32CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \
33 -DSECP256K1_STATIC \ 33 -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \
34 -I$(SECP_INC) -I$(RANDOMX_DIR)/src \ 34 -I$(SECP_INC) -I$(RANDOMX_DIR)/src \
35 -I/usr/include/nlohmann \ 35 -I/usr/include/nlohmann \
36 -I$(IMGUI_DIR) 36 -I$(IMGUI_DIR)
@@ -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 font_roboto.h 56 selftest.h walletcmd.h compat.h sockcount.h proxy.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 64 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
65 65
66GUI_OBJS = obj/gui.o obj/main_gui.o 66GUI_OBJS = obj/gui.o obj/main_gui.o
67 67
@@ -193,5 +193,6 @@ tests: test_bip32 bitflash-node
193 ./bitflash-node -selftest=consensus-limits 193 ./bitflash-node -selftest=consensus-limits
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 197
197.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke 198.PHONY: tests fuzz-net-message-smoke fuzz-script-smoke
diff --git a/src/btfrv.cpp b/src/btfrv.cpp
index 6ac3280..d4f079f 100644
--- a/src/btfrv.cpp
+++ b/src/btfrv.cpp
@@ -5,6 +5,9 @@
5// Windows/Winsock implementation (Layer 6 will #ifdef this for Linux/BSD sockets). 5// Windows/Winsock implementation (Layer 6 will #ifdef this for Linux/BSD sockets).
6 6
7#include "btfrv.h" 7#include "btfrv.h"
8#ifdef BITFLASH_NODE_BUILD
9#include "proxy.h"
10#endif
8 11
9// Portable sockets: Winsock on Windows, BSD sockets on Linux/POSIX (for the 12// Portable sockets: Winsock on Windows, BSD sockets on Linux/POSIX (for the
10// headless relay `bitflashd` that runs on the VPS). 13// headless relay `bitflashd` that runs on the VPS).
@@ -92,6 +95,11 @@ static bool WriteN(SOCKET s, const void* buf, int n)
92 95
93static SOCKET ConnectTo(const char* host, unsigned short port) 96static SOCKET ConnectTo(const char* host, unsigned short port)
94{ 97{
98#ifdef BITFLASH_NODE_BUILD
99 SOCKET s = BtfConnectSocket(host, port, SOCK_RV_DIAL, 10);
100 if (s == INVALID_SOCKET)
101 return INVALID_SOCKET;
102#else
95 struct addrinfo hints, *res = NULL; 103 struct addrinfo hints, *res = NULL;
96 memset(&hints, 0, sizeof(hints)); 104 memset(&hints, 0, sizeof(hints));
97 hints.ai_family = AF_INET; 105 hints.ai_family = AF_INET;
@@ -119,6 +127,7 @@ static SOCKET ConnectTo(const char* host, unsigned short port)
119 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET; 127 CLOSESOCK(s); freeaddrinfo(res); return INVALID_SOCKET;
120 } 128 }
121 freeaddrinfo(res); 129 freeaddrinfo(res);
130#endif
122 131
123 // Clear the timeout -- the socket is now a live tunnel and must not 132 // Clear the timeout -- the socket is now a live tunnel and must not
124 // time out when idle. recv() will block until data arrives or peer closes. 133 // time out when idle. recv() will block until data arrives or peer closes.
diff --git a/src/main_gui.cpp b/src/main_gui.cpp
index b036ac9..bef8503 100644
--- a/src/main_gui.cpp
+++ b/src/main_gui.cpp
@@ -1,6 +1,7 @@
1// Bitflash entry point -- starts node threads then runs GUI (or headless). 1// Bitflash entry point -- starts node threads then runs GUI (or headless).
2 2
3#include "headers_core.h" 3#include "headers_core.h"
4#include "proxy.h"
4#include "selftest.h" 5#include "selftest.h"
5#include "walletcmd.h" 6#include "walletcmd.h"
6#include <thread> // hardware_concurrency, to sanity-check /genproclimit 7#include <thread> // hardware_concurrency, to sanity-check /genproclimit
@@ -110,7 +111,8 @@ static void PrintUsage()
110 printf(" /debug\n"); 111 printf(" /debug\n");
111 printf(" /gen\n"); 112 printf(" /gen\n");
112 printf(" /nogui or /daemon\n"); 113 printf(" /nogui or /daemon\n");
113 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, or wallet-encrypt\n"); 114 printf(" /selftest=wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt,\n");
115 printf(" net-message, consensus-limits, pool-stratum, parse-money, or socks5-proxy\n");
114 printf("\n"); 116 printf("\n");
115 printf("Mining mode:\n"); 117 printf("Mining mode:\n");
116 printf(" /operator\n"); 118 printf(" /operator\n");
@@ -135,6 +137,8 @@ static void PrintUsage()
135 printf("\n"); 137 printf("\n");
136 printf("Network:\n"); 138 printf("Network:\n");
137 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");
141 printf(" /tor[=HOST:PORT] (Tor mode; default SOCKS5 proxy is 127.0.0.1:9050)\n");
138 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 142 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
139 printf("\n"); 143 printf("\n");
140 printf("Wallet:\n"); 144 printf("Wallet:\n");
@@ -282,6 +286,36 @@ static void ParseStartupArguments(int argc, char* argv[])
282 if (!announceRelay.empty()) 286 if (!announceRelay.empty())
283 strBtfAnnounceRelay = announceRelay; 287 strBtfAnnounceRelay = announceRelay;
284 288
289 bool fTorMode = arg(argc, argv, "/tor") || arg(argc, argv, "-tor");
290 if (fTorMode)
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");
298 string err;
299 if (!BtfEnableTorProxy(torProxy, err))
300 fprintf(stderr, "Ignoring /tor=%s: %s\n", torProxy.c_str(), err.c_str());
301 else
302 fprintf(stderr, "Tor mode enabled through SOCKS5 proxy %s\n",
303 BtfSocks5ProxyName().c_str());
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 }
318
285 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for 319 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for
286 // 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.
287 for (int i = 1; i < argc; i++) 321 for (int i = 1; i < argc; i++)
diff --git a/src/makefile.mingw b/src/makefile.mingw
index 678d2e8..4e36a5c 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -15,8 +15,13 @@ WARNFLAGS = -Wformat -Werror=format \
15 -Wno-narrowing -Wno-unused-result 15 -Wno-narrowing -Wno-unused-result
16 16
17CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ 17CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \
18<<<<<<< HEAD
18 -DWIN32 -DNOPCH \ 19 -DWIN32 -DNOPCH \
19 -DSECP256K1_STATIC \ 20 -DSECP256K1_STATIC \
21=======
22 -DWIN32 -DNOPCH -DFD_SETSIZE=1024 \
23 -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \
24>>>>>>> origin/main
20 -I$(DEPS)/include -I$(RANDOMX)/src \ 25 -I$(DEPS)/include -I$(RANDOMX)/src \
21 -I$(DEPS)/include/nlohmann \ 26 -I$(DEPS)/include/nlohmann \
22 -IC:/msys64/ucrt64/include \ 27 -IC:/msys64/ucrt64/include \
@@ -37,7 +42,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 \ 42NODE_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 \ 43 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 \ 44 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 45 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
41 46
42GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o 47GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
43 48
@@ -51,7 +56,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 \ 56HEADERS = 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 \ 57 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 \ 58 randomx_pow.h btfaddr.h btfchan.h btfrv.h btftunnel.h market.h \
54 selftest.h walletcmd.h sockcount.h compat.h font_roboto.h 59 selftest.h walletcmd.h sockcount.h proxy.h compat.h font_roboto.h
55 60
56all: bitflash.exe 61all: bitflash.exe
57 62
@@ -112,3 +117,4 @@ tests: test_bip32.exe bitflash.exe
112 ./bitflash.exe -selftest=net-message 117 ./bitflash.exe -selftest=net-message
113 ./bitflash.exe -selftest=consensus-limits 118 ./bitflash.exe -selftest=consensus-limits
114 ./bitflash.exe -selftest=pool-stratum 119 ./bitflash.exe -selftest=pool-stratum
120 ./bitflash.exe -selftest=socks5-proxy
diff --git a/src/net.cpp b/src/net.cpp
index b6b990a..c24547e 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -19,6 +19,7 @@
19#include <openssl/rand.h> 19#include <openssl/rand.h>
20#include "btfaddr.h" 20#include "btfaddr.h"
21#include "btftunnel.h" 21#include "btftunnel.h"
22#include "proxy.h"
22 23
23void ThreadReconnectCachedBtfPeers(void* parg); 24void ThreadReconnectCachedBtfPeers(void* parg);
24void ThreadMessageHandler2(void* parg); 25void ThreadMessageHandler2(void* parg);
@@ -377,6 +378,9 @@ string GetDiagnosticsText()
377 378
378bool GetMyExternalIP(unsigned int& ipRet) 379bool GetMyExternalIP(unsigned int& ipRet)
379{ 380{
381 if (BtfSocks5ProxyEnabled())
382 return error("GetMyExternalIP() skipped while SOCKS5 proxy is enabled\n");
383
380 // Try several plain-text IP echo services in order. 384 // Try several plain-text IP echo services in order.
381 // Each returns just the IPv4 address as the first line of the HTTP body. 385 // Each returns just the IPv4 address as the first line of the HTTP body.
382 struct { const char* host; const char* path; } services[] = { 386 struct { const char* host; const char* path; } services[] = {
diff --git a/src/nostr.cpp b/src/nostr.cpp
index 9bc538f..d286767 100644
--- a/src/nostr.cpp
+++ b/src/nostr.cpp
@@ -29,6 +29,7 @@
29 29
30#include "btfaddr.h" 30#include "btfaddr.h"
31#include "btfchan.h" 31#include "btfchan.h"
32#include "proxy.h"
32 33
33using json = nlohmann::json; 34using json = nlohmann::json;
34 35
@@ -540,53 +541,8 @@ public:
540 size_t colon = host.find(':'); 541 size_t colon = host.find(':');
541 if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); } 542 if (colon != string::npos) { port = atoi(host.substr(colon + 1).c_str()); host = host.substr(0, colon); }
542 543
543 // Resolve and connect (TCP). AF_UNSPEC so we get both A and AAAA 544 hSocket = BtfConnectSocket(host, (unsigned short)port, SOCK_NOSTR, timeoutSec);
544 // records back and try them in the order the resolver prefers, 545 if (hSocket == INVALID_SOCKET)
545 // rather than hard-failing whenever the IPv4 path to a relay is
546 // filtered/rate-limited (increasingly common for CDN-fronted
547 // relays) while a working IPv6 route exists right there in the
548 // same getaddrinfo() result.
549 struct addrinfo hints, *res = NULL, *rp = NULL;
550 memset(&hints, 0, sizeof(hints));
551 hints.ai_family = AF_UNSPEC;
552 hints.ai_socktype = SOCK_STREAM;
553 char portstr[16]; sprintf(portstr, "%d", port);
554 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
555 return error("Nostr: getaddrinfo %s failed", host.c_str());
556
557 bool fConnected = false;
558 for (rp = res; rp != NULL; rp = rp->ai_next)
559 {
560 hSocket = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), SOCK_NOSTR);
561 if (hSocket == INVALID_SOCKET)
562 continue;
563
564 // Socket receive/send timeout: Windows takes a DWORD of
565 // milliseconds; POSIX takes a struct timeval.
566#ifdef _WIN32
567 DWORD tv = timeoutSec * 1000;
568 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
569 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
570#else
571 struct timeval tv;
572 tv.tv_sec = timeoutSec;
573 tv.tv_usec = 0;
574 setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&tv, sizeof(tv));
575 setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&tv, sizeof(tv));
576#endif
577
578 if (connect(hSocket, rp->ai_addr, (int)rp->ai_addrlen) == 0)
579 {
580 fConnected = true;
581 break;
582 }
583
584 BtfCloseSocket(hSocket);
585 hSocket = INVALID_SOCKET;
586 }
587 freeaddrinfo(res);
588
589 if (!fConnected)
590 return error("Nostr: connect %s:%d failed", host.c_str(), port); 546 return error("Nostr: connect %s:%d failed", host.c_str(), port);
591 547
592 // TLS 548 // TLS
diff --git a/src/proxy.cpp b/src/proxy.cpp
new file mode 100644
index 0000000..7d30911
--- /dev/null
+++ b/src/proxy.cpp
@@ -0,0 +1,377 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers_core.h"
5#include "proxy.h"
6#include "sockcount.h"
7
8#include <errno.h>
9
10static CCriticalSection cs_socks5;
11static bool g_fSocks5Proxy = false;
12static bool g_fTorProxy = false;
13static std::string g_socks5Host;
14static unsigned short g_socks5Port = 0;
15static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050";
16
17static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
18{
19 if (nTimeoutSecs <= 0)
20 return;
21#ifdef _WIN32
22 DWORD tv = (DWORD)nTimeoutSecs * 1000;
23 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
24 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
25#else
26 struct timeval tv;
27 tv.tv_sec = nTimeoutSecs;
28 tv.tv_usec = 0;
29 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
30 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
31#endif
32}
33
34static bool ReadN(SOCKET s, void* buf, int n)
35{
36 char* p = (char*)buf;
37 int off = 0;
38 while (off < n)
39 {
40 int r = recv(s, p + off, n - off, 0);
41 if (r <= 0)
42 return false;
43 off += r;
44 }
45 return true;
46}
47
48static bool WriteN(SOCKET s, const void* buf, int n)
49{
50 const char* p = (const char*)buf;
51 int off = 0;
52 while (off < n)
53 {
54 int r = send(s, p + off, n - off, 0);
55 if (r <= 0)
56 return false;
57 off += r;
58 }
59 return true;
60}
61
62static bool ValidProxyHostChar(char c)
63{
64 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_';
65}
66
67static bool ValidProxyIpv6HostChar(char c)
68{
69 return isxdigit((unsigned char)c) || c == ':' || c == '.';
70}
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
90static bool ParsePort(const std::string& port, unsigned short& portOut,
91 std::string& errOut)
92{
93 if (port.empty())
94 {
95 errOut = "port is empty";
96 return false;
97 }
98 for (size_t i = 0; i < port.size(); i++)
99 {
100 if (!isdigit((unsigned char)port[i]))
101 {
102 errOut = "port is not numeric";
103 return false;
104 }
105 }
106
107 errno = 0;
108 char* end = NULL;
109 long nPort = strtol(port.c_str(), &end, 10);
110 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
111 {
112 errOut = "port is out of range";
113 return false;
114 }
115 portOut = (unsigned short)nPort;
116 return true;
117}
118
119bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
120 unsigned short& portOut, std::string& errOut)
121{
122 hostOut.clear();
123 portOut = 0;
124 errOut.clear();
125
126 std::string host;
127 std::string port;
128 bool fBracketedIpv6 = false;
129 if (!spec.empty() && spec[0] == '[')
130 {
131 size_t close = spec.find(']');
132 if (close == std::string::npos || close == 1 || close + 1 >= spec.size() || spec[close + 1] != ':')
133 {
134 errOut = "expected [IPv6]:PORT";
135 return false;
136 }
137 host = spec.substr(1, close - 1);
138 port = spec.substr(close + 2);
139 fBracketedIpv6 = true;
140 }
141 else
142 {
143 size_t colon = spec.rfind(':');
144 if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size())
145 {
146 errOut = "expected HOST:PORT";
147 return false;
148 }
149 host = spec.substr(0, colon);
150 port = spec.substr(colon + 1);
151 if (host.find(':') != std::string::npos)
152 {
153 errOut = "IPv6 proxy hosts must use [ADDR]:PORT";
154 return false;
155 }
156 }
157
158 if (host.size() > 255)
159 {
160 errOut = "host is too long";
161 return false;
162 }
163 if (fBracketedIpv6)
164 {
165 bool fHasColon = false;
166 for (size_t i = 0; i < host.size(); i++)
167 {
168 fHasColon = fHasColon || host[i] == ':';
169 if (!ValidProxyIpv6HostChar(host[i]))
170 {
171 errOut = "IPv6 proxy host contains unsupported characters";
172 return false;
173 }
174 }
175 if (!fHasColon)
176 {
177 errOut = "bracketed proxy host is not IPv6";
178 return false;
179 }
180 }
181 else
182 {
183 for (size_t i = 0; i < host.size(); i++)
184 {
185 if (!ValidProxyHostChar(host[i]))
186 {
187 errOut = "host contains unsupported characters";
188 return false;
189 }
190 }
191 }
192 if (!ParsePort(port, portOut, errOut))
193 return false;
194
195 hostOut = host;
196 return true;
197}
198
199static bool SetSocks5Proxy(const std::string& spec, bool fTorMode,
200 std::string& errOut)
201{
202 std::string host;
203 unsigned short port = 0;
204 if (!BtfParseSocks5Proxy(spec, host, port, errOut))
205 return false;
206 CRITICAL_BLOCK(cs_socks5)
207 {
208 g_socks5Host = host;
209 g_socks5Port = port;
210 g_fSocks5Proxy = true;
211 g_fTorProxy = fTorMode;
212 }
213 return true;
214}
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
226void BtfClearSocks5Proxy()
227{
228 CRITICAL_BLOCK(cs_socks5)
229 {
230 g_fSocks5Proxy = false;
231 g_fTorProxy = false;
232 g_socks5Host.clear();
233 g_socks5Port = 0;
234 }
235}
236
237bool BtfSocks5ProxyEnabled()
238{
239 CRITICAL_BLOCK(cs_socks5)
240 return g_fSocks5Proxy;
241 return false;
242}
243
244bool BtfTorProxyEnabled()
245{
246 CRITICAL_BLOCK(cs_socks5)
247 return g_fSocks5Proxy && g_fTorProxy;
248 return false;
249}
250
251std::string BtfSocks5ProxyName()
252{
253 CRITICAL_BLOCK(cs_socks5)
254 {
255 if (!g_fSocks5Proxy)
256 return "";
257 if (g_socks5Host.find(':') != std::string::npos)
258 return strprintf("[%s]:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
259 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
260 }
261 return "";
262}
263
264static SOCKET ConnectDirect(const std::string& host, unsigned short port,
265 int nSockSite, int nTimeoutSecs)
266{
267 struct addrinfo hints, *res = NULL, *rp = NULL;
268 memset(&hints, 0, sizeof(hints));
269 hints.ai_family = AF_UNSPEC;
270 hints.ai_socktype = SOCK_STREAM;
271 char portstr[16];
272 sprintf(portstr, "%u", (unsigned)port);
273 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
274 return INVALID_SOCKET;
275
276 SOCKET s = INVALID_SOCKET;
277 for (rp = res; rp != NULL; rp = rp->ai_next)
278 {
279 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), nSockSite);
280 if (s == INVALID_SOCKET)
281 continue;
282 SetSocketTimeout(s, nTimeoutSecs);
283 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) == 0)
284 break;
285 BtfCloseSocket(s);
286 s = INVALID_SOCKET;
287 }
288 freeaddrinfo(res);
289 return s;
290}
291
292static bool Socks5Connect(SOCKET s, const std::string& destHost,
293 unsigned short destPort)
294{
295 if (destHost.empty() || destHost.size() > 255)
296 return false;
297
298 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
299 if (!WriteN(s, hello, sizeof(hello)))
300 return false;
301 unsigned char choice[2] = { 0, 0 };
302 if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00)
303 return false;
304
305 std::vector<unsigned char> req;
306 req.reserve(7 + destHost.size());
307 req.push_back(0x05); // version
308 req.push_back(0x01); // CONNECT
309 req.push_back(0x00); // reserved
310 req.push_back(0x03); // domain name; proxy resolves, avoiding local DNS leak
311 req.push_back((unsigned char)destHost.size());
312 req.insert(req.end(), destHost.begin(), destHost.end());
313 req.push_back((unsigned char)(destPort >> 8));
314 req.push_back((unsigned char)(destPort & 0xff));
315 if (!WriteN(s, &req[0], (int)req.size()))
316 return false;
317
318 unsigned char rep[4] = {0,0,0,0};
319 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05)
320 return false;
321 if (rep[1] != 0x00)
322 {
323 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n",
324 destHost.c_str(), (unsigned)destPort, (unsigned)rep[1]);
325 return false;
326 }
327 int nAddr = 0;
328 if (rep[3] == 0x01) nAddr = 4;
329 else if (rep[3] == 0x04) nAddr = 16;
330 else if (rep[3] == 0x03)
331 {
332 unsigned char len = 0;
333 if (!ReadN(s, &len, 1))
334 return false;
335 nAddr = len;
336 }
337 else
338 return false;
339
340 std::vector<unsigned char> discard(nAddr + 2);
341 return ReadN(s, &discard[0], (int)discard.size());
342}
343
344SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
345 int nSockSite, int nTimeoutSecs)
346{
347 std::string proxyHost;
348 unsigned short proxyPort = 0;
349 bool fProxy = false;
350 CRITICAL_BLOCK(cs_socks5)
351 {
352 fProxy = g_fSocks5Proxy;
353 proxyHost = g_socks5Host;
354 proxyPort = g_socks5Port;
355 }
356
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 }
365 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
366 }
367
368 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
369 if (s == INVALID_SOCKET)
370 return INVALID_SOCKET;
371 if (!Socks5Connect(s, destHost, destPort))
372 {
373 BtfCloseSocket(s);
374 return INVALID_SOCKET;
375 }
376 return s;
377}
diff --git a/src/proxy.h b/src/proxy.h
new file mode 100644
index 0000000..6429cf2
--- /dev/null
+++ b/src/proxy.h
@@ -0,0 +1,34 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3//
4// Optional SOCKS5 outbound proxy support. This is transport plumbing only:
5// no consensus, wallet, mining, or .btf identity data depends on it.
6
7#ifndef BITFLASH_PROXY_H
8#define BITFLASH_PROXY_H
9
10#ifdef _WIN32
11#ifndef WIN32_LEAN_AND_MEAN
12#define WIN32_LEAN_AND_MEAN 1
13#endif
14#include <winsock2.h>
15#else
16#include "compat.h"
17#endif
18
19#include <string>
20
21bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
22 unsigned short& portOut, std::string& errOut);
23bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut);
24bool BtfEnableTorProxy(const std::string& spec, std::string& errOut);
25void BtfClearSocks5Proxy();
26bool BtfSocks5ProxyEnabled();
27bool BtfTorProxyEnabled();
28bool BtfIsTorOnionHost(const std::string& host);
29std::string BtfSocks5ProxyName();
30
31SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
32 int nSockSite, int nTimeoutSecs);
33
34#endif
diff --git a/src/readme.txt b/src/readme.txt
index b8b89c1..794fe20 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -42,6 +42,13 @@ The resulting executable is src/bitflash.exe.
42Command-line options (in addition to the original ones) 42Command-line options (in addition to the original ones)
43------------------------------------------------------- 43-------------------------------------------------------
44 /port=N listen on P2P port N (default 8433) 44 /port=N listen on P2P port N (default 8433)
45 /socks=HOST:PORT
46 route outbound Nostr and .btf rendezvous dials through a
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
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
45 /solomine mine without requiring a connected peer (local testing) 52 /solomine mine without requiring a connected peer (local testing)
46 /operator operator mode (run pool server) 53 /operator operator mode (run pool server)
47 /participant=ADDR 54 /participant=ADDR
diff --git a/src/selftest.cpp b/src/selftest.cpp
index dcb9192..cca5335 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -7,7 +7,9 @@
7 7
8#include "headers_core.h" 8#include "headers_core.h"
9#include "bip32.h" 9#include "bip32.h"
10#include "proxy.h"
10#include "selftest.h" 11#include "selftest.h"
12#include "sockcount.h"
11#include "walletcmd.h" 13#include "walletcmd.h"
12 14
13extern int RunPoolStratumSelfTest(); 15extern int RunPoolStratumSelfTest();
@@ -1253,6 +1255,279 @@ static int RunParseMoneySelfTest()
1253 return nFail == 0 ? 0 : 1; 1255 return nFail == 0 ? 0 : 1;
1254} 1256}
1255 1257
1258static bool SelfTestReadN(SOCKET s, void* buf, int n)
1259{
1260 char* p = (char*)buf;
1261 int off = 0;
1262 while (off < n)
1263 {
1264 int r = recv(s, p + off, n - off, 0);
1265 if (r <= 0)
1266 return false;
1267 off += r;
1268 }
1269 return true;
1270}
1271
1272static bool SelfTestWriteN(SOCKET s, const void* buf, int n)
1273{
1274 const char* p = (const char*)buf;
1275 int off = 0;
1276 while (off < n)
1277 {
1278 int r = send(s, p + off, n - off, 0);
1279 if (r <= 0)
1280 return false;
1281 off += r;
1282 }
1283 return true;
1284}
1285
1286static void SelfTestSetSocketTimeout(SOCKET s, int nTimeoutSecs)
1287{
1288#ifdef _WIN32
1289 DWORD tv = (DWORD)nTimeoutSecs * 1000;
1290 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
1291 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
1292#else
1293 struct timeval tv;
1294 tv.tv_sec = nTimeoutSecs;
1295 tv.tv_usec = 0;
1296 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
1297 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
1298#endif
1299}
1300
1301#ifdef _WIN32
1302struct SelfTestWinsock
1303{
1304 bool fStarted;
1305 SelfTestWinsock() : fStarted(false)
1306 {
1307 WSADATA wsadata;
1308 fStarted = (WSAStartup(MAKEWORD(2,2), &wsadata) == 0);
1309 }
1310 ~SelfTestWinsock()
1311 {
1312 if (fStarted)
1313 WSACleanup();
1314 }
1315};
1316#endif
1317
1318static bool RunSocks5HandshakeProbe(std::string& errOut)
1319{
1320 errOut.clear();
1321#ifdef _WIN32
1322 SelfTestWinsock winsock;
1323 if (!winsock.fStarted)
1324 {
1325 errOut = "could not start Winsock";
1326 return false;
1327 }
1328#endif
1329
1330 SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
1331 if (listener == INVALID_SOCKET)
1332 {
1333 errOut = "could not create listener";
1334 return false;
1335 }
1336 SelfTestSetSocketTimeout(listener, 5);
1337
1338 struct sockaddr_in addr;
1339 memset(&addr, 0, sizeof(addr));
1340 addr.sin_family = AF_INET;
1341 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
1342 addr.sin_port = 0;
1343 if (bind(listener, (struct sockaddr*)&addr, sizeof(addr)) != 0 ||
1344 listen(listener, 1) != 0)
1345 {
1346 closesocket(listener);
1347 errOut = "could not bind/listen on loopback";
1348 return false;
1349 }
1350
1351#ifdef _WIN32
1352 int addrLen = sizeof(addr);
1353#else
1354 socklen_t addrLen = sizeof(addr);
1355#endif
1356 if (getsockname(listener, (struct sockaddr*)&addr, &addrLen) != 0)
1357 {
1358 closesocket(listener);
1359 errOut = "could not read listener port";
1360 return false;
1361 }
1362 unsigned short proxyPort = ntohs(addr.sin_port);
1363
1364 bool fServerOk = false;
1365 std::string serverErr;
1366 std::string requestedHost;
1367 unsigned short requestedPort = 0;
1368 std::thread server([&]() {
1369 SOCKET s = accept(listener, NULL, NULL);
1370 if (s == INVALID_SOCKET)
1371 {
1372 serverErr = "accept failed";
1373 return;
1374 }
1375 SelfTestSetSocketTimeout(s, 5);
1376
1377 unsigned char hello[3] = {0,0,0};
1378 if (!SelfTestReadN(s, hello, sizeof(hello)) ||
1379 hello[0] != 0x05 || hello[1] != 0x01 || hello[2] != 0x00)
1380 {
1381 serverErr = "bad SOCKS5 greeting";
1382 closesocket(s);
1383 return;
1384 }
1385 unsigned char choice[2] = {0x05, 0x00};
1386 if (!SelfTestWriteN(s, choice, sizeof(choice)))
1387 {
1388 serverErr = "could not write method choice";
1389 closesocket(s);
1390 return;
1391 }
1392
1393 unsigned char hdr[5] = {0,0,0,0,0};
1394 if (!SelfTestReadN(s, hdr, sizeof(hdr)) ||
1395 hdr[0] != 0x05 || hdr[1] != 0x01 || hdr[2] != 0x00 || hdr[3] != 0x03)
1396 {
1397 serverErr = "CONNECT did not use domain-name address type";
1398 closesocket(s);
1399 return;
1400 }
1401
1402 unsigned char len = hdr[4];
1403 std::vector<unsigned char> rest((size_t)len + 2);
1404 if (!SelfTestReadN(s, &rest[0], (int)rest.size()))
1405 {
1406 serverErr = "could not read CONNECT target";
1407 closesocket(s);
1408 return;
1409 }
1410 requestedHost.assign((const char*)&rest[0], (size_t)len);
1411 requestedPort = ((unsigned short)rest[len] << 8) | rest[len + 1];
1412
1413 unsigned char reply[10] = {0x05, 0x00, 0x00, 0x01, 0,0,0,0, 0,0};
1414 if (!SelfTestWriteN(s, reply, sizeof(reply)))
1415 {
1416 serverErr = "could not write CONNECT reply";
1417 closesocket(s);
1418 return;
1419 }
1420 fServerOk = true;
1421 closesocket(s);
1422 });
1423
1424 std::string err;
1425 BtfClearSocks5Proxy();
1426 bool fClientOk = BtfSetSocks5Proxy(strprintf("127.0.0.1:%u", (unsigned)proxyPort), err);
1427 SOCKET client = INVALID_SOCKET;
1428 if (fClientOk)
1429 client = BtfConnectSocket("relay.example", 443, SOCK_NOSTR, 5);
1430 if (client != INVALID_SOCKET)
1431 BtfCloseSocket(client);
1432 BtfClearSocks5Proxy();
1433 closesocket(listener);
1434 server.join();
1435
1436 if (!fClientOk)
1437 errOut = strprintf("could not enable proxy: %s", err.c_str());
1438 else if (client == INVALID_SOCKET)
1439 errOut = "client SOCKS5 connect failed";
1440 else if (!fServerOk)
1441 errOut = serverErr.empty() ? "mock server failed" : serverErr;
1442 else if (requestedHost != "relay.example" || requestedPort != 443)
1443 errOut = strprintf("unexpected CONNECT target %s:%u", requestedHost.c_str(), (unsigned)requestedPort);
1444 else
1445 return true;
1446 return false;
1447}
1448
1449static int RunSocks5ProxySelfTest()
1450{
1451 printf("socks5-proxy self-test\n");
1452 int nFail = 0;
1453 std::string host, err;
1454 unsigned short port = 0;
1455
1456 nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) &&
1457 host == "127.0.0.1" && port == 9050,
1458 "parses local Tor proxy endpoint") ? 0 : 1;
1459 nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) &&
1460 host == "localhost" && port == 9050,
1461 "parses hostname proxy endpoint") ? 0 : 1;
1462 nFail += Check(BtfParseSocks5Proxy("[::1]:9050", host, port, err) &&
1463 host == "::1" && port == 9050,
1464 "parses bracketed IPv6 proxy endpoint") ? 0 : 1;
1465 nFail += Check(!BtfParseSocks5Proxy("::1:9050", host, port, err),
1466 "rejects unbracketed IPv6 proxy endpoint") ? 0 : 1;
1467 nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err),
1468 "rejects missing port") ? 0 : 1;
1469 nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err),
1470 "rejects zero port") ? 0 : 1;
1471 nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err),
1472 "rejects out-of-range port") ? 0 : 1;
1473 nFail += Check(!BtfParseSocks5Proxy("localhost:999999999999999999999999999999", host, port, err),
1474 "rejects overflowing port") ? 0 : 1;
1475 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err),
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;
1482
1483 BtfClearSocks5Proxy();
1484 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
1485 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) &&
1486 BtfSocks5ProxyEnabled() &&
1487 BtfSocks5ProxyName() == "127.0.0.1:9050",
1488 "enables parsed SOCKS5 proxy") ? 0 : 1;
1489 BtfClearSocks5Proxy();
1490 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1;
1491
1492 nFail += Check(BtfSetSocks5Proxy("[::1]:9050", err) &&
1493 BtfSocks5ProxyEnabled() &&
1494 BtfSocks5ProxyName() == "[::1]:9050",
1495 "enables bracketed IPv6 proxy endpoint") ? 0 : 1;
1496 BtfClearSocks5Proxy();
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
1518 std::string handshakeErr;
1519 bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr);
1520 nFail += Check(fHandshakeOk, handshakeErr.empty() ?
1521 "completes SOCKS5 domain-name CONNECT handshake" :
1522 strprintf("completes SOCKS5 domain-name CONNECT handshake (%s)",
1523 handshakeErr.c_str()).c_str()) ? 0 : 1;
1524
1525 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1526 nFail, nFail == 1 ? "" : "s");
1527 fflush(stdout);
1528 return nFail == 0 ? 0 : 1;
1529}
1530
1256int RunSelfTest(const std::string& name) 1531int RunSelfTest(const std::string& name)
1257{ 1532{
1258 AttachTerminal(); 1533 AttachTerminal();
@@ -1275,8 +1550,10 @@ int RunSelfTest(const std::string& name)
1275 return RunPoolStratumSelfTest(); 1550 return RunPoolStratumSelfTest();
1276 if (name == "parse-money") 1551 if (name == "parse-money")
1277 return RunParseMoneySelfTest(); 1552 return RunParseMoneySelfTest();
1553 if (name == "socks5-proxy")
1554 return RunSocks5ProxySelfTest();
1278 1555
1279 printf("Unknown self-test '%s'\n", name.c_str()); 1556 printf("Unknown self-test '%s'\n", name.c_str());
1280 printf("Known self-tests: wallet-keypool, wallet-hd, wallet-format, wallet-crypto, wallet-encrypt, net-message, consensus-limits, pool-stratum\n"); 1557 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");
1281 return 1; 1558 return 1;
1282} 1559}