From 34da0def95665afaf08802867e6177a582c27e5a Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 14 Oct 2026 16:26:54 +0800 Subject: [PATCH] Move the C codecs to tests/reference and drop uthash from ss-manager The programs now use only the C++ codecs. The C stream/AEAD/AEAD-2022 implementations stay as the reference the differential tests and the C unit tests compare against. Key derivation and the cipher tables used by the doc snippets move to src/crypto/keys.{hpp,cpp}. ss-manager keeps its servers in an ss_list (at most MAX_PORT_NUM, looked up only on control messages) instead of a uthash table, and tests/test_manager.py covers add/ping/list/remove end to end. --- scripts/no_heap_baseline.txt | 5 -- src/CMakeLists.txt | 8 +- src/common.h | 13 +--- src/crypto/aead.cpp | 12 ++- src/crypto/aead2022.cpp | 10 +-- src/crypto/keys.cpp | 79 ++++++++++++++++++++ src/crypto/keys.hpp | 28 +++++++ src/crypto/stream.cpp | 10 +-- src/local.cpp | 4 +- src/manager.cpp | 51 ++++++++----- src/manager.h | 4 +- src/redir.cpp | 3 +- src/relay/codec.cpp | 39 +++++++--- src/server.cpp | 3 +- src/tunnel.cpp | 3 +- src/utils.cpp | 1 - tests/CMakeLists.txt | 32 +++++--- {src => tests/reference}/aead.cpp | 0 {src => tests/reference}/aead.h | 0 {src => tests/reference}/aead2022.cpp | 0 {src => tests/reference}/aead2022_udp.cpp | 0 {src => tests/reference}/aead_internal.h | 0 {src => tests/reference}/crypto.cpp | 0 {src => tests/reference}/crypto.h | 0 {src => tests/reference}/ss_ref.h | 0 {src => tests/reference}/stream.cpp | 0 {src => tests/reference}/stream.h | 0 tests/test_manager.py | 91 +++++++++++++++++++++++ 28 files changed, 302 insertions(+), 94 deletions(-) create mode 100644 src/crypto/keys.cpp create mode 100644 src/crypto/keys.hpp rename {src => tests/reference}/aead.cpp (100%) rename {src => tests/reference}/aead.h (100%) rename {src => tests/reference}/aead2022.cpp (100%) rename {src => tests/reference}/aead2022_udp.cpp (100%) rename {src => tests/reference}/aead_internal.h (100%) rename {src => tests/reference}/crypto.cpp (100%) rename {src => tests/reference}/crypto.h (100%) rename {src => tests/reference}/ss_ref.h (100%) rename {src => tests/reference}/stream.cpp (100%) rename {src => tests/reference}/stream.h (100%) create mode 100644 tests/test_manager.py diff --git a/scripts/no_heap_baseline.txt b/scripts/no_heap_baseline.txt index 7bd77ec5..961f4f98 100644 --- a/scripts/no_heap_baseline.txt +++ b/scripts/no_heap_baseline.txt @@ -2,10 +2,6 @@ # Format: . check_no_heap.py fails if a file gains # 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/aead.cpp 4 -src/aead2022.cpp 3 -src/aead2022_udp.cpp 5 -src/crypto.cpp 4 src/ipset.cpp 3 src/jconf.cpp 16 src/json.cpp 2 @@ -17,6 +13,5 @@ src/redir.cpp 1 src/rule.cpp 2 src/server.cpp 3 src/ssurl.cpp 3 -src/stream.cpp 3 src/tunnel.cpp 1 src/utils.cpp 10 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 63f80f52..fe5558de 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,10 +15,6 @@ set(SS_ACL_SOURCE ) set(SS_CRYPTO_SOURCE - crypto.cpp - aead.cpp - aead2022.cpp - aead2022_udp.cpp base64.cpp ) @@ -39,9 +35,6 @@ target_include_directories(blake3 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_options(blake3 PRIVATE -Wno-error -Wno-unused-function) set_target_properties(blake3 PROPERTIES POSITION_INDEPENDENT_CODE ON) -if(SS_ENABLE_LEGACY) - list(APPEND SS_CRYPTO_SOURCE stream.cpp) -endif() if(SS_ENABLE_PLUGINS) set(SS_PLUGIN_SOURCE plugin.cpp process.cpp) else() @@ -58,6 +51,7 @@ set(SS_RELAY_SOURCE relay/resolver.cpp crypto/aead.cpp crypto/aead2022.cpp + crypto/keys.cpp ) if(SS_ENABLE_LEGACY) list(APPEND SS_RELAY_SOURCE crypto/stream.cpp) diff --git a/src/common.h b/src/common.h index dc37d5cb..d784fc0c 100644 --- a/src/common.h +++ b/src/common.h @@ -30,23 +30,12 @@ #define MODULE_LOCAL #endif -#include "crypto.h" +#include #ifdef __cplusplus extern "C" { #endif -int init_udprelay(const char *server_host, const char *server_port, -#ifdef MODULE_LOCAL - const struct sockaddr *remote_addr, const int remote_addr_len, -#ifdef MODULE_TUNNEL - const ss_addr_t tunnel_addr, -#endif -#endif - int mtu, crypto_t *crypto, int timeout, const char *iface); - -void free_udprelay(void); - #ifdef __ANDROID__ int protect_socket(int fd); int send_traffic_stat(uint64_t tx, uint64_t rx); diff --git a/src/crypto/aead.cpp b/src/crypto/aead.cpp index 5913dbc2..515c2c4d 100644 --- a/src/crypto/aead.cpp +++ b/src/crypto/aead.cpp @@ -7,17 +7,14 @@ #include -#include "crypto.h" +#include "crypto/keys.hpp" #include "ppbloom.h" -// crypto.h defines min/max macros for the C sources. -#undef min -#undef max - namespace ss::crypto { namespace { +// [cli-aead-ciphers] constexpr AeadSpec kSpecs[] = { { "aes-128-gcm", AeadKind::aes_128_gcm, 16, 12, 16 }, { "aes-192-gcm", AeadKind::aes_192_gcm, 24, 12, 16 }, @@ -27,6 +24,7 @@ constexpr AeadSpec kSpecs[] = { { "xchacha20-ietf-poly1305", AeadKind::xchacha20_poly1305, 32, 24, 16 }, #endif }; +// [cli-aead-ciphers] constexpr std::string_view kSubkeyInfo = "ss-subkey"; @@ -130,7 +128,7 @@ Result MasterKey::from_password(const AeadSpec &spec, const char *pas return fail(Errc::config); } MasterKey master{ spec, {} }; - if (crypto_derive_key(password, master.bytes.data(), spec.key_len) != spec.key_len) { + if (!derive_key(password, { master.bytes.data(), spec.key_len })) { return fail(Errc::config); } return master; @@ -142,7 +140,7 @@ Result MasterKey::from_base64(const AeadSpec &spec, const char *key) return fail(Errc::config); } MasterKey master{ spec, {} }; - if (crypto_parse_key(key, master.bytes.data(), spec.key_len) != spec.key_len) { + if (!parse_key(key, { master.bytes.data(), spec.key_len }, false)) { return fail(Errc::config); } return master; diff --git a/src/crypto/aead2022.cpp b/src/crypto/aead2022.cpp index 434eaf9f..1b16db06 100644 --- a/src/crypto/aead2022.cpp +++ b/src/crypto/aead2022.cpp @@ -8,8 +8,8 @@ #include #include -#include "base64.h" #include "blake3/blake3.h" +#include "crypto/keys.hpp" namespace ss::crypto { @@ -188,15 +188,9 @@ Result Psk::from_base64(const Aead2022Spec &spec, const char *key) noexcept return fail(Errc::config); } Psk psk{ spec, {} }; - // One spare byte tells an over-long key from an exact one. - std::uint8_t out[kMaxKey + 1]; - int n = base64_decode(out, key, static_cast(spec.aead.key_len) + 1); - if (n != static_cast(spec.aead.key_len)) { - sodium_memzero(out, sizeof(out)); + if (!parse_key(key, { psk.bytes.data(), spec.aead.key_len }, true)) { return fail(Errc::config); } - std::memcpy(psk.bytes.data(), out, spec.aead.key_len); - sodium_memzero(out, sizeof(out)); return psk; } diff --git a/src/crypto/keys.cpp b/src/crypto/keys.cpp new file mode 100644 index 00000000..7e74084a --- /dev/null +++ b/src/crypto/keys.cpp @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#include "crypto/keys.hpp" + +#include + +#include +#include + +#include "base64.h" +#include "utils.h" + +namespace ss::crypto { + +namespace { + +constexpr std::size_t kMaxKey = 64; + +} // namespace + +bool derive_key(const char *password, std::span key) noexcept +{ + if (password == nullptr) { + return false; + } + const std::size_t len = std::strlen(password); + std::uint8_t digest[16]; + mbedtls_md5_context md5; + std::size_t at = 0; + bool ok = true; + for (bool first = true; at < key.size(); first = false) { + mbedtls_md5_init(&md5); + ok = ok && mbedtls_md5_starts(&md5) == 0; + if (!first) { + ok = ok && mbedtls_md5_update(&md5, digest, sizeof(digest)) == 0; + } + ok = ok && mbedtls_md5_update(&md5, reinterpret_cast(password), len) == 0 + && mbedtls_md5_finish(&md5, digest) == 0; + mbedtls_md5_free(&md5); + std::size_t n = std::min(sizeof(digest), key.size() - at); + std::memcpy(key.data() + at, digest, n); + at += n; + } + sodium_memzero(digest, sizeof(digest)); + return ok; +} + +bool parse_key(const char *base64, std::span key, bool exact) noexcept +{ + if (base64 == nullptr || key.size() > kMaxKey) { + return false; + } + // One spare byte tells an over-long key from an exact one; base64_decode + // never writes past the size it is given. + std::uint8_t out[kMaxKey + 1]; + int want = static_cast(key.size()) + (exact ? 1 : 0); + int n = base64_decode(out, base64, want); + bool ok = exact ? n == static_cast(key.size()) : n >= static_cast(key.size()); + if (ok) { + std::memcpy(key.data(), out, key.size()); + } + sodium_memzero(out, sizeof(out)); + return ok; +} + +void report_bad_key(const char *what, std::size_t key_len) noexcept +{ + std::uint8_t fresh[kMaxKey]; + char encoded[BASE64_SIZE(kMaxKey)]; + key_len = std::min(key_len, kMaxKey); + randombytes_buf(fresh, key_len); + base64_encode(encoded, sizeof(encoded), fresh, static_cast(key_len)); + sodium_memzero(fresh, sizeof(fresh)); + LOGE("Invalid %s for your chosen cipher!", what); + LOGE("It requires a %zu-byte key encoded with Base64", key_len); + LOGE("Generating a new random key: %s", encoded); + LOGE("Please use the key above or input a valid key"); +} + +} // namespace ss::crypto diff --git a/src/crypto/keys.hpp b/src/crypto/keys.hpp new file mode 100644 index 00000000..7211fb64 --- /dev/null +++ b/src/crypto/keys.hpp @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +#ifndef SS_CRYPTO_KEYS_HPP +#define SS_CRYPTO_KEYS_HPP + +// Turning what the user configured into key bytes. + +#include +#include +#include + +namespace ss::crypto { + +// EVP_BytesToKey with MD5 and no salt, the classic shadowsocks password +// stretching. Returns false for a null password. +bool derive_key(const char *password, std::span key) noexcept; + +// A URL-safe or standard base64 key. With `exact` (AEAD-2022) the decoded +// key must be exactly key.size() bytes; otherwise it must be at least that +// long and only the first key.size() bytes are used. +bool parse_key(const char *base64, std::span key, bool exact) noexcept; + +// Explain a key that does not fit and show a fresh random one of the right +// size, as the C implementation did before exiting. +void report_bad_key(const char *what, std::size_t key_len) noexcept; + +} // namespace ss::crypto + +#endif diff --git a/src/crypto/stream.cpp b/src/crypto/stream.cpp index fb92c8a3..79b69d12 100644 --- a/src/crypto/stream.cpp +++ b/src/crypto/stream.cpp @@ -6,16 +6,13 @@ #include -#include "crypto.h" - -// crypto.h defines min/max macros for the C sources. -#undef min -#undef max +#include "crypto/keys.hpp" namespace ss::crypto { namespace { +// [cli-stream-ciphers] constexpr StreamSpec kStreams[] = { { "aes-128-cfb", StreamKind::aes_cfb, 16, 16 }, { "aes-192-cfb", StreamKind::aes_cfb, 24, 16 }, @@ -30,6 +27,7 @@ constexpr StreamSpec kStreams[] = { { "chacha20", StreamKind::chacha20, 32, 8 }, { "chacha20-ietf", StreamKind::chacha20_ietf, 32, 12 }, }; +// [cli-stream-ciphers] constexpr std::size_t kSodiumBlock = 64; @@ -66,7 +64,7 @@ Result StreamKey::from_password(const StreamSpec &spec, const char *p return fail(Errc::config); } StreamKey key{ spec, {} }; - if (crypto_derive_key(password, key.bytes.data(), spec.key_len) != spec.key_len) { + if (!derive_key(password, { key.bytes.data(), spec.key_len })) { return fail(Errc::config); } return key; diff --git a/src/local.cpp b/src/local.cpp index 712e6510..c29f99eb 100644 --- a/src/local.cpp +++ b/src/local.cpp @@ -58,7 +58,6 @@ #include "socks5.h" #include "acl.h" #include "plugin.h" -#include "crypto.h" #include "jconf.h" #include "common.h" #include "ss_windows.h" @@ -284,7 +283,6 @@ struct Relays { loop = &event_loop; LOGI("initializing ciphers... %s", cipher); if (!method.init(cipher, pass, key, ss::crypto::Role::client)) { - LOGE("failed to initialize ciphers"); return false; } return true; @@ -1087,6 +1085,8 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`. USE_SYSLOG(argv[0], pid_flags); if (pid_flags) { daemonize(pid_path); + if (!program.after_fork()) + FATAL("failed to reinitialize the event loop"); } if (fast_open == 1) { diff --git a/src/manager.cpp b/src/manager.cpp index c806fa80..3654d25c 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -77,7 +77,21 @@ char *executable = (char *)"ss-server"; char *working_dir = NULL; size_t working_dir_size = 0; -static struct server *server_table; +/* The managed servers, looked up by port. There are at most MAX_PORT_NUM, + * and lookups only happen on control messages, so a list does. */ +static struct ss_list server_table; + +static struct server * +find_server(const char *port) +{ + struct ss_list_item *curr, *next; + ss_list_foreach_void(&server_table, curr, next) { + struct server *server = ss_container_of(curr, struct server, entries); + if (strcmp(server->port, port) == 0) + return server; + } + return NULL; +} static int copy_port(char *dst, size_t dst_len, const char *src, size_t src_len) @@ -848,9 +862,10 @@ add_server(struct manager_ctx *manager, struct server *server) return -1; } - struct server *replaced; - // NOLINTNEXTLINE(clang-analyzer-core.DivideZero,clang-analyzer-security.ArrayBound): uthash initializes nonzero buckets and hashes the NUL-terminated port within its key length - HASH_REPLACE_STR(server_table, port, server, replaced); + struct server *replaced = find_server(server->port); + if (replaced != NULL) + ss_list_remove(&replaced->entries); + ss_list_add(&server_table, &server->entries); if (replaced != NULL) { destroy_server(replaced); ss_free(replaced); @@ -927,11 +942,10 @@ remove_server(char *prefix, char *port) { struct server *old_server = NULL; - // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound): uthash hashes only strlen(port) bytes; manager ports are validated and NUL-terminated - HASH_FIND_STR(server_table, port, old_server); + old_server = find_server(port); if (old_server != NULL) { - HASH_DEL(server_table, old_server); + ss_list_remove(&old_server->entries); destroy_server(old_server); ss_free(old_server); } @@ -945,9 +959,7 @@ update_stat(char *port, uint64_t traffic) if (verbose) { LOGI("update traffic %" PRIu64 " for port %s", traffic, port); } - struct server *ret; - // NOLINTNEXTLINE(clang-analyzer-security.ArrayBound): uthash hashes only strlen(port) bytes; manager ports are validated and NUL-terminated - HASH_FIND_STR(server_table, port, ret); + struct server *ret = find_server(port); if (ret != NULL) { struct server *server = (struct server *)ret; server->traffic = traffic; @@ -1017,12 +1029,13 @@ manager_recv_cb(void *ctx) noexcept ERROR("add_sendto"); } } else if (strcmp(action, "list") == 0) { - struct server *server, *next; + struct ss_list_item *curr, *next; char buf[BUF_SIZE]; memset(buf, 0, BUF_SIZE); strcpy(buf, "["); - HASH_ITER(hh, server_table, server, next) { + ss_list_foreach_void(&server_table, curr, next) { + struct server *server = ss_container_of(curr, struct server, entries); char *method = server->method ? server->method : manager->method; char entry_buf[BUF_SIZE]; size_t entry_pos = 0; @@ -1082,7 +1095,7 @@ manager_recv_cb(void *ctx) noexcept update_stat(port, traffic); } else if (strcmp(action, "ping") == 0) { - struct server *server, *next; + struct ss_list_item *curr, *next; char buf[BUF_SIZE]; size_t pos = 0; @@ -1092,7 +1105,8 @@ manager_recv_cb(void *ctx) noexcept goto ERROR_MSG; } - HASH_ITER(hh, server_table, server, next) { + ss_list_foreach_void(&server_table, curr, next) { + struct server *server = ss_container_of(curr, struct server, entries); char entry_buf[64]; int entry_len = snprintf(entry_buf, sizeof(entry_buf), "\"%s\":%" PRIu64 ",", @@ -1755,7 +1769,7 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`. FATAL("Couldn't open the directory"); } - server_table = NULL; + ss_list_init(&server_table); if (conf != NULL) { for (i = 0; i < conf->port_password_num; i++) { @@ -1832,10 +1846,11 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`. } // Clean up - struct server *server, *next; - HASH_ITER(hh, server_table, server, next) { + struct ss_list_item *curr, *next; + ss_list_foreach_void(&server_table, curr, next) { + struct server *server = ss_container_of(curr, struct server, entries); stop_server(working_dir, server->port); - HASH_DEL(server_table, server); + ss_list_remove(&server->entries); destroy_server(server); ss_free(server); } diff --git a/src/manager.h b/src/manager.h index 33c799b8..25d6f8c9 100644 --- a/src/manager.h +++ b/src/manager.h @@ -25,7 +25,7 @@ #include #include "core.h" -#include "uthash.h" +#include "core.h" #include "jconf.h" @@ -64,7 +64,7 @@ struct manager_ctx { }; struct server { - UT_hash_handle hh; + struct ss_list_item entries; /* in server_table, at most MAX_PORT_NUM */ char port[8]; char password[128]; char fast_open[8]; diff --git a/src/redir.cpp b/src/redir.cpp index af519117..5e4a271d 100644 --- a/src/redir.cpp +++ b/src/redir.cpp @@ -51,7 +51,6 @@ #include "netutils.h" #include "utils.h" #include "common.h" -#include "crypto.h" #include "jconf.h" #include "common.h" @@ -763,6 +762,8 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`. USE_SYSLOG(argv[0], pid_flags); if (pid_flags) { daemonize(pid_path); + if (!program.after_fork()) + FATAL("failed to reinitialize the event loop"); } if (no_delay) { diff --git a/src/relay/codec.cpp b/src/relay/codec.cpp index 04c640e6..1112401d 100644 --- a/src/relay/codec.cpp +++ b/src/relay/codec.cpp @@ -4,16 +4,22 @@ #include #include -#include "crypto.h" +#include "crypto/keys.hpp" #include "ppbloom.h" #include "utils.h" -// crypto.h defines min/max macros for the C sources. -#undef min -#undef max - namespace ss::relay { +namespace { + +// Ping-pong bloom filter sizing, as in the C implementation. +constexpr int kBloomEntriesServer = 1000000; +constexpr double kBloomErrorServer = 1e-10; +constexpr int kBloomEntriesClient = 10000; +constexpr double kBloomErrorClient = 1e-15; + +} // namespace + Status Method::init(const char *method, const char *password, const char *key, Role role) noexcept { if (method == nullptr) { @@ -26,22 +32,30 @@ Status Method::init(const char *method, const char *password, const char *key, R // The salt/IV replay filter shared by every connection and datagram. if (!bloom_ready_) { if (role == Role::server) { - ppbloom_init(BF_NUM_ENTRIES_FOR_SERVER, BF_ERROR_RATE_FOR_SERVER); + ppbloom_init(kBloomEntriesServer, kBloomErrorServer); } else { - ppbloom_init(BF_NUM_ENTRIES_FOR_CLIENT, BF_ERROR_RATE_FOR_CLIENT); + ppbloom_init(kBloomEntriesClient, kBloomErrorClient); } bloom_ready_ = true; } if (auto spec = crypto::find_aead2022(method)) { // The password field may carry the base64 PSK too. - SS_TRY_ASSIGN(psk, crypto::Psk::from_base64(*spec, key != nullptr ? key : password)); - key_ = psk; + auto psk = crypto::Psk::from_base64(*spec, key != nullptr ? key : password); + if (!psk) { + crypto::report_bad_key("pre-shared key", spec->aead.key_len); + return fail(psk.error()); + } + key_ = *psk; return {}; } if (auto spec = crypto::find_aead(method)) { if (key != nullptr) { - SS_TRY_ASSIGN(master, crypto::MasterKey::from_base64(*spec, key)); - key_ = master; + auto master = crypto::MasterKey::from_base64(*spec, key); + if (!master) { + crypto::report_bad_key("key", spec->key_len); + return fail(master.error()); + } + key_ = *master; } else { SS_TRY_ASSIGN(master, crypto::MasterKey::from_password(*spec, password)); key_ = master; @@ -53,7 +67,8 @@ Status Method::init(const char *method, const char *password, const char *key, R LOGI("Stream ciphers are insecure, therefore deprecated, and should be almost always avoided."); if (key != nullptr) { crypto::StreamKey stream{ *spec, {} }; - if (crypto_parse_key(key, stream.bytes.data(), spec->key_len) != spec->key_len) { + if (!crypto::parse_key(key, { stream.bytes.data(), spec->key_len }, false)) { + crypto::report_bad_key("key", spec->key_len); return fail(Errc::config); } key_ = stream; diff --git a/src/server.cpp b/src/server.cpp index 9b82ea79..b008a27a 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -70,7 +70,6 @@ enum datatypes { #include "utils.h" #include "acl.h" #include "plugin.h" -#include "crypto.h" #include "jconf.h" #include "common.h" #include "ss_windows.h" @@ -1410,6 +1409,8 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`. USE_SYSLOG(argv[0], pid_flags); if (pid_flags) { daemonize(pid_path); + if (!program.after_fork()) + FATAL("failed to reinitialize the event loop"); } if (ipv6first) { diff --git a/src/tunnel.cpp b/src/tunnel.cpp index abcd14f1..74949da7 100644 --- a/src/tunnel.cpp +++ b/src/tunnel.cpp @@ -52,7 +52,6 @@ #include "netutils.h" #include "utils.h" #include "plugin.h" -#include "crypto.h" #include "jconf.h" #include "common.h" #include "ss_windows.h" @@ -844,6 +843,8 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`. USE_SYSLOG(argv[0], pid_flags); if (pid_flags) { daemonize(pid_path); + if (!program.after_fork()) + FATAL("failed to reinitialize the event loop"); } if (ipv6first) { diff --git a/src/utils.cpp b/src/utils.cpp index c5648a70..e63960b2 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -43,7 +43,6 @@ #include -#include "crypto.h" #include "utils.h" #ifdef HAVE_SETRLIMIT diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 701e4725..8cee9319 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,9 @@ if(NOT WIN32) set(SS_TEST_LAUNCHER sh -c "ulimit -n 1024 2>/dev/null\; exec \"$0\" \"$@\"") endif() +# "crypto.h" and friends come from the C reference implementation. +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/reference) + # Helper function to add a unit test function(ss_add_test name sources libs) if(WIN32 AND "${sources}" MATCHES "utils.cpp") @@ -186,14 +189,15 @@ 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_LIBS};${SS_MATH_LIBRARY}") -# Crypto sources shared by the tests that link the crypto layer. The AEAD-2022 -# ciphers pull in BLAKE3 (linked as the `blake3` target) and the session-based -# UDP codec, so they must be built together with crypto.cpp. +# The original C cipher implementation, kept under tests/reference as an +# independent oracle: the C++ codecs are tested against it byte for byte, +# and udp_session_eviction_client speaks to ss-server with it. The programs +# no longer contain it. set(SS_CRYPTO_TEST_SOURCES - ${PROJECT_SOURCE_DIR}/src/crypto.cpp - ${PROJECT_SOURCE_DIR}/src/aead.cpp - ${PROJECT_SOURCE_DIR}/src/aead2022.cpp - ${PROJECT_SOURCE_DIR}/src/aead2022_udp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/reference/crypto.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/reference/aead.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/reference/aead2022.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/reference/aead2022_udp.cpp ${PROJECT_SOURCE_DIR}/src/ppbloom.cpp ${PROJECT_SOURCE_DIR}/src/cache.cpp ${PROJECT_SOURCE_DIR}/src/base64.cpp @@ -201,7 +205,7 @@ set(SS_CRYPTO_TEST_SOURCES ) if(SS_ENABLE_LEGACY) - list(APPEND SS_CRYPTO_TEST_SOURCES ${PROJECT_SOURCE_DIR}/src/stream.cpp) + list(APPEND SS_CRYPTO_TEST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/reference/stream.cpp) endif() # test_buffer - needs sodium and mbedtls for crypto.cpp buffer functions @@ -230,20 +234,20 @@ ss_add_test(test_aead2022 # test_crypto_aead_cpp - C++ AEAD codecs, differentially against aead.cpp ss_add_test(test_crypto_aead_cpp - "test_crypto_aead_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${SS_CRYPTO_TEST_SOURCES}" + "test_crypto_aead_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${PROJECT_SOURCE_DIR}/src/crypto/keys.cpp;${SS_CRYPTO_TEST_SOURCES}" "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") ss_strict_warnings(test_crypto_aead_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp) # test_crypto_aead2022_cpp - C++ SIP022 codecs, differentially against aead2022.cpp ss_add_test(test_crypto_aead2022_cpp - "test_crypto_aead2022_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead2022.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${SS_CRYPTO_TEST_SOURCES}" + "test_crypto_aead2022_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead2022.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${PROJECT_SOURCE_DIR}/src/crypto/keys.cpp;${SS_CRYPTO_TEST_SOURCES}" "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") ss_strict_warnings(test_crypto_aead2022_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/aead2022.cpp) if(SS_ENABLE_LEGACY) # test_crypto_stream_cpp - C++ stream codecs, differentially against stream.cpp ss_add_test(test_crypto_stream_cpp - "test_crypto_stream_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/stream.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${SS_CRYPTO_TEST_SOURCES}" + "test_crypto_stream_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/stream.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${PROJECT_SOURCE_DIR}/src/crypto/keys.cpp;${SS_CRYPTO_TEST_SOURCES}" "${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}") ss_strict_warnings(test_crypto_stream_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/stream.cpp) endif() @@ -326,6 +330,12 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING) --bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) set_tests_properties(test_experiment_supervisor PROPERTIES LABELS "integration" TIMEOUT 30) + if(TARGET ss-manager) + add_test(NAME test_manager + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_manager.py + --bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + set_tests_properties(test_manager PROPERTIES LABELS "integration" TIMEOUT 60) + endif() endif() add_test(NAME test_experimental_transports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/experiments/test_transport.py) diff --git a/src/aead.cpp b/tests/reference/aead.cpp similarity index 100% rename from src/aead.cpp rename to tests/reference/aead.cpp diff --git a/src/aead.h b/tests/reference/aead.h similarity index 100% rename from src/aead.h rename to tests/reference/aead.h diff --git a/src/aead2022.cpp b/tests/reference/aead2022.cpp similarity index 100% rename from src/aead2022.cpp rename to tests/reference/aead2022.cpp diff --git a/src/aead2022_udp.cpp b/tests/reference/aead2022_udp.cpp similarity index 100% rename from src/aead2022_udp.cpp rename to tests/reference/aead2022_udp.cpp diff --git a/src/aead_internal.h b/tests/reference/aead_internal.h similarity index 100% rename from src/aead_internal.h rename to tests/reference/aead_internal.h diff --git a/src/crypto.cpp b/tests/reference/crypto.cpp similarity index 100% rename from src/crypto.cpp rename to tests/reference/crypto.cpp diff --git a/src/crypto.h b/tests/reference/crypto.h similarity index 100% rename from src/crypto.h rename to tests/reference/crypto.h diff --git a/src/ss_ref.h b/tests/reference/ss_ref.h similarity index 100% rename from src/ss_ref.h rename to tests/reference/ss_ref.h diff --git a/src/stream.cpp b/tests/reference/stream.cpp similarity index 100% rename from src/stream.cpp rename to tests/reference/stream.cpp diff --git a/src/stream.h b/tests/reference/stream.h similarity index 100% rename from src/stream.h rename to tests/reference/stream.h diff --git a/tests/test_manager.py b/tests/test_manager.py new file mode 100644 index 00000000..74f59e7a --- /dev/null +++ b/tests/test_manager.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""ss-manager end to end: add a server over the control socket, relay through +it with ss-local, see its traffic in ping, list it, and remove it.""" +import argparse +import json +import os +from pathlib import Path +import socket +import subprocess +import tempfile +import threading +import time + +from interop import TCPHandler, TCPOrigin, free_port, peers, receive, socks + + +def control(port, message, timeout=5): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.settimeout(timeout) + sock.sendto(message.encode(), ("127.0.0.1", port)) + return sock.recvfrom(65535)[0].decode().rstrip("\0") + + +def wait_listening(port, deadline=10): + end = time.monotonic() + deadline + while time.monotonic() < end: + try: + with socket.create_connection(("127.0.0.1", port), timeout=.2): + return + except OSError: + time.sleep(.05) + raise AssertionError(f"managed server did not listen on {port}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--bin", required=True, type=Path) + args = parser.parse_args() + bins = args.bin.resolve() + manager_port, server_port, local_port = free_port(), free_port(), free_port() + with TCPOrigin(("127.0.0.1", 0), TCPHandler) as origin, tempfile.TemporaryDirectory() as workdir: + threading.Thread(target=origin.serve_forever, daemon=True).start() + manager = subprocess.Popen( + [str(bins / "ss-manager"), "--manager-address", f"127.0.0.1:{manager_port}", + "--executable", str(bins / "ss-server"), "-D", workdir, "-s", "127.0.0.1", + "-m", "aes-256-gcm"], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + try: + # The manager answers once its control socket is up. + for _ in range(50): + try: + reply = control(manager_port, "ping", timeout=.2) + break + except (TimeoutError, OSError): + time.sleep(.1) + else: + raise AssertionError("ss-manager did not answer") + assert reply.startswith("stat:"), reply + reply = control(manager_port, 'add: {"server_port": %d, "password": "secret"}' % server_port) + assert reply == "ok", reply + wait_listening(server_port) + local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(server_port), "-l", str(local_port), + "-k", "secret", "-m", "aes-256-gcm"] + with peers([local], [local_port]): + sock, _ = socks(local_port, 1, ("127.0.0.1", origin.server_address[1])) + payload = os.urandom(65536) + with sock: + sock.sendall(payload) + assert receive(sock, len(payload)) == payload, "relay through the managed server failed" + # ss-server reports traffic to the manager every few seconds. + deadline = time.monotonic() + 15 + traffic = 0 + while time.monotonic() < deadline and traffic == 0: + stat = json.loads(control(manager_port, "ping")[len("stat: "):]) + traffic = stat.get(str(server_port), 0) + time.sleep(.5) + assert traffic > 0, "no traffic statistics from the managed server" + listed = json.loads(control(manager_port, "list")) + assert any(entry.get("server_port") == str(server_port) for entry in listed), listed + assert control(manager_port, 'remove: {"server_port": %d}' % server_port) == "ok" + assert json.loads(control(manager_port, "list")) == [], "server still listed after remove" + finally: + manager.terminate() + out = manager.communicate(timeout=10)[0].decode(errors="replace") + assert manager.returncode == 0, f"ss-manager exited with {manager.returncode}:\n{out}" + print("PASS ss-manager add/ping/list/remove with traffic statistics") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())