check / check (push) Canceled after 0s
The health check answered ok while every DNS lookup failed, as on a network that intercepts DNS queries. A lookup whose root servers give no usable reply now fails with ErrNoRootServerAnswered. At the end of each round of DNS checks the watcher records whether every lookup failed with it or with ErrIntercepted; the health check then gives status error, and /.well-known/healthcheck and /health answer 503. Until the first round finishes it stays ok. The health check now depends on the watcher, so fx builds the watcher first: its first round starts before the HTTP server listens, and the server tests' fx graph includes it. NewWithRootServers lets tests start a resolver's walks at real servers that are not root servers. Model: opus-5-5
90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
// Package resolver provides iterative DNS resolution from root nameservers.
|
|
// It traces the full delegation chain from IANA root servers through TLD
|
|
// and domain nameservers, never relying on upstream recursive resolvers.
|
|
package resolver
|
|
|
|
import (
|
|
"log/slog"
|
|
|
|
"go.uber.org/fx"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/logger"
|
|
)
|
|
|
|
// Query status constants matching the state model.
|
|
const (
|
|
StatusOK = "ok"
|
|
StatusError = "error"
|
|
StatusNXDomain = "nxdomain"
|
|
StatusNoData = "nodata"
|
|
StatusTimeout = "timeout"
|
|
)
|
|
|
|
// MaxCNAMEDepth is the maximum CNAME chain depth to follow.
|
|
const MaxCNAMEDepth = 10
|
|
|
|
// Params contains dependencies for Resolver.
|
|
type Params struct {
|
|
fx.In
|
|
|
|
Logger *logger.Logger
|
|
}
|
|
|
|
// NameserverResponse holds one nameserver's response for a query.
|
|
// FailedTypes lists the record types whose query got no usable reply,
|
|
// and Records holds nothing for them: their records are not known. When
|
|
// no record type got one, Status and Error say the nameserver failed.
|
|
type NameserverResponse struct {
|
|
Nameserver string
|
|
Records map[string][]string
|
|
FailedTypes []string
|
|
Status string
|
|
Error string
|
|
}
|
|
|
|
// Resolver performs iterative DNS resolution from root servers.
|
|
// rootServers are the addresses every walk from the root starts at.
|
|
type Resolver struct {
|
|
log *slog.Logger
|
|
client DNSClient
|
|
tcp DNSClient
|
|
rootServers []string
|
|
}
|
|
|
|
// New creates a new Resolver instance for use with uber/fx.
|
|
func New(
|
|
_ fx.Lifecycle,
|
|
params Params,
|
|
) (*Resolver, error) {
|
|
return &Resolver{
|
|
log: params.Logger.Get(),
|
|
client: &udpClient{timeout: queryTimeoutDuration},
|
|
tcp: &tcpClient{timeout: queryTimeoutDuration},
|
|
rootServers: rootServerList(),
|
|
}, nil
|
|
}
|
|
|
|
// NewFromLogger creates a Resolver directly from an slog.Logger,
|
|
// useful for testing without the fx lifecycle.
|
|
func NewFromLogger(log *slog.Logger) *Resolver {
|
|
return &Resolver{
|
|
log: log,
|
|
client: &udpClient{timeout: queryTimeoutDuration},
|
|
tcp: &tcpClient{timeout: queryTimeoutDuration},
|
|
rootServers: rootServerList(),
|
|
}
|
|
}
|
|
|
|
// NewWithRootServers creates a Resolver as NewFromLogger does, except
|
|
// that every walk from the root starts at servers instead of at the root
|
|
// servers. Tests use it to show what dnswatcher does when no root server
|
|
// answers, or when something on the network answers in their place.
|
|
func NewWithRootServers(log *slog.Logger, servers []string) *Resolver {
|
|
r := NewFromLogger(log)
|
|
r.rootServers = servers
|
|
|
|
return r
|
|
}
|
|
|
|
// Method implementations are in iterative.go.
|