WireGuard 出口
本方案让少数网站使用某个 WireGuard 对端的出口地址,例如一个 VPN 服务,或你在另一个地区运行的 WireGuard 服务器;其余流量照常从本机发出。最终得到一个本地 SOCKS5 代理、一个作为默认出站的 freedom 出站,以及一个由路由规则为选定域名和 geosite 列表选中的 wireguard 出站。
当你的 WireGuard 服务给了你一份 wg-quick 文件(wg0.conf),而你想在 etemenanki-app 中使用它时,就可以按本页操作。WireGuard 协议页面是该出站的完整参考;本页则以一个贴近实际的配置为例,从文件一路走到验证过的出口。
要搭建的结构
Section titled “要搭建的结构”flowchart LR
C["浏览器或应用"] -->|"SOCKS5,127.0.0.1:1080"| I["socks-in"]
I --> R{"路由规则"}
R -->|"example.com、geosite netflix"| W["wg 出站"]
R -->|"没有规则匹配"| D["direct 出站"]
W -->|"加密的 UDP"| P["WireGuard 对端"]
P --> T1["网站,看到的是对端的出口地址"]
D --> T2["网站,看到的是本机地址"]
- SOCKS 入站监听回环地址,因此只有本机上的程序能使用它。
- 路由器从上到下依次尝试规则。匹配 WireGuard 规则的流进入隧道;其他流一律落到
[route].default。 - WireGuard 出站在进程内运行隧道,使用用户态 TCP/IP 协议栈。它不创建网络接口,不添加主机路由,也不需要 root 权限。它同时承载 TCP 和 UDP。
# A local SOCKS5 proxy that sends a few sites out through a WireGuard tunnel# and everything else directly. The WireGuard values are placeholders: copy# yours from the wg-quick file your WireGuard service gives you.
[[inbound]]tag = "socks-in"protocol = "socks"listen = "127.0.0.1"port = 1080# sniffing = true is the default: a flow addressed by IP is still matched by# its TLS SNI or HTTP Host, so the domain rules below apply to it too.
# Listed first, so it is also the implicit default. [route].default names it# explicitly anyway.[[outbound]]tag = "direct"protocol = "freedom"
[[outbound]]tag = "wg"protocol = "wireguard"# An IPv4-only tunnel: never try a destination's IPv6 addresses, even if an# IPv6 entry is added to `address` later, and make --test fail if the IPv4# address below is ever removed.address_family = "ipv4_only"
[outbound.settings]# [Interface] PrivateKey. Generate your own pair with:# wg genkey | tee private.key | wg pubkeyprivate_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="# [Peer] PublicKeypeer_public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="# [Peer] Endpointendpoint = "203.0.113.10:51820"# [Interface] Address, without the /32address = ["10.2.0.2"]# Conservative MTU for an IPv4-only tunnel; use your service's value if it# gives one.mtu = 1280# [Peer] PersistentKeepalivekeepalive = 25# Only if your service asks for it:# reserved = [0, 0, 0]
[route]default = "direct"# Read because the rule below uses a geosite matcher.geosite = "/etc/etemenanki/geosite.dat"
# Rules are tried from top to bottom; the first match wins. Inside one rule# the matchers are alternatives: any one of them is enough.[[route.rule]]outbound = "wg"domain_suffix = ["example.com"]geosite = ["netflix"]其中的密钥和 endpoint 都是解析器能接受的占位值,下一节会用你的 wg-quick 文件填入真实值。geosite 匹配条件需要下载 v2fly 域名列表(以 dlc.dat 发布,见分流),并保存到 [route].geosite 指定的路径。如果只按域名路由,就把 geosite 匹配条件和 geosite 路径一起删掉:--test 会读取该文件,文件不存在时会失败。
转换 wg-quick 文件
Section titled “转换 wg-quick 文件”服务方提供的文件大致如下。上面 TOML 中的注释标明了每个值来自 wg-quick 的哪一行。
[Interface]PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=Address = 10.2.0.2/32DNS = 192.0.2.53
[Peer]PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=AllowedIPs = 0.0.0.0/0Endpoint = 203.0.113.10:51820PersistentKeepalive = 25-
添加出站。 给它一个规则中会引用的 tag。不要添加
server、port或[outbound.stream]表:对端地址写在settings.endpoint中,而 WireGuard 自己通过 UDP 传输。WireGuard 出站会忽略server和port,并拒绝 stream 表。[[outbound]]tag = "wg"protocol = "wireguard"[outbound.settings] -
复制密钥。
PrivateKey对应private_key,[Peer]段中的PublicKey对应peer_public_key。如果文件中有PresharedKey行,把它复制到preshared_key;否则不写这个键。private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="peer_public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="原样粘贴这些值,包括末尾的
=。每个密钥都是 32 字节,写成带填充的 base64(即wg genkey和wg pubkey输出的格式)或 64 位十六进制数字。--test只检查编码:一个能正常解码、但属于别人的密钥也能通过检查,之后对端永远不会应答。 -
复制 endpoint。
Endpoint对应endpoint,格式为host:port。endpoint = "203.0.113.10:51820"该值在最后一个
:处拆分。IPv6 endpoint 不加方括号,写成2001:db8::1:51820:方括号不会被去掉,因此[2001:db8::1]:51820能通过--test,却会在隧道启动时失败。主机名在隧道启动时用系统解析器解析,既不经过[dns],也不在--test期间解析,并使用第一个解析结果。 -
复制地址,去掉前缀长度。
Address = 10.2.0.2/32写成address = ["10.2.0.2"]。每个地址是一个单独的字符串;10.2.0.2/32, 2001:db8:a::2/128这样的列表写成["10.2.0.2", "2001:db8:a::2"]。address = ["10.2.0.2"]带前缀会导致构建失败,报
invalid IP address syntax/in `address`。这里列出的地址族就是隧道能到达的地址族:没有 IPv6 条目时,无论address_family如何设置,IPv6 目标都会被跳过。 -
复制 keepalive。
PersistentKeepalive对应keepalive,单位为秒(最大65535)。如果本机位于 NAT 或有状态防火墙之后,请保留它:它能防止连接空闲时通往对端的 UDP 映射过期。不写这个键,或写0,都会关闭 keepalive。keepalive = 25 -
设置 MTU;如果隧道只支持 IPv4,就固定地址族。 如果文件中有
MTU行,把它复制到mtu;默认值是1420。如果服务只提供 IPv4 隧道(只有一个 IPv4Address,或者有一个服务实际并不路由的 IPv6 地址),就加上下面两行:# 写在 [[outbound]] 表中,与 protocol 放在一起address_family = "ipv4_only"# 写在 [outbound.settings] 中mtu = 1280address_family = "ipv4_only"让出站只使用目标的 IPv4 地址。address中只有一个 IPv4 条目时,隧道本来就会跳过 IPv6 目标,所以这里的设置起的是保护作用:--test会检查它。如果address中没有 IPv4 条目,构建会失败,报outbound wg: wireguard address_family ipv4_only needs an IPv4 address。- 如果服务还列出了一个它并不路由的 IPv6 地址,而你把它复制进了
address,出站仍然不会尝试目标的 IPv6 地址。若使用默认的auto,出站会按解析器返回的顺序尝试这些地址,每次尝试最多等待 10 秒才换下一个。
mtu = 1280是一个保守值,也是 IPv6 允许的最小 MTU,能适应比常见的 1500 字节更窄的路径。如果服务指定了 MTU,就改用该值。 -
只有服务要求时才添加
reserved。 WireGuard 数据包带有三个保留的头部字节,通常为零。少数服务把它们用作客户端标识。如果你的服务给了三个数字,就写成列表:reserved = [1, 2, 3]如果给的是一个很短的 base64 字符串(四个字符,例如
AQID),先把它解码成三个数字:终端窗口 printf '%s' 'AQID' | base64 -d | od -An -tu1# 1 2 3etemenanki-app 会把这些字节写入它发送的每个数据包,并在收到的每个数据包中将其清零。如果服务没有这样要求,就不要写
reserved:标准的 WireGuard 对端会把这些字节当作消息类型的一部分来读取,非零值会导致它丢弃你的数据包。 -
其余行不需要迁移。 下面这些 wg-quick 行在出站中没有对应项:
wg-quick 行 改由什么负责 AllowedIPs由路由规则决定哪些流进入隧道(见下一节)。 DNS主机上不做任何改动。经隧道到达的目标由本机的 [dns]解析器解析。ListenPort通往对端的 UDP socket 使用临时端口。 Table、PreUp、PostUp、PreDown、PostDown不存在需要管理的接口或主机路由。 误复制进来的键会明确报错:
invalid settings: unknown field `allowed_ips`,后面附有所有有效键的列表。 -
检查文件。
终端窗口 etemenanki-app --test -c wireguard-egress.toml当语法、密钥编码和路由都有效时,它会输出
Configuration OK.。它不会联系对端。
对照结果如下:
[Interface]PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=Address = 10.2.0.2/32DNS = 192.0.2.53
[Peer]PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=AllowedIPs = 0.0.0.0/0Endpoint = 203.0.113.10:51820PersistentKeepalive = 25[[outbound]]tag = "wg"protocol = "wireguard"address_family = "ipv4_only"
[outbound.settings]private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="peer_public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="endpoint = "203.0.113.10:51820"address = ["10.2.0.2"]mtu = 1280keepalive = 25katana 节点也能承载同样的隧道,但使用 katana 自己的字段名(例如 local_address,它允许带前缀长度)。该格式见 katana 的出站文档。
选择哪些流量走隧道
Section titled “选择哪些流量走隧道”在 wg-quick 中,AllowedIPs = 0.0.0.0/0 会让整台机器的流量都走隧道。而在这里,只有被路由规则发往 wg 的流才会进入隧道。示例使用一条规则:
[route]default = "direct"geosite = "/etc/etemenanki/geosite.dat"
[[route.rule]]outbound = "wg"domain_suffix = ["example.com"]geosite = ["netflix"]- 同一条规则中的匹配条件是“或”的关系。 发往
example.com、它的任意子域名,或netflixgeosite 列表中任意域名的流,都会发往wg。 - 第一条匹配的规则生效。 如果希望广告即使出现在走隧道的网站上也被拦截,就把拦截广告的
blackhole规则放在这条规则之上;如果有更宽泛的规则会抢走相同的流,就把这条规则放在它之上。 - 其他流量使用
default。 请显式写出这一行。没有这一行时,文件中的第一个[[outbound]]就是默认出站;这里的默认出站之所以是direct,只是因为它排在第一个。 - 如果要改为全部走隧道,就设置
default = "wg",并把例外流量路由到direct。 - 按地址路由。
cidr或geoip匹配条件按目标 IP 分流。客户端以域名给出的目标永远不会匹配它们。
当客户端把域名交给代理、而不是自己解析时,域名规则的效果最好:curl 和大多数工具中使用 socks5h://,浏览器中启用“使用 SOCKS v5 时代理 DNS 查询”。当客户端发送的是纯 IP 地址时,嗅探仍然能从 TLS 或 HTTP 请求中还原出域名,使规则能够匹配,但这条流随后会拨号到那个 IP。使用 ipv4_only 时,以这种方式发送的 IPv6 地址无法进入隧道。所有匹配条件见路由页面。
投入使用前验证出口
Section titled “投入使用前验证出口”能通过 --test 的配置只说明语法正确,还没有证明对端会转发你的流量。在任何真实流量依赖这条隧道之前,先用一个临时进程检查这一点。这次检查与正在运行的代理没有任何共享:不同的进程、不同的文件、不同的端口。
-
做一份检查用的副本。 在一个空目录中复制配置,并改动三处:端口(任选一个空闲端口,这里用
10808);default = "wg",让每个请求都走隧道;为了让检查自成一体,删除[[route.rule]]块和geosite路径。wg-check.toml [[inbound]]tag = "check"protocol = "socks"listen = "127.0.0.1"port = 10808[[outbound]]tag = "direct"protocol = "freedom"[[outbound]]tag = "wg"protocol = "wireguard"address_family = "ipv4_only"[outbound.settings]private_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="peer_public_key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="endpoint = "203.0.113.10:51820"address = ["10.2.0.2"]mtu = 1280keepalive = 25[route]default = "wg" -
校验配置。
终端窗口 etemenanki-app --test -c wg-check.toml -
在前台启动,并打开 WireGuard 日志。
RUST_LOG会覆盖[log].level。终端窗口 RUST_LOG=info,boringtun=debug,etemenanki_app=debug etemenanki-app -c wg-check.toml -
记下本机自己的地址。 在第二个终端中,不经代理访问一个 IP 回显服务:
终端窗口 curl -m 30 https://ifconfig.me -
再经隧道访问一次。
终端窗口 curl -m 30 --socks5-hostname 127.0.0.1:10808 https://ifconfig.me返回的必须是对端的出口地址,并且与第 4 步的结果不同。任何 IP 回显服务都可以;如果服务还会报告地址所在位置,就能顺便确认出口地区。
-
如果第一次请求失败或超时,再运行一次,然后再下结论。隧道要等第一个流到达时才启动,所以第一个连接要等待隧道建立并完成握手,即使配置正确也可能失败。第二次仍然失败,才是真正的失败。
-
用 Ctrl-C 停止进程。 除了检查用的文件,它不会留下任何东西。
curl 只测试了 TCP。该出站也承载 UDP,但测试 UDP 需要一个支持 SOCKS5 UDP ASSOCIATE 的客户端;SOCKS 入站默认允许 UDP(udp = true)。
在正式配置中确认分流
Section titled “在正式配置中确认分流”正式配置运行起来之后,通过端口 1080 检查两条路径:
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me返回的是本机地址,因为回显服务不匹配任何规则,走的是direct。- 要查看隧道路径,临时把回显服务加入 WireGuard 规则,例如
domain_full = ["ifconfig.me"],保存文件后重复请求。这次返回的是出口地址。完成后删掉这一行。
etemenanki-app 会在文件保存时重新加载。重载会替换所有出站,因此会断开已打开的连接,下一个流到达时隧道会重新启动并进行新的握手;见安全地编辑运行中的配置。
| 情形 | 会发生什么 |
|---|---|
启动和 --test |
不打开任何东西。第一个路由到 wg 的流会解析 endpoint、绑定 UDP socket 并启动隧道;握手随第一个数据包进行。 |
| 多个流 | 路由到同一个出站的所有流共用一条隧道和一个 UDP socket。每个流在隧道内都有自己的有界缓冲区,因此一个慢速的流不会拖住其他流。 |
| 目标停止读取 | 只有这个流的发送方会等待。它的数据先填满该流 64 KiB 的 socket 缓冲区,再填满一个可容纳 256 个待写数据块的队列,之后代理停止从该客户端读取,直到目标重新开始读取。隧道上的其他流照常传输。 |
| 无法发送的 UDP 数据报 | 如果数据报比该 UDP 关联整个 64 KiB 的发送缓冲区还大,或者隧道无法寻址它的目标,该数据报会被丢弃。同一 UDP 关联上后续的数据报仍会照常发出。只是遇到缓冲区已满的数据报不会被丢弃,而是等待缓冲区腾出空间。 |
| 有多个地址的 TCP 目标 | 出站依次尝试每个允许的地址,每个 10 秒。 |
| 发往域名的 UDP 数据报 | 发往第一个允许的地址;域名无法解析时丢弃该数据报。 |
| 对端停止应答 | 隧道保持运行,不会重建,因为对端沉默不属于 socket 错误。每个新的 TCP 连接对每个地址等待 10 秒后放弃,报 wireguard: tunnel TCP connect failed for all resolved addresses (…: timed out)。 |
| 隧道的 socket 出错 | 下一个流会记录 wireguard: tunnel driver stopped, rebuilding,构建新隧道并重新解析 endpoint。已运行至少 10 秒的隧道会立即重建。如果启动失败,或隧道在 10 秒内就失效,出站会先等待 2 秒再尝试,之后每次加倍,最长 30 秒;等待期间到达的流会失败,报 wireguard: tunnel is down, waiting before the next attempt。 |
协议页面的隧道生命周期有更多细节。
全部 WireGuard 设置
Section titled “全部 WireGuard 设置”以下是 protocol = "wireguard" 时 [outbound.settings] 下的所有键。未知的键会被拒绝。通用出站键 address_family 写在 [[outbound]] 表本身;对 WireGuard 而言,它作用于隧道内的目标,从不作用于 endpoint。
| 键 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
private_key | string | 是 | — | 本端的 Curve25519 私钥,即 wg-quick 文件里的 PrivateKey。32 字节,写成带填充的标准 base64(wg genkey 的输出格式)或 64 位十六进制。不带填充的 base64 会被拒绝。错误信息:invalid wireguard private_key。 |
peer_public_key | string | 是 | — | 对端的公钥,即 [Peer] 段里的 PublicKey。编码方式与 private_key 相同。错误信息:invalid wireguard peer_public_key。 |
preshared_key | string | 否 | — | 可选的预共享密钥,参与握手计算,即 PresharedKey。编码方式与 private_key 相同。只有对端为你配置了预共享密钥时才填写;两边不一致会导致握手始终无法完成。错误信息:invalid wireguard preshared_key。 |
endpoint | string | 是 | — | 对端的 UDP 地址,格式为 host:port,在最后一个 : 处拆分。host 可以是 IP 地址或域名;域名在隧道启动时用系统解析器(而不是 [dns])解析,取第一个结果。IPv6 不要加方括号(2001:db8::1:51820):方括号不会被去掉,所以 [2001:db8::1]:51820 能通过 --test,但运行时解析失败。错误信息:wireguard endpoint must be host:port、invalid wireguard endpoint port。 |
address | array of IPs | 是 | — | 对端分配给你的隧道内地址,写成不带前缀长度的纯 IP:["10.0.0.2", "2001:db8:a::2"]。10.0.0.2/32 这样的 CIDR 写法会报 invalid IP address syntax。它决定隧道能访问哪些地址族的目标:没有 IPv6 地址时,IPv6 目标不可达。空列表能通过 --test,但之后每个连接都会失败,报 wireguard: no tunnel-local addresses configured。 |
mtu | integer | 否 | 1420 | 隧道内 IP 包的最大字节数;用户态 TCP 协议栈据此确定分段大小。不做范围检查。如果小请求正常、大流量传输卡住,就把它调低(例如 1280)。 |
keepalive | u16 | 否 | — | 持续保活(persistent keepalive)的间隔秒数,即 PersistentKeepalive。不写或写 0 表示关闭。本机位于 NAT 或有状态防火墙之后、且连接可能长时间空闲时应当设置(常用 25)。 |
reserved | array of integers | 否 | — | 恰好三个字节([0, 0, 0] 到 [255, 255, 255]),写入每个发出的 WireGuard 包头的第 1 到 3 字节,做法与 Xray 的 reserved 相同。无论是否设置 reserved,收到的包在解码前都会把这三个字节清零。只有服务方要求时才设置。其他长度都会报 invalid length …, expected an array of length 3。 |
address_family 接受 auto(默认值)、ipv4_only、ipv6_only、prefer_ipv4 和 prefer_ipv6,不区分大小写,- 视同 _,还接受 ipv4、v4 等简写。无法识别的值会失败,报 outbound wg: invalid address_family "…"。完整列表见出站。
| 现象 | 可能的原因及解决方法 |
|---|---|
invalid IP address syntax / in `address` |
address 中带了前缀。应写 "10.2.0.2",而不是 "10.2.0.2/32"。 |
outbound wg: invalid wireguard private_key(或 peer_public_key、preshared_key) |
粘贴不完整、缺少 =,或混入了多余字符。 |
invalid length 2, expected an array of length 3 / in `reserved` |
reserved 必须恰好是三个 0 到 255 之间的数字;更大的数字会失败,报 invalid value: integer `300`, expected u8。base64 字符串会失败,报 invalid type: string "…", expected an array of length 3;按第 7 步的方法解码。 |
日志中出现 wireguard: tunnel start failed: … |
隧道无法启动,例如 endpoint 中的主机名或带方括号的 IPv6 地址无法解析。冒号后面是具体错误。 |
wireguard address_family ipv4_only needs an IPv4 address |
address_family 指定的地址族在 address 中没有对应地址。 |
geosite code not found: … |
你的 geosite.dat 中没有这个代码。 |
日志反复出现 Sending handshake_initiation,始终没有应答 |
对端没有应答:检查 endpoint、peer_public_key、preshared_key、reserved,以及出站 UDP 是否被阻断。 |
| 握手成功,但每个请求都超时 | 对端不为这个密钥转发流量。重试一次;如果问题依旧,先怀疑密钥或账户已过期或被吊销,重新获取配置,然后再去排查 MTU 或路由。 |
| 小页面能加载,大文件下载卡住 | MTU 对这条路径来说太大。调低 mtu,例如调到 1280。 |
wireguard: no usable ipv4_only destination address for … |
目标没有 IPv4 地址,或客户端发送的是 IPv6 地址。这条隧道只能承载 IPv4。 |
| 已列出的网站仍然显示本机地址 | 这条流没有匹配规则:检查域名,并让客户端发送域名(socks5h);或者是更靠前的规则抢走了这条流。 |
协议页面解释了更多 WireGuard 日志行。