Files
dnswatcher/internal/watcher/cname_test.go
T
sneak 3472c8f4c1
check / check (push) Failing after 2m23s
watcher: follow a watched name's CNAME for port and TLS checks (closes #203)
When a watched name's nameservers answer with a CNAME and no address,
the DNS check asks ResolveIPAddresses for the name, which looks it up
again and follows the chain, and saves the addresses at its end in the
hostname state as cnameAddresses. The port and TLS checks use them.
Before, only the A and AAAA records in the answers were used, so a
CNAME into another zone got no port or TLS checks. When following
fails, the addresses the last check saved are kept. The domain check
now runs the hostname check for the apex instead of a copy of it.

Model: opus-5-5
2026-10-01 23:47:49 +00:00

88 lines
2.3 KiB
Go

package watcher_test
import (
"context"
"log/slog"
"slices"
"testing"
"sneak.berlin/go/dnswatcher/internal/livednstest"
"sneak.berlin/go/dnswatcher/internal/resolver"
"sneak.berlin/go/dnswatcher/internal/state"
"sneak.berlin/go/dnswatcher/internal/watcher"
)
// cnameHost is a CNAME into another zone: its nameservers answer with
// the CNAME and no address.
const cnameHost = "www.python.org"
// TestCNAMEIntoAnotherZonePortAndTLSChecks checks cnameHost against
// live DNS. Its port and TLS checks must use the addresses at the end
// of its CNAME chain.
func TestCNAMEIntoAnotherZonePortAndTLSChecks(t *testing.T) {
t.Parallel()
cfg := defaultTestConfig(t)
cfg.Hostnames = []string{cnameHost}
deps := runChecks(t, cfg, nil, nil)
snap := deps.state.GetSnapshot()
hs := snap.Hostnames[cnameHost]
if len(hs.CNAMEAddresses) == 0 {
t.Fatalf(
"%s: no addresses saved from following its CNAME; if it "+
"is no longer a CNAME into another zone, this test "+
"needs another name",
cnameHost,
)
}
for _, ip := range hs.CNAMEAddresses {
ps, ok := snap.Ports[ip+":443"]
if !ok || !slices.Contains(ps.Hostnames, cnameHost) {
t.Errorf("no port state for %s at %s:443", cnameHost, ip)
}
certKey := ip + ":443:" + cnameHost
if _, ok := snap.Certificates[certKey]; !ok {
t.Errorf("no certificate state %s", certKey)
}
}
}
// TestCNAMEThatCannotBeFollowedKeepsPrevious gives a name under
// .invalid, whose lookup fails, answers with a CNAME and no address.
// The addresses the previous check saved from following its CNAME are
// kept.
func TestCNAMEThatCannotBeFollowedKeepsPrevious(t *testing.T) {
t.Parallel()
const name = "www.example.invalid"
w := watcher.NewForTest(
nil, nil, resolver.NewFromLogger(slog.Default()), nil, nil, nil,
)
current := hostnameState(map[string]map[string][]string{
nsA: {"CNAME": {"target.example.invalid."}},
})
prev := &state.HostnameState{CNAMEAddresses: []string{oldIP}}
// The result is the same whether or not live DNS answers, so the
// lookup is not retried.
_ = livednstest.Run(func(ctx context.Context) error {
w.ResolveCNAMEAddresses(ctx, name, current, prev)
return nil
})
if !slices.Equal(current.CNAMEAddresses, prev.CNAMEAddresses) {
t.Errorf(
"saved %v, want %v",
current.CNAMEAddresses, prev.CNAMEAddresses,
)
}
}