从移动网络能连上、坐在沙发上却连不上的 VPN 服务器,通常不是证书问题。局域网内的客户端要求路由器转换一个目标地址是路由器自身公网地址的数据包,这意味着数据包必须先出去再原路回来。这个动作叫发夹(hairpinning),很多路由器要么根本不实现,要么只有在勾选某个选项时才实现。正是从外部测试成功这一点让人困惑,因为它证明监听和转发规则都是好的。

数据包出去又回来

跟着数据包走一遍。客户端解析公网域名,连接到公网地址,路由器的目的 NAT 把目的地址改写为内部服务器。服务器的回应发往客户端的局域网地址,于是它从路由器的 LAN 接口返回,从未经过那条本该把源地址转换回去的 NAT 规则。

现在客户端收到一个来自它从未发送过任何东西的地址的数据包,于是把它丢弃。这种不对称就是全部故障。

RFC 4787 描述了这种情况下对 NAT 的预期行为,简短的说法是:即使在理论上有规定,实践中它也是可选的。有些路由器把这个设置叫 NAT loopback,有些叫发夹 NAT 或 NAT reflection,还有些根本不提供。缺少这项功能的路由器并没有配置错误,它只是做了平常的事,修复要落在别处。不要因为本地测试失败就断定公网转发规则有问题。

检查 1:分别测试两条路径

在局域网之外,用真实用户会用的同一份配置连接,确认会话能够建立。在局域网之内,用同一份配置连接公网地址,并记录确切的错误和时间。

精确的本地测试保留主机名,只替换地址:curl -v --resolve vpn.example.com:443:203.0.113.10 https://vpn.example.com/ 会把请求发往公网地址,同时仍然呈现正确的服务器名称。这样就把域名解析问题和地址转换问题分开了,当同一个域名在咖啡馆能用时,两者很容易混淆。记下客户端是在 TCP 连接阶段失败还是稍后在 TLS 阶段失败,因为前者指向转换,后者指向身份。

检查 2:看服务器那边看到了什么

最有用的观察是服务器的视角。在 VPN 主机上抓包,过滤客户端的局域网地址和路由器的局域网地址,然后重试本地连接。如果什么都没有到达,说明数据包没能在转换中存活。

如果连接来自路由器的局域网地址而不是客户端的地址,说明发夹转换发生了,源地址被改写。这是可用的配置,不是故障。

检查 3:在不碰外部路径的前提下修复

有三种受支持的做法,影响范围依次扩大。最窄的是拆分 DNS:发布一条内部记录,让同一个服务器名在局域网内解析到内部地址,从而完全绕开公网路径。其次是启用路由器文档中记载的 loopback 或 reflection 选项(如果有),并且只对这项服务生效。最宽的是在路由器上加一条源 NAT 规则,把来自局域网的流量改写为指向服务器,让回应经由路由器返回;在 Linux 路由器上,这就是一条位于 MASQUERADE 中、匹配服务器地址和端口的 POSTROUTING 规则。三种做法都不动外部路径,而这正是要保住的性质。

不要为了把内部路径做得像外部路径而扩大无关的端口转发,也不要为了绕开问题把服务搬到别的端口。这两种改动都是为了修复只有内网才看得见的问题,却改变了外网能够到达的范围。

拆分 DNS 有一个值得点名的陷阱。用任意 IP 替换主机名会改变客户端校验的对象,所以内部记录仍必须与证书上的名称一致。

检查 4:事后验证两条路径

在局域网之外重做外部尝试,用同一份配置重做内部尝试,确认服务器日志显示的是两个各自成功的会话,而不是一个成功加一片沉默。然后再确认其他客户端仍能连上监听端口,且通往私有资源的路线没有改变。

记录要短:每个客户端用了哪个地址、服务器看到了什么、三种修复里用了哪一种。RFC 4787 的发夹要求 是这里对 NAT 应有行为的标准参考,也解释了为什么没有这项功能的路由器其实是在正常行事。外网无法复现的本地故障,在被证明是别的原因之前都属于转换问题,所以就该按转换问题去测。

相关文章