Open-source project
pass-with-high-score/blockads-android avatar
pass-with-high-score/blockads-android

BlockAds puts a DNS filter and an HTTPS inspector inside your own Android VPN

Block ads system-wide on Android using local VPN-based DNS filtering. No root needed. No data collection.

2,147 stars123 forksKotlinGPL-3.0

At a glance

What is it?
BlockAds runs a DNS blocklist inside a local Android VPN and can also terminate TLS in Go to inject cosmetic rules into chosen browsers. It works without root, but anything cert-pinned stays outside its reach.
Who is it for?
BlockAds fits anyone who wants ads and trackers stopped without handing DNS to a paid resolver and without root, because the local VpnService path plus a 284-domain passthrough list keeps the blast radius small and visible in the query log. It is the wrong tool for traffic that pins certificates, and for anyone who needs HTTPS inspection of an app rather than a browser.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two routing paths converge on one DNS Trie

BlockAds gives Android two ways to intercept DNS. The default path is a VpnService, so the app owns routing inside the VPN slot Android already reserves, and no root is needed. The second path, Root Proxy Mode, redirects queries through iptables instead, which only works on a handset that is already rooted. Both end in the same place: queries are matched against loaded filter lists through a memory-efficient Trie, a matching query is dropped on the device, and everything else goes back out the normal network path.

The lists that Trie consults are bundled rather than fetched on first run. StevenBlack, AdGuard DNS and EasyList are named, and the defaults are region aware, so the app enables filters for the language it detects. Custom block and allow rules sit on top of them, with whitelisting for a single domain. Every lookup is written to a real-time query log you can search and filter, which is what turns a page that failed to load into a name you can look up.

Upstream resolution is a separate decision from the blocklist. DNS-over-HTTPS is supported against multiple providers, so clean queries can leave the device over an encrypted channel while blocked names are still answered locally. Filter lists refresh on a schedule chosen from 6h / 12h / 24h / 48h. No account is involved anywhere in that path, and settings move between phones through the Export / Import backup the app ships.

HTTPS filtering terminates every flow in Go

HTTPS Filtering is the part of BlockAds that reaches past DNS. With it enabled, a userspace TCP/IP stack built on gVisor netstack through tun2socks terminates each TCP and UDP flow inside the Go tunnel instead of letting the kernel route it. For every flow the tunnel asks Android who owns the connection by calling `ConnectivityManager.getConnectionOwnerUid()`, which turns a socket into an app identity, and only flows from browsers you selected are decrypted. Other apps keep their traffic untouched.

Two escape hatches cover traffic you did not mean to break. Cert-pinned apps refuse interception and pass through, and so do destinations on the 284-domain curated passthrough list. The rewriting happens on the response side: cosmetic CSS rules are applied to HTML, and EasyList `##+js(…)` and AdGuard `#%#//scriptlet(…)` scriptlets are injected through a tiny in-memory local asset host on the device, so injected assets are served out of RAM rather than fetched from anywhere.

What is missing there is the certificate story. The feature list records CA cert install verification that auto-refreshes after returning from Settings, which is the shape of a flow where the user installs the certificate by hand in Android settings first. No per-version policy for user-installed CAs is documented anywhere, and the app declares a min SDK of 24, so behaviour across that range rests on platform defaults the project leaves unstated.

The 284-domain passthrough list is shield and ceiling

At DNS resolution there is one failure mode no setting fixes, and BlockAds answers it with a hand-kept list instead of a mechanism. When a banking, payment or government app resolves a hostname that sits on a tracker list, the app breaks, because a blocked answer looks exactly like no network at all. The curated passthrough list of 284 domains is the escape hatch: those names skip interception whatever the filter lists say, which is why the same 284 reappears in the HTTPS filtering description as a MITM bypass.

Per-app filtering covers the other half of the problem. Selected apps bypass the VPN entirely, so an app that insists on doing its own resolution never meets the Trie. A Quick Settings tile and a home screen widget give it a way back without opening the app, and auto-reconnect on boot means filtering survives a restart.

Where those 284 names come from is not documented. There is no stated refresh interval for the passthrough list in a project that is otherwise precise about the 6h / 12h / 24h / 48h cadence for filter lists, and no criterion for adding an entry. A hostname that turns into an ad host after it was first catalogued, or an app resolving through a CDN address nobody curated, sits outside the list, and unmatched queries pass through by design.

Building it, from a blobless clone to a pinned tunnel AAR

BlockAds is on F-Droid under the package name `app.pwhs.blockads` and on IzzyOnDroid at `apt.izzysoft.de/packages/app.pwhs.blockads`, so for most people a store is the install path and the build section is for people changing code. That section opens with repository size rather than setup: old `tunnel.aar` binaries were committed before they were moved out of the tree, so an ordinary clone drags roughly 1.2GB of history behind it. A blobless clone is offered instead, which comes to about 20MB.

bash
git clone --filter=blob:limit=1m https://github.com/pass-with-high-score/blockads-android.git
cd blockads-android

A shallow clone is offered as the alternative. From there the stated requirements are Android Studio Ladybug or newer, JDK 17 or higher, and Android SDK 36 with a min SDK of 24, with Go 1.23 and gomobile marked optional and needed only if you modify Go code under `tunnel/`.

Rebuilding the tunnel is an explicitly optional step, and the default is to take a prebuilt, checksum-pinned `tunnel.aar` instead:

bash
./scripts/build_tunnel.sh
# Or
./gradlew -Ptunnel.source=local buildGoTunnel

Packaging is an ordinary Gradle run, and the signed variant wants a key of your own:

bash
./gradlew assembleDebug
./gradlew bundleRelease   # requires signing key

The GPL-3.0 grant lets a fork ship its own build under the same terms, and store copy, icon and screenshots sit under `fastlane/metadata/android/en-US/images/`, which is the first place a fork would edit its listing.

Two version streams, one page, different cadences

The version side of this project runs on two separate streams, and the last push on 2026-10-06 landed on the same day as the v6.9.0 tag, so one calendar day produced two differently numbered artifacts.

The app moves on a semver tag. The tunnel moves on a dated tag with a commit hash appended: tunnel-2026.10.06.48a66545 on 2026-10-06 and tunnel-2026.10.05.65949475 on 2026-10-05 both sit in the three most recent entries, next to v6.9.0. That split matters for anything watching the page automatically, since a version query restricted to v* never returns the tunnel builds. The README's own version badge makes the restriction visible, filtering on `?filter=v*`.

For an operator this means the Android app and the Go network layer move independently, so the piece that terminates and rewrites traffic can be replaced on a day when the app does not change at all. The root of the tree also keeps dated self-review artifacts, `ISSUES_REPORT_2026-06-11.md` beside `ISSUES_ANALYSIS.md`, the sort of file left behind by a project that reads its own issue tracker on a schedule.

A 100 percent local claim beside opt-in telemetry

The feature list ends on a flat claim: 100% local, all data stays on your device, and the How It Works paragraph repeats that no data leaves your device. Two entries above that line describe something else entirely. Privacy-first Optional Crash Reporting and Manual Local Logs Export put a path for data to leave, and the onboarding flow includes an Opt-in telemetry step next to the permission and battery optimization screens.

Set side by side, those lines mean the filtering engine is local while the diagnostics are not local by default behaviour. Crash reporting is opt-in, so nothing is transmitted unless you agree during onboarding, and the manual export is a file you choose to write and share yourself. The claim holds for DNS resolution and HTTPS inspection, both of which run on the device, and stops at the diagnostic boundary. No reporting service or endpoint is named anywhere, so what a crash report would carry cannot be read out of the documentation.

What the tree reveals, and the two alternative approaches

The repository root shows why this is not a pure Kotlin project. Next to `app/`, `build.gradle.kts` and `settings.gradle.kts` sit `tunnel/` for the Go network layer, `rules/` for shipped rule data, `scripts/` for the tunnel build, `docs/` for the documentation the build section points at, and a `blockadstv/` module the README never describes. Store metadata runs through `metadata/` and `fastlane/` with a Gemfile beside it, `.run/` holds editor run configurations, and `AGENTS.md`, `GEMINI.md` and `.agents/` sit at the root as tooling files rather than app code.

That layout matters because the alternative path is visible in the feature list too. WireGuard Profile Import is the opposite approach in a single control: instead of matching queries locally against a Trie, the profile sends resolution to an endpoint outside the device, and BlockAds becomes a front end rather than the filter. Root Proxy Mode is the other alternative, trading the Android VPN slot for iptables redirection on a rooted phone. The README does not document which WireGuard providers the import accepts, nor whether locally defined block and allow rules still apply when the query never reaches the handset.

Editorial conclusion

BlockAds fits anyone who wants ads and trackers stopped without handing DNS to a paid resolver and without root, because the local VpnService path plus a 284-domain passthrough list keeps the blast radius small and visible in the query log. It is the wrong tool for traffic that pins certificates, and for anyone who needs HTTPS inspection of an app rather than a browser. Before adopting, read `docs/TUNNEL.md` to see what the Go tunnel terminates, and check the 6h / 12h / 24h / 48h refresh cadence against your own connection.

Frequently asked questions

Does BlockAds need root on Android?

No. Vpn Mode intercepts DNS through Android's VpnService, and the project states that no root is required. A separate Root Proxy Mode uses iptables redirection and does need an already rooted device.

What does HTTPS Filtering in BlockAds actually intercept?

Only flows from browsers you select, after the tunnel resolves the owning app through `ConnectivityManager.getConnectionOwnerUid()`. Cert-pinned apps and destinations on the 284-domain passthrough list are left alone.

Where can I install BlockAds without building it?

From F-Droid under the package name `app.pwhs.blockads`, or from IzzyOnDroid at `apt.izzysoft.de/packages/app.pwhs.blockads`. The project also lists a homepage at `https://blockads.pwhs.app`.

How often does BlockAds refresh its filter lists?

On a schedule you choose from 6h / 12h / 24h / 48h. The curated 284-domain passthrough list has no documented refresh interval.

Does BlockAds send crash data off the device?

Crash reporting is opt-in during onboarding, and manual local log export happens only when the user triggers it. The README names no reporting service or endpoint, so the payload cannot be checked from the documentation.

Official sources

  1. License: GPL-3.0
  2. pass-with-high-score/blockads-android on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/pass-with-high-score-blockads-android.svg)](https://hysenlabs.com/projects/pass-with-high-score-blockads-android)

Was this useful?