Accept combined server endpoints with strict IPv6 parsing

This commit is contained in:
Max Lv
2026-09-13 07:15:28 +08:00
parent 243f1f92ec
commit 454bdc0a67
13 changed files with 214 additions and 23 deletions
+86
View File
@@ -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)
{
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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