Improve DNS output: distinguish NS resolution failures from domain errors (#20)
CI / test (push) Failing after 2m19s

This commit was merged in pull request #20.
This commit is contained in:
2026-06-08 05:59:33 +00:00
parent d6063eb636
commit 25e6039bde
7 changed files with 32 additions and 6 deletions
+2
View File
@@ -144,6 +144,8 @@ func summaryTypeLabel(respType string) string {
return "query type not implemented by server" return "query type not implemented by server"
case "cname_loop": case "cname_loop":
return "resulted in a CNAME loop" return "resulted in a CNAME loop"
case "ns_error":
return "nameserver lookup failed"
case "error": case "error":
return "resulted in an error" return "resulted in an error"
case "referral": case "referral":
+1
View File
@@ -88,6 +88,7 @@ func TestSummaryTypeLabelAllTypes(t *testing.T) {
"refused": "query refused by server", "refused": "query refused by server",
"notimp": "query type not implemented by server", "notimp": "query type not implemented by server",
"cname_loop": "resulted in a CNAME loop", "cname_loop": "resulted in a CNAME loop",
"ns_error": "nameserver lookup failed",
"error": "resulted in an error", "error": "resulted in an error",
"referral": "resulted in a referral", "referral": "resulted in a referral",
"unknown_type": "unknown_type", "unknown_type": "unknown_type",
+6
View File
@@ -220,6 +220,12 @@ func (f *textFormatter) formatResultLine(result traverse.TraversalResult) string
msg = result.Response.ErrorMessage msg = result.Response.ErrorMessage
} }
return f.colorize(fmt.Sprintf("%s %s", prob, msg), colorRed) return f.colorize(fmt.Sprintf("%s %s", prob, msg), colorRed)
case traverse.RespNSResolutionFailed:
msg := "nameserver lookup failed"
if result.Response.ErrorMessage != "" {
msg = result.Response.ErrorMessage
}
return f.colorize(fmt.Sprintf("%s %s", prob, msg), colorYellow)
case traverse.RespError: case traverse.RespError:
msg := "resulted in an error" msg := "resulted in an error"
if result.Response.ErrorMessage != "" { if result.Response.ErrorMessage != "" {
+1
View File
@@ -121,6 +121,7 @@ Answers: []miekgdns.RR{
{"refused", traverse.RespREFUSED, nil, ""}, {"refused", traverse.RespREFUSED, nil, ""},
{"notimp", traverse.RespNOTIMPL, nil, ""}, {"notimp", traverse.RespNOTIMPL, nil, ""},
{"cname_loop", traverse.RespCNAMELoop, nil, "loop detected"}, {"cname_loop", traverse.RespCNAMELoop, nil, "loop detected"},
{"ns_error", traverse.RespNSResolutionFailed, nil, "nameserver ns1.example.com could not be resolved"},
{"error", traverse.RespError, nil, "something went wrong"}, {"error", traverse.RespError, nil, "something went wrong"},
} }
+8 -1
View File
@@ -20,6 +20,11 @@ const (
RespNOTIMPL RespNOTIMPL
RespCNAMELoop RespCNAMELoop
RespError RespError
// RespNSResolutionFailed indicates that the traversal could not resolve the
// IP address of an in-bailiwick nameserver. The domain may still be
// reachable in practice (e.g. via glue records held by the registry), but
// the iterative traversal could not complete that path.
RespNSResolutionFailed
) )
func (rt ResponseType) String() string { func (rt ResponseType) String() string {
@@ -44,6 +49,8 @@ func (rt ResponseType) String() string {
return "cname_loop" return "cname_loop"
case RespError: case RespError:
return "error" return "error"
case RespNSResolutionFailed:
return "ns_error"
default: default:
return "unknown" return "unknown"
} }
@@ -231,7 +238,7 @@ func (r *Response) resolveGlue(child *Referral) {
func (r *Response) IsTerminal() bool { func (r *Response) IsTerminal() bool {
switch r.Type { switch r.Type {
case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError: case RespAnswer, RespNODATA, RespNXDOMAIN, RespSERVFAIL, RespREFUSED, RespNOTIMPL, RespCNAMELoop, RespError, RespNSResolutionFailed:
return true return true
default: default:
return false return false
+2
View File
@@ -246,6 +246,7 @@ func TestResponseIsTerminal(t *testing.T) {
{RespNXDOMAIN, true}, {RespNXDOMAIN, true},
{RespSERVFAIL, true}, {RespSERVFAIL, true},
{RespError, true}, {RespError, true},
{RespNSResolutionFailed, true},
{RespReferral, false}, {RespReferral, false},
{RespCNAMEFollow, false}, {RespCNAMEFollow, false},
} }
@@ -272,6 +273,7 @@ func TestResponseTypeString(t *testing.T) {
{RespNXDOMAIN, "nxdomain"}, {RespNXDOMAIN, "nxdomain"},
{RespSERVFAIL, "servfail"}, {RespSERVFAIL, "servfail"},
{RespError, "error"}, {RespError, "error"},
{RespNSResolutionFailed, "ns_error"},
} }
for _, tt := range tests { for _, tt := range tests {
+10 -3
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"strings"
"sync" "sync"
"time" "time"
@@ -231,10 +232,15 @@ func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *I
ref.State = StateResolved ref.State = StateResolved
} else { } else {
addrs, err := t.ResolveNS(ctx, ref.Name, cache, visitedCopy, t.depth) addrs, err := t.ResolveNS(ctx, ref.Name, cache, visitedCopy, t.depth)
nsName := strings.TrimSuffix(ref.Bailiwick, ".")
if nsName == "" || nsName == "." {
nsName = strings.TrimSuffix(ref.Name, ".")
}
if err != nil { if err != nil {
return &Response{ return &Response{
Referral: ref, Referral: ref,
Type: RespError, Type: RespNSResolutionFailed,
ErrorMessage: fmt.Sprintf("nameserver %s could not be resolved", nsName),
} }
} }
if len(addrs) > 0 { if len(addrs) > 0 {
@@ -243,7 +249,8 @@ func (t *Traverser) processReferral(ctx context.Context, ref *Referral, cache *I
} else { } else {
return &Response{ return &Response{
Referral: ref, Referral: ref,
Type: RespError, Type: RespNSResolutionFailed,
ErrorMessage: fmt.Sprintf("nameserver %s could not be resolved", nsName),
} }
} }
} }
@@ -360,7 +367,7 @@ func (t *Traverser) ResolveNS(ctx context.Context, nsName string, cache *InfoCac
break break
} }
if resp.Type == RespSERVFAIL || resp.Type == RespError { if resp.Type == RespSERVFAIL || resp.Type == RespError || resp.Type == RespNSResolutionFailed {
lastErr = fmt.Errorf("server error resolving %s: %s", nsName, resp.Type) lastErr = fmt.Errorf("server error resolving %s: %s", nsName, resp.Type)
continue continue
} }