Add --max-connections and --max-udp-sessions

The TCP connection pool and the UDP session table are sized from these
options (or max_connections / max_udp_sessions in the configuration
file) at startup instead of fixed constants; the DNS query pool follows
the connection limit. test_limits.py checks that a connection beyond
the limit is refused and that accepting resumes once one closes.
This commit is contained in:
Max Lv
2026-10-15 00:34:30 +08:00
parent 6a1500ddd7
commit 282f3606d7
12 changed files with 198 additions and 9 deletions
+2
View File
@@ -79,6 +79,8 @@ The config file equivalent of command line options is listed as examples below.
- `--fast-open`: `"fast_open": true`
- `--reuse-port`: `"reuse_port": true`
- `--no-delay`: `"no_delay": true`
- `--max-connections 1024`: `"max_connections": 1024`
- `--max-udp-sessions 512`: `"max_udp_sessions": 512`
- `--printable-salt`: `"printable_salt": true` (ss-local only)
- `--plugin "obfs-server"`: `"plugin": "obfs-server"`
- `--plugin-opts "obfs=http"`: `"plugin_opts": "obfs=http"`
+2
View File
@@ -65,6 +65,8 @@ enum {
GETOPT_VAL_REUSE_PORT,
GETOPT_VAL_FAST_OPEN,
GETOPT_VAL_NODELAY,
GETOPT_VAL_MAX_CONNECTIONS,
GETOPT_VAL_MAX_UDP_SESSIONS,
GETOPT_VAL_PRINTABLE_SALT,
GETOPT_VAL_ACL,
GETOPT_VAL_MTU,
+4
View File
@@ -389,6 +389,10 @@ read_jconf(const char *file)
conf.tcp_outgoing_rcvbuf = to_nonnegative_int(value, name);
} else if (strcmp(name, "auth") == 0) {
FATAL("One time auth has been deprecated. Try AEAD ciphers instead.");
} else if (strcmp(name, "max_connections") == 0) {
conf.max_connections = to_nonnegative_int(value, name);
} else if (strcmp(name, "max_udp_sessions") == 0) {
conf.max_udp_sessions = to_nonnegative_int(value, name);
} else if (strcmp(name, "nofile") == 0) {
conf.nofile = to_nonnegative_int(value, name);
} else if (strcmp(name, "nameserver") == 0) {
+2
View File
@@ -92,6 +92,8 @@ typedef struct {
int mptcp;
int ipv6_first;
int no_delay;
int max_connections;
int max_udp_sessions;
int printable_salt;
int tcp_tproxy;
char *workdir;
+24 -2
View File
@@ -111,6 +111,8 @@ static int mode = TCP_ONLY;
static int ipv6first = 0;
int fast_open = 0;
static int no_delay = 0;
static int max_connections = 0;
static int max_udp_sessions = 0;
static int printable_salt = 0;
static int udp_fd = 0;
static int ret_val = 0;
@@ -323,7 +325,7 @@ struct Relays {
config.kind = ss::relay::Kind::local;
config.remote = *remote;
config.timeout_secs = timeout;
config.max_sessions = 256;
config.max_sessions = max_udp_sessions > 0 ? max_udp_sessions : 256;
config.mtu = mtu;
config.iface = iface;
#ifdef __ANDROID__
@@ -372,7 +374,7 @@ fill_tcp_config(ss::relay::TcpConfig &config, const struct sockaddr_storage *rem
config.kind = ss::relay::Kind::local;
config.remotes = { remotes, (size_t)remote_count };
config.timeout_secs = timeout;
config.max_connections = DEFAULT_MAX_CONNECTIONS;
config.max_connections = max_connections > 0 ? max_connections : DEFAULT_MAX_CONNECTIONS;
config.fast_open = fast_open != 0;
config.no_delay = no_delay != 0;
config.printable_salt = printable_salt != 0;
@@ -616,6 +618,8 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
\snippet{doc} utils.cpp cli_long_tcp_outgoing_rcvbuf
\snippet{doc} utils.cpp cli_long_fast_open
\snippet{doc} utils.cpp cli_long_no_delay
\snippet{doc} utils.cpp cli_long_max_connections
\snippet{doc} utils.cpp cli_long_max_udp_sessions
\snippet{doc} utils.cpp cli_long_acl
\snippet{doc} utils.cpp cli_long_mtu
\snippet{doc} utils.cpp cli_long_mptcp
@@ -658,6 +662,8 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
{ "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN },
{ "printable-salt", no_argument, NULL, GETOPT_VAL_PRINTABLE_SALT },
{ "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY },
{ "max-connections", required_argument, NULL, GETOPT_VAL_MAX_CONNECTIONS },
{ "max-udp-sessions", required_argument, NULL, GETOPT_VAL_MAX_UDP_SESSIONS },
{ "acl", required_argument, NULL, GETOPT_VAL_ACL },
{ "mtu", required_argument, NULL, GETOPT_VAL_MTU },
{ "mptcp", no_argument, NULL, GETOPT_VAL_MPTCP },
@@ -716,6 +722,16 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
case GETOPT_VAL_PRINTABLE_SALT:
printable_salt = 1;
break;
case GETOPT_VAL_MAX_CONNECTIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_connections) == -1) {
cli_error("invalid connection limit", c, NULL);
}
break;
case GETOPT_VAL_MAX_UDP_SESSIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_udp_sessions) == -1) {
cli_error("invalid UDP session limit", c, NULL);
}
break;
case GETOPT_VAL_NODELAY:
no_delay = 1;
LOGI("enable TCP no-delay");
@@ -957,6 +973,12 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
if (no_delay == 0) {
no_delay = conf->no_delay;
}
if (max_connections == 0) {
max_connections = conf->max_connections;
}
if (max_udp_sessions == 0) {
max_udp_sessions = conf->max_udp_sessions;
}
if (printable_salt == 0) {
printable_salt = conf->printable_salt;
}
+24 -2
View File
@@ -98,6 +98,8 @@ static int nofile = 0;
#endif
int fast_open = 0;
static int no_delay = 0;
static int max_connections = 0;
static int max_udp_sessions = 0;
static int ret_val = 0;
static struct ss_signal sigint_watcher;
@@ -387,6 +389,8 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
\snippet{doc} utils.cpp cli_long_tcp_outgoing_sndbuf
\snippet{doc} utils.cpp cli_long_tcp_outgoing_rcvbuf
\snippet{doc} utils.cpp cli_long_no_delay
\snippet{doc} utils.cpp cli_long_max_connections
\snippet{doc} utils.cpp cli_long_max_udp_sessions
\snippet{doc} utils.cpp cli_long_password
\snippet{doc} utils.cpp cli_long_key
\snippet{doc} utils.cpp cli_long_help
@@ -421,6 +425,8 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
{ "tcp-outgoing-sndbuf", required_argument, NULL, GETOPT_VAL_TCP_OUTGOING_SNDBUF },
{ "tcp-outgoing-rcvbuf", required_argument, NULL, GETOPT_VAL_TCP_OUTGOING_RCVBUF },
{ "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY },
{ "max-connections", required_argument, NULL, GETOPT_VAL_MAX_CONNECTIONS },
{ "max-udp-sessions", required_argument, NULL, GETOPT_VAL_MAX_UDP_SESSIONS },
{ "password", required_argument, NULL, GETOPT_VAL_PASSWORD },
{ "key", required_argument, NULL, GETOPT_VAL_KEY },
{ "help", no_argument, NULL, GETOPT_VAL_HELP },
@@ -461,6 +467,16 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
if (mptcp)
LOGI("enable multipath TCP (%s)", mptcp > 0 ? "out-of-tree" : "upstream");
break;
case GETOPT_VAL_MAX_CONNECTIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_connections) == -1) {
cli_error("invalid connection limit", c, NULL);
}
break;
case GETOPT_VAL_MAX_UDP_SESSIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_udp_sessions) == -1) {
cli_error("invalid UDP session limit", c, NULL);
}
break;
case GETOPT_VAL_NODELAY:
no_delay = 1;
LOGI("enable TCP no-delay");
@@ -656,6 +672,12 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
if (no_delay == 0) {
no_delay = conf->no_delay;
}
if (max_connections == 0) {
max_connections = conf->max_connections;
}
if (max_udp_sessions == 0) {
max_udp_sessions = conf->max_udp_sessions;
}
if (reuse_port == 0) {
reuse_port = conf->reuse_port;
}
@@ -866,7 +888,7 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
ss::relay::UdpConfig udp_config;
udp_config.kind = ss::relay::Kind::redir;
udp_config.timeout_secs = timeout_secs;
udp_config.max_sessions = 256;
udp_config.max_sessions = max_udp_sessions > 0 ? max_udp_sessions : 256;
udp_config.mtu = mtu;
udp_config.reuse_port = reuse_port != 0;
char *host = remote_addr[0].host;
@@ -883,7 +905,7 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
tcp_config.kind = ss::relay::Kind::redir;
tcp_config.remotes = { remote_storage, (size_t)remote_count };
tcp_config.timeout_secs = timeout_secs;
tcp_config.max_connections = DEFAULT_MAX_CONNECTIONS;
tcp_config.max_connections = max_connections > 0 ? max_connections : DEFAULT_MAX_CONNECTIONS;
tcp_config.fast_open = fast_open != 0;
tcp_config.no_delay = no_delay != 0;
tcp_config.tproxy = tcp_tproxy != 0;
+1
View File
@@ -142,6 +142,7 @@ Status TcpRelay::init(event::Loop &loop, Method &method, const TcpConfig &config
idle_list_.set_timeout(std::max<std::uint64_t>(MIN_TCP_IDLE_TIMEOUT, timeout) * 1000);
SS_TRY(timer_.open(loop, *this));
own_closed_ = false;
LOGI("tcp: up to %u connections (%zu KiB each)", config.max_connections, sizeof(Connection) / 1024);
return {};
}
+26 -3
View File
@@ -132,6 +132,8 @@ static int mode = TCP_ONLY;
static int ipv6first = 0;
int fast_open = 0;
static int no_delay = 0;
static int max_connections = 0;
static int max_udp_sessions = 0;
static int ret_val = 0;
#ifdef HAVE_SETRLIMIT
@@ -987,6 +989,8 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
\snippet{doc} utils.cpp cli_long_tcp_outgoing_sndbuf
\snippet{doc} utils.cpp cli_long_tcp_outgoing_rcvbuf
\snippet{doc} utils.cpp cli_long_no_delay
\snippet{doc} utils.cpp cli_long_max_connections
\snippet{doc} utils.cpp cli_long_max_udp_sessions
\snippet{doc} utils.cpp cli_long_acl
\snippet{doc} utils.cpp cli_long_manager_address
\snippet{doc} utils.cpp cli_long_mtu
@@ -1024,6 +1028,8 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
{ "tcp-outgoing-sndbuf", required_argument, NULL, GETOPT_VAL_TCP_OUTGOING_SNDBUF },
{ "tcp-outgoing-rcvbuf", required_argument, NULL, GETOPT_VAL_TCP_OUTGOING_RCVBUF },
{ "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY },
{ "max-connections", required_argument, NULL, GETOPT_VAL_MAX_CONNECTIONS },
{ "max-udp-sessions", required_argument, NULL, GETOPT_VAL_MAX_UDP_SESSIONS },
{ "acl", required_argument, NULL, GETOPT_VAL_ACL },
{ "manager-address", required_argument, NULL,
GETOPT_VAL_MANAGER_ADDRESS },
@@ -1065,6 +1071,16 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
case GETOPT_VAL_FAST_OPEN:
fast_open = 1;
break;
case GETOPT_VAL_MAX_CONNECTIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_connections) == -1) {
cli_error("invalid connection limit", c, NULL);
}
break;
case GETOPT_VAL_MAX_UDP_SESSIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_udp_sessions) == -1) {
cli_error("invalid UDP session limit", c, NULL);
}
break;
case GETOPT_VAL_NODELAY:
no_delay = 1;
LOGI("enable TCP no-delay");
@@ -1265,6 +1281,12 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
if (no_delay == 0) {
no_delay = conf->no_delay;
}
if (max_connections == 0) {
max_connections = conf->max_connections;
}
if (max_udp_sessions == 0) {
max_udp_sessions = conf->max_udp_sessions;
}
if (reuse_port == 0) {
reuse_port = conf->reuse_port;
}
@@ -1536,14 +1558,15 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
ss::relay::UdpRelay udp_relay;
bool tcp_running = false;
if (!tcp_loop.adopt(ss_native_loop(loop))
|| !tcp_resolver.init(tcp_loop, nameservers, ipv6first != 0, DEFAULT_MAX_CONNECTIONS + 64)) {
|| !tcp_resolver.init(tcp_loop, nameservers, ipv6first != 0,
(max_connections > 0 ? max_connections : DEFAULT_MAX_CONNECTIONS) + 64)) {
FATAL("failed to set up the resolver");
}
if (mode != UDP_ONLY) {
ss::relay::TcpConfig tcp_config;
tcp_config.kind = ss::relay::Kind::server;
tcp_config.timeout_secs = timeout_secs;
tcp_config.max_connections = DEFAULT_MAX_CONNECTIONS;
tcp_config.max_connections = max_connections > 0 ? max_connections : DEFAULT_MAX_CONNECTIONS;
tcp_config.fast_open = fast_open != 0;
tcp_config.no_delay = no_delay != 0;
tcp_config.acl = acl != 0;
@@ -1618,7 +1641,7 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
ss::relay::UdpConfig udp_config;
udp_config.kind = ss::relay::Kind::server;
udp_config.timeout_secs = timeout_secs;
udp_config.max_sessions = 512;
udp_config.max_sessions = max_udp_sessions > 0 ? max_udp_sessions : 512;
udp_config.mtu = mtu;
udp_config.iface = iface;
udp_config.label = remote_port != NULL ? remote_port : "";
+24 -2
View File
@@ -113,6 +113,8 @@ static int mode = TCP_ONLY;
static int nofile = 0;
#endif
static int no_delay = 0;
static int max_connections = 0;
static int max_udp_sessions = 0;
int fast_open = 0;
static int ret_val = 0;
@@ -411,6 +413,8 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
\snippet{doc} utils.cpp cli_long_fast_open
\snippet{doc} utils.cpp cli_long_mtu
\snippet{doc} utils.cpp cli_long_no_delay
\snippet{doc} utils.cpp cli_long_max_connections
\snippet{doc} utils.cpp cli_long_max_udp_sessions
\snippet{doc} utils.cpp cli_long_mptcp
\snippet{doc} utils.cpp cli_long_plugin
\snippet{doc} utils.cpp cli_long_plugin_opts
@@ -449,6 +453,8 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
{ "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN },
{ "mtu", required_argument, NULL, GETOPT_VAL_MTU },
{ "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY },
{ "max-connections", required_argument, NULL, GETOPT_VAL_MAX_CONNECTIONS },
{ "max-udp-sessions", required_argument, NULL, GETOPT_VAL_MAX_UDP_SESSIONS },
{ "mptcp", no_argument, NULL, GETOPT_VAL_MPTCP },
{ "plugin", required_argument, NULL, GETOPT_VAL_PLUGIN },
{ "plugin-opts", required_argument, NULL, GETOPT_VAL_PLUGIN_OPTS },
@@ -502,6 +508,16 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
if (mptcp)
LOGI("enable multipath TCP (%s)", mptcp > 0 ? "out-of-tree" : "upstream");
break;
case GETOPT_VAL_MAX_CONNECTIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_connections) == -1) {
cli_error("invalid connection limit", c, NULL);
}
break;
case GETOPT_VAL_MAX_UDP_SESSIONS:
if (ss_parse_int(optarg, 1, 1 << 20, &max_udp_sessions) == -1) {
cli_error("invalid UDP session limit", c, NULL);
}
break;
case GETOPT_VAL_NODELAY:
no_delay = 1;
LOGI("enable TCP no-delay");
@@ -708,6 +724,12 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
if (no_delay == 0) {
no_delay = conf->no_delay;
}
if (max_connections == 0) {
max_connections = conf->max_connections;
}
if (max_udp_sessions == 0) {
max_udp_sessions = conf->max_udp_sessions;
}
if (reuse_port == 0) {
reuse_port = conf->reuse_port;
}
@@ -965,7 +987,7 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
tcp_config.kind = ss::relay::Kind::tunnel;
tcp_config.remotes = { remote_storage, (size_t)remote_count };
tcp_config.timeout_secs = timeout_secs;
tcp_config.max_connections = DEFAULT_MAX_CONNECTIONS;
tcp_config.max_connections = max_connections > 0 ? max_connections : DEFAULT_MAX_CONNECTIONS;
tcp_config.fast_open = fast_open != 0;
tcp_config.no_delay = no_delay != 0;
tcp_config.upstream.sndbuf = tcp_outgoing_sndbuf;
@@ -1025,7 +1047,7 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
udp_config.tunnel_target = tcp_config.tunnel_target;
udp_config.tunnel_target_len = tcp_config.tunnel_target_len;
udp_config.timeout_secs = timeout_secs;
udp_config.max_sessions = 256;
udp_config.max_sessions = max_udp_sessions > 0 ? max_udp_sessions : 256;
udp_config.mtu = mtu;
udp_config.iface = iface;
#ifdef __ANDROID__
+18
View File
@@ -485,6 +485,20 @@ Only available with MPTCP enabled Linux kernel.
Enable TCP_NODELAY.
[cli_long_no_delay] */
/* [cli_long_max_connections]
\par `--max-connections <number>`
Serve at most this many TCP connections at once (default: 1024). Their
memory is reserved at startup; a connection beyond the limit is refused
until another one closes.
[cli_long_max_connections] */
/* [cli_long_max_udp_sessions]
\par `--max-udp-sessions <number>`
Keep at most this many UDP associations (default: 256 for clients, 512
for ss-server). When the table is full, the least recently used one is
dropped.
[cli_long_max_udp_sessions] */
/* [cli_long_tcp_incoming_sndbuf]
\par `--tcp-incoming-sndbuf <size>`
Set TCP send buffer size for incoming connections.
@@ -704,6 +718,10 @@ usage(void)
cli_help_option("--fast-open", "Enable TCP Fast Open where supported.");
cli_help_option("--reuse-port", "Enable port reuse where supported.");
cli_help_option("--no-delay", "Enable TCP_NODELAY.");
#ifndef MODULE_MANAGER
cli_help_option("--max-connections NUMBER", "TCP connections served at once (default: 1024).");
cli_help_option("--max-udp-sessions NUMBER", "UDP associations kept (default: 256; ss-server 512).");
#endif
#ifdef MODULE_LOCAL
cli_help_option("--printable-salt", "Experimental printable TCP salt (chacha20-ietf-poly1305/aes-256-gcm only).");
#endif
+4
View File
@@ -382,6 +382,10 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
if(SS_ENABLE_LEGACY)
list(APPEND SS_TUNNEL_INTEROP_ARGS --legacy)
endif()
add_test(NAME test_limits
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_limits.py
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
set_tests_properties(test_limits PROPERTIES LABELS "integration" TIMEOUT 60)
add_test(NAME test_tunnel_interop
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tunnel_interop.py ${SS_TUNNEL_INTEROP_ARGS})
set_tests_properties(test_tunnel_interop PROPERTIES LABELS "integration" TIMEOUT 300)
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""--max-connections: connections beyond the limit are refused, and accepting
resumes once a slot frees."""
import argparse
from pathlib import Path
import socket
import threading
import time
from interop import TCPHandler, TCPOrigin, free_port, peers, receive, socks
def echo_once(sock):
sock.sendall(b"ping")
return receive(sock, 4) == b"ping"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--bin", required=True, type=Path)
args = parser.parse_args()
bins = args.bin.resolve()
with TCPOrigin(("127.0.0.1", 0), TCPHandler) as origin:
threading.Thread(target=origin.serve_forever, daemon=True).start()
sport, lport = free_port(), free_port()
server = [str(bins / "ss-server"), "-s", "127.0.0.1", "-p", str(sport), "-k", "pw", "-m", "aes-256-gcm",
"--max-connections", "2"]
local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(sport), "-l", str(lport), "-k", "pw",
"-m", "aes-256-gcm"]
with peers([server, local], [sport, lport]):
target = ("127.0.0.1", origin.server_address[1])
held = []
for _ in range(2):
sock, _ = socks(lport, 1, target)
assert echo_once(sock), "connection within the limit failed"
held.append(sock)
# The third is refused by ss-server, so ss-local drops it.
third, _ = socks(lport, 1, target)
third.settimeout(3)
try:
refused = not echo_once(third)
except (OSError, AssertionError):
refused = True
third.close()
assert refused, "a connection beyond --max-connections was served"
# Free a slot; accepting resumes.
held.pop().close()
deadline = time.monotonic() + 5
while True:
try:
sock, _ = socks(lport, 1, target)
sock.settimeout(3)
if echo_once(sock):
held.append(sock)
break
except (OSError, AssertionError):
pass
assert time.monotonic() < deadline, "accepting did not resume after a slot freed"
time.sleep(.1)
for sock in held:
sock.close()
print("PASS --max-connections refuses and recovers")
return 0
if __name__ == "__main__":
raise SystemExit(main())