跳转到内容

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 客户端。

  1. 为服务端生成一个身份密钥,再为每个用户各生成一个密钥。2022-blake3-aes-256-gcm 使用 32 字节密钥:

    终端窗口
    openssl rand -base64 32 # 服务端的身份 PSK(iPSK)
    openssl rand -base64 32 # alice 的密钥(uPSK)
    openssl rand -base64 32 # bob 的密钥(uPSK)
  2. 把密钥填进服务端配置,然后检查配置:

    终端窗口
    etemenanki-app --test -c /etc/etemenanki/config.toml
  3. 把密码 <iPSK>:<uPSK>(服务端身份密钥在前,用户自己的密钥在后),连同方法、地址和端口一起发给每个用户。

/etc/etemenanki/config.toml
# 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"]

其他 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)见入站。

键类型必填默认值说明
methodstring (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。
passwordstring是—旧版方法:共享密码,密钥由它派生。users 非空时忽略此值,但仍然必须写这个键。Shadowsocks 2022:base64 编码的预共享密钥(标准字母表,带填充,首尾空白会被去掉),2022-blake3-aes-128-gcm 为 16 字节,其余方法为 32 字节。users 非空时,它是身份 PSK(iPSK),每个客户端都要把它放在密码的最前面。密钥过短会报 shadowsocks-2022: PSK too short;过长的密钥会改用其 SHA-256 摘要的前 16 或 32 字节。
usersarray 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[].passwordstring是—旧版方法:该用户的密码。Shadowsocks 2022:该用户的 base64 PSK(uPSK),长度规则与 password 相同。每个用户都要用不同的值。
users[].emailstring否""用户的标签。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。

不配置 users 时,整个端口共用一个密钥。

[inbound.settings]
method = "2022-blake3-aes-128-gcm"
password = "AAAAAAAAAAAAAAAAAAAAAA==" # openssl rand -base64 16

客户端使用相同的方法,并把同一个密钥作为密码。

在 users(或 clients)中列出用户。每个用户有自己的 password,以及可选的 email 标签。两个协议族识别用户的方式不同,顶层 password 的含义也不同。

[inbound.settings]
method = "2022-blake3-aes-256-gcm"
password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # iPSK
[[inbound.settings.users]]
password = "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE=" # alice 的 uPSK
email = "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 加密的。

旧版连接不携带任何标识用户的信息,因此服务端会按列表顺序用每个用户的密钥尝试解密第一个加密块,采用第一个能解密的。如果两个用户的密码相同,总是前一个胜出。每个新连接的开销随用户数增长。

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] 下:

键类型必填默认值说明
methodstring (enum)是—加密方式,接受的值与入站相同:以 2022- 开头的值必须完全等于 2022-blake3-aes-128-gcm、2022-blake3-aes-256-gcm 或 2022-blake3-chacha20-poly1305;其他值按旧版 AEAD 方法或别名处理,不区分大小写。必须与服务端一致。未知的值会报 unknown shadowsocks method 或 unknown shadowsocks-2022 method。
passwordstring是—旧版方法:密码,原样使用(: 没有特殊含义)。Shadowsocks 2022:连接单 PSK 服务端时写一个 base64 PSK;连接多用户服务端时写成 iPSK:uPSK 链,最后一个是你自己的用户密钥,前面的都是身份密钥。每个密钥的长度规则与入站相同,过短会报 shadowsocks-2022: PSK too short。

与入站不同,出站没有 users:一个出站就是一个用户。users、clients 或其他任何键都会报 invalid settings: unknown field。

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 入站不能充当这种服务端。

基于 WebSocket 和 TLS 的 Shadowsocks
[[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 出站的规则之前。上面的客户端示例就是这样做的;这在那里尤其重要,因为第一个出站同时也是所有未匹配规则的流量的默认出站。见路由。

每个 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。

关于 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 路由到其他出站。