Skip to content
Open
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
2 changes: 1 addition & 1 deletion cilium/network_policy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ class PortNetworkPolicyRule : public Logger::Loggable<Logger::Id::config> {
}
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()) {
Expand Down
129 changes: 107 additions & 22 deletions cilium/network_policy.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -371,41 +375,122 @@ class NetworkPolicyMap : public Singleton::Instance, public Logger::Loggable<Log
};
using NetworkPolicyMapSharedPtr = std::shared_ptr<const NetworkPolicyMap>;

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<Logger::Id::config> {
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:
// * <dns-label>
// * <dns-label-1>.<dns-label-2>.<dns-label-3>
re2::RE2::GlobalReplace(&regex_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(&regex_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 "**.<domain>"
if (pattern.starts_with("**.")) {
return lower_sni.ends_with(pattern.substr(2));
}
return matcher_->match(lower_sni);
}

// Pattern is "*.<domain>"
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<const Envoy::Regex::CompiledMatcher> matcher_;
};

} // namespace Cilium
Expand Down
78 changes: 66 additions & 12 deletions tests/cilium_network_policy_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1896,24 +1896,44 @@ 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"));
EXPECT_FALSE(empty.matches("notexample.com"));
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"));
Expand All @@ -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) {
Expand Down
Loading