aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Miek Gieben <miek@miek.nl> 2020-11-05 15:02:07 +0100
committerGravatar GitHub <noreply@github.com> 2020-11-05 15:02:07 +0100
commit7bbcf6920fcb1b9edd385465c5e0fc68c976ea9b (patch)
tree11811ef25dda6431bf5ec607c7c66af3db060652
parentb091eff139c3b53940b47b4dc51ddd7fc53357a5 (diff)
downloadcoredns-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.go1
-rw-r--r--core/plugin/zplugin.go1
-rw-r--r--man/coredns-local.767
-rw-r--r--plugin.cfg1
-rw-r--r--plugin/local/README.md52
-rw-r--r--plugin/local/local.go127
-rw-r--r--plugin/local/local_test.go77
-rw-r--r--plugin/local/metrics.go18
-rw-r--r--plugin/local/setup.go20
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
+}