mirror of https://github.com/hashicorp/consul
feat(dataplane): allow token and tenancy information for proxied DNS (#20899)
* feat(dataplane): allow token and tenancy information for proxied DNS * changelogpull/21008/head
parent
057ad7e952
commit
03ab7367a6
|
@ -0,0 +1,4 @@
|
||||||
|
```release-note:improvement
|
||||||
|
dns: DNS-over-grpc when using `consul-dataplane` now accepts partition, namespace, token as metadata to default those query parameters.
|
||||||
|
`consul-dataplane` v1.5+ will send this information automatically.
|
||||||
|
```
|
|
@ -116,11 +116,17 @@ func (f *V1DataFetcher) FetchNodes(ctx Context, req *QueryPayload) ([]*Result, e
|
||||||
// Nodes are not namespaced, so this is a name error
|
// Nodes are not namespaced, so this is a name error
|
||||||
return nil, ErrNotFound
|
return nil, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := f.dynamicConfig.Load().(*V1DataFetcherDynamicConfig)
|
cfg := f.dynamicConfig.Load().(*V1DataFetcherDynamicConfig)
|
||||||
|
|
||||||
|
// If no datacenter is passed, default to our own
|
||||||
|
datacenter := cfg.Datacenter
|
||||||
|
if req.Tenancy.Datacenter != "" {
|
||||||
|
datacenter = req.Tenancy.Datacenter
|
||||||
|
}
|
||||||
|
|
||||||
// Make an RPC request
|
// Make an RPC request
|
||||||
args := &structs.NodeSpecificRequest{
|
args := &structs.NodeSpecificRequest{
|
||||||
Datacenter: req.Tenancy.Datacenter,
|
Datacenter: datacenter,
|
||||||
PeerName: req.Tenancy.Peer,
|
PeerName: req.Tenancy.Peer,
|
||||||
Node: req.Name,
|
Node: req.Name,
|
||||||
QueryOptions: structs.QueryOptions{
|
QueryOptions: structs.QueryOptions{
|
||||||
|
@ -299,9 +305,15 @@ func (f *V1DataFetcher) FetchWorkload(ctx Context, req *QueryPayload) (*Result,
|
||||||
func (f *V1DataFetcher) FetchPreparedQuery(ctx Context, req *QueryPayload) ([]*Result, error) {
|
func (f *V1DataFetcher) FetchPreparedQuery(ctx Context, req *QueryPayload) ([]*Result, error) {
|
||||||
cfg := f.dynamicConfig.Load().(*V1DataFetcherDynamicConfig)
|
cfg := f.dynamicConfig.Load().(*V1DataFetcherDynamicConfig)
|
||||||
|
|
||||||
|
// If no datacenter is passed, default to our own
|
||||||
|
datacenter := cfg.Datacenter
|
||||||
|
if req.Tenancy.Datacenter != "" {
|
||||||
|
datacenter = req.Tenancy.Datacenter
|
||||||
|
}
|
||||||
|
|
||||||
// Execute the prepared query.
|
// Execute the prepared query.
|
||||||
args := structs.PreparedQueryExecuteRequest{
|
args := structs.PreparedQueryExecuteRequest{
|
||||||
Datacenter: req.Tenancy.Datacenter,
|
Datacenter: datacenter,
|
||||||
QueryIDOrName: req.Name,
|
QueryIDOrName: req.Name,
|
||||||
QueryOptions: structs.QueryOptions{
|
QueryOptions: structs.QueryOptions{
|
||||||
Token: ctx.Token,
|
Token: ctx.Token,
|
||||||
|
@ -548,7 +560,11 @@ func (f *V1DataFetcher) fetchServiceBasedOnTenancy(ctx Context, req *QueryPayloa
|
||||||
return nil, errors.New("sameness groups are not allowed for service lookups based on tenancy")
|
return nil, errors.New("sameness groups are not allowed for service lookups based on tenancy")
|
||||||
}
|
}
|
||||||
|
|
||||||
datacenter := req.Tenancy.Datacenter
|
// If no datacenter is passed, default to our own
|
||||||
|
datacenter := cfg.Datacenter
|
||||||
|
if req.Tenancy.Datacenter != "" {
|
||||||
|
datacenter = req.Tenancy.Datacenter
|
||||||
|
}
|
||||||
if req.Tenancy.Peer != "" {
|
if req.Tenancy.Peer != "" {
|
||||||
datacenter = ""
|
datacenter = ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,7 @@ func Test_FetchVirtualIP(t *testing.T) {
|
||||||
func Test_FetchEndpoints(t *testing.T) {
|
func Test_FetchEndpoints(t *testing.T) {
|
||||||
// set these to confirm that RPC call does not use them for this particular RPC
|
// set these to confirm that RPC call does not use them for this particular RPC
|
||||||
rc := &config.RuntimeConfig{
|
rc := &config.RuntimeConfig{
|
||||||
|
Datacenter: "dc2",
|
||||||
DNSAllowStale: true,
|
DNSAllowStale: true,
|
||||||
DNSMaxStale: 100,
|
DNSMaxStale: 100,
|
||||||
DNSUseCache: true,
|
DNSUseCache: true,
|
||||||
|
|
|
@ -347,6 +347,7 @@ func queryTenancyToResourceTenancy(qTenancy QueryTenancy) *pbresource.Tenancy {
|
||||||
rTenancy.Namespace = qTenancy.Namespace
|
rTenancy.Namespace = qTenancy.Namespace
|
||||||
}
|
}
|
||||||
// In the case of partition, we have the agent's partition as the fallback.
|
// In the case of partition, we have the agent's partition as the fallback.
|
||||||
|
// That is handled in NormalizeRequest.
|
||||||
if qTenancy.Partition != "" {
|
if qTenancy.Partition != "" {
|
||||||
rTenancy.Partition = qTenancy.Partition
|
rTenancy.Partition = qTenancy.Partition
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context is used augment a DNS message with Consul-specific metadata.
|
||||||
|
type Context struct {
|
||||||
|
Token string `mapstructure:"x-consul-token,omitempty"`
|
||||||
|
DefaultNamespace string `mapstructure:"x-consul-namespace,omitempty"`
|
||||||
|
DefaultPartition string `mapstructure:"x-consul-partition,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContextFromGRPCContext returns the request context using the gRPC metadata attached to the
|
||||||
|
// given context. If there is no gRPC metadata, it returns an empty context.
|
||||||
|
func NewContextFromGRPCContext(ctx context.Context) (Context, error) {
|
||||||
|
if ctx == nil {
|
||||||
|
return Context{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCtx := Context{}
|
||||||
|
md, ok := metadata.FromIncomingContext(ctx)
|
||||||
|
if !ok {
|
||||||
|
return reqCtx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m := map[string]string{}
|
||||||
|
for k, v := range md {
|
||||||
|
m[k] = v[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
decoderConfig := &mapstructure.DecoderConfig{
|
||||||
|
Metadata: nil,
|
||||||
|
Result: &reqCtx,
|
||||||
|
WeaklyTypedInput: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := mapstructure.NewDecoder(decoderConfig)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, fmt.Errorf("error creating mapstructure decoder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = decoder.Decode(m)
|
||||||
|
if err != nil {
|
||||||
|
return Context{}, fmt.Errorf("error decoding metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqCtx, nil
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: BUSL-1.1
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewContextFromGRPCContext(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
md := metadata.MD{}
|
||||||
|
testMeta := map[string]string{
|
||||||
|
"x-consul-token": "test-token",
|
||||||
|
"x-consul-namespace": "test-namespace",
|
||||||
|
"x-consul-partition": "test-partition",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range testMeta {
|
||||||
|
md.Set(k, v)
|
||||||
|
}
|
||||||
|
testGRPCContext := metadata.NewIncomingContext(context.Background(), md)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
grpcCtx context.Context
|
||||||
|
expected *Context
|
||||||
|
error error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil grpc context",
|
||||||
|
grpcCtx: nil,
|
||||||
|
expected: &Context{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grpc context w/o metadata",
|
||||||
|
grpcCtx: context.Background(),
|
||||||
|
expected: &Context{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "grpc context w/ kitchen sink",
|
||||||
|
grpcCtx: testGRPCContext,
|
||||||
|
expected: &Context{
|
||||||
|
Token: "test-token",
|
||||||
|
DefaultNamespace: "test-namespace",
|
||||||
|
DefaultPartition: "test-partition",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ctx, err := NewContextFromGRPCContext(tc.grpcCtx)
|
||||||
|
if tc.error != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, Context{}, &ctx)
|
||||||
|
require.Equal(t, tc.error, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, ctx)
|
||||||
|
require.Equal(t, tc.expected, &ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -206,20 +206,24 @@ func getQueryTenancy(reqCtx Context, queryType discovery.QueryType, querySuffixe
|
||||||
return discovery.QueryTenancy{}, errNameNotFound
|
return discovery.QueryTenancy{}, errNameNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't have an explicit partition in the request, try the first fallback
|
// If we don't have an explicit partition/ns in the request, try the first fallback
|
||||||
// which was supplied in the request context. The agent's partition will be used as the last fallback
|
// which was supplied in the request context. The agent's partition will be used as the last fallback
|
||||||
// later in the query processor.
|
// later in the query processor.
|
||||||
if labels.Partition == "" {
|
if labels.Partition == "" {
|
||||||
labels.Partition = reqCtx.DefaultPartition
|
labels.Partition = reqCtx.DefaultPartition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if labels.Namespace == "" {
|
||||||
|
labels.Namespace = reqCtx.DefaultNamespace
|
||||||
|
}
|
||||||
|
|
||||||
// If we have a sameness group, we can return early without further data massage.
|
// If we have a sameness group, we can return early without further data massage.
|
||||||
if labels.SamenessGroup != "" {
|
if labels.SamenessGroup != "" {
|
||||||
return discovery.QueryTenancy{
|
return discovery.QueryTenancy{
|
||||||
Namespace: labels.Namespace,
|
Namespace: labels.Namespace,
|
||||||
Partition: labels.Partition,
|
Partition: labels.Partition,
|
||||||
SamenessGroup: labels.SamenessGroup,
|
SamenessGroup: labels.SamenessGroup,
|
||||||
Datacenter: reqCtx.DefaultDatacenter,
|
// Datacenter is not supported
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,19 +238,19 @@ func getQueryTenancy(reqCtx Context, queryType discovery.QueryType, querySuffixe
|
||||||
Namespace: labels.Namespace,
|
Namespace: labels.Namespace,
|
||||||
Partition: labels.Partition,
|
Partition: labels.Partition,
|
||||||
Peer: labels.Peer,
|
Peer: labels.Peer,
|
||||||
Datacenter: getEffectiveDatacenter(labels, reqCtx.DefaultDatacenter),
|
Datacenter: getEffectiveDatacenter(labels),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEffectiveDatacenter returns the effective datacenter from the parsed labels.
|
// getEffectiveDatacenter returns the effective datacenter from the parsed labels.
|
||||||
func getEffectiveDatacenter(labels *parsedLabels, defaultDC string) string {
|
func getEffectiveDatacenter(labels *parsedLabels) string {
|
||||||
switch {
|
switch {
|
||||||
case labels.Datacenter != "":
|
case labels.Datacenter != "":
|
||||||
return labels.Datacenter
|
return labels.Datacenter
|
||||||
case labels.PeerOrDatacenter != "" && labels.Peer != labels.PeerOrDatacenter:
|
case labels.PeerOrDatacenter != "" && labels.Peer != labels.PeerOrDatacenter:
|
||||||
return labels.PeerOrDatacenter
|
return labels.PeerOrDatacenter
|
||||||
}
|
}
|
||||||
return defaultDC
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// getQueryTypePartsAndSuffixesFromDNSMessage returns the query type, the parts, and suffixes of the query name.
|
// getQueryTypePartsAndSuffixesFromDNSMessage returns the query type, the parts, and suffixes of the query name.
|
||||||
|
|
|
@ -160,8 +160,7 @@ func Test_buildQueryFromDNSMessage(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
requestContext: &Context{
|
requestContext: &Context{
|
||||||
DefaultDatacenter: "default-dc",
|
DefaultPartition: "default-partition",
|
||||||
DefaultPartition: "default-partition",
|
|
||||||
},
|
},
|
||||||
expectedQuery: &discovery.Query{
|
expectedQuery: &discovery.Query{
|
||||||
QueryType: discovery.QueryTypeWorkload,
|
QueryType: discovery.QueryTypeWorkload,
|
||||||
|
@ -169,10 +168,9 @@ func Test_buildQueryFromDNSMessage(t *testing.T) {
|
||||||
Name: "foo",
|
Name: "foo",
|
||||||
PortName: "api",
|
PortName: "api",
|
||||||
Tenancy: discovery.QueryTenancy{
|
Tenancy: discovery.QueryTenancy{
|
||||||
Namespace: "banana",
|
Namespace: "banana",
|
||||||
Partition: "orange",
|
Partition: "orange",
|
||||||
Peer: "apple",
|
Peer: "apple",
|
||||||
Datacenter: "default-dc",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -192,8 +190,7 @@ func Test_buildQueryFromDNSMessage(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
requestContext: &Context{
|
requestContext: &Context{
|
||||||
DefaultDatacenter: "default-dc",
|
DefaultPartition: "default-partition",
|
||||||
DefaultPartition: "default-partition",
|
|
||||||
},
|
},
|
||||||
expectedQuery: &discovery.Query{
|
expectedQuery: &discovery.Query{
|
||||||
QueryType: discovery.QueryTypeService,
|
QueryType: discovery.QueryTypeService,
|
||||||
|
@ -203,7 +200,6 @@ func Test_buildQueryFromDNSMessage(t *testing.T) {
|
||||||
Namespace: "banana",
|
Namespace: "banana",
|
||||||
Partition: "orange",
|
Partition: "orange",
|
||||||
SamenessGroup: "apple",
|
SamenessGroup: "apple",
|
||||||
Datacenter: "default-dc",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,9 +14,8 @@ import (
|
||||||
|
|
||||||
"github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
"github.com/armon/go-radix"
|
"github.com/armon/go-radix"
|
||||||
"github.com/miekg/dns"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
"github.com/hashicorp/consul/agent/config"
|
"github.com/hashicorp/consul/agent/config"
|
||||||
"github.com/hashicorp/consul/agent/discovery"
|
"github.com/hashicorp/consul/agent/discovery"
|
||||||
|
@ -46,13 +45,6 @@ var (
|
||||||
trailingSpacesRE = regexp.MustCompile(" +$")
|
trailingSpacesRE = regexp.MustCompile(" +$")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Context is used augment a DNS message with Consul-specific metadata.
|
|
||||||
type Context struct {
|
|
||||||
Token string
|
|
||||||
DefaultPartition string
|
|
||||||
DefaultDatacenter string
|
|
||||||
}
|
|
||||||
|
|
||||||
// RouterDynamicConfig is the dynamic configuration that can be hot-reloaded
|
// RouterDynamicConfig is the dynamic configuration that can be hot-reloaded
|
||||||
type RouterDynamicConfig struct {
|
type RouterDynamicConfig struct {
|
||||||
ARecordLimit int
|
ARecordLimit int
|
||||||
|
@ -114,13 +106,12 @@ type dnsRecursor interface {
|
||||||
// Router replaces miekg/dns.ServeMux with a simpler router that only checks for the 2-3 valid domains
|
// Router replaces miekg/dns.ServeMux with a simpler router that only checks for the 2-3 valid domains
|
||||||
// that Consul supports and forwards to a single DiscoveryQueryProcessor handler. If there is no match, it will recurse.
|
// that Consul supports and forwards to a single DiscoveryQueryProcessor handler. If there is no match, it will recurse.
|
||||||
type Router struct {
|
type Router struct {
|
||||||
processor DiscoveryQueryProcessor
|
processor DiscoveryQueryProcessor
|
||||||
recursor dnsRecursor
|
recursor dnsRecursor
|
||||||
domain string
|
domain string
|
||||||
altDomain string
|
altDomain string
|
||||||
datacenter string
|
nodeName string
|
||||||
nodeName string
|
logger hclog.Logger
|
||||||
logger hclog.Logger
|
|
||||||
|
|
||||||
tokenFunc func() string
|
tokenFunc func() string
|
||||||
translateAddressFunc func(dc string, addr string, taggedAddresses map[string]string, accept dnsutil.TranslateAddressAccept) string
|
translateAddressFunc func(dc string, addr string, taggedAddresses map[string]string, accept dnsutil.TranslateAddressAccept) string
|
||||||
|
@ -146,7 +137,6 @@ func NewRouter(cfg Config) (*Router, error) {
|
||||||
recursor: newRecursor(logger),
|
recursor: newRecursor(logger),
|
||||||
domain: domain,
|
domain: domain,
|
||||||
altDomain: altDomain,
|
altDomain: altDomain,
|
||||||
datacenter: cfg.AgentConfig.Datacenter,
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
nodeName: cfg.AgentConfig.NodeName,
|
nodeName: cfg.AgentConfig.NodeName,
|
||||||
tokenFunc: cfg.TokenFunc,
|
tokenFunc: cfg.TokenFunc,
|
||||||
|
@ -176,6 +166,7 @@ func (r *Router) HandleRequest(req *dns.Msg, reqCtx Context, remoteAddress net.A
|
||||||
}
|
}
|
||||||
|
|
||||||
r.logger.Trace("received request", "question", req.Question[0].Name, "type", dns.Type(req.Question[0].Qtype).String())
|
r.logger.Trace("received request", "question", req.Question[0].Name, "type", dns.Type(req.Question[0].Qtype).String())
|
||||||
|
r.normalizeContext(&reqCtx)
|
||||||
|
|
||||||
defer func(s time.Time, q dns.Question) {
|
defer func(s time.Time, q dns.Question) {
|
||||||
metrics.MeasureSinceWithLabels([]string{"dns", "query"}, s,
|
metrics.MeasureSinceWithLabels([]string{"dns", "query"}, s,
|
||||||
|
@ -319,10 +310,9 @@ func (r *Router) trimDomain(questionName string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeDNS implements the miekg/dns.Handler interface.
|
// ServeDNS implements the miekg/dns.Handler interface.
|
||||||
// This is a standard DNS listener, so we inject a default request context based on the agent's config.
|
// This is a standard DNS listener.
|
||||||
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||||
reqCtx := r.defaultAgentDNSRequestContext()
|
out := r.HandleRequest(req, Context{}, w.RemoteAddr())
|
||||||
out := r.HandleRequest(req, reqCtx, w.RemoteAddr())
|
|
||||||
w.WriteMsg(out)
|
w.WriteMsg(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -420,16 +410,6 @@ func (r *Router) GetConfig() *RouterDynamicConfig {
|
||||||
return r.dynamicConfig.Load().(*RouterDynamicConfig)
|
return r.dynamicConfig.Load().(*RouterDynamicConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultAgentDNSRequestContext returns a default request context based on the agent's config.
|
|
||||||
func (r *Router) defaultAgentDNSRequestContext() Context {
|
|
||||||
return Context{
|
|
||||||
Token: r.tokenFunc(),
|
|
||||||
DefaultDatacenter: r.datacenter,
|
|
||||||
// We don't need to specify the agent's partition here because that will be handled further down the stack
|
|
||||||
// in the query processor.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getErrorFromECSNotGlobalError returns the underlying error from an ECSNotGlobalError, if it exists.
|
// getErrorFromECSNotGlobalError returns the underlying error from an ECSNotGlobalError, if it exists.
|
||||||
func getErrorFromECSNotGlobalError(err error) error {
|
func getErrorFromECSNotGlobalError(err error) error {
|
||||||
if errors.Is(err, discovery.ErrECSNotGlobal) {
|
if errors.Is(err, discovery.ErrECSNotGlobal) {
|
||||||
|
@ -471,6 +451,16 @@ func validateAndNormalizeRequest(req *dns.Msg) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// normalizeContext makes sure context information is populated with agent defaults as needed.
|
||||||
|
// Right now this is just the ACL token. We do this in the router with the token because DNS doesn't
|
||||||
|
// allow a token to be passed in the request, and we expect ACL tokens upfront in APIs when they are enabled.
|
||||||
|
// Tenancy information is left out because it is safe/expected to assume agent defaults in the backend lookup.
|
||||||
|
func (r *Router) normalizeContext(ctx *Context) {
|
||||||
|
if ctx.Token == "" {
|
||||||
|
ctx.Token = r.tokenFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// stripAnyFailoverSuffix strips off the suffixes that may have been added to the request name.
|
// stripAnyFailoverSuffix strips off the suffixes that may have been added to the request name.
|
||||||
func stripAnyFailoverSuffix(target string) (string, bool) {
|
func stripAnyFailoverSuffix(target string) (string, bool) {
|
||||||
enableFailover := false
|
enableFailover := false
|
||||||
|
|
|
@ -52,22 +52,6 @@ type HandleTestCase struct {
|
||||||
response *dns.Msg
|
response *dns.Msg
|
||||||
}
|
}
|
||||||
|
|
||||||
var testSOA = &dns.SOA{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: "consul.",
|
|
||||||
Rrtype: dns.TypeSOA,
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
Ttl: 4,
|
|
||||||
},
|
|
||||||
Ns: "ns.consul.",
|
|
||||||
Mbox: "hostmaster.consul.",
|
|
||||||
Serial: uint32(time.Now().Unix()),
|
|
||||||
Refresh: 1,
|
|
||||||
Retry: 2,
|
|
||||||
Expire: 3,
|
|
||||||
Minttl: 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_HandleRequest_Validation(t *testing.T) {
|
func Test_HandleRequest_Validation(t *testing.T) {
|
||||||
testCases := []HandleTestCase{
|
testCases := []HandleTestCase{
|
||||||
{
|
{
|
||||||
|
@ -93,6 +77,157 @@ func Test_HandleRequest_Validation(t *testing.T) {
|
||||||
Extra: nil,
|
Extra: nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Context Tests
|
||||||
|
{
|
||||||
|
name: "When a request context is provided, use those field in the query",
|
||||||
|
request: &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Opcode: dns.OpcodeQuery,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{
|
||||||
|
{
|
||||||
|
Name: "foo.service.consul.",
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContext: &Context{
|
||||||
|
Token: "test-token",
|
||||||
|
DefaultNamespace: "test-namespace",
|
||||||
|
DefaultPartition: "test-partition",
|
||||||
|
},
|
||||||
|
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||||
|
result := []*discovery.Result{
|
||||||
|
{
|
||||||
|
Type: discovery.ResultTypeNode,
|
||||||
|
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||||
|
Tenancy: discovery.ResultTenancy{
|
||||||
|
Namespace: "test-namespace",
|
||||||
|
Partition: "test-partition",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||||
|
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(result, nil).
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
ctx := args.Get(0).(discovery.Context)
|
||||||
|
req := args.Get(1).(*discovery.QueryPayload)
|
||||||
|
reqType := args.Get(2).(discovery.LookupType)
|
||||||
|
|
||||||
|
require.Equal(t, "test-token", ctx.Token)
|
||||||
|
|
||||||
|
require.Equal(t, "foo", req.Name)
|
||||||
|
require.Equal(t, "test-namespace", req.Tenancy.Namespace)
|
||||||
|
require.Equal(t, "test-partition", req.Tenancy.Partition)
|
||||||
|
|
||||||
|
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validateAndNormalizeExpected: true,
|
||||||
|
response: &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
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: 123,
|
||||||
|
},
|
||||||
|
A: net.ParseIP("1.2.3.4"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "When a request context is provided, values do not override explicit tenancy",
|
||||||
|
request: &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Opcode: dns.OpcodeQuery,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{
|
||||||
|
{
|
||||||
|
Name: "foo.service.bar.ns.baz.ap.consul.",
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
requestContext: &Context{
|
||||||
|
Token: "test-token",
|
||||||
|
DefaultNamespace: "test-namespace",
|
||||||
|
DefaultPartition: "test-partition",
|
||||||
|
},
|
||||||
|
configureDataFetcher: func(fetcher discovery.CatalogDataFetcher) {
|
||||||
|
result := []*discovery.Result{
|
||||||
|
{
|
||||||
|
Type: discovery.ResultTypeNode,
|
||||||
|
Node: &discovery.Location{Name: "foo", Address: "1.2.3.4"},
|
||||||
|
Tenancy: discovery.ResultTenancy{
|
||||||
|
Namespace: "bar",
|
||||||
|
Partition: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fetcher.(*discovery.MockCatalogDataFetcher).
|
||||||
|
On("FetchEndpoints", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(result, nil).
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
ctx := args.Get(0).(discovery.Context)
|
||||||
|
req := args.Get(1).(*discovery.QueryPayload)
|
||||||
|
reqType := args.Get(2).(discovery.LookupType)
|
||||||
|
|
||||||
|
require.Equal(t, "test-token", ctx.Token)
|
||||||
|
|
||||||
|
require.Equal(t, "foo", req.Name)
|
||||||
|
require.Equal(t, "bar", req.Tenancy.Namespace)
|
||||||
|
require.Equal(t, "baz", req.Tenancy.Partition)
|
||||||
|
|
||||||
|
require.Equal(t, discovery.LookupTypeService, reqType)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validateAndNormalizeExpected: true,
|
||||||
|
response: &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Response: true,
|
||||||
|
Authoritative: true,
|
||||||
|
},
|
||||||
|
Compress: true,
|
||||||
|
Question: []dns.Question{
|
||||||
|
{
|
||||||
|
Name: "foo.service.bar.ns.baz.ap.consul.",
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Answer: []dns.RR{
|
||||||
|
&dns.A{
|
||||||
|
Hdr: dns.RR_Header{
|
||||||
|
Name: "foo.service.bar.ns.baz.ap.consul.",
|
||||||
|
Rrtype: dns.TypeA,
|
||||||
|
Class: dns.ClassINET,
|
||||||
|
Ttl: 123,
|
||||||
|
},
|
||||||
|
A: net.ParseIP("1.2.3.4"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|
|
@ -72,9 +72,10 @@ func (s *ServerV2) Query(ctx context.Context, req *pbdns.QueryRequest) (*pbdns.Q
|
||||||
return nil, status.Error(codes.Internal, fmt.Sprintf("failure decoding dns request: %s", err.Error()))
|
return nil, status.Error(codes.Internal, fmt.Sprintf("failure decoding dns request: %s", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (v2-dns): parse token and other context metadata from the grpc request/metadata (NET-7885)
|
reqCtx, err := agentdns.NewContextFromGRPCContext(ctx)
|
||||||
reqCtx := agentdns.Context{
|
if err != nil {
|
||||||
Token: s.TokenFunc(),
|
s.Logger.Error("error parsing DNS context from grpc metadata", "err", err)
|
||||||
|
return nil, status.Error(codes.Internal, fmt.Sprintf("error parsing DNS context from grpc metadata: %s", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := s.DNSRouter.HandleRequest(msg, reqCtx, remote)
|
resp := s.DNSRouter.HandleRequest(msg, reqCtx, remote)
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
|
||||||
agentdns "github.com/hashicorp/consul/agent/dns"
|
agentdns "github.com/hashicorp/consul/agent/dns"
|
||||||
"github.com/hashicorp/consul/proto-public/pbdns"
|
"github.com/hashicorp/consul/proto-public/pbdns"
|
||||||
|
@ -50,6 +52,7 @@ func (s *DNSTestSuite) TestProxy_V2Success() {
|
||||||
question string
|
question string
|
||||||
configureRouter func(router *agentdns.MockDNSRouter)
|
configureRouter func(router *agentdns.MockDNSRouter)
|
||||||
clientQuery func(qR *pbdns.QueryRequest)
|
clientQuery func(qR *pbdns.QueryRequest)
|
||||||
|
metadata map[string]string
|
||||||
expectedErr error
|
expectedErr error
|
||||||
}{
|
}{
|
||||||
|
|
||||||
|
@ -73,6 +76,28 @@ func (s *DNSTestSuite) TestProxy_V2Success() {
|
||||||
qR.Protocol = pbdns.Protocol_PROTOCOL_TCP
|
qR.Protocol = pbdns.Protocol_PROTOCOL_TCP
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"happy path with context variables set": {
|
||||||
|
question: "abc.com.",
|
||||||
|
configureRouter: func(router *agentdns.MockDNSRouter) {
|
||||||
|
router.On("HandleRequest", mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Run(func(args mock.Arguments) {
|
||||||
|
ctx, ok := args.Get(1).(agentdns.Context)
|
||||||
|
require.True(s.T(), ok, "error casting to agentdns.Context")
|
||||||
|
require.Equal(s.T(), "test-token", ctx.Token, "token not set in context")
|
||||||
|
require.Equal(s.T(), "test-namespace", ctx.DefaultNamespace, "namespace not set in context")
|
||||||
|
require.Equal(s.T(), "test-partition", ctx.DefaultPartition, "partition not set in context")
|
||||||
|
}).
|
||||||
|
Return(basicResponse(), nil)
|
||||||
|
},
|
||||||
|
clientQuery: func(qR *pbdns.QueryRequest) {
|
||||||
|
qR.Protocol = pbdns.Protocol_PROTOCOL_UDP
|
||||||
|
},
|
||||||
|
metadata: map[string]string{
|
||||||
|
"x-consul-token": "test-token",
|
||||||
|
"x-consul-namespace": "test-namespace",
|
||||||
|
"x-consul-partition": "test-partition",
|
||||||
|
},
|
||||||
|
},
|
||||||
"No protocol set": {
|
"No protocol set": {
|
||||||
question: "abc.com.",
|
question: "abc.com.",
|
||||||
clientQuery: func(qR *pbdns.QueryRequest) {},
|
clientQuery: func(qR *pbdns.QueryRequest) {},
|
||||||
|
@ -108,9 +133,18 @@ func (s *DNSTestSuite) TestProxy_V2Success() {
|
||||||
|
|
||||||
bytes, _ := req.Pack()
|
bytes, _ := req.Pack()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if len(tc.metadata) > 0 {
|
||||||
|
md := metadata.MD{}
|
||||||
|
for k, v := range tc.metadata {
|
||||||
|
md.Set(k, v)
|
||||||
|
}
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|
||||||
clientReq := &pbdns.QueryRequest{Msg: bytes}
|
clientReq := &pbdns.QueryRequest{Msg: bytes}
|
||||||
tc.clientQuery(clientReq)
|
tc.clientQuery(clientReq)
|
||||||
clientResp, err := client.Query(context.Background(), clientReq)
|
clientResp, err := client.Query(ctx, clientReq)
|
||||||
if tc.expectedErr != nil {
|
if tc.expectedErr != nil {
|
||||||
s.Require().Error(err, "no errror calling gRPC endpoint")
|
s.Require().Error(err, "no errror calling gRPC endpoint")
|
||||||
s.Require().ErrorContains(err, tc.expectedErr.Error())
|
s.Require().ErrorContains(err, tc.expectedErr.Error())
|
||||||
|
|
Loading…
Reference in New Issue