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-12 10:18:14 -0300
committerBitflash <admin@bitflash.network>2026-08-12 10:18:14 -0300
commitc330b8e5e1c057049b5b361908c94f41344dff80 (patch)
tree44e70a657e4fa70205d9d318839ac68f175a1fb9 /src
parent026ec904b7bde4898fbb889b7758dbcb13bf860a (diff)
parent066379ebcb1dac720221fe1a1841fa6f59df21d9 (diff)
Merge pull request 'net: add optional SOCKS5 outbound proxy' (#27) from socks5-proxy into main
Diffstat (limited to 'src')
-rw-r--r--src/Makefile7
-rw-r--r--src/btfrv.cpp9
-rw-r--r--src/main_gui.cpp16
-rw-r--r--src/makefile.mingw7
-rw-r--r--src/net.cpp4
-rw-r--r--src/nostr.cpp50
-rw-r--r--src/proxy.cpp329
-rw-r--r--src/proxy.h31
-rw-r--r--src/readme.txt4
-rw-r--r--src/selftest.cpp254
10 files changed, 656 insertions, 55 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..90a6050 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,7 @@ 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");
138 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n"); 141 printf(" /btfseed=ADDRESS:ENCHEX (extra bootstrap peer, repeatable)\n");
139 printf("\n"); 142 printf("\n");
140 printf("Wallet:\n"); 143 printf("Wallet:\n");
@@ -282,6 +285,17 @@ static void ParseStartupArguments(int argc, char* argv[])
282 if (!announceRelay.empty()) 285 if (!announceRelay.empty())
283 strBtfAnnounceRelay = announceRelay; 286 strBtfAnnounceRelay = announceRelay;
284 287
288 string socksProxy = argval2(argc, argv, "/socks", "-socks");
289 if (!socksProxy.empty())
290 {
291 string err;
292 if (!BtfSetSocks5Proxy(socksProxy, err))
293 fprintf(stderr, "Ignoring /socks=%s: %s\n", socksProxy.c_str(), err.c_str());
294 else
295 fprintf(stderr, "SOCKS5 proxy enabled for outbound discovery/relay dials: %s\n",
296 BtfSocks5ProxyName().c_str());
297 }
298
285 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for 299 // /btfseed=ADDRESS:ENCHEX -- extra bootstrap peers, repeatable. Useful for
286 // testing the seed path and for private networks that ship no compiled list. 300 // testing the seed path and for private networks that ship no compiled list.
287 for (int i = 1; i < argc; i++) 301 for (int i = 1; i < argc; i++)
diff --git a/src/makefile.mingw b/src/makefile.mingw
index b946e50..28e925c 100644
--- a/src/makefile.mingw
+++ b/src/makefile.mingw
@@ -21,7 +21,7 @@ WARNFLAGS = -Wformat -Werror=format \
21# 64, which capped this node at 63 watchable peers no matter how many it had. 21# 64, which capped this node at 63 watchable peers no matter how many it had.
22CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \ 22CXXFLAGS = -std=gnu++14 -O2 $(WARNFLAGS) -fpermissive -pthread \
23 -DWIN32 -DNOPCH -DFD_SETSIZE=1024 \ 23 -DWIN32 -DNOPCH -DFD_SETSIZE=1024 \
24 -DSECP256K1_STATIC \ 24 -DSECP256K1_STATIC -DBITFLASH_NODE_BUILD \
25 -I$(DEPS)/include -I$(RANDOMX)/src \ 25 -I$(DEPS)/include -I$(RANDOMX)/src \
26 -I$(DEPS)/include/nlohmann \ 26 -I$(DEPS)/include/nlohmann \
27 -IC:/msys64/ucrt64/include \ 27 -IC:/msys64/ucrt64/include \
@@ -42,7 +42,7 @@ IMGUI_OBJS = obj/imgui.o obj/imgui_draw.o obj/imgui_tables.o \
42NODE_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 \
43 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 \
44 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 \
45 obj/selftest.o obj/bip32.o obj/walletcmd.o 45 obj/selftest.o obj/bip32.o obj/walletcmd.o obj/proxy.o
46 46
47GUI_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
48 48
@@ -56,7 +56,7 @@ GUI_OBJS = obj/gui.o obj/main_gui.o obj/bitflash_res.o
56HEADERS = 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 \
57 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 \
58 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 \
59 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
60 60
61all: bitflash.exe 61all: bitflash.exe
62 62
@@ -117,3 +117,4 @@ tests: test_bip32.exe bitflash.exe
117 ./bitflash.exe -selftest=net-message 117 ./bitflash.exe -selftest=net-message
118 ./bitflash.exe -selftest=consensus-limits 118 ./bitflash.exe -selftest=consensus-limits
119 ./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 b9bb95d..fe9964e 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -17,6 +17,7 @@
17#include <openssl/rand.h> 17#include <openssl/rand.h>
18#include "btfaddr.h" 18#include "btfaddr.h"
19#include "btftunnel.h" 19#include "btftunnel.h"
20#include "proxy.h"
20 21
21void ThreadReconnectCachedBtfPeers(void* parg); 22void ThreadReconnectCachedBtfPeers(void* parg);
22void ThreadMessageHandler2(void* parg); 23void ThreadMessageHandler2(void* parg);
@@ -361,6 +362,9 @@ string GetDiagnosticsText()
361 362
362bool GetMyExternalIP(unsigned int& ipRet) 363bool GetMyExternalIP(unsigned int& ipRet)
363{ 364{
365 if (BtfSocks5ProxyEnabled())
366 return error("GetMyExternalIP() skipped while SOCKS5 proxy is enabled\n");
367
364 // Try several plain-text IP echo services in order. 368 // Try several plain-text IP echo services in order.
365 // Each returns just the IPv4 address as the first line of the HTTP body. 369 // Each returns just the IPv4 address as the first line of the HTTP body.
366 struct { const char* host; const char* path; } services[] = { 370 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..d7dc324
--- /dev/null
+++ b/src/proxy.cpp
@@ -0,0 +1,329 @@
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 std::string g_socks5Host;
13static unsigned short g_socks5Port = 0;
14
15static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
16{
17 if (nTimeoutSecs <= 0)
18 return;
19#ifdef _WIN32
20 DWORD tv = (DWORD)nTimeoutSecs * 1000;
21 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
22 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
23#else
24 struct timeval tv;
25 tv.tv_sec = nTimeoutSecs;
26 tv.tv_usec = 0;
27 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
28 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
29#endif
30}
31
32static bool ReadN(SOCKET s, void* buf, int n)
33{
34 char* p = (char*)buf;
35 int off = 0;
36 while (off < n)
37 {
38 int r = recv(s, p + off, n - off, 0);
39 if (r <= 0)
40 return false;
41 off += r;
42 }
43 return true;
44}
45
46static bool WriteN(SOCKET s, const void* buf, int n)
47{
48 const char* p = (const char*)buf;
49 int off = 0;
50 while (off < n)
51 {
52 int r = send(s, p + off, n - off, 0);
53 if (r <= 0)
54 return false;
55 off += r;
56 }
57 return true;
58}
59
60static bool ValidProxyHostChar(char c)
61{
62 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_';
63}
64
65static bool ValidProxyIpv6HostChar(char c)
66{
67 return isxdigit((unsigned char)c) || c == ':' || c == '.';
68}
69
70static bool ParsePort(const std::string& port, unsigned short& portOut,
71 std::string& errOut)
72{
73 if (port.empty())
74 {
75 errOut = "port is empty";
76 return false;
77 }
78 for (size_t i = 0; i < port.size(); i++)
79 {
80 if (!isdigit((unsigned char)port[i]))
81 {
82 errOut = "port is not numeric";
83 return false;
84 }
85 }
86
87 errno = 0;
88 char* end = NULL;
89 long nPort = strtol(port.c_str(), &end, 10);
90 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
91 {
92 errOut = "port is out of range";
93 return false;
94 }
95 portOut = (unsigned short)nPort;
96 return true;
97}
98
99bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
100 unsigned short& portOut, std::string& errOut)
101{
102 hostOut.clear();
103 portOut = 0;
104 errOut.clear();
105
106 std::string host;
107 std::string port;
108 bool fBracketedIpv6 = false;
109 if (!spec.empty() && spec[0] == '[')
110 {
111 size_t close = spec.find(']');
112 if (close == std::string::npos || close == 1 || close + 1 >= spec.size() || spec[close + 1] != ':')
113 {
114 errOut = "expected [IPv6]:PORT";
115 return false;
116 }
117 host = spec.substr(1, close - 1);
118 port = spec.substr(close + 2);
119 fBracketedIpv6 = true;
120 }
121 else
122 {
123 size_t colon = spec.rfind(':');
124 if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size())
125 {
126 errOut = "expected HOST:PORT";
127 return false;
128 }
129 host = spec.substr(0, colon);
130 port = spec.substr(colon + 1);
131 if (host.find(':') != std::string::npos)
132 {
133 errOut = "IPv6 proxy hosts must use [ADDR]:PORT";
134 return false;
135 }
136 }
137
138 if (host.size() > 255)
139 {
140 errOut = "host is too long";
141 return false;
142 }
143 if (fBracketedIpv6)
144 {
145 bool fHasColon = false;
146 for (size_t i = 0; i < host.size(); i++)
147 {
148 fHasColon = fHasColon || host[i] == ':';
149 if (!ValidProxyIpv6HostChar(host[i]))
150 {
151 errOut = "IPv6 proxy host contains unsupported characters";
152 return false;
153 }
154 }
155 if (!fHasColon)
156 {
157 errOut = "bracketed proxy host is not IPv6";
158 return false;
159 }
160 }
161 else
162 {
163 for (size_t i = 0; i < host.size(); i++)
164 {
165 if (!ValidProxyHostChar(host[i]))
166 {
167 errOut = "host contains unsupported characters";
168 return false;
169 }
170 }
171 }
172 if (!ParsePort(port, portOut, errOut))
173 return false;
174
175 hostOut = host;
176 return true;
177}
178
179bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut)
180{
181 std::string host;
182 unsigned short port = 0;
183 if (!BtfParseSocks5Proxy(spec, host, port, errOut))
184 return false;
185 CRITICAL_BLOCK(cs_socks5)
186 {
187 g_socks5Host = host;
188 g_socks5Port = port;
189 g_fSocks5Proxy = true;
190 }
191 return true;
192}
193
194void BtfClearSocks5Proxy()
195{
196 CRITICAL_BLOCK(cs_socks5)
197 {
198 g_fSocks5Proxy = false;
199 g_socks5Host.clear();
200 g_socks5Port = 0;
201 }
202}
203
204bool BtfSocks5ProxyEnabled()
205{
206 CRITICAL_BLOCK(cs_socks5)
207 return g_fSocks5Proxy;
208 return false;
209}
210
211std::string BtfSocks5ProxyName()
212{
213 CRITICAL_BLOCK(cs_socks5)
214 {
215 if (!g_fSocks5Proxy)
216 return "";
217 if (g_socks5Host.find(':') != std::string::npos)
218 return strprintf("[%s]:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
219 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
220 }
221 return "";
222}
223
224static SOCKET ConnectDirect(const std::string& host, unsigned short port,
225 int nSockSite, int nTimeoutSecs)
226{
227 struct addrinfo hints, *res = NULL, *rp = NULL;
228 memset(&hints, 0, sizeof(hints));
229 hints.ai_family = AF_UNSPEC;
230 hints.ai_socktype = SOCK_STREAM;
231 char portstr[16];
232 sprintf(portstr, "%u", (unsigned)port);
233 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
234 return INVALID_SOCKET;
235
236 SOCKET s = INVALID_SOCKET;
237 for (rp = res; rp != NULL; rp = rp->ai_next)
238 {
239 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), nSockSite);
240 if (s == INVALID_SOCKET)
241 continue;
242 SetSocketTimeout(s, nTimeoutSecs);
243 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) == 0)
244 break;
245 BtfCloseSocket(s);
246 s = INVALID_SOCKET;
247 }
248 freeaddrinfo(res);
249 return s;
250}
251
252static bool Socks5Connect(SOCKET s, const std::string& destHost,
253 unsigned short destPort)
254{
255 if (destHost.empty() || destHost.size() > 255)
256 return false;
257
258 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
259 if (!WriteN(s, hello, sizeof(hello)))
260 return false;
261 unsigned char choice[2] = { 0, 0 };
262 if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00)
263 return false;
264
265 std::vector<unsigned char> req;
266 req.reserve(7 + destHost.size());
267 req.push_back(0x05); // version
268 req.push_back(0x01); // CONNECT
269 req.push_back(0x00); // reserved
270 req.push_back(0x03); // domain name; proxy resolves, avoiding local DNS leak
271 req.push_back((unsigned char)destHost.size());
272 req.insert(req.end(), destHost.begin(), destHost.end());
273 req.push_back((unsigned char)(destPort >> 8));
274 req.push_back((unsigned char)(destPort & 0xff));
275 if (!WriteN(s, &req[0], (int)req.size()))
276 return false;
277
278 unsigned char rep[4] = {0,0,0,0};
279 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05)
280 return false;
281 if (rep[1] != 0x00)
282 {
283 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n",
284 destHost.c_str(), (unsigned)destPort, (unsigned)rep[1]);
285 return false;
286 }
287 int nAddr = 0;
288 if (rep[3] == 0x01) nAddr = 4;
289 else if (rep[3] == 0x04) nAddr = 16;
290 else if (rep[3] == 0x03)
291 {
292 unsigned char len = 0;
293 if (!ReadN(s, &len, 1))
294 return false;
295 nAddr = len;
296 }
297 else
298 return false;
299
300 std::vector<unsigned char> discard(nAddr + 2);
301 return ReadN(s, &discard[0], (int)discard.size());
302}
303
304SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
305 int nSockSite, int nTimeoutSecs)
306{
307 std::string proxyHost;
308 unsigned short proxyPort = 0;
309 bool fProxy = false;
310 CRITICAL_BLOCK(cs_socks5)
311 {
312 fProxy = g_fSocks5Proxy;
313 proxyHost = g_socks5Host;
314 proxyPort = g_socks5Port;
315 }
316
317 if (!fProxy)
318 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
319
320 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
321 if (s == INVALID_SOCKET)
322 return INVALID_SOCKET;
323 if (!Socks5Connect(s, destHost, destPort))
324 {
325 BtfCloseSocket(s);
326 return INVALID_SOCKET;
327 }
328 return s;
329}
diff --git a/src/proxy.h b/src/proxy.h
new file mode 100644
index 0000000..b5e69c8
--- /dev/null
+++ b/src/proxy.h
@@ -0,0 +1,31 @@
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);
24void BtfClearSocks5Proxy();
25bool BtfSocks5ProxyEnabled();
26std::string BtfSocks5ProxyName();
27
28SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
29 int nSockSite, int nTimeoutSecs);
30
31#endif
diff --git a/src/readme.txt b/src/readme.txt
index b8b89c1..1fa77df 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -42,6 +42,10 @@ 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
45 /solomine mine without requiring a connected peer (local testing) 49 /solomine mine without requiring a connected peer (local testing)
46 /operator operator mode (run pool server) 50 /operator operator mode (run pool server)
47 /participant=ADDR 51 /participant=ADDR
diff --git a/src/selftest.cpp b/src/selftest.cpp
index dcb9192..aa4e528 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,254 @@ 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
1478 BtfClearSocks5Proxy();
1479 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
1480 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) &&
1481 BtfSocks5ProxyEnabled() &&
1482 BtfSocks5ProxyName() == "127.0.0.1:9050",
1483 "enables parsed SOCKS5 proxy") ? 0 : 1;
1484 BtfClearSocks5Proxy();
1485 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1;
1486
1487 nFail += Check(BtfSetSocks5Proxy("[::1]:9050", err) &&
1488 BtfSocks5ProxyEnabled() &&
1489 BtfSocks5ProxyName() == "[::1]:9050",
1490 "enables bracketed IPv6 proxy endpoint") ? 0 : 1;
1491 BtfClearSocks5Proxy();
1492
1493 std::string handshakeErr;
1494 bool fHandshakeOk = RunSocks5HandshakeProbe(handshakeErr);
1495 nFail += Check(fHandshakeOk, handshakeErr.empty() ?
1496 "completes SOCKS5 domain-name CONNECT handshake" :
1497 strprintf("completes SOCKS5 domain-name CONNECT handshake (%s)",
1498 handshakeErr.c_str()).c_str()) ? 0 : 1;
1499
1500 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1501 nFail, nFail == 1 ? "" : "s");
1502 fflush(stdout);
1503 return nFail == 0 ? 0 : 1;
1504}
1505
1256int RunSelfTest(const std::string& name) 1506int RunSelfTest(const std::string& name)
1257{ 1507{
1258 AttachTerminal(); 1508 AttachTerminal();
@@ -1275,8 +1525,10 @@ int RunSelfTest(const std::string& name)
1275 return RunPoolStratumSelfTest(); 1525 return RunPoolStratumSelfTest();
1276 if (name == "parse-money") 1526 if (name == "parse-money")
1277 return RunParseMoneySelfTest(); 1527 return RunParseMoneySelfTest();
1528 if (name == "socks5-proxy")
1529 return RunSocks5ProxySelfTest();
1278 1530
1279 printf("Unknown self-test '%s'\n", name.c_str()); 1531 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"); 1532 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; 1533 return 1;
1282} 1534}