* Add middleware/cache

Add a caching middleware that caches nxdomain, nodata and successful
responses. It differentiates between DNSSEC on normal DNS replies.

Each reply is compress and scrubbed so it will fit the specific client
asking for it.

* first simple test, less exporting of stuff

* more

* Add middleware/cache

Add a caching middleware that caches nxdomain, nodata and successful
responses. It differentiates between DNSSEC on normal DNS replies.

Each reply is compressed and scrubbed so it will fit the specific client
asking for it. The TTL is decremented with the time spend in the cache.
There is syntax that allows you to cap the TTL for all records, no
matter what. This allows for a shortlived cache, just to absorb query
peaks.

+Tests

* cache test infrastructure

* Testing
This commit is contained in:
Miek Gieben 2016-04-19 11:13:24 +01:00
parent 4e3c82bec5
commit 10db2a80df
12 changed files with 566 additions and 12 deletions

View file

@ -57,6 +57,7 @@ var directiveOrder = []directive{
{"chaos", setup.Chaos}, {"chaos", setup.Chaos},
{"rewrite", setup.Rewrite}, {"rewrite", setup.Rewrite},
{"loadbalance", setup.Loadbalance}, {"loadbalance", setup.Loadbalance},
{"cache", setup.Cache},
{"file", setup.File}, {"file", setup.File},
{"secondary", setup.Secondary}, {"secondary", setup.Secondary},
{"etcd", setup.Etcd}, {"etcd", setup.Etcd},

54
core/setup/cache.go Normal file
View file

@ -0,0 +1,54 @@
package setup
import (
"strconv"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/cache"
)
// Cache sets up the root file path of the server.
func Cache(c *Controller) (middleware.Middleware, error) {
ttl, zones, err := cacheParse(c)
if err != nil {
return nil, err
}
return func(next middleware.Handler) middleware.Handler {
return cache.NewCache(ttl, zones, next)
}, nil
}
func cacheParse(c *Controller) (int, []string, error) {
var (
err error
ttl int
)
for c.Next() {
if c.Val() == "cache" {
// cache [ttl] [zones..]
origins := []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
args := c.RemainingArgs()
if len(args) > 0 {
origins = args
// first args may be just a number, then it is the ttl, if not it is a zone
t := origins[0]
ttl, err = strconv.Atoi(t)
if err == nil {
origins = origins[1:]
if len(origins) == 0 {
// There was *only* the ttl, revert back to server block
origins = []string{c.ServerBlockHosts[c.ServerBlockHostIndex]}
}
}
}
for i, _ := range origins {
origins[i] = middleware.Host(origins[i]).Normalize()
}
return ttl, origins, nil
}
}
return 0, nil, nil
}

View file

@ -40,7 +40,7 @@ func fileParse(c *Controller) (file.Zones, error) {
names := []string{} names := []string{}
for c.Next() { for c.Next() {
if c.Val() == "file" { if c.Val() == "file" {
// file db.file [origin] // file db.file [zones...]
if !c.NextArg() { if !c.NextArg() {
return file.Zones{}, c.ArgErr() return file.Zones{}, c.ArgErr()
} }
@ -83,7 +83,6 @@ func fileParse(c *Controller) (file.Zones, error) {
} }
z[origin].NoReload = noReload z[origin].NoReload = noReload
} }
} }
} }
} }

View file

@ -5,15 +5,12 @@ import (
"github.com/miekg/coredns/middleware/loadbalance" "github.com/miekg/coredns/middleware/loadbalance"
) )
// Root sets up the root file path of the server. // Loadbalance sets up the root file path of the server.
func Loadbalance(c *Controller) (middleware.Middleware, error) { func Loadbalance(c *Controller) (middleware.Middleware, error) {
for c.Next() { for c.Next() {
// and choosing the correct balancer
// TODO(miek): block and option parsing // TODO(miek): block and option parsing
} }
return func(next middleware.Handler) middleware.Handler { return func(next middleware.Handler) middleware.Handler {
return loadbalance.RoundRobin{Next: next} return loadbalance.RoundRobin{Next: next}
}, nil }, nil
return nil, nil
} }

29
middleware/cache/README.md vendored Normal file
View file

@ -0,0 +1,29 @@
# cache
`cache` enables a frontend cache.
## Syntax
~~~
cache [ttl] [zones...]
~~~
* `ttl` max TTL in seconds, if not specified the TTL of the reply (SOA minimum or minimum TTL in the
answer section) will be used.
* `zones` zones it should should cache for. If empty the zones from the configuration block are used.
Each element in the cache is cached according to its TTL, for the negative cache the SOA's MinTTL
value is used.
A cache mostly makes sense with a middleware that is potentially slow, i.e. a proxy that retrieves
answer, or to minimize backend queries for middleware like etcd. Using a cache with the file
middleware essentially doubles the memory load with no concealable increase of query speed.
## Examples
~~~
cache
~~~
Enable caching for all zones.

196
middleware/cache/cache.go vendored Normal file
View file

@ -0,0 +1,196 @@
package cache
/*
The idea behind this implementation is as follows. We have a cache that is index
by a couple different keys, which allows use to have:
- negative cache: qname only for NXDOMAIN responses
- negative cache: qname + qtype for NODATA responses
- positive cache: qname + qtype for succesful responses.
We track DNSSEC responses separately, i.e. under a different cache key.
Each Item stored contains the message split up in the different sections
and a few bits of the msg header.
For instance an NXDOMAIN for blaat.miek.nl will create the
following negative cache entry (do signal state of DO (do off, DO on)).
ncache: do <blaat.miek.nl>
Item:
Ns: <miek.nl> SOA RR
If found a return packet is assembled and returned to the client. Taking size and EDNS0
constraints into account.
We also need to track if the answer received was an authoritative answer, ad bit and other
setting, for this we also store a few header bits.
For the positive cache we use the same idea. Truncated responses are never stored.
*/
import (
"log"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
gcache "github.com/patrickmn/go-cache"
)
// Cache is middleware that looks up responses in a cache and caches replies.
type Cache struct {
Next middleware.Handler
Zones []string
cache *gcache.Cache
cap time.Duration
}
func NewCache(ttl int, zones []string, next middleware.Handler) Cache {
return Cache{Next: next, Zones: zones, cache: gcache.New(defaultDuration, purgeDuration), cap: time.Duration(ttl) * time.Second}
}
type messageType int
const (
success messageType = iota
nameError // NXDOMAIN in header, SOA in auth.
noData // NOERROR in header, SOA in auth.
otherError // Don't cache these.
)
// classify classifies a message, it returns the MessageType.
func classify(m *dns.Msg) (messageType, *dns.OPT) {
opt := m.IsEdns0()
soa := false
if m.Rcode == dns.RcodeSuccess {
return success, opt
}
for _, r := range m.Ns {
if r.Header().Rrtype == dns.TypeSOA {
soa = true
break
}
}
// Check length of different section, and drop stuff that is just to large.
if soa && m.Rcode == dns.RcodeSuccess {
return noData, opt
}
if soa && m.Rcode == dns.RcodeNameError {
return nameError, opt
}
return otherError, opt
}
func cacheKey(m *dns.Msg, t messageType, do bool) string {
if m.Truncated {
return ""
}
qtype := m.Question[0].Qtype
qname := middleware.Name(m.Question[0].Name).Normalize()
switch t {
case success:
return successKey(qname, qtype, do)
case nameError:
return nameErrorKey(qname, do)
case noData:
return noDataKey(qname, qtype, do)
case otherError:
return ""
}
return ""
}
type CachingResponseWriter struct {
dns.ResponseWriter
cache *gcache.Cache
cap time.Duration
}
func NewCachingResponseWriter(w dns.ResponseWriter, cache *gcache.Cache, cap time.Duration) *CachingResponseWriter {
return &CachingResponseWriter{w, cache, cap}
}
func (c *CachingResponseWriter) WriteMsg(res *dns.Msg) error {
do := false
mt, opt := classify(res)
if opt != nil {
do = opt.Do()
}
key := cacheKey(res, mt, do)
c.Set(res, key, mt)
if c.cap != 0 {
setCap(res, uint32(c.cap.Seconds()))
}
return c.ResponseWriter.WriteMsg(res)
}
func (c *CachingResponseWriter) Set(m *dns.Msg, key string, mt messageType) {
if key == "" {
// logger the log? TODO(miek)
return
}
duration := c.cap
switch mt {
case success:
if c.cap == 0 {
duration = minTtl(m.Answer, mt)
}
i := newItem(m, duration)
c.cache.Set(key, i, duration)
case nameError, noData:
if c.cap == 0 {
duration = minTtl(m.Ns, mt)
}
i := newItem(m, duration)
c.cache.Set(key, i, duration)
}
}
func (c *CachingResponseWriter) Write(buf []byte) (int, error) {
log.Printf("[WARNING] Caching called with Write: not caching reply")
n, err := c.ResponseWriter.Write(buf)
return n, err
}
func (c *CachingResponseWriter) Hijack() {
c.ResponseWriter.Hijack()
return
}
func minTtl(rrs []dns.RR, mt messageType) time.Duration {
if mt != success && mt != nameError && mt != noData {
return 0
}
minTtl := maxTtl
for _, r := range rrs {
switch mt {
case nameError, noData:
if r.Header().Rrtype == dns.TypeSOA {
return time.Duration(r.(*dns.SOA).Minttl) * time.Second
}
case success:
if r.Header().Ttl < minTtl {
minTtl = r.Header().Ttl
}
}
}
return time.Duration(minTtl) * time.Second
}
const (
purgeDuration = 1 * time.Minute
defaultDuration = 20 * time.Minute
baseTtl = 5 // minimum ttl that we will allow
maxTtl uint32 = 2 * 3600
)

112
middleware/cache/cache_test.go vendored Normal file
View file

@ -0,0 +1,112 @@
package cache
import (
"testing"
"time"
"github.com/miekg/coredns/middleware"
"github.com/miekg/coredns/middleware/test"
"github.com/miekg/dns"
)
type cacheTestCase struct {
test.Case
in test.Case
AuthenticatedData bool
Authoritative bool
RecursionAvailable bool
Truncated bool
}
var cacheTestCases = []cacheTestCase{
{
RecursionAvailable: true, AuthenticatedData: true, Authoritative: true,
Case: test.Case{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
},
},
in: test.Case{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{
test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx2.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 10 aspmx3.googlemail.com."),
test.MX("miek.nl. 1800 IN MX 5 alt1.aspmx.l.google.com."),
test.MX("miek.nl. 1800 IN MX 5 alt2.aspmx.l.google.com."),
},
},
},
{
Truncated: true,
Case: test.Case{
Qname: "miek.nl.", Qtype: dns.TypeMX,
Answer: []dns.RR{test.MX("miek.nl. 1800 IN MX 1 aspmx.l.google.com.")},
},
in: test.Case{},
},
}
func cacheMsg(m *dns.Msg, tc cacheTestCase) *dns.Msg {
m.RecursionAvailable = tc.RecursionAvailable
m.AuthenticatedData = tc.AuthenticatedData
m.Authoritative = tc.Authoritative
m.Truncated = tc.Truncated
m.Answer = tc.in.Answer
m.Ns = tc.in.Ns
// m.Extra = tc.in.Extra , not the OPT record!
return m
}
func newTestCache() (Cache, *CachingResponseWriter) {
c := NewCache(0, []string{"."}, nil)
crr := NewCachingResponseWriter(nil, c.cache, time.Duration(0))
return c, crr
}
func TestCache(t *testing.T) {
c, crr := newTestCache()
for _, tc := range cacheTestCases {
m := tc.in.Msg()
m = cacheMsg(m, tc)
do := tc.in.Do
mt, _ := classify(m)
key := cacheKey(m, mt, do)
crr.Set(m, key, mt)
name := middleware.Name(m.Question[0].Name).Normalize()
qtype := m.Question[0].Qtype
i, ok := c.Get(name, qtype, do)
if !ok && !m.Truncated {
t.Errorf("Truncated message should not have been cached")
}
if ok {
resp := i.toMsg(m)
if !test.Header(t, tc.Case, resp) {
t.Logf("%v\n", resp)
continue
}
if !test.Section(t, tc.Case, test.Answer, resp.Answer) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc.Case, test.Ns, resp.Ns) {
t.Logf("%v\n", resp)
}
if !test.Section(t, tc.Case, test.Extra, resp.Extra) {
t.Logf("%v\n", resp)
}
}
}
}

44
middleware/cache/handler.go vendored Normal file
View file

@ -0,0 +1,44 @@
package cache
import (
"github.com/miekg/coredns/middleware"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// ServeDNS implements the middleware.Handler interface.
func (c Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := middleware.State{W: w, Req: r}
qname := state.Name()
qtype := state.QType()
zone := middleware.Zones(c.Zones).Matches(qname)
if zone == "" {
return c.Next.ServeDNS(ctx, w, r)
}
do := state.Do() // might need more from OPT record?
if i, ok := c.Get(qname, qtype, do); ok {
resp := i.toMsg(r)
state.SizeAndDo(resp)
w.WriteMsg(resp)
return dns.RcodeSuccess, nil
}
crr := NewCachingResponseWriter(w, c.cache, c.cap)
return c.Next.ServeDNS(ctx, crr, r)
}
func (c Cache) Get(qname string, qtype uint16, do bool) (*item, bool) {
nxdomain := nameErrorKey(qname, do)
if i, ok := c.cache.Get(nxdomain); ok {
return i.(*item), true
}
successOrNoData := successKey(qname, qtype, do)
if i, ok := c.cache.Get(successOrNoData); ok {
return i.(*item), true
}
return nil, false
}

98
middleware/cache/item.go vendored Normal file
View file

@ -0,0 +1,98 @@
package cache
import (
"strconv"
"time"
"github.com/miekg/dns"
)
type item struct {
Authoritative bool
AuthenticatedData bool
RecursionAvailable bool
Answer []dns.RR
Ns []dns.RR
Extra []dns.RR
origTtl uint32
stored time.Time
}
func newItem(m *dns.Msg, d time.Duration) *item {
i := new(item)
i.Authoritative = m.Authoritative
i.AuthenticatedData = m.AuthenticatedData
i.RecursionAvailable = m.RecursionAvailable
i.Answer = m.Answer
i.Ns = m.Ns
i.Extra = make([]dns.RR, len(m.Extra))
// Don't copy OPT record as these are hop-by-hop.
j := 0
for _, e := range m.Extra {
if e.Header().Rrtype == dns.TypeOPT {
continue
}
i.Extra[j] = e
j++
}
i.Extra = i.Extra[:j]
i.origTtl = uint32(d.Seconds())
i.stored = time.Now().UTC()
return i
}
// toMsg turns i into a message, it tailers to reply to m.
func (i *item) toMsg(m *dns.Msg) *dns.Msg {
m1 := new(dns.Msg)
m1.SetReply(m)
m1.Authoritative = i.Authoritative
m1.AuthenticatedData = i.AuthenticatedData
m1.RecursionAvailable = i.RecursionAvailable
m1.Compress = true
m1.Answer = i.Answer
m1.Ns = i.Ns
m1.Extra = i.Extra
ttl := int(i.origTtl) - int(time.Now().UTC().Sub(i.stored).Seconds())
if ttl < baseTtl {
ttl = baseTtl
}
setCap(m1, uint32(ttl))
return m1
}
// setCap sets the ttl on all RRs in all sections.
func setCap(m *dns.Msg, ttl uint32) {
for _, r := range m.Answer {
r.Header().Ttl = uint32(ttl)
}
for _, r := range m.Ns {
r.Header().Ttl = uint32(ttl)
}
for _, r := range m.Extra {
r.Header().Ttl = uint32(ttl)
}
}
// nodataKey returns a caching key for NODATA responses.
func noDataKey(qname string, qtype uint16, do bool) string {
if do {
return "1" + qname + ".." + strconv.Itoa(int(qtype))
}
return "0" + qname + ".." + strconv.Itoa(int(qtype))
}
// nameErrorKey returns a caching key for NXDOMAIN responses.
func nameErrorKey(qname string, do bool) string {
if do {
return "1" + qname
}
return "0" + qname
}
// successKey returns a caching key for successfull answers.
func successKey(qname string, qtype uint16, do bool) string { return noDataKey(qname, qtype, do) }

25
middleware/cache/item_test.go vendored Normal file
View file

@ -0,0 +1,25 @@
package cache
import (
"testing"
"github.com/miekg/dns"
)
func TestKey(t *testing.T) {
if noDataKey("miek.nl.", dns.TypeMX, false) != "0miek.nl...15" {
t.Errorf("failed to create correct key")
}
if noDataKey("miek.nl.", dns.TypeMX, true) != "1miek.nl...15" {
t.Errorf("failed to create correct key")
}
if nameErrorKey("miek.nl.", false) != "0miek.nl." {
t.Errorf("failed to create correct key")
}
if nameErrorKey("miek.nl.", true) != "1miek.nl." {
t.Errorf("failed to create correct key")
}
if noDataKey("miek.nl.", dns.TypeMX, false) != successKey("miek.nl.", dns.TypeMX, false) {
t.Errorf("nameErrorKey and successKey should be the same")
}
}

View file

@ -349,15 +349,13 @@ func (e Etcd) SOA(zone string, state middleware.State) *dns.SOA {
Mbox: "hostmaster." + zone, Mbox: "hostmaster." + zone,
Ns: "ns.dns." + zone, Ns: "ns.dns." + zone,
Serial: uint32(time.Now().Unix()), Serial: uint32(time.Now().Unix()),
Refresh: 14400, Refresh: 7200,
Retry: 3600, Retry: 1800,
Expire: 604800, Expire: 86400,
Minttl: 60, Minttl: 60,
} }
} }
// NS returns the NS records from etcd.
// TODO(miek): DNSKEY and friends... intercepted by the DNSSEC middleware? // TODO(miek): DNSKEY and friends... intercepted by the DNSSEC middleware?
func isDuplicateCNAME(r *dns.CNAME, records []dns.RR) bool { func isDuplicateCNAME(r *dns.CNAME, records []dns.RR) bool {

View file

@ -3,6 +3,7 @@ package loadbalance
import ( import (
"github.com/miekg/coredns/middleware" "github.com/miekg/coredns/middleware"
"github.com/miekg/dns" "github.com/miekg/dns"
"golang.org/x/net/context" "golang.org/x/net/context"
) )
@ -12,7 +13,7 @@ type RoundRobin struct {
Next middleware.Handler Next middleware.Handler
} }
// ServeHTTP implements the middleware.Handler interface. // ServeDNS implements the middleware.Handler interface.
func (rr RoundRobin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (rr RoundRobin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
wrr := NewRoundRobinResponseWriter(w) wrr := NewRoundRobinResponseWriter(w)
return rr.Next.ServeDNS(ctx, wrr, r) return rr.Next.ServeDNS(ctx, wrr, r)