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.cpp16
-rw-r--r--src/makefile.mingw7
-rw-r--r--src/net.cpp4
-rw-r--r--src/nostr.cpp50
-rw-r--r--src/proxy.cpp254
-rw-r--r--src/proxy.h31
-rw-r--r--src/readme.txt3
-rw-r--r--src/selftest.cpp42
10 files changed, 368 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..1e62ff9
--- /dev/null
+++ b/src/proxy.cpp
@@ -0,0 +1,254 @@
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
8static CCriticalSection cs_socks5;
9static bool g_fSocks5Proxy = false;
10static std::string g_socks5Host;
11static unsigned short g_socks5Port = 0;
12
13static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
14{
15 if (nTimeoutSecs <= 0)
16 return;
17#ifdef _WIN32
18 DWORD tv = (DWORD)nTimeoutSecs * 1000;
19 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
20 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
21#else
22 struct timeval tv;
23 tv.tv_sec = nTimeoutSecs;
24 tv.tv_usec = 0;
25 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
26 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
27#endif
28}
29
30static bool ReadN(SOCKET s, void* buf, int n)
31{
32 char* p = (char*)buf;
33 int off = 0;
34 while (off < n)
35 {
36 int r = recv(s, p + off, n - off, 0);
37 if (r <= 0)
38 return false;
39 off += r;
40 }
41 return true;
42}
43
44static bool WriteN(SOCKET s, const void* buf, int n)
45{
46 const char* p = (const char*)buf;
47 int off = 0;
48 while (off < n)
49 {
50 int r = send(s, p + off, n - off, 0);
51 if (r <= 0)
52 return false;
53 off += r;
54 }
55 return true;
56}
57
58static bool ValidProxyHostChar(char c)
59{
60 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_';
61}
62
63bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
64 unsigned short& portOut, std::string& errOut)
65{
66 hostOut.clear();
67 portOut = 0;
68 errOut.clear();
69
70 size_t colon = spec.rfind(':');
71 if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size())
72 {
73 errOut = "expected HOST:PORT";
74 return false;
75 }
76
77 std::string host = spec.substr(0, colon);
78 std::string port = spec.substr(colon + 1);
79 if (host.size() > 255)
80 {
81 errOut = "host is too long";
82 return false;
83 }
84 for (size_t i = 0; i < host.size(); i++)
85 {
86 if (!ValidProxyHostChar(host[i]))
87 {
88 errOut = "host contains unsupported characters";
89 return false;
90 }
91 }
92 for (size_t i = 0; i < port.size(); i++)
93 {
94 if (!isdigit((unsigned char)port[i]))
95 {
96 errOut = "port is not numeric";
97 return false;
98 }
99 }
100 int nPort = atoi(port.c_str());
101 if (nPort <= 0 || nPort > 65535)
102 {
103 errOut = "port is out of range";
104 return false;
105 }
106
107 hostOut = host;
108 portOut = (unsigned short)nPort;
109 return true;
110}
111
112bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut)
113{
114 std::string host;
115 unsigned short port = 0;
116 if (!BtfParseSocks5Proxy(spec, host, port, errOut))
117 return false;
118 CRITICAL_BLOCK(cs_socks5)
119 {
120 g_socks5Host = host;
121 g_socks5Port = port;
122 g_fSocks5Proxy = true;
123 }
124 return true;
125}
126
127void BtfClearSocks5Proxy()
128{
129 CRITICAL_BLOCK(cs_socks5)
130 {
131 g_fSocks5Proxy = false;
132 g_socks5Host.clear();
133 g_socks5Port = 0;
134 }
135}
136
137bool BtfSocks5ProxyEnabled()
138{
139 CRITICAL_BLOCK(cs_socks5)
140 return g_fSocks5Proxy;
141 return false;
142}
143
144std::string BtfSocks5ProxyName()
145{
146 CRITICAL_BLOCK(cs_socks5)
147 {
148 if (!g_fSocks5Proxy)
149 return "";
150 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
151 }
152 return "";
153}
154
155static SOCKET ConnectDirect(const std::string& host, unsigned short port,
156 int nSockSite, int nTimeoutSecs)
157{
158 struct addrinfo hints, *res = NULL, *rp = NULL;
159 memset(&hints, 0, sizeof(hints));
160 hints.ai_family = AF_UNSPEC;
161 hints.ai_socktype = SOCK_STREAM;
162 char portstr[16];
163 sprintf(portstr, "%u", (unsigned)port);
164 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
165 return INVALID_SOCKET;
166
167 SOCKET s = INVALID_SOCKET;
168 for (rp = res; rp != NULL; rp = rp->ai_next)
169 {
170 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), nSockSite);
171 if (s == INVALID_SOCKET)
172 continue;
173 SetSocketTimeout(s, nTimeoutSecs);
174 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) == 0)
175 break;
176 BtfCloseSocket(s);
177 s = INVALID_SOCKET;
178 }
179 freeaddrinfo(res);
180 return s;
181}
182
183static bool Socks5Connect(SOCKET s, const std::string& destHost,
184 unsigned short destPort)
185{
186 if (destHost.empty() || destHost.size() > 255)
187 return false;
188
189 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
190 if (!WriteN(s, hello, sizeof(hello)))
191 return false;
192 unsigned char choice[2] = { 0, 0 };
193 if (!ReadN(s, choice, sizeof(choice)) || choice[0] != 0x05 || choice[1] != 0x00)
194 return false;
195
196 std::vector<unsigned char> req;
197 req.reserve(7 + destHost.size());
198 req.push_back(0x05); // version
199 req.push_back(0x01); // CONNECT
200 req.push_back(0x00); // reserved
201 req.push_back(0x03); // domain name; proxy resolves, avoiding local DNS leak
202 req.push_back((unsigned char)destHost.size());
203 req.insert(req.end(), destHost.begin(), destHost.end());
204 req.push_back((unsigned char)(destPort >> 8));
205 req.push_back((unsigned char)(destPort & 0xff));
206 if (!WriteN(s, &req[0], (int)req.size()))
207 return false;
208
209 unsigned char rep[4] = {0,0,0,0};
210 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05 || rep[1] != 0x00)
211 return false;
212 int nAddr = 0;
213 if (rep[3] == 0x01) nAddr = 4;
214 else if (rep[3] == 0x04) nAddr = 16;
215 else if (rep[3] == 0x03)
216 {
217 unsigned char len = 0;
218 if (!ReadN(s, &len, 1))
219 return false;
220 nAddr = len;
221 }
222 else
223 return false;
224
225 std::vector<unsigned char> discard(nAddr + 2);
226 return ReadN(s, &discard[0], (int)discard.size());
227}
228
229SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
230 int nSockSite, int nTimeoutSecs)
231{
232 std::string proxyHost;
233 unsigned short proxyPort = 0;
234 bool fProxy = false;
235 CRITICAL_BLOCK(cs_socks5)
236 {
237 fProxy = g_fSocks5Proxy;
238 proxyHost = g_socks5Host;
239 proxyPort = g_socks5Port;
240 }
241
242 if (!fProxy)
243 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
244
245 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
246 if (s == INVALID_SOCKET)
247 return INVALID_SOCKET;
248 if (!Socks5Connect(s, destHost, destPort))
249 {
250 BtfCloseSocket(s);
251 return INVALID_SOCKET;
252 }
253 return s;
254}
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..96f32c3 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -42,6 +42,9 @@ 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
45 /solomine mine without requiring a connected peer (local testing) 48 /solomine mine without requiring a connected peer (local testing)
46 /operator operator mode (run pool server) 49 /operator operator mode (run pool server)
47 /participant=ADDR 50 /participant=ADDR
diff --git a/src/selftest.cpp b/src/selftest.cpp
index dcb9192..5b36b9d 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -7,6 +7,7 @@
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"
11#include "walletcmd.h" 12#include "walletcmd.h"
12 13
@@ -1253,6 +1254,43 @@ static int RunParseMoneySelfTest()
1253 return nFail == 0 ? 0 : 1; 1254 return nFail == 0 ? 0 : 1;
1254} 1255}
1255 1256
1257static int RunSocks5ProxySelfTest()
1258{
1259 printf("socks5-proxy self-test\n");
1260 int nFail = 0;
1261 std::string host, err;
1262 unsigned short port = 0;
1263
1264 nFail += Check(BtfParseSocks5Proxy("127.0.0.1:9050", host, port, err) &&
1265 host == "127.0.0.1" && port == 9050,
1266 "parses local Tor proxy endpoint") ? 0 : 1;
1267 nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) &&
1268 host == "localhost" && port == 9050,
1269 "parses hostname proxy endpoint") ? 0 : 1;
1270 nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err),
1271 "rejects missing port") ? 0 : 1;
1272 nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err),
1273 "rejects zero port") ? 0 : 1;
1274 nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err),
1275 "rejects out-of-range port") ? 0 : 1;
1276 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err),
1277 "rejects unsupported authenticated proxy syntax") ? 0 : 1;
1278
1279 BtfClearSocks5Proxy();
1280 nFail += Check(!BtfSocks5ProxyEnabled(), "starts disabled after clear") ? 0 : 1;
1281 nFail += Check(BtfSetSocks5Proxy("127.0.0.1:9050", err) &&
1282 BtfSocks5ProxyEnabled() &&
1283 BtfSocks5ProxyName() == "127.0.0.1:9050",
1284 "enables parsed SOCKS5 proxy") ? 0 : 1;
1285 BtfClearSocks5Proxy();
1286 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1;
1287
1288 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1289 nFail, nFail == 1 ? "" : "s");
1290 fflush(stdout);
1291 return nFail == 0 ? 0 : 1;
1292}
1293
1256int RunSelfTest(const std::string& name) 1294int RunSelfTest(const std::string& name)
1257{ 1295{
1258 AttachTerminal(); 1296 AttachTerminal();
@@ -1275,8 +1313,10 @@ int RunSelfTest(const std::string& name)
1275 return RunPoolStratumSelfTest(); 1313 return RunPoolStratumSelfTest();
1276 if (name == "parse-money") 1314 if (name == "parse-money")
1277 return RunParseMoneySelfTest(); 1315 return RunParseMoneySelfTest();
1316 if (name == "socks5-proxy")
1317 return RunSocks5ProxySelfTest();
1278 1318
1279 printf("Unknown self-test '%s'\n", name.c_str()); 1319 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"); 1320 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; 1321 return 1;
1282} 1322}