diff --git a/CLAUDE.md b/CLAUDE.md index 5b6c10d8..a14e90e8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview shadowsocks-c (formerly shadowsocks-libev) is a lightweight SOCKS5 proxy. Version 3.3.6, licensed under GPLv3. -The sources are C11 and are being ported to C++20 module by module (see `docs/cpp-core.md`); the public -`src/shadowsocks.h` stays a C header with a stable ABI. +The sources are C++20 (no exceptions, no RTTI, no heap allocation after startup; see `docs/cpp-core.md`). +The public `src/shadowsocks.h` stays a C header with a stable ABI. ## Build Commands @@ -47,14 +47,18 @@ Binaries land in `/bin/`, libraries in `/lib/`. ctest --test-dir build -LE memcheck --output-on-failure ``` -C unit tests (`tests/test_*.c`) compile the specific `src/*.c` files they need (see `ss_add_test` in -`tests/CMakeLists.txt`). C++ core tests are `tests/test_core_*.cpp` using `tests/test_helpers.hpp`. +Unit tests compile the specific `src/*.cpp` files they need (see `ss_add_test` in `tests/CMakeLists.txt`). +New tests are C++ (`tests/test_*_cpp.cpp`, `tests/test_core_*.cpp`) using `tests/test_helpers.hpp`; older +ones are C (`tests/test_*.c`). Codec tests compare against the C oracle in `tests/reference/`. The +`alloc_steady_*` tests fail on any heap allocation in steady state. Labels: `unit`, `vendor`, `integration`, `memcheck` (Linux + valgrind). ### Integration Tests ```bash python3 tests/interop.py --self --bin build/bin # TCP/UDP relay, all AEAD/2022 methods +python3 tests/tunnel_interop.py --bin build/bin # ss-tunnel, every method +python3 tests/test_manager.py --bin build/bin # ss-manager (needs -DWITH_SS_MANAGER=ON on macOS) python3 tests/test.py --bin build/bin/ -c tests/aes-gcm.json # one cipher via curl/dig (needs network) python3 tests/test_cli.py --bin-dir build/bin # CLI contract: help/version/exit codes python3 tests/stress_test.py --bin build/bin/ --size 10 @@ -102,31 +106,37 @@ A library `libshadowsocks-c` (legacy names `libshadowsocks-libev.*` are kept) is ### Source Organization (`src/`) **Shared by all binaries:** -- `utils.cpp` - logging, system utilities, `ss_malloc`/`ss_free` +- `utils.cpp` - logging, system utilities, `ss_malloc`/`ss_free` (startup only), CLI help snippets - `jconf.cpp` / `json.cpp` - JSON config file parsing - `netutils.cpp` - network address utilities -- `cache.cpp` - uthash-based connection cache (UDP relay, AEAD-2022 tables) -- `udprelay.cpp` - UDP relay implementation (shared, but uses `#ifdef MODULE_*` for per-binary behavior) -- `ss_event.cpp` / `ss_event.h` - event loop API over libuv -- `resolv.cpp` - asynchronous DNS via c-ares - -**Crypto layer** (behind a common `crypto_t` function-pointer interface): -- `crypto.cpp` / `crypto.h` - crypto initialization, key derivation (HKDF), `buffer_t` management -- `stream.cpp` - legacy stream ciphers -- `aead.cpp` - AEAD ciphers (AES-GCM via Mbed TLS/libsodium, ChaCha20-Poly1305 via libsodium) -- `aead2022.cpp` / `aead2022_udp.cpp` - Shadowsocks 2022 (SIP022), BLAKE3 from `src/blake3` - `ppbloom.cpp` - ping-pong bloom filter for salt replay detection (`third_party/bloom`) +**Relay engines** (`src/relay/`): `tcp.cpp` (one TCP engine for all four programs, `Kind` selects the +handshake), `udp.cpp` (UDP relay), `codec.cpp` (per-connection encoder/decoder variants), `resolver.cpp` +(pooled c-ares queries), `socket.cpp` (upstream sockets, TFO). Each program's `main()` builds a config and +runs them. + +**Crypto** (`src/crypto/`): value-type codecs `aead.cpp` (AES-GCM, ChaCha20-Poly1305), `aead2022.cpp` +(Shadowsocks 2022, BLAKE3 from `src/blake3`), `stream.cpp` (legacy, `SS_ENABLE_LEGACY`), `keys.cpp` +(key derivation, method tables). The old C implementation lives in `tests/reference/` as a test oracle. + **ACL:** `acl.cpp` / `rule.cpp` / `ipset.cpp` - IP prefix sets and PCRE2/literal domain rules. **Plugins:** `plugin.cpp` / `process.cpp` - SIP003 plugin subprocess management. -**C++ core:** `src/core/*.hpp` - header-only ownership and allocation primitives (pools, handles, fixed -buffers, arena, fixed hash map/LRU, intrusive list, bounded heap). See `docs/cpp-core.md`. +**C++ core:** `src/core/*.hpp` - ownership and allocation primitives (pools, handles, fixed buffers, arena, +fixed hash map/LRU, intrusive list, bounded heap, `heap_guard` for third-party allocators). + +**Embedding API:** `src/shadowsocks.h` (unchanged C ABI), `src/shadowsocks/control.h` +(`ss_local_start`/`stop`/`wait`/`destroy`) and `src/shadowsocks/shadowsocks.hpp` (RAII wrapper). ### Event Loop -All binaries use the `ss_event` API (libuv backend: kqueue/epoll/IOCP). The connection lifecycle follows stages defined in `src/common.h`: `STAGE_INIT` -> `STAGE_HANDSHAKE` -> `STAGE_RESOLVE` -> `STAGE_STREAM` -> `STAGE_STOP`. Each binary defines its own `listen_ctx_t`, `server_t`, and `remote_t` structs (note: "server" in `local.h` means the local-side connection, "remote" means the ss-server side). +`src/event/loop.hpp` wraps libuv (kqueue/epoll/IOCP) handles embedded by value in their owners; +`src/event/program.hpp` is the per-process loop, signals and timers. Connections come from fixed pools sized by +`--max-connections`, `--max-udp-sessions` and `--max-dns-queries`; a full TCP pool pauses accepting. +Non-owning references (DNS answers, timers) are generation-checked handles. Each binary still defines its +own `main()` and option parsing in `local.cpp`, `tunnel.cpp`, `redir.cpp`, `server.cpp`, `manager.cpp`. ### Compiler Flags diff --git a/docs/cpp-core.md b/docs/cpp-core.md index 261de834..cbc212a1 100644 --- a/docs/cpp-core.md +++ b/docs/cpp-core.md @@ -1,11 +1,12 @@ # C++20 core and allocation policy -Status: phases 1-3 done, 4-5 in progress. Every module of `src/` is -compiled as C++20 (most of it is still the C design, converted -mechanically). The ownership and allocation primitives (`src/core/`), the -event layer (`src/event/`), allocation-free AEAD and stream codecs -(`src/crypto/`) and the slab-based cache exist and are tested; the relay -engines that will use them are the next phases. +Status: all ten phases are done. Every module of `src/` is C++20. The four +relaying programs run on one TCP engine and one UDP engine (`src/relay/`) +built from the ownership and allocation primitives in `src/core/`, the libuv +wrappers in `src/event/` and value-type codecs in `src/crypto/`. On the data +path they make no heap allocations (see [performance](performance.md)). +The original C codecs and cache are kept only as test oracles in +`tests/reference/`. ## Goals @@ -49,6 +50,7 @@ engines that will use them are the next phases. | `fixed_map.hpp` | `FixedHashMap` (open addressing, backward-shift deletion) and `FixedLru`; replace uthash and `cache.cpp`. | | `intrusive_list.hpp` | `IntrusiveList`/`ListNode`; replaces `ss_list`. Nodes unlink themselves when destroyed. | | `fixed_heap.hpp` | Bounded size-class allocator to hand to libuv, c-ares and Mbed TLS through their allocator hooks. | +| `heap_guard.hpp` | `install_library_heaps()` routes libuv, c-ares and Mbed TLS into their own `FixedHeap`s (executables only, never the library); `heap_seal()` marks the end of startup; `log_heap_report()` prints heap use with `-v`. | ## Event layer (`src/event/loop.hpp`) @@ -60,57 +62,83 @@ handle; a handle closed from another callback gets no further events. Handles close into a `CloseGroup`, which signals completion after the last asynchronous close, so pooled owners implement `AsyncClosable::begin_close` by closing their handles into one. `DeadlineList` drives the idle timeouts -of many owners from one timer. The `ss_event` C API remains for the code -that still uses it and is removed with its last caller. +of many owners from one timer. `program.hpp` holds what every `main()` +shares: the loop, SIGINT/SIGTERM (and SIGCHLD when a plugin runs), periodic +callbacks, a cross-thread `request_stop()`, and `after_fork()` for +`daemonize()`. + +## Relay engines (`src/relay/`) + +| File | Purpose | +|---|---| +| `tcp.hpp` | `TcpRelay`: listeners, a pool of `max_connections` connections, one idle-deadline timer. `Kind` selects the handshake (tunnel, SOCKS5 local, redir, server). A full pool pauses accepting until a slot frees. | +| `udp.hpp` | `UdpRelay`: sessions in a `FixedLru` over a pool of `max_udp_sessions`, one 64 KiB scratch buffer per relay. AEAD-2022 server sessions are `RefPtr`-shared between the session table and their client sockets. | +| `codec.hpp` | `Method` (immutable, from the configuration) and per-connection `Encoder`/`Decoder` variants: plain, stream, AEAD, AEAD-2022. | +| `resolver.hpp` | c-ares with a pool of `max_dns_queries` queries; answers reach their connection through a generation-checked handle, so a closed connection is never called back. | +| `socket.hpp` | Upstream socket setup, TCP Fast Open, SOCKS address encoding. | + +Per-binary behaviour (TOS per redir port, connmark-to-TOS, nftables, manager +statistics, Android protect) plugs in through hooks on the configs, so the +engines do not link it. ## Crypto (`src/crypto/`) -`aead.hpp` (SIP004/SIP007 AEAD) and `stream.hpp` (legacy stream ciphers) -provide value-type codecs: the cipher state is held inside the codec, TCP -decoders reassemble into a fixed buffer sized for one chunk, and HKDF-SHA1 -runs on stack SHA-1 state. They are tested differentially against the C -implementation, which the programs still use until the relay engines move -over. The bundled Mbed TLS enables `MBEDTLS_BLOCK_CIPHER_C` so GCM key setup -does not allocate; with a system Mbed TLS lacking it, `mbedtls_gcm_setkey` -may still allocate. AEAD-2022 (SIP022) codecs follow once the pending -UDP-session rework on `fix/aead2022-udp-session-eviction` has landed. +`aead.hpp` (SIP004/SIP007 AEAD), `aead2022.hpp` (SIP022) and `stream.hpp` +(legacy stream ciphers) provide value-type codecs: the cipher state is held +inside the codec, TCP decoders reassemble into a fixed buffer sized for one +chunk and can hand plaintext out in windows of any size, and HKDF-SHA1 runs +on stack SHA-1 state. `keys.hpp` derives keys and lists the supported +methods. All codecs are tested byte for byte against the C reference +implementation in `tests/reference/`. The bundled Mbed TLS enables +`MBEDTLS_BLOCK_CIPHER_C` so GCM key setup does not allocate; with a system +Mbed TLS lacking it, `mbedtls_gcm_setkey` may still allocate. + +## Capacities + +| Setting | Default | Pool | +|---|---|---| +| `--max-connections` / `max_connections` | 1024 | TCP connections, about 104 KiB each | +| `--max-udp-sessions` / `max_udp_sessions` | 256 (ss-server 512) | UDP associations; the least recently used is dropped when full | +| `--max-dns-queries` / `max_dns_queries` | the connection limit (ss-server +64) | DNS lookups in flight | + +Pools are reserved at startup and only touched slots become resident. ## Phases -1. Build and core (done): C++ enabled in CMake, `src/core/`, unit tests - (`test_core_*`), the no-heap lint, the C++ runtime check and the - allocation audit. -2. Mechanical C-to-C++ conversion, leaf modules first (done). Converted - modules still allocate like the C code; `scripts/no_heap_baseline.txt` - ratchets that down as they are rewritten. -3. Event loop wrapper over libuv (done: `src/event/`, `test_event_cpp`). -4. Crypto: cipher contexts held by value, dispatched with `std::variant` - (AEAD and stream codecs done; AEAD-2022 pending). -5. Services: cache (done: fixed slab, no per-entry allocation), lists, - resolver, ACL and configuration on the new primitives. -6. One TCP relay engine shared by ss-tunnel, ss-local, ss-redir and ss-server. -7. UDP relay engine. -8. Seal the heap after startup; new `max_connections`, `max_udp_sessions` - and `max_dns_queries` settings size the pools. -9. Public API: `shadowsocks.h` kept byte-for-byte, plus a new installed C++ - header with an RAII handle. -10. Remove the C shims. +1. Build and core: C++ in CMake, `src/core/`, the no-heap lint, the C++ + runtime check and the allocation audit. +2. Mechanical C-to-C++ conversion, leaf modules first. + `scripts/no_heap_baseline.txt` ratchets down the remaining startup + allocations in converted modules. +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. +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 + capacity settings above. +9. Public API: `shadowsocks.h` unchanged, plus `shadowsocks/control.h` + (`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`. ## Allocation audit `tests/alloc_audit/alloc_counter.c` is a preloadable `malloc` counter (glibc and macOS). `tests/alloc_audit/measure_alloc.py` warms ss-local and ss-server up, snapshots their counters, relays more connections and reports -heap allocations per connection. It runs as the `alloc_audit_report` ctest -(report-only for now). It becomes a hard check, with a budget of zero, once -the relay is ported. +heap allocations per connection; `--udp` measures datagrams instead and +`--target` picks the destination. The `alloc_steady_programs_*` ctests +require zero for each method family over TCP and UDP. ```sh python3 tests/alloc_audit/measure_alloc.py --bin build/bin \ --shim build/lib/libss_alloc_audit.dylib --method aes-256-gcm ``` -### C baseline +### C baseline (before the rewrite) Commit `553cdc7`, macOS arm64, bundled Release build, 200 connections, each echoing 16 KiB through ss-local and ss-server on loopback. The figures @@ -126,11 +154,8 @@ includes a DNS lookup. | aes-256-cfb (stream) | 77.1 | 26.5 | | aes-256-gcm, 1-byte payload | 85.0 | 36.0 | -After the cache rewrite and `MBEDTLS_BLOCK_CIPHER_C` (programs otherwise -unchanged): aes-256-gcm 87.1 / 36.9, 2022-blake3-aes-256-gcm 80.1 / 31.0. -The new codecs and the cache are checked separately for zero allocations -by `alloc_steady_crypto_aead`, `alloc_steady_crypto_stream` and -`alloc_steady_cache` (`tests/alloc_audit/check_steady.py`). +The C++ programs are at 0 for every row, including the name-resolving +ss-server case; see [performance](performance.md) for the full comparison. Unstripped Release sizes at the same commit: ss-local 1,086,888 bytes, ss-server 1,086,952 bytes, ss-tunnel 542,200 bytes. diff --git a/docs/measurements/cpp20-stress-cpp-macos-arm64.json b/docs/measurements/cpp20-stress-cpp-macos-arm64.json new file mode 100644 index 00000000..16f16af0 --- /dev/null +++ b/docs/measurements/cpp20-stress-cpp-macos-arm64.json @@ -0,0 +1,101 @@ +[ + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 6284.539611763024, + "duration_sec": 0.13348007202148438, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2976, + "server_rss_after_kb": 3648, + "tunnel_rss_before_kb": 2544, + "tunnel_rss_after_kb": 2704, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 4999.74025169413, + "duration_sec": 0.16778087615966797, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2944, + "server_rss_after_kb": 3520, + "tunnel_rss_before_kb": 2544, + "tunnel_rss_after_kb": 2704, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 7772.288340549162, + "duration_sec": 0.10792970657348633, + "bytes_transferred": 104857600, + "server_rss_before_kb": 3040, + "server_rss_after_kb": 3648, + "tunnel_rss_before_kb": 2576, + "tunnel_rss_after_kb": 2736, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 6637.4962673593855, + "duration_sec": 0.12638211250305176, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2976, + "server_rss_after_kb": 3648, + "tunnel_rss_before_kb": 2544, + "tunnel_rss_after_kb": 2704, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 7276.850389925358, + "duration_sec": 0.11527800559997559, + "bytes_transferred": 104857600, + "server_rss_before_kb": 3008, + "server_rss_after_kb": 3584, + "tunnel_rss_before_kb": 2576, + "tunnel_rss_after_kb": 2720, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 6530.947999998515, + "duration_sec": 0.12844395637512207, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2944, + "server_rss_after_kb": 3616, + "tunnel_rss_before_kb": 2528, + "tunnel_rss_after_kb": 2688, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 2890.9696024820796, + "duration_sec": 0.29016590118408203, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2976, + "server_rss_after_kb": 3664, + "tunnel_rss_before_kb": 2576, + "tunnel_rss_after_kb": 2720, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3081.0673712629405, + "duration_sec": 0.2722630500793457, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2944, + "server_rss_after_kb": 3632, + "tunnel_rss_before_kb": 2544, + "tunnel_rss_after_kb": 2688, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 2851.8720147481336, + "duration_sec": 0.2941439151763916, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2976, + "server_rss_after_kb": 3664, + "tunnel_rss_before_kb": 2576, + "tunnel_rss_after_kb": 2720, + "error": null + } +] \ No newline at end of file diff --git a/docs/measurements/cpp20-stress-master-macos-arm64.json b/docs/measurements/cpp20-stress-master-macos-arm64.json new file mode 100644 index 00000000..ff69f1f5 --- /dev/null +++ b/docs/measurements/cpp20-stress-master-macos-arm64.json @@ -0,0 +1,101 @@ +[ + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 7026.096385910413, + "duration_sec": 0.11939215660095215, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2672, + "server_rss_after_kb": 3472, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2432, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 5562.843320410631, + "duration_sec": 0.15079712867736816, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2688, + "server_rss_after_kb": 3376, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2448, + "error": null + }, + { + "cipher": "aes-128-gcm", + "bandwidth_mbps": 6352.506217899637, + "duration_sec": 0.13205194473266602, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2688, + "server_rss_after_kb": 3472, + "tunnel_rss_before_kb": 2256, + "tunnel_rss_after_kb": 2448, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 5698.907670513842, + "duration_sec": 0.14719676971435547, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2720, + "server_rss_after_kb": 3504, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2432, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 7647.779004658522, + "duration_sec": 0.10968685150146484, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2640, + "server_rss_after_kb": 3344, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2400, + "error": null + }, + { + "cipher": "aes-256-gcm", + "bandwidth_mbps": 6828.721334061536, + "duration_sec": 0.12284302711486816, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2656, + "server_rss_after_kb": 3440, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2432, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 2650.4055406654115, + "duration_sec": 0.31650280952453613, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2640, + "server_rss_after_kb": 3456, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2416, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3232.244577277333, + "duration_sec": 0.25952887535095215, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2704, + "server_rss_after_kb": 3424, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2432, + "error": null + }, + { + "cipher": "chacha20-ietf-poly1305", + "bandwidth_mbps": 3036.6328882947946, + "duration_sec": 0.2762470245361328, + "bytes_transferred": 104857600, + "server_rss_before_kb": 2640, + "server_rss_after_kb": 3504, + "tunnel_rss_before_kb": 2240, + "tunnel_rss_after_kb": 2432, + "error": null + } +] \ No newline at end of file diff --git a/docs/measurements/cpp20-udp-cpp-macos-arm64.json b/docs/measurements/cpp20-udp-cpp-macos-arm64.json new file mode 100644 index 00000000..82bc2cee --- /dev/null +++ b/docs/measurements/cpp20-udp-cpp-macos-arm64.json @@ -0,0 +1,74 @@ +[ + { + "cipher": "aes-128-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2357705410104245, + "payload_mbps": 81.43511024624183 + }, + { + "cipher": "aes-128-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2256229999475181, + "payload_mbps": 85.09770725708854 + }, + { + "cipher": "aes-128-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2793988329358399, + "payload_mbps": 68.71896993359674 + }, + { + "cipher": "aes-256-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.37632425012998283, + "payload_mbps": 51.01983194909257 + }, + { + "cipher": "aes-256-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.3483593750279397, + "payload_mbps": 55.115496743729345 + }, + { + "cipher": "aes-256-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.3203372920397669, + "payload_mbps": 59.936824332074636 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.305656208191067, + "payload_mbps": 62.815671612329886 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2925001250114292, + "payload_mbps": 65.6409975867524 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.29241770785301924, + "payload_mbps": 65.65949832850302 + } +] diff --git a/docs/measurements/cpp20-udp-master-macos-arm64.json b/docs/measurements/cpp20-udp-master-macos-arm64.json new file mode 100644 index 00000000..37aca19e --- /dev/null +++ b/docs/measurements/cpp20-udp-master-macos-arm64.json @@ -0,0 +1,74 @@ +[ + { + "cipher": "aes-128-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.29019766696728766, + "payload_mbps": 66.16179999188039 + }, + { + "cipher": "aes-128-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.23557749995961785, + "payload_mbps": 81.50184123395154 + }, + { + "cipher": "aes-128-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.23733379202894866, + "payload_mbps": 80.89872005103298 + }, + { + "cipher": "aes-256-gcm", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.2995963329449296, + "payload_mbps": 64.08623166802664 + }, + { + "cipher": "aes-256-gcm", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.22690250002779067, + "payload_mbps": 84.61784245501224 + }, + { + "cipher": "aes-256-gcm", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.23682612483389676, + "payload_mbps": 81.07213684075752 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 0, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.34253158420324326, + "payload_mbps": 56.05322511984051 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 1, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.3179049580357969, + "payload_mbps": 60.39540911418574 + }, + { + "cipher": "chacha20-ietf-poly1305", + "trial": 2, + "packets": 2000, + "payload_bytes": 1200, + "seconds": 0.3418194579426199, + "payload_mbps": 56.17000306408256 + } +] diff --git a/docs/performance.md b/docs/performance.md index b2f3d1ba..7876e694 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -1,5 +1,97 @@ # Modernization measurements +## C++20 rewrite (2026-10-14) + +Local macOS arm64 (Apple clang), Release, bundled dependencies. Baseline: +`master` at `553cdc7` (C). Comparison: the C++20 engine branch stack. Raw +JSON is in [measurements](measurements/) (`cpp20-*`). These are short +loopback runs on one machine, not capacity guarantees. + +### Heap allocations per relayed connection + +`tests/alloc_audit/measure_alloc.py --target 127.0.0.1`: 200 TCP connections +with a small payload each, after warm-up. Each value is allocations per +connection as ss-server / ss-local. + +| Method | C | C++ | +|---|---:|---:| +| aes-256-gcm | 39.0 / 39.0 | 0 / 0 | +| chacha20-ietf-poly1305 | 33.1 / 33.0 | 0 / 0 | +| 2022-blake3-aes-256-gcm | 34.0 / 32.9 | 0 / 0 | +| aes-256-cfb | 27.1 / 26.4 | 0 / 0 | + +With a host-name target (`localhost`, resolved by ss-server) the C++ programs +are still at 0, because c-ares allocates from its fixed heap. The C ss-server +rises to 89.1 per connection. UDP relaying is also 0 per datagram, where the C relay allocated a +64 KiB buffer for every one. ctest enforces all of this (`alloc_steady_*`, including +`alloc_steady_programs_*`). + +### Size + +Unstripped Release executables, in bytes: + +| Program | C | C++ | +|---|---:|---:| +| ss-local | 1,086,888 | 1,129,960 | +| ss-server | 1,086,952 | 1,130,408 | +| ss-tunnel | 542,200 | 789,144 | + +ss-tunnel grows because it now shares the full relay engine with the other +programs, including the resolver, the UDP relay and every codec. + +### TCP throughput and memory + +`tests/stress_test.py --size 100 --repeat 3`: median of three runs, server RSS +after the transfer. + +| Cipher | C Mbps | C++ Mbps | C server RSS, KiB | C++ server RSS, KiB | +|---|---:|---:|---:|---:| +| aes-128-gcm | 6,353 | 6,285 | 3,472 | 3,648 | +| aes-256-gcm | 6,829 | 6,637 | 3,440 | 3,616 | +| chacha20-ietf-poly1305 | 3,037 | 2,891 | 3,456 | 3,664 | + +The throughput differences are within the run-to-run spread, which is about +±20% for these 0.1–0.3 s transfers. + +RSS with 1,000 established, idle connections through ss-local and ss-server +(aes-256-gcm): + +| | C server | C++ server | C local | C++ local | +|---|---:|---:|---:|---:| +| idle | 2,624 KiB | 2,992 KiB | 2,272 KiB | 2,576 KiB | +| 1,000 connections | 60,256 KiB | 80,912 KiB | 54,416 KiB | 74,752 KiB | + +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. + +### UDP + +`tests/measure_udp.py`: median payload Mbps over three runs of 2,000 echoed +1,200-byte packets. + +| Cipher | C | C++ | +|---|---:|---:| +| aes-128-gcm | 80.9 | 81.4 | +| aes-256-gcm | 81.1 | 55.1 | +| chacha20-ietf-poly1305 | 56.2 | 65.6 | + +This benchmark is dominated by Python and SOCKS round trips. Single runs vary +by more than 30% in both builds (for example, C aes-256-gcm ranged from 64 to +81), so no UDP throughput change is claimed either way. + +### Reproduce + +```sh +python3 tests/alloc_audit/measure_alloc.py --bin build/bin --shim build/lib/libss_alloc_audit.dylib \ + --method aes-256-gcm --target 127.0.0.1 +python3 tests/stress_test.py --bin build/bin --size 100 --repeat 3 --json tcp.json +python3 tests/measure_udp.py --bin build/bin --output udp.json +``` + +## Bundled dependencies (2026-09-10) + Local macOS arm64 results, 2026-09-10. Base commit: `8fe386b`; comparison: `feature/self-contained-portable-c`. Raw JSON lives in [measurements](measurements/). These are short loopback measurements on one machine, not capacity guarantees.