diff --git a/.changelog/15596.txt b/.changelog/15596.txt new file mode 100644 index 000000000..cab67c613 --- /dev/null +++ b/.changelog/15596.txt @@ -0,0 +1,3 @@ +```release-note:improvement +dns: Add support for cluster peering `.service` and `.node` DNS queries. +``` diff --git a/agent/dns.go b/agent/dns.go index b671cf549..b35f80c63 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -105,6 +105,7 @@ type dnsConfig struct { } type serviceLookup struct { + PeerName string Datacenter string Service string Tag string @@ -116,6 +117,7 @@ type serviceLookup struct { type nodeLookup struct { Datacenter string + PeerName string Node string Tag string MaxRecursionLevel int @@ -421,11 +423,18 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { // server side to avoid transferring the entire node list. if err := d.agent.RPC("Catalog.ListNodes", &args, &out); err == nil { for _, n := range out.Nodes { + lookup := serviceLookup{ + // Peering PTR lookups are currently not supported, so we don't + // need to populate that field for creating the node FQDN. + // PeerName: n.PeerName, + Datacenter: n.Datacenter, + EnterpriseMeta: *n.GetEnterpriseMeta(), + } arpa, _ := dns.ReverseAddr(n.Address) if arpa == qName { ptr := &dns.PTR{ Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, - Ptr: fmt.Sprintf("%s.node.%s.%s", n.Node, datacenter, d.domain), + Ptr: nodeCanonicalDNSName(lookup, n.Node, d.domain), } m.Answer = append(m.Answer, ptr) break @@ -685,8 +694,16 @@ type queryLocality struct { // Example query: .virtual..ns..ap..dc.consul datacenter string - // peerOrDatacenter is parsed from DNS queries where the datacenter and peer name are specified in the same query part. + // peer is the peer name parsed from a label that has explicit parts. + // Example query: .virtual..ns..peer..ap.consul + peer string + + // peerOrDatacenter is parsed from DNS queries where the datacenter and peer name are + // specified in the same query part. // Example query: .virtual..consul + // + // Note that this field should only be a "peer" for virtual queries, since virtual IPs should + // not be shared between datacenters. In all other cases, it should be considered a DC. peerOrDatacenter string acl.EnterpriseMeta @@ -763,11 +780,17 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi lookup := serviceLookup{ Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), + PeerName: locality.peer, Connect: false, Ingress: false, MaxRecursionLevel: maxRecursionLevel, EnterpriseMeta: locality.EnterpriseMeta, } + // Only one of dc or peer can be used. + if lookup.PeerName != "" { + lookup.Datacenter = "" + } + // Support RFC 2782 style syntax if n == 2 && strings.HasPrefix(queryParts[1], "_") && strings.HasPrefix(queryParts[0], "_") { @@ -808,6 +831,9 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi return invalid() } + // Peering is not currently supported for connect queries. + // Exposing this likely would not provide much value, since users would + // need to be very familiar with our TLS / SNI / mesh gateways to leverage it. lookup := serviceLookup{ Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Service: queryParts[len(queryParts)-1], @@ -833,13 +859,18 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi // The datacenter of the request is not specified because cross-datacenter virtual IP // queries are not supported. This guard rail is in place because virtual IPs are allocated // within a DC, therefore their uniqueness is not guaranteed globally. - PeerName: locality.peerOrDatacenter, + PeerName: locality.peer, ServiceName: queryParts[len(queryParts)-1], EnterpriseMeta: locality.EnterpriseMeta, QueryOptions: structs.QueryOptions{ Token: d.agent.tokens.UserToken(), }, } + if args.PeerName == "" { + // If the peer name was not explicitly defined, fall back to the ambiguously-parsed version. + args.PeerName = locality.peerOrDatacenter + } + var out string if err := d.agent.RPC("Catalog.VirtualIPForService", &args, &out); err != nil { return err @@ -868,6 +899,8 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi return invalid() } + // Peering is not currently supported for ingress queries. + // We probably should not be encouraging chained calls from ingress to peers anyway. lookup := serviceLookup{ Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Service: queryParts[len(queryParts)-1], @@ -900,10 +933,15 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi lookup := nodeLookup{ Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), + PeerName: locality.peer, Node: node, MaxRecursionLevel: maxRecursionLevel, EnterpriseMeta: locality.EnterpriseMeta, } + // Only one of dc or peer can be used. + if lookup.PeerName != "" { + lookup.Datacenter = "" + } return d.nodeLookup(cfg, lookup, req, resp) @@ -1040,6 +1078,7 @@ func (d *DNSServer) nodeLookup(cfg *dnsConfig, lookup nodeLookup, req, resp *dns // Make an RPC request args := &structs.NodeSpecificRequest{ Datacenter: lookup.Datacenter, + PeerName: lookup.PeerName, Node: lookup.Node, QueryOptions: structs.QueryOptions{ Token: d.agent.tokens.UserToken(), @@ -1366,6 +1405,7 @@ func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, lookup serviceLookup) (st serviceTags = []string{lookup.Tag} } args := structs.ServiceSpecificRequest{ + PeerName: lookup.PeerName, Connect: lookup.Connect, Ingress: lookup.Ingress, Datacenter: lookup.Datacenter, @@ -1416,9 +1456,9 @@ func (d *DNSServer) serviceLookup(cfg *dnsConfig, lookup serviceLookup, req, res // Add various responses depending on the request qType := req.Question[0].Qtype if qType == dns.TypeSRV { - d.serviceSRVRecords(cfg, lookup.Datacenter, out.Nodes, req, resp, ttl, lookup.MaxRecursionLevel) + d.serviceSRVRecords(cfg, lookup, out.Nodes, req, resp, ttl, lookup.MaxRecursionLevel) } else { - d.serviceNodeRecords(cfg, lookup.Datacenter, out.Nodes, req, resp, ttl, lookup.MaxRecursionLevel) + d.serviceNodeRecords(cfg, lookup, out.Nodes, req, resp, ttl, lookup.MaxRecursionLevel) } if len(resp.Answer) == 0 { @@ -1521,10 +1561,14 @@ func (d *DNSServer) preparedQueryLookup(cfg *dnsConfig, datacenter, query string // Add various responses depending on the request. qType := req.Question[0].Qtype + + // This serviceLookup only needs the datacenter field populated, + // because peering is not supported with prepared queries. + lookup := serviceLookup{Datacenter: out.Datacenter} if qType == dns.TypeSRV { - d.serviceSRVRecords(cfg, out.Datacenter, out.Nodes, req, resp, ttl, maxRecursionLevel) + d.serviceSRVRecords(cfg, lookup, out.Nodes, req, resp, ttl, maxRecursionLevel) } else { - d.serviceNodeRecords(cfg, out.Datacenter, out.Nodes, req, resp, ttl, maxRecursionLevel) + d.serviceNodeRecords(cfg, lookup, out.Nodes, req, resp, ttl, maxRecursionLevel) } if len(resp.Answer) == 0 { @@ -1575,7 +1619,7 @@ RPC: } // serviceNodeRecords is used to add the node records for a service lookup -func (d *DNSServer) serviceNodeRecords(cfg *dnsConfig, dc string, nodes structs.CheckServiceNodes, req, resp *dns.Msg, ttl time.Duration, maxRecursionLevel int) { +func (d *DNSServer) serviceNodeRecords(cfg *dnsConfig, lookup serviceLookup, nodes structs.CheckServiceNodes, req, resp *dns.Msg, ttl time.Duration, maxRecursionLevel int) { handled := make(map[string]struct{}) var answerCNAME []dns.RR = nil @@ -1583,7 +1627,7 @@ func (d *DNSServer) serviceNodeRecords(cfg *dnsConfig, dc string, nodes structs. for _, node := range nodes { // Add the node record had_answer := false - records, _ := d.nodeServiceRecords(dc, node, req, ttl, cfg, maxRecursionLevel) + records, _ := d.nodeServiceRecords(lookup, node, req, ttl, cfg, maxRecursionLevel) if len(records) == 0 { continue } @@ -1666,15 +1710,20 @@ func findWeight(node structs.CheckServiceNode) int { } } -func (d *DNSServer) encodeIPAsFqdn(questionName string, dc string, ip net.IP) string { +func (d *DNSServer) encodeIPAsFqdn(questionName string, lookup serviceLookup, ip net.IP) string { ipv4 := ip.To4() respDomain := d.getResponseDomain(questionName) + ipStr := hex.EncodeToString(ip) if ipv4 != nil { - ipStr := hex.EncodeToString(ip) - return fmt.Sprintf("%s.addr.%s.%s", ipStr[len(ipStr)-(net.IPv4len*2):], dc, respDomain) - } else { - return fmt.Sprintf("%s.addr.%s.%s", hex.EncodeToString(ip), dc, respDomain) + ipStr = ipStr[len(ipStr)-(net.IPv4len*2):] } + if lookup.PeerName != "" { + // Exclude the datacenter from the FQDN on the addr for peers. + // This technically makes no difference, since the addr endpoint ignores the DC + // component of the request, but do it anyway for a less confusing experience. + return fmt.Sprintf("%s.addr.%s", ipStr, respDomain) + } + return fmt.Sprintf("%s.addr.%s.%s", ipStr, lookup.Datacenter, respDomain) } func makeARecord(qType uint16, ip net.IP, ttl time.Duration) dns.RR { @@ -1753,16 +1802,16 @@ func (d *DNSServer) makeRecordFromNode(node *structs.Node, qType uint16, qName s // Craft dns records for a service // In case of an SRV query the answer will be a IN SRV and additional data will store an IN A to the node IP // Otherwise it will return a IN A record -func (d *DNSServer) makeRecordFromServiceNode(dc string, serviceNode structs.CheckServiceNode, addr net.IP, req *dns.Msg, ttl time.Duration) ([]dns.RR, []dns.RR) { +func (d *DNSServer) makeRecordFromServiceNode(lookup serviceLookup, serviceNode structs.CheckServiceNode, addr net.IP, req *dns.Msg, ttl time.Duration) ([]dns.RR, []dns.RR) { q := req.Question[0] - respDomain := d.getResponseDomain(q.Name) - ipRecord := makeARecord(q.Qtype, addr, ttl) if ipRecord == nil { return nil, nil } + if q.Qtype == dns.TypeSRV { - nodeFQDN := fmt.Sprintf("%s.node.%s.%s", serviceNode.Node.Node, dc, respDomain) + respDomain := d.getResponseDomain(q.Name) + nodeFQDN := nodeCanonicalDNSName(lookup, serviceNode.Node.Node, respDomain) answers := []dns.RR{ &dns.SRV{ Hdr: dns.RR_Header{ @@ -1773,7 +1822,7 @@ func (d *DNSServer) makeRecordFromServiceNode(dc string, serviceNode structs.Che }, Priority: 1, Weight: uint16(findWeight(serviceNode)), - Port: uint16(d.agent.TranslateServicePort(dc, serviceNode.Service.Port, serviceNode.Service.TaggedAddresses)), + Port: uint16(d.agent.TranslateServicePort(lookup.Datacenter, serviceNode.Service.Port, serviceNode.Service.TaggedAddresses)), Target: nodeFQDN, }, } @@ -1789,7 +1838,7 @@ func (d *DNSServer) makeRecordFromServiceNode(dc string, serviceNode structs.Che // Craft dns records for an IP // In case of an SRV query the answer will be a IN SRV and additional data will store an IN A to the IP // Otherwise it will return a IN A record -func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs.CheckServiceNode, req *dns.Msg, ttl time.Duration) ([]dns.RR, []dns.RR) { +func (d *DNSServer) makeRecordFromIP(lookup serviceLookup, addr net.IP, serviceNode structs.CheckServiceNode, req *dns.Msg, ttl time.Duration) ([]dns.RR, []dns.RR) { q := req.Question[0] ipRecord := makeARecord(q.Qtype, addr, ttl) if ipRecord == nil { @@ -1797,7 +1846,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs } if q.Qtype == dns.TypeSRV { - ipFQDN := d.encodeIPAsFqdn(q.Name, dc, addr) + ipFQDN := d.encodeIPAsFqdn(q.Name, lookup, addr) answers := []dns.RR{ &dns.SRV{ Hdr: dns.RR_Header{ @@ -1808,7 +1857,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs }, Priority: 1, Weight: uint16(findWeight(serviceNode)), - Port: uint16(d.agent.TranslateServicePort(dc, serviceNode.Service.Port, serviceNode.Service.TaggedAddresses)), + Port: uint16(d.agent.TranslateServicePort(lookup.Datacenter, serviceNode.Service.Port, serviceNode.Service.TaggedAddresses)), Target: ipFQDN, }, } @@ -1824,7 +1873,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs // Craft dns records for an FQDN // In case of an SRV query the answer will be a IN SRV and additional data will store an IN A to the IP // Otherwise it will return a CNAME and a IN A record -func (d *DNSServer) makeRecordFromFQDN(dc string, fqdn string, serviceNode structs.CheckServiceNode, req *dns.Msg, ttl time.Duration, cfg *dnsConfig, maxRecursionLevel int) ([]dns.RR, []dns.RR) { +func (d *DNSServer) makeRecordFromFQDN(lookup serviceLookup, fqdn string, serviceNode structs.CheckServiceNode, req *dns.Msg, ttl time.Duration, cfg *dnsConfig, maxRecursionLevel int) ([]dns.RR, []dns.RR) { edns := req.IsEdns0() != nil q := req.Question[0] @@ -1857,7 +1906,7 @@ MORE_REC: }, Priority: 1, Weight: uint16(findWeight(serviceNode)), - Port: uint16(d.agent.TranslateServicePort(dc, serviceNode.Service.Port, serviceNode.Service.TaggedAddresses)), + Port: uint16(d.agent.TranslateServicePort(lookup.Datacenter, serviceNode.Service.Port, serviceNode.Service.TaggedAddresses)), Target: dns.Fqdn(fqdn), }, } @@ -1879,7 +1928,7 @@ MORE_REC: return answers, nil } -func (d *DNSServer) nodeServiceRecords(dc string, node structs.CheckServiceNode, req *dns.Msg, ttl time.Duration, cfg *dnsConfig, maxRecursionLevel int) ([]dns.RR, []dns.RR) { +func (d *DNSServer) nodeServiceRecords(lookup serviceLookup, node structs.CheckServiceNode, req *dns.Msg, ttl time.Duration, cfg *dnsConfig, maxRecursionLevel int) ([]dns.RR, []dns.RR) { addrTranslate := TranslateAddressAcceptDomain if req.Question[0].Qtype == dns.TypeA { addrTranslate |= TranslateAddressAcceptIPv4 @@ -1889,7 +1938,9 @@ func (d *DNSServer) nodeServiceRecords(dc string, node structs.CheckServiceNode, addrTranslate |= TranslateAddressAcceptAny } - serviceAddr := d.agent.TranslateServiceAddress(dc, node.Service.Address, node.Service.TaggedAddresses, addrTranslate) + // The datacenter should be empty during translation if it is a peering lookup. + // This should be fine because we should always prefer the WAN address. + serviceAddr := d.agent.TranslateServiceAddress(lookup.Datacenter, node.Service.Address, node.Service.TaggedAddresses, addrTranslate) nodeAddr := d.agent.TranslateAddress(node.Node.Datacenter, node.Node.Address, node.Node.TaggedAddresses, addrTranslate) if serviceAddr == "" && nodeAddr == "" { return nil, nil @@ -1902,30 +1953,30 @@ func (d *DNSServer) nodeServiceRecords(dc string, node structs.CheckServiceNode, if serviceAddr == "" && nodeIPAddr != nil { if node.Node.Address != nodeAddr { // Do not CNAME node address in case of WAN address - return d.makeRecordFromIP(dc, nodeIPAddr, node, req, ttl) + return d.makeRecordFromIP(lookup, nodeIPAddr, node, req, ttl) } - return d.makeRecordFromServiceNode(dc, node, nodeIPAddr, req, ttl) + return d.makeRecordFromServiceNode(lookup, node, nodeIPAddr, req, ttl) } // There is no service address and the node address is a FQDN (external service) if serviceAddr == "" { - return d.makeRecordFromFQDN(dc, nodeAddr, node, req, ttl, cfg, maxRecursionLevel) + return d.makeRecordFromFQDN(lookup, nodeAddr, node, req, ttl, cfg, maxRecursionLevel) } // The service address is an IP if serviceIPAddr != nil { - return d.makeRecordFromIP(dc, serviceIPAddr, node, req, ttl) + return d.makeRecordFromIP(lookup, serviceIPAddr, node, req, ttl) } // If the service address is a CNAME for the service we are looking // for then use the node address. if dns.Fqdn(serviceAddr) == req.Question[0].Name && nodeIPAddr != nil { - return d.makeRecordFromServiceNode(dc, node, nodeIPAddr, req, ttl) + return d.makeRecordFromServiceNode(lookup, node, nodeIPAddr, req, ttl) } // The service address is a FQDN (external service) - return d.makeRecordFromFQDN(dc, serviceAddr, node, req, ttl, cfg, maxRecursionLevel) + return d.makeRecordFromFQDN(lookup, serviceAddr, node, req, ttl, cfg, maxRecursionLevel) } func (d *DNSServer) generateMeta(qName string, node *structs.Node, ttl time.Duration) []dns.RR { @@ -1950,28 +2001,31 @@ func (d *DNSServer) generateMeta(qName string, node *structs.Node, ttl time.Dura } // serviceARecords is used to add the SRV records for a service lookup -func (d *DNSServer) serviceSRVRecords(cfg *dnsConfig, dc string, nodes structs.CheckServiceNodes, req, resp *dns.Msg, ttl time.Duration, maxRecursionLevel int) { +func (d *DNSServer) serviceSRVRecords(cfg *dnsConfig, lookup serviceLookup, nodes structs.CheckServiceNodes, req, resp *dns.Msg, ttl time.Duration, maxRecursionLevel int) { handled := make(map[string]struct{}) for _, node := range nodes { // Avoid duplicate entries, possible if a node has // the same service the same port, etc. - serviceAddress := d.agent.TranslateServiceAddress(dc, node.Service.Address, node.Service.TaggedAddresses, TranslateAddressAcceptAny) - servicePort := d.agent.TranslateServicePort(dc, node.Service.Port, node.Service.TaggedAddresses) + + // The datacenter should be empty during translation if it is a peering lookup. + // This should be fine because we should always prefer the WAN address. + serviceAddress := d.agent.TranslateServiceAddress(lookup.Datacenter, node.Service.Address, node.Service.TaggedAddresses, TranslateAddressAcceptAny) + servicePort := d.agent.TranslateServicePort(lookup.Datacenter, node.Service.Port, node.Service.TaggedAddresses) tuple := fmt.Sprintf("%s:%s:%d", node.Node.Node, serviceAddress, servicePort) if _, ok := handled[tuple]; ok { continue } handled[tuple] = struct{}{} - answers, extra := d.nodeServiceRecords(dc, node, req, ttl, cfg, maxRecursionLevel) + answers, extra := d.nodeServiceRecords(lookup, node, req, ttl, cfg, maxRecursionLevel) respDomain := d.getResponseDomain(req.Question[0].Name) resp.Answer = append(resp.Answer, answers...) resp.Extra = append(resp.Extra, extra...) if cfg.NodeMetaTXT { - resp.Extra = append(resp.Extra, d.generateMeta(fmt.Sprintf("%s.node.%s.%s", node.Node.Node, dc, respDomain), node.Node, ttl)...) + resp.Extra = append(resp.Extra, d.generateMeta(nodeCanonicalDNSName(lookup, node.Node.Node, respDomain), node.Node, ttl)...) } } } diff --git a/agent/dns_oss.go b/agent/dns_oss.go index de257aa4c..6ccbe2f8d 100644 --- a/agent/dns_oss.go +++ b/agent/dns_oss.go @@ -20,7 +20,30 @@ func getEnterpriseDNSConfig(conf *config.RuntimeConfig) enterpriseDNSConfig { // Peer name is parsed from the same query part that datacenter is, so given this ambiguity // we parse a "peerOrDatacenter". The caller or RPC handler are responsible for disambiguating. func (d *DNSServer) parseLocality(labels []string, cfg *dnsConfig) (queryLocality, bool) { + locality := queryLocality{ + EnterpriseMeta: d.defaultEnterpriseMeta, + } + switch len(labels) { + case 2, 4: + // Support the following formats: + // - [..dc] + // - [..peer] + for i := 0; i < len(labels); i += 2 { + switch labels[i+1] { + case "dc": + locality.datacenter = labels[i] + case "peer": + locality.peer = labels[i] + default: + return queryLocality{}, false + } + } + // Return error when both datacenter and peer are specified. + if locality.datacenter != "" && locality.peer != "" { + return queryLocality{}, false + } + return locality, true case 1: return queryLocality{peerOrDatacenter: labels[0]}, true @@ -34,3 +57,16 @@ func (d *DNSServer) parseLocality(labels []string, cfg *dnsConfig) (queryLocalit func serviceCanonicalDNSName(name, kind, datacenter, domain string, _ *acl.EnterpriseMeta) string { return fmt.Sprintf("%s.%s.%s.%s", name, kind, datacenter, domain) } + +func nodeCanonicalDNSName(lookup serviceLookup, nodeName, respDomain string) string { + if lookup.PeerName != "" { + // We must return a more-specific DNS name for peering so + // that there is no ambiguity with lookups. + return fmt.Sprintf("%s.node.%s.peer.%s", + nodeName, + lookup.PeerName, + respDomain) + } + // Return a simpler format for non-peering nodes. + return fmt.Sprintf("%s.node.%s.%s", nodeName, lookup.Datacenter, respDomain) +} diff --git a/agent/dns_oss_test.go b/agent/dns_oss_test.go new file mode 100644 index 000000000..a394aa871 --- /dev/null +++ b/agent/dns_oss_test.go @@ -0,0 +1,127 @@ +//go:build !consulent +// +build !consulent + +package agent + +import ( + "testing" + + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/agent/structs" + "github.com/hashicorp/consul/testrpc" + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestDNS_OSS_PeeredServices(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + a := StartTestAgent(t, TestAgent{HCL: ``, Overrides: `peering = { test_allow_peer_registrations = true }`}) + defer a.Shutdown() + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + makeReq := func() *structs.RegisterRequest { + return &structs.RegisterRequest{ + PeerName: "peer1", + Datacenter: "dc1", + Node: "peernode1", + Address: "198.18.1.1", + Service: &structs.NodeService{ + PeerName: "peer1", + Kind: structs.ServiceKindConnectProxy, + Service: "web-proxy", + Address: "199.0.0.1", + Port: 12345, + Proxy: structs.ConnectProxyConfig{ + DestinationServiceName: "peer-web", + }, + EnterpriseMeta: *acl.DefaultEnterpriseMeta(), + }, + EnterpriseMeta: *acl.DefaultEnterpriseMeta(), + } + } + + dnsQuery := func(t *testing.T, question string, typ uint16) *dns.Msg { + m := new(dns.Msg) + m.SetQuestion(question, typ) + + c := new(dns.Client) + reply, _, err := c.Exchange(m, a.DNSAddr()) + require.NoError(t, err) + require.Len(t, reply.Answer, 1, "zero valid records found for %q", question) + return reply + } + + assertARec := func(t *testing.T, rec dns.RR, expectName, expectIP string) { + aRec, ok := rec.(*dns.A) + require.True(t, ok, "Extra is not an A record: %T", rec) + require.Equal(t, expectName, aRec.Hdr.Name) + require.Equal(t, expectIP, aRec.A.String()) + } + + assertSRVRec := func(t *testing.T, rec dns.RR, expectName string, expectPort uint16) { + srvRec, ok := rec.(*dns.SRV) + require.True(t, ok, "Answer is not a SRV record: %T", rec) + require.Equal(t, expectName, srvRec.Target) + require.Equal(t, expectPort, srvRec.Port) + } + + t.Run("srv-with-addr-reply", func(t *testing.T) { + require.NoError(t, a.RPC("Catalog.Register", makeReq(), &struct{}{})) + q := dnsQuery(t, "web-proxy.service.peer1.peer.consul.", dns.TypeSRV) + require.Len(t, q.Answer, 1) + require.Len(t, q.Extra, 1) + + addr := "c7000001.addr.consul." + assertSRVRec(t, q.Answer[0], addr, 12345) + assertARec(t, q.Extra[0], addr, "199.0.0.1") + + // Query the addr to make sure it's also valid. + q = dnsQuery(t, addr, dns.TypeA) + require.Len(t, q.Answer, 1) + require.Len(t, q.Extra, 0) + assertARec(t, q.Answer[0], addr, "199.0.0.1") + }) + + t.Run("srv-with-node-reply", func(t *testing.T) { + req := makeReq() + // Clear service address to trigger node response + req.Service.Address = "" + require.NoError(t, a.RPC("Catalog.Register", req, &struct{}{})) + q := dnsQuery(t, "web-proxy.service.peer1.peer.consul.", dns.TypeSRV) + require.Len(t, q.Answer, 1) + require.Len(t, q.Extra, 1) + + nodeName := "peernode1.node.peer1.peer.consul." + assertSRVRec(t, q.Answer[0], nodeName, 12345) + assertARec(t, q.Extra[0], nodeName, "198.18.1.1") + + // Query the node to make sure it's also valid. + q = dnsQuery(t, nodeName, dns.TypeA) + require.Len(t, q.Answer, 1) + require.Len(t, q.Extra, 0) + assertARec(t, q.Answer[0], nodeName, "198.18.1.1") + }) + + t.Run("srv-with-fqdn-reply", func(t *testing.T) { + req := makeReq() + // Set non-ip address to trigger external response + req.Address = "localhost" + req.Service.Address = "" + require.NoError(t, a.RPC("Catalog.Register", req, &struct{}{})) + q := dnsQuery(t, "web-proxy.service.peer1.peer.consul.", dns.TypeSRV) + require.Len(t, q.Answer, 1) + require.Len(t, q.Extra, 0) + assertSRVRec(t, q.Answer[0], "localhost.", 12345) + }) + + t.Run("a-reply", func(t *testing.T) { + require.NoError(t, a.RPC("Catalog.Register", makeReq(), &struct{}{})) + q := dnsQuery(t, "web-proxy.service.peer1.peer.consul.", dns.TypeA) + require.Len(t, q.Answer, 1) + require.Len(t, q.Extra, 0) + assertARec(t, q.Answer[0], "web-proxy.service.peer1.peer.consul.", "199.0.0.1") + }) +}