mirror of https://github.com/bitcoin/bitcoin
Merge 6490355409
into a46065e36c
This commit is contained in:
commit
093986e401
|
@ -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();
|
||||
|
|
27
src/net.cpp
27
src/net.cpp
|
@ -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{};
|
||||
|
|
25
src/net.h
25
src/net.h
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue