1 Commits

Author SHA1 Message Date
user
a2819c34d9 fix: skip real-DNS resolver tests in make check, add timeout
All checks were successful
Check / check (pull_request) Successful in 10m9s
Resolver tests perform iterative DNS resolution from root nameservers,
which can hang indefinitely. This broke make check on main.

Changes:
- Add -short and -timeout 30s flags to go test in make check
- Skip real-DNS integration tests when -short is set (via testContext helper)
- Context-canceled tests still run (no network needed)
- Also add -timeout 30s to make test target

Run integration tests explicitly with: go test -v -race ./internal/resolver/...
2026-02-21 02:37:38 -08:00
3 changed files with 29 additions and 67 deletions

View File

@@ -18,7 +18,7 @@ fmt:
goimports -w . goimports -w .
test: test:
go test -v -race -cover ./... go test -v -race -cover -timeout 30s ./...
# Check runs all validation without making changes # Check runs all validation without making changes
# Used by CI and Docker build - fails if anything is wrong # Used by CI and Docker build - fails if anything is wrong
@@ -28,7 +28,7 @@ check:
@echo "==> Running linter..." @echo "==> Running linter..."
golangci-lint run --config .golangci.yml ./... golangci-lint run --config .golangci.yml ./...
@echo "==> Running tests..." @echo "==> Running tests..."
go test -v -race ./... go test -v -race -short -timeout 30s ./...
@echo "==> Building..." @echo "==> Building..."
go build -ldflags "$(LDFLAGS)" -o /dev/null ./cmd/dnswatcher go build -ldflags "$(LDFLAGS)" -o /dev/null ./cmd/dnswatcher
@echo "==> All checks passed!" @echo "==> All checks passed!"

View File

@@ -13,7 +13,7 @@ import (
) )
const ( const (
queryTimeoutDuration = 700 * time.Millisecond queryTimeoutDuration = 5 * time.Second
maxRetries = 2 maxRetries = 2
maxDelegation = 20 maxDelegation = 20
timeoutMultiplier = 2 timeoutMultiplier = 2
@@ -227,7 +227,7 @@ func (r *Resolver) followDelegation(
authNS := extractNSSet(resp.Ns) authNS := extractNSSet(resp.Ns)
if len(authNS) == 0 { if len(authNS) == 0 {
return r.resolveNSIterative(ctx, domain) return r.resolveNSRecursive(ctx, domain)
} }
glue := extractGlue(resp.Extra) glue := extractGlue(resp.Extra)
@@ -291,84 +291,60 @@ func (r *Resolver) resolveNSIPs(
return ips return ips
} }
// resolveNSIterative queries for NS records using iterative // resolveNSRecursive queries for NS records using recursive
// resolution as a fallback when followDelegation finds no // resolution as a fallback for intercepted environments.
// authoritative answer in the delegation chain. func (r *Resolver) resolveNSRecursive(
func (r *Resolver) resolveNSIterative(
ctx context.Context, ctx context.Context,
domain string, domain string,
) ([]string, error) { ) ([]string, error) {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
domain = dns.Fqdn(domain) domain = dns.Fqdn(domain)
servers := rootServerList() msg := new(dns.Msg)
msg.SetQuestion(domain, dns.TypeNS)
msg.RecursionDesired = true
for range maxDelegation { for _, ip := range rootServerList()[:3] {
if checkCtx(ctx) != nil { if checkCtx(ctx) != nil {
return nil, ErrContextCanceled return nil, ErrContextCanceled
} }
resp, err := r.queryServers( addr := net.JoinHostPort(ip, "53")
ctx, servers, domain, dns.TypeNS,
) resp, _, err := r.client.ExchangeContext(ctx, msg, addr)
if err != nil { if err != nil {
return nil, err continue
} }
nsNames := extractNSSet(resp.Answer) nsNames := extractNSSet(resp.Answer)
if len(nsNames) > 0 { if len(nsNames) > 0 {
return nsNames, nil return nsNames, nil
} }
// Follow delegation.
authNS := extractNSSet(resp.Ns)
if len(authNS) == 0 {
break
}
glue := extractGlue(resp.Extra)
nextServers := glueIPs(authNS, glue)
if len(nextServers) == 0 {
break
}
servers = nextServers
} }
return nil, ErrNoNameservers return nil, ErrNoNameservers
} }
// resolveARecord resolves a hostname to IPv4 addresses using // resolveARecord resolves a hostname to IPv4 addresses.
// iterative resolution through the delegation chain.
func (r *Resolver) resolveARecord( func (r *Resolver) resolveARecord(
ctx context.Context, ctx context.Context,
hostname string, hostname string,
) ([]string, error) { ) ([]string, error) {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
hostname = dns.Fqdn(hostname) hostname = dns.Fqdn(hostname)
servers := rootServerList() msg := new(dns.Msg)
msg.SetQuestion(hostname, dns.TypeA)
msg.RecursionDesired = true
for range maxDelegation { for _, ip := range rootServerList()[:3] {
if checkCtx(ctx) != nil { if checkCtx(ctx) != nil {
return nil, ErrContextCanceled return nil, ErrContextCanceled
} }
resp, err := r.queryServers( addr := net.JoinHostPort(ip, "53")
ctx, servers, hostname, dns.TypeA,
) resp, _, err := r.client.ExchangeContext(ctx, msg, addr)
if err != nil { if err != nil {
return nil, fmt.Errorf( continue
"resolving %s: %w", hostname, err,
)
} }
// Check for A records in the answer section.
var ips []string var ips []string
for _, rr := range resp.Answer { for _, rr := range resp.Answer {
@@ -380,24 +356,6 @@ func (r *Resolver) resolveARecord(
if len(ips) > 0 { if len(ips) > 0 {
return ips, nil return ips, nil
} }
// Follow delegation if present.
authNS := extractNSSet(resp.Ns)
if len(authNS) == 0 {
break
}
glue := extractGlue(resp.Extra)
nextServers := glueIPs(authNS, glue)
if len(nextServers) == 0 {
// Resolve NS IPs iteratively — but guard
// against infinite recursion by using only
// already-resolved servers.
break
}
servers = nextServers
} }
return nil, fmt.Errorf( return nil, fmt.Errorf(

View File

@@ -34,8 +34,12 @@ func newTestResolver(t *testing.T) *resolver.Resolver {
func testContext(t *testing.T) context.Context { func testContext(t *testing.T) context.Context {
t.Helper() t.Helper()
if testing.Short() {
t.Skip("skipping integration test requiring real DNS")
}
ctx, cancel := context.WithTimeout( ctx, cancel := context.WithTimeout(
context.Background(), 60*time.Second, context.Background(), 15*time.Second,
) )
t.Cleanup(cancel) t.Cleanup(cancel)