p2p, rpc: group public IPv6 connection limits by /64

(cherry picked from commit f277d328cf1f7e64423b5eb7dc0e21649f71e47a)
This commit is contained in:
selsta
2026-10-04 18:50:22 -04:00
committed by such-gitea
parent 881dcfbd43
commit f26ac02df9
13 changed files with 275 additions and 8 deletions
@@ -36,6 +36,7 @@
#include "net_utils_base.h"
#include "http_auth.h"
#include "http_base.h"
#include "syncobj.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "net.http"
@@ -65,6 +66,9 @@ namespace net_utils
critical_section m_lock;
};
// RPC limits groupable IPv6 clients by /64 to avoid per-address limit bypasses.
std::string get_rpc_connection_limit_key(const net_utils::network_address& address);
/************************************************************************/
/* */
/************************************************************************/
@@ -225,7 +225,7 @@ namespace net_utils
CRITICAL_REGION_LOCAL(m_config.m_lock);
if (m_config.m_connection_count)
--m_config.m_connection_count;
auto elem = m_config.m_connections.find(m_conn_context.m_remote_address.host_str());
auto elem = m_config.m_connections.find(get_rpc_connection_limit_key(m_conn_context.m_remote_address));
if (elem != m_config.m_connections.end())
{
if (elem->second == 1 || elem->second == 0)
@@ -243,7 +243,7 @@ namespace net_utils
bool simple_http_connection_handler<t_connection_context>::after_init_connection()
{
CRITICAL_REGION_LOCAL(m_config.m_lock);
++m_config.m_connections[m_conn_context.m_remote_address.host_str()];
++m_config.m_connections[get_rpc_connection_limit_key(m_conn_context.m_remote_address)];
++m_config.m_connection_count;
m_initialized = true;
return true;
@@ -155,7 +155,7 @@ namespace epee
return true;
const bool is_private = na.is_loopback() || na.is_local();
const auto elem = config.m_connections.find(na.host_str());
const auto elem = config.m_connections.find(net_utils::http::get_rpc_connection_limit_key(na));
if (elem != config.m_connections.end())
{
if (is_private)
@@ -32,6 +32,7 @@
#include <boost/uuid/uuid.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address_v6.hpp>
#include <cstddef>
#include <typeinfo>
#include <type_traits>
#include "byte_slice.h"
@@ -208,6 +209,9 @@ namespace net_utils
END_KV_SERIALIZE_MAP()
};
bool should_group_ipv6_by_prefix(const boost::asio::ip::address_v6& ip);
boost::asio::ip::address_v6 get_ipv6_subnet_address(const boost::asio::ip::address_v6& ip, const std::size_t prefix_bits);
inline bool operator==(const ipv6_network_address& lhs, const ipv6_network_address& rhs) noexcept
{ return lhs.equal(rhs); }
inline bool operator!=(const ipv6_network_address& lhs, const ipv6_network_address& rhs) noexcept
+1 -1
View File
@@ -31,7 +31,7 @@ set(EPEE_INCLUDE_DIR_BASE "${CMAKE_CURRENT_SOURCE_DIR}/../include")
# Add headers to the file list, to be able to search for them and autosave in IDEs.
monero_find_all_headers(EPEE_HEADERS_PUBLIC "${EPEE_INCLUDE_DIR_BASE}")
monero_add_library(epee byte_slice.cpp byte_stream.cpp hex.cpp abstract_http_client.cpp http_auth.cpp mlog.cpp net_helper.cpp net_utils_base.cpp string_tools.cpp parserse_base_utils.cpp
monero_add_library(epee byte_slice.cpp byte_stream.cpp hex.cpp abstract_http_client.cpp http_auth.cpp http_protocol_handler.cpp mlog.cpp net_helper.cpp net_utils_base.cpp string_tools.cpp parserse_base_utils.cpp
wipeable_string.cpp levin_base.cpp memwipe.c connection_basic.cpp network_throttle.cpp network_throttle-detail.cpp mlocker.cpp buffer.cpp net_ssl.cpp
int-util.cpp portable_storage.cpp
misc_language.cpp
@@ -0,0 +1,44 @@
// Copyright (c) 2026, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "net/http_protocol_handler.h"
namespace epee { namespace net_utils { namespace http
{
std::string get_rpc_connection_limit_key(const net_utils::network_address& address)
{
if (address.get_type_id() == net_utils::ipv6_network_address::get_type_id())
{
const boost::asio::ip::address_v6 ip = address.as<const net_utils::ipv6_network_address>().ip();
if (net_utils::should_group_ipv6_by_prefix(ip))
return net_utils::get_ipv6_subnet_address(ip, 64).to_string() + "/64";
}
return address.host_str();
}
}}}
+30
View File
@@ -1,6 +1,8 @@
#include "net/net_utils_base.h"
#include <algorithm>
#include <boost/uuid/uuid_io.hpp>
#include "string_tools.h"
@@ -45,6 +47,34 @@ 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(); }
static bool is_ipv6_unique_local(const boost::asio::ip::address_v6& ip)
{
const boost::asio::ip::address_v6::bytes_type bytes = ip.to_bytes();
return (bytes[0] & 0xfe) == 0xfc;
}
bool should_group_ipv6_by_prefix(const boost::asio::ip::address_v6& ip)
{
return !ip.is_unspecified() && !ip.is_loopback() && !ip.is_multicast() &&
!ip.is_link_local() && !ip.is_site_local() && !is_ipv6_unique_local(ip) &&
!ip.is_v4_mapped();
}
boost::asio::ip::address_v6 get_ipv6_subnet_address(const boost::asio::ip::address_v6& ip, const std::size_t prefix_bits)
{
boost::asio::ip::address_v6::bytes_type bytes = ip.to_bytes();
const std::size_t bits = std::min(prefix_bits, std::size_t{128});
if (bits < 128)
{
const std::size_t full_bytes = bits / 8;
const std::size_t remainder_bits = bits % 8;
if (remainder_bits != 0)
bytes[full_bytes] &= uint8_t(0xffu << (8 - remainder_bits));
std::fill(bytes.begin() + full_bytes + (remainder_bits != 0), bytes.end(), 0);
}
return boost::asio::ip::address_v6(bytes);
}
bool ipv4_network_subnet::equal(const ipv4_network_subnet& other) const noexcept
{ return is_same_host(other) && m_mask == other.m_mask; }
+1 -1
View File
@@ -176,7 +176,7 @@ namespace nodetool
const command_line::arg_descriptor<bool> arg_pad_transactions = {
"pad-transactions", "Pad relayed transactions to help defend against traffic volume analysis", false
};
const command_line::arg_descriptor<uint32_t> arg_max_connections_per_ip = {"max-connections-per-ip", "Maximum number of p2p connections allowed from the same IP address", 1};
const command_line::arg_descriptor<uint32_t> arg_max_connections_per_ip = {"max-connections-per-ip", "Maximum number of inbound p2p connections allowed from the same IPv4 address or shared across a public IPv6 /64 subnet", 1};
boost::optional<std::vector<proxy>> get_proxies(boost::program_options::variables_map const& vm)
{
+17 -1
View File
@@ -81,6 +81,22 @@ static inline boost::asio::ip::address_v4 make_address_v4_from_v6(const boost::a
namespace nodetool
{
inline bool is_same_p2p_connection_limit_host(const epee::net_utils::network_address& left, const epee::net_utils::network_address& right)
{
if (left.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id() &&
right.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id())
{
const boost::asio::ip::address_v6 left_ip = left.as<const epee::net_utils::ipv6_network_address>().ip();
const boost::asio::ip::address_v6 right_ip = right.as<const epee::net_utils::ipv6_network_address>().ip();
if (epee::net_utils::should_group_ipv6_by_prefix(left_ip) &&
epee::net_utils::should_group_ipv6_by_prefix(right_ip))
return epee::net_utils::get_ipv6_subnet_address(left_ip, 64) ==
epee::net_utils::get_ipv6_subnet_address(right_ip, 64);
}
return left.is_same_host(right);
}
//-----------------------------------------------------------------------------------
template<class t_payload_net_handler>
node_server<t_payload_net_handler>::~node_server()
{
@@ -2935,7 +2951,7 @@ namespace nodetool
m_network_zones.at(epee::net_utils::zone::public_).m_net_server.get_config_object().foreach_connection([&](const p2p_connection_context& cntxt)
{
if (cntxt.m_is_income && cntxt.m_remote_address.is_same_host(address)) {
if (cntxt.m_is_income && is_same_p2p_connection_limit_host(cntxt.m_remote_address, address)) {
count++;
// the only call location happens BEFORE foreach_connection list is updated
+1 -1
View File
@@ -3791,7 +3791,7 @@ namespace cryptonote
const command_line::arg_descriptor<std::size_t> core_rpc_server::arg_rpc_max_connections_per_public_ip = {
"rpc-max-connections-per-public-ip"
, "Max RPC connections per public IP permitted"
, "Max RPC connections permitted per public IPv4 address or shared across a public IPv6 /64 subnet"
, DEFAULT_RPC_MAX_CONNECTIONS_PER_PUBLIC_IP
};
+1 -1
View File
@@ -142,7 +142,7 @@ namespace
const command_line::arg_descriptor<std::string> arg_wallet_dir = {"wallet-dir", "Directory for newly created wallets"};
const command_line::arg_descriptor<bool> arg_prompt_for_password = {"prompt-for-password", "Prompts for password when not provided", false};
const command_line::arg_descriptor<bool> arg_no_initial_sync = {"no-initial-sync", "Skips the initial sync before listening for connections", false};
const command_line::arg_descriptor<std::size_t> arg_rpc_max_connections_per_public_ip = {"rpc-max-connections-per-public-ip", "Max RPC connections per public IP permitted", DEFAULT_RPC_MAX_CONNECTIONS_PER_PUBLIC_IP};
const command_line::arg_descriptor<std::size_t> arg_rpc_max_connections_per_public_ip = {"rpc-max-connections-per-public-ip", "Max RPC connections permitted per public IPv4 address or shared across a public IPv6 /64 subnet", DEFAULT_RPC_MAX_CONNECTIONS_PER_PUBLIC_IP};
const command_line::arg_descriptor<std::size_t> arg_rpc_max_connections_per_private_ip = {"rpc-max-connections-per-private-ip", "Max RPC connections per private and localhost IP permitted", DEFAULT_RPC_MAX_CONNECTIONS_PER_PRIVATE_IP};
const command_line::arg_descriptor<std::size_t> arg_rpc_max_connections = {"rpc-max-connections", "Max RPC connections permitted", DEFAULT_RPC_MAX_CONNECTIONS};
const command_line::arg_descriptor<std::size_t> arg_rpc_response_soft_limit = {"rpc-response-soft-limit", "Max response bytes that can be queued, enforced at next response attempt", DEFAULT_RPC_SOFT_LIMIT_SIZE};
+77
View File
@@ -369,6 +369,83 @@ TEST(HTTP_Server_Auth, NotRequired)
EXPECT_FALSE(auth.get_response(http::http_request_info{}));
}
TEST(HTTP, RpcConnectionLimitKeyIPv6By64)
{
const epee::net_utils::network_address ipv6_a{
epee::net_utils::ipv6_network_address{
boost::asio::ip::make_address_v6("2001:db8:abcd:1234:1111:2222:3333:4444"), 18081
}
};
const epee::net_utils::network_address ipv6_b{
epee::net_utils::ipv6_network_address{
boost::asio::ip::make_address_v6("2001:db8:abcd:1234:ffff:eeee:dddd:cccc"), 18081
}
};
const epee::net_utils::network_address ipv6_c{
epee::net_utils::ipv6_network_address{
boost::asio::ip::make_address_v6("2001:db8:abcd:1235::1"), 18081
}
};
EXPECT_EQ("2001:db8:abcd:1234::/64", http::get_rpc_connection_limit_key(ipv6_a));
EXPECT_EQ(http::get_rpc_connection_limit_key(ipv6_a), http::get_rpc_connection_limit_key(ipv6_b));
EXPECT_NE(http::get_rpc_connection_limit_key(ipv6_a), http::get_rpc_connection_limit_key(ipv6_c));
const epee::net_utils::network_address loopback{
epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6::loopback(), 18081}
};
EXPECT_EQ(loopback.host_str(), http::get_rpc_connection_limit_key(loopback));
const epee::net_utils::network_address link_local{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fe80::1"), 18081}
};
EXPECT_EQ(link_local.host_str(), http::get_rpc_connection_limit_key(link_local));
const epee::net_utils::network_address unique_local_fc{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fc00::1"), 18081}
};
EXPECT_EQ(unique_local_fc.host_str(), http::get_rpc_connection_limit_key(unique_local_fc));
const epee::net_utils::network_address unique_local_fd{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fd00::1"), 18081}
};
EXPECT_EQ(unique_local_fd.host_str(), http::get_rpc_connection_limit_key(unique_local_fd));
const epee::net_utils::network_address unspecified{
epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6::any(), 18081}
};
EXPECT_EQ(unspecified.host_str(), http::get_rpc_connection_limit_key(unspecified));
const epee::net_utils::network_address multicast{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("ff00::1"), 18081}
};
EXPECT_EQ(multicast.host_str(), http::get_rpc_connection_limit_key(multicast));
const epee::net_utils::network_address site_local{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fec0::1"), 18081}
};
EXPECT_EQ(site_local.host_str(), http::get_rpc_connection_limit_key(site_local));
uint32_t ip = 0;
ASSERT_TRUE(epee::string_tools::get_ip_int32_from_string(ip, "203.0.113.1"));
const epee::net_utils::network_address ipv4{
epee::net_utils::ipv4_network_address{ip, 18081}
};
EXPECT_EQ(ipv4.host_str(), http::get_rpc_connection_limit_key(ipv4));
boost::asio::ip::address_v6::bytes_type bytes = {};
bytes[10] = 0xff;
bytes[11] = 0xff;
bytes[12] = 203;
bytes[13] = 0;
bytes[14] = 113;
bytes[15] = 1;
const epee::net_utils::network_address mapped{
epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6{bytes}, 18081}
};
EXPECT_EQ(mapped.host_str(), http::get_rpc_connection_limit_key(mapped));
}
TEST(HTTP_Server_Auth, MissingAuth)
{
http::http_server_auth auth{{"foo", "bar"}, rng};
+92
View File
@@ -147,6 +147,98 @@ static bool is_blocked(Server &server, const epee::net_utils::network_address &a
return false;
}
TEST(node_server, p2p_connection_limit_ipv6_by_64)
{
const epee::net_utils::network_address ipv6_a{
epee::net_utils::ipv6_network_address{
boost::asio::ip::make_address_v6("2001:db8:abcd:1234:1111:2222:3333:4444"), 18080
}
};
const epee::net_utils::network_address ipv6_b{
epee::net_utils::ipv6_network_address{
boost::asio::ip::make_address_v6("2001:db8:abcd:1234:ffff:eeee:dddd:cccc"), 18080
}
};
const epee::net_utils::network_address ipv6_c{
epee::net_utils::ipv6_network_address{
boost::asio::ip::make_address_v6("2001:db8:abcd:1235::1"), 18080
}
};
EXPECT_TRUE(nodetool::is_same_p2p_connection_limit_host(ipv6_a, ipv6_b));
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(ipv6_a, ipv6_c));
const epee::net_utils::network_address loopback_a{
epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6::loopback(), 18080}
};
const epee::net_utils::network_address loopback_b{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("::2"), 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(loopback_a, loopback_b));
const epee::net_utils::network_address link_local_a{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fe80::1"), 18080}
};
const epee::net_utils::network_address link_local_b{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fe80::2"), 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(link_local_a, link_local_b));
const epee::net_utils::network_address unique_local_a{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fc00::1"), 18080}
};
const epee::net_utils::network_address unique_local_b{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fc00::2"), 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(unique_local_a, unique_local_b));
const epee::net_utils::network_address unique_local_c{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fd00::1"), 18080}
};
const epee::net_utils::network_address unique_local_d{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fd00::2"), 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(unique_local_c, unique_local_d));
const epee::net_utils::network_address multicast_a{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("ff00::1"), 18080}
};
const epee::net_utils::network_address multicast_b{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("ff00::2"), 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(multicast_a, multicast_b));
const epee::net_utils::network_address site_local_a{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fec0::1"), 18080}
};
const epee::net_utils::network_address site_local_b{
epee::net_utils::ipv6_network_address{boost::asio::ip::make_address_v6("fec0::2"), 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(site_local_a, site_local_b));
const epee::net_utils::network_address ipv4_a{MAKE_IPV4_ADDRESS_PORT(203, 0, 113, 1, 18080)};
const epee::net_utils::network_address ipv4_b{MAKE_IPV4_ADDRESS_PORT(203, 0, 113, 2, 18080)};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(ipv4_a, ipv4_b));
boost::asio::ip::address_v6::bytes_type bytes_a = {};
bytes_a[10] = 0xff;
bytes_a[11] = 0xff;
bytes_a[12] = 203;
bytes_a[13] = 0;
bytes_a[14] = 113;
bytes_a[15] = 1;
const epee::net_utils::network_address mapped_a{
epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6{bytes_a}, 18080}
};
boost::asio::ip::address_v6::bytes_type bytes_b = bytes_a;
bytes_b[15] = 2;
const epee::net_utils::network_address mapped_b{
epee::net_utils::ipv6_network_address{boost::asio::ip::address_v6{bytes_b}, 18080}
};
EXPECT_FALSE(nodetool::is_same_p2p_connection_limit_host(mapped_a, mapped_b));
}
namespace
{
using path_t = boost::filesystem::path;