resolver, watcher: a domain's nameservers are only its own delegation (closes #222)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
When a domain's parent zone's servers answer NXDOMAIN, LookupNS returns ErrNXDomain. The watcher then saves the domain with no nameservers and nxdomain set, shown on the dashboard and in /api/v1/status, asks for none of its records and removes those saved, so its old nameservers go in one NS Change. A domain with no delegation of its own gets an empty set and its records are still asked at the zone it is in. FindAuthoritativeNameservers moves to a parent name only on one of those two answers; when the servers do not answer, it returns the error. After an upgrade, a domain without its own delegation that was saved with its parent zone's nameservers gets one NS Change; the README says so. Model: opus-5-5
This commit was merged in pull request #250.
This commit is contained in:
@@ -11,7 +11,9 @@ import (
|
||||
|
||||
// DNSResolver performs iterative DNS resolution.
|
||||
type DNSResolver interface {
|
||||
// LookupNS discovers authoritative nameservers for a domain.
|
||||
// LookupNS returns a domain's NS record set, as its parent zone's
|
||||
// servers delegate it: empty when they answer that it has none, and
|
||||
// resolver.ErrNXDomain when they answer that it does not exist.
|
||||
LookupNS(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
|
||||
@@ -289,6 +289,13 @@ func (w *Watcher) checkDomain(
|
||||
domain string,
|
||||
) {
|
||||
nameservers, err := w.resolver.LookupNS(ctx, domain)
|
||||
|
||||
// A domain that does not exist has no nameservers.
|
||||
nxdomain := errors.Is(err, resolver.ErrNXDomain)
|
||||
if nxdomain {
|
||||
nameservers, err = []string{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.logFailedLookup(
|
||||
ctx,
|
||||
@@ -323,9 +330,18 @@ func (w *Watcher) checkDomain(
|
||||
w.state.SetDomainState(domain, &state.DomainState{
|
||||
Nameservers: nameservers,
|
||||
NameserverAddresses: addresses,
|
||||
NXDomain: nxdomain,
|
||||
LastChecked: now,
|
||||
})
|
||||
|
||||
// A domain that does not exist has no records of its own: none are
|
||||
// asked for, and those saved by an earlier check are removed.
|
||||
if nxdomain {
|
||||
w.state.DeleteHostnameState(domain)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The apex domain's records are also checked and saved as a
|
||||
// hostname's, so that the port and TLS checks find its addresses.
|
||||
// Notifications about them name it as a domain (see nameLine).
|
||||
|
||||
@@ -482,6 +482,119 @@ func TestNSChangeDetection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDomainThatDoesNotExist checks a .com domain that does not exist,
|
||||
// with nameservers and records saved by an earlier check. The .com
|
||||
// servers answer that it does not exist, so it is saved with nxdomain
|
||||
// set and no nameservers, an NS Change removes them all, and its saved
|
||||
// records are removed rather than asked for at the .com servers.
|
||||
func TestDomainThatDoesNotExist(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const domain = "dnswatcher-test-does-not-exist.com"
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{domain}
|
||||
|
||||
var deps *testDeps
|
||||
|
||||
livednstest.Retry(t, "watcher checks", func(ctx context.Context) error {
|
||||
var w *watcher.Watcher
|
||||
|
||||
w, deps = newTestWatcher(t, cfg)
|
||||
|
||||
deps.state.SetDomainState(domain, &state.DomainState{
|
||||
Nameservers: []string{oldNS1, oldNS2},
|
||||
})
|
||||
deps.state.SetHostnameState(domain, &state.HostnameState{
|
||||
RecordsByNameserver: map[string]*state.NameserverRecordState{
|
||||
oldNS1: {
|
||||
Records: map[string][]string{"A": {oldIP}},
|
||||
Status: "ok",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
started := time.Now()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
// When no server answered, the domain's state is not saved.
|
||||
ds, _ := deps.state.GetDomainState(domain)
|
||||
if ds.LastChecked.Before(started) {
|
||||
return fmt.Errorf("%s: %w", domain, livednstest.ErrNoAnswer)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
ds, _ := deps.state.GetDomainState(domain)
|
||||
if !ds.NXDomain || len(ds.Nameservers) != 0 {
|
||||
t.Errorf("saved nxdomain %v and nameservers %v, want true and none",
|
||||
ds.NXDomain, ds.Nameservers)
|
||||
}
|
||||
|
||||
if hs, ok := deps.state.GetHostnameState(domain); ok {
|
||||
t.Errorf("records saved for %s: %v", domain, hs.RecordsByNameserver)
|
||||
}
|
||||
|
||||
assertNotified(t, deps, "NS Change: "+domain, "warning")
|
||||
|
||||
// That is the only notification, and it removes both nameservers,
|
||||
// in either order.
|
||||
for _, n := range deps.notifier.getNotifications() {
|
||||
removed := strings.TrimPrefix(
|
||||
n.Message, "Domain: "+domain+"\nAdded: \nRemoved: ",
|
||||
)
|
||||
if removed != oldNS1+", "+oldNS2 && removed != oldNS2+", "+oldNS1 {
|
||||
t.Errorf("unexpected notification: %v", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDomainWithNoDelegationOfItsOwn checks a domain with no delegation
|
||||
// of its own: codeberg.page is on the public suffix list, so
|
||||
// docs.codeberg.page is a domain, but the .page servers delegate only
|
||||
// codeberg.page, whose servers answer for it. It is saved with no
|
||||
// nameservers and without nxdomain, and its records, asked at the
|
||||
// codeberg.page servers, are saved. Those are testSmallDomain's two
|
||||
// nameservers; github.io, the zone of the README's example, has eight.
|
||||
func TestDomainWithNoDelegationOfItsOwn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const domain = "docs.codeberg.page"
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{domain}
|
||||
|
||||
var deps *testDeps
|
||||
|
||||
livednstest.Retry(t, "watcher checks", func(ctx context.Context) error {
|
||||
var w *watcher.Watcher
|
||||
|
||||
w, deps = newTestWatcher(t, cfg)
|
||||
|
||||
err := checkOnce(ctx, w, deps)
|
||||
|
||||
// A domain saved as not existing has no records to wait for;
|
||||
// the checks below fail on it.
|
||||
if ds, ok := deps.state.GetDomainState(domain); ok && ds.NXDomain {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
ds, _ := deps.state.GetDomainState(domain)
|
||||
if ds.NXDomain || len(ds.Nameservers) != 0 {
|
||||
t.Errorf("saved nxdomain %v and nameservers %v, want false and none",
|
||||
ds.NXDomain, ds.Nameservers)
|
||||
}
|
||||
|
||||
if _, ok := deps.state.GetHostnameState(domain); !ok {
|
||||
t.Errorf("no records saved for %s", domain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSAddressChangeDetection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user