commit bb35ac980a79cf5c803e64572c346d2f2f019e3f Author: Knut Ahlers Date: Mon Dec 14 17:13:21 2015 +0100 Initital version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a30f5dc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +dns_check diff --git a/.gobuilder.yml b/.gobuilder.yml new file mode 100644 index 0000000..4f00758 --- /dev/null +++ b/.gobuilder.yml @@ -0,0 +1,4 @@ +build_matrix: + general: + ldflags: + - "-X main.version $(git describe --tags)" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..513f76b --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +default: + +compile: + go generate + go build -ldflags "-X main.version=$(shell git describe --tags || git rev-parse --short HEAD)" . + +bindata: + go-bindata nameservers.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..03d534e --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Luzifer / dns\_check + +dns\_check is a small utility to check major DNS services for records of a FQDN without having to query them one-by-one. + +Use cases: + +- Check whether the IP of your domain is consistent on all services +- Check whether your DNS change is already live for users of those services +- Have an automated check which tells you when something is not right + +You can see the current table of nameservers oncluded on the build by browsing the [nameservers.yaml](nameservers.yaml) file. + +## Usage + +```bash +# ./dns_check --help +Usage of ./dns_check: + -a, --assert=[]: Exit with exit code 2 when these DNS entries were not found + --assert-threshold=100: If used with -a fail when not at least N percent of the nameservers had the expected result + -f, --full-scan[=false]: Scan all nameservers included in this build + -q, --quiet[=false]: Do not communicate by text, use only exit codes + -s, --short[=true]: Use short notation (only when using assertion) + --version[=false]: Print version and exit +``` + +Use case: I know the IP of my domain and want to check whether all services report that IP + +```bash +# ./dns_check -a "188.40.126.69" A luzifer.io +[Level3] (209.244.0.3:53) ✓ +[Level3] (209.244.0.4:53) ✓ +[Verisign] (64.6.64.6:53) ✓ +[Verisign] (64.6.65.6:53) ✓ +[Google] (8.8.8.8:53) ✓ +[Google] (8.8.4.4:53) ✓ +[OpenDNS Home] (208.67.222.222:53) ✓ +[OpenDNS Home] (208.67.220.220:53) ✓ +``` + +Use case: Just tell me the IP of any domain + +```bash +# ./dns_check A luzifer.io +[Google] (8.8.8.8:53) + - 188.40.126.69 +[Google] (8.8.4.4:53) + - 188.40.126.69 +[Level3] (209.244.0.3:53) + - 188.40.126.69 +[Level3] (209.244.0.4:53) + - 188.40.126.69 +[Verisign] (64.6.64.6:53) + - 188.40.126.69 +[Verisign] (64.6.65.6:53) + - 188.40.126.69 +[OpenDNS Home] (208.67.222.222:53) + - 188.40.126.69 +[OpenDNS Home] (208.67.220.220:53) + - 188.40.126.69 +``` diff --git a/bindata.go b/bindata.go new file mode 100644 index 0000000..7276397 --- /dev/null +++ b/bindata.go @@ -0,0 +1,235 @@ +// Code generated by go-bindata. +// sources: +// nameservers.yaml +// DO NOT EDIT! + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _nameserversYaml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x64\x53\x4b\x6f\x9b\x40\x10\xbe\xfb\x57\x20\xf5\xcc\xc0\x3e\x78\xe5\x66\xb9\x6d\x52\x29\x4a\x0f\xb6\x52\xf5\x14\x11\x18\x3b\x28\xb0\x8b\x96\xc5\x6a\xff\x7d\x67\xcd\x6b\xad\x2a\xb3\x04\x3e\xcf\xeb\xfb\x66\x36\x0c\xc3\x5d\xa5\x0d\xbe\xf5\x46\x5f\x9b\x1a\xcd\xf0\xb0\x0b\x82\x30\x78\xc6\x2b\xb6\xe2\xf6\xfa\x8a\xa6\x19\x9a\x8b\xba\x7d\x3c\x6a\x7d\x69\xf1\xf6\xfa\xb3\x47\xf5\xf5\xe5\x18\x3c\xe9\x0e\x77\xfd\xf8\xde\x36\xd5\x9b\x2a\x3b\x1c\xd0\x5c\xe7\x3c\x5f\x82\xe7\x66\xb0\x41\xa5\xbb\xbe\x69\xb1\x0e\xce\x46\x77\xc1\x87\xb5\xfd\x43\x14\xf5\xd5\x30\xf6\xbd\x36\x16\xca\x77\x3d\x5a\x20\xa7\x48\xd7\x91\x6d\xfa\xc1\x9a\xa6\xfa\x1c\xa2\x32\x3a\x1b\xc4\x70\x4a\x1d\xd6\x6a\x08\xe7\xd4\xf0\x61\x3b\xca\x3e\xf5\xe8\xea\xb8\x76\x78\x5c\x00\x97\x12\x62\x10\x0f\x89\xf8\x0f\x94\x13\xb8\x90\x59\xa2\x52\x09\x29\xb8\xc7\x16\x33\x41\xc9\x02\x4d\x8c\x17\xff\x1c\x6e\x7f\x9b\xb7\xfb\x94\x4b\x76\x92\x03\x7e\xed\x4f\x87\xa7\xd5\x5d\x02\x8f\x63\x48\x0b\xc8\x63\x2f\x66\x42\x33\xea\x6a\x46\x0f\xba\xd3\xb5\x0e\x8e\x58\x8d\x06\x5d\x9a\xad\x1e\x4f\x21\x49\xe9\xe9\x97\xe4\x31\xb1\xca\xe8\xdf\x04\xfa\x93\xd8\xe4\xc8\x21\x25\x17\xce\xdd\xf1\x15\x99\xf1\xd8\x9d\xb5\xed\x60\x5f\x5f\x4b\x65\xcb\xcb\x9a\x80\x51\x55\x96\x48\xd7\x25\xdb\xc2\x57\x94\x2d\xe8\x0b\x8d\x50\x2b\x62\xa0\x14\x56\xf6\x58\x9e\xb7\x0c\x05\xd1\x4e\x80\x11\x03\xe6\xb1\x5f\xe1\x6c\x85\x1f\x69\xce\xea\x84\x65\xe7\x33\x67\xc0\x59\x0e\x8c\x15\x74\xee\x27\x9a\x13\x5c\xd0\x11\xf3\xa4\x5d\x51\x2f\x92\x15\x09\xc8\x14\x44\x41\xe6\x97\x5d\x50\xe9\xc9\xf6\xf2\xe3\xb0\x84\x25\x44\x94\xa5\xf4\x2b\x09\xe3\xa9\x3d\xc3\x5c\x50\x3b\x73\x1f\xc7\xae\x34\xf6\xb5\xe9\xd1\xf8\x6a\x67\x34\xa8\x98\xec\x5e\x6b\x87\x32\xb2\x59\xe9\xbf\xeb\xe6\x71\x4a\xca\x5c\x43\x09\x99\x17\xb3\xc0\xce\x26\xf8\x3b\xa9\xe3\xd1\x13\x34\x3d\x0a\xa2\x01\x64\x72\x8b\xf3\xd0\x6c\x42\xf7\xad\x45\xa3\x4a\x7b\xb7\x4f\x37\xdd\x62\x92\x56\x72\xc8\xfc\xb5\x20\x7a\x89\x00\x96\x0a\x48\x66\x55\x7f\x97\xaa\xc6\x3f\xe0\x05\x67\x99\x93\xfe\x6e\xfd\x17\x68\xe6\x57\xa1\x1a\x46\x73\x36\x0d\xdd\x56\xa8\x3f\xd7\x59\xd2\x2d\x14\x02\xa4\xa0\xbd\xd9\x62\x0b\x6a\x83\xc6\xc1\xe8\x2a\xd0\x99\xf0\xa7\xd1\xd0\xe5\x2f\x15\x06\xdf\x5a\x5a\x27\x7a\x5f\x8b\x4b\xc8\x39\x48\x67\x93\x6b\x3f\x2a\x7b\xd8\x9f\x56\x66\xb4\x18\xee\x9e\xcd\x5a\xef\xfe\x05\x00\x00\xff\xff\xa7\x60\x1b\x39\xda\x04\x00\x00") + +func nameserversYamlBytes() ([]byte, error) { + return bindataRead( + _nameserversYaml, + "nameservers.yaml", + ) +} + +func nameserversYaml() (*asset, error) { + bytes, err := nameserversYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "nameservers.yaml", size: 1242, mode: os.FileMode(420), modTime: time.Unix(1450103270, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "nameservers.yaml": nameserversYaml, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "nameservers.yaml": &bintree{nameserversYaml, map[string]*bintree{}}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..9524cf9 --- /dev/null +++ b/dns.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "sort" + + "github.com/miekg/dns" +) + +func getDNSQueryResponse(queryType, fqdn, dnsServer string) ([]string, error) { + qt, ok := dns.StringToType[queryType] + if !ok { + return nil, fmt.Errorf("Query type '%s' is an unknown type.", queryType) + } + + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn(fqdn), qt) + + in, err := dns.Exchange(m, dnsServer) + if err != nil { + return nil, err + } + + responses := []string{} + + switch dns.RcodeToString[in.Rcode] { + // TODO: Catch more error codes (https://github.com/miekg/dns/blob/master/msg.go#L127) + case "NXDOMAIN": + return nil, fmt.Errorf("Domain was not found. (NXDOMAIN)") + } + + for _, a := range in.Answer { + r, err := formatDNSAnswer(a) + if err != nil { + return nil, err + } + for _, rp := range r { + responses = append(responses, rp) + } + } + + sort.Strings(responses) + + return responses, nil +} + +func formatDNSAnswer(a interface{}) (r []string, err error) { + switch a.(type) { + case *dns.A: + r = []string{a.(*dns.A).A.String()} + case *dns.AAAA: + r = []string{a.(*dns.AAAA).AAAA.String()} + case *dns.CNAME: + r = []string{a.(*dns.CNAME).Target} + case *dns.MX: + r = []string{fmt.Sprintf("%d %s", a.(*dns.MX).Preference, a.(*dns.MX).Mx)} + case *dns.NS: + r = []string{a.(*dns.NS).Ns} + case *dns.PTR: + r = []string{a.(*dns.PTR).Ptr} + case *dns.TXT: + r = a.(*dns.TXT).Txt + case *dns.SRV: + r = []string{fmt.Sprintf("%d %d %d %s", + a.(*dns.SRV).Priority, + a.(*dns.SRV).Weight, + a.(*dns.SRV).Port, + a.(*dns.SRV).Target, + )} + default: + err = fmt.Errorf("Got an unexpected answer type: %s", a.(dns.RR).String()) + } + + return +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..40890ae --- /dev/null +++ b/main.go @@ -0,0 +1,182 @@ +package main + +//go:generate make bindata + +import ( + "bytes" + "fmt" + "log" + "os" + "reflect" + "sort" + "time" + + "gopkg.in/yaml.v2" + + "github.com/Luzifer/rconfig" + "github.com/fatih/color" +) + +var ( + cfg = struct { + FullScan bool `flag:"full-scan,f" default:"false" description:"Scan all nameservers included in this build"` + Assert []string `flag:"assert,a" default:"" description:"Exit with exit code 2 when these DNS entries were not found"` + AssertPercentage float64 `flag:"assert-threshold" default:"100.0" description:"If used with -a fail when not at least N percent of the nameservers had the expected result"` + Quiet bool `flag:"quiet,q" default:"false" description:"Do not communicate by text, use only exit codes"` + Short bool `flag:"short,s" default:"true" description:"Use short notation (only when using assertion)"` + Version bool `flag:"version" default:"false" description:"Print version and exit"` + }{} + nameserverDirectory = struct { + CoreProviders []string `yaml:"core_providers"` + PublicNameservers map[string][]string `yaml:"public_nameservers"` + }{} + version = "dev" + + // Color output helpers + red = color.New(color.FgRed).SprintfFunc() + green = color.New(color.FgGreen).SprintFunc() + yellow = color.New(color.FgYellow).SprintFunc() + providerOut = color.New(color.FgWhite).Add(color.BgBlue).SprintfFunc() + serverOut = color.New(color.FgWhite).SprintfFunc() +) + +type checkResult struct { + Provider string + Server string + Results []string + QueryError error + AssertSucceeded bool +} + +func (c checkResult) Print() { + if c.QueryError != nil { + fmt.Printf("%s %s %s\n", + providerOut("[%s]", c.Provider), + serverOut("(%s)", c.Server), + red("Error: %s", c.QueryError), + ) + return + } + + var result string + if len(cfg.Assert) > 0 { + if c.AssertSucceeded { + result = green("\u2713") + } else { + result = red("\u2717") + } + } else { + result = "" + } + + srvBuf := bytes.NewBuffer([]byte{}) + fmt.Fprintf(srvBuf, "%s %s %s\n", + providerOut("[%s]", c.Provider), + serverOut("(%s)", c.Server), + result, + ) + if !cfg.Short { + for _, r := range c.Results { + fmt.Fprintf(srvBuf, " %s %s\n", yellow("-"), r) + } + } + fmt.Print(srvBuf.String()) +} + +func init() { + if err := rconfig.Parse(&cfg); err != nil { + log.Fatalf("Unable to parse arguments: %s", err) + } + + if cfg.Version { + fmt.Printf("dns_check version %s\n", version) + os.Exit(0) + } + + if reflect.DeepEqual(cfg.Assert, []string{""}) { + cfg.Short = false + cfg.Assert = []string{} + } + + if err := loadNameservers(); err != nil { + log.Fatalf("Unable to load nameserver list, probably your build is defect: %s", err) + } +} + +func main() { + args := rconfig.Args() + if len(args) != 3 { + fmt.Println("Usage: dns_check ") + os.Exit(1) + } + + queryType := args[1] + queryFQDN := args[2] + + // Correct ordering is required for DeepEqual + sort.Strings(cfg.Assert) + + wg := make(chan bool, 10) + results := []*checkResult{} + for provider, servers := range nameserverDirectory.PublicNameservers { + if !cfg.FullScan && !isCoreProvider(provider) { + continue + } + + for _, server := range servers { + wg <- true + r := &checkResult{ + Provider: provider, + Server: server, + } + results = append(results, r) + go checkProviderServer(wg, queryType, queryFQDN, provider, server, r) + } + } + for len(wg) > 0 { + time.Sleep(1) + } + + var failCount int + for _, r := range results { + if !r.AssertSucceeded { + failCount++ + } + if !cfg.Quiet { + r.Print() + } + } + + if (1.0-float64(failCount)/float64(len(results)))*100 < cfg.AssertPercentage { + os.Exit(2) + } +} + +func checkProviderServer(wg chan bool, queryType, queryFQDN, provider, server string, r *checkResult) { + r.Results, r.QueryError = getDNSQueryResponse(queryType, queryFQDN, server) + + if len(cfg.Assert) > 0 { + r.AssertSucceeded = reflect.DeepEqual(r.Results, cfg.Assert) + } else { + r.AssertSucceeded = true + } + + <-wg +} + +func isCoreProvider(s string) bool { + for _, v := range nameserverDirectory.CoreProviders { + if v == s { + return true + } + } + return false +} + +func loadNameservers() error { + data, err := Asset("nameservers.yaml") + if err != nil { + return err + } + return yaml.Unmarshal(data, &nameserverDirectory) +} diff --git a/nameservers.yaml b/nameservers.yaml new file mode 100644 index 0000000..1d7447d --- /dev/null +++ b/nameservers.yaml @@ -0,0 +1,64 @@ +--- +core_providers: + - Level3 + - Verisign + - Google + - OpenDNS Home +public_nameservers: + # List compiled from http://pcsupport.about.com/od/tipstricks/a/free-public-dns-servers.htm + Level3: + - 209.244.0.3:53 + - 209.244.0.4:53 + Verisign: + - 64.6.64.6:53 + - 64.6.65.6:53 + Google: + - 8.8.8.8:53 + - 8.8.4.4:53 + DNS.WATCH: + - 84.200.69.80:53 + - 84.200.70.40:53 + Comodo Secure DNS: + - 8.26.56.26:53 + - 8.20.247.20:53 + OpenDNS Home: + - 208.67.222.222:53 + - 208.67.220.220:53 + DNS Advantage: + - 156.154.70.1:53 + - 156.154.71.1:53 + Norton ConnectSafe: + - 199.85.126.10:53 + - 199.85.127.10:53 + GreenTeamDNS: + - 81.218.119.11:53 + - 209.88.198.133:53 + SafeDNS: + - 195.46.39.39:53 + - 195.46.39.40:53 + OpenNIC: + - 50.116.40.226:53 + - 50.116.23.211:53 + SmartViper: + - 208.76.50.50:53 + - 208.76.51.51:53 + Dyn: + - 216.146.35.35:53 + - 216.146.36.36:53 + FreeDNS: + - 37.235.1.174:53 + - 37.235.1.177:53 + Alternate DNS: + - 198.101.242.72:53 + - 23.253.163.53:53 + Yandex.DNS: + - 77.88.8.8:53 + - 77.88.8.1:53 + censurfridns.dk: + - 89.233.43.71:53 + - 91.239.100.100:53 + Hurricane Electric: + - 74.82.42.42:53 + puntCAT: + - 109.69.8.51:53 +