diff --git a/docs/configuration/http_conn_man/headers.rst b/docs/configuration/http_conn_man/headers.rst index 1b257386a5643..24020d0ae40d5 100644 --- a/docs/configuration/http_conn_man/headers.rst +++ b/docs/configuration/http_conn_man/headers.rst @@ -99,6 +99,13 @@ the header value to *true*. This is a convenience to avoid having to parse and understand XFF. +.. _config_http_conn_man_headers_x-envoy-ip-tags: + +x-envoy-ip-tags +--------------- + +This header is populated by the :ref:`Ip Tagging Filter`. Behavior is under development. + .. _config_http_conn_man_headers_x-forwarded-for: x-forwarded-for diff --git a/docs/configuration/http_filters/http_filters.rst b/docs/configuration/http_filters/http_filters.rst index 7efb0bfc73aa8..8834b1cbbeb3a 100644 --- a/docs/configuration/http_filters/http_filters.rst +++ b/docs/configuration/http_filters/http_filters.rst @@ -12,5 +12,6 @@ HTTP filters grpc_http1_bridge_filter grpc_web_filter health_check_filter + ip_tagging_filter rate_limit_filter router_filter diff --git a/docs/configuration/http_filters/ip_tagging_filter.rst b/docs/configuration/http_filters/ip_tagging_filter.rst new file mode 100644 index 0000000000000..0810d748132a0 --- /dev/null +++ b/docs/configuration/http_filters/ip_tagging_filter.rst @@ -0,0 +1,48 @@ +.. _config_http_filters_ip_tagging: + +Ip tagging filter +==================== + +This is an HTTP filter which enables Envoy to tag requests with extra information such as location, cloud source, and any +extra data. This is useful to prevent against DDoS. + +**Note**: this filter is under active development, and currently does not perform any tagging on requests. In other +words, installing this filter is a no-op in the filter chain. + +.. code-block:: json + + { + "type": "decoder", + "name": "ip_tagging", + "config": { + "request_type": "...", + "ip_tags": [] + } + } + +request_type + *(optional, string)* The type of requests the filter should apply to. The supported + types are *internal*, *external* or *both*. A request is considered internal if + :ref:`x-envoy-internal` is set to true. If + :ref:`x-envoy-internal` is not set or false, a + request is considered external. The filter defaults to *both*, and it will apply to all request + types. + +ip_tags: + *(optional, array)* Specifies the list of ip tags to set for a request. + +Ip tags +------- +.. code-block:: json + + { + "ip_tag_name": "...", + "ip_list": [] + } + +ip_tag_name: + *(required, string)* Specifies the ip tag name to apply. + +ip_list: + *(required, list of strings)* A list of IP address and subnet masks that will be tagged with the ``ip_tag_name``. Both + IPv4 and IPv6 CIDR addresses are allowed here. diff --git a/source/common/http/filter/BUILD b/source/common/http/filter/BUILD index d01ea3ca42076..4d7bb76a3f39d 100644 --- a/source/common/http/filter/BUILD +++ b/source/common/http/filter/BUILD @@ -49,6 +49,19 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "ip_tagging_filter_lib", + srcs = ["ip_tagging_filter.cc"], + hdrs = ["ip_tagging_filter.h"], + deps = [ + "//include/envoy/http:filter_interface", + "//include/envoy/json:json_object_interface", + "//source/common/common:assert_lib", + "//source/common/json:config_schemas_lib", + "//source/common/json:json_validator_lib", + ], +) + envoy_cc_library( name = "ratelimit_lib", srcs = ["ratelimit.cc"], diff --git a/source/common/http/filter/ip_tagging_filter.cc b/source/common/http/filter/ip_tagging_filter.cc new file mode 100644 index 0000000000000..f8d292ded42d9 --- /dev/null +++ b/source/common/http/filter/ip_tagging_filter.cc @@ -0,0 +1,29 @@ +#include "common/http/filter/ip_tagging_filter.h" + +namespace Envoy { +namespace Http { + +IpTaggingFilter::IpTaggingFilter(IpTaggingFilterConfigSharedPtr config) : config_(config) {} + +IpTaggingFilter::~IpTaggingFilter() {} + +void IpTaggingFilter::onDestroy() {} + +FilterHeadersStatus IpTaggingFilter::decodeHeaders(HeaderMap&, bool) { + return FilterHeadersStatus::Continue; +} + +FilterDataStatus IpTaggingFilter::decodeData(Buffer::Instance&, bool) { + return FilterDataStatus::Continue; +} + +FilterTrailersStatus IpTaggingFilter::decodeTrailers(HeaderMap&) { + return FilterTrailersStatus::Continue; +} + +void IpTaggingFilter::setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) { + callbacks_ = &callbacks; +} + +} // Http +} // Envoy diff --git a/source/common/http/filter/ip_tagging_filter.h b/source/common/http/filter/ip_tagging_filter.h new file mode 100644 index 0000000000000..13841d6fb1577 --- /dev/null +++ b/source/common/http/filter/ip_tagging_filter.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include + +#include "envoy/http/filter.h" +#include "envoy/json/json_object.h" + +#include "common/common/assert.h" +#include "common/json/config_schemas.h" +#include "common/json/json_validator.h" + +namespace Envoy { +namespace Http { + +/** + * Type of requests the filter should apply to. + */ +enum class FilterRequestType { Internal, External, Both }; + +/** + * Configuration for the ip tagging filter. + */ +class IpTaggingFilterConfig : Json::Validator { +public: + IpTaggingFilterConfig(const Json::Object& json_config) + : Json::Validator(json_config, Json::Schema::IP_TAGGING_HTTP_FILTER_SCHEMA), + request_type_(stringToType(json_config.getString("request_type", "both"))) {} + + FilterRequestType requestType() const { return request_type_; } + +private: + static FilterRequestType stringToType(const std::string& request_type) { + if (request_type == "internal") { + return FilterRequestType::Internal; + } else if (request_type == "external") { + return FilterRequestType::External; + } else { + ASSERT(request_type == "both"); + return FilterRequestType::Both; + } + } + + const FilterRequestType request_type_; +}; + +typedef std::shared_ptr IpTaggingFilterConfigSharedPtr; + +/** + * A filter that tags requests via the x-envoy-ip-tags header based on the request's trusted XFF + * address. + */ +class IpTaggingFilter : public StreamDecoderFilter { +public: + IpTaggingFilter(IpTaggingFilterConfigSharedPtr config); + ~IpTaggingFilter(); + + // Http::StreamFilterBase + void onDestroy() override; + + // Http::StreamDecoderFilter + FilterHeadersStatus decodeHeaders(HeaderMap& headers, bool end_stream) override; + FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) override; + FilterTrailersStatus decodeTrailers(HeaderMap& trailers) override; + void setDecoderFilterCallbacks(StreamDecoderFilterCallbacks& callbacks) override; + +private: + IpTaggingFilterConfigSharedPtr config_; + StreamDecoderFilterCallbacks* callbacks_{}; +}; + +} // Http +} // Envoy diff --git a/source/common/json/config_schemas.cc b/source/common/json/config_schemas.cc index dc90186bbc0f2..33e83729cf815 100644 --- a/source/common/json/config_schemas.cc +++ b/source/common/json/config_schemas.cc @@ -832,6 +832,39 @@ const std::string Json::Schema::FAULT_HTTP_FILTER_SCHEMA(R"EOF( } )EOF"); +const std::string Json::Schema::IP_TAGGING_HTTP_FILTER_SCHEMA(R"EOF( + { + "$schema": "http://json-schema.org/schema#", + "type" : "object", + "properties" : { + "request_type" : { + "type" : "string", + "enum" : ["internal", "external", "both"] + }, + "ip_tags" : { + "type" : "array", + "minItems" : 1, + "uniqueItems" : true, + "items" : { + "type" : "object", + "properties" : { + "ip_tag_name" : { "type" : "string" }, + "ip_list" : { + "type" : "array", + "minItems" : 1, + "uniqueItems" : true, + "items" : { "type" : "string" } + } + }, + "required" : ["ip_tag_name", "ip_list"], + "additionalProperties" : false + } + } + }, + "additionalProperties" : false + } + )EOF"); + const std::string Json::Schema::HEALTH_CHECK_HTTP_FILTER_SCHEMA(R"EOF( { "$schema": "http://json-schema.org/schema#", diff --git a/source/common/json/config_schemas.h b/source/common/json/config_schemas.h index 7037390e93108..414a9044b192d 100644 --- a/source/common/json/config_schemas.h +++ b/source/common/json/config_schemas.h @@ -33,6 +33,7 @@ class Schema { static const std::string BUFFER_HTTP_FILTER_SCHEMA; static const std::string FAULT_HTTP_FILTER_SCHEMA; static const std::string HEALTH_CHECK_HTTP_FILTER_SCHEMA; + static const std::string IP_TAGGING_HTTP_FILTER_SCHEMA; static const std::string RATE_LIMIT_HTTP_FILTER_SCHEMA; static const std::string ROUTER_HTTP_FILTER_SCHEMA; diff --git a/source/server/config/http/BUILD b/source/server/config/http/BUILD index e545a1c99cf37..3829c15d3c784 100644 --- a/source/server/config/http/BUILD +++ b/source/server/config/http/BUILD @@ -64,6 +64,18 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "ip_tagging_lib", + srcs = ["ip_tagging.cc"], + hdrs = ["ip_tagging.h"], + deps = [ + "//include/envoy/server:instance_interface", + "//source/common/http/filter:ip_tagging_filter_lib", + "//source/common/json:config_schemas_lib", + "//source/server/config/network:http_connection_manager_lib", + ], +) + envoy_cc_library( name = "lightstep_lib", srcs = ["lightstep_http_tracer.cc"], diff --git a/source/server/config/http/ip_tagging.cc b/source/server/config/http/ip_tagging.cc new file mode 100644 index 0000000000000..c4cbaea85ad2c --- /dev/null +++ b/source/server/config/http/ip_tagging.cc @@ -0,0 +1,37 @@ +#include "server/config/http/ip_tagging.h" + +#include + +#include "common/http/filter/ip_tagging_filter.h" +#include "common/json/config_schemas.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +HttpFilterFactoryCb IpTaggingFilterConfig::createFilterFactory(HttpFilterType type, + const Json::Object& json_config, + const std::string&, + Server::Instance&) { + if (type != HttpFilterType::Decoder) { + throw EnvoyException( + fmt::format("{} ip tagging filter must be configured as a decoder filter.", name())); + } + + Http::IpTaggingFilterConfigSharedPtr config(new Http::IpTaggingFilterConfig(json_config)); + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter( + Http::StreamDecoderFilterSharedPtr{new Http::IpTaggingFilter(config)}); + }; +} + +std::string IpTaggingFilterConfig::name() { return "ip_tagging"; } + +/** + * Static registration for the ip tagging filter. @see RegisterNamedHttpFilterConfigFactory. + */ +static RegisterNamedHttpFilterConfigFactory register_; + +} // Configuration +} // Server +} // Envoy diff --git a/source/server/config/http/ip_tagging.h b/source/server/config/http/ip_tagging.h new file mode 100644 index 0000000000000..1be838ed71e0b --- /dev/null +++ b/source/server/config/http/ip_tagging.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "envoy/server/instance.h" + +#include "server/config/network/http_connection_manager.h" + +namespace Envoy { +namespace Server { +namespace Configuration { + +/** + * Config registration for the router filter. @see NamedHttpFilterConfigFactory. + */ +class IpTaggingFilterConfig : public NamedHttpFilterConfigFactory { +public: + HttpFilterFactoryCb createFilterFactory(HttpFilterType type, const Json::Object& json_config, + const std::string& stat_prefix, + Server::Instance& server) override; + + std::string name() override; +}; + +} // Configuration +} // Server +} // Envoy diff --git a/test/server/config/http/BUILD b/test/server/config/http/BUILD index 845f205d4f29f..f288f74fdc533 100644 --- a/test/server/config/http/BUILD +++ b/test/server/config/http/BUILD @@ -17,6 +17,7 @@ envoy_cc_test( "//source/server/config/http:fault_lib", "//source/server/config/http:grpc_http1_bridge_lib", "//source/server/config/http:grpc_web_lib", + "//source/server/config/http:ip_tagging_lib", "//source/server/config/http:lightstep_lib", "//source/server/config/http:ratelimit_lib", "//source/server/config/http:router_lib", diff --git a/test/server/config/http/config_test.cc b/test/server/config/http/config_test.cc index 66ad037da01a3..b141ac9351ba5 100644 --- a/test/server/config/http/config_test.cc +++ b/test/server/config/http/config_test.cc @@ -5,6 +5,7 @@ #include "server/config/http/fault.h" #include "server/config/http/grpc_http1_bridge.h" #include "server/config/http/grpc_web.h" +#include "server/config/http/ip_tagging.h" #include "server/config/http/lightstep_http_tracer.h" #include "server/config/http/ratelimit.h" #include "server/config/http/router.h" @@ -193,6 +194,46 @@ TEST(HttpFilterConfigTest, BadRouterFilterConfig) { Json::Exception); } +TEST(HttpFilterConfigTest, IpTaggingFilter) { + std::string json_string = R"EOF( + { + "request_type" : "internal", + "ip_tags" : [ + { "ip_tag_name" : "example_tag", + "ip_list" : ["0.0.0.0"] + } + ] + } + )EOF"; + + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json_string); + NiceMock server; + IpTaggingFilterConfig factory; + HttpFilterFactoryCb cb = + factory.createFilterFactory(HttpFilterType::Decoder, *json_config, "stats", server); + Http::MockFilterChainFactoryCallbacks filter_callback; + EXPECT_CALL(filter_callback, addStreamDecoderFilter(_)); + cb(filter_callback); +} + +TEST(HttpFilterConfigTest, BadIpTaggingFilterConfig) { + std::string json_string = R"EOF( + { + "request_type" : "internal", + "ip_tags" : [ + { "ip_tag_name" : "example_tag" + } + ] + } + )EOF"; + + Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json_string); + NiceMock server; + IpTaggingFilterConfig factory; + EXPECT_THROW(factory.createFilterFactory(HttpFilterType::Decoder, *json_config, "stats", server), + Json::Exception); +} + TEST(HttpFilterConfigTest, DoubleRegistrationTest) { EXPECT_THROW_WITH_MESSAGE( RegisterNamedHttpFilterConfigFactory(), EnvoyException,