mirror of
https://github.com/coredns/coredns.git
synced 2026-08-20 23:08:28 -04:00
* 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
158 lines
4.7 KiB
Go
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)
|
|
}
|