diff --git a/api/envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.proto b/api/envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.proto index 87f725e1c8c25..292ae686cb259 100644 --- a/api/envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.proto +++ b/api/envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.proto @@ -3,6 +3,9 @@ syntax = "proto3"; package envoy.extensions.filters.http.ip_tagging.v3; import "envoy/config/core/v3/address.proto"; +import "envoy/config/core/v3/base.proto"; + +import "google/protobuf/duration.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; @@ -18,7 +21,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // IP tagging :ref:`configuration overview `. // [#extension: envoy.filters.http.ip_tagging] -// [#next-free-field: 6] +// [#next-free-field: 7] message IPTagging { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.ip_tagging.v2.IPTagging"; @@ -53,6 +56,12 @@ message IPTagging { repeated config.core.v3.CidrRange ip_list = 2; } + // Specifies the content of the IP tag file. + // Allow the file to be created with no IP tags. + message IPTags { + repeated IPTag ip_tags = 1; + } + // Specify to which header the tags will be written. message IpTagHeader { // Describes how to apply the tags to the headers. @@ -85,16 +94,33 @@ message IPTagging { HeaderAction action = 2; } + // Common configuration for file based ip tags. + message IpTagsFileProvider { + // Data source from which to retrieve ip tags. + // Only filename based data source is currently supported for IP tags. + // When using this data source, if a ``watched_directory`` is provided, the IP tags file will be re-read when a file move is detected. + // See :ref:`watched_directory ` for more information about the ``watched_directory`` field. + config.core.v3.DataSource ip_tags_datasource = 1; + + // When :ref:`ip_tags ` is configured + // the ip tags will be reloaded from file every ``ip_tags_refresh_rate`` milliseconds. + // Defaults to 0, in this case no refresh will be attempted. + google.protobuf.Duration ip_tags_refresh_rate = 2 [(validate.rules).duration = {gt {}}]; + } + // The type of request the filter should apply to. RequestType request_type = 1 [(validate.rules).enum = {defined_only: true}]; - // [#comment:TODO(ccaraman): Extend functionality to load IP tags from file system. - // Tracked by issue https://github.com/envoyproxy/envoy/issues/2695] // The set of IP tags for the filter. - repeated IPTag ip_tags = 4 [(validate.rules).repeated = {min_items: 1}]; + // Only one of :ref:`ip_tags ` + // or :ref:`ip_tags_datasource ` + // can be set for the IP Tagging filter. + repeated IPTag ip_tags = 4; // Specify to which header the tags will be written. // // If left unspecified, the tags will be appended to the ``x-envoy-ip-tags`` header. IpTagHeader ip_tag_header = 5; + + IpTagsFileProvider ip_tags_file_provider = 6; } diff --git a/docs/BUILD b/docs/BUILD index 2e317b672a110..b5ff3085c1635 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -33,6 +33,7 @@ filegroup( "root/configuration/listeners/network_filters/_include/generic_proxy_filter.yaml", "root/configuration/overview/_include/xds_api/oauth-sds-example.yaml", "root/configuration/security/_include/sds-source-example.yaml", + "root/configuration/http/http_filters/_include/ip-tagging-filter.yaml", ], ) + select({ "//bazel:windows_x86_64": [], diff --git a/docs/root/configuration/http/http_filters/_include/ip-tagging-filter.yaml b/docs/root/configuration/http/http_filters/_include/ip-tagging-filter.yaml new file mode 100644 index 0000000000000..0b47430163ea8 --- /dev/null +++ b/docs/root/configuration/http/http_filters/_include/ip-tagging-filter.yaml @@ -0,0 +1,115 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + http_filters: + - name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - domains: + - '*' + name: local_service + routes: + - match: {prefix: "/"} + route: {cluster: default_service} + - address: + socket_address: + address: 0.0.0.0 + port_value: 9000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress1_http + http_filters: + - name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags_file_provider: + ip_tags_refresh_rate: 5s + ip_tags_datasource: + filename: "/geoip/ip-tags.yaml" + watched_directory: + path: "/geoip/" + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - domains: + - '*' + name: local_service + routes: + - match: {prefix: "/"} + route: {cluster: default_service} + - address: + socket_address: + address: 0.0.0.0 + port_value: 7000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress2_http + http_filters: + - name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags_file_provider: + ip_tags_refresh_rate: 5s + ip_tags_datasource: + filename: "/geoip/ip-tags.json" + watched_directory: + path: "/geoip/" + - name: envoy.filters.http.router + typed_config: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + route_config: + name: local_route + virtual_hosts: + - domains: + - '*' + name: local_service + routes: + - match: {prefix: "/"} + route: {cluster: default_service} + clusters: + - name: default_service + load_assignment: + cluster_name: default_service + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: 127.0.0.1 + port_value: 10001 +admin: + address: + socket_address: + address: 0.0.0.0 + port_value: 9901 diff --git a/docs/root/configuration/http/http_filters/ip_tagging_filter.rst b/docs/root/configuration/http/http_filters/ip_tagging_filter.rst index 73e4a87c05dfd..59191a59ea404 100644 --- a/docs/root/configuration/http/http_filters/ip_tagging_filter.rst +++ b/docs/root/configuration/http/http_filters/ip_tagging_filter.rst @@ -22,12 +22,83 @@ described in the paper `IP-address lookup using LC-tries `_ by S. Nilsson and G. Karlsson. +IP tags can either be provided directly using the :ref:`ip_tags ` API field or +can be loaded from file if :ref:`ip_tags_datasource ` API field is configured. +For file based IP tags YAML and JSON file formats are supported. +IP tags will be dynamically reloaded if ``watched_directory`` is configured for :ref:`ip_tags_datasource ` +and :ref:`ip_tags_refresh_rate ` is set to value greater than zero. Configuration ------------- * This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging``. * :ref:`v3 API reference ` +An example configuration of the filter with inline ip tags may look like the following: + +.. literalinclude:: _include/ip-tagging-filter.yaml + :language: yaml + :lines: 13-21 + :lineno-start: 13 + :linenos: + :caption: :download:`ip-tagging-filter.yaml <_include/ip-tagging-filter.yaml>` + +Below is an example configuration of the filter with the file based ip tags in yaml format: + +.. literalinclude:: _include/ip-tagging-filter.yaml + :language: yaml + :lines: 44-54 + :lineno-start: 44 + :linenos: + :caption: :download:`ip-tagging-filter.yaml <_include/ip-tagging-filter.yaml>` + +Where the *ip-tags.yaml* file would have the following content: + +.. code-block:: yaml + + ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} + +And here is an example configuration of the filter with the file based IP tags in JSON format: + +.. literalinclude:: _include/ip-tagging-filter.yaml + :language: yaml + :lines: 77-87 + :lineno-start: 77 + :linenos: + :caption: :download:`ip-tagging-filter.yaml <_include/ip-tagging-filter.yaml>` + +Where the ``ip-tags.json`` file would have the following content: + +.. code-block:: json + + { + "ip_tags": [ + { + "ip_tag_name": "external_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + }, + { + "ip_tag_name": "internal_request", + "ip_list": [ + { + "address_prefix": "1.2.3.5", + "prefix_len": 32 + } + ] + } + ] + } + Statistics ---------- @@ -41,6 +112,8 @@ the owning HTTP connection manager. .hit, Counter, Total number of requests that have the ```` applied to it no_hit, Counter, Total number of requests with no applicable IP tags total, Counter, Total number of requests the IP Tagging Filter operated on + reload_success, Counter, Total number of successful reloads of IP tags file + reload_error, Counter, Total number of failed reloads of IP tags file Runtime ------- diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 4e9b2f861a56d..af09b994aa78c 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -100,7 +100,8 @@ const static bool should_log = true; FUNCTION(websocket) \ FUNCTION(golang) \ FUNCTION(stats_sinks) \ - FUNCTION(dynamic_modules) + FUNCTION(dynamic_modules) \ + FUNCTION(ip_tagging) // clang-format off enum class Id { diff --git a/source/common/protobuf/utility.h b/source/common/protobuf/utility.h index 1c5ae2b989d5e..635539a0c8676 100644 --- a/source/common/protobuf/utility.h +++ b/source/common/protobuf/utility.h @@ -271,6 +271,8 @@ class MessageUtil { static void loadFromJson(absl::string_view json, Protobuf::Struct& message); static void loadFromYaml(const std::string& yaml, Protobuf::Message& message, ProtobufMessage::ValidationVisitor& validation_visitor); + static absl::Status loadFromYamlNoThrow(const std::string& yaml, Protobuf::Message& message, + ProtobufMessage::ValidationVisitor& validation_visitor); #endif // This function attempts to load Envoy configuration from the specified file diff --git a/source/common/protobuf/yaml_utility.cc b/source/common/protobuf/yaml_utility.cc index 2ee35579d4834..0512bfac09afd 100644 --- a/source/common/protobuf/yaml_utility.cc +++ b/source/common/protobuf/yaml_utility.cc @@ -113,6 +113,20 @@ void jsonConvertInternal(const Protobuf::Message& source, MessageUtil::loadFromJson(json, dest, validation_visitor); } +absl::Status jsonConvertInternalNoThrow(const Protobuf::Message& source, + ProtobufMessage::ValidationVisitor& validation_visitor, + Protobuf::Message& dest) { + absl::Status conversion_status = absl::OkStatus(); + Protobuf::util::JsonPrintOptions json_options; + json_options.preserve_proto_field_names = true; + std::string json; + conversion_status = Protobuf::util::MessageToJsonString(source, &json, json_options); + if (conversion_status.ok()) { + MessageUtil::loadFromJson(json, dest, validation_visitor); + } + return conversion_status; +} + } // namespace void MessageUtil::loadFromJson(absl::string_view json, Protobuf::Message& message, @@ -181,6 +195,38 @@ void MessageUtil::loadFromYaml(const std::string& yaml, Protobuf::Message& messa throw EnvoyException("Unable to convert YAML as JSON: " + yaml); } +absl::Status +MessageUtil::loadFromYamlNoThrow(const std::string& yaml, Protobuf::Message& message, + ProtobufMessage::ValidationVisitor& validation_visitor) { + absl::Status load_status = absl::OkStatus(); + Protobuf::Value value; + TRY_NEEDS_AUDIT { value = parseYamlNode(YAML::Load(yaml)); } + END_TRY + catch (YAML::ParserException& e) { + load_status = absl::InvalidArgumentError(fmt::format("Failed to parse yaml: {}", e.what())); + } + catch (YAML::BadConversion& e) { + load_status = + absl::InvalidArgumentError(fmt::format("Failed to convert to yaml: {}", e.what())); + } + catch (std::exception& e) { + // There is a potentially wide space of exceptions thrown by the YAML parser, + // and enumerating them all may be difficult. Envoy doesn't work well with + // unhandled exceptions, so we capture them and record the exception name in + // the status. + load_status = + absl::InvalidArgumentError(fmt::format("Unexpected YAML exception: {}", e.what())); + } + if (value.kind_case() == Protobuf::Value::kStructValue || + value.kind_case() == Protobuf::Value::kListValue) { + load_status = jsonConvertInternalNoThrow(value, validation_visitor, message); + } else { + load_status = + absl::InvalidArgumentError(fmt::format("Unable to convert YAML as JSON: {}", yaml)); + } + return load_status; +} + std::string MessageUtil::getYamlStringFromMessage(const Protobuf::Message& message, const bool block_print, const bool always_print_primitive_fields) { diff --git a/source/extensions/filters/http/ip_tagging/BUILD b/source/extensions/filters/http/ip_tagging/BUILD index 42182217ba627..8a5b4c3a8793b 100644 --- a/source/extensions/filters/http/ip_tagging/BUILD +++ b/source/extensions/filters/http/ip_tagging/BUILD @@ -19,7 +19,10 @@ envoy_cc_library( deps = [ "//envoy/http:filter_interface", "//envoy/runtime:runtime_interface", + "//envoy/thread_local:thread_local_interface", "//source/common/common:assert_lib", + "//source/common/common:thread_synchronizer_lib", + "//source/common/config:datasource_lib", "//source/common/http:header_map_lib", "//source/common/http:headers_lib", "//source/common/network:lc_trie_lib", @@ -35,6 +38,7 @@ envoy_cc_extension( hdrs = ["config.h"], deps = [ "//envoy/registry", + "//envoy/thread_local:thread_local_interface", "//source/common/protobuf:utility_lib", "//source/extensions/filters/http/common:factory_base_lib", "//source/extensions/filters/http/ip_tagging:ip_tagging_filter_lib", diff --git a/source/extensions/filters/http/ip_tagging/config.cc b/source/extensions/filters/http/ip_tagging/config.cc index c8b04f58522de..db63caede19e8 100644 --- a/source/extensions/filters/http/ip_tagging/config.cc +++ b/source/extensions/filters/http/ip_tagging/config.cc @@ -3,6 +3,7 @@ #include "envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.pb.h" #include "envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.pb.validate.h" #include "envoy/registry/registry.h" +#include "envoy/thread_local/thread_local.h" #include "source/common/protobuf/utility.h" #include "source/extensions/filters/http/ip_tagging/ip_tagging_filter.h" @@ -17,7 +18,10 @@ absl::StatusOr IpTaggingFilterFactory::createFilterFactor const std::string& stat_prefix, Server::Configuration::FactoryContext& context) { absl::StatusOr config = IpTaggingFilterConfig::create( - proto_config, stat_prefix, context.scope(), context.serverFactoryContext().runtime()); + proto_config, stat_prefix, context.serverFactoryContext().singletonManager(), context.scope(), + context.serverFactoryContext().runtime(), context.serverFactoryContext().api(), + context.serverFactoryContext().threadLocal(), + context.serverFactoryContext().mainThreadDispatcher(), context.messageValidationVisitor()); RETURN_IF_NOT_OK_REF(config.status()); return [config = std::move(config.value())](Http::FilterChainFactoryCallbacks& callbacks) -> void { diff --git a/source/extensions/filters/http/ip_tagging/ip_tagging_filter.cc b/source/extensions/filters/http/ip_tagging/ip_tagging_filter.cc index b47f6f56c45d9..2a763cf781897 100644 --- a/source/extensions/filters/http/ip_tagging/ip_tagging_filter.cc +++ b/source/extensions/filters/http/ip_tagging/ip_tagging_filter.cc @@ -3,6 +3,7 @@ #include "envoy/config/core/v3/address.pb.h" #include "envoy/extensions/filters/http/ip_tagging/v3/ip_tagging.pb.h" +#include "source/common/config/datasource.h" #include "source/common/http/header_map_impl.h" #include "source/common/http/headers.h" @@ -13,19 +14,195 @@ namespace Extensions { namespace HttpFilters { namespace IpTagging { +IpTagsProvider::IpTagsProvider(const envoy::config::core::v3::DataSource& ip_tags_datasource, + uint64_t ip_tags_refresh_interval_ms, + Event::Dispatcher& main_dispatcher, Api::Api& api, + ProtobufMessage::ValidationVisitor& validation_visitor, + ThreadLocal::SlotAllocator& tls, Stats::Scope& scope, + Singleton::InstanceSharedPtr owner, absl::Status& creation_status) + : ip_tags_path_(ip_tags_datasource.filename()), scope_(scope.createScope("ip_tagging_reload.")), + stat_name_set_(scope.symbolTable().makeSet("IpTaggingReload")), + stats_prefix_(stat_name_set_->add("ip_tagging_reload")), + unknown_tag_(stat_name_set_->add("unknown_tag.hit")), tags_loader_(api, validation_visitor), + time_source_(api.timeSource()), + ip_tags_refresh_interval_ms_(std::chrono::milliseconds(ip_tags_refresh_interval_ms)), + needs_refresh_(ip_tags_refresh_interval_ms_ > std::chrono::milliseconds(0) && + ip_tags_datasource.has_watched_directory() + ? true + : false), + owner_(owner) { + RETURN_ONLY_IF_NOT_OK_REF(creation_status); + stat_name_set_->rememberBuiltin("reload_success"); + stat_name_set_->rememberBuiltin("reload_error"); + auto tags_or_error = tags_loader_.loadTags(ip_tags_datasource, main_dispatcher, tls); + creation_status = tags_or_error.status(); + if (tags_or_error.status().ok()) { + tags_ = tags_or_error.value(); + } + ip_tags_reload_timer_ = main_dispatcher.createTimer([this]() -> void { + ENVOY_LOG(debug, "Trying to update ip tags in background"); + auto new_tags_or_error = tags_loader_.refreshTags(); + if (new_tags_or_error.status().ok()) { + updateIpTags(new_tags_or_error.value()); + incIpTagsReloadSuccess(); + } else { + ENVOY_LOG(debug, "Failed to reload ip tags, using old data: {}", + new_tags_or_error.status().message()); + incIpTagsReloadError(); + } + ip_tags_reload_timer_->enableTimer(ip_tags_refresh_interval_ms_); + }); + ip_tags_reload_timer_->enableTimer(ip_tags_refresh_interval_ms_); +} + +IpTagsProvider::~IpTagsProvider() { + if (ip_tags_reload_timer_) { + ip_tags_reload_timer_->disableTimer(); + } + ENVOY_LOG(debug, "Shutting down ip tags provider"); +}; + +LcTrieSharedPtr IpTagsProvider::ipTags() ABSL_LOCKS_EXCLUDED(ip_tags_mutex_) { + absl::ReaderMutexLock lock(&ip_tags_mutex_); + return tags_; +} + +void IpTagsProvider::updateIpTags(LcTrieSharedPtr new_tags) ABSL_LOCKS_EXCLUDED(ip_tags_mutex_) { + absl::MutexLock lock(&ip_tags_mutex_); + tags_ = new_tags; +} + +void IpTagsProvider::incCounter(Stats::StatName name) { + Stats::SymbolTable::StoragePtr storage = scope_->symbolTable().join({name}); + scope_->counterFromStatName(Stats::StatName(storage.get())).inc(); +} + +absl::StatusOr> IpTagsRegistrySingleton::getOrCreateProvider( + const envoy::config::core::v3::DataSource& ip_tags_datasource, + uint64_t ip_tags_refresh_interval_ms, Api::Api& api, + ProtobufMessage::ValidationVisitor& validation_visitor, ThreadLocal::SlotAllocator& tls, + Event::Dispatcher& main_dispatcher, Stats::Scope& scope, + std::shared_ptr singleton) { + std::shared_ptr ip_tags_provider; + absl::Status creation_status = absl::OkStatus(); + const uint64_t key = std::hash()(ip_tags_datasource.filename()); + absl::MutexLock lock(&mu_); + auto it = ip_tags_registry_.find(key); + if (it != ip_tags_registry_.end()) { + if (std::shared_ptr provider = it->second.lock()) { + ip_tags_provider = provider; + } else { + ip_tags_provider = std::make_shared( + ip_tags_datasource, ip_tags_refresh_interval_ms, main_dispatcher, api, validation_visitor, + tls, scope, singleton, creation_status); + ip_tags_registry_[key] = ip_tags_provider; + } + } else { + ip_tags_provider = std::make_shared( + ip_tags_datasource, ip_tags_refresh_interval_ms, main_dispatcher, api, validation_visitor, + tls, scope, singleton, creation_status); + ip_tags_registry_[key] = ip_tags_provider; + } + if (!creation_status.ok()) { + return creation_status; + } + return ip_tags_provider; +} + +IpTagsLoader::IpTagsLoader(Api::Api& api, ProtobufMessage::ValidationVisitor& validation_visitor) + : api_(api), validation_visitor_(validation_visitor) {} + +absl::StatusOr +IpTagsLoader::loadTags(const envoy::config::core::v3::DataSource& ip_tags_datasource, + Event::Dispatcher& main_dispatcher, ThreadLocal::SlotAllocator& tls) { + if (!ip_tags_datasource.filename().empty()) { + if (!absl::EndsWith(ip_tags_datasource.filename(), MessageUtil::FileExtensions::get().Yaml) && + !absl::EndsWith(ip_tags_datasource.filename(), MessageUtil::FileExtensions::get().Json)) { + return absl::InvalidArgumentError( + "Unsupported file format, unable to parse ip tags from file " + + ip_tags_datasource.filename()); + } + auto provider_or_error = Config::DataSource::DataSourceProvider::create( + ip_tags_datasource, main_dispatcher, tls, api_, false, 0); + if (!provider_or_error.status().ok()) { + return absl::InvalidArgumentError( + fmt::format("unable to create data source '{}'", provider_or_error.status().message())); + } + data_source_provider_ = std::move(provider_or_error.value()); + ip_tags_path_ = ip_tags_datasource.filename(); + return refreshTags(); + } + return absl::InvalidArgumentError("Cannot load tags from empty filename in datasource."); +} + +absl::StatusOr IpTagsLoader::refreshTags() { + if (data_source_provider_) { + IPTagsProto ip_tags_proto; + const auto new_data = data_source_provider_->data(); + if (absl::EndsWith(ip_tags_path_, MessageUtil::FileExtensions::get().Yaml)) { + auto load_status = + MessageUtil::loadFromYamlNoThrow(new_data, ip_tags_proto, validation_visitor_); + if (!load_status.ok()) { + return load_status; + } + } else if (absl::EndsWith(ip_tags_path_, MessageUtil::FileExtensions::get().Json)) { + bool has_unknown_field; + auto load_status = + MessageUtil::loadFromJsonNoThrow(new_data, ip_tags_proto, has_unknown_field); + if (!load_status.ok()) { + return load_status; + } + } + return parseIpTagsAsProto(ip_tags_proto.ip_tags()); + } else { + return absl::InvalidArgumentError("Unable to load tags from empty datasource"); + } +} + +absl::StatusOr IpTagsLoader::parseIpTagsAsProto( + const Protobuf::RepeatedPtrField< + envoy::extensions::filters::http::ip_tagging::v3::IPTagging::IPTag>& ip_tags) { + std::vector>> tag_data; + tag_data.reserve(ip_tags.size()); + for (const auto& ip_tag : ip_tags) { + std::vector cidr_set; + cidr_set.reserve(ip_tag.ip_list().size()); + for (const envoy::config::core::v3::CidrRange& entry : ip_tag.ip_list()) { + absl::StatusOr cidr_or_error = + Network::Address::CidrRange::create(entry); + if (cidr_or_error.status().ok()) { + cidr_set.emplace_back(std::move(cidr_or_error.value())); + } else { + return absl::InvalidArgumentError( + fmt::format("invalid ip/mask combo '{}/{}' (format is /<# mask bits>)", + entry.address_prefix(), entry.prefix_len().value())); + } + } + tag_data.emplace_back(ip_tag.ip_tag_name(), cidr_set); + } + return std::make_shared>(tag_data); +} + +SINGLETON_MANAGER_REGISTRATION(ip_tags_registry); + absl::StatusOr IpTaggingFilterConfig::create( const envoy::extensions::filters::http::ip_tagging::v3::IPTagging& config, - const std::string& stat_prefix, Stats::Scope& scope, Runtime::Loader& runtime) { + const std::string& stat_prefix, Singleton::Manager& singleton_manager, Stats::Scope& scope, + Runtime::Loader& runtime, Api::Api& api, ThreadLocal::SlotAllocator& tls, + Event::Dispatcher& dispatcher, ProtobufMessage::ValidationVisitor& validation_visitor) { absl::Status creation_status = absl::OkStatus(); auto config_ptr = std::shared_ptr( - new IpTaggingFilterConfig(config, stat_prefix, scope, runtime, creation_status)); + new IpTaggingFilterConfig(config, stat_prefix, singleton_manager, scope, runtime, api, tls, + dispatcher, validation_visitor, creation_status)); RETURN_IF_NOT_OK(creation_status); return config_ptr; } IpTaggingFilterConfig::IpTaggingFilterConfig( const envoy::extensions::filters::http::ip_tagging::v3::IPTagging& config, - const std::string& stat_prefix, Stats::Scope& scope, Runtime::Loader& runtime, + const std::string& stat_prefix, Singleton::Manager& singleton_manager, Stats::Scope& scope, + Runtime::Loader& runtime, Api::Api& api, ThreadLocal::SlotAllocator& tls, + Event::Dispatcher& dispatcher, ProtobufMessage::ValidationVisitor& validation_visitor, absl::Status& creation_status) : request_type_(requestTypeEnum(config.request_type())), scope_(scope), runtime_(runtime), stat_name_set_(scope.symbolTable().makeSet("IpTagging")), @@ -35,42 +212,54 @@ IpTaggingFilterConfig::IpTaggingFilterConfig( ip_tag_header_(config.has_ip_tag_header() ? config.ip_tag_header().header() : ""), ip_tag_header_action_(config.has_ip_tag_header() ? config.ip_tag_header().action() - : HeaderAction::IPTagging_IpTagHeader_HeaderAction_SANITIZE) { - - // Once loading IP tags from a file system is supported, the restriction on the size - // of the set should be removed and observability into what tags are loaded needs - // to be implemented. - // TODO(ccaraman): Remove size check once file system support is implemented. - // Work is tracked by issue https://github.com/envoyproxy/envoy/issues/2695. - if (config.ip_tags().empty()) { - creation_status = - absl::InvalidArgumentError("HTTP IP Tagging Filter requires ip_tags to be specified."); - return; + : HeaderAction::IPTagging_IpTagHeader_HeaderAction_SANITIZE), + ip_tags_registry_(singleton_manager.getTyped( + SINGLETON_MANAGER_REGISTERED_NAME(ip_tags_registry), + [] { return std::make_shared(); })), + tags_loader_(api, validation_visitor) { + + if (config.ip_tags().empty() && !config.has_ip_tags_file_provider()) { + creation_status = absl::InvalidArgumentError( + "HTTP IP Tagging Filter requires either ip_tags or ip_tags_file_provider to be specified."); } - std::vector>> tag_data; - tag_data.reserve(config.ip_tags().size()); - for (const auto& ip_tag : config.ip_tags()) { - std::vector cidr_set; - cidr_set.reserve(ip_tag.ip_list().size()); - for (const envoy::config::core::v3::CidrRange& entry : ip_tag.ip_list()) { + if (!config.ip_tags().empty() && config.has_ip_tags_file_provider()) { + creation_status = absl::InvalidArgumentError( + "Only one of ip_tags or ip_tags_file_provider can be configured."); + } - absl::StatusOr cidr_or_error = - Network::Address::CidrRange::create(entry); - if (cidr_or_error.status().ok()) { - cidr_set.emplace_back(std::move(cidr_or_error.value())); - } else { - creation_status = absl::InvalidArgumentError( - fmt::format("invalid ip/mask combo '{}/{}' (format is /<# mask bits>)", - entry.address_prefix(), entry.prefix_len().value())); - return; - } + RETURN_ONLY_IF_NOT_OK_REF(creation_status); + if (!config.ip_tags().empty()) { + auto trie_or_error = tags_loader_.parseIpTagsAsProto(config.ip_tags()); + if (trie_or_error.status().ok()) { + trie_ = trie_or_error.value(); + } else { + creation_status = trie_or_error.status(); + return; + } + } else { + if (!config.ip_tags_file_provider().has_ip_tags_datasource()) { + creation_status = absl::InvalidArgumentError( + "ip_tags_file_provider requires a valid ip_tags_datasource to be configured."); + return; + } + auto ip_tags_refresh_interval_ms = + PROTOBUF_GET_MS_OR_DEFAULT(config.ip_tags_file_provider(), ip_tags_refresh_rate, 0); + auto provider_or_error = ip_tags_registry_->getOrCreateProvider( + config.ip_tags_file_provider().ip_tags_datasource(), ip_tags_refresh_interval_ms, api, + validation_visitor, tls, dispatcher, scope, ip_tags_registry_); + if (provider_or_error.status().ok()) { + provider_ = provider_or_error.value(); + } else { + creation_status = provider_or_error.status(); + return; + } + if (provider_ && provider_->ipTags()) { + trie_ = provider_->ipTags(); + } else { + creation_status = absl::InvalidArgumentError("Failed to get ip tags from provider"); } - - tag_data.emplace_back(ip_tag.ip_tag_name(), cidr_set); - stat_name_set_->rememberBuiltin(absl::StrCat(ip_tag.ip_tag_name(), ".hit")); } - trie_ = std::make_unique>(tag_data); } void IpTaggingFilterConfig::incCounter(Stats::StatName name) { @@ -85,6 +274,7 @@ IpTaggingFilter::~IpTaggingFilter() = default; void IpTaggingFilter::onDestroy() {} Http::FilterHeadersStatus IpTaggingFilter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { + const bool is_internal_request = headers.EnvoyInternalRequest() && (headers.EnvoyInternalRequest()->value() == Http::Headers::get().EnvoyInternalRequestValues.True.c_str()); @@ -98,6 +288,8 @@ Http::FilterHeadersStatus IpTaggingFilter::decodeHeaders(Http::RequestHeaderMap& std::vector tags = config_->trie().getData(callbacks_->streamInfo().downstreamAddressProvider().remoteAddress()); + // Used for testing. + synchronizer_.syncPoint("_trie_lookup_complete"); applyTags(headers, tags); if (!tags.empty()) { // For a large number(ex > 1000) of tags, stats cardinality will be an issue. diff --git a/source/extensions/filters/http/ip_tagging/ip_tagging_filter.h b/source/extensions/filters/http/ip_tagging/ip_tagging_filter.h index 66f834f9209c8..35d20f31b7cab 100644 --- a/source/extensions/filters/http/ip_tagging/ip_tagging_filter.h +++ b/source/extensions/filters/http/ip_tagging/ip_tagging_filter.h @@ -12,7 +12,10 @@ #include "envoy/http/filter.h" #include "envoy/runtime/runtime.h" #include "envoy/stats/scope.h" +#include "envoy/thread_local/thread_local.h" +#include "source/common/common/thread_synchronizer.h" +#include "source/common/config/datasource.h" #include "source/common/network/cidr_range.h" #include "source/common/network/lc_trie.h" #include "source/common/stats/symbol_table.h" @@ -22,6 +25,131 @@ namespace Extensions { namespace HttpFilters { namespace IpTagging { +using IPTagsProto = envoy::extensions::filters::http::ip_tagging::v3::IPTagging::IPTags; +using LcTrieSharedPtr = std::shared_ptr>; + +/** + * This class is responsible for loading and parsing of ip tags (both inline and file based) + * as well as for periodic refresh of ip tags (file based). + */ +class IpTagsLoader : public Logger::Loggable { +public: + IpTagsLoader(Api::Api& api, ProtobufMessage::ValidationVisitor& validation_visitor); + + /** + * Loads file based ip tags from a data source and parses them into a trie structure. + * @param ip_tags_datasource file based data source to load ip tags from. + * @param dispatcher The dispatcher for the thread used by a data source provider. + * @param tls The thread local slot allocator used by a data source provider. + * @return Valid LcTrieSharedPtr if loading succeeded or error status otherwise. + */ + absl::StatusOr + loadTags(const envoy::config::core::v3::DataSource& ip_tags_datasource, + Event::Dispatcher& dispatcher, ThreadLocal::SlotAllocator& tls); + + /** + * Performs periodic refresh of file based ip tags via data source. + * @param new_data New data from data source which is used to refresh the ip tags structure. + * @return Valid LcTrieSharedPtr if loading succeeded or error status otherwise. + */ + absl::StatusOr refreshTags(); + + /** + * Parses ip tags in a proto format into a trie structure. + * @param ip_tags Collection of ip tags in proto format. + * @return Valid LcTrieSharedPtr if parsing succeeded or error status otherwise. + */ + absl::StatusOr + parseIpTagsAsProto(const Protobuf::RepeatedPtrField< + envoy::extensions::filters::http::ip_tagging::v3::IPTagging::IPTag>& ip_tags); + +private: + Api::Api& api_; + Envoy::Config::DataSource::DataSourceProviderPtr data_source_provider_; + std::string ip_tags_path_; + ProtobufMessage::ValidationVisitor& validation_visitor_; +}; + +using IpTagsReloadSuccessCb = std::function; +using IpTagsReloadErrorCb = std::function; + +/** + * This class owns ip tags trie structure for a configured absolute file path and provides access to + * the ip tags data. It also performs periodic refresh of ip tags data. + */ +class IpTagsProvider : public Logger::Loggable { +public: + IpTagsProvider(const envoy::config::core::v3::DataSource& ip_tags_datasource, + uint64_t ip_tags_refresh_interval_ms, Event::Dispatcher& main_dispatcher, + Api::Api& api, ProtobufMessage::ValidationVisitor& validation_visitor, + ThreadLocal::SlotAllocator& tls, Stats::Scope& scope, + Singleton::InstanceSharedPtr owner, absl::Status& creation_status); + + ~IpTagsProvider(); + + /** + * Getter method for ip tags trie structure which either returns the current version of ip tags + * data or performs a reload of data and returns the most recent version of ip tags data. + * @return Valid LcTrieSharedPtr or nullptr if reload has failed. + */ + LcTrieSharedPtr ipTags() ABSL_LOCKS_EXCLUDED(ip_tags_mutex_); + + void incIpTagsReloadSuccess() { + incCounter(stat_name_set_->getBuiltin("reload_success", unknown_tag_)); + } + void incIpTagsReloadError() { + incCounter(stat_name_set_->getBuiltin("reload_error", unknown_tag_)); + } + +private: + void incCounter(Stats::StatName name); + + const std::string ip_tags_path_; + Stats::ScopeSharedPtr scope_; + Stats::StatNameSetPtr stat_name_set_; + const Stats::StatName stats_prefix_; + const Stats::StatName unknown_tag_; + IpTagsLoader tags_loader_; + TimeSource& time_source_; + MonotonicTime last_reloaded_time_; + const std::chrono::milliseconds ip_tags_refresh_interval_ms_; + const bool needs_refresh_; + mutable absl::Mutex ip_tags_mutex_; + LcTrieSharedPtr tags_ ABSL_GUARDED_BY(ip_tags_mutex_); + Event::TimerPtr ip_tags_reload_timer_; + // A shared_ptr to keep the provider singleton alive as long as any of its providers are in use. + const Singleton::InstanceSharedPtr owner_; + + void updateIpTags(LcTrieSharedPtr new_tags) ABSL_LOCKS_EXCLUDED(ip_tags_mutex_); +}; + +using IpTagsProviderSharedPtr = std::shared_ptr; + +/** + * A singleton for file based loading of ip tags and looking up parsed trie data structures with Ip + * tags. When given equivalent file paths to the Ip tags, the singleton returns pointers to the same + * trie structure. + */ +class IpTagsRegistrySingleton : public Envoy::Singleton::Instance { +public: + IpTagsRegistrySingleton() {} + + absl::StatusOr> + getOrCreateProvider(const envoy::config::core::v3::DataSource& ip_tags_datasource, + uint64_t ip_tags_refresh_interval_ms, Api::Api& api, + ProtobufMessage::ValidationVisitor& validation_visitor, + ThreadLocal::SlotAllocator& tls, Event::Dispatcher& main_dispatcher, + Stats::Scope& scope, std::shared_ptr singleton); + +private: + absl::Mutex mu_; + // Each provider stores shared_ptrs to this singleton, which keeps the singleton + // from being destroyed unless it's no longer keeping track of any providers. + // Each entry in this map consists of a key (hash of an absolute file path to ip tags file) + // and and value (instance of `IpTagsProvider` that owns ip tags data). + absl::flat_hash_map> ip_tags_registry_ ABSL_GUARDED_BY(mu_); +}; + /** * Type of requests the filter should apply to. */ @@ -34,13 +162,22 @@ class IpTaggingFilterConfig { public: using HeaderAction = envoy::extensions::filters::http::ip_tagging::v3::IPTagging::IpTagHeader::HeaderAction; + static absl::StatusOr> create(const envoy::extensions::filters::http::ip_tagging::v3::IPTagging& config, - const std::string& stat_prefix, Stats::Scope& scope, Runtime::Loader& runtime); + const std::string& stat_prefix, Singleton::Manager& singleton_manager, Stats::Scope& scope, + Runtime::Loader& runtime, Api::Api& api, ThreadLocal::SlotAllocator& tls, + Event::Dispatcher& dispatcher, ProtobufMessage::ValidationVisitor& validation_visitor); Runtime::Loader& runtime() { return runtime_; } FilterRequestType requestType() const { return request_type_; } - const Network::LcTrie::LcTrie& trie() const { return *trie_; } + const Network::LcTrie::LcTrie& trie() const { + if (provider_) { + return *(provider_->ipTags()); + } else { + return *trie_; + } + } OptRef ipTagHeader() const { if (ip_tag_header_.get().empty()) { @@ -51,15 +188,24 @@ class IpTaggingFilterConfig { HeaderAction ipTagHeaderAction() const { return ip_tag_header_action_; } void incHit(absl::string_view tag) { + const Stats::StatName tag_counter = + stat_name_set_->getBuiltin(absl::StrCat(tag, ".hit"), unknown_tag_); + if (tag_counter == unknown_tag_) { + stat_name_set_->rememberBuiltin(absl::StrCat(tag, ".hit")); + } incCounter(stat_name_set_->getBuiltin(absl::StrCat(tag, ".hit"), unknown_tag_)); } + void incNoHit() { incCounter(no_hit_); } void incTotal() { incCounter(total_); } private: IpTaggingFilterConfig(const envoy::extensions::filters::http::ip_tagging::v3::IPTagging& config, - const std::string& stat_prefix, Stats::Scope& scope, - Runtime::Loader& runtime, absl::Status& creation_status); + const std::string& stat_prefix, Singleton::Manager& singleton_manager, + Stats::Scope& scope, Runtime::Loader& runtime, Api::Api& api, + ThreadLocal::SlotAllocator& tls, Event::Dispatcher& dispatcher, + ProtobufMessage::ValidationVisitor& validation_visitor, + absl::Status& creation_status); static FilterRequestType requestTypeEnum( envoy::extensions::filters::http::ip_tagging::v3::IPTagging::RequestType request_type) { @@ -76,7 +222,8 @@ class IpTaggingFilterConfig { } void incCounter(Stats::StatName name); - + // Allow the unit test to have access to private members. + friend class IpTaggingFilterConfigPeer; const FilterRequestType request_type_; Stats::Scope& scope_; Runtime::Loader& runtime_; @@ -85,10 +232,15 @@ class IpTaggingFilterConfig { const Stats::StatName no_hit_; const Stats::StatName total_; const Stats::StatName unknown_tag_; - std::unique_ptr> trie_; const Http::LowerCaseString ip_tag_header_; // An empty string indicates that no ip_tag_header is set. const HeaderAction ip_tag_header_action_; + // A shared_ptr to keep the ip tags registry singleton alive as long as any of its trie structures + // are in use. + const std::shared_ptr ip_tags_registry_; + IpTagsLoader tags_loader_; + LcTrieSharedPtr trie_; + std::shared_ptr provider_; }; using IpTaggingFilterConfigSharedPtr = std::shared_ptr; @@ -117,6 +269,10 @@ class IpTaggingFilter : public Http::StreamDecoderFilter { IpTaggingFilterConfigSharedPtr config_; Http::StreamDecoderFilterCallbacks* callbacks_{}; + // Used for testing only. + mutable Thread::ThreadSynchronizer synchronizer_; + // Allow the unit test to have access to private members. + friend class IpTaggingFilterPeer; }; } // namespace IpTagging diff --git a/test/extensions/filters/http/ip_tagging/BUILD b/test/extensions/filters/http/ip_tagging/BUILD index f801cdd0aa39c..28b9dc8a78b28 100644 --- a/test/extensions/filters/http/ip_tagging/BUILD +++ b/test/extensions/filters/http/ip_tagging/BUILD @@ -14,6 +14,9 @@ envoy_package() envoy_extension_cc_test( name = "ip_tagging_filter_test", srcs = ["ip_tagging_filter_test.cc"], + data = [ + "//test/extensions/filters/http/ip_tagging/test_data:ip_tagging_files", + ], extension_names = ["envoy.filters.http.ip_tagging"], rbe_pool = "6gig", deps = [ @@ -21,11 +24,15 @@ envoy_extension_cc_test( "//source/common/http:header_map_lib", "//source/common/network:address_lib", "//source/common/network:utility_lib", + "//source/common/singleton:manager_impl_lib", "//source/extensions/filters/http/ip_tagging:config", "//source/extensions/filters/http/ip_tagging:ip_tagging_filter_lib", + "//test/mocks/api:api_mocks", "//test/mocks/http:http_mocks", + "//test/mocks/protobuf:protobuf_mocks", "//test/mocks/runtime:runtime_mocks", "//test/mocks/stats:stats_mocks", + "//test/mocks/thread_local:thread_local_mocks", "//test/test_common:utility_lib", "@envoy_api//envoy/extensions/filters/http/ip_tagging/v3:pkg_cc_proto", ], @@ -35,6 +42,9 @@ envoy_extension_cc_test( name = "ip_tagging_integration_test", size = "large", srcs = ["ip_tagging_integration_test.cc"], + data = [ + "//test/extensions/filters/http/ip_tagging/test_data:ip_tagging_files", + ], extension_names = ["envoy.filters.http.ip_tagging"], rbe_pool = "6gig", deps = [ diff --git a/test/extensions/filters/http/ip_tagging/ip_tagging_filter_test.cc b/test/extensions/filters/http/ip_tagging/ip_tagging_filter_test.cc index 5cbe8fd5413fb..92d2ac160d05a 100644 --- a/test/extensions/filters/http/ip_tagging/ip_tagging_filter_test.cc +++ b/test/extensions/filters/http/ip_tagging/ip_tagging_filter_test.cc @@ -5,49 +5,286 @@ #include "source/common/buffer/buffer_impl.h" #include "source/common/network/address_impl.h" #include "source/common/network/utility.h" +#include "source/common/singleton/manager_impl.h" #include "source/extensions/filters/http/ip_tagging/ip_tagging_filter.h" +#include "test/mocks/api/mocks.h" #include "test/mocks/http/mocks.h" +#include "test/mocks/protobuf/mocks.h" #include "test/mocks/runtime/mocks.h" #include "test/mocks/stats/mocks.h" +#include "test/mocks/thread_local/mocks.h" +#include "test/test_common/environment.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" +using testing::InvokeWithoutArgs; using testing::Return; namespace Envoy { namespace Extensions { namespace HttpFilters { namespace IpTagging { -namespace { -class IpTaggingFilterTest : public testing::Test { +class IpTaggingFilterConfigPeer { public: - IpTaggingFilterTest() { - ON_CALL(runtime_.snapshot_, featureEnabled("ip_tagging.http_filter_enabled", 100)) - .WillByDefault(Return(true)); + static IpTagsLoader& ipTagsLoader(IpTaggingFilterConfig& filter_config) { + return filter_config.tags_loader_; + } + static std::shared_ptr ipTagsProvider(IpTaggingFilterConfig& filter_config) { + return filter_config.provider_; } + static const std::shared_ptr& + ipTagsRegistry(const IpTaggingFilterConfig& filter_config) { + return filter_config.ip_tags_registry_; + } +}; + +class IpTaggingFilterPeer { +public: + static Thread::ThreadSynchronizer& synchronizer(std::unique_ptr& filter) { + return filter->synchronizer_; + } +}; + +namespace { +namespace { - const std::string internal_request_yaml = R"EOF( +const std::string internal_request_config = R"EOF( request_type: internal ip_tags: - ip_tag_name: internal_request ip_list: - {address_prefix: 1.2.3.5, prefix_len: 32} +)EOF"; + +const std::string internal_request_with_json_file_config = R"EOF( + request_type: internal + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.json" + )EOF"; + +const std::string internal_request_with_yaml_file_with_reload_config = R"EOF( + request_type: internal + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.yaml" + watched_directory: + path: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data" + )EOF"; + +const std::string internal_request_with_yaml_file_config = R"EOF( + request_type: internal + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.yaml" + )EOF"; + +const std::string external_request_config = R"EOF( +request_type: external +ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +)EOF"; + +const std::string external_request_with_json_file_config = R"EOF( + request_type: external + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.json" + )EOF"; + +const std::string external_request_with_yaml_file_config = R"EOF( + request_type: external + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.yaml" + )EOF"; + +const std::string both_request_config = R"EOF( +request_type: both +ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} +)EOF"; + +const std::string both_request_with_json_file_config = R"EOF( +request_type: both +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.json" +)EOF"; + +const std::string both_request_with_yaml_file_config = R"EOF( +request_type: both +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.yaml" +)EOF"; + +const std::string internal_request_with_header_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header +ip_tags: + - ip_tag_name: internal_request_with_optional_header + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +)EOF"; + +const std::string internal_request_with_header_with_json_file_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.json" +)EOF"; + +const std::string internal_request_with_header_with_yaml_file_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.yaml" +)EOF"; + +const std::string internal_request_with_replace_header_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: SANITIZE +ip_tags: + - ip_tag_name: internal_request_with_optional_header + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +)EOF"; + +const std::string internal_request_with_replace_header_with_json_file_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: SANITIZE +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.json" +)EOF"; + +const std::string internal_request_with_replace_header_with_yaml_file_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: SANITIZE +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.yaml" +)EOF"; + +const std::string internal_request_with_append_or_add_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: APPEND_IF_EXISTS_OR_ADD +ip_tags: + - ip_tag_name: internal_request_with_optional_header + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +)EOF"; + +const std::string internal_request_with_append_or_add_with_json_file_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: APPEND_IF_EXISTS_OR_ADD +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.json" +)EOF"; + +const std::string internal_request_with_append_or_add_with_yaml_file_config = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: APPEND_IF_EXISTS_OR_ADD +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.yaml" +)EOF"; + +const std::string duplicate_request_config = R"EOF( +request_type: both +ip_tags: + - ip_tag_name: duplicate_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +)EOF"; + +const std::string duplicate_request_with_json_file_config = R"EOF( +request_type: both +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.json" +)EOF"; + +const std::string duplicate_request_with_yaml_file_config = R"EOF( +request_type: both +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.yaml" +)EOF"; + +const std::string ipv6_config = R"EOF( +ip_tags: + - ip_tag_name: ipv6_request + ip_list: + - {address_prefix: 2001:abcd:ef01:2345:6789:abcd:ef01:234, prefix_len: 64} +)EOF"; +const std::string ipv6_with_yaml_file_config = R"EOF( +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.yaml" )EOF"; +const std::string ipv6_with_json_file_config = R"EOF( +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.json" +)EOF"; + +} // namespace + +class IpTaggingFilterTest : public ::testing::TestWithParam { +public: + IpTaggingFilterTest() + : scope_(stats_.rootScope()), api_(Api::createApiForTest(stats_, time_system_)), + dispatcher_(api_->allocateDispatcher("test_main_thread")) { + ON_CALL(runtime_.snapshot_, featureEnabled("ip_tagging.http_filter_enabled", 100)) + .WillByDefault(Return(true)); + } + void initializeFilter(const std::string& yaml, absl::optional expected_error = absl::nullopt) { envoy::extensions::filters::http::ip_tagging::v3::IPTagging config; - TestUtility::loadFromYaml(yaml, config); + TestUtility::loadFromYaml(TestEnvironment::substitute(yaml), config); auto config_or = - IpTaggingFilterConfig::create(config, "prefix.", *stats_.rootScope(), runtime_); + IpTaggingFilterConfig::create(config, "prefix.", *singleton_manager_, *scope_, runtime_, + *api_, tls_, *dispatcher_, validation_visitor_); if (expected_error.has_value()) { EXPECT_FALSE(config_or.ok()); - EXPECT_EQ(expected_error.value(), absl::StrCat(config_or.status())); + EXPECT_TRUE(absl::StrContains(absl::StrCat(config_or.status()), expected_error.value())); return; } EXPECT_TRUE(config_or.ok()); @@ -62,16 +299,192 @@ request_type: internal } } - NiceMock stats_; + std::unique_ptr singleton_manager_ = + std::make_unique(); + Envoy::Stats::TestUtil::TestStore stats_; + Stats::ScopeSharedPtr scope_; + Event::SimulatedTimeSystem time_system_; + Api::ApiPtr api_; + Event::DispatcherPtr dispatcher_; + NiceMock tls_; IpTaggingFilterConfigSharedPtr config_; std::unique_ptr filter_; NiceMock filter_callbacks_; Buffer::OwnedImpl data_; NiceMock runtime_; + NiceMock validation_visitor_; }; -TEST_F(IpTaggingFilterTest, InternalRequest) { - initializeFilter(internal_request_yaml); +TEST_F(IpTaggingFilterTest, NoIpTagsConfigured) { + const std::string config_yaml = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: SANITIZE +)EOF"; + initializeFilter(config_yaml, "INVALID_ARGUMENT: HTTP IP Tagging Filter requires either ip_tags " + "or ip_tags_file_provider to be specified."); +} + +TEST_F(IpTaggingFilterTest, BothIpTagsAndIpTagsFileConfigured) { + const std::string config_yaml = R"EOF( +request_type: internal +ip_tag_header: + header: x-envoy-optional-header + action: SANITIZE +ip_tags: + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} +ip_tags_file_provider: + ip_tags_datasource: + filename: /test/tags.yaml +)EOF"; + initializeFilter( + config_yaml, + "INVALID_ARGUMENT: Only one of ip_tags or ip_tags_file_provider can be configured."); +} + +TEST_F(IpTaggingFilterTest, EmptyDatasourceConfigured) { + const std::string config_yaml = R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_datasource: +)EOF"; + initializeFilter(config_yaml, "INVALID_ARGUMENT: ip_tags_file_provider requires a valid " + "ip_tags_datasource to be configured."); +} + +TEST_F(IpTaggingFilterTest, EmptyIpTagsFile) { + const std::string config_yaml = R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/empty_file.yaml" +)EOF"; + std::string file = TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/empty_file.yaml"); + initializeFilter( + config_yaml, + absl::StrCat("INVALID_ARGUMENT: unable to create data source 'file ", file, " is empty'")); +} + +TEST_F(IpTaggingFilterTest, EmptyFilenameInDatasourceConfigured) { + const std::string config_yaml = R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_datasource: + filename: +)EOF"; + initializeFilter(config_yaml, + "INVALID_ARGUMENT: Cannot load tags from empty filename in datasource."); +} + +TEST_F(IpTaggingFilterTest, UnsupportedFormatForIpTagsFile) { + const std::string config_yaml = R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_datasource: + filename: /test/tags.csv +)EOF"; + initializeFilter(config_yaml, "INVALID_ARGUMENT: Unsupported file format, unable to parse ip " + "tags from file /test/tags.csv"); +} + +TEST_F(IpTaggingFilterTest, InvalidYamlFile) { + const std::string config_yaml = R"EOF( + request_type: internal + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.yaml" + )EOF"; + initializeFilter(config_yaml, "INVALID_ARGUMENT: Unable to convert YAML as JSON: ip_tags"); +} + +TEST_F(IpTaggingFilterTest, InvalidJsonFile) { + const std::string config_yaml = R"EOF( + request_type: internal + ip_tags_file_provider: + ip_tags_datasource: + filename: "{{ test_rundir }}/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.json" + )EOF"; + initializeFilter(config_yaml, "INVALID_ARGUMENT: invalid JSON"); +} + +TEST_F(IpTaggingFilterTest, InvalidCidr) { + const std::string external_request_yaml = R"EOF( +request_type: external +ip_tags: + - ip_tag_name: fooooooo + ip_list: + - {address_prefix: 12345.12345.12345.12345, prefix_len: 999999} +)EOF"; + initializeFilter( + external_request_yaml, + "INVALID_ARGUMENT: invalid ip/mask combo '12345.12345.12345.12345/999999' (format is " + "/<# mask bits>)"); +} + +TEST_F(IpTaggingFilterTest, ReusesIpTagsProviderInstanceForSameFilePath) { + envoy::extensions::filters::http::ip_tagging::v3::IPTagging proto_config1; + TestUtility::loadFromYaml(TestEnvironment::substitute(internal_request_with_json_file_config), + proto_config1); + absl::StatusOr config1_result = + IpTaggingFilterConfig::create(proto_config1, "prefix.", *singleton_manager_, *scope_, + runtime_, *api_, tls_, *dispatcher_, validation_visitor_); + EXPECT_TRUE(config1_result.ok()); + auto config1 = config1_result.value(); + envoy::extensions::filters::http::ip_tagging::v3::IPTagging proto_config2; + TestUtility::loadFromYaml(TestEnvironment::substitute(internal_request_with_json_file_config), + proto_config2); + absl::StatusOr config2_result = + IpTaggingFilterConfig::create(proto_config2, "prefix.", *singleton_manager_, *scope_, + runtime_, *api_, tls_, *dispatcher_, validation_visitor_); + EXPECT_TRUE(config2_result.ok()); + auto config2 = config2_result.value(); + auto ip_tags_registry1 = IpTaggingFilterConfigPeer::ipTagsRegistry(*config1); + auto ip_tags_registry2 = IpTaggingFilterConfigPeer::ipTagsRegistry(*config2); + EXPECT_EQ(ip_tags_registry1.get(), ip_tags_registry2.get()); + auto provider1 = IpTaggingFilterConfigPeer::ipTagsProvider(*config1); + auto provider2 = IpTaggingFilterConfigPeer::ipTagsProvider(*config2); + EXPECT_NE(nullptr, provider1); + EXPECT_NE(nullptr, provider2); + EXPECT_EQ(provider1->ipTags(), provider2->ipTags()); +} + +TEST_F(IpTaggingFilterTest, DifferentIpTagsProviderInstanceForDifferentFilePath) { + envoy::extensions::filters::http::ip_tagging::v3::IPTagging proto_config1; + TestUtility::loadFromYaml(TestEnvironment::substitute(internal_request_with_json_file_config), + proto_config1); + absl::StatusOr config1_result = + IpTaggingFilterConfig::create(proto_config1, "prefix.", *singleton_manager_, *scope_, + runtime_, *api_, tls_, *dispatcher_, validation_visitor_); + EXPECT_TRUE(config1_result.ok()); + auto config1 = config1_result.value(); + envoy::extensions::filters::http::ip_tagging::v3::IPTagging proto_config2; + TestUtility::loadFromYaml(TestEnvironment::substitute(external_request_with_json_file_config), + proto_config2); + absl::StatusOr config2_result = + IpTaggingFilterConfig::create(proto_config2, "prefix.", *singleton_manager_, *scope_, + runtime_, *api_, tls_, *dispatcher_, validation_visitor_); + EXPECT_TRUE(config2_result.ok()); + auto config2 = config2_result.value(); + auto ip_tags_registry1 = IpTaggingFilterConfigPeer::ipTagsRegistry(*config1); + auto ip_tags_registry2 = IpTaggingFilterConfigPeer::ipTagsRegistry(*config2); + EXPECT_EQ(ip_tags_registry1.get(), ip_tags_registry2.get()); + auto provider1 = IpTaggingFilterConfigPeer::ipTagsProvider(*config1); + auto provider2 = IpTaggingFilterConfigPeer::ipTagsProvider(*config2); + EXPECT_NE(nullptr, provider1); + EXPECT_NE(nullptr, provider2); + EXPECT_NE(provider1->ipTags(), provider2->ipTags()); + ::testing::Mock::VerifyAndClearExpectations(&filter_callbacks_); +} + +class InternalRequestIpTaggingFilterTest : public IpTaggingFilterTest {}; + +TEST_P(InternalRequestIpTaggingFilterTest, InternalRequest) { + const std::string config = GetParam(); + initializeFilter(config); EXPECT_EQ(FilterRequestType::INTERNAL, config_->requestType()); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; @@ -80,12 +493,12 @@ TEST_F(IpTaggingFilterTest, InternalRequest) { filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( remote_address); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.internal_request.hit")); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.total")); - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); EXPECT_EQ("internal_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); Http::TestRequestTrailerMapImpl request_trailers; EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); @@ -96,21 +509,19 @@ TEST_F(IpTaggingFilterTest, InternalRequest) { EXPECT_FALSE(request_headers.has(Http::Headers::get().EnvoyIpTags)); } -TEST_F(IpTaggingFilterTest, ExternalRequest) { - const std::string external_request_yaml = R"EOF( -request_type: external -ip_tags: - - ip_tag_name: external_request - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; - initializeFilter(external_request_yaml); +INSTANTIATE_TEST_CASE_P(InternalRequest, InternalRequestIpTaggingFilterTest, + ::testing::ValuesIn({internal_request_config, + internal_request_with_json_file_config, + internal_request_with_yaml_file_config})); + +class ExternalRequestIpTaggingFilterTest : public IpTaggingFilterTest {}; + +TEST_P(ExternalRequestIpTaggingFilterTest, ExternalRequest) { + const std::string config = GetParam(); + initializeFilter(config); EXPECT_EQ(FilterRequestType::EXTERNAL, config_->requestType()); Http::TestRequestHeaderMapImpl request_headers; - EXPECT_CALL(stats_, counter("prefix.ip_tagging.total")); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.external_request.hit")); - Network::Address::InstanceConstSharedPtr remote_address = Network::Utility::parseInternetAddressNoThrow("1.2.3.4"); filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( @@ -119,6 +530,9 @@ request_type: external EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); EXPECT_EQ("external_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.external_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); Http::TestRequestTrailerMapImpl request_trailers; EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); @@ -129,26 +543,19 @@ request_type: external EXPECT_FALSE(request_headers.has(Http::Headers::get().EnvoyIpTags)); } -TEST_F(IpTaggingFilterTest, BothRequest) { - const std::string both_request_yaml = R"EOF( -request_type: both -ip_tags: - - ip_tag_name: external_request - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} - - ip_tag_name: internal_request - ip_list: - - {address_prefix: 1.2.3.5, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P(ExternalRequest, ExternalRequestIpTaggingFilterTest, + ::testing::ValuesIn({external_request_config, + external_request_with_json_file_config, + external_request_with_yaml_file_config})); + +class BothRequestIpTaggingFilterTest : public IpTaggingFilterTest {}; - initializeFilter(both_request_yaml); +TEST_P(BothRequestIpTaggingFilterTest, BothRequest) { + const std::string config = GetParam(); + initializeFilter(config); EXPECT_EQ(FilterRequestType::BOTH, config_->requestType()); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; - EXPECT_CALL(stats_, counter("prefix.ip_tagging.total")).Times(2); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.internal_request.hit")); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.external_request.hit")); - Network::Address::InstanceConstSharedPtr remote_address = Network::Utility::parseInternetAddressNoThrow("1.2.3.5"); filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( @@ -164,10 +571,22 @@ request_type: both EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); EXPECT_EQ("external_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.external_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 2); } -TEST_F(IpTaggingFilterTest, NoHits) { - initializeFilter(internal_request_yaml); +INSTANTIATE_TEST_CASE_P(BothRequest, BothRequestIpTaggingFilterTest, + ::testing::ValuesIn({both_request_config, + both_request_with_json_file_config, + both_request_with_yaml_file_config})); + +class NoHitsIpTaggingFilterTest : public IpTaggingFilterTest {}; + +TEST_P(NoHitsIpTaggingFilterTest, NoHits) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; Network::Address::InstanceConstSharedPtr remote_address = @@ -175,19 +594,27 @@ TEST_F(IpTaggingFilterTest, NoHits) { filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( remote_address); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.no_hit")); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.total")); - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); EXPECT_FALSE(request_headers.has(Http::Headers::get().EnvoyIpTags)); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.no_hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); Http::TestRequestTrailerMapImpl request_trailers; EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, AppendEntry) { - initializeFilter(internal_request_yaml); +INSTANTIATE_TEST_CASE_P(NoHits, NoHitsIpTaggingFilterTest, + ::testing::ValuesIn({internal_request_config, + internal_request_with_json_file_config, + internal_request_with_yaml_file_config})); + +class AppendEntryFilterTest : public IpTaggingFilterTest {}; + +TEST_P(AppendEntryFilterTest, AppendEntry) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}, {"x-envoy-ip-tags", "test"}}; @@ -204,19 +631,17 @@ TEST_F(IpTaggingFilterTest, AppendEntry) { EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, ReplaceAlternateHeaderWhenActionIsDefaulted) { - const std::string internal_request_yaml = R"EOF( -request_type: internal -ip_tag_header: - header: x-envoy-optional-header -ip_tags: - - ip_tag_name: internal_request_with_optional_header - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P(AppendEntry, AppendEntryFilterTest, + ::testing::ValuesIn({internal_request_config, + internal_request_with_json_file_config, + internal_request_with_yaml_file_config})); - initializeFilter(internal_request_yaml); +class ReplaceAlternateHeaderWhenActionIsDefaultedFilterTest : public IpTaggingFilterTest {}; +TEST_P(ReplaceAlternateHeaderWhenActionIsDefaultedFilterTest, + ReplaceAlternateHeaderWhenActionIsDefaulted) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{ {"x-envoy-internal", "true"}, {"x-envoy-optional-header", "foo"}, // foo will be removed @@ -237,20 +662,17 @@ request_type: internal EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, ReplaceAlternateHeader) { - const std::string internal_request_yaml = R"EOF( -request_type: internal -ip_tag_header: - header: x-envoy-optional-header - action: SANITIZE -ip_tags: - - ip_tag_name: internal_request_with_optional_header - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P(ReplaceAlternateHeaderWhenActionIsDefaulted, + ReplaceAlternateHeaderWhenActionIsDefaultedFilterTest, + ::testing::ValuesIn({internal_request_with_header_config, + internal_request_with_header_with_json_file_config, + internal_request_with_header_with_yaml_file_config})); - initializeFilter(internal_request_yaml); +class ReplaceAlternateHeaderFilterTest : public IpTaggingFilterTest {}; +TEST_P(ReplaceAlternateHeaderFilterTest, ReplaceAlternateHeader) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{ {"x-envoy-internal", "true"}, {"x-envoy-optional-header", "foo"}}; // foo will be removed Network::Address::InstanceConstSharedPtr remote_address = @@ -267,22 +689,23 @@ request_type: internal EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, ClearAlternateHeaderWhenUnmatchedAndSanitized) { - const std::string internal_request_yaml = R"EOF( -request_type: internal -ip_tag_header: - header: x-envoy-optional-header - action: SANITIZE -ip_tags: - - ip_tag_name: internal_request_with_optional_header - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P( + ReplaceAlternateHeader, ReplaceAlternateHeaderFilterTest, + ::testing::ValuesIn({internal_request_with_replace_header_config, + internal_request_with_replace_header_with_json_file_config, + internal_request_with_replace_header_with_yaml_file_config})); - initializeFilter(internal_request_yaml); +class ClearAlternateHeaderWhenUnmatchedAndSanitizedFilterTest : public IpTaggingFilterTest {}; - Http::TestRequestHeaderMapImpl request_headers{ - {"x-envoy-internal", "true"}, {"x-envoy-optional-header", "foo"}}; // header will be removed +TEST_P(ClearAlternateHeaderWhenUnmatchedAndSanitizedFilterTest, + ClearAlternateHeaderWhenUnmatchedAndSanitized) { + const std::string config = GetParam(); + initializeFilter(config); + Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}, + {"x-envoy-optional-header", "foo"}}; // header + // will + // be + // removed Network::Address::InstanceConstSharedPtr remote_address = Network::Utility::parseInternetAddressNoThrow("1.2.3.5"); filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( @@ -296,20 +719,18 @@ request_type: internal EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, AppendForwardAlternateHeader) { - const std::string internal_request_yaml = R"EOF( -request_type: internal -ip_tag_header: - header: x-envoy-optional-header - action: APPEND_IF_EXISTS_OR_ADD -ip_tags: - - ip_tag_name: internal_request_with_optional_header - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P( + ClearAlternateHeaderWhenUnmatchedAndSanitized, + ClearAlternateHeaderWhenUnmatchedAndSanitizedFilterTest, + ::testing::ValuesIn({internal_request_with_replace_header_config, + internal_request_with_replace_header_with_json_file_config, + internal_request_with_replace_header_with_yaml_file_config})); - initializeFilter(internal_request_yaml); +class AppendForwardAlternateHeaderFilterTest : public IpTaggingFilterTest {}; +TEST_P(AppendForwardAlternateHeaderFilterTest, AppendForwardAlternateHeader) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}, {"x-envoy-optional-header", "foo"}}; Network::Address::InstanceConstSharedPtr remote_address = @@ -326,20 +747,19 @@ request_type: internal EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, RetainAlternateHeaderWhenUnmatchedAndAppendForwarded) { - const std::string internal_request_yaml = R"EOF( -request_type: internal -ip_tag_header: - header: x-envoy-optional-header - action: APPEND_IF_EXISTS_OR_ADD -ip_tags: - - ip_tag_name: internal_request_with_optional_header - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P( + AppendForwardAlternateHeader, AppendForwardAlternateHeaderFilterTest, + ::testing::ValuesIn({internal_request_with_append_or_add_config, + internal_request_with_append_or_add_with_json_file_config, + internal_request_with_append_or_add_with_yaml_file_config})); - initializeFilter(internal_request_yaml); +class RetainAlternateHeaderWhenUnmatchedAndAppendForwardedFilterTest : public IpTaggingFilterTest { +}; +TEST_P(RetainAlternateHeaderWhenUnmatchedAndAppendForwardedFilterTest, + RetainAlternateHeaderWhenUnmatchedAndAppendForwarded) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}, {"x-envoy-optional-header", "foo"}}; Network::Address::InstanceConstSharedPtr remote_address = @@ -355,19 +775,18 @@ request_type: internal EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, NestedPrefixes) { - const std::string duplicate_request_yaml = R"EOF( -request_type: both -ip_tags: - - ip_tag_name: duplicate_request - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} - - ip_tag_name: internal_request - ip_list: - - {address_prefix: 1.2.3.4, prefix_len: 32} -)EOF"; +INSTANTIATE_TEST_CASE_P( + RetainAlternateHeaderWhenUnmatchedAndAppendForwarded, + RetainAlternateHeaderWhenUnmatchedAndAppendForwardedFilterTest, + ::testing::ValuesIn({internal_request_with_append_or_add_config, + internal_request_with_append_or_add_with_json_file_config, + internal_request_with_append_or_add_with_yaml_file_config})); + +class NestedPrefixesFilterTest : public IpTaggingFilterTest {}; - initializeFilter(duplicate_request_yaml); +TEST_P(NestedPrefixesFilterTest, NestedPrefixes) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}, {"x-envoy-ip-tags", "test"}}; @@ -376,12 +795,12 @@ request_type: both filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( remote_address); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.total")); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.internal_request.hit")); - EXPECT_CALL(stats_, counter("prefix.ip_tagging.duplicate_request.hit")); - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.duplicate_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + // There is no guarantee for the order tags are returned by the LC-Trie. const std::string header_tag_data = request_headers.get_(Http::Headers::get().EnvoyIpTags.get()); EXPECT_NE(std::string::npos, header_tag_data.find("test")); @@ -393,14 +812,16 @@ request_type: both EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, Ipv6Address) { - const std::string ipv6_addresses_yaml = R"EOF( -ip_tags: - - ip_tag_name: ipv6_request - ip_list: - - {address_prefix: 2001:abcd:ef01:2345:6789:abcd:ef01:234, prefix_len: 64} -)EOF"; - initializeFilter(ipv6_addresses_yaml); +INSTANTIATE_TEST_CASE_P(NestedPrefixes, NestedPrefixesFilterTest, + ::testing::ValuesIn({duplicate_request_config, + duplicate_request_with_json_file_config, + duplicate_request_with_yaml_file_config})); + +class Ipv6AddressTest : public IpTaggingFilterTest {}; + +TEST_P(Ipv6AddressTest, Ipv6Address) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers; Network::Address::InstanceConstSharedPtr remote_address = @@ -416,8 +837,15 @@ TEST_F(IpTaggingFilterTest, Ipv6Address) { EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, RuntimeDisabled) { - initializeFilter(internal_request_yaml); +INSTANTIATE_TEST_CASE_P(Ipv6Address, Ipv6AddressTest, + ::testing::ValuesIn({ipv6_config, ipv6_with_json_file_config, + ipv6_with_yaml_file_config})); + +class RuntimeDisabledTest : public IpTaggingFilterTest {}; + +TEST_P(RuntimeDisabledTest, RuntimeDisabled) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; EXPECT_CALL(runtime_.snapshot_, featureEnabled("ip_tagging.http_filter_enabled", 100)) @@ -429,8 +857,16 @@ TEST_F(IpTaggingFilterTest, RuntimeDisabled) { EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); } -TEST_F(IpTaggingFilterTest, ClearRouteCache) { - initializeFilter(internal_request_yaml); +INSTANTIATE_TEST_CASE_P(RuntimeDisabled, RuntimeDisabledTest, + ::testing::ValuesIn({internal_request_config, + internal_request_with_json_file_config, + internal_request_with_yaml_file_config})); + +class ClearRouteCacheTest : public IpTaggingFilterTest {}; + +TEST_P(ClearRouteCacheTest, ClearRouteCache) { + const std::string config = GetParam(); + initializeFilter(config); Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; Network::Address::InstanceConstSharedPtr remote_address = @@ -449,26 +885,314 @@ TEST_F(IpTaggingFilterTest, ClearRouteCache) { EXPECT_FALSE(request_headers.has(Http::Headers::get().EnvoyIpTags)); } -TEST_F(IpTaggingFilterTest, EmptyIpTags) { - const std::string external_request_yaml = R"EOF( -request_type: external -)EOF"; - initializeFilter(external_request_yaml, - "INVALID_ARGUMENT: HTTP IP Tagging Filter requires ip_tags to be specified."); +INSTANTIATE_TEST_CASE_P(ClearRouteCache, ClearRouteCacheTest, + ::testing::ValuesIn({internal_request_config, + internal_request_with_json_file_config, + internal_request_with_yaml_file_config})); + +TEST_F(IpTaggingFilterTest, InternalRequestWithReload) { + time_system_.advanceTimeWait(std::chrono::seconds(1)); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + + TestEnvironment::createPath(TestEnvironment::temporaryPath("ip_tagging_test")); + + const std::string yaml = + fmt::format(R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_refresh_rate: 2s + ip_tags_datasource: + filename: "{}" + watched_directory: + path: "{}" + )EOF", + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test")); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} + )EOF", + true); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: internal_updated_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} + )EOF", + true); + initializeFilter(yaml); + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(1)); + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + EXPECT_EQ(FilterRequestType::INTERNAL, config_->requestType()); + Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; + + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.5"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ("internal_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + Http::TestRequestTrailerMapImpl request_trailers; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + + // Check external requests don't get a tag. + request_headers = Http::TestRequestHeaderMapImpl{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_FALSE(request_headers.has(Http::Headers::get().EnvoyIpTags)); + + // Update the symlink to point to the new file. + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml")); + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml")); + + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(6)); + // Handle the events if any. + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + EXPECT_TRUE( + TestUtility::waitForCounterEq(stats_, "ip_tagging_reload.reload_success", 1UL, time_system_)); + + EXPECT_EQ(stats_.counterFromString("ip_tagging_reload.reload_error").value(), 0); + + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + + request_headers = {{"x-envoy-internal", "true"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ("internal_updated_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_updated_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 2); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + request_trailers = Http::TestRequestTrailerMapImpl{}; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + + // Remove the files. + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); } -TEST_F(IpTaggingFilterTest, InvalidCidr) { - const std::string external_request_yaml = R"EOF( -request_type: external +TEST_F(IpTaggingFilterTest, InternalRequestWithFailedReloadUsesOldData) { + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(1)); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + + TestEnvironment::createPath(TestEnvironment::temporaryPath("ip_tagging_test")); + + const std::string yaml = + fmt::format(R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_refresh_rate: 2s + ip_tags_datasource: + filename: "{}" + watched_directory: + path: "{}" + )EOF", + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test")); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), R"EOF( ip_tags: - - ip_tag_name: fooooooo + - ip_tag_name: internal_request ip_list: - - {address_prefix: 12345.12345.12345.12345, prefix_len: 999999} -)EOF"; - initializeFilter( - external_request_yaml, - "INVALID_ARGUMENT: invalid ip/mask combo '12345.12345.12345.12345/999999' (format is " - "/<# mask bits>)"); + - {address_prefix: 1.2.3.5, prefix_len: 32} + )EOF", + true); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), R"EOF( +ip_tags + )EOF", + true); + initializeFilter(yaml); + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(1)); + + EXPECT_EQ(FilterRequestType::INTERNAL, config_->requestType()); + Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; + + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.5"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ("internal_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + Http::TestRequestTrailerMapImpl request_trailers; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + + // Check external requests don't get a tag. + request_headers = Http::TestRequestHeaderMapImpl{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_FALSE(request_headers.has(Http::Headers::get().EnvoyIpTags)); + + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(1)); + // Handle the events if any. + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + // Update the symlink to point to the new file. + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml")); + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml")); + + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(6)); + // Handle the events if any. + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + EXPECT_TRUE( + TestUtility::waitForCounterEq(stats_, "ip_tagging_reload.reload_error", 1UL, time_system_)); + + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + + request_headers = {{"x-envoy-internal", "true"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ("internal_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 2); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 2); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + request_trailers = Http::TestRequestTrailerMapImpl{}; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + + // Remove the files. + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + dispatcher_->exit(); +} + +TEST_F(IpTaggingFilterTest, IpTagsReloadedInFlightRequestsNotAffected) { + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(1)); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + + TestEnvironment::createPath(TestEnvironment::temporaryPath("ip_tagging_test")); + + const std::string yaml = + fmt::format(R"EOF( +request_type: internal +ip_tags_file_provider: + ip_tags_refresh_rate: 1s + ip_tags_datasource: + filename: "{}" + watched_directory: + path: "{}" + )EOF", + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test")); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} + )EOF", + true); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: internal_updated_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} + )EOF", + true); + initializeFilter(yaml); + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(1)); + EXPECT_EQ(FilterRequestType::INTERNAL, config_->requestType()); + IpTaggingFilterPeer::synchronizer(filter_).enable(); + std::string sync_point_name = "_trie_lookup_complete"; + // Start a thread that issues request for ip tagging filter and wait in the worker thread right + // before performing lookup from the trie with ip tags. + IpTaggingFilterPeer::synchronizer(filter_).waitOn(sync_point_name); + std::thread t0([&] { + Http::TestRequestHeaderMapImpl request_headers{{"x-envoy-internal", "true"}}; + + Network::Address::InstanceConstSharedPtr remote_address = + Network::Utility::parseInternetAddressNoThrow("1.2.3.5"); + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ("internal_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_request.hit").value(), 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 1); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + Http::TestRequestTrailerMapImpl request_trailers; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + // Second request should get the updated ip tags. + filter_callbacks_.stream_info_.downstream_connection_info_provider_->setRemoteAddress( + remote_address); + + request_headers = {{"x-envoy-internal", "true"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + EXPECT_EQ("internal_updated_request", request_headers.get_(Http::Headers::get().EnvoyIpTags)); + + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.internal_updated_request.hit").value(), + 1); + EXPECT_EQ(stats_.counterFromString("prefix.ip_tagging.total").value(), 2); + + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, false)); + request_trailers = Http::TestRequestTrailerMapImpl{}; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers)); + }); + // Wait until the thread is actually waiting. + IpTaggingFilterPeer::synchronizer(filter_).barrierOn(sync_point_name); + + // Update the symlink to point to the new file. + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml")); + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml")); + + time_system_.advanceTimeAsyncImpl(std::chrono::seconds(6)); + // Handle the events if any. + dispatcher_->run(Event::Dispatcher::RunType::NonBlock); + + EXPECT_TRUE( + TestUtility::waitForCounterEq(stats_, "ip_tagging_reload.reload_success", 1UL, time_system_)); + + IpTaggingFilterPeer::synchronizer(filter_).signal(sync_point_name); + t0.join(); + // Remove the files. + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + dispatcher_->exit(); } } // namespace diff --git a/test/extensions/filters/http/ip_tagging/ip_tagging_integration_test.cc b/test/extensions/filters/http/ip_tagging/ip_tagging_integration_test.cc index 0aef174daafa0..6404efe5693ed 100644 --- a/test/extensions/filters/http/ip_tagging/ip_tagging_integration_test.cc +++ b/test/extensions/filters/http/ip_tagging/ip_tagging_integration_test.cc @@ -1,4 +1,5 @@ #include "test/integration/http_integration.h" +#include "test/test_common/environment.h" namespace Envoy { namespace { @@ -13,7 +14,10 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, IpTaggingIntegrationTest, testing::ValuesIn(TestEnvironment::getIpVersionsForTest())); // Just IP tagging for now. -const char ExampleIpTaggingConfig[] = R"EOF( +const std::string ExampleIpTaggingConfig = R"EOF( + name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging request_type: both ip_tags: - ip_tag_name: external_request @@ -23,15 +27,192 @@ const char ExampleIpTaggingConfig[] = R"EOF( // Make sure that Envoy starts up with an ip tagging filter. TEST_P(IpTaggingIntegrationTest, IpTaggingV3StaticTypedStructConfig) { - config_helper_.prependFilter(absl::StrCat(R"EOF( -name: ip_tagging -typed_config: - "@type": type.googleapis.com/xds.type.v3.TypedStruct - type_url: type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging - value: + config_helper_.prependFilter(ExampleIpTaggingConfig); + initialize(); +} + +TEST_P(IpTaggingIntegrationTest, FileBasedIpTaggingWithReload) { + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + + TestEnvironment::createPath(TestEnvironment::temporaryPath("ip_tagging_test")); + + const std::string yaml = + fmt::format(R"EOF( + name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags_file_provider: + ip_tags_refresh_rate: 1s + ip_tags_datasource: + filename: "{}" + watched_directory: + path: "{}" )EOF", - ExampleIpTaggingConfig)); + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test")); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + )EOF", + true); + config_helper_.prependFilter(TestEnvironment::substitute(yaml)); initialize(); + test_server_->waitForCounterEq("ip_tagging_reload.reload_success", 1); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "1.2.3.4"}}); + + waitForNextUpstreamRequest(); + EXPECT_EQ(upstream_request_->headers().get(Http::Headers::get().EnvoyIpTags)[0]->value(), + "external_request"); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + timeSystem().advanceTimeWait(std::chrono::seconds(2)); + test_server_->waitForCounterGe("ip_tagging_reload.reload_success", 2); + // Simulate file update. + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: external_updated_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + )EOF", + true); + + // Update the symlink to point to the new file. + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml")); + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml")); + timeSystem().advanceTimeWait(std::chrono::seconds(2)); + test_server_->waitForCounterGe("ip_tagging_reload.reload_success", 3); + + response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "1.2.3.4"}}); + + waitForNextUpstreamRequest(); + EXPECT_EQ(upstream_request_->headers().get(Http::Headers::get().EnvoyIpTags)[0]->value(), + "external_updated_request"); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + + // Remove the files. + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); +} + +TEST_P(IpTaggingIntegrationTest, IptaggingFilterWithReloadNoCrashOnLdsUpdate) { + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); + + TestEnvironment::createPath(TestEnvironment::temporaryPath("ip_tagging_test")); + + const std::string yaml = + fmt::format(R"EOF( + name: ip_tagging + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ip_tagging.v3.IPTagging + request_type: both + ip_tags_file_provider: + ip_tags_refresh_rate: 1s + ip_tags_datasource: + filename: "{}" + watched_directory: + path: "{}" + )EOF", + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test")); + + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + )EOF", + true); + config_helper_.prependFilter(TestEnvironment::substitute(yaml)); + initialize(); + test_server_->waitForCounterEq("ip_tagging_reload.reload_success", 1); + + // LDS update to modify the listener and corresponding drain. + { + ConfigHelper new_config_helper(version_, config_helper_.bootstrap()); + new_config_helper.addConfigModifier( + [](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + auto* listener = bootstrap.mutable_static_resources()->mutable_listeners(0); + listener->mutable_listener_filters_timeout()->set_seconds(10); + }); + new_config_helper.setLds("1"); + test_server_->waitForGaugeEq("listener_manager.total_listeners_active", 1); + test_server_->waitForCounterEq("listener_manager.lds.update_success", 2); + test_server_->waitForGaugeEq("listener_manager.total_listeners_draining", 0); + } + timeSystem().advanceTimeWait(std::chrono::seconds(2)); + test_server_->waitForCounterGe("ip_tagging_reload.reload_success", 2); + + // Simulate file update. + TestEnvironment::writeStringToFileForTest( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), R"EOF( +ip_tags: + - ip_tag_name: external_updated_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} + )EOF", + true); + + // Update the symlink to point to the new file. + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml")); + TestEnvironment::renameFile( + TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml"), + TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml")); + timeSystem().advanceTimeWait(std::chrono::seconds(3)); + test_server_->waitForCounterGe("ip_tagging_reload.reload_success", 3); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeHeaderOnlyRequest( + Http::TestRequestHeaderMapImpl{{":method", "GET"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}, + {"x-forwarded-for", "1.2.3.4"}}); + + waitForNextUpstreamRequest(); + EXPECT_EQ(upstream_request_->headers().get(Http::Headers::get().EnvoyIpTags)[0]->value(), + "external_updated_request"); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + ASSERT_TRUE(response->waitForEndStream()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + + // Remove the files. + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_old_target.yaml").c_str()); + unlink(TestEnvironment::temporaryPath("ip_tagging_test/watcher_new_target.yaml").c_str()); } } // namespace diff --git a/test/extensions/filters/http/ip_tagging/test_data/BUILD b/test/extensions/filters/http/ip_tagging/test_data/BUILD new file mode 100644 index 0000000000000..92f9b6dfe9ac6 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/BUILD @@ -0,0 +1,13 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +filegroup( + name = "ip_tagging_files", + srcs = glob(["*"]), +) diff --git a/test/extensions/filters/http/ip_tagging/test_data/empty_file.yaml b/test/extensions/filters/http/ip_tagging/test_data/empty_file.yaml new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.json b/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.json new file mode 100644 index 0000000000000..3b12f20bec6c0 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.json @@ -0,0 +1 @@ +ip_tags diff --git a/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.yaml b/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.yaml new file mode 100644 index 0000000000000..3b12f20bec6c0 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/invalid_tags.yaml @@ -0,0 +1 @@ +ip_tags diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.json b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.json new file mode 100644 index 0000000000000..19481df2a6c62 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.json @@ -0,0 +1,22 @@ +{ +"ip_tags": [ + { + "ip_tag_name": "external_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + }, + { + "ip_tag_name": "internal_request", + "ip_list": [ + { + "address_prefix": "1.2.3.5", + "prefix_len": 32 + } + ] + } +] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.yaml b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.yaml new file mode 100644 index 0000000000000..639fce3146c6d --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_both.yaml @@ -0,0 +1,7 @@ +ip_tags: +- ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +- ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.json b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.json new file mode 100644 index 0000000000000..0b5f0ddc4f97d --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.json @@ -0,0 +1,13 @@ +{ +"ip_tags": [ + { + "ip_tag_name": "external_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + } +] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.yaml b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.yaml new file mode 100644 index 0000000000000..fbea0dc37e732 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_external_request.yaml @@ -0,0 +1,4 @@ +ip_tags: +- ip_tag_name: external_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.json b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.json new file mode 100644 index 0000000000000..ce55c2d9af943 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.json @@ -0,0 +1,13 @@ +{ + "ip_tags": [ + { + "ip_tag_name": "internal_request", + "ip_list": [ + { + "address_prefix": "1.2.3.5", + "prefix_len": 32 + } + ] + } + ] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.yaml b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.yaml new file mode 100644 index 0000000000000..f535ae03a6465 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_internal_request.yaml @@ -0,0 +1,4 @@ +ip_tags: +- ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_updated_external_request.json b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_updated_external_request.json new file mode 100644 index 0000000000000..cbe852f4fc078 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_updated_external_request.json @@ -0,0 +1,22 @@ +{ +"ip_tags": [ + { + "ip_tag_name": "external_updated_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + }, + { + "ip_tag_name": "gcp_zone_a_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + } +] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_updated_internal_request.yaml b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_updated_internal_request.yaml new file mode 100644 index 0000000000000..178179d3675c1 --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_updated_internal_request.yaml @@ -0,0 +1,4 @@ +ip_tags: +- ip_tag_name: internal_updated_request + ip_list: + - {address_prefix: 1.2.3.5, prefix_len: 32} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.json b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.json new file mode 100644 index 0000000000000..9e5aaedd7dfce --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.json @@ -0,0 +1,22 @@ +{ + "ip_tags": [ + { + "ip_tag_name": "duplicate_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + }, + { + "ip_tag_name": "internal_request", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + } + ] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.yaml b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.yaml new file mode 100644 index 0000000000000..340c59fc6af0f --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_duplicate_request.yaml @@ -0,0 +1,7 @@ +ip_tags: +- ip_tag_name: duplicate_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} +- ip_tag_name: internal_request + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.json b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.json new file mode 100644 index 0000000000000..036582c21edbe --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.json @@ -0,0 +1,13 @@ +{ +"ip_tags": [ + { + "ip_tag_name": "internal_request_with_optional_header", + "ip_list": [ + { + "address_prefix": "1.2.3.4", + "prefix_len": 32 + } + ] + } +] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.yaml b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.yaml new file mode 100644 index 0000000000000..9d0df491dc1ea --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ip_tags_with_header.yaml @@ -0,0 +1,4 @@ +ip_tags: +- ip_tag_name: internal_request_with_optional_header + ip_list: + - {address_prefix: 1.2.3.4, prefix_len: 32} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.json b/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.json new file mode 100644 index 0000000000000..a30a77b87a62c --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.json @@ -0,0 +1,13 @@ +{ + "ip_tags": [ + { + "ip_tag_name": "ipv6_request", + "ip_list": [ + { + "address_prefix": "2001:abcd:ef01:2345:6789:abcd:ef01:234", + "prefix_len": 64 + } + ] + } + ] +} diff --git a/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.yaml b/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.yaml new file mode 100644 index 0000000000000..9eb84ff19abde --- /dev/null +++ b/test/extensions/filters/http/ip_tagging/test_data/ipv6_request.yaml @@ -0,0 +1,4 @@ +ip_tags: +- ip_tag_name: ipv6_request + ip_list: + - {address_prefix: 2001:abcd:ef01:2345:6789:abcd:ef01:234, prefix_len: 64}