Skip to content

Commit b713825

Browse files
committed
Merge bitcoin#27213: p2p: Diversify automatic outbound connections with respect to networks
1b52d16 p2p: network-specific management of outbound connections (Martin Zumsande) 65cff00 test: Add test for outbound protection by network (Martin Zumsande) 034f61f p2p: Protect extra full outbound peers by network (Martin Zumsande) 654d9bc p2p: Introduce data struct to track connection counts by network (Amiti Uttarwar) Pull request description: This is joint work with mzumsande. This is a proposal to diversify outbound connections with respect to reachable networks. The existing logic evaluates peers for connection based purely on the frequency of available addresses in `AddrMan`. This PR adds logic to automatically connect to alternate reachable networks and adds eviction logic that protects one existing connection to each network. For instance, if `AddrMan` is populated primarily with IPv4 and IPv6 addresses and only a handful of onion addresses, it is likely that we won't establish any automatic outbound connections to Tor, even if we're capable of doing so. For smaller networks like CJDNS, this is even more of an issue and often requires adding manual peers to ensure regularly being connected to the network. Connecting to multiple networks improves resistance to eclipse attacks for individual nodes. It also benefits the entire p2p network by increasing partition resistance and privacy in general. The automatic connections to alternate networks is done defensively, by first filling all outbound slots with random addresses (as in the status quo) and then adding additional peers from reachable networks the node is currently not connected to. This approach ensures that outbound slots are not left unfilled while attempting to connect to a network that may be unavailable due to a technical issue or misconfiguration that bitcoind cannot detect. Once an additional peer is added and we have one more outbound connection than we want, outbound eviction ensures that peers are protected if they are the only ones for their network. Manual connections are also taken into account: If a user already establishes manual connections to a trusted peer from a network, there is no longer a need to make extra efforts to ensure we also have an automatic connection to it (although this may of course happen by random selection). ACKs for top commit: naumenkogs: ACK 1b52d16 vasild: ACK 1b52d16 Tree-SHA512: 5616c038a5fbb868d4c46c5963cfd53e4599feee25db04b0e18da426d77d22e0994dc4e1da0b810f5b457f424ebbed3db1704f371aa6cad002b3565b20170ec0
2 parents d096743 + 1b52d16 commit b713825

File tree

5 files changed

+132
-4
lines changed

5 files changed

+132
-4
lines changed

src/net.cpp

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ static constexpr std::chrono::seconds MAX_UPLOAD_TIMEFRAME{60 * 60 * 24};
8383
// A random time period (0 to 1 seconds) is added to feeler connections to prevent synchronization.
8484
static constexpr auto FEELER_SLEEP_WINDOW{1s};
8585

86+
/** Frequency to attempt extra connections to reachable networks we're not connected to yet **/
87+
static constexpr auto EXTRA_NETWORK_PEER_INTERVAL{5min};
88+
8689
/** Used to pass flags to the Bind() function */
8790
enum BindFlags {
8891
BF_NONE = 0,
@@ -1138,6 +1141,9 @@ void CConnman::DisconnectNodes()
11381141
// close socket and cleanup
11391142
pnode->CloseSocketDisconnect();
11401143

1144+
// update connection count by network
1145+
if (pnode->IsManualOrFullOutboundConn()) --m_network_conn_counts[pnode->addr.GetNetwork()];
1146+
11411147
// hold in disconnected pool until all refs are released
11421148
pnode->Release();
11431149
m_nodes_disconnected.push_back(pnode);
@@ -1605,6 +1611,28 @@ std::unordered_set<Network> CConnman::GetReachableEmptyNetworks() const
16051611
return networks;
16061612
}
16071613

1614+
bool CConnman::MultipleManualOrFullOutboundConns(Network net) const
1615+
{
1616+
AssertLockHeld(m_nodes_mutex);
1617+
return m_network_conn_counts[net] > 1;
1618+
}
1619+
1620+
bool CConnman::MaybePickPreferredNetwork(std::optional<Network>& network)
1621+
{
1622+
std::array<Network, 5> nets{NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS};
1623+
Shuffle(nets.begin(), nets.end(), FastRandomContext());
1624+
1625+
LOCK(m_nodes_mutex);
1626+
for (const auto net : nets) {
1627+
if (IsReachable(net) && m_network_conn_counts[net] == 0 && addrman.Size(net) != 0) {
1628+
network = net;
1629+
return true;
1630+
}
1631+
}
1632+
1633+
return false;
1634+
}
1635+
16081636
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
16091637
{
16101638
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
@@ -1635,6 +1663,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
16351663
// Minimum time before next feeler connection (in microseconds).
16361664
auto next_feeler = GetExponentialRand(start, FEELER_INTERVAL);
16371665
auto next_extra_block_relay = GetExponentialRand(start, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL);
1666+
auto next_extra_network_peer{GetExponentialRand(start, EXTRA_NETWORK_PEER_INTERVAL)};
16381667
const bool dnsseed = gArgs.GetBoolArg("-dnsseed", DEFAULT_DNSSEED);
16391668
bool add_fixed_seeds = gArgs.GetBoolArg("-fixedseeds", DEFAULT_FIXEDSEEDS);
16401669
const bool use_seednodes{gArgs.IsArgSet("-seednode")};
@@ -1747,6 +1776,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
17471776
auto now = GetTime<std::chrono::microseconds>();
17481777
bool anchor = false;
17491778
bool fFeeler = false;
1779+
std::optional<Network> preferred_net;
17501780

17511781
// Determine what type of connection to open. Opening
17521782
// BLOCK_RELAY connections to addresses from anchors.dat gets the highest
@@ -1796,6 +1826,17 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
17961826
next_feeler = GetExponentialRand(now, FEELER_INTERVAL);
17971827
conn_type = ConnectionType::FEELER;
17981828
fFeeler = true;
1829+
} else if (nOutboundFullRelay == m_max_outbound_full_relay &&
1830+
m_max_outbound_full_relay == MAX_OUTBOUND_FULL_RELAY_CONNECTIONS &&
1831+
now > next_extra_network_peer &&
1832+
MaybePickPreferredNetwork(preferred_net)) {
1833+
// Full outbound connection management: Attempt to get at least one
1834+
// outbound peer from each reachable network by making extra connections
1835+
// and then protecting "only" peers from a network during outbound eviction.
1836+
// This is not attempted if the user changed -maxconnections to a value
1837+
// so low that less than MAX_OUTBOUND_FULL_RELAY_CONNECTIONS are made,
1838+
// to prevent interactions with otherwise protected outbound peers.
1839+
next_extra_network_peer = GetExponentialRand(now, EXTRA_NETWORK_PEER_INTERVAL);
17991840
} else {
18001841
// skip to next iteration of while loop
18011842
continue;
@@ -1849,7 +1890,10 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
18491890
}
18501891
} else {
18511892
// Not a feeler
1852-
std::tie(addr, addr_last_try) = addrman.Select();
1893+
// If preferred_net has a value set, pick an extra outbound
1894+
// peer from that network. The eviction logic in net_processing
1895+
// ensures that a peer from another network will be evicted.
1896+
std::tie(addr, addr_last_try) = addrman.Select(false, preferred_net);
18531897
}
18541898

18551899
// Require outbound IPv4/IPv6 connections, other than feelers, to be to distinct network groups
@@ -1896,6 +1940,9 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
18961940
}
18971941
LogPrint(BCLog::NET, "Making feeler connection to %s\n", addrConnect.ToStringAddrPort());
18981942
}
1943+
1944+
if (preferred_net != std::nullopt) LogPrint(BCLog::NET, "Making network specific connection to %s on %s.\n", addrConnect.ToStringAddrPort(), GetNetworkName(preferred_net.value()));
1945+
18991946
// Record addrman failure attempts when node has at least 2 persistent outbound connections to peers with
19001947
// different netgroups in ipv4/ipv6 networks + all peers in Tor/I2P/CJDNS networks.
19011948
// Don't record addrman failure attempts when node is offline. This can be identified since all local
@@ -2035,6 +2082,9 @@ void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFai
20352082
{
20362083
LOCK(m_nodes_mutex);
20372084
m_nodes.push_back(pnode);
2085+
2086+
// update connection count by network
2087+
if (pnode->IsManualOrFullOutboundConn()) ++m_network_conn_counts[pnode->addr.GetNetwork()];
20382088
}
20392089
}
20402090

src/net.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,22 @@ class CNode
465465
return m_conn_type == ConnectionType::MANUAL;
466466
}
467467

468+
bool IsManualOrFullOutboundConn() const
469+
{
470+
switch (m_conn_type) {
471+
case ConnectionType::INBOUND:
472+
case ConnectionType::FEELER:
473+
case ConnectionType::BLOCK_RELAY:
474+
case ConnectionType::ADDR_FETCH:
475+
return false;
476+
case ConnectionType::OUTBOUND_FULL_RELAY:
477+
case ConnectionType::MANUAL:
478+
return true;
479+
} // no default case, so the compiler can warn about missing cases
480+
481+
assert(false);
482+
}
483+
468484
bool IsBlockOnlyConn() const {
469485
return m_conn_type == ConnectionType::BLOCK_RELAY;
470486
}
@@ -777,6 +793,9 @@ class CConnman
777793
void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant* grantOutbound, const char* strDest, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
778794
bool CheckIncomingNonce(uint64_t nonce);
779795

796+
// alias for thread safety annotations only, not defined
797+
RecursiveMutex& GetNodesMutex() const LOCK_RETURNED(m_nodes_mutex);
798+
780799
bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func);
781800

782801
void PushMessage(CNode* pnode, CSerializedNetMsg&& msg) EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex);
@@ -893,6 +912,8 @@ class CConnman
893912
/** Return true if we should disconnect the peer for failing an inactivity check. */
894913
bool ShouldRunInactivityChecks(const CNode& node, std::chrono::seconds now) const;
895914

915+
bool MultipleManualOrFullOutboundConns(Network net) const EXCLUSIVE_LOCKS_REQUIRED(m_nodes_mutex);
916+
896917
private:
897918
struct ListenSocket {
898919
public:
@@ -1010,6 +1031,18 @@ class CConnman
10101031
*/
10111032
std::vector<CAddress> GetCurrentBlockRelayOnlyConns() const;
10121033

1034+
/**
1035+
* Search for a "preferred" network, a reachable network to which we
1036+
* currently don't have any OUTBOUND_FULL_RELAY or MANUAL connections.
1037+
* There needs to be at least one address in AddrMan for a preferred
1038+
* network to be picked.
1039+
*
1040+
* @param[out] network Preferred network, if found.
1041+
*
1042+
* @return bool Whether a preferred network was found.
1043+
*/
1044+
bool MaybePickPreferredNetwork(std::optional<Network>& network);
1045+
10131046
// Whether the node should be passed out in ForEach* callbacks
10141047
static bool NodeFullyConnected(const CNode* pnode);
10151048

@@ -1048,6 +1081,9 @@ class CConnman
10481081
std::atomic<NodeId> nLastNodeId{0};
10491082
unsigned int nPrevNodeCount{0};
10501083

1084+
// Stores number of full-tx connections (outbound and manual) per network
1085+
std::array<unsigned int, Network::NET_MAX> m_network_conn_counts GUARDED_BY(m_nodes_mutex) = {};
1086+
10511087
/**
10521088
* Cache responses to addr requests to minimize privacy leak.
10531089
* Attack example: scraping addrs in real-time may allow an attacker

src/net_processing.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5142,10 +5142,12 @@ void PeerManagerImpl::EvictExtraOutboundPeers(std::chrono::seconds now)
51425142
// Pick the outbound-full-relay peer that least recently announced
51435143
// us a new block, with ties broken by choosing the more recent
51445144
// connection (higher node id)
5145+
// Protect peers from eviction if we don't have another connection
5146+
// to their network, counting both outbound-full-relay and manual peers.
51455147
NodeId worst_peer = -1;
51465148
int64_t oldest_block_announcement = std::numeric_limits<int64_t>::max();
51475149

5148-
m_connman.ForEachNode([&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
5150+
m_connman.ForEachNode([&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_connman.GetNodesMutex()) {
51495151
AssertLockHeld(::cs_main);
51505152

51515153
// Only consider outbound-full-relay peers that are not already
@@ -5155,6 +5157,9 @@ void PeerManagerImpl::EvictExtraOutboundPeers(std::chrono::seconds now)
51555157
if (state == nullptr) return; // shouldn't be possible, but just in case
51565158
// Don't evict our protected peers
51575159
if (state->m_chain_sync.m_protect) return;
5160+
// If this is the only connection on a particular network that is
5161+
// OUTBOUND_FULL_RELAY or MANUAL, protect it.
5162+
if (!m_connman.MultipleManualOrFullOutboundConns(pnode->addr.GetNetwork())) return;
51585163
if (state->m_last_block_announcement < oldest_block_announcement || (state->m_last_block_announcement == oldest_block_announcement && pnode->GetId() > worst_peer)) {
51595164
worst_peer = pnode->GetId();
51605165
oldest_block_announcement = state->m_last_block_announcement;

src/test/denialofservice_tests.cpp

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,19 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
107107
peerman.FinalizeNode(dummyNode1);
108108
}
109109

110-
static void AddRandomOutboundPeer(NodeId& id, std::vector<CNode*>& vNodes, PeerManager& peerLogic, ConnmanTestMsg& connman, ConnectionType connType)
110+
static void AddRandomOutboundPeer(NodeId& id, std::vector<CNode*>& vNodes, PeerManager& peerLogic, ConnmanTestMsg& connman, ConnectionType connType, bool onion_peer = false)
111111
{
112-
CAddress addr(ip(g_insecure_rand_ctx.randbits(32)), NODE_NONE);
112+
CAddress addr;
113+
114+
if (onion_peer) {
115+
auto tor_addr{g_insecure_rand_ctx.randbytes(ADDR_TORV3_SIZE)};
116+
BOOST_REQUIRE(addr.SetSpecial(OnionToString(tor_addr)));
117+
}
118+
119+
while (!addr.IsRoutable()) {
120+
addr = CAddress(ip(g_insecure_rand_ctx.randbits(32)), NODE_NONE);
121+
}
122+
113123
vNodes.emplace_back(new CNode{id++,
114124
/*sock=*/nullptr,
115125
addr,
@@ -197,6 +207,30 @@ BOOST_AUTO_TEST_CASE(stale_tip_peer_management)
197207
BOOST_CHECK(vNodes[max_outbound_full_relay-1]->fDisconnect == true);
198208
BOOST_CHECK(vNodes.back()->fDisconnect == false);
199209

210+
vNodes[max_outbound_full_relay - 1]->fDisconnect = false;
211+
212+
// Add an onion peer, that will be protected because it is the only one for
213+
// its network, so another peer gets disconnected instead.
214+
SetMockTime(time_init);
215+
AddRandomOutboundPeer(id, vNodes, *peerLogic, *connman, ConnectionType::OUTBOUND_FULL_RELAY, /*onion_peer=*/true);
216+
SetMockTime(time_later);
217+
peerLogic->CheckForStaleTipAndEvictPeers();
218+
219+
for (int i = 0; i < max_outbound_full_relay - 2; ++i) {
220+
BOOST_CHECK(vNodes[i]->fDisconnect == false);
221+
}
222+
BOOST_CHECK(vNodes[max_outbound_full_relay - 2]->fDisconnect == false);
223+
BOOST_CHECK(vNodes[max_outbound_full_relay - 1]->fDisconnect == true);
224+
BOOST_CHECK(vNodes[max_outbound_full_relay]->fDisconnect == false);
225+
226+
// Add a second onion peer which won't be protected
227+
SetMockTime(time_init);
228+
AddRandomOutboundPeer(id, vNodes, *peerLogic, *connman, ConnectionType::OUTBOUND_FULL_RELAY, /*onion_peer=*/true);
229+
SetMockTime(time_later);
230+
peerLogic->CheckForStaleTipAndEvictPeers();
231+
232+
BOOST_CHECK(vNodes.back()->fDisconnect == true);
233+
200234
for (const CNode *node : vNodes) {
201235
peerLogic->FinalizeNode(*node);
202236
}

src/test/util/net.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ struct ConnmanTestMsg : public CConnman {
2929
{
3030
LOCK(m_nodes_mutex);
3131
m_nodes.push_back(&node);
32+
33+
if (node.IsManualOrFullOutboundConn()) ++m_network_conn_counts[node.addr.GetNetwork()];
3234
}
35+
3336
void ClearTestNodes()
3437
{
3538
LOCK(m_nodes_mutex);

0 commit comments

Comments
 (0)