From c0adbae99b704ca1d6b9e56165203299eca22e70 Mon Sep 17 00:00:00 2001 From: houyuwushang Date: Thu, 30 Jul 2026 09:12:56 +0800 Subject: [PATCH] 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 --- plugin/cache/handler.go | 4 ++-- plugin/cache/item.go | 6 ++++-- plugin/cache/item_test.go | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 plugin/cache/item_test.go diff --git a/plugin/cache/handler.go b/plugin/cache/handler.go index 54b4c81b7..643101e6f 100644 --- a/plugin/cache/handler.go +++ b/plugin/cache/handler.go @@ -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) } - now := c.now().UTC() + now := c.now() 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 @@ -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. return true, res.code, res.err } - now := c.now().UTC() + now := c.now() if c.keepttl { now = fresh.stored } diff --git a/plugin/cache/item.go b/plugin/cache/item.go index 1aa16a5df..17f68a04e 100644 --- a/plugin/cache/item.go +++ b/plugin/cache/item.go @@ -62,7 +62,9 @@ func newItem(m *dns.Msg, now time.Time, d time.Duration) *item { i.Extra = i.Extra[:j] 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) @@ -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 { - ttl := int(i.origTTL) - int(now.UTC().Sub(i.stored).Seconds()) + ttl := int(i.origTTL) - int(now.Sub(i.stored).Seconds()) return ttl } diff --git a/plugin/cache/item_test.go b/plugin/cache/item_test.go new file mode 100644 index 000000000..f925c8d01 --- /dev/null +++ b/plugin/cache/item_test.go @@ -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) + } +}