plugin/file: do not expand wildcard across a closer empty non-terminal (#8223)

A query for a name that sits below a closer empty non-terminal was wrongly
answered with a shallower wildcard record instead of NXDOMAIN. Per RFC 4592
2.2.1 a wildcard is the source of synthesis only when the wildcard owner's
parent is the closest encloser of the queried name; if an empty non-terminal
exists between that parent and the queried name, it is the closer encloser and
the shallower wildcard must not be expanded.

Guard the wildcard expansion in Zone.Lookup with closerENTExists, which walks
the strict ancestors of the queried name between the wildcard parent and the
name and reports whether any of them is an empty non-terminal.

Adds TestLookupWildcardRespectsCloserEmptyNonTerminal, which asserts NXDOMAIN
for a name below a closer empty non-terminal and keeps a no-regression case
where a plain wildcard with no closer empty non-terminal still applies.

Signed-off-by: Omkhar Arasaratnam <omkhar@linkedin.com>
Co-authored-by: Omkhar Arasaratnam <omkhar@linkedin.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Omkhar Arasaratnam
2026-07-11 06:03:52 -04:00
committed by GitHub
parent af1e34ebc8
commit 38e26f25c4
2 changed files with 84 additions and 1 deletions

View File

@@ -354,3 +354,59 @@ func TestLookupWildcardAdditional(t *testing.T) {
}
}
}
const closerENTWildcard = `; example.org test file with a wildcard and a deeper empty non-terminal
$TTL 3600
example.org. IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600
example.org. IN NS b.iana-servers.net.
*.example.org. IN A 127.0.0.53
real.deeper.example.org. IN A 127.0.0.54
`
var closerENTWildcardTestCases = []test.Case{
// deeper.example.org. is an empty non-terminal (real.deeper.example.org.
// exists below it), so it is a closer encloser than the wildcard's parent
// example.org. A name under it with no record and no *.deeper.example.org.
// wildcard is an NXDOMAIN, not the *.example.org. wildcard.
{
Qname: "nonexistent.deeper.example.org.", Qtype: dns.TypeA,
Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA(`example.org. 3600 IN SOA sns.dns.icann.org. noc.dns.icann.org. 2015082541 7200 3600 1209600 3600`),
},
},
// No closer empty non-terminal between example.org. and the queried name, so
// the wildcard still applies exactly as before.
{
Qname: "nonexistent.example.org.", Qtype: dns.TypeA,
Answer: []dns.RR{test.A(`nonexistent.example.org. 3600 IN A 127.0.0.53`)},
Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
},
}
func TestLookupWildcardRespectsCloserEmptyNonTerminal(t *testing.T) {
const name = "example.org."
zone, err := Parse(strings.NewReader(closerENTWildcard), name, "stdin", 0)
if err != nil {
t.Fatalf("Expect no error when reading zone, got %q", err)
}
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{name: zone}, Names: []string{name}}}
ctx := context.TODO()
for _, tc := range closerENTWildcardTestCases {
m := tc.Msg()
rec := dnstest.NewRecorder(&test.ResponseWriter{})
_, err := fm.ServeDNS(ctx, rec, m)
if err != nil {
t.Errorf("Expected no error, got %v", err)
return
}
resp := rec.Msg
if err := test.SortAndCheck(resp, tc); err != nil {
t.Error(err)
}
}
}