From b8d26404291f305a659fe4d2147e38a05bad154a Mon Sep 17 00:00:00 2001 From: Michael Zalimeni Date: Tue, 23 May 2023 07:55:06 -0400 Subject: [PATCH] Disable remote proxy patching except AWS Lambda (#17415) To avoid unintended tampering with remote downstreams via service config, refactor BasicEnvoyExtender and RuntimeConfig to disallow typical Envoy extensions from being applied to non-local proxies. Continue to allow this behavior for AWS Lambda and the read-only Validate builtin extensions. Addresses CVE-2023-2816. --- .changelog/17415.txt | 7 + .../builtin/aws-lambda/aws_lambda.go | 21 +- .../builtin/aws-lambda/aws_lambda_test.go | 43 ++- .../builtin/http/localratelimit/ratelimit.go | 12 +- agent/envoyextensions/builtin/lua/lua.go | 13 +- agent/envoyextensions/builtin/wasm/structs.go | 2 +- agent/envoyextensions/builtin/wasm/wasm.go | 15 +- .../envoyextensions/builtin/wasm/wasm_test.go | 68 +++-- .../registered_extensions_test.go | 51 ++++ agent/xds/delta_envoy_extender_oss_test.go | 75 +++-- agent/xds/delta_test.go | 58 ++-- agent/xds/extensionruntime/runtime_config.go | 45 ++- .../runtime_config_oss_test.go | 68 ++--- ...nt-apply-to-local-upstreams.latest.golden} | 0 ...-applies-to-local-upstreams.latest.golden} | 0 ...nt-apply-to-local-upstreams.latest.golden} | 0 ...-applies-to-local-upstreams.latest.golden} | 0 ...-doesnt-applies-to-upstreams.latest.golden | 146 ---------- ...snt-apply-to-local-upstreams.latest.golden | 272 ++++++++++++++++++ ...d-applies-to-local-upstreams.latest.golden | 272 ++++++++++++++++++ ...utbound-applies-to-upstreams.latest.golden | 153 ---------- ...nt-apply-to-local-upstreams.latest.golden} | 0 ...-applies-to-local-upstreams.latest.golden} | 0 agent/xds/xds_protocol_helpers_test.go | 3 +- api/config_entry.go | 4 + .../extensioncommon/basic_envoy_extender.go | 103 ++----- .../extensioncommon/envoy_extender_test.go | 74 +++++ .../extensioncommon/list_envoy_extender.go | 63 +--- envoyextensions/extensioncommon/resources.go | 41 +++ .../extensioncommon/runtime_config.go | 60 ++-- .../extensioncommon/runtime_config_test.go | 11 +- .../upstream_envoy_extender.go | 246 ++++++++++++++++ troubleshoot/proxy/validateupstream.go | 9 +- troubleshoot/validate/validate.go | 8 +- troubleshoot/validate/validate_test.go | 2 +- 35 files changed, 1284 insertions(+), 661 deletions(-) create mode 100644 .changelog/17415.txt rename agent/xds/testdata/builtin_extension/clusters/{lua-inbound-doesnt-applies-to-upstreams.latest.golden => lua-inbound-doesnt-apply-to-local-upstreams.latest.golden} (100%) rename agent/xds/testdata/builtin_extension/clusters/{lua-outbound-applies-to-upstreams.latest.golden => lua-outbound-applies-to-local-upstreams.latest.golden} (100%) rename agent/xds/testdata/builtin_extension/endpoints/{lua-inbound-doesnt-applies-to-upstreams.latest.golden => lua-inbound-doesnt-apply-to-local-upstreams.latest.golden} (100%) rename agent/xds/testdata/builtin_extension/endpoints/{lua-outbound-applies-to-upstreams.latest.golden => lua-outbound-applies-to-local-upstreams.latest.golden} (100%) delete mode 100644 agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-applies-to-upstreams.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/listeners/lua-outbound-applies-to-local-upstreams.latest.golden delete mode 100644 agent/xds/testdata/builtin_extension/listeners/lua-outbound-applies-to-upstreams.latest.golden rename agent/xds/testdata/builtin_extension/routes/{lua-inbound-doesnt-applies-to-upstreams.latest.golden => lua-inbound-doesnt-apply-to-local-upstreams.latest.golden} (100%) rename agent/xds/testdata/builtin_extension/routes/{lua-outbound-applies-to-upstreams.latest.golden => lua-outbound-applies-to-local-upstreams.latest.golden} (100%) create mode 100644 envoyextensions/extensioncommon/envoy_extender_test.go create mode 100644 envoyextensions/extensioncommon/upstream_envoy_extender.go diff --git a/.changelog/17415.txt b/.changelog/17415.txt new file mode 100644 index 0000000000..3f5b1e11cf --- /dev/null +++ b/.changelog/17415.txt @@ -0,0 +1,7 @@ +```release-note:security +extensions: Disable remote downstream proxy patching by Envoy Extensions other than AWS Lambda. Previously, an operator with service:write ACL permissions for an upstream service could modify Envoy proxy config for downstream services without equivalent permissions for those services. This issue only impacts the Lua extension. [[CVE-2023-2816](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-2816)] +``` + +```release-note:breaking-change +extensions: The Lua extension now targets local proxy listeners for the configured service's upstreams, rather than remote downstream listeners for the configured service, when ListenerType is set to outbound in extension configuration. See [CVE-2023-2816](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-2816) changelog entry for more details. +``` diff --git a/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go b/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go index 9579baf93b..8281e1f016 100644 --- a/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go +++ b/agent/envoyextensions/builtin/aws-lambda/aws_lambda.go @@ -42,7 +42,7 @@ func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) if err := a.fromArguments(ext.Arguments); err != nil { return nil, err } - return &extensioncommon.BasicEnvoyExtender{ + return &extensioncommon.UpstreamEnvoyExtender{ Extension: &a, }, nil } @@ -65,7 +65,7 @@ func (a *awsLambda) validate() error { // CanApply returns true if the kind of the provided ExtensionConfiguration matches // the kind of the lambda configuration func (a *awsLambda) CanApply(config *extensioncommon.RuntimeConfig) bool { - return config.Kind == config.OutgoingProxyKind() + return config.Kind == config.UpstreamOutgoingProxyKind() } // PatchRoute modifies the routing configuration for a service of kind TerminatingGateway. If the kind is @@ -75,6 +75,11 @@ func (a *awsLambda) PatchRoute(r *extensioncommon.RuntimeConfig, route *envoy_ro return route, false, nil } + // Only patch outbound routes. + if extensioncommon.IsRouteToLocalAppCluster(route) { + return route, false, nil + } + for _, virtualHost := range route.VirtualHosts { for _, route := range virtualHost.Routes { action, ok := route.Action.(*envoy_route_v3.Route_Route) @@ -95,6 +100,11 @@ func (a *awsLambda) PatchRoute(r *extensioncommon.RuntimeConfig, route *envoy_ro // PatchCluster patches the provided envoy cluster with data required to support an AWS lambda function func (a *awsLambda) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { + // Only patch outbound clusters. + if extensioncommon.IsLocalAppCluster(c) { + return c, false, nil + } + transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{ Sni: "*.amazonaws.com", }) @@ -156,7 +166,12 @@ func (a *awsLambda) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_clus // PatchFilter patches the provided envoy filter with an inserted lambda filter being careful not to // overwrite the http filters. -func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { +func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { + // Only patch outbound filters. + if isInboundListener { + return filter, false, nil + } + if filter.Name != "envoy.filters.network.http_connection_manager" { return filter, false, nil } diff --git a/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go b/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go index 1b8bb222e4..af7b95fd8d 100644 --- a/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go +++ b/agent/envoyextensions/builtin/aws-lambda/aws_lambda_test.go @@ -86,7 +86,7 @@ func TestConstructor(t *testing.T) { if tc.ok { require.NoError(t, err) - require.Equal(t, &extensioncommon.BasicEnvoyExtender{Extension: &tc.expected}, e) + require.Equal(t, &extensioncommon.UpstreamEnvoyExtender{Extension: &tc.expected}, e) } else { require.Error(t, err) } @@ -323,10 +323,11 @@ func TestPatchFilter(t *testing.T) { return v } tests := map[string]struct { - filter *envoy_listener_v3.Filter - expectFilter *envoy_listener_v3.Filter - expectBool bool - expectErr string + filter *envoy_listener_v3.Filter + isInboundFilter bool + expectFilter *envoy_listener_v3.Filter + expectBool bool + expectErr string }{ "invalid filter name is ignored": { filter: &envoy_listener_v3.Filter{Name: "something"}, @@ -416,6 +417,36 @@ func TestPatchFilter(t *testing.T) { }, expectBool: true, }, + "inbound filter ignored": { + filter: &envoy_listener_v3.Filter{ + Name: "envoy.filters.network.http_connection_manager", + ConfigType: &envoy_listener_v3.Filter_TypedConfig{ + TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{ + HttpFilters: []*envoy_http_v3.HttpFilter{ + {Name: "one"}, + {Name: "two"}, + {Name: "envoy.filters.http.router"}, + {Name: "three"}, + }, + }), + }, + }, + expectFilter: &envoy_listener_v3.Filter{ + Name: "envoy.filters.network.http_connection_manager", + ConfigType: &envoy_listener_v3.Filter_TypedConfig{ + TypedConfig: makeAny(&envoy_http_v3.HttpConnectionManager{ + HttpFilters: []*envoy_http_v3.HttpFilter{ + {Name: "one"}, + {Name: "two"}, + {Name: "envoy.filters.http.router"}, + {Name: "three"}, + }, + }), + }, + }, + isInboundFilter: true, + expectBool: false, + }, } for name, tc := range tests { @@ -425,7 +456,7 @@ func TestPatchFilter(t *testing.T) { PayloadPassthrough: true, InvocationMode: "asynchronous", } - f, ok, err := l.PatchFilter(nil, tc.filter) + f, ok, err := l.PatchFilter(nil, tc.filter, tc.isInboundFilter) require.Equal(t, tc.expectBool, ok) if tc.expectErr == "" { require.NoError(t, err) diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go index 0fd289ec30..64be93e085 100644 --- a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go +++ b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go @@ -97,9 +97,7 @@ func (r *ratelimit) validate() error { // CanApply determines if the extension can apply to the given extension configuration. func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool { - // rate limit is only applied to the service itself since the limit is - // aggregated from all downstream connections. - return string(config.Kind) == p.ProxyType && !config.IsUpstream() + return string(config.Kind) == p.ProxyType } // PatchRoute does nothing. @@ -114,7 +112,13 @@ func (p ratelimit) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_clust // PatchFilter inserts a http local rate_limit filter at the head of // envoy.filters.network.http_connection_manager filters -func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { +func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { + // rate limit is only applied to the inbound listener of the service itself + // since the limit is aggregated from all downstream connections. + if !isInboundListener { + return filter, false, nil + } + if filter.Name != "envoy.filters.network.http_connection_manager" { return filter, false, nil } diff --git a/agent/envoyextensions/builtin/lua/lua.go b/agent/envoyextensions/builtin/lua/lua.go index 6e5eeabcaf..b2f2857b0b 100644 --- a/agent/envoyextensions/builtin/lua/lua.go +++ b/agent/envoyextensions/builtin/lua/lua.go @@ -64,11 +64,11 @@ func (l *lua) validate() error { // CanApply determines if the extension can apply to the given extension configuration. func (l *lua) CanApply(config *extensioncommon.RuntimeConfig) bool { - return string(config.Kind) == l.ProxyType && l.matchesListenerDirection(config) + return string(config.Kind) == l.ProxyType } -func (l *lua) matchesListenerDirection(config *extensioncommon.RuntimeConfig) bool { - return (config.IsUpstream() && l.Listener == "outbound") || (!config.IsUpstream() && l.Listener == "inbound") +func (l *lua) matchesListenerDirection(isInboundListener bool) bool { + return (!isInboundListener && l.Listener == "outbound") || (isInboundListener && l.Listener == "inbound") } // PatchRoute does nothing. @@ -82,7 +82,12 @@ func (l *lua) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3 } // PatchFilter inserts a lua filter directly prior to envoy.filters.http.router. -func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { +func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { + // Make sure filter matches extension config. + if !l.matchesListenerDirection(isInboundListener) { + return filter, false, nil + } + if filter.Name != "envoy.filters.network.http_connection_manager" { return filter, false, nil } diff --git a/agent/envoyextensions/builtin/wasm/structs.go b/agent/envoyextensions/builtin/wasm/structs.go index f6cbdb3c6b..4a29035c15 100644 --- a/agent/envoyextensions/builtin/wasm/structs.go +++ b/agent/envoyextensions/builtin/wasm/structs.go @@ -193,7 +193,7 @@ func (p *pluginConfig) asyncDataSource(rtCfg *extensioncommon.RuntimeConfig) (*e // fetch the data from the upstream source. remote := &p.VmConfig.Code.Remote clusterSNI := "" - for service, upstream := range rtCfg.LocalUpstreams { + for service, upstream := range rtCfg.Upstreams { if service == remote.HttpURI.Service { for sni := range upstream.SNI { clusterSNI = sni diff --git a/agent/envoyextensions/builtin/wasm/wasm.go b/agent/envoyextensions/builtin/wasm/wasm.go index 33abce137e..f3812ebb45 100644 --- a/agent/envoyextensions/builtin/wasm/wasm.go +++ b/agent/envoyextensions/builtin/wasm/wasm.go @@ -68,12 +68,13 @@ func (w *wasm) fromArguments(args map[string]any) error { // CanApply indicates if the WASM extension can be applied to the given extension configuration. // Currently the Wasm extension can be applied if the extension configuration is for an inbound -// listener on the a local connect-proxy. -// It does not patch extensions for service upstreams. +// listener (checked below) on a local connect-proxy. func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool { - return config.IsLocal() && w.wasmConfig.ListenerType == "inbound" && - config.Kind == w.wasmConfig.ProxyType + return config.Kind == w.wasmConfig.ProxyType +} +func (w wasm) matchesConfigDirection(isInboundListener bool) bool { + return isInboundListener && w.wasmConfig.ListenerType == "inbound" } // PatchRoute does nothing for the WASM extension. @@ -88,7 +89,11 @@ func (w wasm) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3 // PatchFilter adds a Wasm filter to the HTTP filter chain. // TODO (wasm/tcp): Add support for TCP filters. -func (w wasm) PatchFilter(cfg *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { +func (w wasm) PatchFilter(cfg *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) { + if !w.matchesConfigDirection(isInboundListener) { + return filter, false, nil + } + if filter.Name != "envoy.filters.network.http_connection_manager" { return filter, false, nil } diff --git a/agent/envoyextensions/builtin/wasm/wasm_test.go b/agent/envoyextensions/builtin/wasm/wasm_test.go index a7791e3a3f..f62744077d 100644 --- a/agent/envoyextensions/builtin/wasm/wasm_test.go +++ b/agent/envoyextensions/builtin/wasm/wasm_test.go @@ -33,21 +33,24 @@ import ( func TestHttpWasmExtension(t *testing.T) { t.Parallel() cases := map[string]struct { - extName string - canApply bool - args func(bool) map[string]any - rtCfg func(bool) *extensioncommon.RuntimeConfig - inputFilters func() []*envoy_http_v3.HttpFilter - expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter - errStr string - debug bool + extName string + canApply bool + args func(bool) map[string]any + rtCfg func(bool) *extensioncommon.RuntimeConfig + isInboundFilter bool + inputFilters func() []*envoy_http_v3.HttpFilter + expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter + expPatched bool + errStr string + debug bool }{ "http remote file": { - extName: api.BuiltinWasmExtension, - canApply: true, - args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) }, - rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, - inputFilters: makeTestHttpFilters, + extName: api.BuiltinWasmExtension, + canApply: true, + args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) }, + rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, + isInboundFilter: true, + inputFilters: makeTestHttpFilters, expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter { return []*envoy_http_v3.HttpFilter{ {Name: "one"}, @@ -65,6 +68,7 @@ func TestHttpWasmExtension(t *testing.T) { {Name: "three"}, } }, + expPatched: true, }, "local file": { extName: api.BuiltinWasmExtension, @@ -76,8 +80,9 @@ func TestHttpWasmExtension(t *testing.T) { cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm" return cfg.toMap(t) }, - rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, - inputFilters: makeTestHttpFilters, + rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, + isInboundFilter: true, + inputFilters: makeTestHttpFilters, expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter { return []*envoy_http_v3.HttpFilter{ {Name: "one"}, @@ -95,6 +100,24 @@ func TestHttpWasmExtension(t *testing.T) { {Name: "three"}, } }, + expPatched: true, + }, + "inbound filters ignored": { + extName: api.BuiltinWasmExtension, + canApply: true, + args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) }, + rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, + isInboundFilter: false, + inputFilters: makeTestHttpFilters, + expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter { + return []*envoy_http_v3.HttpFilter{ + {Name: "one"}, + {Name: "two"}, + {Name: "envoy.filters.http.router"}, + {Name: "three"}, + } + }, + expPatched: false, }, "no cluster for remote file": { extName: api.BuiltinWasmExtension, @@ -102,11 +125,13 @@ func TestHttpWasmExtension(t *testing.T) { args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) }, rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { rt := makeTestRuntimeConfig(ent) - rt.LocalUpstreams = nil + rt.Upstreams = nil return rt }, - inputFilters: makeTestHttpFilters, - errStr: "no upstream found for remote service", + isInboundFilter: true, + inputFilters: makeTestHttpFilters, + errStr: "no upstream found for remote service", + expPatched: false, }, } @@ -140,10 +165,10 @@ func TestHttpWasmExtension(t *testing.T) { require.NoError(t, err) inputHttpConMgr := makeHttpConMgr(t, c.inputFilters()) - obsHttpConMgr, patched, err := w.PatchFilter(c.rtCfg(enterprise), inputHttpConMgr) + obsHttpConMgr, patched, err := w.PatchFilter(c.rtCfg(enterprise), inputHttpConMgr, c.isInboundFilter) if c.errStr == "" { require.NoError(t, err) - require.True(t, patched) + require.Equal(t, c.expPatched, patched) cfg := testWasmConfigFromMap(t, c.args(enterprise)) expHttpConMgr := makeHttpConMgr(t, c.expFilters(cfg)) @@ -156,6 +181,7 @@ func TestHttpWasmExtension(t *testing.T) { prototest.AssertDeepEqual(t, expHttpConMgr, obsHttpConMgr) } else { + require.Error(t, err) require.Contains(t, err.Error(), c.errStr) } @@ -554,7 +580,7 @@ func makeTestRuntimeConfig(enterprise bool) *extensioncommon.RuntimeConfig { return &extensioncommon.RuntimeConfig{ Kind: api.ServiceKindConnectProxy, ServiceName: api.CompoundServiceName{Name: "test-service"}, - LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ + Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ { Name: "test-file-server", Namespace: acl.NamespaceOrDefault(ns), diff --git a/agent/envoyextensions/registered_extensions_test.go b/agent/envoyextensions/registered_extensions_test.go index 746810ffc3..ebd1bfbbc9 100644 --- a/agent/envoyextensions/registered_extensions_test.go +++ b/agent/envoyextensions/registered_extensions_test.go @@ -4,9 +4,11 @@ package envoyextensions import ( + "fmt" "testing" "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/envoyextensions/extensioncommon" "github.com/stretchr/testify/require" ) @@ -61,3 +63,52 @@ func TestValidateExtensions(t *testing.T) { }) } } + +// This test is included here so that we can test all registered extensions without creating a cyclic dependency between +// envoyextensions and extensioncommon. +func TestUpstreamExtenderLimitations(t *testing.T) { + type testCase struct { + config *extensioncommon.RuntimeConfig + ok bool + errMsg string + } + unauthorizedExtensionCase := func(name string) testCase { + return testCase{ + config: &extensioncommon.RuntimeConfig{ + Kind: api.ServiceKindConnectProxy, + ServiceName: api.CompoundServiceName{Name: "api"}, + Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{}, + IsSourcedFromUpstream: true, + EnvoyExtension: api.EnvoyExtension{ + Name: name, + }, + }, + ok: false, + errMsg: fmt.Sprintf("extension %q is not permitted to be applied via upstream service config", name), + } + } + cases := map[string]testCase{ + // Make sure future extensions are theoretically covered, even if not registered in the same way. + "unknown extension": unauthorizedExtensionCase("someotherextension"), + } + for name := range extensionConstructors { + // AWS Lambda is the only extension permitted to modify downstream proxy resources. + if name == api.BuiltinAWSLambdaExtension { + continue + } + cases[name] = unauthorizedExtensionCase(name) + } + + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + extender := extensioncommon.UpstreamEnvoyExtender{} + _, err := extender.Extend(nil, tc.config) + if tc.ok { + require.NoError(t, err) + } else { + require.Error(t, err) + require.ErrorContains(t, err, tc.errMsg) + } + }) + } +} diff --git a/agent/xds/delta_envoy_extender_oss_test.go b/agent/xds/delta_envoy_extender_oss_test.go index 490bf016a0..6e867be295 100644 --- a/agent/xds/delta_envoy_extender_oss_test.go +++ b/agent/xds/delta_envoy_extender_oss_test.go @@ -62,17 +62,16 @@ func TestEnvoyExtenderWithSnapshot(t *testing.T) { } } - makeLuaServiceDefaults := func(inbound bool) *structs.ServiceConfigEntry { + // Apply Lua extension to the local service and ensure http is used so the extension can be applied. + makeLuaNsFunc := func(inbound bool) func(ns *structs.NodeService) { listener := "inbound" if !inbound { listener = "outbound" } - return &structs.ServiceConfigEntry{ - Kind: structs.ServiceDefaults, - Name: "db", - Protocol: "http", - EnvoyExtensions: []structs.EnvoyExtension{ + return func(ns *structs.NodeService) { + ns.Proxy.Config["protocol"] = "http" + ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ { Name: api.BuiltinLuaExtension, Arguments: map[string]interface{}{ @@ -84,7 +83,7 @@ function envoy_on_request(request_handle) end`, }, }, - }, + } } } @@ -130,57 +129,47 @@ end`, create: proxycfg.TestConfigSnapshotTerminatingGatewayWithLambdaServiceAndServiceResolvers, }, { - name: "lua-outbound-applies-to-upstreams", + name: "lua-outbound-applies-to-local-upstreams", create: func(t testinf.T) *proxycfg.ConfigSnapshot { - return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nil, nil, makeLuaServiceDefaults(false)) + // upstreams need to be http in order for lua to be applied to listeners. + return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false), nil, &structs.ServiceConfigEntry{ + Kind: structs.ServiceDefaults, + Name: "db", + Protocol: "http", + }, &structs.ServiceConfigEntry{ + Kind: structs.ServiceDefaults, + Name: "geo-cache", + Protocol: "http", + }) }, }, { - name: "lua-inbound-doesnt-applies-to-upstreams", + // We expect an inbound public listener lua filter here because the extension targets inbound. + // The only difference between goldens for this and lua-inbound-applies-to-inbound + // should be that db has HTTP filters rather than TCP. + name: "lua-inbound-doesnt-apply-to-local-upstreams", create: func(t testinf.T) *proxycfg.ConfigSnapshot { - return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nil, nil, makeLuaServiceDefaults(true)) + // db is made an HTTP upstream so that the extension _could_ apply, but does not because + // the direction for the extension is inbound. + return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(true), nil, &structs.ServiceConfigEntry{ + Kind: structs.ServiceDefaults, + Name: "db", + Protocol: "http", + }) }, }, { name: "lua-inbound-applies-to-inbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { - return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { - ns.Proxy.Config["protocol"] = "http" - ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ - { - Name: api.BuiltinLuaExtension, - Arguments: map[string]interface{}{ - "ProxyType": "connect-proxy", - "Listener": "inbound", - "Script": ` -function envoy_on_request(request_handle) - request_handle:headers():add("test", "test") -end`, - }, - }, - } - }, nil) + return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(true), nil) }, }, { + // We expect _no_ lua filters here, because the extension targets outbound, but there are + // no upstream HTTP services. We also should not see public listener, which is HTTP, patched. name: "lua-outbound-doesnt-apply-to-inbound", create: func(t testinf.T) *proxycfg.ConfigSnapshot { - return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { - ns.Proxy.Config["protocol"] = "http" - ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ - { - Name: api.BuiltinLuaExtension, - Arguments: map[string]interface{}{ - "ProxyType": "connect-proxy", - "Listener": "outbound", - "Script": ` -function envoy_on_request(request_handle) - request_handle:headers():add("test", "test") -end`, - }, - }, - } - }, nil) + return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false), nil) }, }, { diff --git a/agent/xds/delta_test.go b/agent/xds/delta_test.go index 1f4fd5e00c..a9febfedfa 100644 --- a/agent/xds/delta_test.go +++ b/agent/xds/delta_test.go @@ -52,20 +52,20 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP(t *testing.T) { var snap *proxycfg.ConfigSnapshot testutil.RunStep(t, "initial setup", func(t *testing.T) { - snap = newTestSnapshot(t, nil, "", &structs.ProxyConfigEntry{ - Kind: structs.ProxyDefaults, - Name: structs.ProxyConfigGlobal, - EnvoyExtensions: []structs.EnvoyExtension{ - { - Name: api.BuiltinLuaExtension, - Arguments: map[string]interface{}{ - "ProxyType": "connect-proxy", - "Listener": "inbound", - "Script": "x = 0", + snap = newTestSnapshot(t, nil, "", + func(ns *structs.NodeService) { + // Add extension for local proxy. + ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{ + { + Name: api.BuiltinLuaExtension, + Arguments: map[string]interface{}{ + "ProxyType": "connect-proxy", + "Listener": "inbound", + "Script": "x = 0", + }, }, - }, - }, - }) + } + }) // Send initial cluster discover. We'll assume we are testing a partial // reconnect and include some initial resource versions that will be @@ -194,7 +194,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP(t *testing.T) { assertDeltaChanBlocked(t, envoy.deltaStream.sendCh) // now reconfigure the snapshot and JUST edit the endpoints to strike one of the two current endpoints for db. - snap = newTestSnapshot(t, snap, "") + snap = newTestSnapshot(t, snap, "", nil) deleteAllButOneEndpoint(snap, UID("db"), "db.default.default.dc1") mgr.DeliverConfig(t, sid, snap) @@ -204,7 +204,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP(t *testing.T) { testutil.RunStep(t, "restore endpoint subscription", func(t *testing.T) { // Restore db's deleted endpoints by generating a new snapshot. - snap = newTestSnapshot(t, snap, "") + snap = newTestSnapshot(t, snap, "", nil) mgr.DeliverConfig(t, sid, snap) // We never send an EDS reply about this change because Envoy is still not subscribed to db. @@ -266,7 +266,7 @@ func TestServer_DeltaAggregatedResources_v3_NackLoop(t *testing.T) { var snap *proxycfg.ConfigSnapshot testutil.RunStep(t, "initial setup", func(t *testing.T) { - snap = newTestSnapshot(t, nil, "") + snap = newTestSnapshot(t, nil, "", nil) // Plug in a bad port for the public listener snap.Port = 1 @@ -402,7 +402,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2(t *testing.T) { assertDeltaChanBlocked(t, envoy.deltaStream.sendCh) // Deliver a new snapshot (tcp with one http upstream) - snap := newTestSnapshot(t, nil, "http2", &structs.ServiceConfigEntry{ + snap := newTestSnapshot(t, nil, "http2", nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http2", @@ -476,7 +476,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2(t *testing.T) { // -- reconfigure with a no-op discovery chain - snap = newTestSnapshot(t, snap, "http2", &structs.ServiceConfigEntry{ + snap = newTestSnapshot(t, snap, "http2", nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http2", @@ -565,7 +565,7 @@ func TestServer_DeltaAggregatedResources_v3_SlowEndpointPopulation(t *testing.T) var snap *proxycfg.ConfigSnapshot testutil.RunStep(t, "get into initial state", func(t *testing.T) { - snap = newTestSnapshot(t, nil, "") + snap = newTestSnapshot(t, nil, "", nil) // Send initial cluster discover. envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{}) @@ -651,7 +651,7 @@ func TestServer_DeltaAggregatedResources_v3_SlowEndpointPopulation(t *testing.T) testutil.RunStep(t, "delayed endpoint update finally comes in", func(t *testing.T) { // Trigger the xds.Server select{} to wake up and notice our hack is disabled. // The actual contents of this change are irrelevant. - snap = newTestSnapshot(t, snap, "") + snap = newTestSnapshot(t, snap, "", nil) mgr.DeliverConfig(t, sid, snap) assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ @@ -694,7 +694,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP_clusterChangesImpa var snap *proxycfg.ConfigSnapshot testutil.RunStep(t, "get into initial state", func(t *testing.T) { - snap = newTestSnapshot(t, nil, "") + snap = newTestSnapshot(t, nil, "", nil) // Send initial cluster discover. envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{}) @@ -770,7 +770,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP_clusterChangesImpa testutil.RunStep(t, "trigger cluster update needing implicit endpoint replacements", func(t *testing.T) { // Update the snapshot in a way that causes a single cluster update. - snap = newTestSnapshot(t, snap, "", &structs.ServiceResolverConfigEntry{ + snap = newTestSnapshot(t, snap, "", nil, &structs.ServiceResolverConfigEntry{ Kind: structs.ServiceResolver, Name: "db", ConnectTimeout: 1337 * time.Second, @@ -839,7 +839,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2_RDS_listenerChan assertDeltaChanBlocked(t, envoy.deltaStream.sendCh) // Deliver a new snapshot (tcp with one http upstream with no-op disco chain) - snap = newTestSnapshot(t, nil, "http2", &structs.ServiceConfigEntry{ + snap = newTestSnapshot(t, nil, "http2", nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http2", @@ -934,7 +934,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2_RDS_listenerChan // Update the snapshot in a way that causes a single listener update. // // Downgrade from http2 to http - snap = newTestSnapshot(t, snap, "http", &structs.ServiceConfigEntry{ + snap = newTestSnapshot(t, snap, "http", nil, &structs.ServiceConfigEntry{ Kind: structs.ServiceDefaults, Name: "db", Protocol: "http", @@ -1110,7 +1110,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLEnforcement(t *testing.T) { // Deliver a new snapshot snap := tt.cfgSnap if snap == nil { - snap = newTestSnapshot(t, nil, "") + snap = newTestSnapshot(t, nil, "", nil) } mgr.DeliverConfig(t, sid, snap) @@ -1236,7 +1236,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLTokenDeleted_StreamTerminatedDuri } // Deliver a new snapshot - snap := newTestSnapshot(t, nil, "") + snap := newTestSnapshot(t, nil, "", nil) mgr.DeliverConfig(t, sid, snap) assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ @@ -1334,7 +1334,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLTokenDeleted_StreamTerminatedInBa } // Deliver a new snapshot - snap := newTestSnapshot(t, nil, "") + snap := newTestSnapshot(t, nil, "", nil) mgr.DeliverConfig(t, sid, snap) assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ @@ -1444,7 +1444,7 @@ func TestServer_DeltaAggregatedResources_v3_CapacityReached(t *testing.T) { mgr.RegisterProxy(t, sid) mgr.DrainStreams(sid) - snap := newTestSnapshot(t, nil, "") + snap := newTestSnapshot(t, nil, "", nil) envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{ InitialResourceVersions: mustMakeVersionMap(t, @@ -1477,7 +1477,7 @@ func TestServer_DeltaAggregatedResources_v3_StreamDrained(t *testing.T) { mgr.RegisterProxy(t, sid) testutil.RunStep(t, "successful request/response", func(t *testing.T) { - snap := newTestSnapshot(t, nil, "") + snap := newTestSnapshot(t, nil, "", nil) envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{ InitialResourceVersions: mustMakeVersionMap(t, diff --git a/agent/xds/extensionruntime/runtime_config.go b/agent/xds/extensionruntime/runtime_config.go index 455c32001d..2c7522131e 100644 --- a/agent/xds/extensionruntime/runtime_config.go +++ b/agent/xds/extensionruntime/runtime_config.go @@ -86,13 +86,12 @@ func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.Compound cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions) for _, ext := range cfgSnapExts { extCfg := extensioncommon.RuntimeConfig{ - EnvoyExtension: ext, - ServiceName: localSvc, - // Upstreams is nil to signify this extension is not being applied to an upstream service, but rather to the local service. - Upstreams: nil, - LocalUpstreams: upstreamMap, - Kind: kind, - Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config), + EnvoyExtension: ext, + ServiceName: localSvc, + IsSourcedFromUpstream: false, + Upstreams: upstreamMap, + Kind: kind, + Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config), } extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg) } @@ -124,17 +123,22 @@ func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.Compound } } + // If applicable, include extension configuration for remote upstreams of the local service. + // This only applies to specific extensions authorized to apply to remote proxies. for svc, exts := range extensionsMap { extensionConfigurationsMap[svc] = []extensioncommon.RuntimeConfig{} for _, ext := range exts { - extCfg := extensioncommon.RuntimeConfig{ - EnvoyExtension: ext, - Kind: kind, - ServiceName: svc, - Upstreams: upstreamMap, - Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config), + if appliesToRemoteDownstreams(ext) { + extCfg := extensioncommon.RuntimeConfig{ + EnvoyExtension: ext, + Kind: kind, + ServiceName: svc, + IsSourcedFromUpstream: true, + Upstreams: upstreamMap, + Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config), + } + extensionConfigurationsMap[svc] = append(extensionConfigurationsMap[svc], extCfg) } - extensionConfigurationsMap[svc] = append(extensionConfigurationsMap[svc], extCfg) } } @@ -169,3 +173,16 @@ func proxyConfigProtocol(cfg map[string]any) string { } return "" } + +// appliesToRemoteDownstreams returns true if the given extension should be applied to remote downstream proxies of the +// service targeted by the extension, rather than just the local proxy. In the context of GetRuntimeConfigurations, this +// determines whether the extension should apply to the local proxy (a downstream of the configured service). +// +// Currently, only the AWS Lambda and Validate extensions are allowed to apply to downstream proxies. +// +// See extensioncommon.RuntimeConfig.IsSourcedFromUpstream and UpstreamEnvoyExtender doc for more information. We make +// this check here out of precaution s.t. even if an unauthorized extension is erroneously constructed with the +// UpstreamEnvoyExtender, this check will not allow the upstream extension configuration to be provided. +func appliesToRemoteDownstreams(extension api.EnvoyExtension) bool { + return extension.Name == api.BuiltinAWSLambdaExtension || extension.Name == api.BuiltinValidateExtension +} diff --git a/agent/xds/extensionruntime/runtime_config_oss_test.go b/agent/xds/extensionruntime/runtime_config_oss_test.go index 24c7ed6ad7..257fd5d737 100644 --- a/agent/xds/extensionruntime/runtime_config_oss_test.go +++ b/agent/xds/extensionruntime/runtime_config_oss_test.go @@ -54,7 +54,8 @@ func TestGetRuntimeConfigurations_TerminatingGateway(t *testing.T) { "PayloadPassthrough": true, }, }, - ServiceName: webService, + ServiceName: webService, + IsSourcedFromUpstream: true, Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ apiService: { SNI: map[string]struct{}{ @@ -107,7 +108,8 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { Namespace: "default", } - // Setup multiple extensions to ensure all of them are in the ExtensionConfiguration map. + // Setup multiple extensions to ensure only the expected one (AWS) is in the ExtensionConfiguration map + // sourced from upstreams, and all local extensions are included. envoyExtensions := []structs.EnvoyExtension{ { Name: api.BuiltinAWSLambdaExtension, @@ -158,27 +160,8 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { "PayloadPassthrough": true, }, }, - ServiceName: dbService, - Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ - dbService: { - SNI: map[string]struct{}{ - "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, - }, - EnvoyID: "db", - OutgoingProxyKind: "connect-proxy", - }, - }, - Kind: api.ServiceKindConnectProxy, - }, - { - EnvoyExtension: api.EnvoyExtension{ - Name: "ext2", - Arguments: map[string]interface{}{ - "arg1": 1, - "arg2": "val2", - }, - }, - ServiceName: dbService, + ServiceName: dbService, + IsSourcedFromUpstream: true, Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ dbService: { SNI: map[string]struct{}{ @@ -206,27 +189,8 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { "PayloadPassthrough": true, }, }, - ServiceName: dbService, - Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ - dbService: { - SNI: map[string]struct{}{ - "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, - }, - EnvoyID: "db", - OutgoingProxyKind: "terminating-gateway", - }, - }, - Kind: api.ServiceKindConnectProxy, - }, - { - EnvoyExtension: api.EnvoyExtension{ - Name: "ext2", - Arguments: map[string]interface{}{ - "arg1": 1, - "arg2": "val2", - }, - }, - ServiceName: dbService, + ServiceName: dbService, + IsSourcedFromUpstream: true, Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ dbService: { SNI: map[string]struct{}{ @@ -255,10 +219,10 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { "PayloadPassthrough": true, }, }, - ServiceName: webService, - Kind: api.ServiceKindConnectProxy, - Upstreams: nil, - LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ + ServiceName: webService, + Kind: api.ServiceKindConnectProxy, + IsSourcedFromUpstream: false, + Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ dbService: { SNI: map[string]struct{}{ "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, @@ -276,10 +240,10 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) { "arg2": "val2", }, }, - ServiceName: webService, - Kind: api.ServiceKindConnectProxy, - Upstreams: nil, - LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ + ServiceName: webService, + Kind: api.ServiceKindConnectProxy, + IsSourcedFromUpstream: false, + Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ dbService: { SNI: map[string]struct{}{ "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, diff --git a/agent/xds/testdata/builtin_extension/clusters/lua-inbound-doesnt-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/clusters/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden similarity index 100% rename from agent/xds/testdata/builtin_extension/clusters/lua-inbound-doesnt-applies-to-upstreams.latest.golden rename to agent/xds/testdata/builtin_extension/clusters/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden diff --git a/agent/xds/testdata/builtin_extension/clusters/lua-outbound-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/clusters/lua-outbound-applies-to-local-upstreams.latest.golden similarity index 100% rename from agent/xds/testdata/builtin_extension/clusters/lua-outbound-applies-to-upstreams.latest.golden rename to agent/xds/testdata/builtin_extension/clusters/lua-outbound-applies-to-local-upstreams.latest.golden diff --git a/agent/xds/testdata/builtin_extension/endpoints/lua-inbound-doesnt-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/endpoints/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden similarity index 100% rename from agent/xds/testdata/builtin_extension/endpoints/lua-inbound-doesnt-applies-to-upstreams.latest.golden rename to agent/xds/testdata/builtin_extension/endpoints/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden diff --git a/agent/xds/testdata/builtin_extension/endpoints/lua-outbound-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/endpoints/lua-outbound-applies-to-local-upstreams.latest.golden similarity index 100% rename from agent/xds/testdata/builtin_extension/endpoints/lua-outbound-applies-to-upstreams.latest.golden rename to agent/xds/testdata/builtin_extension/endpoints/lua-outbound-applies-to-local-upstreams.latest.golden diff --git a/agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-applies-to-upstreams.latest.golden deleted file mode 100644 index f8ad4a4b40..0000000000 --- a/agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-applies-to-upstreams.latest.golden +++ /dev/null @@ -1,146 +0,0 @@ -{ - "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.http_connection_manager", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", - "statPrefix": "upstream.db.default.default.dc1", - "routeConfig": { - "name": "db", - "virtualHosts": [ - { - "name": "db.default.default.dc1", - "domains": [ - "*" - ], - "routes": [ - { - "match": { - "prefix": "/" - }, - "route": { - "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" - } - } - ] - } - ] - }, - "httpFilters": [ - { - "name": "envoy.filters.http.router", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" - } - } - ], - "tracing": { - "randomSampling": {} - } - } - } - ] - } - ], - "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/builtin_extension/listeners/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden new file mode 100644 index 0000000000..01dd7bfa51 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/listeners/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden @@ -0,0 +1,272 @@ +{ + "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.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "upstream.db.default.default.dc1", + "routeConfig": { + "name": "db", + "virtualHosts": [ + { + "name": "db.default.default.dc1", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + } + } + } + ] + } + ], + "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.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "public_listener", + "routeConfig": { + "name": "public_listener", + "virtualHosts": [ + { + "name": "public_listener", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": {} + } + }, + { + "name": "envoy.filters.http.header_to_metadata", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config", + "requestRules": [ + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "trust-domain", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\1" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "partition", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\2" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "namespace", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\3" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "datacenter", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\4" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "service", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\5" + } + } + } + ] + } + }, + { + "name": "envoy.filters.http.lua", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua", + "inlineCode": "\nfunction envoy_on_request(request_handle)\n request_handle:headers():add(\"test\", \"test\")\nend" + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + }, + "forwardClientCertDetails": "APPEND_FORWARD", + "setCurrentClientCertDetails": { + "subject": true, + "cert": true, + "chain": true, + "dns": true, + "uri": true + } + } + } + ], + "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" + } + }, + "alpnProtocols": [ + "http/1.1" + ] + }, + "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/builtin_extension/listeners/lua-outbound-applies-to-local-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/listeners/lua-outbound-applies-to-local-upstreams.latest.golden new file mode 100644 index 0000000000..5ebbbafde8 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/listeners/lua-outbound-applies-to-local-upstreams.latest.golden @@ -0,0 +1,272 @@ +{ + "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.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "upstream.db.default.default.dc1", + "routeConfig": { + "name": "db", + "virtualHosts": [ + { + "name": "db.default.default.dc1", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.lua", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua", + "inlineCode": "\nfunction envoy_on_request(request_handle)\n request_handle:headers():add(\"test\", \"test\")\nend" + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + } + } + } + ] + } + ], + "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.http_connection_manager", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", + "statPrefix": "public_listener", + "routeConfig": { + "name": "public_listener", + "virtualHosts": [ + { + "name": "public_listener", + "domains": [ + "*" + ], + "routes": [ + { + "match": { + "prefix": "/" + }, + "route": { + "cluster": "local_app" + } + } + ] + } + ] + }, + "httpFilters": [ + { + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": {} + } + }, + { + "name": "envoy.filters.http.header_to_metadata", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config", + "requestRules": [ + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "trust-domain", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\1" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "partition", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\2" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "namespace", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\3" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "datacenter", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\4" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "service", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\5" + } + } + } + ] + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + }, + "forwardClientCertDetails": "APPEND_FORWARD", + "setCurrentClientCertDetails": { + "subject": true, + "cert": true, + "chain": true, + "dns": true, + "uri": true + } + } + } + ], + "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" + } + }, + "alpnProtocols": [ + "http/1.1" + ] + }, + "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/builtin_extension/listeners/lua-outbound-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/listeners/lua-outbound-applies-to-upstreams.latest.golden deleted file mode 100644 index 34933d225e..0000000000 --- a/agent/xds/testdata/builtin_extension/listeners/lua-outbound-applies-to-upstreams.latest.golden +++ /dev/null @@ -1,153 +0,0 @@ -{ - "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.http_connection_manager", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager", - "statPrefix": "upstream.db.default.default.dc1", - "routeConfig": { - "name": "db", - "virtualHosts": [ - { - "name": "db.default.default.dc1", - "domains": [ - "*" - ], - "routes": [ - { - "match": { - "prefix": "/" - }, - "route": { - "cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul" - } - } - ] - } - ] - }, - "httpFilters": [ - { - "name": "envoy.filters.http.lua", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua", - "inlineCode": "\nfunction envoy_on_request(request_handle)\n request_handle:headers():add(\"test\", \"test\")\nend" - } - }, - { - "name": "envoy.filters.http.router", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" - } - } - ], - "tracing": { - "randomSampling": {} - } - } - } - ] - } - ], - "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/builtin_extension/routes/lua-inbound-doesnt-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/routes/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden similarity index 100% rename from agent/xds/testdata/builtin_extension/routes/lua-inbound-doesnt-applies-to-upstreams.latest.golden rename to agent/xds/testdata/builtin_extension/routes/lua-inbound-doesnt-apply-to-local-upstreams.latest.golden diff --git a/agent/xds/testdata/builtin_extension/routes/lua-outbound-applies-to-upstreams.latest.golden b/agent/xds/testdata/builtin_extension/routes/lua-outbound-applies-to-local-upstreams.latest.golden similarity index 100% rename from agent/xds/testdata/builtin_extension/routes/lua-outbound-applies-to-upstreams.latest.golden rename to agent/xds/testdata/builtin_extension/routes/lua-outbound-applies-to-local-upstreams.latest.golden diff --git a/agent/xds/xds_protocol_helpers_test.go b/agent/xds/xds_protocol_helpers_test.go index 5b153f4021..cfd91cb187 100644 --- a/agent/xds/xds_protocol_helpers_test.go +++ b/agent/xds/xds_protocol_helpers_test.go @@ -48,9 +48,10 @@ func newTestSnapshot( t *testing.T, prevSnap *proxycfg.ConfigSnapshot, dbServiceProtocol string, + nsFn func(ns *structs.NodeService), additionalEntries ...structs.ConfigEntry, ) *proxycfg.ConfigSnapshot { - snap := proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nil, nil, additionalEntries...) + snap := proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, nsFn, nil, additionalEntries...) snap.ConnectProxy.PreparedQueryEndpoints = map[proxycfg.UpstreamID]structs.CheckServiceNodes{ UID("prepared_query:geo-cache"): proxycfg.TestPreparedQueryNodes(t, "geo-cache"), } diff --git a/api/config_entry.go b/api/config_entry.go index 259031b936..4f90f97c87 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -43,6 +43,10 @@ const ( BuiltinLuaExtension string = "builtin/lua" BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit" BuiltinWasmExtension string = "builtin/wasm" + // BuiltinValidateExtension should not be exposed directly or accepted as a valid configured + // extension type, as it is only used indirectly via troubleshooting tools. It is included here + // for common reference alongside other builtin extensions. + BuiltinValidateExtension string = "builtin/proxy/validate" ) type ConfigEntry interface { diff --git a/envoyextensions/extensioncommon/basic_envoy_extender.go b/envoyextensions/extensioncommon/basic_envoy_extender.go index 06a3f4516f..851f5a3a4c 100644 --- a/envoyextensions/extensioncommon/basic_envoy_extender.go +++ b/envoyextensions/extensioncommon/basic_envoy_extender.go @@ -5,8 +5,6 @@ package extensioncommon import ( "fmt" - "strings" - envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" @@ -37,7 +35,7 @@ type BasicExtension interface { // PatchFilter patches an Envoy filter to include the custom Envoy // configuration required to integrate with the built in extension template. - PatchFilter(*RuntimeConfig, *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) + PatchFilter(cfg *RuntimeConfig, f *envoy_listener_v3.Filter, isInboundListener bool) (*envoy_listener_v3.Filter, bool, error) } var _ EnvoyExtender = (*BasicEnvoyExtender)(nil) @@ -48,20 +46,26 @@ type BasicEnvoyExtender struct { Extension BasicExtension } -func (envoyExtension *BasicEnvoyExtender) Validate(config *RuntimeConfig) error { +func (b *BasicEnvoyExtender) Validate(_ *RuntimeConfig) error { return nil } -func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) { +func (b *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) { var resultErr error + // We don't support patching the local proxy with an upstream's config except in special + // cases supported by UpstreamEnvoyExtender. + if config.IsSourcedFromUpstream { + return nil, fmt.Errorf("%q extension applied as local config but is sourced from an upstream of the local service", config.EnvoyExtension.Name) + } + switch config.Kind { case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: default: return resources, nil } - if !envoyExtender.Extension.CanApply(config) { + if !b.Extension.CanApply(config) { return resources, nil } @@ -73,19 +77,7 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso for nameOrSNI, msg := range resources.Index[indexType] { switch resource := msg.(type) { case *envoy_cluster_v3.Cluster: - // If the Envoy extension configuration is for an upstream service, the Cluster's - // name must match the upstream service's SNI. - if config.IsUpstream() && !config.MatchesUpstreamServiceSNI(nameOrSNI) { - continue - } - - // If the extension's config is for an an inbound listener, the Cluster's name - // must be xdscommon.LocalAppClusterName. - if !config.IsUpstream() && nameOrSNI == xdscommon.LocalAppClusterName { - continue - } - - newCluster, patched, err := envoyExtender.Extension.PatchCluster(config, resource) + newCluster, patched, err := b.Extension.PatchCluster(config, resource) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster: %w", err)) continue @@ -95,7 +87,7 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso } case *envoy_listener_v3.Listener: - newListener, patched, err := envoyExtender.patchListener(config, resource) + newListener, patched, err := b.patchListener(config, resource) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener: %w", err)) continue @@ -105,19 +97,7 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso } case *envoy_route_v3.RouteConfiguration: - // If the Envoy extension configuration is for an upstream service, the route's - // name must match the upstream service's Envoy ID. - matchesEnvoyID := config.EnvoyID() == nameOrSNI - if config.IsUpstream() && !config.MatchesUpstreamServiceSNI(nameOrSNI) && !matchesEnvoyID { - continue - } - - // There aren't routes for inbound services. - if !config.IsUpstream() { - continue - } - - newRoute, patched, err := envoyExtender.Extension.PatchRoute(config, resource) + newRoute, patched, err := b.Extension.PatchRoute(config, resource) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("error patching route: %w", err)) continue @@ -134,40 +114,25 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso return resources, resultErr } -func (envoyExtension BasicEnvoyExtender) patchListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { switch config.Kind { case api.ServiceKindTerminatingGateway: - return envoyExtension.patchTerminatingGatewayListener(config, l) + return b.patchTerminatingGatewayListener(config, l) case api.ServiceKindConnectProxy: - return envoyExtension.patchConnectProxyListener(config, l) + return b.patchConnectProxyListener(config, l) } return l, false, nil } -func (b BasicEnvoyExtender) patchTerminatingGatewayListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { - // We don't support directly targeting terminating gateways with extensions. - if !config.IsUpstream() { - return l, false, nil - } - +func (b *BasicEnvoyExtender) patchTerminatingGatewayListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { var resultErr error patched := false + for _, filterChain := range l.FilterChains { - sni := getSNI(filterChain) - - if sni == "" { - continue - } - - // The filter chain's SNI must match the upstream service's SNI. - if !config.MatchesUpstreamServiceSNI(sni) { - continue - } - var filters []*envoy_listener_v3.Filter for _, filter := range filterChain.Filters { - newFilter, ok, err := b.Extension.PatchFilter(config, filter) + newFilter, ok, err := b.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) @@ -187,37 +152,19 @@ func (b BasicEnvoyExtender) patchTerminatingGatewayListener(config *RuntimeConfi return l, patched, resultErr } -func (b BasicEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { var resultErr error + patched := false - envoyID := "" - if i := strings.IndexByte(l.Name, ':'); i != -1 { - envoyID = l.Name[:i] - } - - if config.IsUpstream() && envoyID == xdscommon.OutboundListenerName { + if IsOutboundTProxyListener(l) { return b.patchTProxyListener(config, l) } - // If the Envoy extension configuration is for an upstream service, the listener's - // name must match the upstream service's EnvoyID or be the outbound listener. - if config.IsUpstream() && envoyID != config.EnvoyID() { - return l, false, nil - } - - // If the Envoy extension configuration is for inbound resources, the - // listener must be named xdscommon.PublicListenerName. - if !config.IsUpstream() && envoyID != xdscommon.PublicListenerName { - return l, false, nil - } - - var patched bool - for _, filterChain := range l.FilterChains { var filters []*envoy_listener_v3.Filter for _, filter := range filterChain.Filters { - newFilter, ok, err := b.Extension.PatchFilter(config, filter) + newFilter, ok, err := b.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) filters = append(filters, filter) @@ -237,7 +184,7 @@ func (b BasicEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l * return l, patched, resultErr } -func (b BasicEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { +func (b *BasicEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { var resultErr error patched := false @@ -252,7 +199,7 @@ func (b BasicEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_ } for _, filter := range filterChain.Filters { - newFilter, ok, err := b.Extension.PatchFilter(config, filter) + newFilter, ok, err := b.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) if err != nil { resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) filters = append(filters, filter) diff --git a/envoyextensions/extensioncommon/envoy_extender_test.go b/envoyextensions/extensioncommon/envoy_extender_test.go new file mode 100644 index 0000000000..6a5c941166 --- /dev/null +++ b/envoyextensions/extensioncommon/envoy_extender_test.go @@ -0,0 +1,74 @@ +package extensioncommon + +import ( + "fmt" + "testing" + + "github.com/hashicorp/consul/api" + "github.com/stretchr/testify/require" +) + +func TestUpstreamConfigSourceLimitations(t *testing.T) { + type testCase struct { + extender EnvoyExtender + config *RuntimeConfig + ok bool + errMsg string + } + cases := map[string]testCase{ + "upstream extender non-upstream config": { + extender: &UpstreamEnvoyExtender{}, + config: &RuntimeConfig{ + Kind: api.ServiceKindConnectProxy, + ServiceName: api.CompoundServiceName{Name: "api"}, + Upstreams: map[api.CompoundServiceName]*UpstreamData{}, + IsSourcedFromUpstream: false, + EnvoyExtension: api.EnvoyExtension{ + Name: api.BuiltinAWSLambdaExtension, + }, + }, + ok: false, + errMsg: fmt.Sprintf("%q extension applied as upstream config but is not sourced from an upstream of the local service", api.BuiltinAWSLambdaExtension), + }, + "basic extender upstream config": { + extender: &BasicEnvoyExtender{}, + config: &RuntimeConfig{ + Kind: api.ServiceKindConnectProxy, + ServiceName: api.CompoundServiceName{Name: "api"}, + Upstreams: map[api.CompoundServiceName]*UpstreamData{}, + IsSourcedFromUpstream: true, + EnvoyExtension: api.EnvoyExtension{ + Name: api.BuiltinLuaExtension, + }, + }, + ok: false, + errMsg: fmt.Sprintf("%q extension applied as local config but is sourced from an upstream of the local service", api.BuiltinLuaExtension), + }, + "list extender upstream config": { + extender: &ListEnvoyExtender{}, + config: &RuntimeConfig{ + Kind: api.ServiceKindConnectProxy, + ServiceName: api.CompoundServiceName{Name: "api"}, + Upstreams: map[api.CompoundServiceName]*UpstreamData{}, + IsSourcedFromUpstream: true, + EnvoyExtension: api.EnvoyExtension{ + Name: api.BuiltinLuaExtension, + }, + }, + ok: false, + errMsg: fmt.Sprintf("%q extension applied as local config but is sourced from an upstream of the local service", api.BuiltinLuaExtension), + }, + } + + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + _, err := tc.extender.Extend(nil, tc.config) + if tc.ok { + require.NoError(t, err) + } else { + require.Error(t, err) + require.ErrorContains(t, err, tc.errMsg) + } + }) + } +} diff --git a/envoyextensions/extensioncommon/list_envoy_extender.go b/envoyextensions/extensioncommon/list_envoy_extender.go index 6c491a5d00..c326d18570 100644 --- a/envoyextensions/extensioncommon/list_envoy_extender.go +++ b/envoyextensions/extensioncommon/list_envoy_extender.go @@ -5,8 +5,6 @@ package extensioncommon import ( "fmt" - "strings" - envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" @@ -54,6 +52,12 @@ func (*ListEnvoyExtender) Validate(config *RuntimeConfig) error { func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) { var resultErr error + // We don't support patching the local proxy with an upstream's config except in special + // cases supported by UpstreamEnvoyExtender. + if config.IsSourcedFromUpstream { + return nil, fmt.Errorf("%q extension applied as local config but is sourced from an upstream of the local service", config.EnvoyExtension.Name) + } + switch config.Kind { case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: default: @@ -67,7 +71,6 @@ func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config clusters := make(ClusterMap) routes := make(RouteMap) listeners := make(ListenerMap) - isUpstream := config.IsUpstream() for _, indexType := range []string{ xdscommon.ListenerType, @@ -77,36 +80,12 @@ func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config for nameOrSNI, msg := range resources.Index[indexType] { switch resource := msg.(type) { case *envoy_cluster_v3.Cluster: - // If the Envoy extension configuration is for an upstream service, the Cluster's - // name must match the upstream service's SNI. - if isUpstream && !config.MatchesUpstreamServiceSNI(nameOrSNI) { - continue - } - - // If the extension's config is for an an inbound listener, the Cluster's name - // must be xdscommon.LocalAppClusterName. - if !isUpstream && nameOrSNI == xdscommon.LocalAppClusterName { - continue - } - clusters[nameOrSNI] = resource case *envoy_listener_v3.Listener: listeners[nameOrSNI] = resource case *envoy_route_v3.RouteConfiguration: - // If the Envoy extension configuration is for an upstream service, the route's - // name must match the upstream service's Envoy ID. - matchesEnvoyID := config.EnvoyID() == nameOrSNI - if isUpstream && !config.MatchesUpstreamServiceSNI(nameOrSNI) && !matchesEnvoyID { - continue - } - - // There aren't routes for inbound services. - if !isUpstream { - continue - } - routes[nameOrSNI] = resource default: @@ -156,11 +135,6 @@ func (e ListEnvoyExtender) patchListeners(config *RuntimeConfig, m ListenerMap) } func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { - // We don't support directly targeting terminating gateways with extensions. - if !config.IsUpstream() { - return l, nil - } - var resultErr error for _, listener := range l { for _, filterChain := range listener.FilterChains { @@ -170,11 +144,6 @@ func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfi continue } - // The filter chain's SNI must match the upstream service's SNI. - if !config.MatchesUpstreamServiceSNI(sni) { - continue - } - patchedFilters, err := e.Extension.PatchFilters(config, filterChain.Filters) if err == nil { filterChain.Filters = patchedFilters @@ -191,14 +160,8 @@ func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfi func (e ListEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { var resultErr error - isUpstream := config.IsUpstream() for nameOrSNI, listener := range l { - envoyID := "" - if id, _, found := strings.Cut(listener.Name, ":"); found { - envoyID = id - } - - if isUpstream && envoyID == xdscommon.OutboundListenerName { + if IsOutboundTProxyListener(listener) { patchedListener, err := e.patchTProxyListener(config, listener) if err == nil { l[nameOrSNI] = patchedListener @@ -208,18 +171,6 @@ func (e ListEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l L continue } - // If the Envoy extension configuration is for an upstream service, the listener's - // name must match the upstream service's EnvoyID or be the outbound listener. - if isUpstream && envoyID != config.EnvoyID() { - continue - } - - // If the Envoy extension configuration is for inbound resources, the - // listener must be named xdscommon.PublicListenerName. - if config.IsLocal() && envoyID != xdscommon.PublicListenerName { - continue - } - patchedListener, err := e.patchConnectProxyListener(config, listener) if err == nil { l[nameOrSNI] = patchedListener diff --git a/envoyextensions/extensioncommon/resources.go b/envoyextensions/extensioncommon/resources.go index e0f084db06..2624bc3515 100644 --- a/envoyextensions/extensioncommon/resources.go +++ b/envoyextensions/extensioncommon/resources.go @@ -4,12 +4,16 @@ package extensioncommon import ( + envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" envoy_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "github.com/hashicorp/consul/envoyextensions/xdscommon" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" + "strings" ) // MakeUpstreamTLSTransportSocket generates an Envoy transport socket for the given TLS context. @@ -59,3 +63,40 @@ func MakeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, erro ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any}, }, nil } + +// GetListenerEnvoyID returns the Envoy ID string parsed from the name of the given Listener. If none is found, it +// returns the empty string. +func GetListenerEnvoyID(l *envoy_listener_v3.Listener) string { + if id, _, found := strings.Cut(l.Name, ":"); found { + return id + } + return "" +} + +// IsLocalAppCluster returns true if the given Cluster represents the local Cluster, which receives inbound traffic to +// the local proxy. +func IsLocalAppCluster(c *envoy_cluster_v3.Cluster) bool { + return c.Name == xdscommon.LocalAppClusterName +} + +// IsRouteToLocalAppCluster takes a RouteConfiguration and returns true if all routes within it target the local +// Cluster. Note that because we currently target RouteConfiguration in PatchRoute, we have to check multiple individual +// Route resources. +func IsRouteToLocalAppCluster(r *envoy_route_v3.RouteConfiguration) bool { + clusterNames := RouteClusterNames(r) + _, match := clusterNames[xdscommon.LocalAppClusterName] + + return match && len(clusterNames) == 1 +} + +// IsInboundPublicListener returns true if the given Listener represents the inbound public Listener for the local +// service. +func IsInboundPublicListener(l *envoy_listener_v3.Listener) bool { + return GetListenerEnvoyID(l) == xdscommon.PublicListenerName +} + +// IsOutboundTProxyListener returns true if the given Listener represents the outbound TProxy Listener for the local +// service. +func IsOutboundTProxyListener(l *envoy_listener_v3.Listener) bool { + return GetListenerEnvoyID(l) == xdscommon.OutboundListenerName +} diff --git a/envoyextensions/extensioncommon/runtime_config.go b/envoyextensions/extensioncommon/runtime_config.go index b9ab1b38c3..2572d3f381 100644 --- a/envoyextensions/extensioncommon/runtime_config.go +++ b/envoyextensions/extensioncommon/runtime_config.go @@ -30,16 +30,30 @@ type RuntimeConfig struct { // EnvoyExtension is the extension that will patch Envoy resources. EnvoyExtension api.EnvoyExtension - // ServiceName is the name of the service the EnvoyExtension is being applied to. It could be the local service or - // an upstream of the local service. + // ServiceName is the name of the service the EnvoyExtension is being applied to. It is typically the local service + // (IsSourcedFromUpstream = false), but can also be an upstream of the local service (IsSourcedFromUpstream = true). ServiceName api.CompoundServiceName - // Upstreams will only be configured if the EnvoyExtension is being applied to an upstream. - // If there are no Upstreams, then EnvoyExtension is being applied to the local service's resources. + // Upstreams represent the upstreams of the local service. This is consistent regardless of the value of + // IsSourcedFromUpstream, which refers to the Envoy extension source. Upstreams map[api.CompoundServiceName]*UpstreamData - // LocalUpstreams will only be configured if the EnvoyExtension is being applied to the local service. - LocalUpstreams map[api.CompoundServiceName]*UpstreamData + // IsSourcedFromUpstream is set to true only in the exceptional cases where upstream service config contains + // extensions that apply to the configured service's downstreams. In those cases, this value will be true when such + // a downstream is the local service. In all other cases, IsSourcedFromUpstream will be false. + // + // This is used exclusively for specific extensions (currently, only AWS Lambda and Validate) in which we + // intentionally apply the extension to downstreams rather than the local proxy of the configured service itself. + // This is generally dangerous, since it circumvents ACLs for the affected downstream services (the upstream owner + // may not have `service:write` for the downstreams). + // + // Extensions used this way MUST be designed to allow only trusted modifications of downstream proxies that impact + // their ability to call the upstream service. Remote configurations MUST NOT be allowed to otherwise modify local + // proxies until we support explicit extension capability controls or require privileges higher than the typical + // `service:write` required to configure extensions. + // + // See UpstreamEnvoyExtender for the code that applies RuntimeConfig with this flag set. + IsSourcedFromUpstream bool // Kind is mode the local Envoy proxy is running in. For now, only connect proxy and // terminating gateways are supported. @@ -49,33 +63,27 @@ type RuntimeConfig struct { Protocol string } -// IsLocal indicates if the extension configuration is for the proxy's local service. -func (ec RuntimeConfig) IsLocal() bool { - return !ec.IsUpstream() -} - -// IsUpstream indicates if the extension configuration is for an upstream service. -func (ec RuntimeConfig) IsUpstream() bool { - _, ok := ec.Upstreams[ec.ServiceName] - return ok -} - // MatchesUpstreamServiceSNI indicates if the extension configuration is for an upstream service -// that matches the given SNI. -func (ec RuntimeConfig) MatchesUpstreamServiceSNI(sni string) bool { - u := ec.Upstreams[ec.ServiceName] +// that matches the given SNI, if the RuntimeConfig corresponds to an upstream of the local service. +// Only used when IsSourcedFromUpstream is true. +func (c RuntimeConfig) MatchesUpstreamServiceSNI(sni string) bool { + u := c.Upstreams[c.ServiceName] _, match := u.SNI[sni] return match } -// EnvoyID returns the unique Envoy identifier of the upstream service. -func (ec RuntimeConfig) EnvoyID() string { - u := ec.Upstreams[ec.ServiceName] +// UpstreamEnvoyID returns the unique Envoy identifier of the upstream service, if the RuntimeConfig corresponds to an +// upstream of the local service. Note that this could be the local service if it targets itself as an upstream. +// Only used when IsSourcedFromUpstream is true. +func (c RuntimeConfig) UpstreamEnvoyID() string { + u := c.Upstreams[c.ServiceName] return u.EnvoyID } -// OutgoingProxyKind returns the service kind for the outgoing listener of an upstream service. -func (ec RuntimeConfig) OutgoingProxyKind() api.ServiceKind { - u := ec.Upstreams[ec.ServiceName] +// UpstreamOutgoingProxyKind returns the service kind for the outgoing listener of the upstream service, if the +// RuntimeConfig corresponds to an upstream of the local service. +// Only used when IsSourcedFromUpstream is true. +func (c RuntimeConfig) UpstreamOutgoingProxyKind() api.ServiceKind { + u := c.Upstreams[c.ServiceName] return u.OutgoingProxyKind } diff --git a/envoyextensions/extensioncommon/runtime_config_test.go b/envoyextensions/extensioncommon/runtime_config_test.go index e68c08dc38..a6cca3b64b 100644 --- a/envoyextensions/extensioncommon/runtime_config_test.go +++ b/envoyextensions/extensioncommon/runtime_config_test.go @@ -30,13 +30,6 @@ func makeTestRuntimeConfig() RuntimeConfig { return rc } -func TestRuntimeConfig_IsUpstream(t *testing.T) { - rc := makeTestRuntimeConfig() - require.True(t, rc.IsUpstream()) - delete(rc.Upstreams, rc.ServiceName) - require.False(t, rc.IsUpstream()) -} - func TestRuntimeConfig_MatchesUpstreamServiceSNI(t *testing.T) { rc := makeTestRuntimeConfig() require.True(t, rc.MatchesUpstreamServiceSNI("sni1")) @@ -46,10 +39,10 @@ func TestRuntimeConfig_MatchesUpstreamServiceSNI(t *testing.T) { func TestRuntimeConfig_EnvoyID(t *testing.T) { rc := makeTestRuntimeConfig() - require.Equal(t, "eid", rc.EnvoyID()) + require.Equal(t, "eid", rc.UpstreamEnvoyID()) } func TestRuntimeConfig_OutgoingProxyKind(t *testing.T) { rc := makeTestRuntimeConfig() - require.Equal(t, api.ServiceKindTerminatingGateway, rc.OutgoingProxyKind()) + require.Equal(t, api.ServiceKindTerminatingGateway, rc.UpstreamOutgoingProxyKind()) } diff --git a/envoyextensions/extensioncommon/upstream_envoy_extender.go b/envoyextensions/extensioncommon/upstream_envoy_extender.go new file mode 100644 index 0000000000..96a0969d44 --- /dev/null +++ b/envoyextensions/extensioncommon/upstream_envoy_extender.go @@ -0,0 +1,246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package extensioncommon + +import ( + "fmt" + envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/envoyextensions/xdscommon" + "github.com/hashicorp/go-multierror" + "google.golang.org/protobuf/proto" +) + +// UpstreamEnvoyExtender facilitates uncommon scenarios in which an upstream service's extension needs to apply changes +// to downstram proxies. Separating this mode from the more typical case of extensions patching just the local proxy for +// the configured service allows us to more effectively enforce controls over this elevated level of privilege. +// +// THIS EXTENDER SHOULD NOT BE USED BY ANY NEW EXTENSIONS! It is only intended for use by the builtin AWS Lambda +// extension and Validate (read-only) pseudo-extension to support their existing behavior. Future changes to the +// extension API will introduce stronger controls around privileged capabilities, at which point this extender can be +// removed. +// +// See documentation in RuntimeConfig.IsSourcedFromUpstream for more details. +type UpstreamEnvoyExtender struct { + Extension BasicExtension +} + +var _ EnvoyExtender = (*UpstreamEnvoyExtender)(nil) + +func (ext *UpstreamEnvoyExtender) Validate(_ *RuntimeConfig) error { + return nil +} + +func (ext *UpstreamEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) { + var resultErr error + + // Assert that extension configuration is exclusively from upstreams of the local service. + if !config.IsSourcedFromUpstream { + return nil, fmt.Errorf("%q extension applied as upstream config but is not sourced from an upstream of the local service", config.EnvoyExtension.Name) + } + + // Only the AWS Lambda and Validate extensions are allowed to apply to downstream proxies. + switch config.EnvoyExtension.Name { + case api.BuiltinAWSLambdaExtension, api.BuiltinValidateExtension: + default: + return nil, fmt.Errorf("extension %q is not permitted to be applied via upstream service config", config.EnvoyExtension.Name) + } + + // The extensions used by this extender only support terminating gateways and connect proxies. + switch config.Kind { + case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: + default: + return resources, nil + } + + if !ext.Extension.CanApply(config) { + return resources, nil + } + + for _, indexType := range []string{ + xdscommon.ListenerType, + xdscommon.RouteType, + xdscommon.ClusterType, + } { + for nameOrSNI, msg := range resources.Index[indexType] { + switch resource := msg.(type) { + case *envoy_cluster_v3.Cluster: + // If the Envoy extension configuration is for an upstream service, the Cluster's + // name must match the upstream service's SNI. + if !config.MatchesUpstreamServiceSNI(nameOrSNI) { + continue + } + + newCluster, patched, err := ext.Extension.PatchCluster(config, resource) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster: %w", err)) + continue + } + if patched { + resources.Index[xdscommon.ClusterType][nameOrSNI] = newCluster + } + + case *envoy_listener_v3.Listener: + newListener, patched, err := ext.patchListener(config, resource) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener: %w", err)) + continue + } + if patched { + resources.Index[xdscommon.ListenerType][nameOrSNI] = newListener + } + + case *envoy_route_v3.RouteConfiguration: + // If the Envoy extension configuration is for an upstream service, the Route's + // name must match the upstream service's Envoy ID. + matchesEnvoyID := config.UpstreamEnvoyID() == nameOrSNI + if !config.MatchesUpstreamServiceSNI(nameOrSNI) && !matchesEnvoyID { + continue + } + + newRoute, patched, err := ext.Extension.PatchRoute(config, resource) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching route: %w", err)) + continue + } + if patched { + resources.Index[xdscommon.RouteType][nameOrSNI] = newRoute + } + default: + resultErr = multierror.Append(resultErr, fmt.Errorf("unsupported type was skipped: %T", resource)) + } + } + } + + return resources, resultErr +} + +func (ext *UpstreamEnvoyExtender) patchListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { + switch config.Kind { + case api.ServiceKindTerminatingGateway: + return ext.patchTerminatingGatewayListener(config, l) + case api.ServiceKindConnectProxy: + return ext.patchConnectProxyListener(config, l) + } + return l, false, nil +} + +func (ext *UpstreamEnvoyExtender) patchTerminatingGatewayListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { + var resultErr error + patched := false + for _, filterChain := range l.FilterChains { + sni := getSNI(filterChain) + + if sni == "" { + continue + } + + // The filter chain's SNI must match the upstream service's SNI. + if !config.MatchesUpstreamServiceSNI(sni) { + continue + } + + var filters []*envoy_listener_v3.Filter + + for _, filter := range filterChain.Filters { + newFilter, ok, err := ext.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) + + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) + filters = append(filters, filter) + continue + } + if ok { + filters = append(filters, newFilter) + patched = true + } else { + filters = append(filters, filter) + } + } + filterChain.Filters = filters + } + + return l, patched, resultErr +} + +func (ext *UpstreamEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { + var resultErr error + envoyID := GetListenerEnvoyID(l) + + // TProxy outbound listeners must be targeted _carefully_ by upstream extensions + // because they will affect any downstream's local proxy (there's a single outbound + // listener for all upstreams). Resources specific to that upstream such as the + // individual filter that targets the upstream should be targeted. + if IsOutboundTProxyListener(l) { + return ext.patchTProxyListener(config, l) + } + + // If the Envoy extension configuration is for an upstream service, the listener's + // name must match the upstream service's EnvoyID or be the outbound listener. + if envoyID != config.UpstreamEnvoyID() { + return l, false, nil + } + + // Below is where we handle upstream listeners when not in TProxy mode. + var patched bool + for _, filterChain := range l.FilterChains { + var filters []*envoy_listener_v3.Filter + + for _, filter := range filterChain.Filters { + newFilter, ok, err := ext.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) + filters = append(filters, filter) + continue + } + + if ok { + filters = append(filters, newFilter) + patched = true + } else { + filters = append(filters, filter) + } + } + filterChain.Filters = filters + } + + return l, patched, resultErr +} + +func (ext *UpstreamEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { + var resultErr error + patched := false + + vip := config.Upstreams[config.ServiceName].VIP + + for _, filterChain := range l.FilterChains { + var filters []*envoy_listener_v3.Filter + + match := filterChainTProxyMatch(vip, filterChain) + if !match { + continue + } + + for _, filter := range filterChain.Filters { + newFilter, ok, err := ext.Extension.PatchFilter(config, filter, IsInboundPublicListener(l)) + if err != nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) + filters = append(filters, filter) + continue + } + + if ok { + filters = append(filters, newFilter) + patched = true + } else { + filters = append(filters, filter) + } + } + filterChain.Filters = filters + } + + return l, patched, resultErr +} diff --git a/troubleshoot/proxy/validateupstream.go b/troubleshoot/proxy/validateupstream.go index fcc7cb8965..d54731a8d2 100644 --- a/troubleshoot/proxy/validateupstream.go +++ b/troubleshoot/proxy/validateupstream.go @@ -78,7 +78,8 @@ func Validate(indexedResources *xdscommon.IndexedResources, envoyID string, vip "envoyID": envoyID, }, }, - ServiceName: emptyServiceKey, + ServiceName: emptyServiceKey, + IsSourcedFromUpstream: true, Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ emptyServiceKey: { VIP: vip, @@ -92,12 +93,12 @@ func Validate(indexedResources *xdscommon.IndexedResources, envoyID string, vip }, Kind: api.ServiceKindConnectProxy, } - basicExtension, err := validate.MakeValidate(extConfig) + ext, err := validate.MakeValidate(extConfig) if err != nil { return []validate.Message{{Message: err.Error()}} } - extender := extensioncommon.BasicEnvoyExtender{ - Extension: basicExtension, + extender := extensioncommon.UpstreamEnvoyExtender{ + Extension: ext, } err = extender.Validate(&extConfig) if err != nil { diff --git a/troubleshoot/validate/validate.go b/troubleshoot/validate/validate.go index 6ba411378f..47c05cc1fb 100644 --- a/troubleshoot/validate/validate.go +++ b/troubleshoot/validate/validate.go @@ -21,8 +21,6 @@ import ( "github.com/hashicorp/consul/envoyextensions/extensioncommon" ) -const builtinValidateExtension = "builtin/proxy/validate" - // Validate contains input information about which proxy resources to validate and output information about resources it // has validated. type Validate struct { @@ -81,8 +79,8 @@ func MakeValidate(ext extensioncommon.RuntimeConfig) (extensioncommon.BasicExten var resultErr error var plugin Validate - if name := ext.EnvoyExtension.Name; name != builtinValidateExtension { - return nil, fmt.Errorf("expected extension name 'builtin/proxy/validate' but got %q", name) + if name := ext.EnvoyExtension.Name; name != api.BuiltinValidateExtension { + return nil, fmt.Errorf("expected extension name '%s' but got %q", api.BuiltinValidateExtension, name) } envoyID, _ := ext.EnvoyExtension.Arguments["envoyID"] @@ -366,7 +364,7 @@ func (p *Validate) PatchCluster(config *extensioncommon.RuntimeConfig, c *envoy_ return c, false, nil } -func (p *Validate) PatchFilter(config *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { +func (p *Validate) PatchFilter(config *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter, _ bool) (*envoy_listener_v3.Filter, bool, error) { // If a single filter exists for a listener we say it exists. p.listener = true diff --git a/troubleshoot/validate/validate_test.go b/troubleshoot/validate/validate_test.go index e479edfd6a..6747d688e1 100644 --- a/troubleshoot/validate/validate_test.go +++ b/troubleshoot/validate/validate_test.go @@ -335,7 +335,7 @@ func TestMakeValidate(t *testing.T) { for n, tc := range cases { t.Run(n, func(t *testing.T) { - extensionName := builtinValidateExtension + extensionName := api.BuiltinValidateExtension if tc.extensionName != "" { extensionName = tc.extensionName }