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>
This commit is contained in:
parent
b091eff139
commit
7bbcf6920f
9 changed files with 364 additions and 0 deletions
|
@ -27,6 +27,7 @@ var Directives = []string{
|
|||
"errors",
|
||||
"log",
|
||||
"dnstap",
|
||||
"local",
|
||||
"dns64",
|
||||
"acl",
|
||||
"any",
|
||||
|
|
|
@ -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"
|
||||
|
|
67
man/coredns-local.7
Normal file
67
man/coredns-local.7
Normal file
|
@ -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.
|
||||
|
|
@ -36,6 +36,7 @@ prometheus:metrics
|
|||
errors:errors
|
||||
log:log
|
||||
dnstap:dnstap
|
||||
local:local
|
||||
dns64:dns64
|
||||
acl:acl
|
||||
any:any
|
||||
|
|
52
plugin/local/README.md
Normal file
52
plugin/local/README.md
Normal file
|
@ -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
plugin/local/local.go
Normal file
127
plugin/local/local.go
Normal file
|
@ -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
plugin/local/local_test.go
Normal file
77
plugin/local/local_test.go
Normal file
|
@ -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
plugin/local/metrics.go
Normal file
18
plugin/local/metrics.go
Normal file
|
@ -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
plugin/local/setup.go
Normal file
20
plugin/local/setup.go
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue