diff --git a/plugin/file/lookup.go b/plugin/file/lookup.go index a3d1eca41..7b5753278 100644 --- a/plugin/file/lookup.go +++ b/plugin/file/lookup.go @@ -203,7 +203,7 @@ func (z *Zone) Lookup(ctx context.Context, state request.Request, qname string) // Haven't found the original name. // Found wildcard. - if wildElem != nil { + if wildElem != nil && !closerENTExists(tr, qname, wildElem.Name()) { // set metadata value for the wildcard record that synthesized the result metadata.SetValueFunc(ctx, "zone/wildcard", func() string { return wildElem.Name() @@ -288,6 +288,33 @@ Out: return nil, ret, nil, rcode } +// closerENTExists reports whether there is an empty-non-terminal between the +// wildcard's parent and qname. Per RFC 4592, such an ENT is the closest +// encloser and the shallower wildcard does not apply to qname. +func closerENTExists(tr *tree.Tree, qname, wildcardName string) bool { + // The wildcard owner is "*."; anything with that exact prefix is not a closer encloser. + if len(wildcardName) < 2 || wildcardName[0] != '*' || wildcardName[1] != '.' { + return false + } + parent := wildcardName[2:] + // Walk strict ancestors of qname that are strict descendants of parent. + // Each ancestor is an ENT if the tree contains any name strictly below it. + name := qname + offset, end := dns.NextLabel(name, 0) + for !end { + name = name[offset:] + if name == parent || !dns.IsSubDomain(parent, name) { + return false + } + // An ENT exists at `name` iff tr.Next(name) returns a descendant of name. + if x, found := tr.Next(name); found && dns.IsSubDomain(name, x.Name()) { + return true + } + offset, end = dns.NextLabel(name, 0) + } + return false +} + // typeFromElem returns the type tp from e and adds signatures (if they exist) and do is true. func typeFromElem(elem *tree.Elem, tp uint16, do bool) []dns.RR { rrs := elem.Type(tp) diff --git a/plugin/file/wildcard_test.go b/plugin/file/wildcard_test.go index b944576db..c57484edf 100644 --- a/plugin/file/wildcard_test.go +++ b/plugin/file/wildcard_test.go @@ -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) + } + } +}