desktop: route wildcard zones to a local DNS stub

A hosts file cannot express a wildcard, so '*.youtube.com' was only ever the
base domain plus the guessed labels. The patterns the server now hands over
as unexpandable are answered by a stub on 127.0.0.1, and the helper owns one
systemd-resolved drop-in that points just those zones at it:

  * the stub is listening before the drop-in is written, and the drop-in is
    removed before the stub stops, so a zone's queries never land on a dead
    port;
  * stub and route live as long as the app, not the interval: between
    intervals they forward everything, instead of restarting resolved (and
    dropping its cache) on every pomodoro;
  * a crashed run leaves a world-readable drop-in the next start adopts, and
    the stub comes back before anything else;
  * masks with nothing routable after their last '*' ('*gram*') are reported
    as unenforceable, not silently dropped.

The Blocker page now reports what is really in place — the hosts region, the
routed zones, and the masks this machine cannot do — rather than the whole
expansion. 39 desktop tests, including the ordering invariants.
This commit is contained in:
timka123 committed 2026-10-19 18:45:42 +03:00
1 parent a531a62afc
commit 36af1e9e83
12 files changed
+1695 -29

No files matched your search

+34 -5
View File
@@ -49,11 +49,35 @@ the same build runs on KDE, GNOME or a bare window manager.
## Blocking
The rules (which sites, which apps, when) live on the backend so the web app can
edit them and a second machine obeys the same list. This app enforces them:
edit them and a second machine obeys the same list. This app enforces them with
two mechanisms, because no single one covers the masks people actually write:
* **Websites** — the managed region of `/etc/hosts` is kept in sync with the
policy. Writing it needs root, which a desktop app must not have, so it calls
`kadence-helper` through **pkexec**.
* **Wildcards a hosts file cannot express** — a hosts file is a list of exact
names, so `*.google.com` is expanded server-side into real hostnames. A bare
`*` in the *subdomain expansion* list changes what that means: `*.google.com`
then covers **every** subdomain, not just the configured labels, and those are
answered by a small DNS stub on `127.0.0.1` (port 5353, falling back to
5453/5553/5354) which the helper points `systemd-resolved` at with one drop-in
for just the zones that need it (`~google.com`). Every other name flows to the
machine's normal resolver untouched. The stub is unprivileged, speaks plain DNS
over UDP and TCP, answers blocked A records with the redirect address and other
record types with NODATA (so `HTTPS`/SVCB hints cannot smuggle an address past
it), and returns SERVFAIL when no upstream can answer.
* **What cannot be routed** — the zone is whatever follows the last `*`, and it
needs at least two labels: `*gram.com` routes `gram.com`, while `*gram*` has
nothing after its last `*` and `*.site` has a single label. Those stay honestly
reported as "cannot be blocked on this computer" instead of pretending; the
phone still matches them against the URLs it sees.
* **Ordering** — the stub is listening *before* the drop-in is written, and the
drop-in is removed *before* the stub stops, so resolved never has a moment with
nowhere to send a zone's queries. The stub and the route belong to the app's
lifetime, not to one interval: between intervals they stay up and forward
everything, because repointing resolved costs a restart and a dropped DNS cache.
If a crash left the drop-in behind, the next start sees it (the file is
world-readable), brings its stub back first, and carries on.
* **Applications** — `/proc` is scanned every few seconds for your own processes
matching the list; they are terminated (SIGTERM, then SIGKILL) or merely
reported, and every attempt is sent to the backend for the evening report.
@@ -65,7 +89,7 @@ edit them and a second machine obeys the same list. This app enforces them:
### The privileged helper
`helper/kadence-helper` is the entire privileged surface, and it is deliberately
dull:
dull. It has two jobs — the hosts region and the resolved drop-in:
* every hostname must match a strict pattern, the redirect must parse as an IP;
* only the region between its own markers is rewritten — your own entries above
@@ -73,12 +97,17 @@ dull:
* the input list is opened `O_NOFOLLOW` and must be a regular file;
* the write is atomic (temp file + `rename`), and the first run backs up
`/etc/hosts` to `/etc/hosts.kadence-backup`;
* `localhost` can never be blocked.
* `localhost` can never be blocked;
* `resolved-on` accepts only real zone names (two or more labels, no `*`, no
ports or junk), deduplicates them, refuses privileged ports, and writes the
drop-in `0644` atomically; `resolved-off` removes it. Both then restart
systemd-resolved and flush its cache; on a machine that does not run resolved,
they say so and the hosts half carries on alone.
```bash
sudo ./helper/install.sh # polkit asks once per session
sudo ./helper/install.sh --passwordless # or never, for your user only
sudo ./helper/install.sh --uninstall # unblocks first, then removes itself
sudo ./helper/install.sh --uninstall # unblocks, removes the drop-in, then removes itself
```
---
@@ -190,7 +219,7 @@ kadence_desktop/
config.py # ~/.config/kadence/desktop.json
tray.py # the painted countdown icon and its menu
integrations/ # notifications, inhibit/DND, MPRIS, launcher, logind, D-Bus, autostart
blocking/ # hosts file, /proc scanning, and the manager that syncs policy
blocking/ # hosts region, DNS stub + resolved route, /proc scanning, the manager that syncs policy
qml/ # Material UI: Timer, Tasks, Blocker, Report, Settings
helper/ # the privileged helper + polkit action + installer
packaging/ # .desktop entry, icon, user installer