diff --git a/CODEOWNERS b/CODEOWNERS index c72b2900ec389..7daa13077d26e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -154,3 +154,5 @@ extensions/filters/http/oauth2 @rgs1 @derekargueta @snowp # HTTP Local Rate Limit /*/extensions/filters/http/local_ratelimit @rgs1 @mattklein123 /*/extensions/filters/common/local_ratelimit @mattklein123 @rgs1 +# HTTP Kill Request +/*/extensions/filters/http/kill_request @qqustc @htuch diff --git a/api/BUILD b/api/BUILD index 3df8b906b0067..b918a3c954c8f 100644 --- a/api/BUILD +++ b/api/BUILD @@ -190,6 +190,7 @@ proto_library( "//envoy/extensions/filters/http/health_check/v3:pkg", "//envoy/extensions/filters/http/ip_tagging/v3:pkg", "//envoy/extensions/filters/http/jwt_authn/v3:pkg", + "//envoy/extensions/filters/http/kill_request/v3:pkg", "//envoy/extensions/filters/http/local_ratelimit/v3:pkg", "//envoy/extensions/filters/http/lua/v3:pkg", "//envoy/extensions/filters/http/oauth2/v3alpha:pkg", diff --git a/api/envoy/extensions/filters/http/kill_request/v3/BUILD b/api/envoy/extensions/filters/http/kill_request/v3/BUILD new file mode 100644 index 0000000000000..9a76b7e148e03 --- /dev/null +++ b/api/envoy/extensions/filters/http/kill_request/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/kill_request/v3/kill_request.proto b/api/envoy/extensions/filters/http/kill_request/v3/kill_request.proto new file mode 100644 index 0000000000000..fd7a3d3397c02 --- /dev/null +++ b/api/envoy/extensions/filters/http/kill_request/v3/kill_request.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.kill_request.v3; + +import "envoy/type/v3/percent.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.kill_request.v3"; +option java_outer_classname = "KillRequestProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Kill Request] +// Kill Request :ref:`configuration overview `. +// [#extension: envoy.filters.http.kill_request] + +// Configuration for KillRequest filter. +message KillRequest { + // The probability that a Kill request will be triggered. + type.v3.FractionalPercent probability = 1; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index dc1162bb93c7c..efd4715f83f96 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -73,6 +73,7 @@ proto_library( "//envoy/extensions/filters/http/health_check/v3:pkg", "//envoy/extensions/filters/http/ip_tagging/v3:pkg", "//envoy/extensions/filters/http/jwt_authn/v3:pkg", + "//envoy/extensions/filters/http/kill_request/v3:pkg", "//envoy/extensions/filters/http/local_ratelimit/v3:pkg", "//envoy/extensions/filters/http/lua/v3:pkg", "//envoy/extensions/filters/http/oauth2/v3alpha:pkg", diff --git a/docs/generate_extension_db.py b/docs/generate_extension_db.py index c6261977696e4..79bfe5fad6c9c 100755 --- a/docs/generate_extension_db.py +++ b/docs/generate_extension_db.py @@ -55,7 +55,12 @@ def GetExtensionMetadata(target): if __name__ == '__main__': output_path = sys.argv[1] extension_db = {} - for extension, target in extensions_build_config.EXTENSIONS.items(): + # Include all extensions from both EXTENSIONS and + # DISABLED_BY_DEFAULT_EXTENSIONS in source/extensions/extensions_build_config.bzl + all_extensions = {} + all_extensions.update(extensions_build_config.EXTENSIONS) + all_extensions.update(extensions_build_config.DISABLED_BY_DEFAULT_EXTENSIONS) + for extension, target in all_extensions.items(): extension_db[extension] = GetExtensionMetadata(target) # The TLS and generic upstream extensions are hard-coded into the build, so # not in source/extensions/extensions_build_config.bzl diff --git a/docs/root/configuration/http/http_filters/http_filters.rst b/docs/root/configuration/http/http_filters/http_filters.rst index 801d9f1319432..1b1e4b420bdc6 100644 --- a/docs/root/configuration/http/http_filters/http_filters.rst +++ b/docs/root/configuration/http/http_filters/http_filters.rst @@ -30,6 +30,7 @@ HTTP filters header_to_metadata_filter ip_tagging_filter jwt_authn_filter + kill_request_filter local_rate_limit_filter lua_filter oauth2_filter diff --git a/docs/root/configuration/http/http_filters/kill_request_filter.rst b/docs/root/configuration/http/http_filters/kill_request_filter.rst new file mode 100644 index 0000000000000..a391baa56bb1d --- /dev/null +++ b/docs/root/configuration/http/http_filters/kill_request_filter.rst @@ -0,0 +1,39 @@ +.. _config_http_filters_kill_request: + +Kill Request +=============== + +The KillRequest filter can be used to crash Envoy when receiving a Kill request. +By default, KillRequest filter is not built into Envoy binary since it is included in *DISABLED_BY_DEFAULT_EXTENSIONS* in *extensions_build_config.bzl*. If you want to use this extension, please move it from *DISABLED_BY_DEFAULT_EXTENSIONS* to *EXTENSIONS*. + +Configuration +------------- + +* This filter should be configured with the name *envoy.filters.http.kill_request*. + +.. _config_http_filters_kill_request_http_header: + +Enable Kill Request via HTTP header +-------------------------------------------- + +The KillRequest filter requires the following header in the request: + +x-envoy-kill-request + whether the request is a Kill request. + The header value must be one of (case-insensitive) ["true", "t", "yes", "y", "1"] + in order for the request to be a Kill request. + +.. note:: + + If the headers appear multiple times only the first value is used. + +The following is an example configuration: + +.. code-block:: yaml + + name: envoy.filters.http.kill_request + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.kill_request.v3.KillRequest + probability: + numerator: 100 + diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 5cf3712f3c06a..ea81db272720c 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -61,6 +61,7 @@ New Features * http: added HCM :ref:`timeout config field ` to control how long a downstream has to finish sending headers before the stream is cancelled. * http: added frame flood and abuse checks to the upstream HTTP/2 codec. This check is off by default and can be enabled by setting the `envoy.reloadable_features.upstream_http2_flood_checks` runtime key to true. * jwt_authn: added support for :ref:`per-route config `. +* kill_request: added new :ref:`HTTP kill request filter `. * listener: added an optional :ref:`default filter chain `. If this field is supplied, and none of the :ref:`filter_chains ` matches, this default filter chain is used to serve the connection. * log: added a new custom flag ``%_`` to the log pattern to print the actual message to log, but with escaped newlines. * lua: added `downstreamDirectRemoteAddress()` and `downstreamLocalAddress()` APIs to :ref:`streamInfo() `. diff --git a/generated_api_shadow/envoy/extensions/filters/http/kill_request/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/http/kill_request/v3/BUILD new file mode 100644 index 0000000000000..9a76b7e148e03 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/kill_request/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/kill_request/v3/kill_request.proto b/generated_api_shadow/envoy/extensions/filters/http/kill_request/v3/kill_request.proto new file mode 100644 index 0000000000000..fd7a3d3397c02 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/kill_request/v3/kill_request.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.kill_request.v3; + +import "envoy/type/v3/percent.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.kill_request.v3"; +option java_outer_classname = "KillRequestProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Kill Request] +// Kill Request :ref:`configuration overview `. +// [#extension: envoy.filters.http.kill_request] + +// Configuration for KillRequest filter. +message KillRequest { + // The probability that a Kill request will be triggered. + type.v3.FractionalPercent probability = 1; +} diff --git a/source/extensions/all_extensions.bzl b/source/extensions/all_extensions.bzl index 5fde35e3c92bb..b054211ca3e00 100644 --- a/source/extensions/all_extensions.bzl +++ b/source/extensions/all_extensions.bzl @@ -1,5 +1,7 @@ load("@bazel_skylib//lib:dicts.bzl", "dicts") -load("@envoy_build_config//:extensions_build_config.bzl", "EXTENSIONS") +load("@envoy_build_config//:extensions_build_config.bzl", "DISABLED_BY_DEFAULT_EXTENSIONS", "EXTENSIONS") + +GLOBAL_DENYLIST = DISABLED_BY_DEFAULT_EXTENSIONS.keys() # These extensions are registered using the extension system but are required for the core Envoy build. # The map may be overridden by extensions specified in envoy_build_config. @@ -12,6 +14,8 @@ _required_extensions = { def envoy_all_extensions(denylist = []): all_extensions = dicts.add(_required_extensions, EXTENSIONS) + denylist = denylist + GLOBAL_DENYLIST + # These extensions can be removed on a site specific basis. return [v for k, v in all_extensions.items() if not k in denylist] @@ -37,7 +41,7 @@ _http_filter_prefix = "envoy.filters.http" def envoy_all_http_filters(): all_extensions = dicts.add(_required_extensions, EXTENSIONS) - return [v for k, v in all_extensions.items() if k.startswith(_http_filter_prefix)] + return [v for k, v in all_extensions.items() if k.startswith(_http_filter_prefix) and k not in GLOBAL_DENYLIST] # All network-layer filters are extensions with names that have the following prefix. _network_filter_prefix = "envoy.filters.network" @@ -49,4 +53,4 @@ _thrift_filter_prefix = "envoy.filters.thrift" def envoy_all_network_filters(): all_extensions = dicts.add(_required_extensions, EXTENSIONS) - return [v for k, v in all_extensions.items() if k.startswith(_network_filter_prefix) or k.startswith(_thrift_filter_prefix)] + return [v for k, v in all_extensions.items() if (k.startswith(_network_filter_prefix) or k.startswith(_thrift_filter_prefix)) and k not in GLOBAL_DENYLIST] diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 8a48fb6782644..556b32ad14ef2 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -227,6 +227,11 @@ EXTENSIONS = { "envoy.wasm.runtime.wasmtime": "//source/extensions/wasm_runtime/wasmtime:config", } +# These filters will not be built into Envoy by default. To build Envoy with any of these filter, please move it to EXTENSIONS. +DISABLED_BY_DEFAULT_EXTENSIONS = { + "envoy.filters.http.kill_request": "//source/extensions/filters/http/kill_request:kill_request_config", +} + # These can be changed to ["//visibility:public"], for downstream builds which # need to directly reference Envoy extensions. EXTENSION_CONFIG_VISIBILITY = ["//:extension_config"] diff --git a/source/extensions/filters/http/kill_request/BUILD b/source/extensions/filters/http/kill_request/BUILD new file mode 100644 index 0000000000000..aa37e09f6a70a --- /dev/null +++ b/source/extensions/filters/http/kill_request/BUILD @@ -0,0 +1,40 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "kill_request_filter_lib", + srcs = ["kill_request_filter.cc"], + hdrs = ["kill_request_filter.h"], + deps = [ + "//include/envoy/common:random_generator_interface", + "//include/envoy/http:filter_interface", + "//include/envoy/http:header_map_interface", + "//source/common/http:header_map_lib", + "//source/common/http:header_utility_lib", + "//source/common/http:headers_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/extensions/filters/http/kill_request/v3:pkg_cc_proto", + ], +) + +envoy_cc_extension( + name = "kill_request_config", + srcs = ["kill_request_config.cc"], + hdrs = ["kill_request_config.h"], + security_posture = "robust_to_untrusted_downstream", + deps = [ + "//include/envoy/registry", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/kill_request:kill_request_filter_lib", + "@envoy_api//envoy/extensions/filters/http/kill_request/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/kill_request/kill_request_config.cc b/source/extensions/filters/http/kill_request/kill_request_config.cc new file mode 100644 index 0000000000000..59a9ec7d44261 --- /dev/null +++ b/source/extensions/filters/http/kill_request/kill_request_config.cc @@ -0,0 +1,31 @@ +#include "extensions/filters/http/kill_request/kill_request_config.h" + +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.h" +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "extensions/filters/http/kill_request/kill_request_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { + +Http::FilterFactoryCb KillRequestFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::kill_request::v3::KillRequest& proto_config, + const std::string&, Server::Configuration::FactoryContext& context) { + return [proto_config, &context](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter( + std::make_shared(proto_config, context.api().randomGenerator())); + }; +} + +/** + * Static registration for the KillRequest filter. @see RegisterFactory. + */ +REGISTER_FACTORY(KillRequestFilterFactory, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/kill_request/kill_request_config.h b/source/extensions/filters/http/kill_request/kill_request_config.h new file mode 100644 index 0000000000000..d7a6120e6b824 --- /dev/null +++ b/source/extensions/filters/http/kill_request/kill_request_config.h @@ -0,0 +1,31 @@ +#pragma once + +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.h" +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.validate.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { + +/** + * Config registration for KillRequestFilter. @see NamedHttpFilterConfigFactory. + */ +class KillRequestFilterFactory + : public Common::FactoryBase { +public: + KillRequestFilterFactory() : FactoryBase(HttpFilterNames::get().KillRequest) {} + +private: + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::kill_request::v3::KillRequest& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/kill_request/kill_request_filter.cc b/source/extensions/filters/http/kill_request/kill_request_filter.cc new file mode 100644 index 0000000000000..9c5c825a786a1 --- /dev/null +++ b/source/extensions/filters/http/kill_request/kill_request_filter.cc @@ -0,0 +1,38 @@ +#include "extensions/filters/http/kill_request/kill_request_filter.h" + +#include + +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { + +bool KillRequestFilter::isKillRequestEnabled() { + return ProtobufPercentHelper::evaluateFractionalPercent(kill_request_.probability(), + random_generator_.random()); +} + +Http::FilterHeadersStatus KillRequestFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + const auto kill_request_header = headers.get(KillRequestHeaders::get().KillRequest); + bool is_kill_request = false; + // This is an implicitly untrusted header, so per the API documentation only + // the first value is used. + if (kill_request_header.empty() || + !absl::SimpleAtob(kill_request_header[0]->value().getStringView(), &is_kill_request)) { + return Http::FilterHeadersStatus::Continue; + } + + if (is_kill_request && isKillRequestEnabled()) { + // Crash Envoy. + raise(SIGABRT); + } + + return Http::FilterHeadersStatus::Continue; +} + +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/kill_request/kill_request_filter.h b/source/extensions/filters/http/kill_request/kill_request_filter.h new file mode 100644 index 0000000000000..82243f819b7a8 --- /dev/null +++ b/source/extensions/filters/http/kill_request/kill_request_filter.h @@ -0,0 +1,92 @@ +#pragma once + +#include + +#include "envoy/common/random_generator.h" +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.h" +#include "envoy/http/filter.h" +#include "envoy/http/header_map.h" + +#include "common/http/headers.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { + +class KillRequestHeaderNameValues { +public: + const char* prefix() const { return ThreadSafeSingleton::get().prefix(); } + + const Http::LowerCaseString KillRequest{absl::StrCat(prefix(), "-kill-request")}; +}; + +using KillRequestHeaders = ConstSingleton; + +/** + * A filter that will crash Envoy if IsKillRequestEnabled() return true and + * incoming request contains HTTP KillRequest header with values in + * one of (case-insensitive) ["true", "t", "yes", "y", "1"]. + */ +class KillRequestFilter : public Http::StreamFilter, Logger::Loggable { +public: + KillRequestFilter( + const envoy::extensions::filters::http::kill_request::v3::KillRequest& kill_request, + Random::RandomGenerator& random_generator) + : kill_request_(kill_request), random_generator_(random_generator) {} + + ~KillRequestFilter() override = default; + + // Http::StreamFilterBase + void onDestroy() override {} + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override { + return Http::FilterDataStatus::Continue; + } + + Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap&) override { + return Http::FilterTrailersStatus::Continue; + } + + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks&) override {} + + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encode100ContinueHeaders(Http::ResponseHeaderMap&) override { + return Http::FilterHeadersStatus::Continue; + } + + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap&, bool) override { + return Http::FilterHeadersStatus::Continue; + } + + Http::FilterDataStatus encodeData(Buffer::Instance&, bool) override { + return Http::FilterDataStatus::Continue; + } + + Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap&) override { + return Http::FilterTrailersStatus::Continue; + } + + Http::FilterMetadataStatus encodeMetadata(Http::MetadataMap&) override { + return Http::FilterMetadataStatus::Continue; + } + + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks&) override {} + +private: + // Return a random boolean value, with probability configured in KillRequest + // equaling true. + bool isKillRequestEnabled(); + + const envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request_; + Random::RandomGenerator& random_generator_; +}; + +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index e869e3fc9bbd3..6aade9fadbd82 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -82,6 +82,8 @@ class HttpFilterNameValues { const std::string AwsLambda = "envoy.filters.http.aws_lambda"; // OAuth filter const std::string OAuth = "envoy.filters.http.oauth2"; + // KillRequest filter + const std::string KillRequest = "envoy.filters.http.kill_request"; }; using HttpFilterNames = ConstSingleton; diff --git a/test/config/integration/BUILD b/test/config/integration/BUILD index 1fbb9f235da1a..0c401ad01bb0a 100644 --- a/test/config/integration/BUILD +++ b/test/config/integration/BUILD @@ -42,5 +42,7 @@ filegroup( filegroup( name = "google_com_proxy_port_0", - srcs = ["google_com_proxy_port_0.yaml"], + srcs = [ + "google_com_proxy_port_0.yaml", + ], ) diff --git a/test/exe/BUILD b/test/exe/BUILD index 283086ab4799b..74178f65337cf 100644 --- a/test/exe/BUILD +++ b/test/exe/BUILD @@ -62,7 +62,10 @@ envoy_sh_test( envoy_cc_test( name = "main_common_test", srcs = ["main_common_test.cc"], - data = ["//test/config/integration:google_com_proxy_port_0"], + data = [ + "//test/config/integration:google_com_proxy_port_0", + "//test/exe/testdata:test_with_kill_request_filter", + ], deps = [ "//source/common/api:api_lib", "//source/exe:main_common_lib", diff --git a/test/exe/main_common_test.cc b/test/exe/main_common_test.cc index 8428cf6b43fe3..9685b6c66250c 100644 --- a/test/exe/main_common_test.cc +++ b/test/exe/main_common_test.cc @@ -455,4 +455,15 @@ TEST_P(MainCommonTest, ConstructDestructLogger) { Logger::Registry::getSink()->log(log_msg); } +// Verify KillRequest filter is not built into Envoy by default. +TEST_P(MainCommonTest, KillRequestFilterIsNotBuiltByDefault) { + config_file_ = TestEnvironment::temporaryFileSubstitute( + "test/exe/testdata/test_with_kill_request_filter.yaml", TestEnvironment::ParamMap(), + TestEnvironment::PortMap(), GetParam()); + argv_ = {"envoy-static", "--use-dynamic-base-id", "-c", config_file_.c_str(), nullptr}; + + EXPECT_THROW_WITH_REGEX(MainCommon main_common(argc(), argv()), EnvoyException, + "unknown type: envoy.extensions.filters.http.kill_request"); +} + } // namespace Envoy diff --git a/test/exe/testdata/BUILD b/test/exe/testdata/BUILD new file mode 100644 index 0000000000000..751268c411bcb --- /dev/null +++ b/test/exe/testdata/BUILD @@ -0,0 +1,15 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +filegroup( + name = "test_with_kill_request_filter", + srcs = [ + "test_with_kill_request_filter.yaml", + ], +) diff --git a/test/exe/testdata/test_with_kill_request_filter.yaml b/test/exe/testdata/test_with_kill_request_filter.yaml new file mode 100644 index 0000000000000..4eb8b0d8bde35 --- /dev/null +++ b/test/exe/testdata/test_with_kill_request_filter.yaml @@ -0,0 +1,34 @@ +admin: + access_log_path: "{{ null_device_path }}" + address: + socket_address: + address: "{{ ip_any_address }}" + port_value: 0 + +static_resources: + listeners: + - name: listener_0 + address: + socket_address: + address: "{{ ip_any_address }}" + port_value: 0 + filter_chains: + - filters: + - name: http + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: AUTO + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + http_filters: + - name: envoy.filters.http.kill_request + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.kill_request.v3.KillRequest + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + diff --git a/test/extensions/extensions_build_system.bzl b/test/extensions/extensions_build_system.bzl index b92680fced5aa..e7b3f6668c17d 100644 --- a/test/extensions/extensions_build_system.bzl +++ b/test/extensions/extensions_build_system.bzl @@ -1,14 +1,17 @@ load("//bazel:envoy_build_system.bzl", "envoy_benchmark_test", "envoy_cc_benchmark_binary", "envoy_cc_mock", "envoy_cc_test", "envoy_cc_test_binary", "envoy_cc_test_library") -load("@envoy_build_config//:extensions_build_config.bzl", "EXTENSIONS") +load("@envoy_build_config//:extensions_build_config.bzl", "DISABLED_BY_DEFAULT_EXTENSIONS", "EXTENSIONS") + +def extension_allowed_for_test(extension_name): + return extension_name in EXTENSIONS or extension_name in DISABLED_BY_DEFAULT_EXTENSIONS # All extension tests should use this version of envoy_cc_test(). It allows compiling out # tests for extensions that the user does not wish to include in their build. -# @param extension_name should match an extension listed in EXTENSIONS. +# @param extension_name should match an extension listed in EXTENSIONS or DISABLED_BY_DEFAULT_EXTENSIONS. def envoy_extension_cc_test( name, extension_name, **kwargs): - if not extension_name in EXTENSIONS: + if not extension_allowed_for_test(extension_name): return envoy_cc_test(name, **kwargs) @@ -17,7 +20,7 @@ def envoy_extension_cc_test_library( name, extension_name, **kwargs): - if not extension_name in EXTENSIONS: + if not extension_allowed_for_test(extension_name): return envoy_cc_test_library(name, **kwargs) @@ -26,7 +29,7 @@ def envoy_extension_cc_mock( name, extension_name, **kwargs): - if not extension_name in EXTENSIONS: + if not extension_allowed_for_test(extension_name): return envoy_cc_mock(name, **kwargs) @@ -35,7 +38,7 @@ def envoy_extension_cc_test_binary( name, extension_name, **kwargs): - if not extension_name in EXTENSIONS: + if not extension_allowed_for_test(extension_name): return envoy_cc_test_binary(name, **kwargs) @@ -44,7 +47,7 @@ def envoy_extension_cc_benchmark_binary( name, extension_name, **kwargs): - if not extension_name in EXTENSIONS: + if not extension_allowed_for_test(extension_name): return envoy_cc_benchmark_binary(name, **kwargs) @@ -53,7 +56,7 @@ def envoy_extension_benchmark_test( name, extension_name, **kwargs): - if not extension_name in EXTENSIONS: + if not extension_allowed_for_test(extension_name): return envoy_benchmark_test(name, **kwargs) diff --git a/test/extensions/filters/http/kill_request/BUILD b/test/extensions/filters/http/kill_request/BUILD new file mode 100644 index 0000000000000..31d3857c26ae2 --- /dev/null +++ b/test/extensions/filters/http/kill_request/BUILD @@ -0,0 +1,49 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "kill_request_filter_test", + srcs = ["kill_request_filter_test.cc"], + extension_name = "envoy.filters.http.kill_request", + deps = [ + "//include/envoy/http:metadata_interface", + "//source/common/buffer:buffer_lib", + "//source/extensions/filters/http/kill_request:kill_request_filter_lib", + "//test/mocks:common_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/kill_request/v3:pkg_cc_proto", + "@envoy_api//envoy/type/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "kill_request_config_test", + srcs = ["kill_request_config_test.cc"], + extension_name = "envoy.filters.http.kill_request", + deps = [ + "//source/extensions/filters/http/kill_request:kill_request_config", + "//test/mocks/server:factory_context_mocks", + "@envoy_api//envoy/extensions/filters/http/kill_request/v3:pkg_cc_proto", + "@envoy_api//envoy/type/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "kill_request_filter_integration_test", + srcs = ["kill_request_filter_integration_test.cc"], + extension_name = "envoy.filters.http.kill_request", + deps = [ + "//source/extensions/filters/http/kill_request:kill_request_config", + "//test/integration:http_protocol_integration_lib", + ], +) diff --git a/test/extensions/filters/http/kill_request/kill_request_config_test.cc b/test/extensions/filters/http/kill_request/kill_request_config_test.cc new file mode 100644 index 0000000000000..a803ae45a3473 --- /dev/null +++ b/test/extensions/filters/http/kill_request/kill_request_config_test.cc @@ -0,0 +1,46 @@ +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.h" +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.validate.h" +#include "envoy/type/v3/percent.pb.h" + +#include "extensions/filters/http/kill_request/kill_request_config.h" + +#include "test/mocks/server/factory_context.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { +namespace { + +using testing::_; + +TEST(KillRequestConfigTest, KillRequestFilterWithCorrectProto) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + kill_request.mutable_probability()->set_numerator(100); + + NiceMock context; + KillRequestFilterFactory factory; + Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(kill_request, "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); +} + +TEST(KillRequestConfigTest, KillRequestFilterWithEmptyProto) { + NiceMock context; + KillRequestFilterFactory factory; + Http::FilterFactoryCb cb = + factory.createFilterFactoryFromProto(*factory.createEmptyConfigProto(), "stats", context); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamFilter(_)); + cb(filter_callback); +} + +} // namespace +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/kill_request/kill_request_filter_integration_test.cc b/test/extensions/filters/http/kill_request/kill_request_filter_integration_test.cc new file mode 100644 index 0000000000000..35b011a858b5e --- /dev/null +++ b/test/extensions/filters/http/kill_request/kill_request_filter_integration_test.cc @@ -0,0 +1,95 @@ +#include "test/integration/http_protocol_integration.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { +namespace { + +class KillRequestFilterIntegrationTest : public Event::TestUsingSimulatedTime, + public HttpProtocolIntegrationTest { +protected: + void initializeFilter(const std::string& filter_config) { + config_helper_.addFilter(filter_config); + initialize(); + } + + const std::string filter_config_ = + R"EOF( +name: envoy.filters.http.kill_request +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.kill_request.v3.KillRequest + probability: + numerator: 100 +)EOF"; +}; + +// Tests should run with all protocols. +class KillRequestFilterIntegrationTestAllProtocols : public KillRequestFilterIntegrationTest {}; +INSTANTIATE_TEST_SUITE_P(Protocols, KillRequestFilterIntegrationTestAllProtocols, + testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()), + HttpProtocolIntegrationTest::protocolTestParamsToString); + +// Request abort controlled via header configuration. +TEST_P(KillRequestFilterIntegrationTestAllProtocols, KillRequestCrashEnvoy) { + initializeFilter(filter_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-envoy-kill-request", "true"}}; + + EXPECT_DEATH(sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 1024), + ""); +} + +TEST_P(KillRequestFilterIntegrationTestAllProtocols, KillRequestDisabledWhenHeaderIsMissing) { + initializeFilter(filter_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + auto response = + sendRequestAndWaitForResponse(default_request_headers_, 0, default_response_headers_, 1024); +} + +TEST_P(KillRequestFilterIntegrationTestAllProtocols, KillRequestDisabledWhenHeaderValueIsInvalid) { + initializeFilter(filter_config_); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-envoy-kill-request", "invalid"}}; + + auto response = + sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 1024); +} + +TEST_P(KillRequestFilterIntegrationTestAllProtocols, KillRequestDisabledByZeroProbability) { + const std::string zero_probability_filter_config = + R"EOF( +name: envoy.filters.http.kill_request +typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.kill_request.v3.KillRequest + probability: + numerator: 0 +)EOF"; + + initializeFilter(zero_probability_filter_config); + codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http"))); + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {":path", "/test/long/url"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-envoy-kill-request", "true"}}; + + auto response = + sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 1024); +} + +} // namespace +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/kill_request/kill_request_filter_test.cc b/test/extensions/filters/http/kill_request/kill_request_filter_test.cc new file mode 100644 index 0000000000000..ff101f241c0fb --- /dev/null +++ b/test/extensions/filters/http/kill_request/kill_request_filter_test.cc @@ -0,0 +1,125 @@ +#include "envoy/extensions/filters/http/kill_request/v3/kill_request.pb.h" +#include "envoy/http/metadata_interface.h" +#include "envoy/type/v3/percent.pb.h" + +#include "common/buffer/buffer_impl.h" + +#include "extensions/filters/http/kill_request/kill_request_filter.h" + +#include "test/mocks/common.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace KillRequest { +namespace { + +using ::testing::Return; + +class KillRequestFilterTest : public testing::Test { +protected: + void + setUpTest(const envoy::extensions::filters::http::kill_request::v3::KillRequest& kill_request) { + filter_ = std::make_unique(kill_request, random_generator_); + } + + std::unique_ptr filter_; + testing::NiceMock random_generator_; + Http::TestRequestHeaderMapImpl request_headers_; +}; + +TEST_F(KillRequestFilterTest, KillRequestCrashEnvoy) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + kill_request.mutable_probability()->set_numerator(1); + setUpTest(kill_request); + request_headers_.addCopy("x-envoy-kill-request", "true"); + + ON_CALL(random_generator_, random()).WillByDefault(Return(0)); + EXPECT_DEATH(filter_->decodeHeaders(request_headers_, false), ""); +} + +TEST_F(KillRequestFilterTest, KillRequestWithMillionDenominatorCrashEnvoy) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + kill_request.mutable_probability()->set_numerator(1); + kill_request.mutable_probability()->set_denominator(envoy::type::v3::FractionalPercent::MILLION); + setUpTest(kill_request); + request_headers_.addCopy("x-envoy-kill-request", "yes"); + + ON_CALL(random_generator_, random()).WillByDefault(Return(0)); + EXPECT_DEATH(filter_->decodeHeaders(request_headers_, false), ""); +} + +TEST_F(KillRequestFilterTest, KillRequestDisabledWhenIsKillRequestEnabledReturnsFalse) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + kill_request.mutable_probability()->set_numerator(0); + setUpTest(kill_request); + request_headers_.addCopy("x-envoy-kill-request", "true"); + + ON_CALL(random_generator_, random()).WillByDefault(Return(1)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); +} + +TEST_F(KillRequestFilterTest, KillRequestDisabledWhenHeaderIsMissing) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + kill_request.mutable_probability()->set_numerator(100); + setUpTest(kill_request); + + ON_CALL(random_generator_, random()).WillByDefault(Return(0)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); +} + +TEST_F(KillRequestFilterTest, KillRequestDisabledWhenHeaderValueIsInvalid) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + kill_request.mutable_probability()->set_numerator(100); + setUpTest(kill_request); + request_headers_.addCopy("x-envoy-kill-request", "invalid"); + + ON_CALL(random_generator_, random()).WillByDefault(Return(0)); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, false)); +} + +TEST_F(KillRequestFilterTest, DecodeDataReturnsContinue) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + setUpTest(kill_request); + Buffer::OwnedImpl data; + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data, false)); +} + +TEST_F(KillRequestFilterTest, DecodeTrailersReturnsContinue) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + setUpTest(kill_request); + Http::TestRequestTrailerMapImpl request_trailers; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); +} + +TEST_F(KillRequestFilterTest, Encode100ContinueHeadersReturnsContinue) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + setUpTest(kill_request); + Http::TestResponseHeaderMapImpl response_headers; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, + filter_->encode100ContinueHeaders(response_headers)); +} + +TEST_F(KillRequestFilterTest, EncodeTrailersReturnsContinue) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + setUpTest(kill_request); + Http::TestResponseTrailerMapImpl response_trailers; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers)); +} + +TEST_F(KillRequestFilterTest, EncodeMetadataReturnsContinue) { + envoy::extensions::filters::http::kill_request::v3::KillRequest kill_request; + setUpTest(kill_request); + Http::MetadataMap metadata_map; + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->encodeMetadata(metadata_map)); +} + +} // namespace +} // namespace KillRequest +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/per_file_coverage.sh b/test/per_file_coverage.sh index 7466b4226a2d6..4232a3cbbf6e5 100755 --- a/test/per_file_coverage.sh +++ b/test/per_file_coverage.sh @@ -40,6 +40,7 @@ declare -a KNOWN_LOW_COVERAGE=( "source/extensions/filters/http/dynamic_forward_proxy:94.9" "source/extensions/filters/http/grpc_json_transcoder:93.3" "source/extensions/filters/http/ip_tagging:91.2" +"source/extensions/filters/http/kill_request:94.4" # Death tests don't report LCOV "source/extensions/filters/http/oauth2:96.5" "source/extensions/filters/listener:96.0" "source/extensions/filters/listener/http_inspector:93.3" diff --git a/tools/dependency/validate.py b/tools/dependency/validate.py index 92178b450074d..df4a6c43520b4 100755 --- a/tools/dependency/validate.py +++ b/tools/dependency/validate.py @@ -144,7 +144,10 @@ def ListExtensions(self): Returns: Dictionary items from source/extensions/extensions_build_config.bzl. """ - return extensions_build_config.EXTENSIONS.items() + all_extensions = {} + all_extensions.update(extensions_build_config.EXTENSIONS) + all_extensions.update(extensions_build_config.DISABLED_BY_DEFAULT_EXTENSIONS) + return all_extensions.items() class Validator(object):