mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-31 22:45:45 +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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user