From aba1b48a38df3d63cdb61963f0138ca616280de1 Mon Sep 17 00:00:00 2001 From: Stas Skokov <7090stas@gmail.com> Date: Sun, 18 Jan 2026 13:06:38 +1100 Subject: [PATCH] [bugfix] self-proxy --- deploy/docker/scripts/start-fptn.sh | 17 +++++----- deploy/linux/deb/create-server-deb-package.sh | 9 ++++- docker-compose/.env.demo | 5 +++ docker-compose/docker-compose.yml | 1 + .../https/obfuscator/methods/detector.h | 3 -- .../config/command_line_config.cpp | 21 +++++++++--- src/fptn-server/config/command_line_config.h | 2 ++ src/fptn-server/fptn-server.cpp | 3 +- src/fptn-server/web/listener/listener.cpp | 7 ++-- src/fptn-server/web/listener/listener.h | 3 ++ src/fptn-server/web/server.cpp | 9 ++--- src/fptn-server/web/server.h | 2 ++ src/fptn-server/web/session/session.cpp | 34 ++++++++++++++----- src/fptn-server/web/session/session.h | 2 ++ 14 files changed, 86 insertions(+), 32 deletions(-) diff --git a/deploy/docker/scripts/start-fptn.sh b/deploy/docker/scripts/start-fptn.sh index f1b99939..0cdc8b2f 100644 --- a/deploy/docker/scripts/start-fptn.sh +++ b/deploy/docker/scripts/start-fptn.sh @@ -6,13 +6,14 @@ echo "[FPTN] Using network interface: $OUT_NETWORK_INTERFACE" exec /usr/local/bin/fptn-server \ --server-key=/etc/fptn/server.key \ --server-crt=/etc/fptn/server.crt \ - --out-network-interface=$OUT_NETWORK_INTERFACE \ + --out-network-interface="$OUT_NETWORK_INTERFACE" \ --server-port=443 \ - --enable-detect-probing=$ENABLE_DETECT_PROBING \ + --enable-detect-probing="$ENABLE_DETECT_PROBING" \ --tun-interface-name=fptn0 \ - --disable-bittorrent=$DISABLE_BITTORRENT \ - --prometheus-access-key=$PROMETHEUS_SECRET_ACCESS_KEY \ - --use-remote-server-auth=$USE_REMOTE_SERVER_AUTH \ - --remote-server-auth-host=$REMOTE_SERVER_AUTH_HOST \ - --remote-server-auth-port=$REMOTE_SERVER_AUTH_PORT \ - --max-active-sessions-per-user=$MAX_ACTIVE_SESSIONS_PER_USER + --disable-bittorrent="$DISABLE_BITTORRENT" \ + --prometheus-access-key="$PROMETHEUS_SECRET_ACCESS_KEY" \ + --use-remote-server-auth="$USE_REMOTE_SERVER_AUTH" \ + --remote-server-auth-host="$REMOTE_SERVER_AUTH_HOST" \ + --remote-server-auth-port="$REMOTE_SERVER_AUTH_PORT" \ + --max-active-sessions-per-user="$MAX_ACTIVE_SESSIONS_PER_USER" \ + --server-external-ips="${SERVER_EXTERNAL_IPS}" diff --git a/deploy/linux/deb/create-server-deb-package.sh b/deploy/linux/deb/create-server-deb-package.sh index cb28837d..2cccdbec 100755 --- a/deploy/linux/deb/create-server-deb-package.sh +++ b/deploy/linux/deb/create-server-deb-package.sh @@ -68,6 +68,12 @@ PROMETHEUS_SECRET_ACCESS_KEY= # Maximum number of active sessions allowed per VPN user MAX_ACTIVE_SESSIONS_PER_USER=3 + +# Public IPv4 addresses of this VPN server (comma-separated). +# Used to prevent proxy loops when clients connect. +# Example: 1.2.3.4,5.6.7.8 +SERVER_EXTERNAL_IPS= + EOL # Create systemd service file for server @@ -90,7 +96,8 @@ ExecStart=/usr/bin/$(basename "$SERVER_BIN") \ --use-remote-server-auth=\${USE_REMOTE_SERVER_AUTH} \ --remote-server-auth-host=\${REMOTE_SERVER_AUTH_HOST} \ --remote-server-auth-port=\${REMOTE_SERVER_AUTH_PORT} \ - --max-active-sessions-per-user=\${MAX_ACTIVE_SESSIONS_PER_USER} + --max-active-sessions-per-user=\${MAX_ACTIVE_SESSIONS_PER_USER} \ + --server-external-ips=\${SERVER_EXTERNAL_IPS} Restart=always WorkingDirectory=/etc/fptn RestartSec=5 diff --git a/docker-compose/.env.demo b/docker-compose/.env.demo index 63c256bd..35a39c57 100644 --- a/docker-compose/.env.demo +++ b/docker-compose/.env.demo @@ -1,6 +1,11 @@ # Configuration for fptn server FPTN_PORT=443 +# Comma-separated list of server's public IPv4 addresses. +# IMPORTANT: Set all server public IPs to prevent proxy loops when server receives requests to itself +# Example: SERVER_EXTERNAL_IPS=1.2.3.4,5.6.7.8 +SERVER_EXTERNAL_IPS= + # Enable detection of probing attempts (accepted values: true or false) ENABLE_DETECT_PROBING=true diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index f70c19a8..a78d2a47 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -36,6 +36,7 @@ services: - REMOTE_SERVER_AUTH_HOST=${REMOTE_SERVER_AUTH_HOST} - REMOTE_SERVER_AUTH_PORT=${REMOTE_SERVER_AUTH_PORT} - MAX_ACTIVE_SESSIONS_PER_USER=${MAX_ACTIVE_SESSIONS_PER_USER} + - SERVER_EXTERNAL_IPS=${SERVER_EXTERNAL_IPS} healthcheck: test: ["CMD", "sh", "-c", "pgrep dnsmasq && pgrep fptn-server"] interval: 30s diff --git a/src/fptn-protocol-lib/https/obfuscator/methods/detector.h b/src/fptn-protocol-lib/https/obfuscator/methods/detector.h index 61bfcd41..f3c0a1fa 100644 --- a/src/fptn-protocol-lib/https/obfuscator/methods/detector.h +++ b/src/fptn-protocol-lib/https/obfuscator/methods/detector.h @@ -22,11 +22,8 @@ inline IObfuscatorSPtr DetectObfuscator( const std::uint8_t* data, std::size_t size) { auto tls_obfuscator = std::make_shared(); if (tls_obfuscator->CheckProtocol(data, size)) { - SPDLOG_DEBUG("Detected TLS obfuscator protocol - using TlsObfuscator"); return tls_obfuscator; } - SPDLOG_DEBUG( - "No specific obfuscator detected - using NoneObfuscator as default"); return nullptr; } diff --git a/src/fptn-server/config/command_line_config.cpp b/src/fptn-server/config/command_line_config.cpp index 1c789ad2..f8f07dfb 100644 --- a/src/fptn-server/config/command_line_config.cpp +++ b/src/fptn-server/config/command_line_config.cpp @@ -12,10 +12,6 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) #include // NOLINT(build/include_order) -using fptn::common::network::IPv4Address; -using fptn::common::network::IPv6Address; -using fptn::config::CommandLineConfig; - namespace { bool ParseBoolean(std::string value) noexcept { try { @@ -30,6 +26,11 @@ bool ParseBoolean(std::string value) noexcept { } } // namespace +namespace fptn::config { + +using fptn::common::network::IPv4Address; +using fptn::common::network::IPv6Address; + CommandLineConfig::CommandLineConfig(int argc, char* argv[]) : argc_(argc), argv_(argv), args_("fptn-server", FPTN_VERSION) { // Required arguments @@ -109,6 +110,12 @@ CommandLineConfig::CommandLineConfig(int argc, char* argv[]) "Enable detection of non-FPTN clients or probing attempts during SSL " "handshake. ") .default_value("false"); + args_.add_argument("--server-external-ips") + .help( + "Public IPv4 address of this VPN server. " + "Prevents proxy loops when clients connect via IP. " + "Example: --server-external-ip 1.2.3.4,5.6.7.8") + .default_value(""); } bool CommandLineConfig::Parse() noexcept { // NOLINT(bugprone-exception-escape) @@ -201,3 +208,9 @@ std::size_t CommandLineConfig::MaxActiveSessionsPerUser() const { return static_cast( args_.get("--max-active-sessions-per-user")); } + +[[nodiscard]] std::string CommandLineConfig::ServerExternalIPs() const { + return args_.get("--server-external-ips"); +} + +} // namespace fptn::config diff --git a/src/fptn-server/config/command_line_config.h b/src/fptn-server/config/command_line_config.h index d5205a93..a1adfaba 100644 --- a/src/fptn-server/config/command_line_config.h +++ b/src/fptn-server/config/command_line_config.h @@ -51,6 +51,8 @@ class CommandLineConfig { [[nodiscard]] std::size_t MaxActiveSessionsPerUser() const; + [[nodiscard]] std::string ServerExternalIPs() const; + private: int argc_; char** argv_; diff --git a/src/fptn-server/fptn-server.cpp b/src/fptn-server/fptn-server.cpp index bf33735d..f9a77d47 100644 --- a/src/fptn-server/fptn-server.cpp +++ b/src/fptn-server/fptn-server.cpp @@ -106,7 +106,8 @@ int main(int argc, char* argv[]) { nat_table, user_manager, token_manager, prometheus, config.PrometheusAccessKey(), config.TunInterfaceIPv4(), config.TunInterfaceIPv6(), config.EnableDetectProbing(), - config.MaxActiveSessionsPerUser()); + config.MaxActiveSessionsPerUser(), + config.ServerExternalIPs()); /* init packet filter */ auto filter_manager = std::make_shared(); diff --git a/src/fptn-server/web/listener/listener.cpp b/src/fptn-server/web/listener/listener.cpp index e6e55321..942f5e40 100644 --- a/src/fptn-server/web/listener/listener.cpp +++ b/src/fptn-server/web/listener/listener.cpp @@ -27,6 +27,7 @@ Listener::Listener(std::uint16_t port, boost::asio::io_context& ioc, fptn::common::jwt_token::TokenManagerSPtr token_manager, HandshakeCacheManagerSPtr handshake_cache_manager, + std::string server_external_ips, WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback) @@ -37,6 +38,7 @@ Listener::Listener(std::uint16_t port, acceptor_(ioc_), token_manager_(std::move(token_manager)), handshake_cache_manager_(std::move(handshake_cache_manager)), + server_external_ips_(std::move(server_external_ips)), ws_open_callback_(std::move(ws_open_callback)), ws_new_ippacket_callback_(std::move(ws_new_ippacket_callback)), ws_close_callback_(std::move(ws_close_callback)), @@ -80,8 +82,9 @@ boost::asio::awaitable Listener::Run() { socket, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (!ec) { auto session = std::make_shared(port_, enable_detect_probing_, - std::move(socket), ctx_, api_handles_, handshake_cache_manager_, - ws_open_callback_, ws_new_ippacket_callback_, ws_close_callback_); + server_external_ips_, std::move(socket), ctx_, api_handles_, + handshake_cache_manager_, ws_open_callback_, + ws_new_ippacket_callback_, ws_close_callback_); // run coroutine boost::asio::co_spawn( ioc_, diff --git a/src/fptn-server/web/listener/listener.h b/src/fptn-server/web/listener/listener.h index c90b4381..56818848 100644 --- a/src/fptn-server/web/listener/listener.h +++ b/src/fptn-server/web/listener/listener.h @@ -30,6 +30,7 @@ class Listener final { boost::asio::io_context& ioc, fptn::common::jwt_token::TokenManagerSPtr token_manager, HandshakeCacheManagerSPtr handshake_cache_manager, + std::string server_external_ips, WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback); @@ -52,6 +53,8 @@ class Listener final { HandshakeCacheManagerSPtr handshake_cache_manager_; + const std::string server_external_ips_; + const WebSocketOpenConnectionCallback ws_open_callback_; const WebSocketNewIPPacketCallback ws_new_ippacket_callback_; const WebSocketCloseConnectionCallback ws_close_callback_; diff --git a/src/fptn-server/web/server.cpp b/src/fptn-server/web/server.cpp index 1d06855e..9da0ff74 100644 --- a/src/fptn-server/web/server.cpp +++ b/src/fptn-server/web/server.cpp @@ -28,6 +28,7 @@ Server::Server(std::uint16_t port, fptn::common::network::IPv6Address dns_server_ipv6, bool enable_detect_probing, std::size_t max_active_sessions_per_user, + std::string server_external_ips, int thread_number) : running_(false), port_(port), @@ -40,6 +41,7 @@ Server::Server(std::uint16_t port, dns_server_ipv6_(std::move(dns_server_ipv6)), enable_detect_probing_(enable_detect_probing), max_active_sessions_per_user_(max_active_sessions_per_user), + server_external_ips_(std::move(server_external_ips)), thread_number_(std::max(1, thread_number)), ioc_(thread_number), from_client_(std::make_unique()), @@ -55,8 +57,7 @@ Server::Server(std::uint16_t port, handshake_cache_manager_ = std::make_shared(ioc_); listener_ = std::make_shared(port_, enable_detect_probing_, ioc_, - token_manager, - handshake_cache_manager_, + token_manager, handshake_cache_manager_, server_external_ips_, // NOLINTNEXTLINE(modernize-avoid-bind) std::bind( &Server::HandleWsOpenConnection, this, _1, _2, _3, _4, _5, _6, _7), @@ -112,10 +113,10 @@ bool Server::Start() { } boost::asio::awaitable Server::RunSender() { - const std::chrono::milliseconds timeout{1}; + constexpr std::chrono::milliseconds kTimeout{10}; while (running_) { - auto optpacket = co_await to_client_->WaitForPacketAsync(timeout); + auto optpacket = co_await to_client_->WaitForPacketAsync(kTimeout); if (optpacket && running_) { SessionSPtr session; diff --git a/src/fptn-server/web/server.h b/src/fptn-server/web/server.h index f4bd559e..abb0750d 100644 --- a/src/fptn-server/web/server.h +++ b/src/fptn-server/web/server.h @@ -40,6 +40,7 @@ class Server final { fptn::common::network::IPv6Address dns_server_ipv6, bool enable_detect_probing, std::size_t max_active_sessions_per_user, + std::string server_external_ips, int thread_number = 4); ~Server(); bool Start(); @@ -91,6 +92,7 @@ class Server final { const fptn::common::network::IPv6Address dns_server_ipv6_; const bool enable_detect_probing_; const std::size_t max_active_sessions_per_user_; + const std::string server_external_ips_; const std::size_t thread_number_; boost::asio::io_context ioc_; diff --git a/src/fptn-server/web/session/session.cpp b/src/fptn-server/web/session/session.cpp index 174496e4..23e84f68 100644 --- a/src/fptn-server/web/session/session.cpp +++ b/src/fptn-server/web/session/session.cpp @@ -38,7 +38,8 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT) namespace { std::atomic client_id_counter = 0; -std::vector GetServerIpAddresses() { +std::vector GetServerIpAddresses( + const std::string& server_external_ips) { static std::mutex ip_mutex; static std::vector server_ips; @@ -46,6 +47,15 @@ std::vector GetServerIpAddresses() { if (server_ips.empty()) { server_ips = fptn::common::network::GetServerIpAddresses(); + + if (!server_external_ips.empty()) { + const auto external_ips = + fptn::common::utils::SplitCommaSeparated(server_external_ips); + std::ranges::copy_if(external_ips, std::back_inserter(server_ips), + [](const std::string& ip) { + return fptn::common::network::IsIpAddress(ip); + }); + } } return server_ips; } @@ -67,6 +77,7 @@ namespace fptn::web { Session::Session(std::uint16_t port, bool enable_detect_probing, + std::string server_external_ips, boost::asio::ip::tcp::socket&& socket, boost::asio::ssl::context& ctx, const ApiHandleMap& api_handles, @@ -76,6 +87,7 @@ Session::Session(std::uint16_t port, WebSocketCloseConnectionCallback ws_close_callback) : port_(port), enable_detect_probing_(enable_detect_probing), + server_external_ips_(std::move(server_external_ips)), ws_(ssl_stream_type( obfuscator_socket_type(tcp_stream_type(std::move(socket))), ctx)), strand_(boost::asio::make_strand(ws_.get_executor())), @@ -182,10 +194,7 @@ boost::asio::awaitable Session::Run() { // Prevent recursive proxy attempts for Reality Mode const auto self_proxy = co_await IsSniSelfProxyAttempt(result.sni); if (self_proxy) { - SPDLOG_WARN( - "Detected recursive proxy attempt in Reality Mode! " - "Client: {}, SNI: {}, Redirecting to default SNI", - client_id_, result.sni); + co_await HandleProxy(FPTN_DEFAULT_SNI, port_); Close(); co_return; } @@ -366,15 +375,16 @@ boost::asio::awaitable Session::IsSniSelfProxyAttempt( // First check if SNI is already an IP address if (fptn::common::network::IsIpAddress(sni)) { // FIXME + SPDLOG_WARN("SNI is IP address, treating as potential self-proxy: {}", sni); co_return true; } // Not an IP address - proceed with DNS resolution using our new function try { - const auto server_ips = GetServerIpAddresses(); + const auto server_ips = GetServerIpAddresses(server_external_ips_); boost::asio::io_context ioc; - auto resolve_result = + const auto resolve_result = fptn::common::network::ResolveWithTimeout(ioc, sni, "", 5); if (!resolve_result.success()) { @@ -386,8 +396,14 @@ boost::asio::awaitable Session::IsSniSelfProxyAttempt( // Iterate through resolved endpoints for (const auto& endpoint : resolve_result.results) { const auto ip = endpoint.endpoint().address().to_string(); - const auto exists = std::ranges::find(server_ips, ip); - if (exists != server_ips.end()) { + if (ip.empty()) { + continue; + } + // check server interfaces + if (std::ranges::find(server_ips, ip) != server_ips.end()) { + SPDLOG_WARN( + "SNI {} resolves to server interface IP {}, blocking self-proxy", + sni, ip); co_return true; } } diff --git a/src/fptn-server/web/session/session.h b/src/fptn-server/web/session/session.h index 51dd4056..a9d75adc 100644 --- a/src/fptn-server/web/session/session.h +++ b/src/fptn-server/web/session/session.h @@ -36,6 +36,7 @@ class Session : public std::enable_shared_from_this { public: explicit Session(std::uint16_t port, bool enable_detect_probing, + std::string server_external_ips, boost::asio::ip::tcp::socket&& socket, boost::asio::ssl::context& ctx, const ApiHandleMap& api_handles, @@ -96,6 +97,7 @@ class Session : public std::enable_shared_from_this { const std::uint16_t port_; const bool enable_detect_probing_; + const std::string server_external_ips_; // TCP -> obfuscator -> SSL -> WebSocket using tcp_stream_type = boost::beast::tcp_stream;