免 root 的 Android VPN:同意、路由与拦截到底归谁
同意、TUN 接口和系统拦截策略是三种不同的授权。在评估一款 Android VPN 应用之前,先弄清每种授权归属哪个组件。
让 Android VPN 应用能够承载流量、阻止泄漏或在重启后继续工作的,并不是 root。人们把这一切概括为„应用拥有 VPN 权限“,这种说法掩盖了三套彼此独立的机制:用户给出的同意、系统建立的报文接口,以及并不由应用定义的平台拦截策略。评估一个 Android VPN 设计,就是要追问某项声明究竟依托三者中的哪一个,因为三者的归属者和失效方式各不相同。
同意通过系统活动获得
VpnService.prepare() 本身不授予任何权限。缺少同意时,它把 intent 返回给系统同意活动,由应用弹出该对话框让用户批准服务。启动应用自己的进程、在清单里添加权限,或展示自制的„启用 VPN“界面,都不会产生这项授权。
这项授权还有范围。它属于某个用户配置文件中的某个包,因此个人副本与工作副本是两个独立决定,设备所有者也可以对受管配置文件作不同处理。如果对话框从未出现且 prepare() 返回 null,说明该配置文件里该包的同意已经存在;这不是应用可信的证据,只说明授权已就位。
接口是系统执行的请求
获得同意后,应用用 VpnService.Builder 描述自己的需求:地址、路由、DNS 服务器、MTU,以及(在支持的情况下)要纳入或排除的应用。establish() 请求系统创建隧道接口,并把匹配的流量导入其中。路由决策归系统所有;应用拿到文件描述符之后,必须对报文做出真正有用的处理。
| What you observe | Who actually owns it |
|---|---|
| VPN consent dialog | The system, triggered by prepare() |
| Routes, DNS and app lists | The app requests them; the system applies them |
| One active VPN per profile | The platform; a new service replaces the old one |
| Always-on and block-without-VPN | Device policy enforced by the platform |
这张表把架构集中在一处。应用参与报文处理,但并不拥有接口、路由表或可运行服务的数量。
隧道套接字必须逃出自己的隧道
承载加密流量的连接本身也是一个套接字。如果它被路由回应用刚刚创建的接口,隧道就会依赖隧道而卡死。VpnService.protect() 把该套接字绑定到底层网络,使其绕过捕获,正因如此,正确的客户端会在第一次握手之前保护自己的外层套接字。
adb shell dumpsys connectivity | grep -i -A5 vpn
adb shell settings get global always_on_vpn_app
adb shell settings get global always_on_vpn_lockdown
这些命令显示已安装的 VPN 网络、始终开启的包,以及是否设置了锁定。它们只是只读观察:如果取值与设计声称的不符,那项声明说的就是应用的界面,而不是系统的状态。
拦截可能属于平台
Android 记录了一项策略,用于阻止不使用 VPN 的连接。这种执行是平台契约,而不是应用在循环里反复重连。开关位于设备的 VPN 设置中,名为„阻止不经过 VPN 的连接“,在受管设备上设备所有者可以为整个配置文件开启它。评估营销口径时,这一区别很重要:客户端可以显示锁形图标并积极重连,而直连流量仍有可能;或者被覆盖的应用会因为系统执行锁定而保持离线。
锁形图标由应用自己绘制,因此它只能证明应用自认为已连接。另一个应用是否失去网络访问,由平台决定,所以有用的检查是在隧道开启时观察那个应用失败。
应用商店里的„始终开启“标签同样不能替代这项契约。始终开启与锁定是设备设置,常由设备所有者施加,其对被排除应用的行为由平台定义。要读设置,而不是看徽章。
设计评审的四个问题
第一,谁给出了同意:哪个包、哪个配置文件,是用户还是受管设备策略?第二,应用请求了什么:哪些路由、哪些地址族、哪些被纳入或排除的应用?第三,隧道不可用时会发生什么:系统拦截,还是流量回落到下层网络?第四,应用如何处理收到的报文?
第四个问题正是权限模型回答不了的。同意与 TUN 接口只能证明应用能收到流量,不能证明它会加密、会认证对端,或能干净地恢复。这些属于实现层面的属性,诚实的评审时间就花在这里。
不必要的 root 请求不会因为把结果叫作紧急开关就变得正当,同意对话框也不等于安全审计。把三种授权分开看,每项声明就都可核查。