From ea951e6bd12279cb900b1b116971b1cc3cb4fbcf Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sat, 12 Sep 2026 21:17:38 +0800 Subject: [PATCH] Redesign CLI flags, help, and argument validation --- README.md | 4 + doc/index.md | 31 +++++ doc/ss-local.md | 2 +- doc/ss-server.md | 2 +- doc/ss-tunnel.md | 2 +- scripts/check_cli_docs.py | 2 +- src/common.h | 5 +- src/local.c | 197 +++++++++++++++++++++++--- src/manager.c | 151 +++++++++++++++++--- src/redir.c | 177 ++++++++++++++++++++--- src/server.c | 178 +++++++++++++++++++++--- src/tunnel.c | 196 +++++++++++++++++++++++--- src/utils.c | 285 +++++++++++++++++++------------------- src/utils.h | 2 + tests/CMakeLists.txt | 10 ++ tests/test_cli.py | 152 ++++++++++++++++++++ tests/test_cli_docs.py | 4 +- 17 files changed, 1161 insertions(+), 239 deletions(-) create mode 100644 tests/test_cli.py diff --git a/README.md b/README.md index 457f61f8e..a10058c63 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,10 @@ Current version: 3.3.6 | [Changelog](debian/changelog) [CLI reference and configuration guide](https://shadowsocks.github.io/shadowsocks-c/) are generated from the source with Doxygen and published after updates to `master`. +Use `--help` for grouped CLI options and `--version` for version information. +See [CLI conventions](https://shadowsocks.github.io/shadowsocks-c/index.html#cli_conventions) +for the long flags, short aliases, and configuration precedence. + ## Community See the [contribution guide](CONTRIBUTION.md) for development setup, testing, diff --git a/doc/index.md b/doc/index.md index 9beb7a335..3bbd16573 100644 --- a/doc/index.md +++ b/doc/index.md @@ -14,3 +14,34 @@ source documentation. [Download the generated man pages](https://shadowsocks.github.io/shadowsocks-c/man-pages.tar.gz) from the published site. The archive includes the six CLI manuals and both overview lookup names. + +\section cli_conventions CLI conventions + +Use descriptive long flags or their existing short aliases. Connections use +different names for the remote server and the local listener: + +- Clients: `--server HOST --server-port PORT` selects the remote Shadowsocks server. +- Listeners: `--listen-address ADDRESS --listen-port PORT` controls where a client + or server accepts connections. The manager uses `--manager-address` for its + control socket and configuration or its API for individual server ports. +- Tunnels: `--destination HOST:PORT` selects the forwarding destination. +- Configuration: `--config FILE`, `--cipher NAME`, and `--password SECRET` map to + `-c`, `-m`, and `-k`. AEAD-2022 keys can also be supplied with `--key BASE64`. + +Explicit transport and address-family flags override the JSON configuration. +The last transport flag +(`--tcp-only`, `--udp`, or `--udp-only`) wins, as does the last address preference +(`--ipv4-first` or `--ipv6-first`). Address preferences do not disable either IP +family. Other options retain their existing semantics, including repeatable +server addresses and `ss-local --server-url` imports. Zero-valued resource +settings such as MTU and TCP buffer sizes retain their existing behavior: they +fall back to the configured value, or the system default when none is configured. + +`--help` and `--version` write to standard output and exit successfully. Unknown +options, missing values, invalid numeric CLI values, and unexpected positional +arguments write usage errors to standard error and exit with status 2. + +The formerly ignored `-l` flag in `ss-server` and `ss-manager` is now an error. +Use `ss-server --listen-port` instead; manager-controlled ports come from its +configuration or API. The removed one-time-authentication flag `-A` remains an +error. Use an AEAD cipher. diff --git a/doc/ss-local.md b/doc/ss-local.md index 7f6655937..e7f44f539 100644 --- a/doc/ss-local.md +++ b/doc/ss-local.md @@ -29,7 +29,7 @@ ss-local(1) can be started from command line and run in foreground. Here is an example: ``` # Start ss-local with given parameters -ss-local -s example.com -p 12345 -l 1080 -k foobar -m aes-256-cfb +ss-local --server example.com --server-port 12345 --listen-port 1080 --password foobar --cipher aes-256-gcm ``` \section ss_local_see_also SEE ALSO diff --git a/doc/ss-server.md b/doc/ss-server.md index e814bc050..e3eac5765 100644 --- a/doc/ss-server.md +++ b/doc/ss-server.md @@ -32,7 +32,7 @@ Check out the `SEE ALSO` section for the default path of config file. ``` # Start the ss-server -ss-server -c /etc/shadowsocks-libev/config.json +ss-server --config /etc/shadowsocks-libev/config.json ``` \section ss_server_incompatibility INCOMPATIBILITY diff --git a/doc/ss-tunnel.md b/doc/ss-tunnel.md index 16c411b24..06b3f0a01 100644 --- a/doc/ss-tunnel.md +++ b/doc/ss-tunnel.md @@ -31,7 +31,7 @@ through the shadowsocks tunnel. Here is an example: ``` # Forward local UDP port 5353 to 8.8.8.8:53 through the ss-server -ss-tunnel -s example.com -p 12345 -l 5353 -k foobar -m aes-256-cfb -L 8.8.8.8:53 -u +ss-tunnel --server example.com --server-port 12345 --listen-port 5353 --password foobar --cipher aes-256-gcm --destination 8.8.8.8:53 --udp # Then configure your system to use 127.0.0.1:5353 as the DNS server dig @127.0.0.1 -p 5353 www.google.com diff --git a/scripts/check_cli_docs.py b/scripts/check_cli_docs.py index 9ac4c43a5..c87a196d7 100644 --- a/scripts/check_cli_docs.py +++ b/scripts/check_cli_docs.py @@ -51,7 +51,7 @@ def options(source, shell=False): if len(tables) != 1: raise ValueError("Expected one long_options table") table = re.sub(r'^\s*#.*$', '', tables[0], flags=re.M) - entry = re.compile(r'\{\s*"([a-z0-9-]+)"\s*,\s*(no_argument|required_argument)\s*,\s*NULL\s*,\s*GETOPT_VAL_[A-Z0-9_]+\s*\}\s*,', re.S) + entry = re.compile(r'\{\s*"([a-z0-9-]+)"\s*,\s*(no_argument|required_argument)\s*,\s*NULL\s*,\s*(?:GETOPT_VAL_[A-Z0-9_]+|\x27[A-Za-z0-9]\x27)\s*\}\s*,', re.S) for match in entry.finditer(table): flag, argument = match.groups() if '--' + flag in result: diff --git a/src/common.h b/src/common.h index 9ad9fc3de..73cccbe2e 100644 --- a/src/common.h +++ b/src/common.h @@ -76,7 +76,10 @@ enum { GETOPT_VAL_TCP_OUTGOING_SNDBUF, GETOPT_VAL_TCP_OUTGOING_RCVBUF, GETOPT_VAL_NFTABLES_SETS, - GETOPT_VAL_SERVER_URL + GETOPT_VAL_SERVER_URL, + GETOPT_VAL_VERSION, + GETOPT_VAL_TCP_ONLY, + GETOPT_VAL_IPV4_FIRST }; #endif // _COMMON_H diff --git a/src/local.c b/src/local.c index 200ef90da..1f664ae95 100644 --- a/src/local.c +++ b/src/local.c @@ -1512,6 +1512,91 @@ main(int argc, char **argv) memset(remote_addr, 0, sizeof(ss_addr_t) * MAX_REMOTE_NUM); +/* [cli_long_config] +\par `--config ` +Read JSON configuration. See the CLI conventions for precedence and default-value behavior. Short alias: `-c`. +[cli_long_config] */ + +/* [cli_long_cipher] +\par `--cipher ` +Select the encryption cipher. See `-m` for supported cipher names and key requirements. Short alias: `-m`. +[cli_long_cipher] */ + +/* [cli_long_timeout] +\par `--timeout ` +Same behavior as `-t`; see that option for details. Short alias: `-t`. +[cli_long_timeout] */ + +/* [cli_long_user] +\par `--user ` +Same behavior as `-a`; see that option for details. Short alias: `-a`. +[cli_long_user] */ + +/* [cli_long_pid_file] +\par `--pid-file ` +Same behavior as `-f`; see that option for details. Short alias: `-f`. +[cli_long_pid_file] */ + +/* [cli_long_nofile] +\par `--nofile ` +Same behavior as `-n`; see that option for details. Short alias: `-n`. +[cli_long_nofile] */ + +/* [cli_long_udp] +\par `--udp` +Enable both TCP and UDP relay, overriding the configured mode. Short alias: `-u`. +[cli_long_udp] */ + +/* [cli_long_udp_only] +\par `--udp-only` +Enable UDP relay only, overriding the configured mode. Short alias: `-U`. +[cli_long_udp_only] */ + +/* [cli_long_ipv6_first] +\par `--ipv6-first` +Prefer IPv6 DNS results; overrides configuration. This does not restrict connections to IPv6. Short alias: `-6`. +[cli_long_ipv6_first] */ + +/* [cli_long_verbose] +\par `--verbose` +Same behavior as `-v`; see that option for details. Short alias: `-v`. +[cli_long_verbose] */ + +/* [cli_long_server] +\par `--server ` +Set a remote Shadowsocks server hostname or IP address; may be repeated. Short alias: `-s`. +[cli_long_server] */ + +/* [cli_long_server_port] +\par `--server-port ` +Set the remote Shadowsocks server port. Short alias: `-p`. +[cli_long_server_port] */ + +/* [cli_long_listen_address] +\par `--listen-address ` +Set the listening address. Server and manager addresses may be repeated. Short alias: `-b`. +[cli_long_listen_address] */ + +/* [cli_long_listen_port] +\par `--listen-port ` +Set the listening port. Short alias: `-l`. +[cli_long_listen_port] */ + +/* [cli_long_interface] +\par `--interface ` +Same behavior as `-i`; see that option for details. Short alias: `-i`. +[cli_long_interface] */ + +/* [cli_long_vpn] +\par `--vpn` +Same behavior as `-V`; see that option for details. Short alias: `-V`. +[cli_long_vpn] */ + +/* [cli_long_stat_path] +\par `--stat-path ` +Same behavior as `-S`; see that option for details. Short alias: `-S`. +[cli_long_stat_path] */ + /* [cli-options] \snippet{doc} utils.c cli_short_f \snippet{doc} utils.c cli_short_s @@ -1533,6 +1618,26 @@ main(int argc, char **argv) \snippet{doc} utils.c cli_short_V \snippet{doc} utils.c cli_short_6 \snippet{doc} utils.c cli_short_A +\snippet{doc} local.c cli_long_config +\snippet{doc} local.c cli_long_cipher +\snippet{doc} local.c cli_long_timeout +\snippet{doc} local.c cli_long_user +\snippet{doc} local.c cli_long_pid_file +\snippet{doc} local.c cli_long_nofile +\snippet{doc} local.c cli_long_udp +\snippet{doc} local.c cli_long_udp_only +\snippet{doc} local.c cli_long_ipv6_first +\snippet{doc} local.c cli_long_verbose +\snippet{doc} local.c cli_long_server +\snippet{doc} local.c cli_long_server_port +\snippet{doc} local.c cli_long_listen_address +\snippet{doc} local.c cli_long_listen_port +\snippet{doc} local.c cli_long_interface +\snippet{doc} utils.c cli_long_version +\snippet{doc} utils.c cli_long_tcp_only +\snippet{doc} utils.c cli_long_ipv4_first +\snippet{doc} local.c cli_long_vpn +\snippet{doc} local.c cli_long_stat_path \snippet{doc} utils.c cli_long_reuse_port \snippet{doc} utils.c cli_long_tcp_incoming_sndbuf \snippet{doc} utils.c cli_long_tcp_incoming_rcvbuf @@ -1551,6 +1656,28 @@ main(int argc, char **argv) \snippet{doc} utils.c cli_long_help [cli-options] */ static struct option long_options[] = { + { "config", required_argument, NULL, 'c' }, + { "cipher", required_argument, NULL, 'm' }, + { "timeout", required_argument, NULL, 't' }, + { "user", required_argument, NULL, 'a' }, + { "pid-file", required_argument, NULL, 'f' }, + { "nofile", required_argument, NULL, 'n' }, + { "udp", no_argument, NULL, 'u' }, + { "udp-only", no_argument, NULL, 'U' }, + { "ipv6-first", no_argument, NULL, '6' }, + { "verbose", no_argument, NULL, 'v' }, + { "server", required_argument, NULL, 's' }, + { "server-port", required_argument, NULL, 'p' }, + { "listen-address", required_argument, NULL, 'b' }, + { "listen-port", required_argument, NULL, 'l' }, + { "interface", required_argument, NULL, 'i' }, + { "version", no_argument, NULL, GETOPT_VAL_VERSION }, + { "tcp-only", no_argument, NULL, GETOPT_VAL_TCP_ONLY }, + { "ipv4-first", no_argument, NULL, GETOPT_VAL_IPV4_FIRST }, +#ifdef __ANDROID__ + { "vpn", no_argument, NULL, 'V' }, + { "stat-path", required_argument, NULL, 'S' }, +#endif { "reuse-port", no_argument, NULL, GETOPT_VAL_REUSE_PORT }, { "tcp-incoming-sndbuf", required_argument, NULL, GETOPT_VAL_TCP_INCOMING_SNDBUF }, { "tcp-incoming-rcvbuf", required_argument, NULL, GETOPT_VAL_TCP_INCOMING_RCVBUF }, @@ -1570,18 +1697,31 @@ main(int argc, char **argv) { NULL, 0, NULL, 0 } }; + int mode_set = 0; + int ipv6first_set = 0; opterr = 0; USE_TTY(); #ifdef __ANDROID__ - while ((c = getopt_long(argc, argv, "f:s:p:l:k:t:m:i:c:b:a:n:S:huUvV6A", + while ((c = getopt_long(argc, argv, ":f:s:p:l:k:t:m:i:c:b:a:n:S:huUvV6A", long_options, NULL)) != -1) { #else - while ((c = getopt_long(argc, argv, "f:s:p:l:k:t:m:i:c:b:a:n:huUv6A", + while ((c = getopt_long(argc, argv, ":f:s:p:l:k:t:m:i:c:b:a:n:huUv6A", long_options, NULL)) != -1) { #endif switch (c) { + case GETOPT_VAL_VERSION: + cli_version(); + exit(EXIT_SUCCESS); + case GETOPT_VAL_TCP_ONLY: + mode = TCP_ONLY; + mode_set = 1; + break; + case GETOPT_VAL_IPV4_FIRST: + ipv6first = 0; + ipv6first_set = 1; + break; case GETOPT_VAL_FAST_OPEN: fast_open = 1; break; @@ -1591,7 +1731,7 @@ main(int argc, char **argv) break; case GETOPT_VAL_MTU: if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { - FATAL("invalid MTU"); + cli_error("invalid MTU", c, NULL); } LOGI("set MTU to %d", mtu); break; @@ -1620,7 +1760,7 @@ main(int argc, char **argv) * since they are applied as getopt reaches them. */ if (ss_url_parse(optarg, &server_url) != 0) { - FATAL("invalid ss:// server URL"); + cli_error("invalid ss:// server URL", c, NULL); } if (remote_num < MAX_REMOTE_NUM) { remote_addr[remote_num].host = server_url.host; @@ -1642,22 +1782,22 @@ main(int argc, char **argv) break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { - FATAL("invalid TCP incoming send buffer size"); + cli_error("invalid TCP incoming send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { - FATAL("invalid TCP incoming receive buffer size"); + cli_error("invalid TCP incoming receive buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { - FATAL("invalid TCP outgoing send buffer size"); + cli_error("invalid TCP outgoing send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { - FATAL("invalid TCP outgoing receive buffer size"); + cli_error("invalid TCP outgoing receive buffer size", c, NULL); } break; case 's': @@ -1666,9 +1806,19 @@ main(int argc, char **argv) } break; case 'p': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } remote_port = optarg; break; case 'l': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } local_port = optarg; break; case GETOPT_VAL_PASSWORD: @@ -1680,6 +1830,11 @@ main(int argc, char **argv) pid_path = optarg; break; case 't': + { + int checked_timeout; + if (ss_parse_int(optarg, 1, INT_MAX, &checked_timeout) != 0) + cli_error("timeout must be a positive integer", c, NULL); + } timeout = optarg; break; case 'm': @@ -1700,14 +1855,16 @@ main(int argc, char **argv) #ifdef HAVE_SETRLIMIT case 'n': if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { - FATAL("invalid nofile"); + cli_error("invalid nofile", c, NULL); } break; #endif case 'u': + mode_set = 1; mode = TCP_AND_UDP; break; case 'U': + mode_set = 1; mode = UDP_ONLY; break; case 'v': @@ -1718,6 +1875,7 @@ main(int argc, char **argv) usage(); exit(EXIT_SUCCESS); case '6': + ipv6first_set = 1; ipv6first = 1; break; #ifdef __ANDROID__ @@ -1729,19 +1887,22 @@ main(int argc, char **argv) break; #endif case 'A': - FATAL("One time auth has been deprecated. Try AEAD ciphers instead."); + cli_error("one-time authentication was removed; use an AEAD cipher", 'A', NULL); + break; + case ':': + cli_error("missing required argument for option", optopt, argv[optind - 1]); break; case '?': - // The option character is not recognized. - LOGE("Unrecognized option: %s", optarg); - opterr = 1; + cli_error("unrecognized or invalid option", optopt, argv[optind - 1]); + break; + default: + cli_error("option is unsupported on this platform", c, NULL); break; } } - if (opterr) { - usage(); - exit(EXIT_FAILURE); + if (optind < argc) { + cli_error("unexpected positional argument", 0, NULL); } if (argc == 1) { @@ -1804,7 +1965,7 @@ main(int argc, char **argv) if (fast_open == 0) { fast_open = conf->fast_open; } - if (mode == TCP_ONLY) { + if (!mode_set) { mode = conf->mode; } if (mtu == 0) { @@ -1821,7 +1982,7 @@ main(int argc, char **argv) nofile = conf->nofile; } #endif - if (ipv6first == 0) { + if (!ipv6first_set) { ipv6first = conf->ipv6_first; } if (acl == 0 && conf->acl != NULL) { diff --git a/src/manager.c b/src/manager.c index 5517ccdb1..d5b799c58 100644 --- a/src/manager.c +++ b/src/manager.c @@ -1276,20 +1276,79 @@ main(int argc, char **argv) Set a server listening hostname or IP address. May be repeated. [cli_short_s] */ -/* [cli_short_l] -\par `-l ` -Accepted for compatibility but ignored by this program; it does not configure a local listener. -[cli_short_l] */ - /* [cli_short_c] \par `-c ` Use a JSON configuration file. The "port_password" field can start multiple ss-server instances. [cli_short_c] */ +/* [cli_long_config] +\par `--config ` +Read JSON configuration. See the CLI conventions for precedence and default-value behavior. Short alias: `-c`. +[cli_long_config] */ + +/* [cli_long_cipher] +\par `--cipher ` +Select the encryption cipher. See `-m` for supported cipher names and key requirements. Short alias: `-m`. +[cli_long_cipher] */ + +/* [cli_long_timeout] +\par `--timeout ` +Same behavior as `-t`; see that option for details. Short alias: `-t`. +[cli_long_timeout] */ + +/* [cli_long_user] +\par `--user ` +Same behavior as `-a`; see that option for details. Short alias: `-a`. +[cli_long_user] */ + +/* [cli_long_pid_file] +\par `--pid-file ` +Same behavior as `-f`; see that option for details. Short alias: `-f`. +[cli_long_pid_file] */ + +/* [cli_long_nofile] +\par `--nofile ` +Same behavior as `-n`; see that option for details. Short alias: `-n`. +[cli_long_nofile] */ + +/* [cli_long_udp] +\par `--udp` +Enable both TCP and UDP relay, overriding the configured mode. Short alias: `-u`. +[cli_long_udp] */ + +/* [cli_long_udp_only] +\par `--udp-only` +Enable UDP relay only, overriding the configured mode. Short alias: `-U`. +[cli_long_udp_only] */ + +/* [cli_long_ipv6_first] +\par `--ipv6-first` +Prefer IPv6 DNS results; overrides configuration. This does not restrict connections to IPv6. Short alias: `-6`. +[cli_long_ipv6_first] */ + +/* [cli_long_verbose] +\par `--verbose` +Same behavior as `-v`; see that option for details. Short alias: `-v`. +[cli_long_verbose] */ + +/* [cli_long_listen_address] +\par `--listen-address ` +Set the listening address. Server and manager addresses may be repeated. Short alias: `-s`. +[cli_long_listen_address] */ + +/* [cli_long_interface] +\par `--interface ` +Same behavior as `-i`; see that option for details. Short alias: `-i`. +[cli_long_interface] */ + +/* [cli_long_nameserver] +\par `--nameserver ` +Same behavior as `-d`; see that option for details. Short alias: `-d`. +[cli_long_nameserver] */ + /* [cli-options] \snippet{doc} utils.c cli_short_f \snippet{doc} manager.c cli_short_s -\snippet{doc} manager.c cli_short_l \snippet{doc} utils.c cli_short_k \snippet{doc} utils.c cli_short_t \snippet{doc} utils.c cli_short_m @@ -1305,6 +1364,22 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s \snippet{doc} utils.c cli_short_U \snippet{doc} utils.c cli_short_v \snippet{doc} utils.c cli_short_A +\snippet{doc} manager.c cli_long_config +\snippet{doc} manager.c cli_long_cipher +\snippet{doc} manager.c cli_long_timeout +\snippet{doc} manager.c cli_long_user +\snippet{doc} manager.c cli_long_pid_file +\snippet{doc} manager.c cli_long_nofile +\snippet{doc} manager.c cli_long_udp +\snippet{doc} manager.c cli_long_udp_only +\snippet{doc} manager.c cli_long_ipv6_first +\snippet{doc} manager.c cli_long_verbose +\snippet{doc} manager.c cli_long_listen_address +\snippet{doc} manager.c cli_long_interface +\snippet{doc} manager.c cli_long_nameserver +\snippet{doc} utils.c cli_long_version +\snippet{doc} utils.c cli_long_tcp_only +\snippet{doc} utils.c cli_long_ipv4_first \snippet{doc} utils.c cli_long_fast_open \snippet{doc} utils.c cli_long_no_delay \snippet{doc} utils.c cli_long_reuse_port @@ -1319,6 +1394,22 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s \snippet{doc} utils.c cli_long_help [cli-options] */ static struct option long_options[] = { + { "config", required_argument, NULL, 'c' }, + { "cipher", required_argument, NULL, 'm' }, + { "timeout", required_argument, NULL, 't' }, + { "user", required_argument, NULL, 'a' }, + { "pid-file", required_argument, NULL, 'f' }, + { "nofile", required_argument, NULL, 'n' }, + { "udp", no_argument, NULL, 'u' }, + { "udp-only", no_argument, NULL, 'U' }, + { "ipv6-first", no_argument, NULL, '6' }, + { "verbose", no_argument, NULL, 'v' }, + { "listen-address", required_argument, NULL, 's' }, + { "interface", required_argument, NULL, 'i' }, + { "nameserver", required_argument, NULL, 'd' }, + { "version", no_argument, NULL, GETOPT_VAL_VERSION }, + { "tcp-only", no_argument, NULL, GETOPT_VAL_TCP_ONLY }, + { "ipv4-first", no_argument, NULL, GETOPT_VAL_IPV4_FIRST }, { "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN }, { "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY }, { "reuse-port", no_argument, NULL, GETOPT_VAL_REUSE_PORT }, @@ -1336,13 +1427,26 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s { NULL, 0, NULL, 0 } }; + int mode_set = 0; + int ipv6first_set = 0; opterr = 0; USE_TTY(); - while ((c = getopt_long(argc, argv, "f:s:l:k:t:m:c:i:d:a:n:D:6huUvA", + while ((c = getopt_long(argc, argv, ":f:s:k:t:m:c:i:d:a:n:D:6huUvA", long_options, NULL)) != -1) switch (c) { + case GETOPT_VAL_VERSION: + cli_version(); + exit(EXIT_SUCCESS); + case GETOPT_VAL_TCP_ONLY: + mode = TCP_ONLY; + mode_set = 1; + break; + case GETOPT_VAL_IPV4_FIRST: + ipv6first = 0; + ipv6first_set = 1; + break; case GETOPT_VAL_REUSE_PORT: reuse_port = 1; break; @@ -1363,7 +1467,7 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s break; case GETOPT_VAL_MTU: if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { - FATAL("invalid MTU"); + cli_error("invalid MTU", c, NULL); } break; case GETOPT_VAL_PLUGIN: @@ -1386,6 +1490,11 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s pid_path = optarg; break; case 't': + { + int checked_timeout; + if (ss_parse_int(optarg, 1, INT_MAX, &checked_timeout) != 0) + cli_error("timeout must be a positive integer", c, NULL); + } timeout = optarg; break; case 'm': @@ -1404,12 +1513,15 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s user = optarg; break; case 'u': + mode_set = 1; mode = TCP_AND_UDP; break; case 'U': + mode_set = 1; mode = UDP_ONLY; break; case '6': + ipv6first_set = 1; ipv6first = 1; break; case GETOPT_VAL_WORKDIR: @@ -1426,23 +1538,26 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s #ifdef HAVE_SETRLIMIT case 'n': if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { - FATAL("invalid nofile"); + cli_error("invalid nofile", c, NULL); } break; #endif case 'A': - FATAL("One time auth has been deprecated. Try AEAD ciphers instead."); + cli_error("one-time authentication was removed; use an AEAD cipher", 'A', NULL); + break; + case ':': + cli_error("missing required argument for option", optopt, argv[optind - 1]); break; case '?': - // The option character is not recognized. - LOGE("Unrecognized option: %s", optarg); - opterr = 1; + cli_error("unrecognized or invalid option", optopt, argv[optind - 1]); + break; + default: + cli_error("option is unsupported on this platform", c, NULL); break; } - if (opterr) { - usage(); - exit(EXIT_FAILURE); + if (optind < argc) { + cli_error("unexpected positional argument", 0, NULL); } if (conf_path != NULL) { @@ -1476,7 +1591,7 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s if (nameservers == NULL) { nameservers = conf->nameserver; } - if (mode == TCP_ONLY) { + if (!mode_set) { mode = conf->mode; } if (mtu == 0) { @@ -1488,7 +1603,7 @@ Use a JSON configuration file. The "port_password" field can start multiple ss-s if (plugin_opts == NULL) { plugin_opts = conf->plugin_opts; } - if (ipv6first == 0) { + if (!ipv6first_set) { ipv6first = conf->ipv6_first; } if (workdir == NULL) { diff --git a/src/redir.c b/src/redir.c index 71e50649e..89f3f872a 100644 --- a/src/redir.c +++ b/src/redir.c @@ -927,6 +927,81 @@ main(int argc, char **argv) Enable UDP relay. Requires Linux TPROXY support and permission to configure transparent proxying. [cli_short_u] */ +/* [cli_long_config] +\par `--config ` +Read JSON configuration. See the CLI conventions for precedence and default-value behavior. Short alias: `-c`. +[cli_long_config] */ + +/* [cli_long_cipher] +\par `--cipher ` +Select the encryption cipher. See `-m` for supported cipher names and key requirements. Short alias: `-m`. +[cli_long_cipher] */ + +/* [cli_long_timeout] +\par `--timeout ` +Same behavior as `-t`; see that option for details. Short alias: `-t`. +[cli_long_timeout] */ + +/* [cli_long_user] +\par `--user ` +Same behavior as `-a`; see that option for details. Short alias: `-a`. +[cli_long_user] */ + +/* [cli_long_pid_file] +\par `--pid-file ` +Same behavior as `-f`; see that option for details. Short alias: `-f`. +[cli_long_pid_file] */ + +/* [cli_long_nofile] +\par `--nofile ` +Same behavior as `-n`; see that option for details. Short alias: `-n`. +[cli_long_nofile] */ + +/* [cli_long_udp] +\par `--udp` +Enable both TCP and UDP relay, overriding the configured mode. Short alias: `-u`. +[cli_long_udp] */ + +/* [cli_long_udp_only] +\par `--udp-only` +Enable UDP relay only, overriding the configured mode. Short alias: `-U`. +[cli_long_udp_only] */ + +/* [cli_long_ipv6_first] +\par `--ipv6-first` +Prefer IPv6 DNS results; overrides configuration. This does not restrict connections to IPv6. Short alias: `-6`. +[cli_long_ipv6_first] */ + +/* [cli_long_verbose] +\par `--verbose` +Same behavior as `-v`; see that option for details. Short alias: `-v`. +[cli_long_verbose] */ + +/* [cli_long_server] +\par `--server ` +Set a remote Shadowsocks server hostname or IP address; may be repeated. Short alias: `-s`. +[cli_long_server] */ + +/* [cli_long_server_port] +\par `--server-port ` +Set the remote Shadowsocks server port. Short alias: `-p`. +[cli_long_server_port] */ + +/* [cli_long_listen_address] +\par `--listen-address ` +Set the listening address. Server and manager addresses may be repeated. Short alias: `-b`. +[cli_long_listen_address] */ + +/* [cli_long_listen_port] +\par `--listen-port ` +Set the listening port. Short alias: `-l`. +[cli_long_listen_port] */ + +/* [cli_long_tproxy] +\par `--tproxy` +Same behavior as `-T`; see that option for details. Short alias: `-T`. +[cli_long_tproxy] */ + /* [cli-options] \snippet{doc} utils.c cli_short_f \snippet{doc} utils.c cli_short_s @@ -946,6 +1021,24 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran \snippet{doc} utils.c cli_short_v \snippet{doc} utils.c cli_short_6 \snippet{doc} utils.c cli_short_A +\snippet{doc} redir.c cli_long_config +\snippet{doc} redir.c cli_long_cipher +\snippet{doc} redir.c cli_long_timeout +\snippet{doc} redir.c cli_long_user +\snippet{doc} redir.c cli_long_pid_file +\snippet{doc} redir.c cli_long_nofile +\snippet{doc} redir.c cli_long_udp +\snippet{doc} redir.c cli_long_udp_only +\snippet{doc} redir.c cli_long_ipv6_first +\snippet{doc} redir.c cli_long_verbose +\snippet{doc} redir.c cli_long_server +\snippet{doc} redir.c cli_long_server_port +\snippet{doc} redir.c cli_long_listen_address +\snippet{doc} redir.c cli_long_listen_port +\snippet{doc} redir.c cli_long_tproxy +\snippet{doc} utils.c cli_long_version +\snippet{doc} utils.c cli_long_tcp_only +\snippet{doc} utils.c cli_long_ipv4_first \snippet{doc} utils.c cli_long_fast_open \snippet{doc} utils.c cli_long_mtu \snippet{doc} utils.c cli_long_mptcp @@ -962,6 +1055,24 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran \snippet{doc} utils.c cli_long_help [cli-options] */ static struct option long_options[] = { + { "config", required_argument, NULL, 'c' }, + { "cipher", required_argument, NULL, 'm' }, + { "timeout", required_argument, NULL, 't' }, + { "user", required_argument, NULL, 'a' }, + { "pid-file", required_argument, NULL, 'f' }, + { "nofile", required_argument, NULL, 'n' }, + { "udp", no_argument, NULL, 'u' }, + { "udp-only", no_argument, NULL, 'U' }, + { "ipv6-first", no_argument, NULL, '6' }, + { "verbose", no_argument, NULL, 'v' }, + { "server", required_argument, NULL, 's' }, + { "server-port", required_argument, NULL, 'p' }, + { "listen-address", required_argument, NULL, 'b' }, + { "listen-port", required_argument, NULL, 'l' }, + { "tproxy", no_argument, NULL, 'T' }, + { "version", no_argument, NULL, GETOPT_VAL_VERSION }, + { "tcp-only", no_argument, NULL, GETOPT_VAL_TCP_ONLY }, + { "ipv4-first", no_argument, NULL, GETOPT_VAL_IPV4_FIRST }, { "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN }, { "mtu", required_argument, NULL, GETOPT_VAL_MTU }, { "mptcp", no_argument, NULL, GETOPT_VAL_MPTCP }, @@ -979,19 +1090,32 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran { NULL, 0, NULL, 0 } }; + int mode_set = 0; + int ipv6first_set = 0; opterr = 0; USE_TTY(); - while ((c = getopt_long(argc, argv, "f:s:p:l:k:t:m:c:b:a:n:huUTv6A", + while ((c = getopt_long(argc, argv, ":f:s:p:l:k:t:m:c:b:a:n:huUTv6A", long_options, NULL)) != -1) { switch (c) { + case GETOPT_VAL_VERSION: + cli_version(); + exit(EXIT_SUCCESS); + case GETOPT_VAL_TCP_ONLY: + mode = TCP_ONLY; + mode_set = 1; + break; + case GETOPT_VAL_IPV4_FIRST: + ipv6first = 0; + ipv6first_set = 1; + break; case GETOPT_VAL_FAST_OPEN: fast_open = 1; break; case GETOPT_VAL_MTU: if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { - FATAL("invalid MTU"); + cli_error("invalid MTU", c, NULL); } LOGI("set MTU to %d", mtu); break; @@ -1018,22 +1142,22 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { - FATAL("invalid TCP incoming send buffer size"); + cli_error("invalid TCP incoming send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { - FATAL("invalid TCP incoming receive buffer size"); + cli_error("invalid TCP incoming receive buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { - FATAL("invalid TCP outgoing send buffer size"); + cli_error("invalid TCP outgoing send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { - FATAL("invalid TCP outgoing receive buffer size"); + cli_error("invalid TCP outgoing receive buffer size", c, NULL); } break; case 's': @@ -1042,9 +1166,19 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran } break; case 'p': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } remote_port = optarg; break; case 'l': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } local_port = optarg; break; case GETOPT_VAL_PASSWORD: @@ -1056,6 +1190,11 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran pid_path = optarg; break; case 't': + { + int checked_timeout; + if (ss_parse_int(optarg, 1, INT_MAX, &checked_timeout) != 0) + cli_error("timeout must be a positive integer", c, NULL); + } timeout = optarg; break; case 'm': @@ -1073,14 +1212,16 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran #ifdef HAVE_SETRLIMIT case 'n': if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { - FATAL("invalid nofile"); + cli_error("invalid nofile", c, NULL); } break; #endif case 'u': + mode_set = 1; mode = TCP_AND_UDP; break; case 'U': + mode_set = 1; mode = UDP_ONLY; break; case 'T': @@ -1094,22 +1235,26 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran usage(); exit(EXIT_SUCCESS); case '6': + ipv6first_set = 1; ipv6first = 1; break; case 'A': - FATAL("One time auth has been deprecated. Try AEAD ciphers instead."); + cli_error("one-time authentication was removed; use an AEAD cipher", 'A', NULL); + break; + case ':': + cli_error("missing required argument for option", optopt, argv[optind - 1]); break; case '?': - // The option character is not recognized. - LOGE("Unrecognized option: %s", optarg); - opterr = 1; + cli_error("unrecognized or invalid option", optopt, argv[optind - 1]); + break; + default: + cli_error("option is unsupported on this platform", c, NULL); break; } } - if (opterr) { - usage(); - exit(EXIT_FAILURE); + if (optind < argc) { + cli_error("unexpected positional argument", 0, NULL); } if (argc == 1) { @@ -1155,7 +1300,7 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran if (plugin_opts == NULL) { plugin_opts = conf->plugin_opts; } - if (mode == TCP_ONLY) { + if (!mode_set) { mode = conf->mode; } if (tcp_tproxy == 0) { @@ -1193,7 +1338,7 @@ Enable UDP relay. Requires Linux TPROXY support and permission to configure tran nofile = conf->nofile; } #endif - if (ipv6first == 0) { + if (!ipv6first_set) { ipv6first = conf->ipv6_first; } dscp_num = conf->dscp_num; diff --git a/src/server.c b/src/server.c index 550bda116..e75e1bdc2 100644 --- a/src/server.c +++ b/src/server.c @@ -1848,21 +1848,90 @@ Set a server listening hostname or IP address. May be repeated. Set the server listening port. [cli_short_p] */ -/* [cli_short_l] -\par `-l ` -Accepted for compatibility but ignored by this program; it does not configure a local listener. -[cli_short_l] */ - /* [cli_short_b] \par `-b ` Set the local address for outbound connections to destination servers. [cli_short_b] */ +/* [cli_long_config] +\par `--config ` +Read JSON configuration. See the CLI conventions for precedence and default-value behavior. Short alias: `-c`. +[cli_long_config] */ + +/* [cli_long_cipher] +\par `--cipher ` +Select the encryption cipher. See `-m` for supported cipher names and key requirements. Short alias: `-m`. +[cli_long_cipher] */ + +/* [cli_long_timeout] +\par `--timeout ` +Same behavior as `-t`; see that option for details. Short alias: `-t`. +[cli_long_timeout] */ + +/* [cli_long_user] +\par `--user ` +Same behavior as `-a`; see that option for details. Short alias: `-a`. +[cli_long_user] */ + +/* [cli_long_pid_file] +\par `--pid-file ` +Same behavior as `-f`; see that option for details. Short alias: `-f`. +[cli_long_pid_file] */ + +/* [cli_long_nofile] +\par `--nofile ` +Same behavior as `-n`; see that option for details. Short alias: `-n`. +[cli_long_nofile] */ + +/* [cli_long_udp] +\par `--udp` +Enable both TCP and UDP relay, overriding the configured mode. Short alias: `-u`. +[cli_long_udp] */ + +/* [cli_long_udp_only] +\par `--udp-only` +Enable UDP relay only, overriding the configured mode. Short alias: `-U`. +[cli_long_udp_only] */ + +/* [cli_long_ipv6_first] +\par `--ipv6-first` +Prefer IPv6 DNS results; overrides configuration. This does not restrict connections to IPv6. Short alias: `-6`. +[cli_long_ipv6_first] */ + +/* [cli_long_verbose] +\par `--verbose` +Same behavior as `-v`; see that option for details. Short alias: `-v`. +[cli_long_verbose] */ + +/* [cli_long_listen_address] +\par `--listen-address ` +Set the listening address. Server and manager addresses may be repeated. Short alias: `-s`. +[cli_long_listen_address] */ + +/* [cli_long_listen_port] +\par `--listen-port ` +Set the listening port. Short alias: `-p`. +[cli_long_listen_port] */ + +/* [cli_long_outbound_address] +\par `--outbound-address ` +Set the source address for connections to destination servers. Short alias: `-b`. +[cli_long_outbound_address] */ + +/* [cli_long_interface] +\par `--interface ` +Same behavior as `-i`; see that option for details. Short alias: `-i`. +[cli_long_interface] */ + +/* [cli_long_nameserver] +\par `--nameserver ` +Same behavior as `-d`; see that option for details. Short alias: `-d`. +[cli_long_nameserver] */ + /* [cli-options] \snippet{doc} utils.c cli_short_f \snippet{doc} server.c cli_short_s \snippet{doc} server.c cli_short_p -\snippet{doc} server.c cli_short_l \snippet{doc} utils.c cli_short_k \snippet{doc} utils.c cli_short_t \snippet{doc} utils.c cli_short_m @@ -1878,6 +1947,24 @@ Set the local address for outbound connections to destination servers. \snippet{doc} utils.c cli_short_v \snippet{doc} utils.c cli_short_6 \snippet{doc} utils.c cli_short_A +\snippet{doc} server.c cli_long_config +\snippet{doc} server.c cli_long_cipher +\snippet{doc} server.c cli_long_timeout +\snippet{doc} server.c cli_long_user +\snippet{doc} server.c cli_long_pid_file +\snippet{doc} server.c cli_long_nofile +\snippet{doc} server.c cli_long_udp +\snippet{doc} server.c cli_long_udp_only +\snippet{doc} server.c cli_long_ipv6_first +\snippet{doc} server.c cli_long_verbose +\snippet{doc} server.c cli_long_listen_address +\snippet{doc} server.c cli_long_listen_port +\snippet{doc} server.c cli_long_outbound_address +\snippet{doc} server.c cli_long_interface +\snippet{doc} server.c cli_long_nameserver +\snippet{doc} utils.c cli_long_version +\snippet{doc} utils.c cli_long_tcp_only +\snippet{doc} utils.c cli_long_ipv4_first \snippet{doc} utils.c cli_long_fast_open \snippet{doc} utils.c cli_long_reuse_port \snippet{doc} utils.c cli_long_tcp_incoming_sndbuf @@ -1897,6 +1984,24 @@ Set the local address for outbound connections to destination servers. \snippet{doc} utils.c cli_long_nftables_sets [cli-options] */ static struct option long_options[] = { + { "config", required_argument, NULL, 'c' }, + { "cipher", required_argument, NULL, 'm' }, + { "timeout", required_argument, NULL, 't' }, + { "user", required_argument, NULL, 'a' }, + { "pid-file", required_argument, NULL, 'f' }, + { "nofile", required_argument, NULL, 'n' }, + { "udp", no_argument, NULL, 'u' }, + { "udp-only", no_argument, NULL, 'U' }, + { "ipv6-first", no_argument, NULL, '6' }, + { "verbose", no_argument, NULL, 'v' }, + { "listen-address", required_argument, NULL, 's' }, + { "listen-port", required_argument, NULL, 'p' }, + { "outbound-address", required_argument, NULL, 'b' }, + { "interface", required_argument, NULL, 'i' }, + { "nameserver", required_argument, NULL, 'd' }, + { "version", no_argument, NULL, GETOPT_VAL_VERSION }, + { "tcp-only", no_argument, NULL, GETOPT_VAL_TCP_ONLY }, + { "ipv4-first", no_argument, NULL, GETOPT_VAL_IPV4_FIRST }, { "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN }, { "reuse-port", no_argument, NULL, GETOPT_VAL_REUSE_PORT }, { "tcp-incoming-sndbuf", required_argument, NULL, GETOPT_VAL_TCP_INCOMING_SNDBUF }, @@ -1922,13 +2027,26 @@ Set the local address for outbound connections to destination servers. { NULL, 0, NULL, 0 } }; + int mode_set = 0; + int ipv6first_set = 0; opterr = 0; USE_TTY(); - while ((c = getopt_long(argc, argv, "f:s:p:l:k:t:m:b:c:i:d:a:n:huUv6A", + while ((c = getopt_long(argc, argv, ":f:s:p:k:t:m:b:c:i:d:a:n:huUv6A", long_options, NULL)) != -1) { switch (c) { + case GETOPT_VAL_VERSION: + cli_version(); + exit(EXIT_SUCCESS); + case GETOPT_VAL_TCP_ONLY: + mode = TCP_ONLY; + mode_set = 1; + break; + case GETOPT_VAL_IPV4_FIRST: + ipv6first = 0; + ipv6first_set = 1; + break; case GETOPT_VAL_FAST_OPEN: fast_open = 1; break; @@ -1945,7 +2063,7 @@ Set the local address for outbound connections to destination servers. break; case GETOPT_VAL_MTU: if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { - FATAL("invalid MTU"); + cli_error("invalid MTU", c, NULL); } LOGI("set MTU to %d", mtu); break; @@ -1968,22 +2086,22 @@ Set the local address for outbound connections to destination servers. break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { - FATAL("invalid TCP incoming send buffer size"); + cli_error("invalid TCP incoming send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { - FATAL("invalid TCP incoming receive buffer size"); + cli_error("invalid TCP incoming receive buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { - FATAL("invalid TCP outgoing send buffer size"); + cli_error("invalid TCP outgoing send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { - FATAL("invalid TCP outgoing receive buffer size"); + cli_error("invalid TCP outgoing receive buffer size", c, NULL); } break; #ifdef USE_NFTABLES @@ -2000,6 +2118,11 @@ Set the local address for outbound connections to destination servers. is_bind_local_addr += parse_local_addr(&local_addr_v4, &local_addr_v6, optarg); break; case 'p': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } server_port = optarg; break; case GETOPT_VAL_PASSWORD: @@ -2011,6 +2134,11 @@ Set the local address for outbound connections to destination servers. pid_path = optarg; break; case 't': + { + int checked_timeout; + if (ss_parse_int(optarg, 1, INT_MAX, &checked_timeout) != 0) + cli_error("timeout must be a positive integer", c, NULL); + } timeout = optarg; break; case 'm': @@ -2031,14 +2159,16 @@ Set the local address for outbound connections to destination servers. #ifdef HAVE_SETRLIMIT case 'n': if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { - FATAL("invalid nofile"); + cli_error("invalid nofile", c, NULL); } break; #endif case 'u': + mode_set = 1; mode = TCP_AND_UDP; break; case 'U': + mode_set = 1; mode = UDP_ONLY; break; case 'v': @@ -2049,22 +2179,26 @@ Set the local address for outbound connections to destination servers. usage(); exit(EXIT_SUCCESS); case '6': + ipv6first_set = 1; ipv6first = 1; break; case 'A': - FATAL("One time auth has been deprecated. Try AEAD ciphers instead."); + cli_error("one-time authentication was removed; use an AEAD cipher", 'A', NULL); + break; + case ':': + cli_error("missing required argument for option", optopt, argv[optind - 1]); break; case '?': - // The option character is not recognized. - LOGE("Unrecognized option: %s", optarg); - opterr = 1; + cli_error("unrecognized or invalid option", optopt, argv[optind - 1]); + break; + default: + cli_error("option is unsupported on this platform", c, NULL); break; } } - if (opterr) { - usage(); - exit(EXIT_FAILURE); + if (optind < argc) { + cli_error("unexpected positional argument", 0, NULL); } if (argc == 1) { @@ -2104,7 +2238,7 @@ Set the local address for outbound connections to destination servers. if (plugin_opts == NULL) { plugin_opts = conf->plugin_opts; } - if (mode == TCP_ONLY) { + if (!mode_set) { mode = conf->mode; } if (mtu == 0) { @@ -2149,7 +2283,7 @@ Set the local address for outbound connections to destination servers. if (nameservers == NULL) { nameservers = conf->nameserver; } - if (ipv6first == 0) { + if (!ipv6first_set) { ipv6first = conf->ipv6_first; } if (acl == 0 && conf->acl != NULL) { diff --git a/src/tunnel.c b/src/tunnel.c index 362b09fc6..bef217a6f 100644 --- a/src/tunnel.c +++ b/src/tunnel.c @@ -957,6 +957,91 @@ main(int argc, char **argv) memset(remote_addr, 0, sizeof(ss_addr_t) * MAX_REMOTE_NUM); +/* [cli_long_config] +\par `--config ` +Read JSON configuration. See the CLI conventions for precedence and default-value behavior. Short alias: `-c`. +[cli_long_config] */ + +/* [cli_long_cipher] +\par `--cipher ` +Select the encryption cipher. See `-m` for supported cipher names and key requirements. Short alias: `-m`. +[cli_long_cipher] */ + +/* [cli_long_timeout] +\par `--timeout ` +Same behavior as `-t`; see that option for details. Short alias: `-t`. +[cli_long_timeout] */ + +/* [cli_long_user] +\par `--user ` +Same behavior as `-a`; see that option for details. Short alias: `-a`. +[cli_long_user] */ + +/* [cli_long_pid_file] +\par `--pid-file ` +Same behavior as `-f`; see that option for details. Short alias: `-f`. +[cli_long_pid_file] */ + +/* [cli_long_nofile] +\par `--nofile ` +Same behavior as `-n`; see that option for details. Short alias: `-n`. +[cli_long_nofile] */ + +/* [cli_long_udp] +\par `--udp` +Enable both TCP and UDP relay, overriding the configured mode. Short alias: `-u`. +[cli_long_udp] */ + +/* [cli_long_udp_only] +\par `--udp-only` +Enable UDP relay only, overriding the configured mode. Short alias: `-U`. +[cli_long_udp_only] */ + +/* [cli_long_ipv6_first] +\par `--ipv6-first` +Prefer IPv6 DNS results; overrides configuration. This does not restrict connections to IPv6. Short alias: `-6`. +[cli_long_ipv6_first] */ + +/* [cli_long_verbose] +\par `--verbose` +Same behavior as `-v`; see that option for details. Short alias: `-v`. +[cli_long_verbose] */ + +/* [cli_long_server] +\par `--server ` +Set a remote Shadowsocks server hostname or IP address; may be repeated. Short alias: `-s`. +[cli_long_server] */ + +/* [cli_long_server_port] +\par `--server-port ` +Set the remote Shadowsocks server port. Short alias: `-p`. +[cli_long_server_port] */ + +/* [cli_long_listen_address] +\par `--listen-address ` +Set the listening address. Server and manager addresses may be repeated. Short alias: `-b`. +[cli_long_listen_address] */ + +/* [cli_long_listen_port] +\par `--listen-port ` +Set the listening port. Short alias: `-l`. +[cli_long_listen_port] */ + +/* [cli_long_interface] +\par `--interface ` +Same behavior as `-i`; see that option for details. Short alias: `-i`. +[cli_long_interface] */ + +/* [cli_long_destination] +\par `--destination ` +Set the forwarding destination as HOST:PORT; use [IPv6]:PORT for an IPv6 literal. Short alias: `-L`. +[cli_long_destination] */ + +/* [cli_long_vpn] +\par `--vpn` +Same behavior as `-V`; see that option for details. Short alias: `-V`. +[cli_long_vpn] */ + /* [cli-options] \snippet{doc} utils.c cli_short_f \snippet{doc} utils.c cli_short_s @@ -978,6 +1063,26 @@ main(int argc, char **argv) \snippet{doc} utils.c cli_short_V \snippet{doc} utils.c cli_short_6 \snippet{doc} utils.c cli_short_A +\snippet{doc} tunnel.c cli_long_config +\snippet{doc} tunnel.c cli_long_cipher +\snippet{doc} tunnel.c cli_long_timeout +\snippet{doc} tunnel.c cli_long_user +\snippet{doc} tunnel.c cli_long_pid_file +\snippet{doc} tunnel.c cli_long_nofile +\snippet{doc} tunnel.c cli_long_udp +\snippet{doc} tunnel.c cli_long_udp_only +\snippet{doc} tunnel.c cli_long_ipv6_first +\snippet{doc} tunnel.c cli_long_verbose +\snippet{doc} tunnel.c cli_long_server +\snippet{doc} tunnel.c cli_long_server_port +\snippet{doc} tunnel.c cli_long_listen_address +\snippet{doc} tunnel.c cli_long_listen_port +\snippet{doc} tunnel.c cli_long_interface +\snippet{doc} tunnel.c cli_long_destination +\snippet{doc} utils.c cli_long_version +\snippet{doc} utils.c cli_long_tcp_only +\snippet{doc} utils.c cli_long_ipv4_first +\snippet{doc} tunnel.c cli_long_vpn \snippet{doc} utils.c cli_long_fast_open \snippet{doc} utils.c cli_long_mtu \snippet{doc} utils.c cli_long_no_delay @@ -994,6 +1099,28 @@ main(int argc, char **argv) \snippet{doc} utils.c cli_long_help [cli-options] */ static struct option long_options[] = { + { "config", required_argument, NULL, 'c' }, + { "cipher", required_argument, NULL, 'm' }, + { "timeout", required_argument, NULL, 't' }, + { "user", required_argument, NULL, 'a' }, + { "pid-file", required_argument, NULL, 'f' }, + { "nofile", required_argument, NULL, 'n' }, + { "udp", no_argument, NULL, 'u' }, + { "udp-only", no_argument, NULL, 'U' }, + { "ipv6-first", no_argument, NULL, '6' }, + { "verbose", no_argument, NULL, 'v' }, + { "server", required_argument, NULL, 's' }, + { "server-port", required_argument, NULL, 'p' }, + { "listen-address", required_argument, NULL, 'b' }, + { "listen-port", required_argument, NULL, 'l' }, + { "interface", required_argument, NULL, 'i' }, + { "destination", required_argument, NULL, 'L' }, + { "version", no_argument, NULL, GETOPT_VAL_VERSION }, + { "tcp-only", no_argument, NULL, GETOPT_VAL_TCP_ONLY }, + { "ipv4-first", no_argument, NULL, GETOPT_VAL_IPV4_FIRST }, +#ifdef __ANDROID__ + { "vpn", no_argument, NULL, 'V' }, +#endif { "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN }, { "mtu", required_argument, NULL, GETOPT_VAL_MTU }, { "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY }, @@ -1011,24 +1138,37 @@ main(int argc, char **argv) { NULL, 0, NULL, 0 } }; + int mode_set = 0; + int ipv6first_set = 0; opterr = 0; USE_TTY(); #ifdef __ANDROID__ - while ((c = getopt_long(argc, argv, "f:s:p:l:k:t:m:i:c:b:L:a:n:huUvV6A", + while ((c = getopt_long(argc, argv, ":f:s:p:l:k:t:m:i:c:b:L:a:n:huUvV6A", long_options, NULL)) != -1) { #else - while ((c = getopt_long(argc, argv, "f:s:p:l:k:t:m:i:c:b:L:a:n:huUv6A", + while ((c = getopt_long(argc, argv, ":f:s:p:l:k:t:m:i:c:b:L:a:n:huUv6A", long_options, NULL)) != -1) { #endif switch (c) { + case GETOPT_VAL_VERSION: + cli_version(); + exit(EXIT_SUCCESS); + case GETOPT_VAL_TCP_ONLY: + mode = TCP_ONLY; + mode_set = 1; + break; + case GETOPT_VAL_IPV4_FIRST: + ipv6first = 0; + ipv6first_set = 1; + break; case GETOPT_VAL_FAST_OPEN: fast_open = 1; break; case GETOPT_VAL_MTU: if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { - FATAL("invalid MTU"); + cli_error("invalid MTU", c, NULL); } LOGI("set MTU to %d", mtu); break; @@ -1055,22 +1195,22 @@ main(int argc, char **argv) break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { - FATAL("invalid TCP incoming send buffer size"); + cli_error("invalid TCP incoming send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { - FATAL("invalid TCP incoming receive buffer size"); + cli_error("invalid TCP incoming receive buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { - FATAL("invalid TCP outgoing send buffer size"); + cli_error("invalid TCP outgoing send buffer size", c, NULL); } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { - FATAL("invalid TCP outgoing receive buffer size"); + cli_error("invalid TCP outgoing receive buffer size", c, NULL); } break; case 's': @@ -1079,9 +1219,19 @@ main(int argc, char **argv) } break; case 'p': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } remote_port = optarg; break; case 'l': + { + uint16_t checked_port; + if (ss_parse_uint16_port(optarg, &checked_port) != 0) + cli_error("port must be an integer from 1 to 65535", c, NULL); + } local_port = optarg; break; case GETOPT_VAL_PASSWORD: @@ -1093,6 +1243,11 @@ main(int argc, char **argv) pid_path = optarg; break; case 't': + { + int checked_timeout; + if (ss_parse_int(optarg, 1, INT_MAX, &checked_timeout) != 0) + cli_error("timeout must be a positive integer", c, NULL); + } timeout = optarg; break; case 'm': @@ -1108,9 +1263,11 @@ main(int argc, char **argv) local_addr = optarg; break; case 'u': + mode_set = 1; mode = TCP_AND_UDP; break; case 'U': + mode_set = 1; mode = UDP_ONLY; break; case 'L': @@ -1122,7 +1279,7 @@ main(int argc, char **argv) #ifdef HAVE_SETRLIMIT case 'n': if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { - FATAL("invalid nofile"); + cli_error("invalid nofile", c, NULL); } break; #endif @@ -1134,6 +1291,7 @@ main(int argc, char **argv) usage(); exit(EXIT_SUCCESS); case '6': + ipv6first_set = 1; ipv6first = 1; break; #ifdef __ANDROID__ @@ -1142,19 +1300,22 @@ main(int argc, char **argv) break; #endif case 'A': - FATAL("One time auth has been deprecated. Try AEAD ciphers instead."); + cli_error("one-time authentication was removed; use an AEAD cipher", 'A', NULL); + break; + case ':': + cli_error("missing required argument for option", optopt, argv[optind - 1]); break; case '?': - // The option character is not recognized. - LOGE("Unrecognized option: %s", optarg); - opterr = 1; + cli_error("unrecognized or invalid option", optopt, argv[optind - 1]); + break; + default: + cli_error("option is unsupported on this platform", c, NULL); break; } } - if (opterr) { - usage(); - exit(EXIT_FAILURE); + if (optind < argc) { + cli_error("unexpected positional argument", 0, NULL); } if (argc == 1) { @@ -1203,7 +1364,10 @@ main(int argc, char **argv) if (tunnel_addr_str == NULL) { tunnel_addr_str = conf->tunnel_address; } - if (mode == TCP_ONLY) { + if (!ipv6first_set) { + ipv6first = conf->ipv6_first; + } + if (!mode_set) { mode = conf->mode; } if (mtu == 0) { diff --git a/src/utils.c b/src/utils.c index 2c9ff1dca..d89615892 100644 --- a/src/utils.c +++ b/src/utils.c @@ -369,7 +369,7 @@ some require backend support): \snippet stream.c cli-stream-ciphers The `2022-blake3-*` ciphers implement Shadowsocks 2022 (SIP022). They require -a base64-encoded pre-shared key supplied with *-k*: 16 bytes for +a base64-encoded pre-shared key supplied with `--key` or `--password` (*-k*): 16 bytes for 2022-blake3-aes-128-gcm and 32 bytes for the other 2022 ciphers. Generate a 32-byte key with `openssl rand -base64 32`. Passwords are not stretched into keys for these ciphers. @@ -409,8 +409,7 @@ Send outbound traffic through the specified network interface where supported by /* [cli_short_b] \par `-b ` -Specify the local address to use while this client is making outbound -connections to the server. +Set the local address to bind for the client listener. [cli_short_b] */ /* [cli_short_u] @@ -560,167 +559,169 @@ Set the executable path of ss-server used by ss-manager. Linux builds with USE_NFTABLES only: add malicious IP addresses to nftables sets. Format: `[:][,[:]...]`. [cli_long_nftables_sets] */ +/* [cli_long_version] +\par `--version` +Print the program name and version to standard output and exit successfully. +[cli_long_version] */ + +/* [cli_long_tcp_only] +\par `--tcp-only` +Enable TCP relay only, overriding the configuration file's mode. +The last of `--tcp-only`, `--udp`, and `--udp-only` wins. +[cli_long_tcp_only] */ + +/* [cli_long_ipv4_first] +\par `--ipv4-first` +Prefer IPv4 DNS results, overriding `ipv6_first` in the configuration file. +This is an address preference, not a restriction to IPv4. The last of +`--ipv4-first` and `--ipv6-first` wins. +[cli_long_ipv4_first] */ + +static const char * +cli_program(void) +{ +#ifdef MODULE_LOCAL + return "ss-local"; +#elif defined(MODULE_REMOTE) + return "ss-server"; +#elif defined(MODULE_TUNNEL) + return "ss-tunnel"; +#elif defined(MODULE_REDIR) + return "ss-redir"; +#elif defined(MODULE_MANAGER) + return "ss-manager"; +#else + return "shadowsocks-c"; +#endif +} + +void +cli_version(void) +{ + printf("%s (shadowsocks-c) %s\n", cli_program(), VERSION); +} + void -usage() +cli_error(const char *message, int option, const char *token) { - printf("\n"); - printf("shadowsocks-c %s\n\n", VERSION); - printf( - " maintained by Max Lv and Linus Yang \n\n"); - printf(" usage:\n\n"); + fprintf(stderr, "%s: %s", cli_program(), message); + /* Report option names, never attached passwords or positional values. */ + if (token != NULL && token[0] == '-' && token[1] == '-') { + size_t length = 2; + while (isalnum((unsigned char)token[length]) || token[length] == '-') + length++; + if (length > 2 && length < 80) + fprintf(stderr, " '%.*s'", (int)length, token); + } else if (option > 0 && option < 128 && isalnum((unsigned char)option)) { + fprintf(stderr, " '-%c'", option); + } + fprintf(stderr, ". Try '%s --help'.\n", cli_program()); + exit(2); +} + +static void +cli_help_option(const char *flags, const char *description) +{ + printf(" %-36s %s\n", flags, description); +} + +void +usage(void) +{ + cli_version(); + printf("Usage: %s [options]\n\n", cli_program()); + puts("Connection:"); +#if defined(MODULE_REMOTE) || defined(MODULE_MANAGER) + cli_help_option("-s, --listen-address HOST", "Server listening address; may be repeated."); +#ifdef MODULE_REMOTE + cli_help_option("-p, --listen-port PORT", "Server listening port."); + cli_help_option("-b, --outbound-address ADDRESS", "Source address for outbound connections."); +#endif +#else + cli_help_option("-s, --server HOST", "Remote server hostname or IP; may be repeated."); + cli_help_option("-p, --server-port PORT", "Remote server port."); + cli_help_option("-b, --listen-address ADDRESS", "Local address to bind."); + cli_help_option("-l, --listen-port PORT", "Local listening port."); +#endif #ifdef MODULE_LOCAL - printf(" ss-local\n"); -#elif MODULE_REMOTE - printf(" ss-server\n"); -#elif MODULE_TUNNEL - printf(" ss-tunnel\n"); -#elif MODULE_REDIR - printf(" ss-redir\n"); -#elif MODULE_MANAGER - printf(" ss-manager\n"); + cli_help_option("--server-url URL", "Import an ss:// URL; later options override it."); #endif - printf("\n"); - printf( - " -s Host name or IP address of your remote server.\n"); - printf( - " -p Port number of your remote server.\n"); - printf( - " -l Port number of your local server.\n"); - printf( - " -k Password of your remote server.\n"); - printf( - " -m Encrypt method: rc4-md5, \n"); - printf( - " aes-128-gcm, aes-192-gcm, aes-256-gcm,\n"); - printf( - " aes-128-cfb, aes-192-cfb, aes-256-cfb,\n"); - printf( - " aes-128-ctr, aes-192-ctr, aes-256-ctr,\n"); - printf( - " camellia-128-cfb, camellia-192-cfb,\n"); - printf( - " camellia-256-cfb, bf-cfb,\n"); - printf( - " chacha20-ietf-poly1305,\n"); -#ifdef FS_HAVE_XCHACHA20IETF - printf( - " xchacha20-ietf-poly1305,\n"); +#ifdef MODULE_TUNNEL + cli_help_option("-L, --destination HOST:PORT", "Destination for local port forwarding."); #endif - printf( - " salsa20, chacha20 and chacha20-ietf.\n"); - printf( - " The default cipher is chacha20-ietf-poly1305.\n"); - printf("\n"); - printf( - " [-a ] Run as another user.\n"); - printf( - " [-f ] The file path to store pid.\n"); - printf( - " [-t ] Socket timeout in seconds.\n"); - printf( - " [-c ] The path to config file.\n"); -#ifdef HAVE_SETRLIMIT - printf( - " [-n ] Max number of open files.\n"); + puts("\nConfiguration and credentials:"); + cli_help_option("-c, --config FILE", "Read JSON configuration."); + cli_help_option("-m, --cipher NAME", "Cipher (default: chacha20-ietf-poly1305)."); + cli_help_option("-k, --password SECRET", "Password, or a base64 pre-shared key for AEAD-2022."); +#ifndef MODULE_MANAGER + cli_help_option("--key BASE64", "Use an explicit base64 key instead of a password."); #endif + puts(" AEAD-2022: 2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,"); + puts(" 2022-blake3-chacha20-poly1305."); + puts("\nTransport and networking:"); + cli_help_option("--tcp-only", "TCP only (default); overrides the configured mode."); + cli_help_option("-u, --udp", "Enable both TCP and UDP relay."); + cli_help_option("-U, --udp-only", "Enable UDP relay only."); + cli_help_option("--ipv4-first", "Prefer IPv4 DNS results; overrides configuration."); + cli_help_option("-6, --ipv6-first", "Prefer IPv6 DNS results."); + cli_help_option("-t, --timeout SECONDS", "Socket timeout (default: 60)."); #ifndef MODULE_REDIR - printf( - " [-i ] Network interface to bind.\n"); + cli_help_option("-i, --interface NAME", "Outbound network interface, where supported."); #endif - printf( - " [-b ] Local address to bind.\n"); - printf("\n"); - printf( - " [-u] Enable UDP relay.\n"); -#ifdef MODULE_REDIR - printf( - " TPROXY is required in redir mode.\n"); +#if defined(MODULE_REMOTE) || defined(MODULE_MANAGER) + cli_help_option("-d, --nameserver ADDRESS", "Name servers for the internal DNS resolver."); #endif - printf( - " [-U] Enable UDP relay and disable TCP relay.\n"); #ifdef MODULE_REDIR - printf( - " [-T] Use tproxy instead of redirect (for tcp).\n"); + cli_help_option("-T, --tproxy", "Use TPROXY for TCP; UDP always requires TPROXY."); #endif -#ifdef MODULE_REMOTE - printf( - " [-6] Resovle hostname to IPv6 address first.\n"); + cli_help_option("--mtu BYTES", "Network MTU (0 selects the default)."); + 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."); +#if !defined(MODULE_MANAGER) && (!defined(MODULE_REMOTE) || defined(__linux__)) + cli_help_option("--mptcp", "Enable Multipath TCP where supported."); #endif - printf("\n"); -#ifdef MODULE_TUNNEL - printf( - " [-L :] Destination server address and port\n"); - printf( - " for local port forwarding.\n"); +#ifndef MODULE_MANAGER + cli_help_option("--tcp-incoming-sndbuf BYTES", "Incoming TCP send buffer (0: system default)."); + cli_help_option("--tcp-incoming-rcvbuf BYTES", "Incoming TCP receive buffer (0: system default)."); + cli_help_option("--tcp-outgoing-sndbuf BYTES", "Outgoing TCP send buffer (0: system default)."); + cli_help_option("--tcp-outgoing-rcvbuf BYTES", "Outgoing TCP receive buffer (0: system default)."); #endif -#ifdef MODULE_REMOTE - printf( - " [-d ] Name servers for internal DNS resolver.\n"); + puts("\nAccess control and plugins:"); +#if defined(MODULE_LOCAL) || defined(MODULE_REMOTE) || defined(MODULE_MANAGER) + cli_help_option("--acl FILE", "Access control list."); #endif - printf( - " [--reuse-port] Enable port reuse.\n"); -#if defined(MODULE_REMOTE) || defined(MODULE_LOCAL) || defined(MODULE_REDIR) - printf( - " [--fast-open] Enable TCP fast open.\n"); - printf( - " with Linux kernel > 3.7.0.\n"); +#if defined(MODULE_REMOTE) && defined(__linux__) && defined(USE_NFTABLES) + cli_help_option("--nftables-sets SETS", "Record malicious IPs in [table:]set[,set...] entries."); #endif - printf( - " [--tcp-incoming-sndbuf] Size of the incoming connection TCP send buffer.\n"); - printf( - " [--tcp-incoming-rcvbuf] Size of the incoming connection TCP receive buffer.\n"); - printf( - " [--tcp-outgoing-sndbuf] Size of the outgoing connection TCP send buffer.\n"); - printf( - " [--tcp-outgoing-rcvbuf] Size of the outgoing connection TCP receive buffer.\n"); -#if defined(MODULE_REMOTE) || defined(MODULE_LOCAL) - printf( - " [--acl ] Path to ACL (Access Control List).\n"); + cli_help_option("--plugin NAME", "SIP003 plugin (requires plugin support in this build)."); + cli_help_option("--plugin-opts OPTIONS", "Options passed to the SIP003 plugin."); + puts("\nProcess and diagnostics:"); + cli_help_option("-a, --user USER", "Run as the specified user."); + cli_help_option("-f, --pid-file FILE", "Daemonize and write the process ID to FILE."); +#ifdef HAVE_SETRLIMIT + cli_help_option("-n, --nofile COUNT", "Maximum number of open files."); #endif #if defined(MODULE_REMOTE) || defined(MODULE_MANAGER) - printf( - " [--manager-address ] UNIX domain socket address.\n"); + cli_help_option("--manager-address ADDRESS", "Manager UNIX socket path or IP address and port."); #endif #ifdef MODULE_MANAGER - printf( - " [--executable ] Path to the executable of ss-server.\n"); - printf( - " [-D ] Path to the working directory of ss-manager.\n"); -#endif - printf( - " [--mtu ] MTU of your network interface.\n"); -#ifdef __linux__ - printf( - " [--mptcp] Enable Multipath TCP on MPTCP Kernel.\n"); -#ifdef USE_NFTABLES - printf( - " [--nftables-sets ] Add malicious IP into nftables sets.\n"); - printf( - " sets spec: [:][,[:]...]\n"); -#endif -#endif -#ifndef MODULE_MANAGER - printf( - " [--no-delay] Enable TCP_NODELAY.\n"); - printf( - " [--key ] Key of your remote server.\n"); + cli_help_option("--executable PATH", "ss-server executable used for managed servers."); + cli_help_option("-D, --workdir DIRECTORY", "Working directory for managed servers."); #endif +#if defined(__ANDROID__) && (defined(MODULE_LOCAL) || defined(MODULE_TUNNEL)) + cli_help_option("-V, --vpn", "Enable Android VPN socket protection."); #ifdef MODULE_LOCAL - printf( - " [--server-url ] Server as an ss:// URL, carrying the\n"); - printf( - " address, cipher, password and plugin.\n"); + cli_help_option("-S, --stat-path PATH", "Android traffic-statistics socket."); +#endif #endif - printf( - " [--plugin ] Enable SIP003 plugin. (Experimental)\n"); - printf( - " [--plugin-opts ] Set SIP003 plugin options. (Experimental)\n"); - printf("\n"); - printf( - " [-v] Verbose mode.\n"); - printf( - " [-h, --help] Print this message.\n"); - printf("\n"); + cli_help_option("-v, --verbose", "Enable verbose logging."); + cli_help_option("-h, --help", "Show this help and exit."); + cli_help_option("--version", "Show the version and exit."); + puts("\nOptions apply in command-line order; later mode and address-family flags win."); + puts("No positional arguments are accepted. Use --option=value for values starting with '-'."); + puts("The obsolete -A option is rejected; use an AEAD cipher instead."); + puts("Manual: https://shadowsocks.github.io/shadowsocks-c/"); } void diff --git a/src/utils.h b/src/utils.h index 9469f8a66..0f8c94985 100644 --- a/src/utils.h +++ b/src/utils.h @@ -219,6 +219,8 @@ int ss_parse_uint16_port(const char *s, uint16_t *out); int run_as(const char *user); void FATAL(const char *msg) __attribute__((noreturn)); void usage(void); +void cli_version(void); +void cli_error(const char *message, int option, const char *token) __attribute__((noreturn)); void daemonize(const char *path); char *ss_strndup(const char *s, size_t n); #ifdef HAVE_SETRLIMIT diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e23b663d1..d8da7e3f6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -214,3 +214,13 @@ ss_add_test(test_resolv_async "test_resolv_async.c;${PROJECT_SOURCE_DIR}/src/resolv.c;${PROJECT_SOURCE_DIR}/src/utils.c" "ss_ev;ss_cares;ss_sodium") set_tests_properties(test_resolv_async PROPERTIES TIMEOUT 30) + +if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING) + find_package(Python3 COMPONENTS Interpreter QUIET) + if(Python3_Interpreter_FOUND) + add_test(NAME test_cli + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_cli.py + --bin-dir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + set_tests_properties(test_cli PROPERTIES LABELS "integration" TIMEOUT 60) + endif() +endif() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 000000000..38d12fcec --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Exercise CLI parsing, diagnostics, and configuration overrides on real binaries.""" + +import argparse +import json +import os +from pathlib import Path +import socket +import subprocess +import tempfile +import time +import unittest + +BIN_DIR = Path(os.environ.get('SS_BIN_DIR', 'build/bin')).resolve() + + +class CliTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + suffix = '.exe' if os.name == 'nt' else '' + cls.binaries = {name: BIN_DIR / ('ss-' + name + suffix) + for name in ('local', 'server', 'tunnel', 'redir', 'manager') + if (BIN_DIR / ('ss-' + name + suffix)).is_file()} + for name in ('local', 'server', 'tunnel'): + if name not in cls.binaries: + raise RuntimeError(f'Missing required CLI binary: ss-{name}') + + def run_cli(self, name, *arguments): + return subprocess.run([str(self.binaries[name]), *arguments], + capture_output=True, text=True, timeout=10) + + def test_help_and_version(self): + for name in self.binaries: + with self.subTest(name=name): + help_result = self.run_cli(name, '--help') + self.assertEqual(help_result.returncode, 0) + self.assertEqual(help_result.stderr, '') + for flag in ('--config', '--cipher', '--tcp-only', '--udp-only', + '--ipv4-first', '--ipv6-first', '--version'): + self.assertIn(flag, help_result.stdout) + version = self.run_cli(name, '--version') + self.assertEqual(version.returncode, 0) + self.assertRegex(version.stdout, rf'^ss-{name} \(shadowsocks-c\) \d+\.\d+') + self.assertEqual(len(version.stdout.splitlines()), 1) + self.assertEqual(version.stderr, '') + if name == 'manager': + self.assertNotIn('--server-port', help_result.stdout) + self.assertNotIn('--listen-port', help_result.stdout) + self.assertNotIn('--tcp-incoming-sndbuf', help_result.stdout) + self.assertNotIn('--mptcp', help_result.stdout) + elif name == 'server': + self.assertIn('--outbound-address', help_result.stdout) + self.assertNotIn('--server-port', help_result.stdout) + + def test_long_aliases_accept_their_arguments(self): + for name in self.binaries: + args = ['--config', 'not-loaded-before-help.json', '--cipher', 'aes-128-gcm', + '--password', 'test-password', '--timeout', '60', '--user', 'nobody', + '--pid-file', 'not-created.pid', '--verbose', '--udp', '--udp-only', + '--tcp-only', '--ipv6-first', '--ipv4-first', '--listen-address', '127.0.0.1'] + if name not in ('server', 'manager'): + args += ['--server', '127.0.0.1', '--server-port', '8388'] + if name != 'manager': + args += ['--listen-port', '1080'] + if name == 'server': + args += ['--outbound-address', '127.0.0.1'] + if name != 'redir': + args += ['--interface', 'lo'] + if name in ('server', 'manager'): + args += ['--nameserver', '127.0.0.1'] + if name == 'tunnel': + args += ['--destination', '127.0.0.1:80'] + if name == 'redir': + args += ['--tproxy'] + result = self.run_cli(name, *args, '--help') + self.assertEqual(result.returncode, 0, (name, result.stderr)) + self.assertIn('Usage:', result.stdout) + + def test_usage_errors_are_stderr_exit_two_without_secrets(self): + secret = 'private-argument-value' + for name in self.binaries: + for args in (['--unknown=' + secret], ['-Z' + secret], [secret], + ['--', secret], ['--password'], ['-k'], ['-A']): + with self.subTest(name=name, arguments=args[:1]): + result = self.run_cli(name, *args) + self.assertEqual(result.returncode, 2) + self.assertEqual(result.stdout, '') + self.assertIn('--help', result.stderr) + self.assertNotIn(secret, result.stderr) + result = self.run_cli(name, '--password') + self.assertIn('missing required argument', result.stderr) + self.assertIn('--password', result.stderr) + for name in ('server', 'manager'): + if name in self.binaries: + self.assertEqual(self.run_cli(name, '-l', '1080').returncode, 2) + + def test_transport_and_address_family_override_configuration(self): + cases = [(['--tcp-only', '--ipv4-first'], False, False), + (['--udp-only', '--tcp-only', '--ipv6-first', '--ipv4-first'], False, False), + (['--tcp-only', '--udp', '--ipv4-first', '--ipv6-first'], True, True)] + for args, udp, ipv6 in cases: + with self.subTest(args=args), tempfile.TemporaryDirectory() as directory: + with socket.socket() as reservation: + reservation.bind(('127.0.0.1', 0)) + port = reservation.getsockname()[1] + config = Path(directory) / 'config.json' + config.write_text(json.dumps({'server': '127.0.0.1', 'server_port': 9, + 'local_address': '127.0.0.1', 'local_port': port, 'password': 'test-password', + 'method': 'aes-128-gcm', 'mode': 'udp_only', 'ipv6_first': True})) + with tempfile.TemporaryFile(mode='w+') as log: + proc = subprocess.Popen([str(self.binaries['local']), '--config', str(config), + '--listen-port', str(port), '--verbose', *args], stdout=log, stderr=log) + try: + deadline = time.monotonic() + 5 + connected = False + while time.monotonic() < deadline and proc.poll() is None: + try: + with socket.create_connection(('127.0.0.1', port), timeout=0.1): + connected = True + break + except OSError: + time.sleep(0.02) + self.assertTrue(connected, 'CLI TCP mode did not override UDP-only config') + finally: + if proc.poll() is None: + proc.terminate() + proc.wait(timeout=5) + log.seek(0) + output = log.read() + self.assertEqual('udprelay enabled' in output, udp, output) + self.assertEqual('resolving hostname to IPv6 address first' in output, ipv6, output) + + def test_numeric_option_validation(self): + for name in self.binaries: + for value in ('0', '-1', 'abc', '2147483648'): + result = self.run_cli(name, '--timeout', value) + self.assertEqual(result.returncode, 2, (name, value)) + self.assertEqual(result.stdout, '') + self.assertIn('positive integer', result.stderr) + if name != 'manager': + for value in ('0', '65536', '-1', 'abc'): + result = self.run_cli(name, '--listen-port', value) + self.assertEqual(result.returncode, 2, (name, value)) + self.assertIn('1 to 65535', result.stderr) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--bin-dir', type=Path, default=BIN_DIR) + args = parser.parse_args() + BIN_DIR = args.bin_dir.resolve() + unittest.main(argv=[__file__]) diff --git a/tests/test_cli_docs.py b/tests/test_cli_docs.py index 959be7a41..8b0032c39 100644 --- a/tests/test_cli_docs.py +++ b/tests/test_cli_docs.py @@ -27,7 +27,7 @@ def setUp(self): def change(self, filename, old, new): path = self.root / filename source = path.read_text(encoding='utf-8') - self.assertIn(old, source) + self.assertTrue(old in source, f'Missing fixture text in {filename}') path.write_text(source.replace(old, new), encoding='utf-8') def test_current_documentation_matches_all_variants(self): @@ -58,7 +58,7 @@ def test_unknown_parser_syntax_fails_closed(self): DOCS.check(self.root) def test_nonliteral_short_options_fail_closed(self): - self.change('src/local.c', '"f:s:p:l:k:t:m:i:c:b:a:n:huUv6A"', 'SHORT_OPTIONS') + self.change('src/local.c', '":f:s:p:l:k:t:m:i:c:b:a:n:huUv6A"', 'SHORT_OPTIONS') with self.assertRaisesRegex(ValueError, 'literal getopt_long'): DOCS.check(self.root)