p2p: reject IPv4-mapped IPv6 peers

This commit is contained in:
selsta
2026-07-31 00:45:08 +02:00
parent 0156263696
commit 9268681a6f
6 changed files with 174 additions and 53 deletions
@@ -32,6 +32,7 @@
#include <boost/uuid/uuid.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v6.hpp>
#include <boost/optional/optional.hpp>
#include <stdexcept>
#include <typeinfo>
#include <type_traits>
@@ -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<ipv4_network_address> 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<ipv4_network_address> 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)
+27 -23
View File
@@ -1,22 +1,12 @@
#include "net/net_utils_base.h"
#include <boost/asio/ip/address_v4.hpp>
#include <boost/uuid/uuid_io.hpp>
#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<ipv4_network_address> 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<const epee::net_utils::ipv6_network_address>().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<ipv4_network_address> mapped =
get_ipv4_mapped_address(other.as<const ipv6_network_address>());
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<const epee::net_utils::ipv6_network_address>().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<ipv4_network_address> mapped =
get_ipv4_mapped_address(this->as<const ipv6_network_address>());
if (mapped)
return other.is_same_host(*mapped);
}
return false;
}
boost::optional<ipv4_network_address> 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<const ipv6_network_address>());
}
std::string print_connection_context(const connection_context_base& ctx)
{
std::stringstream ss;
+6 -1
View File
@@ -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<epee::net_utils::ipv4_network_address> mapped =
epee::net_utils::get_ipv4_mapped_address(address);
if (mapped)
return {std::move(*mapped)};
return {address};
}
else
{
+57 -29
View File
@@ -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<class t_payload_net_handler>
node_server<t_payload_net_handler>::~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<const epee::net_utils::ipv6_network_address>().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<epee::net_utils::ipv4_network_address> 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<peerlist_entry> peers;
std::unordered_set<std::string> 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<const epee::net_utils::ipv6_network_address>().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<epee::net_utils::ipv4_network_address> mapped = epee::net_utils::get_ipv4_mapped_address(na);
if (mapped)
connected_subnets.insert(mapped->ip() & subnet_mask);
else
{
const boost::optional<ipv6_peer_group> 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<const epee::net_utils::ipv6_network_address>().ip();
if (actual_ip.is_v4_mapped())
const boost::optional<epee::net_utils::ipv4_network_address> 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<const epee::net_utils::ipv4_network_address>();
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<peerlist_entry> 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<const epee::net_utils::ipv6_network_address>();
address = ipv6.ip().to_string();
port = epee::string_tools::num_to_string_fast(ipv6.port());
+10
View File
@@ -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<epee::net_utils::network_address> 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<epee::net_utils::ipv4_network_subnet> address = net::get_ipv4_subnet_address("0.0.0.0", true);
+69
View File
@@ -36,6 +36,7 @@
#include "cryptonote_protocol/cryptonote_protocol_handler.h"
#include "cryptonote_protocol/cryptonote_protocol_handler.inl"
#include "unit_tests_utils.h"
#include <algorithm>
#include <condition_variable>
#include <thread>
@@ -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<epee::net_utils::ipv4_network_address> 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<nodetool::peerlist_entry> 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<nodetool::peerlist_entry> gray;
std::vector<nodetool::peerlist_entry> white;
peerlist.get_peerlist(gray, white);
const auto contains_address = [](const std::vector<nodetool::peerlist_entry>& 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;