diff --git a/README.md b/README.md index 3c801c3..b1b5b01 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,12 @@ Similar to [dnsmasq](http://www.thekelleys.org.uk/dnsmasq/doc.html), but support 3. Running - $ sudo ./godns -c godns.conf + $ sudo ./godns -c ./etc/godns.conf 4. Test $ dig www.github.com @127.0.0.1 -More details about how to install and running godns can reference my [blog (Chinese)](http://blog.kenshinx.me/blog/compile-godns/) ## Use godns @@ -58,6 +57,12 @@ resolv-file = "/etc/resolv.conf" If multiple `namerservers` are set in resolv.conf, the upsteam server will try in a top to bottom order +#### server-list-file +Domain-specific nameservers configuration, formatting keep compatible with Dnsmasq. +>server=/google.com/8.8.8.8 + +More cases please refererence [dnsmasq-china-list](https://github.com/felixonmars/dnsmasq-china-list) + #### cache diff --git a/etc/apple.china.conf b/etc/apple.china.conf new file mode 100644 index 0000000..820bf08 --- /dev/null +++ b/etc/apple.china.conf @@ -0,0 +1,46 @@ +server=/adcdownload.apple.com/114.114.114.114 +server=/appldnld.apple.com/114.114.114.114 +server=/cdn-cn1.apple-mapkit.com/114.114.114.114 +server=/cdn-cn2.apple-mapkit.com/114.114.114.114 +server=/cdn-cn3.apple-mapkit.com/114.114.114.114 +server=/cdn-cn4.apple-mapkit.com/114.114.114.114 +server=/cdn.apple-mapkit.com/114.114.114.114 +server=/cdn1.apple-mapkit.com/114.114.114.114 +server=/cdn2.apple-mapkit.com/114.114.114.114 +server=/cdn3.apple-mapkit.com/114.114.114.114 +server=/cdn4.apple-mapkit.com/114.114.114.114 +server=/cds.apple.com/114.114.114.114 +server=/cl1.apple.com/114.114.114.114 +server=/cl2.apple.com.edgekey.net.globalredir.akadns.net/114.114.114.114 +server=/cl2.apple.com.edgekey.net/114.114.114.114 +server=/cl2.apple.com/114.114.114.114 +server=/cl3.apple.com/114.114.114.114 +server=/cl4.apple.com/114.114.114.114 +server=/cl5.apple.com/114.114.114.114 +server=/gsp11-cn.ls.apple.com/114.114.114.114 +server=/gsp12-cn.ls.apple.com/114.114.114.114 +server=/gsp13-cn.ls.apple.com/114.114.114.114 +server=/gsp4-cn.ls.apple.com.edgekey.net.globalredir.akadns.net/114.114.114.114 +server=/gsp4-cn.ls.apple.com.edgekey.net/114.114.114.114 +server=/gsp4-cn.ls.apple.com/114.114.114.114 +server=/gsp5-cn.ls.apple.com/114.114.114.114 +server=/gspe19-cn.ls-apple.com.akadns.net/114.114.114.114 +server=/gspe19-cn.ls.apple.com/114.114.114.114 +server=/gspe21.ls.apple.com/114.114.114.114 +server=/gspe21-ssl.ls.apple.com/114.114.114.114 +server=/gspe35-ssl.ls.apple.com/114.114.114.114 +server=/icloud.cdn-apple.com/114.114.114.114 +server=/images.apple.com/114.114.114.114 +server=/itunes-apple.com.akadns.net/114.114.114.114 +server=/itunes.apple.com/114.114.114.114 +server=/itunesconnect.apple.com/114.114.114.114 +server=/mesu.apple.com/114.114.114.114 +server=/mesu-china.apple.com.akadns.net/114.114.114.114 +server=/phobos-apple.com.akadns.net/114.114.114.114 +server=/phobos.apple.com/114.114.114.114 +server=/store.apple.com/114.114.114.114 +server=/store.storeimages.cdn-apple.com/114.114.114.114 +server=/support.apple.com/114.114.114.114 +server=/swcdn.apple.com/114.114.114.114 +server=/swdist.apple.com/114.114.114.114 +server=/www.apple.com/114.114.114.114 diff --git a/godns.conf b/etc/godns.conf similarity index 83% rename from godns.conf rename to etc/godns.conf index 9a5e1d4..a94be7a 100644 --- a/godns.conf +++ b/etc/godns.conf @@ -12,6 +12,9 @@ host = "127.0.0.1" port = 53 [resolv] +# Domain-specific nameservers configuration, formatting keep compatible with Dnsmasq +# Semicolon separate multiple files. +server-list-file = "./etc/apple.china.conf;./etc/google.china.conf" resolv-file = "/etc/resolv.conf" timeout = 5 # 5 seconds # The concurrency interval request upstream recursive server @@ -21,6 +24,7 @@ interval = 200 # 200 milliseconds setedns0 = false #Support for larger UDP DNS responses [redis] +enable = true host = "127.0.0.1" port = 6379 db = 0 diff --git a/etc/google.china.conf b/etc/google.china.conf new file mode 100644 index 0000000..0928c70 --- /dev/null +++ b/etc/google.china.conf @@ -0,0 +1,42 @@ +server=/265.com/114.114.114.114 +server=/2mdn.net/114.114.114.114 +server=/app-measurement.com/114.114.114.114 +server=/beacons.gcp.gvt2.com/114.114.114.114 +server=/beacons.gvt2.com/114.114.114.114 +server=/beacons3.gvt2.com/114.114.114.114 +server=/c.admob.com/114.114.114.114 +server=/c.android.clients.google.com/114.114.114.114 +server=/cache.pack.google.com/114.114.114.114 +server=/clientservices.googleapis.com/114.114.114.114 +server=/connectivitycheck.gstatic.com/114.114.114.114 +server=/csi.gstatic.com/114.114.114.114 +server=/dl.google.com/114.114.114.114 +server=/doubleclick.net/114.114.114.114 +server=/e.admob.com/114.114.114.114 +server=/fonts.googleapis.com/114.114.114.114 +server=/fonts.gstatic.com/114.114.114.114 +server=/google-analytics.com/114.114.114.114 +server=/googleadservices.com/114.114.114.114 +server=/googleanalytics.com/114.114.114.114 +server=/googlesyndication.com/114.114.114.114 +server=/googletagmanager.com/114.114.114.114 +server=/googletagservices.com/114.114.114.114 +server=/imasdk.googleapis.com/114.114.114.114 +server=/kh.google.com/114.114.114.114 +server=/khm.google.com/114.114.114.114 +server=/khm.googleapis.com/114.114.114.114 +server=/khm0.google.com/114.114.114.114 +server=/khm0.googleapis.com/114.114.114.114 +server=/khm1.google.com/114.114.114.114 +server=/khm1.googleapis.com/114.114.114.114 +server=/khm2.google.com/114.114.114.114 +server=/khm2.googleapis.com/114.114.114.114 +server=/khm3.google.com/114.114.114.114 +server=/khm3.googleapis.com/114.114.114.114 +server=/khmdb.google.com/114.114.114.114 +server=/khmdb.googleapis.com/114.114.114.114 +server=/media.admob.com/114.114.114.114 +server=/mediavisor.doubleclick.com/114.114.114.114 +server=/redirector.gvt1.com/114.114.114.114 +server=/toolbarqueries.google.com/114.114.114.114 +server=/update.googleapis.com/114.114.114.114 diff --git a/etc/server_list.example.conf b/etc/server_list.example.conf new file mode 100644 index 0000000..c0847fe --- /dev/null +++ b/etc/server_list.example.conf @@ -0,0 +1,7 @@ +# Default upstream servers which have higher priority than the nameserver +# that configuration in `/etc/resolv.conf` +server=8.8.8.8#53 + +server=/google.com/8.8.8.8 +server=/baidu.com/114.114.114.114 +# refer https://github.com/felixonmars/dnsmasq-china-list diff --git a/handler.go b/handler.go index b8a26f4..1de7ee1 100644 --- a/handler.go +++ b/handler.go @@ -32,21 +32,12 @@ type GODNSHandler struct { func NewHandler() *GODNSHandler { var ( - clientConfig *dns.ClientConfig cacheConfig CacheSettings resolver *Resolver cache, negCache Cache ) - resolvConfig := settings.ResolvConfig - clientConfig, err := dns.ClientConfigFromFile(resolvConfig.ResolvFile) - if err != nil { - logger.Warn(":%s is not a valid resolv.conf file\n", resolvConfig.ResolvFile) - logger.Error(err.Error()) - panic(err) - } - clientConfig.Timeout = resolvConfig.Timeout - resolver = &Resolver{clientConfig} + resolver = NewResolver(settings.ResolvConfig) cacheConfig = settings.Cache switch cacheConfig.Backend { diff --git a/hosts.go b/hosts.go index d094f8e..c234ef7 100644 --- a/hosts.go +++ b/hosts.go @@ -4,7 +4,6 @@ import ( "bufio" "net" "os" - "regexp" "strings" "sync" "time" @@ -218,7 +217,7 @@ func (f *FileHosts) Refresh() { } ip := sli[0] - if !f.isIP(ip) { + if !isIP(ip) { continue } @@ -240,15 +239,3 @@ func (f *FileHosts) Refresh() { func (f *FileHosts) clear() { f.hosts = make(map[string]string) } - -func (f *FileHosts) isDomain(domain string) bool { - if f.isIP(domain) { - return false - } - match, _ := regexp.MatchString(`^([a-zA-Z0-9\*]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$`, domain) - return match -} - -func (f *FileHosts) isIP(ip string) bool { - return (net.ParseIP(ip) != nil) -} diff --git a/resolver.go b/resolver.go index e20b9d8..1afe8b8 100644 --- a/resolver.go +++ b/resolver.go @@ -1,8 +1,11 @@ package main import ( + "bufio" "fmt" "net" + "os" + "strconv" "strings" "sync" "time" @@ -20,8 +23,106 @@ func (e ResolvError) Error() string { return errmsg } +type RResp struct { + msg *dns.Msg + nameserver string + rtt time.Duration +} + type Resolver struct { - config *dns.ClientConfig + servers []string + domain_server *suffixTreeNode + config *ResolvSettings +} + +func NewResolver(c ResolvSettings) *Resolver { + r := &Resolver{ + servers: []string{}, + domain_server: newSuffixTreeRoot(), + config: &c, + } + + if len(c.ServerListFile) > 0 { + r.ReadServerListFile(c.ServerListFile) + } + + if len(c.ResolvFile) > 0 { + clientConfig, err := dns.ClientConfigFromFile(c.ResolvFile) + if err != nil { + logger.Error(":%s is not a valid resolv.conf file\n", c.ResolvFile) + logger.Error("%s", err) + panic(err) + } + for _, server := range clientConfig.Servers { + nameserver := net.JoinHostPort(server, clientConfig.Port) + r.servers = append(r.servers, nameserver) + } + } + + return r +} + +func (r *Resolver) parseServerListFile(buf *os.File) { + scanner := bufio.NewScanner(buf) + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + + if !strings.HasPrefix(line, "server") { + continue + } + + sli := strings.Split(line, "=") + if len(sli) != 2 { + continue + } + + line = strings.TrimSpace(sli[1]) + + tokens := strings.Split(line, "/") + switch len(tokens) { + case 3: + domain := tokens[1] + ip := tokens[2] + + if !isDomain(domain) || !isIP(ip) { + continue + } + r.domain_server.sinsert(strings.Split(domain, "."), ip) + case 1: + srv_port := strings.Split(line, "#") + if len(srv_port) > 2 { + continue + } + + ip := "" + if ip = srv_port[0]; !isIP(ip) { + continue + } + + port := "53" + if len(srv_port) == 2 { + if _, err := strconv.Atoi(srv_port[1]); err != nil { + continue + } + port = srv_port[1] + } + r.servers = append(r.servers, net.JoinHostPort(ip, port)) + } + } + +} + +func (r *Resolver) ReadServerListFile(path string) { + files := strings.Split(path, ";") + for _, file := range files { + buf, err := os.Open(file) + if err != nil { + panic("Can't open " + file) + } + defer buf.Close() + r.parseServerListFile(buf) + } } // Lookup will ask each nameserver in top-to-bottom fashion, starting a new request @@ -40,7 +141,7 @@ func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error qname := req.Question[0].Name - res := make(chan *dns.Msg, 1) + res := make(chan *RResp, 1) var wg sync.WaitGroup L := func(nameserver string) { defer wg.Done() @@ -59,11 +160,10 @@ func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error if r.Rcode == dns.RcodeServerFailure { return } - } else { - logger.Debug("%s resolv on %s (%s) ttl: %d", UnFqdn(qname), nameserver, net, rtt) } + re := &RResp{r, nameserver, rtt} select { - case res <- r: + case res <- re: default: } } @@ -71,13 +171,15 @@ func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error ticker := time.NewTicker(time.Duration(settings.ResolvConfig.Interval) * time.Millisecond) defer ticker.Stop() // Start lookup on each nameserver top-down, in every second - for _, nameserver := range r.Nameservers() { + nameservers := r.Nameservers(qname) + for _, nameserver := range nameservers { wg.Add(1) go L(nameserver) // but exit early, if we have an answer select { - case r := <-res: - return r, nil + case re := <-res: + logger.Debug("%s resolv on %s rtt: %v", UnFqdn(qname), re.nameserver, re.rtt) + return re.msg, nil case <-ticker.C: continue } @@ -85,26 +187,35 @@ func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error // wait for all the namservers to finish wg.Wait() select { - case r := <-res: - return r, nil + case re := <-res: + logger.Debug("%s resolv on %s rtt: %v", UnFqdn(qname), re.nameserver, re.rtt) + return re.msg, nil default: - return nil, ResolvError{qname, net, r.Nameservers()} + return nil, ResolvError{qname, net, nameservers} } - } // Namservers return the array of nameservers, with port number appended. // '#' in the name is treated as port separator, as with dnsmasq. -func (r *Resolver) Nameservers() (ns []string) { - for _, server := range r.config.Servers { - if i := strings.IndexByte(server, '#'); i > 0 { - server = net.JoinHostPort(server[:i], server[i+1:]) - } else { - server = net.JoinHostPort(server, r.config.Port) - } - ns = append(ns, server) + +func (r *Resolver) Nameservers(qname string) []string { + queryKeys := strings.Split(qname, ".") + queryKeys = queryKeys[:len(queryKeys)-1] // ignore last '.' + + ns := []string{} + if v, found := r.domain_server.search(queryKeys); found { + logger.Debug("%s be found in domain server list, upstream: %v", qname, v) + server := v + nameserver := net.JoinHostPort(server, "53") + ns = append(ns, nameserver) + //Ensure query the specific upstream nameserver in async Lookup() function. + return ns } - return + + for _, nameserver := range r.servers { + ns = append(ns, nameserver) + } + return ns } func (r *Resolver) Timeout() time.Duration { diff --git a/settings.go b/settings.go index 2f8d22c..f2310a6 100644 --- a/settings.go +++ b/settings.go @@ -34,10 +34,11 @@ type Settings struct { } type ResolvSettings struct { - ResolvFile string `toml:"resolv-file"` - Timeout int - Interval int - SetEDNS0 bool + Timeout int + Interval int + SetEDNS0 bool + ServerListFile string `toml:"server-list-file"` + ResolvFile string `toml:"resolv-file"` } type DNSServerSettings struct { @@ -93,7 +94,7 @@ func init() { var configFile string - flag.StringVar(&configFile, "c", "godns.conf", "Look for godns toml-formatting config file in this directory") + flag.StringVar(&configFile, "c", "./etc/godns.conf", "Look for godns toml-formatting config file in this directory") flag.Parse() if _, err := toml.DecodeFile(configFile, &settings); err != nil { diff --git a/sfx_tree.go b/sfx_tree.go new file mode 100644 index 0000000..bfaca0e --- /dev/null +++ b/sfx_tree.go @@ -0,0 +1,65 @@ +package main + +type suffixTreeNode struct { + key string + value string + children map[string]*suffixTreeNode +} + +func newSuffixTreeRoot() *suffixTreeNode { + return newSuffixTree("", "") +} + +func newSuffixTree(key string, value string) *suffixTreeNode { + root := &suffixTreeNode{ + key: key, + value: value, + children: map[string]*suffixTreeNode{}, + } + return root +} + +func (node *suffixTreeNode) ensureSubTree(key string) { + if _, ok := node.children[key]; !ok { + node.children[key] = newSuffixTree(key, "") + } +} + +func (node *suffixTreeNode) insert(key string, value string) { + if c, ok := node.children[key]; ok { + c.value = value + } else { + node.children[key] = newSuffixTree(key, value) + } +} + +func (node *suffixTreeNode) sinsert(keys []string, value string) { + if len(keys) == 0 { + return + } + + key := keys[len(keys)-1] + if len(keys) > 1 { + node.ensureSubTree(key) + node.children[key].sinsert(keys[:len(keys)-1], value) + return + } + + node.insert(key, value) +} + +func (node *suffixTreeNode) search(keys []string) (string, bool) { + if len(keys) == 0 { + return "", false + } + + key := keys[len(keys)-1] + if n, ok := node.children[key]; ok { + if nextValue, found := n.search(keys[:len(keys)-1]); found { + return nextValue, found + } + return n.value, (n.value != "") + } + + return "", false +} diff --git a/sfx_tree_test.go b/sfx_tree_test.go new file mode 100644 index 0000000..b2f0ae1 --- /dev/null +++ b/sfx_tree_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "strings" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func Test_Suffix_Tree(t *testing.T) { + root := newSuffixTreeRoot() + + Convey("Google should not be found", t, func() { + root.insert("cn", "114.114.114.114") + root.sinsert([]string{"baidu", "cn"}, "166.111.8.28") + root.sinsert([]string{"sina", "cn"}, "114.114.114.114") + + v, found := root.search(strings.Split("google.com", ".")) + So(found, ShouldEqual, false) + + v, found = root.search(strings.Split("baidu.cn", ".")) + So(found, ShouldEqual, true) + So(v, ShouldEqual, "166.111.8.28") + }) + + Convey("Google should be found", t, func() { + root.sinsert(strings.Split("com", "."), "") + root.sinsert(strings.Split("google.com", "."), "8.8.8.8") + root.sinsert(strings.Split("twitter.com", "."), "8.8.8.8") + root.sinsert(strings.Split("scholar.google.com", "."), "208.67.222.222") + + v, found := root.search(strings.Split("google.com", ".")) + So(found, ShouldEqual, true) + So(v, ShouldEqual, "8.8.8.8") + + v, found = root.search(strings.Split("www.google.com", ".")) + So(found, ShouldEqual, true) + So(v, ShouldEqual, "8.8.8.8") + + v, found = root.search(strings.Split("scholar.google.com", ".")) + So(found, ShouldEqual, true) + So(v, ShouldEqual, "208.67.222.222") + + v, found = root.search(strings.Split("twitter.com", ".")) + So(found, ShouldEqual, true) + So(v, ShouldEqual, "8.8.8.8") + + v, found = root.search(strings.Split("baidu.cn", ".")) + So(found, ShouldEqual, true) + So(v, ShouldEqual, "166.111.8.28") + }) + +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..1993d59 --- /dev/null +++ b/utils.go @@ -0,0 +1,18 @@ +package main + +import ( + "net" + "regexp" +) + +func isDomain(domain string) bool { + if isIP(domain) { + return false + } + match, _ := regexp.MatchString(`^([a-zA-Z0-9\*]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$`, domain) + return match +} + +func isIP(ip string) bool { + return (net.ParseIP(ip) != nil) +} diff --git a/hosts_test.go b/utils_test.go similarity index 52% rename from hosts_test.go rename to utils_test.go index 2a795a8..715b333 100644 --- a/hosts_test.go +++ b/utils_test.go @@ -8,26 +8,24 @@ import ( func TestHostDomainAndIP(t *testing.T) { Convey("Test Host File Domain and IP regex", t, func() { - f := &FileHosts{} - Convey("1.1.1.1 should be IP and not domain", func() { - So(f.isIP("1.1.1.1"), ShouldEqual, true) - So(f.isDomain("1.1.1.1"), ShouldEqual, false) + So(isIP("1.1.1.1"), ShouldEqual, true) + So(isDomain("1.1.1.1"), ShouldEqual, false) }) Convey("2001:470:20::2 should be IP and not domain", func() { - So(f.isIP("2001:470:20::2"), ShouldEqual, true) - So(f.isDomain("2001:470:20::2"), ShouldEqual, false) + So(isIP("2001:470:20::2"), ShouldEqual, true) + So(isDomain("2001:470:20::2"), ShouldEqual, false) }) Convey("`host` should not be domain and not IP", func() { - So(f.isDomain("host"), ShouldEqual, false) - So(f.isIP("host"), ShouldEqual, false) + So(isDomain("host"), ShouldEqual, false) + So(isIP("host"), ShouldEqual, false) }) Convey("`123.test` should be domain and not IP", func() { - So(f.isDomain("123.test"), ShouldEqual, true) - So(f.isIP("123.test"), ShouldEqual, false) + So(isDomain("123.test"), ShouldEqual, true) + So(isIP("123.test"), ShouldEqual, false) }) })