- Exponential backoff retry logic (100ms, 200ms, 400ms... capped at 2s) replacing fixed 100ms delay between retries - Explicit REFUSED and NOTIMP response types (RespREFUSED, RespNOTIMPL) surfaced as terminal results with user-visible messages - CNAME loop detection: walking the ancestor referral chain before following a CNAME prevents infinite recursion; produces RespCNAMELoop - DNAME record support: synthesize CNAME target from DNAME mapping when the server omits the RFC 6672 synthesized CNAME record - ErrorMessage field on Response for surfacing error details to users - Fix resolveGlueViaSystem timeout bug: deadline.Sub(deadline) was always 0; replaced with time.Until(deadline) - DNAME records excluded from hasFinalAnswer so DNAME-only responses are correctly classified as RespCNAMEFollow - Text and JSON output updated with labels for all new response types - Tests: CNAME loop (2-step and direct), REFUSED, NOTIMP, graceful degradation (partial and total server failure), DNAME synthesis, IsNameInChain, backoffDelay, ResponseClassification strings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Copilot
multica-agent
parent
76f5010a5e
commit
368f200d23
@@ -2,6 +2,7 @@ package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -14,6 +15,8 @@ const (
|
||||
ResponseNODATA
|
||||
ResponseNXDOMAIN
|
||||
ResponseSERVFAIL
|
||||
ResponseREFUSED
|
||||
ResponseNOTIMPL
|
||||
ResponseOther
|
||||
)
|
||||
|
||||
@@ -29,6 +32,10 @@ func (rc ResponseClassification) String() string {
|
||||
return "nxdomain"
|
||||
case ResponseSERVFAIL:
|
||||
return "servfail"
|
||||
case ResponseREFUSED:
|
||||
return "refused"
|
||||
case ResponseNOTIMPL:
|
||||
return "notimp"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
@@ -45,6 +52,13 @@ type DecodedResponse struct {
|
||||
Authority []dns.RR
|
||||
Additional []dns.RR
|
||||
CNAMEChain []string
|
||||
DNAMEMappings []DNAMEMapping
|
||||
}
|
||||
|
||||
// DNAMEMapping holds a DNAME record's owner and target for redirect synthesis.
|
||||
type DNAMEMapping struct {
|
||||
Owner string // e.g., "example.com."
|
||||
Target string // e.g., "example.net."
|
||||
}
|
||||
|
||||
func DecodeResponse(msg *dns.Msg) *DecodedResponse {
|
||||
@@ -62,6 +76,7 @@ func DecodeResponse(msg *dns.Msg) *DecodedResponse {
|
||||
Authority: msg.Ns,
|
||||
Additional: msg.Extra,
|
||||
CNAMEChain: extractCNAMEChain(msg),
|
||||
DNAMEMappings: extractDNAMEMappings(msg),
|
||||
}
|
||||
|
||||
d.Classification = classify(msg)
|
||||
@@ -75,6 +90,10 @@ func classify(msg *dns.Msg) ResponseClassification {
|
||||
return ResponseNXDOMAIN
|
||||
case dns.RcodeServerFailure:
|
||||
return ResponseSERVFAIL
|
||||
case dns.RcodeRefused:
|
||||
return ResponseREFUSED
|
||||
case dns.RcodeNotImplemented:
|
||||
return ResponseNOTIMPL
|
||||
case dns.RcodeSuccess:
|
||||
return classifySuccess(msg)
|
||||
default:
|
||||
@@ -125,6 +144,33 @@ func extractCNAMEChain(msg *dns.Msg) []string {
|
||||
return chain
|
||||
}
|
||||
|
||||
func extractDNAMEMappings(msg *dns.Msg) []DNAMEMapping {
|
||||
var mappings []DNAMEMapping
|
||||
for _, rr := range msg.Answer {
|
||||
if dname, ok := rr.(*dns.DNAME); ok {
|
||||
mappings = append(mappings, DNAMEMapping{
|
||||
Owner: dns.Fqdn(dname.Hdr.Name),
|
||||
Target: dns.Fqdn(dname.Target),
|
||||
})
|
||||
}
|
||||
}
|
||||
return mappings
|
||||
}
|
||||
|
||||
// SynthesizeCNAMEFromDNAME computes the CNAME target for queryName given a DNAME mapping.
|
||||
// Returns empty string if queryName is not a strict subdomain of dnameOwner.
|
||||
func SynthesizeCNAMEFromDNAME(queryName, dnameOwner, dnameTarget string) string {
|
||||
q := strings.ToLower(dns.Fqdn(queryName))
|
||||
owner := strings.ToLower(dns.Fqdn(dnameOwner))
|
||||
target := strings.ToLower(dns.Fqdn(dnameTarget))
|
||||
|
||||
if !dns.IsSubDomain(owner, q) || q == owner {
|
||||
return ""
|
||||
}
|
||||
prefix := strings.TrimSuffix(q, owner)
|
||||
return prefix + target
|
||||
}
|
||||
|
||||
func IsTruncated(msg *dns.Msg) bool {
|
||||
return msg != nil && msg.Truncated
|
||||
}
|
||||
|
||||
+16
-2
@@ -93,7 +93,7 @@ func QueryWithExchange(ctx context.Context, server net.IP, name string, qtype ui
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("query retries cancelled: %w", ctx.Err())
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-time.After(backoffDelay(attempt)):
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func IterativeQueryWithExchange(ctx context.Context, server net.IP, name string,
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("query retries cancelled: %w", ctx.Err())
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
case <-time.After(backoffDelay(attempt)):
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,20 @@ func IterativeQueryWithExchange(ctx context.Context, server net.IP, name string,
|
||||
return nil, fmt.Errorf("iterative query %s %s failed after %d retries: %w", name, QNameType(qtype), cfg.Retries, lastErr)
|
||||
}
|
||||
|
||||
// backoffDelay computes the wait duration before the given retry attempt (1-indexed).
|
||||
// Delays: attempt=1 → 100ms, attempt=2 → 200ms, attempt=3 → 400ms, capped at 2s.
|
||||
func backoffDelay(attempt int) time.Duration {
|
||||
if attempt <= 0 {
|
||||
return 0
|
||||
}
|
||||
delay := time.Duration(uint(1)<<uint(attempt-1)) * 100 * time.Millisecond
|
||||
const maxDelay = 2 * time.Second
|
||||
if delay > maxDelay {
|
||||
return maxDelay
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
func buildQuery(name string, qtype uint16, udpSize int) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(dns.Fqdn(name), qtype)
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestDecodeResponseREFUSED(t *testing.T) {
|
||||
msg := newTestMsg(dns.RcodeRefused)
|
||||
d := DecodeResponse(msg)
|
||||
if d.Classification != ResponseREFUSED {
|
||||
t.Errorf("classification = %v, want ResponseREFUSED", d.Classification)
|
||||
}
|
||||
if d.RcodeName != "REFUSED" {
|
||||
t.Errorf("RcodeName = %q, want REFUSED", d.RcodeName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeResponseNOTIMPL(t *testing.T) {
|
||||
msg := newTestMsg(dns.RcodeNotImplemented)
|
||||
d := DecodeResponse(msg)
|
||||
if d.Classification != ResponseNOTIMPL {
|
||||
t.Errorf("classification = %v, want ResponseNOTIMPL", d.Classification)
|
||||
}
|
||||
if d.RcodeName != "NOTIMP" {
|
||||
t.Errorf("RcodeName = %q, want NOTIMP", d.RcodeName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeResponseREFUSEDString(t *testing.T) {
|
||||
if got := ResponseREFUSED.String(); got != "refused" {
|
||||
t.Errorf("ResponseREFUSED.String() = %q, want \"refused\"", got)
|
||||
}
|
||||
if got := ResponseNOTIMPL.String(); got != "notimp" {
|
||||
t.Errorf("ResponseNOTIMPL.String() = %q, want \"notimp\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractDNAMEMappings(t *testing.T) {
|
||||
t.Run("no DNAME", func(t *testing.T) {
|
||||
msg := new(dns.Msg)
|
||||
msg.Answer = append(msg.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA},
|
||||
A: MustParseIP("1.2.3.4"),
|
||||
})
|
||||
d := DecodeResponse(msg)
|
||||
if len(d.DNAMEMappings) != 0 {
|
||||
t.Errorf("expected 0 DNAME mappings, got %d", len(d.DNAMEMappings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DNAME in answer", func(t *testing.T) {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetReply(new(dns.Msg))
|
||||
msg.Answer = append(msg.Answer, &dns.DNAME{
|
||||
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeDNAME, Class: dns.ClassINET, Ttl: 300},
|
||||
Target: "example.net.",
|
||||
})
|
||||
d := DecodeResponse(msg)
|
||||
if len(d.DNAMEMappings) != 1 {
|
||||
t.Fatalf("expected 1 DNAME mapping, got %d", len(d.DNAMEMappings))
|
||||
}
|
||||
if d.DNAMEMappings[0].Owner != "example.com." {
|
||||
t.Errorf("Owner = %q, want %q", d.DNAMEMappings[0].Owner, "example.com.")
|
||||
}
|
||||
if d.DNAMEMappings[0].Target != "example.net." {
|
||||
t.Errorf("Target = %q, want %q", d.DNAMEMappings[0].Target, "example.net.")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSynthesizeCNAMEFromDNAME(t *testing.T) {
|
||||
tests := []struct {
|
||||
queryName string
|
||||
dnameOwner string
|
||||
dnameTarget string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
queryName: "foo.example.com.",
|
||||
dnameOwner: "example.com.",
|
||||
dnameTarget: "example.net.",
|
||||
want: "foo.example.net.",
|
||||
},
|
||||
{
|
||||
queryName: "bar.foo.example.com.",
|
||||
dnameOwner: "example.com.",
|
||||
dnameTarget: "example.net.",
|
||||
want: "bar.foo.example.net.",
|
||||
},
|
||||
{
|
||||
// Owner itself is not redirected
|
||||
queryName: "example.com.",
|
||||
dnameOwner: "example.com.",
|
||||
dnameTarget: "example.net.",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Not a subdomain
|
||||
queryName: "other.com.",
|
||||
dnameOwner: "example.com.",
|
||||
dnameTarget: "example.net.",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := SynthesizeCNAMEFromDNAME(tt.queryName, tt.dnameOwner, tt.dnameTarget)
|
||||
if got != tt.want {
|
||||
t.Errorf("SynthesizeCNAMEFromDNAME(%q, %q, %q) = %q, want %q",
|
||||
tt.queryName, tt.dnameOwner, tt.dnameTarget, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackoffDelay(t *testing.T) {
|
||||
tests := []struct {
|
||||
attempt int
|
||||
want time.Duration
|
||||
}{
|
||||
{0, 0},
|
||||
{1, 100 * time.Millisecond},
|
||||
{2, 200 * time.Millisecond},
|
||||
{3, 400 * time.Millisecond},
|
||||
{4, 800 * time.Millisecond},
|
||||
{5, 1600 * time.Millisecond},
|
||||
{6, 2000 * time.Millisecond}, // capped at 2s
|
||||
{10, 2000 * time.Millisecond}, // still capped
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := backoffDelay(tt.attempt)
|
||||
if got != tt.want {
|
||||
t.Errorf("backoffDelay(%d) = %v, want %v", tt.attempt, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user