2016-10-08 14:46:22 +01:00
|
|
|
// Package cache implements a cache.
|
2016-04-19 11:13:24 +01:00
|
|
|
package cache
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-06 21:50:24 +02:00
|
|
|
"encoding/binary"
|
2017-06-13 12:39:10 -07:00
|
|
|
"hash/fnv"
|
2018-06-19 20:50:08 +02:00
|
|
|
"net"
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
"strings"
|
2016-04-19 11:13:24 +01:00
|
|
|
"time"
|
|
|
|
|
|
2017-09-14 09:36:06 +01:00
|
|
|
"github.com/coredns/coredns/plugin"
|
|
|
|
|
"github.com/coredns/coredns/plugin/pkg/cache"
|
2018-06-27 21:12:27 +01:00
|
|
|
"github.com/coredns/coredns/plugin/pkg/dnsutil"
|
2017-09-14 09:36:06 +01:00
|
|
|
"github.com/coredns/coredns/plugin/pkg/response"
|
2018-03-25 17:11:10 +01:00
|
|
|
"github.com/coredns/coredns/request"
|
2016-04-19 11:13:24 +01:00
|
|
|
|
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
|
)
|
|
|
|
|
|
2019-09-26 20:19:45 +08:00
|
|
|
// Cache is a plugin that looks up responses in a cache and caches replies.
|
2016-10-08 15:12:28 +01:00
|
|
|
// It has a success and a denial of existence cache.
|
2016-04-19 11:13:24 +01:00
|
|
|
type Cache struct {
|
2017-09-14 09:36:06 +01:00
|
|
|
Next plugin.Handler
|
2016-04-19 11:13:24 +01:00
|
|
|
Zones []string
|
|
|
|
|
|
2022-02-14 12:10:30 -05:00
|
|
|
zonesMetricLabel string
|
2022-09-08 14:56:27 -04:00
|
|
|
viewMetricLabel string
|
2022-02-14 12:10:30 -05:00
|
|
|
|
2026-02-04 02:23:53 +01:00
|
|
|
ncache *cache.Cache[*item]
|
2018-09-03 14:26:02 -05:00
|
|
|
ncap int
|
|
|
|
|
nttl time.Duration
|
|
|
|
|
minnttl time.Duration
|
2016-10-02 08:31:44 +01:00
|
|
|
|
2026-02-04 02:23:53 +01:00
|
|
|
pcache *cache.Cache[*item]
|
2018-09-03 14:26:02 -05:00
|
|
|
pcap int
|
|
|
|
|
pttl time.Duration
|
|
|
|
|
minpttl time.Duration
|
2022-06-17 15:48:57 -04:00
|
|
|
failttl time.Duration // TTL for caching SERVFAIL responses
|
2017-06-13 12:39:10 -07:00
|
|
|
|
|
|
|
|
// Prefetch.
|
|
|
|
|
prefetch int
|
|
|
|
|
duration time.Duration
|
|
|
|
|
percentage int
|
2018-01-17 08:35:22 +01:00
|
|
|
|
2022-05-02 19:16:33 +02:00
|
|
|
// Stale serve
|
2026-05-06 13:02:28 +05:30
|
|
|
staleUpTo time.Duration
|
|
|
|
|
verifyStale bool
|
|
|
|
|
verifyStaleTimeout time.Duration // 0 means wait for upstream until its own timeout (current default).
|
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 13:48:39 +05:30
|
|
|
preferPositive bool
|
2026-08-14 14:27:42 +08:00
|
|
|
staleTTL time.Duration // TTL returned with stale responses; 0 preserves the legacy behavior.
|
|
|
|
|
staleRecheck time.Duration // Delay after a failed refresh before another attempt; 0 preserves the legacy behavior.
|
2019-11-29 11:17:50 -04:00
|
|
|
|
2022-07-28 10:51:08 -04:00
|
|
|
// Positive/negative zone exceptions
|
|
|
|
|
pexcept []string
|
|
|
|
|
nexcept []string
|
|
|
|
|
|
2023-01-27 17:35:24 +01:00
|
|
|
// Keep ttl option
|
|
|
|
|
keepttl bool
|
|
|
|
|
|
2018-01-17 08:35:22 +01:00
|
|
|
// 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,
|
2026-02-04 02:23:53 +01:00
|
|
|
pcache: cache.New[*item](defaultCap),
|
2018-01-17 08:35:22 +01:00
|
|
|
pttl: maxTTL,
|
2018-09-03 14:26:02 -05:00
|
|
|
minpttl: minTTL,
|
2018-01-17 08:35:22 +01:00
|
|
|
ncap: defaultCap,
|
2026-02-04 02:23:53 +01:00
|
|
|
ncache: cache.New[*item](defaultCap),
|
2018-01-17 08:35:22 +01:00
|
|
|
nttl: maxNTTL,
|
2018-09-03 14:26:02 -05:00
|
|
|
minnttl: minNTTL,
|
2022-06-17 15:48:57 -04:00
|
|
|
failttl: minNTTL,
|
2018-01-17 08:35:22 +01:00
|
|
|
prefetch: 0,
|
|
|
|
|
duration: 1 * time.Minute,
|
|
|
|
|
percentage: 10,
|
|
|
|
|
now: time.Now,
|
|
|
|
|
}
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
|
|
|
|
|
2018-10-29 15:13:39 +00:00
|
|
|
// key returns key under which we store the item, -1 will be returned if we don't store the message.
|
2017-06-13 12:39:10 -07:00
|
|
|
// Currently we do not cache Truncated, errors zone transfers or dynamic update messages.
|
2018-10-29 15:13:39 +00:00
|
|
|
// qname holds the already lowercased qname.
|
2023-11-10 07:00:47 -08:00
|
|
|
func key(qname string, m *dns.Msg, t response.Type, do, cd bool) (bool, uint64) {
|
2016-11-07 16:27:50 +00:00
|
|
|
// We don't store truncated responses.
|
2016-04-19 11:13:24 +01:00
|
|
|
if m.Truncated {
|
2018-08-31 17:26:43 -04:00
|
|
|
return false, 0
|
2016-10-02 08:31:44 +01:00
|
|
|
}
|
2020-09-17 16:28:43 +02:00
|
|
|
// Nor errors or Meta or Update.
|
2016-11-07 16:27:50 +00:00
|
|
|
if t == response.OtherError || t == response.Meta || t == response.Update {
|
2018-08-31 17:26:43 -04:00
|
|
|
return false, 0
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
2026-07-06 13:48:51 +08:00
|
|
|
// Negative caching requires an SOA record to determine the denial TTL.
|
|
|
|
|
if t == response.NameError && !hasSOA(m) {
|
|
|
|
|
return false, 0
|
|
|
|
|
}
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
// An upstream may return NOERROR with a non-empty answer that still does not
|
|
|
|
|
// resolve the question and without an SOA to bound a negative TTL: a CNAME
|
|
|
|
|
// chain that does not terminate in the queried type (an incomplete recursion
|
|
|
|
|
// result from a forwarder). This is effectively an SOA-less NODATA response,
|
|
|
|
|
// which per RFC 2308 section 5 SHOULD NOT be cached. response.Typify classifies
|
|
|
|
|
// it as NoError because the answer section is non-empty, so caching it in the
|
|
|
|
|
// positive cache would replay the non-answer to clients until it expires. Skip
|
|
|
|
|
// caching so the next query is resolved upstream again. An empty answer section
|
|
|
|
|
// is deliberately left cacheable: it is indistinguishable from a legitimate
|
|
|
|
|
// NOERROR positive response that carries its data outside the answer section
|
|
|
|
|
// (for example the whoami plugin, which answers in the additional section).
|
|
|
|
|
if t == response.NoError && !hasSOA(m) && isNODATA(m) {
|
|
|
|
|
return false, 0
|
|
|
|
|
}
|
2016-04-19 11:13:24 +01:00
|
|
|
|
2026-07-11 06:18:27 +08:00
|
|
|
return true, hash(qname, m.Question[0].Qtype, m.Question[0].Qclass, do, cd)
|
2016-10-02 08:31:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-06 13:48:51 +08:00
|
|
|
func hasSOA(m *dns.Msg) bool {
|
|
|
|
|
for _, r := range m.Ns {
|
|
|
|
|
if r.Header().Rrtype == dns.TypeSOA {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
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 13:48:39 +05:30
|
|
|
// cacheResponseType returns the response type used by the cache. Typify treats
|
|
|
|
|
// any NOERROR response with a non-empty answer section as NoError, but RFC 2308
|
|
|
|
|
// NODATA responses may contain a CNAME chain. Reclassify those responses when
|
|
|
|
|
// an SOA provides the negative cache TTL.
|
|
|
|
|
func cacheResponseType(m *dns.Msg, now time.Time) response.Type {
|
|
|
|
|
t, _ := response.Typify(m, now)
|
|
|
|
|
if t == response.NoError && hasSOA(m) && isNODATA(m) {
|
|
|
|
|
return response.NoData
|
|
|
|
|
}
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// answersQuestion reports whether a NOERROR response contains an answer to its
|
|
|
|
|
// question. For types other than CNAME and ANY, the queried type must exist at
|
|
|
|
|
// the terminal owner reached by following the CNAME chain from QNAME.
|
|
|
|
|
func answersQuestion(m *dns.Msg) bool {
|
|
|
|
|
if m == nil || m.Rcode != dns.RcodeSuccess || len(m.Question) == 0 || len(m.Answer) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
q := m.Question[0]
|
|
|
|
|
return answerHasType(m.Answer, q.Name, q.Qtype, q.Qclass)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func answerHasType(answer []dns.RR, name string, qtype, qclass uint16) bool {
|
|
|
|
|
if len(answer) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if qtype == dns.TypeANY {
|
|
|
|
|
for _, r := range answer {
|
|
|
|
|
h := r.Header()
|
|
|
|
|
if classMatches(h.Class, qclass) && strings.EqualFold(h.Name, name) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if qtype == dns.TypeCNAME {
|
|
|
|
|
target, ok := uniqueCNAMETarget(answer, name, qclass)
|
|
|
|
|
return ok && target != ""
|
|
|
|
|
}
|
|
|
|
|
terminal, ok := canonicalName(answer, name, qclass)
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
name = terminal
|
|
|
|
|
for _, r := range answer {
|
|
|
|
|
h := r.Header()
|
|
|
|
|
if h.Rrtype == qtype && classMatches(h.Class, qclass) && strings.EqualFold(h.Name, name) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func classMatches(rrClass, qclass uint16) bool {
|
|
|
|
|
return qclass == dns.ClassANY || rrClass == qclass
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// usableAnswer reports whether m is a complete, cache-valid positive response
|
|
|
|
|
// that answers its question. It intentionally permits TTL-zero responses:
|
|
|
|
|
// they are usable for the current client even though they are not retained.
|
|
|
|
|
func usableAnswer(m *dns.Msg, now time.Time) bool {
|
|
|
|
|
if m == nil || m.Truncated || cacheResponseType(m, now) != response.NoError {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return answersQuestion(m)
|
|
|
|
|
}
|
|
|
|
|
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
// isNODATA reports whether a NOERROR response with a non-empty answer section
|
|
|
|
|
// does not answer the question. Following RFC 1034 section 3.6.2 and RFC 2308
|
|
|
|
|
// sections 1 and 2.2, a query of any type other than CNAME (and ANY) is
|
|
|
|
|
// restarted along the CNAME chain, so the effective owner name is the target at
|
|
|
|
|
// the end of the CNAME chain that starts at the question name. The response
|
|
|
|
|
// answers the question only if it carries a record of the queried type at that
|
|
|
|
|
// terminal name (records at any other owner name are irrelevant, and per RFC
|
|
|
|
|
// 1034 a CNAME's owner never co-locates other data). This rule is independent of
|
|
|
|
|
// the queried type: an MX, TXT, SRV, etc. chain that does not reach the queried
|
|
|
|
|
// type is NODATA just like an A or AAAA one. When the chain is malformed (an
|
|
|
|
|
// owner with more than one distinct CNAME target, or a loop) it has no
|
|
|
|
|
// well-defined terminal name, so the response is treated as NODATA, which errs
|
|
|
|
|
// toward re-querying upstream rather than caching a non-answer. An empty answer
|
|
|
|
|
// section returns false so that legitimate positive responses carrying data
|
|
|
|
|
// outside the answer section (for example the whoami plugin) remain cacheable.
|
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 13:48:39 +05:30
|
|
|
// An ANY query is answered only by a record at the queried owner and in the
|
|
|
|
|
// requested class. Note: a bare DNAME (RFC 6672) without its synthesized CNAME
|
|
|
|
|
// is treated as NODATA; standard responses include the synthesized CNAME, which
|
|
|
|
|
// the chain walk follows.
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
func isNODATA(m *dns.Msg) bool {
|
|
|
|
|
if len(m.Answer) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
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 13:48:39 +05:30
|
|
|
return !answersQuestion(m)
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// canonicalName follows the owner-linked CNAME chain in answer starting at name
|
|
|
|
|
// and returns the terminal target name together with a validity flag. Records
|
|
|
|
|
// whose owner is not on the chain are ignored. The chain is invalid (ok=false)
|
|
|
|
|
// when it is not a single unambiguous path to a terminal name: an owner that has
|
|
|
|
|
// more than one distinct CNAME target violates RFC 2181 section 10.1 (an alias
|
|
|
|
|
// has exactly one canonical name), and a revisited owner is a CNAME loop, which
|
|
|
|
|
// RFC 1034 section 3.6.2 says must be signalled as an error. Reporting validity
|
|
|
|
|
// rather than silently stopping keeps the classification order-independent and
|
|
|
|
|
// fail-closed: callers treat a malformed chain as a non-answer. Duplicate CNAME
|
|
|
|
|
// records that name the same target are tolerated, since they still describe a
|
|
|
|
|
// single canonical name.
|
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 13:48:39 +05:30
|
|
|
func canonicalName(answer []dns.RR, name string, qclass uint16) (string, bool) {
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
visited := nameSet{}
|
|
|
|
|
for {
|
|
|
|
|
if visited.contains(name) {
|
|
|
|
|
// Revisited owner: the chain contains a CNAME loop.
|
|
|
|
|
return name, false
|
|
|
|
|
}
|
|
|
|
|
visited.add(name)
|
|
|
|
|
|
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 13:48:39 +05:30
|
|
|
target, ok := uniqueCNAMETarget(answer, name, qclass)
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
if !ok {
|
|
|
|
|
// Owner has more than one distinct canonical name.
|
|
|
|
|
return name, false
|
|
|
|
|
}
|
|
|
|
|
if target == "" {
|
|
|
|
|
// Terminal owner reached: no CNAME continues the chain.
|
|
|
|
|
return name, true
|
|
|
|
|
}
|
|
|
|
|
name = target
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// uniqueCNAMETarget returns the canonical name that owner is aliased to by a
|
|
|
|
|
// CNAME record in answer. ok is false when owner carries more than one distinct
|
|
|
|
|
// CNAME target, which violates RFC 2181 section 10.1. When owner has no CNAME the
|
|
|
|
|
// returned target is empty and ok is true, marking a terminal owner. Duplicate
|
|
|
|
|
// CNAME records naming the same target are tolerated.
|
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 13:48:39 +05:30
|
|
|
func uniqueCNAMETarget(answer []dns.RR, owner string, qclass uint16) (target string, ok bool) {
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
for _, r := range answer {
|
|
|
|
|
c, isCNAME := r.(*dns.CNAME)
|
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 13:48:39 +05:30
|
|
|
if !isCNAME || !classMatches(c.Header().Class, qclass) || !strings.EqualFold(c.Header().Name, owner) {
|
plugin/cache: do not cache SOA-less NODATA responses (#8232)
* plugin/cache: do not cache SOA-less NODATA responses
An upstream may return NOERROR with a non-empty answer that still does not
resolve the question and without an SOA record to bound a negative TTL: a
CNAME chain that does not terminate in a record of the queried type at the
chain's terminal name (an incomplete recursion result from a forwarder).
Because the answer section is non-empty, response.Typify classifies it as
NoError (positive), so the cache plugin stores it keyed on <qname,qtype> and
replays the non-answer to clients until the TTL expires.
Per RFC 2308 section 5, negative responses without an SOA record SHOULD NOT be
cached. Following RFC 1034 section 3.6.2 and RFC 2308 sections 1 and 2.2, the
effective owner name is the target at the end of the CNAME chain, and the
response is NODATA unless it carries the queried type at that terminal name;
this holds for every query type, not just A/AAAA. Skip caching such a response
(mirroring the existing NameError && !hasSOA guard) and let the next query be
resolved upstream again.
An empty answer section is deliberately left cacheable: it is indistinguishable
from a legitimate NOERROR positive response that carries its data outside the
answer section (for example the whoami plugin).
Refs coredns#6958, coredns#5077, coredns#4987.
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: fail closed on malformed CNAME chains in isNODATA
canonicalName now returns a validity flag and rejects chains that are not a
single unambiguous path to a terminal name: an owner with more than one
distinct CNAME target (RFC 2181 section 10.1) and a revisited owner / CNAME
loop (RFC 1034 section 3.6.2). isNODATA treats an invalid chain as a
non-answer, so a SOA-less response with such a chain is not cached. This makes
the classification order-independent (previously the first CNAME per owner
won, so a two-target owner was cacheable or not depending on wire order) and
closes the loop-with-co-located-record case. Duplicate CNAME records naming
the same target are still tolerated.
Adds regression tests for the two-distinct-targets case in both orders, the
CNAME loop with a co-located A, and the tolerated duplicate-identical-CNAME
case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
* plugin/cache: split canonicalName into self-documenting helpers
Extract the per-owner CNAME lookup into uniqueCNAMETarget and loop detection
into a small case-insensitive nameSet type, leaving canonicalName as a short
driver. Signature and algorithm are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
---------
Signed-off-by: Nitin Nizhawan <nitin.nizhawan@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d59b4564-a9df-425f-858e-aadee0f35581
2026-07-30 09:09:49 +05:30
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if target != "" && !strings.EqualFold(target, c.Target) {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
target = c.Target
|
|
|
|
|
}
|
|
|
|
|
return target, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nameSet is a set of domain names compared case-insensitively, used to detect
|
|
|
|
|
// revisited owners (loops) while walking a CNAME chain.
|
|
|
|
|
type nameSet map[string]struct{}
|
|
|
|
|
|
|
|
|
|
func (s nameSet) contains(name string) bool {
|
|
|
|
|
_, ok := s[strings.ToLower(name)]
|
|
|
|
|
return ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s nameSet) add(name string) {
|
|
|
|
|
s[strings.ToLower(name)] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-21 09:29:04 -06:00
|
|
|
var one = []byte("1")
|
|
|
|
|
var zero = []byte("0")
|
|
|
|
|
|
2026-07-11 06:18:27 +08:00
|
|
|
func hash(qname string, qtype, qclass uint16, do, cd bool) uint64 {
|
2018-08-31 17:26:43 -04:00
|
|
|
h := fnv.New64()
|
2022-10-21 09:29:04 -06:00
|
|
|
|
|
|
|
|
if do {
|
|
|
|
|
h.Write(one)
|
|
|
|
|
} else {
|
|
|
|
|
h.Write(zero)
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-10 07:00:47 -08:00
|
|
|
if cd {
|
|
|
|
|
h.Write(one)
|
|
|
|
|
} else {
|
|
|
|
|
h.Write(zero)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 21:50:24 +02:00
|
|
|
var qtypeBytes [2]byte
|
|
|
|
|
binary.BigEndian.PutUint16(qtypeBytes[:], qtype)
|
|
|
|
|
h.Write(qtypeBytes[:])
|
2026-07-11 06:18:27 +08:00
|
|
|
var qclassBytes [2]byte
|
|
|
|
|
binary.BigEndian.PutUint16(qclassBytes[:], qclass)
|
|
|
|
|
h.Write(qclassBytes[:])
|
2018-10-29 15:13:39 +00:00
|
|
|
h.Write([]byte(qname))
|
2018-08-31 17:26:43 -04:00
|
|
|
return h.Sum64()
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
|
|
|
|
|
2018-09-03 14:26:02 -05:00
|
|
|
func computeTTL(msgTTL, minTTL, maxTTL time.Duration) time.Duration {
|
2025-09-10 23:08:27 +03:00
|
|
|
ttl := min(max(msgTTL, minTTL), maxTTL)
|
2018-09-03 14:26:02 -05:00
|
|
|
return ttl
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-23 09:14:12 +01:00
|
|
|
// ResponseWriter is a response writer that caches the reply message.
|
|
|
|
|
type ResponseWriter struct {
|
2016-04-19 11:13:24 +01:00
|
|
|
dns.ResponseWriter
|
2016-10-02 08:31:44 +01:00
|
|
|
*Cache
|
2018-04-27 19:37:49 +01:00
|
|
|
state request.Request
|
|
|
|
|
server string // Server handling the request.
|
2017-06-13 12:39:10 -07:00
|
|
|
|
2020-10-14 04:11:22 +02:00
|
|
|
do bool // When true the original request had the DO bit set.
|
2023-11-10 07:00:47 -08:00
|
|
|
cd bool // When true the original request had the CD bit set.
|
2022-06-17 15:47:35 -04:00
|
|
|
ad bool // When true the original request had the AD bit set.
|
2018-06-19 20:50:08 +02:00
|
|
|
prefetch bool // When true write nothing back to the client.
|
|
|
|
|
remoteAddr net.Addr
|
2022-07-07 17:07:04 -04:00
|
|
|
|
|
|
|
|
wildcardFunc func() string // function to retrieve wildcard name that synthesized the result.
|
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 13:48:39 +05:30
|
|
|
lastResponse *dns.Msg // last response after cache TTL and DNSSEC adjustments.
|
|
|
|
|
lastItem *item // cache item written by the last response, if cacheable.
|
2022-07-07 17:07:04 -04:00
|
|
|
|
2022-07-28 10:51:08 -04:00
|
|
|
pexcept []string // positive zone exceptions
|
|
|
|
|
nexcept []string // negative zone exceptions
|
2018-06-19 20:50:08 +02:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 11:47:11 -04:00
|
|
|
// 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 {
|
2026-08-15 12:28:20 +09:00
|
|
|
req = req.Copy()
|
|
|
|
|
req.AuthenticatedData = true
|
2026-03-24 11:47:11 -04:00
|
|
|
cw := &ResponseWriter{
|
|
|
|
|
Cache: c,
|
|
|
|
|
server: server,
|
|
|
|
|
do: do,
|
|
|
|
|
cd: cd,
|
2026-08-15 12:28:20 +09:00
|
|
|
ad: true,
|
2026-03-24 11:47:11 -04:00
|
|
|
prefetch: true,
|
|
|
|
|
remoteAddr: prefetchAddr,
|
2018-06-19 20:50:08 +02:00
|
|
|
}
|
2026-03-24 11:47:11 -04:00
|
|
|
cw.state = request.Request{Req: req}
|
|
|
|
|
return cw
|
2018-06-19 20:50:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoteAddr implements the dns.ResponseWriter interface.
|
|
|
|
|
func (w *ResponseWriter) RemoteAddr() net.Addr {
|
|
|
|
|
if w.remoteAddr != nil {
|
|
|
|
|
return w.remoteAddr
|
|
|
|
|
}
|
|
|
|
|
return w.ResponseWriter.RemoteAddr()
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-24 11:47:11 -04:00
|
|
|
// 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()
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-23 09:14:12 +01:00
|
|
|
// WriteMsg implements the dns.ResponseWriter interface.
|
2017-06-13 12:39:10 -07:00
|
|
|
func (w *ResponseWriter) WriteMsg(res *dns.Msg) error {
|
2025-06-06 05:14:41 -07:00
|
|
|
res = res.Copy()
|
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 13:48:39 +05:30
|
|
|
w.lastItem = nil
|
|
|
|
|
mt := cacheResponseType(res, w.now().UTC())
|
2016-04-19 11:13:24 +01:00
|
|
|
|
2016-11-07 16:27:50 +00:00
|
|
|
// key returns empty string for anything we don't want to cache.
|
2023-11-10 07:00:47 -08:00
|
|
|
hasKey, key := key(w.state.Name(), res, mt, w.do, w.cd)
|
2016-10-02 08:31:44 +01:00
|
|
|
|
2018-09-03 14:26:02 -05:00
|
|
|
var duration time.Duration
|
2025-04-04 20:27:39 +02:00
|
|
|
switch mt {
|
|
|
|
|
case response.NameError, response.NoData:
|
2026-06-05 21:48:26 -07:00
|
|
|
msgTTL := dnsutil.MinimalTTLWithMaximum(res, mt, w.nttl)
|
2018-09-03 14:26:02 -05:00
|
|
|
duration = computeTTL(msgTTL, w.minnttl, w.nttl)
|
2025-04-04 20:27:39 +02:00
|
|
|
case response.ServerError:
|
2022-06-17 15:48:57 -04:00
|
|
|
duration = w.failttl
|
2025-04-04 20:27:39 +02:00
|
|
|
default:
|
2026-06-05 21:48:26 -07:00
|
|
|
msgTTL := dnsutil.MinimalTTLWithMaximum(res, mt, w.pttl)
|
2018-09-03 14:26:02 -05:00
|
|
|
duration = computeTTL(msgTTL, w.minpttl, w.pttl)
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
|
|
|
|
|
2025-07-03 04:39:46 +02:00
|
|
|
// 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)
|
|
|
|
|
|
2018-08-31 17:26:43 -04:00
|
|
|
if hasKey && duration > 0 {
|
2020-10-15 16:47:07 +02:00
|
|
|
if w.state.Match(res) {
|
|
|
|
|
w.set(res, key, mt, duration)
|
2022-09-08 14:56:27 -04:00
|
|
|
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()))
|
2018-03-25 17:11:10 +01:00
|
|
|
} else {
|
|
|
|
|
// Don't log it, but increment counter
|
2022-09-08 14:56:27 -04:00
|
|
|
cacheDrops.WithLabelValues(w.server, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
2018-03-25 17:11:10 +01:00
|
|
|
}
|
2016-10-02 08:31:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-15 12:28:20 +09:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
w.lastResponse = res.Copy()
|
|
|
|
|
|
2017-06-13 12:39:10 -07:00
|
|
|
if w.prefetch {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2016-10-02 08:31:44 +01:00
|
|
|
|
2020-10-15 16:47:07 +02:00
|
|
|
return w.ResponseWriter.WriteMsg(res)
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
|
|
|
|
|
2018-08-31 17:26:43 -04:00
|
|
|
func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration time.Duration) {
|
|
|
|
|
// duration is expected > 0
|
|
|
|
|
// and key is valid
|
2016-04-19 11:13:24 +01:00
|
|
|
switch mt {
|
2016-10-10 12:09:29 +01:00
|
|
|
case response.NoError, response.Delegation:
|
2022-07-28 10:51:08 -04:00
|
|
|
if plugin.Zones(w.pexcept).Matches(m.Question[0].Name) != "" {
|
|
|
|
|
// zone is in exception list, do not cache
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-01-17 08:35:22 +01:00
|
|
|
i := newItem(m, w.now(), duration)
|
2022-07-07 17:07:04 -04:00
|
|
|
if w.wildcardFunc != nil {
|
|
|
|
|
i.wildcard = w.wildcardFunc()
|
|
|
|
|
}
|
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 13:48:39 +05:30
|
|
|
if w.preferPositive && !i.answering {
|
|
|
|
|
if previous, ok := w.pcache.Get(key); ok {
|
|
|
|
|
i.lastKnownGood = previous.answeringItem(w.state)
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-21 08:58:16 -07:00
|
|
|
if w.pcache.Add(key, i) {
|
2022-09-08 14:56:27 -04:00
|
|
|
evictions.WithLabelValues(w.server, Success, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
2021-03-21 08:58:16 -07:00
|
|
|
}
|
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 13:48:39 +05:30
|
|
|
w.lastItem = i
|
|
|
|
|
// A positive refresh is the newest state for this key. Under the
|
|
|
|
|
// prefer_positive policy, only remove the denial when this response
|
|
|
|
|
// actually answers the question.
|
|
|
|
|
if (!w.preferPositive && w.prefetch) || (w.preferPositive && i.answering) {
|
2020-03-20 05:05:09 -04:00
|
|
|
w.ncache.Remove(key)
|
|
|
|
|
}
|
2016-04-19 11:13:24 +01:00
|
|
|
|
2019-04-08 18:15:05 +08:00
|
|
|
case response.NameError, response.NoData, response.ServerError:
|
2022-07-28 10:51:08 -04:00
|
|
|
if plugin.Zones(w.nexcept).Matches(m.Question[0].Name) != "" {
|
|
|
|
|
// zone is in exception list, do not cache
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-01-17 08:35:22 +01:00
|
|
|
i := newItem(m, w.now(), duration)
|
2022-07-07 17:07:04 -04:00
|
|
|
if w.wildcardFunc != nil {
|
|
|
|
|
i.wildcard = w.wildcardFunc()
|
|
|
|
|
}
|
2021-03-21 08:58:16 -07:00
|
|
|
if w.ncache.Add(key, i) {
|
2022-09-08 14:56:27 -04:00
|
|
|
evictions.WithLabelValues(w.server, Denial, w.zonesMetricLabel, w.viewMetricLabel).Inc()
|
2021-03-21 08:58:16 -07:00
|
|
|
}
|
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 13:48:39 +05:30
|
|
|
w.lastItem = i
|
2016-04-19 11:13:24 +01:00
|
|
|
|
2016-09-07 11:10:16 +01:00
|
|
|
case response.OtherError:
|
2016-05-22 19:43:58 +01:00
|
|
|
// don't cache these
|
|
|
|
|
default:
|
2018-04-19 07:41:56 +01:00
|
|
|
log.Warningf("Caching called with unknown classification: %d", mt)
|
2016-04-19 11:13:24 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-23 09:14:12 +01:00
|
|
|
// Write implements the dns.ResponseWriter interface.
|
2017-06-13 12:39:10 -07:00
|
|
|
func (w *ResponseWriter) Write(buf []byte) (int, error) {
|
2018-04-19 07:41:56 +01:00
|
|
|
log.Warning("Caching called with Write: not caching reply")
|
2017-06-13 12:39:10 -07:00
|
|
|
if w.prefetch {
|
|
|
|
|
return 0, nil
|
|
|
|
|
}
|
|
|
|
|
n, err := w.ResponseWriter.Write(buf)
|
2016-04-19 11:13:24 +01:00
|
|
|
return n, err
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-02 19:16:33 +02:00
|
|
|
// 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.
|
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 13:48:39 +05:30
|
|
|
response *dns.Msg
|
|
|
|
|
item *item
|
2022-05-02 19:16:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newVerifyStaleResponseWriter returns a ResponseWriter to be used when verifying stale cache
|
2026-08-14 14:27:42 +08:00
|
|
|
// entries. It only forwards matching, complete responses that successfully refresh the data
|
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 13:48:39 +05:30
|
|
|
// according to RFC8767, section 4 (response is NoError or NXDomain). With prefer_positive, only
|
|
|
|
|
// a usable positive answer is forwarded; other matching responses are cached without being sent
|
|
|
|
|
// to the client.
|
2022-05-02 19:16:33 +02:00
|
|
|
func newVerifyStaleResponseWriter(w *ResponseWriter) *verifyStaleResponseWriter {
|
|
|
|
|
return &verifyStaleResponseWriter{
|
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 13:48:39 +05:30
|
|
|
ResponseWriter: w,
|
2022-05-02 19:16:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WriteMsg implements the dns.ResponseWriter interface.
|
|
|
|
|
func (w *verifyStaleResponseWriter) WriteMsg(res *dns.Msg) error {
|
|
|
|
|
w.refreshed = false
|
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 13:48:39 +05:30
|
|
|
w.response = nil
|
|
|
|
|
w.item = nil
|
2026-08-14 14:27:42 +08:00
|
|
|
if res == nil || res.Truncated || !w.state.Match(res) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
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 13:48:39 +05:30
|
|
|
if w.preferPositive {
|
|
|
|
|
if usableAnswer(res, w.now().UTC()) {
|
|
|
|
|
w.refreshed = true
|
|
|
|
|
err := w.ResponseWriter.WriteMsg(res)
|
|
|
|
|
w.response = w.lastResponse
|
|
|
|
|
w.item = w.lastItem
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
prefetch := w.prefetch
|
|
|
|
|
w.prefetch = true
|
|
|
|
|
err := w.ResponseWriter.WriteMsg(res)
|
|
|
|
|
w.prefetch = prefetch
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-14 14:27:42 +08:00
|
|
|
responseType, _ := response.Typify(res, w.now().UTC())
|
|
|
|
|
if responseType == response.OtherError || responseType == response.Meta || responseType == response.Update {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if res.Rcode != dns.RcodeSuccess && res.Rcode != dns.RcodeNameError {
|
|
|
|
|
return nil
|
2022-05-02 19:16:33 +02:00
|
|
|
}
|
2026-08-14 14:27:42 +08:00
|
|
|
w.refreshed = true
|
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 13:48:39 +05:30
|
|
|
err := w.ResponseWriter.WriteMsg(res) // stores to the cache and sends to the client
|
|
|
|
|
w.response = w.lastResponse
|
|
|
|
|
w.item = w.lastItem
|
|
|
|
|
return err
|
2022-05-02 19:16:33 +02:00
|
|
|
}
|
|
|
|
|
|
2016-04-19 11:13:24 +01:00
|
|
|
const (
|
2025-11-18 08:34:43 -08:00
|
|
|
maxTTL = dnsutil.MaximumDefaultTTL
|
2018-10-22 16:59:12 -04:00
|
|
|
minTTL = dnsutil.MinimalDefaultTTL
|
2025-11-18 08:34:43 -08:00
|
|
|
maxNTTL = dnsutil.MaximumDefaultTTL / 2
|
2018-10-22 16:59:12 -04:00
|
|
|
minNTTL = dnsutil.MinimalDefaultTTL
|
2016-11-09 10:01:26 +00:00
|
|
|
|
2016-10-02 08:31:44 +01:00
|
|
|
defaultCap = 10000 // default capacity of the cache.
|
2016-10-26 10:01:52 +01:00
|
|
|
|
2016-11-13 14:03:12 +00:00
|
|
|
// Success is the class for caching positive caching.
|
2016-10-26 10:01:52 +01:00
|
|
|
Success = "success"
|
|
|
|
|
// Denial is the class defined for negative caching.
|
|
|
|
|
Denial = "denial"
|
2016-04-19 11:13:24 +01:00
|
|
|
)
|