diff --git a/plugin/cache/README.md b/plugin/cache/README.md index 9322af16c..602072656 100644 --- a/plugin/cache/README.md +++ b/plugin/cache/README.md @@ -39,6 +39,7 @@ cache [TTL] [ZONES...] { prefetch AMOUNT [[DURATION] [PERCENTAGE%]] serve_stale [DURATION] [REFRESH_MODE] servfail DURATION + disable success|denial [ZONES...] } ~~~ @@ -67,6 +68,8 @@ cache [TTL] [ZONES...] { * `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. +* `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. ## Capacity and Eviction @@ -124,3 +127,13 @@ example.org { } } ~~~ + +Enable caching for `example.org`, but do not cache denials in `sub.example.org`: + +~~~ corefile +example.org { + cache { + disable denial sub.example.org + } +} +~~~ \ No newline at end of file diff --git a/plugin/cache/cache.go b/plugin/cache/cache.go index d1989f35b..bfd8c1576 100644 --- a/plugin/cache/cache.go +++ b/plugin/cache/cache.go @@ -43,6 +43,10 @@ type Cache struct { staleUpTo time.Duration verifyStale bool + // Positive/negative zone exceptions + pexcept []string + nexcept []string + // Testing. now func() time.Time } @@ -117,6 +121,8 @@ type ResponseWriter struct { wildcardFunc func() string // function to retrieve wildcard name that synthesized the result. + pexcept []string // positive zone exceptions + nexcept []string // negative zone exceptions } // newPrefetchResponseWriter returns a Cache ResponseWriter to be used in @@ -204,6 +210,10 @@ func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration // and key is valid switch mt { case response.NoError, response.Delegation: + if plugin.Zones(w.pexcept).Matches(m.Question[0].Name) != "" { + // zone is in exception list, do not cache + return + } i := newItem(m, w.now(), duration) if w.wildcardFunc != nil { i.wildcard = w.wildcardFunc() @@ -217,6 +227,10 @@ func (w *ResponseWriter) set(m *dns.Msg, key uint64, mt response.Type, duration } case response.NameError, response.NoData, response.ServerError: + if plugin.Zones(w.nexcept).Matches(m.Question[0].Name) != "" { + // zone is in exception list, do not cache + return + } i := newItem(m, w.now(), duration) if w.wildcardFunc != nil { i.wildcard = w.wildcardFunc() diff --git a/plugin/cache/cache_test.go b/plugin/cache/cache_test.go index 851507a4d..0328c8411 100644 --- a/plugin/cache/cache_test.go +++ b/plugin/cache/cache_test.go @@ -17,8 +17,8 @@ import ( ) type cacheTestCase struct { - test.Case - in test.Case + test.Case // the expected message coming "out" of cache + in test.Case // the test message going "in" to cache AuthenticatedData bool RecursionAvailable bool Truncated bool @@ -163,6 +163,62 @@ var cacheTestCases = []cacheTestCase{ }, shouldCache: true, }, + { + in: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "neg-disabled.example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, + }, + Case: test.Case{}, + shouldCache: false, + }, + { + in: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "pos-disabled.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("pos-disabled.example.org. 3600 IN A 127.0.0.1"), + }, + }, + Case: test.Case{}, + shouldCache: false, + }, + { + in: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "pos-disabled.example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, + }, + Case: test.Case{ + Rcode: dns.RcodeNameError, + Qname: "pos-disabled.example.org.", Qtype: dns.TypeA, + Ns: []dns.RR{ + test.SOA("example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2016082540 7200 3600 1209600 3600"), + }, + }, + shouldCache: true, + }, + { + in: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "neg-disabled.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("neg-disabled.example.org. 3600 IN A 127.0.0.1"), + }, + }, + Case: test.Case{ + Rcode: dns.RcodeSuccess, + Qname: "neg-disabled.example.org.", Qtype: dns.TypeA, + Answer: []dns.RR{ + test.A("neg-disabled.example.org. 3600 IN A 127.0.0.1"), + }, + }, + shouldCache: true, + }, } func cacheMsg(m *dns.Msg, tc cacheTestCase) *dns.Msg { @@ -183,6 +239,9 @@ func newTestCache(ttl time.Duration) (*Cache, *ResponseWriter) { c.nttl = ttl crr := &ResponseWriter{ResponseWriter: nil, Cache: c} + crr.nexcept = []string{"neg-disabled.example.org."} + crr.pexcept = []string{"pos-disabled.example.org."} + return c, crr } diff --git a/plugin/cache/handler.go b/plugin/cache/handler.go index 69ec4928d..3d2f43904 100644 --- a/plugin/cache/handler.go +++ b/plugin/cache/handler.go @@ -38,7 +38,8 @@ func (c *Cache) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) ttl := 0 i := c.getIgnoreTTL(now, state, server) if i == nil { - crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, ad: ad, wildcardFunc: wildcardFunc(ctx)} + crr := &ResponseWriter{ResponseWriter: w, Cache: c, state: state, server: server, do: do, ad: ad, + nexcept: c.nexcept, pexcept: c.pexcept, wildcardFunc: wildcardFunc(ctx)} return c.doRefresh(ctx, state, crr) } ttl = i.ttl(now) diff --git a/plugin/cache/setup.go b/plugin/cache/setup.go index aa487105c..a1ce255a9 100644 --- a/plugin/cache/setup.go +++ b/plugin/cache/setup.go @@ -205,6 +205,35 @@ func cacheParse(c *caddy.Controller) (*Cache, error) { return nil, errors.New("caching SERVFAIL responses over 5 minutes is not permitted") } ca.failttl = d + case "disable": + // disable [success|denial] [zones]... + args := c.RemainingArgs() + if len(args) < 1 { + return nil, c.ArgErr() + } + + var zones []string + if len(args) > 1 { + for _, z := range args[1:] { // args[1:] define the list of zones to disable + nz := plugin.Name(z).Normalize() + if nz == "" { + return nil, fmt.Errorf("invalid disabled zone: %s", z) + } + zones = append(zones, nz) + } + } else { + // if no zones specified, default to root + zones = []string{"."} + } + + switch args[0] { // args[0] defines which cache to disable + case Denial: + ca.nexcept = zones + case Success: + ca.pexcept = zones + default: + return nil, fmt.Errorf("cache type for disable must be %q or %q", Success, Denial) + } default: return nil, c.ArgErr() } diff --git a/plugin/cache/setup_test.go b/plugin/cache/setup_test.go index 5e684c510..5d8b9653c 100644 --- a/plugin/cache/setup_test.go +++ b/plugin/cache/setup_test.go @@ -192,3 +192,42 @@ func TestServfail(t *testing.T) { } } } + +func TestDisable(t *testing.T) { + tests := []struct { + input string + shouldErr bool + nexcept []string + pexcept []string + }{ + // positive + {"disable denial example.com example.org", false, []string{"example.com.", "example.org."}, nil}, + {"disable success example.com example.org", false, nil, []string{"example.com.", "example.org."}}, + {"disable denial", false, []string{"."}, nil}, + {"disable success", false, nil, []string{"."}}, + {"disable denial example.com example.org\ndisable success example.com example.org", false, + []string{"example.com.", "example.org."}, []string{"example.com.", "example.org."}}, + // negative + {"disable invalid example.com example.org", true, nil, nil}, + } + 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 fmt.Sprintf("%v", test.nexcept) != fmt.Sprintf("%v", ca.nexcept) { + t.Errorf("Test %v: Expected %v but got: %v", i, test.nexcept, ca.nexcept) + } + if fmt.Sprintf("%v", test.pexcept) != fmt.Sprintf("%v", ca.pexcept) { + t.Errorf("Test %v: Expected %v but got: %v", i, test.pexcept, ca.pexcept) + } + } +}