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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ See [image and build details](docker/static/README.md).

## Usage

Clients accept a combined remote endpoint: `--server example.com:8388` or
`--server '[2001:db8::1]:8388'`. Repeat the flag for multiple servers. Legacy
`-s HOST -p PORT` remains supported; an embedded port takes priority.

For a detailed and complete list of all supported arguments,
you may refer to the man pages of the applications, respectively.

Expand Down
7 changes: 6 additions & 1 deletion doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ overview lookup names.
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.
- Clients: `--server HOST:PORT` selects the remote Shadowsocks server.
IPv6 endpoints must use brackets, for example `--server '[2001:db8::1]:8388'`
or `--server '[fe80::1%eth0]:8388'`. Bare IPv6 remains a host-only address.
Repeat `--server` for multiple endpoints, each with its own port. The legacy
`--server-port` / `-p` and configured server port are fallbacks only; embedded
ports always win. SIP003 plugins require all endpoints to share one port.
- 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.
Expand Down
2 changes: 1 addition & 1 deletion doc/ss-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 --server example.com --server-port 12345 --listen-port 1080 --password foobar --cipher aes-256-gcm
ss-local --server example.com:12345 --listen-port 1080 --password foobar --cipher aes-256-gcm
```

\section ss_local_see_also SEE ALSO
Expand Down
2 changes: 1 addition & 1 deletion doc/ss-tunnel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 --server example.com --server-port 12345 --listen-port 5353 --password foobar --cipher aes-256-gcm --destination 8.8.8.8:53 --udp
ss-tunnel --server example.com: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
Expand Down
86 changes: 86 additions & 0 deletions src/jconf.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,92 @@ parse_addr(const char *str_in, ss_addr_t *addr)
free(str);
}

/* CLI endpoints are strict; keep the legacy JSON address parser unchanged. */
int
parse_server_endpoint(const char *str, ss_addr_t *addr)
{
ss_addr_t parsed = { 0 };
uint16_t port;
if (str == NULL || *str == '\0')
return -1;
for (const char *p = str; *p; p++) {
if ((unsigned char)*p <= ' ' || *p == '/' || *p == '@')
return -1;
}
parse_addr(str, &parsed);
if (parsed.host == NULL || *parsed.host == '\0')
goto invalid;
if (str[0] == '[') {
const char *end = strchr(str, ']');
if (end == NULL || (end[1] != '\0' && end[1] != ':')
|| (end[1] == ':' && end[2] == '\0'))
goto invalid;
} else if (strchr(str, '[') || strchr(str, ']')) {
goto invalid;
}
if (str[0] == '[' || strchr(parsed.host, ':')) {
/* Validate IPv6 separately from an optional interface scope ID. */
struct ss_ip ip;
char *literal = strdup(parsed.host);
char *scope = strchr(literal, '%');
int valid_scope = 1;
if (scope != NULL) {
*scope++ = '\0';
valid_scope = *scope != '\0';
for (const char *p = scope; *p; p++) {
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| (*p >= '0' && *p <= '9') || *p == '_' || *p == '-' || *p == '.'))
valid_scope = 0;
}
}
int valid = strchr(literal, ':') != NULL && ss_ip_init(&ip, literal) != -1;
free(literal);
if (!valid || !valid_scope)
goto invalid;
} else if (strchr(parsed.host, '%')) {
goto invalid;
}
if (parsed.port != NULL) {
if (ss_parse_uint16_port(parsed.port, &port) != 0)
goto invalid;
for (const char *p = parsed.port; *p; p++) {
if (*p < '0' || *p > '9')
goto invalid;
}
} else if (str[strlen(str) - 1] == ':' && !strchr(parsed.host, ':')) {
goto invalid;
}
*addr = parsed;
return 0;
invalid:
free_addr(&parsed);
return -1;
}

/* Resolve fallback ports before callers reuse the first port for SIP003. */
int
complete_server_ports(ss_addr_t *addr, int count, const char *fallback, int plugin)
{
uint16_t first = 0;
if (count == 0)
return -1;
for (int i = 0; i < count; i++) {
uint16_t port;
const char *value = addr[i].port != NULL ? addr[i].port : fallback;
if (value == NULL || ss_parse_uint16_port(value, &port) != 0)
return -1;
if (plugin && i > 0 && port != first)
return -1;
if (i == 0)
first = port;
}
for (int i = 0; i < count; i++) {
if (addr[i].port == NULL)
addr[i].port = strdup(fallback);
}
return 0;
}

static int
parse_dscp(char *str)
{
Expand Down
2 changes: 2 additions & 0 deletions src/jconf.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,7 @@ typedef struct {
jconf_t *read_jconf(const char *file);
void parse_addr(const char *str, ss_addr_t *addr);
void free_addr(ss_addr_t *addr);
int parse_server_endpoint(const char *str, ss_addr_t *addr);
int complete_server_ports(ss_addr_t *addr, int count, const char *fallback, int plugin);

#endif // _JCONF_H
16 changes: 12 additions & 4 deletions src/local.c
Original file line number Diff line number Diff line change
Expand Up @@ -1563,13 +1563,13 @@ Same behavior as `-v`; see that option for details. Short alias: `-v`.
[cli_long_verbose] */

/* [cli_long_server]
\par `--server <server_host>`
Set a remote Shadowsocks server hostname or IP address; may be repeated. Short alias: `-s`.
\par `--server <HOST:PORT>`
Set a remote server endpoint; may be repeated with different ports. Use `HOST:PORT` for a hostname or IPv4, and `[IPv6]:PORT` for IPv6 (including `%scope` when needed). Bare IPv6 is always a host, never split into a port. Host-only values require the legacy `--server-port` fallback or a configured port. An endpoint port takes priority over the fallback regardless of option order. SIP003 plugins require the same port for all endpoints. Short alias: `-s`.
[cli_long_server] */

/* [cli_long_server_port]
\par `--server-port <server_port>`
Set the remote Shadowsocks server port. Short alias: `-p`.
Legacy fallback port for server values without an embedded port. Prefer `--server HOST:PORT`. Short alias: `-p`.
[cli_long_server_port] */

/* [cli_long_listen_address]
Expand Down Expand Up @@ -1802,7 +1802,11 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
break;
case 's':
if (remote_num < MAX_REMOTE_NUM) {
parse_addr(optarg, &remote_addr[remote_num++]);
if (parse_server_endpoint(optarg, &remote_addr[remote_num]) != 0)
cli_error("invalid server endpoint; use HOST:PORT or [IPv6]:PORT", 's', NULL);
remote_num++;
} else {
cli_error("too many server endpoints", 's', NULL);
}
break;
case 'p':
Expand Down Expand Up @@ -1991,6 +1995,10 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
}
}

if (complete_server_ports(remote_addr, remote_num, remote_port, plugin != NULL) != 0)
cli_error("each server needs a port (1 to 65535); plugins require a shared port", 's', NULL);
remote_port = remote_addr[0].port;

if (remote_num == 0) {
fprintf(stderr, "remote_num is 0\n");
exit(EXIT_FAILURE);
Expand Down
16 changes: 12 additions & 4 deletions src/redir.c
Original file line number Diff line number Diff line change
Expand Up @@ -978,13 +978,13 @@ Same behavior as `-v`; see that option for details. Short alias: `-v`.
[cli_long_verbose] */

/* [cli_long_server]
\par `--server <server_host>`
Set a remote Shadowsocks server hostname or IP address; may be repeated. Short alias: `-s`.
\par `--server <HOST:PORT>`
Set a remote server endpoint; may be repeated with different ports. Use `HOST:PORT` for a hostname or IPv4, and `[IPv6]:PORT` for IPv6 (including `%scope` when needed). Bare IPv6 is always a host, never split into a port. Host-only values require the legacy `--server-port` fallback or a configured port. An endpoint port takes priority over the fallback regardless of option order. SIP003 plugins require the same port for all endpoints. Short alias: `-s`.
[cli_long_server] */

/* [cli_long_server_port]
\par `--server-port <server_port>`
Set the remote Shadowsocks server port. Short alias: `-p`.
Legacy fallback port for server values without an embedded port. Prefer `--server HOST:PORT`. Short alias: `-p`.
[cli_long_server_port] */

/* [cli_long_listen_address]
Expand Down Expand Up @@ -1162,7 +1162,11 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
break;
case 's':
if (remote_num < MAX_REMOTE_NUM) {
parse_addr(optarg, &remote_addr[remote_num++]);
if (parse_server_endpoint(optarg, &remote_addr[remote_num]) != 0)
cli_error("invalid server endpoint; use HOST:PORT or [IPv6]:PORT", 's', NULL);
remote_num++;
} else {
cli_error("too many server endpoints", 's', NULL);
}
break;
case 'p':
Expand Down Expand Up @@ -1345,6 +1349,10 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
dscp = conf->dscp;
}

if (complete_server_ports(remote_addr, remote_num, remote_port, plugin != NULL) != 0)
cli_error("each server needs a port (1 to 65535); plugins require a shared port", 's', NULL);
remote_port = remote_addr[0].port;

if (remote_num == 0 || remote_port == NULL || local_port == NULL
|| (password == NULL && key == NULL)) {
usage();
Expand Down
16 changes: 12 additions & 4 deletions src/tunnel.c
Original file line number Diff line number Diff line change
Expand Up @@ -1008,13 +1008,13 @@ Same behavior as `-v`; see that option for details. Short alias: `-v`.
[cli_long_verbose] */

/* [cli_long_server]
\par `--server <server_host>`
Set a remote Shadowsocks server hostname or IP address; may be repeated. Short alias: `-s`.
\par `--server <HOST:PORT>`
Set a remote server endpoint; may be repeated with different ports. Use `HOST:PORT` for a hostname or IPv4, and `[IPv6]:PORT` for IPv6 (including `%scope` when needed). Bare IPv6 is always a host, never split into a port. Host-only values require the legacy `--server-port` fallback or a configured port. An endpoint port takes priority over the fallback regardless of option order. SIP003 plugins require the same port for all endpoints. Short alias: `-s`.
[cli_long_server] */

/* [cli_long_server_port]
\par `--server-port <server_port>`
Set the remote Shadowsocks server port. Short alias: `-p`.
Legacy fallback port for server values without an embedded port. Prefer `--server HOST:PORT`. Short alias: `-p`.
[cli_long_server_port] */

/* [cli_long_listen_address]
Expand Down Expand Up @@ -1215,7 +1215,11 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
break;
case 's':
if (remote_num < MAX_REMOTE_NUM) {
parse_addr(optarg, &remote_addr[remote_num++]);
if (parse_server_endpoint(optarg, &remote_addr[remote_num]) != 0)
cli_error("invalid server endpoint; use HOST:PORT or [IPv6]:PORT", 's', NULL);
remote_num++;
} else {
cli_error("too many server endpoints", 's', NULL);
}
break;
case 'p':
Expand Down Expand Up @@ -1404,6 +1408,10 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
#endif
}

if (complete_server_ports(remote_addr, remote_num, remote_port, plugin != NULL) != 0)
cli_error("each server needs a port (1 to 65535); plugins require a shared port", 's', NULL);
remote_port = remote_addr[0].port;

if (remote_num == 0 || remote_port == NULL || tunnel_addr_str == NULL
|| local_port == NULL || (password == NULL && key == NULL)) {
usage();
Expand Down
8 changes: 4 additions & 4 deletions src/utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,12 @@ ss_is_ipv6addr(const char *addr)

/* [cli_short_s]
\par `-s <server_host>`
Set the server's hostname or IP.
For clients, set a server endpoint as `HOST:PORT` or `[IPv6]:PORT`; host-only values use the legacy `-p` or configured fallback. Bare IPv6 is never split at its last colon. For server and manager, set the listening address.
[cli_short_s] */

/* [cli_short_p]
\par `-p <server_port>`
Set the server's port number.
For clients, set the legacy fallback port for server values without an embedded port. For ss-server, set the listening port.
[cli_short_p] */

/* [cli_short_l]
Expand Down Expand Up @@ -638,8 +638,8 @@ usage(void)
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("-s, --server HOST:PORT", "Remote endpoint; IPv6: [ADDRESS]:PORT. Repeatable.");
cli_help_option("-p, --server-port PORT", "Legacy fallback for servers without a port.");
cli_help_option("-b, --listen-address ADDRESS", "Local address to bind.");
cli_help_option("-l, --listen-port PORT", "Local listening port.");
#endif
Expand Down
3 changes: 1 addition & 2 deletions tests/stress_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,7 @@ def tunnel_args(ss_tunnel, cipher, password, server_port, local_port, fwd_host,
"""Build command-line args for ss-tunnel."""
return [
ss_tunnel,
"-s", "127.0.0.1",
"-p", str(server_port),
"--server", "127.0.0.1:%d" % server_port,
"-l", str(local_port),
"-k", password,
"-m", cipher,
Expand Down
25 changes: 23 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ def test_transport_and_address_family_override_configuration(self):
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,
config.write_text(json.dumps({'server': '127.0.0.1', 'server_port': 0,
'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)
'--server', '127.0.0.1:9', '--listen-port', str(port), '--verbose', *args], stdout=log, stderr=log)
try:
deadline = time.monotonic() + 5
connected = False
Expand All @@ -130,6 +130,27 @@ def test_transport_and_address_family_override_configuration(self):
self.assertEqual('udprelay enabled' in output, udp, output)
self.assertEqual('resolving hostname to IPv6 address first' in output, ipv6, output)

def test_server_endpoint_arguments(self):
for name in ('local', 'tunnel', 'redir'):
if name not in self.binaries:
continue
for endpoint in ('example.com:8388', '127.0.0.1:8388', '[::1]:8388',
'[fe80::1%eth0]:8388', '2001:db8::1:8388'):
result = self.run_cli(name, '--server', endpoint, '--help')
self.assertEqual(result.returncode, 0, (name, endpoint, result.stderr))
for endpoint in ('[::1', '[::1]:', '[::1]junk', '[::1]:65536',
'host:', ':8388', 'host:0', 'host:bad', '[host]:80'):
result = self.run_cli(name, '--server', endpoint)
self.assertEqual(result.returncode, 2, (name, endpoint))
self.assertEqual(result.stdout, '')
self.assertIn('invalid server endpoint', result.stderr)
for args in (['--server', '2001:db8::1:8388'],
['--server', 'host:8388', '--server', 'other'],
['--server', 'host:8388', '--server', 'other:8389', '--plugin', 'unused']):
result = self.run_cli(name, *args)
self.assertEqual(result.returncode, 2, (name, args))
self.assertIn('each server needs a port', result.stderr)

def test_numeric_option_validation(self):
for name in self.binaries:
for value in ('0', '-1', 'abc', '2147483648'):
Expand Down
Loading
Loading