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

+43 -7
View File
@@ -1,8 +1,10 @@
// The focus guard: what is blocked, when, and what it caught.
//
// Lists accept masks. A hosts file has no wildcards of its own, so `*.x.com` is
// expanded server-side into real hostnames; this page shows that count and says
// plainly when a pattern cannot be expressed that way.
// expanded server-side into real hostnames, and the patterns that leaves over
// are answered by the local DNS stub for the zones it can route. The stats show
// what is *really* in place on this computer — the hosts region and the routed
// zones — and every banner says plainly what could not be enforced here.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -42,6 +44,26 @@ ScrollView {
visible: controller.blockerError !== ""
text: controller.blockerError
}
Banner {
visible: controller.invalidExpansions.length > 0
text: controller.invalidExpansions.join(", ")
+ " is not a subdomain label, so it was ignored. Each entry is one "
+ "label — web, m, mobile. A bare `*` is kept and means every subdomain."
}
Banner {
visible: controller.blockerActive && controller.unroutableDomains.length > 0
text: controller.unroutableDomains.join(", ")
+ " cannot be blocked on this computer: no DNS zone can be routed for "
+ "a mask with nothing after its last `*`. The phone still matches it "
+ "against the URLs you open."
}
Banner {
visible: controller.blockerActive && controller.blockedDomains.length > 0
&& controller.appliedDomains.length === 0
text: "Blocking is on, but this machine's hosts file has none of the "
+ controller.blockedDomains.length + " hostnames in it yet. Either the "
+ "write has not run, or it was refused — a message above would say which."
}
Panel {
padding: 0
@@ -50,8 +72,21 @@ ScrollView {
spacing: 0
Repeater {
model: [
{ label: "Hostnames", value: "" + controller.blockedDomains.length, caption: "after expansion" },
{ label: "Apps", value: "" + controller.blockedApps.length, caption: "closed on sight" },
{ label: "Hostnames",
value: "" + controller.appliedDomains.length,
caption: controller.config.enforce_blocking && controller.config.block_hosts_file
? "in /etc/hosts"
: "not written (enforcement off)" },
{ label: "Zones",
value: "" + controller.routedZones.length,
caption: "routed to the stub" },
{ label: "Apps",
value: "" + controller.blockedApps.length,
caption: !controller.config.enforce_blocking || !controller.config.block_apps
? "not enforced"
: (controller.settings.blocker
&& controller.settings.blocker.kill_blocked_apps
? "closed on sight" : "warned only") },
{ label: "Attempts today",
value: controller.stats.today ? "" + controller.stats.today.blocked_attempts : "0",
caption: "turned away" },
@@ -125,8 +160,9 @@ ScrollView {
Repeater {
model: [
{ pattern: "youtube.com", meaning: "the site itself, plus www. and m." },
{ pattern: "*.google.com", meaning: "every subdomain in the expansion list" },
{ pattern: "*gram*", meaning: "matches apps; a hosts file cannot express it" }
{ pattern: "*.google.com", meaning: "the bare domain plus every subdomain in the expansion list" },
{ pattern: "*gram*", meaning: "matches apps, and URLs on the phone; no DNS zone can be routed for it" },
{ pattern: "* in the list below", meaning: "makes *.site cover every subdomain — answered by the DNS stub" }
]
delegate: RowLayout {
required property var modelData
@@ -260,7 +296,7 @@ ScrollView {
TextField {
id: subdomainField
Layout.preferredWidth: 420
placeholderText: "*.site expands to: www, m, app…"
placeholderText: "*.site expands to: www, m, web… (`*` = every subdomain)"
text: (controller.settings.blocker && controller.settings.blocker.subdomain_expansions
? controller.settings.blocker.subdomain_expansions : []).join(", ")
}