mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-31 02:03:20 -04:00 
			
		
		
		
	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:
		| @@ -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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user