From 624a46fddcf41f180883224aa44a48b7d02f3fa1 Mon Sep 17 00:00:00 2001 From: bigeagle Date: Tue, 3 Feb 2015 22:53:57 +0800 Subject: [PATCH 1/5] dnsmasq style domain specific server supported --- godns.conf | 1 + handler.go | 17 +++------- hosts.go | 8 ++--- hosts_test.go | 18 +++++----- resolver.go | 87 +++++++++++++++++++++++++++++++++++++++++++----- settings.go | 8 +++-- sfx_tree.go | 65 ++++++++++++++++++++++++++++++++++++ sfx_tree_test.go | 49 +++++++++++++++++++++++++++ 8 files changed, 214 insertions(+), 39 deletions(-) create mode 100644 sfx_tree.go create mode 100644 sfx_tree_test.go diff --git a/godns.conf b/godns.conf index 7ce4b6e..5234a62 100644 --- a/godns.conf +++ b/godns.conf @@ -14,6 +14,7 @@ port = 53 [resolv] resolv-file = "/etc/resolv.conf" timeout = 5 # 5 seconds +domain-server-file = "/etc/godns.d/servers" [redis] host = "127.0.0.1" diff --git a/handler.go b/handler.go index 91467c3..ad6a268 100644 --- a/handler.go +++ b/handler.go @@ -33,21 +33,12 @@ type GODNSHandler struct { func NewHandler() *GODNSHandler { var ( - clientConfig *dns.ClientConfig - cacheConfig CacheSettings - resolver *Resolver - cache Cache + cacheConfig CacheSettings + resolver *Resolver + cache Cache ) - resolvConfig := settings.ResolvConfig - clientConfig, err := dns.ClientConfigFromFile(resolvConfig.ResolvFile) - if err != nil { - logger.Printf(":%s is not a valid resolv.conf file\n", resolvConfig.ResolvFile) - logger.Println(err) - 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 17d6445..7390e76 100644 --- a/hosts.go +++ b/hosts.go @@ -116,7 +116,7 @@ func (f *FileHosts) GetAll() map[string]string { domain := sli[len(sli)-1] ip := sli[0] - if !f.isDomain(domain) || !f.isIP(ip) { + if !isDomain(domain) || !isIP(ip) { continue } @@ -125,14 +125,14 @@ func (f *FileHosts) GetAll() map[string]string { return hosts } -func (f *FileHosts) isDomain(domain string) bool { - if f.isIP(domain) { +func isDomain(domain string) bool { + if isIP(domain) { return false } match, _ := regexp.MatchString("^[a-zA-Z0-9][a-zA-Z0-9-]", domain) return match } -func (f *FileHosts) isIP(ip string) bool { +func isIP(ip string) bool { return (net.ParseIP(ip) != nil) } diff --git a/hosts_test.go b/hosts_test.go index af70747..4499e8a 100644 --- a/hosts_test.go +++ b/hosts_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 be domain and not IP", func() { - So(f.isDomain("host"), ShouldEqual, true) - So(f.isIP("host"), ShouldEqual, false) + So(isDomain("host"), ShouldEqual, true) + 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) }) }) diff --git a/resolver.go b/resolver.go index 11756c1..438ed4c 100644 --- a/resolver.go +++ b/resolver.go @@ -1,10 +1,13 @@ package main import ( + "bufio" "fmt" - "github.com/miekg/dns" + "os" "strings" "time" + + "github.com/miekg/dns" ) type ResolvError struct { @@ -18,7 +21,62 @@ func (e ResolvError) Error() string { } type Resolver struct { - config *dns.ClientConfig + config *dns.ClientConfig + domain_server *suffixTreeNode +} + +func NewResolver(c ResolvSettings) *Resolver { + var clientConfig *dns.ClientConfig + clientConfig, err := dns.ClientConfigFromFile(c.ResolvFile) + if err != nil { + logger.Printf(":%s is not a valid resolv.conf file\n", c.ResolvFile) + logger.Println(err) + panic(err) + } + clientConfig.Timeout = c.Timeout + + domain_server := newSuffixTreeRoot() + r := &Resolver{clientConfig, domain_server} + + if len(c.DomainServerFile) > 0 { + r.ReadDomainServerFile(c.DomainServerFile) + } + return r +} + +func (r *Resolver) ReadDomainServerFile(file string) { + buf, err := os.Open(file) + if err != nil { + panic("Can't open " + 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, "/") + if len(tokens) != 3 { + continue + } + domain := tokens[1] + ip := tokens[2] + if !isDomain(domain) || !isIP(ip) { + continue + } + r.domain_server.sinsert(strings.Split(domain, "."), ip) + } + } func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error) { @@ -29,8 +87,8 @@ func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error } qname := req.Question[0].Name - - for _, nameserver := range r.Nameservers() { + nameservers := r.Nameservers(qname) + for _, nameserver := range nameservers { r, rtt, err := c.Exchange(req, nameserver) if err != nil { Debug("%s socket error on %s", qname, nameserver) @@ -41,19 +99,30 @@ func (r *Resolver) Lookup(net string, req *dns.Msg) (message *dns.Msg, err error Debug("%s failed to get an valid answer on %s", qname, nameserver) continue } - Debug("%s resolv on %s ttl: %d", UnFqdn(qname), nameserver, rtt) + Debug("%s resolv on %s rtt: %v", UnFqdn(qname), nameserver, rtt) return r, nil } - return nil, ResolvError{qname, r.Nameservers()} - + return nil, ResolvError{qname, nameservers} } -func (r *Resolver) Nameservers() (ns []string) { +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 { + Debug("found upstream: %v", v) + server := v + nameserver := server + ":53" + ns = append(ns, nameserver) + } + for _, server := range r.config.Servers { nameserver := server + ":" + r.config.Port ns = append(ns, nameserver) } - return + return ns } func (r *Resolver) Timeout() time.Duration { diff --git a/settings.go b/settings.go index c729b8d..0660532 100644 --- a/settings.go +++ b/settings.go @@ -3,9 +3,10 @@ package main import ( "flag" "fmt" - "github.com/BurntSushi/toml" "os" "strconv" + + "github.com/BurntSushi/toml" ) var ( @@ -24,8 +25,9 @@ type Settings struct { } type ResolvSettings struct { - ResolvFile string `toml:"resolv-file"` - Timeout int + ResolvFile string `toml:"resolv-file"` + DomainServerFile string `toml:"domain-server-file"` + Timeout int } type DNSServerSettings struct { 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..47471dd --- /dev/null +++ b/sfx_tree_test.go @@ -0,0 +1,49 @@ +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("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") + }) + +} From 952f39ddf25fae5ea302df9b8a101ceaaaf25e88 Mon Sep 17 00:00:00 2001 From: bigeagle Date: Tue, 3 Feb 2015 23:03:47 +0800 Subject: [PATCH 2/5] add option to disable redis --- godns.conf | 1 + hosts.go | 20 ++++++++++++++++++-- settings.go | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/godns.conf b/godns.conf index 5234a62..ec06255 100644 --- a/godns.conf +++ b/godns.conf @@ -17,6 +17,7 @@ timeout = 5 # 5 seconds domain-server-file = "/etc/godns.d/servers" [redis] +enable = true host = "127.0.0.1" port = 6379 db = 0 diff --git a/hosts.go b/hosts.go index 7390e76..2af165c 100644 --- a/hosts.go +++ b/hosts.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "errors" "net" "os" "regexp" @@ -17,8 +18,14 @@ type Hosts struct { func NewHosts(hs HostsSettings, rs RedisSettings) Hosts { fileHosts := &FileHosts{hs.HostsFile} - redis := &redis.Client{Addr: rs.Addr(), Db: rs.DB, Password: rs.Password} - redisHosts := &RedisHosts{redis, hs.RedisKey} + + var rc *redis.Client + if rs.Enable { + rc = &redis.Client{Addr: rs.Addr(), Db: rs.DB, Password: rs.Password} + } else { + rc = nil + } + redisHosts := &RedisHosts{rc, hs.RedisKey} hosts := Hosts{fileHosts.GetAll(), redisHosts} return hosts @@ -69,17 +76,26 @@ type RedisHosts struct { } func (r *RedisHosts) GetAll() map[string]string { + if r.redis == nil { + return map[string]string{} + } var hosts = make(map[string]string) r.redis.Hgetall(r.key, hosts) return hosts } func (r *RedisHosts) Get(domain string) (ip string, ok bool) { + if r.redis == nil { + return "", false + } b, err := r.redis.Hget(r.key, domain) return string(b), err == nil } func (r *RedisHosts) Set(domain, ip string) (bool, error) { + if r.redis == nil { + return false, errors.New("Redis not enabled") + } return r.redis.Hset(r.key, domain, []byte(ip)) } diff --git a/settings.go b/settings.go index 0660532..1481146 100644 --- a/settings.go +++ b/settings.go @@ -36,6 +36,7 @@ type DNSServerSettings struct { } type RedisSettings struct { + Enable bool Host string Port int DB int From 07283594890426adf8495a921909d513cd0fdc05 Mon Sep 17 00:00:00 2001 From: bigeagle Date: Tue, 3 Feb 2015 23:09:20 +0800 Subject: [PATCH 3/5] redis-enable is now in hosts --- hosts.go | 2 +- settings.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/hosts.go b/hosts.go index 2af165c..87ebb01 100644 --- a/hosts.go +++ b/hosts.go @@ -20,7 +20,7 @@ func NewHosts(hs HostsSettings, rs RedisSettings) Hosts { fileHosts := &FileHosts{hs.HostsFile} var rc *redis.Client - if rs.Enable { + if hs.RedisEnable { rc = &redis.Client{Addr: rs.Addr(), Db: rs.DB, Password: rs.Password} } else { rc = nil diff --git a/settings.go b/settings.go index 1481146..4c5b153 100644 --- a/settings.go +++ b/settings.go @@ -36,7 +36,6 @@ type DNSServerSettings struct { } type RedisSettings struct { - Enable bool Host string Port int DB int @@ -58,10 +57,11 @@ type CacheSettings struct { } type HostsSettings struct { - Enable bool - HostsFile string `toml:"host-file"` - RedisKey string `toml:"redis-key"` - TTL uint32 `toml:"ttl"` + Enable bool + HostsFile string `toml:"host-file"` + RedisEnable bool `toml:"redis-enable"` + RedisKey string `toml:"redis-key"` + TTL uint32 `toml:"ttl"` } func init() { From b3b393912280dec8412a2a934401f76c98376ec1 Mon Sep 17 00:00:00 2001 From: bigeagle Date: Tue, 3 Feb 2015 23:20:36 +0800 Subject: [PATCH 4/5] only cache successful lookups --- handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handler.go b/handler.go index ad6a268..82102c0 100644 --- a/handler.go +++ b/handler.go @@ -136,7 +136,7 @@ func (h *GODNSHandler) do(Net string, w dns.ResponseWriter, req *dns.Msg) { w.WriteMsg(mesg) - if IPQuery > 0 { + if IPQuery > 0 && len(mesg.Answer) > 0 { err = h.cache.Set(key, mesg) if err != nil { From 3df6ec09f30d9bcf9e1225d7922d228513ba904d Mon Sep 17 00:00:00 2001 From: bigeagle Date: Wed, 4 Feb 2015 16:21:08 +0800 Subject: [PATCH 5/5] Specify server in server-list-file --- examples/china.conf | 5 ++ godns.conf => examples/godns.conf | 2 +- resolver.go | 78 +++++++++++++++++++++---------- settings.go | 6 +-- 4 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 examples/china.conf rename godns.conf => examples/godns.conf (94%) diff --git a/examples/china.conf b/examples/china.conf new file mode 100644 index 0000000..e886276 --- /dev/null +++ b/examples/china.conf @@ -0,0 +1,5 @@ +server=8.8.8.8#53 +server=127.0.0.1#5553 + +server=/baidu.com/114.114.114.114 +# refer https://github.com/felixonmars/dnsmasq-china-list diff --git a/godns.conf b/examples/godns.conf similarity index 94% rename from godns.conf rename to examples/godns.conf index ec06255..ea6f535 100644 --- a/godns.conf +++ b/examples/godns.conf @@ -12,9 +12,9 @@ host = "127.0.0.1" port = 53 [resolv] +server-list-file = "/etc/godns.d/servers" resolv-file = "/etc/resolv.conf" timeout = 5 # 5 seconds -domain-server-file = "/etc/godns.d/servers" [redis] enable = true diff --git a/resolver.go b/resolver.go index 438ed4c..41a93da 100644 --- a/resolver.go +++ b/resolver.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "os" + "strconv" "strings" "time" @@ -21,30 +22,40 @@ func (e ResolvError) Error() string { } type Resolver struct { - config *dns.ClientConfig + servers []string domain_server *suffixTreeNode + config *ResolvSettings } func NewResolver(c ResolvSettings) *Resolver { - var clientConfig *dns.ClientConfig - clientConfig, err := dns.ClientConfigFromFile(c.ResolvFile) - if err != nil { - logger.Printf(":%s is not a valid resolv.conf file\n", c.ResolvFile) - logger.Println(err) - panic(err) + r := &Resolver{ + servers: []string{}, + domain_server: newSuffixTreeRoot(), + config: &c, } - clientConfig.Timeout = c.Timeout - domain_server := newSuffixTreeRoot() - r := &Resolver{clientConfig, domain_server} - - if len(c.DomainServerFile) > 0 { - r.ReadDomainServerFile(c.DomainServerFile) + if len(c.ServerListFile) > 0 { + r.ReadServerListFile(c.ServerListFile) + // Debug("%v", r.servers) } + + if len(c.ResolvFile) > 0 { + clientConfig, err := dns.ClientConfigFromFile(c.ResolvFile) + if err != nil { + logger.Printf(":%s is not a valid resolv.conf file\n", c.ResolvFile) + logger.Println(err) + panic(err) + } + for _, server := range clientConfig.Servers { + nameserver := server + ":" + clientConfig.Port + r.servers = append(r.servers, nameserver) + } + } + return r } -func (r *Resolver) ReadDomainServerFile(file string) { +func (r *Resolver) ReadServerListFile(file string) { buf, err := os.Open(file) if err != nil { panic("Can't open " + file) @@ -66,15 +77,35 @@ func (r *Resolver) ReadDomainServerFile(file string) { line = strings.TrimSpace(sli[1]) tokens := strings.Split(line, "/") - if len(tokens) != 3 { - continue + 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, ip+":"+port) + } - domain := tokens[1] - ip := tokens[2] - if !isDomain(domain) || !isIP(ip) { - continue - } - r.domain_server.sinsert(strings.Split(domain, "."), ip) } } @@ -118,8 +149,7 @@ func (r *Resolver) Nameservers(qname string) []string { ns = append(ns, nameserver) } - for _, server := range r.config.Servers { - nameserver := server + ":" + r.config.Port + for _, nameserver := range r.servers { ns = append(ns, nameserver) } return ns diff --git a/settings.go b/settings.go index 4c5b153..c4282ad 100644 --- a/settings.go +++ b/settings.go @@ -25,9 +25,9 @@ type Settings struct { } type ResolvSettings struct { - ResolvFile string `toml:"resolv-file"` - DomainServerFile string `toml:"domain-server-file"` - Timeout int + ServerListFile string `toml:"server-list-file"` + ResolvFile string `toml:"resolv-file"` + Timeout int } type DNSServerSettings struct {