mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-29 20:43:44 +00:00
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>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
# Outer-transport experiments
|
||||
|
||||
These Python prototypes extend the printable-salt investigation. They wrap the
|
||||
TCP ciphertext of unchanged Shadowsocks peers. They are research fixtures, not
|
||||
production plugins. Both endpoints need a matching relay; UDP is not wrapped.
|
||||
The original `--printable-salt` flag remains a separate, disabled-by-default,
|
||||
wire-compatible client option.
|
||||
|
||||
Base64 has been dropped from the active experiments. Earlier measurements are
|
||||
retained unchanged in the historical report linked below.
|
||||
|
||||
## Candidates and tradeoffs
|
||||
|
||||
| Mode | Transformation | Bulk expansion | Main observable property |
|
||||
|---|---|---:|---|
|
||||
| raw | Transparent relay control | 0% | Ordinary Shadowsocks ciphertext |
|
||||
| printable salt | Random 6–12-byte printable prefix, tested separately | 0% | First six bytes always printable |
|
||||
| Base85 | Base85 records with newline separators | About 25.01% | Wider printable alphabet |
|
||||
| lowpop85 | Base85 symbols mapped to 85 low-popcount byte values | About 25.01% | Restricted alphabet, mostly zero bits |
|
||||
| TLS | Verified TLS 1.3 with two-byte inner record lengths | Variable; handshake plus TLS records | Actual OpenSSL TLS handshake and record behavior |
|
||||
|
||||
The bulk expansion figures include delimiters/padding for 16 KiB input records.
|
||||
Small writes have proportionally more framing overhead. EOF is represented by
|
||||
an empty encoded record, allowing TCP half-close through the relays. Base85 records preserve every ciphertext byte. The low-popcount mapping is public and
|
||||
reversible; its symbols have at most three set bits, with `0xff` as a separator.
|
||||
It is a simple baseline, **not** the keyed bit-shuffling construction from the
|
||||
[modified-Shadowsocks implementation](https://gfw.report/blog/modified_shadowsocks/en/).
|
||||
|
||||
Base85 and lowpop85 have about `log2(85) = 6.41` bits per symbol. None destroys information or
|
||||
makes the underlying encryption weaker merely by encoding it. Conversely,
|
||||
lower byte entropy does not establish lower detectability: these encodings have
|
||||
recognizable alphabets. A capable classifier can decode them or learn their
|
||||
patterns. The low-popcount variant is particularly easy to distinguish from
|
||||
uniform bytes.
|
||||
|
||||
TLS uses a fresh, short-lived experiment certificate, verified by the client,
|
||||
with hostname `experiment.invalid`. It is genuine TLS, not a fabricated TLS
|
||||
prefix. It does not imitate a browser or carry HTTP, and the self-signed
|
||||
certificate and OpenSSL handshake can themselves be fingerprinted. No existing
|
||||
server certificate, key, or service is reused or modified.
|
||||
|
||||
## Local screening
|
||||
|
||||
```sh
|
||||
python3 tests/experiments/test_transport.py
|
||||
python3 tests/experiments/benchmark.py \
|
||||
--output build-artifacts/printable-salt/encoding-screen.json
|
||||
python3 tests/experiments/local_interop.py --bin build-printable-stock/bin \
|
||||
--output build-artifacts/printable-salt/encoding-local-interop.json
|
||||
```
|
||||
|
||||
The tests cover fragmented and coalesced records, malformed/truncated input,
|
||||
record size limits, half-close (including TLS), certificate verification,
|
||||
concurrency, and empty readiness connections.
|
||||
Local interoperability exercises all four relay modes around an independently
|
||||
built stock server/client pair, with both supported ciphers, concurrent payloads
|
||||
from one byte to one MiB, and a verified ten MiB bidirectional transfer.
|
||||
|
||||
The synthetic benchmark uses uniform random bytes as a ciphertext model. It
|
||||
measures byte histograms, popcount, expansion, and encode/decode speed, and checks
|
||||
2,000 independent 100-byte samples per mode against the four published
|
||||
[historical byte-statistic exemptions](https://gfw.report/publications/usenixsecurity23/en/).
|
||||
Those classifier results are simulated, not observed firewall decisions.
|
||||
These timings measure the Python prototypes, not the inherent speed of each format.
|
||||
|
||||
## Route screening
|
||||
|
||||
After verifying SSH trust, route, unused ports, available disk, and server health:
|
||||
|
||||
```sh
|
||||
python3 tests/experiments/route_screen.py \
|
||||
--host root@SERVER --host-key-alias VERIFIED_KNOWN_HOST \
|
||||
--address SERVER_IPV4 --stock-bin build-printable-stock/bin/ss-local \
|
||||
--ports 32220 32221 32222 32223 \
|
||||
--output build-artifacts/printable-salt/encoding-route-v2
|
||||
python3 tests/experiments/analyze.py \
|
||||
build-artifacts/printable-salt/encoding-route-v2
|
||||
```
|
||||
|
||||
The protocol is saved before outcomes are observed. Four two-minute phases rotate
|
||||
each mode over every port. Each mode attempts a verified one KiB transfer every
|
||||
five seconds, plus one ten MiB transfer per phase: 100 transfers per mode, 400 in
|
||||
total. Raw mode traverses the same relay architecture, providing a control for
|
||||
Python relay overhead. The backend is one unchanged stock server using
|
||||
`chacha20-ietf-poly1305`; all phases use fresh test credentials from the same run.
|
||||
|
||||
Server-local clients verify each entire relay/crypto path at phase start and
|
||||
every 30 seconds. Management or local-control failure aborts the run. Ports and
|
||||
listeners are isolated; no firewall rule or production service is changed.
|
||||
A remote watchdog bounds the supervisor lifetime; a 60-second control-command
|
||||
timeout also stops it if a broken SSH path never delivers EOF. Cleanup stops owned processes,
|
||||
removes private configs and certificate keys, and retrieves captures.
|
||||
|
||||
Captures include full experiment packets and the SSH control flow. They contain
|
||||
address metadata and are kept in a private output directory. The analyzer reports
|
||||
first-arriving payloads separately from payloads at the TCP stream start: packet
|
||||
reordering can make these differ. It also separates other source addresses from
|
||||
the test client and looks for exact replayed opening payloads. A matching replay
|
||||
from another address is evidence of replay traffic, not sufficient attribution
|
||||
to a particular actor. A lack of observed probes is not proof of resistance.
|
||||
|
||||
Performance figures are end-to-end echo timings with concurrent traffic sharing
|
||||
the same route and a small server. TLS includes a fresh connection handshake.
|
||||
Small-transfer completion time is the latency metric; local SOCKS handshake time
|
||||
is not remote RTT. Bulk throughput counts payload bytes once, though the echo
|
||||
moves them in both directions. An eight-minute all-success result establishes only
|
||||
short-run compatibility and measured prototype performance. It cannot establish
|
||||
long-term censorship resistance or a speed advantage from a small sample.
|
||||
|
||||
Recorded results: [2026-09-23 measurements and limitations](measurements/printable-salt-2026-09-23/README.md).
|
||||
Reference in New Issue
Block a user