diff --git a/src/addrman.h b/src/addrman.h --- a/src/addrman.h +++ b/src/addrman.h @@ -6,14 +6,19 @@ #ifndef BITCOIN_ADDRMAN_H #define BITCOIN_ADDRMAN_H +#include +#include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -67,16 +72,19 @@ CAddrInfo() : CAddress(), source() {} //! Calculate in which "tried" bucket this entry belongs - int GetTriedBucket(const uint256 &nKey) const; + int GetTriedBucket(const uint256 &nKey, + const std::vector &asmap) const; //! Calculate in which "new" bucket this entry belongs, given a certain //! source - int GetNewBucket(const uint256 &nKey, const CNetAddr &src) const; + int GetNewBucket(const uint256 &nKey, const CNetAddr &src, + const std::vector &asmap) const; //! Calculate in which "new" bucket this entry belongs, using its default //! source - int GetNewBucket(const uint256 &nKey) const { - return GetNewBucket(nKey, source); + int GetNewBucket(const uint256 &nKey, + const std::vector &asmap) const { + return GetNewBucket(nKey, source, asmap); } //! Calculate in which position of a bucket to store this entry. @@ -188,6 +196,8 @@ * Stochastical (IP) address manager */ class CAddrMan { + friend class CAddrManTest; + protected: //! critical section to protect the inner data structures mutable RecursiveMutex cs; @@ -294,9 +304,29 @@ EXCLUSIVE_LOCKS_REQUIRED(cs); public: + // Compressed IP->ASN mapping, loaded from a file when a node starts. + // Should be always empty if no file was provided. + // This mapping is then used for bucketing nodes in Addrman. + // + // If asmap is provided, nodes will be bucketed by + // AS they belong to, in order to make impossible for a node + // to connect to several nodes hosted in a single AS. + // This is done in response to Erebus attack, but also to generally + // diversify the connections every node creates, + // especially useful when a large fraction of nodes + // operate under a couple of cloud providers. + // + // If a new asmap was provided, the existing records + // would be re-bucketed accordingly. + std::vector m_asmap; + + // Read asmap from provided binary file + static std::vector DecodeAsmap(fs::path path); + /** * serialized format: - * * version byte (currently 1) + * * version byte (1 for pre-asmap files, 2 for files including asmap + * version) * * 0x20 + nKey (serialized as if it were a vector, for backward * compatibility) * * nNew @@ -328,7 +358,7 @@ template void Serialize(Stream &s) const { LOCK(cs); - uint8_t nVersion = 1; + uint8_t nVersion = 2; s << nVersion; s << uint8_t(32); s << nKey; @@ -372,13 +402,19 @@ } } } + // Store asmap version after bucket entries so that it + // can be ignored by older clients for backward compatibility. + uint256 asmap_version; + if (m_asmap.size() != 0) { + asmap_version = SerializeHash(m_asmap); + } + s << asmap_version; } template void Unserialize(Stream &s) { LOCK(cs); Clear(); - uint8_t nVersion; s >> nVersion; uint8_t nKeySize; @@ -414,17 +450,6 @@ mapAddr[info] = n; info.nRandomPos = vRandom.size(); vRandom.push_back(n); - if (nVersion != 1 || nUBuckets != ADDRMAN_NEW_BUCKET_COUNT) { - // In case the new table data cannot be used (nVersion unknown, - // or bucket count wrong), immediately try to give them a - // reference based on their primary source address. - int nUBucket = info.GetNewBucket(nKey); - int nUBucketPos = info.GetBucketPosition(nKey, true, nUBucket); - if (vvNew[nUBucket][nUBucketPos] == -1) { - vvNew[nUBucket][nUBucketPos] = n; - info.nRefCount++; - } - } } nIdCount = nNew; @@ -433,7 +458,7 @@ for (int n = 0; n < nTried; n++) { CAddrInfo info; s >> info; - int nKBucket = info.GetTriedBucket(nKey); + int nKBucket = info.GetTriedBucket(nKey, m_asmap); int nKBucketPos = info.GetBucketPosition(nKey, false, nKBucket); if (vvTried[nKBucket][nKBucketPos] == -1) { info.nRandomPos = vRandom.size(); @@ -449,7 +474,11 @@ } nTried -= nLost; - // Deserialize positions in the new table (if possible). + // Store positions in the new table buckets to apply later (if + // possible). Represents which entry belonged to which bucket when + // serializing + std::map entryToBucket; + for (int bucket = 0; bucket < nUBuckets; bucket++) { int nSize = 0; s >> nSize; @@ -457,16 +486,44 @@ int nIndex = 0; s >> nIndex; if (nIndex >= 0 && nIndex < nNew) { - CAddrInfo &info = mapInfo[nIndex]; - int nUBucketPos = - info.GetBucketPosition(nKey, true, bucket); - if (nVersion == 1 && - nUBuckets == ADDRMAN_NEW_BUCKET_COUNT && - vvNew[bucket][nUBucketPos] == -1 && - info.nRefCount < ADDRMAN_NEW_BUCKETS_PER_ADDRESS) { - info.nRefCount++; - vvNew[bucket][nUBucketPos] = nIndex; - } + entryToBucket[nIndex] = bucket; + } + } + } + + uint256 supplied_asmap_version; + if (m_asmap.size() != 0) { + supplied_asmap_version = SerializeHash(m_asmap); + } + uint256 serialized_asmap_version; + if (nVersion > 1) { + s >> serialized_asmap_version; + } + + for (int n = 0; n < nNew; n++) { + CAddrInfo &info = mapInfo[n]; + int bucket = entryToBucket[n]; + int nUBucketPos = info.GetBucketPosition(nKey, true, bucket); + if (nVersion == 2 && nUBuckets == ADDRMAN_NEW_BUCKET_COUNT && + vvNew[bucket][nUBucketPos] == -1 && + info.nRefCount < ADDRMAN_NEW_BUCKETS_PER_ADDRESS && + serialized_asmap_version == supplied_asmap_version) { + // Bucketing has not changed, using existing bucket positions + // for the new table + vvNew[bucket][nUBucketPos] = n; + info.nRefCount++; + } else { + // In case the new table data cannot be used (nVersion unknown, + // bucket count wrong or new asmap), try to give them a + // reference based on their primary source address. + LogPrint(BCLog::ADDRMAN, + "Bucketing method was updated, re-bucketing addrman " + "entries from disk\n"); + bucket = info.GetNewBucket(nKey, m_asmap); + nUBucketPos = info.GetBucketPosition(nKey, true, bucket); + if (vvNew[bucket][nUBucketPos] == -1) { + vvNew[bucket][nUBucketPos] = n; + info.nRefCount++; } } } diff --git a/src/addrman.cpp b/src/addrman.cpp --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -10,21 +10,23 @@ #include -int CAddrInfo::GetTriedBucket(const uint256 &nKey) const { +int CAddrInfo::GetTriedBucket(const uint256 &nKey, + const std::vector &asmap) const { uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetKey()).GetCheapHash(); - uint64_t hash2 = - (CHashWriter(SER_GETHASH, 0) - << nKey << GetGroup() << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)) - .GetCheapHash(); + uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) + << nKey << GetGroup(asmap) + << (hash1 % ADDRMAN_TRIED_BUCKETS_PER_GROUP)) + .GetCheapHash(); return hash2 % ADDRMAN_TRIED_BUCKET_COUNT; } -int CAddrInfo::GetNewBucket(const uint256 &nKey, const CNetAddr &src) const { - std::vector vchSourceGroupKey = src.GetGroup(); - uint64_t hash1 = - (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup() << vchSourceGroupKey) - .GetCheapHash(); +int CAddrInfo::GetNewBucket(const uint256 &nKey, const CNetAddr &src, + const std::vector &asmap) const { + std::vector vchSourceGroupKey = src.GetGroup(asmap); + uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) + << nKey << GetGroup(asmap) << vchSourceGroupKey) + .GetCheapHash(); uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << vchSourceGroupKey << (hash1 % ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP)) @@ -175,7 +177,7 @@ assert(info.nRefCount == 0); // which tried bucket to move the entry to - int nKBucket = info.GetTriedBucket(nKey); + int nKBucket = info.GetTriedBucket(nKey, m_asmap); int nKBucketPos = info.GetBucketPosition(nKey, false, nKBucket); // first make space to add it (the existing tried entry there is moved to @@ -192,7 +194,7 @@ nTried--; // find which new bucket it belongs to - int nUBucket = infoOld.GetNewBucket(nKey); + int nUBucket = infoOld.GetNewBucket(nKey, m_asmap); int nUBucketPos = infoOld.GetBucketPosition(nKey, true, nUBucket); ClearNew(nUBucket, nUBucketPos); assert(vvNew[nUBucket][nUBucketPos] == -1); @@ -261,7 +263,7 @@ } // which tried bucket to move the entry to - int tried_bucket = info.GetTriedBucket(nKey); + int tried_bucket = info.GetTriedBucket(nKey, m_asmap); int tried_bucket_pos = info.GetBucketPosition(nKey, false, tried_bucket); // Will moving this address into tried evict another entry? @@ -348,7 +350,7 @@ fNew = true; } - int nUBucket = pinfo->GetNewBucket(nKey, source); + int nUBucket = pinfo->GetNewBucket(nKey, source, m_asmap); int nUBucketPos = pinfo->GetBucketPosition(nKey, true, nUBucket); if (vvNew[nUBucket][nUBucketPos] != nId) { bool fInsert = vvNew[nUBucket][nUBucketPos] == -1; @@ -514,7 +516,7 @@ if (!setTried.count(vvTried[n][i])) { return -11; } - if (mapInfo[vvTried[n][i]].GetTriedBucket(nKey) != n) { + if (mapInfo[vvTried[n][i]].GetTriedBucket(nKey, m_asmap) != n) { return -17; } if (mapInfo[vvTried[n][i]].GetBucketPosition(nKey, false, n) != @@ -640,7 +642,7 @@ CAddrInfo &info_new = id_new_it->second; // Which tried bucket to move the entry to. - int tried_bucket = info_new.GetTriedBucket(nKey); + int tried_bucket = info_new.GetTriedBucket(nKey, m_asmap); int tried_bucket_pos = info_new.GetBucketPosition(nKey, false, tried_bucket); if (!info_new.IsValid()) { @@ -720,10 +722,32 @@ CAddrInfo &newInfo = id_new_it->second; // which tried bucket to move the entry to - int tried_bucket = newInfo.GetTriedBucket(nKey); + int tried_bucket = newInfo.GetTriedBucket(nKey, m_asmap); int tried_bucket_pos = newInfo.GetBucketPosition(nKey, false, tried_bucket); int id_old = vvTried[tried_bucket][tried_bucket_pos]; return mapInfo[id_old]; } + +std::vector CAddrMan::DecodeAsmap(fs::path path) { + std::vector bits; + FILE *filestr = fsbridge::fopen(path, "rb"); + CAutoFile file(filestr, SER_DISK, CLIENT_VERSION); + if (file.IsNull()) { + LogPrintf("Failed to open asmap file from disk.\n"); + return bits; + } + fseek(filestr, 0, SEEK_END); + int length = ftell(filestr); + LogPrintf("Opened asmap file %s (%d bytes) from disk.\n", path, length); + fseek(filestr, 0, SEEK_SET); + char cur_byte; + for (int i = 0; i < length; ++i) { + file >> cur_byte; + for (int bit = 0; bit < 8; ++bit) { + bits.push_back((cur_byte >> bit) & 1); + } + } + return bits; +} diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -92,6 +94,8 @@ #define MIN_CORE_FILEDESCRIPTORS 150 #endif +static const char *DEFAULT_ASMAP_FILENAME = "ip_asn.map"; + /** * The PID file facilities. */ @@ -710,6 +714,10 @@ "Tor control port password (default: empty)", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::CONNECTION); + gArgs.AddArg("-asmap=", + "Specify asn mapping used for bucketing of the peers. Path " + "should be relative to the -datadir path.", + ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); #ifdef USE_UPNP #if USE_UPNP gArgs.AddArg("-upnp", @@ -2765,6 +2773,28 @@ return false; } + // Read asmap file if configured + if (gArgs.IsArgSet("-asmap")) { + std::string asmap_file = gArgs.GetArg("-asmap", ""); + if (asmap_file.empty()) { + asmap_file = DEFAULT_ASMAP_FILENAME; + } + const fs::path asmap_path = GetDataDir() / asmap_file; + std::vector asmap = CAddrMan::DecodeAsmap(asmap_path); + if (asmap.size() == 0) { + InitError( + strprintf(_("Could not find or parse specified asmap: '%s'"), + asmap_path)); + return false; + } + node.connman->SetAsmap(asmap); + const uint256 asmap_version = SerializeHash(asmap); + LogPrintf("Using asmap version %s for IP bucketing.\n", + asmap_version.ToString()); + } else { + LogPrintf("Using /16 prefix for IP bucketing.\n"); + } + // Step 13: finished SetRPCWarmupFinished(); diff --git a/src/net.h b/src/net.h --- a/src/net.h +++ b/src/net.h @@ -168,6 +168,7 @@ bool m_use_addrman_outgoing = true; std::vector m_specified_outgoing; std::vector m_added_nodes; + std::vector m_asmap; }; void Init(const Options &connOptions) { @@ -356,6 +357,8 @@ */ int64_t PoissonNextSendInbound(int64_t now, int average_interval_seconds); + void SetAsmap(std::vector asmap) { addrman.m_asmap = asmap; } + private: struct ListenSocket { public: diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -1892,7 +1892,7 @@ // peers also have the added issue that they're attacker // controlled and could be used to prevent us from // connecting to particular hosts if we used them here. - setConnected.insert(pnode->addr.GetGroup()); + setConnected.insert(pnode->addr.GetGroup(addrman.m_asmap)); if (pnode->m_tx_relay == nullptr) { nOutboundBlockRelay++; } else if (!pnode->fFeeler) { @@ -1943,7 +1943,8 @@ // Require outbound connections, other than feelers, to be to // distinct network groups - if (!fFeeler && setConnected.count(addr.GetGroup())) { + if (!fFeeler && + setConnected.count(addr.GetGroup(addrman.m_asmap))) { break; } @@ -2995,7 +2996,7 @@ } uint64_t CConnman::CalculateKeyedNetGroup(const CAddress &ad) const { - std::vector vchNetGroup(ad.GetGroup()); + std::vector vchNetGroup(ad.GetGroup(addrman.m_asmap)); return GetDeterministicRandomizer(RANDOMIZER_ID_NETGROUP) .Write(vchNetGroup.data(), vchNetGroup.size()) diff --git a/src/netaddress.h b/src/netaddress.h --- a/src/netaddress.h +++ b/src/netaddress.h @@ -98,7 +98,7 @@ unsigned int GetByte(int n) const; uint64_t GetHash() const; bool GetInAddr(struct in_addr *pipv4Addr) const; - std::vector GetGroup() const; + std::vector GetGroup(const std::vector &asmap) const; std::vector GetAddrBytes() const { return {std::begin(ip), std::end(ip)}; } diff --git a/src/netaddress.cpp b/src/netaddress.cpp --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -7,6 +7,7 @@ #include #include +#include #include static const uint8_t pchIPv4[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; @@ -413,7 +414,7 @@ * @note No two connections will be attempted to addresses with the same network * group. */ -std::vector CNetAddr::GetGroup() const { +std::vector CNetAddr::GetGroup(const std::vector &asmap) const { std::vector vchRet; int nClass = NET_IPV6; int nStartByte = 0; @@ -463,6 +464,27 @@ nBits = 32; } + // If asmap is supplied and the address is IPv4/IPv6, + // ignore nBits and use 32/128 bits to obtain ASN from asmap. + // ASN is then returned to be used for bucketing. + if (asmap.size() != 0 && (nClass == NET_IPV4 || nClass == NET_IPV6)) { + nClass = NET_IPV6; + std::vector ip_bits(128); + for (int8_t byte_i = 0; byte_i < 16; ++byte_i) { + uint8_t cur_byte = GetByte(15 - byte_i); + for (uint8_t bit_i = 0; bit_i < 8; ++bit_i) { + ip_bits[byte_i * 8 + bit_i] = (cur_byte >> (7 - bit_i)) & 1; + } + } + + uint32_t asn = Interpret(asmap, ip_bits); + vchRet.push_back(nClass); + for (int i = 0; i < 4; i++) { + vchRet.push_back((asn >> (8 * i)) & 0xFF); + } + return vchRet; + } + vchRet.push_back(nClass); // push our ip onto vchRet byte by byte... diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt --- a/src/test/CMakeLists.txt +++ b/src/test/CMakeLists.txt @@ -73,12 +73,40 @@ ) endif() +function(gen_asmap_headers HEADERS_VAR) + foreach(INPUT_FILE ${ARGN}) + set(OUTPUT_FILE "${CMAKE_CURRENT_BINARY_DIR}/${INPUT_FILE}.h") + + add_custom_command( + OUTPUT "${OUTPUT_FILE}" + COMMENT "Generate ASMAP header from ${INPUT_FILE}" + COMMAND + "${Python_EXECUTABLE}" + "${CMAKE_CURRENT_SOURCE_DIR}/data/generate_asmap.py" + "${CMAKE_CURRENT_SOURCE_DIR}/${INPUT_FILE}" + "${OUTPUT_FILE}" + MAIN_DEPENDENCY "${INPUT_FILE}" + DEPENDS + "data/generate_header.py" + VERBATIM + ) + list(APPEND ${HEADERS_VAR} "${OUTPUT_FILE}") + endforeach() + set(${HEADERS_VAR} ${${HEADERS_VAR}} PARENT_SCOPE) +endfunction() + +gen_asmap_headers(ASMAP_HEADERS + data/asmap.raw +) + add_boost_unit_tests_to_suite(bitcoin test_bitcoin fixture.cpp jsonutil.cpp scriptflags.cpp sigutil.cpp + ${ASMAP_HEADERS} + # Tests generated from JSON ${JSON_HEADERS} diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -6,7 +6,9 @@ #include #include #include +#include +#include #include #include @@ -14,12 +16,18 @@ #include class CAddrManTest : public CAddrMan { +private: + bool deterministic; + public: - explicit CAddrManTest(bool makeDeterministic = true) { + explicit CAddrManTest(bool makeDeterministic = true, + std::vector asmap = std::vector()) { if (makeDeterministic) { // Set addrman addr placement to be deterministic. MakeDeterministic(); } + deterministic = makeDeterministic; + m_asmap = asmap; } //! Ensure that bucket placement is always the same for testing purposes. @@ -44,6 +52,20 @@ CAddrMan::Delete(nId); } + // Used to test deserialization + std::pair GetBucketAndEntry(const CAddress &addr) { + LOCK(cs); + int nId = mapAddr[addr]; + for (int bucket = 0; bucket < ADDRMAN_NEW_BUCKET_COUNT; ++bucket) { + for (int entry = 0; entry < ADDRMAN_BUCKET_SIZE; ++entry) { + if (nId == vvNew[bucket][entry]) { + return std::pair(bucket, entry); + } + } + } + return std::pair(-1, -1); + } + // Simulates connection failure so that we can test eviction of offline // nodes void SimConnFail(CService &addr) { @@ -56,6 +78,14 @@ int64_t nLastTry = GetAdjustedTime() - 61; Attempt(addr, count_failure, nLastTry); } + + void Clear() { + CAddrMan::Clear(); + if (deterministic) { + nKey.SetNull(); + insecure_rand = FastRandomContext(true); + } + } }; static CNetAddr ResolveIP(const char *ip) { @@ -80,6 +110,17 @@ return ResolveService(ip.c_str(), port); } +static std::vector FromBytes(const uint8_t *source, int vector_size) { + std::vector result(vector_size); + for (int byte_i = 0; byte_i < vector_size / 8; ++byte_i) { + uint8_t cur_byte = source[byte_i]; + for (int bit_i = 0; bit_i < 8; ++bit_i) { + result[byte_i * 8 + bit_i] = (cur_byte >> bit_i) & 1; + } + } + return result; +} + BOOST_FIXTURE_TEST_SUITE(addrman_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(addrman_simple) { @@ -399,7 +440,7 @@ BOOST_CHECK_EQUAL(addrman.size(), 2006U); } -BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket) { +BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket_legacy) { CAddrManTest addrman; CAddress addr1 = CAddress(ResolveService("250.1.1.1", 8333), NODE_NONE); @@ -412,29 +453,34 @@ uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); - BOOST_CHECK_EQUAL(info1.GetTriedBucket(nKey1), 40); + // use /16 + std::vector asmap; + + BOOST_CHECK_EQUAL(info1.GetTriedBucket(nKey1, asmap), 40); // Test: Make sure key actually randomizes bucket placement. A fail on // this test could be a security issue. - BOOST_CHECK(info1.GetTriedBucket(nKey1) != info1.GetTriedBucket(nKey2)); + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != + info1.GetTriedBucket(nKey2, asmap)); // Test: Two addresses with same IP but different ports can map to // different buckets because they have different keys. CAddrInfo info2 = CAddrInfo(addr2, source1); BOOST_CHECK(info1.GetKey() != info2.GetKey()); - BOOST_CHECK(info1.GetTriedBucket(nKey1) != info2.GetTriedBucket(nKey1)); + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != + info2.GetTriedBucket(nKey1, asmap)); std::set buckets; for (int i = 0; i < 255; i++) { CAddrInfo infoi = CAddrInfo( CAddress(ResolveService("250.1.1." + std::to_string(i)), NODE_NONE), ResolveIP("250.1.1." + std::to_string(i))); - int bucket = infoi.GetTriedBucket(nKey1); + int bucket = infoi.GetTriedBucket(nKey1, asmap); buckets.insert(bucket); } - // Test: IP addresses in the same group (\16 prefix for IPv4) should - // never get more than 8 buckets + // Test: IP addresses in the same /16 prefix should never get more than 8 + // buckets with legacy grouping BOOST_CHECK_EQUAL(buckets.size(), 8U); buckets.clear(); @@ -443,15 +489,15 @@ CAddress(ResolveService("250." + std::to_string(j) + ".1.1"), NODE_NONE), ResolveIP("250." + std::to_string(j) + ".1.1")); - int bucket = infoj.GetTriedBucket(nKey1); + int bucket = infoj.GetTriedBucket(nKey1, asmap); buckets.insert(bucket); } - // Test: IP addresses in the different groups should map to more than - // 8 buckets. + // Test: IP addresses in the different /16 prefix should map to more than 8 + // buckets with legacy grouping BOOST_CHECK_EQUAL(buckets.size(), 160U); } -BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) { +BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket_legacy) { CAddrManTest addrman; CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); @@ -464,25 +510,30 @@ uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); + // use /16 + std::vector asmap; + // Test: Make sure the buckets are what we expect - BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1), 786); - BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, source1), 786); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), 786); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, source1, asmap), 786); // Test: Make sure key actually randomizes bucket placement. A fail on // this test could be a security issue. - BOOST_CHECK(info1.GetNewBucket(nKey1) != info1.GetNewBucket(nKey2)); + BOOST_CHECK(info1.GetNewBucket(nKey1, asmap) != + info1.GetNewBucket(nKey2, asmap)); // Test: Ports should not affect bucket placement in the addr CAddrInfo info2 = CAddrInfo(addr2, source1); BOOST_CHECK(info1.GetKey() != info2.GetKey()); - BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1), info2.GetNewBucket(nKey1)); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), + info2.GetNewBucket(nKey1, asmap)); std::set buckets; for (int i = 0; i < 255; i++) { CAddrInfo infoi = CAddrInfo( CAddress(ResolveService("250.1.1." + std::to_string(i)), NODE_NONE), ResolveIP("250.1.1." + std::to_string(i))); - int bucket = infoi.GetNewBucket(nKey1); + int bucket = infoi.GetNewBucket(nKey1, asmap); buckets.insert(bucket); } // Test: IP addresses in the same group (\16 prefix for IPv4) should @@ -496,10 +547,10 @@ std::to_string(j % 256) + ".1.1"), NODE_NONE), ResolveIP("251.4.1.1")); - int bucket = infoj.GetNewBucket(nKey1); + int bucket = infoj.GetNewBucket(nKey1, asmap); buckets.insert(bucket); } - // Test: IP addresses in the same source groups should map to no more + // Test: IP addresses in the same source groups should map to NO MORE // than 64 buckets. BOOST_CHECK(buckets.size() <= 64); @@ -508,7 +559,7 @@ CAddrInfo infoj = CAddrInfo(CAddress(ResolveService("250.1.1.1"), NODE_NONE), ResolveIP("250." + std::to_string(p) + ".1.1")); - int bucket = infoj.GetNewBucket(nKey1); + int bucket = infoj.GetNewBucket(nKey1, asmap); buckets.insert(bucket); } // Test: IP addresses in the different source groups should map to more @@ -516,6 +567,235 @@ BOOST_CHECK(buckets.size() > 64); } +// The following three test cases use asmap.raw +// We use an artificial minimal mock mapping +// 250.0.0.0/8 AS1000 +// 101.1.0.0/16 AS1 +// 101.2.0.0/16 AS2 +// 101.3.0.0/16 AS3 +// 101.4.0.0/16 AS4 +// 101.5.0.0/16 AS5 +// 101.6.0.0/16 AS6 +// 101.7.0.0/16 AS7 +// 101.8.0.0/16 AS8 +BOOST_AUTO_TEST_CASE(caddrinfo_get_tried_bucket) { + CAddrManTest addrman; + + CAddress addr1 = CAddress(ResolveService("250.1.1.1", 8333), NODE_NONE); + CAddress addr2 = CAddress(ResolveService("250.1.1.1", 9999), NODE_NONE); + + CNetAddr source1 = ResolveIP("250.1.1.1"); + + CAddrInfo info1 = CAddrInfo(addr1, source1); + + uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); + uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); + + std::vector asmap = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); + + BOOST_CHECK_EQUAL(info1.GetTriedBucket(nKey1, asmap), 236); + + // Test: Make sure key actually randomizes bucket placement. A fail on + // this test could be a security issue. + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != + info1.GetTriedBucket(nKey2, asmap)); + + // Test: Two addresses with same IP but different ports can map to + // different buckets because they have different keys. + CAddrInfo info2 = CAddrInfo(addr2, source1); + + BOOST_CHECK(info1.GetKey() != info2.GetKey()); + BOOST_CHECK(info1.GetTriedBucket(nKey1, asmap) != + info2.GetTriedBucket(nKey1, asmap)); + + std::set buckets; + for (int j = 0; j < 255; j++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService("101." + std::to_string(j) + ".1.1"), + NODE_NONE), + ResolveIP("101." + std::to_string(j) + ".1.1")); + int bucket = infoj.GetTriedBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different /16 prefix MAY map to more than + // 8 buckets. + BOOST_CHECK(buckets.size() > 8); + + buckets.clear(); + for (int j = 0; j < 255; j++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService("250." + std::to_string(j) + ".1.1"), + NODE_NONE), + ResolveIP("250." + std::to_string(j) + ".1.1")); + int bucket = infoj.GetTriedBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different /16 prefix MAY NOT map to more than + // 8 buckets. + BOOST_CHECK(buckets.size() == 8); +} + +BOOST_AUTO_TEST_CASE(caddrinfo_get_new_bucket) { + CAddrManTest addrman; + + CAddress addr1 = CAddress(ResolveService("250.1.2.1", 8333), NODE_NONE); + CAddress addr2 = CAddress(ResolveService("250.1.2.1", 9999), NODE_NONE); + + CNetAddr source1 = ResolveIP("250.1.2.1"); + + CAddrInfo info1 = CAddrInfo(addr1, source1); + + uint256 nKey1 = (uint256)(CHashWriter(SER_GETHASH, 0) << 1).GetHash(); + uint256 nKey2 = (uint256)(CHashWriter(SER_GETHASH, 0) << 2).GetHash(); + + std::vector asmap = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); + + // Test: Make sure the buckets are what we expect + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), 795); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, source1, asmap), 795); + + // Test: Make sure key actually randomizes bucket placement. A fail on + // this test could be a security issue. + BOOST_CHECK(info1.GetNewBucket(nKey1, asmap) != + info1.GetNewBucket(nKey2, asmap)); + + // Test: Ports should not affect bucket placement in the addr + CAddrInfo info2 = CAddrInfo(addr2, source1); + BOOST_CHECK(info1.GetKey() != info2.GetKey()); + BOOST_CHECK_EQUAL(info1.GetNewBucket(nKey1, asmap), + info2.GetNewBucket(nKey1, asmap)); + + std::set buckets; + for (int i = 0; i < 255; i++) { + CAddrInfo infoi = CAddrInfo( + CAddress(ResolveService("250.1.1." + std::to_string(i)), NODE_NONE), + ResolveIP("250.1.1." + std::to_string(i))); + int bucket = infoi.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the same /16 prefix + // usually map to the same bucket. + BOOST_CHECK_EQUAL(buckets.size(), 1U); + + buckets.clear(); + for (int j = 0; j < 4 * 255; j++) { + CAddrInfo infoj = CAddrInfo( + CAddress(ResolveService(std::to_string(250 + (j / 255)) + "." + + std::to_string(j % 256) + ".1.1"), + NODE_NONE), + ResolveIP("251.4.1.1")); + int bucket = infoj.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the same source /16 prefix should not map to more + // than 64 buckets. + BOOST_CHECK(buckets.size() <= 64); + + buckets.clear(); + for (int p = 0; p < 255; p++) { + CAddrInfo infoj = + CAddrInfo(CAddress(ResolveService("250.1.1.1"), NODE_NONE), + ResolveIP("101." + std::to_string(p) + ".1.1")); + int bucket = infoj.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different source /16 prefixes usually map to + // MORE than 1 bucket. + BOOST_CHECK(buckets.size() > 1); + + buckets.clear(); + for (int p = 0; p < 255; p++) { + CAddrInfo infoj = + CAddrInfo(CAddress(ResolveService("250.1.1.1"), NODE_NONE), + ResolveIP("250." + std::to_string(p) + ".1.1")); + int bucket = infoj.GetNewBucket(nKey1, asmap); + buckets.insert(bucket); + } + // Test: IP addresses in the different source /16 prefixes sometimes map to + // NO MORE than 1 bucket. + BOOST_CHECK(buckets.size() == 1); +} + +BOOST_AUTO_TEST_CASE(addrman_serialization) { + std::vector asmap1 = FromBytes(asmap_raw, sizeof(asmap_raw) * 8); + + CAddrManTest addrman_asmap1(true, asmap1); + CAddrManTest addrman_asmap1_dup(true, asmap1); + CAddrManTest addrman_noasmap; + CDataStream stream(SER_NETWORK, PROTOCOL_VERSION); + + CAddress addr = CAddress(ResolveService("250.1.1.1"), NODE_NONE); + CNetAddr default_source; + + addrman_asmap1.Add(addr, default_source); + + stream << addrman_asmap1; + // serizalizing/deserializing addrman with the same asmap + stream >> addrman_asmap1_dup; + + std::pair bucketAndEntry_asmap1 = + addrman_asmap1.GetBucketAndEntry(addr); + std::pair bucketAndEntry_asmap1_dup = + addrman_asmap1_dup.GetBucketAndEntry(addr); + BOOST_CHECK(bucketAndEntry_asmap1.second != -1); + BOOST_CHECK(bucketAndEntry_asmap1_dup.second != -1); + + BOOST_CHECK(bucketAndEntry_asmap1.first == bucketAndEntry_asmap1_dup.first); + BOOST_CHECK(bucketAndEntry_asmap1.second == + bucketAndEntry_asmap1_dup.second); + + // deserializing asmaped peers.dat to non-asmaped addrman + stream << addrman_asmap1; + stream >> addrman_noasmap; + std::pair bucketAndEntry_noasmap = + addrman_noasmap.GetBucketAndEntry(addr); + BOOST_CHECK(bucketAndEntry_noasmap.second != -1); + BOOST_CHECK(bucketAndEntry_asmap1.first != bucketAndEntry_noasmap.first); + BOOST_CHECK(bucketAndEntry_asmap1.second != bucketAndEntry_noasmap.second); + + // deserializing non-asmaped peers.dat to asmaped addrman + addrman_asmap1.Clear(); + addrman_noasmap.Clear(); + addrman_noasmap.Add(addr, default_source); + stream << addrman_noasmap; + stream >> addrman_asmap1; + std::pair bucketAndEntry_asmap1_deser = + addrman_asmap1.GetBucketAndEntry(addr); + BOOST_CHECK(bucketAndEntry_asmap1_deser.second != -1); + BOOST_CHECK(bucketAndEntry_asmap1_deser.first != + bucketAndEntry_noasmap.first); + BOOST_CHECK(bucketAndEntry_asmap1_deser.first == + bucketAndEntry_asmap1_dup.first); + BOOST_CHECK(bucketAndEntry_asmap1_deser.second == + bucketAndEntry_asmap1_dup.second); + + // used to map to different buckets, now maps to the same bucket. + addrman_asmap1.Clear(); + addrman_noasmap.Clear(); + CAddress addr1 = CAddress(ResolveService("250.1.1.1"), NODE_NONE); + CAddress addr2 = CAddress(ResolveService("250.2.1.1"), NODE_NONE); + addrman_noasmap.Add(addr, default_source); + addrman_noasmap.Add(addr2, default_source); + std::pair bucketAndEntry_noasmap_addr1 = + addrman_noasmap.GetBucketAndEntry(addr1); + std::pair bucketAndEntry_noasmap_addr2 = + addrman_noasmap.GetBucketAndEntry(addr2); + BOOST_CHECK(bucketAndEntry_noasmap_addr1.first != + bucketAndEntry_noasmap_addr2.first); + BOOST_CHECK(bucketAndEntry_noasmap_addr1.second != + bucketAndEntry_noasmap_addr2.second); + stream << addrman_noasmap; + stream >> addrman_asmap1; + std::pair bucketAndEntry_asmap1_deser_addr1 = + addrman_asmap1.GetBucketAndEntry(addr1); + std::pair bucketAndEntry_asmap1_deser_addr2 = + addrman_asmap1.GetBucketAndEntry(addr2); + BOOST_CHECK(bucketAndEntry_asmap1_deser_addr1.first == + bucketAndEntry_asmap1_deser_addr2.first); + BOOST_CHECK(bucketAndEntry_asmap1_deser_addr1.second != + bucketAndEntry_asmap1_deser_addr2.second); +} + BOOST_AUTO_TEST_CASE(addrman_selecttriedcollision) { CAddrManTest addrman; diff --git a/src/test/data/asmap.raw b/src/test/data/asmap.raw new file mode 100644 index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@ asmap; typedef std::vector Vec8; // Local -> !Routable() - BOOST_CHECK(ResolveIP("127.0.0.1").GetGroup() == Vec8{0}); + BOOST_CHECK(ResolveIP("127.0.0.1").GetGroup(asmap) == Vec8{0}); // !Valid -> !Routable() - BOOST_CHECK(ResolveIP("257.0.0.1").GetGroup() == Vec8{0}); + BOOST_CHECK(ResolveIP("257.0.0.1").GetGroup(asmap) == Vec8{0}); // RFC1918 -> !Routable() - BOOST_CHECK(ResolveIP("10.0.0.1").GetGroup() == Vec8{0}); + BOOST_CHECK(ResolveIP("10.0.0.1").GetGroup(asmap) == Vec8{0}); // RFC3927 -> !Routable() - BOOST_CHECK(ResolveIP("169.254.1.1").GetGroup() == Vec8{0}); + BOOST_CHECK(ResolveIP("169.254.1.1").GetGroup(asmap) == Vec8{0}); // IPv4 - BOOST_CHECK(ResolveIP("1.2.3.4").GetGroup() == Vec8({NET_IPV4, 1, 2})); + BOOST_CHECK(ResolveIP("1.2.3.4").GetGroup(asmap) == Vec8({NET_IPV4, 1, 2})); // RFC6145 - BOOST_CHECK(ResolveIP("::FFFF:0:102:304").GetGroup() == + BOOST_CHECK(ResolveIP("::FFFF:0:102:304").GetGroup(asmap) == Vec8({NET_IPV4, 1, 2})); // RFC6052 - BOOST_CHECK(ResolveIP("64:FF9B::102:304").GetGroup() == + BOOST_CHECK(ResolveIP("64:FF9B::102:304").GetGroup(asmap) == Vec8({NET_IPV4, 1, 2})); // RFC3964 - BOOST_CHECK(ResolveIP("2002:102:304:9999:9999:9999:9999:9999").GetGroup() == - Vec8({NET_IPV4, 1, 2})); + BOOST_CHECK( + ResolveIP("2002:102:304:9999:9999:9999:9999:9999").GetGroup(asmap) == + Vec8({NET_IPV4, 1, 2})); // RFC4380 - BOOST_CHECK(ResolveIP("2001:0:9999:9999:9999:9999:FEFD:FCFB").GetGroup() == - Vec8({NET_IPV4, 1, 2})); + BOOST_CHECK( + ResolveIP("2001:0:9999:9999:9999:9999:FEFD:FCFB").GetGroup(asmap) == + Vec8({NET_IPV4, 1, 2})); // Tor BOOST_CHECK( - ResolveIP("FD87:D87E:EB43:edb1:8e4:3588:e546:35ca").GetGroup() == + ResolveIP("FD87:D87E:EB43:edb1:8e4:3588:e546:35ca").GetGroup(asmap) == Vec8({NET_ONION, 239})); // he.net BOOST_CHECK( - ResolveIP("2001:470:abcd:9999:9999:9999:9999:9999").GetGroup() == + ResolveIP("2001:470:abcd:9999:9999:9999:9999:9999").GetGroup(asmap) == Vec8({NET_IPV6, 32, 1, 4, 112, 175})); // IPv6 BOOST_CHECK( - ResolveIP("2001:2001:9999:9999:9999:9999:9999:9999").GetGroup() == + ResolveIP("2001:2001:9999:9999:9999:9999:9999:9999").GetGroup(asmap) == Vec8({NET_IPV6, 32, 1, 32, 1})); // baz.net sha256 hash: // 12929400eb4607c4ac075f087167e75286b179c693eb059a01774b864e8fe505 Vec8 internal_group = {NET_INTERNAL, 0x12, 0x92, 0x94, 0x00, 0xeb, 0x46, 0x07, 0xc4, 0xac, 0x07}; - BOOST_CHECK(CreateInternal("baz.net").GetGroup() == internal_group); + BOOST_CHECK(CreateInternal("baz.net").GetGroup(asmap) == internal_group); } BOOST_AUTO_TEST_CASE(netbase_parsenetwork) {