net: use generic network key for addrcache
What changed, and why it matters
This Bitcoin Core change tweaks how the node picks a secret 'network key' used when replying to peer address requests. Previously the key was computed fresh each time from the peer's network type and the local bind address. Now it is computed once when a connection is created and stored on the CNode object. The stated goal is to make it easier to reuse the same anti-fingerprinting key in other parts of the code later. It is a refactoring/hardening patch, not a fix for an active bug or exploit.
No urgent action. Treat as routine hardening/refactoring. Reviewers may want to confirm that caching the key on CNode does not introduce new side channels (e.g., the key being observable by other peers) and that the deterministic derivation inputs remain appropriate for future consumers of m_network_key.
Security signals we found
Anti-fingerprinting hardening: per-connection network key derivation
RANDOMIZER_ID renamed from ADDRCACHE to NETWORKKEY, indicating intended broader use
No bounds, memory, or cryptographic weakness visible in diff
Test-only constructor call sites updated to pass an explicit network_key
Evidence from the diff
The commit replaces the per-call RANDOMIZER_ID_ADDRCACHE deterministic randomizer with a connection-level RANDOMIZER_ID_NETWORKKEY stored as CNode::m_network_key. The key is derived at connection setup from the network class (NET_ONION for inbound onion connections), the local bind address bytes, and the bind port (for inbound; 0 for outbound because OS-assigned outbound ports are not stable). GetAddresses() then uses requestor.m_network_key directly instead of recomputing cache_id. This centralizes the anti-fingerprinting seed and makes it available for future uses. No vulnerability is fixed; the change is defensive and preparatory.
Changed components
src/net.cppsrc/net.hCNode constructor and connection setupCConnman::GetAddresses addr-response cacheunit/fuzz test helpersInspect captured patch +61 / −26
diff --git a/src/net.cpp b/src/net.cpp
index 50988114..440b04b6 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
//
@@ -530,6 +530,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,
@@ -539,6 +546,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),
@@ -1832,6 +1840,11 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr<Sock>&& 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},
@@ -1841,6 +1854,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr<Sock>&& sock,
/*addrNameIn=*/"",
ConnectionType::INBOUND,
inbound_onion,
+ network_id,
CNodeOptions{
.permission_flags = permission_flags,
.prefer_evict = discouraged,
@@ -3519,15 +3533,9 @@ std::vector<CAddress> CConnman::GetAddressesUnsafe(size_t max_addresses, size_t
std::vector<CAddress> 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<std::chrono::microseconds>();
- 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);
@@ -3804,6 +3812,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},
@@ -3816,6 +3825,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 afbcc52b..8749addd 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/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp
index 16fdd4b7..0dc03230 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<CNode*>& 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 9aebac30..c6842a35 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 698001a7..381103aa 100644
--- a/src/test/fuzz/util/net.h
+++ b/src/test/fuzz/util/net.h
@@ -239,6 +239,8 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional<N
const std::string addr_name = fuzzed_data_provider.ConsumeRandomLengthString(64);
const ConnectionType conn_type = fuzzed_data_provider.PickValueInArray(ALL_CONNECTION_TYPES);
const bool inbound_onion{conn_type == ConnectionType::INBOUND ? fuzzed_data_provider.ConsumeBool() : false};
+ const uint64_t network_id = fuzzed_data_provider.ConsumeIntegral<uint64_t>();
+
NetPermissionFlags permission_flags = ConsumeWeakEnum(fuzzed_data_provider, ALL_NET_PERMISSION_FLAGS);
if constexpr (ReturnUniquePtr) {
return std::make_unique<CNode>(node_id,
@@ -250,6 +252,7 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional<N
addr_name,
conn_type,
inbound_onion,
+ network_id,
CNodeOptions{ .permission_flags = permission_flags });
} else {
return CNode{node_id,
@@ -261,6 +264,7 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional<N
addr_name,
conn_type,
inbound_onion,
+ network_id,
CNodeOptions{ .permission_flags = permission_flags }};
}
}
diff --git a/src/test/net_peer_connection_tests.cpp b/src/test/net_peer_connection_tests.cpp
index ba3224bc..bff2c924 100644
--- a/src/test/net_peer_connection_tests.cpp
+++ b/src/test/net_peer_connection_tests.cpp
@@ -72,7 +72,8 @@ void AddPeer(NodeId& id, std::vector<CNode*>& 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 0036d94c..711b067a 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);
Why this scored 31/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.