diff --git a/contrib/epee/include/net/net_utils_base.h b/contrib/epee/include/net/net_utils_base.h index e64857627..bed3b4227 100644 --- a/contrib/epee/include/net/net_utils_base.h +++ b/contrib/epee/include/net/net_utils_base.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -226,6 +227,8 @@ namespace net_utils inline bool operator>=(const ipv6_network_address& lhs, const ipv6_network_address& rhs) noexcept { return !lhs.less(rhs); } + boost::optional get_ipv4_mapped_address(const ipv6_network_address& address); + class network_address { struct interface @@ -352,6 +355,8 @@ namespace net_utils END_KV_SERIALIZE_MAP() }; + boost::optional get_ipv4_mapped_address(const network_address& address); + inline bool operator==(const network_address& lhs, const network_address& rhs) { return lhs.equal(rhs); } inline bool operator!=(const network_address& lhs, const network_address& rhs) diff --git a/contrib/epee/src/net_utils_base.cpp b/contrib/epee/src/net_utils_base.cpp index 74b29b1fe..36b55920d 100644 --- a/contrib/epee/src/net_utils_base.cpp +++ b/contrib/epee/src/net_utils_base.cpp @@ -1,22 +1,12 @@ #include "net/net_utils_base.h" +#include #include #include "string_tools.h" #include "net/local_ip.h" -static inline uint32_t make_address_v4_from_v6(const boost::asio::ip::address_v6& a) -{ - const auto &bytes = a.to_bytes(); - uint32_t v4 = 0; - v4 = (v4 << 8) | bytes[12]; - v4 = (v4 << 8) | bytes[13]; - v4 = (v4 << 8) | bytes[14]; - v4 = (v4 << 8) | bytes[15]; - return htonl(v4); -} - static inline uint32_t make_ipv4_cidr_mask(const uint8_t mask) { if (mask == 0) @@ -52,6 +42,17 @@ namespace epee { namespace net_utils bool ipv6_network_address::is_loopback() const { return m_address.is_loopback(); } bool ipv6_network_address::is_local() const { return m_address.is_link_local(); } + boost::optional get_ipv4_mapped_address(const ipv6_network_address& address) + { + const boost::asio::ip::address_v6 ip = address.ip(); + if (!ip.is_v4_mapped()) + return boost::none; + + const boost::asio::ip::address_v4 ipv4 = + boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, ip); + return ipv4_network_address{SWAP32BE(ipv4.to_uint()), address.port()}; + } + bool ipv4_network_subnet::equal(const ipv4_network_subnet& other) const noexcept { return is_same_host(other) && m_mask == other.m_mask; } @@ -109,25 +110,28 @@ namespace epee { namespace net_utils const auto this_id = get_type_id(); if (this_id == ipv4_network_address::get_type_id() && other.get_type_id() == ipv6_network_address::get_type_id()) { - const boost::asio::ip::address_v6 &actual_ip = other.as().ip(); - if (actual_ip.is_v4_mapped()) - { - const uint32_t v4ip = make_address_v4_from_v6(actual_ip); - return is_same_host(ipv4_network_address(v4ip, 0)); - } + const boost::optional mapped = + get_ipv4_mapped_address(other.as()); + if (mapped) + return is_same_host(*mapped); } else if (this_id == ipv6_network_address::get_type_id() && other.get_type_id() == ipv4_network_address::get_type_id()) { - const boost::asio::ip::address_v6 &actual_ip = this->as().ip(); - if (actual_ip.is_v4_mapped()) - { - const uint32_t v4ip = make_address_v4_from_v6(actual_ip); - return other.is_same_host(ipv4_network_address(v4ip, 0)); - } + const boost::optional mapped = + get_ipv4_mapped_address(this->as()); + if (mapped) + return other.is_same_host(*mapped); } return false; } + boost::optional get_ipv4_mapped_address(const network_address& address) + { + if (address.get_type_id() != ipv6_network_address::get_type_id()) + return boost::none; + return get_ipv4_mapped_address(address.as()); + } + std::string print_connection_context(const connection_context_base& ctx) { std::stringstream ss; diff --git a/src/net/parse.cpp b/src/net/parse.cpp index 604511a9f..51527b77f 100644 --- a/src/net/parse.cpp +++ b/src/net/parse.cpp @@ -187,7 +187,12 @@ namespace net if (ipv6) { - return {epee::net_utils::ipv6_network_address{v6, port}}; + const epee::net_utils::ipv6_network_address address{v6, port}; + const boost::optional mapped = + epee::net_utils::get_ipv4_mapped_address(address); + if (mapped) + return {std::move(*mapped)}; + return {address}; } else { diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 01c19577b..b84bcebba 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -142,6 +142,20 @@ namespace nodetool return ipv6_peer_group{{bytes[0], bytes[1], bytes[2], bytes[3]}}; } //----------------------------------------------------------------------------------- + + inline bool is_forbidden_ipv4_mapped_ipv6_address(const epee::net_utils::network_address& address) + { + return bool(epee::net_utils::get_ipv4_mapped_address(address)); + } + + inline bool should_skip_connect_address(const epee::net_utils::network_address& address, bool use_ipv6) + { + if (address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id() && !use_ipv6) + return true; + + return is_forbidden_ipv4_mapped_ipv6_address(address); + } + template node_server::~node_server() { @@ -1474,6 +1488,9 @@ namespace nodetool if (zone.m_connect == nullptr) // outgoing connections in zone not possible return false; + if (should_skip_connect_address(na, m_use_ipv6)) + return false; + if (zone.m_our_address == na) return false; @@ -1653,17 +1670,9 @@ namespace nodetool // Local helper method to get the host string, i.e. the pure IP address without port const auto get_host_string = [](const epee::net_utils::network_address &address) { - if (address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id()) - { - const boost::asio::ip::address_v6 actual_ip = address.as().ip(); - if (actual_ip.is_v4_mapped()) - { - auto v4ip = boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, actual_ip); - uint32_t actual_ipv4; - memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4)); - return epee::net_utils::ipv4_network_address(actual_ipv4, 0).host_str(); - } - } + const boost::optional mapped = epee::net_utils::get_ipv4_mapped_address(address); + if (mapped) + return epee::net_utils::ipv4_network_address(mapped->ip(), 0).host_str(); return address.host_str(); }; @@ -1676,9 +1685,12 @@ namespace nodetool std::vector peers; std::unordered_set hosts; size_t total_peers_size = 0; - zone.m_peerlist.foreach(use_white_list, [&peers, &hosts, &total_peers_size, &get_host_string](const peerlist_entry &peer) + zone.m_peerlist.foreach(use_white_list, [this, &peers, &hosts, &total_peers_size, &get_host_string](const peerlist_entry &peer) { ++total_peers_size; + if (should_skip_connect_address(peer.adr, m_use_ipv6)) + return true; + const std::string host_string = get_host_string(peer.adr); if (hosts.insert(host_string).second) { @@ -1722,14 +1734,9 @@ namespace nodetool else if (cntxt.m_remote_address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id()) { const epee::net_utils::network_address na = cntxt.m_remote_address; - const boost::asio::ip::address_v6 &actual_ip = na.as().ip(); - if (actual_ip.is_v4_mapped()) - { - auto v4ip = boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, actual_ip); - uint32_t actual_ipv4; - memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4)); - connected_subnets.insert(actual_ipv4 & subnet_mask); - } + const boost::optional mapped = epee::net_utils::get_ipv4_mapped_address(na); + if (mapped) + connected_subnets.insert(mapped->ip() & subnet_mask); else { const boost::optional group = get_ipv6_peer_group(na); @@ -1785,13 +1792,10 @@ namespace nodetool else if (peer.adr.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id()) { const epee::net_utils::network_address na = peer.adr; - const boost::asio::ip::address_v6 &actual_ip = na.as().ip(); - if (actual_ip.is_v4_mapped()) + const boost::optional mapped = epee::net_utils::get_ipv4_mapped_address(na); + if (mapped) { - auto v4ip = boost::asio::ip::make_address_v4(boost::asio::ip::v4_mapped, actual_ip); - uint32_t actual_ipv4; - memcpy(&actual_ipv4, v4ip.to_bytes().data(), sizeof(actual_ipv4)); - uint32_t subnet = actual_ipv4 & subnet_mask; + uint32_t subnet = mapped->ip() & subnet_mask; take = subnets.find(subnet) == subnets.end(); if (take) subnets.insert(subnet); @@ -2328,11 +2332,15 @@ namespace nodetool bool ignore = false; peerlist_entry &be = local_peerlist[i]; epee::net_utils::network_address &na = be.adr; - if (na.is_loopback() || na.is_local()) + if (is_forbidden_ipv4_mapped_ipv6_address(na)) { ignore = true; } - else if (be.adr.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id()) + else if (na.is_loopback() || na.is_local()) + { + ignore = true; + } + else if (na.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id()) { const epee::net_utils::ipv4_network_address &ipv4 = na.as(); if (ipv4.ip() == 0) @@ -2381,7 +2389,8 @@ namespace nodetool LOG_TRACE_CC(context, "REMOTE PEERLIST: " << ENDL << print_peerlist_to_string(peerlist_)); CRITICAL_REGION_LOCAL(m_blocked_hosts_lock); return m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.merge_peerlist(peerlist_, [this](const peerlist_entry &pe) { - return !is_addr_recently_failed(pe.adr) && is_remote_host_allowed(pe.adr); + return !is_forbidden_ipv4_mapped_ipv6_address(pe.adr) && + !is_addr_recently_failed(pe.adr) && is_remote_host_allowed(pe.adr); }); } //----------------------------------------------------------------------------------- @@ -2669,6 +2678,9 @@ namespace nodetool std::vector local_peerlist_new; zone.m_peerlist.get_peerlist_head(local_peerlist_new, true, max_peerlist_size); + local_peerlist_new.erase(std::remove_if(local_peerlist_new.begin(), local_peerlist_new.end(), [](const peerlist_entry& peer) { + return is_forbidden_ipv4_mapped_ipv6_address(peer.adr); + }), local_peerlist_new.end()); /* Tor/I2P nodes receiving connections via forwarding (from tor/i2p daemon) do not know the address of the connecting peer. This is relayed to them, @@ -2793,6 +2805,9 @@ namespace nodetool //fill response zone.m_peerlist.get_peerlist_head(rsp.local_peerlist_new, true); + rsp.local_peerlist_new.erase(std::remove_if(rsp.local_peerlist_new.begin(), rsp.local_peerlist_new.end(), [](const peerlist_entry& peer) { + return is_forbidden_ipv4_mapped_ipv6_address(peer.adr); + }), rsp.local_peerlist_new.end()); for (const auto &e: rsp.local_peerlist_new) context.sent_addresses.insert(e.adr); get_local_node_data(rsp.node_data, zone); @@ -3098,6 +3113,16 @@ namespace nodetool if (!zone.second.m_peerlist.get_random_gray_peer(pe)) continue; + if (is_forbidden_ipv4_mapped_ipv6_address(pe.adr)) + { + zone.second.m_peerlist.remove_from_peer_gray(pe); + LOG_PRINT_L2("PEER EVICTED FROM GRAY PEER LIST: address: " << pe.adr.host_str() << " Peer ID: " << peerid_to_string(pe.id)); + continue; + } + + if (should_skip_connect_address(pe.adr, m_use_ipv6)) + continue; + if (!check_connection_and_handshake_with_peer(pe.adr, pe.last_seen)) { zone.second.m_peerlist.remove_from_peer_gray(pe); @@ -3182,6 +3207,9 @@ namespace nodetool } else if (is_ipv6) { + if (epee::net_utils::get_ipv4_mapped_address(na)) + return boost::none; + const epee::net_utils::ipv6_network_address &ipv6 = na.as(); address = ipv6.ip().to_string(); port = epee::string_tools::num_to_string_fast(ipv6.port()); diff --git a/tests/unit_tests/net.cpp b/tests/unit_tests/net.cpp index 70132d378..9c1b593eb 100644 --- a/tests/unit_tests/net.cpp +++ b/tests/unit_tests/net.cpp @@ -1025,6 +1025,16 @@ TEST(get_network_address, ipv4) EXPECT_STREQ("23.0.0.254:2000", address->str().c_str()); } +TEST(get_network_address, ipv4_mapped_ipv6) +{ + expect address = + net::get_network_address("[::ffff:203.0.113.1]:18080", 0); + ASSERT_TRUE(bool(address)); + EXPECT_EQ(epee::net_utils::address_type::ipv4, address->get_type_id()); + EXPECT_STREQ("203.0.113.1", address->host_str().c_str()); + EXPECT_STREQ("203.0.113.1:18080", address->str().c_str()); +} + TEST(get_network_address, ipv4subnet) { expect address = net::get_ipv4_subnet_address("0.0.0.0", true); diff --git a/tests/unit_tests/node_server.cpp b/tests/unit_tests/node_server.cpp index ad997b43f..b03826dac 100644 --- a/tests/unit_tests/node_server.cpp +++ b/tests/unit_tests/node_server.cpp @@ -36,6 +36,7 @@ #include "cryptonote_protocol/cryptonote_protocol_handler.h" #include "cryptonote_protocol/cryptonote_protocol_handler.inl" #include "unit_tests_utils.h" +#include #include #include @@ -167,6 +168,36 @@ static bool is_blocked(Server &server, const epee::net_utils::network_address &a return false; } +TEST(node_server, ipv4_mapped_ipv6_address) +{ + boost::asio::ip::address_v6::bytes_type bytes = {}; + bytes[10] = 0xff; + bytes[11] = 0xff; + bytes[12] = 45; + bytes[13] = 155; + bytes[14] = 69; + bytes[15] = 10; + + const epee::net_utils::network_address mapped{ + epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6{bytes}, 18080} + }; + const boost::optional ipv4 = epee::net_utils::get_ipv4_mapped_address(mapped); + ASSERT_TRUE(bool(ipv4)); + EXPECT_EQ("45.155.69.10", ipv4->host_str()); + EXPECT_EQ(18080, ipv4->port()); + + const epee::net_utils::network_address ipv6{ + epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6::loopback(), 18080} + }; + EXPECT_FALSE(epee::net_utils::get_ipv4_mapped_address(ipv6)); + + EXPECT_TRUE(nodetool::is_forbidden_ipv4_mapped_ipv6_address(mapped)); + EXPECT_FALSE(nodetool::is_forbidden_ipv4_mapped_ipv6_address(ipv6)); + EXPECT_TRUE(nodetool::should_skip_connect_address(mapped, true)); + EXPECT_TRUE(nodetool::should_skip_connect_address(ipv6, false)); + EXPECT_FALSE(nodetool::should_skip_connect_address(ipv6, true)); +} + namespace { using path_t = boost::filesystem::path; @@ -229,6 +260,44 @@ namespace } } +TEST(node_server, peerlist_merge_rejects_ipv4_mapped_ipv6_address) +{ + boost::asio::ip::address_v6::bytes_type bytes = {}; + bytes[10] = 0xff; + bytes[11] = 0xff; + bytes[12] = 45; + bytes[13] = 155; + bytes[14] = 69; + bytes[15] = 10; + + const epee::net_utils::network_address mapped{ + epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6{bytes}, 18080} + }; + const epee::net_utils::network_address ipv4{MAKE_IPV4_ADDRESS_PORT(11, 22, 33, 44, 18080)}; + + std::vector remote_peerlist; + remote_peerlist.push_back(make_peer(mapped, 1, 100)); + remote_peerlist.push_back(make_peer(ipv4, 2, 200)); + + nodetool::peerlist_manager peerlist; + ASSERT_TRUE(peerlist.init(nodetool::peerlist_types{}, false)); + ASSERT_TRUE(peerlist.merge_peerlist(remote_peerlist, [](const nodetool::peerlist_entry& pe) { + return !nodetool::is_forbidden_ipv4_mapped_ipv6_address(pe.adr); + })); + + std::vector gray; + std::vector white; + peerlist.get_peerlist(gray, white); + const auto contains_address = [](const std::vector& peers, const epee::net_utils::network_address& address) { + return std::any_of(peers.begin(), peers.end(), [&address](const nodetool::peerlist_entry& peer) { + return peer.adr == address; + }); + }; + EXPECT_TRUE(contains_address(gray, ipv4)); + EXPECT_FALSE(contains_address(gray, mapped)); + EXPECT_FALSE(contains_address(white, mapped)); +} + TEST(ban, add) { test_core pr_core;