Commit Graph
29 Commits
Author SHA1 Message Date
Max Lv 335eb10742 Add an embedding API: ss_local_start/stop/wait and a C++ wrapper
<shadowsocks/control.h> starts ss-local on a library-owned thread,
stops it from any thread through a libuv async handle and waits for it;
<shadowsocks/shadowsocks.hpp> wraps that in a move-only, header-only
shadowsocks::LocalServer whose destructor stops and waits. The library
exports only C symbols, shadowsocks.h is unchanged, and the shared
library version becomes 2.1.0 (SOVERSION 2). test_local_api_cpp covers
the lifecycle; the installed-package consumer gains a C++ program.
2026-10-15 00:34:30 +08:00
Max Lv 282f3606d7 Add --max-connections and --max-udp-sessions
The TCP connection pool and the UDP session table are sized from these
options (or max_connections / max_udp_sessions in the configuration
file) at startup instead of fixed constants; the DNS query pool follows
the connection limit. test_limits.py checks that a connection beyond
the limit is refused and that accepting resumes once one closes.
2026-10-15 00:34:30 +08:00
Max Lv c69fb4c4d9 Seal the heap: bounded library heaps and zero-allocation program tests
The programs point libuv, c-ares and Mbed TLS (MBEDTLS_PLATFORM_MEMORY in
the bundled build) at their own FixedHeap before anything runs, mark the
end of startup with heap_seal(), and in verbose mode report heap use and
any allocation after it at exit. The library build keeps the host's
allocator.

measure_alloc.py gains --udp, and the alloc_steady_programs_* tests now
fail on any malloc per relayed TCP connection (by name and by address)
or UDP association, for every cipher family.
2026-10-15 00:34:30 +08:00
Max Lv 892b45d1e5 Add the C++ TCP relay engine and run ss-tunnel on it
One engine (src/relay/tcp) for the four TCP relays: connections live in a
fixed pool owned through PoolPtr, handshake, cipher state and buffers are
inline, timeouts are intrusive deadline lists on one timer and DNS answers
come back through generation-checked handles. A full pool refuses new
connections and pauses accepting until a slot frees.

ss-tunnel is the first user; its hand-written relay callbacks are gone.
tests/tunnel_interop.py covers every method against ss-server and
shadowsocks-rust.
2026-10-14 22:53:09 +08:00
Max Lv 78ca0af745 Rewrite the resolver in C++ on a fixed query pool
Queries live in pool slots with their first A and AAAA answer inline,
c-ares sockets are watched from a fixed table, and answers reach the
waiter through a callback plus opaque words instead of a back-pointer.
resolv.cpp keeps the C API as a shim. The event Loop can now adopt the
ss_event loop, and a Poll can stop watching a socket it does not own.
2026-10-14 22:53:09 +08:00
Max Lv 47163a57fd Add allocation-free C++ SIP022 codecs
TCP request/response streams with the salt echo between the two
directions, an exact-match salt pool on FixedLru, and UDP packets sealed
and opened in place with caller-owned session state. Differentially
tested against aead2022.cpp in both roles; the steady phase allocates
nothing.
2026-10-14 22:53:08 +08:00
Max Lv a6113cec4b Own AEAD-2022 UDP sessions by reference so eviction cannot free a live session
The session table and the relay contexts each hold a reference; evicting a
session from the full table no longer frees memory a relay still uses.
Ported from the parked fix/aead2022-udp-session-eviction work.
2026-10-14 22:53:08 +08:00
Max Lv d91ef14f76 Add allocation-free C++ stream cipher codecs
src/crypto/stream.hpp implements the legacy stream methods this build
supports (AES CFB/CTR, Camellia CFB, Salsa20, ChaCha20, ChaCha20-IETF)
as value types. The AES and Camellia contexts are held by value through
Mbed TLS's block-cipher APIs instead of an allocated
mbedtls_cipher_context_t, and the libsodium ciphers track their
keystream position, finishing partial 64-byte blocks from a stack block
instead of the padding copies stream.cpp makes.

test_crypto_stream_cpp runs TCP streams (with fragments that split
keystream blocks) and UDP packets between the C and C++
implementations in both directions for all twelve methods and checks
IV replay; alloc_steady_crypto_stream requires zero allocations for new
connections and packets.
2026-10-14 22:53:07 +08:00
Max Lv ed75faf80f Rewrite the cache on a fixed slab without per-entry allocation
cache.cpp keeps its C API but replaces uthash and the two mallocs per
entry with one slab of nodes holding their keys inline, linked in
last-use order, and an open-addressing index of node ids that hashes
through the slab with a per-cache random seed. Both blocks are reserved
by cache_create() and touched only as entries are used, so a large,
mostly empty cache costs no resident memory.

Semantics are unchanged: an insert evicts the least recently used entry
once the cache holds capacity entries (the AEAD-2022 session table's
4095/4096 boundary), lookups refresh recency and age, cache_clear()
drops stale entries, and free_cb may re-enter the cache. Re-inserting a
key now replaces its data instead of shadowing it. cache_create_keyed()
sizes keys per cache; the AEAD-2022 salt pool and session table use it.

test_cache_cpp covers the boundary, ordering, replacement, ageing,
re-entrant callbacks and limits; alloc_steady_cache requires zero
allocations across 100,000 inserts on a full cache. AEAD-2022 ss-server
drops from 84 to 80 allocations per relayed connection.
2026-10-14 22:53:07 +08:00
Max Lv 110459eba8 Disable the zero-allocation checks with a system Mbed TLS
They hold for the bundled Mbed TLS (MBEDTLS_BLOCK_CIPHER_C); a system
one may allocate in GCM key setup, as the Debian package build shows.
2026-10-14 22:53:06 +08:00
Max Lv 556c1f0696 Add allocation-free C++ AEAD codecs
src/crypto/aead.hpp implements the SIP004/SIP007 AEAD ciphers as value
types: AeadEngine holds the libsodium or Mbed TLS GCM state by value,
AeadTcpEncryptor/AeadTcpDecryptor frame the chunk stream with a fixed
reassembly buffer, and aead_udp_seal/open handle packets. HKDF-SHA1 runs
on stack SHA-1 state instead of Mbed TLS's allocating HMAC API.

The bundled Mbed TLS config now enables MBEDTLS_BLOCK_CIPHER_C, which
keeps the AES context inside mbedtls_gcm_context, so GCM key setup no
longer allocates. That alone saves the C programs two allocations per
connection.

test_crypto_aead_cpp checks HKDF against RFC 5869 and crypto_hkdf, runs
TCP streams and UDP packets between the C and C++ implementations in
both directions for all five AEAD methods, and covers tampering,
framing and salt replay. alloc_steady_crypto_aead runs it under the
malloc counter and requires zero allocations across 600 new
connections and 600 packets (tests/alloc_audit/check_steady.py).

ss_strict_warnings() now applies -Wextra per source file so targets
mixing new C++ with converted C modules keep the latter at -Wall, and
Result gains move assignment.
2026-10-14 22:51:56 +08:00
Max Lv df7a6dc13f Add the C++ event layer over libuv
src/event/loop.hpp wraps uv_loop/poll/timer/signal/async handles that
live by value inside their owner and dispatch to an owner member
function chosen at compile time. A Poll owns its socket and carries
read and write interest on one handle, closing the descriptor in
libuv's close callback. CloseGroup collects the asynchronous closes of
an owner's handles and reports completion afterwards, which is how
pooled owners implement AsyncClosable. DeadlineList drives the idle
timeouts of many owners from one timer.

test_event_cpp covers readiness, closing a peer from inside a
callback, recycling a pooled owner only after its handles closed,
repeating timers, deadline ordering and cross-thread wakeups.
2026-10-14 22:51:38 +08:00
Max Lv 227ad1bb9c Compile the event loop, UDP relay and programs as C++
Rename utils, ss_event, udprelay, winsock, local, server, tunnel, redir
and manager from .c to .cpp. Inside the moved files only the
\snippet{doc} file references and header comments change; snippet
references in doc/*.md, tests/test_cli_docs.py, the build files and
CONTRIBUTION.md follow the new names.

Also cast the Windows-only string literal returned by
get_default_conf(), found by the zig x86_64-windows-gnu build.

Every module of src/ except the unbuilt android.c and the vendored
blake3 is now compiled as C++20. The new .cpp files join the no-heap
ratchet baseline.
2026-10-14 22:51:17 +08:00
Max Lv 349d895fd2 Compile the crypto, config, DNS, ACL and plugin modules as C++
Rename netutils, jconf, stream, aead, aead2022, aead2022_udp, crypto,
acl, plugin, plugin_disabled and resolv from .c to .cpp without changing
their contents, and update source lists, snippet references, test
comments and debian/copyright.

The Doxygen source list and scripts/check_cli_docs.py now accept either
extension for each module, so the remaining programs can move one at a
time. The new .cpp files are added to the no-heap ratchet baseline.
2026-10-14 22:51:16 +08:00
Max Lv 738956b5da Compile the leaf modules as C++
Rename base64, json, ipset, process, ppbloom, rule, ssurl and cache from
.c to .cpp without touching their contents, and update the CMake source
lists, debian/copyright and the docs.

Ported code keeps -Wall; -Wextra is now opt-in through
ss_strict_warnings() for code written as C++ (the core tests).
check_no_heap.py gains a ratchet baseline for the converted modules,
which still allocate the way the C code did. A new check_cxx_runtime
test verifies with nm that the programs and libshadowsocks-c.a still
need no C++ runtime library, so C consumers are unaffected.
2026-10-14 22:51:15 +08:00
Max Lv f90cfe9034 Add C++20 ownership and allocation core
Header-only primitives for the no-heap-after-startup design:
Result/Status, Region, ObjectPool with PoolPtr (std::unique_ptr with a
pool deleter), generation-checked Handle and asynchronous close support,
intrusive RefPtr, FixedBuffer, MonotonicArena, FixedHashMap and FixedLru,
IntrusiveList, and a bounded FixedHeap for third-party allocator hooks.

Unit tests cover each primitive (test_core_*). scripts/check_no_heap.py
rejects heap allocation in C++ sources under src/ and runs as a ctest.
clang-tidy now also analyses C++ sources and the core headers.
2026-10-14 12:51:47 +08:00
Max LvandClaude 553cdc7945 Add opt-in printable TCP salt experiment for ss-local (#3063)
Add `ss-local --printable-salt` / `"printable_salt": true`, disabled by
default. For each outgoing TCP salt it picks a prefix length uniformly
from 6-12 bytes and samples those bytes as printable ASCII (0x20-0x7e)
immediately before key derivation. The salt stays 32 bytes with no
framing or markers, so the wire format is unchanged and any stock
server interoperates. Only chacha20-ietf-poly1305 and aes-256-gcm are
accepted; other ciphers fail at startup. UDP, server responses, and
AEAD-2022 are untouched.

Crypto unit tests cover length distribution, empty first writes, exact
framing overhead, salt-tail preservation, byte-at-a-time decryption,
tampering and replay rejection. CLI tests check local-only exposure and
cipher rejection via flag, config key and bad config type. interop.py
gains --printable-salt and --server-bin for independent stock servers.

docs/printable-salt.md records the design, entropy bound, fingerprint
caveats and route-experiment procedure; docs/encoding-experiments.md
and tests/experiments/ hold the Base85/lowpop85/TLS outer-transport
prototypes. docs/measurements/printable-salt-2026-09-23/ archives the
sanitized results of the aborted field trials.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 09:09:51 +08:00
Max Lv ea951e6bd1 Redesign CLI flags, help, and argument validation 2026-09-12 21:17:38 +08:00
Max Lv 8773939dce Raise the file descriptor limit for descriptor-heavy tests
test_event opens 256 UDP receivers plus a sender on top of the loop's own
descriptors. macOS starts processes with a soft RLIMIT_NOFILE of 256, so
socket() eventually returned -1 and the unchecked descriptor tripped the
bind() assertion when running ctest from a stock terminal (#3058).

Wrap every registered unit test in a small sh launcher that raises the soft
limit to 1024 before exec'ing the binary, and have test_event raise its own
limit as well so it also passes when run directly. Assert on socket()
failures so a limit problem is reported as such instead of as a bind error.

Fixes #3058

Claude-Session: https://claude.ai/code/session_01PSG4XV3jiqitDgdgFSGATx
2026-09-12 15:23:43 +08:00
Max Lv bdc77408cb Rename to shadowsocks-c with libuv, async DNS and CI lint (#3051)
Rename the project to shadowsocks-c while retaining ss-* commands, the embedding ABI, and legacy library/package lookup compatibility.

Replace libev with bundled libuv for IOCP/kqueue event backends, expand asynchronous runtime DNS coverage, and add actionlint/Ruff with strict clang-tidy failure handling. Validate native platforms, static builds, packaging, interoperability, sanitizers, and canonical/legacy consumers.

All 22 hosted checks pass at reviewed head a8493250eb.
2026-09-11 14:18:37 +08:00
Max Lv 201e70c4bd Modernize C builds with offline dependencies and portable runtime helpers (#3050)
Bundle pinned offline dependencies, remove libcork/libipset and submodule requirements, and add portable runtime helpers with relocatable static/shared library installations.

Add Clang static build validation for Linux/musl, macOS and Windows, real TCP/UDP and SIP003 interoperability coverage, and recorded performance tradeoffs. All 21 hosted checks pass at the reviewed PR head 8d504e6218.
2026-09-11 13:42:11 +08:00
Max Lv 8cc58cea95 Accept an ss:// server URL on the ss-local command line
shadowsocks-rust takes a whole server definition as a URL; libev could
only be given the pieces separately, so importing a server meant taking a
URL apart by hand. ss-local now accepts --server-url and reads the
address, port, cipher, password and SIP003 plugin from it.

Both encodings are handled: the SIP002 form with the userinfo in
base64url, and the older form where the whole body is one base64 blob.
The userinfo is also accepted in the clear, because that is what
shadowsocks-rust's `ssurl` emits for the 2022 ciphers, whose base64
pre-shared keys it percent-encodes instead. IPv6 literals, a trailing
slash, the plugin query and the '#' tag are all covered.

Options given later on the command line still win, since they are applied
as getopt reaches them.

ssurl.c sits with the ss-local sources rather than the shared ones: it
depends on base64.c, which ss-manager does not link.

The tests include URLs copied verbatim from `ssurl --encode` output, so
the parser is pinned against what another implementation really produces
rather than only against my reading of the spec. Verified end to end by
pointing ss-local at a rust-generated URL and proxying through their
ssserver.
2026-08-19 20:15:38 +08:00
Max Lv 3728a993a7 Add an interoperability test against shadowsocks-rust
A cipher can round-trip perfectly against our own implementation and
still be wrong: header layout, padding rules, and nonce ordering are only
pinned down by talking to another implementation. This drives ss-local
against rust's ssserver and rust's sslocal against ss-server, over a
local origin server so it needs no network access.

Covers the three SIP022 methods plus aes-256-gcm, so a regression in the
legacy path shows up here too. Skips itself when shadowsocks-rust or the
built binaries are absent, so it is safe to run anywhere.
2026-08-19 18:42:39 +08:00
Max Lv 46791234c0 Add Shadowsocks 2022 (SIP022) ciphers
shadowsocks-rust has supported the 2022 edition for years while libev was
stuck on the 2017 AEAD construction, the largest functional gap between
the two implementations. This adds the three SIP022 methods:

  2022-blake3-aes-128-gcm
  2022-blake3-aes-256-gcm
  2022-blake3-chacha20-poly1305

TCP uses BLAKE3 derive_key for session subkeys, standalone header chunks
carrying a type byte and timestamp, request padding, and a response header
echoing the request salt. Replay protection uses an exact-match salt pool
with 60s retention rather than the bloom filter, which SIP022 forbids
because false positives are unacceptable there.

UDP is session-based: an AES-ECB separate header for the AES methods and a
merged XChaCha20-Poly1305 construction for ChaCha, with a per-session
sliding window replay filter. Servers must route by client session ID,
which is only known after decryption, so the crypto layer owns the
server-side session table and hands the session back to udprelay for the
reply path.

The 2022 code lives in aead2022.c and aead2022_udp.c so aead.c keeps its
current size, with aead_internal.h carrying what they share. BLAKE3 is
vendored (portable backend only) as its own build target.

Keys are base64 pre-shared keys of exactly the cipher's key size; per
SIP022 a password is never stretched into a key.

Verified interoperable in both directions against shadowsocks-rust 1.24.0
for all three ciphers over TCP and UDP, including an 8MB transfer checked
by digest, plus unit tests for header framing, partial delivery, tamper
detection, and UDP replay rejection.
2026-08-19 18:15:35 +08:00
Max Lv 5559eec939 Harden security-sensitive input handling (#3045)
* harden security-sensitive input handling

* enable ctest failure checks in ci

* add valgrind leak smoke tests

* split tests into separate workflow
2026-07-15 11:10:24 +08:00
Pain 95b8a2ca7a Fix CMake Warning 2026-04-16 07:39:14 -07:00
Max LvandClaude Opus 4.6 6423c8e496 Add Debian package build test and statically link bundled submodules
Add tests/test_deb_build.sh with four phases:
1. Build .deb packages via dpkg-buildpackage
2. Verify all three packages contain expected files (binaries, man
   pages, shared library, headers, pkg-config)
3. Install packages and resolve dependencies
4. Smoke-test installed binaries, ldconfig, and headers

Add a Linux-only CI step in build.yml to run the test.

Statically link the bundled submodule libraries (libcork, libipset,
libbloom) into all targets instead of building them as shared libs.
These are vendored code built from git submodules, not system
libraries, so static linking is correct and eliminates
dpkg-shlibdeps warnings about missing .so files.

Pass -DWITH_STATIC=OFF in debian/rules since debian build-depends
only provide shared library dev packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 11:32:13 +08:00
Max LvandClaude Opus 4.6 5c2d0e43fa Add ss-setup TUI tool with unit tests and CI integration
Interactive whiptail/dialog TUI for server/client config generation,
systemd service management, SIP003 plugin installation, and ss:// URI
output. Includes 131 bash unit tests covering all utility functions
(validation, JSON generation, URI encoding, config round-trip parsing).
Tests run in CTest and as a dedicated GitHub Actions step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 18:30:20 +08:00
Max LvandClaude Opus 4.6 7bb250739e Modernize build: switch to CMake, add unit tests, fix man pages
* Modernize build: switch to CMake, add unit tests, fix man pages

Remove autotools (configure.ac, Makefile.am, m4/, autogen.sh) and make
CMake the sole build system. Enhance CMake with proper Find modules for
MbedTLS, libsodium, PCRE, and c-ares with version/feature detection.
Add support for connmarktos, nftables, stack protector, and MinGW.

Add 10 unit test suites (CTest) covering base64, buffer, cache, crypto,
jconf, json, netutils, ppbloom, rule, and utils modules. Add a Python
stress test measuring bandwidth per cipher with memory leak detection.

Fix man page typos ("Resovle" -> "Resolve"), document missing TCP buffer
options, fix ss-manager duplicate --executable entry, add example section
to ss-tunnel, and document --workdir and --nftables-sets options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Migrate from PCRE to PCRE2

Replace legacy PCRE (libpcre3) with PCRE2 (libpcre2-8). PCRE1 has been
end-of-life since 2021.

- src/rule.h: use pcre2_code and pcre2_match_data types
- src/rule.c: pcre_compile -> pcre2_compile, pcre_exec -> pcre2_match,
  pcre_free -> pcre2_code_free, with proper error message retrieval
- cmake/FindPCRE.cmake -> cmake/FindPCRE2.cmake: find libpcre2-8 via
  pkg-config, pcre2-config, or manual search
- Update all CMakeLists.txt, config.h.cmake, configure.cmake, README.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Update Dockerfiles: use CMake build and pcre2-dev

The alpine Dockerfile still used autotools (autogen.sh/configure) which
were removed. Switch to cmake build and replace pcre-dev with pcre2-dev.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix Docker build: disable tests and remove git dep

The tests/ directory is excluded by .dockerignore, so pass
-DBUILD_TESTING=OFF to cmake. Submodules are already checked out
by actions/checkout, no need for git inside the container.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix Docker build: disable static libs (not available in alpine)

Alpine's mbedtls-dev only provides shared libraries. Disable static
build since Docker only needs shared binaries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Rewrite GitHub Actions: build and test directly on runner

Replace Docker-based CI with direct cmake build on ubuntu-latest and
macos-latest. Install dependencies via apt/brew, build with cmake,
run unit tests with ctest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix CI: use mbedtls@3 on macOS, disable static build

- macOS: brew's mbedtls is now v4 (incompatible), use mbedtls@3
- Disable static build since CI runners lack static lib packages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix test build on Linux: add stdint.h and link libm

- test_ppbloom.c: add missing #include <stdint.h> for uint8_t
- test_json: link against libm for pow() used in json.c

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix test_ppbloom link: add libm for bloom's log()

On Linux, libbloom uses log() which requires explicit -lm linkage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix Linux build: link libm for test_buffer and test_crypto, fix warnings

Both test_buffer and test_crypto link libbloom.so which uses log() from
libm. Also fix unused variable warnings in test_json.c that would fail
with -Werror.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix Linux build: link libm for test_jconf (json.c uses pow)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* CI: add unit test and stress test steps to GitHub Actions

Split the Test step into separate "Unit tests" (ctest) and "Stress test"
(stress_test.py with 10MB transfer across all AEAD ciphers) steps for
better visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 17:06:47 +08:00