mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-25 18:23:42 +00:00
Accept combined server endpoints with strict IPv6 parsing
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
+6
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+86
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
+12
-4
@@ -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]
|
||||
@@ -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':
|
||||
@@ -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);
|
||||
|
||||
+12
-4
@@ -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]
|
||||
@@ -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':
|
||||
@@ -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();
|
||||
|
||||
+12
-4
@@ -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]
|
||||
@@ -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':
|
||||
@@ -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();
|
||||
|
||||
+4
-4
@@ -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]
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
+23
-2
@@ -104,12 +104,12 @@ class CliTests(unittest.TestCase):
|
||||
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
|
||||
@@ -130,6 +130,27 @@ class CliTests(unittest.TestCase):
|
||||
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'):
|
||||
|
||||
@@ -181,6 +181,55 @@ test_read_jconf_rejects_out_of_range_int_options(void)
|
||||
remove(path);
|
||||
}
|
||||
|
||||
static void
|
||||
test_server_endpoints(void)
|
||||
{
|
||||
const char *valid[][3] = {
|
||||
{ "example.com:8388", "example.com", "8388" },
|
||||
{ "127.0.0.1:1", "127.0.0.1", "1" },
|
||||
{ "[2001:db8::1]:65535", "2001:db8::1", "65535" },
|
||||
{ "[fe80::1%eth0]:8388", "fe80::1%eth0", "8388" },
|
||||
{ "[fe80::1%3]:8388", "fe80::1%3", "8388" },
|
||||
{ "2001:db8::1:8388", "2001:db8::1:8388", NULL },
|
||||
{ "fe80::1%eth0", "fe80::1%eth0", NULL },
|
||||
{ "::", "::", NULL },
|
||||
{ "[::1]", "::1", NULL },
|
||||
{ "example.com", "example.com", NULL }
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(valid) / sizeof(valid[0]); i++) {
|
||||
ss_addr_t addr = { 0 };
|
||||
assert(parse_server_endpoint(valid[i][0], &addr) == 0);
|
||||
assert(strcmp(addr.host, valid[i][1]) == 0);
|
||||
assert(valid[i][2] ? addr.port && strcmp(addr.port, valid[i][2]) == 0 : addr.port == NULL);
|
||||
free_addr(&addr);
|
||||
}
|
||||
const char *invalid[] = { "", ":8388", "host:", "host:0", "host:65536",
|
||||
"host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk",
|
||||
"[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80",
|
||||
"[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80",
|
||||
"[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" };
|
||||
for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
|
||||
ss_addr_t addr = { 0 };
|
||||
assert(parse_server_endpoint(invalid[i], &addr) == -1);
|
||||
assert(addr.host == NULL && addr.port == NULL);
|
||||
}
|
||||
ss_addr_t addr[2] = { { 0 }, { 0 } };
|
||||
assert(parse_server_endpoint("[::1]:8388", &addr[0]) == 0);
|
||||
assert(parse_server_endpoint("localhost:8389", &addr[1]) == 0);
|
||||
assert(complete_server_ports(addr, 2, NULL, 0) == 0);
|
||||
assert(complete_server_ports(addr, 2, "9000", 0) == 0);
|
||||
assert(strcmp(addr[0].port, "8388") == 0 && strcmp(addr[1].port, "8389") == 0);
|
||||
assert(complete_server_ports(addr, 2, NULL, 1) == -1);
|
||||
free_addr(&addr[1]);
|
||||
memset(&addr[1], 0, sizeof(addr[1]));
|
||||
assert(parse_server_endpoint("::1", &addr[1]) == 0);
|
||||
assert(complete_server_ports(addr, 2, NULL, 0) == -1);
|
||||
assert(complete_server_ports(addr, 2, "8388", 1) == 0);
|
||||
assert(strcmp(addr[1].port, "8388") == 0);
|
||||
free_addr(&addr[0]);
|
||||
free_addr(&addr[1]);
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
@@ -190,6 +239,7 @@ main(int argc, char **argv)
|
||||
read_jconf(argv[2]);
|
||||
return 0;
|
||||
}
|
||||
test_server_endpoints();
|
||||
test_parse_addr_ipv4_with_port();
|
||||
test_parse_addr_ipv6_with_port();
|
||||
test_parse_addr_hostname_with_port();
|
||||
|
||||
Reference in New Issue
Block a user