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

@@ -41,6 +41,7 @@ cache [TTL] [ZONES...] {
denial CAPACITY [TTL] [MINTTL]
prefetch AMOUNT [[DURATION] [PERCENTAGE%]]
serve_stale [DURATION] [immediate [RESPONSE_TTL [FAILURE_RECHECK]] | verify [VERIFY_TIMEOUT [RESPONSE_TTL [FAILURE_RECHECK]]]]
serve_stale_policy prefer_positive
servfail DURATION
disable success|denial [ZONES...]
keepttl
@@ -89,6 +90,16 @@ cache [TTL] [ZONES...] {
intact. The default of `0` preserves the existing retry behavior. RFC 8767 recommends `30s` and says this
value should not exceed 5 minutes. Examples: `serve_stale 1h immediate 30s 30s` and
`serve_stale 1h verify 100ms 30s 30s`.
* `serve_stale_policy` controls cache selection while `serve_stale` is enabled. The only supported policy is
`prefer_positive`. It checks the success cache before the denial cache and returns an eligible positive response
when it actually answers the question, even when a cached NXDOMAIN, NODATA, SERVFAIL, or NOTIMP response also
exists. The positive response must be unexpired or within the configured `serve_stale` duration.
The positive response is retained independently when a later NOERROR response does not answer the question (for example, an empty response
without SOA, a referral, or a response carrying data only in the additional section), so such a refresh cannot
destroy the last-known-good answer. A usable positive refresh replaces the retained answer.
The policy is disabled by default because it can mask legitimate record deletion or removal until the positive response exceeds
the stale duration or is evicted. In `immediate` mode, the stale positive response is returned first and the cache
refreshes in the background. In `verify` mode, only a refreshed positive answer replaces the stale response.
* `servfail` cache SERVFAIL responses for **DURATION**. Setting **DURATION** to 0 will disable caching of SERVFAIL
responses. If this option is not set, SERVFAIL responses will be cached for 5 seconds. **DURATION** may not be
greater than 5 minutes.