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

View File

@@ -226,6 +226,41 @@ func TestServeStaleFailureRecheck(t *testing.T) {
}
}
func TestServeStalePolicy(t *testing.T) {
tests := []struct {
input string
shouldErr bool
preferPositive bool
}{
{"serve_stale\nserve_stale_policy prefer_positive", false, true},
{"serve_stale_policy PREFER_POSITIVE\nserve_stale", false, true},
{"serve_stale", false, false},
// fails
{"serve_stale_policy prefer_positive", true, false},
{"serve_stale\nserve_stale_policy", true, false},
{"serve_stale\nserve_stale_policy prefer_positive extra", true, false},
{"serve_stale\nserve_stale_policy invalid", true, false},
{"serve_stale\nserve_stale_policy prefer_positive\nserve_stale_policy prefer_positive", true, false},
}
for i, test := range tests {
c := caddy.NewTestController("dns", fmt.Sprintf("cache {\n%s\n}", test.input))
ca, err := cacheParse(c)
if test.shouldErr && err == nil {
t.Errorf("Test %v: Expected error but found nil", i)
continue
} else if !test.shouldErr && err != nil {
t.Errorf("Test %v: Expected no error but found error: %v", i, err)
continue
}
if test.shouldErr {
continue
}
if ca.preferPositive != test.preferPositive {
t.Errorf("Test %v: Expected preferPositive %v but found %v", i, test.preferPositive, ca.preferPositive)
}
}
}
func TestServfail(t *testing.T) {
tests := []struct {
input string