Files
selfpost/internal/dnscheck/resolver_test.go
T
mixandClaude Opus 5.5 006bed7022
test / test (push) Canceled after 0s
audit 4: DNS outages unknown, DMARC limits said, typed Health, one TLS mode per upstream
M14, M15: queryEach gives each resolver 2 s within the check's context and
asks the one that answered last first; an address or PTR lookup not
answered is Unknown, an answered mismatch beside it still an error;
Checker.Server keeps the last answer's addresses, so SPF does not go Unknown
with it. Notifications report faults only, now tested with SPF unknown
alone and beside a wrong DKIM.

DMARC reports: tests of malformed XML, gzip and zip bombs, a message over
the limit, the service's Resync, hosted suggestion and validation. The
finding: both limits cut silently and an oversized report failed as
damaged; readAtMost refuses it for its size (MaxMessage 10 MiB, moved into
IngestMessage).

healthReadings replaces map[string]any for Health and Overview.

L5: an inbound upstream has one TLS mode - a second domain to the same
[host]:port with another mode is refused, naming the first; the TLS policy
map holds each next hop once.

Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 631 s) on this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 12:15:58 +03:00

136 lines
4.4 KiB
Go

package dnscheck
import (
"context"
"errors"
"net"
"testing"
"time"
)
func TestWithDefaultPort(t *testing.T) {
cases := map[string]string{
"1.1.1.1": "1.1.1.1:53",
"1.1.1.1:5353": "1.1.1.1:5353",
"dns.example.com": "dns.example.com:53",
"2606:4700:4700::": "[2606:4700:4700::]:53",
"[::1]:5353": "[::1]:5353",
}
for in, want := range cases {
if got := withDefaultPort(in); got != want {
t.Errorf("withDefaultPort(%q) = %q, want %q", in, got, want)
}
}
}
func TestParseResolvers(t *testing.T) {
got := ParseResolvers(" 1.1.1.1 , ,8.8.8.8:53,")
want := []string{"1.1.1.1", "8.8.8.8:53"}
if len(got) != len(want) {
t.Fatalf("got %q, want %q", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("got %q, want %q", got, want)
}
}
if r := ParseResolvers(""); len(r) != 0 {
t.Errorf("empty string parsed to %q, want nothing", r)
}
}
func TestNewExternalResolverFallsBackToDefaults(t *testing.T) {
if n := len(newExternalResolver(nil).servers); n != len(DefaultResolvers) {
t.Errorf("resolver count = %d, want %d (the defaults)", n, len(DefaultResolvers))
}
}
// The checks must reach the second resolver when the first is unreachable, but
// must not second-guess an authoritative "no such name" — otherwise a domain
// that genuinely lacks a record costs one timeout per configured resolver.
func TestQueryEachTriesTheNextResolverOnlyOnFailure(t *testing.T) {
e := &externalResolver{servers: []*net.Resolver{{}, {}, {}}, timeout: time.Second}
asked := 0
got, err := queryEach(context.Background(), e, func(context.Context, int) (string, error) {
asked++
if asked < 3 {
return "", &net.DNSError{Err: "timed out", IsTimeout: true}
}
return "answer", nil
})
if err != nil || got != "answer" {
t.Fatalf("got (%q, %v), want (\"answer\", nil)", got, err)
}
if asked != 3 {
t.Errorf("asked %d resolvers, want 3", asked)
}
asked = 0
_, err = queryEach(context.Background(), e, func(context.Context, int) (string, error) {
asked++
return "", notFound("absent.example")
})
var dnsErr *net.DNSError
if !errors.As(err, &dnsErr) || !dnsErr.IsNotFound {
t.Fatalf("err = %v, want a not-found DNSError", err)
}
if asked != 1 {
t.Errorf("NXDOMAIN asked %d resolvers, want 1 — it is an answer, not a failure", asked)
}
}
// A first resolver that drops packets costs its own time, not the check's:
// the second is asked with time left, and answers; and the next question goes
// to the one that answered (audit 2026-10-12, M14).
func TestADroppingResolverLeavesTheNextItsTime(t *testing.T) {
e := &externalResolver{servers: []*net.Resolver{{}, {}, {}}, timeout: 50 * time.Millisecond}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
var order []int
drops := func(ctx context.Context, i int) (string, error) {
order = append(order, i)
if i == 0 {
<-ctx.Done() // a resolver that never answers
return "", &net.DNSError{Err: "i/o timeout", IsTimeout: true}
}
if err := ctx.Err(); err != nil {
return "", err
}
return "answer", nil
}
start := time.Now()
got, err := queryEach(ctx, e, drops)
if err != nil || got != "answer" {
t.Fatalf("got (%q, %v): the second resolver was not given its time", got, err)
}
if took := time.Since(start); took > 500*time.Millisecond {
t.Errorf("the dropping resolver took %v of the check's second", took)
}
order = nil
if _, err := queryEach(ctx, e, drops); err != nil || len(order) != 1 || order[0] != 1 {
t.Errorf("the next question went to %v (%v), want the resolver that answered", order, err)
}
}
// The reason this package does not use net.DefaultResolver: systemd-resolved
// answers PTR queries for the machine's own addresses out of the local
// hostname, which is not what the rest of the internet sees. The dial hook must
// therefore ignore the address the standard resolver picked from
// /etc/resolv.conf and connect to the configured one.
func TestExternalResolverDialsOnlyTheConfiguredAddress(t *testing.T) {
e := newExternalResolver([]string{"192.0.2.53"})
if len(e.servers) != 1 {
t.Fatalf("servers = %d, want 1", len(e.servers))
}
conn, err := e.servers[0].Dial(context.Background(), "udp", "127.0.0.53:53")
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
if got := conn.RemoteAddr().String(); got != "192.0.2.53:53" {
t.Errorf("connected to %q, want 192.0.2.53:53 (the system resolver won)", got)
}
}