Shadowsocks 协议
Xray-core 实现了两代 Shadowsocks:经典 AEAD 加密方式(原始 Shadowsocks)位于 proxy/shadowsocks/,以及现代 Shadowsocks 2022 协议(通过 sing-shadowsocks 库)位于 proxy/shadowsocks_2022/。
概述
| 特性 | 经典 SS | Shadowsocks 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 位) | 类型 |
|---|---|
0x01 | IPv4(4 字节) |
0x03 | 域名(1 字节长度 + 字符串) |
0x04 | IPv6(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 加密方式遵循相同的模式:
- IV:流前面附加的随机字节(大小因加密方式而异)
- 子密钥派生:以 IV 为盐的 HKDF-SHA1:
HKDF(key, iv, "ss-subkey") -> subkey - 认证分块:每个分块为
[encrypted_length(2B + 16B tag)] [encrypted_payload(N + 16B tag)] - 随机数:自增计数器
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_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 | 无(明文) |
源码:proxy/shadowsocks/config.go:62-93
密码到密钥的派生
经典 Shadowsocks 使用迭代 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
}源码:proxy/shadowsocks/config.go:213-228
HKDF 子密钥派生
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 解密:
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 使用确定性排空器在关闭无效连接前读取随机数量的数据,以防止探测:
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)。
与经典版本的主要差异
- 预共享密钥替代密码——密钥以 base64 编码,且必须精确匹配加密方式的密钥大小
- 重放保护内置于协议中(基于时间戳)
- 独立的头部和载荷加密,使用不同的随机数
- 多用户使用服务器级 PSK + 每用户 PSK(EIH — 加密身份头)
支持的方法
可用的方法来自 shadowaead_2022.List:
2022-blake3-aes-128-gcm2022-blake3-aes-256-gcm2022-blake3-chacha20-poly1305
单用户入站
文件:proxy/shadowsocks_2022/inbound.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 编码)和每用户密码:
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
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-57、proxy/shadowsocks_2022/outbound.go:98-155
UDP over TCP(UoT)
出站支持在启用 UdpOverTcp 时将 UDP 通过 TCP 隧道传输:
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:
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:
- 生成随机 IV,写入连接
- 通过
account.Cipher.NewEncryptionWriter()创建加密写入器 - 写入 SOCKS5 风格的地址头
- 通过加密写入器流式传输载荷
对于 UDP:
- 每个出站数据包通过
EncodeUDPPacket()独立加密 - 响应数据包通过
DecodeUDPPacket()解码
源码:proxy/shadowsocks/client.go:48-195
实现说明
流式加密已弃用:
NoneCipher类型存在是为了向后兼容,但不提供加密。流式加密(RC4、不带 Poly1305 的 ChaCha20)已被完全移除。IV 唯一性:虽然代码中有
ErrIVNotUnique错误类型,但验证器中的 IV 检查已被注释掉(validator.go:148)。Xray 中的经典 Shadowsocks 协议依赖 AEAD 标签进行认证,而非 IV 跟踪。地址类型掩码:Shadowsocks 地址解析器屏蔽地址类型字节的高 4 位(
b & 0x0F),这允许在高位中嵌入额外标志。Cone NAT:经典版和 2022 版服务端都支持 UDP 的 "cone" 模式。启用后,来自同一客户端的后续数据包复用第一个目标地址进行分发,模拟 NAT 行为。
中继模式:Shadowsocks 2022 有一个中继入站(
inbound_relay.go),允许多跳中继链,但这是一个更专业的用例。sing-bridge:Shadowsocks 2022 代码使用
singbridge辅助函数在sing库的类型(如M.Socksaddr)和 Xray-core 的内部类型(如net.Destination)之间进行转换。