Commit 7bbcf692 authored by Miek Gieben's avatar Miek Gieben Committed by GitHub

add local plugin (#4262)

* add local plugin

See: #4260
Signed-off-by: default avatarMiek Gieben <miek@miek.nl>

* stickler bot
Signed-off-by: default avatarMiek Gieben <miek@miek.nl>

* See Also
Signed-off-by: default avatarMiek Gieben <miek@miek.nl>
parent b091eff1
...@@ -27,6 +27,7 @@ var Directives = []string{ ...@@ -27,6 +27,7 @@ var Directives = []string{
"errors", "errors",
"log", "log",
"dnstap", "dnstap",
"local",
"dns64", "dns64",
"acl", "acl",
"any", "any",
......
...@@ -31,6 +31,7 @@ import ( ...@@ -31,6 +31,7 @@ import (
_ "github.com/coredns/coredns/plugin/k8s_external" _ "github.com/coredns/coredns/plugin/k8s_external"
_ "github.com/coredns/coredns/plugin/kubernetes" _ "github.com/coredns/coredns/plugin/kubernetes"
_ "github.com/coredns/coredns/plugin/loadbalance" _ "github.com/coredns/coredns/plugin/loadbalance"
_ "github.com/coredns/coredns/plugin/local"
_ "github.com/coredns/coredns/plugin/log" _ "github.com/coredns/coredns/plugin/log"
_ "github.com/coredns/coredns/plugin/loop" _ "github.com/coredns/coredns/plugin/loop"
_ "github.com/coredns/coredns/plugin/metadata" _ "github.com/coredns/coredns/plugin/metadata"
......
.\" 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.
...@@ -36,6 +36,7 @@ prometheus:metrics ...@@ -36,6 +36,7 @@ prometheus:metrics
errors:errors errors:errors
log:log log:log
dnstap:dnstap dnstap:dnstap
local:local
dns64:dns64 dns64:dns64
acl:acl acl:acl
any:any any:any
......
# 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.
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
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)
}
}
}
}
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.",
})
)
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
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment