Shadowsocks Protocol
Xray-core implements two generations of Shadowsocks: the classic AEAD ciphers (original Shadowsocks) in proxy/shadowsocks/, and the modern Shadowsocks 2022 protocol (via the sing-shadowsocks library) in proxy/shadowsocks_2022/.
Overview
| Feature | Classic SS | Shadowsocks 2022 |
|---|---|---|
| Inbound | Yes | Yes (single + multi-user + relay) |
| Outbound | Yes | Yes |
| TCP | Yes | Yes |
| UDP | Yes | Yes |
| Ciphers | AES-128/256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305, None | 2022-blake3-aes-128/256-gcm, 2022-blake3-chacha20-poly1305 |
| Multi-user | Yes (AEAD only) | Yes (with separate PSKs) |
| Replay protection | Behavior seed drainer | Built-in (sing library) |
| Key format | Password (MD5 derivation) | Base64 pre-shared key |
Classic Shadowsocks
Wire Format -- TCP Stream
+---------+-------------------+-------------------+
| [IV] | Encrypted Header | Encrypted Payload |
| 0-32B | (chunked AEAD) | (chunked AEAD) |
+---------+-------------------+-------------------+Header (inside first encrypted chunk):
+----------+----------+------+
| AddrType | Address | Port |
| 1 byte | variable | 2B |
+----------+----------+------+Source: proxy/shadowsocks/protocol.go:57-131
Address types:
| Byte (low 4 bits) | Type |
|---|---|
0x01 | IPv4 (4 bytes) |
0x03 | Domain (1-byte length + string) |
0x04 | IPv6 (16 bytes) |
Note: The address parser uses a custom type parser that masks the high 4 bits: b & 0x0F
Source: proxy/shadowsocks/protocol.go:24-31
Wire Format -- UDP Packet
Each UDP packet is independently encrypted:
+---------+----------+----------+------+---------+----------+
| IV | AddrType | Address | Port | Payload | Auth Tag |
| 0-32B | 1B | variable | 2B | ... | 16B |
+---------+----------+----------+------+---------+----------+Source: proxy/shadowsocks/protocol.go:207-228
Cipher Implementations
AEAD Ciphers
All AEAD ciphers follow the same pattern:
- IV: Random bytes (cipher-specific size) prepended to the stream
- Sub-key derivation: HKDF-SHA1 with the IV as salt:
HKDF(key, iv, "ss-subkey") -> subkey - Authenticated chunks: Each chunk is
[encrypted_length(2B + 16B tag)] [encrypted_payload(N + 16B tag)] - Nonce: Auto-incrementing counter
func (c *AEADCipher) createAuthenticator(key, iv []byte) *crypto.AEADAuthenticator {
subkey := make([]byte, c.KeyBytes)
hkdfSHA1(key, iv, subkey)
aead := c.AEADAuthCreator(subkey)
nonce := crypto.GenerateAEADNonceWithSize(aead.NonceSize())
return &crypto.AEADAuthenticator{
AEAD: aead,
NonceGenerator: nonce,
}
}Source: proxy/shadowsocks/config.go:138-147
| Cipher | Key Size | IV Size | AEAD |
|---|---|---|---|
AES_128_GCM | 16 | 16 | AES-128-GCM |
AES_256_GCM | 32 | 32 | AES-256-GCM |
CHACHA20_POLY1305 | 32 | 32 | ChaCha20-Poly1305 |
XCHACHA20_POLY1305 | 32 | 32 | XChaCha20-Poly1305 |
NONE | 0 | 0 | None (plaintext) |
Source: proxy/shadowsocks/config.go:62-93
Password-to-Key Derivation
Classic Shadowsocks derives encryption keys from passwords using iterated MD5:
func passwordToCipherKey(password []byte, keySize int32) []byte {
key := make([]byte, 0, keySize)
md5Sum := md5.Sum(password)
key = append(key, md5Sum[:]...)
for int32(len(key)) < keySize {
md5Hash := md5.New()
md5Hash.Write(md5Sum[:])
md5Hash.Write(password)
md5Hash.Sum(md5Sum[:0])
key = append(key, md5Sum[:]...)
}
return key
}Source: proxy/shadowsocks/config.go:213-228
HKDF Sub-key Derivation
func hkdfSHA1(secret, salt, outKey []byte) {
r := hkdf.New(sha1.New, secret, salt, []byte("ss-subkey"))
io.ReadFull(r, outKey)
}Source: proxy/shadowsocks/config.go:230-233
Multi-User Support
The Validator iterates over all registered users, attempting AEAD decryption with each user's key:
func (v *Validator) Get(bs []byte, command RequestCommand) (...) {
for _, user := range v.users {
account := user.Account.(*MemoryAccount)
if account.Cipher.IsAEAD() {
aeadCipher := account.Cipher.(*AEADCipher)
iv := bs[:ivLen]
subkey := hkdfSHA1(account.Key, iv, ...)
aead := aeadCipher.AEADAuthCreator(subkey)
// Try to decrypt first chunk
ret, matchErr = aead.Open(data[:0], nonce, bs[ivLen:ivLen+18], nil)
if matchErr == nil { return user }
}
}
}Source: proxy/shadowsocks/validator.go:112-154
Limitation: Non-AEAD ciphers (None) only support a single user because there is no authentication tag to match against.
Source: proxy/shadowsocks/validator.go:33-35
Behavior Seed Drainer
Like VMess, Shadowsocks uses a deterministic drainer to read random amounts of data before closing invalid connections, preventing probing:
hashkdf := hmac.New(sha256.New, []byte("SSBSKDF"))
hashkdf.Write(account.Key)
behaviorSeed = crc64.Update(behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil))Source: proxy/shadowsocks/validator.go:39-41
Shadowsocks 2022
Shadowsocks 2022 is a complete redesign with better security properties. Xray-core delegates the protocol implementation to the sing-shadowsocks library (github.com/sagernet/sing-shadowsocks).
Key Differences from Classic
- Pre-shared keys instead of passwords -- keys are base64-encoded and must match the cipher's key size exactly
- Replay protection is built into the protocol (timestamp-based)
- Separate header and payload encryption with different nonces
- Multi-user uses a server-level PSK + per-user PSKs (EIH -- Encrypted Identity Header)
Supported Methods
The available methods come from shadowaead_2022.List:
2022-blake3-aes-128-gcm2022-blake3-aes-256-gcm2022-blake3-chacha20-poly1305
Single-User Inbound
File: proxy/shadowsocks_2022/inbound.go
service, err := shadowaead_2022.NewServiceWithPassword(config.Method, config.Key, 500, inbound, nil)Source: proxy/shadowsocks_2022/inbound.go:55-58
The service handles:
- TCP:
service.NewConnection(ctx, connection, metadata) - UDP:
service.NewPacket(ctx, pc, packet, metadata)
Multi-User Inbound
File: proxy/shadowsocks_2022/inbound_multi.go
Uses shadowaead_2022.NewMultiService[int] with a server PSK (base64-encoded) and per-user passwords:
service, err := shadowaead_2022.NewMultiService[int](config.Method, psk, 500, inbound, nil)
service.UpdateUsersWithPasswords(indices, passwords)Source: proxy/shadowsocks_2022/inbound_multi.go:76-86
User identification happens via the sing library's internal EIH (Encrypted Identity Header) mechanism.
Outbound
File: proxy/shadowsocks_2022/outbound.go
method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil)
// TCP
serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
// UDP
serverConn := o.method.DialPacketConn(connection)Source: proxy/shadowsocks_2022/outbound.go:47-57, proxy/shadowsocks_2022/outbound.go:98-155
UDP over TCP (UoT)
The outbound supports tunneling UDP over TCP when UdpOverTcp is enabled:
if config.UdpOverTcp {
o.uotClient = &uot.Client{Version: uint8(config.UdpOverTcpVersion)}
}Source: proxy/shadowsocks_2022/outbound.go:58-60
Inbound Handler (Classic SS Server)
File: proxy/shadowsocks/server.go
The server handles both TCP and UDP:
func (s *Server) Network() []net.Network {
list := s.config.Network
if len(list) == 0 {
list = append(list, net.Network_TCP)
}
return list
}Source: proxy/shadowsocks/server.go:81-87
TCP flow: Read the encrypted header, match user via Validator.Get(), decrypt address, dispatch.
UDP flow: Each UDP packet is independently encrypted. The server decodes each packet, extracts the destination, and dispatches via udp.Dispatcher.
Outbound Handler (Classic SS Client)
File: proxy/shadowsocks/client.go
For TCP:
- Generate random IV, write to connection
- Create encryption writer via
account.Cipher.NewEncryptionWriter() - Write SOCKS5-style address header
- Stream payload through the encryption writer
For UDP:
- Each outbound packet is independently encrypted with
EncodeUDPPacket() - Response packets decoded with
DecodeUDPPacket()
Source: proxy/shadowsocks/client.go:48-195
Implementation Notes
Stream cipher deprecation: The
NoneCiphertype exists for backward compatibility but provides no encryption. Stream ciphers (RC4, ChaCha20 without Poly1305) have been removed entirely.IV uniqueness: While the code has an
ErrIVNotUniqueerror type, the IV check is commented out in the validator (validator.go:148). The classic Shadowsocks protocol in Xray relies on the AEAD tag for authentication rather than IV tracking.Address type masking: The Shadowsocks address parser masks the upper 4 bits of the address type byte (
b & 0x0F), which allows embedding additional flags in the high bits.Cone NAT: Both classic and 2022 servers support "cone" mode for UDP. When enabled, subsequent packets from the same client reuse the first destination for dispatching, emulating NAT behavior.
Relay mode: Shadowsocks 2022 has a relay inbound (
inbound_relay.go) that allows multi-hop relay chains, though this is a more specialized use case.sing-bridge: The Shadowsocks 2022 code uses
singbridgehelpers to convert between thesinglibrary's types (likeM.Socksaddr) and Xray-core's internal types (likenet.Destination).