watcher: notify nameserver address changes (closes #105)
check / check (push) Failing after 3m10s
check / check (push) Failing after 3m10s
Each domain check now looks up the addresses every nameserver's name resolves to, with the resolver's ResolveIPAddresses, and saves them sorted in the domain's state. A nameserver that stays in the delegation and resolves to different addresses sends one NS Address Change notification naming the domain, the nameserver and the old and new addresses. Added or removed nameservers get only the NS change notification. A failed or empty lookup keeps the previous addresses, because the resolver returns no address without an error when every server it asks times out. State files without the field load, and the next check fills it in silently. Watcher tests that run domain checks use example.com, which has two nameservers, to stay within the per-attempt limit. Model: opus-5-5
This commit is contained in:
@@ -216,13 +216,25 @@ func (w *Watcher) checkDomain(
|
||||
now := time.Now().UTC()
|
||||
|
||||
prev, hasPrev := w.state.GetDomainState(domain)
|
||||
|
||||
var prevAddresses map[string][]string
|
||||
if hasPrev {
|
||||
prevAddresses = prev.NameserverAddresses
|
||||
}
|
||||
|
||||
addresses := w.resolveNameserverAddresses(
|
||||
ctx, nameservers, prevAddresses,
|
||||
)
|
||||
|
||||
if hasPrev && !w.firstRun {
|
||||
w.detectNSChanges(ctx, domain, prev.Nameservers, nameservers)
|
||||
w.detectNSAddressChanges(ctx, domain, prevAddresses, addresses)
|
||||
}
|
||||
|
||||
w.state.SetDomainState(domain, &state.DomainState{
|
||||
Nameservers: nameservers,
|
||||
LastChecked: now,
|
||||
Nameservers: nameservers,
|
||||
NameserverAddresses: addresses,
|
||||
LastChecked: now,
|
||||
})
|
||||
|
||||
// Also look up A/AAAA records for the apex domain so that
|
||||
@@ -290,6 +302,73 @@ func (w *Watcher) detectNSChanges(
|
||||
)
|
||||
}
|
||||
|
||||
// resolveNameserverAddresses returns the sorted addresses each
|
||||
// nameserver's name resolves to. A nameserver whose lookup fails or
|
||||
// finds no address keeps its addresses from prev: the resolver finds no
|
||||
// address, without an error, when every server it asks times out, and
|
||||
// that is not an address change.
|
||||
func (w *Watcher) resolveNameserverAddresses(
|
||||
ctx context.Context,
|
||||
nameservers []string,
|
||||
prev map[string][]string,
|
||||
) map[string][]string {
|
||||
addresses := make(map[string][]string, len(nameservers))
|
||||
|
||||
for _, ns := range nameservers {
|
||||
ips, err := w.resolver.ResolveIPAddresses(ctx, ns)
|
||||
if err == nil && len(ips) > 0 {
|
||||
sort.Strings(ips)
|
||||
addresses[ns] = ips
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
w.log.Error(
|
||||
"no addresses found for nameserver",
|
||||
"nameserver", ns,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
if prevIPs, ok := prev[ns]; ok {
|
||||
addresses[ns] = prevIPs
|
||||
}
|
||||
}
|
||||
|
||||
return addresses
|
||||
}
|
||||
|
||||
// detectNSAddressChanges notifies when a nameserver in both checks
|
||||
// resolves to different addresses. A nameserver added or removed is
|
||||
// reported by detectNSChanges alone, and one with no addresses saved by
|
||||
// the previous check, as in a state file from before they were saved,
|
||||
// is not compared.
|
||||
func (w *Watcher) detectNSAddressChanges(
|
||||
ctx context.Context,
|
||||
domain string,
|
||||
prev, current map[string][]string,
|
||||
) {
|
||||
for ns, cur := range current {
|
||||
old, ok := prev[ns]
|
||||
if !ok || sliceEqual(old, cur) {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(
|
||||
"Domain: %s\nNameserver: %s\nOld: %s\nNew: %s",
|
||||
domain, ns,
|
||||
strings.Join(old, ", "),
|
||||
strings.Join(cur, ", "),
|
||||
)
|
||||
|
||||
w.notify.SendNotification(
|
||||
ctx,
|
||||
"NS Address Change: "+domain,
|
||||
msg,
|
||||
"warning",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Watcher) checkHostname(
|
||||
ctx context.Context,
|
||||
hostname string,
|
||||
|
||||
Reference in New Issue
Block a user