initial commit
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user