mirror of
https://github.com/coredns/coredns.git
synced 2026-07-12 10:40:10 -04:00
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>
413 lines
13 KiB
Go
413 lines
13 KiB
Go
package file
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/coredns/coredns/plugin/pkg/dnstest"
|
|
"github.com/coredns/coredns/plugin/test"
|
|
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// these examples don't have an additional opt RR set, because that's gets added by the server.
|
|
var wildcardTestCases = []test.Case{
|
|
{
|
|
Qname: "wild.dnssex.nl.", Qtype: dns.TypeTXT,
|
|
Answer: []dns.RR{
|
|
test.TXT(`wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
|
|
},
|
|
Ns: dnssexAuth[:len(dnssexAuth)-1], // remove RRSIG on the end
|
|
},
|
|
{
|
|
Qname: "a.wild.dnssex.nl.", Qtype: dns.TypeTXT,
|
|
Answer: []dns.RR{
|
|
test.TXT(`a.wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
|
|
},
|
|
Ns: dnssexAuth[:len(dnssexAuth)-1], // remove RRSIG on the end
|
|
},
|
|
{
|
|
Qname: "wild.dnssex.nl.", Qtype: dns.TypeTXT, Do: true,
|
|
Answer: []dns.RR{
|
|
test.RRSIG("wild.dnssex.nl. 1800 IN RRSIG TXT 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. FUZSTyvZfeuuOpCm"),
|
|
test.TXT(`wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
|
|
},
|
|
Ns: append([]dns.RR{
|
|
test.NSEC("a.dnssex.nl. 14400 IN NSEC www.dnssex.nl. A AAAA RRSIG NSEC"),
|
|
test.RRSIG("a.dnssex.nl. 14400 IN RRSIG NSEC 8 3 14400 20160428190224 20160329190224 14460 dnssex.nl. S+UMs2ySgRaaRY"),
|
|
}, dnssexAuth...),
|
|
},
|
|
{
|
|
Qname: "a.wild.dnssex.nl.", Qtype: dns.TypeTXT, Do: true,
|
|
Answer: []dns.RR{
|
|
test.RRSIG("a.wild.dnssex.nl. 1800 IN RRSIG TXT 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. FUZSTyvZfeuuOpCm"),
|
|
test.TXT(`a.wild.dnssex.nl. 1800 IN TXT "Doing It Safe Is Better"`),
|
|
},
|
|
Ns: append([]dns.RR{
|
|
test.NSEC("a.dnssex.nl. 14400 IN NSEC www.dnssex.nl. A AAAA RRSIG NSEC"),
|
|
test.RRSIG("a.dnssex.nl. 14400 IN RRSIG NSEC 8 3 14400 20160428190224 20160329190224 14460 dnssex.nl. S+UMs2ySgRaaRY"),
|
|
}, dnssexAuth...),
|
|
},
|
|
// nodata responses
|
|
{
|
|
Qname: "wild.dnssex.nl.", Qtype: dns.TypeSRV,
|
|
Ns: []dns.RR{
|
|
test.SOA(`dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1459281744 14400 3600 604800 14400`),
|
|
},
|
|
},
|
|
{
|
|
Qname: "wild.dnssex.nl.", Qtype: dns.TypeSRV, Do: true,
|
|
Ns: []dns.RR{
|
|
// TODO(miek): needs closest encloser proof as well? This is the wrong answer
|
|
test.NSEC(`*.dnssex.nl. 14400 IN NSEC a.dnssex.nl. TXT RRSIG NSEC`),
|
|
test.RRSIG(`*.dnssex.nl. 14400 IN RRSIG NSEC 8 2 14400 20160428190224 20160329190224 14460 dnssex.nl. os6INm6q2eXknD5z8TaaDOV+Ge/Ko+2dXnKP+J1fqJzafXJVH1F0nDrcXmMlR6jlBHA=`),
|
|
test.RRSIG(`dnssex.nl. 1800 IN RRSIG SOA 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. CA/Y3m9hCOiKC/8ieSOv8SeP964Bq++lyH8BZJcTaabAsERs4xj5PRtcxicwQXZiF8fYUCpROlUS0YR8Cdw=`),
|
|
test.SOA(`dnssex.nl. 1800 IN SOA linode.atoom.net. miek.miek.nl. 1459281744 14400 3600 604800 14400`),
|
|
},
|
|
},
|
|
}
|
|
|
|
var dnssexAuth = []dns.RR{
|
|
test.NS("dnssex.nl. 1800 IN NS linode.atoom.net."),
|
|
test.NS("dnssex.nl. 1800 IN NS ns-ext.nlnetlabs.nl."),
|
|
test.NS("dnssex.nl. 1800 IN NS omval.tednet.nl."),
|
|
test.RRSIG("dnssex.nl. 1800 IN RRSIG NS 8 2 1800 20160428190224 20160329190224 14460 dnssex.nl. dLIeEvP86jj5ndkcLzhgvWixTABjWAGRTGQsPsVDFXsGMf9TGGC9FEomgkCVeNC0="),
|
|
}
|
|
|
|
func TestLookupWildcard(t *testing.T) {
|
|
zone, err := Parse(strings.NewReader(dbDnssexNLSigned), testzone1, "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{testzone1: zone}, Names: []string{testzone1}}}
|
|
ctx := context.TODO()
|
|
|
|
for _, tc := range wildcardTestCases {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
var wildcardDoubleTestCases = []test.Case{
|
|
{
|
|
Qname: "wild.w.example.org.", Qtype: dns.TypeTXT,
|
|
Answer: []dns.RR{
|
|
test.TXT(`wild.w.example.org. IN TXT "Wildcard"`),
|
|
},
|
|
Ns: exampleAuth,
|
|
},
|
|
{
|
|
Qname: "wild.c.example.org.", Qtype: dns.TypeTXT,
|
|
Answer: []dns.RR{
|
|
test.TXT(`wild.c.example.org. IN TXT "c Wildcard"`),
|
|
},
|
|
Ns: exampleAuth,
|
|
},
|
|
{
|
|
Qname: "wild.d.example.org.", Qtype: dns.TypeTXT,
|
|
Answer: []dns.RR{
|
|
test.TXT(`alias.example.org. IN TXT "Wildcard CNAME expansion"`),
|
|
test.CNAME(`wild.d.example.org. IN CNAME alias.example.org`),
|
|
},
|
|
Ns: exampleAuth,
|
|
},
|
|
{
|
|
Qname: "alias.example.org.", Qtype: dns.TypeTXT,
|
|
Answer: []dns.RR{
|
|
test.TXT(`alias.example.org. IN TXT "Wildcard CNAME expansion"`),
|
|
},
|
|
Ns: exampleAuth,
|
|
},
|
|
}
|
|
|
|
var exampleAuth = []dns.RR{
|
|
test.NS("example.org. 3600 IN NS a.iana-servers.net."),
|
|
test.NS("example.org. 3600 IN NS b.iana-servers.net."),
|
|
}
|
|
|
|
func TestLookupDoubleWildcard(t *testing.T) {
|
|
zone, err := Parse(strings.NewReader(exampleOrg), "example.org.", "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{"example.org.": zone}, Names: []string{"example.org."}}}
|
|
ctx := context.TODO()
|
|
|
|
for _, tc := range wildcardDoubleTestCases {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReplaceWithAsteriskLabel(t *testing.T) {
|
|
tests := []struct {
|
|
in, out string
|
|
}{
|
|
{".", ""},
|
|
{"miek.nl.", "*.nl."},
|
|
{"www.miek.nl.", "*.miek.nl."},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := replaceWithAsteriskLabel(tc.in)
|
|
if got != tc.out {
|
|
t.Errorf("Expected to be %s, got %s", tc.out, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
var apexWildcardTestCases = []test.Case{
|
|
{
|
|
Qname: "foo.example.org.", Qtype: dns.TypeA,
|
|
Answer: []dns.RR{test.A(`foo.example.org. 3600 IN A 127.0.0.54`)},
|
|
Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
|
|
},
|
|
{
|
|
Qname: "bar.example.org.", Qtype: dns.TypeA,
|
|
Answer: []dns.RR{test.A(`bar.example.org. 3600 IN A 127.0.0.53`)},
|
|
Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
|
|
},
|
|
}
|
|
|
|
func TestLookupApexWildcard(t *testing.T) {
|
|
const name = "example.org."
|
|
zone, err := Parse(strings.NewReader(apexWildcard), 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 apexWildcardTestCases {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
var multiWildcardTestCases = []test.Case{
|
|
{
|
|
Qname: "foo.example.org.", Qtype: dns.TypeA,
|
|
Answer: []dns.RR{test.A(`foo.example.org. 3600 IN A 127.0.0.54`)},
|
|
Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
|
|
},
|
|
{
|
|
Qname: "bar.example.org.", Qtype: dns.TypeA,
|
|
Answer: []dns.RR{test.A(`bar.example.org. 3600 IN A 127.0.0.53`)},
|
|
Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
|
|
},
|
|
{
|
|
Qname: "bar.intern.example.org.", Qtype: dns.TypeA,
|
|
Answer: []dns.RR{test.A(`bar.intern.example.org. 3600 IN A 127.0.1.52`)},
|
|
Ns: []dns.RR{test.NS(`example.org. 3600 IN NS b.iana-servers.net.`)},
|
|
},
|
|
}
|
|
|
|
func TestLookupMultiWildcard(t *testing.T) {
|
|
const name = "example.org."
|
|
zone, err := Parse(strings.NewReader(doubleWildcard), 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 multiWildcardTestCases {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
const exampleOrg = `; example.org test file
|
|
$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 NS a.iana-servers.net.
|
|
example.org. IN A 127.0.0.1
|
|
example.org. IN A 127.0.0.2
|
|
*.w.example.org. IN TXT "Wildcard"
|
|
a.b.c.w.example.org. IN TXT "Not a wildcard"
|
|
*.c.example.org. IN TXT "c Wildcard"
|
|
*.d.example.org. IN CNAME alias.example.org.
|
|
alias.example.org. IN TXT "Wildcard CNAME expansion"
|
|
`
|
|
|
|
const apexWildcard = `; example.org test file with wildcard at apex
|
|
$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
|
|
foo.example.org. IN A 127.0.0.54
|
|
`
|
|
|
|
const doubleWildcard = `; example.org test file with wildcard at apex
|
|
$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
|
|
*.intern.example.org. IN A 127.0.1.52
|
|
foo.example.org. IN A 127.0.0.54
|
|
`
|
|
|
|
// Wildcard-synthesized answers must run additional-section processing for
|
|
// MX/SRV/SVCB/HTTPS targets, just like non-wildcard answers do.
|
|
// See https://github.com/coredns/coredns/issues/6629.
|
|
const dbWildcardAdditional = `
|
|
$TTL 30M
|
|
$ORIGIN example.org.
|
|
@ IN SOA ns.example.org. admin.example.org. 2024010100 14400 3600 604800 14400
|
|
IN NS ns.example.org.
|
|
ns IN A 192.0.2.1
|
|
mail IN A 192.0.2.10
|
|
IN AAAA 2001:db8::10
|
|
*.wild IN MX 10 mail.example.org.
|
|
`
|
|
|
|
const testWildcardAdditionalOrigin = "example.org."
|
|
|
|
var wildcardAdditionalTestCases = []test.Case{
|
|
{
|
|
// Wildcard-synthesized MX answer includes glue (A/AAAA) for the
|
|
// in-bailiwick target in the additional section.
|
|
Qname: "foo.wild.example.org.", Qtype: dns.TypeMX,
|
|
Answer: []dns.RR{
|
|
test.MX("foo.wild.example.org. 1800 IN MX 10 mail.example.org."),
|
|
},
|
|
Ns: []dns.RR{
|
|
test.NS("example.org. 1800 IN NS ns.example.org."),
|
|
},
|
|
Extra: []dns.RR{
|
|
test.A("mail.example.org. 1800 IN A 192.0.2.10"),
|
|
test.AAAA("mail.example.org. 1800 IN AAAA 2001:db8::10"),
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestLookupWildcardAdditional(t *testing.T) {
|
|
zone, err := Parse(strings.NewReader(dbWildcardAdditional), testWildcardAdditionalOrigin, "stdin", 0)
|
|
if err != nil {
|
|
t.Fatalf("Expected no error when reading zone, got %q", err)
|
|
}
|
|
|
|
fm := File{Next: test.ErrorHandler(), Zones: Zones{Z: map[string]*Zone{testWildcardAdditionalOrigin: zone}, Names: []string{testWildcardAdditionalOrigin}}}
|
|
ctx := context.TODO()
|
|
|
|
for _, tc := range wildcardAdditionalTestCases {
|
|
m := tc.Msg()
|
|
|
|
rec := dnstest.NewRecorder(&test.ResponseWriter{})
|
|
if _, err := fm.ServeDNS(ctx, rec, m); err != nil {
|
|
t.Errorf("Expected no error for %q/%d, got %v", tc.Qname, tc.Qtype, err)
|
|
continue
|
|
}
|
|
|
|
if err := test.SortAndCheck(rec.Msg, tc); err != nil {
|
|
t.Errorf("Test %q/%d: %v", tc.Qname, tc.Qtype, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|