Ready for CoreDNS testing

This commit is contained in:
Dessa Simpson 2025-12-01 01:13:27 -07:00
parent 772469d21b
commit b79df4bf1d
9 changed files with 290 additions and 124 deletions

6
TODO.txt Normal file
View file

@ -0,0 +1,6 @@
- CNAME resolution
- Ensure NS records don't point inward (will require changes to unrelated tests)
- Alias values via `!alias <hostname> <type> [index, default 0]`
- e.g. `www: !alias example.com A`
- Maybe ALIAS "records" too, to reduce complexity vs
- Either way, resolved at validate time, not at query time, and only searches within loaded zones, not external

16
testdata/bad_cname_with_other.yaml vendored Normal file
View file

@ -0,0 +1,16 @@
org:
example:
"@":
- type: SOA
value: ns1.example.com. admin.example.com. 1 1 1 1 1
- type: A
value: 192.0.2.100
- type: AAAA
value: 2001:db8::100
- type: NS
value: ns1.example.com
"ext":
- type: CNAME
value: example.com
- type: A
value: 1.1.1.1

16
testdata/bad_glue_with_other.yaml vendored Normal file
View file

@ -0,0 +1,16 @@
org:
example:
"@":
- type: SOA
value: ns1.example.com. admin.example.com. 1 1 1 1 1
- type: NS
value: ns1.example.com
delegated:
"@":
- type: NS
value: ns1.delegated.example.org
"ns1":
- type: A
value: 1.2.3.4
- type: TXT
value: foobar

16
testdata/bad_ns_with_other.yaml vendored Normal file
View file

@ -0,0 +1,16 @@
org:
example:
"@":
- type: SOA
value: ns1.example.com. admin.example.com. 1 1 1 1 1
- type: A
value: 192.0.2.100
- type: AAAA
value: 2001:db8::100
- type: NS
value: ns1.example.com
"ext":
- type: NS
value: ns1.example.com
- type: A
value: 1.1.1.1

14
testdata/bad_ns_with_subzone.yaml vendored Normal file
View file

@ -0,0 +1,14 @@
org:
example:
"@":
- type: SOA
value: ns1.example.com. admin.example.com. 1 1 1 1 1
- type: NS
value: ns1.example.com
delegated:
"@":
- type: NS
value: ns1.example.com
subzone:
- type: A
value: 1.2.3.4

9
testdata/zones.yaml vendored
View file

@ -59,6 +59,15 @@ com: # Comment
- type: NS - type: NS
ttl: 3600 ttl: 3600
value: ns2.example.org value: ns2.example.org
client:
"@":
- type: NS
ttl: 3600
value: ns1.client.example.com
ns1:
- type: A
ttl: 3600
value: 192.0.2.3
folders: !include nested/zone.yaml folders: !include nested/zone.yaml
org: org:
example: !include example.org.yaml example: !include example.org.yaml

View file

@ -26,26 +26,6 @@ type YamlPluginConfig struct {
var log = clog.NewWithPlugin("yaml") var log = clog.NewWithPlugin("yaml")
func (y YamlPlugin) lookupRRs(qname string, qtype string) ([]dns.RR, bool) {
records, ok := y.Zone.LookupType(qname, qtype)
if !ok {
return nil, false
}
rrs := []dns.RR{}
for _, record := range records {
ttl := record.Ttl
if ttl == 0 {
ttl = y.Config.DefaultTtl
}
rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s", qname, ttl, record.Type, record.Value))
if err != nil {
return nil, false
}
rrs = append(rrs, rr)
}
return rrs, true
}
func (y YamlPlugin) Name() string { return "yaml" } func (y YamlPlugin) Name() string { return "yaml" }
func (y YamlPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { func (y YamlPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
@ -57,17 +37,16 @@ func (y YamlPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.M
reply.SetReply(r) reply.SetReply(r)
reply.Authoritative = true reply.Authoritative = true
rrs, ok := y.lookupRRs(qname, qtype) rcode, err := y.lookupRRs(qname, qtype, reply)
if !ok { if err != nil {
return dns.RcodeNameError, nil return dns.RcodeServerFailure, fmt.Errorf("failed to lookup RRs for %s %s: %w", qname, qtype, err)
} }
reply.Answer = rrs
w.WriteMsg(reply) w.WriteMsg(reply)
return dns.RcodeSuccess, nil return rcode, nil
} }
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
c.Next() // yaml c.Next() // skip "yaml"
filename := "zones.yaml" filename := "zones.yaml"
if c.NextArg() { if c.NextArg() {
filename = c.Val() filename = c.Val()
@ -97,3 +76,37 @@ func setup(c *caddy.Controller) error {
} }
func init() { plugin.Register("yaml", setup) } func init() { plugin.Register("yaml", setup) }
func (y YamlPlugin) lookupRRs(qname string, qtype string, reply *dns.Msg) (int, error) {
var rcode int
res, ok := y.Zone.LookupType(qname, qtype)
if !ok {
// NXDOMAIN
rcode = dns.RcodeNameError
}
if res.IsReferral {
reply.Authoritative = false
}
sections := []struct {
source []NamedRecord
dest *[]dns.RR
}{
{res.Answer, &reply.Answer},
{res.Ns, &reply.Ns},
{res.Extra, &reply.Extra},
}
for _, section := range sections {
for _, record := range section.source {
ttl := record.Record.Ttl
if ttl == 0 {
ttl = y.Config.DefaultTtl
}
rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s", record.Name, ttl, record.Record.Type, record.Record.Value))
if err != nil {
return rcode, fmt.Errorf("failed to generate RR for %s %s %s: %w", record.Name, record.Record.Type, record.Record.Value, err)
}
*section.dest = append(*section.dest, rr)
}
}
return rcode, nil
}

View file

@ -18,11 +18,24 @@ type Record struct {
Value string Value string
} }
type NamedRecord struct {
Name string
Record Record
}
type Zone struct { type Zone struct {
Subzones map[string]*Zone Subzones map[string]*Zone
Records []Record Records []Record
GlueRecords []Record SOA NamedRecord
IsDelegationPoint bool IsDelegationPoint bool
GlueRecords []NamedRecord // Only set (optionally) for delegation points
}
type LookupResult struct {
Answer []NamedRecord
Ns []NamedRecord
Extra []NamedRecord
IsReferral bool
} }
type contextKey string type contextKey string
@ -57,31 +70,40 @@ func LoadZoneBytes(data []byte, filename string) (*Zone, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := z.Validate(".", false); err != nil { if err := z.Validate(".", nil, nil); err != nil {
return nil, err return nil, err
} }
return z, nil return z, nil
} }
func (z *Zone) Validate(name string, zoneApexPresent bool) error { func (z *Zone) Validate(name string, soa *Record, ns *[]Record) error {
nameservers := map[string]bool{} nameservers, otherRecords := []Record{}, []Record{}
otherRecords := []Record{} nameserversMap := map[string]bool{}
isZoneApex := false isZoneApex := false
cnameCount := 0
for _, record := range z.Records { for _, record := range z.Records {
switch record.Type { switch record.Type {
case "SOA": case "SOA":
isZoneApex = true isZoneApex = true
soa = &record
z.SOA = NamedRecord{Name: name, Record: record}
case "NS": case "NS":
nameservers[record.Value] = true nameservers = append(nameservers, record)
nameserversMap[record.Value] = true
case "CNAME":
cnameCount++
default: default:
otherRecords = append(otherRecords, record) otherRecords = append(otherRecords, record)
} }
} }
zoneApexPresent = zoneApexPresent || isZoneApex if cnameCount > 1 || (cnameCount > 0 && len(otherRecords) > 0) {
return fmt.Errorf("%s: extraneous records found next to CNAME", name)
}
z.IsDelegationPoint = !isZoneApex && len(nameservers) > 0 z.IsDelegationPoint = !isZoneApex && len(nameservers) > 0
if !zoneApexPresent { if soa == nil {
// Outside zone // Outside zone
if z.IsDelegationPoint { if z.IsDelegationPoint {
return fmt.Errorf("%s: delegation point found outside zone", name) return fmt.Errorf("%s: delegation point found outside zone", name)
@ -93,51 +115,51 @@ func (z *Zone) Validate(name string, zoneApexPresent bool) error {
if len(nameservers) == 0 { if len(nameservers) == 0 {
return fmt.Errorf("%s: zone apex missing NS records", name) return fmt.Errorf("%s: zone apex missing NS records", name)
} }
ns = &nameservers
} else if len(nameservers) > 0 { } else if len(nameservers) > 0 {
// Delegation point (does not fall through to subzone validation) // Delegation point (does not fall through to subzone validation)
if len(otherRecords) > 0 { if len(otherRecords) > 0 {
return fmt.Errorf("%s: non-glue, non-NS records found at delegation point: %v", name, otherRecords) return fmt.Errorf("%s: non-glue, non-NS records found at delegation point: %v", name, otherRecords)
} }
for subname, subzone := range z.Subzones { for subname, subzone := range z.Subzones {
glueRecords, err := subzone.GetGlueRecords(concatName(name, subname), nameservers) // This populates z.GlueRecords directly
err := subzone.GetGlueRecords(concatName(name, subname), nameserversMap)
if err != nil { if err != nil {
return err return err
} }
z.GlueRecords = append(z.GlueRecords, glueRecords...)
} }
return nil return nil
} }
// Subzone validation // Subzone validation
// Either we're outside a zone, at a zone apex, or at a non-delegated subzone // Either we're outside a zone, at a zone apex, or at a non-delegated subzone
for subname, subzone := range z.Subzones { for subname, subzone := range z.Subzones {
if err := subzone.Validate(concatName(name, subname), zoneApexPresent); err != nil { if err := subzone.Validate(concatName(name, subname), soa, ns); err != nil {
return err return err
} }
} }
return nil return nil
} }
func (z *Zone) GetGlueRecords(name string, nameservers map[string]bool) ([]Record, error) { func (z *Zone) GetGlueRecords(name string, nameserversMap map[string]bool) error {
// If the domain is not a nameserver, it must have no records // If the domain is not a nameserver, it must have no records
if _, ok := nameservers[name]; !ok { if _, ok := nameserversMap[strings.TrimSuffix(name, ".")]; !ok {
if len(z.Records) > 0 { if len(z.Records) > 0 {
return nil, fmt.Errorf("%s: non-glue records found under delegation point: %v", name, z.Records) return fmt.Errorf("%s: non-glue records found under delegation point: %v", name, z.Records)
} }
} }
// Any records under a delegation point must be glue records // Any records under a delegation point must be glue records
for _, record := range z.Records { for _, record := range z.Records {
if !(record.Type == "A" || record.Type == "AAAA") { if !(record.Type == "A" || record.Type == "AAAA") {
return nil, fmt.Errorf("%s: non-glue record found under delegation point: %v", name, record) return fmt.Errorf("%s: non-glue record found under delegation point: %v", name, record)
} }
z.GlueRecords = append(z.GlueRecords, NamedRecord{Name: name, Record: record})
} }
for subname, subzone := range z.Subzones { for subname, subzone := range z.Subzones {
if glueRecords, err := subzone.GetGlueRecords(concatName(name, subname), nameservers); err != nil { if err := subzone.GetGlueRecords(concatName(name, subname), nameserversMap); err != nil {
return nil, err return err
} else {
z.GlueRecords = append(z.GlueRecords, glueRecords...)
} }
} }
return z.GlueRecords, nil return nil
} }
func (r *Record) UnmarshalYAML(ctx context.Context, data []byte) error { func (r *Record) UnmarshalYAML(ctx context.Context, data []byte) error {
@ -252,38 +274,67 @@ func nameToPath(name string) []string {
return path return path
} }
func (z *Zone) Lookup(name string) ([]Record, bool) { func (z *Zone) Lookup(name string) (LookupResult, bool) {
res := LookupResult{}
path := nameToPath(name) path := nameToPath(name)
for _, label := range path { for _, label := range path {
// Support empty name and trailing dot // Support empty name and trailing dot
if label == "" { if label == "" {
continue continue
} }
if sz, ok := z.Subzones[label]; ok { if z.IsDelegationPoint {
z = sz res.IsReferral = true
// Capture NS records
for _, record := range z.Records {
if record.Type == "NS" {
res.Ns = append(res.Ns, NamedRecord{Name: name, Record: record})
}
}
// Retrieve glue records from cache
res.Extra = append(res.Extra, z.GlueRecords...)
return res, true
}
if sz, ok := z.Subzones[label]; !ok {
// NXDOMAIN
res.Ns = []NamedRecord{z.SOA}
return res, false
} else { } else {
return nil, false z = sz
} }
} }
return z.Records, true res.Answer = []NamedRecord{}
for _, record := range z.Records {
res.Answer = append(res.Answer, NamedRecord{Name: name, Record: record})
}
if len(res.Answer) == 0 {
// NODATA
res.Ns = []NamedRecord{z.SOA}
return res, true
}
return res, true
} }
func (z *Zone) FilterRecords(records []Record, recordType string) []Record { func (z *Zone) FilterRecords(res LookupResult, recordType string) LookupResult {
filtered := []Record{} filtered := []NamedRecord{}
for _, record := range records { for _, nr := range res.Answer {
if record.Type == recordType { if nr.Record.Type == recordType {
filtered = append(filtered, record) filtered = append(filtered, nr)
} }
} }
return filtered res.Answer = filtered
if len(res.Answer) == 0 && !res.IsReferral {
// This ends up as NODATA even if it had data before filtering
res.Ns = []NamedRecord{z.SOA}
}
return res
} }
func (z *Zone) LookupType(name string, recordType string) ([]Record, bool) { func (z *Zone) LookupType(name string, recordType string) (LookupResult, bool) {
records, ok := z.Lookup(name) res, ok := z.Lookup(name)
if !ok { if !ok {
return nil, false return LookupResult{}, false
} }
return z.FilterRecords(records, recordType), true return z.FilterRecords(res, recordType), true
} }
func concatName(name string, subname string) string { func concatName(name string, subname string) string {

View file

@ -14,15 +14,15 @@ func assertOk(t *testing.T, ok bool) {
} }
} }
func assertNotOk(t *testing.T, ok bool, records []Record) { func assertNotOk(t *testing.T, ok bool, res LookupResult) {
if ok { if ok {
t.Fatalf("Expected not ok, got ok (%v)", records) t.Fatalf("Expected not ok, got ok (%v)", res)
} }
} }
func assertRecordCount(t *testing.T, records []Record, expected int) { func assertRecordCount(t *testing.T, res LookupResult, expected int) {
if len(records) != expected { if len(res.Answer) != expected {
t.Fatalf("Expected %d records, got %d (%v)", expected, len(records), records) t.Fatalf("Expected %d records, got %d (%v)", expected, len(res.Answer), res.Answer)
} }
} }
@ -65,25 +65,25 @@ func TestEmptyZone(t *testing.T) {
} }
t.Run("Lookup empty string", func(t *testing.T) { t.Run("Lookup empty string", func(t *testing.T) {
records, ok := zEmpty.Lookup("") res, ok := zEmpty.Lookup("")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 0) assertRecordCount(t, res, 0)
}) })
t.Run("Lookup single dot", func(t *testing.T) { t.Run("Lookup single dot", func(t *testing.T) {
records, ok := zEmpty.Lookup(".") res, ok := zEmpty.Lookup(".")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 0) assertRecordCount(t, res, 0)
}) })
t.Run("Lookup example.com", func(t *testing.T) { t.Run("Lookup example.com", func(t *testing.T) {
records, ok := zEmpty.Lookup("example.com") res, ok := zEmpty.Lookup("example.com")
assertNotOk(t, ok, records) assertNotOk(t, ok, res)
}) })
t.Run("LookupType example.com A", func(t *testing.T) { t.Run("LookupType example.com A", func(t *testing.T) {
records, ok := zEmpty.LookupType("example.com", "A") res, ok := zEmpty.LookupType("example.com", "A")
assertNotOk(t, ok, records) assertNotOk(t, ok, res)
}) })
} }
@ -104,23 +104,23 @@ func TestSimpleZone(t *testing.T) {
} }
t.Run("Lookup", func(t *testing.T) { t.Run("Lookup", func(t *testing.T) {
records, ok := zSimple.Lookup("") res, ok := zSimple.Lookup("")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 4) assertRecordCount(t, res, 4)
assertRecord(t, records[0], "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1") assertRecord(t, res.Answer[0].Record, "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1")
assertRecord(t, records[1], "A", 0, "192.0.2.100") assertRecord(t, res.Answer[1].Record, "A", 0, "192.0.2.100")
assertRecord(t, records[2], "AAAA", 0, "2001:db8::100") assertRecord(t, res.Answer[2].Record, "AAAA", 0, "2001:db8::100")
assertRecord(t, records[3], "NS", 0, "ns1.example.com") assertRecord(t, res.Answer[3].Record, "NS", 0, "ns1.example.com")
}) })
t.Run("LookupType", func(t *testing.T) { t.Run("LookupType", func(t *testing.T) {
records, ok := zSimple.LookupType("", "A") res, ok := zSimple.LookupType("", "A")
if !ok { if !ok {
t.Fatalf("Expected ok, got false") t.Fatalf("Expected ok, got false")
} }
assertRecordCount(t, records, 1) assertRecordCount(t, res, 1)
assertRecord(t, records[0], "A", 0, "192.0.2.100") assertRecord(t, res.Answer[0].Record, "A", 0, "192.0.2.100")
}) })
} }
@ -141,93 +141,93 @@ func TestFullZone(t *testing.T) {
} }
t.Run("Lookup example.com", func(t *testing.T) { t.Run("Lookup example.com", func(t *testing.T) {
records, ok := zFull.Lookup("example.com") res, ok := zFull.Lookup("example.com")
if !ok { if !ok {
t.Fatalf("Expected ok, got false") t.Fatalf("Expected ok, got false")
} }
assertRecordCount(t, records, 8) assertRecordCount(t, res, 8)
assertRecord(t, records[0], "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1") assertRecord(t, res.Answer[0].Record, "SOA", 0, "ns1.example.com. admin.example.com. 1 1 1 1 1")
assertRecord(t, records[1], "A", 0, "192.0.2.1") assertRecord(t, res.Answer[1].Record, "A", 0, "192.0.2.1")
assertRecord(t, records[2], "AAAA", 0, "2001:db8::1") assertRecord(t, res.Answer[2].Record, "AAAA", 0, "2001:db8::1")
assertRecord(t, records[3], "MX", 3600, "10 mail.example.com") // Default TTL assertRecord(t, res.Answer[3].Record, "MX", 3600, "10 mail.example.com") // Default TTL
assertRecord(t, records[4], "TXT", 300, "v=spf1 a mx include:mail.example.com ~all") assertRecord(t, res.Answer[4].Record, "TXT", 300, "v=spf1 a mx include:mail.example.com ~all")
assertRecord(t, records[5], "CAA", 86400, "0 issue \"letsencrypt.org\"") assertRecord(t, res.Answer[5].Record, "CAA", 86400, "0 issue \"letsencrypt.org\"")
assertRecord(t, records[6], "TXT", 3600, "foo=bar") assertRecord(t, res.Answer[6].Record, "TXT", 3600, "foo=bar")
assertRecord(t, records[7], "NS", 0, "ns1.example.com") assertRecord(t, res.Answer[7].Record, "NS", 0, "ns1.example.com")
}) })
t.Run("LookupType example.com TXT", func(t *testing.T) { t.Run("LookupType example.com TXT", func(t *testing.T) {
records, ok := zFull.LookupType("example.com", "TXT") res, ok := zFull.LookupType("example.com", "TXT")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 2) assertRecordCount(t, res, 2)
assertRecord(t, records[0], "TXT", 300, "v=spf1 a mx include:mail.example.com ~all") assertRecord(t, res.Answer[0].Record, "TXT", 300, "v=spf1 a mx include:mail.example.com ~all")
assertRecord(t, records[1], "TXT", 3600, "foo=bar") assertRecord(t, res.Answer[1].Record, "TXT", 3600, "foo=bar")
}) })
t.Run("Lookup www.example.com", func(t *testing.T) { t.Run("Lookup www.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("www.example.com") res, ok := zFull.Lookup("www.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 1) assertRecordCount(t, res, 1)
assertRecord(t, records[0], "CNAME", 3600, "example.com") assertRecord(t, res.Answer[0].Record, "CNAME", 3600, "example.com")
}) })
t.Run("LookupType www.example.com CNAME", func(t *testing.T) { t.Run("LookupType www.example.com CNAME", func(t *testing.T) {
records, ok := zFull.LookupType("www.example.com", "CNAME") res, ok := zFull.LookupType("www.example.com", "CNAME")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 1) assertRecordCount(t, res, 1)
assertRecord(t, records[0], "CNAME", 3600, "example.com") assertRecord(t, res.Answer[0].Record, "CNAME", 3600, "example.com")
}) })
t.Run("Lookup www.example.com TXT", func(t *testing.T) { t.Run("Lookup www.example.com TXT", func(t *testing.T) {
records, ok := zFull.LookupType("www.example.com", "TXT") res, ok := zFull.LookupType("www.example.com", "TXT")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 0) assertRecordCount(t, res, 0)
}) })
t.Run("Lookup status.example.com", func(t *testing.T) { t.Run("Lookup status.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("status.example.com") res, ok := zFull.Lookup("status.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 2) assertRecordCount(t, res, 2)
assertRecord(t, records[0], "A", 3600, "198.51.100.24") assertRecord(t, res.Answer[0].Record, "A", 3600, "198.51.100.24")
assertRecord(t, records[1], "A", 3600, "203.0.113.24") assertRecord(t, res.Answer[1].Record, "A", 3600, "203.0.113.24")
}) })
t.Run("Lookup partner.example.com", func(t *testing.T) { t.Run("Lookup partner.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("partner.example.com") res, ok := zFull.Lookup("partner.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 2) assertRecordCount(t, res, 2)
assertRecord(t, records[0], "NS", 3600, "ns1.example.org") assertRecord(t, res.Answer[0].Record, "NS", 3600, "ns1.example.org")
assertRecord(t, records[1], "NS", 3600, "ns2.example.org") assertRecord(t, res.Answer[1].Record, "NS", 3600, "ns2.example.org")
}) })
t.Run("Lookup unused.example.com", func(t *testing.T) { t.Run("Lookup unused.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("unused.example.com") res, ok := zFull.Lookup("unused.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 0) assertRecordCount(t, res, 0)
}) })
t.Run("Lookup ftp.internal.example.com", func(t *testing.T) { t.Run("Lookup ftp.internal.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("ftp.internal.example.com") res, ok := zFull.Lookup("ftp.internal.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 1) assertRecordCount(t, res, 1)
assertRecord(t, records[0], "A", 3600, "10.0.0.2") assertRecord(t, res.Answer[0].Record, "A", 3600, "10.0.0.2")
}) })
t.Run("Lookup _xmpp-server._tcp.example.com", func(t *testing.T) { t.Run("Lookup _xmpp-server._tcp.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("_xmpp-server._tcp.example.com") res, ok := zFull.Lookup("_xmpp-server._tcp.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 1) assertRecordCount(t, res, 1)
assertRecord(t, records[0], "SRV", 3600, "10 0 5269 example.com") assertRecord(t, res.Answer[0].Record, "SRV", 3600, "10 0 5269 example.com")
}) })
t.Run("Lookup multilayer.nested.folders.example.com", func(t *testing.T) { t.Run("Lookup multilayer.nested.folders.example.com", func(t *testing.T) {
records, ok := zFull.Lookup("multilayer.nested.folders.example.com") res, ok := zFull.Lookup("multilayer.nested.folders.example.com")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, records, 1) assertRecordCount(t, res, 1)
assertRecord(t, records[0], "A", 3600, "192.0.2.1") assertRecord(t, res.Answer[0].Record, "A", 3600, "192.0.2.1")
}) })
} }
@ -238,6 +238,11 @@ func TestBadZones(t *testing.T) {
errorSubstring string errorSubstring string
} }
var badZones = []badZone{ var badZones = []badZone{
{
name: "CnameWithOther",
filename: "testdata/bad_cname_with_other.yaml",
errorSubstring: "extraneous records found next to CNAME",
},
{ {
name: "NonexistentFile", name: "NonexistentFile",
filename: "testdata/bad_nonexistent.yaml", filename: "testdata/bad_nonexistent.yaml",
@ -303,6 +308,26 @@ func TestBadZones(t *testing.T) {
filename: "testdata/bad_missing_ns.yaml", filename: "testdata/bad_missing_ns.yaml",
errorSubstring: "zone apex missing NS records", errorSubstring: "zone apex missing NS records",
}, },
{
name: "NsWithOther",
filename: "testdata/bad_ns_with_other.yaml",
errorSubstring: "non-glue, non-NS records found at delegation point",
},
{
name: "NsWithSubzone",
filename: "testdata/bad_ns_with_subzone.yaml",
errorSubstring: "non-glue records found under delegation point",
},
{
name: "GlueWithOther",
filename: "testdata/bad_glue_with_other.yaml",
errorSubstring: "non-glue record found under delegation point",
},
{
name: "CnameWithOther",
filename: "testdata/bad_cname_with_other.yaml",
errorSubstring: "extraneous records found next to CNAME",
},
} }
for _, badZone := range badZones { for _, badZone := range badZones {