当模拟器无法访问一个在宿主机浏览器里能正常打开的构建服务器时,有用的问题不是 VPN 是否工作,而是虚拟设备缺了两种机制中的哪一个。虚拟设备在模拟器 NAT 之后运行自己的网络栈,而它的解析器列表是启动时取的一份快照,所以之后才建立的隧道只改变宿主机侧的路径,并不改变虚拟设备所询问的对象。理解这两层,几乎能解释这个问题的所有变体。

虚拟设备是固定 NAT 之后的独立网络栈

QEMU 的用户模式网络把虚拟设备放在 10.0.2.15,网关在 10.0.2.2,DNS 服务在 10.0.2.3 到 10.0.2.6。虚拟设备的出站流量不由宿主机内核转发,而是被转换成宿主机侧的套接字:每条连接一个,从宿主机的地址空间发出。正因如此,虚拟设备的连接在创建的那一刻就继承了宿主机的路由决策;也正因如此,虚拟设备的路由表中没有任何指向你的隧道的条目。

同一机制也给虚拟设备留了一个有用的出口。10.0.2.2 是宿主机回环接口的别名,因此笔记本上监听 127.0.0.1 的服务,可以从虚拟设备通过该地址访问。客户端只在回环上暴露的东西都能这样访问,这也是下文所述代理方案的基础。

虚拟设备做不到的,是把宿主机网络当成自己的命名空间来观察。这里没有网桥、没有共享路由表、也没有继承来的解析器,只有接收其数据包的 NAT 服务,以及启动器在虚拟设备启动时拍下的那份快照。

DNS 是快照,不是实时视图

启动时模拟器读取宿主机的解析器列表,并把最多四个服务器交给虚拟设备,这些服务器以 10.0.2.3 到 10.0.2.6 的地址暴露出来。如果那一刻隧道是断开的,快照里装的就是笔记本当时使用的公共解析器,而虚拟设备终其一生都在问它。公共域名能解析,内部域名失败,所有症状都指向 VPN,而真正陈旧的值其实是隧道建立前几分钟抓取的解析器列表。

因此顺序修正并非迷信。先连接隧道,再重启模拟器,并且优先选择冷启动而不是恢复快照,因为快速启动镜像可能带着之前保存的解析器与路由状态回来。如果你需要看虚拟设备当前在用什么,去问设备,而不是问宿主机:

adb shell dumpsys connectivity | grep -i -A2 dns
adb shell ping -c 1 build.internal.example

模拟器在启动时也接受显式指定的解析器,当你知道虚拟设备应当使用哪台服务器时这很有用,而如果那台服务器是公共的就很危险:

emulator -avd Pixel_API_34 -dns-server 203.0.113.53

公共解析器没有内部区域的副本,所以把它钉死会把顺序问题变成永久故障。如果配置文件推送私有 DNS,就交给隧道来提供,并冷启动虚拟设备,而不是把地址硬编码。

路由在连接建立时跟随宿主机

由于虚拟设备的连接会变成宿主机的套接字,路由决策在套接字创建时做出。请求之前隧道已经建立,意味着请求会按配置文件的路由经隧道发出。只承载选定前缀的分流隧道配置会乐意把你的私有子网留在外面,所以宿主机可达性只是关于虚拟设备的一个假设,而不是结论。已存在的连接会保留创建时的路径,这就是为什么重新连接的客户端并不总能修好模拟器里卡住的下载。

验证要在宿主机接口上做,而不是看虚拟设备的统计:

sudo tcpdump -ni tun0 'host 203.0.113.10'

如果虚拟设备的请求出现在隧道接口上,说明路由正常,问题在更上层。如果它出现在物理接口上,说明配置没有覆盖该前缀,虚拟设备侧再怎么改配置也改变不了。

当客户端只提供回环代理时

一些商业客户端根本不创建系统隧道。它们在 127.0.0.1 上暴露 HTTP 或 SOCKS 监听器,并期望应用来使用。虚拟设备看不到那个套接字,但可以让模拟器改用这个代理:要么通过扩展控制面板(Settings,然后是 Proxy,手动模式,主机 127.0.0.1 以及客户端打印出的端口),要么在启动时指定:

emulator -avd Pixel_API_34 -http-proxy http://127.0.0.1:8080

端口请从客户端窗口复制;猜出来的端口会静默失败,看起来像隧道坏了。要记住它覆盖什么:HTTP 代理设置会被识别代理的应用遵守,而原始套接字完全忽略它。如果负载是自己打开连接的原生二进制程序,代理路径并不等价于隧道,比较必须显式进行。

在虚拟设备内部证明结果

在同一次会话里用两个测试把解析和路由分开。数字地址能响应而域名失败,是 DNS 快照问题,解法是顺序或显式解析器。两者都失败则指向路由或策略,下一项检查是应用是否运行在带有自己网络限制的工作配置中。

最后,要清楚你测的是哪个虚拟设备。非常古老的模拟器镜像出厂时不带虚拟设备侧 VPN 客户端所需的 tun 支持,所以那里任何配置都不可能工作;当前镜像则包含该支持。在当前镜像上失败的客户端,是客户端或配置的问题,重建宿主机隧道不会改变结果。

相关文章