upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/proxy.cpp
diff options
context:
space:
mode:
authorbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-05 22:46:52 +0000
commitceae3d076b5ad11ee84729ae43f14e9312896646 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/proxy.cpp
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parent66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff)
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git merge-base returns no result at all -- so anyone cloning this repository landed on a default branch that was neither current nor related to the running node. The 28 commits on main were audited before this merge: every one is already present in the tree by content, including the two that are hardest to see because they were rewritten afterwards -- the payout reorg-safety work (PayoutBlockStillOnChain, since evolved to re-check on every retry rather than once at maturity) and the nostr relay certificate verification (SSL_set1_host, now with a Windows root-store path). Nothing on main is lost by this merge. Recorded as a merge rather than a force-push: main only ever moves forward, its old history stays reachable as the first parent, and this is revertible. The tree is taken wholesale from the merged branch, so main's content is now exactly what was built and tested. What this brings onto main: - compactblock: BIP152-style compact block relay ported from upstream 1.2.23, primitives and unit test only, with the RAND_bytes failure path closed and the reconstruction allocation bounded. - upstream/main 17741d9: isolated testnet parameters and genesis, optional 2 MB large pages for RandomX. Mainnet genesis verified byte-identical through upstream's BuildGenesisBlock() refactor. - deploy: every image built on bookworm so binaries run on Debian 12, and the container build now honours the RandomX and libsecp256k1 pins it had been silently ignoring since before 1.2.15. Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus constants unchanged against e4f898c apart from the pre-existing MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'src/proxy.cpp')
-rw-r--r--src/proxy.cpp422
1 files changed, 422 insertions, 0 deletions
diff --git a/src/proxy.cpp b/src/proxy.cpp
new file mode 100644
index 0000000..eed054c
--- /dev/null
+++ b/src/proxy.cpp
@@ -0,0 +1,422 @@
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";
16static const int TOR_CONNECT_TIMEOUT_SECS = 120;
17
18static void SetSocketTimeout(SOCKET s, int nTimeoutSecs)
19{
20 if (nTimeoutSecs <= 0)
21 return;
22#ifdef _WIN32
23 DWORD tv = (DWORD)nTimeoutSecs * 1000;
24 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
25 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
26#else
27 struct timeval tv;
28 tv.tv_sec = nTimeoutSecs;
29 tv.tv_usec = 0;
30 setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
31 setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
32#endif
33}
34
35static bool ReadN(SOCKET s, void* buf, int n)
36{
37 char* p = (char*)buf;
38 int off = 0;
39 while (off < n)
40 {
41 int r = recv(s, p + off, n - off, 0);
42 if (r <= 0)
43 return false;
44 off += r;
45 }
46 return true;
47}
48
49static bool WriteN(SOCKET s, const void* buf, int n)
50{
51 const char* p = (const char*)buf;
52 int off = 0;
53 while (off < n)
54 {
55 int r = send(s, p + off, n - off, 0);
56 if (r <= 0)
57 return false;
58 off += r;
59 }
60 return true;
61}
62
63static bool ValidProxyHostChar(char c)
64{
65 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_';
66}
67
68static bool ValidProxyIpv6HostChar(char c)
69{
70 return isxdigit((unsigned char)c) || c == ':' || c == '.';
71}
72
73static std::string LowerProxyHost(std::string host)
74{
75 if (!host.empty() && host[host.size() - 1] == '.')
76 host.erase(host.size() - 1);
77 for (size_t i = 0; i < host.size(); i++)
78 host[i] = (char)tolower((unsigned char)host[i]);
79 return host;
80}
81
82bool BtfIsTorOnionHost(const std::string& host)
83{
84 std::string lower = LowerProxyHost(host);
85 static const char* suffix = ".onion";
86 size_t suffixLen = strlen(suffix);
87 return lower.size() > suffixLen &&
88 lower.compare(lower.size() - suffixLen, suffixLen, suffix) == 0;
89}
90
91static bool ParsePort(const std::string& port, unsigned short& portOut,
92 std::string& errOut)
93{
94 if (port.empty())
95 {
96 errOut = "port is empty";
97 return false;
98 }
99 for (size_t i = 0; i < port.size(); i++)
100 {
101 if (!isdigit((unsigned char)port[i]))
102 {
103 errOut = "port is not numeric";
104 return false;
105 }
106 }
107
108 errno = 0;
109 char* end = NULL;
110 long nPort = strtol(port.c_str(), &end, 10);
111 if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535)
112 {
113 errOut = "port is out of range";
114 return false;
115 }
116 portOut = (unsigned short)nPort;
117 return true;
118}
119
120bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
121 unsigned short& portOut, std::string& errOut)
122{
123 hostOut.clear();
124 portOut = 0;
125 errOut.clear();
126
127 std::string host;
128 std::string port;
129 bool fBracketedIpv6 = false;
130 if (!spec.empty() && spec[0] == '[')
131 {
132 size_t close = spec.find(']');
133 if (close == std::string::npos || close == 1 || close + 1 >= spec.size() || spec[close + 1] != ':')
134 {
135 errOut = "expected [IPv6]:PORT";
136 return false;
137 }
138 host = spec.substr(1, close - 1);
139 port = spec.substr(close + 2);
140 fBracketedIpv6 = true;
141 }
142 else
143 {
144 size_t colon = spec.rfind(':');
145 if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size())
146 {
147 errOut = "expected HOST:PORT";
148 return false;
149 }
150 host = spec.substr(0, colon);
151 port = spec.substr(colon + 1);
152 if (host.find(':') != std::string::npos)
153 {
154 errOut = "IPv6 proxy hosts must use [ADDR]:PORT";
155 return false;
156 }
157 }
158
159 if (host.size() > 255)
160 {
161 errOut = "host is too long";
162 return false;
163 }
164 if (fBracketedIpv6)
165 {
166 bool fHasColon = false;
167 for (size_t i = 0; i < host.size(); i++)
168 {
169 fHasColon = fHasColon || host[i] == ':';
170 if (!ValidProxyIpv6HostChar(host[i]))
171 {
172 errOut = "IPv6 proxy host contains unsupported characters";
173 return false;
174 }
175 }
176 if (!fHasColon)
177 {
178 errOut = "bracketed proxy host is not IPv6";
179 return false;
180 }
181 }
182 else
183 {
184 for (size_t i = 0; i < host.size(); i++)
185 {
186 if (!ValidProxyHostChar(host[i]))
187 {
188 errOut = "host contains unsupported characters";
189 return false;
190 }
191 }
192 }
193 if (!ParsePort(port, portOut, errOut))
194 return false;
195
196 hostOut = host;
197 return true;
198}
199
200static bool SetSocks5Proxy(const std::string& spec, bool fTorMode,
201 std::string& errOut)
202{
203 std::string host;
204 unsigned short port = 0;
205 if (!BtfParseSocks5Proxy(spec, host, port, errOut))
206 return false;
207 CRITICAL_BLOCK(cs_socks5)
208 {
209 g_socks5Host = host;
210 g_socks5Port = port;
211 g_fSocks5Proxy = true;
212 g_fTorProxy = fTorMode;
213 }
214 return true;
215}
216
217bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut)
218{
219 return SetSocks5Proxy(spec, false, errOut);
220}
221
222bool BtfEnableTorProxy(const std::string& spec, std::string& errOut)
223{
224 return SetSocks5Proxy(spec.empty() ? DEFAULT_TOR_SOCKS5_PROXY : spec, true, errOut);
225}
226
227void BtfClearSocks5Proxy()
228{
229 CRITICAL_BLOCK(cs_socks5)
230 {
231 g_fSocks5Proxy = false;
232 g_fTorProxy = false;
233 g_socks5Host.clear();
234 g_socks5Port = 0;
235 }
236}
237
238bool BtfSocks5ProxyEnabled()
239{
240 CRITICAL_BLOCK(cs_socks5)
241 return g_fSocks5Proxy;
242 return false;
243}
244
245bool BtfTorProxyEnabled()
246{
247 CRITICAL_BLOCK(cs_socks5)
248 return g_fSocks5Proxy && g_fTorProxy;
249 return false;
250}
251
252std::string BtfSocks5ProxyName()
253{
254 CRITICAL_BLOCK(cs_socks5)
255 {
256 if (!g_fSocks5Proxy)
257 return "";
258 if (g_socks5Host.find(':') != std::string::npos)
259 return strprintf("[%s]:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
260 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
261 }
262 return "";
263}
264
265static SOCKET ConnectDirect(const std::string& host, unsigned short port,
266 int nSockSite, int nTimeoutSecs)
267{
268 struct addrinfo hints, *res = NULL, *rp = NULL;
269 memset(&hints, 0, sizeof(hints));
270 hints.ai_family = AF_UNSPEC;
271 hints.ai_socktype = SOCK_STREAM;
272 char portstr[16];
273 sprintf(portstr, "%u", (unsigned)port);
274 if (getaddrinfo(host.c_str(), portstr, &hints, &res) != 0 || !res)
275 return INVALID_SOCKET;
276
277 SOCKET s = INVALID_SOCKET;
278 for (rp = res; rp != NULL; rp = rp->ai_next)
279 {
280 s = BtfSocketTag(socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol), nSockSite);
281 if (s == INVALID_SOCKET)
282 continue;
283 SetSocketTimeout(s, nTimeoutSecs);
284 if (connect(s, rp->ai_addr, (int)rp->ai_addrlen) == 0)
285 break;
286 BtfCloseSocket(s);
287 s = INVALID_SOCKET;
288 }
289 freeaddrinfo(res);
290 return s;
291}
292
293static bool Socks5Connect(SOCKET s, const std::string& destHost,
294 unsigned short destPort)
295{
296 if (destHost.empty() || destHost.size() > 255)
297 return false;
298
299 unsigned char hello[3] = { 0x05, 0x01, 0x00 }; // SOCKS5, one method, no auth
300 if (!WriteN(s, hello, sizeof(hello)))
301 {
302 error("SOCKS5 hello write failed for %s:%u\n",
303 destHost.c_str(), (unsigned)destPort);
304 return false;
305 }
306 unsigned char choice[2] = { 0, 0 };
307 if (!ReadN(s, choice, sizeof(choice)))
308 {
309 error("SOCKS5 method selection read failed for %s:%u\n",
310 destHost.c_str(), (unsigned)destPort);
311 return false;
312 }
313 if (choice[0] != 0x05 || choice[1] != 0x00)
314 {
315 error("SOCKS5 proxy rejected no-auth method for %s:%u (version=0x%02x method=0x%02x)\n",
316 destHost.c_str(), (unsigned)destPort,
317 (unsigned)choice[0], (unsigned)choice[1]);
318 return false;
319 }
320
321 std::vector<unsigned char> req;
322 req.reserve(7 + destHost.size());
323 req.push_back(0x05); // version
324 req.push_back(0x01); // CONNECT
325 req.push_back(0x00); // reserved
326 req.push_back(0x03); // domain name; proxy resolves, avoiding local DNS leak
327 req.push_back((unsigned char)destHost.size());
328 req.insert(req.end(), destHost.begin(), destHost.end());
329 req.push_back((unsigned char)(destPort >> 8));
330 req.push_back((unsigned char)(destPort & 0xff));
331 if (!WriteN(s, &req[0], (int)req.size()))
332 {
333 error("SOCKS5 CONNECT request write failed for %s:%u\n",
334 destHost.c_str(), (unsigned)destPort);
335 return false;
336 }
337
338 unsigned char rep[4] = {0,0,0,0};
339 if (!ReadN(s, rep, sizeof(rep)))
340 {
341 error("SOCKS5 CONNECT reply read failed for %s:%u\n",
342 destHost.c_str(), (unsigned)destPort);
343 return false;
344 }
345 if (rep[0] != 0x05)
346 {
347 error("SOCKS5 CONNECT reply has invalid version 0x%02x for %s:%u\n",
348 (unsigned)rep[0], destHost.c_str(), (unsigned)destPort);
349 return false;
350 }
351 if (rep[1] != 0x00)
352 {
353 error("SOCKS5 CONNECT to %s:%u failed with reply 0x%02x\n",
354 destHost.c_str(), (unsigned)destPort, (unsigned)rep[1]);
355 return false;
356 }
357 int nAddr = 0;
358 if (rep[3] == 0x01) nAddr = 4;
359 else if (rep[3] == 0x04) nAddr = 16;
360 else if (rep[3] == 0x03)
361 {
362 unsigned char len = 0;
363 if (!ReadN(s, &len, 1))
364 return false;
365 nAddr = len;
366 }
367 else
368 {
369 error("SOCKS5 CONNECT reply has invalid address type 0x%02x for %s:%u\n",
370 (unsigned)rep[3], destHost.c_str(), (unsigned)destPort);
371 return false;
372 }
373
374 std::vector<unsigned char> discard(nAddr + 2);
375 if (!ReadN(s, &discard[0], (int)discard.size()))
376 {
377 error("SOCKS5 CONNECT bind address read failed for %s:%u\n",
378 destHost.c_str(), (unsigned)destPort);
379 return false;
380 }
381 return true;
382}
383
384SOCKET BtfConnectSocket(const std::string& destHost, unsigned short destPort,
385 int nSockSite, int nTimeoutSecs)
386{
387 std::string proxyHost;
388 unsigned short proxyPort = 0;
389 bool fProxy = false;
390 bool fTorProxy = false;
391 CRITICAL_BLOCK(cs_socks5)
392 {
393 fProxy = g_fSocks5Proxy;
394 fTorProxy = g_fTorProxy;
395 proxyHost = g_socks5Host;
396 proxyPort = g_socks5Port;
397 }
398
399 if (!fProxy)
400 {
401 if (BtfIsTorOnionHost(destHost))
402 {
403 error("Refusing direct .onion connection to %s:%u; use /tor or /socks\n",
404 destHost.c_str(), (unsigned)destPort);
405 return INVALID_SOCKET;
406 }
407 return ConnectDirect(destHost, destPort, nSockSite, nTimeoutSecs);
408 }
409
410 if (fTorProxy && nTimeoutSecs > 0 && nTimeoutSecs < TOR_CONNECT_TIMEOUT_SECS)
411 nTimeoutSecs = TOR_CONNECT_TIMEOUT_SECS;
412
413 SOCKET s = ConnectDirect(proxyHost, proxyPort, nSockSite, nTimeoutSecs);
414 if (s == INVALID_SOCKET)
415 return INVALID_SOCKET;
416 if (!Socks5Connect(s, destHost, destPort))
417 {
418 BtfCloseSocket(s);
419 return INVALID_SOCKET;
420 }
421 return s;
422}