// Source: https://github.com/bortzmeyer/check-soa/blob/master/check-soa.go // 2-Clause BSD License: Copyright (c) 2012, Stephane Bortzmeyer All rights reserved. // 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 // Heavily modified for RSMon package cdns import ( "errors" "fmt" "net" "strings" "time" "github.com/miekg/dns" "github.com/weppos/publicsuffix-go/publicsuffix" "rocketgit.ru/rsmon/worker/app/models" ) const ( stateOK = "OK" stateERR = "ERR" stateWARN = "WARN" ) var localhost net.IP func init() { localhost = net.ParseIP("127.0.0.1") } // Perform checks if domain is resolvable via it's DNS servers func Perform(c *models.Check) *Result { result := &Result{} result.State = "FAIL" host := c.Monitor.Host if host == "" { result.State = "FAIL" result.Error = errors.New("empty host name") return result } start := time.Now() // log.Println("run dns:", host) if host == "localhost" || strings.HasPrefix(host, "localhost:") { result.Warnings = append(result.Warnings, "DNS check not possible for localhost, please disable") return result } addr := net.ParseIP(host) if addr != nil { result.Warnings = append(result.Warnings, "DNS check not possible for ip address, please disable") return result } zname, err := publicsuffix.Domain(host) if err != nil { result.Warnings = append(result.Warnings, "Failed to get public suffix: "+err.Error()) zname = host } // if zname != host { // result.Infos = append(result.Infos, "not top level domain, running NS check for "+zname) // } zone := dns.Fqdn(zname) nsChan := make(chan DNSreply) // log.Println(zone) go localQuery(nsChan, zone, dns.TypeNS) nsResult := <-nsChan if nsResult.r == nil { // All recursive resolvers were unreachable, so this worker could not // establish whether the domain is healthy. Do not open an outage event. result.State = "FAIL" result.Error = fmt.Errorf("cannot retrieve the list of name servers for %s: %s", zone, nsResult.err) return result } if nsResult.r.Rcode == dns.RcodeNameError { result.State = stateERR result.Error = fmt.Errorf("no such domain %s", zone) return result } // spew.Dump(nsResult) nslist := make(map[string]nameServer, 0) for i := range nsResult.r.Answer { ans := nsResult.r.Answer[i] if ns, ok := ans.(*dns.NS); ok { name := ns.Ns nslist[name] = nameServer{name: name, ips: make([]string, MaxAddresses)} } } // spew.Dump(nslist) numNS, numNSaddr, success, results := masterTask(zone, nslist) if success { result.State = stateOK } else { result.State = stateERR } if numNS == 0 { result.State = stateERR result.Error = fmt.Errorf("no NS records for zone \"%s\"", zone) return result } if numNSaddr == 0 { result.State = stateERR result.Error = fmt.Errorf("no IP addresses for name servers of %s", zone) return result } gallOK := true ganyOK := false failedNS := []string{} lzone := dns.Fqdn(host) for _, rzt := range results { //nolint:gocritic // range copy is acceptable here // spew.Dump(rzt) allOK := true anyOK := false ns := NSServer{Name: rzt.name} for i := 0; i < len(rzt.ips); i++ { ip := NSIP{ ResponseTime: rzt.rtts[i], IP: rzt.ips[i], } if rzt.success[i] { anyOK = true ganyOK = true ip.State = stateOK ip.Serial = rzt.serial[i] } else { allOK = false gallOK = false ip.State = stateERR ip.Error = errors.New(rzt.errMsg[i]) failedNS = append(failedNS, rzt.name) // spew.Dump(rzt) } ns.NSIPs = append(ns.NSIPs, ip) if result.State == stateOK { // spew.Dump(ns) // log.Println("fetching records for", lzone, "from", ns.NSIPs[0].IP) ns.Response, err = FetchARecords(lzone, ns.NSIPs[0].IP) if err != nil { ns.State = stateERR ns.Error = err gallOK = false failedNS = append(failedNS, rzt.name+"/"+ns.NSIPs[0].IP) // log.Println("failed:", err) } } } if len(rzt.ips) == 0 { ns.State = stateERR ns.Error = errors.New(rzt.globalErrMsg) failedNS = append(failedNS, rzt.name+"/no ip for dns server") gallOK = false } else { if allOK { ns.State = stateOK } else { if anyOK { ns.State = "WARN" // log.Println("failed NS") // spew.Dump(ns) if ns.Error == nil { ns.Error = errors.New("some servers failed") } } else { ns.State = stateERR if ns.Error == nil { ns.Error = errors.New("all servers failed") } } } } result.NSServers = append(result.NSServers, ns) } if gallOK { result.State = stateOK } else { if ganyOK { result.State = stateWARN result.Warnings = append(result.Warnings, "some servers failed: "+strings.Join(failedNS, ",")) } else { result.State = stateERR if result.Error == nil { result.Error = errors.New("all servers failed") } } } for _, ni := range result.NSServers { // log.Println(ni.Name) for _, r := range ni.Response { ip := r.Value.Inet if ip.Equal(localhost) { result.State = stateERR err := "resolves to localhost/127.0.0.1" if result.Error == nil { result.Error = errors.New(err) } else if result.Error.Error() != err { result.Warnings = append(result.Warnings, err) } } // log.Println(r.Name, r.Kind, r.Value) } } _, maxt := result.Times() if maxt > 2*time.Second { if result.State == stateOK { result.State = stateWARN } result.Warnings = append(result.Warnings, "slow") } result.Duration = time.Since(start) // spew.Dump(result) return result }