系统托盘图标上看不出两种故障的区别:流量进入隧道,和流量悄悄绕过隧道。两种情况下客户端都显示 Connected,因为 Connected 只说明隧道接口存在、客户端装好了它想装的路由。某个应用的某个包有没有用这些路由,是另一个问题,答案在协议栈的其他位置。四项测量能定下大半——路由查询、第一跳、物理网卡抓包和外部观察——服务器自己的会话视图覆盖其余部分。

从数据包会走的路由开始

路由表就是决策本身,所以问它某个目的地,而不是通读全表去猜。在 Linux 上,ip route get 会给出内核选定的接口和源地址:

ip route get 203.0.113.10
ip route get 10.20.5.7

在 Windows 上,Find-NetRoute -RemoteIPAddress 203.0.113.10 比 route print -4 更有用,原因相同:它报告的是胜出的那条路由,而不是全部现有路由。不过还是要读一遍表,看它的 0.0.0.0 行。那里的 /1 对说明全隧道客户端重写了默认路由:OpenVPN 的 redirect-gateway def1 选项通过隧道安装 0.0.0.0/1 和 128.0.0.0/1,在最长前缀匹配上压过已有的 0.0.0.0/0,而不删除物理默认路由。所以两条默认路由可以同时存在,而一切照旧进入隧道。

两个细节会让路由表说谎。第一,看匹配到的前缀,而不是默认路由的条数,因为接口跃点或 /1 对决定胜负。第二,已存在的连接会保留建立时的路径;隧道起来后,一个打开的会话可能继续走旧路由,直到它被重置,所以重连看起来像修好了路由问题。客户端在跑——ps ax | grep openvpn——只证明进程启动了。

跟踪一个内网名称和一个公网名称

路由查询表明意图;路径表明投递。连上之后,跟踪一个必须是内网的名称和一个必须不是的名称,比较第一跳:

traceroute intranet.corp.example
traceroute 1.1.1.1

两者第一跳相同,说明内网名称根本没进隧道,故障在它上面,通常是一个解析到公网地址的名称,或一条从未安装的路由。如果内网流量从 10.8.0.1 出去、公网流量从 192.168.1.1 出去,分流隧道正是按配置在做事。跟踪到 VPN 网关之后只返回超时,通常是回程路径或服务器防火墙的问题,而不是缺少转发路由。

被过滤的 ICMP 会让这项检查说谎:两条跟踪上都是星号,看着像隧道死了,通常是防火墙。能测 TCP 端口就测 TCP 端口——Linux 上是 traceroute -T -p 443——并记住跟踪不说明是哪个解析器应答的:名称可以在本地解析,同时仍能通过隧道访问。

在物理网卡上抓包,而不是在隧道上

物理链路是唯一能显示网络真实所见的地方。隧道起来后,去办公室的会话应当显示为发往 VPN 服务器的加密载荷,而不是到 10.20.5.7 的连接:

sudo tcpdump -ni eth0 udp port 53
sudo tcpdump -ni eth0 host 10.20.0.5

第一个过滤器抓的是本地发出的名称解析,这是最常见也最安静的泄漏——页面照样打开,只是解析器看到了查询。第二个抓的是去往本应只存在于隧道内的内网地址的普通流量。在 Windows 上,pktmon 不用驱动就能做同样的事:pktmon filter add -p 53,然后 pktmon start --capture --pkt-size 128,然后 pktmon stop 和 pktmon format pktmon.etl -o capture.txt。这些标志在不同 Windows 版本间略有差异,所以信任语法之前先查 pktmon filter add /?。

抓物理网卡是为了找泄漏;在隧道上抓到的,是本来就相信在那里的包,在服务器上抓到的,是源为网关的不可读外层包。任何接口的字节计数器描述的是包,不是程序;把流量归到某个进程,需要把套接字映射到进程的监视器。

问一个外部服务,再按应用分别问一遍

唯一能证明出口的检查来自外部:一个把你看到的源地址回显给你的服务。在能选择接口的 shell 里跑它,并且跑两次:

sudo curl --interface tun0 https://example.net/ip
sudo curl --interface eth0 https://example.net/ip

答案不同,就有了两条路径的证据,也有了测试某个具体应用的办法:把它绑定到接口,或只带一条路由的网络命名空间里运行,再看远端记录到哪个地址。答案相同才是值得注意的故障:要么绑定没生效,要么主机与远端之间有东西在包到达前重写了源地址。在 Windows 上,curl --interface 要的是网卡地址而不是名字。

报告 VPN 地址的浏览器只证明了浏览器。邮件客户端、包管理器、备份代理或第二个浏览器配置文件都可能走别的路,尤其是当代理设置或分流路由覆盖了其中某一个。地址变化本身不是泄漏证据:负载均衡器可能按看到的出口给出不同答案,所以要比对记录的源地址,而不是页面。

用服务器视图确认对端

服务器回答的是另一个问题:某个对端是否连着、搬运了多少、会话有没有重启。服务器上对端在线,而客户端计数器几乎不动,说明应用流量并没有进入隧道,客户端路由表说什么都一样。而一个为用户搬了大量数据、用户却说完全不通的对端,是另一场调查。

这里有两点陷阱。做了伪装的隧道会让每条流看着都来自网关,于是日志和按客户端计费都塌缩到一个地址上。而客户端被当作本地主机,所以有没有人通过 VPN 拷贝文件这个问题,要由服务器上的文件共享流量或服务器上的主机审计来回答——观察隧道只给出端点和流量,给不出文件名。

把设计决定和泄漏分开

隧道之外的东西并不都是泄漏。链路本地和广播流量按设计留在本地,通过多播找到的打印机继续在 LAN 上宣告,DHCP 必须从本地出去。绑定到指定接口的应用是有意绕过路由表,只有防火墙规则、网络命名空间或策略路由能改变这一点。

IPv6 值得在清单上单独占一行。只承载 IPv4 的隧道会给双栈主机留下第二条可用路径,双栈目的地会优先选择它——如果解析器可通过 IPv6 访问,DNS 也一样。在把 IPv4 结果当作完整结论之前,要么关掉网卡上的 IPv6,要么确认隧道承载了它。

当各项检查不一致时,不一致本身通常就点出了故障:

What you seeWhat it meansNext step
The route picks tun0 but the capture shows cleartext to 10.20.5.7Something bypassed the table: a bound app, a policy route or a session older than the tunnelReset the flow, check the app bind, check the ip rule list
The route picks eth0 for an internal nameThe name resolves to a public address, or the route was never installedCompare the resolved address with the routes the server pushes
The capture shows DNS to the LAN resolverResolver leak: data is tunneled, lookups are notPoint the resolver inside, or push it from the server
All four checks agree and one app still failsPath selection is fine; suspect MTU, a per-app proxy or the far endTest the same destination from a known-good client

一个可行的顺序:在连接前后各保存一次路由表,跟踪一个内网和一个公网名称,在物理网卡上抓 DNS,从 shell 里针对具体应用回显出口地址,然后读服务器看流量。如果前四项一致,隧道承载的就是你以为的东西,剩下的问题在它上面。如果有两项不一致,不一致的那一对就是故障,而不是图标。

相关文章