diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/proxy.cpp | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (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.cpp | 422 |
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 | |||
| 10 | static CCriticalSection cs_socks5; | ||
| 11 | static bool g_fSocks5Proxy = false; | ||
| 12 | static bool g_fTorProxy = false; | ||
| 13 | static std::string g_socks5Host; | ||
| 14 | static unsigned short g_socks5Port = 0; | ||
| 15 | static const char* DEFAULT_TOR_SOCKS5_PROXY = "127.0.0.1:9050"; | ||
| 16 | static const int TOR_CONNECT_TIMEOUT_SECS = 120; | ||
| 17 | |||
| 18 | static 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 | |||
| 35 | static 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 | |||
| 49 | static 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 | |||
| 63 | static bool ValidProxyHostChar(char c) | ||
| 64 | { | ||
| 65 | return isalnum((unsigned char)c) || c == '.' || c == '-' || c == '_'; | ||
| 66 | } | ||
| 67 | |||
| 68 | static bool ValidProxyIpv6HostChar(char c) | ||
| 69 | { | ||
| 70 | return isxdigit((unsigned char)c) || c == ':' || c == '.'; | ||
| 71 | } | ||
| 72 | |||
| 73 | static 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 | |||
| 82 | bool 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 | |||
| 91 | static 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 | |||
| 120 | bool 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 | |||
| 200 | static 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 | |||
| 217 | bool BtfSetSocks5Proxy(const std::string& spec, std::string& errOut) | ||
| 218 | { | ||
| 219 | return SetSocks5Proxy(spec, false, errOut); | ||
| 220 | } | ||
| 221 | |||
| 222 | bool BtfEnableTorProxy(const std::string& spec, std::string& errOut) | ||
| 223 | { | ||
| 224 | return SetSocks5Proxy(spec.empty() ? DEFAULT_TOR_SOCKS5_PROXY : spec, true, errOut); | ||
| 225 | } | ||
| 226 | |||
| 227 | void 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 | |||
| 238 | bool BtfSocks5ProxyEnabled() | ||
| 239 | { | ||
| 240 | CRITICAL_BLOCK(cs_socks5) | ||
| 241 | return g_fSocks5Proxy; | ||
| 242 | return false; | ||
| 243 | } | ||
| 244 | |||
| 245 | bool BtfTorProxyEnabled() | ||
| 246 | { | ||
| 247 | CRITICAL_BLOCK(cs_socks5) | ||
| 248 | return g_fSocks5Proxy && g_fTorProxy; | ||
| 249 | return false; | ||
| 250 | } | ||
| 251 | |||
| 252 | std::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 | |||
| 265 | static 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 | |||
| 293 | static 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 | |||
| 384 | SOCKET 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 | } | ||