出站
出站决定入站接受的连接接下来去往哪里。它可以直接拨号目标(freedom),可以丢弃连接(blackhole),也可以作为 SOCKS、HTTP、Trojan、VLESS、VMess、Shadowsocks、Hysteria 2 或 WireGuard 的客户端,把连接交给另一台代理服务器。对每条 TCP 连接和每个 UDP 数据包,路由器都会选出一个出站,或者一个由多个出站组成的负载均衡器。
本页介绍所有出站的共同点:[[outbound]] 的字段、address_family 策略、哪些协议能承载 UDP,以及 UDP 如何路由。各协议自己的设置见对应的协议页面。
配置里至少要有一个出站。没有 [route] 段时,所有流都发往第一个出站:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
[[outbound]]tag = "direct"protocol = "freedom"更典型的客户端会把大部分流量经代理服务器发出,一部分直连,另一部分拦截:
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080
# 除非 [route].default 指定了别的出站,否则第一个出站就是默认出站。[[outbound]]tag = "proxy"protocol = "vless"server = "proxy.example.com"port = 443address_family = "prefer_ipv4"
[outbound.stream]network = "ws"security = "tls"
[outbound.stream.ws]path = "/ws"
[outbound.settings]id = "11111111-2222-3333-4444-555555555555"
[[outbound]]tag = "direct"protocol = "freedom"address_family = "ipv4_only"
[[outbound]]tag = "block"protocol = "blackhole"
[[route.rule]]outbound = "block"domain_suffix = ["ads.example.com"]
[[route.rule]]outbound = "direct"cidr = ["10.0.0.0/8", "192.168.0.0/16"]proxy 排在最前面,所以它是默认出站。VLESS 客户端通过 TLS 之上的 WebSocket 连接 proxy.example.com,并以 proxy.example.com 作为 TLS 服务器名,因为未设置 tls.server_name 时会回退到 server。
每个 [[outbound]] 表只接受下面这些键。出现其他键就是解析错误,错误信息会指出所在行,例如 unknown field `sever`, expected one of `tag`, `protocol`, `server`, `port`, `stream`, `address_family`, `settings` 。
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
tag | string | 是 | — | 出站的名称,路由规则、[route].default 和 [[balancer]].outbounds 都用它来引用出站。在所有出站之间必须唯一(否则报 duplicate outbound tag: …),负载均衡器也不能使用相同的 tag。没有设置 [route].default 时,文件里的第一个 [[outbound]] 就是默认路由。 |
protocol | string (enum) | 是 | — | 出站使用的协议:freedom(别名 direct)、blackhole(别名 block)、socks、http、trojan、vless、vmess、shadowsocks、hysteria2(别名 hysteria、hy2)或 wireguard。精确匹配,区分大小写;其他取值报 unknown protocol。 |
server | string | 视情况 | — | 上游服务器的主机名或 IP 地址,不带端口。socks、http、trojan、vless、vmess、shadowsocks 和 hysteria2 必填(否则报 missing server);freedom、blackhole 和 wireguard 不会用它转发流量,只在作为负载均衡器成员时用作健康探测的目标。IPv6 地址直接写(2001:db8::1),不加方括号。它同时是 TLS 服务器名(包括 hysteria2 的 server_name)、WebSocket Host 和 gRPC authority 的回退值。 |
port | u16 | 视情况 | — | 上游服务器的端口。与 server 在同样的协议下必填(否则报 missing port);其他协议只在作为负载均衡器成员时把它用作健康探测的目标。hysteria2 下是 UDP 端口,其他协议下是 TCP 端口。 |
stream | table | 否 | — | 通往上游的传输层:network(tcp、tls、ws、grpc)、security,以及 tls、ws、grpc 子表。socks、http、trojan、vless、vmess 和 shadowsocks 会使用它。freedom、blackhole、wireguard 和 hysteria2 拒绝 tcp 以外的 network 和 none 以外的 security,并忽略 tls、ws、grpc 子表。不写即为普通 TCP。 |
address_family | string (enum) | 否 | "auto" | 解析域名时使用哪个 IP 地址族:auto、ipv4_only、ipv6_only、prefer_ipv4 或 prefer_ipv6,另有若干别名。取值会去掉首尾空白、不区分大小写,- 视同 _。对代理类出站,它作用于 server 的解析;对 freedom,作用于目标地址;对 wireguard,作用于隧道内的目标地址。未知取值报 invalid address_family。 |
settings | table | 视情况 | {} | 协议专属设置,见各协议页面。trojan、vless、vmess、shadowsocks、hysteria2 和 wireguard 有必填键,因此必须写;socks 和 http 可选;freedom 和 blackhole 完全不读取。未知键会被拒绝,这里的错误形如 outbound <tag>: invalid settings: …。 |
protocol
Section titled “protocol”| 取值 | 别名 | 作用 | 页面 |
|---|---|---|---|
freedom |
direct |
自己拨号目标 | Freedom 与 blackhole |
blackhole |
block |
丢弃流 | Freedom 与 blackhole |
socks |
SOCKS5 客户端:CONNECT,UDP 使用 UDP ASSOCIATE |
SOCKS | |
http |
HTTP CONNECT 客户端 |
HTTP | |
trojan |
Trojan 客户端 | Trojan | |
vless |
VLESS 客户端 | VLESS | |
vmess |
VMess 客户端 | VMess | |
shadowsocks |
Shadowsocks 客户端,AEAD 或 2022(由 settings.method 决定) |
Shadowsocks | |
hysteria2 |
hysteria、hy2 |
基于 QUIC 的 Hysteria 2 客户端 | Hysteria 2 |
wireguard |
通往单个 peer 的用户态 WireGuard 隧道 | WireGuard |
协议名区分大小写:protocol = "Freedom" 会报 outbound direct: unknown protocol "Freedom"。
server 与 port
Section titled “server 与 port”六种基于 stream 的代理和 Hysteria 2 需要拨号上游服务器,所以这两个键都必须写。其余三种从不读取它们:
freedom按每个流自己的地址发送。blackhole哪里也不去。wireguard从settings.endpoint(host:port形式)获取 peer 地址。
对这三种出站,server 或 port 里写什么都会被接受,但永远不会用于转发流量。唯一仍然起作用的场合是负载均衡器,见下文的负载均衡器成员。
server 只能是主机名或 IP 地址,不能带其他内容。IPv6 地址直接写,不加方括号:
server = "2001:db8::10" # 正确# server = "[2001:db8::10]" # 能通过 --test,但之后每次拨号都会失败无法解析为 IP 地址的值会被当作主机名。因此带方括号的 [2001:db8::10] 是一个任何解析器都无法应答的“名字”。--test 不解析域名,发现不了这个问题;连接会在运行时失败。
stream
Section titled “stream”[outbound.stream] 选择代理客户端连接其服务器所用的传输层:普通 TCP、TLS、WebSocket 或 gRPC,后两者可以叠加 TLS,也可以不加。它的键与入站相同,详见传输层。
有四种协议用不到传输层:
freedom和blackhole不拨号代理服务器。wireguard自己发送 UDP 包。hysteria2使用自己的 QUIC 连接,它的 TLS 设置放在[outbound.settings]中。
对这四种协议,tcp 以外的 network 或 none 以外的 security 会被拒绝,而不是被悄悄忽略:
outbound wg: protocol wireguard does not support stream network "ws"outbound wg: protocol wireguard does not support stream security "tls"settings
Section titled “settings”[outbound.settings] 存放只有某一种协议才认识的内容:密码、UUID、加密方式、WireGuard 密钥等。各协议页面列出了它的键。以下两点对所有协议都成立:
- 未知键会被拒绝。例如在 SOCKS 出站里误写
username,会报outbound x: invalid settings: unknown field `username`, expected `user` or `pass`。 - settings 表在文件解析完成之后才读取,所以它的错误信息带的是出站的 tag,而不是行号。
freedom 和 blackhole 没有任何设置,也从不读取这张表,因此其中的内容都不会被检查。
各协议支持情况
Section titled “各协议支持情况”| 协议 | 需要 server 和 port |
[outbound.stream] |
承载 UDP | 负载均衡器成员 |
|---|---|---|---|---|
freedom |
否(忽略) | 拒绝 | 是 | 仅在写了 server 和 port 时 |
blackhole |
否(忽略) | 拒绝 | 吞掉 | 仅在写了 server 和 port 时 |
socks |
是 | 是 | 是,通过 UDP ASSOCIATE |
是 |
http |
是 | 是 | 否 | 是 |
trojan |
是 | 是 | 是 | 是 |
vless |
是 | 是 | 是,每个关联只有一个目标 | 是 |
vmess |
是 | 是 | 是,每个关联只有一个目标 | 是 |
shadowsocks |
是 | 是 | 否 | 是 |
hysteria2 |
是(UDP 端口) | 拒绝 | 是,前提是服务器允许 UDP | 否 |
wireguard |
否(settings.endpoint) |
拒绝 | 是 | 仅在写了 server 和 port 时 |
[outbound.stream]列的“拒绝” 表示:只要network或security要求普通 TCP 以外的东西,就会报错,见上文。- 承载 UDP 说明路由到该出站的 UDP 数据包会怎样。
blackhole接收并丢弃它。http和shadowsocks(AEAD 和 2022 方法都一样)在这个客户端里不支持数据报,路由给它们的数据包会被丢弃(见UDP 发往不支持数据报的出站)。vless和vmess在 UDP 关联的头部固定了目标,它们的 UDP 封装不携带逐包地址。同一关联的子链路上的所有数据包,都会发往打开该子链路的那个数据包的目标(见UDP 如何到达出站)。socks、trojan、hysteria2、wireguard和freedom则为每个数据包单独寻址。hysteria2只有在服务器允许时才中继 UDP。服务器拒绝时,程序会为每个出站以warn级别记录一次hysteria2: …; datagrams routed to this outbound are dropped,并丢弃这些数据包。
- 负载均衡器成员 由负载均衡器的健康探测决定,探测方式是 TCP 连接成员的
server和port。hysteria2总是被拒绝:它的服务器只监听 UDP,TCP 探测会让它永远处于 down 状态。freedom、blackhole和wireguard在没有server和port时被拒绝,而这正是通常情况。这些情况的错误信息都是balancer <tag>: outbound <tag> has no upstream a TCP health probe can reach, so it cannot be balanced。如果确实在这三种出站上写了server和port,它会被接受为成员,探测也会检查这个地址,尽管该出站从不向那里发送流量。参见负载均衡器。
address_family
Section titled “address_family”address_family 决定出站可以使用一个域名的哪些 IP 地址,以及按什么顺序尝试。它按出站单独设置,所以直连出站可以只用 IPv4,而代理出站可以优先使用 IPv6。
可接受的取值
Section titled “可接受的取值”匹配之前,取值会先去掉首尾空白、转成小写,并把所有 - 换成 _。"IPv4-Only"、" ipv4_only " 和 "ipv4only" 的含义完全相同。取值必须是 TOML 字符串:address_family = 4 是类型错误,address_family = "4" 则没问题。
| 策略 | 也可写作 | 使用的地址 | 顺序 |
|---|---|---|---|
auto(默认) |
""(空字符串) |
IPv4 和 IPv6 | 按解析器返回的顺序 |
ipv4_only |
ipv4、v4、4、ipv4only |
仅 IPv4 | 按返回顺序 |
ipv6_only |
ipv6、v6、6、ipv6only |
仅 IPv6 | 按返回顺序 |
prefer_ipv4 |
prefer_v4、ipv4_prefer、v4_prefer |
IPv4 和 IPv6 | IPv4 在前,IPv6 保留作为后备 |
prefer_ipv6 |
prefer_v6、ipv6_prefer、v6_prefer |
IPv4 和 IPv6 | IPv6 在前,IPv4 保留作为后备 |
其他任何取值都会让配置无法加载:
outbound direct: invalid address_family "either"auto 保持解析器给出的顺序。使用默认的主机解析器时,就是系统返回的顺序,遵循主机的地址选择规则;在 [dns] 中配置了 DNS 服务器时,IPv4 结果排在 IPv6 结果之前。只有当这个顺序对某个出站不合适时,才需要显式指定策略,例如主机的 IPv6 路由不通。
对 TCP 连接,出站按这个顺序依次尝试允许的地址,使用第一个连得上的地址。Hysteria 2 连接 server 的 QUIC 连接也是如此。所有查询都经过 [dns] 中配置的解析器(除非 [dns] 另有配置,否则就是主机解析器),唯一的例外见下文。
同一个键在不同协议下解析的是不同的名字:
| 协议 | address_family 作用于 |
|---|---|
freedom |
每个流的目标地址。对 UDP,它还限制打开哪些本地 socket:ipv4_only 只打开 IPv4 socket,ipv6_only 只打开 IPv6 socket。 |
socks、http、trojan、vless、vmess、shadowsocks |
server 名。目标地址按原样(包括域名)交给上游代理,由上游解析。 |
hysteria2 |
server 名。与上面的代理一样,目标地址不经解析直接交给服务器。 |
wireguard |
隧道内的目标地址。 域名先按策略解析,再限制到 settings.address 中有地址的那些地址族。peer 的 endpoint 不受影响(见下文)。 |
blackhole |
无。取值甚至不会被校验。 |
已经是 IP 地址的目标不会再解析,具体怎么处理取决于流量类型:
- 经
freedom或wireguard的 TCP:该地址会像解析出的地址一样被过滤。设置了address_family = "ipv4_only"的freedom出站会拒绝连接2001:db8::1。 - 经
freedom的 UDP:策略不过滤 IP 地址,但ipv4_only只打开 IPv4 socket,ipv6_only只打开 IPv6 socket。发往另一地址族的数据包会被丢弃,关联继续工作。 - 经
wireguard的 UDP:发往 IP 地址的数据包按原样送入隧道,既不检查策略,也不检查settings.address。
没有可用地址时
Section titled “没有可用地址时”如果策略过滤掉了所有地址,连接会失败,错误信息会给出策略和目标,例如:
dial: no usable ipv4_only destination address for example.com:443wireguard 出站使用 wireguard: 前缀而不是 dial:,并补充说明隧道地址允许哪些地址族,例如 wireguard: no usable ipv6_only destination address for example.com:443 (local address supports IPv4 only)。
这种失败发生在运行时、针对单个连接,而不是在加载时。协议处理程序以 debug 级别记录失败的连接,所以要看到这些信息,请设置 [log].level = "debug"。
对经 freedom 或 wireguard 的 UDP,域名在每条子链路上只解析一次(见UDP 如何到达出站),解析时应用策略,之后发往该域名的所有数据包都使用第一个允许的地址。域名逐个查询,查询期间发往其他域名的数据包需要等待。无法解析、或解析结果中没有允许地址的域名,在该子链路存续期间,其数据包都会被丢弃。
UDP 如何到达出站
Section titled “UDP 如何到达出站”TCP 连接只在建立时路由一次,之后一直使用路由器选中的出站。UDP 关联不能这样处理:每个数据报都带有自己的目标,一个关联(例如 SOCKS5 的 UDP ASSOCIATE)可能与许多对端通信。
因此 etemenanki-app 逐包路由 UDP。每个关联维护自己的一组子链路,每个出站最多一条:
flowchart LR
C["客户端 UDP 关联"] --> R{"逐包路由"}
R -- "发往 192.0.2.53:53" --> D["子链路:direct"]
R -- "发往 example.com:443" --> P["子链路:proxy"]
R -- "发往 ads.example.com:443" --> B["子链路:block"]
D --> R2["回复合并返回"]
P --> R2
B -.-> R2
R2 --> C
实际效果如下:
- 规则能看到每个数据包。 路由器按每个数据报自己的目标地址和端口进行匹配,同时结合关联的入站 tag、源地址以及
network = "udp"。UDP 数据包不附带嗅探到的域名,所以发往 IP 地址的数据包只能匹配 IP、端口等非域名规则。 - 一个关联可以同时使用多个出站。 DNS 可以直连,游戏流量则走代理,客户端无需做任何特殊处理。
- 同一出站的目标共用一条子链路。 路由到同一个出站的所有数据包使用同一条子链路,不论目标有多少个。发往某个出站的第一个数据包会打开这条子链路。
- VLESS 和 VMess 子链路保持第一个目标。 这两个协议在 UDP 关联建立时指定一个目标,之后不再逐包携带地址。路由器之后发往同一个
vless或vmess出站的每个数据包,不论自身地址是什么,都会发往这第一个目标,回复看起来也都来自它。如果一个客户端关联通过 VLESS 或 VMess 与多个对端通信,只有第一个对端能被正确访问。 - 每个关联最多 64 条子链路。 需要第 65 个出站时,关联会关闭最久没有发送过数据的子链路,该子链路上仍在途中的回复会丢失。只有一个关联用到超过 64 个不同的出站和负载均衡器时才会出现这种情况。
- 子链路无法打开时,数据包被丢弃。 关联继续工作,发往该出站的下一个数据包会再次尝试。子链路打开期间,关联的出向数据包会等待,直到打开成功或失败。
- 结束的子链路会被替换。 如果子链路的连接关闭,例如上游代理断开了它,关联会移除这条子链路,发往该出站的下一个数据包会打开新的子链路。
- 负载均衡器在子链路打开时选择成员。 之后关联一直使用这个成员,直到子链路结束。
UDP 发往不支持数据报的出站
Section titled “UDP 发往不支持数据报的出站”http 和 shadowsocks 出站只承载 TCP,但路由器并不知道这一点:匹配到指向它们的规则的 UDP 数据包会被路由过去,子链路打开失败,数据包被丢弃。程序会以 debug 级别记录:
udp fan-out: opening an outbound failed: http carries no datagrams对 Shadowsocks,信息中的名字是 shadowsocks 或 shadowsocks-2022。之后发往该出站的每个数据包都会以同样方式被丢弃。
要让 UDP 正常工作,就在捕获它的规则之前把它发往别处。规则按首条匹配生效,因此把 network = "udp" 规则放在最前面即可:
# UDP 直连,其余流量继续往下匹配,交给 HTTP 代理。[[route.rule]]outbound = "direct"network = "udp"
[[route.rule]]outbound = "corp-http"domain_suffix = ["example.com"]tag、默认路由与负载均衡器
Section titled “tag、默认路由与负载均衡器”- tag 必须唯一。 两个出站使用相同 tag 会报
duplicate outbound tag: <tag>。 - 第一个出站是默认出站。 没有匹配任何规则的流,在设置了
[route].default时发往它,否则发往文件中的第一个[[outbound]]。请把想作为兜底的出站放在最前面,或显式设置default。参见路由。 - 凡是能写出站 tag 的地方,都能写负载均衡器 tag,包括
[route].default和[[route.rule]].outbound。负载均衡器 tag 不能与出站 tag 重复(balancer tag <tag> collides with an outbound tag)。负载均衡器不能把另一个负载均衡器列为成员:该成员会被报告为balancer <tag> references unknown outbound tag: <tag>。 - 未知 tag 是错误。 规则或默认值引用了不存在的出站或负载均衡器时,会报
route references unknown outbound tag: <tag>。
[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "trojan-a"protocol = "trojan"server = "2001:db8::10"port = 443
[outbound.stream]network = "tls"
[outbound.stream.tls]server_name = "proxy.example.com"
[outbound.settings]password = "replace-with-a-long-random-password"
[[outbound]]tag = "ss-b"protocol = "shadowsocks"server = "203.0.113.20"port = 8388
[outbound.settings]method = "2022-blake3-aes-256-gcm"password = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" # 生成方法:openssl rand -base64 32
[[balancer]]tag = "pool"outbounds = ["trojan-a", "ss-b"]
[[route.rule]]outbound = "pool"domain_suffix = ["example.com"]这里 direct 排在最前面,所以仍是默认出站。发往 example.com 的流量在 trojan-a 健康探测成功时走 trojan-a,失败时走 ss-b,因为默认的负载均衡策略 failover 会按列表顺序选择第一个健康的成员。ss-b 是 Shadowsocks 出站,所以只要负载均衡器选中它,发往 example.com 的 UDP 就会被丢弃。如果这一点有影响,请在这条规则之前加一条 network = "udp" 规则。
下表中的错误会让 etemenanki-app --test -c config.toml 和正常启动都失败。使用 --test 时,日志行以 configuration invalid: 开头;启动时以 failed to start: 开头。
| 信息 | 原因 | 解决方法 |
|---|---|---|
config defines no outbounds |
文件中没有 [[outbound]] |
至少添加一个,哪怕只是 freedom |
duplicate outbound tag: <tag> |
两个出站使用了相同 tag | 重命名其中一个 |
outbound <tag>: unknown protocol "…" |
协议名拼错或用了大写 | 使用协议表中的小写名称 |
outbound <tag>: missing server / missing port |
代理出站没有指定上游 | 添加 server 和 port |
outbound <tag>: invalid address_family "…" |
取值不在可接受列表中 | 使用 auto、ipv4_only、ipv6_only、prefer_ipv4 或 prefer_ipv6 |
outbound <tag>: protocol <protocol> does not support stream network "…" / stream security "…" |
在 freedom、blackhole、wireguard 或 hysteria2 上写了 [outbound.stream] |
删除 [outbound.stream] 块 |
outbound <tag>: invalid settings: missing field … / unknown field … |
缺少必填设置,或键名拼错 | 在对应协议页面核对准确的键名 |
outbound <tag>: wireguard address_family ipv6_only needs an IPv6 address |
策略排除了所有隧道地址 | 添加该地址族的隧道地址,或修改策略 |
balancer <tag>: outbound <tag> has no upstream a TCP health probe can reach, so it cannot be balanced |
成员是 hysteria2,或是没有 server 和 port 的 freedom、blackhole、wireguard |
只对基于 stream 的代理做负载均衡 |
balancer <tag> references unknown outbound tag: <tag> |
成员不是出站,包括成员是另一个负载均衡器的情况 | 只列出出站 |
balancer tag <tag> collides with an outbound tag |
负载均衡器复用了某个出站或之前某个负载均衡器的 tag | 重命名负载均衡器 |
route references unknown outbound tag: <tag> |
规则或默认值引用了不存在的 tag | 修正 tag,或定义该出站 |
以下信息只在有流量时出现,记录在 debug 日志中:
| 信息 | 原因 |
|---|---|
dial: no usable <policy> destination address for <host>:<port> |
address_family 过滤掉了目标或服务器的所有地址(WireGuard 使用 wireguard: 而不是 dial:) |
udp fan-out: opening an outbound failed: … carries no datagrams |
UDP 被路由到了 http 或 shadowsocks |