Skip to content

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

FeatureClassic SSShadowsocks 2022
InboundYesYes (single + multi-user + relay)
OutboundYesYes
TCPYesYes
UDPYesYes
CiphersAES-128/256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305, None2022-blake3-aes-128/256-gcm, 2022-blake3-chacha20-poly1305
Multi-userYes (AEAD only)Yes (with separate PSKs)
Replay protectionBehavior seed drainerBuilt-in (sing library)
Key formatPassword (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
0x01IPv4 (4 bytes)
0x03Domain (1-byte length + string)
0x04IPv6 (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:

  1. IV: Random bytes (cipher-specific size) prepended to the stream
  2. Sub-key derivation: HKDF-SHA1 with the IV as salt: HKDF(key, iv, "ss-subkey") -> subkey
  3. Authenticated chunks: Each chunk is [encrypted_length(2B + 16B tag)] [encrypted_payload(N + 16B tag)]
  4. Nonce: Auto-incrementing counter
go
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

CipherKey SizeIV SizeAEAD
AES_128_GCM1616AES-128-GCM
AES_256_GCM3232AES-256-GCM
CHACHA20_POLY13053232ChaCha20-Poly1305
XCHACHA20_POLY13053232XChaCha20-Poly1305
NONE00None (plaintext)

Source: proxy/shadowsocks/config.go:62-93

Password-to-Key Derivation

Classic Shadowsocks derives encryption keys from passwords using iterated MD5:

go
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

go
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:

go
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:

go
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

  1. Pre-shared keys instead of passwords -- keys are base64-encoded and must match the cipher's key size exactly
  2. Replay protection is built into the protocol (timestamp-based)
  3. Separate header and payload encryption with different nonces
  4. 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-gcm
  • 2022-blake3-aes-256-gcm
  • 2022-blake3-chacha20-poly1305

Single-User Inbound

File: proxy/shadowsocks_2022/inbound.go

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:

go
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

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:

go
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:

go
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:

  1. Generate random IV, write to connection
  2. Create encryption writer via account.Cipher.NewEncryptionWriter()
  3. Write SOCKS5-style address header
  4. Stream payload through the encryption writer

For UDP:

  1. Each outbound packet is independently encrypted with EncodeUDPPacket()
  2. Response packets decoded with DecodeUDPPacket()

Source: proxy/shadowsocks/client.go:48-195

Implementation Notes

  1. Stream cipher deprecation: The NoneCipher type exists for backward compatibility but provides no encryption. Stream ciphers (RC4, ChaCha20 without Poly1305) have been removed entirely.

  2. IV uniqueness: While the code has an ErrIVNotUnique error 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.

  3. 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.

  4. 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.

  5. 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.

  6. sing-bridge: The Shadowsocks 2022 code uses singbridge helpers to convert between the sing library's types (like M.Socksaddr) and Xray-core's internal types (like net.Destination).

Technical analysis for re-implementation purposes.