ClearDeck 御流

工作原理

一条连接进来,一条规则做判断,判断结果被记下来。全部发生在你的手机上 —— 没有任何一步需要别的机器参与。

为什么必须申请 VPN 连接

Android 只给了应用一条正规途径去看其他应用的流量:VpnService 接口。御流申请它,然后用它当一个本地防火墙。

建立隧道之后,每个应用的连接都会先进入御流自己的 TUN 网卡,被判定,然后要么当场拒绝,要么原样直连出去。

关键在于转发这一步不存在 —— 御流没有远端服务器,也就没有地方可以把流量转出去。这与「用 VPN 去广告」的常见做法在架构上就是两回事。

两层判断,因为主机名不总是够用

拆成两层不是为了显得复杂,而是因为「能判断什么」本身分成两种:只看主机名就能定的,和必须看到完整地址才能定的。

域名层

主机名即答案

连接到 ad.doubleclick.net,这个主机名本身就说明了问题。域名层的规则就是一份域名清单 —— 不解密任何内容,命中即拒绝。内置 441,386 条。

代价:几乎没有。CPU 占用可以忽略,也看不到任何请求内容。

深度层

同一个域名下也要分路径

同一个主机名上,「正文页」和「广告位」往往是同一个域名。深度解析在本机解密连接、读出完整路径再判断 —— 所以 /article 放行、/promo 拒绝。内置 11,135 条地址规则,需要你手动开启并装一次证书。

代价:这是侵入性最强的一层,因为它要解密。所以它默认不开,且可以只对你选的应用生效。

一次判定长什么样

下面是内核在真机上打出的一条日志(只把应用包名换成了示例名)。它一次说清了四件事:

内核日志 · 真机
[TCP] 172.19.0.1:44142(com.example.app, uid=10123) --> www.google-analytics.com:443 match RuleSet(cleardeck-track) using REJECT
com.example.app, uid=10123
这条连接属于哪个应用 —— 这是「按应用」统计的来源
www.google-analytics.com:443
它想去哪里
RuleSet(cleardeck-track)
命中了哪个模块 —— 拦截构成那张环形图就是按这个字段归类的
using REJECT
判定结果。若是放行,这里会是直连出去

按应用归因是怎么做到的

内核被设置为始终解析连接归属的进程,每来一条连接,应用就向系统查一次这个 socket 属于哪个 uid。

这个查询不便宜,所以做了一层信号量节流 —— 这是仪表盘上每一格统计都能按应用拆开的原因。

认不出归属的流量不会消失也不会被算错,它们会显示为「未知来源」或 uid xxx。

两个进程,一个引擎

界面进程
Compose 界面跑在应用主进程里
引擎进程
内核跑在独立的 :tun 进程里
所以
界面崩溃不会掐断 VPN —— 防护还在跑,只是你看不到它
一个 Go 运行时
内核与深度引擎编译成同一个 libclash.so,因为一个进程装不下两个 c-shared Go 运行时

数字是怎么来的

拦截次数

来自内核的日志流,不是轮询快照 —— 所以统计不受采样间隔影响,一次都不会漏。

流量去向

按可注册域名聚合,同一个站点的子域会合成一条,CDN 子域不会碎成几十行。

已节省流量

这是估算:每条被拒绝的连接,按「同一个主机名在放行连接上的平均大小」计价;只被拦截过、从没成功连接过的主机,按每次 16 KB 估。

刻意不做的事

代价是多少

实测:调优后,30 分钟内引擎进程累计 2,067 jiffies = 20.67 秒 = 1.148% 单核;同一时段界面进程只有 19 jiffies。

调优后的采样周期

项目 改前 改后 省电模式
连接快照推送 1 s 3 s 15 s
通知刷新 3 s 5 s —
统计落库 15 s 30 s —
日志折叠 500 ms 1 s —
界面轮询 5 s 10 s —
内网扫描 10 s 30 s —

把连接快照从 1 秒放宽到 3 秒,是唯一值得牺牲的一项:每一次快照都要把所有活着的连接重新序列化成 JSON,界面看没看都在发生。放弃的是「生命周期短于一个周期的连接」的字节数与一条历史记录;长连接的字节数不受影响(累计值靠差值算),被拦截的连接完全不受影响(拒绝来自日志流,不走这个快照)。

上线时通知你

第一个版本正在收尾。留下邮箱,准备好时我们通知你一次 —— 只发这一封。

预约通道即将开放。

仅用于上线通知,不发推广邮件,也不会分享给第三方。