diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index c8079204b..d7874f17b 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -12,6 +12,7 @@ package dnsserver var directives = []string{ "tls", + "nsid", "root", "bind", "debug", diff --git a/core/zplugin.go b/core/zplugin.go index 7ec05e543..30539b500 100644 --- a/core/zplugin.go +++ b/core/zplugin.go @@ -23,6 +23,7 @@ import ( _ "github.com/coredns/coredns/plugin/loadbalance" _ "github.com/coredns/coredns/plugin/log" _ "github.com/coredns/coredns/plugin/metrics" + _ "github.com/coredns/coredns/plugin/nsid" _ "github.com/coredns/coredns/plugin/pprof" _ "github.com/coredns/coredns/plugin/proxy" _ "github.com/coredns/coredns/plugin/reverse" diff --git a/plugin.cfg b/plugin.cfg index cad019926..21881bf5c 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -20,6 +20,7 @@ # log:log tls:tls +nsid:nsid root:root bind:bind debug:debug diff --git a/plugin/nsid/README.md b/plugin/nsid/README.md new file mode 100644 index 000000000..3a0805d5d --- /dev/null +++ b/plugin/nsid/README.md @@ -0,0 +1,27 @@ +# nsid + +*nsid* add an identifier of this server to each reply. + +This plugin implements RFC 5001 and adds an EDNS0 OPT resource record to replies that uniquely +identifies the server. This can be useful in anycast setups to see which server was responsible for +generating the reply and for debugging. + +## Syntax + +~~ txt +nsid [DATA] +~~ + +**DATA** is the string to use in the nsid record. + +If **DATA** is not given, the host's name is used. + +## Examples + +Enable nsid: + +~~ corefile +. { + nsid +} +~~ diff --git a/plugin/nsid/nsid.go b/plugin/nsid/nsid.go new file mode 100644 index 000000000..728c14bdd --- /dev/null +++ b/plugin/nsid/nsid.go @@ -0,0 +1,54 @@ +// Package nsid implements NSID protocol +package nsid + +import ( + "encoding/hex" + + "github.com/coredns/coredns/plugin" + + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +// Nsid plugin +type Nsid struct { + Next plugin.Handler + Data string +} + +// ResponseWriter is a response writer that adds NSID response +type ResponseWriter struct { + dns.ResponseWriter + Data string +} + +// ServeDNS implements the plugin.Handler interface. +func (n Nsid) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + if option := r.IsEdns0(); option != nil { + for _, o := range option.Option { + if _, ok := o.(*dns.EDNS0_NSID); ok { + nw := &ResponseWriter{ResponseWriter: w, Data: n.Data} + return plugin.NextOrFailure(n.Name(), n.Next, ctx, nw, r) + + } + } + } + return plugin.NextOrFailure(n.Name(), n.Next, ctx, w, r) +} + +// WriteMsg implements the dns.ResponseWriter interface. +func (w *ResponseWriter) WriteMsg(res *dns.Msg) error { + if option := res.IsEdns0(); option != nil { + for _, o := range option.Option { + if e, ok := o.(*dns.EDNS0_NSID); ok { + e.Code = dns.EDNS0NSID + e.Nsid = hex.EncodeToString([]byte(w.Data)) + } + } + } + returned := w.ResponseWriter.WriteMsg(res) + return returned +} + +// Name implements the Handler interface. +func (n Nsid) Name() string { return "nsid" } diff --git a/plugin/nsid/nsid_test.go b/plugin/nsid/nsid_test.go new file mode 100644 index 000000000..143d40cba --- /dev/null +++ b/plugin/nsid/nsid_test.go @@ -0,0 +1,73 @@ +package nsid + +import ( + "encoding/hex" + "testing" + + "github.com/coredns/coredns/plugin" + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + "github.com/coredns/coredns/plugin/whoami" + + "github.com/miekg/dns" + "golang.org/x/net/context" +) + +func TestNsid(t *testing.T) { + em := Nsid{ + Data: "NSID", + } + + tests := []struct { + next plugin.Handler + qname string + qtype uint16 + expectedCode int + expectedReply string + expectedErr error + }{ + { + next: whoami.Whoami{}, + qname: ".", + expectedCode: dns.RcodeSuccess, + expectedReply: hex.EncodeToString([]byte("NSID")), + expectedErr: nil, + }, + } + + ctx := context.TODO() + + for i, tc := range tests { + req := new(dns.Msg) + if tc.qtype == 0 { + tc.qtype = dns.TypeA + } + req.SetQuestion(dns.Fqdn(tc.qname), tc.qtype) + req.Question[0].Qclass = dns.ClassINET + + req.SetEdns0(4096, false) + option := req.Extra[0].(*dns.OPT) + option.Option = append(option.Option, &dns.EDNS0_NSID{Code: dns.EDNS0NSID, Nsid: ""}) + em.Next = tc.next + + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + code, err := em.ServeDNS(ctx, rec, req) + + if err != tc.expectedErr { + t.Errorf("Test %d: Expected error %v, but got %v", i, tc.expectedErr, err) + } + if code != int(tc.expectedCode) { + t.Errorf("Test %d: Expected status code %d, but got %d", i, tc.expectedCode, code) + } + if tc.expectedReply != "" { + for _, extra := range rec.Msg.Extra { + if option, ok := extra.(*dns.OPT); ok { + e := option.Option[0].(*dns.EDNS0_NSID) + if e.Nsid != tc.expectedReply { + t.Errorf("Test %d: Expected answer %s, but got %s", i, tc.expectedReply, e.Nsid) + } + } + } + } + } +} diff --git a/plugin/nsid/setup.go b/plugin/nsid/setup.go new file mode 100644 index 000000000..104bdfd9a --- /dev/null +++ b/plugin/nsid/setup.go @@ -0,0 +1,48 @@ +package nsid + +import ( + "os" + "strings" + + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" + + "github.com/mholt/caddy" +) + +func init() { + caddy.RegisterPlugin("nsid", caddy.Plugin{ + ServerType: "dns", + Action: setup, + }) +} + +func setup(c *caddy.Controller) error { + nsid, err := nsidParse(c) + if err != nil { + return plugin.Error("nsid", err) + } + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + return Nsid{Next: next, Data: nsid} + }) + + return nil +} + +func nsidParse(c *caddy.Controller) (string, error) { + // Use hostname as the default + nsid, err := os.Hostname() + if err != nil { + nsid = "localhost" + } + for c.Next() { + args := c.RemainingArgs() + if len(args) == 0 { + return nsid, nil + } + nsid = strings.Join(args, " ") + return nsid, nil + } + return nsid, nil +} diff --git a/plugin/nsid/setup_test.go b/plugin/nsid/setup_test.go new file mode 100644 index 000000000..71ed90d0a --- /dev/null +++ b/plugin/nsid/setup_test.go @@ -0,0 +1,58 @@ +package nsid + +import ( + "os" + "strings" + "testing" + + "github.com/mholt/caddy" +) + +func TestSetupNsid(t *testing.T) { + defaultNsid, err := os.Hostname() + if err != nil { + defaultNsid = "localhost" + } + tests := []struct { + input string + shouldErr bool + expectedData string + expectedErrContent string // substring from the expected error. Empty for positive cases. + }{ + { + `nsid`, false, defaultNsid, "", + }, + { + `nsid "ps0"`, false, "ps0", "", + }, + { + `nsid "worker1"`, false, "worker1", "", + }, + { + `nsid "tf 2"`, false, "tf 2", "", + }, + } + + for i, test := range tests { + c := caddy.NewTestController("dns", test.input) + nsid, err := nsidParse(c) + + if test.shouldErr && err == nil { + t.Errorf("Test %d: Expected error but found %s for input %s", i, err, test.input) + } + + if err != nil { + if !test.shouldErr { + t.Errorf("Test %d: Expected no error but found one for input %s. Error was: %v", i, test.input, err) + } + + if !strings.Contains(err.Error(), test.expectedErrContent) { + t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) + } + } + + if !test.shouldErr && nsid != test.expectedData { + t.Errorf("Nsid not correctly set for input %s. Expected: %s, actual: %s", test.input, test.expectedData, nsid) + } + } +}