diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index 9a7390ecd..4726345e3 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -37,6 +37,7 @@ var Directives = []string{ "rewrite", "dnssec", "autopath", + "minimal", "template", "transfer", "hosts", diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index afd77eb99..eee813910 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -36,6 +36,7 @@ import ( _ "github.com/coredns/coredns/plugin/loop" _ "github.com/coredns/coredns/plugin/metadata" _ "github.com/coredns/coredns/plugin/metrics" + _ "github.com/coredns/coredns/plugin/minimal" _ "github.com/coredns/coredns/plugin/nsid" _ "github.com/coredns/coredns/plugin/pprof" _ "github.com/coredns/coredns/plugin/ready" diff --git a/plugin.cfg b/plugin.cfg index 08048a3cf..58f176793 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -46,6 +46,7 @@ cache:cache rewrite:rewrite dnssec:dnssec autopath:autopath +minimal:minimal template:template transfer:transfer hosts:hosts diff --git a/plugin/minimal/README.md b/plugin/minimal/README.md new file mode 100644 index 000000000..e667fee25 --- /dev/null +++ b/plugin/minimal/README.md @@ -0,0 +1,36 @@ +# minimal + +## Name + +*minimal* - minimizes size of the DNS response message whenever possible. + +## Description + +The *minimal* plugin tries to minimize the size of the response. Depending on the response type it +removes resource records from the AUTHORITY and ADDITIONAL sections. + +Specifically this plugin looks at successful responses (this excludes negative responses, i.e. +nodata or name error). If the successful response isn't a delegation only the RRs in the answer +section are written to the client. + +## Syntax + +~~~ txt +minimal +~~~ + +## Examples + +Enable minimal responses: + +~~~ corefile +example.org { + whoami + forward . 8.8.8.8 + minimal +} +~~~ + +## See Also + +[BIND 9 Configuration Reference](https://bind9.readthedocs.io/en/latest/reference.html#boolean-options) diff --git a/plugin/minimal/minimal.go b/plugin/minimal/minimal.go new file mode 100644 index 000000000..6b5a7a731 --- /dev/null +++ b/plugin/minimal/minimal.go @@ -0,0 +1,54 @@ +package minimal + +import ( + "context" + "fmt" + "time" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/nonwriter" + "github.com/coredns/coredns/plugin/pkg/response" + "github.com/miekg/dns" +) + +// minimalHandler implements the plugin.Handler interface. +type minimalHandler struct { + Next plugin.Handler +} + +func (m *minimalHandler) Name() string { return "minimal" } + +func (m *minimalHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + nw := nonwriter.New(w) + + rcode, err := plugin.NextOrFailure(m.Name(), m.Next, ctx, nw, r) + if err != nil { + return rcode, err + } + + ty, _ := response.Typify(nw.Msg, time.Now().UTC()) + cl := response.Classify(ty) + + // if response is Denial or Error pass through also if the type is Delegation pass through + if cl == response.Denial || cl == response.Error || ty == response.Delegation { + w.WriteMsg(nw.Msg) + return 0, nil + } + if ty != response.NoError { + w.WriteMsg(nw.Msg) + return 0, plugin.Error("minimal", fmt.Errorf("unhandled response type %q for %q", ty, nw.Msg.Question[0].Name)) + } + + // copy over the original Msg params, deep copy not required as RRs are not modified + d := &dns.Msg{ + MsgHdr: nw.Msg.MsgHdr, + Compress: nw.Msg.Compress, + Question: nw.Msg.Question, + Answer: nw.Msg.Answer, + Ns: nil, + Extra: nil, + } + + w.WriteMsg(d) + return 0, nil +} diff --git a/plugin/minimal/minimal_test.go b/plugin/minimal/minimal_test.go new file mode 100644 index 000000000..b579a9970 --- /dev/null +++ b/plugin/minimal/minimal_test.go @@ -0,0 +1,152 @@ +package minimal + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/miekg/dns" +) + +// testHandler implements plugin.Handler and will be used to create a stub handler for the test +type testHandler struct { + Response *test.Case + Next plugin.Handler +} + +func (t *testHandler) Name() string { return "test-handler" } + +func (t *testHandler) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + d := new(dns.Msg) + d.SetReply(r) + if t.Response != nil { + d.Answer = t.Response.Answer + d.Ns = t.Response.Ns + d.Extra = t.Response.Extra + d.Rcode = t.Response.Rcode + } + w.WriteMsg(d) + return 0, nil +} + +func TestMinimizeResponse(t *testing.T) { + baseAnswer := []dns.RR{ + test.A("example.com. 293 IN A 142.250.76.46"), + } + baseNs := []dns.RR{ + test.NS("example.com. 157127 IN NS ns2.example.com."), + test.NS("example.com. 157127 IN NS ns1.example.com."), + test.NS("example.com. 157127 IN NS ns3.example.com."), + test.NS("example.com. 157127 IN NS ns4.example.com."), + } + + baseExtra := []dns.RR{ + test.A("ns2.example.com. 316273 IN A 216.239.34.10"), + test.AAAA("ns2.example.com. 157127 IN AAAA 2001:4860:4802:34::a"), + test.A("ns3.example.com. 316274 IN A 216.239.36.10"), + test.AAAA("ns3.example.com. 157127 IN AAAA 2001:4860:4802:36::a"), + test.A("ns1.example.com. 165555 IN A 216.239.32.10"), + test.AAAA("ns1.example.com. 165555 IN AAAA 2001:4860:4802:32::a"), + test.A("ns4.example.com. 190188 IN A 216.239.38.10"), + test.AAAA("ns4.example.com. 157127 IN AAAA 2001:4860:4802:38::a"), + } + + tests := []struct { + active bool + original test.Case + minimal test.Case + }{ + { // minimization possible NoError case + original: test.Case{ + Answer: baseAnswer, + Ns: nil, + Extra: baseExtra, + Rcode: 0, + }, + minimal: test.Case{ + Answer: baseAnswer, + Ns: nil, + Extra: nil, + Rcode: 0, + }, + }, + { // delegate response case + original: test.Case{ + Answer: nil, + Ns: baseNs, + Extra: baseExtra, + Rcode: 0, + }, + minimal: test.Case{ + Answer: nil, + Ns: baseNs, + Extra: baseExtra, + Rcode: 0, + }, + }, { // negative response case + original: test.Case{ + Answer: baseAnswer, + Ns: baseNs, + Extra: baseExtra, + Rcode: 2, + }, + minimal: test.Case{ + Answer: baseAnswer, + Ns: baseNs, + Extra: baseExtra, + Rcode: 2, + }, + }, + } + + for i, tc := range tests { + req := new(dns.Msg) + req.SetQuestion("example.com", dns.TypeA) + + tHandler := &testHandler{ + Response: &tc.original, + Next: nil, + } + o := &minimalHandler{Next: tHandler} + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := o.ServeDNS(context.TODO(), rec, req) + + if err != nil { + t.Errorf("Expected no error, but got %q", err) + } + + if len(tc.minimal.Answer) != len(rec.Msg.Answer) { + t.Errorf("Test %d: Expected %d Answer, but got %d", i, len(tc.minimal.Answer), len(req.Answer)) + continue + } + if len(tc.minimal.Ns) != len(rec.Msg.Ns) { + t.Errorf("Test %d: Expected %d Ns, but got %d", i, len(tc.minimal.Ns), len(req.Ns)) + continue + } + + if len(tc.minimal.Extra) != len(rec.Msg.Extra) { + t.Errorf("Test %d: Expected %d Extras, but got %d", i, len(tc.minimal.Extra), len(req.Extra)) + continue + } + + for j, a := range rec.Msg.Answer { + if tc.minimal.Answer[j].String() != a.String() { + t.Errorf("Test %d: Expected Answer %d to be %v, but got %v", i, j, tc.minimal.Answer[j], a) + } + } + + for j, a := range rec.Msg.Ns { + if tc.minimal.Ns[j].String() != a.String() { + t.Errorf("Test %d: Expected NS %d to be %v, but got %v", i, j, tc.minimal.Ns[j], a) + } + } + + for j, a := range rec.Msg.Extra { + if tc.minimal.Extra[j].String() != a.String() { + t.Errorf("Test %d: Expected Extra %d to be %v, but got %v", i, j, tc.minimal.Extra[j], a) + } + } + } +} diff --git a/plugin/minimal/setup.go b/plugin/minimal/setup.go new file mode 100644 index 000000000..1bf37a652 --- /dev/null +++ b/plugin/minimal/setup.go @@ -0,0 +1,24 @@ +package minimal + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { + plugin.Register("minimal", setup) +} + +func setup(c *caddy.Controller) error { + c.Next() + if c.NextArg() { + return plugin.Error("minimal", c.ArgErr()) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return &minimalHandler{Next: next} + }) + + return nil +} diff --git a/plugin/minimal/setup_test.go b/plugin/minimal/setup_test.go new file mode 100644 index 000000000..49341c441 --- /dev/null +++ b/plugin/minimal/setup_test.go @@ -0,0 +1,19 @@ +package minimal + +import ( + "testing" + + "github.com/coredns/caddy" +) + +func TestSetup(t *testing.T) { + c := caddy.NewTestController("dns", `minimal-response`) + if err := setup(c); err != nil { + t.Fatalf("Expected no errors, but got: %v", err) + } + + c = caddy.NewTestController("dns", `minimal-response example.org`) + if err := setup(c); err == nil { + t.Fatalf("Expected errors, but got: %v", err) + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go index ee1bf8429..9bac48885 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -69,7 +69,7 @@ func (f HandlerFunc) Name() string { return "handlerfunc" } // Error returns err with 'plugin/name: ' prefixed to it. func Error(name string, err error) error { return fmt.Errorf("%s/%s: %s", "plugin", name, err) } -// NextOrFailure calls next.ServeDNS when next is not nil, otherwise it will return, a ServerFailure and a nil error. +// NextOrFailure calls next.ServeDNS when next is not nil, otherwise it will return, a ServerFailure and a `no next plugin found` error. func NextOrFailure(name string, next Handler, ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { // nolint: golint if next != nil { if span := ot.SpanFromContext(ctx); span != nil {