blocker: an exception reads like a block, so make it behave like one

`youtube.com` on the block list catches `music.youtube.com` — a plain entry
covers its subtree. `youtube.com` on the *allow* list caught only itself,
because exceptions were glob-matched whatever they looked like and a plain name
has no metacharacters. The same text in two boxes of the same form meant a
subtree in one and a single hostname in the other, with nothing in the UI
saying so.

A plain exception now covers its subdomains; a masked one is glob-matched
exactly as before, which is still how you spare something narrower than a whole
subtree (`docs.*`, `*.docs.google.com`).

Three enforcers decide this and all three had to move together, or an exception
would work in the browser and not at the resolver: `blocker_service.is_allowed`,
`:core`'s `isAllowed`, and the desktop DNS stub's own allow check, which was
reusing the generic glob matcher. Parity tests on both sides, plus the two
sentences in the web UI and the README that promised the old behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
timka123andClaude Opus 5 committed 2026-10-22 17:24:17 +03:00
1 parent b9c0762f23
commit 5facf41f10
7 files changed
+120 -9

No files matched your search

+18 -1
View File
@@ -116,6 +116,23 @@ def _matches(name: str, patterns: list[str]) -> bool:
return any(fnmatch.fnmatchcase(name, pattern) for pattern in patterns)
def _allowed(name: str, entries: list[str]) -> bool:
"""Whether an exception covers this name.
Not [_matches]: a plain exception covers its subdomains, the same way a plain
block-list entry does. Parity with `blocker_service.is_allowed` and `:core`'s
`isAllowed` — the stub is the third place that decides this, and an exception
that works in the browser but not at the resolver is worse than neither.
"""
for entry in entries:
if any(char in entry for char in "*?["):
if fnmatch.fnmatchcase(name, entry):
return True
elif name == entry or name.endswith(f".{entry}"):
return True
return False
# ==========================================================================
# The DNS wire format, as little of it as a blocker needs
# ==========================================================================
@@ -228,7 +245,7 @@ class _Rules:
return (
self.active
and bool(self.patterns)
and not _matches(name, list(self.allow))
and not _allowed(name, list(self.allow))
and _matches(name, list(self.patterns))
)