watcher: a hostname whose zone's nameservers all stop answering gets NS Failure (closes #284)
check / check (push) Waiting to run
check / check (push) Waiting to run
When none of the nameservers of a hostname's zone answered, the walk to them failed and the check was only logged. The walk now returns a NoAnswerError naming the nameservers of the zone it reached through a referral when none of them answers. When those are the nameservers the previous check saved for the name, each is saved as failed with the reason, so it gets an NS Failure and later an NS Recovery. A domain with no delegation of its own, whose NS lookup is that same walk, gets the same. A walk that fails before that zone, as when the host loses its network, is still only logged. Model: opus-5-5
This commit is contained in:
@@ -2,6 +2,7 @@ package watcher_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
@@ -14,6 +15,10 @@ import (
|
||||
"sneak.berlin/go/dnswatcher/internal/watcher"
|
||||
)
|
||||
|
||||
// errNoReply is why a walk that got no reply from a zone's servers
|
||||
// failed.
|
||||
var errNoReply = errors.New("all servers failed: i/o timeout")
|
||||
|
||||
// answered is what a check saves for a nameserver that answered with
|
||||
// these records.
|
||||
func answered(records map[string][]string) *state.NameserverRecordState {
|
||||
@@ -183,6 +188,134 @@ func TestNSFailureAlertNamesHostnameNameserverAndReason(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestZoneNameserversAllStopAnswering runs three checks of host from
|
||||
// the point where its lookup has returned. On the first, nsA and nsB,
|
||||
// the nameservers of its zone, answer. On the second, the lookup fails
|
||||
// with the error the walk to them returns when neither answers (see
|
||||
// the resolver's TestFollowDelegation_NoAnswerAfterReferral), given
|
||||
// here because no live zone has nameservers that never answer. For a
|
||||
// hostname it is the lookup of its records that fails, and for a domain
|
||||
// with no delegation of its own the lookup of its delegation. Both are
|
||||
// saved as failed, with the reason, and each gets an NS Failure. On the
|
||||
// third, both answer, and each gets an NS Recovery. A domain's records
|
||||
// are checked as a hostname's, so its first and third checks are too.
|
||||
func TestZoneNameserversAllStopAnswering(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
records := map[string][]string{"A": {ip1}}
|
||||
bothAnswer := map[string]*resolver.NameserverResponse{
|
||||
nsA: response(records), nsB: response(records),
|
||||
}
|
||||
noAnswer := &resolver.NoAnswerError{
|
||||
Nameservers: []string{nsA, nsB}, Err: errNoReply,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
isDomain bool
|
||||
}{
|
||||
{"hostname", false},
|
||||
{"domain with no delegation of its own", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
if tt.isDomain {
|
||||
cfg.Domains = []string{host}
|
||||
} else {
|
||||
cfg.Hostnames = []string{host}
|
||||
}
|
||||
|
||||
// The watcher is given each lookup's result, so it has no
|
||||
// resolver.
|
||||
deps := newTestDeps(t, cfg)
|
||||
w := watcher.NewForTest(
|
||||
cfg, deps.state, nil,
|
||||
deps.portChecker, deps.tlsChecker, deps.notifier,
|
||||
)
|
||||
w.SetFirstRun(false)
|
||||
|
||||
w.CheckHostnameResult(t.Context(), host, bothAnswer, nil)
|
||||
|
||||
if tt.isDomain {
|
||||
w.CheckDomainResult(t.Context(), host, nil, noAnswer)
|
||||
} else {
|
||||
w.CheckHostnameResult(t.Context(), host, nil, noAnswer)
|
||||
}
|
||||
|
||||
current, _ := deps.state.GetHostnameState(host)
|
||||
|
||||
for _, ns := range []string{nsA, nsB} {
|
||||
got := current.RecordsByNameserver[ns]
|
||||
if got.Status != failed().Status || got.Error != errNoReply.Error() {
|
||||
t.Errorf(
|
||||
"%s saved with status %q, error %q; want %q, %q",
|
||||
ns, got.Status, got.Error, failed().Status, errNoReply,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if n := countNotifications(deps, "NS Failure: "+host); n != 2 {
|
||||
t.Errorf("sent %d NS Failure notifications, want 2", n)
|
||||
}
|
||||
|
||||
w.CheckHostnameResult(t.Context(), host, bothAnswer, nil)
|
||||
|
||||
if n := countNotifications(deps, "NS Recovery: "+host); n != 2 {
|
||||
t.Errorf("sent %d NS Recovery notifications, want 2", n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFailedNameserversOnlyForThoseOfThePreviousCheck gives
|
||||
// FailedNameservers lookup errors after a check in which nsA and nsB
|
||||
// answered. Only an error that names both makes them failed.
|
||||
func TestFailedNameserversOnlyForThoseOfThePreviousCheck(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
records := map[string][]string{"A": {ip1}}
|
||||
bothAnswered := saved(map[string]*state.NameserverRecordState{
|
||||
nsA: answered(records), nsB: answered(records),
|
||||
})
|
||||
noAnswer := func(nameservers ...string) error {
|
||||
return &resolver.NoAnswerError{Nameservers: nameservers, Err: errNoReply}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
prev *state.HostnameState
|
||||
wantFailed bool
|
||||
}{
|
||||
{"both did not answer", noAnswer(nsA, nsB), bothAnswered, true},
|
||||
{
|
||||
"the servers of a parent zone did not answer",
|
||||
noAnswer(nsC), bothAnswered, false,
|
||||
},
|
||||
{
|
||||
"the zone's delegation lists only nsA now",
|
||||
noAnswer(nsA), bothAnswered, false,
|
||||
},
|
||||
{"no root server answered", errNoReply, bothAnswered, false},
|
||||
{"no previous check", noAnswer(nsA, nsB), nil, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := watcher.FailedNameservers(tt.err, tt.prev)
|
||||
if (got != nil) != tt.wantFailed {
|
||||
t.Errorf("returned %v, want failed: %v", got, tt.wantFailed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNameserverThatNeverAnswers asks a nameserver address where
|
||||
// nothing answers, 192.0.2.1, and checks what the watcher saves for it.
|
||||
// The deadline outlasts the resolver's first two-second try, as in the
|
||||
|
||||
Reference in New Issue
Block a user