常见陷阱
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 中要加 |
被接受但被忽略的键
Section titled “被接受但被忽略的键”解析器会拒绝它不认识的键。下面这些键都是已知的,因此能通过解析,但它们所在的对象从不读取它们。--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 不做检查 |
下面各小节解释那些一行说不清的条目。
入站上的 address_family
Section titled “入站上的 address_family”[[inbound]] 有一个 address_family 键,解析器接受其中的任意字符串,包括不是有效策略的值。但没有任何入站读取它。这个键只在 [[outbound]] 上起作用,用来控制出站拨号时使用哪个地址族。请把它放在出站上:
[[outbound]]tag = "direct"protocol = "freedom"address_family = "prefer_ipv4"可接受的取值见出站。
入站上的客户端 TLS 键
Section titled “入站上的客户端 TLS 键”入站是 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 键
Section titled “出站上的服务端 TLS 键”出站是 TLS 客户端,从不出示证书。[outbound.stream.tls] 中的 cert_file 和 key_file 会被接受并忽略,相应文件也不会被读取。出站读取的是 server_name、allow_insecure 和 ca_file(见传输层)。
所选 network 不使用的子表
Section titled “所选 network 不使用的子表”每种 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"[[outbound]]tag = "hy2"protocol = "hysteria2"server = "proxy.example.com"port = 443
[outbound.settings]password = "replace-with-a-long-random-password"server_name = "cdn.example.com"ca_file = "/etc/etemenanki/ca.pem"在这些协议上,请完全省略 [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 键
Section titled “后端不使用的 DNS 键”每种 [dns] 后端只读取自己的键,其余一律忽略:
| 键 | system |
udp |
tls |
https |
|---|---|---|---|---|
server |
忽略 | 必填 | 必填 | 必填 |
server_name |
忽略 | 忽略 | 必填 | 忽略 |
url |
忽略 | 忽略 | 忽略 | 必填 |
ca_file |
读取但不使用 | 读取但不使用 | 使用 | 使用 |
ca_file 是个特例:只要设置了它,无论使用哪种后端,etemenanki-app 都会在构建配置时读取该文件。即使后端是 system、这个文件根本用不到,路径不存在也会导致构建失败。使用 https 时,TLS 名称取自 url 中的主机,因此 server_name 不起作用。url 中写的端口同样被忽略:解析器始终拨号到 server。见 DNS。
auth = "none" 时的 SOCKS accounts
Section titled “auth = "none" 时的 SOCKS accounts”SOCKS 入站默认 auth = "none"。添加 accounts 并不会改变这一点:这些账号会被接受并忽略,任何能连上的人都可以使用这个代理。两者都要设置:
[inbound.settings]auth = "password"accounts = [{ user = "alice", pass = "replace-with-a-long-random-password" }]反过来的错误同样会被接受:auth = "password" 却没有 accounts,构建出的代理谁也登录不了。
HTTP 入站没有 auth 键。只要 accounts 非空,它就要求认证。
旧版 Shadowsocks 的 password 与 users
Section titled “旧版 Shadowsocks 的 password 与 users”旧版(AEAD)Shadowsocks 入站始终要求 password:省略它会报错 inbound ss-in: invalid settings: missing field `password` 。但只要 users 非空,就只接受各用户的密码,顶层 password 永远不会被使用,用它配置的客户端无法连接。请给每个客户端分配 users 中的某个密码。
Shadowsocks 2022 入站则不同:配置了 users 时,顶层 password 是服务端的身份密钥,协议要求必须提供。见 Shadowsocks。
规则中未知的 inbound_tag
Section titled “规则中未知的 inbound_tag”规则中指定的每个 outbound 都必须存在,否则构建会失败并报 route references unknown outbound tag。但规则中的 inbound_tag 值不做检查。tag 拼错后,这个匹配条件就什么也匹配不到,而且没有任何错误或警告提示:
[[inbound]]tag = "socks-in"# …
[[route.rule]]inbound_tag = ["sock-in"] # 没有这个入站;这条规则永远不会因它而匹配outbound = "block"规则看起来不起作用时,请把规则中的 tag 与每个 [[inbound]] 的 tag 逐一核对。
TCP 上的 TLS 写作 network = "tls"
Section titled “TCP 上的 TLS 写作 network = "tls"”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"[inbound.stream]network = "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 默认值不同
Section titled “各入站的 UDP 默认值不同”有三种入站提供了中继 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 = trueHysteria 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 和出站。
相对路径以工作目录为准
Section titled “相对路径以工作目录为准”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["绑定新的监听器"]
重载会断开所有连接
Section titled “重载会断开所有连接”重载不会给正在运行的代理打补丁。它会构建一个完整的新 generation(一代实例),取消旧的,再启动新的。取消旧 generation 会结束它承载的所有连接,包括那些设置没有任何变化的入站上的连接。由于检查的是文件字节,只改一行注释也足以触发这一过程。
两代实例交替之间,监听器会短暂关闭。如果新 generation 的某个监听器无法绑定,例如另一个进程恰好在这段间隙里占用了端口,错误会被记录下来,该入站会一直不可用,直到下一次重载成功。其他入站正常启动。
请把所有改动一次性保存,并选在断开连接影响最小的时候。分几步写入文件的编辑器也能正确处理:事件会先收集 200 ms,然后才执行重载。
[log].level 只读取一次
Section titled “[log].level 只读取一次”日志级别在进程启动时设置,重载不会改变它。重载日志中可能会出现 log changed,但正在生效的过滤器保持不变。要应用新的级别,请重启进程。
两个相关细节:
- 如果设置了
RUST_LOG环境变量且其值是可解析的过滤器,它会完全覆盖[log].level。 - 日志级别不做校验。不是级别名的值,例如
level = "verbose",会被当作同名模块的过滤器,结果屏蔽掉所有日志,包括错误日志。这时如果配置有误,--test会以状态码 1 退出,却不输出原因。请使用error、warn、info、debug或trace。
轮换证书需要修改配置文件
Section titled “轮换证书需要修改配置文件”证书、CA 文件和 geodata 文件都是在构建配置时读取的。在磁盘上替换这些文件,不会对正在运行的代理产生任何影响:目录变化触发的重载发现配置文件的字节没变,就此停止。
续期证书之后,请执行以下操作之一:
- 修改配置文件的字节,例如添加一行
# reload-stamp: 2026-09-24这样的注释,这会重新构建 generation 并读取新文件; - 重启进程。
两种方式都会断开已有连接。证书续期钩子可以完成其中任意一种。
同样的规则也会影响失败的重载。etemenanki-app 会记住上次尝试的文件字节,即使那次尝试失败了。如果重载因为某个引用的文件缺失而失败,创建该文件并不会触发重试;文件就位后,需要再修改一次配置文件。
第一个出站就是默认路由
Section titled “第一个出站就是默认路由”没有任何规则匹配的流量会发往 [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 |