Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions deploy/docker/scripts/start-fptn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
9 changes: 8 additions & 1 deletion deploy/linux/deb/create-server-deb-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docker-compose/.env.demo
Original file line number Diff line number Diff line change
@@ -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

Expand Down
1 change: 1 addition & 0 deletions docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions src/fptn-protocol-lib/https/obfuscator/methods/detector.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ inline IObfuscatorSPtr DetectObfuscator(
const std::uint8_t* data, std::size_t size) {
auto tls_obfuscator = std::make_shared<TlsObfuscator>();
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;
}

Expand Down
21 changes: 17 additions & 4 deletions src/fptn-server/config/command_line_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT)

#include <spdlog/spdlog.h> // 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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -201,3 +208,9 @@ std::size_t CommandLineConfig::MaxActiveSessionsPerUser() const {
return static_cast<std::size_t>(
args_.get<int>("--max-active-sessions-per-user"));
}

[[nodiscard]] std::string CommandLineConfig::ServerExternalIPs() const {
return args_.get<std::string>("--server-external-ips");
}

} // namespace fptn::config
2 changes: 2 additions & 0 deletions src/fptn-server/config/command_line_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ class CommandLineConfig {

[[nodiscard]] std::size_t MaxActiveSessionsPerUser() const;

[[nodiscard]] std::string ServerExternalIPs() const;

private:
int argc_;
char** argv_;
Expand Down
3 changes: 2 additions & 1 deletion src/fptn-server/fptn-server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<fptn::filter::Manager>();
Expand Down
7 changes: 5 additions & 2 deletions src/fptn-server/web/listener/listener.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)),
Expand Down Expand Up @@ -80,8 +82,9 @@ boost::asio::awaitable<void> Listener::Run() {
socket, boost::asio::redirect_error(boost::asio::use_awaitable, ec));
if (!ec) {
auto session = std::make_shared<Session>(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_,
Expand Down
3 changes: 3 additions & 0 deletions src/fptn-server/web/listener/listener.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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_;
Expand Down
9 changes: 5 additions & 4 deletions src/fptn-server/web/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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<std::size_t>(1, thread_number)),
ioc_(thread_number),
from_client_(std::make_unique<fptn::common::data::Channel>()),
Expand All @@ -55,8 +57,7 @@ Server::Server(std::uint16_t port,
handshake_cache_manager_ = std::make_shared<HandshakeCacheManager>(ioc_);

listener_ = std::make_shared<Listener>(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),
Expand Down Expand Up @@ -112,10 +113,10 @@ bool Server::Start() {
}

boost::asio::awaitable<void> 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;

Expand Down
2 changes: 2 additions & 0 deletions src/fptn-server/web/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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_;
Expand Down
34 changes: 25 additions & 9 deletions src/fptn-server/web/session/session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,24 @@ Distributed under the MIT License (https://opensource.org/licenses/MIT)
namespace {
std::atomic<fptn::ClientID> client_id_counter = 0;

std::vector<std::string> GetServerIpAddresses() {
std::vector<std::string> GetServerIpAddresses(
const std::string& server_external_ips) {
static std::mutex ip_mutex;
static std::vector<std::string> server_ips;

const std::scoped_lock lock(ip_mutex); // mutex

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;
}
Expand All @@ -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,
Expand All @@ -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())),
Expand Down Expand Up @@ -182,10 +194,7 @@ boost::asio::awaitable<void> 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;
}
Expand Down Expand Up @@ -366,15 +375,16 @@ boost::asio::awaitable<bool> 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()) {
Expand All @@ -386,8 +396,14 @@ boost::asio::awaitable<bool> 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;
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/fptn-server/web/session/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Session : public std::enable_shared_from_this<Session> {
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,
Expand Down Expand Up @@ -96,6 +97,7 @@ class Session : public std::enable_shared_from_this<Session> {

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;
Expand Down