Wasm Envoy HTTP extension (#16877)

This commit is contained in:
Chris Thain 2023-04-06 14:12:07 -07:00 committed by GitHub
parent 2b0a5b52c2
commit f9126b6c3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2440 additions and 164 deletions

3
.changelog/16877.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
xds: Add a built-in Envoy extension that inserts Wasm HTTP filters.
```

View File

@ -95,7 +95,7 @@ func (a *awsLambda) PatchRoute(r *extensioncommon.RuntimeConfig, route *envoy_ro
// PatchCluster patches the provided envoy cluster with data required to support an AWS lambda function
func (a *awsLambda) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) {
transportSocket, err := makeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
Sni: "*.amazonaws.com",
})
@ -169,7 +169,7 @@ func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_
return filter, false, errors.New("error unmarshalling filter")
}
lambdaHttpFilter, err := makeEnvoyHTTPFilter(
lambdaHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
"envoy.filters.http.aws_lambda",
&envoy_lambda_v3.Config{
Arn: a.ARN,
@ -204,7 +204,7 @@ func (a *awsLambda) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_
config.StripPortMode = &envoy_http_v3.HttpConnectionManager_StripAnyHostPort{
StripAnyHostPort: true,
}
newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config)
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config)
if err != nil {
return filter, false, errors.New("error making new filter")
}

View File

@ -152,7 +152,7 @@ func TestPatchCluster(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
transportSocket, err := makeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
transportSocket, err := extensioncommon.MakeUpstreamTLSTransportSocket(&envoy_tls_v3.UpstreamTlsContext{
Sni: "*.amazonaws.com",
})
require.NoError(t, err)

View File

@ -1,61 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package localratelimit
import (
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_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"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// This is copied from xds and not put into the shared package because I'm not
// convinced it should be shared.
func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) {
if tlsContext == nil {
return nil, nil
}
return makeTransportSocket("tls", tlsContext)
}
func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) {
any, err := anypb.New(config)
if err != nil {
return nil, err
}
return &envoy_core_v3.TransportSocket{
Name: name,
ConfigType: &envoy_core_v3.TransportSocket_TypedConfig{
TypedConfig: any,
},
}, nil
}
func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) {
any, err := anypb.New(cfg)
if err != nil {
return nil, err
}
return &envoy_http_v3.HttpFilter{
Name: name,
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: any},
}, nil
}
func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) {
any, err := anypb.New(cfg)
if err != nil {
return nil, err
}
return &envoy_listener_v3.Filter{
Name: name,
ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any},
}, nil
}

View File

@ -162,7 +162,7 @@ func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_l
}
}
ratelimitHttpFilter, err := makeEnvoyHTTPFilter(
ratelimitHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
"envoy.filters.http.local_ratelimit",
&envoy_ratelimit.LocalRateLimit{
TokenBucket: &tokenBucket,
@ -184,7 +184,7 @@ func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_l
changedFilters = append(changedFilters, config.HttpFilters...)
config.HttpFilters = changedFilters
newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config)
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config)
if err != nil {
return filter, false, errors.New("error making new filter")
}

View File

@ -1,61 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lua
import (
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_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"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// This is copied from xds and not put into the shared package because I'm not
// convinced it should be shared.
func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) {
if tlsContext == nil {
return nil, nil
}
return makeTransportSocket("tls", tlsContext)
}
func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) {
any, err := anypb.New(config)
if err != nil {
return nil, err
}
return &envoy_core_v3.TransportSocket{
Name: name,
ConfigType: &envoy_core_v3.TransportSocket_TypedConfig{
TypedConfig: any,
},
}, nil
}
func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) {
any, err := anypb.New(cfg)
if err != nil {
return nil, err
}
return &envoy_http_v3.HttpFilter{
Name: name,
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{TypedConfig: any},
}, nil
}
func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) {
any, err := anypb.New(cfg)
if err != nil {
return nil, err
}
return &envoy_listener_v3.Filter{
Name: name,
ConfigType: &envoy_listener_v3.Filter_TypedConfig{TypedConfig: any},
}, nil
}

View File

@ -94,7 +94,7 @@ func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listen
if config == nil {
return filter, false, errors.New("error unmarshalling filter")
}
luaHttpFilter, err := makeEnvoyHTTPFilter(
luaHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
"envoy.filters.http.lua",
&envoy_lua_v3.Lua{
InlineCode: l.Script,
@ -124,7 +124,7 @@ func (l *lua) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listen
config.HttpFilters = changedFilters
}
newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config)
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", config)
if err != nil {
return filter, false, errors.New("error making new filter")
}

View File

@ -0,0 +1,435 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package wasm
import (
"fmt"
"net/url"
"time"
envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/mapstructure"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// wasmConfig defines the configuration for a Wasm Envoy extension.
type wasmConfig struct {
// Protocol is the type of Wasm filter to apply, "tcp" or "http".
Protocol string
// ProxyType identifies the type of Envoy proxy that this extension applies to.
// The extension will only be configured for proxies that match this type and
// will be ignored for all other proxy types.
ProxyType api.ServiceKind
// ListenerType identifies the listener type the filter will be applied to.
ListenerType string
// PluginConfig holds the configuration for the Wasm plugin.
PluginConfig pluginConfig
}
// pluginConfig defines a Wasm plugin configuration.
type pluginConfig struct {
// Name is the unique name for the filter in a VM. For use in identifying the
// filter if multiple filters are handled by the same VmID and RootID.
// Also used for logging/debugging.
Name string
// RootID is a unique ID for a set of filters in a VM which will share a
// RootContext and Contexts if applicable (e.g. a Wasm HttpFilter and a Wasm AccessLog).
// All filters with the same RootID and VmID will share Context(s).
RootID string
// VmConfig is the configuration for starting or finding the Wasm VM that the
// filter will run in.
VmConfig vmConfig
// Configuration holds the configuration that will be encoded as bytes and passed to
// the plugin on startup (proxy_on_configure).
Configuration string
// CapabilityRestrictionConfiguration controls the Wasm ABI capabilities available
// to the filter.
CapabilityRestrictionConfiguration capabilityRestrictionConfiguration
// failOpen controls the behavior when a runtime error occurs during filter
// processing.
//
// If set to false runtime errors will result in a failed request.
// For TCP filters, the connection will be closed. For HTTP filters a 503
// status is returned.
//
// If set to true, a runtime error will result in the filter being bypassed.
failOpen bool
}
// vmConfig defines a Wasm VM configuration.
type vmConfig struct {
// VmID is an ID which will be used along with a hash of the Wasm code to
// determine which VM will be used for the plugin. All plugins which use
// the same VmID and code will use the same VM. May be left blank.
VmID string
// Runtime is the Wasm runtime type, one of: v8, wasmtime, wamr, or wavm.
Runtime string
// Code references the Wasm code that will run in the filter.
Code dataSource
// Configuration holds the configuration that will be encoded as bytes and
// passed to the plugin during VM startup (proxy_on_vm_start).
Configuration string
// EnvironmentVariables specifies environment variables to be injected to
// this VM which will be available through WASIs environ_get and
// environ_get_sizes system calls.
EnvironmentVariables environmentVariables
}
// dataSource defines a local or remote location where Wasm code will be loaded from.
type dataSource struct {
// Local supports loading files from a local volume.
Local localDataSource
// Remote supports loading files from a remote server.
Remote remoteDataSource
}
// environmentVariables defines the environment variables that will be made available
// to the Wasm filter.
type environmentVariables struct {
// HostEnvKeys holds the keys of Envoys environment variables exposed to this VM.
// If a key exists in Envoys environment variables, then that key-value pair will
// be injected into the Wasm VM. If a key does not exist, it will be ignored.
HostEnvKeys []string
// KeyValues is a list of key-value pairs to be injected to this VM in the form of "KEY=VALUE".
KeyValues map[string]string
}
// localDataSource defines a file from a local file system.
type localDataSource struct {
// Filename is the path to the Wasm file on the local file system.
Filename string
}
// remoteDataSource defines a file from a remote file server.
type remoteDataSource struct {
// HttpURI
HttpURI httpURI
// SHA256 of the remote file. Used to validate the remote file.
SHA256 string
// RetryPolicy determines how retries are handled.
RetryPolicy retryPolicy
}
// httpURI defines a remote file using an HTTP URI.
type httpURI struct {
// Service is the upstream service the Wasm plugin will be fetched from.
Service api.CompoundServiceName
// URI is the location of the Wasm file on the upstream service.
URI string
// Timeout sets the maximum duration that a response can take.
Timeout string
timeout time.Duration
}
// retryPolicy defines how to handle retries when fetching remote files.
type retryPolicy struct {
// RetryBackOff holds parameters that control retry backoff strategy.
RetryBackOff retryBackoff
// NumRetries specifies the allowed number of retries.
NumRetries int
}
// retryBackoff holds parameters that control retry backoff strategy.
type retryBackoff struct {
// BaseInterval is the base interval to be used for the next back off
// computation. It should be greater than zero and less than or equal
// to MaxInterval.
BaseInterval string
// MaxInterval is the maximum interval between retries.
MaxInterval string
baseInterval time.Duration
maxInterval time.Duration
}
// capabilityRestrictionConfiguration controls Wasm capabilities available to modules.
type capabilityRestrictionConfiguration struct {
// AllowedCapabilities specifies the Wasm capabilities which will be allowed.
// Capabilities are mapped by name. The value which each capability maps to is
// currently ignored and should be left empty.
AllowedCapabilities map[string]any
}
// newWasmConfig creates a filterConfig from the given args.
// It starts with the default wasm configuration and merges in the config
// from the given args.
func newWasmConfig(args map[string]any) (*wasmConfig, error) {
cfg := &wasmConfig{}
if err := mapstructure.Decode(args, cfg); err != nil {
return cfg, err
}
cfg.normalize()
return cfg, nil
}
func (p *pluginConfig) asyncDataSource(rtCfg *extensioncommon.RuntimeConfig) (*envoy_core_v3.AsyncDataSource, error) {
// Local data source
if filename := p.VmConfig.Code.Local.Filename; filename != "" {
return &envoy_core_v3.AsyncDataSource{
Specifier: &envoy_core_v3.AsyncDataSource_Local{
Local: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_Filename{
Filename: filename,
},
},
},
}, nil
}
// Remote data source
// For a remote file, ensure there is an upstream cluster for the host specified in the URL.
// Envoy requires an explicit cluster in order to perform the DNS lookup required to actually
// fetch the data from the upstream source.
remote := &p.VmConfig.Code.Remote
clusterSNI := ""
for service, upstream := range rtCfg.LocalUpstreams {
if service == remote.HttpURI.Service {
for sni := range upstream.SNI {
clusterSNI = sni
break
}
}
}
if clusterSNI == "" {
return nil, fmt.Errorf("no upstream found for remote service %q", remote.HttpURI.Service.Name)
}
d := time.Second
if remote.HttpURI.timeout > 0 {
d = remote.HttpURI.timeout
}
timeout := &durationpb.Duration{Seconds: int64(d.Seconds())}
return &envoy_core_v3.AsyncDataSource{
Specifier: &envoy_core_v3.AsyncDataSource_Remote{
Remote: &envoy_core_v3.RemoteDataSource{
Sha256: remote.SHA256,
HttpUri: &envoy_core_v3.HttpUri{
Uri: remote.HttpURI.URI,
HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{
Cluster: clusterSNI,
},
Timeout: timeout,
},
RetryPolicy: p.retryPolicy(),
},
},
}, nil
}
func (p *pluginConfig) capConfig() *envoy_wasm_v3.CapabilityRestrictionConfig {
if len(p.CapabilityRestrictionConfiguration.AllowedCapabilities) == 0 {
return nil
}
allowedCaps := make(map[string]*envoy_wasm_v3.SanitizationConfig)
for key := range p.CapabilityRestrictionConfiguration.AllowedCapabilities {
allowedCaps[key] = &envoy_wasm_v3.SanitizationConfig{}
}
return &envoy_wasm_v3.CapabilityRestrictionConfig{
AllowedCapabilities: allowedCaps,
}
}
func (p *pluginConfig) envoyPluginConfig(rtCfg *extensioncommon.RuntimeConfig) (*envoy_wasm_v3.PluginConfig, error) {
var err error
var pluginCfgData, vmCfgData *anypb.Any
if p.Configuration != "" {
pluginCfgData, err = anypb.New(wrapperspb.String(p.Configuration))
if err != nil {
return nil, fmt.Errorf("failed to encode Wasm plugin configuration: %w", err)
}
}
if p.VmConfig.Configuration != "" {
vmCfgData, err = anypb.New(wrapperspb.String(p.VmConfig.Configuration))
if err != nil {
return nil, fmt.Errorf("failed to encode Wasm VM configuration: %w", err)
}
}
code, err := p.asyncDataSource(rtCfg)
if err != nil {
return nil, fmt.Errorf("failed to encode async data source configuration: %w", err)
}
var envVars *envoy_wasm_v3.EnvironmentVariables
if len(p.VmConfig.EnvironmentVariables.HostEnvKeys) > 0 ||
len(p.VmConfig.EnvironmentVariables.KeyValues) > 0 {
envVars = &envoy_wasm_v3.EnvironmentVariables{
HostEnvKeys: p.VmConfig.EnvironmentVariables.HostEnvKeys,
KeyValues: p.VmConfig.EnvironmentVariables.KeyValues,
}
}
return &envoy_wasm_v3.PluginConfig{
Name: p.Name,
RootId: p.RootID,
Vm: &envoy_wasm_v3.PluginConfig_VmConfig{
VmConfig: &envoy_wasm_v3.VmConfig{
VmId: p.VmConfig.VmID,
Runtime: fmt.Sprintf("envoy.wasm.runtime.%s", p.VmConfig.Runtime),
Code: code,
Configuration: vmCfgData,
EnvironmentVariables: envVars,
},
},
Configuration: pluginCfgData,
CapabilityRestrictionConfig: p.capConfig(),
FailOpen: p.failOpen,
}, nil
}
func (p *pluginConfig) retryPolicy() *envoy_core_v3.RetryPolicy {
remote := &p.VmConfig.Code.Remote
if remote.RetryPolicy.NumRetries <= 0 &&
remote.RetryPolicy.RetryBackOff.BaseInterval == "" &&
remote.RetryPolicy.RetryBackOff.MaxInterval == "" {
return nil
}
retryPolicy := &envoy_core_v3.RetryPolicy{}
if remote.RetryPolicy.NumRetries > 0 {
retryPolicy.NumRetries = wrapperspb.UInt32(uint32(remote.RetryPolicy.NumRetries))
}
var baseInterval, maxInterval *durationpb.Duration
if remote.RetryPolicy.RetryBackOff.baseInterval > 0 {
baseInterval = &durationpb.Duration{Seconds: int64(remote.RetryPolicy.RetryBackOff.baseInterval.Seconds())}
}
if remote.RetryPolicy.RetryBackOff.maxInterval > 0 {
maxInterval = &durationpb.Duration{Seconds: int64(remote.RetryPolicy.RetryBackOff.maxInterval.Seconds())}
}
if baseInterval != nil || maxInterval != nil {
retryPolicy.RetryBackOff = &envoy_core_v3.BackoffStrategy{
BaseInterval: baseInterval,
MaxInterval: maxInterval,
}
}
return retryPolicy
}
func (w *wasmConfig) normalize() {
if w.ProxyType == "" {
w.ProxyType = api.ServiceKindConnectProxy
}
if w.PluginConfig.VmConfig.Runtime == "" {
w.PluginConfig.VmConfig.Runtime = supportedRuntimes[0]
}
httpURI := &w.PluginConfig.VmConfig.Code.Remote.HttpURI
httpURI.Service.Namespace = acl.NamespaceOrDefault(httpURI.Service.Namespace)
httpURI.Service.Partition = acl.PartitionOrDefault(httpURI.Service.Partition)
if httpURI.timeout <= 0 {
httpURI.timeout = time.Second
}
}
// validate ensures the filterConfig is valid or it returns an error.
// This method must be called before using the configuration.
func (w *wasmConfig) validate() error {
var err, resultErr error
if w.Protocol != "tcp" && w.Protocol != "http" {
resultErr = multierror.Append(resultErr, fmt.Errorf(`unsupported Protocol %q, expected "tcp" or "http"`, w.Protocol))
}
if w.ProxyType != api.ServiceKindConnectProxy {
resultErr = multierror.Append(resultErr, fmt.Errorf("unsupported ProxyType %q, only %q is supported", w.ProxyType, api.ServiceKindConnectProxy))
}
if w.ListenerType != "inbound" && w.ListenerType != "outbound" {
resultErr = multierror.Append(resultErr, fmt.Errorf(`unsupported ListenerType %q, expected "inbound" or "outbound"`, w.ListenerType))
}
if err = validateRuntime(w.PluginConfig.VmConfig.Runtime); err != nil {
resultErr = multierror.Append(resultErr, err)
}
httpURI := &w.PluginConfig.VmConfig.Code.Remote.HttpURI
isLocal := w.PluginConfig.VmConfig.Code.Local.Filename != ""
isRemote := httpURI.Service.Name != "" || httpURI.URI != ""
if isLocal == isRemote {
resultErr = multierror.Append(resultErr, fmt.Errorf("VmConfig.Code must provide exactly one of Local or Remote data source"))
}
// If the data source is Local then validation is complete.
if isLocal {
return resultErr
}
// Validate the remote data source fields.
// Both Service and URI are required inputs for remote data sources.
// We could catch this above in the isRemote check; however, we do an explicit check
// here for UX to give the user extra feedback in case they only provide one or the other.
if httpURI.Service.Name == "" || httpURI.URI == "" {
resultErr = multierror.Append(resultErr, fmt.Errorf("both Service and URI are required for Remote data sources"))
}
if w.PluginConfig.VmConfig.Code.Remote.SHA256 == "" {
resultErr = multierror.Append(resultErr, fmt.Errorf("SHA256 checksum is required for Remote data sources"))
}
if _, err := url.Parse(httpURI.URI); err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("invalid HttpURI.URI: %w", err))
}
if httpURI.Timeout != "" {
httpURI.timeout, err = time.ParseDuration(httpURI.Timeout)
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse HttpURI.Timeout %q as a duration: %w", httpURI.Timeout, err))
}
}
retryPolicy := &w.PluginConfig.VmConfig.Code.Remote.RetryPolicy
if retryPolicy.NumRetries < 0 {
resultErr = multierror.Append(resultErr, fmt.Errorf("RetryPolicy.NumRetries must be greater than or equal to 0"))
}
if retryPolicy.RetryBackOff.BaseInterval != "" {
retryPolicy.RetryBackOff.baseInterval, err = time.ParseDuration(retryPolicy.RetryBackOff.BaseInterval)
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse RetryBackOff.BaseInterval %q: %w", retryPolicy.RetryBackOff.BaseInterval, err))
}
}
if retryPolicy.RetryBackOff.MaxInterval != "" {
retryPolicy.RetryBackOff.maxInterval, err = time.ParseDuration(retryPolicy.RetryBackOff.MaxInterval)
if err != nil {
resultErr = multierror.Append(resultErr, fmt.Errorf("failed to parse RetryBackOff.MaxInterval %q: %w", retryPolicy.RetryBackOff.MaxInterval, err))
}
}
if retryPolicy.RetryBackOff.BaseInterval != "" && retryPolicy.RetryBackOff.baseInterval <= 0 {
resultErr = multierror.Append(resultErr, fmt.Errorf("RetryBackOff.BaseInterval %q must be greater than zero and less than or equal to RetryBackOff.MaxInterval %q",
retryPolicy.RetryBackOff.BaseInterval,
retryPolicy.RetryBackOff.MaxInterval),
)
}
if retryPolicy.RetryBackOff.MaxInterval != "" &&
retryPolicy.RetryBackOff.maxInterval < retryPolicy.RetryBackOff.baseInterval {
resultErr = multierror.Append(resultErr, fmt.Errorf("RetryBackOff.MaxInterval %q must be greater than or equal to RetryBackOff.BaseInterval %q",
retryPolicy.RetryBackOff.MaxInterval,
retryPolicy.RetryBackOff.BaseInterval),
)
}
return resultErr
}
func validateRuntime(s string) error {
for _, rt := range supportedRuntimes {
if s == rt {
return nil
}
}
return fmt.Errorf("unsupported runtime %q", s)
}

View File

@ -0,0 +1,143 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package wasm
import (
"errors"
"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"
envoy_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
)
// wasm is a built-in Envoy extension that can patch filter chains to insert Wasm plugins.
type wasm struct {
name string
wasmConfig *wasmConfig
}
var supportedRuntimes = []string{"v8", "wamr", "wavm", "wasmtime"}
var _ extensioncommon.BasicExtension = (*wasm)(nil)
func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) {
w, err := construct(ext)
if err != nil {
return nil, err
}
return &extensioncommon.BasicEnvoyExtender{
Extension: &w,
}, nil
}
func construct(ext api.EnvoyExtension) (wasm, error) {
w := wasm{name: ext.Name}
if w.name != api.BuiltinWasmExtension {
return w, fmt.Errorf("expected extension name %q but got %q", api.BuiltinWasmExtension, w.name)
}
if err := w.fromArguments(ext.Arguments); err != nil {
return w, err
}
// Configure the failure behavior for the filter. If the plugin is required,
// then filter runtime errors result in a failed request (fail "closed").
// Otherwise, runtime errors result in the filter being skipped (fail "open").
w.wasmConfig.PluginConfig.failOpen = !ext.Required
return w, nil
}
func (w *wasm) fromArguments(args map[string]any) error {
var err error
w.wasmConfig, err = newWasmConfig(args)
if err != nil {
return fmt.Errorf("error decoding extension arguments: %w", err)
}
return w.wasmConfig.validate()
}
// CanApply indicates if the WASM extension can be applied to the given extension configuration.
// Currently the Wasm extension can be applied if the extension configuration is for an inbound
// listener on the a local connect-proxy.
// It does not patch extensions for service upstreams.
func (w wasm) CanApply(config *extensioncommon.RuntimeConfig) bool {
return config.IsLocal() && w.wasmConfig.ListenerType == "inbound" &&
config.Kind == w.wasmConfig.ProxyType
}
// PatchRoute does nothing for the WASM extension.
func (w wasm) PatchRoute(_ *extensioncommon.RuntimeConfig, r *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) {
return r, false, nil
}
// PatchCluster does nothing for the WASM extension.
func (w wasm) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) {
return c, false, nil
}
// PatchFilter adds a Wasm filter to the HTTP filter chain.
// TODO (wasm/tcp): Add support for TCP filters.
func (w wasm) PatchFilter(cfg *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) {
if filter.Name != "envoy.filters.network.http_connection_manager" {
return filter, false, nil
}
if typedConfig := filter.GetTypedConfig(); typedConfig == nil {
return filter, false, errors.New("failed to get typed config for http filter")
}
httpConnMgr := envoy_resource_v3.GetHTTPConnectionManager(filter)
if httpConnMgr == nil {
return filter, false, errors.New("failed to get HTTP connection manager")
}
wasmPluginConfig, err := w.wasmConfig.PluginConfig.envoyPluginConfig(cfg)
if err != nil {
return filter, false, fmt.Errorf("failed to encode Envoy Wasm configuration: %w", err)
}
extHttpFilter, err := extensioncommon.MakeEnvoyHTTPFilter(
"envoy.filters.http.wasm",
&envoy_http_wasm_v3.Wasm{Config: wasmPluginConfig},
)
if err != nil {
return filter, false, err
}
var (
changedFilters = make([]*envoy_http_v3.HttpFilter, 0, len(httpConnMgr.HttpFilters)+1)
changed bool
)
// We need to be careful about overwriting http filters completely because
// http filters validates intentions with the RBAC filter. This inserts the
// filter before `envoy.filters.http.router` while keeping everything
// else intact.
for _, httpFilter := range httpConnMgr.HttpFilters {
if httpFilter.Name == "envoy.filters.http.router" {
changedFilters = append(changedFilters, extHttpFilter)
changed = true
}
changedFilters = append(changedFilters, httpFilter)
}
if changed {
httpConnMgr.HttpFilters = changedFilters
}
newFilter, err := extensioncommon.MakeFilter("envoy.filters.network.http_connection_manager", httpConnMgr)
if err != nil {
return filter, false, errors.New("error making new filter")
}
return newFilter, true, nil
}

View File

@ -0,0 +1,639 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package wasm
import (
"encoding/json"
"fmt"
"net/url"
"strings"
"testing"
"time"
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_http_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/wasm/v3"
envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
envoy_wasm_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/wasm/v3"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/consul/proto/private/prototest"
)
func TestHttpWasmExtension(t *testing.T) {
t.Parallel()
cases := map[string]struct {
extName string
canApply bool
args func(bool) map[string]any
rtCfg func(bool) *extensioncommon.RuntimeConfig
inputFilters func() []*envoy_http_v3.HttpFilter
expFilters func(tc testWasmConfig) []*envoy_http_v3.HttpFilter
errStr string
debug bool
}{
"http remote file": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{
Name: "envoy.filters.http.wasm",
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{
TypedConfig: makeAny(t,
&envoy_http_wasm_v3.Wasm{
Config: tc.toHttpWasmFilter(t),
}),
},
},
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
},
},
"local file": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.Protocol = "http"
cfg.ListenerType = "inbound"
cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm"
return cfg.toMap(t)
},
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig { return makeTestRuntimeConfig(ent) },
inputFilters: makeTestHttpFilters,
expFilters: func(tc testWasmConfig) []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{
Name: "envoy.filters.http.wasm",
ConfigType: &envoy_http_v3.HttpFilter_TypedConfig{
TypedConfig: makeAny(t,
&envoy_http_wasm_v3.Wasm{
Config: tc.toHttpWasmFilter(t),
}),
},
},
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
},
},
"no cluster for remote file": {
extName: api.BuiltinWasmExtension,
canApply: true,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
rtCfg: func(ent bool) *extensioncommon.RuntimeConfig {
rt := makeTestRuntimeConfig(ent)
rt.LocalUpstreams = nil
return rt
},
inputFilters: makeTestHttpFilters,
errStr: "no upstream found for remote service",
},
}
for _, enterprise := range []bool{false, true} {
for name, c := range cases {
c := c
t.Run(fmt.Sprintf("%s_ent_%t", name, enterprise), func(t *testing.T) {
t.Parallel()
rtCfg := c.rtCfg(enterprise)
rtCfg.EnvoyExtension = api.EnvoyExtension{
Name: c.extName,
Arguments: c.args(enterprise),
}
w, err := construct(rtCfg.EnvoyExtension)
require.NoError(t, err)
require.Equal(t, c.canApply, w.CanApply(rtCfg))
if !c.canApply {
return
}
route, patched, err := w.PatchRoute(c.rtCfg(enterprise), nil)
require.Nil(t, route)
require.False(t, patched)
require.NoError(t, err)
cluster, patched, err := w.PatchCluster(c.rtCfg(enterprise), nil)
require.Nil(t, cluster)
require.False(t, patched)
require.NoError(t, err)
inputHttpConMgr := makeHttpConMgr(t, c.inputFilters())
obsHttpConMgr, patched, err := w.PatchFilter(c.rtCfg(enterprise), inputHttpConMgr)
if c.errStr == "" {
require.NoError(t, err)
require.True(t, patched)
cfg := testWasmConfigFromMap(t, c.args(enterprise))
expHttpConMgr := makeHttpConMgr(t, c.expFilters(cfg))
if c.debug {
t.Logf("cfg =\n%s\n\n", cfg.toJSON(t))
t.Logf("expFilterJSON =\n%s\n\n", protoToJSON(t, expHttpConMgr))
t.Logf("obsfilterJSON =\n%s\n\n", protoToJSON(t, obsHttpConMgr))
}
prototest.AssertDeepEqual(t, expHttpConMgr, obsHttpConMgr)
} else {
require.Contains(t, err.Error(), c.errStr)
}
})
}
}
}
func TestWasmConstructor(t *testing.T) {
t.Parallel()
cases := map[string]struct {
name string
args func(bool) map[string]any
errStr string
}{
"with no arguments": {
name: api.BuiltinWasmExtension,
args: func(_ bool) map[string]any { return nil },
errStr: "VmConfig.Code must provide exactly one of Local or Remote data source",
},
"invalid protocol": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.Protocol = "invalid"
return cfg.toMap(t)
},
errStr: `unsupported Protocol "invalid", expected "tcp" or "http"`,
},
"invalid proxy type": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.ProxyType = "invalid"
return cfg.toMap(t)
},
errStr: "unsupported ProxyType",
},
"invalid listener type": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.ListenerType = "invalid"
return cfg.toMap(t)
},
errStr: `unsupported ListenerType "invalid", expected "inbound" or "outbound"`,
},
"invalid runtime": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Runtime = "invalid"
return cfg.toMap(t)
},
errStr: "unsupported runtime",
},
"both local and remote files": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Local.Filename = "plugin.wasm"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "http://file-server/plugin.wasm"
return cfg.toMap(t)
},
errStr: "VmConfig.Code must provide exactly one of Local or Remote data source",
},
"service and uri required for remote files": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
return cfg.toMap(t)
},
errStr: "both Service and URI are required for Remote data sources",
},
"no sha for remote file": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "http://file-server/plugin.wasm"
return cfg.toMap(t)
},
errStr: "SHA256 checksum is required for Remote data sources",
},
"invalid url for remote file": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "://bogus.url.com/error"
return cfg.toMap(t)
},
errStr: `invalid HttpURI.URI: parse "://bogus.url.com/error": missing protocol scheme`,
},
"decoding error": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
a := makeTestWasmConfig(ent).toMap(t)
setField(a, "PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval", 1000)
return a
},
errStr: "got unconvertible type",
},
"invalid http timeout": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout = "invalid"
return cfg.toMap(t)
},
errStr: `failed to parse HttpURI.Timeout "invalid" as a duration`,
},
"invalid num retries": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries = -1
return cfg.toMap(t)
},
errStr: "RetryPolicy.NumRetries must be greater than or equal to 0",
},
"invalid base interval": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "0s"
return cfg.toMap(t)
},
errStr: `RetryBackOff.BaseInterval "0s" must be greater than zero and less than or equal to RetryBackOff.MaxInterval`,
},
"invalid max interval": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "10s"
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "5s"
return cfg.toMap(t)
},
errStr: `RetryBackOff.MaxInterval "5s" must be greater than or equal to RetryBackOff.BaseInterval "10s"`,
},
"invalid base interval duration": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "invalid"
return cfg.toMap(t)
},
errStr: `failed to parse RetryBackOff.BaseInterval "invalid"`,
},
"invalid max interval duration": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any {
cfg := newTestWasmConfig(ent)
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "invalid"
return cfg.toMap(t)
},
errStr: `failed to parse RetryBackOff.MaxInterval "invalid"`,
},
"invalid extension name": {
name: "invalid",
args: func(ent bool) map[string]any { return newTestWasmConfig(ent).toMap(t) },
errStr: `expected extension name "builtin/wasm" but got "invalid"`,
},
"valid configuration": {
name: api.BuiltinWasmExtension,
args: func(ent bool) map[string]any { return makeTestWasmConfig(ent).toMap(t) },
},
}
for _, enterprise := range []bool{false, true} {
for name, c := range cases {
c := c
t.Run(fmt.Sprintf("%s_ent_%t", name, enterprise), func(t *testing.T) {
t.Parallel()
svc := api.CompoundServiceName{Name: "svc"}
ext := extensioncommon.RuntimeConfig{
ServiceName: svc,
EnvoyExtension: api.EnvoyExtension{
Name: c.name,
Arguments: c.args(enterprise),
},
}
e, err := Constructor(ext.EnvoyExtension)
if c.errStr == "" {
require.NoError(t, err)
require.NotNil(t, e)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), c.errStr)
}
})
}
}
}
type testWasmConfig struct {
Required bool
Protocol string
ProxyType string
ListenerType string
PluginConfig struct {
Name string
RootID string
VmConfig struct {
VmID string
Runtime string
Code struct {
Local struct {
Filename string
}
Remote struct {
HttpURI struct {
Service api.CompoundServiceName
URI string
Timeout string
}
SHA256 string
RetryPolicy struct {
RetryBackOff struct {
BaseInterval string
MaxInterval string
}
NumRetries int
}
}
}
Configuration string
EnvironmentVariables struct {
HostEnvKeys []string
KeyValues map[string]string
}
}
Configuration string
CapabilityRestrictionConfiguration struct {
AllowedCapabilities map[string]any
}
}
}
func testWasmConfigFromMap(t *testing.T, m map[string]any) testWasmConfig {
t.Helper()
var cfg testWasmConfig
require.NoError(t, mapstructure.Decode(m, &cfg))
return cfg
}
func (c testWasmConfig) toMap(t *testing.T) map[string]any {
t.Helper()
var m map[string]any
require.NoError(t, json.Unmarshal(c.toJSON(t), &m))
return m
}
func (c testWasmConfig) toJSON(t *testing.T) []byte {
t.Helper()
b, err := json.MarshalIndent(c, "", " ")
require.NoError(t, err)
return b
}
func (cfg testWasmConfig) toHttpWasmFilter(t *testing.T) *envoy_wasm_v3.PluginConfig {
t.Helper()
var code *envoy_core_v3.AsyncDataSource
if cfg.PluginConfig.VmConfig.Code.Local.Filename != "" {
code = &envoy_core_v3.AsyncDataSource{
Specifier: &envoy_core_v3.AsyncDataSource_Local{
Local: &envoy_core_v3.DataSource{
Specifier: &envoy_core_v3.DataSource_Filename{
Filename: cfg.PluginConfig.VmConfig.Code.Local.Filename,
},
},
},
}
} else {
cluster, err := url.Parse(cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI)
require.NoError(t, err)
timeout, err := time.ParseDuration(cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout)
require.NoError(t, err)
baseInterval, err := time.ParseDuration(cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval)
require.NoError(t, err)
maxInterval, err := time.ParseDuration(cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval)
require.NoError(t, err)
code = &envoy_core_v3.AsyncDataSource{
Specifier: &envoy_core_v3.AsyncDataSource_Remote{
Remote: &envoy_core_v3.RemoteDataSource{
Sha256: cfg.PluginConfig.VmConfig.Code.Remote.SHA256,
HttpUri: &envoy_core_v3.HttpUri{
Uri: cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI,
Timeout: &durationpb.Duration{Seconds: int64(timeout.Seconds())},
HttpUpstreamType: &envoy_core_v3.HttpUri_Cluster{
Cluster: cluster.Host,
},
},
RetryPolicy: &envoy_core_v3.RetryPolicy{
RetryBackOff: &envoy_core_v3.BackoffStrategy{
BaseInterval: &durationpb.Duration{Seconds: int64(baseInterval.Seconds())},
MaxInterval: &durationpb.Duration{Seconds: int64(maxInterval.Seconds())},
},
NumRetries: wrapperspb.UInt32(uint32(cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries)),
},
},
},
}
}
var capConfig *envoy_wasm_v3.CapabilityRestrictionConfig
if len(cfg.PluginConfig.CapabilityRestrictionConfiguration.AllowedCapabilities) > 0 {
caps := make(map[string]*envoy_wasm_v3.SanitizationConfig)
for cap := range cfg.PluginConfig.CapabilityRestrictionConfiguration.AllowedCapabilities {
caps[cap] = &envoy_wasm_v3.SanitizationConfig{}
}
capConfig = &envoy_wasm_v3.CapabilityRestrictionConfig{AllowedCapabilities: caps}
}
var vmConfiguration *anypb.Any
if cfg.PluginConfig.VmConfig.Configuration != "" {
vmConfiguration = makeAny(t, wrapperspb.String(cfg.PluginConfig.VmConfig.Configuration))
}
var envVars *envoy_wasm_v3.EnvironmentVariables
if len(cfg.PluginConfig.VmConfig.EnvironmentVariables.HostEnvKeys) > 0 ||
len(cfg.PluginConfig.VmConfig.EnvironmentVariables.KeyValues) > 0 {
envVars = &envoy_wasm_v3.EnvironmentVariables{
HostEnvKeys: cfg.PluginConfig.VmConfig.EnvironmentVariables.HostEnvKeys,
KeyValues: cfg.PluginConfig.VmConfig.EnvironmentVariables.KeyValues,
}
}
var pluginConfiguration *anypb.Any
if cfg.PluginConfig.Configuration != "" {
pluginConfiguration = makeAny(t, wrapperspb.String(cfg.PluginConfig.Configuration))
}
rt := cfg.PluginConfig.VmConfig.Runtime
if rt == "" {
rt = supportedRuntimes[0]
}
return &envoy_wasm_v3.PluginConfig{
Name: cfg.PluginConfig.Name,
RootId: cfg.PluginConfig.RootID,
Vm: &envoy_wasm_v3.PluginConfig_VmConfig{
VmConfig: &envoy_wasm_v3.VmConfig{
VmId: cfg.PluginConfig.VmConfig.VmID,
Runtime: fmt.Sprintf("envoy.wasm.runtime.%s", rt),
Code: code,
Configuration: vmConfiguration,
EnvironmentVariables: envVars,
},
},
Configuration: pluginConfiguration,
FailOpen: !cfg.Required,
CapabilityRestrictionConfig: capConfig,
}
}
func makeAny(t *testing.T, m proto.Message) *anypb.Any {
t.Helper()
v, err := anypb.New(m)
require.NoError(t, err)
return v
}
func makeHttpConMgr(t *testing.T, filters []*envoy_http_v3.HttpFilter) *envoy_listener_v3.Filter {
t.Helper()
return &envoy_listener_v3.Filter{
Name: "envoy.filters.network.http_connection_manager",
ConfigType: &envoy_listener_v3.Filter_TypedConfig{
TypedConfig: makeAny(t, &envoy_http_v3.HttpConnectionManager{
HttpFilters: filters,
}),
},
}
}
func makeTestHttpFilters() []*envoy_http_v3.HttpFilter {
return []*envoy_http_v3.HttpFilter{
{Name: "one"},
{Name: "two"},
{Name: "envoy.filters.http.router"},
{Name: "three"},
}
}
func makeTestRuntimeConfig(enterprise bool) *extensioncommon.RuntimeConfig {
var ns, ap string
if enterprise {
ns = "ns1"
ap = "ap1"
}
return &extensioncommon.RuntimeConfig{
Kind: api.ServiceKindConnectProxy,
ServiceName: api.CompoundServiceName{Name: "test-service"},
LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
{
Name: "test-file-server",
Namespace: acl.NamespaceOrDefault(ns),
Partition: acl.PartitionOrDefault(ap),
}: {
SNI: map[string]struct{}{"test-file-server": {}},
EnvoyID: "test-file-server",
},
},
}
}
func makeTestWasmConfig(enterprise bool) *testWasmConfig {
cfg := newTestWasmConfig(enterprise)
cfg.Required = false
cfg.Protocol = "http"
cfg.ProxyType = "connect-proxy"
cfg.ListenerType = "inbound"
cfg.PluginConfig.Name = "test-plugin-name"
cfg.PluginConfig.RootID = "test-root-id"
cfg.PluginConfig.VmConfig.VmID = "test-vm-id"
cfg.PluginConfig.VmConfig.Runtime = "wasmtime"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Name = "test-file-server"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.URI = "https://test-file-server/plugin.wasm"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Timeout = "5s"
cfg.PluginConfig.VmConfig.Code.Remote.SHA256 = "d05d88b0ce8a8f1d5176481e0af3ae5c65ed82cbfb8c61506c5354b076078545"
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.BaseInterval = "3s"
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.RetryBackOff.MaxInterval = "15s"
cfg.PluginConfig.VmConfig.Code.Remote.RetryPolicy.NumRetries = 3
cfg.PluginConfig.VmConfig.Configuration = "test-vm-configuration"
cfg.PluginConfig.VmConfig.EnvironmentVariables.HostEnvKeys = []string{"PATH"}
cfg.PluginConfig.VmConfig.EnvironmentVariables.KeyValues = map[string]string{"TEST_VAR": "TEST_VAL"}
cfg.PluginConfig.Configuration = "test-plugin-configuration"
cfg.PluginConfig.CapabilityRestrictionConfiguration.AllowedCapabilities = map[string]any{"proxy_on_vm_start": true}
return cfg
}
func newTestWasmConfig(enterprise bool) *testWasmConfig {
cfg := &testWasmConfig{}
if enterprise {
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Namespace = "ns1"
cfg.PluginConfig.VmConfig.Code.Remote.HttpURI.Service.Partition = "ap1"
}
return cfg
}
func protoToJSON(t *testing.T, pb proto.Message) string {
t.Helper()
m := protojson.MarshalOptions{
Indent: " ",
}
gotJSON, err := m.Marshal(pb)
require.NoError(t, err)
return string(gotJSON)
}
func setField(m map[string]any, path string, value any) {
upsertField(m, path, value, 0)
}
func upsertField(m map[string]any, path string, value any, index int) {
keys := strings.Split(path, ".")
key := keys[index]
if val, ok := m[key]; ok {
// update the value
if index == len(keys)-1 {
m[key] = value
} else {
upsertField(val.(map[string]any), path, value, index+1)
}
} else {
// key does not exist so insert it
if index == len(keys)-1 {
m[key] = value
} else {
newMap := make(map[string]any)
m[key] = newMap
upsertField(newMap, path, value, index+1)
}
}
}

View File

@ -9,6 +9,7 @@ import (
awslambda "github.com/hashicorp/consul/agent/envoyextensions/builtin/aws-lambda"
"github.com/hashicorp/consul/agent/envoyextensions/builtin/http/localratelimit"
"github.com/hashicorp/consul/agent/envoyextensions/builtin/lua"
"github.com/hashicorp/consul/agent/envoyextensions/builtin/wasm"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/envoyextensions/extensioncommon"
"github.com/hashicorp/go-multierror"
@ -20,6 +21,7 @@ var extensionConstructors = map[string]extensionConstructor{
api.BuiltinLuaExtension: lua.Constructor,
api.BuiltinAWSLambdaExtension: awslambda.Constructor,
api.BuiltinLocalRatelimitExtension: localratelimit.Constructor,
api.BuiltinWasmExtension: wasm.Constructor,
}
// ConstructExtension attempts to lookup and build an extension from the registry with the

View File

@ -232,6 +232,66 @@ end`,
}, nil)
},
},
{
name: "wasm-http-local-file",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) {
ns.Proxy.Config["protocol"] = "http"
ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{
{
Name: api.BuiltinWasmExtension,
Arguments: map[string]interface{}{
"Protocol": "http",
"ListenerType": "inbound",
"PluginConfig": map[string]interface{}{
"VmConfig": map[string]interface{}{
"Code": map[string]interface{}{
"Local": map[string]interface{}{
"Filename": "/path/to/extension.wasm",
},
},
},
"Configuration": `{"foo": "bar"}`,
},
},
},
}
}, nil)
},
},
{
name: "wasm-http-remote-file",
create: func(t testinf.T) *proxycfg.ConfigSnapshot {
return proxycfg.TestConfigSnapshot(t, func(ns *structs.NodeService) {
ns.Proxy.Config["protocol"] = "http"
ns.Proxy.EnvoyExtensions = []structs.EnvoyExtension{
{
Name: api.BuiltinWasmExtension,
Arguments: map[string]interface{}{
"Protocol": "http",
"ListenerType": "inbound",
"PluginConfig": map[string]interface{}{
"VmConfig": map[string]interface{}{
"Code": map[string]interface{}{
"Remote": map[string]interface{}{
"HttpURI": map[string]interface{}{
"Service": map[string]interface{}{
"Name": "db",
},
"URI": "https://db/plugin.wasm",
},
"SHA256": "d05d88b0ce8a8f1d5176481e0af3ae5c65ed82cbfb8c61506c5354b076078545",
},
},
},
"Configuration": `{"foo": "bar"}`,
},
},
},
}
}, nil)
},
},
}
latestEnvoyVersion := xdscommon.EnvoyVersions[0]

View File

@ -89,8 +89,9 @@ func GetRuntimeConfigurations(cfgSnap *proxycfg.ConfigSnapshot) map[api.Compound
EnvoyExtension: ext,
ServiceName: localSvc,
// Upstreams is nil to signify this extension is not being applied to an upstream service, but rather to the local service.
Upstreams: nil,
Kind: kind,
Upstreams: nil,
LocalUpstreams: upstreamMap,
Kind: kind,
}
extensionConfigurationsMap[localSvc] = append(extensionConfigurationsMap[localSvc], extCfg)
}

View File

@ -256,8 +256,17 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
},
},
ServiceName: webService,
Upstreams: nil,
Kind: api.ServiceKindConnectProxy,
Upstreams: nil,
LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
dbService: {
SNI: map[string]struct{}{
"db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {},
},
EnvoyID: "db",
OutgoingProxyKind: "connect-proxy",
},
},
},
{
EnvoyExtension: api.EnvoyExtension{
@ -268,8 +277,17 @@ func TestGetRuntimeConfigurations_ConnectProxy(t *testing.T) {
},
},
ServiceName: webService,
Upstreams: nil,
Kind: api.ServiceKindConnectProxy,
Upstreams: nil,
LocalUpstreams: map[api.CompoundServiceName]*extensioncommon.UpstreamData{
dbService: {
SNI: map[string]struct{}{
"db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul": {},
},
EnvoyID: "db",
OutgoingProxyKind: "connect-proxy",
},
},
},
},
},

View File

@ -0,0 +1,145 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "5s",
"circuitBreakers": {
},
"outlierDetection": {
},
"commonLbConfig": {
"healthyPanicThreshold": {
}
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"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"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
}
]
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "5s",
"circuitBreakers": {
},
"outlierDetection": {
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"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"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target"
},
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target"
}
]
}
},
"sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
}
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "local_app",
"type": "STATIC",
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "local_app",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 8080
}
}
}
}
]
}
]
}
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"nonce": "00000001"
}

View File

@ -0,0 +1,145 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"altStatName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "5s",
"circuitBreakers": {
},
"outlierDetection": {
},
"commonLbConfig": {
"healthyPanicThreshold": {
}
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"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"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/db"
}
]
}
},
"sni": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"type": "EDS",
"edsClusterConfig": {
"edsConfig": {
"ads": {
},
"resourceApiVersion": "V3"
}
},
"connectTimeout": "5s",
"circuitBreakers": {
},
"outlierDetection": {
},
"transportSocket": {
"name": "tls",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
"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"
},
"matchSubjectAltNames": [
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc1/svc/geo-cache-target"
},
{
"exact": "spiffe://11111111-2222-3333-4444-555555555555.consul/ns/default/dc/dc2/svc/geo-cache-target"
}
]
}
},
"sni": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul"
}
}
},
{
"@type": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"name": "local_app",
"type": "STATIC",
"connectTimeout": "5s",
"loadAssignment": {
"clusterName": "local_app",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 8080
}
}
}
}
]
}
]
}
}
],
"typeUrl": "type.googleapis.com/envoy.config.cluster.v3.Cluster",
"nonce": "00000001"
}

View File

@ -0,0 +1,75 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
},
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.20.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
}
],
"typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"nonce": "00000001"
}

View File

@ -0,0 +1,75 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
},
{
"@type": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"clusterName": "geo-cache.default.dc1.query.11111111-2222-3333-4444-555555555555.consul",
"endpoints": [
{
"lbEndpoints": [
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.10.1.1",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
},
{
"endpoint": {
"address": {
"socketAddress": {
"address": "10.20.1.2",
"portValue": 8080
}
}
},
"healthStatus": "HEALTHY",
"loadBalancingWeight": 1
}
]
}
]
}
],
"typeUrl": "type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment",
"nonce": "00000001"
}

View File

@ -0,0 +1,271 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "db:127.0.0.1:9191",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.db.default.default.dc1",
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "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.wasm",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm",
"config": {
"vmConfig": {
"runtime": "envoy.wasm.runtime.v8",
"code": {
"local": {
"filename": "/path/to/extension.wasm"
}
}
},
"configuration": {
"@type": "type.googleapis.com/google.protobuf.StringValue",
"value": "{\"foo\": \"bar\"}"
},
"failOpen": true
}
}
},
{
"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,276 @@
{
"versionInfo": "00000001",
"resources": [
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "db:127.0.0.1:9191",
"address": {
"socketAddress": {
"address": "127.0.0.1",
"portValue": 9191
}
},
"filterChains": [
{
"filters": [
{
"name": "envoy.filters.network.tcp_proxy",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy",
"statPrefix": "upstream.db.default.default.dc1",
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul"
}
}
]
}
],
"trafficDirection": "OUTBOUND"
},
{
"@type": "type.googleapis.com/envoy.config.listener.v3.Listener",
"name": "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.wasm",
"typedConfig": {
"@type": "type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm",
"config": {
"vmConfig": {
"runtime": "envoy.wasm.runtime.v8",
"code": {
"remote": {
"httpUri": {
"uri": "https://db/plugin.wasm",
"cluster": "db.default.dc1.internal.11111111-2222-3333-4444-555555555555.consul",
"timeout": "1s"
},
"sha256": "d05d88b0ce8a8f1d5176481e0af3ae5c65ed82cbfb8c61506c5354b076078545"
}
}
},
"configuration": {
"@type": "type.googleapis.com/google.protobuf.StringValue",
"value": "{\"foo\": \"bar\"}"
},
"failOpen": true
}
}
},
{
"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,5 @@
{
"versionInfo": "00000001",
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"nonce": "00000001"
}

View File

@ -0,0 +1,5 @@
{
"versionInfo": "00000001",
"typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
"nonce": "00000001"
}

View File

@ -41,6 +41,7 @@ const (
BuiltinAWSLambdaExtension string = "builtin/aws/lambda"
BuiltinLuaExtension string = "builtin/lua"
BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit"
BuiltinWasmExtension string = "builtin/wasm"
)
type ConfigEntry interface {

View File

@ -1,29 +1,27 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package awslambda
package extensioncommon
import (
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_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"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
// This is copied from xds and not put into the shared package because I'm not
// convinced it should be shared.
func makeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) {
// MakeUpstreamTLSTransportSocket generates an Envoy transport socket for the given TLS context.
func MakeUpstreamTLSTransportSocket(tlsContext *envoy_tls_v3.UpstreamTlsContext) (*envoy_core_v3.TransportSocket, error) {
if tlsContext == nil {
return nil, nil
}
return makeTransportSocket("tls", tlsContext)
return MakeTransportSocket("tls", tlsContext)
}
func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) {
// MakeTransportSocket generates an Envoy transport socket from the given proto message.
func MakeTransportSocket(name string, config proto.Message) (*envoy_core_v3.TransportSocket, error) {
any, err := anypb.New(config)
if err != nil {
return nil, err
@ -36,7 +34,8 @@ func makeTransportSocket(name string, config proto.Message) (*envoy_core_v3.Tran
}, nil
}
func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) {
// MakeEnvoyHTTPFilter generates an Envoy HTTP filter from the given proto message.
func MakeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFilter, error) {
any, err := anypb.New(cfg)
if err != nil {
return nil, err
@ -48,7 +47,8 @@ func makeEnvoyHTTPFilter(name string, cfg proto.Message) (*envoy_http_v3.HttpFil
}, nil
}
func makeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) {
// MakeFilter generates an Envoy listener filter from the given proto message.
func MakeFilter(name string, cfg proto.Message) (*envoy_listener_v3.Filter, error) {
any, err := anypb.New(cfg)
if err != nil {
return nil, err

View File

@ -23,10 +23,9 @@ type UpstreamData struct {
VIP string
}
// RuntimeConfig is the configuration for an extension attached to a service on the local proxy. Currently, it
// is only created for the local proxy's upstream service if the upstream service has an extension configured. In the
// future it will also include information about the service local to the local proxy as well. It should depend on the
// API client rather than the structs package because the API client is meant to be public.
// RuntimeConfig is the configuration for an extension attached to a service on the local proxy.
// It should depend on the API client rather than the structs package because the API client is
// meant to be public.
type RuntimeConfig struct {
// EnvoyExtension is the extension that will patch Envoy resources.
EnvoyExtension api.EnvoyExtension
@ -39,27 +38,40 @@ type RuntimeConfig struct {
// If there are no Upstreams, then EnvoyExtension is being applied to the local service's resources.
Upstreams map[api.CompoundServiceName]*UpstreamData
// LocalUpstreams will only be configured if the EnvoyExtension is being applied to the local service.
LocalUpstreams map[api.CompoundServiceName]*UpstreamData
// Kind is mode the local Envoy proxy is running in. For now, only connect proxy and
// terminating gateways are supported.
Kind api.ServiceKind
}
// IsLocal indicates if the extension configuration is for the proxy's local service.
func (ec RuntimeConfig) IsLocal() bool {
return !ec.IsUpstream()
}
// IsUpstream indicates if the extension configuration is for an upstream service.
func (ec RuntimeConfig) IsUpstream() bool {
_, ok := ec.Upstreams[ec.ServiceName]
return ok
}
// MatchesUpstreamServiceSNI indicates if the extension configuration is for an upstream service
// that matches the given SNI.
func (ec RuntimeConfig) MatchesUpstreamServiceSNI(sni string) bool {
u := ec.Upstreams[ec.ServiceName]
_, match := u.SNI[sni]
return match
}
// EnvoyID returns the unique Envoy identifier of the upstream service.
func (ec RuntimeConfig) EnvoyID() string {
u := ec.Upstreams[ec.ServiceName]
return u.EnvoyID
}
// OutgoingProxyKind returns the service kind for the outgoing listener of an upstream service.
func (ec RuntimeConfig) OutgoingProxyKind() api.ServiceKind {
u := ec.Upstreams[ec.ServiceName]
return u.OutgoingProxyKind

View File

@ -7,7 +7,7 @@ load helpers
}
@test "s1 has lambda http filter for l1" {
assert_lambda_envoy_dynamic_http_filter_exists localhost:19000 l1 $AWS_LAMBDA_ARN
assert_lambda_envoy_dynamic_http_filter_exists localhost:19000 $AWS_LAMBDA_ARN
}
@test "terminating gateway has lambda cluster for l2" {
@ -15,7 +15,7 @@ load helpers
}
@test "terminating gateway has lambda http filter for l2" {
assert_lambda_envoy_dynamic_http_filter_exists localhost:20000 l2 $AWS_LAMBDA_ARN
assert_lambda_envoy_dynamic_http_filter_exists localhost:20000 $AWS_LAMBDA_ARN
}
@test "s1 can call l1 through its sidecar-proxy" {

View File

@ -0,0 +1,4 @@
#!/bin/bash
snapshot_envoy_admin localhost:19000 s1 primary || true
snapshot_envoy_admin localhost:19001 s2 primary || true

BIN
test/integration/connect/envoy/case-wasm/data/dummy.wasm (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,16 @@
services {
name = "s1"
port = 8080
connect {
sidecar_service {
proxy {
upstreams = [
{
destination_name = "s2"
local_bind_port = 5000
}
]
}
}
}
}

View File

@ -0,0 +1,5 @@
services {
name = "s2"
port = 8181
connect { sidecar_service {} }
}

View File

@ -0,0 +1,33 @@
#!/bin/bash
set -eEuo pipefail
upsert_config_entry primary '
Kind = "service-defaults"
Name = "s2"
Protocol = "http"
EnvoyExtensions = [
{
Name = "builtin/wasm"
Arguments = {
Protocol = "http"
ListenerType = "inbound"
PluginConfig = {
VmConfig = {
Code = {
Local = {
Filename = "/workdir/primary/data/dummy.wasm"
}
}
}
Configuration = "plugin configuration"
}
}
}
]
'
register_services primary
gen_envoy_bootstrap s1 19000 primary
gen_envoy_bootstrap s2 19001 primary

View File

@ -0,0 +1,3 @@
#!/bin/bash
export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy"

View File

@ -0,0 +1,25 @@
#!/usr/bin/env bats
load helpers
@test "s1 proxy admin is up on :19000" {
retry_default curl -f -s localhost:19000/stats -o /dev/null
}
@test "s2 proxy admin is up on :19001" {
retry_default curl -f -s localhost:19001/stats -o /dev/null
}
@test "s1 proxy listener should be up and have right cert" {
assert_proxy_presents_cert_uri localhost:21000 s1
}
@test "s2 proxy is configured with a wasm http filter" {
run get_envoy_http_filter localhost:19001 envoy.filters.http.wasm
[ "$status" == 0 ]
[ "$(echo "$output" | jq -r '.typed_config.config.vm_config.runtime')" == "envoy.wasm.runtime.v8" ]
[ "$(echo "$output" | jq -r '.typed_config.config.vm_config.code.local.filename')" == "/workdir/primary/data/dummy.wasm" ]
[ "$(echo "$output" | jq -r '.typed_config.config.configuration.value')" == "plugin configuration" ]
[ "$(echo "$output" | jq -r '.typed_config.config.fail_open')" == "true" ]
}

View File

@ -260,6 +260,14 @@ function get_envoy_network_rbac_once {
echo "$output" | jq --raw-output '.configs[2].dynamic_listeners[].active_state.listener.filter_chains[0].filters[] | select(.name == "envoy.filters.network.rbac") | .typed_config'
}
function get_envoy_http_filter {
local HOSTPORT=$1
local FILTER_NAME=$2
run retry_default curl -s -f $HOSTPORT/config_dump
[ "$status" -eq 0 ]
echo "$output" | jq --raw-output ".configs[2].dynamic_listeners[] | .active_state.listener.filter_chains[].filters[] | select(.name == \"envoy.filters.network.http_connection_manager\") | .typed_config.http_filters[] | select(.name == \"${FILTER_NAME}\")"
}
function get_envoy_listener_filters {
local HOSTPORT=$1
run retry_default curl -s -f $HOSTPORT/config_dump
@ -1078,15 +1086,6 @@ function assert_service_has_imported {
fi
}
function get_lambda_envoy_http_filter {
local HOSTPORT=$1
local NAME_PREFIX=$2
run retry_default curl -s -f $HOSTPORT/config_dump
[ "$status" -eq 0 ]
# get the full http filter object so the individual fields can be validated.
echo "$output" | jq --raw-output ".configs[2].dynamic_listeners[] | .active_state.listener.filter_chains[].filters[] | select(.name == \"envoy.filters.network.http_connection_manager\") | .typed_config.http_filters[] | select(.name == \"envoy.filters.http.aws_lambda\") | .typed_config"
}
function register_lambdas {
local DC=${1:-primary}
# register lambdas to the catalog
@ -1114,13 +1113,12 @@ function assert_lambda_envoy_dynamic_cluster_exists {
function assert_lambda_envoy_dynamic_http_filter_exists {
local HOSTPORT=$1
local NAME_PREFIX=$2
local ARN=$3
local ARN=$2
local FILTER=$(get_lambda_envoy_http_filter $HOSTPORT $NAME_PREFIX)
local FILTER=$(get_envoy_http_filter $HOSTPORT 'envoy.filters.http.aws_lambda')
[ -n "$FILTER" ]
[ "$(echo $FILTER | jq -r '.arn')" == "$ARN" ]
[ "$(echo $FILTER | jq -r '.typed_config | .arn')" == "$ARN" ]
}
function varsub {