被拦截的 Android 应用为何仍显示网络用量
用量计数器统计的是某一处边界上的字节数,而不是请求是否成功。请核对时间窗口、UID 归属,以及 VPN 服务归哪个应用所有。
你在 Android VPN 防火墙中拦截某个应用,应用停止工作,它的用量行却仍在攀升。诱人的结论是拦截漏了,或者计数器坏了。这两者都不成立。用量记录只说明在某个观察点上,字节被记在了那个身份名下;它并没有说远程服务是否接受了请求。一旦你知道被测的是哪处边界、字节被算在哪个身份上,这两种观察就不再互相矛盾。
计数器统计的是字节,不是送达
Android 把流量归属到某个 UID,并双向计数,而且是在接口边界处计数,而不是在应用请求成功的那一刻。
被拒绝的连接尝试同样会搬动数据包:SYN、重置或随后的重传,以及先于两者的 DNS 查询。即使什么也没送达,这些字节依然存在;在某些路径上,它们在任何过滤器做出决定之前就被计入。
这就是为什么在每个请求都失败时,被拦截应用的用量行仍会增长。计数器回答的是「有多少字节被记在这个身份名下」,而不是「这个身份是否到达互联网」。图表无法区分被接受的流与被礼貌拒绝的握手,把两个方向相加的总量也做不到。
归属跟着 UID,而不是图标
该行是按 UID 统计的,一个 UID 可能被多个包共用,而一个可见应用可能把工作交给以自身身份运行的独立组件。由共享 WebView 进程绘制的浏览器标签页、后台同步服务,或与主应用一同安装的配套包,都会产生用量界面可能以你意想不到的名字、或在你拦截的那个应用名下显示的流量。被拦截的包与承载其请求的辅助程序是两个不同的主体,而规则只覆盖了其中一个。
数字看起来不对时,比较 UID 而不是标签。用量 API 按 UID 查询,adb shell dumpsys package <package> | grep userId= 会打印用于比较的号码,所以两条 UID 相同的记录从计数器角度看就是同一个身份。
时间窗口可能早于你的规则
用量数据按桶收集并定期轮询,而不是实时流式传输。日总量可能包含你启用拦截之前的几分钟、上一小时的后台同步,或已经在途的流量。把累计总量与一次被拒请求相比、再把差额称为泄漏,这是测量错误,不是防火墙发现。
使用更短的窗口和干净的起点。记下修改规则的时刻,等过一个采集间隔,然后只看之后的数据。如果在规则之后开始的窗口里总量仍在上升,你就有值得调查的东西;如果不再上升,先前的增长只是被显示出来的历史。
VPN 服务可能并不是你配置的那个
VPN 防火墙只能看到它自己的服务所承载的流量。如果另一个 VPN 应用启动并接管了活动连接,你的过滤器就不再处于路径上,尽管它的接口仍留在设置里。按应用设置的绕过列表是故意这么做的,而排除某个应用的配置在该应用随后能正常工作时并不是泄漏。
IPv4、IPv6 与 DNS 也可能不一致。只处理 IPv4 的过滤器可能把 IPv6 或私有 DNS 解析器留在外面,于是应用的一部分流量走了你的规则从未描述的路径。在改写任何东西之前,先确认当前哪个应用拥有 VPN、排除列表又说了什么:
adb shell settings get secure always_on_vpn_app
adb shell dumpsys connectivity
adb shell dumpsys netstats detail
测试一次新的请求,并记录四项事实
选一个无害的请求,只做一次,记下四项事实:生效的规则、应用的 UID、网络状态和时间。然后检查你的过滤器是否记录了这条流,以及目的端或网关是否看到了同一个连接。两侧都能识别出的同一条流才是证据;一个汇总总量不是。
站得住脚的结论会指明被拒绝的是什么、又是哪一项观察确认了它。「计数器涨了」不是发现,「所有用量计数器都是假的」也不是。一句关于 OSI 层的口号解决不了这件事:诚实的答案是,计数器与拦截可以同时正确,只是它们在描述不同的东西。Android 的 NetworkStatsManager 参考文档记录了这些桶如何被查询与归属。