ClearDeck

How it works

A connection arrives, a rule decides, and the verdict is written down. All of it happens on your phone — no step needs another machine.

Why a VPN connection is unavoidable

Android gives an app exactly one supported way to see the traffic of other apps: the VpnService API. ClearDeck asks for it and then uses it as a local firewall.

Once the tunnel is up, each app’s connections enter ClearDeck’s own TUN interface first, get judged, and are then either rejected on the spot or sent straight out.

The crucial part is that forwarding does not exist. ClearDeck has no remote server, so there is nowhere for traffic to be forwarded to. Architecturally that is a different thing from the usual "VPN ad blocker".

Two layers, because a hostname is not always enough

The split is not decoration — it follows from what can actually be decided: things a hostname settles, and things that need the full address.

Domain layer

The hostname is the answer

A connection to ad.doubleclick.net announces itself. Domain rules are plain host lists — nothing is decrypted, and a match is rejected on sight. 441,386 of them ship in the app.

Cost: essentially none. Negligible CPU, and no request content is ever seen.

Deep layer

One hostname, many paths

On a single hostname, the article and the ad slot are often the same domain. Deep inspection decrypts the connection on your phone, reads the full path, and judges that — so /article is allowed and /promo is not. 11,135 URL rules, off until you turn them on and install the certificate once.

Cost: this is the invasive layer, because it decrypts. That is why it is off by default and can be limited to the apps you choose.

What one decision looks like

Below is a line the kernel actually printed on a real device (only the package name is swapped for an example). It settles four things at once:

kernel log · real device
[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
which app owns this connection — the source of every per-app number
www.google-analytics.com:443
where it was going
RuleSet(cleardeck-track)
which module matched — this field is what the blocked-breakdown donut groups by
using REJECT
the verdict. A connection that is allowed goes straight out instead

How per-app attribution works

The kernel is put into a mode where it resolves the owning process of every connection. For each connection, the app asks the system which uid owns that socket.

That lookup is not cheap, so it is throttled with a semaphore — this is what makes every figure on the dashboard splittable by app.

Traffic whose owner cannot be resolved is neither dropped nor misattributed: it shows up as an unknown source or uid xxx.

Two processes, one engine

UI process
the Compose interface runs in the app’s main process
Engine process
the kernel runs in a separate :tun process
So
a UI crash cannot cut the VPN — protection keeps running, you just cannot see it
One Go runtime
the kernel and the deep engine compile into one libclash.so, because a process cannot hold two c-shared Go runtimes

Where the numbers come from

Blocked count

from the kernel’s log stream, not a polling snapshot — so it is immune to the sampling interval and never misses one.

Traffic destinations

aggregated by registrable domain, so subdomains of one site fold together and CDNs do not fragment into dozens of rows.

Traffic saved

an estimate: each rejected connection is priced at what the same hostname averaged on allowed connections; hosts that were only ever blocked are charged a flat 16 KB per block.

What it deliberately does not do

What it costs

Measured: after tuning, the engine process accumulated 2,067 jiffies over 30 minutes = 20.67 s = 1.148% of one core, while the UI process used only 19 jiffies in the same window.

Sampling periods after tuning

Period Before After Power saving
Connection snapshot 1 s 3 s 15 s
Notification refresh 3 s 5 s —
Stats flush 15 s 30 s —
Log folding 500 ms 1 s —
UI polling 5 s 10 s —
Intranet scan 10 s 30 s —

Widening the connection snapshot from 1 s to 3 s is the one sacrifice worth making: every tick re-serialises every live connection to JSON whether or not a screen is watching. What it gives up is the byte count and one history row of connections that open and close inside a single interval. Long-lived connections are unaffected (cumulative counters, diffed), and blocked connections are entirely unaffected — rejections come from the log stream, not that snapshot.

Get notified when ClearDeck launches

We are finishing the first release. Leave an email and we will tell you when it is ready — once, and nothing else.

Signups will open shortly.

Only used to tell you about the launch. No newsletter, no sharing.