跳转到内容

出站

出站决定入站接受的连接接下来去往哪里。它可以直接拨号目标(freedom),可以丢弃连接(blackhole),也可以作为 SOCKS、HTTP、Trojan、VLESS、VMess、Shadowsocks、Hysteria 2 或 WireGuard 的客户端,把连接交给另一台代理服务器。对每条 TCP 连接和每个 UDP 数据包,路由器都会选出一个出站,或者一个由多个出站组成的负载均衡器。

本页介绍所有出站的共同点:[[outbound]] 的字段、address_family 策略、哪些协议能承载 UDP,以及 UDP 如何路由。各协议自己的设置见对应的协议页面。

配置里至少要有一个出站。没有 [route] 段时,所有流都发往第一个出站:

config.toml
[[inbound]]
tag = "socks-in"
protocol = "socks"
listen = "127.0.0.1"
port = 1080
[[outbound]]
tag = "direct"
protocol = "freedom"

更典型的客户端会把大部分流量经代理服务器发出,一部分直连,另一部分拦截:

config.toml
[[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 = 443
address_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` 。

键类型必填默认值说明
tagstring是—出站的名称,路由规则、[route].default 和 [[balancer]].outbounds 都用它来引用出站。在所有出站之间必须唯一(否则报 duplicate outbound tag: …),负载均衡器也不能使用相同的 tag。没有设置 [route].default 时,文件里的第一个 [[outbound]] 就是默认路由。
protocolstring (enum)是—出站使用的协议:freedom(别名 direct)、blackhole(别名 block)、socks、http、trojan、vless、vmess、shadowsocks、hysteria2(别名 hysteria、hy2)或 wireguard。精确匹配,区分大小写;其他取值报 unknown protocol。
serverstring视情况—上游服务器的主机名或 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 的回退值。
portu16视情况—上游服务器的端口。与 server 在同样的协议下必填(否则报 missing port);其他协议只在作为负载均衡器成员时把它用作健康探测的目标。hysteria2 下是 UDP 端口,其他协议下是 TCP 端口。
streamtable否—通往上游的传输层: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_familystring (enum)否"auto"解析域名时使用哪个 IP 地址族:auto、ipv4_only、ipv6_only、prefer_ipv4 或 prefer_ipv6,另有若干别名。取值会去掉首尾空白、不区分大小写,- 视同 _。对代理类出站,它作用于 server 的解析;对 freedom,作用于目标地址;对 wireguard,作用于隧道内的目标地址。未知取值报 invalid address_family。
settingstable视情况{}协议专属设置,见各协议页面。trojan、vless、vmess、shadowsocks、hysteria2 和 wireguard 有必填键,因此必须写;socks 和 http 可选;freedom 和 blackhole 完全不读取。未知键会被拒绝,这里的错误形如 outbound <tag>: invalid settings: …。
取值 别名 作用 页面
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"。

六种基于 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 不解析域名,发现不了这个问题;连接会在运行时失败。

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

[outbound.settings] 存放只有某一种协议才认识的内容:密码、UUID、加密方式、WireGuard 密钥等。各协议页面列出了它的键。以下两点对所有协议都成立:

  • 未知键会被拒绝。例如在 SOCKS 出站里误写 username,会报 outbound x: invalid settings: unknown field `username`, expected `user` or `pass` 。
  • settings 表在文件解析完成之后才读取,所以它的错误信息带的是出站的 tag,而不是行号。

freedom 和 blackhole 没有任何设置,也从不读取这张表,因此其中的内容都不会被检查。

协议 需要 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 决定出站可以使用一个域名的哪些 IP 地址,以及按什么顺序尝试。它按出站单独设置,所以直连出站可以只用 IPv4,而代理出站可以优先使用 IPv6。

匹配之前,取值会先去掉首尾空白、转成小写,并把所有 - 换成 _。"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。

如果策略过滤掉了所有地址,连接会失败,错误信息会给出策略和目标,例如:

dial: no usable ipv4_only destination address for example.com:443

wireguard 出站使用 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 如何到达出站),解析时应用策略,之后发往该域名的所有数据包都使用第一个允许的地址。域名逐个查询,查询期间发往其他域名的数据包需要等待。无法解析、或解析结果中没有允许地址的域名,在该子链路存续期间,其数据包都会被丢弃。

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 个不同的出站和负载均衡器时才会出现这种情况。
  • 子链路无法打开时,数据包被丢弃。 关联继续工作,发往该出站的下一个数据包会再次尝试。子链路打开期间,关联的出向数据包会等待,直到打开成功或失败。
  • 结束的子链路会被替换。 如果子链路的连接关闭,例如上游代理断开了它,关联会移除这条子链路,发往该出站的下一个数据包会打开新的子链路。
  • 负载均衡器在子链路打开时选择成员。 之后关联一直使用这个成员,直到子链路结束。

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 必须唯一。 两个出站使用相同 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