Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac4a17352a | ||
|
|
fe01cdda1e |
@@ -633,7 +633,8 @@ repository's `Dockerfile` and runs it. The app needs:
|
||||
abandoned, and the number abandoned is logged at warn level rather
|
||||
than dropped silently. Notifications generated after shutdown has
|
||||
begun are refused and logged, so a late burst cannot extend the
|
||||
shutdown.
|
||||
shutdown. A DNS lookup, port check or TLS check that shutdown cuts
|
||||
short saves nothing and sends no notification.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ https://git.eeqj.de/sneak/dnswatcher/issues/107
|
||||
|
||||
- 2026-10-01: each nameserver's addresses are saved with its domain, and a
|
||||
change while it stays in the delegation is notified (closes #105).
|
||||
- 2026-10-01: a port or TLS check that shutdown cuts short saves nothing and
|
||||
sends no notification, as a cut-short DNS lookup already did (closes #185).
|
||||
- 2026-10-01: the client address from `X-Forwarded-For` is the last entry that
|
||||
is not a trusted proxy, not the first, which the client sets (closes #181).
|
||||
- 2026-10-01: a nameserver that does not answer is saved as `error` with the
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package watcher_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
||||
"sneak.berlin/go/dnswatcher/internal/resolver"
|
||||
"sneak.berlin/go/dnswatcher/internal/state"
|
||||
"sneak.berlin/go/dnswatcher/internal/tlscheck"
|
||||
"sneak.berlin/go/dnswatcher/internal/watcher"
|
||||
)
|
||||
|
||||
// TestCancelledCheckSavesNothing runs a check with its context already
|
||||
// cancelled, which is how the rest of a check runs once shutdown cuts it
|
||||
// short. The real resolver drops the DNS lookup without sending a query,
|
||||
// and the real port and TLS checkers fail without connecting. The port
|
||||
// and certificate state the last check saved must stay as it was, and
|
||||
// nothing may be notified.
|
||||
func TestCancelledCheckSavesNothing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Hostnames = []string{host}
|
||||
|
||||
// newTestWatcher's watcher has stand-in checkers. This one, on the
|
||||
// same state and notifier, has the real ones.
|
||||
_, deps := newTestWatcher(t, cfg)
|
||||
w := watcher.NewForTest(
|
||||
cfg,
|
||||
deps.state,
|
||||
resolver.NewFromLogger(slog.Default()),
|
||||
portcheck.NewStandalone(),
|
||||
tlscheck.NewStandalone(),
|
||||
deps.notifier,
|
||||
)
|
||||
|
||||
// The last check found host at a local address, with both ports
|
||||
// open and a good certificate.
|
||||
const localIP = "127.0.0.1"
|
||||
|
||||
deps.state.SetHostnameState(host, hostnameState(
|
||||
map[string]map[string][]string{nsA: {"A": {localIP}}},
|
||||
))
|
||||
|
||||
ports := map[string]*state.PortState{
|
||||
localIP + ":80": {Open: true, Hostnames: []string{host}},
|
||||
localIP + ":443": {Open: true, Hostnames: []string{host}},
|
||||
}
|
||||
for key, ps := range ports {
|
||||
deps.state.SetPortState(key, ps)
|
||||
}
|
||||
|
||||
certKey := localIP + ":443:" + host
|
||||
cert := &state.CertificateState{CommonName: host, Status: "ok"}
|
||||
deps.state.SetCertificateState(certKey, cert)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
for key, want := range ports {
|
||||
got, _ := deps.state.GetPortState(key)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("port %s saved as %+v, want %+v", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
got, _ := deps.state.GetCertificateState(certKey)
|
||||
if !reflect.DeepEqual(got, cert) {
|
||||
t.Errorf("certificate saved as %+v, want %+v", got, cert)
|
||||
}
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
if len(notifications) != 0 {
|
||||
t.Errorf("sent %v, want no notifications", notifications)
|
||||
}
|
||||
}
|
||||
@@ -118,8 +118,10 @@ func TestNSAddressChangeAlertNamesDomainNameserverAndAddresses(
|
||||
}
|
||||
|
||||
// TestNameserverWithNoAddressKeepsPrevious looks up nameserver names
|
||||
// under .invalid, which live DNS never resolves. The one with addresses
|
||||
// saved by the previous check keeps them; the one without gets none.
|
||||
// with no address: two under .invalid, whose lookup fails with an
|
||||
// error, and one that does not exist under a real zone, which live DNS
|
||||
// answers with no address and no error. Each one with addresses saved
|
||||
// by the previous check keeps them; the one without gets none.
|
||||
func TestNameserverWithNoAddressKeepsPrevious(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -127,7 +129,9 @@ func TestNameserverWithNoAddressKeepsPrevious(t *testing.T) {
|
||||
nil, nil, resolver.NewFromLogger(slog.Default()), nil, nil, nil,
|
||||
)
|
||||
|
||||
prev := map[string][]string{oldNS1: {oldIP}}
|
||||
nonexistentNS := "this-surely-does-not-exist-xyz." + testSmallDomain + "."
|
||||
|
||||
prev := map[string][]string{oldNS1: {oldIP}, nonexistentNS: {oldIP}}
|
||||
|
||||
var got map[string][]string
|
||||
|
||||
@@ -135,7 +139,7 @@ func TestNameserverWithNoAddressKeepsPrevious(t *testing.T) {
|
||||
// lookup is not retried.
|
||||
_ = livednstest.Run(func(ctx context.Context) error {
|
||||
got = w.ResolveNameserverAddresses(
|
||||
ctx, []string{oldNS1, oldNS2}, prev,
|
||||
ctx, []string{oldNS1, oldNS2, nonexistentNS}, prev,
|
||||
)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -737,6 +737,13 @@ func (w *Watcher) checkSinglePort(
|
||||
hostnames []string,
|
||||
) {
|
||||
result, err := w.portCheck.CheckPort(ctx, ip, port)
|
||||
|
||||
// A check the context cut short says nothing about the port, so it
|
||||
// is neither saved nor notified.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
w.log.Error(
|
||||
"port check failed",
|
||||
@@ -812,6 +819,13 @@ func (w *Watcher) checkTLSCert(
|
||||
hostname string,
|
||||
) {
|
||||
cert, err := w.tlsCheck.CheckCertificate(ctx, ip, hostname)
|
||||
|
||||
// A check the context cut short says nothing about the certificate,
|
||||
// so it is neither saved nor notified.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
certKey := fmt.Sprintf("%s:%d:%s", ip, tlsPort, hostname)
|
||||
now := time.Now().UTC()
|
||||
prev, hasPrev := w.state.GetCertificateState(certKey)
|
||||
|
||||
@@ -27,11 +27,12 @@ import (
|
||||
// so tests assert on what the watcher does with the answers, never on
|
||||
// the records these zones publish. testHost's nameservers and addresses
|
||||
// stay the same from one check to the next, which the tests that check
|
||||
// it twice rely on, and so do the addresses of testSmallDomain's
|
||||
// nameservers. A domain check looks up each nameserver's addresses,
|
||||
// about a second per nameserver, so the tests that check a domain use
|
||||
// testSmallDomain, which has two nameservers. The tests that query
|
||||
// testDomain's nameservers directly do no domain check.
|
||||
// it twice rely on, and testSmallDomain's nameservers stay the same
|
||||
// between a test looking them up and its check. A domain check looks up
|
||||
// each nameserver's addresses, about a second per nameserver, so the
|
||||
// tests that check a domain use testSmallDomain, which has two
|
||||
// nameservers, and check it once. The tests that query testDomain's
|
||||
// nameservers directly do no domain check.
|
||||
const (
|
||||
testDomain = "google.com"
|
||||
testSmallDomain = "example.com"
|
||||
@@ -284,6 +285,26 @@ func runChecks(
|
||||
return deps
|
||||
}
|
||||
|
||||
// lookupNameservers returns the nameservers live DNS lists for domain,
|
||||
// for a test to save in the state its check starts from.
|
||||
func lookupNameservers(t *testing.T, domain string) []string {
|
||||
t.Helper()
|
||||
|
||||
res := resolver.NewFromLogger(slog.Default())
|
||||
|
||||
var nameservers []string
|
||||
|
||||
livednstest.Retry(t, "LookupNS("+domain+")", func(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
nameservers, err = res.LookupNS(ctx, domain)
|
||||
|
||||
return err
|
||||
})
|
||||
|
||||
return nameservers
|
||||
}
|
||||
|
||||
// addresses returns the A and AAAA values saved for a hostname.
|
||||
func addresses(hs *state.HostnameState) []string {
|
||||
var ips []string
|
||||
@@ -451,16 +472,21 @@ func TestNSAddressChangeDetection(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testSmallDomain}
|
||||
|
||||
// Between the checks, save for every nameserver an address live DNS
|
||||
// never returns.
|
||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
||||
ds, _ := deps.state.GetDomainState(testSmallDomain)
|
||||
for _, ns := range ds.Nameservers {
|
||||
ds.NameserverAddresses[ns] = []string{oldIP}
|
||||
nameservers := lookupNameservers(t, testSmallDomain)
|
||||
|
||||
// The saved state lists the nameservers live DNS lists, each at an
|
||||
// address live DNS never returns.
|
||||
deps := runChecks(t, cfg, func(deps *testDeps) {
|
||||
nsAddresses := make(map[string][]string, len(nameservers))
|
||||
for _, ns := range nameservers {
|
||||
nsAddresses[ns] = []string{oldIP}
|
||||
}
|
||||
|
||||
deps.state.SetDomainState(testSmallDomain, ds)
|
||||
})
|
||||
deps.state.SetDomainState(testSmallDomain, &state.DomainState{
|
||||
Nameservers: nameservers,
|
||||
NameserverAddresses: nsAddresses,
|
||||
})
|
||||
}, nil)
|
||||
|
||||
title := "NS Address Change: " + testSmallDomain
|
||||
ds, _ := deps.state.GetDomainState(testSmallDomain)
|
||||
@@ -501,17 +527,17 @@ func TestNSAddedAndRemovedIsNoAddressChange(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testSmallDomain}
|
||||
|
||||
// Between the checks, replace the first nameserver in the saved
|
||||
// state with one live DNS does not list, so that the next check
|
||||
// finds the first nameserver added and that one removed.
|
||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
||||
ds, _ := deps.state.GetDomainState(testSmallDomain)
|
||||
delete(ds.NameserverAddresses, ds.Nameservers[0])
|
||||
ds.NameserverAddresses[oldNS1] = []string{oldIP}
|
||||
ds.Nameservers = append([]string{oldNS1}, ds.Nameservers[1:]...)
|
||||
nameservers := lookupNameservers(t, testSmallDomain)
|
||||
|
||||
deps.state.SetDomainState(testSmallDomain, ds)
|
||||
})
|
||||
// The saved state lists oldNS1, which live DNS does not, in place of
|
||||
// the first nameserver live DNS lists, so that the check finds that
|
||||
// one added and oldNS1 removed. Only oldNS1 has addresses saved.
|
||||
deps := runChecks(t, cfg, func(deps *testDeps) {
|
||||
deps.state.SetDomainState(testSmallDomain, &state.DomainState{
|
||||
Nameservers: append([]string{oldNS1}, nameservers[1:]...),
|
||||
NameserverAddresses: map[string][]string{oldNS1: {oldIP}},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
if n := countNotifications(deps, "NS Change: "+testSmallDomain); n != 1 {
|
||||
t.Errorf("sent %d NS changes, want 1", n)
|
||||
|
||||
Reference in New Issue
Block a user