From 2285528788d7f1d959ffca70a840d36a6c6c8ab2 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Wed, 14 Oct 2026 12:51:47 +0800 Subject: [PATCH] Document the C++20 port and allocation baseline docs/cpp-core.md describes the language profile, the core primitives, the phase plan and the measured C baseline (77-89 allocations per connection in ss-server, 27-39 in ss-local). The compatibility contract now keeps the C11 public header instead of a C11 code base. CLAUDE.md drops stale libev, submodule and gnu99 references. --- CLAUDE.md | 105 +++++++++++++++++++++--------------------- docs/cpp-core.md | 97 ++++++++++++++++++++++++++++++++++++++ docs/modernization.md | 9 +++- 3 files changed, 157 insertions(+), 54 deletions(-) create mode 100644 docs/cpp-core.md diff --git a/CLAUDE.md b/CLAUDE.md index 01b2e909..05e6d4cb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,78 +4,82 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -shadowsocks-libev is a lightweight SOCKS5 proxy written in pure C. Version 3.3.6, licensed under GPLv3. +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. ## Build Commands ### CMake (sole build system) ```bash -git submodule update --init --recursive -mkdir -p build && cd build -cmake .. -make -sudo make install +cmake --preset bundled # or: cmake -S . -B build -G Ninja +cmake --build --preset bundled +ctest --preset bundled ``` -On macOS, CMake should auto-detect library paths. If needed, specify paths: -```bash -cmake .. -DCMAKE_PREFIX_PATH="/usr/local/opt/mbedtls;/usr/local/opt/libsodium" -``` +Bundled mode (the default, `SS_DEPENDENCY_MODE=bundled`) builds every dependency from the pinned archives in +`third_party/dist` with no network access. `SS_DEPENDENCY_MODE=system` uses distro libraries instead. +There are no git submodules. -CMake outputs binaries to `build/bin/` (static) and `build/shared/bin/` (shared). +Binaries land in `/bin/`, libraries in `/lib/`. ### Build Dependencies -- cmake (>= 3.2), a C compiler (gcc or clang), pkg-config -- libmbedtls, libsodium (>= 1.0.4), libpcre3, libev, libc-ares -- asciidoc + xmlto (documentation only) +- CMake >= 3.20, C11 and C++20 compilers (GCC >= 11, Clang >= 14, Apple Clang >= 15), Ninja for the presets +- System mode only: Mbed TLS 2.x/3.x, libsodium >= 1.0.4, PCRE2 (8-bit), libuv, c-ares +- Doxygen (documentation only) ### CMake Options -- `-DWITH_EMBEDDED_SRC=OFF`: use system libcork/libipset/libbloom instead of bundled submodules -- `-DWITH_DOC_MAN=OFF`: skip man page generation -- `-DENABLE_CONNMARKTOS=ON`: Linux netfilter conntrack QoS support -- `-DENABLE_NFTABLES=ON`: nftables firewall integration -- `-DDISABLE_SSP=ON`: disable stack protector -- `-DBUILD_TESTING=OFF`: disable unit tests +- `-DSS_MINIMAL=ON`: no regex, plugins, manager or legacy stream ciphers +- `-DWITH_STATIC=OFF`: link third-party dependencies dynamically +- `-DSS_BUILD_EXECUTABLES / SS_BUILD_STATIC_LIBRARY / SS_BUILD_SHARED_LIBRARY`: choose outputs +- `-DWITH_DOC_MAN=ON` / `-DWITH_DOC_HTML=ON`: build documentation +- `-DENABLE_CONNMARKTOS=ON`, `-DENABLE_NFTABLES=ON`: Linux netfilter features +- `-DENABLE_SANITIZERS=ON`, `-DENABLE_COVERAGE=ON`, `-DDISABLE_SSP=ON`, `-DBUILD_TESTING=OFF` ## Testing ### Unit Tests (CTest) ```bash -cd build -ctest --output-on-failure +ctest --test-dir build -LE memcheck --output-on-failure ``` -10 unit test modules cover: base64, buffer, crypto, json, jconf, cache, ppbloom, rule, netutils, utils. +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`. +Labels: `unit`, `vendor`, `integration`, `memcheck` (Linux + valgrind). ### Integration Tests -Integration tests use Python and require `curl` and `dig` to be available: ```bash -bash tests/test.sh +python3 tests/interop.py --self --bin build/bin # TCP/UDP relay, all AEAD/2022 methods +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 ``` -The test harness (`tests/test.py`) starts ss-server, ss-local, and ss-tunnel locally, then runs curl through the SOCKS5 proxy and dig through the tunnel. Each test config in `tests/*.json` exercises a different cipher. +### Allocation audit -Run a single cipher test: ```bash -python tests/test.py --bin build/bin/ -c tests/aes-gcm.json +python3 tests/alloc_audit/measure_alloc.py --bin build/bin --shim build/lib/libss_alloc_audit.dylib ``` +Reports heap allocations per relayed connection (C baseline numbers are in `docs/cpp-core.md`). + ## Code Formatting Uses **uncrustify** with the config at `.uncrustify.cfg`. Key settings: 4-space indent, no tabs, 120-column width, K&R brace style (braces on same line). ## Code Quality Tooling -- **clang-tidy**: config in `.clang-tidy` (clang-analyzer + bugprone + cert checks, scoped to `src/`). The build exports `compile_commands.json` automatically. Run locally: +- **clang-tidy**: config in `.clang-tidy`. The build exports `compile_commands.json` automatically. Run locally: ```bash - run-clang-tidy -quiet -p build '/src/[^/]+\.c$' + run-clang-tidy -quiet -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_core_[^/]+\.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/`. - **Sanitizers**: `cmake .. -DENABLE_SANITIZERS=ON` builds with ASan + UBSan. CI runs ctest and the stress test under sanitizers on every PR. - **Coverage**: `cmake .. -DENABLE_COVERAGE=ON`, run tests, then `make coverage` (needs lcov). HTML report lands in `build/coverage-html/`. CI uploads it as the `coverage-html` artifact. @@ -91,44 +95,41 @@ Each binary is compiled with a module define that controls conditional compilati | `ss-server` | `MODULE_REMOTE` | Server-side proxy | | `ss-tunnel` | `MODULE_TUNNEL` | Port forwarding tunnel (implies `MODULE_LOCAL`) | | `ss-redir` | `MODULE_REDIR` | Transparent proxy via iptables (Linux only, implies `MODULE_LOCAL`) | -| `ss-manager` | `MODULE_MANAGER` | Multi-server manager daemon | +| `ss-manager` | `MODULE_MANAGER` | Multi-server manager daemon (Linux only by default) | -A shared library `libshadowsocks-libev` is also built from the ss-local sources with `-DLIB_ONLY`. Its public API is in `src/shadowsocks.h`. +A library `libshadowsocks-c` (legacy names `libshadowsocks-libev.*` are kept) is built from the ss-local sources with `-DLIB_ONLY`. Its public API is in `src/shadowsocks.h`. ### Source Organization (`src/`) **Shared by all binaries:** -- `utils.c` - logging, system utilities +- `utils.c` - logging, system utilities, `ss_malloc`/`ss_free` - `jconf.c` / `json.c` - JSON config file parsing - `netutils.c` - network address utilities -- `cache.c` - hash-based LRU connection cache +- `cache.c` - uthash-based connection cache (UDP relay, AEAD-2022 tables) - `udprelay.c` - UDP relay implementation (shared, but uses `#ifdef MODULE_*` for per-binary behavior) +- `ss_event.c` / `ss_event.h` - event loop API over libuv +- `resolv.c` - asynchronous DNS via c-ares -**Crypto layer** (two parallel implementations behind a common `crypto_t` interface): -- `crypto.c` / `crypto.h` - crypto initialization, key derivation (HKDF), buffer management. Defines `crypto_t` with function pointers for encrypt/decrypt. -- `stream.c` - stream cipher implementation (CFB mode via mbedTLS) -- `aead.c` - AEAD cipher implementation (AES-GCM via mbedTLS, ChaCha20-Poly1305 via libsodium) -- `ppbloom.c` - ping-pong bloom filter for nonce replay detection +**Crypto layer** (behind a common `crypto_t` function-pointer interface): +- `crypto.c` / `crypto.h` - crypto initialization, key derivation (HKDF), `buffer_t` management +- `stream.c` - legacy stream ciphers +- `aead.c` - AEAD ciphers (AES-GCM via Mbed TLS/libsodium, ChaCha20-Poly1305 via libsodium) +- `aead2022.c` / `aead2022_udp.c` - Shadowsocks 2022 (SIP022), BLAKE3 from `src/blake3` +- `ppbloom.c` - ping-pong bloom filter for salt replay detection (`third_party/bloom`) -**ACL (Access Control Lists):** -- `acl.c` / `rule.c` - IP/domain-based routing rules using libipset +**ACL:** `acl.c` / `rule.c` / `ipset.c` - IP prefix sets and PCRE2/literal domain rules. -**Plugin support:** -- `plugin.c` - SIP003 plugin subprocess management +**Plugins:** `plugin.c` / `process.c` - SIP003 plugin subprocess management. -### Bundled Submodules - -Three git submodules in the repo root (can be replaced with system libs via `-DWITH_EMBEDDED_SRC=OFF`): -- `libcork/` - data structures (dllist, hash-table, buffers) -- `libipset/` - IP set operations for ACL -- `libbloom/` - bloom filter implementation +**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`. ### Event Loop -All binaries use **libev** for async I/O. 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). +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). ### Compiler Flags -Default flags from `CMakeLists.txt`: `-g -O2 -Wall -Werror -Wno-deprecated-declarations -fno-strict-aliasing -std=gnu99 -D_GNU_SOURCE` +From `cmake/ProjectOptions.cmake`: `-Wall -Werror -Wno-deprecated-declarations -fno-strict-aliasing -fstack-protector-strong`, plus `_GNU_SOURCE` on Linux. C is C11 with GNU extensions. C++ is C++20 without extensions and adds `-Wextra -fno-exceptions -fno-rtti -fno-threadsafe-statics`. -The `-Werror` flag means all warnings are errors - new code must compile warning-free. +`-Werror` means all warnings are errors — new code must compile warning-free. diff --git a/docs/cpp-core.md b/docs/cpp-core.md new file mode 100644 index 00000000..24ecf11e --- /dev/null +++ b/docs/cpp-core.md @@ -0,0 +1,97 @@ +# C++20 core and allocation policy + +Status: phase 1 of an incremental port. The ownership and allocation +primitives in `src/core/` exist and are tested; no program code uses them yet. +All binaries are still built from the C sources. + +## Goals + +- Move `src/` from C11 to C++20, one module at a time, with every step + keeping the CLI, the JSON configuration, protocol behaviour and the public + `shadowsocks.h` C ABI unchanged. +- Make ownership explicit. Each object has exactly one owner, a + `std::unique_ptr`. Non-owning references are generation-checked handles + that go stale instead of dangling. +- No heap allocation after startup. Connections, buffers, cipher contexts, + DNS queries and UDP sessions come from fixed-capacity pools reserved once + at startup. The steady-state data path does not call `malloc`. When a pool + is full, new work is refused rather than allocated. + +## Language profile + +- C++20, `-fno-exceptions -fno-rtti -fno-threadsafe-statics`, warnings as + errors. Errors are returned as `ss::Result` (`src/core/result.hpp`), which + implements a subset of `std::expected`. +- Only header-only parts of the standard library are used (``, + ``, ``, ``, ``'s `std::unique_ptr`). This + keeps the binaries and the static library free of libstdc++/libc++ symbols, + so C consumers of `libshadowsocks-c.a` do not need a C++ runtime. +- `scripts/check_no_heap.py` (a ctest) rejects `new`, `malloc`, allocating + containers, `std::string`, `std::function` and `std::shared_ptr` in C++ + sources under `src/`. A startup reservation that really must allocate is + marked `// heap-ok: `. +- blake3 (`src/blake3`) and `third_party/bloom` stay C. +- Minimum compilers: GCC 11, Clang 14, Apple Clang 15. + +## Primitives (`src/core/`) + +| Header | Purpose | +|---|---| +| `result.hpp` | `Result`, `Status`, `Errc`, `SS_TRY`. | +| `region.hpp` | `Region`, the only startup `calloc` in the core. Also ASan poison helpers. | +| `pool.hpp` | `ObjectPool`, `PoolPtr` (`std::unique_ptr` with a pool deleter), `Handle`. Types that embed libuv handles model `AsyncClosable`: their slot is recycled only after the asynchronous close finishes. | +| `ref.hpp` | `RefCounted`/`RefPtr`, intrusive shared ownership for the few genuinely shared objects (AEAD-2022 UDP sessions). Used instead of `std::shared_ptr` because its control block cannot be pooled portably. | +| `buffer.hpp` | `FixedBuffer`: inline bytes with `std::span` views; replaces `buffer_t`. | +| `arena.hpp` | `MonotonicArena` for configuration data that lives as long as the process. | +| `fixed_map.hpp` | `FixedHashMap` (open addressing, backward-shift deletion) and `FixedLru`; replace uthash and `cache.c`. | +| `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. | + +## Phases + +1. Build and core (this change): C++ enabled in CMake, `src/core/`, unit tests + (`test_core_*`), the no-heap lint and the allocation audit. +2. Mechanical C-to-C++ conversion, leaf modules first. +3. Event loop wrapper over libuv. +4. Crypto: cipher contexts held by value, dispatched with `std::variant`. +5. Services: cache, 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. + +## 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. + +```sh +python3 tests/alloc_audit/measure_alloc.py --bin build/bin \ + --shim build/lib/libss_alloc_audit.dylib --method aes-256-gcm +``` + +### C baseline + +Commit `553cdc7`, macOS arm64, bundled Release build, 200 connections, each +echoing 16 KiB through ss-local and ss-server on loopback. The figures +include allocations made inside libuv, c-ares and Mbed TLS. ss-server +resolves the destination (`localhost`) for every connection, so its count +includes a DNS lookup. + +| Method | ss-server allocations/connection | ss-local allocations/connection | +|---|---:|---:| +| aes-256-gcm | 89.1 | 39.2 | +| chacha20-ietf-poly1305 | 83.1 | 33.0 | +| 2022-blake3-aes-256-gcm | 84.0 | 33.0 | +| aes-256-cfb (stream) | 77.1 | 26.5 | +| aes-256-gcm, 1-byte payload | 85.0 | 36.0 | + +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/modernization.md b/docs/modernization.md index ed8f19f9..d3c8a27c 100644 --- a/docs/modernization.md +++ b/docs/modernization.md @@ -6,12 +6,17 @@ review: https://github.com/shadowsocks/shadowsocks-libev/pull/3050 (merged). ## Compatibility contract -Keep C11 and the existing CLI, JSON configuration, ss:// parsing, SIP003 plugins, +Keep the existing CLI, JSON configuration, ss:// parsing, SIP003 plugins, ACL blacklist/whitelist/outbound precedence, public shadowsocks.h API, legacy stream ciphers, AEAD and SIP022 TCP/UDP behavior in the compatibility profile. Removing regex or legacy ciphers is explicit opt-in, with configuration errors for unavailable functionality. Linux redirection/netfilter remains Linux-only. +The sources are moving from C11 to C++20 module by module, with an explicit +ownership model and no heap allocation after startup. The public +`shadowsocks.h` header stays a C11 header with an unchanged ABI. See +[C++20 core and allocation policy](cpp-core.md). + ## Work and evidence - [x] Baseline: fresh rebuild, unit/integration results, binary/dependency sizes, @@ -44,7 +49,7 @@ The fresh baseline passes all 13 original unit tests. ## Build interface -Requires CMake 3.20+, a C11 compiler, and a build executor. Presets use Ninja; +Requires CMake 3.20+, C11 and C++20 compilers, and a build executor. Presets use Ninja; manual configuration may use another generator. ```