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
This commit is contained in:
Nitin Nizhawan
2026-08-14 13:48:39 +05:30
committed by GitHub
parent 2eb7d16071
commit 87ccb6f90e
7 changed files with 658 additions and 52 deletions

21
plugin/cache/setup.go vendored
View File

@@ -63,6 +63,8 @@ func cacheParse(c *caddy.Controller) (*Cache, error) {
}
}
origins := plugin.OriginsFromArgsOrServerBlock(args, c.ServerBlockKeys)
serveStaleConfigured := false
serveStalePolicyConfigured := false
// Refinements? In an extra block.
for c.NextBlock() {
@@ -171,6 +173,7 @@ func cacheParse(c *caddy.Controller) (*Cache, error) {
}
case "serve_stale":
serveStaleConfigured = true
args := c.RemainingArgs()
if len(args) > 5 {
return nil, c.ArgErr()
@@ -236,6 +239,21 @@ func cacheParse(c *caddy.Controller) (*Cache, error) {
}
}
}
case "serve_stale_policy":
if serveStalePolicyConfigured {
return nil, errors.New("serve_stale_policy can only be specified once")
}
serveStalePolicyConfigured = true
args := c.RemainingArgs()
if len(args) != 1 {
return nil, c.ArgErr()
}
switch strings.ToLower(args[0]) {
case "prefer_positive":
ca.preferPositive = true
default:
return nil, fmt.Errorf("invalid serve_stale_policy: %s", args[0])
}
case "servfail":
args := c.RemainingArgs()
if len(args) != 1 {
@@ -292,6 +310,9 @@ func cacheParse(c *caddy.Controller) (*Cache, error) {
return nil, c.ArgErr()
}
}
if serveStalePolicyConfigured && !serveStaleConfigured {
return nil, errors.New("serve_stale_policy requires serve_stale")
}
ca.Zones = origins
ca.zonesMetricLabel = strings.Join(origins, ",")