Add peering `.service` and `.node` DNS lookups. (#15596)

Add peering `.service` and `.node` DNS lookups.
This commit is contained in:
Derek Menteer 2022-11-29 12:23:18 -06:00 committed by GitHub
parent a070840dc7
commit 79bef1982f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 257 additions and 37 deletions

3
.changelog/15596.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
dns: Add support for cluster peering `.service` and `.node` DNS queries.
```

View File

@ -105,6 +105,7 @@ type dnsConfig struct {
} }
type serviceLookup struct { type serviceLookup struct {
PeerName string
Datacenter string Datacenter string
Service string Service string
Tag string Tag string
@ -116,6 +117,7 @@ type serviceLookup struct {
type nodeLookup struct { type nodeLookup struct {
Datacenter string Datacenter string
PeerName string
Node string Node string
Tag string Tag string
MaxRecursionLevel int 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. // server side to avoid transferring the entire node list.
if err := d.agent.RPC("Catalog.ListNodes", &args, &out); err == nil { if err := d.agent.RPC("Catalog.ListNodes", &args, &out); err == nil {
for _, n := range out.Nodes { 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) arpa, _ := dns.ReverseAddr(n.Address)
if arpa == qName { if arpa == qName {
ptr := &dns.PTR{ ptr := &dns.PTR{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, 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) m.Answer = append(m.Answer, ptr)
break break
@ -685,8 +694,16 @@ type queryLocality struct {
// Example query: <service>.virtual.<namespace>.ns.<partition>.ap.<datacenter>.dc.consul // Example query: <service>.virtual.<namespace>.ns.<partition>.ap.<datacenter>.dc.consul
datacenter string 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: <service>.virtual.<namespace>.ns.<peer>.peer.<partition>.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: <service>.virtual.<peerOrDatacenter>.consul // Example query: <service>.virtual.<peerOrDatacenter>.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 peerOrDatacenter string
acl.EnterpriseMeta acl.EnterpriseMeta
@ -763,11 +780,17 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
lookup := serviceLookup{ lookup := serviceLookup{
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
PeerName: locality.peer,
Connect: false, Connect: false,
Ingress: false, Ingress: false,
MaxRecursionLevel: maxRecursionLevel, MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: locality.EnterpriseMeta, EnterpriseMeta: locality.EnterpriseMeta,
} }
// Only one of dc or peer can be used.
if lookup.PeerName != "" {
lookup.Datacenter = ""
}
// Support RFC 2782 style syntax // Support RFC 2782 style syntax
if n == 2 && strings.HasPrefix(queryParts[1], "_") && strings.HasPrefix(queryParts[0], "_") { 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() 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{ lookup := serviceLookup{
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
Service: queryParts[len(queryParts)-1], 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 // 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 // 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. // within a DC, therefore their uniqueness is not guaranteed globally.
PeerName: locality.peerOrDatacenter, PeerName: locality.peer,
ServiceName: queryParts[len(queryParts)-1], ServiceName: queryParts[len(queryParts)-1],
EnterpriseMeta: locality.EnterpriseMeta, EnterpriseMeta: locality.EnterpriseMeta,
QueryOptions: structs.QueryOptions{ QueryOptions: structs.QueryOptions{
Token: d.agent.tokens.UserToken(), 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 var out string
if err := d.agent.RPC("Catalog.VirtualIPForService", &args, &out); err != nil { if err := d.agent.RPC("Catalog.VirtualIPForService", &args, &out); err != nil {
return err return err
@ -868,6 +899,8 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid() 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{ lookup := serviceLookup{
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
Service: queryParts[len(queryParts)-1], Service: queryParts[len(queryParts)-1],
@ -900,10 +933,15 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
lookup := nodeLookup{ lookup := nodeLookup{
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter), Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
PeerName: locality.peer,
Node: node, Node: node,
MaxRecursionLevel: maxRecursionLevel, MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: locality.EnterpriseMeta, EnterpriseMeta: locality.EnterpriseMeta,
} }
// Only one of dc or peer can be used.
if lookup.PeerName != "" {
lookup.Datacenter = ""
}
return d.nodeLookup(cfg, lookup, req, resp) 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 // Make an RPC request
args := &structs.NodeSpecificRequest{ args := &structs.NodeSpecificRequest{
Datacenter: lookup.Datacenter, Datacenter: lookup.Datacenter,
PeerName: lookup.PeerName,
Node: lookup.Node, Node: lookup.Node,
QueryOptions: structs.QueryOptions{ QueryOptions: structs.QueryOptions{
Token: d.agent.tokens.UserToken(), Token: d.agent.tokens.UserToken(),
@ -1366,6 +1405,7 @@ func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, lookup serviceLookup) (st
serviceTags = []string{lookup.Tag} serviceTags = []string{lookup.Tag}
} }
args := structs.ServiceSpecificRequest{ args := structs.ServiceSpecificRequest{
PeerName: lookup.PeerName,
Connect: lookup.Connect, Connect: lookup.Connect,
Ingress: lookup.Ingress, Ingress: lookup.Ingress,
Datacenter: lookup.Datacenter, Datacenter: lookup.Datacenter,
@ -1416,9 +1456,9 @@ func (d *DNSServer) serviceLookup(cfg *dnsConfig, lookup serviceLookup, req, res
// Add various responses depending on the request // Add various responses depending on the request
qType := req.Question[0].Qtype qType := req.Question[0].Qtype
if qType == dns.TypeSRV { 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 { } 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 { 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. // Add various responses depending on the request.
qType := req.Question[0].Qtype 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 { 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 { } 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 { if len(resp.Answer) == 0 {
@ -1575,7 +1619,7 @@ RPC:
} }
// serviceNodeRecords is used to add the node records for a service lookup // 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{}) handled := make(map[string]struct{})
var answerCNAME []dns.RR = nil var answerCNAME []dns.RR = nil
@ -1583,7 +1627,7 @@ func (d *DNSServer) serviceNodeRecords(cfg *dnsConfig, dc string, nodes structs.
for _, node := range nodes { for _, node := range nodes {
// Add the node record // Add the node record
had_answer := false 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 { if len(records) == 0 {
continue 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() ipv4 := ip.To4()
respDomain := d.getResponseDomain(questionName) respDomain := d.getResponseDomain(questionName)
if ipv4 != nil {
ipStr := hex.EncodeToString(ip) ipStr := hex.EncodeToString(ip)
return fmt.Sprintf("%s.addr.%s.%s", ipStr[len(ipStr)-(net.IPv4len*2):], dc, respDomain) if ipv4 != nil {
} else { ipStr = ipStr[len(ipStr)-(net.IPv4len*2):]
return fmt.Sprintf("%s.addr.%s.%s", hex.EncodeToString(ip), dc, respDomain)
} }
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 { 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 // 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 // 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 // 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] q := req.Question[0]
respDomain := d.getResponseDomain(q.Name)
ipRecord := makeARecord(q.Qtype, addr, ttl) ipRecord := makeARecord(q.Qtype, addr, ttl)
if ipRecord == nil { if ipRecord == nil {
return nil, nil return nil, nil
} }
if q.Qtype == dns.TypeSRV { 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{ answers := []dns.RR{
&dns.SRV{ &dns.SRV{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
@ -1773,7 +1822,7 @@ func (d *DNSServer) makeRecordFromServiceNode(dc string, serviceNode structs.Che
}, },
Priority: 1, Priority: 1,
Weight: uint16(findWeight(serviceNode)), 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, Target: nodeFQDN,
}, },
} }
@ -1789,7 +1838,7 @@ func (d *DNSServer) makeRecordFromServiceNode(dc string, serviceNode structs.Che
// Craft dns records for an IP // 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 // 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 // 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] q := req.Question[0]
ipRecord := makeARecord(q.Qtype, addr, ttl) ipRecord := makeARecord(q.Qtype, addr, ttl)
if ipRecord == nil { if ipRecord == nil {
@ -1797,7 +1846,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs
} }
if q.Qtype == dns.TypeSRV { if q.Qtype == dns.TypeSRV {
ipFQDN := d.encodeIPAsFqdn(q.Name, dc, addr) ipFQDN := d.encodeIPAsFqdn(q.Name, lookup, addr)
answers := []dns.RR{ answers := []dns.RR{
&dns.SRV{ &dns.SRV{
Hdr: dns.RR_Header{ Hdr: dns.RR_Header{
@ -1808,7 +1857,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs
}, },
Priority: 1, Priority: 1,
Weight: uint16(findWeight(serviceNode)), 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, Target: ipFQDN,
}, },
} }
@ -1824,7 +1873,7 @@ func (d *DNSServer) makeRecordFromIP(dc string, addr net.IP, serviceNode structs
// Craft dns records for an FQDN // 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 // 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 // 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 edns := req.IsEdns0() != nil
q := req.Question[0] q := req.Question[0]
@ -1857,7 +1906,7 @@ MORE_REC:
}, },
Priority: 1, Priority: 1,
Weight: uint16(findWeight(serviceNode)), 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), Target: dns.Fqdn(fqdn),
}, },
} }
@ -1879,7 +1928,7 @@ MORE_REC:
return answers, nil 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 addrTranslate := TranslateAddressAcceptDomain
if req.Question[0].Qtype == dns.TypeA { if req.Question[0].Qtype == dns.TypeA {
addrTranslate |= TranslateAddressAcceptIPv4 addrTranslate |= TranslateAddressAcceptIPv4
@ -1889,7 +1938,9 @@ func (d *DNSServer) nodeServiceRecords(dc string, node structs.CheckServiceNode,
addrTranslate |= TranslateAddressAcceptAny 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) nodeAddr := d.agent.TranslateAddress(node.Node.Datacenter, node.Node.Address, node.Node.TaggedAddresses, addrTranslate)
if serviceAddr == "" && nodeAddr == "" { if serviceAddr == "" && nodeAddr == "" {
return nil, nil return nil, nil
@ -1902,30 +1953,30 @@ func (d *DNSServer) nodeServiceRecords(dc string, node structs.CheckServiceNode,
if serviceAddr == "" && nodeIPAddr != nil { if serviceAddr == "" && nodeIPAddr != nil {
if node.Node.Address != nodeAddr { if node.Node.Address != nodeAddr {
// Do not CNAME node address in case of WAN address // 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) // There is no service address and the node address is a FQDN (external service)
if serviceAddr == "" { 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 // The service address is an IP
if serviceIPAddr != nil { 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 // If the service address is a CNAME for the service we are looking
// for then use the node address. // for then use the node address.
if dns.Fqdn(serviceAddr) == req.Question[0].Name && nodeIPAddr != nil { 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) // 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 { 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 // 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{}) handled := make(map[string]struct{})
for _, node := range nodes { for _, node := range nodes {
// Avoid duplicate entries, possible if a node has // Avoid duplicate entries, possible if a node has
// the same service the same port, etc. // 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) tuple := fmt.Sprintf("%s:%s:%d", node.Node.Node, serviceAddress, servicePort)
if _, ok := handled[tuple]; ok { if _, ok := handled[tuple]; ok {
continue continue
} }
handled[tuple] = struct{}{} 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) respDomain := d.getResponseDomain(req.Question[0].Name)
resp.Answer = append(resp.Answer, answers...) resp.Answer = append(resp.Answer, answers...)
resp.Extra = append(resp.Extra, extra...) resp.Extra = append(resp.Extra, extra...)
if cfg.NodeMetaTXT { 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)...)
} }
} }
} }

View File

@ -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 // 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. // we parse a "peerOrDatacenter". The caller or RPC handler are responsible for disambiguating.
func (d *DNSServer) parseLocality(labels []string, cfg *dnsConfig) (queryLocality, bool) { func (d *DNSServer) parseLocality(labels []string, cfg *dnsConfig) (queryLocality, bool) {
locality := queryLocality{
EnterpriseMeta: d.defaultEnterpriseMeta,
}
switch len(labels) { switch len(labels) {
case 2, 4:
// Support the following formats:
// - [.<datacenter>.dc]
// - [.<peer>.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: case 1:
return queryLocality{peerOrDatacenter: labels[0]}, true 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 { func serviceCanonicalDNSName(name, kind, datacenter, domain string, _ *acl.EnterpriseMeta) string {
return fmt.Sprintf("%s.%s.%s.%s", name, kind, datacenter, domain) 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)
}

127
agent/dns_oss_test.go Normal file
View File

@ -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")
})
}