aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/net.cpp52
-rw-r--r--src/net.h36
-rw-r--r--src/net_processing.cpp7
-rw-r--r--src/test/denialofservice_tests.cpp38
-rw-r--r--src/test/util/net.h3
5 files changed, 132 insertions, 4 deletions
diff --git a/src/net.cpp b/src/net.cpp
index 1eda51e9df..b51043ba27 100644
--- a/src/net.cpp
+++ b/src/net.cpp
@@ -83,6 +83,9 @@ static constexpr std::chrono::seconds MAX_UPLOAD_TIMEFRAME{60 * 60 * 24};
// A random time period (0 to 1 seconds) is added to feeler connections to prevent synchronization.
static constexpr auto FEELER_SLEEP_WINDOW{1s};
+/** Frequency to attempt extra connections to reachable networks we're not connected to yet **/
+static constexpr auto EXTRA_NETWORK_PEER_INTERVAL{5min};
+
/** Used to pass flags to the Bind() function */
enum BindFlags {
BF_NONE = 0,
@@ -1138,6 +1141,9 @@ void CConnman::DisconnectNodes()
// close socket and cleanup
pnode->CloseSocketDisconnect();
+ // update connection count by network
+ if (pnode->IsManualOrFullOutboundConn()) --m_network_conn_counts[pnode->addr.GetNetwork()];
+
// hold in disconnected pool until all refs are released
pnode->Release();
m_nodes_disconnected.push_back(pnode);
@@ -1605,6 +1611,28 @@ std::unordered_set<Network> CConnman::GetReachableEmptyNetworks() const
return networks;
}
+bool CConnman::MultipleManualOrFullOutboundConns(Network net) const
+{
+ AssertLockHeld(m_nodes_mutex);
+ return m_network_conn_counts[net] > 1;
+}
+
+bool CConnman::MaybePickPreferredNetwork(std::optional<Network>& network)
+{
+ std::array<Network, 5> nets{NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS};
+ Shuffle(nets.begin(), nets.end(), FastRandomContext());
+
+ LOCK(m_nodes_mutex);
+ for (const auto net : nets) {
+ if (IsReachable(net) && m_network_conn_counts[net] == 0 && addrman.Size(net) != 0) {
+ network = net;
+ return true;
+ }
+ }
+
+ return false;
+}
+
void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
{
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
@@ -1635,6 +1663,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
// Minimum time before next feeler connection (in microseconds).
auto next_feeler = GetExponentialRand(start, FEELER_INTERVAL);
auto next_extra_block_relay = GetExponentialRand(start, EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL);
+ auto next_extra_network_peer{GetExponentialRand(start, EXTRA_NETWORK_PEER_INTERVAL)};
const bool dnsseed = gArgs.GetBoolArg("-dnsseed", DEFAULT_DNSSEED);
bool add_fixed_seeds = gArgs.GetBoolArg("-fixedseeds", DEFAULT_FIXEDSEEDS);
const bool use_seednodes{gArgs.IsArgSet("-seednode")};
@@ -1747,6 +1776,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
auto now = GetTime<std::chrono::microseconds>();
bool anchor = false;
bool fFeeler = false;
+ std::optional<Network> preferred_net;
// Determine what type of connection to open. Opening
// BLOCK_RELAY connections to addresses from anchors.dat gets the highest
@@ -1796,6 +1826,17 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
next_feeler = GetExponentialRand(now, FEELER_INTERVAL);
conn_type = ConnectionType::FEELER;
fFeeler = true;
+ } else if (nOutboundFullRelay == m_max_outbound_full_relay &&
+ m_max_outbound_full_relay == MAX_OUTBOUND_FULL_RELAY_CONNECTIONS &&
+ now > next_extra_network_peer &&
+ MaybePickPreferredNetwork(preferred_net)) {
+ // Full outbound connection management: Attempt to get at least one
+ // outbound peer from each reachable network by making extra connections
+ // and then protecting "only" peers from a network during outbound eviction.
+ // This is not attempted if the user changed -maxconnections to a value
+ // so low that less than MAX_OUTBOUND_FULL_RELAY_CONNECTIONS are made,
+ // to prevent interactions with otherwise protected outbound peers.
+ next_extra_network_peer = GetExponentialRand(now, EXTRA_NETWORK_PEER_INTERVAL);
} else {
// skip to next iteration of while loop
continue;
@@ -1849,7 +1890,10 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect)
}
} else {
// Not a feeler
- std::tie(addr, addr_last_try) = addrman.Select();
+ // If preferred_net has a value set, pick an extra outbound
+ // peer from that network. The eviction logic in net_processing
+ // ensures that a peer from another network will be evicted.
+ std::tie(addr, addr_last_try) = addrman.Select(false, preferred_net);
}
// 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)
}
LogPrint(BCLog::NET, "Making feeler connection to %s\n", addrConnect.ToStringAddrPort());
}
+
+ if (preferred_net != std::nullopt) LogPrint(BCLog::NET, "Making network specific connection to %s on %s.\n", addrConnect.ToStringAddrPort(), GetNetworkName(preferred_net.value()));
+
// Record addrman failure attempts when node has at least 2 persistent outbound connections to peers with
// different netgroups in ipv4/ipv6 networks + all peers in Tor/I2P/CJDNS networks.
// 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
{
LOCK(m_nodes_mutex);
m_nodes.push_back(pnode);
+
+ // update connection count by network
+ if (pnode->IsManualOrFullOutboundConn()) ++m_network_conn_counts[pnode->addr.GetNetwork()];
}
}
diff --git a/src/net.h b/src/net.h
index 7427d0f45b..1ea0ad868a 100644
--- a/src/net.h
+++ b/src/net.h
@@ -465,6 +465,22 @@ public:
return m_conn_type == ConnectionType::MANUAL;
}
+ bool IsManualOrFullOutboundConn() const
+ {
+ switch (m_conn_type) {
+ case ConnectionType::INBOUND:
+ case ConnectionType::FEELER:
+ case ConnectionType::BLOCK_RELAY:
+ case ConnectionType::ADDR_FETCH:
+ return false;
+ case ConnectionType::OUTBOUND_FULL_RELAY:
+ case ConnectionType::MANUAL:
+ return true;
+ } // no default case, so the compiler can warn about missing cases
+
+ assert(false);
+ }
+
bool IsBlockOnlyConn() const {
return m_conn_type == ConnectionType::BLOCK_RELAY;
}
@@ -777,6 +793,9 @@ public:
void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant* grantOutbound, const char* strDest, ConnectionType conn_type) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex);
bool CheckIncomingNonce(uint64_t nonce);
+ // alias for thread safety annotations only, not defined
+ RecursiveMutex& GetNodesMutex() const LOCK_RETURNED(m_nodes_mutex);
+
bool ForNode(NodeId id, std::function<bool(CNode* pnode)> func);
void PushMessage(CNode* pnode, CSerializedNetMsg&& msg) EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex);
@@ -893,6 +912,8 @@ public:
/** Return true if we should disconnect the peer for failing an inactivity check. */
bool ShouldRunInactivityChecks(const CNode& node, std::chrono::seconds now) const;
+ bool MultipleManualOrFullOutboundConns(Network net) const EXCLUSIVE_LOCKS_REQUIRED(m_nodes_mutex);
+
private:
struct ListenSocket {
public:
@@ -1010,6 +1031,18 @@ private:
*/
std::vector<CAddress> GetCurrentBlockRelayOnlyConns() const;
+ /**
+ * Search for a "preferred" network, a reachable network to which we
+ * currently don't have any OUTBOUND_FULL_RELAY or MANUAL connections.
+ * There needs to be at least one address in AddrMan for a preferred
+ * network to be picked.
+ *
+ * @param[out] network Preferred network, if found.
+ *
+ * @return bool Whether a preferred network was found.
+ */
+ bool MaybePickPreferredNetwork(std::optional<Network>& network);
+
// Whether the node should be passed out in ForEach* callbacks
static bool NodeFullyConnected(const CNode* pnode);
@@ -1048,6 +1081,9 @@ private:
std::atomic<NodeId> nLastNodeId{0};
unsigned int nPrevNodeCount{0};
+ // Stores number of full-tx connections (outbound and manual) per network
+ std::array<unsigned int, Network::NET_MAX> m_network_conn_counts GUARDED_BY(m_nodes_mutex) = {};
+
/**
* Cache responses to addr requests to minimize privacy leak.
* Attack example: scraping addrs in real-time may allow an attacker
diff --git a/src/net_processing.cpp b/src/net_processing.cpp
index be6777d14b..adae85a5c4 100644
--- a/src/net_processing.cpp
+++ b/src/net_processing.cpp
@@ -5142,10 +5142,12 @@ void PeerManagerImpl::EvictExtraOutboundPeers(std::chrono::seconds now)
// Pick the outbound-full-relay peer that least recently announced
// us a new block, with ties broken by choosing the more recent
// connection (higher node id)
+ // Protect peers from eviction if we don't have another connection
+ // to their network, counting both outbound-full-relay and manual peers.
NodeId worst_peer = -1;
int64_t oldest_block_announcement = std::numeric_limits<int64_t>::max();
- m_connman.ForEachNode([&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
+ m_connman.ForEachNode([&](CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, m_connman.GetNodesMutex()) {
AssertLockHeld(::cs_main);
// Only consider outbound-full-relay peers that are not already
@@ -5155,6 +5157,9 @@ void PeerManagerImpl::EvictExtraOutboundPeers(std::chrono::seconds now)
if (state == nullptr) return; // shouldn't be possible, but just in case
// Don't evict our protected peers
if (state->m_chain_sync.m_protect) return;
+ // If this is the only connection on a particular network that is
+ // OUTBOUND_FULL_RELAY or MANUAL, protect it.
+ if (!m_connman.MultipleManualOrFullOutboundConns(pnode->addr.GetNetwork())) return;
if (state->m_last_block_announcement < oldest_block_announcement || (state->m_last_block_announcement == oldest_block_announcement && pnode->GetId() > worst_peer)) {
worst_peer = pnode->GetId();
oldest_block_announcement = state->m_last_block_announcement;
diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp
index 9193d9a8b3..b740a51574 100644
--- a/src/test/denialofservice_tests.cpp
+++ b/src/test/denialofservice_tests.cpp
@@ -107,9 +107,19 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction)
peerman.FinalizeNode(dummyNode1);
}
-static void AddRandomOutboundPeer(NodeId& id, std::vector<CNode*>& vNodes, PeerManager& peerLogic, ConnmanTestMsg& connman, ConnectionType connType)
+static void AddRandomOutboundPeer(NodeId& id, std::vector<CNode*>& vNodes, PeerManager& peerLogic, ConnmanTestMsg& connman, ConnectionType connType, bool onion_peer = false)
{
- CAddress addr(ip(g_insecure_rand_ctx.randbits(32)), NODE_NONE);
+ CAddress addr;
+
+ if (onion_peer) {
+ auto tor_addr{g_insecure_rand_ctx.randbytes(ADDR_TORV3_SIZE)};
+ BOOST_REQUIRE(addr.SetSpecial(OnionToString(tor_addr)));
+ }
+
+ while (!addr.IsRoutable()) {
+ addr = CAddress(ip(g_insecure_rand_ctx.randbits(32)), NODE_NONE);
+ }
+
vNodes.emplace_back(new CNode{id++,
/*sock=*/nullptr,
addr,
@@ -197,6 +207,30 @@ BOOST_AUTO_TEST_CASE(stale_tip_peer_management)
BOOST_CHECK(vNodes[max_outbound_full_relay-1]->fDisconnect == true);
BOOST_CHECK(vNodes.back()->fDisconnect == false);
+ vNodes[max_outbound_full_relay - 1]->fDisconnect = false;
+
+ // Add an onion peer, that will be protected because it is the only one for
+ // its network, so another peer gets disconnected instead.
+ SetMockTime(time_init);
+ AddRandomOutboundPeer(id, vNodes, *peerLogic, *connman, ConnectionType::OUTBOUND_FULL_RELAY, /*onion_peer=*/true);
+ SetMockTime(time_later);
+ peerLogic->CheckForStaleTipAndEvictPeers();
+
+ for (int i = 0; i < max_outbound_full_relay - 2; ++i) {
+ BOOST_CHECK(vNodes[i]->fDisconnect == false);
+ }
+ BOOST_CHECK(vNodes[max_outbound_full_relay - 2]->fDisconnect == false);
+ BOOST_CHECK(vNodes[max_outbound_full_relay - 1]->fDisconnect == true);
+ BOOST_CHECK(vNodes[max_outbound_full_relay]->fDisconnect == false);
+
+ // Add a second onion peer which won't be protected
+ SetMockTime(time_init);
+ AddRandomOutboundPeer(id, vNodes, *peerLogic, *connman, ConnectionType::OUTBOUND_FULL_RELAY, /*onion_peer=*/true);
+ SetMockTime(time_later);
+ peerLogic->CheckForStaleTipAndEvictPeers();
+
+ BOOST_CHECK(vNodes.back()->fDisconnect == true);
+
for (const CNode *node : vNodes) {
peerLogic->FinalizeNode(*node);
}
diff --git a/src/test/util/net.h b/src/test/util/net.h
index e6506b0d08..b2f6ebb163 100644
--- a/src/test/util/net.h
+++ b/src/test/util/net.h
@@ -29,7 +29,10 @@ struct ConnmanTestMsg : public CConnman {
{
LOCK(m_nodes_mutex);
m_nodes.push_back(&node);
+
+ if (node.IsManualOrFullOutboundConn()) ++m_network_conn_counts[node.addr.GetNetwork()];
}
+
void ClearTestNodes()
{
LOCK(m_nodes_mutex);