From 83a2af1450a3967a05767287dbae362f312550d9 Mon Sep 17 00:00:00 2001 From: Stas Skokov <7090stas@gmail.com> Date: Sun, 18 Jan 2026 18:27:53 +1100 Subject: [PATCH] [feature] support subdomains in SNI whitelist --- deploy/linux/deb/create-server-deb-package.sh | 13 ++++++++-- docker-compose/.env.demo | 13 ++++++++-- src/fptn-server/web/session/session.cpp | 24 +++++++------------ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/deploy/linux/deb/create-server-deb-package.sh b/deploy/linux/deb/create-server-deb-package.sh index 9cbac39..8725a29 100755 --- a/deploy/linux/deb/create-server-deb-package.sh +++ b/deploy/linux/deb/create-server-deb-package.sh @@ -55,10 +55,19 @@ DEFAULT_PROXY_DOMAIN=cdnvideo.com # Comma-separated list of allowed website domains for non-VPN clients # This acts like a "whitelist" of websites that scanning bots are allowed to reach # Behavior logic: -# - List is empty (default): proxy all non-VPN traffic to DEFAULT_PROXY_DOMAIN +# - List is empty (default): allows ALL domains, proxy all non-VPN traffic to the SNI in the TLS-handshake # - List is NOT empty: use as whitelist: -# - Client SNI in list -> proxy to client's SNI\ +# - Client SNI in list -> proxy to client's SNI # - Client SNI not in list -> proxy to --default-proxy-domain +# Domain matching includes all subdomains: +# - If "example.com" is in the list, it will match: +# - example.com (exact match) +# - www.example.com +# - api.example.com +# - any.other.sub.example.com +# Examples: +# ALLOWED_SNI_LIST=example.com,test.org +# This allows: example.com, test.org and ALL their subdomains ALLOWED_SNI_LIST= # Block BitTorrent traffic to prevent abuse (accepted values: true or false) diff --git a/docker-compose/.env.demo b/docker-compose/.env.demo index 46e4a55..bb96940 100644 --- a/docker-compose/.env.demo +++ b/docker-compose/.env.demo @@ -16,10 +16,19 @@ DEFAULT_PROXY_DOMAIN=cdnvideo.com # Comma-separated list of allowed website domains for non-VPN clients # This acts like a "whitelist" of websites that scanning bots are allowed to reach # Behavior logic: -# - List is empty (default): proxy all non-VPN traffic to DEFAULT_PROXY_DOMAIN +# - List is empty (default): allows ALL domains, proxy all non-VPN traffic to the SNI in the TLS-handshake # - List is NOT empty: use as whitelist: -# - Client SNI in list -> proxy to client's SNI\ +# - Client SNI in list -> proxy to client's SNI # - Client SNI not in list -> proxy to --default-proxy-domain +# Domain matching includes all subdomains: +# - If "example.com" is in the list, it will match: +# - example.com (exact match) +# - www.example.com +# - api.example.com +# - any.other.sub.example.com +# Examples: +# ALLOWED_SNI_LIST=example.com,test.org +# This allows: example.com, test.org and ALL their subdomains ALLOWED_SNI_LIST= # Block BitTorrent traffic to prevent abuse (accepted values: true or false) diff --git a/src/fptn-server/web/session/session.cpp b/src/fptn-server/web/session/session.cpp index 23af569..e225f90 100644 --- a/src/fptn-server/web/session/session.cpp +++ b/src/fptn-server/web/session/session.cpp @@ -326,7 +326,14 @@ boost::asio::awaitable Session::DetectProbing() { if (!allowed_sni_list_.empty()) { const bool sni_allowed = std::ranges::any_of( allowed_sni_list_, [&sni](const std::string& allowed_sni) { - return sni == allowed_sni; + if (sni == allowed_sni) { + return true; + } + // check subdomains + if (sni.size() > allowed_sni.size() + 1) { + return sni.ends_with("." + allowed_sni); + } + return false; }); if (!sni_allowed) { sni = default_proxy_domain_; @@ -496,21 +503,6 @@ boost::asio::awaitable Session::IsRealityHandshake() { } } - // Validate allowed sni - if (!allowed_sni_list_.empty()) { - const bool sni_allowed = std::ranges::any_of( - allowed_sni_list_, [&sni](const std::string& allowed_sni) { - return sni == allowed_sni; - }); - if (!sni_allowed) { - sni = default_proxy_domain_; - SPDLOG_WARN( - "SNI '{}' not in allowed list, using default domain: {} " - "(client_id={})", - sni, default_proxy_domain_, client_id_); - } - } - // Check if this is a reality mode handshake by examining session ID constexpr std::size_t kSessionLen = 32; std::size_t session_len = std::min(