Shadowsocks
Shadowsocks 是一种加密代理协议,本身没有握手:客户端发出的最初几个字节是一个随机 salt,salt 之后的所有内容(包括目标地址)都经过加密。etemenanki-app 既可以作为服务端([[inbound]]),也可以作为客户端([[outbound]])使用它,支持两个协议族:
- 旧版 AEAD(SIP004),每个用户的密钥都由密码派生。用于不支持 2022 的旧客户端。
- Shadowsocks 2022(SIP022),使用随机的 base64 密钥、BLAKE3 密钥派生,并在每个请求中携带时间戳。新部署优先使用它。
method 的值决定使用哪个协议族,两个协议族都支持同一端口多用户。仅 TCP 入站和出站都不承载 UDP。
| 旧版 AEAD | Shadowsocks 2022 | |
|---|---|---|
method 取值 |
aes-128-gcm、aes-256-gcm、chacha20-poly1305、xchacha20-poly1305 及其别名 |
2022-blake3-aes-128-gcm、2022-blake3-aes-256-gcm、2022-blake3-chacha20-poly1305 |
| 方法名匹配 | 不区分大小写 | 精确匹配 |
password 的内容 |
任意字符串,密钥由它派生 | 长度等于该加密方式密钥长度(16 或 32 字节)的 base64 密钥 |
| 同一端口多用户 | 支持,任意方法均可 | 支持,仅限两种 AES-GCM 方法 |
| 服务端如何识别用户 | 依次尝试每个用户的密钥 | 读取加密的身份头 |
| 时钟需要一致 | 否 | 是,误差在 30 秒以内 |
流加密、none、plain |
不支持 | 不支持 |
下面搭建一个有两个用户的 Shadowsocks 2022 服务端,并为其中一个用户配置 etemenanki-app 客户端。
-
为服务端生成一个身份密钥,再为每个用户各生成一个密钥。
2022-blake3-aes-256-gcm使用 32 字节密钥:终端窗口 openssl rand -base64 32 # 服务端的身份 PSK(iPSK)openssl rand -base64 32 # alice 的密钥(uPSK)openssl rand -base64 32 # bob 的密钥(uPSK) -
把密钥填进服务端配置,然后检查配置:
终端窗口 etemenanki-app --test -c /etc/etemenanki/config.toml -
把密码
<iPSK>:<uPSK>(服务端身份密钥在前,用户自己的密钥在后),连同方法、地址和端口一起发给每个用户。
# A Shadowsocks 2022 server on port 8388 with two users and a direct exit.# Every key here is a placeholder. Generate each one with# `openssl rand -base64 32` (2022-blake3-aes-256-gcm takes 32-byte keys).
[log]level = "info"
[[inbound]]tag = "ss-in"protocol = "shadowsocks"listen = "0.0.0.0"port = 8388
[inbound.settings]method = "2022-blake3-aes-256-gcm"# The identity PSK (iPSK). Every client puts it first in its password.password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
# One entry per user. Each `password` is that user's own key (uPSK), so a# client's full password is "<iPSK>:<uPSK>".[[inbound.settings.users]]password = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE="email = "alice@example.com"
[[inbound.settings.users]]password = "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII="email = "bob@example.com"
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "block"protocol = "blackhole"
[route]default = "direct"
# Keep clients away from the server's own private networks.[[route.rule]]outbound = "block"cidr = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8", "fc00::/7", "::1/128"]在 127.0.0.1:1080 上提供本地 SOCKS5 代理,以 alice 的身份通过服务端转发 TCP,UDP 则直连:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "ss-out"protocol = "shadowsocks"server = "proxy.example.com"port = 8388
[outbound.settings]method = "2022-blake3-aes-256-gcm"# "<iPSK>:<uPSK>":先是服务端的身份密钥,然后是 alice 自己的密钥。password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE="
[[outbound]]tag = "direct"protocol = "freedom"
# Shadowsocks 只承载 TCP,所以把 UDP 发往能承载它的出站。[[route.rule]]network = "udp"outbound = "direct"其他 Shadowsocks 2022 客户端填写同样的值:方法 2022-blake3-aes-256-gcm,密码 <iPSK>:<uPSK>。请在这些客户端中关闭 UDP 中继,见仅 TCP。
入站和出站接受的全部 method 取值:
method |
别名 | 协议族 | 密钥来源 | 密钥和 salt 长度 | 多用户 |
|---|---|---|---|---|---|
aes-128-gcm |
aead_aes_128_gcm |
旧版 | 密码 | 16 字节 | 支持 |
aes-256-gcm |
aead_aes_256_gcm |
旧版 | 密码 | 32 字节 | 支持 |
chacha20-poly1305 |
chacha20-ietf-poly1305、aead_chacha20_poly1305 |
旧版 | 密码 | 32 字节 | 支持 |
xchacha20-poly1305 |
xchacha20-ietf-poly1305 |
旧版 | 密码 | 32 字节 | 支持 |
2022-blake3-aes-128-gcm |
无 | 2022 | base64 PSK | 16 字节 | 支持 |
2022-blake3-aes-256-gcm |
无 | 2022 | base64 PSK | 32 字节 | 支持 |
2022-blake3-chacha20-poly1305 |
无 | 2022 | base64 PSK | 32 字节 | 不支持 |
etemenanki-app 这样解析这个值:
- 以
2022-开头的值是 2022 方法,必须与三个名称之一完全一致。2022-BLAKE3-AES-128-GCM会报unknown shadowsocks-2022 method。 - 其他值都是旧版方法,匹配时不区分大小写,所以
AES-256-GCM和CHACHA20-IETF-POLY1305都可以用。 - 两种值都不会去掉首尾空白。带前导空格的
" aes-128-gcm"会报错。 - 不支持流加密(
aes-256-cfb、rc4-md5之类),也没有none或plain方法。它们会报unknown shadowsocks method。
旧版方法先用 OpenSSL 的 EVP_BytesToKey 从密码派生主密钥,再用 HKDF-SHA1 和随机 salt 派生每个连接的子密钥。Shadowsocks 2022 方法则用 BLAKE3 从 PSK 和随机 salt 派生每个连接的子密钥。
对 Shadowsocks 2022 来说,每个密钥(单个 PSK、iPSK 以及每个 uPSK)都是长度等于该加密方式密钥长度的随机字节,以带填充的标准 base64 书写:
method |
命令 | 输出长度 |
|---|---|---|
2022-blake3-aes-128-gcm |
openssl rand -base64 16 |
24 个字符,以 == 结尾 |
2022-blake3-aes-256-gcm、2022-blake3-chacha20-poly1305 |
openssl rand -base64 32 |
44 个字符,以 = 结尾 |
URL 安全字母表(- 和 _)以及不带填充的密钥都会被拒绝。密钥前后的空格或换行会被去掉。
旧版方法的密码可以是任意字符串。请使用足够长的随机字符串,例如 openssl rand -base64 24 的输出。
在 [[inbound]] 中写 protocol = "shadowsocks",并在 [inbound.settings] 下使用以下键。入站通用键(tag、listen、port、sniffing)见入站。
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
method | string (enum) | 是 | — | 加密方式。以 2022- 开头的值表示 Shadowsocks 2022,必须完全等于 2022-blake3-aes-128-gcm、2022-blake3-aes-256-gcm 或 2022-blake3-chacha20-poly1305。其他值都按旧版 AEAD 方法处理,不区分大小写:aes-128-gcm、aes-256-gcm、chacha20-poly1305、xchacha20-poly1305 或它们的别名。取值不会去掉首尾空白。未知的值会报 unknown shadowsocks method 或 unknown shadowsocks-2022 method。 |
password | string | 是 | — | 旧版方法:共享密码,密钥由它派生。users 非空时忽略此值,但仍然必须写这个键。Shadowsocks 2022:base64 编码的预共享密钥(标准字母表,带填充,首尾空白会被去掉),2022-blake3-aes-128-gcm 为 16 字节,其余方法为 32 字节。users 非空时,它是身份 PSK(iPSK),每个客户端都要把它放在密码的最前面。密钥过短会报 shadowsocks-2022: PSK too short;过长的密钥会改用其 SHA-256 摘要的前 16 或 32 字节。 |
users | array of tables | 否 | [] | 同一端口上的多个用户,每项写作 { password = "…", email = "…" }。也可以写成别名 clients,但不能和 users 同时出现。为空表示只用一个共享密码或 PSK。Shadowsocks 2022 的多用户需要 AES-GCM 方法;使用 2022-blake3-chacha20-poly1305 时配置会报 shadowsocks-2022: multi-user requires an aes-gcm method。 |
users[].password | string | 是 | — | 旧版方法:该用户的密码。Shadowsocks 2022:该用户的 base64 PSK(uPSK),长度规则与 password 相同。每个用户都要用不同的值。 |
users[].email | string | 否 | "" | 用户的标签。etemenanki-app 接受这个键,是为了让从 Xray 复制过来的配置可以原样使用,但并不使用它:没有路由规则按它匹配,日志也不会打印它。 |
设置表是严格的。未在此列出的键,例如 Xray 的 network、level 或按客户端设置的 method,都会报 invalid settings: unknown field。
入站只监听 TCP,不支持传输层。只有要求纯 TCP 的 [inbound.stream] 才会被接受:network 未设置、为空或为 "tcp",并且 security 未设置、为空或为 "none"。其他 network,或 security = "tls",会报 protocol shadowsocks does not support stream network "…" 或 … stream security "…"。如果服务端需要把 Shadowsocks 放在 TLS 或 WebSocket 之后,请改用支持传输层的协议,例如 Trojan 或 VLESS。
单个密码或 PSK
Section titled “单个密码或 PSK”不配置 users 时,整个端口共用一个密钥。
[inbound.settings]method = "2022-blake3-aes-128-gcm"password = "AAAAAAAAAAAAAAAAAAAAAA==" # openssl rand -base64 16客户端使用相同的方法,并把同一个密钥作为密码。
[inbound.settings]method = "chacha20-ietf-poly1305"password = "replace-with-a-long-random-password"客户端使用相同的方法和密码。
同一端口多用户
Section titled “同一端口多用户”在 users(或 clients)中列出用户。每个用户有自己的 password,以及可选的 email 标签。两个协议族识别用户的方式不同,顶层 password 的含义也不同。
[inbound.settings]method = "2022-blake3-aes-256-gcm"password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # iPSK
[[inbound.settings.users]]password = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=" # alice 的 uPSKemail = "alice@example.com"- 顶层
password成为身份 PSK(iPSK)。所有用户共用它,而单凭它已经无法接入。 - 每个用户的
password是该用户的 uPSK。 - 每个客户端把密码设为
<iPSK>:<uPSK>。 - 多用户需要
2022-blake3-aes-128-gcm或2022-blake3-aes-256-gcm。使用2022-blake3-chacha20-poly1305时配置会报shadowsocks-2022: multi-user requires an aes-gcm method,因为 SIP022 身份头是用 AES 加密的。
[inbound.settings]method = "aes-256-gcm"password = "unused-but-required"users = [ { password = "replace-with-a-long-random-password", email = "alice@example.com" }, { password = "replace-with-another-long-random-password", email = "bob@example.com" },]users中有条目后,顶层password会被忽略,但这个键仍然必须写。使用它的客户端会被拒绝。- 每个客户端使用自己那个用户的密码,没有密钥链。
- 任何旧版方法都可以。
旧版连接不携带任何标识用户的信息,因此服务端会按列表顺序用每个用户的密钥尝试解密第一个加密块,采用第一个能解密的。如果两个用户的密码相同,总是前一个胜出。每个新连接的开销随用户数增长。
2022 连接会标明自己的用户。客户端在 salt 之后放一个身份头:它是 uPSK 的哈希,用从 iPSK 派生的密钥加密。服务端解密后在表中查找这个哈希,所以用户数量不影响开销:
flowchart LR
C["客户端:password = iPSK:uPSK"] -->|"salt + 身份头 + 请求"| D["服务端用 iPSK 解密身份头"]
D --> L{"是某个已配置 uPSK 的哈希?"}
L -->|是| S["用该用户的 uPSK 生成会话密钥"]
L -->|否| X["关闭连接"]
S --> R["解密请求,打开流"]
请给每个用户不同的 uPSK。如果两个用户共用一个,服务端无法区分他们,会把两者都当作后一个条目。
用户保存在配置文件中。要添加或删除用户,请编辑文件并重载,见热重载。
在 [[outbound]] 中写 protocol = "shadowsocks",etemenanki-app 就成为 Shadowsocks 客户端。server 和 port 必填,指定 Shadowsocks 服务端;address_family 控制如何解析服务端名称。这些通用键见出站。协议相关的键写在 [outbound.settings] 下:
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
method | string (enum) | 是 | — | 加密方式,接受的值与入站相同:以 2022- 开头的值必须完全等于 2022-blake3-aes-128-gcm、2022-blake3-aes-256-gcm 或 2022-blake3-chacha20-poly1305;其他值按旧版 AEAD 方法或别名处理,不区分大小写。必须与服务端一致。未知的值会报 unknown shadowsocks method 或 unknown shadowsocks-2022 method。 |
password | string | 是 | — | 旧版方法:密码,原样使用(: 没有特殊含义)。Shadowsocks 2022:连接单 PSK 服务端时写一个 base64 PSK;连接多用户服务端时写成 iPSK:uPSK 链,最后一个是你自己的用户密钥,前面的都是身份密钥。每个密钥的长度规则与入站相同,过短会报 shadowsocks-2022: PSK too short。 |
与入站不同,出站没有 users:一个出站就是一个用户。users、clients 或其他任何键都会报 invalid settings: unknown field。
连接 2022 服务端
Section titled “连接 2022 服务端”password 的形式取决于服务端的配置方式:
| 服务端 | 客户端 password |
|---|---|
| 单个 PSK,无用户 | "<PSK>" |
| 多用户 | "<iPSK>:<uPSK>" |
出站按 : 拆分 password。最后一个密钥是用户密钥,用于会话。它之前的每个密钥都是身份密钥,出站会按顺序为每个身份密钥写一个身份头。出站也接受 iPSK1:iPSK2:uPSK 这样更长的链,但 etemenanki-app 服务端只读取一个身份头,所以连接它时请恰好给两个密钥。
只有 AES-GCM 方法支持身份头。出站不会拒绝与 2022-blake3-chacha20-poly1305 搭配的密钥链,但 etemenanki-app 服务端不接受这种连接。
旧版 password 原样使用,其中的 : 只是普通字符。
出站可以在任意客户端传输层中运行 Shadowsocks:TLS、WebSocket 或 gRPC,在 [outbound.stream] 中设置(见传输层)。这适用于把 Shadowsocks 放在这类传输层之后的服务端。etemenanki-app 入站不能充当这种服务端。
[[outbound]]tag = "ss-ws"protocol = "shadowsocks"server = "proxy.example.com"port = 443
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ss"
[outbound.settings]method = "aes-256-gcm"password = "replace-with-a-long-random-password"无论哪个协议族,入站和出站都不承载 UDP。
- 入站只绑定 TCP 监听器。通过 Shadowsocks 中继 UDP 的客户端得不到任何响应,所以请在客户端关闭 UDP 中继,或用其他方式发送 UDP。
- 路由器发往 Shadowsocks 出站的 UDP 流会失败,报
shadowsocks carries no datagrams(旧版)或shadowsocks-2022 carries no datagrams,记录在debug级别,其数据包被丢弃。请添加一条network = "udp"规则把 UDP 发往别处,并放在任何可能把 UDP 发往 Shadowsocks 出站的规则之前。上面的客户端示例就是这样做的;这在那里尤其重要,因为第一个出站同时也是所有未匹配规则的流量的默认出站。见路由。
时钟偏差(仅 2022)
Section titled “时钟偏差(仅 2022)”每个 Shadowsocks 2022 请求和响应都携带 Unix 时间戳。服务端会拒绝时间戳与自身时钟相差超过 30 秒的请求,etemenanki-app 客户端也按同样的条件拒绝响应。请保持两台机器的时钟同步,例如使用 NTP。旧版协议族没有时间戳。
当服务端无法解密连接、无法将其匹配到用户,或发现时间戳超出范围时,会直接关闭连接,不发送任何内容。原因记录在 debug 级别(设置 [log] level = "debug"),形如 shadowsocks-2022 connection from Some(198.51.100.7) ended: proxy core: shadowsocks-2022: bad timestamp。没有匹配到任何用户的旧版连接会记录 shadowsocks connection from … ended: proxy core: shadowsocks: no matching user;身份头指向未配置用户的 2022 连接会记录 … proxy core: shadowsocks-2022: unknown identity。
密钥错误不指明所属条目
Section titled “密钥错误不指明所属条目”关于 2022 密钥的错误(PSK too short、decode PSK、multi-user requires an aes-gcm method)不会指明来自哪个入站或出站。如果配置中有多个 Shadowsocks 条目,请逐一检查它们的密钥。
| 消息 | 原因 | 解决方法 |
|---|---|---|
inbound ss-in: unknown shadowsocks method "none" |
该方法不是 AEAD 加密方式。不支持 none、plain 和流加密。 |
使用加密方式中列出的方法。 |
inbound ss-in: unknown shadowsocks-2022 method "2022-BLAKE3-AES-128-GCM" |
2022 方法名区分大小写,且取值不会去掉首尾空白。 | 准确书写名称,全部小写,不带空格。 |
shadowsocks-2022: PSK too short (16 < 32) |
密钥解码后为 16 字节,但该方法需要 32 字节。 | 用 openssl rand -base64 32 生成密钥,或改用 2022-blake3-aes-128-gcm。 |
shadowsocks-2022: PSK too short (0 < 16) |
2022 的 password 为空,或密钥链中有空的部分,例如末尾多了一个 :。 |
填写每一个密钥。 |
decode PSK: Invalid symbol 45, offset 3. |
密钥不是标准 base64。这里的 45 是 -,来自 URL 安全编码的密钥或普通密码。 |
使用标准 base64,例如 openssl rand 的输出。 |
decode PSK: Invalid padding |
密钥丢失了末尾的 = 字符。 |
完整复制密钥,包括填充。 |
shadowsocks-2022: multi-user requires an aes-gcm method |
在 2022-blake3-chacha20-poly1305 下设置了 users。 |
改用 AES-GCM 的 2022 方法,或删除 users。 |
inbound ss-in: invalid settings: missing field `password` |
缺少 password,设置了 users 时也会出现这种情况。 |
添加顶层 password。旧版多用户模式下不会使用它的值。 |
inbound ss-in: invalid settings: duplicate field `users` |
同时写了 users 和 clients。 |
只保留其中一个。 |
inbound ss-in: protocol shadowsocks does not support stream network "ws" |
入站的 [inbound.stream] 设置了传输层。 |
删除它。只有出站支持传输层。 |
udp fan-out: opening an outbound failed: shadowsocks-2022 carries no datagrams(debug 日志) |
有 UDP 流被路由到了 Shadowsocks 出站。 | 用一条 network = "udp" 规则把 UDP 路由到其他出站。 |