C++20 port 16/16: config parsing on arenas, unit tests in C++ (#3080)

* Rename test_jconf.c to test_jconf_cpp.cpp

* Parse the config file on arenas

read_jconf now reads the file and builds the JSON tree in a scratch arena
that is released before it returns, and copies the parsed strings into one
process-lifetime arena instead of a malloc per string. parse_addr and the
config parser share one address splitter.

Port test_jconf to C++ and cover every field copied out of the parse tree,
a re-read, and the densest file the size limit accepts.

* tests: rename remaining C unit tests to .cpp (no content changes)

* tests: port remaining C unit tests to the C++ test harness

Convert acl, acl_literal, aead2022, base64, buffer, cache (legacy
reference), crypto, ipset, ipset_reference, json, netutils, ppbloom,
process, ref, ssurl and utils tests to test_helpers.hpp. ctest names are
unchanged; every assertion is kept. Build the ported sources with the
strict C++ warning set.

* Run clang-tidy on every ported test

The test regex skipped file names with digits (test_aead2022_cpp.cpp,
test_base64_cpp.cpp); include them, fix the two optional accesses it then
reports, and build test_jconf with the strict warning set.

* docs: per-connection RSS on Linux against the C relay

* test_jconf: check read_jconf results before using them
This commit is contained in:
Max Lv authored and GitHub committed 2026-10-15 10:17:23 +08:00
1 parent f1f31c3c05
commit ee7539155d
41 files changed
+2300 -2070

No files matched your search

+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
run: |
set -o pipefail
tidy_status=0
run-clang-tidy-18 -quiet -p build -extra-arg=-Wno-unknown-warning-option "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$?
run-clang-tidy-18 -quiet -p build -extra-arg=-Wno-unknown-warning-option "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z0-9_]+_cpp)\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$?
grep 'warning:' tidy.log | sort -u > warnings.txt || true
count="$(wc -l < warnings.txt)"
echo "clang-tidy warnings: $count (max allowed: $MAX_WARNINGS)"
+1 -1
View File
@@ -80,7 +80,7 @@ Uses **uncrustify** with the config at `.uncrustify.cfg`. Key settings: 4-space
- **clang-tidy**: config in `.clang-tidy`. The build exports `compile_commands.json` automatically. Run locally:
```bash
run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z_]+_cpp)\.cpp)$'
run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_(core_[^/]+|[a-z0-9_]+_cpp)\.cpp)$'
```
On macOS with Homebrew LLVM, add `-extra-arg="-isysroot$(xcrun --show-sdk-path)"`. CI (`clang-tidy` job in `tests.yml`) enforces a warning-count ratchet via `MAX_WARNINGS` — lower it when fixing findings; never raise it without justification.
- **No-heap lint**: `scripts/check_no_heap.py --self-test` (also a ctest) rejects heap allocation in C++ sources under `src/`.
+8 -2
View File
@@ -113,7 +113,12 @@ Pools are reserved at startup and only touched slots become resident.
3. Event layer over libuv (`src/event/`).
4. Crypto codecs held by value: AEAD, stream and AEAD-2022.
5. Services: the cache on a fixed slab, `IntrusiveList` for ACL rules and
the manager, the pooled resolver.
the manager, the pooled resolver, and `read_jconf` on arenas (the file and
JSON tree in a scratch arena released before it returns, the parsed
strings in one process-lifetime arena). ACL rules and IP-set nodes still
come from `malloc` at startup: the IP-set trie frees nodes as it merges
prefixes, and its size depends on the ACL file, so a bump arena would
have to reserve for the worst case.
6. One TCP engine for ss-tunnel, ss-local, ss-redir and ss-server.
7. One UDP engine.
8. Library heaps for libuv, c-ares and Mbed TLS, `heap_seal()`, and the
@@ -122,7 +127,8 @@ Pools are reserved at startup and only touched slots become resident.
(`ss_local_start`/`stop`/`wait`/`destroy`) and the RAII
`shadowsocks/shadowsocks.hpp`.
10. Removal of `ss_event`, the C resolver, the C codecs (now test oracles),
uthash and `ss_list`.
uthash and `ss_list`, and the unit tests ported to C++
(`tests/test_helpers.hpp`).
## Allocation audit
+17 -2
View File
@@ -63,8 +63,23 @@ RSS with 1,000 established, idle connections through ss-local and ss-server
Each connection slot carries about 104 KiB of inline buffers, against roughly
58 KiB that the C relay allocated on demand. The pool is reserved virtually at
startup and only touched slots become resident. A shared buffer pool with read
backpressure would cut this; it is the main open trade-off of the rewrite.
startup and only the touched pages of a slot become resident.
The table above was taken on Apple Silicon, whose 16 KiB pages round every
touched buffer up to a whole page. The same test on Linux (4 KiB pages, GCC,
Alpine container, aes-256-gcm, one small request per connection) gives:
| | C server | C++ server | C local | C++ local |
|---|---:|---:|---:|---:|
| idle | 1,380 KiB | 1,812 KiB | 1,248 KiB | 1,684 KiB |
| 1,000 connections | 27,604 KiB | 37,248 KiB | 24,744 KiB | 34,236 KiB |
| per connection | 26.2 KiB | 35.4 KiB | 23.5 KiB | 32.6 KiB |
So the fixed slots cost about 9 KiB per connection more than C on Linux.
Construction does not zero-fill the buffers (a user-provided `FixedBuffer`
constructor changed nothing), so the difference is page granularity across
the separate inline buffers. A shared buffer pool with read backpressure
would cut it; it is the main open trade-off of the rewrite.
### UDP
+1 -1
View File
@@ -3,7 +3,7 @@
# violations, and also when it loses some, so the number here always goes
# down. Remove a file once it is rewritten on the src/core primitives.
src/ipset.cpp 3
src/jconf.cpp 16
src/jconf.cpp 6
src/json.cpp 2
src/local.cpp 1
src/manager.cpp 14
+135 -73
View File
@@ -34,6 +34,11 @@
#include "string.h"
#include "core.h"
#include "core/arena.hpp"
#include <memory>
#include <new>
#include <string_view>
#define check_json_value_type(value, expected_type, message) \
do { \
@@ -41,15 +46,115 @@
FATAL((message)); \
} while (0)
/*
* The parsed configuration lives as long as the process, so its strings come
* from one arena reserved when the file is read. The file text and the JSON
* tree use a second, scratch arena that is released before read_jconf
* returns. Nothing here goes back to malloc per string.
*/
namespace {
alignas(ss::MonotonicArena) unsigned char conf_arena_storage[sizeof(ss::MonotonicArena)];
ss::MonotonicArena *conf_arena;
// The arena is never destroyed: callers keep jconf_t pointers until exit.
ss::MonotonicArena &
config_arena(std::size_t bytes)
{
if (conf_arena == nullptr) {
conf_arena = ::new (conf_arena_storage) ss::MonotonicArena(); // heap-ok: placement into static storage
} else {
// A second read_jconf replaces the previous result (conf is static).
*conf_arena = ss::MonotonicArena();
}
if (!conf_arena->reserve(bytes)) {
FATAL("No enough memory.");
}
return *conf_arena;
}
char *
conf_strndup(std::string_view text)
{
auto copy = conf_arena->intern(text);
if (!copy) {
FATAL("No enough memory.");
}
return const_cast<char *>(copy->data());
}
// json-parser's allocator hooks. The scratch Region is calloc'ed and never
// reused within one parse, so `zero` already holds.
void *
scratch_alloc(size_t size, int, void *user)
{
auto bytes = static_cast<ss::MonotonicArena *>(user)->allocate(size);
return bytes ? bytes->data() : nullptr;
}
void
scratch_free(void *, void *)
{
}
struct AddrParts {
std::string_view host;
std::string_view port; // empty when there is none
};
// Split "host", "host:port", "[v6]" or "[v6]:port". IPv6 literals without
// brackets and anything else with several colons stay whole.
AddrParts
split_addr(const char *str)
{
// Plain pointer arithmetic: string_view::substr can throw out_of_range,
// which would pull in the C++ runtime.
std::string_view text(str);
auto slice = [&](std::size_t from, std::size_t to) { return std::string_view(str + from, to - from); };
struct ss_ip ip;
if (ss_ip_init(&ip, str) != -1) {
return { text, {} };
}
if (text.front() == '[') {
auto end = text.find(']');
if (end == std::string_view::npos) {
return { text, {} };
}
AddrParts parts{ slice(1, end), {} };
if (end + 1 < text.size() && text[end + 1] == ':') {
parts.port = slice(end + 2, text.size());
}
return parts;
}
auto colon = text.find(':');
if (colon == std::string_view::npos || text.find(':', colon + 1) != std::string_view::npos) {
return { text, {} };
}
return { slice(0, colon), slice(colon + 1, text.size()) };
}
void
parse_conf_addr(const char *str, ss_addr_t *addr)
{
if (str == NULL) {
return;
}
AddrParts parts = *str == '\0' ? AddrParts{} : split_addr(str);
addr->host = conf_strndup(parts.host);
addr->port = parts.port.empty() ? NULL : conf_strndup(parts.port);
}
} // namespace
static char *
to_string(const json_value *value)
{
if (value->type == json_string) {
return ss_strndup(value->u.string.ptr, value->u.string.length);
return conf_strndup(std::string_view(value->u.string.ptr, value->u.string.length));
} else if (value->type == json_integer) {
char buf[32];
snprintf(buf, sizeof(buf), "%" PRId64, value->u.integer);
return strdup(buf);
return conf_strndup(buf);
} else if (value->type == json_null) {
return NULL;
} else {
@@ -83,56 +188,14 @@ parse_addr(const char *str_in, ss_addr_t *addr)
{
if (str_in == NULL)
return;
int ret = -1, n = 0, len;
char *str = strdup(str_in);
len = strlen(str_in);
struct ss_ip ip;
if (ss_ip_init(&ip, str) != -1) {
addr->host = str;
if (*str_in == '\0') {
addr->host = strdup(str_in);
addr->port = NULL;
return;
}
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 (ret == -1 || n != 1) {
addr->host = strdup(str);
addr->port = NULL;
} else {
addr->host = ss_strndup(str, ret);
if (ret < len - 1) {
addr->port = strdup(str + ret + 1);
} else {
addr->port = NULL;
}
}
free(str);
AddrParts parts = split_addr(str_in);
addr->host = ss_strndup(parts.host.data(), parts.host.size());
addr->port = parts.port.empty() ? NULL : ss_strndup(parts.port.data(), parts.port.size());
}
/* CLI endpoints are strict; keep the legacy JSON address parser unchanged. */
@@ -263,7 +326,6 @@ read_jconf(const char *file)
memset(&conf, 0, sizeof(jconf_t));
char *buf;
json_value *obj;
FILE *f = fopen(file, "rb");
@@ -283,22 +345,31 @@ read_jconf(const char *file)
FATAL("Too large config file.");
}
buf = (char *)ss_malloc(pos + 1);
if (buf == NULL) {
// Every string kept in conf copies part of the file plus a NUL: a JSON
// string or key, an integer printed back in decimal, or the two halves of
// an address (whose unsplit copy is kept too). 16x the file size is a
// comfortable bound.
std::size_t len = static_cast<std::size_t>(pos);
config_arena(16 * len + 4096);
// json-parser needs a json_value (and an array or object slot) for every
// value, each costing at least one byte of input.
ss::MonotonicArena scratch;
if (!scratch.reserve(len + 1 + 128 * (len + 1))) {
FATAL("No enough memory.");
}
char *buf = reinterpret_cast<char *>(scratch.allocate(len + 1, 1)->data());
int nread = fread(buf, pos, 1, f);
if (!nread) {
if (len > 0 && fread(buf, len, 1, f) != 1) {
FATAL("Failed to read the config file.");
}
fclose(f);
buf[pos] = '\0'; // end of string
buf[len] = '\0'; // end of string
json_settings settings = { 0UL, 0, NULL, NULL, NULL };
json_settings settings = { 0UL, 0, scratch_alloc, scratch_free, &scratch, 0 };
char error_buf[512];
obj = json_parse_ex(&settings, buf, pos, error_buf);
obj = json_parse_ex(&settings, buf, len, error_buf);
if (obj == NULL) {
FATAL(error_buf);
@@ -316,15 +387,11 @@ read_jconf(const char *file)
break;
}
json_value *v = value->u.array.values[j];
char *addr_str = to_string(v);
parse_addr(addr_str, conf.remote_addr + j);
ss_free(addr_str);
parse_conf_addr(to_string(v), conf.remote_addr + j);
conf.remote_num = j + 1;
}
} else if (value->type == json_string) {
char* tmp_str = to_string(value);
parse_addr(tmp_str, conf.remote_addr);
ss_free(tmp_str);
parse_conf_addr(to_string(value), conf.remote_addr);
conf.remote_num = 1;
}
} else if (strcmp(name, "port_password") == 0) {
@@ -335,8 +402,8 @@ read_jconf(const char *file)
}
json_value *v = value->u.object.values[j].value;
if (v->type == json_string) {
conf.port_password[j].port = ss_strndup(value->u.object.values[j].name,
value->u.object.values[j].name_length);
conf.port_password[j].port = conf_strndup(
std::string_view(value->u.object.values[j].name, value->u.object.values[j].name_length));
conf.port_password[j].password = to_string(v);
conf.port_password_num = j + 1;
}
@@ -364,9 +431,7 @@ read_jconf(const char *file)
conf.user = to_string(value);
} else if (strcmp(name, "plugin") == 0) {
conf.plugin = to_string(value);
// NOLINTNEXTLINE(clang-analyzer-unix.Malloc): config strings live for the process lifetime
if (conf.plugin && strlen(conf.plugin) == 0) {
ss_free(conf.plugin);
conf.plugin = NULL;
}
} else if (strcmp(name, "plugin_opts") == 0) {
@@ -409,12 +474,11 @@ read_jconf(const char *file)
if (v->type == json_string) {
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);
char *port = conf_strndup(std::string_view(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);
}
}
}
@@ -435,7 +499,6 @@ read_jconf(const char *file)
LOGI("ignore unknown mode: %s, use tcp_only as fallback",
mode_str);
ss_free(mode_str);
} else if (strcmp(name, "mtu") == 0) {
conf.mtu = to_nonnegative_int(value, name);
} else if (strcmp(name, "mptcp") == 0) {
@@ -479,7 +542,6 @@ read_jconf(const char *file)
FATAL("Invalid config file");
}
ss_free(buf);
json_value_free(obj);
// The tree and the file text go with the scratch arena.
return &conf;
}
+24 -16
View File
@@ -117,26 +117,26 @@ if(Python3_Interpreter_FOUND)
endif()
# Shared-reference ownership and consuming pointer moves.
ss_add_test(test_ref "test_ref.c" "")
ss_add_test(test_ref "test_ref_cpp.cpp" "")
# test_base64 - no external deps
ss_add_test(test_base64
"test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.cpp"
"test_base64_cpp.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp"
"")
# test_utils - needs sodium (for ss_malloc -> uses sodium in utils.cpp)
ss_add_test(test_utils
"test_utils.c;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_utils_cpp.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"ss_sodium")
# test_json - standalone json parser
ss_add_test(test_json
"test_json.c;${PROJECT_SOURCE_DIR}/src/json.cpp"
"test_json_cpp.cpp;${PROJECT_SOURCE_DIR}/src/json.cpp"
"${SS_MATH_LIBRARY}")
# test_ssurl - ss:// URI parsing (SIP002 and legacy)
ss_add_test(test_ssurl
"test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_ssurl_cpp.cpp;${PROJECT_SOURCE_DIR}/src/ssurl.cpp;${PROJECT_SOURCE_DIR}/src/base64.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"ss_sodium")
# test_netutils - address parsing and resolver integration
@@ -146,12 +146,12 @@ set(TEST_NETUTILS_LIBS
ss_cares
)
ss_add_test(test_netutils
"test_netutils.c;${PROJECT_SOURCE_DIR}/src/netutils.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_netutils_cpp.cpp;${PROJECT_SOURCE_DIR}/src/netutils.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"${TEST_NETUTILS_LIBS}")
# test_cache - expiration uses a platform monotonic clock
ss_add_test(test_cache
"test_cache.c;${CMAKE_CURRENT_SOURCE_DIR}/reference/cache.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_cache_reference_cpp.cpp;${CMAKE_CURRENT_SOURCE_DIR}/reference/cache.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"ss_sodium")
# test_cache_cpp - fixed-capacity cache edge cases
@@ -164,7 +164,7 @@ ss_strict_warnings(test_cache_cpp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/reference/cach
set(TEST_PPBLOOM_LIBS "")
list(APPEND TEST_PPBLOOM_LIBS bloom)
ss_add_test(test_ppbloom
"test_ppbloom.c;${PROJECT_SOURCE_DIR}/src/ppbloom.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_ppbloom_cpp.cpp;${PROJECT_SOURCE_DIR}/src/ppbloom.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"${TEST_PPBLOOM_LIBS};ss_sodium;${SS_MATH_LIBRARY}")
if(SS_ENABLE_REGEX)
@@ -180,7 +180,7 @@ ss_add_test(test_rule_cpp
# test_acl - ACL precedence and prefix matching
set(TEST_ACL_LIBS ${TEST_RULE_LIBS})
ss_add_test(test_acl
"test_acl.c;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_acl_cpp.cpp;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"${TEST_ACL_LIBS}")
endif()
@@ -192,7 +192,7 @@ set(TEST_JCONF_LIBS
ss_cares
)
ss_add_test(test_jconf
"test_jconf.c;${PROJECT_SOURCE_DIR}/src/jconf.cpp;${PROJECT_SOURCE_DIR}/src/json.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp;${PROJECT_SOURCE_DIR}/src/netutils.cpp"
"test_jconf_cpp.cpp;${PROJECT_SOURCE_DIR}/src/jconf.cpp;${PROJECT_SOURCE_DIR}/src/json.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp;${PROJECT_SOURCE_DIR}/src/netutils.cpp"
"${TEST_JCONF_LIBS};${SS_MATH_LIBRARY}")
# The original C cipher implementation, kept under tests/reference as an
@@ -213,7 +213,7 @@ set(SS_CRYPTO_TEST_SOURCES
# test_buffer - needs sodium and mbedtls for crypto.cpp buffer functions
ss_add_test(test_buffer
"test_buffer.c;${SS_CRYPTO_TEST_SOURCES}"
"test_buffer_cpp.cpp;${SS_CRYPTO_TEST_SOURCES}"
"ss_sodium;;ss_mbedcrypto;ss_ev;${TEST_PPBLOOM_LIBS};blake3;${SS_MATH_LIBRARY}")
# test_crypto - full crypto test
@@ -227,12 +227,12 @@ list(APPEND TEST_CRYPTO_LIBS bloom)
# BLAKE3 backs the 2022 ciphers' key derivation
list(APPEND TEST_CRYPTO_LIBS blake3)
ss_add_test(test_crypto
"test_crypto.c;${SS_CRYPTO_TEST_SOURCES}"
"test_crypto_cpp.cpp;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
# test_aead2022 - Shadowsocks 2022 Edition (SIP022) ciphers
ss_add_test(test_aead2022
"test_aead2022.c;${SS_CRYPTO_TEST_SOURCES}"
"test_aead2022_cpp.cpp;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
# test_crypto_aead_cpp - C++ AEAD codecs, differentially against aead.cpp
@@ -266,11 +266,11 @@ add_test(NAME test_ss_setup
set_tests_properties(test_ss_setup PROPERTIES LABELS "integration")
ss_add_test(test_ipset
"test_ipset.c;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_ipset_cpp.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"ss_sodium")
ss_add_test(test_process
"test_process.c;${PROJECT_SOURCE_DIR}/src/process.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_process_cpp.cpp;${PROJECT_SOURCE_DIR}/src/process.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"ss_sodium")
set_tests_properties(test_process PROPERTIES TIMEOUT 20)
@@ -288,9 +288,17 @@ if(SS_ENABLE_REGEX)
list(APPEND TEST_LITERAL_LIBS ss_pcre2)
endif()
ss_add_test(test_acl_literal
"test_acl_literal.c;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"test_acl_literal_cpp.cpp;${PROJECT_SOURCE_DIR}/src/acl.cpp;${PROJECT_SOURCE_DIR}/src/ipset.cpp;${PROJECT_SOURCE_DIR}/src/rule.cpp;${PROJECT_SOURCE_DIR}/src/utils.cpp"
"${TEST_LITERAL_LIBS}")
# Tests ported from C build with the full C++ warning set. test_ipset_reference
# is intentionally absent: it needs the pre-migration libipset archives.
ss_strict_warnings(
test_acl_cpp.cpp test_acl_literal_cpp.cpp test_aead2022_cpp.cpp test_base64_cpp.cpp
test_buffer_cpp.cpp test_cache_reference_cpp.cpp test_crypto_cpp.cpp test_ipset_cpp.cpp
test_jconf_cpp.cpp test_json_cpp.cpp test_netutils_cpp.cpp test_ppbloom_cpp.cpp test_process_cpp.cpp
test_ref_cpp.cpp test_ssurl_cpp.cpp test_utils_cpp.cpp)
# Preloadable malloc counter for the allocation audit. It interposes the
# system allocator, so it is only built where that is supported (glibc,
-87
View File
@@ -1,87 +0,0 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include "test_helpers.h"
#include "acl.h"
int verbose = 0;
static void
test_invalid_regex_is_not_installed(void)
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
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();
remove(path);
}
static void
test_long_acl_line_is_discarded(void)
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
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();
remove(path);
}
static void
test_invalid_cidr_is_discarded(void)
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
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();
remove(path);
}
int
main(void)
{
test_invalid_regex_is_not_installed();
test_long_acl_line_is_discarded();
test_invalid_cidr_is_discarded();
return 0;
}
+92
View File
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// ACL file parsing (src/acl.cpp): invalid rules are discarded, valid ones
// still match.
#include <cstdio>
#include <cstdlib>
#include "acl.h"
#include "test_helpers.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
void test_invalid_regex_is_not_installed()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fprintf(f, "[black_list]\n");
std::fprintf(f, "[invalid\n");
std::fprintf(f, "^blocked\\.example$\n");
std::fprintf(f, "[outbound_block_list]\n");
std::fprintf(f, "[also-invalid\n");
std::fprintf(f, "^outbound\\.example$\n");
std::fclose(f);
SS_CHECK(init_acl(path) == 0);
SS_CHECK(acl_match_host("does-not-crash.example") == 0);
SS_CHECK(acl_match_host("blocked.example") == 1);
SS_CHECK(outbound_block_match_host("does-not-crash.example") == 0);
SS_CHECK(outbound_block_match_host("outbound.example") == 1);
free_acl();
std::remove(path);
}
void test_long_acl_line_is_discarded()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fprintf(f, "[black_list]\n");
for (int i = 0; i < 400; i++) {
std::fputc('a', f);
}
std::fprintf(f, "\n");
std::fprintf(f, "^kept\\.example$\n");
std::fclose(f);
SS_CHECK(init_acl(path) == 0);
SS_CHECK(acl_match_host("kept.example") == 1);
free_acl();
std::remove(path);
}
void test_invalid_cidr_is_discarded()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fprintf(f, "[black_list]\n");
std::fprintf(f, "1.2.3.0/999\n");
std::fprintf(f, "2.2.2.2/-1\n");
std::fprintf(f, "3.3.3.3/abc\n");
std::fprintf(f, "4.4.4.0/24\n");
std::fprintf(f, "^path/with/slash\\.example$\n");
std::fclose(f);
SS_CHECK(init_acl(path) == 0);
SS_CHECK(acl_match_host("1.2.3.1") == 0);
SS_CHECK(acl_match_host("2.2.2.2") == 0);
SS_CHECK(acl_match_host("3.3.3.3") == 0);
SS_CHECK(acl_match_host("4.4.4.8") == 1);
SS_CHECK(acl_match_host("path/with/slash.example") == 1);
free_acl();
std::remove(path);
}
} // namespace
int main()
{
SS_RUN(test_invalid_regex_is_not_installed);
SS_RUN(test_long_acl_line_is_discarded);
SS_RUN(test_invalid_cidr_is_discarded);
return 0;
}
-52
View File
@@ -1,52 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "acl.h"
int verbose = 0;
int
main(int argc, char **argv)
{
(void)argc;
char path[4096];
assert(snprintf(path, sizeof(path), "%s.acl", argv[0]) < (int)sizeof(path));
FILE *file = fopen(path, "w");
assert(file != NULL);
fputs("[black_list]\nfull:Exact.example\nsuffix:blocked.example\n10.0.0.0/8\n"
"2001:db8::/32\n[white_list]\nsuffix:allowed.example\n"
"[outbound_block_list]\nfull:outbound.example\n", file);
fclose(file);
assert(init_acl(path) == 0);
assert(acl_match_host("EXACT.example") == 1);
assert(acl_match_host("sub.exact.example") == 0);
assert(acl_match_host("blocked.example") == 1);
assert(acl_match_host("a.blocked.example") == 1);
assert(acl_match_host("notblocked.example") == 0);
assert(acl_match_host("blocked.example.evil") == 0);
assert(acl_match_host("a.allowed.example") == -1);
assert(outbound_block_match_host("outbound.example") == 1);
assert(outbound_block_match_host("a.outbound.example") == 0);
assert(acl_match_host("10.42.1.2") == 1);
assert(acl_remove_ip("10.42.1.2") == 0);
assert(acl_match_host("10.42.1.2") == 0);
assert(acl_match_host("10.42.1.3") == 1);
assert(acl_add_ip("10.42.1.2") == 0);
assert(acl_match_host("10.42.1.2") == 1);
assert(acl_remove_ip("2001:db8::42") == 0);
assert(acl_match_host("2001:db8::42") == 0);
assert(acl_match_host("2001:db8::43") == 1);
free_acl();
#if !SS_ENABLE_REGEX
file = fopen(path, "w");
assert(file != NULL);
fputs("[black_list]\n10.0.0.0/8\n^old\\.regex$\n", file);
fclose(file);
assert(init_acl(path) == -1);
assert(acl_match_host("10.1.1.1") == 0);
#endif
remove(path);
return 0;
}
+71
View File
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// ACL literal rules (full:/suffix:), CIDR sets and runtime IP edits
// (src/acl.cpp).
#include <cstdio>
#ifdef HAVE_CONFIG_H
#include "config.h" // SS_ENABLE_REGEX
#endif
#include "acl.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
void write_file(const char *path, const char *content)
{
FILE *file = std::fopen(path, "w");
SS_CHECK(file != nullptr);
std::fputs(content, file);
std::fclose(file);
}
void test_acl_literal(const char *argv0)
{
char path[4096];
SS_CHECK(std::snprintf(path, sizeof(path), "%s.acl", argv0) < static_cast<int>(sizeof(path)));
write_file(path,
"[black_list]\nfull:Exact.example\nsuffix:blocked.example\n10.0.0.0/8\n"
"2001:db8::/32\n[white_list]\nsuffix:allowed.example\n"
"[outbound_block_list]\nfull:outbound.example\n");
SS_CHECK(init_acl(path) == 0);
SS_CHECK(acl_match_host("EXACT.example") == 1);
SS_CHECK(acl_match_host("sub.exact.example") == 0);
SS_CHECK(acl_match_host("blocked.example") == 1);
SS_CHECK(acl_match_host("a.blocked.example") == 1);
SS_CHECK(acl_match_host("notblocked.example") == 0);
SS_CHECK(acl_match_host("blocked.example.evil") == 0);
SS_CHECK(acl_match_host("a.allowed.example") == -1);
SS_CHECK(outbound_block_match_host("outbound.example") == 1);
SS_CHECK(outbound_block_match_host("a.outbound.example") == 0);
SS_CHECK(acl_match_host("10.42.1.2") == 1);
SS_CHECK(acl_remove_ip("10.42.1.2") == 0);
SS_CHECK(acl_match_host("10.42.1.2") == 0);
SS_CHECK(acl_match_host("10.42.1.3") == 1);
SS_CHECK(acl_add_ip("10.42.1.2") == 0);
SS_CHECK(acl_match_host("10.42.1.2") == 1);
SS_CHECK(acl_remove_ip("2001:db8::42") == 0);
SS_CHECK(acl_match_host("2001:db8::42") == 0);
SS_CHECK(acl_match_host("2001:db8::43") == 1);
free_acl();
#if !SS_ENABLE_REGEX
write_file(path, "[black_list]\n10.0.0.0/8\n^old\\.regex$\n");
SS_CHECK(init_acl(path) == -1);
SS_CHECK(acl_match_host("10.1.1.1") == 0);
#endif
std::remove(path);
}
} // namespace
int main(int argc, char **argv)
{
SS_CHECK(argc >= 1);
test_acl_literal(argv[0]);
std::printf("ok test_acl_literal\n");
return 0;
}
@@ -1,53 +1,53 @@
/*
* Unit tests for the Shadowsocks 2022 Edition (SIP022) cipher suite.
*/
// SPDX-License-Identifier: GPL-3.0-or-later
// Shadowsocks 2022 Edition (SIP022) cipher suite in the legacy crypto
// reference (tests/reference/aead.cpp): BLAKE3 vectors, TCP and UDP round
// trips, tamper and replay rejection.
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <sodium.h>
int verbose = 0;
#include "test_helpers.hpp"
#include "crypto.h"
#include "aead.h"
#include "blake3/blake3.h"
#include "crypto.h"
#include "ppbloom.h"
#include "utils.h"
#include "blake3/blake3.h"
/* Provide nonce_cache symbol needed by crypto.cpp */
struct cache *nonce_cache = NULL;
extern "C" {
int verbose = 0;
// Provide the nonce_cache symbol needed by the crypto reference.
struct cache *nonce_cache = nullptr;
}
namespace {
/* 32-byte and 16-byte PSKs in base64 */
#define PSK32 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
#define PSK16 "AAAAAAAAAAAAAAAAAAAAAA=="
static const char *methods_2022[] = {
const char *const methods_2022[] = {
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"2022-blake3-chacha20-poly1305",
};
static const char *
psk_for(const char *method)
const char *psk_for(const char *method)
{
return strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? PSK16 : PSK32;
return std::strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? PSK16 : PSK32;
}
static crypto_t *
make_crypto(const char *method)
crypto_t *make_crypto(const char *method)
{
crypto_t *c = crypto_init(psk_for(method), NULL, method);
assert(c != NULL);
crypto_t *c = crypto_init(psk_for(method), nullptr, method);
SS_CHECK(c != nullptr);
return c;
}
static void
free_crypto(crypto_t *c)
void free_crypto(crypto_t *c)
{
/*
* crypto_init() sets up the nonce bloom filter each time it is called,
@@ -64,14 +64,13 @@ free_crypto(crypto_t *c)
* nothing interoperates. Vector generated from the BLAKE3 reference for
* context "shadowsocks 2022 session subkey" over 32 zero bytes of material.
*/
static void
test_blake3_derive_key_known_vector(void)
void test_blake3_derive_key_known_vector()
{
blake3_hasher h;
uint8_t material[32];
uint8_t out[32];
memset(material, 0, sizeof(material));
std::memset(material, 0, sizeof(material));
blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX);
blake3_hasher_update(&h, material, sizeof(material));
blake3_hasher_finalize(&h, out, sizeof(out));
@@ -81,20 +80,18 @@ test_blake3_derive_key_known_vector(void)
blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX);
blake3_hasher_update(&h, material, sizeof(material));
blake3_hasher_finalize(&h, out2, sizeof(out2));
assert(memcmp(out, out2, 32) == 0);
SS_CHECK(std::memcmp(out, out2, 32) == 0);
/* A different context string must give a different subkey. */
uint8_t out3[32];
blake3_hasher_init_derive_key(&h, "some other context");
blake3_hasher_update(&h, material, sizeof(material));
blake3_hasher_finalize(&h, out3, sizeof(out3));
assert(memcmp(out, out3, 32) != 0);
(void)out3;
SS_CHECK(std::memcmp(out, out3, 32) != 0);
}
/* BLAKE3 hash of the empty input, from the reference test vectors. */
static void
test_blake3_empty_hash_vector(void)
void test_blake3_empty_hash_vector()
{
blake3_hasher h;
uint8_t out[32];
@@ -108,16 +105,14 @@ test_blake3_empty_hash_vector(void)
blake3_hasher_init(&h);
blake3_hasher_update(&h, "", 0);
blake3_hasher_finalize(&h, out, sizeof(out));
assert(memcmp(out, expected, 32) == 0);
(void)expected;
SS_CHECK(std::memcmp(out, expected, 32) == 0);
}
/*
* A client request stream must round-trip through a server decrypt context:
* salt, fixed header, variable header with the SOCKS address, then payload.
*/
static void
test_tcp_request_roundtrip(const char *method)
void test_tcp_request_roundtrip(const char *method)
{
/* SOCKS address: IPv4 1.2.3.4:80 */
static const uint8_t addr[] = { 0x01, 1, 2, 3, 4, 0x00, 0x50 };
@@ -137,35 +132,35 @@ test_tcp_request_roundtrip(const char *method)
/* First write: the address header. */
buffer_t buf;
memset(&buf, 0, sizeof(buf));
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 4096);
memcpy(buf.data, addr, sizeof(addr));
std::memcpy(buf.data, addr, sizeof(addr));
buf.len = sizeof(addr);
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
SS_CHECK(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
/* Second write: stream payload. */
buffer_t buf2;
memset(&buf2, 0, sizeof(buf2));
std::memset(&buf2, 0, sizeof(buf2));
balloc(&buf2, 4096);
size_t plen = strlen(payload);
memcpy(buf2.data, payload, plen);
size_t plen = std::strlen(payload);
std::memcpy(buf2.data, payload, plen);
buf2.len = plen;
assert(c->encrypt(&buf2, &enc, 4096) == CRYPTO_OK);
SS_CHECK(c->encrypt(&buf2, &enc, 4096) == CRYPTO_OK);
/* Feed both writes to the server in one go, as they arrive on the wire. */
buffer_t wire;
memset(&wire, 0, sizeof(wire));
std::memset(&wire, 0, sizeof(wire));
balloc(&wire, 8192);
memcpy(wire.data, buf.data, buf.len);
memcpy(wire.data + buf.len, buf2.data, buf2.len);
std::memcpy(wire.data, buf.data, buf.len);
std::memcpy(wire.data + buf.len, buf2.data, buf2.len);
wire.len = buf.len + buf2.len;
assert(c->decrypt(&wire, &dec, 8192) == CRYPTO_OK);
SS_CHECK(c->decrypt(&wire, &dec, 8192) == CRYPTO_OK);
/* The server sees the SOCKS address followed by the payload. */
assert(wire.len == sizeof(addr) + plen);
assert(memcmp(wire.data, addr, sizeof(addr)) == 0);
assert(memcmp(wire.data + sizeof(addr), payload, plen) == 0);
SS_CHECK(wire.len == sizeof(addr) + plen);
SS_CHECK(std::memcmp(wire.data, addr, sizeof(addr)) == 0);
SS_CHECK(std::memcmp(wire.data + sizeof(addr), payload, plen) == 0);
bfree(&buf);
bfree(&buf2);
@@ -179,8 +174,7 @@ test_tcp_request_roundtrip(const char *method)
* A stream split across arbitrary boundaries must still decrypt: the
* decryptor buffers partial chunks and returns CRYPTO_NEED_MORE.
*/
static void
test_tcp_partial_delivery(const char *method)
void test_tcp_partial_delivery(const char *method)
{
static const uint8_t addr[] = { 0x01, 10, 0, 0, 1, 0x1f, 0x90 };
crypto_t *c = make_crypto(method);
@@ -194,33 +188,32 @@ test_tcp_partial_delivery(const char *method)
dec.role = CRYPTO_ROLE_SERVER;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 4096);
memcpy(buf.data, addr, sizeof(addr));
std::memcpy(buf.data, addr, sizeof(addr));
buf.len = sizeof(addr);
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
SS_CHECK(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
/* Deliver the ciphertext one byte at a time. */
size_t total = buf.len;
int got_ok = 0;
for (size_t i = 0; i < total; i++) {
buffer_t piece;
memset(&piece, 0, sizeof(piece));
std::memset(&piece, 0, sizeof(piece));
balloc(&piece, 4096);
piece.data[0] = buf.data[i];
piece.len = 1;
int rc = c->decrypt(&piece, &dec, 4096);
assert(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE);
SS_CHECK(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE);
if (rc == CRYPTO_OK) {
assert(piece.len == sizeof(addr));
assert(memcmp(piece.data, addr, sizeof(addr)) == 0);
SS_CHECK(piece.len == sizeof(addr));
SS_CHECK(std::memcmp(piece.data, addr, sizeof(addr)) == 0);
got_ok = 1;
}
bfree(&piece);
}
assert(got_ok);
(void)got_ok;
SS_CHECK(got_ok);
bfree(&buf);
c->ctx_release(&enc);
@@ -229,8 +222,7 @@ test_tcp_partial_delivery(const char *method)
}
/* A tampered ciphertext must fail authentication rather than pass through. */
static void
test_tcp_tamper_detection(const char *method)
void test_tcp_tamper_detection(const char *method)
{
static const uint8_t addr[] = { 0x01, 127, 0, 0, 1, 0x00, 0x50 };
crypto_t *c = make_crypto(method);
@@ -244,16 +236,16 @@ test_tcp_tamper_detection(const char *method)
dec.role = CRYPTO_ROLE_SERVER;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 4096);
memcpy(buf.data, addr, sizeof(addr));
std::memcpy(buf.data, addr, sizeof(addr));
buf.len = sizeof(addr);
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
SS_CHECK(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
/* Flip a bit inside the first header chunk (past the salt). */
buf.data[c->cipher->key_len + 2] ^= 0x01;
assert(c->decrypt(&buf, &dec, 4096) == CRYPTO_ERROR);
SS_CHECK(c->decrypt(&buf, &dec, 4096) == CRYPTO_ERROR);
bfree(&buf);
c->ctx_release(&enc);
@@ -262,8 +254,7 @@ test_tcp_tamper_detection(const char *method)
}
/* UDP packets must round-trip through the session interface. */
static void
test_udp_roundtrip(const char *method)
void test_udp_roundtrip(const char *method)
{
static const uint8_t pkt[] = {
0x01, 8, 8, 8, 8, 0x00, 0x35, /* 8.8.8.8:53 */
@@ -271,26 +262,26 @@ test_udp_roundtrip(const char *method)
};
crypto_t *c = make_crypto(method);
assert(c->encrypt_udp != NULL);
assert(c->decrypt_udp != NULL);
SS_CHECK(c->encrypt_udp != nullptr);
SS_CHECK(c->decrypt_udp != nullptr);
void *client_session = NULL;
void *server_session = NULL;
void *client_session = nullptr;
void *server_session = nullptr;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 2048);
memcpy(buf.data, pkt, sizeof(pkt));
std::memcpy(buf.data, pkt, sizeof(pkt));
buf.len = sizeof(pkt);
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
assert(buf.len > sizeof(pkt));
SS_CHECK(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
SS_CHECK(buf.len > sizeof(pkt));
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
assert(buf.len == sizeof(pkt));
assert(memcmp(buf.data, pkt, sizeof(pkt)) == 0);
SS_CHECK(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
SS_CHECK(buf.len == sizeof(pkt));
SS_CHECK(std::memcmp(buf.data, pkt, sizeof(pkt)) == 0);
bfree(&buf);
c->udp_session_release(client_session);
@@ -300,39 +291,38 @@ test_udp_roundtrip(const char *method)
}
/* A replayed UDP packet must be rejected by the sliding window filter. */
static void
test_udp_replay_rejected(const char *method)
void test_udp_replay_rejected(const char *method)
{
static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 };
crypto_t *c = make_crypto(method);
void *client_session = NULL;
void *server_session = NULL;
void *client_session = nullptr;
void *server_session = nullptr;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 2048);
memcpy(buf.data, pkt, sizeof(pkt));
std::memcpy(buf.data, pkt, sizeof(pkt));
buf.len = sizeof(pkt);
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
SS_CHECK(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
/* Keep a copy of the wire packet so we can replay it verbatim. */
buffer_t copy;
memset(&copy, 0, sizeof(copy));
std::memset(&copy, 0, sizeof(copy));
balloc(&copy, 2048);
memcpy(copy.data, buf.data, buf.len);
std::memcpy(copy.data, buf.data, buf.len);
copy.len = buf.len;
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
SS_CHECK(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
/* A rejected packet must not publish a session into a fresh relay slot. */
void *rejected_session = NULL;
assert(c->decrypt_udp(&copy, c->cipher, 2048, &rejected_session) == CRYPTO_ERROR);
assert(rejected_session == NULL);
void *rejected_session = nullptr;
SS_CHECK(c->decrypt_udp(&copy, c->cipher, 2048, &rejected_session) == CRYPTO_ERROR);
SS_CHECK(rejected_session == nullptr);
/* the very same packet again must be refused */
assert(c->decrypt_udp(&copy, c->cipher, 2048, &server_session) == CRYPTO_ERROR);
SS_CHECK(c->decrypt_udp(&copy, c->cipher, 2048, &server_session) == CRYPTO_ERROR);
bfree(&buf);
bfree(&copy);
@@ -343,17 +333,16 @@ test_udp_replay_rejected(const char *method)
}
/* Encrypt one client packet into buf and hand it to the server's slot. */
static void
client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len,
void client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len,
void **client_session, void **server_session)
{
memcpy(buf->data, pkt, len);
std::memcpy(buf->data, pkt, len);
buf->len = len;
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->encrypt_udp(buf, c->cipher, 2048, client_session) == CRYPTO_OK);
SS_CHECK(c->encrypt_udp(buf, c->cipher, 2048, client_session) == CRYPTO_OK);
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(buf, c->cipher, 2048, server_session) == CRYPTO_OK);
assert(buf->len == len && memcmp(buf->data, pkt, len) == 0);
SS_CHECK(c->decrypt_udp(buf, c->cipher, 2048, server_session) == CRYPTO_OK);
SS_CHECK(buf->len == len && std::memcmp(buf->data, pkt, len) == 0);
}
/*
@@ -361,34 +350,33 @@ client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len,
* the table with fresh client session IDs used to free the session behind
* the relay's back, so the reply path encrypted with freed memory.
*/
static void
test_udp_session_survives_table_eviction(const char *method)
void test_udp_session_survives_table_eviction(const char *method)
{
static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 };
crypto_t *c = make_crypto(method);
void *client_session = NULL;
void *server_session = NULL;
void *client_session = nullptr;
void *server_session = nullptr;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 2048);
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session);
assert(server_session != NULL);
SS_CHECK(server_session != nullptr);
void *first = server_session;
void *shared_session = NULL;
void *shared_session = nullptr;
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &shared_session);
assert(shared_session == first);
SS_CHECK(shared_session == first);
/* Reattaching the same slot must not acquire an extra reference. */
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session);
assert(server_session == first);
SS_CHECK(server_session == first);
/* Each new client session ID inserts a table entry; overflow evicts ours. */
for (int i = 0; i < AEAD_2022_UDP_SESSION_TABLE_CAP + 1; i++) {
void *other_client = NULL, *other_server = NULL;
void *other_client = nullptr, *other_server = nullptr;
client_to_server(c, &buf, pkt, sizeof(pkt), &other_client, &other_server);
assert(other_server != NULL && other_server != first);
SS_CHECK(other_server != nullptr && other_server != first);
c->udp_session_release(other_client);
c->udp_session_release(other_server);
}
@@ -397,18 +385,18 @@ test_udp_session_survives_table_eviction(const char *method)
c->udp_session_release(shared_session);
/* The reply must still encrypt with the evicted session and verify. */
memcpy(buf.data, pkt, sizeof(pkt));
std::memcpy(buf.data, pkt, sizeof(pkt));
buf.len = sizeof(pkt);
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->encrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
assert(server_session == first);
SS_CHECK(c->encrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
SS_CHECK(server_session == first);
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
assert(buf.len == sizeof(pkt) && memcmp(buf.data, pkt, sizeof(pkt)) == 0);
SS_CHECK(c->decrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
SS_CHECK(buf.len == sizeof(pkt) && std::memcmp(buf.data, pkt, sizeof(pkt)) == 0);
/* The client's next packet re-creates a table entry and swaps the slot. */
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session);
assert(server_session != NULL && server_session != first);
SS_CHECK(server_session != nullptr && server_session != first);
/* Releasing the slot after eviction must neither leak nor double free. */
c->udp_session_release(server_session);
@@ -420,15 +408,14 @@ test_udp_session_survives_table_eviction(const char *method)
}
/* The 2022 ciphers must reject a PSK of the wrong size rather than pad it. */
static void
test_psk_parsing(void)
void test_psk_parsing()
{
uint8_t key[32];
/* Correct size decodes. */
assert(crypto_parse_psk(PSK32, key, 32) == 32);
SS_CHECK(crypto_parse_psk(PSK32, key, 32) == 32);
for (int i = 0; i < 32; i++)
assert(key[i] == 0);
SS_CHECK(key[i] == 0);
/*
* The standard alphabet ('+' and '/') and the URL-safe one ('-' and '_')
@@ -436,38 +423,40 @@ test_psk_parsing(void)
* tools using either convention.
*/
uint8_t k1[32], k2[32];
assert(crypto_parse_psk("//////////////////////////////////////////8=", k1, 32) == 32);
assert(crypto_parse_psk("__________________________________________8=", k2, 32) == 32);
assert(memcmp(k1, k2, 32) == 0);
SS_CHECK(crypto_parse_psk("//////////////////////////////////////////8=", k1, 32) == 32);
SS_CHECK(crypto_parse_psk("__________________________________________8=", k2, 32) == 32);
SS_CHECK(std::memcmp(k1, k2, 32) == 0);
for (int i = 0; i < 32; i++)
assert(k1[i] == 0xff);
SS_CHECK(k1[i] == 0xff);
/* A key with distinct bytes must decode to exactly those bytes. */
uint8_t k3[32];
assert(crypto_parse_psk("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
SS_CHECK(crypto_parse_psk("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
k3, 32) == 32);
for (int i = 0; i < 32; i++)
assert(k3[i] == i);
SS_CHECK(k3[i] == i);
}
int
main(void)
} // namespace
int main()
{
if (sodium_init() < 0)
if (sodium_init() < 0) {
return 1;
}
test_blake3_empty_hash_vector();
test_blake3_derive_key_known_vector();
test_psk_parsing();
SS_RUN(test_blake3_empty_hash_vector);
SS_RUN(test_blake3_derive_key_known_vector);
SS_RUN(test_psk_parsing);
for (size_t i = 0; i < sizeof(methods_2022) / sizeof(methods_2022[0]); i++) {
const char *m = methods_2022[i];
for (const char *m : methods_2022) {
test_tcp_request_roundtrip(m);
test_tcp_partial_delivery(m);
test_tcp_tamper_detection(m);
test_udp_roundtrip(m);
test_udp_replay_rejected(m);
test_udp_session_survives_table_eviction(m);
std::printf("ok %s\n", m);
}
return 0;
-116
View File
@@ -1,116 +0,0 @@
#include <assert.h>
#include <string.h>
#include <stdint.h>
#include "base64.h"
static void
test_encode_decode(void)
{
/* Known test vector: "Hello" */
const uint8_t input[] = "Hello";
char encoded[BASE64_SIZE(5)];
uint8_t decoded[5];
char *ret = base64_encode(encoded, sizeof(encoded), input, 5);
assert(ret != NULL);
assert(strlen(encoded) > 0);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
assert(decoded_len == 5);
assert(memcmp(decoded, input, 5) == 0);
}
static void
test_empty_input(void)
{
char encoded[BASE64_SIZE(0)];
char *ret = base64_encode(encoded, sizeof(encoded), (const uint8_t *)"", 0);
assert(ret != NULL);
assert(strlen(encoded) == 0);
}
static void
test_single_byte(void)
{
const uint8_t input[] = { 0x41 }; /* 'A' */
char encoded[BASE64_SIZE(1)];
uint8_t decoded[1];
char *ret = base64_encode(encoded, sizeof(encoded), input, 1);
assert(ret != NULL);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
assert(decoded_len == 1);
assert(decoded[0] == 0x41);
}
static void
test_two_bytes(void)
{
const uint8_t input[] = { 0x41, 0x42 }; /* "AB" */
char encoded[BASE64_SIZE(2)];
uint8_t decoded[2];
char *ret = base64_encode(encoded, sizeof(encoded), input, 2);
assert(ret != NULL);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
assert(decoded_len == 2);
assert(decoded[0] == 0x41);
assert(decoded[1] == 0x42);
}
static void
test_three_bytes(void)
{
const uint8_t input[] = { 0x00, 0xFF, 0x80 };
char encoded[BASE64_SIZE(3)];
uint8_t decoded[3];
char *ret = base64_encode(encoded, sizeof(encoded), input, 3);
assert(ret != NULL);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
assert(decoded_len == 3);
assert(decoded[0] == 0x00);
assert(decoded[1] == 0xFF);
assert(decoded[2] == 0x80);
}
static void
test_roundtrip_binary(void)
{
const uint8_t input[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16 };
char encoded[BASE64_SIZE(17)];
uint8_t decoded[17];
char *ret = base64_encode(encoded, sizeof(encoded), input, 17);
assert(ret != NULL);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
assert(decoded_len == 17);
assert(memcmp(decoded, input, 17) == 0);
}
static void
test_invalid_chars(void)
{
/* Character '!' (ASCII 33) is below the base64 range start ('+' = 43) */
int decoded_len = base64_decode((uint8_t[4]){}, "!!!!", 4);
assert(decoded_len == -1);
}
int
main(void)
{
test_encode_decode();
test_empty_input();
test_single_byte();
test_two_bytes();
test_three_bytes();
test_roundtrip_binary();
test_invalid_chars();
return 0;
}
+108
View File
@@ -0,0 +1,108 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Base64 encoder/decoder (src/base64.cpp): round trips and invalid input.
#include <cstdint>
#include <cstring>
#include "base64.h"
#include "test_helpers.hpp"
namespace {
void test_encode_decode()
{
const uint8_t input[] = "Hello";
char encoded[BASE64_SIZE(5)];
uint8_t decoded[5];
char *ret = base64_encode(encoded, sizeof(encoded), input, 5);
SS_CHECK(ret != nullptr);
SS_CHECK(std::strlen(encoded) > 0);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
SS_CHECK(decoded_len == 5);
SS_CHECK(std::memcmp(decoded, input, 5) == 0);
}
void test_empty_input()
{
char encoded[BASE64_SIZE(0)];
char *ret = base64_encode(encoded, sizeof(encoded), reinterpret_cast<const uint8_t *>(""), 0);
SS_CHECK(ret != nullptr);
SS_CHECK(std::strlen(encoded) == 0);
}
void test_single_byte()
{
const uint8_t input[] = { 0x41 }; // 'A'
char encoded[BASE64_SIZE(1)];
uint8_t decoded[1];
SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 1) != nullptr);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
SS_CHECK(decoded_len == 1);
SS_CHECK(decoded[0] == 0x41);
}
void test_two_bytes()
{
const uint8_t input[] = { 0x41, 0x42 }; // "AB"
char encoded[BASE64_SIZE(2)];
uint8_t decoded[2];
SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 2) != nullptr);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
SS_CHECK(decoded_len == 2);
SS_CHECK(decoded[0] == 0x41);
SS_CHECK(decoded[1] == 0x42);
}
void test_three_bytes()
{
const uint8_t input[] = { 0x00, 0xFF, 0x80 };
char encoded[BASE64_SIZE(3)];
uint8_t decoded[3];
SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 3) != nullptr);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
SS_CHECK(decoded_len == 3);
SS_CHECK(decoded[0] == 0x00);
SS_CHECK(decoded[1] == 0xFF);
SS_CHECK(decoded[2] == 0x80);
}
void test_roundtrip_binary()
{
const uint8_t input[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
char encoded[BASE64_SIZE(17)];
uint8_t decoded[17];
SS_CHECK(base64_encode(encoded, sizeof(encoded), input, 17) != nullptr);
int decoded_len = base64_decode(decoded, encoded, sizeof(decoded));
SS_CHECK(decoded_len == 17);
SS_CHECK(std::memcmp(decoded, input, 17) == 0);
}
void test_invalid_chars()
{
// Character '!' (ASCII 33) is below the base64 range start ('+' = 43).
uint8_t out[4] = {};
SS_CHECK(base64_decode(out, "!!!!", 4) == -1);
}
} // namespace
int main()
{
SS_RUN(test_encode_decode);
SS_RUN(test_empty_input);
SS_RUN(test_single_byte);
SS_RUN(test_two_bytes);
SS_RUN(test_three_bytes);
SS_RUN(test_roundtrip_binary);
SS_RUN(test_invalid_chars);
return 0;
}
-103
View File
@@ -1,103 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
int verbose = 0;
#include "crypto.h"
/* Provide nonce_cache symbol needed by crypto.cpp */
struct cache *nonce_cache = NULL;
static void
test_balloc(void)
{
buffer_t buf;
memset(&buf, 0, sizeof(buf));
int ret = balloc(&buf, 100);
assert(ret == 100);
(void)ret;
assert(buf.data != NULL);
assert(buf.capacity >= 100);
assert(buf.len == 0);
assert(buf.idx == 0);
bfree(&buf);
assert(buf.data == NULL);
assert(buf.capacity == 0);
}
static void
test_brealloc(void)
{
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 50);
buf.len = 10;
/* Grow the buffer */
int ret = brealloc(&buf, 10, 200);
assert(ret == 200);
(void)ret;
assert(buf.capacity >= 200);
assert(buf.len == 10);
bfree(&buf);
}
static void
test_bprepend(void)
{
buffer_t dst, src;
memset(&dst, 0, sizeof(dst));
memset(&src, 0, sizeof(src));
balloc(&dst, 100);
balloc(&src, 100);
/* Put some data in src */
memcpy(src.data, "HEADER", 6);
src.len = 6;
/* Put some data in dst */
memcpy(dst.data, "BODY", 4);
dst.len = 4;
int ret = bprepend(&dst, &src, 200);
assert(ret == 10);
(void)ret;
assert(dst.len == 10);
assert(memcmp(dst.data, "HEADERBODY", 10) == 0);
bfree(&dst);
bfree(&src);
}
static void
test_balloc_zero(void)
{
buffer_t buf;
memset(&buf, 0, sizeof(buf));
int ret = balloc(&buf, 0);
assert(ret == 0);
(void)ret;
/* A zero-capacity buffer should still succeed */
bfree(&buf);
}
int
main(void)
{
test_balloc();
test_brealloc();
test_bprepend();
test_balloc_zero();
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Legacy buffer helpers in the C crypto reference (tests/reference/crypto.h):
// balloc, brealloc and bprepend.
#include <cstdlib>
#include <cstring>
#include "test_helpers.hpp"
#include "crypto.h"
extern "C" {
int verbose = 0;
// Provide the nonce_cache symbol needed by the crypto reference.
struct cache *nonce_cache = nullptr;
}
namespace {
void test_balloc()
{
buffer_t buf;
std::memset(&buf, 0, sizeof(buf));
SS_CHECK(balloc(&buf, 100) == 100);
SS_CHECK(buf.data != nullptr);
SS_CHECK(buf.capacity >= 100);
SS_CHECK(buf.len == 0);
SS_CHECK(buf.idx == 0);
bfree(&buf);
SS_CHECK(buf.data == nullptr);
SS_CHECK(buf.capacity == 0);
}
void test_brealloc()
{
buffer_t buf;
std::memset(&buf, 0, sizeof(buf));
balloc(&buf, 50);
buf.len = 10;
// Grow the buffer.
SS_CHECK(brealloc(&buf, 10, 200) == 200);
SS_CHECK(buf.capacity >= 200);
SS_CHECK(buf.len == 10);
bfree(&buf);
}
void test_bprepend()
{
buffer_t dst;
buffer_t src;
std::memset(&dst, 0, sizeof(dst));
std::memset(&src, 0, sizeof(src));
balloc(&dst, 100);
balloc(&src, 100);
std::memcpy(src.data, "HEADER", 6);
src.len = 6;
std::memcpy(dst.data, "BODY", 4);
dst.len = 4;
SS_CHECK(bprepend(&dst, &src, 200) == 10);
SS_CHECK(dst.len == 10);
SS_CHECK(std::memcmp(dst.data, "HEADERBODY", 10) == 0);
bfree(&dst);
bfree(&src);
}
void test_balloc_zero()
{
buffer_t buf;
std::memset(&buf, 0, sizeof(buf));
SS_CHECK(balloc(&buf, 0) == 0);
// A zero-capacity buffer should still succeed.
bfree(&buf);
}
} // namespace
int main()
{
SS_RUN(test_balloc);
SS_RUN(test_brealloc);
SS_RUN(test_bprepend);
SS_RUN(test_balloc_zero);
return 0;
}
-129
View File
@@ -1,129 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
int verbose = 0;
#include "cache.h"
static void
test_create_delete(void)
{
struct cache *c = NULL;
int ret = cache_create(&c, 100, NULL);
assert(ret == 0);
assert(c != NULL);
ret = cache_delete(c, 0);
assert(ret == 0);
(void)ret;
}
static void
test_create_null(void)
{
int ret = cache_create(NULL, 100, NULL);
assert(ret == EINVAL);
(void)ret;
}
static void
test_insert_lookup(void)
{
struct cache *c = NULL;
cache_create(&c, 100, NULL);
char *data = strdup("test_data");
cache_insert(c, "key1", 4, data);
char *result = NULL;
cache_lookup(c, "key1", 4, &result);
assert(result != NULL);
assert(strcmp(result, "test_data") == 0);
cache_delete(c, 0);
}
static void
test_key_exist(void)
{
struct cache *c = NULL;
cache_create(&c, 100, NULL);
char *data = strdup("value");
cache_insert(c, "mykey", 5, data);
assert(cache_key_exist(c, "mykey", 5) == 1);
assert(cache_key_exist(c, "nokey", 5) == 0);
cache_delete(c, 0);
}
static void
test_remove(void)
{
struct cache *c = NULL;
cache_create(&c, 100, NULL);
char *data = strdup("to_remove");
cache_insert(c, "rmkey", 5, data);
assert(cache_key_exist(c, "rmkey", 5) == 1);
cache_remove(c, "rmkey", 5);
assert(cache_key_exist(c, "rmkey", 5) == 0);
cache_delete(c, 0);
}
static void
test_lookup_missing(void)
{
struct cache *c = NULL;
cache_create(&c, 100, NULL);
char *result = (char *)0xdeadbeef;
cache_lookup(c, "missing", 7, &result);
assert(result == NULL);
cache_delete(c, 0);
}
static void
test_eviction(void)
{
struct cache *c = NULL;
cache_create(&c, 3, NULL);
/* Insert 3 entries to fill cache */
cache_insert(c, "k1", 2, strdup("v1"));
cache_insert(c, "k2", 2, strdup("v2"));
cache_insert(c, "k3", 2, strdup("v3"));
/* This should trigger eviction of the oldest entry */
cache_insert(c, "k4", 2, strdup("v4"));
/* k1 should have been evicted */
assert(cache_key_exist(c, "k1", 2) == 0);
/* k4 should exist */
assert(cache_key_exist(c, "k4", 2) == 1);
cache_delete(c, 0);
}
int
main(void)
{
test_create_delete();
test_create_null();
test_insert_lookup();
test_key_exist();
test_remove();
test_lookup_missing();
test_eviction();
return 0;
}
+1 -1
View File
@@ -1,6 +1,6 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
// Edge cases of the fixed-capacity cache (src/cache.cpp) beyond
// test_cache.c, and proof that a full cache churns without allocating.
// test_cache_reference_cpp.cpp, and proof that a full cache churns without allocating.
#include <cerrno>
#include <cstdint>
#include <cstdio>
+160
View File
@@ -0,0 +1,160 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Basic behaviour of the legacy cache (tests/reference/cache.cpp): create,
// insert, lookup, remove and eviction.
#include <cerrno>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include "cache.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
char *dup_string(const char *text)
{
const size_t size = std::strlen(text) + 1;
auto *copy = static_cast<char *>(std::malloc(size));
SS_CHECK(copy != nullptr);
std::memcpy(copy, text, size);
return copy;
}
// The legacy API takes writable keys; copy literals into a buffer.
struct Key {
char text[16];
size_t length;
explicit Key(const char *literal) : text{}, length(std::strlen(literal))
{
SS_CHECK(length < sizeof(text));
std::memcpy(text, literal, length);
}
};
void insert(struct cache *c, const char *key, const char *value)
{
Key k(key);
cache_insert(c, k.text, k.length, dup_string(value));
}
void lookup(struct cache *c, const char *key, char **result)
{
Key k(key);
cache_lookup(c, k.text, k.length, result);
}
int exists(struct cache *c, const char *key)
{
Key k(key);
return cache_key_exist(c, k.text, k.length);
}
void remove_key(struct cache *c, const char *key)
{
Key k(key);
cache_remove(c, k.text, k.length);
}
void test_create_delete()
{
struct cache *c = nullptr;
SS_CHECK(cache_create(&c, 100, nullptr) == 0);
SS_CHECK(c != nullptr);
SS_CHECK(cache_delete(c, 0) == 0);
}
void test_create_null()
{
SS_CHECK(cache_create(nullptr, 100, nullptr) == EINVAL);
}
void test_insert_lookup()
{
struct cache *c = nullptr;
cache_create(&c, 100, nullptr);
insert(c, "key1", "test_data");
char *result = nullptr;
lookup(c, "key1", &result);
SS_CHECK(result != nullptr);
SS_CHECK(std::strcmp(result, "test_data") == 0);
cache_delete(c, 0);
}
void test_key_exist()
{
struct cache *c = nullptr;
cache_create(&c, 100, nullptr);
insert(c, "mykey", "value");
SS_CHECK(exists(c, "mykey") == 1);
SS_CHECK(exists(c, "nokey") == 0);
cache_delete(c, 0);
}
void test_remove()
{
struct cache *c = nullptr;
cache_create(&c, 100, nullptr);
insert(c, "rmkey", "to_remove");
SS_CHECK(exists(c, "rmkey") == 1);
remove_key(c, "rmkey");
SS_CHECK(exists(c, "rmkey") == 0);
cache_delete(c, 0);
}
void test_lookup_missing()
{
struct cache *c = nullptr;
cache_create(&c, 100, nullptr);
char *result = reinterpret_cast<char *>(static_cast<uintptr_t>(0xdeadbeef));
lookup(c, "missing", &result);
SS_CHECK(result == nullptr);
cache_delete(c, 0);
}
void test_eviction()
{
struct cache *c = nullptr;
cache_create(&c, 3, nullptr);
// Fill the cache.
insert(c, "k1", "v1");
insert(c, "k2", "v2");
insert(c, "k3", "v3");
// This triggers eviction of the oldest entry.
insert(c, "k4", "v4");
SS_CHECK(exists(c, "k1") == 0);
SS_CHECK(exists(c, "k4") == 1);
cache_delete(c, 0);
}
} // namespace
int main()
{
SS_RUN(test_create_delete);
SS_RUN(test_create_null);
SS_RUN(test_insert_lookup);
SS_RUN(test_key_exist);
SS_RUN(test_remove);
SS_RUN(test_lookup_missing);
SS_RUN(test_eviction);
return 0;
}
+3 -2
View File
@@ -398,8 +398,9 @@ void test_methods()
salt_pool();
SS_CHECK(!sc::find_aead2022("aes-256-gcm"));
auto spec = sc::find_aead2022("2022-blake3-aes-256-gcm");
SS_CHECK(!sc::Psk::from_base64(*spec, kPsk16)); // too short
SS_CHECK(!sc::Psk::from_base64(*spec, "not base64"));
SS_CHECK(spec.has_value());
SS_CHECK(!sc::Psk::from_base64(spec.value(), kPsk16)); // too short
SS_CHECK(!sc::Psk::from_base64(spec.value(), "not base64"));
}
void test_no_heap_in_steady_state()
+105 -111
View File
@@ -1,79 +1,78 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
// SPDX-License-Identifier: GPL-3.0-or-later
// Legacy crypto reference (tests/reference/crypto.cpp): MD5, key derivation,
// HKDF, key parsing, AEAD salt replay and the printable-salt experiment.
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <sodium.h>
int verbose = 0;
#include "test_helpers.hpp"
#include "crypto.h"
#include "ppbloom.h"
#include "utils.h"
/* Provide nonce_cache symbol needed by crypto.cpp */
struct cache *nonce_cache = NULL;
extern "C" {
int verbose = 0;
// Provide the nonce_cache symbol needed by the crypto reference.
struct cache *nonce_cache = nullptr;
}
static void
test_crypto_md5(void)
namespace {
void test_crypto_md5()
{
/* MD5("") = d41d8cd98f00b204e9800998ecf8427e */
unsigned char result[16];
crypto_md5((const unsigned char *)"", 0, result);
crypto_md5(reinterpret_cast<const unsigned char *>(""), 0, result);
unsigned char expected[] = {
0xd4, 0x1d, 0x8c, 0xd9, 0x8f, 0x00, 0xb2, 0x04,
0xe9, 0x80, 0x09, 0x98, 0xec, 0xf8, 0x42, 0x7e
};
assert(memcmp(result, expected, 16) == 0);
(void)expected;
SS_CHECK(std::memcmp(result, expected, 16) == 0);
/* MD5("abc") = 900150983cd24fb0d6963f7d28e17f72 */
crypto_md5((const unsigned char *)"abc", 3, result);
crypto_md5(reinterpret_cast<const unsigned char *>("abc"), 3, result);
unsigned char expected_abc[] = {
0x90, 0x01, 0x50, 0x98, 0x3c, 0xd2, 0x4f, 0xb0,
0xd6, 0x96, 0x3f, 0x7d, 0x28, 0xe1, 0x7f, 0x72
};
assert(memcmp(result, expected_abc, 16) == 0);
(void)expected_abc;
SS_CHECK(std::memcmp(result, expected_abc, 16) == 0);
}
static void
test_crypto_derive_key(void)
void test_crypto_derive_key()
{
uint8_t key[32];
assert(crypto_derive_key(NULL, key, 32) == 0);
SS_CHECK(crypto_derive_key(nullptr, key, 32) == 0);
/* derive_key should produce deterministic output from a password */
int ret = crypto_derive_key("password", key, 32);
assert(ret == 32);
SS_CHECK(ret == 32);
/* Same password should produce same key */
uint8_t key2[32];
ret = crypto_derive_key("password", key2, 32);
assert(ret == 32);
assert(memcmp(key, key2, 32) == 0);
SS_CHECK(ret == 32);
SS_CHECK(std::memcmp(key, key2, 32) == 0);
/* Different password should produce different key */
uint8_t key3[32];
ret = crypto_derive_key("different", key3, 32);
assert(ret == 32);
assert(memcmp(key, key3, 32) != 0);
(void)ret;
SS_CHECK(ret == 32);
SS_CHECK(std::memcmp(key, key3, 32) != 0);
}
static void
test_crypto_hkdf(void)
void test_crypto_hkdf()
{
/* RFC 5869 Test Case 1 */
const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
assert(md != NULL);
SS_CHECK(md != nullptr);
unsigned char ikm[22];
memset(ikm, 0x0b, 22);
std::memset(ikm, 0x0b, 22);
unsigned char salt[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
@@ -88,8 +87,7 @@ test_crypto_hkdf(void)
unsigned char okm[42];
int ret = crypto_hkdf(md, salt, sizeof(salt), ikm, sizeof(ikm),
info, sizeof(info), okm, sizeof(okm));
assert(ret == 0);
(void)ret;
SS_CHECK(ret == 0);
unsigned char expected_okm[] = {
0x3c, 0xb2, 0x5f, 0x25, 0xfa, 0xac, 0xd5, 0x7a,
@@ -99,18 +97,16 @@ test_crypto_hkdf(void)
0x34, 0x00, 0x72, 0x08, 0xd5, 0xb8, 0x87, 0x18,
0x58, 0x65
};
assert(memcmp(okm, expected_okm, 42) == 0);
(void)expected_okm;
SS_CHECK(std::memcmp(okm, expected_okm, 42) == 0);
}
static void
test_crypto_hkdf_extract(void)
void test_crypto_hkdf_extract()
{
const mbedtls_md_info_t *md = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
assert(md != NULL);
SS_CHECK(md != nullptr);
unsigned char ikm[22];
memset(ikm, 0x0b, 22);
std::memset(ikm, 0x0b, 22);
unsigned char salt[] = {
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
@@ -119,8 +115,7 @@ test_crypto_hkdf_extract(void)
unsigned char prk[32];
int ret = crypto_hkdf_extract(md, salt, sizeof(salt), ikm, sizeof(ikm), prk);
assert(ret == 0);
(void)ret;
SS_CHECK(ret == 0);
/* RFC 5869 Test Case 1 PRK */
unsigned char expected_prk[] = {
@@ -129,12 +124,10 @@ test_crypto_hkdf_extract(void)
0x90, 0xb6, 0xc7, 0x3b, 0xb5, 0x0f, 0x9c, 0x31,
0x22, 0xec, 0x84, 0x4a, 0xd7, 0xc2, 0xb3, 0xe5
};
assert(memcmp(prk, expected_prk, 32) == 0);
(void)expected_prk;
SS_CHECK(std::memcmp(prk, expected_prk, 32) == 0);
}
static void
test_crypto_parse_key(void)
void test_crypto_parse_key()
{
/* base64_encode uses URL-safe base64 with -_ instead of +/ */
uint8_t key[32];
@@ -143,29 +136,27 @@ test_crypto_parse_key(void)
/* 32 zero bytes = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" in standard base64 */
/* With URL-safe: same since no +/ needed */
int ret = crypto_parse_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", key, 32);
assert(ret == 32);
(void)ret;
SS_CHECK(ret == 32);
/* All bytes should be 0 */
for (int i = 0; i < 32; i++) {
assert(key[i] == 0);
SS_CHECK(key[i] == 0);
}
}
static void
test_aead_repeat_salt_rejection_releases_context(void)
void test_aead_repeat_salt_rejection_releases_context()
{
crypto_t *crypto = crypto_init("password", NULL, "aes-128-gcm");
assert(crypto != NULL);
crypto_t *crypto = crypto_init("password", nullptr, "aes-128-gcm");
SS_CHECK(crypto != nullptr);
buffer_t packet;
memset(&packet, 0, sizeof(packet));
std::memset(&packet, 0, sizeof(packet));
balloc(&packet, 64);
memcpy(packet.data, "payload", 7);
std::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);
SS_CHECK(crypto->encrypt_all(&packet, crypto->cipher, 128) == CRYPTO_OK);
SS_CHECK(crypto->decrypt_all(&packet, crypto->cipher, 128) == CRYPTO_ERROR);
bfree(&packet);
ppbloom_free();
@@ -175,40 +166,39 @@ test_aead_repeat_salt_rejection_releases_context(void)
/* A nonprintable sentinel exposes the selected span without adding wire state.
* Retain the random tail so every test connection still has a fresh salt. */
static void
test_printable_salt_lengths(const char *method)
void test_printable_salt_lengths(const char *method)
{
crypto_t *crypto = crypto_init("password", NULL, method);
assert(crypto != NULL);
crypto_t *crypto = crypto_init("password", nullptr, method);
SS_CHECK(crypto != nullptr);
unsigned seen = 0;
buffer_t buf = {0};
buffer_t buf{};
balloc(&buf, 2048);
for (int sample = 0; sample < 512; sample++) {
cipher_ctx_t enc;
crypto->ctx_init(crypto->cipher, &enc, 1);
enc.printable_salt = 1;
memset(enc.salt, 0xff, 12);
std::memset(enc.salt, 0xff, 12);
uint8_t tail[20];
memcpy(tail, enc.salt + 12, sizeof(tail));
std::memcpy(tail, enc.salt + 12, sizeof(tail));
buf.data[0] = 'x';
buf.len = 1;
assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
assert(buf.len == 32 + 2 + 32 + 1);
SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
SS_CHECK(buf.len == 32 + 2 + 32 + 1);
size_t length = 0;
while (length < 12 && enc.salt[length] != 0xff) {
assert(enc.salt[length] >= 0x20 && enc.salt[length] <= 0x7e);
SS_CHECK(enc.salt[length] >= 0x20 && enc.salt[length] <= 0x7e);
length++;
}
assert(length >= 6 && length <= 12);
SS_CHECK(length >= 6 && length <= 12);
for (size_t i = length; i < 12; i++)
assert(enc.salt[i] == 0xff);
assert(memcmp(tail, enc.salt + 12, sizeof(tail)) == 0);
assert(memcmp(buf.data, enc.salt, 32) == 0);
SS_CHECK(enc.salt[i] == 0xff);
SS_CHECK(std::memcmp(tail, enc.salt + 12, sizeof(tail)) == 0);
SS_CHECK(std::memcmp(buf.data, enc.salt, 32) == 0);
seen |= 1u << (length - 6);
crypto->ctx_release(&enc);
}
/* Probability of missing a length with unbiased sampling is < 4e-34. */
assert(seen == 0x7f);
SS_CHECK(seen == 0x7f);
bfree(&buf);
ppbloom_free();
ss_free(crypto->cipher);
@@ -217,38 +207,38 @@ test_printable_salt_lengths(const char *method)
/* Exercise empty first writes, exact wire overhead, fragmented input, tampering
* and replay with the receiver's independent bloom filter. */
static void
test_printable_salt(const char *method, int enabled)
void test_printable_salt(const char *method, int enabled)
{
crypto_t *crypto = crypto_init("password", NULL, method);
assert(crypto != NULL);
crypto_t *crypto = crypto_init("password", nullptr, method);
SS_CHECK(crypto != nullptr);
cipher_ctx_t enc, dec;
crypto->ctx_init(crypto->cipher, &enc, 1);
crypto->ctx_init(crypto->cipher, &dec, 0);
assert(enc.printable_salt == 0);
SS_CHECK(enc.printable_salt == 0);
enc.printable_salt = enabled;
uint8_t original[32];
memcpy(original, enc.salt, 32);
buffer_t buf = {0}, fragment = {0};
std::memcpy(original, enc.salt, 32);
buffer_t buf{};
buffer_t fragment{};
balloc(&buf, 2048);
balloc(&fragment, 2048);
assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
assert(enc.init == 0 && buf.len == 0);
assert(memcmp(original, enc.salt, 32) == 0);
memcpy(buf.data, "payload", 7);
SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
SS_CHECK(enc.init == 0 && buf.len == 0);
SS_CHECK(std::memcmp(original, enc.salt, 32) == 0);
std::memcpy(buf.data, "payload", 7);
buf.len = 7;
assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
assert(buf.len == 32 + 2 + 32 + 7);
assert(memcmp(buf.data, enc.salt, 32) == 0);
assert(memcmp(original + 12, enc.salt + 12, 20) == 0);
SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
SS_CHECK(buf.len == 32 + 2 + 32 + 7);
SS_CHECK(std::memcmp(buf.data, enc.salt, 32) == 0);
SS_CHECK(std::memcmp(original + 12, enc.salt + 12, 20) == 0);
if (enabled) {
for (int i = 0; i < 6; i++)
assert(enc.salt[i] >= 0x20 && enc.salt[i] <= 0x7e);
SS_CHECK(enc.salt[i] >= 0x20 && enc.salt[i] <= 0x7e);
} else {
assert(memcmp(original, enc.salt, 32) == 0);
SS_CHECK(std::memcmp(original, enc.salt, 32) == 0);
}
uint8_t wire[73];
memcpy(wire, buf.data, sizeof(wire));
std::memcpy(wire, buf.data, sizeof(wire));
ppbloom_free();
ppbloom_init(10000, 1e-15);
for (size_t i = 0; i < sizeof(wire); i++) {
@@ -256,33 +246,33 @@ test_printable_salt(const char *method, int enabled)
fragment.len = 1;
int result = crypto->decrypt(&fragment, &dec, 2048);
if (i + 1 < sizeof(wire)) {
assert(result == CRYPTO_NEED_MORE);
SS_CHECK(result == CRYPTO_NEED_MORE);
} else {
assert(result == CRYPTO_OK);
assert(fragment.len == 7 && memcmp(fragment.data, "payload", 7) == 0);
SS_CHECK(result == CRYPTO_OK);
SS_CHECK(fragment.len == 7 && std::memcmp(fragment.data, "payload", 7) == 0);
}
}
memcpy(original, enc.salt, 32);
memcpy(buf.data, "next", 4);
std::memcpy(original, enc.salt, 32);
std::memcpy(buf.data, "next", 4);
buf.len = 4;
assert(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
assert(buf.len == 2 + 32 + 4);
assert(memcmp(original, enc.salt, 32) == 0);
assert(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_OK);
assert(buf.len == 4 && memcmp(buf.data, "next", 4) == 0);
SS_CHECK(crypto->encrypt(&buf, &enc, 2048) == CRYPTO_OK);
SS_CHECK(buf.len == 2 + 32 + 4);
SS_CHECK(std::memcmp(original, enc.salt, 32) == 0);
SS_CHECK(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_OK);
SS_CHECK(buf.len == 4 && std::memcmp(buf.data, "next", 4) == 0);
crypto->ctx_release(&dec);
crypto->ctx_init(crypto->cipher, &dec, 0);
memcpy(buf.data, wire, sizeof(wire));
std::memcpy(buf.data, wire, sizeof(wire));
buf.len = sizeof(wire);
assert(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR);
SS_CHECK(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR);
crypto->ctx_release(&dec);
ppbloom_free();
ppbloom_init(10000, 1e-15);
crypto->ctx_init(crypto->cipher, &dec, 0);
memcpy(buf.data, wire, sizeof(wire));
std::memcpy(buf.data, wire, sizeof(wire));
buf.data[sizeof(wire) - 1] ^= 1;
buf.len = sizeof(wire);
assert(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR);
SS_CHECK(crypto->decrypt(&buf, &dec, 2048) == CRYPTO_ERROR);
crypto->ctx_release(&dec);
crypto->ctx_release(&enc);
bfree(&buf);
@@ -292,26 +282,30 @@ test_printable_salt(const char *method, int enabled)
ss_free(crypto);
}
int
main(void)
} // namespace
int main()
{
if (sodium_init() < 0) {
return 1;
}
test_printable_salt_lengths("chacha20-ietf-poly1305");
std::printf("ok test_printable_salt_lengths chacha20-ietf-poly1305\n");
test_printable_salt_lengths("aes-256-gcm");
std::printf("ok test_printable_salt_lengths aes-256-gcm\n");
test_printable_salt("chacha20-ietf-poly1305", 0);
test_printable_salt("chacha20-ietf-poly1305", 1);
test_printable_salt("aes-256-gcm", 0);
test_printable_salt("aes-256-gcm", 1);
test_crypto_md5();
test_crypto_derive_key();
test_crypto_hkdf();
test_crypto_hkdf_extract();
test_crypto_parse_key();
test_aead_repeat_salt_rejection_releases_context();
std::printf("ok test_printable_salt\n");
SS_RUN(test_crypto_md5);
SS_RUN(test_crypto_derive_key);
SS_RUN(test_crypto_hkdf);
SS_RUN(test_crypto_hkdf_extract);
SS_RUN(test_crypto_parse_key);
SS_RUN(test_aead_repeat_salt_rejection_releases_context);
assert(crypto_init("password", NULL, "aes-256-cfb") == NULL);
SS_CHECK(crypto_init("password", nullptr, "aes-256-cfb") == nullptr);
return 0;
}
-59
View File
@@ -1,59 +0,0 @@
#include <assert.h>
#include <stdint.h>
#include <string.h>
#include "ipset.h"
int verbose = 0;
static uint32_t state = 42;
static uint32_t
next_random(void)
{
state = state * 1664525u + 1013904223u;
return state;
}
static void
check_family(unsigned width)
{
struct ss_ipset set = {0};
uint8_t address[16] = {0};
bool oracle[256] = {false};
unsigned last = width / 8 - 1;
/* Independent exhaustive bitmap oracle over a /24 or /120. Prefix writes
* can cover the whole space; host deletion must punch holes in them. */
for (unsigned round = 0; round < 2000; round++) {
unsigned host = next_random() >> 24;
unsigned bits = next_random() % 9;
bool present = (next_random() >> 31) != 0;
unsigned mask = bits == 0 ? 0 : (255u << (8 - bits)) & 255u;
address[last] = (uint8_t)host;
ss_ipset_assign(&set, address, width, width - 8 + bits, present);
for (unsigned i = 0; i < 256; i++) {
if ((i & mask) == (host & mask)) {
oracle[i] = present;
}
address[last] = (uint8_t)i;
assert(ss_ipset_contains(&set, address, width) == oracle[i]);
}
}
ss_ipset_assign(&set, address, width, 0, true);
memset(address, 255, sizeof(address));
assert(ss_ipset_contains(&set, address, width));
ss_ipset_assign(&set, address, width, width, false);
assert(!ss_ipset_contains(&set, address, width));
address[last]--;
assert(ss_ipset_contains(&set, address, width));
ss_ipset_assign(&set, address, width, width + 1, false);
assert(ss_ipset_contains(&set, address, width));
ss_ipset_clear(&set);
ss_ipset_clear(&set);
assert(!ss_ipset_contains(&set, address, width));
}
int
main(void)
{
check_family(32);
check_family(128);
return 0;
}
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// IP prefix set (src/ipset.cpp) checked against an exhaustive bitmap oracle.
#include <cstdint>
#include <cstring>
#include "ipset.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
uint32_t g_state = 42;
uint32_t next_random()
{
g_state = g_state * 1664525u + 1013904223u;
return g_state;
}
void check_family(unsigned width)
{
ss_ipset set{};
uint8_t address[16] = {};
bool oracle[256] = {};
const unsigned last = width / 8 - 1;
// Independent exhaustive bitmap oracle over a /24 or /120. Prefix writes
// can cover the whole space; host deletion must punch holes in them.
for (unsigned round = 0; round < 2000; round++) {
const unsigned host = next_random() >> 24;
const unsigned bits = next_random() % 9;
const bool present = (next_random() >> 31) != 0;
const unsigned mask = bits == 0 ? 0 : (255u << (8 - bits)) & 255u;
address[last] = static_cast<uint8_t>(host);
ss_ipset_assign(&set, address, width, width - 8 + bits, present);
for (unsigned i = 0; i < 256; i++) {
if ((i & mask) == (host & mask)) {
oracle[i] = present;
}
address[last] = static_cast<uint8_t>(i);
SS_CHECK(ss_ipset_contains(&set, address, width) == oracle[i]);
}
}
ss_ipset_assign(&set, address, width, 0, true);
std::memset(address, 255, sizeof(address));
SS_CHECK(ss_ipset_contains(&set, address, width));
ss_ipset_assign(&set, address, width, width, false);
SS_CHECK(!ss_ipset_contains(&set, address, width));
address[last]--;
SS_CHECK(ss_ipset_contains(&set, address, width));
ss_ipset_assign(&set, address, width, width + 1, false);
SS_CHECK(ss_ipset_contains(&set, address, width));
ss_ipset_clear(&set);
ss_ipset_clear(&set);
SS_CHECK(!ss_ipset_contains(&set, address, width));
}
void test_ipv4()
{
check_family(32);
}
void test_ipv6()
{
check_family(128);
}
} // namespace
int main()
{
SS_RUN(test_ipv4);
SS_RUN(test_ipv6);
return 0;
}
-59
View File
@@ -1,59 +0,0 @@
/* Optional migration differential test. Link against the pre-migration ipset
* and cork archives; these are intentionally not production dependencies. */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <ipset/ipset.h>
#include "ipset.h"
int verbose = 0;
static uint32_t state = 0x725cab91;
static uint32_t next_random(void)
{
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
return state;
}
static void check_family(unsigned width)
{
struct ss_ipset current = {0};
struct ip_set reference;
ipset_init(&reference);
for (unsigned round = 0; round < 2000; round++) {
uint8_t bytes[16];
for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)next_random();
unsigned prefix = next_random() % (width + 1);
bool present = (next_random() & 1) != 0;
ss_ipset_assign(&current, bytes, width, prefix, present);
struct cork_ipv4 v4;
struct cork_ipv6 v6;
memcpy(v4._.u8, bytes, sizeof(v4._.u8));
memcpy(v6._.u8, bytes, sizeof(v6._.u8));
if (width == 32) {
if (present) ipset_ipv4_add_network(&reference, &v4, prefix);
else ipset_ipv4_remove_network(&reference, &v4, prefix);
} else {
if (present) ipset_ipv6_add_network(&reference, &v6, prefix);
else ipset_ipv6_remove_network(&reference, &v6, prefix);
}
for (unsigned sample = 0; sample < 257; sample++) {
if (sample != 0) {
for (unsigned i = 0; i < sizeof(bytes); i++) bytes[i] = (uint8_t)next_random();
}
memcpy(v4._.u8, bytes, sizeof(v4._.u8));
memcpy(v6._.u8, bytes, sizeof(v6._.u8));
bool old = width == 32 ? ipset_contains_ipv4(&reference, &v4) : ipset_contains_ipv6(&reference, &v6);
assert(old == ss_ipset_contains(&current, bytes, width));
}
}
ipset_done(&reference);
ss_ipset_clear(&current);
}
int main(void)
{
ipset_init_library();
check_family(32);
check_family(128);
puts("1,028,000 IPv4/IPv6 membership comparisons matched the original libipset");
return 0;
}
+84
View File
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Optional migration differential test. Link against the pre-migration ipset
// and cork archives; these are intentionally not production dependencies.
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <ipset/ipset.h>
#include "ipset.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
uint32_t g_state = 0x725cab91;
uint32_t next_random()
{
g_state ^= g_state << 13;
g_state ^= g_state >> 17;
g_state ^= g_state << 5;
return g_state;
}
void check_family(unsigned width)
{
ss_ipset current{};
ip_set reference;
ipset_init(&reference);
for (unsigned round = 0; round < 2000; round++) {
uint8_t bytes[16];
for (unsigned i = 0; i < sizeof(bytes); i++) {
bytes[i] = static_cast<uint8_t>(next_random());
}
const unsigned prefix = next_random() % (width + 1);
const bool present = (next_random() & 1) != 0;
ss_ipset_assign(&current, bytes, width, prefix, present);
cork_ipv4 v4;
cork_ipv6 v6;
std::memcpy(v4._.u8, bytes, sizeof(v4._.u8));
std::memcpy(v6._.u8, bytes, sizeof(v6._.u8));
if (width == 32) {
if (present) {
ipset_ipv4_add_network(&reference, &v4, prefix);
} else {
ipset_ipv4_remove_network(&reference, &v4, prefix);
}
} else {
if (present) {
ipset_ipv6_add_network(&reference, &v6, prefix);
} else {
ipset_ipv6_remove_network(&reference, &v6, prefix);
}
}
for (unsigned sample = 0; sample < 257; sample++) {
if (sample != 0) {
for (unsigned i = 0; i < sizeof(bytes); i++) {
bytes[i] = static_cast<uint8_t>(next_random());
}
}
std::memcpy(v4._.u8, bytes, sizeof(v4._.u8));
std::memcpy(v6._.u8, bytes, sizeof(v6._.u8));
const bool old = width == 32 ? ipset_contains_ipv4(&reference, &v4) : ipset_contains_ipv6(&reference, &v6);
SS_CHECK(old == ss_ipset_contains(&current, bytes, width));
}
}
ipset_done(&reference);
ss_ipset_clear(&current);
}
} // namespace
int main()
{
ipset_init_library();
check_family(32);
check_family(128);
std::puts("1,028,000 IPv4/IPv6 membership comparisons matched the original libipset");
return 0;
}
-253
View File
@@ -1,253 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "test_helpers.h"
#ifndef _WIN32
#include <sys/wait.h>
#endif
int verbose = 0;
#include "netutils.h"
#include "jconf.h"
static void
test_parse_addr_ipv4_with_port(void)
{
ss_addr_t addr;
memset(&addr, 0, sizeof(addr));
parse_addr("192.168.1.1:8080", &addr);
assert(addr.host != NULL);
assert(addr.port != NULL);
assert(strcmp(addr.host, "192.168.1.1") == 0);
assert(strcmp(addr.port, "8080") == 0);
free_addr(&addr);
}
static void
test_parse_addr_ipv6_with_port(void)
{
ss_addr_t addr;
memset(&addr, 0, sizeof(addr));
parse_addr("[::1]:443", &addr);
assert(addr.host != NULL);
assert(addr.port != NULL);
assert(strcmp(addr.host, "::1") == 0);
assert(strcmp(addr.port, "443") == 0);
free_addr(&addr);
}
static void
test_parse_addr_hostname_with_port(void)
{
ss_addr_t addr;
memset(&addr, 0, sizeof(addr));
parse_addr("example.com:1234", &addr);
assert(addr.host != NULL);
assert(addr.port != NULL);
assert(strcmp(addr.host, "example.com") == 0);
assert(strcmp(addr.port, "1234") == 0);
free_addr(&addr);
}
static void
test_parse_addr_no_port(void)
{
ss_addr_t addr;
memset(&addr, 0, sizeof(addr));
parse_addr("10.0.0.1", &addr);
assert(addr.host != NULL);
assert(strcmp(addr.host, "10.0.0.1") == 0);
/* Port may be NULL when no port is specified */
free_addr(&addr);
}
static void
test_parse_addr_ipv6_no_port(void)
{
ss_addr_t addr;
memset(&addr, 0, sizeof(addr));
parse_addr("::1", &addr);
assert(addr.host != NULL);
assert(strcmp(addr.host, "::1") == 0);
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[4096];
FILE *f = test_tempfile(path, sizeof(path));
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);
remove(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");
}
static const char *test_executable;
static void
test_read_jconf_rejects_out_of_range_int_options(void)
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
assert(f != NULL);
write_minimal_config(f, "\"tcp_incoming_sndbuf\":2147483648");
fclose(f);
#ifdef _WIN32
char command[3 * 4096];
assert(snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"",
test_executable, path) < (int)sizeof(command));
STARTUPINFOA startup = {0};
PROCESS_INFORMATION child = {0};
startup.cb = sizeof(startup);
assert(CreateProcessA(test_executable, command, NULL, NULL, FALSE, 0,
NULL, NULL, &startup, &child));
CloseHandle(child.hThread);
assert(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0);
DWORD status;
assert(GetExitCodeProcess(child.hProcess, &status));
CloseHandle(child.hProcess);
/* FATAL exits with -1, which is a valid nonzero DWORD exit status. */
assert(status != 0);
#else
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);
#endif
remove(path);
}
static void
test_server_endpoints(void)
{
const char *valid[][3] = {
{ "example.com:8388", "example.com", "8388" },
{ "127.0.0.1:1", "127.0.0.1", "1" },
{ "[2001:db8::1]:65535", "2001:db8::1", "65535" },
{ "[fe80::1%eth0]:8388", "fe80::1%eth0", "8388" },
{ "[fe80::1%3]:8388", "fe80::1%3", "8388" },
{ "2001:db8::1:8388", "2001:db8::1:8388", NULL },
{ "fe80::1%eth0", "fe80::1%eth0", NULL },
{ "::", "::", NULL },
{ "[::1]", "::1", NULL },
{ "example.com", "example.com", NULL }
};
for (size_t i = 0; i < sizeof(valid) / sizeof(valid[0]); i++) {
ss_addr_t addr = { 0 };
assert(parse_server_endpoint(valid[i][0], &addr) == 0);
assert(strcmp(addr.host, valid[i][1]) == 0);
assert(valid[i][2] ? addr.port && strcmp(addr.port, valid[i][2]) == 0 : addr.port == NULL);
free_addr(&addr);
}
const char *invalid[] = { "", ":8388", "host:", "host:0", "host:65536",
"host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk",
"[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80",
"[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80",
"[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" };
for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) {
ss_addr_t addr = { 0 };
assert(parse_server_endpoint(invalid[i], &addr) == -1);
assert(addr.host == NULL && addr.port == NULL);
}
ss_addr_t addr[2] = { { 0 }, { 0 } };
assert(parse_server_endpoint("[::1]:8388", &addr[0]) == 0);
assert(parse_server_endpoint("localhost:8389", &addr[1]) == 0);
assert(complete_server_ports(addr, 2, NULL, 0) == 0);
assert(complete_server_ports(addr, 2, "9000", 0) == 0);
assert(strcmp(addr[0].port, "8388") == 0 && strcmp(addr[1].port, "8389") == 0);
assert(complete_server_ports(addr, 2, NULL, 1) == -1);
free_addr(&addr[1]);
memset(&addr[1], 0, sizeof(addr[1]));
assert(parse_server_endpoint("::1", &addr[1]) == 0);
assert(complete_server_ports(addr, 2, NULL, 0) == -1);
assert(complete_server_ports(addr, 2, "8388", 1) == 0);
assert(strcmp(addr[1].port, "8388") == 0);
free_addr(&addr[0]);
free_addr(&addr[1]);
}
int
main(int argc, char **argv)
{
test_executable = argv[0];
test_network_init();
if (argc == 3 && strcmp(argv[1], "--read-config") == 0) {
read_jconf(argv[2]);
return 0;
}
test_server_endpoints();
test_parse_addr_ipv4_with_port();
test_parse_addr_ipv6_with_port();
test_parse_addr_hostname_with_port();
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();
test_read_jconf_rejects_out_of_range_int_options();
return 0;
}
+294
View File
@@ -0,0 +1,294 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
// Configuration parsing: the address splitters and read_jconf, whose result
// lives in the configuration arena.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "test_helpers.hpp"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "test_helpers.h"
#ifndef _WIN32
#include <sys/wait.h>
#endif
extern "C" int verbose;
int verbose = 0;
#include "netutils.h" // defines ss_addr_t, which jconf.h uses
#include "jconf.h"
namespace {
bool
same(const char *a, const char *b)
{
return a == nullptr ? b == nullptr : b != nullptr && std::strcmp(a, b) == 0;
}
struct AddrCase {
const char *input;
const char *host;
const char *port;
};
// parse_addr keeps anything it cannot split whole, so malformed bracketed
// IPv6 and IPv6-like host names are not rewritten.
constexpr AddrCase kAddrCases[] = {
{ "192.168.1.1:8080", "192.168.1.1", "8080" },
{ "[::1]:443", "::1", "443" },
{ "example.com:1234", "example.com", "1234" },
{ "10.0.0.1", "10.0.0.1", nullptr },
{ "::1", "::1", nullptr },
{ "[::1", "[::1", nullptr },
{ "bad::host", "bad::host", nullptr },
{ "[::1]:", "::1", nullptr },
{ "[::1]junk", "::1", nullptr },
{ "host:", "host", nullptr },
{ "", "", nullptr },
};
void
test_parse_addr()
{
for (const AddrCase &c : kAddrCases) {
ss_addr_t addr = {};
parse_addr(c.input, &addr);
SS_CHECK(same(addr.host, c.host));
SS_CHECK(same(addr.port, c.port));
free_addr(&addr);
}
}
void
write_config(const char *path, const char *body)
{
FILE *f = std::fopen(path, "wb");
SS_CHECK(f != nullptr);
std::fputs(body, f);
std::fclose(f);
}
void
test_read_jconf_preserves_64bit_integer_strings()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fclose(f);
write_config(path, "{\n\"server\":\"127.0.0.1\",\n\"server_port\":4294967296,\n"
"\"password\":\"password\",\n\"method\":\"aes-128-gcm\"\n}\n");
jconf_t *conf = read_jconf(path);
SS_CHECK(conf != nullptr);
SS_CHECK(same(conf->remote_port, "4294967296"));
std::remove(path);
}
// Every field that read_jconf copies out of the parse tree, which is gone by
// the time it returns.
void
test_read_jconf_copies_out_of_the_parse_tree()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fclose(f);
write_config(path,
"{\n"
" \"server\": [\"192.168.1.1:8080\", \"[::1]:443\", \"example.com\", \"bad::host\"],\n"
" \"server_port\": 8388,\n"
" \"local_address\": \"127.0.0.1\",\n"
" \"local_port\": \"1080\",\n"
" \"password\": \"pass\\u00e9word\",\n"
" \"method\": \"chacha20-ietf-poly1305\",\n"
" \"timeout\": 60,\n"
" \"plugin\": \"\",\n"
" \"plugin_opts\": \"a=b;c=d\",\n"
" \"mode\": \"tcp_and_udp\",\n"
" \"port_password\": {\"8381\": \"one\", \"8382\": \"two\", \"8383\": 3},\n"
" \"dscp\": {\"8381\": \"EF\", \"8382\": \"AF41\"},\n"
" \"acl\": null,\n"
" \"max_connections\": 64\n"
"}\n");
jconf_t *conf = read_jconf(path);
SS_CHECK(conf != nullptr);
SS_CHECK(conf->remote_num == 4);
SS_CHECK(same(conf->remote_addr[0].host, "192.168.1.1") && same(conf->remote_addr[0].port, "8080"));
SS_CHECK(same(conf->remote_addr[1].host, "::1") && same(conf->remote_addr[1].port, "443"));
SS_CHECK(same(conf->remote_addr[2].host, "example.com") && conf->remote_addr[2].port == nullptr);
SS_CHECK(same(conf->remote_addr[3].host, "bad::host") && conf->remote_addr[3].port == nullptr);
SS_CHECK(same(conf->remote_port, "8388"));
SS_CHECK(same(conf->local_addr, "127.0.0.1"));
SS_CHECK(same(conf->local_port, "1080"));
SS_CHECK(same(conf->password, "pass\xc3\xa9word"));
SS_CHECK(same(conf->method, "chacha20-ietf-poly1305"));
SS_CHECK(same(conf->timeout, "60"));
SS_CHECK(conf->plugin == nullptr);
SS_CHECK(same(conf->plugin_opts, "a=b;c=d"));
SS_CHECK(conf->mode == TCP_AND_UDP);
// Non-string passwords are skipped, as before.
SS_CHECK(conf->port_password_num == 2);
SS_CHECK(same(conf->port_password[0].port, "8381") && same(conf->port_password[0].password, "one"));
SS_CHECK(same(conf->port_password[1].port, "8382") && same(conf->port_password[1].password, "two"));
SS_CHECK(conf->dscp_num == 2);
SS_CHECK(same(conf->dscp[0].port, "8381") && conf->dscp[0].dscp == 46);
SS_CHECK(same(conf->dscp[1].port, "8382") && conf->dscp[1].dscp == 34);
SS_CHECK(conf->acl == nullptr);
SS_CHECK(conf->max_connections == 64);
// A second read replaces the first one.
write_config(path, "{\"server\":\"10.0.0.1:1\",\"method\":\"aes-256-gcm\",\"password\":\"x\"}");
conf = read_jconf(path);
SS_CHECK(conf != nullptr);
SS_CHECK(conf->remote_num == 1);
SS_CHECK(same(conf->remote_addr[0].host, "10.0.0.1") && same(conf->remote_addr[0].port, "1"));
SS_CHECK(same(conf->method, "aes-256-gcm"));
SS_CHECK(conf->remote_port == nullptr && conf->port_password_num == 0);
std::remove(path);
}
// The largest accepted file must fit both arenas: many short values cost the
// most tree per input byte.
void
test_read_jconf_dense_file_fits()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fputs("{\"method\":\"aes-128-gcm\",\"password\":\"p\",\"server\":\"h\",\"x\":[", f);
long written = std::ftell(f);
while (written < MAX_CONF_SIZE - 16) {
std::fputs("[0],", f);
written += 4;
}
std::fputs("0]}", f);
std::fclose(f);
jconf_t *conf = read_jconf(path);
SS_CHECK(conf != nullptr);
SS_CHECK(same(conf->method, "aes-128-gcm"));
SS_CHECK(same(conf->remote_addr[0].host, "h"));
std::remove(path);
}
const char *test_executable;
void
test_read_jconf_rejects_out_of_range_int_options()
{
char path[4096];
FILE *f = test_tempfile(path, sizeof(path));
SS_CHECK(f != nullptr);
std::fclose(f);
write_config(path, "{\n\"server\":\"127.0.0.1\",\n\"server_port\":8388,\n\"password\":\"password\",\n"
"\"method\":\"aes-128-gcm\",\n\"tcp_incoming_sndbuf\":2147483648\n}\n");
#ifdef _WIN32
char command[3 * 4096];
SS_CHECK(std::snprintf(command, sizeof(command), "\"%s\" --read-config \"%s\"", test_executable, path)
< static_cast<int>(sizeof(command)));
STARTUPINFOA startup = {};
PROCESS_INFORMATION child = {};
startup.cb = sizeof(startup);
SS_CHECK(CreateProcessA(test_executable, command, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup,
&child));
CloseHandle(child.hThread);
SS_CHECK(WaitForSingleObject(child.hProcess, 10000) == WAIT_OBJECT_0);
DWORD status;
SS_CHECK(GetExitCodeProcess(child.hProcess, &status));
CloseHandle(child.hProcess);
// FATAL exits with -1, which is a valid nonzero DWORD exit status.
SS_CHECK(status != 0);
#else
std::fflush(stdout); // the child's FATAL exit would flush it again
pid_t pid = fork();
SS_CHECK(pid >= 0);
if (pid == 0) {
read_jconf(path);
_exit(0);
}
int status;
SS_CHECK(waitpid(pid, &status, 0) == pid);
SS_CHECK(WIFEXITED(status));
SS_CHECK(WEXITSTATUS(status) != 0);
#endif
std::remove(path);
}
void
test_server_endpoints()
{
const AddrCase valid[] = {
{ "example.com:8388", "example.com", "8388" },
{ "127.0.0.1:1", "127.0.0.1", "1" },
{ "[2001:db8::1]:65535", "2001:db8::1", "65535" },
{ "[fe80::1%eth0]:8388", "fe80::1%eth0", "8388" },
{ "[fe80::1%3]:8388", "fe80::1%3", "8388" },
{ "2001:db8::1:8388", "2001:db8::1:8388", nullptr },
{ "fe80::1%eth0", "fe80::1%eth0", nullptr },
{ "::", "::", nullptr },
{ "[::1]", "::1", nullptr },
{ "example.com", "example.com", nullptr },
};
for (const AddrCase &c : valid) {
ss_addr_t addr = {};
SS_CHECK(parse_server_endpoint(c.input, &addr) == 0);
SS_CHECK(same(addr.host, c.host));
SS_CHECK(same(addr.port, c.port));
free_addr(&addr);
}
const char *invalid[] = { "", ":8388", "host:", "host:0", "host:65536",
"host:-1", "host:+1", "host:abc", "host:80:90", "[::1", "[::1]junk",
"[::1]:", "[::1]:0", "[::1]:65536", "[::1]:80:90", "[]:80",
"[example.com]:80", "[127.0.0.1]:80", "[bad::host]:80", "host]:80",
"[fe80::1%]:80", "[fe80::1%a%b]:80", "host :80", "ss://host:80" };
for (const char *input : invalid) {
ss_addr_t addr = {};
SS_CHECK(parse_server_endpoint(input, &addr) == -1);
SS_CHECK(addr.host == nullptr && addr.port == nullptr);
}
ss_addr_t addr[2] = {};
SS_CHECK(parse_server_endpoint("[::1]:8388", &addr[0]) == 0);
SS_CHECK(parse_server_endpoint("localhost:8389", &addr[1]) == 0);
SS_CHECK(complete_server_ports(addr, 2, nullptr, 0) == 0);
SS_CHECK(complete_server_ports(addr, 2, "9000", 0) == 0);
SS_CHECK(same(addr[0].port, "8388") && same(addr[1].port, "8389"));
SS_CHECK(complete_server_ports(addr, 2, nullptr, 1) == -1);
free_addr(&addr[1]);
addr[1] = {};
SS_CHECK(parse_server_endpoint("::1", &addr[1]) == 0);
SS_CHECK(complete_server_ports(addr, 2, nullptr, 0) == -1);
SS_CHECK(complete_server_ports(addr, 2, "8388", 1) == 0);
SS_CHECK(same(addr[1].port, "8388"));
free_addr(&addr[0]);
free_addr(&addr[1]);
}
} // namespace
int
main(int argc, char **argv)
{
test_executable = argv[0];
test_network_init();
if (argc == 3 && std::strcmp(argv[1], "--read-config") == 0) {
read_jconf(argv[2]);
return 0;
}
SS_RUN(test_server_endpoints);
SS_RUN(test_parse_addr);
SS_RUN(test_read_jconf_preserves_64bit_integer_strings);
SS_RUN(test_read_jconf_copies_out_of_the_parse_tree);
SS_RUN(test_read_jconf_dense_file_fits);
SS_RUN(test_read_jconf_rejects_out_of_range_int_options);
return 0;
}
-161
View File
@@ -1,161 +0,0 @@
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include "json.h"
static void
test_parse_simple_object(void)
{
const char *json_str = "{\"key\": \"value\", \"num\": 42}";
json_value *val = json_parse(json_str, strlen(json_str));
assert(val != NULL);
assert(val->type == json_object);
assert(val->u.object.length == 2);
/* Check first entry */
assert(strcmp(val->u.object.values[0].name, "key") == 0);
assert(val->u.object.values[0].value->type == json_string);
assert(strcmp(val->u.object.values[0].value->u.string.ptr, "value") == 0);
/* Check second entry */
assert(strcmp(val->u.object.values[1].name, "num") == 0);
assert(val->u.object.values[1].value->type == json_integer);
assert(val->u.object.values[1].value->u.integer == 42);
json_value_free(val);
}
static void
test_parse_array(void)
{
const char *json_str = "[1, 2, 3]";
json_value *val = json_parse(json_str, strlen(json_str));
assert(val != NULL);
assert(val->type == json_array);
assert(val->u.array.length == 3);
assert(val->u.array.values[0]->type == json_integer);
assert(val->u.array.values[0]->u.integer == 1);
assert(val->u.array.values[1]->u.integer == 2);
assert(val->u.array.values[2]->u.integer == 3);
json_value_free(val);
}
static void
test_parse_nested(void)
{
const char *json_str = "{\"outer\": {\"inner\": true}}";
json_value *val = json_parse(json_str, strlen(json_str));
assert(val != NULL);
assert(val->type == json_object);
assert(val->u.object.length == 1);
json_value *outer = val->u.object.values[0].value;
assert(outer->type == json_object);
assert(outer->u.object.length == 1);
assert(outer->u.object.values[0].value->type == json_boolean);
assert(outer->u.object.values[0].value->u.boolean != 0);
json_value_free(val);
}
static void
test_parse_types(void)
{
const char *json_str = "{\"s\": \"hello\", \"i\": -5, \"d\": 3.14, \"b\": false, \"n\": null}";
json_value *val = json_parse(json_str, strlen(json_str));
assert(val != NULL);
assert(val->type == json_object);
assert(val->u.object.length == 5);
assert(val->u.object.values[0].value->type == json_string);
assert(val->u.object.values[1].value->type == json_integer);
assert(val->u.object.values[1].value->u.integer == -5);
assert(val->u.object.values[2].value->type == json_double);
assert(val->u.object.values[3].value->type == json_boolean);
assert(val->u.object.values[3].value->u.boolean == 0);
assert(val->u.object.values[4].value->type == json_null);
json_value_free(val);
}
static void
test_parse_invalid(void)
{
/* Missing closing brace */
assert(json_parse("{\"key\": 1", 9) == NULL);
/* Empty string */
assert(json_parse("", 0) == NULL);
/* Just garbage */
assert(json_parse("not json", 8) == NULL);
}
static void
test_parse_truncated_escape_sequences(void)
{
/* Four bytes remain after 'u', but the fourth hex read would hit EOF. */
const char *truncated_unicode_escape = "\"\\uB2D";
assert(json_parse(truncated_unicode_escape, strlen(truncated_unicode_escape)) == NULL);
/* Six bytes remain after the high surrogate, but the sixth read would hit EOF. */
const char *truncated_surrogate_pair = "\"\\udfff\\ufff";
assert(json_parse(truncated_surrogate_pair, strlen(truncated_surrogate_pair)) == NULL);
}
static void
test_parse_truncated_literals(void)
{
assert(json_parse("tru", 3) == NULL);
assert(json_parse("fals", 4) == NULL);
assert(json_parse("nul", 3) == NULL);
}
static void
test_parse_empty_object(void)
{
const char *json_str = "{}";
json_value *val = json_parse(json_str, strlen(json_str));
assert(val != NULL);
assert(val->type == json_object);
assert(val->u.object.length == 0);
json_value_free(val);
}
static void
test_parse_empty_array(void)
{
const char *json_str = "[]";
json_value *val = json_parse(json_str, strlen(json_str));
assert(val != NULL);
assert(val->type == json_array);
assert(val->u.array.length == 0);
json_value_free(val);
}
int
main(void)
{
const char *wide_numbers = "[4294967296,9223372036854775807,-4294967296,0.4294967296]";
json_value *wide = json_parse(wide_numbers, strlen(wide_numbers));
assert(wide != NULL && wide->type == json_array && wide->u.array.length == 4);
assert(wide->u.array.values[0]->u.integer == INT64_C(4294967296));
assert(wide->u.array.values[1]->u.integer == INT64_MAX);
assert(wide->u.array.values[2]->u.integer == -INT64_C(4294967296));
assert(wide->u.array.values[3]->u.dbl > 0.4294967295 && wide->u.array.values[3]->u.dbl < 0.4294967297);
json_value_free(wide);
assert(json_parse("9223372036854775808", 19) == NULL);
test_parse_simple_object();
test_parse_array();
test_parse_nested();
test_parse_types();
test_parse_invalid();
test_parse_truncated_escape_sequences();
test_parse_truncated_literals();
test_parse_empty_object();
test_parse_empty_array();
return 0;
}
+158
View File
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// JSON parser (src/json.cpp): values, nesting and malformed input.
#include <cstdint>
#include <cstring>
#include "json.h"
#include "test_helpers.hpp"
namespace {
json_value *parse(const char *text)
{
return json_parse(text, std::strlen(text));
}
void test_parse_simple_object()
{
json_value *val = parse("{\"key\": \"value\", \"num\": 42}");
SS_CHECK(val != nullptr);
SS_CHECK(val->type == json_object);
SS_CHECK(val->u.object.length == 2);
// First entry.
SS_CHECK(std::strcmp(val->u.object.values[0].name, "key") == 0);
SS_CHECK(val->u.object.values[0].value->type == json_string);
SS_CHECK(std::strcmp(val->u.object.values[0].value->u.string.ptr, "value") == 0);
// Second entry.
SS_CHECK(std::strcmp(val->u.object.values[1].name, "num") == 0);
SS_CHECK(val->u.object.values[1].value->type == json_integer);
SS_CHECK(val->u.object.values[1].value->u.integer == 42);
json_value_free(val);
}
void test_parse_array()
{
json_value *val = parse("[1, 2, 3]");
SS_CHECK(val != nullptr);
SS_CHECK(val->type == json_array);
SS_CHECK(val->u.array.length == 3);
SS_CHECK(val->u.array.values[0]->type == json_integer);
SS_CHECK(val->u.array.values[0]->u.integer == 1);
SS_CHECK(val->u.array.values[1]->u.integer == 2);
SS_CHECK(val->u.array.values[2]->u.integer == 3);
json_value_free(val);
}
void test_parse_nested()
{
json_value *val = parse("{\"outer\": {\"inner\": true}}");
SS_CHECK(val != nullptr);
SS_CHECK(val->type == json_object);
SS_CHECK(val->u.object.length == 1);
json_value *outer = val->u.object.values[0].value;
SS_CHECK(outer->type == json_object);
SS_CHECK(outer->u.object.length == 1);
SS_CHECK(outer->u.object.values[0].value->type == json_boolean);
SS_CHECK(outer->u.object.values[0].value->u.boolean != 0);
json_value_free(val);
}
void test_parse_types()
{
json_value *val = parse("{\"s\": \"hello\", \"i\": -5, \"d\": 3.14, \"b\": false, \"n\": null}");
SS_CHECK(val != nullptr);
SS_CHECK(val->type == json_object);
SS_CHECK(val->u.object.length == 5);
SS_CHECK(val->u.object.values[0].value->type == json_string);
SS_CHECK(val->u.object.values[1].value->type == json_integer);
SS_CHECK(val->u.object.values[1].value->u.integer == -5);
SS_CHECK(val->u.object.values[2].value->type == json_double);
SS_CHECK(val->u.object.values[3].value->type == json_boolean);
SS_CHECK(val->u.object.values[3].value->u.boolean == 0);
SS_CHECK(val->u.object.values[4].value->type == json_null);
json_value_free(val);
}
void test_parse_invalid()
{
// Missing closing brace.
SS_CHECK(json_parse("{\"key\": 1", 9) == nullptr);
// Empty string.
SS_CHECK(json_parse("", 0) == nullptr);
// Just garbage.
SS_CHECK(json_parse("not json", 8) == nullptr);
}
void test_parse_truncated_escape_sequences()
{
// Four bytes remain after 'u', but the fourth hex read would hit EOF.
SS_CHECK(parse("\"\\uB2D") == nullptr);
// Six bytes remain after the high surrogate, but the sixth read would hit EOF.
SS_CHECK(parse("\"\\udfff\\ufff") == nullptr);
}
void test_parse_truncated_literals()
{
SS_CHECK(json_parse("tru", 3) == nullptr);
SS_CHECK(json_parse("fals", 4) == nullptr);
SS_CHECK(json_parse("nul", 3) == nullptr);
}
void test_parse_empty_object()
{
json_value *val = parse("{}");
SS_CHECK(val != nullptr);
SS_CHECK(val->type == json_object);
SS_CHECK(val->u.object.length == 0);
json_value_free(val);
}
void test_parse_empty_array()
{
json_value *val = parse("[]");
SS_CHECK(val != nullptr);
SS_CHECK(val->type == json_array);
SS_CHECK(val->u.array.length == 0);
json_value_free(val);
}
void test_parse_wide_numbers()
{
json_value *wide = parse("[4294967296,9223372036854775807,-4294967296,0.4294967296]");
SS_CHECK(wide != nullptr && wide->type == json_array && wide->u.array.length == 4);
SS_CHECK(wide->u.array.values[0]->u.integer == INT64_C(4294967296));
SS_CHECK(wide->u.array.values[1]->u.integer == INT64_MAX);
SS_CHECK(wide->u.array.values[2]->u.integer == -INT64_C(4294967296));
SS_CHECK(wide->u.array.values[3]->u.dbl > 0.4294967295 && wide->u.array.values[3]->u.dbl < 0.4294967297);
json_value_free(wide);
SS_CHECK(json_parse("9223372036854775808", 19) == nullptr);
}
} // namespace
int main()
{
SS_RUN(test_parse_wide_numbers);
SS_RUN(test_parse_simple_object);
SS_RUN(test_parse_array);
SS_RUN(test_parse_nested);
SS_RUN(test_parse_types);
SS_RUN(test_parse_invalid);
SS_RUN(test_parse_truncated_escape_sequences);
SS_RUN(test_parse_truncated_literals);
SS_RUN(test_parse_empty_object);
SS_RUN(test_parse_empty_array);
return 0;
}
-138
View File
@@ -1,138 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include "test_helpers.h"
int verbose = 0;
#include "netutils.h"
static void
test_get_sockaddr_len(void)
{
struct sockaddr_in addr4;
struct sockaddr_in6 addr6;
struct sockaddr_storage unknown;
memset(&addr4, 0, sizeof(addr4));
addr4.sin_family = AF_INET;
assert(get_sockaddr_len((struct sockaddr *)&addr4) == sizeof(struct sockaddr_in));
memset(&addr6, 0, sizeof(addr6));
addr6.sin6_family = AF_INET6;
assert(get_sockaddr_len((struct sockaddr *)&addr6) == sizeof(struct sockaddr_in6));
memset(&unknown, 0, sizeof(unknown));
unknown.ss_family = AF_UNSPEC;
assert(get_sockaddr_len((struct sockaddr *)&unknown) == 0);
}
static void
test_sockaddr_cmp(void)
{
struct sockaddr_storage a, b;
struct sockaddr_in *a4 = (struct sockaddr_in *)&a;
struct sockaddr_in *b4 = (struct sockaddr_in *)&b;
/* Same address and port */
memset(&a, 0, sizeof(a));
memset(&b, 0, sizeof(b));
a4->sin_family = AF_INET;
b4->sin_family = AF_INET;
a4->sin_port = htons(80);
b4->sin_port = htons(80);
inet_pton(AF_INET, "127.0.0.1", &a4->sin_addr);
inet_pton(AF_INET, "127.0.0.1", &b4->sin_addr);
assert(sockaddr_cmp(&a, &b, sizeof(struct sockaddr_in)) == 0);
/* Different port */
b4->sin_port = htons(81);
assert(sockaddr_cmp(&a, &b, sizeof(struct sockaddr_in)) != 0);
}
static void
test_sockaddr_cmp_addr(void)
{
struct sockaddr_storage a, b;
struct sockaddr_in *a4 = (struct sockaddr_in *)&a;
struct sockaddr_in *b4 = (struct sockaddr_in *)&b;
memset(&a, 0, sizeof(a));
memset(&b, 0, sizeof(b));
a4->sin_family = AF_INET;
b4->sin_family = AF_INET;
a4->sin_port = htons(80);
b4->sin_port = htons(443);
inet_pton(AF_INET, "10.0.0.1", &a4->sin_addr);
inet_pton(AF_INET, "10.0.0.1", &b4->sin_addr);
/* Same address, different port - should be equal */
assert(sockaddr_cmp_addr(&a, &b, sizeof(struct sockaddr_in)) == 0);
/* Different address */
inet_pton(AF_INET, "10.0.0.2", &b4->sin_addr);
assert(sockaddr_cmp_addr(&a, &b, sizeof(struct sockaddr_in)) != 0);
}
static void
test_validate_hostname(void)
{
/* Valid hostnames */
assert(validate_hostname("example.com", 11) == 1);
assert(validate_hostname("sub.example.com", 15) == 1);
assert(validate_hostname("a", 1) == 1);
assert(validate_hostname("a-b", 3) == 1);
assert(validate_hostname("123.456", 7) == 1);
/* Invalid hostnames */
assert(validate_hostname(NULL, 0) == 0);
assert(validate_hostname("", 0) == 0);
assert(validate_hostname(".example.com", 12) == 0); /* starts with dot */
assert(validate_hostname("-example.com", 12) == 0); /* label starts with hyphen */
assert(validate_hostname("example-.com", 12) == 0); /* label ends with hyphen */
/* Too long hostname (> 255) */
char long_name[260];
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("localhost", "8388", &storage, 0, 0) == -1);
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
main(void)
{
test_network_init();
test_get_sockaddr_len();
test_sockaddr_cmp();
test_sockaddr_cmp_addr();
test_validate_hostname();
test_get_sockaddr_rejects_invalid_ip_literal_ports();
return 0;
}
+140
View File
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Address helpers (src/netutils.cpp): sockaddr comparison, hostname
// validation and literal-address parsing.
#include <cstring>
#include "netutils.h"
#include "test_helpers.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
// get_sockaddr() takes writable strings; copy the literals.
ssize_t resolve(const char *host, const char *port, sockaddr_storage *storage)
{
char host_buf[64];
char port_buf[16];
SS_CHECK(std::strlen(host) < sizeof(host_buf) && std::strlen(port) < sizeof(port_buf));
std::strcpy(host_buf, host);
std::strcpy(port_buf, port);
return get_sockaddr(host_buf, port_buf, storage, 0, 0);
}
void test_get_sockaddr_len()
{
sockaddr_in addr4{};
addr4.sin_family = AF_INET;
SS_CHECK(get_sockaddr_len(reinterpret_cast<sockaddr *>(&addr4)) == sizeof(sockaddr_in));
sockaddr_in6 addr6{};
addr6.sin6_family = AF_INET6;
SS_CHECK(get_sockaddr_len(reinterpret_cast<sockaddr *>(&addr6)) == sizeof(sockaddr_in6));
sockaddr_storage unknown{};
unknown.ss_family = AF_UNSPEC;
SS_CHECK(get_sockaddr_len(reinterpret_cast<sockaddr *>(&unknown)) == 0);
}
void test_sockaddr_cmp()
{
sockaddr_storage a{};
sockaddr_storage b{};
auto *a4 = reinterpret_cast<sockaddr_in *>(&a);
auto *b4 = reinterpret_cast<sockaddr_in *>(&b);
// Same address and port.
a4->sin_family = AF_INET;
b4->sin_family = AF_INET;
a4->sin_port = htons(80);
b4->sin_port = htons(80);
inet_pton(AF_INET, "127.0.0.1", &a4->sin_addr);
inet_pton(AF_INET, "127.0.0.1", &b4->sin_addr);
SS_CHECK(sockaddr_cmp(&a, &b, sizeof(sockaddr_in)) == 0);
// Different port.
b4->sin_port = htons(81);
SS_CHECK(sockaddr_cmp(&a, &b, sizeof(sockaddr_in)) != 0);
}
void test_sockaddr_cmp_addr()
{
sockaddr_storage a{};
sockaddr_storage b{};
auto *a4 = reinterpret_cast<sockaddr_in *>(&a);
auto *b4 = reinterpret_cast<sockaddr_in *>(&b);
a4->sin_family = AF_INET;
b4->sin_family = AF_INET;
a4->sin_port = htons(80);
b4->sin_port = htons(443);
inet_pton(AF_INET, "10.0.0.1", &a4->sin_addr);
inet_pton(AF_INET, "10.0.0.1", &b4->sin_addr);
// Same address, different port - should be equal.
SS_CHECK(sockaddr_cmp_addr(&a, &b, sizeof(sockaddr_in)) == 0);
// Different address.
inet_pton(AF_INET, "10.0.0.2", &b4->sin_addr);
SS_CHECK(sockaddr_cmp_addr(&a, &b, sizeof(sockaddr_in)) != 0);
}
void test_validate_hostname()
{
// Valid hostnames.
SS_CHECK(validate_hostname("example.com", 11) == 1);
SS_CHECK(validate_hostname("sub.example.com", 15) == 1);
SS_CHECK(validate_hostname("a", 1) == 1);
SS_CHECK(validate_hostname("a-b", 3) == 1);
SS_CHECK(validate_hostname("123.456", 7) == 1);
// Invalid hostnames.
SS_CHECK(validate_hostname(nullptr, 0) == 0);
SS_CHECK(validate_hostname("", 0) == 0);
SS_CHECK(validate_hostname(".example.com", 12) == 0); // starts with dot
SS_CHECK(validate_hostname("-example.com", 12) == 0); // label starts with hyphen
SS_CHECK(validate_hostname("example-.com", 12) == 0); // label ends with hyphen
// Too long hostname (> 255).
char long_name[260];
std::memset(long_name, 'a', 259);
long_name[259] = '\0';
SS_CHECK(validate_hostname(long_name, 259) == 0);
// Explicit length must be honored; callers may pass non-NUL data.
const char raw_name[3] = { 'a', 'b', 'c' };
SS_CHECK(validate_hostname(raw_name, 3) == 1);
}
void test_get_sockaddr_rejects_invalid_ip_literal_ports()
{
sockaddr_storage storage{};
auto *addr4 = reinterpret_cast<sockaddr_in *>(&storage);
SS_CHECK(resolve("localhost", "8388", &storage) == -1);
SS_CHECK(resolve("127.0.0.1", "8388", &storage) == 0);
SS_CHECK(addr4->sin_family == AF_INET);
SS_CHECK(ntohs(addr4->sin_port) == 8388);
storage = sockaddr_storage{};
SS_CHECK(resolve("127.0.0.1", "70000", &storage) == -1);
storage = sockaddr_storage{};
SS_CHECK(resolve("127.0.0.1", "12x", &storage) == -1);
}
} // namespace
int main()
{
test_network_init();
SS_RUN(test_get_sockaddr_len);
SS_RUN(test_sockaddr_cmp);
SS_RUN(test_sockaddr_cmp_addr);
SS_RUN(test_validate_hostname);
SS_RUN(test_get_sockaddr_rejects_invalid_ip_literal_ports);
return 0;
}
-74
View File
@@ -1,74 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
int verbose = 0;
#include "ppbloom.h"
static void
test_init_free(void)
{
int ret = ppbloom_init(1000, 0.01);
assert(ret == 0);
(void)ret;
ppbloom_free();
}
static void
test_add_check(void)
{
ppbloom_init(1000, 0.01);
const char *item1 = "hello";
const char *item2 = "world";
const char *item3 = "missing";
/* Not in filter initially */
assert(ppbloom_check(item1, strlen(item1)) == 0);
assert(ppbloom_check(item2, strlen(item2)) == 0);
/* Add items */
ppbloom_add(item1, strlen(item1));
ppbloom_add(item2, strlen(item2));
/* Should be found */
assert(ppbloom_check(item1, strlen(item1)) == 1);
assert(ppbloom_check(item2, strlen(item2)) == 1);
/* Should not be found */
assert(ppbloom_check(item3, strlen(item3)) == 0);
(void)item3;
ppbloom_free();
}
static void
test_binary_data(void)
{
ppbloom_init(1000, 0.01);
const uint8_t data1[] = { 0x00, 0x01, 0x02, 0x03 };
const uint8_t data2[] = { 0xFF, 0xFE, 0xFD, 0xFC };
ppbloom_add(data1, sizeof(data1));
assert(ppbloom_check(data1, sizeof(data1)) == 1);
assert(ppbloom_check(data2, sizeof(data2)) == 0);
(void)data2;
ppbloom_free();
}
int
main(void)
{
test_init_free();
test_add_check();
test_binary_data();
return 0;
}
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Ping-pong bloom filter (src/ppbloom.cpp): add and membership checks.
#include <cstdint>
#include <cstring>
#include "ppbloom.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
void test_init_free()
{
SS_CHECK(ppbloom_init(1000, 0.01) == 0);
ppbloom_free();
}
void test_add_check()
{
ppbloom_init(1000, 0.01);
const char *item1 = "hello";
const char *item2 = "world";
const char *item3 = "missing";
// Not in the filter initially.
SS_CHECK(ppbloom_check(item1, std::strlen(item1)) == 0);
SS_CHECK(ppbloom_check(item2, std::strlen(item2)) == 0);
ppbloom_add(item1, std::strlen(item1));
ppbloom_add(item2, std::strlen(item2));
SS_CHECK(ppbloom_check(item1, std::strlen(item1)) == 1);
SS_CHECK(ppbloom_check(item2, std::strlen(item2)) == 1);
SS_CHECK(ppbloom_check(item3, std::strlen(item3)) == 0);
ppbloom_free();
}
void test_binary_data()
{
ppbloom_init(1000, 0.01);
const uint8_t data1[] = { 0x00, 0x01, 0x02, 0x03 };
const uint8_t data2[] = { 0xFF, 0xFE, 0xFD, 0xFC };
ppbloom_add(data1, sizeof(data1));
SS_CHECK(ppbloom_check(data1, sizeof(data1)) == 1);
SS_CHECK(ppbloom_check(data2, sizeof(data2)) == 0);
ppbloom_free();
}
} // namespace
int main()
{
SS_RUN(test_init_free);
SS_RUN(test_add_check);
SS_RUN(test_binary_data);
return 0;
}
-85
View File
@@ -1,85 +0,0 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ss_process.h"
#ifdef _WIN32
#include <windows.h>
static void pause_tick(void) { Sleep(10); }
#else
#include <time.h>
static void pause_tick(void) { struct timespec delay = {0, 10000000}; nanosleep(&delay, NULL); }
#endif
int verbose = 0;
int
main(int argc, char **argv)
{
if (argc > 1 && strcmp(argv[1], "--child") == 0) {
assert(argc == 5);
FILE *file = fopen(argv[2], "w");
assert(file != NULL);
assert(strcmp(argv[3], "spaces and \"quotes\" with \\") == 0);
assert(strcmp(argv[4], "") == 0);
assert(strcmp(getenv("SS_PROCESS_TEST"), "child value") == 0);
fputs("ready\n", file);
fclose(file);
for (unsigned i = 0; i < 1000; i++) pause_tick();
return 0;
}
if (argc > 1 && strcmp(argv[1], "--exit") == 0) return 17;
struct ss_process *process = ss_process_new("ss-no-such-program-ef91ac");
ss_process_arg(process, "ss-no-such-program-ef91ac");
int started = ss_process_start(process, 0);
assert(started == -1 || started == 0);
/* POSIX permits exec failure to be reported by child exit (notably under
* Valgrind's spawn wrapper), rather than synchronously by posix_spawnp. */
if (started == 0) {
for (unsigned i = 0; i < 500 && ss_process_running(process); i++) pause_tick();
assert(!ss_process_running(process));
}
ss_process_free(process);
char path[4096];
assert(snprintf(path, sizeof(path), "%s.child-output", argv[0]) < (int)sizeof(path));
remove(path);
process = ss_process_new(argv[0]);
ss_process_arg(process, argv[0]);
ss_process_arg(process, "--child");
ss_process_arg(process, path);
ss_process_arg(process, "spaces and \"quotes\" with \\");
ss_process_arg(process, "");
ss_process_env(process, "SS_PROCESS_TEST", "child value");
const char *before = getenv("SS_PROCESS_TEST");
char *saved = before == NULL ? NULL : strdup(before);
assert(ss_process_start(process, 0) == 0);
bool ready = false;
for (unsigned i = 0; i < 500; i++) {
FILE *file = fopen(path, "r");
if (file != NULL) {
char line[16];
ready = fgets(line, sizeof(line), file) != NULL && strcmp(line, "ready\n") == 0;
fclose(file);
}
if (ready) break;
pause_tick();
}
assert(ready);
assert(ss_process_running(process));
const char *after = getenv("SS_PROCESS_TEST");
assert(saved == NULL ? after == NULL : after != NULL && strcmp(after, saved) == 0);
free(saved);
ss_process_free(process);
remove(path);
process = ss_process_new(argv[0]);
ss_process_arg(process, argv[0]);
ss_process_arg(process, "--exit");
assert(ss_process_start(process, 0) == 0);
for (unsigned i = 0; i < 500 && ss_process_running(process); i++) pause_tick();
assert(!ss_process_running(process));
ss_process_free(process);
ss_process_free(NULL);
return 0;
}
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Child process management (src/process.cpp): spawn failure, argument and
// environment passing, and exit detection. The test binary re-executes itself
// as the child via --child / --exit.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include "ss_process.h"
#include "test_helpers.hpp"
#ifdef _WIN32
#include <windows.h>
#else
#include <time.h>
#endif
extern "C" {
int verbose = 0;
}
namespace {
void pause_tick()
{
#ifdef _WIN32
Sleep(10);
#else
timespec delay = { 0, 10000000 };
nanosleep(&delay, nullptr);
#endif
}
const char *const k_tricky_arg = "spaces and \"quotes\" with \\";
int run_child(int argc, char **argv)
{
SS_CHECK(argc == 5);
FILE *file = std::fopen(argv[2], "w");
SS_CHECK(file != nullptr);
SS_CHECK(std::strcmp(argv[3], k_tricky_arg) == 0);
SS_CHECK(std::strcmp(argv[4], "") == 0);
const char *env = std::getenv("SS_PROCESS_TEST");
SS_CHECK(env != nullptr && std::strcmp(env, "child value") == 0);
std::fputs("ready\n", file);
std::fclose(file);
for (unsigned i = 0; i < 1000; i++) {
pause_tick();
}
return 0;
}
void test_missing_program()
{
ss_process *process = ss_process_new("ss-no-such-program-ef91ac");
ss_process_arg(process, "ss-no-such-program-ef91ac");
const int started = ss_process_start(process, 0);
SS_CHECK(started == -1 || started == 0);
// POSIX permits exec failure to be reported by child exit (notably under
// Valgrind's spawn wrapper), rather than synchronously by posix_spawnp.
if (started == 0) {
for (unsigned i = 0; i < 500 && ss_process_running(process); i++) {
pause_tick();
}
SS_CHECK(!ss_process_running(process));
}
ss_process_free(process);
}
void test_args_and_environment(const char *self)
{
const std::string path = std::string(self) + ".child-output";
std::remove(path.c_str());
ss_process *process = ss_process_new(self);
ss_process_arg(process, self);
ss_process_arg(process, "--child");
ss_process_arg(process, path.c_str());
ss_process_arg(process, k_tricky_arg);
ss_process_arg(process, "");
ss_process_env(process, "SS_PROCESS_TEST", "child value");
const char *before = std::getenv("SS_PROCESS_TEST");
const std::string saved(before == nullptr ? "" : before);
const bool had_value = before != nullptr;
SS_CHECK(ss_process_start(process, 0) == 0);
bool ready = false;
for (unsigned i = 0; i < 500; i++) {
FILE *file = std::fopen(path.c_str(), "r");
if (file != nullptr) {
char line[16];
ready = std::fgets(line, sizeof(line), file) != nullptr && std::strcmp(line, "ready\n") == 0;
std::fclose(file);
}
if (ready) {
break;
}
pause_tick();
}
SS_CHECK(ready);
SS_CHECK(ss_process_running(process));
// The parent's environment is left untouched.
const char *after = std::getenv("SS_PROCESS_TEST");
SS_CHECK(had_value ? after != nullptr && saved == after : after == nullptr);
ss_process_free(process);
std::remove(path.c_str());
}
void test_exit_detection(const char *self)
{
ss_process *process = ss_process_new(self);
ss_process_arg(process, self);
ss_process_arg(process, "--exit");
SS_CHECK(ss_process_start(process, 0) == 0);
for (unsigned i = 0; i < 500 && ss_process_running(process); i++) {
pause_tick();
}
SS_CHECK(!ss_process_running(process));
ss_process_free(process);
ss_process_free(nullptr);
}
} // namespace
int main(int argc, char **argv)
{
if (argc > 1 && std::strcmp(argv[1], "--child") == 0) {
return run_child(argc, argv);
}
if (argc > 1 && std::strcmp(argv[1], "--exit") == 0) {
return 17;
}
SS_RUN(test_missing_program);
test_args_and_environment(argv[0]);
std::printf("ok test_args_and_environment\n");
test_exit_detection(argv[0]);
std::printf("ok test_exit_detection\n");
return 0;
}
-63
View File
@@ -1,63 +0,0 @@
#include <assert.h>
#include <stdlib.h>
#include "ss_ref.h"
typedef struct {
ss_ref_t ref;
int *destroyed;
} test_object_t;
static void
destroy_object(void *object)
{
test_object_t *value = object;
++*value->destroyed;
free(value);
}
static void *
new_object(int *destroyed)
{
test_object_t *value = malloc(sizeof(*value));
assert(value != NULL);
ss_ref_init(value, destroy_object);
value->destroyed = destroyed;
return value;
}
int
main(void)
{
int destroyed = 0;
void *table = new_object(&destroyed);
void *relay = NULL;
ss_ref_assign(&relay, table);
ss_ref_assign(&relay, relay); /* Self-assignment must preserve ownership. */
ss_ptr_reset(&table, ss_ref_release); /* Simulate eviction. */
assert(table == NULL && destroyed == 0);
ss_ptr_reset(&relay, ss_ref_release);
assert(relay == NULL && destroyed == 1);
ss_ptr_reset(&relay, ss_ref_release); /* Repeated cleanup is harmless. */
assert(destroyed == 1);
void *query = new_object(&destroyed);
relay = new_object(&destroyed);
ss_ptr_move(&relay, &query, ss_ref_release);
assert(query == NULL && relay != NULL && destroyed == 2);
ss_ptr_move(&relay, &relay, ss_ref_release); /* Self-move. */
assert(relay != NULL && destroyed == 2);
ss_ref_assign(&query, relay);
ss_ptr_move(&relay, &query, ss_ref_release); /* Two owners, same object. */
assert(query == NULL && relay != NULL && destroyed == 2);
ss_ref_assign(&relay, NULL);
assert(relay == NULL && destroyed == 3);
relay = new_object(&destroyed);
ss_ptr_move(&relay, &query, ss_ref_release); /* Move an empty source. */
assert(relay == NULL && query == NULL && destroyed == 4);
assert(ss_ref_retain(NULL) == NULL);
ss_ref_release(NULL);
ss_ptr_reset(&relay, NULL); /* Legacy crypto has no session/callback. */
return 0;
}
+72
View File
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Intrusive reference counting (tests/reference/ss_ref.h): assignment,
// move and release helpers used by the legacy relay code.
#include <cstdlib>
#include "ss_ref.h"
#include "test_helpers.hpp"
namespace {
struct test_object_t {
ss_ref_t ref;
int *destroyed;
};
void destroy_object(void *object)
{
auto *value = static_cast<test_object_t *>(object);
++*value->destroyed;
std::free(value);
}
void *new_object(int *destroyed)
{
auto *value = static_cast<test_object_t *>(std::malloc(sizeof(test_object_t)));
SS_CHECK(value != nullptr);
ss_ref_init(value, destroy_object);
value->destroyed = destroyed;
return value;
}
void test_ref_lifecycle()
{
int destroyed = 0;
void *table = new_object(&destroyed);
void *relay = nullptr;
ss_ref_assign(&relay, table);
ss_ref_assign(&relay, relay); // Self-assignment must preserve ownership.
ss_ptr_reset(&table, ss_ref_release); // Simulate eviction.
SS_CHECK(table == nullptr && destroyed == 0);
ss_ptr_reset(&relay, ss_ref_release);
SS_CHECK(relay == nullptr && destroyed == 1);
ss_ptr_reset(&relay, ss_ref_release); // Repeated cleanup is harmless.
SS_CHECK(destroyed == 1);
void *query = new_object(&destroyed);
relay = new_object(&destroyed);
ss_ptr_move(&relay, &query, ss_ref_release);
SS_CHECK(query == nullptr && relay != nullptr && destroyed == 2);
ss_ptr_move(&relay, &relay, ss_ref_release); // Self-move.
SS_CHECK(relay != nullptr && destroyed == 2);
ss_ref_assign(&query, relay);
ss_ptr_move(&relay, &query, ss_ref_release); // Two owners, same object.
SS_CHECK(query == nullptr && relay != nullptr && destroyed == 2);
ss_ref_assign(&relay, nullptr);
SS_CHECK(relay == nullptr && destroyed == 3);
relay = new_object(&destroyed);
ss_ptr_move(&relay, &query, ss_ref_release); // Move an empty source.
SS_CHECK(relay == nullptr && query == nullptr && destroyed == 4);
SS_CHECK(ss_ref_retain(nullptr) == nullptr);
ss_ref_release(nullptr);
ss_ptr_reset(&relay, nullptr); // Legacy crypto has no session/callback.
}
} // namespace
int main()
{
SS_RUN(test_ref_lifecycle);
return 0;
}
-232
View File
@@ -1,232 +0,0 @@
/*
* Unit tests for ss:// URI parsing (SIP002 and the legacy form).
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
int verbose = 0;
#include "ssurl.h"
#define STREQ(a, b) ((a) != NULL && strcmp((a), (b)) == 0)
/* ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888 */
static void
test_sip002_basic(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888",
&u) == 0);
assert(STREQ(u.method, "aes-256-gcm"));
assert(STREQ(u.password, "password"));
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
assert(u.plugin == NULL);
ss_url_free(&u);
}
/* A tag after '#' is descriptive only and must not leak into the fields. */
static void
test_sip002_tag(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
"#Example%20Server", &u) == 0);
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.tag, "Example Server"));
ss_url_free(&u);
}
/* SIP003 plugin, with its options after the ';'. */
static void
test_sip002_plugin(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
"/?plugin=obfs-local%3Bobfs%3Dhttp#tag", &u) == 0);
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
assert(STREQ(u.plugin, "obfs-local"));
assert(STREQ(u.plugin_opts, "obfs=http"));
assert(STREQ(u.tag, "tag"));
ss_url_free(&u);
}
/* The trailing slash before a query is allowed, and so is no query at all. */
static void
test_sip002_trailing_slash(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@example.com:8388/",
&u) == 0);
assert(STREQ(u.host, "example.com"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* A bracketed IPv6 literal must keep its address and port apart. */
static void
test_sip002_ipv6(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@[2001:db8::1]:8388",
&u) == 0);
assert(STREQ(u.host, "2001:db8::1"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* Some producers write the userinfo in the clear rather than base64. */
static void
test_sip002_plain_userinfo(void)
{
ss_url_t u;
assert(ss_url_parse("ss://aes-256-gcm:secret@example.com:8388", &u) == 0);
assert(STREQ(u.method, "aes-256-gcm"));
assert(STREQ(u.password, "secret"));
assert(STREQ(u.host, "example.com"));
ss_url_free(&u);
}
/*
* A 2022 pre-shared key is base64 and can contain '+', '/' and '='. Encoded
* into the userinfo it must survive the round trip byte for byte.
*/
static void
test_sip002_2022_psk(void)
{
ss_url_t u;
/* base64("2022-blake3-aes-256-gcm:ZG9uJ3QgdXNlIHRoaXMga2V5Pz8/Pz8/Pz8=") */
const char *uri =
"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wkc5dUozUWdkWE5sSUhSb2FYTWdhMlY1"
"Pz8_Pz8_Pz89@example.com:8388";
assert(ss_url_parse(uri, &u) == 0);
assert(STREQ(u.method, "2022-blake3-aes-256-gcm"));
assert(STREQ(u.host, "example.com"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* Legacy form: the whole body is one base64 blob. */
static void
test_legacy(void)
{
ss_url_t u;
/* base64("aes-256-cfb:password@192.168.100.1:8888") */
assert(ss_url_parse("ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAMTkyLjE2OC4xMDAuMTo4ODg4",
&u) == 0);
assert(STREQ(u.method, "aes-256-cfb"));
assert(STREQ(u.password, "password"));
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
ss_url_free(&u);
}
/* Malformed input must be rejected, not half-parsed. */
static void
test_rejects_malformed(void)
{
ss_url_t u;
static const char *bad[] = {
NULL,
"",
"http://example.com",
"ss://",
"ss://@example.com:8388", /* empty userinfo */
"ss://YWVzOnB3@example.com", /* no port */
"ss://YWVzOnB3@:8388", /* no host */
"ss://YWVzOnB3@example.com:", /* empty port */
"ss://bm9jb2xvbg@example.com:8388", /* userinfo lacks ':' */
"ss://YWVzOnB3@[2001:db8::1", /* unterminated bracket */
};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
memset(&u, 0xa5, sizeof(u));
assert(ss_url_parse(bad[i], &u) == -1);
/* a rejected parse must leave nothing behind to free */
assert(u.method == NULL && u.host == NULL && u.port == NULL);
}
}
/*
* URIs emitted by shadowsocks-rust's `ssurl --encode`, copied verbatim.
* These pin the shapes another implementation actually produces, which is
* what the parser has to accept in the field.
*/
static void
test_rust_ssurl_output(void)
{
ss_url_t u;
/* A 2022 cipher: plain userinfo, '=' of the key percent-encoded. */
assert(ss_url_parse("ss://2022-blake3-aes-256-gcm:"
"ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE%3D"
"@192.168.100.1:8888", &u) == 0);
assert(STREQ(u.method, "2022-blake3-aes-256-gcm"));
assert(STREQ(u.password, "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE="));
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
ss_url_free(&u);
/* A legacy cipher: unpadded base64 userinfo, plugin in the query. */
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206dGVzdC1wYXNzd29yZA"
"@192.168.100.1:8888/?plugin=obfs%2Dlocal%3Bobfs%3Dhttp",
&u) == 0);
assert(STREQ(u.method, "aes-256-gcm"));
assert(STREQ(u.password, "test-password"));
assert(STREQ(u.plugin, "obfs-local"));
assert(STREQ(u.plugin_opts, "obfs=http"));
ss_url_free(&u);
/* An IPv6 server. */
assert(ss_url_parse("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwdw"
"@[2001:db8::1]:8388", &u) == 0);
assert(STREQ(u.method, "chacha20-ietf-poly1305"));
assert(STREQ(u.password, "pw"));
assert(STREQ(u.host, "2001:db8::1"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* Freeing a zeroed struct, or NULL, must be safe. */
static void
test_free_is_safe(void)
{
ss_url_t u = { 0 };
ss_url_free(&u);
ss_url_free(NULL);
}
int
main(void)
{
test_sip002_basic();
test_sip002_tag();
test_sip002_plugin();
test_sip002_trailing_slash();
test_sip002_ipv6();
test_sip002_plain_userinfo();
test_sip002_2022_psk();
test_legacy();
test_rust_ssurl_output();
test_rejects_malformed();
test_free_is_safe();
return 0;
}
+224
View File
@@ -0,0 +1,224 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// ss:// URI parsing (src/ssurl.cpp): SIP002, the legacy form and malformed
// input.
#include <cstddef>
#include <cstring>
#include "ssurl.h"
#include "test_helpers.hpp"
extern "C" {
int verbose = 0;
}
namespace {
bool streq(const char *a, const char *b)
{
return a != nullptr && std::strcmp(a, b) == 0;
}
/* ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888 */
void test_sip002_basic()
{
ss_url_t u;
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888",
&u) == 0);
SS_CHECK(streq(u.method, "aes-256-gcm"));
SS_CHECK(streq(u.password, "password"));
SS_CHECK(streq(u.host, "192.168.100.1"));
SS_CHECK(streq(u.port, "8888"));
SS_CHECK(u.plugin == nullptr);
ss_url_free(&u);
}
/* A tag after '#' is descriptive only and must not leak into the fields. */
void test_sip002_tag()
{
ss_url_t u;
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
"#Example%20Server", &u) == 0);
SS_CHECK(streq(u.host, "192.168.100.1"));
SS_CHECK(streq(u.tag, "Example Server"));
ss_url_free(&u);
}
/* SIP003 plugin, with its options after the ';'. */
void test_sip002_plugin()
{
ss_url_t u;
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
"/?plugin=obfs-local%3Bobfs%3Dhttp#tag", &u) == 0);
SS_CHECK(streq(u.host, "192.168.100.1"));
SS_CHECK(streq(u.port, "8888"));
SS_CHECK(streq(u.plugin, "obfs-local"));
SS_CHECK(streq(u.plugin_opts, "obfs=http"));
SS_CHECK(streq(u.tag, "tag"));
ss_url_free(&u);
}
/* The trailing slash before a query is allowed, and so is no query at all. */
void test_sip002_trailing_slash()
{
ss_url_t u;
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@example.com:8388/",
&u) == 0);
SS_CHECK(streq(u.host, "example.com"));
SS_CHECK(streq(u.port, "8388"));
ss_url_free(&u);
}
/* A bracketed IPv6 literal must keep its address and port apart. */
void test_sip002_ipv6()
{
ss_url_t u;
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@[2001:db8::1]:8388",
&u) == 0);
SS_CHECK(streq(u.host, "2001:db8::1"));
SS_CHECK(streq(u.port, "8388"));
ss_url_free(&u);
}
/* Some producers write the userinfo in the clear rather than base64. */
void test_sip002_plain_userinfo()
{
ss_url_t u;
SS_CHECK(ss_url_parse("ss://aes-256-gcm:secret@example.com:8388", &u) == 0);
SS_CHECK(streq(u.method, "aes-256-gcm"));
SS_CHECK(streq(u.password, "secret"));
SS_CHECK(streq(u.host, "example.com"));
ss_url_free(&u);
}
/*
* A 2022 pre-shared key is base64 and can contain '+', '/' and '='. Encoded
* into the userinfo it must survive the round trip byte for byte.
*/
void test_sip002_2022_psk()
{
ss_url_t u;
/* base64("2022-blake3-aes-256-gcm:ZG9uJ3QgdXNlIHRoaXMga2V5Pz8/Pz8/Pz8=") */
const char *uri =
"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wkc5dUozUWdkWE5sSUhSb2FYTWdhMlY1"
"Pz8_Pz8_Pz89@example.com:8388";
SS_CHECK(ss_url_parse(uri, &u) == 0);
SS_CHECK(streq(u.method, "2022-blake3-aes-256-gcm"));
SS_CHECK(streq(u.host, "example.com"));
SS_CHECK(streq(u.port, "8388"));
ss_url_free(&u);
}
/* Legacy form: the whole body is one base64 blob. */
void test_legacy()
{
ss_url_t u;
/* base64("aes-256-cfb:password@192.168.100.1:8888") */
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAMTkyLjE2OC4xMDAuMTo4ODg4",
&u) == 0);
SS_CHECK(streq(u.method, "aes-256-cfb"));
SS_CHECK(streq(u.password, "password"));
SS_CHECK(streq(u.host, "192.168.100.1"));
SS_CHECK(streq(u.port, "8888"));
ss_url_free(&u);
}
/* Malformed input must be rejected, not half-parsed. */
void test_rejects_malformed()
{
ss_url_t u;
static const char *const bad[] = {
nullptr,
"",
"http://example.com",
"ss://",
"ss://@example.com:8388", /* empty userinfo */
"ss://YWVzOnB3@example.com", /* no port */
"ss://YWVzOnB3@:8388", /* no host */
"ss://YWVzOnB3@example.com:", /* empty port */
"ss://bm9jb2xvbg@example.com:8388", /* userinfo lacks ':' */
"ss://YWVzOnB3@[2001:db8::1", /* unterminated bracket */
};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
std::memset(&u, 0xa5, sizeof(u));
SS_CHECK(ss_url_parse(bad[i], &u) == -1);
/* a rejected parse must leave nothing behind to free */
SS_CHECK(u.method == nullptr && u.host == nullptr && u.port == nullptr);
}
}
/*
* URIs emitted by shadowsocks-rust's `ssurl --encode`, copied verbatim.
* These pin the shapes another implementation actually produces, which is
* what the parser has to accept in the field.
*/
void test_rust_ssurl_output()
{
ss_url_t u;
/* A 2022 cipher: plain userinfo, '=' of the key percent-encoded. */
SS_CHECK(ss_url_parse("ss://2022-blake3-aes-256-gcm:"
"ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE%3D"
"@192.168.100.1:8888", &u) == 0);
SS_CHECK(streq(u.method, "2022-blake3-aes-256-gcm"));
SS_CHECK(streq(u.password, "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE="));
SS_CHECK(streq(u.host, "192.168.100.1"));
SS_CHECK(streq(u.port, "8888"));
ss_url_free(&u);
/* A legacy cipher: unpadded base64 userinfo, plugin in the query. */
SS_CHECK(ss_url_parse("ss://YWVzLTI1Ni1nY206dGVzdC1wYXNzd29yZA"
"@192.168.100.1:8888/?plugin=obfs%2Dlocal%3Bobfs%3Dhttp",
&u) == 0);
SS_CHECK(streq(u.method, "aes-256-gcm"));
SS_CHECK(streq(u.password, "test-password"));
SS_CHECK(streq(u.plugin, "obfs-local"));
SS_CHECK(streq(u.plugin_opts, "obfs=http"));
ss_url_free(&u);
/* An IPv6 server. */
SS_CHECK(ss_url_parse("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwdw"
"@[2001:db8::1]:8388", &u) == 0);
SS_CHECK(streq(u.method, "chacha20-ietf-poly1305"));
SS_CHECK(streq(u.password, "pw"));
SS_CHECK(streq(u.host, "2001:db8::1"));
SS_CHECK(streq(u.port, "8388"));
ss_url_free(&u);
}
/* Freeing a zeroed struct, or nullptr, must be safe. */
void test_free_is_safe()
{
ss_url_t u{};
ss_url_free(&u);
ss_url_free(nullptr);
}
} // namespace
int main()
{
SS_RUN(test_sip002_basic);
SS_RUN(test_sip002_tag);
SS_RUN(test_sip002_plugin);
SS_RUN(test_sip002_trailing_slash);
SS_RUN(test_sip002_ipv6);
SS_RUN(test_sip002_plain_userinfo);
SS_RUN(test_sip002_2022_psk);
SS_RUN(test_legacy);
SS_RUN(test_rejects_malformed);
SS_RUN(test_rust_ssurl_output);
SS_RUN(test_free_is_safe);
return 0;
}
-113
View File
@@ -1,113 +0,0 @@
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
int verbose = 0;
#include "utils.h"
static void
test_ss_itoa(void)
{
char *s;
s = ss_itoa(0);
assert(s != NULL);
assert(strcmp(s, "0") == 0);
s = ss_itoa(42);
assert(s != NULL);
assert(strcmp(s, "42") == 0);
s = ss_itoa(-1);
assert(s != NULL);
assert(strcmp(s, "-1") == 0);
s = ss_itoa(12345);
assert(s != NULL);
assert(strcmp(s, "12345") == 0);
(void)s;
}
static void
test_ss_isnumeric(void)
{
assert(ss_isnumeric("12345") == 1);
assert(ss_isnumeric("0") == 1);
assert(ss_isnumeric("") == 0);
assert(ss_isnumeric("abc") == 0);
assert(ss_isnumeric("123abc") == 0);
assert(ss_isnumeric("12.34") == 0);
}
static void
test_ss_strndup(void)
{
char *s;
s = ss_strndup("hello world", 5);
assert(s != NULL);
assert(strcmp(s, "hello") == 0);
assert(strlen(s) == 5);
free(s);
s = ss_strndup("short", 10);
assert(s != NULL);
assert(strcmp(s, "short") == 0);
free(s);
s = ss_strndup("", 0);
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
main(void)
{
test_ss_itoa();
test_ss_isnumeric();
test_ss_strndup();
test_ss_parse_int();
return 0;
}
+100
View File
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// String and number helpers (src/utils.cpp).
#include <climits>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include "test_helpers.hpp"
#include "utils.h"
extern "C" {
int verbose = 0;
}
namespace {
void test_ss_itoa()
{
char *s = ss_itoa(0);
SS_CHECK(s != nullptr && std::strcmp(s, "0") == 0);
s = ss_itoa(42);
SS_CHECK(s != nullptr && std::strcmp(s, "42") == 0);
s = ss_itoa(-1);
SS_CHECK(s != nullptr && std::strcmp(s, "-1") == 0);
s = ss_itoa(12345);
SS_CHECK(s != nullptr && std::strcmp(s, "12345") == 0);
}
void test_ss_isnumeric()
{
SS_CHECK(ss_isnumeric("12345") == 1);
SS_CHECK(ss_isnumeric("0") == 1);
SS_CHECK(ss_isnumeric("") == 0);
SS_CHECK(ss_isnumeric("abc") == 0);
SS_CHECK(ss_isnumeric("123abc") == 0);
SS_CHECK(ss_isnumeric("12.34") == 0);
}
void test_ss_strndup()
{
char *s = ss_strndup("hello world", 5);
SS_CHECK(s != nullptr && std::strcmp(s, "hello") == 0 && std::strlen(s) == 5);
std::free(s);
s = ss_strndup("short", 10);
SS_CHECK(s != nullptr && std::strcmp(s, "short") == 0);
std::free(s);
s = ss_strndup("", 0);
SS_CHECK(s != nullptr && std::strcmp(s, "") == 0);
std::free(s);
const char raw[3] = { 'a', 'b', 'c' };
s = ss_strndup(raw, sizeof(raw));
SS_CHECK(s != nullptr && std::strcmp(s, "abc") == 0);
std::free(s);
}
void test_ss_parse_int()
{
int value = -1;
uint16_t port = 0;
SS_CHECK(ss_parse_int("0", 0, 10, &value) == 0);
SS_CHECK(value == 0);
SS_CHECK(ss_parse_int("10", 0, 10, &value) == 0);
SS_CHECK(value == 10);
SS_CHECK(ss_parse_int("-1", 0, 10, &value) == -1);
SS_CHECK(ss_parse_int("11", 0, 10, &value) == -1);
SS_CHECK(ss_parse_int("12x", 0, 100, &value) == -1);
SS_CHECK(ss_parse_int("", 0, 100, &value) == -1);
SS_CHECK(ss_parse_int("999999999999999999999999", 0, INT_MAX, &value) == -1);
SS_CHECK(ss_parse_int("1", 10, 0, &value) == -1);
SS_CHECK(ss_parse_int("1", 0, 10, nullptr) == -1);
SS_CHECK(ss_parse_uint16_port("1", &port) == 0);
SS_CHECK(port == 1);
SS_CHECK(ss_parse_uint16_port("65535", &port) == 0);
SS_CHECK(port == 65535);
SS_CHECK(ss_parse_uint16_port("0", &port) == -1);
SS_CHECK(ss_parse_uint16_port("65536", &port) == -1);
SS_CHECK(ss_parse_uint16_port("22x", &port) == -1);
SS_CHECK(ss_parse_uint16_port("22", nullptr) == -1);
}
} // namespace
int main()
{
SS_RUN(test_ss_itoa);
SS_RUN(test_ss_isnumeric);
SS_RUN(test_ss_strndup);
SS_RUN(test_ss_parse_int);
return 0;
}