Skip to content
Browse files

First commit

  • Loading branch information...
1 parent 660436a commit cf376e6f0a610a5fcf897fdf71d231ec48e84957 @bortzmeyer committed
Showing with 511 additions and 1 deletion.
  1. +75 −1 README.md
  2. +436 −0 check-soa.go
View
76 README.md
@@ -1,4 +1,78 @@
check-soa
=========
-A simple command-line DNS testing tool
+A simple command-line DNS testing tool
+
+Summary
+-------
+
+Its main use is for having rapidly an idea of the health of a DNS
+zone. It queries each name server of the zone for the SOA record and
+displays the value of the serial number for each server.
+
+It tries to be simple and fast. As a result, it is not a competitor
+for serious DNS checkers like [Zonecheck](http://www.zonecheck.fr/).
+
+Usage
+-----
+
+ check-soa ZONENAME
+
+`-h` will tell you the possible options.
+
+Installation
+------------
+It is written in [Go](http://golang.org), so you need a Go compiler
+installed. Here, we assume you have cgo.
+
+check-soa depends on GoDNS so you need to install it:
+ go get github.com/miekg/dns
+
+Then, compile:
+ go build check-soa.go
+
+And copy the executable where you want.
+
+Origins
+-------
+This program is heavily inspired by the original check-soa found in
+the excellent book "DNS and BIND" by Liu and Albitz. Unlike the
+original one, my check-soa:
+* queries the name servers in parallel
+* works with IPv4 and IPv6
+
+Reference site
+--------------
+[At Github](https://github.com/bortzmeyer/check-soa)
+
+Licence
+-------
+Copyright (c) 2012, Stephane Bortzmeyer
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS'
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+Author
+------
+
+Stephane Bortzmeyer <bortzmeyer@nic.fr>
+
View
436 check-soa.go
@@ -0,0 +1,436 @@
+/*
+
+A simple program to have rapidly an idea of the health of a DNS
+zone. It queries each name server of the zone for the SOA record and
+displays the value of the serial number for each server.
+
+Stephane Bortzmeyer <bortzmeyer@nic.fr>
+
+*/
+
+package main
+
+import (
+ "errors"
+ "flag"
+ "fmt"
+ "github.com/miekg/dns"
+ "net"
+ "os"
+ "sort"
+ "strings"
+ "time"
+)
+
+const (
+ TIMEOUT float64 = float64(1.5)
+ MAXTRIALS uint = 3
+ MAX_NAMESERVERS uint = 20
+ MAX_ADDRESSES uint = 10
+ EDNSBUFFERSIZE uint16 = 4096
+)
+
+var (
+ /* TODO: make it per-thread? It does not seem necessary, the goroutines
+ do not modify it */
+ conf *dns.ClientConfig
+ v4only *bool
+ v6only *bool
+ debug *bool
+ quiet *bool
+ noedns *bool
+ times *bool
+ timeout time.Duration
+ maxTrials *int
+)
+
+type DNSreply struct {
+ qname string
+ qtype uint16
+ r *dns.Msg
+ err error
+ nameserver string
+ rtt time.Duration
+}
+
+type SOAreply struct {
+ name string
+ address string
+ serial uint32
+ retrieved bool
+ msg string
+ rtt time.Duration
+}
+
+type nameServer struct {
+ name string
+ ips []string
+ globalErrMsg string
+ success []bool
+ errMsg []string
+ serial []uint32
+ rtts []time.Duration
+}
+
+type Results map[string]nameServer
+
+func localQuery(mychan chan DNSreply, qname string, qtype uint16) {
+ var result DNSreply
+ var trials uint
+ result.qname = qname
+ result.qtype = qtype
+ result.r = nil
+ result.err = errors.New("No name server to answer the question")
+ localm := new(dns.Msg)
+ localm.MsgHdr.RecursionDesired = true
+ localm.Question = make([]dns.Question, 1)
+ localc := new(dns.Client)
+ localc.ReadTimeout = timeout
+ localm.Question[0] = dns.Question{qname, qtype, dns.ClassINET}
+Tests:
+ for trials = 0; trials < uint(*maxTrials); trials++ {
+ Resolvers:
+ for serverIndex := range conf.Servers {
+ server := conf.Servers[serverIndex]
+ result.nameserver = server
+ r, rtt, err := localc.Exchange(localm, server+":"+conf.Port)
+ if r == nil {
+ result.r = nil
+ result.err = err
+ if err.(net.Error).Timeout() {
+ // Try another resolver
+ break Resolvers
+ } else { // We give in
+ break Tests
+ }
+ } else {
+ result.rtt = rtt
+ if r.Rcode == dns.RcodeSuccess {
+ // TODO: for rcodes like SERVFAIL, trying another resolver could make sense
+ result.r = r
+ result.err = nil
+ break Tests
+ } else {
+ // All the other codes are errors. Yes, it may
+ // happens that one resolver returns REFUSED
+ // and the others work but we do not handle
+ // this case. TODO: delete the resolver from
+ // the list and try another one
+ result.r = r
+ result.err = errors.New(dns.RcodeToString[r.Rcode])
+ break Tests
+ }
+ }
+ }
+ }
+ if *debug {
+ fmt.Printf("DEBUG: end of request %s %d\n", qname, qtype)
+ }
+ mychan <- result
+}
+
+func soaQuery(mychan chan SOAreply, zone string, name string, server string) {
+ var result SOAreply
+ var trials uint
+ result.retrieved = false
+ result.name = name
+ result.address = server
+ result.msg = "UNKNOWN"
+ m := new(dns.Msg)
+ // TODO: allow option DNSSEC DO (to detect firewall or fragmentation problems)
+ if !*noedns {
+ m.SetEdns0(EDNSBUFFERSIZE, true)
+ }
+ m.MsgHdr.RecursionDesired = false
+ m.Question = make([]dns.Question, 1)
+ c := new(dns.Client)
+ c.ReadTimeout = timeout
+ m.Question[0] = dns.Question{zone, dns.TypeSOA, dns.ClassINET}
+ nsAddressPort := ""
+ if strings.ContainsAny(":", server) {
+ /* IPv6 address */
+ nsAddressPort = "[" + server + "]:53"
+ } else {
+ nsAddressPort = server + ":53"
+ }
+ if *debug {
+ fmt.Printf("DEBUG Querying SOA from %s\n", nsAddressPort)
+ }
+ for trials = 0; trials < uint(*maxTrials); trials++ {
+ soa, rtt, err := c.Exchange(m, nsAddressPort)
+ if soa == nil {
+ result.rtt = 0
+ if !err.(net.Error).Timeout() {
+ result.msg = fmt.Sprintf("%s", err)
+ break
+ } else {
+ result.msg = fmt.Sprintf("Timeout")
+ }
+ } else {
+ result.rtt = rtt
+ if soa.Rcode != dns.RcodeSuccess {
+ result.msg = dns.RcodeToString[soa.Rcode]
+ break
+ } else {
+ if len(soa.Answer) == 0 { /* May happen if the server is a recursor, not authoritative, since we query with RD=0 */
+ result.msg = "0 answer"
+ break
+ } else {
+ rsoa := soa.Answer[0]
+ switch rsoa.(type) {
+ case *dns.SOA:
+ if soa.MsgHdr.Authoritative {
+ result.retrieved = true
+ result.serial = rsoa.(*dns.SOA).Serial
+ result.msg = "OK"
+ } else {
+ result.msg = "Not authoritative"
+ }
+ default:
+ // TODO: a name server can send us other RR types.
+ fmt.Printf("Internal error when processing %s\n", rsoa)
+ os.Exit(1)
+ }
+ break
+ }
+ }
+ }
+ }
+ mychan <- result
+}
+
+func masterTask(zone string, nameserverRecords []dns.RR) (uint, bool, Results) {
+ var (
+ numRequests uint
+ )
+ success := true
+ addressChannel := make(chan DNSreply)
+ soaChannel := make(chan SOAreply)
+ numNS := uint(0)
+ numAddrNS := uint(0)
+ nameservers := make(map[string]nameServer)
+ results := make(Results)
+ for i := range nameserverRecords {
+ ans := nameserverRecords[i]
+ switch ans.(type) {
+ case *dns.NS:
+ name := ans.(*dns.NS).Ns
+ nameservers[name] = nameServer{name: name, ips: make([]string, MAX_ADDRESSES)}
+ if !*v6only {
+ go localQuery(addressChannel, name, dns.TypeA)
+ }
+ if !*v4only {
+ go localQuery(addressChannel, name, dns.TypeAAAA)
+ }
+ numNS += 1
+ }
+ }
+ if *v6only || *v4only {
+ numRequests = numNS
+ } else {
+ numRequests = numNS * 2
+ }
+ for i := uint(0); i < numRequests; i++ {
+ addrResult := <-addressChannel
+ addrFamily := "IPv6"
+ if addrResult.qtype == dns.TypeA {
+ addrFamily = "IPv4"
+ }
+ if addrResult.r == nil {
+ // TODO We may have different globalErrMsg is it
+ // works with IPv4 but not IPv6 (it should not happen but it does)
+ nameservers[addrResult.qname] = nameServer{
+ name: addrResult.qname,
+ ips: nil,
+ globalErrMsg: fmt.Sprintf("Cannot get the %s address: %s", addrFamily, addrResult.err)}
+ success = false
+ } else {
+ if addrResult.r.Rcode != dns.RcodeSuccess {
+ nameservers[addrResult.qname] = nameServer{
+ name: addrResult.qname,
+ ips: nil,
+ globalErrMsg: fmt.Sprintf("Cannot get the %s address: %s", addrFamily, dns.RcodeToString[addrResult.r.Rcode])}
+ success = false
+ } else {
+ for j := range addrResult.r.Answer {
+ ansa := addrResult.r.Answer[j]
+ var ns string
+ switch ansa.(type) {
+ case *dns.A:
+ ns = ansa.(*dns.A).A.String()
+ nameservers[addrResult.qname] = nameServer{name: addrResult.qname, ips: append(nameservers[addrResult.qname].ips, ns)}
+ numAddrNS += 1
+ go soaQuery(soaChannel, zone, addrResult.qname, ns)
+ case *dns.AAAA:
+ ns = ansa.(*dns.AAAA).AAAA.String()
+ nameservers[addrResult.qname] = nameServer{name: addrResult.qname, ips: append(nameservers[addrResult.qname].ips, ns)}
+ numAddrNS += 1
+ go soaQuery(soaChannel, zone, addrResult.qname, ns)
+ }
+ }
+ }
+ }
+ }
+ for i := uint(0); i < numAddrNS; i++ {
+ if *debug {
+ fmt.Printf("DEBUG Getting result for ns #%d/%d\n", i+1, numAddrNS)
+ }
+ soaResult := <-soaChannel
+ _, present := results[soaResult.name]
+ if !present {
+ results[soaResult.name] = nameServer{name: soaResult.name,
+ ips: make([]string, 0),
+ success: make([]bool, 0),
+ errMsg: make([]string, 0),
+ serial: make([]uint32, 0),
+ rtts: make([]time.Duration, 0)}
+ }
+ if !soaResult.retrieved {
+ results[soaResult.name] = nameServer{name: soaResult.name,
+ ips: append(results[soaResult.name].ips, soaResult.address),
+ success: append(results[soaResult.name].success, false),
+ errMsg: append(results[soaResult.name].errMsg, fmt.Sprintf("%s", soaResult.msg)),
+ serial: append(results[soaResult.name].serial, 0),
+ rtts: append(results[soaResult.name].rtts, soaResult.rtt)}
+ } else {
+ results[soaResult.name] = nameServer{name: soaResult.name,
+ ips: append(results[soaResult.name].ips, soaResult.address),
+ success: append(results[soaResult.name].success, true),
+ errMsg: append(results[soaResult.name].errMsg, ""),
+ serial: append(results[soaResult.name].serial, soaResult.serial),
+ rtts: append(results[soaResult.name].rtts, soaResult.rtt)}
+ }
+ }
+ for name := range nameservers {
+ if nameservers[name].ips == nil {
+ results[name] = nameservers[name]
+ }
+ }
+ return numNS, success, results
+}
+
+func main() {
+ var (
+ err error
+ )
+ flag.Usage = func() {
+ fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, "%s [options] ZONE\n", os.Args[0])
+ flag.PrintDefaults()
+ }
+ v4only = flag.Bool("4", false, "Use only IPv4")
+ v6only = flag.Bool("6", false, "Use only IPv6")
+ help := flag.Bool("h", false, "Print help")
+ debug = flag.Bool("d", false, "Debugging")
+ quiet = flag.Bool("q", false, "Quiet mode, display only errors")
+ noedns = flag.Bool("r", false, "Disable EDNS format")
+ times = flag.Bool("i", false, "Display the response time of servers")
+ timeoutI := flag.Float64("t", float64(TIMEOUT), "Timeout in seconds")
+ maxTrials = flag.Int("n", int(MAXTRIALS), "Number of trials before giving in")
+ flag.Parse()
+ if *debug && *quiet {
+ fmt.Fprintf(os.Stderr, "debug or quiet but not both\n")
+ flag.Usage()
+ os.Exit(1)
+ }
+ if *v4only && *v6only {
+ fmt.Fprintf(os.Stderr, "v4-only or v6-only but not both\n")
+ flag.Usage()
+ os.Exit(1)
+ }
+ if len(flag.Args()) != 1 {
+ fmt.Fprintf(os.Stderr, "Only one argument expected, %d arguments received\n", len(flag.Args()))
+ flag.Usage()
+ os.Exit(1)
+ }
+ if *timeoutI <= 0 {
+ fmt.Fprintf(os.Stderr, "Timeout must be positive, not %d\n", *timeoutI)
+ flag.Usage()
+ os.Exit(1)
+ }
+ timeout = time.Duration(*timeoutI * float64(time.Second))
+ if *maxTrials <= 0 {
+ fmt.Fprintf(os.Stderr, "Number of trials must be positive, not %d\n", *maxTrials)
+ flag.Usage()
+ os.Exit(1)
+ }
+ if *help {
+ flag.Usage()
+ os.Exit(0)
+ }
+ zone := flag.Arg(0)
+ if zone[len(zone)-1] != '.' {
+ zone += "."
+ }
+ conf, err = dns.ClientConfigFromFile("/etc/resolv.conf")
+ if conf == nil {
+ fmt.Printf("Cannot initialize the local resolver: %s\n", err)
+ os.Exit(1)
+ }
+ nsChan := make(chan DNSreply)
+ go localQuery(nsChan, zone, dns.TypeNS)
+ nsResult := <-nsChan
+ if nsResult.r == nil {
+ fmt.Printf("Cannot retrieve the list of name servers for %s: %s\n", zone, nsResult.err)
+ os.Exit(1)
+ }
+ if nsResult.r.Rcode == dns.RcodeNameError {
+ fmt.Printf("No such domain %s\n", zone)
+ os.Exit(1)
+ }
+ numNS, success, results := masterTask(zone, nsResult.r.Answer)
+ if numNS == 0 {
+ fmt.Printf("No NS records for \"%s\". It is probably a domain but not a zone\n", zone)
+ os.Exit(1)
+ }
+ /* TODO: test if all name servers have the same serial ? */
+ keys := make([]string, len(results))
+ i := 0
+ for k, _ := range results {
+ keys[i] = k
+ i++
+ }
+ // TODO: allow to sort by response time?
+ sort.Strings(keys)
+ for k := range keys {
+ serverOK := true
+ result := results[keys[k]]
+ for i := 0; i < len(result.ips); i++ {
+ if !result.success[i] {
+ serverOK = false
+ break
+ }
+ if result.ips == nil {
+ serverOK = false
+ break
+ }
+ }
+ if !*quiet || !serverOK {
+ fmt.Printf("%s\n", keys[k])
+ }
+ for i := 0; i < len(result.ips); i++ {
+ code := "ERROR"
+ msg := ""
+ if result.success[i] {
+ code = "OK"
+ msg = fmt.Sprintf("%d", result.serial[i])
+ } else {
+ msg = result.errMsg[i]
+ }
+ if *times && result.rtts[i] != 0 {
+ msg = msg + fmt.Sprintf(" (%d ms)", int(float64(result.rtts[i])/1e6))
+ }
+ if !*quiet || !result.success[i] {
+ fmt.Printf("\t%s: %s: %s\n", result.ips[i], code, msg)
+ }
+ }
+ if len(result.ips) == 0 {
+ fmt.Printf("\t%s\n", result.globalErrMsg)
+ }
+ }
+ if success {
+ os.Exit(0)
+ } else {
+ os.Exit(1)
+ }
+}

0 comments on commit cf376e6

Please sign in to comment.
Something went wrong with that request. Please try again.