net: increase inbound capacity for block-relay-only connections
What changed, and why it matters
This Bitcoin Core commit increases the default maximum peer connections from 125 to 200 and reserves half of inbound slots for block-relay-only peers that don't forward ordinary transactions. It also adds eviction logic so that when a new transaction-relaying inbound peer arrives and the node is at capacity, it tries to disconnect an existing transaction-relaying peer instead of a block-relay-only peer. The change is described by the authors as a network-capacity improvement, not a security fix, but it can make certain denial-of-service and eclipse-style attacks harder by diversifying peer types and limiting how many transaction-relaying inbound peers an attacker can occupy.
Treat as a routine network-hardening improvement. Reviewers and operators should verify that the new default connection limits and reserved full-relay percentage behave correctly under load, do not inadvertently degrade transaction propagation, and that the eviction logic does not disconnect protected peers incorrectly. No emergency deployment is warranted based on the supplied materials.
Security signals we found
Default network capacity increased and inbound peer composition restricted, which can reduce the relative share of attacker-controlled transaction-relay inbounds
New eviction logic specifically targets transaction-relaying inbound peers, making it harder for an attacker to monopolize inbound slots with full-relay connections
Block-relay-only inbound capacity is expanded, improving network partition resistance
No explicit security bug, CVE, or vulnerability description is present in the commit or supplied references
Change is defensive/hardening in nature rather than a patch for a known exploit
Evidence from the diff
The patch changes DEFAULT_MAX_PEER_CONNECTIONS from 125 to 200 and introduces FULL_RELAY_INBOUND_PCT (50%), capping the number of inbound transaction-relay peers at 50% of available inbound slots. It adds CConnman::EvictTxPeerIfFull() and PeerManagerImpl::MaybeDisconnectForTxRelayCapacity(), invoked after a peer’s VERSION message, to preferentially evict an existing full-relay inbound peer when over capacity and only disconnect the new peer if no eviction candidate exists. Tests are updated to reflect the new slot arithmetic. There is no explicit security framing in the commit message or diff.
Changed components
src/net.cppsrc/net.hsrc/net_processing.cppsrc/init.cpptest/functional/p2p_eviction.pytest/functional/interface_usdt_net.pytest/functional/p2p_opportunistic_1p1c.pyInspect captured patch +73 / −9
diff --git a/src/init.cpp b/src/init.cpp
index d2e29020..7f708412 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -578,7 +578,11 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
argsman.AddArg("-forcednsseed", strprintf("Always query for peer addresses via DNS lookup (default: %u)", DEFAULT_FORCEDNSSEED), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-listen", strprintf("Accept connections from outside (default: %u if no -proxy, -connect or -maxconnections=0)", DEFAULT_LISTEN), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-listenonion", strprintf("Automatically create Tor onion service (default: %d)", DEFAULT_LISTEN_ONION), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
- argsman.AddArg("-maxconnections=<n>", strprintf("Maintain at most <n> automatic connections to peers (default: %u). This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u. It does not apply to short-lived private broadcast connections either, which have a separate limit of %u.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS, MAX_PRIVATE_BROADCAST_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
+ argsman.AddArg("-maxconnections=<n>", strprintf("Maintain at most <n> automatic connections to peers (default: %u). %u slots of these are reserved for outgoing connections, %u percent of the remaining ones can support transaction relay. "
+ "This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u. "
+ "It does not apply to short-lived private broadcast connections either, which have a separate limit of %u.",
+ DEFAULT_MAX_PEER_CONNECTIONS, MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS + MAX_FEELER_CONNECTIONS, static_cast<int>(100 * FULL_RELAY_INBOUND_PCT), MAX_ADDNODE_CONNECTIONS, MAX_PRIVATE_BROADCAST_CONNECTIONS),
+ ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxreceivebuffer=<n>", strprintf("Maximum per-connection receive buffer, <n>*1000 bytes (default: %u)", DEFAULT_MAXRECEIVEBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxsendbuffer=<n>", strprintf("Maximum per-connection memory usage for the send buffer, <n>*1000 bytes (default: %u)", DEFAULT_MAXSENDBUFFER), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxuploadtarget=<n>", strprintf("Tries to keep outbound traffic under the given target per 24h. Limit does not apply to peers with 'download' permission or blocks created within past week. 0 = no limit (default: %s). Optional suffix units [k|K|m|M|g|G|t|T] (default: M). Lowercase is 1000 base while uppercase is 1024 base", DEFAULT_MAX_UPLOAD_TARGET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
diff --git a/src/net.cpp b/src/net.cpp
index 74746ac1..c9cd94b1 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -2535,6 +2535,23 @@ int CConnman::GetExtraBlockRelayCount() const
return std::max(block_relay_peers - m_max_outbound_block_relay, 0);
}
+bool CConnman::EvictTxPeerIfFull(std::optional<NodeId> protect_peer)
+{
+ int tx_inbound_peers{0};
+ {
+ LOCK(m_nodes_mutex);
+ for (const CNode* pnode : m_nodes) {
+ if (!pnode->fDisconnect && pnode->IsInboundConn() && pnode->m_relays_txs) {
+ ++tx_inbound_peers;
+ }
+ }
+ }
+ if (tx_inbound_peers > m_max_inbound_full_relay) {
+ return AttemptToEvictConnection(/*evict_tx_relay_peer_only=*/true, protect_peer);
+ }
+ return true;
+}
+
std::unordered_set<Network> CConnman::GetReachableEmptyNetworks() const
{
std::unordered_set<Network> networks{};
diff --git a/src/net.h b/src/net.h
index 22f44df4..fa5768cb 100644
--- a/src/net.h
+++ b/src/net.h
@@ -78,7 +78,9 @@ static constexpr size_t MAX_PRIVATE_BROADCAST_CONNECTIONS{64};
/** -listen default */
static const bool DEFAULT_LISTEN = true;
/** The maximum number of peer connections to maintain. */
-static const unsigned int DEFAULT_MAX_PEER_CONNECTIONS = 125;
+static const unsigned int DEFAULT_MAX_PEER_CONNECTIONS{200};
+/** Percentage of inbound connection slots that tx-relaying peers can use */
+static const int FULL_RELAY_INBOUND_PCT{50};
/** The default for -maxuploadtarget. 0 = Unlimited */
static const std::string DEFAULT_MAX_UPLOAD_TARGET{"0M"};
/** Default for blocks only*/
@@ -1086,7 +1088,7 @@ public:
struct Options
{
ServiceFlags m_local_services = NODE_NONE;
- int m_max_automatic_connections = 0;
+ int m_max_automatic_connections = DEFAULT_MAX_PEER_CONNECTIONS;
CClientUIInterface* uiInterface = nullptr;
NetEventsInterface* m_msgproc = nullptr;
BanMan* m_banman = nullptr;
@@ -1122,6 +1124,7 @@ public:
m_max_outbound_block_relay = std::min(MAX_BLOCK_RELAY_ONLY_CONNECTIONS, m_max_automatic_connections - m_max_outbound_full_relay);
m_max_automatic_outbound = m_max_outbound_full_relay + m_max_outbound_block_relay + m_max_feeler;
m_max_inbound = std::max(0, m_max_automatic_connections - m_max_automatic_outbound);
+ m_max_inbound_full_relay = std::max(0, static_cast<int>(FULL_RELAY_INBOUND_PCT / 100.0 * m_max_inbound));
m_use_addrman_outgoing = connOptions.m_use_addrman_outgoing;
m_client_interface = connOptions.uiInterface;
m_banman = connOptions.m_banman;
@@ -1343,6 +1346,16 @@ public:
int GetExtraFullOutboundCount() const EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex);
// Count the number of block-relay-only peers we have over our limit.
int GetExtraBlockRelayCount() const EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex);
+ /**
+ * If we are at capacity for inbound tx-relay peers, attempt to evict one.
+ * @param[in] protect_peer NodeId of a peer we want to protect
+ * @return bool Returns true if successful (either there is
+ * no need for eviction, or a peer was evicted).
+ * Returns false, if we are full but couldn't find
+ * a peer to evict (all eligible peers are protected)
+ * so that the caller can deal with this.
+ */
+ bool EvictTxPeerIfFull(std::optional<NodeId> protect_peer = std::nullopt) EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex);
bool AddNode(const AddedNodeParams& add) EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
bool RemoveAddedNode(std::string_view node) EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
@@ -1720,6 +1733,7 @@ private:
int m_max_feeler{MAX_FEELER_CONNECTIONS};
int m_max_automatic_outbound;
int m_max_inbound;
+ int m_max_inbound_full_relay;
bool m_use_addrman_outgoing;
CClientUIInterface* m_client_interface;
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index c01f93c2..8905b158 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -602,6 +602,20 @@ private:
*/
bool MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer);
+ /** If an inbound peer wants tx relay and we are at capacity for those, attempt to
+ * evict a tx-relaying inbound peer - possibly node itself, unless it is protected.
+ * Only if no peer can be evicted, disconnect node.
+ *
+ * @param[in] node The node that wants to relay txs to us.
+ * @param[in] msg_type The message that triggered this check, for logging.
+ * @param[in] protect_peer Peer that is exempt from being evicted.
+ * @return True if the node was disconnected because no eviction candidate
+ * was found. If false is returned, a non-protected node may still have
+ * been marked for disconnection via regular eviction.
+ */
+ bool MaybeDisconnectForTxRelayCapacity(CNode& node, const std::string& msg_type,
+ std::optional<NodeId> protect_peer = std::nullopt);
+
/** Handle a transaction whose result was not MempoolAcceptResult::ResultType::VALID.
* @param[in] first_time_failure Whether we should consider inserting into vExtraTxnForCompact, adding
* a new orphan to resolve, or looking for a package to submit.
@@ -3772,6 +3786,9 @@ void PeerManagerImpl::ProcessMessage(Peer& peer, CNode& pfrom, const std::string
// MakeAndPushFeature(pfrom, NetMsgFeature::FOO, uint32_t{1});
}
+ // If we have too many tx-relaying inbound peers, attempt to evict an existing one.
+ // Only if this fails, disconnect this peer.
+ if (MaybeDisconnectForTxRelayCapacity(pfrom, msg_type, /*protect_peer=*/pfrom.GetId())) return;
MakeAndPushMessage(pfrom, NetMsgType::VERACK);
// Potentially mark this peer as a preferred download peer.
@@ -5138,6 +5155,16 @@ bool PeerManagerImpl::MaybeDiscourageAndDisconnect(CNode& pnode, Peer& peer)
return true;
}
+bool PeerManagerImpl::MaybeDisconnectForTxRelayCapacity(CNode& node, const std::string& msg_type, std::optional<NodeId> protect_peer)
+{
+ if (!node.IsInboundConn() || !node.m_relays_txs) return false;
+ if (m_connman.EvictTxPeerIfFull(protect_peer)) return false;
+
+ LogDebug(BCLog::NET, "failed to find a tx-relaying eviction candidate - connection dropped after %s message, peer=%d\n", msg_type, node.GetId());
+ node.fDisconnect = true;
+ return true;
+}
+
bool PeerManagerImpl::ProcessMessages(CNode& node, std::atomic<bool>& interruptMsgProc)
{
AssertLockNotHeld(m_tx_download_mutex);
diff --git a/test/functional/interface_usdt_net.py b/test/functional/interface_usdt_net.py
index de481133..6ec3d126 100755
--- a/test/functional/interface_usdt_net.py
+++ b/test/functional/interface_usdt_net.py
@@ -35,8 +35,8 @@ MAX_MSG_DATA_LENGTH = 150
# from net_address.h
NETWORK_TYPE_UNROUTABLE = 0
# Use in -maxconnections. Results in a maximum of 21 inbound connections
-MAX_CONNECTIONS = 32
-MAX_INBOUND_CONNECTIONS = MAX_CONNECTIONS - 10 - 1 # 10 outbound and 1 feeler
+MAX_CONNECTIONS = 53
+MAX_INBOUND_CONNECTIONS = 21 # 10 outbound and 1 feeler, (MAX_CONNECTIONS - 10 - 1) / 2 slots for tx-relaying inbounds
net_tracepoints_program = """
#include <uapi/linux/ptrace.h>
diff --git a/test/functional/p2p_eviction.py b/test/functional/p2p_eviction.py
index c96f2a2c..f297b8f3 100755
--- a/test/functional/p2p_eviction.py
+++ b/test/functional/p2p_eviction.py
@@ -45,10 +45,12 @@ class SlowP2PInterface(P2PInterface):
class P2PEvict(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
- # The choice of maxconnections=32 results in a maximum of 21 inbound connections
- # (32 - 10 outbound - 1 feeler). 20 inbound peers are protected from eviction:
+ # The choice of maxconnections=53 results in a maximum of 21 tx-relaying inbound connections
+ # (53 - 10 outbound - 1 feeler) * 0.5 = 21. The other inbound slots are reserved for block-relay-only
+ # peers that don't play a role in this test.
+ # 20 inbound peers are protected from eviction:
# 4 by netgroup, 4 that sent us blocks, 4 that sent us transactions and 8 via lowest ping time
- self.extra_args = [['-maxconnections=32']]
+ self.extra_args = [['-maxconnections=53']]
def run_test(self):
protected_peers = set() # peers that we expect to be protected from eviction
diff --git a/test/functional/p2p_opportunistic_1p1c.py b/test/functional/p2p_opportunistic_1p1c.py
index 77616de1..6841d7c3 100755
--- a/test/functional/p2p_opportunistic_1p1c.py
+++ b/test/functional/p2p_opportunistic_1p1c.py
@@ -76,7 +76,7 @@ class PackageRelayTest(BitcoinTestFramework):
self.setup_clean_chain = True
self.num_nodes = 1
self.extra_args = [[
- "-maxmempool=5",
+ "-maxmempool=5","-maxconnections=150"
]]
def create_tx_below_mempoolminfee(self, wallet, utxo_to_spend=None):
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.