From b412fb5853219fee7c0fcad7bab8a72d846d7bc5 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Fri, 12 Apr 2019 23:07:58 +0700 Subject: all: refactoring with latest update on dns package All the server core functionalities (caches and forwarding) now implemented inside "dns.Server". The main function of this package are for reading options from configuration file (or from command line options) and watching changes from system resolv.conf. There are also some major changes on configuration file. * "server.parent" option now use URI format instead of IP:PORT. This will allow parent name servers to be UDP, TCP, and/or DoH simultaneusly. * "server.doh.parent" and "server.parent.connection" are removed, redundant with new "server.parent" format. * "cache.threshold" is renamed to "cache.prune_threshold". --- CHANGELOG.adoc | 19 ++ caches.go | 121 ------------ caches_test.go | 180 ----------------- cacheslist.go | 162 --------------- cacheslist_test.go | 140 ------------- cacheworker.go | 167 ---------------- cacheworker_test.go | 228 --------------------- cmd/rescached/config.go | 121 +++--------- cmd/rescached/main.go | 15 +- cmd/rescached/rescached.cfg | 61 +++--- cmd/resolver/main.go | 2 +- cmd/resolverbench/main.go | 25 +-- doc/rescached.cfg.adoc | 96 +++++---- go.mod | 8 +- go.sum | 19 +- listrequest.go | 131 ------------ listrequest_test.go | 156 --------------- listresponse.go | 120 ----------- listresponse_test.go | 121 ------------ options.go | 97 +++++---- options_test.go | 54 ----- rescached.go | 473 ++++---------------------------------------- rescached_test.go | 473 -------------------------------------------- response.go | 91 --------- response_test.go | 144 -------------- 25 files changed, 216 insertions(+), 3008 deletions(-) delete mode 100644 caches.go delete mode 100644 caches_test.go delete mode 100644 cacheslist.go delete mode 100644 cacheslist_test.go delete mode 100644 cacheworker.go delete mode 100644 cacheworker_test.go delete mode 100644 listrequest.go delete mode 100644 listrequest_test.go delete mode 100644 listresponse.go delete mode 100644 listresponse_test.go delete mode 100644 options_test.go delete mode 100644 rescached_test.go delete mode 100644 response.go delete mode 100644 response_test.go diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 6084fe8..b7d724f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,3 +1,22 @@ += Rescached v3.0.0 (2019-05-xx) + +All the server core functionalities (caches and forwarding) now +implemented inside "dns.Server". The main function of this package are +for reading options from configuration file (or from command line options) +and watching changes from system resolv.conf. + +There are also some major changes on configuration file, + +* "server.parent" option now use URI format instead of IP:PORT. + This will allow parent name servers to be UDP, TCP, and/or DoH + simultaneusly. + +* "server.doh.parent" and "server.parent.connection" are removed, + redundant with new "server.parent" format. + +* "cache.threshold" is renamed to "cache.prune_threshold". + + = Rescached v2.1.2 (2019-03-22) == Bug Fix diff --git a/caches.go b/caches.go deleted file mode 100644 index 1269071..0000000 --- a/caches.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "sort" - "strings" - "sync" -) - -// -// caches represent a mapping between domain-name and cached responses. -// -type caches struct { - sync.Mutex - v map[string]*listResponse -} - -// -// newCaches create and initialize new caches. -// -func newCaches() *caches { - return &caches{ - v: make(map[string]*listResponse), - } -} - -// -// get cached response based on request name, type, and class -// -func (c *caches) get(qname string, qtype, qclass uint16) ( - lres *listResponse, res *response, -) { - c.Lock() - lres, ok := c.v[qname] - c.Unlock() - if !ok { - return - } - - res = lres.get(qtype, qclass) - - return -} - -// -// length return the number of keys in map. -// -func (c *caches) length() (n int) { - c.Lock() - n = len(c.v) - c.Unlock() - return -} - -// -// add response to caches. -// -func (c *caches) add(key string, res *response) { - lres := newListResponse(res) - c.Lock() - c.v[key] = lres - c.Unlock() -} - -// -// remove cache by name, type, and class; and return the cached response. -// If no record found it will return nil. -// -func (c *caches) remove(qname string, qtype, qclass uint16) *response { - c.Lock() - lres, ok := c.v[qname] - c.Unlock() - if !ok { - return nil - } - - res := lres.remove(qtype, qclass) - if lres.v.Len() == 0 { - c.Lock() - delete(c.v, qname) - c.Unlock() - } - - return res -} - -// -// String return the string interpretation of content of caches ordered in -// ascending order by keys. -// -func (c *caches) String() string { - c.Lock() - var out strings.Builder - - keys := make([]string, 0, len(c.v)) - for key := range c.v { - keys = append(keys, key) - } - - sort.Strings(keys) - - out.WriteString("caches[") - for x, k := range keys { - val, ok := c.v[k] - if ok { - if x > 0 { - out.WriteByte(' ') - } - out.WriteString(k) - out.WriteByte(':') - out.WriteString(val.String()) - } - } - out.WriteByte(']') - c.Unlock() - - return out.String() -} diff --git a/caches_test.go b/caches_test.go deleted file mode 100644 index b243aa5..0000000 --- a/caches_test.go +++ /dev/null @@ -1,180 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "testing" - - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -var _testCaches = newCaches() // nolint - -func TestCachesAdd(t *testing.T) { - t.Logf("_testResponses[0]: %+v\n", _testResponses[0]) - - cases := []struct { - desc string - msg *dns.Message - expLen uint64 - }{{ - desc: "New", - msg: &dns.Message{ - Packet: []byte{1}, - Header: &dns.SectionHeader{ - ANCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("1"), - Type: 1, - Class: 1, - }, - Answer: []*dns.ResourceRecord{{ - TTL: 1, - }}, - }, - - expLen: 1, - }, { - desc: "New", - msg: &dns.Message{ - Packet: []byte{2}, - Header: &dns.SectionHeader{ - ANCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("2"), - Type: 2, - Class: 1, - }, - Answer: []*dns.ResourceRecord{{ - TTL: 1, - }}, - }, - expLen: 2, - }, { - desc: "Replace", - msg: &dns.Message{ - Packet: []byte{1, 1}, - Header: &dns.SectionHeader{ - ANCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("1"), - Type: 1, - Class: 1, - }, - Answer: []*dns.ResourceRecord{{ - TTL: 1, - }}, - }, - expLen: 2, - }} - - for _, c := range cases { - t.Log(c.desc) - - key := string(c.msg.Question.Name) - res := newResponse(c.msg) - res.accessedAt = 0 - - _testCaches.add(key, res) - } -} - -func TestCachesGet(t *testing.T) { - cases := []struct { - desc string - qname string - qtype uint16 - qclass uint16 - exp *response - }{{ - desc: "Cache hit", - qname: "1", - qtype: 1, - qclass: 1, - exp: _testResponses[2], - }, { - desc: "Cache miss on qname", - qname: "3", - qtype: 1, - qclass: 1, - exp: nil, - }, { - desc: "Cache miss on qtype", - qname: "1", - qtype: 0, - qclass: 1, - exp: nil, - }, { - desc: "Cache miss on qclass", - qname: "1", - qtype: 1, - qclass: 0, - exp: nil, - }} - - for _, c := range cases { - t.Log(c.desc) - - _, got := _testCaches.get(c.qname, c.qtype, c.qclass) - if got == nil { - test.Assert(t, "caches.get", c.exp, got, true) - continue - } - - test.Assert(t, "caches.get", c.exp.message, got.message, true) - } -} - -func TestCachesRemove(t *testing.T) { - cases := []struct { - desc string - qname string - qtype uint16 - qclass uint16 - exp *response - }{{ - desc: "With qname not exist", - qname: "3", - qtype: 1, - qclass: 1, - }, { - desc: "With qtype not exist", - qname: "1", - qtype: 0, - qclass: 1, - }, { - desc: "With qclass not exist", - qname: "1", - qtype: 1, - qclass: 0, - }, { - desc: "With record exist", - qname: "1", - qtype: 1, - qclass: 1, - exp: _testResponses[2], - }, { - desc: "With record exist, again", - qname: "1", - qtype: 1, - qclass: 1, - }} - - for _, c := range cases { - t.Log(c.desc) - - got := _testCaches.remove(c.qname, c.qtype, c.qclass) - if got == nil { - test.Assert(t, "caches.remove", c.exp, got, true) - continue - } - - test.Assert(t, "caches.remove", c.exp.message, got.message, true) - } -} diff --git a/cacheslist.go b/cacheslist.go deleted file mode 100644 index 5fee0da..0000000 --- a/cacheslist.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "container/list" - "fmt" - "strings" - "sync" - "sync/atomic" - "time" -) - -// -// cachesList maintain cache of response using list.List where it will be -// pruned based on threshold (time where item is last accessed). -// The list is ordered in FIFO based on last accessed time, where the old item -// reside at the top of list and the new item at the end of list. -// -type cachesList struct { - threshold time.Duration - sync.Mutex - v *list.List -} - -// -// newCachesList create and initialize new cachesList. -// The threshold value MUST be negative duration or it will be converted to -// negative based on current value. -// -func newCachesList(threshold time.Duration) *cachesList { - if threshold > 0 { - threshold *= -1 - } - return &cachesList{ - threshold: threshold, - v: list.New(), - } -} - -// -// String return string interpretation of cachesList. -// -func (cl *cachesList) String() string { - var out strings.Builder - - out.WriteString("cachesList[") - x := 0 - cl.Lock() - for el := cl.v.Front(); el != nil; el = el.Next() { - if x == 0 { - x++ - } else { - out.WriteByte(' ') - } - - res := el.Value.(*response) - fmt.Fprintf(&out, "&{%d %d %s}", res.receivedAt, - res.accessedAt, res.message.Question) - } - cl.Unlock() - out.WriteByte(']') - - return out.String() -} - -// -// items return content of list as slice of response. -// -func (cl *cachesList) items() (items []*response) { - el := cl.v.Front() - - for el != nil { - res := el.Value.(*response) - - items = append(items, res) - - el = el.Next() - } - - return -} - -// -// length return the number of item in list. -// -func (cl *cachesList) length() (n int) { - cl.Lock() - n = cl.v.Len() - cl.Unlock() - return n -} - -// -// push the new response to the end of list. -// This function assume that the response.accessedAt time is using current -// timestamp (greater or equal with last item in list). -// -func (cl *cachesList) push(res *response) { - if res == nil { - return - } - cl.Lock() - res.el = cl.v.PushBack(res) - cl.Unlock() -} - -// -// fix update the accessedAt value to current timestamp and move or push the -// response to the end of list. -// -func (cl *cachesList) fix(res *response) { - if res == nil { - return - } - - cl.Lock() - - atomic.StoreInt64(&res.accessedAt, time.Now().Unix()) - if res.el != nil { - cl.v.MoveToBack(res.el) - } else { - res.el = cl.v.PushBack(res) - } - - cl.Unlock() -} - -// -// prune remove response in list that have accessed time less than current -// time + -threshold. -// -func (cl *cachesList) prune() (lres []*response) { - cl.Lock() - - var next *list.Element - el := cl.v.Front() - exp := time.Now().Add(cl.threshold).Unix() - - fmt.Println("= prune threshold:", exp) - - for el != nil { - res := el.Value.(*response) - if res.AccessedAt() > exp { - break - } - - next = el.Next() - - cl.v.Remove(el) - res.el = nil - lres = append(lres, res) - - el = next - } - - cl.Unlock() - - return -} diff --git a/cacheslist_test.go b/cacheslist_test.go deleted file mode 100644 index c59a0c0..0000000 --- a/cacheslist_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "testing" - "time" - - "github.com/shuLhan/share/lib/test" -) - -func TestCachesListPush(t *testing.T) { - testCachesList := newCachesList(0) - - cases := []struct { - desc string - res *response - expLen int - exp []*response - }{{ - desc: "With empty response", - }, { - desc: "With valid response", - res: &response{ - accessedAt: 0, - }, - expLen: 1, - exp: []*response{ - { - accessedAt: 0, - }, - }, - }} - - for _, c := range cases { - t.Log(c.desc) - - testCachesList.push(c.res) - - test.Assert(t, "length", c.expLen, testCachesList.length(), - true) - - got := testCachesList.items() - for x, exp := range c.exp { - test.Assert(t, "response.receivedAt", exp.receivedAt, - got[x].receivedAt, true) - test.Assert(t, "response.accessedAt", exp.accessedAt, - got[x].accessedAt, true) - test.Assert(t, "response.el", got[x].el, nil, false) - } - } -} - -func TestCachesListFix(t *testing.T) { - timeNow := time.Now().Unix() - testCachesList := newCachesList(0) - testResponses := []*response{{ - receivedAt: 0, - }, { - receivedAt: 1, - }} - - cases := []struct { - desc string - res *response - expLen int - exp []*response - }{{ - desc: "With empty response", - }, { - desc: "With response.receivedAt is 1", - res: testResponses[1], - expLen: 1, - exp: []*response{ - testResponses[1], - }, - }, { - desc: "With response.receivedAt is 0", - res: testResponses[0], - expLen: 2, - exp: []*response{ - testResponses[1], - testResponses[0], - }, - }, { - desc: "With response.receivedAt is 1", - res: testResponses[1], - expLen: 2, - exp: []*response{ - testResponses[0], - testResponses[1], - }, - }} - - for _, c := range cases { - t.Log(c.desc) - - testCachesList.fix(c.res) - - test.Assert(t, "length", c.expLen, testCachesList.length(), - true) - - got := testCachesList.items() - for x, exp := range c.exp { - test.Assert(t, "response.receivedAt", exp.receivedAt, - got[x].receivedAt, true) - test.Assert(t, "response.accessedAt", - timeNow <= got[x].accessedAt, true, true) - test.Assert(t, "response.el", got[x].el, nil, false) - } - } -} - -func TestCachesListPrune(t *testing.T) { - timeNow := time.Now().Unix() - testCachesList := newCachesList(time.Minute) - testResponses := []*response{{ - receivedAt: 0, - accessedAt: timeNow - 60, - }, { - receivedAt: 1, - accessedAt: timeNow - 59, - }} - - for _, res := range testResponses { - testCachesList.push(res) - } - - got := testCachesList.prune() - - test.Assert(t, "length", 1, testCachesList.length(), true) - - for _, res := range got { - test.Assert(t, "response.receivedAt", int64(0), res.receivedAt, true) - test.Assert(t, "response.accessedAt", timeNow-60, res.accessedAt, true) - test.Assert(t, "response.el", nil, res.el, false) - } -} diff --git a/cacheworker.go b/cacheworker.go deleted file mode 100644 index 2bcfa78..0000000 --- a/cacheworker.go +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "fmt" - "log" - "time" - - libbytes "github.com/shuLhan/share/lib/bytes" - "github.com/shuLhan/share/lib/debug" - "github.com/shuLhan/share/lib/dns" -) - -const ( - maxWorkerQueue = 32 -) - -// -// cacheWorker is a worker that manage cache in map and list. -// Any addition, update, or remove to cache go through this worker. -// -type cacheWorker struct { - upsertQueue chan *dns.Message - caches *caches - cachesList *cachesList - pruneDelay time.Duration -} - -// -// newCacheWorker create and initialize worker with a timer to prune the cache -// (cacheDelay) and a duration for cache to be considered to be pruned. -// -func newCacheWorker(pruneDelay, cacheThreshold time.Duration) *cacheWorker { - return &cacheWorker{ - upsertQueue: make(chan *dns.Message, maxWorkerQueue), - caches: newCaches(), - cachesList: newCachesList(cacheThreshold), - pruneDelay: pruneDelay, - } -} - -func (cw *cacheWorker) start() { - go cw.pruneWorker() - - for msg := range cw.upsertQueue { - _ = cw.upsert(msg, false) - } -} - -func (cw *cacheWorker) pruneWorker() { - ticker := time.NewTicker(cw.pruneDelay) - - defer ticker.Stop() - - for t := range ticker.C { - fmt.Printf("= pruning at %v\n", t) - - cw.prune() - } -} - -// -// upsert update or insert a DNS message to caches in map and in the list. -// It will return true if response is added or updated in cache, otherwise it -// will return false. -// -func (cw *cacheWorker) upsert(msg *dns.Message, isLocal bool) bool { - if msg == nil { - return false - } - if msg.Header.RCode != dns.RCodeOK { - log.Printf("! Response error: %d %s\n", msg.Header.RCode, - msg.Question) - return false - } - - libbytes.ToLower(&msg.Question.Name) - qname := string(msg.Question.Name) - - lres, res := cw.caches.get(qname, msg.Question.Type, msg.Question.Class) - if lres == nil { - cw.push(qname, msg, isLocal) - return true - } - // Cache list contains other type. - if res == nil { - res = lres.add(msg, isLocal) - if !isLocal { - cw.cachesList.push(res) - } - return true - } - - _ = lres.update(res, msg) - - if !isLocal { - cw.cachesList.fix(res) - - if debug.Value >= 1 { - fmt.Printf("+ update : Total:%-4d ID:%-5d %s\n", - cw.cachesList.length(), res.message.Header.ID, - res.message.Question) - } - } - - return true -} - -// -// push new DNS message with domain-name "qname" as a key on map. -// If isLocal is false, the message will also pushed to cachesList. -// -func (cw *cacheWorker) push(qname string, msg *dns.Message, isLocal bool) { - res := newResponse(msg) - if isLocal { - res.receivedAt = 0 - } - - cw.caches.add(qname, res) - - if !isLocal { - cw.cachesList.push(res) - - if debug.Value >= 1 { - fmt.Printf("+ insert : Total:%-4d ID:%-5d %s\n", - cw.cachesList.length(), res.message.Header.ID, - res.message.Question) - } - } -} - -func (cw *cacheWorker) remove(res *response) { - if res == nil || res.message == nil { - return - } - if res.el != nil { - return - } - - qname := string(res.message.Question.Name) - - cw.caches.remove(qname, res.message.Question.Type, - res.message.Question.Class) - - if debug.Value > 0 { - fmt.Printf("= pruning: %4d %10d %s\n", cw.cachesList.length(), - res.accessedAt, res.message.Question) - } - - res.el = nil - res.message = nil -} - -func (cw *cacheWorker) prune() { - lres := cw.cachesList.prune() - if len(lres) == 0 { - return - } - - for _, res := range lres { - cw.remove(res) - } - fmt.Printf("= pruning %d records\n", len(lres)) -} diff --git a/cacheworker_test.go b/cacheworker_test.go deleted file mode 100644 index 31ef2f7..0000000 --- a/cacheworker_test.go +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "container/list" - "regexp" - "testing" - "time" - - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -var testCacheWorker = newCacheWorker(10*time.Second, -10*time.Second) // nolint: gochecknoglobals - -func assertCaches(t *testing.T, exp string) { - got := testCacheWorker.caches.String() - re, err := regexp.Compile(exp) - if err != nil { - t.Fatal(err) - } - if !re.MatchString(got) { - t.Fatalf("Expecting caches:\n\t%s\n got:\n%s\n", exp, got) - } -} - -func assertCachesList(t *testing.T, exp string) { - re, err := regexp.Compile(exp) - if err != nil { - t.Fatal(err) - } - - got := testCacheWorker.cachesList.String() - if !re.MatchString(got) { - t.Fatalf("Expecting cachesList:\n%s\n got:\n%s\n", exp, got) - } -} - -func TestCacheWorkerUpsert(t *testing.T) { - cases := []struct { - desc string - msg *dns.Message - isLocal bool - expReturn bool - expCaches string - expCachesList string - }{{ - desc: "With empty message", - expCaches: `^caches\[\]$`, - expCachesList: `^cachesList\[\]$`, - }, { - desc: "With response code is not OK", - msg: &dns.Message{ - Header: &dns.SectionHeader{ - RCode: dns.RCodeErrFormat, - }, - }, - expCaches: `^caches\[\]$`, - expCachesList: `^cachesList\[\]$`, - }, { - desc: "With new message, local - A", - msg: &dns.Message{ - Header: &dns.SectionHeader{ - RCode: dns.RCodeOK, - QDCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("local"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - isLocal: true, - expReturn: true, - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\]\]$`, - expCachesList: `^cachesList\[\]$`, - }, { - desc: "With new message, test1 - A", - msg: &dns.Message{ - Header: &dns.SectionHeader{ - RCode: dns.RCodeOK, - QDCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("test1"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - expReturn: true, - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:A}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d+ \d+ &{Name:test1 Type:A}}\]$`, - }, { - desc: "With new message, different type, test1 - NS", - msg: &dns.Message{ - Header: &dns.SectionHeader{ - RCode: dns.RCodeOK, - QDCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("test1"), - Type: dns.QueryTypeNS, - Class: dns.QueryClassIN, - }, - }, - expReturn: true, - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:A}} {\d+ \d+ &{Name:test1 Type:NS}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d+ \d+ &{Name:test1 Type:A}} &{\d+ \d+ &{Name:test1 Type:NS}}\]$`, // nolint: lll - }, { - desc: "With updated message, test1 - A", - msg: &dns.Message{ - Header: &dns.SectionHeader{ - RCode: dns.RCodeOK, - QDCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("test1"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - expReturn: true, - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:A}} {\d+ \d+ &{Name:test1 Type:NS}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d+ \d+ &{Name:test1 Type:NS}} &{\d+ \d+ &{Name:test1 Type:A}}\]$`, // nolint: lll - }, { - desc: "With new message, test2 - A", - msg: &dns.Message{ - Header: &dns.SectionHeader{ - RCode: dns.RCodeOK, - QDCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("test2"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - expReturn: true, - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:A}} {\d+ \d+ &{Name:test1 Type:NS}}\] test2:\[{\d+ \d+ &{Name:test2 Type:A}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d+ \d+ &{Name:test1 Type:NS}} &{\d+ \d+ &{Name:test1 Type:A}} &{\d+ \d+ &{Name:test2 Type:A}}\]$`, // nolint: lll - }} - - for _, c := range cases { - t.Log(c.desc) - - gotReturn := testCacheWorker.upsert(c.msg, c.isLocal) - - test.Assert(t, "return value", c.expReturn, gotReturn, true) - - assertCaches(t, c.expCaches) - assertCachesList(t, c.expCachesList) - } - - first := testCacheWorker.cachesList.v.Front() - if first != nil { - res := first.Value.(*response) - testCacheWorker.cachesList.fix(res) - } -} - -func TestCacheWorkerRemove(t *testing.T) { - cases := []struct { - desc string - el *list.Element - expCaches string - expCachesList string - }{{ - desc: "With nil response", - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:A}} {\d+ \d+ &{Name:test1 Type:NS}}\] test2:\[{\d+ \d+ &{Name:test2 Type:A}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d{10} \d{10} &{Name:test1 Type:A}} &{\d{10} \d{10} &{Name:test2 Type:A}} &{\d{10} \d{10} &{Name:test1 Type:NS}}\]$`, // nolint: lll - }, { - desc: "Removing one element", - el: testCacheWorker.cachesList.v.Front(), - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:NS}}\] test2:\[{\d+ \d+ &{Name:test2 Type:A}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d{10} \d{10} &{Name:test2 Type:A}} &{\d{10} \d{10} &{Name:test1 Type:NS}}\]$`, // nolint: lll - }} - - for _, c := range cases { - var res *response - - t.Log(c.desc) - - if c.el != nil { - v := testCacheWorker.cachesList.v.Remove(c.el) - res = v.(*response) - res.el = nil - } - - testCacheWorker.remove(res) - - assertCaches(t, c.expCaches) - assertCachesList(t, c.expCachesList) - } -} - -func TestCacheWorkerPrune(t *testing.T) { - cases := []struct { - desc string - res *response - expCaches string - expCachesList string - }{{ - desc: "With no items pruned", - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:NS}}\] test2:\[{\d+ \d+ &{Name:test2 Type:A}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d{10} \d{10} &{Name:test2 Type:A}} &{\d{10} \d{10} &{Name:test1 Type:NS}}\]$`, // nolint: lll - }, { - desc: "Pruning one element", - res: testCacheWorker.cachesList.v.Front().Value.(*response), - expCaches: `^caches\[local:\[{\d+ \d+ &{Name:local Type:A}}\] test1:\[{\d+ \d+ &{Name:test1 Type:NS}}\]\]$`, // nolint: lll - expCachesList: `^cachesList\[&{\d+ \d+ &{Name:test1 Type:NS}}\]$`, // nolint: lll - }} - - for _, c := range cases { - t.Log(c.desc) - - if c.res != nil { - c.res.accessedAt = time.Now().Add(-20 * time.Second).Unix() - } - - testCacheWorker.prune() - - assertCaches(t, c.expCaches) - assertCachesList(t, c.expCachesList) - } -} diff --git a/cmd/rescached/config.go b/cmd/rescached/config.go index f3a9c89..dc31cb7 100644 --- a/cmd/rescached/config.go +++ b/cmd/rescached/config.go @@ -5,8 +5,9 @@ package main import ( + "crypto/tls" + "fmt" "log" - "net/url" "strconv" "strings" "time" @@ -32,117 +33,55 @@ func parseConfig(file string) (opts *rescached.Options, err error) { opts = rescached.NewOptions() - opts.FilePID = cfg.GetString(cfgSecRescached, "", "file.pid", - "rescached.pid") - opts.FileResolvConf = cfg.GetString(cfgSecRescached, "", - "file.resolvconf", "") - opts.DoHCert = cfg.GetString(cfgSecRescached, "", - "server.doh.certificate", "") - opts.DoHCertKey = cfg.GetString(cfgSecRescached, "", - "server.doh.certificate.key", "") + opts.FilePID, _ = cfg.Get(cfgSecRescached, "", "file.pid", "rescached.pid") + opts.FileResolvConf, _ = cfg.Get(cfgSecRescached, "", "file.resolvconf", "") + + dohCertFile, _ := cfg.Get(cfgSecRescached, "", "server.doh.certificate", "") + dohPrivateKey, _ := cfg.Get(cfgSecRescached, "", "server.doh.certificate.key", "") opts.DoHAllowInsecure = cfg.GetBool(cfgSecRescached, "", "server.doh.allow_insecure", false) - parseParentConnection(cfg, opts) - err = parseNSParent(cfg, opts) if err != nil { return nil, err } - err = parseDoHParent(cfg, opts) - if err != nil { - return nil, err - } - parseListen(cfg, opts) - opts.DirHosts = cfg.GetString(cfgSecRescached, "", "dir.hosts", "") - opts.DirMaster = cfg.GetString(cfgSecRescached, "", "dir.master", "") + opts.DirHosts, _ = cfg.Get(cfgSecRescached, "", "dir.hosts", "") + opts.DirMaster, _ = cfg.Get(cfgSecRescached, "", "dir.master", "") parseDoHPort(cfg, opts) parseTimeout(cfg, opts) parseCachePruneDelay(cfg, opts) parseCacheThreshold(cfg, opts) parseDebugLevel(cfg) - return opts, nil -} - -func parseNSParent(cfg *ini.Ini, opts *rescached.Options) (err error) { - v, ok := cfg.Get(cfgSecRescached, "", "server.parent") - if !ok { - return - } - - nsParents := strings.Split(v, ",") - - for _, ns := range nsParents { - ns := strings.TrimSpace(ns) - - addr, err := libnet.ParseUDPAddr(ns, dns.DefaultPort) + if len(dohCertFile) > 0 && len(dohPrivateKey) > 0 { + cert, err := tls.LoadX509KeyPair(dohCertFile, dohPrivateKey) if err != nil { - return err + return nil, fmt.Errorf("rescached: error loading certificate: " + err.Error()) } - - opts.NSParents = append(opts.NSParents, addr) + opts.DoHCertificate = &cert } - return nil + return opts, nil } -func parseDoHParent(cfg *ini.Ini, opts *rescached.Options) (err error) { - v, ok := cfg.Get(cfgSecRescached, "", "server.doh.parent") - if !ok { - return - } - - dohParents := strings.Split(v, ",") - - for _, ns := range dohParents { - ns := strings.TrimSpace(ns) - - if !strings.HasPrefix(ns, "https://") { - continue - } - _, err = url.Parse(ns) - if err != nil { - return err - } +func parseNSParent(cfg *ini.Ini, opts *rescached.Options) (err error) { + parents := cfg.Gets(cfgSecRescached, "", "server.parent") - found := false - for _, known := range opts.DoHParents { - if ns == known { - found = true - break - } - } - if !found { - opts.DoHParents = append(opts.DoHParents, ns) + for _, ns := range parents { + ns = strings.TrimSpace(ns) + if len(ns) > 0 { + opts.NameServers = append(opts.NameServers, ns) } } return nil } -func parseParentConnection(cfg *ini.Ini, opts *rescached.Options) { - network := cfg.GetString(cfgSecRescached, "", - "server.parent.connection", "udp") - network = strings.ToLower(network) - - switch network { - case "udp": - opts.ConnType = dns.ConnTypeUDP - case "tcp": - opts.ConnType = dns.ConnTypeTCP - default: - log.Printf("Invalid network: '%s', using default 'udp'\n", - network) - } -} - func parseListen(cfg *ini.Ini, opts *rescached.Options) { - listen := cfg.GetString(cfgSecRescached, "", "server.listen", - "127.0.0.1") + listen, _ := cfg.Get(cfgSecRescached, "", "server.listen", "127.0.0.1") _, ip, port := libnet.ParseIPPort(listen, dns.DefaultPort) if ip == nil { @@ -151,12 +90,12 @@ func parseListen(cfg *ini.Ini, opts *rescached.Options) { return } - opts.ListenAddress = ip.String() - opts.ListenPort = port + opts.IPAddress = ip.String() + opts.Port = port } func parseDoHPort(cfg *ini.Ini, opts *rescached.Options) { - v := cfg.GetString(cfgSecRescached, "", "server.doh.listen.port", "443") + v, _ := cfg.Get(cfgSecRescached, "", "server.doh.listen.port", "443") port, err := strconv.Atoi(v) if err != nil { port = int(dns.DefaultDoHPort) @@ -166,7 +105,7 @@ func parseDoHPort(cfg *ini.Ini, opts *rescached.Options) { } func parseTimeout(cfg *ini.Ini, opts *rescached.Options) { - v := cfg.GetString(cfgSecRescached, "", "server.timeout", "6") + v, _ := cfg.Get(cfgSecRescached, "", "server.timeout", "6") timeout, err := strconv.Atoi(v) if err != nil { return @@ -176,7 +115,7 @@ func parseTimeout(cfg *ini.Ini, opts *rescached.Options) { } func parseCachePruneDelay(cfg *ini.Ini, opts *rescached.Options) { - v, ok := cfg.Get(cfgSecRescached, "", "cache.prune_delay") + v, ok := cfg.Get(cfgSecRescached, "", "cache.prune_delay", "") if !ok { return } @@ -185,14 +124,14 @@ func parseCachePruneDelay(cfg *ini.Ini, opts *rescached.Options) { var err error - opts.CachePruneDelay, err = time.ParseDuration(v) + opts.PruneDelay, err = time.ParseDuration(v) if err != nil { return } } func parseCacheThreshold(cfg *ini.Ini, opts *rescached.Options) { - v, ok := cfg.Get(cfgSecRescached, "", "cache.threshold") + v, ok := cfg.Get(cfgSecRescached, "", "cache.prune_threshold", "") if !ok { return } @@ -201,14 +140,14 @@ func parseCacheThreshold(cfg *ini.Ini, opts *rescached.Options) { var err error - opts.CacheThreshold, err = time.ParseDuration(v) + opts.PruneThreshold, err = time.ParseDuration(v) if err != nil { return } } func parseDebugLevel(cfg *ini.Ini) { - v, ok := cfg.Get(cfgSecRescached, "", "debug") + v, ok := cfg.Get(cfgSecRescached, "", "debug", "") if !ok { return } diff --git a/cmd/rescached/main.go b/cmd/rescached/main.go index feb9376..3e5e8d9 100644 --- a/cmd/rescached/main.go +++ b/cmd/rescached/main.go @@ -24,21 +24,16 @@ func createRescachedServer(fileConfig string) *rescached.Server { log.Fatal(err) } - if debug.Value >= 1 { - fmt.Printf("= config: %+v\n", opts) + rcd, err := rescached.New(opts) + if err != nil { + log.Fatal(err) } - rcd := rescached.New(opts) - err = rcd.WritePID() if err != nil { log.Fatal(err) } - rcd.LoadHostsDir(opts.DirHosts) - rcd.LoadMasterDir(opts.DirMaster) - rcd.LoadHostsFile("") - return rcd } @@ -58,7 +53,6 @@ func debugRuntime(rcd *rescached.Server) { debug.WriteHeapProfile("rescached", true) memHeap.Collect() - println(rcd.CachesStats()) fmt.Printf("= rescached: MemHeap{RelHeapAlloc:%d RelHeapObjects:%d DiffHeapObjects:%d}\n", memHeap.RelHeapAlloc, memHeap.RelHeapObjects, @@ -68,7 +62,6 @@ func debugRuntime(rcd *rescached.Server) { func main() { var ( - err error fileConfig string defConfig = "/etc/rescached/rescached.cfg" ) @@ -86,7 +79,7 @@ func main() { go debugRuntime(rcd) } - err = rcd.Start() + err := rcd.Start() if err != nil { log.Println(err) rcd.Stop() diff --git a/cmd/rescached/rescached.cfg b/cmd/rescached/rescached.cfg index 8e8ec22..b306215 100644 --- a/cmd/rescached/rescached.cfg +++ b/cmd/rescached/rescached.cfg @@ -3,43 +3,18 @@ ## ## server.parent:: List of parent DNS servers, separated by commas. ## -## Format:: -## Default address:: 1.1.1.1, 1.0.0.1 -## Default port:: 53 -## - -server.parent=1.1.1.1, 1.0.0.1:53 - -## -## server.parent.doh:: List of parent DNS servers over HTTPS, separated by -## commas. To be able to use DNS over HTTPS, rescached must also run DoH -## server. That means, certificate and private key files must not be empties. -## -## Format:: https:///dns-query , ... -## Default:: https://cloudflare-dns.com/dns-query -## - -#server.doh.parent = https://cloudflare-dns.com/dns-query - -## -## server.parent.doh.allow_insecure:: If its true, allow insecure TLS -## connection to parent DoH server. -## -## Format:: true | false -## Default:: false +## Format:: ## - -#server.doh.allow_insecure = false - -## -## server.parent.connection:: Type of connection to parent server. +## nameservers = [ scheme "://"] ( ip-address / domain-name ) [ ":" port ] +## scheme = ( "tcp" / "udp" / "https") ## -## Format:: tcp | udp -## Default:: udp +## Default scheme:: udp +## Default address:: (None) +## Default port:: 53 ## -server.parent.connection=udp -#server.parent.connection=tcp +server.parent=udp://1.1.1.1 +server.parent=udp://1.0.0.1:53 ## ## server.listen:: Local IP address that rescached will listening for client @@ -76,6 +51,16 @@ server.listen=127.0.0.1:53 #server.doh.certificate.key = /etc/rescached/localhost.key.pem +## +## server.doh.allow_insecure:: If its true, allow serving self signed +## certificate on DoH connection. +## +## Format:: true | false +## Default:: false +## + +#server.doh.allow_insecure = false + ## ## server.timeout:: Timeout value, in seconds, for sending and waiting ## packet from client or parent server. @@ -97,13 +82,14 @@ server.listen=127.0.0.1:53 #cache.prune_delay = 1h ## -## cache.threshold:: The duration when the cache will be considered expired. +## cache.prune_threshold:: The duration when the cache will be considered +## expired. ## ## Format:: Duration. Valid time units are "s", "m", "h". ## Default:: -1h ## -#cache.threshold = -1h +#cache.prune_threshold = -1h ## ## dir.hosts:: If its set, rescached will load all (host) files in path. @@ -138,9 +124,8 @@ file.pid=/var/run/rescached.pid ## ## file.resolvconf:: A path to dynamically generated resolv.conf (5) by -## resolvconf (8). If set, the nameserver values in referenced file will -## replace "server.parent" value and "server.parent" will become a fallback in -## case the referenced file being deleted or can't be parsed. +## resolvconf (8). If set, the nameserver values in referenced file will be +## used as fallback nameserver. ## ## Format:: /any/path/to/file ## Default:: /etc/rescached/resolv.conf diff --git a/cmd/resolver/main.go b/cmd/resolver/main.go index 6af3f47..b47eba3 100644 --- a/cmd/resolver/main.go +++ b/cmd/resolver/main.go @@ -132,7 +132,7 @@ func lookup(opts *options, ns string, timeout time.Duration, qname []byte) *dns. log.Fatal("! Pack:", err) } - res, err := cl.Query(req, nil) + res, err := cl.Query(req) if err != nil { log.Println("! Lookup: ", err) return nil diff --git a/cmd/resolverbench/main.go b/cmd/resolverbench/main.go index 948452e..c84023e 100644 --- a/cmd/resolverbench/main.go +++ b/cmd/resolverbench/main.go @@ -33,46 +33,27 @@ func main() { } var nfail int - res := libdns.NewMessage() fmt.Printf("= Benchmarking with %d messages\n", len(msgs)) timeStart := time.Now() for x := 0; x < len(msgs); x++ { - //fmt.Printf("< Request: %6d %s\n", x, msgs[x].Question) - - _, err = cl.Send(msgs[x], cl.Addr) + res, err := cl.Query(msgs[x]) if err != nil { nfail++ log.Println("! Send error: ", err) continue } - res.Reset() - - _, err = cl.Recv(res) - if err != nil { - nfail++ - log.Println("! Recv error: ", err) - continue - } - - err = res.Unpack() - if err != nil { - nfail++ - log.Println("! Unpack:", err) - continue - } - exp := msgs[x].Answer[0].RData().([]byte) got := res.Answer[0].RData().([]byte) if !bytes.Equal(exp, got) { nfail++ - log.Printf(`! Answer not matched: + log.Printf(`! Answer not matched %s: expecting: %s got: %s -`, exp, got) +`, msgs[x].Question, exp, got) } } timeEnd := time.Now() diff --git a/doc/rescached.cfg.adoc b/doc/rescached.cfg.adoc index c5815b2..c4e409d 100644 --- a/doc/rescached.cfg.adoc +++ b/doc/rescached.cfg.adoc @@ -27,7 +27,6 @@ The configuration is using INI format where each options is grouped by header in square bracket: * +[rescached]+ -* +[log]+ == OPTIONS @@ -39,36 +38,46 @@ This group of options contain the main configuration. [[server.parent]] ==== +server.parent+ -Format:: IP-ADDRESS:PORT, IP-ADDRESS:PORT, ... +Format:: + +---- +nameservers = nameserver *( "," nameserver ) +nameserver = [ scheme "://"] ( ip-address / domain-name ) [ ":" port ] +scheme = ( "tcp" / "udp" / "https") +---- + Default:: -* Address: 1.1.1.1 and 1.0.0.1 +* Address: udp://1.1.1.1 , udp://1.0.0.1 * Port: 53 Description:: List of parent DNS servers, separated by commas. ++ When +rescached+ receive a query from client and when it does not have a cached address of query, it will pass the query to those parent server. -+rescached+ use a Google DNS public server as a default parent address if not set. -The reason for this is that Google DNS public server use a simple and small -size of response/answer. ++rescached+ use Cloudflare DNS public servers as a default parent name servers +if not set. +The reason for this is that Cloudflare DNS public server use a simple and +small size of response/answer. ++ Please, do not use OpenDNS server. If certain host-name not found (i.e. typo in host-name), OpenDNS will reply with its own address, instead of replying with empty answer. This will make +rescached+ caching a false data. ++ To check if your parent server reply the unknown host-name with no answer, use *resolver*(1) tool. +Example:: +---- + ## Using UDP connection to forward request to parent name server. + server.parent = udp://1.1.1.1 -[[server.parent.connection]] -==== +server.parent.connection+ - -Format:: String ("udp" or "tcp", without quotes) -Default:: udp -Description:: Type of protocol that will be used to send request to -+server.parent+. -When +rescached+ receive query from client it will forward the query to the -+server.parent+ using default protocol (UDP). -In case UDP is blocked, you can set this variable to "tcp". + ## Using TCP connection to forward request to parent name server. + server.parent = tcp://1.0.0.1 + ## Using DNS over HTTPS to forward request to parent name server. + server.parent = https://cloudflare-dns.com/dns-query +---- [[server.listen]] ==== +server.listen+ @@ -81,26 +90,6 @@ If you want rescached to serve a query from another host in your local network, change this value to +0.0.0.0:53+. -[[server.doh.parent]] -==== +server.doh.parent+ - -Format:: https:///dns-query, ... -Default:: https://cloudflare-dns.com/dns-query -Description:: List of parent DNS over HTTPS (DoH), separated by commas. -To be able to use DNS over HTTPS, rescached must also run DoH server. -That means, DoH certificate and private key files must not be empties. - -Only queries from DoH server will be forwarded to the parent DoH name server, -other queries (from UDP or TCP) will be forwarded to "+server.parent+". - -[[server.doh.allow_insecure]] -==== +server.doh.allow_insecure+ - -Format:: true | false -Default:: false -Description:: If its true, allow insecure TLS connection to parent DoH server. - - [[server.doh.certificate]] ==== +server.doh.certificate+ @@ -117,6 +106,14 @@ Default:: (empty) Description:: Path to certificate private key file to serve DNS over HTTPS. +[[server.doh.allow_insecure]] +==== +server.doh.allow_insecure+ + +Format:: true | false +Default:: false +Description:: If its true, the certificate is self-signed. + + [[server.doh.listen.port]] ==== +server.doh.listen.port+ @@ -139,14 +136,15 @@ Format:: Duration Default:: 1h Description:: Every N seconds/minutes/hours, rescached will traverse all caches and remove response that has not been accessed less than -+cache.threshold+. ++cache.prune_threshold+. -[[cache.threshold]] -==== +cache.threshold+ +[[cache.prune_threshold]] +==== +cache.prune_threshold+ Format:: Duration -Default:: 1h +Default:: -1h Description:: The duration when the cache will be considered expired. +Its value must negative and less than -1 minute. [[dir.hosts]] ==== +dir.hosts+ @@ -158,14 +156,14 @@ If set, rescached will load all hosts formatted files inside the directory. If its empty or unset, it will not loading hosts files even in default location. -[[dir.zone]] -==== +dir.zone+ +[[dir.master]] +==== +dir.master+ Format:: string -Default:: /etc/rescached/zone.d -Description:: Path to zone directory. +Default:: /etc/rescached/master.d +Description:: Path to master directory. If set, rescached will load all master files inside directory. -If its empty or unset, it will not loading zone file even in default +If its empty or unset, it will not loading master file even in default location. [[file.pid]] @@ -184,8 +182,8 @@ directory where user running +rescached+. Format:: /any/path/to/file Default:: /etc/rescached/resolv.conf -Description:: A path to dynamically generated resolv.conf (5) by -resolvconf (8). If set, the nameserver values in referenced file will +Description:: A path to dynamically generated *resolv.conf*(5) by +*resolvconf*(8). If set, the nameserver values in referenced file will replace "server.parent" value and "server.parent" will become a fallback in case the referenced file being deleted or can't be parsed. @@ -223,9 +221,9 @@ seconds. .............................................................................. [rescached] -server.parent=127.0.0.1:54 +server.parent=udp://127.0.0.1:54 cache.prune_delay=60s -cache.threshold=60s +cache.prune_threshold=60s .............................................................................. Save the above script into +rescached.cfg+ and run it, diff --git a/go.mod b/go.mod index 89eee83..7de9bd4 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,5 @@ module github.com/shuLhan/rescached-go -require ( - github.com/shuLhan/share v0.5.0 - golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 // indirect - golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 // indirect -) +go 1.11 + +require github.com/shuLhan/share v0.7.1-0.20190704154406-f04e13ee0b3a diff --git a/go.sum b/go.sum index ed01829..84ca528 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,9 @@ -github.com/shuLhan/share v0.4.0 h1:bp5SlzcF7u1feOc4BvWHHVtUXecaq26Lyn8rROMbQ1k= -github.com/shuLhan/share v0.4.0/go.mod h1:n3BWSX6aGXiCmpc89DsfnhNnax89yhKLHFu6WC9ODes= -github.com/shuLhan/share v0.5.0 h1:GuZP4gINDNAletm02ngpci2N5v3Qpr7uNfR6TOwQ7jM= -github.com/shuLhan/share v0.5.0/go.mod h1:5wZbVA4A5FYrj999Er2rWzVyj80Ym+7pcC8/IF9TDJY= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +github.com/shuLhan/share v0.7.1-0.20190704154406-f04e13ee0b3a h1:YAPMVxfApe6OhDGeaj4ysFrYVISEWBBoSrPNHaO/Voc= +github.com/shuLhan/share v0.7.1-0.20190704154406-f04e13ee0b3a/go.mod h1:p2/wsQ00pX/F1ysg+tXaix99ca2poS7iiWd6WWcVnuI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190614084037-d442b75600c5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/listrequest.go b/listrequest.go deleted file mode 100644 index 9c5523c..0000000 --- a/listrequest.go +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "container/list" - "fmt" - "strings" - "sync" - - "github.com/shuLhan/share/lib/dns" -) - -// -// listRequest represent list of active DNS requests. -// Each request is maintained as FIFO, where new request will be at the end of -// list. -// -type listRequest struct { - sync.Mutex - v *list.List -} - -// -// newListRequest create and initialize new listRequest. -// -func newListRequest(req *dns.Request) (listReq *listRequest) { - listReq = &listRequest{ - v: list.New(), - } - if req != nil { - listReq.v.PushBack(req) - } - return -} - -// -// String return string interpretation of listRequest as a slice. -// -func (listReq *listRequest) String() string { - var out strings.Builder - - out.WriteByte('[') - x := 0 - listReq.Lock() - for e := listReq.v.Front(); e != nil; e = e.Next() { - if x == 0 { - x++ - } else { - out.WriteByte(' ') - } - req := e.Value.(*dns.Request) - fmt.Fprintf(&out, "&{Kind:%d Message.Question:%s}", req.Kind, - req.Message.Question) - } - listReq.Unlock() - out.WriteByte(']') - - return out.String() -} - -// -// push new request to the end of list only if its not nil. -// -func (listReq *listRequest) push(req *dns.Request) { - if req == nil { - return - } - listReq.Lock() - listReq.v.PushBack(req) - listReq.Unlock() -} - -// -// isExist will return true if query type and class exist in list; otherwise -// it will return false. -// -func (listReq *listRequest) isExist(qtype, qclass uint16) (yes bool) { - listReq.Lock() - - for e := listReq.v.Front(); e != nil; e = e.Next() { - req := e.Value.(*dns.Request) - if qtype != req.Message.Question.Type { - continue - } - if qclass != req.Message.Question.Class { - continue - } - yes = true - break - } - - listReq.Unlock() - return -} - -// -// pops detach requests that have the same query type and class from list and -// return it. -// -func (listReq *listRequest) pops(qtype, qclass uint16) ( - reqs []*dns.Request, isEmpty bool, -) { - listReq.Lock() - - e := listReq.v.Front() - for e != nil { - next := e.Next() - req := e.Value.(*dns.Request) - if qtype != req.Message.Question.Type { - e = next - continue - } - if qclass != req.Message.Question.Class { - e = next - continue - } - listReq.v.Remove(e) - reqs = append(reqs, req) - e = next - } - - if listReq.v.Len() == 0 { - isEmpty = true - } - - listReq.Unlock() - return -} diff --git a/listrequest_test.go b/listrequest_test.go deleted file mode 100644 index 6d139a5..0000000 --- a/listrequest_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "testing" - - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -var testRequests = []*dns.Request{{ // nolint: gochecknoglobals - Message: &dns.Message{ - Question: &dns.SectionQuestion{ - Type: 1, - Class: 1, - }, - }, -}, { - Message: &dns.Message{ - Question: &dns.SectionQuestion{ - Type: 2, - Class: 1, - }, - }, -}, { - Message: &dns.Message{ - Question: &dns.SectionQuestion{ - Type: 3, - Class: 1, - }, - }, -}} - -var testListRequest = newListRequest(testRequests[0]) // nolint: gochecknoglobals - -func TestListRequestPush(t *testing.T) { - cases := []struct { - desc string - req *dns.Request - expLen int - exp string - }{{ - desc: "With empty request", - expLen: 1, - exp: `[&{Kind:0 Message.Question:&{Name: Type:A}}]`, - }, { - desc: "With non empty request (1)", - req: testRequests[1], - expLen: 2, - exp: `[&{Kind:0 Message.Question:&{Name: Type:A}} &{Kind:0 Message.Question:&{Name: Type:NS}}]`, // nolint: lll - }, { - desc: "With non empty request (2)", - req: testRequests[2], - expLen: 3, - exp: `[&{Kind:0 Message.Question:&{Name: Type:A}} &{Kind:0 Message.Question:&{Name: Type:NS}} &{Kind:0 Message.Question:&{Name: Type:}}]`, // nolint: lll - }} - - for _, c := range cases { - t.Log(c.desc) - - testListRequest.push(c.req) - - test.Assert(t, "length", c.expLen, testListRequest.v.Len(), true) - test.Assert(t, "String", c.exp, testListRequest.String(), true) - } -} - -func TestListRequestIsExist(t *testing.T) { - cases := []struct { - desc string - qtype uint16 - qclass uint16 - exp bool - }{{ - desc: "With qtype not found", - qtype: 0, - qclass: 1, - }, { - desc: "With qclass not found", - qtype: 1, - qclass: 0, - }, { - desc: "With qtype and qclass found", - qtype: 1, - qclass: 1, - exp: true, - }} - - for _, c := range cases { - t.Log(c.desc) - - got := testListRequest.isExist(c.qtype, c.qclass) - test.Assert(t, "IsExist", c.exp, got, true) - } -} - -func TestListRequestPops(t *testing.T) { - cases := []struct { - desc string - qtype uint16 - qclass uint16 - expIsEmpty bool - expLen int - exp []*dns.Request - }{{ - desc: "With qtype not found", - qtype: 0, - qclass: 1, - }, { - desc: "With qclass not found", - qtype: 1, - qclass: 0, - }, { - desc: "With qtype and qclass found (1)", - qtype: 1, - qclass: 1, - expLen: 1, - exp: []*dns.Request{ - testRequests[0], - }, - }, { - desc: "With qtype and qclass found (2)", - qtype: 2, - qclass: 1, - expLen: 1, - exp: []*dns.Request{ - testRequests[1], - }, - }, { - desc: "With qtype and qclass found (3)", - qtype: 3, - qclass: 1, - expLen: 1, - expIsEmpty: true, - exp: []*dns.Request{ - testRequests[2], - }, - }} - - for _, c := range cases { - t.Log(c.desc) - - gots, gotIsEmpty := testListRequest.pops(c.qtype, c.qclass) - - test.Assert(t, "IsEmpty", c.expIsEmpty, gotIsEmpty, true) - test.Assert(t, "len", c.expLen, len(gots), true) - - for x, got := range gots { - test.Assert(t, "request", c.exp[x], got, true) - } - } - -} diff --git a/listresponse.go b/listresponse.go deleted file mode 100644 index 64d1342..0000000 --- a/listresponse.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "container/list" - "strings" - "sync" - - "github.com/shuLhan/share/lib/dns" -) - -// -// listResponse represent cached DNS response. -// -type listResponse struct { - sync.Mutex - v *list.List -} - -func newListResponse(res *response) (lres *listResponse) { - lres = &listResponse{ - v: list.New(), - } - if res != nil { - lres.v.PushBack(res) - } - return -} - -// -// get cached response based on request type and class. -// -func (lres *listResponse) get(qtype, qclass uint16) *response { - lres.Lock() - for e := lres.v.Front(); e != nil; e = e.Next() { - res := e.Value.(*response) - if qtype != res.message.Question.Type { - continue - } - if qclass != res.message.Question.Class { - continue - } - lres.Unlock() - return res - } - lres.Unlock() - return nil -} - -func (lres *listResponse) add(msg *dns.Message, isLocal bool) *response { - res := newResponse(msg) - if isLocal { - res.receivedAt = 0 - } - lres.Lock() - lres.v.PushBack(res) - lres.Unlock() - return res -} - -// -// update the message field in response with "msg" and return the replaced -// message. -// -func (lres *listResponse) update(res *response, msg *dns.Message) *dns.Message { - if res == nil || msg == nil { - return nil - } - lres.Lock() - oldMsg := res.update(msg) - lres.Unlock() - return oldMsg -} - -func (lres *listResponse) remove(qtype, qclass uint16) *response { - lres.Lock() - for e := lres.v.Front(); e != nil; e = e.Next() { - res := e.Value.(*response) - if qtype != res.message.Question.Type { - continue - } - if qclass != res.message.Question.Class { - continue - } - lres.v.Remove(e) - lres.Unlock() - return res - } - lres.Unlock() - return nil -} - -// -// String convert all response in cache into string, as in slice. -// -func (lres *listResponse) String() string { - var b strings.Builder - - b.WriteByte('[') - first := true - - lres.Lock() - for e := lres.v.Front(); e != nil; e = e.Next() { - if first { - first = false - } else { - b.WriteByte(' ') - } - ev := e.Value.(*response) - b.WriteString(ev.String()) - } - lres.Unlock() - - b.WriteByte(']') - - return b.String() -} diff --git a/listresponse_test.go b/listresponse_test.go deleted file mode 100644 index b276cf6..0000000 --- a/listresponse_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "testing" - - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -var _testListResponse = newListResponse(nil) // nolint - -func TestListResponseAdd(t *testing.T) { - cases := []struct { - desc string - msg *dns.Message - expLen int - exp string - }{{ - desc: "New", - msg: _testResponses[0].message, - expLen: 1, - exp: `[{0 0 &{Name:1 Type:A}}]`, - }, { - desc: "New", - msg: _testResponses[1].message, - expLen: 2, - exp: `[{0 0 &{Name:1 Type:A}} {0 0 &{Name:2 Type:NS}}]`, - }, { - desc: "Replace", - msg: _testResponses[2].message, - expLen: 3, - exp: `[{0 0 &{Name:1 Type:A}} {0 0 &{Name:2 Type:NS}} {0 0 &{Name:1 Type:A}}]`, - }} - - for _, c := range cases { - t.Logf(c.desc) - - res := _testListResponse.add(c.msg, true) - res.accessedAt = 0 - - test.Assert(t, "listResponse.Len", c.expLen, _testListResponse.v.Len(), true) - test.Assert(t, "listResponse", c.exp, _testListResponse.String(), true) - } -} - -func TestListResponseUpdate(t *testing.T) { - testListResponse := newListResponse(nil) - testResponse := &response{ - receivedAt: 0, - } - testMessages := []*dns.Message{{ - Packet: []byte{1}, - }, { - Packet: []byte{2}, - }} - - cases := []struct { - desc string - res *response - msg *dns.Message - exp *dns.Message - }{{ - desc: "With empty response", - msg: &dns.Message{}, - }, { - desc: "With empty message", - res: testResponse, - }, { - desc: "With nil return", - res: testResponse, - msg: testMessages[0], - }, { - desc: "With non nil return", - res: testResponse, - msg: testMessages[1], - exp: testMessages[0], - }} - - for _, c := range cases { - t.Log(c.desc) - - got := testListResponse.update(c.res, c.msg) - - test.Assert(t, "message", c.exp, got, true) - } -} - -func TestListResponseGet(t *testing.T) { - cases := []struct { - desc string - qtype uint16 - qclass uint16 - exp *response - }{{ - desc: "Cache hit", - qtype: 1, - qclass: 1, - exp: _testResponses[0], - }, { - desc: "Cache miss", - qtype: 0, - qclass: 1, - exp: nil, - }} - - for _, c := range cases { - t.Log(c.desc) - - got := _testListResponse.get(c.qtype, c.qclass) - if got == nil { - test.Assert(t, "response.get", c.exp, got, true) - continue - } - - test.Assert(t, "response.get", c.exp.message, got.message, true) - } -} diff --git a/options.go b/options.go index baff4f3..633c371 100644 --- a/options.go +++ b/options.go @@ -5,36 +5,24 @@ package rescached import ( - "net" + "fmt" "time" "github.com/shuLhan/share/lib/dns" + libnet "github.com/shuLhan/share/lib/net" + libstrings "github.com/shuLhan/share/lib/strings" ) // // Options for running rescached. // type Options struct { - ListenAddress string - NSParents []*net.UDPAddr - Timeout time.Duration - CachePruneDelay time.Duration - CacheThreshold time.Duration - + dns.ServerOptions + Timeout time.Duration FilePID string FileResolvConf string DirHosts string DirMaster string - - DoHParents []string - DoHCert string - DoHCertKey string - - ListenPort uint16 - DoHPort uint16 - - ConnType dns.ConnType - DoHAllowInsecure bool } // @@ -42,55 +30,58 @@ type Options struct { // func NewOptions() *Options { return &Options{ - ListenAddress: "127.0.0.1", - ConnType: dns.ConnTypeUDP, - Timeout: 6 * time.Second, - CachePruneDelay: time.Hour, - CacheThreshold: -1 * time.Hour, - FilePID: "rescached.pid", - ListenPort: dns.DefaultPort, + ServerOptions: dns.ServerOptions{ + IPAddress: "127.0.0.1", + }, + + Timeout: 6 * time.Second, + FilePID: "rescached.pid", } } // -// init check and initialize the options with default value if its empty. +// init check and initialize the Options instance with default values. // func (opts *Options) init() { - if len(opts.ListenAddress) == 0 { - opts.ListenAddress = "127.0.0.1" - } - if opts.ConnType == 0 { - opts.ConnType = dns.ConnTypeUDP - } - if len(opts.NSParents) == 0 { - opts.NSParents = []*net.UDPAddr{{ - IP: net.ParseIP("1.1.1.1"), - Port: int(dns.DefaultPort), - }, { - IP: net.ParseIP("1.0.0.1"), - Port: int(dns.DefaultPort), - }} + if len(opts.IPAddress) == 0 { + opts.IPAddress = "127.0.0.1" } if opts.Timeout <= 0 || opts.Timeout > (6*time.Second) { opts.Timeout = 6 * time.Second } - if opts.CachePruneDelay <= 0 { - opts.CachePruneDelay = time.Hour - } - if opts.CacheThreshold == 0 { - opts.CacheThreshold = -1 * time.Hour - } else if opts.CacheThreshold > 0 { - opts.CacheThreshold = -1 * opts.CacheThreshold - } if len(opts.FilePID) == 0 { opts.FilePID = "rescached.pid" } - if opts.ListenPort == 0 { - opts.ListenPort = dns.DefaultPort + if len(opts.FileResolvConf) > 0 { + opts.loadResolvConf() } - if len(opts.DoHParents) == 0 { - opts.DoHParents = []string{ - "https://cloudflare-dns.com/dns-query", - } +} + +func (opts *Options) loadResolvConf() (ok bool, err error) { + rc, err := libnet.NewResolvConf(opts.FileResolvConf) + if err != nil { + return false, err } + + fmt.Printf("loadResolvConf: %+v\n", rc) + + if len(rc.NameServers) == 0 { + return false, nil + } + + for x := 0; x < len(rc.NameServers); x++ { + rc.NameServers[x] = "udp://" + rc.NameServers[x] + } + + if libstrings.IsEqual(opts.NameServers, rc.NameServers) { + return false, nil + } + + if len(opts.NameServers) == 0 { + opts.NameServers = rc.NameServers + } else { + opts.FallbackNS = rc.NameServers + } + + return true, nil } diff --git a/options_test.go b/options_test.go deleted file mode 100644 index 445afdf..0000000 --- a/options_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "net" - "testing" - "time" - - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -func TestOptionsInit(t *testing.T) { - cases := []struct { - desc string - opts *Options - exp *Options - }{{ - desc: "With positive cache threshold", - opts: &Options{ - CacheThreshold: time.Minute, - }, - exp: &Options{ - ListenAddress: "127.0.0.1", - ConnType: dns.ConnTypeUDP, - NSParents: []*net.UDPAddr{{ - IP: net.ParseIP("1.1.1.1"), - Port: int(dns.DefaultPort), - }, { - IP: net.ParseIP("1.0.0.1"), - Port: int(dns.DefaultPort), - }}, - Timeout: 6 * time.Second, - CachePruneDelay: time.Hour, - CacheThreshold: -1 * time.Minute, - FilePID: "rescached.pid", - ListenPort: dns.DefaultPort, - DoHParents: []string{ - "https://cloudflare-dns.com/dns-query", - }, - }, - }} - - for _, c := range cases { - t.Log(c.desc) - - c.opts.init() - - test.Assert(t, "Options.Init", c.exp, c.opts, true) - } -} diff --git a/rescached.go b/rescached.go index 31a0db4..b70a047 100644 --- a/rescached.go +++ b/rescached.go @@ -6,280 +6,73 @@ package rescached import ( - "bytes" - "errors" "fmt" "io/ioutil" "log" - "net" "os" - "path/filepath" "strconv" - libbytes "github.com/shuLhan/share/lib/bytes" "github.com/shuLhan/share/lib/debug" "github.com/shuLhan/share/lib/dns" libio "github.com/shuLhan/share/lib/io" - libnet "github.com/shuLhan/share/lib/net" -) - -const ( - _maxQueue = 512 - _maxForwarder = 4 -) - -// List of error messages. -var ( - ErrNetworkType = errors.New("invalid network type") ) // Server implement caching DNS server. type Server struct { - dnsServer *dns.Server - nsParents []*net.UDPAddr - reqQueue chan *dns.Request - fwQueue chan *dns.Request - fwDoHQueue chan *dns.Request - fwStop chan bool - cw *cacheWorker - opts *Options + dns *dns.Server + opts *Options } // // New create and initialize new rescached server. // -func New(opts *Options) *Server { +func New(opts *Options) (srv *Server, err error) { if opts == nil { opts = NewOptions() } opts.init() - srv := &Server{ - dnsServer: new(dns.Server), - reqQueue: make(chan *dns.Request, _maxQueue), - fwQueue: make(chan *dns.Request, _maxQueue), - fwDoHQueue: make(chan *dns.Request, _maxQueue), - fwStop: make(chan bool), - cw: newCacheWorker(opts.CachePruneDelay, opts.CacheThreshold), - opts: opts, - } - - if len(srv.opts.FileResolvConf) == 0 { - srv.nsParents = srv.opts.NSParents - } else { - err := srv.loadResolvConf() - if err != nil { - log.Printf("! loadResolvConf: %s\n", err) - srv.nsParents = srv.opts.NSParents - } - } - - srv.dnsServer.Handler = srv - - return srv -} - -func (srv *Server) CachesStats() string { - return fmt.Sprintf("= rescached: CachesStats{caches:%d cachesList:%d}", - srv.cw.caches.length(), srv.cw.cachesList.length()) -} - -// -// LoadHostsFile parse hosts formatted file and put it into caches. -// -func (srv *Server) LoadHostsFile(path string) { - if len(path) == 0 { - fmt.Println("= Loading system hosts file") - } else { - fmt.Printf("= Loading hosts file '%s'\n", path) - } - - msgs, err := dns.HostsLoad(path) - if err != nil { - return - } - - srv.populateCaches(msgs) -} - -// -// LoadHostsDir load all host formatted files in directory. -// -func (srv *Server) LoadHostsDir(dir string) { - if len(dir) == 0 { - return - } - - d, err := os.Open(dir) - if err != nil { - log.Println("! loadHostsDir: Open:", err) - return - } - - fis, err := d.Readdir(0) - if err != nil { - log.Println("! loadHostsDir: Readdir:", err) - err = d.Close() - if err != nil { - log.Println("! loadHostsDir: Close:", err) - } - return - } - - for x := 0; x < len(fis); x++ { - if fis[x].IsDir() { - continue - } - - hostsFile := filepath.Join(dir, fis[x].Name()) - - srv.LoadHostsFile(hostsFile) - } - - err = d.Close() - if err != nil { - log.Println("! loadHostsDir: Close:", err) - } -} - -// -// LoadMasterDir load all master formatted files in directory. -// -func (srv *Server) LoadMasterDir(dir string) { - if len(dir) == 0 { - return - } - - d, err := os.Open(dir) - if err != nil { - log.Println("! loadMasterDir: ", err) - return - } - - fis, err := d.Readdir(0) - if err != nil { - log.Println("! loadMasterDir: ", err) - err = d.Close() - if err != nil { - log.Println("! loadMasterDir: Close:", err) - } - return - } - - for x := 0; x < len(fis); x++ { - if fis[x].IsDir() { - continue - } - - masterFile := filepath.Join(dir, fis[x].Name()) - - srv.LoadMasterFile(masterFile) - } - - err = d.Close() - if err != nil { - log.Println("! loadHostsDir: Close:", err) - } -} - -// -// LostMasterFile parse master file and put the result into caches. -// -func (srv *Server) LoadMasterFile(path string) { - fmt.Printf("= Loading master file '%s'\n", path) - - msgs, err := dns.MasterLoad(path, "", 0) - if err != nil { - return - } - - srv.populateCaches(msgs) -} - -func (srv *Server) loadResolvConf() error { - rc, err := libnet.NewResolvConf(srv.opts.FileResolvConf) - if err != nil { - return err + if debug.Value >= 1 { + fmt.Printf("= config: %+v\n", opts) } - nsAddrs, err := dns.ParseNameServers(rc.NameServers) + dnsServer, err := dns.NewServer(&opts.ServerOptions) if err != nil { - return err + return nil, err } - if len(nsAddrs) > 0 { - srv.nsParents = nsAddrs - } else { - srv.nsParents = srv.opts.NSParents - } + dnsServer.LoadHostsDir(opts.DirHosts) + dnsServer.LoadMasterDir(opts.DirMaster) + dnsServer.LoadHostsFile("") - return nil -} - -func (srv *Server) populateCaches(msgs []*dns.Message) { - n := 0 - for x := 0; x < len(msgs); x++ { - ok := srv.cw.upsert(msgs[x], true) - if ok { - n++ - } - msgs[x] = nil + srv = &Server{ + dns: dnsServer, + opts: opts, } - fmt.Printf("== %d record cached\n", n) -} - -// -// ServeDNS handle DNS request from server. -// -func (srv *Server) ServeDNS(req *dns.Request) { - srv.reqQueue <- req + return srv, nil } // // Start the server, waiting for DNS query from clients, read it and response // it. // -func (srv *Server) Start() error { - fmt.Printf("= Listening on '%s:%d'\n", srv.opts.ListenAddress, - srv.opts.ListenPort) - - err := srv.runForwarders() - if err != nil { - return err - } - - if len(srv.opts.DoHCert) > 0 && len(srv.opts.DoHCertKey) > 0 { - fmt.Printf("= DoH listening on '%s:%d'\n", - srv.opts.ListenAddress, srv.opts.DoHPort) +func (srv *Server) Start() (err error) { + fmt.Printf("= Listening on '%s:%d'\n", srv.opts.IPAddress, + srv.opts.Port) - err = srv.runDoHForwarders() + if len(srv.opts.FileResolvConf) > 0 { + _, err = libio.NewWatcher(srv.opts.FileResolvConf, 0, srv.watchResolvConf) if err != nil { - return err + log.Fatal("rescached: Start:", err) } } - if len(srv.opts.FileResolvConf) > 0 { - go srv.watchResolvConf() - } - - go srv.cw.start() - go srv.processRequestQueue() - - serverOptions := &dns.ServerOptions{ - IPAddress: srv.opts.ListenAddress, - UDPPort: srv.opts.ListenPort, - TCPPort: srv.opts.ListenPort, - DoHPort: srv.opts.DoHPort, - DoHCert: srv.opts.DoHCert, - DoHCertKey: srv.opts.DoHCertKey, - DoHAllowInsecure: srv.opts.DoHAllowInsecure, - } + srv.dns.Start() + srv.dns.Wait() - err = srv.dnsServer.ListenAndServe(serverOptions) - - return err + return nil } // @@ -318,219 +111,21 @@ func (srv *Server) WritePID() error { return err } -func (srv *Server) runForwarders() (err error) { - max := _maxForwarder - - fmt.Printf("= Name servers: %v\n", srv.nsParents) - - if len(srv.nsParents) > max { - max = len(srv.nsParents) - } - - for x := 0; x < max; x++ { - var ( - cl dns.Client - raddr *net.UDPAddr - ) - - nsIdx := x % len(srv.nsParents) - raddr = srv.nsParents[nsIdx] - - if srv.opts.ConnType == dns.ConnTypeUDP { - cl, err = dns.NewUDPClient(raddr.String()) - if err != nil { - log.Fatal("runForwarders: NewUDPClient:", err) - return - } - } - - go srv.processForwardQueue(cl, raddr) - } - return -} - -func (srv *Server) runDoHForwarders() error { - fmt.Printf("= DoH name servers: %v\n", srv.opts.DoHParents) - - for x := 0; x < len(srv.opts.DoHParents); x++ { - cl, err := dns.NewDoHClient(srv.opts.DoHParents[x], srv.opts.DoHAllowInsecure) - if err != nil { - log.Fatal("runDoHForwarders: NewDoHClient:", err) - return err - } - - go srv.processDoHForwardQueue(cl) - } - - return nil -} - -func (srv *Server) stopForwarders() { - srv.fwStop <- true -} - -// -// processRequest process request from any connection, forward it to parent -// name server if no response from cache or if cache is expired; or send the -// cached response back to request. -// -func (srv *Server) processRequest(req *dns.Request) { - if req == nil { +func (srv *Server) watchResolvConf(ns *libio.NodeState) { + switch ns.State { + case libio.FileStateDeleted: + log.Printf("= ResolvConf: file %q deleted\n", srv.opts.FileResolvConf) return - } - if debug.Value >= 1 { - fmt.Printf("< request: Kind:%-4s ID:%-5d %s\n", - dns.ConnTypeNames[req.Kind], - req.Message.Header.ID, req.Message.Question) - } - - // Check if request query name exist in cache. - libbytes.ToLower(&req.Message.Question.Name) - qname := string(req.Message.Question.Name) - _, res := srv.cw.caches.get(qname, req.Message.Question.Type, req.Message.Question.Class) - if res == nil || res.isExpired() { - if req.Kind == dns.ConnTypeDoH { - srv.fwDoHQueue <- req - } else { - srv.fwQueue <- req - } - return - } - - srv.processRequestResponse(req, res.message) - - // Ignore update on local caches - if res.receivedAt == 0 { - if debug.Value >= 1 { - fmt.Printf("= local : ID:%-5d %s\n", - res.message.Header.ID, res.message.Question) - } - } else { - if debug.Value >= 1 { - fmt.Printf("= cache : Total:%-4d ID:%-5d %s\n", - srv.cw.cachesList.length(), - res.message.Header.ID, res.message.Question) - } - - srv.cw.cachesList.fix(res) - } -} - -func (srv *Server) processRequestResponse(req *dns.Request, res *dns.Message) { - res.SetID(req.Message.Header.ID) - - switch req.Kind { - case dns.ConnTypeUDP, dns.ConnTypeTCP: - if req.Sender != nil { - _, err := req.Sender.Send(res, req.UDPAddr) - if err != nil { - log.Println("! processRequest: Sender.Send:", err) - } - } - - case dns.ConnTypeDoH: - if req.ResponseWriter != nil { - _, err := req.ResponseWriter.Write(res.Packet) - if err != nil { - log.Println("! processRequest: ResponseWriter.Write:", err) - } - req.ChanResponded <- true - } - } -} - -func (srv *Server) processRequestQueue() { - for req := range srv.reqQueue { - srv.processRequest(req) - } -} - -func (srv *Server) processForwardQueue(cl dns.Client, raddr net.Addr) { - for { - select { - case req := <-srv.fwQueue: - var ( - err error - res *dns.Message - ) - - switch srv.opts.ConnType { - case dns.ConnTypeUDP: - res, err = cl.Query(req.Message, raddr) - - case dns.ConnTypeTCP: - cl, err = dns.NewTCPClient(raddr.String()) - if err != nil { - continue - } - - res, err = cl.Query(req.Message, nil) - - cl.Close() - } - if err != nil { - continue - } - - srv.processForwardResponse(req, res) - - case <-srv.fwStop: - return - } - } -} - -func (srv *Server) processDoHForwardQueue(cl *dns.DoHClient) { - for req := range srv.fwDoHQueue { - res, err := cl.Query(req.Message, nil) + default: + ok, err := srv.opts.loadResolvConf() if err != nil { - continue + log.Println("rescached: loadResolvConf: " + err.Error()) + break } - - srv.processForwardResponse(req, res) - } -} - -func (srv *Server) processForwardResponse(req *dns.Request, res *dns.Message) { - if bytes.Equal(req.Message.Question.Name, res.Question.Name) { - if req.Message.Question.Type != res.Question.Type { - return - } - } - - srv.processRequestResponse(req, res) - - srv.cw.upsertQueue <- res -} - -func (srv *Server) watchResolvConf() { - watcher, err := libio.NewWatcher(srv.opts.FileResolvConf, 0) - if err != nil { - log.Fatal("! watchResolvConf: ", err) - } - - for fi := range watcher.C { - if fi == nil { - if srv.nsParents[0] == srv.opts.NSParents[0] { - continue - } - - log.Printf("= ResolvConf: file '%s' deleted\n", - srv.opts.FileResolvConf) - - srv.nsParents = srv.opts.NSParents - } else { - err := srv.loadResolvConf() - if err != nil { - log.Printf("! loadResolvConf: %s\n", err) - srv.nsParents = srv.opts.NSParents - } + if !ok { + break } - srv.stopForwarders() - err = srv.runForwarders() - if err != nil { - log.Printf("! watchResolvConf: %s\n", err) - } + srv.dns.RestartForwarders(srv.opts.NameServers, srv.opts.FallbackNS) } } diff --git a/rescached_test.go b/rescached_test.go deleted file mode 100644 index 22ab579..0000000 --- a/rescached_test.go +++ /dev/null @@ -1,473 +0,0 @@ -// Copyright 2019, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "fmt" - "log" - "net" - "os" - "regexp" - "testing" - "time" - - "github.com/shuLhan/share/lib/debug" - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -var testServer *Server // nolint: gochecknoglobals - -func TestMain(m *testing.M) { - // Make debug counted on coverage - debug.Value = 2 - - // Add response for testing non-expired message, so we can check if - // response.message.SubTTL work as expected. - msg := dns.NewMessage() - msg.Packet = []byte{ - // Header - 0x8c, 0xdb, 0x81, 0x80, - 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, - // Question - 0x07, 0x6b, 0x69, 0x6c, 0x61, 0x62, 0x69, 0x74, - 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x00, - 0x00, 0x01, 0x00, 0x01, - // Answer - 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, - 0x00, 0x00, 0x01, 0x68, - 0x00, 0x04, - 0x67, 0xc8, 0x04, 0xa2, - // OPT - 0x00, 0x00, 0x29, 0x05, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - } - err := msg.Unpack() - if err != nil { - log.Fatal(err) - } - - res := newResponse(msg) - _testResponses = append(_testResponses, res) - - testServer = New(nil) - - testServer.LoadHostsDir("testdata/hosts.d") - testServer.LoadMasterDir("testdata/master.d") - - os.Exit(m.Run()) -} - -func TestNew(t *testing.T) { - cases := []struct { - desc string - opts *Options - expOpts *Options - expNSParents string - }{{ - desc: "With nil options", - expOpts: &Options{ - ListenAddress: "127.0.0.1", - ConnType: dns.ConnTypeUDP, - NSParents: []*net.UDPAddr{{ - IP: net.ParseIP("1.1.1.1"), - Port: int(dns.DefaultPort), - }, { - IP: net.ParseIP("1.0.0.1"), - Port: int(dns.DefaultPort), - }}, - Timeout: 6 * time.Second, - CachePruneDelay: time.Hour, - CacheThreshold: -1 * time.Hour, - FilePID: "rescached.pid", - ListenPort: dns.DefaultPort, - DoHParents: []string{ - "https://cloudflare-dns.com/dns-query", - }, - }, - expNSParents: "[1.1.1.1:53 1.0.0.1:53]", - }, { - desc: "With resolv.conf file not exist", - opts: &Options{ - FileResolvConf: "testdata/notexist", - }, - expOpts: &Options{ - ListenAddress: "127.0.0.1", - ListenPort: dns.DefaultPort, - ConnType: dns.ConnTypeUDP, - Timeout: 6 * time.Second, - CachePruneDelay: time.Hour, - CacheThreshold: -1 * time.Hour, - FilePID: "rescached.pid", - FileResolvConf: "testdata/notexist", - NSParents: []*net.UDPAddr{{ - IP: net.ParseIP("1.1.1.1"), - Port: int(dns.DefaultPort), - }, { - IP: net.ParseIP("1.0.0.1"), - Port: int(dns.DefaultPort), - }}, - DoHParents: []string{ - "https://cloudflare-dns.com/dns-query", - }, - }, - expNSParents: "[1.1.1.1:53 1.0.0.1:53]", - }, { - desc: "With testdata/resolv.conf.empty", - opts: &Options{ - FileResolvConf: "testdata/resolv.conf.empty", - }, - expOpts: &Options{ - ListenAddress: "127.0.0.1", - ListenPort: dns.DefaultPort, - ConnType: dns.ConnTypeUDP, - Timeout: 6 * time.Second, - CachePruneDelay: time.Hour, - CacheThreshold: -1 * time.Hour, - FilePID: "rescached.pid", - FileResolvConf: "testdata/resolv.conf.empty", - NSParents: []*net.UDPAddr{{ - IP: net.ParseIP("1.1.1.1"), - Port: int(dns.DefaultPort), - }, { - IP: net.ParseIP("1.0.0.1"), - Port: int(dns.DefaultPort), - }}, - DoHParents: []string{ - "https://cloudflare-dns.com/dns-query", - }, - }, - expNSParents: "[1.1.1.1:53 1.0.0.1:53]", - }, { - desc: "With testdata/resolv.conf", - opts: &Options{ - FileResolvConf: "testdata/resolv.conf", - }, - expOpts: &Options{ - ListenAddress: "127.0.0.1", - ListenPort: dns.DefaultPort, - ConnType: dns.ConnTypeUDP, - Timeout: 6 * time.Second, - CachePruneDelay: time.Hour, - CacheThreshold: -1 * time.Hour, - FilePID: "rescached.pid", - FileResolvConf: "testdata/resolv.conf", - NSParents: []*net.UDPAddr{{ - IP: net.ParseIP("1.1.1.1"), - Port: int(dns.DefaultPort), - }, { - IP: net.ParseIP("1.0.0.1"), - Port: int(dns.DefaultPort), - }}, - DoHParents: []string{ - "https://cloudflare-dns.com/dns-query", - }, - }, - expNSParents: "[192.168.1.1:53]", - }} - - for _, c := range cases { - t.Log(c.desc) - - srv := New(c.opts) - - test.Assert(t, "Options", c.expOpts, srv.opts, true) - gotNSParents := fmt.Sprintf("%s", srv.nsParents) - test.Assert(t, "NSParents", c.expNSParents, gotNSParents, true) - } -} - -func TestLoadHostsDir(t *testing.T) { - cases := []struct { - desc string - dir string - expCaches string - }{{ - desc: "With empty directory", - expCaches: `caches\[\]`, - }, { - desc: "With directory not exist", - dir: "testdata/notexist", - expCaches: `caches\[\]`, - }, { - desc: "With non empty directory", - dir: "testdata/hosts.d", - expCaches: `caches\[1.test:\[{0 \d+ &{Name:1.test Type:A}}\] 2.test:\[{0 \d+ &{Name:2.test Type:A}}\]\]`, // nolint: lll - }} - - srv := New(nil) - - for _, c := range cases { - t.Log(c.desc) - - srv.LoadHostsDir(c.dir) - - gotCaches := srv.cw.caches.String() - re, err := regexp.Compile(c.expCaches) - if err != nil { - t.Fatal(err) - } - if !re.MatchString(gotCaches) { - t.Fatalf("Expecting caches:\n\t%s\n got:\n%s\n", - c.expCaches, gotCaches) - } - } -} - -func TestLoadMasterDir(t *testing.T) { - cases := []struct { - desc string - dir string - expCaches string - }{{ - desc: "With empty directory", - expCaches: `caches\[\]`, - }, { - desc: "With directory not exist", - dir: "testdata/notexist", - expCaches: `caches\[\]`, - }, { - desc: "With non empty directory", - dir: "testdata/master.d", - expCaches: `caches\[test.x:\[{0 \d+ &{Name:test.x Type:A}}\]\]`, // nolint: lll - }} - - srv := New(nil) - - for _, c := range cases { - t.Log(c.desc) - - srv.LoadMasterDir(c.dir) - - gotCaches := srv.cw.caches.String() - re, err := regexp.Compile(c.expCaches) - if err != nil { - t.Fatal(err) - } - if !re.MatchString(gotCaches) { - t.Fatalf("Expecting caches:\n\t%s\n got:\n%s\n", - c.expCaches, gotCaches) - } - } -} - -func TestWritePID(t *testing.T) { - cases := []struct { - desc string - filePID string - expErr string - }{{ - desc: "With empty PID", - expErr: "open : no such file or directory", - }, { - desc: "With PID file not exist", - filePID: "testdata/test.pid", - }, { - desc: "With PID file exist", - filePID: "testdata/test.pid", - expErr: "writePID: PID file 'testdata/test.pid' exist", - }} - - srv := New(nil) - - srv.opts.FilePID = "testdata/test.pid" - srv.RemovePID() - - for _, c := range cases { - t.Log(c.desc) - - srv.opts.FilePID = c.filePID - - err := srv.WritePID() - if err != nil { - test.Assert(t, "error", c.expErr, err.Error(), true) - } - } -} - -func TestProcessRequest(t *testing.T) { - cases := []struct { - desc string - req *dns.Request - expFw *dns.Request - expFwDoH *dns.Request - }{{ - desc: "With nil request", - }, { - desc: "With request type UDP and not exist in cache", - req: &dns.Request{ - Kind: dns.ConnTypeUDP, - Message: &dns.Message{ - Header: &dns.SectionHeader{}, - Question: &dns.SectionQuestion{ - Name: []byte("notexist"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - }, - expFw: &dns.Request{ - Kind: dns.ConnTypeUDP, - Message: &dns.Message{ - Header: &dns.SectionHeader{}, - Question: &dns.SectionQuestion{ - Name: []byte("notexist"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - }, - }, { - desc: "With request type UDP and not exist in cache (again)", - req: &dns.Request{ - Kind: dns.ConnTypeUDP, - Message: &dns.Message{ - Header: &dns.SectionHeader{}, - Question: &dns.SectionQuestion{ - Name: []byte("notexist"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - }, - }, { - desc: "With request type DoH and not exist in cache", - req: &dns.Request{ - Kind: dns.ConnTypeDoH, - Message: &dns.Message{ - Header: &dns.SectionHeader{}, - Question: &dns.SectionQuestion{ - Name: []byte("doh"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - }, - expFwDoH: &dns.Request{ - Kind: dns.ConnTypeDoH, - Message: &dns.Message{ - Header: &dns.SectionHeader{}, - Question: &dns.SectionQuestion{ - Name: []byte("doh"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - }, - }, { - desc: "With request type UDP and exist in cache", - req: &dns.Request{ - Kind: dns.ConnTypeUDP, - Message: &dns.Message{ - Header: &dns.SectionHeader{ - ID: 1000, - }, - Question: &dns.SectionQuestion{ - Name: []byte("1.test"), - Type: dns.QueryTypeA, - Class: dns.QueryClassIN, - }, - }, - }, - }} - - for _, c := range cases { - t.Log(c.desc) - - testServer.processRequest(c.req) - - if c.expFw != nil { - gotFw := <-testServer.fwQueue - test.Assert(t, "forward queue", c.req, gotFw, true) - } - if c.expFwDoH != nil { - gotFw := <-testServer.fwDoHQueue - test.Assert(t, "forward DoH", c.req, gotFw, true) - } - } -} - -// -// This test push request with UDP connection and read the response back. -// -func TestProcessRequestUDP(t *testing.T) { - udpClient, err := dns.NewUDPClient("127.0.0.1:53") - if err != nil { - t.Fatal("TestProcessRequestQueueUDP:", err) - } - - clLocalAddr := udpClient.Conn.LocalAddr().(*net.UDPAddr) - - cases := []struct { - qname string - exp string - id uint16 - qtype uint16 - }{{ - id: 1000, - qname: "1.test", - qtype: dns.QueryTypeA, - exp: "127.0.0.1", - }, { - id: 1001, - qname: "2.test", - qtype: dns.QueryTypeA, - exp: "127.0.0.2", - }, { - id: 1002, - qname: "test.x", - qtype: dns.QueryTypeA, - exp: "127.0.0.3", - }} - - for _, c := range cases { - msg := &dns.Message{ - Header: &dns.SectionHeader{ - ID: c.id, - IsQuery: true, - QDCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte(c.qname), - Type: c.qtype, - Class: dns.QueryClassIN, - }, - } - - _, err = msg.Pack() - if err != nil { - t.Fatal("msg.Pack:", err) - } - - req := &dns.Request{ - Kind: dns.ConnTypeUDP, - UDPAddr: &net.UDPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: clLocalAddr.Port, - }, - Sender: udpClient, - Message: msg, - } - - testServer.processRequest(req) - - res := dns.NewMessage() - - _, err = udpClient.Recv(res) - if err != nil { - t.Fatal("udp client.Recv:", err) - } - - err = res.Unpack() - if err != nil { - t.Fatal("dns.Message.Unpack:", err) - } - - test.Assert(t, "id", c.id, res.Header.ID, true) - - got := string(res.Answer[0].RData().([]byte)) - test.Assert(t, "answer", c.exp, got, true) - } -} diff --git a/response.go b/response.go deleted file mode 100644 index 016f232..0000000 --- a/response.go +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "container/list" - "fmt" - "sync/atomic" - "time" - - "github.com/shuLhan/share/lib/debug" - "github.com/shuLhan/share/lib/dns" -) - -// -// response represent internal DNS response for caching. -// -type response struct { - // Time when message is received. - receivedAt int64 - // Time when message last accessed in cache. - accessedAt int64 - message *dns.Message - // Pointer to response element in list. - el *list.Element -} - -func newResponse(msg *dns.Message) *response { - curtime := time.Now().Unix() - return &response{ - receivedAt: curtime, - accessedAt: curtime, - message: msg, - } -} - -// -// AccessedAt return the timestamp when response last accessed in cache. -// -func (res *response) AccessedAt() int64 { - return atomic.LoadInt64(&res.accessedAt) -} - -// -// String return the interpretation of response as text. -// The message field only representated by the Question section. -// -func (res *response) String() string { - return fmt.Sprintf("{%d %d %s}", res.receivedAt, res.accessedAt, - res.message.Question.String()) -} - -// -// isExpired will return true if response message is expired, otherwise -// it will return false. -// If response is not expired, all TTL in RR will be decreased to current time -// minus time they were received. -// -func (res *response) isExpired() bool { - // Local responses from hosts file will never be expired. - if res.receivedAt == 0 { - return false - } - - timeNow := time.Now().Unix() - elapSeconds := uint32(timeNow - res.receivedAt) - res.receivedAt = timeNow - - if res.message.IsExpired(elapSeconds) { - if debug.Value >= 1 { - fmt.Printf("- expired: Elaps:%-4d ID:%-5d %s\n", - elapSeconds, - res.message.Header.ID, res.message.Question) - } - - return true - } - - res.message.SubTTL(elapSeconds) - - return false -} - -func (res *response) update(newMsg *dns.Message) *dns.Message { - oldMsg := res.message - atomic.StoreInt64(&res.accessedAt, time.Now().Unix()) - res.message = newMsg - return oldMsg -} diff --git a/response_test.go b/response_test.go deleted file mode 100644 index bb14348..0000000 --- a/response_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package rescached - -import ( - "testing" - "time" - - "github.com/shuLhan/share/lib/dns" - "github.com/shuLhan/share/lib/test" -) - -var _testResponses = []*response{{ // nolint - accessedAt: 0, - receivedAt: 0, - message: &dns.Message{ - Packet: []byte{1}, - Header: &dns.SectionHeader{ - ANCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("1"), - Type: 1, - Class: 1, - }, - Answer: []*dns.ResourceRecord{{ - TTL: 1, - }}, - }, -}, { - accessedAt: 1, - receivedAt: time.Now().Unix() - 1, - message: &dns.Message{ - Packet: []byte{2}, - Header: &dns.SectionHeader{ - ANCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("2"), - Type: 2, - Class: 1, - }, - Answer: []*dns.ResourceRecord{{ - TTL: 1, - }}, - }, -}, { - accessedAt: 2, - message: &dns.Message{ - Packet: []byte{1, 1}, - Header: &dns.SectionHeader{ - ANCount: 1, - }, - Question: &dns.SectionQuestion{ - Name: []byte("1"), - Type: 1, - Class: 1, - }, - Answer: []*dns.ResourceRecord{{ - TTL: 1, - }}, - }, -}} - -func TestResponseAccessedAt(t *testing.T) { - cases := []struct { - desc string - res *response - exp int64 - }{{ - desc: "With accessedAt is 0", - res: _testResponses[0], - }, { - desc: "With accessedAt is 1", - res: _testResponses[1], - exp: 1, - }, { - desc: "With accessedAt is 2", - res: _testResponses[2], - exp: 2, - }} - - for _, c := range cases { - t.Log(c.desc) - - test.Assert(t, "AccessedAt", c.exp, c.res.AccessedAt(), true) - } -} - -func TestResponseIsExpired(t *testing.T) { - cases := []struct { - desc string - res *response - exp bool - }{{ - desc: "With local response", - res: _testResponses[0], - exp: false, - }, { - desc: "With one answer expired", - res: _testResponses[1], - exp: true, - }, { - desc: "With no expiration", - res: _testResponses[3], - exp: false, - }} - - for _, c := range cases { - t.Log(c.desc) - - test.Assert(t, "isExpired", c.exp, c.res.isExpired(), true) - } -} - -func TestResponseUpdate(t *testing.T) { - res := _testResponses[0] - orgMsg := res.message - - cases := []struct { - desc string - msg *dns.Message - exp *dns.Message - }{{ - desc: "With empty message", - exp: _testResponses[0].message, - }, { - desc: "With non empty message", - msg: _testResponses[2].message, - exp: nil, - }} - - for _, c := range cases { - t.Log(c.desc) - - got := res.update(c.msg) - - test.Assert(t, "update", c.exp, got, true) - } - - res.message = orgMsg -} -- cgit v1.3