diff options
| author | bitflash <admin@bitflash.network> | 2026-08-18 10:04:28 -0300 |
|---|---|---|
| committer | bitflash <admin@bitflash.network> | 2026-08-18 10:04:28 -0300 |
| commit | db84acfc5f99ac1ddf4962879b4cbaf1af9952dd (patch) | |
| tree | ca7e4c67216880361dfd484c27fbf7b8c9d2d269 | |
| parent | a6eb8dab830be3135c054ae3ae442886f1867b4e (diff) | |
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.
| -rw-r--r-- | src/net.cpp | 17 | ||||
| -rw-r--r-- | src/net.h | 9 |
2 files changed, 26 insertions, 0 deletions
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) | |||
| 1622 | unsigned int nNodes = 0; | 1622 | unsigned int nNodes = 0; |
| 1623 | CRITICAL_BLOCK(cs_vNodes) | 1623 | CRITICAL_BLOCK(cs_vNodes) |
| 1624 | nNodes = (unsigned int)vNodes.size(); | 1624 | nNodes = (unsigned int)vNodes.size(); |
| 1625 | // Also count how many connections this one address already | ||
| 1626 | // holds. A single IP that keeps opening sockets -- a broken | ||
| 1627 | // node or a deliberate connection-exhaustion flood -- must not | ||
| 1628 | // be able to take the whole table, which is what starved the | ||
| 1629 | // relays. | ||
| 1630 | unsigned int nFromThisIP = 0; | ||
| 1631 | CRITICAL_BLOCK(cs_vNodes) | ||
| 1632 | for (vector<CNode*>::iterator it = vNodes.begin(); it != vNodes.end(); ++it) | ||
| 1633 | if ((*it)->addr.ip == addr.ip) | ||
| 1634 | nFromThisIP++; | ||
| 1635 | |||
| 1625 | if (nNodes >= MAX_CONNECTIONS) | 1636 | if (nNodes >= MAX_CONNECTIONS) |
| 1626 | { | 1637 | { |
| 1627 | LogPrint("net", "refusing connection from %s, already at %u\n", | 1638 | LogPrint("net", "refusing connection from %s, already at %u\n", |
| 1628 | addr.ToString().c_str(), nNodes); | 1639 | addr.ToString().c_str(), nNodes); |
| 1629 | BtfCloseSocket(hSocket); | 1640 | BtfCloseSocket(hSocket); |
| 1630 | } | 1641 | } |
| 1642 | else if (nFromThisIP >= MAX_CONNECTIONS_PER_IP) | ||
| 1643 | { | ||
| 1644 | LogPrint("net", "refusing connection from %s, already %u from that address\n", | ||
| 1645 | addr.ToString().c_str(), nFromThisIP); | ||
| 1646 | BtfCloseSocket(hSocket); | ||
| 1647 | } | ||
| 1631 | else | 1648 | else |
| 1632 | { | 1649 | { |
| 1633 | LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); | 1650 | LogPrint("net", "accepted connection from %s\n", addr.ToString().c_str()); |
| @@ -112,6 +112,15 @@ static const int BTF_INCOMPLETE_MESSAGE_TIMEOUT_SECS = 2 * 60; | |||
| 112 | // 125 is Bitcoin's number and remains a conservative public-node default. | 112 | // 125 is Bitcoin's number and remains a conservative public-node default. |
| 113 | static const unsigned int MAX_CONNECTIONS = 125; | 113 | static const unsigned int MAX_CONNECTIONS = 125; |
| 114 | 114 | ||
| 115 | // Cap inbound connections from any single address. Without it one source can | ||
| 116 | // hold every slot up to MAX_CONNECTIONS -- exactly what happened to the | ||
| 117 | // rendezvous relays, where a single IP parked hundreds of connections and | ||
| 118 | // starved real peers. A legitimate peer, even a relay talking to a relay, | ||
| 119 | // needs only a handful; 32 leaves generous room while denying any one address | ||
| 120 | // more than a quarter of the table. The firewall connlimit on the relays does | ||
| 121 | // the same thing a layer lower; this makes every build carry the defense. | ||
| 122 | static const unsigned int MAX_CONNECTIONS_PER_IP = 32; | ||
| 123 | |||
| 115 | // A node marked for disconnect is normally held until its buffers drain, so | 124 | // A node marked for disconnect is normally held until its buffers drain, so |
| 116 | // a last message still goes out. But vSend cannot drain through a socket | 125 | // a last message still goes out. But vSend cannot drain through a socket |
| 117 | // whose far end is gone, and that made such a node immortal: it stayed in | 126 | // whose far end is gone, and that made such a node immortal: it stayed in |