connect: add toggle to globally disable wildcard outbound network access when transparent proxy is enabled (#9973)

This adds a new config entry kind "cluster" with a single special name "cluster" where this can be controlled.
This commit is contained in:
R.B. Boyer 2021-04-06 13:19:59 -05:00 committed by GitHub
parent f0ba6f858a
commit 499fee73b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 875 additions and 29 deletions

3
.changelog/9973.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
connect: add toggle to globally disable wildcard outbound network access when transparent proxy is enabled
```

View File

@ -4095,6 +4095,116 @@ func TestLoad_IntegrationWithFlags(t *testing.T) {
}
},
})
run(t, testCase{
desc: "ConfigEntry bootstrap cluster (snake-case)",
args: []string{`-data-dir=` + dataDir},
json: []string{`{
"config_entries": {
"bootstrap": [
{
"kind": "cluster",
"name": "cluster",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"transparent_proxy": {
"catalog_destinations_only": true
}
}
]
}
}`,
},
hcl: []string{`
config_entries {
bootstrap {
kind = "cluster"
name = "cluster"
meta {
"foo" = "bar"
"gir" = "zim"
}
transparent_proxy {
catalog_destinations_only = true
}
}
}
`,
},
expected: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
rt.ConfigEntryBootstrap = []structs.ConfigEntry{
&structs.ClusterConfigEntry{
Kind: "cluster",
Name: "cluster",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
EnterpriseMeta: *defaultEntMeta,
TransparentProxy: structs.TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
},
}
},
})
run(t, testCase{
desc: "ConfigEntry bootstrap cluster (camel-case)",
args: []string{`-data-dir=` + dataDir},
json: []string{`{
"config_entries": {
"bootstrap": [
{
"Kind": "cluster",
"Name": "cluster",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"TransparentProxy": {
"CatalogDestinationsOnly": true
}
}
]
}
}`,
},
hcl: []string{`
config_entries {
bootstrap {
Kind = "cluster"
Name = "cluster"
Meta {
"foo" = "bar"
"gir" = "zim"
}
TransparentProxy {
CatalogDestinationsOnly = true
}
}
}
`,
},
expected: func(rt *RuntimeConfig) {
rt.DataDir = dataDir
rt.ConfigEntryBootstrap = []structs.ConfigEntry{
&structs.ClusterConfigEntry{
Kind: "cluster",
Name: "cluster",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
EnterpriseMeta: *defaultEntMeta,
TransparentProxy: structs.TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
},
}
},
})
///////////////////////////////////
// Defaults sanity checks

View File

@ -419,6 +419,16 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
}
require.NoError(t, fsm.state.EnsureConfigEntry(26, serviceIxn))
// cluster config entry
clusterConfig := &structs.ClusterConfigEntry{
Kind: structs.ClusterConfig,
Name: structs.ClusterConfigCluster,
TransparentProxy: structs.TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
}
require.NoError(t, fsm.state.EnsureConfigEntry(27, clusterConfig))
// Snapshot
snap, err := fsm.Snapshot()
require.NoError(t, err)
@ -691,6 +701,11 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
require.NoError(t, err)
require.Equal(t, serviceIxn, serviceIxnEntry)
// Verify cluster config entry is restored
_, clusterConfigEntry, err := fsm2.state.ConfigEntry(nil, structs.ClusterConfig, structs.ClusterConfigCluster, structs.DefaultEnterpriseMeta())
require.NoError(t, err)
require.Equal(t, clusterConfig, clusterConfigEntry)
// Snapshot
snap, err = fsm2.Snapshot()
require.NoError(t, err)

View File

@ -362,6 +362,7 @@ func validateProposedConfigEntryInGraph(
return err
}
case structs.ServiceIntentions:
case structs.ClusterConfig:
default:
return fmt.Errorf("unhandled kind %q during validation of %q", kind, name)
}

View File

@ -5,8 +5,9 @@ import (
"fmt"
"sort"
"github.com/hashicorp/consul/agent/structs"
"github.com/mitchellh/copystructure"
"github.com/hashicorp/consul/agent/structs"
)
// TODO(ingress): Can we think of a better for this bag of data?
@ -59,6 +60,9 @@ type configSnapshotConnectProxy struct {
// intentions.
Intentions structs.Intentions
IntentionsSet bool
ClusterConfig *structs.ClusterConfigEntry
ClusterConfigSet bool
}
func (c *configSnapshotConnectProxy) IsEmpty() bool {
@ -75,7 +79,8 @@ func (c *configSnapshotConnectProxy) IsEmpty() bool {
len(c.WatchedGatewayEndpoints) == 0 &&
len(c.WatchedServiceChecks) == 0 &&
len(c.PreparedQueryEndpoints) == 0 &&
len(c.UpstreamConfig) == 0
len(c.UpstreamConfig) == 0 &&
!c.ClusterConfigSet
}
type configSnapshotTerminatingGateway struct {
@ -355,6 +360,9 @@ type ConfigSnapshot struct {
func (s *ConfigSnapshot) Valid() bool {
switch s.Kind {
case structs.ServiceKindConnectProxy:
if s.Proxy.TransparentProxy && !s.ConnectProxy.ClusterConfigSet {
return false
}
return s.Roots != nil &&
s.ConnectProxy.Leaf != nil &&
s.ConnectProxy.IntentionsSet

View File

@ -46,6 +46,7 @@ const (
serviceResolverIDPrefix = "service-resolver:"
serviceIntentionsIDPrefix = "service-intentions:"
intentionUpstreamsID = "intention-upstreams"
clusterConfigEntryID = "cluster-config"
svcChecksWatchIDPrefix = cachetype.ServiceHTTPChecksName + ":"
serviceIDPrefix = string(structs.UpstreamDestTypeService) + ":"
preparedQueryIDPrefix = string(structs.UpstreamDestTypePreparedQuery) + ":"
@ -315,6 +316,17 @@ func (s *state) initWatchesConnectProxy(snap *ConfigSnapshot) error {
if err != nil {
return err
}
err = s.cache.Notify(s.ctx, cachetype.ConfigEntryName, &structs.ConfigEntryQuery{
Kind: structs.ClusterConfig,
Name: structs.ClusterConfigCluster,
Datacenter: s.source.Datacenter,
QueryOptions: structs.QueryOptions{Token: s.token},
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
}, clusterConfigEntryID, s.ch)
if err != nil {
return err
}
}
// Watch for updates to service endpoints for all upstreams
@ -846,6 +858,24 @@ func (s *state) handleUpdateConnectProxy(u cache.UpdateEvent, snap *ConfigSnapsh
}
svcID := structs.ServiceIDFromString(strings.TrimPrefix(u.CorrelationID, svcChecksWatchIDPrefix))
snap.ConnectProxy.WatchedServiceChecks[svcID] = resp
case u.CorrelationID == clusterConfigEntryID:
resp, ok := u.Result.(*structs.ConfigEntryResponse)
if !ok {
return fmt.Errorf("invalid type for response: %T", u.Result)
}
if resp.Entry != nil {
clusterConf, ok := resp.Entry.(*structs.ClusterConfigEntry)
if !ok {
return fmt.Errorf("invalid type for config entry: %T", resp.Entry)
}
snap.ConnectProxy.ClusterConfig = clusterConf
} else {
snap.ConnectProxy.ClusterConfig = nil
}
snap.ConnectProxy.ClusterConfigSet = true
default:
return s.handleUpdateUpstreams(u, &snap.ConnectProxy.ConfigSnapshotUpstreams)
}

View File

@ -288,6 +288,18 @@ func genVerifyDiscoveryChainWatch(expected *structs.DiscoveryChainRequest) verif
}
}
func genVerifyClusterConfigWatch(expectedDatacenter string) verifyWatchRequest {
return func(t testing.TB, cacheType string, request cache.Request) {
require.Equal(t, cachetype.ConfigEntryName, cacheType)
reqReal, ok := request.(*structs.ConfigEntryQuery)
require.True(t, ok)
require.Equal(t, expectedDatacenter, reqReal.Datacenter)
require.Equal(t, structs.ClusterConfigCluster, reqReal.Name)
require.Equal(t, structs.ClusterConfig, reqReal.Kind)
}
}
func genVerifyGatewayWatch(expectedDatacenter string) verifyWatchRequest {
return func(t testing.TB, cacheType string, request cache.Request) {
require.Equal(t, cachetype.InternalServiceDumpName, cacheType)
@ -1540,6 +1552,7 @@ func TestState_WatchesAndUpdates(t *testing.T) {
"api", "", "dc1", false),
leafWatchID: genVerifyLeafWatch("api", "dc1"),
intentionsWatchID: genVerifyIntentionWatch("api", "dc1"),
clusterConfigEntryID: genVerifyClusterConfigWatch("dc1"),
},
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
require.False(t, snap.Valid(), "proxy without roots/leaf/intentions is not valid")
@ -1562,6 +1575,13 @@ func TestState_WatchesAndUpdates(t *testing.T) {
Result: TestIntentions(),
Err: nil,
},
{
CorrelationID: clusterConfigEntryID,
Result: &structs.ConfigEntryResponse{
Entry: nil, // no explicit config
},
Err: nil,
},
},
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
require.True(t, snap.Valid(), "proxy with roots/leaf/intentions is valid")
@ -1571,6 +1591,8 @@ func TestState_WatchesAndUpdates(t *testing.T) {
require.True(t, snap.MeshGateway.IsEmpty())
require.True(t, snap.IngressGateway.IsEmpty())
require.True(t, snap.TerminatingGateway.IsEmpty())
require.True(t, snap.ConnectProxy.ClusterConfigSet)
require.Nil(t, snap.ConnectProxy.ClusterConfig)
},
},
},
@ -1596,6 +1618,7 @@ func TestState_WatchesAndUpdates(t *testing.T) {
"api", "", "dc1", false),
leafWatchID: genVerifyLeafWatch("api", "dc1"),
intentionsWatchID: genVerifyIntentionWatch("api", "dc1"),
clusterConfigEntryID: genVerifyClusterConfigWatch("dc1"),
},
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
require.False(t, snap.Valid(), "proxy without roots/leaf/intentions is not valid")
@ -1619,6 +1642,17 @@ func TestState_WatchesAndUpdates(t *testing.T) {
Result: TestIntentions(),
Err: nil,
},
{
CorrelationID: clusterConfigEntryID,
Result: &structs.ConfigEntryResponse{
Entry: &structs.ClusterConfigEntry{
Kind: structs.ClusterConfig,
Name: structs.ClusterConfigCluster,
TransparentProxy: structs.TransparentProxyClusterConfig{},
},
},
Err: nil,
},
},
verifySnapshot: func(t testing.TB, snap *ConfigSnapshot) {
require.True(t, snap.Valid(), "proxy with roots/leaf/intentions is valid")
@ -1628,6 +1662,8 @@ func TestState_WatchesAndUpdates(t *testing.T) {
require.True(t, snap.MeshGateway.IsEmpty())
require.True(t, snap.IngressGateway.IsEmpty())
require.True(t, snap.TerminatingGateway.IsEmpty())
require.True(t, snap.ConnectProxy.ClusterConfigSet)
require.NotNil(t, snap.ConnectProxy.ClusterConfig)
},
},
// Receiving an intention should lead to spinning up a discovery chain watch

View File

@ -26,8 +26,10 @@ const (
IngressGateway string = "ingress-gateway"
TerminatingGateway string = "terminating-gateway"
ServiceIntentions string = "service-intentions"
ClusterConfig string = "cluster"
ProxyConfigGlobal string = "global"
ClusterConfigCluster string = "cluster"
DefaultServiceProtocol = "tcp"
)
@ -41,6 +43,7 @@ var AllConfigEntryKinds = []string{
IngressGateway,
TerminatingGateway,
ServiceIntentions,
ClusterConfig,
}
// ConfigEntry is the interface for centralized configuration stored in Raft.
@ -496,6 +499,8 @@ func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
return &TerminatingGatewayConfigEntry{Name: name}, nil
case ServiceIntentions:
return &ServiceIntentionsConfigEntry{Name: name}, nil
case ClusterConfig:
return &ClusterConfigEntry{Name: name}, nil
default:
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
}

View File

@ -0,0 +1,102 @@
package structs
import (
"fmt"
"github.com/hashicorp/consul/acl"
)
type ClusterConfigEntry struct {
Kind string
Name string
// TransparentProxy contains cluster-wide options pertaining to TPROXY mode
// when enabled.
TransparentProxy TransparentProxyClusterConfig `alias:"transparent_proxy"`
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex
}
// TransparentProxyClusterConfig contains cluster-wide options pertaining to
// TPROXY mode when enabled.
type TransparentProxyClusterConfig struct {
// CatalogDestinationsOnly can be used to disable the pass-through that
// allows traffic to destinations outside of the mesh.
CatalogDestinationsOnly bool `alias:"catalog_destinations_only"`
}
func (e *ClusterConfigEntry) GetKind() string {
return ClusterConfig
}
func (e *ClusterConfigEntry) GetName() string {
if e == nil {
return ""
}
return e.Name
}
func (e *ClusterConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *ClusterConfigEntry) Normalize() error {
if e == nil {
return fmt.Errorf("config entry is nil")
}
e.Kind = ClusterConfig
e.Name = ClusterConfigCluster
e.EnterpriseMeta.Normalize()
return nil
}
func (e *ClusterConfigEntry) Validate() error {
if e == nil {
return fmt.Errorf("config entry is nil")
}
if e.Name != ClusterConfigCluster {
return fmt.Errorf("invalid name (%q), only %q is supported", e.Name, ClusterConfigCluster)
}
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
return e.validateEnterpriseMeta()
}
func (e *ClusterConfigEntry) CanRead(authz acl.Authorizer) bool {
return true
}
func (e *ClusterConfigEntry) CanWrite(authz acl.Authorizer) bool {
var authzContext acl.AuthorizerContext
e.FillAuthzContext(&authzContext)
return authz.OperatorWrite(&authzContext) == acl.Allow
}
func (e *ClusterConfigEntry) GetRaftIndex() *RaftIndex {
if e == nil {
return &RaftIndex{}
}
return &e.RaftIndex
}
func (e *ClusterConfigEntry) GetEnterpriseMeta() *EnterpriseMeta {
if e == nil {
return nil
}
return &e.EnterpriseMeta
}

View File

@ -0,0 +1,5 @@
package structs
func (e *ClusterConfigEntry) validateEnterpriseMeta() error {
return nil
}

View File

@ -1300,6 +1300,42 @@ func TestDecodeConfigEntry(t *testing.T) {
},
},
},
{
name: "cluster",
snake: `
kind = "cluster"
name = "cluster"
meta {
"foo" = "bar"
"gir" = "zim"
}
transparent_proxy {
catalog_destinations_only = true
}
`,
camel: `
Kind = "cluster"
Name = "cluster"
Meta {
"foo" = "bar"
"gir" = "zim"
}
TransparentProxy {
CatalogDestinationsOnly = true
}
`,
expect: &ClusterConfigEntry{
Kind: "cluster",
Name: "cluster",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
TransparentProxy: TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
},
},
} {
tc := tc

View File

@ -223,6 +223,9 @@ func (s *Server) listenersFromSnapshotConnectProxy(cInfo connectionInfo, cfgSnap
})
// Add a catch-all filter chain that acts as a TCP proxy to non-catalog destinations
if cfgSnap.ConnectProxy.ClusterConfig == nil ||
!cfgSnap.ConnectProxy.ClusterConfig.TransparentProxy.CatalogDestinationsOnly {
filterChain, err := s.makeUpstreamFilterChainForDiscoveryChain(
"passthrough",
OriginalDestinationClusterName,
@ -236,6 +239,7 @@ func (s *Server) listenersFromSnapshotConnectProxy(cInfo connectionInfo, cfgSnap
return nil, err
}
outboundListener.FilterChains = append(outboundListener.FilterChains, filterChain)
}
resources = append(resources, outboundListener)
}

View File

@ -2,7 +2,16 @@ package xds
import (
"bytes"
"path/filepath"
"sort"
"testing"
"text/template"
"time"
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
testinf "github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/consul/discoverychain"
"github.com/hashicorp/consul/agent/proxycfg"
@ -10,13 +19,6 @@ import (
"github.com/hashicorp/consul/agent/xds/proxysupport"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/types"
testinf "github.com/mitchellh/go-testing-interface"
"github.com/stretchr/testify/require"
"path/filepath"
"sort"
"testing"
"text/template"
"time"
)
func TestListenersFromSnapshot(t *testing.T) {
@ -481,6 +483,48 @@ func TestListenersFromSnapshot(t *testing.T) {
setup: func(snap *proxycfg.ConfigSnapshot) {
snap.Proxy.TransparentProxy = true
snap.ConnectProxy.ClusterConfigSet = true
// DiscoveryChain without an UpstreamConfig should yield a filter chain when in TransparentProxy mode
snap.ConnectProxy.DiscoveryChain["google"] = discoverychain.TestCompileConfigEntries(
t, "google", "default", "dc1",
connect.TestClusterID+".consul", "dc1", nil)
snap.ConnectProxy.WatchedUpstreamEndpoints["google"] = map[string]structs.CheckServiceNodes{
"google.default.dc1": {
structs.CheckServiceNode{
Node: &structs.Node{
Address: "8.8.8.8",
Datacenter: "dc1",
},
Service: &structs.NodeService{
Service: "google",
Port: 9090,
},
},
},
}
// DiscoveryChains without endpoints do not get a filter chain because there are no addresses to match on.
snap.ConnectProxy.DiscoveryChain["no-endpoints"] = discoverychain.TestCompileConfigEntries(
t, "no-endpoints", "default", "dc1",
connect.TestClusterID+".consul", "dc1", nil)
},
},
{
name: "transparent-proxy-catalog-destinations-only",
create: proxycfg.TestConfigSnapshot,
setup: func(snap *proxycfg.ConfigSnapshot) {
snap.Proxy.TransparentProxy = true
snap.ConnectProxy.ClusterConfigSet = true
snap.ConnectProxy.ClusterConfig = &structs.ClusterConfigEntry{
Kind: structs.ClusterConfig,
Name: structs.ClusterConfigCluster,
TransparentProxy: structs.TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
}
// DiscoveryChain without an UpstreamConfig should yield a filter chain when in TransparentProxy mode
snap.ConnectProxy.DiscoveryChain["google"] = discoverychain.TestCompileConfigEntries(
t, "google", "default", "dc1",

View File

@ -0,0 +1,157 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "db:127.0.0.1:9191",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.db.default.dc1",
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "outbound_listener:127.0.0.1:15001",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 15001
}
},
"filterChains": [
{
"filterChainMatch": {
"prefixRanges": [
{
"addressPrefix": "8.8.8.8",
"prefixLen": 32
}
]
},
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.google.default.dc1",
"cluster": "google.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"listenerFilters": [
{
"name": "envoy.filters.listener.original_dst"
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "prepared_query:geo-cache:127.10.10.10:8181",
"address": {
"socketAddress": {
"address": "127.10.10.10",
"portValue": 8181
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.prepared_query_geo-cache",
"cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "public_listener:0.0.0.0:9999",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 9999
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.rbac.v3.RBAC",
"rules": {
},
"statPrefix": "connect_authz"
}
},
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "public_listener",
"cluster": "local_app"
}
}
],
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
"commonTlsContext": {
"tlsParams": {
},
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"requireClientCertificate": true
}
}
}
],
"trafficDirection": "INBOUND"
}
],
"typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener",
"nonce": "00000001"
}

View File

@ -0,0 +1,157 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.api.v2.Listener",
"name": "db:127.0.0.1:9191",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "upstream.db.default.dc1",
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.api.v2.Listener",
"name": "outbound_listener:127.0.0.1:15001",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 15001
}
},
"filterChains": [
{
"filterChainMatch": {
"prefixRanges": [
{
"addressPrefix": "8.8.8.8",
"prefixLen": 32
}
]
},
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "upstream.google.default.dc1",
"cluster": "google.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"listenerFilters": [
{
"name": "envoy.filters.listener.original_dst"
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.api.v2.Listener",
"name": "prepared_query:geo-cache:127.10.10.10:8181",
"address": {
"socketAddress": {
"address": "127.10.10.10",
"portValue": 8181
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "upstream.prepared_query_geo-cache",
"cluster": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.api.v2.Listener",
"name": "public_listener:0.0.0.0:9999",
"address": {
"socketAddress": {
"address": "0.0.0.0",
"portValue": 9999
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.rbac",
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.filter.network.rbac.v2.RBAC",
"rules": {
},
"statPrefix": "connect_authz"
}
},
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.config.filter.network.tcp_proxy.v2.TcpProxy",
"statPrefix": "public_listener",
"cluster": "local_app"
}
}
],
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.api.v2.auth.DownstreamTlsContext",
"commonTlsContext": {
"tlsParams": {
},
"tlsCertificates": [
{
"certificateChain": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n"
},
"privateKey": {
"inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n"
}
}
],
"validationContext": {
"trustedCa": {
"inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n"
}
}
},
"requireClientCertificate": true
}
}
}
],
"trafficDirection": "INBOUND"
}
],
"typeUrl": "type.googleapis.com/envoy.api.v2.Listener",
"nonce": "00000001"
}

View File

@ -21,8 +21,10 @@ const (
IngressGateway string = "ingress-gateway"
TerminatingGateway string = "terminating-gateway"
ServiceIntentions string = "service-intentions"
ClusterConfig string = "cluster"
ProxyConfigGlobal string = "global"
ClusterConfigCluster string = "cluster"
)
type ConfigEntry interface {
@ -260,6 +262,8 @@ func makeConfigEntry(kind, name string) (ConfigEntry, error) {
return &TerminatingGatewayConfigEntry{Kind: kind, Name: name}, nil
case ServiceIntentions:
return &ServiceIntentionsConfigEntry{Kind: kind, Name: name}, nil
case ClusterConfig:
return &ClusterConfigEntry{Kind: kind, Name: name}, nil
default:
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
}

View File

@ -0,0 +1,39 @@
package api
type ClusterConfigEntry struct {
Kind string
Name string
Namespace string `json:",omitempty"`
TransparentProxy TransparentProxyClusterConfig `alias:"transparent_proxy"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64
}
type TransparentProxyClusterConfig struct {
CatalogDestinationsOnly bool `alias:"catalog_destinations_only"`
}
func (e *ClusterConfigEntry) GetKind() string {
return e.Kind
}
func (e *ClusterConfigEntry) GetName() string {
return e.Name
}
func (e *ClusterConfigEntry) GetNamespace() string {
return e.Namespace
}
func (e *ClusterConfigEntry) GetMeta() map[string]string {
return e.Meta
}
func (e *ClusterConfigEntry) GetCreateIndex() uint64 {
return e.CreateIndex
}
func (e *ClusterConfigEntry) GetModifyIndex() uint64 {
return e.ModifyIndex
}

View File

@ -1124,6 +1124,33 @@ func TestDecodeConfigEntry(t *testing.T) {
},
},
},
{
name: "cluster",
body: `
{
"Kind": "cluster",
"Name": "cluster",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"TransparentProxy": {
"CatalogDestinationsOnly": true
}
}
`,
expect: &ClusterConfigEntry{
Kind: "cluster",
Name: "cluster",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
TransparentProxy: TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
},
},
} {
tc := tc

View File

@ -8,11 +8,12 @@ import (
"github.com/hashicorp/consul/agent/structs"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestConfigWrite_noTabs(t *testing.T) {
@ -2534,6 +2535,68 @@ func TestParseConfigEntry(t *testing.T) {
},
},
},
{
name: "cluster",
snake: `
kind = "cluster"
name = "cluster"
meta {
"foo" = "bar"
"gir" = "zim"
}
transparent_proxy {
catalog_destinations_only = true
}
`,
camel: `
Kind = "cluster"
Name = "cluster"
Meta {
"foo" = "bar"
"gir" = "zim"
}
TransparentProxy {
CatalogDestinationsOnly = true
}
`,
snakeJSON: `
{
"kind": "cluster",
"name": "cluster",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"transparent_proxy": {
"catalog_destinations_only": true
}
}
`,
camelJSON: `
{
"Kind": "cluster",
"Name": "cluster",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"TransparentProxy": {
"CatalogDestinationsOnly": true
}
}
`,
expect: &api.ClusterConfigEntry{
Kind: "cluster",
Name: "cluster",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
TransparentProxy: api.TransparentProxyClusterConfig{
CatalogDestinationsOnly: true,
},
},
},
} {
tc := tc