plugin/cache: preserve monotonic time for TTL expiry (#8346)

Keep cache timestamps and TTL calculations on the time values returned by the cache clock. Converting them with UTC strips Go's monotonic clock reading and can extend cached entries when the wall clock moves backward.

Add a regression test that verifies new cache items retain the original monotonic timestamp.

Fixes #5478.

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
houyuwushang
2026-07-30 09:12:56 +08:00
committed by GitHub
parent 3c2c33bb50
commit c0adbae99b
3 changed files with 29 additions and 4 deletions

View File

@@ -26,7 +26,7 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
return plugin.NextOrFailure(c.Name(), c.Next, ctx, w, rc) return plugin.NextOrFailure(c.Name(), c.Next, ctx, w, rc)
} }
now := c.now().UTC() now := c.now()
server := metrics.WithServer(ctx) server := metrics.WithServer(ctx)
// On cache refresh, we will just use the DO bit from the incoming query for the refresh since we key our cache // On cache refresh, we will just use the DO bit from the incoming query for the refresh since we key our cache
@@ -158,7 +158,7 @@ func (c *Cache) verifyWithTimeout(ctx context.Context, state request.Request, w
// Should not happen: refreshed=true means the upstream response was cacheable. // Should not happen: refreshed=true means the upstream response was cacheable.
return true, res.code, res.err return true, res.code, res.err
} }
now := c.now().UTC() now := c.now()
if c.keepttl { if c.keepttl {
now = fresh.stored now = fresh.stored
} }

View File

@@ -62,7 +62,9 @@ func newItem(m *dns.Msg, now time.Time, d time.Duration) *item {
i.Extra = i.Extra[:j] i.Extra = i.Extra[:j]
i.origTTL = uint32(d.Seconds()) i.origTTL = uint32(d.Seconds())
i.stored = now.UTC() // Keep the monotonic clock reading so TTL expiry is unaffected by wall
// clock adjustments.
i.stored = now
i.Freq = new(freq.Freq) i.Freq = new(freq.Freq)
@@ -102,7 +104,7 @@ func (i *item) toMsg(m *dns.Msg, now time.Time, do bool, ad bool) *dns.Msg {
} }
func (i *item) ttl(now time.Time) int { func (i *item) ttl(now time.Time) int {
ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds()) ttl := int(i.origTTL) - int(now.Sub(i.stored).Seconds())
return ttl return ttl
} }

23
plugin/cache/item_test.go vendored Normal file
View File

@@ -0,0 +1,23 @@
package cache
import (
"reflect"
"testing"
"time"
"github.com/miekg/dns"
)
func TestNewItemPreservesMonotonicClock(t *testing.T) {
now := time.Now()
if reflect.DeepEqual(now, now.Round(0)) {
t.Fatal("time.Now did not include a monotonic clock reading")
}
i := newItem(new(dns.Msg), now, time.Minute)
// DeepEqual compares the complete time representation, including its
// monotonic clock reading. Time.Equal intentionally ignores that detail.
if !reflect.DeepEqual(i.stored, now) {
t.Fatalf("stored time = %v; want original time %v", i.stored, now)
}
}