Skip to content

Commit d0601e6

Browse files
committed
Merge #17812: config, net, test: asmap feature refinements and functional tests
1ba3e1c init: move asmap code earlier in init process (Jon Atack) 5ba829e rpc: fix getpeerinfo RPCResult `mapped_as` type (Jon Atack) c90b9a2 net: extract conditional to bool CNetAddr::IsHeNet (Jon Atack) 819fb55 logging: asmap logging and #include fixups (Jon Atack) dcaf543 test: add functional test for an empty, unparsable asmap (Jon Atack) b8d0412 config: separate the asmap finding and parsing checks (Jon Atack) 81c38a2 config: enable passing -asmap an absolute file path (Jon Atack) fbe9b02 config: use default value in -asmap config (Jon Atack) 08b9926 test: add feature_asmap functional tests (Jon Atack) Pull request description: This PR builds on PR #16702 to add functional tests / sanity checks and user-facing refinements for passing `-asmap` to configure ASN-based IP bucketing in addrman. As per our review discussion in that PR, the idea here is to handle aspects like functional tests and config arg handling that can help the PR be merged while enabling the author to focus on the bucketing itself. - [x] add feature functional tests to verify node behaviour and debug log output when launching - `bitcoind` with no `-asmap` arg - `bitcoind -asmap=RELATIVE_FILENAME` to the unit test data skeleton asmap - `bitcoind -asmap` with no filename specified using the default asmap file - `bitcoind -asmap` with no filename specified and a missing default asmap file - [x] add the ability to pass absolute path filenames to the `-asmap` config arg in addition to datadir-relative path filenames as per bitcoin/bitcoin#16702 (comment), and add test coverage - [x] separate the asmap file finding and parsing checks, which allows adding tests for the case of a found but unparseable or empty asmap - [x] add test for an empty asmap - [x] various asmap fixups - [x] move the asmap init code earlier in the init process to provide immediate feedback when passing an `-asmap` config arg. This speeds up the `feature_asmap` functional test from 60 to 5 seconds! Credit to Wladimir J. van der Laan for the suggestion. ACKs for top commit: practicalswift: ACK 1ba3e1c -- diff looks correct fanquake: ACK 1ba3e1c Tree-SHA512: e9094460a597ac5597449acfe631c87b71d3ede6a12c7ae61b26d1161b3eefed8e7e25c4fb0505864cebd89300b7c4cf9378060aa9155441029315df15fa3283
2 parents cbc32d6 + 1ba3e1c commit d0601e6

File tree

8 files changed

+153
-35
lines changed

8 files changed

+153
-35
lines changed

src/addrman.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
#include <addrman.h>
77

88
#include <hash.h>
9-
#include <serialize.h>
109
#include <logging.h>
10+
#include <serialize.h>
1111

1212
int CAddrInfo::GetTriedBucket(const uint256& nKey, const std::vector<bool> &asmap) const
1313
{
1414
uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetKey()).GetCheapHash();
1515
uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup(asmap) << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)).GetCheapHash();
1616
int tried_bucket = hash2 % ADDRMAN_TRIED_BUCKET_COUNT;
1717
uint32_t mapped_as = GetMappedAS(asmap);
18-
LogPrint(BCLog::NET, "IP %s mapped to AS%i belongs to tried bucket %i.\n", ToStringIP(), mapped_as, tried_bucket);
18+
LogPrint(BCLog::NET, "IP %s mapped to AS%i belongs to tried bucket %i\n", ToStringIP(), mapped_as, tried_bucket);
1919
return tried_bucket;
2020
}
2121

@@ -26,7 +26,7 @@ int CAddrInfo::GetNewBucket(const uint256& nKey, const CNetAddr& src, const std:
2626
uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << vchSourceGroupKey << (hash1 % ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP)).GetCheapHash();
2727
int new_bucket = hash2 % ADDRMAN_NEW_BUCKET_COUNT;
2828
uint32_t mapped_as = GetMappedAS(asmap);
29-
LogPrint(BCLog::NET, "IP %s mapped to AS%i belongs to new bucket %i.\n", ToStringIP(), mapped_as, new_bucket);
29+
LogPrint(BCLog::NET, "IP %s mapped to AS%i belongs to new bucket %i\n", ToStringIP(), mapped_as, new_bucket);
3030
return new_bucket;
3131
}
3232

@@ -630,12 +630,12 @@ std::vector<bool> CAddrMan::DecodeAsmap(fs::path path)
630630
FILE *filestr = fsbridge::fopen(path, "rb");
631631
CAutoFile file(filestr, SER_DISK, CLIENT_VERSION);
632632
if (file.IsNull()) {
633-
LogPrintf("Failed to open asmap file from disk.\n");
633+
LogPrintf("Failed to open asmap file from disk\n");
634634
return bits;
635635
}
636636
fseek(filestr, 0, SEEK_END);
637637
int length = ftell(filestr);
638-
LogPrintf("Opened asmap file %s (%d bytes) from disk.\n", path, length);
638+
LogPrintf("Opened asmap file %s (%d bytes) from disk\n", path, length);
639639
fseek(filestr, 0, SEEK_SET);
640640
char cur_byte;
641641
for (int i = 0; i < length; ++i) {

src/addrman.h

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,22 @@
66
#ifndef BITCOIN_ADDRMAN_H
77
#define BITCOIN_ADDRMAN_H
88

9+
#include <clientversion.h>
910
#include <netaddress.h>
1011
#include <protocol.h>
1112
#include <random.h>
1213
#include <sync.h>
1314
#include <timedata.h>
1415
#include <util/system.h>
15-
#include <clientversion.h>
1616

17+
#include <fs.h>
18+
#include <hash.h>
19+
#include <iostream>
1720
#include <map>
1821
#include <set>
1922
#include <stdint.h>
20-
#include <vector>
21-
#include <iostream>
2223
#include <streams.h>
23-
#include <fs.h>
24-
#include <hash.h>
25-
24+
#include <vector>
2625

2726
/**
2827
* Extended statistics about a CAddress

src/init.cpp

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@
4747
#include <txdb.h>
4848
#include <txmempool.h>
4949
#include <ui_interface.h>
50+
#include <util/asmap.h>
5051
#include <util/moneystr.h>
5152
#include <util/system.h>
5253
#include <util/threadnames.h>
5354
#include <util/translation.h>
54-
#include <util/asmap.h>
5555
#include <validation.h>
5656
#include <hash.h>
5757

@@ -408,6 +408,7 @@ void SetupServerArgs()
408408
ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
409409

410410
gArgs.AddArg("-addnode=<ip>", "Add a node to connect to and attempt to keep the connection open (see the `addnode` RPC command help for more info). This option can be specified multiple times to add multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
411+
gArgs.AddArg("-asmap=<file>", strprintf("Specify asn mapping used for bucketing of the peers (default: %s). Relative paths will be prefixed by the net-specific datadir location.", DEFAULT_ASMAP_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
411412
gArgs.AddArg("-banscore=<n>", strprintf("Threshold for disconnecting misbehaving peers (default: %u)", DEFAULT_BANSCORE_THRESHOLD), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
412413
gArgs.AddArg("-bantime=<n>", strprintf("Number of seconds to keep misbehaving peers from reconnecting (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
413414
gArgs.AddArg("-bind=<addr>", "Bind to given address and always listen on it. Use [host]:port notation for IPv6", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
@@ -436,7 +437,6 @@ void SetupServerArgs()
436437
gArgs.AddArg("-peertimeout=<n>", strprintf("Specify p2p connection timeout in seconds. This option determines the amount of time a peer may be inactive before the connection to it is dropped. (minimum: 1, default: %d)", DEFAULT_PEER_CONNECT_TIMEOUT), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::CONNECTION);
437438
gArgs.AddArg("-torcontrol=<ip>:<port>", strprintf("Tor control port to use if onion listening enabled (default: %s)", DEFAULT_TOR_CONTROL), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
438439
gArgs.AddArg("-torpassword=<pass>", "Tor control port password (default: empty)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::CONNECTION);
439-
gArgs.AddArg("-asmap=<file>", "Specify asn mapping used for bucketing of the peers. Path should be relative to the -datadir path.", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
440440
#ifdef USE_UPNP
441441
#if USE_UPNP
442442
gArgs.AddArg("-upnp", "Use UPnP to map the listening port (default: 1 when listening and no -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
@@ -1418,6 +1418,31 @@ bool AppInitMain(NodeContext& node)
14181418
return InitError(ResolveErrMsg("externalip", strAddr));
14191419
}
14201420

1421+
// Read asmap file if configured
1422+
if (gArgs.IsArgSet("-asmap")) {
1423+
fs::path asmap_path = fs::path(gArgs.GetArg("-asmap", ""));
1424+
if (asmap_path.empty()) {
1425+
asmap_path = DEFAULT_ASMAP_FILENAME;
1426+
}
1427+
if (!asmap_path.is_absolute()) {
1428+
asmap_path = GetDataDir() / asmap_path;
1429+
}
1430+
if (!fs::exists(asmap_path)) {
1431+
InitError(strprintf(_("Could not find asmap file %s").translated, asmap_path));
1432+
return false;
1433+
}
1434+
std::vector<bool> asmap = CAddrMan::DecodeAsmap(asmap_path);
1435+
if (asmap.size() == 0) {
1436+
InitError(strprintf(_("Could not parse asmap file %s").translated, asmap_path));
1437+
return false;
1438+
}
1439+
const uint256 asmap_version = SerializeHash(asmap);
1440+
node.connman->SetAsmap(std::move(asmap));
1441+
LogPrintf("Using asmap version %s for IP bucketing\n", asmap_version.ToString());
1442+
} else {
1443+
LogPrintf("Using /16 prefix for IP bucketing\n");
1444+
}
1445+
14211446
#if ENABLE_ZMQ
14221447
g_zmq_notification_interface = CZMQNotificationInterface::Create();
14231448

@@ -1825,25 +1850,6 @@ bool AppInitMain(NodeContext& node)
18251850
return false;
18261851
}
18271852

1828-
// Read asmap file if configured
1829-
if (gArgs.IsArgSet("-asmap")) {
1830-
std::string asmap_file = gArgs.GetArg("-asmap", "");
1831-
if (asmap_file.empty()) {
1832-
asmap_file = DEFAULT_ASMAP_FILENAME;
1833-
}
1834-
const fs::path asmap_path = GetDataDir() / asmap_file;
1835-
std::vector<bool> asmap = CAddrMan::DecodeAsmap(asmap_path);
1836-
if (asmap.size() == 0) {
1837-
InitError(strprintf(_("Could not find or parse specified asmap: '%s'").translated, asmap_path));
1838-
return false;
1839-
}
1840-
const uint256 asmap_version = SerializeHash(asmap);
1841-
node.connman->SetAsmap(std::move(asmap));
1842-
LogPrintf("Using asmap version %s for IP bucketing.\n", asmap_version.ToString());
1843-
} else {
1844-
LogPrintf("Using /16 prefix for IP bucketing.\n");
1845-
}
1846-
18471853
// ********************************************************* Step 13: finished
18481854

18491855
SetRPCWarmupFinished();

src/netaddress.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ bool CNetAddr::IsRFC7343() const
210210
return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x00 && (GetByte(12) & 0xF0) == 0x20);
211211
}
212212

213+
bool CNetAddr::IsHeNet() const
214+
{
215+
return (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x04 && GetByte(12) == 0x70);
216+
}
217+
213218
/**
214219
* @returns Whether or not this is a dummy address that maps an onion address
215220
* into IPv6.
@@ -516,7 +521,7 @@ std::vector<unsigned char> CNetAddr::GetGroup(const std::vector<bool> &asmap) co
516521
} else if (IsTor()) {
517522
nStartByte = 6;
518523
nBits = 4;
519-
} else if (GetByte(15) == 0x20 && GetByte(14) == 0x01 && GetByte(13) == 0x04 && GetByte(12) == 0x70) {
524+
} else if (IsHeNet()) {
520525
// for he.net, use /36 groups
521526
nBits = 36;
522527
} else {

src/netaddress.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class CNetAddr
4545
*/
4646
void SetRaw(Network network, const uint8_t *data);
4747

48-
public:
4948
bool SetInternal(const std::string& name);
5049

5150
bool SetSpecial(const std::string &strName); // for Tor addresses
@@ -66,6 +65,7 @@ class CNetAddr
6665
bool IsRFC4862() const; // IPv6 autoconfig (FE80::/64)
6766
bool IsRFC6052() const; // IPv6 well-known prefix for IPv4-embedded address (64:FF9B::/96)
6867
bool IsRFC6145() const; // IPv6 IPv4-translated address (::FFFF:0:0:0/96) (actually defined in RFC2765)
68+
bool IsHeNet() const; // IPv6 Hurricane Electric - https://he.net (2001:0470::/36)
6969
bool IsTor() const;
7070
bool IsLocal() const;
7171
bool IsRoutable() const;

src/rpc/net.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ static UniValue getpeerinfo(const JSONRPCRequest& request)
8686
{RPCResult::Type::STR, "addr", "(host:port) The IP address and port of the peer"},
8787
{RPCResult::Type::STR, "addrbind", "(ip:port) Bind address of the connection to the peer"},
8888
{RPCResult::Type::STR, "addrlocal", "(ip:port) Local address as reported by the peer"},
89-
{RPCResult::Type::STR, "mapped_as", "The AS in the BGP route to the peer used for diversifying peer selection"},
89+
{RPCResult::Type::NUM, "mapped_as", "The AS in the BGP route to the peer used for diversifying\n"
90+
"peer selection (only available if the asmap config flag is set)"},
9091
{RPCResult::Type::STR_HEX, "services", "The services offered"},
9192
{RPCResult::Type::ARR, "servicesnames", "the services offered, in human-readable form",
9293
{

test/functional/feature_asmap.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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 asmap config argument for ASN-based IP bucketing.
6+
7+
Verify node behaviour and debug log when launching bitcoind in these cases:
8+
9+
1. `bitcoind` with no -asmap arg, using /16 prefix for IP bucketing
10+
11+
2. `bitcoind -asmap=<absolute path>`, using the unit test skeleton asmap
12+
13+
3. `bitcoind -asmap=<relative path>`, using the unit test skeleton asmap
14+
15+
4. `bitcoind -asmap/-asmap=` with no file specified, using the default asmap
16+
17+
5. `bitcoind -asmap` with no file specified and a missing default asmap file
18+
19+
6. `bitcoind -asmap` with an empty (unparsable) default asmap file
20+
21+
The tests are order-independent.
22+
23+
"""
24+
import os
25+
import shutil
26+
27+
from test_framework.test_framework import BitcoinTestFramework
28+
29+
DEFAULT_ASMAP_FILENAME = 'ip_asn.map' # defined in src/init.cpp
30+
ASMAP = '../../src/test/data/asmap.raw' # path to unit test skeleton asmap
31+
VERSION = 'fec61fa21a9f46f3b17bdcd660d7f4cd90b966aad3aec593c99b35f0aca15853'
32+
33+
def expected_messages(filename):
34+
return ['Opened asmap file "{}" (59 bytes) from disk'.format(filename),
35+
'Using asmap version {} for IP bucketing'.format(VERSION)]
36+
37+
class AsmapTest(BitcoinTestFramework):
38+
def set_test_params(self):
39+
self.setup_clean_chain = False
40+
self.num_nodes = 1
41+
42+
def test_without_asmap_arg(self):
43+
self.log.info('Test bitcoind with no -asmap arg passed')
44+
self.stop_node(0)
45+
with self.node.assert_debug_log(['Using /16 prefix for IP bucketing']):
46+
self.start_node(0)
47+
48+
def test_asmap_with_absolute_path(self):
49+
self.log.info('Test bitcoind -asmap=<absolute path>')
50+
self.stop_node(0)
51+
filename = os.path.join(self.datadir, 'my-map-file.map')
52+
shutil.copyfile(self.asmap_raw, filename)
53+
with self.node.assert_debug_log(expected_messages(filename)):
54+
self.start_node(0, ['-asmap={}'.format(filename)])
55+
os.remove(filename)
56+
57+
def test_asmap_with_relative_path(self):
58+
self.log.info('Test bitcoind -asmap=<relative path>')
59+
self.stop_node(0)
60+
name = 'ASN_map'
61+
filename = os.path.join(self.datadir, name)
62+
shutil.copyfile(self.asmap_raw, filename)
63+
with self.node.assert_debug_log(expected_messages(filename)):
64+
self.start_node(0, ['-asmap={}'.format(name)])
65+
os.remove(filename)
66+
67+
def test_default_asmap(self):
68+
shutil.copyfile(self.asmap_raw, self.default_asmap)
69+
for arg in ['-asmap', '-asmap=']:
70+
self.log.info('Test bitcoind {} (using default map file)'.format(arg))
71+
self.stop_node(0)
72+
with self.node.assert_debug_log(expected_messages(self.default_asmap)):
73+
self.start_node(0, [arg])
74+
os.remove(self.default_asmap)
75+
76+
def test_default_asmap_with_missing_file(self):
77+
self.log.info('Test bitcoind -asmap with missing default map file')
78+
self.stop_node(0)
79+
msg = "Error: Could not find asmap file \"{}\"".format(self.default_asmap)
80+
self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg)
81+
82+
def test_empty_asmap(self):
83+
self.log.info('Test bitcoind -asmap with empty map file')
84+
self.stop_node(0)
85+
with open(self.default_asmap, "w", encoding="utf-8") as f:
86+
f.write("")
87+
msg = "Error: Could not parse asmap file \"{}\"".format(self.default_asmap)
88+
self.node.assert_start_raises_init_error(extra_args=['-asmap'], expected_msg=msg)
89+
os.remove(self.default_asmap)
90+
91+
def run_test(self):
92+
self.node = self.nodes[0]
93+
self.datadir = os.path.join(self.node.datadir, self.chain)
94+
self.default_asmap = os.path.join(self.datadir, DEFAULT_ASMAP_FILENAME)
95+
self.asmap_raw = os.path.join(os.path.dirname(os.path.realpath(__file__)), ASMAP)
96+
97+
self.test_without_asmap_arg()
98+
self.test_asmap_with_absolute_path()
99+
self.test_asmap_with_relative_path()
100+
self.test_default_asmap()
101+
self.test_default_asmap_with_missing_file()
102+
self.test_empty_asmap()
103+
104+
105+
if __name__ == '__main__':
106+
AsmapTest().main()

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@
206206
'p2p_dos_header_tree.py',
207207
'p2p_unrequested_blocks.py',
208208
'feature_includeconf.py',
209+
'feature_asmap.py',
209210
'rpc_deriveaddresses.py',
210211
'rpc_deriveaddresses.py --usecli',
211212
'rpc_scantxoutset.py',

0 commit comments

Comments
 (0)