mirror of
https://github.com/coredns/coredns.git
synced 2026-07-10 17:50:12 -04:00
fix(local): handle names under .localhost. (#8151)
This commit is contained in:
committed by
GitHub
parent
faeb8ba699
commit
e45ad5f87a
@@ -1,24 +1,26 @@
|
||||
.\" Generated by Mmark Markdown Processer - mmark.miek.nl
|
||||
.TH "COREDNS-LOCAL" 7 "March 2026" "CoreDNS" "CoreDNS Plugins"
|
||||
.\" Generated by Mmark Markdown Processor - mmark.miek.nl
|
||||
.TH "COREDNS-LOCAL" 7 "June 2026" "CoreDNS" "CoreDNS Plugins"
|
||||
|
||||
.SH "NAME"
|
||||
.PP
|
||||
\fIlocal\fP - respond to local names.
|
||||
\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.
|
||||
\fIlocal\fP will respond with a basic reply to a "local request". Local requests are defined to be
|
||||
names in the following zones: localhost, 0.in\-addr.arpa, 127.in\-addr.arpa and 255.in\-addr.arpa,
|
||||
any query under \fB\fC.localhost.\fR, and, by default for backward compatibility, any query prefixed by
|
||||
\fB\fClocalhost.\fR. When seeing one of the non\-apex localhost forms 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
|
||||
With \fIlocal\fP enabled any query falling under these zones will get a reply. This 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.
|
||||
The zones are mostly empty, only \fB\fClocalhost.\fR, names under \fB\fC.localhost.\fR, and legacy
|
||||
\fB\fClocalhost.<domain>\fR names return loopback address records (A and AAAA), and only
|
||||
\fB\fC1.0.0.127.in-addr.arpa.\fR has a reverse (PTR) record.
|
||||
|
||||
.SH "SYNTAX"
|
||||
.PP
|
||||
@@ -30,13 +32,32 @@ local
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.PP
|
||||
.RS
|
||||
|
||||
.nf
|
||||
local {
|
||||
localhost\_prefix on|off
|
||||
}
|
||||
|
||||
.fi
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\fB\fClocalhost_prefix\fR controls the legacy \fB\fClocalhost.<domain>\fR behavior. The default is \fB\fCon\fR for
|
||||
backward compatibility. Set it to \fB\fCoff\fR to only treat names under \fB\fC.localhost.\fR as special, which
|
||||
matches RFC 6761. The legacy prefix behavior is deprecated and may be disabled by default in a
|
||||
future release.
|
||||
|
||||
.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.
|
||||
\fB\fCcoredns_local_localhost_requests_total{}\fR \- a counter of the number of non\-apex localhost
|
||||
special\-case queries CoreDNS has seen. This includes \fB\fC.localhost.\fR names and, when
|
||||
\fB\fClocalhost_prefix\fR is \fB\fCon\fR, legacy \fB\fClocalhost.<domain>\fR names. It does \fInot\fP count \fB\fClocalhost.\fR
|
||||
queries.
|
||||
|
||||
|
||||
.PP
|
||||
|
||||
@@ -6,16 +6,18 @@
|
||||
|
||||
## 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.
|
||||
*local* will respond with a basic reply to a "local request". Local requests are defined to be
|
||||
names in the following zones: localhost, 0.in-addr.arpa, 127.in-addr.arpa and 255.in-addr.arpa,
|
||||
any query under `.localhost.`, and, by default for backward compatibility, any query prefixed by
|
||||
`localhost.`. When seeing one of the non-apex localhost forms 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
|
||||
With *local* enabled any query falling under these zones will get a reply. This 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.
|
||||
The zones are mostly empty, only `localhost.`, names under `.localhost.`, and legacy
|
||||
`localhost.<domain>` names return loopback address records (A and AAAA), and only
|
||||
`1.0.0.127.in-addr.arpa.` has a reverse (PTR) record.
|
||||
|
||||
## Syntax
|
||||
|
||||
@@ -23,12 +25,25 @@ The zones are mostly empty, only `localhost.` address records (A and AAAA) are d
|
||||
local
|
||||
~~~
|
||||
|
||||
~~~ txt
|
||||
local {
|
||||
localhost_prefix on|off
|
||||
}
|
||||
~~~
|
||||
|
||||
`localhost_prefix` controls the legacy `localhost.<domain>` behavior. The default is `on` for
|
||||
backward compatibility. Set it to `off` to only treat names under `.localhost.` as special, which
|
||||
matches RFC 6761. The legacy prefix behavior is deprecated and may be disabled by default in a
|
||||
future release.
|
||||
|
||||
## 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.
|
||||
* `coredns_local_localhost_requests_total{}` - a counter of the number of non-apex localhost
|
||||
special-case queries CoreDNS has seen. This includes `.localhost.` names and, when
|
||||
`localhost_prefix` is `on`, legacy `localhost.<domain>` names. It 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
|
||||
|
||||
@@ -16,7 +16,8 @@ var log = clog.NewWithPlugin("local")
|
||||
|
||||
// Local is a plugin that returns standard replies for local queries.
|
||||
type Local struct {
|
||||
Next plugin.Handler
|
||||
Next plugin.Handler
|
||||
disableLocalhostPrefix bool
|
||||
}
|
||||
|
||||
var zones = []string{"localhost.", "0.in-addr.arpa.", "127.in-addr.arpa.", "255.in-addr.arpa."}
|
||||
@@ -37,8 +38,11 @@ func (l Local) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
|
||||
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
|
||||
name := state.Name()
|
||||
matchesRFCSubdomain := len(name) > lc && strings.HasSuffix(name, ".localhost.")
|
||||
matchesLegacyPrefix := len(name) > lc && !l.disableLocalhostPrefix && strings.HasPrefix(name, "localhost.")
|
||||
if matchesRFCSubdomain || matchesLegacyPrefix {
|
||||
// Intercept special localhost subdomain queries and return loopback addresses for A/AAAA queries.
|
||||
log.Debugf("Intercepting localhost query for %q %s, from %s", state.Name(), state.Type(), state.IP())
|
||||
LocalhostCount.Inc()
|
||||
reply := doLocalhost(state)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/coredns/plugin"
|
||||
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
||||
"github.com/coredns/coredns/plugin/test"
|
||||
|
||||
@@ -24,13 +25,12 @@ var testcases = []struct {
|
||||
{"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")},
|
||||
{"a.localhost.", dns.TypeA, dns.RcodeSuccess, test.A("a.localhost. IN A 127.0.0.1"), nil},
|
||||
{"a.localhost.", dns.TypeAAAA, dns.RcodeSuccess, test.AAAA("a.localhost. IN AAAA ::1"), nil},
|
||||
{"a.localhost.", dns.TypeMX, dns.RcodeSuccess, nil, test.SOA("a.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) {
|
||||
@@ -75,3 +75,72 @@ func TestLocal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalInterceptsLocalhostPrefixByDefault(t *testing.T) {
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("localhost.example.net.", dns.TypeA)
|
||||
|
||||
nextCalled := false
|
||||
l := Local{Next: plugin.HandlerFunc(func(_ context.Context, _ dns.ResponseWriter, _ *dns.Msg) (int, error) {
|
||||
nextCalled = true
|
||||
return dns.RcodeSuccess, nil
|
||||
})}
|
||||
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
_, err := l.ServeDNS(context.TODO(), rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %q", err)
|
||||
}
|
||||
if nextCalled {
|
||||
t.Fatal("expected legacy localhost prefix query to be intercepted by default")
|
||||
}
|
||||
if rec.Msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected rcode %d, got %d", dns.RcodeSuccess, rec.Msg.Rcode)
|
||||
}
|
||||
if len(rec.Msg.Answer) != 1 {
|
||||
t.Fatalf("expected 1 answer RR, got %d", len(rec.Msg.Answer))
|
||||
}
|
||||
if got := rec.Msg.Answer[0].Header().Name; got != "localhost.example.net." {
|
||||
t.Fatalf("expected answer name %q, got %q", "localhost.example.net.", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalDoesNotInterceptLocalhostPrefixWhenDisabled(t *testing.T) {
|
||||
req := new(dns.Msg)
|
||||
req.SetQuestion("localhost.example.net.", dns.TypeA)
|
||||
|
||||
expected := test.A("localhost.example.net. IN A 192.0.2.1")
|
||||
nextCalled := false
|
||||
l := Local{
|
||||
disableLocalhostPrefix: true,
|
||||
Next: plugin.HandlerFunc(func(_ context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
|
||||
nextCalled = true
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Answer = []dns.RR{expected}
|
||||
if err := w.WriteMsg(m); err != nil {
|
||||
return dns.RcodeServerFailure, err
|
||||
}
|
||||
return dns.RcodeSuccess, nil
|
||||
}),
|
||||
}
|
||||
|
||||
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
||||
_, err := l.ServeDNS(context.TODO(), rec, req)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %q", err)
|
||||
}
|
||||
if !nextCalled {
|
||||
t.Fatal("expected query to fall through to the next plugin")
|
||||
}
|
||||
if rec.Msg.Rcode != dns.RcodeSuccess {
|
||||
t.Fatalf("expected rcode %d, got %d", dns.RcodeSuccess, rec.Msg.Rcode)
|
||||
}
|
||||
if len(rec.Msg.Answer) != 1 {
|
||||
t.Fatalf("expected 1 answer RR, got %d", len(rec.Msg.Answer))
|
||||
}
|
||||
if got := rec.Msg.Answer[0].Header().Name; got != expected.Header().Name {
|
||||
t.Fatalf("expected answer name %q, got %q", expected.Header().Name, got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// LocalhostCount report the number of times we've seen a localhost.<domain> query.
|
||||
// LocalhostCount reports the number of times we've seen a non-apex localhost special-case query.
|
||||
LocalhostCount = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Namespace: plugin.Namespace,
|
||||
Subsystem: "local",
|
||||
Name: "localhost_requests_total",
|
||||
Help: "Counter of localhost.<domain> requests.",
|
||||
Help: "Counter of special localhost subdomain requests.",
|
||||
})
|
||||
)
|
||||
|
||||
@@ -9,15 +9,10 @@ import (
|
||||
func init() { plugin.Register("local", setup) }
|
||||
|
||||
func setup(c *caddy.Controller) error {
|
||||
c.Next() // 'local'
|
||||
if c.NextArg() {
|
||||
return plugin.Error("local", c.ArgErr())
|
||||
l, err := parse(c)
|
||||
if err != nil {
|
||||
return plugin.Error("local", err)
|
||||
}
|
||||
if c.NextBlock() {
|
||||
return plugin.Error("local", c.Errf("unknown property '%s'", c.Val()))
|
||||
}
|
||||
|
||||
l := Local{}
|
||||
|
||||
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
|
||||
l.Next = next
|
||||
@@ -26,3 +21,34 @@ func setup(c *caddy.Controller) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parse(c *caddy.Controller) (Local, error) {
|
||||
l := Local{}
|
||||
|
||||
for c.Next() {
|
||||
if len(c.RemainingArgs()) != 0 {
|
||||
return l, c.ArgErr()
|
||||
}
|
||||
for c.NextBlock() {
|
||||
switch c.Val() {
|
||||
case "localhost_prefix":
|
||||
args := c.RemainingArgs()
|
||||
if len(args) != 1 {
|
||||
return l, c.ArgErr()
|
||||
}
|
||||
switch args[0] {
|
||||
case "on":
|
||||
l.disableLocalhostPrefix = false
|
||||
case "off":
|
||||
l.disableLocalhostPrefix = true
|
||||
default:
|
||||
return l, c.Errf("localhost_prefix expects 'on' or 'off', got %q", args[0])
|
||||
}
|
||||
default:
|
||||
return l, c.Errf("unknown property '%s'", c.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/coredns/caddy"
|
||||
"github.com/coredns/coredns/core/dnsserver"
|
||||
"github.com/coredns/coredns/plugin"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
@@ -32,3 +36,37 @@ func TestSetupRejectsBlockOptions(t *testing.T) {
|
||||
t.Fatal("expected error for unexpected block option, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupLocalhostPrefixOption(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", `local {
|
||||
localhost_prefix off
|
||||
}`)
|
||||
if err := setup(c); err != nil {
|
||||
t.Fatalf("expected no errors, but got: %v", err)
|
||||
}
|
||||
|
||||
cfg := dnsserver.GetConfig(c)
|
||||
if len(cfg.Plugin) != 1 {
|
||||
t.Fatalf("expected 1 plugin, got %d", len(cfg.Plugin))
|
||||
}
|
||||
|
||||
handler := cfg.Plugin[0](plugin.HandlerFunc(func(context.Context, dns.ResponseWriter, *dns.Msg) (int, error) {
|
||||
return 0, nil
|
||||
}))
|
||||
l, ok := handler.(Local)
|
||||
if !ok {
|
||||
t.Fatalf("expected Local handler, got %T", handler)
|
||||
}
|
||||
if !l.disableLocalhostPrefix {
|
||||
t.Fatal("expected localhost_prefix off to disable legacy localhost prefix handling")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupRejectsInvalidLocalhostPrefixValue(t *testing.T) {
|
||||
c := caddy.NewTestController("dns", `local {
|
||||
localhost_prefix maybe
|
||||
}`)
|
||||
if err := setup(c); err == nil {
|
||||
t.Fatal("expected error for invalid localhost_prefix value, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user