热点客户端的分离隧穿不等于按应用排除
把应用排除在 VPN 之外只改变手机本身。通过热点连接笔记本时,需要一条按来源、目的与 NAT 匹配的独立转发规则。
两个听起来等价的要求并不等价:「把这个应用排除在 VPN 之外」与「把我热点上那台笔记本的这个目的地址放到 VPN 之外」。前者改变的是手机上的套接字如何选择网络。后者必须改变的是,手机已经接收之后来自另一台设备的报文该走哪条路。把两者混为一谈,热点客户端就会得到一个只对手机自带浏览器生效的例外。
两个问题由两个子系统负责
Android 把应用联网与网络共享分开管理。应用套接字由连通性栈分配到某个网络,而 VPN 应用决定自己的哪些套接字进入隧道。来自已连接客户端的转发报文由网络共享子系统处理,它会向 netd 询问路由、策略规则以及下行接口上的转换。
按应用排除只在应用打开套接字时被评估。转发报文永远不会走到这项检查,因为它并非源自本机上的应用套接字。这正是两种设置无法互相替代的全部原因,而这也不是任何一方的缺陷。
按应用排除究竟改变了什么
VpnService 的构建器可以用 addDisallowedApplication 指定留在隧道之外的软件包,或用 addAllowedApplication 指定唯一允许进入隧道的软件包。这些应用随后在底层网络上打开套接字,而 VPN 继续为其他一切保有默认路由。
作用范围很精确:该软件包自己的套接字,在本机上,且仅在隧道建立期间。它不涉及转发的流量,不涉及第二台设备,也不涉及系统替网络共享发出的报文。
它也不是目的地址控制。排除一个浏览器,会把它能到达的每个地址都排除掉,包括你想留在隧道里的那个。当需求指明的是 目的地址而不是应用 时,应用选择器从结构上就是错的工具。
共享报文在手机看来是什么样
客户端报文到达热点接口时,带着手机分配的私有源地址,比如 192.168.43.20,以及一个公网目的地址。要转发它,手机需要一条指向上行链路的路由,以及一条转换规则,好让回包找到来路,因为客户端的私有地址在蜂窝网络里毫无意义。
Android 用选择路由表的策略规则来表达这一点,当网络与 VPN 变化时,netd 会重建相关表和规则。从论坛帖子或同一机型的旧版本里抄来的表号、规则优先级或接口名,并不能建立持久的归属。也正是这种重建,让手写的规则可能只撑一小时,就在下一次重连后失效。
热点例外真正需要的是什么策略
在动任何规则之前,先把决策写成来源、目的和预期路径。一个已批准目的地址的最小形态是这样的:
| Source | Destination | Intended path |
|---|---|---|
| One authorized hotspot client | One approved public service address | Ordinary cellular uplink |
| The same client | All other covered destinations | Shared VPN |
| The phone's own apps | Their existing destinations | Unchanged phone policy |
两个方向都必须完整。直连分支需要一条经由当前蜂窝上行链路的可用路由;VPN 分支需要隧道路由加上转发许可。任何源地址转换都必须与所选的出口一致,因为应用在隧道接口上的 NAT 呈现的是隧道地址而不是蜂窝地址,回程就会与去程不一致。
回包必须通过被跟踪的状态或显式路由回到同一个下行客户端,而出向报文计数器证明不了其中任何一点。VPN 自身的外层传输也要留在例外范围之外,否则隧道的承载报文会继承一套为客户网页请求写下的策略。
抓到的一个地址不是目的地址
从浏览器读到的单个 IPv4 地址只是一个快照。大型服务会用任播或一组轮换的边缘地址作答,很多还是双栈,一次登录流程可能碰到页面从不显示的认证与遥测主机。围绕你碰巧看到的那个地址构建的例外,会在服务下一次重新平衡时失效。
使用服务方文档化的地址,或它公布的一段范围,并把两种地址族都算进去。如果需求无法表达为一个稳定的集合,那多半是一个面向服务的策略问题,而不是手机上的路由技巧。
从下行一侧验证
按这个顺序做三项检查:笔记本访问被选中的服务、一个必须留在 VPN 内的普通目的地址、以及一个必须不受影响的手机应用。然后在重连之后和上行链路变化之后再重复一遍,因为那正是策略表被重建的时候。
如果手机的共享实现无法在重连之后维持这份约定,就把选择性策略移到受支持的下行路由器上,而不是在手机上堆积 root 规则。路由器可以用稳定的归属来表达来源与目的匹配,而 网络共享模块的文档 才是关于手机究竟在做什么的正确参考。