Skip to content

Commit 14ceddd

Browse files
committed
Merge #18991: Cache responses to GETADDR to prevent topology leaks
3bd67ba Test addr response caching (Gleb Naumenko) cf1569e Add addr permission flag enabling non-cached addr sharing (Gleb Naumenko) acd6135 Cache responses to addr requests (Gleb Naumenko) 7cc0e81 Remove useless 2500 limit on AddrMan queries (Gleb Naumenko) ded742b Move filtering banned addrs inside GetAddresses() (Gleb Naumenko) Pull request description: This is a very simple code change with a big p2p privacy benefit. It’s currently trivial to scrape any reachable node’s AddrMan (a database of all nodes known to them along with the timestamps). We do have a limit of one GETADDR per connection, but a spy can disconnect and reconnect even from the same IP, and send GETADDR again and again. Since we respond with 1,000 random records at most, depending on the AddrMan size it takes probably up to 100 requests for an spy to make sure they scraped (almost) everything. I even have a script for that. It is totally doable within couple minutes. Then, with some extra protocol knowledge a spy can infer the direct peers of the victim, and other topological stuff. I suggest to cache responses to GETADDR on a daily basis, so that an attacker gets at most 1,000 records per day, and can’t track the changes in real time. I will be following up with more improvements to addr relay privacy, but this one alone is a very effective. And simple! I doubt any of the real software does *reconnect to get new addrs from a given peer*, so we shouldn’t be cutting anyone. I also believe it doesn’t have any negative implications on the overall topology quality. And the records being “outdated” for at most a day doesn’t break any honest assumptions either. ACKs for top commit: jnewbery: reACK 3bd67ba promag: Code review ACK 3bd67ba. ariard: Code Review ACK 3bd67ba Tree-SHA512: dfa5d03205c2424e40a3f8a41af9306227e1ca18beead3b3dda44aa2a082175bb1c6d929dbc7ea8e48e01aed0d50f0d54491caa1147471a2b72a46c3ca06b66f
2 parents a787428 + 3bd67ba commit 14ceddd

File tree

11 files changed

+183
-13
lines changed

11 files changed

+183
-13
lines changed

src/addrman.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class CAddrInfo : public CAddress
157157
#define ADDRMAN_GETADDR_MAX_PCT 23
158158

159159
//! the maximum number of nodes to return in a getaddr call
160-
#define ADDRMAN_GETADDR_MAX 2500
160+
#define ADDRMAN_GETADDR_MAX 1000
161161

162162
//! Convenience
163163
#define ADDRMAN_TRIED_BUCKET_COUNT (1 << ADDRMAN_TRIED_BUCKET_COUNT_LOG2)

src/net.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2530,7 +2530,24 @@ void CConnman::AddNewAddresses(const std::vector<CAddress>& vAddr, const CAddres
25302530

25312531
std::vector<CAddress> CConnman::GetAddresses()
25322532
{
2533-
return addrman.GetAddr();
2533+
std::vector<CAddress> addresses = addrman.GetAddr();
2534+
if (m_banman) {
2535+
addresses.erase(std::remove_if(addresses.begin(), addresses.end(),
2536+
[this](const CAddress& addr){return m_banman->IsDiscouraged(addr) || m_banman->IsBanned(addr);}),
2537+
addresses.end());
2538+
}
2539+
return addresses;
2540+
}
2541+
2542+
std::vector<CAddress> CConnman::GetAddresses(Network requestor_network)
2543+
{
2544+
const auto current_time = GetTime<std::chrono::microseconds>();
2545+
if (m_addr_response_caches.find(requestor_network) == m_addr_response_caches.end() ||
2546+
m_addr_response_caches[requestor_network].m_update_addr_response < current_time) {
2547+
m_addr_response_caches[requestor_network].m_addrs_response_cache = GetAddresses();
2548+
m_addr_response_caches[requestor_network].m_update_addr_response = current_time + std::chrono::hours(21) + GetRandMillis(std::chrono::hours(6));
2549+
}
2550+
return m_addr_response_caches[requestor_network].m_addrs_response_cache;
25342551
}
25352552

25362553
bool CConnman::AddNode(const std::string& strNode)

src/net.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <atomic>
2828
#include <cstdint>
2929
#include <deque>
30+
#include <map>
3031
#include <thread>
3132
#include <memory>
3233
#include <condition_variable>
@@ -52,6 +53,9 @@ static const int TIMEOUT_INTERVAL = 20 * 60;
5253
static const int FEELER_INTERVAL = 120;
5354
/** The maximum number of new addresses to accumulate before announcing. */
5455
static const unsigned int MAX_ADDR_TO_SEND = 1000;
56+
// TODO: remove ADDRMAN_GETADDR_MAX and let the caller specify this limit with MAX_ADDR_TO_SEND.
57+
static_assert(MAX_ADDR_TO_SEND == ADDRMAN_GETADDR_MAX,
58+
"Max allowed ADDR message size should be equal to the max number of records returned from AddrMan.");
5559
/** Maximum length of incoming protocol messages (no message over 4 MB is currently acceptable). */
5660
static const unsigned int MAX_PROTOCOL_MESSAGE_LENGTH = 4 * 1000 * 1000;
5761
/** Maximum length of the user agent string in `version` message */
@@ -251,6 +255,13 @@ class CConnman
251255
void MarkAddressGood(const CAddress& addr);
252256
void AddNewAddresses(const std::vector<CAddress>& vAddr, const CAddress& addrFrom, int64_t nTimePenalty = 0);
253257
std::vector<CAddress> GetAddresses();
258+
/**
259+
* Cache is used to minimize topology leaks, so it should
260+
* be used for all non-trusted calls, for example, p2p.
261+
* A non-malicious call (from RPC or a peer with addr permission) should
262+
* call the function without a parameter to avoid using the cache.
263+
*/
264+
std::vector<CAddress> GetAddresses(Network requestor_network);
254265

255266
// This allows temporarily exceeding m_max_outbound_full_relay, with the goal of finding
256267
// a peer that is better than all our current peers.
@@ -415,6 +426,29 @@ class CConnman
415426
std::atomic<NodeId> nLastNodeId{0};
416427
unsigned int nPrevNodeCount{0};
417428

429+
/**
430+
* Cache responses to addr requests to minimize privacy leak.
431+
* Attack example: scraping addrs in real-time may allow an attacker
432+
* to infer new connections of the victim by detecting new records
433+
* with fresh timestamps (per self-announcement).
434+
*/
435+
struct CachedAddrResponse {
436+
std::vector<CAddress> m_addrs_response_cache;
437+
std::chrono::microseconds m_update_addr_response{0};
438+
};
439+
440+
/**
441+
* Addr responses stored in different caches
442+
* per network prevent cross-network node identification.
443+
* If a node for example is multi-homed under Tor and IPv6,
444+
* a single cache (or no cache at all) would let an attacker
445+
* to easily detect that it is the same node by comparing responses.
446+
* The used memory equals to 1000 CAddress records (or around 32 bytes) per
447+
* distinct Network (up to 5) we have/had an inbound peer from,
448+
* resulting in at most ~160 KB.
449+
*/
450+
std::map<Network, CachedAddrResponse> m_addr_response_caches;
451+
418452
/**
419453
* Services this instance offers.
420454
*

src/net_permissions.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const std::vector<std::string> NET_PERMISSIONS_DOC{
1515
"relay (relay even in -blocksonly mode)",
1616
"mempool (allow requesting BIP35 mempool contents)",
1717
"download (allow getheaders during IBD, no disconnect after maxuploadtarget limit)",
18+
"addr (responses to GETADDR avoid hitting the cache and contain random records with the most up-to-date info)"
1819
};
1920

2021
namespace {
@@ -50,6 +51,7 @@ bool TryParsePermissionFlags(const std::string str, NetPermissionFlags& output,
5051
else if (permission == "download") NetPermissions::AddFlag(flags, PF_DOWNLOAD);
5152
else if (permission == "all") NetPermissions::AddFlag(flags, PF_ALL);
5253
else if (permission == "relay") NetPermissions::AddFlag(flags, PF_RELAY);
54+
else if (permission == "addr") NetPermissions::AddFlag(flags, PF_ADDR);
5355
else if (permission.length() == 0); // Allow empty entries
5456
else {
5557
error = strprintf(_("Invalid P2P permission: '%s'"), permission);
@@ -75,6 +77,7 @@ std::vector<std::string> NetPermissions::ToStrings(NetPermissionFlags flags)
7577
if (NetPermissions::HasFlag(flags, PF_RELAY)) strings.push_back("relay");
7678
if (NetPermissions::HasFlag(flags, PF_MEMPOOL)) strings.push_back("mempool");
7779
if (NetPermissions::HasFlag(flags, PF_DOWNLOAD)) strings.push_back("download");
80+
if (NetPermissions::HasFlag(flags, PF_ADDR)) strings.push_back("addr");
7881
return strings;
7982
}
8083

src/net_permissions.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ enum NetPermissionFlags {
2929
PF_NOBAN = (1U << 4) | PF_DOWNLOAD,
3030
// Can query the mempool
3131
PF_MEMPOOL = (1U << 5),
32+
// Can request addrs without hitting a privacy-preserving cache
33+
PF_ADDR = (1U << 7),
3234

3335
// True if the user did not specifically set fine grained permissions
3436
PF_ISIMPLICIT = (1U << 31),
35-
PF_ALL = PF_BLOOMFILTER | PF_FORCERELAY | PF_RELAY | PF_NOBAN | PF_MEMPOOL | PF_DOWNLOAD,
37+
PF_ALL = PF_BLOOMFILTER | PF_FORCERELAY | PF_RELAY | PF_NOBAN | PF_MEMPOOL | PF_DOWNLOAD | PF_ADDR,
3638
};
3739

3840
class NetPermissions

src/net_processing.cpp

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2541,7 +2541,7 @@ void ProcessMessage(
25412541
if (!pfrom.IsAddrRelayPeer()) {
25422542
return;
25432543
}
2544-
if (vAddr.size() > 1000)
2544+
if (vAddr.size() > MAX_ADDR_TO_SEND)
25452545
{
25462546
LOCK(cs_main);
25472547
Misbehaving(pfrom.GetId(), 20, strprintf("addr message size = %u", vAddr.size()));
@@ -3474,13 +3474,15 @@ void ProcessMessage(
34743474
pfrom.fSentAddr = true;
34753475

34763476
pfrom.vAddrToSend.clear();
3477-
std::vector<CAddress> vAddr = connman.GetAddresses();
3477+
std::vector<CAddress> vAddr;
3478+
if (pfrom.HasPermission(PF_ADDR)) {
3479+
vAddr = connman.GetAddresses();
3480+
} else {
3481+
vAddr = connman.GetAddresses(pfrom.addr.GetNetwork());
3482+
}
34783483
FastRandomContext insecure_rand;
34793484
for (const CAddress &addr : vAddr) {
3480-
bool banned_or_discouraged = banman && (banman->IsDiscouraged(addr) || banman->IsBanned(addr));
3481-
if (!banned_or_discouraged) {
3482-
pfrom.PushAddress(addr, insecure_rand);
3483-
}
3485+
pfrom.PushAddress(addr, insecure_rand);
34843486
}
34853487
return;
34863488
}
@@ -4079,8 +4081,8 @@ bool PeerLogicValidation::SendMessages(CNode* pto)
40794081
{
40804082
pto->m_addr_known->insert(addr.GetKey());
40814083
vAddr.push_back(addr);
4082-
// receiver rejects addr messages larger than 1000
4083-
if (vAddr.size() >= 1000)
4084+
// receiver rejects addr messages larger than MAX_ADDR_TO_SEND
4085+
if (vAddr.size() >= MAX_ADDR_TO_SEND)
40844086
{
40854087
connman->PushMessage(pto, msgMaker.Make(NetMsgType::ADDR, vAddr));
40864088
vAddr.clear();

src/test/fuzz/net_permissions.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ void test_one_input(const std::vector<uint8_t>& buffer)
2424
NetPermissionFlags::PF_FORCERELAY,
2525
NetPermissionFlags::PF_NOBAN,
2626
NetPermissionFlags::PF_MEMPOOL,
27+
NetPermissionFlags::PF_ADDR,
2728
NetPermissionFlags::PF_ISIMPLICIT,
2829
NetPermissionFlags::PF_ALL,
2930
}) :

src/test/netbase_tests.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,14 @@ BOOST_AUTO_TEST_CASE(netpermissions_test)
406406
BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay,[email protected]/32", whitelistPermissions, error));
407407

408408
const auto strings = NetPermissions::ToStrings(PF_ALL);
409-
BOOST_CHECK_EQUAL(strings.size(), 6U);
409+
BOOST_CHECK_EQUAL(strings.size(), 7U);
410410
BOOST_CHECK(std::find(strings.begin(), strings.end(), "bloomfilter") != strings.end());
411411
BOOST_CHECK(std::find(strings.begin(), strings.end(), "forcerelay") != strings.end());
412412
BOOST_CHECK(std::find(strings.begin(), strings.end(), "relay") != strings.end());
413413
BOOST_CHECK(std::find(strings.begin(), strings.end(), "noban") != strings.end());
414414
BOOST_CHECK(std::find(strings.begin(), strings.end(), "mempool") != strings.end());
415415
BOOST_CHECK(std::find(strings.begin(), strings.end(), "download") != strings.end());
416+
BOOST_CHECK(std::find(strings.begin(), strings.end(), "addr") != strings.end());
416417
}
417418

418419
BOOST_AUTO_TEST_CASE(netbase_dont_resolve_strings_with_embedded_nul_characters)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2020 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test addr response caching"""
6+
7+
import time
8+
from test_framework.messages import (
9+
CAddress,
10+
NODE_NETWORK,
11+
NODE_WITNESS,
12+
msg_addr,
13+
msg_getaddr,
14+
)
15+
from test_framework.mininode import (
16+
P2PInterface,
17+
mininode_lock
18+
)
19+
from test_framework.test_framework import BitcoinTestFramework
20+
from test_framework.util import (
21+
assert_equal,
22+
)
23+
24+
MAX_ADDR_TO_SEND = 1000
25+
26+
def gen_addrs(n):
27+
addrs = []
28+
for i in range(n):
29+
addr = CAddress()
30+
addr.time = int(time.time())
31+
addr.nServices = NODE_NETWORK | NODE_WITNESS
32+
# Use first octets to occupy different AddrMan buckets
33+
first_octet = i >> 8
34+
second_octet = i % 256
35+
addr.ip = "{}.{}.1.1".format(first_octet, second_octet)
36+
addr.port = 8333
37+
addrs.append(addr)
38+
return addrs
39+
40+
class AddrReceiver(P2PInterface):
41+
42+
def __init__(self):
43+
super().__init__()
44+
self.received_addrs = None
45+
46+
def get_received_addrs(self):
47+
with mininode_lock:
48+
return self.received_addrs
49+
50+
def on_addr(self, message):
51+
self.received_addrs = []
52+
for addr in message.addrs:
53+
self.received_addrs.append(addr.ip)
54+
55+
def addr_received(self):
56+
return self.received_addrs is not None
57+
58+
59+
class AddrTest(BitcoinTestFramework):
60+
def set_test_params(self):
61+
self.setup_clean_chain = False
62+
self.num_nodes = 1
63+
64+
def run_test(self):
65+
self.log.info('Create connection that sends and requests addr messages')
66+
addr_source = self.nodes[0].add_p2p_connection(P2PInterface())
67+
68+
msg_send_addrs = msg_addr()
69+
self.log.info('Fill peer AddrMan with a lot of records')
70+
# Since these addrs are sent from the same source, not all of them will be stored,
71+
# because we allocate a limited number of AddrMan buckets per addr source.
72+
total_addrs = 10000
73+
addrs = gen_addrs(total_addrs)
74+
for i in range(int(total_addrs/MAX_ADDR_TO_SEND)):
75+
msg_send_addrs.addrs = addrs[i * MAX_ADDR_TO_SEND:(i + 1) * MAX_ADDR_TO_SEND]
76+
addr_source.send_and_ping(msg_send_addrs)
77+
78+
responses = []
79+
self.log.info('Send many addr requests within short time to receive same response')
80+
N = 5
81+
cur_mock_time = int(time.time())
82+
for i in range(N):
83+
addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver())
84+
addr_receiver.send_and_ping(msg_getaddr())
85+
# Trigger response
86+
cur_mock_time += 5 * 60
87+
self.nodes[0].setmocktime(cur_mock_time)
88+
addr_receiver.wait_until(addr_receiver.addr_received)
89+
responses.append(addr_receiver.get_received_addrs())
90+
for response in responses[1:]:
91+
assert_equal(response, responses[0])
92+
assert(len(response) < MAX_ADDR_TO_SEND)
93+
94+
cur_mock_time += 3 * 24 * 60 * 60
95+
self.nodes[0].setmocktime(cur_mock_time)
96+
97+
self.log.info('After time passed, see a new response to addr request')
98+
last_addr_receiver = self.nodes[0].add_p2p_connection(AddrReceiver())
99+
last_addr_receiver.send_and_ping(msg_getaddr())
100+
# Trigger response
101+
cur_mock_time += 5 * 60
102+
self.nodes[0].setmocktime(cur_mock_time)
103+
last_addr_receiver.wait_until(last_addr_receiver.addr_received)
104+
# new response is different
105+
assert(set(responses[0]) != set(last_addr_receiver.get_received_addrs()))
106+
107+
108+
if __name__ == '__main__':
109+
AddrTest().main()

test/functional/p2p_permissions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ def run_test(self):
9696
self.checkpermission(
9797
# all permission added
9898
99-
["forcerelay", "noban", "mempool", "bloomfilter", "relay", "download"],
99+
["forcerelay", "noban", "mempool", "bloomfilter", "relay", "download", "addr"],
100100
False)
101101

102102
self.stop_node(1)

0 commit comments

Comments
 (0)