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.
This commit is contained in:
Michael Zalimeni 2023-05-23 07:55:06 -04:00 committed by GitHub
parent eccdf81977
commit 4cae008559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1284 additions and 661 deletions

7
.changelog/17415.txt Normal file
View File

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

View File

@ -42,7 +42,7 @@ func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error)
if err := a.fromArguments(ext.Arguments); err != nil { if err := a.fromArguments(ext.Arguments); err != nil {
return nil, err return nil, err
} }
return &extensioncommon.BasicEnvoyExtender{ return &extensioncommon.UpstreamEnvoyExtender{
Extension: &a, Extension: &a,
}, nil }, nil
} }
@ -65,7 +65,7 @@ func (a *awsLambda) validate() error {
// CanApply returns true if the kind of the provided ExtensionConfiguration matches // CanApply returns true if the kind of the provided ExtensionConfiguration matches
// the kind of the lambda configuration // the kind of the lambda configuration
func (a *awsLambda) CanApply(config *extensioncommon.RuntimeConfig) bool { 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 // 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 return route, false, nil
} }
// Only patch outbound routes.
if extensioncommon.IsRouteToLocalAppCluster(route) {
return route, false, nil
}
for _, virtualHost := range route.VirtualHosts { for _, virtualHost := range route.VirtualHosts {
for _, route := range virtualHost.Routes { for _, route := range virtualHost.Routes {
action, ok := route.Action.(*envoy_route_v3.Route_Route) 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 // 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) { 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{ transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
Sni: "*.amazonaws.com", 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 // PatchFilter patches the provided envoy filter with an inserted lambda filter being careful not to
// overwrite the http filters. // 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" { if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil return filter, false, nil
} }

View File

@ -86,7 +86,7 @@ func TestConstructor(t *testing.T) {
if tc.ok { if tc.ok {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, &extensioncommon.BasicEnvoyExtender{Extension: &tc.expected}, e) require.Equal(t, &extensioncommon.UpstreamEnvoyExtender{Extension: &tc.expected}, e)
} else { } else {
require.Error(t, err) require.Error(t, err)
} }
@ -323,10 +323,11 @@ func TestPatchFilter(t *testing.T) {
return v return v
} }
tests := map[string]struct { tests := map[string]struct {
filter *envoy_listener_v3.Filter filter *envoy_listener_v3.Filter
expectFilter *envoy_listener_v3.Filter isInboundFilter bool
expectBool bool expectFilter *envoy_listener_v3.Filter
expectErr string expectBool bool
expectErr string
}{ }{
"invalid filter name is ignored": { "invalid filter name is ignored": {
filter: &envoy_listener_v3.Filter{Name: "something"}, filter: &envoy_listener_v3.Filter{Name: "something"},
@ -416,6 +417,36 @@ func TestPatchFilter(t *testing.T) {
}, },
expectBool: true, 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 { for name, tc := range tests {
@ -425,7 +456,7 @@ func TestPatchFilter(t *testing.T) {
PayloadPassthrough: true, PayloadPassthrough: true,
InvocationMode: "asynchronous", 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) require.Equal(t, tc.expectBool, ok)
if tc.expectErr == "" { if tc.expectErr == "" {
require.NoError(t, err) require.NoError(t, err)

View File

@ -97,9 +97,7 @@ func (r *ratelimit) validate() error {
// CanApply determines if the extension can apply to the given extension configuration. // CanApply determines if the extension can apply to the given extension configuration.
func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool { func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool {
// rate limit is only applied to the service itself since the limit is return string(config.Kind) == p.ProxyType
// aggregated from all downstream connections.
return string(config.Kind) == p.ProxyType && !config.IsUpstream()
} }
// PatchRoute does nothing. // 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 // PatchFilter inserts a http local rate_limit filter at the head of
// envoy.filters.network.http_connection_manager filters // 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" { if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil return filter, false, nil
} }

View File

@ -64,11 +64,11 @@ func (l *lua) validate() error {
// CanApply determines if the extension can apply to the given extension configuration. // CanApply determines if the extension can apply to the given extension configuration.
func (l *lua) CanApply(config *extensioncommon.RuntimeConfig) bool { 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 { func (l *lua) matchesListenerDirection(isInboundListener bool) bool {
return (config.IsUpstream() && l.Listener == "outbound") || (!config.IsUpstream() && l.Listener == "inbound") return (!isInboundListener && l.Listener == "outbound") || (isInboundListener && l.Listener == "inbound")
} }
// PatchRoute does nothing. // 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. // 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" { if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil return filter, false, nil
} }

View File

@ -193,7 +193,7 @@ func (p *pluginConfig) asyncDataSource(rtCfg *extensioncommon.RuntimeConfig) (*e
// fetch the data from the upstream source. // fetch the data from the upstream source.
remote := &p.VmConfig.Code.Remote remote := &p.VmConfig.Code.Remote
clusterSNI := "" clusterSNI := ""
for service, upstream := range rtCfg.LocalUpstreams { for service, upstream := range rtCfg.Upstreams {
if service == remote.HttpURI.Service { if service == remote.HttpURI.Service {
for sni := range upstream.SNI { for sni := range upstream.SNI {
clusterSNI = sni clusterSNI = sni

View File

@ -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. // 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 // Currently the Wasm extension can be applied if the extension configuration is for an inbound
// listener on the a local connect-proxy. // listener (checked below) on a local connect-proxy.
// It does not patch extensions for service upstreams.
func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool { func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool {
return config.IsLocal() && w.wasmConfig.ListenerType == "inbound" && return config.Kind == w.wasmConfig.ProxyType
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. // 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. // PatchFilter adds a Wasm filter to the HTTP filter chain.
// TODO (wasm/tcp): Add support for TCP filters. // 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" { if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil return filter, false, nil
} }

View File

@ -33,21 +33,24 @@ import (
func TestHttpWasmExtension(t *testing.T) { func TestHttpWasmExtension(t *testing.T) {
t.Parallel() t.Parallel()
cases := map[string]struct { cases := map[string]struct {
extName string extName string
canApply bool canApply bool
args func(bool) map[string]any args func(bool) map[string]any
rtCfg func(bool) *extensioncommon.RuntimeConfig rtCfg func(bool) *extensioncommon.RuntimeConfig
inputFilters func() []*envoy_http_v3.HttpFilter isInboundFilter bool
expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter inputFilters func() []*envoy_http_v3.HttpFilter
errStr string expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter
debug bool expPatched bool
errStr string
debug bool
}{ }{
"http remote file": { "http remote file": {
extName: api.BuiltinWasmExtension, extName: api.BuiltinWasmExtension,
canApply: true, canApply: true,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) }, args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
inputFilters: makeTestHttpFilters, isInboundFilter: true,
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter { expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{ return []*envoy_http_v3.HttpFilter{
{Name: "one"}, {Name: "one"},
@ -65,6 +68,7 @@ func TestHttpWasmExtension(t *testing.T) {
{Name: "three"}, {Name: "three"},
} }
}, },
expPatched: true,
}, },
"local file": { "local file": {
extName: api.BuiltinWasmExtension, extName: api.BuiltinWasmExtension,
@ -76,8 +80,9 @@ func TestHttpWasmExtension(t *testing.T) {
cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm" cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm"
return cfg.toMap(t) return cfg.toMap(t)
}, },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) }, rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
inputFilters: makeTestHttpFilters, isInboundFilter: true,
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter { expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{ return []*envoy_http_v3.HttpFilter{
{Name: "one"}, {Name: "one"},
@ -95,6 +100,24 @@ func TestHttpWasmExtension(t *testing.T) {
{Name: "three"}, {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": { "no cluster for remote file": {
extName: api.BuiltinWasmExtension, extName: api.BuiltinWasmExtension,
@ -102,11 +125,13 @@ func TestHttpWasmExtension(t *testing.T) {
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) }, args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { rtCfg: func(ent bool) *extensioncommon.RuntimeConfig {
rt := makeTestRuntimeConfig(ent) rt := makeTestRuntimeConfig(ent)
rt.LocalUpstreams = nil rt.Upstreams = nil
return rt return rt
}, },
inputFilters: makeTestHttpFilters, isInboundFilter: true,
errStr: "no upstream found for remote service", inputFilters: makeTestHttpFilters,
errStr: "no upstream found for remote service",
expPatched: false,
}, },
} }
@ -140,10 +165,10 @@ func TestHttpWasmExtension(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
inputHttpConMgr := makeHttpConMgr(t, c.inputFilters()) 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 == "" { if c.errStr == "" {
require.NoError(t, err) require.NoError(t, err)
require.True(t, patched) require.Equal(t, c.expPatched, patched)
cfg := testWasmConfigFromMap(t, c.args(enterprise)) cfg := testWasmConfigFromMap(t, c.args(enterprise))
expHttpConMgr := makeHttpConMgr(t, c.expFilters(cfg)) expHttpConMgr := makeHttpConMgr(t, c.expFilters(cfg))
@ -156,6 +181,7 @@ func TestHttpWasmExtension(t *testing.T) {
prototest.AssertDeepEqual(t, expHttpConMgr, obsHttpConMgr) prototest.AssertDeepEqual(t, expHttpConMgr, obsHttpConMgr)
} else { } else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errStr) require.Contains(t, err.Error(), c.errStr)
} }
@ -554,7 +580,7 @@ func makeTestRuntimeConfig(enterprise bool) *extensioncommon.RuntimeConfig {
return &extensioncommon.RuntimeConfig{ return &extensioncommon.RuntimeConfig{
Kind: api.ServiceKindConnectProxy, Kind: api.ServiceKindConnectProxy,
ServiceName: api.CompoundServiceName{Name: "test-service"}, ServiceName: api.CompoundServiceName{Name: "test-service"},
LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
{ {
Name: "test-file-server", Name: "test-file-server",
Namespace: acl.NamespaceOrDefault(ns), Namespace: acl.NamespaceOrDefault(ns),

View File

@ -4,9 +4,11 @@
package envoyextensions package envoyextensions
import ( import (
"fmt"
"testing" "testing"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/stretchr/testify/require" "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)
}
})
}
}

View File

@ -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" listener := "inbound"
if !inbound { if !inbound {
listener = "outbound" listener = "outbound"
} }
return &structs.ServiceConfigEntry{ return func(ns *structs.NodeService) {
Kind: structs.ServiceDefaults, ns.Proxy.Config["protocol"] = "http"
Name: "db", ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{
Protocol: "http",
EnvoyExtensions: []structs.EnvoyExtension{
{ {
Name: api.BuiltinLuaExtension, Name: api.BuiltinLuaExtension,
Arguments: map[string]interface{}{ Arguments: map[string]interface{}{
@ -84,7 +83,7 @@ function envoy_on_request(request_handle)
end`, end`,
}, },
}, },
}, }
} }
} }
@ -130,57 +129,47 @@ end`,
create: proxycfg.TestConfigSnapshotTerminatingGatewayWithLambdaServiceAndServiceResolvers, create: proxycfg.TestConfigSnapshotTerminatingGatewayWithLambdaServiceAndServiceResolvers,
}, },
{ {
name: "lua-outbound-applies-to-upstreams", name: "lua-outbound-applies-to-local-upstreams",
create: func(t testinf.T) *proxycfg.ConfigSnapshot { 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 { 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", name: "lua-inbound-applies-to-inbound",
create: func(t testinf.T) *proxycfg.ConfigSnapshot { create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(true), nil)
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)
}, },
}, },
{ {
// 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", name: "lua-outbound-doesnt-apply-to-inbound",
create: func(t testinf.T) *proxycfg.ConfigSnapshot { create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) { return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", false, makeLuaNsFunc(false), nil)
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)
}, },
}, },
{ {

View File

@ -52,20 +52,20 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP(t *testing.T) {
var snap *proxycfg.ConfigSnapshot var snap *proxycfg.ConfigSnapshot
testutil.RunStep(t, "initial setup", func(t *testing.T) { testutil.RunStep(t, "initial setup", func(t *testing.T) {
snap = newTestSnapshot(t, nil, "", &structs.ProxyConfigEntry{ snap = newTestSnapshot(t, nil, "",
Kind: structs.ProxyDefaults, func(ns *structs.NodeService) {
Name: structs.ProxyConfigGlobal, // Add extension for local proxy.
EnvoyExtensions: []structs.EnvoyExtension{ ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{
{ {
Name: api.BuiltinLuaExtension, Name: api.BuiltinLuaExtension,
Arguments: map[string]interface{}{ Arguments: map[string]interface{}{
"ProxyType": "connect-proxy", "ProxyType": "connect-proxy",
"Listener": "inbound", "Listener": "inbound",
"Script": "x = 0", "Script": "x = 0",
},
}, },
}, }
}, })
})
// Send initial cluster discover. We'll assume we are testing a partial // Send initial cluster discover. We'll assume we are testing a partial
// reconnect and include some initial resource versions that will be // 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) assertDeltaChanBlocked(t, envoy.deltaStream.sendCh)
// now reconfigure the snapshot and JUST edit the endpoints to strike one of the two current endpoints for db. // 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") deleteAllButOneEndpoint(snap, UID("db"), "db.default.default.dc1")
mgr.DeliverConfig(t, sid, snap) 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) { testutil.RunStep(t, "restore endpoint subscription", func(t *testing.T) {
// Restore db's deleted endpoints by generating a new snapshot. // Restore db's deleted endpoints by generating a new snapshot.
snap = newTestSnapshot(t, snap, "") snap = newTestSnapshot(t, snap, "", nil)
mgr.DeliverConfig(t, sid, snap) mgr.DeliverConfig(t, sid, snap)
// We never send an EDS reply about this change because Envoy is still not subscribed to db. // 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 var snap *proxycfg.ConfigSnapshot
testutil.RunStep(t, "initial setup", func(t *testing.T) { 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 // Plug in a bad port for the public listener
snap.Port = 1 snap.Port = 1
@ -402,7 +402,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2(t *testing.T) {
assertDeltaChanBlocked(t, envoy.deltaStream.sendCh) assertDeltaChanBlocked(t, envoy.deltaStream.sendCh)
// Deliver a new snapshot (tcp with one http upstream) // 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, Kind: structs.ServiceDefaults,
Name: "db", Name: "db",
Protocol: "http2", Protocol: "http2",
@ -476,7 +476,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2(t *testing.T) {
// -- reconfigure with a no-op discovery chain // -- 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, Kind: structs.ServiceDefaults,
Name: "db", Name: "db",
Protocol: "http2", Protocol: "http2",
@ -565,7 +565,7 @@ func TestServer_DeltaAggregatedResources_v3_SlowEndpointPopulation(t *testing.T)
var snap *proxycfg.ConfigSnapshot var snap *proxycfg.ConfigSnapshot
testutil.RunStep(t, "get into initial state", func(t *testing.T) { testutil.RunStep(t, "get into initial state", func(t *testing.T) {
snap = newTestSnapshot(t, nil, "") snap = newTestSnapshot(t, nil, "", nil)
// Send initial cluster discover. // Send initial cluster discover.
envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{}) 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) { 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. // Trigger the xds.Server select{} to wake up and notice our hack is disabled.
// The actual contents of this change are irrelevant. // The actual contents of this change are irrelevant.
snap = newTestSnapshot(t, snap, "") snap = newTestSnapshot(t, snap, "", nil)
mgr.DeliverConfig(t, sid, snap) mgr.DeliverConfig(t, sid, snap)
assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{
@ -694,7 +694,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_TCP_clusterChangesImpa
var snap *proxycfg.ConfigSnapshot var snap *proxycfg.ConfigSnapshot
testutil.RunStep(t, "get into initial state", func(t *testing.T) { testutil.RunStep(t, "get into initial state", func(t *testing.T) {
snap = newTestSnapshot(t, nil, "") snap = newTestSnapshot(t, nil, "", nil)
// Send initial cluster discover. // Send initial cluster discover.
envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{}) 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) { 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. // 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, Kind: structs.ServiceResolver,
Name: "db", Name: "db",
ConnectTimeout: 1337 * time.Second, ConnectTimeout: 1337 * time.Second,
@ -839,7 +839,7 @@ func TestServer_DeltaAggregatedResources_v3_BasicProtocol_HTTP2_RDS_listenerChan
assertDeltaChanBlocked(t, envoy.deltaStream.sendCh) assertDeltaChanBlocked(t, envoy.deltaStream.sendCh)
// Deliver a new snapshot (tcp with one http upstream with no-op disco chain) // 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, Kind: structs.ServiceDefaults,
Name: "db", Name: "db",
Protocol: "http2", 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. // Update the snapshot in a way that causes a single listener update.
// //
// Downgrade from http2 to http // Downgrade from http2 to http
snap = newTestSnapshot(t, snap, "http", &structs.ServiceConfigEntry{ snap = newTestSnapshot(t, snap, "http", nil, &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults, Kind: structs.ServiceDefaults,
Name: "db", Name: "db",
Protocol: "http", Protocol: "http",
@ -1110,7 +1110,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLEnforcement(t *testing.T) {
// Deliver a new snapshot // Deliver a new snapshot
snap := tt.cfgSnap snap := tt.cfgSnap
if snap == nil { if snap == nil {
snap = newTestSnapshot(t, nil, "") snap = newTestSnapshot(t, nil, "", nil)
} }
mgr.DeliverConfig(t, sid, snap) mgr.DeliverConfig(t, sid, snap)
@ -1236,7 +1236,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLTokenDeleted_StreamTerminatedDuri
} }
// Deliver a new snapshot // Deliver a new snapshot
snap := newTestSnapshot(t, nil, "") snap := newTestSnapshot(t, nil, "", nil)
mgr.DeliverConfig(t, sid, snap) mgr.DeliverConfig(t, sid, snap)
assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{
@ -1334,7 +1334,7 @@ func TestServer_DeltaAggregatedResources_v3_ACLTokenDeleted_StreamTerminatedInBa
} }
// Deliver a new snapshot // Deliver a new snapshot
snap := newTestSnapshot(t, nil, "") snap := newTestSnapshot(t, nil, "", nil)
mgr.DeliverConfig(t, sid, snap) mgr.DeliverConfig(t, sid, snap)
assertDeltaResponseSent(t, envoy.deltaStream.sendCh, &envoy_discovery_v3.DeltaDiscoveryResponse{ 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.RegisterProxy(t, sid)
mgr.DrainStreams(sid) mgr.DrainStreams(sid)
snap := newTestSnapshot(t, nil, "") snap := newTestSnapshot(t, nil, "", nil)
envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{ envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{
InitialResourceVersions: mustMakeVersionMap(t, InitialResourceVersions: mustMakeVersionMap(t,
@ -1477,7 +1477,7 @@ func TestServer_DeltaAggregatedResources_v3_StreamDrained(t *testing.T) {
mgr.RegisterProxy(t, sid) mgr.RegisterProxy(t, sid)
testutil.RunStep(t, "successful request/response", func(t *testing.T) { 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{ envoy.SendDeltaReq(t, xdscommon.ClusterType, &envoy_discovery_v3.DeltaDiscoveryRequest{
InitialResourceVersions: mustMakeVersionMap(t, InitialResourceVersions: mustMakeVersionMap(t,

View File

@ -86,13 +86,12 @@ func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.Compound
cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions) cfgSnapExts := convertEnvoyExtensions(cfgSnap.Proxy.EnvoyExtensions)
for _, ext := range cfgSnapExts { for _, ext := range cfgSnapExts {
extCfg := extensioncommon.RuntimeConfig{ extCfg := extensioncommon.RuntimeConfig{
EnvoyExtension: ext, EnvoyExtension: ext,
ServiceName: localSvc, ServiceName: localSvc,
// Upstreams is nil to signify this extension is not being applied to an upstream service, but rather to the local service. IsSourcedFromUpstream: false,
Upstreams: nil, Upstreams: upstreamMap,
LocalUpstreams: upstreamMap, Kind: kind,
Kind: kind, Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config),
Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config),
} }
extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg) 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 { for svc, exts := range extensionsMap {
extensionConfigurationsMap[svc] = []extensioncommon.RuntimeConfig{} extensionConfigurationsMap[svc] = []extensioncommon.RuntimeConfig{}
for _, ext := range exts { for _, ext := range exts {
extCfg := extensioncommon.RuntimeConfig{ if appliesToRemoteDownstreams(ext) {
EnvoyExtension: ext, extCfg := extensioncommon.RuntimeConfig{
Kind: kind, EnvoyExtension: ext,
ServiceName: svc, Kind: kind,
Upstreams: upstreamMap, ServiceName: svc,
Protocol: proxyConfigProtocol(cfgSnap.Proxy.Config), 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 "" 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
}

View File

@ -54,7 +54,8 @@ func TestGetRuntimeConfigurations_TerminatingGateway(t *testing.T) {
"PayloadPassthrough": true, "PayloadPassthrough": true,
}, },
}, },
ServiceName: webService, ServiceName: webService,
IsSourcedFromUpstream: true,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
apiService: { apiService: {
SNI: map[string]struct{}{ SNI: map[string]struct{}{
@ -107,7 +108,8 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
Namespace: "default", 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{ envoyExtensions := []structs.EnvoyExtension{
{ {
Name: api.BuiltinAWSLambdaExtension, Name: api.BuiltinAWSLambdaExtension,
@ -158,27 +160,8 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
"PayloadPassthrough": true, "PayloadPassthrough": true,
}, },
}, },
ServiceName: dbService, ServiceName: dbService,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ IsSourcedFromUpstream: true,
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,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
dbService: { dbService: {
SNI: map[string]struct{}{ SNI: map[string]struct{}{
@ -206,27 +189,8 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
"PayloadPassthrough": true, "PayloadPassthrough": true,
}, },
}, },
ServiceName: dbService, ServiceName: dbService,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ IsSourcedFromUpstream: true,
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,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
dbService: { dbService: {
SNI: map[string]struct{}{ SNI: map[string]struct{}{
@ -255,10 +219,10 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
"PayloadPassthrough": true, "PayloadPassthrough": true,
}, },
}, },
ServiceName: webService, ServiceName: webService,
Kind: api.ServiceKindConnectProxy, Kind: api.ServiceKindConnectProxy,
Upstreams: nil, IsSourcedFromUpstream: false,
LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
dbService: { dbService: {
SNI: map[string]struct{}{ SNI: map[string]struct{}{
"db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {},
@ -276,10 +240,10 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
"arg2": "val2", "arg2": "val2",
}, },
}, },
ServiceName: webService, ServiceName: webService,
Kind: api.ServiceKindConnectProxy, Kind: api.ServiceKindConnectProxy,
Upstreams: nil, IsSourcedFromUpstream: false,
LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
dbService: { dbService: {
SNI: map[string]struct{}{ SNI: map[string]struct{}{
"db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {}, "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {},

View File

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

View File

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

View File

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

View File

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

View File

@ -48,9 +48,10 @@ func newTestSnapshot(
t *testing.T, t *testing.T,
prevSnap *proxycfg.ConfigSnapshot, prevSnap *proxycfg.ConfigSnapshot,
dbServiceProtocol string, dbServiceProtocol string,
nsFn func(ns *structs.NodeService),
additionalEntries ...structs.ConfigEntry, additionalEntries ...structs.ConfigEntry,
) *proxycfg.ConfigSnapshot { ) *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{ snap.ConnectProxy.PreparedQueryEndpoints = map[proxycfg.UpstreamID]structs.CheckServiceNodes{
UID("prepared_query:geo-cache"): proxycfg.TestPreparedQueryNodes(t, "geo-cache"), UID("prepared_query:geo-cache"): proxycfg.TestPreparedQueryNodes(t, "geo-cache"),
} }

View File

@ -43,6 +43,10 @@ const (
BuiltinLuaExtension string = "builtin/lua" BuiltinLuaExtension string = "builtin/lua"
BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit" BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit"
BuiltinWasmExtension string = "builtin/wasm" 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 { type ConfigEntry interface {

View File

@ -5,8 +5,6 @@ package extensioncommon
import ( import (
"fmt" "fmt"
"strings"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 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_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_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 // PatchFilter patches an Envoy filter to include the custom Envoy
// configuration required to integrate with the built in extension template. // 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) var _ EnvoyExtender = (*BasicEnvoyExtender)(nil)
@ -48,20 +46,26 @@ type BasicEnvoyExtender struct {
Extension BasicExtension Extension BasicExtension
} }
func (envoyExtension *BasicEnvoyExtender) Validate(config *RuntimeConfig) error { func (b *BasicEnvoyExtender) Validate(_ *RuntimeConfig) error {
return nil 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 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 { switch config.Kind {
case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy:
default: default:
return resources, nil return resources, nil
} }
if !envoyExtender.Extension.CanApply(config) { if !b.Extension.CanApply(config) {
return resources, nil return resources, nil
} }
@ -73,19 +77,7 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso
for nameOrSNI, msg := range resources.Index[indexType] { for nameOrSNI, msg := range resources.Index[indexType] {
switch resource := msg.(type) { switch resource := msg.(type) {
case *envoy_cluster_v3.Cluster: case *envoy_cluster_v3.Cluster:
// If the Envoy extension configuration is for an upstream service, the Cluster's newCluster, patched, err := b.Extension.PatchCluster(config, resource)
// 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)
if err != nil { if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster: %w", err)) resultErr = multierror.Append(resultErr, fmt.Errorf("error patching cluster: %w", err))
continue continue
@ -95,7 +87,7 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso
} }
case *envoy_listener_v3.Listener: case *envoy_listener_v3.Listener:
newListener, patched, err := envoyExtender.patchListener(config, resource) newListener, patched, err := b.patchListener(config, resource)
if err != nil { if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener: %w", err)) resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener: %w", err))
continue continue
@ -105,19 +97,7 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso
} }
case *envoy_route_v3.RouteConfiguration: case *envoy_route_v3.RouteConfiguration:
// If the Envoy extension configuration is for an upstream service, the route's newRoute, patched, err := b.Extension.PatchRoute(config, resource)
// 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)
if err != nil { if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching route: %w", err)) resultErr = multierror.Append(resultErr, fmt.Errorf("error patching route: %w", err))
continue continue
@ -134,40 +114,25 @@ func (envoyExtender *BasicEnvoyExtender) Extend(resources *xdscommon.IndexedReso
return resources, resultErr 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 { switch config.Kind {
case api.ServiceKindTerminatingGateway: case api.ServiceKindTerminatingGateway:
return envoyExtension.patchTerminatingGatewayListener(config, l) return b.patchTerminatingGatewayListener(config, l)
case api.ServiceKindConnectProxy: case api.ServiceKindConnectProxy:
return envoyExtension.patchConnectProxyListener(config, l) return b.patchConnectProxyListener(config, l)
} }
return l, false, nil return l, false, nil
} }
func (b BasicEnvoyExtender) patchTerminatingGatewayListener(config *RuntimeConfig, l *envoy_listener_v3.Listener) (proto.Message, bool, error) { 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
}
var resultErr error var resultErr error
patched := false patched := false
for _, filterChain := range l.FilterChains { 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 var filters []*envoy_listener_v3.Filter
for _, filter := range filterChain.Filters { 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 { if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) 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 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 var resultErr error
patched := false
envoyID := "" if IsOutboundTProxyListener(l) {
if i := strings.IndexByte(l.Name, ':'); i != -1 {
envoyID = l.Name[:i]
}
if config.IsUpstream() && envoyID == xdscommon.OutboundListenerName {
return b.patchTProxyListener(config, 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 { for _, filterChain := range l.FilterChains {
var filters []*envoy_listener_v3.Filter var filters []*envoy_listener_v3.Filter
for _, filter := range filterChain.Filters { 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 { if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err))
filters = append(filters, filter) filters = append(filters, filter)
@ -237,7 +184,7 @@ func (b BasicEnvoyExtender) patchConnectProxyListener(config *RuntimeConfig, l *
return l, patched, resultErr 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 var resultErr error
patched := false patched := false
@ -252,7 +199,7 @@ func (b BasicEnvoyExtender) patchTProxyListener(config *RuntimeConfig, l *envoy_
} }
for _, filter := range filterChain.Filters { 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 { if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err)) resultErr = multierror.Append(resultErr, fmt.Errorf("error patching listener filter: %w", err))
filters = append(filters, filter) filters = append(filters, filter)

View File

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

View File

@ -5,8 +5,6 @@ package extensioncommon
import ( import (
"fmt" "fmt"
"strings"
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" 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_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_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) { func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config *RuntimeConfig) (*xdscommon.IndexedResources, error) {
var resultErr 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 { switch config.Kind {
case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy: case api.ServiceKindTerminatingGateway, api.ServiceKindConnectProxy:
default: default:
@ -67,7 +71,6 @@ func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config
clusters := make(ClusterMap) clusters := make(ClusterMap)
routes := make(RouteMap) routes := make(RouteMap)
listeners := make(ListenerMap) listeners := make(ListenerMap)
isUpstream := config.IsUpstream()
for _, indexType := range []string{ for _, indexType := range []string{
xdscommon.ListenerType, xdscommon.ListenerType,
@ -77,36 +80,12 @@ func (e *ListEnvoyExtender) Extend(resources *xdscommon.IndexedResources, config
for nameOrSNI, msg := range resources.Index[indexType] { for nameOrSNI, msg := range resources.Index[indexType] {
switch resource := msg.(type) { switch resource := msg.(type) {
case *envoy_cluster_v3.Cluster: 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 clusters[nameOrSNI] = resource
case *envoy_listener_v3.Listener: case *envoy_listener_v3.Listener:
listeners[nameOrSNI] = resource listeners[nameOrSNI] = resource
case *envoy_route_v3.RouteConfiguration: 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 routes[nameOrSNI] = resource
default: default:
@ -156,11 +135,6 @@ func (e ListEnvoyExtender) patchListeners(config *RuntimeConfig, m ListenerMap)
} }
func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { 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 var resultErr error
for _, listener := range l { for _, listener := range l {
for _, filterChain := range listener.FilterChains { for _, filterChain := range listener.FilterChains {
@ -170,11 +144,6 @@ func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfi
continue 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) patchedFilters, err := e.Extension.PatchFilters(config, filterChain.Filters)
if err == nil { if err == nil {
filterChain.Filters = patchedFilters filterChain.Filters = patchedFilters
@ -191,14 +160,8 @@ func (e ListEnvoyExtender) patchTerminatingGatewayListeners(config *RuntimeConfi
func (e ListEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) { func (e ListEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l ListenerMap) (ListenerMap, error) {
var resultErr error var resultErr error
isUpstream := config.IsUpstream()
for nameOrSNI, listener := range l { for nameOrSNI, listener := range l {
envoyID := "" if IsOutboundTProxyListener(listener) {
if id, _, found := strings.Cut(listener.Name, ":"); found {
envoyID = id
}
if isUpstream && envoyID == xdscommon.OutboundListenerName {
patchedListener, err := e.patchTProxyListener(config, listener) patchedListener, err := e.patchTProxyListener(config, listener)
if err == nil { if err == nil {
l[nameOrSNI] = patchedListener l[nameOrSNI] = patchedListener
@ -208,18 +171,6 @@ func (e ListEnvoyExtender) patchConnectProxyListeners(config *RuntimeConfig, l L
continue 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) patchedListener, err := e.patchConnectProxyListener(config, listener)
if err == nil { if err == nil {
l[nameOrSNI] = patchedListener l[nameOrSNI] = patchedListener

View File

@ -4,12 +4,16 @@
package extensioncommon package extensioncommon
import ( 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_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_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_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" 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/proto"
"google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/anypb"
"strings"
) )
// MakeUpstreamTLSTransportSocket generates an Envoy transport socket for the given TLS context. // 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}, ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any},
}, nil }, 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
}

View File

@ -30,16 +30,30 @@ type RuntimeConfig struct {
// EnvoyExtension is the extension that will patch Envoy resources. // EnvoyExtension is the extension that will patch Envoy resources.
EnvoyExtension api.EnvoyExtension EnvoyExtension api.EnvoyExtension
// ServiceName is the name of the service the EnvoyExtension is being applied to. It could be the local service or // ServiceName is the name of the service the EnvoyExtension is being applied to. It is typically the local service
// an upstream of the local service. // (IsSourcedFromUpstream = false), but can also be an upstream of the local service (IsSourcedFromUpstream = true).
ServiceName api.CompoundServiceName ServiceName api.CompoundServiceName
// Upstreams will only be configured if the EnvoyExtension is being applied to an upstream. // Upstreams represent the upstreams of the local service. This is consistent regardless of the value of
// If there are no Upstreams, then EnvoyExtension is being applied to the local service's resources. // IsSourcedFromUpstream, which refers to the Envoy extension source.
Upstreams map[api.CompoundServiceName]*UpstreamData Upstreams map[api.CompoundServiceName]*UpstreamData
// LocalUpstreams will only be configured if the EnvoyExtension is being applied to the local service. // IsSourcedFromUpstream is set to true only in the exceptional cases where upstream service config contains
LocalUpstreams map[api.CompoundServiceName]*UpstreamData // 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 // Kind is mode the local Envoy proxy is running in. For now, only connect proxy and
// terminating gateways are supported. // terminating gateways are supported.
@ -49,33 +63,27 @@ type RuntimeConfig struct {
Protocol string 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 // MatchesUpstreamServiceSNI indicates if the extension configuration is for an upstream service
// that matches the given SNI. // that matches the given SNI, if the RuntimeConfig corresponds to an upstream of the local service.
func (ec RuntimeConfig) MatchesUpstreamServiceSNI(sni string) bool { // Only used when IsSourcedFromUpstream is true.
u := ec.Upstreams[ec.ServiceName] func (c RuntimeConfig) MatchesUpstreamServiceSNI(sni string) bool {
u := c.Upstreams[c.ServiceName]
_, match := u.SNI[sni] _, match := u.SNI[sni]
return match return match
} }
// EnvoyID returns the unique Envoy identifier of the upstream service. // UpstreamEnvoyID returns the unique Envoy identifier of the upstream service, if the RuntimeConfig corresponds to an
func (ec RuntimeConfig) EnvoyID() string { // upstream of the local service. Note that this could be the local service if it targets itself as an upstream.
u := ec.Upstreams[ec.ServiceName] // Only used when IsSourcedFromUpstream is true.
func (c RuntimeConfig) UpstreamEnvoyID() string {
u := c.Upstreams[c.ServiceName]
return u.EnvoyID return u.EnvoyID
} }
// OutgoingProxyKind returns the service kind for the outgoing listener of an upstream service. // UpstreamOutgoingProxyKind returns the service kind for the outgoing listener of the upstream service, if the
func (ec RuntimeConfig) OutgoingProxyKind() api.ServiceKind { // RuntimeConfig corresponds to an upstream of the local service.
u := ec.Upstreams[ec.ServiceName] // Only used when IsSourcedFromUpstream is true.
func (c RuntimeConfig) UpstreamOutgoingProxyKind() api.ServiceKind {
u := c.Upstreams[c.ServiceName]
return u.OutgoingProxyKind return u.OutgoingProxyKind
} }

View File

@ -30,13 +30,6 @@ func makeTestRuntimeConfig() RuntimeConfig {
return rc 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) { func TestRuntimeConfig_MatchesUpstreamServiceSNI(t *testing.T) {
rc := makeTestRuntimeConfig() rc := makeTestRuntimeConfig()
require.True(t, rc.MatchesUpstreamServiceSNI("sni1")) require.True(t, rc.MatchesUpstreamServiceSNI("sni1"))
@ -46,10 +39,10 @@ func TestRuntimeConfig_MatchesUpstreamServiceSNI(t *testing.T) {
func TestRuntimeConfig_EnvoyID(t *testing.T) { func TestRuntimeConfig_EnvoyID(t *testing.T) {
rc := makeTestRuntimeConfig() rc := makeTestRuntimeConfig()
require.Equal(t, "eid", rc.EnvoyID()) require.Equal(t, "eid", rc.UpstreamEnvoyID())
} }
func TestRuntimeConfig_OutgoingProxyKind(t *testing.T) { func TestRuntimeConfig_OutgoingProxyKind(t *testing.T) {
rc := makeTestRuntimeConfig() rc := makeTestRuntimeConfig()
require.Equal(t, api.ServiceKindTerminatingGateway, rc.OutgoingProxyKind()) require.Equal(t, api.ServiceKindTerminatingGateway, rc.UpstreamOutgoingProxyKind())
} }

View File

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

View File

@ -78,7 +78,8 @@ func Validate(indexedResources *xdscommon.IndexedResources, envoyID string, vip
"envoyID": envoyID, "envoyID": envoyID,
}, },
}, },
ServiceName: emptyServiceKey, ServiceName: emptyServiceKey,
IsSourcedFromUpstream: true,
Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{ Upstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
emptyServiceKey: { emptyServiceKey: {
VIP: vip, VIP: vip,
@ -92,12 +93,12 @@ func Validate(indexedResources *xdscommon.IndexedResources, envoyID string, vip
}, },
Kind: api.ServiceKindConnectProxy, Kind: api.ServiceKindConnectProxy,
} }
basicExtension, err := validate.MakeValidate(extConfig) ext, err := validate.MakeValidate(extConfig)
if err != nil { if err != nil {
return []validate.Message{{Message: err.Error()}} return []validate.Message{{Message: err.Error()}}
} }
extender := extensioncommon.BasicEnvoyExtender{ extender := extensioncommon.UpstreamEnvoyExtender{
Extension: basicExtension, Extension: ext,
} }
err = extender.Validate(&extConfig) err = extender.Validate(&extConfig)
if err != nil { if err != nil {

View File

@ -21,8 +21,6 @@ import (
"github.com/hashicorp/consul/envoyextensions/extensioncommon" "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 // Validate contains input information about which proxy resources to validate and output information about resources it
// has validated. // has validated.
type Validate struct { type Validate struct {
@ -81,8 +79,8 @@ func MakeValidate(ext extensioncommon.RuntimeConfig) (extensioncommon.BasicExten
var resultErr error var resultErr error
var plugin Validate var plugin Validate
if name := ext.EnvoyExtension.Name; name != builtinValidateExtension { if name := ext.EnvoyExtension.Name; name != api.BuiltinValidateExtension {
return nil, fmt.Errorf("expected extension name 'builtin/proxy/validate' but got %q", name) return nil, fmt.Errorf("expected extension name '%s' but got %q", api.BuiltinValidateExtension, name)
} }
envoyID, _ := ext.EnvoyExtension.Arguments["envoyID"] envoyID, _ := ext.EnvoyExtension.Arguments["envoyID"]
@ -366,7 +364,7 @@ func (p *Validate) PatchCluster(config *extensioncommon.RuntimeConfig, c *envoy_
return c, false, nil 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. // If a single filter exists for a listener we say it exists.
p.listener = true p.listener = true

View File

@ -335,7 +335,7 @@ func TestMakeValidate(t *testing.T) {
for n, tc := range cases { for n, tc := range cases {
t.Run(n, func(t *testing.T) { t.Run(n, func(t *testing.T) {
extensionName := builtinValidateExtension extensionName := api.BuiltinValidateExtension
if tc.extensionName != "" { if tc.extensionName != "" {
extensionName = tc.extensionName extensionName = tc.extensionName
} }