Parse peer name for virtual IP DNS queries (#13602)

This commit updates the DNS query locality parsing so that the virtual
IP for an imported service can be queried.

Note that:
- Support for parsing a peer in other service discovery queries was not
  added.
- Querying another datacenter for a virtual IP is not supported. This
  was technically allowed in 1.11 but is being rolled back for 1.13
  because it is not a use-case we intended to support. Virtual IPs in
  different datacenters are going to collide because they are allocated
  sequentially.
This commit is contained in:
Freddy 2022-07-06 10:30:04 -06:00 committed by GitHub
parent 4ce9651421
commit ed9808c4f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 41 deletions

View File

@ -676,15 +676,34 @@ func (e ecsNotGlobalError) Unwrap() error {
return e.error
}
type queryLocality struct {
// datacenter is the datacenter parsed from a label that has an explicit datacenter part.
// Example query: <service>.virtual.<namespace>.ns.<partition>.ap.<datacenter>.dc.consul
datacenter string
// peerOrDatacenter is parsed from DNS queries where the datacenter and peer name are specified in the same query part.
// Example query: <service>.virtual.<peerOrDatacenter>.consul
peerOrDatacenter string
acl.EnterpriseMeta
}
func (l queryLocality) effectiveDatacenter(defaultDC string) string {
// Prefer the value parsed from a query with explicit parts: <namespace>.ns.<partition>.ap.<datacenter>.dc
if l.datacenter != "" {
return l.datacenter
}
// Fall back to the ambiguously parsed DC or Peer.
if l.peerOrDatacenter != "" {
return l.peerOrDatacenter
}
// If all are empty, use a default value.
return defaultDC
}
// dispatch is used to parse a request and invoke the correct handler.
// parameter maxRecursionLevel will handle whether recursive call can be performed
func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursionLevel int) error {
// By default the query is in the default datacenter
datacenter := d.agent.config.Datacenter
// have to deref to clone it so we don't modify (start from the agent's defaults)
var entMeta = d.defaultEnterpriseMeta
// Choose correct response domain
respDomain := d.getResponseDomain(req.Question[0].Name)
@ -733,16 +752,17 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid()
}
if !d.parseDatacenterAndEnterpriseMeta(querySuffixes, cfg, &datacenter, &entMeta) {
locality, ok := d.parseLocality(querySuffixes, cfg)
if !ok {
return invalid()
}
lookup := serviceLookup{
Datacenter: datacenter,
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
Connect: false,
Ingress: false,
MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: entMeta,
EnterpriseMeta: locality.EnterpriseMeta,
}
// Support RFC 2782 style syntax
if n == 2 && strings.HasPrefix(queryParts[1], "_") && strings.HasPrefix(queryParts[0], "_") {
@ -779,17 +799,18 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid()
}
if !d.parseDatacenterAndEnterpriseMeta(querySuffixes, cfg, &datacenter, &entMeta) {
locality, ok := d.parseLocality(querySuffixes, cfg)
if !ok {
return invalid()
}
lookup := serviceLookup{
Datacenter: datacenter,
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
Service: queryParts[len(queryParts)-1],
Connect: true,
Ingress: false,
MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: entMeta,
EnterpriseMeta: locality.EnterpriseMeta,
}
// name.connect.consul
return d.serviceLookup(cfg, lookup, req, resp)
@ -799,14 +820,18 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid()
}
if !d.parseDatacenterAndEnterpriseMeta(querySuffixes, cfg, &datacenter, &entMeta) {
locality, ok := d.parseLocality(querySuffixes, cfg)
if !ok {
return invalid()
}
args := structs.ServiceSpecificRequest{
Datacenter: datacenter,
// The datacenter of the request is not specified because cross-datacenter virtual IP
// queries are not supported. This guard rail is in place because virtual IPs are allocated
// within a DC, therefore their uniqueness is not guaranteed globally.
PeerName: locality.peerOrDatacenter,
ServiceName: queryParts[len(queryParts)-1],
EnterpriseMeta: entMeta,
EnterpriseMeta: locality.EnterpriseMeta,
QueryOptions: structs.QueryOptions{
Token: d.agent.tokens.UserToken(),
},
@ -834,17 +859,18 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid()
}
if !d.parseDatacenterAndEnterpriseMeta(querySuffixes, cfg, &datacenter, &entMeta) {
locality, ok := d.parseLocality(querySuffixes, cfg)
if !ok {
return invalid()
}
lookup := serviceLookup{
Datacenter: datacenter,
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
Service: queryParts[len(queryParts)-1],
Connect: false,
Ingress: true,
MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: entMeta,
EnterpriseMeta: locality.EnterpriseMeta,
}
// name.ingress.consul
return d.serviceLookup(cfg, lookup, req, resp)
@ -854,13 +880,14 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
return invalid()
}
if !d.parseDatacenterAndEnterpriseMeta(querySuffixes, cfg, &datacenter, &entMeta) {
locality, ok := d.parseLocality(querySuffixes, cfg)
if !ok {
return invalid()
}
// Namespace should not be set for node queries
ns := entMeta.NamespaceOrEmpty()
if ns != "" && ns != acl.DefaultNamespaceName {
// Nodes are only registered in the default namespace so queries
// must not specify a non-default namespace.
if !locality.InDefaultNamespace() {
return invalid()
}
@ -868,15 +895,17 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
node := strings.Join(queryParts, ".")
lookup := nodeLookup{
Datacenter: datacenter,
Datacenter: locality.effectiveDatacenter(d.agent.config.Datacenter),
Node: node,
MaxRecursionLevel: maxRecursionLevel,
EnterpriseMeta: entMeta,
EnterpriseMeta: locality.EnterpriseMeta,
}
return d.nodeLookup(cfg, lookup, req, resp)
case "query":
datacenter := d.agent.config.Datacenter
// ensure we have a query name
if len(queryParts) < 1 {
return invalid()
@ -905,7 +934,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
if err != nil {
return invalid()
}
//check if the query type is A for IPv4 or ANY
// check if the query type is A for IPv4 or ANY
aRecord := &dns.A{
Hdr: dns.RR_Header{
Name: qName + respDomain,
@ -926,7 +955,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi
if err != nil {
return invalid()
}
//check if the query type is AAAA for IPv6 or ANY
// check if the query type is AAAA for IPv6 or ANY
aaaaRecord := &dns.AAAA{
Hdr: dns.RR_Header{
Name: qName + respDomain,

View File

@ -16,15 +16,19 @@ func getEnterpriseDNSConfig(conf *config.RuntimeConfig) enterpriseDNSConfig {
return enterpriseDNSConfig{}
}
func (d *DNSServer) parseDatacenterAndEnterpriseMeta(labels []string, _ *dnsConfig, datacenter *string, _ *acl.EnterpriseMeta) bool {
// parseLocality can parse peer name or datacenter from a DNS query's labels.
// Peer name is parsed from the same query part that datacenter is, so given this ambiguity
// we parse a "peerOrDatacenter". The caller or RPC handler are responsible for disambiguating.
func (d *DNSServer) parseLocality(labels []string, cfg *dnsConfig) (queryLocality, bool) {
switch len(labels) {
case 1:
*datacenter = labels[0]
return true
return queryLocality{peerOrDatacenter: labels[0]}, true
case 0:
return true
return queryLocality{}, true
}
return false
return queryLocality{}, false
}
func serviceCanonicalDNSName(name, kind, datacenter, domain string, _ *acl.EnterpriseMeta) string {

View File

@ -11,6 +11,7 @@ import (
"testing"
"time"
"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/serf/coordinate"
"github.com/miekg/dns"
"github.com/stretchr/testify/require"
@ -458,7 +459,7 @@ func TestDNSCycleRecursorCheck(t *testing.T) {
},
})
defer server2.Shutdown()
//Mock the agent startup with the necessary configs
// Mock the agent startup with the necessary configs
agent := NewTestAgent(t,
`recursors = ["`+server1.Addr+`", "`+server2.Addr+`"]
`)
@ -496,7 +497,7 @@ func TestDNSCycleRecursorCheckAllFail(t *testing.T) {
MsgHdr: dns.MsgHdr{Rcode: dns.RcodeRefused},
})
defer server3.Shutdown()
//Mock the agent startup with the necessary configs
// Mock the agent startup with the necessary configs
agent := NewTestAgent(t,
`recursors = ["`+server1.Addr+`", "`+server2.Addr+`","`+server3.Addr+`"]
`)
@ -507,7 +508,7 @@ func TestDNSCycleRecursorCheckAllFail(t *testing.T) {
// Agent request
client := new(dns.Client)
in, _, _ := client.Exchange(m, agent.DNSAddr())
//Verify if we hit SERVFAIL from Consul
// Verify if we hit SERVFAIL from Consul
require.Equal(t, dns.RcodeServerFailure, in.Rcode)
}
func TestDNS_NodeLookup_CNAME(t *testing.T) {
@ -1756,14 +1757,42 @@ func TestDNS_ConnectServiceLookup(t *testing.T) {
require.Equal(t, uint32(0), srvRec.Hdr.Ttl)
require.Equal(t, "127.0.0.55", cnameRec.A.String())
}
}
// Look up the virtual IP of the proxy.
questions = []string{
"db.virtual.consul.",
func TestDNS_VirtualIPLookup(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
for _, question := range questions {
t.Parallel()
a := NewTestAgent(t, "")
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
server, ok := a.delegate.(*consul.Server)
require.True(t, ok)
// The proxy service will not receive a virtual IP if the server is not assigning virtual IPs yet.
retry.Run(t, func(r *retry.R) {
_, entry, err := server.FSM().State().SystemMetadataGet(nil, structs.SystemMetadataVirtualIPsEnabled)
require.NoError(r, err)
require.NotNil(r, entry)
})
type testCase struct {
name string
reg *structs.RegisterRequest
question string
expect string
}
run := func(t *testing.T, tc testCase) {
var out struct{}
require.Nil(t, a.RPC("Catalog.Register", tc.reg, &out))
m := new(dns.Msg)
m.SetQuestion(question, dns.TypeA)
m.SetQuestion(tc.question, dns.TypeA)
c := new(dns.Client)
in, _, err := c.Exchange(m, a.DNSAddr())
@ -1772,7 +1801,54 @@ func TestDNS_ConnectServiceLookup(t *testing.T) {
aRec, ok := in.Answer[0].(*dns.A)
require.True(t, ok)
require.Equal(t, "240.0.0.1", aRec.A.String())
require.Equal(t, tc.expect, aRec.A.String())
}
tt := []testCase{
{
name: "local query",
reg: &structs.RegisterRequest{
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.55",
Service: &structs.NodeService{
Kind: structs.ServiceKindConnectProxy,
Service: "web-proxy",
Port: 12345,
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "db",
},
},
},
question: "db.virtual.consul.",
expect: "240.0.0.1",
},
{
name: "query for imported service",
reg: &structs.RegisterRequest{
PeerName: "frontend",
Datacenter: "dc1",
Node: "foo",
Address: "127.0.0.55",
Service: &structs.NodeService{
PeerName: "frontend",
Kind: structs.ServiceKindConnectProxy,
Service: "web-proxy",
Port: 12345,
Proxy: structs.ConnectProxyConfig{
DestinationServiceName: "db",
},
},
},
question: "db.virtual.frontend.consul.",
expect: "240.0.0.2",
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
run(t, tc)
})
}
}

View File

@ -376,12 +376,14 @@ If you need more complex behavior, please use the
To find the unique virtual IP allocated for a service:
```text
<service>.virtual.<domain>
<service>.virtual[.peer].<domain>
```
This will return the unique virtual IP for any [Connect-capable](/docs/connect)
service. Each Connect service has a virtual IP assigned to it by Consul - this is used
by sidecar proxies for the [Transparent Proxy](/docs/connect/transparent-proxy) feature.
The peer name is an optional part of the FQDN, and it is used to query for the virtual IP
of a service imported from that peer.
The virtual IP is also added to the service's [Tagged Addresses](/docs/discovery/services#tagged-addresses)
under the `consul-virtual` tag.