Fixes a bug that broke the YouTube app for everyone running 1.1.0, and stops
the rule parser storing entries that could never match.
Verified on an Android 14 emulator with the tunnel running — the same Android
version the bug was reported on. Every endpoint the YouTube app needs now
resolves, and filtering is untouched:
| domain | 1.2.0 |
|---|---|
youtubei.googleapis.com |
resolves (172.217.115.4) — was NXDOMAIN in 1.1.0 |
www.youtube.com, i.ytimg.com, yt3.ggpht.com |
resolve |
rr1---sn-*.googlevideo.com, jnn-pa.googleapis.com |
resolve |
graph.facebook.com |
resolves — was NXDOMAIN in 1.1.0 |
s.youtube.com |
blocked, as intended |
pagead2.googlesyndication.com |
blocked, as intended |
iOS is still not verified — no Swift in the PacketTunnel target has been
compiled on a Mac. Treat this release as Android-only, as with 1.1.0.
🐛 YouTube would not load with the tunnel on
-
youtubei.googleapis.comwas blocked by default, which breaks the YouTube
app outright. It is not a tracker: it is YouTube's InnerTube API, the one
the app fetches its home feed, its search results and the player config
carrying the stream URLs from. Blocked, the app renders its skeleton and
nothing ever fills it in — no thumbnails, no playback. Reported on Realme /
Android 14, but nothing about it was device-specific.The rule came from
seed_default_rules(), a built-in list that applies
before any filter list is downloaded, in the Trackers category, which is on
by default. So it hit every user from the first launch, and no setting on
screen explained why. -
graph.facebook.comremoved for the same reason — the Facebook Graph
API, which every app offering Facebook login depends on. -
Genuine telemetry stays blocked:
s.youtube.comand
video-stats.l.google.comare playback statistics, and YouTube works fine
without them. -
test_seed_rules_never_block_an_app_s_own_apinow guards the seed list, and
the comment above it says what the list is allowed to contain.
🎛️ Three of the four rule categories had nothing in them
Every downloaded list fed category 0 (Ads), so Trackers, Malware and Adult held
only the hardcoded seed rules — and Adult held nothing at all.
Two things had to be wrong for that. FilterSource.categoryId defaulted to 0
and no default source overrode it — but fixing that alone changes nothing,
because syncAllFilters() called loadRulesText(content) without the category
and the engine then applied its own default of 0. The field was only ever
reaching the per-category files written for the iOS extension.
| Category | Was | Now |
|---|---|---|
| Ads | ~251,000 domains | AdGuard DNS + StevenBlack + OISD Small |
| Trackers | 5 seed rules | Peter Lowe's list (3,526) |
| Malware | 3 invented domains | URLhaus (370 real hosts, +0.0 MB) |
| Adult | nothing at all | StevenBlack adult hosts (76,751), opt-in |
crypto-miner.org,bad-malware-site.netandphishing-login.comwere not
real. The Malware category protected against three names that do not exist,
and counted them as rules loaded.- The adult list is not downloaded until it is switched on: 76,751 hostnames
cost 4.3 MB in the trie, and the Adult category is off by default, so paying
that up front would be 4.3 MB of the iOS extension's budget spent on nothing. - Honest caveat: DNS blocklists do not split cleanly into "ads" and "trackers".
Nearly everything in the Trackers list is also in the ad lists, so disabling
Trackers would still not unblock much.
Known gap, not fixed here. The categories have no switches on screen — the
only Switch in the app is Scheduled Parental Controls, and the only thing it
does is turn the Adult category on. Since that category was empty, the
schedule filtered nothing whatsoever before this release, and it still does
nothing until the adult list is switched on in Rules. Wiring the two together
is a change worth making carefully: the obvious version routes through
syncAllFilters(), which starts by clearing every downloaded rule and
re-fetching ~7 MB, so a flick of that switch on a bad connection would leave
the user with no filtering at all.
🧽 The user's own lists shipped pre-filled with invented entries
_whitelist came seeded with mybank.com and workplace.com, _blacklist
with bad-tracker.net and crypto-miner.org. A fresh install presented four
rules as though the user had written them. This is the same class of problem as
the fabricated statistics removed in 1.1.0, and worse in one way: whitelist
entries are pushed into the engine, so the app really was allowing two domains
nobody chose.
- The split-tunnel bypass list was seeded too, and that one had teeth. It
shipped holdingcom.zing.zaloandcom.vietcombank.mobile, and the list
goes toaddDisallowedApplication()when the tunnel is built. A messaging app
and a banking app were carved out of the VPN on every fresh install, chosen by
nobody, in an app whose entire promise is that traffic goes through it. - A source declared
isEnabled: falsecould not stay off. The preference
stores only the ids that are off, and a missing key was read as an empty
list, which switched everything on. Guarded by
a source declared off stays off before any toggle is saved.
All three lists now start empty, and the declared defaults survive first launch.
🌏 The four languages now cover the whole app
The settings screen offers English, Tiếng Việt, 한국어 and 日本語, and all four
tables were complete — but only the dashboard and part of settings ever read
them. Rules, Analytics, Logs and the navigation bar were hardcoded English, so
switching language changed roughly a quarter of what is on screen.
- Every user-visible string in all six screens now goes through
AppStrings:
35 keys became 118, in each of the four languages. That includes the filter
list names and descriptions under "Subscribe to Filter Lists", which are
looked up assrc_<id>_name— a list the user added themselves has no
translation and keeps whatever they typed. - The screens never rebuilt on a language change.
MainNavigationScreen
held its five screens in aconstlist in a field, soIndexedStackgot the
identical widget objects every build and Flutter skipped the whole subtree.
Only the tab labels changed language; everything behind them stayed as it
was.conston the individual constructors does the same thing, since a
const constructor is canonicalised. every language defines every stringfails if a translation is missing.
AppStrings.getfalls back to English, so a gap does not throw — the screen
just quietly renders in the wrong language. The test compares raw lookups.- Fixed
theme_titlein Japanese, which readサイバーパンクネ온カラー— a Korean
syllable had found its way into the middle of a Japanese string.
⌨️ Text fields in Settings cleared themselves every two seconds
The DoH URL field and the split-tunnel package field created their
TextEditingController inside build(). The settings screen watches
VpnProvider, which notifies every two seconds while the tunnel is up, so both
fields were handed a brand new empty controller on every tick: whatever you
typed disappeared mid-sentence, and neither controller was ever disposed.
Setting a custom DoH resolver or excluding an app while protected was simply
not possible. Confirmed on device before and after the fix — the controllers
now belong to a State that disposes them.
⏱️ A category change no longer hides behind a cached answer
The engine caches replies for five minutes and nothing invalidated them when a
category was switched on, so a rule change silently did not apply to anything
already looked up. aegis_set_category now clears the cache, guarded by
test_toggling_a_category_drops_cached_answers.
This does not make the change instant. Android keeps its own resolver cache
outside the app, and that one cannot be flushed from here — measured on device,
a domain resolved two minutes before the Adult category was switched on kept
resolving afterwards, and only started returning NXDOMAIN once the tunnel was
stopped and started again. Domains never looked up before are blocked
immediately. Anyone turning on parental controls in a hurry should restart the
tunnel.
🧪 Guard against the next one
test_seed_rules_never_block_an_app_s_own_api now covers eleven endpoints an
app cannot start without — YouTube, Instagram, Telegram, Twitter, OpenAI, and
Firebase Cloud Messaging, where a block would silently kill push notifications.
🧹 Filter rule parsing
-
Rules that could never match are no longer stored. The parser ended in a
catch-all — any line with a dot and no space became a "domain" — so filter
syntax the DNS matcher cannot express was kept verbatim. Measured against the
lists actually shipped: 669 such entries in the AdGuard DNS filter, and
17,779 in EasyList. Each one occupied memory, inflated the "rules loaded"
count shown to the user, and matched nothing.AdGuard DNS StevenBlack Peter Lowe unusable entries before 669 0 0 after 0 0 0 -
||domainwithout a trailing^now loads. 172 rules in the AdGuard DNS
filter are written that way. They fell through to the catch-all and were
stored with the||still attached, so those domains were never actually
blocked while the UI counted them as active rules. The same fix applies to
@@||domainexceptions. -
Wildcards (
||ads.livetv*.me^), regex, path-scoped and resource-type rules
are now dropped deliberately rather than stored as garbage. A DNS filter sees
a hostname and nothing else; none of these can be honoured. -
Added Peter Lowe's Ad and Tracking Server List to the default sources.
Hostname-only, so every line survives the DNS parser — 3,525 rules, zero
unusable. It is one of the lists uBlock Origin Lite enables by default. -
Added OISD Small to the default sources, and deliberately not OISD Big.
Measured against the lists already shipped, with memory as the deciding
factor — the iOS PacketTunnel extension has a hard limit in the tens of MB,
and the merged trie already costs 18.3 MB before either list is added.domains already covered newly blocked trie cost OISD Small 56,747 93.5% 3,673 +0.2 MB OISD Big 265,831 32.3% 179,917 +11.1 MB OISD Big blocks a great deal more, but 11.1 MB on top of 18.3 MB is not a
trade the extension can make, and every list in the defaults is enabled for
every user. It can still be added by hand as a custom source.
What's Changed
- ci: move every action off the deprecated Node 20 runtime by @vannt-dev in #20
- fix(engine): stop storing filter rules the DNS matcher can never match by @vannt-dev in #21
- fix: YouTube broken by a seed rule, plus filter-list tuning (v1.2.0) by @vannt-dev in #22
- fix: clear the DNS cache on a category change, and finish the translations by @vannt-dev in #24
- Release 1.2.0 — YouTube fix and release audit by @vannt-dev in #23
Full Changelog: v1.1.0...v1.2.0