mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
628 lines
15 KiB
628 lines
15 KiB
// 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) |
|
}) |
|
} |
|
}
|
|
|