From a237ba4230d3aa293d7fa504d7ebab5792fd00ff Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Thu, 27 Aug 2020 11:03:21 +0200 Subject: [PATCH] net: recognize TORv3/I2P/CJDNS networks Recognizing addresses from those networks allows us to accept and gossip them, even though we don't know how to connect to them (yet). Co-authored-by: eriknylund --- src/crypto/common.h | 7 ++ src/netaddress.cpp | 243 +++++++++++++++++++++++++++++++------- src/netaddress.h | 37 +++++- src/test/base32_tests.cpp | 5 +- src/test/net_tests.cpp | 104 +++++++++++++++- src/utilstrencodings.cpp | 13 +- src/utilstrencodings.h | 16 ++- 7 files changed, 367 insertions(+), 58 deletions(-) diff --git a/src/crypto/common.h b/src/crypto/common.h index 6906ee7807b27..7c4e09939c8d9 100644 --- a/src/crypto/common.h +++ b/src/crypto/common.h @@ -53,6 +53,13 @@ void static inline WriteLE64(unsigned char* ptr, uint64_t x) memcpy(ptr, (char*)&v, 8); } +uint16_t static inline ReadBE16(const unsigned char* ptr) +{ + uint16_t x; + memcpy((char*)&x, ptr, 2); + return be16toh(x); +} + uint32_t static inline ReadBE32(const unsigned char* ptr) { uint32_t x; diff --git a/src/netaddress.cpp b/src/netaddress.cpp index af2968dc3f9b8..dcb93e0c51dfd 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -4,8 +4,11 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include "crypto/common.h" +#include "crypto/sha3.h" #include "netaddress.h" #include "hash.h" +#include "prevector.h" #include "tinyformat.h" #include "util/asmap.h" #include "util/string.h" @@ -29,7 +32,18 @@ CNetAddr::BIP155Network CNetAddr::GetBIP155Network() const case NET_IPV6: return BIP155Network::IPV6; case NET_ONION: - return BIP155Network::TORV2; + switch (m_addr.size()) { + case ADDR_TORV2_SIZE: + return BIP155Network::TORV2; + case ADDR_TORV3_SIZE: + return BIP155Network::TORV3; + default: + assert(false); + } + case NET_I2P: + return BIP155Network::I2P; + case NET_CJDNS: + return BIP155Network::CJDNS; case NET_INTERNAL: // should have been handled before calling this function case NET_UNROUTABLE: // m_net is never and should not be set to NET_UNROUTABLE case NET_MAX: // m_net is never and should not be set to NET_MAX @@ -66,6 +80,30 @@ bool CNetAddr::SetNetFromBIP155Network(uint8_t possible_bip155_net, size_t addre throw std::ios_base::failure( strprintf("BIP155 TORv2 address with length %u (should be %u)", address_size, ADDR_TORV2_SIZE)); + case BIP155Network::TORV3: + if (address_size == ADDR_TORV3_SIZE) { + m_net = NET_ONION; + return true; + } + throw std::ios_base::failure( + strprintf("BIP155 TORv3 address with length %u (should be %u)", address_size, + ADDR_TORV3_SIZE)); + case BIP155Network::I2P: + if (address_size == ADDR_I2P_SIZE) { + m_net = NET_I2P; + return true; + } + throw std::ios_base::failure( + strprintf("BIP155 I2P address with length %u (should be %u)", address_size, + ADDR_I2P_SIZE)); + case BIP155Network::CJDNS: + if (address_size == ADDR_CJDNS_SIZE) { + m_net = NET_CJDNS; + return true; + } + throw std::ios_base::failure( + strprintf("BIP155 CJDNS address with length %u (should be %u)", address_size, + ADDR_CJDNS_SIZE)); } // Don't throw on addresses with unknown network ids (maybe from the future). @@ -92,7 +130,13 @@ void CNetAddr::SetIP(const CNetAddr& ipIn) assert(ipIn.m_addr.size() == ADDR_IPV6_SIZE); break; case NET_ONION: - assert(ipIn.m_addr.size() == ADDR_TORV2_SIZE); + assert(ipIn.m_addr.size() == ADDR_TORV2_SIZE || ipIn.m_addr.size() == ADDR_TORV3_SIZE); + break; + case NET_I2P: + assert(ipIn.m_addr.size() == ADDR_I2P_SIZE); + break; + case NET_CJDNS: + assert(ipIn.m_addr.size() == ADDR_CJDNS_SIZE); break; case NET_INTERNAL: assert(ipIn.m_addr.size() == ADDR_INTERNAL_SIZE); @@ -150,24 +194,80 @@ bool CNetAddr::SetInternal(const std::string &name) return true; } +namespace torv3 { +// https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n2135 +static constexpr size_t CHECKSUM_LEN = 2; +static const unsigned char VERSION[] = {3}; +static constexpr size_t TOTAL_LEN = ADDR_TORV3_SIZE + CHECKSUM_LEN + sizeof(VERSION); + +static void Checksum(Span addr_pubkey, uint8_t (&checksum)[CHECKSUM_LEN]) +{ + // TORv3 CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2] + static const unsigned char prefix[] = ".onion checksum"; + static constexpr size_t prefix_len = 15; + + SHA3_256 hasher; + + hasher.Write(MakeSpan(prefix).first(prefix_len)); + hasher.Write(addr_pubkey); + hasher.Write(VERSION); + + uint8_t checksum_full[SHA3_256::OUTPUT_SIZE]; + + hasher.Finalize(checksum_full); + + memcpy(checksum, checksum_full, sizeof(checksum)); +} + +}; // namespace torv3 + /** - * Parse a TORv2 address and set this object to it. + * Parse a TOR address and set this object to it. * * @returns Whether or not the operation was successful. * * @see CNetAddr::IsTor() */ -bool CNetAddr::SetSpecial(const std::string& strName) +bool CNetAddr::SetSpecial(const std::string& str) { - if (strName.size()>6 && strName.substr(strName.size() - 6, 6) == ".onion") { - std::vector vchAddr = DecodeBase32(strName.substr(0, strName.size() - 6).c_str()); - if (vchAddr.size() != ADDR_TORV2_SIZE) { + static const char* suffix{".onion"}; + static constexpr size_t suffix_len{6}; + + if (!ValidAsCString(str) || str.size() <= suffix_len || + str.substr(str.size() - suffix_len) != suffix) { + return false; + } + + bool invalid; + const auto& input = DecodeBase32(str.substr(0, str.size() - suffix_len).c_str(), &invalid); + + if (invalid) { + return false; + } + + switch (input.size()) { + case ADDR_TORV2_SIZE: + m_net = NET_ONION; + m_addr.assign(input.begin(), input.end()); + return true; + case torv3::TOTAL_LEN: { + Span input_pubkey{input.data(), ADDR_TORV3_SIZE}; + Span input_checksum{input.data() + ADDR_TORV3_SIZE, torv3::CHECKSUM_LEN}; + Span input_version{input.data() + ADDR_TORV3_SIZE + torv3::CHECKSUM_LEN, sizeof(torv3::VERSION)}; + + uint8_t calculated_checksum[torv3::CHECKSUM_LEN]; + torv3::Checksum(input_pubkey, calculated_checksum); + + if (input_checksum != calculated_checksum || input_version != torv3::VERSION) { return false; } + m_net = NET_ONION; - m_addr.assign(vchAddr.begin(), vchAddr.end()); + m_addr.assign(input_pubkey.begin(), input_pubkey.end()); return true; } + } + return false; } @@ -284,13 +384,21 @@ bool CNetAddr::IsHeNet() const } /** - * @returns Whether or not this is a dummy address that maps an onion address - * into IPv6. - * + * Check whether this object represents a TOR address. * @see CNetAddr::SetSpecial(const std::string &) */ bool CNetAddr::IsTor() const { return m_net == NET_ONION; } +/** + * Check whether this object represents an I2P address. + */ +bool CNetAddr::IsI2P() const { return m_net == NET_I2P; } + +/** + * Check whether this object represents a CJDNS address. + */ +bool CNetAddr::IsCJDNS() const { return m_net == NET_CJDNS; } + bool CNetAddr::IsLocal() const { // IPv4 loopback (127.0.0.0/8 or 0.0.0.0/8) @@ -369,28 +477,72 @@ enum Network CNetAddr::GetNetwork() const return m_net; } +static std::string IPv6ToString(Span a) +{ + assert(a.size() == ADDR_IPV6_SIZE); + // clang-format off + return strprintf("%x:%x:%x:%x:%x:%x:%x:%x", + ReadBE16(&a[0]), + ReadBE16(&a[2]), + ReadBE16(&a[4]), + ReadBE16(&a[6]), + ReadBE16(&a[8]), + ReadBE16(&a[10]), + ReadBE16(&a[12]), + ReadBE16(&a[14])); + // clang-format on +} + std::string CNetAddr::ToStringIP() const { - if (IsTor()) - return EncodeBase32(m_addr) + ".onion"; - if (IsInternal()) + switch (m_net) { + case NET_IPV4: + case NET_IPV6: { + CService serv(*this, 0); + struct sockaddr_storage sockaddr; + socklen_t socklen = sizeof(sockaddr); + if (serv.GetSockAddr((struct sockaddr*)&sockaddr, &socklen)) { + char name[1025] = ""; + if (!getnameinfo((const struct sockaddr*)&sockaddr, socklen, name, + sizeof(name), nullptr, 0, NI_NUMERICHOST)) + return std::string(name); + } + if (m_net == NET_IPV4) { + return strprintf("%u.%u.%u.%u", m_addr[0], m_addr[1], m_addr[2], m_addr[3]); + } + return IPv6ToString(m_addr); + } + case NET_ONION: + switch (m_addr.size()) { + case ADDR_TORV2_SIZE: + return EncodeBase32(m_addr) + ".onion"; + case ADDR_TORV3_SIZE: { + + uint8_t checksum[torv3::CHECKSUM_LEN]; + torv3::Checksum(m_addr, checksum); + + // TORv3 onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion" + prevector address{m_addr.begin(), m_addr.end()}; + address.insert(address.end(), checksum, checksum + torv3::CHECKSUM_LEN); + address.insert(address.end(), torv3::VERSION, torv3::VERSION + sizeof(torv3::VERSION)); + + return EncodeBase32(address) + ".onion"; + } + default: + assert(false); + } + case NET_I2P: + return EncodeBase32(m_addr, false /* don't pad with = */) + ".b32.i2p"; + case NET_CJDNS: + return IPv6ToString(m_addr); + case NET_INTERNAL: return EncodeBase32(m_addr) + ".internal"; - CService serv(*this, 0); - struct sockaddr_storage sockaddr; - socklen_t socklen = sizeof(sockaddr); - if (serv.GetSockAddr((struct sockaddr*)&sockaddr, &socklen)) { - char name[1025] = ""; - if (!getnameinfo((const struct sockaddr*)&sockaddr, socklen, name, sizeof(name), nullptr, 0, NI_NUMERICHOST)) - return std::string(name); - } - if (IsIPv4()) - return strprintf("%u.%u.%u.%u", m_addr[0], m_addr[1], m_addr[2], m_addr[3]); - assert(IsIPv6()); - return strprintf("%x:%x:%x:%x:%x:%x:%x:%x", - m_addr[0] << 8 | m_addr[1], m_addr[2] << 8 | m_addr[3], - m_addr[4] << 8 | m_addr[5], m_addr[6] << 8 | m_addr[7], - m_addr[8] << 8 | m_addr[9], m_addr[10] << 8 | m_addr[11], - m_addr[12] << 8 | m_addr[13], m_addr[14] << 8 | m_addr[15]); + case NET_UNROUTABLE: // m_net is never and should not be set to NET_UNROUTABLE + case NET_MAX: // m_net is never and should not be set to NET_MAX + assert(false); + } // no default case, so the compiler can warn about missing cases + + assert(false); } std::string CNetAddr::ToString() const @@ -464,21 +616,22 @@ uint32_t CNetAddr::GetLinkedIPv4() const assert(false); } -uint32_t CNetAddr::GetNetClass() const { - uint32_t net_class = NET_IPV6; - if (IsLocal()) { - net_class = 255; - } +uint32_t CNetAddr::GetNetClass() const +{ + // Make sure that if we return NET_IPV6, then IsIPv6() is true. The callers expect that. + + // Check for "internal" first because such addresses are also !IsRoutable() + // and we don't want to return NET_UNROUTABLE in that case. if (IsInternal()) { - net_class = NET_INTERNAL; - } else if (!IsRoutable()) { - net_class = NET_UNROUTABLE; - } else if (HasLinkedIPv4()) { - net_class = NET_IPV4; - } else if (IsTor()) { - net_class = NET_ONION; + return NET_INTERNAL; } - return net_class; + if (!IsRoutable()) { + return NET_UNROUTABLE; + } + if (HasLinkedIPv4()) { + return NET_IPV4; + } + return m_net; } uint32_t CNetAddr::GetMappedAS(const std::vector &asmap) const { @@ -553,7 +706,7 @@ std::vector CNetAddr::GetGroup(const std::vector &asmap) co vchRet.push_back((ipv4 >> 24) & 0xFF); vchRet.push_back((ipv4 >> 16) & 0xFF); return vchRet; - } else if (IsTor()) { + } else if (IsTor() || IsI2P() || IsCJDNS()) { nBits = 4; } else if (IsHeNet()) { // for he.net, use /36 groups @@ -768,7 +921,7 @@ std::string CService::ToStringPort() const std::string CService::ToStringIPPort() const { - if (IsIPv4() || IsTor() || IsInternal()) { + if (IsIPv4() || IsTor() || IsI2P() || IsInternal()) { return ToStringIP() + ":" + ToStringPort(); } else { return "[" + ToStringIP() + "]:" + ToStringPort(); diff --git a/src/netaddress.h b/src/netaddress.h index 19d8f1f4435a4..5bc26bce0d6c3 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -51,9 +51,15 @@ enum Network /// IPv6 NET_IPV6, - /// TORv2 + /// TOR (v2 or v3) NET_ONION, + /// I2P + NET_I2P, + + /// CJDNS + NET_CJDNS, + /// A set of addresses that represent the hash of a string or FQDN. We use /// them in CAddrMan to keep track of which DNS seeds were used. NET_INTERNAL, @@ -94,6 +100,16 @@ static constexpr size_t ADDR_IPV6_SIZE = 16; /// Size of TORv2 address (in bytes). static constexpr size_t ADDR_TORV2_SIZE = 10; +/// Size of TORv3 address (in bytes). This is the length of just the address +/// as used in BIP155, without the checksum and the version byte. +static constexpr size_t ADDR_TORV3_SIZE = 32; + +/// Size of I2P address (in bytes). +static constexpr size_t ADDR_I2P_SIZE = 32; + +/// Size of CJDNS address (in bytes). +static constexpr size_t ADDR_CJDNS_SIZE = 16; + /// Size of "internal" (NET_INTERNAL) address (in bytes). static constexpr size_t ADDR_INTERNAL_SIZE = 10; @@ -155,6 +171,8 @@ class CNetAddr bool IsRFC6145() const; // IPv6 IPv4-translated address (::FFFF:0:0:0/96) bool IsHeNet() const; // IPv6 Hurricane Electric - https://he.net (2001:0470::/36) bool IsTor() const; + bool IsI2P() const; + bool IsCJDNS() const; bool IsLocal() const; bool IsRoutable() const; bool IsInternal() const; @@ -223,6 +241,9 @@ friend class CSubNet; IPV4 = 1, IPV6 = 2, TORV2 = 3, + TORV3 = 4, + I2P = 5, + CJDNS = 6, }; /** @@ -249,13 +270,12 @@ friend class CSubNet; * @retval true the network was recognized, is valid and `m_net` was set * @retval false not recognised (from future?) and should be silently ignored * @throws std::ios_base::failure if the network is one of the BIP155 founding - * networks recognized by this software (id 1..3) and has wrong address size. + * networks (id 1..6) with wrong address size. */ bool SetNetFromBIP155Network(uint8_t possible_bip155_net, size_t address_size); /** * Serialize in pre-ADDRv2/BIP155 format to an array. - * Some addresses (e.g. TORv3) cannot be serialized in pre-BIP155 format. */ void SerializeV1Array(uint8_t (&arr)[V1_SERIALIZATION_SIZE]) const { @@ -273,6 +293,9 @@ friend class CSubNet; memcpy(arr + prefix_size, m_addr.data(), m_addr.size()); return; case NET_ONION: + if (m_addr.size() == ADDR_TORV3_SIZE) { + break; + } prefix_size = sizeof(TORV2_IN_IPV6_PREFIX); assert(prefix_size + m_addr.size() == sizeof(arr)); memcpy(arr, TORV2_IN_IPV6_PREFIX.data(), prefix_size); @@ -284,17 +307,21 @@ friend class CSubNet; memcpy(arr, INTERNAL_IN_IPV6_PREFIX.data(), prefix_size); memcpy(arr + prefix_size, m_addr.data(), m_addr.size()); return; + case NET_I2P: + break; + case NET_CJDNS: + break; case NET_UNROUTABLE: case NET_MAX: assert(false); } // no default case, so the compiler can warn about missing cases - assert(false); + // Serialize TORv3, I2P and CJDNS as all-zeros. + memset(arr, 0x0, V1_SERIALIZATION_SIZE); } /** * Serialize in pre-ADDRv2/BIP155 format to a stream. - * Some addresses (e.g. TORv3) cannot be serialized in pre-BIP155 format. */ template void SerializeV1Stream(Stream& s) const diff --git a/src/test/base32_tests.cpp b/src/test/base32_tests.cpp index 941c519b6390d..f810632c24d22 100644 --- a/src/test/base32_tests.cpp +++ b/src/test/base32_tests.cpp @@ -13,10 +13,13 @@ BOOST_AUTO_TEST_CASE(base32_testvectors) { static const std::string vstrIn[] = {"","f","fo","foo","foob","fooba","foobar"}; static const std::string vstrOut[] = {"","my======","mzxq====","mzxw6===","mzxw6yq=","mzxw6ytb","mzxw6ytboi======"}; + static const std::string vstrOutNoPadding[] = {"","my","mzxq","mzxw6","mzxw6yq","mzxw6ytb","mzxw6ytboi"}; for (unsigned int i=0; i> addr; + BOOST_CHECK(addr.IsValid()); + BOOST_CHECK(addr.IsTor()); + BOOST_CHECK_EQUAL(addr.ToString(), + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion"); + BOOST_REQUIRE(s.empty()); + + // Invalid TORv3, with bogus length. + s << MakeSpan(ParseHex("04" // network type (TORv3) + "00" // address length + "00" // address + )); + BOOST_CHECK_EXCEPTION(s >> addr, std::ios_base::failure, + HasReason("BIP155 TORv3 address with length 0 (should be 32)")); + BOOST_REQUIRE(!s.empty()); // The stream is not consumed on invalid input. + s.clear(); + + // Valid I2P. + s << MakeSpan(ParseHex("05" // network type (I2P) + "20" // address length + "a2894dabaec08c0051a481a6dac88b64" // address + "f98232ae42d4b6fd2fa81952dfe36a87")); + s >> addr; + BOOST_CHECK(addr.IsValid()); + BOOST_CHECK_EQUAL(addr.ToString(), + "ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p"); + BOOST_REQUIRE(s.empty()); + + // Invalid I2P, with bogus length. + s << MakeSpan(ParseHex("05" // network type (I2P) + "03" // address length + "00" // address + )); + BOOST_CHECK_EXCEPTION(s >> addr, std::ios_base::failure, + HasReason("BIP155 I2P address with length 3 (should be 32)")); + BOOST_REQUIRE(!s.empty()); // The stream is not consumed on invalid input. + s.clear(); + + // Valid CJDNS. + s << MakeSpan(ParseHex("06" // network type (CJDNS) + "10" // address length + "fc000001000200030004000500060007" // address + )); + s >> addr; + BOOST_CHECK(addr.IsValid()); + BOOST_CHECK_EQUAL(addr.ToString(), "fc00:1:2:3:4:5:6:7"); + BOOST_REQUIRE(s.empty()); + + // Invalid CJDNS, with bogus length. + s << MakeSpan(ParseHex("06" // network type (CJDNS) + "01" // address length + "00" // address + )); + BOOST_CHECK_EXCEPTION(s >> addr, std::ios_base::failure, + HasReason("BIP155 CJDNS address with length 1 (should be 16)")); + BOOST_REQUIRE(!s.empty()); // The stream is not consumed on invalid input. + s.clear(); + // Unknown, with extreme length. s << MakeSpan(ParseHex("aa" // network type (unknown) "fe00000002" // address length (CompactSize's MAX_SIZE) diff --git a/src/utilstrencodings.cpp b/src/utilstrencodings.cpp index 7ce40d44a1ab5..f94154349f495 100644 --- a/src/utilstrencodings.cpp +++ b/src/utilstrencodings.cpp @@ -192,22 +192,27 @@ std::string DecodeBase64(const std::string& str) return std::string((const char*)vchRet.data(), vchRet.size()); } -std::string EncodeBase32(Span input) +std::string EncodeBase32(Span input, bool pad) { static const char *pbase32 = "abcdefghijklmnopqrstuvwxyz234567"; std::string str; str.reserve(((input.size() + 4) / 5) * 8); ConvertBits<8, 5, true>([&](int v) { str += pbase32[v]; }, input.begin(), input.end()); - while (str.size() % 8) str += '='; + if (pad) { + while (str.size() % 8) { + str += '='; + } + } return str; } -std::string EncodeBase32(const std::string& str) +std::string EncodeBase32(const std::string& str, bool pad) { - return EncodeBase32(MakeUCharSpan(str)); + return EncodeBase32(MakeUCharSpan(str), pad); } + std::vector DecodeBase32(const char* p, bool* pfInvalid) { static const int decode32_table[256] = diff --git a/src/utilstrencodings.h b/src/utilstrencodings.h index 022cfb5d8356e..d972a8ebb3ddb 100644 --- a/src/utilstrencodings.h +++ b/src/utilstrencodings.h @@ -59,8 +59,20 @@ std::string EncodeBase64(Span input); std::string EncodeBase64(const std::string& str); std::vector DecodeBase32(const char* p, bool* pfInvalid = NULL); std::string DecodeBase32(const std::string& str); -std::string EncodeBase32(Span input); -std::string EncodeBase32(const std::string& str); + +/** + * Base32 encode. + * If `pad` is true, then the output will be padded with '=' so that its length + * is a multiple of 8. + */ +std::string EncodeBase32(Span input, bool pad = true); + +/** + * Base32 encode. + * If `pad` is true, then the output will be padded with '=' so that its length + * is a multiple of 8. + */ +std::string EncodeBase32(const std::string& str, bool pad = true); std::string i64tostr(int64_t n); std::string itostr(int n);