1 Commits
Author SHA1 Message Date
clawbot a93389e1a0 watcher: send the inconsistency alert once per disagreement (closes #158)
check / check (push) Successful in 1m5s
detectInconsistencies alerted for neighbouring pairs of nameservers whose
records differed, on every DNS check, for as long as they differed. It now
also takes the previous hostname state, compares every pair of nameservers,
and alerts for a pair that differs unless both were in that state and
already differed there, so a nameserver new on a check that answers
differently is reported once. The state loaded at startup is the previous
state for the first check, so a disagreement saved before a restart is not
reported again. The choice of pairs is tested on record data, and the alert
through the hostname change detection with the notifier stand-in and no
resolver. The README describes the new behaviour.

Model: opus-5-5
2026-09-29 02:29:53 +02:00
4 changed files with 26 additions and 15 deletions
+9 -6
View File
@@ -74,12 +74,15 @@ rejected.
This is distinct from "responded with no records."
- **NS recovery**: A previously-unreachable nameserver starts
responding again.
- **Inconsistency detected**: Two nameservers that agreed on the
previous check now return different record sets for the same
hostname. Every pair of nameservers is compared. The alert is sent
once for each such pair, on the check where they start to disagree,
and not again while they keep disagreeing, including after a restart.
If they agree again and later disagree, it is sent again.
- **Inconsistency detected**: Two nameservers return different record
sets for the same hostname and did not already differ on the previous
check. Every pair of nameservers is compared. The alert is sent once
for each such pair, on the check where they start to disagree, and not
again while they keep disagreeing, including after a restart. A
nameserver that was not in the previous check (newly added, or back
after dropping out) and answers differently is reported on the check
where it appears. If a pair agrees again and later disagrees, the
alert is sent again.
### TCP Port Monitoring
+4 -3
View File
@@ -24,9 +24,10 @@ Rationale, Design, TODO, License, Author) if any are still missing.
# Completed Steps
- 2026-09-28: the inconsistency alert is sent once, on the check where two
nameservers that agreed start to disagree, instead of on every check while
they disagree, and not again after a restart. Every pair of nameservers is
compared, not only neighbours in sorted order of name (closes #158).
nameservers start to disagree or where a nameserver that disagrees first
appears, instead of on every check while they disagree, and not again after
a restart. Every pair of nameservers is compared, not only neighbours in
sorted order of name (closes #158).
- 2026-09-28: DNS names in record values (CNAME, MX, SRV and NS targets) are
lower-cased, so nameservers that answer in different letter case no longer
count as inconsistent or as a record change (closes #157).
+9 -3
View File
@@ -80,10 +80,10 @@ func TestNewlyDisagreeingPairs(t *testing.T) {
want: [][][2]string{nil, nil},
},
{
name: "nameserver new on this check does not alert",
name: "nameserver new on the first check and disagreeing alerts once",
loaded: onlyA,
checks: []map[string]map[string][]string{disagree},
want: [][][2]string{nil},
checks: []map[string]map[string][]string{disagree, disagree},
want: [][][2]string{alert, nil},
},
{
name: "disagreement after agreeing again alerts again",
@@ -123,6 +123,7 @@ func TestNewlyDisagreeingPairs(t *testing.T) {
func TestInconsistencyAlert(t *testing.T) {
t.Parallel()
onlyA := map[string]map[string][]string{nsA: {"A": {ip1}}}
agree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip1}}}
disagree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip2}}}
@@ -143,6 +144,11 @@ func TestInconsistencyAlert(t *testing.T) {
loaded: disagree,
want: 0,
},
{
name: "nameserver new on the first check and disagreeing alerts once",
loaded: onlyA,
want: 1,
},
}
for _, tt := range tests {
+4 -3
View File
@@ -471,8 +471,9 @@ func (w *Watcher) detectInconsistencies(
}
// newlyDisagreeingPairs returns every pair of nameservers whose records
// differ in current but were equal in prev, in sorted order of name.
// A pair with a nameserver missing from prev is not returned.
// differ in current, in sorted order of name, except pairs where both
// nameservers were in prev and already differed there. A nameserver
// missing from prev is paired with every nameserver it differs from.
func newlyDisagreeingPairs(
prev *state.HostnameState,
current map[string]map[string][]string,
@@ -495,7 +496,7 @@ func newlyDisagreeingPairs(
prev1, ok1 := prev.RecordsByNameserver[ns1]
prev2, ok2 := prev.RecordsByNameserver[ns2]
if !ok1 || !ok2 || !recordsEqual(prev1.Records, prev2.Records) {
if ok1 && ok2 && !recordsEqual(prev1.Records, prev2.Records) {
continue
}