Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions api/bazel/repository_locations.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
1 change: 1 addition & 0 deletions docs/root/api-v3/common_messages/common_messages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,16 @@ 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 <envoy_v3_api_msg_.xds.type.matcher.v3.IPMatcher>` applies to network inputs.
* :ref:`Trie-based IP matcher <envoy_v3_api_msg_.xds.type.matcher.v3.IPMatcher>` for matching IP addresses.

.. _extension_envoy.matching.custom_matchers.domain_matcher:

* :ref:`Server name matcher <envoy_v3_api_msg_.xds.type.matcher.v3.ServerNameMatcher>` for matching domain names (e.g.
`SNI <https://en.wikipedia.org/wiki/Server_Name_Indication>`).

Filter Integration
##################
Expand Down
15 changes: 15 additions & 0 deletions source/extensions/common/matcher/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -36,3 +37,17 @@ 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/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",
],
)
48 changes: 48 additions & 0 deletions source/extensions/common/matcher/domain_matcher.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#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 {
namespace Extensions {
namespace Common {
namespace Matcher {

void DomainMatcherUtility::validateServerName(const std::string& 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) {
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<Network::MatchingData> {};
class UdpNetworkDomainMatcherFactory : public DomainMatcherFactoryBase<Network::UdpMatchingData> {};
class HttpDomainMatcherFactory : public DomainMatcherFactoryBase<Http::HttpMatchingData> {};

REGISTER_FACTORY(NetworkDomainMatcherFactory,
::Envoy::Matcher::CustomMatcherFactory<Network::MatchingData>);
REGISTER_FACTORY(UdpNetworkDomainMatcherFactory,
::Envoy::Matcher::CustomMatcherFactory<Network::UdpMatchingData>);
REGISTER_FACTORY(HttpDomainMatcherFactory,
::Envoy::Matcher::CustomMatcherFactory<Http::HttpMatchingData>);

} // namespace Matcher
} // namespace Common
} // namespace Extensions
} // namespace Envoy
209 changes: 209 additions & 0 deletions source/extensions/common/matcher/domain_matcher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#pragma once

#include "envoy/common/optref.h"
#include "envoy/matcher/matcher.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"

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;

/**
* A "compressed" trie node with individual domain parts as edge values. Example:
*
* (P)
* `com` `org`
* (P) ()
* `envoy`
* (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` but not `com`;
* - `envoy.org` that matches the domain `envoy.org` exactly.
*/
template <class DataType> struct DomainNode {
absl::flat_hash_map<std::string, DomainNode<DataType>> children_;
// Matches the exact path from the root.
OptRef<OnMatch<DataType>> exact_on_match_;
// Matches the prefix for the path from the root.
OptRef<OnMatch<DataType>> prefix_on_match_;
Comment thread
kyessenov marked this conversation as resolved.
// Closest parent node with a prefix match.
OptRef<DomainNode<DataType>> 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<const std::string> parts, OnMatch<DataType>& 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<DataType>& child = children_[parts[0]];
return child.insert(parts.subspan(1), on_match);
}
}

// Link parent prefix from child nodes.
void link(OptRef<DomainNode<DataType>> 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<DomainNode const*, bool> find(absl::Span<const std::string> 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 <class DataType> struct DomainTree {
DomainNode<DataType> root_;
std::vector<OnMatch<DataType>> on_matches_;
};

/**
* 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 using a trie.
**/
template <class DataType> class DomainMatcher : public MatchTree<DataType> {
public:
DomainMatcher(DataInputPtr<DataType>&& data_input, absl::optional<OnMatch<DataType>> on_no_match,
const std::shared_ptr<DomainTree<DataType>>& domain_tree)
: data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)),
domain_tree_(domain_tree) {}

typename MatchTree<DataType>::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_};
}
std::vector<std::string> 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<OnMatch<DataType>> on_match;
if (exact) {
on_match = node->exact_on_match_;
exact = false;
} else {
on_match = node->prefix_on_match_;
}
if (on_match) {
if (on_match->action_cb_) {
return {MatchState::MatchComplete, OnMatch<DataType>{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<DataType>{matched.result_, nullptr}};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to escape the if (on_match) { block w/o return ?
In which case?

If the answer is yes, should on_match degrade from exact_on_match to prefix_on_match ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it can have nested matchers so the match can complete without a result.
It does degrade, because it traverses the parent_prefix_ edges. E.g. it'll try wider and wider patters by going from the node to the root.

}
node = node->parent_prefix_.ptr();
}
return {MatchState::MatchComplete, on_no_match_};
}

private:
const DataInputPtr<DataType> data_input_;
const absl::optional<OnMatch<DataType>> on_no_match_;
const std::shared_ptr<DomainTree<DataType>> domain_tree_;
};

template <class DataType>
class DomainMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory<DataType> {
public:
::Envoy::Matcher::MatchTreeFactoryCb<DataType> createCustomMatcherFactoryCb(
const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& factory_context,
::Envoy::Matcher::DataInputFactoryCb<DataType> data_input,
absl::optional<::Envoy::Matcher::OnMatchFactoryCb<DataType>> on_no_match,
::Envoy::Matcher::OnMatchFactory<DataType>& on_match_factory) override {
const auto& typed_config =
MessageUtil::downcastAndValidate<const xds::type::matcher::v3::ServerNameMatcher&>(
config, factory_context.messageValidationVisitor());
auto domain_tree = std::make_shared<DomainTree<DataType>>();
// Ensures pointer stability when populating the vector.
domain_tree->on_matches_.reserve(typed_config.domain_matchers().size());
absl::flat_hash_map<std::string, std::reference_wrapper<OnMatch<DataType>>> server_names;
for (const auto& domain_matcher : typed_config.domain_matchers()) {
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()) {
DomainMatcherUtility::validateServerName(server_name);
auto [_, inserted] = server_names.try_emplace(absl::AsciiStrToLower(server_name), on_match);
if (!inserted) {
DomainMatcherUtility::duplicateServerNameError(server_name);
}
}
}
for (const auto& [server_name, on_match] : server_names) {
std::vector<std::string> parts = absl::StrSplit(server_name, ".");
std::reverse(parts.begin(), parts.end());
domain_tree->root_.insert(parts, on_match);
}
domain_tree->root_.link(OptRef<DomainNode<DataType>>());
return [data_input, on_no_match, domain_tree]() {
return std::make_unique<DomainMatcher<DataType>>(
data_input(), on_no_match ? absl::make_optional(on_no_match.value()()) : absl::nullopt,
domain_tree);
};
}
ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return std::make_unique<xds::type::matcher::v3::ServerNameMatcher>();
}
std::string name() const override { return "envoy.matching.custom_matchers.domain_matcher"; }
};

} // namespace Matcher
} // namespace Common
} // namespace Extensions
} // namespace Envoy
8 changes: 8 additions & 0 deletions source/extensions/common/matcher/trie_matcher.cc
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
#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 {
namespace Extensions {
namespace Common {
namespace Matcher {

class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase<Network::MatchingData> {};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should these class definitions be moved to .cc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency. It doesn't seem to be useful to export these in a header.

class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase<Network::UdpMatchingData> {};
class HttpTrieMatcherFactory : public TrieMatcherFactoryBase<Http::HttpMatchingData> {};

REGISTER_FACTORY(NetworkTrieMatcherFactory,
::Envoy::Matcher::CustomMatcherFactory<Network::MatchingData>);
REGISTER_FACTORY(UdpNetworkTrieMatcherFactory,
::Envoy::Matcher::CustomMatcherFactory<Network::UdpMatchingData>);
REGISTER_FACTORY(HttpTrieMatcherFactory,
::Envoy::Matcher::CustomMatcherFactory<Http::HttpMatchingData>);

} // namespace Matcher
} // namespace Common
Expand Down
4 changes: 0 additions & 4 deletions source/extensions/common/matcher/trie_matcher.h
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -153,9 +152,6 @@ class TrieMatcherFactoryBase : public ::Envoy::Matcher::CustomMatcherFactory<Dat
std::string name() const override { return "envoy.matching.custom_matchers.trie_matcher"; }
};

class NetworkTrieMatcherFactory : public TrieMatcherFactoryBase<Network::MatchingData> {};
class UdpNetworkTrieMatcherFactory : public TrieMatcherFactoryBase<Network::UdpMatchingData> {};

} // namespace Matcher
} // namespace Common
} // namespace Extensions
Expand Down
1 change: 1 addition & 0 deletions source/extensions/extensions_build_config.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,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
Expand Down
7 changes: 7 additions & 0 deletions source/extensions/extensions_metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -823,5 +823,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
Loading