diff --git a/contrib/epee/include/net/net_utils_base.h b/contrib/epee/include/net/net_utils_base.h index c52791344..f9b1917a9 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 "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; diff --git a/contrib/epee/src/net_utils_base.cpp b/contrib/epee/src/net_utils_base.cpp index a2ca3d3d6..74b29b1fe 100644 --- a/contrib/epee/src/net_utils_base.cpp +++ b/contrib/epee/src/net_utils_base.cpp @@ -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(); } diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 93735c42b..d8f25e182 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -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(first)), mask}; + } + + void subtract_ipv4_subnet( + const epee::net_utils::ipv4_network_subnet &blocked, + const epee::net_utils::ipv4_network_subnet &removed, + std::vector &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 &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 node_server::~node_server() { @@ -390,11 +450,38 @@ namespace nodetool bool node_server::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())) + { + i = m_blocked_hosts.erase(i); + unblocked = true; + } + else + ++i; + } + + std::map blocked_subnets; + std::vector 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; } //----------------------------------------------------------------------------------- diff --git a/tests/unit_tests/net.cpp b/tests/unit_tests/net.cpp index 6caaf3ce6..307147154 100644 --- a/tests/unit_tests/net.cpp +++ b/tests/unit_tests/net.cpp @@ -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 diff --git a/tests/unit_tests/node_server.cpp b/tests/unit_tests/node_server.cpp index be52a341d..d7627192a 100644 --- a/tests/unit_tests/node_server.cpp +++ b/tests/unit_tests/node_server.cpp @@ -315,18 +315,14 @@ TEST(ban, subnet) test_core pr_core; cryptonote::t_cryptonote_protocol_handler 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 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;