跳转到内容

常见陷阱

etemenanki-app 对配置文件要求严格。未知的键、拼错的协议或未知的 security 值都会让 --test 和正式启动失败。但这种严格只管键叫什么、能取哪些值,并不管你把键放在的那个对象是否真的会读取它。此外还有少数行为是有意为之,却仍然容易让人踩坑。

本页汇总了这些情况。每一项都说明会发生什么、为什么,以及应该怎么做。如果配置通过了 --test,行为却与预期不符,或者你准备把 Xray 的配置迁移过来,请先读一读本页。

陷阱 现象 解决办法
被接受但被忽略的键 输出 Configuration OK.,但这个键不起作用 删掉这个键,或把它移到会被读取的位置
TCP 上的 TLS security = "tls" is not valid with network = "tcp" 写成 network = "tls"
ca_file 是追加信任 公共信任的证书仍然能通过验证 预期结果是系统根证书加上你的 CA
UDP 默认值 Hysteria 2 客户端无法使用 UDP 在 Hysteria 2 入站上设置 udp = true
相对路径 报错 No such file or directory (os error 2),却不说是哪个文件 使用绝对路径
重载会断开连接 每次修改后所有客户端都要重连 规划好修改时机,把多处改动合并到一次保存
重载时的 [log].level 新的日志级别不生效 重启进程
证书轮换 仍在提供旧证书 修改配置文件,或重启进程
默认路由 未匹配的流量去了意料之外的出站 设置 [route].default
规则的匹配条件 一条规则匹配到的流量远超预期 每条规则只写一种条件,最具体的放在最前
IPv6 写法 --test 通过,但每次拨号都失败 server、endpoint 和 listen 中不加方括号;[dns].server 中要加

解析器会拒绝它不认识的键。下面这些键都是已知的,因此能通过解析,但它们所在的对象从不读取它们。--test 会输出 Configuration OK.,而这些值不产生任何效果。

键 在哪里被忽略 原因
address_family 所有 [[inbound]] 只有出站才需要选择目标地址
stream.tls.server_name、allow_insecure、ca_file 入站 它们用于配置 TLS 客户端
stream.grpc.authority 入站 服务端不检查客户端发送的 authority
stream.tls.cert_file、key_file 出站 不支持客户端证书
[stream.ws]、[stream.grpc]、[stream.tls] network 用不到它们的任何对象 只读取所选 network 对应的表
任何 [stream.*] 子表 没有传输层的协议 这些协议只检查 network 和 security
settings、server、port freedom、blackhole 出站 它们拨号到流自身的目标地址,或者根本不拨号
address_family blackhole 出站 连校验都不做
server、port wireguard 出站 对端地址由 settings.endpoint 指定
[dns] 中的键 用不到它们的后端 每种后端只读取自己的键
accounts auth = "none" 的 SOCKS 入站 只有 auth = "password" 才会读取
password 配置了 users 的旧版 Shadowsocks 入站 使用每个用户各自的密码
不存在于任何入站的 inbound_tag [[route.rule]] 规则中的 tag 不做检查

下面各小节解释那些一行说不清的条目。

[[inbound]] 有一个 address_family 键,解析器接受其中的任意字符串,包括不是有效策略的值。但没有任何入站读取它。这个键只在 [[outbound]] 上起作用,用来控制出站拨号时使用哪个地址族。请把它放在出站上:

[[outbound]]
tag = "direct"
protocol = "freedom"
address_family = "prefer_ipv4"

可接受的取值见出站。

入站是 TLS 服务端。它只从 [inbound.stream.tls] 中读取 cert_file 和 key_file。server_name、allow_insecure 和 ca_file 都是客户端设置:写在入站上会被接受并忽略,其中的 ca_file 甚至不会被打开,所以即使指向一个不存在的文件,也能通过 --test。服务端不验证客户端证书。

[inbound.stream.grpc] authority 被忽略也是同样的原因。入站的 [inbound.stream.ws] host 则不同:设置之后,服务端会把它与请求的 Host 头比较,对请求其他主机的请求返回 404。

出站是 TLS 客户端,从不出示证书。[outbound.stream.tls] 中的 cert_file 和 key_file 会被接受并忽略,相应文件也不会被读取。出站读取的是 server_name、allow_insecure 和 ca_file(见传输层)。

每种 network 只读取自己的子表:

network 读取
tcp(默认) 无
tls [stream.tls]
ws [stream.ws];security = "tls" 时还读取 [stream.tls]
grpc [stream.grpc];security = "tls" 时还读取 [stream.tls]

network = "grpc" 下的 [stream.ws] 表不起任何作用。在入站上,WebSocket 或 gRPC stream 没有设置 security = "tls" 时,[stream.tls] 表同样不起作用。唯一的例外是出站:即使不启用 TLS,它仍会读取 tls.server_name,在未设置 ws.host 时用作 WebSocket Host 头的后备值,在未设置 grpc.authority 时用作 gRPC authority 的后备值。其他 TLS 键不会被读取。

没有传输层的协议上的 stream 子表

Section titled “没有传输层的协议上的 stream 子表”

有些协议从不构建传输层:

  • socks、shadowsocks、hysteria2 和 tun 入站;
  • freedom、blackhole、hysteria2 和 wireguard 出站;
  • listen 为 Unix socket 路径的 http、trojan、vless 或 vmess 入站。

对这些协议,etemenanki-app 只检查 stream.network 和 stream.security。network 不是 tcp,或 security 不是 none,都会被拒绝:

inbound socks-in: protocol socks does not support stream network "ws"
outbound direct: protocol freedom does not support stream security "tls"
inbound local-in: protocol vless over a unix socket does not support stream network "ws"

子表本身不做检查。这些协议上的 [stream.tls]、[stream.ws] 和 [stream.grpc] 都会被接受并忽略。最容易浪费时间的是 Hysteria 2,它的 TLS 设置位于 [outbound.settings] 中:

[[outbound]]
tag = "hy2"
protocol = "hysteria2"
server = "proxy.example.com"
port = 443
# 会被接受,但从不读取:证书仍然按系统根证书
# 和名称 proxy.example.com 进行验证。
[outbound.stream.tls]
server_name = "cdn.example.com"
ca_file = "/etc/etemenanki/ca.pem"
[outbound.settings]
password = "replace-with-a-long-random-password"

在这些协议上,请完全省略 [stream] 块。

freedom、blackhole 和 WireGuard 上的 server、port 与 settings

Section titled “freedom、blackhole 和 WireGuard 上的 server、port 与 settings”

freedom 拨号到每个流自身的目标地址,blackhole 则不拨号。两者都不读取 server、port 或 [outbound.settings]。它们的 settings 表完全不做检查,因此 domainStrategy 或 redirect 这类 Xray 键能通过 --test,但不起任何作用。blackhole 也不读取 address_family,所以这里写了无效值也不会报错;freedom 则会读取并校验它。

wireguard 出站从 settings.endpoint 获取对端地址,忽略 server 和 port。

只有一个地方会读取这三种协议上的 server 和 port:负载均衡器。负载均衡器的成员需要 server 和 port 来做 TCP 连接探测,而它只看这两个键是否存在,不看协议(唯一被它拒绝的是 Hysteria 2)。因此,一个多写了 server 和 port 的 freedom、blackhole 或 wireguard 出站会被接受为成员,并按该地址进行探测,而它的流量仍然是直连、被丢弃或走隧道。这样的探测结果对这个出站本身毫无意义。

每种 [dns] 后端只读取自己的键,其余一律忽略:

键 system udp tls https
server 忽略 必填 必填 必填
server_name 忽略 忽略 必填 忽略
url 忽略 忽略 忽略 必填
ca_file 读取但不使用 读取但不使用 使用 使用

ca_file 是个特例:只要设置了它,无论使用哪种后端,etemenanki-app 都会在构建配置时读取该文件。即使后端是 system、这个文件根本用不到,路径不存在也会导致构建失败。使用 https 时,TLS 名称取自 url 中的主机,因此 server_name 不起作用。url 中写的端口同样被忽略:解析器始终拨号到 server。见 DNS。

SOCKS 入站默认 auth = "none"。添加 accounts 并不会改变这一点:这些账号会被接受并忽略,任何能连上的人都可以使用这个代理。两者都要设置:

[inbound.settings]
auth = "password"
accounts = [{ user = "alice", pass = "replace-with-a-long-random-password" }]

反过来的错误同样会被接受:auth = "password" 却没有 accounts,构建出的代理谁也登录不了。

HTTP 入站没有 auth 键。只要 accounts 非空,它就要求认证。

旧版(AEAD)Shadowsocks 入站始终要求 password:省略它会报错 inbound ss-in: invalid settings: missing field `password` 。但只要 users 非空,就只接受各用户的密码,顶层 password 永远不会被使用,用它配置的客户端无法连接。请给每个客户端分配 users 中的某个密码。

Shadowsocks 2022 入站则不同:配置了 users 时,顶层 password 是服务端的身份密钥,协议要求必须提供。见 Shadowsocks。

规则中指定的每个 outbound 都必须存在,否则构建会失败并报 route references unknown outbound tag。但规则中的 inbound_tag 值不做检查。tag 拼错后,这个匹配条件就什么也匹配不到,而且没有任何错误或警告提示:

[[inbound]]
tag = "socks-in"
# …
[[route.rule]]
inbound_tag = ["sock-in"] # 没有这个入站;这条规则永远不会因它而匹配
outbound = "block"

规则看起来不起作用时,请把规则中的 tag 与每个 [[inbound]] 的 tag 逐一核对。

Xray 把普通 TCP 上的 TLS 写成 network = "tcp" 加 security = "tls"。etemenanki-app 拒绝这种组合,包括没写 network 只写了 security = "tls" 的情况,因为 network 默认就是 tcp:

inbound trojan-in: security = "tls" is not valid with network = "tcp"; use network = "tls" for TLS over plain TCP (security = "tls" layers TLS under network = "ws" or "grpc")

拒绝是有意为之。如果在这里悄悄构建出明文监听器或拨号器,Trojan 的密码哈希或 VLESS 的 UUID 就会在发现错误之前以明文发送出去。

[inbound.stream]
network = "tcp"
security = "tls"

network 与 security 的组合方式:

network 未设置 security 或为 "none" security = "tls"
tcp 普通 TCP 拒绝
tls TCP 上的 TLS TCP 上的 TLS
ws 明文 WebSocket TLS 上的 WebSocket
grpc 明文 gRPC TLS 上的 gRPC

network = "tls" 始终是 TLS,即使旁边写了 security = "none"。除 tls、none 或空字符串以外的任何 security 值都会被拒绝,大小写不同也不行:security = "TLS" 会报错 unknown stream security "TLS" (expected "tls" or "none")。

ca_file 是在系统根证书之外追加信任

Section titled “ca_file 是在系统根证书之外追加信任”

每个用于验证服务端的 ca_file,都会把其中的证书添加到系统默认的信任库中,而不是替换信任库。这一点对该键出现的三个位置都成立:

  • [outbound.stream.tls] ca_file;
  • Hysteria 2 出站 [outbound.settings] 中的 ca_file;
  • [dns] ca_file。

设置了 ca_file 后,持有对应名称的公共信任证书的服务端仍然能通过验证,也没有任何键可以把信任限制为只认该文件。ca_file 的用途是让你自己 CA 签发的证书通过验证,而不是固定(pin)某个证书。

ca_file 不能与 allow_insecure = true 同时使用。构建会失败并报 outbound <tag>: tls.allow_insecure and tls.ca_file cannot both be set,Hysteria 2 则报 allow_insecure and ca_file cannot both be set。

有三种入站提供了中继 UDP 的开关,但它们的默认值并不一致。trojan、vless 和 vmess 入站无需开关即可承载 UDP。

入站 键 默认值
socks settings.udp true
tun settings.udp true
hysteria2 settings.udp false

没有设置 udp = true 的 Hysteria 2 服务端只接受 TCP 流,来自客户端的 UDP(DNS、QUIC、游戏、语音通话)到不了任何出站。请显式开启:

[inbound.settings]
cert_file = "/etc/etemenanki/cert.pem"
key_file = "/etc/etemenanki/key.pem"
password = "replace-with-a-long-random-password"
udp = true

Hysteria 2 入站上的 udp_idle_timeout 需要 udp = true。否则构建会失败并报 udp_idle_timeout is set but udp is not enabled,而不是忽略这个超时设置。

http 和 shadowsocks 入站完全不承载 UDP。在出站一侧,http 和 shadowsocks 同样不能承载 UDP:路由到它们的 UDP 流会失败,报错 http carries no datagrams(或 shadowsocks、shadowsocks-2022)。见 Hysteria 2 和出站。

etemenanki-app 按原样打开配置中指定的每个文件。相对路径基于进程当前的工作目录解析,而不是配置文件所在的目录。这适用于:

  • [stream.tls] 中的 cert_file、key_file 和 ca_file;
  • Hysteria 2 设置中的 cert_file、key_file 和 ca_file;
  • [dns] ca_file;
  • [route] geoip 和 geosite;
  • -c 路径本身,默认是工作目录下的 config.toml。

在 /etc/etemenanki 中能通过 --test 的配置,在服务管理器从 / 启动时可能失败。错误信息不会指出是哪个文件。启动时记录的是下面第一行,--test 输出的是第二行:

ERROR etemenanki_app: failed to start: No such file or directory (os error 2)
ERROR etemenanki_app: configuration invalid: No such file or directory (os error 2)

Unix socket 的 listen 是唯一不能写成相对路径的路径。只有以 / 开头的 listen 值才会被 etemenanki-app 当作 socket 路径。run/proxy.sock 这样的相对路径会被当作要配合 TCP 端口绑定的主机,因此 --test 会报错 inbound <tag>: port is required。请写完整路径,例如 listen = "/run/etemenanki/proxy.sock"。

请全部使用绝对路径。如果一定要用相对路径,就把服务的工作目录设置成对应目录(systemd unit 中的 WorkingDirectory=),并在同一目录下运行 --test。

etemenanki-app 会监视配置文件所在的目录,在文件内容变化时重载。下面的流程图标出了容易出乎意料的地方,完整说明见热重载。

flowchart TB
  A["配置目录发生变化"] --> B["读取配置文件"]
  B --> C{"字节与上次尝试相同?"}
  C -->|是| D["什么也不做"]
  C -->|否| E["解析并构建,读取所有引用的文件"]
  E -->|失败| F["记录错误,保留旧 generation"]
  E -->|成功| G["取消旧 generation:所有连接断开"]
  G --> H["绑定新的监听器"]

重载不会给正在运行的代理打补丁。它会构建一个完整的新 generation(一代实例),取消旧的,再启动新的。取消旧 generation 会结束它承载的所有连接,包括那些设置没有任何变化的入站上的连接。由于检查的是文件字节,只改一行注释也足以触发这一过程。

两代实例交替之间,监听器会短暂关闭。如果新 generation 的某个监听器无法绑定,例如另一个进程恰好在这段间隙里占用了端口,错误会被记录下来,该入站会一直不可用,直到下一次重载成功。其他入站正常启动。

请把所有改动一次性保存,并选在断开连接影响最小的时候。分几步写入文件的编辑器也能正确处理:事件会先收集 200 ms,然后才执行重载。

日志级别在进程启动时设置,重载不会改变它。重载日志中可能会出现 log changed,但正在生效的过滤器保持不变。要应用新的级别,请重启进程。

两个相关细节:

  • 如果设置了 RUST_LOG 环境变量且其值是可解析的过滤器,它会完全覆盖 [log].level。
  • 日志级别不做校验。不是级别名的值,例如 level = "verbose",会被当作同名模块的过滤器,结果屏蔽掉所有日志,包括错误日志。这时如果配置有误,--test 会以状态码 1 退出,却不输出原因。请使用 error、warn、info、debug 或 trace。

证书、CA 文件和 geodata 文件都是在构建配置时读取的。在磁盘上替换这些文件,不会对正在运行的代理产生任何影响:目录变化触发的重载发现配置文件的字节没变,就此停止。

续期证书之后,请执行以下操作之一:

  • 修改配置文件的字节,例如添加一行 # reload-stamp: 2026-09-24 这样的注释,这会重新构建 generation 并读取新文件;
  • 重启进程。

两种方式都会断开已有连接。证书续期钩子可以完成其中任意一种。

同样的规则也会影响失败的重载。etemenanki-app 会记住上次尝试的文件字节,即使那次尝试失败了。如果重载因为某个引用的文件缺失而失败,创建该文件并不会触发重试;文件就位后,需要再修改一次配置文件。

没有任何规则匹配的流量会发往 [route].default。未设置 default 时,发往文件中的第一个 [[outbound]]。调整出站的顺序,或在最前面新增一个出站,都会改变未匹配流量的去向。如果第一个出站是 blackhole,所有没被规则发往别处的流量都会被丢弃。完全没有 [[outbound]] 的配置会报错 config defines no outbounds。

请显式指定默认出站,这样就与文件中的顺序无关了:

[route]
default = "direct"

default 可以指定出站或负载均衡器。不存在的 tag 会报错 route references unknown outbound tag: <tag>。

规则中任一匹配条件命中即匹配

Section titled “规则中任一匹配条件命中即匹配”

一条 [[route.rule]] 可以列出多个匹配条件,但它们之间是 OR 关系,而不是 AND:只要其中一个命中,规则就生效。这与 Xray 相反,Xray 要求一条规则中的所有条件同时成立。

# 匹配所有发往 example.com 的流,同时也匹配所有发往
# 任意地址 443 端口的流,而不仅是 example.com 的 443 端口。
[[route.rule]]
domain_suffix = ["example.com"]
port = ["443"]
outbound = "proxy"

规则按文件顺序依次尝试,第一个匹配的生效,因此无法在一条规则中同时要求两个条件。请每条规则只写一种条件,并把最具体的规则放在最前面。

关于匹配还有两个细节:

  • 没有任何匹配条件的规则永远不会匹配。
  • cidr 和 geoip 只匹配目标地址是 IP 地址的流。以域名寻址的流不会为了路由而解析域名,所以 IP 规则永远匹配不到它。反过来,域名规则看到的是请求的域名,以及通过嗅探得到的域名。

全部匹配条件见路由。

server、endpoint 和 listen 中的 IPv6 地址

Section titled “server、endpoint 和 listen 中的 IPv6 地址”

在以下位置书写 IPv6 地址时不要加方括号:

  • 出站的 server;
  • WireGuard 出站的 settings.endpoint;
  • 入站的 listen。

对 etemenanki-app 来说,带方括号的地址不是 IP 地址,因此会被当作主机名,而没有任何解析器能解析 [2001:db8::1] 这样的名称。--test 既不解析名称也不绑定端口,所以会接受这个值。问题要到之后才出现:经该出站的每次拨号都失败,WireGuard 隧道始终建立不起来,或者启动失败并报错:

ERROR etemenanki_app: failed to start: inbound socks-in bind [::]:1080 failed: failed to lookup address information: Name or service not known

[dns] server 是例外。它按 socket 地址解析,因此其中的 IPv6 地址必须加方括号,其他写法都会被 --test 拒绝:

键 正确 错误
server "2001:db8::10" "[2001:db8::10]"
settings.endpoint(WireGuard) "2001:db8::10:51820" "[2001:db8::10]:51820"
listen "::" "[::]"
[dns] server "[2001:db8::53]:53" "2001:db8::53:53"(报错 dns: invalid server address: invalid socket address syntax)

WireGuard 的 endpoint 在最后一个冒号处拆分,所以不加方括号的写法并无歧义:最后一个 : 之前的部分是地址,之后的部分是端口。

本页提到的错误中,--test 能够报告的有:

错误信息 原因 解决办法
security = "tls" is not valid with network = "tcp"; … Xray 风格的 TCP 上的 TLS,或写了 security = "tls" 却没写 network network = "tls"
unknown stream security "…" (expected "tls" or "none") 拼写错误或大小写不对 security = "tls"
protocol <protocol> does not support stream network "…" / stream security "…" 在没有传输层的协议上写了 [stream] 块 删除 [stream] 块
No such file or directory (os error 2) 配置中引用的某个文件不存在,常见原因是相对路径 使用绝对路径
tls.allow_insecure and tls.ca_file cannot both be set 同一个出站上同时设置了两种验证选项 只保留一个
udp_idle_timeout is set but udp is not enabled Hysteria 2 设置了超时却没有 udp = true 添加 udp = true,或删除超时设置
invalid settings: missing field `password` 旧版 Shadowsocks 入站配置了 users 但没有 password 添加 password;它不会用于用户认证
dns: invalid server address: invalid socket address syntax [dns] server 中写了主机名,或 IPv6 地址没加方括号 写成 IP socket 地址,IPv6 加方括号
route references unknown outbound tag: <tag> [route].default 或某条规则指定的出站不存在 修正 tag