From 60925f1846ff65dfa8233622d94fffbe6574d1e3 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Tue, 19 Sep 2023 19:05:02 -0400 Subject: [PATCH] Add SSL support to P2P --- .../epee/include/net/abstract_tcp_server2.h | 26 +- .../epee/include/net/abstract_tcp_server2.inl | 184 ++++++++---- contrib/epee/include/net/connection_basic.hpp | 36 +-- contrib/epee/include/net/levin_base.h | 13 +- .../net/levin_protocol_handler_async.h | 6 +- contrib/epee/include/net/net_ssl.h | 29 +- contrib/epee/include/net/net_utils_base.h | 17 +- .../include/storages/levin_abstract_invoke2.h | 3 +- contrib/epee/src/CMakeLists.txt | 1 + contrib/epee/src/levin_base.cpp | 21 +- contrib/epee/src/net_ssl.cpp | 50 ++- docs/LEVIN_PROTOCOL.md | 44 +++ src/cryptonote_config.h | 1 + .../cryptonote_protocol_defs.h | 6 +- src/cryptonote_protocol/levin_notify.cpp | 34 +-- src/daemon/rpc_command_executor.cpp | 18 +- src/p2p/net_node.cpp | 20 +- src/p2p/net_node.h | 75 ++++- src/p2p/net_node.inl | 284 +++++++++++------- src/p2p/net_peerlist.h | 17 +- src/p2p/net_peerlist_boost_serialization.h | 26 +- src/p2p/p2p_protocol_defs.h | 99 +++--- .../functional_tests/functional_tests_rpc.py | 6 +- tests/functional_tests/p2p.py | 30 +- tests/net_load_tests/net_load_tests.h | 2 +- .../epee_levin_protocol_handler_async.cpp | 8 +- tests/unit_tests/levin.cpp | 44 +-- tests/unit_tests/node_server.cpp | 5 +- tests/unit_tests/test_peerlist.cpp | 14 +- 29 files changed, 756 insertions(+), 363 deletions(-) diff --git a/contrib/epee/include/net/abstract_tcp_server2.h b/contrib/epee/include/net/abstract_tcp_server2.h index bc0da66e2..2ea34b022 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.h +++ b/contrib/epee/include/net/abstract_tcp_server2.h @@ -131,6 +131,7 @@ namespace net_utils void terminate(); void on_terminating(); + bool send(epee::byte_slice message); bool start_internal( bool is_income, @@ -296,8 +297,19 @@ namespace net_utils bool speed_limit_is_enabled() const; ///< tells us should we be sleeping here (e.g. do not sleep on RPC connections) - + void set_ssl_enabled() + { + m_state.ssl.enabled = true; + m_state.ssl.handshaked = true; + } bool cancel(); + + //! Used by boosted_tcp_server class in async_connect_internal + template + auto wrap(F&& f) + { + return m_strand.wrap(std::forward(f)); + } private: //----------------- i_service_endpoint --------------------- @@ -376,10 +388,11 @@ namespace net_utils } bool add_connection(t_connection_context& out, boost::asio::ip::tcp::socket&& sock, network_address real_remote, epee::net_utils::ssl_support_t ssl_support = epee::net_utils::ssl_support_t::e_ssl_support_autodetect); - try_connect_result_t try_connect(connection_ptr new_connection_l, const std::string& adr, const std::string& port, boost::asio::ip::tcp::socket &sock_, const boost::asio::ip::tcp::endpoint &remote_endpoint, const std::string &bind_ip, uint32_t conn_timeout, epee::net_utils::ssl_support_t ssl_support); - bool connect(const std::string& adr, const std::string& port, uint32_t conn_timeot, t_connection_context& cn, const std::string& bind_ip = "0.0.0.0", epee::net_utils::ssl_support_t ssl_support = epee::net_utils::ssl_support_t::e_ssl_support_autodetect); + try_connect_result_t try_connect(connection_ptr new_connection_l, const std::string& adr, const std::string& port, boost::asio::ip::tcp::socket &sock_, const boost::asio::ip::tcp::endpoint &remote_endpoint, const std::string &bind_ip, uint32_t conn_timeout, epee::net_utils::ssl_options_t& ssl_support); + bool connect(const std::string& adr, const std::string& port, uint32_t conn_timeot, t_connection_context& cn, const std::string& bind_ip = "0.0.0.0", epee::net_utils::ssl_options_t ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_autodetect); template - bool connect_async(const std::string& adr, const std::string& port, uint32_t conn_timeot, const t_callback &cb, const std::string& bind_ip = "0.0.0.0", epee::net_utils::ssl_support_t ssl_support = epee::net_utils::ssl_support_t::e_ssl_support_autodetect); + bool connect_async(const std::string& adr, const std::string& port, uint32_t conn_timeot, const t_callback &cb, const std::string& bind_ip = "0.0.0.0", epee::net_utils::ssl_options_t ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_autodetect); + boost::asio::ssl::context& get_ssl_context() noexcept { @@ -477,6 +490,11 @@ namespace net_utils bool is_thread_worker(); + template + bool connect_async_internal(const connection_ptr& new_connection_l, const boost::asio::ip::tcp::endpoint& remote_endpoint, uint32_t conn_timeout, const t_callback &cb); + + bool remove_connection(const connection_ptr& ptr); + const std::shared_ptr::shared_state> m_state; /// The io_service used to perform asynchronous operations. diff --git a/contrib/epee/include/net/abstract_tcp_server2.inl b/contrib/epee/include/net/abstract_tcp_server2.inl index d88f18194..31686b635 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.inl +++ b/contrib/epee/include/net/abstract_tcp_server2.inl @@ -895,7 +895,7 @@ namespace net_utils boost::uuids::random_generator()(), *real_remote, is_income, - connection_basic::m_ssl_support == ssl_support_t::e_ssl_support_enabled + connection_basic::m_ssl_support ); m_host = real_remote->host_str(); try { host_count(1); } catch(...) { /* ignore */ } @@ -1559,7 +1559,7 @@ namespace net_utils } //--------------------------------------------------------------------------------- template - typename boosted_tcp_server::try_connect_result_t boosted_tcp_server::try_connect(connection_ptr new_connection_l, const std::string& adr, const std::string& port, boost::asio::ip::tcp::socket &sock_, const boost::asio::ip::tcp::endpoint &remote_endpoint, const std::string &bind_ip, uint32_t conn_timeout, epee::net_utils::ssl_support_t ssl_support) + typename boosted_tcp_server::try_connect_result_t boosted_tcp_server::try_connect(connection_ptr new_connection_l, const std::string& adr, const std::string& port, boost::asio::ip::tcp::socket &sock_, const boost::asio::ip::tcp::endpoint &remote_endpoint, const std::string &bind_ip, uint32_t conn_timeout, epee::net_utils::ssl_options_t& ssl_options) { TRY_ENTRY(); @@ -1635,7 +1635,7 @@ namespace net_utils { // Handshake MDEBUG("Handshaking SSL..."); - if (!new_connection_l->handshake(boost::asio::ssl::stream_base::client)) + if (!new_connection_l->client_handshake(ssl_options)) { if (ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect) { @@ -1649,6 +1649,7 @@ namespace net_utils sock_.close(); return CONNECT_FAILURE; } + new_connection_l->set_ssl_enabled(); } return CONNECT_SUCCESS; @@ -1657,11 +1658,11 @@ namespace net_utils } //--------------------------------------------------------------------------------- template - bool boosted_tcp_server::connect(const std::string& adr, const std::string& port, uint32_t conn_timeout, t_connection_context& conn_context, const std::string& bind_ip, epee::net_utils::ssl_support_t ssl_support) + bool boosted_tcp_server::connect(const std::string& adr, const std::string& port, uint32_t conn_timeout, t_connection_context& conn_context, const std::string& bind_ip, epee::net_utils::ssl_options_t ssl_options) { TRY_ENTRY(); - connection_ptr new_connection_l(new connection(io_service_, m_state, m_connection_type, ssl_support) ); + connection_ptr new_connection_l(new connection(io_service_, m_state, m_connection_type, ssl_options.support) ); connections_mutex.lock(); connections_.insert(new_connection_l); MDEBUG("connections_ size now " << connections_.size()); @@ -1746,24 +1747,22 @@ namespace net_utils //boost::asio::ip::tcp::endpoint remote_endpoint(boost::asio::ip::address::from_string(addr.c_str()), port); boost::asio::ip::tcp::endpoint remote_endpoint(*iterator); - auto try_connect_result = try_connect(new_connection_l, adr, port, sock_, remote_endpoint, bind_ip_to_use, conn_timeout, ssl_support); + auto try_connect_result = try_connect(new_connection_l, adr, port, sock_, remote_endpoint, bind_ip_to_use, conn_timeout, ssl_options); if (try_connect_result == CONNECT_FAILURE) return false; - if (ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect && try_connect_result == CONNECT_NO_SSL) + if (ssl_options.support == epee::net_utils::ssl_support_t::e_ssl_support_autodetect && try_connect_result == CONNECT_NO_SSL) { // we connected, but could not connect with SSL, try without MERROR("SSL handshake failed on an autodetect connection, reconnecting without SSL"); new_connection_l->disable_ssl(); - try_connect_result = try_connect(new_connection_l, adr, port, sock_, remote_endpoint, bind_ip_to_use, conn_timeout, epee::net_utils::ssl_support_t::e_ssl_support_disabled); + ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_disabled; + try_connect_result = try_connect(new_connection_l, adr, port, sock_, remote_endpoint, bind_ip_to_use, conn_timeout, ssl_options); if (try_connect_result != CONNECT_SUCCESS) return false; } // start adds the connection to the config object's list, so we don't need to have it locally anymore - connections_mutex.lock(); - connections_.erase(new_connection_l); - connections_mutex.unlock(); - bool r = new_connection_l->start(false, 1 < m_threads_count); + bool r = remove_connection(new_connection_l) && new_connection_l->start(false, 1 < m_threads_count); if (r) { new_connection_l->get_context(conn_context); @@ -1783,10 +1782,10 @@ namespace net_utils } //--------------------------------------------------------------------------------- template template - bool boosted_tcp_server::connect_async(const std::string& adr, const std::string& port, uint32_t conn_timeout, const t_callback &cb, const std::string& bind_ip, epee::net_utils::ssl_support_t ssl_support) + bool boosted_tcp_server::connect_async(const std::string& adr, const std::string& port, uint32_t conn_timeout, const t_callback &cb, const std::string& bind_ip, epee::net_utils::ssl_options_t ssl_options) { TRY_ENTRY(); - connection_ptr new_connection_l(new connection(io_service_, m_state, m_connection_type, ssl_support) ); + connection_ptr new_connection_l(new connection(io_service_, m_state, m_connection_type, ssl_options.support) ); connections_mutex.lock(); connections_.insert(new_connection_l); MDEBUG("connections_ size now " << connections_.size()); @@ -1863,60 +1862,113 @@ namespace net_utils return false; } } - - boost::shared_ptr sh_deadline(new boost::asio::deadline_timer(io_service_)); - //start deadline - sh_deadline->expires_from_now(boost::posix_time::milliseconds(conn_timeout)); - sh_deadline->async_wait([=](const boost::system::error_code& error) - { - if(error != boost::asio::error::operation_aborted) - { - _dbg3("Failed to connect to " << adr << ':' << port << ", because of timeout (" << conn_timeout << ")"); - new_connection_l->socket().close(); - } - }); - //start async connect - sock_.async_connect(remote_endpoint, [=](const boost::system::error_code& ec_) - { - t_connection_context conn_context = AUTO_VAL_INIT(conn_context); - boost::system::error_code ignored_ec; - boost::asio::ip::tcp::socket::endpoint_type lep = new_connection_l->socket().local_endpoint(ignored_ec); - if(!ec_) - {//success - if(!sh_deadline->cancel()) - { - cb(conn_context, boost::asio::error::operation_aborted);//this mean that deadline timer already queued callback with cancel operation, rare situation - }else - { - _dbg3("[sock " << new_connection_l->socket().native_handle() << "] Connected success to " << adr << ':' << port << - " from " << lep.address().to_string() << ':' << lep.port()); - // start adds the connection to the config object's list, so we don't need to have it locally anymore - connections_mutex.lock(); - connections_.erase(new_connection_l); - connections_mutex.unlock(); - bool r = new_connection_l->start(false, 1 < m_threads_count); - if (r) - { - new_connection_l->get_context(conn_context); - cb(conn_context, ec_); - } - else - { - _dbg3("[sock " << new_connection_l->socket().native_handle() << "] Failed to start connection to " << adr << ':' << port); - cb(conn_context, boost::asio::error::fault); - } - } - }else - { - _dbg3("[sock " << new_connection_l->socket().native_handle() << "] Failed to connect to " << adr << ':' << port << - " from " << lep.address().to_string() << ':' << lep.port() << ": " << ec_.message() << ':' << ec_.value()); - cb(conn_context, ec_); - } - }); - return true; + ssl_options.configure(new_connection_l->socket_, boost::asio::ssl::stream_base::client); + return connect_async_internal(new_connection_l, remote_endpoint, conn_timeout, cb); CATCH_ENTRY_L0("boosted_tcp_server::connect_async", false); } - + + template template + bool boosted_tcp_server::connect_async_internal(const connection_ptr& new_connection_l, const boost::asio::ip::tcp::endpoint& remote_endpoint, uint32_t conn_timeout, const t_callback &cb) + { + if (!new_connection_l) + return false; + + TRY_ENTRY(); + + const auto on_timer = [=](boost::system::error_code error) + { + if (error != boost::asio::error::operation_aborted) + { + _dbg3("Failed to connect to " << remote_endpoint << ", because of timeout (" << conn_timeout << ')'); + new_connection_l->socket().close(error); // ignore errors + } + }; + + auto sh_deadline = std::make_shared(io_service_); + sh_deadline->expires_from_now(boost::posix_time::milliseconds(conn_timeout)); + sh_deadline->async_wait(new_connection_l->wrap(on_timer)); + + new_connection_l->socket().async_connect(remote_endpoint, new_connection_l->wrap([=](const boost::system::error_code& ec_) + { + const auto on_cancel = [=](const boost::system::error_code& error) + { + boost::system::error_code ignored_ec{}; + const auto lep = new_connection_l->socket().local_endpoint(ignored_ec); + _dbg3("[sock " << new_connection_l->socket().native_handle() << "] to " << remote_endpoint << " from " << lep << " failed: " << error.message()); + if (remove_connection(new_connection_l)) + cb(t_connection_context{}, error); + }; + + if(!ec_) + {//success + const auto on_ready = [=] () + { + if (sh_deadline->cancel()) + { + boost::system::error_code ignored_ec{}; + const auto lep = new_connection_l->socket().local_endpoint(ignored_ec); + _dbg3("[sock " << new_connection_l->socket().native_handle() << "] Connected successfully to " << remote_endpoint << + " from " << lep.address().to_string() << ':' << lep.port()); + + if (remove_connection(new_connection_l) && new_connection_l->start(false, 1 < m_threads_count)) + { + t_connection_context conn_context{}; + new_connection_l->get_context(conn_context); + cb(conn_context, ec_); + } + else + on_cancel(boost::asio::error::fault); + } + else // if timer already expired + on_cancel(boost::asio::error::operation_aborted); + }; + + if (new_connection_l->get_ssl_support() != ssl_support_t::e_ssl_support_disabled) + { + // set new timer for handshake + if (sh_deadline->expires_from_now(boost::posix_time::milliseconds(conn_timeout))) + { + sh_deadline->async_wait(new_connection_l->wrap(on_timer)); + new_connection_l->socket_.async_handshake(boost::asio::ssl::stream_base::client, new_connection_l->wrap([=] (const boost::system::error_code& ec) + { + if (ec) + { + if (new_connection_l->get_ssl_support() == ssl_support_t::e_ssl_support_autodetect) + { + _dbg3("[sock " << new_connection_l->socket().native_handle() << "] SSL connection to " << + remote_endpoint << " failed: " << ec.message() << ". Trying without SSL"); + new_connection_l->disable_ssl(); + connect_async_internal(new_connection_l, remote_endpoint, conn_timeout, cb); + } + else // ssl mandatory and failed + on_cancel(ec); + } + else // ssl handshake complete + on_ready(); + })); + } + else // if timer already expired + on_cancel(boost::asio::error::operation_aborted); + } + else // ssl disabled + on_ready(); + } + else // ec_ has error + on_cancel(ec_); + })); + return true; + CATCH_ENTRY_L0("boosted_tcp_server::connect_async_internal", false); + } + + template + bool boosted_tcp_server::remove_connection(const connection_ptr& new_connection) + { + if (!new_connection) + return false; + const boost::lock_guard sync{connections_mutex}; + connections_.erase(new_connection); + return true; + } } // namespace } // namespace diff --git a/contrib/epee/include/net/connection_basic.hpp b/contrib/epee/include/net/connection_basic.hpp index e3093de51..cbb464a7c 100644 --- a/contrib/epee/include/net/connection_basic.hpp +++ b/contrib/epee/include/net/connection_basic.hpp @@ -132,37 +132,15 @@ class connection_basic { // not-templated base class for rapid developmet of som ssl_support_t get_ssl_support() const { return m_ssl_support; } void disable_ssl() { m_ssl_support = epee::net_utils::ssl_support_t::e_ssl_support_disabled; } - bool handshake(boost::asio::ssl::stream_base::handshake_type type, boost::asio::const_buffer buffer = {}) + bool client_handshake(ssl_options_t& ssl) + { + return ssl.handshake(socket_, boost::asio::ssl::stream_base::client); + } + + bool server_handshake(boost::asio::const_buffer buffer) { //m_state != nullptr verified in constructor - return m_state->ssl_options().handshake(socket_, type, buffer); - } - - template - void async_read_some(const MutableBufferSequence &buffers, ReadHandler &&handler) - { - if (m_ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_enabled) - socket_.async_read_some(buffers, std::forward(handler)); - else - socket().async_read_some(buffers, std::forward(handler)); - } - - template - void async_write_some(const ConstBufferSequence &buffers, WriteHandler &&handler) - { - if (m_ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_enabled) - socket_.async_write_some(buffers, std::forward(handler)); - else - socket().async_write_some(buffers, std::forward(handler)); - } - - template - void async_write(const ConstBufferSequence &buffers, WriteHandler &&handler) - { - if (m_ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_enabled) - boost::asio::async_write(socket_, buffers, std::forward(handler)); - else - boost::asio::async_write(socket(), buffers, std::forward(handler)); + return m_state->ssl_options().handshake(socket_, boost::asio::ssl::stream_base::server, buffer); } // various handlers to be called from connection class: diff --git a/contrib/epee/include/net/levin_base.h b/contrib/epee/include/net/levin_base.h index b680691ad..d9e057ced 100644 --- a/contrib/epee/include/net/levin_base.h +++ b/contrib/epee/include/net/levin_base.h @@ -130,7 +130,7 @@ namespace levin //! Provides space for levin (p2p) header, so that payload can be sent without copy class message_writer { - byte_slice finalize(uint32_t command, uint32_t flags, uint32_t return_code, bool expect_response); + byte_slice finalize(uint32_t command, uint32_t flags, uint32_t return_code, bool expect_response, bool pad); public: using header = bucket_head2; @@ -147,12 +147,13 @@ namespace levin { return buffer.size() < sizeof(header) ? 0 : buffer.size() - sizeof(header); } - - byte_slice finalize_invoke(uint32_t command) { return finalize(command, LEVIN_PACKET_REQUEST, 0, true); } - byte_slice finalize_notify(uint32_t command) { return finalize(command, LEVIN_PACKET_REQUEST, 0, false); } - byte_slice finalize_response(uint32_t command, uint32_t return_code) + + // `pad == true` will add 0-8192 of zero bytes (actual amount randomized) + byte_slice finalize_invoke(uint32_t command, bool pad) { return finalize(command, LEVIN_PACKET_REQUEST, 0, true, pad); } + byte_slice finalize_notify(uint32_t command, bool pad) { return finalize(command, LEVIN_PACKET_REQUEST, 0, false, pad); } + byte_slice finalize_response(uint32_t command, uint32_t return_code, bool pad) { - return finalize(command, LEVIN_PACKET_RESPONSE, return_code, false); + return finalize(command, LEVIN_PACKET_RESPONSE, return_code, false, pad); } //! Has space for levin header until a finalize method is used diff --git a/contrib/epee/include/net/levin_protocol_handler_async.h b/contrib/epee/include/net/levin_protocol_handler_async.h index 5122f1677..68448010c 100644 --- a/contrib/epee/include/net/levin_protocol_handler_async.h +++ b/contrib/epee/include/net/levin_protocol_handler_async.h @@ -486,7 +486,7 @@ public: bool is_response = (m_oponent_protocol_ver == LEVIN_PROTOCOL_VER_1 && m_current_head.m_flags&LEVIN_PACKET_RESPONSE); - MDEBUG(m_connection_context << "LEVIN_PACKET_RECEIVED. [len=" << m_current_head.m_cb + MDEBUG(m_connection_context << "LEVIN_PACKET_RECEIVED. [len=" << m_current_head.m_cb << ", flags" << m_current_head.m_flags << ", r?=" << m_current_head.m_have_to_return_data <<", cmd = " << m_current_head.m_command @@ -526,7 +526,7 @@ public: if (m_current_head.m_command == m_connection_context.handshake_command() && m_connection_context.handshake_complete()) m_max_packet_size = m_config.m_max_packet_size; - if(!send_message(return_message.finalize_response(m_current_head.m_command, return_code))) + if(!send_message(return_message.finalize_response(m_current_head.m_command, return_code, m_connection_context.should_pad()))) return false; } else @@ -620,7 +620,7 @@ public: if (command == m_connection_context.handshake_command()) m_max_packet_size = m_config.m_max_packet_size; - if(!send_message(in_msg.finalize_invoke(command))) + if(!send_message(in_msg.finalize_invoke(command, m_connection_context.should_pad()))) { LOG_ERROR_CC(m_connection_context, "Failed to do_send"); err_code = LEVIN_ERROR_CONNECTION; diff --git a/contrib/epee/include/net/net_ssl.h b/contrib/epee/include/net/net_ssl.h index c6ef925ba..f57fd53de 100644 --- a/contrib/epee/include/net/net_ssl.h +++ b/contrib/epee/include/net/net_ssl.h @@ -46,7 +46,7 @@ namespace epee namespace net_utils { enum class ssl_support_t: uint8_t { - e_ssl_support_disabled, + e_ssl_support_disabled = 0, e_ssl_support_enabled, e_ssl_support_autodetect, }; @@ -107,6 +107,9 @@ namespace net_utils //! \return True if `host` can be verified using `this` configuration WITHOUT system "root" CAs. bool has_strong_verification(boost::string_ref host) const noexcept; + //! \return All fingerprints + const std::vector>& fingerprints() const noexcept { return fingerprints_; } + //! Search against internal fingerprints. Always false if `behavior() != user_certificate_check`. bool has_fingerprint(boost::asio::ssl::verify_context &ctx) const; @@ -151,6 +154,30 @@ namespace net_utils bool create_ec_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert); bool create_rsa_ssl_certificate(EVP_PKEY *&pkey, X509 *&cert); + std::vector convert_fingerprint(const boost::string_ref id); + + /** + * @brief Create a binary X509 certificate fingerprint + * + * @param[in] cert The certificate which will be used to create the fingerprint + * @param[in] fdig The digest algorithm to use, defaults to SHA-256 b/c that is what ssl_options_t uses + * @return The binary fingerprint string + * + * @throw boost::system_error if there is an OpenSSL error + */ + std::string get_ssl_fingerprint(const X509 *cert, const EVP_MD *fdig = EVP_sha256()); + + /** + * @brief Create a binary X509 certificate fingerprint + * + * @param[in] context The context for the current certificate. + * @param[in] fdig The digest algorithm to use, defaults to SHA-256 b/c that is what ssl_options_t uses + * @return The binary fingerprint string + * + * @throw boost::system_error if there is an OpenSSL error + */ + std::string get_ssl_fingerprint(boost::asio::ssl::context& context, const EVP_MD *fdig = EVP_sha256()); + /** * @brief Create a human-readable X509 certificate fingerprint * diff --git a/contrib/epee/include/net/net_utils_base.h b/contrib/epee/include/net/net_utils_base.h index 722206ee1..4612e917d 100644 --- a/contrib/epee/include/net/net_utils_base.h +++ b/contrib/epee/include/net/net_utils_base.h @@ -32,11 +32,13 @@ #include #include #include +#include #include #include #include "byte_slice.h" #include "enums.h" #include "misc_log_ex.h" +#include "net_ssl.h" #include "serialization/keyvalue_serialization.h" #include "int-util.h" @@ -367,7 +369,7 @@ namespace net_utils const network_address m_remote_address; const bool m_is_income; const time_t m_started; - const bool m_ssl; + const ssl_support_t m_ssl; time_t m_last_recv; time_t m_last_send; uint64_t m_recv_cnt; @@ -378,7 +380,7 @@ namespace net_utils double m_max_speed_up; connection_context_base(boost::uuids::uuid connection_id, - const network_address &remote_address, bool is_income, bool ssl, + const network_address &remote_address, bool is_income, ssl_support_t ssl, time_t last_recv = 0, time_t last_send = 0, uint64_t recv_cnt = 0, uint64_t send_cnt = 0): m_connection_id(connection_id), @@ -400,7 +402,7 @@ namespace net_utils m_remote_address(), m_is_income(false), m_started(time(NULL)), - m_ssl(false), + m_ssl(ssl_support_t::e_ssl_support_disabled), m_last_recv(0), m_last_send(0), m_recv_cnt(0), @@ -421,11 +423,18 @@ namespace net_utils set_details(a.m_connection_id, a.m_remote_address, a.m_is_income, a.m_ssl); return *this; } + + bool should_pad() const + { + if (m_remote_address.get_zone() == zone::public_) + return m_ssl != ssl_support_t::e_ssl_support_disabled; + return true; // all other zones use encryption by default + } private: template friend class connection; - void set_details(boost::uuids::uuid connection_id, const network_address &remote_address, bool is_income, bool ssl) + void set_details(boost::uuids::uuid connection_id, const network_address &remote_address, bool is_income, ssl_support_t ssl) { this->~connection_context_base(); new(this) connection_context_base(connection_id, remote_address, is_income, ssl); diff --git a/contrib/epee/include/storages/levin_abstract_invoke2.h b/contrib/epee/include/storages/levin_abstract_invoke2.h index 9d3dda0d6..cb6e486fc 100644 --- a/contrib/epee/include/storages/levin_abstract_invoke2.h +++ b/contrib/epee/include/storages/levin_abstract_invoke2.h @@ -111,7 +111,8 @@ namespace epee levin::message_writer to_send; stg.store_to_binary(to_send.buffer); - int res = transport.send(to_send.finalize_notify(command), conn_id); + const bool ssl = (context.m_ssl != ssl_support_t::e_ssl_support_disabled); + int res = transport.send(to_send.finalize_notify(command, ssl), conn_id); if(res <=0 ) { MERROR("Failed to notify command " << command << " return code " << res); diff --git a/contrib/epee/src/CMakeLists.txt b/contrib/epee/src/CMakeLists.txt index 71ff94f00..cb282472e 100644 --- a/contrib/epee/src/CMakeLists.txt +++ b/contrib/epee/src/CMakeLists.txt @@ -70,6 +70,7 @@ target_link_libraries(epee ${Boost_REGEX_LIBRARY} ${Boost_SYSTEM_LIBRARY} ${OPENSSL_LIBRARIES} + ${sodium_LIBRARIES} PRIVATE ${EXTRA_LIBRARIES}) diff --git a/contrib/epee/src/levin_base.cpp b/contrib/epee/src/levin_base.cpp index 111fe6651..933b0fddd 100644 --- a/contrib/epee/src/levin_base.cpp +++ b/contrib/epee/src/levin_base.cpp @@ -28,6 +28,8 @@ #include "net/levin_base.h" +#include +#include "cryptonote_config.h" #include "int-util.h" namespace epee @@ -41,15 +43,25 @@ namespace levin buffer.put_n(0, sizeof(header)); } - byte_slice message_writer::finalize(const uint32_t command, const uint32_t flags, const uint32_t return_code, const bool expect_response) + byte_slice message_writer::finalize(const uint32_t command, const uint32_t flags, const uint32_t return_code, const bool expect_response, const bool pad) { if (buffer.size() < sizeof(header)) throw std::runtime_error{"levin_writer::finalize already called"}; - header head = make_header(command, payload_size(), flags, expect_response); + std::uint32_t pad_bytes = 0; + if (pad) + { + static_assert(P2P_MAX_LEVIN_PAD_BYTES <= std::numeric_limits::max(), "invalid max ssl pad bytes"); + pad_bytes = randombytes_uniform(P2P_MAX_LEVIN_PAD_BYTES); + if (std::numeric_limits::max() - payload_size() < pad_bytes) + throw std::runtime_error{"levin_write::finalize failed to pad bytes due to overflow"}; + } + + header head = make_header(command, payload_size() + pad_bytes, flags, expect_response); head.m_return_code = SWAP32LE(return_code); std::memcpy(buffer.tellp() - buffer.size(), std::addressof(head), sizeof(head)); + buffer.put_n(0, pad_bytes); return byte_slice{std::move(buffer)}; } @@ -92,12 +104,13 @@ namespace levin will ignore extra bytes. So just pad with zeroes and otherwise send a "normal", not fragmented message. */ + // do not use randomized padding, this must be to message boundary message.buffer.put_n(0, noise_size - message.buffer.size()); - return message.finalize_notify(command); + return message.finalize_notify(command, false); } // fragment message - const byte_slice payload_bytes = message.finalize_notify(command); + const byte_slice payload_bytes = message.finalize_notify(command, false); span payload = to_span(payload_bytes); const size_t payload_space = noise_size - sizeof(bucket_head2); diff --git a/contrib/epee/src/net_ssl.cpp b/contrib/epee/src/net_ssl.cpp index 0ad71d9c0..f07f9e9ef 100644 --- a/contrib/epee/src/net_ssl.cpp +++ b/contrib/epee/src/net_ssl.cpp @@ -27,6 +27,7 @@ // 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 @@ -521,18 +522,19 @@ void ssl_options_t::configure( { socket.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert); - - socket.set_verify_callback([&](const bool preverified, boost::asio::ssl::verify_context &ctx) + // callback occurs after `configure` has exited, so keep copies and not references + const ssl_options_t this_copy(*this); + socket.set_verify_callback([host, this_copy = std::move(this_copy)](const bool preverified, boost::asio::ssl::verify_context &ctx) { // preverified means it passed system or user CA check. System CA is never loaded // when fingerprints are whitelisted. const bool verified = preverified && - (verification != ssl_verification_t::system_ca || host.empty() || boost::asio::ssl::rfc2818_verification(host)(preverified, ctx)); + (this_copy.verification != ssl_verification_t::system_ca || host.empty() || boost::asio::ssl::rfc2818_verification(host)(preverified, ctx)); - if (!verified && !has_fingerprint(ctx)) + if (!verified && !this_copy.has_fingerprint(ctx)) { // autodetect will reconnect without SSL - warn and keep connection encrypted - if (support != ssl_support_t::e_ssl_support_autodetect) + if (this_copy.support != ssl_support_t::e_ssl_support_autodetect) { MERROR("SSL certificate is not in the allowed list, connection dropped"); return false; @@ -644,6 +646,44 @@ bool ssl_options_t::handshake( return true; } +std::vector convert_fingerprint(const boost::string_ref id) +{ + std::vector out; + out.resize(id.size()); + std::copy(id.begin(), id.end(), out.begin()); + return out; +} + +std::string get_ssl_fingerprint(const X509 *cert, const EVP_MD *fdig) +{ + CHECK_AND_ASSERT_THROW_MES(cert && fdig, "Pointer args to get_ssl_fingerprint cannot be null"); + + std::string md; + unsigned n = 0; + md.resize(EVP_MAX_MD_SIZE); + if (!X509_digest(cert, fdig, reinterpret_cast(&md[0]), &n)) + { + const unsigned long ssl_err_val = static_cast(ERR_get_error()); + const boost::system::error_code ssl_err_code = boost::asio::error::ssl_errors(static_cast(ssl_err_val)); + MERROR("Failed to create SSL fingerprint: " << ERR_reason_error_string(ssl_err_val)); + throw boost::system::system_error(ssl_err_code, ERR_reason_error_string(ssl_err_val)); + } + md.resize(n); + return md; +} + +std::string get_ssl_fingerprint(boost::asio::ssl::context &ssl_context, const EVP_MD *fdig) +{ + SSL_CTX *ctx = ssl_context.native_handle(); + CHECK_AND_ASSERT_THROW_MES(ctx, "Failed to get SSL context"); + + X509* ssl_cert = nullptr; + std::unique_ptr dflt_SSL(SSL_new(ctx), SSL_free); + CHECK_AND_ASSERT_THROW_MES(dflt_SSL, "Failed to create new SSL object"); + CHECK_AND_ASSERT_THROW_MES((ssl_cert = SSL_get_certificate(dflt_SSL.get())), "Failed to get SSL certificate"); + return get_ssl_fingerprint(ssl_cert, fdig); +} + std::string get_hr_ssl_fingerprint(const X509 *cert, const EVP_MD *fdig) { unsigned int j; diff --git a/docs/LEVIN_PROTOCOL.md b/docs/LEVIN_PROTOCOL.md index 2a5dacb84..d2abb7812 100644 --- a/docs/LEVIN_PROTOCOL.md +++ b/docs/LEVIN_PROTOCOL.md @@ -135,6 +135,50 @@ the `Expect Response` field zeroed. When a message of this type is received, the contents can be safely ignored. +## Encryption +The Levin protocol now has optional SSL encryption. Each peer can have 4 states: +(1) no encryption, (2) autodetect encryption, (3) mandatory encryption with no +certificate verification, or (4) mandatory encryption with certificate +verification. + +When a peerlist is shared, the encryption information is ignored (not +trusted), and is put into `autodetect` mode by default. The node data shared +during handshakes OR pings can change the node state to `no encryption` or +`authenticated encryption`, based on direction information from the peer. + +> The server/responder must always be in autodetect SSL mode, so that it can +handle any possible state that the client/initiator is in. + +> If a node changes certificates, "old" peers will be unable to connect until +the node is completely purged from the white/gray lists OR until the node makes +a direct connection to provide the new encryption state. Incoming connections +from "new" peers (that never saw the old certificate) will still be possible. + +### No Encryption +If a peer chooses to have no encryption, it will send `encryption_mode = 1` in +Handshake messages. All clients/initiators can then skip an attempt to connect +via SSL. + +### Autodetect Encryption +All peers start in this state, and only move to `no encryption` or +`authenticated encryption` when: (1) the peer is listed on the CLI explicitly +requesting one of the other states, or (2) the peer sends a handshake or ping +message indicating it should be in another state. + +When connecting to a peer of this type, the client/initiator should attempt +SSL with no server/responder verification. If the connection fails, it should +immediately attempt a non-SSL connection. + +### Authenticated Encryption +If a peer chooses to have a persistent SSL certificate, it will send +`encryption_ver = 2` AND `ssl_finger = ` in the +Handshake messages. + +When connecting to a peer of this type, the client/initiator should attempt +a SSL connection with verification of the SSL fingerprint. The connection +should fail if the fingerprint check fails - it is treated identically to +a invalid/old port number. + ## Commands ### P2P (Admin) Commands diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index fec905928..29d361a9d 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -147,6 +147,7 @@ #define P2P_DEFAULT_SYNC_SEARCH_CONNECTIONS_COUNT 2 #define P2P_DEFAULT_LIMIT_RATE_UP 2048 // kB/s #define P2P_DEFAULT_LIMIT_RATE_DOWN 8192 // kB/s +#define P2P_MAX_LEVIN_PAD_BYTES 8192 #define P2P_FAILED_ADDR_FORGET_SECONDS (60*60) //1 hour #define P2P_IP_BLOCKTIME (60*60*24) //24 hour diff --git a/src/cryptonote_protocol/cryptonote_protocol_defs.h b/src/cryptonote_protocol/cryptonote_protocol_defs.h index 23d9144a5..705fcff90 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_defs.h +++ b/src/cryptonote_protocol/cryptonote_protocol_defs.h @@ -34,6 +34,7 @@ #include "serialization/keyvalue_serialization.h" #include "cryptonote_basic/cryptonote_basic.h" #include "cryptonote_basic/blobdatatype.h" +#include "net/net_ssl.h" namespace cryptonote { @@ -49,7 +50,7 @@ namespace cryptonote bool incoming; bool localhost; bool local_ip; - bool ssl; + epee::net_utils::ssl_support_t ssl; std::string address; std::string host; @@ -87,9 +88,11 @@ namespace cryptonote uint8_t address_type; BEGIN_KV_SERIALIZE_MAP() + uint8_t ssl_i = uint8_t(ssl); KV_SERIALIZE(incoming) KV_SERIALIZE(localhost) KV_SERIALIZE(local_ip) + epee::serialization::selector::serialize(ssl_i, stg, hparent_section, "ssl"); KV_SERIALIZE(address) KV_SERIALIZE(host) KV_SERIALIZE(ip) @@ -112,6 +115,7 @@ namespace cryptonote KV_SERIALIZE(height) KV_SERIALIZE(pruning_seed) KV_SERIALIZE(address_type) + ssl = epee::net_utils::ssl_support_t(ssl_i); END_KV_SERIALIZE_MAP() }; diff --git a/src/cryptonote_protocol/levin_notify.cpp b/src/cryptonote_protocol/levin_notify.cpp index 1c3a2901c..b05017ff5 100644 --- a/src/cryptonote_protocol/levin_notify.cpp +++ b/src/cryptonote_protocol/levin_notify.cpp @@ -159,40 +159,12 @@ namespace levin return get_out_connections(p2p, get_blockchain_height(p2p, core)); } - epee::levin::message_writer make_tx_message(std::vector&& txs, const bool pad, const bool fluff) + epee::levin::message_writer make_tx_message(std::vector&& txs, const bool fluff) { NOTIFY_NEW_TRANSACTIONS::request request{}; request.txs = std::move(txs); request.dandelionpp_fluff = fluff; - if (pad) - { - size_t bytes = 9 /* header */ + 4 /* 1 + 'txs' */ + tools::get_varint_data(request.txs.size()).size(); - for(auto tx_blob_it = request.txs.begin(); tx_blob_it!=request.txs.end(); ++tx_blob_it) - bytes += tools::get_varint_data(tx_blob_it->size()).size() + tx_blob_it->size(); - - // stuff some dummy bytes in to stay safe from traffic volume analysis - static constexpr const size_t granularity = 1024; - size_t padding = granularity - bytes % granularity; - const size_t overhead = 2 /* 1 + '_' */ + tools::get_varint_data(padding).size(); - if (overhead > padding) - padding = 0; - else - padding -= overhead; - request._ = std::string(padding, ' '); - - epee::byte_slice arg_buff; - epee::serialization::store_t_to_binary(request, arg_buff); - - // we probably lowballed the payload size a bit, so added a but too much. Fix this now. - size_t remove = arg_buff.size() % granularity; - if (remove > request._.size()) - request._.clear(); - else - request._.resize(request._.size() - remove); - // if the size of _ moved enough, we might lose byte in size encoding, we don't care - } - epee::levin::message_writer out; if (!epee::serialization::store_t_to_binary(request, out.buffer)) throw std::runtime_error{"Failed to serialize to epee binary format"}; @@ -202,7 +174,7 @@ namespace levin bool make_payload_send_txs(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination, const bool pad, const bool fluff) { - epee::byte_slice blob = make_tx_message(std::move(txs), pad, fluff).finalize_notify(NOTIFY_NEW_TRANSACTIONS::ID); + epee::byte_slice blob = make_tx_message(std::move(txs), fluff).finalize_notify(NOTIFY_NEW_TRANSACTIONS::ID, pad); return p2p.send(std::move(blob), destination); } @@ -847,7 +819,7 @@ namespace levin // Padding is not useful when using noise mode. Send as stem so receiver // forwards in Dandelion++ mode. epee::byte_slice message = epee::levin::make_fragmented_notify( - zone_->noise.size(), NOTIFY_NEW_TRANSACTIONS::ID, make_tx_message(std::move(txs), false, false) + zone_->noise.size(), NOTIFY_NEW_TRANSACTIONS::ID, make_tx_message(std::move(txs), false) ); if (CRYPTONOTE_MAX_FRAGMENTS * zone_->noise.size() < message.size()) { diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index fbf26db85..2c8600bf5 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -653,7 +653,7 @@ bool t_rpc_command_executor::print_connections() { tools::msg_writer() << std::setw(host_field_width) << std::left << "Remote Host" << std::setw(8) << "Type" - << std::setw(6) << "SSL" + << std::setw(13) << "SSL" << std::setw(20) << "Peer id" << std::setw(20) << "Support Flags" << std::setw(30) << "Recv/Sent (inactive,sec)" @@ -664,6 +664,20 @@ bool t_rpc_command_executor::print_connections() { << std::setw(10) << "Up (kB/s)" << std::setw(13) << "Up(now)" << std::endl; + const auto ssl_string = [] (const epee::net_utils::ssl_support_t ssl) -> const char* + { + switch (ssl) + { + default: + case epee::net_utils::ssl_support_t::e_ssl_support_disabled: + break; + case epee::net_utils::ssl_support_t::e_ssl_support_autodetect: + return "autodetect"; + case epee::net_utils::ssl_support_t::e_ssl_support_enabled: + return "verified"; + } + return "no"; + }; for (auto & info : res.connections) { @@ -674,7 +688,7 @@ bool t_rpc_command_executor::print_connections() { //<< std::setw(30) << std::left << in_out << std::setw(host_field_width) << std::left << address << std::setw(8) << (get_address_type_name((epee::net_utils::address_type)info.address_type)) - << std::setw(6) << (info.ssl ? "yes" : "no") + << std::setw(13) << ssl_string(info.ssl) << std::setw(20) << info.peer_id << std::setw(20) << info.support_flags << std::setw(30) << std::to_string(info.recv_count) + "(" + std::to_string(info.recv_idle_time) + ")/" + std::to_string(info.send_count) + "(" + std::to_string(info.send_idle_time) + ")" diff --git a/src/p2p/net_node.cpp b/src/p2p/net_node.cpp index 13ff06c8b..a616d2125 100644 --- a/src/p2p/net_node.cpp +++ b/src/p2p/net_node.cpp @@ -142,17 +142,23 @@ namespace nodetool const command_line::arg_descriptor arg_p2p_external_port = {"p2p-external-port", "External port for p2p network protocol (if port forwarding used with NAT)", 0}; const command_line::arg_descriptor arg_p2p_allow_local_ip = {"allow-local-ip", "Allow local ip add to peer list, mostly in debug purposes"}; - const command_line::arg_descriptor > arg_p2p_add_peer = {"add-peer", "Manually add peer to local peerlist"}; - const command_line::arg_descriptor > arg_p2p_add_priority_node = {"add-priority-node", "Specify list of peers to connect to and attempt to keep the connection open"}; - const command_line::arg_descriptor > arg_p2p_add_exclusive_node = {"add-exclusive-node", "Specify list of peers to connect to only." - " If this option is given the options add-priority-node and seed-node are ignored"}; - const command_line::arg_descriptor > arg_p2p_seed_node = {"seed-node", "Connect to a node to retrieve peer addresses, and disconnect"}; + const command_line::arg_descriptor > arg_p2p_add_peer = {"add-peer", "Manually add peer to local peerlist. Append \",sha-256 fingerprint\" to make authenticated connection." + "Append \",no_encryption\" to disable SSL autodetect."}; + const command_line::arg_descriptor > arg_p2p_add_priority_node = {"add-priority-node", "Specify list of peers to connect to and attempt to keep the connection open. " + "Append \",sha-256 fingerprint\" to make authenticated connection. Append \",no_encryption\" to disable SSL autodetect."}; + const command_line::arg_descriptor > arg_p2p_add_exclusive_node = {"add-exclusive-node", "Specify list of peers to connect to only. " + " If this option is given the options add-priority-node and seed-node are ignored. " + "Append \",sha-256 fingerprint\" to make authenticated connection. Append \",no_encryption\" to disable SSL autodetect."}; + const command_line::arg_descriptor > arg_p2p_seed_node = {"seed-node", "Connect to a node to retrieve peer addresses, and disconnect. " + "Append \",sha-256 fingerprint\" to make authenticated connection. Append \",no_encryption\" to disable SSL autodetect."}; const command_line::arg_descriptor > arg_tx_proxy = {"tx-proxy", "Send local txes through proxy: ,[,max_connections][,disable_noise] i.e. \"tor,127.0.0.1:9050,100,disable_noise\""}; const command_line::arg_descriptor > arg_anonymous_inbound = {"anonymous-inbound", ",<[bind-ip:]port>[,max_connections] i.e. \"x.onion,127.0.0.1:18083,100\""}; const command_line::arg_descriptor arg_ban_list = {"ban-list", "Specify ban list file, one IP address per line"}; const command_line::arg_descriptor arg_p2p_hide_my_port = {"hide-my-port", "Do not announce yourself as peerlist candidate", false, true}; const command_line::arg_descriptor arg_no_sync = {"no-sync", "Don't synchronize the blockchain with other peers", false}; const command_line::arg_descriptor arg_enable_dns_blocklist = {"enable-dns-blocklist", "Apply realtime blocklist from DNS", false}; + const command_line::arg_descriptor arg_p2p_disable_encryption = {"p2p-disable-encryption", "Disable all P2P encryption", false}; + const command_line::arg_descriptor arg_p2p_persistent_cert = {"p2p-persistent-cert", "Persist p2p SSL certificate across monerod runs to authenticate/identify server", false}; const command_line::arg_descriptor arg_no_igd = {"no-igd", "Disable UPnP port mapping"}; const command_line::arg_descriptor arg_igd = {"igd", "UPnP port mapping (disabled, enabled, delayed)", "delayed"}; @@ -167,8 +173,10 @@ namespace nodetool const command_line::arg_descriptor arg_limit_rate = {"limit-rate", "set limit-rate [kB/s]", -1}; const command_line::arg_descriptor arg_pad_transactions = { - "pad-transactions", "Pad relayed transactions to help defend against traffic volume analysis", false + "pad-transactions", "Pad relayed transactions to help defend against traffic volume analysis. Always enabled on SSL, Tor, and I2P connections.", 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}; boost::optional> get_proxies(boost::program_options::variables_map const& vm) diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index 459a6a396..5ab528a99 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -110,17 +110,58 @@ namespace nodetool struct p2p_connection_context_t: base_type //t_payload_net_handler::connection_context //public net_utils::connection_context_base { p2p_connection_context_t() - : peer_id(0), + : cert_finger(), + peer_id(0), support_flags(0), + encryption_mode(emode_ssl_autodetect), m_in_timedsync(false) {} + std::string cert_finger; peerid_type peer_id; uint32_t support_flags; + uint8_t encryption_mode; bool m_in_timedsync; std::set sent_addresses; }; + struct p2p_address + { + epee::net_utils::network_address na; + epee::net_utils::ssl_options_t ssl = epee::net_utils::ssl_support_t::e_ssl_support_autodetect; + + template + void update_peer(T& peer) const + { + peer.adr = na; + peer.encryption_mode = emode_ssl_autodetect; + if (!ssl) + peer.encryption_mode = emode_disabled; + if (!ssl.fingerprints().empty()) + { + peer.encryption_mode = emode_ssl_enabled; + auto& fingerprint = ssl.fingerprints().front(); + peer.cert_finger.resize(fingerprint.size()); + std::copy(fingerprint.begin(), fingerprint.end(), peer.cert_finger.begin()); + } + } + + bool operator==(const p2p_address& rhs) const + { return na == rhs.na; } + }; + + struct string_address + { + std::string address; + epee::net_utils::ssl_options_t ssl; + + bool operator<(const string_address& rhs) const noexcept + { return address < rhs.address; } + + bool operator==(const string_address& rhs) const noexcept + { return address == rhs.address; } + }; + template class node_server: public epee::levin::levin_commands_handler >, public i_p2p_endpoint, @@ -139,7 +180,7 @@ namespace nodetool typedef epee::net_utils::boosted_tcp_server> net_server; struct network_zone; - using connect_func = boost::optional(network_zone&, epee::net_utils::network_address const&, epee::net_utils::ssl_support_t); + using connect_func = boost::optional(network_zone&, p2p_address const&); struct config_t { @@ -165,6 +206,7 @@ namespace nodetool m_bind_ipv6_address(), m_port(), m_port_ipv6(), + m_ssl_finger(), m_notifier(), m_our_address(), m_peerlist(), @@ -172,6 +214,7 @@ namespace nodetool m_proxy_address(), m_current_number_of_out_peers(0), m_current_number_of_in_peers(0), + m_ssl_support(epee::net_utils::ssl_support_t::e_ssl_support_disabled), m_seed_nodes_lock(), m_can_pingback(false), m_seed_nodes_initialized(false) @@ -187,6 +230,7 @@ namespace nodetool m_bind_ipv6_address(), m_port(), m_port_ipv6(), + m_ssl_finger(), m_notifier(), m_our_address(), m_peerlist(), @@ -194,6 +238,7 @@ namespace nodetool m_proxy_address(), m_current_number_of_out_peers(0), m_current_number_of_in_peers(0), + m_ssl_support(epee::net_utils::ssl_support_t::e_ssl_support_disabled), m_seed_nodes_lock(), m_can_pingback(false), m_seed_nodes_initialized(false) @@ -203,11 +248,12 @@ namespace nodetool connect_func* m_connect; net_server m_net_server; - std::vector m_seed_nodes; + std::vector m_seed_nodes; std::string m_bind_ip; std::string m_bind_ipv6_address; std::string m_port; std::string m_port_ipv6; + std::string m_ssl_finger; cryptonote::levin::notify m_notifier; epee::net_utils::network_address m_our_address; // in anonymity networks peerlist_manager m_peerlist; @@ -215,6 +261,7 @@ namespace nodetool boost::asio::ip::tcp::endpoint m_proxy_address; std::atomic m_current_number_of_out_peers; std::atomic m_current_number_of_in_peers; + epee::net_utils::ssl_support_t m_ssl_support; boost::shared_mutex m_seed_nodes_lock; bool m_can_pingback; bool m_seed_nodes_initialized; @@ -370,7 +417,7 @@ namespace nodetool bool make_new_connection_from_anchor_peerlist(const std::vector& anchor_peerlist); bool make_new_connection_from_peerlist(network_zone& zone, bool use_white_list); - bool try_to_connect_and_handshake_with_new_peer(const epee::net_utils::network_address& na, bool just_take_peerlist = false, uint64_t last_seen_stamp = 0, PeerType peer_type = white, uint64_t first_seen_stamp = 0); + bool try_to_connect_and_handshake_with_new_peer(const p2p_address& peer, bool just_take_peerlist = false, uint64_t last_seen_stamp = 0, PeerType peer_type = white, uint64_t first_seen_stamp = 0); size_t get_random_index_with_fixed_probability(size_t max_index); bool is_peer_used(const peerlist_entry& peer); bool is_peer_used(const anchor_peerlist_entry& peer); @@ -390,9 +437,9 @@ namespace nodetool void record_addr_failed(const epee::net_utils::network_address& addr); bool is_addr_recently_failed(const epee::net_utils::network_address& addr); bool is_priority_node(const epee::net_utils::network_address& na); - std::set get_ip_seed_nodes() const; - std::set get_dns_seed_nodes(); - std::set get_seed_nodes(epee::net_utils::zone); + std::set get_ip_seed_nodes() const; + std::set get_dns_seed_nodes(); + std::set get_seed_nodes(epee::net_utils::zone); bool connect_to_seed(epee::net_utils::zone); template @@ -415,7 +462,7 @@ namespace nodetool size_t get_outgoing_connections_count(); size_t get_outgoing_connections_count(network_zone&); - bool check_connection_and_handshake_with_peer(const epee::net_utils::network_address& na, uint64_t last_seen_stamp); + bool check_connection_and_handshake_with_peer(const p2p_address& peer, uint64_t last_seen_stamp); bool gray_peerlist_housekeeping(); bool check_incoming_connections(); @@ -474,16 +521,16 @@ namespace nodetool epee::math_helper::once_a_time_seconds<3600, false> m_incoming_connections_interval; epee::math_helper::once_a_time_seconds<7000> m_dns_blocklist_interval; - std::list m_priority_peers; - std::vector m_exclusive_peers; + std::list m_priority_peers; + std::vector m_exclusive_peers; std::atomic_flag m_fallback_seed_nodes_added; std::vector m_command_line_peers; uint64_t m_peer_livetime; //keep connections to initiate some interactions - static boost::optional public_connect(network_zone&, epee::net_utils::network_address const&, epee::net_utils::ssl_support_t); - static boost::optional socks_connect(network_zone&, epee::net_utils::network_address const&, epee::net_utils::ssl_support_t); + static boost::optional public_connect(network_zone&, p2p_address const&); + static boost::optional socks_connect(network_zone&, p2p_address const&); /* A `std::map` provides constant iterators and key/value pointers even with @@ -510,8 +557,6 @@ namespace nodetool boost::uuids::uuid m_network_id; cryptonote::network_type m_nettype; - epee::net_utils::ssl_support_t m_ssl_support; - bool m_enable_dns_seed_nodes; bool m_enable_dns_blocklist; @@ -538,6 +583,8 @@ namespace nodetool extern const command_line::arg_descriptor arg_p2p_hide_my_port; extern const command_line::arg_descriptor arg_no_sync; extern const command_line::arg_descriptor arg_enable_dns_blocklist; + extern const command_line::arg_descriptor arg_p2p_disable_encryption; + extern const command_line::arg_descriptor arg_p2p_persistent_cert; extern const command_line::arg_descriptor arg_no_igd; extern const command_line::arg_descriptor arg_igd; diff --git a/src/p2p/net_node.inl b/src/p2p/net_node.inl index 505ba026e..3f8f35524 100644 --- a/src/p2p/net_node.inl +++ b/src/p2p/net_node.inl @@ -50,6 +50,7 @@ #include "common/util.h" #include "common/dns_utils.h" #include "common/pruning.h" +#include "hex.h" #include "net/error.h" #include "misc_log_ex.h" #include "p2p_protocol_defs.h" @@ -80,6 +81,19 @@ static inline boost::asio::ip::address_v4 make_address_v4_from_v6(const boost::a namespace nodetool { + static string_address parse_address(std::string adr) + { + namespace net = epee::net_utils; + const auto pos = adr.find(','); + net::ssl_options_t e2e_mode{net::ssl_support_t::e_ssl_support_autodetect}; + if (boost::string_ref{adr}.ends_with("no_encryption")) + e2e_mode = net::ssl_options_t{net::ssl_support_t::e_ssl_support_disabled}; + else if (pos != std::string::npos) + e2e_mode = net::ssl_options_t{{epee::from_hex_locale::to_vector(boost::string_ref{adr}.substr(pos + 1))}, {}}; + adr.erase(std::min(adr.size(), adr.find(','))); + return {std::move(adr) , std::move(e2e_mode)}; + } + template node_server::~node_server() { @@ -94,7 +108,7 @@ namespace nodetool } } //----------------------------------------------------------------------------------- - inline bool append_net_address(std::vector & seed_nodes, std::string const & addr, uint16_t default_port); + inline bool append_net_address(std::vector & seed_nodes, string_address addr, uint16_t default_port); //----------------------------------------------------------------------------------- template void node_server::init_options(boost::program_options::options_description& desc) @@ -117,6 +131,8 @@ namespace nodetool command_line::add_arg(desc, arg_p2p_hide_my_port); command_line::add_arg(desc, arg_no_sync); command_line::add_arg(desc, arg_enable_dns_blocklist); + command_line::add_arg(desc, arg_p2p_disable_encryption); + command_line::add_arg(desc, arg_p2p_persistent_cert); command_line::add_arg(desc, arg_no_igd); command_line::add_arg(desc, arg_igd); command_line::add_arg(desc, arg_out_peers); @@ -407,7 +423,6 @@ namespace nodetool { bool testnet = command_line::get_arg(vm, cryptonote::arg_testnet_on); bool stagenet = command_line::get_arg(vm, cryptonote::arg_stagenet_on); - const bool pad_txs = command_line::get_arg(vm, arg_pad_transactions); m_nettype = testnet ? cryptonote::TESTNET : stagenet ? cryptonote::STAGENET : cryptonote::MAINNET; network_zone& public_zone = m_network_zones[epee::net_utils::zone::public_]; @@ -451,6 +466,8 @@ namespace nodetool m_offline = command_line::get_arg(vm, cryptonote::arg_offline); m_use_ipv6 = command_line::get_arg(vm, arg_p2p_use_ipv6); m_require_ipv4 = !command_line::get_arg(vm, arg_p2p_ignore_ipv4); + const bool pad_txs = + !command_line::get_arg(vm, arg_p2p_disable_encryption) || command_line::get_arg(vm, arg_pad_transactions); public_zone.m_notifier = cryptonote::levin::notify{ public_zone.m_net_server.get_io_service(), public_zone.m_net_server.get_config_shared(), nullptr, epee::net_utils::zone::public_, pad_txs, m_payload_handler.get_core() }; @@ -463,11 +480,13 @@ namespace nodetool nodetool::peerlist_entry pe = AUTO_VAL_INIT(pe); pe.id = crypto::rand(); const uint16_t default_port = cryptonote::get_config(m_nettype).P2P_DEFAULT_PORT; - expect adr = net::get_network_address(pr_str, default_port); + string_address str_addr = parse_address(pr_str); + expect adr = net::get_network_address(str_addr.address, default_port); if (adr) { add_zone(adr->get_zone()); - pe.adr = std::move(*adr); + p2p_address p2p_peer{std::move(*adr), std::move(str_addr.ssl)}; + p2p_peer.update_peer(pe); m_command_line_peers.push_back(std::move(pe)); continue; } @@ -475,13 +494,13 @@ namespace nodetool adr == net::error::unsupported_address, false, "Bad address (\"" << pr_str << "\"): " << adr.error().message() ); - std::vector resolved_addrs; - bool r = append_net_address(resolved_addrs, pr_str, default_port); + std::vector resolved_addrs; + bool r = append_net_address(resolved_addrs, str_addr, default_port); CHECK_AND_ASSERT_MES(r, false, "Failed to parse or resolve address from string: " << pr_str); - for (const epee::net_utils::network_address& addr : resolved_addrs) + for (const p2p_address& peer : resolved_addrs) { pe.id = crypto::rand(); - pe.adr = addr; + peer.update_peer(pe); m_command_line_peers.push_back(pe); } } @@ -605,7 +624,7 @@ namespace nodetool } zone.m_notifier = cryptonote::levin::notify{ - zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise), proxy.zone, pad_txs, m_payload_handler.get_core() + zone.m_net_server.get_io_service(), zone.m_net_server.get_config_shared(), std::move(this_noise), proxy.zone, true /* pad txs */, m_payload_handler.get_core() }; } @@ -654,8 +673,8 @@ namespace nodetool } //----------------------------------------------------------------------------------- inline bool append_net_address( - std::vector & seed_nodes - , std::string const & addr + std::vector & seed_nodes + , string_address peer , uint16_t default_port ) { @@ -664,7 +683,7 @@ namespace nodetool // Split addr string into host string and port string std::string host; std::string port = std::to_string(default_port); - net::get_network_address_host_and_port(addr, host, port); + net::get_network_address_host_and_port(peer.address, host, port); MINFO("Resolving node address: host=" << host << ", port=" << port); io_service io_srv; @@ -681,13 +700,13 @@ namespace nodetool if (endpoint.address().is_v4()) { epee::net_utils::network_address na{epee::net_utils::ipv4_network_address{boost::asio::detail::socket_ops::host_to_network_long(endpoint.address().to_v4().to_ulong()), endpoint.port()}}; - seed_nodes.push_back(na); + seed_nodes.push_back(p2p_address{na, peer.ssl}); MINFO("Added node: " << na.str()); } else { epee::net_utils::network_address na{epee::net_utils::ipv6_network_address{endpoint.address().to_v6(), endpoint.port()}}; - seed_nodes.push_back(na); + seed_nodes.push_back(p2p_address{na, peer.ssl}); MINFO("Added node: " << na.str()); } } @@ -696,43 +715,44 @@ namespace nodetool //----------------------------------------------------------------------------------- template - std::set node_server::get_ip_seed_nodes() const + std::set node_server::get_ip_seed_nodes() const { - std::set full_addrs; + const auto autodetect = epee::net_utils::ssl_support_t::e_ssl_support_autodetect; + std::set full_addrs; if (m_nettype == cryptonote::TESTNET) { - full_addrs.insert("176.9.0.187:28080"); - full_addrs.insert("51.79.173.165:28080"); - full_addrs.insert("192.99.8.110:28080"); - full_addrs.insert("37.187.74.171:28080"); - full_addrs.insert("77.172.183.193:28080"); + full_addrs.insert(string_address{"176.9.0.187:28080", autodetect}); + full_addrs.insert(string_address{"51.79.173.165:28080", autodetect}); + full_addrs.insert(string_address{"192.99.8.110:28080", autodetect}); + full_addrs.insert(string_address{"37.187.74.171:28080", autodetect}); + full_addrs.insert(string_address{"77.172.183.193:28080", autodetect}); } else if (m_nettype == cryptonote::STAGENET) { - full_addrs.insert("176.9.0.187:38080"); - full_addrs.insert("51.79.173.165:38080"); - full_addrs.insert("192.99.8.110:38080"); - full_addrs.insert("37.187.74.171:38080"); - full_addrs.insert("77.172.183.193:38080"); + full_addrs.insert(string_address{"176.9.0.187:38080", autodetect}); + full_addrs.insert(string_address{"51.79.173.165:38080", autodetect}); + full_addrs.insert(string_address{"192.99.8.110:38080", autodetect}); + full_addrs.insert(string_address{"37.187.74.171:38080", autodetect}); + full_addrs.insert(string_address{"77.172.183.193:38080", autodetect}); } else if (m_nettype == cryptonote::FAKECHAIN) { } else { - full_addrs.insert("176.9.0.187:18080"); - full_addrs.insert("88.198.163.90:18080"); - full_addrs.insert("66.85.74.134:18080"); - full_addrs.insert("51.79.173.165:18080"); - full_addrs.insert("192.99.8.110:18080"); - full_addrs.insert("37.187.74.171:18080"); - full_addrs.insert("77.172.183.193:18080"); + full_addrs.insert(string_address{"176.9.0.187:18080", autodetect}); + full_addrs.insert(string_address{"88.198.163.90:18080", autodetect}); + full_addrs.insert(string_address{"66.85.74.134:18080", autodetect}); + full_addrs.insert(string_address{"51.79.173.165:18080", autodetect}); + full_addrs.insert(string_address{"192.99.8.110:18080", autodetect}); + full_addrs.insert(string_address{"37.187.74.171:18080", autodetect}); + full_addrs.insert(string_address{"77.172.183.193:18080", autodetect}); } return full_addrs; } //----------------------------------------------------------------------------------- template - std::set node_server::get_dns_seed_nodes() + std::set node_server::get_dns_seed_nodes() { if (!m_exclusive_peers.empty() || m_offline) { @@ -753,7 +773,7 @@ namespace nodetool return get_ip_seed_nodes(); } - std::set full_addrs; + std::set full_addrs; // for each hostname in the seed nodes list, attempt to DNS resolve and // add the result addresses as seed nodes @@ -825,7 +845,7 @@ namespace nodetool if (result.size()) { for (const auto& addr_string : result) - full_addrs.insert(addr_string + ":" + std::to_string(cryptonote::get_config(m_nettype).P2P_DEFAULT_PORT)); + full_addrs.insert(string_address{addr_string + ":" + std::to_string(cryptonote::get_config(m_nettype).P2P_DEFAULT_PORT), epee::net_utils::ssl_support_t::e_ssl_support_disabled}); } ++i; } @@ -847,8 +867,10 @@ namespace nodetool } //----------------------------------------------------------------------------------- template - std::set node_server::get_seed_nodes(epee::net_utils::zone zone) + std::set node_server::get_seed_nodes(epee::net_utils::zone zone) { + // `.onion` and `.i2p` are already end-to-end encrypted + const auto no_encryption = epee::net_utils::ssl_support_t::e_ssl_support_disabled; switch (zone) { case epee::net_utils::zone::public_: @@ -856,23 +878,25 @@ namespace nodetool case epee::net_utils::zone::tor: if (m_nettype == cryptonote::MAINNET) { + // tor hidden services are already e2e ssl encrypted return { - "zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083", - "qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion:18083", - "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18083", - "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18083", - "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18083", - "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18083", + string_address{"zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.onion:18083", no_encryption}, + string_address{"qz43zul2x56jexzoqgkx2trzwcfnr6l3hbtfcfx54g4r3eahy3bssjyd.onion:18083", no_encryption}, + string_address{"plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18083", no_encryption}, + string_address{"plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18083", no_encryption}, + string_address{"plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18083", no_encryption}, + string_address{"aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18083", no_encryption}, }; } return {}; case epee::net_utils::zone::i2p: if (m_nettype == cryptonote::MAINNET) { + // i2p services are already e2e aes noise encrypted return { - "uqj3aphckqtjsitz7kxx5flqpwjlq5ppr3chazfued7xucv3nheq.b32.i2p", - "vdmnehdjkpkg57nthgnjfuaqgku673r5bpbqg56ix6fyqoywgqrq.b32.i2p", - "ugnlcdciyhghh2zert7c3kl4biwkirc43ke33jiy5slnd3mv2trq.b32.i2p", + string_address{"uqj3aphckqtjsitz7kxx5flqpwjlq5ppr3chazfued7xucv3nheq.b32.i2p", no_encryption}, + string_address{"vdmnehdjkpkg57nthgnjfuaqgku673r5bpbqg56ix6fyqoywgqrq.b32.i2p", no_encryption}, + string_address{"ugnlcdciyhghh2zert7c3kl4biwkirc43ke33jiy5slnd3mv2trq.b32.i2p", no_encryption} }; } return {}; @@ -956,7 +980,6 @@ namespace nodetool return res; //try to bind - m_ssl_support = epee::net_utils::ssl_support_t::e_ssl_support_disabled; for (auto& zone : m_network_zones) { zone.second.m_net_server.get_config_object().set_handler(this); @@ -974,8 +997,39 @@ namespace nodetool ipv6_port = zone.second.m_port_ipv6; MINFO("Binding (IPv6) on " << zone.second.m_bind_ipv6_address << ":" << zone.second.m_port_ipv6); } - res = zone.second.m_net_server.init_server(zone.second.m_port, zone.second.m_bind_ip, ipv6_port, ipv6_addr, m_use_ipv6, m_require_ipv4, epee::net_utils::ssl_support_t::e_ssl_support_disabled); + + namespace net = epee::net_utils; + bool store_cert = false; + bool share_fingerprint = false; + net::ssl_options_t p2p_ssl{net::ssl_support_t::e_ssl_support_disabled}; + const auto key_files = m_config_folder + "/p2p_public"; + if (zone.first == epee::net_utils::zone::public_ && !command_line::get_arg(vm, arg_p2p_disable_encryption)) + { + p2p_ssl = net::ssl_options_t{net::ssl_support_t::e_ssl_support_autodetect}; + if (command_line::get_arg(vm, arg_p2p_persistent_cert)) + { + share_fingerprint = true; + p2p_ssl = net::ssl_options_t{net::ssl_support_t::e_ssl_support_enabled}; + MINFO("Using persistent SSL certificate for p2p"); + if (boost::filesystem::exists(key_files + ".key") && boost::filesystem::exists(key_files + ".crt")) + { + p2p_ssl.auth.private_key_path = key_files + ".key"; + p2p_ssl.auth.certificate_path = key_files + ".crt"; + } + else + store_cert = true; + } + } + else + MINFO("Generating new temporary SSL certificate for p2p"); + + zone.second.m_ssl_support = p2p_ssl.support; + res = zone.second.m_net_server.init_server(zone.second.m_port, zone.second.m_bind_ip, ipv6_port, ipv6_addr, m_use_ipv6, m_require_ipv4, std::move(p2p_ssl)); CHECK_AND_ASSERT_MES(res, false, "Failed to bind server"); + if (store_cert) + net::store_ssl_keys(zone.second.m_net_server.get_ssl_context(), key_files); + if (share_fingerprint) + zone.second.m_ssl_finger = net::get_ssl_fingerprint(zone.second.m_net_server.get_ssl_context()); } } @@ -1138,7 +1192,6 @@ namespace nodetool bool node_server::do_handshake_with_peer(peerid_type& pi, p2p_connection_context& context_, bool just_take_peerlist) { network_zone& zone = m_network_zones.at(context_.m_remote_address.get_zone()); - typename COMMAND_HANDSHAKE::request arg; typename COMMAND_HANDSHAKE::response rsp; get_local_node_data(arg.node_data, zone); @@ -1187,6 +1240,8 @@ namespace nodetool context.m_rpc_port = rsp.node_data.rpc_port; context.m_rpc_credits_per_hash = rsp.node_data.rpc_credits_per_hash; context.support_flags = rsp.node_data.support_flags; + context.encryption_mode = rsp.node_data.encryption_mode; + context.cert_finger = std::move(rsp.node_data.cert_finger); const auto azone = context.m_remote_address.get_zone(); network_zone& zone = m_network_zones.at(azone); zone.m_peerlist.set_peer_just_seen(rsp.node_data.peer_id, context.m_remote_address, context.m_pruning_seed, context.m_rpc_port, context.m_rpc_credits_per_hash); @@ -1363,13 +1418,13 @@ namespace nodetool } while(0) template - bool node_server::try_to_connect_and_handshake_with_new_peer(const epee::net_utils::network_address& na, bool just_take_peerlist, uint64_t last_seen_stamp, PeerType peer_type, uint64_t first_seen_stamp) + bool node_server::try_to_connect_and_handshake_with_new_peer(const p2p_address& peer, bool just_take_peerlist, uint64_t last_seen_stamp, PeerType peer_type, uint64_t first_seen_stamp) { - network_zone& zone = m_network_zones.at(na.get_zone()); + network_zone& zone = m_network_zones.at(peer.na.get_zone()); if (zone.m_connect == nullptr) // outgoing connections in zone not possible return false; - if (zone.m_our_address == na) + if (zone.m_our_address == peer.na) return false; if (zone.m_current_number_of_out_peers == zone.m_config.m_net_config.max_out_connection_count) // out peers limit @@ -1384,17 +1439,17 @@ namespace nodetool } - MDEBUG("Connecting to " << na.str() << "(peer_type=" << peer_type << ", last_seen: " + MDEBUG("Connecting to " << peer.na.str() << "(peer_type=" << peer_type << ", last_seen: " << (last_seen_stamp ? epee::misc_utils::get_time_interval_string(time(NULL) - last_seen_stamp):"never") << ")..."); - auto con = zone.m_connect(zone, na, m_ssl_support); + auto con = zone.m_connect(zone, peer); if(!con) { - bool is_priority = is_priority_node(na); - LOG_PRINT_CC_PRIORITY_NODE(is_priority, bool(con), "Connect failed to " << na.str() + bool is_priority = is_priority_node(peer.na); + LOG_PRINT_CC_PRIORITY_NODE(is_priority, bool(con), "Connect failed to " << peer.na.str() /*<< ", try " << try_count*/); - record_addr_failed(na); + record_addr_failed(peer.na); return false; } @@ -1404,11 +1459,11 @@ namespace nodetool if(!res) { - bool is_priority = is_priority_node(na); + bool is_priority = is_priority_node(peer.na); LOG_PRINT_CC_PRIORITY_NODE(is_priority, *con, "Failed to HANDSHAKE with peer " - << na.str() + << peer.na.str() /*<< ", try " << try_count*/); - record_addr_failed(na); + record_addr_failed(peer.na); return false; } @@ -1420,7 +1475,7 @@ namespace nodetool } peerlist_entry pe_local = AUTO_VAL_INIT(pe_local); - pe_local.adr = na; + pe_local.adr = peer.na; pe_local.id = pi; time_t last_seen; time(&last_seen); @@ -1428,13 +1483,17 @@ namespace nodetool pe_local.pruning_seed = con->m_pruning_seed; pe_local.rpc_port = con->m_rpc_port; pe_local.rpc_credits_per_hash = con->m_rpc_credits_per_hash; + pe_local.encryption_mode = con->encryption_mode; + pe_local.cert_finger = con->cert_finger; zone.m_peerlist.append_with_peer_white(pe_local); //update last seen and push it to peerlist manager anchor_peerlist_entry ape = AUTO_VAL_INIT(ape); - ape.adr = na; + ape.adr = peer.na; ape.id = pi; ape.first_seen = first_seen_stamp ? first_seen_stamp : time(nullptr); + ape.encryption_mode = con->encryption_mode; + ape.cert_finger = con->cert_finger; zone.m_peerlist.append_with_peer_anchor(ape); zone.m_notifier.on_handshake_complete(con->m_connection_id, con->m_is_income); @@ -1445,22 +1504,22 @@ namespace nodetool } template - bool node_server::check_connection_and_handshake_with_peer(const epee::net_utils::network_address& na, uint64_t last_seen_stamp) + bool node_server::check_connection_and_handshake_with_peer(const p2p_address& peer, uint64_t last_seen_stamp) { - network_zone& zone = m_network_zones.at(na.get_zone()); + network_zone& zone = m_network_zones.at(peer.na.get_zone()); if (zone.m_connect == nullptr) return false; - LOG_PRINT_L1("Connecting to " << na.str() << "(last_seen: " + LOG_PRINT_L1("Connecting to " << peer.na.str() << "(last_seen: " << (last_seen_stamp ? epee::misc_utils::get_time_interval_string(time(NULL) - last_seen_stamp):"never") << ")..."); - auto con = zone.m_connect(zone, na, m_ssl_support); + auto con = zone.m_connect(zone, peer); if (!con) { - bool is_priority = is_priority_node(na); + bool is_priority = is_priority_node(peer.na); - LOG_PRINT_CC_PRIORITY_NODE(is_priority, p2p_connection_context{}, "Connect failed to " << na.str()); - record_addr_failed(na); + LOG_PRINT_CC_PRIORITY_NODE(is_priority, p2p_connection_context{}, "Connect failed to " << peer.na.str()); + record_addr_failed(peer.na); return false; } @@ -1469,10 +1528,10 @@ namespace nodetool peerid_type pi = AUTO_VAL_INIT(pi); const bool res = do_handshake_with_peer(pi, *con, true); if (!res) { - bool is_priority = is_priority_node(na); + bool is_priority = is_priority_node(peer.na); - LOG_PRINT_CC_PRIORITY_NODE(is_priority, *con, "Failed to HANDSHAKE with peer " << na.str()); - record_addr_failed(na); + LOG_PRINT_CC_PRIORITY_NODE(is_priority, *con, "Failed to HANDSHAKE with peer " << peer.na.str()); + record_addr_failed(peer.na); return false; } @@ -1530,7 +1589,7 @@ namespace nodetool << "[peer_type=" << anchor << "] first_seen: " << epee::misc_utils::get_time_interval_string(time(NULL) - pe.first_seen)); - if(!try_to_connect_and_handshake_with_new_peer(pe.adr, false, 0, anchor, pe.first_seen)) { + if(!try_to_connect_and_handshake_with_new_peer({pe.adr, get_p2p_encryption(pe)}, false, 0, anchor, pe.first_seen)) { _note("Handshake failed"); continue; } @@ -1719,7 +1778,7 @@ namespace nodetool << "[peer_list=" << (use_white_list ? white : gray) << "] last_seen: " << (pe.last_seen ? epee::misc_utils::get_time_interval_string(time(NULL) - pe.last_seen) : "never")); - if(!try_to_connect_and_handshake_with_new_peer(pe.adr, false, pe.last_seen, use_white_list ? white : gray)) { + if(!try_to_connect_and_handshake_with_new_peer({pe.adr, get_p2p_encryption(pe)}, false, pe.last_seen, use_white_list ? white : gray)) { _note("Handshake failed"); continue; } @@ -1743,8 +1802,8 @@ namespace nodetool for (const auto& full_addr : get_seed_nodes(zone)) { // seeds should have hostname converted to IP already - MDEBUG("Seed node: " << full_addr); - server.m_seed_nodes.push_back(MONERO_UNWRAP(net::get_network_address(full_addr, default_port))); + MDEBUG("Seed node: " << full_addr.address); + server.m_seed_nodes.push_back(p2p_address{MONERO_UNWRAP(net::get_network_address(full_addr.address, default_port)), full_addr.ssl}); } MDEBUG("Number of seed nodes: " << server.m_seed_nodes.size()); } @@ -1761,7 +1820,7 @@ namespace nodetool return false; peerlist_entry pe_seed{}; - pe_seed.adr = server.m_seed_nodes[current_index]; + pe_seed.adr = server.m_seed_nodes[current_index].na; if (is_peer_used(pe_seed)) is_connected_to_at_least_one_seed_node = true; else if (try_to_connect_and_handshake_with_new_peer(server.m_seed_nodes[current_index], true)) @@ -1778,7 +1837,7 @@ namespace nodetool for (const auto &peer: get_ip_seed_nodes()) { - MDEBUG("Fallback seed node: " << peer); + MDEBUG("Fallback seed node: " << peer.address); append_net_address(server.m_seed_nodes, peer, cryptonote::get_config(m_nettype).P2P_DEFAULT_PORT); } } @@ -2206,6 +2265,8 @@ namespace nodetool node_data.rpc_credits_per_hash = zone.m_can_pingback ? m_rpc_credits_per_hash : 0; node_data.network_id = m_network_id; node_data.support_flags = zone.m_config.m_support_flags; + node_data.encryption_mode = get_encryption_mode(zone.m_ssl_support); + node_data.cert_finger = zone.m_ssl_finger; return true; } //----------------------------------------------------------------------------------- @@ -2225,7 +2286,14 @@ namespace nodetool template bool node_server::relay_notify_to_list(int command, epee::levin::message_writer data_buff, std::vector> connections) { - epee::byte_slice message = data_buff.finalize_notify(command); + /* This is typically used for relaying blocks, and conveniently uses the + same ref-counted buffer for every connection. Unfortunately, this means + every connection is sent a message of identical size, which should be + identifiable by a ISP/level spy. The alternatives are less efficient (a + uniquely padded message foreach link). Defending against this type of + analysis is likely always complicated, as each message type will have + unique */ + epee::byte_slice message = data_buff.finalize_notify(command, false); std::sort(connections.begin(), connections.end()); auto zone = m_network_zones.begin(); for(const auto& c_id: connections) @@ -2317,7 +2385,7 @@ namespace nodetool return false; network_zone& zone = m_network_zones.at(context.m_remote_address.get_zone()); - int res = zone.m_net_server.get_config_object().send(message.finalize_notify(command), context.m_connection_id); + int res = zone.m_net_server.get_config_object().send(message.finalize_notify(command, context.should_pad()), context.m_connection_id); return res > 0; } //----------------------------------------------------------------------------------- @@ -2371,6 +2439,7 @@ namespace nodetool { address = epee::net_utils::network_address{epee::net_utils::ipv6_network_address(ipv6_addr, node_data.my_port)}; } + peerid_type pr = node_data.peer_id; bool r = zone.m_net_server.connect_async(ip, port, zone.m_config.m_net_config.ping_connection_timeout, [cb, /*context,*/ address, pr, this]( const typename net_server::t_connection_context& ping_context, @@ -2421,7 +2490,7 @@ namespace nodetool return false; } return true; - }, "0.0.0.0", m_ssl_support); + }, "0.0.0.0", get_p2p_encryption(node_data)); if(!r) { LOG_WARNING_CC(context, "Failed to call connect_async, network error."); @@ -2498,7 +2567,7 @@ namespace nodetool real peer with that identity banned/blacklisted. */ if(outgoing_to_same_zone) - rsp.local_peerlist_new.push_back(peerlist_entry{zone.m_our_address, zone.m_config.m_peer_id, std::time(nullptr)}); + rsp.local_peerlist_new.push_back(peerlist_entry{zone.m_our_address, {}, zone.m_config.m_peer_id, std::time(nullptr)}); LOG_DEBUG_CC(context, "COMMAND_TIMED_SYNC"); return 1; @@ -2578,7 +2647,7 @@ namespace nodetool peerid_type peer_id_l = arg.node_data.peer_id; uint32_t port_l = arg.node_data.my_port; //try ping to be sure that we can add this peer to peer_list - try_ping(arg.node_data, context, [peer_id_l, port_l, context, this]() + try_ping(arg.node_data, context, [arg, peer_id_l, port_l, context, this]() { CHECK_AND_ASSERT_MES((context.m_remote_address.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id() || context.m_remote_address.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id()), void(), "Only IPv4 or IPv6 addresses are supported here"); @@ -2600,6 +2669,8 @@ namespace nodetool pe.pruning_seed = context.m_pruning_seed; pe.rpc_port = context.m_rpc_port; pe.rpc_credits_per_hash = context.m_rpc_credits_per_hash; + pe.encryption_mode = arg.node_data.encryption_mode; + pe.cert_finger = arg.node_data.cert_finger; this->m_network_zones.at(context.m_remote_address.get_zone()).m_peerlist.append_with_peer_white(pe); LOG_DEBUG_CC(context, "PING SUCCESS " << context.m_remote_address.host_str() << ":" << port_l); }); @@ -2696,22 +2767,22 @@ namespace nodetool template bool node_server::is_priority_node(const epee::net_utils::network_address& na) { - return (std::find(m_priority_peers.begin(), m_priority_peers.end(), na) != m_priority_peers.end()) || (std::find(m_exclusive_peers.begin(), m_exclusive_peers.end(), na) != m_exclusive_peers.end()); + return (std::find(m_priority_peers.begin(), m_priority_peers.end(), p2p_address{na}) != m_priority_peers.end()) || (std::find(m_exclusive_peers.begin(), m_exclusive_peers.end(), p2p_address{na}) != m_exclusive_peers.end()); } template template bool node_server::connect_to_peerlist(const Container& peers) { const network_zone& public_zone = m_network_zones.at(epee::net_utils::zone::public_); - for(const epee::net_utils::network_address& na: peers) + for(const p2p_address& peer: peers) { if(public_zone.m_net_server.is_stop_signal_sent()) return false; - if(is_addr_connected(na)) + if(is_addr_connected(peer.na)) continue; - try_to_connect_and_handshake_with_new_peer(na); + try_to_connect_and_handshake_with_new_peer(peer); } return true; @@ -2725,19 +2796,20 @@ namespace nodetool for(const std::string& pr_str: perrs) { const uint16_t default_port = cryptonote::get_config(m_nettype).P2P_DEFAULT_PORT; - expect adr = net::get_network_address(pr_str, default_port); + auto peer = parse_address(pr_str); + expect adr = net::get_network_address(peer.address, default_port); if (adr) { add_zone(adr->get_zone()); - container.push_back(std::move(*adr)); + container.push_back(p2p_address{std::move(*adr), std::move(peer.ssl)}); continue; } - std::vector resolved_addrs; - bool r = append_net_address(resolved_addrs, pr_str, default_port); + std::vector resolved_addrs; + bool r = append_net_address(resolved_addrs, peer, default_port); CHECK_AND_ASSERT_MES(r, false, "Failed to parse or resolve address from string: " << pr_str); - for (const epee::net_utils::network_address& addr : resolved_addrs) + for (p2p_address& addr : resolved_addrs) { - container.push_back(addr); + container.push_back(std::move(addr)); } } @@ -2916,8 +2988,8 @@ namespace nodetool peerlist_entry pe{}; if (!zone.second.m_peerlist.get_random_gray_peer(pe)) continue; - - if (!check_connection_and_handshake_with_peer(pe.adr, pe.last_seen)) + + if (!check_connection_and_handshake_with_peer({pe.adr, get_p2p_encryption(pe)}, pe.last_seen)) { 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)); @@ -3102,13 +3174,13 @@ namespace nodetool template boost::optional> - node_server::socks_connect(network_zone& zone, const epee::net_utils::network_address& remote, epee::net_utils::ssl_support_t ssl_support) + node_server::socks_connect(network_zone& zone, const p2p_address& peer) { - auto result = socks_connect_internal(zone.m_net_server.get_stop_signal(), zone.m_net_server.get_io_service(), zone.m_proxy_address, remote); + auto result = socks_connect_internal(zone.m_net_server.get_stop_signal(), zone.m_net_server.get_io_service(), zone.m_proxy_address, peer.na); if (result) // if no error { p2p_connection_context context{}; - if (zone.m_net_server.add_connection(context, std::move(*result), remote, ssl_support)) + if (zone.m_net_server.add_connection(context, std::move(*result), peer.na, epee::net_utils::ssl_support_t::e_ssl_support_disabled)) return {std::move(context)}; } return boost::none; @@ -3116,10 +3188,10 @@ namespace nodetool template boost::optional> - node_server::public_connect(network_zone& zone, epee::net_utils::network_address const& na, epee::net_utils::ssl_support_t ssl_support) + node_server::public_connect(network_zone& zone, const p2p_address& peer) { - bool is_ipv4 = na.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id(); - bool is_ipv6 = na.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id(); + bool is_ipv4 = peer.na.get_type_id() == epee::net_utils::ipv4_network_address::get_type_id(); + bool is_ipv6 = peer.na.get_type_id() == epee::net_utils::ipv6_network_address::get_type_id(); CHECK_AND_ASSERT_MES(is_ipv4 || is_ipv6, boost::none, "Only IPv4 or IPv6 addresses are supported here"); @@ -3128,13 +3200,13 @@ namespace nodetool if (is_ipv4) { - const epee::net_utils::ipv4_network_address &ipv4 = na.as(); + const epee::net_utils::ipv4_network_address &ipv4 = peer.na.as(); address = epee::string_tools::get_ip_string_from_int32(ipv4.ip()); port = epee::string_tools::num_to_string_fast(ipv4.port()); } else if (is_ipv6) { - const epee::net_utils::ipv6_network_address &ipv6 = na.as(); + const epee::net_utils::ipv6_network_address &ipv6 = peer.na.as(); address = ipv6.ip().to_string(); port = epee::string_tools::num_to_string_fast(ipv6.port()); } @@ -3144,10 +3216,14 @@ namespace nodetool return boost::none; } + const auto ssl = + zone.m_ssl_support == epee::net_utils::ssl_support_t::e_ssl_support_disabled ? + zone.m_ssl_support : peer.ssl; + typename net_server::t_connection_context con{}; const bool res = zone.m_net_server.connect(address, port, zone.m_config.m_net_config.connection_timeout, - con, "0.0.0.0", ssl_support); + con, "0.0.0.0", ssl); if (res) return {std::move(con)}; diff --git a/src/p2p/net_peerlist.h b/src/p2p/net_peerlist.h index c2f700598..36869bd9c 100644 --- a/src/p2p/net_peerlist.h +++ b/src/p2p/net_peerlist.h @@ -396,20 +396,30 @@ namespace nodetool //update gray list auto by_addr_it_gr = m_peers_gray.get().find(ple.adr); + + peerlist_entry new_ple = ple; + if(by_addr_it_gr == m_peers_gray.get().end()) { - //put new record into white list - m_peers_gray.insert(ple); + // Do not trust encryption information from 3rd parties + new_ple.encryption_mode = emode_ssl_autodetect; + new_ple.cert_finger.clear(); + + m_peers_gray.insert(new_ple); trim_gray_peerlist(); }else { //update record in gray list - peerlist_entry new_ple = ple; if (by_addr_it_gr->pruning_seed && ple.pruning_seed == 0) // guard against older nodes not passing pruning info around new_ple.pruning_seed = by_addr_it_gr->pruning_seed; if (by_addr_it_gr->rpc_port && ple.rpc_port == 0) // guard against older nodes not passing RPC port around new_ple.rpc_port = by_addr_it_gr->rpc_port; new_ple.last_seen = by_addr_it_gr->last_seen; // do not overwrite the last seen timestamp, incoming peer list are untrusted + + // use existing values for encryption on replace + new_ple.encryption_mode = by_addr_it_gr->encryption_mode; + new_ple.cert_finger = by_addr_it_gr->cert_finger; + m_peers_gray.replace(by_addr_it_gr, new_ple); } return true; @@ -420,7 +430,6 @@ namespace nodetool bool peerlist_manager::append_with_peer_anchor(const anchor_peerlist_entry& ple) { TRY_ENTRY(); - CRITICAL_REGION_LOCAL(m_peerlist_lock); auto by_addr_it_anchor = m_peers_anchor.get().find(ple.adr); diff --git a/src/p2p/net_peerlist_boost_serialization.h b/src/p2p/net_peerlist_boost_serialization.h index 0b7a54860..e9c66457f 100644 --- a/src/p2p/net_peerlist_boost_serialization.h +++ b/src/p2p/net_peerlist_boost_serialization.h @@ -38,7 +38,8 @@ #include "net/i2p_address.h" #include "p2p/p2p_protocol_defs.h" -BOOST_CLASS_VERSION(nodetool::peerlist_entry, 3) +BOOST_CLASS_VERSION(nodetool::peerlist_entry, 4) +BOOST_CLASS_VERSION(nodetool::anchor_peerlist_entry, 1) namespace boost { @@ -238,6 +239,18 @@ namespace boost return; } a & pl.rpc_credits_per_hash; + + if (ver < 4) + { + if (!typename Archive::is_saving()) + { + pl.encryption_mode = nodetool::emode_ssl_autodetect; + pl.cert_finger.clear(); + } + return; + } + a & pl.encryption_mode; + a & pl.cert_finger; } template @@ -246,6 +259,17 @@ namespace boost a & pl.adr; a & pl.id; a & pl.first_seen; + if (ver < 1) + { + if (!typename Archive::is_saving()) + { + pl.encryption_mode = nodetool::emode_ssl_autodetect; + pl.cert_finger.clear(); + } + return; + } + a & pl.encryption_mode; + a & pl.cert_finger; } } } diff --git a/src/p2p/p2p_protocol_defs.h b/src/p2p/p2p_protocol_defs.h index 18b853d35..4173b3cef 100644 --- a/src/p2p/p2p_protocol_defs.h +++ b/src/p2p/p2p_protocol_defs.h @@ -33,10 +33,12 @@ #include #include #include +#include #include "serialization/keyvalue_serialization.h" #include "net/net_utils_base.h" #include "net/tor_address.h" // needed for serialization #include "net/i2p_address.h" // needed for serialization +#include "net/net_ssl.h" #include "misc_language.h" #include "string_tools.h" #include "time_helper.h" @@ -48,6 +50,11 @@ namespace nodetool typedef boost::uuids::uuid uuid; typedef uint64_t peerid_type; + // The current modes/versions for p2p encryption + constexpr uint8_t emode_ssl_autodetect = 0; + constexpr uint8_t emode_disabled = 1; + constexpr uint8_t emode_ssl_enabled = 2; + static inline std::string peerid_to_string(peerid_type peer_id) { std::ostringstream s; @@ -55,6 +62,48 @@ namespace nodetool return epee::string_tools::pad_string(s.str(), 16, '0', true); } + inline uint8_t get_encryption_mode(const epee::net_utils::ssl_support_t support) + { + switch (support) + { + case epee::net_utils::ssl_support_t::e_ssl_support_enabled: + return emode_ssl_enabled; + case epee::net_utils::ssl_support_t::e_ssl_support_disabled: + return emode_disabled; + default: + case epee::net_utils::ssl_support_t::e_ssl_support_autodetect: + break; + } + return emode_ssl_autodetect; + } + + template + inline epee::net_utils::ssl_options_t get_p2p_encryption(const T& peer) + { + namespace net = epee::net_utils; + switch (peer.encryption_mode) + { + default: + case emode_ssl_autodetect: + return net::ssl_support_t::e_ssl_support_autodetect; + case emode_disabled: + return net::ssl_support_t::e_ssl_support_disabled; + case emode_ssl_enabled: + break; + } + + if (peer.cert_finger.empty()) + { + // SSL is required, but no verification + net::ssl_options_t out = net::ssl_support_t::e_ssl_support_enabled; + out.verification = net::ssl_verification_t::none; + return out; + } + // Fingerprint specified, require SSL + std::vector> fingers{net::convert_fingerprint(peer.cert_finger)}; + return {std::move(fingers), {}}; + } + #pragma pack (push, 1) struct network_address_old @@ -72,11 +121,13 @@ namespace nodetool struct peerlist_entry_base { AddressType adr; + std::string cert_finger; peerid_type id; int64_t last_seen; uint32_t pruning_seed; uint16_t rpc_port; uint32_t rpc_credits_per_hash; + uint8_t encryption_mode; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(adr) @@ -85,16 +136,9 @@ namespace nodetool KV_SERIALIZE_OPT(pruning_seed, (uint32_t)0) KV_SERIALIZE_OPT(rpc_port, (uint16_t)0) KV_SERIALIZE_OPT(rpc_credits_per_hash, (uint32_t)0) + KV_SERIALIZE_OPT(encryption_mode, emode_ssl_autodetect) + KV_SERIALIZE_OPT(cert_finger, std::string{}) END_KV_SERIALIZE_MAP() - - BEGIN_SERIALIZE() - FIELD(adr) - FIELD(id) - VARINT_FIELD(last_seen) - VARINT_FIELD(pruning_seed) - VARINT_FIELD(rpc_port) - VARINT_FIELD(rpc_credits_per_hash) - END_SERIALIZE() }; typedef peerlist_entry_base peerlist_entry; @@ -102,44 +146,21 @@ namespace nodetool struct anchor_peerlist_entry_base { AddressType adr; + std::string cert_finger; peerid_type id; int64_t first_seen; + uint8_t encryption_mode; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(adr) KV_SERIALIZE(id) KV_SERIALIZE(first_seen) + KV_SERIALIZE_OPT(encryption_mode, emode_ssl_autodetect) + KV_SERIALIZE_OPT(cert_finger, std::string{}) END_KV_SERIALIZE_MAP() - - BEGIN_SERIALIZE() - FIELD(adr) - FIELD(id) - VARINT_FIELD(first_seen) - END_SERIALIZE() }; typedef anchor_peerlist_entry_base anchor_peerlist_entry; - template - struct connection_entry_base - { - AddressType adr; - peerid_type id; - bool is_income; - - BEGIN_KV_SERIALIZE_MAP() - KV_SERIALIZE(adr) - KV_SERIALIZE(id) - KV_SERIALIZE(is_income) - END_KV_SERIALIZE_MAP() - - BEGIN_SERIALIZE() - FIELD(adr) - FIELD(id) - FIELD(is_income) - END_SERIALIZE() - }; - typedef connection_entry_base connection_entry; - #pragma pack(pop) inline @@ -184,12 +205,14 @@ namespace nodetool struct basic_node_data { - uuid network_id; + std::string cert_finger; + uuid network_id; uint32_t my_port; uint16_t rpc_port; uint32_t rpc_credits_per_hash; peerid_type peer_id; uint32_t support_flags; + uint8_t encryption_mode; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_VAL_POD_AS_BLOB(network_id) @@ -198,6 +221,8 @@ namespace nodetool KV_SERIALIZE_OPT(rpc_port, (uint16_t)(0)) KV_SERIALIZE_OPT(rpc_credits_per_hash, (uint32_t)0) KV_SERIALIZE_OPT(support_flags, (uint32_t)0) + KV_SERIALIZE_OPT(encryption_mode, emode_ssl_autodetect) + KV_SERIALIZE_OPT(cert_finger, std::string{}) END_KV_SERIALIZE_MAP() }; diff --git a/tests/functional_tests/functional_tests_rpc.py b/tests/functional_tests/functional_tests_rpc.py index e483352a4..6e3620392 100755 --- a/tests/functional_tests/functional_tests_rpc.py +++ b/tests/functional_tests/functional_tests_rpc.py @@ -56,9 +56,9 @@ monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", st monerod_extra = [ ["--offline"], ["--rpc-payment-address", "44SKxxLQw929wRF6BA9paQ1EWFshNnKhXM3qz6Mo3JGDE2YG3xyzVutMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUiqhcwR", "--rpc-payment-difficulty", str(DIFFICULTY), "--rpc-payment-credits", "5000", "--offline"], - ["--add-exclusive-node", "127.0.0.1:18283"], - ["--add-exclusive-node", "127.0.0.1:18282"], - ["--rpc-login", "md5_lover:Z1ON0101", "--offline"], + ["--add-exclusive-node", "127.0.0.1:18283", "--add-exclusive-node", "127.0.0.1:18284", "--p2p-disable-encryption"], + ["--add-exclusive-node", "127.0.0.1:18282", "--add-exclusive-node", "127.0.0.1:18284"], + ["--rpc-login", "md5_lover:Z1ON0101", "--add-exclusive-node", "127.0,0,1:18282,no_encryption", "--add-exclusive-node", "127.0.0.1:18283"], ] wallet_base = [builddir + "/bin/monero-wallet-rpc", "--wallet-dir", WALLET_DIRECTORY, "--rpc-bind-port", "wallet_port", "--rpc-ssl", "disabled", "--daemon-ssl", "disabled", "--log-level", "1", "--allow-mismatched-daemon-version"] wallet_extra = [ diff --git a/tests/functional_tests/p2p.py b/tests/functional_tests/p2p.py index 2c582cc8a..f0467d2c8 100755 --- a/tests/functional_tests/p2p.py +++ b/tests/functional_tests/p2p.py @@ -45,6 +45,7 @@ class P2PTest(): self.mine(80) self.test_p2p_reorg() self.test_p2p_tx_propagation() + self.test_p2p_ssl() def reset(self): print('Resetting blockchain') @@ -78,6 +79,8 @@ class P2PTest(): daemon3 = Daemon(idx = 3) # give sync some time + daemon2.stop_mining() + daemon3.stop_mining() time.sleep(1) res = daemon2.get_info() @@ -168,6 +171,8 @@ class P2PTest(): res = daemon.get_transaction_pool_hashes() assert not 'tx_hashes' in res or len(res.tx_hashes) == 0 + daemon2.stop_mining() + daemon3.stop_mining() self.wallet.refresh() res = self.wallet.get_balance() @@ -176,13 +181,36 @@ class P2PTest(): assert len(res.tx_hash) == 32*2 txid = res.tx_hash - time.sleep(5) + time.sleep(45) for daemon in [daemon2, daemon3]: res = daemon.get_transaction_pool_hashes() assert len(res.tx_hashes) == 1 assert res.tx_hashes[0] == txid + def test_p2p_ssl(self): + print('Testing P2P SSL') + daemon2 = Daemon(idx = 2) + daemon3 = Daemon(idx = 3) + daemon4 = Daemon(idx = 4, username="md5_lover", password="Z1ON0101") + + connections = daemon2.get_connections().connections + for connection in connections: + assert connection.ssl == 0 + + connections = daemon3.get_connections().connections + for connection in connections: + if connection.port == "18282": + assert connection.ssl == 0 + elif connection.port == "18284": + assert connection.ssl == 2 + + connections = daemon4.get_connections().connections + for connection in connections: + if connection.port == "18282": + assert connection.ssl == 0 + elif connection.port == "18283": + assert connection.ssl == 2 if __name__ == '__main__': P2PTest().run_test() diff --git a/tests/net_load_tests/net_load_tests.h b/tests/net_load_tests/net_load_tests.h index 67d5605d7..854e96701 100644 --- a/tests/net_load_tests/net_load_tests.h +++ b/tests/net_load_tests/net_load_tests.h @@ -47,7 +47,7 @@ namespace net_load_tests { struct test_connection_context : epee::net_utils::connection_context_base { - test_connection_context(): epee::net_utils::connection_context_base(boost::uuids::nil_uuid(), {}, false, false), m_closed(false) {} + test_connection_context(): epee::net_utils::connection_context_base(boost::uuids::nil_uuid(), {}, false, epee::net_utils::ssl_support_t::e_ssl_support_disabled), m_closed(false) {} static constexpr int handshake_command() noexcept { return 1001; } static constexpr bool handshake_complete() noexcept { return true; } size_t get_max_bytes(int command) const { return LEVIN_DEFAULT_MAX_PACKET_SIZE; } diff --git a/tests/unit_tests/epee_levin_protocol_handler_async.cpp b/tests/unit_tests/epee_levin_protocol_handler_async.cpp index 05cc6412f..9cf6b7663 100644 --- a/tests/unit_tests/epee_levin_protocol_handler_async.cpp +++ b/tests/unit_tests/epee_levin_protocol_handler_async.cpp @@ -372,14 +372,14 @@ TEST_F(positive_test_connection_to_levin_protocol_handler_calls, handler_process epee::levin::bucket_head2 resp_head; ASSERT_LT(sizeof(resp_head), send_data.size()); std::memcpy(std::addressof(resp_head), send_data.data(), sizeof(resp_head)); - std::string out_data = send_data.substr(sizeof(resp_head)); + std::string out_data = send_data.substr(sizeof(resp_head), expected_out_data.size()); // Check sent response ASSERT_EQ(expected_out_data, out_data); ASSERT_EQ(LEVIN_SIGNATURE, SWAP64LE(resp_head.m_signature)); ASSERT_EQ(expected_command, SWAP32LE(resp_head.m_command)); ASSERT_EQ(expected_return_code, SWAP32LE(resp_head.m_return_code)); - ASSERT_EQ(expected_out_data.size(), SWAP64LE(resp_head.m_cb)); + ASSERT_LE(expected_out_data.size(), SWAP64LE(resp_head.m_cb)); ASSERT_FALSE(resp_head.m_have_to_return_data); ASSERT_EQ(SWAP32LE(LEVIN_PROTOCOL_VER_1), resp_head.m_protocol_version); ASSERT_TRUE(0 != (SWAP32LE(resp_head.m_flags) & LEVIN_PACKET_RESPONSE)); @@ -438,7 +438,7 @@ TEST_F(positive_test_connection_to_levin_protocol_handler_calls, handler_process message.buffer.write(epee::to_span(in_data)); const epee::byte_slice noise = epee::levin::make_noise_notify(1024); - const epee::byte_slice notify = message.finalize_notify(expected_command); + const epee::byte_slice notify = message.finalize_notify(expected_command, false); test_connection_ptr conn = create_connection(); @@ -479,7 +479,7 @@ TEST_F(positive_test_connection_to_levin_protocol_handler_calls, handler_process in_fragmented_data.buffer.put_n('c', 1024 * 4); const epee::byte_slice noise = epee::levin::make_noise_notify(1024); - const epee::byte_slice notify = message.finalize_notify(expected_command); + const epee::byte_slice notify = message.finalize_notify(expected_command, false); epee::byte_slice fragmented = epee::levin::make_fragmented_notify(noise.size(), expected_fragmented_command, std::move(in_fragmented_data)); EXPECT_EQ(5u, fragmented.size() / 1024); diff --git a/tests/unit_tests/levin.cpp b/tests/unit_tests/levin.cpp index d686df87d..ed1ec3ea6 100644 --- a/tests/unit_tests/levin.cpp +++ b/tests/unit_tests/levin.cpp @@ -177,7 +177,7 @@ namespace handler_(std::addressof(endpoint_), connections, context_) { using base_type = epee::net_utils::connection_context_base; - static_cast(context_) = base_type{random_generator(), {}, is_incoming, false}; + static_cast(context_) = base_type{random_generator(), {}, is_incoming, epee::net_utils::ssl_support_t::e_ssl_support_disabled}; context_.m_state = cryptonote::cryptonote_connection_context::state_normal; handler_.after_init_connection(); } @@ -397,7 +397,7 @@ TEST(make_header, expect_return) TEST(message_writer, invoke_with_empty_payload) { - const epee::byte_slice message = epee::levin::message_writer{}.finalize_invoke(443); + const epee::byte_slice message = epee::levin::message_writer{}.finalize_invoke(443, false); const epee::levin::bucket_head2 header = epee::levin::make_header(443, 0, LEVIN_PACKET_REQUEST, true); ASSERT_EQ(sizeof(header), message.size()); @@ -412,7 +412,7 @@ TEST(message_writer, invoke_with_payload) epee::levin::message_writer writer{}; writer.buffer.write(epee::to_span(bytes)); - const epee::byte_slice message = writer.finalize_invoke(443); + const epee::byte_slice message = writer.finalize_invoke(443, false); const epee::levin::bucket_head2 header = epee::levin::make_header(443, bytes.size(), LEVIN_PACKET_REQUEST, true); @@ -423,7 +423,7 @@ TEST(message_writer, invoke_with_payload) TEST(message_writer, notify_with_empty_payload) { - const epee::byte_slice message = epee::levin::message_writer{}.finalize_notify(443); + const epee::byte_slice message = epee::levin::message_writer{}.finalize_notify(443, false); const epee::levin::bucket_head2 header = epee::levin::make_header(443, 0, LEVIN_PACKET_REQUEST, false); ASSERT_EQ(sizeof(header), message.size()); @@ -438,7 +438,7 @@ TEST(message_writer, notify_with_payload) epee::levin::message_writer writer{}; writer.buffer.write(epee::to_span(bytes)); - const epee::byte_slice message = writer.finalize_notify(443); + const epee::byte_slice message = writer.finalize_notify(443, false); const epee::levin::bucket_head2 header = epee::levin::make_header(443, bytes.size(), LEVIN_PACKET_REQUEST, false); @@ -449,7 +449,7 @@ TEST(message_writer, notify_with_payload) TEST(message_writer, response_with_empty_payload) { - const epee::byte_slice message = epee::levin::message_writer{}.finalize_response(443, 1); + const epee::byte_slice message = epee::levin::message_writer{}.finalize_response(443, 1, false); epee::levin::bucket_head2 header = epee::levin::make_header(443, 0, LEVIN_PACKET_RESPONSE, false); header.m_return_code = SWAP32LE(1); @@ -465,7 +465,7 @@ TEST(message_writer, response_with_payload) epee::levin::message_writer writer{}; writer.buffer.write(epee::to_span(bytes)); - const epee::byte_slice message = writer.finalize_response(443, 6450); + const epee::byte_slice message = writer.finalize_response(443, 6450, false); epee::levin::bucket_head2 header = epee::levin::make_header(443, bytes.size(), LEVIN_PACKET_RESPONSE, false); header.m_return_code = SWAP32LE(6450); @@ -480,9 +480,9 @@ TEST(message_writer, error) epee::levin::message_writer writer{}; writer.buffer.clear(); - EXPECT_THROW(writer.finalize_invoke(0), std::runtime_error); - EXPECT_THROW(writer.finalize_notify(0), std::runtime_error); - EXPECT_THROW(writer.finalize_response(0, 0), std::runtime_error); + EXPECT_THROW(writer.finalize_invoke(0, false), std::runtime_error); + EXPECT_THROW(writer.finalize_notify(0, false), std::runtime_error); + EXPECT_THROW(writer.finalize_response(0, 0, false), std::runtime_error); } TEST(make_noise, invalid) @@ -1068,7 +1068,7 @@ TEST_F(levin_notify, fluff_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(notification.dandelionpp_fluff); } } @@ -1133,7 +1133,7 @@ TEST_F(levin_notify, stem_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); } @@ -1196,7 +1196,7 @@ TEST_F(levin_notify, stem_no_outs_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(sorted_txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(notification.dandelionpp_fluff); } } @@ -1265,7 +1265,7 @@ TEST_F(levin_notify, local_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(their_txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); } @@ -1294,7 +1294,7 @@ TEST_F(levin_notify, local_with_padding) EXPECT_EQ(1u, receiver_.notified_size()); auto notification = receiver_.get_notification().second; EXPECT_EQ(my_txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(!notification.dandelionpp_fluff); has_stemmed |= is_stem; @@ -1362,7 +1362,7 @@ TEST_F(levin_notify, forward_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_EQ(!is_stem, notification.dandelionpp_fluff); } @@ -1755,7 +1755,7 @@ TEST_F(levin_notify, private_fluff_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(notification.dandelionpp_fluff); } } @@ -1807,7 +1807,7 @@ TEST_F(levin_notify, private_stem_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(notification.dandelionpp_fluff); } } @@ -1859,7 +1859,7 @@ TEST_F(levin_notify, private_local_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(notification.dandelionpp_fluff); } } @@ -1911,7 +1911,7 @@ TEST_F(levin_notify, private_forward_with_padding) { auto notification = receiver_.get_notification().second; EXPECT_EQ(txs, notification.txs); - EXPECT_FALSE(notification._.empty()); + EXPECT_TRUE(notification._.empty()); EXPECT_TRUE(notification.dandelionpp_fluff); } } @@ -2385,7 +2385,7 @@ TEST_F(levin_notify, command_max_bytes) { epee::levin::message_writer dest{}; dest.buffer.write(epee::to_span(payload)); - bytes = dest.finalize_notify(ping_command); + bytes = dest.finalize_notify(ping_command, false); } EXPECT_EQ(1, get_connections().send(bytes.clone(), contexts_.front().get_id())); @@ -2401,7 +2401,7 @@ TEST_F(levin_notify, command_max_bytes) payload.push_back('h'); epee::levin::message_writer dest{}; dest.buffer.write(epee::to_span(payload)); - bytes = dest.finalize_notify(ping_command); + bytes = dest.finalize_notify(ping_command, true); } EXPECT_EQ(1, get_connections().send(std::move(bytes), contexts_.front().get_id())); diff --git a/tests/unit_tests/node_server.cpp b/tests/unit_tests/node_server.cpp index 6f490d7b9..5e01d66f7 100644 --- a/tests/unit_tests/node_server.cpp +++ b/tests/unit_tests/node_server.cpp @@ -530,14 +530,14 @@ TEST(cryptonote_protocol_handler, race_condition) } virtual bool invoke_notify_to_peer(int command, epee::levin::message_writer in, const contexts::basic& context) override { if (shared_state) - return shared_state->send(in.finalize_notify(command), context.m_connection_id); + return shared_state->send(in.finalize_notify(command, true), context.m_connection_id); else return {}; } virtual bool relay_notify_to_list(int command, epee::levin::message_writer in, connections_t connections) override { if (shared_state) { for (auto &e: connections) - shared_state->send(in.finalize_notify(command), e.second); + shared_state->send(in.finalize_notify(command, true), e.second); } return {}; } @@ -1090,6 +1090,7 @@ TEST(node_server, race_condition) conn->get_context(context); event_t handshaked; typename messages::handshake::request_t msg{{ + std::string{}, ::config::NETWORK_ID, 58080, }}; diff --git a/tests/unit_tests/test_peerlist.cpp b/tests/unit_tests/test_peerlist.cpp index c5343b757..4627cb931 100644 --- a/tests/unit_tests/test_peerlist.cpp +++ b/tests/unit_tests/test_peerlist.cpp @@ -111,13 +111,13 @@ TEST(peerlist_storage, store) std::string buffer{}; { nodetool::peerlist_types types{}; - types.white.push_back({epee::net_utils::ipv4_network_address{1000, 10}, 44, 55}); - types.white.push_back({net::tor_address::unknown(), 64, 75}); - types.gray.push_back({net::tor_address::unknown(), 99, 88}); - types.gray.push_back({epee::net_utils::ipv4_network_address{2000, 20}, 84, 45}); - types.anchor.push_back({epee::net_utils::ipv4_network_address{999, 654}, 444, 555}); - types.anchor.push_back({net::tor_address::unknown(), 14, 33}); - types.anchor.push_back({net::tor_address::unknown(), 24, 22}); + types.white.push_back({epee::net_utils::ipv4_network_address{1000, 10}, {}, 44, 55}); + types.white.push_back({net::tor_address::unknown(), {}, 64, 75}); + types.gray.push_back({net::tor_address::unknown(), {}, 99, 88}); + types.gray.push_back({epee::net_utils::ipv4_network_address{2000, 20}, {}, 84, 45}); + types.anchor.push_back({epee::net_utils::ipv4_network_address{999, 654}, {}, 444, 555}); + types.anchor.push_back({net::tor_address::unknown(), {}, 14, 33}); + types.anchor.push_back({net::tor_address::unknown(), {}, 24, 22}); std::ostringstream stream{}; EXPECT_TRUE(peers.store(stream, types));