Skip to content

Shadowsocks 协议

Xray-core 实现了两代 Shadowsocks:经典 AEAD 加密方式(原始 Shadowsocks)位于 proxy/shadowsocks/,以及现代 Shadowsocks 2022 协议(通过 sing-shadowsocks 库)位于 proxy/shadowsocks_2022/

概述

特性经典 SSShadowsocks 2022
入站是(单用户 + 多用户 + 中继)
出站
TCP
UDP
加密方式AES-128/256-GCM、ChaCha20-Poly1305、XChaCha20-Poly1305、无2022-blake3-aes-128/256-gcm、2022-blake3-chacha20-poly1305
多用户是(仅 AEAD)是(使用独立 PSK)
重放保护行为种子排空内置(sing 库)
密钥格式密码(MD5 派生)Base64 预共享密钥

经典 Shadowsocks

线路格式 — TCP 流

+---------+-------------------+-------------------+
| [IV]    | Encrypted Header  | Encrypted Payload |
| 0-32B   | (chunked AEAD)    | (chunked AEAD)    |
+---------+-------------------+-------------------+

头部(在第一个加密分块内):

+----------+----------+------+
| AddrType | Address  | Port |
| 1 byte   | variable | 2B   |
+----------+----------+------+

源码:proxy/shadowsocks/protocol.go:57-131

地址类型

字节值(低 4 位)类型
0x01IPv4(4 字节)
0x03域名(1 字节长度 + 字符串)
0x04IPv6(16 字节)

注意:地址解析器使用自定义类型解析器,会屏蔽高 4 位:b & 0x0F

源码:proxy/shadowsocks/protocol.go:24-31

线路格式 — UDP 数据包

每个 UDP 数据包独立加密:

+---------+----------+----------+------+---------+----------+
| IV      | AddrType | Address  | Port | Payload | Auth Tag |
| 0-32B   | 1B       | variable | 2B   | ...     | 16B      |
+---------+----------+----------+------+---------+----------+

源码:proxy/shadowsocks/protocol.go:207-228

加密实现

AEAD 加密方式

所有 AEAD 加密方式遵循相同的模式:

  1. IV:流前面附加的随机字节(大小因加密方式而异)
  2. 子密钥派生:以 IV 为盐的 HKDF-SHA1:HKDF(key, iv, "ss-subkey") -> subkey
  3. 认证分块:每个分块为 [encrypted_length(2B + 16B tag)] [encrypted_payload(N + 16B tag)]
  4. 随机数:自增计数器
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,
    }
}

源码:proxy/shadowsocks/config.go:138-147

加密方式密钥大小IV 大小AEAD
AES_128_GCM1616AES-128-GCM
AES_256_GCM3232AES-256-GCM
CHACHA20_POLY13053232ChaCha20-Poly1305
XCHACHA20_POLY13053232XChaCha20-Poly1305
NONE00无(明文)

源码:proxy/shadowsocks/config.go:62-93

密码到密钥的派生

经典 Shadowsocks 使用迭代 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
}

源码:proxy/shadowsocks/config.go:213-228

HKDF 子密钥派生

go
func hkdfSHA1(secret, salt, outKey []byte) {
    r := hkdf.New(sha1.New, secret, salt, []byte("ss-subkey"))
    io.ReadFull(r, outKey)
}

源码:proxy/shadowsocks/config.go:230-233

多用户支持

Validator 遍历所有已注册用户,尝试使用每个用户的密钥进行 AEAD 解密:

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 }
        }
    }
}

源码:proxy/shadowsocks/validator.go:112-154

限制:非 AEAD 加密方式(None)仅支持单用户,因为没有认证标签可供匹配。

源码:proxy/shadowsocks/validator.go:33-35

行为种子排空器

与 VMess 类似,Shadowsocks 使用确定性排空器在关闭无效连接前读取随机数量的数据,以防止探测:

go
hashkdf := hmac.New(sha256.New, []byte("SSBSKDF"))
hashkdf.Write(account.Key)
behaviorSeed = crc64.Update(behaviorSeed, crc64.MakeTable(crc64.ECMA), hashkdf.Sum(nil))

源码:proxy/shadowsocks/validator.go:39-41

Shadowsocks 2022

Shadowsocks 2022 是一次彻底重新设计,具有更好的安全特性。Xray-core 将协议实现委托给 sing-shadowsocks 库(github.com/sagernet/sing-shadowsocks)。

与经典版本的主要差异

  1. 预共享密钥替代密码——密钥以 base64 编码,且必须精确匹配加密方式的密钥大小
  2. 重放保护内置于协议中(基于时间戳)
  3. 独立的头部和载荷加密,使用不同的随机数
  4. 多用户使用服务器级 PSK + 每用户 PSK(EIH — 加密身份头)

支持的方法

可用的方法来自 shadowaead_2022.List

  • 2022-blake3-aes-128-gcm
  • 2022-blake3-aes-256-gcm
  • 2022-blake3-chacha20-poly1305

单用户入站

文件:proxy/shadowsocks_2022/inbound.go

go
service, err := shadowaead_2022.NewServiceWithPassword(config.Method, config.Key, 500, inbound, nil)

源码:proxy/shadowsocks_2022/inbound.go:55-58

该服务处理:

  • TCP:service.NewConnection(ctx, connection, metadata)
  • UDP:service.NewPacket(ctx, pc, packet, metadata)

多用户入站

文件:proxy/shadowsocks_2022/inbound_multi.go

使用 shadowaead_2022.NewMultiService[int],配合服务器 PSK(base64 编码)和每用户密码:

go
service, err := shadowaead_2022.NewMultiService[int](config.Method, psk, 500, inbound, nil)
service.UpdateUsersWithPasswords(indices, passwords)

源码:proxy/shadowsocks_2022/inbound_multi.go:76-86

用户识别通过 sing 库内部的 EIH(加密身份头)机制实现。

出站

文件: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)

源码:proxy/shadowsocks_2022/outbound.go:47-57proxy/shadowsocks_2022/outbound.go:98-155

UDP over TCP(UoT)

出站支持在启用 UdpOverTcp 时将 UDP 通过 TCP 隧道传输:

go
if config.UdpOverTcp {
    o.uotClient = &uot.Client{Version: uint8(config.UdpOverTcpVersion)}
}

源码:proxy/shadowsocks_2022/outbound.go:58-60

入站处理器(经典 SS 服务端)

文件:proxy/shadowsocks/server.go

服务端同时处理 TCP 和 UDP:

go
func (s *Server) Network() []net.Network {
    list := s.config.Network
    if len(list) == 0 {
        list = append(list, net.Network_TCP)
    }
    return list
}

源码:proxy/shadowsocks/server.go:81-87

TCP 流程:读取加密头部,通过 Validator.Get() 匹配用户,解密地址,分发。

UDP 流程:每个 UDP 数据包独立加密。服务端解码每个数据包,提取目标地址,并通过 udp.Dispatcher 分发。

出站处理器(经典 SS 客户端)

文件:proxy/shadowsocks/client.go

对于 TCP:

  1. 生成随机 IV,写入连接
  2. 通过 account.Cipher.NewEncryptionWriter() 创建加密写入器
  3. 写入 SOCKS5 风格的地址头
  4. 通过加密写入器流式传输载荷

对于 UDP:

  1. 每个出站数据包通过 EncodeUDPPacket() 独立加密
  2. 响应数据包通过 DecodeUDPPacket() 解码

源码:proxy/shadowsocks/client.go:48-195

实现说明

  1. 流式加密已弃用NoneCipher 类型存在是为了向后兼容,但不提供加密。流式加密(RC4、不带 Poly1305 的 ChaCha20)已被完全移除。

  2. IV 唯一性:虽然代码中有 ErrIVNotUnique 错误类型,但验证器中的 IV 检查已被注释掉(validator.go:148)。Xray 中的经典 Shadowsocks 协议依赖 AEAD 标签进行认证,而非 IV 跟踪。

  3. 地址类型掩码:Shadowsocks 地址解析器屏蔽地址类型字节的高 4 位(b & 0x0F),这允许在高位中嵌入额外标志。

  4. Cone NAT:经典版和 2022 版服务端都支持 UDP 的 "cone" 模式。启用后,来自同一客户端的后续数据包复用第一个目标地址进行分发,模拟 NAT 行为。

  5. 中继模式:Shadowsocks 2022 有一个中继入站(inbound_relay.go),允许多跳中继链,但这是一个更专业的用例。

  6. sing-bridge:Shadowsocks 2022 代码使用 singbridge 辅助函数在 sing 库的类型(如 M.Socksaddr)和 Xray-core 的内部类型(如 net.Destination)之间进行转换。

用于重新实现目的的技术分析。