Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backport: merge bitcoin#22778, #25156, #26497, #27213, #28189, #28155, #28895, partial bitcoin#26396 (networking backports: part 9) #6365

Merged
merged 10 commits into from
Oct 27, 2024
Merged
8 changes: 8 additions & 0 deletions doc/release-notes-6365.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
P2P and network changes
------

- Nodes with multiple reachable networks will actively try to have at least one
outbound connection to each network. This improves individual resistance to
eclipse attacks and network level resistance to partition attacks. Users no
longer need to perform active measures to ensure being connected to multiple
enabled networks.
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ BITCOIN_TESTS =\
test/minisketch_tests.cpp \
test/miner_tests.cpp \
test/multisig_tests.cpp \
test/net_peer_connection_tests.cpp \
test/net_peer_eviction_tests.cpp \
test/net_tests.cpp \
test/netbase_tests.cpp \
Expand Down
4 changes: 3 additions & 1 deletion src/Makefile.test_fuzz.include
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ TEST_FUZZ_H = \
test/fuzz/fuzz.h \
test/fuzz/FuzzedDataProvider.h \
test/fuzz/util.h \
test/util/mining.h
test/util/mining.h \
test/fuzz/util/net.h

libtest_fuzz_a_CPPFLAGS = $(AM_CPPFLAGS) $(BITCOIN_INCLUDES) $(MINIUPNPC_CPPFLAGS) $(EVENT_CFLAGS) $(EVENT_PTHREADS_CFLAGS)
libtest_fuzz_a_CXXFLAGS = $(AM_CXXFLAGS) $(PIE_FLAGS)
libtest_fuzz_a_SOURCES = \
test/fuzz/fuzz.cpp \
test/util/mining.cpp \
test/fuzz/util.cpp \
test/fuzz/util/net.cpp \
$(TEST_FUZZ_H)

LIBTEST_FUZZ += $(LIBBITCOIN_SERVER)
Expand Down
141 changes: 112 additions & 29 deletions src/net.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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,
Expand Down Expand Up @@ -520,21 +523,25 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo
const uint16_t default_port{pszDest != nullptr ? Params().GetDefaultPort(pszDest) :
Params().GetDefaultPort()};
if (pszDest) {
const std::vector<CService> resolved{Lookup(pszDest, default_port, fNameLookup && !HaveNameProxy(), 256)};
std::vector<CService> resolved{Lookup(pszDest, default_port, fNameLookup && !HaveNameProxy(), 256)};
if (!resolved.empty()) {
const CService& rnd{resolved[GetRand(resolved.size())]};
addrConnect = CAddress{MaybeFlipIPv6toCJDNS(rnd), NODE_NONE};
if (!addrConnect.IsValid()) {
LogPrint(BCLog::NET, "Resolver returned invalid address %s for %s\n", addrConnect.ToStringAddrPort(), pszDest);
return nullptr;
}
// It is possible that we already have a connection to the IP/port pszDest resolved to.
// In that case, drop the connection that was just created.
LOCK(m_nodes_mutex);
CNode* pnode = FindNode(static_cast<CService>(addrConnect));
if (pnode) {
LogPrintf("Failed to open new connection, already connected\n");
return nullptr;
Shuffle(resolved.begin(), resolved.end(), FastRandomContext());
// If the connection is made by name, it can be the case that the name resolves to more than one address.
// We don't want to connect any more of them if we are already connected to one
for (const auto& r : resolved) {
addrConnect = CAddress{MaybeFlipIPv6toCJDNS(r), NODE_NONE};
if (!addrConnect.IsValid()) {
LogPrint(BCLog::NET, "Resolver returned invalid address %s for %s\n", addrConnect.ToStringAddrPort(), pszDest);
return nullptr;
}
// It is possible that we already have a connection to the IP/port pszDest resolved to.
// In that case, drop the connection that was just created.
LOCK(m_nodes_mutex);
CNode* pnode = FindNode(static_cast<CService>(addrConnect));
if (pnode) {
LogPrintf("Not opening a connection to %s, already connected to %s\n", pszDest, addrConnect.ToStringAddrPort());
return nullptr;
}
}
}
}
Expand Down Expand Up @@ -2252,6 +2259,9 @@ void CConnman::DisconnectNodes()
// close socket and cleanup
pnode->CloseSocketDisconnect(this);

// 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);
Expand Down Expand Up @@ -3178,6 +3188,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, CDeterministicMNManager& dmnman)
{
AssertLockNotHeld(m_unused_i2p_sessions_mutex);
Expand Down Expand Up @@ -3213,6 +3245,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, CDe
// 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")};
Expand Down Expand Up @@ -3341,6 +3374,7 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, CDe
auto now = GetTime<std::chrono::microseconds>();
bool anchor = false;
bool fFeeler = false;
std::optional<Network> preferred_net;
bool onion_only = false;

// Determine what type of connection to open. Opening
Expand Down Expand Up @@ -3391,6 +3425,17 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, CDe
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 if (nOutboundOnionRelay < m_max_outbound_onion && IsReachable(Network::NET_ONION)) {
onion_only = true;
} else {
Expand Down Expand Up @@ -3448,7 +3493,10 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, CDe
}
} 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);
}

auto dmn = mnList.GetMNByService(addr);
Expand Down Expand Up @@ -3499,6 +3547,17 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, CDe
continue;
}

// Do not make automatic outbound connections to addnode peers, to
// not use our limited outbound slots for them and to ensure
// addnode connections benefit from their intended protections.
if (AddedNodesContain(addr)) {
LogPrint(BCLog::NET, "Not making automatic %s%s connection to %s peer selected for manual (addnode) connection%s\n",
preferred_net.has_value() ? "network-specific " : "",
ConnectionTypeAsString(conn_type), GetNetworkName(addr.GetNetwork()),
fLogIPs ? strprintf(": %s", addr.ToStringAddrPort()) : "");
continue;
}

addrConnect = addr;
break;
}
Expand All @@ -3515,6 +3574,9 @@ void CConnman::ThreadOpenConnections(const std::vector<std::string> connect, CDe
LogPrint(BCLog::NET, "Making feeler connection\n");
}
}

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
Expand All @@ -3540,7 +3602,7 @@ std::vector<CAddress> CConnman::GetCurrentBlockRelayOnlyConns() const
return ret;
}

std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const
std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo(bool include_connected) const
{
std::vector<AddedNodeInfo> ret;

Expand Down Expand Up @@ -3575,6 +3637,9 @@ std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const
// strAddNode is an IP:port
auto it = mapConnected.find(service);
if (it != mapConnected.end()) {
if (!include_connected) {
continue;
}
addedNode.resolvedAddress = service;
addedNode.fConnected = true;
addedNode.fInbound = it->second;
Expand All @@ -3583,6 +3648,9 @@ std::vector<AddedNodeInfo> CConnman::GetAddedNodeInfo() const
// strAddNode is a name
auto it = mapConnectedByName.find(addr.m_added_node);
if (it != mapConnectedByName.end()) {
if (!include_connected) {
continue;
}
addedNode.resolvedAddress = it->second.second;
addedNode.fConnected = true;
addedNode.fInbound = it->second.first;
Expand All @@ -3601,21 +3669,19 @@ void CConnman::ThreadOpenAddedConnections()
while (true)
{
CSemaphoreGrant grant(*semAddnode);
std::vector<AddedNodeInfo> vInfo = GetAddedNodeInfo();
std::vector<AddedNodeInfo> vInfo = GetAddedNodeInfo(/*include_connected=*/false);
bool tried = false;
for (const AddedNodeInfo& info : vInfo) {
if (!info.fConnected) {
if (!grant) {
// If we've used up our semaphore and need a new one, let's not wait here since while we are waiting
// the addednodeinfo state might change.
break;
}
tried = true;
CAddress addr(CService(), NODE_NONE);
OpenNetworkConnection(addr, false, std::move(grant), info.m_params.m_added_node.c_str(), ConnectionType::MANUAL, info.m_params.m_use_v2transport);
if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) return;
grant = CSemaphoreGrant(*semAddnode, /*fTry=*/true);
if (!grant) {
// If we've used up our semaphore and need a new one, let's not wait here since while we are waiting
// the addednodeinfo state might change.
break;
}
tried = true;
CAddress addr(CService(), NODE_NONE);
OpenNetworkConnection(addr, false, std::move(grant), info.m_params.m_added_node.c_str(), ConnectionType::MANUAL, info.m_params.m_use_v2transport);
if (!interruptNet.sleep_for(std::chrono::milliseconds(500))) return;
grant = CSemaphoreGrant(*semAddnode, /*fTry=*/true);
}
// See if any reconnections are desired.
PerformReconnections();
Expand Down Expand Up @@ -3877,6 +3943,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()];
}
{
if (m_edge_trig_events) {
Expand Down Expand Up @@ -4499,9 +4568,12 @@ std::vector<CAddress> CConnman::GetAddresses(CNode& requestor, size_t max_addres

bool CConnman::AddNode(const AddedNodeParams& add)
{
const CService resolved(LookupNumeric(add.m_added_node, Params().GetDefaultPort(add.m_added_node)));
const bool resolved_is_valid{resolved.IsValid()};

LOCK(m_added_nodes_mutex);
for (const auto& it : m_added_node_params) {
if (add.m_added_node == it.m_added_node) return false;
if (add.m_added_node == it.m_added_node || (resolved_is_valid && resolved == LookupNumeric(it.m_added_node, Params().GetDefaultPort(it.m_added_node)))) return false;
}

m_added_node_params.push_back(add);
Expand All @@ -4520,6 +4592,17 @@ bool CConnman::RemoveAddedNode(const std::string& strNode)
return false;
}

bool CConnman::AddedNodesContain(const CAddress& addr) const
{
AssertLockNotHeld(m_added_nodes_mutex);
const std::string addr_str{addr.ToStringAddr()};
const std::string addr_port_str{addr.ToStringAddrPort()};
LOCK(m_added_nodes_mutex);
return (m_added_node_params.size() < 24 // bound the query to a reasonable limit
&& std::any_of(m_added_node_params.cbegin(), m_added_node_params.cend(),
[&](const auto& p) { return p.m_added_node == addr_str || p.m_added_node == addr_port_str; }));
}

bool CConnman::AddPendingMasternode(const uint256& proTxHash)
{
LOCK(cs_vPendingMasternodes);
Expand Down
45 changes: 40 additions & 5 deletions src/net.h
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,22 @@ class CNode
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;
}
Expand Down Expand Up @@ -929,10 +945,8 @@ class CNode
/** Whether this peer provides all services that we want. Used for eviction decisions */
std::atomic_bool m_has_all_wanted_services{false};

/** Whether we should relay transactions to this peer (their version
* message did not include fRelay=false and this is not a block-relay-only
* connection). This only changes from false to true. It will never change
* back to false. Used only in inbound eviction logic. */
/** Whether we should relay transactions to this peer. This only changes
* from false to true. It will never change back to false. */
std::atomic_bool m_relays_txs{false};

/** Whether this peer has loaded a bloom filter. Used only in inbound
Expand Down Expand Up @@ -1283,6 +1297,9 @@ friend class CNode;
EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex, !mutexMsgProc);
bool CheckIncomingNonce(uint64_t nonce);

// alias for thread safety annotations only, not defined
RecursiveMutex& GetNodesMutex() const LOCK_RETURNED(m_nodes_mutex);

struct CFullyConnectedOnly {
bool operator() (const CNode* pnode) const {
return NodeFullyConnected(pnode);
Expand Down Expand Up @@ -1464,7 +1481,8 @@ friend class CNode;

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);
std::vector<AddedNodeInfo> GetAddedNodeInfo() const EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
bool AddedNodesContain(const CAddress& addr) const EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);
std::vector<AddedNodeInfo> GetAddedNodeInfo(bool include_connected) const EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex);

/**
* Attempts to open a connection. Currently only used from tests.
Expand Down Expand Up @@ -1536,6 +1554,8 @@ friend class CNode;
/** 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);

/**
* RAII helper to atomically create a copy of `m_nodes` and add a reference
* to each of the nodes. The nodes are released when this object is destroyed.
Expand Down Expand Up @@ -1736,6 +1756,18 @@ friend class CNode;
*/
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);

Expand Down Expand Up @@ -1778,6 +1810,9 @@ friend class CNode;
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) = {};

std::vector<uint256> vPendingMasternodes;
mutable RecursiveMutex cs_vPendingMasternodes;
std::map<std::pair<Consensus::LLMQType, uint256>, std::set<uint256>> masternodeQuorumNodes GUARDED_BY(cs_vPendingMasternodes);
Expand Down
Loading
Loading