From 282f3606d7463105036b81ffbf21fe88e4d6a6b6 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 14 Oct 2026 15:55:07 +0800 Subject: [PATCH] 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. --- doc/shadowsocks-c.md | 2 ++ src/common.h | 2 ++ src/jconf.cpp | 4 +++ src/jconf.h | 2 ++ src/local.cpp | 26 +++++++++++++++-- src/redir.cpp | 26 +++++++++++++++-- src/relay/tcp.cpp | 1 + src/server.cpp | 29 +++++++++++++++++-- src/tunnel.cpp | 26 +++++++++++++++-- src/utils.cpp | 18 ++++++++++++ tests/CMakeLists.txt | 4 +++ tests/test_limits.py | 67 ++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 tests/test_limits.py diff --git a/doc/shadowsocks-c.md b/doc/shadowsocks-c.md index e1ce79c1..40df44e8 100644 --- a/doc/shadowsocks-c.md +++ b/doc/shadowsocks-c.md @@ -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"` diff --git a/src/common.h b/src/common.h index 771ff785..dc37d5cb 100644 --- a/src/common.h +++ b/src/common.h @@ -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, diff --git a/src/jconf.cpp b/src/jconf.cpp index b7bf0392..d6c82546 100644 --- a/src/jconf.cpp +++ b/src/jconf.cpp @@ -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) { diff --git a/src/jconf.h b/src/jconf.h index 807b6bb2..6e51ce8e 100644 --- a/src/jconf.h +++ b/src/jconf.h @@ -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; diff --git a/src/local.cpp b/src/local.cpp index dd71cd34..72e6d536 100644 --- a/src/local.cpp +++ b/src/local.cpp @@ -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; } diff --git a/src/redir.cpp b/src/redir.cpp index 6c8d9d2a..c688dfcf 100644 --- a/src/redir.cpp +++ b/src/redir.cpp @@ -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; diff --git a/src/relay/tcp.cpp b/src/relay/tcp.cpp index 3250817e..086d3aa2 100644 --- a/src/relay/tcp.cpp +++ b/src/relay/tcp.cpp @@ -142,6 +142,7 @@ Status TcpRelay::init(event::Loop &loop, Method &method, const TcpConfig &config idle_list_.set_timeout(std::max(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 {}; } diff --git a/src/server.cpp b/src/server.cpp index 66d8a2fd..86f7cf73 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -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 : ""; diff --git a/src/tunnel.cpp b/src/tunnel.cpp index 30c3b7a8..3409861f 100644 --- a/src/tunnel.cpp +++ b/src/tunnel.cpp @@ -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__ diff --git a/src/utils.cpp b/src/utils.cpp index 0b6d61ae..c5648a70 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -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 ` +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 ` +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 ` 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index de229144..0277352b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/test_limits.py b/tests/test_limits.py new file mode 100644 index 00000000..ab4c3188 --- /dev/null +++ b/tests/test_limits.py @@ -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())