Add resolver API definition and comprehensive live-DNS test suite

The test suite defines the full resolver contract using live DNS
queries against controlled records in the sneak.cloud zone
(Cloudflare). Covers FindAuthoritativeNameservers, QueryNameserver,
QueryAllNameservers, LookupNS, and ResolveIPAddresses, including
sorting/determinism guarantees, trailing-dot handling, per-NS
response status model, lame-delegation detection, NXDOMAIN
semantics, CNAME following, and context cancellation.

Also adds DNSSEC validation to planned future features in README.
This commit is contained in:
2026-07-07 03:06:49 +02:00
parent 144a2df665
commit 483bed68a1
5 changed files with 1136 additions and 16 deletions

View File

@@ -377,6 +377,13 @@ docker run -d \
--- ---
## Planned Future Features (Post-1.0)
- **DNSSEC validation**: Validate the DNSSEC chain of trust during
iterative resolution and report DNSSEC failures as notifications.
---
## Project Structure ## Project Structure
Follows the conventions defined in `CONVENTIONS.md`, adapted from the Follows the conventions defined in `CONVENTIONS.md`, adapted from the

4
go.mod
View File

@@ -9,16 +9,19 @@ require (
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
go.uber.org/fx v1.24.0 go.uber.org/fx v1.24.0
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect
@@ -36,4 +39,5 @@ require (
golang.org/x/sys v0.35.0 // indirect golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -0,0 +1,27 @@
package resolver
import "errors"
// Sentinel errors returned by the resolver.
var (
// ErrNotImplemented indicates a method is stubbed out.
ErrNotImplemented = errors.New(
"resolver not yet implemented",
)
// ErrNoNameservers is returned when no authoritative NS
// could be discovered for a domain.
ErrNoNameservers = errors.New(
"no authoritative nameservers found",
)
// ErrCNAMEDepthExceeded is returned when a CNAME chain
// exceeds MaxCNAMEDepth.
ErrCNAMEDepthExceeded = errors.New(
"CNAME chain depth exceeded",
)
// ErrContextCanceled wraps context cancellation for the
// resolver's iterative queries.
ErrContextCanceled = errors.New("context canceled")
)

View File

@@ -1,9 +1,10 @@
// Package resolver provides iterative DNS resolution from root nameservers. // 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 package resolver
import ( import (
"context" "context"
"errors"
"log/slog" "log/slog"
"go.uber.org/fx" "go.uber.org/fx"
@@ -11,8 +12,16 @@ import (
"sneak.berlin/go/dnswatcher/internal/logger" "sneak.berlin/go/dnswatcher/internal/logger"
) )
// ErrNotImplemented indicates the resolver is not yet implemented. // Query status constants matching the state model.
var ErrNotImplemented = errors.New("resolver not yet implemented") const (
StatusOK = "ok"
StatusError = "error"
StatusNXDomain = "nxdomain"
StatusNoData = "nodata"
)
// MaxCNAMEDepth is the maximum CNAME chain depth to follow.
const MaxCNAMEDepth = 10
// Params contains dependencies for Resolver. // Params contains dependencies for Resolver.
type Params struct { type Params struct {
@@ -21,12 +30,20 @@ type Params struct {
Logger *logger.Logger Logger *logger.Logger
} }
// NameserverResponse holds one nameserver's response for a query.
type NameserverResponse struct {
Nameserver string
Records map[string][]string
Status string
Error string
}
// Resolver performs iterative DNS resolution from root servers. // Resolver performs iterative DNS resolution from root servers.
type Resolver struct { type Resolver struct {
log *slog.Logger log *slog.Logger
} }
// New creates a new Resolver instance. // New creates a new Resolver instance for use with uber/fx.
func New( func New(
_ fx.Lifecycle, _ fx.Lifecycle,
params Params, params Params,
@@ -36,8 +53,48 @@ func New(
}, nil }, nil
} }
// LookupNS performs iterative resolution to find authoritative // NewFromLogger creates a Resolver directly from an slog.Logger,
// nameservers for the given domain. // useful for testing without the fx lifecycle.
func NewFromLogger(log *slog.Logger) *Resolver {
return &Resolver{log: log}
}
// FindAuthoritativeNameservers traces the delegation chain from
// root servers to discover all authoritative nameservers for the
// given domain. Returns the NS hostnames (e.g. ["ns1.example.com.",
// "ns2.example.com."]).
func (r *Resolver) FindAuthoritativeNameservers(
_ context.Context,
_ string,
) ([]string, error) {
return nil, ErrNotImplemented
}
// QueryNameserver queries a specific authoritative nameserver for
// all supported record types (A, AAAA, CNAME, MX, TXT, SRV, CAA,
// NS) for the given hostname. Returns a NameserverResponse with
// per-type record slices and a status indicating success or the
// type of failure.
func (r *Resolver) QueryNameserver(
_ context.Context,
_ string,
_ string,
) (*NameserverResponse, error) {
return nil, ErrNotImplemented
}
// QueryAllNameservers discovers the authoritative nameservers for
// the hostname's parent domain, then queries each one independently.
// Returns a map from nameserver hostname to its response.
func (r *Resolver) QueryAllNameservers(
_ context.Context,
_ string,
) (map[string]*NameserverResponse, error) {
return nil, ErrNotImplemented
}
// LookupNS returns the NS record set for a domain by performing
// iterative resolution. This is used for domain (apex) monitoring.
func (r *Resolver) LookupNS( func (r *Resolver) LookupNS(
_ context.Context, _ context.Context,
_ string, _ string,
@@ -45,17 +102,10 @@ func (r *Resolver) LookupNS(
return nil, ErrNotImplemented return nil, ErrNotImplemented
} }
// LookupAllRecords performs iterative resolution to find all DNS
// records for the given hostname.
func (r *Resolver) LookupAllRecords(
_ context.Context,
_ string,
) (map[string][]string, error) {
return nil, ErrNotImplemented
}
// ResolveIPAddresses resolves a hostname to all IPv4 and IPv6 // ResolveIPAddresses resolves a hostname to all IPv4 and IPv6
// addresses, following CNAME chains. // addresses by querying all authoritative nameservers and following
// CNAME chains up to MaxCNAMEDepth. Returns a deduplicated list
// of IP address strings.
func (r *Resolver) ResolveIPAddresses( func (r *Resolver) ResolveIPAddresses(
_ context.Context, _ context.Context,
_ string, _ string,

File diff suppressed because it is too large Load Diff