diff --git a/plugin/cache/README.md b/plugin/cache/README.md index 6fc20ae2c..d516a91db 100644 --- a/plugin/cache/README.md +++ b/plugin/cache/README.md @@ -39,6 +39,7 @@ cache [TTL] [ZONES...] { serve_stale [DURATION] [REFRESH_MODE] servfail DURATION disable success|denial [ZONES...] + keepttl } ~~~ @@ -69,6 +70,11 @@ cache [TTL] [ZONES...] { greater than 5 minutes. * `disable` disable the success or denial cache for the listed **ZONES**. If no **ZONES** are given, the specified cache will be disabled for all zones. +* `keepttl` do not age TTL when serving responses from cache. The entry will still be removed from cache + when the TTL expires as normal, but until it expires responses will include the original TTL instead + of the remaining TTL. This can be useful if CoreDNS is used as an authoritative server and you want + to serve a consistent TTL to downstream clients. This is **NOT** recommended when CoreDNS is caching + records it is not authoritative for because it could result in downstream clients using stale answers. ## Capacity and Eviction @@ -135,4 +141,4 @@ example.org { disable denial sub.example.org } } -~~~ \ No newline at end of file +~~~ diff --git a/plugin/cache/cache.go b/plugin/cache/cache.go index 54f2587fa..563b2abbe 100644 --- a/plugin/cache/cache.go +++ b/plugin/cache/cache.go @@ -48,6 +48,9 @@ type Cache struct { pexcept []string nexcept []string + // Keep ttl option + keepttl bool + // Testing. now func() time.Time } diff --git a/plugin/cache/cache_test.go b/plugin/cache/cache_test.go index 861e9b8fd..5a023f400 100644 --- a/plugin/cache/cache_test.go +++ b/plugin/cache/cache_test.go @@ -690,6 +690,44 @@ func TestCacheWildcardMetadata(t *testing.T) { } } +func TestCacheKeepTTL(t *testing.T) { + defaultTtl := 60 + + c := New() + c.Next = ttlBackend(defaultTtl) + + req := new(dns.Msg) + req.SetQuestion("cached.org.", dns.TypeA) + ctx := context.TODO() + + // Cache cached.org. with 60s TTL + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.keepttl = true + c.ServeDNS(ctx, rec, req) + + tests := []struct { + name string + futureSeconds int + }{ + {"cached.org.", 0}, + {"cached.org.", 30}, + {"uncached.org.", 60}, + } + + for i, tt := range tests { + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + c.now = func() time.Time { return time.Now().Add(time.Duration(tt.futureSeconds) * time.Second) } + r := req.Copy() + r.SetQuestion(tt.name, dns.TypeA) + c.ServeDNS(ctx, rec, r) + + recTtl := rec.Msg.Answer[0].Header().Ttl + if defaultTtl != int(recTtl) { + t.Errorf("Test %d: expecting TTL=%d, got TTL=%d", i, defaultTtl, recTtl) + } + } +} + // wildcardMetadataBackend mocks a backend that reponds with a response for qname synthesized by wildcard // and sets the zone/wildcard metadata value func wildcardMetadataBackend(qname, wildcard string) plugin.Handler { diff --git a/plugin/cache/handler.go b/plugin/cache/handler.go index e60637af8..de7cae456 100644 --- a/plugin/cache/handler.go +++ b/plugin/cache/handler.go @@ -71,6 +71,11 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) }) } + if c.keepttl { + // If keepttl is enabled we fake the current time to the stored + // one so that we always get the original TTL + now = i.stored + } resp := i.toMsg(r, now, do, ad) w.WriteMsg(resp) return dns.RcodeSuccess, nil diff --git a/plugin/cache/setup.go b/plugin/cache/setup.go index 6a537d986..7e5dfa174 100644 --- a/plugin/cache/setup.go +++ b/plugin/cache/setup.go @@ -240,6 +240,11 @@ func cacheParse(c *caddy.Controller) (*Cache, error) { default: return nil, fmt.Errorf("cache type for disable must be %q or %q", Success, Denial) } + case "keepttl": + if len(args) != 0 { + return nil, c.ArgErr() + } + ca.keepttl = true default: return nil, c.ArgErr() } diff --git a/plugin/cache/setup_test.go b/plugin/cache/setup_test.go index 5d8b9653c..46ac5bd9e 100644 --- a/plugin/cache/setup_test.go +++ b/plugin/cache/setup_test.go @@ -231,3 +231,32 @@ func TestDisable(t *testing.T) { } } } + +func TestKeepttl(t *testing.T) { + tests := []struct { + input string + shouldErr bool + }{ + // positive + {"keepttl", false}, + // negative + {"keepttl arg1", true}, + } + 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.keepttl { + t.Errorf("Test %v: Expected keepttl enabled but disabled", i) + } + } +}