Files
coredns/plugin/cache/item.go
Nitin Nizhawan 87ccb6f90e plugin/cache: add prefer_positive stale policy (#8378)
* plugin/cache: add prefer_positive stale policy

Add an opt-in serve_stale_policy that prefers an eligible success-cache
answer over denial-cache entries while serve_stale is enabled. Preserve the
existing ncache-first behavior when the policy is absent.

Also classify SOA-backed CNAME NODATA responses in the cache so incomplete
answers cannot be selected as positive stale responses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6
Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com>

* plugin/cache: retain last-known-good positive answers

Keep an answering success-cache item reachable when a later NOERROR or
referral response overwrites the visible cache key without answering the
question. This lets prefer_positive survive empty responses, referrals, and
additional-only data while leaving policy-off lookup behavior unchanged.

Return the exact accepted verify refresh item instead of re-reading an
ambiguous cache key, avoiding expired TTL wraparound for uncacheable replies.
Add regression coverage for non-answer refreshes, NODATA, SERVFAIL, NOTIMP,
stale-window expiry, and bounded verify reply shaping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6
Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com>

* plugin/cache: validate preferred stale answers

Reject truncated, DNSSEC-expired, mismatched-class, unrelated ANY, and ambiguous CNAME refreshes before replacing a stale last-known-good answer. Precompute answer eligibility when cache items are created so prefer_positive hits avoid repeated CNAME walks.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6
Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com>

---------

Signed-off-by: Nitin Nizhawan <nnizhawan@microsoft.com>
Co-authored-by: Nitin Nizhawan <nnizhawan@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 25da81ab-92dd-4663-b480-efd6262090c6
2026-08-14 01:18:39 -07:00

158 lines
4.7 KiB
Go

package cache
import (
"strings"
"sync/atomic"
"time"
"github.com/coredns/coredns/plugin/cache/freq"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
type item struct {
Name string
QType uint16
QClass uint16
Rcode int
AuthenticatedData bool
RecursionAvailable bool
Answer []dns.RR
Ns []dns.RR
Extra []dns.RR
wildcard string
answering bool // immutable result of validating that this item answers its question.
lastKnownGood *item // answering item retained when a non-answer overwrites this success-cache key.
origTTL uint32
stored time.Time
*freq.Freq
// refreshing bounds in-flight refreshes for this item to one. retryAfter
// suppresses another attempt after a failed refresh when failure recheck
// is configured. A successful refresh normally replaces this item with a
// new one whose refresh state is zero-valued.
refreshing atomic.Bool
retryAfter atomic.Pointer[time.Time]
}
func newItem(m *dns.Msg, now time.Time, d time.Duration) *item {
i := new(item)
if len(m.Question) != 0 {
i.Name = m.Question[0].Name
i.QType = m.Question[0].Qtype
i.QClass = m.Question[0].Qclass
}
i.Rcode = m.Rcode
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 records 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.answering = answersQuestion(m)
i.origTTL = uint32(d.Seconds())
// Keep the monotonic clock reading so TTL expiry is unaffected by wall
// clock adjustments.
i.stored = now
i.Freq = new(freq.Freq)
return i
}
// toMsg turns i into a message, it tailors the reply to m.
// The Authoritative bit should be set to 0, but some client stub resolver implementations, most notably,
// on some legacy systems(e.g. ubuntu 14.04 with glib version 2.20), low-level glibc function `getaddrinfo`
// useb by Python/Ruby/etc.. will discard answers that do not have this bit set.
// So we're forced to always set this to 1; regardless if the answer came from the cache or not.
// On newer systems(e.g. ubuntu 16.04 with glib version 2.23), this issue is resolved.
// So we may set this bit back to 0 in the future ?
func (i *item) toMsg(m *dns.Msg, now time.Time, do bool, ad bool) *dns.Msg {
ttl := uint32(i.ttl(now)) // #nosec G115 -- ttl is bounded by DNS TTL limits
return i.toMsgWithTTL(m, ttl, do, ad)
}
// toMsgWithTTL returns the cached item with an explicit TTL on every RR.
func (i *item) toMsgWithTTL(m *dns.Msg, ttl uint32, do bool, ad bool) *dns.Msg {
m1 := new(dns.Msg)
m1.SetReply(m)
// Set this to true as some DNS clients discard the *entire* packet when it's non-authoritative.
// This is probably not according to spec, but the bit itself is not super useful as this point, so
// just set it to true.
m1.Authoritative = true
m1.AuthenticatedData = i.AuthenticatedData
if !do && !ad {
// When DNSSEC was not wanted, it can't be authenticated data.
// However, retain the AD bit if the requester set the AD bit, per RFC6840 5.7-5.8
m1.AuthenticatedData = false
}
m1.RecursionAvailable = i.RecursionAvailable
m1.Rcode = i.Rcode
m1.Answer = filterRRSlice(i.Answer, ttl, true)
m1.Ns = filterRRSlice(i.Ns, ttl, true)
m1.Extra = filterRRSlice(i.Extra, ttl, true)
return m1
}
func (i *item) ttl(now time.Time) int {
ttl := int(i.origTTL) - int(now.Sub(i.stored).Seconds())
return ttl
}
func (i *item) matches(state request.Request) bool {
if state.QType() == i.QType && state.QClass() == i.QClass && strings.EqualFold(state.QName(), i.Name) {
return true
}
return false
}
func (i *item) answersQuestion(state request.Request) bool {
return i.answering && i.matches(state)
}
func (i *item) answeringItem(state request.Request) *item {
if i.answersQuestion(state) {
return i
}
if i.lastKnownGood != nil && i.lastKnownGood.answersQuestion(state) {
return i.lastKnownGood
}
return nil
}
func (i *item) beginRefresh(now time.Time, failureRecheck time.Duration) bool {
if failureRecheck > 0 {
if retryAfter := i.retryAfter.Load(); retryAfter != nil && now.Before(*retryAfter) {
return false
}
}
return i.refreshing.CompareAndSwap(false, true)
}
func (i *item) endRefresh(now time.Time, failureRecheck time.Duration, refreshed bool) {
if failureRecheck > 0 && !refreshed {
retryAfter := now.Add(failureRecheck)
i.retryAfter.Store(&retryAfter)
} else {
i.retryAfter.Store(nil)
}
// Publish the retry deadline before allowing another refresh to start.
i.refreshing.Store(false)
}