mirror of https://github.com/status-im/consul.git
DNS v2 - split up router into multiple responsibilities & break up router tests into multiple files. (#20688)
* Update agent/dns.go Co-authored-by: Michael Zalimeni <michael.zalimeni@hashicorp.com> * PR feedback * split tests out into multiple files. * Extract responsibilities from router into discoveryResultsFetcher, messageSerializer, responseGenerator. * adding recordmaker tests * add response generator test coverage. * changing tests case name based on PR feedback --------- Co-authored-by: Michael Zalimeni <michael.zalimeni@hashicorp.com>
This commit is contained in:
parent
a15a957a36
commit
a1c6181677
|
@ -106,7 +106,6 @@ const (
|
|||
type Result struct {
|
||||
Service *Location // The name and address of the service.
|
||||
Node *Location // The name and address of the node.
|
||||
Weight uint32 // SRV queries
|
||||
Metadata map[string]string // Used to collect metadata into TXT Records
|
||||
Type ResultType // Used to reconstruct the fqdn name of the resource
|
||||
DNS DNSConfig // Used for DNS-specific configuration for this result
|
||||
|
|
|
@ -4,14 +4,95 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/internal/dnsutil"
|
||||
)
|
||||
|
||||
// discoveryResultsFetcher is a facade for the DNS router to formulate
|
||||
// and execute discovery queries.
|
||||
type discoveryResultsFetcher struct{}
|
||||
|
||||
// getQueryOptions is a struct to hold the options for getQueryResults method.
|
||||
type getQueryOptions struct {
|
||||
req *dns.Msg
|
||||
reqCtx Context
|
||||
qName string
|
||||
remoteAddress net.Addr
|
||||
processor DiscoveryQueryProcessor
|
||||
logger hclog.Logger
|
||||
domain string
|
||||
altDomain string
|
||||
}
|
||||
|
||||
// getQueryResults returns a discovery.Result from a DNS message.
|
||||
func (d discoveryResultsFetcher) getQueryResults(opts *getQueryOptions) ([]*discovery.Result, *discovery.Query, error) {
|
||||
reqType := parseRequestType(opts.req)
|
||||
|
||||
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,
|
||||
Tenancy: discovery.QueryTenancy{
|
||||
// We specify the partition here so that in the case we are a client agent in a non-default partition.
|
||||
// We don't want the query processors default partition to be used.
|
||||
// This is a small hack because for V1 CE, this is not the correct default partition name, but we
|
||||
// need to add something to disambiguate the empty field.
|
||||
Partition: acl.DefaultPartitionName, //NOTE: note this won't work if we ever have V2 client agents
|
||||
},
|
||||
Limit: 3,
|
||||
},
|
||||
}
|
||||
|
||||
results, err := opts.processor.QueryByName(query, discovery.Context{Token: opts.reqCtx.Token})
|
||||
return results, query, err
|
||||
case requestTypeName:
|
||||
query, err := buildQueryFromDNSMessage(opts.req, opts.reqCtx, opts.domain, opts.altDomain, opts.remoteAddress)
|
||||
if err != nil {
|
||||
opts.logger.Error("error building discovery query from DNS request", "error", err)
|
||||
return nil, query, err
|
||||
}
|
||||
results, err := opts.processor.QueryByName(query, discovery.Context{Token: opts.reqCtx.Token})
|
||||
|
||||
if getErrorFromECSNotGlobalError(err) != nil {
|
||||
opts.logger.Error("error processing discovery query", "error", err)
|
||||
return nil, query, err
|
||||
}
|
||||
return results, query, err
|
||||
case requestTypeIP:
|
||||
ip := dnsutil.IPFromARPA(opts.qName)
|
||||
if ip == nil {
|
||||
opts.logger.Error("error building IP from DNS request", "name", opts.qName)
|
||||
return nil, nil, errNameNotFound
|
||||
}
|
||||
results, err := opts.processor.QueryByIP(ip, discovery.Context{Token: opts.reqCtx.Token})
|
||||
return results, nil, err
|
||||
case requestTypeAddress:
|
||||
results, err := buildAddressResults(opts.req)
|
||||
if err != nil {
|
||||
opts.logger.Error("error processing discovery query", "error", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
return results, nil, nil
|
||||
}
|
||||
|
||||
opts.logger.Error("error parsing discovery query type", "requestType", reqType)
|
||||
return nil, nil, errInvalidQuestion
|
||||
}
|
||||
|
||||
// buildQueryFromDNSMessage returns a discovery.Query from a DNS message.
|
||||
func buildQueryFromDNSMessage(req *dns.Msg, reqCtx Context, domain, altDomain string,
|
||||
remoteAddress net.Addr) (*discovery.Query, error) {
|
||||
|
@ -46,6 +127,32 @@ func buildQueryFromDNSMessage(req *dns.Msg, reqCtx Context, domain, altDomain st
|
|||
}, nil
|
||||
}
|
||||
|
||||
// buildAddressResults returns a discovery.Result from a DNS request for addr. records.
|
||||
func buildAddressResults(req *dns.Msg) ([]*discovery.Result, error) {
|
||||
domain := dns.CanonicalName(req.Question[0].Name)
|
||||
labels := dns.SplitDomainName(domain)
|
||||
hexadecimal := labels[0]
|
||||
|
||||
if len(hexadecimal)/2 != 4 && len(hexadecimal)/2 != 16 {
|
||||
return nil, errNameNotFound
|
||||
}
|
||||
|
||||
var ip net.IP
|
||||
ip, err := hex.DecodeString(hexadecimal)
|
||||
if err != nil {
|
||||
return nil, errNameNotFound
|
||||
}
|
||||
|
||||
return []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{
|
||||
Address: ip.String(),
|
||||
},
|
||||
Type: discovery.ResultTypeNode, // We choose node by convention since we do not know the origin of the IP
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getQueryNameAndTagFromParts returns the query name and tag from the query parts that are taken from the original dns question.
|
||||
func getQueryNameAndTagFromParts(queryType discovery.QueryType, queryParts []string) (string, string) {
|
||||
n := len(queryParts)
|
||||
|
@ -146,7 +253,7 @@ func getEffectiveDatacenter(labels *parsedLabels, defaultDC string) string {
|
|||
func getQueryTypePartsAndSuffixesFromDNSMessage(req *dns.Msg, domain, altDomain string) (queryType discovery.QueryType, parts []string, suffixes []string) {
|
||||
// Get the QName without the domain suffix
|
||||
// TODO (v2-dns): we will also need to handle the "failover" and "no-failover" suffixes here.
|
||||
// They come AFTER the domain. See `stripSuffix` in router.go
|
||||
// They come AFTER the domain. See `stripAnyFailoverSuffix` in router.go
|
||||
qName := trimDomainFromQuestionName(req.Question[0].Name, domain, altDomain)
|
||||
|
||||
// Split into the label parts
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
)
|
||||
|
||||
// dnsRecordMaker creates DNS records to be used when generating
|
||||
// responses to dns requests.
|
||||
type dnsRecordMaker struct{}
|
||||
|
||||
// makeSOA returns an SOA record for the given domain and config.
|
||||
func (dnsRecordMaker) makeSOA(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,
|
||||
}
|
||||
}
|
||||
|
||||
// makeNS returns an NS record for the given domain and fqdn.
|
||||
func (dnsRecordMaker) makeNS(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,
|
||||
}
|
||||
}
|
||||
|
||||
// makeIPBasedRecord returns 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
|
||||
// since there is only ever one result. Right now choosing to leave it off for simplification.
|
||||
func (dnsRecordMaker) makeIPBasedRecord(name string, addr *dnsAddress, ttl uint32) dns.RR {
|
||||
|
||||
if addr.IsIPV4() {
|
||||
// check if the query type is A for IPv4 or ANY
|
||||
return &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
},
|
||||
A: addr.IP(),
|
||||
}
|
||||
}
|
||||
|
||||
return &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
},
|
||||
AAAA: addr.IP(),
|
||||
}
|
||||
}
|
||||
|
||||
// makeCNAME returns a CNAME record for the given name and target.
|
||||
func (dnsRecordMaker) makeCNAME(name string, target string, ttl uint32) *dns.CNAME {
|
||||
return &dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
},
|
||||
Target: dns.Fqdn(target),
|
||||
}
|
||||
}
|
||||
|
||||
// makeSRV returns an SRV record for the given name and target.
|
||||
func (dnsRecordMaker) makeSRV(name, target string, weight uint16, ttl uint32, port *discovery.Port) *dns.SRV {
|
||||
return &dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
},
|
||||
Priority: 1,
|
||||
Weight: weight,
|
||||
Port: uint16(port.Number),
|
||||
Target: target,
|
||||
}
|
||||
}
|
||||
|
||||
// makeTXT returns a TXT record for the given name and result metadata.
|
||||
func (dnsRecordMaker) makeTXT(name string, metadata map[string]string, ttl uint32) []dns.RR {
|
||||
extra := make([]dns.RR, 0, len(metadata))
|
||||
for key, value := range metadata {
|
||||
txt := value
|
||||
if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") {
|
||||
txt = encodeKVasRFC1464(key, value)
|
||||
}
|
||||
|
||||
extra = append(extra, &dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ttl,
|
||||
},
|
||||
Txt: []string{txt},
|
||||
})
|
||||
}
|
||||
return extra
|
||||
}
|
||||
|
||||
// encodeKVasRFC1464 encodes a key-value pair according to RFC1464
|
||||
func encodeKVasRFC1464(key, value string) (txt string) {
|
||||
// For details on these replacements c.f. https://www.ietf.org/rfc/rfc1464.txt
|
||||
key = strings.Replace(key, "`", "``", -1)
|
||||
key = strings.Replace(key, "=", "`=", -1)
|
||||
|
||||
// Backquote the leading spaces
|
||||
leadingSpacesRE := regexp.MustCompile("^ +")
|
||||
numLeadingSpaces := len(leadingSpacesRE.FindString(key))
|
||||
key = leadingSpacesRE.ReplaceAllString(key, strings.Repeat("` ", numLeadingSpaces))
|
||||
|
||||
// Backquote the trailing spaces
|
||||
numTrailingSpaces := len(trailingSpacesRE.FindString(key))
|
||||
key = trailingSpacesRE.ReplaceAllString(key, strings.Repeat("` ", numTrailingSpaces))
|
||||
|
||||
value = strings.Replace(value, "`", "``", -1)
|
||||
|
||||
return key + "=" + value
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
)
|
||||
|
||||
func TestDNSRecordMaker_makeSOA(t *testing.T) {
|
||||
cfg := &RouterDynamicConfig{
|
||||
SOAConfig: SOAConfig{
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
},
|
||||
}
|
||||
domain := "testdomain."
|
||||
expected := &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,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
}
|
||||
actual := dnsRecordMaker{}.makeSOA(domain, cfg)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestDNSRecordMaker_makeNS(t *testing.T) {
|
||||
domain := "testdomain."
|
||||
fqdn := "ns.testdomain."
|
||||
ttl := uint32(123)
|
||||
expected := &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
}
|
||||
actual := dnsRecordMaker{}.makeNS(domain, fqdn, ttl)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestDNSRecordMaker_makeIPBasedRecord(t *testing.T) {
|
||||
ipv4Addr := newDNSAddress("1.2.3.4")
|
||||
ipv6Addr := newDNSAddress("2001:db8:1:2:cafe::1337")
|
||||
testCases := []struct {
|
||||
name string
|
||||
recordHeaderName string
|
||||
addr *dnsAddress
|
||||
ttl uint32
|
||||
expected dns.RR
|
||||
}{
|
||||
{
|
||||
name: "IPv4",
|
||||
recordHeaderName: "my.service.dc1.consul.",
|
||||
addr: ipv4Addr,
|
||||
ttl: 123,
|
||||
expected: &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.dc1.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: ipv4Addr.IP(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IPv6",
|
||||
recordHeaderName: "my.service.dc1.consul.",
|
||||
addr: ipv6Addr,
|
||||
ttl: 123,
|
||||
expected: &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.dc1.consul.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
AAAA: ipv6Addr.IP(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := dnsRecordMaker{}.makeIPBasedRecord(tc.recordHeaderName, tc.addr, tc.ttl)
|
||||
require.Equal(t, tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSRecordMaker_makeCNAME(t *testing.T) {
|
||||
name := "my.service.consul."
|
||||
target := "foo"
|
||||
ttl := uint32(123)
|
||||
expected := &dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.consul.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Target: "foo.",
|
||||
}
|
||||
actual := dnsRecordMaker{}.makeCNAME(name, target, ttl)
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestDNSRecordMaker_makeSRV(t *testing.T) {
|
||||
name := "my.service.consul."
|
||||
target := "foo"
|
||||
ttl := uint32(123)
|
||||
expected := &dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Priority: 1,
|
||||
Weight: uint16(345),
|
||||
Port: uint16(234),
|
||||
Target: "foo",
|
||||
}
|
||||
actual := dnsRecordMaker{}.makeSRV(name, target, uint16(345), ttl, &discovery.Port{Number: 234})
|
||||
require.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestDNSRecordMaker_makeTXT(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
metadata map[string]string
|
||||
ttl uint32
|
||||
expected []dns.RR
|
||||
}{
|
||||
{
|
||||
name: "single metadata",
|
||||
metadata: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
ttl: 123,
|
||||
expected: []dns.RR{
|
||||
&dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.consul.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Txt: []string{"key=value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple metadata entries",
|
||||
metadata: map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
ttl: 123,
|
||||
expected: []dns.RR{
|
||||
&dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.consul.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Txt: []string{"key1=value1"},
|
||||
},
|
||||
&dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.consul.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Txt: []string{"key2=value2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "'rfc1035-' prefixed- metadata entry",
|
||||
metadata: map[string]string{
|
||||
"rfc1035-key": "value",
|
||||
},
|
||||
ttl: 123,
|
||||
expected: []dns.RR{
|
||||
&dns.TXT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "my.service.consul.",
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Txt: []string{"value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := dnsRecordMaker{}.makeTXT("my.service.consul.", tc.metadata, tc.ttl)
|
||||
require.ElementsMatchf(t, tc.expected, actual, "expected: %v, actual: %v", tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,656 @@
|
|||
// 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:
|
||||
// We put A/AAAA/CNAME records in the additional section for SRV requests
|
||||
a, e := d.getAnswerExtrasForAddressAndTarget(nodeAddress, serviceAddress, opts)
|
||||
answer = append(answer, a...)
|
||||
extra = append(extra, e...)
|
||||
|
||||
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():
|
||||
a, e := getAnswerExtrasForIP(qName, nodeAddress, opts.req.Question[0],
|
||||
reqType, opts.result, opts.ttl, opts.responseDomain, &opts.port, opts.dnsRecordMaker)
|
||||
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)
|
||||
a, e := getAnswerExtrasForIP(canonicalNodeName, nodeAddress, opts.req.Question[0], reqType,
|
||||
opts.result, opts.ttl, opts.responseDomain, &opts.port, opts.dnsRecordMaker)
|
||||
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)
|
||||
a, e := getAnswerExtrasForIP(canonicalNodeName, nodeAddress, opts.req.Question[0],
|
||||
reqType, opts.result, opts.ttl, opts.responseDomain, &opts.port, opts.dnsRecordMaker)
|
||||
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...)
|
||||
|
||||
// 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)
|
||||
a, e := getAnswerExtrasForIP(canonicalServiceName, serviceAddress,
|
||||
opts.req.Question[0], reqType, opts.result, opts.ttl, opts.responseDomain, &opts.port, opts.dnsRecordMaker)
|
||||
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)
|
||||
a, e := getAnswerExtrasForIP(canonicalNodeName, nodeAddress, opts.req.Question[0],
|
||||
reqType, opts.result, opts.ttl, opts.responseDomain, &opts.port, opts.dnsRecordMaker)
|
||||
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.
|
||||
func getAnswerExtrasForIP(name string, addr *dnsAddress, question dns.Question,
|
||||
reqType requestType, result *discovery.Result, ttl uint32, domain string,
|
||||
port *discovery.Port, maker dnsRecordMaker) (answer []dns.RR, extra []dns.RR) {
|
||||
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 {
|
||||
if result.Type == discovery.ResultTypeService && addr.IsIP() && result.Node.Address != addr.String() {
|
||||
// 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,
|
||||
}
|
||||
}
|
|
@ -1,14 +1,20 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/hashicorp/consul/lib"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/miekg/dns"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -28,8 +34,116 @@ const (
|
|||
maxUDPDatagramSize = math.MaxUint16 - 68
|
||||
)
|
||||
|
||||
// dnsResponseGenerator is used to:
|
||||
// - generate DNS responses for errors
|
||||
// - trim and truncate DNS responses
|
||||
// - EDNS to the response
|
||||
type dnsResponseGenerator struct{}
|
||||
|
||||
// createRefusedResponse returns a REFUSED message. This is the default behavior for unmatched queries in
|
||||
// upstream miekg/dns.
|
||||
func (d dnsResponseGenerator) createRefusedResponse(req *dns.Msg) *dns.Msg {
|
||||
// Return a REFUSED message
|
||||
m := &dns.Msg{}
|
||||
m.SetRcode(req, dns.RcodeRefused)
|
||||
return m
|
||||
}
|
||||
|
||||
// createServerFailureResponse returns a SERVFAIL message.
|
||||
func (d dnsResponseGenerator) createServerFailureResponse(req *dns.Msg, cfg *RouterDynamicConfig, recursionAvailable bool) *dns.Msg {
|
||||
// Return a SERVFAIL message
|
||||
m := &dns.Msg{}
|
||||
m.SetReply(req)
|
||||
m.Compress = !cfg.DisableCompression
|
||||
m.SetRcode(req, dns.RcodeServerFailure)
|
||||
m.RecursionAvailable = recursionAvailable
|
||||
if edns := req.IsEdns0(); edns != nil {
|
||||
d.setEDNS(req, m, true)
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// createAuthoritativeResponse returns an authoritative message that contains the SOA in the event that data is
|
||||
// not return for a query. There can be multiple reasons for not returning data, hence the rcode argument.
|
||||
func (d dnsResponseGenerator) createAuthoritativeResponse(req *dns.Msg, cfg *RouterDynamicConfig, domain string, rcode int, ecsGlobal bool) *dns.Msg {
|
||||
m := &dns.Msg{}
|
||||
m.SetRcode(req, rcode)
|
||||
m.Compress = !cfg.DisableCompression
|
||||
m.Authoritative = true
|
||||
m.RecursionAvailable = canRecurse(cfg)
|
||||
if edns := req.IsEdns0(); edns != nil {
|
||||
d.setEDNS(req, m, ecsGlobal)
|
||||
}
|
||||
|
||||
// We add the SOA on NameErrors
|
||||
maker := &dnsRecordMaker{}
|
||||
soa := maker.makeSOA(domain, cfg)
|
||||
m.Ns = append(m.Ns, soa)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// generateResponseFromErrorOpts is used to pass options to generateResponseFromError.
|
||||
type generateResponseFromErrorOpts struct {
|
||||
req *dns.Msg
|
||||
err error
|
||||
qName string
|
||||
configCtx *RouterDynamicConfig
|
||||
responseDomain string
|
||||
isECSGlobal bool
|
||||
query *discovery.Query
|
||||
canRecurse bool
|
||||
logger hclog.Logger
|
||||
}
|
||||
|
||||
// generateResponseFromError generates a response from an error.
|
||||
func (d dnsResponseGenerator) generateResponseFromError(opts *generateResponseFromErrorOpts) *dns.Msg {
|
||||
switch {
|
||||
case errors.Is(opts.err, errInvalidQuestion):
|
||||
opts.logger.Error("invalid question", "name", opts.qName)
|
||||
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeNameError, opts.isECSGlobal)
|
||||
case errors.Is(opts.err, errNameNotFound):
|
||||
opts.logger.Error("name not found", "name", opts.qName)
|
||||
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeNameError, opts.isECSGlobal)
|
||||
case errors.Is(opts.err, errNotImplemented):
|
||||
opts.logger.Error("query not implemented", "name", opts.qName, "type", dns.Type(opts.req.Question[0].Qtype).String())
|
||||
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeNotImplemented, opts.isECSGlobal)
|
||||
case errors.Is(opts.err, discovery.ErrNotSupported):
|
||||
opts.logger.Debug("query name syntax not supported", "name", opts.req.Question[0].Name)
|
||||
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeNameError, opts.isECSGlobal)
|
||||
case errors.Is(opts.err, discovery.ErrNotFound):
|
||||
opts.logger.Debug("query name not found", "name", opts.req.Question[0].Name)
|
||||
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeNameError, opts.isECSGlobal)
|
||||
case errors.Is(opts.err, discovery.ErrNoData):
|
||||
opts.logger.Debug("no data available", "name", opts.qName)
|
||||
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeSuccess, opts.isECSGlobal)
|
||||
case errors.Is(opts.err, discovery.ErrNoPathToDatacenter):
|
||||
dc := ""
|
||||
if opts.query != nil {
|
||||
dc = opts.query.QueryPayload.Tenancy.Datacenter
|
||||
}
|
||||
opts.logger.Debug("no path to datacenter", "datacenter", dc)
|
||||
return d.createAuthoritativeResponse(opts.req, opts.configCtx, opts.responseDomain, dns.RcodeNameError, opts.isECSGlobal)
|
||||
}
|
||||
opts.logger.Error("error processing discovery query", "error", opts.err)
|
||||
return d.createServerFailureResponse(opts.req, opts.configCtx, opts.canRecurse)
|
||||
}
|
||||
|
||||
// trimDNSResponse will trim the response for UDP and TCP
|
||||
func trimDNSResponse(cfg *RouterDynamicConfig, network string, req, resp *dns.Msg, logger hclog.Logger) {
|
||||
func (d dnsResponseGenerator) trimDNSResponse(cfg *RouterDynamicConfig, remoteAddress net.Addr, req, resp *dns.Msg, logger hclog.Logger) {
|
||||
// Switch to TCP if the client is
|
||||
network := "udp"
|
||||
if _, ok := remoteAddress.(*net.TCPAddr); ok {
|
||||
network = "tcp"
|
||||
}
|
||||
|
||||
var trimmed bool
|
||||
originalSize := resp.Len()
|
||||
originalNumRecords := len(resp.Answer)
|
||||
|
@ -52,6 +166,58 @@ func trimDNSResponse(cfg *RouterDynamicConfig, network string, req, resp *dns.Ms
|
|||
}
|
||||
}
|
||||
|
||||
// setEDNS is used to set the responses EDNS size headers and
|
||||
// possibly the ECS headers as well if they were present in the
|
||||
// original request
|
||||
func (d dnsResponseGenerator) setEDNS(request *dns.Msg, response *dns.Msg, ecsGlobal bool) {
|
||||
edns := request.IsEdns0()
|
||||
if edns == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// cannot just use the SetEdns0 function as we need to embed
|
||||
// the ECS option as well
|
||||
ednsResp := new(dns.OPT)
|
||||
ednsResp.Hdr.Name = "."
|
||||
ednsResp.Hdr.Rrtype = dns.TypeOPT
|
||||
ednsResp.SetUDPSize(edns.UDPSize())
|
||||
|
||||
// Set up the ECS option if present
|
||||
if subnet := ednsSubnetForRequest(request); subnet != nil {
|
||||
subOp := new(dns.EDNS0_SUBNET)
|
||||
subOp.Code = dns.EDNS0SUBNET
|
||||
subOp.Family = subnet.Family
|
||||
subOp.Address = subnet.Address
|
||||
subOp.SourceNetmask = subnet.SourceNetmask
|
||||
if c := response.Rcode; ecsGlobal || c == dns.RcodeNameError || c == dns.RcodeServerFailure || c == dns.RcodeRefused || c == dns.RcodeNotImplemented {
|
||||
// reply is globally valid and should be cached accordingly
|
||||
subOp.SourceScope = 0
|
||||
} else {
|
||||
// reply is only valid for the subnet it was queried with
|
||||
subOp.SourceScope = subnet.SourceNetmask
|
||||
}
|
||||
ednsResp.Option = append(ednsResp.Option, subOp)
|
||||
}
|
||||
|
||||
response.Extra = append(response.Extra, ednsResp)
|
||||
}
|
||||
|
||||
// ednsSubnetForRequest looks through the request to find any EDS subnet options
|
||||
func ednsSubnetForRequest(req *dns.Msg) *dns.EDNS0_SUBNET {
|
||||
// IsEdns0 returns the EDNS RR if present or nil otherwise
|
||||
edns := req.IsEdns0()
|
||||
if edns == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, o := range edns.Option {
|
||||
if subnet, ok := o.(*dns.EDNS0_SUBNET); ok {
|
||||
return subnet
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// trimTCPResponse limit the MaximumSize of messages to 64k as it is the limit
|
||||
// of DNS responses
|
||||
func trimTCPResponse(req, resp *dns.Msg) (trimmed bool) {
|
|
@ -0,0 +1,739 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
)
|
||||
|
||||
func TestDNSResponseGenerator_generateResponseFromError(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
opts *generateResponseFromErrorOpts
|
||||
expectedResponse *dns.Msg
|
||||
}{
|
||||
{
|
||||
name: "error is nil returns server failure",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{},
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: false,
|
||||
Rcode: dns.RcodeServerFailure,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is invalid question returns name error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "invalid-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "invalid-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: errInvalidQuestion,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "invalid-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is name not found returns name error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "invalid-name",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "invalid-name",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: errNameNotFound,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "invalid-name",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is not implemented returns not implemented error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "some-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: errNotImplemented,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNotImplemented,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is not supported returns name error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "some-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: discovery.ErrNotSupported,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is not found returns name error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "some-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: discovery.ErrNotFound,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is no data returns success with soa",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "some-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: discovery.ErrNoData,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is no path to datacenter returns name error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "some-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: discovery.ErrNoPathToDatacenter,
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: []dns.RR{
|
||||
&dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 0,
|
||||
},
|
||||
Ns: "ns.testdomain.",
|
||||
Mbox: "hostmaster.testdomain.",
|
||||
Serial: uint32(time.Now().Unix()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error is something else returns server failure error",
|
||||
opts: &generateResponseFromErrorOpts{
|
||||
req: &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
},
|
||||
qName: "some-question",
|
||||
responseDomain: "testdomain.",
|
||||
logger: testutil.Logger(t),
|
||||
configCtx: &RouterDynamicConfig{
|
||||
DisableCompression: true,
|
||||
},
|
||||
err: errors.New("KABOOM"),
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: false,
|
||||
Rcode: dns.RcodeServerFailure,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "some-question",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassANY,
|
||||
},
|
||||
},
|
||||
Ns: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tc.opts.req.IsEdns0()
|
||||
actualResponse := dnsResponseGenerator{}.generateResponseFromError(tc.opts)
|
||||
require.Equal(t, tc.expectedResponse, actualResponse)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSResponseGenerator_setEDNS(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *dns.Msg
|
||||
response *dns.Msg
|
||||
ecsGlobal bool
|
||||
expectedResponse *dns.Msg
|
||||
}{
|
||||
{
|
||||
name: "request is not edns0, response is not edns0",
|
||||
req: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.OPT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ".",
|
||||
Rrtype: dns.TypeOPT,
|
||||
Class: 4096,
|
||||
Ttl: 0,
|
||||
},
|
||||
Option: []dns.EDNS0{
|
||||
&dns.EDNS0_SUBNET{
|
||||
Code: 1,
|
||||
Family: 2,
|
||||
SourceNetmask: 3,
|
||||
SourceScope: 4,
|
||||
Address: net.ParseIP("255.255.255.255"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.OPT{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: ".",
|
||||
Rrtype: dns.TypeOPT,
|
||||
Class: 4096,
|
||||
Ttl: 0,
|
||||
},
|
||||
Option: []dns.EDNS0{
|
||||
&dns.EDNS0_SUBNET{
|
||||
Code: 8,
|
||||
Family: 2,
|
||||
SourceNetmask: 3,
|
||||
SourceScope: 3,
|
||||
Address: net.ParseIP("255.255.255.255"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dnsResponseGenerator{}.setEDNS(tc.req, tc.response, tc.ecsGlobal)
|
||||
require.Equal(t, tc.expectedResponse, tc.response)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSResponseGenerator_trimDNSResponse(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
req *dns.Msg
|
||||
response *dns.Msg
|
||||
cfg *RouterDynamicConfig
|
||||
remoteAddress net.Addr
|
||||
expectedResponse *dns.Msg
|
||||
}{
|
||||
{
|
||||
name: "network is udp, enable truncate is true, answer count of 1 is less/equal than configured max f 1, response is not trimmed",
|
||||
req: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: &RouterDynamicConfig{
|
||||
UDPAnswerLimit: 1,
|
||||
},
|
||||
remoteAddress: &net.UDPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network is udp, enable truncate is true, answer count of 2 is greater than configure UDP max f 2, response is trimmed",
|
||||
req: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: &RouterDynamicConfig{
|
||||
UDPAnswerLimit: 1,
|
||||
},
|
||||
remoteAddress: &net.UDPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo1.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo2.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("2.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo1.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "network is tcp, enable truncate is true, answer is less than 64k limit, response is not trimmed",
|
||||
req: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
cfg: &RouterDynamicConfig{},
|
||||
remoteAddress: &net.TCPAddr{
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResponse: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
logger := testutil.Logger(t)
|
||||
dnsResponseGenerator{}.trimDNSResponse(tc.cfg, tc.remoteAddress, tc.req, tc.response, logger)
|
||||
require.Equal(t, tc.expectedResponse, tc.response)
|
||||
})
|
||||
|
||||
}
|
||||
}
|
1209
agent/dns/router.go
1209
agent/dns/router.go
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,405 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_ADDR(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "test A 'addr.' query, ipv4 response",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul", // "intentionally missing the trailing dot"
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("192.0.2.10"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test AAAA 'addr.' query, ipv4 response",
|
||||
// Since we asked for an AAAA record, the A record that resolves from the address is attached as an extra
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul",
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("192.0.2.10"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test SRV 'addr.' query, ipv4 response",
|
||||
// Since we asked for a SRV record, the A record that resolves from the address is attached as an extra
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("192.0.2.10"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test ANY 'addr.' query, ipv4 response",
|
||||
// The response to ANY should look the same as the A response
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "c000020a.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("192.0.2.10"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test AAAA 'addr.' query, ipv6 response",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul",
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
AAAA: net.ParseIP("2001:db8:1:2:cafe::1337"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test A 'addr.' query, ipv6 response",
|
||||
// Since we asked for an A record, the AAAA record that resolves from the address is attached as an extra
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
AAAA: net.ParseIP("2001:db8:1:2:cafe::1337"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test SRV 'addr.' query, ipv6 response",
|
||||
// Since we asked for an SRV record, the AAAA record that resolves from the address is attached as an extra
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
AAAA: net.ParseIP("2001:db8:1:2:cafe::1337"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test ANY 'addr.' query, ipv6 response",
|
||||
// The response to ANY should look the same as the AAAA response
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "20010db800010002cafe000000001337.addr.dc1.consul.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
AAAA: net.ParseIP("2001:db8:1:2:cafe::1337"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test malformed 'addr.' query",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000.addr.dc1.consul", // too short
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Rcode: dns.RcodeNameError, // NXDOMAIN
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000.addr.dc1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_NS(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "vanilla NS query",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "consul.",
|
||||
Qtype: dns.TypeNS,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return([]*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-one", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-two", Address: "4.5.6.7"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
}, 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)
|
||||
require.Equal(t, 3, req.Limit)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "consul.",
|
||||
Qtype: dns.TypeNS,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "consul.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "server-one.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "consul.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "server-two.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "server-one.workload.default.ns.default.ap.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.default.ns.default.ap.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("4.5.6.7"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NS query against alternate domain",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "testdomain.",
|
||||
Qtype: dns.TypeNS,
|
||||
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,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return([]*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-one", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-two", Address: "4.5.6.7"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
}, 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)
|
||||
require.Equal(t, 3, req.Limit)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "testdomain.",
|
||||
Qtype: dns.TypeNS,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "server-one.workload.default.ns.default.ap.testdomain.",
|
||||
},
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "server-two.workload.default.ns.default.ap.testdomain.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "server-one.workload.default.ns.default.ap.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.default.ns.default.ap.testdomain.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("4.5.6.7"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_PreparedQuery(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "v1 prepared query w/ TTL override, ANY query, returns A record",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSDomain: "consul",
|
||||
DNSNodeTTL: 123 * time.Second,
|
||||
DNSSOA: config.RuntimeSOAConfig{
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
// We shouldn't use this if we have the override defined
|
||||
DNSServiceTTL: map[string]time.Duration{
|
||||
"foo": 1 * time.Second,
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchPreparedQuery", mock.Anything, mock.Anything).
|
||||
Return([]*discovery.Result{
|
||||
{
|
||||
Service: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Node: &discovery.Location{Name: "bar", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeService,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
TTL: getUint32Ptr(3),
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
}, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
require.Equal(t, "foo", req.Name)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.query.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 3,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 prepared query w/ matching service TTL, ANY query, returns A record",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.dc1.cluster.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSDomain: "consul",
|
||||
DNSNodeTTL: 123 * time.Second,
|
||||
DNSSOA: config.RuntimeSOAConfig{
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
// Results should use this as the TTL
|
||||
DNSServiceTTL: map[string]time.Duration{
|
||||
"foo": 1 * time.Second,
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchPreparedQuery", mock.Anything, mock.Anything).
|
||||
Return([]*discovery.Result{
|
||||
{
|
||||
Service: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Node: &discovery.Location{Name: "bar", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeService,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc1",
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
// Intentionally no TTL here.
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
}, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Equal(t, "dc1", req.Tenancy.Datacenter)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.query.dc1.cluster.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.query.dc1.cluster.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 1,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,563 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_PTR(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "PTR lookup for node, query type is ANY",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Service: &discovery.Location{Name: "bar", Address: "foo"},
|
||||
Type: discovery.ResultTypeNode,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "1.2.3.4", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.node.dc2.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PTR lookup for IPV6 node",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo", Address: "2001:db8::567:89ab"},
|
||||
Service: &discovery.Location{Name: "web", Address: "foo"},
|
||||
Type: discovery.ResultTypeNode,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "2001:db8::567:89ab", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.node.dc2.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PTR lookup for invalid IP address",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "257.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "257.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypeANY,
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "PTR lookup for invalid subdomain",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.blah.arpa",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeNameError,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.blah.arpa.",
|
||||
Qtype: dns.TypeANY,
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[ENT] PTR Lookup for node w/ peer name in default partition, query type is ANY",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeNode,
|
||||
Service: &discovery.Location{Name: "foo-web", Address: "foo"},
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
PeerName: "peer1",
|
||||
Partition: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "1.2.3.4", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.node.peer1.peer.default.ap.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[ENT] PTR Lookup for service in default namespace, query type is PTR",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeService,
|
||||
Service: &discovery.Location{Name: "foo", Address: "foo"},
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "1.2.3.4", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.service.default.dc2.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[ENT] PTR Lookup for service in a non-default namespace, query type is PTR",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-node", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeService,
|
||||
Service: &discovery.Location{Name: "foo", Address: "foo"},
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
Namespace: "bar",
|
||||
Partition: "baz",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "1.2.3.4", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.service.bar.dc2.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[CE] PTR Lookup for node w/ peer name, query type is ANY",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeNode,
|
||||
Service: &discovery.Location{Name: "foo", Address: "foo"},
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
PeerName: "peer1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "1.2.3.4", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.node.peer1.peer.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "[CE] PTR Lookup for service, query type is PTR",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Service: &discovery.Location{Name: "foo", Address: "foo"},
|
||||
Type: discovery.ResultTypeService,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Datacenter: "dc2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchRecordsByIp", mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(net.IP)
|
||||
|
||||
require.NotNil(t, req)
|
||||
require.Equal(t, "1.2.3.4", req.String())
|
||||
})
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Qtype: dns.TypePTR,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.PTR{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ptr: "foo.service.dc2.consul.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,296 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_recursor(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "recursors not configured, non-matching domain",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
// configureRecursor: call not expected.
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Rcode: dns.RcodeRefused,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursors configured, matching domain",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSRecursors: []string{"8.8.8.8"},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureRecursor: func(recursor dnsRecursor) {
|
||||
resp := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "google.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
}
|
||||
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: "google.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "google.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursors configured, no matching domain",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSRecursors: []string{"8.8.8.8"},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureRecursor: func(recursor dnsRecursor) {
|
||||
recursor.(*mockDnsRecursor).On("handle", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(nil, errRecursionFailed)
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: false,
|
||||
Rcode: dns.RcodeServerFailure,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursors configured, unhandled error calling recursors",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSRecursors: []string{"8.8.8.8"},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureRecursor: func(recursor dnsRecursor) {
|
||||
err := errors.New("ahhhhh!!!!")
|
||||
recursor.(*mockDnsRecursor).On("handle", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(nil, err)
|
||||
},
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: false,
|
||||
Rcode: dns.RcodeServerFailure,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "google.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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"},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/hashicorp/consul/agent/structs"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_SOA(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
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{
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-one", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-two", Address: "4.5.6.7"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
}, 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)
|
||||
require.Equal(t, 3, req.Limit)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
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.default.ns.default.ap.consul.",
|
||||
},
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "consul.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "server-two.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "server-one.workload.default.ns.default.ap.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.default.ns.default.ap.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,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return([]*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-one", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &discovery.Location{Name: "server-two", Address: "4.5.6.7"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
}},
|
||||
}, 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)
|
||||
require.Equal(t, 3, req.Limit)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
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.default.ns.default.ap.testdomain.",
|
||||
},
|
||||
&dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "testdomain.",
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Ns: "server-two.workload.default.ns.default.ap.testdomain.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "server-one.workload.default.ns.default.ap.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.default.ns.default.ap.testdomain.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("4.5.6.7"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,628 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/hashicorp/consul/internal/resource"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_V2Services(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "A/AAAA Query a service and return multiple A records",
|
||||
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) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-1", Address: "10.0.0.1"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
// Intentionally not in the mesh
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-2", Address: "10.0.0.2"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
{
|
||||
Name: "mesh",
|
||||
Number: 21000,
|
||||
},
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
reqType := args.Get(2).(discovery.LookupType)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Empty(t, req.PortName)
|
||||
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
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,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("10.0.0.2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SRV Query with a multi-port service return multiple SRV records",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-1", Address: "10.0.0.1"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
// Intentionally not in the mesh
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-2", Address: "10.0.0.2"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
{
|
||||
Name: "mesh",
|
||||
Number: 21000,
|
||||
},
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
reqType := args.Get(2).(discovery.LookupType)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Empty(t, req.PortName)
|
||||
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 2,
|
||||
Priority: 1,
|
||||
Port: 5678,
|
||||
Target: "api.port.foo-1.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 3,
|
||||
Priority: 1,
|
||||
Port: 5678,
|
||||
Target: "api.port.foo-2.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 3,
|
||||
Priority: 1,
|
||||
Port: 21000,
|
||||
Target: "mesh.port.foo-2.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "api.port.foo-1.workload.default.ns.default.ap.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("10.0.0.1"),
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "api.port.foo-2.workload.default.ns.default.ap.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("10.0.0.2"),
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "mesh.port.foo-2.workload.default.ns.default.ap.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("10.0.0.2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SRV Query with a multi-port service where the client requests a specific port, returns SRV and A records",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "mesh.port.foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-2", Address: "10.0.0.2"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "mesh",
|
||||
Number: 21000,
|
||||
},
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 3,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
reqType := args.Get(2).(discovery.LookupType)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Equal(t, "mesh", req.PortName)
|
||||
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "mesh.port.foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "mesh.port.foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 3,
|
||||
Priority: 1,
|
||||
Port: 21000,
|
||||
Target: "mesh.port.foo-2.workload.default.ns.default.ap.consul.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "mesh.port.foo-2.workload.default.ns.default.ap.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("10.0.0.2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SRV Query with a multi-port service that has workloads w/ hostnames (no recursors)",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-1", Address: "foo-1.example.com"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
{
|
||||
Name: "web",
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
reqType := args.Get(2).(discovery.LookupType)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Empty(t, req.PortName)
|
||||
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 2,
|
||||
Priority: 1,
|
||||
Port: 5678,
|
||||
Target: "foo-1.example.com.",
|
||||
},
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 2,
|
||||
Priority: 1,
|
||||
Port: 8080,
|
||||
Target: "foo-1.example.com.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SRV Query with a multi-port service that has workloads w/ hostnames (no recursor)",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
results := []*discovery.Result{
|
||||
{
|
||||
Node: &discovery.Location{Name: "foo-1", Address: "foo-1.example.com"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: resource.DefaultNamespaceName,
|
||||
Partition: resource.DefaultPartitionName,
|
||||
},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
{
|
||||
Name: "web",
|
||||
Number: 8080,
|
||||
},
|
||||
},
|
||||
DNS: discovery.DNSConfig{
|
||||
Weight: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(results, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
reqType := args.Get(2).(discovery.LookupType)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Empty(t, req.PortName)
|
||||
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||
})
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSRecursors: []string{"8.8.8.8"},
|
||||
DNSDomain: "consul",
|
||||
DNSNodeTTL: 123 * time.Second,
|
||||
DNSSOA: config.RuntimeSOAConfig{
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureRecursor: func(recursor dnsRecursor) {
|
||||
resp := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo-1.example.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo-1.example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
}
|
||||
recursor.(*mockDnsRecursor).On("handle",
|
||||
mock.Anything, mock.Anything, mock.Anything).Return(resp, nil)
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.service.consul.",
|
||||
Qtype: dns.TypeSRV,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 2,
|
||||
Priority: 1,
|
||||
Port: 5678,
|
||||
Target: "foo-1.example.com.",
|
||||
},
|
||||
&dns.SRV{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.service.consul.",
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
Weight: 2,
|
||||
Priority: 1,
|
||||
Port: 8080,
|
||||
Target: "foo-1.example.com.",
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo-1.example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
// TODO (v2-dns): This needs to be de-duplicated (NET-8064)
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo-1.example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: uint32(123),
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_Virtual(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "test A 'virtual.' query, ipv4 response",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.virtual.dc1.consul", // "intentionally missing the trailing dot"
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).On("FetchVirtualIP",
|
||||
mock.Anything, mock.Anything).Return(&discovery.Result{
|
||||
Node: &discovery.Location{Address: "240.0.0.2"},
|
||||
Type: discovery.ResultTypeVirtual,
|
||||
}, nil)
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "c000020a.virtual.dc1.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "c000020a.virtual.dc1.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("240.0.0.2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test A 'virtual.' query, ipv6 response",
|
||||
// Since we asked for an A record, the AAAA record that resolves from the address is attached as an extra
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.virtual.dc1.consul",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).On("FetchVirtualIP",
|
||||
mock.Anything, mock.Anything).Return(&discovery.Result{
|
||||
Node: &discovery.Location{Address: "2001:db8:1:2:cafe::1337"},
|
||||
Type: discovery.ResultTypeVirtual,
|
||||
}, nil)
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "20010db800010002cafe000000001337.virtual.dc1.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Extra: []dns.RR{
|
||||
&dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "20010db800010002cafe000000001337.virtual.dc1.consul.",
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
AAAA: net.ParseIP("2001:db8:1:2:cafe::1337"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,515 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/hashicorp/consul/agent/config"
|
||||
"github.com/hashicorp/consul/agent/discovery"
|
||||
)
|
||||
|
||||
func Test_HandleRequest_workloads(t *testing.T) {
|
||||
testCases := []HandleTestCase{
|
||||
{
|
||||
name: "workload A query w/ port, returns A record",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
result := &discovery.Result{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchWorkload", mock.Anything, mock.Anything).
|
||||
Return(result, nil). //TODO
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Equal(t, "api", req.PortName)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workload ANY query w/o port, returns A record",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.workload.consul.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
result := &discovery.Result{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchWorkload", mock.Anything, mock.Anything).
|
||||
Return(result, nil). //TODO
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Empty(t, req.PortName)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.workload.consul.",
|
||||
Qtype: dns.TypeANY,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.workload.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workload A query with namespace, partition, and cluster id; IPV4 address; returns A record",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.workload.bar.ns.baz.ap.dc3.dc.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
result := &discovery.Result{
|
||||
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{
|
||||
Namespace: "bar",
|
||||
Partition: "baz",
|
||||
// We currently don't set the datacenter in any of the V2 results.
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchWorkload", mock.Anything, mock.Anything).
|
||||
Return(result, nil).
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Empty(t, req.PortName)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.workload.bar.ns.baz.ap.dc3.dc.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.workload.bar.ns.baz.ap.dc3.dc.consul.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workload w/hostname address, ANY query (no recursor)",
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
result := &discovery.Result{
|
||||
Node: &discovery.Location{Name: "foo", Address: "foo.example.com"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchWorkload", mock.Anything, mock.Anything).
|
||||
Return(result, nil). //TODO
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Equal(t, "api", req.PortName)
|
||||
})
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Target: "foo.example.com.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workload w/hostname address, ANY query (w/ recursor)",
|
||||
// https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2 both the CNAME and the A record should be in the answer
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
result := &discovery.Result{
|
||||
Node: &discovery.Location{Name: "foo", Address: "foo.example.com"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchWorkload", mock.Anything, mock.Anything).
|
||||
Return(result, nil). //TODO
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Equal(t, "api", req.PortName)
|
||||
})
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSRecursors: []string{"8.8.8.8"},
|
||||
DNSDomain: "consul",
|
||||
DNSNodeTTL: 123 * time.Second,
|
||||
DNSSOA: config.RuntimeSOAConfig{
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureRecursor: func(recursor dnsRecursor) {
|
||||
resp := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.example.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
}
|
||||
recursor.(*mockDnsRecursor).On("handle",
|
||||
mock.Anything, mock.Anything, mock.Anything).Return(resp, nil)
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Target: "foo.example.com.",
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "workload w/hostname address, CNAME query (w/ recursor)",
|
||||
// https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2 only the CNAME should be in the answer
|
||||
request: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeCNAME,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
},
|
||||
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||
result := &discovery.Result{
|
||||
Node: &discovery.Location{Name: "foo", Address: "foo.example.com"},
|
||||
Type: discovery.ResultTypeWorkload,
|
||||
Tenancy: discovery.ResultTenancy{},
|
||||
Ports: []discovery.Port{
|
||||
{
|
||||
Name: "api",
|
||||
Number: 5678,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||
On("FetchWorkload", mock.Anything, mock.Anything).
|
||||
Return(result, nil). //TODO
|
||||
Run(func(args mock.Arguments) {
|
||||
req := args.Get(1).(*discovery.QueryPayload)
|
||||
|
||||
require.Equal(t, "foo", req.Name)
|
||||
require.Equal(t, "api", req.PortName)
|
||||
})
|
||||
},
|
||||
agentConfig: &config.RuntimeConfig{
|
||||
DNSRecursors: []string{"8.8.8.8"},
|
||||
DNSDomain: "consul",
|
||||
DNSNodeTTL: 123 * time.Second,
|
||||
DNSSOA: config.RuntimeSOAConfig{
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
Expire: 3,
|
||||
Minttl: 4,
|
||||
},
|
||||
DNSUDPAnswerLimit: maxUDPAnswerLimit,
|
||||
},
|
||||
configureRecursor: func(recursor dnsRecursor) {
|
||||
resp := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
Rcode: dns.RcodeSuccess,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "foo.example.com.",
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.example.com.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
}
|
||||
recursor.(*mockDnsRecursor).On("handle",
|
||||
mock.Anything, mock.Anything, mock.Anything).Return(resp, nil)
|
||||
},
|
||||
validateAndNormalizeExpected: true,
|
||||
response: &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Compress: true,
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Qtype: dns.TypeCNAME,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "api.port.foo.workload.consul.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
Target: "foo.example.com.",
|
||||
},
|
||||
// TODO (v2-dns): this next record is wrong per the RFC-1034 mentioned in the comment above (NET-8060)
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "foo.example.com.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 123,
|
||||
},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
runHandleTestCases(t, tc)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue