The unauthenticated stream ciphers (aes-*-cfb, aes-*-ctr, camellia-*-cfb, chacha20, chacha20-ietf, salsa20) are gone from every build, together with the SS_ENABLE_LEGACY option, the C++ stream codecs, their C test oracle and the stream-only tests and configurations. A configuration naming one of them, or RC4/table, now fails at startup with a message pointing to an AEAD method, and test_cli checks that. Shell completions list only the supported methods (and now include the 2022 ones), and the docs and the compatibility contract say the stream ciphers were removed.
6.9 KiB
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 (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 (stream, since removed) | 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
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.
These are short loopback measurements on one machine, not capacity guarantees.
Size and runtime dependencies
Unstripped Release executable sizes in bytes:
| Program | Old system-shared | Old static dependencies | Bundled | Minimal |
|---|---|---|---|---|
| ss-local | 295,912 | 1,326,328 | 756,920 | 416,424 |
| ss-server | 314,360 | 1,500,968 | 962,632 | 622,024 |
| ss-tunnel | 268,680 | 828,712 | 416,440 | 397,704 |
Bundled programs link only macOS OS libraries (libSystem, and where needed
libresolv). They need no separately installed libev, c-ares, PCRE2, libsodium
or Mbed TLS. Compared with the old static-dependency build, ss-server shrinks
about 36%; minimal shrinks about 59%. Compared with a shared-dependency binary
alone, bundling makes the executable larger; that comparison excludes the old
libraries' disk footprint. Bundled source archives add roughly 12 MB to checkout
and release size. There is no configure-time network access or submodule fetch.
TCP and resident memory
Median of three 100 MiB transfers using tests/stress_test.py, old shared
dependencies versus bundled. Both sets pass all nine transfers.
| Cipher | Before Mbps | Bundled Mbps | Before server RSS after, KiB | Bundled server RSS after, KiB |
|---|---|---|---|---|
| AES-128-GCM | 7,002 | 7,003 | 3,488 | 3,040 |
| AES-256-GCM | 7,776 | 8,498 | 3,488 | 2,992 |
| ChaCha20-IETF-Poly1305 | 3,439 | 3,268 | 3,472 | 3,072 |
Bundled tunnel RSS after transfer was about 2,000–2,032 KiB versus 2,576–2,592 KiB before. RSS includes OS accounting and shared-page effects; this is not a proof of bounded memory under arbitrary workloads. Transfer times are short (roughly 0.1–0.3 s), so the apparent AES-256 improvement and ChaCha regression need longer controlled measurements before drawing performance claims.
UDP
Median of three runs, 2,000 sequential 1,200-byte echo packets per run through SOCKS5 UDP ASSOCIATE. Every packet's payload and relay source port are checked.
| Cipher | Before payload Mbps | Bundled payload Mbps |
|---|---|---|
| AES-128-GCM | 77.43 | 75.01 |
| AES-256-GCM | 81.34 | 63.33 |
| ChaCha20-IETF-Poly1305 | 78.65 | 67.89 |
This round-trip benchmark includes Python thread creation, scheduling and SOCKS overhead. It shows lower observed UDP rates in this run, up to 22%; modernization is not claimed to improve UDP throughput. The portable libsodium backend is a maintenance/performance tradeoff. Optimized backends require separate compiler probes and cross-platform known-answer tests before adoption.
Independently of these timings, the new interoperability test exposed the old MTU-sized receive buffer truncating 4 KiB UDP payloads. Full datagram reception now passes 1, 128, 1,200 and 4,096-byte interoperability cases against shadowsocks-rust in both directions.
Reproduce
python3 tests/stress_test.py --bin build-bundled/bin --size 100 --repeat 3 --json tcp.json
python3 tests/measure_udp.py --bin build-bundled/bin --output udp.json
python3 tests/interop.py --bin build-bundled/bin
python3 tests/interop.py --self --bin build-bundled/bin --plugin tests/sip003_fixture.py
Run comparisons serially. The baseline was rebuilt from its own worktree using
the preserved original dependency checkouts; no modernization fixes were applied
to that source. The historical curl interop harness obeyed no_proxy and could
bypass SOCKS, so its old results are excluded from compatibility evidence.