Vigil
See which app on your phone is talking to whom.
On-device DNS monitor that attributes every lookup to the app that made it.
- Platform
- Android 10+ (min SDK 29)
- Approach
- Local VPN, DNS traffic only
- Behaviour
- Audits, never blocks
- Architecture
- Multi-module Gradle, Hilt
- 90,000+
- tracker domains bundled
- 76
- unit tests
- 512
- app lookups cached
- Android 10+
- minimum version
In the app
What it is
Vigil sets up a local-only VPN tunnel, reads the DNS queries your apps send, and shows which app looked up which domain, flagging the ones that match a bundled list of tracker domains. Everything happens on the phone. It never blocks anything: it logs each lookup, forwards it to a normal DNS resolver, and gets out of the way.
- 01
An app asks for a domain
Before any app can reach a server, it has to look the name up over DNS.
- 02
Vigil's tunnel catches the lookup
The VPN only routes a sentinel DNS address (10.0.0.2) into the tunnel, so just port 53 queries come through. The rest of your traffic never touches it.
- 03
It works out who asked
The query is mapped back to the app that sent it, with a 512-entry LRU cache so repeat lookups cost almost nothing.
- 04
The domain gets a verdict
A reverse-label suffix trie holding 90,000+ domains checks it in time proportional to the name's length, and the result is tracker or clean.
- 05
Logged, then forwarded
The log is batched into Room, 50 records at a time or every 2 seconds. Meanwhile the query goes to your upstream resolver through a protected socket that bypasses the tunnel, and the answer is handed back to the app.
What it does
Live feed
A searchable stream of every lookup, filterable to trackers only or safe only, with the app that made each one.
Per-app stats
See which apps are the chatty ones, and how many of their lookups were trackers.
A private DNS warning
Encrypted DNS (DoT or DoH) skips the tunnel entirely, so Vigil detects it and shows a banner instead of silently missing traffic.
Themes
System, Catppuccin, Gruvbox, Nord, Dracula and more, set in Geist type.
How it works
- 1
App
DNS query, UDP 53
- 2
Local VPN tunnel
10.0.0.2/32 sentinel
- 3
Packet engine
hostname, txid, UID
- 4
Suffix trie
tracker or clean
- 5
Batched log
Room, 50 or 2 s
- 6
Upstream DNS
protect()ed socket
- 7
Answer back
to the app
Questions
- What does Vigil do?
- Vigil sets up a local-only VPN tunnel, reads the DNS queries your apps send, and shows which app looked up which domain. Domains that match a bundled list of 90,000+ known trackers are flagged.
- Does Vigil block trackers?
- No. It audits only. It logs each lookup, forwards it to a normal DNS resolver and gets out of the way.
- Does Vigil route all my traffic?
- No. Only DNS queries enter the tunnel, so everything else bypasses it. Vigil never sees the content of your other traffic, and nothing leaves the phone.
- Where does Vigil's tracker list come from?
- Vigil bundles StevenBlack/hosts, an open-source hosts list of about 90,000 domains maintained by its community. I didn't compile the list; Vigil's job is matching every DNS query against it quickly.
- What does Vigil need?
- Android 10 or newer (min SDK 29). It does not need root.
Under the hood
Only DNS goes through the tunnel
Rather than capturing everything, Vigil routes only the sentinel DNS address into the VPN. That keeps it light on battery and speed, and it means the app never sees the contents of your other traffic.
Packets decoded by hand
Vigil reads bytes straight from the tunnel's file descriptor and decodes the UDP header and DNS question itself, pulling out the hostname and the transaction ID, so it can rebuild the response for the app afterwards.
A trie that reads domains backwards
Domains are stored label by label from the right, so ads.tracker.example.com is walked as com, example, tracker, ads. One pass costs the length of the name, not the size of the list, and a match on example.com automatically covers every subdomain under it, with no database query.
Audit, never block
Vigil logs a verdict for each lookup but always forwards the query, so your connectivity stays exactly as it was. It's a way to see what's happening, not a firewall.
Writes that never stall the UI
Logs sit in a memory buffer and are flushed to Room in one go when it reaches 50 items or after 2 seconds, guarded by a mutex. The main thread never waits on the disk.
Honest about its limits
If Private DNS is on, lookups bypass the tunnel. Vigil checks for that and says so, instead of looking quiet and implying nothing is happening.
Strict module boundaries
The code is split across Gradle modules (app, feature, vpn, data, a Room database module, and a pure-JVM domain module with zero Android dependencies), so the networking core can be tested without a device.
Stack
- Language
- Kotlin
- UI
- Jetpack Compose, Geist fonts
- Dependency injection
- Hilt
- Networking
- VpnService, protect()ed upstream socket
- Classification
- Reverse-label suffix trie, ~90K hosts
- Tracker list
- StevenBlack/hosts, an open-source list (not mine)
- Caching
- LRU cache (512 entries)
- Storage
- Room
- Concurrency
- Coroutines, mutex-guarded buffer
- SDK range
- Min 29, target 36
Written about
More from the same desk



