diff --git a/cmd/redirectord/redirectord.go b/cmd/redirectord/redirectord.go index e86590532335b6b1e959b2c693ae9d7e483586d1..40328916a15178f95f473d38249f7268d789ff77 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 afa681b9a91febc91b3c72086034cdfe62f09a22..417701514fb939a7e29e10c4bc1f87b6ac4fd159 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 44ca024d5041a9b1d6ab72f60234c25690707cbb..3fa1949a6cf685531a898d963674a121d9d6b060 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 }