2
0
mirror of https://github.com/status-im/consul.git synced 2025-02-24 03:18:26 +00:00

Parse peer name for virtual IP DNS queries ()

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 2a945facec
commit 3542138e4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 152 additions and 41 deletions
agent
website/content/docs/discovery

@ -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,

@ -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 {

@ -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)
})
}
}

@ -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.