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>
6.3 KiB
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.
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
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. 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:
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.