From 450d8a69b578b51b02b4bcda6acf2ba63d645178 Mon Sep 17 00:00:00 2001 From: Frank Schroeder Date: Fri, 4 Aug 2017 13:24:04 +0200 Subject: [PATCH] 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. --- agent/dns.go | 183 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 114 insertions(+), 69 deletions(-) diff --git a/agent/dns.go b/agent/dns.go index 8e458f666..90020741e 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -137,7 +137,7 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { // Only add the SOA if requested if req.Question[0].Qtype == dns.TypeSOA { - d.addSOA(d.domain, m) + d.addSOA(m) } datacenter := d.agent.config.Datacenter @@ -210,13 +210,40 @@ func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) { m.Authoritative = true m.RecursionAvailable = (len(d.recursors) > 0) - // Only add the SOA if requested - if req.Question[0].Qtype == dns.TypeSOA { - d.addSOA(d.domain, m) - } + switch req.Question[0].Qtype { + case dns.TypeSOA: + 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 - d.dispatch(network, req, m) + // add CNAMEs for "ns." to the Extra + // 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 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) addSOA(domain string, msg *dns.Msg) { - soa := &dns.SOA{ +func (d *DNSServer) soa() *dns.SOA { + return &dns.SOA{ Hdr: dns.RR_Header{ - Name: domain, + Name: d.domain, Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: 0, }, - Ns: "ns." + domain, - Mbox: "postmaster." + domain, - Serial: uint32(time.Now().Unix()), + Ns: "ns." + d.domain, + Serial: uint32(time.Now().Unix()), + + // todo(fs): make these configurable + Mbox: "postmaster." + d.domain, Refresh: 3600, Retry: 600, Expire: 86400, 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 @@ -375,8 +467,7 @@ PARSE: return INVALID: d.logger.Printf("[WARN] dns: QName invalid: %s", qName) - d.addSOA(d.domain, resp) - d.addAuthority(resp) + d.addSOA(resp) resp.SetRcode(req, dns.RcodeNameError) } @@ -418,8 +509,7 @@ RPC: // If we have no address, return not found! if out.NodeServices == nil { - d.addSOA(d.domain, resp) - d.addAuthority(resp) + d.addSOA(resp) resp.SetRcode(req, dns.RcodeNameError) return } @@ -433,9 +523,6 @@ RPC: if records != nil { 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 @@ -649,8 +736,7 @@ RPC: // If we have no nodes, return not found! if len(out.Nodes) == 0 { - d.addSOA(d.domain, resp) - d.addAuthority(resp) + d.addSOA(resp) resp.SetRcode(req, dns.RcodeNameError) return } @@ -666,9 +752,6 @@ RPC: 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 network != "tcp" { 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 len(resp.Answer) == 0 && !resp.Truncated { - d.addSOA(d.domain, resp) + d.addSOA(resp) 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. func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, resp *dns.Msg) { // Execute the prepared query. @@ -757,8 +805,7 @@ RPC: // not a full on server error. We have to use a string compare // here since the RPC layer loses the type information. if err.Error() == consul.ErrQueryNotFound.Error() { - d.addSOA(d.domain, resp) - d.addAuthority(resp) + d.addSOA(resp) resp.SetRcode(req, dns.RcodeNameError) return } @@ -800,8 +847,7 @@ RPC: // If we have no nodes, return not found! if len(out.Nodes) == 0 { - d.addSOA(d.domain, resp) - d.addAuthority(resp) + d.addSOA(resp) resp.SetRcode(req, dns.RcodeNameError) return } @@ -826,8 +872,7 @@ RPC: // If the answer is empty and the response isn't truncated, return not found if len(resp.Answer) == 0 && !resp.Truncated { - d.addAuthority(resp) - d.addSOA(d.domain, resp) + d.addSOA(resp) return } }