你在手机上用 chroot 跑起一个 Linux 发行版,在里面装好 VPN 客户端,shell 宣布隧道已经建立。可当你切到浏览器,流量照旧走移动数据,仿佛什么都没发生。另一种情形更糟:客户端在 Android 真正使用的命名空间里加了默认路由,于是手机上所有应用都断网。这两种现象源自同一处缺失,而且都不是 VPN 客户端的缺陷。

shell 说已连接,应用却不认账

第一种结果很常见。从 chroot 里打开的用户态隧道可以完成握手、打印统计,并 承载同一个 shell 自己发出的流量,而 Android 应用仍然使用手机默认网络。没有任何东西拦截它们,因为没有任何东西告诉 Android 存在一个 VPN。

第二种结果更危险。如果 chroot 与 Android 共享网络命名空间(默认就是如此),一个有足够权限的进程就能添加影响整台手机的路由和 TUN 接口。此时隧道对每个应用都有效,包括你并不打算走隧道的应用,而手机的上网能力就取决于一个没有生命周期管理、没有连接指示、也没有始终开启策略的守护进程。

你遇到的是哪一种,用一条命令就能判断。把 chroot 里的 ip route 与外部 Android shell 里的同一条命令做对比。输出相同,说明 chroot 处在宿主命名空间中:它做的任何网络改动都是整机范围的。输出不同,说明启动器为它创建了独立命名空间,它的隧道从结构上就对 Android 不可见。

chroot 改变了什么,又没有改变什么

ObjectEffect of a chroot
Pathname resolutionChanged: lookups start at the new root
Kernel and driversUnchanged: the phone's kernel, its TUN driver, its limits
Network namespaceUnchanged unless the launcher creates one
Android VPN registrationUnchanged: no app consented, no VPN is selected

chroot 系统调用只做一件事:改变路径解析所使用的根目录。手册页明确写着它不是安全边界,也完全没有提到接口、路由或 VPN 状态。人们期待 Linux 容器提供的那些东西——独立网络栈、被过滤的文件系统、资源限制——都来自其他机制,而启动器用没用这些机制则是另一回事。

所以,说 chroot 隔离了 VPN 的说法,通常回答的是错误的问题。真正该问的是:进程运行在哪个命名空间里,被限制在哪个 SELinux 域中。这两点取决于它如何被启动,而与 chroot 这个词无关。

TUN 需要三样东西,设备节点只是其中之一

传统用户态隧道需要能打开的 TUN 设备、内核对该设备的支持,以及配置接口和路由的权限。这是三项彼此独立的要求,仅在 /dev 下创建一个路径名,一项都满足不了:

ls -l /dev/net/tun
ip tuntap add dev tun0 mode tun

Android 的内核通常编译了 TUN 支持,因为平台自带的 VPN 服务要用它,但这并不意味着你的进程就能用。配置接口需要网络管理能力,而 SELinux 决定进程能否访问该设备节点。被拒绝时看起来像莫名其妙的权限错误,答案在审计日志里:

dmesg | grep avc
logcat -d | grep "avc: denied"

Root 解决不了这个问题。SELinux 对以 uid 0 运行的进程与对其他进程一视同仁,因此从 root shell 启动的 chroot 依然可能被拒绝访问它看得见的设备。为了让旧安装包跑起来而关闭强制模式,是影响整台手机、后果远超隧道的决定,也修不好一个过时的二进制文件。

Android 应用为什么无视你的隧道

Android 支持的模型是:VPN 应用通过平台的 VPN 服务注册,向用户请求一次授权,然后拿到属于自己的隧道的文件描述符。此后平台把流量导入该描述符,并执行用户或管理员选定的应用范围,所以同一套机制可以按应用、按配置文件生效,还能与始终开启、无 VPN 时阻断等设置组合。Android VPN 开发指南描述的就是这次握手,关键在于它是注册,而不是路由技巧。

chroot 里的守护进程从不注册。没有授权对话框,状态栏没有钥匙图标,没有按应用包含或排除的名单,也不参与始终开启策略。因此应用照旧使用默认网络,隧道只承载 shell 自己发出的流量。

反过来做也没用。把路由推进宿主命名空间会捕获一切,包括平台期望留在本地的流量,而且既没有按应用的控制,用户也无法看到或撤销它。并不存在某种中间设置,让 chroot 守护进程只为某一个应用路由流量。

二进制兼容是另一堵墙

即便网络这一侧有意思,可执行文件本身也有要求:CPU 架构、动态加载器、C 库,以及它期望的内核功能。Android 用的是 bionic 而不是 glibc,为桌面发行版编译的旧二进制可能需要设备上早已不存在的库。

把库文件名符号链接到新版本并不会带来 ABI 兼容,而十年前打包归档的加密库也不是仍在维护的组件。如果方案依赖这一点,这个方案就是在手机上以网络权限运行无人维护的代码。

在 Android 环境里验证,然后收尾

要从真正重要的那一侧测试你真正关心的东西:打开具体应用,访问具体的私有目标,确认是哪个解析器在应答,然后断开隧道,确认文档所述的行为。chroot 里终端的测试结果只能证明那个终端的执行环境,别的什么也证明不了。

如果实验过程中改过宿主路由,要改回去并记录改了什么。然后检查手机是否配置了始终开启的 VPN,因为遗留了一条规则的旧实验,几个月后会看起来像平台故障。

对于真实的远程访问需求,请使用服务商支持的客户端,或经过批准的网关设计。值得问的问题不是能不能让一个 root 安装包跑起来,而是受支持的实现能否把你需要的应用送过你设想的边界,同时不在设备上留下无主的特权改动。

相关文章