diff --git a/contrib/epee/include/net/abstract_tcp_server2.h b/contrib/epee/include/net/abstract_tcp_server2.h index 4bf4b29c89b..2b24dffb467 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.h +++ b/contrib/epee/include/net/abstract_tcp_server2.h @@ -319,8 +319,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 boost::asio::bind_executor(m_strand, std::forward(f)); + } private: //----------------- i_service_endpoint --------------------- @@ -401,10 +412,10 @@ 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, std::chrono::milliseconds conn_timeout, 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, t_connection_context&& initial = t_connection_context{}); + bool connect_async(const std::string& adr, const std::string& port, std::chrono::milliseconds 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, t_connection_context&& initial = t_connection_context{}); boost::asio::ssl::context& get_ssl_context() noexcept { @@ -503,6 +514,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, std::chrono::milliseconds conn_timeout, const t_callback &cb); + + bool remove_connection(const connection_ptr& ptr); + const std::shared_ptr::shared_state> m_state; /// The io_context 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 4b8d9ffc5ac..def9a9e8174 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.inl +++ b/contrib/epee/include/net/abstract_tcp_server2.inl @@ -982,7 +982,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 */ } @@ -1709,7 +1709,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(); @@ -1785,7 +1785,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) { @@ -1799,6 +1799,7 @@ namespace net_utils sock_.close(); return CONNECT_FAILURE; } + new_connection_l->set_ssl_enabled(); } return CONNECT_SUCCESS; @@ -1807,11 +1808,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_context_, m_state, m_connection_type, ssl_support) ); + connection_ptr new_connection_l(new connection(io_context_, m_state, m_connection_type, ssl_options.support) ); connections_mutex.lock(); connections_.insert(new_connection_l); MDEBUG("connections_ size now " << connections_.size()); @@ -1899,24 +1900,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); @@ -1935,10 +1934,10 @@ namespace net_utils } //--------------------------------------------------------------------------------- template template - bool boosted_tcp_server::connect_async(const std::string& adr, const std::string& port, const std::chrono::milliseconds conn_timeout, const t_callback &cb, const std::string& bind_ip, epee::net_utils::ssl_support_t ssl_support, t_connection_context&& initial) + bool boosted_tcp_server::connect_async(const std::string& adr, const std::string& port, const std::chrono::milliseconds conn_timeout, const t_callback &cb, const std::string& bind_ip, epee::net_utils::ssl_options_t ssl_options, t_connection_context&& initial) { TRY_ENTRY(); - connection_ptr new_connection_l(new connection(io_context_, m_state, m_connection_type, ssl_support, std::move(initial)) ); + connection_ptr new_connection_l(new connection(io_context_, m_state, m_connection_type, ssl_options.support, std::move(initial)) ); connections_mutex.lock(); connections_.insert(new_connection_l); MDEBUG("connections_ size now " << connections_.size()); @@ -2015,60 +2014,117 @@ namespace net_utils return false; } } - - std::shared_ptr sh_deadline(std::make_shared(io_context_)); - //start deadline - sh_deadline->expires_after(conn_timeout); - sh_deadline->async_wait([=](const boost::system::error_code& error) + + 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, const std::chrono::milliseconds 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 " << adr << ':' << port << ", because of timeout (" << conn_timeout.count() << ")"); - new_connection_l->socket().close(); - } - }); - //start async connect - sock_.async_connect(remote_endpoint, [=](const boost::system::error_code& ec_) + if (error != boost::asio::error::operation_aborted) + { + _dbg3("Failed to connect to " << remote_endpoint << ", because of timeout (" << conn_timeout.count() << " ms)"); + new_connection_l->socket().close(error); // ignore errors + } + }; + + auto sh_deadline = std::make_shared(io_context_); + sh_deadline->expires_after(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_) { - 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); + 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 - 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) + const auto on_ready = [=] () { - new_connection_l->get_context(conn_context); - cb(conn_context, ec_); - } - else + 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_after(conn_timeout)) { - _dbg3("[sock " << new_connection_l->socket().native_handle() << "] Failed to start connection to " << adr << ':' << port); - cb(conn_context, boost::asio::error::fault); + 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) + { + sh_deadline->cancel(); + 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 + { + new_connection_l->set_ssl_enabled(); + on_ready(); + } + })); } + else // if timer already expired + on_cancel(boost::asio::error::operation_aborted); } - }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_); + 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; - CATCH_ENTRY_L0("boosted_tcp_server::connect_async", false); } - } // namespace } // namespace diff --git a/contrib/epee/include/net/connection_basic.hpp b/contrib/epee/include/net/connection_basic.hpp index 7993060de6d..0eb45d38047 100644 --- a/contrib/epee/include/net/connection_basic.hpp +++ b/contrib/epee/include/net/connection_basic.hpp @@ -131,10 +131,9 @@ 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) { - //m_state != nullptr verified in constructor - return m_state->ssl_options().handshake(strand_.context(), socket_, type, buffer); + return ssl.handshake(strand_.context(), socket_, boost::asio::ssl::stream_base::client); } template diff --git a/contrib/epee/include/net/levin_base.h b/contrib/epee/include/net/levin_base.h index 9dc1035afd7..48969d403b0 100644 --- a/contrib/epee/include/net/levin_base.h +++ b/contrib/epee/include/net/levin_base.h @@ -131,7 +131,7 @@ constexpr const std::chrono::milliseconds LEVIN_DEFAULT_TIMEOUT_PRECONFIGURED{0} //! 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; @@ -148,12 +148,13 @@ constexpr const std::chrono::milliseconds LEVIN_DEFAULT_TIMEOUT_PRECONFIGURED{0} { 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-8191 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 1c05f31d99d..46238b6e4ef 100644 --- a/contrib/epee/include/net/levin_protocol_handler_async.h +++ b/contrib/epee/include/net/levin_protocol_handler_async.h @@ -534,7 +534,7 @@ class async_protocol_handler 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 @@ -628,7 +628,7 @@ class async_protocol_handler 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 5fd14b65da5..3cc981f6740 100644 --- a/contrib/epee/include/net/net_ssl.h +++ b/contrib/epee/include/net/net_ssl.h @@ -47,7 +47,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, }; @@ -108,6 +108,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; @@ -154,6 +157,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 88b50f60213..d890a012fde 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" @@ -369,7 +371,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; @@ -380,7 +382,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), @@ -402,7 +404,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), @@ -423,11 +425,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 0f5c358132c..5dda06d27ad 100644 --- a/contrib/epee/include/storages/levin_abstract_invoke2.h +++ b/contrib/epee/include/storages/levin_abstract_invoke2.h @@ -111,7 +111,7 @@ 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); + int res = transport.send(to_send.finalize_notify(command, context.should_pad()), 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 bfde1e2e56c..f6cdf238e44 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 963e33fb2a8..22f54bb0e46 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 c07f0065a04..499fa977f3b 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 @@ -529,18 +530,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() || MONERO_HOSTNAME_VERIFY(host)(preverified, ctx)); + (this_copy.verification != ssl_verification_t::system_ca || host.empty() || MONERO_HOSTNAME_VERIFY(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; @@ -652,6 +654,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 2a5dacb8455..85dd766b9b4 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_mode = 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 e264354ceda..1bf9268c25e 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -161,6 +161,7 @@ #define P2P_DEFAULT_LIMIT_RATE_DOWN 32768 // kB/s #define P2P_REQUEST_FAILURE_THRESHOLD_PERCENTAGE 70 // if more than 70% of requests fail, the peer is dropped #define P2P_MIN_SAMPLE_SIZE_FOR_DROPPING 5 // minimum number of requests to consider dropping a peer for failed requests +#define P2P_MAX_LEVIN_PAD_BYTES 8192 #define P2P_FAILED_ADDR_FORGET_SECONDS (60) //1 minute #define P2P_IP_BLOCKTIME (60*2) //2 minutes diff --git a/src/cryptonote_protocol/cryptonote_protocol_defs.h b/src/cryptonote_protocol/cryptonote_protocol_defs.h index 4907f0476cf..4516ff00b1e 100644 --- a/src/cryptonote_protocol/cryptonote_protocol_defs.h +++ b/src/cryptonote_protocol/cryptonote_protocol_defs.h @@ -36,6 +36,7 @@ #include "cryptonote_basic/blobdatatype.h" #include "byte_slice.h" #include "storages/portable_storage_template_helper.h" +#include "net/net_ssl.h" namespace cryptonote { @@ -51,7 +52,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; @@ -89,9 +90,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) @@ -114,6 +117,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 6294f450812..4d5cfa76817 100644 --- a/src/cryptonote_protocol/levin_notify.cpp +++ b/src/cryptonote_protocol/levin_notify.cpp @@ -162,17 +162,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) - { - pad_tx_request(request); - } - 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"}; @@ -194,14 +189,14 @@ 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); } // TODO: do we want to pad to avoid revealing definitive n txs in the req? - bool make_payload_send_txs_relay_v2(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination) + bool make_payload_send_txs_relay_v2(connections& p2p, std::vector&& txs, const boost::uuids::uuid& destination, const bool pad) { - epee::byte_slice blob = make_tx_hash_message(std::move(txs)).finalize_notify(NOTIFY_TX_POOL_HASH::ID); + epee::byte_slice blob = make_tx_hash_message(std::move(txs)).finalize_notify(NOTIFY_TX_POOL_HASH::ID, pad); return p2p.send(std::move(blob), destination); } @@ -408,7 +403,7 @@ namespace levin return std::memcmp(&lhs, &rhs, sizeof(lhs)) < 0; }); connection.first.erase(unique(connection.first.begin(), connection.first.end()), connection.first.end()); - make_payload_send_txs_relay_v2(*zone_->p2p, std::move(connection.first), connection.second); + make_payload_send_txs_relay_v2(*zone_->p2p, std::move(connection.first), connection.second, zone_->pad_txs); } for (auto& connection : connections) @@ -880,7 +875,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 1e840a27b4e..84a468235c9 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -649,7 +649,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)" @@ -660,6 +660,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) { @@ -670,7 +684,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 7e35450584b..b97552b67ca 100644 --- a/src/p2p/net_node.cpp +++ b/src/p2p/net_node.cpp @@ -149,17 +149,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 \",\" 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 \",\" 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 \",\" 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 \",\" 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: ,[socks5://[user:pass@]][,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)", "disabled"}; @@ -174,7 +180,7 @@ 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 p2p connections allowed from the same IP address", 10}; diff --git a/src/p2p/net_node.h b/src/p2p/net_node.h index 9eef3385249..ffd60b71fde 100644 --- a/src/p2p/net_node.h +++ b/src/p2p/net_node.h @@ -110,19 +110,60 @@ namespace nodetool struct p2p_connection_context_t: base_type //t_payload_net_handler::connection_context //public net_utils::connection_context_base { explicit p2p_connection_context_t(bool is_ping = false) - : peer_id(0), + : cert_finger(), + peer_id(0), support_flags(0), + encryption_mode(emode_ssl_autodetect), is_ping(is_ping), m_in_timedsync(false) {} + std::string cert_finger; peerid_type peer_id; uint32_t support_flags; + uint8_t encryption_mode; bool m_in_timedsync; bool is_ping; 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, @@ -142,7 +183,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 { @@ -168,6 +209,7 @@ namespace nodetool m_bind_ipv6_address(), m_port(), m_port_ipv6(), + m_ssl_finger(), m_notifier(), m_our_address(), m_peerlist(), @@ -175,6 +217,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) @@ -190,6 +233,7 @@ namespace nodetool m_bind_ipv6_address(), m_port(), m_port_ipv6(), + m_ssl_finger(), m_notifier(), m_our_address(), m_peerlist(), @@ -197,6 +241,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) @@ -206,11 +251,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; @@ -218,6 +264,7 @@ namespace nodetool net::socks::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; @@ -368,7 +415,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); @@ -380,9 +427,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 @@ -405,7 +452,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(); @@ -463,16 +510,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::atomic_flag m_fallback_seed_nodes_added; + std::list m_priority_peers; + std::vector m_exclusive_peers; + std::atomic_flag m_fallback_seed_nodes_added = ATOMIC_FLAG_INIT; 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 @@ -499,8 +546,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; @@ -527,6 +572,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 dc412e1c8c3..72a560615ec 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" @@ -77,6 +78,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() { @@ -91,7 +105,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) @@ -114,6 +128,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); @@ -425,7 +441,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); bool regtest = command_line::get_arg(vm, cryptonote::arg_regtest_on); - const bool pad_txs = command_line::get_arg(vm, arg_pad_transactions); m_nettype = testnet ? cryptonote::TESTNET : stagenet ? cryptonote::STAGENET : regtest ? cryptonote::FAKECHAIN : cryptonote::MAINNET; network_zone& public_zone = m_network_zones[epee::net_utils::zone::public_]; @@ -444,6 +459,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_context(), public_zone.m_net_server.get_config_shared(), nullptr, epee::net_utils::zone::public_, pad_txs, m_payload_handler.get_core() }; @@ -456,11 +473,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; } @@ -468,13 +487,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); } } @@ -608,7 +627,7 @@ namespace nodetool } zone.m_notifier = cryptonote::levin::notify{ - zone.m_net_server.get_io_context(), 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_context(), zone.m_net_server.get_config_shared(), std::move(this_noise), proxy.zone, true /* pad txs */, m_payload_handler.get_core() }; } @@ -657,8 +676,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 ) { @@ -667,7 +686,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); boost::system::error_code ec; @@ -682,13 +701,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_uint()), 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()); } } @@ -697,39 +716,40 @@ 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("185.141.216.177:28180"); - full_addrs.insert("208.123.187.228:28080"); + full_addrs.insert(string_address{"185.141.216.177:28180", autodetect}); + full_addrs.insert(string_address{"208.123.187.228:28080", autodetect}); } else if (m_nettype == cryptonote::STAGENET) { - full_addrs.insert("176.9.0.187:38080"); - full_addrs.insert("192.99.8.110:38080"); - full_addrs.insert("37.187.74.171:38080"); - full_addrs.insert("88.99.195.15:38080"); - full_addrs.insert("5.104.84.64:38080"); + full_addrs.insert(string_address{"176.9.0.187: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{"88.99.195.15:38080", autodetect}); + full_addrs.insert(string_address{"5.104.84.64: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("192.99.8.110:18080"); - full_addrs.insert("37.187.74.171:18080"); - full_addrs.insert("88.99.195.15:18080"); - full_addrs.insert("5.104.84.64: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{"192.99.8.110:18080", autodetect}); + full_addrs.insert(string_address{"37.187.74.171:18080", autodetect}); + full_addrs.insert(string_address{"88.99.195.15:18080", autodetect}); + full_addrs.insert(string_address{"5.104.84.64: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) { @@ -754,7 +774,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 @@ -826,7 +846,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; } @@ -848,8 +868,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_: @@ -857,29 +879,31 @@ 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", - "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion:18083", - "plowsoffjexmxalw73tkjmf422gq6575fc7vicuu4javzn2ynnte6tyd.onion:18083", - "plowsofe6cleftfmk2raiw5h2x66atrik3nja4bfd3zrfa2hdlgworad.onion:18083", - "aclc4e2jhhtr44guufbnwk5bzwhaecinax4yip4wr4tjn27sjsfg6zqd.onion:18083", - "lykcas4tus7mkm4bhsgqe4drtd4awi7gja24goscc47xfgzj54yofyqd.onion:18083", + string_address{"zbjkbsxc5munw3qusl7j2hpcmikhqocdf4pqhnhtpzw5nt5jrmofptid.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}, + string_address{"lykcas4tus7mkm4bhsgqe4drtd4awi7gja24goscc47xfgzj54yofyqd.onion:18083", no_encryption}, }; } else if (m_nettype == cryptonote::TESTNET) { return { - "stressgguj7ugyxtqe7czeoelobeb3cnyhltooueuae2t3avd5ynepid.onion:28184", + string_address{"stressgguj7ugyxtqe7czeoelobeb3cnyhltooueuae2t3avd5ynepid.onion:28184", 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 {}; @@ -963,7 +987,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); @@ -982,8 +1005,38 @@ 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; + 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 if (zone.first == epee::net_utils::zone::public_) + MINFO("Not using encryption 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()); } } @@ -1141,7 +1194,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); @@ -1190,6 +1242,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); @@ -1366,13 +1420,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 @@ -1387,17 +1441,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; } @@ -1407,11 +1461,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; } @@ -1423,7 +1477,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); @@ -1431,13 +1485,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, (con->support_flags & P2P_SUPPORT_FLAG_TX_RELAY_V2)); @@ -1448,22 +1506,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; } @@ -1472,10 +1530,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; } @@ -1533,7 +1591,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; } @@ -1795,7 +1853,7 @@ namespace nodetool << "] last_seen: " << (candidate.last_seen ? epee::misc_utils::get_time_interval_string(time(NULL) - candidate.last_seen) : "never")); const time_t begin_connect = time(NULL); - if (!try_to_connect_and_handshake_with_new_peer(candidate.adr, false, candidate.last_seen, use_white_list ? white : gray)) { + if(!try_to_connect_and_handshake_with_new_peer({candidate.adr, get_p2p_encryption(candidate)}, false, candidate.last_seen, use_white_list ? white : gray)) { time_t fail_connect = time(NULL); _note("Handshake failed after " << epee::misc_utils::get_time_interval_string(fail_connect - begin_connect)); continue; @@ -1821,8 +1879,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()); } @@ -1839,7 +1897,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)) @@ -1856,7 +1914,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); } } @@ -2282,6 +2340,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; } //----------------------------------------------------------------------------------- @@ -2301,7 +2361,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) @@ -2393,7 +2460,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; } //----------------------------------------------------------------------------------- @@ -2447,6 +2514,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, @@ -2497,7 +2565,7 @@ namespace nodetool return false; } return true; - }, "0.0.0.0", m_ssl_support, p2p_connection_context{true /* is_ping */}); + }, "0.0.0.0", get_p2p_encryption(node_data), p2p_connection_context{true /* is_ping */}); if(!r) { LOG_WARNING_CC(context, "Failed to call connect_async, network error."); @@ -2565,7 +2633,7 @@ namespace nodetool { local_peerlist_new.insert( local_peerlist_new.begin() + crypto::rand_range(std::size_t(0), local_peerlist_new.size()), - peerlist_entry{zone.m_our_address, zone.m_config.m_peer_id, 0} + peerlist_entry{zone.m_our_address, {}, zone.m_config.m_peer_id, 0} ); } @@ -2643,7 +2711,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"); @@ -2665,6 +2733,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); }); @@ -2761,22 +2831,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; @@ -2790,19 +2860,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)); } } @@ -2982,8 +3053,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)); @@ -3035,13 +3106,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_context(), zone.m_proxy_address, remote); + auto result = socks_connect_internal(zone.m_net_server.get_stop_signal(), zone.m_net_server.get_io_context(), 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; @@ -3049,10 +3120,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"); @@ -3061,13 +3132,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()); } @@ -3077,10 +3148,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 3d22ecd8e6d..03ca4ba0122 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 81ae3e6032d..d05e9d667bf 100644 --- a/src/p2p/net_peerlist_boost_serialization.h +++ b/src/p2p/net_peerlist_boost_serialization.h @@ -39,7 +39,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 { @@ -239,6 +240,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 @@ -247,6 +260,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 cc0c6cf5bf2..4224fbf2eb3 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,22 +50,72 @@ 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; s << std::hex << peer_id; 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), {}}; + } + template 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) @@ -72,16 +124,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; @@ -89,20 +134,18 @@ 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; @@ -148,12 +191,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) @@ -162,6 +207,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/blockchain.py b/tests/functional_tests/blockchain.py index 1e8ebdc8438..8a8bcbf296c 100755 --- a/tests/functional_tests/blockchain.py +++ b/tests/functional_tests/blockchain.py @@ -286,7 +286,9 @@ def wait_until_same_chain(): # Disconnect the daemons from their peers daemon0.out_peers(0) + daemon0.in_peers(0) daemon1.out_peers(0) + daemon1.in_peers(0) # Generate blocks on each daemon, have the second daemon mine 1 extra block so it has the heavier chain # Note: these two calls could be concurrent to speed this test up @@ -302,7 +304,9 @@ def wait_until_same_chain(): # Now re-connect the daemons and wait until daemon0 gets onto daemon1's heavier chain daemon0.out_peers(12) + daemon0.in_peers(12) daemon1.out_peers(12) + daemon1.in_peers(12) (res0_info, res1_info) = wait_until_same_chain() assert res0_info.height == (genblocks1_res.height + 1) diff --git a/tests/functional_tests/functional_tests_rpc.py b/tests/functional_tests/functional_tests_rpc.py index 4be87b5592a..cbdf01aad4d 100755 --- a/tests/functional_tests/functional_tests_rpc.py +++ b/tests/functional_tests/functional_tests_rpc.py @@ -56,9 +56,9 @@ monerod_extra = [ ["--offline"], ["--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", "2", "--allow-mismatched-daemon-version"] wallet_extra = [ diff --git a/tests/functional_tests/p2p.py b/tests/functional_tests/p2p.py index c662f04d9e4..24f64e52a0d 100755 --- a/tests/functional_tests/p2p.py +++ b/tests/functional_tests/p2p.py @@ -59,6 +59,7 @@ def run_test(self): txid = self.test_p2p_tx_propagation() self.test_p2p_block_propagation_new(txid) self.bench_p2p_heavy_block_propagation_speed() + self.test_p2p_ssl() def reset(self): print('Resetting blockchain') @@ -92,6 +93,8 @@ def test_p2p_reorg(self): daemon3 = Daemon(idx = 3) # give sync some time + daemon2.stop_mining() + daemon3.stop_mining() time.sleep(1) res = daemon2.get_info() @@ -106,7 +109,9 @@ def test_p2p_reorg(self): # disconnect daemons and mine separately on both daemon2.out_peers(0) + daemon2.in_peers(0) daemon3.out_peers(0) + daemon3.in_peers(0) res = daemon2.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 2) res = daemon3.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 3) @@ -123,7 +128,9 @@ def test_p2p_reorg(self): # reconnect, daemon2 will now switch to daemon3's chain daemon2.out_peers(8) + daemon2.in_peers(8) daemon3.out_peers(8) + daemon3.in_peers(8) time.sleep(10) res = daemon2.get_info() assert res.height == height + 3 @@ -131,7 +138,9 @@ def test_p2p_reorg(self): # disconect, mine on daemon2 again more than daemon3 daemon2.out_peers(0) + daemon2.in_peers(0) daemon3.out_peers(0) + daemon3.in_peers(0) res = daemon2.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 3) res = daemon3.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 2) @@ -148,7 +157,9 @@ def test_p2p_reorg(self): # reconnect, daemon3 will now switch to daemon2's chain daemon2.out_peers(8) + daemon2.in_peers(8) daemon3.out_peers(8) + daemon3.in_peers(8) time.sleep(5) res = daemon3.get_info() assert res.height == height + 6 @@ -156,12 +167,16 @@ def test_p2p_reorg(self): # disconnect and mine a lot on daemon3 daemon2.out_peers(0) + daemon2.in_peers(0) daemon3.out_peers(0) + daemon3.in_peers(0) res = daemon3.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', 500) # reconnect and wait for sync daemon2.out_peers(8) + daemon2.in_peers(8) daemon3.out_peers(8) + daemon3.in_peers(8) deadline = time.monotonic() + 240 result = None while result is None: @@ -183,6 +198,9 @@ def test_p2p_tx_propagation(self): res = daemon.get_transaction_pool_hashes() assert len(res.get('tx_hashes', [])) == 0 + for daemon in daemons: + daemon.stop_mining() + self.wallet.refresh() res = self.wallet.get_balance() @@ -454,5 +472,29 @@ def bench_p2p_heavy_block_propagation_speed(self): print(' Average block propagation time: {:.2f}'.format(avg_prop)) print(' Median block propagation time: {:.2f}'.format(median_prop)) + 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 1ab019afb12..2dccbf6c324 100644 --- a/tests/net_load_tests/net_load_tests.h +++ b/tests/net_load_tests/net_load_tests.h @@ -46,7 +46,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; } volatile bool m_closed; diff --git a/tests/unit_tests/epee_levin_protocol_handler_async.cpp b/tests/unit_tests/epee_levin_protocol_handler_async.cpp index 08aa5414ab4..113a8c4adeb 100644 --- a/tests/unit_tests/epee_levin_protocol_handler_async.cpp +++ b/tests/unit_tests/epee_levin_protocol_handler_async.cpp @@ -371,14 +371,16 @@ 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()); + std::string padding = send_data.substr(sizeof(resp_head) + expected_out_data.size()); // Check sent response ASSERT_EQ(expected_out_data, out_data); + ASSERT_EQ(padding.size(), std::count(padding.begin(), padding.end(), 0)); 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() + padding.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)); @@ -437,7 +439,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(); @@ -478,7 +480,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 a9dcf8ae8cc..c2d6d7ba3de 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(); } @@ -406,7 +406,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()); @@ -421,7 +421,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); @@ -432,7 +432,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()); @@ -447,7 +447,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); @@ -458,7 +458,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); @@ -474,7 +474,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); @@ -489,9 +489,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) @@ -609,6 +609,10 @@ TEST_F(levin_notify, defaulted) EXPECT_FALSE(notifier.send_txs(std::move(txs), mock_tx_hashes(txs.size()), random_generator_(), cryptonote::relay_method::local)); } +/* Padding is now performed at the protocol level instead of payload. This + provides a quicker and more accurate padding. It is performed in + `contrib/epee/include/net/levin_base.h`. */ + TEST_F(levin_notify, fluff_without_padding) { std::shared_ptr notifier_ptr = make_notifier(0, true, false); @@ -1077,7 +1081,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); } } @@ -1142,7 +1146,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); } @@ -1205,7 +1209,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); } } @@ -1274,7 +1278,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); } @@ -1303,7 +1307,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; @@ -1371,7 +1375,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); } @@ -1764,7 +1768,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); } } @@ -1816,7 +1820,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); } } @@ -1868,7 +1872,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); } } @@ -1920,7 +1924,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); } } @@ -2451,7 +2455,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())); @@ -2467,7 +2471,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 94f9fa85fa7..96be63fd435 100644 --- a/tests/unit_tests/node_server.cpp +++ b/tests/unit_tests/node_server.cpp @@ -704,14 +704,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 {}; } @@ -1255,6 +1255,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 b4d050848fc..1580dd68647 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));