Improve conformance with DNS behavior

This commit is contained in:
Dessa Simpson 2025-12-02 00:57:49 -07:00
parent b79df4bf1d
commit 07da2a1269
6 changed files with 96 additions and 50 deletions

27
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,27 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CoreDNS",
"type": "go",
"request": "launch",
"mode": "exec",
"program": "${workspaceFolder}/../coredns/coredns",
"args": [],
"cwd": "${workspaceFolder}/testdata",
"env": {
"GOWORK": "${workspaceFolder}/../coredns/go.work"
},
"preLaunchTask": "Build CoreDNS",
"showLog": true
},
{
"name": "Attach to CoreDNS",
"type": "go",
"request": "attach",
"mode": "remote",
"port": 2345,
"host": "127.0.0.1"
}
]
}

21
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,21 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build CoreDNS",
"type": "shell",
"command": "go generate && go build -gcflags='all=-N -l' -o coredns",
"options": {
"cwd": "${workspaceFolder}/../coredns",
"env": {
"GOWORK": "${workspaceFolder}/../coredns/go.work"
}
},
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$go"]
}
]
}

5
testdata/Corefile vendored Normal file
View file

@ -0,0 +1,5 @@
example.com example.org {
debug
bind 127.0.0.36
yaml zones.yaml
}

View file

@ -78,11 +78,10 @@ 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) { func (y YamlPlugin) lookupRRs(qname string, qtype string, reply *dns.Msg) (int, error) {
var rcode int
res, ok := y.Zone.LookupType(qname, qtype) res, ok := y.Zone.LookupType(qname, qtype)
if !ok { if !ok {
// NXDOMAIN // NXDOMAIN
rcode = dns.RcodeNameError reply.Rcode = dns.RcodeNameError
} }
if res.IsReferral { if res.IsReferral {
reply.Authoritative = false reply.Authoritative = false
@ -101,12 +100,14 @@ func (y YamlPlugin) lookupRRs(qname string, qtype string, reply *dns.Msg) (int,
if ttl == 0 { if ttl == 0 {
ttl = y.Config.DefaultTtl ttl = y.Config.DefaultTtl
} }
rr, err := dns.NewRR(fmt.Sprintf("%s %d %s %s", record.Name, ttl, record.Record.Type, record.Record.Value)) rrString := fmt.Sprintf("%s %d %s %s", record.Name, ttl, record.Record.Type, record.Record.Value)
rr, err := dns.NewRR(rrString)
if err != nil { 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) log.Errorf("Failed to parse RR string: %q", rrString)
return reply.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) *section.dest = append(*section.dest, rr)
} }
} }
return rcode, nil return reply.Rcode, nil
} }

View file

@ -76,7 +76,7 @@ func LoadZoneBytes(data []byte, filename string) (*Zone, error) {
return z, nil return z, nil
} }
func (z *Zone) Validate(name string, soa *Record, ns *[]Record) error { func (z *Zone) Validate(name string, soa *NamedRecord, ns *[]Record) error {
nameservers, otherRecords := []Record{}, []Record{} nameservers, otherRecords := []Record{}, []Record{}
nameserversMap := map[string]bool{} nameserversMap := map[string]bool{}
isZoneApex := false isZoneApex := false
@ -85,8 +85,7 @@ func (z *Zone) Validate(name string, soa *Record, ns *[]Record) error {
switch record.Type { switch record.Type {
case "SOA": case "SOA":
isZoneApex = true isZoneApex = true
soa = &record soa = &NamedRecord{Name: name, Record: record}
z.SOA = NamedRecord{Name: name, Record: record}
case "NS": case "NS":
nameservers = append(nameservers, record) nameservers = append(nameservers, record)
nameserversMap[record.Value] = true nameserversMap[record.Value] = true
@ -97,12 +96,12 @@ func (z *Zone) Validate(name string, soa *Record, ns *[]Record) error {
} }
} }
z.IsDelegationPoint = !isZoneApex && len(nameservers) > 0
if cnameCount > 1 || (cnameCount > 0 && len(otherRecords) > 0) { if cnameCount > 1 || (cnameCount > 0 && len(otherRecords) > 0) {
return fmt.Errorf("%s: extraneous records found next to CNAME", name) return fmt.Errorf("%s: extraneous records found next to CNAME", name)
} }
z.IsDelegationPoint = !isZoneApex && len(nameservers) > 0
if soa == nil { if soa == nil {
// Outside zone // Outside zone
if z.IsDelegationPoint { if z.IsDelegationPoint {
@ -115,9 +114,10 @@ func (z *Zone) Validate(name string, soa *Record, ns *[]Record) 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)
} }
z.SOA = *soa
ns = &nameservers 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)
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)
} }
@ -129,9 +129,14 @@ func (z *Zone) Validate(name string, soa *Record, ns *[]Record) error {
} }
} }
return nil return nil
} else {
// Normal node
// Cache SOA at each node in the zone tree
z.SOA = *soa
} }
// 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), soa, ns); err != nil { if err := subzone.Validate(concatName(name, subname), soa, ns); err != nil {
return err return err
@ -275,6 +280,11 @@ func nameToPath(name string) []string {
} }
func (z *Zone) Lookup(name string) (LookupResult, bool) { func (z *Zone) Lookup(name string) (LookupResult, bool) {
res, ok := z.LookupType(name, "")
return res, ok
}
func (z *Zone) LookupType(name string, recordType string) (LookupResult, bool) {
res := LookupResult{} res := LookupResult{}
path := nameToPath(name) path := nameToPath(name)
for _, label := range path { for _, label := range path {
@ -282,18 +292,6 @@ func (z *Zone) Lookup(name string) (LookupResult, bool) {
if label == "" { if label == "" {
continue continue
} }
if z.IsDelegationPoint {
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 { if sz, ok := z.Subzones[label]; !ok {
// NXDOMAIN // NXDOMAIN
res.Ns = []NamedRecord{z.SOA} res.Ns = []NamedRecord{z.SOA}
@ -301,11 +299,28 @@ func (z *Zone) Lookup(name string) (LookupResult, bool) {
} else { } else {
z = sz z = sz
} }
if z.IsDelegationPoint {
// Retrieve glue records from cache, whether referral or NS query
res.Extra = append(res.Extra, z.GlueRecords...)
if recordType != "NS" {
// Treat as referral unless client requested NS records
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})
}
}
return res, true
}
}
} }
res.Answer = []NamedRecord{} res.Answer = []NamedRecord{}
for _, record := range z.Records { for _, record := range z.Records {
if recordType == "" || record.Type == recordType {
res.Answer = append(res.Answer, NamedRecord{Name: name, Record: record}) res.Answer = append(res.Answer, NamedRecord{Name: name, Record: record})
} }
}
if len(res.Answer) == 0 { if len(res.Answer) == 0 {
// NODATA // NODATA
res.Ns = []NamedRecord{z.SOA} res.Ns = []NamedRecord{z.SOA}
@ -314,29 +329,6 @@ func (z *Zone) Lookup(name string) (LookupResult, bool) {
return res, true return res, true
} }
func (z *Zone) FilterRecords(res LookupResult, recordType string) LookupResult {
filtered := []NamedRecord{}
for _, nr := range res.Answer {
if nr.Record.Type == recordType {
filtered = append(filtered, nr)
}
}
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) (LookupResult, bool) {
res, ok := z.Lookup(name)
if !ok {
return LookupResult{}, false
}
return z.FilterRecords(res, recordType), true
}
func concatName(name string, subname string) string { func concatName(name string, subname string) string {
if name == "." { if name == "." {
return subname + "." return subname + "."

View file

@ -195,8 +195,8 @@ func TestFullZone(t *testing.T) {
assertRecord(t, res.Answer[1].Record, "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 NS", func(t *testing.T) {
res, ok := zFull.Lookup("partner.example.com") res, ok := zFull.LookupType("partner.example.com", "NS")
assertOk(t, ok) assertOk(t, ok)
assertRecordCount(t, res, 2) assertRecordCount(t, res, 2)
assertRecord(t, res.Answer[0].Record, "NS", 3600, "ns1.example.org") assertRecord(t, res.Answer[0].Record, "NS", 3600, "ns1.example.org")