WireGuard 换到 Wi‑Fi 中继后失联的排查记录

最近把一台 Tegra 设备的上网方式改了一下,结果顺手踩了个 WireGuard 的坑。

这台设备原本接的是一台独立随身 Wi-Fi,WireGuard 客户端长期回连家里的主路由,一直很稳定。后来为了省掉这条独立公网链路,改成用另一台路由器中继家里的 Wi-Fi,再通过网线给 Tegra 上网。

网络本身是通的,设备也能正常访问互联网,但 WireGuard 死活握不上了。

最后的问题并不在 WireGuard 配置本身,而在 Endpoint 的访问路径。解决方式也很简单:内部网络对 WireGuard 域名做一次 Split DNS,直接解析到主路由的 LAN 地址。

下面把这次排查过程记一下。

文中的公网域名和公网 IP 做了替换,内网地址保留实际网段,方便说明路由关系。

拓扑变化

原来的网络很简单:

graph TD A[Tegra] --> B[随身 Wi-Fi] B --> C[Internet] C --> D["主路由公网地址:51822"] D --> E[WireGuard Server]

Tegra 上的 WireGuard Endpoint 是一个 DDNS 域名:

ENDPOINT=home.example.com:51822

设备不在家庭网络里,因此它访问这个域名时,就是一次普通的公网入站连接。

后来改成了下面这样:

graph TD A["主路由
LAN: 192.168.100.1"] -->|Wi-Fi| B["中继路由
WAN: 192.168.100.x
LAN: 192.168.110.1"] B -->|NAT| C["Tegra
192.168.110.172"]

中继路由不是单纯二层桥接,而是自己又做了一层 NAT。

所以 Tegra 的网络参数变成了:

IP:      192.168.110.172/24
Gateway: 192.168.110.1

WireGuard 接口还是原来的:

wg0: 10.10.2.10/24

最明显的线索:只发不收

先看 wg show:

peer: ...
  endpoint: <公网IP>:51822
  allowed ips: 10.10.2.0/24
  transfer: 0 B received, 1.30 KiB sent
  persistent keepalive: every 25 seconds

这个状态其实已经很有价值了。

WireGuard 一直在发握手包,PersistentKeepalive 也正常工作,但收到的数据始终是 0。

也就是说问题大概率不是“WireGuard 没启动”,而是:

graph LR A[Tegra] -->|"UDP 握手包持续发出"| B["公网 Endpoint"] B -. "没有响应返回" .-> C["0 B received"]

接着看路由:

ip route

结果里最关键的是:

default via 192.168.110.1 dev enP7p1s0f0
10.10.2.0/24 dev wg0
192.168.110.0/24 dev enP7p1s0f0

这里也顺便排除了一个常见问题:AllowedIPs 只有 10.10.2.0/24,并没有把主路由网段或者默认路由塞进 WireGuard。

所以 Endpoint 不可能被错误地再次路由进 wg0。

换句话说,先不用折腾 MTU、密钥、AllowedIPs,这几个方向都不像主因。

一开始怀疑 NAT Loopback

因为设备现在已经重新回到了家庭网络下面,而 Endpoint 仍然指向主路由的公网地址,所以第一反应自然是 Hairpin NAT / NAT Loopback。

路径大概会变成:

graph TD A["Tegra
192.168.110.172"] --> B["中继路由 LAN
192.168.110.1"] B -->|NAT| C["中继路由 WAN
192.168.100.x"] C --> D["主路由公网 Endpoint
:51822"] D --> E["再绕回主路由 WireGuard"]

这种“从内部访问自己的公网地址,再通过端口映射折回来”的行为,本来就很依赖路由器的 NAT Loopback 实现,UDP 又更容易遇到各种奇怪情况。

不过排查时还出现了一个干扰项。

通过:

curl -4 https://api.ipify.org

看到的出口 IPv4 和 WireGuard Endpoint 对应的公网 IPv4 并不相同。

这意味着不能简单地说“肯定就是 Hairpin NAT”。主路由可能还有多 WAN、策略路由、VPN 出口,或者别的网络路径。

与其继续猜公网路径,不如直接验证一件更简单的事:

如果不走公网地址,直接访问主路由 LAN 地址,WireGuard 能不能握手?

真正定位问题的一条命令

主路由 LAN 地址是:

192.168.100.1

于是临时把 WireGuard peer 的 Endpoint 改成:

wg set wg0 peer <SERVER_PUBLIC_KEY> endpoint 192.168.100.1:51822

再执行:

wg show

立刻就有数据了。

到这里其实已经没必要继续研究公网那条路径到底在哪一层被吃掉了。

至少可以确定:

  1. WireGuard 客户端配置没问题;
  2. Server 的监听端口没问题;
  3. 中继路由的双 NAT 并不妨碍 Tegra 主动访问 192.168.100.1;
  4. 出问题的是“内部设备仍然通过公网 Endpoint 回连主路由”这条路径。

这比继续改防火墙规则或者 WireGuard 参数要直接得多。

最后的处理:Split DNS

当然,直接把 Tegra 的配置改成:

ENDPOINT=192.168.100.1:51822

也能用。

但这样会带来一个新的问题:以后设备再切回随身 Wi-Fi,192.168.100.1 就不可达了。

原来的 DDNS 域名其实正适合解决这个问题,只需要让它在不同网络里解析成不同地址。

公网 DNS 保持:

home.example.com -> 主路由公网 IP

家庭内部 DNS 改成:

home.example.com -> 192.168.100.1

这样 WireGuard 配置完全不用动:

ENDPOINT=home.example.com:51822

设备在外面时:

graph TD A[home.example.com] --> B["主路由公网 IP"] B --> C[WireGuard]

设备在家庭网络或者它的下级 NAT 网络里时:

graph TD A[home.example.com] --> B["192.168.100.1"] B --> C[WireGuard]

这就是很典型的 Split DNS。

相比去修 Hairpin NAT,我更喜欢这个方案。内部流量本来就没必要绕公网地址,直接走 LAN 路径更短,行为也更可控。

一个容易忽略的小坑

WireGuard 并不会一直盯着 DNS 记录实时刷新 Endpoint。

配置被应用时,域名会被解析成 IP,内核最终保存的也是具体 Endpoint 地址。因此修改内部 DNS 后,最好重新加载一次 WireGuard 配置,或者重启负责配置它的服务。

例如:

systemctl restart [email protected]

然后确认:

wg show

看到的 Endpoint 已经变成:

192.168.100.1:51822

同时可以用:

getent ahostsv4 home.example.com

确认内部 DNS 的结果确实是 192.168.100.1。

如果域名还有 AAAA 记录,也最好顺手检查一下内部 IPv6 的解析结果,避免客户端优先走了另一条 IPv6 路径。

这次排查真正有用的几个判断点

回头看,这次问题最容易让人跑偏的地方,是看到 WireGuard 不通就开始查密钥、MTU、防火墙、AllowedIPs。

实际上最关键的信息只有几个:

transfer: 0 B received, 1.30 KiB sent

说明客户端确实在发包。

AllowedIPs = 10.10.2.0/24

说明 Endpoint 没有被错误地塞回隧道。

再加上:

wg set ... endpoint 192.168.100.1:51822

之后立刻恢复握手,问题范围就已经缩到公网 Endpoint 这条路径了。

网络问题很多时候就是这样。先把数据包到底“准备往哪走”弄清楚,比盯着应用层配置来回改有效得多。

最后的拓扑没有任何变化:

graph TD A["主路由
192.168.100.1"] -->|Wi-Fi| B["中继路由
192.168.110.1"] B -->|NAT| C["Tegra
192.168.110.172"] C --> D["WireGuard
10.10.2.10"]

真正改变的只有一件事:

在内部网络里,不再让 WireGuard 为了连接同一台主路由,先去访问它的公网地址。