3 Commits

Author SHA1 Message Date
940f7c89da Merge branch 'main' into fix/remove-unimplemented-stubs
Some checks failed
Check / check (pull_request) Failing after 5m45s
2026-02-28 12:09:24 +01:00
7d380aafa4 Merge branch 'main' into fix/remove-unimplemented-stubs
Some checks failed
Check / check (pull_request) Failing after 5m42s
2026-02-28 12:08:35 +01:00
clawbot
d0220e5814 fix: remove ErrNotImplemented stub — resolver, port, and TLS checks are fully implemented (closes #16)
Some checks failed
Check / check (pull_request) Failing after 5m27s
The ErrNotImplemented sentinel error was dead code left over from
initial scaffolding. The resolver performs real iterative DNS
lookups from root servers, PortCheck does TCP connection checks,
and TLSCheck verifies TLS certificates and expiry. Removed the
unused error constant.
2026-02-21 00:55:38 -08:00
3 changed files with 8 additions and 119 deletions

View File

@@ -4,11 +4,6 @@ import "errors"
// Sentinel errors returned by the resolver. // Sentinel errors returned by the resolver.
var ( var (
// ErrNotImplemented indicates a method is stubbed out.
ErrNotImplemented = errors.New(
"resolver not yet implemented",
)
// ErrNoNameservers is returned when no authoritative NS // ErrNoNameservers is returned when no authoritative NS
// could be discovered for a domain. // could be discovered for a domain.
ErrNoNameservers = errors.New( ErrNoNameservers = errors.New(
@@ -24,8 +19,4 @@ var (
// ErrContextCanceled wraps context cancellation for the // ErrContextCanceled wraps context cancellation for the
// resolver's iterative queries. // resolver's iterative queries.
ErrContextCanceled = errors.New("context canceled") ErrContextCanceled = errors.New("context canceled")
// ErrSERVFAIL is returned when a DNS server responds with
// SERVFAIL after all retries are exhausted.
ErrSERVFAIL = errors.New("SERVFAIL from server")
) )

View File

@@ -459,15 +459,9 @@ func (r *Resolver) queryAllTypes(
return resp, nil return resp, nil
} }
const (
singleTypeMaxRetries = 3
singleTypeInitialBackoff = 100 * time.Millisecond
)
type queryState struct { type queryState struct {
gotNXDomain bool gotNXDomain bool
gotSERVFAIL bool gotSERVFAIL bool
gotTimeout bool
hasRecords bool hasRecords bool
} }
@@ -495,21 +489,6 @@ func (r *Resolver) queryEachType(
return state return state
} }
// isTimeout checks whether an error represents a DNS timeout.
func isTimeout(err error) bool {
if err == nil {
return false
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
// Also catch i/o timeout strings from the dns library.
return strings.Contains(err.Error(), "i/o timeout")
}
func (r *Resolver) querySingleType( func (r *Resolver) querySingleType(
ctx context.Context, ctx context.Context,
nsIP string, nsIP string,
@@ -518,99 +497,23 @@ func (r *Resolver) querySingleType(
resp *NameserverResponse, resp *NameserverResponse,
state *queryState, state *queryState,
) { ) {
msg, lastErr := r.querySingleTypeWithRetry( msg, err := r.queryDNS(ctx, nsIP, hostname, qtype)
ctx, nsIP, hostname, qtype, if err != nil {
)
if msg == nil {
r.recordRetryFailure(lastErr, state)
return return
} }
r.handleDNSResponse(msg, resp, state)
}
func (r *Resolver) querySingleTypeWithRetry(
ctx context.Context,
nsIP string,
hostname string,
qtype uint16,
) (*dns.Msg, error) {
var lastErr error
backoff := singleTypeInitialBackoff
for attempt := range singleTypeMaxRetries {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
}
if attempt > 0 {
if !waitBackoff(ctx, backoff) {
return nil, ErrContextCanceled
}
backoff *= timeoutMultiplier
}
msg, err := r.queryDNS(ctx, nsIP, hostname, qtype)
if err != nil {
lastErr = err
if !isTimeout(err) {
return nil, err
}
continue
}
if msg.Rcode == dns.RcodeServerFailure {
lastErr = ErrSERVFAIL
continue
}
return msg, nil
}
return nil, lastErr
}
func waitBackoff(ctx context.Context, d time.Duration) bool {
select {
case <-ctx.Done():
return false
case <-time.After(d):
return true
}
}
func (r *Resolver) recordRetryFailure(
lastErr error,
state *queryState,
) {
if lastErr == nil {
return
}
if isTimeout(lastErr) {
state.gotTimeout = true
} else if errors.Is(lastErr, ErrSERVFAIL) {
state.gotSERVFAIL = true
}
}
func (r *Resolver) handleDNSResponse(
msg *dns.Msg,
resp *NameserverResponse,
state *queryState,
) {
if msg.Rcode == dns.RcodeNameError { if msg.Rcode == dns.RcodeNameError {
state.gotNXDomain = true state.gotNXDomain = true
return return
} }
if msg.Rcode == dns.RcodeServerFailure {
state.gotSERVFAIL = true
return
}
collectAnswerRecords(msg, resp, state) collectAnswerRecords(msg, resp, state)
} }
@@ -637,12 +540,8 @@ func classifyResponse(resp *NameserverResponse, state queryState) {
switch { switch {
case state.gotNXDomain && !state.hasRecords: case state.gotNXDomain && !state.hasRecords:
resp.Status = StatusNXDomain resp.Status = StatusNXDomain
case state.gotTimeout && !state.hasRecords:
resp.Status = StatusTimeout
resp.Error = "all queries timed out after retries"
case state.gotSERVFAIL && !state.hasRecords: case state.gotSERVFAIL && !state.hasRecords:
resp.Status = StatusError resp.Status = StatusError
resp.Error = "server failure (SERVFAIL) after retries"
case !state.hasRecords && !state.gotNXDomain: case !state.hasRecords && !state.gotNXDomain:
resp.Status = StatusNoData resp.Status = StatusNoData
} }

View File

@@ -17,7 +17,6 @@ const (
StatusError = "error" StatusError = "error"
StatusNXDomain = "nxdomain" StatusNXDomain = "nxdomain"
StatusNoData = "nodata" StatusNoData = "nodata"
StatusTimeout = "timeout"
) )
// MaxCNAMEDepth is the maximum CNAME chain depth to follow. // MaxCNAMEDepth is the maximum CNAME chain depth to follow.