diff --git a/CODEOWNERS b/CODEOWNERS index f3fff86f24ee5..20097a403f082 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -92,3 +92,4 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/network/echo @htuch @alyssawilk /*/extensions/filters/udp/udp_proxy @mattklein123 @danzh2010 /*/extensions/clusters/aggregate @yxue @snowp +/*/extensions/filters/network/local_ratelimit @mattklein123 @junr03 diff --git a/api/BUILD b/api/BUILD index b9d24fc7dbbfc..1b4044aa47958 100644 --- a/api/BUILD +++ b/api/BUILD @@ -84,6 +84,8 @@ proto_library( "//envoy/config/filter/network/ext_authz/v3alpha:pkg", "//envoy/config/filter/network/http_connection_manager/v2:pkg", "//envoy/config/filter/network/http_connection_manager/v3alpha:pkg", + "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", + "//envoy/config/filter/network/local_rate_limit/v3alpha:pkg", "//envoy/config/filter/network/mongo_proxy/v2:pkg", "//envoy/config/filter/network/mongo_proxy/v3alpha:pkg", "//envoy/config/filter/network/mysql_proxy/v1alpha1:pkg", diff --git a/api/docs/BUILD b/api/docs/BUILD index f92d12d133bd3..024c0760ebf80 100644 --- a/api/docs/BUILD +++ b/api/docs/BUILD @@ -59,6 +59,7 @@ proto_library( "//envoy/config/filter/network/dubbo_proxy/v2alpha1:pkg", "//envoy/config/filter/network/ext_authz/v2:pkg", "//envoy/config/filter/network/http_connection_manager/v2:pkg", + "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", "//envoy/config/filter/network/mongo_proxy/v2:pkg", "//envoy/config/filter/network/mysql_proxy/v1alpha1:pkg", "//envoy/config/filter/network/rate_limit/v2:pkg", diff --git a/api/envoy/api/v2/route/route.proto b/api/envoy/api/v2/route/route.proto index b1435f1782bc9..a3f52ab178437 100644 --- a/api/envoy/api/v2/route/route.proto +++ b/api/envoy/api/v2/route/route.proto @@ -1178,7 +1178,7 @@ message VirtualCluster { core.RequestMethod method = 3 [deprecated = true]; } -// Global rate limiting :ref:`architecture overview `. +// Global rate limiting :ref:`architecture overview `. message RateLimit { // [#next-free-field: 7] message Action { diff --git a/api/envoy/api/v3alpha/route/route.proto b/api/envoy/api/v3alpha/route/route.proto index f2b5d3c60251d..a3ac573d1809f 100644 --- a/api/envoy/api/v3alpha/route/route.proto +++ b/api/envoy/api/v3alpha/route/route.proto @@ -1167,7 +1167,7 @@ message VirtualCluster { string name = 2 [(validate.rules).string = {min_bytes: 1}]; } -// Global rate limiting :ref:`architecture overview `. +// Global rate limiting :ref:`architecture overview `. message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RateLimit"; diff --git a/api/envoy/config/filter/network/local_rate_limit/v2alpha/BUILD b/api/envoy/config/filter/network/local_rate_limit/v2alpha/BUILD new file mode 100644 index 0000000000000..5a866264352a8 --- /dev/null +++ b/api/envoy/config/filter/network/local_rate_limit/v2alpha/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/api/v2/core:pkg", + "//envoy/type:pkg", + ], +) diff --git a/api/envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.proto b/api/envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.proto new file mode 100644 index 0000000000000..39360de70a286 --- /dev/null +++ b/api/envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package envoy.config.filter.network.local_rate_limit.v2alpha; + +import "envoy/api/v2/core/base.proto"; +import "envoy/type/token_bucket.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.local_rate_limit.v2alpha"; +option java_outer_classname = "LocalRateLimitProto"; +option java_multiple_files = true; + +// [#protodoc-title: Local rate limit] +// Local rate limit :ref:`configuration overview `. +// [#extension: envoy.filters.network.local_ratelimit] + +message LocalRateLimit { + // The prefix to use when emitting :ref:`statistics + // `. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // The token bucket configuration to use for rate limiting connections that are processed by the + // filter's filter chain. Each incoming connection processed by the filter consumes a single + // token. If the token is available, the connection will be allowed. If no tokens are available, + // the connection will be immediately closed. + // + // .. note:: + // In the current implementation each filter and filter chain has an independent rate limit. + // + // .. note:: + // In the current implementation the token bucket's :ref:`fill_interval + // ` must be >= 50ms to avoid too aggressive + // refills. + type.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; + + // Runtime flag that controls whether the filter is enabled or not. If not specified, defaults + // to enabled. + api.v2.core.RuntimeFeatureFlag runtime_enabled = 3; +} diff --git a/api/envoy/config/filter/network/local_rate_limit/v3alpha/BUILD b/api/envoy/config/filter/network/local_rate_limit/v3alpha/BUILD new file mode 100644 index 0000000000000..ed63cfa839196 --- /dev/null +++ b/api/envoy/config/filter/network/local_rate_limit/v3alpha/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/api/v3alpha/core:pkg", + "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", + "//envoy/type/v3alpha:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/config/filter/network/local_rate_limit/v3alpha/local_rate_limit.proto b/api/envoy/config/filter/network/local_rate_limit/v3alpha/local_rate_limit.proto new file mode 100644 index 0000000000000..31852ba798ab8 --- /dev/null +++ b/api/envoy/config/filter/network/local_rate_limit/v3alpha/local_rate_limit.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package envoy.config.filter.network.local_rate_limit.v3alpha; + +import "envoy/api/v3alpha/core/base.proto"; +import "envoy/type/v3alpha/token_bucket.proto"; + +import "udpa/annotations/versioning.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.local_rate_limit.v3alpha"; +option java_outer_classname = "LocalRateLimitProto"; +option java_multiple_files = true; + +// [#protodoc-title: Local rate limit] +// Local rate limit :ref:`configuration overview `. +// [#extension: envoy.filters.network.local_ratelimit] + +message LocalRateLimit { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.local_rate_limit.v2alpha.LocalRateLimit"; + + // The prefix to use when emitting :ref:`statistics + // `. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // The token bucket configuration to use for rate limiting connections that are processed by the + // filter's filter chain. Each incoming connection processed by the filter consumes a single + // token. If the token is available, the connection will be allowed. If no tokens are available, + // the connection will be immediately closed. + // + // .. note:: + // In the current implementation each filter and filter chain has an independent rate limit. + // + // .. note:: + // In the current implementation the token bucket's :ref:`fill_interval + // ` must be >= 50ms to avoid too + // aggressive refills. + type.v3alpha.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; + + // Runtime flag that controls whether the filter is enabled or not. If not specified, defaults + // to enabled. + api.v3alpha.core.RuntimeFeatureFlag runtime_enabled = 3; +} diff --git a/api/envoy/type/token_bucket.proto b/api/envoy/type/token_bucket.proto new file mode 100644 index 0000000000000..b293b76be192e --- /dev/null +++ b/api/envoy/type/token_bucket.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package envoy.type; + +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type"; +option java_outer_classname = "TokenBucketProto"; +option java_multiple_files = true; + +// [#protodoc-title: Token bucket] + +// Configures a token bucket, typically used for rate limiting. +message TokenBucket { + // The maximum tokens that the bucket can hold. This is also the number of tokens that the bucket + // initially contains. + uint32 max_tokens = 1 [(validate.rules).uint32 = {gt: 0}]; + + // The number of tokens added to the bucket during each fill interval. If not specified, defaults + // to a single token. + google.protobuf.UInt32Value tokens_per_fill = 2 [(validate.rules).uint32 = {gt: 0}]; + + // The fill interval that tokens are added to the bucket. During each fill interval + // `tokens_per_fill` are added to the bucket. The bucket will never contain more than + // `max_tokens` tokens. + google.protobuf.Duration fill_interval = 3 [(validate.rules).duration = { + required: true + gt {} + }]; +} diff --git a/api/envoy/type/v3alpha/token_bucket.proto b/api/envoy/type/v3alpha/token_bucket.proto new file mode 100644 index 0000000000000..bd5479827c998 --- /dev/null +++ b/api/envoy/type/v3alpha/token_bucket.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package envoy.type.v3alpha; + +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/versioning.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.v3alpha"; +option java_outer_classname = "TokenBucketProto"; +option java_multiple_files = true; + +// [#protodoc-title: Token bucket] + +// Configures a token bucket, typically used for rate limiting. +message TokenBucket { + option (udpa.annotations.versioning).previous_message_type = "envoy.type.TokenBucket"; + + // The maximum tokens that the bucket can hold. This is also the number of tokens that the bucket + // initially contains. + uint32 max_tokens = 1 [(validate.rules).uint32 = {gt: 0}]; + + // The number of tokens added to the bucket during each fill interval. If not specified, defaults + // to a single token. + google.protobuf.UInt32Value tokens_per_fill = 2 [(validate.rules).uint32 = {gt: 0}]; + + // The fill interval that tokens are added to the bucket. During each fill interval + // `tokens_per_fill` are added to the bucket. The bucket will never contain more than + // `max_tokens` tokens. + google.protobuf.Duration fill_interval = 3 [(validate.rules).duration = { + required: true + gt {} + }]; +} diff --git a/docs/root/api-v2/config/filter/network/network.rst b/docs/root/api-v2/config/filter/network/network.rst index 706f81eccf9d8..40a28a6cf87c3 100644 --- a/docs/root/api-v2/config/filter/network/network.rst +++ b/docs/root/api-v2/config/filter/network/network.rst @@ -8,4 +8,5 @@ Network filters */empty/* */v1alpha1/* */v2/* + */v2alpha/* */v2alpha1/* diff --git a/docs/root/api-v2/types/types.rst b/docs/root/api-v2/types/types.rst index c8a39c59b55d8..2b9bf3210a7e7 100644 --- a/docs/root/api-v2/types/types.rst +++ b/docs/root/api-v2/types/types.rst @@ -10,6 +10,7 @@ Types ../type/http_status.proto ../type/percent.proto ../type/range.proto + ../type/token_bucket.proto ../type/matcher/metadata.proto ../type/matcher/number.proto ../type/matcher/regex.proto diff --git a/docs/root/configuration/http/http_filters/rate_limit_filter.rst b/docs/root/configuration/http/http_filters/rate_limit_filter.rst index e40d817edcf1e..e4596d3992718 100644 --- a/docs/root/configuration/http/http_filters/rate_limit_filter.rst +++ b/docs/root/configuration/http/http_filters/rate_limit_filter.rst @@ -3,7 +3,7 @@ Rate limit ========== -* Global rate limiting :ref:`architecture overview ` +* Global rate limiting :ref:`architecture overview ` * :ref:`v2 API reference ` * This filter should be configured with the name *envoy.rate_limit*. diff --git a/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst b/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst new file mode 100644 index 0000000000000..5939a63ae7d12 --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/local_rate_limit_filter.rst @@ -0,0 +1,46 @@ +.. _config_network_filters_local_rate_limit: + +Local rate limit +================ + +* Local rate limiting :ref:`architecture overview ` +* :ref:`v2 API reference + ` +* This filter should be configured with the name *envoy.filters.network.local_ratelimit*. + +.. note:: + Global rate limiting is also supported via the :ref:`global rate limit filter + `. + +Overview +-------- + +The local rate limit filter applies a :ref:`token bucket +` rate +limit to incoming connections that are processed by the filter's filter chain. Each connection +processed by the filter utilizes a single token, and if no tokens are available, the connection will +be immediately closed without further filter iteration. + +.. note:: + In the current implementation each filter and filter chain has an independent rate limit. + +.. _config_network_filters_local_rate_limit_stats: + +Statistics +---------- + +Every configured local rate limit filter has statistics rooted at *local_ratelimit..* +with the following statistics: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + rate_limited, Counter, Total connections that have been closed due to rate limit exceeded + +Runtime +------- + +The local rate limit filter can be runtime feature flagged via the :ref:`enabled +` +configuration field. diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index f43f474ac6547..b7785bbe1e8df 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -14,6 +14,7 @@ filters. client_ssl_auth_filter echo_filter ext_authz_filter + local_rate_limit_filter mongo_proxy_filter mysql_proxy_filter rate_limit_filter diff --git a/docs/root/configuration/listeners/network_filters/rate_limit_filter.rst b/docs/root/configuration/listeners/network_filters/rate_limit_filter.rst index 71b445ae21d40..09a7aa2801456 100644 --- a/docs/root/configuration/listeners/network_filters/rate_limit_filter.rst +++ b/docs/root/configuration/listeners/network_filters/rate_limit_filter.rst @@ -3,10 +3,14 @@ Rate limit ========== -* Global rate limiting :ref:`architecture overview ` +* Global rate limiting :ref:`architecture overview ` * :ref:`v2 API reference ` * This filter should be configured with the name *envoy.ratelimit*. +.. note:: + Local rate limiting is also supported via the :ref:`local rate limit filter + `. + .. _config_network_filters_rate_limit_stats: Statistics diff --git a/docs/root/configuration/other_features/rate_limit.rst b/docs/root/configuration/other_features/rate_limit.rst index 571c11b7df891..a4c456257a2ad 100644 --- a/docs/root/configuration/other_features/rate_limit.rst +++ b/docs/root/configuration/other_features/rate_limit.rst @@ -3,7 +3,7 @@ Rate limit service ================== -The :ref:`rate limit service ` configuration specifies the global rate +The :ref:`rate limit service ` configuration specifies the global rate limit service Envoy should talk to when it needs to make global rate limit decisions. If no rate limit service is configured, a "null" service will be used which will always return OK if called. diff --git a/docs/root/configuration/other_protocols/thrift_filters/rate_limit_filter.rst b/docs/root/configuration/other_protocols/thrift_filters/rate_limit_filter.rst index 95ee09bf53ea5..4fa27e08febd1 100644 --- a/docs/root/configuration/other_protocols/thrift_filters/rate_limit_filter.rst +++ b/docs/root/configuration/other_protocols/thrift_filters/rate_limit_filter.rst @@ -3,7 +3,7 @@ Rate limit ========== -* Global rate limiting :ref:`architecture overview ` +* Global rate limiting :ref:`architecture overview ` * :ref:`v2 API reference ` * This filter should be configured with the name *envoy.filters.thrift.rate_limit*. diff --git a/docs/root/faq/load_balancing/transient_failures.rst b/docs/root/faq/load_balancing/transient_failures.rst index 2e31976b5eb4a..109a630eb0dc3 100644 --- a/docs/root/faq/load_balancing/transient_failures.rst +++ b/docs/root/faq/load_balancing/transient_failures.rst @@ -38,7 +38,7 @@ The following application status codes in gRPC are considered safe for automatic * *CANCELLED* - Return this code if there is an error that can be retried in the service. * *RESOURCE_EXHAUSTED* - Return this code if some of the resources that service depends on are exhausted in that instance so that retrying - to another instance would help. Please note that for shared resource exhaustion, returning this will not help. Instead :ref:`rate limiting ` + to another instance would help. Please note that for shared resource exhaustion, returning this will not help. Instead :ref:`rate limiting ` should be used to handle such cases. The HTTP Status codes *502 (Bad Gateway)*, *503 (Service Unavailable)* and *504 (Gateway Timeout)* are all mapped to gRPC status code *UNAVAILABLE*. diff --git a/docs/root/install/ref_configs.rst b/docs/root/install/ref_configs.rst index bc14ae225e242..c4c5fad8f04af 100644 --- a/docs/root/install/ref_configs.rst +++ b/docs/root/install/ref_configs.rst @@ -50,7 +50,7 @@ A few notes about the example configurations: disable this or enable `Zipkin `_ or `Datadog `_ tracing, delete or change the :ref:`tracing configuration ` accordingly. * The configuration demonstrates the use of a :ref:`global rate limiting service - `. To disable this delete the :ref:`rate limit configuration + `. To disable this delete the :ref:`rate limit configuration `. * :ref:`Route discovery service ` is configured for the service to service reference configuration and it is assumed to be running at `rds.yourcompany.net`. diff --git a/docs/root/intro/arch_overview/listeners/listener_filters.rst b/docs/root/intro/arch_overview/listeners/listener_filters.rst index 74635afa3eebf..f8fbace9f08b5 100644 --- a/docs/root/intro/arch_overview/listeners/listener_filters.rst +++ b/docs/root/intro/arch_overview/listeners/listener_filters.rst @@ -11,6 +11,6 @@ and also make interaction between multiple such features more explicit. The API for listener filters is relatively simple since ultimately these filters operate on newly accepted sockets. Filters in the chain can stop and subsequently continue iteration to further filters. This allows for more complex scenarios such as calling a :ref:`rate limiting -service `, etc. Envoy already includes several listener filters that +service `, etc. Envoy already includes several listener filters that are documented in this architecture overview as well as the :ref:`configuration reference `. diff --git a/docs/root/intro/arch_overview/listeners/listeners.rst b/docs/root/intro/arch_overview/listeners/listeners.rst index 01dca7c998e4f..a6e201fdb1168 100644 --- a/docs/root/intro/arch_overview/listeners/listeners.rst +++ b/docs/root/intro/arch_overview/listeners/listeners.rst @@ -18,7 +18,7 @@ composed of one or more network level (L3/L4) :ref:`filters `, :ref:`TLS client +Envoy is used for (e.g., :ref:`rate limiting `, :ref:`TLS client authentication `, :ref:`HTTP connection management `, MongoDB :ref:`sniffing `, raw :ref:`TCP proxy `, etc.). diff --git a/docs/root/intro/arch_overview/listeners/network_filters.rst b/docs/root/intro/arch_overview/listeners/network_filters.rst index d370199c5d22c..b1f5e8ef48b61 100644 --- a/docs/root/intro/arch_overview/listeners/network_filters.rst +++ b/docs/root/intro/arch_overview/listeners/network_filters.rst @@ -17,7 +17,7 @@ The API for network level filters is relatively simple since ultimately the filt bytes and a small number of connection events (e.g., TLS handshake complete, connection disconnected locally or remotely, etc.). Filters in the chain can stop and subsequently continue iteration to further filters. This allows for more complex scenarios such as calling a :ref:`rate limiting -service `, etc. Network level filters can also share state (static and +service `, etc. Network level filters can also share state (static and dynamic) among themselves within the context of a single downstream connection. Refer to :ref:`data sharing between filters ` for more details. Envoy already includes several network level filters that are documented in this architecture diff --git a/docs/root/intro/arch_overview/other_features/global_rate_limiting.rst b/docs/root/intro/arch_overview/other_features/global_rate_limiting.rst index b15ef05c1414e..e8dbfbc2a2bbb 100644 --- a/docs/root/intro/arch_overview/other_features/global_rate_limiting.rst +++ b/docs/root/intro/arch_overview/other_features/global_rate_limiting.rst @@ -1,4 +1,4 @@ -.. _arch_overview_rate_limit: +.. _arch_overview_global_rate_limit: Global rate limiting ==================== @@ -29,3 +29,10 @@ written in Go which uses a Redis backend. Envoy’s rate limit integration has t :ref:`Configuration reference ` Rate limit service :ref:`configuration `. + +Note that Envoy also supports :ref:`local rate limiting `. +Local rate limiting can be used in conjunction with global rate limiting to reduce load on the +global rate limit service. For example, a local token bucket rate limit can absorb very large bursts +in load that might otherwise overwhelm a global rate limit service. Thus, the rate limit is applied +in two stages. The initial coarse grained limiting is performed by the token bucket limit before +a fine grained global limit finishes the job. diff --git a/docs/root/intro/arch_overview/other_features/local_rate_limiting.rst b/docs/root/intro/arch_overview/other_features/local_rate_limiting.rst new file mode 100644 index 0000000000000..0fc3369db24f8 --- /dev/null +++ b/docs/root/intro/arch_overview/other_features/local_rate_limiting.rst @@ -0,0 +1,11 @@ +.. _arch_overview_local_rate_limit: + +Local rate limiting +=================== + +Envoy supports local (non-distributed) rate limiting of L4 connections via the +:ref:`local rate limit filter `. + +Note that Envoy also supports :ref:`global rate limiting `. Local +rate limiting can be used in conjunction with global rate limiting to reduce load on the global +rate limit service. diff --git a/docs/root/intro/arch_overview/other_features/other_features.rst b/docs/root/intro/arch_overview/other_features/other_features.rst index 6b8dc5f037363..168e40f92a701 100644 --- a/docs/root/intro/arch_overview/other_features/other_features.rst +++ b/docs/root/intro/arch_overview/other_features/other_features.rst @@ -4,6 +4,7 @@ Other features .. toctree:: :maxdepth: 2 + local_rate_limiting global_rate_limiting scripting ip_transparency diff --git a/docs/root/intro/version_history.rst b/docs/root/intro/version_history.rst index 3bc532bbea291..d246a0ffcf28b 100644 --- a/docs/root/intro/version_history.rst +++ b/docs/root/intro/version_history.rst @@ -11,7 +11,7 @@ Version history * api: added ability to specify `mode` for :ref:`Pipe `. * buffer: remove old implementation * build: official released binary is now built against libc++. -* cluster: added :ref: `aggregate cluster ` that allows load balancing between clusters. +* cluster: added :ref:`aggregate cluster ` that allows load balancing between clusters. * decompressor: remove decompressor hard assert failure and replace with an error flag. * ext_authz: added :ref:`configurable ability` to send the :ref:`certificate` to the `ext_authz` service. * health check: gRPC health checker sets the gRPC deadline to the configured timeout duration. @@ -19,11 +19,12 @@ Version history * http: added the ability to sanitize headers nominated by the Connection header. This new behavior is guarded by envoy.reloadable_features.connection_header_sanitization which defaults to true. * http: blocks unsupported transfer-encodings. Can be reverted temporarily by setting runtime feature `envoy.reloadable_features.reject_unsupported_transfer_encodings` to false. * http: support :ref:`auto_host_rewrite_header` in the dynamic forward proxy. -* jwt_authn: added :ref: `allow_missing` option that accepts request without token but rejects bad request with bad tokens. +* jwt_authn: added :ref:`allow_missing` option that accepts request without token but rejects bad request with bad tokens. * jwt_authn: added :ref:`bypass_cors_preflight` to allow bypassing the CORS preflight request. * lb_subset_config: new fallback policy for selectors: :ref:`KEYS_SUBSET` * listeners: added :ref:`reuse_port` option. * logger: added :ref:`--log-format-escaped ` command line option to escape newline characters in application logs. +* ratelimit: added :ref:`local rate limit ` network filter. * rbac: added support for matching all subject alt names instead of first in :ref:`principal_name `. * redis: performance improvement for larger split commands by avoiding string copies. * redis: correctly follow MOVE/ASK redirection for mirrored clusters. @@ -1043,7 +1044,7 @@ Version history * Envoy configuration is now checked against a JSON schema. * :ref:`Ring hash ` consistent load balancer, as well as HTTP consistent hash routing based on a policy. -* Vastly :ref:`enhanced global rate limit configuration ` via the HTTP +* Vastly :ref:`enhanced global rate limit configuration ` via the HTTP rate limiting filter. * HTTP routing to a cluster retrieved from a header. * Weighted cluster HTTP routing. diff --git a/docs/root/intro/what_is_envoy.rst b/docs/root/intro/what_is_envoy.rst index 1e75ab089c8f9..2de7b36d775c1 100644 --- a/docs/root/intro/what_is_envoy.rst +++ b/docs/root/intro/what_is_envoy.rst @@ -44,7 +44,7 @@ authentication `, etc. architectures that Envoy :ref:`supports ` an additional HTTP L7 filter layer. HTTP filters can be plugged into the HTTP connection management subsystem that perform different tasks such as :ref:`buffering `, :ref:`rate limiting -`, :ref:`routing/forwarding `, sniffing +`, :ref:`routing/forwarding `, sniffing Amazon's :ref:`DynamoDB `, etc. **First class HTTP/2 support:** When operating in HTTP mode, Envoy :ref:`supports @@ -96,7 +96,7 @@ components in a distributed system is a complex problem. Because Envoy is a self instead of a library, it is able to implement advanced load balancing techniques in a single place and have them be accessible to any application. Currently Envoy includes support for :ref:`automatic retries `, :ref:`circuit breaking `, -:ref:`global rate limiting ` via an external rate limiting service, +:ref:`global rate limiting ` via an external rate limiting service, :ref:`request shadowing `, and :ref:`outlier detection `. Future support is planned for request racing. diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index b9d24fc7dbbfc..1b4044aa47958 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -84,6 +84,8 @@ proto_library( "//envoy/config/filter/network/ext_authz/v3alpha:pkg", "//envoy/config/filter/network/http_connection_manager/v2:pkg", "//envoy/config/filter/network/http_connection_manager/v3alpha:pkg", + "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", + "//envoy/config/filter/network/local_rate_limit/v3alpha:pkg", "//envoy/config/filter/network/mongo_proxy/v2:pkg", "//envoy/config/filter/network/mongo_proxy/v3alpha:pkg", "//envoy/config/filter/network/mysql_proxy/v1alpha1:pkg", diff --git a/generated_api_shadow/envoy/admin/v2alpha/clusters.proto b/generated_api_shadow/envoy/admin/v2alpha/clusters.proto index dee414b48fc48..af3e9aa21087b 100644 --- a/generated_api_shadow/envoy/admin/v2alpha/clusters.proto +++ b/generated_api_shadow/envoy/admin/v2alpha/clusters.proto @@ -141,6 +141,6 @@ message HostHealthStatus { // Health status as reported by EDS. Note: only HEALTHY and UNHEALTHY are currently supported // here. - // TODO(mrice32): pipe through remaining EDS health status possibilities. + // [#comment:TODO(mrice32): pipe through remaining EDS health status possibilities.] api.v2.core.HealthStatus eds_health_status = 3; } diff --git a/generated_api_shadow/envoy/admin/v3alpha/clusters.proto b/generated_api_shadow/envoy/admin/v3alpha/clusters.proto index 2b060a9fc26ee..6829d5e24dfe4 100644 --- a/generated_api_shadow/envoy/admin/v3alpha/clusters.proto +++ b/generated_api_shadow/envoy/admin/v3alpha/clusters.proto @@ -152,6 +152,6 @@ message HostHealthStatus { // Health status as reported by EDS. Note: only HEALTHY and UNHEALTHY are currently supported // here. - // TODO(mrice32): pipe through remaining EDS health status possibilities. + // [#comment:TODO(mrice32): pipe through remaining EDS health status possibilities.] api.v3alpha.core.HealthStatus eds_health_status = 3; } diff --git a/generated_api_shadow/envoy/api/v2/cds.proto b/generated_api_shadow/envoy/api/v2/cds.proto index d174d929dc497..298847e57d533 100644 --- a/generated_api_shadow/envoy/api/v2/cds.proto +++ b/generated_api_shadow/envoy/api/v2/cds.proto @@ -517,7 +517,7 @@ message Cluster { // *TransportSocketMatch* in this field. Other client Envoys receive CDS without // *transport_socket_match* set, and still send plain text traffic to the same cluster. // - // TODO(incfly): add a detailed architecture doc on intended usage. + // [#comment:TODO(incfly): add a detailed architecture doc on intended usage.] repeated TransportSocketMatch transport_socket_matches = 43; // Supplies the name of the cluster which must be unique across all clusters. diff --git a/generated_api_shadow/envoy/api/v2/core/config_source.proto b/generated_api_shadow/envoy/api/v2/core/config_source.proto index 00dae5bdf8f8d..042a847891678 100644 --- a/generated_api_shadow/envoy/api/v2/core/config_source.proto +++ b/generated_api_shadow/envoy/api/v2/core/config_source.proto @@ -38,7 +38,8 @@ message ApiConfigSource { // with every update, the xDS server only sends what has changed since the last update. // // DELTA_GRPC is not yet entirely implemented! Initially, only CDS is available. - // Do not use for other xDSes. TODO(fredlas) update/remove this warning when appropriate. + // Do not use for other xDSes. + // [#comment:TODO(fredlas) update/remove this warning when appropriate.] DELTA_GRPC = 3; } diff --git a/generated_api_shadow/envoy/api/v2/route/route.proto b/generated_api_shadow/envoy/api/v2/route/route.proto index b1435f1782bc9..a3f52ab178437 100644 --- a/generated_api_shadow/envoy/api/v2/route/route.proto +++ b/generated_api_shadow/envoy/api/v2/route/route.proto @@ -1178,7 +1178,7 @@ message VirtualCluster { core.RequestMethod method = 3 [deprecated = true]; } -// Global rate limiting :ref:`architecture overview `. +// Global rate limiting :ref:`architecture overview `. message RateLimit { // [#next-free-field: 7] message Action { diff --git a/generated_api_shadow/envoy/api/v3alpha/cds.proto b/generated_api_shadow/envoy/api/v3alpha/cds.proto index 798eddab27fb7..f7e070452395c 100644 --- a/generated_api_shadow/envoy/api/v3alpha/cds.proto +++ b/generated_api_shadow/envoy/api/v3alpha/cds.proto @@ -557,7 +557,7 @@ message Cluster { // *TransportSocketMatch* in this field. Other client Envoys receive CDS without // *transport_socket_match* set, and still send plain text traffic to the same cluster. // - // TODO(incfly): add a detailed architecture doc on intended usage. + // [#comment:TODO(incfly): add a detailed architecture doc on intended usage.] repeated TransportSocketMatch transport_socket_matches = 43; // Supplies the name of the cluster which must be unique across all clusters. diff --git a/generated_api_shadow/envoy/api/v3alpha/core/config_source.proto b/generated_api_shadow/envoy/api/v3alpha/core/config_source.proto index fe98e177c9993..a14b34d2c80ab 100644 --- a/generated_api_shadow/envoy/api/v3alpha/core/config_source.proto +++ b/generated_api_shadow/envoy/api/v3alpha/core/config_source.proto @@ -42,7 +42,8 @@ message ApiConfigSource { // with every update, the xDS server only sends what has changed since the last update. // // DELTA_GRPC is not yet entirely implemented! Initially, only CDS is available. - // Do not use for other xDSes. TODO(fredlas) update/remove this warning when appropriate. + // Do not use for other xDSes. + // [#comment:TODO(fredlas) update/remove this warning when appropriate.] DELTA_GRPC = 3; } diff --git a/generated_api_shadow/envoy/api/v3alpha/route/route.proto b/generated_api_shadow/envoy/api/v3alpha/route/route.proto index 037d90ec6645f..6b4e54e691471 100644 --- a/generated_api_shadow/envoy/api/v3alpha/route/route.proto +++ b/generated_api_shadow/envoy/api/v3alpha/route/route.proto @@ -1256,7 +1256,7 @@ message VirtualCluster { core.RequestMethod hidden_envoy_deprecated_method = 3 [deprecated = true]; } -// Global rate limiting :ref:`architecture overview `. +// Global rate limiting :ref:`architecture overview `. message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RateLimit"; diff --git a/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v2alpha/BUILD b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v2alpha/BUILD new file mode 100644 index 0000000000000..5a866264352a8 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v2alpha/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/api/v2/core:pkg", + "//envoy/type:pkg", + ], +) diff --git a/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.proto b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.proto new file mode 100644 index 0000000000000..39360de70a286 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package envoy.config.filter.network.local_rate_limit.v2alpha; + +import "envoy/api/v2/core/base.proto"; +import "envoy/type/token_bucket.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.local_rate_limit.v2alpha"; +option java_outer_classname = "LocalRateLimitProto"; +option java_multiple_files = true; + +// [#protodoc-title: Local rate limit] +// Local rate limit :ref:`configuration overview `. +// [#extension: envoy.filters.network.local_ratelimit] + +message LocalRateLimit { + // The prefix to use when emitting :ref:`statistics + // `. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // The token bucket configuration to use for rate limiting connections that are processed by the + // filter's filter chain. Each incoming connection processed by the filter consumes a single + // token. If the token is available, the connection will be allowed. If no tokens are available, + // the connection will be immediately closed. + // + // .. note:: + // In the current implementation each filter and filter chain has an independent rate limit. + // + // .. note:: + // In the current implementation the token bucket's :ref:`fill_interval + // ` must be >= 50ms to avoid too aggressive + // refills. + type.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; + + // Runtime flag that controls whether the filter is enabled or not. If not specified, defaults + // to enabled. + api.v2.core.RuntimeFeatureFlag runtime_enabled = 3; +} diff --git a/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v3alpha/BUILD b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v3alpha/BUILD new file mode 100644 index 0000000000000..ed63cfa839196 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v3alpha/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/api/v3alpha/core:pkg", + "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", + "//envoy/type/v3alpha:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v3alpha/local_rate_limit.proto b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v3alpha/local_rate_limit.proto new file mode 100644 index 0000000000000..31852ba798ab8 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/local_rate_limit/v3alpha/local_rate_limit.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package envoy.config.filter.network.local_rate_limit.v3alpha; + +import "envoy/api/v3alpha/core/base.proto"; +import "envoy/type/v3alpha/token_bucket.proto"; + +import "udpa/annotations/versioning.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.local_rate_limit.v3alpha"; +option java_outer_classname = "LocalRateLimitProto"; +option java_multiple_files = true; + +// [#protodoc-title: Local rate limit] +// Local rate limit :ref:`configuration overview `. +// [#extension: envoy.filters.network.local_ratelimit] + +message LocalRateLimit { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.local_rate_limit.v2alpha.LocalRateLimit"; + + // The prefix to use when emitting :ref:`statistics + // `. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // The token bucket configuration to use for rate limiting connections that are processed by the + // filter's filter chain. Each incoming connection processed by the filter consumes a single + // token. If the token is available, the connection will be allowed. If no tokens are available, + // the connection will be immediately closed. + // + // .. note:: + // In the current implementation each filter and filter chain has an independent rate limit. + // + // .. note:: + // In the current implementation the token bucket's :ref:`fill_interval + // ` must be >= 50ms to avoid too + // aggressive refills. + type.v3alpha.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; + + // Runtime flag that controls whether the filter is enabled or not. If not specified, defaults + // to enabled. + api.v3alpha.core.RuntimeFeatureFlag runtime_enabled = 3; +} diff --git a/generated_api_shadow/envoy/type/token_bucket.proto b/generated_api_shadow/envoy/type/token_bucket.proto new file mode 100644 index 0000000000000..b293b76be192e --- /dev/null +++ b/generated_api_shadow/envoy/type/token_bucket.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package envoy.type; + +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type"; +option java_outer_classname = "TokenBucketProto"; +option java_multiple_files = true; + +// [#protodoc-title: Token bucket] + +// Configures a token bucket, typically used for rate limiting. +message TokenBucket { + // The maximum tokens that the bucket can hold. This is also the number of tokens that the bucket + // initially contains. + uint32 max_tokens = 1 [(validate.rules).uint32 = {gt: 0}]; + + // The number of tokens added to the bucket during each fill interval. If not specified, defaults + // to a single token. + google.protobuf.UInt32Value tokens_per_fill = 2 [(validate.rules).uint32 = {gt: 0}]; + + // The fill interval that tokens are added to the bucket. During each fill interval + // `tokens_per_fill` are added to the bucket. The bucket will never contain more than + // `max_tokens` tokens. + google.protobuf.Duration fill_interval = 3 [(validate.rules).duration = { + required: true + gt {} + }]; +} diff --git a/generated_api_shadow/envoy/type/v3alpha/token_bucket.proto b/generated_api_shadow/envoy/type/v3alpha/token_bucket.proto new file mode 100644 index 0000000000000..bd5479827c998 --- /dev/null +++ b/generated_api_shadow/envoy/type/v3alpha/token_bucket.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package envoy.type.v3alpha; + +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/versioning.proto"; + +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.v3alpha"; +option java_outer_classname = "TokenBucketProto"; +option java_multiple_files = true; + +// [#protodoc-title: Token bucket] + +// Configures a token bucket, typically used for rate limiting. +message TokenBucket { + option (udpa.annotations.versioning).previous_message_type = "envoy.type.TokenBucket"; + + // The maximum tokens that the bucket can hold. This is also the number of tokens that the bucket + // initially contains. + uint32 max_tokens = 1 [(validate.rules).uint32 = {gt: 0}]; + + // The number of tokens added to the bucket during each fill interval. If not specified, defaults + // to a single token. + google.protobuf.UInt32Value tokens_per_fill = 2 [(validate.rules).uint32 = {gt: 0}]; + + // The fill interval that tokens are added to the bucket. During each fill interval + // `tokens_per_fill` are added to the bucket. The bucket will never contain more than + // `max_tokens` tokens. + google.protobuf.Duration fill_interval = 3 [(validate.rules).duration = { + required: true + gt {} + }]; +} diff --git a/source/common/common/BUILD b/source/common/common/BUILD index af0b166fa6625..389cbf2eec350 100644 --- a/source/common/common/BUILD +++ b/source/common/common/BUILD @@ -236,6 +236,16 @@ envoy_cc_library( external_deps = ["abseil_base"], ) +envoy_cc_library( + name = "thread_synchronizer_lib", + srcs = ["thread_synchronizer.cc"], + hdrs = ["thread_synchronizer.h"], + external_deps = ["abseil_synchronization"], + deps = [ + ":assert_lib", + ], +) + envoy_cc_library( name = "thread_lib", hdrs = ["thread.h"], diff --git a/source/common/common/thread_synchronizer.cc b/source/common/common/thread_synchronizer.cc new file mode 100644 index 0000000000000..28f6c11caf815 --- /dev/null +++ b/source/common/common/thread_synchronizer.cc @@ -0,0 +1,81 @@ +#include "common/common/thread_synchronizer.h" + +namespace Envoy { +namespace Thread { + +void ThreadSynchronizer::enable() { + ASSERT(data_ == nullptr); + data_ = std::make_unique(); +} + +ThreadSynchronizer::SynchronizerEntry& +ThreadSynchronizer::getOrCreateEntry(absl::string_view event_name) { + absl::MutexLock lock(&data_->mutex_); + auto& existing_entry = data_->entries_[event_name]; + if (existing_entry == nullptr) { + ENVOY_LOG(debug, "thread synchronzier: creating entry: {}", event_name); + existing_entry = std::make_unique(); + } + return *existing_entry; +} + +void ThreadSynchronizer::waitOnWorker(absl::string_view event_name) { + SynchronizerEntry& entry = getOrCreateEntry(event_name); + absl::MutexLock lock(&entry.mutex_); + ENVOY_LOG(debug, "thread synchronizer: waiting on next {}", event_name); + ASSERT(!entry.wait_on_); + entry.wait_on_ = true; +} + +void ThreadSynchronizer::syncPointWorker(absl::string_view event_name) { + SynchronizerEntry& entry = getOrCreateEntry(event_name); + absl::MutexLock lock(&entry.mutex_); + + // See if we are ignoring waits. If so, just return. + if (!entry.wait_on_) { + ENVOY_LOG(debug, "thread synchronizer: sync point {}: ignoring", event_name); + return; + } + entry.wait_on_ = false; + + // See if we are already signaled. If so, just clear signaled and return. + if (entry.signaled_) { + ENVOY_LOG(debug, "thread synchronizer: sync point {}: already signaled", event_name); + entry.signaled_ = false; + return; + } + + // Now signal any barrier waiters. + entry.at_barrier_ = true; + + // Now wait to be signaled. + ENVOY_LOG(debug, "thread synchronizer: blocking on sync point {}", event_name); + entry.mutex_.Await(absl::Condition(&entry.signaled_)); + ENVOY_LOG(debug, "thread synchronizer: done blocking for sync point {}", event_name); + + // Clear the barrier and signaled before unlocking and returning. + ASSERT(entry.at_barrier_); + entry.at_barrier_ = false; + ASSERT(entry.signaled_); + entry.signaled_ = false; +} + +void ThreadSynchronizer::barrierOnWorker(absl::string_view event_name) { + SynchronizerEntry& entry = getOrCreateEntry(event_name); + absl::MutexLock lock(&entry.mutex_); + ASSERT(!entry.at_barrier_ && entry.wait_on_); + ENVOY_LOG(debug, "thread synchronizer: barrier on {}", event_name); + entry.mutex_.Await(absl::Condition(&entry.at_barrier_)); + ENVOY_LOG(debug, "thread synchronizer: barrier complete {}", event_name); +} + +void ThreadSynchronizer::signalWorker(absl::string_view event_name) { + SynchronizerEntry& entry = getOrCreateEntry(event_name); + absl::MutexLock lock(&entry.mutex_); + ASSERT(!entry.signaled_); + ENVOY_LOG(debug, "thread synchronizer: signaling {}", event_name); + entry.signaled_ = true; +} + +} // namespace Thread +} // namespace Envoy diff --git a/source/common/common/thread_synchronizer.h b/source/common/common/thread_synchronizer.h new file mode 100644 index 0000000000000..b86df8a926bce --- /dev/null +++ b/source/common/common/thread_synchronizer.h @@ -0,0 +1,97 @@ +#pragma once + +#include "common/common/assert.h" +#include "common/common/logger.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" + +namespace Envoy { +namespace Thread { + +/** + * This class allows for forcing hard to test thread permutations. It is loosely modeled after: + * https://github.com/apache/incubator-pagespeed-mod/blob/master/pagespeed/kernel/thread/thread_synchronizer.h + * + * The idea is that there is almost no cost if the synchronizer is not enabled by test code as + * there is a single inline pointer check. + */ +class ThreadSynchronizer : Logger::Loggable { +public: + /** + * Enable the synchronizer. This should be called once per test by test code. + */ + void enable(); + + /** + * This is the only API that should generally be called from production code. It introduces + * a "sync point" that test code can then use to force blocking, thread barriers, etc. Even + * when the synchronizer is enabled(), the syncPoint() will do nothing unless it has been + * registered to block via waitOn(). + */ + void syncPoint(absl::string_view event_name) { + if (data_ != nullptr) { + syncPointWorker(event_name); + } + } + + /** + * The next time the sync point registered with event_name is invoked via syncPoint(), the calling + * code will block until signaled. Note that this is a one-shot operation and the sync point's + * wait status will be cleared. + */ + void waitOn(absl::string_view event_name) { + ASSERT(data_ != nullptr, "call enable() from test code before calling this method"); + waitOnWorker(event_name); + } + + /** + * This call will block until the next time the sync point registered with event_name is invoked. + * The event_name must have been previously registered for blocking via waitOn(). The typical + * test pattern is to have a thread arrive at a sync point, block, and then release a test + * thread which continues test execution, eventually calling signal() to release the other thread. + */ + void barrierOn(absl::string_view event_name) { + ASSERT(data_ != nullptr, "call enable() from test code before calling this method"); + barrierOnWorker(event_name); + } + + /** + * Signal an event such that a thread that is blocked within syncPoint() will now proceed. + */ + void signal(absl::string_view event_name) { + ASSERT(data_ != nullptr, "call enable() from test code before calling this method"); + signalWorker(event_name); + } + +private: + struct SynchronizerEntry { + ~SynchronizerEntry() { + // Make sure we don't have any pending signals which would indicate a bad test. + ASSERT(!signaled_); + } + + absl::Mutex mutex_; + bool wait_on_ ABSL_GUARDED_BY(mutex_){}; + bool signaled_ ABSL_GUARDED_BY(mutex_){}; + bool at_barrier_ ABSL_GUARDED_BY(mutex_){}; + }; + + struct SynchronizerData { + absl::Mutex mutex_; + absl::flat_hash_map> + entries_ ABSL_GUARDED_BY(mutex_); + }; + + SynchronizerEntry& getOrCreateEntry(absl::string_view event_name); + void syncPointWorker(absl::string_view event_name); + void waitOnWorker(absl::string_view event_name); + void barrierOnWorker(absl::string_view event_name); + void signalWorker(absl::string_view event_name); + + std::unique_ptr data_; +}; + +} // namespace Thread +} // namespace Envoy diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 2503e46434161..822acd102cc6c 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -84,6 +84,7 @@ EXTENSIONS = { "envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config", # WiP "envoy.filters.network.kafka": "//source/extensions/filters/network/kafka:kafka_request_codec_lib", + "envoy.filters.network.local_ratelimit": "//source/extensions/filters/network/local_ratelimit:config", "envoy.filters.network.mongo_proxy": "//source/extensions/filters/network/mongo_proxy:config", "envoy.filters.network.mysql_proxy": "//source/extensions/filters/network/mysql_proxy:config", "envoy.filters.network.ratelimit": "//source/extensions/filters/network/ratelimit:config", diff --git a/source/extensions/filters/network/local_ratelimit/BUILD b/source/extensions/filters/network/local_ratelimit/BUILD new file mode 100644 index 0000000000000..8cdfc5bd788af --- /dev/null +++ b/source/extensions/filters/network/local_ratelimit/BUILD @@ -0,0 +1,43 @@ +licenses(["notice"]) # Apache 2 + +# Local ratelimit L4 network filter +# Public docs: docs/root/configuration/network_filters/local_rate_limit_filter.rst + +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_package", +) + +envoy_package() + +envoy_cc_library( + name = "local_ratelimit_lib", + srcs = ["local_ratelimit.cc"], + hdrs = ["local_ratelimit.h"], + deps = [ + "//include/envoy/event:dispatcher_interface", + "//include/envoy/event:timer_interface", + "//include/envoy/network:filter_interface", + "//include/envoy/runtime:runtime_interface", + "//include/envoy/stats:stats_macros", + "//source/common/common:thread_synchronizer_lib", + "//source/common/protobuf:utility_lib", + "//source/common/runtime:runtime_lib", + "@envoy_api//envoy/config/filter/network/local_rate_limit/v2alpha:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "robust_to_untrusted_downstream", + deps = [ + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:factory_base_lib", + "//source/extensions/filters/network/local_ratelimit:local_ratelimit_lib", + "@envoy_api//envoy/config/filter/network/local_rate_limit/v2alpha:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/network/local_ratelimit/config.cc b/source/extensions/filters/network/local_ratelimit/config.cc new file mode 100644 index 0000000000000..77100dc1efea6 --- /dev/null +++ b/source/extensions/filters/network/local_ratelimit/config.cc @@ -0,0 +1,29 @@ +#include "extensions/filters/network/local_ratelimit/config.h" + +#include "extensions/filters/network/local_ratelimit/local_ratelimit.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace LocalRateLimitFilter { + +Network::FilterFactoryCb LocalRateLimitConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::config::filter::network::local_rate_limit::v2alpha::LocalRateLimit& proto_config, + Server::Configuration::FactoryContext& context) { + ConfigSharedPtr filter_config( + new Config(proto_config, context.dispatcher(), context.scope(), context.runtime())); + return [filter_config](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter(std::make_shared(filter_config)); + }; +} + +/** + * Static registration for the local rate limit filter. @see RegisterFactory. + */ +REGISTER_FACTORY(LocalRateLimitConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory); + +} // namespace LocalRateLimitFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/local_ratelimit/config.h b/source/extensions/filters/network/local_ratelimit/config.h new file mode 100644 index 0000000000000..a865b7cd8629d --- /dev/null +++ b/source/extensions/filters/network/local_ratelimit/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include "envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.pb.h" +#include "envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.pb.validate.h" + +#include "extensions/filters/network/common/factory_base.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace LocalRateLimitFilter { + +/** + * Config registration for the local rate limit filter. @see NamedNetworkFilterConfigFactory. + */ +class LocalRateLimitConfigFactory + : public Common::FactoryBase< + envoy::config::filter::network::local_rate_limit::v2alpha::LocalRateLimit> { +public: + LocalRateLimitConfigFactory() : FactoryBase(NetworkFilterNames::get().LocalRateLimit) {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::config::filter::network::local_rate_limit::v2alpha::LocalRateLimit& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace LocalRateLimitFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc b/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc new file mode 100644 index 0000000000000..ba127e1178426 --- /dev/null +++ b/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc @@ -0,0 +1,94 @@ +#include "extensions/filters/network/local_ratelimit/local_ratelimit.h" + +#include "envoy/event/dispatcher.h" + +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace LocalRateLimitFilter { + +Config::Config( + const envoy::config::filter::network::local_rate_limit::v2alpha::LocalRateLimit& proto_config, + Event::Dispatcher& dispatcher, Stats::Scope& scope, Runtime::Loader& runtime) + : fill_timer_(dispatcher.createTimer([this] { onFillTimer(); })), + max_tokens_(proto_config.token_bucket().max_tokens()), + tokens_per_fill_( + PROTOBUF_GET_WRAPPED_OR_DEFAULT(proto_config.token_bucket(), tokens_per_fill, 1)), + fill_interval_(PROTOBUF_GET_MS_REQUIRED(proto_config.token_bucket(), fill_interval)), + enabled_(proto_config.runtime_enabled(), runtime), + stats_(generateStats(proto_config.stat_prefix(), scope)), tokens_(max_tokens_) { + if (fill_interval_ < std::chrono::milliseconds(50)) { + throw EnvoyException("local rate limit token bucket fill timer must be >= 50ms"); + } + fill_timer_->enableTimer(fill_interval_); +} + +LocalRateLimitStats Config::generateStats(const std::string& prefix, Stats::Scope& scope) { + const std::string final_prefix = "local_rate_limit." + prefix; + return {ALL_LOCAL_RATE_LIMIT_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; +} + +void Config::onFillTimer() { + // Relaxed consistency is used for all operations because we don't care about ordering, just the + // final atomic correctness. + uint32_t expected_tokens = tokens_.load(std::memory_order_relaxed); + uint32_t new_tokens_value; + do { + // expected_tokens is either initialized above or reloaded during the CAS failure below. + new_tokens_value = std::min(max_tokens_, expected_tokens + tokens_per_fill_); + + // Testing hook. + synchronizer_.syncPoint("on_fill_timer_pre_cas"); + + // Loop while the weak CAS fails trying to update the tokens value. + } while ( + !tokens_.compare_exchange_weak(expected_tokens, new_tokens_value, std::memory_order_relaxed)); + + ENVOY_LOG(trace, "local_rate_limit: fill tokens={}", new_tokens_value); + fill_timer_->enableTimer(fill_interval_); +} + +bool Config::canCreateConnection() { + // Relaxed consistency is used for all operations because we don't care about ordering, just the + // final atomic correctness. + uint32_t expected_tokens = tokens_.load(std::memory_order_relaxed); + do { + // expected_tokens is either initialized above or reloaded during the CAS failure below. + if (expected_tokens == 0) { + return false; + } + + // Testing hook. + synchronizer_.syncPoint("can_create_connection_pre_cas"); + + // Loop while the weak CAS fails trying to subtract 1 from expected. + } while (!tokens_.compare_exchange_weak(expected_tokens, expected_tokens - 1, + std::memory_order_relaxed)); + + // We successfully decremented the counter by 1. + return true; +} + +Network::FilterStatus Filter::onNewConnection() { + if (!config_->enabled()) { + ENVOY_CONN_LOG(trace, "local_rate_limit: runtime disabled", read_callbacks_->connection()); + return Network::FilterStatus::Continue; + } + + if (!config_->canCreateConnection()) { + config_->stats().rate_limited_.inc(); + ENVOY_CONN_LOG(trace, "local_rate_limit: rate limiting connection", + read_callbacks_->connection()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + return Network::FilterStatus::StopIteration; + } + + return Network::FilterStatus::Continue; +} + +} // namespace LocalRateLimitFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/local_ratelimit/local_ratelimit.h b/source/extensions/filters/network/local_ratelimit/local_ratelimit.h new file mode 100644 index 0000000000000..1b1208940b82b --- /dev/null +++ b/source/extensions/filters/network/local_ratelimit/local_ratelimit.h @@ -0,0 +1,87 @@ +#pragma once + +#include "envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.pb.h" +#include "envoy/event/timer.h" +#include "envoy/network/filter.h" +#include "envoy/runtime/runtime.h" +#include "envoy/stats/stats_macros.h" + +#include "common/common/thread_synchronizer.h" +#include "common/runtime/runtime_features.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace LocalRateLimitFilter { + +/** + * All local rate limit stats. @see stats_macros.h + */ +#define ALL_LOCAL_RATE_LIMIT_STATS(COUNTER) COUNTER(rate_limited) + +/** + * Struct definition for all local rate limit stats. @see stats_macros.h + */ +struct LocalRateLimitStats { + ALL_LOCAL_RATE_LIMIT_STATS(GENERATE_COUNTER_STRUCT) +}; + +/** + * Configuration shared across all connections. Must be thread safe. + */ +class Config : Logger::Loggable { +public: + Config( + const envoy::config::filter::network::local_rate_limit::v2alpha::LocalRateLimit& proto_config, + Event::Dispatcher& dispatcher, Stats::Scope& scope, Runtime::Loader& runtime); + + bool canCreateConnection(); + bool enabled() { return enabled_.enabled(); } + LocalRateLimitStats& stats() { return stats_; } + +private: + static LocalRateLimitStats generateStats(const std::string& prefix, Stats::Scope& scope); + void onFillTimer(); + + // TODO(mattklein123): Determine if/how to merge this with token_bucket_impl.h/cc. This + // implementation is geared towards multi-threading as well assumes a high call rate (which is + // why a fixed periodic refresh timer is used). + const Event::TimerPtr fill_timer_; + const uint32_t max_tokens_; + const uint32_t tokens_per_fill_; + const std::chrono::milliseconds fill_interval_; + Runtime::FeatureFlag enabled_; + LocalRateLimitStats stats_; + std::atomic tokens_; + Thread::ThreadSynchronizer synchronizer_; // Used for testing only. + + friend class LocalRateLimitTestBase; +}; + +using ConfigSharedPtr = std::shared_ptr; + +/** + * Per-connection local rate limit filter. + */ +class Filter : public Network::ReadFilter, Logger::Loggable { +public: + Filter(const ConfigSharedPtr& config) : config_(config) {} + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance&, bool) override { + return Network::FilterStatus::Continue; + } + Network::FilterStatus onNewConnection() override; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& read_callbacks) override { + read_callbacks_ = &read_callbacks; + } + +private: + const ConfigSharedPtr config_; + Network::ReadFilterCallbacks* read_callbacks_{}; +}; + +} // namespace LocalRateLimitFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index 68318adc2830d..27d1ba8f50141 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -20,6 +20,8 @@ class NetworkFilterNameValues { const std::string DubboProxy = "envoy.filters.network.dubbo_proxy"; // HTTP connection manager filter const std::string HttpConnectionManager = "envoy.http_connection_manager"; + // Local rate limit filter + const std::string LocalRateLimit = "envoy.filters.network.local_ratelimit"; // Mongo proxy filter const std::string MongoProxy = "envoy.mongo_proxy"; // MySQL proxy filter diff --git a/test/config/utility.cc b/test/config/utility.cc index 2badd14cfff8d..0bac1b54956b9 100644 --- a/test/config/utility.cc +++ b/test/config/utility.cc @@ -722,6 +722,19 @@ envoy::api::v2::listener::Filter* ConfigHelper::getFilterFromListener(const std: return nullptr; } +void ConfigHelper::addNetworkFilter(const std::string& filter_yaml) { + RELEASE_ASSERT(!finalized_, ""); + auto* filter_chain = + bootstrap_.mutable_static_resources()->mutable_listeners(0)->mutable_filter_chains(0); + auto* filter_list_back = filter_chain->add_filters(); + TestUtility::loadFromYaml(filter_yaml, *filter_list_back); + + // Now move it to the front. + for (int i = filter_chain->filters_size() - 1; i > 0; --i) { + filter_chain->mutable_filters()->SwapElements(i, i - 1); + } +} + bool ConfigHelper::loadHttpConnectionManager( envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager& hcm) { RELEASE_ASSERT(!finalized_, ""); diff --git a/test/config/utility.h b/test/config/utility.h index 39841ce95612b..7e6f1c63a93d9 100644 --- a/test/config/utility.h +++ b/test/config/utility.h @@ -130,6 +130,9 @@ class ConfigHelper { // Add an HTTP filter prior to existing filters. void addFilter(const std::string& filter_yaml); + // Add a network filter prior to existing filters. + void addNetworkFilter(const std::string& filter_yaml); + // Sets the client codec to the specified type. void setClientCodec( envoy::config::filter::network::http_connection_manager::v2::HttpConnectionManager::CodecType diff --git a/test/extensions/filters/network/local_ratelimit/BUILD b/test/extensions/filters/network/local_ratelimit/BUILD new file mode 100644 index 0000000000000..24170c575cc72 --- /dev/null +++ b/test/extensions/filters/network/local_ratelimit/BUILD @@ -0,0 +1,36 @@ +licenses(["notice"]) # Apache 2 + +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +envoy_package() + +envoy_extension_cc_test( + name = "local_ratelimit_test", + srcs = ["local_ratelimit_test.cc"], + extension_name = "envoy.filters.network.local_ratelimit", + deps = [ + "//source/extensions/filters/network/local_ratelimit:local_ratelimit_lib", + "//test/mocks/event:event_mocks", + "//test/mocks/network:network_mocks", + "//test/mocks/runtime:runtime_mocks", + "@envoy_api//envoy/config/filter/network/local_rate_limit/v2alpha:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "local_ratelimit_integration_test", + srcs = ["local_ratelimit_integration_test.cc"], + extension_name = "envoy.filters.network.local_ratelimit", + deps = [ + "//source/extensions/filters/network/local_ratelimit:config", + "//source/extensions/filters/network/tcp_proxy:config", + "//test/integration:integration_lib", + ], +) diff --git a/test/extensions/filters/network/local_ratelimit/local_ratelimit_integration_test.cc b/test/extensions/filters/network/local_ratelimit/local_ratelimit_integration_test.cc new file mode 100644 index 0000000000000..d895f03739f9c --- /dev/null +++ b/test/extensions/filters/network/local_ratelimit/local_ratelimit_integration_test.cc @@ -0,0 +1,61 @@ +#include "test/integration/integration.h" + +namespace Envoy { +namespace { + +class LocalRateLimitIntegrationTest : public Event::TestUsingSimulatedTime, + public testing::TestWithParam, + public BaseIntegrationTest { +public: + LocalRateLimitIntegrationTest() + : BaseIntegrationTest(GetParam(), ConfigHelper::TCP_PROXY_CONFIG) {} + + ~LocalRateLimitIntegrationTest() override { + test_server_.reset(); + fake_upstreams_.clear(); + } + + void setup(const std::string& filter_yaml) { + config_helper_.addNetworkFilter(filter_yaml); + BaseIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P(IpVersions, LocalRateLimitIntegrationTest, + testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + TestUtility::ipTestParamsToString); + +// Make sure the filter works in the basic case. +TEST_P(LocalRateLimitIntegrationTest, NoRateLimiting) { + setup(R"EOF( +name: envoy.filters.network.local_ratelimit +typed_config: + "@type": type.googleapis.com/envoy.config.filter.network.local_rate_limit.v2alpha.LocalRateLimit + stat_prefix: local_rate_limit_stats + token_bucket: + max_tokens: 1 + fill_interval: 0.2s +)EOF"); + + IntegrationTcpClientPtr tcp_client = makeTcpConnection(lookupPort("listener_0")); + FakeRawConnectionPtr fake_upstream_connection; + ASSERT_TRUE(fake_upstreams_[0]->waitForRawConnection(fake_upstream_connection)); + tcp_client->write("hello"); + ASSERT_TRUE(fake_upstream_connection->waitForData(5)); + ASSERT_TRUE(fake_upstream_connection->write("world")); + tcp_client->waitForData("world"); + tcp_client->close(); + ASSERT_TRUE(fake_upstream_connection->waitForDisconnect(true)); + + EXPECT_EQ(0, + test_server_->counter("local_rate_limit.local_rate_limit_stats.rate_limited")->value()); +} + +// TODO(mattklein123): Create an integration test that tests rate limiting. Right now this is +// not easily possible using simulated time due to the fact that simulated time runs alarms on +// their correct threads when woken up, but does not have any barrier for when the alarms have +// actually fired. This makes a deterministic test impossible without resorting to hacks like +// storing the number of tokens in a stat, etc. + +} // namespace +} // namespace Envoy diff --git a/test/extensions/filters/network/local_ratelimit/local_ratelimit_test.cc b/test/extensions/filters/network/local_ratelimit/local_ratelimit_test.cc new file mode 100644 index 0000000000000..4bd980619e556 --- /dev/null +++ b/test/extensions/filters/network/local_ratelimit/local_ratelimit_test.cc @@ -0,0 +1,297 @@ +#include "envoy/config/filter/network/local_rate_limit/v2alpha/local_rate_limit.pb.validate.h" + +#include "common/stats/isolated_store_impl.h" + +#include "extensions/filters/network/local_ratelimit/local_ratelimit.h" + +#include "test/mocks/event/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/runtime/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::InSequence; +using testing::NiceMock; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace LocalRateLimitFilter { + +class LocalRateLimitTestBase : public testing::Test { +public: + void initialize(const std::string& filter_yaml, bool expect_timer_create = true) { + envoy::config::filter::network::local_rate_limit::v2alpha::LocalRateLimit proto_config; + TestUtility::loadFromYamlAndValidate(filter_yaml, proto_config); + fill_timer_ = new Event::MockTimer(&dispatcher_); + if (expect_timer_create) { + EXPECT_CALL(*fill_timer_, enableTimer(_, nullptr)); + } + config_ = std::make_shared(proto_config, dispatcher_, stats_store_, runtime_); + } + + Thread::ThreadSynchronizer& synchronizer() { return config_->synchronizer_; } + + NiceMock dispatcher_; + Stats::IsolatedStoreImpl stats_store_; + NiceMock runtime_; + Event::MockTimer* fill_timer_{}; + ConfigSharedPtr config_; +}; + +// Make sure we fail with a fill rate this is too fast. +TEST_F(LocalRateLimitTestBase, TooFastFillRate) { + EXPECT_THROW_WITH_MESSAGE(initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 1 + fill_interval: 0.049s +)EOF", + false), + EnvoyException, + "local rate limit token bucket fill timer must be >= 50ms"); +} + +// Verify various token bucket CAS edge cases. +TEST_F(LocalRateLimitTestBase, CasEdgeCases) { + // This tests the case in which a connection creation races with the fill timer. + { + initialize(R"EOF( + stat_prefix: local_rate_limit_stats + token_bucket: + max_tokens: 1 + fill_interval: 0.05s + )EOF"); + + synchronizer().enable(); + + // Start a thread and start the fill callback. This will wait pre-CAS. + synchronizer().waitOn("on_fill_timer_pre_cas"); + std::thread t1([&] { + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + }); + // Wait until the thread is actually waiting. + synchronizer().barrierOn("on_fill_timer_pre_cas"); + + // Create a connection. This should succeed. + EXPECT_TRUE(config_->canCreateConnection()); + + // Now signal the thread to continue which should cause a CAS failure and the loop to repeat. + synchronizer().signal("on_fill_timer_pre_cas"); + t1.join(); + + // 1 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); + } + + // This tests the case in which two connection creations race. + { + initialize(R"EOF( + stat_prefix: local_rate_limit_stats + token_bucket: + max_tokens: 1 + fill_interval: 0.2s + )EOF"); + + synchronizer().enable(); + + // Start a thread and see if we can create a connection. This will wait pre-CAS. + synchronizer().waitOn("can_create_connection_pre_cas"); + std::thread t1([&] { EXPECT_FALSE(config_->canCreateConnection()); }); + // Wait until the thread is actually waiting. + synchronizer().barrierOn("can_create_connection_pre_cas"); + + // Create the connection on this thread, which should cause the CAS to fail on the other thread. + EXPECT_TRUE(config_->canCreateConnection()); + synchronizer().signal("can_create_connection_pre_cas"); + t1.join(); + } +} + +// Verify token bucket functionality with a single token. +TEST_F(LocalRateLimitTestBase, TokenBucket) { + initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 1 + fill_interval: 0.2s +)EOF"); + + // 1 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); + + // 0 -> 1 tokens + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); + + // 0 -> 1 tokens + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 1 tokens + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); +} + +// Verify token bucket functionality with max tokens and tokens per fill > 1. +TEST_F(LocalRateLimitTestBase, TokenBucketMultipleTokensPerFill) { + initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 2 + tokens_per_fill: 2 + fill_interval: 0.2s +)EOF"); + + // 2 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); + + // 0 -> 2 tokens + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // 2 -> 1 tokens + EXPECT_TRUE(config_->canCreateConnection()); + + // 1 -> 2 tokens + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // 2 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); +} + +// Verify token bucket functionality with max tokens > tokens per fill. +TEST_F(LocalRateLimitTestBase, TokenBucketMaxTokensGreaterThanTokensPerFill) { + initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 2 + tokens_per_fill: 1 + fill_interval: 0.2s +)EOF"); + + // 2 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); + + // 0 -> 1 tokens + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 0 tokens + EXPECT_TRUE(config_->canCreateConnection()); + EXPECT_FALSE(config_->canCreateConnection()); +} + +class LocalRateLimitFilterTest : public LocalRateLimitTestBase { +public: + struct ActiveFilter { + ActiveFilter(const ConfigSharedPtr& config) : filter_(config) { + filter_.initializeReadFilterCallbacks(read_filter_callbacks_); + } + + NiceMock read_filter_callbacks_; + Filter filter_; + }; +}; + +// Basic no rate limit case. +TEST_F(LocalRateLimitFilterTest, NoRateLimit) { + initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 1 + fill_interval: 0.2s +)EOF"); + + InSequence s; + ActiveFilter active_filter(config_); + EXPECT_EQ(Network::FilterStatus::Continue, active_filter.filter_.onNewConnection()); + EXPECT_EQ(0, TestUtility::findCounter(stats_store_, + "local_rate_limit.local_rate_limit_stats.rate_limited") + ->value()); +} + +// Basic rate limit case. +TEST_F(LocalRateLimitFilterTest, RateLimit) { + initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 1 + fill_interval: 0.2s +)EOF"); + + // First connection is OK. + InSequence s; + ActiveFilter active_filter1(config_); + EXPECT_EQ(Network::FilterStatus::Continue, active_filter1.filter_.onNewConnection()); + + // Second connection should be rate limited. + ActiveFilter active_filter2(config_); + EXPECT_CALL(active_filter2.read_filter_callbacks_.connection_, close(_)); + EXPECT_EQ(Network::FilterStatus::StopIteration, active_filter2.filter_.onNewConnection()); + EXPECT_EQ(1, TestUtility::findCounter(stats_store_, + "local_rate_limit.local_rate_limit_stats.rate_limited") + ->value()); + + // Refill the bucket. + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); + fill_timer_->invokeCallback(); + + // Third connection is OK. + ActiveFilter active_filter3(config_); + EXPECT_EQ(Network::FilterStatus::Continue, active_filter3.filter_.onNewConnection()); +} + +// Verify the runtime disable functionality. +TEST_F(LocalRateLimitFilterTest, RuntimeDisabled) { + initialize(R"EOF( +stat_prefix: local_rate_limit_stats +token_bucket: + max_tokens: 1 + fill_interval: 0.2s +runtime_enabled: + default_value: true + runtime_key: foo_key +)EOF"); + + // First connection is OK. + InSequence s; + ActiveFilter active_filter1(config_); + EXPECT_CALL(runtime_.snapshot_, getBoolean("foo_key", true)).WillOnce(Return(true)); + EXPECT_EQ(Network::FilterStatus::Continue, active_filter1.filter_.onNewConnection()); + + // Second connection should be rate limited but won't be due to filter disable. + ActiveFilter active_filter2(config_); + EXPECT_CALL(runtime_.snapshot_, getBoolean("foo_key", true)).WillOnce(Return(false)); + EXPECT_EQ(Network::FilterStatus::Continue, active_filter2.filter_.onNewConnection()); + EXPECT_EQ(0, TestUtility::findCounter(stats_store_, + "local_rate_limit.local_rate_limit_stats.rate_limited") + ->value()); +} + +} // namespace LocalRateLimitFilter +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/tools/spelling_dictionary.txt b/tools/spelling_dictionary.txt index 522f9769c677b..c24d646693720 100644 --- a/tools/spelling_dictionary.txt +++ b/tools/spelling_dictionary.txt @@ -987,6 +987,7 @@ superset symlink symlinked symlinks +synchronizer syncookie sys syscall