diff --git a/contrib/epee/include/net/abstract_tcp_server2.h b/contrib/epee/include/net/abstract_tcp_server2.h index be9999203c6..952a6a3cf53 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.h +++ b/contrib/epee/include/net/abstract_tcp_server2.h @@ -64,6 +64,7 @@ #define MONERO_DEFAULT_LOG_CATEGORY "net" #define ABSTRACT_SERVER_SEND_QUE_MAX_COUNT 1000 +#define ABSTRACT_SERVER_SEND_QUE_MAX_BYTES_DEFAULT 100 * 1024 * 1024 namespace epee { @@ -169,6 +170,7 @@ namespace net_utils } read; struct { std::deque queue; + std::size_t total_bytes; bool wait_consume; } write; }; @@ -267,11 +269,17 @@ namespace net_utils struct shared_state : connection_basic_shared_state, t_protocol_handler::config_type { shared_state() - : connection_basic_shared_state(), t_protocol_handler::config_type(), pfilter(nullptr), plimit(nullptr), stop_signal_sent(false) + : connection_basic_shared_state(), + t_protocol_handler::config_type(), + pfilter(nullptr), + plimit(nullptr), + response_soft_limit(ABSTRACT_SERVER_SEND_QUE_MAX_BYTES_DEFAULT), + stop_signal_sent(false) {} i_connection_filter* pfilter; i_connection_limit* plimit; + std::size_t response_soft_limit; bool stop_signal_sent; }; @@ -378,6 +386,7 @@ namespace net_utils void set_connection_filter(i_connection_filter* pfilter); void set_connection_limit(i_connection_limit* plimit); + void set_response_soft_limit(std::size_t limit); void set_default_remote(epee::net_utils::network_address remote) { diff --git a/contrib/epee/include/net/abstract_tcp_server2.inl b/contrib/epee/include/net/abstract_tcp_server2.inl index 8a3a8299c47..c3c25825a07 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.inl +++ b/contrib/epee/include/net/abstract_tcp_server2.inl @@ -498,10 +498,12 @@ namespace net_utils if (m_state.socket.cancel_write) { m_state.socket.cancel_write = false; m_state.data.write.queue.clear(); + m_state.data.write.total_bytes = 0; state_status_check(); } else if (ec.value()) { m_state.data.write.queue.clear(); + m_state.data.write.total_bytes = 0; interrupt(); } else { @@ -526,8 +528,11 @@ namespace net_utils start_timer(get_default_timeout(), true); } - assert(bytes_transferred == m_state.data.write.queue.back().size()); + const std::size_t byte_count = m_state.data.write.queue.back().size(); + assert(bytes_transferred == byte_count); m_state.data.write.queue.pop_back(); + m_state.data.write.total_bytes -= + std::min(m_state.data.write.total_bytes, byte_count); m_state.condition.notify_all(); start_write(); } @@ -671,8 +676,9 @@ namespace net_utils return; if (m_state.timers.throttle.out.wait_expire) return; - if (m_state.socket.wait_write) - return; + // \NOTE See on_terminating() comments + //if (m_state.socket.wait_write) + // return; if (m_state.socket.wait_shutdown) return; if (m_state.protocol.wait_init) @@ -730,8 +736,13 @@ namespace net_utils return; if (m_state.timers.throttle.out.wait_expire) return; - if (m_state.socket.wait_write) - return; + // Writes cannot be canceled due to `async_write` being a "composed" + // handler. ASIO has new cancellation routines, not available in 1.66, to + // handle this situation. The problem is that if cancel is called after an + // intermediate handler is queued, the op will not check the cancel flag in + // our code, and will instead queue up another write. + //if (m_state.socket.wait_write) + // return; if (m_state.socket.wait_shutdown) return; if (m_state.protocol.wait_init) @@ -758,6 +769,8 @@ namespace net_utils std::lock_guard guard(m_state.lock); if (m_state.status != status_t::RUNNING || m_state.socket.wait_handshake) return false; + if (std::numeric_limits::max() - m_state.data.write.total_bytes < message.size()) + return false; // Wait for the write queue to fall below the max. If it doesn't after a // randomized delay, drop the connection. @@ -775,7 +788,14 @@ namespace net_utils std::uniform_int_distribution<>(5000, 6000)(rng) ); }; - if (m_state.data.write.queue.size() <= ABSTRACT_SERVER_SEND_QUE_MAX_COUNT) + + // The bytes check intentionally does not include incoming message size. + // This allows for a soft overflow; a single http response will never fail + // this check, but multiple responses could. Clients can avoid this case + // by reading the entire response before making another request. P2P + // should never hit the MAX_BYTES check (when using default values). + if (m_state.data.write.queue.size() <= ABSTRACT_SERVER_SEND_QUE_MAX_COUNT && + m_state.data.write.total_bytes <= static_cast(connection_basic::get_state()).response_soft_limit) return true; m_state.data.write.wait_consume = true; bool success = m_state.condition.wait_for( @@ -784,14 +804,23 @@ namespace net_utils [this]{ return ( m_state.status != status_t::RUNNING || - m_state.data.write.queue.size() <= - ABSTRACT_SERVER_SEND_QUE_MAX_COUNT + ( + m_state.data.write.queue.size() <= + ABSTRACT_SERVER_SEND_QUE_MAX_COUNT && + m_state.data.write.total_bytes <= + static_cast(connection_basic::get_state()).response_soft_limit + ) ); } ); m_state.data.write.wait_consume = false; if (!success) { - terminate(); + // synchronize with intermediate writes on `m_strand` + auto self = connection::shared_from_this(); + boost::asio::post(m_strand, [this, self] { + std::lock_guard guard(m_state.lock); + terminate(); + }); return false; } else @@ -817,7 +846,9 @@ namespace net_utils ) { if (!wait_consume()) return false; + const std::size_t byte_count = message.size(); m_state.data.write.queue.emplace_front(std::move(message)); + m_state.data.write.total_bytes += byte_count; start_write(); } else { @@ -827,6 +858,7 @@ namespace net_utils m_state.data.write.queue.emplace_front( message.take_slice(CHUNK_SIZE) ); + m_state.data.write.total_bytes += m_state.data.write.queue.front().size(); start_write(); } } @@ -1363,6 +1395,13 @@ namespace net_utils } //--------------------------------------------------------------------------------- template + void boosted_tcp_server::set_response_soft_limit(const std::size_t limit) + { + assert(m_state != nullptr); // always set in constructor + m_state->response_soft_limit = limit; + } + //--------------------------------------------------------------------------------- + template bool boosted_tcp_server::run_server(size_t threads_count, bool wait, const boost::thread::attributes& attrs) { TRY_ENTRY(); diff --git a/contrib/epee/include/net/http_protocol_handler.h b/contrib/epee/include/net/http_protocol_handler.h index 258b07e2c5a..8b73964dd2b 100644 --- a/contrib/epee/include/net/http_protocol_handler.h +++ b/contrib/epee/include/net/http_protocol_handler.h @@ -32,6 +32,7 @@ #include #include +#include #include "net_utils_base.h" #include "http_auth.h" #include "http_base.h" @@ -54,8 +55,13 @@ namespace net_utils { std::string m_folder; std::vector m_access_control_origins; + std::unordered_map m_connections; boost::optional m_user; size_t m_max_content_length{std::numeric_limits::max()}; + std::size_t m_connection_count{0}; + std::size_t m_max_public_ip_connections{3}; + std::size_t m_max_private_ip_connections{25}; + std::size_t m_max_connections{100}; critical_section m_lock; }; @@ -70,7 +76,7 @@ namespace net_utils typedef http_server_config config_type; simple_http_connection_handler(i_service_endpoint* psnd_hndlr, config_type& config, t_connection_context& conn_context); - virtual ~simple_http_connection_handler(){} + virtual ~simple_http_connection_handler(); bool release_protocol() { @@ -86,10 +92,7 @@ namespace net_utils { return true; } - bool after_init_connection() - { - return true; - } + bool after_init_connection(); virtual bool handle_recv(const void* ptr, size_t cb); virtual bool handle_request(const http::http_request_info& query_info, http_response_info& response); @@ -146,6 +149,7 @@ namespace net_utils protected: i_service_endpoint* m_psnd_hndlr; t_connection_context& m_conn_context; + bool m_initialized; }; template @@ -212,10 +216,6 @@ namespace net_utils } void handle_qued_callback() {} - bool after_init_connection() - { - return true; - } private: //simple_http_connection_handler::config_type m_stub_config; diff --git a/contrib/epee/include/net/http_protocol_handler.inl b/contrib/epee/include/net/http_protocol_handler.inl index f7d2074b2e7..6647d1f15c5 100644 --- a/contrib/epee/include/net/http_protocol_handler.inl +++ b/contrib/epee/include/net/http_protocol_handler.inl @@ -208,11 +208,46 @@ namespace net_utils m_newlines(0), m_bytes_read(0), m_psnd_hndlr(psnd_hndlr), - m_conn_context(conn_context) + m_conn_context(conn_context), + m_initialized(false) { } //-------------------------------------------------------------------------------------------- + template + simple_http_connection_handler::~simple_http_connection_handler() + { + try + { + if (m_initialized) + { + 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()); + if (elem != m_config.m_connections.end()) + { + if (elem->second == 1 || elem->second == 0) + m_config.m_connections.erase(elem); + else + --(elem->second); + } + } + } + catch (...) + {} + } + //-------------------------------------------------------------------------------------------- + template + bool simple_http_connection_handler::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_connection_count; + m_initialized = true; + return true; + } + //-------------------------------------------------------------------------------------------- template bool simple_http_connection_handler::set_ready_state() { diff --git a/contrib/epee/include/net/http_server_handlers_map2.h b/contrib/epee/include/net/http_server_handlers_map2.h index 8d68f041b0c..8e7db53c040 100644 --- a/contrib/epee/include/net/http_server_handlers_map2.h +++ b/contrib/epee/include/net/http_server_handlers_map2.h @@ -71,7 +71,7 @@ else if((query_info.m_URI == s_pattern) && (cond)) \ { \ handled = true; \ - uint64_t ticks = misc_utils::get_tick_count(); \ + uint64_t ticks = epee::misc_utils::get_tick_count(); \ boost::value_initialized req; \ bool parse_res = epee::serialization::load_t_from_json(static_cast(req), query_info.m_body); \ if (!parse_res) \ @@ -107,7 +107,7 @@ else if(query_info.m_URI == s_pattern) \ { \ handled = true; \ - uint64_t ticks = misc_utils::get_tick_count(); \ + uint64_t ticks = epee::misc_utils::get_tick_count(); \ boost::value_initialized req; \ bool parse_res = epee::serialization::load_t_from_binary(static_cast(req), epee::strspan(query_info.m_body)); \ if (!parse_res) \ @@ -117,7 +117,7 @@ response_info.m_response_comment = "Bad request"; \ return true; \ } \ - uint64_t ticks1 = misc_utils::get_tick_count(); \ + uint64_t ticks1 = epee::misc_utils::get_tick_count(); \ boost::value_initialized resp;\ MINFO(m_conn_context << "calling " << s_pattern); \ bool res = false; \ @@ -129,7 +129,7 @@ response_info.m_response_comment = "Internal Server Error"; \ return true; \ } \ - uint64_t ticks2 = misc_utils::get_tick_count(); \ + uint64_t ticks2 = epee::misc_utils::get_tick_count(); \ epee::byte_slice buffer; \ epee::serialization::store_t_to_binary(static_cast(resp), buffer, 64 * 1024); \ uint64_t ticks3 = epee::misc_utils::get_tick_count(); \ diff --git a/contrib/epee/include/net/http_server_impl_base.h b/contrib/epee/include/net/http_server_impl_base.h index d88b53c9427..7c7b8add2a7 100644 --- a/contrib/epee/include/net/http_server_impl_base.h +++ b/contrib/epee/include/net/http_server_impl_base.h @@ -33,6 +33,7 @@ #include #include +#include "cryptonote_config.h" #include "net/abstract_tcp_server2.h" #include "http_protocol_handler.h" #include "net/http_server_handlers_map2.h" @@ -44,7 +45,8 @@ namespace epee { template - class http_server_impl_base: public net_utils::http::i_http_server_handler + class http_server_impl_base: public net_utils::http::i_http_server_handler, + net_utils::i_connection_limit { public: @@ -60,8 +62,16 @@ namespace epee const std::string& bind_ipv6_address = "::", bool use_ipv6 = false, bool require_ipv4 = true, std::vector access_control_origins = std::vector(), boost::optional user = boost::none, - net_utils::ssl_options_t ssl_options = net_utils::ssl_support_t::e_ssl_support_autodetect) + net_utils::ssl_options_t ssl_options = net_utils::ssl_support_t::e_ssl_support_autodetect, + const std::size_t max_public_ip_connections = DEFAULT_RPC_MAX_CONNECTIONS_PER_PUBLIC_IP, + const std::size_t max_private_ip_connections = DEFAULT_RPC_MAX_CONNECTIONS_PER_PRIVATE_IP, + const std::size_t max_connections = DEFAULT_RPC_MAX_CONNECTIONS, + const std::size_t response_soft_limit = DEFAULT_RPC_SOFT_LIMIT_SIZE) { + if (max_connections < max_public_ip_connections) + throw std::invalid_argument{"Max public IP connections cannot be more than max connections"}; + if (max_connections < max_private_ip_connections) + throw std::invalid_argument{"Max private IP connections cannot be more than max connections"}; //set self as callback handler m_net_server.get_config_object().m_phandler = static_cast(this); @@ -75,6 +85,11 @@ namespace epee m_net_server.get_config_object().m_access_control_origins = std::move(access_control_origins); m_net_server.get_config_object().m_user = std::move(user); + m_net_server.get_config_object().m_max_public_ip_connections = max_public_ip_connections; + m_net_server.get_config_object().m_max_private_ip_connections = max_private_ip_connections; + m_net_server.get_config_object().m_max_connections = max_connections; + m_net_server.set_response_soft_limit(response_soft_limit); + m_net_server.set_connection_limit(this); MGINFO("Binding on " << bind_ip << " (IPv4):" << bind_port); if (use_ipv6) @@ -131,6 +146,26 @@ namespace epee } protected: + + virtual bool is_host_limit(const net_utils::network_address& na) override final + { + auto& config = m_net_server.get_config_object(); + CRITICAL_REGION_LOCAL(config.m_lock); + if (config.m_max_connections <= config.m_connection_count) + return true; + + const bool is_private = na.is_loopback() || na.is_local(); + const auto elem = config.m_connections.find(na.host_str()); + if (elem != config.m_connections.end()) + { + if (is_private) + return config.m_max_private_ip_connections <= elem->second; + else + return config.m_max_public_ip_connections <= elem->second; + } + return false; + } + net_utils::boosted_tcp_server > m_net_server; }; } diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index d69556acd9a..82891b9de05 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -127,6 +127,10 @@ #define COMMAND_RPC_GET_BLOCKS_FAST_MAX_BLOCK_COUNT 1000 #define COMMAND_RPC_GET_BLOCKS_FAST_MAX_TX_COUNT 20000 +#define DEFAULT_RPC_MAX_CONNECTIONS_PER_PUBLIC_IP 3 +#define DEFAULT_RPC_MAX_CONNECTIONS_PER_PRIVATE_IP 25 +#define DEFAULT_RPC_MAX_CONNECTIONS 100 +#define DEFAULT_RPC_SOFT_LIMIT_SIZE 25 * 1024 * 1024 // 25 MiB #define MAX_RPC_CONTENT_LENGTH 1048576 // 1 MB #define P2P_LOCAL_WHITE_PEERLIST_LIMIT 1000 diff --git a/src/daemon/main.cpp b/src/daemon/main.cpp index 1d4baaa328c..38b99ff49c0 100644 --- a/src/daemon/main.cpp +++ b/src/daemon/main.cpp @@ -83,7 +83,7 @@ uint16_t parse_public_rpc_port(const po::variables_map &vm) } uint16_t rpc_port; - if (!string_tools::get_xtype_from_string(rpc_port, rpc_port_str)) + if (!epee::string_tools::get_xtype_from_string(rpc_port, rpc_port_str)) { throw std::runtime_error("invalid RPC port " + rpc_port_str); } diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index b6364ff7702..0d3688c7655 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -1063,7 +1063,7 @@ bool t_rpc_command_executor::print_transaction(crypto::hash transaction_hash, cryptonote::blobdata blob; std::string source = as_hex.empty() ? pruned_as_hex + prunable_as_hex : as_hex; bool pruned = !pruned_as_hex.empty() && prunable_as_hex.empty(); - if (!string_tools::parse_hexstr_to_binbuff(source, blob)) + if (!epee::string_tools::parse_hexstr_to_binbuff(source, blob)) { tools::fail_msg_writer() << "Failed to parse tx to get json format"; } diff --git a/src/p2p/net_node.cpp b/src/p2p/net_node.cpp index f9803fd813f..2085b38ee06 100644 --- a/src/p2p/net_node.cpp +++ b/src/p2p/net_node.cpp @@ -169,7 +169,7 @@ namespace nodetool const command_line::arg_descriptor arg_pad_transactions = { "pad-transactions", "Pad relayed transactions to help defend against traffic volume analysis", false }; - const command_line::arg_descriptor arg_max_connections_per_ip = {"max-connections-per-ip", "Maximum number of connections allowed from the same IP address", 1}; + const command_line::arg_descriptor arg_max_connections_per_ip = {"max-connections-per-ip", "Maximum number of p2p connections allowed from the same IP address", 1}; boost::optional> get_proxies(boost::program_options::variables_map const& vm) { diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 1b0e3f26113..95f3ddd4009 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -163,6 +163,10 @@ namespace cryptonote command_line::add_arg(desc, arg_rpc_payment_difficulty); command_line::add_arg(desc, arg_rpc_payment_credits); command_line::add_arg(desc, arg_rpc_payment_allow_free_loopback); + command_line::add_arg(desc, arg_rpc_max_connections_per_public_ip); + command_line::add_arg(desc, arg_rpc_max_connections_per_private_ip); + command_line::add_arg(desc, arg_rpc_max_connections); + command_line::add_arg(desc, arg_rpc_response_soft_limit); } //------------------------------------------------------------------------------------------------------------------------------ core_rpc_server::core_rpc_server( @@ -369,11 +373,28 @@ namespace cryptonote } } + const auto max_connections_public = command_line::get_arg(vm, arg_rpc_max_connections_per_public_ip); + const auto max_connections_private = command_line::get_arg(vm, arg_rpc_max_connections_per_private_ip); + const auto max_connections = command_line::get_arg(vm, arg_rpc_max_connections); + + if (max_connections < max_connections_public) + { + MFATAL(arg_rpc_max_connections_per_public_ip.name << " is bigger than " << arg_rpc_max_connections.name); + return false; + } + if (max_connections < max_connections_private) + { + MFATAL(arg_rpc_max_connections_per_private_ip.name << " is bigger than " << arg_rpc_max_connections.name); + return false; + } + auto rng = [](size_t len, uint8_t *ptr){ return crypto::rand(len, ptr); }; const bool inited = epee::http_server_impl_base::init( rng, std::move(port), std::move(bind_ip_str), std::move(bind_ipv6_str), std::move(rpc_config->use_ipv6), std::move(rpc_config->require_ipv4), - std::move(rpc_config->access_control_origins), std::move(http_login), std::move(rpc_config->ssl_options) + std::move(rpc_config->access_control_origins), std::move(http_login), std::move(rpc_config->ssl_options), + max_connections_public, max_connections_private, max_connections, + command_line::get_arg(vm, arg_rpc_response_soft_limit) ); m_net_server.get_config_object().m_max_content_length = MAX_RPC_CONTENT_LENGTH; @@ -3748,4 +3769,28 @@ namespace cryptonote , "Allow free access from the loopback address (ie, the local host)" , false }; + + const command_line::arg_descriptor core_rpc_server::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 core_rpc_server::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 core_rpc_server::arg_rpc_max_connections = { + "rpc-max-connections" + , "Max RPC connections permitted" + , DEFAULT_RPC_MAX_CONNECTIONS + }; + + const command_line::arg_descriptor core_rpc_server::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 + }; } // namespace cryptonote diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index 0274f4db84b..90c05f41ac3 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -47,10 +47,6 @@ #undef MONERO_DEFAULT_LOG_CATEGORY #define MONERO_DEFAULT_LOG_CATEGORY "daemon.rpc" -// yes, epee doesn't properly use its full namespace when calling its -// functions from macros. *sigh* -using namespace epee; - namespace cryptonote { /************************************************************************/ @@ -60,7 +56,6 @@ namespace cryptonote { public: - static const command_line::arg_descriptor arg_public_node; static const command_line::arg_descriptor arg_rpc_bind_port; static const command_line::arg_descriptor arg_rpc_restricted_bind_port; static const command_line::arg_descriptor arg_restricted_rpc; @@ -77,6 +72,10 @@ namespace cryptonote static const command_line::arg_descriptor arg_rpc_payment_difficulty; static const command_line::arg_descriptor arg_rpc_payment_credits; static const command_line::arg_descriptor arg_rpc_payment_allow_free_loopback; + static const command_line::arg_descriptor arg_rpc_max_connections_per_public_ip; + static const command_line::arg_descriptor arg_rpc_max_connections_per_private_ip; + static const command_line::arg_descriptor arg_rpc_max_connections; + static const command_line::arg_descriptor arg_rpc_response_soft_limit; typedef epee::net_utils::connection_context_base connection_context; diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 14c66c5f547..b84b7b28439 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -129,6 +129,10 @@ namespace const command_line::arg_descriptor arg_wallet_dir = {"wallet-dir", "Directory for newly created wallets"}; const command_line::arg_descriptor arg_prompt_for_password = {"prompt-for-password", "Prompts for password when not provided", false}; const command_line::arg_descriptor arg_no_initial_sync = {"no-initial-sync", "Skips the initial sync before listening for connections", false}; + const command_line::arg_descriptor 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 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 arg_rpc_max_connections = {"rpc-max-connections", "Max RPC connections permitted", DEFAULT_RPC_MAX_CONNECTIONS}; + const command_line::arg_descriptor 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}; constexpr const char default_rpc_username[] = "monero"; @@ -325,13 +329,30 @@ namespace tools check_background_mining(); + const auto max_connections_public = command_line::get_arg(vm, arg_rpc_max_connections_per_public_ip); + const auto max_connections_private = command_line::get_arg(vm, arg_rpc_max_connections_per_private_ip); + const auto max_connections = command_line::get_arg(vm, arg_rpc_max_connections); + + if (max_connections < max_connections_public) + { + MFATAL(arg_rpc_max_connections_per_public_ip.name << " is bigger than " << arg_rpc_max_connections.name); + return false; + } + if (max_connections < max_connections_private) + { + MFATAL(arg_rpc_max_connections_per_private_ip.name << " is bigger than " << arg_rpc_max_connections.name); + return false; + } + m_net_server.set_threads_prefix("RPC"); auto rng = [](size_t len, uint8_t *ptr) { return crypto::rand(len, ptr); }; return epee::http_server_impl_base::init( rng, std::move(bind_port), std::move(rpc_config->bind_ip), std::move(rpc_config->bind_ipv6_address), std::move(rpc_config->use_ipv6), std::move(rpc_config->require_ipv4), std::move(rpc_config->access_control_origins), std::move(http_login), - std::move(rpc_config->ssl_options) + std::move(rpc_config->ssl_options), + max_connections_public, max_connections_private, max_connections, + command_line::get_arg(vm, arg_rpc_response_soft_limit) ); } //------------------------------------------------------------------------------------------------------------------------------ @@ -4940,6 +4961,10 @@ int main(int argc, char** argv) { command_line::add_arg(desc_params, arg_prompt_for_password); command_line::add_arg(desc_params, arg_rpc_client_secret_key); command_line::add_arg(desc_params, arg_no_initial_sync); + command_line::add_arg(desc_params, arg_rpc_max_connections_per_public_ip); + command_line::add_arg(desc_params, arg_rpc_max_connections_per_private_ip); + command_line::add_arg(desc_params, arg_rpc_max_connections); + command_line::add_arg(desc_params, arg_rpc_response_soft_limit); daemonizer::init_options(hidden_options, desc_params); desc_params.add(hidden_options); diff --git a/tests/functional_tests/functional_tests_rpc.py b/tests/functional_tests/functional_tests_rpc.py index f0203bad11a..d8ec78810f0 100755 --- a/tests/functional_tests/functional_tests_rpc.py +++ b/tests/functional_tests/functional_tests_rpc.py @@ -48,7 +48,7 @@ FUNCTIONAL_TESTS_DIRECTORY = builddir + "/tests/functional_tests" DIFFICULTY = 10 -monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", str(DIFFICULTY), "--no-igd", "--p2p-bind-port", "monerod_p2p_port", "--rpc-bind-port", "monerod_rpc_port", "--zmq-rpc-bind-port", "monerod_zmq_port", "--zmq-pub", "monerod_zmq_pub", "--non-interactive", "--disable-dns-checkpoints", "--check-updates", "disabled", "--rpc-ssl", "disabled", "--data-dir", "monerod_data_dir", "--log-level", "1"] +monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", str(DIFFICULTY), "--no-igd", "--p2p-bind-port", "monerod_p2p_port", "--rpc-bind-port", "monerod_rpc_port", "--zmq-rpc-bind-port", "monerod_zmq_port", "--zmq-pub", "monerod_zmq_pub", "--non-interactive", "--disable-dns-checkpoints", "--check-updates", "disabled", "--rpc-ssl", "disabled", "--data-dir", "monerod_data_dir", "--log-level", "1", "--rpc-max-connections-per-private-ip", "100", "--rpc-max-connections", "100"] monerod_extra = [ ["--offline"], ["--rpc-payment-address", "44SKxxLQw929wRF6BA9paQ1EWFshNnKhXM3qz6Mo3JGDE2YG3xyzVutMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUiqhcwR", "--rpc-payment-difficulty", str(DIFFICULTY), "--rpc-payment-credits", "5000", "--offline"], diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index fec36803eba..2415dfea8d9 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -48,6 +48,7 @@ set(unit_tests_sources dns_resolver.cpp epee_boosted_tcp_server.cpp epee_levin_protocol_handler_async.cpp + epee_http_server.cpp epee_serialization.cpp epee_utils.cpp expect.cpp diff --git a/tests/unit_tests/epee_http_server.cpp b/tests/unit_tests/epee_http_server.cpp new file mode 100644 index 00000000000..1d3b60d5485 --- /dev/null +++ b/tests/unit_tests/epee_http_server.cpp @@ -0,0 +1,200 @@ +// Copyright (c) 2014-2024, 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 +#include +#include +#include +#include +#include +#include "gtest/gtest.h" +#include "net/http_server_handlers_map2.h" +#include "net/http_server_impl_base.h" +#include "storages/portable_storage_template_helper.h" + +namespace +{ + constexpr const std::size_t payload_size = 26 * 1024 * 1024; + constexpr const std::size_t max_private_ips = 25; + struct dummy + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(payload) + END_KV_SERIALIZE_MAP() + + std::string payload; + }; + }; + + std::string make_payload() + { + dummy::request body{}; + const auto body_serialized = epee::serialization::store_t_to_binary(body); + return std::string{ + reinterpret_cast(body_serialized.data()), + body_serialized.size() + }; + } + + struct http_server : epee::http_server_impl_base + { + using connection_context = epee::net_utils::connection_context_base; + + http_server() + : epee::http_server_impl_base(), + dummy_size(payload_size) + {} + + CHAIN_HTTP_TO_MAP2(connection_context); //forward http requests to uri map + + BEGIN_URI_MAP2() + MAP_URI_AUTO_BIN2("/dummy", on_dummy, dummy) + END_URI_MAP2() + + bool on_dummy(const dummy::request&, dummy::response& res, const connection_context *ctx = NULL) + { + res.payload.resize(dummy_size.load(), 'f'); + return true; + } + + std::atomic dummy_size; + }; +} // anonymous + +TEST(http_server, response_soft_limit) +{ + namespace http = boost::beast::http; + + http_server server{}; + server.init(nullptr, "8080"); + server.run(1, false); + + boost::system::error_code error{}; + boost::asio::io_context context{}; + boost::asio::ip::tcp::socket stream{context}; + stream.connect( + boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), 8080 + }, + error + ); + EXPECT_FALSE(bool(error)); + + http::request req{http::verb::get, "/dummy", 11}; + req.set(http::field::host, "127.0.0.1"); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + req.body() = make_payload(); + req.prepare_payload(); + http::write(stream, req, error); + EXPECT_FALSE(bool(error)); + + { + dummy::response payload{}; + boost::beast::flat_buffer buffer; + http::response> res; + http::read(stream, buffer, res, error); + EXPECT_FALSE(bool(error)); + EXPECT_EQ(200u, res.result_int()); + EXPECT_TRUE(epee::serialization::load_t_from_binary(payload, res.body())); + EXPECT_EQ(payload_size, std::count(payload.payload.begin(), payload.payload.end(), 'f')); + } + + while (!error) + http::write(stream, req, error); + server.send_stop_signal(); +} + +TEST(http_server, private_ip_limit) +{ + namespace http = boost::beast::http; + + http_server server{}; + server.dummy_size = 1; + server.init(nullptr, "8080"); + server.run(1, false); + + boost::system::error_code error{}; + boost::asio::io_context context{}; + + http::request req{http::verb::get, "/dummy", 11}; + req.set(http::field::host, "127.0.0.1"); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + req.body() = make_payload(); + req.prepare_payload(); + + std::vector streams{}; + for (std::size_t i = 0; i < max_private_ips; ++i) + { + streams.emplace_back(context); + streams.back().connect( + boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), 8080 + }, + error + ); + http::write(streams.back(), req, error); + EXPECT_FALSE(bool(error)); + + dummy::response payload{}; + boost::beast::flat_buffer buffer; + http::response> res; + + http::read(streams.back(), buffer, res, error); + EXPECT_FALSE(bool(error)); + } + + boost::asio::ip::tcp::socket stream{context}; + stream.connect( + boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), 8080 + }, + error + ); + bool failed = bool(error); + http::write(stream, req, error); + failed |= bool(error); + { + dummy::response payload{}; + boost::beast::flat_buffer buffer; + http::response> res; + + // make sure server ran async_accept code + http::read(stream, buffer, res, error); + } + failed |= bool(error); + EXPECT_TRUE(failed); +}