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:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-11 15:51:52 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-11 15:51:52 -0300
commit066379ebcb1dac720221fe1a1841fa6f59df21d9 (patch)
treec679ea133384fb33f2f77602e99430896ef9b50c /src
parentff3829b28fd2ddc85a9a3bb4424cd5b4719854ab (diff)
net: harden SOCKS5 proxy parsing and tests
Diffstat (limited to 'src')
-rw-r--r--src/proxy.cpp115
-rw-r--r--src/readme.txt1
-rw-r--r--src/selftest.cpp212
3 files changed, 308 insertions, 20 deletions
diff --git a/src/proxy.cpp b/src/proxy.cpp
index 1e62ff9..d7dc324 100644
--- a/src/proxy.cpp
+++ b/src/proxy.cpp
@@ -5,6 +5,8 @@
5#include "proxy.h" 5#include "proxy.h"
6#include "sockcount.h" 6#include "sockcount.h"
7 7
8#include <errno.h>
9
8static CCriticalSection cs_socks5; 10static CCriticalSection cs_socks5;
9static bool g_fSocks5Proxy = false; 11static bool g_fSocks5Proxy = false;
10static std::string g_socks5Host; 12static std::string g_socks5Host;
@@ -60,6 +62,40 @@ static bool ValidProxyHostChar(char c)
60 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_'; 62 return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_';
61} 63}
62 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
63bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut, 99bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
64 unsigned short& portOut, std::string& errOut) 100 unsigned short& portOut, std::string& errOut)
65{ 101{
@@ -67,45 +103,76 @@ bool BtfParseSocks5Proxy(const std::string& spec, std::string& hostOut,
67 portOut = 0; 103 portOut = 0;
68 errOut.clear(); 104 errOut.clear();
69 105
70 size_t colon = spec.rfind(':'); 106 std::string host;
71 if (colon == std::string::npos || colon == 0 || colon + 1 >= spec.size()) 107 std::string port;
108 bool fBracketedIpv6 = false;
109 if (!spec.empty() && spec[0] == '[')
72 { 110 {
73 errOut = "expected HOST:PORT"; 111 size_t close = spec.find(']');
74 return false; 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 }
75 } 136 }
76 137
77 std::string host = spec.substr(0, colon);
78 std::string port = spec.substr(colon + 1);
79 if (host.size() > 255) 138 if (host.size() > 255)
80 { 139 {
81 errOut = "host is too long"; 140 errOut = "host is too long";
82 return false; 141 return false;
83 } 142 }
84 for (size_t i = 0; i < host.size(); i++) 143 if (fBracketedIpv6)
85 { 144 {
86 if (!ValidProxyHostChar(host[i])) 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)
87 { 156 {
88 errOut = "host contains unsupported characters"; 157 errOut = "bracketed proxy host is not IPv6";
89 return false; 158 return false;
90 } 159 }
91 } 160 }
92 for (size_t i = 0; i < port.size(); i++) 161 else
93 { 162 {
94 if (!isdigit((unsigned char)port[i])) 163 for (size_t i = 0; i < host.size(); i++)
95 { 164 {
96 errOut = "port is not numeric"; 165 if (!ValidProxyHostChar(host[i]))
97 return false; 166 {
167 errOut = "host contains unsupported characters";
168 return false;
169 }
98 } 170 }
99 } 171 }
100 int nPort = atoi(port.c_str()); 172 if (!ParsePort(port, portOut, errOut))
101 if (nPort <= 0 || nPort > 65535)
102 {
103 errOut = "port is out of range";
104 return false; 173 return false;
105 }
106 174
107 hostOut = host; 175 hostOut = host;
108 portOut = (unsigned short)nPort;
109 return true; 176 return true;
110} 177}
111 178
@@ -147,6 +214,8 @@ std::string BtfSocks5ProxyName()
147 { 214 {
148 if (!g_fSocks5Proxy) 215 if (!g_fSocks5Proxy)
149 return ""; 216 return "";
217 if (g_socks5Host.find(':') != std::string::npos)
218 return strprintf("[%s]:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
150 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port); 219 return strprintf("%s:%u", g_socks5Host.c_str(), (unsigned)g_socks5Port);
151 } 220 }
152 return ""; 221 return "";
@@ -207,8 +276,14 @@ static bool Socks5Connect(SOCKET s, const std::string& destHost,
207 return false; 276 return false;
208 277
209 unsigned char rep[4] = {0,0,0,0}; 278 unsigned char rep[4] = {0,0,0,0};
210 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05 || rep[1] != 0x00) 279 if (!ReadN(s, rep, sizeof(rep)) || rep[0] != 0x05)
211 return false; 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 }
212 int nAddr = 0; 287 int nAddr = 0;
213 if (rep[3] == 0x01) nAddr = 4; 288 if (rep[3] == 0x01) nAddr = 4;
214 else if (rep[3] == 0x04) nAddr = 16; 289 else if (rep[3] == 0x04) nAddr = 16;
diff --git a/src/readme.txt b/src/readme.txt
index 96f32c3..1fa77df 100644
--- a/src/readme.txt
+++ b/src/readme.txt
@@ -45,6 +45,7 @@ Command-line options (in addition to the original ones)
45 /socks=HOST:PORT 45 /socks=HOST:PORT
46 route outbound Nostr and .btf rendezvous dials through a 46 route outbound Nostr and .btf rendezvous dials through a
47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor 47 SOCKS5 proxy, e.g. /socks=127.0.0.1:9050 for local Tor
48 or /socks=[::1]:9050 for an IPv6 loopback proxy
48 /solomine mine without requiring a connected peer (local testing) 49 /solomine mine without requiring a connected peer (local testing)
49 /operator operator mode (run pool server) 50 /operator operator mode (run pool server)
50 /participant=ADDR 51 /participant=ADDR
diff --git a/src/selftest.cpp b/src/selftest.cpp
index 5b36b9d..aa4e528 100644
--- a/src/selftest.cpp
+++ b/src/selftest.cpp
@@ -9,6 +9,7 @@
9#include "bip32.h" 9#include "bip32.h"
10#include "proxy.h" 10#include "proxy.h"
11#include "selftest.h" 11#include "selftest.h"
12#include "sockcount.h"
12#include "walletcmd.h" 13#include "walletcmd.h"
13 14
14extern int RunPoolStratumSelfTest(); 15extern int RunPoolStratumSelfTest();
@@ -1254,6 +1255,197 @@ static int RunParseMoneySelfTest()
1254 return nFail == 0 ? 0 : 1; 1255 return nFail == 0 ? 0 : 1;
1255} 1256}
1256 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
1257static int RunSocks5ProxySelfTest() 1449static int RunSocks5ProxySelfTest()
1258{ 1450{
1259 printf("socks5-proxy self-test\n"); 1451 printf("socks5-proxy self-test\n");
@@ -1267,12 +1459,19 @@ static int RunSocks5ProxySelfTest()
1267 nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) && 1459 nFail += Check(BtfParseSocks5Proxy("localhost:9050", host, port, err) &&
1268 host == "localhost" && port == 9050, 1460 host == "localhost" && port == 9050,
1269 "parses hostname proxy endpoint") ? 0 : 1; 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;
1270 nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err), 1467 nFail += Check(!BtfParseSocks5Proxy("localhost", host, port, err),
1271 "rejects missing port") ? 0 : 1; 1468 "rejects missing port") ? 0 : 1;
1272 nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err), 1469 nFail += Check(!BtfParseSocks5Proxy("localhost:0", host, port, err),
1273 "rejects zero port") ? 0 : 1; 1470 "rejects zero port") ? 0 : 1;
1274 nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err), 1471 nFail += Check(!BtfParseSocks5Proxy("localhost:70000", host, port, err),
1275 "rejects out-of-range port") ? 0 : 1; 1472 "rejects out-of-range port") ? 0 : 1;
1473 nFail += Check(!BtfParseSocks5Proxy("localhost:999999999999999999999999999999", host, port, err),
1474 "rejects overflowing port") ? 0 : 1;
1276 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err), 1475 nFail += Check(!BtfParseSocks5Proxy("user:pass@localhost:9050", host, port, err),
1277 "rejects unsupported authenticated proxy syntax") ? 0 : 1; 1476 "rejects unsupported authenticated proxy syntax") ? 0 : 1;
1278 1477
@@ -1285,6 +1484,19 @@ static int RunSocks5ProxySelfTest()
1285 BtfClearSocks5Proxy(); 1484 BtfClearSocks5Proxy();
1286 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1; 1485 nFail += Check(!BtfSocks5ProxyEnabled(), "clear disables proxy") ? 0 : 1;
1287 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
1288 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED", 1500 printf("%s (%d failure%s)\n", nFail == 0 ? "ALL TESTS PASSED" : "TESTS FAILED",
1289 nFail, nFail == 1 ? "" : "s"); 1501 nFail, nFail == 1 ? "" : "s");
1290 fflush(stdout); 1502 fflush(stdout);