Repository navigation
feat(alerting): notice when the house stops filtering DNS (#126) - #291
Merged
Merged
Conversation
ADR-0010 leaves the public upstreams in Unbound's forwarder list so that a dead AdGuard costs filtering rather than connectivity. That is the right trade for a household whose runbook has no branch for "everything is up and it still doesn't work", and it is not free: it converts a loud failure into a silent one. The only symptom is advertisements reappearing, which nobody reports and which is indistinguishable from a blocklist that never covered a given site. Nothing here would have caught it. The containers rules need a metrics agent on a mini PC that does not exist, InstanceDown covers a host rather than a container, network.rules.yaml does not touch DNS, and — the trap worth stating — a probe down the normal resolver path always passes, because the fallback is doing its job. Two `dns` modules and two rules. AdGuardNotAnswering asks whether it answers at all; AdGuardNotFiltering asks whether it is still blocking, which is the quieter failure: a blocklist that silently stopped updating leaves a perfectly healthy service filtering nothing. Both warning, so they route to `default` rather than `urgent` — nothing is down and nobody is blocked. The probes go straight at port 53 on the filter, under their own scrape job. Separate from `blackbox` for two reasons rather than tidiness: EndpointUnreachable is critical and routes to urgent, which a filter designed to fail open must never do, and EndpointNameNotResolving joins on `name` in a way that means nothing when the target is itself a resolver. AdGuardNotFiltering carries an `and on (name)` join so a stopped AdGuard raises one alert and not two, and dns.test.yaml asserts that silence — a join that stops matching passes `promtool check rules` and can never fire (#63). All four states were measured against a real AdGuard Home running the default filter before either rule was written: healthy, blocklists not loaded, blocking mode changed, stopped — plus the trap itself, Unbound at 10.0.99.1, which answers the liveness probe and fails the filtering one. The canary is doubleclick.net, a real blocklist entry rather than a rule of our own, because a custom rule would keep working while every downloaded list failed to load. The targets ship written out and disabled, the call #91 made for the iLO and pfSense UIs: AdGuard needs the mini PC #102 has not bought, and Unbound recurses and forwards to nothing today (#123, verified on morpheus 2026-09-04). The scrape job lands anyway, so enabling them is a file_sd append with no restart. #123's roadmap entry is corrected: the rules were writable today, only the target was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Two
dnsprobe modules and two alert rules that notice when the house stops filtering DNS —AdGuardNotAnswering(does it answer at all?) andAdGuardNotFiltering(is it still blocking, or merely alive?). Both probes are aimed straight at port 53 on the filter, under a newblackbox-dnsscrape job. Nothing changes in the running stack today: the two targets ship written out and commented, because AdGuard does not exist yet.Why
Closes #126, and the monitoring half of #123 — the same failure, filed twice.
ADR-0010 leaves the public upstreams in Unbound's forwarder list so a dead AdGuard costs filtering rather than connectivity. Right trade, and not free: it converts a loud failure into a silent one. The only symptom is advertisements reappearing, which nobody reports and which is indistinguishable from a blocklist that never covered a given site.
Nothing already here would catch it — the containers rules need an agent on a box that does not exist,
InstanceDowncovers a host not a container,network.rules.yamldoes not touch DNS. And the trap worth stating out loud: a probe down the normal resolver path always passes, because the fallback is doing its job.Three design points:
EndpointUnreachableiscriticaland routes tourgent, which a filter designed to fail open must never do;EndpointNameNotResolvingjoinsvia=dnstovia=addressonname, a join that means nothing when the target is a resolver. Arole!=matcher on the existing rule would quietly capture the next target somebody adds without the label.AdGuardNotFilteringcarries anand on (name)join so a stopped AdGuard raises one alert and not two — the ContainerHighMemory cannot fire — no service sets a memory limit #63 hazard, since a join that stops matching passespromtool check rulesand can never fire.Both rules are
warning, so they route todefaultrather thanurgent. Nothing is down, nobody is blocked, and ADR-0010 already says filtering here is a convenience rather than a control.Blast radius
None today — the two targets are commented out, so the new job discovers nothing and neither rule has a series to evaluate. When #102 enables them, the blast radius is one
warningnotification to the existingdefaultreceiver; no new receiver, no routing change.secrets/*.sops.yamlVerification
make validatepasses (all checks passed, 2 skipped — not a deployment checkout).promtool test rulesis green across all 9 test files,check rulesacross all 11 rule files, and Prometheus was booted against the comment-onlytargets/blackbox-dns.yamlto confirm an emptyfile_sdfile loads without error.The probes were measured against a real AdGuard Home before either rule was written — the image pulled locally, the default AdGuard DNS filter loaded (180,105 rules), probed from a throwaway
prom/blackbox-exporter:v0.28.0:dns_answersdns_filtersfiltering_enabled: false)10.0.99.1instead — the trapThat last row is the point of the whole change: the resolver the household actually talks to passes a liveness check and fails a filtering check, which is exactly the wrong answer a probe on the normal path would have given.
Two smaller things checked rather than assumed:
doubleclick.netreally is on the default filter and comes back as a single0.0.0.0A record (six real A records unfiltered, so the probe discriminates), and blackbox v0.28.0 fails a validator over an empty answer section rather than passing vacuously — otherwise a NOERROR/NODATA reply would have looked "filtered".The four rows above are the four cases in
tests/dns.test.yaml.make validatepassesFollow-up
Enabling the two targets belongs to #102, and its roadmap entry now says so. The target file carries the address assumption to correct (
10.0.99.40, the next free Winterfell reservation) and the two commands to verify before uncommenting — including the negative one against Unbound, because a filtering probe that cannot go red is measuring nothing.#123's Monitoring entry said this was blocked on #102. It was right that the probe cannot go through
morpheusand right that "answering" and "filtering" are two questions; it was wrong about one thing, and the entry has been corrected: what could not be written today was the target, not the rules.🤖 Generated with Claude Code