plugin/etcd: allow disabling legacy apex fallback (#8468)

Signed-off-by: houyuwushang <liuluoqianqiu@outlook.com>
This commit is contained in:
houyuwushang
2026-08-24 08:56:12 +08:00
committed by GitHub
parent 3b9f85bb71
commit b8720090b5
9 changed files with 128 additions and 7 deletions

View File

@@ -37,4 +37,7 @@ type ServiceBackend interface {
} }
// Options are extra options that can be specified for a lookup. // Options are extra options that can be specified for a lookup.
type Options struct{} type Options struct {
// NoApexFallback disables the legacy zone-root lookup when apex records are absent.
NoApexFallback bool
}

View File

@@ -524,7 +524,7 @@ func checkForApex(ctx context.Context, b ServiceBackend, zone string, state requ
services, err := b.Services(ctx, state, false, opt) services, err := b.Services(ctx, state, false, opt)
state.Req.Question[0].Name = old state.Req.Question[0].Name = old
if err == nil || !b.IsNameError(err) { if err == nil || !b.IsNameError(err) || opt.NoApexFallback {
return services, err return services, err
} }

View File

@@ -604,6 +604,37 @@ func TestCheckForApexFallback(t *testing.T) {
} }
} }
func TestCheckForApexFallbackDisabled(t *testing.T) {
errName := errors.New("name not found")
calls := 0
b := &mockBackend{
mockServices: func(_ctx context.Context, state request.Request, _exact bool, _opt Options) ([]msg.Service, error) {
calls++
if state.QName() != "apex.dns.example.org." {
t.Fatalf("unexpected fallback lookup for %s", state.QName())
}
return nil, errName
},
mockNameError: func(err error) bool { return errors.Is(err, errName) },
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeA)
state := request.Request{Req: req, W: &test.ResponseWriter{}}
services, err := checkForApex(context.Background(), b, "example.org.", state, Options{NoApexFallback: true})
if !errors.Is(err, errName) {
t.Fatalf("expected name error, got %v", err)
}
if len(services) != 0 {
t.Fatalf("expected no services, got %+v", services)
}
if calls != 1 {
t.Fatalf("expected one apex lookup, got %d calls", calls)
}
if state.QName() != "example.org." {
t.Fatalf("query name was not restored: %s", state.QName())
}
}
func TestCheckForApexBackendError(t *testing.T) { func TestCheckForApexBackendError(t *testing.T) {
errBackend := context.DeadlineExceeded errBackend := context.DeadlineExceeded
calls := 0 calls := 0

View File

@@ -37,6 +37,7 @@ etcd [ZONES...] {
endpoint ENDPOINT... endpoint ENDPOINT...
credentials USERNAME PASSWORD credentials USERNAME PASSWORD
tls CERT KEY CACERT tls CERT KEY CACERT
no_apex_fallback
} }
~~~ ~~~
@@ -55,6 +56,10 @@ etcd [ZONES...] {
* three arguments - path to cert PEM file, path to client private key PEM file, path to CA PEM * three arguments - path to cert PEM file, path to client private key PEM file, path to CA PEM
file - if the server certificate is not signed by a system-installed CA and client certificate file - if the server certificate is not signed by a system-installed CA and client certificate
is needed. is needed.
* `no_apex_fallback` disables the legacy zone-root lookup used by address queries and apex-existence
checks at a configured zone apex. When `apex.dns.ZONE` does not exist, the lookup returns a name
error instead of issuing a prefix range over the complete zone subtree. Migrate zone-apex address
records to the `dns/apex` layout before enabling this option.
CoreDNS sets the minimum TLS version to TLS 1.2. The maximum TLS version, TLS 1.2 cipher suites, and CoreDNS sets the minimum TLS version to TLS 1.2. The maximum TLS version, TLS 1.2 cipher suites, and
key exchange mechanisms use the Go `crypto/tls` defaults. key exchange mechanisms use the Go `crypto/tls` defaults.
@@ -93,10 +98,11 @@ zone's root prefix for compatibility with older SkyDNS layouts. On a large zone,
the entire zone subtree. the entire zone subtree.
Store zone-apex address records below the `dns/apex` path, as shown in the examples below, to avoid Store zone-apex address records below the `dns/apex` path, as shown in the examples below, to avoid
the zone-wide fallback. The *cache* plugin reduces repeated backend reads for the same DNS question, the zone-wide fallback. Once all apex records use that layout, enable `no_apex_fallback` to prevent a
but does not reduce the size of the first prefix response. Use the *log* plugin to identify the client missing or deleted apex entry from triggering a zone-root scan. The *cache* plugin reduces repeated
and question name that trigger a lookup, and `coredns_dns_requests_total` from the *prometheus* plugin backend reads for the same DNS question, but does not reduce the size of the first prefix response.
to measure query volume by zone and type. Use the *log* plugin to identify the client and question name that trigger a lookup, and
`coredns_dns_requests_total` from the *prometheus* plugin to measure query volume by zone and type.
## Examples ## Examples

View File

@@ -40,6 +40,8 @@ type Etcd struct {
Client *etcdcv3.Client Client *etcdcv3.Client
MinLeaseTTL uint32 // minimum TTL for lease-based records MinLeaseTTL uint32 // minimum TTL for lease-based records
MaxLeaseTTL uint32 // maximum TTL for lease-based records MaxLeaseTTL uint32 // maximum TTL for lease-based records
// NoApexFallback disables the legacy zone-root lookup when apex.dns records are absent.
NoApexFallback bool
endpoints []string // Stored here as well, to aid in testing. endpoints []string // Stored here as well, to aid in testing.
} }

View File

@@ -11,7 +11,7 @@ import (
// ServeDNS implements the plugin.Handler interface. // ServeDNS implements the plugin.Handler interface.
func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (e *Etcd) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
opt := plugin.Options{} opt := plugin.Options{NoApexFallback: e.NoApexFallback}
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}
zone := plugin.Zones(e.Zones).Matches(state.Name()) zone := plugin.Zones(e.Zones).Matches(state.Name())

View File

@@ -352,4 +352,46 @@ func TestLookup(t *testing.T) {
} }
} }
func TestLookupNoApexFallback(t *testing.T) {
etc := newEtcdPlugin()
etc.Zones = []string{"strict.test."}
child := &msg.Service{Host: "10.0.0.1", Key: "x.child.strict.test."}
set(t, etc, child.Key, 0, child)
defer delete(t, etc, child.Key)
query := func() *dns.Msg {
m := new(dns.Msg)
m.SetQuestion("strict.test.", dns.TypeA)
rec := dnstest.NewRecorder(&test.ResponseWriter{})
if _, err := etc.ServeDNS(ctxt, rec, m); err != nil {
t.Fatalf("unexpected lookup error: %v", err)
}
return rec.Msg
}
legacy := query()
if legacy.Rcode != dns.RcodeSuccess || len(legacy.Answer) != 1 {
t.Fatalf("legacy fallback returned rcode %s with %d answers", dns.RcodeToString[legacy.Rcode], len(legacy.Answer))
}
etc.NoApexFallback = true
strict := query()
if strict.Rcode != dns.RcodeNameError || len(strict.Answer) != 0 {
t.Fatalf("disabled fallback returned rcode %s with %d answers", dns.RcodeToString[strict.Rcode], len(strict.Answer))
}
apex := &msg.Service{Host: "192.0.2.1", Key: "x.apex.dns.strict.test."}
set(t, etc, apex.Key, 0, apex)
defer delete(t, etc, apex.Key)
strictWithApex := query()
if strictWithApex.Rcode != dns.RcodeSuccess || len(strictWithApex.Answer) != 1 {
t.Fatalf("dedicated apex lookup returned rcode %s with %d answers", dns.RcodeToString[strictWithApex.Rcode], len(strictWithApex.Answer))
}
if got := strictWithApex.Answer[0].String(); got != "strict.test.\t300\tIN\tA\t192.0.2.1" {
t.Fatalf("unexpected apex answer: %s", got)
}
}
var ctxt context.Context var ctxt context.Context

View File

@@ -62,6 +62,11 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) {
etc.Fall.SetZonesFromArgs(c.RemainingArgs()) etc.Fall.SetZonesFromArgs(c.RemainingArgs())
case "debug": case "debug":
/* it is a noop now */ /* it is a noop now */
case "no_apex_fallback":
if len(c.RemainingArgs()) != 0 {
return &Etcd{}, c.ArgErr()
}
etc.NoApexFallback = true
case "path": case "path":
if !c.NextArg() { if !c.NextArg() {
return &Etcd{}, c.ArgErr() return &Etcd{}, c.ArgErr()

View File

@@ -162,6 +162,38 @@ func TestSetupEtcd(t *testing.T) {
} }
} }
func TestSetupNoApexFallback(t *testing.T) {
tests := []struct {
name string
input string
expected bool
wantErr bool
}{
{name: "default", input: `etcd`, expected: false},
{name: "disabled", input: "etcd {\nno_apex_fallback\n}", expected: true},
{name: "reject arguments", input: "etcd {\nno_apex_fallback unexpected\n}", wantErr: true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
c := caddy.NewTestController("dns", tc.input)
etcd, err := etcdParse(c)
if tc.wantErr {
if err == nil {
t.Fatal("expected an error")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if etcd.NoApexFallback != tc.expected {
t.Fatalf("NoApexFallback = %t, want %t", etcd.NoApexFallback, tc.expected)
}
})
}
}
func TestParseTTL(t *testing.T) { func TestParseTTL(t *testing.T) {
tests := []struct { tests := []struct {
input string input string