起因:翻墙明明是好的,Google 就是连不上
事情的起点很简单:我的这台 OnePlus(ColorOS)手机上了 Root(KernelSU),日常用的是本机部署的 sing-box,TProxy 透明代理模式翻墙。浏览器、微信、Twitter、Instagram、YouTube,一切正常。
唯独 Google Play 服务、Google Play 商店、Gmail 这几个谷歌自家的东西,死活连不上。Play 商店刷新没反应,Gmail 收不到信,账号同步永远转圈。
第一反应是"代理没配好"——毕竟这几个应用平时就是翻墙圈里最容易被针对性拦截的对象。但排查过程中我顺手换了标准的系统 VPN(TUN)模式、也试了 App 里直接配 HTTP/SOCKS 代理,结果谷歌套件反而都能连上,只有回到 TProxy 透明代理才会出问题。这就不像是代理规则漏配了——规则是同一套,能不能连居然跟"用哪种方式接入代理"绑在了一起,更像是有什么东西在专门认这个。
第一个插曲:设计没问题,但撞上了没有 IPv6 出口的网络
先从最容易查的地方下手。翻出 sing-box 的运行日志,里面躺着一堆报错:
ERROR connection: connection download closed: remote error:
dial tcp [2607:f8b0:...]:443: connect: network is unreachable
再看路由配置,一条叫 googlefcm 的规则集被我设成了"直连"——这不是我配错了,是故意的:这条规则的域名走的是我自己在 Cloudflare Worker 上部署的一个专供 FCM 用的 DNS,只解析返回国内可直连的 IP,本意就是让谷歌推送服务不用绕代理,省点电和流量。
但这条规则集覆盖的不只是推送,还包含了 GMS 联网要用到的一批 googleapis.com 域名。日志里那些连不上的 IPv6 地址,根子在于我当时所在的网络环境压根没有原生 IPv6 出口——不管 DNS 解析出来的 IP 选得再"聪明",只要走的是 IPv6,这条网络里就是走不通,跟直连规则本身设计得对不对没关系。
把这条规则临时改成走代理,绕开了本地缺 IPv6 出口这个环境限制,日志清净了,报错消失了。
但谷歌套件依然连不上。 说明这只是个插曲,后面还有一个更深的问题在等着。
第二阶段:把能想到的嫌疑人一个个排除
这时候思路变成了"既然不是代理配置问题,那就是系统在拦"。于是把所有可能拦截网络的机制挨个查了一遍:
- Android 标准的省电策略 / Data Saver:查了对应 App 的联网状态,
effective=NONE,没被限制,排除。 - iptables 里代理软件生效的那条链:让手机实际操作一次 Play 商店,同时盯着 iptables 计数器——结果谷歌套件的 UID 命中数永远是 0,而同样在翻墙的 YouTube 能正常命中。这说明问题比"代理有没有生效"还要更早一步:这几个 App 压根就没有产生出站网络包,连尝试连接的动作都没有。
- OPPO 自带的流量监控 App:日志里确实看到了可疑的拦截广播,但把这个 App 强制禁用之后问题依旧——后来才想明白,真正做判断的逻辑很可能跑在系统底层服务里,禁用一个前台 App 根本关不掉它。
- Xposed 应用管控工具:翻了它的规则数据库,压根没有任何规则在管这几个 App,排除。
- App 自身的重试退避:强制杀掉全部谷歌进程重新拉起,全新进程照样是 0 网络包——证明这不是"缓存了一个坏状态",而是持续性的系统级拦截。
到这一步,“没有产生任何出站网络包"这个现象已经说明,拦截点比 iptables、比标准的 Android 网络策略都要更底层。常规工具全部失效——说明这压根不是常规工具能看到的地方。
真正的元凶:一张藏在内核里、连 iptables 都看不见的名单
Linux 内核有个叫 eBPF 的东西,可以在内核态挂一段小程序,在网络包甚至 socket 创建的那一刻就做判断——比 iptables 这种传统防火墙规则更底层、更早介入。而且这类程序可以把自己的数据结构"钉”(pin)在文件系统的一个特殊路径下,方便管理,但普通的排查工具完全不知道要去看这里。
抱着"死马当活马医"的心态翻了一下 /sys/fs/bpf/ 这个路径,发现一堆厂商自定义的东西,其中几个名字很扎眼:
map_oplus-netd_app_wlan_socket_uid_limit_map
map_oplus-netd_app_qcom_socket_uid_limit_map
翻译过来就是"给 WLAN/蜂窝网络用的、按应用 UID 限制 socket 的名单"。
手机上没有能读取这种数据的工具,于是从官方渠道下载了一份静态编译好的 bpftool(一个专门操作 eBPF 数据的命令行工具),推到手机上直接跑。dump 出这张表一看:
里面精确躺着 4 条记录,对应的 UID 分别是 Google Play 服务、Google Play 商店、Google 服务框架、GMS 配置更新器——一个不多一个不少,跟受影响的应用完全对上。
这就是真正的元凶:厂商在内核层维护了一张专门针对谷歌套件的限制名单,运行在比 iptables、比标准 Android 网络策略、比任何 Xposed 权限管理工具都更底层的地方,所以之前所有常规排查手段才会全部失效——它们压根看不到这一层。
把这 4 条记录删掉,不用重启任何进程,谷歌套件立刻恢复正常联网。
为什么只有翻墙时才会中招:一个合理但过于粗暴的判断逻辑
问题解决了,但还有一个疑问:为什么只有在用 TProxy 透明代理、或者连自带翻墙能力的 WiFi 时才会中招,用标准的系统 VPN 或者手动配置代理却完全没事?
我的理解是:厂商大概率是想做一件本身没错的事——如果手机根本没挂代理,那在国内环境下大概率是连不上 Google 的,与其让这几个服务在后台疯狂重试白白耗电,不如直接掐掉它们的联网权限来省电。
问题出在它判断"有没有代理"的方式太粗糙:它看的可能只是"系统层面能不能感知到一个正在运行的 VPN,或者一个显式配置的代理",而不是真的去探测网络是否可达。标准 VPN 会在系统里注册、状态栏会有小钥匙图标;手动配置的 HTTP/SOCKS 代理系统里也有据可查。但 TProxy 透明代理压根不建 VPN 接口、不改任何 App 的代理设置,它的整个设计目的就是"对上层完全无感";连了一个自带翻墙能力的 WiFi 也是同理,手机自己什么都没配置,只是连了个普通热点。
对系统来说,这两种情况看起来跟"完全没挂代理"一模一样,于是被一起误伤了。这个省电逻辑本身没毛病,但检测手段完全没考虑到"有一种翻墙方式,就是要让你系统层面看不出来"这件事。
把修复做成永久生效的模块
手动删一次很爽,但这张表大概率是开机时被系统服务重新写入的——一重启,限制又会回来。与其每次重启都手动跑一遍命令,不如做成一个开机自动执行的 KernelSU 模块。
写模块的时候踩了两个坑,都值得记一下:
第一个坑:UID 不能写死。 最初的修复脚本里直接把那 4 个 UID 数字硬编码了进去,但后来意识到——UID 是 Android 系统按应用安装顺序动态分配的,同一个包名在不同手机上、甚至同一台手机重装几次之后,对应的 UID 都可能不一样。写死数字的脚本换一台手机基本就是废的。后来改成运行时动态解析:读 /data/data/<包名> 这个目录的属主 UID(应用的数据目录归属者就是它的 UID,这是 Android 一直以来很稳定的行为),从包名一路推到 UID,再拼成 eBPF 要的那个 key 格式,全程不依赖任何写死的数字。
第二个坑:要不要一直挂着轮询。 一开始担心这张表会在手机运行期间被系统随时重新写入,所以模块设计成开机后一直后台常驻检查。但实测发现系统只在开机的那个时间点写一次,运行期间从不会再恢复。于是把常驻轮询去掉了,只在开机后的几分钟内高频检查一遍确保赶上系统写入的时机,然后就退出——不占用任何额外的后台资源。
模块现在整个是开源的:oplus-gms-ebpf-unlock
写在最后
这次排查前后最有意思的地方,是它逼着我一路往下挖:从"代理配置是不是错了",挖到"是不是被系统限流了",再挖到"是不是有什么工具都看不到的拦截层"——每往下一层,之前那些"标准答案"(省电策略、iptables、应用权限管理)就都被证明是错的,直到摸到内核里那张连 iptables -L 都看不见的 eBPF 名单。
如果你也在用国产 ColorOS/Oplus 系机型、Root 了手机、用的是 TProxy 透明代理这类"无感"翻墙方式,又发现谷歌套件死活连不上——现在你知道该往哪查了。