├── resolver ├── resolver-interface.go ├── ipAddr.go ├── soa.go ├── diffStatus.go ├── cacheIpAddr.go └── diffResponse.go ├── go.mod ├── go.sum ├── README.md ├── main.go └── helpers └── helpers.go /resolver/resolver-interface.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import "github.com/miekg/dns" 4 | 5 | type Resolver interface { 6 | Resolve(domain string, client *dns.Client) (string, error) 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AlfredBerg/dnsline 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/miekg/dns v1.1.62 7 | github.com/patrickmn/go-cache v2.1.0+incompatible 8 | ) 9 | 10 | require ( 11 | golang.org/x/mod v0.20.0 // indirect 12 | golang.org/x/net v0.28.0 // indirect 13 | golang.org/x/sync v0.8.0 // indirect 14 | golang.org/x/sys v0.24.0 // indirect 15 | golang.org/x/tools v0.24.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /resolver/ipAddr.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/AlfredBerg/dnsline/helpers" 7 | "github.com/miekg/dns" 8 | ) 9 | 10 | type ipAddr struct { 11 | recordType uint16 12 | externalResolver string 13 | } 14 | 15 | func NewIpAddr(recordType uint16, externalResolver string) ipAddr { 16 | return ipAddr{recordType: recordType, externalResolver: externalResolver} 17 | } 18 | 19 | func (r ipAddr) Resolve(domain string, client *dns.Client) (string, error) { 20 | var err error 21 | var result *dns.Msg 22 | result, err = helpers.Resolve(domain, r.recordType, client, r.externalResolver, nil) 23 | 24 | if err != nil { 25 | return "", errors.New(fmt.Sprintf("%s failed with %s", domain, err)) 26 | } 27 | resultString := helpers.PrettyPrintResult(result) 28 | return resultString, nil 29 | } 30 | -------------------------------------------------------------------------------- /resolver/soa.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/AlfredBerg/dnsline/helpers" 7 | "github.com/miekg/dns" 8 | "github.com/patrickmn/go-cache" 9 | ) 10 | 11 | type soa struct { 12 | recordType uint16 13 | cache *cache.Cache 14 | } 15 | 16 | func NewSoa(recordType uint16, cache *cache.Cache) soa { 17 | return soa{recordType: recordType, cache: cache} 18 | } 19 | 20 | func (r soa) Resolve(domain string, client *dns.Client) (string, error) { 21 | nameservers, err := helpers.GetAuthoritativeNameservers(domain, client, r.recordType, r.cache) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | authority := "" 27 | //Lets just get the first ns record and what it is authoritative over 28 | for _, answer := range nameservers { 29 | switch r := answer.(type) { 30 | case *dns.NS: 31 | authority = r.Hdr.Name 32 | break 33 | } 34 | if authority != "" { 35 | break 36 | } 37 | } 38 | if authority == "" { 39 | return "", errors.New(fmt.Sprintf("%s Did not get any ns records", domain)) 40 | } 41 | return fmt.Sprintf("%s for %s", authority, domain), err 42 | } 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 2 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 3 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 4 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 5 | golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= 6 | golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 7 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 8 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 9 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 10 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 11 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 12 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 13 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 14 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 15 | -------------------------------------------------------------------------------- /resolver/diffStatus.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "fmt" 5 | "github.com/AlfredBerg/dnsline/helpers" 6 | "github.com/miekg/dns" 7 | "github.com/patrickmn/go-cache" 8 | "strings" 9 | ) 10 | 11 | type diffStatus struct { 12 | recordType uint16 13 | cache *cache.Cache 14 | } 15 | 16 | func NewDiffStatus(recordType uint16, cache *cache.Cache) diffStatus { 17 | return diffStatus{recordType: recordType, cache: cache} 18 | } 19 | 20 | func (r diffStatus) Resolve(domain string, client *dns.Client) (string, error) { 21 | nameservers, err := helpers.GetAuthoritativeNameservers(domain, client, r.recordType, r.cache) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | var statuses []string 27 | thereIsDifference := false 28 | firstStatus := "" 29 | for _, answer := range nameservers { 30 | switch rec := answer.(type) { 31 | case *dns.NS: 32 | statusAnswer, err := helpers.Resolve(domain, r.recordType, client, rec.Ns, r.cache) 33 | if err != nil { 34 | return "", err 35 | } 36 | status := dns.RcodeToString[statusAnswer.Rcode] 37 | if firstStatus == "" { 38 | firstStatus = status 39 | } else { 40 | if firstStatus != status { 41 | thereIsDifference = true 42 | } 43 | } 44 | statuses = append(statuses, fmt.Sprintf("%s@%s", status, rec.Ns)) 45 | } 46 | } 47 | return fmt.Sprintf("%s [%s] %v", domain, strings.Join(statuses, " "), thereIsDifference), err 48 | } 49 | -------------------------------------------------------------------------------- /resolver/cacheIpAddr.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "errors" 5 | "github.com/AlfredBerg/dnsline/helpers" 6 | "github.com/miekg/dns" 7 | "github.com/patrickmn/go-cache" 8 | ) 9 | 10 | type cacheIpAddr struct { 11 | recordType uint16 12 | cache *cache.Cache 13 | } 14 | 15 | func NewCacheIpAddr(recordType uint16, cache *cache.Cache) cacheIpAddr { 16 | return cacheIpAddr{recordType: recordType, cache: cache} 17 | } 18 | 19 | func (r cacheIpAddr) Resolve(domain string, client *dns.Client) (string, error) { 20 | nextDomain := domain 21 | var cnameResponses []dns.RR 22 | var aResponses []dns.RR 23 | for i := 0; i < 10; i++ { 24 | nameservers, err := helpers.GetAuthoritativeNameservers(nextDomain, client, r.recordType, r.cache) 25 | if err != nil { 26 | return "", err 27 | } 28 | nameserver := "" 29 | //TODO: randomize ns? 30 | for _, ns := range nameservers { 31 | if ns, ok := ns.(*dns.NS); ok { 32 | nameserver = ns.Ns 33 | } 34 | } 35 | 36 | if nameserver == "" { 37 | return "", errors.New("no ns to query found") 38 | } 39 | 40 | var result *dns.Msg 41 | result, err = helpers.Resolve(nextDomain, r.recordType, client, nameserver, r.cache) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | done := false 47 | if len(result.Answer) != 0 { 48 | if dns.RcodeToString[result.Rcode] == "NOERROR" { 49 | for _, record := range result.Answer { 50 | switch r := record.(type) { 51 | case *dns.CNAME: 52 | cnameResponses = append(cnameResponses, r) 53 | nextDomain = r.Target 54 | case *dns.A: 55 | aResponses = append(aResponses, r) 56 | done = true 57 | case *dns.AAAA: 58 | aResponses = append(aResponses, r) 59 | done = true 60 | } 61 | } 62 | } 63 | } else { 64 | done = true 65 | } 66 | 67 | if done { 68 | if len(cnameResponses) != 0 { 69 | //Reconstruct a response if we had to ask multiple servers 70 | result.Question[0].Name = dns.Fqdn(domain) 71 | result.Answer = cnameResponses 72 | result.Answer = append(result.Answer, aResponses...) 73 | } 74 | 75 | resultString := helpers.PrettyPrintResult(result) 76 | return resultString, nil 77 | } 78 | } 79 | return "", errors.New("to long cname chain") 80 | 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dnsline 2 | 3 | Making it easy to grep & cut accurate dns results. 4 | 5 | Dnsline uses a similar approach as a recursive dns resolver with a cache to get the results. This means 6 | that there is no need to supply a list of resolvers, and the results will come directly from the 7 | authoritative dns servers. 8 | 9 | ## Install 10 | 11 | ``` 12 | go install github.com/AlfredBerg/dnsline@latest 13 | ``` 14 | Old go versions <1.16: 15 | ``` 16 | go get -u github.com/AlfredBerg/dnsline 17 | ``` 18 | 19 | ## Usage 20 | 21 | Basic usage: 22 | 23 | ``` 24 | $ cat domains.txt | dnsline 25 | example.com.>[203.0.113.1] 26 | cname.example.com.>cname-chain.example.com.>[203.0.113.1 203.0.113.2] 27 | ``` 28 | 29 | Get response difference 30 | ``` 31 | $ cat domains.txt | dnsline -m responsediff 32 | example.com [203.0.113.1]@a.iana-servers.net. false 33 | cdn.example.com [203.0.113.1]@a.iana-servers.net. [203.0.113.2]@b.iana-servers.net. true 34 | ``` 35 | 36 | Get status difference 37 | ``` 38 | $ cat domains.txt | dnsline -m statusdiff 39 | example.com [NOERROR@a.iana-servers.net. NOERROR@b.iana-servers.net.] false 40 | foo.example.com [NXDOMAIN@a.iana-servers.net. NXDOMAIN@b.iana-servers.net.] false 41 | diff.example.com [NXDOMAIN@a.iana-servers.net. NOERROR@b.iana-servers.net.] true 42 | ``` 43 | 44 | Get SOA (Start Of Authority) 45 | ``` 46 | $ cat domains.txt | dnsline -m soa 47 | example.com. for example.com 48 | example.com. for foo.example.com 49 | delegation.example.com. for a.delegation.example.com 50 | ``` 51 | 52 | ## Options 53 | 54 | ``` 55 | $ dnsline -h 56 | Usage: 57 | -c int 58 | Number of goroutines that try to resolve domains. (default 10) 59 | -i string 60 | File to read input from if STDIN is not used. 61 | -m string 62 | Mode to use. Available: resolve, soa, statusdiff, responsediff 63 | resolve: Resolve the input domains and output the whole CNAME chain and all ip-addresses. 64 | soa: Print the FQDN of the closes SOA (Start Of Authority). 65 | statusdiff: Check if there is a difference between the statuses from the authoritative nameservers. 66 | responsediff: Check if there is a difference between the resolved address(es) from the authoritative nameservers. 67 | (default "resolve") 68 | -q Query for AAAA records instead of A records 69 | -x Disable the nameserver cache. With this disabled all queries will begin from the DNS ROOT. 70 | ``` 71 | -------------------------------------------------------------------------------- /resolver/diffResponse.go: -------------------------------------------------------------------------------- 1 | package resolver 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/AlfredBerg/dnsline/helpers" 7 | "github.com/miekg/dns" 8 | "github.com/patrickmn/go-cache" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | type diffResponse struct { 14 | recordType uint16 15 | cache *cache.Cache 16 | } 17 | 18 | func NewDiffResponse(recordType uint16, cache *cache.Cache) diffResponse { 19 | return diffResponse{recordType: recordType, cache: cache} 20 | } 21 | 22 | func (r diffResponse) Resolve(domain string, client *dns.Client) (string, error) { 23 | nameservers, err := helpers.GetAuthoritativeNameservers(domain, client, r.recordType, r.cache) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | thereIsDifference := false 29 | firstNs := "" 30 | diffNs := "" 31 | var firstResponse, differentResponse []string 32 | for i, answer := range nameservers { 33 | switch rec := answer.(type) { 34 | case *dns.NS: 35 | statusAnswer, err := helpers.Resolve(domain, r.recordType, client, rec.Ns, r.cache) 36 | if err != nil { 37 | return "", err 38 | } 39 | records := []string{} 40 | for _, record := range statusAnswer.Answer { 41 | rString := "" 42 | switch r := record.(type) { 43 | case *dns.A: 44 | rString = r.A.String() 45 | case *dns.AAAA: 46 | rString = r.AAAA.String() 47 | case *dns.CNAME: 48 | rString = r.Target 49 | default: 50 | return "", errors.New(fmt.Sprintf("Unknown record type: %s", record.String())) 51 | } 52 | records = append(records, rString) 53 | } 54 | sort.Strings(records) 55 | 56 | if i == 0 { 57 | firstResponse = records 58 | firstNs = rec.Ns 59 | } else { 60 | if len(firstResponse) != len(records) { 61 | thereIsDifference = true 62 | break 63 | } 64 | if len(records) == 0 { 65 | break 66 | } 67 | for r := range firstResponse { 68 | if firstResponse[r] != records[r] { 69 | thereIsDifference = true 70 | break 71 | } 72 | } 73 | if thereIsDifference { 74 | differentResponse = records 75 | diffNs = rec.Ns 76 | break 77 | } 78 | } 79 | } 80 | if thereIsDifference { 81 | break 82 | } 83 | } 84 | b := strings.Builder{} 85 | b.WriteString(fmt.Sprintf("%s [%s]@%s %v", domain, strings.Join(firstResponse, " "), firstNs, thereIsDifference)) 86 | if thereIsDifference { 87 | b.WriteString(fmt.Sprintf("[%s]@%s", strings.Join(differentResponse, " "), diffNs)) 88 | } 89 | return b.String(), nil 90 | } 91 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "github.com/AlfredBerg/dnsline/resolver" 8 | "github.com/miekg/dns" 9 | "github.com/patrickmn/go-cache" 10 | "log" 11 | "os" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | var ( 18 | //TODO: flag for number of dns retries? 19 | quadLookup = flag.Bool("q", false, "Query for AAAA records instead of A records") 20 | disableNameserverCache = flag.Bool("x", false, "Disable the nameserver cache. With this disabled all queries will begin from the DNS ROOT.") 21 | concurrency = flag.Int("c", 10, "Number of goroutines that try to resolve domains.") 22 | inFile = flag.String("i", "", "File to read input from if STDIN is not used.") 23 | //externalResolver = flag.String("r", "1.1.1.1", "Resolver to use.") 24 | mode = flag.String("m", "resolve", "Mode to use. Available: resolve, soa, statusdiff, responsediff\n\t"+ 25 | "resolve: Resolve the input domains and output the whole CNAME chain and all ip-addresses.\n\t"+ 26 | "soa: Print the FQDN of the closes SOA (Start Of Authority).\n\t"+ 27 | "statusdiff: Check if there is a difference between the statuses from the authoritative nameservers.\n\t"+ 28 | "responsediff: Check if there is a difference between the resolved address(es) from the authoritative nameservers.\n") 29 | ) 30 | 31 | func main() { 32 | flag.Usage = func() { 33 | fmt.Fprintf(os.Stderr, "Usage: \n") 34 | flag.PrintDefaults() 35 | } 36 | flag.Parse() 37 | 38 | recordType := dns.TypeA 39 | if *quadLookup { 40 | recordType = dns.TypeAAAA 41 | } 42 | 43 | c := cache.New(5*time.Minute, 10*time.Minute) 44 | if *disableNameserverCache { 45 | c = nil 46 | } 47 | 48 | var resolveMethod resolver.Resolver 49 | if *mode == "soa" { 50 | resolveMethod = resolver.NewSoa(recordType, c) 51 | } else if *mode == "statusdiff" { 52 | fmt.Fprint(os.Stderr, "domain [status@ns.example.com] isDifference\n") 53 | resolveMethod = resolver.NewDiffStatus(recordType, c) 54 | } else if *mode == "responsediff" { 55 | fmt.Fprint(os.Stderr, "domain [response]@ns1 ([response]@ns2) isDifference\n") 56 | resolveMethod = resolver.NewDiffResponse(recordType, c) 57 | } else if *mode == "resolve" { 58 | fmt.Fprint(os.Stderr, "domain>cname(s)>[127.0.0.1 127.0.0.2]\n") 59 | resolveMethod = resolver.NewCacheIpAddr(recordType, c) 60 | //}else if *mode == "externalResolve" { 61 | // resolveMethod = resolver.NewIpAddr(recordType, *externalResolver) 62 | } else { 63 | log.Fatal("A unknown -m mode was specified") 64 | } 65 | 66 | domains := make(chan string) 67 | output := make(chan string) 68 | var workerWG sync.WaitGroup 69 | for i := 0; i < *concurrency; i++ { 70 | workerWG.Add(1) 71 | go func() { 72 | client := new(dns.Client) 73 | for domain := range domains { 74 | resultString, err := resolveMethod.Resolve(domain, client) 75 | if err != nil { 76 | log.Printf("Error for %s: %s\n", domain, err.Error()) 77 | continue 78 | } 79 | output <- resultString 80 | } 81 | workerWG.Done() 82 | }() 83 | } 84 | 85 | // Close the output channel when the workers are done 86 | go func() { 87 | workerWG.Wait() 88 | close(output) 89 | }() 90 | 91 | go func() { 92 | var sc *bufio.Scanner 93 | if *inFile == "" { 94 | sc = bufio.NewScanner(os.Stdin) 95 | } else { 96 | f, err := os.Open(*inFile) 97 | if err != nil { 98 | panic(err) 99 | } 100 | sc = bufio.NewScanner(f) 101 | } 102 | for sc.Scan() { 103 | domain := strings.ToLower(sc.Text()) 104 | domains <- domain 105 | } 106 | if sc.Err() != nil { 107 | panic(sc.Err()) 108 | } 109 | close(domains) 110 | }() 111 | 112 | for line := range output { 113 | fmt.Println(line) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/miekg/dns" 7 | "github.com/patrickmn/go-cache" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | I_ROOT_SERVER = "192.36.148.17" 14 | MAX_RETRIES = 3 15 | ) 16 | 17 | func Resolve(label string, recordType uint16, client *dns.Client, nameserver string, c *cache.Cache) (*dns.Msg, error) { 18 | var finalError error 19 | for i := 0; i < MAX_RETRIES; i++ { 20 | m := new(dns.Msg) 21 | m.SetQuestion(dns.Fqdn(label), recordType) 22 | answer, _, err := client.Exchange(m, nameserver+":53") 23 | if err != nil { 24 | finalError = err 25 | continue 26 | } 27 | 28 | if zoneCut := getAnswerZoneCut(answer); c != nil && zoneCut != "" { 29 | c.Add(zoneCut, answer, 0) 30 | } 31 | return answer, nil 32 | } 33 | return nil, finalError 34 | } 35 | 36 | //Gets where the delegation starts/stops. At returns "" for nameservers in SOA records (common for nxdomain responses) 37 | func getAnswerZoneCut(answer *dns.Msg) string { 38 | for _, ns := range answer.Ns { 39 | if ns, ok := ns.(*dns.NS); ok { 40 | return ns.Hdr.Name 41 | } 42 | } 43 | return "" 44 | } 45 | 46 | //Returns a list of RRs that should contain NS records for the closes authoritative zone to the input domain. It is the responsibility of the caller to check that the RRs are actually NS records 47 | func GetAuthoritativeNameservers(domain string, client *dns.Client, recordType uint16, cache *cache.Cache) ([]dns.RR, error) { 48 | var previousNSresourceRecords []dns.RR 49 | previousNsServer := "fromCache" 50 | maxDepth := 10 51 | 52 | gotCacheResult, answer := getClosestCachedNameserver(domain, cache) 53 | 54 | var err error 55 | if !gotCacheResult { 56 | answer, err = Resolve(domain, recordType, client, I_ROOT_SERVER, cache) 57 | previousNsServer = I_ROOT_SERVER 58 | if err != nil { 59 | return nil, err 60 | } 61 | } else { 62 | var ns *dns.NS 63 | for _, answer := range answer.Ns { 64 | switch r := answer.(type) { 65 | case *dns.NS: 66 | ns = r 67 | break 68 | } 69 | } 70 | if ns != nil { 71 | previousNSresourceRecords = answer.Ns 72 | } 73 | } 74 | //Lets (kinda) act like a recursive resolver from ROOT. The closest SOA will be the last place that had nameservers 75 | previousNSresourceRecords = answer.Ns 76 | for i := 0; i < maxDepth; i++ { 77 | if answer.Authoritative { 78 | if previousNSresourceRecords != nil { 79 | return previousNSresourceRecords, nil 80 | } 81 | return nil, errors.New(fmt.Sprintf("unexpected state, got a authorative response but never got any NS servers. Rcode: %s from %s", dns.RcodeToString[answer.Rcode], previousNsServer)) 82 | } 83 | 84 | //Pick the first NS record 85 | var ns *dns.NS 86 | for _, answer := range answer.Ns { 87 | switch r := answer.(type) { 88 | case *dns.NS: 89 | ns = r 90 | } 91 | } 92 | 93 | if ns == nil { 94 | return previousNSresourceRecords, errors.New(fmt.Sprintf("unexpected response, got a response not containing ns records and it is not authorative. Rcode: %s from %s", dns.RcodeToString[answer.Rcode], previousNsServer)) 95 | } 96 | 97 | previousNSresourceRecords = answer.Ns 98 | //Lazy, lets not look at the glue records or traverse the dns-tree ourself again for the address of the NS record. Make the default OS resolver handle that 99 | answer, err = Resolve(domain, dns.TypeA, client, ns.Ns, cache) 100 | previousNsServer = ns.Ns 101 | if err != nil { 102 | return nil, errors.New(fmt.Sprintf("%s failed with %s", domain, err)) 103 | } 104 | } 105 | return nil, errors.New(fmt.Sprintf("%s depth limit reached before answer", domain)) 106 | } 107 | 108 | func getClosestCachedNameserver(domain string, c *cache.Cache) (gotCacheResult bool, answer *dns.Msg) { 109 | if c == nil { 110 | return false, nil 111 | } 112 | 113 | labels := dns.SplitDomainName(domain) 114 | //Find the longest string of labels in cache 115 | for i := range labels { 116 | d := strings.Join(labels[i:], ".") 117 | if interfaceResult, found := c.Get(dns.Fqdn(d)); found { 118 | if in, success := interfaceResult.(*dns.Msg); success { 119 | return true, in 120 | } 121 | } 122 | } 123 | return false, nil 124 | } 125 | 126 | func PrettyPrintResult(result *dns.Msg) string { 127 | var b strings.Builder 128 | if len(result.Question) != 0 { 129 | b.WriteString(result.Question[0].Name + ">") 130 | } 131 | var aRecords []string 132 | for _, answer := range result.Answer { 133 | switch r := answer.(type) { 134 | case *dns.CNAME: 135 | b.WriteString(r.Target) 136 | case *dns.A: 137 | aRecords = append(aRecords, r.A.String()) 138 | case *dns.AAAA: 139 | aRecords = append(aRecords, r.AAAA.String()) 140 | default: 141 | b.WriteString("UNKNOWN-RECORD-TYPE") 142 | } 143 | if len(aRecords) == 0 { 144 | b.WriteString(">") 145 | } 146 | } 147 | sort.Strings(aRecords) 148 | b.WriteString("[") 149 | b.WriteString(strings.Join(aRecords, " ")) 150 | b.WriteString("]") 151 | return b.String() 152 | } 153 | --------------------------------------------------------------------------------