diff --git a/dnstest/message.go b/dnstest/message.go index 8268f9f6..b3885e41 100644 --- a/dnstest/message.go +++ b/dnstest/message.go @@ -70,6 +70,14 @@ func A(hdr dns.RR_Header, ip net.IP) *dns.A { } } +// AAAA returns an AAAA record set with the given arguments. +func AAAA(hdr dns.RR_Header, ip net.IP) *dns.AAAA { + return &dns.AAAA{ + Hdr: hdr, + AAAA: ip.To16(), + } +} + // SRV returns a SRV record set with the given arguments. func SRV(hdr dns.RR_Header, target string, port, priority, weight uint16) *dns.SRV { return &dns.SRV{ diff --git a/docs/docs/configuration-parameters.md b/docs/docs/configuration-parameters.md index 188ce368..a9a1db20 100644 --- a/docs/docs/configuration-parameters.md +++ b/docs/docs/configuration-parameters.md @@ -79,7 +79,7 @@ It is sufficient to specify just one of the `zk` or `masters` field. If both are `timeout` is the timeout threshold, in seconds, for connections and requests to external DNS requests. The default value is 5 seconds. -`listener` is the IP address of Mesos-DNS. In SOA replies, Mesos-DNS identifies hostname `mesos-dns.domain` as the primary nameserver for the domain. It uses this IP address in an A record for `mesos-dns.domain`. The default value is "0.0.0.0", which instructs Mesos-DNS to create an A record for every IP address associated with a network interface on the server that runs the Mesos-DNS process. +`listener` is the IP address of Mesos-DNS. In SOA replies, Mesos-DNS identifies hostname `mesos-dns.domain` as the primary nameserver for the domain. It uses this IP address in an A or AAAA record for `mesos-dns.domain`. The default value is "0.0.0.0", which instructs Mesos-DNS to create an A record for every IP address associated with a network interface on the server that runs the Mesos-DNS process. `dnson` is a boolean field that controls whether Mesos-DNS listens for DNS requests or not. The default value is `true`. diff --git a/docs/docs/http.md b/docs/docs/http.md index 30b79595..5866176c 100644 --- a/docs/docs/http.md +++ b/docs/docs/http.md @@ -49,7 +49,7 @@ curl http://10.190.238.173:8123/v1/config ``` ## `GET /v1/hosts/{host}` -Lists in JSON format the IP address(es) that correspond to a hostname. It is the equivalent of DNS A record lookup. Note, the HTTP interface only translates hostnames in the Mesos domain. +Lists in JSON format the IP address(es) that correspond to a hostname. It is the equivalent of DNS A and AAAA record lookup. Note, the HTTP interface only translates hostnames in the Mesos domain. ```console $ curl http://10.190.238.173:8123/v1/hosts/nginx.marathon.mesos diff --git a/docs/docs/naming.md b/docs/docs/naming.md index 1a7b0b91..785120c0 100644 --- a/docs/docs/naming.md +++ b/docs/docs/naming.md @@ -4,12 +4,12 @@ title: Service Naming # Service Naming -Mesos-DNS defines a DNS domain for Mesos tasks (default `.mesos`, see [instructions on configuration](configuration-parameters.html)). Running tasks can be discovered by looking up A and, optionally, SRV records within the Mesos domain. +Mesos-DNS defines a DNS domain for Mesos tasks (default `.mesos`, see [instructions on configuration](configuration-parameters.html)). Running tasks can be discovered by looking up A, AAAA, and, optionally, SRV records within the Mesos domain. -## A Records +## A and AAAA Records -An A record associates a hostname to an IP address. -For task `task` launched by framework `framework`, Mesos-DNS generates an A record for hostname `task.framework.domain` that provides one of the following: +A and AAAA records associate a hostname to an IPv4 or IPv6 address respectively. +For task `task` launched by framework `framework`, Mesos-DNS generates an A or AAAA record for hostname `task.framework.domain` that provides one of the following: - the IP address of the task's network container (provided by a Mesos containerizer); or - the IP address of the specific slave running the task. @@ -64,7 +64,31 @@ For example, a query of the A records for `search.marathon.slave.mesos` would yi - `MesosContainerizer.NetworkSettings.IPAddress`. In general support for these will not be available before Mesos 0.24. - + +If the following conditions are true... + +- the Mesos-DNS IP-source configuration prioritizes container IPs; and +- the Mesos containerizer that launches the task provides a container IP `fd01:b::2:8000:2` for the task `search.marathon.mesos` + +...then a lookup for AAAA would give: + +``` console +$ dig search.marathon.mesos + +; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> search.marathon.mesos +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 45066 +;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 + +;; QUESTION SECTION: +;search.marathon.mesos. IN AAAA + +;; ANSWER SECTION: +search.marathon.mesos. 60 IN AAAA fd01:b::2:8000:2 +``` + + ## SRV Records An SRV record associates a service name to a hostname and an IP port. @@ -102,10 +126,10 @@ The following table illustrates the rules that govern SRV generation: ## Other Records Mesos-DNS generates a few special records: -- for the leading master: A record (`leader.domain`) and SRV records (`_leader._tcp.domain` and `_leader._udp.domain`); and -- for all framework schedulers: A records (`{framework}.domain`) and SRV records (`_framework._tcp.{framework}.domain`) -- for every known Mesos master: A records (`master.domain`) and SRV records (`_master._tcp.domain` and `_master._udp.domain`); and -- for every known Mesos slave: A records (`slave.domain`) and SRV records (`_slave._tcp.domain`). +- for the leading master: A or AAAA record (`leader.domain`) and SRV records (`_leader._tcp.domain` and `_leader._udp.domain`); and +- for all framework schedulers: A and AAAA records (`{framework}.domain`) and SRV records (`_framework._tcp.{framework}.domain`) +- for every known Mesos master: A or AAAA records (`master.domain`) and SRV records (`_master._tcp.domain` and `_master._udp.domain`); and +- for every known Mesos slave: A or AAAA records (`slave.domain`) and SRV records (`_slave._tcp.domain`). Note that, if you configure Mesos-DNS to detect the leading master through Zookeeper, then this is the only master it knows about. If you configure Mesos-DNS using the `masters` field, it will generate master records for every master in the list. diff --git a/docs/index.md b/docs/index.md index 419b273e..766028d4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,6 +25,6 @@ Mesos-DNS is designed to be a minimal, stateless service that is easy to deploy

-Mesos-DNS periodically queries the Mesos master(s), retrieves the state of all running tasks from all running frameworks, and generates DNS records for these tasks (A and SRV records). As tasks start, finish, fail, or restart on the Mesos cluster, Mesos-DNS updates the DNS records to reflect the latest state. The configuration of Mesos-DNS is minimal. You simply point it to the Mesos masters at launch. Frameworks do not need to communicate with Mesos-DNS at all. Applications and services running on Mesos slaves can discover the IP addresses and ports of other applications they depend upon by issuing DNS lookup requests or by issuing HTTP request through a REST API. Mesos-DNS replies directly to requests for tasks launched by Mesos. For DNS requests for other hostnames or services, Mesos-DNS uses an external nameserver to derive replies. Alternatively, you can configure your existing DNS server to forward only the requests for Mesos tasks to Mesos-DNS. +Mesos-DNS periodically queries the Mesos master(s), retrieves the state of all running tasks from all running frameworks, and generates DNS records for these tasks (A, AAAA, and SRV records). As tasks start, finish, fail, or restart on the Mesos cluster, Mesos-DNS updates the DNS records to reflect the latest state. The configuration of Mesos-DNS is minimal. You simply point it to the Mesos masters at launch. Frameworks do not need to communicate with Mesos-DNS at all. Applications and services running on Mesos slaves can discover the IP addresses and ports of other applications they depend upon by issuing DNS lookup requests or by issuing HTTP request through a REST API. Mesos-DNS replies directly to requests for tasks launched by Mesos. For DNS requests for other hostnames or services, Mesos-DNS uses an external nameserver to derive replies. Alternatively, you can configure your existing DNS server to forward only the requests for Mesos tasks to Mesos-DNS. Mesos-DNS is simple and stateless. It does not require consensus mechanisms, persistent storage, or a replicated log. This is possible because Mesos-DNS does not implement heartbeats, health monitoring, or lifetime management for applications. This functionality is already available by the Mesos master, slaves, and frameworks. Mesos-DNS can be made fault-tolerant by launching with a framework like [Marathon](https://github.com/mesosphere/marathon), that can monitor application health and re-launch it on failures. On restart after a failure, Mesos-DNS retrieves the latest state from the Mesos master(s) and serves DNS requests without further coordination. It can be easily replicated to improve availability or to load balance DNS requests in clusters with large numbers of slaves. diff --git a/factories/fake.json b/factories/fake.json index 3b791fab..c219f05e 100644 --- a/factories/fake.json +++ b/factories/fake.json @@ -1,5 +1,5 @@ { - "activated_slaves": 3, + "activated_slaves": 4, "build_date": "2014-07-18 18:52:23", "build_time": 1405709543, "build_user": "root", @@ -38,6 +38,69 @@ "zk_session_timeout": "10secs" }, "frameworks": [ + { + "active": true, + "checkpoint": false, + "completed_tasks": [], + "failover_timeout": 1200, + "hostname": "localhost", + "pid": "scheduler(1)@[2001:db8::1]:25501", + "id": "20140703-014514-3041283216-5050-5349-0001", + "name": "ipv6-framework", + "offers": [], + "registered_time": 1414913537.10742, + "reregistered_time": 1414913537.10744, + "resources": { + "cpus": 0, + "disk": 0, + "mem": 0 + }, + "role": "*", + "tasks": [ + { + "executor_id": "", + "framework_id": "20140703-014514-3041283216-5050-5349-0001", + "id": "toy-store.b8db9f73-562f-11e4-a088-c20493233aa5", + "name": "toy-store", + "resources": { + "cpus": 1, + "disk": 0, + "mem": 1024, + "ports": "[31354-31354, 31355-31355]" + }, + "slave_id": "20140803-194712-3041283216-5050-2116-3", + "state": "TASK_RUNNING", + "statuses": [ + { + "state": "TASK_RUNNING", + "timestamp": 1507139816.34576, + "container_status": { + "container_id": { + "value": "da87d8aa-98f1-4fc3-a260-f92c116fc926" + }, + "network_infos": [ + { + "ip_addresses": [ + { + "protocol": "IPv4", + "ip_address": "12.0.1.2" + }, + { + "protocol": "IPv6", + "ip_address": "fd01:b::1:8000:2" + } + ], + "name": "dcos6" + } + ] + } + } + ] + } + ], + "unregistered_time": 0, + "user": "root" + }, { "active": true, "checkpoint": false, @@ -619,6 +682,22 @@ "mem": 31016, "ports": "[31000-32000]" } + }, + { + "attributes": { + "host": "dev-123-1g.c.mesos.internal" + }, + "hostname": "2001:db8::1", + "id": "20140803-194712-3041283216-5050-2116-3", + "pid": "slave(1)@[2001:db8::1]:5051", + "registered_time": 1414913537.56731, + "reregistered_time": 1414913537.56735, + "resources": { + "cpus": 8, + "disk": 1855784, + "mem": 31016, + "ports": "[31000-32000]" + } } ], "staged_tasks": 310, diff --git a/models/models.go b/models/models.go index e0c2a65b..c2742888 100644 --- a/models/models.go +++ b/models/models.go @@ -8,10 +8,11 @@ type AXFRResourceRecordSet map[string][]string // This is the internal structure of how mesos-dns works today and the transformation of string -> DNS Struct // happens on actual query time. Why this logic happens at query time? Who knows. -// AXFRRecords are the As, and SRVs that actually make up the Mesos-DNS zone +// AXFRRecords are the As, AAAAs, and SRVs that actually make up the Mesos-DNS zone type AXFRRecords struct { - As AXFRResourceRecordSet - SRVs AXFRResourceRecordSet + As AXFRResourceRecordSet + AAAAs AXFRResourceRecordSet + SRVs AXFRResourceRecordSet } // AXFR is a rough representation of a "transfer" of the Mesos-DNS data diff --git a/records/config.go b/records/config.go index b2c20f23..de883528 100644 --- a/records/config.go +++ b/records/config.go @@ -412,9 +412,13 @@ func localAddies() []string { if err != nil { logging.Error.Println(err) } - t4 := ip.To4() - if t4 != nil { + // The servers in cservers above could include + // ipv6, so we should include local ipv6 for + // that filtering + if t4 := ip.To4(); t4 != nil { bad = append(bad, t4.String()) + } else if t6 := ip.To16(); t6 != nil { + bad = append(bad, t6.String()) } } diff --git a/records/config_test.go b/records/config_test.go index 1588c9ec..239724e8 100644 --- a/records/config_test.go +++ b/records/config_test.go @@ -5,13 +5,10 @@ import ( ) func TestNonLocalAddies(t *testing.T) { - nlocal := []string{"127.0.0.1"} + nlocal := localAddies() addies := nonLocalAddies(nlocal) - - for i := 0; i < len(addies); i++ { - if "127.0.0.1" == addies[i] { - t.Error("finding a local address") - } + if len(addies) > 0 { + t.Error("finding a local address") } } diff --git a/records/generator.go b/records/generator.go index fde76648..2f45c59d 100644 --- a/records/generator.go +++ b/records/generator.go @@ -72,6 +72,8 @@ type rrsKind string const ( // A record types A rrsKind = "A" + // AAAA record types + AAAA rrsKind = "AAAA" // SRV record types SRV = "SRV" ) @@ -80,6 +82,8 @@ func (kind rrsKind) rrs(rg *RecordGenerator) rrs { switch kind { case A: return rg.As + case AAAA: + return rg.AAAAs case SRV: return rg.SRVs default: @@ -91,8 +95,9 @@ func (kind rrsKind) rrs(rg *RecordGenerator) rrs { // them. TODO(kozyraki): Refactor when discovery id is available. type RecordGenerator struct { As rrs + AAAAs rrs SRVs rrs - SlaveIPs map[string]string + SlaveIPs map[string][]string EnumData EnumerationData stateLoader func(masters []string) (state.State, error) } @@ -196,28 +201,12 @@ func hashString(s string) string { return zbase32.EncodeToString(hash[:])[:5] } -// attempt to translate the hostname into an IPv4 address. logs an error if IP -// lookup fails. if an IP address cannot be found, returns the same hostname -// that was given. upon success returns the IP address as a string. -func hostToIP4(hostname string) (string, bool) { - ip := net.ParseIP(hostname) - if ip == nil { - t, err := net.ResolveIPAddr("ip4", hostname) - if err != nil { - logging.Error.Printf("cannot translate hostname %q into an ip4 address", hostname) - return hostname, false - } - ip = t.IP - } - return ip.String(), true -} - // InsertState transforms a StateJSON into RecordGenerator RRs func (rg *RecordGenerator) InsertState(sj state.State, domain, ns, listener string, masters, ipSources []string, spec labels.Func) error { - - rg.SlaveIPs = map[string]string{} + rg.SlaveIPs = map[string][]string{} rg.SRVs = rrs{} rg.As = rrs{} + rg.AAAAs = rrs{} rg.frameworkRecords(sj, domain, spec) rg.slaveRecords(sj, domain, spec) rg.listenerRecord(listener, ns) @@ -227,16 +216,18 @@ func (rg *RecordGenerator) InsertState(sj state.State, domain, ns, listener stri return nil } -// frameworkRecords injects A and SRV records into the generator store: +// frameworkRecords injects A, AAAA, and SRV records into the generator store: // frameworkname.domain. // resolves to IPs of each framework // _framework._tcp.frameworkname.domain. // resolves to the driver port and IP of each framework func (rg *RecordGenerator) frameworkRecords(sj state.State, domain string, spec labels.Func) { for _, f := range sj.Frameworks { - fname := labels.DomainFrag(f.Name, labels.Sep, spec) host, port := f.HostPort() - if address, ok := hostToIP4(host); ok { + if ips := hostToIPs(host); len(ips) > 0 { + fname := labels.DomainFrag(f.Name, labels.Sep, spec) a := fname + "." + domain + "." - rg.insertRR(a, address, A) + for _, ip := range ips { + rg.insertRR(a, ip.String(), rrsKindForIP(ip)) + } if port != "" { srvAddress := net.JoinHostPort(a, port) rg.insertRR("_framework._tcp."+a, srvAddress, SRV) @@ -249,18 +240,24 @@ func (rg *RecordGenerator) frameworkRecords(sj state.State, domain string, spec // slave.domain. // resolves to IPs of all slaves // _slave._tcp.domain. // resolves to the driver port and IP of all slaves func (rg *RecordGenerator) slaveRecords(sj state.State, domain string, spec labels.Func) { + a := "slave." + domain + "." for _, slave := range sj.Slaves { - address, ok := hostToIP4(slave.PID.Host) - if ok { - a := "slave." + domain + "." - rg.insertRR(a, address, A) + slaveIPs := []string{} + if ips := hostToIPs(slave.PID.Host); len(ips) > 0 { + for _, ip := range ips { + rg.insertRR(a, ip.String(), rrsKindForIP(ip)) + slaveIPs = append(slaveIPs, ip.String()) + } srv := net.JoinHostPort(a, slave.PID.Port) rg.insertRR("_slave._tcp."+domain+".", srv, SRV) } else { - logging.VeryVerbose.Printf("string '%q' for slave with id %q is not a valid IP address", address, slave.ID) - address = labels.DomainFrag(address, labels.Sep, spec) + logging.VeryVerbose.Printf("string %q for slave with id %q is not a valid IP address", slave.PID.Host, slave.ID) + } + if len(slaveIPs) == 0 { + address := labels.DomainFrag(slave.PID.Host, labels.Sep, spec) + slaveIPs = append(slaveIPs, address) } - rg.SlaveIPs[slave.ID] = address + rg.SlaveIPs[slave.ID] = slaveIPs } } @@ -292,7 +289,7 @@ func (rg *RecordGenerator) slaveRecords(sj state.State, domain string, spec labe // list. There are probably better ways to do it. func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader string) { // create records for leader - // A records + // A and AAAA records h := strings.Split(leader, "@") if len(h) < 2 { logging.Error.Println(leader) @@ -304,10 +301,11 @@ func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader logging.Error.Println(err) return } + ipKind := rrsKindForIPStr(ip) leaderRecord := "leader." + domain + "." - rg.insertRR(leaderRecord, ip, A) + rg.insertRR(leaderRecord, ip, ipKind) allMasterRecord := "master." + domain + "." - rg.insertRR(allMasterRecord, ip, A) + rg.insertRR(allMasterRecord, ip, ipKind) // SRV records tcp := "_leader._tcp." + domain + "." @@ -325,10 +323,11 @@ func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader logging.Error.Println(err) continue } + masterIPKind := rrsKindForIPStr(masterIP) - // A records (master and masterN) + // A and AAAA records (master and masterN) if master != leaderAddress { - added := rg.insertRR(allMasterRecord, masterIP, A) + added := rg.insertRR(allMasterRecord, masterIP, masterIPKind) if !added { // duplicate master?! continue @@ -341,9 +340,8 @@ func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader } perMasterRecord := "master" + strconv.Itoa(idx) + "." + domain + "." - rg.insertRR(perMasterRecord, masterIP, A) + rg.insertRR(perMasterRecord, masterIP, masterIPKind) idx++ - if master == leaderAddress { addedLeaderMasterN = true } @@ -355,18 +353,18 @@ func (rg *RecordGenerator) masterRecord(domain string, masters []string, leader logging.Error.Printf("warning: leader %q is not in master list", leader) } extraMasterRecord := "master" + strconv.Itoa(idx) + "." + domain + "." - rg.insertRR(extraMasterRecord, ip, A) + rg.insertRR(extraMasterRecord, ip, ipKind) } } -// A record for mesos-dns (the name is listed in SOA replies) +// A or AAAA record for mesos-dns (the name is listed in SOA replies) func (rg *RecordGenerator) listenerRecord(listener string, ns string) { if listener == "0.0.0.0" { rg.setFromLocal(listener, ns) } else if listener == "127.0.0.1" { rg.insertRR(ns, "127.0.0.1", A) } else { - rg.insertRR(ns, listener, A) + rg.insertRR(ns, listener, rrsKindForIPStr(listener)) } } @@ -380,7 +378,7 @@ func (rg *RecordGenerator) taskRecords(sj state.State, domain string, spec label for _, task := range f.Tasks { var ok bool - task.SlaveIP, ok = rg.SlaveIPs[task.SlaveID] + task.SlaveIPs, ok = rg.SlaveIPs[task.SlaveID] // only do running and discoverable tasks if ok && (task.State == "TASK_RUNNING") { @@ -391,11 +389,11 @@ func (rg *RecordGenerator) taskRecords(sj state.State, domain string, spec label } type context struct { - taskName, - taskID, - slaveID, - taskIP, - slaveIP string + taskName string + taskID string + slaveID string + taskIPs []net.IP + slaveIPs []string } func (rg *RecordGenerator) taskRecord(task state.Task, f state.Framework, domain string, spec labels.Func, ipSources []string, enumFW *EnumerableFramework) { @@ -409,8 +407,8 @@ func (rg *RecordGenerator) taskRecord(task state.Task, f state.Framework, domain spec(task.Name), hashString(task.ID), slaveIDTail(task.SlaveID), - task.IP(ipSources...), - task.SlaveIP, + task.IPs(ipSources...), + task.SlaveIPs, } // use DiscoveryInfo name if defined instead of task name @@ -432,15 +430,30 @@ func (rg *RecordGenerator) taskContextRecord(ctx context, task state.Task, f sta tail := "." + domain + "." - // insert canonical A records + // insert canonical A / AAAA records canonical := ctx.taskName + "-" + ctx.taskID + "-" + ctx.slaveID + "." + fname arec := ctx.taskName + "." + fname - rg.insertTaskRR(arec+tail, ctx.taskIP, A, enumTask) - rg.insertTaskRR(canonical+tail, ctx.taskIP, A, enumTask) + // Only use the first ipv4 and first ipv6 found in sources + tIPs := ipsTo4And6(ctx.taskIPs) + for _, tIP := range tIPs { + rg.insertTaskRR(arec+tail, tIP.String(), rrsKindForIP(tIP), enumTask) + rg.insertTaskRR(canonical+tail, tIP.String(), rrsKindForIP(tIP), enumTask) + } - rg.insertTaskRR(arec+".slave"+tail, ctx.slaveIP, A, enumTask) - rg.insertTaskRR(canonical+".slave"+tail, ctx.slaveIP, A, enumTask) + // slaveIPs already only has at most one ipv4 and one ipv6 + for _, sIPStr := range ctx.slaveIPs { + if sIP := net.ParseIP(sIPStr); sIP != nil { + rg.insertTaskRR(arec+".slave"+tail, sIP.String(), rrsKindForIP(sIP), enumTask) + rg.insertTaskRR(canonical+".slave"+tail, sIP.String(), rrsKindForIP(sIP), enumTask) + } else { + // ack: slave IP may not be an actual IP if labels.DomainFrag was used. + // Does labels.DomainFrag produce a valid A record value? + // Issue to track: https://github.com/mesosphere/mesos-dns/issues/509 + rg.insertTaskRR(arec+".slave"+tail, sIPStr, A, enumTask) + rg.insertTaskRR(canonical+".slave"+tail, sIPStr, A, enumTask) + } + } // recordName generates records for ctx.taskName, given some generation chain recordName := func(gen chain) { gen("_" + ctx.taskName) } @@ -481,7 +494,7 @@ func (rg *RecordGenerator) taskContextRecord(ctx context, task state.Task, f sta } } -// A records for each local interface +// A and AAAA records for each local interface // If this causes problems you should explicitly set the // listener address in config.json func (rg *RecordGenerator) setFromLocal(host string, ns string) { @@ -512,12 +525,7 @@ func (rg *RecordGenerator) setFromLocal(host string, ns string) { continue } - ip = ip.To4() - if ip == nil { - continue - } - - rg.insertRR(ns, ip.String(), A) + rg.insertRR(ns, ip.String(), rrsKindForIP(ip)) } } } @@ -543,6 +551,64 @@ func (rg *RecordGenerator) insertRR(name, host string, kind rrsKind) (added bool return } +func rrsKindForIP(ip net.IP) rrsKind { + if ip.To4() != nil { + return A + } else if ip.To16() != nil { + return AAAA + } + panic("unknown ip type: " + ip.String()) +} + +func rrsKindForIPStr(ip string) rrsKind { + if parsedIP := net.ParseIP(ip); parsedIP != nil { + return rrsKindForIP(parsedIP) + } + panic("unable to parse ip: " + ip) +} + +// ipsTo4And6 returns a list with at most 1 ipv4 and 1 ipv6 +// from a list of IPs +func ipsTo4And6(allIPs []net.IP) (ips []net.IP) { + var ipv4, ipv6 net.IP + for _, ip := range allIPs { + if ipv4 != nil && ipv6 != nil { + break + } else if t4 := ip.To4(); t4 != nil { + if ipv4 == nil { + ipv4 = t4 + } + } else if t6 := ip.To16(); t6 != nil { + if ipv6 == nil { + ipv6 = t6 + } + } + } + ips = []net.IP{} + if ipv4 != nil { + ips = append(ips, ipv4) + } + if ipv6 != nil { + ips = append(ips, ipv6) + } + return +} + +// hostToIPs attempts to parse a hostname into an ip. +// If that doesn't work it will perform a lookup and try to +// find one ipv4 and one ipv6 in the results. +func hostToIPs(hostname string) (ips []net.IP) { + if ip := net.ParseIP(hostname); ip != nil { + ips = []net.IP{ip} + } else if allIPs, err := net.LookupIP(hostname); err == nil { + ips = ipsTo4And6(allIPs) + } + if len(ips) == 0 { + logging.VeryVerbose.Printf("cannot translate hostname %q into an ipv4 or ipv6 address", hostname) + } + return +} + // return the slave number from a Mesos slave id func slaveIDTail(slaveID string) string { fields := strings.Split(slaveID, "-") diff --git a/records/generator_perf_test.go b/records/generator_perf_test.go index 9f76b7b1..91aff366 100644 --- a/records/generator_perf_test.go +++ b/records/generator_perf_test.go @@ -85,7 +85,7 @@ func BenchmarkTaskRecord_withoutDiscoveryInfo(b *testing.B) { ti = rand.Int31n(taskCount) ) tt.task.Name = tasks[ti] - tt.task.SlaveIP = slaves[si] + tt.task.SlaveIPs = []string{slaves[si]} tt.task.SlaveID = "ID-" + slaves[si] tt.rg.taskRecord(tt.task, tt.f, tt.domain, tt.spec, tt.ipSources, &tt.enumFW) } diff --git a/records/generator_test.go b/records/generator_test.go index 78f9f59d..ca0ad30a 100644 --- a/records/generator_test.go +++ b/records/generator_test.go @@ -2,6 +2,7 @@ package records import ( "encoding/json" + "fmt" "io/ioutil" "math/rand" "net" @@ -35,25 +36,31 @@ func (rg *RecordGenerator) exists(name, host string, kind rrsKind) bool { func TestParseState_SOAMname(t *testing.T) { rg := &RecordGenerator{} rg.stateLoader = func(_ []string) (s state.State, err error) { - s.Leader = "foo@123:45" // required or else ParseState bails + s.Leader = "foo@0.1.2.3:45" // required or else ParseState bails return } - err := rg.ParseState(Config{SOAMname: "jdef123.mesos.", Listener: "4.5.6.7"}) - if err != nil { + cfg1 := Config{SOAMname: "jdef123.mesos.", Listener: "4.5.6.7"} + if err := rg.ParseState(cfg1); err != nil { t.Fatal("unexpected error", err) - } - if !rg.exists("jdef123.mesos.", "4.5.6.7", A) { + } else if !rg.exists("jdef123.mesos.", "4.5.6.7", A) { t.Fatalf("failed to locate A record for SOAMname, A records: %#v", rg.As) } + cfg2 := Config{SOAMname: "ack456.mesos.", Listener: "2001:db8::1"} + if err := rg.ParseState(cfg2); err != nil { + t.Fatal("unexpected error", err) + } else if !rg.exists("ack456.mesos.", "2001:db8::1", AAAA) { + t.Fatalf("failed to locate AAAA record for SOAMname, AAAA records: %#v", rg.AAAAs) + } +} + +type expectedRR struct { + name string + host string + kind rrsKind } func TestMasterRecord(t *testing.T) { // masterRecord(domain string, masters []string, leader string) - type expectedRR struct { - name string - host string - kind rrsKind - } tt := []struct { domain string masters []string @@ -65,77 +72,97 @@ func TestMasterRecord(t *testing.T) { {"foo.com", nil, "1@", nil}, {"foo.com", nil, "@2", nil}, {"foo.com", nil, "3@4", nil}, - {"foo.com", nil, "5@6:7", + {"foo.com", nil, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master0.foo.com.", "6", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master0.foo.com.", "0.0.0.6", A}, + {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, + {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, + }}, + {"foo.com", nil, "5@[2001:db8::1]:7", + []expectedRR{ + {"leader.foo.com.", "2001:db8::1", AAAA}, + {"master.foo.com.", "2001:db8::1", AAAA}, + {"master0.foo.com.", "2001:db8::1", AAAA}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, // single master: leader and fallback - {"foo.com", []string{"6:7"}, "5@6:7", + {"foo.com", []string{"0.0.0.6:7"}, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master0.foo.com.", "6", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master0.foo.com.", "0.0.0.6", A}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, // leader not in fallback list - {"foo.com", []string{"8:9"}, "5@6:7", + {"foo.com", []string{"0.0.0.8:9"}, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master.foo.com.", "8", A}, - {"master1.foo.com.", "6", A}, - {"master0.foo.com.", "8", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.8", A}, + {"master1.foo.com.", "0.0.0.6", A}, + {"master0.foo.com.", "0.0.0.8", A}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, // duplicate fallback masters, leader not in fallback list - {"foo.com", []string{"8:9", "8:9"}, "5@6:7", + {"foo.com", []string{"0.0.0.8:9", "0.0.0.8:9"}, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master.foo.com.", "8", A}, - {"master1.foo.com.", "6", A}, - {"master0.foo.com.", "8", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.8", A}, + {"master1.foo.com.", "0.0.0.6", A}, + {"master0.foo.com.", "0.0.0.8", A}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, // leader that's also listed in the fallback list (at the end) - {"foo.com", []string{"8:9", "6:7"}, "5@6:7", + {"foo.com", []string{"0.0.0.8:9", "0.0.0.6:7"}, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master.foo.com.", "8", A}, - {"master1.foo.com.", "6", A}, - {"master0.foo.com.", "8", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.8", A}, + {"master1.foo.com.", "0.0.0.6", A}, + {"master0.foo.com.", "0.0.0.8", A}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, // duplicate leading masters in the fallback list - {"foo.com", []string{"8:9", "6:7", "6:7"}, "5@6:7", + {"foo.com", []string{"0.0.0.8:9", "0.0.0.6:7", "0.0.0.6:7"}, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master.foo.com.", "8", A}, - {"master1.foo.com.", "6", A}, - {"master0.foo.com.", "8", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.8", A}, + {"master1.foo.com.", "0.0.0.6", A}, + {"master0.foo.com.", "0.0.0.8", A}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, // leader that's also listed in the fallback list (in the middle) - {"foo.com", []string{"8:9", "6:7", "bob:0"}, "5@6:7", + {"foo.com", []string{"0.0.0.8:9", "0.0.0.6:7", "0.0.0.7:0"}, "5@0.0.0.6:7", + []expectedRR{ + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.8", A}, + {"master.foo.com.", "0.0.0.7", A}, + {"master0.foo.com.", "0.0.0.8", A}, + {"master1.foo.com.", "0.0.0.6", A}, + {"master2.foo.com.", "0.0.0.7", A}, + {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, + {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, + }}, + {"foo.com", []string{"0.0.0.8:9", "0.0.0.6:7", "[2001:db8::1]:0"}, "5@0.0.0.6:7", []expectedRR{ - {"leader.foo.com.", "6", A}, - {"master.foo.com.", "6", A}, - {"master.foo.com.", "8", A}, - {"master.foo.com.", "bob", A}, - {"master0.foo.com.", "8", A}, - {"master1.foo.com.", "6", A}, - {"master2.foo.com.", "bob", A}, + {"leader.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.6", A}, + {"master.foo.com.", "0.0.0.8", A}, + {"master.foo.com.", "2001:db8::1", AAAA}, + {"master0.foo.com.", "0.0.0.8", A}, + {"master1.foo.com.", "0.0.0.6", A}, + {"master2.foo.com.", "2001:db8::1", AAAA}, {"_leader._tcp.foo.com.", "leader.foo.com.:7", SRV}, {"_leader._udp.foo.com.", "leader.foo.com.:7", SRV}, }}, @@ -143,6 +170,7 @@ func TestMasterRecord(t *testing.T) { for i, tc := range tt { rg := &RecordGenerator{} rg.As = make(rrs) + rg.AAAAs = make(rrs) rg.SRVs = make(rrs) t.Logf("test case %d", i+1) rg.masterRecord(tc.domain, tc.masters, tc.leader) @@ -150,35 +178,54 @@ func TestMasterRecord(t *testing.T) { if len(rg.As) > 0 { t.Fatalf("test case %d: unexpected As: %v", i+1, rg.As) } + if len(rg.AAAAs) > 0 { + t.Fatalf("test case %d: unexpected AAAAs: %v", i+1, rg.AAAAs) + } if len(rg.SRVs) > 0 { t.Fatalf("test case %d: unexpected SRVs: %v", i+1, rg.SRVs) } } - expectedA := make(rrs) - expectedSRV := make(rrs) - for _, e := range tc.expect { - found := rg.exists(e.name, e.host, e.kind) - if !found { - t.Fatalf("test case %d: missing expected record: name=%q host=%q kind=%s, As=%v", i+1, e.name, e.host, e.kind, rg.As) - } - switch e.kind { - case A: - expectedA.add(e.name, e.host) - case SRV: - expectedSRV.add(e.name, e.host) - default: - t.Fatalf("unexpected kind %q", e.kind) - } + eA, eAAAA, eSRV, err := expectRecords(rg, tc.expect) + if err != nil { + t.Fatalf("test case %d: %s, As=%v, AAAAs=%v, SRVs=%v", i+1, err, rg.As, rg.AAAAs, rg.SRVs) + } + if !reflect.DeepEqual(rg.As, eA) { + t.Fatalf("test case %d: expected As of %v instead of %v", i+1, eA, rg.As) } - if !reflect.DeepEqual(rg.As, expectedA) { - t.Fatalf("test case %d: expected As of %v instead of %v", i+1, expectedA, rg.As) + if !reflect.DeepEqual(rg.AAAAs, eAAAA) { + t.Fatalf("test case %d: expected AAAAs of %v instead of %v", i+1, eAAAA, rg.AAAAs) } - if !reflect.DeepEqual(rg.SRVs, expectedSRV) { - t.Fatalf("test case %d: expected SRVs of %v instead of %v", i+1, expectedSRV, rg.SRVs) + if !reflect.DeepEqual(rg.SRVs, eSRV) { + t.Fatalf("test case %d: expected SRVs of %v instead of %v", i+1, eSRV, rg.SRVs) } } } +func expectRecords(rg *RecordGenerator, expect []expectedRR) (eA, eAAAA, eSRV rrs, err error) { + eA = make(rrs) + eAAAA = make(rrs) + eSRV = make(rrs) + for _, e := range expect { + found := rg.exists(e.name, e.host, e.kind) + if !found { + err = fmt.Errorf("missing expected record: name=%q host=%q kind=%s", e.name, e.host, e.kind) + return + } + switch e.kind { + case A: + eA.add(e.name, e.host) + case AAAA: + eAAAA.add(e.name, e.host) + case SRV: + eSRV.add(e.name, e.host) + default: + err = fmt.Errorf("unexpected kind: %q", e.kind) + return + } + } + return +} + func testRecordGenerator(t *testing.T, spec labels.Func, ipSources []string) RecordGenerator { var sj state.State @@ -202,10 +249,11 @@ func testRecordGenerator(t *testing.T, spec labels.Func, ipSources []string) Rec // ensure we are parsing what we think we are func TestInsertState(t *testing.T) { - rg := testRecordGenerator(t, labels.RFC952, []string{"docker", "mesos", "host"}) + rg := testRecordGenerator(t, labels.RFC952, []string{"netinfo", "docker", "mesos", "host"}) rgDocker := testRecordGenerator(t, labels.RFC952, []string{"docker", "host"}) rgMesos := testRecordGenerator(t, labels.RFC952, []string{"mesos", "host"}) rgSlave := testRecordGenerator(t, labels.RFC952, []string{"host"}) + rgNetinfo := testRecordGenerator(t, labels.RFC952, []string{"netinfo"}) for i, tt := range []struct { rrs rrs @@ -225,6 +273,12 @@ func TestInsertState(t *testing.T) { {rg.As, "slave.mesos.", []string{"1.2.3.10", "1.2.3.11", "1.2.3.12"}}, {rg.As, "some-box.chronoswithaspaceandmixe.mesos.", []string{"1.2.3.11"}}, // ensure we translate the framework name as well {rg.As, "marathon.mesos.", []string{"1.2.3.11"}}, + + {rg.AAAAs, "toy-store.ipv6-framework.mesos.", []string{"fd01:b::1:8000:2"}}, + {rg.AAAAs, "toy-store.ipv6-framework.slave.mesos.", []string{"2001:db8::1"}}, + {rg.AAAAs, "ipv6-framework.mesos.", []string{"2001:db8::1"}}, + {rg.AAAAs, "slave.mesos.", []string{"2001:db8::1"}}, + {rg.SRVs, "_big-dog._tcp.marathon.mesos.", []string{ "big-dog-4dfjd-0.marathon.mesos.:80", "big-dog-4dfjd-0.marathon.mesos.:443", @@ -263,15 +317,28 @@ func TestInsertState(t *testing.T) { {rgSlave.As, "nginx.marathon.mesos.", []string{"1.2.3.11"}}, {rgSlave.As, "car-store.marathon.slave.mesos.", []string{"1.2.3.11"}}, + {rgSlave.AAAAs, "toy-store.ipv6-framework.mesos.", []string{"2001:db8::1"}}, + {rgSlave.AAAAs, "toy-store.ipv6-framework.slave.mesos.", []string{"2001:db8::1"}}, + {rgMesos.As, "liquor-store.marathon.mesos.", []string{"1.2.3.11", "1.2.3.12"}}, {rgMesos.As, "liquor-store.marathon.slave.mesos.", []string{"1.2.3.11", "1.2.3.12"}}, {rgMesos.As, "nginx.marathon.mesos.", []string{"10.3.0.3"}}, {rgMesos.As, "car-store.marathon.slave.mesos.", []string{"1.2.3.11"}}, + {rgMesos.AAAAs, "toy-store.ipv6-framework.mesos.", []string{"2001:db8::1"}}, + {rgMesos.AAAAs, "toy-store.ipv6-framework.slave.mesos.", []string{"2001:db8::1"}}, + + {rgNetinfo.As, "toy-store.ipv6-framework.mesos.", []string{"12.0.1.2"}}, + + {rgNetinfo.AAAAs, "toy-store.ipv6-framework.mesos.", []string{"fd01:b::1:8000:2"}}, + {rgNetinfo.AAAAs, "toy-store.ipv6-framework.slave.mesos.", []string{"2001:db8::1"}}, + {rgDocker.As, "liquor-store.marathon.mesos.", []string{"10.3.0.1", "10.3.0.2"}}, {rgDocker.As, "liquor-store.marathon.slave.mesos.", []string{"1.2.3.11", "1.2.3.12"}}, {rgDocker.As, "nginx.marathon.mesos.", []string{"1.2.3.11"}}, {rgDocker.As, "car-store.marathon.slave.mesos.", []string{"1.2.3.11"}}, + {rgDocker.AAAAs, "toy-store.ipv6-framework.mesos.", []string{"2001:db8::1"}}, + {rgDocker.AAAAs, "toy-store.ipv6-framework.slave.mesos.", []string{"2001:db8::1"}}, } { // convert want into a map[string]struct{} (string set) for simpler comparison // via reflect.DeepEqual @@ -283,7 +350,7 @@ func TestInsertState(t *testing.T) { if len(got) == 0 && len(want) == 0 { continue } - t.Errorf("test #%d: %q: got: %q, want: %q", i, tt.name, got, want) + t.Errorf("test #%d: %q: got: %q, want: %q", i+1, tt.name, got, want) } } } diff --git a/records/state/state.go b/records/state/state.go index 6834f377..05d3bab2 100644 --- a/records/state/state.go +++ b/records/state/state.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/mesos/mesos-go/upid" "github.com/mesosphere/mesos-dns/logging" + "github.com/mesosphere/mesos-dns/records/state/upid" ) // Resources holds resources as defined in the /state.json Mesos HTTP endpoint. @@ -93,7 +93,8 @@ type Task struct { Resources `json:"resources"` DiscoveryInfo DiscoveryInfo `json:"discovery"` - SlaveIP string `json:"-"` + // SlaveIPs is used internally and contains ipv4, ipv6, or both + SlaveIPs []string `json:"-"` } // HasDiscoveryInfo return whether the DiscoveryInfo was provided in the state.json @@ -137,7 +138,7 @@ var sources = map[string]func(*Task) []string{ // hostIPs is an IPSource which returns the IP addresses of the slave a Task // runs on. -func hostIPs(t *Task) []string { return []string{t.SlaveIP} } +func hostIPs(t *Task) []string { return t.SlaveIPs } // networkInfoIPs returns IP addresses from a given Task's // []Status.ContainerStatus.[]NetworkInfos.[]IPAddresses.IPAddress diff --git a/records/state/state_test.go b/records/state/state_test.go index 92a91028..18e6d7d1 100644 --- a/records/state/state_test.go +++ b/records/state/state_test.go @@ -6,8 +6,8 @@ import ( "reflect" "testing" - "github.com/mesos/mesos-go/upid" . "github.com/mesosphere/mesos-dns/records/state" + "github.com/mesosphere/mesos-dns/records/state/upid" ) func TestResources_Ports(t *testing.T) { @@ -30,6 +30,7 @@ func TestPID_UnmarshalJSON(t *testing.T) { {`"slave(1)@127.0.0.1:5051"`, makePID("slave(1)", "127.0.0.1", "5051"), nil}, {` "slave(1)@127.0.0.1:5051" `, makePID("slave(1)", "127.0.0.1", "5051"), nil}, {`" slave(1)@127.0.0.1:5051 "`, makePID("slave(1)", "127.0.0.1", "5051"), nil}, + {`" slave(1)@[2001:db8::1]:5051 "`, makePID("slave(1)", "2001:db8::1", "5051"), nil}, } { var pid PID if err := json.Unmarshal([]byte(tt.data), &pid); !reflect.DeepEqual(err, tt.err) { @@ -87,11 +88,11 @@ func TestTask_IPs(t *testing.T) { }, { // source order Task: task( - slaveIP("2.3.4.5"), - statuses(status(state("TASK_RUNNING"), netinfos(netinfo("1.2.3.4")))), + slaveIPs("2.3.4.5"), + statuses(status(state("TASK_RUNNING"), netinfos(netinfo("1.2.3.4", "fd01:b::1:8000:2")))), ), srcs: []string{"host", "netinfo"}, - want: ips("2.3.4.5", "1.2.3.4"), + want: ips("2.3.4.5", "1.2.3.4", "fd01:b::1:8000:2"), }, { // statuses state Task: task( @@ -165,8 +166,8 @@ func statuses(st ...Status) taskOpt { } } -func slaveIP(ip string) taskOpt { - return func(t *Task) { t.SlaveIP = ip } +func slaveIPs(ips ...string) taskOpt { + return func(t *Task) { t.SlaveIPs = ips } } func status(opts ...statusOpt) Status { diff --git a/records/state/upid/upid.go b/records/state/upid/upid.go new file mode 100644 index 00000000..a3fa791e --- /dev/null +++ b/records/state/upid/upid.go @@ -0,0 +1,51 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package upid + +import ( + "fmt" + "net" + "strings" +) + +// UPID is a equivalent of the UPID in libprocess. +type UPID struct { + ID string + Host string + Port string +} + +// Parse parses the UPID from the input string. +func Parse(input string) (*UPID, error) { + var err error + upid := new(UPID) + + splits := strings.Split(input, "@") + if len(splits) != 2 { + return nil, fmt.Errorf("Expect one `@' in the input") + } + upid.ID = splits[0] + + // Using network "tcp" allows us to resolve ipv4 and ipv6 + if _, err = net.ResolveTCPAddr("tcp", splits[1]); err != nil { + return nil, err + } + upid.Host, upid.Port, err = net.SplitHostPort(splits[1]) + return upid, err +} diff --git a/resolver/resolver.go b/resolver/resolver.go index 32b16b85..ca61aa30 100644 --- a/resolver/resolver.go +++ b/resolver/resolver.go @@ -241,6 +241,26 @@ func (res *Resolver) formatA(dom string, target string) (*dns.A, error) { }, nil } +// returns the AAAA resource record for target +// assumes target is a well formed IPv6 address +func (res *Resolver) formatAAAA(dom string, target string) (*dns.AAAA, error) { + ttl := uint32(res.config.TTL) + + aaaa := net.ParseIP(target) + if aaaa == nil { + return nil, errors.New("invalid target") + } + + return &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: dom, + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: ttl}, + AAAA: aaaa.To16(), + }, nil +} + // formatSOA returns the SOA resource record for the mesos domain func (res *Resolver) formatSOA(dom string) *dns.SOA { ttl := uint32(res.config.TTL) @@ -315,7 +335,7 @@ func rcode(err error) int { // HandleMesos is a resolver request handler that responds to a resource // question with resource answer(s) -// it can handle {A, SRV, ANY} +// it can handle {A, AAAA, SRV, ANY} func (res *Resolver) HandleMesos(w dns.ResponseWriter, r *dns.Msg) { logging.CurLog.MesosRequests.Inc() @@ -333,6 +353,8 @@ func (res *Resolver) HandleMesos(w dns.ResponseWriter, r *dns.Msg) { errs.Add(res.handleSRV(rs, name, m, r)) case dns.TypeA: errs.Add(res.handleA(rs, name, m)) + case dns.TypeAAAA: + errs.Add(res.handleAAAA(rs, name, m)) case dns.TypeSOA: errs.Add(res.handleSOA(m, r)) case dns.TypeNS: @@ -341,6 +363,7 @@ func (res *Resolver) HandleMesos(w dns.ResponseWriter, r *dns.Msg) { errs.Add( res.handleSRV(rs, name, m, r), res.handleA(rs, name, m), + res.handleAAAA(rs, name, m), res.handleSOA(m, r), res.handleNS(m, r), ) @@ -363,7 +386,8 @@ func (res *Resolver) HandleMesos(w dns.ResponseWriter, r *dns.Msg) { func (res *Resolver) handleSRV(rs *records.RecordGenerator, name string, m, r *dns.Msg) error { var errs multiError - added := map[string]struct{}{} // track the A RR's we've already added, avoid dups + aAdded := map[string]struct{}{} // track the A RR's we've already added, avoid dups + aaaaAdded := map[string]struct{}{} // track the AAAA RR's we've already added, avoid dups for srv := range rs.SRVs[name] { srvRR, err := res.formatSRV(r.Question[0].Name, srv) if err != nil { @@ -372,23 +396,32 @@ func (res *Resolver) handleSRV(rs *records.RecordGenerator, name string, m, r *d } m.Answer = append(m.Answer, srvRR) - host := strings.Split(srv, ":")[0] - if _, found := added[host]; found { - // avoid dups - continue + host, _, err := net.SplitHostPort(srv) + if err != nil { + logging.Error.Println(err) } - if len(rs.As[host]) == 0 { + if len(rs.As[host]) == 0 && len(rs.AAAAs[host]) == 0 { continue } - - if a, ok := rs.As.First(host); ok { - aRR, err := res.formatA(host, a) - if err != nil { - errs.Add(err) - continue + if _, aFound := aAdded[host]; !aFound { + if a, ok := rs.As.First(host); ok { + if aRR, err := res.formatA(host, a); err == nil { + m.Extra = append(m.Extra, aRR) + aAdded[host] = struct{}{} + } else { + errs.Add(err) + } + } + } + if _, aaaaFound := aaaaAdded[host]; !aaaaFound { + if aaaa, ok := rs.AAAAs.First(host); ok { + if aaaaRR, err := res.formatAAAA(host, aaaa); err == nil { + m.Extra = append(m.Extra, aaaaRR) + aaaaAdded[host] = struct{}{} + } else { + errs.Add(err) + } } - m.Extra = append(m.Extra, aRR) - added[host] = struct{}{} } } return errs @@ -407,6 +440,19 @@ func (res *Resolver) handleA(rs *records.RecordGenerator, name string, m *dns.Ms return errs } +func (res *Resolver) handleAAAA(rs *records.RecordGenerator, name string, m *dns.Msg) error { + var errs multiError + for aaaa := range rs.AAAAs[name] { + rr, err := res.formatAAAA(name, aaaa) + if err != nil { + errs.Add(err) + continue + } + m.Answer = append(m.Answer, rr) + } + return errs +} + func (res *Resolver) handleSOA(m, r *dns.Msg) error { m.Ns = append(m.Ns, res.formatSOA(r.Question[0].Name)) return nil @@ -427,26 +473,16 @@ func (res *Resolver) handleEmpty(rs *records.RecordGenerator, name string, m, r m.Rcode = dns.RcodeNameError - // Because we don't implement AAAA records, AAAA queries will always - // go via this path - // Unfortunately, we don't implement AAAA queries in Mesos-DNS, - // and although the 'Not Implemented' error code seems more suitable, - // RFCs do not recommend it: https://tools.ietf.org/html/rfc4074 - // Therefore we always return success, which is synonymous with NODATA - // to get a positive cache on no records AAAA - // Further information: - // PR: https://github.com/mesosphere/mesos-dns/pull/366 - // Issue: https://github.com/mesosphere/mesos-dns/issues/363 - // The second component is just a matter of returning NODATA if we have // SRV or A records for the given name, but no neccessarily the given query - if (qType == dns.TypeAAAA) || (len(rs.SRVs[name])+len(rs.As[name]) > 0) { + if len(rs.SRVs[name])+len(rs.As[name])+len(rs.AAAAs[name]) > 0 { m.Rcode = dns.RcodeSuccess } logging.CurLog.MesosNXDomain.Inc() logging.VeryVerbose.Println("total A rrs:\t" + strconv.Itoa(len(rs.As))) + logging.VeryVerbose.Println("total AAAA rrs:\t" + strconv.Itoa(len(rs.AAAAs))) logging.VeryVerbose.Println("failed looking for " + r.Question[0].String()) m.Ns = append(m.Ns, res.formatSOA(r.Question[0].Name)) @@ -606,8 +642,9 @@ func (res *Resolver) RestAXFR(req *restful.Request, resp *restful.Response) { records := res.records() AXFRRecords := models.AXFRRecords{ - SRVs: records.SRVs.ToAXFRResourceRecordSet(), - As: records.As.ToAXFRResourceRecordSet(), + SRVs: records.SRVs.ToAXFRResourceRecordSet(), + As: records.As.ToAXFRResourceRecordSet(), + AAAAs: records.AAAAs.ToAXFRResourceRecordSet(), } AXFR := models.AXFR{ Records: AXFRRecords, @@ -652,10 +689,14 @@ func (res *Resolver) RestHost(req *restful.Request, resp *restful.Response) { } aRRs := rs.As[dom] - records := make([]record, 0, len(aRRs)) + aaaaRRs := rs.AAAAs[dom] + records := make([]record, 0, len(aRRs)+len(aaaaRRs)) for ip := range aRRs { records = append(records, record{dom, ip}) } + for ip := range aaaaRRs { + records = append(records, record{dom, ip}) + } if len(records) == 0 { records = append(records, record{}) @@ -715,11 +756,12 @@ func (res *Resolver) RestService(req *restful.Request, resp *restful.Response) { logging.Error.Println(err) continue } - var ip string - if r, ok := rs.As.First(host); ok { - ip = r + if aR, aOk := rs.As.First(host); aOk { + records = append(records, record{service, host, aR, port}) + } + if aaaaR, aaaaOk := rs.AAAAs.First(host); aaaaOk { + records = append(records, record{service, host, aaaaR, port}) } - records = append(records, record{service, host, ip, port}) } if len(records) == 0 { diff --git a/resolver/resolver_test.go b/resolver/resolver_test.go index 461168ea..64fce303 100644 --- a/resolver/resolver_test.go +++ b/resolver/resolver_test.go @@ -130,6 +130,15 @@ func runHandlers() error { A(RRHeader("chronos.marathon.mesos.", dns.TypeA, 60), net.ParseIP("1.2.3.11")))), }, + { // ipv6 + res.HandleMesos, + Message( + Question("toy-store.ipv6-framework.mesos.", dns.TypeAAAA), + Header(true, dns.RcodeSuccess), + Answers( + AAAA(RRHeader("toy-store.ipv6-framework.mesos.", dns.TypeAAAA, 60), + net.ParseIP("fd01:b::1:8000:2")))), + }, { res.HandleMesos, Message( @@ -212,7 +221,7 @@ func runHandlers() error { res.HandleMesos, Message( Question("missing.mesos.", dns.TypeAAAA), - Header(true, dns.RcodeSuccess), + Header(true, dns.RcodeNameError), NSs( SOA(RRHeader("missing.mesos.", dns.TypeSOA, 60), "ns1.mesos", "root.ns1.mesos", 60))), @@ -386,7 +395,7 @@ func fakeDNS() (*Resolver, error) { config := records.NewConfig() config.Masters = []string{"144.76.157.37:5050"} config.RecurseOn = false - config.IPSources = []string{"docker", "mesos", "host"} + config.IPSources = []string{"netinfo", "docker", "mesos", "host"} res := New("", config) res.rng.Seed(0) // for deterministic tests diff --git a/urls/urls.go b/urls/urls.go index d30200e7..eb43eeab 100644 --- a/urls/urls.go +++ b/urls/urls.go @@ -2,6 +2,7 @@ package urls import ( "fmt" + "net" "net/url" "strings" ) @@ -37,6 +38,10 @@ func Path(path string) Option { return func(b *Builder) { b.Path = path } } // zk://username:password@host1:port1,host2:port2,.../path // file:///path/to/file (where file contains one of the above) func SplitHostPort(pair string) (string, string, error) { + if host, port, err := net.SplitHostPort(pair); err == nil { + return host, port, nil + } + h := strings.SplitN(pair, ":", 2) if len(h) != 2 { return "", "", fmt.Errorf("unable to parse proto from %q", pair)