plugin/hosts: make unsupported type fallthrough opt-in (#8282)

This commit is contained in:
houyuwushang
2026-07-20 10:59:12 +08:00
committed by GitHub
parent 201d86a098
commit 98bdb44f4d
6 changed files with 154 additions and 22 deletions

View File

@@ -70,3 +70,11 @@ plugin/trace: Correct Zipkin v2 endpoint docs (https://github.com/coredns/coredn
plugin/transfer: Configure notify source address (https://github.com/coredns/coredns/pull/8192) plugin/transfer: Configure notify source address (https://github.com/coredns/coredns/pull/8192)
plugin/transfer: Fix panic in CoreDNS transfer plugin caused by empty DNS record (https://github.com/coredns/coredns/pull/8207) plugin/transfer: Fix panic in CoreDNS transfer plugin caused by empty DNS record (https://github.com/coredns/coredns/pull/8207)
plugin/tsig: Don't echo client's TSIG.Error if verification is successful (https://github.com/coredns/coredns/pull/8215) plugin/tsig: Don't echo client's TSIG.Error if verification is successful (https://github.com/coredns/coredns/pull/8215)
### Hosts Fallthrough Behavior Change
In CoreDNS 1.14.5, queries for record types unsupported by the *hosts* plugin continue to the next
plugin when `fallthrough` is configured, even if the queried name has an A or AAAA entry in the
hosts data. Previously, those queries received an authoritative NODATA response. Deployments that
use hosts entries as split-horizon overrides should review whether forwarding TXT, HTTPS, SVCB, or
other unsupported query types upstream is appropriate.

View File

@@ -73,6 +73,7 @@ hosts [FILE [ZONES...]] {
no_reverse no_reverse
reload DURATION reload DURATION
fallthrough [ZONES...] fallthrough [ZONES...]
fallthrough_unsupported
} }
~~~ ~~~
@@ -93,6 +94,12 @@ hosts [FILE [ZONES...]] {
If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin If **[ZONES...]** is omitted, then fallthrough happens for all zones for which the plugin
is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only is authoritative. If specific zones are listed (for example `in-addr.arpa` and `ip6.arpa`), then only
queries for those zones will be subject to fallthrough. queries for those zones will be subject to fallthrough.
By default, queries for unsupported record types return an authoritative NODATA response when
the queried name has an A or AAAA entry in the hosts data.
* `fallthrough_unsupported` extends `fallthrough` to unsupported query types when the queried name
exists in the hosts data. For example, TXT, HTTPS, and SVCB queries for a name with an A or AAAA
entry are passed to the next plugin. If that plugin is *forward*, these queries are sent upstream.
This option requires `fallthrough` and uses the same zone scope.
## Metrics ## Metrics

View File

@@ -18,6 +18,8 @@ type Hosts struct {
*Hostsfile *Hostsfile
Fall fall.F Fall fall.F
fallthroughUnsupported bool
} }
// ServeDNS implements the plugin.Handle interface. // ServeDNS implements the plugin.Handle interface.
@@ -51,7 +53,7 @@ func (h Hosts) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (
ips := h.LookupStaticHostV6(qname) ips := h.LookupStaticHostV6(qname)
answers = aaaa(qname, h.options.ttl, ips) answers = aaaa(qname, h.options.ttl, ips)
default: default:
if h.Fall.Through(qname) { if h.fallthroughUnsupported && h.Fall.Through(qname) {
return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r) return plugin.NextOrFailure(h.Name(), h.Next, ctx, w, r)
} }
} }

View File

@@ -58,31 +58,81 @@ func TestLookupA(t *testing.T) {
} }
func TestFallthroughUnsupportedType(t *testing.T) { func TestFallthroughUnsupportedType(t *testing.T) {
h := Hosts{ tests := []struct {
Next: test.NextHandler(dns.RcodeRefused, nil), name string
Hostsfile: &Hostsfile{ qname string
Origins: []string{"."}, fall fall.F
hmap: newMap(), unsupported bool
inline: newMap(), expectFallthrough bool
options: newOptions(), }{
{
name: "existing name returns nodata by default",
qname: "example.org.",
fall: fall.Root,
},
{
name: "existing name falls through with opt-in",
qname: "example.org.",
fall: fall.Root,
unsupported: true,
expectFallthrough: true,
},
{
name: "opt-in respects fallthrough zones",
qname: "example.org.",
fall: fall.F{Zones: []string{"example.net."}},
unsupported: true,
},
{
name: "missing name still falls through",
qname: "missing.example.org.",
fall: fall.Root,
expectFallthrough: true,
}, },
Fall: fall.Root,
} }
h.hmap = h.parse(strings.NewReader(hostsExample))
m := new(dns.Msg) for _, tc := range tests {
m.SetQuestion("example.org.", dns.TypeTXT) t.Run(tc.name, func(t *testing.T) {
rec := dnstest.NewRecorder(&test.ResponseWriter{}) h := Hosts{
Next: test.NextHandler(dns.RcodeRefused, nil),
Hostsfile: &Hostsfile{
Origins: []string{"."},
hmap: newMap(),
inline: newMap(),
options: newOptions(),
},
Fall: tc.fall,
fallthroughUnsupported: tc.unsupported,
}
h.hmap = h.parse(strings.NewReader(hostsExample))
rcode, err := h.ServeDNS(context.Background(), rec, m) m := new(dns.Msg)
if err != nil { m.SetQuestion(tc.qname, dns.TypeTXT)
t.Fatalf("Expected no error, got %v", err) rec := dnstest.NewRecorder(&test.ResponseWriter{})
}
if rcode != dns.RcodeRefused { rcode, err := h.ServeDNS(context.Background(), rec, m)
t.Fatalf("Expected fallthrough rcode %d, got %d", dns.RcodeRefused, rcode) if err != nil {
} t.Fatalf("Expected no error, got %v", err)
if rec.Msg != nil { }
t.Fatalf("Expected no response from hosts after fallthrough, got %#v", rec.Msg) if tc.expectFallthrough {
if rcode != dns.RcodeRefused {
t.Fatalf("Expected fallthrough rcode %d, got %d", dns.RcodeRefused, rcode)
}
if rec.Msg != nil {
t.Fatalf("Expected no response from hosts after fallthrough, got %#v", rec.Msg)
}
return
}
if rcode != dns.RcodeSuccess {
t.Fatalf("Expected authoritative NODATA rcode %d, got %d", dns.RcodeSuccess, rcode)
}
if rec.Msg == nil {
t.Fatal("Expected authoritative NODATA response from hosts, got no response")
}
if !rec.Msg.Authoritative || len(rec.Msg.Answer) != 0 {
t.Fatalf("Expected authoritative NODATA response, got %#v", rec.Msg)
}
})
} }
} }

View File

@@ -112,6 +112,11 @@ func hostsParse(c *caddy.Controller) (Hosts, error) {
switch c.Val() { switch c.Val() {
case "fallthrough": case "fallthrough":
h.Fall.SetZonesFromArgs(c.RemainingArgs()) h.Fall.SetZonesFromArgs(c.RemainingArgs())
case "fallthrough_unsupported":
if len(c.RemainingArgs()) != 0 {
return h, c.ArgErr()
}
h.fallthroughUnsupported = true
case "no_reverse": case "no_reverse":
h.options.autoReverse = false h.options.autoReverse = false
case "ttl": case "ttl":
@@ -150,6 +155,9 @@ func hostsParse(c *caddy.Controller) (Hosts, error) {
} }
} }
} }
if h.fallthroughUnsupported && len(h.Fall.Zones) == 0 {
return h, c.Err("fallthrough_unsupported requires fallthrough")
}
h.initInline(inline) h.initInline(inline)

View File

@@ -167,3 +167,60 @@ func TestHostsInlineParse(t *testing.T) {
} }
} }
} }
func TestHostsParseFallthroughUnsupported(t *testing.T) {
tests := []struct {
name string
input string
shouldErr bool
}{
{
name: "with fallthrough",
input: `hosts {
fallthrough
fallthrough_unsupported
}`,
},
{
name: "before fallthrough",
input: `hosts {
fallthrough_unsupported
fallthrough example.org
}`,
},
{
name: "without fallthrough",
input: `hosts {
fallthrough_unsupported
}`,
shouldErr: true,
},
{
name: "with arguments",
input: `hosts {
fallthrough
fallthrough_unsupported example.org
}`,
shouldErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := caddy.NewTestController("dns", test.input)
h, err := hostsParse(c)
if test.shouldErr {
if err == nil {
t.Fatal("Expected an error, got none")
}
return
}
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if !h.fallthroughUnsupported {
t.Fatal("Expected fallthrough_unsupported to be enabled")
}
})
}
}