diff --git a/src/common/data/channel_async.h b/src/common/data/channel_async.h index b14d2338..b150e2a9 100644 --- a/src/common/data/channel_async.h +++ b/src/common/data/channel_async.h @@ -100,15 +100,6 @@ class ChannelAsync { co_await timer.async_wait(boost::asio::use_awaitable); - // while (!mutex_.try_lock()) { - // if (std::chrono::steady_clock::now() - start > - // std::chrono::seconds(3)) { - // spdlog::error("Session::send: failed to acquire lock within - // timeout"); co_return false; - // } - // std::this_thread::yield(); // Yield to avoid busy waiting - // } - co_return; } diff --git a/src/common/network/ip_address.h b/src/common/network/ip_address.h index 412f2bdc..a5629a2a 100644 --- a/src/common/network/ip_address.h +++ b/src/common/network/ip_address.h @@ -134,6 +134,11 @@ class IPv4Address : public IPAddress { return *this; } + IPv4Address& operator=(const std::string& ip) { + *this = IPv4Address(ip); + return *this; + } + static IPv4Address Create(const std::string& ip) { return IPv4Address(ip); } static IPv4Address Create(const boost::asio::ip::address& ip_addr) { @@ -203,6 +208,11 @@ class IPv6Address : public IPAddress { return *this; } + IPv6Address& operator=(const std::string& ip) { + *this = IPv6Address(ip); + return *this; + } + static IPv6Address Create(const std::string& ip) { return IPv6Address(ip); } static IPv6Address Create(const boost::asio::ip::address& ip_addr) { @@ -315,6 +325,16 @@ class IPv4Address : public IPAddress { return *this; } + IPv4Address& operator=(const pcpp::IPv4Address& other) { + *this = IPv4Address(other); + return *this; + } + + IPv4Address& operator=(const std::string& ip) { + *this = IPv4Address(ip); + return *this; + } + static IPv4Address Create(std::string ip) { return IPv4Address(std::move(ip)); } @@ -365,6 +385,16 @@ class IPv6Address : public IPAddress { return *this; } + IPv6Address& operator=(const pcpp::IPv6Address& other) { + *this = IPv6Address(other); + return *this; + } + + IPv6Address& operator=(const std::string& ip) { + *this = IPv6Address(ip); + return *this; + } + static IPv6Address Create(std::string ip) { return IPv6Address(std::move(ip)); } diff --git a/src/common/network/ip_packet.h b/src/common/network/ip_packet.h index 8a70e72d..2f91df3b 100644 --- a/src/common/network/ip_packet.h +++ b/src/common/network/ip_packet.h @@ -164,8 +164,10 @@ class IPPacket { parsed_packet_ = pcpp::Packet(&raw_packet_, false); if (pcpp::LINKTYPE_IPV4 == ip_type) { ipv4_layer_ = parsed_packet_.getLayerOfType(); + ipv4_ = ipv4_layer_->getDstIPv4Address(); } else if (pcpp::LINKTYPE_IPV6 == ip_type) { ipv6_layer_ = parsed_packet_.getLayerOfType(); + ipv6_ = ipv6_layer_->getDstIPv6Address(); } } catch (const std::exception& e) { SPDLOG_WARN( @@ -197,6 +199,14 @@ class IPPacket { } } + fptn::common::network::IPv4Address DstIPv4Address() const noexcept { + return ipv4_; + } + + fptn::common::network::IPv6Address DstIPv6Address() const noexcept { + return ipv6_; + } + void SetClientId(fptn::ClientID client_id) noexcept { client_id_ = client_id; } @@ -391,6 +401,9 @@ class IPPacket { pcpp::IPv4Layer* ipv4_layer_ = nullptr; pcpp::IPv6Layer* ipv6_layer_ = nullptr; + + fptn::common::network::IPv4Address ipv4_; + fptn::common::network::IPv6Address ipv6_; }; using IPPacketPtr = std::unique_ptr; diff --git a/src/common/utils/utils.h b/src/common/utils/utils.h index 56da81a5..918badfb 100644 --- a/src/common/utils/utils.h +++ b/src/common/utils/utils.h @@ -20,11 +20,11 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include namespace fptn::common::utils { -inline std::string GenerateRandomString(int length) { +inline std::string GenerateRandomString(const int length) { const std::string characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - std::mt19937 gen{std::random_device {}()}; + std::mt19937 gen{std::random_device{}()}; std::uniform_int_distribution dist(0, characters.size() - 1); std::string result; @@ -77,4 +77,14 @@ inline std::string ToLowerCase(const std::string& str) { return str; } +inline std::string FilterDigitsOnly(const std::string& input) { + std::string result; + result.reserve(input.size()); + + std::ranges::copy_if(input, std::back_inserter(result), + [](const unsigned char c) { return std::isdigit(c); }); + + return result; +} + } // namespace fptn::common::utils diff --git a/src/common/utils/uuid4.h b/src/common/utils/uuid4.h new file mode 100644 index 00000000..08325c8f --- /dev/null +++ b/src/common/utils/uuid4.h @@ -0,0 +1,23 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include + +#include +#include +#include + +namespace fptn::common::utils { + +inline std::string GenerateUUID4() { + boost::uuids::random_generator generator; + const boost::uuids::uuid uuid = generator(); + return boost::uuids::to_string(uuid); +} + +} // namespace fptn::common::utils diff --git a/src/fptn-client/CMakeLists.txt b/src/fptn-client/CMakeLists.txt index b5d1d565..35f5e51a 100644 --- a/src/fptn-client/CMakeLists.txt +++ b/src/fptn-client/CMakeLists.txt @@ -35,8 +35,6 @@ add_executable( fptn-client-cli.cpp vpn/vpn_client.h vpn/vpn_client.cpp - vpn/http/client.h - vpn/http/client.cpp routing/route_manager.h routing/route_manager.cpp config/config_file.cpp @@ -124,8 +122,6 @@ WarningsAsErrors: '' fptn-client-gui.cpp vpn/vpn_client.h vpn/vpn_client.cpp - vpn/http/client.h - vpn/http/client.cpp routing/route_manager.h routing/route_manager.cpp config/config_file.cpp diff --git a/src/fptn-client/config/config_file.cpp b/src/fptn-client/config/config_file.cpp index 208889e8..638717a5 100644 --- a/src/fptn-client/config/config_file.cpp +++ b/src/fptn-client/config/config_file.cpp @@ -22,14 +22,14 @@ using fptn::utils::speed_estimator::ServerInfo; namespace fptn::config { ConfigFile::ConfigFile(std::string sni, - fptn::protocol::https::CensorshipStrategy censorship_strategy) + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy) : sni_(std::move(sni)), censorship_strategy_(censorship_strategy), version_(0) {} // NOLINT(whitespace/indent_namespace) ConfigFile::ConfigFile(std::string token, std::string sni, - fptn::protocol::https::CensorshipStrategy censorship_strategy) + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy) : token_(std::move(token)), sni_(std::move(sni)), censorship_strategy_(censorship_strategy), diff --git a/src/fptn-client/config/config_file.h b/src/fptn-client/config/config_file.h index b989745d..b99799db 100644 --- a/src/fptn-client/config/config_file.h +++ b/src/fptn-client/config/config_file.h @@ -13,16 +13,15 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "fptn-client/utils/speed_estimator/server_info.h" #include "fptn-client/utils/speed_estimator/speed_estimator.h" -#include "fptn-protocol-lib/https/censorship_strategy.h" namespace fptn::config { class ConfigFile final { public: explicit ConfigFile(std::string sni, - fptn::protocol::https::CensorshipStrategy censorship_strategy); + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy); explicit ConfigFile(std::string token, std::string sni, - fptn::protocol::https::CensorshipStrategy censorship_strategy); + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy); bool Parse(); fptn::utils::speed_estimator::ServerInfo FindFastestServer( @@ -48,7 +47,7 @@ class ConfigFile final { private: const std::string token_; const std::string sni_; - const fptn::protocol::https::CensorshipStrategy censorship_strategy_; + const fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy_; int version_; std::string service_name_; diff --git a/src/fptn-client/fptn-client-cli.cpp b/src/fptn-client/fptn-client-cli.cpp index 0058f1eb..eb7f157e 100644 --- a/src/fptn-client/fptn-client-cli.cpp +++ b/src/fptn-client/fptn-client-cli.cpp @@ -25,6 +25,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "common/network/net_interface.h" #include "config/config_file.h" +#include "fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h" #include "fptn-protocol-lib/https/obfuscator/methods/detector.h" #include "fptn-protocol-lib/time/time_provider.h" #include "plugins/blacklist/domain_blacklist.h" @@ -224,14 +225,14 @@ int main(int argc, char* argv[]) { } const auto bypass_method = args.get("--bypass-method"); - fptn::protocol::https::CensorshipStrategy censorship_strategy = - fptn::protocol::https::CensorshipStrategy::kSni; + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy = + fptn::protocol::https::HttpsInitConnectionStrategy::kSni; if (bypass_method == "obfuscation") { censorship_strategy = - fptn::protocol::https::CensorshipStrategy::kTlsObfuscator; + fptn::protocol::https::HttpsInitConnectionStrategy::kTlsObfuscator; } else if (bypass_method == "sni-reality") { censorship_strategy = - fptn::protocol::https::CensorshipStrategy::kSniRealityMode; + fptn::protocol::https::HttpsInitConnectionStrategy::kSniRealityMode; } /* parse network lists */ @@ -314,17 +315,28 @@ int main(int argc, char* argv[]) { split_domains_str, blacklist_domains_str); /* auth & dns */ - auto http_client = std::make_unique(server_ip, - selected_server.port, tun_interface_address_ipv4, - tun_interface_address_ipv6, sni, selected_server.md5_fingerprint, - censorship_strategy); + const auto strategy = fptn::protocol::connection::strategies:: + ConnectionStrategy::kLongTermConnection; + auto connection_manager = + fptn::protocol::connection::ConnectionManagerBuilder() + .SetConnectionStrategyType(strategy) + .SetServer(server_ip, selected_server.port) + .SetTunInterface( + tun_interface_address_ipv4, tun_interface_address_ipv6) + .SetSNI(sni) + .SetServerFingerprint(selected_server.md5_fingerprint) + .SetCensorshipStrategy(censorship_strategy) + .SetMaxReconnections(5) + .SetConnectionTimeout(10000) + .Build(); + const bool status = - http_client->Login(config.GetUsername(), config.GetPassword()); + connection_manager->Login(config.GetUsername(), config.GetPassword()); if (!status) { SPDLOG_ERROR("The username or password you entered is incorrect"); return EXIT_FAILURE; } - const auto [dnsServerIPv4, dnsServerIPv6] = http_client->GetDns(); + const auto [dnsServerIPv4, dnsServerIPv6] = connection_manager->GetDns(); if (dnsServerIPv4.IsEmpty() || dnsServerIPv6.IsEmpty()) { SPDLOG_ERROR("DNS server error! Check your connection!"); return EXIT_FAILURE; @@ -364,9 +376,8 @@ int main(int argc, char* argv[]) { } /* vpn client */ - fptn::vpn::VpnClient vpn_client(std::move(http_client), - std::move(virtual_network_interface), dnsServerIPv4, dnsServerIPv6, - std::move(client_plugins)); + fptn::vpn::VpnClient vpn_client(std::move(connection_manager), + std::move(virtual_network_interface), std::move(client_plugins)); vpn_client.Start(); // Wait for the WebSocket tunnel to establish diff --git a/src/fptn-client/fptn-client-gui.cpp b/src/fptn-client/fptn-client-gui.cpp index fbed1c34..33a8584e 100644 --- a/src/fptn-client/fptn-client-gui.cpp +++ b/src/fptn-client/fptn-client-gui.cpp @@ -63,10 +63,10 @@ int main(int argc, char* argv[]) { // Setup signal handler #if defined(__APPLE__) || defined(__linux__) - std::signal(SIGINT, SignalHandler); - std::signal(SIGHUP, SignalHandler); - std::signal(SIGTERM, SignalHandler); - std::signal(SIGQUIT, SignalHandler); + ::signal(SIGINT, SignalHandler); + ::signal(SIGHUP, SignalHandler); + ::signal(SIGTERM, SignalHandler); + ::signal(SIGQUIT, SignalHandler); #if __linux__ std::signal(SIGPWR, SignalHandler); #endif diff --git a/src/fptn-client/gui/sni_autoscan_dialog/sni_autoscan_dialog.cpp b/src/fptn-client/gui/sni_autoscan_dialog/sni_autoscan_dialog.cpp index 3867e036..1472c64d 100644 --- a/src/fptn-client/gui/sni_autoscan_dialog/sni_autoscan_dialog.cpp +++ b/src/fptn-client/gui/sni_autoscan_dialog/sni_autoscan_dialog.cpp @@ -247,7 +247,7 @@ void SniAutoscanDialog::WorkerThread(int thread_id) { constexpr int kHandshakeTimeout = 2; fptn::protocol::https::ApiClient client(server.host.toStdString(), server.port, sni, server.md5_fingerprint.toStdString(), - protocol::https::CensorshipStrategy::kSni); + protocol::https::HttpsInitConnectionStrategy::kSni); handshake_ok = client.TestHandshake(kHandshakeTimeout); if (handshake_ok) { diff --git a/src/fptn-client/gui/tray/tray.cpp b/src/fptn-client/gui/tray/tray.cpp index 12fefa45..62f5d995 100644 --- a/src/fptn-client/gui/tray/tray.cpp +++ b/src/fptn-client/gui/tray/tray.cpp @@ -27,6 +27,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "common/network/ip_packet.h" #include "common/system/command.h" +#include "fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" #include "fptn-protocol-lib/time/time_provider.h" #include "gui/autoupdate/autoupdate.h" @@ -726,16 +727,16 @@ bool TrayApp::startVpn(QString& err_msg) { const std::string sni = !settings_->SNI().isEmpty() ? settings_->SNI().toStdString() : FPTN_DEFAULT_SNI; - fptn::protocol::https::CensorshipStrategy censorship_strategy = - fptn::protocol::https::CensorshipStrategy::kSni; + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy = + fptn::protocol::https::HttpsInitConnectionStrategy::kSni; if (settings_->BypassMethod() == "OBFUSCATION") { SPDLOG_INFO("Using obfuscation to bypass censorship"); censorship_strategy = - fptn::protocol::https::CensorshipStrategy::kTlsObfuscator; + fptn::protocol::https::HttpsInitConnectionStrategy::kTlsObfuscator; } else if (settings_->BypassMethod() == "SNI-REALITY") { SPDLOG_INFO("Using reality mode to bypass censorship"); censorship_strategy = - fptn::protocol::https::CensorshipStrategy::kSniRealityMode; + fptn::protocol::https::HttpsInitConnectionStrategy::kSniRealityMode; } else { SPDLOG_INFO("Using SNI spoofing to bypass censorship"); } @@ -783,15 +784,27 @@ bool TrayApp::startVpn(QString& err_msg) { return false; } - auto http_client = std::make_unique(server_ip, - selected_server_.port, tun_interface_address_ipv4, - tun_interface_address_ipv6, sni, selected_server_.md5_fingerprint, - censorship_strategy); - // login - bool login_status = - http_client->Login(selected_server_.username, selected_server_.password); + // const auto strategy = fptn::protocol::connection::strategies:: + // ConnectionStrategy::kLongTermConnection; + const auto strategy = fptn::protocol::connection::strategies:: + ConnectionStrategy::kConnectionPool; + auto connection_manager = + fptn::protocol::connection::ConnectionManagerBuilder() + .SetConnectionStrategyType(strategy) + .SetServer(server_ip, selected_server_.port) + .SetTunInterface( + tun_interface_address_ipv4, tun_interface_address_ipv6) + .SetSNI(sni) + .SetServerFingerprint(selected_server_.md5_fingerprint) + .SetCensorshipStrategy(censorship_strategy) + .SetMaxReconnections(5) + .SetConnectionTimeout(10000) + .Build(); + + const bool login_status = connection_manager->Login( + selected_server_.username, selected_server_.password); if (!login_status) { - const std::string error = http_client->LatestError(); + const std::string error = connection_manager->LatestError(); err_msg = QObject::tr( "Unable to connect to the server. Please use the Telegram " "bot to generate a new TOKEN with your personal settings, " @@ -802,9 +815,9 @@ bool TrayApp::startVpn(QString& err_msg) { } // get dns - const auto [dns_server_ipv4, dns_server_ipv6] = http_client->GetDns(); + const auto [dns_server_ipv4, dns_server_ipv6] = connection_manager->GetDns(); if (dns_server_ipv4.IsEmpty() || dns_server_ipv6.IsEmpty()) { - const std::string error = http_client->LatestError(); + const std::string error = connection_manager->LatestError(); err_msg = QObject::tr("DNS server error! Check your connection!") + "\n\n" + QObject::tr("Error message: ") + QString::fromStdString(error); return false; @@ -859,10 +872,10 @@ bool TrayApp::startVpn(QString& err_msg) { 126 // IPv6 netmask }); - // setup vpn client с плагинами - vpn_client_ = std::make_unique(std::move(http_client), - std::move(virtual_network_interface), dns_server_ipv4, dns_server_ipv6, - std::move(client_plugins)); + // vpn client + vpn_client_ = + std::make_unique(std::move(connection_manager), + std::move(virtual_network_interface), std::move(client_plugins)); // Wait for the WebSocket tunnel to establish vpn_client_->Start(); diff --git a/src/fptn-client/gui/tray/tray.h b/src/fptn-client/gui/tray/tray.h index 6d82b6ab..b2a58b97 100644 --- a/src/fptn-client/gui/tray/tray.h +++ b/src/fptn-client/gui/tray/tray.h @@ -32,7 +32,6 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "gui/tray/tray.h" #include "routing/route_manager.h" #include "utils/speed_estimator/server_info.h" -#include "vpn/http/client.h" #include "vpn/vpn_client.h" namespace fptn::gui { @@ -119,6 +118,5 @@ class TrayApp : public QWidget { // connecting std::atomic connecting_in_progress_{false}; - // std::future connecting_; }; } // namespace fptn::gui diff --git a/src/fptn-client/utils/speed_estimator/speed_estimator.cpp b/src/fptn-client/utils/speed_estimator/speed_estimator.cpp index 633c02ac..85f79f58 100644 --- a/src/fptn-client/utils/speed_estimator/speed_estimator.cpp +++ b/src/fptn-client/utils/speed_estimator/speed_estimator.cpp @@ -32,7 +32,7 @@ std::uint64_t GetDownloadTimeMs(const ServerInfo& server, const std::string& sni, int timeout, const std::string& md5_fingerprint, - fptn::protocol::https::CensorshipStrategy censorship_strategy) { + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy) { try { auto const start = std::chrono::high_resolution_clock::now(); ApiClient cli( @@ -55,7 +55,7 @@ std::uint64_t GetDownloadTimeMs(const ServerInfo& server, ServerInfo FindFastestServer(const std::string& sni, const std::vector& servers, - fptn::protocol::https::CensorshipStrategy censorship_strategy, + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy, int timeout_sec) { // randomly select half of the servers std::vector shuffled_servers = servers; diff --git a/src/fptn-client/utils/speed_estimator/speed_estimator.h b/src/fptn-client/utils/speed_estimator/speed_estimator.h index b4a03f30..f9bd0fb1 100644 --- a/src/fptn-client/utils/speed_estimator/speed_estimator.h +++ b/src/fptn-client/utils/speed_estimator/speed_estimator.h @@ -11,7 +11,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include "fptn-client/utils/speed_estimator/server_info.h" -#include "fptn-protocol-lib/https/censorship_strategy.h" +#include "fptn-protocol-lib/https/connection_config.h" #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" namespace fptn::utils::speed_estimator { @@ -20,11 +20,11 @@ std::uint64_t GetDownloadTimeMs(const ServerInfo& server, const std::string& sni, int timeout, const std::string& md5_fingerprint, - fptn::protocol::https::CensorshipStrategy censorship_strategy); + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy); ServerInfo FindFastestServer(const std::string& sni, const std::vector& servers, - fptn::protocol::https::CensorshipStrategy censorship_strategy, + fptn::protocol::https::HttpsInitConnectionStrategy censorship_strategy, int timeout_sec = 15); }; // namespace fptn::utils::speed_estimator diff --git a/src/fptn-client/vpn/http/client.h b/src/fptn-client/vpn/http/client.h deleted file mode 100644 index ab492ed1..00000000 --- a/src/fptn-client/vpn/http/client.h +++ /dev/null @@ -1,85 +0,0 @@ -/*============================================================================= -Copyright (c) 2024-2025 Stas Skokov - -Distributed under the MIT License (https://opensource.org/licenses/MIT) -=============================================================================*/ - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "common/network/ip_address.h" -#include "common/network/ip_packet.h" - -#include "fptn-protocol-lib/https/censorship_strategy.h" -#include "fptn-protocol-lib/https/websocket_client/websocket_client.h" - -namespace fptn::vpn::http { - -using IPv4Address = fptn::common::network::IPv4Address; -using IPv6Address = fptn::common::network::IPv6Address; - -class Client final { - public: - using NewIPPacketCallback = - std::function; - - public: - Client(IPv4Address server_ip, - int server_port, - IPv4Address tun_interface_address_ipv4, - IPv6Address tun_interface_address_ipv6, - std::string sni, - std::string md5_fingerprint, - fptn::protocol::https::CensorshipStrategy censorship_strategy, - NewIPPacketCallback new_ip_pkt_callback = nullptr); - ~Client(); - bool Login(const std::string& username, - const std::string& password, - int timeout_sec = 15); - std::pair GetDns(); - bool Start(); - bool Stop(); - bool Send(fptn::common::network::IPPacketPtr packet) const; - void SetRecvIPPacketCallback(const NewIPPacketCallback& callback) noexcept; - bool IsStarted() const; - - const std::string& LatestError() const; - - protected: - void Run(); - - private: - const int kMaxReconnectionAttempts_ = 5; - - std::thread th_; - mutable std::mutex mutex_; - std::atomic running_; - - const IPv4Address server_ip_; - const int server_port_; - - const IPv4Address tun_interface_address_ipv4_; - const IPv6Address tun_interface_address_ipv6_; - const std::string sni_; - const std::string md5_fingerprint_; - - fptn::protocol::https::CensorshipStrategy censorship_strategy_; - - NewIPPacketCallback new_ip_pkt_callback_; - - std::string access_token_; - fptn::protocol::https::WebsocketClientSPtr ws_; - - std::string latest_error_; - - std::atomic reconnection_attempts_; -}; - -using ClientPtr = std::unique_ptr; -} // namespace fptn::vpn::http diff --git a/src/fptn-client/vpn/vpn_client.cpp b/src/fptn-client/vpn/vpn_client.cpp index 63fdba99..42251649 100644 --- a/src/fptn-client/vpn/vpn_client.cpp +++ b/src/fptn-client/vpn/vpn_client.cpp @@ -15,30 +15,36 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) namespace fptn::vpn { -VpnClient::VpnClient(fptn::vpn::http::ClientPtr http_client, +VpnClient::VpnClient( + fptn::protocol::connection::ConnectionManagerPtr connectin_manager, fptn::common::network::TunInterfacePtr virtual_net_interface, - fptn::common::network::IPv4Address dns_server_ipv4, - fptn::common::network::IPv6Address dns_server_ipv6, fptn::plugin::PluginList plugins, std::size_t thread_pool_size) : running_(false), - http_client_(std::move(http_client)), + connectin_manager_(std::move(connectin_manager)), virtual_net_interface_(std::move(virtual_net_interface)), - dns_server_ipv4_(std::move(dns_server_ipv4)), - dns_server_ipv6_(std::move(dns_server_ipv6)), plugins_(std::move(plugins)), - thread_pool_size_(thread_pool_size) {} // NOLINT + thread_pool_size_(thread_pool_size) { + // NOLINTNEXTLINE(modernize-avoid-bind) + connectin_manager_->SetRecvIPPacketCallback(std::bind( + &VpnClient::HandlePacketFromWebSocket, this, std::placeholders::_1)); + + virtual_net_interface_->SetRecvIPPacketCallback( + // NOLINTNEXTLINE(modernize-avoid-bind) + std::bind(&VpnClient::HandlePacketFromVirtualNetworkInterface, this, + std::placeholders::_1)); +} VpnClient::~VpnClient() { Stop(); } -bool VpnClient::IsStarted() { +bool VpnClient::IsStarted() const { if (!running_) { return false; } const std::unique_lock lock(mutex_); // mutex - return running_ && http_client_ && http_client_->IsStarted(); + return running_ && connectin_manager_ && connectin_manager_->IsStarted(); } bool VpnClient::Start() { @@ -55,16 +61,7 @@ bool VpnClient::Start() { } } - // NOLINTNEXTLINE(modernize-avoid-bind) - http_client_->SetRecvIPPacketCallback(std::bind( - &VpnClient::HandlePacketFromWebSocket, this, std::placeholders::_1)); - - virtual_net_interface_->SetRecvIPPacketCallback( - // NOLINTNEXTLINE(modernize-avoid-bind) - std::bind(&VpnClient::HandlePacketFromVirtualNetworkInterface, this, - std::placeholders::_1)); - - http_client_->Start(); + connectin_manager_->Start(); virtual_net_interface_->Start(); running_ = true; @@ -112,10 +109,10 @@ bool VpnClient::Stop() { SPDLOG_DEBUG("Virtual network interface stopped successfully"); } - if (http_client_) { + if (connectin_manager_) { SPDLOG_INFO("Stopping HTTP client"); - http_client_->Stop(); - http_client_.reset(); + connectin_manager_->Stop(); + connectin_manager_.reset(); SPDLOG_DEBUG("HTTP client stopped successfully"); } return true; @@ -155,8 +152,8 @@ void VpnClient::HandlePacketFromVirtualNetworkInterface( const std::unique_lock lock(mutex_); // mutex - if (running_ && http_client_) { - http_client_->Send(std::move(packet)); + if (running_ && connectin_manager_) { + connectin_manager_->Send(std::move(packet)); } } @@ -186,8 +183,10 @@ void VpnClient::ProcessWebSocketPackets() { { std::unique_lock lock(mutex_); // mutex - ws_queue_cv_.wait( - lock, [this]() { return !ws_packet_queue_.empty() || !running_; }); + if (ws_packet_queue_.empty()) { + ws_queue_cv_.wait( + lock, [this]() { return !ws_packet_queue_.empty() || !running_; }); + } if (!running_ && ws_packet_queue_.empty()) { break; } @@ -201,7 +200,6 @@ void VpnClient::ProcessWebSocketPackets() { continue; } - // Обрабатываем пакет через плагины if (running_ && !plugins_.empty()) { for (const auto& plugin : plugins_) { if (packet) { diff --git a/src/fptn-client/vpn/vpn_client.h b/src/fptn-client/vpn/vpn_client.h index d1c65e20..1f623878 100644 --- a/src/fptn-client/vpn/vpn_client.h +++ b/src/fptn-client/vpn/vpn_client.h @@ -9,24 +9,25 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include #include -#include #include #include // NOLINT(build/include_order) + +#include + #include "common/network/ip_address.h" #include "common/network/ip_packet.h" #include "common/network/net_interface.h" -#include "http/client.h" +#include "fptn-protocol-lib/connection/connection_manager/connection_manager.h" #include "plugins/split/tunneling.h" namespace fptn::vpn { class VpnClient final { public: - explicit VpnClient(fptn::vpn::http::ClientPtr http_client, + explicit VpnClient( + fptn::protocol::connection::ConnectionManagerPtr connectin_manager, fptn::common::network::TunInterfacePtr virtual_net_interface, - fptn::common::network::IPv4Address dns_server_ipv4, - fptn::common::network::IPv6Address dns_server_ipv6, fptn::plugin::PluginList plugins, std::size_t thread_pool_size = 4); ~VpnClient(); @@ -34,7 +35,7 @@ class VpnClient final { bool Stop(); std::size_t GetSendRate(); std::size_t GetReceiveRate(); - bool IsStarted(); + bool IsStarted() const; protected: void HandlePacketFromVirtualNetworkInterface( @@ -47,11 +48,8 @@ class VpnClient final { mutable std::mutex mutex_; std::atomic running_; - fptn::vpn::http::ClientPtr http_client_; + fptn::protocol::connection::ConnectionManagerPtr connectin_manager_; fptn::common::network::TunInterfacePtr virtual_net_interface_; - const fptn::common::network::IPv4Address dns_server_ipv4_; - const fptn::common::network::IPv6Address dns_server_ipv6_; - const fptn::plugin::PluginList plugins_; const std::size_t thread_pool_size_; diff --git a/src/fptn-protocol-lib/CMakeLists.txt b/src/fptn-protocol-lib/CMakeLists.txt index 8ee08590..3b386d0a 100644 --- a/src/fptn-protocol-lib/CMakeLists.txt +++ b/src/fptn-protocol-lib/CMakeLists.txt @@ -100,6 +100,16 @@ set(FPTN_CLIENT_PROTOCOL_SOURCES https/utils/tls/tls.cpp https/websocket_client/websocket_client.h https/websocket_client/websocket_client.cpp + connection/connection_manager/connection_manager.h + connection/connection_manager/connection_manager.cpp + connection/connection_manager_builder/connection_manager_builder.h + connection/connection_manager_builder/connection_manager_builder.cpp + connection/strategies/base_strategy_connection.h + connection/strategies/base_strategy_connection.cpp + connection/strategies/long_term_connection/long_term_connection.h + connection/strategies/long_term_connection/long_term_connection.cpp + connection/strategies/connection_pool/connection_pool.h + connection/strategies/connection_pool/connection_pool.cpp protobuf/protocol.h protobuf/protocol.cpp time/time_provider.h diff --git a/src/fptn-client/vpn/http/client.cpp b/src/fptn-protocol-lib/connection/connection_manager/connection_manager.cpp similarity index 59% rename from src/fptn-client/vpn/http/client.cpp rename to src/fptn-protocol-lib/connection/connection_manager/connection_manager.cpp index b273381b..70e184c9 100644 --- a/src/fptn-client/vpn/http/client.cpp +++ b/src/fptn-protocol-lib/connection/connection_manager/connection_manager.cpp @@ -1,10 +1,10 @@ /*============================================================================= -Copyright (c) 2024-2025 Stas Skokov +Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ -#include "vpn/http/client.h" +#include "connection_manager.h" #include #include @@ -15,45 +15,47 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include // NOLINT(build/include_order) -#include "common/network/ip_address.h" - +#include "connection/strategies/connection_pool/connection_pool.h" +#include "fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h" +#include "fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.h" #include "fptn-protocol-lib/https/api_client/api_client.h" -#include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" -#include "routing/route_manager.h" +#include "fptn-protocol-lib/https/connection_config.h" + +namespace fptn::protocol::connection { using fptn::common::network::IPv4Address; using fptn::common::network::IPv6Address; using fptn::protocol::https::ApiClient; -using fptn::vpn::http::Client; - -Client::Client(IPv4Address server_ip, - int server_port, - IPv4Address tun_interface_address_ipv4, - IPv6Address tun_interface_address_ipv6, - std::string sni, - std::string md5_fingerprint, - fptn::protocol::https::CensorshipStrategy censorship_strategy, - NewIPPacketCallback new_ip_pkt_callback) + +ConnectionManager::ConnectionManager( + strategies::ConnectionStrategy connection_strategy_type, + fptn::protocol::https::ConnectionConfig config) : running_(false), - server_ip_(std::move(server_ip)), - server_port_(server_port), - tun_interface_address_ipv4_(std::move(tun_interface_address_ipv4)), - tun_interface_address_ipv6_(std::move(tun_interface_address_ipv6)), - sni_(std::move(sni)), - md5_fingerprint_(std::move(md5_fingerprint)), - censorship_strategy_(censorship_strategy), - new_ip_pkt_callback_(std::move(new_ip_pkt_callback)), - reconnection_attempts_(kMaxReconnectionAttempts_) {} - -Client::~Client() { Stop(); } - -bool Client::Login( + reconnection_attempts_(0), + connection_strategy_type_(connection_strategy_type), + config_(std::move(config)) {} + +ConnectionManager::~ConnectionManager() { + if (strategy_connection_) { + strategy_connection_->Stop(); + strategy_connection_.reset(); + } +} + +void ConnectionManager::SetRecvIPPacketCallback( + const fptn::protocol::https::RecvIPPacketCallback& callback) { + config_.common.recv_ip_packet_callback = callback; +} + +bool ConnectionManager::Login( const std::string& username, const std::string& password, int timeout_sec) { const std::string request = fmt::format( R"({{ "username": "{}", "password": "{}" }})", username, password); - const std::string ip = server_ip_.ToString(); - ApiClient cli(ip, server_port_, sni_, md5_fingerprint_, censorship_strategy_); + const std::string ip = config_.common.server_ip.ToString(); + ApiClient cli(ip, config_.common.server_port, config_.common.sni, + config_.common.md5_fingerprint, + config_.common.https_init_connection_strategy); const auto resp = cli.Post("/api/v1/login", request, "application/json", timeout_sec); @@ -65,7 +67,7 @@ bool Client::Login( "Error: Access token not found in the response. Check your " "conection"); } else { - access_token_ = msg["access_token"]; + jwt_access_token_ = msg["access_token"]; SPDLOG_INFO("Login successful"); return true; } @@ -84,12 +86,11 @@ bool Client::Login( return false; } -std::pair Client::GetDns() { - SPDLOG_INFO("Obtained DNS server address. Connecting to {}:{}", - server_ip_.ToString(), server_port_); - - const std::string ip = server_ip_.ToString(); - ApiClient cli(ip, server_port_, sni_, md5_fingerprint_, censorship_strategy_); +std::pair ConnectionManager::GetDns() { + const std::string ip = config_.common.server_ip.ToString(); + ApiClient cli(ip, config_.common.server_port, config_.common.sni, + config_.common.md5_fingerprint, + config_.common.https_init_connection_strategy); const auto resp = cli.Get("/api/v1/dns"); if (resp.code == 200) { @@ -120,17 +121,48 @@ std::pair Client::GetDns() { return {IPv4Address(), IPv6Address()}; } -void Client::SetRecvIPPacketCallback( - const NewIPPacketCallback& callback) noexcept { - new_ip_pkt_callback_ = callback; +bool ConnectionManager::Start() { + running_ = true; + th_ = std::thread(&ConnectionManager::Run, this); + return th_.joinable(); +} + +bool ConnectionManager::Stop() { + if (!running_) { + return false; + } + + SPDLOG_INFO("Stopping client"); + { + const std::unique_lock lock(mutex_); // mutex + + if (!running_) { // Double-check after acquiring lock + return false; + } + running_ = false; + } + + if (strategy_connection_) { + strategy_connection_->Stop(); + strategy_connection_.reset(); + } + + if (th_.joinable()) { + try { + th_.join(); + } catch (...) { + SPDLOG_WARN("Unexpected exception during thread join"); + } + } + return true; } -bool Client::Send(fptn::common::network::IPPacketPtr packet) const { +bool ConnectionManager::Send(fptn::common::network::IPPacketPtr packet) const { try { const std::unique_lock lock(mutex_); // mutex - if (ws_ && running_) { - ws_->Send(std::move(packet)); + if (strategy_connection_ && running_) { + strategy_connection_->Send(std::move(packet)); return true; } } catch (const std::runtime_error& err) { @@ -141,31 +173,44 @@ bool Client::Send(fptn::common::network::IPPacketPtr packet) const { return false; } -void Client::Run() { +bool ConnectionManager::IsStarted() const { + return running_ && strategy_connection_; +} + +const std::string& ConnectionManager::LatestError() const { + return latest_error_; +} + +void ConnectionManager::Run() { // Time window for counting attempts (1 minute) constexpr auto kReconnectionWindow = std::chrono::seconds(120); // Delay between reconnection attempts constexpr auto kReconnectionDelay = std::chrono::milliseconds(300); // Current count of reconnection attempts - reconnection_attempts_ = kMaxReconnectionAttempts_; + reconnection_attempts_ = 0; auto window_start_time = std::chrono::steady_clock::now(); - while (running_ && reconnection_attempts_) { + const auto max_reconnection = config_.common.max_reconnections; + while (running_ && reconnection_attempts_ < max_reconnection) { { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalInnerCondition - if (running_) { // Double-check after acquiring lock - ws_ = std::make_shared( - server_ip_, server_port_, tun_interface_address_ipv4_, - tun_interface_address_ipv6_, new_ip_pkt_callback_, sni_, - access_token_, md5_fingerprint_, censorship_strategy_, nullptr, 32); + if (running_ && connection_strategy_type_ == + strategies::ConnectionStrategy::kLongTermConnection) { + strategy_connection_ = + strategies::LongTermConnection::Create(jwt_access_token_, config_); + } else if (running_ && + connection_strategy_type_ == + strategies::ConnectionStrategy::kConnectionPool) { + strategy_connection_ = + strategies::ConnectionPool::Create(jwt_access_token_, config_); } } - if (running_ && ws_) { - ws_->Run(); // Start the WebSocket client + if (running_ && strategy_connection_) { + strategy_connection_->Start(); // Start the WebSocket client } if (!running_) { @@ -173,13 +218,13 @@ void Client::Run() { } // clean - if (ws_) { + if (strategy_connection_) { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress knownConditionTrueFalse - if (ws_ && running_) { - ws_->Stop(); - ws_.reset(); + if (strategy_connection_ && running_) { + strategy_connection_->Stop(); + strategy_connection_.reset(); } } @@ -190,18 +235,17 @@ void Client::Run() { // Reconnection attempt counting logic if (elapsed >= kReconnectionWindow) { // Reset counter if we're past the time window - reconnection_attempts_ = kMaxReconnectionAttempts_; + reconnection_attempts_ = 0; window_start_time = current_time; } else { - --reconnection_attempts_; // Decrement counter if within time window + ++reconnection_attempts_; // Decrement counter if within time window } // Log connection failure and wait before retrying SPDLOG_ERROR( "Connection closed (attempt {}/{} in current window). Reconnecting in " "{}ms...", - kMaxReconnectionAttempts_ - reconnection_attempts_, - kMaxReconnectionAttempts_, kReconnectionDelay.count()); + reconnection_attempts_, max_reconnection, kReconnectionDelay.count()); std::this_thread::sleep_for(kReconnectionDelay); } @@ -211,44 +255,4 @@ void Client::Run() { } } -bool Client::Start() { - running_ = true; - th_ = std::thread(&Client::Run, this); - return th_.joinable(); -} - -bool Client::Stop() { - if (!running_) { - return false; - } - - SPDLOG_INFO("Stopping client"); - { - const std::unique_lock lock(mutex_); // mutex - - if (!running_) { // Double-check after acquiring lock - return false; - } - running_ = false; - } - - if (ws_) { - ws_->Stop(); - ws_.reset(); - } - - if (th_.joinable()) { - try { - th_.join(); - } catch (...) { - SPDLOG_WARN("Unexpected exception during thread join"); - } - } - return true; -} - -bool Client::IsStarted() const { - return running_ && reconnection_attempts_ > 0; -} - -const std::string& Client::LatestError() const { return latest_error_; } +} // namespace fptn::protocol::connection diff --git a/src/fptn-protocol-lib/connection/connection_manager/connection_manager.h b/src/fptn-protocol-lib/connection/connection_manager/connection_manager.h new file mode 100644 index 00000000..02a6cd53 --- /dev/null +++ b/src/fptn-protocol-lib/connection/connection_manager/connection_manager.h @@ -0,0 +1,65 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "fptn-protocol-lib/connection/strategies/base_strategy_connection.h" +#include "fptn-protocol-lib/https/connection_config.h" + +namespace fptn::protocol::connection { + +using IPv4Address = fptn::common::network::IPv4Address; +using IPv6Address = fptn::common::network::IPv6Address; + +class ConnectionManager final { + public: + ConnectionManager(strategies::ConnectionStrategy connection_strategy_type, + fptn::protocol::https::ConnectionConfig config); + ~ConnectionManager(); + + void SetRecvIPPacketCallback( + const fptn::protocol::https::RecvIPPacketCallback& callback); + + bool Login(const std::string& username, + const std::string& password, + int timeout_sec = 15); + std::pair GetDns(); + + bool Start(); + bool Stop(); + bool Send(fptn::common::network::IPPacketPtr packet) const; + bool IsStarted() const; + + const std::string& LatestError() const; + + protected: + void Run(); + + private: + std::thread th_; + mutable std::mutex mutex_; + std::atomic running_; + + std::string jwt_access_token_; + std::string latest_error_; + std::size_t reconnection_attempts_; + + strategies::ConnectionStrategy connection_strategy_type_; + fptn::protocol::https::ConnectionConfig config_; + + strategies::StrategyConnectionPtr strategy_connection_; +}; + +using ConnectionManagerPtr = std::unique_ptr; + +} // namespace fptn::protocol::connection diff --git a/src/fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.cpp b/src/fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.cpp new file mode 100644 index 00000000..5bd2f671 --- /dev/null +++ b/src/fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.cpp @@ -0,0 +1,80 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#include "fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h" + +namespace fptn::protocol::connection { + +ConnectionManagerBuilder::ConnectionManagerBuilder() : config_{} {} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetConnectionStrategyType( + strategies::ConnectionStrategy connection_strategy_type) { + connection_strategy_type_ = connection_strategy_type; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetServer( + const IPv4Address& ip, int port) { + config_.common.server_ip = ip; + config_.common.server_port = port; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetSNI( + const std::string& sni) { + config_.common.sni = sni; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetServerFingerprint( + const std::string& md5_fingerprint) { + config_.common.md5_fingerprint = md5_fingerprint; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetTunInterface( + const IPv4Address& ipv4, const IPv6Address& ipv6) { + config_.common.tun_interface_address_ipv4 = ipv4; + config_.common.tun_interface_address_ipv6 = ipv6; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetCensorshipStrategy( + fptn::protocol::https::HttpsInitConnectionStrategy strategy) { + config_.common.https_init_connection_strategy = strategy; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetOnConnectedCallback( + const fptn::protocol::https::OnConnectedCallback& callback) { + config_.common.on_connected_callback = callback; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetMaxReconnections( + int max_attempts) { + config_.common.max_reconnections = max_attempts; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetPoolSize( + std::size_t pool_size) { + config_.pool.size = pool_size; + return *this; +} + +ConnectionManagerBuilder& ConnectionManagerBuilder::SetConnectionTimeout( + int timeout_ms) { + config_.common.connection_timeout_ms = timeout_ms; + return *this; +} + +ConnectionManagerPtr ConnectionManagerBuilder::Build() { + return std::make_unique( + connection_strategy_type_, config_); +} + +} // namespace fptn::protocol::connection diff --git a/src/fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h b/src/fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h new file mode 100644 index 00000000..77e94551 --- /dev/null +++ b/src/fptn-protocol-lib/connection/connection_manager_builder/connection_manager_builder.h @@ -0,0 +1,57 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include +#include + +#include "common/network/ip_packet.h" + +#include "fptn-protocol-lib/connection/connection_manager/connection_manager.h" +#include "fptn-protocol-lib/https/connection_config.h" + +namespace fptn::protocol::connection { + +class ConnectionManagerBuilder { + public: + ConnectionManagerBuilder(); + + ConnectionManagerBuilder& SetConnectionStrategyType( + strategies::ConnectionStrategy connection_strategy); + + ConnectionManagerBuilder& SetServer(const IPv4Address& ip, int port); + + ConnectionManagerBuilder& SetSNI(const std::string& sni); + + ConnectionManagerBuilder& SetServerFingerprint( + const std::string& md5_fingerprint); + + ConnectionManagerBuilder& SetTunInterface( + const IPv4Address& ipv4, const IPv6Address& ipv6); + + ConnectionManagerBuilder& SetCensorshipStrategy( + fptn::protocol::https::HttpsInitConnectionStrategy strategy); + + ConnectionManagerBuilder& SetOnConnectedCallback( + const fptn::protocol::https::OnConnectedCallback& callback); + + ConnectionManagerBuilder& SetMaxReconnections(int max_attempts); + + ConnectionManagerBuilder& SetPoolSize(std::size_t pool_size); + + ConnectionManagerBuilder& SetConnectionTimeout(int timeout_ms); + + ConnectionManagerPtr Build(); + + private: + strategies::ConnectionStrategy connection_strategy_type_ = + strategies::ConnectionStrategy::kLongTermConnection; + fptn::protocol::https::ConnectionConfig config_; +}; + +} // namespace fptn::protocol::connection diff --git a/src/fptn-protocol-lib/connection/strategies/base_strategy_connection.cpp b/src/fptn-protocol-lib/connection/strategies/base_strategy_connection.cpp new file mode 100644 index 00000000..b4d14b8f --- /dev/null +++ b/src/fptn-protocol-lib/connection/strategies/base_strategy_connection.cpp @@ -0,0 +1,68 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#include "fptn-protocol-lib/connection/strategies/base_strategy_connection.h" + +#include + +namespace fptn::protocol::connection::strategies { + +BaseStrategyConnection::BaseStrategyConnection(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config, + int thread_number) + : ioc_(thread_number), + jwt_access_token_(std::move(jwt_access_token)), + config_(std::move(config)) {} + +BaseStrategyConnection::~BaseStrategyConnection() { + // Stop io_context + try { + if (!ioc_.stopped()) { + SPDLOG_INFO("Stopping io_context..."); + ioc_.stop(); + } + } catch (const boost::system::system_error& err) { + SPDLOG_ERROR("Exception while stopping io_context: {}", err.what()); + } catch (...) { + SPDLOG_ERROR("Unknown exception while stopping io_context"); + } +} + +fptn::protocol::https::ConnectionConfig BaseStrategyConnection::Config() const { + return config_; +} + +const std::string& BaseStrategyConnection::JWTAccessToken() const { + return jwt_access_token_; +} + +boost::asio::io_context& BaseStrategyConnection::GetIOContext() { return ioc_; } + +bool BaseStrategyConnection::RunningStatus() const { return running_; } + +void BaseStrategyConnection::SetRunningStatus(const bool value) { + running_ = value; +} + +void BaseStrategyConnection::RunEventLoop() { + try { + // ioc_.run(); + constexpr std::chrono::milliseconds kTimeout(1); + while (running_) { + const std::size_t processed = ioc_.poll_one(); + if (processed == 0) { + std::this_thread::sleep_for(kTimeout); + } + } + if (!ioc_.stopped()) { + ioc_.stop(); + } + } catch (...) { + SPDLOG_WARN("Exception while running"); + } +} + +} // namespace fptn::protocol::connection::strategies diff --git a/src/fptn-protocol-lib/connection/strategies/base_strategy_connection.h b/src/fptn-protocol-lib/connection/strategies/base_strategy_connection.h new file mode 100644 index 00000000..7f569421 --- /dev/null +++ b/src/fptn-protocol-lib/connection/strategies/base_strategy_connection.h @@ -0,0 +1,62 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include + +#include + +#include "fptn-protocol-lib/https/connection_config.h" + +namespace fptn::protocol::connection::strategies { + +enum class ConnectionStrategy : int { + kLongTermConnection = 0, + kConnectionPool = 1 +}; + +using IPv4Address = fptn::common::network::IPv4Address; +using IPv6Address = fptn::common::network::IPv6Address; + +class BaseStrategyConnection { + public: + explicit BaseStrategyConnection(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config, + int thread_number = 4); + virtual ~BaseStrategyConnection(); + + fptn::protocol::https::ConnectionConfig Config() const; + const std::string& JWTAccessToken() const; + + public: + virtual void Start() = 0; + + virtual void Stop() = 0; + + virtual bool Send(fptn::common::network::IPPacketPtr packet) = 0; + + virtual bool IsStarted() = 0; + + protected: + bool RunningStatus() const; + void SetRunningStatus(bool value); + + boost::asio::io_context& GetIOContext(); + void RunEventLoop(); + + private: + boost::asio::io_context ioc_; + std::atomic running_{false}; + + const std::string jwt_access_token_; + const fptn::protocol::https::ConnectionConfig config_; +}; + +using StrategyConnectionPtr = std::unique_ptr; + +} // namespace fptn::protocol::connection::strategies diff --git a/src/fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.cpp b/src/fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.cpp new file mode 100644 index 00000000..bd9a731a --- /dev/null +++ b/src/fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.cpp @@ -0,0 +1,316 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#include "fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.h" + +#include + +#include "common/utils/uuid4.h" + +namespace { + +bool RemoveFromConnectionList( + fptn::protocol::connection::strategies::ConnectionList& connections, + const std::uint64_t id) { + for (auto it = connections.begin(); it != connections.end(); ++it) { + if ((*it)->connection_id == id) { + connections.erase(it); + return true; + } + } + return false; +} + +} // namespace + +namespace fptn::protocol::connection::strategies { +ConnectionPool::ConnectionPool(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config) + : BaseStrategyConnection(std::move(jwt_access_token), std::move(config)), + session_id_(common::utils::GenerateUUID4()) { + std::random_device rd; + random_generator_.seed(rd()); +} + +ConnectionPool::~ConnectionPool() { + Stop(); // NOLINT +} + +void ConnectionPool::Start() { + SetRunningStatus(true); + boost::asio::co_spawn( + GetIOContext(), + [this]() -> boost::asio::awaitable { + co_await ManagePoolCoroutine(); + }, + boost::asio::detached); + RunEventLoop(); +} + +void ConnectionPool::Stop() { + const std::unique_lock lock(mutex_); // mutex + + SetRunningStatus(false); + for (auto& ctx : all_connections_) { + if (ctx && ctx->client) { + ctx->status = ConnectionStatus::kError; + ctx->client->Stop(); + } + } + + all_connections_.clear(); + sending_data_connections_.clear(); + getting_data_connections_.clear(); +} + +bool ConnectionPool::Send(fptn::common::network::IPPacketPtr packet) { + if (!IsStarted()) { + return false; + } + + std::shared_ptr connection; + { + const std::shared_lock lock(mutex_); // read-only lock + + if (sending_data_connections_.empty()) { + return false; + } + const int random_index = + GetRandomInt(0, sending_data_connections_.size() - 1); + const int sender_index = random_index % sending_data_connections_.size(); + std::cerr << "sender_index>" << sender_index << "\n"; + connection = sending_data_connections_[sender_index]; + } + return connection && connection->client->Send(std::move(packet)); +} + +bool ConnectionPool::IsStarted() { return RunningStatus(); } + +boost::asio::awaitable ConnectionPool::ManagePoolCoroutine() { + boost::asio::steady_timer timer(GetIOContext()); + bool first_itteration = true; + while (IsStarted()) { + try { + co_await boost::asio::post(boost::asio::use_awaitable); + + co_await RemoveExpiredConnections(); + + co_await UpdateConnectionsStatus(); + + co_await CreateMissingConnections(); + + if (first_itteration) { + first_itteration = false; + auto config = Config(); + config.common.on_connected_callback(); + } + } catch (const std::exception& e) { + SPDLOG_ERROR("Error in ManagePoolCoroutine: {}", e.what()); + } + timer.expires_after(std::chrono::milliseconds(100)); + co_await timer.async_wait(boost::asio::use_awaitable); + } + + co_return; +} + +boost::asio::awaitable> +ConnectionPool::CreateNewConnection(int sending_mode_seconds, int ttl_seconds) { + try { + auto connection = + std::make_shared(sending_mode_seconds, ttl_seconds); + connection->status = ConnectionStatus::kCreating; + + auto config = Config(); + config.common.on_connected_callback = nullptr; + + connection->client = + std::make_shared( + JWTAccessToken(), config, GetIOContext()); + + connection->client->Run(); + co_await boost::asio::post(boost::asio::use_awaitable); + + boost::asio::steady_timer timer(GetIOContext()); + + for (int i = 0; i < 10; i++) { + if (connection->client->IsStarted()) { + connection->status = ConnectionStatus::kCreating; + SPDLOG_INFO("Connection #{} READY", connection->connection_id); + co_return connection; + } + + timer.expires_after(std::chrono::milliseconds(500)); + co_await timer.async_wait(boost::asio::use_awaitable); + } + connection->status = ConnectionStatus::kError; + connection->client->Stop(); + SPDLOG_ERROR("Connection #{} FAILED to start", connection->connection_id); + } catch (const std::exception& err) { + SPDLOG_ERROR("Failed to create connection: {}", err.what()); + } + co_return nullptr; +} + +boost::asio::awaitable ConnectionPool::RemoveExpiredConnections() { + std::vector> dead_connections; + { + const std::unique_lock lock(mutex_); // mutex + + for (auto it = all_connections_.begin(); it != all_connections_.end();) { + auto& connection = *it; + const bool dead = connection->IsExpired() || + connection->status == ConnectionStatus::kError; + if (dead) { + dead_connections.push_back(connection); + it = all_connections_.erase(it); + + // remove from other collections + RemoveFromConnectionList( + getting_data_connections_, connection->connection_id); + RemoveFromConnectionList( + sending_data_connections_, connection->connection_id); + } else { + ++it; + } + } + } + if (!dead_connections.empty()) { + // close connection in background + boost::asio::co_spawn( + GetIOContext(), + [closed_connections = + std::move(dead_connections)]() -> boost::asio::awaitable { + for (const auto& connection : closed_connections) { + if (connection && connection->client) { + connection->client->Stop(); + } + } + co_return; + }, + boost::asio::detached); + } + co_return; +} + +boost::asio::awaitable ConnectionPool::UpdateConnectionsStatus() { + const std::unique_lock lock(mutex_); // mutex + + const auto now = std::chrono::system_clock::now(); + + for (auto& connection : all_connections_) { + const ConnectionStatus old_status = connection->status; + + // change status + if (connection->status == ConnectionStatus::kSending && + connection->SendTimeExpired()) { + connection->status = ConnectionStatus::kReceiving; + } else if (connection->status == ConnectionStatus::kCreating && + connection->client->IsStarted()) { + if (connection->timings.send_mode_until > now) { + connection->status = ConnectionStatus::kSending; + } else { + connection->status = ConnectionStatus::kReceiving; + } + } + + // move to collection + if (old_status != connection->status) { + RemoveFromConnectionList( + sending_data_connections_, connection->connection_id); + RemoveFromConnectionList( + getting_data_connections_, connection->connection_id); + + if (connection->status == ConnectionStatus::kSending) { + sending_data_connections_.push_back(connection); + SPDLOG_INFO("Connection #{} moved to SENDING (until {})", + connection->connection_id, + std::chrono::duration_cast( + connection->timings.send_mode_until - now) + .count()); + } else if (connection->status == ConnectionStatus::kReceiving) { + getting_data_connections_.push_back(connection); + SPDLOG_INFO( + "Connection #{} moved to RECEIVING", connection->connection_id); + } + } + } + co_return; +} + +boost::asio::awaitable ConnectionPool::CreateMissingConnections() { + if (!IsStarted()) { + co_return; + } + + int sending_count = 0; + int receiving_count = 0; + + { + const std::unique_lock lock(mutex_); // mutex + + for (const auto& connection : all_connections_) { + if (connection->status == ConnectionStatus::kSending && + connection->CanSend()) { + sending_count++; + } + if (connection->status == ConnectionStatus::kReceiving && + !connection->IsExpired()) { + receiving_count++; + } + } + } + + const int target_per_type = std::max(1, settings_.min_connections / 2); + + // Create sending connections + if (sending_count < target_per_type) { + const int need = target_per_type - sending_count; + for (int i = 0; i < need; i++) { + if (all_connections_.size() >= settings_.max_connections) { + break; + } + const int send_time = + GetRandomInt(settings_.sending_mode_range.min_seconds, + settings_.sending_mode_range.max_seconds); + const int ttl = GetRandomInt(settings_.connection_ttl_range.min_seconds, + settings_.connection_ttl_range.max_seconds); + auto connection = co_await CreateNewConnection(send_time, ttl); + if (connection) { + const std::unique_lock lock(mutex_); // mutex + + all_connections_.push_back(std::move(connection)); + } + } + } + + // create receiving connections + if (receiving_count < target_per_type) { + const int need = target_per_type - receiving_count; + for (int i = 0; i < need; i++) { + if (all_connections_.size() >= settings_.max_connections) { + break; + } + const int ttl = GetRandomInt(settings_.connection_ttl_range.min_seconds, + settings_.connection_ttl_range.max_seconds); + + auto connection = co_await CreateNewConnection(0, ttl); + if (connection) { + const std::unique_lock lock(mutex_); // mutex + + all_connections_.push_back(std::move(connection)); + } + } + } + co_return; +} + +int ConnectionPool::GetRandomInt(const int min, const int max) const { + std::uniform_int_distribution dist(min, max); + return dist(random_generator_); +} + +} // namespace fptn::protocol::connection::strategies diff --git a/src/fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.h b/src/fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.h new file mode 100644 index 00000000..46ac8ec5 --- /dev/null +++ b/src/fptn-protocol-lib/connection/strategies/connection_pool/connection_pool.h @@ -0,0 +1,140 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "fptn-protocol-lib/connection/strategies/base_strategy_connection.h" +#include "fptn-protocol-lib/https/websocket_client/websocket_client.h" + +namespace fptn::protocol::connection::strategies { + +enum class ConnectionStatus : int { kCreating, kSending, kReceiving, kError }; +struct ConnectionContext { + std::uint64_t connection_id; + ConnectionStatus status; + + struct { + std::chrono::system_clock::time_point created_at; + std::chrono::system_clock::time_point last_used_at; + std::chrono::system_clock::time_point + send_mode_until; // До когда может отправлять + std::chrono::system_clock::time_point expire_after; // Когда умрет + } timings; + + struct { + std::size_t packets_sent = 0; + std::size_t packets_received = 0; + std::size_t bytes_sent = 0; + std::size_t bytes_received = 0; + } stats; + + std::shared_ptr client; + + explicit ConnectionContext(int send_mode_seconds, int ttl_seconds) + : connection_id(0), status(ConnectionStatus::kCreating) { + static std::uint64_t connection_id_counter = 0; + connection_id = ++connection_id_counter; + + const auto now = std::chrono::system_clock::now(); + timings.created_at = now; + timings.last_used_at = now; + timings.send_mode_until = now + std::chrono::seconds(send_mode_seconds); + timings.expire_after = now + std::chrono::seconds(ttl_seconds); + } + + [[nodiscard]] + bool CanSend() const noexcept { + return status == ConnectionStatus::kSending && + std::chrono::system_clock::now() < timings.send_mode_until; + } + + [[nodiscard]] + bool SendTimeExpired() const noexcept { + return std::chrono::system_clock::now() >= timings.send_mode_until; + } + + [[nodiscard]] + bool IsExpired() const noexcept { + return timings.expire_after <= std::chrono::system_clock::now(); + } +}; + +struct PoolSettings { + std::size_t min_connections = 4; + std::size_t max_connections = 8; + + struct { + int min_seconds = 8; + int max_seconds = 40; + } connection_ttl_range; + + struct { + int min_seconds = 5; + int max_seconds = 15; + } sending_mode_range; +}; + +using ConnectionList = std::vector>; + +class ConnectionPool : public BaseStrategyConnection { + public: + static std::unique_ptr Create(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config) { + return std::make_unique( + std::move(jwt_access_token), std::move(config)); + } + + explicit ConnectionPool(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config); + ~ConnectionPool() override; + + protected: + boost::asio::awaitable ManagePoolCoroutine(); + + boost::asio::awaitable> + CreateNewConnection(int sending_mode_seconds, int ttl_seconds); + + boost::asio::awaitable RemoveExpiredConnections(); + + boost::asio::awaitable UpdateConnectionsStatus(); + + boost::asio::awaitable CreateMissingConnections(); + + private: + int GetRandomInt(const int min, const int max) const; + + public: + void Start() override; + + void Stop() override; + + bool Send(fptn::common::network::IPPacketPtr packet) override; + + bool IsStarted() override; + + private: + mutable std::shared_mutex mutex_; + mutable std::mt19937 random_generator_; + + const std::string session_id_; + + const PoolSettings settings_; + + ConnectionList all_connections_; + ConnectionList getting_data_connections_; + ConnectionList sending_data_connections_; +}; + +} // namespace fptn::protocol::connection::strategies diff --git a/src/fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.cpp b/src/fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.cpp new file mode 100644 index 00000000..1b7b85d9 --- /dev/null +++ b/src/fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.cpp @@ -0,0 +1,53 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#include "fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.h" + +namespace fptn::protocol::connection::strategies { + +LongTermConnection::LongTermConnection(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config) + : BaseStrategyConnection(std::move(jwt_access_token), std::move(config)) {} + +LongTermConnection::~LongTermConnection() { + Stop(); // NOLINT +} + +void LongTermConnection::Start() { + websocket_client_ = std::make_unique( + JWTAccessToken(), Config(), GetIOContext()); + + SetRunningStatus(true); + websocket_client_->Run(); + RunEventLoop(); +} + +void LongTermConnection::Stop() { + const std::unique_lock lock(mutex_); // mutex + + SetRunningStatus(false); + + if (websocket_client_) { + websocket_client_->Stop(); + websocket_client_.reset(); + } +} + +bool LongTermConnection::Send(fptn::common::network::IPPacketPtr packet) { + if (websocket_client_) { + const std::unique_lock lock(mutex_); // mutex + + if (RunningStatus()) { + // cppcheck-suppress knownConditionTrueFalse + return websocket_client_ && websocket_client_->Send(std::move(packet)); + } + } + return false; +} + +bool LongTermConnection::IsStarted() { return RunningStatus(); } + +} // namespace fptn::protocol::connection::strategies diff --git a/src/fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.h b/src/fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.h new file mode 100644 index 00000000..ee11538c --- /dev/null +++ b/src/fptn-protocol-lib/connection/strategies/long_term_connection/long_term_connection.h @@ -0,0 +1,46 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include + +#include "fptn-protocol-lib/connection/strategies/base_strategy_connection.h" +#include "fptn-protocol-lib/https/websocket_client/websocket_client.h" + +namespace fptn::protocol::connection::strategies { + +class LongTermConnection : public BaseStrategyConnection { + public: + static std::unique_ptr Create( + std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config) { + return std::make_unique( + std::move(jwt_access_token), std::move(config)); + } + + explicit LongTermConnection(std::string jwt_access_token, + fptn::protocol::https::ConnectionConfig config); + ~LongTermConnection() override; + + public: + void Start() override; + + void Stop() override; + + bool Send(fptn::common::network::IPPacketPtr packet) override; + + bool IsStarted() override; + + private: + mutable std::mutex mutex_; + + + fptn::protocol::https::WebsocketClientPtr websocket_client_; +}; + +} // namespace fptn::protocol::connection::strategies diff --git a/src/fptn-protocol-lib/https/api_client/api_client.cpp b/src/fptn-protocol-lib/https/api_client/api_client.cpp index 9cbe1054..63cc7d5c 100644 --- a/src/fptn-protocol-lib/https/api_client/api_client.cpp +++ b/src/fptn-protocol-lib/https/api_client/api_client.cpp @@ -243,8 +243,9 @@ using tcp_stream_type = boost::beast::tcp_stream; using obfuscator_socket_type = obfuscator::TcpStream; using ssl_stream_type = boost::beast::ssl_stream; -ApiClient::ApiClient( - const std::string& host, int port, CensorshipStrategy censorship_strategy) +ApiClient::ApiClient(const std::string& host, + int port, + HttpsInitConnectionStrategy censorship_strategy) : host_(host), port_(port), sni_(host), @@ -253,7 +254,7 @@ ApiClient::ApiClient( ApiClient::ApiClient(std::string host, int port, std::string sni, - CensorshipStrategy censorship_strategy) + HttpsInitConnectionStrategy censorship_strategy) : host_(std::move(host)), port_(port), sni_(std::move(sni)), @@ -263,7 +264,7 @@ ApiClient::ApiClient(std::string host, int port, std::string sni, std::string md5_fingerprint, - CensorshipStrategy censorship_strategy) + HttpsInitConnectionStrategy censorship_strategy) : host_(std::move(host)), port_(port), sni_(std::move(sni)), @@ -306,7 +307,7 @@ bool ApiClient::TestHandshake(int timeout) const { timeout, "TestHandshake", "", host_, false); } -ApiClient ApiClient::Clone() const { +ApiClient ApiClient::Clone() const { // NOLINT ApiClient temp_client( host_, port_, sni_, expected_md5_fingerprint_, censorship_strategy_); return temp_client; @@ -376,7 +377,7 @@ Response ApiClient::GetImpl(const std::string& handle, int timeout) const { boost::asio::ssl::context ctx(ssl_ctx); fptn::protocol::https::obfuscator::IObfuscatorSPtr obfuscator = nullptr; - if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { + if (censorship_strategy_ == HttpsInitConnectionStrategy::kTlsObfuscator) { obfuscator = std::make_shared(); } @@ -413,7 +414,8 @@ Response ApiClient::GetImpl(const std::string& handle, int timeout) const { SetSocketTimeouts(socket, timeout); // Perform fake handshake if enabled - if (censorship_strategy_ == CensorshipStrategy::kSniRealityMode) { + if (censorship_strategy_ == + HttpsInitConnectionStrategy::kSniRealityMode) { const bool perform_status = PerformFakeHandshake(socket); if (!perform_status) { SPDLOG_WARN( @@ -555,7 +557,7 @@ Response ApiClient::PostImpl(const std::string& handle, boost::asio::ssl::context ctx(ssl_ctx); fptn::protocol::https::obfuscator::IObfuscatorSPtr obfuscator = nullptr; - if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { + if (censorship_strategy_ == HttpsInitConnectionStrategy::kTlsObfuscator) { obfuscator = std::make_shared(); } @@ -592,7 +594,8 @@ Response ApiClient::PostImpl(const std::string& handle, SetSocketTimeouts(socket, timeout); // Perform fake handshake if enabled - if (censorship_strategy_ == CensorshipStrategy::kSniRealityMode) { + if (censorship_strategy_ == + HttpsInitConnectionStrategy::kSniRealityMode) { const bool perform_status = PerformFakeHandshake(socket); if (!perform_status) { SPDLOG_WARN( @@ -734,7 +737,7 @@ bool ApiClient::TestHandshakeImpl(int timeout) const { boost::asio::ssl::context ctx(ssl_ctx); fptn::protocol::https::obfuscator::IObfuscatorSPtr obfuscator = nullptr; - if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { + if (censorship_strategy_ == HttpsInitConnectionStrategy::kTlsObfuscator) { obfuscator = std::make_shared(); } @@ -780,7 +783,7 @@ bool ApiClient::TestHandshakeImpl(int timeout) const { SetSocketTimeouts(socket, timeout); // Perform fake handshake if enabled - if (censorship_strategy_ == CensorshipStrategy::kSniRealityMode) { + if (censorship_strategy_ == HttpsInitConnectionStrategy::kSniRealityMode) { SPDLOG_INFO("TestHandshake - Performing fake handshake"); if (!PerformFakeHandshake(socket)) { SPDLOG_WARN( diff --git a/src/fptn-protocol-lib/https/api_client/api_client.h b/src/fptn-protocol-lib/https/api_client/api_client.h index 6aa0cc80..bd8c736a 100644 --- a/src/fptn-protocol-lib/https/api_client/api_client.h +++ b/src/fptn-protocol-lib/https/api_client/api_client.h @@ -14,7 +14,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include -#include "fptn-protocol-lib/https/censorship_strategy.h" +#include "fptn-protocol-lib/https/connection_config.h" namespace fptn::protocol::https { @@ -49,18 +49,18 @@ class ApiClient { public: ApiClient(const std::string& host, int port, - CensorshipStrategy censorship_strategy); + HttpsInitConnectionStrategy censorship_strategy); ApiClient(std::string host, int port, std::string sni, - CensorshipStrategy censorship_strategy); + HttpsInitConnectionStrategy censorship_strategy); ApiClient(std::string host, int port, std::string sni, std::string md5_fingerprint, - CensorshipStrategy censorship_strategy); + HttpsInitConnectionStrategy censorship_strategy); Response Get(const std::string& handle, int timeout = 15) const; Response Post(const std::string& handle, @@ -91,7 +91,7 @@ class ApiClient { const int port_; const std::string sni_; const std::string expected_md5_fingerprint_; - const CensorshipStrategy censorship_strategy_; + const HttpsInitConnectionStrategy censorship_strategy_; }; using HttpsClientPtr = std::unique_ptr; diff --git a/src/fptn-protocol-lib/https/censorship_strategy.h b/src/fptn-protocol-lib/https/censorship_strategy.h deleted file mode 100644 index 6359c7fc..00000000 --- a/src/fptn-protocol-lib/https/censorship_strategy.h +++ /dev/null @@ -1,15 +0,0 @@ -/*============================================================================= -Copyright (c) 2024-2025 Stas Skokov - -Distributed under the MIT License (https://opensource.org/licenses/MIT) -=============================================================================*/ - -#pragma once - -namespace fptn::protocol::https { -enum class CensorshipStrategy : int { - kSni = 0, - kTlsObfuscator = 1, - kSniRealityMode = 2 -}; -} // namespace fptn::protocol::https diff --git a/src/fptn-protocol-lib/https/connection_config.h b/src/fptn-protocol-lib/https/connection_config.h new file mode 100644 index 00000000..83306d9e --- /dev/null +++ b/src/fptn-protocol-lib/https/connection_config.h @@ -0,0 +1,67 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include +#include + +#include "common/network/ip_address.h" +#include "common/network/ip_packet.h" + +namespace fptn::protocol::https { + +enum class HttpsInitConnectionStrategy : int { + kSni = 0, + kTlsObfuscator = 1, + kSniRealityMode = 2 +}; + +using IPv4Address = fptn::common::network::IPv4Address; +using IPv6Address = fptn::common::network::IPv6Address; + +using RecvIPPacketCallback = + std::function; + +using OnConnectedCallback = std::function; + +struct ConnectionConfig { + struct Common { + IPv4Address server_ip; + std::uint16_t server_port = 443; + + std::string sni; + std::string md5_fingerprint; + HttpsInitConnectionStrategy https_init_connection_strategy; + + IPv4Address tun_interface_address_ipv4; + IPv6Address tun_interface_address_ipv6; + + std::size_t connection_timeout_ms = 10000; + std::size_t max_reconnections = 5; + + OnConnectedCallback on_connected_callback = nullptr; + RecvIPPacketCallback recv_ip_packet_callback = nullptr; + } common; + + struct Pool { + std::size_t size = 3; + // int max_requests_per_connection = 1000; + // bool prewarm = true; + } pool; + + bool Validate() const { + if (common.server_ip.ToString().empty()) { + return false; + } + if (pool.size == 0) { + return false; + } + return true; + } +}; +}; // namespace fptn::protocol::https diff --git a/src/fptn-protocol-lib/https/websocket_client/websocket_client.cpp b/src/fptn-protocol-lib/https/websocket_client/websocket_client.cpp index 279d6360..72d9eec6 100644 --- a/src/fptn-protocol-lib/https/websocket_client/websocket_client.cpp +++ b/src/fptn-protocol-lib/https/websocket_client/websocket_client.cpp @@ -18,63 +18,51 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) namespace fptn::protocol::https { -WebsocketClient::WebsocketClient(fptn::common::network::IPv4Address server_ip, - int server_port, - fptn::common::network::IPv4Address tun_interface_address_ipv4, - fptn::common::network::IPv6Address tun_interface_address_ipv6, - NewIPPacketCallback new_ip_pkt_callback, - std::string sni, - std::string access_token, - std::string expected_md5_fingerprint, - CensorshipStrategy censorship_strategy, - OnConnectedCallback on_connected_callback, - int thread_number) - : ioc_(thread_number), +WebsocketClient::WebsocketClient(std::string jwt_access_token, + ConnectionConfig config, + boost::asio::io_context& ioc) + : ioc_(ioc), ctx_(https::utils::CreateNewSslCtx()), resolver_(boost::asio::make_strand(ioc_)), - censorship_strategy_(censorship_strategy), ws_(ssl_stream_type( obfuscator_socket_type(boost::asio::make_strand(ioc_), nullptr), ctx_)), strand_(boost::asio::make_strand(ioc_)), watchdog_timer_(strand_), write_channel_(strand_, kMaxSizeOutQueue_), - server_ip_(std::move(server_ip)), - server_port_str_(std::to_string(server_port)), - tun_interface_address_ipv4_(std::move(tun_interface_address_ipv4)), - tun_interface_address_ipv6_(std::move(tun_interface_address_ipv6)), - new_ip_pkt_callback_(std::move(new_ip_pkt_callback)), - sni_(std::move(sni)), - access_token_(std::move(access_token)), - expected_md5_fingerprint_(std::move(expected_md5_fingerprint)), - on_connected_callback_(std::move(on_connected_callback)) { + + jwt_access_token_(std::move(jwt_access_token)), + config_(std::move(config)) { auto* ssl = ws_.next_layer().native_handle(); - https::utils::SetHandshakeSni(ssl, sni_); + https::utils::SetHandshakeSni(ssl, config_.common.sni); https::utils::SetHandshakeSessionID(ssl); - if (censorship_strategy_ == CensorshipStrategy::kSni) { + if (config_.common.https_init_connection_strategy == + HttpsInitConnectionStrategy::kSni) { obfuscator_ = nullptr; } - if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { + if (config_.common.https_init_connection_strategy == + HttpsInitConnectionStrategy::kTlsObfuscator) { obfuscator_ = std::make_shared(); ws_.next_layer().next_layer().set_obfuscator(obfuscator_); } - if (censorship_strategy_ == CensorshipStrategy::kSniRealityMode) { + if (config_.common.https_init_connection_strategy == + HttpsInitConnectionStrategy::kSniRealityMode) { obfuscator_ = nullptr; } https::utils::AttachCertificateVerificationCallback( ssl, [this](const std::string& md5_fingerprint) mutable { - if (expected_md5_fingerprint_.empty()) { + if (config_.common.md5_fingerprint.empty()) { return true; } - if (md5_fingerprint == expected_md5_fingerprint_) { + if (md5_fingerprint == config_.common.md5_fingerprint) { return true; } SPDLOG_ERROR("Certificate MD5 mismatch. Expected: {}, got: {}.", - expected_md5_fingerprint_, md5_fingerprint); + config_.common.md5_fingerprint, md5_fingerprint); return false; }); @@ -92,19 +80,6 @@ WebsocketClient::~WebsocketClient() { } catch (...) { SPDLOG_WARN("Unknown error in ~WebsocketClient"); } - - // Stop io_context - try { - if (!ioc_.stopped()) { - SPDLOG_INFO("Stopping io_context..."); - ioc_.stop(); - } - } catch (const boost::system::system_error& err) { - SPDLOG_ERROR("Exception while stopping io_context: {}", err.what()); - } catch (...) { - SPDLOG_ERROR("Unknown exception while stopping io_context"); - } - SPDLOG_INFO("WebsocketClient removed"); } void WebsocketClient::Run() { @@ -113,7 +88,8 @@ void WebsocketClient::Run() { return; } - SPDLOG_INFO("Connecting to {}:{}", server_ip_.ToString(), server_port_str_); + SPDLOG_INFO("Connecting to {}:{}", config_.common.server_ip.ToString(), + config_.common.server_port); auto self = weak_from_this(); boost::asio::co_spawn( @@ -127,19 +103,6 @@ void WebsocketClient::Run() { } }, boost::asio::detached); - try { - while (running_ || !was_stopped_) { - const std::size_t processed = ioc_.poll_one(); - if (processed == 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - } - if (!ioc_.stopped()) { - ioc_.stop(); - } - } catch (...) { - SPDLOG_WARN("Exception while running"); - } } bool WebsocketClient::Stop() { @@ -158,9 +121,6 @@ bool WebsocketClient::Stop() { running_ = false; was_connected_ = false; - new_ip_pkt_callback_ = nullptr; - on_connected_callback_ = nullptr; - boost::system::error_code ec; try { @@ -307,9 +267,11 @@ boost::asio::awaitable WebsocketClient::Connect() { boost::system::error_code ec; try { // DNS resolution + const std::string server_port_str = + std::to_string(config_.common.server_port); boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); - auto results = co_await resolver_.async_resolve(server_ip_.ToString(), - server_port_str_, + auto results = co_await resolver_.async_resolve( + config_.common.server_ip.ToString(), server_port_str, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("Resolve error: {}", ec.message()); @@ -324,7 +286,8 @@ boost::asio::awaitable WebsocketClient::Connect() { co_return false; } - SPDLOG_INFO("Connected to {}:{}", server_ip_.ToString(), server_port_str_); + SPDLOG_INFO("Connected to {}:{}", config_.common.server_ip.ToString(), + config_.common.server_port); // TCP options boost::beast::get_lowest_layer(ws_).socket().set_option( @@ -335,7 +298,8 @@ boost::asio::awaitable WebsocketClient::Connect() { // packet inspection Then resets the connection state and activates // obfuscation for the real encrypted tunnel This dual-handshake approach // makes traffic analysis significantly more difficult - if (censorship_strategy_ == CensorshipStrategy::kSniRealityMode) { + if (config_.common.https_init_connection_strategy == + HttpsInitConnectionStrategy::kSniRealityMode) { const bool status = co_await PerformFakeHandshake(); if (!status) { co_return false; @@ -396,13 +360,16 @@ boost::asio::awaitable WebsocketClient::Connect() { // WebSocket handshake ws_.set_option(boost::beast::websocket::stream_base::decorator( [this](boost::beast::websocket::request_type& req) { - req.set("Authorization", "Bearer " + access_token_); - req.set("ClientIP", tun_interface_address_ipv4_.ToString()); - req.set("ClientIPv6", tun_interface_address_ipv6_.ToString()); + req.set("Authorization", "Bearer " + jwt_access_token_); + req.set( + "ClientIP", config_.common.tun_interface_address_ipv4.ToString()); + req.set("ClientIPv6", + config_.common.tun_interface_address_ipv6.ToString()); req.set("Client-Agent", fmt::format("FptnClient({}/{})", FPTN_USER_OS, FPTN_VERSION)); })); - co_await ws_.async_handshake(server_ip_.ToString(), kUrlWebSocket_, + co_await ws_.async_handshake(config_.common.server_ip.ToString(), + kUrlWebSocket_, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("WebSocket handshake error: {}", ec.message()); @@ -412,8 +379,8 @@ boost::asio::awaitable WebsocketClient::Connect() { was_connected_ = true; SPDLOG_INFO("WebSocket connection established successfully"); - if (on_connected_callback_) { - on_connected_callback_(); + if (config_.common.on_connected_callback) { + config_.common.on_connected_callback(); } co_return true; } catch (const std::exception& e) { @@ -444,8 +411,8 @@ boost::asio::awaitable WebsocketClient::RunReader() { std::string data = boost::beast::buffers_to_string(buffer.data()); std::string raw = protobuf::GetProtoPayload(std::move(data)); auto packet = fptn::common::network::IPPacket::Parse(std::move(raw)); - if (running_ && packet && new_ip_pkt_callback_) { - new_ip_pkt_callback_(std::move(packet)); + if (running_ && packet && config_.common.recv_ip_packet_callback) { + config_.common.recv_ip_packet_callback(std::move(packet)); } } buffer.consume(buffer.size()); @@ -497,10 +464,13 @@ boost::asio::awaitable WebsocketClient::RunSender() { boost::asio::awaitable WebsocketClient::PerformFakeHandshake() { boost::system::error_code ec; try { - SPDLOG_INFO("Generating and sending fake TLS handshake to {}", sni_); - const auto handshake_data = utils::GenerateDecoyTlsHandshake(sni_); + SPDLOG_INFO( + "Generating and sending fake TLS handshake to {}", config_.common.sni); + const auto handshake_data = + utils::GenerateDecoyTlsHandshake(config_.common.sni); if (handshake_data.empty()) { - SPDLOG_WARN("Failed to generate handshake data for SNI: {}", sni_); + SPDLOG_WARN( + "Failed to generate handshake data for SNI: {}", config_.common.sni); co_return false; } diff --git a/src/fptn-protocol-lib/https/websocket_client/websocket_client.h b/src/fptn-protocol-lib/https/websocket_client/websocket_client.h index c6163ec4..41e8f238 100644 --- a/src/fptn-protocol-lib/https/websocket_client/websocket_client.h +++ b/src/fptn-protocol-lib/https/websocket_client/websocket_client.h @@ -24,7 +24,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "common/network/ip_address.h" #include "common/network/ip_packet.h" -#include "fptn-protocol-lib/https/censorship_strategy.h" +#include "fptn-protocol-lib/https/connection_config.h" #include "fptn-protocol-lib/https/obfuscator/tcp_stream/tcp_stream.h" #include "fptn-protocol-lib/https/utils/tls/tls.h" #include "fptn-protocol-lib/protobuf/protocol.h" @@ -33,21 +33,9 @@ namespace fptn::protocol::https { class WebsocketClient : public std::enable_shared_from_this { public: - using NewIPPacketCallback = - std::function; - using OnConnectedCallback = std::function; - - explicit WebsocketClient(fptn::common::network::IPv4Address server_ip, - int server_port, - fptn::common::network::IPv4Address tun_interface_address_ipv4, - fptn::common::network::IPv6Address tun_interface_address_ipv6, - NewIPPacketCallback new_ip_pkt_callback, - std::string sni, - std::string access_token, - std::string expected_md5_fingerprint, - CensorshipStrategy censorship_strategy, - OnConnectedCallback on_connected_callback = nullptr, - int thread_number = 4); + explicit WebsocketClient(std::string jwt_access_token, + ConnectionConfig config, + boost::asio::io_context& ioc); virtual ~WebsocketClient(); @@ -81,18 +69,17 @@ class WebsocketClient : public std::enable_shared_from_this { std::atomic was_inited_{false}; std::atomic was_connected_{false}; - boost::asio::io_context ioc_; + boost::asio::io_context& ioc_; boost::asio::ssl::context ctx_; boost::asio::ip::tcp::resolver resolver_; - const CensorshipStrategy censorship_strategy_; + boost::asio::cancellation_signal cancel_signal_; // TCP -> obfuscator -> SSL -> WebSocket using tcp_stream_type = boost::beast::tcp_stream; using obfuscator_socket_type = obfuscator::TcpStream; using ssl_stream_type = boost::beast::ssl_stream; using websocket_type = boost::beast::websocket::stream; - websocket_type ws_; boost::asio::strand strand_; @@ -103,25 +90,12 @@ class WebsocketClient : public std::enable_shared_from_this { boost::system::error_code, fptn::common::network::IPPacketPtr)> write_channel_; - const fptn::common::network::IPv4Address server_ip_; - const std::string server_port_str_; - - const fptn::common::network::IPv4Address tun_interface_address_ipv4_; - const fptn::common::network::IPv6Address tun_interface_address_ipv6_; - - NewIPPacketCallback new_ip_pkt_callback_; - - const std::string sni_; - const std::string access_token_; - const std::string expected_md5_fingerprint_; - - OnConnectedCallback on_connected_callback_; - - boost::asio::cancellation_signal cancel_signal_; + const std::string jwt_access_token_; + const ConnectionConfig config_; obfuscator::IObfuscatorSPtr obfuscator_; }; -using WebsocketClientSPtr = std::shared_ptr; +using WebsocketClientPtr = std::shared_ptr; } // namespace fptn::protocol::https diff --git a/src/fptn-server/CMakeLists.txt b/src/fptn-server/CMakeLists.txt index adc3f291..8ed043b3 100644 --- a/src/fptn-server/CMakeLists.txt +++ b/src/fptn-server/CMakeLists.txt @@ -35,20 +35,24 @@ include_directories(${Boost_INCLUDE_DIRS}) add_executable( "${PROJECT_NAME}" fptn-server.cpp - nat/table.h - nat/table.cpp - client/session.h - client/session.cpp network/virtual_interface.h network/virtual_interface.cpp - traffic_shaper/leaky_bucket.h - traffic_shaper/leaky_bucket.cpp + nat/table.h + nat/table.cpp + nat/statistic/metrics.h + nat/statistic/metrics.cpp + nat/connection_multiplexer/connection_multiplexer.h + nat/connection_multiplexer/connection_multiplexer.cpp + nat/client_connection/client_connection.h + nat/client_connection/client_connection.cpp + nat/traffic_shaper/leaky_bucket.h + nat/traffic_shaper/leaky_bucket.cpp routing/iptables.h routing/iptables.cpp web/listener/listener.h web/listener/listener.cpp - web/session/session.h - web/session/session.cpp + web/client_endpoint/client_endpoint.h + web/client_endpoint/client_endpoint.cpp web/server.h web/server.cpp web/handshake/handshake_cache_manager.h @@ -62,8 +66,6 @@ add_executable( filter/filters/bittorrent/bittorrent.cpp vpn/manager.h vpn/manager.cpp - statistic/metrics.h - statistic/metrics.cpp config/command_line_config.cpp config/command_line_config.h user/user_manager.cpp diff --git a/src/fptn-server/client/session.cpp b/src/fptn-server/client/session.cpp deleted file mode 100644 index 5b71b4ff..00000000 --- a/src/fptn-server/client/session.cpp +++ /dev/null @@ -1,98 +0,0 @@ -/*============================================================================= -Copyright (c) 2024-2025 Stas Skokov - -Distributed under the MIT License (https://opensource.org/licenses/MIT) -=============================================================================*/ - -#include "client/session.h" - -#include -#include - -using fptn::client::Session; -using fptn::common::network::IPv4Address; -using fptn::common::network::IPv6Address; - -Session::Session(ClientID client_id, - std::string user_name, - IPv4Address client_ipv4, - IPv4Address fake_client_ipv4, - IPv6Address client_ipv6, - IPv6Address fake_client_ipv6, - fptn::traffic_shaper::LeakyBucketSPtr to_client, - fptn::traffic_shaper::LeakyBucketSPtr from_client) - : client_id_(client_id), - user_name_(std::move(user_name)), - client_ipv4_(std::move(client_ipv4)), - fake_client_ipv4_(std::move(fake_client_ipv4)), - client_ipv6_(std::move(client_ipv6)), - fake_client_ipv6_(std::move(fake_client_ipv6)), - to_client_(std::move(to_client)), - from_client_(std::move(from_client)) {} - -const fptn::ClientID& Session::ClientId() const noexcept { return client_id_; } - -const std::string& Session::UserName() const noexcept { return user_name_; } - -const IPv4Address& Session::ClientIPv4() const noexcept { return client_ipv4_; } - -const IPv4Address& Session::FakeClientIPv4() const noexcept { - return fake_client_ipv4_; -} - -const IPv6Address& Session::ClientIPv6() const noexcept { return client_ipv6_; } - -const IPv6Address& Session::FakeClientIPv6() const noexcept { - return fake_client_ipv6_; -} - -fptn::traffic_shaper::LeakyBucketSPtr& -Session::TrafficShaperToClient() noexcept { - return to_client_; -} - -fptn::traffic_shaper::LeakyBucketSPtr& -Session::TrafficShaperFromClient() noexcept { - return from_client_; -} - -fptn::common::network::IPPacketPtr Session::ChangeIPAddressToClientIP( - fptn::common::network::IPPacketPtr packet) noexcept { - packet->SetClientId(client_id_); - -#ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP - if (packet->IsIPv4()) { - packet->SetDstIPv4Address(client_ipv4_.ToString()); - } else if (packet->IsIPv6()) { - packet->SetDstIPv6Address(client_ipv6_.ToString()); - } -#else - if (packet->IsIPv4()) { - packet->SetDstIPv4Address(client_ipv4_.Get()); - } else if (packet->IsIPv6()) { - packet->SetDstIPv6Address(client_ipv6_.Get()); - } -#endif - packet->ComputeCalculateFields(); - return packet; -} - -fptn::common::network::IPPacketPtr Session::ChangeIPAddressToFakeIP( - fptn::common::network::IPPacketPtr packet) noexcept { - packet->SetClientId(client_id_); -#ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP - if (packet->IsIPv4()) { - packet->SetSrcIPv4Address(fake_client_ipv4_.ToString()); - } else if (packet->IsIPv6()) { - packet->SetSrcIPv6Address(fake_client_ipv6_.ToString()); - } -#else - if (packet->IsIPv4()) { - packet->SetSrcIPv4Address(fake_client_ipv4_.Get()); - } else if (packet->IsIPv6()) { - packet->SetSrcIPv6Address(fake_client_ipv6_.Get()); - } -#endif - packet->ComputeCalculateFields(); - return packet; -} diff --git a/src/fptn-server/client/session.h b/src/fptn-server/client/session.h deleted file mode 100644 index f185f0ac..00000000 --- a/src/fptn-server/client/session.h +++ /dev/null @@ -1,68 +0,0 @@ -/*============================================================================= -Copyright (c) 2024-2025 Stas Skokov - -Distributed under the MIT License (https://opensource.org/licenses/MIT) -=============================================================================*/ -#pragma once - -#include -#include -#include - -#include // NOLINT(build/include_order) -#include // NOLINT(build/include_order) -#include // NOLINT(build/include_order) - -#include "common/client_id.h" -#include "common/network/ip_address.h" - -#include "traffic_shaper/leaky_bucket.h" - -namespace fptn::client { - -using fptn::common::network::IPv4Address; -using fptn::common::network::IPv6Address; - -class Session final { - public: - Session(ClientID client_id, - std::string user_name, - IPv4Address client_ipv4, - IPv4Address fake_client_ipv4, - IPv6Address client_ipv6, - IPv6Address fake_client_ipv6, - fptn::traffic_shaper::LeakyBucketSPtr to_client, - fptn::traffic_shaper::LeakyBucketSPtr from_client); - [[nodiscard]] const ClientID& ClientId() const noexcept; - - [[nodiscard]] const std::string& UserName() const noexcept; - - [[nodiscard]] const IPv4Address& ClientIPv4() const noexcept; - [[nodiscard]] const IPv4Address& FakeClientIPv4() const noexcept; - - [[nodiscard]] const IPv6Address& ClientIPv6() const noexcept; - [[nodiscard]] const IPv6Address& FakeClientIPv6() const noexcept; - - fptn::traffic_shaper::LeakyBucketSPtr& TrafficShaperToClient() noexcept; - fptn::traffic_shaper::LeakyBucketSPtr& TrafficShaperFromClient() noexcept; - - fptn::common::network::IPPacketPtr ChangeIPAddressToClientIP( - fptn::common::network::IPPacketPtr packet) noexcept; - fptn::common::network::IPPacketPtr ChangeIPAddressToFakeIP( - fptn::common::network::IPPacketPtr packet) noexcept; - - private: - const ClientID client_id_; - const std::string user_name_; - const IPv4Address client_ipv4_; - const IPv4Address fake_client_ipv4_; - const IPv6Address client_ipv6_; - const IPv6Address fake_client_ipv6_; - - fptn::traffic_shaper::LeakyBucketSPtr to_client_; - fptn::traffic_shaper::LeakyBucketSPtr from_client_; -}; - -using SessionSPtr = std::shared_ptr; - -} // namespace fptn::client diff --git a/src/fptn-server/fptn-server.cpp b/src/fptn-server/fptn-server.cpp index fe8ad9f5..5959caac 100644 --- a/src/fptn-server/fptn-server.cpp +++ b/src/fptn-server/fptn-server.cpp @@ -23,7 +23,7 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "nat/table.h" #include "network/virtual_interface.h" #include "routing/iptables.h" -#include "statistic/metrics.h" +#include "nat/statistic/metrics.h" #include "user/user_manager.h" #include "vpn/manager.h" #include "web/server.h" diff --git a/src/fptn-server/nat/client_connection/client_connection.cpp b/src/fptn-server/nat/client_connection/client_connection.cpp new file mode 100644 index 00000000..2d5b5903 --- /dev/null +++ b/src/fptn-server/nat/client_connection/client_connection.cpp @@ -0,0 +1,22 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#include "nat/client_connection/client_connection.h" + +#include "common/network/ip_packet.h" + +#include "nat/connect_params.h" + +namespace fptn::nat { + +ClientConnection::ClientConnection(fptn::nat::ConnectParams params) + : params_(std::move(params)) {} + +const fptn::nat::ConnectParams& ClientConnection::Params() const noexcept { + return params_; +} + +} // namespace fptn::nat diff --git a/src/fptn-server/nat/client_connection/client_connection.h b/src/fptn-server/nat/client_connection/client_connection.h new file mode 100644 index 00000000..08c186f8 --- /dev/null +++ b/src/fptn-server/nat/client_connection/client_connection.h @@ -0,0 +1,33 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include + +#include "nat/connect_params.h" + +namespace fptn::nat { + +class ClientConnection final { + public: + static std::unique_ptr Create( + fptn::nat::ConnectParams params) { + return std::make_unique(std::move(params)); + } + + public: + ClientConnection(fptn::nat::ConnectParams params); + + const fptn::nat::ConnectParams& Params() const noexcept; + + private: + const fptn::nat::ConnectParams params_; +}; + +using ClientConnectionPtr = std::unique_ptr; + +} // namespace fptn::client diff --git a/src/fptn-server/nat/connect_params.h b/src/fptn-server/nat/connect_params.h new file mode 100644 index 00000000..c22387cf --- /dev/null +++ b/src/fptn-server/nat/connect_params.h @@ -0,0 +1,78 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#pragma once + +#include +#include +#include +#include + +#include "common/client_id.h" +#include "common/network/ip_address.h" + +namespace fptn::nat { +struct ConnectParams { + ClientID client_id = MAX_CLIENT_ID; + struct Request { + std::string url; + + std::string jwt_auth_token; + std::string session_id; + + std::uint64_t connection_weight = 1; + + fptn::common::network::IPv4Address client_ipv4; + fptn::common::network::IPv4Address client_tun_vpn_ipv4; + fptn::common::network::IPv6Address client_tun_vpn_ipv6; + } request; + + struct User { + std::string username; + std::size_t bandwidth_bites_seconds = 0; + } user; + + struct Timings { + std::chrono::system_clock::time_point expire_after; + std::chrono::system_clock::time_point silence_mode_until; + + [[nodiscard]] bool HasExpiration() const noexcept { + return expire_after > std::chrono::system_clock::time_point{}; + } + + [[nodiscard]] bool IsSilenceModeActive() const noexcept { + const auto now = std::chrono::system_clock::now(); + return silence_mode_until > now; + } + + void SetExpireAfter(const std::uint64_t ts) noexcept { + if (ts != 0) { + expire_after = std::chrono::system_clock::from_time_t( + static_cast(ts)); + } else { + expire_after = std::chrono::system_clock::time_point{}; + } + } + + void SetSilenceModeUntil(const std::uint64_t ts) noexcept { + if (ts != 0) { + silence_mode_until = std::chrono::system_clock::from_time_t( + static_cast(ts)); + } else { + silence_mode_until = std::chrono::system_clock::time_point{}; + } + } + + } timings; + + bool Validate() const { + return client_id != MAX_CLIENT_ID && !request.client_ipv4.IsEmpty() && + !request.client_tun_vpn_ipv4.IsEmpty() && + !request.client_tun_vpn_ipv6.IsEmpty() && + !request.jwt_auth_token.empty() && !request.session_id.empty(); + } +}; +} // namespace fptn::nat diff --git a/src/fptn-server/nat/connection_multiplexer/connection_multiplexer.cpp b/src/fptn-server/nat/connection_multiplexer/connection_multiplexer.cpp new file mode 100644 index 00000000..313b4d73 --- /dev/null +++ b/src/fptn-server/nat/connection_multiplexer/connection_multiplexer.cpp @@ -0,0 +1,134 @@ +/*============================================================================= +Copyright (c) 2024-2025 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +=============================================================================*/ + +#include "nat/connection_multiplexer/connection_multiplexer.h" + +#include +#include + +#include "fptn-server/nat/connect_params.h" + +namespace fptn::nat { + +ConnectionMultiplexer::ConnectionMultiplexer(const ConnectParams& params, + IPv4Address fake_client_ipv4, + IPv6Address fake_client_ipv6) + : username_(params.user.username), + session_id_(params.request.session_id), + fake_client_ipv4_(std::move(fake_client_ipv4)), + fake_client_ipv6_(std::move(fake_client_ipv6)), + shaper_to_websocket_(params.user.bandwidth_bites_seconds), + shaper_from_websocket_(params.user.bandwidth_bites_seconds) { + connection_params_.insert( + {params.client_id, ClientConnection::Create(params)}); +} + +bool ConnectionMultiplexer::AddClientConnection(const ConnectParams& params) { + const std::unique_lock lock(mutex_); // mutex + + if (connection_params_.contains(params.client_id)) { + return false; + } + auto client_connection = ClientConnection::Create(params); + connection_params_.insert({params.client_id, std::move(client_connection)}); + return true; +} + +common::network::IPPacketPtr ConnectionMultiplexer::PacketPreparingToWebsocket( + common::network::IPPacketPtr packet) { + const std::unique_lock lock(mutex_); // mutex + + if (connection_params_.empty()) { + return nullptr; + } + + if (!shaper_to_websocket_.CanProcessPacket(packet->Size())) { + return nullptr; + } + +#ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP + if (packet->IsIPv4()) { + packet->SetSrcIPv4Address(fake_client_ipv4_.ToString()); + } else if (packet->IsIPv6()) { + packet->SetSrcIPv6Address(fake_client_ipv6_.ToString()); + } else { + return nullptr; + } +#else + if (packet->IsIPv4()) { + packet->SetSrcIPv4Address(fake_client_ipv4_.Get()); + } else if (packet->IsIPv6()) { + packet->SetSrcIPv6Address(fake_client_ipv6_.Get()); + } else { + return nullptr; + } +#endif + packet->ComputeCalculateFields(); + return packet; +} + +common::network::IPPacketPtr +ConnectionMultiplexer::PacketPreparingFromWebsocket( + common::network::IPPacketPtr packet) { + if (!shaper_from_websocket_.CanProcessPacket(packet->Size())) { + return nullptr; + } + + const auto& connection = connection_params_.begin()->second; +#ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP + if (packet->IsIPv4()) { + packet->SetDstIPv4Address( + connection->Params().request.client_tun_vpn_ipv4.ToString()); + } else if (packet->IsIPv6()) { + packet->SetDstIPv6Address( + connection->Params().request.client_tun_vpn_ipv6.ToString()); + } +#else + if (packet->IsIPv4()) { + packet->SetDstIPv4Address( + connection->Params().request.client_tun_vpn_ipv4.Get()); + } else if (packet->IsIPv6()) { + packet->SetDstIPv6Address( + connection->Params().request.client_tun_vpn_ipv6.Get()); + } +#endif + packet->ComputeCalculateFields(); + return packet; +} + +bool ConnectionMultiplexer::HasClientId(fptn::ClientID client_id) const { + const std::unique_lock lock(mutex_); // mutex + + return connection_params_.contains(client_id); +} + +bool ConnectionMultiplexer::DelConnectionByClientId(fptn::ClientID client_id) { + const std::unique_lock lock(mutex_); // mutex + + return connection_params_.erase(client_id) > 0; +} + +const std::string& ConnectionMultiplexer::Username() const { return username_; } + +std::size_t ConnectionMultiplexer::Size() const { + const std::unique_lock lock(mutex_); // mutex + + return connection_params_.size(); +} + +const std::string& ConnectionMultiplexer::SessionId() const { + return session_id_; +} + +const IPv4Address& ConnectionMultiplexer::FakeClientIPv4() const noexcept { + return fake_client_ipv4_; +} + +const IPv6Address& ConnectionMultiplexer::FakeClientIPv6() const noexcept { + return fake_client_ipv6_; +} + +} // namespace fptn::nat diff --git a/src/fptn-server/nat/connection_multiplexer/connection_multiplexer.h b/src/fptn-server/nat/connection_multiplexer/connection_multiplexer.h new file mode 100644 index 00000000..708feaa4 --- /dev/null +++ b/src/fptn-server/nat/connection_multiplexer/connection_multiplexer.h @@ -0,0 +1,86 @@ +/*============================================================================= +Copyright (c) 2024-2026 Stas Skokov + +Distributed under the MIT License (https://opensource.org/licenses/MIT) +==============================================================================*/ + +#pragma once + +#include +#include +#include + +#include "common/client_id.h" +#include "common/network/ip_address.h" + +#include "fptn-server/nat/traffic_shaper/leaky_bucket.h" +#include "nat/client_connection/client_connection.h" + +namespace fptn::nat { + +using fptn::common::network::IPv4Address; +using fptn::common::network::IPv6Address; + +using ClientConnections = + std::unordered_map; + +class ConnectionMultiplexer { + public: + static std::shared_ptr Create( + const ConnectParams& params, + IPv4Address fake_client_ipv4, + IPv6Address fake_client_ipv6) { + return std::make_shared( + params, std::move(fake_client_ipv4), std::move(fake_client_ipv6)); + } + + public: + ConnectionMultiplexer(const ConnectParams& params, + IPv4Address fake_client_ipv4, + IPv6Address fake_client_ipv6); + + bool AddClientConnection(const ConnectParams& params); + + common::network::IPPacketPtr PacketPreparingToWebsocket( + common::network::IPPacketPtr packet); + + common::network::IPPacketPtr PacketPreparingFromWebsocket( + common::network::IPPacketPtr packet); + + bool HasClientId(fptn::ClientID client_id) const; + bool DelConnectionByClientId(fptn::ClientID client_id); + + public: + [[nodiscard]] + const std::string& Username() const; + + [[nodiscard]] + std::size_t Size() const; + + [[nodiscard]] + const std::string& SessionId() const; + + [[nodiscard]] + const IPv4Address& FakeClientIPv4() const noexcept; + + [[nodiscard]] + const IPv6Address& FakeClientIPv6() const noexcept; + + private: + mutable std::mutex mutex_; + + const std::string username_; + const std::string session_id_; + + const IPv4Address fake_client_ipv4_; + const IPv6Address fake_client_ipv6_; + + fptn::nat::traffic_shaper::LeakyBucket shaper_to_websocket_; + fptn::nat::traffic_shaper::LeakyBucket shaper_from_websocket_; + + ClientConnections connection_params_; +}; + +using ConnectionMultiplexerSPtr = std::shared_ptr; + +} // namespace fptn::nat diff --git a/src/fptn-server/statistic/metrics.cpp b/src/fptn-server/nat/statistic/metrics.cpp similarity index 98% rename from src/fptn-server/statistic/metrics.cpp rename to src/fptn-server/nat/statistic/metrics.cpp index d14d859d..b2367987 100644 --- a/src/fptn-server/statistic/metrics.cpp +++ b/src/fptn-server/nat/statistic/metrics.cpp @@ -4,7 +4,7 @@ Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ -#include "statistic/metrics.h" +#include "nat/statistic/metrics.h" #include #include diff --git a/src/fptn-server/statistic/metrics.h b/src/fptn-server/nat/statistic/metrics.h similarity index 100% rename from src/fptn-server/statistic/metrics.h rename to src/fptn-server/nat/statistic/metrics.h diff --git a/src/fptn-server/nat/table.cpp b/src/fptn-server/nat/table.cpp index d5a1e7d1..8700c266 100644 --- a/src/fptn-server/nat/table.cpp +++ b/src/fptn-server/nat/table.cpp @@ -1,5 +1,5 @@ /*============================================================================= -Copyright (c) 2024-2025 Stas Skokov +Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ @@ -12,7 +12,72 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include // NOLINT(build/include_order) -using fptn::nat::Table; +namespace { + +fptn::nat::ConnectionMultiplexerSPtr GetMultiplexerBySessionId( + const fptn::nat::NatMultiplexers& multiplexers, + const std::string& session_id) { + if (multiplexers.contains(session_id)) { + return multiplexers.at(session_id); + } + return nullptr; +} + +fptn::nat::ConnectionMultiplexerSPtr GetMultiplexerByClientId( + const fptn::nat::NatMultiplexers& multiplexers, + const fptn::ClientID& client_id) { + const auto it = + std::ranges::find_if(multiplexers, [&client_id](const auto& pair) { + const auto& multiplexer = pair.second; + return multiplexer && multiplexer->HasClientId(client_id); + }); + if (it != multiplexers.end()) { + return it->second; + } + return nullptr; +} + +fptn::nat::ConnectionMultiplexerSPtr GetMultiplexerByFakeIPv4( + const fptn::nat::NatMultiplexers& multiplexers, + const fptn::common::network::IPv4Address& ip) { + const auto it = std::ranges::find_if(multiplexers, [&ip](const auto& pair) { + const auto& multiplexer = pair.second; + return multiplexer && multiplexer->FakeClientIPv4() == ip; + }); + if (it != multiplexers.end()) { + return it->second; + } + return nullptr; +} + +fptn::nat::ConnectionMultiplexerSPtr GetMultiplexerByFakeIPv6( + const fptn::nat::NatMultiplexers& multiplexers, + const fptn::common::network::IPv6Address& ip) { + const auto it = std::ranges::find_if(multiplexers, [&ip](const auto& pair) { + const auto& multiplexer = pair.second; + return multiplexer && multiplexer->FakeClientIPv6() == ip; + }); + if (it != multiplexers.end()) { + return it->second; + } + return nullptr; +} + +std::size_t GetNumberActiveSessionByUsername( + const fptn::nat::NatMultiplexers& multiplexers, + const std::string& username) { + std::size_t number = 0; + for (const auto& [session_id, multiplexer] : multiplexers) { + if (multiplexer->Username() == username) { + ++number; + } + } + return number; +} + +} // namespace + +namespace fptn::nat { Table::Table(fptn::common::network::IPv4Address tun_ipv4, fptn::common::network::IPv4Address tun_ipv4_network_address, @@ -30,149 +95,124 @@ Table::Table(fptn::common::network::IPv4Address tun_ipv4, ipv4_generator_(tun_ipv4_network_address_, tun_network_ipv4_mask_), ipv6_generator_(tun_ipv6_network_address_, tun_network_ipv6_mask_) {} -fptn::client::SessionSPtr Table::CreateClientSession(ClientID client_id, - const std::string& user_name, - const fptn::common::network::IPv4Address& client_ipv4, - const fptn::common::network::IPv6Address& client_ipv6, - const fptn::traffic_shaper::LeakyBucketSPtr& to_client, - const fptn::traffic_shaper::LeakyBucketSPtr& from_client) { +ConnectionMultiplexerSPtr Table::AddConnection( + const fptn::nat::ConnectParams& params) { const std::unique_lock lock(mutex_); // mutex - if (!client_id_to_sessions_.contains(client_id)) { - if (client_number_ >= ipv4_generator_.NumAvailableAddresses()) { - /* || client_number_ >= client_ipv6.NumAvailableAddresses() */ - SPDLOG_INFO("Client limit was exceeded"); - return nullptr; - } - client_number_ += 1; - try { - const auto fake_ipv4 = GetUniqueIPv4Address(); - const auto fake_ipv6 = GetUniqueIPv6Address(); - auto session = std::make_shared(client_id, - user_name, client_ipv4, fake_ipv4, client_ipv6, fake_ipv6, to_client, - from_client); - client_id_to_sessions_.insert({client_id, session}); - ipv4_to_sessions_.insert( - {fake_ipv4.ToInt(), session}); // ipv4 -> session - ipv6_to_sessions_.insert( - {fake_ipv6.ToString(), session}); // ipv6 -> session - return session; - } catch (const std::runtime_error& err) { - SPDLOG_INFO("Client error: {}", err.what()); - } catch (const std::exception& e) { - SPDLOG_ERROR( - "Standard exception while creating client session: {}", e.what()); - } catch (...) { - SPDLOG_ERROR("An unknown error occurred while creating client session."); + if (client_number_ >= ipv4_generator_.NumAvailableAddresses()) { + SPDLOG_INFO("Client limit was exceeded"); + return nullptr; + } + + if (auto mplx = + GetMultiplexerBySessionId(multiplexers_, params.request.session_id)) { + if (mplx->AddClientConnection(params)) { + multiplexers_.insert({params.request.session_id, mplx}); + return mplx; } + return nullptr; } - return nullptr; + + const auto fake_ipv4_opt = GetUniqueIPv4Address(); + const auto fake_ipv6_opt = GetUniqueIPv6Address(); + if (!fake_ipv4_opt.has_value() || !fake_ipv6_opt.has_value()) { + return nullptr; + } + + const auto& fake_ipv4 = fake_ipv4_opt.value(); + const auto& fake_ipv6 = fake_ipv6_opt.value(); + + auto mplx = ConnectionMultiplexer::Create(params, fake_ipv4, fake_ipv6); + + // DO NOT REMOVE + // session_id_to_connections_.insert({params.request.session_id, mplx}); + // ipv4_to_mplxs_.insert({fake_ipv4.ToInt(), mplx}); // ipv4 -> session + // ipv6_to_mplxs_.insert({fake_ipv6.ToString(), mplx}); // ipv6 -> session + + multiplexers_.insert({params.request.session_id, mplx}); + + client_number_ += 1; + + return mplx; } -bool Table::DelClientSession(ClientID client_id) { - fptn::client::SessionSPtr ipv4_session; - fptn::client::SessionSPtr ipv6_session; - { - const std::unique_lock lock(mutex_); // mutex - - auto it = client_id_to_sessions_.find(client_id); - if (it != client_id_to_sessions_.end()) { - const IPv4INT ipv4_int = it->second->FakeClientIPv4().ToInt(); - const std::string ipv6_str = it->second->FakeClientIPv6().ToString(); - client_id_to_sessions_.erase(it); - - // delete ipv4 -> session - { - auto it_ipv4 = ipv4_to_sessions_.find(ipv4_int); - if (it_ipv4 != ipv4_to_sessions_.end()) { - ipv4_session = std::move(it_ipv4->second); - ipv4_to_sessions_.erase(it_ipv4); - } - } - // delete ipv6 -> session - { - auto it_ipv6 = ipv6_to_sessions_.find(ipv6_str); - if (it_ipv6 != ipv6_to_sessions_.end()) { - ipv6_session = std::move(it_ipv6->second); - ipv6_to_sessions_.erase(it_ipv6); - } - } - } +bool Table::DelConnectionByClientId(ClientID client_id) noexcept { + const std::unique_lock lock(mutex_); + + auto multiplexer = GetMultiplexerByClientId(multiplexers_, client_id); + if (multiplexer == nullptr) { + return false; } - return ipv4_session != nullptr && ipv6_session != nullptr; + + multiplexer->DelConnectionByClientId(client_id); + + if (multiplexer->Size() == 0) { + return multiplexers_.erase(multiplexer->SessionId()) > 0; + } + return true; } -fptn::client::SessionSPtr Table::GetSessionByFakeIPv4( +fptn::nat::ConnectionMultiplexerSPtr Table::GetConnectionMultiplexerByFakeIPv4( const fptn::common::network::IPv4Address& ip) noexcept { const std::unique_lock lock(mutex_); // mutex - const auto it = ipv4_to_sessions_.find(ip.ToInt()); - if (it != ipv4_to_sessions_.end()) { - return it->second; - } - return nullptr; + return ::GetMultiplexerByFakeIPv4(multiplexers_, ip); } -fptn::client::SessionSPtr Table::GetSessionByFakeIPv6( +fptn::nat::ConnectionMultiplexerSPtr Table::GetConnectionMultiplexerByFakeIPv6( const fptn::common::network::IPv6Address& ip) noexcept { const std::unique_lock lock(mutex_); // mutex - auto it = ipv6_to_sessions_.find(ip.ToString()); - if (it != ipv6_to_sessions_.end()) { - return it->second; - } - return nullptr; + return ::GetMultiplexerByFakeIPv6(multiplexers_, ip); } -fptn::client::SessionSPtr Table::GetSessionByClientId( +fptn::nat::ConnectionMultiplexerSPtr Table::GetConnectionMultiplexerByClientId( ClientID clientId) noexcept { const std::unique_lock lock(mutex_); // mutex - auto it = client_id_to_sessions_.find(clientId); - if (it != client_id_to_sessions_.end()) { - return it->second; - } - return nullptr; + return ::GetMultiplexerByClientId(multiplexers_, clientId); } std::size_t Table::GetNumberActiveSessionByUsername( - const std::string& username) { + const std::string& username) noexcept { const std::unique_lock lock(mutex_); // mutex - return std::count_if(ipv4_to_sessions_.begin(), ipv4_to_sessions_.end(), - [&username]( - const auto& pair) { return pair.second->UserName() == username; }); + return ::GetNumberActiveSessionByUsername(multiplexers_, username); } -fptn::common::network::IPv4Address Table::GetUniqueIPv4Address() { +std::optional Table::GetUniqueIPv4Address() noexcept { for (std::uint32_t i = 0; i < ipv4_generator_.NumAvailableAddresses(); i++) { - const auto ip = ipv4_generator_.GetNextAddress(); - if (ip != tun_ipv4_ && !ipv4_to_sessions_.contains(ip.ToInt())) { + auto ip = ipv4_generator_.GetNextAddress(); + if (ip != tun_ipv4_ && GetMultiplexerByFakeIPv4(multiplexers_, ip)) { return ip; } } - throw std::runtime_error("No available address"); + return std::nullopt; } -fptn::common::network::IPv6Address Table::GetUniqueIPv6Address() { +std::optional Table::GetUniqueIPv6Address() noexcept { for (int i = 0; i < ipv6_generator_.NumAvailableAddresses(); i++) { - const auto ip = ipv6_generator_.GetNextAddress(); - if (ip != tun_ipv6_ && !ipv6_to_sessions_.contains(ip.ToString())) { + auto ip = ipv6_generator_.GetNextAddress(); + if (ip != tun_ipv6_ && !GetMultiplexerByFakeIPv6(multiplexers_, ip)) { return ip; } } - throw std::runtime_error("No available address"); + return std::nullopt; } -void Table::UpdateStatistic(const fptn::statistic::MetricsSPtr& prometheus) { +void Table::UpdateStatistic( + const fptn::statistic::MetricsSPtr& prometheus) noexcept { const std::unique_lock lock(mutex_); // mutex - prometheus->UpdateActiveSessions(client_id_to_sessions_.size()); - for (const auto& client : client_id_to_sessions_) { - auto client_id = client.first; - const auto& session = client.second; - prometheus->UpdateStatistics(client_id, session->UserName(), - session->TrafficShaperToClient()->FullDataAmount(), - session->TrafficShaperFromClient()->FullDataAmount()); - } + (void)prometheus; + // + // prometheus->UpdateActiveSessions(client_id_to_mplxs_.size()); + // for (const auto& client : client_id_to_mplxs_) { + // auto client_id = client.first; + // const auto& session = client.second; + // prometheus->UpdateStatistics(client_id, session->UserName(), + // session->TrafficShaperToClient()->FullDataAmount(), + // session->TrafficShaperFromClient()->FullDataAmount()); + // } } +} // namespace fptn::nat \ No newline at end of file diff --git a/src/fptn-server/nat/table.h b/src/fptn-server/nat/table.h index 9244f74e..03650655 100644 --- a/src/fptn-server/nat/table.h +++ b/src/fptn-server/nat/table.h @@ -8,55 +8,59 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include +#include #include #include -#include // NOLINT(build/include_order) -#include // NOLINT(build/include_order) -#include // NOLINT(build/include_order) - #include "common/network/ip_address.h" #include "common/network/ipv4_generator.h" #include "common/network/ipv6_generator.h" -#include "client/session.h" -#include "statistic/metrics.h" -#include "traffic_shaper/leaky_bucket.h" +#include "connect_params.h" +#include "connection_multiplexer/connection_multiplexer.h" +#include "nat/client_connection/client_connection.h" +#include "nat/statistic/metrics.h" +#include "nat/traffic_shaper/leaky_bucket.h" namespace fptn::nat { +using fptn::common::network::IPv4Address; +using fptn::common::network::IPv6Address; + +using NatMultiplexers = + std::unordered_map; + class Table final { using IPv4INT = std::uint32_t; public: - Table(fptn::common::network::IPv4Address tun_ipv4, - fptn::common::network::IPv4Address tun_ipv4_network_address, + Table(IPv4Address tun_ipv4, + IPv4Address tun_ipv4_network_address, std::uint32_t tun_network_ipv4_mask, - fptn::common::network::IPv6Address tun_ipv6, - fptn::common::network::IPv6Address tun_ipv6_network_address, + IPv6Address tun_ipv6, + IPv6Address tun_ipv6_network_address, std::uint32_t tun_network_ipv6_mask); - fptn::client::SessionSPtr CreateClientSession(ClientID client_id, - const std::string& user_name, - const fptn::common::network::IPv4Address& client_ipv4, - const fptn::common::network::IPv6Address& client_ipv6, - const fptn::traffic_shaper::LeakyBucketSPtr& to_client, - const fptn::traffic_shaper::LeakyBucketSPtr& from_client); - bool DelClientSession(ClientID client_id); - void UpdateStatistic(const fptn::statistic::MetricsSPtr& prometheus); + ConnectionMultiplexerSPtr AddConnection( + const fptn::nat::ConnectParams& mod_params); + + bool DelConnectionByClientId(ClientID client_id) noexcept; + + void UpdateStatistic(const fptn::statistic::MetricsSPtr& prometheus) noexcept; public: - fptn::client::SessionSPtr GetSessionByFakeIPv4( - const fptn::common::network::IPv4Address& ip) noexcept; - fptn::client::SessionSPtr GetSessionByFakeIPv6( - const fptn::common::network::IPv6Address& ip) noexcept; - fptn::client::SessionSPtr GetSessionByClientId(ClientID clientId) noexcept; + fptn::nat::ConnectionMultiplexerSPtr GetConnectionMultiplexerByFakeIPv4( + const IPv4Address& ip) noexcept; + fptn::nat::ConnectionMultiplexerSPtr GetConnectionMultiplexerByFakeIPv6( + const IPv6Address& ip) noexcept; + fptn::nat::ConnectionMultiplexerSPtr GetConnectionMultiplexerByClientId( + ClientID clientId) noexcept; - std::size_t GetNumberActiveSessionByUsername(const std::string& username); + std::size_t GetNumberActiveSessionByUsername(const std::string& username) noexcept; protected: - fptn::common::network::IPv4Address GetUniqueIPv4Address(); - fptn::common::network::IPv6Address GetUniqueIPv6Address(); + std::optional GetUniqueIPv4Address() noexcept; + std::optional GetUniqueIPv6Address() noexcept; private: mutable std::mutex mutex_; @@ -74,13 +78,14 @@ class Table final { fptn::common::network::IPv4AddressGenerator ipv4_generator_; fptn::common::network::IPv6AddressGenerator ipv6_generator_; - - std::unordered_map - ipv4_to_sessions_; // ipv4 - std::unordered_map - ipv6_to_sessions_; // ipv6 - std::unordered_map - client_id_to_sessions_; + // DO NOT REMOVE + // std::unordered_map + // session_id_to_connections_; // session_id -> multiplexor + // std::unordered_map + // ipv4_to_mplxs_; // ipv4 + // std::unordered_map + // ipv6_to_mplxs_; // ipv6 + NatMultiplexers multiplexers_; }; typedef std::shared_ptr TableSPtr; diff --git a/src/fptn-server/traffic_shaper/leaky_bucket.cpp b/src/fptn-server/nat/traffic_shaper/leaky_bucket.cpp similarity index 68% rename from src/fptn-server/traffic_shaper/leaky_bucket.cpp rename to src/fptn-server/nat/traffic_shaper/leaky_bucket.cpp index 5b75e155..dc9b475b 100644 --- a/src/fptn-server/traffic_shaper/leaky_bucket.cpp +++ b/src/fptn-server/nat/traffic_shaper/leaky_bucket.cpp @@ -1,12 +1,14 @@ /*============================================================================= -Copyright (c) 2024-2025 Stas Skokov +Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ -#include "traffic_shaper/leaky_bucket.h" +#include "nat/traffic_shaper/leaky_bucket.h" -using fptn::traffic_shaper::LeakyBucket; +#include + +namespace fptn::nat::traffic_shaper { LeakyBucket::LeakyBucket(std::size_t max_bites_per_second) : current_amount_(0), @@ -18,13 +20,12 @@ std::size_t LeakyBucket::FullDataAmount() const noexcept { return full_data_amount_; } -bool LeakyBucket::CheckSpeedLimit(std::size_t packet_size) noexcept { +bool LeakyBucket::CanProcessPacket(std::size_t packet_size) noexcept { const std::unique_lock lock(mutex_); // mutex - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast( - now - last_leak_time_) - .count(); + const auto now = std::chrono::steady_clock::now(); + const auto elapsed = std::chrono::duration_cast( + now - last_leak_time_).count(); if (elapsed < 1000) { if (current_amount_ + packet_size < max_bytes_per_second_) { current_amount_ += packet_size; @@ -37,3 +38,4 @@ bool LeakyBucket::CheckSpeedLimit(std::size_t packet_size) noexcept { current_amount_ = packet_size; return true; } +} // namespace fptn::nat::traffic_shaper diff --git a/src/fptn-server/traffic_shaper/leaky_bucket.h b/src/fptn-server/nat/traffic_shaper/leaky_bucket.h similarity index 79% rename from src/fptn-server/traffic_shaper/leaky_bucket.h rename to src/fptn-server/nat/traffic_shaper/leaky_bucket.h index b81ce069..0cb62da8 100644 --- a/src/fptn-server/traffic_shaper/leaky_bucket.h +++ b/src/fptn-server/nat/traffic_shaper/leaky_bucket.h @@ -1,5 +1,5 @@ /*============================================================================= -Copyright (c) 2024-2025 Stas Skokov +Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ @@ -7,16 +7,17 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #pragma once #include +#include #include #include #include "common/network/ip_packet.h" -namespace fptn::traffic_shaper { +namespace fptn::nat::traffic_shaper { class LeakyBucket final { public: explicit LeakyBucket(std::size_t max_bites_per_second); - bool CheckSpeedLimit(std::size_t packet_size) noexcept; + bool CanProcessPacket(std::size_t packet_size) noexcept; std::size_t FullDataAmount() const noexcept; private: @@ -29,4 +30,4 @@ class LeakyBucket final { }; using LeakyBucketSPtr = std::shared_ptr; -} // namespace fptn::traffic_shaper +} // namespace fptn::nat::traffic_shaper diff --git a/src/fptn-server/user/user_manager.cpp b/src/fptn-server/user/user_manager.cpp index 2ee760dc..a1980f16 100644 --- a/src/fptn-server/user/user_manager.cpp +++ b/src/fptn-server/user/user_manager.cpp @@ -26,7 +26,7 @@ UserManager::UserManager(const std::string& userfile, // remote user list http_api_client_ = std::make_unique(remote_server_ip_, - remote_server_port, protocol::https::CensorshipStrategy::kSni); + remote_server_port, protocol::https::HttpsInitConnectionStrategy::kSni); } else { // local user list common_manager_ = diff --git a/src/fptn-server/vpn/manager.cpp b/src/fptn-server/vpn/manager.cpp index 271f130a..bbc27cd3 100644 --- a/src/fptn-server/vpn/manager.cpp +++ b/src/fptn-server/vpn/manager.cpp @@ -60,11 +60,13 @@ bool Manager::Start() { network_interface_->Start(); for (size_t i = 0; i < thread_pool_size_; ++i) { - read_to_client_threads_.emplace_back(&Manager::RunToClient, this); + read_to_client_threads_.emplace_back( + &Manager::RunProcessingToWebsocket, this); } for (size_t i = 0; i < thread_pool_size_; ++i) { - read_from_client_threads_.emplace_back(&Manager::RunFromClient, this); + read_from_client_threads_.emplace_back( + &Manager::RunProcessingFromWebsocket, this); } collect_statistics_ = std::thread(&Manager::RunCollectStatistics, this); @@ -72,7 +74,7 @@ bool Manager::Start() { return collect_statistic_status; } -void Manager::RunToClient() const noexcept { +void Manager::RunProcessingToWebsocket() const noexcept { constexpr std::chrono::milliseconds kTimeout{100}; while (running_) { @@ -80,34 +82,29 @@ void Manager::RunToClient() const noexcept { if (!packet || !running_) { continue; } - if (!packet->IsIPv4() && !packet->IsIPv6()) { - continue; - } - // get session using "fake" client address - fptn::client::SessionSPtr session = nullptr; + + fptn::nat::ConnectionMultiplexerSPtr connection_multiplexer = nullptr; if (packet->IsIPv4()) { - session = nat_->GetSessionByFakeIPv4(fptn::common::network::IPv4Address( - packet->IPv4Layer()->getDstIPv4Address())); + connection_multiplexer = + nat_->GetConnectionMultiplexerByFakeIPv4(packet->DstIPv4Address()); } else if (packet->IsIPv6()) { - session = nat_->GetSessionByFakeIPv6(fptn::common::network::IPv6Address( - packet->IPv6Layer()->getDstIPv6Address())); + connection_multiplexer = + nat_->GetConnectionMultiplexerByFakeIPv6(packet->DstIPv6Address()); } - if (!session || !running_) { - continue; - } - // check shaper - auto& shaper = session->TrafficShaperToClient(); - if (shaper && !shaper->CheckSpeedLimit(packet->Size())) { + + if (!connection_multiplexer || !running_) { continue; } - // send - if (running_) { - web_server_->Send(session->ChangeIPAddressToClientIP(std::move(packet))); + + packet = + connection_multiplexer->PacketPreparingToWebsocket(std::move(packet)); + if (packet && running_) { + web_server_->Send(std::move(packet)); } } } -void Manager::RunFromClient() const noexcept { +void Manager::RunProcessingFromWebsocket() const noexcept { constexpr std::chrono::milliseconds kTimeout{100}; while (running_) { @@ -118,40 +115,32 @@ void Manager::RunFromClient() const noexcept { if (!packet->IsIPv4() && !packet->IsIPv6()) { continue; } - // get session - const auto session = nat_->GetSessionByClientId(packet->ClientId()); - if (!session || !running_) { - continue; - } - // check shaper - auto shaper = session->TrafficShaperFromClient(); - if (shaper && !shaper->CheckSpeedLimit(packet->Size())) { - continue; - } - // filter - packet = filter_->Apply(std::move(packet)); - if (!packet || !running_) { + + const auto connection_multiplexer = + nat_->GetConnectionMultiplexerByClientId(packet->ClientId()); + if (!connection_multiplexer || !running_) { continue; } - // send - if (running_) { - network_interface_->Send( - session->ChangeIPAddressToFakeIP(std::move(packet))); + + packet = + connection_multiplexer->PacketPreparingFromWebsocket(std::move(packet)); + if (packet && running_) { + network_interface_->Send(std::move(packet)); } } } void Manager::RunCollectStatistics() noexcept { - const std::chrono::milliseconds timeout{300}; - const std::chrono::seconds collect_interval{2}; + constexpr std::chrono::milliseconds kTimeout{300}; + constexpr std::chrono::seconds kCollectInterval{10}; std::chrono::steady_clock::time_point last_collection_time; while (running_) { auto now = std::chrono::steady_clock::now(); - if (now - last_collection_time > collect_interval) { + if (now - last_collection_time > kCollectInterval) { nat_->UpdateStatistic(prometheus_); last_collection_time = now; } - std::this_thread::sleep_for(timeout); + std::this_thread::sleep_for(kTimeout); } } diff --git a/src/fptn-server/vpn/manager.h b/src/fptn-server/vpn/manager.h index 9c80a048..0f8756f4 100644 --- a/src/fptn-server/vpn/manager.h +++ b/src/fptn-server/vpn/manager.h @@ -30,8 +30,8 @@ class Manager final { bool Start(); protected: - void RunToClient() const noexcept; - void RunFromClient() const noexcept; + void RunProcessingToWebsocket() const noexcept; + void RunProcessingFromWebsocket() const noexcept; void RunCollectStatistics() noexcept; private: diff --git a/src/fptn-server/web/session/session.cpp b/src/fptn-server/web/client_endpoint/client_endpoint.cpp similarity index 85% rename from src/fptn-server/web/session/session.cpp rename to src/fptn-server/web/client_endpoint/client_endpoint.cpp index e225f903..1dddfa6e 100644 --- a/src/fptn-server/web/session/session.cpp +++ b/src/fptn-server/web/client_endpoint/client_endpoint.cpp @@ -1,10 +1,10 @@ /*============================================================================= -Copyright (c) 2024-2025 Stas Skokov +Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ -#include "web/session/session.h" +#include "web/client_endpoint/client_endpoint.h" #include #include @@ -14,7 +14,6 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include -#include #include #include #include @@ -34,6 +33,8 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "fptn-protocol-lib/https/utils/tls/tls.h" #include "fptn-protocol-lib/protobuf/protocol.h" #include "fptn-protocol-lib/time/time_provider.h" +#include "fptn-server/nat/connect_params.h" +#include "nat/connect_params.h" namespace { std::atomic client_id_counter = 0; @@ -71,11 +72,45 @@ void SetSocketTimeouts(boost::asio::ip::tcp::socket& socket, int timeout_sec) { reinterpret_cast(&tv), sizeof(tv)); } +std::uint64_t ParseRequestUint( + const boost::beast::http::request& request, + const std::string& param_name, + const std::uint64_t default_value = UINT64_MAX) { + if (request.contains(param_name)) { + const std::string value_str = + fptn::common::utils::FilterDigitsOnly(request[param_name]); + if (value_str.empty()) { + return default_value; + } + try { + std::size_t pos = 0; + const std::uint64_t value = std::stoull(value_str, &pos, 10); + if (pos != value_str.size()) { + return default_value; + } + return value; + } catch (const std::exception&) { + return default_value; + } + } + return default_value; +} + +std::string ParseRequestStr( + const boost::beast::http::request& request, + const std::string& param_name, + const std::string& default_value) { + if (request.contains(param_name)) { + return request[param_name]; + } + return default_value; +} + } // namespace namespace fptn::web { -Session::Session(std::uint16_t port, +ClientEndpoint::ClientEndpoint(std::uint16_t port, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, @@ -125,25 +160,26 @@ Session::Session(std::uint16_t port, boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(15)); init_completed_ = true; } catch (const boost::system::system_error& err) { - SPDLOG_ERROR("Session::init failed (client_id={}): {} [{}]", client_id_, - err.what(), err.code().message()); + SPDLOG_ERROR("ClientEndpoint::init failed (client_id={}): {} [{}]", + client_id_, err.what(), err.code().message()); } catch (const std::exception& e) { - SPDLOG_ERROR("Session::init unexpected exception (client_id={}): {}", + SPDLOG_ERROR("ClientEndpoint::init unexpected exception (client_id={}): {}", client_id_, e.what()); } catch (...) { SPDLOG_ERROR( - "Session::init unknown fatal error (client_id={})", client_id_); + "ClientEndpoint::init unknown fatal error (client_id={})", client_id_); } } -Session::~Session() { Close(); } +ClientEndpoint::~ClientEndpoint() { Close(); } -boost::asio::awaitable Session::Run() { +boost::asio::awaitable ClientEndpoint::Run() { boost::system::error_code ec; running_ = true; if (!init_completed_) { - SPDLOG_ERROR("Session not initialized. Closing connection (client_id={})", + SPDLOG_ERROR( + "ClientEndpoint not initialized. Closing connection (client_id={})", client_id_); Close(); co_return; @@ -251,7 +287,8 @@ boost::asio::awaitable Session::Run() { co_return; } -boost::asio::awaitable Session::DetectProbing() { +boost::asio::awaitable +ClientEndpoint::DetectProbing() { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); // Peek data without consuming it from the socket buffer!!! @@ -356,7 +393,7 @@ boost::asio::awaitable Session::DetectProbing() { } } - // Get Session ID + // Get ClientEndpoint ID constexpr std::size_t kSessionLen = 32; std::size_t session_len = std::min( static_cast(kSessionLen), hello->getSessionIDLength()); @@ -370,7 +407,7 @@ boost::asio::awaitable Session::DetectProbing() { std::uint8_t session_id[kSessionLen] = {0}; std::memcpy(session_id, hello->getSessionID(), session_len); - // Check Session ID + // Check ClientEndpoint ID const bool is_fptn_session_id = protocol::https::utils::IsFptnClientSessionID(session_id, session_len); const bool is_decoy_session_id = @@ -378,7 +415,7 @@ boost::asio::awaitable Session::DetectProbing() { session_id, session_len); if (!is_fptn_session_id && !is_decoy_session_id) { SPDLOG_ERROR( - "Session ID does not match FPTN client format (client_id={})", + "ClientEndpoint ID does not match FPTN client format (client_id={})", client_id_); co_return ProbingResult{ .is_probing = true, .sni = sni, .should_close = false}; @@ -400,7 +437,7 @@ boost::asio::awaitable Session::DetectProbing() { } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) -boost::asio::awaitable Session::IsSniSelfProxyAttempt( +boost::asio::awaitable ClientEndpoint::IsSniSelfProxyAttempt( const std::string& sni) const { // First check if SNI is already an IP address if (fptn::common::network::IsIpAddress(sni)) { @@ -445,7 +482,8 @@ boost::asio::awaitable Session::IsSniSelfProxyAttempt( co_return false; } -boost::asio::awaitable Session::IsRealityHandshake() { +boost::asio::awaitable +ClientEndpoint::IsRealityHandshake() { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); @@ -529,7 +567,7 @@ boost::asio::awaitable Session::IsRealityHandshake() { .is_reality_mode = true, .sni = "", .should_close = true}; } -boost::asio::awaitable Session::HandleRealityMode( +boost::asio::awaitable ClientEndpoint::HandleRealityMode( const std::string& sni) { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); @@ -567,7 +605,7 @@ boost::asio::awaitable Session::HandleRealityMode( co_return false; } -boost::asio::awaitable Session::HandleProxy( +boost::asio::awaitable ClientEndpoint::HandleProxy( const std::string& sni, int port) { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); boost::asio::ip::tcp::socket target_socket( @@ -667,7 +705,7 @@ boost::asio::awaitable Session::HandleProxy( co_return status; } -boost::asio::awaitable Session::RunReader() { +boost::asio::awaitable ClientEndpoint::RunReader() { boost::system::error_code ec; boost::beast::flat_buffer buffer; auto token = boost::asio::redirect_error(boost::asio::use_awaitable, ec); @@ -700,7 +738,7 @@ boost::asio::awaitable Session::RunReader() { co_return; } -boost::asio::awaitable Session::RunSender() { +boost::asio::awaitable ClientEndpoint::RunSender() { try { while (running_ && ws_.is_open()) { auto [ec, packet] = co_await write_channel_.async_receive( @@ -730,7 +768,7 @@ boost::asio::awaitable Session::RunSender() { co_return; } -boost::asio::awaitable Session::ProcessRequest() { +boost::asio::awaitable ClientEndpoint::ProcessRequest() { bool status = false; try { @@ -751,13 +789,13 @@ boost::asio::awaitable Session::ProcessRequest() { status = co_await HandleHttp(request); } } catch (const boost::system::system_error& err) { - SPDLOG_ERROR("Session::handshake failed (client_id={}): {} [{}]", + SPDLOG_ERROR("ClientEndpoint::handshake failed (client_id={}): {} [{}]", client_id_, err.what(), err.code().message()); } co_return status; } -boost::asio::awaitable Session::HandleHttp( +boost::asio::awaitable ClientEndpoint::HandleHttp( const boost::beast::http::request& request) { const std::string url = request.target(); @@ -810,59 +848,69 @@ boost::asio::awaitable Session::HandleHttp( co_await boost::beast::http::async_write( ws_.next_layer(), *res_ptr, boost::asio::use_awaitable); } catch (const boost::beast::system_error& e) { - SPDLOG_ERROR("Session::HandleHttp write error (client_id={}): {}", + SPDLOG_ERROR("ClientEndpoint::HandleHttp write error (client_id={}): {}", client_id_, e.what()); } catch (...) { SPDLOG_ERROR( - "Session::HandleHttp write unknown error (client_id={})", client_id_); + "ClientEndpoint::HandleHttp write unknown error (client_id={})", + client_id_); } co_return false; } -boost::asio::awaitable Session::HandleWebSocket( +boost::asio::awaitable ClientEndpoint::HandleWebSocket( const boost::beast::http::request& request) { - boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::hours(12)); + boost::system::error_code ec; + const std::string client_ip_str = boost::beast::get_lowest_layer(ws_) + .socket() + .remote_endpoint(ec) + .address() + .to_string(); + if (ec) { + SPDLOG_ERROR("Failed to get remote endpoint: {}", ec.message()); + co_return false; + } + const std::string client_vpn_ipv6_str = request.contains("ClientIPv6") + ? request["ClientIPv6"] + : FPTN_CLIENT_DEFAULT_ADDRESS_IP6; if (request.contains("Authorization") && request.contains("ClientIP")) { - std::string token = request["Authorization"]; - boost::replace_first(token, "Bearer ", ""); + try { + fptn::nat::ConnectParams params = {}; + params.client_id = client_id_; - const std::string client_vpn_ipv4_str = request["ClientIP"]; + params.request.url = request.target(); + params.request.session_id = ParseRequestStr( + request, "SessionID", common::utils::GenerateRandomString(32)); + params.request.connection_weight = + ParseRequestUint(request, "ConnectionWeight", 1); + params.request.jwt_auth_token = + common::utils::RemoveSubstring(request["Authorization"], {"Bearer "}); - boost::system::error_code ec; - const std::string client_ip_str = boost::beast::get_lowest_layer(ws_) - .socket() - .remote_endpoint(ec) - .address() - .to_string(); - if (ec) { - SPDLOG_ERROR("Failed to get remote endpoint: {}", ec.message()); - co_return false; - } + params.request.client_ipv4 = client_ip_str; + params.request.client_tun_vpn_ipv4 = request["ClientIP"]; + params.request.client_tun_vpn_ipv6 = client_vpn_ipv6_str; + + params.timings.SetExpireAfter( + ParseRequestUint(request, "ExpireAfterTimestamp", 0)); + params.timings.SetExpireAfter( + ParseRequestUint(request, "SilenceModeUntilTimestamp", 0)); + + const bool status = ws_open_callback_(params, shared_from_this()); + ws_session_was_opened_ = status; - try { - const common::network::IPv4Address client_ip(client_ip_str); - const common::network::IPv4Address client_vpn_ipv4(client_vpn_ipv4_str); - - const std::string client_vpn_ipv6_str = - (request.contains("ClientIPv6") ? request["ClientIPv6"] - : FPTN_CLIENT_DEFAULT_ADDRESS_IP6); - const common::network::IPv6Address client_vpn_ipv6(client_vpn_ipv6_str); - - const bool status = - ws_open_callback_(client_id_, client_ip, client_vpn_ipv4, - client_vpn_ipv6, shared_from_this(), request.target(), token); - ws_session_was_opened_ = true; co_return status; } catch (const std::exception& ex) { SPDLOG_ERROR( - "Session::Open (client_id={}): Exception caught while creating IP " + "ClientEndpoint::Open (client_id={}): Exception caught while " + "creating IP " "addresses or running callback: {}", client_id_, ex.what()); } catch (...) { SPDLOG_ERROR( - "Session::Open (client_id={}): Unknown fatal error caught while " + "ClientEndpoint::Open (client_id={}): Unknown fatal error caught " + "while " "creating IP addresses or running callback", client_id_); } @@ -870,7 +918,7 @@ boost::asio::awaitable Session::HandleWebSocket( co_return false; } -void Session::Close() { +void ClientEndpoint::Close() { if (!running_) { return; } @@ -893,27 +941,8 @@ void Session::Close() { "Failed to cancel session or close write_channel: {}", err.what()); } catch (...) { SPDLOG_WARN( - "Session::Close unknown fatal error (client_id={})", client_id_); - } - - // Set socket linger option for fast close - /* - try { - auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); - if (tcp_socket.is_open()) { - tcp_socket.set_option(boost::asio::socket_base::linger(true, 0)); - } - } catch (const std::exception& err) { - SPDLOG_WARN( - "Session::Close exception setting linger option (client_id={}): {}", - client_id_, err.what()); - } catch (...) { - SPDLOG_WARN( - "Session::Close unknown error setting linger option (client_id={})", - client_id_); + "ClientEndpoint::Close unknown fatal error (client_id={})", client_id_); } - */ - // Close TCP socket first try { auto& tcp_layer = boost::beast::get_lowest_layer(ws_); @@ -926,11 +955,11 @@ void Session::Close() { tcp_layer.socket().close(ec); } } catch (const std::exception& err) { - SPDLOG_WARN("Session::Close TCP socket error (client_id={}): {}", + SPDLOG_WARN("ClientEndpoint::Close TCP socket error (client_id={}): {}", client_id_, err.what()); } catch (...) { - SPDLOG_WARN( - "Session::Close TCP socket unknown error (client_id={})", client_id_); + SPDLOG_WARN("ClientEndpoint::Close TCP socket unknown error (client_id={})", + client_id_); } // Close WebSocket @@ -940,11 +969,11 @@ void Session::Close() { ws_.close(boost::beast::websocket::close_code::normal, ec); } } catch (const std::exception& err) { - SPDLOG_WARN("Session::Close WebSocket error (client_id={}): {}", client_id_, - err.what()); + SPDLOG_WARN("ClientEndpoint::Close WebSocket error (client_id={}): {}", + client_id_, err.what()); } catch (...) { - SPDLOG_WARN( - "Session::Close WebSocket unknown error (client_id={})", client_id_); + SPDLOG_WARN("ClientEndpoint::Close WebSocket unknown error (client_id={})", + client_id_); } // Close SSL @@ -954,11 +983,13 @@ void Session::Close() { ::SSL_set_quiet_shutdown(ssl_layer.native_handle(), 1); } } catch (const std::exception& err) { - SPDLOG_ERROR("Session::Close SSL shutdown exception (client_id={}): {}", + SPDLOG_ERROR( + "ClientEndpoint::Close SSL shutdown exception (client_id={}): {}", client_id_, err.what()); } catch (...) { SPDLOG_ERROR( - "Session::Close SSL shutdown unknown error (client_id={})", client_id_); + "ClientEndpoint::Close SSL shutdown unknown error (client_id={})", + client_id_); } if (ws_close_callback_ && ws_session_was_opened_) { @@ -975,7 +1006,8 @@ void Session::Close() { } } -boost::asio::awaitable Session::Send(common::network::IPPacketPtr pkt) { +boost::asio::awaitable ClientEndpoint::Send( + common::network::IPPacketPtr pkt) { auto self = shared_from_this(); boost::asio::post(strand_, [self, pkt = std::move(pkt)]() mutable { if (self->running_ && self->write_channel_.is_open()) { @@ -983,15 +1015,15 @@ boost::asio::awaitable Session::Send(common::network::IPPacketPtr pkt) { boost::system::error_code(), std::move(pkt)); if (!status && !self->full_queue_) { self->full_queue_ = true; - SPDLOG_WARN( - "Session::send queue is full (client_id={})", self->client_id_); + SPDLOG_WARN("ClientEndpoint::send queue is full (client_id={})", + self->client_id_); } } }); co_return true; } -boost::asio::awaitable Session::DetectObfuscator() { +boost::asio::awaitable ClientEndpoint::DetectObfuscator() { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); diff --git a/src/fptn-server/web/session/session.h b/src/fptn-server/web/client_endpoint/client_endpoint.h similarity index 94% rename from src/fptn-server/web/session/session.h rename to src/fptn-server/web/client_endpoint/client_endpoint.h index 6c6bf2fa..1da0618b 100644 --- a/src/fptn-server/web/session/session.h +++ b/src/fptn-server/web/client_endpoint/client_endpoint.h @@ -26,16 +26,16 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "common/jwt_token/token_manager.h" #include "fptn-protocol-lib/https/obfuscator/tcp_stream/tcp_stream.h" -#include "web/api/handle.h" +#include "web/handle/handle.h" #include "web/handshake/handshake_cache_manager.h" namespace fptn::web { using IObfuscator = std::optional; -class Session : public std::enable_shared_from_this { +class ClientEndpoint : public std::enable_shared_from_this { public: - explicit Session(std::uint16_t port, + explicit ClientEndpoint(std::uint16_t port, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, @@ -47,7 +47,7 @@ class Session : public std::enable_shared_from_this { WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback); - virtual ~Session(); + virtual ~ClientEndpoint(); void Close(); // async @@ -135,5 +135,5 @@ class Session : public std::enable_shared_from_this { boost::asio::cancellation_signal cancel_signal_; }; -using SessionSPtr = std::shared_ptr; +using ClientEndpointSPtr = std::shared_ptr; } // namespace fptn::web diff --git a/src/fptn-server/web/api/handle.h b/src/fptn-server/web/handle/handle.h similarity index 69% rename from src/fptn-server/web/api/handle.h rename to src/fptn-server/web/handle/handle.h index 27ec824c..ecffb612 100644 --- a/src/fptn-server/web/api/handle.h +++ b/src/fptn-server/web/handle/handle.h @@ -10,15 +10,13 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include #include -#include -#include #include -#include #include "common/client_id.h" -#include "common/network/ip_address.h" #include "common/network/ip_packet.h" +#include "nat/connect_params.h" + namespace fptn::web { namespace http { using request = boost::beast::http::request; @@ -53,20 +51,14 @@ inline ApiHandle GetApiHandle(const ApiHandleMap& m, return nullptr; } -class Session; +class ClientEndpoint; using WebSocketOpenConnectionCallback = std::function& session, - const std::string& url, - const std::string& access_token)>; + fptn::nat::ConnectParams, const std::shared_ptr& session)>; -using WebSocketNewIPPacketCallback = std::function; +using WebSocketNewIPPacketCallback = + std::function; -using WebSocketCloseConnectionCallback = std::function; +using WebSocketCloseConnectionCallback = + std::function; } // namespace fptn::web diff --git a/src/fptn-server/web/listener/listener.cpp b/src/fptn-server/web/listener/listener.cpp index bef097a3..c410b1cf 100644 --- a/src/fptn-server/web/listener/listener.cpp +++ b/src/fptn-server/web/listener/listener.cpp @@ -86,7 +86,7 @@ boost::asio::awaitable Listener::Run() { co_await acceptor_.async_accept( socket, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (!ec) { - auto session = std::make_shared(port_, + auto session = std::make_shared(port_, // probing settings enable_detect_probing_, default_proxy_domain_, allowed_sni_list_, server_external_ips_, std::move(socket), ctx_, diff --git a/src/fptn-server/web/listener/listener.h b/src/fptn-server/web/listener/listener.h index 97a8e32b..ac8862e6 100644 --- a/src/fptn-server/web/listener/listener.h +++ b/src/fptn-server/web/listener/listener.h @@ -18,9 +18,9 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "common/jwt_token/token_manager.h" -#include "web/api/handle.h" +#include "web/client_endpoint/client_endpoint.h" +#include "web/handle/handle.h" #include "web/handshake/handshake_cache_manager.h" -#include "web/session/session.h" namespace fptn::web { diff --git a/src/fptn-server/web/server.cpp b/src/fptn-server/web/server.cpp index da06424f..b0b82d27 100644 --- a/src/fptn-server/web/server.cpp +++ b/src/fptn-server/web/server.cpp @@ -67,21 +67,24 @@ Server::Server(std::uint16_t port, // ioc ioc_, token_manager, handshake_cache_manager_, server_external_ips_, // NOLINTNEXTLINE(modernize-avoid-bind) - std::bind( - &Server::HandleWsOpenConnection, this, _1, _2, _3, _4, _5, _6, _7), + std::bind(&Server::HandleWsOpenConnection, this, _1, _2), // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&Server::HandleWsNewIPPacket, this, _1), // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&Server::HandleWsCloseConnection, this, _1)); + listener_->AddApiHandle( // NOLINTNEXTLINE(modernize-avoid-bind) kUrlDns_, "GET", std::bind(&Server::HandleApiDns, this, _1, _2)); + listener_->AddApiHandle( // NOLINTNEXTLINE(modernize-avoid-bind) kUrlLogin_, "POST", std::bind(&Server::HandleApiLogin, this, _1, _2)); + listener_->AddApiHandle(kUrlTestFileBin_, "GET", // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&Server::HandleApiTestFile, this, _1, _2)); + if (!prometheus_access_key.empty()) { // Construct the URL for accessing Prometheus statistics by appending the // access key @@ -126,13 +129,13 @@ boost::asio::awaitable Server::RunSender() { while (running_) { auto optpacket = co_await to_client_->WaitForPacketAsync(kTimeout); if (optpacket && running_) { - SessionSPtr session; + ClientEndpointSPtr session; { const std::unique_lock lock(mutex_); // mutex - auto it = sessions_.find(optpacket->get()->ClientId()); - if (it != sessions_.end()) { + auto it = client_endpoints_.find(optpacket->get()->ClientId()); + if (it != client_endpoints_.end()) { session = it->second; } } @@ -154,12 +157,12 @@ bool Server::Stop() { listener_->Stop(); - for (auto& session : sessions_) { + for (auto& session : client_endpoints_) { if (session.second) { session.second->Close(); } } - sessions_.clear(); + client_endpoints_.clear(); if (!ioc_.stopped()) { ioc_.stop(); } @@ -249,67 +252,66 @@ int Server::HandleApiTestFile(const http::request& req, http::response& resp) { return 200; } -bool Server::HandleWsOpenConnection(fptn::ClientID client_id, - const fptn::common::network::IPv4Address& client_ip, - const fptn::common::network::IPv4Address& client_vpn_ipv4, - const fptn::common::network::IPv6Address& client_vpn_ipv6, - const SessionSPtr& session, - const std::string& url, - const std::string& access_token) { +bool Server::HandleWsOpenConnection( + const fptn::nat::ConnectParams& params, const ClientEndpointSPtr& session) { if (!running_) { SPDLOG_ERROR("Server is not running"); return false; } - if (url != kUrlWebSocket_) { - SPDLOG_ERROR("Wrong URL \"{}\"", url); + if (params.request.url != kUrlWebSocket_) { + SPDLOG_ERROR("Wrong URL \"{}\"", params.request.url); return false; } - if (!client_vpn_ipv4.IsEmpty() && !client_vpn_ipv6.IsEmpty()) { - std::string username; - std::size_t bandwidth_bites_seconds = 0; - if (token_manager_->Validate( - access_token, username, bandwidth_bites_seconds)) { - const std::unique_lock lock(mutex_); // mutex - - const auto active_sessions = - nat_table_->GetNumberActiveSessionByUsername(username); - - if (active_sessions > max_active_sessions_per_user_) { - SPDLOG_WARN( - "Session limit exceeded for user '{}': {} active (limit: {})", - username, active_sessions, max_active_sessions_per_user_); - return false; - } - if (sessions_.contains(client_id)) { - SPDLOG_WARN("Client with same ID already exists!"); - } else { - const auto shaper_to_client = - std::make_shared( - bandwidth_bites_seconds); - const auto shaper_from_client = - std::make_shared( - bandwidth_bites_seconds); - const auto nat_session = nat_table_->CreateClientSession(client_id, - username, client_vpn_ipv4, client_vpn_ipv6, shaper_to_client, - shaper_from_client); - SPDLOG_INFO( - "NEW SESSION! Username={} client_id={} Bandwidth={} ClientIP={} " - "VirtualIPv4={} VirtualIPv6={}", - username, client_id, bandwidth_bites_seconds, client_ip.ToString(), - nat_session->FakeClientIPv4().ToString(), - nat_session->FakeClientIPv6().ToString()); - if (running_) { - sessions_.insert({client_id, session}); - return true; - } - return false; - } - } else { - SPDLOG_WARN("WRONG TOKEN: {}", username); - } - } else { - SPDLOG_WARN("Wrong ClientIP or ClientIPv6"); + if (client_endpoints_.contains(params.client_id)) { + SPDLOG_WARN("Client with same ID already exists!"); + return false; + } + + if (!params.Validate()) { + SPDLOG_WARN("Wrong request: {}", params.client_id); + } + + std::string username; + std::size_t bandwidth_bites_seconds = 0; + // get username and bandwidth from a jwt-token + if (!token_manager_->Validate( + params.request.jwt_auth_token, username, bandwidth_bites_seconds)) { + SPDLOG_WARN("WRONG TOKEN: {}", username); + return false; + } + + fptn::nat::ConnectParams mod_params = params; + mod_params.user.username = username; + mod_params.user.bandwidth_bites_seconds += bandwidth_bites_seconds; + + const std::unique_lock lock(mutex_); // mutex + + const auto active_sessions = + nat_table_->GetNumberActiveSessionByUsername(username); + + if (active_sessions > max_active_sessions_per_user_) { + SPDLOG_WARN("Session limit exceeded for user '{}': {} active (limit: {})", + username, active_sessions, max_active_sessions_per_user_); + return false; + } + + const auto connection_multiplexer = nat_table_->AddConnection(mod_params); + if (!connection_multiplexer) { + return false; + } + + SPDLOG_INFO( + "New session: username={} client_id={} Bandwidth={} ClientIP={} " + "VirtualIPv4={} VirtualIPv6={}", + mod_params.user.username, mod_params.client_id, + mod_params.user.bandwidth_bites_seconds, + mod_params.request.client_ipv4.ToString(), + connection_multiplexer->FakeClientIPv4().ToString(), + connection_multiplexer->FakeClientIPv6().ToString()); + if (running_) { + client_endpoints_.insert({mod_params.client_id, session}); + return true; } return false; } @@ -320,14 +322,14 @@ void Server::HandleWsNewIPPacket( } void Server::HandleWsCloseConnection(fptn::ClientID client_id) noexcept { - SessionSPtr session; + ClientEndpointSPtr session; if (running_) { const std::unique_lock lock(mutex_); // mutex - auto it = sessions_.find(client_id); - if (it != sessions_.end()) { + auto it = client_endpoints_.find(client_id); + if (it != client_endpoints_.end()) { session = std::move(it->second); - sessions_.erase(it); + client_endpoints_.erase(it); } } if (session != nullptr) { @@ -337,5 +339,5 @@ void Server::HandleWsCloseConnection(fptn::ClientID client_id) noexcept { SPDLOG_WARN( "Attempted to close non-existent session (client_id={})", client_id); } - nat_table_->DelClientSession(client_id); + nat_table_->DelConnectionByClientId(client_id); } diff --git a/src/fptn-server/web/server.h b/src/fptn-server/web/server.h index 35665dd6..dfbfa05e 100644 --- a/src/fptn-server/web/server.h +++ b/src/fptn-server/web/server.h @@ -24,6 +24,8 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include "handshake/handshake_cache_manager.h" #include "listener/listener.h" +#include "nat/connect_params.h" +#include "nat/statistic/metrics.h" #include "nat/table.h" #include "user/user_manager.h" @@ -64,13 +66,8 @@ class Server final { protected: // websocket - bool HandleWsOpenConnection(fptn::ClientID client_id, - const fptn::common::network::IPv4Address& client_ip, - const fptn::common::network::IPv4Address& client_vpn_ipv4, - const fptn::common::network::IPv6Address& client_vpn_ipv6, - const SessionSPtr& session, - const std::string& url, - const std::string& access_token); + bool HandleWsOpenConnection( + const fptn::nat::ConnectParams& params, const ClientEndpointSPtr& session); void HandleWsNewIPPacket(fptn::common::network::IPPacketPtr packet) noexcept; void HandleWsCloseConnection(fptn::ClientID client_id) noexcept; @@ -109,7 +106,7 @@ class Server final { HandshakeCacheManagerSPtr handshake_cache_manager_; std::vector ioc_threads_; - std::unordered_map sessions_; + std::unordered_map client_endpoints_; }; using ServerPtr = std::unique_ptr; diff --git a/tests/fptnlib/api_client/ApiClientTest.cpp b/tests/fptnlib/api_client/ApiClientTest.cpp index bdef4d73..2d766f45 100644 --- a/tests/fptnlib/api_client/ApiClientTest.cpp +++ b/tests/fptnlib/api_client/ApiClientTest.cpp @@ -34,7 +34,7 @@ TEST(ApiClientTest, GitHubReleasesConnection) { TEST(ApiClientTest, GitHubHandshakeTest) { fptn::protocol::https::ApiClient client("api.github.com", 443, - "api.github.com", fptn::protocol::https::CensorshipStrategy::kSni); + "api.github.com", fptn::protocol::https::HttpsInitConnectionStrategy::kSni); bool handshake_success = client.TestHandshake(10); EXPECT_TRUE(handshake_success); diff --git a/tests/server/CMakeLists.txt b/tests/server/CMakeLists.txt index 794bd619..7748a1ac 100644 --- a/tests/server/CMakeLists.txt +++ b/tests/server/CMakeLists.txt @@ -32,7 +32,7 @@ set(LIBS include_directories(${FPTN_SERVER_PATH}) # --- MetricTest test --- -add_executable(MetricTest statistic/MetricTest.cpp ${FPTN_SERVER_PATH}/statistic/metrics.h ${FPTN_SERVER_PATH}/statistic/metrics.cpp) +add_executable(MetricTest statistic/MetricTest.cpp ${FPTN_SERVER_PATH}/nat/statistic/metrics.h ${FPTN_SERVER_PATH}/nat/statistic/metrics.cpp) target_link_libraries(MetricTest PRIVATE ${LIBS}) add_test(NAME MetricTest COMMAND MetricTest)