From 499fee73b329307ae72311ee234d2587eed83afe Mon Sep 17 00:00:00 2001 From: "R.B. Boyer" <4903+rboyer@users.noreply.github.com> Date: Tue, 6 Apr 2021 13:19:59 -0500 Subject: [PATCH] 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. --- .changelog/9973.txt | 3 + agent/config/runtime_test.go | 110 ++++++++++++ agent/consul/fsm/snapshot_oss_test.go | 15 ++ agent/consul/state/config_entry.go | 1 + agent/proxycfg/snapshot.go | 12 +- agent/proxycfg/state.go | 30 ++++ agent/proxycfg/state_test.go | 44 ++++- agent/structs/config_entry.go | 7 +- agent/structs/config_entry_cluster.go | 102 ++++++++++++ agent/structs/config_entry_cluster_oss.go | 5 + agent/structs/config_entry_test.go | 36 ++++ agent/xds/listeners.go | 28 ++-- agent/xds/listeners_test.go | 58 ++++++- ...alog-destinations-only.envoy-1-17-x.golden | 157 ++++++++++++++++++ ...inations-only.v2compat.envoy-1-17-x.golden | 157 ++++++++++++++++++ api/config_entry.go | 6 +- api/config_entry_cluster.go | 39 +++++ api/config_entry_test.go | 27 +++ command/config/write/config_write_test.go | 67 +++++++- 19 files changed, 875 insertions(+), 29 deletions(-) create mode 100644 .changelog/9973.txt create mode 100644 agent/structs/config_entry_cluster.go create mode 100644 agent/structs/config_entry_cluster_oss.go create mode 100644 agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.envoy-1-17-x.golden create mode 100644 agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.v2compat.envoy-1-17-x.golden create mode 100644 api/config_entry_cluster.go diff --git a/.changelog/9973.txt b/.changelog/9973.txt new file mode 100644 index 0000000000..adc953abf6 --- /dev/null +++ b/.changelog/9973.txt @@ -0,0 +1,3 @@ +```release-note:feature +connect: add toggle to globally disable wildcard outbound network access when transparent proxy is enabled +``` diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go index 47c54ec240..a68ed5f944 100644 --- a/agent/config/runtime_test.go +++ b/agent/config/runtime_test.go @@ -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 diff --git a/agent/consul/fsm/snapshot_oss_test.go b/agent/consul/fsm/snapshot_oss_test.go index a04d813191..917e89fb63 100644 --- a/agent/consul/fsm/snapshot_oss_test.go +++ b/agent/consul/fsm/snapshot_oss_test.go @@ -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) diff --git a/agent/consul/state/config_entry.go b/agent/consul/state/config_entry.go index 318178a2f0..e0c278e4d0 100644 --- a/agent/consul/state/config_entry.go +++ b/agent/consul/state/config_entry.go @@ -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) } diff --git a/agent/proxycfg/snapshot.go b/agent/proxycfg/snapshot.go index 2974dcd202..e79fc1507e 100644 --- a/agent/proxycfg/snapshot.go +++ b/agent/proxycfg/snapshot.go @@ -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 diff --git a/agent/proxycfg/state.go b/agent/proxycfg/state.go index 9bb65b23cc..31b8c352d1 100644 --- a/agent/proxycfg/state.go +++ b/agent/proxycfg/state.go @@ -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) } diff --git a/agent/proxycfg/state_test.go b/agent/proxycfg/state_test.go index 0c98e6c0a5..bc8a4daa6e 100644 --- a/agent/proxycfg/state_test.go +++ b/agent/proxycfg/state_test.go @@ -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) @@ -1538,8 +1550,9 @@ func TestState_WatchesAndUpdates(t *testing.T) { rootsWatchID: genVerifyRootsWatch("dc1"), intentionUpstreamsID: genVerifyServiceSpecificRequest(intentionUpstreamsID, "api", "", "dc1", false), - leafWatchID: genVerifyLeafWatch("api", "dc1"), - intentionsWatchID: genVerifyIntentionWatch("api", "dc1"), + 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) }, }, }, @@ -1594,8 +1616,9 @@ func TestState_WatchesAndUpdates(t *testing.T) { rootsWatchID: genVerifyRootsWatch("dc1"), intentionUpstreamsID: genVerifyServiceSpecificRequest(intentionUpstreamsID, "api", "", "dc1", false), - leafWatchID: genVerifyLeafWatch("api", "dc1"), - intentionsWatchID: genVerifyIntentionWatch("api", "dc1"), + 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 diff --git a/agent/structs/config_entry.go b/agent/structs/config_entry.go index c2427dfa98..c28718fd2f 100644 --- a/agent/structs/config_entry.go +++ b/agent/structs/config_entry.go @@ -26,8 +26,10 @@ const ( IngressGateway string = "ingress-gateway" TerminatingGateway string = "terminating-gateway" ServiceIntentions string = "service-intentions" + ClusterConfig string = "cluster" - ProxyConfigGlobal string = "global" + 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) } diff --git a/agent/structs/config_entry_cluster.go b/agent/structs/config_entry_cluster.go new file mode 100644 index 0000000000..1592ffed62 --- /dev/null +++ b/agent/structs/config_entry_cluster.go @@ -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 +} diff --git a/agent/structs/config_entry_cluster_oss.go b/agent/structs/config_entry_cluster_oss.go new file mode 100644 index 0000000000..1919fa3a9e --- /dev/null +++ b/agent/structs/config_entry_cluster_oss.go @@ -0,0 +1,5 @@ +package structs + +func (e *ClusterConfigEntry) validateEnterpriseMeta() error { + return nil +} diff --git a/agent/structs/config_entry_test.go b/agent/structs/config_entry_test.go index bff3315c50..b8c4f60400 100644 --- a/agent/structs/config_entry_test.go +++ b/agent/structs/config_entry_test.go @@ -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 diff --git a/agent/xds/listeners.go b/agent/xds/listeners.go index c866cf62eb..6ec12b8d63 100644 --- a/agent/xds/listeners.go +++ b/agent/xds/listeners.go @@ -223,19 +223,23 @@ func (s *Server) listenersFromSnapshotConnectProxy(cInfo connectionInfo, cfgSnap }) // Add a catch-all filter chain that acts as a TCP proxy to non-catalog destinations - filterChain, err := s.makeUpstreamFilterChainForDiscoveryChain( - "passthrough", - OriginalDestinationClusterName, - "tcp", - nil, - nil, - cfgSnap, - nil, - ) - if err != nil { - return nil, err + if cfgSnap.ConnectProxy.ClusterConfig == nil || + !cfgSnap.ConnectProxy.ClusterConfig.TransparentProxy.CatalogDestinationsOnly { + + filterChain, err := s.makeUpstreamFilterChainForDiscoveryChain( + "passthrough", + OriginalDestinationClusterName, + "tcp", + nil, + nil, + cfgSnap, + nil, + ) + if err != nil { + return nil, err + } + outboundListener.FilterChains = append(outboundListener.FilterChains, filterChain) } - outboundListener.FilterChains = append(outboundListener.FilterChains, filterChain) resources = append(resources, outboundListener) } diff --git a/agent/xds/listeners_test.go b/agent/xds/listeners_test.go index dc93f5d39f..dc2cd2e9d3 100644 --- a/agent/xds/listeners_test.go +++ b/agent/xds/listeners_test.go @@ -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", diff --git a/agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.envoy-1-17-x.golden b/agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.envoy-1-17-x.golden new file mode 100644 index 0000000000..cd38c0d064 --- /dev/null +++ b/agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.envoy-1-17-x.golden @@ -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" +} \ No newline at end of file diff --git a/agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.v2compat.envoy-1-17-x.golden b/agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.v2compat.envoy-1-17-x.golden new file mode 100644 index 0000000000..f63b436846 --- /dev/null +++ b/agent/xds/testdata/listeners/transparent-proxy-catalog-destinations-only.v2compat.envoy-1-17-x.golden @@ -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" +} \ No newline at end of file diff --git a/api/config_entry.go b/api/config_entry.go index f303187cdd..ed9215efcf 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -21,8 +21,10 @@ const ( IngressGateway string = "ingress-gateway" TerminatingGateway string = "terminating-gateway" ServiceIntentions string = "service-intentions" + ClusterConfig string = "cluster" - ProxyConfigGlobal string = "global" + 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) } diff --git a/api/config_entry_cluster.go b/api/config_entry_cluster.go new file mode 100644 index 0000000000..952a867d0a --- /dev/null +++ b/api/config_entry_cluster.go @@ -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 +} diff --git a/api/config_entry_test.go b/api/config_entry_test.go index 443995e123..7badcd6264 100644 --- a/api/config_entry_test.go +++ b/api/config_entry_test.go @@ -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 diff --git a/command/config/write/config_write_test.go b/command/config/write/config_write_test.go index e628c13e46..9a9597e311 100644 --- a/command/config/write/config_write_test.go +++ b/command/config/write/config_write_test.go @@ -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