From db84acfc5f99ac1ddf4962879b4cbaf1af9952dd Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 10:04:28 -0300 Subject: net: cap inbound connections per address A single IP could hold every inbound slot up to MAX_CONNECTIONS -- which is what a connection-exhaustion flood did to the rendezvous relays, one address parking hundreds of sockets and starving real peers. Add MAX_CONNECTIONS_PER_IP (32): the accept path counts existing connections from the same address and refuses a new one past the cap. The relays' firewall connlimit does the same a layer lower; this makes every build carry the defense, including future poll()-based relays with no FD ceiling. --- src/net.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index 3660735..6e74cbb 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1622,12 +1622,29 @@ void ThreadSocketHandler2(void* parg) unsigned int nNodes = 0; CRITICAL_BLOCK(cs_vNodes) nNodes = (unsigned int)vNodes.size(); + // Also count how many connections this one address already + // holds. A single IP that keeps opening sockets -- a broken + // node or a deliberate connection-exhaustion flood -- must not + // be able to take the whole table, which is what starved the + // relays. + unsigned int nFromThisIP = 0; + CRITICAL_BLOCK(cs_vNodes) + for (vector::iterator it = vNodes.begin(); it != vNodes.end(); ++it) + if ((*it)->addr.ip == addr.ip) + nFromThisIP++; + if (nNodes >= MAX_CONNECTIONS) { LogPrint("net", "refusing connection from %s, already at %u\n", addr.ToString().c_str(), nNodes); BtfCloseSocket(hSocket); } + else if (nFromThisIP >= MAX_CONNECTIONS_PER_IP) + { + LogPrint("net", "refusing connection from %s, already %u from that address\n", + addr.ToString().c_str(), nFromThisIP); + BtfCloseSocket(hSocket); + } else { LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); -- cgit v1.2.3 From 33092ffe8ee5fb5f7c080a44831e5557b63deb63 Mon Sep 17 00:00:00 2001 From: bitflash Date: Tue, 18 Aug 2026 10:24:01 -0300 Subject: net: exempt loopback from the per-IP connection cap Review follow-up. A managed-Tor node receives every inbound onion peer over its local hidden-service listener, so they all accept as 127.0.0.1. Capping loopback would cap onion peers as one group and refuse the 33rd legitimate onion peer -- the wrong limit for a network moving toward onion-only. Exempt loopback (127.0.0.0/8): the global MAX_CONNECTIONS still bounds the total, and the direct-IP flood this defends against never arrives over loopback. Bench: a 40-connection flood from 127.0.0.1 is now held in full (no per-IP refusal), while a flood from the machine's LAN address still caps at exactly 32. --- src/net.cpp | 22 +++++++++++++++++----- src/net.h | 8 ++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) (limited to 'src/net.cpp') diff --git a/src/net.cpp b/src/net.cpp index 6e74cbb..4603d2d 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1627,11 +1627,23 @@ void ThreadSocketHandler2(void* parg) // node or a deliberate connection-exhaustion flood -- must not // be able to take the whole table, which is what starved the // relays. + // + // Loopback is exempt. Onion peers reach a managed-Tor node + // through its local hidden-service listener, so every one of + // them accepts as 127.0.0.1; capping loopback would cap all + // inbound onion peers as a single group, which is the opposite + // of where the network is headed. The global MAX_CONNECTIONS + // still bounds the total, and the direct-IP flood this defends + // against never arrives over loopback. (Per-onion-identity + // limiting, once a peer's identity is known past accept(), is + // the right tool for onion and is left for later.) + bool fLoopback = (addr.GetByte(3) == 127); unsigned int nFromThisIP = 0; - CRITICAL_BLOCK(cs_vNodes) - for (vector::iterator it = vNodes.begin(); it != vNodes.end(); ++it) - if ((*it)->addr.ip == addr.ip) - nFromThisIP++; + if (!fLoopback) + CRITICAL_BLOCK(cs_vNodes) + for (vector::iterator it = vNodes.begin(); it != vNodes.end(); ++it) + if ((*it)->addr.ip == addr.ip) + nFromThisIP++; if (nNodes >= MAX_CONNECTIONS) { @@ -1639,7 +1651,7 @@ void ThreadSocketHandler2(void* parg) addr.ToString().c_str(), nNodes); BtfCloseSocket(hSocket); } - else if (nFromThisIP >= MAX_CONNECTIONS_PER_IP) + else if (!fLoopback && nFromThisIP >= MAX_CONNECTIONS_PER_IP) { LogPrint("net", "refusing connection from %s, already %u from that address\n", addr.ToString().c_str(), nFromThisIP); diff --git a/src/net.h b/src/net.h index 4fdd1b3..e3d4a93 100644 --- a/src/net.h +++ b/src/net.h @@ -119,6 +119,14 @@ static const unsigned int MAX_CONNECTIONS = 125; // needs only a handful; 32 leaves generous room while denying any one address // more than a quarter of the table. The firewall connlimit on the relays does // the same thing a layer lower; this makes every build carry the defense. +// +// Loopback (127.0.0.0/8) is exempt in the accept path. A managed-Tor node +// receives every inbound onion peer over its local hidden-service listener, so +// they all appear as 127.0.0.1 -- capping loopback would cap onion peers as a +// group, which is exactly wrong for a network moving toward onion-only. The +// global MAX_CONNECTIONS still bounds the total; a per-onion-identity limit, +// applied once identity is known after accept(), is the right future tool for +// onion floods. static const unsigned int MAX_CONNECTIONS_PER_IP = 32; // A node marked for disconnect is normally held until its buffers drain, so -- cgit v1.2.3