Wire protocol
Each UDP datagram on the wire is:
[ salt (salt_len bytes) ] ++ [ AEAD ciphertext ++ tag (16 bytes) ]salt_len == key_lenof the cipher: 16 bytes foraes-128-gcm, 32 bytes foraes-256-gcmandchacha20-poly1305. A fresh random salt is generated for every datagram.- Subkey:
subkey = HKDF-SHA1(ikm = master_key, salt = salt, info = "ss-subkey", L = key_len). - Nonce: the all-zero 12-byte nonce for every UDP packet. This is safe because each datagram has a unique random salt and therefore a unique subkey, so the
(subkey, nonce)pair is never reused. - Master key: derived from the password string with shadowsocks'
EVP_BytesToKey(the OpenSSL legacy MD5-based KDF): repeatedly computed_0 = MD5(password),d_i = MD5(d_{i-1} ++ password), and concatenate untilkey_lenbytes are available. (Implemented in-tree; no external crate.) - Plaintext: the raw IP packet read from the TUN device. UDP datagram boundaries are the frame boundaries — there is no length prefix, no multiplexing, and no reassembly. One IP packet maps to exactly one datagram.
The scheme is the shadowsocks.org AEAD UDP scheme, matched byte-for-byte, with one deliberate deviation below.
Deviation from ss-proxy
Standard shadowsocks UDP relays prepend a SOCKS-style target address to the plaintext. ShadowVPN does not. This is a fixed point-to-point tunnel, not a SOCKS proxy: the plaintext is exactly the raw IP packet, with no address header. Everything else (salt, HKDF-SHA1 "ss-subkey" subkey, zero nonce, AEAD tag) matches the shadowsocks UDP AEAD scheme byte-for-byte. This deviation is also documented in src/crypto.rs and src/protocol.rs.
Keepalive
A ShadowVPN convention, not part of the ss spec.
The client periodically sends a tiny encrypted datagram — a 5-byte plaintext: a 0x00 marker followed by the client's 4-byte tunnel IP — so that stateful NAT/firewall mappings stay open and the server learns the client's current source address before any real traffic flows.
- In the default learning mode, the announced tunnel IP lets the server map (and re-map, after a NAT rebind) the client's UDP address from the keepalive alone.
- In
--natmode, the keepalive refreshes an existing lease, and the mapping itself is allocated by the first real packet.
The server drops any decrypted payload smaller than a 20-byte IPv4 header, so the keepalive never reaches the TUN write path (older 1-byte 0x00 keepalives are still accepted and treated as refresh-only).
The interval is 15 seconds by default (keepalive_secs / --keepalive-secs) — keep it below the path's UDP NAT timeout.
Optional carrier obfuscation
The salt ++ AEAD envelope can optionally be wrapped in a cosmetic carrier — a QUIC 1-RTT short-header framing or base64 encoding — before hitting the wire. This changes nothing above: the envelope is unwrapped before decryption. See carrier obfuscation.