直连与黑洞
etemenanki-app 有两个不与其他代理服务器通信的出站。freedom(别名 direct)从运行 app 的机器直接连接目标。blackhole(别名 block)接收流量后将其丢弃。几乎每份配置都至少会用到其中一个:freedom 作为服务端的默认路由或客户端本地流量的出口,blackhole 作为拦截规则的目标。
本页说明两者分别如何处理 TCP 和 UDP、它们读取的少数几个键、freedom 如何解析域名和选择地址,以及流量被黑洞后客户端会看到什么。所有出站共用的字段见出站。
freedom |
blackhole |
|
|---|---|---|
| 别名 | direct |
block |
| TCP | 解析目标并连接 | 报告成功,随即结束流,并丢弃客户端写入的所有数据 |
| UDP | 从本地 socket 把每个数据包发往各自的目标 | 接收所有数据包,从不回应 |
| 读取的键 | tag、protocol、address_family、stream(仅校验) |
tag、protocol、stream(仅校验) |
server、port |
不用于转发流量 | 不用于转发流量 |
[outbound.settings] |
忽略,从不解析 | 忽略,从不解析 |
| 解析器 | [dns] |
无 |
| 作为负载均衡器成员 | 被拒绝,除非误设了 server 和 port |
被拒绝,除非误设了 server 和 port |
一个客户端:除拦截列表外,所有流量都直接发出。文件中的第一个出站就是默认路由,因此没有任何规则匹配的流都会落到 direct:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "block"protocol = "blackhole"
[[route.rule]]outbound = "block"domain_suffix = ["ads.example.com", "tracker.example.net"]对 ads.example.com 或其任意子域名的请求会交给 block,其余流量都直接拨号。启动前可以用 etemenanki-app --test -c config.toml 检查文件,它会输出 Configuration OK. 或遇到的第一个错误。
这两个协议都没有自己的设置项。对于 [[outbound]] 的通用键,它们各自的处理方式如下:
| 键 | freedom |
blackhole |
|---|---|---|
tag |
必填,在所有出站和负载均衡器之间唯一 | 必填,在所有出站和负载均衡器之间唯一 |
protocol |
"freedom" 或 "direct" |
"blackhole" 或 "block" |
server、port |
可以填写,但不用于转发流量:每个流都发往各自的目标。port 仍必须是合法端口号。见下文关于负载均衡器的说明。 |
可以填写,但不用于转发流量。负载均衡器的说明同样适用。 |
stream |
只允许普通 TCP:network 必须缺省、为 "" 或 "tcp",security 必须缺省、为 "" 或 "none",且均为小写。tls、ws 和 grpc 子表不会被使用,但其中的未知键仍会导致解析失败。 |
与 freedom 规则相同 |
address_family |
过滤目标地址并决定其顺序。默认 "auto"。未知取值会导致加载失败。 |
可以填写但会被忽略,连取值都不会校验。 |
settings |
从不解析。任意键,甚至非表类型的值,都会被接受且不产生任何效果。 | 从不解析 |
协议名区分大小写:protocol = "Direct" 会报错 outbound direct: unknown protocol "Direct"。[[outbound]] 表本身仍会拒绝未知键,因此即使在这两个协议上,拼错的 adress_family 也是一个带行号的解析错误。
address_family 可取 auto(默认)、ipv4_only、ipv6_only、prefer_ipv4 或 prefer_ipv6,会去除首尾空白且不区分大小写,- 视同 _,另外还接受 ipv4、v6 等少量简写。完整列表见出站。
负载均衡器会拒绝 freedom 或 blackhole 成员,因为它们没有可供探测的上游。这项检查只看 server 和 port,所以同时带有这两个键的上述任一协议出站会被接受为成员,并在该地址上接受探测,而它的流量仍然是直连或被丢弃。请不要填写 server 和 port;见负载均衡器。
freedom
Section titled “freedom”freedom 为路由到它的每个流从主机上新建一个连接或 socket。目标就是客户端请求的地址:一个域名,或原样使用的 IP 地址。
当嗅探从一个以 IP 寻址的流中识别出域名时,该域名只用于路由。freedom 仍然连接客户端发送的 IP 地址。
flowchart LR
F["发往 host:port 的流"] --> Q{"是域名?"}
Q -- "是" --> R["通过 [dns] 解析"]
Q -- "否,是 IP" --> A
R --> A["应用 address_family:先过滤,再排序"]
A --> C["依次连接每个地址,每次尝试 10 秒"]
C -- "首个成功" --> OK["中继"]
C -- "全部失败" --> E["流失败"]
- 域名通过
[dns]中配置的解析器查询。没有[dns]段时,使用操作系统的解析器。IP 地址不做查询。 address_family去掉不允许的地址;对于prefer_ipv4或prefer_ipv6,还会把首选地址族移到前面。另一个地址族仍保留在列表中作为后备。freedom依次连接剩余的地址,保留第一个成功的连接。每次尝试最多可能耗时 10 秒,之后才会尝试下一个地址。- 如果所有尝试都失败,流即失败,错误信息会列出每个地址及其各自的失败原因。
address_family = "auto" 时,按解析器返回的顺序尝试各地址:
[dns].backend |
顺序 |
|---|---|
system(默认) |
操作系统返回的顺序,遵循其地址选择规则 |
udp、tls、https |
先全部 A 记录,再全部 AAAA 记录 |
如果希望无论使用哪种后端都有固定的偏好,请选择 prefer_ipv6 或 prefer_ipv4。
这些尝试是串行而非并行的。freedom 不会让 IPv4 与 IPv6 竞速(Happy Eyeballs),因此一个静默丢包的地址会耗尽整整 10 秒,之后才尝试下一个。在某个地址族不可用的主机上,应把 address_family 设为可用的那一个,而不要依赖后备:
[[outbound]]tag = "direct"protocol = "freedom"address_family = "ipv4_only" # 这台主机没有可用的 IPv6 路由每个连接的源地址和网卡由操作系统根据路由表选择。etemenanki-app 没有为 freedom socket 设置源地址、出口网卡或防火墙标记(firewall mark)的选项。
与 etemenanki-app 中所有 UDP 一样,经过 freedom 的 UDP 按数据包逐个路由(见 UDP 如何到达出站)。某个 UDP 关联中第一个路由到 freedom 出站的数据包会打开本地 socket,该关联之后发往这个出站的所有数据包都共用这些 socket。
- 每个地址族一个 socket。
freedom绑定一个未连接的 IPv4 socket 和一个仅 IPv6 的 socket,各自使用临时端口。ipv4_only只打开 IPv4 socket,ipv6_only只打开 IPv6 socket,因此发往另一地址族 IP 的数据包会被丢弃。如果某个地址族无法绑定,例如主机禁用了 IPv6,freedom会继续使用另一个。如果两者都无法绑定,数据包会被丢弃,该关联会在debug级别记录udp fan-out: opening an outbound failed: udp: no usable local socket in any requested family。 - 每个数据包发往各自的目标,从对应地址族的 socket 发出。发往 IPv6 地址但没有打开 IPv6 socket 时,该数据包会被丢弃,关联本身继续工作。
- 域名在每个关联中只解析一次。 发往某个名称的第一个数据包会触发一次经由
[dns]的查询并应用address_family,freedom保留剩下的第一个地址。在该关联上,之后发往这个名称的所有数据包都发往这一个地址。UDP 不会回退到第二个地址。 - 无法解析的名称,或解析结果全部被
address_family过滤掉的名称,在该关联剩余的时间里,发往它的数据包都会被丢弃。每次丢弃都会在debug级别记录为freedom: dropping a datagram to an unresolvable …。 - 查询逐个进行。 在某个名称查询期间,该关联上外发的数据包都会等待。
- 回复返回给客户端时,带的是回复实际来源的 IP 地址和端口,而不是客户端发送时使用的名称。
blackhole
Section titled “blackhole”blackhole 从不建立连接。它立即应答,使入站认为流已建立,然后将其丢弃。
流的出站一侧立即报告流结束,客户端写入的每个字节都会被接收并丢弃。因此入站会以成功回复完成握手,然后关闭朝向客户端的发送方向:
| 入站 | 客户端看到的结果 |
|---|---|
SOCKS5 CONNECT |
回复 0x00(成功),随后流结束。curl 报告 Empty reply from server。 |
HTTP CONNECT |
HTTP/1.1 200 Connection established,随后流结束。TLS 客户端握手失败。 |
| HTTP 普通请求 | 连接关闭,没有响应。curl 报告 Empty reply from server。 |
| Trojan、VLESS、VMess、Shadowsocks、Hysteria 2 | 隧道建立,其中的流不带任何数据即结束。 |
当客户端关闭自己一侧,或中继空闲超时(两个方向均无数据达 300 秒)触发时,连接才完全关闭;见限制。持续写入的客户端会让连接一直保持打开,写入的所有数据都被丢弃。
路由到 blackhole 的每个数据报都会被接收并丢弃。不会发回任何内容,也不会有错误传给客户端,因此被拦截的 DNS 查询或 QUIC 握手只会一直得不到回应,直到客户端放弃。同一关联上路由到其他出站的数据包不受影响。
没有响应选项
Section titled “没有响应选项”Xray 的 blackhole 可以用一个预设的 403 响应回答 HTTP 请求,etemenanki-app 的不能:没有 response 设置,[outbound.settings.response] 表会被忽略。被拦截的普通 HTTP 请求永远得不到响应,连接只是被关闭。
把广告和跟踪域名发往 blackhole,其余流量直连。geosite 列表来自 geosite.dat 文件:
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "block"protocol = "blackhole"
[route]default = "direct"geosite = "/etc/etemenanki/geosite.dat"
[[route.rule]]outbound = "block"geosite = ["category-ads-all"]在代理服务器上,freedom 通常是默认路由:客户端请求的任何目标都由服务器拨号。把它放在第一个,或在 [route].default 中指定它:
[[outbound]]tag = "direct"protocol = "freedom"address_family = "prefer_ipv4"
[[outbound]]tag = "block"protocol = "blackhole"
[route]default = "direct"geoip = "/etc/etemenanki/geoip.dat"
# 不让客户端访问服务器自身所在的私有网络。[[route.rule]]outbound = "block"geoip = ["private"]丢弃发往 443 端口的 UDP 会让浏览器从 HTTP/3 回退到 TCP,在 TCP 上嗅探和域名规则都能生效:
[[route.rule]]outbound = "block"network = "udp"port = ["443"]请把这条规则放在任何可能把这类 UDP 发往别处的规则之前。规则按首个匹配生效。
以下错误会使 etemenanki-app --test -c config.toml 和正常启动失败。日志行以 configuration invalid: 开头。
| 消息 | 原因 | 解决方法 |
|---|---|---|
outbound <tag>: unknown protocol "Direct" |
协议名大小写错误或拼写错误 | 用小写写 freedom、direct、blackhole 或 block |
outbound <tag>: invalid address_family "either" |
freedom 上的 address_family 取值不在可接受列表中 |
使用 auto、ipv4_only、ipv6_only、prefer_ipv4 或 prefer_ipv6 |
outbound <tag>: protocol freedom does not support stream network "ws" |
[outbound.stream] 中的 network 不是 tcp |
删除 [outbound.stream] 块 |
outbound <tag>: protocol blackhole does not support stream security "tls" |
[outbound.stream] 中的 security 不是 none |
删除 [outbound.stream] 块 |
balancer <tag>: outbound <tag> has no upstream a TCP health probe can reach, so it cannot be balanced |
未设置 server 和 port 的 freedom 或 blackhole 被列在 [[balancer]].outbounds 中 |
改用规则而非负载均衡器路由到它们;见负载均衡器 |
错误信息使用规范协议名,因此写成 protocol = "block" 的出站会被报告为 blackhole。
以下错误只在有流量时出现,记录在 debug 日志中([log].level = "debug")。携带原因的日志行取决于入站:SOCKS 入站记录 socks connection from Some(<ip>) ended: <reason>,HTTP 入站记录 http: connect failed: <reason>,Trojan、VLESS、VMess 和 Shadowsocks 入站记录 <protocol>: outbound gone: <reason>。原因包括:
| 原因 | 说明 |
|---|---|
failed to connect to any address (192.0.2.10:443: Connection refused (os error 111)) |
每个解析出的地址都拒绝连接或超时。每个地址都会连同各自的错误列出。 |
connect to 192.0.2.10:443 timed out |
某次尝试达到 10 秒上限时,出现在上面的列表中 |
dial: no usable ipv6_only destination address for example.com:443 |
address_family 过滤掉了目标的所有地址 |
failed to lookup address information: Name or service not known |
系统解析器无法解析该名称。文本来自操作系统,因平台而异。 |
dns: server returned rcode 3、dns: query timed out、dns: example.com did not resolve |
udp、tls 或 https 解析器无法解析该名称:名称不存在、在查询超时内没有收到应答,或者没有 A 或 AAAA 记录 |
freedom: dropping a datagram to an unresolvable … |
在该关联上,一个 UDP 数据包发往了未能解析的名称 |
freedom 拨号失败时客户端看到的结果取决于入站:
| 入站 | 客户端看到的结果 |
|---|---|
SOCKS5 CONNECT |
所有连接尝试都失败(包括超时)时回复 0x05(连接被拒绝),其他失败(例如名称无法解析)回复 0x04(主机不可达) |
开启嗅探时对 IP 地址的 SOCKS5 或 HTTP CONNECT |
成功回复在拨号前就已发出,因此客户端只会看到连接关闭 |
其他情况下的 HTTP CONNECT |
HTTP/1.1 502 Bad Gateway |
| HTTP 普通请求 | 连接关闭,没有响应 |