diff --git a/cilium/network_policy.cc b/cilium/network_policy.cc index 2fa9a16e4..c83f2715c 100644 --- a/cilium/network_policy.cc +++ b/cilium/network_policy.cc @@ -386,7 +386,7 @@ class PortNetworkPolicyRule : public Logger::Loggable { } for (const auto& sni : rule.server_names()) { ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule(): Allowing SNI {} by rule {}", sni, name_); - allowed_snis_.emplace_back(sni); + allowed_snis_.emplace_back(parent.regexEngine(), sni); } if (rule.has_http_rules()) { for (const auto& http_rule : rule.http_rules().http_rules()) { diff --git a/cilium/network_policy.h b/cilium/network_policy.h index 8f0f34820..bf4c728bd 100644 --- a/cilium/network_policy.h +++ b/cilium/network_policy.h @@ -31,6 +31,7 @@ #include "source/common/common/assert.h" #include "source/common/common/logger.h" #include "source/common/common/macros.h" +#include "source/common/common/regex.h" #include "source/common/common/thread.h" #include "source/common/init/target_impl.h" #include "source/common/protobuf/message_validator_impl.h" @@ -42,6 +43,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/status/status.h" #include "absl/strings/ascii.h" +#include "absl/strings/str_replace.h" #include "absl/strings/string_view.h" #include "cilium/accesslog.h" #include "cilium/api/npds.pb.h" @@ -263,6 +265,8 @@ class NetworkPolicyMapImpl : public Envoy::Config::SubscriptionCallbacks, return *transport_factory_context_; } + Regex::Engine& regexEngine() const { return context_.regexEngine(); } + void tlsWrapperMissingPolicyInc() const; private: @@ -371,41 +375,122 @@ class NetworkPolicyMap : public Singleton::Instance, public Logger::Loggable; -struct SniPattern { - std::string pattern; +// SniPattern provides a matcher for allowed SNI patterns. +// SNI match pattern provided by the user only supports one regex character('*'), which +// can be used as a wildcard specifier. +// +// - "**." is a special prefix which matches all multilevel subdomains in the prefix. +// - "*" matches 0 or more DNS valid characters, and may occur anywhere in the pattern. +// +// Additionaly a full wildcard pattern can be specified using "*" that matches all +// names consisting of valid DNS characters. +// +// Examples: +// 1. `*.cilium.io` matches subdomains of cilium at that level +// www.cilium.io and blog.cilium.io match, cilium.io and google.com do not +// 2. `*cilium.io` matches cilium.io and all subdomains ends with "cilium.io" +// except those containing "." separator, subcilium.io and sub-cilium.io match, +// www.cilium.io and blog.cilium.io does not +// 3. `sub*.cilium.io` matches subdomains of cilium where the subdomain component +// begins with "sub". sub.cilium.io and subdomain.cilium.io match while www.cilium.io, +// blog.cilium.io, cilium.io and google.com do not +// 4. `**.cilium.io` matches all multilevel subdomains of cilium.io. +// "app.cilium.io" and "test.app.cilium.io" match but not "cilium.io" +class SniPattern : public Logger::Loggable { +public: + static const re2::RE2& getValidNonRegexCharsRE() { + // Regular expression to match a SNI pattern without any regex characters('*'). + // If the input sni pattern matches this, we can simply rely on explicit full string match. + CONSTRUCT_ON_FIRST_USE(re2::RE2, "^[-a-zA-Z0-9_.]*$"); + } + + static const re2::RE2& getFullWildcardMatchRE() { + // Explicit '*' is a special case which implies a full wildcard for matching. + CONSTRUCT_ON_FIRST_USE(re2::RE2, "^[*]{1,}$"); + } + + static const re2::RE2& getSubdomainWildcardSpecifierPrefixRE() { + // Regular expression to match subdomain wildcard prefix in sni patterns. + // This regex will match prefixes like '**[.]', '****[.]' and so on. + CONSTRUCT_ON_FIRST_USE(re2::RE2, "^[*]{2,}\\[\\.\\]"); + } + + static const re2::RE2& getWildcardSpecifierRE() { + // Regular expression to match wildcard in SNI pattern. + // Given this match is a superset of subdomain wildcard prefix specifier it should + // be used once the regex is resolved for the former. + CONSTRUCT_ON_FIRST_USE(re2::RE2, "[*]{1,}"); + } + + // Constructs SniPattern with the provided regex engine for input match pattern. + explicit SniPattern(const Regex::Engine& engine, const absl::string_view& sni) { + if (re2::RE2::FullMatch(sni, getValidNonRegexCharsRE())) { + match_name_ = absl::AsciiStrToLower(sni); + return; + } + + std::string regex_expr; + if (re2::RE2::FullMatch(sni, getFullWildcardMatchRE())) { + // For a full wildcard match pattern replace with static wildcard pattern for DNS characters. + regex_expr = "([-a-zA-Z0-9_]+([.][-a-zA-Z0-9_]+){0,})"; + } else { + // Convert '.' to regex literal '[.]' + regex_expr = absl::StrReplaceAll(absl::AsciiStrToLower(sni), {{".", "[.]"}}); + + // Replace subdomain wildcard specifier prefix with multilevel subdomain match. + // The replaced regex pattern matches one or more entire DNS labels, for example: + // * + // * .. + re2::RE2::GlobalReplace(®ex_expr, getSubdomainWildcardSpecifierPrefixRE(), + "([-a-zA-Z0-9_]+([.][-a-zA-Z0-9_]+){0,})[.]"); + + // Replace wildcard specifier '*' with regex for any number of valid DNS characters within + // subdomain boundry(doesn't include '.' literal). + re2::RE2::GlobalReplace(®ex_expr, getWildcardSpecifierRE(), "[-a-zA-Z0-9_]*"); + } + + // Anchor the final regular expression. + auto regex_matcher = engine.matcher(fmt::format("^{}$", regex_expr)); + if (!regex_matcher.ok()) { + ENVOY_LOG(error, + "Cilium SNI: Failed to create pattern for SNI {} " + "[Status: {}]", + sni, regex_matcher.status().ToString()); + return; + } - explicit SniPattern(const std::string& p) : pattern(absl::AsciiStrToLower(p)) {} + matcher_ = std::move(regex_matcher.value()); + } bool matches(const absl::string_view sni) const { - if (pattern.empty() || sni.empty()) { + if (!isExplicitFullMatch() && !matcher_) { return false; } auto const lower_sni = absl::AsciiStrToLower(sni); - // Perform lower case exact match if there is no wildcard prefix - if (!pattern.starts_with("*")) { - return pattern == lower_sni; + if (isExplicitFullMatch()) { + return match_name_ == lower_sni; } - // Pattern is "**." - if (pattern.starts_with("**.")) { - return lower_sni.ends_with(pattern.substr(2)); - } + return matcher_->match(lower_sni); + } - // Pattern is "*." - if (pattern.starts_with("*.")) { - auto const sub_pattern = pattern.substr(1); - if (!lower_sni.ends_with(sub_pattern)) { - return false; + void toString(std::string& res) const { + if (isExplicitFullMatch()) { + res.append(fmt::format("\"{}\"", match_name_)); + } else { + if (matcher_) { + res.append(fmt::format("\"{}\"", matcher_->pattern())); + } else { + res.append("\"\""); } - auto const prefix = lower_sni.substr(0, sni.size() - sub_pattern.size()); - // Make sure that only and exactly one label is before the wildcard - return !prefix.empty() && prefix.find_first_of('.') == std::string::npos; } - - return false; } - void toString(std::string& res) const { res.append(fmt::format("\"{}\"", pattern)); } +private: + bool isExplicitFullMatch() const { return !match_name_.empty(); } + + std::string match_name_; + std::unique_ptr matcher_; }; } // namespace Cilium diff --git a/tests/cilium_network_policy_test.cc b/tests/cilium_network_policy_test.cc index 76dd34313..7ddeecdec 100644 --- a/tests/cilium_network_policy_test.cc +++ b/tests/cilium_network_policy_test.cc @@ -1896,8 +1896,10 @@ TEST_F(CiliumNetworkPolicyTest, EmptyRulesAllow) { } TEST_F(CiliumNetworkPolicyTest, SNIPatternMatching) { + Regex::GoogleReEngine engine; + // Test empty pattern - SniPattern empty(""); + SniPattern empty(engine, ""); EXPECT_FALSE(empty.matches("example.com")); EXPECT_FALSE(empty.matches("EXAMPLE.COM")); EXPECT_FALSE(empty.matches("www.example.com")); @@ -1905,15 +1907,33 @@ TEST_F(CiliumNetworkPolicyTest, SNIPatternMatching) { EXPECT_FALSE(empty.matches("")); // Test exact matches - SniPattern exact("example.com"); + SniPattern exact(engine, "example.com"); EXPECT_TRUE(exact.matches("example.com")); EXPECT_TRUE(exact.matches("EXAMPLE.COM")); EXPECT_FALSE(exact.matches("www.example.com")); EXPECT_FALSE(exact.matches("notexample.com")); EXPECT_FALSE(exact.matches("")); + SniPattern exact_with_subdomain(engine, "foo.bar.example.com"); + EXPECT_TRUE(exact_with_subdomain.matches("foo.bar.example.com")); + EXPECT_TRUE(exact_with_subdomain.matches("foo.BAR.example.COM")); + EXPECT_FALSE(exact_with_subdomain.matches("bar.example.com")); + EXPECT_FALSE(exact_with_subdomain.matches("foo.bar.example.org")); + EXPECT_FALSE(exact_with_subdomain.matches("")); + + SniPattern full_wildcard(engine, "*"); + EXPECT_TRUE(full_wildcard.matches("localhost")); + EXPECT_TRUE(full_wildcard.matches("example.com")); + EXPECT_TRUE(full_wildcard.matches("foo.007.example.com")); + EXPECT_TRUE(full_wildcard.matches("foo.bar.example.com")); + EXPECT_TRUE(full_wildcard.matches("foo.BAR.example.COM")); + EXPECT_TRUE(full_wildcard.matches("foo-bar.example.com")); + EXPECT_FALSE(full_wildcard.matches("example.com.")); + EXPECT_FALSE(full_wildcard.matches("ex@mple.com")); + EXPECT_FALSE(full_wildcard.matches("")); + // Test wildcard matches - SniPattern wild("*.example.com"); + SniPattern wild(engine, "*.example.com"); EXPECT_TRUE(wild.matches("foo.example.com")); EXPECT_TRUE(wild.matches("bar.example.com")); EXPECT_TRUE(wild.matches("FOO.EXAMPLE.COM")); @@ -1922,31 +1942,65 @@ TEST_F(CiliumNetworkPolicyTest, SNIPatternMatching) { EXPECT_FALSE(wild.matches("fooexample.com")); EXPECT_FALSE(wild.matches("")); + // Wildcard with hyphen + SniPattern wildcard_hyphen(engine, "sub.example-*.com"); + EXPECT_TRUE(wildcard_hyphen.matches("sub.example-foo.com")); + EXPECT_TRUE(wildcard_hyphen.matches("sub.example-007.com")); + EXPECT_TRUE(wildcard_hyphen.matches("sub.example-foo-bar.com")); + EXPECT_FALSE(wildcard_hyphen.matches("sub.example.com")); + EXPECT_FALSE(wildcard_hyphen.matches("sub.example-foo.bar.com")); + // Test subdomain wildcard matches - SniPattern subwild("*.sub.example.com"); + SniPattern subwild(engine, "*.sub.example.com"); EXPECT_TRUE(subwild.matches("foo.sub.example.com")); EXPECT_TRUE(subwild.matches("bar.sub.example.com")); + EXPECT_TRUE(subwild.matches("007.sub.example.com")); EXPECT_FALSE(subwild.matches("sub.example.com")); EXPECT_FALSE(subwild.matches("foo.example.com")); EXPECT_FALSE(subwild.matches("foo.bar.sub.example.com")); // Test subdomain double wildcard matches - SniPattern double_wildcard("**.sub.example.com"); + SniPattern double_wildcard(engine, "**.sub.example.com"); EXPECT_TRUE(double_wildcard.matches("foo.sub.example.com")); EXPECT_TRUE(double_wildcard.matches("bar.sub.example.com")); EXPECT_FALSE(double_wildcard.matches("sub.example.com")); EXPECT_FALSE(double_wildcard.matches("foo.example.com")); EXPECT_TRUE(double_wildcard.matches("foo.bar.sub.example.com")); - // Test with unsupported wildcard label - SniPattern wildcard_label("*example.com"); - EXPECT_FALSE(wildcard_label.matches("foo.example.com")); - EXPECT_FALSE(wildcard_label.matches("bar.example.com")); - EXPECT_FALSE(wildcard_label.matches("FOO.EXAMPLE.COM")); - EXPECT_FALSE(wildcard_label.matches("example.com")); - EXPECT_FALSE(wildcard_label.matches("foo.bar.example.com")); + // Test wildcard label in between the subdomains + SniPattern wildcard_label(engine, "sub.*.com"); + EXPECT_TRUE(wildcard_label.matches("sub.foo.com")); + EXPECT_TRUE(wildcard_label.matches("sub.bar.com")); + EXPECT_TRUE(wildcard_label.matches("sub.foobar.COM")); + EXPECT_FALSE(wildcard_label.matches("test.sub.example.com")); + EXPECT_FALSE(wildcard_label.matches("sub.com")); EXPECT_FALSE(wildcard_label.matches("fooexample.com")); EXPECT_FALSE(wildcard_label.matches("")); + + // Test wildcard label in between name + SniPattern mixed_wildcard_label(engine, "sub.ex*e.com"); + EXPECT_TRUE(mixed_wildcard_label.matches("sub.exe.com")); + EXPECT_TRUE(mixed_wildcard_label.matches("sub.example.com")); + EXPECT_FALSE(mixed_wildcard_label.matches("sub.foobar.COM")); + EXPECT_FALSE(mixed_wildcard_label.matches("sub.someexe.com")); + EXPECT_FALSE(mixed_wildcard_label.matches("")); + + // Multiple wildcard labels + SniPattern multi_wildcard_labels(engine, "sub.*.*.example.com"); + EXPECT_TRUE(multi_wildcard_labels.matches("sub.foo.bar.example.com")); + EXPECT_FALSE(multi_wildcard_labels.matches("sub.foo.example.com")); + EXPECT_FALSE(multi_wildcard_labels.matches("sub.example.com")); + EXPECT_FALSE(multi_wildcard_labels.matches("")); + + // Multiple wildcard labels with multilevel subdomain prefix wildcard. + SniPattern all_wildcard_labels(engine, "**.sub.*.ex*e.com"); + EXPECT_TRUE(all_wildcard_labels.matches("foo.sub.bar.example.com")); + EXPECT_TRUE(all_wildcard_labels.matches("test.foo.sub.bar.example.com")); + EXPECT_TRUE(all_wildcard_labels.matches("test.foo.sub.bar.exe.com")); + EXPECT_FALSE(all_wildcard_labels.matches("test.sub.foobar.com")); + EXPECT_FALSE(all_wildcard_labels.matches("test.sub.example.com")); + EXPECT_FALSE(all_wildcard_labels.matches("sub.test.example.com")); + EXPECT_FALSE(all_wildcard_labels.matches("")); } TEST_F(CiliumNetworkPolicyTest, OrderedRules) {