你想象 VPN 服务器从数据包中读取笔记本地址并写入日志。它真正读到的是最后一跳 NAT 放进外层报头的地址,而在用手机开热点的场景里,这往往是数百名其他用户共享的地址。你自己的 192.168.x.x 从不在那个报头里穿越互联网,而隧道地址是在连接建立之后才由服务器分配的。所以当服务器日志和笔记本上的 ip addr 对不上时,两者都没有错;它们描述的是同一个会话的不同层次。

三个地址,日志里只有一个

混乱来自把三个地址压缩成一个。它们各有不同的所有者,存在于不同的报头中,并通过不同的路径到达服务器。

AddressWho assigns itWhere it sitsVisible to the VPN server
Device address, e.g. 192.168.1.24Your router via DHCPOuter header, before translationNo, rewritten in place
Translated address and portRouter, ISP CGNAT, or bothOuter header, after translationYes, this is the tuple it logs
Tunnel address, e.g. 10.8.0.6The VPN server itselfInner header, after decapsulationYes, but it chose it

连接时唯一未知的是中间那一行。服务器按守护进程的不同把它写进对端表、状态文件或流日志:WireGuard 称它为准入点,OpenVPN 的状态文件标它为 Real Address,strongSwan 则把它报告为 IKE_SA 的对端主机。只要有来自另一来源的数据包到达,这个值就会刷新,因此没人重启任何东西它也会变。

最下面一行是人们误读成身份声明的那个。它标记的是服务器配置里的一个槽位,而不是一个人,因为服务器是从自己的池子里选的。两个用户可能在不同日期拿到同一个隧道地址,一个人也可能从两台设备持有两个。

隧道保持在线时,记录的元组为什么会变

地址转换是带定时器的表,不是永久绑定。家用路由器在最后一个包之后还会保留一段 UDP 映射,运营商网关按自己的时间表回收端口,而一台从 Wi-Fi 切到 LTE 的手机会带着完全不同的源回来。这里 Linux conntrack 的默认值是有用的标尺:nf_conntrack_udp_timeout 是 30 秒,流式版本是 120,所以一条安静的隧道可能在用户察觉之前很久就丢掉映射。

隧道能存活,是因为协议认证的是对端而不是地址。WireGuard 在握手之后是无状态的,因此任何通过认证的数据包都会更新保存的准入点,重新绑定会在下一个包上自愈;这正是 persistent-keepalive = 25 存在的原因,为的是在空闲时让映射保持温热。OpenVPN 的 --float 做同样的事,让通过认证的客户端从新的地址和端口继续。在默认关闭 float 的 2.4 和 2.5 构建中,重新绑定会重置会话,客户端重新连接。

IPsec 用更多活动部件达到同样的效果。IKEv2 通过比较 NAT 探测载荷中源和目的的哈希来发现转换,切换到 UDP 4500 封装,并跟踪 keepalive 到新的元组。当 keepalive 被阻断时,SA 在纸面上仍然存在,而一个方向的流量悄然停止。

服务器能归因什么,不能归因什么

值得消灭的迷思是:转换让运营商看不到你。它改变的是标签,不是观测。运营商 NAT 后面的网关看到的是 100.64.0.0/10 空间,或许多用户共享的一个公网地址,仍然可以把这条流绑定到证书主体、WireGuard 公钥、被分配的隧道地址、字节计数器和时间窗口。这些都不需要你的 LAN 地址。

限制也朝另一个方向起作用。日志里的共享地址指向网关,而不是家庭;同一转换地址上不同端口的两个会话是两台设备;如果端口被回收,两个不同日期的同一地址可能是两个互不相关的用户。只记录对端密钥的服务器在没有账户数据库的情况下无法把它对应到客户,而只记录地址的服务器无法区分同一台路由器后面的笔记本和手机。

与其猜测,不如比较两端

要弄清发生了什么,就在同一时刻在两端抓同一个连接,比较元组而不是地址。在客户端,任何转换之前:

ss -uapn 'dport = :51820'
ip -4 route get 203.0.113.10
wg show wg0 endpoints

然后读取路由器或 CGNAT 网关上的转换条目,以及最终到达服务器的内容:

conntrack -L -p udp --dport 51820
tcpdump -ni eth0 udp port 51820

三种模式解释了大多数不一致。服务器上的源端口与本地端口不同而目的匹配:普通端口转换。服务器上的源地址属于不属于你的 ISP 段:至少多了一层 NAT。数据包在空闲期后停止,并以新端口恢复:映射过期,而不是路由故障。如果时间戳相差超过一两秒,你比较的就是两次不同的尝试。

差异在哪里把事情弄坏

看起来像服务器故障的中断通常从这里开始。固定在家庭地址上的访问规则 在 ISP 租约变化后失效。匹配转换地址的 fail2ban 规则会同时封禁该网关后面的所有用户。带固定远端地址的站点间对端在移动链路变化后停止续钥。隧道协议本身没问题;对外层元组的假设有问题。

同样的混乱也出现在服务器一侧。如果 VPN 盒子本身在 NAT 后面,就需要端口转发和可用的转发路径;strongSwan 转发指南 覆盖了 IPsec 的这种情况。只在私有地址上可达的服务器永远看不到客户端呈现的转换元组,这与客户端被转换是不同的问题。

按顺序完成这四项检查,并在第一个失败处停下:

  • 同时在两端抓包;确认源端口、目的端口和时间戳一致。
  • 读取服务器对端列表里它当前持有的地址,而不是你预期的地址。
  • 检查 conntrack 或 NAT 表中的条目及其剩余寿命。
  • 确认客户端上隧道接口的地址与服务器分配的地址一致,而不是 LAN 地址。

如果两次抓包在元组上一致而隧道仍然失败,那么转换不是你的问题;转而检查握手和隧道内部的路由表。

相关文章