2024-03-01 15:36:37 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package dns
import (
"encoding/hex"
"fmt"
"net"
"strings"
"time"
"github.com/miekg/dns"
"github.com/hashicorp/consul/agent/discovery"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/internal/dnsutil"
)
// messageSerializer is the high level orchestrator for generating the Answer,
// Extra, and Ns records for a DNS response.
type messageSerializer struct { }
// serializeOptions are the options for serializing a discovery.Result into a DNS message.
type serializeOptions struct {
req * dns . Msg
reqCtx Context
query * discovery . Query
results [ ] * discovery . Result
resp * dns . Msg
cfg * RouterDynamicConfig
responseDomain string
remoteAddress net . Addr
maxRecursionLevel int
dnsRecordMaker dnsRecordMaker
translateAddressFunc func ( dc string , addr string , taggedAddresses map [ string ] string , accept dnsutil . TranslateAddressAccept ) string
translateServiceAddressFunc func ( dc string , address string , taggedAddresses map [ string ] structs . ServiceAddress , accept dnsutil . TranslateAddressAccept ) string
resolveCnameFunc func ( cfgContext * RouterDynamicConfig , name string , reqCtx Context , remoteAddress net . Addr , maxRecursionLevel int ) [ ] dns . RR
}
// serializeQueryResults converts a discovery.Result into a DNS message.
func ( d messageSerializer ) serialize ( opts * serializeOptions ) ( * dns . Msg , error ) {
resp := new ( dns . Msg )
resp . SetReply ( opts . req )
resp . Compress = ! opts . cfg . DisableCompression
resp . Authoritative = true
resp . RecursionAvailable = canRecurse ( opts . cfg )
opts . resp = resp
qType := opts . req . Question [ 0 ] . Qtype
reqType := parseRequestType ( opts . req )
// Always add the SOA record if requested.
if qType == dns . TypeSOA {
resp . Answer = append ( resp . Answer , opts . dnsRecordMaker . makeSOA ( opts . responseDomain , opts . cfg ) )
}
switch {
case qType == dns . TypeSOA , reqType == requestTypeAddress :
for _ , result := range opts . results {
for _ , port := range getPortsFromResult ( result ) {
ans , ex , ns := d . getAnswerExtraAndNs ( serializeToGetAnswerExtraAndNsOptions ( opts , result , port ) )
resp . Answer = append ( resp . Answer , ans ... )
resp . Extra = append ( resp . Extra , ex ... )
resp . Ns = append ( resp . Ns , ns ... )
}
}
case qType == dns . TypeSRV :
handled := make ( map [ string ] struct { } )
for _ , result := range opts . results {
for _ , port := range getPortsFromResult ( result ) {
// Avoid duplicate entries, possible if a node has
// the same service the same port, etc.
// 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.
address := ""
if result . Service != nil {
address = result . Service . Address
} else {
address = result . Node . Address
}
tuple := fmt . Sprintf ( "%s:%s:%d" , result . Node . Name , address , port . Number )
if _ , ok := handled [ tuple ] ; ok {
continue
}
handled [ tuple ] = struct { } { }
ans , ex , ns := d . getAnswerExtraAndNs ( serializeToGetAnswerExtraAndNsOptions ( opts , result , port ) )
resp . Answer = append ( resp . Answer , ans ... )
resp . Extra = append ( resp . Extra , ex ... )
resp . Ns = append ( resp . Ns , ns ... )
}
}
default :
// default will send it to where it does some de-duping while it calls getAnswerExtraAndNs and recurses.
d . appendResultsToDNSResponse ( opts )
}
if opts . query != nil && opts . query . QueryType != discovery . QueryTypeVirtual &&
len ( resp . Answer ) == 0 && len ( resp . Extra ) == 0 {
return nil , discovery . ErrNoData
}
return resp , nil
}
// appendResultsToDNSResponse builds dns message from the discovery results and
// appends them to the dns response.
func ( d messageSerializer ) appendResultsToDNSResponse ( opts * serializeOptions ) {
// Always add the SOA record if requested.
if opts . req . Question [ 0 ] . Qtype == dns . TypeSOA {
opts . resp . Answer = append ( opts . resp . Answer , opts . dnsRecordMaker . makeSOA ( opts . responseDomain , opts . cfg ) )
}
handled := make ( map [ string ] struct { } )
var answerCNAME [ ] dns . RR = nil
count := 0
for _ , result := range opts . results {
for _ , port := range getPortsFromResult ( result ) {
// Add the node record
had_answer := false
ans , extra , _ := d . getAnswerExtraAndNs ( serializeToGetAnswerExtraAndNsOptions ( opts , result , port ) )
opts . resp . Extra = append ( opts . resp . Extra , extra ... )
if len ( ans ) == 0 {
continue
}
// Avoid duplicate entries, possible if a node has
// the same service on multiple ports, etc.
if _ , ok := handled [ ans [ 0 ] . String ( ) ] ; ok {
continue
}
handled [ ans [ 0 ] . String ( ) ] = struct { } { }
switch ans [ 0 ] . ( type ) {
case * dns . CNAME :
// keep track of the first CNAME + associated RRs but don't add to the resp.Answer yet
// this will only be added if no non-CNAME RRs are found
if len ( answerCNAME ) == 0 {
answerCNAME = ans
}
default :
opts . resp . Answer = append ( opts . resp . Answer , ans ... )
had_answer = true
}
if had_answer {
count ++
if count == opts . cfg . ARecordLimit {
// We stop only if greater than 0 or we reached the limit
return
}
}
}
}
if len ( opts . resp . Answer ) == 0 && len ( answerCNAME ) > 0 {
opts . resp . Answer = answerCNAME
}
}
// getAnswerExtraAndNsOptions are the options for getting the Answer, Extra, and Ns records for a DNS response.
type getAnswerExtraAndNsOptions struct {
port discovery . Port
result * discovery . Result
req * dns . Msg
reqCtx Context
query * discovery . Query
results [ ] * discovery . Result
resp * dns . Msg
cfg * RouterDynamicConfig
responseDomain string
remoteAddress net . Addr
maxRecursionLevel int
ttl uint32
dnsRecordMaker dnsRecordMaker
translateAddressFunc func ( dc string , addr string , taggedAddresses map [ string ] string , accept dnsutil . TranslateAddressAccept ) string
translateServiceAddressFunc func ( dc string , address string , taggedAddresses map [ string ] structs . ServiceAddress , accept dnsutil . TranslateAddressAccept ) string
resolveCnameFunc func ( cfgContext * RouterDynamicConfig , name string , reqCtx Context , remoteAddress net . Addr , maxRecursionLevel int ) [ ] dns . RR
}
// getAnswerAndExtra creates the dns answer and extra from discovery results.
func ( d messageSerializer ) getAnswerExtraAndNs ( opts * getAnswerExtraAndNsOptions ) ( answer [ ] dns . RR , extra [ ] dns . RR , ns [ ] dns . RR ) {
serviceAddress , nodeAddress := d . getServiceAndNodeAddresses ( opts )
qName := opts . req . Question [ 0 ] . Name
ttlLookupName := qName
if opts . query != nil {
ttlLookupName = opts . query . QueryPayload . Name
}
opts . ttl = getTTLForResult ( ttlLookupName , opts . result . DNS . TTL , opts . query , opts . cfg )
qType := opts . req . Question [ 0 ] . Qtype
// TODO (v2-dns): skip records that refer to a workload/node that don't have a valid DNS name.
// Special case responses
switch {
// PTR requests are first since they are a special case of domain overriding question type
case parseRequestType ( opts . req ) == requestTypeIP :
ptrTarget := ""
if opts . result . Type == discovery . ResultTypeNode {
ptrTarget = opts . result . Node . Name
} else if opts . result . Type == discovery . ResultTypeService {
ptrTarget = opts . result . Service . Name
}
ptr := & dns . PTR {
Hdr : dns . RR_Header { Name : qName , Rrtype : dns . TypePTR , Class : dns . ClassINET , Ttl : 0 } ,
Ptr : canonicalNameForResult ( opts . result . Type , ptrTarget , opts . responseDomain , opts . result . Tenancy , opts . port . Name ) ,
}
answer = append ( answer , ptr )
case qType == dns . TypeNS :
resultType := opts . result . Type
target := opts . result . Node . Name
if parseRequestType ( opts . req ) == requestTypeConsul && resultType == discovery . ResultTypeService {
resultType = discovery . ResultTypeNode
}
fqdn := canonicalNameForResult ( resultType , target , opts . responseDomain , opts . result . Tenancy , opts . port . Name )
extraRecord := opts . dnsRecordMaker . makeIPBasedRecord ( fqdn , nodeAddress , opts . ttl )
answer = append ( answer , opts . dnsRecordMaker . makeNS ( opts . responseDomain , fqdn , opts . ttl ) )
extra = append ( extra , extraRecord )
case qType == dns . TypeSOA :
// to be returned in the result.
fqdn := canonicalNameForResult ( opts . result . Type , opts . result . Node . Name , opts . responseDomain , opts . result . Tenancy , opts . port . Name )
extraRecord := opts . dnsRecordMaker . makeIPBasedRecord ( fqdn , nodeAddress , opts . ttl )
ns = append ( ns , opts . dnsRecordMaker . makeNS ( opts . responseDomain , fqdn , opts . ttl ) )
extra = append ( extra , extraRecord )
case qType == dns . TypeSRV :
2024-06-25 17:42:25 +00:00
fallthrough
2024-03-01 15:36:37 +00:00
default :
a , e := d . getAnswerExtrasForAddressAndTarget ( nodeAddress , serviceAddress , opts )
answer = append ( answer , a ... )
extra = append ( extra , e ... )
}
a , e := getAnswerAndExtraTXT ( opts . req , opts . cfg , qName , opts . result , opts . ttl ,
opts . responseDomain , opts . query , & opts . port , opts . dnsRecordMaker )
answer = append ( answer , a ... )
extra = append ( extra , e ... )
return
}
// getServiceAndNodeAddresses returns the service and node addresses from a discovery result.
func ( d messageSerializer ) getServiceAndNodeAddresses ( opts * getAnswerExtraAndNsOptions ) ( * dnsAddress , * dnsAddress ) {
addrTranslate := dnsutil . TranslateAddressAcceptDomain
if opts . req . Question [ 0 ] . Qtype == dns . TypeA {
addrTranslate |= dnsutil . TranslateAddressAcceptIPv4
} else if opts . req . Question [ 0 ] . Qtype == dns . TypeAAAA {
addrTranslate |= dnsutil . TranslateAddressAcceptIPv6
} else {
addrTranslate |= dnsutil . TranslateAddressAcceptAny
}
// 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 := newDNSAddress ( "" )
if opts . result . Service != nil {
sa := opts . translateServiceAddressFunc ( opts . result . Tenancy . Datacenter ,
opts . result . Service . Address , getServiceAddressMapFromLocationMap ( opts . result . Service . TaggedAddresses ) ,
addrTranslate )
serviceAddress = newDNSAddress ( sa )
}
nodeAddress := newDNSAddress ( "" )
if opts . result . Node != nil {
na := opts . translateAddressFunc ( opts . result . Tenancy . Datacenter , opts . result . Node . Address ,
getStringAddressMapFromTaggedAddressMap ( opts . result . Node . TaggedAddresses ) , addrTranslate )
nodeAddress = newDNSAddress ( na )
}
return serviceAddress , nodeAddress
}
// getAnswerExtrasForAddressAndTarget creates the dns answer and extra from nodeAddress and serviceAddress dnsAddress pairs.
func ( d messageSerializer ) getAnswerExtrasForAddressAndTarget ( nodeAddress * dnsAddress ,
serviceAddress * dnsAddress , opts * getAnswerExtraAndNsOptions ) ( answer [ ] dns . RR , extra [ ] dns . RR ) {
qName := opts . req . Question [ 0 ] . Name
reqType := parseRequestType ( opts . req )
switch {
case ( reqType == requestTypeAddress || opts . result . Type == discovery . ResultTypeVirtual ) &&
serviceAddress . IsEmptyString ( ) && nodeAddress . IsIP ( ) :
2024-06-25 17:42:25 +00:00
a , e := getAnswerExtrasForIP ( qName , nodeAddress , opts . req . Question [ 0 ] , reqType , opts . result , opts . ttl , opts . responseDomain , & opts . port , opts . dnsRecordMaker , false )
2024-03-01 15:36:37 +00:00
answer = append ( answer , a ... )
extra = append ( extra , e ... )
case opts . result . Type == discovery . ResultTypeNode && nodeAddress . IsIP ( ) :
canonicalNodeName := canonicalNameForResult ( opts . result . Type ,
opts . result . Node . Name , opts . responseDomain , opts . result . Tenancy , opts . port . Name )
2024-06-25 17:42:25 +00:00
a , e := getAnswerExtrasForIP ( canonicalNodeName , nodeAddress , opts . req . Question [ 0 ] , reqType , opts . result , opts . ttl , opts . responseDomain , & opts . port , opts . dnsRecordMaker , false )
2024-03-01 15:36:37 +00:00
answer = append ( answer , a ... )
extra = append ( extra , e ... )
case opts . result . Type == discovery . ResultTypeNode && ! nodeAddress . IsIP ( ) :
a , e := d . makeRecordFromFQDN ( serviceAddress . FQDN ( ) , opts )
answer = append ( answer , a ... )
extra = append ( extra , e ... )
case serviceAddress . IsEmptyString ( ) && nodeAddress . IsEmptyString ( ) :
return nil , nil
// There is no service address and the node address is an IP
case serviceAddress . IsEmptyString ( ) && nodeAddress . IsIP ( ) :
resultType := discovery . ResultTypeNode
if opts . result . Type == discovery . ResultTypeWorkload {
resultType = discovery . ResultTypeWorkload
}
canonicalNodeName := canonicalNameForResult ( resultType , opts . result . Node . Name ,
opts . responseDomain , opts . result . Tenancy , opts . port . Name )
2024-06-25 17:42:25 +00:00
a , e := getAnswerExtrasForIP ( canonicalNodeName , nodeAddress , opts . req . Question [ 0 ] , reqType , opts . result , opts . ttl , opts . responseDomain , & opts . port , opts . dnsRecordMaker , nodeAddress . String ( ) == opts . result . Node . Address ) // We compare the node address to the result to detect changes from the WAN translation
2024-03-01 15:36:37 +00:00
answer = append ( answer , a ... )
extra = append ( extra , e ... )
// There is no service address and the node address is a FQDN (external service)
case serviceAddress . IsEmptyString ( ) :
a , e := d . makeRecordFromFQDN ( nodeAddress . FQDN ( ) , opts )
answer = append ( answer , a ... )
extra = append ( extra , e ... )
2024-06-25 17:42:25 +00:00
case serviceAddress . IsIP ( ) && opts . req . Question [ 0 ] . Qtype == dns . TypeSRV :
a , e := getAnswerExtrasForIP ( qName , serviceAddress , opts . req . Question [ 0 ] , requestTypeName , opts . result , opts . ttl , opts . responseDomain , & opts . port , opts . dnsRecordMaker , false )
answer = append ( answer , a ... )
extra = append ( extra , e ... )
2024-03-01 15:36:37 +00:00
// The service address is an IP
case serviceAddress . IsIP ( ) :
canonicalServiceName := canonicalNameForResult ( discovery . ResultTypeService ,
opts . result . Service . Name , opts . responseDomain , opts . result . Tenancy , opts . port . Name )
2024-06-25 17:42:25 +00:00
a , e := getAnswerExtrasForIP ( canonicalServiceName , serviceAddress , opts . req . Question [ 0 ] , reqType , opts . result , opts . ttl , opts . responseDomain , & opts . port , opts . dnsRecordMaker , false )
2024-03-01 15:36:37 +00:00
answer = append ( answer , a ... )
extra = append ( extra , e ... )
// If the service address is a CNAME for the service we are looking
// for then use the node address.
case serviceAddress . FQDN ( ) == opts . req . Question [ 0 ] . Name && nodeAddress . IsIP ( ) :
canonicalNodeName := canonicalNameForResult ( discovery . ResultTypeNode ,
opts . result . Node . Name , opts . responseDomain , opts . result . Tenancy , opts . port . Name )
2024-06-25 17:42:25 +00:00
a , e := getAnswerExtrasForIP ( canonicalNodeName , nodeAddress , opts . req . Question [ 0 ] , reqType , opts . result , opts . ttl , opts . responseDomain , & opts . port , opts . dnsRecordMaker , nodeAddress . String ( ) == opts . result . Node . Address ) // We compare the node address to the result to detect changes from the WAN translation
2024-03-01 15:36:37 +00:00
answer = append ( answer , a ... )
extra = append ( extra , e ... )
// The service address is a FQDN (internal or external service name)
default :
a , e := d . makeRecordFromFQDN ( serviceAddress . FQDN ( ) , opts )
answer = append ( answer , a ... )
extra = append ( extra , e ... )
}
return
}
// makeRecordFromFQDN creates a DNS record from a FQDN.
func ( d messageSerializer ) makeRecordFromFQDN ( fqdn string , opts * getAnswerExtraAndNsOptions ) ( [ ] dns . RR , [ ] dns . RR ) {
edns := opts . req . IsEdns0 ( ) != nil
q := opts . req . Question [ 0 ]
more := opts . resolveCnameFunc ( opts . cfg , dns . Fqdn ( fqdn ) , opts . reqCtx , opts . remoteAddress , opts . maxRecursionLevel )
var additional [ ] dns . RR
extra := 0
MORE_REC :
for _ , rr := range more {
switch rr . Header ( ) . Rrtype {
case dns . TypeCNAME , dns . TypeA , dns . TypeAAAA , dns . TypeTXT :
// set the TTL manually
rr . Header ( ) . Ttl = opts . ttl
additional = append ( additional , rr )
extra ++
if extra == maxRecurseRecords && ! edns {
break MORE_REC
}
}
}
if q . Qtype == dns . TypeSRV {
answer := opts . dnsRecordMaker . makeSRV ( q . Name , fqdn , uint16 ( opts . result . DNS . Weight ) , opts . ttl , & opts . port )
return [ ] dns . RR { answer } , additional
}
address := ""
if opts . result . Service != nil && opts . result . Service . Address != "" {
address = opts . result . Service . Address
} else if opts . result . Node != nil {
address = opts . result . Node . Address
}
answers := [ ] dns . RR {
opts . dnsRecordMaker . makeCNAME ( q . Name , address , opts . ttl ) ,
}
answers = append ( answers , additional ... )
return answers , nil
}
// getAnswerAndExtraTXT determines whether a TXT needs to be create and then
// returns the TXT record in the answer or extra depending on the question type.
func getAnswerAndExtraTXT ( req * dns . Msg , cfg * RouterDynamicConfig , qName string ,
result * discovery . Result , ttl uint32 , domain string , query * discovery . Query ,
port * discovery . Port , maker dnsRecordMaker ) ( answer [ ] dns . RR , extra [ ] dns . RR ) {
if ! shouldAppendTXTRecord ( query , cfg , req ) {
return
}
recordHeaderName := qName
serviceAddress := newDNSAddress ( "" )
if result . Service != nil {
serviceAddress = newDNSAddress ( result . Service . Address )
}
if result . Type != discovery . ResultTypeNode &&
result . Type != discovery . ResultTypeVirtual &&
! serviceAddress . IsInternalFQDN ( domain ) &&
! serviceAddress . IsExternalFQDN ( domain ) {
recordHeaderName = canonicalNameForResult ( discovery . ResultTypeNode , result . Node . Name ,
domain , result . Tenancy , port . Name )
}
qType := req . Question [ 0 ] . Qtype
generateMeta := false
metaInAnswer := false
if qType == dns . TypeANY || qType == dns . TypeTXT {
generateMeta = true
metaInAnswer = true
} else if cfg . NodeMetaTXT {
generateMeta = true
}
// Do not generate txt records if we don't have to: https://github.com/hashicorp/consul/pull/5272
if generateMeta {
meta := maker . makeTXT ( recordHeaderName , result . Metadata , ttl )
if metaInAnswer {
answer = append ( answer , meta ... )
} else {
extra = append ( extra , meta ... )
}
}
return answer , extra
}
// shouldAppendTXTRecord determines whether a TXT record should be appended to the response.
func shouldAppendTXTRecord ( query * discovery . Query , cfg * RouterDynamicConfig , req * dns . Msg ) bool {
qType := req . Question [ 0 ] . Qtype
switch {
// Node records
case query != nil && query . QueryType == discovery . QueryTypeNode && ( cfg . NodeMetaTXT || qType == dns . TypeANY || qType == dns . TypeTXT ) :
return true
// Service records
case query != nil && query . QueryType == discovery . QueryTypeService && cfg . NodeMetaTXT && qType == dns . TypeSRV :
return true
// Prepared query records
case query != nil && query . QueryType == discovery . QueryTypePreparedQuery && cfg . NodeMetaTXT && qType == dns . TypeSRV :
return true
}
return false
}
// getAnswerExtrasForIP creates the dns answer and extra from IP dnsAddress pairs.
2024-06-25 17:42:25 +00:00
func getAnswerExtrasForIP ( name string , addr * dnsAddress , question dns . Question , reqType requestType , result * discovery . Result , ttl uint32 , domain string , port * discovery . Port , maker dnsRecordMaker , addressOverridden bool ) ( answer [ ] dns . RR , extra [ ] dns . RR ) {
2024-03-01 15:36:37 +00:00
qType := question . Qtype
canReturnARecord := qType == dns . TypeSRV || qType == dns . TypeA || qType == dns . TypeANY || qType == dns . TypeNS || qType == dns . TypeTXT
canReturnAAAARecord := qType == dns . TypeSRV || qType == dns . TypeAAAA || qType == dns . TypeANY || qType == dns . TypeNS || qType == dns . TypeTXT
if reqType != requestTypeAddress && result . Type != discovery . ResultTypeVirtual {
switch {
// check IPV4
case addr . IsIP ( ) && addr . IsIPV4 ( ) && ! canReturnARecord ,
// check IPV6
addr . IsIP ( ) && ! addr . IsIPV4 ( ) && ! canReturnAAAARecord :
return
}
}
// Have to pass original question name here even if the system has recursed
// and stripped off the domain suffix.
recHdrName := question . Name
if qType == dns . TypeSRV {
nameSplit := strings . Split ( name , "." )
if len ( nameSplit ) > 1 && nameSplit [ 1 ] == addrLabel {
recHdrName = name
} else {
recHdrName = name
}
name = question . Name
}
if reqType != requestTypeAddress && qType == dns . TypeSRV {
2024-06-25 17:42:25 +00:00
if addr . IsIP ( ) && question . Name == name && ! addressOverridden {
2024-03-01 15:36:37 +00:00
// encode the ip to be used in the header of the A/AAAA record
// as well as the target of the SRV record.
recHdrName = encodeIPAsFqdn ( result , addr . IP ( ) , domain )
}
if result . Type == discovery . ResultTypeWorkload {
recHdrName = canonicalNameForResult ( result . Type , result . Node . Name , domain , result . Tenancy , port . Name )
}
srv := maker . makeSRV ( name , recHdrName , uint16 ( result . DNS . Weight ) , ttl , port )
answer = append ( answer , srv )
}
record := maker . makeIPBasedRecord ( recHdrName , addr , ttl )
isARecordWhenNotExplicitlyQueried := record . Header ( ) . Rrtype == dns . TypeA && qType != dns . TypeA && qType != dns . TypeANY
isAAAARecordWhenNotExplicitlyQueried := record . Header ( ) . Rrtype == dns . TypeAAAA && qType != dns . TypeAAAA && qType != dns . TypeANY
// For explicit A/AAAA queries, we must only return those records in the answer section.
if isARecordWhenNotExplicitlyQueried ||
isAAAARecordWhenNotExplicitlyQueried {
extra = append ( extra , record )
} else {
answer = append ( answer , record )
}
return
}
// getPortsFromResult returns the ports from a discovery result.
func getPortsFromResult ( result * discovery . Result ) [ ] discovery . Port {
if len ( result . Ports ) > 0 {
return result . Ports
}
// return one record.
return [ ] discovery . Port { { } }
}
// encodeIPAsFqdn encodes an IP address as a FQDN.
func encodeIPAsFqdn ( result * discovery . Result , ip net . IP , responseDomain string ) string {
ipv4 := ip . To4 ( )
ipStr := hex . EncodeToString ( ip )
if ipv4 != nil {
ipStr = ipStr [ len ( ipStr ) - ( net . IPv4len * 2 ) : ]
}
if result . Tenancy . 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 , responseDomain )
}
return fmt . Sprintf ( "%s.addr.%s.%s" , ipStr , result . Tenancy . Datacenter , responseDomain )
}
// canonicalNameForResult returns the canonical name for a discovery result.
func canonicalNameForResult ( resultType discovery . ResultType , target , domain string ,
tenancy discovery . ResultTenancy , portName string ) string {
switch resultType {
case discovery . ResultTypeService :
if tenancy . Namespace != "" {
return fmt . Sprintf ( "%s.%s.%s.%s.%s" , target , "service" , tenancy . Namespace , tenancy . Datacenter , domain )
}
return fmt . Sprintf ( "%s.%s.%s.%s" , target , "service" , tenancy . Datacenter , domain )
case discovery . ResultTypeNode :
if tenancy . PeerName != "" && tenancy . Partition != "" {
// We must return a more-specific DNS name for peering so
// that there is no ambiguity with lookups.
// Nodes are always registered in the default namespace, so
// the `.ns` qualifier is not required.
return fmt . Sprintf ( "%s.node.%s.peer.%s.ap.%s" ,
target ,
tenancy . PeerName ,
tenancy . Partition ,
domain )
}
if tenancy . 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" ,
target ,
tenancy . PeerName ,
domain )
}
// Return a simpler format for non-peering nodes.
return fmt . Sprintf ( "%s.node.%s.%s" , target , tenancy . Datacenter , domain )
case discovery . ResultTypeWorkload :
// TODO (v2-dns): it doesn't appear this is being used to return a result. Need to investigate and refactor
if portName != "" {
return fmt . Sprintf ( "%s.port.%s.workload.%s.ns.%s.ap.%s" , portName , target , tenancy . Namespace , tenancy . Partition , domain )
}
return fmt . Sprintf ( "%s.workload.%s.ns.%s.ap.%s" , target , tenancy . Namespace , tenancy . Partition , domain )
}
return ""
}
// getServiceAddressMapFromLocationMap converts a map of Location to a map of ServiceAddress.
func getServiceAddressMapFromLocationMap ( taggedAddresses map [ string ] * discovery . TaggedAddress ) map [ string ] structs . ServiceAddress {
taggedServiceAddresses := make ( map [ string ] structs . ServiceAddress , len ( taggedAddresses ) )
for k , v := range taggedAddresses {
taggedServiceAddresses [ k ] = structs . ServiceAddress {
Address : v . Address ,
Port : int ( v . Port . Number ) ,
}
}
return taggedServiceAddresses
}
// getStringAddressMapFromTaggedAddressMap converts a map of Location to a map of string.
func getStringAddressMapFromTaggedAddressMap ( taggedAddresses map [ string ] * discovery . TaggedAddress ) map [ string ] string {
taggedServiceAddresses := make ( map [ string ] string , len ( taggedAddresses ) )
for k , v := range taggedAddresses {
taggedServiceAddresses [ k ] = v . Address
}
return taggedServiceAddresses
}
// getTTLForResult returns the TTL for a given result.
func getTTLForResult ( name string , overrideTTL * uint32 , query * discovery . Query , cfg * RouterDynamicConfig ) uint32 {
// In the case we are not making a discovery query, such as addr. or arpa. lookups,
// use the node TTL by convention
if query == nil {
return uint32 ( cfg . NodeTTL / time . Second )
}
if overrideTTL != nil {
// If a result was provided with an override, use that. This is the case for some prepared queries.
return * overrideTTL
}
switch query . QueryType {
case discovery . QueryTypeService , discovery . QueryTypePreparedQuery :
ttl , ok := cfg . GetTTLForService ( name )
if ok {
return uint32 ( ttl / time . Second )
}
fallthrough
default :
return uint32 ( cfg . NodeTTL / time . Second )
}
}
// serializeToGetAnswerExtraAndNsOptions converts serializeOptions to getAnswerExtraAndNsOptions.
func serializeToGetAnswerExtraAndNsOptions ( opts * serializeOptions ,
result * discovery . Result , port discovery . Port ) * getAnswerExtraAndNsOptions {
return & getAnswerExtraAndNsOptions {
port : port ,
result : result ,
req : opts . req ,
reqCtx : opts . reqCtx ,
query : opts . query ,
results : opts . results ,
resp : opts . resp ,
cfg : opts . cfg ,
responseDomain : opts . responseDomain ,
remoteAddress : opts . remoteAddress ,
maxRecursionLevel : opts . maxRecursionLevel ,
translateAddressFunc : opts . translateAddressFunc ,
translateServiceAddressFunc : opts . translateServiceAddressFunc ,
resolveCnameFunc : opts . resolveCnameFunc ,
dnsRecordMaker : opts . dnsRecordMaker ,
}
}