diff options
author | 2020-11-05 15:02:07 +0100 | |
---|---|---|
committer | 2020-11-05 15:02:07 +0100 | |
commit | 7bbcf6920fcb1b9edd385465c5e0fc68c976ea9b (patch) | |
tree | 11811ef25dda6431bf5ec607c7c66af3db060652 | |
parent | b091eff139c3b53940b47b4dc51ddd7fc53357a5 (diff) | |
download | coredns-7bbcf6920fcb1b9edd385465c5e0fc68c976ea9b.tar.gz coredns-7bbcf6920fcb1b9edd385465c5e0fc68c976ea9b.tar.zst coredns-7bbcf6920fcb1b9edd385465c5e0fc68c976ea9b.zip |
add local plugin (#4262)
* add local plugin
See: #4260
Signed-off-by: Miek Gieben <miek@miek.nl>
* stickler bot
Signed-off-by: Miek Gieben <miek@miek.nl>
* See Also
Signed-off-by: Miek Gieben <miek@miek.nl>
-rw-r--r-- | core/dnsserver/zdirectives.go | 1 | ||||
-rw-r--r-- | core/plugin/zplugin.go | 1 | ||||
-rw-r--r-- | man/coredns-local.7 | 67 | ||||
-rw-r--r-- | plugin.cfg | 1 | ||||
-rw-r--r-- | plugin/local/README.md | 52 | ||||
-rw-r--r-- | plugin/local/local.go | 127 | ||||
-rw-r--r-- | plugin/local/local_test.go | 77 | ||||
-rw-r--r-- | plugin/local/metrics.go | 18 | ||||
-rw-r--r-- | plugin/local/setup.go | 20 |
9 files changed, 364 insertions, 0 deletions
diff --git a/core/dnsserver/zdirectives.go b/core/dnsserver/zdirectives.go index 1bf449cb4..9a7390ecd 100644 --- a/core/dnsserver/zdirectives.go +++ b/core/dnsserver/zdirectives.go @@ -27,6 +27,7 @@ var Directives = []string{ "errors", "log", "dnstap", + "local", "dns64", "acl", "any", diff --git a/core/plugin/zplugin.go b/core/plugin/zplugin.go index 102c11e97..afd77eb99 100644 --- a/core/plugin/zplugin.go +++ b/core/plugin/zplugin.go @@ -31,6 +31,7 @@ import ( _ "github.com/coredns/coredns/plugin/k8s_external" _ "github.com/coredns/coredns/plugin/kubernetes" _ "github.com/coredns/coredns/plugin/loadbalance" + _ "github.com/coredns/coredns/plugin/local" _ "github.com/coredns/coredns/plugin/log" _ "github.com/coredns/coredns/plugin/loop" _ "github.com/coredns/coredns/plugin/metadata" diff --git a/man/coredns-local.7 b/man/coredns-local.7 new file mode 100644 index 000000000..f549db0da --- /dev/null +++ b/man/coredns-local.7 @@ -0,0 +1,67 @@ +.\" Generated by Mmark Markdown Processer - mmark.miek.nl +.TH "COREDNS-LOCAL" 7 "November 2020" "CoreDNS" "CoreDNS Plugins" + +.SH "NAME" +.PP +\fIlocal\fP - respond to local names. + +.SH "DESCRIPTION" +.PP +\fIlocal\fP will respond with a basic reply to a "local request". Local request are defined to be +names in the following zones: localhost, 0.in-addr.arpa, 127.in-addr.arpa and 255.in-addr.arpa \fIand\fP +any query asking for \fB\fClocalhost.<domain>\fR. When seeing the latter a metric counter is increased and +if \fIdebug\fP is enabled a debug log is emitted. + +.PP +With \fIlocal\fP enabled any query falling under these zones will get a reply. The prevents the query +from "escaping" to the internet and putting strain on external infrastructure. + +.PP +The zones are mostly empty, only \fB\fClocalhost.\fR address records (A and AAAA) are defined and a +\fB\fC1.0.0.127.in-addr.arpa.\fR reverse (PTR) record. + +.SH "SYNTAX" +.PP +.RS + +.nf +local + +.fi +.RE + +.SH "METRICS" +.PP +If monitoring is enabled (via the \fIprometheus\fP plugin) then the following metric is exported: + +.IP \(bu 4 +\fB\fCcoredns_local_localhost_requests_total{}\fR - a counter of the number of \fB\fClocalhost.<domain>\fR +requests CoreDNS has seen. Note this does \fInot\fP count \fB\fClocalhost.\fR queries. + + +.PP +Note that this metric \fIdoes not\fP have a \fB\fCserver\fR label, because it's more interesting to find the +client(s) performing these queries than to see which server handled it. You'll need to inspect the +debug log to get the client IP address. + +.SH "EXAMPLES" +.PP +.RS + +.nf +\&. { + local +} + +.fi +.RE + +.SH "BUGS" +.PP +Only the \fB\fCin-addr.arpa.\fR reverse zone is implemented, \fB\fCip6.arpa.\fR queries are not intercepted. + +.SH "ALSO SEE" +.PP +BIND9's configuration in Debian comes with these zones preconfigured. See the \fIdebug\fP plugin for +enabling debug logging. + diff --git a/plugin.cfg b/plugin.cfg index 1c1bdfd7c..08048a3cf 100644 --- a/plugin.cfg +++ b/plugin.cfg @@ -36,6 +36,7 @@ prometheus:metrics errors:errors log:log dnstap:dnstap +local:local dns64:dns64 acl:acl any:any diff --git a/plugin/local/README.md b/plugin/local/README.md new file mode 100644 index 000000000..08fff0103 --- /dev/null +++ b/plugin/local/README.md @@ -0,0 +1,52 @@ +# local + +## Name + +*local* - respond to local names. + +## Description + +*local* will respond with a basic reply to a "local request". Local request are defined to be +names in the following zones: localhost, 0.in-addr.arpa, 127.in-addr.arpa and 255.in-addr.arpa *and* +any query asking for `localhost.<domain>`. When seeing the latter a metric counter is increased and +if *debug* is enabled a debug log is emitted. + +With *local* enabled any query falling under these zones will get a reply. The prevents the query +from "escaping" to the internet and putting strain on external infrastructure. + +The zones are mostly empty, only `localhost.` address records (A and AAAA) are defined and a +`1.0.0.127.in-addr.arpa.` reverse (PTR) record. + +## Syntax + +~~~ txt +local +~~~ + +## Metrics + +If monitoring is enabled (via the *prometheus* plugin) then the following metric is exported: + +* `coredns_local_localhost_requests_total{}` - a counter of the number of `localhost.<domain>` + requests CoreDNS has seen. Note this does *not* count `localhost.` queries. + +Note that this metric *does not* have a `server` label, because it's more interesting to find the +client(s) performing these queries than to see which server handled it. You'll need to inspect the +debug log to get the client IP address. + +## Examples + +~~~ corefile +. { + local +} +~~~ + +## Bugs + +Only the `in-addr.arpa.` reverse zone is implemented, `ip6.arpa.` queries are not intercepted. + +## See Also + +BIND9's configuration in Debian comes with these zones preconfigured. See the *debug* plugin for +enabling debug logging. diff --git a/plugin/local/local.go b/plugin/local/local.go new file mode 100644 index 000000000..570f113da --- /dev/null +++ b/plugin/local/local.go @@ -0,0 +1,127 @@ +package local + +import ( + "context" + "net" + "strings" + + "github.com/coredns/coredns/plugin" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/request" + + "github.com/miekg/dns" +) + +var log = clog.NewWithPlugin("local") + +// Local is a plugin that returns standard replies for local queries. +type Local struct { + Next plugin.Handler +} + +var zones = []string{"localhost.", "0.in-addr.arpa.", "127.in-addr.arpa.", "255.in-addr.arpa."} + +func soaFromOrigin(origin string) []dns.RR { + hdr := dns.RR_Header{Name: origin, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeSOA} + return []dns.RR{&dns.SOA{Hdr: hdr, Ns: "localhost.", Mbox: "root.localhost.", Serial: 1, Refresh: 0, Retry: 0, Expire: 0, Minttl: ttl}} +} + +func nsFromOrigin(origin string) []dns.RR { + hdr := dns.RR_Header{Name: origin, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeNS} + return []dns.RR{&dns.NS{Hdr: hdr, Ns: "localhost."}} +} + +// ServeDNS implements the plugin.Handler interface. +func (l Local) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { + state := request.Request{W: w, Req: r} + qname := state.QName() + + lc := len("localhost.") + if len(state.Name()) > lc && strings.HasPrefix(state.Name(), "localhost.") { + // we have multiple labels, but the first one is localhost, intercept this and return 127.0.0.1 or ::1 + log.Debugf("Intercepting localhost query for %q %s, from %s", state.Name(), state.Type(), state.IP()) + LocalhostCount.Inc() + reply := doLocalhost(state) + w.WriteMsg(reply) + return 0, nil + } + + zone := plugin.Zones(zones).Matches(qname) + if zone == "" { + return plugin.NextOrFailure(l.Name(), l.Next, ctx, w, r) + } + + m := new(dns.Msg) + m.SetReply(r) + zone = qname[len(qname)-len(zone):] + + switch q := state.Name(); q { + case "localhost.", "0.in-addr.arpa.", "127.in-addr.arpa.", "255.in-addr.arpa.": + switch state.QType() { + case dns.TypeA: + if q != "localhost." { + // nodata + m.Ns = soaFromOrigin(qname) + break + } + + hdr := dns.RR_Header{Name: qname, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeA} + m.Answer = []dns.RR{&dns.A{Hdr: hdr, A: net.ParseIP("127.0.0.1").To4()}} + case dns.TypeAAAA: + if q != "localhost." { + // nodata + m.Ns = soaFromOrigin(qname) + break + } + + hdr := dns.RR_Header{Name: qname, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeAAAA} + m.Answer = []dns.RR{&dns.AAAA{Hdr: hdr, AAAA: net.ParseIP("::1")}} + case dns.TypeSOA: + m.Answer = soaFromOrigin(qname) + case dns.TypeNS: + m.Answer = nsFromOrigin(qname) + default: + // nodata + m.Ns = soaFromOrigin(qname) + } + case "1.0.0.127.in-addr.arpa.": + switch state.QType() { + case dns.TypePTR: + hdr := dns.RR_Header{Name: qname, Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypePTR} + m.Answer = []dns.RR{&dns.PTR{Hdr: hdr, Ptr: "localhost."}} + default: + // nodata + m.Ns = soaFromOrigin(zone) + } + } + + if len(m.Answer) == 0 && len(m.Ns) == 0 { + m.Ns = soaFromOrigin(zone) + m.Rcode = dns.RcodeNameError + } + + w.WriteMsg(m) + return 0, nil +} + +// Name implements the plugin.Handler interface. +func (l Local) Name() string { return "local" } + +func doLocalhost(state request.Request) *dns.Msg { + m := new(dns.Msg) + m.SetReply(state.Req) + switch state.QType() { + case dns.TypeA: + hdr := dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeA} + m.Answer = []dns.RR{&dns.A{Hdr: hdr, A: net.ParseIP("127.0.0.1").To4()}} + case dns.TypeAAAA: + hdr := dns.RR_Header{Name: state.QName(), Ttl: ttl, Class: dns.ClassINET, Rrtype: dns.TypeAAAA} + m.Answer = []dns.RR{&dns.AAAA{Hdr: hdr, AAAA: net.ParseIP("::1")}} + default: + // nodata + m.Ns = soaFromOrigin(state.QName()) + } + return m +} + +const ttl = 604800 diff --git a/plugin/local/local_test.go b/plugin/local/local_test.go new file mode 100644 index 000000000..8e1561ad4 --- /dev/null +++ b/plugin/local/local_test.go @@ -0,0 +1,77 @@ +package local + +import ( + "context" + "testing" + + "github.com/coredns/coredns/plugin/pkg/dnstest" + "github.com/coredns/coredns/plugin/test" + + "github.com/miekg/dns" +) + +var testcases = []struct { + question string + qtype uint16 + rcode int + answer dns.RR + ns dns.RR +}{ + {"localhost.", dns.TypeA, dns.RcodeSuccess, test.A("localhost. IN A 127.0.0.1"), nil}, + {"localHOst.", dns.TypeA, dns.RcodeSuccess, test.A("localHOst. IN A 127.0.0.1"), nil}, + {"localhost.", dns.TypeAAAA, dns.RcodeSuccess, test.AAAA("localhost. IN AAAA ::1"), nil}, + {"localhost.", dns.TypeNS, dns.RcodeSuccess, test.NS("localhost. IN NS localhost."), nil}, + {"localhost.", dns.TypeSOA, dns.RcodeSuccess, test.SOA("localhost. IN SOA root.localhost. localhost. 1 0 0 0 0"), nil}, + {"127.in-addr.arpa.", dns.TypeA, dns.RcodeSuccess, nil, test.SOA("127.in-addr.arpa. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"localhost.", dns.TypeMX, dns.RcodeSuccess, nil, test.SOA("localhost. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"a.localhost.", dns.TypeA, dns.RcodeNameError, nil, test.SOA("localhost. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"1.0.0.127.in-addr.arpa.", dns.TypePTR, dns.RcodeSuccess, test.PTR("1.0.0.127.in-addr.arpa. IN PTR localhost."), nil}, + {"1.0.0.127.in-addr.arpa.", dns.TypeMX, dns.RcodeSuccess, nil, test.SOA("127.in-addr.arpa. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"2.0.0.127.in-addr.arpa.", dns.TypePTR, dns.RcodeNameError, nil, test.SOA("127.in-addr.arpa. IN SOA root.localhost. localhost. 1 0 0 0 0")}, + {"localhost.example.net.", dns.TypeA, dns.RcodeSuccess, test.A("localhost.example.net. IN A 127.0.0.1"), nil}, + {"localhost.example.net.", dns.TypeAAAA, dns.RcodeSuccess, test.AAAA("localhost.example.net IN AAAA ::1"), nil}, + {"localhost.example.net.", dns.TypeSOA, dns.RcodeSuccess, nil, test.SOA("localhost.example.net. IN SOA root.localhost.example.net. localhost.example.net. 1 0 0 0 0")}, +} + +func TestLocal(t *testing.T) { + req := new(dns.Msg) + l := &Local{} + + for i, tc := range testcases { + req.SetQuestion(tc.question, tc.qtype) + rec := dnstest.NewRecorder(&test.ResponseWriter{}) + _, err := l.ServeDNS(context.TODO(), rec, req) + + if err != nil { + t.Errorf("Test %d, expected no error, but got %q", i, err) + continue + } + if rec.Msg.Rcode != tc.rcode { + t.Errorf("Test %d, expected rcode %d, got %d", i, tc.rcode, rec.Msg.Rcode) + } + if tc.answer == nil && len(rec.Msg.Answer) > 0 { + t.Errorf("Test %d, expected no answer RR, got %s", i, rec.Msg.Answer[0]) + continue + } + if tc.ns == nil && len(rec.Msg.Ns) > 0 { + t.Errorf("Test %d, expected no authority RR, got %s", i, rec.Msg.Ns[0]) + continue + } + if tc.answer != nil { + if x := tc.answer.Header().Rrtype; x != rec.Msg.Answer[0].Header().Rrtype { + t.Errorf("Test %d, expected RR type %d in answer, got %d", i, x, rec.Msg.Answer[0].Header().Rrtype) + } + if x := tc.answer.Header().Name; x != rec.Msg.Answer[0].Header().Name { + t.Errorf("Test %d, expected RR name %q in answer, got %q", i, x, rec.Msg.Answer[0].Header().Name) + } + } + if tc.ns != nil { + if x := tc.ns.Header().Rrtype; x != rec.Msg.Ns[0].Header().Rrtype { + t.Errorf("Test %d, expected RR type %d in authority, got %d", i, x, rec.Msg.Ns[0].Header().Rrtype) + } + if x := tc.ns.Header().Name; x != rec.Msg.Ns[0].Header().Name { + t.Errorf("Test %d, expected RR name %q in authority, got %q", i, x, rec.Msg.Ns[0].Header().Name) + } + } + } +} diff --git a/plugin/local/metrics.go b/plugin/local/metrics.go new file mode 100644 index 000000000..361f9ab33 --- /dev/null +++ b/plugin/local/metrics.go @@ -0,0 +1,18 @@ +package local + +import ( + "github.com/coredns/coredns/plugin" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + // LocalhostCount report the number of times we've seen a localhost.<domain> query. + LocalhostCount = promauto.NewCounter(prometheus.CounterOpts{ + Namespace: plugin.Namespace, + Subsystem: "local", + Name: "localhost_requests_total", + Help: "Counter of localhost.<domain> requests.", + }) +) diff --git a/plugin/local/setup.go b/plugin/local/setup.go new file mode 100644 index 000000000..9bd0dd605 --- /dev/null +++ b/plugin/local/setup.go @@ -0,0 +1,20 @@ +package local + +import ( + "github.com/coredns/caddy" + "github.com/coredns/coredns/core/dnsserver" + "github.com/coredns/coredns/plugin" +) + +func init() { plugin.Register("local", setup) } + +func setup(c *caddy.Controller) error { + l := Local{} + + dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { + l.Next = next + return l + }) + + return nil +} |