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:
Max Lv
2026-10-13 09:09:51 +08:00
committed by GitHub
co-authored by Claude
parent d52ddf2bce
commit 553cdc7945
36 changed files with 3389 additions and 1 deletions
+9
View File
@@ -592,6 +592,15 @@ aead_encrypt(buffer_t *plaintext, cipher_ctx_t *cipher_ctx, size_t capacity)
ciphertext->len = out_len;
if (!cipher_ctx->init) {
if (cipher_ctx->printable_salt) {
/* Only ss-local TCP opts in, after ctx_init. Keep UDP salt
* generation untouched and derive the key from the final salt. */
/* Vary the constrained span without adding a delimiter or marker. */
size_t prefix_len = 6 + randombytes_uniform(7);
for (size_t i = 0; i < prefix_len; i++) {
cipher_ctx->salt[i] = 0x20 + randombytes_uniform(95);
}
}
memcpy(ciphertext->data, cipher_ctx->salt, salt_len);
aead_cipher_ctx_set_key(cipher_ctx, 1);
cipher_ctx->init = 1;
+1
View File
@@ -61,6 +61,7 @@ enum {
GETOPT_VAL_REUSE_PORT,
GETOPT_VAL_FAST_OPEN,
GETOPT_VAL_NODELAY,
GETOPT_VAL_PRINTABLE_SALT,
GETOPT_VAL_ACL,
GETOPT_VAL_MTU,
GETOPT_VAL_MPTCP,
+1
View File
@@ -116,6 +116,7 @@ typedef struct {
typedef struct cipher_ctx {
uint32_t init;
uint64_t counter;
uint8_t printable_salt; /* opt-in client TCP salt prefix; never used by UDP */
cipher_evp_t *evp;
aes256gcm_ctx *aes256gcm_ctx;
cipher_t *cipher;
+5
View File
@@ -451,6 +451,11 @@ read_jconf(const char *file)
value, json_boolean,
"invalid config file: option 'no_delay' must be a boolean");
conf.no_delay = value->u.boolean;
} else if (strcmp(name, "printable_salt") == 0) {
check_json_value_type(
value, json_boolean,
"invalid config file: option 'printable_salt' must be a boolean");
conf.printable_salt = value->u.boolean;
} else if (strcmp(name, "tcp_tproxy") == 0) {
check_json_value_type(
value, json_boolean,
+1
View File
@@ -86,6 +86,7 @@ typedef struct {
int mptcp;
int ipv6_first;
int no_delay;
int printable_salt;
int tcp_tproxy;
char *workdir;
char *acl;
+17
View File
@@ -102,6 +102,7 @@ static int mode = TCP_ONLY;
static int ipv6first = 0;
int fast_open = 0;
static int no_delay = 0;
static int printable_salt = 0;
static int udp_fd = 0;
static int ret_val = 0;
@@ -1265,6 +1266,7 @@ new_server(int fd)
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);
server->e_ctx->printable_salt = printable_salt;
cipher_ctx_pair(server->e_ctx, server->d_ctx);
ss_io_init(&server->recv_ctx->io, server_recv_cb, fd, SS_READ);
@@ -1653,6 +1655,7 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
\snippet{doc} utils.c cli_long_password
\snippet{doc} utils.c cli_long_key
\snippet{doc} utils.c cli_long_server_url
\snippet{doc} utils.c cli_long_printable_salt
\snippet{doc} utils.c cli_long_help
[cli-options] */
static struct option long_options[] = {
@@ -1684,6 +1687,7 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
{ "tcp-outgoing-sndbuf", required_argument, NULL, GETOPT_VAL_TCP_OUTGOING_SNDBUF },
{ "tcp-outgoing-rcvbuf", required_argument, NULL, GETOPT_VAL_TCP_OUTGOING_RCVBUF },
{ "fast-open", no_argument, NULL, GETOPT_VAL_FAST_OPEN },
{ "printable-salt", no_argument, NULL, GETOPT_VAL_PRINTABLE_SALT },
{ "no-delay", no_argument, NULL, GETOPT_VAL_NODELAY },
{ "acl", required_argument, NULL, GETOPT_VAL_ACL },
{ "mtu", required_argument, NULL, GETOPT_VAL_MTU },
@@ -1740,6 +1744,9 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
if (mptcp)
LOGI("enable multipath TCP (%s)", mptcp > 0 ? "out-of-tree" : "upstream");
break;
case GETOPT_VAL_PRINTABLE_SALT:
printable_salt = 1;
break;
case GETOPT_VAL_NODELAY:
no_delay = 1;
LOGI("enable TCP no-delay");
@@ -1981,6 +1988,9 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
if (no_delay == 0) {
no_delay = conf->no_delay;
}
if (printable_salt == 0) {
printable_salt = conf->printable_salt;
}
#ifdef HAVE_SETRLIMIT
if (nofile == 0) {
nofile = conf->nofile;
@@ -2081,6 +2091,13 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
if (method == NULL) {
method = "chacha20-ietf-poly1305";
}
if (printable_salt && strcmp(method, "chacha20-ietf-poly1305") != 0
&& strcmp(method, "aes-256-gcm") != 0) {
FATAL("--printable-salt (printable_salt) requires chacha20-ietf-poly1305 or aes-256-gcm");
}
if (printable_salt) {
LOGI("enable experimental printable TCP salt");
}
if (timeout == NULL) {
timeout = "60";
+13
View File
@@ -427,6 +427,16 @@ Enable UDP relay and disable TCP relay.
Resolve hostname to IPv6 address first.
[cli_short_6] */
/* [cli_long_printable_salt]
\par `--printable-salt`
Experimental client TCP salt camouflage, disabled by default. Config key: `"printable_salt": true`
(ss-local only). Randomly choose 6-12 leading
salt bytes and sample each as printable ASCII (0x20-0x7e). Requires chacha20-ietf-poly1305 or
aes-256-gcm. Adds no framing bytes; leaves UDP and server responses unchanged.
The 32-byte salt retains at least 238 bits of entropy. Salt uniqueness
remains essential. This is not TLS impersonation or proven censorship resistance.
[cli_long_printable_salt] */
/* [cli_long_fast_open]
\par `--fast-open`
Enable TCP Fast Open where supported by the operating system.
@@ -678,6 +688,9 @@ usage(void)
cli_help_option("--fast-open", "Enable TCP Fast Open where supported.");
cli_help_option("--reuse-port", "Enable port reuse where supported.");
cli_help_option("--no-delay", "Enable TCP_NODELAY.");
#ifdef MODULE_LOCAL
cli_help_option("--printable-salt", "Experimental printable TCP salt (chacha20-ietf-poly1305/aes-256-gcm only).");
#endif
#if !defined(MODULE_MANAGER) && (!defined(MODULE_REMOTE) || defined(__linux__))
cli_help_option("--mptcp", "Enable Multipath TCP where supported.");
#endif