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-06 01:53:03 +0000
committerbitflash-maintainer <noreply@bitflash.invalid>2026-09-06 01:53:03 +0000
commitef9f06d43fc6577c15df2256abbb25b20d4227b8 (patch)
tree7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/proxy.cpp
parentbe63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff)
parentceae3d076b5ad11ee84729ae43f14e9312896646 (diff)
Merge branch 'chore/main-catchup-2026-08-26' into 'main'HEADmain
Bring main onto the line this project actually ships (incl. the 1.2.23 compactblock port) Closes #6, #7, #8, #11, #35, #34, #29, #28, #27, and #16 See merge request !16
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}