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.
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.
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:
[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
:tunprocess - 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
- No remote node, and nothing that could relay traffic.
- No root, no hosts file, no system partition changes.
- No WakeLock, no AlarmManager, no WorkManager, no JobScheduler, no boot receiver — the tunnel lives on a foreground service rather than a held wake lock.
- It does not collect the content of your traffic. Ever.
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.