Files
vibedns/internal/authoritative/answer.go
T
2026-08-16 21:18:45 -05:00

323 lines
8.2 KiB
Go

package authoritative
import (
"strings"
"github.com/miekg/dns"
)
// maxCNAMEChain bounds in-zone CNAME following so a loop cannot hang a query.
const maxCNAMEChain = 12
// Answer builds an authoritative reply for the question in req.
//
// It returns nil when no configured zone covers the question, which tells the
// caller to fall through to the cache and the recursive resolver.
func (idx *Index) Answer(req *dns.Msg, do bool) *dns.Msg {
if idx == nil || len(req.Question) == 0 {
return nil
}
q := req.Question[0]
if q.Qclass != dns.ClassINET && q.Qclass != dns.ClassANY {
return nil
}
z := idx.Lookup(q.Name)
if z == nil {
return nil
}
return z.Answer(req, do)
}
// Answer builds an authoritative reply for req from this zone.
func (z *Zone) Answer(req *dns.Msg, do bool) *dns.Msg {
q := req.Question[0]
qname := strings.ToLower(dns.Fqdn(q.Name))
qtype := q.Qtype
m := new(dns.Msg)
m.SetReply(req)
m.Authoritative = true
m.Compress = true
// A delegation between the apex and the queried name means the answer
// belongs to a child zone: return a referral rather than our own data.
if z.hasDelegations {
if dp := z.delegationFor(qname, qtype); dp != "" {
z.writeReferral(m, dp, do)
return m
}
}
name := qname
for depth := 0; depth < maxCNAMEChain; depth++ {
node, synthesised := z.resolveNode(name)
if node == nil {
// The name has no data. Distinguish "exists but no records of this
// type" (NODATA) from "does not exist at all" (NXDOMAIN).
if _, exists := z.ents[name]; exists || depth > 0 {
z.writeNoData(m, do)
} else {
m.Rcode = dns.RcodeNameError
z.writeNoData(m, do)
}
return m
}
// A CNAME is followed unless the client asked for the CNAME itself.
if cnames := node.types[dns.TypeCNAME]; len(cnames) > 0 && qtype != dns.TypeCNAME && qtype != dns.TypeANY {
rr := materialise(cnames[0], name, synthesised)
m.Answer = append(m.Answer, rr)
z.appendSignatures(m, node, dns.TypeCNAME, name, synthesised, do)
target := strings.ToLower(rr.(*dns.CNAME).Target)
if !dns.IsSubDomain(z.Name, target) {
// The chain leaves our zone; the client (or the recursor in
// front of it) has to continue from here.
m.Authoritative = true
return m
}
name = target
continue
}
if qtype == dns.TypeANY {
for _, t := range node.typeList() {
for _, rr := range node.types[t] {
m.Answer = append(m.Answer, materialise(rr, name, synthesised))
}
}
if len(m.Answer) == 0 {
z.writeNoData(m, do)
} else {
z.addAuthorityNS(m, do)
}
return m
}
rrs := node.types[qtype]
if len(rrs) == 0 {
z.writeNoData(m, do)
return m
}
for _, rr := range rrs {
m.Answer = append(m.Answer, materialise(rr, name, synthesised))
}
z.appendSignatures(m, node, qtype, name, synthesised, do)
z.addAuthorityNS(m, do)
z.addAdditional(m, do)
return m
}
// Chain too long: return what we have rather than looping.
return m
}
// resolveNode finds the RRsets for a name, falling back to wildcard synthesis
// using the RFC 4592 closest-encloser rule.
func (z *Zone) resolveNode(name string) (node *nameNode, synthesised bool) {
if n, ok := z.names[name]; ok {
return n, false
}
if _, exists := z.ents[name]; exists {
return nil, false // empty non-terminal: exists, but holds no data
}
if len(z.wildcards) == 0 {
return nil, false
}
ce := z.closestEncloser(name)
if wn, ok := z.wildcards["*."+ce]; ok {
return wn, true
}
return nil, false
}
// closestEncloser returns the deepest ancestor of name that exists in the zone.
func (z *Zone) closestEncloser(name string) string {
n := name
for {
if n == z.Name {
return z.Name
}
i, end := dns.NextLabel(n, 0)
if end {
return z.Name
}
n = n[i:]
if !dns.IsSubDomain(z.Name, n) {
return z.Name
}
if _, ok := z.ents[n]; ok {
return n
}
}
}
// delegationFor returns the deepest delegation point at or above qname, or "".
// A DS query at the delegation point itself is answered from the parent side,
// so it is not treated as a referral.
func (z *Zone) delegationFor(qname string, qtype uint16) string {
n := qname
for {
if n == z.Name || !dns.IsSubDomain(z.Name, n) {
return ""
}
if _, ok := z.delegations[n]; ok {
if n == qname && qtype == dns.TypeDS {
return ""
}
return n
}
i, end := dns.NextLabel(n, 0)
if end {
return ""
}
n = n[i:]
}
}
// writeReferral fills the authority section with the child zone's NS records
// and the additional section with any in-zone glue.
func (z *Zone) writeReferral(m *dns.Msg, delegation string, do bool) {
m.Authoritative = false
node := z.delegations[delegation]
if node == nil {
return
}
for _, rr := range node.types[dns.TypeNS] {
m.Ns = append(m.Ns, dns.Copy(rr))
}
// A signed delegation carries a DS RRset (or a proof of its absence).
if dsNode, ok := z.names[delegation]; ok && do {
for _, rr := range dsNode.types[dns.TypeDS] {
m.Ns = append(m.Ns, dns.Copy(rr))
}
for _, rr := range dsNode.types[dns.TypeRRSIG] {
if sig, ok := rr.(*dns.RRSIG); ok && sig.TypeCovered == dns.TypeDS {
m.Ns = append(m.Ns, dns.Copy(rr))
}
}
}
z.addGlueFor(m, m.Ns)
}
// writeNoData puts the SOA in the authority section, which is what tells a
// resolver how long to cache the negative answer.
func (z *Zone) writeNoData(m *dns.Msg, do bool) {
if z.soa == nil {
return
}
soa := dns.Copy(z.soa).(*dns.SOA)
// RFC 2308: the negative caching TTL is the lesser of the SOA TTL and the
// SOA MINIMUM field.
if soa.Minttl < soa.Hdr.Ttl {
soa.Hdr.Ttl = soa.Minttl
}
m.Ns = append(m.Ns, soa)
if do {
if apex, ok := z.names[z.Name]; ok {
for _, rr := range apex.types[dns.TypeRRSIG] {
if sig, ok := rr.(*dns.RRSIG); ok && sig.TypeCovered == dns.TypeSOA {
m.Ns = append(m.Ns, dns.Copy(rr))
}
}
}
}
}
// addAuthorityNS adds the zone's NS RRset to a positive answer, except when the
// answer already is that RRset.
func (z *Zone) addAuthorityNS(m *dns.Msg, do bool) {
if len(m.Answer) == 0 || len(z.ns) == 0 {
return
}
if h := m.Answer[0].Header(); h.Rrtype == dns.TypeNS && h.Name == z.Name {
return
}
if h := m.Answer[0].Header(); h.Rrtype == dns.TypeSOA {
return
}
for _, rr := range z.ns {
m.Ns = append(m.Ns, dns.Copy(rr))
}
if do {
if apex, ok := z.names[z.Name]; ok {
for _, rr := range apex.types[dns.TypeRRSIG] {
if sig, ok := rr.(*dns.RRSIG); ok && sig.TypeCovered == dns.TypeNS {
m.Ns = append(m.Ns, dns.Copy(rr))
}
}
}
}
}
// addAdditional supplies address records for names referenced by the answer,
// saving the client a follow-up query.
func (z *Zone) addAdditional(m *dns.Msg, do bool) {
z.addGlueFor(m, m.Answer)
z.addGlueFor(m, m.Ns)
}
func (z *Zone) addGlueFor(m *dns.Msg, section []dns.RR) {
seen := map[string]bool{}
for _, rr := range m.Extra {
seen[strings.ToLower(rr.Header().Name)] = true
}
for _, rr := range section {
var target string
switch v := rr.(type) {
case *dns.MX:
target = v.Mx
case *dns.SRV:
target = v.Target
case *dns.NS:
target = v.Ns
default:
continue
}
target = strings.ToLower(dns.Fqdn(target))
if target == "" || seen[target] || !dns.IsSubDomain(z.Name, target) {
continue
}
node, ok := z.names[target]
if !ok {
continue
}
seen[target] = true
for _, t := range []uint16{dns.TypeA, dns.TypeAAAA} {
for _, arr := range node.types[t] {
m.Extra = append(m.Extra, dns.Copy(arr))
}
}
}
}
// appendSignatures adds the RRSIGs covering an RRset when the client set DO.
//
// Zones served here are not signed by this application; signatures are only
// present when a pre-signed zone file was imported. Serving them unchanged
// keeps such zones verifiable, and leaves room for an in-process signer later.
func (z *Zone) appendSignatures(m *dns.Msg, node *nameNode, covered uint16, name string, synthesised, do bool) {
if !do {
return
}
for _, rr := range node.types[dns.TypeRRSIG] {
sig, ok := rr.(*dns.RRSIG)
if !ok || sig.TypeCovered != covered {
continue
}
m.Answer = append(m.Answer, materialise(rr, name, synthesised))
}
}
// materialise copies an RR, rewriting the owner name when the record came from
// a wildcard node.
func materialise(rr dns.RR, owner string, synthesised bool) dns.RR {
c := dns.Copy(rr)
if synthesised {
c.Header().Name = owner
}
return c
}