我从外网开始,就像普通用户那样:在手机浏览器的移动数据里输入这个名字,看着连接一直挂着直到超时。名字能解析,所以 DNS 没问题。家里路由器的端口转发页面也是死路,因为它的 WAN 地址显示 100.64.31.42,而这个地址不是运营商能从互联网路由到我的地址。这个数字解释了整次故障:线路处在运营商级 NAT之后,所以隧道我这端的公网地址属于运营商。下面是我从那个卡住的标签页走到一个能应答的服务的完整路径。

从客户端开始,由外向内排查

在改动任何规则之前,先证明链条断在哪里。在笔记本上运行 dig +short files.example.net,返回了 VPS 的地址,这就排除了本机 DNS 缓存过期。接着 curl -v --connect-timeout 5 files.example.net:5022 是超时而不是被拒绝。这个区别很重要:超时通常意味着包在路上被丢弃,而被拒绝意味着有东西应答并说了不。

再往里一跳是 VPS 本身。ss -ltnp 没有显示任何在 5022 上监听的东西,这在 DNAT 规则存在之前是正常的。在同一台机器上,wg show(OpenVPN 则是 ip -br a show tun0)确认家里的对端在线且握手是新的。如果对端掉线,任何 NAT 规则都没用,家里路由器 VPN 页面上的绿灯也毫无意义。

确认家里的线路确实在 CGNAT 之后

把路由器在 WAN 接口上报告的地址,和外部服务看到的地址对比。100.64.0.0/10,也就是 100.64.x.x 到 100.127.x.x 之间的任何地址,都是 RFC 6598 的共享地址段,从互联网不可达。当路由器声称自己有普通的私有地址、而公网查询返回无关结果时,你的结论也一样。CGNAT 线路还可能拿到一个只维持到下次租约的公网地址,于是端口转发看起来正常工作一小时,然后悄悄失效。

有些运营商会给真正的公网地址,但在家庭账户上封掉入站 80 和 443。如果你的服务只有网页、WAN 地址又确实是公网,那么在搭中继之前先问客服。如果不是,下面的设计对每个端口同样适用,而且值得停掉路由器那侧的尝试:运营商网络里的转换设备会在连接到达你的 WAN 接口之前就把它终止。

把名字指向 VPS 地址

DNS 不转发流量,所以服务的 A 记录必须指向 VPS 的公网地址。指向 192.168.1.20 的 A 记录只对已经在 VPN 内的客户端有意义;从互联网看,它解析到一个没人能路由的地址。VPS 自身的管理访问我另用一个名字,这样永远不会搞混某条记录指的是哪台主机。

如果 VPS 和家里机器都提供 HTTP,就给各自一个名字,让 VPS 决定谁来应答。把两者塞进一个名字、指望代理选中正确的后端,正是你花一下午调试错主机的典型做法。

只对一个端口做 DNAT,SSH 留在 22

直接的做法是在 VPS 上做目的地址 NAT。为正在运行的内核开启转发,并在规则里保留家庭服务器的隧道地址,而不是它的局域网地址:

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A PREROUTING -p tcp -d 198.51.100.10 --dport 5022 -j DNAT --to-destination 10.8.0.2

让连到 VPS 自身的 SSH 留在 22,把另一个端口,例如 5022,转发到家里机器。把 22 转发到一台关机的机器上,你就失去了唯一能修好它的通道。同样的逻辑也适用于代理:把 VPS 的管理端口留在中继规则之外。

规则一旦生效就做持久化。在 Debian 或 Ubuntu 上,apt install iptables-persistent 之后用 netfilter-persistent save 把它们写入 /etc/iptables/rules.v4。在使用 firewalld 的主机上,等价做法是在区域上做永久端口转发,生效前需要 firewall-cmd --reload。只活在内存里的规则会在服务商第一次维护重启时消失,而且往往是在糟糕的时间点。

修好回程路径,或者干脆不需要它

DNAT 只改写目的地址。家庭服务器看到的连接来自原始客户端地址,如果它的默认路由指向家里路由器,回包就会从家里的 WAN 出去,而运营商的转换设备会在那里把它丢掉。这种不对称就是我遇到的第二个故障,尽管 DNAT 规则看起来完全正确。

有两种修法有效。给家里机器一条策略路由,让来自隧道的流量从隧道回去:

ip rule add from 10.8.0.2 table 100
ip route add default dev wg0 table 100

或者让 VPS 改写源地址,使回包有一个能到达的地址,并让 conntrack 在出方向把它翻回来:

iptables -t nat -A POSTROUTING -d 10.8.0.2 -p tcp --dport 5022 -j SNAT --to-source 10.8.0.1

SNAT 版本会把真实客户端地址从家庭服务器上隐藏掉,所以应用日志里看到的对端是 10.8.0.1。要么在应用层把原始地址传下去,要么接受失去按客户端记日志,但要有意识地决定,而不是在事故中才发现。

从外网检查整条路径

从一个既不是 VPS 也不是家里的网络验证:移动数据上的手机,或者别处的廉价 shell。nc -vz files.example.net 5022 应该报告连接成功,openssl s_client -connect files.example.net:443 -servername files.example.net 应该显示公网名字的证书。然后检查服务商的安全组或云防火墙。一条从来看不到包的 DNAT 规则,通常是被主机上面的一层挡住了。

最后,决定隧道掉线时会发生什么。DNAT 规则会继续接受连接,并悄悄把它们灌进一条死隧道,看起来和彻底坏掉的服务一模一样。在 VPS 上做一个在对端离线时拒绝该端口的健康检查,只花几行代码,却能省下一小时瞎猜。之后就别再改规则了,先观察一天再相信它。

相关文章