p2p: fix IPv4 subnet arithmetic

This commit is contained in:
Ap4sh
2026-06-30 20:40:44 +02:00
parent a8478d21b2
commit d2e6f9bb57
5 changed files with 210 additions and 25 deletions
+10 -5
View File
@@ -32,6 +32,7 @@
#include <boost/uuid/uuid.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v6.hpp>
#include <stdexcept>
#include <typeinfo>
#include <type_traits>
#include "byte_slice.h"
@@ -128,19 +129,23 @@ namespace net_utils
public:
constexpr ipv4_network_subnet() noexcept
: ipv4_network_subnet(0, 0)
: m_ip(0), m_mask(0)
{}
constexpr ipv4_network_subnet(uint32_t ip, uint8_t mask) noexcept
: m_ip(ip), m_mask(mask) {}
ipv4_network_subnet(uint32_t ip, uint8_t mask)
: m_ip(ip), m_mask(mask)
{
CHECK_AND_ASSERT_THROW_MES(mask <= 32, "invalid IPv4 subnet mask");
}
bool equal(const ipv4_network_subnet& other) const noexcept;
bool less(const ipv4_network_subnet& other) const noexcept;
constexpr bool is_same_host(const ipv4_network_subnet& other) const noexcept
bool is_same_host(const ipv4_network_subnet& other) const noexcept
{ return subnet() == other.subnet(); }
bool matches(const ipv4_network_address &address) const;
constexpr uint32_t subnet() const noexcept { return m_ip & ~(0xffffffffull << m_mask); }
uint8_t mask() const noexcept { return m_mask; }
uint32_t subnet() const noexcept;
std::string str() const;
std::string host_str() const;
bool is_loopback() const;
+11 -1
View File
@@ -17,6 +17,13 @@ static inline uint32_t make_address_v4_from_v6(const boost::asio::ip::address_v6
return htonl(v4);
}
static inline uint32_t make_ipv4_cidr_mask(const uint8_t mask)
{
if (mask == 0)
return 0;
return SWAP32BE(0xffffffffu << (32 - mask));
}
namespace epee { namespace net_utils
{
bool ipv4_network_address::equal(const ipv4_network_address& other) const noexcept
@@ -52,6 +59,9 @@ namespace epee { namespace net_utils
bool ipv4_network_subnet::less(const ipv4_network_subnet& other) const noexcept
{ return subnet() < other.subnet() ? true : (other.subnet() < subnet() ? false : (m_mask < other.m_mask)); }
uint32_t ipv4_network_subnet::subnet() const noexcept
{ return m_ip & make_ipv4_cidr_mask(m_mask); }
std::string ipv4_network_subnet::str() const
{ return string_tools::get_ip_string_from_int32(subnet()) + "/" + std::to_string(m_mask); }
@@ -60,7 +70,7 @@ namespace epee { namespace net_utils
bool ipv4_network_subnet::is_local() const { return net_utils::is_ip_local(subnet()); }
bool ipv4_network_subnet::matches(const ipv4_network_address &address) const
{
return (address.ip() & ~(0xffffffffull << m_mask)) == subnet();
return (address.ip() & make_ipv4_cidr_mask(m_mask)) == subnet();
}
+91 -4
View File
@@ -77,6 +77,66 @@ static inline boost::asio::ip::address_v4 make_address_v4_from_v6(const boost::a
namespace nodetool
{
namespace
{
uint64_t ipv4_subnet_size(const uint8_t mask)
{
CHECK_AND_ASSERT_THROW_MES(mask <= 32, "invalid IPv4 subnet mask");
return mask == 0 ? (uint64_t{1} << 32) : (uint64_t{1} << (32 - mask));
}
uint64_t ipv4_subnet_first(const epee::net_utils::ipv4_network_subnet &subnet)
{
return SWAP32BE(subnet.subnet());
}
uint64_t ipv4_subnet_last(const epee::net_utils::ipv4_network_subnet &subnet)
{
return ipv4_subnet_first(subnet) + ipv4_subnet_size(subnet.mask()) - 1;
}
epee::net_utils::ipv4_network_subnet make_ipv4_subnet(const uint64_t first, const uint8_t mask)
{
return {SWAP32BE(static_cast<uint32_t>(first)), mask};
}
void subtract_ipv4_subnet(
const epee::net_utils::ipv4_network_subnet &blocked,
const epee::net_utils::ipv4_network_subnet &removed,
std::vector<epee::net_utils::ipv4_network_subnet> &result)
{
const uint64_t blocked_first = ipv4_subnet_first(blocked);
const uint64_t blocked_last = ipv4_subnet_last(blocked);
const uint64_t removed_first = ipv4_subnet_first(removed);
const uint64_t removed_last = ipv4_subnet_last(removed);
if (removed_last < blocked_first || blocked_last < removed_first)
{
result.push_back(blocked);
return;
}
if (removed_first <= blocked_first && blocked_last <= removed_last)
return;
const uint8_t child_mask = blocked.mask() + 1;
CHECK_AND_ASSERT_THROW_MES(child_mask <= 32, "invalid IPv4 subnet split");
const uint64_t child_size = ipv4_subnet_size(child_mask);
subtract_ipv4_subnet(make_ipv4_subnet(blocked_first, child_mask), removed, result);
subtract_ipv4_subnet(make_ipv4_subnet(blocked_first + child_size, child_mask), removed, result);
}
void emplace_blocked_subnet(
std::map<epee::net_utils::ipv4_network_subnet, time_t> &subnets,
const epee::net_utils::ipv4_network_subnet &subnet,
const time_t limit)
{
auto entry = subnets.find(subnet);
if (entry == subnets.end() || entry->second < limit)
subnets[subnet] = limit;
}
}
template<class t_payload_net_handler>
node_server<t_payload_net_handler>::~node_server()
{
@@ -390,11 +450,38 @@ namespace nodetool
bool node_server<t_payload_net_handler>::unblock_subnet(const epee::net_utils::ipv4_network_subnet &subnet)
{
CRITICAL_REGION_LOCAL(m_blocked_hosts_lock);
auto i = m_blocked_subnets.find(subnet);
if (i == m_blocked_subnets.end())
bool unblocked = false;
for (auto i = m_blocked_hosts.begin(); i != m_blocked_hosts.end(); )
{
auto address = net::get_network_address(i->first, 0);
if (address && address->get_type_id() == epee::net_utils::ipv4_network_address::get_type_id()
&& subnet.matches(address->template as<epee::net_utils::ipv4_network_address>()))
{
i = m_blocked_hosts.erase(i);
unblocked = true;
}
else
++i;
}
std::map<epee::net_utils::ipv4_network_subnet, time_t> blocked_subnets;
std::vector<epee::net_utils::ipv4_network_subnet> remaining;
remaining.reserve(32);
for (const auto &blocked_subnet : m_blocked_subnets)
{
remaining.clear();
subtract_ipv4_subnet(blocked_subnet.first, subnet, remaining);
if (remaining.size() != 1 || remaining.front() != blocked_subnet.first)
unblocked = true;
for (const auto &entry : remaining)
emplace_blocked_subnet(blocked_subnets, entry, blocked_subnet.second);
}
if (!unblocked)
return false;
m_blocked_subnets.erase(i);
MCLOG_CYAN(el::Level::Info, "global", "Subnet " << subnet.host_str() << " unblocked.");
m_blocked_subnets.swap(blocked_subnets);
MCLOG_CYAN(el::Level::Info, "global", "Subnet " << subnet.str() << " unblocked.");
return true;
}
//-----------------------------------------------------------------------------------
+7
View File
@@ -877,6 +877,13 @@ TEST(get_network_address, ipv4subnet)
address = net::get_ipv4_subnet_address("12.34.56.78/16");
EXPECT_STREQ("12.34.0.0/16", address->str().c_str());
address = net::get_ipv4_subnet_address("172.16.1.2/12");
EXPECT_STREQ("172.16.0.0/12", address->str().c_str());
EXPECT_TRUE(address->matches(epee::net_utils::ipv4_network_address{MAKE_IP(172,16,0,0), 0}));
EXPECT_TRUE(address->matches(epee::net_utils::ipv4_network_address{MAKE_IP(172,31,255,255), 0}));
EXPECT_FALSE(address->matches(epee::net_utils::ipv4_network_address{MAKE_IP(172,15,255,255), 0}));
EXPECT_FALSE(address->matches(epee::net_utils::ipv4_network_address{MAKE_IP(172,32,0,0), 0}));
}
namespace
+91 -15
View File
@@ -315,18 +315,14 @@ TEST(ban, subnet)
test_core pr_core;
cryptonote::t_cryptonote_protocol_handler<test_core> cprotocol(pr_core, NULL);
Server server(cprotocol);
{
boost::program_options::options_description opts{};
Server::init_options(opts);
cryptonote::core::init_options(opts);
const auto node_dir = create_temp_dir("ban-subnet-%%%%%%%%%%%%%%%%");
ASSERT_TRUE(!node_dir.empty());
auto auto_remove_node_dir = epee::misc_utils::create_scope_leave_handler([&node_dir](){
remove_tree(node_dir);
});
const char *argv[] = {"ban_subnet_test"};
boost::program_options::variables_map vm;
boost::program_options::store(
boost::program_options::parse_command_line(1, argv, opts), vm
);
server.init(vm);
}
const auto vm = make_regtest_options(node_dir);
ASSERT_TRUE(server.init(vm));
cprotocol.set_p2p_endpoint(&server);
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(1,2,3,4,24), 10));
@@ -345,14 +341,94 @@ TEST(ban, subnet)
ASSERT_TRUE(server.get_blocked_subnets().size() == 1);
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(1,255,3,255), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(1,0,3,255), &seconds));
ASSERT_FALSE(server.unblock_subnet(MAKE_IPV4_SUBNET(1,2,3,8,24)));
ASSERT_TRUE(server.get_blocked_subnets().size() == 1);
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(1,2,3,4,8), 10));
ASSERT_TRUE(server.get_blocked_subnets().size() == 1);
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(1,2,3,8,24)));
ASSERT_TRUE(server.get_blocked_subnets().size() == 16);
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(1,2,3,8), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(1,2,2,255), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(1,2,4,0), &seconds));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(1,255,0,0,8)));
ASSERT_TRUE(server.get_blocked_subnets().size() == 0);
}
TEST(ban, subnet_arithmetic)
{
time_t seconds;
test_core pr_core;
cryptonote::t_cryptonote_protocol_handler<test_core> cprotocol(pr_core, NULL);
Server server(cprotocol);
const auto node_dir = create_temp_dir("ban-subnet-arithmetic-%%%%%%%%%%%%%%%%");
ASSERT_TRUE(!node_dir.empty());
auto auto_remove_node_dir = epee::misc_utils::create_scope_leave_handler([&node_dir](){
remove_tree(node_dir);
});
const auto vm = make_regtest_options(node_dir);
ASSERT_TRUE(server.init(vm));
cprotocol.set_p2p_endpoint(&server);
ASSERT_NO_THROW((void)(MAKE_IPV4_SUBNET(0,0,0,0,0)));
ASSERT_NO_THROW((void)(MAKE_IPV4_SUBNET(203,0,113,9,32)));
ASSERT_THROW((void)(MAKE_IPV4_SUBNET(203,0,113,9,33)), std::runtime_error);
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(172,16,0,0,16), 10));
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(172,16,1,0,24), 10));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(172,16,0,0,16)));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,1,1), &seconds));
ASSERT_TRUE(server.get_blocked_subnets().empty());
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(172,16,0,0,12), 10));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,1,1), &seconds));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(172,16,1,0,24)));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,1,1), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,1,255), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,0,255), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,2,0), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,31,255,255), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,15,255,255), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,32,0,0), &seconds));
ASSERT_TRUE(server.get_blocked_subnets().size() == 12);
ASSERT_TRUE(server.block_host(MAKE_IPV4_ADDRESS(172,16,2,42), 10));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(172,16,2,0,24)));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,2,42), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,2,255), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,3,0), &seconds));
ASSERT_TRUE(server.get_blocked_subnets().size() == 12);
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(172,16,3,7,32)));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,3,6), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,3,7), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(172,16,3,8), &seconds));
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(203,0,113,9,32), 10));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,9), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,8), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,10), &seconds));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(203,0,113,9,32)));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,9), &seconds));
ASSERT_TRUE(server.block_host(MAKE_IPV4_ADDRESS(10,1,2,3), 10));
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(192,0,2,0,24), 10));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(0,0,0,0,0)));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(10,1,2,3), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(192,0,2,100), &seconds));
ASSERT_TRUE(server.get_blocked_hosts().empty());
ASSERT_TRUE(server.get_blocked_subnets().empty());
ASSERT_TRUE(server.block_subnet(MAKE_IPV4_SUBNET(0,0,0,0,0), 10));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(0,0,0,0), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(255,255,255,255), &seconds));
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(203,0,113,9,32)));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,8), &seconds));
ASSERT_FALSE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,9), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(203,0,113,10), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(0,0,0,0), &seconds));
ASSERT_TRUE(server.is_host_blocked(MAKE_IPV4_ADDRESS(255,255,255,255), &seconds));
ASSERT_TRUE(server.get_blocked_subnets().size() == 32);
ASSERT_TRUE(server.unblock_subnet(MAKE_IPV4_SUBNET(0,0,0,0,0)));
ASSERT_TRUE(server.get_blocked_subnets().empty());
}
TEST(ban, ignores_port)
{
test_core pr_core;