From 59d1f20b441e559507d51b10ddf5712f4885bcfd Mon Sep 17 00:00:00 2001
From: ale <ale@incal.net>
Date: Tue, 24 Nov 2015 09:34:31 +0000
Subject: [PATCH] serve authoritative NS records for the zone

---
 cmd/redirectord/redirectord.go | 35 +++++++++++++++++++++++++++++++-
 fe/dns.go                      | 37 +++++++++++++++++++++++++++++++++-
 fe/dns_test.go                 |  2 +-
 3 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go
index e8659053..40328916 100644
--- a/cmd/redirectord/redirectord.go
+++ b/cmd/redirectord/redirectord.go
@@ -1,10 +1,13 @@
 package main
 
 import (
+	"errors"
 	"flag"
 	"fmt"
 	"log"
+	"strings"
 
+	"net"
 	_ "net/http/pprof"
 
 	"git.autistici.org/ale/autoradio"
@@ -21,11 +24,25 @@ var (
 	staticDir   = flag.String("static-dir", "/usr/share/autoradio/htdocs/static", "Static content directory")
 	templateDir = flag.String("template-dir", "/usr/share/autoradio/htdocs/templates", "HTML templates directory")
 	lbPolicy    = flag.String("lb-policy", "listeners_available,listeners_score,weighted", "Load balancing rules specification (see godoc documentation for details)")
+	nameservers = flag.String("nameservers", "", "Comma-separated list of name servers (not IPs) for the zone specified in --domain")
 
 	// Default DNS TTL (seconds).
 	dnsTtl = 5
 )
 
+func getFQDN(ips []net.IP) (string, error) {
+	for _, ip := range ips {
+		if names, err := net.LookupAddr(ip.String()); err == nil && len(names) > 0 {
+			// This is a pretty weak criteria for qualification.
+			name := strings.TrimSuffix(names[0], ".")
+			if strings.Contains(name, ".") {
+				return names[0], nil
+			}
+		}
+	}
+	return "", errors.New("reverse resolution failed")
+}
+
 func main() {
 	log.SetFlags(0)
 	flag.Parse()
@@ -42,7 +59,23 @@ func main() {
 
 	client := autoradio.NewClient(autoradio.NewEtcdClient(false))
 
-	dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl)
+	// If no nameservers are specified, use the fqdn of the local
+	// host. It is not going to provide a lot of reliability for
+	// clients that cache the authoritative NS records for long,
+	// but at least it will work.
+	var ns []string
+	if *nameservers != "" {
+		ns = strings.Split(*nameservers, ",")
+	} else {
+		fqdn, err := getFQDN(*publicIPs)
+		if err != nil {
+			log.Fatal("Could not determine fully-qualified name of local host, and --nameservers is not specified")
+		}
+		log.Printf("autodetected fqdn %s", fqdn)
+		ns = []string{fqdn}
+	}
+
+	dnsRed := fe.NewDNSRedirector(client, *domain, *publicIPs, dnsTtl, ns)
 	dnsRed.Start(fmt.Sprintf(":%d", *dnsPort))
 
 	red, err := fe.NewHTTPRedirector(client, *domain, *lbPolicy, *staticDir, *templateDir)
diff --git a/fe/dns.go b/fe/dns.go
index afa681b9..41770151 100644
--- a/fe/dns.go
+++ b/fe/dns.go
@@ -81,7 +81,26 @@ func parseEtcdClusterState(urls []string) *etcdClusterState {
 	return &state
 }
 
+func addDotToList(l []string) []string {
+	var out []string
+	for _, s := range l {
+		if !strings.HasSuffix(s, ".") {
+			s += "."
+		}
+		out = append(out, s)
+	}
+	return out
+}
+
 // DNSRedirector sends clients to backends using DNS.
+//
+// The DNSRedirector needs some basic information in order to generate
+// the required structural records for the zone (such as NS records).
+// Right now the list of nameservers has to be specified manually, but
+// it would be nice if we were able to autodetect them, perhaps by
+// using the list of nodes (which would require a way to translate
+// short name -> fqdn for each node, which is difficult without
+// additional requirements on the setup).
 type DNSRedirector struct {
 	client         *autoradio.Client
 	origin         string
@@ -90,12 +109,13 @@ type DNSRedirector struct {
 	etcdCluster    *etcdClusterState
 	ttl            int
 	soa            dns.RR
+	nameservers    []string
 	queryTable     map[string]ipFunc
 }
 
 // NewDNSRedirector returns a DNS server for the given origin and
 // publicIp. The A records served will have the specified ttl.
-func NewDNSRedirector(client *autoradio.Client, origin string, publicIps []net.IP, ttl int) *DNSRedirector {
+func NewDNSRedirector(client *autoradio.Client, origin string, publicIps []net.IP, ttl int, nameservers []string) *DNSRedirector {
 	if !strings.HasSuffix(origin, ".") {
 		origin += "."
 	}
@@ -123,6 +143,7 @@ func NewDNSRedirector(client *autoradio.Client, origin string, publicIps []net.I
 		originNumParts: len(dns.SplitDomainName(origin)),
 		publicIps:      publicIps,
 		ttl:            ttl,
+		nameservers:    addDotToList(nameservers),
 		soa:            soa,
 		queryTable: map[string]ipFunc{
 			"":       getAutoradioIPs,
@@ -259,6 +280,20 @@ func (d *DNSRedirector) handleQuestion(q dns.Question, m *dns.Msg) bool {
 		m.Answer = append(m.Answer, d.soa)
 		return true
 
+	case query == "" && q.Qtype == dns.TypeNS:
+		for _, ns := range d.nameservers {
+			m.Answer = append(m.Answer, &dns.NS{
+				Hdr: dns.RR_Header{
+					Name:   d.withOrigin(query),
+					Rrtype: dns.TypeNS,
+					Class:  dns.ClassINET,
+					Ttl:    3600,
+				},
+				Ns: ns,
+			})
+		}
+		return true
+
 	case q.Qtype == dns.TypeSRV:
 		if d.etcdCluster == nil {
 			return false
diff --git a/fe/dns_test.go b/fe/dns_test.go
index 44ca024d..3fa1949a 100644
--- a/fe/dns_test.go
+++ b/fe/dns_test.go
@@ -32,7 +32,7 @@ func createTestDNSRedirector(t testing.TB, withNode bool) *DNSRedirector {
 
 	client := autoradio.NewClient(etcd)
 	client.WaitForNodes()
-	d := NewDNSRedirector(client, "example.com", []net.IP{net.ParseIP("2.3.4.5")}, 30)
+	d := NewDNSRedirector(client, "example.com", []net.IP{net.ParseIP("2.3.4.5")}, 30, []string{"ns1", "ns2"})
 	d.updateEtcdCluster()
 	return d
 }
-- 
GitLab