This commit is contained in:
Martin Zumsande 2024-04-29 04:29:28 +02:00 committed by GitHub
commit 093986e401
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 168 additions and 9 deletions

View File

@ -520,7 +520,11 @@ void SetupServerArgs(ArgsManager& argsman)
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.", DEFAULT_MAX_PEER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-maxconnections=<n>", strprintf("Permit a maximum of <n> automatic connections with peers (default: %u). %u slots are reserved for outgoing connections. See -inboundrelaypercent for more information about limits applied to transaction relay inbound peers."
"This limit does not apply to connections manually added via -addnode or the addnode RPC, which have a separate limit of %u.",
DEFAULT_MAX_PEER_CONNECTIONS, MAX_OUTBOUND_FULL_RELAY_CONNECTIONS + MAX_BLOCK_RELAY_ONLY_CONNECTIONS + MAX_FEELER_CONNECTIONS, MAX_ADDNODE_CONNECTIONS),
ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-inboundrelaypercent=<n>", strprintf("Permit a maximum percent of inbound connections to relay transactions, to limit memory utilization (0 to 100, default: %u).", DEFAULT_FULL_RELAY_INBOUND_PCT), 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("-maxtimeadjustment", strprintf("Maximum allowed median peer time offset adjustment. Local perspective of time may be influenced by outbound peers forward or backward by this amount (default: %u seconds).", DEFAULT_MAX_TIME_ADJUSTMENT), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
@ -1810,6 +1814,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
CConnman::Options connOptions;
connOptions.nLocalServices = nLocalServices;
connOptions.m_max_automatic_connections = nMaxConnections;
connOptions.m_full_relay_inbound_percent = std::clamp<int>(args.GetIntArg("-inboundrelaypercent", DEFAULT_FULL_RELAY_INBOUND_PCT), 0, 100);
connOptions.uiInterface = &uiInterface;
connOptions.m_banman = node.banman.get();
connOptions.m_msgproc = node.peerman.get();

View File

@ -1651,7 +1651,7 @@ std::pair<size_t, bool> CConnman::SocketSendData(CNode& node) const
return {nSentSize, data_left};
}
/** Try to find a connection to evict when the node is full.
/** Try to find an inbound connection to evict.
* Extreme care must be taken to avoid opening the node to attacker
* triggered network partitioning.
* The strategy used here is to protect a small number of peers
@ -1659,7 +1659,7 @@ std::pair<size_t, bool> CConnman::SocketSendData(CNode& node) const
* to forge. In order to partition a node the attacker must be
* simultaneously better at all of them than honest peers.
*/
bool CConnman::AttemptToEvictConnection()
bool CConnman::AttemptToEvictConnection(bool evict_tx_relay_peer, std::optional<NodeId> protect_peer)
{
std::vector<NodeEvictionCandidate> vEvictionCandidates;
{
@ -1668,6 +1668,12 @@ bool CConnman::AttemptToEvictConnection()
for (const CNode* node : m_nodes) {
if (node->fDisconnect)
continue;
if (protect_peer.has_value() && node->GetId() == protect_peer) {
continue;
}
if (evict_tx_relay_peer && !node->m_relays_txs) {
continue;
}
NodeEvictionCandidate candidate{
.id = node->GetId(),
.m_connected = node->m_connected,
@ -2380,6 +2386,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->IsInboundConn() && pnode->m_relays_txs) {
++tx_inbound_peers;
}
}
}
if (tx_inbound_peers > m_max_inbound_full_relay) {
return AttemptToEvictConnection(/*evict_tx_relay_peer=*/true, protect_peer);
}
return true;
}
std::unordered_set<Network> CConnman::GetReachableEmptyNetworks() const
{
std::unordered_set<Network> networks{};

View File

@ -74,7 +74,9 @@ static const int MAX_FEELER_CONNECTIONS = 1;
/** -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};
/** Default percentage of inbound connection slots that tx-relaying peers can use */
static const int DEFAULT_FULL_RELAY_INBOUND_PCT{50};
/** The default for -maxuploadtarget. 0 = Unlimited */
static const std::string DEFAULT_MAX_UPLOAD_TARGET{"0M"};
/** Default for blocks only*/
@ -1040,6 +1042,7 @@ public:
{
ServiceFlags nLocalServices = NODE_NONE;
int m_max_automatic_connections = 0;
int m_full_relay_inbound_percent = 0;
CClientUIInterface* uiInterface = nullptr;
NetEventsInterface* m_msgproc = nullptr;
BanMan* m_banman = nullptr;
@ -1074,6 +1077,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>(connOptions.m_full_relay_inbound_percent / 100.0 * m_max_inbound));
m_use_addrman_outgoing = connOptions.m_use_addrman_outgoing;
m_client_interface = connOptions.uiInterface;
m_banman = connOptions.m_banman;
@ -1184,6 +1188,16 @@ public:
int GetExtraFullOutboundCount() const;
// Count the number of block-relay-only peers we have over our limit.
int GetExtraBlockRelayCount() const;
/**
* 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);
bool AddNode(const AddedNodeParams& add) EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
bool RemoveAddedNode(const std::string& node) EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
@ -1338,7 +1352,13 @@ private:
*/
bool AlreadyConnectedToAddress(const CAddress& addr);
bool AttemptToEvictConnection();
/**
* Try to find an inbound connection to evict.
* @param[in] evict_tx_relay_peer Whether to only select full relay peers for eviction
* @param[in] protect_peer Protect peer with node id
* @return True if a node was marked for disconnect
*/
bool AttemptToEvictConnection(bool evict_tx_relay_peer = false, std::optional<NodeId> protect_peer = std::nullopt);
CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type, bool use_v2transport) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr, const std::vector<NetWhitelistPermissions>& ranges) const;
@ -1492,6 +1512,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;

View File

@ -3607,6 +3607,15 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
}
}
// If we have too many tx-relaying inbound peers, attempt to evict an existing one.
// Only if this fails, disconnect this peer.
if (pfrom.IsInboundConn() && pfrom.m_relays_txs) {
if (!m_connman.EvictTxPeerIfFull(/*protect_peer=*/pfrom.GetId())) {
LogPrint(BCLog::NET, "failed to find a tx-relaying eviction candidate - connection dropped peer=%i\n", pfrom.GetId());
pfrom.fDisconnect = true;
return;
}
}
MakeAndPushMessage(pfrom, NetMsgType::VERACK);
// Potentially mark this peer as a preferred download peer.
@ -4938,6 +4947,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
}
pfrom.m_bloom_filter_loaded = true;
pfrom.m_relays_txs = true;
if (pfrom.IsInboundConn() && !m_connman.EvictTxPeerIfFull()) {
// We don't have room for another tx-relay peer, disconnect
LogPrint(BCLog::NET, "filterload received, but no capacity for tx-relay and no other peer to evict. disconnecting peer=%d\n", pfrom.GetId());
pfrom.fDisconnect = true;
};
}
return;
}
@ -4986,6 +5000,11 @@ void PeerManagerImpl::ProcessMessage(CNode& pfrom, const std::string& msg_type,
}
pfrom.m_bloom_filter_loaded = false;
pfrom.m_relays_txs = true;
if (pfrom.IsInboundConn() && !m_connman.EvictTxPeerIfFull()) {
// We don't have room for another tx-relay peer, disconnect
LogPrint(BCLog::NET, "filterclear received, but no capacity for tx-relay and no other peer to evict. disconnecting peer=%d\n", pfrom.GetId());
pfrom.fDisconnect = true;
};
return;
}

View File

@ -0,0 +1,86 @@
#!/usr/bin/env python3
# Copyright (c) 2023-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
from test_framework.test_framework import BitcoinTestFramework
from test_framework.messages import (
msg_version,
msg_filterload
)
from test_framework.p2p import (
P2PInterface,
P2P_SERVICES,
P2P_SUBVERSION,
P2P_VERSION,
)
class P2PConnectionLimits(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
# scenario : we have 2 inbound slots and allow a maximum of 1 tx-relaying inbound peer
self.extra_args = [['-maxconnections=13']] # 11 slots are reserved for outbounds, leaving 2 inbound slots
def run_test(self):
self.test_inbound_limits()
def create_blocks_only_version(self):
no_txrelay_version_msg = msg_version()
no_txrelay_version_msg.nVersion = P2P_VERSION
no_txrelay_version_msg.strSubVer = P2P_SUBVERSION
no_txrelay_version_msg.nServices = P2P_SERVICES
no_txrelay_version_msg.relay = 0
return no_txrelay_version_msg
def test_inbound_limits(self):
node = self.nodes[0]
self.log.info('Test with 2 inbound slots, one of which allows tx-relay')
node.add_p2p_connection(P2PInterface())
self.log.info('Connect a full-relay inbound peer - test that eviction is triggered')
# Since there is no unprotected peer to evict here, the new peer is dropped instead.
with node.assert_debug_log(['failed to find a tx-relaying eviction candidate - connection dropped']):
self.nodes[0].add_p2p_connection(P2PInterface(), expect_success=False, wait_for_verack=False)
self.wait_until(lambda: len(node.getpeerinfo()) == 1)
node.disconnect_p2ps()
self.log.info('Connect a block-relay inbound peer - test that second full relay peer is accepted')
peer1 = self.nodes[0].add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False)
peer1.send_message(self.create_blocks_only_version())
peer1.wait_for_verack()
node.add_p2p_connection(P2PInterface())
self.wait_until(lambda: len(node.getpeerinfo()) == 2)
self.log.info('Connecting another full-relay peer triggers non-specific eviction')
with node.assert_debug_log(['failed to find an eviction candidate - connection dropped (full)']):
self.nodes[0].add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False, expect_success=False)
self.wait_until(lambda: len(node.getpeerinfo()) == 2)
self.log.info('Run with bloom filter support and check that a switch to tx relay during runtime can trigger eviction')
self.restart_node(0, ['-maxconnections=13', '-peerbloomfilters'])
peer1 = self.nodes[0].add_p2p_connection(P2PInterface(), send_version=False, wait_for_verack=False)
peer1.send_message(self.create_blocks_only_version())
peer1.wait_for_verack()
node.add_p2p_connection(P2PInterface())
self.wait_until(lambda: len(node.getpeerinfo()) == 2)
with node.assert_debug_log(['filterload received, but no capacity for tx-relay and no other peer to evict. disconnecting peer']):
peer1.send_message(msg_filterload(data=b'\xbb'*(100)))
self.wait_until(lambda: len(node.getpeerinfo()) == 1)
self.log.info('Test different values of inboundrelaypercent')
self.restart_node(0, ['-maxconnections=13', '-inboundrelaypercent=0'])
with node.assert_debug_log(['failed to find a tx-relaying eviction candidate - connection dropped']):
self.nodes[0].add_p2p_connection(P2PInterface(), expect_success=False, wait_for_verack=False)
self.restart_node(0, ['-maxconnections=13', '-inboundrelaypercent=100'])
node.add_p2p_connection(P2PInterface())
node.add_p2p_connection(P2PInterface())
self.wait_until(lambda: len(node.getpeerinfo()) == 2)
if __name__ == '__main__':
P2PConnectionLimits().main()

View File

@ -46,10 +46,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

View File

@ -665,7 +665,7 @@ class TestNode():
assert_msg += "with expected error " + expected_msg
self._raise_assertion_error(assert_msg)
def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, **kwargs):
def add_p2p_connection(self, p2p_conn, *, wait_for_verack=True, send_version=True, supports_v2_p2p=None, wait_for_v2_handshake=True, expect_success=True, **kwargs):
"""Add an inbound p2p connection to the node.
This method adds the p2p connection to the self.p2ps list and also
@ -693,6 +693,8 @@ class TestNode():
p2p_conn.peer_connect(**kwargs, send_version=send_version, net=self.chain, timeout_factor=self.timeout_factor, supports_v2_p2p=supports_v2_p2p)()
self.p2ps.append(p2p_conn)
if not expect_success:
return p2p_conn
p2p_conn.wait_until(lambda: p2p_conn.is_connected, check_connected=False)
if supports_v2_p2p and wait_for_v2_handshake:
p2p_conn.wait_until(lambda: p2p_conn.v2_state.tried_v2_handshake)

View File

@ -360,6 +360,7 @@ BASE_SCRIPTS = [
'p2p_tx_privacy.py',
'rpc_scanblocks.py',
'p2p_sendtxrcncl.py',
'p2p_connection_limits.py',
'rpc_scantxoutset.py',
'feature_unsupported_utxo_db.py',
'feature_logging.py',