From 8dabfb4a2eccd772c1d436511356088201d792fb Mon Sep 17 00:00:00 2001 From: Ravin Perera <33562092+ravinsp@users.noreply.github.com> Date: Fri, 17 Dec 2021 07:08:26 +0530 Subject: [PATCH] IP ban refactor with hpws. (#354) * Refactored corebill. * Integrated hpws ban tracking. * Removed session handler helpers. * Improved bad behaviour reporting. --- CMakeLists.txt | 4 +- src/bill/corebill.cpp | 91 -------- src/bill/corebill.h | 24 -- src/comm/comm_server.hpp | 52 +++-- src/comm/comm_session.cpp | 36 ++- src/comm/comm_session.hpp | 19 +- src/comm/hpws.hpp | 64 +++++- src/consensus.cpp | 1 - src/corebill/corebill.hpp | 28 +++ src/corebill/tracker.cpp | 60 +++++ src/corebill/tracker.hpp | 25 +++ src/msg/fbuf/p2pmsg_conversion.cpp | 2 - src/p2p/p2p.hpp | 1 - src/p2p/peer_comm_server.cpp | 6 +- src/p2p/peer_comm_session.cpp | 297 ++++++++++++++++++++++++- src/p2p/peer_session_handler.cpp | 342 ----------------------------- src/p2p/peer_session_handler.hpp | 17 -- src/p2p/self_node.cpp | 20 +- src/usr/user_comm_session.cpp | 84 ++++++- src/usr/user_session_handler.cpp | 102 --------- src/usr/user_session_handler.hpp | 15 -- src/usr/usr.cpp | 1 - src/usr/usr.hpp | 1 - test/bin/hpws | Bin 43656 -> 43656 bytes 24 files changed, 641 insertions(+), 651 deletions(-) delete mode 100644 src/bill/corebill.cpp delete mode 100644 src/bill/corebill.h create mode 100644 src/corebill/corebill.hpp create mode 100644 src/corebill/tracker.cpp create mode 100644 src/corebill/tracker.hpp delete mode 100644 src/p2p/peer_session_handler.cpp delete mode 100644 src/p2p/peer_session_handler.hpp delete mode 100644 src/usr/user_session_handler.cpp delete mode 100644 src/usr/user_session_handler.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3be708c7..2cbbc4f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ add_executable(hpcore src/crypto.cpp src/conf.cpp src/hplog.cpp - src/bill/corebill.cpp + src/corebill/tracker.cpp src/hpfs/hpfs_mount.cpp src/hpfs/hpfs_serve.cpp src/hpfs/hpfs_sync.cpp @@ -57,11 +57,9 @@ add_executable(hpcore src/msg/usrmsg_parser.cpp src/p2p/peer_comm_server.cpp src/p2p/peer_comm_session.cpp - src/p2p/peer_session_handler.cpp src/p2p/self_node.cpp src/p2p/p2p.cpp src/usr/user_comm_session.cpp - src/usr/user_session_handler.cpp src/usr/input_nonce_map.cpp src/usr/usr.cpp src/usr/read_req.cpp diff --git a/src/bill/corebill.cpp b/src/bill/corebill.cpp deleted file mode 100644 index 53a77f36..00000000 --- a/src/bill/corebill.cpp +++ /dev/null @@ -1,91 +0,0 @@ -#include "../pchheader.hpp" -#include "../util/util.hpp" -#include "../util/ttl_set.hpp" -#include "../hplog.hpp" -#include "corebill.h" - -namespace corebill -{ - - // How many violations can occur for a host before being escalated. - constexpr uint32_t VIOLATION_THRESHOLD = 10; - - // Violation cooldown interval. - constexpr uint32_t VIOLATION_REFRESH_INTERVAL = 600 * 1000; // 10 minutes - - // Keeps track of violation count against offending hosts. - std::unordered_map violation_counter; - - // Graylist mutex. - std::mutex graylist_mutex; - - // Keeps the graylisted hosts. - util::ttl_set graylist; - - // Keeps the whitelisted hosts who would be ignored in all violation tracking. - std::unordered_set whitelist; - - /** - * Report a violation. Violation means a force disconnection of a socket due to some threshold exceeding. - */ - void report_violation(const std::string host) - { - if (whitelist.find(host) != whitelist.end()) // Is in whitelist - { - LOG_DEBUG << host << " is whitelisted. Ignoring the violation."; - return; - } - - violation_stat &stat = violation_counter[host]; - - const uint64_t time_now = util::get_epoch_milliseconds(); - - stat.counter++; - - if (stat.timestamp == 0) - { - // Reset counter timestamp. - stat.timestamp = time_now; - } - - // Check whether we have exceeded the threshold within the monitering interval. - const uint64_t elapsed_time = time_now - stat.timestamp; - if (elapsed_time <= VIOLATION_REFRESH_INTERVAL && stat.counter > VIOLATION_THRESHOLD) - { - // IP exceeded violation threshold. - - stat.timestamp = 0; - stat.counter = 0; - std::scoped_lock gray_list_lock(graylist_mutex); - graylist.emplace(host, VIOLATION_REFRESH_INTERVAL); - LOG_WARNING << host << " placed on graylist."; - } - else if (elapsed_time > VIOLATION_REFRESH_INTERVAL) - { - // Start the counter fresh. - stat.timestamp = time_now; - stat.counter = 1; - } - } - - void add_to_whitelist(const std::string host) - { - // Add to whitelist and remove from all other offender lists. - whitelist.emplace(host); - std::scoped_lock gray_list_lock(graylist_mutex); - graylist.erase(host); - violation_counter.erase(host); - } - - void remove_from_whitelist(const std::string host) - { - whitelist.erase(host); - } - - bool is_banned(const std::string &host) - { - std::scoped_lock gray_list_lock(graylist_mutex); - return graylist.exists(host); - } - -} // namespace corebill \ No newline at end of file diff --git a/src/bill/corebill.h b/src/bill/corebill.h deleted file mode 100644 index 8ac0bec3..00000000 --- a/src/bill/corebill.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef _HP_COREBILL_ -#define _HP_COREBILL_ - -#include "../pchheader.hpp" - -namespace corebill -{ - -/** - * Keeps the violation counter and the timestamp of the monitoring window. - */ -struct violation_stat -{ - uint32_t counter = 0; - uint64_t timestamp = 0; -}; - -void report_violation(const std::string host); -void add_to_whitelist(const std::string host); -bool is_banned(const std::string &host); - -} // namespace corebill - -#endif \ No newline at end of file diff --git a/src/comm/comm_server.hpp b/src/comm/comm_server.hpp index be939ce3..3bffe279 100644 --- a/src/comm/comm_server.hpp +++ b/src/comm/comm_server.hpp @@ -4,7 +4,8 @@ #include "../pchheader.hpp" #include "../hplog.hpp" #include "../util/util.hpp" -#include "../bill/corebill.h" +#include "../corebill/corebill.hpp" +#include "../corebill/tracker.hpp" #include "hpws.hpp" #include "comm_session.hpp" @@ -57,6 +58,8 @@ namespace comm { util::sleep(100); + apply_ban_updates(); + // Accept any new incoming connection if available. check_for_new_connection(); @@ -106,6 +109,29 @@ namespace comm LOG_INFO << name << " listener stopped."; } + void apply_ban_updates() + { + corebill::ban_update b; + while (violation_tracker.ban_updates.try_dequeue(b)) + { + in_addr ia4 = {}; + in6_addr ia6 = {}; + + if (inet_pton((b.is_ipv4 ? AF_INET : AF_INET6), b.host.c_str(), (b.is_ipv4 ? (void *)&ia4 : (void *)&ia6)) == 1) + { + const uint32_t *addr = b.is_ipv4 ? (uint32_t *)&ia4.s_addr : ia6.__in6_u.__u6_addr32; + if (b.is_ban) + hpws_server->ban_ip(addr, b.ttl_sec, b.is_ipv4); + else + hpws_server->unban_ip(addr, b.is_ipv4); + } + else + { + LOG_ERROR << "Invalid host " << b.host << " in ban update."; + } + } + } + void check_for_new_connection() { if (listen_port == 0) @@ -134,18 +160,12 @@ namespace comm else { const std::string &host_address = std::get(host_result); - if (!corebill::is_banned(host_address)) - { - // We do not directly add to sessions list. We simply add to new_sessions list under a lock so the main server - // loop will take care of initialize the new sessions. This is because inherited classes (eg. peer_comm_server) - // need a way to safely inject new sessions from another thread. - std::scoped_lock lock(new_sessions_mutex); - new_sessions.emplace_back(host_address, std::move(client), true, metric_thresholds); - } - else - { - LOG_DEBUG << "Dropping " << name << " connection for banned host " << host_address; - } + + // We do not directly add to sessions list. We simply add to new_sessions list under a lock so the main server + // loop will take care of initialize the new sessions. This is because inherited classes (eg. peer_comm_server) + // need a way to safely inject new sessions from another thread. + std::scoped_lock lock(new_sessions_mutex); + new_sessions.emplace_back(this->violation_tracker, host_address, std::move(client), client.is_ipv4, true, metric_thresholds); } // If the hpws client object was not added to a session so far, in will get dstructed and the channel will close. @@ -181,7 +201,7 @@ namespace comm messages_processed = true; if (result == -1) - session.mark_for_closure(); + session.mark_for_closure(CLOSE_VIOLATION::VIOLATION_MSG_READ); } } } @@ -195,7 +215,7 @@ namespace comm messages_processed = true; if (result == -1) - session.mark_for_closure(); + session.mark_for_closure(CLOSE_VIOLATION::VIOLATION_MSG_READ); } } @@ -233,6 +253,8 @@ namespace comm } public: + corebill::tracker violation_tracker; + comm_server(std::string_view name, const uint16_t port, const uint64_t (&metric_thresholds)[5], const uint64_t max_msg_size, const uint64_t max_in_connections, const uint64_t max_in_connections_per_host, const bool use_priority_queues) : name(name), diff --git a/src/comm/comm_session.cpp b/src/comm/comm_session.cpp index 9ca23fad..45868c7b 100644 --- a/src/comm/comm_session.cpp +++ b/src/comm/comm_session.cpp @@ -2,7 +2,7 @@ #include "../hplog.hpp" #include "../util/util.hpp" #include "../conf.hpp" -#include "../bill/corebill.h" +#include "../corebill/tracker.hpp" #include "hpws.hpp" #include "comm_session.hpp" @@ -12,11 +12,13 @@ namespace comm constexpr uint32_t UNVERIFIED_INACTIVE_TIMEOUT = 5000; // Time threshold ms for unverified inactive connections. constexpr uint16_t MAX_IN_MSG_QUEUE_SIZE = 255; // Maximum in message queue size, The size passed is rounded to next number in binary sequence 1(1),11(3),111(7),1111(15),11111(31).... - comm_session::comm_session( - std::string_view host_address, hpws::client &&hpws_client, const bool is_inbound, const uint64_t (&metric_thresholds)[5]) - : uniqueid(host_address), + comm_session::comm_session(corebill::tracker &violation_tracker, + std::string_view host_address, hpws::client &&hpws_client, const bool is_ipv4, const bool is_inbound, const uint64_t (&metric_thresholds)[5]) + : violation_tracker(violation_tracker), + uniqueid(host_address), host_address(host_address), hpws_client(std::move(hpws_client)), + is_ipv4(is_ipv4), is_inbound(is_inbound), in_msg_queue1(MAX_IN_MSG_QUEUE_SIZE), in_msg_queue2(MAX_IN_MSG_QUEUE_SIZE) @@ -81,7 +83,10 @@ namespace comm const int priority = get_message_priority(data); if (priority == 0) // priority 0 means a bad message. { - increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); + if (challenge_status == comm::CHALLENGE_STATUS::CHALLENGE_VERIFIED) + increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); + else + should_disconnect = true; // Disconnect if we receive a bad message before challenge verification. } else if (priority == 1 || priority == 2) { @@ -110,9 +115,14 @@ namespace comm if (should_disconnect) { + // Report bad behaviour for connection drops occuring prior to challenge verification. + const CLOSE_VIOLATION reason = (challenge_status != comm::CHALLENGE_STATUS::CHALLENGE_VERIFIED) + ? CLOSE_VIOLATION::VIOLATION_READ_ERROR + : CLOSE_VIOLATION::VIOLATION_NONE; + // Here we mark the session as needing to close. // The session will be properly "closed" and cleaned up by the global comm_server thread. - mark_for_closure(); + mark_for_closure(reason); break; } } @@ -242,12 +252,15 @@ namespace comm * Mark the session as needing to close. The session will be properly "closed" * and cleaned up by the global comm_server thread. */ - void comm_session::mark_for_closure() + void comm_session::mark_for_closure(const CLOSE_VIOLATION reason) { - if (state == SESSION_STATE::CLOSED) + if (state == SESSION_STATE::MUST_CLOSE || state == SESSION_STATE::CLOSED) return; state = SESSION_STATE::MUST_CLOSE; + + if (reason != CLOSE_VIOLATION::VIOLATION_NONE) + violation_tracker.report_violation(host_address, is_ipv4, std::to_string(reason)); } /** @@ -323,17 +336,16 @@ namespace comm t.timestamp = time_now; } - // Check whether we have exceeded the threshold within the monitering interval. + // Check whether we have exceeded the threshold within the monitoring interval. const uint64_t elapsed_time = time_now - t.timestamp; if (elapsed_time <= t.intervalms && t.counter_value > t.threshold_limit) { - mark_for_closure(); t.timestamp = 0; t.counter_value = 0; LOG_INFO << "Session " << display_name() << " threshold exceeded. (type:" << threshold_type << " limit:" << t.threshold_limit << ")"; - corebill::report_violation(host_address); + mark_for_closure(CLOSE_VIOLATION::VIOLATION_THRESHOLD_EXCEEDED); } else if (elapsed_time > t.intervalms) { @@ -356,7 +368,7 @@ namespace comm if (util::get_epoch_milliseconds() - last_activity_timestamp >= timeout) { LOG_DEBUG << "Closing " << display_name() << " connection due to inactivity."; - mark_for_closure(); + mark_for_closure(CLOSE_VIOLATION::VIOLATION_INACTIVITY); } } diff --git a/src/comm/comm_session.hpp b/src/comm/comm_session.hpp index 6c381a0f..76a6ae8e 100644 --- a/src/comm/comm_session.hpp +++ b/src/comm/comm_session.hpp @@ -3,12 +3,12 @@ #include "../pchheader.hpp" #include "../conf.hpp" +#include "../corebill/tracker.hpp" #include "hpws.hpp" #include "comm_session_threshold.hpp" namespace comm { - enum CHALLENGE_STATUS { NOT_ISSUED, @@ -24,12 +24,22 @@ namespace comm CLOSED // Session is fully closed. }; + enum CLOSE_VIOLATION + { + VIOLATION_NONE = 0, + VIOLATION_MSG_READ = 1, + VIOLATION_READ_ERROR = 2, + VIOLATION_THRESHOLD_EXCEEDED = 3, + VIOLATION_INACTIVITY = 4 + }; + /** * Represents an active WebSocket connection */ class comm_session { private: + corebill::tracker &violation_tracker; std::optional hpws_client; std::vector thresholds; // track down various communication thresholds @@ -53,14 +63,15 @@ namespace comm std::string uniqueid; // Verified session: Pubkey in hex format, Unverified session: IP address. std::string pubkey; // Pubkey in binary format. const bool is_inbound; + const bool is_ipv4; // Whether the host is ipv4 or ipv6. const std::string host_address; // Connection host address of the remote party. std::string issued_challenge; SESSION_STATE state = SESSION_STATE::NONE; CHALLENGE_STATUS challenge_status = CHALLENGE_STATUS::NOT_ISSUED; uint64_t last_activity_timestamp; // Keep track of the last activity timestamp in milliseconds. - comm_session( - std::string_view host_address, hpws::client &&hpws_client, const bool is_inbound, const uint64_t (&metric_thresholds)[5]); + comm_session(corebill::tracker &violation_tracker, + std::string_view host_address, hpws::client &&hpws_client, const bool is_ipv4, const bool is_inbound, const uint64_t (&metric_thresholds)[5]); int init(); int process_next_inbound_message(const uint16_t priority); int send(const std::vector &message, const uint16_t priority = 2); @@ -68,7 +79,7 @@ namespace comm int process_outbound_message(std::string_view message); void process_outbound_msg_queue(); void check_last_activity_rules(); - void mark_for_closure(); + void mark_for_closure(const CLOSE_VIOLATION reason = CLOSE_VIOLATION::VIOLATION_NONE); void close(); void mark_as_verified(); virtual const std::string display_name() const; diff --git a/src/comm/hpws.hpp b/src/comm/hpws.hpp index b6b63e4b..aa287bff 100644 --- a/src/comm/hpws.hpp +++ b/src/comm/hpws.hpp @@ -2,7 +2,6 @@ #define HPWS_INCLUDE #include #include -#include #include #include #include @@ -18,6 +17,7 @@ #include #include #include +#include #define DECODE_O_SIZE(control_msg, into) \ { \ @@ -84,6 +84,7 @@ namespace hpws pid_t child_pid, int buffer_fd[4], void *buffer[4]) : endpoint(endpoint), + is_ipv4(endpoint.sa.sa_family == AF_INET), max_buffer_size(max_buffer_size), child_pid(child_pid), get(get) { @@ -100,6 +101,8 @@ namespace hpws } public: + bool is_ipv4 = false; + // No copy constructor client(const client &) = delete; @@ -107,6 +110,7 @@ namespace hpws client(client &&old) : child_pid(old.child_pid), max_buffer_size(old.max_buffer_size), endpoint(old.endpoint), + is_ipv4(old.is_ipv4), get(old.get) { old.moved = true; @@ -152,9 +156,11 @@ namespace hpws const std::variant host_address() { char hostname[NI_MAXHOST]; - const int ret = getnameinfo((sockaddr *)&endpoint, sizeof(sockaddr), hostname, sizeof(hostname), NULL, 0, NI_NUMERICHOST); - if (ret != 0) - return error{10, gai_strerror(ret)}; + const char *ret = (endpoint.sa.sa_family == AF_INET) + ? inet_ntop(AF_INET, &endpoint.sin.sin_addr, hostname, NI_MAXHOST) + : inet_ntop(AF_INET6, &endpoint.sin6.sin6_addr, hostname, NI_MAXHOST); + if (!ret) + return error{10, "error in inet_ntop"}; return hostname; } @@ -605,6 +611,56 @@ namespace hpws return max_buffer_size_; } + void ban_ip(const uint32_t *addr, const uint32_t ttl_sec, const bool ipv4) + { + const size_t len = ipv4 ? 11 : 23; + char buf[len]; + buf[0] = 'i'; + buf[1] = '+'; + buf[2] = ipv4 ? '4' : '6'; + + uint32_t *addr_buf = (uint32_t *)&buf[3]; + if (ipv4) + { + addr_buf[0] = addr[0]; + } + else + { + addr_buf[0] = addr[0]; + addr_buf[1] = addr[2]; + addr_buf[2] = addr[3]; + addr_buf[3] = addr[4]; + } + + *(uint32_t *)&buf[len - 4] = ttl_sec; + + write(this->master_control_fd_, buf, len); + } + + void unban_ip(const uint32_t *addr, const bool ipv4) + { + const size_t len = ipv4 ? 7 : 19; + char buf[len]; + buf[0] = 'i'; + buf[1] = '-'; + buf[2] = ipv4 ? '4' : '6'; + + uint32_t *addr_buf = (uint32_t *)&buf[3]; + if (ipv4) + { + addr_buf[0] = addr[0]; + } + else + { + addr_buf[0] = addr[0]; + addr_buf[1] = addr[2]; + addr_buf[2] = addr[3]; + addr_buf[3] = addr[4]; + } + + write(this->master_control_fd_, buf, len); + } + std::variant accept(const bool no_block = false) { diff --git a/src/consensus.cpp b/src/consensus.cpp index 8b6d46e2..14c0c270 100644 --- a/src/consensus.cpp +++ b/src/consensus.cpp @@ -7,7 +7,6 @@ #include "msg/fbuf/p2pmsg_conversion.hpp" #include "msg/usrmsg_parser.hpp" #include "msg/usrmsg_common.hpp" -#include "p2p/peer_session_handler.hpp" #include "hplog.hpp" #include "crypto.hpp" #include "util/h32.hpp" diff --git a/src/corebill/corebill.hpp b/src/corebill/corebill.hpp new file mode 100644 index 00000000..16427946 --- /dev/null +++ b/src/corebill/corebill.hpp @@ -0,0 +1,28 @@ +#ifndef _HP_COREBILL_ +#define _HP_COREBILL_ + +#include "../pchheader.hpp" + +namespace corebill +{ + + /** + * Keeps the violation counter and the timestamp of the monitoring window. + */ + struct violation_stat + { + uint32_t counter = 0; + uint64_t timestamp = 0; + }; + + struct ban_update + { + bool is_ban = false; // Whether to ban or unban. + bool is_ipv4 = false; // If host is ipv4 or ipv6. + std::string host; + uint32_t ttl_sec; // Time in seconds to enforce the ban. Relevent only for bans. + }; + +} // namespace corebill + +#endif \ No newline at end of file diff --git a/src/corebill/tracker.cpp b/src/corebill/tracker.cpp new file mode 100644 index 00000000..4601be48 --- /dev/null +++ b/src/corebill/tracker.cpp @@ -0,0 +1,60 @@ +#include "../pchheader.hpp" +#include "corebill.hpp" +#include "tracker.hpp" +#include "../util/util.hpp" + +namespace corebill +{ + // How many violations can a host make within the refresh interval before being banned. + constexpr uint32_t VIOLATION_THRESHOLD = 5; + + // Violation cooldown interval. + constexpr uint32_t VIOLATION_REFRESH_INTERVAL = 600 * 1000; // 10 minutes + + // Ban period. + constexpr uint32_t BAN_TTL_SEC = 600; // 10 minutes. + + /** + * Report a violation. Violation means the connection has displayed a bad behaviour. + * When multiple violations occur within a time window, we ban that host from connecting again for a certain duration. + */ + void tracker::report_violation(const std::string &host, const bool ipv4, const std::string &reason) + { + std::scoped_lock lock(ban_mutex); + + violation_stat &stat = violation_counter[host]; + const uint64_t time_now = util::get_epoch_milliseconds(); + + LOG_INFO << "Reported violation '" << reason << "' from " << host; + + // Check whether we have exceeded the violation threshold within the time window. + const uint64_t elapsed_time = time_now - stat.timestamp; + if (elapsed_time <= VIOLATION_REFRESH_INTERVAL && (stat.counter + 1) > VIOLATION_THRESHOLD) + { + violation_counter.erase(host); + + // IP exceeded violation threshold. We must ban the host. + LOG_WARNING << "Banning " << host << " for " << BAN_TTL_SEC << "s"; + ban_updates.enqueue(ban_update{true, ipv4, host, BAN_TTL_SEC}); // Inform hpws about the ban. + banned_hosts.emplace(host, BAN_TTL_SEC * 1000); // Add to local ban list to cross-check outgoing connections. + return; + } + + if (stat.timestamp == 0 || elapsed_time > VIOLATION_REFRESH_INTERVAL) + { + // Start the counter fresh. + stat.timestamp = time_now; + stat.counter = 1; + } + else + { + stat.counter++; + } + } + + bool tracker::is_banned(const std::string &host) + { + std::scoped_lock lock(ban_mutex); + return banned_hosts.exists(host); + } +} \ No newline at end of file diff --git a/src/corebill/tracker.hpp b/src/corebill/tracker.hpp new file mode 100644 index 00000000..ae164248 --- /dev/null +++ b/src/corebill/tracker.hpp @@ -0,0 +1,25 @@ +#ifndef _HP_COREBILL_TRACKER_ +#define _HP_COREBILL_TRACKER_ + +#include "../pchheader.hpp" +#include "../util/ttl_set.hpp" +#include "corebill.hpp" + +namespace corebill +{ + class tracker + { + private: + // Keeps track of violation count against offending hosts. + std::unordered_map violation_counter; + util::ttl_set banned_hosts; + std::mutex ban_mutex; + + public: + moodycamel::ConcurrentQueue ban_updates; + void report_violation(const std::string &host, const bool ipv4, const std::string &reason); + bool is_banned(const std::string &host); + }; +} + +#endif \ No newline at end of file diff --git a/src/msg/fbuf/p2pmsg_conversion.cpp b/src/msg/fbuf/p2pmsg_conversion.cpp index 12ede0cc..f250c667 100644 --- a/src/msg/fbuf/p2pmsg_conversion.cpp +++ b/src/msg/fbuf/p2pmsg_conversion.cpp @@ -17,8 +17,6 @@ namespace msg::fbuf::p2pmsg /** * This section contains Flatbuffer message reading/writing helpers. - * These helpers are mainly used by peer_session_handler and other components which sends outgoing p2p messages. - * * A p2p flatbuffer message is a bucket with hp version and the message 'content'. */ diff --git a/src/p2p/p2p.hpp b/src/p2p/p2p.hpp index 534d13ac..ab5dc95e 100644 --- a/src/p2p/p2p.hpp +++ b/src/p2p/p2p.hpp @@ -10,7 +10,6 @@ #include "../msg/fbuf/p2pmsg_generated.h" #include "peer_comm_server.hpp" #include "peer_comm_session.hpp" -#include "peer_session_handler.hpp" namespace p2p { diff --git a/src/p2p/peer_comm_server.cpp b/src/p2p/peer_comm_server.cpp index a68e2faf..ac9e569c 100644 --- a/src/p2p/peer_comm_server.cpp +++ b/src/p2p/peer_comm_server.cpp @@ -198,12 +198,12 @@ namespace p2p else { const std::string &host_address = std::get(host_result); - p2p::peer_comm_session session(host_address, std::move(client), false, metric_thresholds); + p2p::peer_comm_session session(this->violation_tracker, host_address, std::move(client), client.is_ipv4, false, metric_thresholds); // Skip if this peer is banned due to corebill violations. - if (corebill::is_banned(host_address)) + if (violation_tracker.is_banned(host_address)) { - LOG_DEBUG << "Skipping peer " << host_address << " from connecting. This peer is banned."; + LOG_DEBUG << "Skipping connecting to banned peer " << host_address; continue; } diff --git a/src/p2p/peer_comm_session.cpp b/src/p2p/peer_comm_session.cpp index 93054596..6bfcd152 100644 --- a/src/p2p/peer_comm_session.cpp +++ b/src/p2p/peer_comm_session.cpp @@ -1,32 +1,317 @@ #include "../pchheader.hpp" +#include "../util/rollover_hashset.hpp" +#include "../msg/fbuf/p2pmsg_generated.h" +#include "../msg/fbuf/p2pmsg_conversion.hpp" +#include "../msg/fbuf/common_helpers.hpp" +#include "../crypto.hpp" +#include "../sc/hpfs_log_sync.hpp" +#include "../sc/sc.hpp" +#include "../ledger/ledger.hpp" #include "peer_comm_session.hpp" -#include "peer_session_handler.hpp" + +namespace p2pmsg = msg::fbuf::p2pmsg; namespace p2p { + // Max size of messages which are subjected to duplicate message check. + constexpr size_t MAX_SIZE_FOR_DUP_CHECK = 1 * 1024 * 1024; // 1 MB + + // The set of recent peer message hashes used for duplicate detection. + util::rollover_hashset recent_peermsg_hashes(200); + + /** + * This gets hit every time a peer connects to HP via the peer port (configured in config). + * @param session connected session. + * @return returns 0 if connection is successful and peer challenge is sent otherwise, -1. + */ int peer_comm_session::handle_connect() { - return p2p::handle_peer_connect(*this); + // Skip new inbound connection if max inbound connection cap is reached. + if (is_inbound && calculate_available_capacity() == 0) + { + LOG_DEBUG << "Max peer connection cap reached. Rejecting new peer connection [" << display_name() << "]"; + return -1; + } + + // Send peer challenge. + flatbuffers::FlatBufferBuilder fbuf; + p2pmsg::create_msg_from_peer_challenge(fbuf, issued_challenge); + std::string_view msg = std::string_view( + reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); + send(msg); + challenge_status = comm::CHALLENGE_STATUS::CHALLENGE_ISSUED; + return 0; } + /** + * Returns the priority that should be assigned to the message. + * @return 0 if bad message. 1 or 2 if correct priority was assigned. + */ int peer_comm_session::get_message_priority(std::string_view msg) { - return p2p::get_message_priority(msg); + if (!p2pmsg::verify_peer_message(msg)) + { + LOG_DEBUG << "Flatbuffer verify: Bad peer message."; + return 0; + } + + const auto p2p_msg = p2pmsg::GetP2PMsg(msg.data()); + const msg::fbuf::p2pmsg::P2PMsgContent type = p2p_msg->content_type(); + + if (type == p2pmsg::P2PMsgContent_ProposalMsg || type == p2pmsg::P2PMsgContent_NonUnlProposalMsg) + return 1; // High priority + else + return 2; // Low priority } + /** + * Peer session on message callback method. Validate and handle each type of peer messages. + * @return 0 on normal execution. -1 when session needs to be closed as a result of message handling. + */ int peer_comm_session::handle_message(std::string_view msg) { - return p2p::handle_peer_message(*this, msg); + const size_t message_size = msg.size(); + // Adding message size to peer message characters(bytes) per minute counter. + increment_metric(comm::SESSION_THRESHOLDS::MAX_RAWBYTES_PER_MINUTE, message_size); + + const peer_message_info mi = p2pmsg::get_peer_message_info(msg, this); + if (!mi.p2p_msg) // Message buffer will be null if peer message was too old. + return 0; + + // Messages larger than the duplicate message threshold is ignored from the duplicate message check + // due to the overhead in hash generation for larger messages. + if (message_size <= MAX_SIZE_FOR_DUP_CHECK && !recent_peermsg_hashes.try_emplace(crypto::get_hash(msg))) + { + increment_metric(comm::SESSION_THRESHOLDS::MAX_DUPMSGS_PER_MINUTE, 1); + LOG_DEBUG << "Duplicate peer message. type:" << mi.type << " from:" << display_name(); + return 0; + } + + // Check whether the message is qualified for message forwarding. + if (p2p::validate_for_peer_msg_forwarding(*this, mi.type, mi.originated_on)) + { + // Npl messages and consensus proposals are forwarded only to unl nodes if relavent flags (npl and consensus) are set to private. + // If consensus and npl flags are public, these messages are forward to all the connected nodes. + const bool unl_only = (!conf::cfg.contract.is_npl_public && mi.type == p2pmsg::P2PMsgContent_NplMsg) || + (!conf::cfg.contract.is_consensus_public && mi.type == p2pmsg::P2PMsgContent_ProposalMsg); + if (need_consensus_msg_forwarding) + { + // Forward messages received by weakly connected nodes to other peers. + p2p::broadcast_message(msg, false, false, unl_only, this); + } + else + { + // Forward message received from other nodes to weakly connected peers. + p2p::broadcast_message(msg, false, true, unl_only, this); + } + } + + if (mi.type == p2pmsg::P2PMsgContent_PeerChallengeMsg) + { + const p2p::peer_challenge chall = p2pmsg::create_peer_challenge_from_msg(mi); + + // Check whether contract ids match. + if (chall.contract_id != conf::cfg.contract.id) + { + LOG_ERROR << "Contract id mismatch. Dropping connection " << display_name(); + return -1; + } + + // Remember the time config reported by this peer. + reported_time_config = chall.time_config; + + // Whether this node is a full history node or not. + is_full_history = chall.is_full_history; + + // Sending the challenge response to the sender. + flatbuffers::FlatBufferBuilder fbuf; + p2pmsg::create_peer_challenge_response_from_challenge(fbuf, chall.challenge); + return send(msg::fbuf::builder_to_string_view(fbuf)); + } + else if (mi.type == p2pmsg::P2PMsgContent_PeerChallengeResponseMsg) + { + // Ignore if challenge is already resolved. + if (challenge_status == comm::CHALLENGE_STATUS::CHALLENGE_ISSUED) + return p2p::resolve_peer_challenge(*this, p2pmsg::create_peer_challenge_response_from_msg(mi)); + } + + if (challenge_status != comm::CHALLENGE_STATUS::CHALLENGE_VERIFIED) + { + LOG_DEBUG << "Cannot accept messages. Peer challenge unresolved. " << display_name(); + return 0; + } + + if (mi.type == p2pmsg::P2PMsgContent_PeerListResponseMsg) + { + const std::vector merge_peers = p2pmsg::create_peer_list_response_from_msg(mi); + p2p::merge_peer_list("Peer_Discovery", &merge_peers, NULL, this); + } + else if (mi.type == p2pmsg::P2PMsgContent_PeerListRequestMsg) + { + p2p::send_known_peer_list(this); + } + else if (mi.type == p2pmsg::P2PMsgContent_PeerCapacityAnnouncementMsg) + { + if (known_ipport.has_value()) + { + const p2p::peer_capacity_announcement ann = p2pmsg::create_peer_capacity_announcement_from_msg(mi); + p2p::update_known_peer_available_capacity(known_ipport.value(), ann.available_capacity, ann.timestamp); + } + } + else if (mi.type == p2pmsg::P2PMsgContent_PeerRequirementAnnouncementMsg) + { + const p2p::peer_requirement_announcement ann = p2pmsg::create_peer_requirement_announcement_from_msg(mi); + need_consensus_msg_forwarding = ann.need_consensus_msg_forwarding; + LOG_DEBUG << "Peer requirement: " << display_name() << " consensus msg forwarding:" << ann.need_consensus_msg_forwarding; + } + else if (mi.type == p2pmsg::P2PMsgContent_NonUnlProposalMsg) + { + handle_nonunl_proposal_message(p2pmsg::create_nonunl_proposal_from_msg(mi)); + } + else if (mi.type == p2pmsg::P2PMsgContent_ProposalMsg) + { + const util::h32 hash = p2pmsg::verify_proposal_msg_trust(mi); + if (hash == util::h32_empty) + { + increment_metric(comm::SESSION_THRESHOLDS::MAX_BADSIGMSGS_PER_MINUTE, 1); + LOG_DEBUG << "Proposal rejected due to trust failure. " << display_name(); + return 0; + } + + handle_proposal_message(p2pmsg::create_proposal_from_msg(mi, hash)); + } + else if (mi.type == p2pmsg::P2PMsgContent_NplMsg) + { + if (!p2pmsg::verify_npl_msg_trust(mi)) + { + increment_metric(comm::SESSION_THRESHOLDS::MAX_BADSIGMSGS_PER_MINUTE, 1); + LOG_DEBUG << "Npl message rejected due to trust failure. " << display_name(); + return 0; + } + + handle_npl_message(p2pmsg::create_npl_from_msg(mi)); + } + else if (mi.type == p2pmsg::P2PMsgContent_HpfsRequestMsg) + { + const p2p::hpfs_request hr = p2pmsg::create_hpfs_request_from_msg(mi); + if (hr.mount_id == sc::contract_fs.mount_id) + { + // Check the cap and insert request with lock. + std::scoped_lock lock(ctx.collected_msgs.contract_hpfs_requests_mutex); + + // If max number of state requests reached skip the rest. + if (ctx.collected_msgs.contract_hpfs_requests.size() < p2p::HPFS_REQ_LIST_CAP) + ctx.collected_msgs.contract_hpfs_requests.push_back(std::make_pair(pubkey, std::move(hr))); + else + LOG_DEBUG << "Hpfs contract fs request rejected. Maximum hpfs contract fs request count reached. " << display_name(); + } + else if (hr.mount_id == ledger::ledger_fs.mount_id) + { + // Check the cap and insert request with lock. + std::scoped_lock lock(ctx.collected_msgs.ledger_hpfs_requests_mutex); + + // If max number of state requests reached skip the rest. + if (ctx.collected_msgs.ledger_hpfs_requests.size() < p2p::HPFS_REQ_LIST_CAP) + ctx.collected_msgs.ledger_hpfs_requests.push_back(std::make_pair(pubkey, std::move(hr))); + else + LOG_DEBUG << "Hpfs ledger fs request rejected. Maximum hpfs ledger fs request count reached. " << display_name(); + } + } + else if (mi.type == p2pmsg::P2PMsgContent_HpfsResponseMsg) + { + const p2pmsg::HpfsResponseMsg &resp_msg = *mi.p2p_msg->content_as_HpfsResponseMsg(); + + // Only accept hpfs responses if hpfs fs is syncing. + if (sc::contract_sync_worker.is_syncing && resp_msg.mount_id() == sc::contract_fs.mount_id) + { + // Check the cap and insert state_response with lock. + std::scoped_lock lock(ctx.collected_msgs.contract_hpfs_responses_mutex); + + // If max number of state responses reached skip the rest. + if (ctx.collected_msgs.contract_hpfs_responses.size() < p2p::HPFS_RES_LIST_CAP) + ctx.collected_msgs.contract_hpfs_responses.push_back(std::make_pair(uniqueid, std::string(msg))); + else + LOG_DEBUG << "Contract hpfs response rejected. Maximum response count reached. " << display_name(); + } + else if (ledger::ledger_sync_worker.is_syncing && resp_msg.mount_id() == ledger::ledger_fs.mount_id) + { + // Check the cap and insert state_response with lock. + std::scoped_lock lock(ctx.collected_msgs.ledger_hpfs_responses_mutex); + + // If max number of state responses reached skip the rest. + if (ctx.collected_msgs.ledger_hpfs_responses.size() < p2p::HPFS_RES_LIST_CAP) + ctx.collected_msgs.ledger_hpfs_responses.push_back(std::make_pair(uniqueid, std::string(msg))); + else + LOG_DEBUG << "Ledger hpfs response rejected. Maximum response count reached. " << display_name(); + } + } + else if (mi.type == p2pmsg::P2PMsgContent_HpfsLogRequest) + { + if (conf::cfg.node.history == conf::HISTORY::FULL) + { + // Check the cap and insert log record request with lock. + std::scoped_lock lock(ctx.collected_msgs.hpfs_log_request_mutex); + + // If max number of log record requests reached, skip the rest. + if (ctx.collected_msgs.hpfs_log_requests.size() < p2p::LOG_RECORD_REQ_LIST_CAP) + { + const p2p::hpfs_log_request hpfs_log_request = p2pmsg::create_hpfs_log_request_from_msg(mi); + ctx.collected_msgs.hpfs_log_requests.push_back(std::make_pair(uniqueid, std::move(hpfs_log_request))); + } + else + LOG_DEBUG << "Hpfs log request rejected. Maximum request count reached. " << display_name(); + } + } + else if (mi.type == p2pmsg::P2PMsgContent_HpfsLogResponse) + { + if (conf::cfg.node.history == conf::HISTORY::FULL && sc::hpfs_log_sync::sync_ctx.is_syncing) + { + // Check the cap and insert log record response with lock. + std::scoped_lock lock(ctx.collected_msgs.hpfs_log_response_mutex); + + // If max number of log record responses reached, skip the rest. + if (ctx.collected_msgs.hpfs_log_responses.size() < p2p::LOG_RECORD_RES_LIST_CAP) + { + const p2p::hpfs_log_response hpfs_log_response = p2pmsg::create_hpfs_log_response_from_msg(mi); + ctx.collected_msgs.hpfs_log_responses.push_back(std::make_pair(uniqueid, std::move(hpfs_log_response))); + } + else + LOG_DEBUG << "Hpfs log response rejected. Maximum response count reached. " << display_name(); + } + } + else + { + increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); + LOG_DEBUG << "Received invalid peer message type [" << mi.type << "]. " << display_name(); + } + return 0; } void peer_comm_session::handle_close() { - p2p::handle_peer_close(*this); + { + // Erase the corresponding pubkey peer connection if it's this session. + std::scoped_lock lock(ctx.peer_connections_mutex); + const auto itr = ctx.peer_connections.find(pubkey); + if (itr != ctx.peer_connections.end() && itr->second == this) + { + ctx.peer_connections.erase(itr); + } + } + + // Update peer properties to default on peer close. + if (known_ipport.has_value()) + p2p::update_known_peer_available_capacity(known_ipport.value(), -1, 0); } + /** + * Logic related to peer sessions on verfied is invoked here. + */ void peer_comm_session::handle_on_verified() { - p2p::handle_peer_on_verified(*this); + // Sending newly verified node the requirement of consensus msg fowarding if this node is weakly connected. + if (status::get_weakly_connected()) + p2p::send_peer_requirement_announcement(true, this); } } // namespace p2p \ No newline at end of file diff --git a/src/p2p/peer_session_handler.cpp b/src/p2p/peer_session_handler.cpp deleted file mode 100644 index a2c604c7..00000000 --- a/src/p2p/peer_session_handler.cpp +++ /dev/null @@ -1,342 +0,0 @@ -#include "../pchheader.hpp" -#include "../conf.hpp" -#include "../consensus.hpp" -#include "../crypto.hpp" -#include "../util/util.hpp" -#include "../util/rollover_hashset.hpp" -#include "../hplog.hpp" -#include "../msg/fbuf/p2pmsg_generated.h" -#include "../msg/fbuf/p2pmsg_conversion.hpp" -#include "../msg/fbuf/common_helpers.hpp" -#include "../ledger/ledger.hpp" -#include "peer_comm_session.hpp" -#include "p2p.hpp" -#include "../unl.hpp" -#include "../sc/hpfs_log_sync.hpp" -#include "../status.hpp" - -namespace p2pmsg = msg::fbuf::p2pmsg; - -namespace p2p -{ - // Max size of messages which are subjected to duplicate message check. - constexpr size_t MAX_SIZE_FOR_DUP_CHECK = 1 * 1024 * 1024; // 1 MB - - // The set of recent peer message hashes used for duplicate detection. - util::rollover_hashset recent_peermsg_hashes(200); - - /** - * This gets hit every time a peer connects to HP via the peer port (configured in config). - * @param session connected session. - * @return returns 0 if connection is successful and peer challenge is sent otherwise, -1. - */ - int handle_peer_connect(p2p::peer_comm_session &session) - { - // Skip new inbound connection if max inbound connection cap is reached. - if (session.is_inbound && calculate_available_capacity() == 0) - { - LOG_DEBUG << "Max peer connection cap reached. Rejecting new peer connection [" << session.display_name() << "]"; - return -1; - } - - // Send peer challenge. - flatbuffers::FlatBufferBuilder fbuf; - p2pmsg::create_msg_from_peer_challenge(fbuf, session.issued_challenge); - std::string_view msg = std::string_view( - reinterpret_cast(fbuf.GetBufferPointer()), fbuf.GetSize()); - session.send(msg); - session.challenge_status = comm::CHALLENGE_ISSUED; - return 0; - } - - /** - * Returns the priority that should be assigned to the message. - * @return 0 if bad message. 1 or 2 if correct priority was assigned. - */ - int get_message_priority(std::string_view message) - { - if (!p2pmsg::verify_peer_message(message)) - { - LOG_DEBUG << "Flatbuffer verify: Bad peer message."; - return 0; - } - - const auto p2p_msg = p2pmsg::GetP2PMsg(message.data()); - const msg::fbuf::p2pmsg::P2PMsgContent type = p2p_msg->content_type(); - - if (type == p2pmsg::P2PMsgContent_ProposalMsg || type == p2pmsg::P2PMsgContent_NonUnlProposalMsg) - return 1; // High priority - else - return 2; // Low priority - } - - /** - * Peer session on message callback method. Validate and handle each type of peer messages. - * @return 0 on normal execution. -1 when session needs to be closed as a result of message handling. - */ - int handle_peer_message(p2p::peer_comm_session &session, std::string_view message) - { - const size_t message_size = message.size(); - // Adding message size to peer message characters(bytes) per minute counter. - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_RAWBYTES_PER_MINUTE, message_size); - - const peer_message_info mi = p2pmsg::get_peer_message_info(message, &session); - if (!mi.p2p_msg) // Message buffer will be null if peer message was too old. - return 0; - - // Messages larger than the duplicate message threshold is ignored from the duplicate message check - // due to the overhead in hash generation for larger messages. - if (message_size <= MAX_SIZE_FOR_DUP_CHECK && !recent_peermsg_hashes.try_emplace(crypto::get_hash(message))) - { - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_DUPMSGS_PER_MINUTE, 1); - LOG_DEBUG << "Duplicate peer message. type:" << mi.type << " from:" << session.display_name(); - return 0; - } - - // Check whether the message is qualified for message forwarding. - if (p2p::validate_for_peer_msg_forwarding(session, mi.type, mi.originated_on)) - { - // Npl messages and consensus proposals are forwarded only to unl nodes if relavent flags (npl and consensus) are set to private. - // If consensus and npl flags are public, these messages are forward to all the connected nodes. - const bool unl_only = (!conf::cfg.contract.is_npl_public && mi.type == p2pmsg::P2PMsgContent_NplMsg) || - (!conf::cfg.contract.is_consensus_public && mi.type == p2pmsg::P2PMsgContent_ProposalMsg); - if (session.need_consensus_msg_forwarding) - { - // Forward messages received by weakly connected nodes to other peers. - p2p::broadcast_message(message, false, false, unl_only, &session); - } - else - { - // Forward message received from other nodes to weakly connected peers. - p2p::broadcast_message(message, false, true, unl_only, &session); - } - } - - if (mi.type == p2pmsg::P2PMsgContent_PeerChallengeMsg) - { - const p2p::peer_challenge chall = p2pmsg::create_peer_challenge_from_msg(mi); - - // Check whether contract ids match. - if (chall.contract_id != conf::cfg.contract.id) - { - LOG_ERROR << "Contract id mismatch. Dropping connection " << session.display_name(); - return -1; - } - - // Remember the time config reported by this peer. - session.reported_time_config = chall.time_config; - - // Whether this node is a full history node or not. - session.is_full_history = chall.is_full_history; - - // Sending the challenge response to the sender. - flatbuffers::FlatBufferBuilder fbuf; - p2pmsg::create_peer_challenge_response_from_challenge(fbuf, chall.challenge); - return session.send(msg::fbuf::builder_to_string_view(fbuf)); - } - else if (mi.type == p2pmsg::P2PMsgContent_PeerChallengeResponseMsg) - { - // Ignore if challenge is already resolved. - if (session.challenge_status == comm::CHALLENGE_ISSUED) - return p2p::resolve_peer_challenge(session, p2pmsg::create_peer_challenge_response_from_msg(mi)); - } - - if (session.challenge_status != comm::CHALLENGE_VERIFIED) - { - LOG_DEBUG << "Cannot accept messages. Peer challenge unresolved. " << session.display_name(); - return 0; - } - - if (mi.type == p2pmsg::P2PMsgContent_PeerListResponseMsg) - { - const std::vector merge_peers = p2pmsg::create_peer_list_response_from_msg(mi); - p2p::merge_peer_list("Peer_Discovery", &merge_peers, NULL, &session); - } - else if (mi.type == p2pmsg::P2PMsgContent_PeerListRequestMsg) - { - p2p::send_known_peer_list(&session); - } - else if (mi.type == p2pmsg::P2PMsgContent_PeerCapacityAnnouncementMsg) - { - if (session.known_ipport.has_value()) - { - const p2p::peer_capacity_announcement ann = p2pmsg::create_peer_capacity_announcement_from_msg(mi); - p2p::update_known_peer_available_capacity(session.known_ipport.value(), ann.available_capacity, ann.timestamp); - } - } - else if (mi.type == p2pmsg::P2PMsgContent_PeerRequirementAnnouncementMsg) - { - const p2p::peer_requirement_announcement ann = p2pmsg::create_peer_requirement_announcement_from_msg(mi); - session.need_consensus_msg_forwarding = ann.need_consensus_msg_forwarding; - LOG_DEBUG << "Peer requirement: " << session.display_name() << " consensus msg forwarding:" << ann.need_consensus_msg_forwarding; - } - else if (mi.type == p2pmsg::P2PMsgContent_NonUnlProposalMsg) - { - handle_nonunl_proposal_message(p2pmsg::create_nonunl_proposal_from_msg(mi)); - } - else if (mi.type == p2pmsg::P2PMsgContent_ProposalMsg) - { - const util::h32 hash = p2pmsg::verify_proposal_msg_trust(mi); - if (hash == util::h32_empty) - { - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_BADSIGMSGS_PER_MINUTE, 1); - LOG_DEBUG << "Proposal rejected due to trust failure. " << session.display_name(); - return 0; - } - - handle_proposal_message(p2pmsg::create_proposal_from_msg(mi, hash)); - } - else if (mi.type == p2pmsg::P2PMsgContent_NplMsg) - { - if (!p2pmsg::verify_npl_msg_trust(mi)) - { - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_BADSIGMSGS_PER_MINUTE, 1); - LOG_DEBUG << "Npl message rejected due to trust failure. " << session.display_name(); - return 0; - } - - handle_npl_message(p2pmsg::create_npl_from_msg(mi)); - } - else if (mi.type == p2pmsg::P2PMsgContent_HpfsRequestMsg) - { - const p2p::hpfs_request hr = p2pmsg::create_hpfs_request_from_msg(mi); - if (hr.mount_id == sc::contract_fs.mount_id) - { - // Check the cap and insert request with lock. - std::scoped_lock lock(ctx.collected_msgs.contract_hpfs_requests_mutex); - - // If max number of state requests reached skip the rest. - if (ctx.collected_msgs.contract_hpfs_requests.size() < p2p::HPFS_REQ_LIST_CAP) - ctx.collected_msgs.contract_hpfs_requests.push_back(std::make_pair(session.pubkey, std::move(hr))); - else - LOG_DEBUG << "Hpfs contract fs request rejected. Maximum hpfs contract fs request count reached. " << session.display_name(); - } - else if (hr.mount_id == ledger::ledger_fs.mount_id) - { - // Check the cap and insert request with lock. - std::scoped_lock lock(ctx.collected_msgs.ledger_hpfs_requests_mutex); - - // If max number of state requests reached skip the rest. - if (ctx.collected_msgs.ledger_hpfs_requests.size() < p2p::HPFS_REQ_LIST_CAP) - ctx.collected_msgs.ledger_hpfs_requests.push_back(std::make_pair(session.pubkey, std::move(hr))); - else - LOG_DEBUG << "Hpfs ledger fs request rejected. Maximum hpfs ledger fs request count reached. " << session.display_name(); - } - } - else if (mi.type == p2pmsg::P2PMsgContent_HpfsResponseMsg) - { - const p2pmsg::HpfsResponseMsg &resp_msg = *mi.p2p_msg->content_as_HpfsResponseMsg(); - - // Only accept hpfs responses if hpfs fs is syncing. - if (sc::contract_sync_worker.is_syncing && resp_msg.mount_id() == sc::contract_fs.mount_id) - { - // Check the cap and insert state_response with lock. - std::scoped_lock lock(ctx.collected_msgs.contract_hpfs_responses_mutex); - - // If max number of state responses reached skip the rest. - if (ctx.collected_msgs.contract_hpfs_responses.size() < p2p::HPFS_RES_LIST_CAP) - ctx.collected_msgs.contract_hpfs_responses.push_back(std::make_pair(session.uniqueid, std::string(message))); - else - LOG_DEBUG << "Contract hpfs response rejected. Maximum response count reached. " << session.display_name(); - } - else if (ledger::ledger_sync_worker.is_syncing && resp_msg.mount_id() == ledger::ledger_fs.mount_id) - { - // Check the cap and insert state_response with lock. - std::scoped_lock lock(ctx.collected_msgs.ledger_hpfs_responses_mutex); - - // If max number of state responses reached skip the rest. - if (ctx.collected_msgs.ledger_hpfs_responses.size() < p2p::HPFS_RES_LIST_CAP) - ctx.collected_msgs.ledger_hpfs_responses.push_back(std::make_pair(session.uniqueid, std::string(message))); - else - LOG_DEBUG << "Ledger hpfs response rejected. Maximum response count reached. " << session.display_name(); - } - } - else if (mi.type == p2pmsg::P2PMsgContent_HpfsLogRequest) - { - if (conf::cfg.node.history == conf::HISTORY::FULL) - { - // Check the cap and insert log record request with lock. - std::scoped_lock lock(ctx.collected_msgs.hpfs_log_request_mutex); - - // If max number of log record requests reached, skip the rest. - if (ctx.collected_msgs.hpfs_log_requests.size() < p2p::LOG_RECORD_REQ_LIST_CAP) - { - const p2p::hpfs_log_request hpfs_log_request = p2pmsg::create_hpfs_log_request_from_msg(mi); - ctx.collected_msgs.hpfs_log_requests.push_back(std::make_pair(session.uniqueid, std::move(hpfs_log_request))); - } - else - LOG_DEBUG << "Hpfs log request rejected. Maximum request count reached. " << session.display_name(); - } - } - else if (mi.type == p2pmsg::P2PMsgContent_HpfsLogResponse) - { - if (conf::cfg.node.history == conf::HISTORY::FULL && sc::hpfs_log_sync::sync_ctx.is_syncing) - { - // Check the cap and insert log record response with lock. - std::scoped_lock lock(ctx.collected_msgs.hpfs_log_response_mutex); - - // If max number of log record responses reached, skip the rest. - if (ctx.collected_msgs.hpfs_log_responses.size() < p2p::LOG_RECORD_RES_LIST_CAP) - { - const p2p::hpfs_log_response hpfs_log_response = p2pmsg::create_hpfs_log_response_from_msg(mi); - ctx.collected_msgs.hpfs_log_responses.push_back(std::make_pair(session.uniqueid, std::move(hpfs_log_response))); - } - else - LOG_DEBUG << "Hpfs log response rejected. Maximum response count reached. " << session.display_name(); - } - } - else - { - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); - LOG_DEBUG << "Received invalid peer message type [" << mi.type << "]. " << session.display_name(); - } - return 0; - } - - /** - * Handles messages that we receive from ourselves. - */ - int handle_self_message(std::string_view message) - { - const peer_message_info mi = p2pmsg::get_peer_message_info(message); - - if (mi.type == p2pmsg::P2PMsgContent_ProposalMsg) - handle_proposal_message(p2pmsg::create_proposal_from_msg(mi, hash_proposal_msg(*mi.p2p_msg->content_as_ProposalMsg()))); - else if (mi.type == p2pmsg::P2PMsgContent_NonUnlProposalMsg) - handle_nonunl_proposal_message(p2pmsg::create_nonunl_proposal_from_msg(mi)); - else if (mi.type == p2pmsg::P2PMsgContent_NplMsg) - handle_npl_message(p2pmsg::create_npl_from_msg(mi)); - - return 0; - } - - //peer session on message callback method - int handle_peer_close(const p2p::peer_comm_session &session) - { - { - // Erase the corresponding pubkey peer connection if it's this session. - std::scoped_lock lock(ctx.peer_connections_mutex); - const auto itr = ctx.peer_connections.find(session.pubkey); - if (itr != ctx.peer_connections.end() && itr->second == &session) - { - ctx.peer_connections.erase(itr); - } - } - - // Update peer properties to default on peer close. - if (session.known_ipport.has_value()) - p2p::update_known_peer_available_capacity(session.known_ipport.value(), -1, 0); - - return 0; - } - - /** - * Logic related to peer sessions on verfied is invoked here. - */ - void handle_peer_on_verified(p2p::peer_comm_session &session) - { - // Sending newly verified node the requirement of consensus msg fowarding if this node is weakly connected. - if (status::get_weakly_connected()) - p2p::send_peer_requirement_announcement(true, &session); - } -} // namespace p2p \ No newline at end of file diff --git a/src/p2p/peer_session_handler.hpp b/src/p2p/peer_session_handler.hpp deleted file mode 100644 index 3757275c..00000000 --- a/src/p2p/peer_session_handler.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#ifndef _HP_P2P_PEER_SESSION_HANDLER_ -#define _HP_P2P_PEER_SESSION_HANDLER_ - -#include "../pchheader.hpp" -#include "peer_comm_session.hpp" - -namespace p2p -{ - int handle_peer_connect(p2p::peer_comm_session &session); - int get_message_priority(std::string_view message); - int handle_peer_message(p2p::peer_comm_session &session, std::string_view message); - int handle_self_message(std::string_view message); - int handle_peer_close(const p2p::peer_comm_session &session); - void handle_peer_on_verified(p2p::peer_comm_session &session); - -} // namespace p2p -#endif \ No newline at end of file diff --git a/src/p2p/self_node.cpp b/src/p2p/self_node.cpp index 33cb66e6..64414947 100644 --- a/src/p2p/self_node.cpp +++ b/src/p2p/self_node.cpp @@ -1,5 +1,11 @@ #include "../pchheader.hpp" -#include "peer_session_handler.hpp" +#include "../conf.hpp" +#include "p2p.hpp" +#include "../msg/fbuf/p2pmsg_generated.h" +#include "../msg/fbuf/p2pmsg_conversion.hpp" +#include "../msg/fbuf/common_helpers.hpp" + +namespace p2pmsg = msg::fbuf::p2pmsg; namespace p2p::self { @@ -16,7 +22,17 @@ namespace p2p::self { std::string msg; if (msg_queue.try_dequeue(msg)) - return p2p::handle_self_message(msg); + { + // Handle the message we received from ourselves. + const peer_message_info mi = p2pmsg::get_peer_message_info(msg); + + if (mi.type == p2pmsg::P2PMsgContent_ProposalMsg) + handle_proposal_message(p2pmsg::create_proposal_from_msg(mi, hash_proposal_msg(*mi.p2p_msg->content_as_ProposalMsg()))); + else if (mi.type == p2pmsg::P2PMsgContent_NonUnlProposalMsg) + handle_nonunl_proposal_message(p2pmsg::create_nonunl_proposal_from_msg(mi)); + else if (mi.type == p2pmsg::P2PMsgContent_NplMsg) + handle_npl_message(p2pmsg::create_npl_from_msg(mi)); + } return 0; } diff --git a/src/usr/user_comm_session.cpp b/src/usr/user_comm_session.cpp index c4d19fdc..92ffa86f 100644 --- a/src/usr/user_comm_session.cpp +++ b/src/usr/user_comm_session.cpp @@ -1,24 +1,98 @@ #include "../pchheader.hpp" #include "../util/util.hpp" +#include "../msg/json/usrmsg_json.hpp" #include "user_comm_session.hpp" -#include "user_session_handler.hpp" +#include "usr.hpp" + +namespace jusrmsg = msg::usrmsg::json; namespace usr { + /** + * This gets hit every time a client connects to HP via the public port (configured in config). + * @return returns 0 if connection is successful and user challenge is sent, otherwise -1. + */ int user_comm_session::handle_connect() { - return usr::handle_user_connect(*this); + // Allow connection only if the maximum capacity is not reached. 0 means allowing unlimited connections. + if ((conf::cfg.user.max_connections == 0) || (ctx.users.size() < conf::cfg.user.max_connections)) + { + LOG_DEBUG << "User client connected " << display_name(); + + // As soon as a user connects, we issue them a challenge message. We remember the + // challenge we issued and later verify the user's response with it. + std::vector msg; + jusrmsg::create_user_challenge(msg, issued_challenge); + send(msg); + + // Set the challenge-issued value to true. + challenge_status = comm::CHALLENGE_STATUS::CHALLENGE_ISSUED; + return 0; + } + else + { + LOG_DEBUG << "Dropping the user connection. Maximum user capacity reached. [" << display_name() << "] (limit: " << conf::cfg.user.max_connections << ")."; + return -1; + } } + /** + * This gets hit every time we receive some data from a client connected to the HP public port. + */ int user_comm_session::handle_message(std::string_view msg) { - return usr::handle_user_message(*this, msg); + // Adding message size to user message characters(bytes) per minute counter. + increment_metric(comm::SESSION_THRESHOLDS::MAX_RAWBYTES_PER_MINUTE, msg.size()); + + // First check whether this session is pending challenge. + // Meaning we have previously issued a challenge to the client. + if (challenge_status == comm::CHALLENGE_STATUS::CHALLENGE_ISSUED) + { + if (verify_challenge(msg, *this) == 0) + return 0; + + LOG_DEBUG << "User challenge verification failed. " << display_name(); + } + // Check whether this session belongs to an authenticated (challenge-verified) user. + else if (challenge_status == comm::CHALLENGE_STATUS::CHALLENGE_VERIFIED) + { + // Check whether this user is among authenticated users + // and perform authenticated msg processing. + + const auto itr = ctx.users.find(pubkey); + if (itr != ctx.users.end()) + { + // This is an authed user. + connected_user &user = itr->second; + if (handle_authed_user_message(user, msg) != 0) + { + increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); + LOG_DEBUG << "Bad message from user " << display_name(); + } + } + else + { + increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); + LOG_DEBUG << "User session id not found: " << display_name(); + } + + return 0; + } + + // If for any reason we reach this point, we should drop the connection because none of the + // valid cases match. + LOG_DEBUG << "Dropping the user connection " << display_name(); + return -1; } + /** + * This gets hit every time a client disconnects from the HP public port. + */ void user_comm_session::handle_close() { - usr::handle_user_close(*this); + // Session belongs to an authed user. + if (challenge_status == comm::CHALLENGE_STATUS::CHALLENGE_VERIFIED) + remove_user(pubkey); } - } // namespace usr \ No newline at end of file diff --git a/src/usr/user_session_handler.cpp b/src/usr/user_session_handler.cpp deleted file mode 100644 index 0dcde16b..00000000 --- a/src/usr/user_session_handler.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "../pchheader.hpp" -#include "../hplog.hpp" -#include "../msg/json/usrmsg_json.hpp" -#include "../bill/corebill.h" -#include "usr.hpp" -#include "user_session_handler.hpp" -#include "user_comm_session.hpp" - -namespace jusrmsg = msg::usrmsg::json; - -namespace usr -{ - /** - * This gets hit every time a client connects to HP via the public port (configured in config). - * @param session connected session. - * @return returns 0 if connection is successful and user challenge is sent, otherwise -1. - */ - int handle_user_connect(usr::user_comm_session &session) - { - // Allow connection only if the maximum capacity is not reached. 0 means allowing unlimited connections. - if ((conf::cfg.user.max_connections == 0) || (usr::ctx.users.size() < conf::cfg.user.max_connections)) - { - LOG_DEBUG << "User client connected " << session.display_name(); - - // As soon as a user connects, we issue them a challenge message. We remember the - // challenge we issued and later verify the user's response with it. - std::vector msg; - jusrmsg::create_user_challenge(msg, session.issued_challenge); - session.send(msg); - - // Set the challenge-issued value to true. - session.challenge_status = comm::CHALLENGE_ISSUED; - return 0; - } - else - { - LOG_DEBUG << "Dropping the user connection. Maximum user capacity reached. Session: " << session.display_name() << " (limit: " << conf::cfg.user.max_connections << ")."; - return -1; - } - } - - /** - * This gets hit every time we receive some data from a client connected to the HP public port. - */ - int handle_user_message(usr::user_comm_session &session, std::string_view message) - { - // Adding message size to user message characters(bytes) per minute counter. - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_RAWBYTES_PER_MINUTE, message.size()); - - // First check whether this session is pending challenge. - // Meaning we have previously issued a challenge to the client. - if (session.challenge_status == comm::CHALLENGE_ISSUED) - { - if (verify_challenge(message, session) == 0) - return 0; - } - // Check whether this session belongs to an authenticated (challenge-verified) user. - else if (session.challenge_status == comm::CHALLENGE_VERIFIED) - { - // Check whether this user is among authenticated users - // and perform authenticated msg processing. - - const auto itr = ctx.users.find(session.pubkey); - if (itr != ctx.users.end()) - { - // This is an authed user. - connected_user &user = itr->second; - if (handle_authed_user_message(user, message) != 0) - { - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); - LOG_DEBUG << "Bad message from user " << session.display_name(); - } - } - else - { - session.increment_metric(comm::SESSION_THRESHOLDS::MAX_BADMSGS_PER_MINUTE, 1); - LOG_DEBUG << "User session id not found: " << session.display_name(); - } - - return 0; - } - - // If for any reason we reach this point, we should drop the connection because none of the - // valid cases match. - LOG_DEBUG << "Dropping the user connection " << session.display_name(); - corebill::report_violation(session.host_address); - return -1; - } - - /** - * This gets hit every time a client disconnects from the HP public port. - */ - int handle_user_close(const usr::user_comm_session &session) - { - // Session belongs to an authed user. - if (session.challenge_status == comm::CHALLENGE_VERIFIED) - remove_user(session.pubkey); - - return 0; - } - -} // namespace usr \ No newline at end of file diff --git a/src/usr/user_session_handler.hpp b/src/usr/user_session_handler.hpp deleted file mode 100644 index 3c72590b..00000000 --- a/src/usr/user_session_handler.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef _HP_USER_SESSION_HANDLER_ -#define _HP_USER_SESSION_HANDLER_ - -#include "../pchheader.hpp" -#include "user_comm_session.hpp" - -namespace usr -{ - int handle_user_connect(usr::user_comm_session &session); - int handle_user_message(usr::user_comm_session &session, std::string_view message); - int handle_user_close(const usr::user_comm_session &session); - -} // namespace usr - -#endif \ No newline at end of file diff --git a/src/usr/usr.cpp b/src/usr/usr.cpp index 0c1a4190..564ef771 100644 --- a/src/usr/usr.cpp +++ b/src/usr/usr.cpp @@ -12,7 +12,6 @@ #include "../hpfs/hpfs_mount.hpp" #include "../status.hpp" #include "usr.hpp" -#include "user_session_handler.hpp" #include "user_comm_session.hpp" #include "user_comm_server.hpp" #include "user_input.hpp" diff --git a/src/usr/usr.hpp b/src/usr/usr.hpp index 313adec0..90ae1d11 100644 --- a/src/usr/usr.hpp +++ b/src/usr/usr.hpp @@ -9,7 +9,6 @@ #include "../msg/usrmsg_parser.hpp" #include "user_comm_session.hpp" #include "user_comm_server.hpp" -#include "user_session_handler.hpp" #include "user_input.hpp" #include "user_common.hpp" diff --git a/test/bin/hpws b/test/bin/hpws index 26dd64b3eac2bab7bb9b37d8742a1264d9411c99..91634d23613fc2b8c29a69f966eaeeecfb4749e0 100755 GIT binary patch literal 43656 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 literal 43656 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