net: 3 Tor/HTTP resilience fixes from experimental patch

1. -torconnecttimeout config option (init.cpp, netbase.h, netbase.cpp)
   SOCKS5/Tor negotiation bound. Default 60s. Range 5-180s. Without this, a
   dead/slow .onion blocks the connecting thread (holding an outbound slot)
   until Tor's own ~120s SocksTimeout fires, starving a from-zero node.

   Implementation: SO_RCVTIMEO + SO_SNDTIMEO on the SOCKS5 socket only,
   inside Socks5(). Both Linux/BSD and Win32 paths. Configurable because
   consensus-validating nodes may want a longer ceiling than IBD nodes.

2. HTTP seed fetch: chunked-encoding support (net.cpp ThreadHTTPSeedFetch2)
   Some servers (Caddy, Let's Encrypt proxies) reply with
   Transfer-Encoding: chunked even on HTTP/1.1 + Connection: close. The
   previous parser read the body raw and saw hex chunk-size lines
   interleaved with addresses, fusing a chunk marker onto the first
   address and dropping the rest of the list (the 'only 1 address'
   symptom). De-chunk first when header advertises chunked, then parse.

3. Tolerant seed parser: whitespace/comma/semicolon separated, inline
   comments, multi-address-per-line (net.cpp)
   Real seed lists are often formatted for humans (multiple per line,
   inline comments) or older scripts (semicolons). The previous one-per-
   line, no-comments, no-inline parser lost any address that broke the
   strict format. Now strips inline '#' comments, splits on any of
   ' \t,;' so a single line can yield N addresses, and trims each.

Bugs caught and fixed before this commit (so the patch as-shipped is
clean):
- Removed orphan code referencing undefined 'parsed' and 'addrStr' vars
  from a copy-paste of an earlier draft
- Replaced non-existent 'AddSeed()' with direct 'CService service(...)'
  construction followed by 'addrman.Add(CAddress, CService)' (correct
  addrman.Add signature, not CNetAddr)
- Tightened 'addrman.Add' call to the actual signature: address + source
This commit is contained in:
Sami Ahmed
2026-06-21 19:48:48 -07:00
parent 55f1b03848
commit 78256e65d7
4 changed files with 122 additions and 45 deletions
+11
View File
@@ -506,6 +506,7 @@ std::string HelpMessage()
" -dbcache=<n> " + _("Set database cache size in megabytes (default: 25)") + "\n" + " -dbcache=<n> " + _("Set database cache size in megabytes (default: 25)") + "\n" +
" -dblogsize=<n> " + _("Set database disk log size in megabytes (default: 100)") + "\n" + " -dblogsize=<n> " + _("Set database disk log size in megabytes (default: 100)") + "\n" +
" -timeout=<n> " + _("Specify connection timeout in milliseconds (default: 5000)") + "\n" + " -timeout=<n> " + _("Specify connection timeout in milliseconds (default: 5000)") + "\n" +
" -torconnecttimeout=<n> " + _("Max time (ms) to wait for Tor to reach a peer .onion before giving up (default: 60000, range 5000-180000)") + "\n" +
//" -proxy=<ip:port> " + _("Connect through socks proxy") + "\n" + //" -proxy=<ip:port> " + _("Connect through socks proxy") + "\n" +
//" -socks=<n> " + _("Select the version of socks proxy to use (4-5, default: 5)") + "\n" + //" -socks=<n> " + _("Select the version of socks proxy to use (4-5, default: 5)") + "\n" +
" -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n" " -tor=<ip:port> " + _("Use proxy to reach tor hidden services (default: same as -proxy)") + "\n"
@@ -780,6 +781,16 @@ bool AppInit2()
nConnectTimeout = nNewTimeout; nConnectTimeout = nNewTimeout;
} }
// SOCKS5/Tor negotiation timeout. Separate from -timeout (which only covers
// the instant local connect to the Tor SOCKS proxy); this bounds how long we
// wait for Tor to reach the target .onion before giving up on that peer.
if (mapArgs.count("-torconnecttimeout"))
{
int nTorTimeout = GetArg("-torconnecttimeout", 60000);
if (nTorTimeout >= 5000 && nTorTimeout <= 180000)
nSocksNegotiationTimeout = nTorTimeout;
}
if (mapArgs.count("-paytxfee")) if (mapArgs.count("-paytxfee"))
{ {
if (!ParseMoney(mapArgs["-paytxfee"], nTransactionFee)) if (!ParseMoney(mapArgs["-paytxfee"], nTransactionFee))
+85 -45
View File
@@ -1742,10 +1742,78 @@ bool ThreadHTTPSeedFetch2(void* parg)
return false; return false;
} }
std::string headers = response.substr(0, headerEnd);
std::string body = response.substr(headerEnd + 4); std::string body = response.substr(headerEnd + 4);
// Parse one address per line: "address:port" or just "address" // Some servers (e.g. Caddy / Let's Encrypt fronting the seed list) reply
// with Transfer-Encoding: chunked even on HTTP/1.1 + Connection: close. The
// body then carries hex chunk-size lines interleaved with the data; parsing
// it raw fuses a chunk marker onto an address and we lose most of the list
// (the classic "only 1 address" symptom). De-chunk first when present.
{
std::string h = headers;
for (char& c : h) c = (char)tolower((unsigned char)c);
if (h.find("transfer-encoding:") != std::string::npos &&
h.find("chunked") != std::string::npos)
{
std::string decoded;
size_t pos = 0;
while (pos < body.size()) {
size_t eol = body.find("\r\n", pos);
if (eol == std::string::npos) break;
std::string sizeLine = body.substr(pos, eol - pos);
size_t semi = sizeLine.find(';'); // strip chunk extensions
if (semi != std::string::npos) sizeLine = sizeLine.substr(0, semi);
unsigned long chunkSize = strtoul(sizeLine.c_str(), nullptr, 16);
pos = eol + 2;
if (chunkSize == 0) break; // last chunk
if (pos + chunkSize > body.size())
chunkSize = body.size() - pos; // defensive clamp
decoded.append(body, pos, chunkSize);
pos += chunkSize;
if (pos + 2 <= body.size() && body.compare(pos, 2, "\r\n") == 0)
pos += 2; // trailing CRLF after data
}
body.swap(decoded);
}
}
if (fDebug)
printf("HTTPS seed fetch: %d body bytes to parse\n", (int)body.size());
// Tolerant parse: accept one-per-line OR several addresses on one line
// (whitespace / comma / semicolon separated), and ignore inline '#' comments.
int found = 0; int found = 0;
auto addSeed = [&](std::string addrStr) -> void {
while (!addrStr.empty() && (addrStr.back()=='\r' || addrStr.back()==' ' || addrStr.back()=='\t'))
addrStr.pop_back();
while (!addrStr.empty() && (addrStr.front()==' ' || addrStr.front()=='\t'))
addrStr.erase(addrStr.begin());
if (addrStr.empty())
return;
int port = GetDefaultPort();
size_t onionPos = addrStr.find(".onion:");
if (onionPos != std::string::npos) {
port = atoi(addrStr.substr(onionPos + 7).c_str());
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion"
} else if (addrStr.find(".onion") == std::string::npos) {
return; // Tor-native: skip non-.onion addresses
}
if (port <= 0 || port > 65535)
port = GetDefaultPort();
CService service(addrStr, port);
if (service.IsValid()) {
CAddress addr(service);
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
addrman.Add(addr, service);
printf("HTTPS seed: added %s:%d\n", addrStr.c_str(), port);
found++;
}
};
std::istringstream lines(body); std::istringstream lines(body);
std::string line; std::string line;
while (std::getline(lines, line)) while (std::getline(lines, line))
@@ -1753,51 +1821,23 @@ bool ThreadHTTPSeedFetch2(void* parg)
if (fShutdown) if (fShutdown)
return false; return false;
// Trim whitespace and carriage returns // Strip inline comments (everything from '#' onward)
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) size_t hashPos = line.find('#');
line.pop_back(); if (hashPos != std::string::npos)
while (!line.empty() && (line.front() == ' ' || line.front() == '\t')) line = line.substr(0, hashPos);
line.erase(line.begin());
if (line.empty() || line[0] == '#') // Split on whitespace / comma / semicolon so multiple addresses on
continue; // one line are all captured.
size_t start = 0;
// Parse address:port while (start <= line.size()) {
std::string addrStr = line; size_t sep = line.find_first_of(" \t,;", start);
int port = GetDefaultPort(); std::string tok = (sep == std::string::npos)
? line.substr(start)
// For .onion addresses, the last colon before port is after ".onion" : line.substr(start, sep - start);
size_t onionPos = addrStr.find(".onion:"); if (!tok.empty())
if (onionPos != std::string::npos) { addSeed(tok);
port = atoi(addrStr.substr(onionPos + 7).c_str()); if (sep == std::string::npos) break;
addrStr = addrStr.substr(0, onionPos + 6); // keep ".onion" start = sep + 1;
} else if (addrStr.find(".onion") == std::string::npos) {
// Tor-native: skip non-.onion addresses
continue;
}
if (port <= 0 || port > 65535)
port = GetDefaultPort();
CNetAddr parsed;
bool resolved = parsed.SetSpecial(addrStr);
if (!resolved) {
std::vector<CNetAddr> vIP;
if (LookupHost(addrStr.c_str(), vIP, 1, false) && !vIP.empty()) {
parsed = vIP[0];
resolved = true;
}
}
if (resolved) {
CAddress addr(CService(parsed, port));
addr.nTime = GetTime() - 3*24*60*60; // 3 days ago
addrman.Add(addr, CNetAddr("https-seed", true));
// Queue the first 8 seeds for immediate direct connection
if (found < 8) {
std::string oneShotAddr = addrStr + ":" + std::to_string(port);
AddOneShot(oneShotAddr);
}
found++;
} }
} }
+25
View File
@@ -21,6 +21,13 @@ static proxyType proxyInfo[NET_MAX];
static proxyType nameproxyInfo; static proxyType nameproxyInfo;
static CCriticalSection cs_proxyInfos; static CCriticalSection cs_proxyInfos;
int nConnectTimeout = 5000; int nConnectTimeout = 5000;
// Bound for the SOCKS5 negotiation over Tor (ms). The recv() calls in Socks5()
// wait for Tor to build a circuit and fetch the v3 hidden-service descriptor for
// the target .onion; with no timeout a dead/slow onion blocks the connecting
// thread (holding an outbound slot) until Tor's own ~120s SocksTimeout fires.
// Configurable via -torconnecttimeout. Default 60s: long enough for a healthy
// onion to answer, short enough that bad peers don't starve a from-zero node.
int nSocksNegotiationTimeout = 60000;
bool fNameLookup = false; bool fNameLookup = false;
static const unsigned char pchIPv4[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff }; static const unsigned char pchIPv4[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };
@@ -223,6 +230,24 @@ bool static Socks5(string strDest, int port, SOCKET& hSocket)
closesocket(hSocket); closesocket(hSocket);
return error("Hostname too long"); return error("Hostname too long");
} }
// Bound the blocking SOCKS5 handshake so a slow/dead .onion can't stall this
// thread (and hold an outbound connection slot) waiting on Tor. A timeout makes
// the recv() below return < expected, which the existing checks treat as a
// clean failure so the connector moves on to the next peer.
{
#ifdef WIN32
DWORD tv = (DWORD)nSocksNegotiationTimeout;
setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));
setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv));
#else
struct timeval tv;
tv.tv_sec = nSocksNegotiationTimeout / 1000;
tv.tv_usec = (nSocksNegotiationTimeout % 1000) * 1000;
setsockopt(hSocket, SOL_SOCKET, SO_RCVTIMEO, (const void*)&tv, sizeof(tv));
setsockopt(hSocket, SOL_SOCKET, SO_SNDTIMEO, (const void*)&tv, sizeof(tv));
#endif
}
char pszSocks5Init[] = "\5\1\0"; char pszSocks5Init[] = "\5\1\0";
if (fDebug) if (fDebug)
{ {
+1
View File
@@ -29,6 +29,7 @@ enum Network
}; };
extern int nConnectTimeout; extern int nConnectTimeout;
extern int nSocksNegotiationTimeout;
extern bool fNameLookup; extern bool fNameLookup;
/** IP address (IPv6, or IPv4 using mapped IPv6 range (::FFFF:0:0/96)) */ /** IP address (IPv6, or IPv4 using mapped IPv6 range (::FFFF:0:0/96)) */