@@ -3,6 +3,7 @@ package scanner
33import (
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
154155func 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