dns: provide correct SOA and NS responses

This patch changes the behavior of the DNS server as follows:

* The SOA response contains the SOA record in the Answer section instead
  of the Authority section. It also contains NS records in the Authority
  and the corresponding A glue records in the Extra section.
  In addition, CNAMEs are added to the Extra section to make the
  MNAME of the SOA record resolvable.

  AAAA glue records are not yet supported.

* The NS response returns up to three random servers from the
  consul cluster in the Answer section and the glue A
  records in the Extra section.

  AAAA glue records are not yet supported.
This commit is contained in:
Frank Schroeder 2017-08-04 13:24:04 +02:00
parent bff45ee1da
commit 450d8a69b5
No known key found for this signature in database
GPG Key ID: 4D65C6EAEC87DECD
1 changed files with 114 additions and 69 deletions

View File

@ -137,7 +137,7 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) {
// Only add the SOA if requested // Only add the SOA if requested
if req.Question[0].Qtype == dns.TypeSOA { if req.Question[0].Qtype == dns.TypeSOA {
d.addSOA(d.domain, m) d.addSOA(m)
} }
datacenter := d.agent.config.Datacenter datacenter := d.agent.config.Datacenter
@ -210,13 +210,40 @@ func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) {
m.Authoritative = true m.Authoritative = true
m.RecursionAvailable = (len(d.recursors) > 0) m.RecursionAvailable = (len(d.recursors) > 0)
// Only add the SOA if requested switch req.Question[0].Qtype {
if req.Question[0].Qtype == dns.TypeSOA { case dns.TypeSOA:
d.addSOA(d.domain, m) ns, glue := d.nameservers()
} m.Answer = append(m.Answer, d.soa())
m.Ns = append(m.Ns, ns...)
m.Extra = append(m.Extra, glue...)
// Dispatch the correct handler // add CNAMEs for "ns.<domain>" to the Extra
d.dispatch(network, req, m) // section to make the MNAME entry in the SOA
// record resolvable
for _, rr := range ns {
cname := &dns.CNAME{
Hdr: dns.RR_Header{
Name: "ns." + d.domain,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: uint32(d.config.NodeTTL / time.Second),
},
Target: rr.(*dns.NS).Ns,
}
m.Extra = append(m.Extra, cname)
}
m.SetRcode(req, dns.RcodeSuccess)
case dns.TypeNS:
ns, glue := d.nameservers()
m.Answer = ns
m.Extra = glue
m.SetRcode(req, dns.RcodeSuccess)
default:
d.dispatch(network, req, m)
}
// Handle EDNS // Handle EDNS
if edns := req.IsEdns0(); edns != nil { if edns := req.IsEdns0(); edns != nil {
@ -229,24 +256,89 @@ func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) {
} }
} }
// addSOA is used to add an SOA record to a message for the given domain func (d *DNSServer) soa() *dns.SOA {
func (d *DNSServer) addSOA(domain string, msg *dns.Msg) { return &dns.SOA{
soa := &dns.SOA{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
Name: domain, Name: d.domain,
Rrtype: dns.TypeSOA, Rrtype: dns.TypeSOA,
Class: dns.ClassINET, Class: dns.ClassINET,
Ttl: 0, Ttl: 0,
}, },
Ns: "ns." + domain, Ns: "ns." + d.domain,
Mbox: "postmaster." + domain, Serial: uint32(time.Now().Unix()),
Serial: uint32(time.Now().Unix()),
// todo(fs): make these configurable
Mbox: "postmaster." + d.domain,
Refresh: 3600, Refresh: 3600,
Retry: 600, Retry: 600,
Expire: 86400, Expire: 86400,
Minttl: 0, Minttl: 0,
} }
msg.Ns = append(msg.Ns, soa) }
// addSOA is used to add an SOA record to a message for the given domain
func (d *DNSServer) addSOA(msg *dns.Msg) {
msg.Ns = append(msg.Ns, d.soa())
}
// nameservers returns the names and ip addresses of up to three random servers
// in the current cluster which serve as authoritative name servers for zone.
func (d *DNSServer) nameservers() (ns []dns.RR, extra []dns.RR) {
// get server names and store them in a map to randomize the output
servers := map[string]net.IP{}
for name, addr := range d.agent.delegate.ServerAddrs() {
ip := net.ParseIP(strings.Split(addr, ":")[0])
if ip == nil {
continue
}
// name is "name.dc" and domain is "consul."
// we want "name.node.dc.consul."
lastdot := strings.LastIndexByte(name, '.')
fqdn := name[:lastdot] + ".node" + name[lastdot:] + "." + d.domain
// create a consistent, unique and sanitized name for the server
fqdn = dns.Fqdn(strings.ToLower(fqdn))
servers[fqdn] = ip
}
if len(servers) == 0 {
return
}
for name, ip := range servers {
// the name server record
nsrr := &dns.NS{
Hdr: dns.RR_Header{
Name: d.domain,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 0,
},
Ns: name,
}
ns = append(ns, nsrr)
// the glue record providing the ip address
a := &dns.A{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(d.config.NodeTTL / time.Second),
},
A: ip,
}
extra = append(extra, a)
// don't provide more than 3 servers
if len(ns) >= 3 {
return
}
}
return
} }
// dispatch is used to parse a request and invoke the correct handler // dispatch is used to parse a request and invoke the correct handler
@ -375,8 +467,7 @@ PARSE:
return return
INVALID: INVALID:
d.logger.Printf("[WARN] dns: QName invalid: %s", qName) d.logger.Printf("[WARN] dns: QName invalid: %s", qName)
d.addSOA(d.domain, resp) d.addSOA(resp)
d.addAuthority(resp)
resp.SetRcode(req, dns.RcodeNameError) resp.SetRcode(req, dns.RcodeNameError)
} }
@ -418,8 +509,7 @@ RPC:
// If we have no address, return not found! // If we have no address, return not found!
if out.NodeServices == nil { if out.NodeServices == nil {
d.addSOA(d.domain, resp) d.addSOA(resp)
d.addAuthority(resp)
resp.SetRcode(req, dns.RcodeNameError) resp.SetRcode(req, dns.RcodeNameError)
return return
} }
@ -433,9 +523,6 @@ RPC:
if records != nil { if records != nil {
resp.Answer = append(resp.Answer, records...) resp.Answer = append(resp.Answer, records...)
} }
// Add NS record and A record
d.addAuthority(resp)
} }
// formatNodeRecord takes a Node and returns an A, AAAA, or CNAME record // formatNodeRecord takes a Node and returns an A, AAAA, or CNAME record
@ -649,8 +736,7 @@ RPC:
// If we have no nodes, return not found! // If we have no nodes, return not found!
if len(out.Nodes) == 0 { if len(out.Nodes) == 0 {
d.addSOA(d.domain, resp) d.addSOA(resp)
d.addAuthority(resp)
resp.SetRcode(req, dns.RcodeNameError) resp.SetRcode(req, dns.RcodeNameError)
return return
} }
@ -666,9 +752,6 @@ RPC:
d.serviceNodeRecords(datacenter, out.Nodes, req, resp, ttl) d.serviceNodeRecords(datacenter, out.Nodes, req, resp, ttl)
} }
// Add NS and A records
d.addAuthority(resp)
// If the network is not TCP, restrict the number of responses // If the network is not TCP, restrict the number of responses
if network != "tcp" { if network != "tcp" {
wasTrimmed := trimUDPResponse(d.config, req, resp) wasTrimmed := trimUDPResponse(d.config, req, resp)
@ -681,46 +764,11 @@ RPC:
// If the answer is empty and the response isn't truncated, return not found // If the answer is empty and the response isn't truncated, return not found
if len(resp.Answer) == 0 && !resp.Truncated { if len(resp.Answer) == 0 && !resp.Truncated {
d.addSOA(d.domain, resp) d.addSOA(resp)
return return
} }
} }
// addAuthority adds NS records and corresponding A records with the IP addresses of servers
func (d *DNSServer) addAuthority(msg *dns.Msg) {
serverAddrs := d.agent.delegate.ServerAddrs()
for name, addr := range serverAddrs {
ipAddrStr := strings.Split(addr, ":")[0]
sanitizedName := InvalidDnsRe.ReplaceAllString(name, "-") // does some basic sanitization of the name
nsName := "server-" + sanitizedName + "." + d.domain
ip := net.ParseIP(ipAddrStr)
if ip != nil {
ns := &dns.NS{
Hdr: dns.RR_Header{
Name: d.domain,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 0,
},
Ns: nsName,
}
msg.Ns = append(msg.Ns, ns)
// add an A record for the NS record
a := &dns.A{
Hdr: dns.RR_Header{
Name: nsName,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(d.config.NodeTTL / time.Second),
},
A: ip,
}
msg.Extra = append(msg.Extra, a)
}
}
}
// preparedQueryLookup is used to handle a prepared query. // preparedQueryLookup is used to handle a prepared query.
func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, resp *dns.Msg) { func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, resp *dns.Msg) {
// Execute the prepared query. // Execute the prepared query.
@ -757,8 +805,7 @@ RPC:
// not a full on server error. We have to use a string compare // not a full on server error. We have to use a string compare
// here since the RPC layer loses the type information. // here since the RPC layer loses the type information.
if err.Error() == consul.ErrQueryNotFound.Error() { if err.Error() == consul.ErrQueryNotFound.Error() {
d.addSOA(d.domain, resp) d.addSOA(resp)
d.addAuthority(resp)
resp.SetRcode(req, dns.RcodeNameError) resp.SetRcode(req, dns.RcodeNameError)
return return
} }
@ -800,8 +847,7 @@ RPC:
// If we have no nodes, return not found! // If we have no nodes, return not found!
if len(out.Nodes) == 0 { if len(out.Nodes) == 0 {
d.addSOA(d.domain, resp) d.addSOA(resp)
d.addAuthority(resp)
resp.SetRcode(req, dns.RcodeNameError) resp.SetRcode(req, dns.RcodeNameError)
return return
} }
@ -826,8 +872,7 @@ RPC:
// If the answer is empty and the response isn't truncated, return not found // If the answer is empty and the response isn't truncated, return not found
if len(resp.Answer) == 0 && !resp.Truncated { if len(resp.Answer) == 0 && !resp.Truncated {
d.addAuthority(resp) d.addSOA(resp)
d.addSOA(d.domain, resp)
return return
} }
} }