Add peering `.service` and `.node` DNS lookups. (#15596)
Add peering `.service` and `.node` DNS lookups.
This commit is contained in:
parent
a070840dc7
commit
79bef1982f
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
dns: Add support for cluster peering `.service` and `.node` DNS queries.
|
||||||
|
```
|
128
agent/dns.go
128
agent/dns.go
|
@ -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)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue