diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1e2a554..24a18db3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,31 +32,23 @@ jobs: run: | mkdir -p build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release - make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) + jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + make -j"$jobs" - - name: Unit tests - run: | - cd build - ctest --output-on-failure - - - name: ss-setup TUI tests - run: bash tests/test_ss_setup.sh - - - name: Stress test - run: | - python3 tests/stress_test.py --bin build/shared/bin/ --size 10 - - - name: ss-redir transparent proxy test (QEMU) - if: runner.os == 'Linux' - run: | - sudo apt-get install -y qemu-system-x86 - bash tests/test_redir_qemu.sh build/shared/bin/ - timeout-minutes: 8 - - - name: Debian package build test - if: runner.os == 'Linux' + debian-package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install dependencies run: | + sudo apt-get update + sudo apt-get install -y libpcre2-dev libmbedtls-dev libsodium-dev libev-dev libc-ares-dev sudo apt-get install -y --no-install-recommends \ build-essential debhelper dpkg-dev fakeroot asciidoc-base xmlto pkg-config - bash tests/test_deb_build.sh + + - name: Debian package build test + run: bash tests/test_deb_build.sh timeout-minutes: 10 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..d88e6caf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,57 @@ +name: tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + tests: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libpcre2-dev libmbedtls-dev libsodium-dev libev-dev libc-ares-dev valgrind + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install mbedtls@3 libsodium libev c-ares pcre2 + + - name: Build test binaries + run: | + mkdir -p build && cd build + cmake .. -DCMAKE_BUILD_TYPE=Release + jobs="$(nproc 2>/dev/null || sysctl -n hw.ncpu)" + make -j"$jobs" + + - name: CTest + run: ctest --test-dir build -LE memcheck --output-on-failure --no-tests=error + + - name: Valgrind leak smoke tests + if: runner.os == 'Linux' + run: ctest --test-dir build -L memcheck --output-on-failure --no-tests=error + + - name: ss-setup TUI tests + run: bash tests/test_ss_setup.sh + + - name: Stress test + run: | + python3 tests/stress_test.py --bin build/shared/bin/ --size 10 + + - name: ss-redir transparent proxy test (QEMU) + if: runner.os == 'Linux' + run: | + sudo apt-get install -y qemu-system-x86 + bash tests/test_redir_qemu.sh build/shared/bin/ + timeout-minutes: 8 diff --git a/cmake/config.h.cmake b/cmake/config.h.cmake index 82216519..55984be4 100644 --- a/cmake/config.h.cmake +++ b/cmake/config.h.cmake @@ -162,9 +162,6 @@ /* Define to the sub-directory where libtool stores uninstalled libraries. */ #cmakedefine LT_OBJDIR "@LT_OBJDIR@" -/* Define to 1 if assertions should be disabled. */ -#cmakedefine NDEBUG 1 - /* Name of package */ #define PACKAGE "@PROJECT_NAME@" diff --git a/cmake/configure.cmake b/cmake/configure.cmake index 2c2d915f..16574975 100644 --- a/cmake/configure.cmake +++ b/cmake/configure.cmake @@ -102,7 +102,6 @@ check_function_exists(posix_memalign HAVE_POSIX_MEMALIGN) # Define to the sub-directory where libtool stores uninstalled libraries. set(LT_OBJDIR ".libs/") -set(NDEBUG 1) set(PACKAGE ${PROJECT_NAME}) set(PACKAGE_BUGREPORT max.c.lv@gmail.com) set(PACKAGE_NAME ${PROJECT_NAME}) diff --git a/libcork b/libcork index 074e074b..5ed3f837 160000 --- a/libcork +++ b/libcork @@ -1 +1 @@ -Subproject commit 074e074b26e9e372e90e6ade215217763c8644aa +Subproject commit 5ed3f837171fc50614f579c0571365914e6c3ab9 diff --git a/src/acl.c b/src/acl.c index c77bbd78..39886c99 100644 --- a/src/acl.c +++ b/src/acl.c @@ -53,25 +53,40 @@ static struct ip_set outbound_block_list_ipv4; static struct ip_set outbound_block_list_ipv6; static struct cork_dllist outbound_block_list_rules; -static void -parse_addr_cidr(const char *str, char *host, int *cidr) +static int +parse_addr_cidr(const char *str, char *host, size_t host_len, int *cidr) { int ret = -1; const char *pch; + size_t addr_len; + + if (host_len == 0) { + return -1; + } pch = strchr(str, '/'); while (pch != NULL) { ret = pch - str; pch = strchr(pch + 1, '/'); } + addr_len = ret == -1 ? strlen(str) : (size_t)ret; + if (addr_len >= host_len) { + return -1; + } + if (ret == -1) { - strcpy(host, str); + memcpy(host, str, addr_len); + host[addr_len] = '\0'; *cidr = -1; } else { - memcpy(host, str, ret); - host[ret] = '\0'; - *cidr = atoi(str + ret + 1); + memcpy(host, str, addr_len); + host[addr_len] = '\0'; + if (ss_parse_int(str + ret + 1, 0, 128, cidr) == -1) { + *cidr = -2; + } } + + return 0; } char * @@ -194,18 +209,29 @@ init_acl(const char *path) char host[MAX_HOSTNAME_LEN]; int cidr; - parse_addr_cidr(line, host, &cidr); + if (parse_addr_cidr(line, host, sizeof(host), &cidr) == -1) { + LOGE("invalid ACL entry: %s", line); + continue; + } struct cork_ip addr; int err = cork_ip_init(&addr, host); if (!err) { if (addr.version == 4) { + if (cidr > 32 || cidr == -2) { + LOGE("invalid ACL IPv4 CIDR: %s", line); + continue; + } if (cidr >= 0) { ipset_ipv4_add_network(list_ipv4, &(addr.ip.v4), cidr); } else { ipset_ipv4_add(list_ipv4, &(addr.ip.v4)); } } else if (addr.version == 6) { + if (cidr > 128 || cidr == -2) { + LOGE("invalid ACL IPv6 CIDR: %s", line); + continue; + } if (cidr >= 0) { ipset_ipv6_add_network(list_ipv6, &(addr.ip.v6), cidr); } else { @@ -214,8 +240,13 @@ init_acl(const char *path) } } else { rule_t *rule = new_rule(); - accept_rule_arg(rule, line); - init_rule(rule); + if (rule == NULL) { + continue; + } + if (accept_rule_arg(rule, line) != 1 || init_rule(rule) != 1) { + free_rule(rule); + continue; + } add_rule(rules, rule); } } @@ -242,9 +273,12 @@ free_acl(void) ipset_done(&black_list_ipv6); ipset_done(&white_list_ipv4); ipset_done(&white_list_ipv6); + ipset_done(&outbound_block_list_ipv4); + ipset_done(&outbound_block_list_ipv6); free_rules(&black_list_rules); free_rules(&white_list_rules); + free_rules(&outbound_block_list_rules); } int diff --git a/src/aead.c b/src/aead.c index 6f84d75a..185a7aa1 100644 --- a/src/aead.c +++ b/src/aead.c @@ -470,6 +470,7 @@ aead_decrypt_all(buffer_t *ciphertext, cipher_t *cipher, size_t capacity) if (ppbloom_check((void *)salt, salt_len) == 1) { LOGE("crypto: AEAD: repeat salt detected"); + aead_ctx_release(&cipher_ctx); return CRYPTO_ERROR; } diff --git a/src/base64.c b/src/base64.c index e1147fa4..52247fc6 100644 --- a/src/base64.c +++ b/src/base64.c @@ -50,7 +50,8 @@ static const uint8_t map2[] = int base64_decode(uint8_t *out, const char *in, int out_size) { - int i, v; + int i; + unsigned int v; uint8_t *dst = out; v = 0; diff --git a/src/crypto.c b/src/crypto.c index 76c426b5..b8e0dfbe 100644 --- a/src/crypto.c +++ b/src/crypto.c @@ -206,6 +206,10 @@ int crypto_derive_key(const char *pass, uint8_t *key, size_t key_len) { size_t datal; + + if (pass == NULL) + return 0; + datal = strlen((const char *)pass); const digest_type_t *md = mbedtls_md_info_from_string("MD5"); @@ -221,8 +225,6 @@ crypto_derive_key(const char *pass, uint8_t *key, size_t key_len) mds = mbedtls_md_get_size(md); memset(&c, 0, sizeof(mbedtls_md_context_t)); - if (pass == NULL) - return key_len; if (mbedtls_md_setup(&c, md, 0)) return 0; diff --git a/src/jconf.c b/src/jconf.c index d2815eff..9635dcde 100644 --- a/src/jconf.c +++ b/src/jconf.c @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include "netutils.h" #include "utils.h" @@ -45,7 +47,9 @@ to_string(const json_value *value) if (value->type == json_string) { return ss_strndup(value->u.string.ptr, value->u.string.length); } else if (value->type == json_integer) { - return strdup(ss_itoa(value->u.integer)); + char buf[32]; + snprintf(buf, sizeof(buf), "%" PRId64, value->u.integer); + return strdup(buf); } else if (value->type == json_null) { return NULL; } else { @@ -55,6 +59,18 @@ to_string(const json_value *value) return 0; } +static int +to_nonnegative_int(const json_value *value, const char *option) +{ + check_json_value_type(value, json_integer, + "invalid config file: option must be an integer"); + if (value->u.integer < 0 || value->u.integer > INT_MAX) { + LOGE("invalid config file: option '%s' is out of range", option); + FATAL("invalid config file: integer option is out of range"); + } + return (int)value->u.integer; +} + void free_addr(ss_addr_t *addr) { @@ -68,8 +84,7 @@ parse_addr(const char *str_in, ss_addr_t *addr) if (str_in == NULL) return; - int ipv6 = 0, ret = -1, n = 0, len; - char *pch; + int ret = -1, n = 0, len; char *str = strdup(str_in); len = strlen(str_in); @@ -80,33 +95,36 @@ parse_addr(const char *str_in, ss_addr_t *addr) return; } - pch = strchr(str, ':'); + if (str[0] == '[') { + char *end = strchr(str, ']'); + if (end != NULL) { + addr->host = ss_strndup(str + 1, end - str - 1); + if (end[1] == ':' && end[2] != '\0') { + addr->port = strdup(end + 2); + } else { + addr->port = NULL; + } + free(str); + return; + } + + addr->host = str; + addr->port = NULL; + return; + } + + char *pch = strchr(str, ':'); while (pch != NULL) { n++; ret = pch - str; pch = strchr(pch + 1, ':'); } - if (n > 1) { - ipv6 = 1; - if (str[ret - 1] != ']') { - ret = -1; - } - } - - if (ret == -1) { - if (ipv6) { - addr->host = ss_strndup(str + 1, strlen(str) - 2); - } else { - addr->host = strdup(str); - } + if (ret == -1 || n != 1) { + addr->host = strdup(str); addr->port = NULL; } else { - if (ipv6) { - addr->host = ss_strndup(str + 1, ret - 2); - } else { - addr->host = ss_strndup(str, ret); - } + addr->host = ss_strndup(str, ret); if (ret < len - 1) { addr->port = strdup(str + ret + 1); } else { @@ -275,27 +293,17 @@ read_jconf(const char *file) "invalid config file: option 'reuse_port' must be a boolean"); conf.reuse_port = value->u.boolean; } else if (strcmp(name, "tcp_incoming_sndbuf") == 0) { - check_json_value_type(value, json_integer, - "invalid config file: option 'tcp_incoming_sndbuf' must be an integer"); - conf.tcp_incoming_sndbuf = value->u.integer; + conf.tcp_incoming_sndbuf = to_nonnegative_int(value, name); } else if (strcmp(name, "tcp_incoming_rcvbuf") == 0) { - check_json_value_type(value, json_integer, - "invalid config file: option 'tcp_incoming_rcvbuf' must be an integer"); - conf.tcp_incoming_rcvbuf = value->u.integer; + conf.tcp_incoming_rcvbuf = to_nonnegative_int(value, name); } else if (strcmp(name, "tcp_outgoing_sndbuf") == 0) { - check_json_value_type(value, json_integer, - "invalid config file: option 'tcp_outgoing_sndbuf' must be an integer"); - conf.tcp_outgoing_sndbuf = value->u.integer; + conf.tcp_outgoing_sndbuf = to_nonnegative_int(value, name); } else if (strcmp(name, "tcp_outgoing_rcvbuf") == 0) { - check_json_value_type(value, json_integer, - "invalid config file: option 'tcp_outgoing_rcvbuf' must be an integer"); - conf.tcp_outgoing_rcvbuf = value->u.integer; + conf.tcp_outgoing_rcvbuf = to_nonnegative_int(value, name); } else if (strcmp(name, "auth") == 0) { FATAL("One time auth has been deprecated. Try AEAD ciphers instead."); } else if (strcmp(name, "nofile") == 0) { - check_json_value_type(value, json_integer, - "invalid config file: option 'nofile' must be an integer"); - conf.nofile = value->u.integer; + conf.nofile = to_nonnegative_int(value, name); } else if (strcmp(name, "nameserver") == 0) { conf.nameserver = to_string(value); } else if (strcmp(name, "dscp") == 0) { @@ -306,12 +314,14 @@ read_jconf(const char *file) } json_value *v = value->u.object.values[j].value; if (v->type == json_string) { - int dscp = parse_dscp(to_string(v)); + char *dscp_str = to_string(v); + int dscp = parse_dscp(dscp_str); char *port = ss_strndup(value->u.object.values[j].name, value->u.object.values[j].name_length); conf.dscp[j].port = port; conf.dscp[j].dscp = dscp; conf.dscp_num = j + 1; + ss_free(dscp_str); } } } @@ -334,9 +344,7 @@ read_jconf(const char *file) ss_free(mode_str); } else if (strcmp(name, "mtu") == 0) { - check_json_value_type(value, json_integer, - "invalid config file: option 'mtu' must be an integer"); - conf.mtu = value->u.integer; + conf.mtu = to_nonnegative_int(value, name); } else if (strcmp(name, "mptcp") == 0) { check_json_value_type(value, json_boolean, "invalid config file: option 'mptcp' must be a boolean"); diff --git a/src/json.c b/src/json.c index 830acee4..67e363a5 100644 --- a/src/json.c +++ b/src/json.c @@ -111,7 +111,7 @@ static int new_value (json_state * state, json_type type) { json_value * value; - int values_size; + unsigned long values_size; if (!state->first_pass) { @@ -128,6 +128,9 @@ static int new_value (json_state * state, if (value->u.array.length == 0) break; + if (value->u.array.length > state->ulong_max / sizeof (json_value *)) + return 0; + if (! (value->u.array.values = (json_value **) json_alloc (state, value->u.array.length * sizeof (json_value *), 0)) ) { @@ -142,10 +145,16 @@ static int new_value (json_state * state, if (value->u.object.length == 0) break; + if (value->u.object.length > state->ulong_max / sizeof (*value->u.object.values)) + return 0; + values_size = sizeof (*value->u.object.values) * value->u.object.length; + if (value->object_mem_size > state->ulong_max - values_size) + return 0; + if (! (value->u.object.values = (json_object_entry *) json_alloc - (state, values_size + (CONV_PTR value->u.object.values), 0)) ) + (state, values_size + value->object_mem_size, 0)) ) { return 0; } @@ -412,7 +421,7 @@ json_value * json_parse_ex (json_settings * settings, case json_object: if (state.first_pass) - (*(json_char **) &top->u.object.values) += string_length + 1; + top->object_mem_size += string_length + 1; else { top->u.object.values [top->u.object.length].name diff --git a/src/json.h b/src/json.h index f6549ec4..da983f3d 100644 --- a/src/json.h +++ b/src/json.h @@ -159,6 +159,8 @@ typedef struct _json_value } _reserved; + unsigned long object_mem_size; + #ifdef JSON_TRACK_SOURCE /* Location of the value in the source JSON @@ -280,4 +282,3 @@ void json_value_free_ex (json_settings * settings, #endif - diff --git a/src/local.c b/src/local.c index 51f62c4a..e9e659e9 100644 --- a/src/local.c +++ b/src/local.c @@ -399,6 +399,9 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf) sprintf(port, "%d", p); } } else if (atyp == SOCKS5_ATYP_DOMAIN) { + if (buf->len < request_len + 1) { + return -1; + } uint8_t name_len = *(uint8_t *)(buf->data + request_len); if (buf->len < request_len + 1 + name_len + 2) { return -1; @@ -1428,6 +1431,7 @@ main(int argc, char **argv) int pid_flags = 0; int mtu = 0; int mptcp = 0; + int timeout_secs = 0; char *user = NULL; char *local_port = NULL; char *local_addr = NULL; @@ -1491,7 +1495,9 @@ main(int argc, char **argv) acl = !init_acl(optarg); break; case GETOPT_VAL_MTU: - mtu = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { + FATAL("invalid MTU"); + } LOGI("set MTU to %d", mtu); break; case GETOPT_VAL_MPTCP: @@ -1516,16 +1522,24 @@ main(int argc, char **argv) reuse_port = 1; break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: - tcp_incoming_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { + FATAL("invalid TCP incoming send buffer size"); + } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: - tcp_incoming_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { + FATAL("invalid TCP incoming receive buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: - tcp_outgoing_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { + FATAL("invalid TCP outgoing send buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: - tcp_outgoing_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { + FATAL("invalid TCP outgoing receive buffer size"); + } break; case 's': if (remote_num < MAX_REMOTE_NUM) { @@ -1566,7 +1580,9 @@ main(int argc, char **argv) break; #ifdef HAVE_SETRLIMIT case 'n': - nofile = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { + FATAL("invalid nofile"); + } break; #endif case 'u': @@ -1781,6 +1797,9 @@ main(int argc, char **argv) if (timeout == NULL) { timeout = "60"; } + if (ss_parse_int(timeout, 1, INT_MAX, &timeout_secs) == -1) { + FATAL("invalid timeout"); + } #ifdef HAVE_SETRLIMIT /* @@ -1918,7 +1937,7 @@ main(int argc, char **argv) if (plugin != NULL) break; } - listen_ctx.timeout = atoi(timeout); + listen_ctx.timeout = timeout_secs; listen_ctx.iface = iface; listen_ctx.mptcp = mptcp; @@ -1974,6 +1993,9 @@ main(int argc, char **argv) struct sockaddr *addr = (struct sockaddr *)storage; udp_fd = init_udprelay(local_addr, local_port, addr, get_sockaddr_len(addr), mtu, crypto, listen_ctx.timeout, iface); + if (udp_fd == -1) { + FATAL("failed to initialize UDP relay"); + } } #ifdef HAVE_LAUNCHD @@ -2146,6 +2168,9 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat struct sockaddr *addr = (struct sockaddr *)(&storage); udp_fd = init_udprelay(local_addr, local_port_str, addr, get_sockaddr_len(addr), mtu, crypto, timeout, NULL); + if (udp_fd == -1) { + return -1; + } } // Init connections diff --git a/src/manager.c b/src/manager.c index 0c881f0e..60a4e8fb 100644 --- a/src/manager.c +++ b/src/manager.c @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #include #include #include +#include #include #include @@ -48,6 +50,7 @@ #include #include #include +#include #include #if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_NET_IF_H) && defined(__linux__) @@ -65,6 +68,8 @@ #define BUF_SIZE 65535 #endif +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) + int verbose = 0; char *executable = "ss-server"; char *working_dir = NULL; @@ -72,6 +77,31 @@ int working_dir_size = 0; static struct cork_hash_table *server_table; +static int +copy_port(char *dst, size_t dst_len, const char *src, size_t src_len) +{ + if (dst_len == 0 || src == NULL || src_len == 0 || src_len >= dst_len) { + return -1; + } + + for (size_t i = 0; i < src_len; i++) { + if (!isdigit((unsigned char)src[i])) { + return -1; + } + } + + memcpy(dst, src, src_len); + dst[src_len] = '\0'; + + uint16_t port = 0; + if (ss_parse_uint16_port(dst, &port) == -1) { + dst[0] = '\0'; + return -1; + } + + return 0; +} + static int setnonblocking(int fd) { @@ -97,6 +127,180 @@ destroy_server(struct server *server) } static void +write_json_string(FILE *f, const char *str) +{ + const unsigned char *p = (const unsigned char *)str; + + fputc('"', f); + while (*p) { + switch (*p) { + case '"': + fputs("\\\"", f); + break; + case '\\': + fputs("\\\\", f); + break; + case '\b': + fputs("\\b", f); + break; + case '\f': + fputs("\\f", f); + break; + case '\n': + fputs("\\n", f); + break; + case '\r': + fputs("\\r", f); + break; + case '\t': + fputs("\\t", f); + break; + default: + if (*p < 0x20) { + fprintf(f, "\\u%04x", *p); + } else { + fputc(*p, f); + } + break; + } + p++; + } + fputc('"', f); +} + +static int +append_char(char *buf, size_t buf_size, size_t *pos, char ch) +{ + if (*pos >= buf_size - 1) { + return -1; + } + buf[(*pos)++] = ch; + buf[*pos] = '\0'; + return 0; +} + +static int +append_string(char *buf, size_t buf_size, size_t *pos, const char *str) +{ + while (*str) { + if (append_char(buf, buf_size, pos, *str++) == -1) { + return -1; + } + } + return 0; +} + +static int +append_json_string(char *buf, size_t buf_size, size_t *pos, const char *str) +{ + const unsigned char *p = (const unsigned char *)str; + + if (append_char(buf, buf_size, pos, '"') == -1) { + return -1; + } + while (*p) { + char escaped[7]; + + switch (*p) { + case '"': + if (append_string(buf, buf_size, pos, "\\\"") == -1) { + return -1; + } + break; + case '\\': + if (append_string(buf, buf_size, pos, "\\\\") == -1) { + return -1; + } + break; + case '\b': + if (append_string(buf, buf_size, pos, "\\b") == -1) { + return -1; + } + break; + case '\f': + if (append_string(buf, buf_size, pos, "\\f") == -1) { + return -1; + } + break; + case '\n': + if (append_string(buf, buf_size, pos, "\\n") == -1) { + return -1; + } + break; + case '\r': + if (append_string(buf, buf_size, pos, "\\r") == -1) { + return -1; + } + break; + case '\t': + if (append_string(buf, buf_size, pos, "\\t") == -1) { + return -1; + } + break; + default: + if (*p < 0x20) { + snprintf(escaped, sizeof(escaped), "\\u%04x", *p); + if (append_string(buf, buf_size, pos, escaped) == -1) { + return -1; + } + } else if (append_char(buf, buf_size, pos, *p) == -1) { + return -1; + } + break; + } + p++; + } + return append_char(buf, buf_size, pos, '"'); +} + +static int +append_list_entry(char *buf, size_t buf_size, size_t *pos, + struct server *server, const char *method) +{ + if (append_string(buf, buf_size, pos, "\n\t{\"server_port\":") == -1 || + append_json_string(buf, buf_size, pos, server->port) == -1 || + append_string(buf, buf_size, pos, ",\"password\":") == -1 || + append_json_string(buf, buf_size, pos, server->password) == -1 || + append_string(buf, buf_size, pos, ",\"method\":") == -1 || + append_json_string(buf, buf_size, pos, method) == -1 || + append_string(buf, buf_size, pos, "},") == -1) { + return -1; + } + return 0; +} + +static int +prepare_sigchld_for_wait(struct sigaction *old_action) +{ + struct sigaction action; + + if (sigaction(SIGCHLD, NULL, old_action) == -1) { + ERROR("sigaction"); + return -1; + } + if (old_action->sa_handler != SIG_IGN) { + return 0; + } + + memset(&action, 0, sizeof(action)); + action.sa_handler = SIG_DFL; + sigemptyset(&action.sa_mask); + if (sigaction(SIGCHLD, &action, NULL) == -1) { + ERROR("sigaction"); + return -1; + } + return 1; +} + +static void +restore_sigchld_after_wait(int restore, const struct sigaction *old_action) +{ + if (restore == 1 && sigaction(SIGCHLD, old_action, NULL) == -1) { + ERROR("sigaction"); + } +} + +static int build_config(char *prefix, struct manager_ctx *manager, struct server *server) { char *path = NULL; @@ -110,15 +314,19 @@ build_config(char *prefix, struct manager_ctx *manager, struct server *server) LOGE("unable to open config file"); } ss_free(path); - return; + return -1; } fprintf(f, "{\n"); - fprintf(f, "\"server_port\":%d,\n", atoi(server->port)); - fprintf(f, "\"password\":\"%s\"", server->password); - if (server->method) - fprintf(f, ",\n\"method\":\"%s\"", server->method); - else if (manager->method) - fprintf(f, ",\n\"method\":\"%s\"", manager->method); + fprintf(f, "\"server_port\":%s,\n", server->port); + fprintf(f, "\"password\":"); + write_json_string(f, server->password); + if (server->method) { + fprintf(f, ",\n\"method\":"); + write_json_string(f, server->method); + } else if (manager->method) { + fprintf(f, ",\n\"method\":"); + write_json_string(f, manager->method); + } if (server->fast_open[0]) fprintf(f, ",\n\"fast_open\": %s", server->fast_open); else if (manager->fast_open) @@ -129,105 +337,212 @@ build_config(char *prefix, struct manager_ctx *manager, struct server *server) fprintf(f, ",\n\"no_delay\": true"); if (manager->reuse_port) fprintf(f, ",\n\"reuse_port\": true"); - if (server->mode) - fprintf(f, ",\n\"mode\":\"%s\"", server->mode); - if (server->plugin) - fprintf(f, ",\n\"plugin\":\"%s\"", server->plugin); - if (server->plugin_opts) - fprintf(f, ",\n\"plugin_opts\":\"%s\"", server->plugin_opts); + if (server->mode) { + fprintf(f, ",\n\"mode\":"); + write_json_string(f, server->mode); + } + if (server->plugin) { + fprintf(f, ",\n\"plugin\":"); + write_json_string(f, server->plugin); + } + if (server->plugin_opts) { + fprintf(f, ",\n\"plugin_opts\":"); + write_json_string(f, server->plugin_opts); + } fprintf(f, "\n}\n"); fclose(f); ss_free(path); + return 0; } -static char * -construct_command_line(struct manager_ctx *manager, struct server *server) +static int +add_server_arg(char **argv, int *argc, int max_argc, char *arg) +{ + if (*argc >= max_argc - 1) { + return -1; + } + + argv[(*argc)++] = arg; + argv[*argc] = NULL; + return 0; +} + +static int +start_server_process(struct manager_ctx *manager, struct server *server) { - static char cmd[BUF_SIZE]; - int i; int port; + int argc = 0; + int status; + char *argv[64 + MAX_REMOTE_NUM * 2]; + char *pid_path = NULL; + char *conf_path = NULL; + char nofile[32]; + char mtu[32]; + struct sigaction old_sigchld; + int restore_sigchld = 0; - port = atoi(server->port); - - build_config(working_dir, manager, server); - - memset(cmd, 0, BUF_SIZE); - snprintf(cmd, BUF_SIZE, - "%s --manager-address %s -f %s/.shadowsocks_%d.pid -c %s/.shadowsocks_%d.conf", - executable, manager->manager_address, working_dir, port, working_dir, port); - - if (manager->acl != NULL) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " --acl %s", manager->acl); + if (ss_parse_int(server->port, 1, UINT16_MAX, &port) == -1) { + return -1; } - if (manager->timeout != NULL) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -t %s", manager->timeout); + + if (build_config(working_dir, manager, server) == -1) { + return -1; } + + int pid_path_size = strlen(working_dir) + strlen(server->port) + 20; + int conf_path_size = strlen(working_dir) + strlen(server->port) + 20; + pid_path = ss_malloc(pid_path_size); + conf_path = ss_malloc(conf_path_size); + snprintf(pid_path, pid_path_size, "%s/.shadowsocks_%d.pid", working_dir, port); + snprintf(conf_path, conf_path_size, "%s/.shadowsocks_%d.conf", working_dir, port); + + argv[0] = NULL; + + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), executable) == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--manager-address") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->manager_address) == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-f") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), pid_path) == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-c") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), conf_path) == -1) { + goto ERROR; + } + + if (manager->acl != NULL && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--acl") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->acl) == -1)) { + goto ERROR; + } + if (manager->timeout != NULL && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-t") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->timeout) == -1)) { + goto ERROR; + } + #ifdef HAVE_SETRLIMIT if (manager->nofile) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -n %d", manager->nofile); + snprintf(nofile, sizeof(nofile), "%d", manager->nofile); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-n") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), nofile) == -1) { + goto ERROR; + } } #endif - if (manager->user != NULL) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -a %s", manager->user); + if (manager->user != NULL && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-a") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->user) == -1)) { + goto ERROR; } if (manager->verbose) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -v"); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-v") == -1) { + goto ERROR; + } } if (server->mode == NULL && manager->mode == UDP_ONLY) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -U"); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-U") == -1) { + goto ERROR; + } } if (server->mode == NULL && manager->mode == TCP_AND_UDP) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -u"); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-u") == -1) { + goto ERROR; + } } - if (manager->iface) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -i \"%s\"", manager->iface); + if (manager->iface && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-i") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->iface) == -1)) { + goto ERROR; } if (server->fast_open[0] == 0 && manager->fast_open) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " --fast-open"); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--fast-open") == -1) { + goto ERROR; + } } if (server->no_delay[0] == 0 && manager->no_delay) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " --no-delay"); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--no-delay") == -1) { + goto ERROR; + } } if (manager->ipv6first) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -6"); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-6") == -1) { + goto ERROR; + } } if (manager->mtu) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " --mtu %d", manager->mtu); + snprintf(mtu, sizeof(mtu), "%d", manager->mtu); + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--mtu") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), mtu) == -1) { + goto ERROR; + } } - if (server->plugin == NULL && manager->plugin) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " --plugin \"%s\"", manager->plugin); + if (server->plugin == NULL && manager->plugin && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--plugin") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->plugin) == -1)) { + goto ERROR; } - if (server->plugin_opts == NULL && manager->plugin_opts) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " --plugin-opts \"%s\"", manager->plugin_opts); + if (server->plugin_opts == NULL && manager->plugin_opts && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "--plugin-opts") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->plugin_opts) == -1)) { + goto ERROR; } - if (manager->nameservers) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -d \"%s\"", manager->nameservers); + if (manager->nameservers && + (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-d") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->nameservers) == -1)) { + goto ERROR; } - for (i = 0; i < manager->host_num; i++) { - int len = strlen(cmd); - snprintf(cmd + len, BUF_SIZE - len, " -s %s", manager->hosts[i]); + for (int i = 0; i < manager->host_num; i++) { + if (add_server_arg(argv, &argc, ARRAY_SIZE(argv), "-s") == -1 || + add_server_arg(argv, &argc, ARRAY_SIZE(argv), manager->hosts[i]) == -1) { + goto ERROR; + } } if (verbose) { - LOGI("cmd: %s", cmd); + LOGI("exec: %s", executable); } - return cmd; + restore_sigchld = prepare_sigchld_for_wait(&old_sigchld); + if (restore_sigchld == -1) { + goto ERROR; + } + + pid_t pid = fork(); + if (pid == -1) { + ERROR("fork"); + goto ERROR; + } + + if (pid == 0) { + execvp(argv[0], argv); + ERROR("execvp"); + _exit(127); + } + + while (waitpid(pid, &status, 0) == -1) { + if (errno != EINTR) { + ERROR("waitpid"); + goto ERROR; + } + } + + restore_sigchld_after_wait(restore_sigchld, &old_sigchld); + restore_sigchld = 0; + + ss_free(pid_path); + ss_free(conf_path); + + if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { + return 0; + } + + LOGE("server process exited unexpectedly"); + return -1; + +ERROR: + restore_sigchld_after_wait(restore_sigchld, &old_sigchld); + ss_free(pid_path); + ss_free(conf_path); + return -1; } static char * @@ -287,6 +602,7 @@ get_server(char *buf, int len) struct server *server = ss_malloc(sizeof(struct server)); memset(server, 0, sizeof(struct server)); + int valid = 1; if (obj->type == json_object) { int i = 0; for (i = 0; i < obj->u.object.length; i++) { @@ -294,13 +610,30 @@ get_server(char *buf, int len) json_value *value = obj->u.object.values[i].value; if (strcmp(name, "server_port") == 0) { if (value->type == json_string) { - strncpy(server->port, value->u.string.ptr, 7); + if (copy_port(server->port, sizeof(server->port), + value->u.string.ptr, + value->u.string.length) == -1) { + LOGE("invalid server_port"); + valid = 0; + break; + } } else if (value->type == json_integer) { - snprintf(server->port, 8, "%" PRIu64 "", value->u.integer); + if (value->u.integer <= 0 || value->u.integer > 65535) { + LOGE("invalid server_port"); + valid = 0; + break; + } + snprintf(server->port, sizeof(server->port), "%" PRId64, value->u.integer); } } else if (strcmp(name, "password") == 0) { if (value->type == json_string) { - strncpy(server->password, value->u.string.ptr, 127); + if (value->u.string.length >= sizeof(server->password)) { + LOGE("password is too long"); + valid = 0; + break; + } + memcpy(server->password, value->u.string.ptr, value->u.string.length); + server->password[value->u.string.length] = '\0'; } } else if (strcmp(name, "method") == 0) { if (value->type == json_string) { @@ -328,12 +661,18 @@ get_server(char *buf, int len) } } else { LOGE("invalid data: %s", data); + valid = 0; break; } } } json_value_free(obj); + if (!valid) { + destroy_server(server); + ss_free(server); + return NULL; + } return server; } @@ -343,6 +682,9 @@ parse_traffic(char *buf, int len, char *port, uint64_t *traffic) char *data = get_data(buf, len); char error_buf[512]; json_settings settings = { 0 }; + int found = 0; + + port[0] = '\0'; if (data == NULL) { LOGE("No data found"); @@ -361,14 +703,19 @@ parse_traffic(char *buf, int len, char *port, uint64_t *traffic) char *name = obj->u.object.values[i].name; json_value *value = obj->u.object.values[i].value; if (value->type == json_integer) { - strncpy(port, name, 7); - *traffic = value->u.integer; + if (value->u.integer < 0 || + copy_port(port, 8, name, obj->u.object.values[i].name_length) == -1) { + json_value_free(obj); + return -1; + } + *traffic = (uint64_t)value->u.integer; + found = 1; } } } json_value_free(obj); - return 0; + return found ? 0 : -1; } static int @@ -433,12 +780,11 @@ create_and_bind(const char *host, const char *port, int protocol) s = bind(listen_sock, rp->ai_addr, rp->ai_addrlen); if (s == 0) { /* We managed to bind successfully! */ - close(listen_sock); - break; } else { ERROR("bind"); + close(listen_sock); } } @@ -451,47 +797,37 @@ create_and_bind(const char *host, const char *port, int protocol) return -1; } - return listen_sock; + return 0; } static int check_port(struct manager_ctx *manager, struct server *server) { bool both_tcp_udp = manager->mode == TCP_AND_UDP; - int fd_count = manager->host_num * (both_tcp_udp ? 2 : 1); int bind_err = 0; - int *sock_fds = (int *)ss_malloc(fd_count * sizeof(int)); - memset(sock_fds, 0, fd_count * sizeof(int)); - /* try to bind each interface */ for (int i = 0; i < manager->host_num; i++) { LOGI("try to bind interface: %s, port: %s", manager->hosts[i], server->port); + int tcp_udp_ret; if (manager->mode == UDP_ONLY) { - sock_fds[i] = create_and_bind(manager->hosts[i], server->port, IPPROTO_UDP); + tcp_udp_ret = create_and_bind(manager->hosts[i], server->port, IPPROTO_UDP); } else { - sock_fds[i] = create_and_bind(manager->hosts[i], server->port, IPPROTO_TCP); + tcp_udp_ret = create_and_bind(manager->hosts[i], server->port, IPPROTO_TCP); } + int udp_ret = 0; if (both_tcp_udp) { - sock_fds[i + manager->host_num] = create_and_bind(manager->hosts[i], server->port, IPPROTO_UDP); + udp_ret = create_and_bind(manager->hosts[i], server->port, IPPROTO_UDP); } - if (sock_fds[i] == -1 || (both_tcp_udp && sock_fds[i + manager->host_num] == -1)) { + if (tcp_udp_ret == -1 || udp_ret == -1) { bind_err = -1; break; } } - /* clean socks */ - for (int i = 0; i < fd_count; i++) - if (sock_fds[i] > 0) { - close(sock_fds[i]); - } - - ss_free(sock_fds); - return bind_err == -1 ? -1 : 0; } @@ -505,15 +841,14 @@ add_server(struct manager_ctx *manager, struct server *server) return -1; } - bool new = false; - cork_hash_table_put(server_table, (void *)server->port, (void *)server, &new, NULL, NULL); - - char *cmd = construct_command_line(manager, server); - if (system(cmd) == -1) { - ERROR("add_server_system"); + if (start_server_process(manager, server) == -1) { + ERROR("add_server_process"); return -1; } + bool new = false; + cork_hash_table_put(server_table, (void *)server->port, (void *)server, &new, NULL, NULL); + return 0; } @@ -643,12 +978,14 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) if (ret == -1) { msg = "port is not available"; msg_len = 21; + destroy_server(server); + ss_free(server); } else { msg = "ok"; msg_len = 2; } - if (sendto(manager->fd, msg, msg_len, 0, (struct sockaddr *)&claddr, len) != 2) { + if (sendto(manager->fd, msg, msg_len, 0, (struct sockaddr *)&claddr, len) != msg_len) { ERROR("add_sendto"); } } else if (strcmp(action, "list") == 0) { @@ -656,15 +993,21 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) struct cork_hash_table_entry *entry; char buf[BUF_SIZE]; memset(buf, 0, BUF_SIZE); - sprintf(buf, "["); + strcpy(buf, "["); cork_hash_table_iterator_init(server_table, &iter); while ((entry = cork_hash_table_iterator_next(&iter)) != NULL) { struct server *server = (struct server *)entry->value; char *method = server->method ? server->method : manager->method; + char entry_buf[BUF_SIZE]; + size_t entry_pos = 0; size_t pos = strlen(buf); - size_t entry_len = strlen(server->port) + strlen(server->password) + strlen(method); - if (pos > BUF_SIZE - entry_len - 50) { + + if (append_list_entry(entry_buf, sizeof(entry_buf), &entry_pos, server, method) == -1) { + LOGE("list entry is too large"); + continue; + } + if (pos >= BUF_SIZE - entry_pos) { if (sendto(manager->fd, buf, pos, 0, (struct sockaddr *)&claddr, len) != pos) { ERROR("list_sendto"); @@ -672,8 +1015,7 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) memset(buf, 0, BUF_SIZE); pos = 0; } - sprintf(buf + pos, "\n\t{\"server_port\":\"%s\",\"password\":\"%s\",\"method\":\"%s\"},", - server->port, server->password, method); + memcpy(buf + pos, entry_buf, entry_pos + 1); } size_t pos = strlen(buf); @@ -719,33 +1061,55 @@ manager_recv_cb(EV_P_ ev_io *w, int revents) struct cork_hash_table_iterator server_iter; char buf[BUF_SIZE]; + size_t pos = 0; memset(buf, 0, BUF_SIZE); - sprintf(buf, "stat: {"); + if (append_string(buf, sizeof(buf), &pos, "stat: {") == -1) { + goto ERROR_MSG; + } cork_hash_table_iterator_init(server_table, &server_iter); while ((entry = cork_hash_table_iterator_next(&server_iter)) != NULL) { struct server *server = (struct server *)entry->value; - size_t pos = strlen(buf); - if (pos > BUF_SIZE / 2) { - buf[pos - 1] = '}'; + char entry_buf[64]; + int entry_len = snprintf(entry_buf, sizeof(entry_buf), + "\"%s\":%" PRIu64 ",", + server->port, server->traffic); + if (entry_len < 0 || (size_t)entry_len >= sizeof(entry_buf)) { + LOGE("ping entry is too large"); + continue; + } + + if (pos >= BUF_SIZE - (size_t)entry_len) { + if (pos > 7) { + buf[pos - 1] = '}'; + } else if (append_char(buf, sizeof(buf), &pos, '}') == -1) { + goto ERROR_MSG; + } if (sendto(manager->fd, buf, pos, 0, (struct sockaddr *)&claddr, len) != pos) { ERROR("ping_sendto"); } memset(buf, 0, BUF_SIZE); - } else { - sprintf(buf + pos, "\"%s\":%" PRIu64 ",", server->port, server->traffic); + pos = 0; + if (append_string(buf, sizeof(buf), &pos, "stat: {") == -1) { + goto ERROR_MSG; + } + } + + if (append_string(buf, sizeof(buf), &pos, entry_buf) == -1) { + LOGE("ping response is too large"); + continue; } } - size_t pos = strlen(buf); if (pos > 7) { buf[pos - 1] = '}'; } else { - buf[pos] = '}'; - pos++; + if (append_char(buf, sizeof(buf), &pos, '}') == -1) { + goto ERROR_MSG; + } } if (sendto(manager->fd, buf, pos, 0, (struct sockaddr *)&claddr, len) @@ -876,6 +1240,7 @@ main(int argc, char **argv) int mode = TCP_ONLY; int mtu = 0; int ipv6first = 0; + int timeout_secs = 0; #ifdef HAVE_SETRLIMIT static int nofile = 0; @@ -932,7 +1297,9 @@ main(int argc, char **argv) executable = optarg; break; case GETOPT_VAL_MTU: - mtu = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { + FATAL("invalid MTU"); + } break; case GETOPT_VAL_PLUGIN: plugin = optarg; @@ -993,7 +1360,9 @@ main(int argc, char **argv) exit(EXIT_SUCCESS); #ifdef HAVE_SETRLIMIT case 'n': - nofile = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { + FATAL("invalid nofile"); + } break; #endif case 'A': @@ -1084,6 +1453,10 @@ main(int argc, char **argv) if (timeout == NULL) { timeout = "60"; } + if (ss_parse_int(timeout, 1, INT_MAX, &timeout_secs) == -1) { + FATAL("invalid timeout"); + } + (void)timeout_secs; USE_SYSLOG(argv[0], pid_flags); if (pid_flags) { @@ -1201,7 +1574,7 @@ main(int argc, char **argv) if (dp != NULL) { while ((ep = readdir(dp)) != NULL) { size_t len = strlen(ep->d_name); - if (strcmp(ep->d_name + len - 3, "pid") == 0) { + if (len >= 3 && strcmp(ep->d_name + len - 3, "pid") == 0) { kill_server(working_dir, ep->d_name); if (verbose) LOGI("kill %s", ep->d_name); @@ -1219,8 +1592,19 @@ main(int argc, char **argv) for (i = 0; i < conf->port_password_num; i++) { struct server *server = ss_malloc(sizeof(struct server)); memset(server, 0, sizeof(struct server)); - strncpy(server->port, conf->port_password[i].port, 7); - strncpy(server->password, conf->port_password[i].password, 127); + if (copy_port(server->port, sizeof(server->port), + conf->port_password[i].port, + strlen(conf->port_password[i].port)) == -1) { + LOGE("invalid port_password port"); + ss_free(server); + continue; + } + if (strlen(conf->port_password[i].password) >= sizeof(server->password)) { + LOGE("port_password password is too long"); + ss_free(server); + continue; + } + strcpy(server->password, conf->port_password[i].password); add_server(&manager, server); } } @@ -1231,6 +1615,11 @@ main(int argc, char **argv) if (ip_addr.host == NULL || ip_addr.port == NULL) { struct sockaddr_un svaddr; + if (strlen(manager_address) >= sizeof(svaddr.sun_path)) { + ss_free(working_dir); + FATAL("manager unix socket path is too long"); + } + sfd = socket(AF_UNIX, SOCK_DGRAM, 0); /* Create server socket */ if (sfd == -1) { ss_free(working_dir); diff --git a/src/netutils.c b/src/netutils.c index 35b2dc18..1c925926 100644 --- a/src/netutils.c +++ b/src/netutils.c @@ -20,7 +20,10 @@ * . */ +#include +#include #include +#include #include @@ -53,6 +56,26 @@ extern int verbose; static const char valid_label_bytes[] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; +static int +parse_numeric_port(const char *port, in_port_t *port_out) +{ + char *endptr; + unsigned long value; + + if (port == NULL || *port == '\0') { + return -1; + } + + errno = 0; + value = strtoul(port, &endptr, 10); + if (errno == ERANGE || *endptr != '\0' || value > UINT16_MAX) { + return -1; + } + + *port_out = (in_port_t)value; + return 0; +} + int set_reuseport(int socket) { @@ -132,26 +155,34 @@ get_sockaddr(char *host, char *port, { struct cork_ip ip; if (cork_ip_init(&ip, host) != -1) { + in_port_t numeric_port = 0; + if (port != NULL && parse_numeric_port(port, &numeric_port) == -1) { + LOGE("invalid port: %s", port); + return -1; + } if (ip.version == 4) { struct sockaddr_in *addr = (struct sockaddr_in *)storage; addr->sin_family = AF_INET; inet_pton(AF_INET, host, &(addr->sin_addr)); if (port != NULL) { - addr->sin_port = htons(atoi(port)); + addr->sin_port = htons(numeric_port); } } else if (ip.version == 6) { struct sockaddr_in6 *addr = (struct sockaddr_in6 *)storage; addr->sin6_family = AF_INET6; inet_pton(AF_INET6, host, &(addr->sin6_addr)); if (port != NULL) { - addr->sin6_port = htons(atoi(port)); + addr->sin6_port = htons(numeric_port); } } return 0; } else { #ifdef __ANDROID__ extern int vpn; - assert(!vpn); // protecting DNS packets isn't supported yet + if (vpn) { + LOGE("protecting DNS packets isn't supported yet"); + return -1; + } #endif struct addrinfo hints; struct addrinfo *result, *rp; @@ -263,6 +294,8 @@ sockaddr_cmp_addr(struct sockaddr_storage *addr1, int validate_hostname(const char *hostname, const int hostname_len) { + const char *hostname_end; + if (hostname == NULL) return 0; @@ -272,14 +305,15 @@ validate_hostname(const char *hostname, const int hostname_len) if (hostname[0] == '.') return 0; + hostname_end = hostname + hostname_len; const char *label = hostname; - while (label < hostname + hostname_len) { - size_t label_len = hostname_len - (label - hostname); - const char *next_dot = strchr(label, '.'); + while (label < hostname_end) { + size_t label_len = hostname_end - label; + const char *next_dot = memchr(label, '.', label_len); if (next_dot != NULL) label_len = next_dot - label; - if (label + label_len > hostname + hostname_len) + if (label + label_len > hostname_end) return 0; if (label_len > 63 || label_len < 1) @@ -288,8 +322,12 @@ validate_hostname(const char *hostname, const int hostname_len) if (label[0] == '-' || label[label_len - 1] == '-') return 0; - if (strspn(label, valid_label_bytes) < label_len) - return 0; + for (size_t i = 0; i < label_len; i++) { + if (memchr(valid_label_bytes, label[i], + sizeof(valid_label_bytes) - 1) == NULL) { + return 0; + } + } label += label_len + 1; } diff --git a/src/plugin.c b/src/plugin.c index b67a9a4f..fe244af6 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -245,11 +245,6 @@ start_plugin(const char *plugin, #endif enum plugin_mode mode) { -#ifndef __MINGW32__ - char *new_path = NULL; - const char *current_path; - size_t new_path_len; -#endif int ret; if (plugin == NULL) @@ -259,29 +254,7 @@ start_plugin(const char *plugin, return 0; #ifndef __MINGW32__ - /* - * Add current dir to PATH, so we can search plugin in current dir - */ - env = cork_env_clone_current(); - current_path = cork_env_get(env, "PATH"); - if (current_path != NULL) { -#ifdef HAVE_GET_CURRENT_DIR_NAME - char *cwd = get_current_dir_name(); - if (cwd) { -#else - char cwd[PATH_MAX]; - if (getcwd(cwd, PATH_MAX) != NULL) { -#endif - new_path_len = strlen(current_path) + strlen(cwd) + 2; - new_path = ss_malloc(new_path_len); - snprintf(new_path, new_path_len, "%s:%s", cwd, current_path); -#ifdef HAVE_GET_CURRENT_DIR_NAME - free(cwd); -#endif - } - } - if (new_path != NULL) - cork_env_add(env, "PATH", new_path); + env = cork_env_clone_current(); #else sub_control_port = control_port; #endif @@ -292,9 +265,6 @@ start_plugin(const char *plugin, else ret = start_ss_plugin(plugin, plugin_opts, remote_host, remote_port, local_host, local_port, mode); -#ifndef __MINGW32__ - ss_free(new_path); -#endif env = NULL; return ret; } diff --git a/src/redir.c b/src/redir.c index 86b7238d..9ac716b7 100644 --- a/src/redir.c +++ b/src/redir.c @@ -897,6 +897,7 @@ main(int argc, char **argv) int pid_flags = 0; int mptcp = 0; int mtu = 0; + int timeout_secs = 0; char *user = NULL; char *local_port = NULL; char *local_addr = NULL; @@ -951,7 +952,9 @@ main(int argc, char **argv) fast_open = 1; break; case GETOPT_VAL_MTU: - mtu = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { + FATAL("invalid MTU"); + } LOGI("set MTU to %d", mtu); break; case GETOPT_VAL_MPTCP: @@ -976,16 +979,24 @@ main(int argc, char **argv) reuse_port = 1; break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: - tcp_incoming_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { + FATAL("invalid TCP incoming send buffer size"); + } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: - tcp_incoming_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { + FATAL("invalid TCP incoming receive buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: - tcp_outgoing_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { + FATAL("invalid TCP outgoing send buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: - tcp_outgoing_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { + FATAL("invalid TCP outgoing receive buffer size"); + } break; case 's': if (remote_num < MAX_REMOTE_NUM) { @@ -1023,7 +1034,9 @@ main(int argc, char **argv) break; #ifdef HAVE_SETRLIMIT case 'n': - nofile = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { + FATAL("invalid nofile"); + } break; #endif case 'u': @@ -1178,6 +1191,9 @@ main(int argc, char **argv) if (timeout == NULL) { timeout = "600"; } + if (ss_parse_int(timeout, 1, INT_MAX, &timeout_secs) == -1) { + FATAL("invalid timeout"); + } #ifdef HAVE_SETRLIMIT /* @@ -1260,6 +1276,7 @@ main(int argc, char **argv) char *remote_str = ss_malloc(buf_size); snprintf(remote_str, buf_size, "%s", remote_addr[0].host); + len = strlen(remote_str); for (int i = 1; i < remote_num; i++) { snprintf(remote_str + len, buf_size - len, "|%s", remote_addr[i].host); len = strlen(remote_str); @@ -1312,7 +1329,7 @@ main(int argc, char **argv) if (plugin != NULL) break; } - listen_ctx.timeout = atoi(timeout); + listen_ctx.timeout = timeout_secs; listen_ctx.mptcp = mptcp; struct ev_loop *loop = EV_DEFAULT; @@ -1354,8 +1371,11 @@ main(int argc, char **argv) FATAL("failed to resolve the provided hostname"); } struct sockaddr *addr = (struct sockaddr *)storage; - init_udprelay(local_addr, local_port, addr, - get_sockaddr_len(addr), mtu, crypto, listen_ctx_current->timeout, NULL); + if (init_udprelay(local_addr, local_port, addr, + get_sockaddr_len(addr), mtu, crypto, + listen_ctx_current->timeout, NULL) == -1) { + FATAL("failed to initialize UDP relay"); + } } if (mode == UDP_ONLY) { diff --git a/src/rule.c b/src/rule.c index 6f8428de..92b6d8a4 100644 --- a/src/rule.c +++ b/src/rule.c @@ -35,8 +35,6 @@ #include "rule.h" #include "utils.h" -static void free_rule(rule_t *); - rule_t * new_rule() { @@ -108,7 +106,9 @@ lookup_rule(const struct cork_dllist *rules, const char *name, size_t name_len) name_len = 0; } - cork_dllist_foreach_void(rules, curr, next) { + for (curr = cork_dllist_start(rules); !cork_dllist_is_end(rules, curr); + curr = next) { + next = curr->next; rule_t *rule = cork_container_of(curr, rule_t, entries); if (pcre2_match(rule->pattern_re, (PCRE2_SPTR)name, name_len, 0, 0, rule->match_data, NULL) >= 0) @@ -125,7 +125,7 @@ remove_rule(rule_t *rule) free_rule(rule); } -static void +void free_rule(rule_t *rule) { if (rule == NULL) diff --git a/src/rule.h b/src/rule.h index ae907f77..e5703f5c 100644 --- a/src/rule.h +++ b/src/rule.h @@ -37,18 +37,19 @@ #include typedef struct rule { + struct cork_dllist_item entries; + char *pattern; /* Runtime fields */ pcre2_code *pattern_re; pcre2_match_data *match_data; - - struct cork_dllist_item entries; } rule_t; void add_rule(struct cork_dllist *, rule_t *); int init_rule(rule_t *); rule_t *lookup_rule(const struct cork_dllist *, const char *, size_t); +void free_rule(rule_t *); void remove_rule(rule_t *); rule_t *new_rule(); int accept_rule_arg(rule_t *, const char *); diff --git a/src/server.c b/src/server.c index ef347a5c..409f9fba 100644 --- a/src/server.c +++ b/src/server.c @@ -208,6 +208,12 @@ stat_update_cb(EV_P_ ev_timer *watcher, int revents) memset(&svaddr, 0, sizeof(struct sockaddr_un)); svaddr.sun_family = AF_UNIX; + if (strlen(manager_addr) >= sizeof(svaddr.sun_path)) { + LOGE("manager unix socket path is too long"); + close(sfd); + unlink(claddr.sun_path); + return; + } strncpy(svaddr.sun_path, manager_addr, sizeof(svaddr.sun_path) - 1); if (sendto(sfd, resp, strlen(resp) + 1, 0, (struct sockaddr *)&svaddr, @@ -1031,8 +1037,13 @@ server_recv_cb(EV_P_ ev_io *w, int revents) int offset = 0; int need_query = 0; + if (server->buf->len < 1) { + report_addr(server->fd, "missing address type"); + stop_server(EV_A_ server); + return; + } char atyp = server->buf->data[offset++]; - char host[255] = { 0 }; + char host[256] = { 0 }; uint16_t port = 0; struct addrinfo info; struct sockaddr_storage storage; @@ -1063,9 +1074,15 @@ server_recv_cb(EV_P_ ev_io *w, int revents) info.ai_addr = (struct sockaddr *)addr; } else if ((atyp & ADDRTYPE_MASK) == 3) { // Domain name + if (server->buf->len < offset + 1) { + report_addr(server->fd, "missing host name length"); + stop_server(EV_A_ server); + return; + } uint8_t name_len = *(uint8_t *)(server->buf->data + offset); if (name_len + 4 <= server->buf->len) { memcpy(host, server->buf->data + offset + 1, name_len); + host[name_len] = '\0'; offset += name_len + 1; } else { report_addr(server->fd, "invalid host name length"); @@ -1791,6 +1808,7 @@ main(int argc, char **argv) int pid_flags = 0; int mptcp = 0; int mtu = 0; + int timeout_secs = 0; char *user = NULL; char *password = NULL; char *key = NULL; @@ -1861,7 +1879,9 @@ main(int argc, char **argv) manager_addr = optarg; break; case GETOPT_VAL_MTU: - mtu = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { + FATAL("invalid MTU"); + } LOGI("set MTU to %d", mtu); break; case GETOPT_VAL_PLUGIN: @@ -1882,16 +1902,24 @@ main(int argc, char **argv) reuse_port = 1; break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: - tcp_incoming_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { + FATAL("invalid TCP incoming send buffer size"); + } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: - tcp_incoming_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { + FATAL("invalid TCP incoming receive buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: - tcp_outgoing_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { + FATAL("invalid TCP outgoing send buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: - tcp_outgoing_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { + FATAL("invalid TCP outgoing receive buffer size"); + } break; #ifdef USE_NFTABLES case GETOPT_VAL_NFTABLES_SETS: @@ -1937,7 +1965,9 @@ main(int argc, char **argv) break; #ifdef HAVE_SETRLIMIT case 'n': - nofile = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { + FATAL("invalid nofile"); + } break; #endif case 'u': @@ -2142,6 +2172,9 @@ main(int argc, char **argv) if (timeout == NULL) { timeout = "60"; } + if (ss_parse_int(timeout, 1, INT_MAX, &timeout_secs) == -1) { + FATAL("invalid timeout"); + } #ifdef HAVE_SETRLIMIT /* @@ -2313,7 +2346,7 @@ main(int argc, char **argv) listen_ctx_t *listen_ctx = &listen_ctx_list[i]; // Setup proxy context - listen_ctx->timeout = atoi(timeout); + listen_ctx->timeout = timeout_secs; listen_ctx->fd = listenfd; listen_ctx->iface = iface; listen_ctx->loop = loop; @@ -2345,7 +2378,7 @@ main(int argc, char **argv) else LOGI("udp server listening at %s:%s", host ? host : "0.0.0.0", port); // Setup UDP - int err = init_udprelay(host, port, mtu, crypto, atoi(timeout), iface); + int err = init_udprelay(host, port, mtu, crypto, timeout_secs, iface); if (err == -1) continue; num_listen_ctx++; diff --git a/src/stream.c b/src/stream.c index 849c5118..f5e0a956 100644 --- a/src/stream.c +++ b/src/stream.c @@ -358,19 +358,6 @@ stream_encrypt(buffer_t *plaintext, cipher_ctx_t *cipher_ctx, size_t capacity) cipher_t *cipher = cipher_ctx->cipher; - // In-place fast path for non-Salsa20 ciphers after init. - // mbedtls_cipher_update supports output == input for CFB/CTR stream modes. - if (cipher_ctx->init && cipher->method < SALSA20) { - size_t out_len = plaintext->len; - int err = cipher_ctx_update(cipher_ctx, - (uint8_t *)plaintext->data, &out_len, - (const uint8_t *)plaintext->data, plaintext->len); - if (err) - return CRYPTO_ERROR; - plaintext->len = out_len; - return CRYPTO_OK; - } - static buffer_t tmp = { 0, 0, 0, NULL }; int err = CRYPTO_OK; @@ -500,29 +487,6 @@ stream_decrypt(buffer_t *ciphertext, cipher_ctx_t *cipher_ctx, size_t capacity) cipher_t *cipher = cipher_ctx->cipher; - // In-place fast path for non-Salsa20 ciphers after init. - // mbedtls_cipher_update supports output == input for CFB/CTR stream modes. - if (cipher_ctx->init && cipher->method < SALSA20) { - if (ciphertext->len <= 0) - return CRYPTO_NEED_MORE; - size_t out_len = ciphertext->len; - int err = cipher_ctx_update(cipher_ctx, - (uint8_t *)ciphertext->data, &out_len, - (const uint8_t *)ciphertext->data, ciphertext->len); - if (err) - return CRYPTO_ERROR; - ciphertext->len = out_len; - if (cipher_ctx->init == 1 && cipher->method >= RC4_MD5) { - if (ppbloom_check((void *)cipher_ctx->nonce, cipher->nonce_len) == 1) { - LOGE("crypto: stream: repeat IV detected"); - return CRYPTO_ERROR; - } - ppbloom_add((void *)cipher_ctx->nonce, cipher->nonce_len); - cipher_ctx->init = 2; - } - return CRYPTO_OK; - } - static buffer_t tmp = { 0, 0, 0, NULL }; int err = CRYPTO_OK; @@ -561,11 +525,20 @@ stream_decrypt(buffer_t *ciphertext, cipher_ctx_t *cipher_ctx, size_t capacity) cipher_ctx->counter = 0; cipher_ctx->init = 1; + /* + * Register the IV as soon as it is known to be fresh. Deferring the + * add until after the payload is decrypted leaves a window in which a + * peer that sends exactly nonce_len bytes returns CRYPTO_NEED_MORE + * below with the IV checked but not yet recorded, so a concurrent + * connection replaying the same IV would pass ppbloom_check(). + */ if (cipher->method >= RC4_MD5) { if (ppbloom_check((void *)nonce, nonce_len) == 1) { LOGE("crypto: stream: repeat IV detected"); return CRYPTO_ERROR; } + ppbloom_add((void *)nonce, nonce_len); + cipher_ctx->init = 2; } } @@ -606,18 +579,6 @@ stream_decrypt(buffer_t *ciphertext, cipher_ctx_t *cipher_ctx, size_t capacity) dump("CIPHER", ciphertext->data, ciphertext->len); #endif - // Add to bloom filter - if (cipher_ctx->init == 1) { - if (cipher->method >= RC4_MD5) { - if (ppbloom_check((void *)cipher_ctx->nonce, cipher->nonce_len) == 1) { - LOGE("crypto: stream: repeat IV detected"); - return CRYPTO_ERROR; - } - ppbloom_add((void *)cipher_ctx->nonce, cipher->nonce_len); - cipher_ctx->init = 2; - } - } - bswap_data(ciphertext, plaintext); ciphertext->len = plaintext->len; diff --git a/src/tunnel.c b/src/tunnel.c index 9f0dd578..4640c561 100644 --- a/src/tunnel.c +++ b/src/tunnel.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,28 @@ static void close_and_free_remote(EV_P_ remote_t *remote); static void free_server(server_t *server); static void close_and_free_server(EV_P_ server_t *server); +static int +validate_tunnel_addr(const ss_addr_t *addr) +{ + struct cork_ip ip; + uint16_t port; + + if (addr->host == NULL || addr->port == NULL) { + return -1; + } + if (ss_parse_uint16_port(addr->port, &port) == -1) { + return -1; + } + if (cork_ip_init(&ip, addr->host) == -1) { + size_t host_len = strlen(addr->host); + if (!validate_hostname(addr->host, host_len)) { + return -1; + } + } + + return 0; +} + #ifdef __ANDROID__ int vpn = 0; #endif @@ -410,7 +433,12 @@ remote_send_cb(EV_P_ ev_io *w, int revents) if (r == 0) { remote_send_ctx->connected = 1; - assert(remote->buf->len == 0); + if (remote->buf->len != 0) { + LOGE("unexpected pending tunnel data"); + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } buffer_t *abuf = remote->buf; ss_addr_t *sa = &server->destaddr; @@ -447,13 +475,26 @@ remote_send_cb(EV_P_ ev_io *w, int revents) // send as domain int host_len = strlen(sa->host); + if (host_len > UINT8_MAX) { + LOGE("tunnel host name is too long"); + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } abuf->data[abuf->len++] = 3; abuf->data[abuf->len++] = host_len; memcpy(abuf->data + abuf->len, sa->host, host_len); abuf->len += host_len; } - uint16_t port = htons(atoi(sa->port)); + uint16_t port_num; + if (ss_parse_uint16_port(sa->port, &port_num) == -1) { + LOGE("invalid tunnel port"); + close_and_free_remote(EV_A_ remote); + close_and_free_server(EV_A_ server); + return; + } + uint16_t port = htons(port_num); memcpy(abuf->data + abuf->len, &port, 2); abuf->len += 2; @@ -889,6 +930,7 @@ main(int argc, char **argv) int pid_flags = 0; int mptcp = 0; int mtu = 0; + int timeout_secs = 0; char *user = NULL; char *local_port = NULL; char *local_addr = NULL; @@ -949,7 +991,9 @@ main(int argc, char **argv) fast_open = 1; break; case GETOPT_VAL_MTU: - mtu = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) { + FATAL("invalid MTU"); + } LOGI("set MTU to %d", mtu); break; case GETOPT_VAL_MPTCP: @@ -974,16 +1018,24 @@ main(int argc, char **argv) reuse_port = 1; break; case GETOPT_VAL_TCP_INCOMING_SNDBUF: - tcp_incoming_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_sndbuf) == -1) { + FATAL("invalid TCP incoming send buffer size"); + } break; case GETOPT_VAL_TCP_INCOMING_RCVBUF: - tcp_incoming_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_incoming_rcvbuf) == -1) { + FATAL("invalid TCP incoming receive buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_SNDBUF: - tcp_outgoing_sndbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_sndbuf) == -1) { + FATAL("invalid TCP outgoing send buffer size"); + } break; case GETOPT_VAL_TCP_OUTGOING_RCVBUF: - tcp_outgoing_rcvbuf = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &tcp_outgoing_rcvbuf) == -1) { + FATAL("invalid TCP outgoing receive buffer size"); + } break; case 's': if (remote_num < MAX_REMOTE_NUM) { @@ -1033,7 +1085,9 @@ main(int argc, char **argv) break; #ifdef HAVE_SETRLIMIT case 'n': - nofile = atoi(optarg); + if (ss_parse_int(optarg, 0, INT_MAX, &nofile) == -1) { + FATAL("invalid nofile"); + } break; #endif case 'v': @@ -1223,6 +1277,9 @@ main(int argc, char **argv) if (timeout == NULL) { timeout = "60"; } + if (ss_parse_int(timeout, 1, INT_MAX, &timeout_secs) == -1) { + FATAL("invalid timeout"); + } #ifdef HAVE_SETRLIMIT /* @@ -1269,6 +1326,9 @@ main(int argc, char **argv) if (tunnel_addr.port == NULL) { FATAL("tunnel port is not defined"); } + if (validate_tunnel_addr(&tunnel_addr) == -1) { + FATAL("invalid tunnel address"); + } #ifdef __MINGW32__ // Listen on plugin control port @@ -1310,6 +1370,7 @@ main(int argc, char **argv) char *remote_str = ss_malloc(buf_size); snprintf(remote_str, buf_size, "%s", remote_addr[0].host); + len = strlen(remote_str); for (int i = 1; i < remote_num; i++) { snprintf(remote_str + len, buf_size - len, "|%s", remote_addr[i].host); len = strlen(remote_str); @@ -1371,7 +1432,7 @@ main(int argc, char **argv) if (plugin != NULL) break; } - listen_ctx.timeout = atoi(timeout); + listen_ctx.timeout = timeout_secs; listen_ctx.iface = iface; listen_ctx.mptcp = mptcp; @@ -1408,8 +1469,10 @@ main(int argc, char **argv) FATAL("failed to resolve the provided hostname"); } struct sockaddr *addr = (struct sockaddr *)storage; - init_udprelay(local_addr, local_port, addr, get_sockaddr_len(addr), - tunnel_addr, mtu, crypto, listen_ctx.timeout, iface); + if (init_udprelay(local_addr, local_port, addr, get_sockaddr_len(addr), + tunnel_addr, mtu, crypto, listen_ctx.timeout, iface) == -1) { + FATAL("failed to initialize UDP relay"); + } } if (mode == UDP_ONLY) { diff --git a/src/udprelay.c b/src/udprelay.c index 66a62974..6b7c916e 100644 --- a/src/udprelay.c +++ b/src/udprelay.c @@ -280,6 +280,7 @@ parse_udprelay_header(const char *buf, const size_t buf_len, char tmp[MAX_HOSTNAME_LEN] = { 0 }; struct cork_ip ip; memcpy(tmp, buf + offset + 1, name_len); + tmp[name_len] = '\0'; if (cork_ip_init(&ip, tmp) != -1) { if (ip.version == 4) { struct sockaddr_in *addr = (struct sockaddr_in *)storage; @@ -288,14 +289,18 @@ parse_udprelay_header(const char *buf, const size_t buf_len, addr->sin_family = AF_INET; } else if (ip.version == 6) { struct sockaddr_in6 *addr = (struct sockaddr_in6 *)storage; - inet_pton(AF_INET, tmp, &(addr->sin6_addr)); + inet_pton(AF_INET6, tmp, &(addr->sin6_addr)); memcpy(&addr->sin6_port, buf + offset + 1 + name_len, sizeof(uint16_t)); addr->sin6_family = AF_INET6; } + } else if (!validate_hostname(tmp, name_len)) { + LOGE("[udp] invalid host name"); + return 0; } } if (host != NULL) { memcpy(host, buf + offset + 1, name_len); + host[name_len] = '\0'; } offset += 1 + name_len; } @@ -1060,6 +1065,10 @@ server_recv_cb(EV_P_ ev_io *w, int revents) #ifdef __ANDROID__ tx += buf->len; #endif + if (buf->len < 3) { + LOGE("[udp] invalid SOCKS5 UDP header"); + goto CLEAN_UP; + } uint8_t frag = *(uint8_t *)(buf->data + 2); offset += 3; #endif @@ -1124,7 +1133,11 @@ server_recv_cb(EV_P_ ev_io *w, int revents) char addr_header[MAX_ADDR_HEADER_SIZE] = { 0 }; char *host = server_ctx->tunnel_addr.host; char *port = server_ctx->tunnel_addr.port; - uint16_t port_num = (uint16_t)atoi(port); + uint16_t port_num = 0; + if (ss_parse_uint16_port(port, &port_num) == -1) { + LOGE("[udp] invalid tunnel port"); + goto CLEAN_UP; + } uint16_t port_net_num = htons(port_num); int addr_header_len = 0; @@ -1161,6 +1174,10 @@ server_recv_cb(EV_P_ ev_io *w, int revents) // send as domain int host_len = strlen(host); + if (host_len > UINT8_MAX) { + LOGE("[udp] tunnel host name is too long"); + goto CLEAN_UP; + } addr_header[addr_header_len++] = 3; addr_header[addr_header_len++] = host_len; memcpy(addr_header + addr_header_len, host, host_len); @@ -1428,7 +1445,13 @@ server_recv_cb(EV_P_ ev_io *w, int revents) query_ctx->remote_ctx = remote_ctx; } - resolv_start(host, htons(atoi(port)), resolv_cb, resolv_free_cb, query_ctx); + int port_num = 0; + if (ss_parse_int(port, 0, UINT16_MAX, &port_num) == -1) { + LOGE("[udp] invalid target port"); + resolv_free_cb(query_ctx); + goto CLEAN_UP; + } + resolv_start(host, htons((uint16_t)port_num), resolv_cb, resolv_free_cb, query_ctx); } #endif @@ -1465,6 +1488,10 @@ init_udprelay(const char *server_host, const char *server_port, // Initialize MTU if (mtu > 0) { + if (mtu <= PACKET_HEADER_SIZE) { + LOGE("MTU must be greater than %d", PACKET_HEADER_SIZE); + return -1; + } packet_size = mtu - PACKET_HEADER_SIZE; buf_size = packet_size * 2; } diff --git a/src/utils.c b/src/utils.c index d3ff2aba..039089b6 100644 --- a/src/utils.c +++ b/src/utils.c @@ -27,9 +27,10 @@ #include #include #include +#include +#include #ifndef __MINGW32__ #include -#include #include #include #else @@ -104,6 +105,40 @@ ss_isnumeric(const char *s) return *s == '\0'; } +int +ss_parse_int(const char *s, int min_value, int max_value, int *out) +{ + char *endptr; + long value; + + if (s == NULL || *s == '\0' || out == NULL || min_value > max_value) { + return -1; + } + + errno = 0; + value = strtol(s, &endptr, 10); + if (errno == ERANGE || endptr == s || *endptr != '\0' || + value < min_value || value > max_value) { + return -1; + } + + *out = (int)value; + return 0; +} + +int +ss_parse_uint16_port(const char *s, uint16_t *out) +{ + int value; + + if (out == NULL || ss_parse_int(s, 1, UINT16_MAX, &value) == -1) { + return -1; + } + + *out = (uint16_t)value; + return 0; +} + /* * setuid() and setgid() for a specified user. */ @@ -214,16 +249,16 @@ run_as(const char *user) char * ss_strndup(const char *s, size_t n) { - size_t len = strlen(s); char *ret; - if (len <= n) { - return strdup(s); + size_t len = 0; + while (len < n && s[len] != '\0') { + len++; } - ret = ss_malloc(n + 1); - strncpy(ret, s, n); - ret[n] = '\0'; + ret = ss_malloc(len + 1); + memcpy(ret, s, len); + ret[len] = '\0'; return ret; } diff --git a/src/utils.h b/src/utils.h index 1df24c16..a52bd253 100644 --- a/src/utils.h +++ b/src/utils.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -218,6 +219,8 @@ void ERROR(const char *s); char *ss_itoa(int i); int ss_isnumeric(const char *s); +int ss_parse_int(const char *s, int min_value, int max_value, int *out); +int ss_parse_uint16_port(const char *s, uint16_t *out); int run_as(const char *user); void FATAL(const char *msg); void usage(void); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d2a0199d..29f4afbe 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -13,6 +13,10 @@ if(WITH_EMBEDDED_SRC) include_directories(${PROJECT_SOURCE_DIR}/libbloom) endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_program(VALGRIND_EXECUTABLE NAMES valgrind) +endif() + # Helper function to add a unit test function(ss_add_test name sources libs) add_executable(${name} ${sources}) @@ -21,6 +25,24 @@ function(ss_add_test name sources libs) # Keep assertions enabled for tests even in Release builds. target_compile_options(${name} PRIVATE -UNDEBUG) add_test(NAME ${name} COMMAND ${name}) + set_tests_properties(${name} PROPERTIES LABELS "unit") + + if(VALGRIND_EXECUTABLE) + add_test(NAME memcheck_${name} + COMMAND ${VALGRIND_EXECUTABLE} + --tool=memcheck + --leak-check=full + --show-leak-kinds=definite,indirect + --errors-for-leak-kinds=definite,indirect + --error-exitcode=1 + --track-origins=yes + --quiet + $) + set_tests_properties(memcheck_${name} + PROPERTIES + LABELS "memcheck" + TIMEOUT 120) + endif() endfunction() # test_base64 - no external deps @@ -83,6 +105,17 @@ ss_add_test(test_rule "test_rule.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" "${TEST_RULE_LIBS}") +# test_acl - needs pcre, cork, and ipset +set(TEST_ACL_LIBS ${TEST_RULE_LIBS}) +if(WITH_EMBEDDED_SRC) + list(APPEND TEST_ACL_LIBS ipset) +else() + list(APPEND TEST_ACL_LIBS ${LIBCORKIPSET_SHARED}) +endif() +ss_add_test(test_acl + "test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.c;${PROJECT_SOURCE_DIR}/src/rule.c;${PROJECT_SOURCE_DIR}/src/utils.c" + "${TEST_ACL_LIBS}") + # test_jconf - needs cork and json set(TEST_JCONF_LIBS ${LIBSODIUM_SHARED} @@ -123,3 +156,4 @@ ss_add_test(test_crypto add_test(NAME test_ss_setup COMMAND bash "${PROJECT_SOURCE_DIR}/tests/test_ss_setup.sh" WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}") +set_tests_properties(test_ss_setup PROPERTIES LABELS "integration") diff --git a/tests/test_acl.c b/tests/test_acl.c new file mode 100644 index 00000000..080bd95e --- /dev/null +++ b/tests/test_acl.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include + +#include "acl.h" + +int verbose = 0; + +static void +test_invalid_regex_is_not_installed(void) +{ + char path[] = "/tmp/ss_acl_test.XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + + FILE *f = fdopen(fd, "w"); + assert(f != NULL); + fprintf(f, "[black_list]\n"); + fprintf(f, "[invalid\n"); + fprintf(f, "^blocked\\.example$\n"); + fprintf(f, "[outbound_block_list]\n"); + fprintf(f, "[also-invalid\n"); + fprintf(f, "^outbound\\.example$\n"); + fclose(f); + + assert(init_acl(path) == 0); + assert(acl_match_host("does-not-crash.example") == 0); + assert(acl_match_host("blocked.example") == 1); + assert(outbound_block_match_host("does-not-crash.example") == 0); + assert(outbound_block_match_host("outbound.example") == 1); + free_acl(); + + unlink(path); +} + +static void +test_long_acl_line_is_discarded(void) +{ + char path[] = "/tmp/ss_acl_test.XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + + FILE *f = fdopen(fd, "w"); + assert(f != NULL); + fprintf(f, "[black_list]\n"); + for (int i = 0; i < 400; i++) { + fputc('a', f); + } + fprintf(f, "\n"); + fprintf(f, "^kept\\.example$\n"); + fclose(f); + + assert(init_acl(path) == 0); + assert(acl_match_host("kept.example") == 1); + free_acl(); + + unlink(path); +} + +static void +test_invalid_cidr_is_discarded(void) +{ + char path[] = "/tmp/ss_acl_test.XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + + FILE *f = fdopen(fd, "w"); + assert(f != NULL); + fprintf(f, "[black_list]\n"); + fprintf(f, "1.2.3.0/999\n"); + fprintf(f, "2.2.2.2/-1\n"); + fprintf(f, "3.3.3.3/abc\n"); + fprintf(f, "4.4.4.0/24\n"); + fprintf(f, "^path/with/slash\\.example$\n"); + fclose(f); + + assert(init_acl(path) == 0); + assert(acl_match_host("1.2.3.1") == 0); + assert(acl_match_host("2.2.2.2") == 0); + assert(acl_match_host("3.3.3.3") == 0); + assert(acl_match_host("4.4.4.8") == 1); + assert(acl_match_host("path/with/slash.example") == 1); + free_acl(); + + unlink(path); +} + +int +main(void) +{ + test_invalid_regex_is_not_installed(); + test_long_acl_line_is_discarded(); + test_invalid_cidr_is_discarded(); + return 0; +} diff --git a/tests/test_buffer.c b/tests/test_buffer.c index 5d5639f9..e021df76 100644 --- a/tests/test_buffer.c +++ b/tests/test_buffer.c @@ -20,7 +20,7 @@ test_balloc(void) memset(&buf, 0, sizeof(buf)); int ret = balloc(&buf, 100); - assert(ret == 0); + assert(ret == 100); (void)ret; assert(buf.data != NULL); assert(buf.capacity >= 100); @@ -43,7 +43,7 @@ test_brealloc(void) /* Grow the buffer */ int ret = brealloc(&buf, 10, 200); - assert(ret == 0); + assert(ret == 200); (void)ret; assert(buf.capacity >= 200); assert(buf.len == 10); @@ -70,7 +70,7 @@ test_bprepend(void) dst.len = 4; int ret = bprepend(&dst, &src, 200); - assert(ret == 0); + assert(ret == 10); (void)ret; assert(dst.len == 10); assert(memcmp(dst.data, "HEADERBODY", 10) == 0); diff --git a/tests/test_crypto.c b/tests/test_crypto.c index 7359ad0c..1de03236 100644 --- a/tests/test_crypto.c +++ b/tests/test_crypto.c @@ -10,6 +10,8 @@ int verbose = 0; #include "crypto.h" +#include "ppbloom.h" +#include "utils.h" /* Provide nonce_cache symbol needed by crypto.c */ struct cache *nonce_cache = NULL; @@ -43,6 +45,8 @@ test_crypto_derive_key(void) { uint8_t key[32]; + assert(crypto_derive_key(NULL, key, 32) == 0); + /* derive_key should produce deterministic output from a password */ int ret = crypto_derive_key("password", key, 32); assert(ret == 32); @@ -148,6 +152,76 @@ test_crypto_parse_key(void) } } +static void +test_aead_repeat_salt_rejection_releases_context(void) +{ + crypto_t *crypto = crypto_init("password", NULL, "aes-128-gcm"); + assert(crypto != NULL); + + buffer_t packet; + memset(&packet, 0, sizeof(packet)); + balloc(&packet, 64); + memcpy(packet.data, "payload", 7); + packet.len = 7; + + assert(crypto->encrypt_all(&packet, crypto->cipher, 128) == CRYPTO_OK); + assert(crypto->decrypt_all(&packet, crypto->cipher, 128) == CRYPTO_ERROR); + + bfree(&packet); + ppbloom_free(); + ss_free(crypto->cipher); + ss_free(crypto); +} + +/* + * Round-trip a multi-segment stream through a stream cipher. + * + * The segments deliberately have lengths that are not a multiple of the + * cipher block size, and there is deliberately more than one of them: only + * the first segment carries the nonce, so a codec that mishandles the + * post-nonce steady state still round-trips a single segment correctly. + */ +static void +test_stream_multi_segment_roundtrip(const char *method) +{ + static const char *segments[] = { + "first-segment", + "second", + "a third segment that is a good deal longer than one block", + }; + + crypto_t *crypto = crypto_init("password", NULL, method); + assert(crypto != NULL); + + cipher_ctx_t enc_ctx, dec_ctx; + crypto->ctx_init(crypto->cipher, &enc_ctx, 1); + crypto->ctx_init(crypto->cipher, &dec_ctx, 0); + + for (size_t i = 0; i < sizeof(segments) / sizeof(segments[0]); i++) { + size_t len = strlen(segments[i]); + + buffer_t buf; + memset(&buf, 0, sizeof(buf)); + balloc(&buf, 2048); + memcpy(buf.data, segments[i], len); + buf.len = len; + + assert(crypto->encrypt(&buf, &enc_ctx, 2048) == CRYPTO_OK); + assert(crypto->decrypt(&buf, &dec_ctx, 2048) == CRYPTO_OK); + + assert(buf.len == len); + assert(memcmp(buf.data, segments[i], len) == 0); + + bfree(&buf); + } + + crypto->ctx_release(&enc_ctx); + crypto->ctx_release(&dec_ctx); + ppbloom_free(); + ss_free(crypto->cipher); + ss_free(crypto); +} + int main(void) { @@ -160,5 +234,14 @@ main(void) test_crypto_hkdf(); test_crypto_hkdf_extract(); test_crypto_parse_key(); + test_aead_repeat_salt_rejection_releases_context(); + + /* mbedTLS-backed ciphers and libsodium-backed ciphers use different + * code paths in stream.c, so cover both. */ + test_stream_multi_segment_roundtrip("aes-256-cfb"); + test_stream_multi_segment_roundtrip("aes-256-ctr"); + test_stream_multi_segment_roundtrip("camellia-128-cfb"); + test_stream_multi_segment_roundtrip("chacha20-ietf"); + test_stream_multi_segment_roundtrip("salsa20"); return 0; } diff --git a/tests/test_jconf.c b/tests/test_jconf.c index 797112e6..bda25696 100644 --- a/tests/test_jconf.c +++ b/tests/test_jconf.c @@ -3,8 +3,11 @@ #endif #include +#include #include #include +#include +#include int verbose = 0; @@ -73,6 +76,98 @@ test_parse_addr_ipv6_no_port(void) free_addr(&addr); } +static void +test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten(void) +{ + ss_addr_t addr; + memset(&addr, 0, sizeof(addr)); + parse_addr("[::1", &addr); + assert(addr.host != NULL); + assert(addr.port == NULL); + assert(strcmp(addr.host, "[::1") == 0); + free_addr(&addr); +} + +static void +test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(void) +{ + ss_addr_t addr; + memset(&addr, 0, sizeof(addr)); + parse_addr("bad::host", &addr); + assert(addr.host != NULL); + assert(addr.port == NULL); + assert(strcmp(addr.host, "bad::host") == 0); + free_addr(&addr); +} + +static void +test_read_jconf_preserves_64bit_integer_strings(void) +{ + char path[] = "/tmp/ss_jconf_test.XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + + FILE *f = fdopen(fd, "w"); + assert(f != NULL); + fprintf(f, "{\n"); + fprintf(f, "\"server\":\"127.0.0.1\",\n"); + fprintf(f, "\"server_port\":4294967296,\n"); + fprintf(f, "\"password\":\"password\",\n"); + fprintf(f, "\"method\":\"aes-128-gcm\"\n"); + fprintf(f, "}\n"); + fclose(f); + + jconf_t *conf = read_jconf(path); + assert(conf != NULL); + assert(conf->remote_port != NULL); + assert(strcmp(conf->remote_port, "4294967296") == 0); + + unlink(path); +} + +static void +write_minimal_config(FILE *f, const char *extra_line) +{ + fprintf(f, "{\n"); + fprintf(f, "\"server\":\"127.0.0.1\",\n"); + fprintf(f, "\"server_port\":8388,\n"); + fprintf(f, "\"password\":\"password\",\n"); + fprintf(f, "\"method\":\"aes-128-gcm\""); + if (extra_line != NULL) { + fprintf(f, ",\n%s", extra_line); + } + fprintf(f, "\n}\n"); +} + +#ifndef __MINGW32__ +static void +test_read_jconf_rejects_out_of_range_int_options(void) +{ + char path[] = "/tmp/ss_jconf_test.XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + + FILE *f = fdopen(fd, "w"); + assert(f != NULL); + write_minimal_config(f, "\"tcp_incoming_sndbuf\":2147483648"); + fclose(f); + + pid_t pid = fork(); + assert(pid >= 0); + if (pid == 0) { + read_jconf(path); + _exit(0); + } + + int status; + assert(waitpid(pid, &status, 0) == pid); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) != 0); + + unlink(path); +} +#endif + int main(void) { @@ -81,5 +176,11 @@ main(void) test_parse_addr_hostname_with_port(); test_parse_addr_no_port(); test_parse_addr_ipv6_no_port(); + test_parse_addr_malformed_bracketed_ipv6_is_not_rewritten(); + test_parse_addr_invalid_ipv6_like_host_is_not_rewritten(); + test_read_jconf_preserves_64bit_integer_strings(); +#ifndef __MINGW32__ + test_read_jconf_rejects_out_of_range_int_options(); +#endif return 0; } diff --git a/tests/test_netutils.c b/tests/test_netutils.c index beb99dd8..d75153d8 100644 --- a/tests/test_netutils.c +++ b/tests/test_netutils.c @@ -102,6 +102,28 @@ test_validate_hostname(void) memset(long_name, 'a', 259); long_name[259] = '\0'; assert(validate_hostname(long_name, 259) == 0); + + /* Explicit length must be honored; callers may pass non-NUL data. */ + char raw_name[3] = { 'a', 'b', 'c' }; + assert(validate_hostname(raw_name, 3) == 1); +} + +static void +test_get_sockaddr_rejects_invalid_ip_literal_ports(void) +{ + struct sockaddr_storage storage; + struct sockaddr_in *addr4 = (struct sockaddr_in *)&storage; + + memset(&storage, 0, sizeof(storage)); + assert(get_sockaddr("127.0.0.1", "8388", &storage, 0, 0) == 0); + assert(addr4->sin_family == AF_INET); + assert(ntohs(addr4->sin_port) == 8388); + + memset(&storage, 0, sizeof(storage)); + assert(get_sockaddr("127.0.0.1", "70000", &storage, 0, 0) == -1); + + memset(&storage, 0, sizeof(storage)); + assert(get_sockaddr("127.0.0.1", "12x", &storage, 0, 0) == -1); } int @@ -111,5 +133,6 @@ main(void) test_sockaddr_cmp(); test_sockaddr_cmp_addr(); test_validate_hostname(); + test_get_sockaddr_rejects_invalid_ip_literal_ports(); return 0; } diff --git a/tests/test_rule.c b/tests/test_rule.c index 8c14b27a..2bfde205 100644 --- a/tests/test_rule.c +++ b/tests/test_rule.c @@ -37,8 +37,7 @@ test_accept_rule_arg(void) assert(ret == -1); (void)ret; - free(rule->pattern); - free(rule); + free_rule(rule); } static void @@ -52,12 +51,7 @@ test_init_rule(void) (void)ret; assert(rule->pattern_re != NULL); - if (rule->match_data) - pcre2_match_data_free(rule->match_data); - if (rule->pattern_re) - pcre2_code_free(rule->pattern_re); - free(rule->pattern); - free(rule); + free_rule(rule); } static void @@ -70,8 +64,7 @@ test_init_rule_invalid(void) assert(ret == 0); /* Should fail */ (void)ret; - free(rule->pattern); - free(rule); + free_rule(rule); } static void @@ -104,14 +97,8 @@ test_lookup_rule(void) (void)found; /* Clean up */ - if (rule1->match_data) pcre2_match_data_free(rule1->match_data); - if (rule1->pattern_re) pcre2_code_free(rule1->pattern_re); - free(rule1->pattern); - free(rule1); - if (rule2->match_data) pcre2_match_data_free(rule2->match_data); - if (rule2->pattern_re) pcre2_code_free(rule2->pattern_re); - free(rule2->pattern); - free(rule2); + free_rule(rule1); + free_rule(rule2); } int diff --git a/tests/test_utils.c b/tests/test_utils.c index 83046dab..7312fa20 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -64,6 +64,42 @@ test_ss_strndup(void) assert(s != NULL); assert(strcmp(s, "") == 0); free(s); + + char raw[3] = { 'a', 'b', 'c' }; + s = ss_strndup(raw, sizeof(raw)); + assert(s != NULL); + assert(strcmp(s, "abc") == 0); + free(s); +} + +static void +test_ss_parse_int(void) +{ + int value = -1; + uint16_t port = 0; + + assert(ss_parse_int("0", 0, 10, &value) == 0); + assert(value == 0); + + assert(ss_parse_int("10", 0, 10, &value) == 0); + assert(value == 10); + + assert(ss_parse_int("-1", 0, 10, &value) == -1); + assert(ss_parse_int("11", 0, 10, &value) == -1); + assert(ss_parse_int("12x", 0, 100, &value) == -1); + assert(ss_parse_int("", 0, 100, &value) == -1); + assert(ss_parse_int("999999999999999999999999", 0, INT_MAX, &value) == -1); + assert(ss_parse_int("1", 10, 0, &value) == -1); + assert(ss_parse_int("1", 0, 10, NULL) == -1); + + assert(ss_parse_uint16_port("1", &port) == 0); + assert(port == 1); + assert(ss_parse_uint16_port("65535", &port) == 0); + assert(port == 65535); + assert(ss_parse_uint16_port("0", &port) == -1); + assert(ss_parse_uint16_port("65536", &port) == -1); + assert(ss_parse_uint16_port("22x", &port) == -1); + assert(ss_parse_uint16_port("22", NULL) == -1); } int @@ -72,5 +108,6 @@ main(void) test_ss_itoa(); test_ss_isnumeric(); test_ss_strndup(); + test_ss_parse_int(); return 0; }