watcher, config: no Record Change or Inconsistency for listed names (closes #255)
check / check (push) Successful in 2m16s

DNSWATCHER_SKIP_RECORD_NOTIFICATIONS takes a comma-separated list of
names from DNSWATCHER_TARGETS, read as the targets are (letter case,
trailing dot, repeats). For a listed name no Record Change and no
Inconsistency notification is sent; its records are still checked and
saved, and its other notifications are sent. A listed name that is not
a target stops startup with an error naming it.

The two detections return early for a listed name. Apex domains are
covered too: their own records go through the same detection.

Live DNS cannot be made to disagree on purpose, so the Inconsistency
test feeds records to the change detection directly, as the existing
inconsistency tests do; the live test covers Record Change.

Model: opus-5-5
This commit was merged in pull request #258.
This commit is contained in:
2026-10-06 03:01:46 +02:00
parent 6822996134
commit d1060315b7
6 changed files with 275 additions and 66 deletions
+44 -29
View File
@@ -99,10 +99,11 @@ notification endpoint set, changes show only on the dashboard; see
records, stored per nameserver. Their changes are notified as a hostname's
are, as a record change, NS query failure, NS recovery, inconsistency or CNAME
address change, in a message that starts `Domain:` where a hostname's starts
`Hostname:`. A domain with no delegation of its own has these records asked at
the servers of the zone it is in, as a hostname has. A domain that does not
exist has none: they are not asked for, and those saved by an earlier check
are removed without a notification.
`Hostname:`. A domain listed in `DNSWATCHER_SKIP_RECORD_NOTIFICATIONS` gets no
record change or inconsistency notification. A domain with no delegation of
its own has these records asked at the servers of the zone it is in, as a
hostname has. A domain that does not exist has none: they are not asked for,
and those saved by an earlier check are removed without a notification.
### DNS Hostname Monitoring (Subdomains)
@@ -136,10 +137,14 @@ notification endpoint set, changes show only on the dashboard; see
lower case, because names are case-insensitive and nameservers may answer in
any letter case. TXT and CAA values keep their letter case; they are not
lower-cased.
- Any observable change in any nameserver's response triggers a notification.
This includes:
- Any observable change in any nameserver's response triggers a notification,
except a record change or an inconsistency for a domain or hostname listed in
`DNSWATCHER_SKIP_RECORD_NOTIFICATIONS`. This includes:
- **Record change**: A nameserver returns different records than it did on
the previous check (additions, removals, value changes).
the previous check (additions, removals, value changes). For a domain or
hostname listed in `DNSWATCHER_SKIP_RECORD_NOTIFICATIONS`, neither a
record change nor an inconsistency is notified; its records are still
checked and saved, and its other notifications are sent.
- **NS query failure**: A nameserver that previously responded becomes
unreachable (timeout, SERVFAIL, REFUSED, network error). This is distinct
from "responded with no records": a nameserver that answers NXDOMAIN or
@@ -160,7 +165,8 @@ notification endpoint set, changes show only on the dashboard; see
failed on it, and answers differently is reported on the check where it
answers. So is a pair that differs in a record type whose query to either
nameserver failed on the previous check. If a pair agrees again and later
disagrees, the alert is sent again.
disagrees, the alert is sent again. For a domain or hostname listed in
`DNSWATCHER_SKIP_RECORD_NOTIFICATIONS`, no inconsistency is notified.
- **CNAME address change**: The addresses at the end of a name's CNAME chain
differ from those of the previous check. They are found when its
nameservers answer with a CNAME and no address; a name that answers with
@@ -186,9 +192,13 @@ notification endpoint set, changes show only on the dashboard; see
- Any change in port availability triggers a notification:
- Port transitioned from open to closed (or vice versa).
- New IP appeared (from DNS change): its port state is recorded without a
port notification; the DNS change notification shows the new address.
port notification; the DNS change notification shows the new address. A
domain or hostname listed in `DNSWATCHER_SKIP_RECORD_NOTIFICATIONS` gets
no notification for an address added to its own A or AAAA records.
- IP disappeared (from DNS change) — noted in the DNS change notification;
port state for that IP is removed. When none of a name's nameservers
port state for that IP is removed. A domain or hostname listed in
`DNSWATCHER_SKIP_RECORD_NOTIFICATIONS` gets no notification for an address
removed from its own A or AAAA records. When none of a name's nameservers
answered, its addresses are not known, so the port state saved for them is
kept.
@@ -210,7 +220,9 @@ notification endpoint set, changes show only on the dashboard; see
**Every observable state change produces a notification.** dnswatcher is
designed as a real-time change feed — degradations, failures, recoveries, and
routine changes are all reported equally.
routine changes are all reported equally. A domain or hostname listed in
`DNSWATCHER_SKIP_RECORD_NOTIFICATIONS` gets no record change or inconsistency
notification.
Supported notification backends:
@@ -409,23 +421,24 @@ following precedence (highest to lowest):
### Environment Variables
| Variable | Description | Default |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------- |
| `PORT` | HTTP listen port | `8080` |
| `DNSWATCHER_DEBUG` | Enable debug logging | `false` |
| `DNSWATCHER_DATA_DIR` | Directory for state file | `/var/lib/dnswatcher` |
| `DNSWATCHER_TARGETS` | Comma-separated DNS names (auto-classified via PSL) | `""` |
| `DNSWATCHER_SLACK_WEBHOOK` | Slack incoming webhook URL | `""` |
| `DNSWATCHER_MATTERMOST_WEBHOOK` | Mattermost incoming webhook URL | `""` |
| `DNSWATCHER_NTFY_TOPIC` | ntfy topic URL | `""` |
| `DNSWATCHER_DNS_INTERVAL` | DNS check interval, a positive duration such as `30m`; empty means the default, anything else stops startup | `1h` |
| `DNSWATCHER_TLS_INTERVAL` | TLS check interval, a positive duration such as `6h`; empty means the default, anything else stops startup | `12h` |
| `DNSWATCHER_TLS_EXPIRY_WARNING` | Days before expiry to warn | `7` |
| `DNSWATCHER_SENTRY_DSN` | Sentry DSN for error reporting | `""` |
| `DNSWATCHER_MAINTENANCE_MODE` | Only sets `maintenanceMode` in the health check response; changes nothing else | `false` |
| `DNSWATCHER_METRICS_USERNAME` | Basic auth username for /metrics, which is served only when this is set | `""` |
| `DNSWATCHER_METRICS_PASSWORD` | Basic auth password for /metrics | `""` |
| `DNSWATCHER_SEND_TEST_NOTIFICATION` | Send a test notification after first scan completes | `false` |
| Variable | Description | Default |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| `PORT` | HTTP listen port | `8080` |
| `DNSWATCHER_DEBUG` | Enable debug logging | `false` |
| `DNSWATCHER_DATA_DIR` | Directory for state file | `/var/lib/dnswatcher` |
| `DNSWATCHER_TARGETS` | Comma-separated DNS names (auto-classified via PSL) | `""` |
| `DNSWATCHER_SKIP_RECORD_NOTIFICATIONS` | Comma-separated names from `DNSWATCHER_TARGETS` for which no record change or inconsistency is notified; any other name stops startup | `""` |
| `DNSWATCHER_SLACK_WEBHOOK` | Slack incoming webhook URL | `""` |
| `DNSWATCHER_MATTERMOST_WEBHOOK` | Mattermost incoming webhook URL | `""` |
| `DNSWATCHER_NTFY_TOPIC` | ntfy topic URL | `""` |
| `DNSWATCHER_DNS_INTERVAL` | DNS check interval, a positive duration such as `30m`; empty means the default, anything else stops startup | `1h` |
| `DNSWATCHER_TLS_INTERVAL` | TLS check interval, a positive duration such as `6h`; empty means the default, anything else stops startup | `12h` |
| `DNSWATCHER_TLS_EXPIRY_WARNING` | Days before expiry to warn | `7` |
| `DNSWATCHER_SENTRY_DSN` | Sentry DSN for error reporting | `""` |
| `DNSWATCHER_MAINTENANCE_MODE` | Only sets `maintenanceMode` in the health check response; changes nothing else | `false` |
| `DNSWATCHER_METRICS_USERNAME` | Basic auth username for /metrics, which is served only when this is set | `""` |
| `DNSWATCHER_METRICS_PASSWORD` | Basic auth password for /metrics | `""` |
| `DNSWATCHER_SEND_TEST_NOTIFICATION` | Send a test notification after first scan completes | `false` |
**`DNSWATCHER_TARGETS` is required.** dnswatcher will refuse to start if no
monitoring targets are configured. A monitoring daemon with nothing to monitor
@@ -786,7 +799,9 @@ docker run -d \
nameservers answered, the addresses an earlier check found at the end of
the chain are used.
4. **On change detection**: Send notifications to all configured endpoints,
update in-memory state, persist to disk.
update in-memory state, persist to disk. A record change or inconsistency for
a domain or hostname listed in `DNSWATCHER_SKIP_RECORD_NOTIFICATIONS` sends
no notification, but the state is still updated and saved.
5. **Shutdown**: The watcher stops checking and saves the final state to disk,
and shutdown waits for that save before it goes on. Then it waits for
in-flight notification deliveries to complete. Both waits share the fx