From 1c5ca0da53a7abd7e7ea7e8621cd1fcb72959802 Mon Sep 17 00:00:00 2001 From: cskh Date: Tue, 7 Feb 2023 21:56:15 -0500 Subject: [PATCH] feat: envoy extension - http local rate limit (#16196) - http local rate limit - Apply rate limit only to local_app - unit test and integ test --- .../builtin/http/localratelimit/copied.go | 58 ++++ .../builtin/http/localratelimit/ratelimit.go | 198 ++++++++++++++ .../http/localratelimit/ratelimit_test.go | 160 +++++++++++ .../envoyextensions/registered_extensions.go | 6 +- agent/xds/delta_envoy_extender_oss_test.go | 21 ++ ...cal-ratelimit-applyto-filter.latest.golden | 127 +++++++++ ...cal-ratelimit-applyto-filter.latest.golden | 75 +++++ ...cal-ratelimit-applyto-filter.latest.golden | 256 ++++++++++++++++++ ...cal-ratelimit-applyto-filter.latest.golden | 5 + api/config_entry.go | 5 +- go.mod | 2 +- .../envoy/case-envoyext-ratelimit/capture.sh | 4 + .../case-envoyext-ratelimit/service_s1.hcl | 16 ++ .../case-envoyext-ratelimit/service_s2.hcl | 5 + .../envoy/case-envoyext-ratelimit/setup.sh | 46 ++++ .../envoy/case-envoyext-ratelimit/vars.sh | 3 + .../envoy/case-envoyext-ratelimit/verify.bats | 57 ++++ 17 files changed, 1039 insertions(+), 5 deletions(-) create mode 100644 agent/envoyextensions/builtin/http/localratelimit/copied.go create mode 100644 agent/envoyextensions/builtin/http/localratelimit/ratelimit.go create mode 100644 agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go create mode 100644 agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden create mode 100644 agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden create mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/capture.sh create mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/service_s1.hcl create mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/service_s2.hcl create mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/setup.sh create mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/vars.sh create mode 100644 test/integration/connect/envoy/case-envoyext-ratelimit/verify.bats diff --git a/agent/envoyextensions/builtin/http/localratelimit/copied.go b/agent/envoyextensions/builtin/http/localratelimit/copied.go new file mode 100644 index 000000000..ec1d4988e --- /dev/null +++ b/agent/envoyextensions/builtin/http/localratelimit/copied.go @@ -0,0 +1,58 @@ +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 +} diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go new file mode 100644 index 000000000..3ffea6f1f --- /dev/null +++ b/agent/envoyextensions/builtin/http/localratelimit/ratelimit.go @@ -0,0 +1,198 @@ +package localratelimit + +import ( + "errors" + "fmt" + "time" + + envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + envoy_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_ratelimit "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/local_ratelimit/v3" + envoy_http_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + envoy_resource_v3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + "github.com/golang/protobuf/ptypes/wrappers" + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/mapstructure" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/envoyextensions/extensioncommon" +) + +type ratelimit struct { + ProxyType string + + // Token bucket of the rate limit + MaxTokens *int + TokensPerFill *int + FillInterval *int + + // Percent of requests to be rate limited + FilterEnabled *uint32 + FilterEnforced *uint32 +} + +var _ extensioncommon.BasicExtension = (*ratelimit)(nil) + +// Constructor follows a specific function signature required for the extension registration. +func Constructor(ext api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) { + var r ratelimit + if name := ext.Name; name != api.BuiltinLocalRatelimitExtension { + return nil, fmt.Errorf("expected extension name 'ratelimit' but got %q", name) + } + + if err := r.fromArguments(ext.Arguments); err != nil { + return nil, err + } + + return &extensioncommon.BasicEnvoyExtender{ + Extension: &r, + }, nil +} + +func (r *ratelimit) fromArguments(args map[string]interface{}) error { + if err := mapstructure.Decode(args, r); err != nil { + return fmt.Errorf("error decoding extension arguments: %v", err) + } + return r.validate() +} + +func (r *ratelimit) validate() error { + var resultErr error + + // NOTE: Envoy requires FillInterval value must be greater than 0. + // If unset, it is considered as 0. + if r.FillInterval == nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("FillInterval(in second) is missing")) + } else if *r.FillInterval <= 0 { + resultErr = multierror.Append(resultErr, fmt.Errorf("FillInterval(in second) must be greater than 0, got %d", *r.FillInterval)) + } + + // NOTE: Envoy requires MaxToken value must be greater than 0. + // If unset, it is considered as 0. + if r.MaxTokens == nil { + resultErr = multierror.Append(resultErr, fmt.Errorf("MaxTokens is missing")) + } else if *r.MaxTokens <= 0 { + resultErr = multierror.Append(resultErr, fmt.Errorf("MaxTokens must be greater than 0, got %d", r.MaxTokens)) + } + + // TokensPerFill is allowed to unset. In this case, envoy + // uses its default value, which is 1. + if r.TokensPerFill != nil && *r.TokensPerFill <= 0 { + resultErr = multierror.Append(resultErr, fmt.Errorf("TokensPerFill must be greater than 0, got %d", *r.TokensPerFill)) + } + + if err := validateProxyType(r.ProxyType); err != nil { + resultErr = multierror.Append(resultErr, err) + } + + return resultErr +} + +// CanApply determines if the extension can apply to the given extension configuration. +func (p *ratelimit) CanApply(config *extensioncommon.RuntimeConfig) bool { + // rate limit is only applied to the service itself since the limit is + // aggregated from all downstream connections. + return string(config.Kind) == p.ProxyType && !config.IsUpstream() +} + +// PatchRoute does nothing. +func (p ratelimit) PatchRoute(_ *extensioncommon.RuntimeConfig, route *envoy_route_v3.RouteConfiguration) (*envoy_route_v3.RouteConfiguration, bool, error) { + return route, false, nil +} + +// PatchCluster does nothing. +func (p ratelimit) PatchCluster(_ *extensioncommon.RuntimeConfig, c *envoy_cluster_v3.Cluster) (*envoy_cluster_v3.Cluster, bool, error) { + return c, false, nil +} + +// PatchFilter inserts a http local rate_limit filter at the head of +// envoy.filters.network.http_connection_manager filters +func (p ratelimit) PatchFilter(_ *extensioncommon.RuntimeConfig, filter *envoy_listener_v3.Filter) (*envoy_listener_v3.Filter, bool, error) { + if filter.Name != "envoy.filters.network.http_connection_manager" { + return filter, false, nil + } + if typedConfig := filter.GetTypedConfig(); typedConfig == nil { + return filter, false, errors.New("error getting typed config for http filter") + } + + config := envoy_resource_v3.GetHTTPConnectionManager(filter) + if config == nil { + return filter, false, errors.New("error unmarshalling filter") + } + + tokenBucket := envoy_type_v3.TokenBucket{} + + if p.TokensPerFill != nil { + tokenBucket.TokensPerFill = &wrappers.UInt32Value{ + Value: uint32(*p.TokensPerFill), + } + } + if p.MaxTokens != nil { + tokenBucket.MaxTokens = uint32(*p.MaxTokens) + } + + if p.FillInterval != nil { + tokenBucket.FillInterval = durationpb.New(time.Duration(*p.FillInterval) * time.Second) + } + + var FilterEnabledDefault *envoy_core_v3.RuntimeFractionalPercent + if p.FilterEnabled != nil { + FilterEnabledDefault = &envoy_core_v3.RuntimeFractionalPercent{ + DefaultValue: &envoy_type_v3.FractionalPercent{ + Numerator: *p.FilterEnabled, + Denominator: envoy_type_v3.FractionalPercent_HUNDRED, + }, + } + } + + var FilterEnforcedDefault *envoy_core_v3.RuntimeFractionalPercent + if p.FilterEnforced != nil { + FilterEnforcedDefault = &envoy_core_v3.RuntimeFractionalPercent{ + DefaultValue: &envoy_type_v3.FractionalPercent{ + Numerator: *p.FilterEnforced, + Denominator: envoy_type_v3.FractionalPercent_HUNDRED, + }, + } + } + + ratelimitHttpFilter, err := makeEnvoyHTTPFilter( + "envoy.filters.http.local_ratelimit", + &envoy_ratelimit.LocalRateLimit{ + TokenBucket: &tokenBucket, + StatPrefix: "local_ratelimit", + FilterEnabled: FilterEnabledDefault, + FilterEnforced: FilterEnforcedDefault, + }, + ) + + if err != nil { + return filter, false, err + } + + changedFilters := make([]*envoy_http_v3.HttpFilter, 0, len(config.HttpFilters)+1) + + // The ratelimitHttpFilter is inserted as the first element of the http + // filter chain. + changedFilters = append(changedFilters, ratelimitHttpFilter) + changedFilters = append(changedFilters, config.HttpFilters...) + config.HttpFilters = changedFilters + + newFilter, err := makeFilter("envoy.filters.network.http_connection_manager", config) + if err != nil { + return filter, false, errors.New("error making new filter") + } + + return newFilter, true, nil +} + +func validateProxyType(t string) error { + if t != "connect-proxy" { + return fmt.Errorf("unexpected ProxyType %q", t) + } + + return nil +} diff --git a/agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go b/agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go new file mode 100644 index 000000000..5c68b1f51 --- /dev/null +++ b/agent/envoyextensions/builtin/http/localratelimit/ratelimit_test.go @@ -0,0 +1,160 @@ +package localratelimit + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/api" + "github.com/hashicorp/consul/envoyextensions/extensioncommon" +) + +func TestConstructor(t *testing.T) { + makeArguments := func(overrides map[string]interface{}) map[string]interface{} { + m := map[string]interface{}{ + "ProxyType": "connect-proxy", + } + + for k, v := range overrides { + m[k] = v + } + + return m + } + + cases := map[string]struct { + extensionName string + arguments map[string]interface{} + expected ratelimit + ok bool + expectedErrMsg string + }{ + "with no arguments": { + arguments: nil, + ok: false, + }, + "with an invalid name": { + arguments: makeArguments(map[string]interface{}{}), + extensionName: "bad", + ok: false, + }, + "MaxToken is missing": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 30, + "TokensPerFill": 5, + }), + expectedErrMsg: "MaxTokens is missing", + ok: false, + }, + "MaxTokens <= 0": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 30, + "TokensPerFill": 5, + "MaxTokens": 0, + }), + expectedErrMsg: "MaxTokens must be greater than 0", + ok: false, + }, + "FillInterval is missing": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "TokensPerFill": 5, + "MaxTokens": 10, + }), + expectedErrMsg: "FillInterval(in second) is missing", + ok: false, + }, + "FillInterval <= 0": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 0, + "TokensPerFill": 5, + "MaxTokens": 10, + }), + expectedErrMsg: "FillInterval(in second) must be greater than 0", + ok: false, + }, + "TokensPerFill <= 0": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 30, + "TokensPerFill": 0, + "MaxTokens": 10, + }), + expectedErrMsg: "TokensPerFill must be greater than 0", + ok: false, + }, + "FilterEnabled < 0": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 30, + "TokensPerFill": 5, + "MaxTokens": 10, + "FilterEnabled": -1, + }), + expectedErrMsg: "cannot parse 'FilterEnabled', -1 overflows uint", + ok: false, + }, + "FilterEnforced < 0": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 30, + "TokensPerFill": 5, + "MaxTokens": 10, + "FilterEnforced": -1, + }), + expectedErrMsg: "cannot parse 'FilterEnforced', -1 overflows uint", + ok: false, + }, + "valid everything": { + arguments: makeArguments(map[string]interface{}{ + "ProxyType": "connect-proxy", + "FillInterval": 30, + "MaxTokens": 20, + "TokensPerFill": 5, + }), + expected: ratelimit{ + ProxyType: "connect-proxy", + MaxTokens: intPointer(20), + FillInterval: intPointer(30), + TokensPerFill: intPointer(5), + }, + ok: true, + }, + } + + for n, tc := range cases { + t.Run(n, func(t *testing.T) { + + extensionName := api.BuiltinLocalRatelimitExtension + if tc.extensionName != "" { + extensionName = tc.extensionName + } + + svc := api.CompoundServiceName{Name: "svc"} + ext := extensioncommon.RuntimeConfig{ + ServiceName: svc, + EnvoyExtension: api.EnvoyExtension{ + Name: extensionName, + Arguments: tc.arguments, + }, + } + + e, err := Constructor(ext.EnvoyExtension) + + if tc.ok { + require.NoError(t, err) + require.Equal(t, &extensioncommon.BasicEnvoyExtender{Extension: &tc.expected}, e) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrMsg) + } + }) + } +} + +func intPointer(i int) *int { + return &i +} diff --git a/agent/envoyextensions/registered_extensions.go b/agent/envoyextensions/registered_extensions.go index c765df7c8..fed8d3c59 100644 --- a/agent/envoyextensions/registered_extensions.go +++ b/agent/envoyextensions/registered_extensions.go @@ -4,6 +4,7 @@ import ( "fmt" 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/api" "github.com/hashicorp/consul/envoyextensions/extensioncommon" @@ -13,8 +14,9 @@ import ( type extensionConstructor func(api.EnvoyExtension) (extensioncommon.EnvoyExtender, error) var extensionConstructors = map[string]extensionConstructor{ - api.BuiltinLuaExtension: lua.Constructor, - api.BuiltinAWSLambdaExtension: awslambda.Constructor, + api.BuiltinLuaExtension: lua.Constructor, + api.BuiltinAWSLambdaExtension: awslambda.Constructor, + api.BuiltinLocalRatelimitExtension: localratelimit.Constructor, } // ConstructExtension attempts to lookup and build an extension from the registry with the diff --git a/agent/xds/delta_envoy_extender_oss_test.go b/agent/xds/delta_envoy_extender_oss_test.go index 97a610fd4..f6791ba65 100644 --- a/agent/xds/delta_envoy_extender_oss_test.go +++ b/agent/xds/delta_envoy_extender_oss_test.go @@ -208,6 +208,27 @@ end`, return proxycfg.TestConfigSnapshotDiscoveryChain(t, "default", nsFunc, nil, makeLambdaServiceDefaults(true)) }, }, + { + name: "http-local-ratelimit-applyto-filter", + 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.BuiltinLocalRatelimitExtension, + Arguments: map[string]interface{}{ + "ProxyType": "connect-proxy", + "MaxTokens": 3, + "TokensPerFill": 2, + "FillInterval": 10, + "FilterEnabled": 100, + "FilterEnforced": 100, + }, + }, + } + }, nil) + }, + }, } latestEnvoyVersion := xdscommon.EnvoyVersions[0] diff --git a/agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden new file mode 100644 index 000000000..6f67c341d --- /dev/null +++ b/agent/xds/testdata/builtin_extension/clusters/http-local-ratelimit-applyto-filter.latest.golden @@ -0,0 +1,127 @@ +{ + "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" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden new file mode 100644 index 000000000..e8e6b94a1 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/endpoints/http-local-ratelimit-applyto-filter.latest.golden @@ -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" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden new file mode 100644 index 000000000..29c336296 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/listeners/http-local-ratelimit-applyto-filter.latest.golden @@ -0,0 +1,256 @@ +{ + "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.local_ratelimit", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit", + "statPrefix": "local_ratelimit", + "tokenBucket": { + "maxTokens": 3, + "tokensPerFill": 2, + "fillInterval": "10s" + }, + "filterEnabled": { + "defaultValue": { + "numerator": 100 + } + }, + "filterEnforced": { + "defaultValue": { + "numerator": 100 + } + } + } + }, + { + "name": "envoy.filters.http.rbac", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC", + "rules": {} + } + }, + { + "name": "envoy.filters.http.header_to_metadata", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.header_to_metadata.v3.Config", + "requestRules": [ + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "trust-domain", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\1" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "partition", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\2" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "namespace", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\3" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "datacenter", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\4" + } + } + }, + { + "header": "x-forwarded-client-cert", + "onHeaderPresent": { + "metadataNamespace": "consul", + "key": "service", + "regexValueRewrite": { + "pattern": { + "googleRe2": {}, + "regex": ".*URI=spiffe://([^/]+.[^/]+)(?:/ap/([^/]+))?/ns/([^/]+)/dc/([^/]+)/svc/([^/;,]+).*" + }, + "substitution": "\\5" + } + } + } + ] + } + }, + { + "name": "envoy.filters.http.router", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router" + } + } + ], + "tracing": { + "randomSampling": {} + }, + "forwardClientCertDetails": "APPEND_FORWARD", + "setCurrentClientCertDetails": { + "subject": true, + "cert": true, + "chain": true, + "dns": true, + "uri": true + } + } + } + ], + "transportSocket": { + "name": "tls", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext", + "commonTlsContext": { + "tlsParams": {}, + "tlsCertificates": [ + { + "certificateChain": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICjDCCAjKgAwIBAgIIC5llxGV1gB8wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowDjEMMAoG\nA1UEAxMDd2ViMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEADPv1RHVNRfa2VKR\nAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Favq5E0ivpNtv1QnFhxtPd7d5k4e+T7\nSkW1TaOCAXIwggFuMA4GA1UdDwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcD\nAgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADBoBgNVHQ4EYQRfN2Q6MDc6ODc6M2E6\nNDA6MTk6NDc6YzM6NWE6YzA6YmE6NjI6ZGY6YWY6NGI6ZDQ6MDU6MjU6NzY6M2Q6\nNWE6OGQ6MTY6OGQ6Njc6NWU6MmU6YTA6MzQ6N2Q6ZGM6ZmYwagYDVR0jBGMwYYBf\nZDE6MTE6MTE6YWM6MmE6YmE6OTc6YjI6M2Y6YWM6N2I6YmQ6ZGE6YmU6YjE6OGE6\nZmM6OWE6YmE6YjU6YmM6ODM6ZTc6NWU6NDE6NmY6ZjI6NzM6OTU6NTg6MGM6ZGIw\nWQYDVR0RBFIwUIZOc3BpZmZlOi8vMTExMTExMTEtMjIyMi0zMzMzLTQ0NDQtNTU1\nNTU1NTU1NTU1LmNvbnN1bC9ucy9kZWZhdWx0L2RjL2RjMS9zdmMvd2ViMAoGCCqG\nSM49BAMCA0gAMEUCIGC3TTvvjj76KMrguVyFf4tjOqaSCRie3nmHMRNNRav7AiEA\npY0heYeK9A6iOLrzqxSerkXXQyj5e9bE4VgUnxgPU6g=\n-----END CERTIFICATE-----\n" + }, + "privateKey": { + "inlineString": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIMoTkpRggp3fqZzFKh82yS4LjtJI+XY+qX/7DefHFrtdoAoGCCqGSM49\nAwEHoUQDQgAEADPv1RHVNRfa2VKRAB16b6rZnEt7tuhaxCFpQXPj7M2omb0B9Fav\nq5E0ivpNtv1QnFhxtPd7d5k4e+T7SkW1TQ==\n-----END EC PRIVATE KEY-----\n" + } + } + ], + "validationContext": { + "trustedCa": { + "inlineString": "-----BEGIN CERTIFICATE-----\nMIICXDCCAgKgAwIBAgIICpZq70Z9LyUwCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ\nVGVzdCBDQSAyMB4XDTE5MDMyMjEzNTgyNloXDTI5MDMyMjEzNTgyNlowFDESMBAG\nA1UEAxMJVGVzdCBDQSAyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIhywH1gx\nAsMwuF3ukAI5YL2jFxH6Usnma1HFSfVyxbXX1/uoZEYrj8yCAtdU2yoHETyd+Zx2\nThhRLP79pYegCaOCATwwggE4MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTAD\nAQH/MGgGA1UdDgRhBF9kMToxMToxMTphYzoyYTpiYTo5NzpiMjozZjphYzo3Yjpi\nZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1ZTo0MTo2ZjpmMjo3\nMzo5NTo1ODowYzpkYjBqBgNVHSMEYzBhgF9kMToxMToxMTphYzoyYTpiYTo5Nzpi\nMjozZjphYzo3YjpiZDpkYTpiZTpiMTo4YTpmYzo5YTpiYTpiNTpiYzo4MzplNzo1\nZTo0MTo2ZjpmMjo3Mzo5NTo1ODowYzpkYjA/BgNVHREEODA2hjRzcGlmZmU6Ly8x\nMTExMTExMS0yMjIyLTMzMzMtNDQ0NC01NTU1NTU1NTU1NTUuY29uc3VsMAoGCCqG\nSM49BAMCA0gAMEUCICOY0i246rQHJt8o8Oya0D5PLL1FnmsQmQqIGCi31RwnAiEA\noR5f6Ku+cig2Il8T8LJujOp2/2A72QcHZA57B13y+8o=\n-----END CERTIFICATE-----\n" + } + }, + "alpnProtocols": [ + "http/1.1" + ] + }, + "requireClientCertificate": true + } + } + } + ], + "trafficDirection": "INBOUND" + } + ], + "typeUrl": "type.googleapis.com/envoy.config.listener.v3.Listener", + "nonce": "00000001" +} \ No newline at end of file diff --git a/agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden b/agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden new file mode 100644 index 000000000..d08109381 --- /dev/null +++ b/agent/xds/testdata/builtin_extension/routes/http-local-ratelimit-applyto-filter.latest.golden @@ -0,0 +1,5 @@ +{ + "versionInfo": "00000001", + "typeUrl": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration", + "nonce": "00000001" +} \ No newline at end of file diff --git a/api/config_entry.go b/api/config_entry.go index 4e9682ee6..39b7727c8 100644 --- a/api/config_entry.go +++ b/api/config_entry.go @@ -33,8 +33,9 @@ const ( ) const ( - BuiltinAWSLambdaExtension string = "builtin/aws/lambda" - BuiltinLuaExtension string = "builtin/lua" + BuiltinAWSLambdaExtension string = "builtin/aws/lambda" + BuiltinLuaExtension string = "builtin/lua" + BuiltinLocalRatelimitExtension string = "builtin/http/localratelimit" ) type ConfigEntry interface { diff --git a/go.mod b/go.mod index cc10da76d..f9a563a57 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/fsnotify/fsnotify v1.5.1 github.com/go-openapi/runtime v0.24.1 github.com/go-openapi/strfmt v0.21.3 + github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.8 github.com/google/gofuzz v1.2.0 github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 @@ -151,7 +152,6 @@ require ( github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect - github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.0 // indirect github.com/google/go-querystring v1.0.0 // indirect diff --git a/test/integration/connect/envoy/case-envoyext-ratelimit/capture.sh b/test/integration/connect/envoy/case-envoyext-ratelimit/capture.sh new file mode 100644 index 000000000..1a11f7d5e --- /dev/null +++ b/test/integration/connect/envoy/case-envoyext-ratelimit/capture.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +snapshot_envoy_admin localhost:19000 s1 primary || true +snapshot_envoy_admin localhost:19001 s2 primary || true diff --git a/test/integration/connect/envoy/case-envoyext-ratelimit/service_s1.hcl b/test/integration/connect/envoy/case-envoyext-ratelimit/service_s1.hcl new file mode 100644 index 000000000..0d8957c00 --- /dev/null +++ b/test/integration/connect/envoy/case-envoyext-ratelimit/service_s1.hcl @@ -0,0 +1,16 @@ +services { + name = "s1" + port = 8080 + connect { + sidecar_service { + proxy { + upstreams = [ + { + destination_name = "s2" + local_bind_port = 5000 + } + ] + } + } + } +} diff --git a/test/integration/connect/envoy/case-envoyext-ratelimit/service_s2.hcl b/test/integration/connect/envoy/case-envoyext-ratelimit/service_s2.hcl new file mode 100644 index 000000000..9c23e79c7 --- /dev/null +++ b/test/integration/connect/envoy/case-envoyext-ratelimit/service_s2.hcl @@ -0,0 +1,5 @@ +services { + name = "s2" + port = 8181 + connect { sidecar_service {} } +} diff --git a/test/integration/connect/envoy/case-envoyext-ratelimit/setup.sh b/test/integration/connect/envoy/case-envoyext-ratelimit/setup.sh new file mode 100644 index 000000000..c49c39d60 --- /dev/null +++ b/test/integration/connect/envoy/case-envoyext-ratelimit/setup.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -eEuo pipefail + +upsert_config_entry primary ' +Kind = "service-defaults" +Name = "s2" +Protocol = "http" +EnvoyExtensions = [ + { + Name = "builtin/http/localratelimit", + Arguments = { + ProxyType = "connect-proxy" + MaxTokens = 1, + TokensPerFill = 1, + FillInterval = 120, + FilterEnabled = 100, + FilterEnforced = 100, + } + } +] +' + +upsert_config_entry primary ' +Kind = "service-defaults" +Name = "s1" +Protocol = "tcp" +EnvoyExtensions = [ + { + Name = "builtin/http/localratelimit", + Arguments = { + ProxyType = "connect-proxy" + MaxTokens = 1, + TokensPerFill = 1, + FillInterval = 120, + FilterEnabled = 100, + FilterEnforced = 100, + } + } +] +' + +register_services primary + +gen_envoy_bootstrap s1 19000 primary +gen_envoy_bootstrap s2 19001 primary diff --git a/test/integration/connect/envoy/case-envoyext-ratelimit/vars.sh b/test/integration/connect/envoy/case-envoyext-ratelimit/vars.sh new file mode 100644 index 000000000..433e50c1b --- /dev/null +++ b/test/integration/connect/envoy/case-envoyext-ratelimit/vars.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export REQUIRED_SERVICES="s1 s1-sidecar-proxy s2 s2-sidecar-proxy" diff --git a/test/integration/connect/envoy/case-envoyext-ratelimit/verify.bats b/test/integration/connect/envoy/case-envoyext-ratelimit/verify.bats new file mode 100644 index 000000000..b8303348e --- /dev/null +++ b/test/integration/connect/envoy/case-envoyext-ratelimit/verify.bats @@ -0,0 +1,57 @@ +#!/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 listener should be up and have right cert" { + assert_proxy_presents_cert_uri localhost:21001 s2 +} + +@test "s2 proxy should be healthy" { + assert_service_has_healthy_instances s2 1 +} + +@test "s1 upstream should have healthy endpoints for s2" { + assert_upstream_has_endpoints_in_status 127.0.0.1:19000 s2.default.primary HEALTHY 1 +} + +@test "s2 proxy should have been configured with http local ratelimit filters" { + HTTP_FILTERS=$(get_envoy_http_filters localhost:19001) + PUB=$(echo "$HTTP_FILTERS" | grep -E "^public_listener:" | cut -f 2 -d ' ') + + echo "HTTP_FILTERS = $HTTP_FILTERS" + echo "PUB = $PUB" + + [ "$PUB" = "envoy.filters.http.local_ratelimit,envoy.filters.http.rbac,envoy.filters.http.header_to_metadata,envoy.filters.http.router" ] +} + +@test "s1(tcp) proxy should not be changed by http/localratelimit extension" { + TCP_FILTERS=$(get_envoy_listener_filters localhost:19000) + PUB=$(echo "$TCP_FILTERS" | grep -E "^public_listener:" | cut -f 2 -d ' ') + + echo "TCP_FILTERS = $TCP_FILTERS" + echo "PUB = $PUB" + + [ "$PUB" = "envoy.filters.network.rbac,envoy.filters.network.tcp_proxy" ] +} + +@test "first connection to s2 - success" { + run retry_default curl -s -f -d hello localhost:5000 + [ "$status" -eq 0 ] + [[ "$output" == *"hello"* ]] +} + +@test "ratelimit to s2 is in effect - return code 429" { + retry_default must_fail_http_connection localhost:5000 429 +}