mirror of
https://github.com/coredns/coredns.git
synced 2026-06-15 13:40:11 -04:00
* plugin/cache: allow cache TTLs above default 3600s This change allows the cache plugin to honor configured maximum TTL values above the default 3600s limit. Default behavior remains unchanged This PR fixes 7846 Signed-off-by: Yong Tang <yong.tang.github@outlook.com> * Keep MinimalTTL Signed-off-by: Yong Tang <yong.tang.github@outlook.com> --------- Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
357 lines
9.9 KiB
Go
357 lines
9.9 KiB
Go
// Package cache implements a cache.
|
|
package cache
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"hash/fnv"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/coredns/coredns/plugin"
|
|
"github.com/coredns/coredns/plugin/pkg/cache"
|
|
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
|
"github.com/coredns/coredns/plugin/pkg/response"
|
|
"github.com/coredns/coredns/request"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// Cache is a plugin that looks up responses in a cache and caches replies.
|
|
// It has a success and a denial of existence cache.
|
|
type Cache struct {
|
|
Next plugin.Handler
|
|
Zones []string
|
|
|
|
zonesMetricLabel string
|
|
viewMetricLabel string
|
|
|
|
ncache *cache.Cache[*item]
|
|
ncap int
|
|
nttl time.Duration
|
|
minnttl time.Duration
|
|
|
|
pcache *cache.Cache[*item]
|
|
pcap int
|
|
pttl time.Duration
|
|
minpttl time.Duration
|
|
failttl time.Duration // TTL for caching SERVFAIL responses
|
|
|
|
// Prefetch.
|
|
prefetch int
|
|
duration time.Duration
|
|
percentage int
|
|
|
|
// Stale serve
|
|
staleUpTo time.Duration
|
|
verifyStale bool
|
|
verifyStaleTimeout time.Duration // 0 means wait for upstream until its own timeout (current default).
|
|
|
|
// Positive/negative zone exceptions
|
|
pexcept []string
|
|
nexcept []string
|
|
|
|
// Keep ttl option
|
|
keepttl bool
|
|
|
|
// Testing.
|
|
now func() time.Time
|
|
}
|
|
|
|
// New returns an initialized Cache with default settings. It's up to the
|
|
// caller to set the Next handler.
|
|
func New() *Cache {
|
|
return &Cache{
|
|
Zones: []string{"."},
|
|
pcap: defaultCap,
|
|
pcache: cache.New[*item](defaultCap),
|
|
pttl: maxTTL,
|
|
minpttl: minTTL,
|
|
ncap: defaultCap,
|
|
ncache: cache.New[*item](defaultCap),
|
|
nttl: maxNTTL,
|
|
minnttl: minNTTL,
|
|
failttl: minNTTL,
|
|
prefetch: 0,
|
|
duration: 1 * time.Minute,
|
|
percentage: 10,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
// key returns key under which we store the item, -1 will be returned if we don't store the message.
|
|
// Currently we do not cache Truncated, errors zone transfers or dynamic update messages.
|
|
// qname holds the already lowercased qname.
|
|
func key(qname string, m *dns.Msg, t response.Type, do, cd bool) (bool, uint64) {
|
|
// We don't store truncated responses.
|
|
if m.Truncated {
|
|
return false, 0
|
|
}
|
|
// Nor errors or Meta or Update.
|
|
if t == response.OtherError || t == response.Meta || t == response.Update {
|
|
return false, 0
|
|
}
|
|
|
|
return true, hash(qname, m.Question[0].Qtype, do, cd)
|
|
}
|
|
|
|
var one = []byte("1")
|
|
var zero = []byte("0")
|
|
|
|
func hash(qname string, qtype uint16, do, cd bool) uint64 {
|
|
h := fnv.New64()
|
|
|
|
if do {
|
|
h.Write(one)
|
|
} else {
|
|
h.Write(zero)
|
|
}
|
|
|
|
if cd {
|
|
h.Write(one)
|
|
} else {
|
|
h.Write(zero)
|
|
}
|
|
|
|
var qtypeBytes [2]byte
|
|
binary.BigEndian.PutUint16(qtypeBytes[:], qtype)
|
|
h.Write(qtypeBytes[:])
|
|
h.Write([]byte(qname))
|
|
return h.Sum64()
|
|
}
|
|
|
|
func computeTTL(msgTTL, minTTL, maxTTL time.Duration) time.Duration {
|
|
ttl := min(max(msgTTL, minTTL), maxTTL)
|
|
return ttl
|
|
}
|
|
|
|
// ResponseWriter is a response writer that caches the reply message.
|
|
type ResponseWriter struct {
|
|
dns.ResponseWriter
|
|
*Cache
|
|
state request.Request
|
|
server string // Server handling the request.
|
|
|
|
do bool // When true the original request had the DO bit set.
|
|
cd bool // When true the original request had the CD bit set.
|
|
ad bool // When true the original request had the AD bit set.
|
|
prefetch bool // When true write nothing back to the client.
|
|
remoteAddr net.Addr
|
|
|
|
wildcardFunc func() string // function to retrieve wildcard name that synthesized the result.
|
|
|
|
pexcept []string // positive zone exceptions
|
|
nexcept []string // negative zone exceptions
|
|
}
|
|
|
|
// prefetchAddr is the synthetic remote address for prefetch requests. There is
|
|
// no client connection, and per request.Proto the address type is what selects
|
|
// the response-size budget; TCP ensures upstream replies aren't truncated.
|
|
var prefetchAddr = &net.TCPAddr{}
|
|
|
|
// newPrefetchResponseWriter returns a ResponseWriter for prefetch requests.
|
|
// Prefetch has no client connection: the inner ResponseWriter is nil, WriteMsg
|
|
// short-circuits after caching when w.prefetch is true, and the nil-safe
|
|
// overrides below make the remaining dns.ResponseWriter methods well-defined.
|
|
func newPrefetchResponseWriter(server string, req *dns.Msg, do, cd bool, c *Cache) *ResponseWriter {
|
|
cw := &ResponseWriter{
|
|
Cache: c,
|
|
server: server,
|
|
do: do,
|
|
cd: cd,
|
|
prefetch: true,
|
|
remoteAddr: prefetchAddr,
|
|
}
|
|
cw.state = request.Request{Req: req}
|
|
return cw
|
|
}
|
|
|
|
// RemoteAddr implements the dns.ResponseWriter interface.
|
|
func (w *ResponseWriter) RemoteAddr() net.Addr {
|
|
if w.remoteAddr != nil {
|
|
return w.remoteAddr
|
|
}
|
|
return w.ResponseWriter.RemoteAddr()
|
|
}
|
|
|
|
// The following overrides make a nil inner ResponseWriter well-defined.
|
|
// Prefetch constructs a ResponseWriter with no client connection; WriteMsg
|
|
// and Write already short-circuit on w.prefetch before delegating, and
|
|
// RemoteAddr uses w.remoteAddr. These cover the rest of the interface.
|
|
|
|
func (w *ResponseWriter) LocalAddr() net.Addr {
|
|
if w.ResponseWriter == nil {
|
|
return prefetchAddr
|
|
}
|
|
return w.ResponseWriter.LocalAddr()
|
|
}
|
|
|
|
func (w *ResponseWriter) Close() error {
|
|
if w.ResponseWriter == nil {
|
|
return nil
|
|
}
|
|
return w.ResponseWriter.Close()
|
|
}
|
|
|
|
func (w *ResponseWriter) TsigStatus() error {
|
|
if w.ResponseWriter == nil {
|
|
return nil
|
|
}
|
|
return w.ResponseWriter.TsigStatus()
|
|
}
|
|
|
|
func (w *ResponseWriter) TsigTimersOnly(b bool) {
|
|
if w.ResponseWriter == nil {
|
|
return
|
|
}
|
|
w.ResponseWriter.TsigTimersOnly(b)
|
|
}
|
|
|
|
func (w *ResponseWriter) Hijack() {
|
|
if w.ResponseWriter == nil {
|
|
return
|
|
}
|
|
w.ResponseWriter.Hijack()
|
|
}
|
|
|
|
// WriteMsg implements the dns.ResponseWriter interface.
|
|
func (w *ResponseWriter) WriteMsg(res *dns.Msg) error {
|
|
res = res.Copy()
|
|
mt, _ := response.Typify(res, w.now().UTC())
|
|
|
|
// key returns empty string for anything we don't want to cache.
|
|
hasKey, key := key(w.state.Name(), res, mt, w.do, w.cd)
|
|
|
|
var duration time.Duration
|
|
switch mt {
|
|
case response.NameError, response.NoData:
|
|
msgTTL := dnsutil.MinimalTTLWithMaximum(res, mt, w.nttl)
|
|
duration = computeTTL(msgTTL, w.minnttl, w.nttl)
|
|
case response.ServerError:
|
|
duration = w.failttl
|
|
default:
|
|
msgTTL := dnsutil.MinimalTTLWithMaximum(res, mt, w.pttl)
|
|
duration = computeTTL(msgTTL, w.minpttl, w.pttl)
|
|
}
|
|
|
|
// Apply capped TTL to this reply to avoid jarring TTL experience 1799 -> 8 (e.g.)
|
|
ttl := uint32(duration.Seconds())
|
|
res.Answer = filterRRSlice(res.Answer, ttl, false)
|
|
res.Ns = filterRRSlice(res.Ns, ttl, false)
|
|
res.Extra = filterRRSlice(res.Extra, ttl, false)
|
|
|
|
if !w.do && !w.ad {
|
|
// unset AD bit if requester is not OK with DNSSEC
|
|
// But retain AD bit if requester set the AD bit in the request, per RFC6840 5.7-5.8
|
|
res.AuthenticatedData = false
|
|
}
|
|
|
|
if hasKey && duration > 0 {
|
|
if w.state.Match(res) {
|
|
w.set(res, key, mt, duration)
|
|
cacheSize.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.pcache.Len()))
|
|
cacheSize.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Set(float64(w.ncache.Len()))
|
|
} else {
|
|
// Don't log it, but increment counter
|
|
cacheDrops.WithLabelValues(w.server, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
|
}
|
|
}
|
|
|
|
if w.prefetch {
|
|
return nil
|
|
}
|
|
|
|
return w.ResponseWriter.WriteMsg(res)
|
|
}
|
|
|
|
func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration time.Duration) {
|
|
// duration is expected > 0
|
|
// and key is valid
|
|
switch mt {
|
|
case response.NoError, response.Delegation:
|
|
if plugin.Zones(w.pexcept).Matches(m.Question[0].Name) != "" {
|
|
// zone is in exception list, do not cache
|
|
return
|
|
}
|
|
i := newItem(m, w.now(), duration)
|
|
if w.wildcardFunc != nil {
|
|
i.wildcard = w.wildcardFunc()
|
|
}
|
|
if w.pcache.Add(key, i) {
|
|
evictions.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
|
}
|
|
// when pre-fetching, remove the negative cache entry if it exists
|
|
if w.prefetch {
|
|
w.ncache.Remove(key)
|
|
}
|
|
|
|
case response.NameError, response.NoData, response.ServerError:
|
|
if plugin.Zones(w.nexcept).Matches(m.Question[0].Name) != "" {
|
|
// zone is in exception list, do not cache
|
|
return
|
|
}
|
|
i := newItem(m, w.now(), duration)
|
|
if w.wildcardFunc != nil {
|
|
i.wildcard = w.wildcardFunc()
|
|
}
|
|
if w.ncache.Add(key, i) {
|
|
evictions.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
|
}
|
|
|
|
case response.OtherError:
|
|
// don't cache these
|
|
default:
|
|
log.Warningf("Caching called with unknown classification: %d", mt)
|
|
}
|
|
}
|
|
|
|
// Write implements the dns.ResponseWriter interface.
|
|
func (w *ResponseWriter) Write(buf []byte) (int, error) {
|
|
log.Warning("Caching called with Write: not caching reply")
|
|
if w.prefetch {
|
|
return 0, nil
|
|
}
|
|
n, err := w.ResponseWriter.Write(buf)
|
|
return n, err
|
|
}
|
|
|
|
// verifyStaleResponseWriter is a response writer that only writes messages if they should replace a
|
|
// stale cache entry, and otherwise discards them.
|
|
type verifyStaleResponseWriter struct {
|
|
*ResponseWriter
|
|
refreshed bool // set to true if the last WriteMsg wrote to ResponseWriter, false otherwise.
|
|
}
|
|
|
|
// newVerifyStaleResponseWriter returns a ResponseWriter to be used when verifying stale cache
|
|
// entries. It only forward writes if an entry was successfully refreshed according to RFC8767,
|
|
// section 4 (response is NoError or NXDomain), and ignores any other response.
|
|
func newVerifyStaleResponseWriter(w *ResponseWriter) *verifyStaleResponseWriter {
|
|
return &verifyStaleResponseWriter{
|
|
w,
|
|
false,
|
|
}
|
|
}
|
|
|
|
// WriteMsg implements the dns.ResponseWriter interface.
|
|
func (w *verifyStaleResponseWriter) WriteMsg(res *dns.Msg) error {
|
|
w.refreshed = false
|
|
if res.Rcode == dns.RcodeSuccess || res.Rcode == dns.RcodeNameError {
|
|
w.refreshed = true
|
|
return w.ResponseWriter.WriteMsg(res) // stores to the cache and send to client
|
|
}
|
|
return nil // else discard
|
|
}
|
|
|
|
const (
|
|
maxTTL = dnsutil.MaximumDefaultTTL
|
|
minTTL = dnsutil.MinimalDefaultTTL
|
|
maxNTTL = dnsutil.MaximumDefaultTTL / 2
|
|
minNTTL = dnsutil.MinimalDefaultTTL
|
|
|
|
defaultCap = 10000 // default capacity of the cache.
|
|
|
|
// Success is the class for caching positive caching.
|
|
Success = "success"
|
|
// Denial is the class defined for negative caching.
|
|
Denial = "denial"
|
|
)
|