mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-29 20:43:44 +00:00
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.
This commit is contained in:
@@ -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 `<build>/bin/`, libraries in `<build>/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.
|
||||
|
||||
@@ -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<T>` (`src/core/result.hpp`), which
|
||||
implements a subset of `std::expected`.
|
||||
- Only header-only parts of the standard library are used (`<array>`,
|
||||
`<span>`, `<optional>`, `<variant>`, `<memory>`'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: <reason>`.
|
||||
- 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<T>`, `Status`, `Errc`, `SS_TRY`. |
|
||||
| `region.hpp` | `Region`, the only startup `calloc` in the core. Also ASan poison helpers. |
|
||||
| `pool.hpp` | `ObjectPool<T>`, `PoolPtr<T>` (`std::unique_ptr` with a pool deleter), `Handle<T>`. Types that embed libuv handles model `AsyncClosable`: their slot is recycled only after the asynchronous close finishes. |
|
||||
| `ref.hpp` | `RefCounted<T>`/`RefPtr<T>`, 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<N>`: 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.
|
||||
@@ -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.
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user