323 lines
8.2 KiB
Go
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
|
|
}
|