feat(v2dns): add partial support for SOA records (#20320)

This commit is contained in:
Dan Stough 2024-01-24 15:32:42 -05:00 committed by GitHub
parent 1f29ee604a
commit 6828780131
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 566 additions and 71 deletions

View File

@ -12,10 +12,38 @@ import (
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
) )
var (
ErrNoData = fmt.Errorf("no data")
ErrECSNotGlobal = fmt.Errorf("ECS response is not global")
)
// ECSNotGlobalError may be used to wrap an error or nil, to indicate that the
// EDNS client subnet source scope is not global.
// TODO (v2-dns): prepared queries errors are wrapped by this
type ECSNotGlobalError struct {
error
}
func (e ECSNotGlobalError) Error() string {
if e.error == nil {
return ""
}
return e.error.Error()
}
func (e ECSNotGlobalError) Is(other error) bool {
return other == ErrECSNotGlobal
}
func (e ECSNotGlobalError) Unwrap() error {
return e.error
}
// Query is used to request a name-based Service Discovery lookup. // Query is used to request a name-based Service Discovery lookup.
type Query struct { type Query struct {
QueryType QueryType QueryType QueryType
QueryPayload QueryPayload QueryPayload QueryPayload
Limit int
} }
// QueryType is used to filter service endpoints. // QueryType is used to filter service endpoints.

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"strings"
"sync/atomic" "sync/atomic"
"time" "time"
@ -144,9 +145,10 @@ func (r *Router) HandleRequest(req *dns.Msg, reqCtx discovery.Context, remoteAdd
return createServerFailureResponse(req, configCtx, false) return createServerFailureResponse(req, configCtx, false)
} }
reqType, responseDomain, needRecurse := r.parseDomain(req) responseDomain, needRecurse := r.parseDomain(req)
if needRecurse && !canRecurse(configCtx) { if needRecurse && !canRecurse(configCtx) {
return createServerFailureResponse(req, configCtx, true) // This is the same error as an unmatched domain
return createRefusedResponse(req)
} }
if needRecurse { if needRecurse {
@ -161,15 +163,23 @@ func (r *Router) HandleRequest(req *dns.Msg, reqCtx discovery.Context, remoteAdd
return resp return resp
} }
reqType := parseRequestType(req)
results, err := r.getQueryResults(req, reqCtx, reqType, configCtx) results, err := r.getQueryResults(req, reqCtx, reqType, configCtx)
switch {
if err != nil && errors.Is(err, errNameNotFound) { case errors.Is(err, errNameNotFound):
r.logger.Error("name not found", "name", req.Question[0].Name) r.logger.Error("name not found", "name", req.Question[0].Name)
return createNameErrorResponse(req, configCtx, responseDomain)
} ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal)
if err != nil { return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeNameError, ecsGlobal)
// TODO (v2-dns): there is another case here where the discovery service returns "name not found"
case errors.Is(err, discovery.ErrNoData):
r.logger.Debug("no data available", "name", req.Question[0].Name)
ecsGlobal := !errors.Is(err, discovery.ErrECSNotGlobal)
return createAuthoritativeResponse(req, configCtx, responseDomain, dns.RcodeSuccess, ecsGlobal)
case err != nil:
r.logger.Error("error processing discovery query", "error", err) r.logger.Error("error processing discovery query", "error", err)
return createServerFailureResponse(req, configCtx, false) return createServerFailureResponse(req, configCtx, canRecurse(configCtx))
} }
// This needs the question information because it affects the serialization format. // This needs the question information because it affects the serialization format.
@ -185,6 +195,17 @@ func (r *Router) HandleRequest(req *dns.Msg, reqCtx discovery.Context, remoteAdd
// getQueryResults returns a discovery.Result from a DNS message. // getQueryResults returns a discovery.Result from a DNS message.
func (r *Router) getQueryResults(req *dns.Msg, reqCtx discovery.Context, reqType requestType, cfgCtx *RouterDynamicConfig) ([]*discovery.Result, error) { func (r *Router) getQueryResults(req *dns.Msg, reqCtx discovery.Context, reqType requestType, cfgCtx *RouterDynamicConfig) ([]*discovery.Result, error) {
switch reqType { switch reqType {
case requestTypeConsul:
// This is a special case of discovery.QueryByName where we know that we need to query the consul service
// regardless of the question name.
query := &discovery.Query{
QueryType: discovery.QueryTypeService,
QueryPayload: discovery.QueryPayload{
Name: structs.ConsulServiceName,
},
Limit: 3, // TODO (v2-dns): need to thread this through to the backend and make sure we shuffle the results
}
return r.processor.QueryByName(query, reqCtx)
case requestTypeName: case requestTypeName:
query, err := buildQueryFromDNSMessage(req, r.domain, r.altDomain, cfgCtx, r.defaultEntMeta) query, err := buildQueryFromDNSMessage(req, r.domain, r.altDomain, cfgCtx, r.defaultEntMeta)
if err != nil { if err != nil {
@ -224,44 +245,58 @@ func (r *Router) ReloadConfig(newCfg *config.RuntimeConfig) error {
type requestType string type requestType string
const ( const (
requestTypeName requestType = "NAME" // A/AAAA/CNAME/SRV/SOA requestTypeName requestType = "NAME" // A/AAAA/CNAME/SRV
requestTypeIP requestType = "IP" requestTypeIP requestType = "IP" // PTR
requestTypeAddress requestType = "ADDR" requestTypeAddress requestType = "ADDR" // Custom addr. A/AAAA lookups
requestTypeConsul requestType = "CONSUL" // SOA/NS
) )
// parseQuery converts a DNS message into a generic discovery request. // parseDomain converts a DNS message into a generic discovery request.
// If the request domain does not match "consul." or the alternative domain, // If the request domain does not match "consul." or the alternative domain,
// it will return true for needRecurse. The logic is based on miekg/dns.ServeDNS matcher. // it will return true for needRecurse. The logic is based on miekg/dns.ServeDNS matcher.
// The implementation assumes that the only valid domains are "consul." and the alternative domain, and // The implementation assumes that the only valid domains are "consul." and the alternative domain, and
// that DS query types are not supported. // that DS query types are not supported.
func (r *Router) parseDomain(req *dns.Msg) (requestType, string, bool) { func (r *Router) parseDomain(req *dns.Msg) (string, bool) {
target := dns.CanonicalName(req.Question[0].Name) target := dns.CanonicalName(req.Question[0].Name)
target, _ = stripSuffix(target) target, _ = stripSuffix(target)
for offset, overflow := 0, false; !overflow; offset, overflow = dns.NextLabel(target, offset) { for offset, overflow := 0, false; !overflow; offset, overflow = dns.NextLabel(target, offset) {
subdomain := target[offset:] subdomain := target[offset:]
switch subdomain { switch subdomain {
case ".":
// We don't support consul having a domain or altdomain attached to the root.
return "", true
case r.domain: case r.domain:
if isAddrSubdomain(target) { return r.domain, false
return requestTypeAddress, r.domain, false
}
return requestTypeName, r.domain, false
case r.altDomain: case r.altDomain:
// TODO (v2-dns): the default, unspecified alt domain should be ".". Next label should never return this return r.altDomain, false
// but write a test to verify that.
if isAddrSubdomain(target) {
return requestTypeAddress, r.altDomain, false
}
return requestTypeName, r.altDomain, false
case arpaDomain: case arpaDomain:
// PTR queries always respond with the primary domain. // PTR queries always respond with the primary domain.
return requestTypeIP, r.domain, false return r.domain, false
// Default: fallthrough // Default: fallthrough
} }
} }
// No match found; recurse if possible // No match found; recurse if possible
return "", "", true return "", true
}
// parseRequestType inspects the DNS message type and question name to determine the requestType of request.
// We assume by the time this is called, we are responding to a question with a domain we serve.
// This is used internally to determine which query processor method (if any) to invoke.
func parseRequestType(req *dns.Msg) requestType {
switch {
case req.Question[0].Qtype == dns.TypeSOA || req.Question[0].Qtype == dns.TypeNS:
// SOA and NS type supersede the domain
// NOTE!: In V1 of the DNS server it was possible to serve a PTR lookup using the arpa domain but a SOA question type.
// This also included the SOA record. This seemed inconsistent and unnecessary - it was removed for simplicity.
return requestTypeConsul
case isPTRSubdomain(req.Question[0].Name):
return requestTypeIP
case isAddrSubdomain(req.Question[0].Name):
return requestTypeAddress
default:
return requestTypeName
}
} }
// serializeQueryResults converts a discovery.Result into a DNS message. // serializeQueryResults converts a discovery.Result into a DNS message.
@ -272,7 +307,10 @@ func (r *Router) serializeQueryResults(req *dns.Msg, results []*discovery.Result
resp.Authoritative = true resp.Authoritative = true
resp.RecursionAvailable = canRecurse(cfg) resp.RecursionAvailable = canRecurse(cfg)
// TODO (v2-dns): add SOA if that is the question type // Always add the SOA record if requested.
if req.Question[0].Qtype == dns.TypeSOA {
resp.Answer = append(resp.Answer, makeSOARecord(responseDomain, cfg))
}
for _, result := range results { for _, result := range results {
appendResultToDNSResponse(result, req, resp, responseDomain, cfg) appendResultToDNSResponse(result, req, resp, responseDomain, cfg)
@ -334,6 +372,18 @@ func isAddrSubdomain(domain string) bool {
return false return false
} }
// isPTRSubdomain returns true if the domain ends in the PTR domain, "in-addr.arpa.".
func isPTRSubdomain(domain string) bool {
labels := dns.SplitDomainName(domain)
labelCount := len(labels)
if labelCount < 3 {
return false
}
return fmt.Sprintf("%s.%s.", labels[labelCount-2], labels[labelCount-1]) == arpaDomain
}
// getDynamicRouterConfig takes agent config and creates/resets the config used by DNS Router // getDynamicRouterConfig takes agent config and creates/resets the config used by DNS Router
func getDynamicRouterConfig(conf *config.RuntimeConfig) (*RouterDynamicConfig, error) { func getDynamicRouterConfig(conf *config.RuntimeConfig) (*RouterDynamicConfig, error) {
cfg := &RouterDynamicConfig{ cfg := &RouterDynamicConfig{
@ -402,7 +452,7 @@ func setEDNS(request *dns.Msg, response *dns.Msg, ecsGlobal bool) {
ednsResp.Hdr.Rrtype = dns.TypeOPT ednsResp.Hdr.Rrtype = dns.TypeOPT
ednsResp.SetUDPSize(edns.UDPSize()) ednsResp.SetUDPSize(edns.UDPSize())
// Setup the ECS option if present // Set up the ECS option if present
if subnet := ednsSubnetForRequest(request); subnet != nil { if subnet := ednsSubnetForRequest(request); subnet != nil {
subOp := new(dns.EDNS0_SUBNET) subOp := new(dns.EDNS0_SUBNET)
subOp.Code = dns.EDNS0SUBNET subOp.Code = dns.EDNS0SUBNET
@ -426,7 +476,6 @@ func setEDNS(request *dns.Msg, response *dns.Msg, ecsGlobal bool) {
func ednsSubnetForRequest(req *dns.Msg) *dns.EDNS0_SUBNET { func ednsSubnetForRequest(req *dns.Msg) *dns.EDNS0_SUBNET {
// IsEdns0 returns the EDNS RR if present or nil otherwise // IsEdns0 returns the EDNS RR if present or nil otherwise
edns := req.IsEdns0() edns := req.IsEdns0()
if edns == nil { if edns == nil {
return nil return nil
} }
@ -436,11 +485,11 @@ func ednsSubnetForRequest(req *dns.Msg) *dns.EDNS0_SUBNET {
return subnet return subnet
} }
} }
return nil return nil
} }
// createRefusedResponse returns a REFUSED message. // createRefusedResponse returns a REFUSED message. This is the default behavior for unmatched queries in
// upstream miekg/dns.
func createRefusedResponse(req *dns.Msg) *dns.Msg { func createRefusedResponse(req *dns.Msg) *dns.Msg {
// Return a REFUSED message // Return a REFUSED message
m := &dns.Msg{} m := &dns.Msg{}
@ -448,32 +497,20 @@ func createRefusedResponse(req *dns.Msg) *dns.Msg {
return m return m
} }
// createNameErrorResponse returns a NXDOMAIN message. // createAuthoritativeResponse returns an authoritative message that contains the SOA in the event that data is
func createNameErrorResponse(req *dns.Msg, cfg *RouterDynamicConfig, domain string) *dns.Msg { // not return for a query. There can be multiple reasons for not returning data, hence the rcode argument.
// Return a NXDOMAIN message func createAuthoritativeResponse(req *dns.Msg, cfg *RouterDynamicConfig, domain string, rcode int, ecsGlobal bool) *dns.Msg {
m := &dns.Msg{} m := &dns.Msg{}
m.SetRcode(req, dns.RcodeNameError) m.SetRcode(req, rcode)
m.Compress = !cfg.DisableCompression m.Compress = !cfg.DisableCompression
m.Authoritative = true m.Authoritative = true
m.RecursionAvailable = canRecurse(cfg)
if edns := req.IsEdns0(); edns != nil {
setEDNS(req, m, ecsGlobal)
}
// We add the SOA on NameErrors // We add the SOA on NameErrors
// TODO (v2-dns): refactor into a common function soa := makeSOARecord(domain, cfg)
soa := &dns.SOA{
Hdr: dns.RR_Header{
Name: domain,
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
// Has to be consistent with MinTTL to avoid invalidation
Ttl: cfg.SOAConfig.Minttl,
},
Ns: "ns." + domain,
Serial: uint32(time.Now().Unix()),
Mbox: "hostmaster." + domain,
Refresh: cfg.SOAConfig.Refresh,
Retry: cfg.SOAConfig.Retry,
Expire: cfg.SOAConfig.Expire,
Minttl: cfg.SOAConfig.Minttl,
}
m.Ns = append(m.Ns, soa) m.Ns = append(m.Ns, soa)
return m return m
@ -504,7 +541,7 @@ func buildAddressResults(req *dns.Msg) ([]*discovery.Result, error) {
} }
// buildQueryFromDNSMessage appends the discovery result to the dns message. // buildQueryFromDNSMessage appends the discovery result to the dns message.
func appendResultToDNSResponse(result *discovery.Result, req *dns.Msg, resp *dns.Msg, _ string, cfg *RouterDynamicConfig) { func appendResultToDNSResponse(result *discovery.Result, req *dns.Msg, resp *dns.Msg, domain string, cfg *RouterDynamicConfig) {
ip, ok := convertToIp(result) ip, ok := convertToIp(result)
// if the result is not an IP, we can try to recurse on the hostname. // if the result is not an IP, we can try to recurse on the hostname.
@ -516,7 +553,7 @@ func appendResultToDNSResponse(result *discovery.Result, req *dns.Msg, resp *dns
var ttl uint32 var ttl uint32
switch result.Type { switch result.Type {
case discovery.ResultTypeNode, discovery.ResultTypeVirtual: case discovery.ResultTypeNode, discovery.ResultTypeVirtual, discovery.ResultTypeWorkload:
ttl = uint32(cfg.NodeTTL / time.Second) ttl = uint32(cfg.NodeTTL / time.Second)
case discovery.ResultTypeService: case discovery.ResultTypeService:
// TODO (v2-dns): implement service TTL using the radix tree // TODO (v2-dns): implement service TTL using the radix tree
@ -527,12 +564,32 @@ func appendResultToDNSResponse(result *discovery.Result, req *dns.Msg, resp *dns
record, isIPV4 := makeRecord(qName, ip, ttl) record, isIPV4 := makeRecord(qName, ip, ttl)
if qType == dns.TypeSRV { // TODO (v2-dns): skip records that refer to a workload/node that don't have a valid DNS name.
// Special case responses
switch qType {
case dns.TypeSOA:
// TODO (v2-dns): fqdn in V1 has the datacenter included, this would need to be added to discovery.Result
// to be returned in the result.
fqdn := fmt.Sprintf("%s.%s.%s", result.Target, strings.ToLower(string(result.Type)), domain)
extraRecord, _ := makeRecord(fqdn, ip, ttl) // TODO (v2-dns): this is not sufficient, because recursion and CNAMES are supported
resp.Ns = append(resp.Ns, makeNSRecord(domain, fqdn, ttl))
resp.Extra = append(resp.Extra, extraRecord)
return
case dns.TypeNS:
// TODO (v2-dns): fqdn in V1 has the datacenter included, this would need to be added to discovery.Result
fqdn := fmt.Sprintf("%s.%s.%s.", result.Target, strings.ToLower(string(result.Type)), domain)
extraRecord, _ := makeRecord(fqdn, ip, ttl) // TODO (v2-dns): this is not sufficient, because recursion and CNAMES are supported
resp.Answer = append(resp.Ns, makeNSRecord(domain, fqdn, ttl))
resp.Extra = append(resp.Extra, extraRecord)
return
case dns.TypeSRV:
// We put A/AAAA/CNAME records in the additional section for SRV requests // We put A/AAAA/CNAME records in the additional section for SRV requests
resp.Extra = append(resp.Extra, record) resp.Extra = append(resp.Extra, record)
// TODO (v2-dns): implement SRV records for the answer section // TODO (v2-dns): implement SRV records for the answer section
return return
} }
@ -558,11 +615,41 @@ func convertToIp(result *discovery.Result) (net.IP, bool) {
return ip, true return ip, true
} }
// n A or AAAA record for the given name and IP. func makeSOARecord(domain string, cfg *RouterDynamicConfig) dns.RR {
return &dns.SOA{
Hdr: dns.RR_Header{
Name: domain,
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
// Has to be consistent with MinTTL to avoid invalidation
Ttl: cfg.SOAConfig.Minttl,
},
Ns: "ns." + domain,
Serial: uint32(time.Now().Unix()),
Mbox: "hostmaster." + domain,
Refresh: cfg.SOAConfig.Refresh,
Retry: cfg.SOAConfig.Retry,
Expire: cfg.SOAConfig.Expire,
Minttl: cfg.SOAConfig.Minttl,
}
}
func makeNSRecord(domain, fqdn string, ttl uint32) dns.RR {
return &dns.NS{
Hdr: dns.RR_Header{
Name: domain,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: ttl,
},
Ns: fqdn,
}
}
// makeRecord an A or AAAA record for the given name and IP.
// Note: we might want to pass in the Query Name here, which is used in addr. and virtual. queries // Note: we might want to pass in the Query Name here, which is used in addr. and virtual. queries
// since there is only ever one result. Right now choosing to leave it off for simplification. // since there is only ever one result. Right now choosing to leave it off for simplification.
func makeRecord(name string, ip net.IP, ttl uint32) (dns.RR, bool) { func makeRecord(name string, ip net.IP, ttl uint32) (dns.RR, bool) {
isIPV4 := ip.To4() != nil isIPV4 := ip.To4() != nil
if isIPV4 { if isIPV4 {

View File

@ -17,15 +17,16 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/discovery" "github.com/hashicorp/consul/agent/discovery"
"github.com/hashicorp/consul/agent/structs"
) )
// TODO (v2-dns) // TODO (v2-dns)
// Test Parameters // TBD Test Cases
// 1. Domain vs AltDomain vs non-consul Main domain // 1. Reload the configuration (e.g. SOA)
// 2. Reload the configuration (e.g. SOA) // 2. Something to check the token makes it through to the data fetcher
// 3. Something to check the token makes it through to the data fetcher // 3. Something case-insensitive
// 4. Something case insensitive // 4. Test the edns settings.
func Test_HandleRequest(t *testing.T) { func Test_HandleRequest(t *testing.T) {
type testCase struct { type testCase struct {
@ -59,13 +60,10 @@ func Test_HandleRequest(t *testing.T) {
// configureRecursor: call not expected. // configureRecursor: call not expected.
response: &dns.Msg{ response: &dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery, Opcode: dns.OpcodeQuery,
Response: true, Response: true,
Authoritative: false, Rcode: dns.RcodeRefused,
Rcode: dns.RcodeServerFailure,
RecursionAvailable: true,
}, },
Compress: true,
Question: []dns.Question{ Question: []dns.Question{
{ {
Name: "google.com.", Name: "google.com.",
@ -148,7 +146,7 @@ func Test_HandleRequest(t *testing.T) {
}, },
}, },
{ {
name: "recursors configured, matching domain", name: "recursors configured, no matching domain",
request: &dns.Msg{ request: &dns.Msg{
MsgHdr: dns.MsgHdr{ MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery, Opcode: dns.OpcodeQuery,
@ -226,6 +224,93 @@ func Test_HandleRequest(t *testing.T) {
}, },
}, },
}, },
{
name: "recursors configured, the root domain is handled by the recursor",
request: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
},
Question: []dns.Question{
{
Name: ".",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
},
},
},
agentConfig: &config.RuntimeConfig{
DNSRecursors: []string{"8.8.8.8"},
},
configureRecursor: func(recursor dnsRecursor) {
// this response is modeled after `dig .`
resp := &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
Response: true,
Authoritative: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{
{
Name: ".",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
},
},
Answer: []dns.RR{
&dns.SOA{
Hdr: dns.RR_Header{
Name: ".",
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: 86391,
},
Ns: "a.root-servers.net.",
Serial: 2024012200,
Mbox: "nstld.verisign-grs.com.",
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
},
},
}
recursor.(*mockDnsRecursor).On("handle",
mock.Anything, mock.Anything, mock.Anything).Return(resp, nil)
},
response: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
Response: true,
Authoritative: true,
Rcode: dns.RcodeSuccess,
},
Question: []dns.Question{
{
Name: ".",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
},
},
Answer: []dns.RR{
&dns.SOA{
Hdr: dns.RR_Header{
Name: ".",
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: 86391,
},
Ns: "a.root-servers.net.",
Serial: 2024012200,
Mbox: "nstld.verisign-grs.com.",
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
},
},
},
},
// addr queries // addr queries
{ {
name: "test A 'addr.' query, ipv4 response", name: "test A 'addr.' query, ipv4 response",
@ -709,10 +794,303 @@ func Test_HandleRequest(t *testing.T) {
}, },
}, },
}, },
// SOA Queries
{
name: "vanilla SOA query",
request: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
},
Question: []dns.Question{
{
Name: "consul.",
Qtype: dns.TypeSOA,
Qclass: dns.ClassINET,
},
},
},
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
fetcher.(*discovery.MockCatalogDataFetcher).
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
Return([]*discovery.Result{
{
Address: "1.2.3.4",
Type: discovery.ResultTypeWorkload,
Target: "server-one", // This would correlate to the workload name
},
{
Address: "4.5.6.7",
Type: discovery.ResultTypeWorkload,
Target: "server-two", // This would correlate to the workload name
},
}, nil).
Run(func(args mock.Arguments) {
req := args.Get(1).(*discovery.QueryPayload)
reqType := args.Get(2).(discovery.LookupType)
require.Equal(t, discovery.LookupTypeService, reqType)
require.Equal(t, structs.ConsulServiceName, req.Name)
})
},
response: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
Response: true,
Authoritative: true,
},
Compress: true,
Question: []dns.Question{
{
Name: "consul.",
Qtype: dns.TypeSOA,
Qclass: dns.ClassINET,
},
},
Answer: []dns.RR{
&dns.SOA{
Hdr: dns.RR_Header{
Name: "consul.",
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: 4,
},
Ns: "ns.consul.",
Serial: uint32(time.Now().Unix()),
Mbox: "hostmaster.consul.",
Refresh: 1,
Expire: 3,
Retry: 2,
Minttl: 4,
},
},
Ns: []dns.RR{
&dns.NS{
Hdr: dns.RR_Header{
Name: "consul.",
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 123,
},
Ns: "server-one.workload.consul.",
},
&dns.NS{
Hdr: dns.RR_Header{
Name: "consul.",
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 123,
},
Ns: "server-two.workload.consul.",
},
},
Extra: []dns.RR{
&dns.A{
Hdr: dns.RR_Header{
Name: "server-one.workload.consul.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 123,
},
A: net.ParseIP("1.2.3.4"),
},
&dns.A{
Hdr: dns.RR_Header{
Name: "server-two.workload.consul.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 123,
},
A: net.ParseIP("4.5.6.7"),
},
},
},
},
{
name: "SOA query against alternate domain",
request: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
},
Question: []dns.Question{
{
Name: "testdomain.",
Qtype: dns.TypeSOA,
Qclass: dns.ClassINET,
},
},
},
agentConfig: &config.RuntimeConfig{
DNSDomain: "consul",
DNSAltDomain: "testdomain",
DNSNodeTTL: 123 * time.Second,
DNSSOA: config.RuntimeSOAConfig{
Refresh: 1,
Retry: 2,
Expire: 3,
Minttl: 4,
},
},
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
fetcher.(*discovery.MockCatalogDataFetcher).
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
Return([]*discovery.Result{
{
Address: "1.2.3.4",
Type: discovery.ResultTypeWorkload,
Target: "server-one", // This would correlate to the workload name
},
{
Address: "4.5.6.7",
Type: discovery.ResultTypeWorkload,
Target: "server-two", // This would correlate to the workload name
},
}, nil).
Run(func(args mock.Arguments) {
req := args.Get(1).(*discovery.QueryPayload)
reqType := args.Get(2).(discovery.LookupType)
require.Equal(t, discovery.LookupTypeService, reqType)
require.Equal(t, structs.ConsulServiceName, req.Name)
})
},
response: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
Response: true,
Authoritative: true,
},
Compress: true,
Question: []dns.Question{
{
Name: "testdomain.",
Qtype: dns.TypeSOA,
Qclass: dns.ClassINET,
},
},
Answer: []dns.RR{
&dns.SOA{
Hdr: dns.RR_Header{
Name: "testdomain.",
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: 4,
},
Ns: "ns.testdomain.",
Serial: uint32(time.Now().Unix()),
Mbox: "hostmaster.testdomain.",
Refresh: 1,
Expire: 3,
Retry: 2,
Minttl: 4,
},
},
Ns: []dns.RR{
&dns.NS{
Hdr: dns.RR_Header{
Name: "testdomain.",
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 123,
},
Ns: "server-one.workload.testdomain.",
},
&dns.NS{
Hdr: dns.RR_Header{
Name: "testdomain.",
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 123,
},
Ns: "server-two.workload.testdomain.",
},
},
Extra: []dns.RR{
&dns.A{
Hdr: dns.RR_Header{
Name: "server-one.workload.testdomain.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 123,
},
A: net.ParseIP("1.2.3.4"),
},
&dns.A{
Hdr: dns.RR_Header{
Name: "server-two.workload.testdomain.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 123,
},
A: net.ParseIP("4.5.6.7"),
},
},
},
},
// Service Lookup
{
name: "When no data is return from a query, send SOA",
request: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
},
Question: []dns.Question{
{
Name: "foo.service.consul.",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
},
},
},
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
fetcher.(*discovery.MockCatalogDataFetcher).
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
Return(nil, discovery.ErrNoData).
Run(func(args mock.Arguments) {
req := args.Get(1).(*discovery.QueryPayload)
reqType := args.Get(2).(discovery.LookupType)
require.Equal(t, discovery.LookupTypeService, reqType)
require.Equal(t, "foo", req.Name)
})
},
response: &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
Response: true,
Authoritative: true,
},
Compress: true,
Question: []dns.Question{
{
Name: "foo.service.consul.",
Qtype: dns.TypeA,
Qclass: dns.ClassINET,
},
},
Ns: []dns.RR{
&dns.SOA{
Hdr: dns.RR_Header{
Name: "consul.",
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: 4,
},
Ns: "ns.consul.",
Serial: uint32(time.Now().Unix()),
Mbox: "hostmaster.consul.",
Refresh: 1,
Expire: 3,
Retry: 2,
Minttl: 4,
},
},
},
},
// TODO (v2-dns): add a test to make sure only 3 records are returned
} }
run := func(t *testing.T, tc testCase) { run := func(t *testing.T, tc testCase) {
cdf := &discovery.MockCatalogDataFetcher{} cdf := discovery.NewMockCatalogDataFetcher(t)
if tc.configureDataFetcher != nil { if tc.configureDataFetcher != nil {
tc.configureDataFetcher(cdf) tc.configureDataFetcher(cdf)
} }

View File

@ -285,6 +285,7 @@ func TestDNSCycleRecursorCheck(t *testing.T) {
A: []byte{0xAC, 0x15, 0x2D, 0x43}, // 172 , 21, 45, 67 A: []byte{0xAC, 0x15, 0x2D, 0x43}, // 172 , 21, 45, 67
}, },
} }
require.NotNil(t, in)
require.Equal(t, wantAnswer, in.Answer) require.Equal(t, wantAnswer, in.Answer)
}) })
} }
@ -323,6 +324,7 @@ func TestDNSCycleRecursorCheckAllFail(t *testing.T) {
in, _, err := client.Exchange(m, agent.DNSAddr()) in, _, err := client.Exchange(m, agent.DNSAddr())
require.NoError(t, err) require.NoError(t, err)
// Verify if we hit SERVFAIL from Consul // Verify if we hit SERVFAIL from Consul
require.NotNil(t, in)
require.Equal(t, dns.RcodeServerFailure, in.Rcode) require.Equal(t, dns.RcodeServerFailure, in.Rcode)
}) })
} }