diff --git a/src/net.cpp b/src/net.cpp index 584b4280905..ed5aaad1fb2 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -108,7 +108,7 @@ const std::string NET_MESSAGE_TYPE_OTHER = "*other*"; static const uint64_t RANDOMIZER_ID_NETGROUP = 0x6c0edd8036ef4036ULL; // SHA256("netgroup")[0:8] static const uint64_t RANDOMIZER_ID_LOCALHOSTNONCE = 0xd93e69e2bbfa5735ULL; // SHA256("localhostnonce")[0:8] -static const uint64_t RANDOMIZER_ID_ADDRCACHE = 0x1cf2e4ddd306dda9ULL; // SHA256("addrcache")[0:8] +static const uint64_t RANDOMIZER_ID_NETWORKKEY = 0x0e8a2b136c592a7dULL; // SHA256("networkkey")[0:8] // // Global state variables // @@ -506,6 +506,13 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo if (!addr_bind.IsValid()) { addr_bind = GetBindAddress(*sock); } + uint64_t network_id = GetDeterministicRandomizer(RANDOMIZER_ID_NETWORKKEY) + .Write(target_addr.GetNetClass()) + .Write(addr_bind.GetAddrBytes()) + // For outbound connections, the port of the bound address is randomly + // assigned by the OS and would therefore not be useful for seeding. + .Write(0) + .Finalize(); CNode* pnode = new CNode(id, std::move(sock), target_addr, @@ -515,6 +522,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo pszDest ? pszDest : "", conn_type, /*inbound_onion=*/false, + network_id, CNodeOptions{ .permission_flags = permission_flags, .i2p_sam_session = std::move(i2p_transient_session), @@ -1808,6 +1816,11 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, ServiceFlags local_services = GetLocalServices(); const bool use_v2transport(local_services & NODE_P2P_V2); + uint64_t network_id = GetDeterministicRandomizer(RANDOMIZER_ID_NETWORKKEY) + .Write(inbound_onion ? NET_ONION : addr.GetNetClass()) + .Write(addr_bind.GetAddrBytes()) + .Write(addr_bind.GetPort()) // inbound connections use bind port + .Finalize(); CNode* pnode = new CNode(id, std::move(sock), CAddress{addr, NODE_NONE}, @@ -1817,6 +1830,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, /*addrNameIn=*/"", ConnectionType::INBOUND, inbound_onion, + network_id, CNodeOptions{ .permission_flags = permission_flags, .prefer_evict = discouraged, @@ -3506,15 +3520,9 @@ std::vector CConnman::GetAddressesUnsafe(size_t max_addresses, size_t std::vector CConnman::GetAddresses(CNode& requestor, size_t max_addresses, size_t max_pct) { auto local_socket_bytes = requestor.addrBind.GetAddrBytes(); - uint64_t cache_id = GetDeterministicRandomizer(RANDOMIZER_ID_ADDRCACHE) - .Write(requestor.ConnectedThroughNetwork()) - .Write(local_socket_bytes) - // For outbound connections, the port of the bound address is randomly - // assigned by the OS and would therefore not be useful for seeding. - .Write(requestor.IsInboundConn() ? requestor.addrBind.GetPort() : 0) - .Finalize(); + uint64_t network_id = requestor.m_network_key; const auto current_time = GetTime(); - auto r = m_addr_response_caches.emplace(cache_id, CachedAddrResponse{}); + auto r = m_addr_response_caches.emplace(network_id, CachedAddrResponse{}); CachedAddrResponse& cache_entry = r.first->second; if (cache_entry.m_cache_entry_expiration < current_time) { // If emplace() added new one it has expiration 0. cache_entry.m_addrs_response_cache = GetAddressesUnsafe(max_addresses, max_pct, /*network=*/std::nullopt); @@ -3793,6 +3801,7 @@ CNode::CNode(NodeId idIn, const std::string& addrNameIn, ConnectionType conn_type_in, bool inbound_onion, + uint64_t network_key, CNodeOptions&& node_opts) : m_transport{MakeTransport(idIn, node_opts.use_v2transport, conn_type_in == ConnectionType::INBOUND)}, m_permission_flags{node_opts.permission_flags}, @@ -3805,6 +3814,7 @@ CNode::CNode(NodeId idIn, m_inbound_onion{inbound_onion}, m_prefer_evict{node_opts.prefer_evict}, nKeyedNetGroup{nKeyedNetGroupIn}, + m_network_key{network_key}, m_conn_type{conn_type_in}, id{idIn}, nLocalHostNonce{nLocalHostNonceIn}, diff --git a/src/net.h b/src/net.h index 92aabae6404..9b6582a3911 100644 --- a/src/net.h +++ b/src/net.h @@ -738,6 +738,10 @@ public: std::atomic_bool fPauseRecv{false}; std::atomic_bool fPauseSend{false}; + /** Network key used to prevent fingerprinting our node across networks. + * Influenced by the network and the bind address (+ bind port for inbounds) */ + const uint64_t m_network_key; + const ConnectionType m_conn_type; /** Move all messages from the received queue to the processing queue. */ @@ -889,6 +893,7 @@ public: const std::string& addrNameIn, ConnectionType conn_type_in, bool inbound_onion, + uint64_t network_key, CNodeOptions&& node_opts = {}); CNode(const CNode&) = delete; CNode& operator=(const CNode&) = delete; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 5a157b2b65f..37778322151 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -807,7 +807,7 @@ private: uint32_t GetFetchFlags(const Peer& peer) const; - std::atomic m_next_inv_to_inbounds{0us}; + std::map m_next_inv_to_inbounds_per_network_key GUARDED_BY(g_msgproc_mutex); /** Number of nodes with fSyncStarted. */ int nSyncStarted GUARDED_BY(cs_main) = 0; @@ -837,12 +837,14 @@ private: /** * For sending `inv`s to inbound peers, we use a single (exponentially - * distributed) timer for all peers. If we used a separate timer for each + * distributed) timer for all peers with the same network key. If we used a separate timer for each * peer, a spy node could make multiple inbound connections to us to - * accurately determine when we received the transaction (and potentially - * determine the transaction's origin). */ + * accurately determine when we received a transaction (and potentially + * determine the transaction's origin). Each network key has its own timer + * to make fingerprinting harder. */ std::chrono::microseconds NextInvToInbounds(std::chrono::microseconds now, - std::chrono::seconds average_interval) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); + std::chrono::seconds average_interval, + uint64_t network_key) EXCLUSIVE_LOCKS_REQUIRED(g_msgproc_mutex); // All of the following cache a recent block, and are protected by m_most_recent_block_mutex @@ -1143,15 +1145,15 @@ static bool CanServeWitnesses(const Peer& peer) } std::chrono::microseconds PeerManagerImpl::NextInvToInbounds(std::chrono::microseconds now, - std::chrono::seconds average_interval) + std::chrono::seconds average_interval, + uint64_t network_key) { - if (m_next_inv_to_inbounds.load() < now) { - // If this function were called from multiple threads simultaneously - // it would possible that both update the next send variable, and return a different result to their caller. - // This is not possible in practice as only the net processing thread invokes this function. - m_next_inv_to_inbounds = now + m_rng.rand_exp_duration(average_interval); + auto [it, inserted] = m_next_inv_to_inbounds_per_network_key.try_emplace(network_key, 0us); + auto& timer{it->second}; + if (timer < now) { + timer = now + m_rng.rand_exp_duration(average_interval); } - return m_next_inv_to_inbounds; + return timer; } bool PeerManagerImpl::IsBlockRequested(const uint256& hash) @@ -5715,7 +5717,7 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (tx_relay->m_next_inv_send_time < current_time) { fSendTrickle = true; if (pto->IsInboundConn()) { - tx_relay->m_next_inv_send_time = NextInvToInbounds(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL); + tx_relay->m_next_inv_send_time = NextInvToInbounds(current_time, INBOUND_INVENTORY_BROADCAST_INTERVAL, pto->m_network_key); } else { tx_relay->m_next_inv_send_time = current_time + m_rng.rand_exp_duration(OUTBOUND_INVENTORY_BROADCAST_INTERVAL); } diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp index 16fdd4b7b65..0dc0323028e 100644 --- a/src/test/denialofservice_tests.cpp +++ b/src/test/denialofservice_tests.cpp @@ -62,7 +62,8 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction) CAddress(), /*addrNameIn=*/"", ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/0}; connman.Handshake( /*node=*/dummyNode1, @@ -128,7 +129,8 @@ void AddRandomOutboundPeer(NodeId& id, std::vector& vNodes, PeerManager& CAddress(), /*addrNameIn=*/"", connType, - /*inbound_onion=*/false}); + /*inbound_onion=*/false, + /*network_key=*/0}); CNode &node = *vNodes.back(); node.SetCommonVersion(PROTOCOL_VERSION); @@ -327,7 +329,8 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) CAddress(), /*addrNameIn=*/"", ConnectionType::INBOUND, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/1}; nodes[0]->SetCommonVersion(PROTOCOL_VERSION); peerLogic->InitializeNode(*nodes[0], NODE_NETWORK); nodes[0]->fSuccessfullyConnected = true; @@ -347,7 +350,8 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) CAddress(), /*addrNameIn=*/"", ConnectionType::INBOUND, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/1}; nodes[1]->SetCommonVersion(PROTOCOL_VERSION); peerLogic->InitializeNode(*nodes[1], NODE_NETWORK); nodes[1]->fSuccessfullyConnected = true; @@ -377,7 +381,8 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) CAddress(), /*addrNameIn=*/"", ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/2}; nodes[2]->SetCommonVersion(PROTOCOL_VERSION); peerLogic->InitializeNode(*nodes[2], NODE_NETWORK); nodes[2]->fSuccessfullyConnected = true; @@ -419,7 +424,8 @@ BOOST_AUTO_TEST_CASE(DoS_bantime) CAddress(), /*addrNameIn=*/"", ConnectionType::INBOUND, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/1}; dummyNode.SetCommonVersion(PROTOCOL_VERSION); peerLogic->InitializeNode(dummyNode, NODE_NETWORK); dummyNode.fSuccessfullyConnected = true; diff --git a/src/test/fuzz/p2p_headers_presync.cpp b/src/test/fuzz/p2p_headers_presync.cpp index 9aebac30557..c6842a35849 100644 --- a/src/test/fuzz/p2p_headers_presync.cpp +++ b/src/test/fuzz/p2p_headers_presync.cpp @@ -70,7 +70,7 @@ void HeadersSyncSetup::ResetAndInitialize() for (auto conn_type : conn_types) { CAddress addr{}; - m_connections.push_back(new CNode(id++, nullptr, addr, 0, 0, addr, "", conn_type, false)); + m_connections.push_back(new CNode(id++, nullptr, addr, 0, 0, addr, "", conn_type, false, 0)); CNode& p2p_node = *m_connections.back(); connman.Handshake( diff --git a/src/test/fuzz/util/net.h b/src/test/fuzz/util/net.h index 2756e49cba6..fd07f572b7c 100644 --- a/src/test/fuzz/util/net.h +++ b/src/test/fuzz/util/net.h @@ -275,6 +275,8 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional(); + NetPermissionFlags permission_flags = ConsumeWeakEnum(fuzzed_data_provider, ALL_NET_PERMISSION_FLAGS); if constexpr (ReturnUniquePtr) { return std::make_unique(node_id, @@ -286,6 +288,7 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional& nodes, PeerManager& peerman, Connm CAddress{}, /*addrNameIn=*/"", conn_type, - /*inbound_onion=*/inbound_onion}); + /*inbound_onion=*/inbound_onion, + /*network_key=*/0}); CNode& node = *nodes.back(); node.SetCommonVersion(PROTOCOL_VERSION); diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 0036d94c2fa..711b067ad26 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -67,7 +67,8 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) CAddress(), pszDest, ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false); + /*inbound_onion=*/false, + /*network_key=*/0); BOOST_CHECK(pnode1->IsFullOutboundConn() == true); BOOST_CHECK(pnode1->IsManualConn() == false); BOOST_CHECK(pnode1->IsBlockOnlyConn() == false); @@ -85,7 +86,8 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) CAddress(), pszDest, ConnectionType::INBOUND, - /*inbound_onion=*/false); + /*inbound_onion=*/false, + /*network_key=*/1); BOOST_CHECK(pnode2->IsFullOutboundConn() == false); BOOST_CHECK(pnode2->IsManualConn() == false); BOOST_CHECK(pnode2->IsBlockOnlyConn() == false); @@ -103,7 +105,8 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) CAddress(), pszDest, ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false); + /*inbound_onion=*/false, + /*network_key=*/2); BOOST_CHECK(pnode3->IsFullOutboundConn() == true); BOOST_CHECK(pnode3->IsManualConn() == false); BOOST_CHECK(pnode3->IsBlockOnlyConn() == false); @@ -121,7 +124,8 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) CAddress(), pszDest, ConnectionType::INBOUND, - /*inbound_onion=*/true); + /*inbound_onion=*/true, + /*network_key=*/3); BOOST_CHECK(pnode4->IsFullOutboundConn() == false); BOOST_CHECK(pnode4->IsManualConn() == false); BOOST_CHECK(pnode4->IsBlockOnlyConn() == false); @@ -613,7 +617,8 @@ BOOST_AUTO_TEST_CASE(ipv4_peer_with_ipv6_addrMe_test) CAddress{}, /*pszDest=*/std::string{}, ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false); + /*inbound_onion=*/false, + /*network_key=*/0); pnode->fSuccessfullyConnected.store(true); // the peer claims to be reaching us via IPv6 @@ -667,7 +672,8 @@ BOOST_AUTO_TEST_CASE(get_local_addr_for_peer_port) /*addrBindIn=*/CService{}, /*addrNameIn=*/std::string{}, /*conn_type_in=*/ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/0}; peer_out.fSuccessfullyConnected = true; peer_out.SetAddrLocal(peer_us); @@ -688,7 +694,8 @@ BOOST_AUTO_TEST_CASE(get_local_addr_for_peer_port) /*addrBindIn=*/CService{}, /*addrNameIn=*/std::string{}, /*conn_type_in=*/ConnectionType::INBOUND, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/1}; peer_in.fSuccessfullyConnected = true; peer_in.SetAddrLocal(peer_us); @@ -825,7 +832,8 @@ BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message) /*addrBindIn=*/CService{}, /*addrNameIn=*/std::string{}, /*conn_type_in=*/ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false}; + /*inbound_onion=*/false, + /*network_key=*/2}; const uint64_t services{NODE_NETWORK | NODE_WITNESS}; const int64_t time{0}; @@ -900,7 +908,8 @@ BOOST_AUTO_TEST_CASE(advertise_local_address) CAddress{}, /*pszDest=*/std::string{}, ConnectionType::OUTBOUND_FULL_RELAY, - /*inbound_onion=*/false); + /*inbound_onion=*/false, + /*network_key=*/0); }; g_reachable_nets.Add(NET_CJDNS);