From af1e16c48aa120c7f5b58563a7fe85a91ee13ba4 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Thu, 31 Mar 2022 10:07:22 -0700 Subject: [PATCH 1/9] initial Signed-off-by: Kuat Yessenov --- api/bazel/repository_locations.bzl | 6 +- source/extensions/common/matcher/BUILD | 12 ++ .../common/matcher/domain_matcher.cc | 21 +++ .../common/matcher/domain_matcher.h | 145 ++++++++++++++++++ 4 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 source/extensions/common/matcher/domain_matcher.cc create mode 100644 source/extensions/common/matcher/domain_matcher.h diff --git a/api/bazel/repository_locations.bzl b/api/bazel/repository_locations.bzl index 98fa8906b1dec..b79a09d1ea618 100644 --- a/api/bazel/repository_locations.bzl +++ b/api/bazel/repository_locations.bzl @@ -33,9 +33,9 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_desc = "xDS API Working Group (xDS-WG)", project_url = "https://github.com/cncf/xds", # During the UDPA -> xDS migration, we aren't working with releases. - version = "7f1daf1720fc185f3b63f70d25aefaeef83d88d7", - sha256 = "62c0daaff43fd9a62c280bf2b0c2b670372b24377ea5e9ea4302cf748dd53cba", - release_date = "2022-03-14", + version = "eded343319d09f30032952beda9840bbd3dcf7ac", + sha256 = "79f15d6d3e9b2948174f583f6ad435a63e84b1e84acfc9ae2587be6798e44590", + release_date = "2022-03-30", strip_prefix = "xds-{version}", urls = ["https://github.com/cncf/xds/archive/{version}.tar.gz"], use_category = ["api"], diff --git a/source/extensions/common/matcher/BUILD b/source/extensions/common/matcher/BUILD index a4b96c3f92bfa..7995c12027060 100644 --- a/source/extensions/common/matcher/BUILD +++ b/source/extensions/common/matcher/BUILD @@ -36,3 +36,15 @@ envoy_cc_extension( "@com_github_cncf_udpa//xds/type/matcher/v3:pkg_cc_proto", ], ) + +envoy_cc_extension( + name = "domain_matcher_lib", + srcs = ["domain_matcher.cc"], + hdrs = ["domain_matcher.h"], + deps = [ + "//envoy/matcher:matcher_interface", + "//envoy/registry", + "//source/common/matcher:matcher_lib", + "@com_github_cncf_udpa//xds/type/matcher/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/common/matcher/domain_matcher.cc b/source/extensions/common/matcher/domain_matcher.cc new file mode 100644 index 0000000000000..256da5d249358 --- /dev/null +++ b/source/extensions/common/matcher/domain_matcher.cc @@ -0,0 +1,21 @@ +#include "source/extensions/common/matcher/domain_matcher.h" + +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { + +class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; +class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; + +REGISTER_FACTORY(TcpServerNameMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); +//REGISTER_FACTORY(HttpServerNameMatcherFactory, +// ::Envoy::Matcher::CustomMatcherFactory); + +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h new file mode 100644 index 0000000000000..1bd4a578b3a95 --- /dev/null +++ b/source/extensions/common/matcher/domain_matcher.h @@ -0,0 +1,145 @@ +#pragma once + +#include "envoy/matcher/matcher.h" +#include "envoy/network/filter.h" +#include "envoy/server/factory_context.h" + +#include "source/common/matcher/matcher.h" + +#include "xds/type/matcher/v3/domain.pb.h" +#include "xds/type/matcher/v3/domain.pb.validate.h" + +#include "absl/strings/match.h" +#include "absl/strings/ascii.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { + +namespace { +bool isWildcardServerName(const std::string& name) { + return absl::StartsWith(name, "*.") || name == "*"; +} + +/** + * A "compressed" trie node with domain parts as values. + */ +class DomainNode { +private: + absl::flat_hash_map> children_; + absl::optional> exact_on_match_; + absl::optional> prefix_on_match_; +}; +} // namespace + +using ::Envoy::Matcher::DataInputFactoryCb; +using ::Envoy::Matcher::DataInputGetResult; +using ::Envoy::Matcher::DataInputPtr; +using ::Envoy::Matcher::evaluateMatch; +using ::Envoy::Matcher::MatchState; +using ::Envoy::Matcher::MatchTree; +using ::Envoy::Matcher::OnMatch; +using ::Envoy::Matcher::OnMatchFactory; +using ::Envoy::Matcher::OnMatchFactoryCb; + +/** + * Implementation of a `sublinear` domain matcher. +template +class TrieMatcher : public MatchTree { +public: + TrieMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, + const std::shared_ptr>>& trie) + : data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), trie_(trie) {} + + typename MatchTree::MatchResult match(const DataType& data) override { + const auto input = data_input_->get(data); + if (input.data_availability_ != DataInputGetResult::DataAvailability::AllDataAvailable) { + return {MatchState::UnableToMatch, absl::nullopt}; + } + if (!input.data_) { + return {MatchState::MatchComplete, on_no_match_}; + } + const Network::Address::InstanceConstSharedPtr addr = + Network::Utility::parseInternetAddressNoThrow(*input.data_); + if (!addr) { + return {MatchState::MatchComplete, on_no_match_}; + } + auto values = trie_->getData(addr); + // The candidates returned by the LC trie are not in any specific order, so we + // sort them by the prefix length first (longest first), and the order of declaration second. + std::sort(values.begin(), values.end(), TrieNodeComparator()); + bool first = true; + for (const auto node : values) { + if (!first && node.exclusive_) { + continue; + } + if (node.on_match_->action_cb_) { + return {MatchState::MatchComplete, OnMatch{node.on_match_->action_cb_, nullptr}}; + } + // Resume any subtree matching to preserve backtracking progress. + auto matched = evaluateMatch(*node.on_match_->matcher_, data); + if (matched.match_state_ == MatchState::UnableToMatch) { + return {MatchState::UnableToMatch, absl::nullopt}; + } + if (matched.match_state_ == MatchState::MatchComplete && matched.result_) { + return {MatchState::MatchComplete, OnMatch{matched.result_, nullptr}}; + } + if (first) { + first = false; + } + } + return {MatchState::MatchComplete, on_no_match_}; + } + +private: + const DataInputPtr data_input_; + const absl::optional> on_no_match_; + std::shared_ptr>> trie_; +}; + */ + +template +class DomainMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory { +public: + ::Envoy::Matcher::MatchTreeFactoryCb + createCustomMatcherFactoryCb(const Protobuf::Message& config, + Server::Configuration::ServerFactoryContext& factory_context, + DataInputFactoryCb data_input, + absl::optional> on_no_match, + OnMatchFactory& on_match_factory) override { + const auto& typed_config = + MessageUtil::downcastAndValidate( + config, factory_context.messageValidationVisitor()); + absl::flat_hash_map> server_names; + for (const auto& domain_matcher : typed_config.domain_matchers()) { + OnMatch on_match = *on_match_factory.createOnMatch(domain_matcher.on_match()).value()(); + if (server_name.find('*') != std::string::npos && !isWildcardServerName(server_name)) { + throw EnvoyException(fmt::format("invalid domain wildcard: {}", server_name)); + } + for (const auto& server_name : domain_matcher.domains()) { + // Reject internationalized domains to avoid ambiguity with case sensitivity. + if (!absl::ascii_isascii(server_name)) { + throw EnvoyException(fmt::format("non-ASCII domains are not supported: {}", server_name)); + } + auto [_, inserted] = server_names.try_emplace(ascii::AsciiStrToLower(server_name), on_match); + if (!inserted) { + throw EnvoyException(fmt::format("duplicate domain: {}", server_name)); + } + } + } + return [data_input, on_no_match]() { + return std::make_unique>( + data_input(), on_no_match ? absl::make_optional(on_no_match.value()()) : absl::nullopt); + }; + }; + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + std::string name() const override { return "envoy.matching.custom_matchers.domain_matcher"; } +}; + +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy From 34974604b263657055f1f43bb6280e82b34d8f0a Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Mon, 4 Apr 2022 09:25:04 -0700 Subject: [PATCH 2/9] matchers: add server name matcher Signed-off-by: Kuat Yessenov --- source/extensions/common/matcher/BUILD | 3 + .../common/matcher/domain_matcher.cc | 37 +- .../common/matcher/domain_matcher.h | 212 +++++++---- .../extensions/common/matcher/trie_matcher.cc | 8 + .../extensions/common/matcher/trie_matcher.h | 4 - source/extensions/extensions_build_config.bzl | 1 + source/extensions/extensions_metadata.yaml | 7 + test/extensions/common/matcher/BUILD | 29 +- .../common/matcher/custom_matcher_test.h | 80 ++++ .../common/matcher/domain_matcher_test.cc | 351 ++++++++++++++++++ .../common/matcher/trie_matcher_test.cc | 63 +--- 11 files changed, 643 insertions(+), 152 deletions(-) create mode 100644 test/extensions/common/matcher/custom_matcher_test.h create mode 100644 test/extensions/common/matcher/domain_matcher_test.cc diff --git a/source/extensions/common/matcher/BUILD b/source/extensions/common/matcher/BUILD index 7995c12027060..73a145776c98d 100644 --- a/source/extensions/common/matcher/BUILD +++ b/source/extensions/common/matcher/BUILD @@ -26,6 +26,7 @@ envoy_cc_extension( srcs = ["trie_matcher.cc"], hdrs = ["trie_matcher.h"], deps = [ + "//envoy/http:filter_interface", "//envoy/matcher:matcher_interface", "//envoy/network:filter_interface", "//envoy/registry", @@ -42,7 +43,9 @@ envoy_cc_extension( srcs = ["domain_matcher.cc"], hdrs = ["domain_matcher.h"], deps = [ + "//envoy/http:filter_interface", "//envoy/matcher:matcher_interface", + "//envoy/network:filter_interface", "//envoy/registry", "//source/common/matcher:matcher_lib", "@com_github_cncf_udpa//xds/type/matcher/v3:pkg_cc_proto", diff --git a/source/extensions/common/matcher/domain_matcher.cc b/source/extensions/common/matcher/domain_matcher.cc index 256da5d249358..4bcf60b582df7 100644 --- a/source/extensions/common/matcher/domain_matcher.cc +++ b/source/extensions/common/matcher/domain_matcher.cc @@ -1,5 +1,7 @@ #include "source/extensions/common/matcher/domain_matcher.h" +#include "envoy/http/filter.h" +#include "envoy/network/filter.h" #include "envoy/registry/registry.h" namespace Envoy { @@ -7,13 +9,38 @@ namespace Extensions { namespace Common { namespace Matcher { -class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; -class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; +namespace { +bool isWildcardServerName(const std::string& name) { + return absl::StartsWith(name, "*.") || name == "*"; +} +} // namespace -REGISTER_FACTORY(TcpServerNameMatcherFactory, +void DomainMatcherUtility::validateServerName(const std::string& server_name) { + if (server_name.find('*') != std::string::npos && !isWildcardServerName(server_name)) { + throw EnvoyException(fmt::format("invalid domain wildcard: {}", server_name)); + } + // Reject internationalized domains to avoid ambiguity with case sensitivity. + for (char c : server_name) { + if (!absl::ascii_isascii(c)) { + throw EnvoyException(fmt::format("non-ASCII domains are not supported: {}", server_name)); + } + } +} + +void DomainMatcherUtility::duplicateServerNameError(const std::string& server_name) { + throw EnvoyException(fmt::format("duplicate domain: {}", server_name)); +} + +class NetworkDomainMatcherFactory : public DomainMatcherFactoryBase {}; +class UdpNetworkDomainMatcherFactory : public DomainMatcherFactoryBase {}; +class HttpDomainMatcherFactory : public DomainMatcherFactoryBase {}; + +REGISTER_FACTORY(NetworkDomainMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); -//REGISTER_FACTORY(HttpServerNameMatcherFactory, -// ::Envoy::Matcher::CustomMatcherFactory); +REGISTER_FACTORY(UdpNetworkDomainMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); +REGISTER_FACTORY(HttpDomainMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); } // namespace Matcher } // namespace Common diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h index 1bd4a578b3a95..5726c3cd0c848 100644 --- a/source/extensions/common/matcher/domain_matcher.h +++ b/source/extensions/common/matcher/domain_matcher.h @@ -1,56 +1,112 @@ #pragma once +#include "envoy/common/optref.h" #include "envoy/matcher/matcher.h" -#include "envoy/network/filter.h" #include "envoy/server/factory_context.h" #include "source/common/matcher/matcher.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/ascii.h" +#include "absl/strings/match.h" +#include "absl/strings/str_split.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" #include "xds/type/matcher/v3/domain.pb.h" #include "xds/type/matcher/v3/domain.pb.validate.h" -#include "absl/strings/match.h" -#include "absl/strings/ascii.h" - namespace Envoy { namespace Extensions { namespace Common { namespace Matcher { +using ::Envoy::Matcher::DataInputGetResult; +using ::Envoy::Matcher::DataInputPtr; +using ::Envoy::Matcher::evaluateMatch; +using ::Envoy::Matcher::MatchState; +using ::Envoy::Matcher::MatchTree; +using ::Envoy::Matcher::OnMatch; + namespace { -bool isWildcardServerName(const std::string& name) { - return absl::StartsWith(name, "*.") || name == "*"; -} /** - * A "compressed" trie node with domain parts as values. + * A "compressed" trie node with domain parts as edge values. */ -class DomainNode { -private: - absl::flat_hash_map> children_; - absl::optional> exact_on_match_; - absl::optional> prefix_on_match_; +template struct DomainNode { + absl::flat_hash_map> children_; + // Matches the exact path from the root. + OptRef> exact_on_match_; + // Matches the prefix for the path from the root. + OptRef> prefix_on_match_; + // Closest parent node with a prefix match. + OptRef> parent_prefix_; + + // Insert an on-match for the parts of the domain by handling the last optional + // wildcard symbol as a special non-part. + void insert(absl::Span parts, OnMatch& on_match) { + if (parts.empty()) { + ASSERT(!exact_on_match_); + exact_on_match_ = makeOptRef(on_match); + } else if (parts[0] == "*") { + // Prefix wildcards are unique and wildcard must be the last part. + ASSERT(!prefix_on_match_ && parts.size() == 1); + prefix_on_match_ = makeOptRef(on_match); + } else { + DomainNode& child = children_[parts[0]]; + return child.insert(parts.subspan(1), on_match); + } + } + + // Link parent prefix from child nodes. + void link(OptRef> parent_prefix) { + parent_prefix_ = parent_prefix; + if (prefix_on_match_) { + parent_prefix = makeOptRef(*this); + } + for (auto& [_, child] : children_) { + child.link(parent_prefix); + } + } + + // Find the deepest node matching the reversed parts of the domain. + // Returns a node and a bool indicating whether the match is exact for this node. + std::pair find(absl::Span parts) const { + if (parts.empty()) { + return std::make_pair(this, true); + } + auto it = children_.find(parts[0]); + if (it != children_.end()) { + return it->second.find(parts.subspan(1)); + } + return std::make_pair(this, false); + } +}; + +template struct DomainTree { + DomainNode root_; + std::vector> on_matches_; }; + } // namespace -using ::Envoy::Matcher::DataInputFactoryCb; -using ::Envoy::Matcher::DataInputGetResult; -using ::Envoy::Matcher::DataInputPtr; -using ::Envoy::Matcher::evaluateMatch; -using ::Envoy::Matcher::MatchState; -using ::Envoy::Matcher::MatchTree; -using ::Envoy::Matcher::OnMatch; -using ::Envoy::Matcher::OnMatchFactory; -using ::Envoy::Matcher::OnMatchFactoryCb; +/** + * General utilities for domain name matching. + */ +class DomainMatcherUtility { +public: + static void validateServerName(const std::string& server_name); + static void duplicateServerNameError(const std::string& server_name); +}; /** - * Implementation of a `sublinear` domain matcher. -template -class TrieMatcher : public MatchTree { + * Implementation of a sublinear domain matcher using a trie. + **/ +template class DomainMatcher : public MatchTree { public: - TrieMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, - const std::shared_ptr>>& trie) - : data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), trie_(trie) {} + DomainMatcher(DataInputPtr&& data_input, absl::optional> on_no_match, + const std::shared_ptr>& domain_tree) + : data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), + domain_tree_(domain_tree) {} typename MatchTree::MatchResult match(const DataType& data) override { const auto input = data_input_->get(data); @@ -60,34 +116,31 @@ class TrieMatcher : public MatchTree { if (!input.data_) { return {MatchState::MatchComplete, on_no_match_}; } - const Network::Address::InstanceConstSharedPtr addr = - Network::Utility::parseInternetAddressNoThrow(*input.data_); - if (!addr) { - return {MatchState::MatchComplete, on_no_match_}; - } - auto values = trie_->getData(addr); - // The candidates returned by the LC trie are not in any specific order, so we - // sort them by the prefix length first (longest first), and the order of declaration second. - std::sort(values.begin(), values.end(), TrieNodeComparator()); - bool first = true; - for (const auto node : values) { - if (!first && node.exclusive_) { - continue; - } - if (node.on_match_->action_cb_) { - return {MatchState::MatchComplete, OnMatch{node.on_match_->action_cb_, nullptr}}; - } - // Resume any subtree matching to preserve backtracking progress. - auto matched = evaluateMatch(*node.on_match_->matcher_, data); - if (matched.match_state_ == MatchState::UnableToMatch) { - return {MatchState::UnableToMatch, absl::nullopt}; + std::vector parts = absl::StrSplit(*input.data_, "."); + std::reverse(parts.begin(), parts.end()); + // Traverse from the most specific match to the root and check for prefix matches. + auto [node, exact] = domain_tree_->root_.find(parts); + while (node) { + OptRef> on_match; + if (exact) { + on_match = node->exact_on_match_; + exact = false; + } else { + on_match = node->prefix_on_match_; } - if (matched.match_state_ == MatchState::MatchComplete && matched.result_) { - return {MatchState::MatchComplete, OnMatch{matched.result_, nullptr}}; - } - if (first) { - first = false; + if (on_match) { + if (on_match->action_cb_) { + return {MatchState::MatchComplete, OnMatch{on_match->action_cb_, nullptr}}; + } + auto matched = evaluateMatch(*on_match->matcher_, data); + if (matched.match_state_ == MatchState::UnableToMatch) { + return {MatchState::UnableToMatch, absl::nullopt}; + } + if (matched.match_state_ == MatchState::MatchComplete && matched.result_) { + return {MatchState::MatchComplete, OnMatch{matched.result_, nullptr}}; + } } + node = node->parent_prefix_.ptr(); } return {MatchState::MatchComplete, on_no_match_}; } @@ -95,46 +148,49 @@ class TrieMatcher : public MatchTree { private: const DataInputPtr data_input_; const absl::optional> on_no_match_; - std::shared_ptr>> trie_; + const std::shared_ptr> domain_tree_; }; - */ template class DomainMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory { public: - ::Envoy::Matcher::MatchTreeFactoryCb - createCustomMatcherFactoryCb(const Protobuf::Message& config, - Server::Configuration::ServerFactoryContext& factory_context, - DataInputFactoryCb data_input, - absl::optional> on_no_match, - OnMatchFactory& on_match_factory) override { + ::Envoy::Matcher::MatchTreeFactoryCb createCustomMatcherFactoryCb( + const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& factory_context, + ::Envoy::Matcher::DataInputFactoryCb data_input, + absl::optional<::Envoy::Matcher::OnMatchFactoryCb> on_no_match, + ::Envoy::Matcher::OnMatchFactory& on_match_factory) override { const auto& typed_config = - MessageUtil::downcastAndValidate( + MessageUtil::downcastAndValidate( config, factory_context.messageValidationVisitor()); - absl::flat_hash_map> server_names; + auto domain_tree = std::make_shared>(); + // Ensures pointer stability when populating the vector. + domain_tree->on_matches_.reserve(typed_config.domain_matchers().size()); + absl::flat_hash_map>> server_names; for (const auto& domain_matcher : typed_config.domain_matchers()) { - OnMatch on_match = *on_match_factory.createOnMatch(domain_matcher.on_match()).value()(); - if (server_name.find('*') != std::string::npos && !isWildcardServerName(server_name)) { - throw EnvoyException(fmt::format("invalid domain wildcard: {}", server_name)); - } + auto& on_match = domain_tree->on_matches_.emplace_back( + on_match_factory.createOnMatch(domain_matcher.on_match()).value()()); for (const auto& server_name : domain_matcher.domains()) { - // Reject internationalized domains to avoid ambiguity with case sensitivity. - if (!absl::ascii_isascii(server_name)) { - throw EnvoyException(fmt::format("non-ASCII domains are not supported: {}", server_name)); - } - auto [_, inserted] = server_names.try_emplace(ascii::AsciiStrToLower(server_name), on_match); + DomainMatcherUtility::validateServerName(server_name); + auto [_, inserted] = server_names.try_emplace(absl::AsciiStrToLower(server_name), on_match); if (!inserted) { - throw EnvoyException(fmt::format("duplicate domain: {}", server_name)); + DomainMatcherUtility::duplicateServerNameError(server_name); } } } - return [data_input, on_no_match]() { + for (const auto& [server_name, on_match] : server_names) { + std::vector parts = absl::StrSplit(server_name, "."); + std::reverse(parts.begin(), parts.end()); + domain_tree->root_.insert(parts, on_match); + } + domain_tree->root_.link(OptRef>()); + return [data_input, on_no_match, domain_tree]() { return std::make_unique>( - data_input(), on_no_match ? absl::make_optional(on_no_match.value()()) : absl::nullopt); + data_input(), on_no_match ? absl::make_optional(on_no_match.value()()) : absl::nullopt, + domain_tree); }; - }; + } ProtobufTypes::MessagePtr createEmptyConfigProto() override { - return std::make_unique(); + return std::make_unique(); } std::string name() const override { return "envoy.matching.custom_matchers.domain_matcher"; } }; diff --git a/source/extensions/common/matcher/trie_matcher.cc b/source/extensions/common/matcher/trie_matcher.cc index 99cfcabd30566..bab889286c2ff 100644 --- a/source/extensions/common/matcher/trie_matcher.cc +++ b/source/extensions/common/matcher/trie_matcher.cc @@ -1,5 +1,7 @@ #include "source/extensions/common/matcher/trie_matcher.h" +#include "envoy/http/filter.h" +#include "envoy/network/filter.h" #include "envoy/registry/registry.h" namespace Envoy { @@ -7,10 +9,16 @@ namespace Extensions { namespace Common { namespace Matcher { +class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; +class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; +class HttpTrieMatcherFactory : public TrieMatcherFactoryBase {}; + REGISTER_FACTORY(NetworkTrieMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); REGISTER_FACTORY(UdpNetworkTrieMatcherFactory, ::Envoy::Matcher::CustomMatcherFactory); +REGISTER_FACTORY(HttpTrieMatcherFactory, + ::Envoy::Matcher::CustomMatcherFactory); } // namespace Matcher } // namespace Common diff --git a/source/extensions/common/matcher/trie_matcher.h b/source/extensions/common/matcher/trie_matcher.h index 028795142b8c4..ac113de893124 100644 --- a/source/extensions/common/matcher/trie_matcher.h +++ b/source/extensions/common/matcher/trie_matcher.h @@ -1,7 +1,6 @@ #pragma once #include "envoy/matcher/matcher.h" -#include "envoy/network/filter.h" #include "envoy/server/factory_context.h" #include "source/common/matcher/matcher.h" @@ -153,9 +152,6 @@ class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory {}; -class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase {}; - } // namespace Matcher } // namespace Common } // namespace Extensions diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 44b80a834300f..f3cc53259a6c0 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -335,6 +335,7 @@ EXTENSIONS = { # "envoy.matching.custom_matchers.trie_matcher": "//source/extensions/common/matcher:trie_matcher_lib", + "envoy.matching.custom_matchers.domain_matcher": "//source/extensions/common/matcher:domain_matcher_lib", } # These can be changed to ["//visibility:public"], for downstream builds which diff --git a/source/extensions/extensions_metadata.yaml b/source/extensions/extensions_metadata.yaml index 21722c02f5198..c67210e822e81 100644 --- a/source/extensions/extensions_metadata.yaml +++ b/source/extensions/extensions_metadata.yaml @@ -818,5 +818,12 @@ envoy.matching.inputs.application_protocol: envoy.matching.custom_matchers.trie_matcher: categories: - envoy.matching.network.custom_matchers + - envoy.matching.http.custom_matchers + security_posture: unknown + status: alpha +envoy.matching.custom_matchers.domain_matcher: + categories: + - envoy.matching.network.custom_matchers + - envoy.matching.http.custom_matchers security_posture: unknown status: alpha diff --git a/test/extensions/common/matcher/BUILD b/test/extensions/common/matcher/BUILD index a6a0bf0d0a5ad..f45da22db52bd 100644 --- a/test/extensions/common/matcher/BUILD +++ b/test/extensions/common/matcher/BUILD @@ -1,6 +1,7 @@ load( "//bazel:envoy_build_system.bzl", "envoy_cc_test", + "envoy_cc_test_library", "envoy_package", ) @@ -22,18 +23,38 @@ envoy_cc_test( name = "trie_matcher_test", srcs = ["trie_matcher_test.cc"], deps = [ - "//source/common/matcher:matcher_lib", + ":custom_matcher_test", "//source/common/network:address_lib", "//source/common/network/matching:data_impl_lib", "//source/common/network/matching:inputs_lib", "//source/extensions/common/matcher:trie_matcher_lib", - "//test/common/matcher:test_utility_lib", "//test/mocks/http:http_mocks", - "//test/mocks/matcher:matcher_mocks", "//test/mocks/network:network_mocks", - "//test/mocks/server:factory_context_mocks", "//test/mocks/stream_info:stream_info_mocks", + ], +) + +envoy_cc_test_library( + name = "custom_matcher_test", + hdrs = ["custom_matcher_test.h"], + deps = [ + "//envoy/matcher:matcher_interface", + "//envoy/registry", + "//source/common/matcher:matcher_lib", + "//source/common/protobuf:utility_lib", + "//test/common/matcher:test_utility_lib", + "//test/mocks/matcher:matcher_mocks", + "//test/mocks/server:factory_context_mocks", "//test/test_common:registry_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) + +envoy_cc_test( + name = "domain_matcher_test", + srcs = ["domain_matcher_test.cc"], + deps = [ + ":custom_matcher_test", + "//source/extensions/common/matcher:domain_matcher_lib", + ], +) diff --git a/test/extensions/common/matcher/custom_matcher_test.h b/test/extensions/common/matcher/custom_matcher_test.h new file mode 100644 index 0000000000000..c3b3bfd809236 --- /dev/null +++ b/test/extensions/common/matcher/custom_matcher_test.h @@ -0,0 +1,80 @@ +#include "envoy/config/core/v3/extension.pb.h" +#include "envoy/matcher/matcher.h" +#include "envoy/registry/registry.h" + +#include "source/common/matcher/matcher.h" +#include "source/common/protobuf/utility.h" + +#include "test/common/matcher/test_utility.h" +#include "test/mocks/matcher/mocks.h" +#include "test/mocks/server/factory_context.h" +#include "test/test_common/registry.h" + +#include "gtest/gtest.h" +#include "xds/type/matcher/v3/matcher.pb.h" +#include "xds/type/matcher/v3/matcher.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { + +using ::Envoy::Matcher::ActionFactory; +using ::Envoy::Matcher::CustomMatcherFactory; +using ::Envoy::Matcher::DataInputGetResult; +using ::Envoy::Matcher::MatchTreeFactory; +using ::Envoy::Matcher::MockMatchTreeValidationVisitor; +using ::Envoy::Matcher::StringAction; +using ::Envoy::Matcher::StringActionFactory; +using ::Envoy::Matcher::TestData; + +template class CustomMatcherTest : public ::testing::Test { +public: + CustomMatcherTest() + : inject_action_(action_factory_), inject_matcher_(matcher_factory_), + factory_(context_, factory_context_, validation_visitor_) { + EXPECT_CALL(validation_visitor_, performDataInputValidation(_, _)).Times(testing::AnyNumber()); + } + + void loadConfig(const std::string& config) { + MessageUtil::loadFromYaml(config, matcher_, ProtobufMessage::getStrictValidationVisitor()); + TestUtility::validate(matcher_); + } + void validateMatch(const std::string& output) { + auto match_tree = factory_.create(matcher_); + const auto result = match_tree()->match(TestData()); + EXPECT_EQ(result.match_state_, MatchState::MatchComplete); + EXPECT_TRUE(result.on_match_.has_value()); + EXPECT_NE(result.on_match_->action_cb_, nullptr); + auto action = result.on_match_->action_cb_(); + const auto value = action->getTyped(); + EXPECT_EQ(value.string_, output); + } + void validateNoMatch() { + auto match_tree = factory_.create(matcher_); + const auto result = match_tree()->match(TestData()); + EXPECT_EQ(result.match_state_, MatchState::MatchComplete); + EXPECT_FALSE(result.on_match_.has_value()); + } + void validateUnableToMatch() { + auto match_tree = factory_.create(matcher_); + const auto result = match_tree()->match(TestData()); + EXPECT_EQ(result.match_state_, MatchState::UnableToMatch); + } + + StringActionFactory action_factory_; + Registry::InjectFactory> inject_action_; + CustomMatcherFactoryBase matcher_factory_; + Registry::InjectFactory> inject_matcher_; + MockMatchTreeValidationVisitor validation_visitor_; + + absl::string_view context_ = ""; + NiceMock factory_context_; + MatchTreeFactory factory_; + xds::type::matcher::v3::Matcher matcher_; +}; + +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/matcher/domain_matcher_test.cc b/test/extensions/common/matcher/domain_matcher_test.cc new file mode 100644 index 0000000000000..8ddc8b0986eae --- /dev/null +++ b/test/extensions/common/matcher/domain_matcher_test.cc @@ -0,0 +1,351 @@ +#include + +#include "source/extensions/common/matcher/domain_matcher.h" + +#include "test/extensions/common/matcher/custom_matcher_test.h" + +namespace Envoy { +namespace Extensions { +namespace Common { +namespace Matcher { +namespace { + +using ::Envoy::Matcher::TestDataInputFactory; + +class DomainMatcherTest + : public CustomMatcherTest> {}; + +TEST_F(DomainMatcherTest, TestMatcher) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + - domains: + - example.com + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: bar + )EOF"; + loadConfig(yaml); + + { + auto input = TestDataInputFactory("input", "example.com"); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory("input", "envoy.com"); + validateMatch("foo"); + } + { + auto input = TestDataInputFactory("input", "envoy.org"); + validateNoMatch(); + } + { + auto input = TestDataInputFactory("input", "xxx"); + validateNoMatch(); + } +} + +TEST_F(DomainMatcherTest, TestMatcherInvalidWildcard) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "com.*" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, "invalid domain wildcard: com.*"); +} + +TEST_F(DomainMatcherTest, TestMatcherUnicodeDomain) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "®.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, + "non-ASCII domains are not supported: ®.com"); +} + +TEST_F(DomainMatcherTest, TestMatcherDuplicateDomain) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + - domains: + - "EXAMPLE.COM" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: bar + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, "duplicate domain: EXAMPLE.COM"); +} + +TEST_F(DomainMatcherTest, TestMatcherOnNoMatch) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo +on_no_match: + action: + name: bar + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: bar + )EOF"; + loadConfig(yaml); + + { + auto input = TestDataInputFactory("input", "example.com"); + validateMatch("foo"); + } + { + auto input = TestDataInputFactory("input", "envoy.com"); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory("input", "xxx"); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory( + "input", {DataInputGetResult::DataAvailability::AllDataAvailable, absl::nullopt}); + validateMatch("bar"); + } + { + auto input = TestDataInputFactory( + "input", {DataInputGetResult::DataAvailability::MoreDataMightBeAvailable, "example.com"}); + validateUnableToMatch(); + } +} + +TEST_F(DomainMatcherTest, TestMatcherComplex) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m1 + - domains: + - "*.com" + - "*.org" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m2 + - domains: + - "com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m3 + - domains: + - "example.com" + - "*.example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m4 + - domains: + - "sub.example.com" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m5 + - domains: + - "*.envoy.org" + on_match: + matcher: + matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: ip_matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*.sub.envoy.org" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m6 + - domains: + - "*" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: m7 + )EOF"; + loadConfig(yaml); + + // The trie above represents the following decision tree: + // + // *(m1 prefix) + // com(m2 prefix, m3 exact) org(m2 prefix) + // example.com (m4) + // sub.example.com (m5 exact) envoy.org (m7 prefix) + // sub.envoy.org (m6 prefix) + { + auto input = TestDataInputFactory("input", "envoy"); + validateMatch("m1"); + } + { + auto input = TestDataInputFactory("input", "com"); + validateMatch("m3"); + } + { + auto input = TestDataInputFactory("input", "org"); + validateMatch("m1"); + } + { + auto input = TestDataInputFactory("input", "envoy.com"); + validateMatch("m2"); + } + { + auto input = TestDataInputFactory("input", "example.com"); + validateMatch("m4"); + } + { + auto input = TestDataInputFactory("input", "host.example.com"); + validateMatch("m4"); + } + { + auto input = TestDataInputFactory("input", "sub.example.com"); + validateMatch("m5"); + } + { + auto input = TestDataInputFactory("input", "host.sub.example.com"); + validateMatch("m4"); + } + { + auto input = TestDataInputFactory("input", "envoy.org"); + validateMatch("m2"); + } + { + auto input = TestDataInputFactory("input", "sub.envoy.org"); + validateMatch("m7"); + } + { + auto input = TestDataInputFactory("input", "host.sub.envoy.org"); + validateMatch("m6"); + } +} + +} // namespace +} // namespace Matcher +} // namespace Common +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/common/matcher/trie_matcher_test.cc b/test/extensions/common/matcher/trie_matcher_test.cc index a3ee5b0584841..e81187577a884 100644 --- a/test/extensions/common/matcher/trie_matcher_test.cc +++ b/test/extensions/common/matcher/trie_matcher_test.cc @@ -1,25 +1,11 @@ #include -#include "envoy/config/core/v3/extension.pb.h" -#include "envoy/matcher/matcher.h" -#include "envoy/registry/registry.h" - -#include "source/common/matcher/matcher.h" #include "source/common/network/address_impl.h" #include "source/common/network/matching/data_impl.h" -#include "source/common/protobuf/utility.h" #include "source/extensions/common/matcher/trie_matcher.h" -#include "test/common/matcher/test_utility.h" -#include "test/mocks/matcher/mocks.h" +#include "test/extensions/common/matcher/custom_matcher_test.h" #include "test/mocks/network/mocks.h" -#include "test/mocks/server/factory_context.h" -#include "test/test_common/registry.h" -#include "test/test_common/utility.h" - -#include "gtest/gtest.h" -#include "xds/type/matcher/v3/matcher.pb.h" -#include "xds/type/matcher/v3/matcher.pb.validate.h" namespace Envoy { namespace Extensions { @@ -28,7 +14,6 @@ namespace Matcher { namespace { using ::Envoy::Matcher::ActionFactory; -using ::Envoy::Matcher::CustomMatcherFactory; using ::Envoy::Matcher::DataInputGetResult; using ::Envoy::Matcher::MatchTreeFactory; using ::Envoy::Matcher::MockMatchTreeValidationVisitor; @@ -37,51 +22,7 @@ using ::Envoy::Matcher::StringActionFactory; using ::Envoy::Matcher::TestData; using ::Envoy::Matcher::TestDataInputFactory; -class TrieMatcherTest : public ::testing::Test { -public: - TrieMatcherTest() - : inject_action_(action_factory_), inject_matcher_(trie_matcher_factory_), - factory_(context_, factory_context_, validation_visitor_) { - EXPECT_CALL(validation_visitor_, performDataInputValidation(_, _)).Times(testing::AnyNumber()); - } - - void loadConfig(const std::string& config) { - MessageUtil::loadFromYaml(config, matcher_, ProtobufMessage::getStrictValidationVisitor()); - TestUtility::validate(matcher_); - } - void validateMatch(const std::string& output) { - auto match_tree = factory_.create(matcher_); - const auto result = match_tree()->match(TestData()); - EXPECT_EQ(result.match_state_, MatchState::MatchComplete); - EXPECT_TRUE(result.on_match_.has_value()); - EXPECT_NE(result.on_match_->action_cb_, nullptr); - auto action = result.on_match_->action_cb_(); - const auto value = action->getTyped(); - EXPECT_EQ(value.string_, output); - } - void validateNoMatch() { - auto match_tree = factory_.create(matcher_); - const auto result = match_tree()->match(TestData()); - EXPECT_EQ(result.match_state_, MatchState::MatchComplete); - EXPECT_FALSE(result.on_match_.has_value()); - } - void validateUnableToMatch() { - auto match_tree = factory_.create(matcher_); - const auto result = match_tree()->match(TestData()); - EXPECT_EQ(result.match_state_, MatchState::UnableToMatch); - } - - StringActionFactory action_factory_; - Registry::InjectFactory> inject_action_; - TrieMatcherFactoryBase trie_matcher_factory_; - Registry::InjectFactory> inject_matcher_; - MockMatchTreeValidationVisitor validation_visitor_; - - absl::string_view context_ = ""; - NiceMock factory_context_; - MatchTreeFactory factory_; - xds::type::matcher::v3::Matcher matcher_; -}; +class TrieMatcherTest : public CustomMatcherTest> {}; TEST_F(TrieMatcherTest, TestMatcher) { const std::string yaml = R"EOF( From 87b9c29825affd89731e7535fe8274302db480d2 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Mon, 4 Apr 2022 09:33:17 -0700 Subject: [PATCH 3/9] doc fix-up Signed-off-by: Kuat Yessenov --- docs/root/api-v3/common_messages/common_messages.rst | 1 + .../intro/arch_overview/advanced/matching/matching_api.rst | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/root/api-v3/common_messages/common_messages.rst b/docs/root/api-v3/common_messages/common_messages.rst index e4e126e6912cd..1196cd4d105fe 100644 --- a/docs/root/api-v3/common_messages/common_messages.rst +++ b/docs/root/api-v3/common_messages/common_messages.rst @@ -35,6 +35,7 @@ Common messages ../extensions/matching/common_inputs/network/v3/network_inputs.proto ../../xds/type/v3/range.proto ../../xds/type/v3/typed_struct.proto + ../../xds/type/matcher/v3/domain.proto ../../xds/type/matcher/v3/ip.proto ../../xds/type/matcher/v3/matcher.proto ../../xds/type/matcher/v3/range.proto diff --git a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst index a178ba64925e2..1295b993c0441 100644 --- a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst +++ b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst @@ -66,11 +66,12 @@ Custom Matching Algorithms ************************** In addition to the built-in exact and prefix matchers, these custom matchers -are available in some contexts: +are available in any context: .. _extension_envoy.matching.custom_matchers.trie_matcher: -* :ref:`Trie-based IP matcher ` applies to network inputs. +* :ref:`Trie-based IP matcher `. +* :ref:`Server name matcher `. Filter Integration ################## From f2ed038efb3750e4a43a906be3054e70ae368618 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Mon, 4 Apr 2022 13:53:41 -0700 Subject: [PATCH 4/9] fix docs Signed-off-by: Kuat Yessenov --- .../intro/arch_overview/advanced/matching/matching_api.rst | 3 +++ tools/extensions/extensions_check.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst index 1295b993c0441..d06c6af2eeb01 100644 --- a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst +++ b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst @@ -71,6 +71,9 @@ are available in any context: .. _extension_envoy.matching.custom_matchers.trie_matcher: * :ref:`Trie-based IP matcher `. + +.. _extension_envoy.matching.custom_matchers.domain_matcher: + * :ref:`Server name matcher `. Filter Integration diff --git a/tools/extensions/extensions_check.py b/tools/extensions/extensions_check.py index de667a4eb6b7a..b8e3c1eb0a0c0 100644 --- a/tools/extensions/extensions_check.py +++ b/tools/extensions/extensions_check.py @@ -62,7 +62,7 @@ "envoy.wasm.runtime", "envoy.common.key_value", "envoy.network.dns_resolver", "envoy.rbac.matchers", "envoy.access_loggers.extension_filters", "envoy.http.stateful_session", "envoy.matching.http.input", "envoy.matching.network.input", - "envoy.matching.network.custom_matchers") + "envoy.matching.network.custom_matchers", "envoy.matching.http.custom_matchers") EXTENSION_STATUS_VALUES = ( # This extension is stable and is expected to be production usable. From d1db52cbf13d8bd4f476ad0562e8267ea2bad097 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Mon, 4 Apr 2022 14:05:39 -0700 Subject: [PATCH 5/9] typo Signed-off-by: Kuat Yessenov --- source/extensions/common/matcher/domain_matcher.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h index 5726c3cd0c848..2f2e35b5443b3 100644 --- a/source/extensions/common/matcher/domain_matcher.h +++ b/source/extensions/common/matcher/domain_matcher.h @@ -99,7 +99,7 @@ class DomainMatcherUtility { }; /** - * Implementation of a sublinear domain matcher using a trie. + * Implementation of a `sublinear` domain matcher using a trie. **/ template class DomainMatcher : public MatchTree { public: From 4314c08f1826e280721d9d06510e5a4fd23e1274 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Mon, 4 Apr 2022 17:01:44 -0700 Subject: [PATCH 6/9] ci Signed-off-by: Kuat Yessenov --- source/extensions/common/matcher/domain_matcher.h | 4 ---- test/extensions/common/matcher/custom_matcher_test.h | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h index 2f2e35b5443b3..6963da81af9c2 100644 --- a/source/extensions/common/matcher/domain_matcher.h +++ b/source/extensions/common/matcher/domain_matcher.h @@ -27,8 +27,6 @@ using ::Envoy::Matcher::MatchState; using ::Envoy::Matcher::MatchTree; using ::Envoy::Matcher::OnMatch; -namespace { - /** * A "compressed" trie node with domain parts as edge values. */ @@ -87,8 +85,6 @@ template struct DomainTree { std::vector> on_matches_; }; -} // namespace - /** * General utilities for domain name matching. */ diff --git a/test/extensions/common/matcher/custom_matcher_test.h b/test/extensions/common/matcher/custom_matcher_test.h index c3b3bfd809236..d817825a6cfaa 100644 --- a/test/extensions/common/matcher/custom_matcher_test.h +++ b/test/extensions/common/matcher/custom_matcher_test.h @@ -22,6 +22,7 @@ namespace Matcher { using ::Envoy::Matcher::ActionFactory; using ::Envoy::Matcher::CustomMatcherFactory; using ::Envoy::Matcher::DataInputGetResult; +using ::Envoy::Matcher::MatchState; using ::Envoy::Matcher::MatchTreeFactory; using ::Envoy::Matcher::MockMatchTreeValidationVisitor; using ::Envoy::Matcher::StringAction; From 1a3981c13b50149e5fd03433a44259969d4fbc4b Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Wed, 6 Apr 2022 13:44:05 -0700 Subject: [PATCH 7/9] review Signed-off-by: Kuat Yessenov --- .../advanced/matching/matching_api.rst | 5 +++-- source/extensions/common/matcher/domain_matcher.h | 13 ++++++++++++- .../common/matcher/custom_matcher_test.h | 1 - .../common/matcher/domain_matcher_test.cc | 14 +++++++------- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst index d06c6af2eeb01..efe0d24f28d14 100644 --- a/docs/root/intro/arch_overview/advanced/matching/matching_api.rst +++ b/docs/root/intro/arch_overview/advanced/matching/matching_api.rst @@ -70,11 +70,12 @@ are available in any context: .. _extension_envoy.matching.custom_matchers.trie_matcher: -* :ref:`Trie-based IP matcher `. +* :ref:`Trie-based IP matcher ` for matching IP addresses. .. _extension_envoy.matching.custom_matchers.domain_matcher: -* :ref:`Server name matcher `. +* :ref:`Server name matcher ` for matching domain names (e.g. + `SNI `). Filter Integration ################## diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h index 6963da81af9c2..3a1f3267d2f76 100644 --- a/source/extensions/common/matcher/domain_matcher.h +++ b/source/extensions/common/matcher/domain_matcher.h @@ -28,7 +28,18 @@ using ::Envoy::Matcher::MatchTree; using ::Envoy::Matcher::OnMatch; /** - * A "compressed" trie node with domain parts as edge values. + * A "compressed" trie node with individual domain parts as edge values. Example: + * + * () + * `com` `org` + * (`*.com`) () + * `envoy` + * (`envoy.org`) + * + * represents the following patterns: + * - `*` that matches any domain; + * - `*.com` that matches any domain ending with `.com`; + * - `envoy.org` that matches domain `envoy.org` exactly. */ template struct DomainNode { absl::flat_hash_map> children_; diff --git a/test/extensions/common/matcher/custom_matcher_test.h b/test/extensions/common/matcher/custom_matcher_test.h index d817825a6cfaa..640f279edc19d 100644 --- a/test/extensions/common/matcher/custom_matcher_test.h +++ b/test/extensions/common/matcher/custom_matcher_test.h @@ -21,7 +21,6 @@ namespace Matcher { using ::Envoy::Matcher::ActionFactory; using ::Envoy::Matcher::CustomMatcherFactory; -using ::Envoy::Matcher::DataInputGetResult; using ::Envoy::Matcher::MatchState; using ::Envoy::Matcher::MatchTreeFactory; using ::Envoy::Matcher::MockMatchTreeValidationVisitor; diff --git a/test/extensions/common/matcher/domain_matcher_test.cc b/test/extensions/common/matcher/domain_matcher_test.cc index 8ddc8b0986eae..bdbcaf5b6e67e 100644 --- a/test/extensions/common/matcher/domain_matcher_test.cc +++ b/test/extensions/common/matcher/domain_matcher_test.cc @@ -23,7 +23,7 @@ TEST_F(DomainMatcherTest, TestMatcher) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: @@ -72,7 +72,7 @@ TEST_F(DomainMatcherTest, TestMatcherInvalidWildcard) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: @@ -98,7 +98,7 @@ TEST_F(DomainMatcherTest, TestMatcherUnicodeDomain) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: @@ -125,7 +125,7 @@ TEST_F(DomainMatcherTest, TestMatcherDuplicateDomain) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: @@ -159,7 +159,7 @@ TEST_F(DomainMatcherTest, TestMatcherOnNoMatch) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: @@ -212,7 +212,7 @@ TEST_F(DomainMatcherTest, TestMatcherComplex) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: @@ -268,7 +268,7 @@ TEST_F(DomainMatcherTest, TestMatcherComplex) { typed_config: "@type": type.googleapis.com/google.protobuf.StringValue custom_match: - name: ip_matcher + name: matcher typed_config: "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: From e86c0e2e2070d8f5334890bf271f87a61a2e9d2b Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Wed, 6 Apr 2022 15:35:45 -0700 Subject: [PATCH 8/9] workaround gcc bug Signed-off-by: Kuat Yessenov --- test/extensions/common/matcher/custom_matcher_test.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/extensions/common/matcher/custom_matcher_test.h b/test/extensions/common/matcher/custom_matcher_test.h index 640f279edc19d..0614121646047 100644 --- a/test/extensions/common/matcher/custom_matcher_test.h +++ b/test/extensions/common/matcher/custom_matcher_test.h @@ -47,7 +47,7 @@ template class CustomMatcherTest : public ::tes EXPECT_TRUE(result.on_match_.has_value()); EXPECT_NE(result.on_match_->action_cb_, nullptr); auto action = result.on_match_->action_cb_(); - const auto value = action->getTyped(); + const auto value = action->template getTyped(); EXPECT_EQ(value.string_, output); } void validateNoMatch() { From dc8ed26a85b46749207f102d6685c03e508a2c87 Mon Sep 17 00:00:00 2001 From: Kuat Yessenov Date: Wed, 6 Apr 2022 21:44:04 -0700 Subject: [PATCH 9/9] review Signed-off-by: Kuat Yessenov --- .../common/matcher/domain_matcher.cc | 16 +++++----- .../common/matcher/domain_matcher.h | 11 ++++--- .../common/matcher/domain_matcher_test.cc | 32 +++++++++++++++++-- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/source/extensions/common/matcher/domain_matcher.cc b/source/extensions/common/matcher/domain_matcher.cc index 4bcf60b582df7..7cbc8ec0c11f9 100644 --- a/source/extensions/common/matcher/domain_matcher.cc +++ b/source/extensions/common/matcher/domain_matcher.cc @@ -9,15 +9,15 @@ namespace Extensions { namespace Common { namespace Matcher { -namespace { -bool isWildcardServerName(const std::string& name) { - return absl::StartsWith(name, "*.") || name == "*"; -} -} // namespace - void DomainMatcherUtility::validateServerName(const std::string& server_name) { - if (server_name.find('*') != std::string::npos && !isWildcardServerName(server_name)) { - throw EnvoyException(fmt::format("invalid domain wildcard: {}", server_name)); + auto pos = server_name.rfind('*'); + if (pos != std::string::npos) { + if (pos != 0) { + throw EnvoyException(fmt::format("wildcard only allowed in the prefix: {}", server_name)); + } + if (server_name != "*" && !absl::StartsWith(server_name, "*.")) { + throw EnvoyException(fmt::format("wildcard must be the first domain part: {}", server_name)); + } } // Reject internationalized domains to avoid ambiguity with case sensitivity. for (char c : server_name) { diff --git a/source/extensions/common/matcher/domain_matcher.h b/source/extensions/common/matcher/domain_matcher.h index 3a1f3267d2f76..28b121649387c 100644 --- a/source/extensions/common/matcher/domain_matcher.h +++ b/source/extensions/common/matcher/domain_matcher.h @@ -30,16 +30,17 @@ using ::Envoy::Matcher::OnMatch; /** * A "compressed" trie node with individual domain parts as edge values. Example: * - * () + * (P) * `com` `org` - * (`*.com`) () + * (P) () * `envoy` - * (`envoy.org`) + * (E) * + * with `P` signifying the prefix match and `E` signifying the exact match, * represents the following patterns: * - `*` that matches any domain; - * - `*.com` that matches any domain ending with `.com`; - * - `envoy.org` that matches domain `envoy.org` exactly. + * - `*.com` that matches any domain ending with `.com` but not `com`; + * - `envoy.org` that matches the domain `envoy.org` exactly. */ template struct DomainNode { absl::flat_hash_map> children_; diff --git a/test/extensions/common/matcher/domain_matcher_test.cc b/test/extensions/common/matcher/domain_matcher_test.cc index bdbcaf5b6e67e..ba4df910d77c1 100644 --- a/test/extensions/common/matcher/domain_matcher_test.cc +++ b/test/extensions/common/matcher/domain_matcher_test.cc @@ -77,7 +77,7 @@ TEST_F(DomainMatcherTest, TestMatcherInvalidWildcard) { "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher domain_matchers: - domains: - - "com.*" + - "*.com.*" on_match: action: name: test_action @@ -87,7 +87,35 @@ TEST_F(DomainMatcherTest, TestMatcherInvalidWildcard) { )EOF"; loadConfig(yaml); auto input = TestDataInputFactory("input", "example.com"); - EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, "invalid domain wildcard: com.*"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, + "wildcard only allowed in the prefix: *.com.*"); +} + +TEST_F(DomainMatcherTest, TestMatcherPartialWildcard) { + const std::string yaml = R"EOF( +matcher_tree: + input: + name: input + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + custom_match: + name: matcher + typed_config: + "@type": type.googleapis.com/xds.type.matcher.v3.ServerNameMatcher + domain_matchers: + - domains: + - "*nvoy.org" + on_match: + action: + name: test_action + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + value: foo + )EOF"; + loadConfig(yaml); + auto input = TestDataInputFactory("input", "example.com"); + EXPECT_THROW_WITH_MESSAGE(validateMatch("foo"), EnvoyException, + "wildcard must be the first domain part: *nvoy.org"); } TEST_F(DomainMatcherTest, TestMatcherUnicodeDomain) {