diff --git a/examples/all_nameservers_lookup/main.go b/examples/all_nameservers_lookup/main.go new file mode 100644 index 00000000..d1c55123 --- /dev/null +++ b/examples/all_nameservers_lookup/main.go @@ -0,0 +1,80 @@ +/* ZDNS Copyright 2024 Regents of the University of Michigan +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not +* use this file except in compliance with the License. You may obtain a copy +* of the License at http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +* implied. See the License for the specific language governing +* permissions and limitations under the License. + */ + +package main + +import ( + "net" + "time" + + "github.com/miekg/dns" + log "github.com/sirupsen/logrus" + + "github.com/zmap/zdns/examples/utils" + "github.com/zmap/zdns/src/zdns" +) + +func main() { + // Perform the lookup + domain := "google.com" + dnsQuestion := &zdns.Question{Name: domain, Type: dns.TypeA, Class: dns.ClassINET} + resolver := initializeResolver() + // LookupAllNameserversIterative will query all root nameservers, and then all TLD nameservers, and then all authoritative nameservers for the domain. + result, _, status, err := resolver.LookupAllNameserversIterative(dnsQuestion, nil) + if err != nil { + log.Fatal("Error looking up domain: ", err) + } + log.Warnf("Result: %v", result) + log.Warnf("Status: %v", status) + log.Info("We can also specify which root nameservers to use by setting the argument.") + + result, _, status, err = resolver.LookupAllNameserversIterative(dnsQuestion, []zdns.NameServer{{IP: net.ParseIP("198.41.0.4"), Port: 53}}) // a.root-servers.net + if err != nil { + log.Fatal("Error looking up domain: ", err) + } + log.Warnf("Result: %v", result) + log.Warnf("Status: %v", status) + + log.Info("You can query multiple recursive resolvers as well") + + externalResult, _, status, err := resolver.LookupAllNameserversExternal(dnsQuestion, []zdns.NameServer{{IP: net.ParseIP("1.1.1.1"), Port: 53}, {IP: net.ParseIP("8.8.8.8"), Port: 53}}) // Cloudflare and Google recursive resolvers, respectively + if err != nil { + log.Fatal("Error looking up domain: ", err) + } + log.Warnf("Result: %v", externalResult) + log.Warnf("Status: %v", status) + resolver.Close() +} + +func initializeResolver() *zdns.Resolver { + localAddr, err := utils.GetLocalIPByConnecting() + if err != nil { + log.Fatal("Error getting local IP: ", err) + } + // Create a ResolverConfig object + resolverConfig := zdns.NewResolverConfig() + // Set any desired options on the ResolverConfig object + resolverConfig.LogLevel = log.InfoLevel + resolverConfig.LocalAddrsV4 = []net.IP{localAddr} + resolverConfig.ExternalNameServersV4 = []zdns.NameServer{{IP: net.ParseIP("1.1.1.1"), Port: 53}} + resolverConfig.RootNameServersV4 = zdns.RootServersV4 + resolverConfig.IPVersionMode = zdns.IPv4Only + resolverConfig.Timeout = time.Minute + resolverConfig.IterativeTimeout = time.Minute + // Create a new Resolver object with the ResolverConfig object, it will retain all settings set on the ResolverConfig object + resolver, err := zdns.InitResolver(resolverConfig) + if err != nil { + log.Fatal("Error initializing resolver: ", err) + } + return resolver +} diff --git a/examples/multi_thread_lookup/multi_threaded.go b/examples/multi_thread_lookup/multi_threaded.go index b46fdc09..2a823082 100644 --- a/examples/multi_thread_lookup/multi_threaded.go +++ b/examples/multi_thread_lookup/multi_threaded.go @@ -14,6 +14,7 @@ package main import ( + "context" "net" "sync" @@ -44,7 +45,7 @@ func main() { wg.Add(2) go func() { defer wg.Done() - result1, _, _, err1 := resolver1.IterativeLookup(dnsQuestion1) + result1, _, _, err1 := resolver1.IterativeLookup(context.Background(), dnsQuestion1) if err1 != nil { log.Fatal("Error looking up domain: ", err1) } @@ -52,7 +53,7 @@ func main() { }() go func() { defer wg.Done() - result2, _, _, err2 := resolver2.IterativeLookup(dnsQuestion2) + result2, _, _, err2 := resolver2.IterativeLookup(context.Background(), dnsQuestion2) if err2 != nil { log.Fatal("Error looking up domain: ", err2) } diff --git a/examples/single_lookup/simple.go b/examples/single_lookup/simple.go index 74d975ea..9d7c9176 100644 --- a/examples/single_lookup/simple.go +++ b/examples/single_lookup/simple.go @@ -14,6 +14,7 @@ package main import ( + "context" "encoding/json" "net" @@ -30,7 +31,7 @@ func main() { dnsQuestion := &zdns.Question{Name: domain, Type: dns.TypeA, Class: dns.ClassINET} resolver := initializeResolver() - result, _, status, err := resolver.ExternalLookup(dnsQuestion, &zdns.NameServer{IP: net.ParseIP("1.1.1.1"), Port: 53}) + result, _, status, err := resolver.ExternalLookup(context.Background(), dnsQuestion, &zdns.NameServer{IP: net.ParseIP("1.1.1.1"), Port: 53}) if err != nil { log.Fatal("Error looking up domain: ", err) } @@ -44,7 +45,7 @@ func main() { log.Warn("\n\n This lookup just used the Cloudflare recursive resolver, let's run our own recursion.") // Iterative Lookups start at the root nameservers and follow the chain of referrals to the authoritative nameservers. - result, trace, status, err := resolver.IterativeLookup(&zdns.Question{Name: domain, Type: dns.TypeA, Class: dns.ClassINET}) + result, trace, status, err := resolver.IterativeLookup(context.Background(), &zdns.Question{Name: domain, Type: dns.TypeA, Class: dns.ClassINET}) if err != nil { log.Fatal("Error looking up domain: ", err) } diff --git a/src/cli/cli.go b/src/cli/cli.go index 1bf5b58e..26963bef 100644 --- a/src/cli/cli.go +++ b/src/cli/cli.go @@ -44,14 +44,14 @@ type StatusHandler interface { // GeneralOptions core options for all ZDNS modules // Order here is the order they'll be printed to the user, so preserve alphabetical order type GeneralOptions struct { - LookupAllNameServers bool `long:"all-nameservers" description:"Perform the lookup via all the nameservers for the domain."` + LookupAllNameServers bool `long:"all-nameservers" description:"Behavior is dependent on --iterative. In --iterative, --all-name-servers will query all root servers, then all gtld servers, etc. recording the responses at each layer. In non-iterative mode, the query will be sent to all external resolvers specified in --name-servers."` CacheSize int `long:"cache-size" default:"10000" description:"how many items can be stored in internal recursive cache"` GoMaxProcs int `long:"go-processes" default:"0" description:"number of OS processes (GOMAXPROCS by default)"` IterationTimeout int `long:"iteration-timeout" default:"8" description:"timeout for a single iterative step in an iterative query, in seconds. Only applicable with --iterative"` IterativeResolution bool `long:"iterative" description:"Perform own iteration instead of relying on recursive resolver"` MaxDepth int `long:"max-depth" default:"10" description:"how deep should we recurse when performing iterative lookups"` NameServerMode bool `long:"name-server-mode" description:"Treats input as nameservers to query with a static query rather than queries to send to a static name server"` - NameServersString string `long:"name-servers" description:"List of DNS servers to use. Can be passed as comma-delimited string or via @/path/to/file. If no port is specified, defaults to 53."` + NameServersString string `long:"name-servers" description:"List of DNS servers to use. Can be passed as comma-delimited string or via @/path/to/file. If no port is specified, defaults to 53. If not provided, defaults to either the default root servers in --iterative or the recursive resolvers specified in /etc/resolv.conf or OS equivalent."` UseNanoseconds bool `long:"nanoseconds" description:"Use nanosecond resolution timestamps in output"` NetworkTimeout int `long:"network-timeout" default:"2" description:"timeout for round trip network operations, in seconds"` DisableFollowCNAMEs bool `long:"no-follow-cnames" description:"do not follow CNAMEs/DNAMEs in the lookup process"` diff --git a/src/cli/config_validation.go b/src/cli/config_validation.go index 459d6774..14bc41ae 100644 --- a/src/cli/config_validation.go +++ b/src/cli/config_validation.go @@ -128,10 +128,6 @@ func validateClientSubnetString(gc *CLIConf) error { } func parseNameServers(gc *CLIConf) error { - if gc.LookupAllNameServers && gc.NameServersString != "" { - log.Fatal("name servers cannot be specified in --all-nameservers mode.") - } - if gc.NameServersString != "" { if gc.NameServerMode { log.Fatal("name servers cannot be specified on command line in --name-server-mode") diff --git a/src/cli/modules.go b/src/cli/modules.go index ec267b07..1b13e66b 100644 --- a/src/cli/modules.go +++ b/src/cli/modules.go @@ -14,6 +14,7 @@ package cli import ( + "context" "fmt" "github.com/miekg/dns" @@ -165,14 +166,23 @@ func (lm *BasicLookupModule) NewFlags() interface{} { return lm } +// Lookup performs a DNS lookup using the given resolver and lookupName. +// The behavior with respect to the nameServers is determined by the LookupAllNameServers and IsIterative fields. +// non-Iterative + all-Nameservers query -> we'll send a query to each of the resolver's external nameservers +// non-Iterative query -> we'll send a query to the nameserver provided. If none provided, a random nameserver from the resolver's external nameservers will be used +// iterative + all-Nameservers query -> we'll send a query to each root NS and query all nameservers down the chain. +// iterative query -> we'll send a query to a random root NS and query all nameservers down the chain. func (lm *BasicLookupModule) Lookup(resolver *zdns.Resolver, lookupName string, nameServer *zdns.NameServer) (interface{}, zdns.Trace, zdns.Status, error) { + if lm.LookupAllNameServers && lm.IsIterative { + return resolver.LookupAllNameserversIterative(&zdns.Question{Name: lookupName, Type: lm.DNSType, Class: lm.DNSClass}, nil) + } if lm.LookupAllNameServers { - return resolver.LookupAllNameservers(&zdns.Question{Name: lookupName, Type: lm.DNSType, Class: lm.DNSClass}, nameServer) + return resolver.LookupAllNameserversExternal(&zdns.Question{Name: lookupName, Type: lm.DNSType, Class: lm.DNSClass}, nil) } if lm.IsIterative { - return resolver.IterativeLookup(&zdns.Question{Name: lookupName, Type: lm.DNSType, Class: lm.DNSClass}) + return resolver.IterativeLookup(context.Background(), &zdns.Question{Name: lookupName, Type: lm.DNSType, Class: lm.DNSClass}) } - return resolver.ExternalLookup(&zdns.Question{Type: lm.DNSType, Class: lm.DNSClass, Name: lookupName}, nameServer) + return resolver.ExternalLookup(context.Background(), &zdns.Question{Type: lm.DNSType, Class: lm.DNSClass, Name: lookupName}, nameServer) } func GetLookupModule(name string) (LookupModule, error) { diff --git a/src/internal/util/util.go b/src/internal/util/util.go index 094c8608..61316aa2 100644 --- a/src/internal/util/util.go +++ b/src/internal/util/util.go @@ -57,9 +57,9 @@ func IsStringValidDomainName(domain string) bool { } // HasCtxExpired checks if the context has expired. Common function used in various places. -func HasCtxExpired(ctx *context.Context) bool { +func HasCtxExpired(ctx context.Context) bool { select { - case <-(*ctx).Done(): + case <-(ctx).Done(): return true default: return false diff --git a/src/modules/alookup/a_lookup.go b/src/modules/alookup/a_lookup.go index 72eeda1b..4b1cc8a8 100644 --- a/src/modules/alookup/a_lookup.go +++ b/src/modules/alookup/a_lookup.go @@ -34,6 +34,9 @@ func init() { // CLIInit initializes the ALookupModule with the given parameters, used to call ALOOKUP from the command line func (aMod *ALookupModule) CLIInit(gc *cli.CLIConf, resolverConfig *zdns.ResolverConfig) error { + if gc.LookupAllNameServers { + return errors.New("ALOOKUP module does not support --all-nameservers") + } aMod.Init(aMod.IPv4Lookup, aMod.IPv6Lookup) err := aMod.baseModule.CLIInit(gc, resolverConfig) if err != nil { diff --git a/src/modules/axfr/axfr.go b/src/modules/axfr/axfr.go index dc6c0498..a0ef6d88 100644 --- a/src/modules/axfr/axfr.go +++ b/src/modules/axfr/axfr.go @@ -151,6 +151,9 @@ func (axfrMod *AxfrLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfi if gc.IterativeResolution { log.Fatal("AXFR module does not support iterative resolution") } + if gc.LookupAllNameServers { + return errors.New("AXFR module does not support --all-nameservers") + } var err error if axfrMod.BlacklistPath != "" { axfrMod.Blacklist = safeblacklist.New() diff --git a/src/modules/bindversion/bindversion.go b/src/modules/bindversion/bindversion.go index d26bb676..729b73e1 100644 --- a/src/modules/bindversion/bindversion.go +++ b/src/modules/bindversion/bindversion.go @@ -15,7 +15,10 @@ package bindversion import ( + "context" + "github.com/miekg/dns" + "github.com/pkg/errors" "github.com/zmap/zdns/src/cli" "github.com/zmap/zdns/src/zdns" @@ -42,6 +45,9 @@ func init() { // CLIInit initializes the BindVersion lookup module func (bindVersionMod *BindVersionLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error { + if gc.LookupAllNameServers { + return errors.New("AXFR module does not support --all-nameservers") + } return bindVersionMod.BasicLookupModule.CLIInit(gc, rc) } @@ -51,9 +57,9 @@ func (bindVersionMod *BindVersionLookupModule) Lookup(r *zdns.Resolver, lookupNa var status zdns.Status var err error if bindVersionMod.IsIterative { - innerRes, trace, status, err = r.IterativeLookup(&zdns.Question{Name: BindVersionQueryName, Type: dns.TypeTXT, Class: dns.ClassCHAOS}) + innerRes, trace, status, err = r.IterativeLookup(context.Background(), &zdns.Question{Name: BindVersionQueryName, Type: dns.TypeTXT, Class: dns.ClassCHAOS}) } else { - innerRes, trace, status, err = r.ExternalLookup(&zdns.Question{Name: BindVersionQueryName, Type: dns.TypeTXT, Class: dns.ClassCHAOS}, nameServer) + innerRes, trace, status, err = r.ExternalLookup(context.Background(), &zdns.Question{Name: BindVersionQueryName, Type: dns.TypeTXT, Class: dns.ClassCHAOS}, nameServer) } resString, resStatus, err := zdns.CheckTxtRecords(innerRes, status, nil, err) res := Result{BindVersion: resString} diff --git a/src/modules/bindversion/bindversion_test.go b/src/modules/bindversion/bindversion_test.go index df38e4ef..8173b9c9 100644 --- a/src/modules/bindversion/bindversion_test.go +++ b/src/modules/bindversion/bindversion_test.go @@ -15,6 +15,7 @@ package bindversion import ( + "context" "net" "testing" @@ -35,7 +36,7 @@ var queries []QueryRecord // DoSingleDstServerLookup(r *Resolver, q Question, nameServer string, isIterative bool) (*SingleQueryResult, Trace, Status, error) type MockLookup struct{} -func (ml MockLookup) DoDstServersLookup(r *zdns.Resolver, question zdns.Question, nameServers []zdns.NameServer, isIterative bool) (*zdns.SingleQueryResult, zdns.Trace, zdns.Status, error) { +func (ml MockLookup) DoDstServersLookup(ctx context.Context, r *zdns.Resolver, question zdns.Question, nameServers []zdns.NameServer, isIterative bool) (*zdns.SingleQueryResult, zdns.Trace, zdns.Status, error) { queries = append(queries, QueryRecord{q: question, NameServer: &nameServers[0]}) if res, ok := mockResults[question.Name]; ok { return res, nil, zdns.StatusNoError, nil diff --git a/src/modules/dmarc/dmarc.go b/src/modules/dmarc/dmarc.go index 13b749ec..6c7fd34b 100644 --- a/src/modules/dmarc/dmarc.go +++ b/src/modules/dmarc/dmarc.go @@ -42,6 +42,9 @@ type DmarcLookupModule struct { // CLIInit initializes the DMARC lookup module func (dmarcMod *DmarcLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error { + if gc.LookupAllNameServers { + return errors.New("DMARC module does not support --all-nameservers") + } dmarcMod.re = regexp.MustCompile(dmarcPrefixRegexp) dmarcMod.BasicLookupModule.DNSType = dns.TypeTXT dmarcMod.BasicLookupModule.DNSClass = dns.ClassINET diff --git a/src/modules/dmarc/dmarc_test.go b/src/modules/dmarc/dmarc_test.go index 513cb533..d6d0c2b5 100644 --- a/src/modules/dmarc/dmarc_test.go +++ b/src/modules/dmarc/dmarc_test.go @@ -15,6 +15,7 @@ package dmarc import ( + "context" "net" "testing" @@ -35,7 +36,7 @@ var queries []QueryRecord type MockLookup struct{} -func (ml MockLookup) DoDstServersLookup(r *zdns.Resolver, question zdns.Question, nameServers []zdns.NameServer, isIterative bool) (*zdns.SingleQueryResult, zdns.Trace, zdns.Status, error) { +func (ml MockLookup) DoDstServersLookup(ctx context.Context, r *zdns.Resolver, question zdns.Question, nameServers []zdns.NameServer, isIterative bool) (*zdns.SingleQueryResult, zdns.Trace, zdns.Status, error) { queries = append(queries, QueryRecord{question, &nameServers[0]}) if res, ok := mockResults[question.Name]; ok { return res, nil, zdns.StatusNoError, nil diff --git a/src/modules/mxlookup/mx_lookup.go b/src/modules/mxlookup/mx_lookup.go index 5fdefcbc..40107e4d 100644 --- a/src/modules/mxlookup/mx_lookup.go +++ b/src/modules/mxlookup/mx_lookup.go @@ -14,6 +14,7 @@ package mxlookup import ( + "context" "strings" "github.com/miekg/dns" @@ -56,6 +57,9 @@ type MXLookupModule struct { // CLIInit initializes the MXLookupModule with the given parameters, used to call MXLookup from the command line func (mxMod *MXLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error { + if gc.LookupAllNameServers { + return errors.New("MXLOOKUP module does not support --all-nameservers") + } if !mxMod.IPv4Lookup && !mxMod.IPv6Lookup { // need to use one of the two mxMod.IPv4Lookup = true @@ -92,9 +96,9 @@ func (mxMod *MXLookupModule) Lookup(r *zdns.Resolver, lookupName string, nameSer var status zdns.Status var err error if mxMod.BasicLookupModule.IsIterative { - res, trace, status, err = r.IterativeLookup(&zdns.Question{Name: lookupName, Type: dns.TypeMX, Class: dns.ClassINET}) + res, trace, status, err = r.IterativeLookup(context.Background(), &zdns.Question{Name: lookupName, Type: dns.TypeMX, Class: dns.ClassINET}) } else { - res, trace, status, err = r.ExternalLookup(&zdns.Question{Name: lookupName, Type: dns.TypeMX, Class: dns.ClassINET}, nameServer) + res, trace, status, err = r.ExternalLookup(context.Background(), &zdns.Question{Name: lookupName, Type: dns.TypeMX, Class: dns.ClassINET}, nameServer) } if status != zdns.StatusNoError || err != nil { return nil, trace, status, err diff --git a/src/modules/nslookup/ns_lookup.go b/src/modules/nslookup/ns_lookup.go index f25ca877..1d314743 100644 --- a/src/modules/nslookup/ns_lookup.go +++ b/src/modules/nslookup/ns_lookup.go @@ -37,6 +37,9 @@ type NSLookupModule struct { // CLIInit initializes the NSLookupModule with the given parameters, used to call NSLookup from the command line func (nsMod *NSLookupModule) CLIInit(gc *cli.CLIConf, resolverConf *zdns.ResolverConfig) error { + if gc.LookupAllNameServers { + return errors.New("NSLOOKUP module does not support --all-nameservers") + } if !nsMod.IPv4Lookup && !nsMod.IPv6Lookup { log.Debug("NSModule: neither --ipv4-lookup nor --ipv6-lookup specified, will only request A records for each NS server") nsMod.IPv4Lookup = true diff --git a/src/modules/spf/spf.go b/src/modules/spf/spf.go index fa94cf91..702de2db 100644 --- a/src/modules/spf/spf.go +++ b/src/modules/spf/spf.go @@ -42,6 +42,9 @@ type SpfLookupModule struct { // CLIInit initializes the SPF lookup module func (spfMod *SpfLookupModule) CLIInit(gc *cli.CLIConf, rc *zdns.ResolverConfig) error { + if gc.LookupAllNameServers { + return errors.New("SPF module does not support --all-nameservers") + } spfMod.re = regexp.MustCompile(spfPrefixRegexp) spfMod.BasicLookupModule.DNSType = dns.TypeTXT spfMod.BasicLookupModule.DNSClass = dns.ClassINET diff --git a/src/modules/spf/spf_test.go b/src/modules/spf/spf_test.go index a5a04150..d204e1ae 100644 --- a/src/modules/spf/spf_test.go +++ b/src/modules/spf/spf_test.go @@ -15,6 +15,7 @@ package spf import ( + "context" "net" "testing" @@ -35,7 +36,7 @@ var queries []QueryRecord type MockLookup struct{} -func (ml MockLookup) DoDstServersLookup(r *zdns.Resolver, question zdns.Question, nameServers []zdns.NameServer, isIterative bool) (*zdns.SingleQueryResult, zdns.Trace, zdns.Status, error) { +func (ml MockLookup) DoDstServersLookup(ctx context.Context, r *zdns.Resolver, question zdns.Question, nameServers []zdns.NameServer, isIterative bool) (*zdns.SingleQueryResult, zdns.Trace, zdns.Status, error) { queries = append(queries, QueryRecord{question, &nameServers[0]}) if res, ok := mockResults[question.Name]; ok { return res, nil, zdns.StatusNoError, nil diff --git a/src/zdns/alookup.go b/src/zdns/alookup.go index 9520e1f8..9e4c1d1b 100644 --- a/src/zdns/alookup.go +++ b/src/zdns/alookup.go @@ -14,6 +14,7 @@ package zdns import ( + "context" "strings" "github.com/pkg/errors" @@ -23,7 +24,7 @@ import ( "github.com/zmap/zdns/src/internal/util" ) -// DoTargetedLookup performs a lookup of the given domain name against the given nameserver, looking up both IPv4 and IPv6 addresses +// DoTargetedLookup performs a lookup of the given name against the given nameserver, looking up both IPv4 and IPv6 addresses // Will follow CNAME records as well as A/AAAA records to get IP addresses func (r *Resolver) DoTargetedLookup(name string, nameServer *NameServer, isIterative, lookupA, lookupAAAA bool) (*IPResult, Trace, Status, error) { name = strings.ToLower(name) @@ -38,9 +39,9 @@ func (r *Resolver) DoTargetedLookup(name string, nameServer *NameServer, isItera var err error if lookupA && isIterative { - singleQueryRes, ipv4Trace, ipv4status, err = r.IterativeLookup(&Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET}) + singleQueryRes, ipv4Trace, ipv4status, err = r.IterativeLookup(context.Background(), &Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET}) } else if lookupA { - singleQueryRes, ipv4Trace, ipv4status, err = r.ExternalLookup(&Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET}, nameServer) + singleQueryRes, ipv4Trace, ipv4status, err = r.ExternalLookup(context.Background(), &Question{Name: name, Type: dns.TypeA, Class: dns.ClassINET}, nameServer) } ipv4, _ = getIPAddressesFromQueryResult(singleQueryRes, "A", name) if len(ipv4) > 0 { @@ -50,9 +51,9 @@ func (r *Resolver) DoTargetedLookup(name string, nameServer *NameServer, isItera } singleQueryRes = &SingleQueryResult{} // reset result if lookupAAAA && isIterative { - singleQueryRes, ipv6Trace, ipv6status, _ = r.IterativeLookup(&Question{Name: name, Type: dns.TypeAAAA, Class: dns.ClassINET}) + singleQueryRes, ipv6Trace, ipv6status, _ = r.IterativeLookup(context.Background(), &Question{Name: name, Type: dns.TypeAAAA, Class: dns.ClassINET}) } else if lookupAAAA { - singleQueryRes, ipv6Trace, ipv6status, _ = r.ExternalLookup(&Question{Name: name, Type: dns.TypeAAAA, Class: dns.ClassINET}, nameServer) + singleQueryRes, ipv6Trace, ipv6status, _ = r.ExternalLookup(context.Background(), &Question{Name: name, Type: dns.TypeAAAA, Class: dns.ClassINET}, nameServer) } ipv6, _ = getIPAddressesFromQueryResult(singleQueryRes, "AAAA", name) if len(ipv6) > 0 { diff --git a/src/zdns/conf.go b/src/zdns/conf.go index 5dff5c7e..97d9889b 100644 --- a/src/zdns/conf.go +++ b/src/zdns/conf.go @@ -23,7 +23,7 @@ const ( ) type TargetedDomain struct { - Domain string `json:"domain"` + Domain string `json:"name"` Nameservers []string `json:"nameservers"` } @@ -52,35 +52,35 @@ const ( ) var RootServersV4 = []NameServer{ - {IP: net.ParseIP("198.41.0.4"), Port: 53}, // A - {IP: net.ParseIP("170.247.170.2"), Port: 53}, // B - Changed several times, this is current as of July '24 - {IP: net.ParseIP("192.33.4.12"), Port: 53}, // C - {IP: net.ParseIP("199.7.91.13"), Port: 53}, // D - {IP: net.ParseIP("192.203.230.10"), Port: 53}, // E - {IP: net.ParseIP("192.5.5.241"), Port: 53}, // F - {IP: net.ParseIP("192.112.36.4"), Port: 53}, // G - {IP: net.ParseIP("198.97.190.53"), Port: 53}, // H - {IP: net.ParseIP("192.36.148.17"), Port: 53}, // I - {IP: net.ParseIP("192.58.128.30"), Port: 53}, // J - {IP: net.ParseIP("193.0.14.129"), Port: 53}, // K - {IP: net.ParseIP("199.7.83.42"), Port: 53}, // L - {IP: net.ParseIP("202.12.27.33"), Port: 53}, // M + {IP: net.ParseIP("198.41.0.4"), Port: 53, DomainName: "a.root-servers.net"}, // A + {IP: net.ParseIP("170.247.170.2"), Port: 53, DomainName: "b.root-servers.net"}, // B - Changed several times, this is current as of July '24 + {IP: net.ParseIP("192.33.4.12"), Port: 53, DomainName: "c.root-servers.net"}, // C + {IP: net.ParseIP("199.7.91.13"), Port: 53, DomainName: "d.root-servers.net"}, // D + {IP: net.ParseIP("192.203.230.10"), Port: 53, DomainName: "e.root-servers.net"}, // E + {IP: net.ParseIP("192.5.5.241"), Port: 53, DomainName: "f.root-servers.net"}, // F + {IP: net.ParseIP("192.112.36.4"), Port: 53, DomainName: "g.root-servers.net"}, // G + {IP: net.ParseIP("198.97.190.53"), Port: 53, DomainName: "h.root-servers.net"}, // H + {IP: net.ParseIP("192.36.148.17"), Port: 53, DomainName: "i.root-servers.net"}, // I + {IP: net.ParseIP("192.58.128.30"), Port: 53, DomainName: "j.root-servers.net"}, // J + {IP: net.ParseIP("193.0.14.129"), Port: 53, DomainName: "k.root-servers.net"}, // K + {IP: net.ParseIP("199.7.83.42"), Port: 53, DomainName: "l.root-servers.net"}, // L + {IP: net.ParseIP("202.12.27.33"), Port: 53, DomainName: "m.root-servers.net"}, // M } var RootServersV6 = []NameServer{ - {IP: net.ParseIP("2001:503:ba3e::2:30"), Port: 53}, // A - {IP: net.ParseIP("2801:1b8:10::b"), Port: 53}, // B - {IP: net.ParseIP("2001:500:2::c"), Port: 53}, // C - {IP: net.ParseIP("2001:500:2d::d"), Port: 53}, // D - {IP: net.ParseIP("2001:500:a8::e"), Port: 53}, // E - {IP: net.ParseIP("2001:500:2f::f"), Port: 53}, // F - {IP: net.ParseIP("2001:500:12::d0d"), Port: 53}, // G - {IP: net.ParseIP("2001:500:1::53"), Port: 53}, // H - {IP: net.ParseIP("2001:7fe::53"), Port: 53}, // I - {IP: net.ParseIP("2001:503:c27::2:30"), Port: 53}, // J - {IP: net.ParseIP("2001:7fd::1"), Port: 53}, // K - {IP: net.ParseIP("2001:500:9f::42"), Port: 53}, // L - {IP: net.ParseIP("2001:dc3::35"), Port: 53}, // M + {IP: net.ParseIP("2001:503:ba3e::2:30"), Port: 53, DomainName: "a.root-servers.net"}, // A + {IP: net.ParseIP("2801:1b8:10::b"), Port: 53, DomainName: "b.root-servers.net"}, // B + {IP: net.ParseIP("2001:500:2::c"), Port: 53, DomainName: "c.root-servers.net"}, // C + {IP: net.ParseIP("2001:500:2d::d"), Port: 53, DomainName: "d.root-servers.net"}, // D + {IP: net.ParseIP("2001:500:a8::e"), Port: 53, DomainName: "e.root-servers.net"}, // E + {IP: net.ParseIP("2001:500:2f::f"), Port: 53, DomainName: "f.root-servers.net"}, // F + {IP: net.ParseIP("2001:500:12::d0d"), Port: 53, DomainName: "g.root-servers.net"}, // G + {IP: net.ParseIP("2001:500:1::53"), Port: 53, DomainName: "h.root-servers.net"}, // H + {IP: net.ParseIP("2001:7fe::53"), Port: 53, DomainName: "i.root-servers.net"}, // I + {IP: net.ParseIP("2001:503:c27::2:30"), Port: 53, DomainName: "j.root-servers.net"}, // J + {IP: net.ParseIP("2001:7fd::1"), Port: 53, DomainName: "k.root-servers.net"}, // K + {IP: net.ParseIP("2001:500:9f::42"), Port: 53, DomainName: "l.root-servers.net"}, // L + {IP: net.ParseIP("2001:dc3::35"), Port: 53, DomainName: "m.root-servers.net"}, // M } var DefaultExternalResolversV4 = []NameServer{ diff --git a/src/zdns/lookup.go b/src/zdns/lookup.go index c61b782f..7e031361 100644 --- a/src/zdns/lookup.go +++ b/src/zdns/lookup.go @@ -33,6 +33,8 @@ import ( "github.com/zmap/zdns/src/internal/util" ) +var ErrorContextExpired = errors.New("context expired") + // GetDNSServers returns a list of IPv4, IPv6 DNS servers from a file, or an error if one occurs func GetDNSServers(path string) (ipv4, ipv6 []string, err error) { c, err := dns.ClientConfigFromFile(path) @@ -67,17 +69,17 @@ func GetDNSServers(path string) (ipv4, ipv6 []string, err error) { // Lookup client interface for help in mocking type Lookuper interface { - DoDstServersLookup(r *Resolver, q Question, nameServer []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) + DoDstServersLookup(ctx context.Context, r *Resolver, q Question, nameServer []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) } type LookupClient struct{} // DoDstServersLookup performs a DNS lookup for a given question against a list of interchangeable nameservers -func (lc LookupClient) DoDstServersLookup(r *Resolver, q Question, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { - return r.doDstServersLookup(q, nameServers, isIterative) +func (lc LookupClient) DoDstServersLookup(ctx context.Context, r *Resolver, q Question, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { + return r.doDstServersLookup(ctx, q, nameServers, isIterative) } -func (r *Resolver) doDstServersLookup(q Question, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { +func (r *Resolver) doDstServersLookup(ctx context.Context, q Question, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { var err error // nameserver is required if len(nameServers) == 0 { @@ -91,14 +93,12 @@ func (r *Resolver) doDstServersLookup(q Question, nameServers []NameServer, isIt // if that looks likely, use it as is if err != nil && !util.IsStringValidDomainName(q.Name) { return nil, nil, StatusIllegalInput, err - // q.Name is a valid domain name, we can continue + // q.Name is a valid name, we can continue } else { // remove trailing "." added by dns.ReverseAddr q.Name = qname[:len(qname)-1] } } - ctx, cancel := context.WithTimeout(context.Background(), r.timeout) - defer cancel() retries := r.retries questionWithMeta := QuestionWithMetadata{ Q: q, @@ -121,7 +121,7 @@ func (r *Resolver) lookup(ctx context.Context, qWithMeta *QuestionWithMetadata, var trace Trace var status Status var err error - if util.HasCtxExpired(&ctx) { + if util.HasCtxExpired(ctx) { return res, trace, StatusTimeout, nil } if isIterative { @@ -273,55 +273,388 @@ func isLookupComplete(originalName string, candidateSet map[string][]Answer, cNa return false } -// TODO - This is incomplete. We only lookup all nameservers for the initial name server lookup, then just send the DNS query to this set. -// If we want to iteratively lookup all nameservers at each level of the query, we need to fix this. -// Issue - https://github.com/zmap/zdns/issues/362 -func (r *Resolver) LookupAllNameservers(q *Question, nameServer *NameServer) (*CombinedResults, Trace, Status, error) { - var retv CombinedResults - var curServer string - - // Lookup both ipv4 and ipv6 addresses of nameservers. - nsResults, nsTrace, nsStatus, nsError := r.DoNSLookup(q.Name, nameServer, false, true, true) - - // Terminate early if nameserver lookup also failed - if nsStatus != StatusNoError { - return nil, nsTrace, nsStatus, nsError - } - if nsResults == nil { - return nil, nsTrace, nsStatus, errors.New("no results from nameserver lookup") - } - - // fullTrace holds the complete trace including all lookups - var fullTrace = Trace{} - if nsTrace != nil { - fullTrace = append(fullTrace, nsTrace...) - } - for _, nserver := range nsResults.Servers { - // Use all the ipv4 and ipv6 addresses of each nameserver - nameserver := nserver.Name - ips := util.Concat(nserver.IPv4Addresses, nserver.IPv6Addresses) - for _, ip := range ips { - // construct the nameserver - res, trace, status, err := r.ExternalLookup(q, &NameServer{ - IP: net.ParseIP(ip), - Port: DefaultDNSPort, - }) - if err != nil { - // log and move on - log.Errorf("lookup for domain %s to nameserver %s failed with error %s. Continueing to next nameserver", q.Name, curServer, err) +// LookupAllNameserversExternal will query all nameServers with the given question and return the results +// If nameServers is empty, it will use the externalNameServers from the resolver +func (r *Resolver) LookupAllNameserversExternal(q *Question, nameServers []NameServer) ([]SingleQueryResult, Trace, Status, error) { + ctx, cancel := context.WithTimeout(context.Background(), r.timeout) + defer cancel() + retv := make([]SingleQueryResult, 0) + var trace Trace + if len(nameServers) == 0 && len(r.externalNameServers) == 0 { + return retv, trace, StatusIllegalInput, errors.New("no external nameservers specified") + } + if len(nameServers) == 0 { + nameServers = r.externalNameServers + } + + for _, ns := range nameServers { + if util.HasCtxExpired(ctx) { + return retv, trace, StatusTimeout, ErrorContextExpired + } + result, currTrace, status, err := r.ExternalLookup(ctx, q, &ns) + trace = append(trace, currTrace...) + if err != nil { + log.Errorf("LookupAllNameserversExternal of name %s errored for %s/%s: %v", q.Name, ns.DomainName, ns.IP.String(), err) + continue + } + if status == StatusNoError { + retv = append(retv, *result) + log.Debugf("LookupAllNameserversExternal of name %s succeeded for %s/%s", q.Name, ns.DomainName, ns.IP.String()) + } + } + return retv, trace, StatusNoError, nil +} + +// filterNameServersForUniqueNames will filter out duplicate nameservers based on the name. +// Usually we'll have duplicates if a nameserver has both an IPv4 and IPv6 address. We'll use r.ipVersionMode and r.iterationIPPreference to determine which to keep. +func (r *Resolver) filterNameServersForUniqueNames(nameServers []NameServer) []NameServer { + uniqNameServersSet := make(map[string][]NameServer) + for _, ns := range nameServers { + if _, ok := uniqNameServersSet[ns.DomainName]; !ok { + // no slice, add one + uniqNameServersSet[ns.DomainName] = make([]NameServer, 0, 1) + } + uniqNameServersSet[ns.DomainName] = append(uniqNameServersSet[ns.DomainName], ns) + } + // nameservers not grouped by name + filteredNameServersSet := make([]NameServer, 0, len(uniqNameServersSet)) + for _, nsSlice := range uniqNameServersSet { + var ipv4NS, ipv6NS *NameServer + for _, ns := range nsSlice { + if ns.IP.To4() != nil { + ipv4NS = &ns + } else if util.IsIPv6(&ns.IP) { + ipv6NS = &ns + } + } + if ipv4NS == nil && ipv6NS == nil { + // can be the case that nameservers don't have IPs (like if we have an authority but no additionals) + // use the first NS if so + if len(nsSlice) > 0 { + filteredNameServersSet = append(filteredNameServersSet, nsSlice[0]) continue } + } + // If we only have one IP version, we'll keep that + if ipv4NS == nil { + filteredNameServersSet = append(filteredNameServersSet, *ipv6NS) + continue + } + if ipv6NS == nil { + filteredNameServersSet = append(filteredNameServersSet, *ipv4NS) + continue + } + // If we have both, we'll use the resolver's settings to determine which to keep + if r.ipVersionMode == IPv4Only { + filteredNameServersSet = append(filteredNameServersSet, *ipv4NS) + } else if r.ipVersionMode == IPv6Only { + filteredNameServersSet = append(filteredNameServersSet, *ipv6NS) + } else if r.iterationIPPreference == PreferIPv4 { + filteredNameServersSet = append(filteredNameServersSet, *ipv4NS) + } else if r.iterationIPPreference == PreferIPv6 { + filteredNameServersSet = append(filteredNameServersSet, *ipv6NS) + } + } + return filteredNameServersSet +} + +// LookupAllNameserversIterative will send a query to all name servers at each level of DNS resolution. +// It starts at either the provided rootNameServers or r.rootNameServers if none are provided as arguments and queries all. +// If the responses contain an authoritative answer, the function will return the result and a trace for each queried nameserver. +// If the responses do not contain an authoritative answer, the function will continue to the next layer of nameservers. +// At each layer, we'll de-duplicate the referral nameservers from the previous layer and query them. For example, if all +// root nameservers return a-m.gtld-servers.net, we'll only query each gtld-server once. +// +// Additionally, we'll query each layer for NS records, and once we have the set of authoritative nameservers, we'll query with +// the original question type. This helps find sibling nameservers that aren't listed with the TLD. +func (r *Resolver) LookupAllNameserversIterative(q *Question, rootNameServers []NameServer) (*AllNameServersResult, Trace, Status, error) { + perNameServerRetriesLimit := 2 + ctx, cancel := context.WithTimeout(context.Background(), r.timeout) + defer cancel() + retv := AllNameServersResult{ + LayeredResponses: make(map[string][]ExtendedResult), + } + var trace Trace + currentLayer := "." + var err error + currentLayerNameServers := rootNameServers + if len(currentLayerNameServers) == 0 { + // no root nameservers provided, use the resolver's root nameservers + currentLayerNameServers = r.rootNameServers + } + originalQuestionType := q.Type + q.Type = dns.TypeNS + var layerResults []ExtendedResult + var currTrace Trace + for { + // Filter out duplicate nameservers by name, we'll treat IPv4 and IPv6 addresses as the same nameserver + currentLayerNameServers = r.filterNameServersForUniqueNames(currentLayerNameServers) + // Getting the NameServers + layerResults, currTrace, _, err = r.queryAllNameServersInLayer(ctx, perNameServerRetriesLimit, q, currentLayerNameServers) + trace = append(trace, currTrace...) + if err != nil && errors.Is(err, ErrorContextExpired) { + return &retv, trace, StatusTimeout, err + } else if err != nil { + return &retv, trace, StatusError, errors.Wrapf(err, "error encountered on layer %s", currentLayer) + } else if len(retv.LayeredResponses[currentLayer]) == 0 { + retv.LayeredResponses[currentLayer] = layerResults + } else { + retv.LayeredResponses[currentLayer] = append(retv.LayeredResponses[currentLayer], layerResults...) + } + var newNameServers []NameServer + newNameServers, err = r.extractNameServersFromLayerResults(layerResults) + if err != nil { + return &retv, trace, StatusError, errors.Wrapf(err, "error extracting nameservers from layer %s", currentLayer) + } + // Set the next layer to query + var newLayer string + newLayer, err = nextAuthority(q.Name, currentLayer) + if err != nil { + return &retv, trace, StatusError, errors.Wrapf(err, "error determining next authority for layer %s", currentLayer) + } + if newLayer == currentLayer { + // we've reached the final layer + currentLayerNameServers = append(currentLayerNameServers, newNameServers...) + break + } + if len(newNameServers) == 0 { + // check if we have no referral nameservers because we've hit a CNAME or DNAME + foundReferral := false + for _, res := range layerResults { + for _, ans := range res.Res.Answers { + if a, ok := ans.(Answer); ok { + if a.RrType == dns.TypeCNAME || a.RrType == dns.TypeDNAME { + foundReferral = true + break + } + } + } + } + if foundReferral { + // we don't handle iterative all-nameservers lookups with C/DNAMEs, returning the results we've collected + // thus far + return &retv, trace, StatusNoError, nil + } + // we have no more nameservers to query, error + return &retv, trace, StatusError, errors.Errorf("no nameservers found for layer %s", currentLayer) + } + currentLayerNameServers = newNameServers + currentLayer = newLayer + } + // de-dupe nameservers + uniqNameServers := r.filterNameServersForUniqueNames(currentLayerNameServers) + // Now that we have an exhaustive list of leaf NSes, we'll query the original NSes + q.Type = originalQuestionType + layerResults, currTrace, _, err = r.queryAllNameServersInLayer(ctx, perNameServerRetriesLimit, q, uniqNameServers) + trace = append(trace, currTrace...) + if err != nil { + return &retv, trace, StatusError, errors.Wrapf(err, "error encountered on layer %s", currentLayer) + } else if len(retv.LayeredResponses[currentLayer]) == 0 { + retv.LayeredResponses[currentLayer] = layerResults + } else { + retv.LayeredResponses[currentLayer] = append(retv.LayeredResponses[currentLayer], layerResults...) + } + + return &retv, trace, StatusNoError, nil +} - fullTrace = append(fullTrace, trace...) - extendedResult := ExtendedResult{ - Res: *res, - Status: status, - Nameserver: nameserver, +// extractNameServersFromLayerResults +// extracts unique nameservers from Additionals/Authorities. Uniques by nameserver name, not by IP +func (r *Resolver) extractNameServersFromLayerResults(layerResults []ExtendedResult) ([]NameServer, error) { + type mapKey struct { + Type uint16 + Name string + Answer string + } + uniqueAdditionals := make(map[mapKey]Answer) + uniqueAuthorities := make(map[mapKey]Answer) + uniqueAnswers := make(map[mapKey]Answer) + for _, res := range layerResults { + if res.Status != StatusNoError { + continue + } + for _, ans := range res.Res.Additional { + if a, ok := ans.(Answer); ok { + uniqueAdditionals[mapKey{Type: a.RrType, Name: a.Name, Answer: a.Answer}] = a + } + } + for _, ans := range res.Res.Authorities { + if a, ok := ans.(Answer); ok { + uniqueAuthorities[mapKey{Type: a.RrType, Name: a.Name, Answer: a.Answer}] = a } - retv.Results = append(retv.Results, extendedResult) + } + for _, ans := range res.Res.Answers { + if a, ok := ans.(Answer); ok { + if a.RrType == dns.TypeNS { + uniqueAnswers[mapKey{Type: a.RrType, Name: a.Name, Answer: a.Answer}] = a + } + } + } + } + // We have a map of unique additional and authority records. Now we need to extract the nameservers from them. + v4NameServers := make(map[string]NameServer) + v6NameServers := make(map[string]NameServer) + for _, authorities := range uniqueAuthorities { + if authorities.RrType == dns.TypeNS { + v4NameServers[strings.TrimSuffix(authorities.Answer, ".")] = NameServer{DomainName: strings.TrimSuffix(authorities.Answer, ".")} + v6NameServers[strings.TrimSuffix(authorities.Answer, ".")] = NameServer{DomainName: strings.TrimSuffix(authorities.Answer, ".")} + } + } + for _, additionals := range uniqueAdditionals { + additionals.Name = strings.TrimSuffix(additionals.Name, ".") + if additionals.RrType == dns.TypeA { + if ns, ok := v4NameServers[additionals.Name]; ok { + ns.IP = net.ParseIP(additionals.Answer) + v4NameServers[additionals.Name] = ns + } + } + if additionals.RrType == dns.TypeAAAA { + if ns, ok := v6NameServers[additionals.Name]; ok { + ns.IP = net.ParseIP(additionals.Answer) + v6NameServers[additionals.Name] = ns + } + } + } + uniqNameServersSet := make(map[string]NameServer) + if r.ipVersionMode != IPv6Only { + for _, ns := range v4NameServers { + key := ns.DomainName + ns.IP.String() + if _, ok := uniqNameServersSet[key]; !ok { + uniqNameServersSet[key] = ns + + } + } + } + if r.ipVersionMode != IPv4Only { + for _, ns := range v6NameServers { + key := ns.DomainName + ns.IP.String() + if _, ok := uniqNameServersSet[key]; !ok { + uniqNameServersSet[key] = ns + } + } + } + // append any NS answers too + for _, answer := range uniqueAnswers { + ns := NameServer{ + DomainName: strings.TrimSuffix(answer.Answer, "."), + } + key := ns.DomainName + if _, ok := uniqNameServersSet[key]; !ok { + uniqNameServersSet[key] = ns + } + } + uniqNameServers := make([]NameServer, 0, len(uniqNameServersSet)) + for _, ns := range uniqNameServersSet { + uniqNameServers = append(uniqNameServers, ns) + } + return uniqNameServers, nil +} + +func (r *Resolver) populateNameServerIP(ctx context.Context, nameServer *NameServer) (Trace, error) { + if nameServer.IP != nil { + // already have an IP + return nil, nil + } + retries := r.retries + var q Question + if r.ipVersionMode == IPv4Only { + q = Question{dns.TypeA, dns.ClassINET, nameServer.DomainName} + } else if r.ipVersionMode == IPv6Only { + q = Question{dns.TypeAAAA, dns.ClassINET, nameServer.DomainName} + } else if r.iterationIPPreference == PreferIPv4 { + q = Question{dns.TypeA, dns.ClassINET, nameServer.DomainName} + } else { + q = Question{dns.TypeAAAA, dns.ClassINET, nameServer.DomainName} + } + res, nsTrace, status, err := r.followingLookup(ctx, &QuestionWithMetadata{ + Q: q, + RetriesRemaining: &retries, + }, r.rootNameServers, true) + if err == nil && status == StatusNoError { + for _, ans := range res.Answers { + if a, ok := ans.(Answer); ok { + if a.RrType == q.Type { + nameServer.IP = net.ParseIP(a.Answer) + return nsTrace, nil + } + } + } + } + // if we get here, we couldn't find an IP for the nameserver, let's try with the other A/AAAA if we can + if r.ipVersionMode == IPv4OrIPv6 { + if q.Type == dns.TypeA { + q.Type = dns.TypeAAAA + } else { + q.Type = dns.TypeA + } + res, nsTrace, status, err = r.followingLookup(ctx, &QuestionWithMetadata{ + Q: q, + RetriesRemaining: &retries, + }, r.rootNameServers, true) + if err == nil && status == StatusNoError { + for _, ans := range res.Answers { + if a, ok := ans.(Answer); ok { + if a.RrType == q.Type { + nameServer.IP = net.ParseIP(a.Answer) + return nsTrace, nil + } + } + } + } + } + if err != nil { + return nil, errors.Wrapf(err, "could not find IP for nameserver: %s", nameServer.DomainName) + } + return nil, errors.Errorf("could not find IP for nameserver: %s", nameServer.DomainName) +} + +// queryAllNameServersInLayer queries all nameservers in a given layer +// Returns a slice of ExtendedResults from each NS, a Trace, whether any answer is authoritative, and an error if one occurs +func (r *Resolver) queryAllNameServersInLayer(ctx context.Context, perNameServerRetriesLimit int, q *Question, currentNameServers []NameServer) ([]ExtendedResult, Trace, bool, error) { + trace := make([]TraceStep, 0) + currentLayerResults := make([]ExtendedResult, 0, len(currentNameServers)) + isAuthoritative := false + for _, nameServer := range currentNameServers { + var extResult *ExtendedResult + for retry := 0; retry < perNameServerRetriesLimit; retry++ { + if util.HasCtxExpired(ctx) { + return currentLayerResults, trace, false, ErrorContextExpired + } + if nameServer.IP == nil { + nsTrace, err := r.populateNameServerIP(ctx, &nameServer) + if err != nil { + log.Debugf("LookupAllNameserversIterative of name %s errored for %s: %v", q.Name, nameServer.DomainName, err) + continue + } + trace = append(trace, nsTrace...) + // we've populated NS IP, we can proceed + } + result, currTrace, status, err := r.ExternalLookup(ctx, q, &nameServer) + trace = append(trace, currTrace...) + extResult = &ExtendedResult{Status: status, Nameserver: nameServer.DomainName, Type: dns.TypeToString[q.Type]} + if result != nil { + extResult.Res = *result + } + if err == nil && status == StatusNoError && result != nil { + if result.Flags.Authoritative { + isAuthoritative = true + } + // successful result, continue to next nameserver + break + } + if err != nil { + log.Debugf("LookupAllNameserversIterative of name %s errored for %s: %v", q.Name, nameServer.IP.String(), err) + } else { + log.Debugf("LookupAllNameserversIterative of name %s failed for %s: %v", q.Name, nameServer.IP.String(), status) + } + } + if extResult == nil { + log.Debugf("LookupAllNameserversIterative of name %s against nameserver %s ran out of retries, continueing to next nameserver", q.Name, nameServer.IP.String()) + } else { + currentLayerResults = append(currentLayerResults, *extResult) } } - return &retv, fullTrace, StatusNoError, nil + return currentLayerResults, trace, isAuthoritative, nil } func (r *Resolver) iterativeLookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, @@ -331,7 +664,7 @@ func (r *Resolver) iterativeLookup(ctx context.Context, qWithMeta *QuestionWithM return nil, trace, StatusError, errors.New("max recursion depth reached") } // check that context hasn't expired - if util.HasCtxExpired(&ctx) { + if util.HasCtxExpired(ctx) { r.verboseLog(depth+1, "-> Context expired") return nil, trace, StatusTimeout, nil } @@ -352,7 +685,7 @@ func (r *Resolver) iterativeLookup(ctx context.Context, qWithMeta *QuestionWithM t.Try = getTryNumber(r.retries, *qWithMeta.RetriesRemaining) trace = append(trace, t) } - if status == StatusTimeout && util.HasCtxExpired(&iterationStepCtx) && !util.HasCtxExpired(&ctx) { + if status == StatusTimeout && util.HasCtxExpired(iterationStepCtx) && !util.HasCtxExpired(ctx) { // ctx's have a deadline of the minimum of their deadline and their parent's // retryingLookup doesn't disambiguate of whether the timeout was caused by the iteration timeout or the global timeout // we'll disambiguate here by checking if the iteration context has expired but the global context hasn't @@ -391,8 +724,8 @@ func (r *Resolver) iterativeLookup(ctx context.Context, qWithMeta *QuestionWithM func (r *Resolver) cyclingLookup(ctx context.Context, qWithMeta *QuestionWithMetadata, nameServers []NameServer, layer string, depth int, recursionDesired bool) (*SingleQueryResult, IsCached, Status, error) { var cacheBasedOnNameServer bool var cacheNonAuthoritative bool - if recursionDesired { - // we're doing an external lookup and need to set the recursionDesired bit + if recursionDesired || r.lookupAllNameServers { + // we're doing an external or all-nameservers lookup and need to set the recursionDesired bit // Additionally, in external mode we may perform the same lookup against multiple nameservers, so the cache should be based on the nameserver as well cacheBasedOnNameServer = true cacheNonAuthoritative = true @@ -409,7 +742,7 @@ func (r *Resolver) cyclingLookup(ctx context.Context, qWithMeta *QuestionWithMet var nameServer *NameServer for *qWithMeta.RetriesRemaining >= 0 { - if util.HasCtxExpired(&ctx) { + if util.HasCtxExpired(ctx) { return &SingleQueryResult{}, false, StatusTimeout, nil } // get random unqueried nameserver @@ -447,7 +780,7 @@ func getRandomNonQueriedNameServer(nameServers []NameServer, queriedNameServers // cachedLookup performs a DNS lookup with caching // returns the result, whether it was cached, the status, and an error if one occurred -// layer is the domain name layer we're currently querying ex: ".", "com.", "example.com." +// layer is the name layer we're currently querying ex: ".", "com.", "example.com." // depth is the current depth of the lookup, used for iterative lookups // requestIteration is whether to set the "recursion desired" bit in the DNS query // cacheBasedOnNameServer is whether to consider a cache hit based on DNS question and nameserver, or just question @@ -882,7 +1215,7 @@ func (r *Resolver) iterateOnAuthorities(ctx context.Context, qWithMeta *Question newTrace := trace nameServers := make([]NameServer, 0, len(result.Authorities)) for i, elem := range result.Authorities { - if util.HasCtxExpired(&ctx) { + if util.HasCtxExpired(ctx) { return &SingleQueryResult{}, newTrace, StatusTimeout, nil } var ns *NameServer @@ -1017,12 +1350,12 @@ func FindTxtRecord(res *SingleQueryResult, regex *regexp.Regexp) (string, error) } // populateResults is a helper function to populate the candidateSet, cnameSet, and garbage maps to follow CNAMES -// These maps are keyed by the domain name and contain the relevant answers for that domain +// These maps are keyed by the name and contain the relevant answers for that name // candidateSet is a map of Answers that have a type matching the requested type. // cnameSet is a map of Answers that are CNAME records // dnameSet is a map of Answers that are DNAME records // garbage is a map of Answers that are not of the requested type or CNAME records -// follows CNAME/DNAME and A/AAAA records to get all IPs for a given domain +// follows CNAME/DNAME and A/AAAA records to get all IPs for a given name func populateResults(records []interface{}, dnsType uint16, candidateSet map[string][]Answer, cnameSet map[string][]Answer, dnameSet map[string][]Answer, garbage map[string][]Answer) { var ans Answer var ok bool diff --git a/src/zdns/lookup_test.go b/src/zdns/lookup_test.go index 7f04a187..8c2d4cd3 100644 --- a/src/zdns/lookup_test.go +++ b/src/zdns/lookup_test.go @@ -14,12 +14,16 @@ package zdns import ( + "context" "encoding/hex" + "fmt" "math/rand" "net" "reflect" "regexp" + "sort" "testing" + "time" "github.com/stretchr/testify/require" @@ -30,20 +34,20 @@ import ( "github.com/zmap/zdns/src/internal/util" ) -type domainNS struct { - domain string - ns string +type nameAndIP struct { + name string + IP string } -var mockResults = make(map[domainNS]SingleQueryResult) +var mockResults = make(map[nameAndIP]SingleQueryResult) -var protocolStatus = make(map[domainNS]Status) +var protocolStatus = make(map[nameAndIP]Status) type MockLookupClient struct{} -func (mc MockLookupClient) DoDstServersLookup(r *Resolver, q Question, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { +func (mc MockLookupClient) DoDstServersLookup(ctx context.Context, r *Resolver, q Question, nameServers []NameServer, isIterative bool) (*SingleQueryResult, Trace, Status, error) { ns := nameServers[rand.Intn(len(nameServers))] - curDomainNs := domainNS{domain: q.Name, ns: ns.String()} + curDomainNs := nameAndIP{name: q.Name, IP: ns.String()} if res, ok := mockResults[curDomainNs]; ok { var status = StatusNoError if protStatus, ok := protocolStatus[curDomainNs]; ok { @@ -56,8 +60,8 @@ func (mc MockLookupClient) DoDstServersLookup(r *Resolver, q Question, nameServe } func InitTest(t *testing.T) *ResolverConfig { - protocolStatus = make(map[domainNS]Status) - mockResults = make(map[domainNS]SingleQueryResult) + protocolStatus = make(map[nameAndIP]Status) + mockResults = make(map[nameAndIP]SingleQueryResult) mc := MockLookupClient{} config := NewResolverConfig() @@ -630,7 +634,7 @@ func TestOneA(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -658,7 +662,7 @@ func TestTwoA(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -693,7 +697,7 @@ func TestQuadAWithoutFlag(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -729,7 +733,7 @@ func TestOnlyQuadA(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -759,7 +763,7 @@ func TestAandQuadA(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -795,7 +799,7 @@ func TestTwoQuadA(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -832,7 +836,7 @@ func TestNoResults(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: nil, @@ -854,7 +858,7 @@ func TestQuadAWithCname(t *testing.T) { domain1 := "cname.example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -889,7 +893,7 @@ func TestUnexpectedMxOnly(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -925,7 +929,7 @@ func TestMxAndAdditionals(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -966,7 +970,7 @@ func TestMismatchIpType(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -1002,7 +1006,7 @@ func TestEmptyNonTerminal(t *testing.T) { domain1 := "leaf.intermediate.example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -1020,7 +1024,7 @@ func TestEmptyNonTerminal(t *testing.T) { dom2 := "intermediate.example.com" - domainNS2 := domainNS{domain: dom2, ns: ns1.String()} + domainNS2 := nameAndIP{name: dom2, IP: ns1.String()} mockResults[domainNS2] = SingleQueryResult{ Answers: nil, @@ -1038,7 +1042,7 @@ func TestEmptyNonTerminal(t *testing.T) { verifyResult(t, *res, nil, nil) } -// Test Non-existent domain in the zone returns NXDOMAIN +// Test Non-existent name in the zone returns NXDOMAIN func TestNXDomain(t *testing.T) { config := InitTest(t) @@ -1063,9 +1067,9 @@ func TestAandQuadADedup(t *testing.T) { domain2 := "cname2.example.com" domain3 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} - domainNS2 := domainNS{domain: domain2, ns: ns1.String()} - domainNS3 := domainNS{domain: domain3, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} + domainNS2 := nameAndIP{name: domain2, IP: ns1.String()} + domainNS3 := nameAndIP{name: domain3, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{Answer{ @@ -1159,7 +1163,7 @@ func TestServFail(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{} name := "example.com" @@ -1196,7 +1200,7 @@ func TestNsAInAdditional(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{ @@ -1239,7 +1243,7 @@ func TestTwoNSInAdditional(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{ @@ -1299,7 +1303,7 @@ func TestAandQuadAInAdditional(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{ @@ -1348,7 +1352,7 @@ func TestNsMismatchIpType(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{ @@ -1397,7 +1401,7 @@ func TestAandQuadALookup(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{ @@ -1417,7 +1421,7 @@ func TestAandQuadALookup(t *testing.T) { dom2 := "ns1.example.com" - domainNS2 := domainNS{domain: dom2, ns: ns1.String()} + domainNS2 := nameAndIP{name: dom2, IP: ns1.String()} mockResults[domainNS2] = SingleQueryResult{ Answers: []interface{}{ @@ -1470,7 +1474,7 @@ func TestNsServFail(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{} protocolStatus[domainNS1] = StatusServFail @@ -1488,7 +1492,7 @@ func TestErrorInTargetedLookup(t *testing.T) { domain1 := "example.com" ns1 := &config.ExternalNameServersV4[0] - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} + domainNS1 := nameAndIP{name: domain1, IP: ns1.String()} mockResults[domainNS1] = SingleQueryResult{ Answers: []interface{}{ @@ -1514,411 +1518,409 @@ func TestErrorInTargetedLookup(t *testing.T) { } // Test One NS with one IP with only ipv4-lookup -func TestAllNsLookupOneNs(t *testing.T) { +func TestAllNsLookupOneNsThreeLevels(t *testing.T) { config := InitTest(t) config.LocalAddrsV4 = []net.IP{net.ParseIP("127.0.0.1")} resolver, err := InitResolver(config) require.NoError(t, err) - - ns1 := &config.ExternalNameServersV4[0] - domain1 := "example.com" - nsDomain1 := "ns1.example.com" - ipv4_1 := "127.0.0.2" - - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} - mockResults[domainNS1] = SingleQueryResult{ - Answers: []interface{}{ + exampleName := "example.com" + + rootServer := "a.root-servers.net" + rootServerIP := "1.1.1.1" + comServer := "a.gtld-servers.net" + comServerIP := "2.2.2.2" + exampleNSServer := "ns1.example.com" + exampleNSServerIP := "3.3.3.3" + exampleNameAAnswer := "4.4.4.4" + + mockResults[nameAndIP{name: exampleName, IP: rootServerIP + ":53"}] = SingleQueryResult{ + Authorities: []interface{}{ Answer{ TTL: 3600, Type: "NS", + RrType: dns.TypeNS, Class: "IN", - Name: "example.com.", - Answer: nsDomain1 + ".", + Name: "com.", + Answer: comServer + ".", }, }, Additional: []interface{}{ Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", - Name: nsDomain1 + ".", - Answer: ipv4_1, + Name: comServer + ".", + Answer: comServerIP, }, }, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, } - - domainNS2 := domainNS{domain: domain1, ns: ipv4_1 + ":53"} - ipv4_2 := "127.0.0.3" - mockResults[domainNS2] = SingleQueryResult{ - Answers: []interface{}{ - Answer{ - TTL: 3600, - Type: "A", - Class: "IN", - Name: "example.com.", - Answer: ipv4_2, - }, - }, - Additional: nil, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, - } - - expectedRes := []ExtendedResult{ - { - Nameserver: nsDomain1, - Status: StatusNoError, - Res: mockResults[domainNS2], - }, - } - q := Question{ - Type: dns.TypeNS, - Class: dns.ClassINET, - Name: "example.com", - } - - results, _, _, err := resolver.LookupAllNameservers(&q, ns1) - require.NoError(t, err) - verifyCombinedResult(t, results.Results, expectedRes) -} - -// Test One NS with two IPs with only ipv4-lookup - -func TestAllNsLookupOneNsMultipleIps(t *testing.T) { - config := InitTest(t) - config.IPVersionMode = IPv4Only - resolver, err := InitResolver(config) - require.NoError(t, err) - - ns1 := &config.ExternalNameServersV4[0] - domain1 := "example.com" - nsDomain1 := "ns1.example.com" - ipv4_1 := "127.0.0.2" - ipv4_2 := "127.0.0.3" - - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} - mockResults[domainNS1] = SingleQueryResult{ - Answers: []interface{}{ + mockResults[nameAndIP{name: exampleName, IP: comServerIP + ":53"}] = SingleQueryResult{ + Authorities: []interface{}{ Answer{ TTL: 3600, Type: "NS", + RrType: dns.TypeNS, Class: "IN", Name: "example.com.", - Answer: nsDomain1 + ".", + Answer: exampleNSServer + ".", }, }, Additional: []interface{}{ Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", - Name: nsDomain1 + ".", - Answer: ipv4_1, - }, - Answer{ - TTL: 3600, - Type: "A", - Class: "IN", - Name: nsDomain1 + ".", - Answer: ipv4_2, + Name: exampleNSServer + ".", + Answer: exampleNSServerIP, }, }, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, } - - domainNS2 := domainNS{domain: domain1, ns: ipv4_1 + ":53"} - ipv4_3 := "127.0.0.4" - ipv6_1 := "::1" - mockResults[domainNS2] = SingleQueryResult{ + mockResults[nameAndIP{name: exampleName, IP: exampleNSServerIP + ":53"}] = SingleQueryResult{ Answers: []interface{}{ Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", Name: "example.com.", - Answer: ipv4_3, - }, - Answer{ - TTL: 3600, - Type: "AAAA", - Class: "IN", - Name: "example.com.", - Answer: ipv6_1, + Answer: exampleNameAAnswer, }, }, - Additional: nil, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, } - domainNS3 := domainNS{domain: domain1, ns: ipv4_2 + ":53"} - ipv4_4 := "127.0.0.5" - ipv6_2 := "::2" - mockResults[domainNS3] = SingleQueryResult{ - Answers: []interface{}{ - Answer{ - TTL: 3600, - Type: "A", - Class: "IN", - Name: "example.com.", - Answer: ipv4_4, - }, - Answer{ - TTL: 3600, - Type: "AAAA", - Class: "IN", - Name: "example.com.", - Answer: ipv6_2, + expectedRes := map[string][]ExtendedResult{ + ".": { + { + Status: StatusNoError, + Nameserver: rootServer, + Type: "NS", + Res: SingleQueryResult{ + Authorities: []interface{}{ + Answer{ + TTL: 3600, + Type: "NS", + RrType: dns.TypeNS, + Class: "IN", + Name: "com.", + Answer: "a.gtld-servers.net.", + }, + }, + Additional: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "a.gtld-servers.net.", + Answer: "2.2.2.2", + }, + }, + }, }, }, - Additional: nil, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, - } - - expectedRes := []ExtendedResult{ - { - Nameserver: nsDomain1, - Status: StatusNoError, - Res: mockResults[domainNS2], + "com": { + { + Status: StatusNoError, + Type: "NS", + Nameserver: comServer, + Res: SingleQueryResult{ + Authorities: []interface{}{ + Answer{ + TTL: 3600, + Type: "NS", + RrType: dns.TypeNS, + Class: "IN", + Name: "example.com.", + Answer: "ns1.example.com.", + }, + }, + Additional: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "ns1.example.com.", + Answer: "3.3.3.3", + }, + }, + }, + }, }, - { - Nameserver: nsDomain1, - Status: StatusNoError, - Res: mockResults[domainNS3], + "example.com": { + { + Status: StatusNoError, + Type: "NS", + Nameserver: exampleNSServer, + Res: SingleQueryResult{ + Answers: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "example.com.", + Answer: "4.4.4.4", + }, + }, + }, + }, + { + Status: StatusNoError, + Type: "A", + Nameserver: exampleNSServer, + Res: SingleQueryResult{ + Answers: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "example.com.", + Answer: "4.4.4.4", + }, + }, + }, + }, }, } - q := Question{ - Type: dns.TypeNS, + Type: dns.TypeA, Class: dns.ClassINET, Name: "example.com", } - results, _, _, err := resolver.LookupAllNameservers(&q, ns1) + results, _, _, err := resolver.LookupAllNameserversIterative(&q, []NameServer{{DomainName: rootServer, IP: net.ParseIP(rootServerIP)}}) require.NoError(t, err) - verifyCombinedResult(t, results.Results, expectedRes) + verifyCombinedResult(t, results.LayeredResponses, expectedRes) } -// Test One NS with two IPs with only ipv4-lookup -func TestAllNsLookupTwoNs(t *testing.T) { +// Test AllNameservers with a ".", ".com", and "example.com". We'll have two .com servers and one will error. Should still be able to resolve the query. +func TestAllNsLookupErrorInOne(t *testing.T) { config := InitTest(t) - config.IPVersionMode = IPv4Only + config.LocalAddrsV4 = []net.IP{net.ParseIP("127.0.0.1")} + config.Timeout = time.Hour + config.IterativeTimeout = time.Hour resolver, err := InitResolver(config) require.NoError(t, err) - - ns1 := &config.ExternalNameServersV4[0] - domain1 := "example.com" - nsDomain1 := "ns1.example.com" - nsDomain2 := "ns2.example.com" - ipv4_1 := "127.0.0.2" - ipv4_2 := "127.0.0.3" - - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} - mockResults[domainNS1] = SingleQueryResult{ - Answers: []interface{}{ + exampleName := "example.com" + + rootServer := "a.root-servers.net" + rootServerIP := "1.1.1.1" + comServerA := "a.gtld-servers.net" + comServerB := "b.gtld-servers.net" + comServerAIP := "2.2.2.2" + comServerBIP := "3.3.3.3" + exampleNSServer := "ns1.example.com" + exampleNSServerIP := "4.4.4.4" + exampleNameAAnswer := "5.5.5.5" + + mockResults[nameAndIP{name: exampleName, IP: rootServerIP + ":53"}] = SingleQueryResult{ + Authorities: []interface{}{ Answer{ TTL: 3600, Type: "NS", + RrType: dns.TypeNS, Class: "IN", - Name: "example.com.", - Answer: nsDomain1 + ".", + Name: "com.", + Answer: comServerA + ".", }, Answer{ TTL: 3600, Type: "NS", + RrType: dns.TypeNS, Class: "IN", - Name: "example.com.", - Answer: nsDomain2 + ".", + Name: "com.", + Answer: comServerB + ".", }, }, Additional: []interface{}{ Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", - Name: nsDomain1 + ".", - Answer: ipv4_1, + Name: comServerA + ".", + Answer: comServerAIP, }, Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", - Name: nsDomain2 + ".", - Answer: ipv4_2, + Name: comServerB + ".", + Answer: comServerBIP, }, }, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, } - - domainNS2 := domainNS{domain: domain1, ns: ipv4_1 + ":53"} - ipv4_3 := "127.0.0.4" - mockResults[domainNS2] = SingleQueryResult{ - Answers: []interface{}{ - Answer{ - TTL: 3600, - Type: "A", - Class: "IN", - Name: "example.com.", - Answer: ipv4_3, - }, - }, - Additional: nil, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, - } - - domainNS3 := domainNS{domain: domain1, ns: ipv4_2 + ":53"} - ipv4_4 := "127.0.0.5" - mockResults[domainNS3] = SingleQueryResult{ - Answers: []interface{}{ - Answer{ - TTL: 3600, - Type: "A", - Class: "IN", - Name: "example.com.", - Answer: ipv4_4, - }, - }, - Additional: nil, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, - } - - expectedRes := []ExtendedResult{ - { - Nameserver: nsDomain1, - Status: StatusNoError, - Res: mockResults[domainNS2], - }, - { - Nameserver: nsDomain2, - Status: StatusNoError, - Res: mockResults[domainNS3], - }, - } - - q := Question{ - Type: dns.TypeNS, - Class: dns.ClassINET, - Name: "example.com", - } - - results, _, _, err := resolver.LookupAllNameservers(&q, ns1) - require.NoError(t, err) - verifyCombinedResult(t, results.Results, expectedRes) -} - -// Test error in A lookup via targeted lookup records - -func TestAllNsLookupErrorInOne(t *testing.T) { - config := InitTest(t) - config.IPVersionMode = IPv4Only - resolver, err := InitResolver(config) - require.NoError(t, err) - - ns1 := &config.ExternalNameServersV4[0] - domain1 := "example.com" - nsDomain1 := "ns1.example.com" - ipv4_1 := "127.0.0.2" - ipv4_2 := "127.0.0.3" - - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} - mockResults[domainNS1] = SingleQueryResult{ - Answers: []interface{}{ + // Error in comServerB + mockResults[nameAndIP{name: exampleName, IP: comServerAIP + ":53"}] = SingleQueryResult{} + protocolStatus[nameAndIP{name: exampleName, IP: comServerAIP + ":53"}] = StatusServFail + // Success in comServerA + mockResults[nameAndIP{name: exampleName, IP: comServerBIP + ":53"}] = SingleQueryResult{ + Authorities: []interface{}{ Answer{ TTL: 3600, Type: "NS", + RrType: dns.TypeNS, Class: "IN", - Name: "example.com", - Answer: nsDomain1 + ".", + Name: "example.com.", + Answer: exampleNSServer + ".", }, }, Additional: []interface{}{ Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", - Name: nsDomain1 + ".", - Answer: ipv4_1, - }, - Answer{ - TTL: 3600, - Type: "A", - Class: "IN", - Name: nsDomain1 + ".", - Answer: ipv4_2, + Name: exampleNSServer + ".", + Answer: exampleNSServerIP, }, }, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, } - - domainNS2 := domainNS{domain: domain1, ns: ipv4_1 + ":53"} - ipv4_3 := "127.0.0.4" - ipv6_1 := "::1" - mockResults[domainNS2] = SingleQueryResult{ + mockResults[nameAndIP{name: exampleName, IP: exampleNSServerIP + ":53"}] = SingleQueryResult{ Answers: []interface{}{ Answer{ TTL: 3600, Type: "A", + RrType: dns.TypeA, Class: "IN", Name: "example.com.", - Answer: ipv4_3, - }, - Answer{ - TTL: 3600, - Type: "AAAA", - Class: "IN", - Name: "example.com.", - Answer: ipv6_1, + Answer: exampleNameAAnswer, }, }, - Additional: nil, - Authorities: nil, - Protocol: "", - Flags: DNSFlags{}, } - domainNS3 := domainNS{domain: domain1, ns: ipv4_2 + ":53"} - protocolStatus[domainNS3] = StatusServFail - mockResults[domainNS3] = SingleQueryResult{} - - expectedRes := []ExtendedResult{ - { - Nameserver: nsDomain1, - Status: StatusNoError, - Res: mockResults[domainNS2], + expectedRes := map[string][]ExtendedResult{ + ".": { + { + Status: StatusNoError, + Type: "NS", + Nameserver: rootServer, + Res: SingleQueryResult{ + Authorities: []interface{}{ + Answer{ + TTL: 3600, + Type: "NS", + RrType: dns.TypeNS, + Class: "IN", + Name: "com.", + Answer: "a.gtld-servers.net.", + }, + Answer{ + TTL: 3600, + Type: "NS", + RrType: dns.TypeNS, + Class: "IN", + Name: "com.", + Answer: "b.gtld-servers.net.", + }, + }, + Additional: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "a.gtld-servers.net.", + Answer: comServerAIP, + }, + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "b.gtld-servers.net.", + Answer: comServerBIP, + }, + }, + }, + }, + }, + "com": { + { + Status: StatusServFail, + Type: "NS", + Nameserver: comServerA, + Res: SingleQueryResult{}, + }, + { + Status: StatusNoError, + Type: "NS", + Nameserver: comServerB, + Res: SingleQueryResult{ + Authorities: []interface{}{ + Answer{ + TTL: 3600, + Type: "NS", + RrType: dns.TypeNS, + Class: "IN", + Name: "example.com.", + Answer: exampleNSServer + ".", + }, + }, + Additional: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "ns1.example.com.", + Answer: exampleNSServerIP, + }, + }, + }, + }, }, - { - Nameserver: nsDomain1, - Status: StatusServFail, - Res: mockResults[domainNS3], + "example.com": { + { + Status: StatusNoError, + Type: "NS", + Nameserver: exampleNSServer, + Res: SingleQueryResult{ + Answers: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "example.com.", + Answer: exampleNameAAnswer, + }, + }, + }, + }, + { + Status: StatusNoError, + Type: "A", + Nameserver: exampleNSServer, + Res: SingleQueryResult{ + Answers: []interface{}{ + Answer{ + TTL: 3600, + Type: "A", + RrType: dns.TypeA, + Class: "IN", + Name: "example.com.", + Answer: exampleNameAAnswer, + }, + }, + }, + }, }, } - q := Question{ - Type: dns.TypeNS, + Type: dns.TypeA, Class: dns.ClassINET, Name: "example.com", } - results, _, _, err := resolver.LookupAllNameservers(&q, ns1) + results, _, _, err := resolver.LookupAllNameserversIterative(&q, []NameServer{{DomainName: rootServer, IP: net.ParseIP(rootServerIP)}}) require.NoError(t, err) - verifyCombinedResult(t, results.Results, expectedRes) + verifyCombinedResult(t, results.LayeredResponses, expectedRes) } func TestAllNsLookupNXDomain(t *testing.T) { @@ -1934,36 +1936,16 @@ func TestAllNsLookupNXDomain(t *testing.T) { Name: "example.com", } - res, _, status, err := resolver.LookupAllNameservers(&q, ns1) - - assert.Equal(t, StatusNXDomain, status) - assert.Nil(t, res) - require.NoError(t, err) -} - -func TestAllNsLookupServFail(t *testing.T) { - config := InitTest(t) - config.IPVersionMode = IPv4Only - resolver, err := InitResolver(config) - require.NoError(t, err) + res, _, status, err := resolver.LookupAllNameserversIterative(&q, []NameServer{*ns1}) - ns1 := &config.ExternalNameServersV4[0] - domain1 := "example.com" - domainNS1 := domainNS{domain: domain1, ns: ns1.String()} - - protocolStatus[domainNS1] = StatusServFail - mockResults[domainNS1] = SingleQueryResult{} - - q := Question{ - Type: dns.TypeNS, - Class: dns.ClassINET, - Name: "example.com", + expectedResponse := map[string][]ExtendedResult{ + ".": {{Status: StatusNXDomain, Type: "NS"}}, } - res, _, status, err := resolver.LookupAllNameservers(&q, ns1) - - assert.Equal(t, StatusServFail, status) - assert.Nil(t, res) - require.NoError(t, err) + if !reflect.DeepEqual(res.LayeredResponses, expectedResponse) { + t.Errorf("Expected %v, Received %v", expectedResponse, res.LayeredResponses) + } + assert.Equal(t, StatusError, status) + require.Error(t, err) // could not successfully complete lookup, so this should error } func TestInvalidInputsLookup(t *testing.T) { @@ -1979,11 +1961,11 @@ func TestInvalidInputsLookup(t *testing.T) { } t.Run("no port attached to nameserver", func(t *testing.T) { - _, _, _, err := resolver.ExternalLookup(&q, &NameServer{IP: net.ParseIP("127.0.0.53")}) + _, _, _, err := resolver.ExternalLookup(context.Background(), &q, &NameServer{IP: net.ParseIP("127.0.0.53")}) assert.Nil(t, err) }) t.Run("invalid nameserver address", func(t *testing.T) { - result, trace, status, err := resolver.ExternalLookup(&q, &NameServer{IP: net.ParseIP("987.987.987.987"), Port: 53}) + result, trace, status, err := resolver.ExternalLookup(context.Background(), &q, &NameServer{IP: net.ParseIP("987.987.987.987"), Port: 53}) assert.Nil(t, result) assert.Nil(t, trace) assert.Equal(t, StatusIllegalInput, status) @@ -2014,7 +1996,21 @@ func verifyNsResult(t *testing.T, servers []NSRecord, expectedServersMap map[str } } -func verifyCombinedResult(t *testing.T, records []ExtendedResult, expectedRecords []ExtendedResult) { +func verifyCombinedResult(t *testing.T, records map[string][]ExtendedResult, expectedRecords map[string][]ExtendedResult) { + for layer := range expectedRecords { + assert.Contains(t, records, layer, fmt.Sprintf("Layer %s not found in combined result", layer)) + } + for layer, expectedLayerResults := range expectedRecords { + sort.Slice(records[layer], func(i, j int) bool { + return records[layer][i].Nameserver < records[layer][j].Nameserver + }) + sort.Slice(records[layer], func(i, j int) bool { + return expectedLayerResults[i].Nameserver < expectedLayerResults[j].Nameserver + }) + if !reflect.DeepEqual(records[layer], expectedLayerResults) { + t.Errorf("Combined result not matching for layer %s, expected %v, found %v", layer, expectedLayerResults, records[layer]) + } + } if !reflect.DeepEqual(records, expectedRecords) { t.Errorf("Combined result not matching, expected %v, found %v", expectedRecords, records) } diff --git a/src/zdns/nslookup.go b/src/zdns/nslookup.go index 73dec5fd..464f670c 100644 --- a/src/zdns/nslookup.go +++ b/src/zdns/nslookup.go @@ -14,6 +14,7 @@ package zdns import ( + "context" "strings" "github.com/miekg/dns" @@ -52,9 +53,9 @@ func (r *Resolver) DoNSLookup(lookupName string, nameServer *NameServer, isItera var status Status var err error if isIterative { - ns, trace, status, err = r.IterativeLookup(&Question{Name: lookupName, Type: dns.TypeNS, Class: dns.ClassINET}) + ns, trace, status, err = r.IterativeLookup(context.Background(), &Question{Name: lookupName, Type: dns.TypeNS, Class: dns.ClassINET}) } else { - ns, trace, status, err = r.ExternalLookup(&Question{Name: lookupName, Type: dns.TypeNS, Class: dns.ClassINET}, nameServer) + ns, trace, status, err = r.ExternalLookup(context.Background(), &Question{Name: lookupName, Type: dns.TypeNS, Class: dns.ClassINET}, nameServer) } diff --git a/src/zdns/qa.go b/src/zdns/qa.go index 4e949a94..91d1072a 100644 --- a/src/zdns/qa.go +++ b/src/zdns/qa.go @@ -52,7 +52,7 @@ type TraceStep struct { Try int `json:"try" groups:"trace"` } -// Result contains all the metadata from a complete lookup(s) for a domain. Results is keyed with the ModuleName. +// Result contains all the metadata from a complete lookup(s) for a name. Results is keyed with the ModuleName. type Result struct { AlteredName string `json:"altered_name,omitempty" groups:"short,normal,long,trace"` Name string `json:"name,omitempty" groups:"short,normal,long,trace"` @@ -63,7 +63,7 @@ type Result struct { Results map[string]SingleModuleResult `json:"results,omitempty" groups:"short,normal,long,trace"` } -// SingleModuleResult contains all the metadata from a complete lookup for a domain, potentially after following many CNAMEs/etc. +// SingleModuleResult contains all the metadata from a complete lookup for a name, potentially after following many CNAMEs/etc. type SingleModuleResult struct { Status string `json:"status,omitempty" groups:"short,normal,long,trace"` Error string `json:"error,omitempty" groups:"short,normal,long,trace"` @@ -79,20 +79,20 @@ type SingleQueryResult struct { Additional []interface{} `json:"additionals,omitempty" groups:"short,normal,long,trace"` Authorities []interface{} `json:"authorities,omitempty" groups:"short,normal,long,trace"` Protocol string `json:"protocol" groups:"protocol,normal,long,trace"` - Resolver string `json:"resolver" groups:"resolver,normal,long,trace"` + Resolver string `json:"resolver" groups:"resolver,normal,long,trace"` // IP address Flags DNSFlags `json:"flags" groups:"flags,long,trace"` TLSServerHandshake interface{} `json:"tls_handshake,omitempty" groups:"normal,long,trace"` // used for --tls and --https, JSON string of the TLS handshake } type ExtendedResult struct { + Type string `json:"type" groups:"short,normal,long,trace"` Res SingleQueryResult `json:"result,omitempty" groups:"short,normal,long,trace"` Status Status `json:"status" groups:"short,normal,long,trace"` - Nameserver string `json:"nameserver" groups:"short,normal,long,trace"` - Trace Trace `json:"trace,omitempty" groups:"trace"` + Nameserver string `json:"nameserver" groups:"short,normal,long,trace"` // NS name queried for this result } -type CombinedResults struct { - Results []ExtendedResult `json:"results" groups:"short,normal,long,trace"` +type AllNameServersResult struct { + LayeredResponses map[string][]ExtendedResult `json:"per_layer_responses" groups:"short,normal,long,trace"` } type IPResult struct { diff --git a/src/zdns/resolver.go b/src/zdns/resolver.go index fa496fc9..c7f74dac 100644 --- a/src/zdns/resolver.go +++ b/src/zdns/resolver.go @@ -15,6 +15,7 @@ package zdns import ( + "context" "fmt" "math/rand" "net" @@ -84,7 +85,7 @@ type ResolverConfig struct { ExternalNameServersV6 []NameServer // v6 name servers used for external lookups RootNameServersV4 []NameServer // v4 root servers used for iterative lookups RootNameServersV6 []NameServer // v6 root servers used for iterative lookups - LookupAllNameServers bool // perform the lookup via all the nameservers for the domain + LookupAllNameServers bool // perform the lookup via all the nameservers for the name FollowCNAMEs bool // whether iterative lookups should follow CNAMEs/DNAMEs DNSConfigFilePath string // path to the DNS config file, ex: /etc/resolv.conf @@ -279,7 +280,7 @@ type Resolver struct { networkTimeout time.Duration // timeout for a single on-the-wire network call iterativeTimeout time.Duration // timeout for a layer of the iterative lookup - timeout time.Duration // timeout for the entire domain lookup + timeout time.Duration // timeout for the entire name lookup maxDepth int externalNameServers []NameServer // name servers used by external lookups (either OS or user specified) rootNameServers []NameServer // root servers used for iterative lookups @@ -542,7 +543,7 @@ func (r *Resolver) getConnectionInfo(nameServer *NameServer) (*ConnectionInfo, e ServerName: nameServer.DomainName, }) } else { - // If no domain name is provided, we can't verify the server's certificate + // If no name is provided, we can't verify the server's certificate tlsConn = tls.Client(conn, &tls.Config{ InsecureSkipVerify: true, }) @@ -596,7 +597,7 @@ func getNewTCPConn(nameServer *NameServer, connInfo *ConnectionInfo) error { // multiple lookups concurrently, create a new Resolver object for each concurrent lookup. // Returns the result of the lookup, the trace of the lookup (what each nameserver along the lookup returned), the // status of the lookup, and any error that occurred. -func (r *Resolver) ExternalLookup(q *Question, dstServer *NameServer) (*SingleQueryResult, Trace, Status, error) { +func (r *Resolver) ExternalLookup(ctx context.Context, q *Question, dstServer *NameServer) (*SingleQueryResult, Trace, Status, error) { if r.isClosed { log.Fatal("resolver has been closed, cannot perform lookup") } @@ -614,7 +615,7 @@ func (r *Resolver) ExternalLookup(q *Question, dstServer *NameServer) (*SingleQu } // dstServer has been validated and has a port, continue with lookup r.lastUsedExternalNameServer = dstServer - lookup, trace, status, err := r.lookupClient.DoDstServersLookup(r, *q, []NameServer{*dstServer}, false) + lookup, trace, status, err := r.lookupClient.DoDstServersLookup(ctx, r, *q, []NameServer{*dstServer}, false) return lookup, trace, status, err } @@ -624,11 +625,11 @@ func (r *Resolver) ExternalLookup(q *Question, dstServer *NameServer) (*SingleQu // multiple lookups concurrently, create a new Resolver object for each concurrent lookup. // Returns the result of the lookup, the trace of the lookup (what each nameserver along the lookup returned), the // status of the lookup, and any error that occurred. -func (r *Resolver) IterativeLookup(q *Question) (*SingleQueryResult, Trace, Status, error) { +func (r *Resolver) IterativeLookup(ctx context.Context, q *Question) (*SingleQueryResult, Trace, Status, error) { if r.isClosed { log.Fatal("resolver has been closed, cannot perform lookup") } - return r.lookupClient.DoDstServersLookup(r, *q, r.rootNameServers, true) + return r.lookupClient.DoDstServersLookup(ctx, r, *q, r.rootNameServers, true) } // Close cleans up any resources used by the resolver. This should be called when the resolver is no longer needed. diff --git a/testing/integration_tests.py b/testing/integration_tests.py index 5754bf4f..9eabbe85 100755 --- a/testing/integration_tests.py +++ b/testing/integration_tests.py @@ -1419,6 +1419,98 @@ def test_external_lookup_cache(self): # the second query has a much smaller response time than the first to show it's being cached self.assertTrue(first_duration / 50 > second_duration, f"Second query {second_duration} should be faster than the first {first_duration}") + def test_lookup_all_nameservers_single_zone_iterative(self): + """ + Test that --all-nameservers --iterative lookups work with domains whose nameservers are all in the same zone + zdns-testing.com has nameservers ns-cloud-c1/2/3/4.googledomains.com, which are all in the .com zone and so will have their IPs + provided as additionals in the .com response + """ + # zdns-testing.com's nameservers are all in the .com zone, so we should only have to query the .com nameservers + c = "A zdns-testing.com --all-nameservers --iterative --timeout=60" + cmd,res = self.run_zdns(c, "") + self.assertSuccess(res, cmd, "A") + # Check for layers + self.assertIn(".", res["results"]["A"]["data"]["per_layer_responses"], "Should have the root (.) layer") + self.assertIn("com", res["results"]["A"]["data"]["per_layer_responses"], "Should have the .com layer") + self.assertIn("zdns-testing.com", res["results"]["A"]["data"]["per_layer_responses"], "Should have the google.com layer") + # check for a.root-servers.net, b.root-servers.net, ... m.root-servers.net + self.check_for_existance_of_root_and_com_nses(res) + # check for the google.com nameservers + actual_zdns_testing_leaf_NS_answers = [] + actual_zdns_testing_leaf_A_answers = [] + for entry in res["results"]["A"]["data"]["per_layer_responses"]["zdns-testing.com"]: + if entry["type"] == "NS": + actual_zdns_testing_leaf_NS_answers.append(entry) + elif entry["type"] == "A": + actual_zdns_testing_leaf_A_answers.append(entry) + else: + self.fail(f"Unexpected record type {entry['type']}") + + + + # Check that we have "1.2.3.4", "2.3.4.5", and "3.4.5.6" as the A records and valid NS records for all expected Leaf NSes + if len(actual_zdns_testing_leaf_A_answers) != 4 or len(actual_zdns_testing_leaf_NS_answers) != 4: + self.fail("Should have 4 A and 4 NS record sets") + expectedAnswers = ["1.2.3.4", "2.3.4.5", "3.4.5.6"] + for entry in actual_zdns_testing_leaf_A_answers: + actualAnswers = [] + for answer in entry["result"]["answers"]: + actualAnswers.append(answer["answer"]) + # sort + actualAnswers.sort() + expectedAnswers.sort() + self.assertEqual(actualAnswers, expectedAnswers, "Should have the expected A records") + + def check_for_existance_of_root_and_com_nses(self, res): + actual_root_ns = [] + for entry in res["results"]["A"]["data"]["per_layer_responses"]["."]: + actual_root_ns.append(entry["nameserver"]) + for letter in "abcdefghijklm": + self.assertIn(f"{letter}.root-servers.net", actual_root_ns, "Should have the root nameservers") + # check for the .com nameservers + actual_com_nses = [] + for entry in res["results"]["A"]["data"]["per_layer_responses"]["com"]: + actual_com_nses.append(entry["nameserver"]) + for letter in "abcdefghijklm": + self.assertIn(f"{letter}.gtld-servers.net", actual_com_nses, "Should have the .com nameservers") + + def test_lookup_all_nameservers_multi_zone_iterative(self): + """ + Test that --all-nameservers lookups work with domains whose nameservers have their nameservers in different zones + In this case, example.com has a/b.iana-servers.net as nameservers, which are in the .com zone, but whose nameservers + are dig -t NS iana-servers.com -> ns.icann.org, a/b/c.iana-servers.net. This means the .com nameservers will not + provide the IPs in additionals. + """ + # example.com has nameservers in .com, .org, and .net, we'll have to iteratively figure out their IP addresses too + c = "A example.com --all-nameservers --iterative --timeout=60" + cmd,res = self.run_zdns(c, "") + self.assertSuccess(res, cmd, "A") + # Check for layers + self.assertIn(".", res["results"]["A"]["data"]["per_layer_responses"], "Should have the root (.) layer") + self.assertIn("com", res["results"]["A"]["data"]["per_layer_responses"], "Should have the .com layer") + self.assertIn("example.com", res["results"]["A"]["data"]["per_layer_responses"], "Should have the example.com layer") + self.check_for_existance_of_root_and_com_nses(res) + # check for the example.com nameservers + actual_example_nses = [] + for entry in res["results"]["A"]["data"]["per_layer_responses"]["example.com"]: + actual_example_nses.append(entry["nameserver"]) + expected_example_nses = ["a.iana-servers.net", "b.iana-servers.net"] + for ns in expected_example_nses: + self.assertIn(ns, actual_example_nses, "Should have the example.com nameservers") + + def test_lookup_all_nameservers_external_lookup(self): + """ + Test that --all-nameservers lookups work with external resolvers: cloudflare.com and google.com + """ + c = "A google.com --all-nameservers --name-servers='1.1.1.1,8.8.8.8'" + cmd,res = self.run_zdns(c, "") + self.assertSuccess(res, cmd, "A") + actual_resolvers = [] + for entry in res["results"]["A"]["data"]: + actual_resolvers.append(entry["resolver"]) + expected_resolvers = ["1.1.1.1:53", "8.8.8.8:53"] + for resolver in expected_resolvers: + self.assertIn(resolver, actual_resolvers, "Should have the expected resolvers")