From be9807559ab631ee1721d583457c352b6d2a7a6e Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 14:14:52 +0000 Subject: [PATCH 1/8] Add sni_to_metadata Signed-off-by: Ben Plotnick --- CODEOWNERS | 2 + api/BUILD | 1 + .../filters/network/sni_to_metadata/v3/BUILD | 12 + .../sni_to_metadata/v3/sni_to_metadata.proto | 53 ++ api/versioning/BUILD | 1 + .../network_filters/sni_to_metadata.rst | 12 + source/extensions/extensions_build_config.bzl | 1 + source/extensions/extensions_metadata.yaml | 7 + .../filters/network/sni_to_metadata/BUILD | 39 ++ .../filters/network/sni_to_metadata/config.cc | 31 ++ .../filters/network/sni_to_metadata/config.h | 34 ++ .../filters/network/sni_to_metadata/filter.cc | 138 +++++ .../filters/network/sni_to_metadata/filter.h | 93 ++++ .../filters/network/sni_to_metadata/BUILD | 40 ++ .../network/sni_to_metadata/config_test.cc | 169 ++++++ .../network/sni_to_metadata/filter_test.cc | 518 ++++++++++++++++++ 16 files changed, 1151 insertions(+) create mode 100644 api/envoy/extensions/filters/network/sni_to_metadata/v3/BUILD create mode 100644 api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto create mode 100644 docs/root/configuration/listeners/network_filters/sni_to_metadata.rst create mode 100644 source/extensions/filters/network/sni_to_metadata/BUILD create mode 100644 source/extensions/filters/network/sni_to_metadata/config.cc create mode 100644 source/extensions/filters/network/sni_to_metadata/config.h create mode 100644 source/extensions/filters/network/sni_to_metadata/filter.cc create mode 100644 source/extensions/filters/network/sni_to_metadata/filter.h create mode 100644 test/extensions/filters/network/sni_to_metadata/BUILD create mode 100644 test/extensions/filters/network/sni_to_metadata/config_test.cc create mode 100644 test/extensions/filters/network/sni_to_metadata/filter_test.cc diff --git a/CODEOWNERS b/CODEOWNERS index 28a89e8b4671..d7f9c73fb327 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -69,6 +69,8 @@ extensions/filters/common/original_src @klarose @mattklein123 /*/extensions/filters/network/sni_cluster @rshriram @ggreenway # sni_dynamic_forward_proxy extension /*/extensions/filters/network/sni_dynamic_forward_proxy @rshriram @UNOWNED +# sni_to_metadata extension +/*/extensions/filters/network/sni_to_metadata @bplotnick @kbaichoo # tracers.datadog extension /*/extensions/tracers/datadog @dmehala @mattklein123 # tracers.xray extension diff --git a/api/BUILD b/api/BUILD index 8c411195c113..4765dc374118 100644 --- a/api/BUILD +++ b/api/BUILD @@ -254,6 +254,7 @@ proto_library( "//envoy/extensions/filters/network/set_filter_state/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3:pkg", + "//envoy/extensions/filters/network/sni_to_metadata/v3:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/header_to_metadata/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3:pkg", diff --git a/api/envoy/extensions/filters/network/sni_to_metadata/v3/BUILD b/api/envoy/extensions/filters/network/sni_to_metadata/v3/BUILD new file mode 100644 index 000000000000..bfc486330911 --- /dev/null +++ b/api/envoy/extensions/filters/network/sni_to_metadata/v3/BUILD @@ -0,0 +1,12 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/type/matcher/v3:pkg", + "@com_github_cncf_xds//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto b/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto new file mode 100644 index 000000000000..8769be5d93d4 --- /dev/null +++ b/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.sni_to_metadata.v3; + +import "envoy/type/matcher/v3/regex.proto"; + +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.sni_to_metadata.v3"; +option java_outer_classname = "SniToMetadataProto"; +option java_multiple_files = true; +option go_package = "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/sni_to_metadata/v3;sni_to_metadatav3"; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: SNI to Metadata Filter] + +// Configuration proto schema for ``envoy.extensions.filters.network.sni_to_metadata`` network filter. +// [#extension: envoy.filters.network.sni_to_metadata] +message SniToMetadataFilter { + // MetadataTarget defines where to store extracted metadata. + message MetadataTarget { + // The metadata namespace to use when storing the result. + // If empty, defaults to ``envoy.filters.network.sni_to_metadata`` + string metadata_namespace = 1; + + // The metadata key to use when storing the result. + string metadata_key = 2 [(validate.rules).string = {min_len: 1}]; + + // The metadata value to store. If empty, the entire matched SNI value will be used. + // This field supports capture group substitution using numbered groups from the regex pattern. + // For example: ``app-\\1-\\2`` where ``\\1`` and ``\\2`` refer to the first and second capture groups (note escaped backslashes). + string metadata_value = 3; + } + + // ConnectionRule defines a rule for extracting metadata from SNI. + message ConnectionRule { + // The regex pattern to match against the SNI value. + // Supports Google RE2 numbered capture groups. + // Example: ``^([^.]+)\.([^.]+)\.([^.]+)\.foo\.example\.com$`` + // If not specified, the rule will always match and use the entire SNI value. + type.matcher.v3.RegexMatcher pattern = 1; + + // List of metadata targets to populate when this rule matches. + // Each target can use capture groups from the regex pattern in its metadata_value. + // If no pattern is specified, metadata_value will be used as-is or default to the full SNI. + repeated MetadataTarget metadata_targets = 2 [(validate.rules).repeated = {min_items: 1}]; + } + + // List of connection rules to evaluate against the SNI. + // Rules are evaluated in order, and the first matching rule will be applied. + repeated ConnectionRule connection_rules = 1 [(validate.rules).repeated = {min_items: 1}]; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index cdad8e287906..4f7d07a85d7d 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -191,6 +191,7 @@ proto_library( "//envoy/extensions/filters/network/set_filter_state/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3:pkg", + "//envoy/extensions/filters/network/sni_to_metadata/v3:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/header_to_metadata/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/payload_to_metadata/v3:pkg", diff --git a/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst b/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst new file mode 100644 index 000000000000..2c4a5e3dbae8 --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst @@ -0,0 +1,12 @@ +.. _config_network_filters_sni_to_metadata: + +SNI to Metadata Filter +======================= + +.. attention:: + + SNI to Metadata Filter support should be considered alpha and not production ready. + +The SNI to Metadata Filter is a filter that extracts metadata from the SNI of the client connection and stores it in the connection dynamic metadata. + +It is able to conditionally extract based on regex patters as well as extract fields and format the metadata using regex capture groups. \ No newline at end of file diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 5e65a1131d3f..4f9334a71c86 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -236,6 +236,7 @@ EXTENSIONS = { "envoy.filters.network.set_filter_state": "//source/extensions/filters/network/set_filter_state:config", "envoy.filters.network.sni_cluster": "//source/extensions/filters/network/sni_cluster:config", "envoy.filters.network.sni_dynamic_forward_proxy": "//source/extensions/filters/network/sni_dynamic_forward_proxy:config", + "envoy.filters.network.sni_to_metadata": "//source/extensions/filters/network/sni_to_metadata:config", "envoy.filters.network.wasm": "//source/extensions/filters/network/wasm:config", "envoy.filters.network.zookeeper_proxy": "//source/extensions/filters/network/zookeeper_proxy:config", "envoy.filters.network.generic_proxy": "//source/extensions/filters/network/generic_proxy:config", diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 302c60991a7b..d0a55df1a6ef 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -804,6 +804,13 @@ envoy.filters.network.sni_dynamic_forward_proxy: status: alpha type_urls: - envoy.extensions.filters.network.sni_dynamic_forward_proxy.v3.FilterConfig +envoy.filters.network.sni_to_metadata: + categories: + - envoy.filters.network + security_posture: unknown + status: alpha + type_urls: + - envoy.extensions.filters.network.sni_to_metadata.v3.SniToMetadataFilter envoy.filters.network.tcp_proxy: categories: - envoy.filters.network diff --git a/source/extensions/filters/network/sni_to_metadata/BUILD b/source/extensions/filters/network/sni_to_metadata/BUILD new file mode 100644 index 000000000000..bb9698afd7d9 --- /dev/null +++ b/source/extensions/filters/network/sni_to_metadata/BUILD @@ -0,0 +1,39 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", + "envoy_proto_library", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":filter_lib", + "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", + "//envoy/registry", + "//source/common/common:logger_lib", + "//source/extensions/filters/network/common:factory_base_lib", + ], +) + +envoy_cc_library( + name = "filter_lib", + srcs = ["filter.cc"], + hdrs = ["filter.h"], + deps = [ + "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", + "//envoy/network:filter_interface", + "//envoy/network:connection_interface", + "//source/common/protobuf", + "//source/common/common:regex_lib", + "//source/common/common:logger_lib", + "@com_google_absl//absl/strings", + "@com_googlesource_code_re2//:re2", + ], +) diff --git a/source/extensions/filters/network/sni_to_metadata/config.cc b/source/extensions/filters/network/sni_to_metadata/config.cc new file mode 100644 index 000000000000..a36030adc10a --- /dev/null +++ b/source/extensions/filters/network/sni_to_metadata/config.cc @@ -0,0 +1,31 @@ +#include "source/extensions/filters/network/sni_to_metadata/config.h" + +#include "source/extensions/filters/network/sni_to_metadata/filter.h" +#include "source/common/protobuf/utility.h" + +#include "absl/strings/str_cat.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SniToMetadata { + +Network::FilterFactoryCb SniToMetadataFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, + Server::Configuration::FactoryContext& context) { + + ConfigSharedPtr filter_config = + std::make_shared(config, context.serverFactoryContext().regexEngine()); + + return [filter_config](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter(std::make_shared(filter_config)); + }; +} + +REGISTER_FACTORY(SniToMetadataFilterFactory, + Server::Configuration::NamedNetworkFilterConfigFactory); + +} // namespace SniToMetadata +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/sni_to_metadata/config.h b/source/extensions/filters/network/sni_to_metadata/config.h new file mode 100644 index 000000000000..de46189db581 --- /dev/null +++ b/source/extensions/filters/network/sni_to_metadata/config.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "envoy/server/filter_config.h" +#include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" +#include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.validate.h" + +#include "source/extensions/filters/network/common/factory_base.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SniToMetadata { + +/** + * Config registration for the SNI to metadata filter. @see NamedNetworkFilterConfigFactory. + */ +class SniToMetadataFilterFactory + : public Common::FactoryBase< + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter> { +public: + SniToMetadataFilterFactory() : FactoryBase("envoy.filters.network.sni_to_metadata") {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, + Server::Configuration::FactoryContext& context) override; +}; + +} // namespace SniToMetadata +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/sni_to_metadata/filter.cc b/source/extensions/filters/network/sni_to_metadata/filter.cc new file mode 100644 index 000000000000..f14bd3096452 --- /dev/null +++ b/source/extensions/filters/network/sni_to_metadata/filter.cc @@ -0,0 +1,138 @@ +#include "source/extensions/filters/network/sni_to_metadata/filter.h" + +#include + +#include "source/common/protobuf/protobuf.h" +#include "source/common/common/regex.h" +#include "source/common/common/utility.h" + +#include "envoy/common/exception.h" + +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SniToMetadata { + +Config::Config( + const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, + Regex::Engine& regex_engine) { + // Compile all connection rules + for (const auto& rule_config : config.connection_rules()) { + CompiledConnectionRule compiled_rule; + + // Compile the regex pattern if one is specified + if (rule_config.has_pattern()) { + auto regex_result = Regex::Utility::parseRegex(rule_config.pattern(), regex_engine); + THROW_IF_NOT_OK_REF(regex_result.status()); + compiled_rule.regex_matcher = std::move(regex_result.value()); + } + // If no pattern is specified, regex_matcher remains nullptr + + for (const auto& target : rule_config.metadata_targets()) { + compiled_rule.metadata_targets.push_back(target); + } + + compiled_rules_.push_back(std::move(compiled_rule)); + } +} + +Filter::Filter(ConfigSharedPtr config) : config_(std::move(config)), initialized_(false) {} + +Network::FilterStatus Filter::onData(Buffer::Instance&, bool) { + // Only process once per connection + if (initialized_) { + return Network::FilterStatus::Continue; + } + + initialized_ = true; + + absl::string_view sni = callbacks_->connection().requestedServerName(); + + if (sni.empty()) { + ENVOY_CONN_LOG(trace, "sni_to_metadata: no SNI found", callbacks_->connection()); + return Network::FilterStatus::Continue; + } + + ENVOY_CONN_LOG(trace, "sni_to_metadata: processing SNI {}", callbacks_->connection(), sni); + + bool any_rule_matched = processConnectionRules(sni); + + if (!any_rule_matched) { + ENVOY_CONN_LOG(debug, "sni_to_metadata: no rules matched SNI {}", callbacks_->connection(), + sni); + } + + return Network::FilterStatus::Continue; +} + +bool Filter::processConnectionRules(absl::string_view sni) { + bool any_matched = false; + + // Evaluate rules in order, stop at first match + for (const auto& rule : config_->compiledRules()) { + if (applyConnectionRule(rule, sni)) { + any_matched = true; + break; + } + } + + return any_matched; +} + +bool Filter::applyConnectionRule(const CompiledConnectionRule& rule, absl::string_view sni) { + // Check if rule matches + if (rule.regex_matcher != nullptr) { + // Rule has a regex pattern - check if it matches + if (!rule.regex_matcher->match(sni)) { + return false; + } + ENVOY_CONN_LOG(trace, "sni_to_metadata: regex rule matched SNI {}", callbacks_->connection(), + sni); + } else { + // Rule has no pattern - always matches + ENVOY_CONN_LOG(trace, "sni_to_metadata: pattern-less rule applied to SNI {}", + callbacks_->connection(), sni); + } + + // Apply metadata targets for this rule + bool metadata_applied = false; + for (const auto& target : rule.metadata_targets) { + std::string metadata_value; + + if (target.metadata_value().empty()) { + // Use the full SNI if no specific value is configured + metadata_value = std::string(sni); + } else if (rule.regex_matcher != nullptr) { + // Apply regex substitution using capture groups + metadata_value = rule.regex_matcher->replaceAll(sni, target.metadata_value()); + } else { + // No regex pattern - use metadata_value as-is (no substitution) + metadata_value = target.metadata_value(); + } + + if (!metadata_value.empty()) { + absl::string_view effective_namespace = target.metadata_namespace().empty() + ? DEFAULT_METADATA_NAMESPACE + : target.metadata_namespace(); + + // Store in dynamic metadata + Protobuf::Struct& metadata = (*callbacks_->connection() + .streamInfo() + .dynamicMetadata() + .mutable_filter_metadata())[effective_namespace]; + (*metadata.mutable_fields())[target.metadata_key()].set_string_value(std::move(metadata_value)); + + metadata_applied = true; + } + } + + return metadata_applied; +} + +} // namespace SniToMetadata +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/network/sni_to_metadata/filter.h b/source/extensions/filters/network/sni_to_metadata/filter.h new file mode 100644 index 000000000000..18b1c0abcecf --- /dev/null +++ b/source/extensions/filters/network/sni_to_metadata/filter.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/network/filter.h" +#include "envoy/network/connection.h" + +#include "source/common/common/logger.h" +#include "source/common/common/regex.h" + +#include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SniToMetadata { + +/** + * Represents a compiled connection rule with optional regex matcher and metadata targets. + */ +struct CompiledConnectionRule { + // Compiled regex pattern for SNI matching (nullptr if no pattern specified) + Regex::CompiledMatcherPtr regex_matcher; + + // List of metadata targets to populate when this rule matches + std::vector< + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter::MetadataTarget> + metadata_targets; +}; + +/** + * Configuration for the SniToMetadata filter. + */ +class Config { +public: + Config(const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, + Regex::Engine& regex_engine); + + const std::vector& compiledRules() const { return compiled_rules_; } + +private: + // Compiled connection rules ready for processing + std::vector compiled_rules_; +}; + +using ConfigSharedPtr = std::shared_ptr; + +/** + * A network filter that extracts SNI from the connection, applies regex patterns with capture + * groups, and stores the results into dynamic metadata according to the configured rules. + */ +class Filter : public Network::ReadFilter, Logger::Loggable { +public: + Filter(ConfigSharedPtr config); + + // Network::ReadFilter + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) override { + callbacks_ = &callbacks; + } + Network::FilterStatus onData(Buffer::Instance&, bool) override; + Network::FilterStatus onNewConnection() override { return Network::FilterStatus::Continue; } + +private: + /** + * Process SNI against all connection rules and apply matching metadata. + * Returns true if any rule matched and metadata was applied. + */ + bool processConnectionRules(absl::string_view sni); + + /** + * Apply a specific connection rule to the SNI value. + * Returns true if the rule matched and metadata was applied. + */ + bool applyConnectionRule(const CompiledConnectionRule& rule, absl::string_view sni); + + ConfigSharedPtr config_; + Network::ReadFilterCallbacks* callbacks_; + bool initialized_ : 1; + + // Default metadata namespace + static constexpr absl::string_view DEFAULT_METADATA_NAMESPACE = + "envoy.filters.network.sni_to_metadata"; +}; + +} // namespace SniToMetadata +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/sni_to_metadata/BUILD b/test/extensions/filters/network/sni_to_metadata/BUILD new file mode 100644 index 000000000000..b91d087dbba6 --- /dev/null +++ b/test/extensions/filters/network/sni_to_metadata/BUILD @@ -0,0 +1,40 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_package", +) + +load( + "//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "filter_test", + srcs = ["filter_test.cc"], + extension_names = ["envoy.filters.network.sni_to_metadata"], + rbe_pool = "6gig", + deps = [ + "//source/extensions/filters/network/sni_to_metadata:filter_lib", + "//test/mocks/network:network_mocks", + "//test/mocks/stream_info:stream_info_mocks", + "//test/mocks/server:factory_context_mocks", + "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", + ], +) + +envoy_extension_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + extension_names = ["envoy.filters.network.sni_to_metadata"], + rbe_pool = "6gig", + deps = [ + "//source/extensions/filters/network/sni_to_metadata:config", + "//test/mocks/server:factory_context_mocks", + "//test/mocks/network:network_mocks", + "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", + ], +) \ No newline at end of file diff --git a/test/extensions/filters/network/sni_to_metadata/config_test.cc b/test/extensions/filters/network/sni_to_metadata/config_test.cc new file mode 100644 index 000000000000..285af73ccd32 --- /dev/null +++ b/test/extensions/filters/network/sni_to_metadata/config_test.cc @@ -0,0 +1,169 @@ +#include "source/extensions/filters/network/sni_to_metadata/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/mocks/network/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SniToMetadata { + +// Test that the factory creates a filter config properly +TEST(SniToMetadataFilterConfigTest, ValidConfigProto) { + NiceMock context; + SniToMetadataFilterFactory factory; + + // Create a valid configuration + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("policy_name"); + target->set_metadata_namespace("envoy.filters.network.sni_to_metadata"); + target->set_metadata_value("app-\\1"); + + // Use the public interface to create filter factory + auto cb_result = factory.createFilterFactoryFromProto(config, context); + EXPECT_TRUE(cb_result.ok()); + Network::FilterFactoryCb cb = cb_result.value(); + EXPECT_TRUE(cb); + + // Test that the callback creates a filter when called + NiceMock filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +// Test config validation - invalid regex should fail +TEST(SniToMetadataFilterConfigTest, InvalidRegexFails) { + NiceMock context; + SniToMetadataFilterFactory factory; + + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex("[invalid regex pattern"); // Missing closing bracket + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("test_key"); + target->set_metadata_namespace("test.namespace"); + target->set_metadata_value("\\1"); + + // This should throw due to invalid regex + EXPECT_THROW(auto result = factory.createFilterFactoryFromProto(config, context), EnvoyException); +} + +// Test valid config with multiple rules and targets +TEST(SniToMetadataFilterConfigTest, ValidComplexConfig) { + NiceMock context; + SniToMetadataFilterFactory factory; + + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + // First rule + auto* rule1 = config.add_connection_rules(); + auto* pattern1 = rule1->mutable_pattern(); + pattern1->mutable_google_re2(); + pattern1->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + auto* target1 = rule1->add_metadata_targets(); + target1->set_metadata_key("app_name"); + target1->set_metadata_namespace("custom.metadata"); + target1->set_metadata_value("\\1"); + + auto* target2 = rule1->add_metadata_targets(); + target2->set_metadata_key("region"); + target2->set_metadata_namespace("custom.metadata"); + target2->set_metadata_value("\\2"); + + // Second rule + auto* rule2 = config.add_connection_rules(); + auto* pattern2 = rule2->mutable_pattern(); + pattern2->mutable_google_re2(); + pattern2->set_regex(R"(^([^.]+)\.service\.([^.]+)\.com$)"); + + auto* target3 = rule2->add_metadata_targets(); + target3->set_metadata_key("service_name"); + target3->set_metadata_namespace("another.namespace"); + target3->set_metadata_value("\\1"); + + // This should succeed + auto cb_result = factory.createFilterFactoryFromProto(config, context); + EXPECT_TRUE(cb_result.ok()); + Network::FilterFactoryCb cb = cb_result.value(); + EXPECT_TRUE(cb); + + // Test that the callback creates a filter when called + NiceMock filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +// Test default namespace behavior +TEST(SniToMetadataFilterConfigTest, DefaultNamespaceBehavior) { + NiceMock context; + SniToMetadataFilterFactory factory; + + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex(R"(^([^.]+)\..*$)"); + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("test_key"); + // metadata_namespace is intentionally left empty to test default behavior + target->set_metadata_value("\\1"); + + // This should succeed - empty namespace should use default + auto cb_result = factory.createFilterFactoryFromProto(config, context); + EXPECT_TRUE(cb_result.ok()); + Network::FilterFactoryCb cb = cb_result.value(); + EXPECT_TRUE(cb); + + // Test that the callback creates a filter when called + NiceMock filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +// Test valid config without pattern +TEST(SniToMetadataFilterConfigTest, ValidConfigWithoutPattern) { + NiceMock context; + SniToMetadataFilterFactory factory; + + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + auto* rule = config.add_connection_rules(); + // No pattern specified - should always match + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("full_sni"); + target->set_metadata_namespace("test.namespace"); + // metadata_value empty - should use full SNI + + // This should succeed + auto cb_result = factory.createFilterFactoryFromProto(config, context); + EXPECT_TRUE(cb_result.ok()); + Network::FilterFactoryCb cb = cb_result.value(); + EXPECT_TRUE(cb); + + // Test that the callback creates a filter when called + NiceMock filter_manager; + EXPECT_CALL(filter_manager, addReadFilter(_)); + cb(filter_manager); +} + +} // namespace SniToMetadata +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/sni_to_metadata/filter_test.cc b/test/extensions/filters/network/sni_to_metadata/filter_test.cc new file mode 100644 index 000000000000..86fc6252938a --- /dev/null +++ b/test/extensions/filters/network/sni_to_metadata/filter_test.cc @@ -0,0 +1,518 @@ +#include "source/extensions/filters/network/sni_to_metadata/filter.h" + +#include "test/mocks/network/mocks.h" +#include "test/mocks/stream_info/mocks.h" +#include "test/mocks/server/factory_context.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::_; +using testing::NiceMock; +using testing::Return; +using testing::ReturnRef; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace SniToMetadata { + +class SniToMetadataFilterTest : public testing::Test { +public: + void setUpFilter( + const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config) { + auto config_shared = + std::make_shared(config, context_.serverFactoryContext().regexEngine()); + filter_ = std::make_unique(config_shared); + filter_->initializeReadFilterCallbacks(filter_callbacks_); + + ON_CALL(filter_callbacks_, connection()).WillByDefault(ReturnRef(connection_)); + ON_CALL(connection_, streamInfo()).WillByDefault(ReturnRef(stream_info_)); + ON_CALL(Const(connection_), streamInfo()).WillByDefault(ReturnRef(stream_info_)); + ON_CALL(stream_info_, dynamicMetadata()).WillByDefault(ReturnRef(dynamic_metadata_)); + } + + // Helper to create a basic config with a single rule + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter createBasicConfig() { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("shard_id"); + target->set_metadata_namespace("envoy.filters.network.sni_to_metadata"); + target->set_metadata_value("shard-\\1-\\2"); + + return config; + } + +protected: + NiceMock context_; + std::unique_ptr filter_; + NiceMock filter_callbacks_; + NiceMock connection_; + NiceMock stream_info_; + envoy::config::core::v3::Metadata dynamic_metadata_; +}; + +// Test successful SNI extraction and metadata setting with capture groups +TEST_F(SniToMetadataFilterTest, SuccessfulSniExtractionWithCaptureGroups) { + auto config = createBasicConfig(); + setUpFilter(config); + + // Mock SNI from requestedServerName + ON_CALL(connection_, requestedServerName()) + .WillByDefault(Return("myapp.us-west-2.prod.example.com")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify the metadata was set correctly with capture group substitution + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("envoy.filters.network.sni_to_metadata")); + + const auto& metadata = filter_metadata.at("envoy.filters.network.sni_to_metadata"); + ASSERT_TRUE(metadata.fields().contains("shard_id")); + EXPECT_EQ(metadata.fields().at("shard_id").string_value(), "shard-myapp-us-west-2"); +} + +// Test multiple metadata targets with different capture groups +TEST_F(SniToMetadataFilterTest, MultipleCaptureGroupsInDifferentTargets) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + // First target: app name + auto* target1 = rule->add_metadata_targets(); + target1->set_metadata_key("app_name"); + target1->set_metadata_namespace("test.namespace"); + target1->set_metadata_value("\\1"); + + // Second target: region + auto* target2 = rule->add_metadata_targets(); + target2->set_metadata_key("region"); + target2->set_metadata_namespace("test.namespace"); + target2->set_metadata_value("\\2"); + + // Third target: environment + auto* target3 = rule->add_metadata_targets(); + target3->set_metadata_key("environment"); + target3->set_metadata_namespace("test.namespace"); + target3->set_metadata_value("\\3"); + + // Fourth target: combined value + auto* target4 = rule->add_metadata_targets(); + target4->set_metadata_key("combined"); + target4->set_metadata_namespace("test.namespace"); + target4->set_metadata_value("\\1-in-\\2-\\3"); + + setUpFilter(config); + + ON_CALL(connection_, requestedServerName()) + .WillByDefault(Return("webapp.us-east-1.test.example.com")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify all metadata was set correctly + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("test.namespace")); + + const auto& metadata = filter_metadata.at("test.namespace"); + ASSERT_TRUE(metadata.fields().contains("app_name")); + ASSERT_TRUE(metadata.fields().contains("region")); + ASSERT_TRUE(metadata.fields().contains("environment")); + ASSERT_TRUE(metadata.fields().contains("combined")); + + EXPECT_EQ(metadata.fields().at("app_name").string_value(), "webapp"); + EXPECT_EQ(metadata.fields().at("region").string_value(), "us-east-1"); + EXPECT_EQ(metadata.fields().at("environment").string_value(), "test"); + EXPECT_EQ(metadata.fields().at("combined").string_value(), "webapp-in-us-east-1-test"); +} + +// Test no SNI available +TEST_F(SniToMetadataFilterTest, NoSniAvailable) { + auto config = createBasicConfig(); + setUpFilter(config); + + // Mock empty SNI + ON_CALL(connection_, requestedServerName()).WillByDefault(Return("")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify no metadata was set + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + EXPECT_FALSE(filter_metadata.contains("envoy.filters.network.sni_to_metadata")); +} + +// Test SNI that doesn't match pattern +TEST_F(SniToMetadataFilterTest, SniDoesNotMatchPattern) { + auto config = createBasicConfig(); + setUpFilter(config); + + // Mock SNI that doesn't match the expected pattern + ON_CALL(connection_, requestedServerName()).WillByDefault(Return("invalid.pattern.com")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify no metadata was set + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + EXPECT_FALSE(filter_metadata.contains("envoy.filters.network.sni_to_metadata")); +} + +// Test multiple rules with first matching rule applied +TEST_F(SniToMetadataFilterTest, MultipleRulesFirstMatchWins) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + // First rule: matches example.com pattern + auto* rule1 = config.add_connection_rules(); + auto* pattern1 = rule1->mutable_pattern(); + pattern1->mutable_google_re2(); + pattern1->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + auto* target1 = rule1->add_metadata_targets(); + target1->set_metadata_key("source"); + target1->set_metadata_namespace("test.namespace"); + target1->set_metadata_value("example"); + + // Second rule: matches any pattern with 3 parts + auto* rule2 = config.add_connection_rules(); + auto* pattern2 = rule2->mutable_pattern(); + pattern2->mutable_google_re2(); + pattern2->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)$)"); + + auto* target2 = rule2->add_metadata_targets(); + target2->set_metadata_key("source"); + target2->set_metadata_namespace("test.namespace"); + target2->set_metadata_value("generic"); + + setUpFilter(config); + + ON_CALL(connection_, requestedServerName()) + .WillByDefault(Return("myapp.us-west-2.prod.example.com")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify only the first rule's metadata was applied + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("test.namespace")); + + const auto& metadata = filter_metadata.at("test.namespace"); + ASSERT_TRUE(metadata.fields().contains("source")); + EXPECT_EQ(metadata.fields().at("source").string_value(), "example"); +} + +// Test metadata target without metadata_value uses full SNI +TEST_F(SniToMetadataFilterTest, NoMetadataValueUsesFullSni) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("full_sni"); + target->set_metadata_namespace("test.namespace"); + // metadata_value is intentionally left empty + + setUpFilter(config); + + std::string test_sni = "myapp.us-west-2.prod.example.com"; + ON_CALL(connection_, requestedServerName()).WillByDefault(Return(test_sni)); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify the full SNI was used as metadata value + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("test.namespace")); + + const auto& metadata = filter_metadata.at("test.namespace"); + ASSERT_TRUE(metadata.fields().contains("full_sni")); + EXPECT_EQ(metadata.fields().at("full_sni").string_value(), test_sni); +} + +// Test default metadata namespace when not specified +TEST_F(SniToMetadataFilterTest, DefaultMetadataNamespace) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex(R"(^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$)"); + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("app_name"); + // metadata_namespace is intentionally left empty to test default + target->set_metadata_value("\\1"); + + setUpFilter(config); + + ON_CALL(connection_, requestedServerName()) + .WillByDefault(Return("testapp.region.env.example.com")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify the default namespace was used + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("envoy.filters.network.sni_to_metadata")); + + const auto& metadata = filter_metadata.at("envoy.filters.network.sni_to_metadata"); + ASSERT_TRUE(metadata.fields().contains("app_name")); + EXPECT_EQ(metadata.fields().at("app_name").string_value(), "testapp"); +} + +// Test that filter only processes once per connection +TEST_F(SniToMetadataFilterTest, OnlyProcessOncePerConnection) { + auto config = createBasicConfig(); + setUpFilter(config); + + ON_CALL(connection_, requestedServerName()) + .WillByDefault(Return("myapp.us-west-2.prod.example.com")); + + Buffer::OwnedImpl buffer1("test1"); + Buffer::OwnedImpl buffer2("test2"); + + // First call should process + Network::FilterStatus status1 = filter_->onData(buffer1, false); + EXPECT_EQ(Network::FilterStatus::Continue, status1); + + // Verify metadata was set + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("envoy.filters.network.sni_to_metadata")); + + const auto& metadata = filter_metadata.at("envoy.filters.network.sni_to_metadata"); + ASSERT_TRUE(metadata.fields().contains("shard_id")); + EXPECT_EQ(metadata.fields().at("shard_id").string_value(), "shard-myapp-us-west-2"); + + // Clear the metadata to verify second call doesn't set it again + dynamic_metadata_.clear_filter_metadata(); + + // Second call should skip processing + Network::FilterStatus status2 = filter_->onData(buffer2, false); + EXPECT_EQ(Network::FilterStatus::Continue, status2); + + // Verify no metadata was set on second call + const auto& filter_metadata2 = dynamic_metadata_.filter_metadata(); + EXPECT_FALSE(filter_metadata2.contains("envoy.filters.network.sni_to_metadata")); +} + +// Test complex regex patterns +TEST_F(SniToMetadataFilterTest, ComplexRegexPatterns) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + // More complex pattern that captures service, version, region, and environment + pattern->set_regex(R"(^([^.]+)-v(\d+)\.([^.]+)\.([^.]+)\.service\.company\.com$)"); + + auto* target1 = rule->add_metadata_targets(); + target1->set_metadata_key("service_name"); + target1->set_metadata_namespace("company.metadata"); + target1->set_metadata_value("\\1"); + + auto* target2 = rule->add_metadata_targets(); + target2->set_metadata_key("service_version"); + target2->set_metadata_namespace("company.metadata"); + target2->set_metadata_value("v\\2"); + + auto* target3 = rule->add_metadata_targets(); + target3->set_metadata_key("deployment"); + target3->set_metadata_namespace("company.metadata"); + target3->set_metadata_value("\\1-v\\2-\\3-\\4"); + + setUpFilter(config); + + ON_CALL(connection_, requestedServerName()) + .WillByDefault(Return("user-service-v42.us-west-2.production.service.company.com")); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify all metadata was set correctly + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("company.metadata")); + + const auto& metadata = filter_metadata.at("company.metadata"); + ASSERT_TRUE(metadata.fields().contains("service_name")); + ASSERT_TRUE(metadata.fields().contains("service_version")); + ASSERT_TRUE(metadata.fields().contains("deployment")); + + EXPECT_EQ(metadata.fields().at("service_name").string_value(), "user-service"); + EXPECT_EQ(metadata.fields().at("service_version").string_value(), "v42"); + EXPECT_EQ(metadata.fields().at("deployment").string_value(), + "user-service-v42-us-west-2-production"); +} + +// Test rule without pattern captures full SNI +TEST_F(SniToMetadataFilterTest, NoPatternCapturesFullSni) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + // No pattern specified - should always match + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("full_sni"); + target->set_metadata_namespace("test.namespace"); + // metadata_value is intentionally left empty to use full SNI + + setUpFilter(config); + + std::string test_sni = "example.com"; + ON_CALL(connection_, requestedServerName()).WillByDefault(Return(test_sni)); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify the full SNI was used as metadata value + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("test.namespace")); + + const auto& metadata = filter_metadata.at("test.namespace"); + ASSERT_TRUE(metadata.fields().contains("full_sni")); + EXPECT_EQ(metadata.fields().at("full_sni").string_value(), test_sni); +} + +// Test rule without pattern uses static metadata value +TEST_F(SniToMetadataFilterTest, NoPatternWithStaticValue) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + auto* rule = config.add_connection_rules(); + // No pattern specified - should always match + + auto* target1 = rule->add_metadata_targets(); + target1->set_metadata_key("service_type"); + target1->set_metadata_namespace("test.namespace"); + target1->set_metadata_value("web-service"); // Static value + + auto* target2 = rule->add_metadata_targets(); + target2->set_metadata_key("original_sni"); + target2->set_metadata_namespace("test.namespace"); + // metadata_value empty - should use full SNI + + setUpFilter(config); + + std::string test_sni = "api.example.com"; + ON_CALL(connection_, requestedServerName()).WillByDefault(Return(test_sni)); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify both metadata values were set correctly + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("test.namespace")); + + const auto& metadata = filter_metadata.at("test.namespace"); + ASSERT_TRUE(metadata.fields().contains("service_type")); + ASSERT_TRUE(metadata.fields().contains("original_sni")); + + EXPECT_EQ(metadata.fields().at("service_type").string_value(), "web-service"); + EXPECT_EQ(metadata.fields().at("original_sni").string_value(), test_sni); +} + +// Test mixed rules: one with pattern, one without +TEST_F(SniToMetadataFilterTest, MixedRulesPatternAndNoPattern) { + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + + // First rule: no pattern (should always match, but we want it to not match this SNI) + auto* rule1 = config.add_connection_rules(); + auto* pattern1 = rule1->mutable_pattern(); + pattern1->mutable_google_re2(); + pattern1->set_regex(R"(^api\.(.+)$)"); // Only matches SNIs starting with "api." + + auto* target1 = rule1->add_metadata_targets(); + target1->set_metadata_key("api_domain"); + target1->set_metadata_namespace("test.namespace"); + target1->set_metadata_value("\\1"); + + // Second rule: no pattern (fallback - should match anything) + auto* rule2 = config.add_connection_rules(); + // No pattern specified + + auto* target2 = rule2->add_metadata_targets(); + target2->set_metadata_key("fallback"); + target2->set_metadata_namespace("test.namespace"); + target2->set_metadata_value("default-service"); + + setUpFilter(config); + + // Test with SNI that doesn't match first rule + std::string test_sni = "webapp.example.com"; + ON_CALL(connection_, requestedServerName()).WillByDefault(Return(test_sni)); + + Buffer::OwnedImpl buffer("test"); + + Network::FilterStatus status = filter_->onData(buffer, false); + EXPECT_EQ(Network::FilterStatus::Continue, status); + + // Verify only the fallback rule was applied (first matching rule wins) + const auto& filter_metadata = dynamic_metadata_.filter_metadata(); + ASSERT_TRUE(filter_metadata.contains("test.namespace")); + + const auto& metadata = filter_metadata.at("test.namespace"); + EXPECT_FALSE(metadata.fields().contains("api_domain")); // First rule didn't match + ASSERT_TRUE(metadata.fields().contains("fallback")); // Second rule matched + + EXPECT_EQ(metadata.fields().at("fallback").string_value(), "default-service"); +} + +// Test invalid regex pattern returns error +TEST_F(SniToMetadataFilterTest, InvalidRegexReturnsError) { + NiceMock context; + + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + auto* rule = config.add_connection_rules(); + auto* pattern = rule->mutable_pattern(); + pattern->mutable_google_re2(); + pattern->set_regex("[invalid regex pattern"); // Missing closing bracket + + auto* target = rule->add_metadata_targets(); + target->set_metadata_key("test_key"); + target->set_metadata_namespace("test.namespace"); + target->set_metadata_value("\\1"); + + // This should fail to create the config + EXPECT_THROW( + { + auto config_shared = + std::make_shared(config, context.serverFactoryContext().regexEngine()); + }, + EnvoyException); +} + +} // namespace SniToMetadata +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file From 60613d1f9157789fbe35a5327d041ba43f9a9ae2 Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 15:36:00 +0000 Subject: [PATCH 2/8] make exception free Signed-off-by: Ben Plotnick --- .../filters/network/sni_to_metadata/BUILD | 4 +-- .../filters/network/sni_to_metadata/config.cc | 15 ++++++-- .../filters/network/sni_to_metadata/config.h | 14 ++++---- .../filters/network/sni_to_metadata/filter.cc | 7 ++-- .../filters/network/sni_to_metadata/filter.h | 10 ++++-- .../filters/network/well_known_names.h | 2 ++ .../network/sni_to_metadata/config_test.cc | 5 +-- .../network/sni_to_metadata/filter_test.cc | 36 +++++++++---------- 8 files changed, 56 insertions(+), 37 deletions(-) diff --git a/source/extensions/filters/network/sni_to_metadata/BUILD b/source/extensions/filters/network/sni_to_metadata/BUILD index bb9698afd7d9..eb3180a525e8 100644 --- a/source/extensions/filters/network/sni_to_metadata/BUILD +++ b/source/extensions/filters/network/sni_to_metadata/BUILD @@ -15,10 +15,10 @@ envoy_cc_library( hdrs = ["config.h"], deps = [ ":filter_lib", - "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", - "//envoy/registry", "//source/common/common:logger_lib", + "//source/extensions/filters/network:well_known_names", "//source/extensions/filters/network/common:factory_base_lib", + "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/network/sni_to_metadata/config.cc b/source/extensions/filters/network/sni_to_metadata/config.cc index a36030adc10a..5fbe6f6109ac 100644 --- a/source/extensions/filters/network/sni_to_metadata/config.cc +++ b/source/extensions/filters/network/sni_to_metadata/config.cc @@ -1,5 +1,7 @@ #include "source/extensions/filters/network/sni_to_metadata/config.h" +#include "envoy/registry/registry.h" + #include "source/extensions/filters/network/sni_to_metadata/filter.h" #include "source/common/protobuf/utility.h" @@ -10,12 +12,19 @@ namespace Extensions { namespace NetworkFilters { namespace SniToMetadata { -Network::FilterFactoryCb SniToMetadataFilterFactory::createFilterFactoryFromProtoTyped( - const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, +SniToMetadataFilterFactory::SniToMetadataFilterFactory() + : Common::ExceptionFreeFactoryBase( + NetworkFilterNames::get().SniToMetadata) {} + +absl::StatusOr SniToMetadataFilterFactory::createFilterFactoryFromProtoTyped( + const FilterConfig& config, Server::Configuration::FactoryContext& context) { + absl::Status creation_status = absl::OkStatus(); ConfigSharedPtr filter_config = - std::make_shared(config, context.serverFactoryContext().regexEngine()); + std::make_shared(config, context.serverFactoryContext().regexEngine(), creation_status); + + RETURN_IF_NOT_OK_REF(creation_status); return [filter_config](Network::FilterManager& filter_manager) -> void { filter_manager.addReadFilter(std::make_shared(filter_config)); diff --git a/source/extensions/filters/network/sni_to_metadata/config.h b/source/extensions/filters/network/sni_to_metadata/config.h index de46189db581..2f1bc1eb8ce0 100644 --- a/source/extensions/filters/network/sni_to_metadata/config.h +++ b/source/extensions/filters/network/sni_to_metadata/config.h @@ -2,29 +2,31 @@ #include -#include "envoy/server/filter_config.h" #include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" #include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.validate.h" #include "source/extensions/filters/network/common/factory_base.h" +#include "source/extensions/filters/network/well_known_names.h" namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace SniToMetadata { +using FilterConfig = + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter; + /** * Config registration for the SNI to metadata filter. @see NamedNetworkFilterConfigFactory. */ class SniToMetadataFilterFactory - : public Common::FactoryBase< - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter> { + : public Common::ExceptionFreeFactoryBase { public: - SniToMetadataFilterFactory() : FactoryBase("envoy.filters.network.sni_to_metadata") {} + SniToMetadataFilterFactory(); private: - Network::FilterFactoryCb createFilterFactoryFromProtoTyped( - const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, + absl::StatusOr createFilterFactoryFromProtoTyped( + const FilterConfig& config, Server::Configuration::FactoryContext& context) override; }; diff --git a/source/extensions/filters/network/sni_to_metadata/filter.cc b/source/extensions/filters/network/sni_to_metadata/filter.cc index f14bd3096452..92b3dbeb0ca7 100644 --- a/source/extensions/filters/network/sni_to_metadata/filter.cc +++ b/source/extensions/filters/network/sni_to_metadata/filter.cc @@ -17,8 +17,9 @@ namespace NetworkFilters { namespace SniToMetadata { Config::Config( - const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, - Regex::Engine& regex_engine) { + const FilterConfig& config, + Regex::Engine& regex_engine, + absl::Status& creation_status) { // Compile all connection rules for (const auto& rule_config : config.connection_rules()) { CompiledConnectionRule compiled_rule; @@ -26,7 +27,7 @@ Config::Config( // Compile the regex pattern if one is specified if (rule_config.has_pattern()) { auto regex_result = Regex::Utility::parseRegex(rule_config.pattern(), regex_engine); - THROW_IF_NOT_OK_REF(regex_result.status()); + SET_AND_RETURN_IF_NOT_OK(regex_result.status(), creation_status); compiled_rule.regex_matcher = std::move(regex_result.value()); } // If no pattern is specified, regex_matcher remains nullptr diff --git a/source/extensions/filters/network/sni_to_metadata/filter.h b/source/extensions/filters/network/sni_to_metadata/filter.h index 18b1c0abcecf..0b5129c7753f 100644 --- a/source/extensions/filters/network/sni_to_metadata/filter.h +++ b/source/extensions/filters/network/sni_to_metadata/filter.h @@ -20,6 +20,9 @@ namespace Extensions { namespace NetworkFilters { namespace SniToMetadata { +using FilterConfig = + envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter; + /** * Represents a compiled connection rule with optional regex matcher and metadata targets. */ @@ -29,7 +32,7 @@ struct CompiledConnectionRule { // List of metadata targets to populate when this rule matches std::vector< - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter::MetadataTarget> + FilterConfig::MetadataTarget> metadata_targets; }; @@ -38,8 +41,9 @@ struct CompiledConnectionRule { */ class Config { public: - Config(const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config, - Regex::Engine& regex_engine); + Config(const FilterConfig& config, + Regex::Engine& regex_engine, + absl::Status& creation_status); const std::vector& compiledRules() const { return compiled_rules_; } diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index 45483e7a66d8..ec73d126b92b 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -55,6 +55,8 @@ class NetworkFilterNameValues { const std::string SniCluster = "envoy.filters.network.sni_cluster"; // SNI Dynamic forward proxy filter const std::string SniDynamicForwardProxy = "envoy.filters.network.sni_dynamic_forward_proxy"; + // SNI to metadata filter + const std::string SniToMetadata = "envoy.filters.network.sni_to_metadata"; // ZooKeeper proxy filter const std::string ZooKeeperProxy = "envoy.filters.network.zookeeper_proxy"; // WebAssembly filter diff --git a/test/extensions/filters/network/sni_to_metadata/config_test.cc b/test/extensions/filters/network/sni_to_metadata/config_test.cc index 285af73ccd32..d98c671b97af 100644 --- a/test/extensions/filters/network/sni_to_metadata/config_test.cc +++ b/test/extensions/filters/network/sni_to_metadata/config_test.cc @@ -59,8 +59,9 @@ TEST(SniToMetadataFilterConfigTest, InvalidRegexFails) { target->set_metadata_namespace("test.namespace"); target->set_metadata_value("\\1"); - // This should throw due to invalid regex - EXPECT_THROW(auto result = factory.createFilterFactoryFromProto(config, context), EnvoyException); + // This should fail to create the config + auto result = factory.createFilterFactoryFromProto(config, context); + EXPECT_FALSE(result.ok()); } // Test valid config with multiple rules and targets diff --git a/test/extensions/filters/network/sni_to_metadata/filter_test.cc b/test/extensions/filters/network/sni_to_metadata/filter_test.cc index 86fc6252938a..5210e995d18e 100644 --- a/test/extensions/filters/network/sni_to_metadata/filter_test.cc +++ b/test/extensions/filters/network/sni_to_metadata/filter_test.cc @@ -20,9 +20,11 @@ namespace SniToMetadata { class SniToMetadataFilterTest : public testing::Test { public: void setUpFilter( - const envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter& config) { + const FilterConfig& config) { + absl::Status creation_status = absl::OkStatus(); auto config_shared = - std::make_shared(config, context_.serverFactoryContext().regexEngine()); + std::make_shared(config, context_.serverFactoryContext().regexEngine(), creation_status); + EXPECT_TRUE(creation_status.ok()); filter_ = std::make_unique(config_shared); filter_->initializeReadFilterCallbacks(filter_callbacks_); @@ -33,8 +35,8 @@ class SniToMetadataFilterTest : public testing::Test { } // Helper to create a basic config with a single rule - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter createBasicConfig() { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig createBasicConfig() { + FilterConfig config; auto* rule = config.add_connection_rules(); auto* pattern = rule->mutable_pattern(); @@ -83,7 +85,7 @@ TEST_F(SniToMetadataFilterTest, SuccessfulSniExtractionWithCaptureGroups) { // Test multiple metadata targets with different capture groups TEST_F(SniToMetadataFilterTest, MultipleCaptureGroupsInDifferentTargets) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); auto* pattern = rule->mutable_pattern(); @@ -178,7 +180,7 @@ TEST_F(SniToMetadataFilterTest, SniDoesNotMatchPattern) { // Test multiple rules with first matching rule applied TEST_F(SniToMetadataFilterTest, MultipleRulesFirstMatchWins) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; // First rule: matches example.com pattern auto* rule1 = config.add_connection_rules(); @@ -223,7 +225,7 @@ TEST_F(SniToMetadataFilterTest, MultipleRulesFirstMatchWins) { // Test metadata target without metadata_value uses full SNI TEST_F(SniToMetadataFilterTest, NoMetadataValueUsesFullSni) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); auto* pattern = rule->mutable_pattern(); @@ -256,7 +258,7 @@ TEST_F(SniToMetadataFilterTest, NoMetadataValueUsesFullSni) { // Test default metadata namespace when not specified TEST_F(SniToMetadataFilterTest, DefaultMetadataNamespace) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); auto* pattern = rule->mutable_pattern(); @@ -324,7 +326,7 @@ TEST_F(SniToMetadataFilterTest, OnlyProcessOncePerConnection) { // Test complex regex patterns TEST_F(SniToMetadataFilterTest, ComplexRegexPatterns) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); auto* pattern = rule->mutable_pattern(); @@ -374,7 +376,7 @@ TEST_F(SniToMetadataFilterTest, ComplexRegexPatterns) { // Test rule without pattern captures full SNI TEST_F(SniToMetadataFilterTest, NoPatternCapturesFullSni) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); // No pattern specified - should always match @@ -405,7 +407,7 @@ TEST_F(SniToMetadataFilterTest, NoPatternCapturesFullSni) { // Test rule without pattern uses static metadata value TEST_F(SniToMetadataFilterTest, NoPatternWithStaticValue) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); // No pattern specified - should always match @@ -444,7 +446,7 @@ TEST_F(SniToMetadataFilterTest, NoPatternWithStaticValue) { // Test mixed rules: one with pattern, one without TEST_F(SniToMetadataFilterTest, MixedRulesPatternAndNoPattern) { - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; // First rule: no pattern (should always match, but we want it to not match this SNI) auto* rule1 = config.add_connection_rules(); @@ -492,7 +494,7 @@ TEST_F(SniToMetadataFilterTest, MixedRulesPatternAndNoPattern) { TEST_F(SniToMetadataFilterTest, InvalidRegexReturnsError) { NiceMock context; - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter config; + FilterConfig config; auto* rule = config.add_connection_rules(); auto* pattern = rule->mutable_pattern(); pattern->mutable_google_re2(); @@ -504,12 +506,10 @@ TEST_F(SniToMetadataFilterTest, InvalidRegexReturnsError) { target->set_metadata_value("\\1"); // This should fail to create the config - EXPECT_THROW( - { + absl::Status creation_status = absl::OkStatus(); auto config_shared = - std::make_shared(config, context.serverFactoryContext().regexEngine()); - }, - EnvoyException); + std::make_shared(config, context.serverFactoryContext().regexEngine(), creation_status); + EXPECT_FALSE(creation_status.ok()); } } // namespace SniToMetadata From 8e7f06b97e91f15a11d46688c55531b390d6afa9 Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 15:37:34 +0000 Subject: [PATCH 3/8] format Signed-off-by: Ben Plotnick --- .../sni_to_metadata/v3/sni_to_metadata.proto | 2 +- .../filters/network/sni_to_metadata/BUILD | 9 ++++---- .../filters/network/sni_to_metadata/config.cc | 17 +++++++------- .../filters/network/sni_to_metadata/config.h | 14 +++++------ .../filters/network/sni_to_metadata/filter.cc | 23 +++++++++---------- .../filters/network/sni_to_metadata/filter.h | 20 ++++++---------- .../filters/network/sni_to_metadata/BUILD | 9 +++----- .../network/sni_to_metadata/config_test.cc | 4 ++-- .../network/sni_to_metadata/filter_test.cc | 15 ++++++------ 9 files changed, 49 insertions(+), 64 deletions(-) diff --git a/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto b/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto index 8769be5d93d4..816b240e062f 100644 --- a/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto +++ b/api/envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.proto @@ -37,7 +37,7 @@ message SniToMetadataFilter { message ConnectionRule { // The regex pattern to match against the SNI value. // Supports Google RE2 numbered capture groups. - // Example: ``^([^.]+)\.([^.]+)\.([^.]+)\.foo\.example\.com$`` + // Example: ``^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$`` // If not specified, the rule will always match and use the entire SNI value. type.matcher.v3.RegexMatcher pattern = 1; diff --git a/source/extensions/filters/network/sni_to_metadata/BUILD b/source/extensions/filters/network/sni_to_metadata/BUILD index eb3180a525e8..66784c8e2a6d 100644 --- a/source/extensions/filters/network/sni_to_metadata/BUILD +++ b/source/extensions/filters/network/sni_to_metadata/BUILD @@ -2,7 +2,6 @@ load( "//bazel:envoy_build_system.bzl", "envoy_cc_library", "envoy_extension_package", - "envoy_proto_library", ) licenses(["notice"]) # Apache 2 @@ -27,13 +26,13 @@ envoy_cc_library( srcs = ["filter.cc"], hdrs = ["filter.h"], deps = [ - "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", - "//envoy/network:filter_interface", "//envoy/network:connection_interface", - "//source/common/protobuf", - "//source/common/common:regex_lib", + "//envoy/network:filter_interface", "//source/common/common:logger_lib", + "//source/common/common:regex_lib", + "//source/common/protobuf", "@com_google_absl//absl/strings", "@com_googlesource_code_re2//:re2", + "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/network/sni_to_metadata/config.cc b/source/extensions/filters/network/sni_to_metadata/config.cc index 5fbe6f6109ac..7fb70423b0e8 100644 --- a/source/extensions/filters/network/sni_to_metadata/config.cc +++ b/source/extensions/filters/network/sni_to_metadata/config.cc @@ -2,8 +2,8 @@ #include "envoy/registry/registry.h" -#include "source/extensions/filters/network/sni_to_metadata/filter.h" #include "source/common/protobuf/utility.h" +#include "source/extensions/filters/network/sni_to_metadata/filter.h" #include "absl/strings/str_cat.h" @@ -13,16 +13,15 @@ namespace NetworkFilters { namespace SniToMetadata { SniToMetadataFilterFactory::SniToMetadataFilterFactory() - : Common::ExceptionFreeFactoryBase( - NetworkFilterNames::get().SniToMetadata) {} + : Common::ExceptionFreeFactoryBase(NetworkFilterNames::get().SniToMetadata) {} -absl::StatusOr SniToMetadataFilterFactory::createFilterFactoryFromProtoTyped( - const FilterConfig& config, - Server::Configuration::FactoryContext& context) { +absl::StatusOr +SniToMetadataFilterFactory::createFilterFactoryFromProtoTyped( + const FilterConfig& config, Server::Configuration::FactoryContext& context) { absl::Status creation_status = absl::OkStatus(); - ConfigSharedPtr filter_config = - std::make_shared(config, context.serverFactoryContext().regexEngine(), creation_status); + ConfigSharedPtr filter_config = std::make_shared( + config, context.serverFactoryContext().regexEngine(), creation_status); RETURN_IF_NOT_OK_REF(creation_status); @@ -37,4 +36,4 @@ REGISTER_FACTORY(SniToMetadataFilterFactory, } // namespace SniToMetadata } // namespace NetworkFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/source/extensions/filters/network/sni_to_metadata/config.h b/source/extensions/filters/network/sni_to_metadata/config.h index 2f1bc1eb8ce0..432174d60551 100644 --- a/source/extensions/filters/network/sni_to_metadata/config.h +++ b/source/extensions/filters/network/sni_to_metadata/config.h @@ -13,24 +13,22 @@ namespace Extensions { namespace NetworkFilters { namespace SniToMetadata { -using FilterConfig = - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter; +using FilterConfig = envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter; /** * Config registration for the SNI to metadata filter. @see NamedNetworkFilterConfigFactory. */ -class SniToMetadataFilterFactory - : public Common::ExceptionFreeFactoryBase { +class SniToMetadataFilterFactory : public Common::ExceptionFreeFactoryBase { public: SniToMetadataFilterFactory(); private: - absl::StatusOr createFilterFactoryFromProtoTyped( - const FilterConfig& config, - Server::Configuration::FactoryContext& context) override; + absl::StatusOr + createFilterFactoryFromProtoTyped(const FilterConfig& config, + Server::Configuration::FactoryContext& context) override; }; } // namespace SniToMetadata } // namespace NetworkFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/source/extensions/filters/network/sni_to_metadata/filter.cc b/source/extensions/filters/network/sni_to_metadata/filter.cc index 92b3dbeb0ca7..0b9306cd5bc4 100644 --- a/source/extensions/filters/network/sni_to_metadata/filter.cc +++ b/source/extensions/filters/network/sni_to_metadata/filter.cc @@ -2,11 +2,11 @@ #include -#include "source/common/protobuf/protobuf.h" +#include "envoy/common/exception.h" + #include "source/common/common/regex.h" #include "source/common/common/utility.h" - -#include "envoy/common/exception.h" +#include "source/common/protobuf/protobuf.h" #include "absl/status/status.h" #include "absl/strings/str_cat.h" @@ -16,10 +16,8 @@ namespace Extensions { namespace NetworkFilters { namespace SniToMetadata { -Config::Config( - const FilterConfig& config, - Regex::Engine& regex_engine, - absl::Status& creation_status) { +Config::Config(const FilterConfig& config, Regex::Engine& regex_engine, + absl::Status& creation_status) { // Compile all connection rules for (const auto& rule_config : config.connection_rules()) { CompiledConnectionRule compiled_rule; @@ -121,10 +119,11 @@ bool Filter::applyConnectionRule(const CompiledConnectionRule& rule, absl::strin // Store in dynamic metadata Protobuf::Struct& metadata = (*callbacks_->connection() - .streamInfo() - .dynamicMetadata() - .mutable_filter_metadata())[effective_namespace]; - (*metadata.mutable_fields())[target.metadata_key()].set_string_value(std::move(metadata_value)); + .streamInfo() + .dynamicMetadata() + .mutable_filter_metadata())[effective_namespace]; + (*metadata.mutable_fields())[target.metadata_key()].set_string_value( + std::move(metadata_value)); metadata_applied = true; } @@ -136,4 +135,4 @@ bool Filter::applyConnectionRule(const CompiledConnectionRule& rule, absl::strin } // namespace SniToMetadata } // namespace NetworkFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/source/extensions/filters/network/sni_to_metadata/filter.h b/source/extensions/filters/network/sni_to_metadata/filter.h index 0b5129c7753f..16a1fcf53711 100644 --- a/source/extensions/filters/network/sni_to_metadata/filter.h +++ b/source/extensions/filters/network/sni_to_metadata/filter.h @@ -1,18 +1,17 @@ #pragma once +#include #include #include -#include #include "envoy/buffer/buffer.h" -#include "envoy/network/filter.h" +#include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" #include "envoy/network/connection.h" +#include "envoy/network/filter.h" #include "source/common/common/logger.h" #include "source/common/common/regex.h" -#include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" - #include "absl/strings/string_view.h" namespace Envoy { @@ -20,8 +19,7 @@ namespace Extensions { namespace NetworkFilters { namespace SniToMetadata { -using FilterConfig = - envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter; +using FilterConfig = envoy::extensions::filters::network::sni_to_metadata::v3::SniToMetadataFilter; /** * Represents a compiled connection rule with optional regex matcher and metadata targets. @@ -31,9 +29,7 @@ struct CompiledConnectionRule { Regex::CompiledMatcherPtr regex_matcher; // List of metadata targets to populate when this rule matches - std::vector< - FilterConfig::MetadataTarget> - metadata_targets; + std::vector metadata_targets; }; /** @@ -41,9 +37,7 @@ struct CompiledConnectionRule { */ class Config { public: - Config(const FilterConfig& config, - Regex::Engine& regex_engine, - absl::Status& creation_status); + Config(const FilterConfig& config, Regex::Engine& regex_engine, absl::Status& creation_status); const std::vector& compiledRules() const { return compiled_rules_; } @@ -94,4 +88,4 @@ class Filter : public Network::ReadFilter, Logger::Loggable } // namespace SniToMetadata } // namespace NetworkFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/extensions/filters/network/sni_to_metadata/BUILD b/test/extensions/filters/network/sni_to_metadata/BUILD index b91d087dbba6..204151e9fa0f 100644 --- a/test/extensions/filters/network/sni_to_metadata/BUILD +++ b/test/extensions/filters/network/sni_to_metadata/BUILD @@ -2,7 +2,6 @@ load( "//bazel:envoy_build_system.bzl", "envoy_package", ) - load( "//test/extensions:extensions_build_system.bzl", "envoy_extension_cc_test", @@ -20,9 +19,8 @@ envoy_extension_cc_test( deps = [ "//source/extensions/filters/network/sni_to_metadata:filter_lib", "//test/mocks/network:network_mocks", - "//test/mocks/stream_info:stream_info_mocks", "//test/mocks/server:factory_context_mocks", - "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", + "//test/mocks/stream_info:stream_info_mocks", ], ) @@ -33,8 +31,7 @@ envoy_extension_cc_test( rbe_pool = "6gig", deps = [ "//source/extensions/filters/network/sni_to_metadata:config", - "//test/mocks/server:factory_context_mocks", "//test/mocks/network:network_mocks", - "@envoy_api//envoy/extensions/filters/network/sni_to_metadata/v3:pkg_cc_proto", + "//test/mocks/server:factory_context_mocks", ], -) \ No newline at end of file +) diff --git a/test/extensions/filters/network/sni_to_metadata/config_test.cc b/test/extensions/filters/network/sni_to_metadata/config_test.cc index d98c671b97af..210baf4478ea 100644 --- a/test/extensions/filters/network/sni_to_metadata/config_test.cc +++ b/test/extensions/filters/network/sni_to_metadata/config_test.cc @@ -1,7 +1,7 @@ #include "source/extensions/filters/network/sni_to_metadata/config.h" -#include "test/mocks/server/factory_context.h" #include "test/mocks/network/mocks.h" +#include "test/mocks/server/factory_context.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -167,4 +167,4 @@ TEST(SniToMetadataFilterConfigTest, ValidConfigWithoutPattern) { } // namespace SniToMetadata } // namespace NetworkFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/extensions/filters/network/sni_to_metadata/filter_test.cc b/test/extensions/filters/network/sni_to_metadata/filter_test.cc index 5210e995d18e..4a6fb482aa7a 100644 --- a/test/extensions/filters/network/sni_to_metadata/filter_test.cc +++ b/test/extensions/filters/network/sni_to_metadata/filter_test.cc @@ -1,8 +1,8 @@ #include "source/extensions/filters/network/sni_to_metadata/filter.h" #include "test/mocks/network/mocks.h" -#include "test/mocks/stream_info/mocks.h" #include "test/mocks/server/factory_context.h" +#include "test/mocks/stream_info/mocks.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -19,11 +19,10 @@ namespace SniToMetadata { class SniToMetadataFilterTest : public testing::Test { public: - void setUpFilter( - const FilterConfig& config) { + void setUpFilter(const FilterConfig& config) { absl::Status creation_status = absl::OkStatus(); - auto config_shared = - std::make_shared(config, context_.serverFactoryContext().regexEngine(), creation_status); + auto config_shared = std::make_shared( + config, context_.serverFactoryContext().regexEngine(), creation_status); EXPECT_TRUE(creation_status.ok()); filter_ = std::make_unique(config_shared); filter_->initializeReadFilterCallbacks(filter_callbacks_); @@ -507,12 +506,12 @@ TEST_F(SniToMetadataFilterTest, InvalidRegexReturnsError) { // This should fail to create the config absl::Status creation_status = absl::OkStatus(); - auto config_shared = - std::make_shared(config, context.serverFactoryContext().regexEngine(), creation_status); + auto config_shared = std::make_shared( + config, context.serverFactoryContext().regexEngine(), creation_status); EXPECT_FALSE(creation_status.ok()); } } // namespace SniToMetadata } // namespace NetworkFilters } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy From fb53e8a881fa2467a2789697b54669ab34526171 Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 16:40:21 +0000 Subject: [PATCH 4/8] Add docs Signed-off-by: Ben Plotnick --- .../network_filters/network_filters.rst | 1 + .../network_filters/sni_to_metadata.rst | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index 1cadbdc33bb6..a772a2583106 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -33,6 +33,7 @@ filters. set_filter_state sni_cluster_filter sni_dynamic_forward_proxy_filter + sni_to_metadata_filter tcp_proxy_filter thrift_proxy_filter wasm_filter diff --git a/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst b/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst index 2c4a5e3dbae8..db81eca61293 100644 --- a/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst +++ b/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst @@ -1,12 +1,28 @@ .. _config_network_filters_sni_to_metadata: -SNI to Metadata Filter +SNI-to-Metadata Filter ======================= .. attention:: - SNI to Metadata Filter support should be considered alpha and not production ready. + SNI-to-Metadata Filter support should be considered alpha and not production ready. -The SNI to Metadata Filter is a filter that extracts metadata from the SNI of the client connection and stores it in the connection dynamic metadata. -It is able to conditionally extract based on regex patters as well as extract fields and format the metadata using regex capture groups. \ No newline at end of file +The SNI-to-Metadata Filter is a filter that extracts the SNI of the client connection and stores it in the connection dynamic metadata. +It is able to conditionally extract based on regex patters as well as extract fields and format the metadata using regex capture groups. + +Example Configuration +---------------------- + +.. code-block:: yaml + + network_filters: + - name: envoy.filters.network.sni_to_metadata + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.sni_to_metadata.v3.SniToMetadataFilter + connection_rules: + - pattern: ^([^.]+)\.([^.]+)\.([^.]+)\.example\.com$ + metadata_targets: + - metadata_key: app_name + metadata_namespace: envoy.lb + metadata_value: \1 From 5a7c625080e0569d2b36eeacda2fb420a698d1df Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 16:50:59 +0000 Subject: [PATCH 5/8] Remove extraneous includes Signed-off-by: Ben Plotnick --- source/extensions/filters/network/sni_to_metadata/config.cc | 4 +--- source/extensions/filters/network/sni_to_metadata/config.h | 3 --- source/extensions/filters/network/sni_to_metadata/filter.h | 2 -- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/source/extensions/filters/network/sni_to_metadata/config.cc b/source/extensions/filters/network/sni_to_metadata/config.cc index 7fb70423b0e8..44c3f0d36d31 100644 --- a/source/extensions/filters/network/sni_to_metadata/config.cc +++ b/source/extensions/filters/network/sni_to_metadata/config.cc @@ -2,10 +2,8 @@ #include "envoy/registry/registry.h" -#include "source/common/protobuf/utility.h" #include "source/extensions/filters/network/sni_to_metadata/filter.h" - -#include "absl/strings/str_cat.h" +#include "source/extensions/filters/network/well_known_names.h" namespace Envoy { namespace Extensions { diff --git a/source/extensions/filters/network/sni_to_metadata/config.h b/source/extensions/filters/network/sni_to_metadata/config.h index 432174d60551..6a749761f473 100644 --- a/source/extensions/filters/network/sni_to_metadata/config.h +++ b/source/extensions/filters/network/sni_to_metadata/config.h @@ -1,12 +1,9 @@ #pragma once -#include - #include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" #include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.validate.h" #include "source/extensions/filters/network/common/factory_base.h" -#include "source/extensions/filters/network/well_known_names.h" namespace Envoy { namespace Extensions { diff --git a/source/extensions/filters/network/sni_to_metadata/filter.h b/source/extensions/filters/network/sni_to_metadata/filter.h index 16a1fcf53711..fb644d884ddf 100644 --- a/source/extensions/filters/network/sni_to_metadata/filter.h +++ b/source/extensions/filters/network/sni_to_metadata/filter.h @@ -1,12 +1,10 @@ #pragma once #include -#include #include #include "envoy/buffer/buffer.h" #include "envoy/extensions/filters/network/sni_to_metadata/v3/sni_to_metadata.pb.h" -#include "envoy/network/connection.h" #include "envoy/network/filter.h" #include "source/common/common/logger.h" From 7ec6c2de69331c71d5f3268889fd9983b405c402 Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 17:02:09 +0000 Subject: [PATCH 6/8] Add changelog Signed-off-by: Ben Plotnick --- changelogs/current.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelogs/current.yaml b/changelogs/current.yaml index cfcdfdb3df3b..c81f2015fd6b 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -523,5 +523,10 @@ new_features: change: | Added ``max_downstream_connection_duration_jitter_percentage`` to allow adding a jitter to the max downstream connection duration. This can be used to avoid thundering herd problems with many clients being disconnected and possibly reconnecting at the same time. +- area: sni_to_metadata + change: | + Added a new SNI-to-Metadata filter that extracts the SNI of the client connection and stores it in the connection dynamic metadata. + It is able to conditionally extract based on regex patters as well as extract fields and format the metadata using regex capture groups. + See :ref:`SNI-to-Metadata Filter ` for more details. deprecated: From 93798dd4dae36825d3aa0faeec9e69fd919bcb0f Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 20:10:28 +0000 Subject: [PATCH 7/8] envoy_cc_extension Signed-off-by: Ben Plotnick --- source/extensions/filters/network/sni_to_metadata/BUILD | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/extensions/filters/network/sni_to_metadata/BUILD b/source/extensions/filters/network/sni_to_metadata/BUILD index 66784c8e2a6d..ab0b1d4ed00a 100644 --- a/source/extensions/filters/network/sni_to_metadata/BUILD +++ b/source/extensions/filters/network/sni_to_metadata/BUILD @@ -1,5 +1,6 @@ load( "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", "envoy_cc_library", "envoy_extension_package", ) @@ -8,7 +9,7 @@ licenses(["notice"]) # Apache 2 envoy_extension_package() -envoy_cc_library( +envoy_cc_extension( name = "config", srcs = ["config.cc"], hdrs = ["config.h"], From a948a8a4b7ebcee4def6518d65ecb6efcfbc7f21 Mon Sep 17 00:00:00 2001 From: Ben Plotnick Date: Mon, 29 Sep 2025 21:02:22 +0000 Subject: [PATCH 8/8] Fix docs ref Signed-off-by: Ben Plotnick --- .../{sni_to_metadata.rst => sni_to_metadata_filter.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/root/configuration/listeners/network_filters/{sni_to_metadata.rst => sni_to_metadata_filter.rst} (100%) diff --git a/docs/root/configuration/listeners/network_filters/sni_to_metadata.rst b/docs/root/configuration/listeners/network_filters/sni_to_metadata_filter.rst similarity index 100% rename from docs/root/configuration/listeners/network_filters/sni_to_metadata.rst rename to docs/root/configuration/listeners/network_filters/sni_to_metadata_filter.rst