Skip to content

Commit

Permalink
add local plugin (coredns#4262)
Browse files Browse the repository at this point in the history
* add local plugin

See: coredns#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>
  • Loading branch information
miekg authored Nov 5, 2020
1 parent b091eff commit 7bbcf69
Show file tree
Hide file tree
Showing 9 changed files with 364 additions and 0 deletions.
1 change: 1 addition & 0 deletions core/dnsserver/zdirectives.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var Directives = []string{
"errors",
"log",
"dnstap",
"local",
"dns64",
"acl",
"any",
Expand Down
1 change: 1 addition & 0 deletions core/plugin/zplugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
67 changes: 67 additions & 0 deletions man/coredns-local.7
Original file line number Diff line number Diff line change
@@ -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.

1 change: 1 addition & 0 deletions plugin.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ prometheus:metrics
errors:errors
log:log
dnstap:dnstap
local:local
dns64:dns64
acl:acl
any:any
Expand Down
52 changes: 52 additions & 0 deletions plugin/local/README.md
Original file line number Diff line number Diff line change
@@ -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.
127 changes: 127 additions & 0 deletions plugin/local/local.go
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions plugin/local/local_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
18 changes: 18 additions & 0 deletions plugin/local/metrics.go
Original file line number Diff line number Diff line change
@@ -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.",
})
)
20 changes: 20 additions & 0 deletions plugin/local/setup.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 7bbcf69

Please sign in to comment.