Skip to content

Commit 9c73cf2

Browse files
committed
Fix NS delegation check: query parent zone authoritative nameservers
Direct NS queries for subdomain delegations (e.g. t.example.com) return NXDOMAIN at recursive resolvers. New two-strategy approach: 1. Try direct NS query (fast path) 2. Fall back to querying parent zone's authoritative NS for the delegation Tested and verified working with real subdomain delegations.
1 parent 814c2a5 commit 9c73cf2

2 files changed

Lines changed: 88 additions & 12 deletions

File tree

cmd/scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func printPreFlight(ipCount int, domain, dnsttBin, slipstreamBin string, steps [
242242
fmt.Fprintf(w, " %s\u2718%s Domain: %s%s%s — %sNS delegation NOT found!%s\n",
243243
colorRed, colorReset, colorCyan, domain, colorReset, colorRed, colorReset)
244244
fmt.Fprintf(w, " %sTunnel/e2e steps will likely fail. Verify your DNS setup:%s\n", colorDim, colorReset)
245-
fmt.Fprintf(w, " %snslookup -type=NS %s 8.8.8.8%s\n", colorDim, domain, colorReset)
245+
fmt.Fprintf(w, " %sdig NS %s @8.8.8.8 (or check your registrar/Cloudflare dashboard)%s\n", colorDim, domain, colorReset)
246246
}
247247
}
248248
fmt.Fprintf(w, "\n")

internal/scanner/dns.go

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package scanner
33
import (
44
"context"
55
"net"
6+
"strings"
67
"time"
78

89
"github.com/miekg/dns"
@@ -152,27 +153,102 @@ func QueryNSMulti(domain string, timeout time.Duration) ([]string, bool) {
152153
}
153154

154155
func QueryNS(resolver, domain string, timeout time.Duration) ([]string, bool) {
156+
// Strategy 1: direct NS query — works when the recursive resolver returns
157+
// the delegation NS in Answer or Authority.
155158
r, ok := queryRaw(resolver, domain, dns.TypeNS, timeout)
156-
if !ok {
159+
if ok {
160+
var hosts []string
161+
for _, ans := range r.Answer {
162+
if ns, ok := ans.(*dns.NS); ok {
163+
hosts = append(hosts, ns.Ns)
164+
}
165+
}
166+
if len(hosts) == 0 {
167+
for _, ans := range r.Ns {
168+
if ns, ok := ans.(*dns.NS); ok {
169+
hosts = append(hosts, ns.Ns)
170+
}
171+
}
172+
}
173+
if len(hosts) > 0 {
174+
return hosts, true
175+
}
176+
}
177+
178+
// Strategy 2: query the parent zone's authoritative nameservers directly.
179+
// For "t.example.com", find NS of "example.com", then ask those servers
180+
// for NS of "t.example.com". This is how subdomain delegation actually
181+
// works in the DNS hierarchy.
182+
parent := parentZone(domain)
183+
if parent == "" {
157184
return nil, false
158185
}
159-
var hosts []string
160-
// Check Answer section first
161-
for _, ans := range r.Answer {
186+
// Get parent zone NS from the resolver
187+
pr, pok := queryRaw(resolver, parent, dns.TypeNS, timeout)
188+
if !pok {
189+
return nil, false
190+
}
191+
var parentNS []string
192+
for _, ans := range pr.Answer {
162193
if ns, ok := ans.(*dns.NS); ok {
163-
hosts = append(hosts, ns.Ns)
194+
parentNS = append(parentNS, ns.Ns)
164195
}
165196
}
166-
// For subdomain delegations, NS records are often in the Authority section
167-
if len(hosts) == 0 {
168-
for _, ans := range r.Ns {
197+
if len(parentNS) == 0 {
198+
return nil, false
199+
}
200+
201+
// Resolve the first parent NS to an IP and query it directly
202+
for _, nsHost := range parentNS {
203+
nsHost = strings.TrimSuffix(nsHost, ".")
204+
// Resolve the NS hostname to an IP via the same resolver
205+
ar, aok := queryRaw(resolver, nsHost, dns.TypeA, timeout)
206+
if !aok {
207+
continue
208+
}
209+
var nsIP string
210+
for _, ans := range ar.Answer {
211+
if a, ok := ans.(*dns.A); ok {
212+
nsIP = a.A.String()
213+
break
214+
}
215+
}
216+
if nsIP == "" {
217+
continue
218+
}
219+
// Ask the parent's authoritative NS for the subdomain's NS records
220+
dr, dok := queryRaw(nsIP, domain, dns.TypeNS, timeout)
221+
if !dok {
222+
continue
223+
}
224+
var hosts []string
225+
for _, ans := range dr.Answer {
169226
if ns, ok := ans.(*dns.NS); ok {
170227
hosts = append(hosts, ns.Ns)
171228
}
172229
}
230+
if len(hosts) == 0 {
231+
for _, ans := range dr.Ns {
232+
if ns, ok := ans.(*dns.NS); ok {
233+
hosts = append(hosts, ns.Ns)
234+
}
235+
}
236+
}
237+
if len(hosts) > 0 {
238+
return hosts, true
239+
}
173240
}
174-
if len(hosts) == 0 {
175-
return nil, false
241+
return nil, false
242+
}
243+
244+
// parentZone returns the parent zone of a domain.
245+
// e.g. "t.example.com" → "example.com", "example.com" → "com"
246+
// Returns "" if the domain has no parent (is a TLD or empty).
247+
func parentZone(domain string) string {
248+
domain = strings.TrimSuffix(domain, ".")
249+
parts := strings.SplitN(domain, ".", 2)
250+
if len(parts) < 2 || parts[1] == "" {
251+
return ""
176252
}
177-
return hosts, true
253+
return parts[1]
178254
}

0 commit comments

Comments
 (0)