跳转到内容

直连与黑洞

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:

config.toml
[[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 为路由到它的每个流从主机上新建一个连接或 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["流失败"]
  1. 域名通过 [dns] 中配置的解析器查询。没有 [dns] 段时,使用操作系统的解析器。IP 地址不做查询。
  2. address_family 去掉不允许的地址;对于 prefer_ipv4 或 prefer_ipv6,还会把首选地址族移到前面。另一个地址族仍保留在列表中作为后备。
  3. freedom 依次连接剩余的地址,保留第一个成功的连接。每次尝试最多可能耗时 10 秒,之后才会尝试下一个地址。
  4. 如果所有尝试都失败,流即失败,错误信息会列出每个地址及其各自的失败原因。

这些尝试是串行而非并行的。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 从不建立连接。它立即应答,使入站认为流已建立,然后将其丢弃。

流的出站一侧立即报告流结束,客户端写入的每个字节都会被接收并丢弃。因此入站会以成功回复完成握手,然后关闭朝向客户端的发送方向:

入站 客户端看到的结果
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 握手只会一直得不到回应,直到客户端放弃。同一关联上路由到其他出站的数据包不受影响。

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"]

以下错误会使 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 普通请求 连接关闭,没有响应

见 SOCKS 和 HTTP。