Files
coredns/plugin/cache/cache.go
Yong Tang 3342b414e3 plugin/cache: allow cache TTLs above default 3600s (#8134)
* 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>
2026-06-06 07:48:26 +03:00

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"
)