consul/agent/dns/router_v2_services_test.go

629 lines
15 KiB
Go
Raw Normal View History

// 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 (with 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)
})
}
}