连上 OpenVPN 却什么都打不开:从客户端往外排查
隧道已起,浏览器却一直在转圈。从客户端往外走:两次抓包、一个内核开关、真正的出口网卡,最后才是 DNS。
客户端窗口显示已连接,隧道地址也在,浏览器却仍在转圈。这种组合很正常,因为 握手成功只证明隧道建起来了,并不能说明服务器会不会转发你的数据包、重写它们的源地址,或者回应你的 DNS 查询。多数人的错误是先打开服务器配置。相反,应当从客户端往外走,因为下面每一步都会告诉你下一步是否值得检查。
先从客户端开始,再往外走
按这个顺序做三项测试,记下第一项在哪里失败。ping 服务器的隧道地址,这能证明隧道本身能承载流量。用数字 ping 一个公网地址,这能证明转发和地址转换,且不涉及域名解析。然后再尝试解析主机名,这是唯一会用到 DNS 的测试。每个结果都能从怀疑名单里划掉一整层。
不要因为浏览器感觉更快就直接跳到第三项测试。页面打不开会同时覆盖这三层,正是从这里起步,人们才会去重建一条本来能用的隧道。打不开的页面不会告诉你三层里坏的是哪一层。
两次抓包能看出数据包停在哪里
在客户端持续 ping 一个公网地址的同时,在隧道网卡上抓一次,在应该通往互联网的上行链路上抓一次。两次同时进行,时间戳才能对得上。
sudo tcpdump -ni tun0 -c 20 icmp
sudo tcpdump -ni enp1s0 -c 20 icmp
如果请求出现在隧道上却从未出现在上行链路,说明服务器在隧道与网线之间把它们丢掉了,原因是转发或转发的防火墙策略,而不是 NAT。如果两处都出现,说明服务器尽到了本职,缺失的是更外层的回应,指向地址转换、上游过滤或运营商侧的限制。
上行链路的名称要从路由表拿,而不是照抄教程。云镜像和改过名的网卡很少与指南里的示例网卡一致,而在错误设备上抓包,看起来和一台不转发的服务器一模一样:
ip -4 route get 9.9.9.9
这段输出里的 dev 字段就是后面 NAT 规则要用的名字,把它记下来。
转发是一个内核开关加一条防火墙策略
内核首先得同意转发。先读出当前值,然后把设置持久化,而不是只写进正在运行的内核:
sysctl net.ipv4.ip_forward
printf 'net.ipv4.ip_forward=1\n' > /etc/sysctl.d/70-vpn-forward.conf
sysctl --system
转发只是其中一半。在使用 UFW 的主机上,转发策略在 /etc/default/ufw 中设置,必须改成 ACCEPT 之后重新加载才会生效。转发路径上的默认丢弃会让握手保持完全健康,却悄悄丢弃每一个试图从隧道外出的数据包,这正是你最初遇到的症状。
直接管理 nftables 的主机面对的是同一个问题,只是语法不同。客户端测试进行时去看转发链上的计数器,因为在配置文件里,被查询的链和负责丢弃的链长得一模一样。
在 NAT 规则里写上真正的出口网卡
源地址转换应该落在真正离开主机的网卡上,并用服务器实际下发的地址池。这条规则的两半都必须描述现实,哪一半都不能省。
iptables -t nat -A POSTROUTING -s 10.44.0.0/24 -o enp1s0 -j MASQUERADE
经典的失败是从指南里抄来一条写着 -o eth0 的规则,而机器的网卡根本不叫这个名字。规则会毫无警告地装上,什么也匹配不到,客户端依旧没有外网。地址池是同一个错误的另一半:如果服务器下发 10.44.0.0/24,而规则覆盖的是 10.8.0.0/24,就永远匹配不上。
要看计数器,而不是相信规则文字。列表会给出每条规则的包数,从不匹配的规则无论看起来多正确都会停在零。在据此下结论之前,先让它跑一分钟真实的客户端流量:
iptables -t nat -L POSTROUTING -v -n
真实客户端测试之后计数仍为零,说明匹配条件写错了,而不是隧道坏了。一旦匹配上,就把规则集保存下来,让它扛得住重启;只活在你 shell 历史里的修复,第一次重启就没了。
如果服务器同时是云上的 NAT 实例,平台也有发言权。做地址转换的实例通常需要在服务商提供该选项的地方关闭源目地址检查,VPN 端口本身也必须能从外部访问。
DNS 是第一个故障背后的第二个故障
按地址 ping 通、按名字失败,是解析器的问题,而且它有自己的成因。保留本地解析器的客户端会把查询送进全量隧道,然后等一个永远不会来的回答,因为本地解析器现在位于路由的错误一侧。
从服务器下发解析器,客户端才知道该问谁。两条记录能在第一条不可达时提供后备,而且两条都应该是隧道真正允许的解析器。
push "dhcp-option DNS 9.9.9.9"
push "dhcp-option DNS 149.112.112.112"
Windows 客户端仍可能退回本地解析器,这正是 block-outside-dns 存在的原因,代价是失去只有本地网络才能解析的名字。使用 systemd-resolved 的 Linux 客户端,除非脚本把它装进去,否则常常忽略下发的解析器,所以在断定下发被忽略之前,先看看隧道设备的 resolvectl status。
按用户的体验收尾。从客户端加载一个真实页面,确认它从哪个地址出去,重启服务器,重新连接,再把这三次测试重做一遍。只有这第二遍,才能证明持久化的规则、防火墙策略和下发的解析器一起活了下来。