diff --git a/Makefile.api b/Makefile.api index de6cd2fc2..8939558c6 100644 --- a/Makefile.api +++ b/Makefile.api @@ -87,8 +87,8 @@ all: cilium-go-targets .PHONY: cilium-go-targets cilium-go-targets: $(CILIUM_PROTO_SOURCES) $(ENVOY_API_PROTO_PATH) Makefile.api + go install tool $(QUIET)set -e; \ - echo "NOTE: protoc tools referred to in go.mod tools section must be installed with "go install" for protoc to find them."; \ for path in $(CILIUM_PROTO_DIRS) ; do \ $(ECHO_GEN) envoy/$$path; \ $(PROTOC) -I $(ENVOY_API_PROTO_PATH) -I $(CILIUM_PROTO_PATH) $(PROTO_DEPS) "--go_out=plugins=grpc$(GO_MAPPINGS):$(GO_OUT)" --go_opt=module=github.com/cilium/proxy/go "--validate_out=lang=go$(GO_MAPPINGS):$(GO_OUT)" --validate_opt=module=github.com/cilium/proxy/go $${path}*.proto; \ diff --git a/cilium/accesslog.cc b/cilium/accesslog.cc index 39075dc0d..704f5a68d 100644 --- a/cilium/accesslog.cc +++ b/cilium/accesslog.cc @@ -80,7 +80,7 @@ CONST_STRING_VIEW(xRequestIdSV, "x-request-id"); CONST_STRING_VIEW(statusSV, ":status"); void AccessLog::Entry::initFromConnection( - const std::string& policy_name, uint32_t proxy_id, bool ingress, uint32_t source_identity, + const std::string& policy_name, uint16_t proxy_id, bool ingress, uint32_t source_identity, const Network::Address::InstanceConstSharedPtr& source_address, uint32_t destination_identity, const Network::Address::InstanceConstSharedPtr& destination_address, TimeSource* time_source) { request_logged_ = false; @@ -143,7 +143,7 @@ bool AccessLog::Entry::updateFromMetadata(const std::string& l7proto, return changed; } -void AccessLog::Entry::initFromRequest(const std::string& policy_name, uint32_t proxy_id, +void AccessLog::Entry::initFromRequest(const std::string& policy_name, uint16_t proxy_id, bool ingress, uint32_t source_identity, const Network::Address::InstanceConstSharedPtr& src_address, uint32_t destination_identity, diff --git a/cilium/accesslog.h b/cilium/accesslog.h index 54acaf28f..0ad874d67 100644 --- a/cilium/accesslog.h +++ b/cilium/accesslog.h @@ -32,7 +32,7 @@ class AccessLog : public UDSClient { // wrapper for protobuf class Entry : public StreamInfo::FilterState::Object { public: - void initFromRequest(const std::string& policy_name, uint32_t proxy_id, bool ingress, + void initFromRequest(const std::string& policy_name, uint16_t proxy_id, bool ingress, uint32_t source_identity, const Network::Address::InstanceConstSharedPtr& source_address, uint32_t destination_identity, @@ -43,7 +43,7 @@ class AccessLog : public UDSClient { const Http::RequestHeaderMap&); void updateFromResponse(const Http::ResponseHeaderMap&, TimeSource&); - void initFromConnection(const std::string& policy_name, uint32_t proxy_id, bool ingress, + void initFromConnection(const std::string& policy_name, uint16_t proxy_id, bool ingress, uint32_t source_identity, const Network::Address::InstanceConstSharedPtr& source_address, uint32_t destination_identity, diff --git a/cilium/api/bpf_metadata.proto b/cilium/api/bpf_metadata.proto index 1e8e5cd61..b28d58c18 100644 --- a/cilium/api/bpf_metadata.proto +++ b/cilium/api/bpf_metadata.proto @@ -5,6 +5,7 @@ option go_package = "github.com/cilium/proxy/go/cilium/api;cilium"; package cilium; import "google/protobuf/duration.proto"; +import "validate/validate.proto"; message BpfMetadata { // File system root for bpf. Bpf will not be used if left empty. @@ -45,7 +46,7 @@ message BpfMetadata { // proxy_id is passed to access log messages and allows relating access log messages to // listeners. - uint32 proxy_id = 8; + uint32 proxy_id = 8 [(validate.rules).uint32.lte = 65535]; // policy_update_warning_limit is the time in milliseconds after which a warning is logged if // network policy update took longer diff --git a/cilium/api/npds.proto b/cilium/api/npds.proto index 154a3c4be..d60335d96 100644 --- a/cilium/api/npds.proto +++ b/cilium/api/npds.proto @@ -127,21 +127,26 @@ message TLSContext { // If all the predicates of a rule match a flow, the flow is matched by the // rule. message PortNetworkPolicyRule { - reserved 1; // used in Cilium versions upto 1.14 - // Precedence level for this rule. Rules with **higher** numeric values take // precedence, even over deny rules of lower precedence level. // The lowest precedence (zero) is used when not specified. uint32 precedence = 10; - // Traffic on this port is denied for all `remote_policies` if true - bool deny = 8; + // Optional verdict, mutually exclusive. If missing then the verdict is an allow. + oneof verdict { + // Precedence after which policy evaluation should be continued at for the selected + // remotes_policies. + uint32 pass_precedence = 1; + + // Traffic on this port is denied for all `remote_policies` if true + bool deny = 8; + } // ProxyID is non-zero if the rule was an allow rule with an explicit listener reference. // The given value corresponds to the 'proxy_id' value in the BpfMetadata listener filter // configuration. // This rule should be ignored if not executing in the referred listener. - uint32 proxy_id = 9; + uint32 proxy_id = 9 [(validate.rules).uint32.lte = 65535]; // Optional name for the rule, can be used in logging and error messages. string name = 5; diff --git a/cilium/bpf_metadata.cc b/cilium/bpf_metadata.cc index 00423b278..a6eca8743 100644 --- a/cilium/bpf_metadata.cc +++ b/cilium/bpf_metadata.cc @@ -202,7 +202,7 @@ Config::Config(const ::cilium::BpfMetadata& config, : so_linger_(config.has_original_source_so_linger_time() ? config.original_source_so_linger_time() : -1), - proxy_id_(config.proxy_id()), is_ingress_(config.is_ingress()), + proxy_id_(uint16_t(config.proxy_id())), is_ingress_(config.is_ingress()), use_original_source_address_(config.use_original_source_address()), is_l7lb_(config.is_l7lb()), ipv4_source_address_( diff --git a/cilium/bpf_metadata.h b/cilium/bpf_metadata.h index b69738e08..56b352429 100644 --- a/cilium/bpf_metadata.h +++ b/cilium/bpf_metadata.h @@ -42,7 +42,7 @@ struct SocketMetadata : public Logger::Loggable { Network::Address::InstanceConstSharedPtr source_address_ipv4, Network::Address::InstanceConstSharedPtr source_address_ipv6, Network::Address::InstanceConstSharedPtr original_dest_address, - const PolicyResolverSharedPtr& policy_resolver, uint32_t proxy_id, + const PolicyResolverSharedPtr& policy_resolver, uint16_t proxy_id, std::string&& proxylib_l7_proto, absl::string_view sni) : ingress_source_identity_(ingress_source_identity), source_identity_(source_identity), ingress_(ingress), is_l7lb_(l7lb), port_(port), pod_ip_(std::move(pod_ip)), @@ -120,7 +120,7 @@ struct SocketMetadata : public Logger::Loggable { std::string pod_ip_; // pod policy to enforce, if any; empty only when there is no local pod (i.e. // north/south l7lb) std::string ingress_policy_name_; // Ingress policy to enforce, if any - uint32_t proxy_id_; + uint16_t proxy_id_; std::string proxylib_l7_proto_; std::string sni_; const PolicyResolverSharedPtr policy_resolver_; @@ -158,7 +158,7 @@ class Config : public Cilium::PolicyResolver, virtual bool addPrivilegedSocketOptions() { return true; }; int so_linger_; // negative if disabled - uint32_t proxy_id_; + uint16_t proxy_id_; bool is_ingress_; bool use_original_source_address_; bool is_l7lb_; diff --git a/cilium/filter_state_cilium_policy.h b/cilium/filter_state_cilium_policy.h index 34f4d9375..09c65784f 100644 --- a/cilium/filter_state_cilium_policy.h +++ b/cilium/filter_state_cilium_policy.h @@ -40,7 +40,7 @@ class CiliumPolicyFilterState : public StreamInfo::FilterState::Object, CiliumPolicyFilterState(uint32_t ingress_source_identity, uint32_t source_identity, bool ingress, bool l7lb, uint16_t port, std::string&& pod_ip, std::string&& ingress_policy_name, - const PolicyResolverSharedPtr& policy_resolver, uint32_t proxy_id, + const PolicyResolverSharedPtr& policy_resolver, uint16_t proxy_id, absl::string_view sni) : ingress_source_identity_(ingress_source_identity), source_identity_(source_identity), ingress_(ingress), is_l7lb_(l7lb), port_(port), pod_ip_(std::move(pod_ip)), @@ -91,7 +91,7 @@ class CiliumPolicyFilterState : public StreamInfo::FilterState::Object, uint16_t port_; std::string pod_ip_; std::string ingress_policy_name_; - uint32_t proxy_id_; + uint16_t proxy_id_; std::string sni_; private: diff --git a/cilium/network_policy.cc b/cilium/network_policy.cc index 817a4740b..992b72e1f 100644 --- a/cilium/network_policy.cc +++ b/cilium/network_policy.cc @@ -1,11 +1,16 @@ #include "cilium/network_policy.h" +#include #include +#include #include #include +#include #include +#include #include +#include #include #include #include @@ -55,6 +60,34 @@ #include "cilium/ipcache.h" #include "cilium/secret_watcher.h" +namespace fmt { + +template <> struct formatter { + constexpr auto parse(fmt::format_parse_context& ctx) { return ctx.begin(); } + + template + auto format(Envoy::Cilium::RuleVerdict verdict, FormatContext& ctx) const { + absl::string_view name; + switch (verdict) { + case Envoy::Cilium::RuleVerdict::None: + name = "NONE"; + break; + case Envoy::Cilium::RuleVerdict::Allow: + name = "ALLOW"; + break; + case Envoy::Cilium::RuleVerdict::Deny: + name = "DENY"; + break; + default: + name = "UNKNOWN"; + break; + } + return std::ranges::copy(name, ctx.out()).out; + } +}; + +} // namespace fmt + namespace Envoy { namespace Cilium { @@ -418,10 +451,20 @@ SniPattern::SniPattern(const Regex::Engine& engine, absl::string_view sni) { class PortNetworkPolicyRule : public Logger::Loggable { public: + PortNetworkPolicyRule() + : name_("default allow rule"), deny_(false), proxy_id_(0), precedence_(0), + tier_last_precedence_(0), mutable_remotes_(false), l7_proto_("") {} + PortNetworkPolicyRule(const NetworkPolicyMapImpl& parent, - const cilium::PortNetworkPolicyRule& rule) - : name_(rule.name()), deny_(rule.deny()), precedence_(rule.precedence()), - proxy_id_(rule.proxy_id()), l7_proto_(rule.l7_proto()) { + const cilium::PortNetworkPolicyRule& rule, bool shared_resource) + : name_(rule.name()), deny_(rule.deny()), proxy_id_(uint16_t(rule.proxy_id())), + precedence_(rule.precedence()), tier_last_precedence_(rule.pass_precedence()), + mutable_remotes_(!shared_resource), l7_proto_(rule.l7_proto()) { + if (tier_last_precedence_ > precedence_) { + throw EnvoyException( + fmt::format("PortNetworkPolicyRule: pass_precedence {} must be lower than precedence {}", + tier_last_precedence_, precedence_)); + } for (const auto& remote : rule.remote_policies()) { ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule(): {} remote {} by rule: {}", deny_ ? "Denying" : "Allowing", remote, name_); @@ -440,11 +483,12 @@ class PortNetworkPolicyRule : public Logger::Loggable { allowed_snis_.emplace_back(parent.regexEngine(), sni); } if (rule.has_http_rules()) { + http_rules_ = std::make_shared>(); for (const auto& http_rule : rule.http_rules().http_rules()) { if (http_rule.header_matches_size() > 0) { has_headermatches_ = true; } - http_rules_.emplace_back(parent, http_rule); + http_rules_->emplace_back(parent, http_rule); } } if (l7_proto_.length() > 0 && rule.has_l7_rules()) { @@ -458,168 +502,136 @@ class PortNetworkPolicyRule : public Logger::Loggable { } } - bool allowed(uint32_t proxy_id, uint32_t remote_id, bool& denied) const { + // inheritpassprecedence bumps up the precedence of a rule in a lower tier to the precedence + // range reserved right after the precedence of the given pass rule. + void inheritPassPrecedence(const PortNetworkPolicyRule& pass_rule) { + precedence_ -= pass_rule.tier_last_precedence_; + precedence_ += pass_rule.precedence_; + } + + bool isRemoteWildcard() const { return remotes_.empty(); } + + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id) const { // proxy_id must match if we have any. if (proxy_id_ != 0 && proxy_id != proxy_id_) { - return false; + return RuleVerdict::None; } // Remote ID must match if we have any. - if (!remotes_.empty()) { - auto match = remotes_.find(remote_id); - if (match != remotes_.end()) { - // remote ID matched - if (deny_) { - // Explicit deny - denied = true; - return false; - } - // Explicit allow - return true; - } - // Not found, not allowed, but also not explicitly denied - return false; + if (!isRemoteWildcard() && !remotes_.contains(remote_id)) { + return RuleVerdict::None; // no verdict } // Allow rules allow by default when remotes_ is empty, deny rules do not - if (deny_) { - denied = true; - return false; - } - return true; + return deny_ ? RuleVerdict::Deny : RuleVerdict::Allow; } - bool allowed(uint32_t proxy_id, uint32_t remote_id, absl::string_view sni, bool& denied) const { + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni) const { // sni must match if we have any - if (!allowed_snis_.empty()) { - if (sni.length() == 0) { - return false; - } - bool matched = false; - for (const auto& pattern : allowed_snis_) { - if (pattern.matches(sni)) { - matched = true; - break; - } - } - if (!matched) { - return false; - } + if (!allowed_snis_.empty() && + (sni.length() == 0 || std::ranges::none_of(allowed_snis_, [&](const auto& pattern) { + return pattern.matches(sni); + }))) { + return RuleVerdict::None; } - return allowed(proxy_id, remote_id, denied); + return getVerdict(proxy_id, remote_id); } - bool allowed(uint32_t proxy_id, uint32_t remote_id, Envoy::Http::RequestHeaderMap& headers, - Cilium::AccessLog::Entry& log_entry, bool& denied) const { - if (!allowed(proxy_id, remote_id, denied)) { - return false; + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id, + Envoy::Http::RequestHeaderMap& headers, + Cilium::AccessLog::Entry& log_entry) const { + auto verdict = getVerdict(proxy_id, remote_id); + if (!hasHttpRules() || verdict != RuleVerdict::Allow) { + return verdict; } - if (!http_rules_.empty()) { - bool allowed = false; - for (const auto& rule : http_rules_) { - if (rule.allowed(headers)) { - // Return on the first match if no rule has HeaderMatches - if (!has_headermatches_) { - allowed = true; - break; - } - // orherwise evaluate all rules to run all the header actions, - // and remember if any of them matched - if (rule.headerMatches(headers, log_entry)) { - allowed = true; - } + if (!has_headermatches_) { + if (std::ranges::any_of(*http_rules_, [&](auto& r) { return r.allowed(headers); })) { + return RuleVerdict::Allow; + } + return RuleVerdict::None; + } + + // Evaluate all rules to run all the header actions, + // and remember if any of them matched + bool header_matched = false; + for (const auto& rule : *http_rules_) { + if (rule.allowed(headers)) { + if (rule.headerMatches(headers, log_entry)) { + header_matched = true; } } - return allowed; } - // Empty set matches any payload - return true; + return (header_matched) ? RuleVerdict::Allow : RuleVerdict::None; } - bool useProxylib(uint32_t proxy_id, uint32_t remote_id, std::string& l7_proto, - bool& denied) const { - if (!allowed(proxy_id, remote_id, denied)) { - return false; + RuleVerdict useProxylib(uint16_t proxy_id, uint32_t remote_id, std::string& l7_proto) const { + auto verdict = getVerdict(proxy_id, remote_id); + if (verdict != RuleVerdict::Allow) { + return verdict; } if (l7_proto_.length() > 0) { ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule::useProxylib(): returning {}", l7_proto_); l7_proto = l7_proto_; - return true; + return RuleVerdict::Allow; // found a proxylib match } - return false; + // keep looking past allows if no proxylib + return RuleVerdict::None; } // Envoy Metadata matcher, called after deny has already been checked for - bool allowed(uint32_t proxy_id, uint32_t remote_id, - const envoy::config::core::v3::Metadata& metadata, bool& denied) const { - if (!allowed(proxy_id, remote_id, denied)) { - return false; + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id, + const envoy::config::core::v3::Metadata& metadata) const { + auto verdict = getVerdict(proxy_id, remote_id); + if (verdict != RuleVerdict::Allow) { + return verdict; } - for (const auto& rule : l7_deny_rules_) { - if (rule.matches(metadata)) { - ENVOY_LOG(trace, - "Cilium L7 PortNetworkPolicyRule::allowed(): DENY due to " - "a matching deny rule {}", - rule.name_); - return false; // request is denied if any deny rule matches - } + + if (std::ranges::any_of(l7_deny_rules_, + [&](const auto& rule) { return rule.matches(metadata); })) { + ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule: DENY due to a matching deny rule"); + return RuleVerdict::Deny; // request is denied if any deny rule matches } - if (!l7_allow_rules_.empty()) { - for (const auto& rule : l7_allow_rules_) { - if (rule.matches(metadata)) { - ENVOY_LOG(trace, - "Cilium L7 PortNetworkPolicyRule::allowed(): ALLOW due " - "to a matching allow rule {}", - rule.name_); - return true; - } - } - ENVOY_LOG(trace, - "Cilium L7 PortNetworkPolicyRule::allowed(): DENY due to " - "all {} allow rules mismatching", - l7_allow_rules_.size()); - return false; + + if (l7_allow_rules_.empty()) { + ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule: default ALLOW due to no allow rules"); + return RuleVerdict::Allow; // allowed by default + } + + if (std::ranges::any_of(l7_allow_rules_, + [&](const auto& rule) { return rule.matches(metadata); })) { + ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule: ALLOW due to a matching allow rule"); + return RuleVerdict::Allow; } - ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule::allowed(): default ALLOW " - "due to no allow rules"); - return true; // allowed by default + + ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRule: SKIP due to all allow rules mismatching"); + return RuleVerdict::None; } - Ssl::ContextSharedPtr getServerTlsContext(uint32_t proxy_id, uint32_t remote_id, - absl::string_view sni, - const Ssl::ContextConfig** config, - bool& raw_socket_allowed, bool& denied) const { - if (allowed(proxy_id, remote_id, sni, denied)) { - if (server_context_) { - *config = &server_context_->getTlsContextConfig(); - return server_context_->getTlsContext(); - } - raw_socket_allowed = true; + // getServerTlsContext returns true if the rule has server TLS context that was passed to the + // caller via the reference arguments. + bool getServerTlsContext(Ssl::ContextSharedPtr& tls_context, + const Ssl::ContextConfig*& config) const { + if (server_context_) { + tls_context = server_context_->getTlsContext(); + config = &server_context_->getTlsContextConfig(); + return true; } - return nullptr; + return false; } - Ssl::ContextSharedPtr getClientTlsContext(uint32_t proxy_id, uint32_t remote_id, - absl::string_view sni, - const Ssl::ContextConfig** config, - bool& raw_socket_allowed, bool& denied) const { - if (allowed(proxy_id, remote_id, sni, denied)) { - if (client_context_) { - *config = &client_context_->getTlsContextConfig(); - return client_context_->getTlsContext(); - } - raw_socket_allowed = true; + // getClientTlsContext returns true if the rule has client TLS context that was passed to the + // caller via the reference arguments. + bool getClientTlsContext(Ssl::ContextSharedPtr& tls_context, + const Ssl::ContextConfig*& config) const { + if (client_context_) { + tls_context = client_context_->getTlsContext(); + config = &client_context_->getTlsContextConfig(); + return true; } - return nullptr; + return false; } void toString(int indent, std::string& res) const { res.append(indent - 2, ' ').append("- remotes: ["); - int count = 0; - for (auto remote : remotes_) { - if (count++ > 0) { - res.append(","); - } - res.append(fmt::format("{}", remote)); - } + res.append(fmt::format("{}", fmt::join(remotes_, ","))); res.append("]\n"); if (name_.length() > 0) { @@ -631,6 +643,10 @@ class PortNetworkPolicyRule : public Logger::Loggable { if (precedence_) { res.append(indent, ' ').append(fmt::format("precedence: {}\n", precedence_)); } + if (tier_last_precedence_) { + res.append(indent, ' ') + .append(fmt::format("tier_last_precedence: {}\n", tier_last_precedence_)); + } if (proxy_id_ != 0) { res.append(indent, ' ').append(fmt::format("proxy_id: {}\n", proxy_id_)); } @@ -647,9 +663,9 @@ class PortNetworkPolicyRule : public Logger::Loggable { res.append("]\n"); } - if (!http_rules_.empty()) { + if (hasHttpRules()) { res.append(indent, ' ').append("http_rules:\n"); - for (auto& rule : http_rules_) { + for (auto& rule : *http_rules_) { rule.toString(indent + 2, res); } } @@ -671,23 +687,27 @@ class PortNetworkPolicyRule : public Logger::Loggable { } } - bool hasHttpRules() const { return !http_rules_.empty(); } + bool hasHttpRules() const { return http_rules_ && !http_rules_->empty(); } std::string name_; - DownstreamTLSContextPtr server_context_; - UpstreamTLSContextPtr client_context_; + DownstreamTLSContextSharedPtr server_context_; + UpstreamTLSContextSharedPtr client_context_; bool has_headermatches_{false}; bool deny_; + uint16_t proxy_id_; uint32_t precedence_; - uint32_t proxy_id_; + uint32_t tier_last_precedence_; absl::btree_set remotes_; + bool mutable_remotes_; - std::vector allowed_snis_; // All SNIs allowed if empty. - std::vector http_rules_; // Allowed if empty, but remote is checked first. + std::vector allowed_snis_; // All SNIs allowed if empty. + std::shared_ptr> + http_rules_; // Allowed if empty, but remote is checked first. std::string l7_proto_{}; std::vector l7_allow_rules_; std::vector l7_deny_rules_; }; +using PortNetworkPolicyRuleSharedPtr = std::shared_ptr; using PortNetworkPolicyRuleConstSharedPtr = std::shared_ptr; class PortNetworkPolicyRules : public Logger::Loggable { @@ -701,194 +721,235 @@ class PortNetworkPolicyRules : public Logger::Loggable { } } + void clear() { + rules_.clear(); + can_short_circuit_ = true; + has_pass_rules_ = false; + } + + // Move assignment operator + PortNetworkPolicyRules& operator=(PortNetworkPolicyRules&& other) noexcept = default; + + // Move constructor + PortNetworkPolicyRules(PortNetworkPolicyRules&& other) noexcept = default; + + // Copy constructors + PortNetworkPolicyRules& operator=(const PortNetworkPolicyRules&) = default; + PortNetworkPolicyRules(const PortNetworkPolicyRules&) = default; + + void updateFor(const PortNetworkPolicyRuleConstSharedPtr& rule) { + if (rule->has_headermatches_) { + can_short_circuit_ = false; + } + if (rule->tier_last_precedence_ != 0) { + has_pass_rules_ = true; + } + } + + void addDefaultAllowRule() { rules_.emplace_back(std::make_shared()); } + + // append merges 'rules' to 'rules_' by placing the new 'rules' to the end of 'rules_'. + // First call marks 'rules_' as initialized. Of further calls, if either is empty, + // we must add a default allow rule to retain the semantics of an empty rules. void append(const NetworkPolicyMapImpl& parent, - const Protobuf::RepeatedPtrField& rules) { + const Protobuf::RepeatedPtrField& rules, + bool shared_resource) { + if (initialized_ && rules.empty() != rules_.empty()) { + // add an explicit allow-all rule to keep the combined semantics + addDefaultAllowRule(); + } for (const auto& it : rules) { - rules_.emplace_back(std::make_shared(parent, it)); - if (rules_.back()->has_headermatches_) { - can_short_circuit_ = false; - } + rules_.emplace_back(std::make_shared(parent, it, shared_resource)); + updateFor(rules_.back()); } + initialized_ = true; } + // prepend merges 'rules' to 'rules_' by placing the new 'rules' to the front of 'rules_'. + // First call marks 'rules_' as initialized. Of further calls, if either is empty, + // we must add a default allow rule to retain the semantics of an empty rules. void prepend(const NetworkPolicyMapImpl& parent, - const Protobuf::RepeatedPtrField& rules) { + const Protobuf::RepeatedPtrField& rules, + bool shared_resource) { + if (initialized_ && rules.empty() != rules_.empty()) { + // add an explicit allow-all rule to keep the combined semantics + rules_.emplace(rules_.begin(), std::make_shared()); + } for (const auto& it : rules) { - rules_.emplace(rules_.begin(), std::make_shared(parent, it)); - if (rules_.front()->has_headermatches_) { - can_short_circuit_ = false; + rules_.emplace(rules_.begin(), + std::make_shared(parent, it, shared_resource)); + updateFor(rules_.front()); + } + initialized_ = true; + } + + // appendNonPassRules merges non-pass rules from 'rules' to 'rules_' by placing the new rules to + // the end of 'rules_'. First call marks 'rules_' as initialized. Of further calls, if either is + // empty, we must add a default allow rule to retain the semantics of an empty rules. + void appendNonPassRules(const std::vector& rules) { + if (initialized_ && rules.empty() != rules_.empty()) { + // add an explicit allow-all rule to keep the combined semantics + addDefaultAllowRule(); + } + for (auto& rule : rules) { + if (rule->tier_last_precedence_ == 0) { + rules_.insert(rules_.end(), rule); + updateFor(rule); } } + initialized_ = true; } - // sort by descending precedence + // sort by descending precedence, retaining the original order within each precedence level void sort() { - std::sort(rules_.begin(), rules_.end(), - [](const PortNetworkPolicyRuleConstSharedPtr& a, - const PortNetworkPolicyRuleConstSharedPtr& b) { - return (a->precedence_ > b->precedence_) || - (a->precedence_ == b->precedence_ && (a->deny_ && !b->deny_)); - }); + // sortRules(rules_); + std::stable_sort(rules_.begin(), rules_.end(), + [](const PortNetworkPolicyRuleConstSharedPtr& a, + const PortNetworkPolicyRuleConstSharedPtr& b) { + return (a->precedence_ > b->precedence_) || + (a->precedence_ == b->precedence_ && (a->deny_ && !b->deny_)); + }); } bool empty() const { return rules_.empty(); } - enum class RuleVerdict { - None, - Intermediate, - Final, - }; + template RuleVerdict forEachRule(bool can_short_circuit, F&& func) const { + RuleVerdict verdict = RuleVerdict::None; + uint32_t verdict_precedence = 0; - template void forEachRule(F&& func) const { - uint32_t precedence = 0; - bool have_verdict = false; + // Uninitialized rules match nothing + ASSERT(initialized_, "uninitialized rules"); + if (!initialized_) { + return verdict; + } + + // Empty set matches any payload from anyone + if (empty()) { + return RuleVerdict::Allow; + } for (const auto& rule : rules_) { // lower precedence rules are skipped if there is a verdict - if (rule->precedence_ < precedence && have_verdict) { + if (verdict != RuleVerdict::None && rule->precedence_ < verdict_precedence) { break; } - precedence = rule->precedence_; - - RuleVerdict v = func(*rule); + auto rule_verdict = func(*rule); + if (rule_verdict != RuleVerdict::None) { + verdict = rule_verdict; + verdict_precedence = rule->precedence_; - if (v == RuleVerdict::Final) { - break; - } - if (v == RuleVerdict::Intermediate) { - have_verdict = true; + // Short-circuit on the first deny or on first allow if no rules have HeaderMatches + if (rule_verdict == RuleVerdict::Deny || can_short_circuit) { + break; + } } } + return verdict; } - bool allowed(uint32_t proxy_id, uint32_t remote_id, Envoy::Http::RequestHeaderMap& headers, - Cilium::AccessLog::Entry& log_entry, bool& denied) const { + // forEachRulePred return a RuleVerdict by scanning through all rules, getting the verdict for + // each rule and checking if the predicate matches. Returns RuleVerdict::Allow if any rule allows + // the traffic, even if the predicate returns false. Stops as soon as the first rule returns + // 'true' for the predicate. + // This is used to the applicable TLS context from the rules, if any. Note that in this use 'pred' + // has side effects, but it is idempotent. + template RuleVerdict forEachRulePred(F&& get_verdict, P&& pred) const { + RuleVerdict verdict = RuleVerdict::None; + uint32_t verdict_precedence = 0; + + // Uninitialized rules match nothing + ASSERT(initialized_, "uninitialized rules"); + if (!initialized_) { + return verdict; + } + // Empty set matches any payload from anyone - if (rules_.empty()) { - return true; + if (empty()) { + return RuleVerdict::Allow; } - bool allowed = false; - forEachRule([&](const auto& rule) { - if (rule.allowed(proxy_id, remote_id, headers, log_entry, denied)) { - allowed = true; - // Short-circuit on the first match if no rules have HeaderMatches - if (can_short_circuit_) { - return RuleVerdict::Final; + for (const auto& rule : rules_) { + auto rule_verdict = get_verdict(*rule); + switch (rule_verdict) { + case RuleVerdict::Deny: + // return higher precedence allow verdict if any. + if (verdict != RuleVerdict::None && verdict_precedence > rule->precedence_) { + return verdict; + } + return rule_verdict; + case RuleVerdict::Allow: + if (pred(*rule)) { + // Return after the first allow verdict that fulfills the predicate + return rule_verdict; + } + // store highest precedence allow verdict that does not fulfill the predicate + if (verdict == RuleVerdict::None) { + verdict = rule_verdict; + verdict_precedence = rule->precedence_; } - return RuleVerdict::Intermediate; + break; + case RuleVerdict::None: + break; } - return denied ? RuleVerdict::Final : RuleVerdict::None; + } + return verdict; + } + + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id, + Envoy::Http::RequestHeaderMap& headers, + Cilium::AccessLog::Entry& log_entry) const { + auto verdict = forEachRule(can_short_circuit_, [&](const auto& rule) { + return rule.getVerdict(proxy_id, remote_id, headers, log_entry); }); ENVOY_LOG(trace, - "Cilium L7 PortNetworkPolicyRules(proxy_id: {}, remote_id: {}, headers: {}): " - "{}", - proxy_id, remote_id, headers, allowed && !denied ? "ALLOWED" : "DENIED"); - return allowed && !denied; + "Cilium L7 PortNetworkPolicyRules(proxy_id: {}, remote_id: {}, headers: {}): {}", + proxy_id, remote_id, headers, verdict); + return verdict; } - bool allowed(uint32_t proxy_id, uint32_t remote_id, absl::string_view sni, bool& denied) const { - // Empty set matches any payload from anyone - if (rules_.empty()) { - return true; - } - - bool allowed = false; - forEachRule([&](const auto& rule) { - if (rule.allowed(proxy_id, remote_id, sni, denied)) { - allowed = true; - // Short-circuit on the first match if no rules have HeaderMatches - if (can_short_circuit_) { - return RuleVerdict::Final; - } - return RuleVerdict::Intermediate; - } - return denied ? RuleVerdict::Final : RuleVerdict::None; - }); + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni) const { + auto verdict = forEachRule( + true, [&](const auto& rule) { return rule.getVerdict(proxy_id, remote_id, sni); }); - ENVOY_LOG(trace, - "Cilium L7 PortNetworkPolicyRules(proxy_id: {}, remote_id: {}, sni: {}): " - "{}", - proxy_id, remote_id, sni, allowed && !denied ? "ALLOWED" : "DENIED"); - return allowed && !denied; - } - - bool useProxylib(uint32_t proxy_id, uint32_t remote_id, std::string& l7_proto) const { - bool denied = false; - bool use_proxylib = false; - forEachRule([&](const auto& rule) { - if (rule.useProxylib(proxy_id, remote_id, l7_proto, denied)) { - use_proxylib = true; - return RuleVerdict::Final; - } - return denied ? RuleVerdict::Final : RuleVerdict::None; - }); - return use_proxylib && !denied; + ENVOY_LOG(trace, "Cilium L7 PortNetworkPolicyRules(proxy_id: {}, remote_id: {}, sni: {}): {}", + proxy_id, remote_id, sni, verdict); + return verdict; } - bool allowed(uint32_t proxy_id, uint32_t remote_id, - const envoy::config::core::v3::Metadata& metadata, bool& denied) const { - // Empty set matches any payload from anyone - if (rules_.empty()) { - return true; - } + RuleVerdict useProxylib(uint16_t proxy_id, uint32_t remote_id, std::string& l7_proto) const { + return forEachRule( + true, [&](const auto& rule) { return rule.useProxylib(proxy_id, remote_id, l7_proto); }); + } - bool allowed = false; - forEachRule([&](const auto& rule) { - if (rule.allowed(proxy_id, remote_id, metadata, denied)) { - allowed = true; - // Short-circuit on the first match if no rules have HeaderMatches - if (can_short_circuit_) { - return RuleVerdict::Final; - } - return RuleVerdict::Intermediate; - } - return denied ? RuleVerdict::Final : RuleVerdict::None; - }); + RuleVerdict getVerdict(uint16_t proxy_id, uint32_t remote_id, + const envoy::config::core::v3::Metadata& metadata) const { + auto verdict = forEachRule( + true, [&](const auto& rule) { return rule.getVerdict(proxy_id, remote_id, metadata); }); ENVOY_LOG(trace, - "Cilium L7 PortNetworkPolicyRules(proxy_id: {}, remote_id: {}, metadata: {}): " - "{}", - proxy_id, remote_id, metadata.DebugString(), - allowed && !denied ? "ALLOWED" : "DENIED"); - return allowed && !denied; - } - - Ssl::ContextSharedPtr getServerTlsContext(uint32_t proxy_id, uint32_t remote_id, - absl::string_view sni, - const Ssl::ContextConfig** config, - bool& raw_socket_allowed) const { - bool denied = false; - Ssl::ContextSharedPtr tls_ctx = nullptr; - forEachRule([&](const auto& rule) { - Ssl::ContextSharedPtr server_context = - rule.getServerTlsContext(proxy_id, remote_id, sni, config, raw_socket_allowed, denied); - if (server_context) { - tls_ctx = server_context; - return RuleVerdict::Final; - } - return denied ? RuleVerdict::Final : RuleVerdict::None; - }); - return denied ? nullptr : tls_ctx; - } - - Ssl::ContextSharedPtr getClientTlsContext(uint32_t proxy_id, uint32_t remote_id, - absl::string_view sni, - const Ssl::ContextConfig** config, - bool& raw_socket_allowed) const { - bool denied = false; - Ssl::ContextSharedPtr tls_ctx = nullptr; - forEachRule([&](const auto& rule) { - Ssl::ContextSharedPtr client_context = - rule.getClientTlsContext(proxy_id, remote_id, sni, config, raw_socket_allowed, denied); - if (client_context) { - tls_ctx = client_context; - return RuleVerdict::Final; - } - return denied ? RuleVerdict::Final : RuleVerdict::None; - }); - return denied ? nullptr : tls_ctx; + "Cilium L7 PortNetworkPolicyRules(proxy_id: {}, remote_id: {}, metadata: {}): {}", + proxy_id, remote_id, metadata.DebugString(), verdict); + + return verdict; + } + + RuleVerdict getServerTlsContext(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, + Ssl::ContextSharedPtr& tls_ctx, + const Ssl::ContextConfig*& config) const { + tls_ctx = nullptr; + return forEachRulePred( + [&](const auto& rule) { return rule.getVerdict(proxy_id, remote_id, sni); }, + [&](const auto& rule) { return rule.getServerTlsContext(tls_ctx, config); }); + } + + RuleVerdict getClientTlsContext(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, + Ssl::ContextSharedPtr& tls_ctx, + const Ssl::ContextConfig*& config) const { + tls_ctx = nullptr; + return forEachRulePred( + [&](const auto& rule) { return rule.getVerdict(proxy_id, remote_id, sni); }, + [&](const auto& rule) { return rule.getClientTlsContext(tls_ctx, config); }); } void toString(int indent, std::string& res) const { @@ -913,70 +974,48 @@ class PortNetworkPolicyRules : public Logger::Loggable { // ordered set of rules as a sorted vector std::vector rules_; // Allowed if empty. bool can_short_circuit_{true}; + bool has_pass_rules_{false}; + bool initialized_{false}; }; -// end port is zero on lookup! -PortPolicy::PortPolicy(const PolicyMap& map, uint16_t port) - : map_(map), wildcard_rules_([&]() { - const auto it = map_.find({0, 0}); - return it != map_.cend() ? &it->second : nullptr; - }()), - port_rules_([&]() { - const auto it = map_.find({port, port}); - return it != map_.cend() ? &it->second : nullptr; - }()), - has_http_rules_((port_rules_ && port_rules_->hasHttpRules()) || - (wildcard_rules_ && wildcard_rules_->hasHttpRules())) {} - -// forRange is used for policy lookups, so it will need to check both port-specific and -// wildcard-port rules, as either of them could contain rules that must be evaluated (i.e., deny -// or header match rules with side effects). -bool PortPolicy::forRange( - std::function allowed) const { - bool allow = false; - bool denied = false; - if (port_rules_) { - if (allowed(*port_rules_, denied)) { - allow = true; - } +namespace { + +const PortNetworkPolicyRules* findPortRules(const PolicyMap& map, uint16_t port) { + // Look up with an exact port first, then fall back to the wildcard port (0). If policy is found + // with the exact port, then the returned policy also contains all the wildcard port rules, so we + // do not need to perform a separate wildcard port policy lookup. If no policy is defined for the + // given port, then the wildcard port policy, consisting just of the wildcard port rules, is used, + // if one exists. + // + // On lookups we'll set both ends of the port range to the same port number, which will find the + // one range that it overlaps with in the map, if one exists (ref. PortRangeCompare definition). + if (const auto it = map.find({port, port}); it != map.cend()) { + return &it->second; } - // Wildcard port can deny a specific remote, so need to check for it too. - if (!(allow && wildcard_rules_ && wildcard_rules_->can_short_circuit_)) { - if (wildcard_rules_ && allowed(*wildcard_rules_, denied)) { - allow = true; - } + if (const auto wildcard = map.find({0, 0}); wildcard != map.cend()) { + return &wildcard->second; } - return allow && !denied; + return nullptr; } -// forFirstRange is used for proxylib parser and TLS context selection. -// -// rules for the specific ports are checked first, and within there singe-port ranges are placed in -// the front, while actual ranges are placed in the back. This results in the following precedence -// order for both proxylib parser and TLS context selection: -// -// 1. single port rules (e.g., port 80) -// 2. port ranges (e.g., ports 80-90) -// 3. Wildcard port rules -// -bool PortPolicy::forFirstRange(std::function f) const { - if (port_rules_ && f(*port_rules_)) { - return true; - } - // Check the wildcard port entry - if (wildcard_rules_ && f(*wildcard_rules_)) { - return true; +} // namespace + +PortPolicy::PortPolicy(const PolicyMap& map, uint16_t port) + : map_(map), port_rules_(findPortRules(map_, port)), + has_http_rules_(port_rules_ && port_rules_->hasHttpRules()) {} + +bool PortPolicy::useProxylib(uint16_t proxy_id, uint32_t remote_id, std::string& l7_proto) const { + if (port_rules_) { + auto verdict = port_rules_->useProxylib(proxy_id, remote_id, l7_proto); + if (verdict == RuleVerdict::Allow) { + return true; + } } + l7_proto = ""; return false; } -bool PortPolicy::useProxylib(uint32_t proxy_id, uint32_t remote_id, std::string& l7_proto) const { - return forFirstRange([&](const PortNetworkPolicyRules& rules) -> bool { - return rules.useProxylib(proxy_id, remote_id, l7_proto); - }); -} - -bool PortPolicy::allowed(uint32_t proxy_id, uint32_t remote_id, +bool PortPolicy::allowed(uint16_t proxy_id, uint32_t remote_id, Envoy::Http::RequestHeaderMap& headers, Cilium::AccessLog::Entry& log_entry) const { // Network layer policy has already been enforced. If there are no http rules, then there is @@ -984,46 +1023,55 @@ bool PortPolicy::allowed(uint32_t proxy_id, uint32_t remote_id, if (!has_http_rules_) { return true; } - return forRange([&](const PortNetworkPolicyRules& rules, bool& denied) -> bool { - return rules.allowed(proxy_id, remote_id, headers, log_entry, denied); - }); + if (!port_rules_) { + return false; + } + return port_rules_->getVerdict(proxy_id, remote_id, headers, log_entry) == RuleVerdict::Allow; } -bool PortPolicy::allowed(uint32_t proxy_id, uint32_t remote_id, absl::string_view sni) const { - return forRange([&](const PortNetworkPolicyRules& rules, bool& denied) -> bool { - return rules.allowed(proxy_id, remote_id, sni, denied); - }); +bool PortPolicy::allowed(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni) const { + if (!port_rules_) { + return false; + } + return port_rules_->getVerdict(proxy_id, remote_id, sni) == RuleVerdict::Allow; } -bool PortPolicy::allowed(uint32_t proxy_id, uint32_t remote_id, +bool PortPolicy::allowed(uint16_t proxy_id, uint32_t remote_id, const envoy::config::core::v3::Metadata& metadata) const { - return forRange([&](const PortNetworkPolicyRules& rules, bool& denied) -> bool { - return rules.allowed(proxy_id, remote_id, metadata, denied); - }); + if (!port_rules_) { + return false; + } + return port_rules_->getVerdict(proxy_id, remote_id, metadata) == RuleVerdict::Allow; } -Ssl::ContextSharedPtr PortPolicy::getServerTlsContext(uint32_t proxy_id, uint32_t remote_id, +Ssl::ContextSharedPtr PortPolicy::getServerTlsContext(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, - const Ssl::ContextConfig** config, + const Ssl::ContextConfig*& config, bool& raw_socket_allowed) const { - Ssl::ContextSharedPtr ret; - forFirstRange([&](const PortNetworkPolicyRules& rules) -> bool { - ret = rules.getServerTlsContext(proxy_id, remote_id, sni, config, raw_socket_allowed); - return ret != nullptr; - }); - return ret; + Ssl::ContextSharedPtr tls_ctx; + + config = nullptr; + raw_socket_allowed = false; + if (port_rules_) { + auto verdict = port_rules_->getServerTlsContext(proxy_id, remote_id, sni, tls_ctx, config); + raw_socket_allowed = verdict == RuleVerdict::Allow && tls_ctx == nullptr && config == nullptr; + } + return tls_ctx; } -Ssl::ContextSharedPtr PortPolicy::getClientTlsContext(uint32_t proxy_id, uint32_t remote_id, +Ssl::ContextSharedPtr PortPolicy::getClientTlsContext(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, - const Ssl::ContextConfig** config, + const Ssl::ContextConfig*& config, bool& raw_socket_allowed) const { - Ssl::ContextSharedPtr ret; - forFirstRange([&](const PortNetworkPolicyRules& rules) -> bool { - ret = rules.getClientTlsContext(proxy_id, remote_id, sni, config, raw_socket_allowed); - return ret != nullptr; - }); - return ret; + Ssl::ContextSharedPtr tls_ctx; + + config = nullptr; + raw_socket_allowed = false; + if (port_rules_) { + auto verdict = port_rules_->getClientTlsContext(proxy_id, remote_id, sni, tls_ctx, config); + raw_socket_allowed = verdict == RuleVerdict::Allow && tls_ctx == nullptr && config == nullptr; + } + return tls_ctx; } // Ranges overlap when one is not completely below or above the other @@ -1032,6 +1080,447 @@ bool inline rangesOverlap(const PortRange& a, const PortRange& b) { return a.second >= b.first && a.first <= b.second; } +template +absl::btree_set intersection(const absl::btree_set& a, const absl::btree_set& b) { + absl::btree_set result; + std::set_intersection(a.begin(), a.end(), b.begin(), b.end(), + std::inserter(result, result.begin())); + return result; +} + +// ShadowedRemotes maintains state for shadowed remote identities within a tier. When an higher +// precedence rule has a verdict for a given remote identity, that identity becomes "shadowed" and +// is removed from the set of remote identities of the remaining rules of the tier. For pass +// verdicts this shadowing is immediate, for allow/deny verdicts the shadowing takes place for the +// next precedence level, so that rules on the same precedence level do not shadow each other. +class ShadowedRemotes { +public: + // reset is used to re-initialize state for new port range + void reset(uint32_t first_precedence) { + shadowed_pass_remotes_.clear(); + shadowed_nonpass_remotes_.clear(); + current_precedence_nonpass_remotes_.clear(); + shadow_all_lower_precedence_ = false; + current_precedence_has_wildcard_ = false; + previous_precedence_ = first_precedence; + } + + // resetForNewTier is used to re-initialize state for each new tier + void resetForNewTier(uint32_t first_precedence) { + shadowed_pass_remotes_.clear(); + // shadowed_nonpass_remotes_ are kept for the new tier + // shadow_all_lower_precedence_ is not reset for the new tier + current_precedence_nonpass_remotes_.clear(); + // current_precedence_has_wildcard_ = false; + previous_precedence_ = first_precedence; + } + + // shadowRemotes marks 'remotes' as shadowed and returns 'true' if any of them were not already + // shadowed. + bool shadowPassRemotes(const absl::btree_set& pass_remotes) { + bool any_unshadowed = false; + for (auto remote : pass_remotes) { + if (!shadowed_pass_remotes_.contains(remote)) { + if (!shadowed_nonpass_remotes_.contains(remote)) { + any_unshadowed = true; + } + shadowed_pass_remotes_.insert(remote); + } + } + return any_unshadowed; + } + + // filterShadowPassRemotes filters out any already shadowed remotes from the pass rule and marks + // the remaining remotes as shadowed. Returns 'true' if any remotes remain. + bool filterShadowPassRemotes(PortNetworkPolicyRule& rule) { + absl::erase_if(rule.remotes_, [&](const auto& x) { + return shadowed_pass_remotes_.contains(x) || shadowed_nonpass_remotes_.contains(x); + }); + if (rule.remotes_.empty()) { + return false; + } + shadowed_pass_remotes_.insert(rule.remotes_.begin(), rule.remotes_.end()); + return true; + } + + // shadowRule collects the remotes from 'rule' for shadowing once the first rule on the next + // (lower) precedence level is processed. No shadowing between rules on the same precedence + // level. Returns 'true' if the rule itself should be skipped. + bool shadowNonpassRule(PortNetworkPolicyRule& rule) { + // Same-precedence allow/deny rules do not shadow each other. + // Only after leaving a precedence level do its verdict identities shadow + // lower-precedence rules. + if (rule.precedence_ != previous_precedence_) { + shadowed_nonpass_remotes_.insert(current_precedence_nonpass_remotes_.begin(), + current_precedence_nonpass_remotes_.end()); + current_precedence_nonpass_remotes_.clear(); + if (current_precedence_has_wildcard_) { + shadow_all_lower_precedence_ = true; + } + // current_precedence_has_wildcard_ = false; + previous_precedence_ = rule.precedence_; + } + + if (shadow_all_lower_precedence_) { + return true; + } + + if (rule.isRemoteWildcard()) { + current_precedence_has_wildcard_ = true; + } else { + if (rule.mutable_remotes_) { + // Check if this rule is (partially) shadowed by nonpass rules on any higher tier. + if (!shadowed_nonpass_remotes_.empty()) { + absl::erase_if(rule.remotes_, + [&](const auto& x) { return shadowed_nonpass_remotes_.contains(x); }); + if (rule.remotes_.empty()) { + return true; + } + } + + // Check if this rule is (partially) shadowed by pass rules on this tier. + if (!shadowed_pass_remotes_.empty()) { + absl::erase_if(rule.remotes_, + [&](const auto& x) { return shadowed_pass_remotes_.contains(x); }); + if (rule.remotes_.empty()) { + return true; + } + } + } else { + // rule.remotes_ can not be modified, check if it is completely shadowed + if (std::ranges::all_of(rule.remotes_, [&](const auto& x) { + return shadowed_nonpass_remotes_.contains(x) || shadowed_pass_remotes_.contains(x); + })) { + return true; + } + } + + // Defer shadowing identities until this precedence level is complete, + // so same-precedence rules do not shadow each other. + current_precedence_nonpass_remotes_.insert(rule.remotes_.begin(), rule.remotes_.end()); + } + + return false; + } + +private: + absl::btree_set shadowed_pass_remotes_; + absl::btree_set shadowed_nonpass_remotes_; + absl::btree_set current_precedence_nonpass_remotes_; + uint32_t previous_precedence_{0}; + bool shadow_all_lower_precedence_{false}; + bool current_precedence_has_wildcard_{false}; +}; + +class Passes { +public: + // reset state for a new port range + void reset(uint32_t first_precedence) { + wildcard_it_ = wildcard_pass_rules_.begin(); + pass_rules_.clear(); + pass_rules_tier_index_.clear(); + + tier_pass_rules_.clear(); + tier_wildcard_pass_ = false; + pass_precedence_ = 0; + + shadowing_.reset(first_precedence); + } + + void resetForNextTier(uint32_t first_precedence) { + // Move the collected pass rules to be considered for lower tiers. + pass_rules_tier_index_.push_back(pass_rules_.size()); + pass_rules_.insert(pass_rules_.end(), std::make_move_iterator(tier_pass_rules_.begin()), + std::make_move_iterator(tier_pass_rules_.end())); + + tier_pass_rules_.clear(); + tier_wildcard_pass_ = false; + pass_precedence_ = 0; + + shadowing_.resetForNewTier(first_precedence); + } + + void inheritHigherTierWildcardPassRules(uint32_t precedence) { + // Inherit *higher tier* pass rules from the wildcard port. + // Needed since the wildcard port may have more tiers. + for (; wildcard_it_ != wildcard_pass_rules_.end() && + (*wildcard_it_)->tier_last_precedence_ > precedence; + wildcard_it_++) { + const auto& wildcard_rule = *wildcard_it_; + // Add index entry if this starts a new pass tier. + if (pass_rules_.empty() || + wildcard_rule->tier_last_precedence_ != pass_rules_.back()->tier_last_precedence_) { + pass_rules_tier_index_.push_back(pass_rules_.size()); + } + pass_rules_.insert(pass_rules_.end(), wildcard_rule); + } + } + + void inheritCurrentTierWildcardPassRules(uint32_t precedence) { + // Inherit *current tier* higher or equal precedence pass rules from wildcard port. + for (; + wildcard_it_ != wildcard_pass_rules_.end() && (*wildcard_it_)->precedence_ >= precedence && + (*wildcard_it_)->tier_last_precedence_ <= precedence; + wildcard_it_++) { + const auto& wildcard_rule = *wildcard_it_; + + ensurePassPrecedence(wildcard_rule->tier_last_precedence_); + + if (tier_wildcard_pass_) { + continue; + } + + // Insert to tier_pass_rules_ if any unshadowed remotes remain. + if (wildcard_rule->isRemoteWildcard()) { + tier_wildcard_pass_ = true; + } else if (!shadowing_.shadowPassRemotes(wildcard_rule->remotes_)) { + // Only insert if some remotes are not already shadowed. + // We do not remove already-shadowed remotes from wildcard_rule because + // wildcard pass entries are shared and deep-copying here is expensive. + continue; + } + tier_pass_rules_.emplace_back(wildcard_rule); + } + } + + // addPassRule adds state from a rule with a pass verdict. + void addPassRule(const PortNetworkPolicyRuleConstSharedPtr& rule) { + ensurePassPrecedence(rule->tier_last_precedence_); + + auto& mutable_rule = const_cast(*rule); + + if (!tier_wildcard_pass_) { + if (mutable_rule.isRemoteWildcard()) { + tier_wildcard_pass_ = true; + } else if (!shadowing_.filterShadowPassRemotes(mutable_rule)) { + return; + } + if (!tier_pass_rules_.empty() && tier_pass_rules_.back()->precedence_ == rule->precedence_) { + // Same-precedence pass rule already exists; merge remotes. + tier_pass_rules_.back()->remotes_.insert(mutable_rule.remotes_.begin(), + mutable_rule.remotes_.end()); + } else { + tier_pass_rules_.emplace_back(std::const_pointer_cast(rule)); + } + } + } + + bool + promoteRuleFromHigherTierPasses(std::vector& rules, + std::vector::iterator& it) { + // Mutable reference to the rule for in-place updates below. + auto& rule = const_cast(**it); + + bool promoted = false; + + // Check if this rule needs to be (partially) promoted due to higher-tier passes: + // - pick highest-precedence pass from each higher tier for each remote ID + // - apply in reverse order of tiers + // - if all remotes are covered, promotion can happen fully in-place. + int tier_end = pass_rules_.size(); + for (int tier_start : pass_rules_tier_index_ | std::views::reverse) { + // Skip pass rules on same or lower tiers. + if (pass_rules_[tier_start]->tier_last_precedence_ < rule.precedence_) { + continue; + } + + for (int idx = tier_start; idx < tier_end; idx++) { + auto& pass_rule = pass_rules_[idx]; + // Whole rule is promoted in-place if pass is wildcard or sets are equal. + if (pass_rule->isRemoteWildcard() || rule.remotes_ == pass_rule->remotes_) { + rule.inheritPassPrecedence(*pass_rule); + promoted = true; + break; // Later pass verdicts on this tier have no effect. + } + + // Pass rule is not wildcard and sets differ. + // If mutable_rule is wildcard, keep original and add promoted clone. + if (rule.isRemoteWildcard()) { + auto new_rule = std::make_shared(rule); + new_rule->remotes_ = pass_rule->remotes_; + new_rule->inheritPassPrecedence(*pass_rule); + it = rules.insert(it, new_rule); + it++; + promoted = true; + continue; // Later pass verdicts may specify other remote sets. + } + + // Neither side is wildcard; split by set intersection. + auto remotes = intersection(pass_rule->remotes_, rule.remotes_); + if (!remotes.empty()) { + auto new_rule = std::make_shared(rule); + new_rule->remotes_ = remotes; + new_rule->inheritPassPrecedence(*pass_rule); + it = rules.insert(it, new_rule); + it++; + promoted = true; + for (const auto& remote : remotes) { + rule.remotes_.erase(remote); + } + } + } + // Update for previous tier, if any. + tier_end = tier_start; + } + + return promoted; + } + + // storeWildcardPassRules stores the current pass rules as wildcard port pass rules to be + // considered when processing non-wildcard port rules. + void storeWildcardPassRules(const PortRange& port_range) { + if (!wildcard_pass_rules_.empty()) { + throw EnvoyException(fmt::format("PortNetworkPolicy: Wildcard port range {}-{}, but " + "wildcard pass rules has already been set", + port_range.first, port_range.second)); + } + wildcard_pass_rules_ = pass_rules_; + + // store also the pass rules for the last tier, as they are not yet included in pass_rules_. + if (pass_precedence_ != 0 && !tier_pass_rules_.empty()) { + wildcard_pass_rules_.insert(wildcard_pass_rules_.end(), + std::make_move_iterator(tier_pass_rules_.begin()), + std::make_move_iterator(tier_pass_rules_.end())); + } + } + + // Applies pass verdicts for rules on a given port range. + // Returns true if resulting rules should be kept, false if the rules became empty. + // + // - a pass verdict rule applies on a given port (range) (can be the wildcard port), + // and a set of remote IDs (L3). If the remote ID set is empty, then it applies to all peers. + // (there is no "wildcard identity" (e.g., '0') in the set of the remote IDs) + // - a pass verdict rule (like a deny verdict rule) has no L7 rule components + // - each pass verdict rule has a specific precedence and pass_precedence, and the function is + // to bypass the remaining lower precedence rules upto the pass_precedence, and to promote the + // priority of the intersecting remote ID set lower precedence rules to immediately follow + // the precedence of the pass verdict rule. + // - Precedence promotion is needed to make a verdicts found via actual port vs. wildcard port + // comparable. This allows a higher precedence allow rule to take precedence over a lower + // precedence deny rule, even it the allow originally had a lower precedence, but was + // "passed-to" from a higher precedece pass rule. + // + // We pre-process the rules accordingly here so that the policy lookup at enforcement time + // does not need to consider pass verdicts at all. The key insights to consider are: + // - rules are already split up to non-overlapping port ranges, so we only need + // to consider the remote ID sets (and the wildcard port) + // - if the lower precedence rule remote ID set is covered by the pass rule remote ID set, + // then we can simply promote the precedence (and re-sort afterwards) + // - if the pass rule applies to all remote IDs (empty set == wildcard), then it covers all + // possible sets of remote IDs + // - if not wildcard, but the sets are the same, then the pass verdicts "covers" the rule + // in question + // - otherwise the rule needs to be split into two: + // - one with the intersection of the remote IDs of the two rules, with precedence promotion + // - other with the remaining remote IDs, left with the original precedence + // - this includes the case where the lower precedence rule applies to all identities + // (empty ID set) + void apply(const PortRange& port_range, PortNetworkPolicyRules& rules) { + if (rules.rules_.empty() && !wildcard_pass_rules_.empty()) { + // add the default allow rule so that the wildcard port pass can apply to it. + rules.addDefaultAllowRule(); + } + if (!rules.rules_.empty() && (!wildcard_pass_rules_.empty() || rules.has_pass_rules_)) { + bool must_sort = false; + + // reset state for the new range's rules + reset(rules.rules_.front()->precedence_); + + bool keep = false; // assume rule is dropped + for (auto it = rules.rules_.begin(); it != rules.rules_.end(); + it = keep ? (keep = false, ++it) : rules.rules_.erase(it)) { + auto& rule = *it; + + // Check if we have reached the next tier. + if (pass_precedence_ != 0 && rule->precedence_ < pass_precedence_) { + resetForNextTier(rule->precedence_); + } + + // Skip remaining rules on this tier? + if (tier_wildcard_pass_) { + continue; + } + + // Inherit wildcard-port pass rules affecting this rule. + inheritHigherTierWildcardPassRules(rule->precedence_); + inheritCurrentTierWildcardPassRules(rule->precedence_); + + // skip remaining rules on this tier? + if (tier_wildcard_pass_) { + continue; + } + + // Is this a pass verdict rule? + if (rule->tier_last_precedence_ != 0) { + addPassRule(rule); + // Pass rules are not kept + continue; + } + + // Is the rule shadowed? (If not then updates shadowed state) + if (shadowing_.shadowNonpassRule(const_cast(*rule))) { + continue; + } + + // Apply passes to the rule and insert. + if (promoteRuleFromHigherTierPasses(rules.rules_, it)) { + must_sort = true; + } + // keep rule in place + keep = true; + } + + // Have to sort if precedences have been updated in-place. + if (must_sort) { + rules.sort(); + + // remove shadowed rules due to promoted precedences + shadowing_.reset(rules.rules_.front()->precedence_); + for (auto it = rules.rules_.begin(); it != rules.rules_.end(); + it = keep ? ++it : rules.rules_.erase(it)) { + keep = !shadowing_.shadowNonpassRule(const_cast(**it)); + } + } + + // Store wildcard port passes for consideration for non-wildcard ports. + if (port_range.first == 0) { + storeWildcardPassRules(port_range); + } + + // Mark ranges with all rules removed for clean-up. + if (rules.empty()) { + // Empty rule set would always allow. Mark for removal. + empty_ranges_.push_back(port_range); + } + } + } + + std::vector& emptyRanges() { return empty_ranges_; } + +private: + void ensurePassPrecedence(uint32_t tier_last_precedence) { + // pass_precedence_ is non-zero when a pass verdict has been seen and + // defines the end of the current tier. All pass verdicts on a specific + // tier must have the same pass_precedence so tier boundaries stay unambiguous. + if (tier_last_precedence == 0 || + (pass_precedence_ != 0 && tier_last_precedence != pass_precedence_)) { + throw EnvoyException(fmt::format("PortNetworkPolicy: Inconsistent pass precedence {} != {}", + tier_last_precedence, pass_precedence_)); + } + pass_precedence_ = tier_last_precedence; + } + + std::vector empty_ranges_; + std::vector wildcard_pass_rules_; + std::vector::iterator wildcard_it_; + ShadowedRemotes shadowing_; + std::vector pass_rules_; + std::vector pass_rules_tier_index_; + uint32_t pass_precedence_{0}; + std::vector tier_pass_rules_; + bool tier_wildcard_pass_{false}; +}; + class PortNetworkPolicy : public Logger::Loggable { public: PortNetworkPolicy(const NetworkPolicyMapImpl& parent, @@ -1180,6 +1669,7 @@ class PortNetworkPolicy : public Logger::Loggable { RELEASE_ASSERT(it != rules_.end(), "first overlapping entry not found"); } // Add rules to all the overlapping entries + bool shared_resource = rule_range.first == 0; // wildcard port rules are shared bool singular = rule_range.first == rule_range.second; for (; it != rules_.end() && rangesOverlap(it->first, rule_range); it++) { auto range = it->first; @@ -1190,10 +1680,14 @@ class PortNetworkPolicy : public Logger::Loggable { // Exact port rules go to the front of the list. // This gives precedence for trivial range rules for proxylib parser // and TLS context selection. - rules.prepend(parent, rule.rules()); + // prepend() inserts each rule at begin() while iterating forward, + // so the relative order of rules from this batch is reversed. This + // is harmless: equal-precedence rules are evaluated as alternatives + // (stable sort only affects presentation/debug ordering). + rules.prepend(parent, rule.rules(), shared_resource); } else { // Rules with a non-trivial range go to the back of the list - rules.append(parent, rule.rules()); + rules.append(parent, rule.rules(), shared_resource); } } } else { @@ -1201,9 +1695,46 @@ class PortNetworkPolicy : public Logger::Loggable { } } - // sort rules into descending precedence + // Apply wildcard port rules to all ranges + const PortNetworkPolicyRules* wildcard_rules = nullptr; + for (auto& [port_range, rules] : rules_) { + if (port_range.first == 0) { + wildcard_rules = &rules; + continue; + } + if (!wildcard_rules) { + break; + } + rules.appendNonPassRules(wildcard_rules->rules_); + } + + bool have_passes = false; + + // sort rules on each non-overlapping port range into descending precedence + // port ranges themselves remain in the sorted order. + // This way we can efficiently find the list of rules applicable to any given port, + // and then process those rules in the order of decreasing precedence. for (auto& pair : rules_) { pair.second.sort(); + if (pair.second.has_pass_rules_) { + have_passes = true; + } + } + + // Apply pass verdicts, if any. + if (have_passes) { + Passes passes; + + // This loop always iterates the wildcard port first, if rules for it exist. + for (auto& [port_range, rules] : rules_) { + passes.apply(port_range, rules); + } + + // Delete port ranges that only contained pass rules. + // Otherwise the policy would always accept. + for (auto port_range : passes.emptyRanges()) { + rules_.erase(port_range); + } } } @@ -1237,7 +1768,7 @@ class PolicyInstanceImpl : public PolicyInstance { ingress_(parent, policy_proto_.ingress_per_port_policies()), egress_(parent, policy_proto_.egress_per_port_policies()) {} - bool allowed(bool ingress, uint32_t proxy_id, uint32_t remote_id, uint16_t port, + bool allowed(bool ingress, uint16_t proxy_id, uint32_t remote_id, uint16_t port, Envoy::Http::RequestHeaderMap& headers, Cilium::AccessLog::Entry& log_entry) const override { const auto port_policy = findPortPolicy(ingress, port); @@ -1247,7 +1778,7 @@ class PolicyInstanceImpl : public PolicyInstance { return port_policy.allowed(proxy_id, remote_id, headers, log_entry); } - bool allowed(bool ingress, uint32_t proxy_id, uint32_t remote_id, absl::string_view sni, + bool allowed(bool ingress, uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, uint16_t port) const override { const auto port_policy = findPortPolicy(ingress, port); return port_policy.allowed(proxy_id, remote_id, sni); @@ -1257,7 +1788,7 @@ class PolicyInstanceImpl : public PolicyInstance { return ingress ? ingress_.findPortPolicy(port) : egress_.findPortPolicy(port); } - bool useProxylib(bool ingress, uint32_t proxy_id, uint32_t remote_id, uint16_t port, + bool useProxylib(bool ingress, uint16_t proxy_id, uint32_t remote_id, uint16_t port, std::string& l7_proto) const override { const auto port_policy = findPortPolicy(ingress, port); return port_policy.useProxylib(proxy_id, remote_id, l7_proto); @@ -1575,12 +2106,12 @@ class AllowAllEgressPolicyInstanceImpl : public PolicyInstance { empty_map_.emplace(std::make_pair(uint16_t(1), uint16_t(1)), PortNetworkPolicyRules{}); } - bool allowed(bool ingress, uint32_t, uint32_t, uint16_t, Envoy::Http::RequestHeaderMap&, + bool allowed(bool ingress, uint16_t, uint32_t, uint16_t, Envoy::Http::RequestHeaderMap&, Cilium::AccessLog::Entry&) const override { return ingress ? false : true; } - bool allowed(bool ingress, uint32_t, uint32_t, absl::string_view, uint16_t) const override { + bool allowed(bool ingress, uint16_t, uint32_t, absl::string_view, uint16_t) const override { return ingress ? false : true; } @@ -1588,7 +2119,7 @@ class AllowAllEgressPolicyInstanceImpl : public PolicyInstance { return ingress ? PortPolicy(empty_map_, 0) : PortPolicy(empty_map_, 1); } - bool useProxylib(bool, uint32_t, uint32_t, uint16_t, std::string&) const override { + bool useProxylib(bool, uint16_t, uint32_t, uint16_t, std::string&) const override { return false; } @@ -1619,12 +2150,12 @@ class DenyAllPolicyInstanceImpl : public PolicyInstance { public: DenyAllPolicyInstanceImpl() = default; - bool allowed(bool, uint32_t, uint32_t, uint16_t, Envoy::Http::RequestHeaderMap&, + bool allowed(bool, uint16_t, uint32_t, uint16_t, Envoy::Http::RequestHeaderMap&, Cilium::AccessLog::Entry&) const override { return false; } - bool allowed(bool, uint32_t, uint32_t, absl::string_view, uint16_t) const override { + bool allowed(bool, uint16_t, uint32_t, absl::string_view, uint16_t) const override { return false; } @@ -1632,7 +2163,7 @@ class DenyAllPolicyInstanceImpl : public PolicyInstance { return PortPolicy(empty_map_, 0); } - bool useProxylib(bool, uint32_t, uint32_t, uint16_t, std::string&) const override { + bool useProxylib(bool, uint16_t, uint32_t, uint16_t, std::string&) const override { return false; } diff --git a/cilium/network_policy.h b/cilium/network_policy.h index ec07116f4..128a15170 100644 --- a/cilium/network_policy.h +++ b/cilium/network_policy.h @@ -75,6 +75,13 @@ class PortNetworkPolicyRules; // use of named ports and numbered ports in Cilium Network Policy at the same time). using PolicyMap = absl::btree_map; +// Supported message types +using RuleVerdict = enum { + None = 0, + Allow = 1, + Deny = 2, +}; + // PortPolicy holds a reference to a set of rules in a policy map that apply to the given port. // Methods then iterate through the set to determine if policy allows or denies. This is needed to // support multiple rules on the same port, like when named ports are used, or when deny policies @@ -93,36 +100,33 @@ class PortPolicy : public Logger::Loggable { // useProxylib returns true if a proxylib parser should be used. // 'l7_proto' is set to the parser name in that case. - bool useProxylib(uint32_t proxy_id, uint32_t remote_id, std::string& l7_proto) const; + bool useProxylib(uint16_t proxy_id, uint32_t remote_id, std::string& l7_proto) const; // HTTP-layer policy check. 'headers' and 'log_entry' may be manipulated by the policy. - bool allowed(uint32_t proxy_id, uint32_t remote_id, Envoy::Http::RequestHeaderMap& headers, + bool allowed(uint16_t proxy_id, uint32_t remote_id, Envoy::Http::RequestHeaderMap& headers, Cilium::AccessLog::Entry& log_entry) const; // Network-layer policy check - bool allowed(uint32_t proxy_id, uint32_t remote_id, absl::string_view sni) const; + bool allowed(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni) const; // Envoy filter metadata policy check - bool allowed(uint32_t proxy_id, uint32_t remote_id, + bool allowed(uint16_t proxy_id, uint32_t remote_id, const envoy::config::core::v3::Metadata& metadata) const; // getServerTlsContext returns the server TLS context, if any. If a non-null pointer is returned, // then also the config pointer '*config' is set. // If '*config' is nullptr and 'raw_socket_allowed' is 'true' on return then the policy // allows the connection without TLS and a raw socket should be used. - Ssl::ContextSharedPtr getServerTlsContext(uint32_t proxy_id, uint32_t remote_id, + Ssl::ContextSharedPtr getServerTlsContext(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, - const Ssl::ContextConfig** config, + const Ssl::ContextConfig*& config, bool& raw_socket_allowed) const; // getClientTlsContext returns the client TLS context, if any. If a non-null pointer is returned, // then also the config pointer '*config' is set. // If '*config' is nullptr and 'raw_socket_allowed' is 'true' on return then the policy // allows the connection without TLS and a raw socket should be used. - Ssl::ContextSharedPtr getClientTlsContext(uint32_t proxy_id, uint32_t remote_id, + Ssl::ContextSharedPtr getClientTlsContext(uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, - const Ssl::ContextConfig** config, + const Ssl::ContextConfig*& config, bool& raw_socket_allowed) const; private: - bool forRange(std::function allowed) const; - bool forFirstRange(std::function f) const; - const PolicyMap& map_; // using raw pointers by design: // - pointer to distinguish between no rules and empty rules @@ -132,7 +136,6 @@ class PortPolicy : public Logger::Loggable { // before the old rules are deleted; worker thread drop references to policy rules before // returning to the event loop, so after the posted lambda executes it is safe to delete the old // rules. - const PortNetworkPolicyRules* wildcard_rules_; const PortNetworkPolicyRules* port_rules_; const bool has_http_rules_; }; @@ -158,18 +161,18 @@ class PolicyInstance { } }; - virtual bool allowed(bool ingress, uint32_t proxy_id, uint32_t remote_id, uint16_t port, + virtual bool allowed(bool ingress, uint16_t proxy_id, uint32_t remote_id, uint16_t port, Envoy::Http::RequestHeaderMap& headers, Cilium::AccessLog::Entry& log_entry) const PURE; - virtual bool allowed(bool ingress, uint32_t proxy_id, uint32_t remote_id, absl::string_view sni, + virtual bool allowed(bool ingress, uint16_t proxy_id, uint32_t remote_id, absl::string_view sni, uint16_t port) const PURE; virtual const PortPolicy findPortPolicy(bool ingress, uint16_t port) const PURE; // Returns true if the policy specifies l7 protocol for the connection, and // returns the l7 protocol string in 'l7_proto' - virtual bool useProxylib(bool ingress, uint32_t proxy_id, uint32_t remote_id, uint16_t port, + virtual bool useProxylib(bool ingress, uint16_t proxy_id, uint32_t remote_id, uint16_t port, std::string& l7_proto) const PURE; virtual const std::string& conntrackName() const PURE; @@ -451,7 +454,7 @@ class SniPattern : public Logger::Loggable { bool isExplicitFullMatch() const { return !match_name_.empty(); } std::string match_name_; - std::unique_ptr matcher_; + std::shared_ptr matcher_; }; } // namespace Cilium diff --git a/cilium/secret_watcher.h b/cilium/secret_watcher.h index 703bd7900..c532d0ade 100644 --- a/cilium/secret_watcher.h +++ b/cilium/secret_watcher.h @@ -83,7 +83,7 @@ class DownstreamTLSContext : protected TLSContext { std::vector server_names_; Ssl::ServerContextSharedPtr server_context_ ABSL_GUARDED_BY(ssl_context_mutex_){}; }; -using DownstreamTLSContextPtr = std::unique_ptr; +using DownstreamTLSContextSharedPtr = std::shared_ptr; class UpstreamTLSContext : protected TLSContext { public: @@ -100,7 +100,7 @@ class UpstreamTLSContext : protected TLSContext { Ssl::ClientContextConfigPtr client_config_; Ssl::ClientContextSharedPtr client_context_ ABSL_GUARDED_BY(ssl_context_mutex_){}; }; -using UpstreamTLSContextPtr = std::unique_ptr; +using UpstreamTLSContextSharedPtr = std::shared_ptr; } // namespace Cilium } // namespace Envoy diff --git a/cilium/tls_wrapper.cc b/cilium/tls_wrapper.cc index 6a2e453ab..e6a9907b8 100644 --- a/cilium/tls_wrapper.cc +++ b/cilium/tls_wrapper.cc @@ -131,9 +131,9 @@ class SslSocketWrapper : public Network::TransportSocket, Logger::Loggableproxy_id_; Envoy::Ssl::ContextSharedPtr ctx = - is_client ? port_policy.getClientTlsContext(proxy_id, remote_id, sni, &config, + is_client ? port_policy.getClientTlsContext(proxy_id, remote_id, sni, config, raw_socket_allowed) - : port_policy.getServerTlsContext(proxy_id, remote_id, sni, &config, + : port_policy.getServerTlsContext(proxy_id, remote_id, sni, config, raw_socket_allowed); if (ctx) { // create the underlying SslSocket diff --git a/cilium/websocket.cc b/cilium/websocket.cc index 65c3718c1..c84a2b279 100644 --- a/cilium/websocket.cc +++ b/cilium/websocket.cc @@ -134,7 +134,7 @@ Network::FilterStatus Instance::onNewConnection() { std::string pod_ip; bool is_ingress; uint32_t identity, destination_identity; - uint32_t proxy_id; + uint16_t proxy_id; auto& conn = callbacks_->connection(); diff --git a/go.mod b/go.mod index 88e55c908..7cc7f9187 100644 --- a/go.mod +++ b/go.mod @@ -40,5 +40,4 @@ require ( tool ( github.com/envoyproxy/protoc-gen-validate github.com/golang/protobuf/protoc-gen-go - google.golang.org/protobuf ) diff --git a/go/cilium/api/bpf_metadata.pb.go b/go/cilium/api/bpf_metadata.pb.go index eeecbfbd4..3b860b778 100644 --- a/go/cilium/api/bpf_metadata.pb.go +++ b/go/cilium/api/bpf_metadata.pb.go @@ -7,6 +7,7 @@ package cilium import ( + _ "github.com/envoyproxy/protoc-gen-validate/validate" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" durationpb "google.golang.org/protobuf/types/known/durationpb" @@ -228,7 +229,7 @@ var File_cilium_api_bpf_metadata_proto protoreflect.FileDescriptor const file_cilium_api_bpf_metadata_proto_rawDesc = "" + "\n" + - "\x1dcilium/api/bpf_metadata.proto\x12\x06cilium\x1a\x1egoogle/protobuf/duration.proto\"\x89\x06\n" + + "\x1dcilium/api/bpf_metadata.proto\x12\x06cilium\x1a\x1egoogle/protobuf/duration.proto\x1a\x17validate/validate.proto\"\x94\x06\n" + "\vBpfMetadata\x12\x19\n" + "\bbpf_root\x18\x01 \x01(\tR\abpfRoot\x12\x1d\n" + "\n" + @@ -237,8 +238,8 @@ const file_cilium_api_bpf_metadata_proto_rawDesc = "" + "\ais_l7lb\x18\x04 \x01(\bR\x06isL7lb\x12.\n" + "\x13ipv4_source_address\x18\x05 \x01(\tR\x11ipv4SourceAddress\x12.\n" + "\x13ipv6_source_address\x18\x06 \x01(\tR\x11ipv6SourceAddress\x123\n" + - "\x16enforce_policy_on_l7lb\x18\a \x01(\bR\x13enforcePolicyOnL7lb\x12\x19\n" + - "\bproxy_id\x18\b \x01(\rR\aproxyId\x12X\n" + + "\x16enforce_policy_on_l7lb\x18\a \x01(\bR\x13enforcePolicyOnL7lb\x12$\n" + + "\bproxy_id\x18\b \x01(\rB\t\xfaB\x06*\x04\x18\xff\xff\x03R\aproxyId\x12X\n" + "\x1bpolicy_update_warning_limit\x18\t \x01(\v2\x19.google.protobuf.DurationR\x18policyUpdateWarningLimit\x12(\n" + "\x10l7lb_policy_name\x18\n" + " \x01(\tR\x0el7lbPolicyName\x12G\n" + diff --git a/go/cilium/api/bpf_metadata.pb.validate.go b/go/cilium/api/bpf_metadata.pb.validate.go index b9c4ae84e..8180998d2 100644 --- a/go/cilium/api/bpf_metadata.pb.validate.go +++ b/go/cilium/api/bpf_metadata.pb.validate.go @@ -71,7 +71,16 @@ func (m *BpfMetadata) validate(all bool) error { // no validation rules for EnforcePolicyOnL7Lb - // no validation rules for ProxyId + if m.GetProxyId() > 65535 { + err := BpfMetadataValidationError{ + field: "ProxyId", + reason: "value must be less than or equal to 65535", + } + if !all { + return err + } + errors = append(errors, err) + } if all { switch v := interface{}(m.GetPolicyUpdateWarningLimit()).(type) { diff --git a/go/cilium/api/npds.pb.go b/go/cilium/api/npds.pb.go index 4ca1ca12a..7d8094e97 100644 --- a/go/cilium/api/npds.pb.go +++ b/go/cilium/api/npds.pb.go @@ -430,8 +430,13 @@ type PortNetworkPolicyRule struct { // precedence, even over deny rules of lower precedence level. // The lowest precedence (zero) is used when not specified. Precedence uint32 `protobuf:"varint,10,opt,name=precedence,proto3" json:"precedence,omitempty"` - // Traffic on this port is denied for all `remote_policies` if true - Deny bool `protobuf:"varint,8,opt,name=deny,proto3" json:"deny,omitempty"` + // Optional verdict, mutually exclusive. If missing then the verdict is an allow. + // + // Types that are valid to be assigned to Verdict: + // + // *PortNetworkPolicyRule_PassPrecedence + // *PortNetworkPolicyRule_Deny + Verdict isPortNetworkPolicyRule_Verdict `protobuf_oneof:"verdict"` // ProxyID is non-zero if the rule was an allow rule with an explicit listener reference. // The given value corresponds to the 'proxy_id' value in the BpfMetadata listener filter // configuration. @@ -519,9 +524,27 @@ func (x *PortNetworkPolicyRule) GetPrecedence() uint32 { return 0 } +func (x *PortNetworkPolicyRule) GetVerdict() isPortNetworkPolicyRule_Verdict { + if x != nil { + return x.Verdict + } + return nil +} + +func (x *PortNetworkPolicyRule) GetPassPrecedence() uint32 { + if x != nil { + if x, ok := x.Verdict.(*PortNetworkPolicyRule_PassPrecedence); ok { + return x.PassPrecedence + } + } + return 0 +} + func (x *PortNetworkPolicyRule) GetDeny() bool { if x != nil { - return x.Deny + if x, ok := x.Verdict.(*PortNetworkPolicyRule_Deny); ok { + return x.Deny + } } return false } @@ -609,6 +632,25 @@ func (x *PortNetworkPolicyRule) GetL7Rules() *L7NetworkPolicyRules { return nil } +type isPortNetworkPolicyRule_Verdict interface { + isPortNetworkPolicyRule_Verdict() +} + +type PortNetworkPolicyRule_PassPrecedence struct { + // Precedence after which policy evaluation should be continued at for the selected + // remotes_policies. + PassPrecedence uint32 `protobuf:"varint,1,opt,name=pass_precedence,json=passPrecedence,proto3,oneof"` +} + +type PortNetworkPolicyRule_Deny struct { + // Traffic on this port is denied for all `remote_policies` if true + Deny bool `protobuf:"varint,8,opt,name=deny,proto3,oneof"` +} + +func (*PortNetworkPolicyRule_PassPrecedence) isPortNetworkPolicyRule_Verdict() {} + +func (*PortNetworkPolicyRule_Deny) isPortNetworkPolicyRule_Verdict() {} + type isPortNetworkPolicyRule_L7 interface { isPortNetworkPolicyRule_L7() } @@ -1177,14 +1219,15 @@ const file_cilium_api_npds_proto_rawDesc = "" + "\fserver_names\x18\x04 \x03(\tR\vserverNames\x12A\n" + "\x1dvalidation_context_sds_secret\x18\x05 \x01(\tR\x1avalidationContextSdsSecret\x12$\n" + "\x0etls_sds_secret\x18\x06 \x01(\tR\ftlsSdsSecret\x12%\n" + - "\x0ealpn_protocols\x18\a \x03(\tR\ralpnProtocols\"\xbe\x05\n" + + "\x0ealpn_protocols\x18\a \x03(\tR\ralpnProtocols\"\xfb\x05\n" + "\x15PortNetworkPolicyRule\x12\x1e\n" + "\n" + "precedence\x18\n" + " \x01(\rR\n" + - "precedence\x12\x12\n" + - "\x04deny\x18\b \x01(\bR\x04deny\x12\x19\n" + - "\bproxy_id\x18\t \x01(\rR\aproxyId\x12\x12\n" + + "precedence\x12)\n" + + "\x0fpass_precedence\x18\x01 \x01(\rH\x00R\x0epassPrecedence\x12\x14\n" + + "\x04deny\x18\b \x01(\bH\x00R\x04deny\x12$\n" + + "\bproxy_id\x18\t \x01(\rB\t\xfaB\x06*\x04\x18\xff\xff\x03R\aproxyId\x12\x12\n" + "\x04name\x18\x05 \x01(\tR\x04name\x12'\n" + "\x0fremote_policies\x18\a \x03(\rR\x0eremotePolicies\x12H\n" + "\x16downstream_tls_context\x18\x03 \x01(\v2\x12.cilium.TLSContextR\x14downstreamTlsContext\x12D\n" + @@ -1192,11 +1235,12 @@ const file_cilium_api_npds_proto_rawDesc = "" + "\fserver_names\x18\x06 \x03(\tB~\xfaB{\x92\x01x\"vrt2r^(([*]{1,2}|[*]?[-a-zA-Z0-9_]+([*][-a-zA-Z0-9_]+)*[*]?)[.])*([*]{1,2}|[*]?[-a-zA-Z0-9_]+([*][-a-zA-Z0-9_]+)*[*]?)$R\vserverNames\x12\x19\n" + "\bl7_proto\x18\x02 \x01(\tR\al7Proto\x12?\n" + "\n" + - "http_rules\x18d \x01(\v2\x1e.cilium.HttpNetworkPolicyRulesH\x00R\thttpRules\x12B\n" + - "\vkafka_rules\x18e \x01(\v2\x1f.cilium.KafkaNetworkPolicyRulesH\x00R\n" + + "http_rules\x18d \x01(\v2\x1e.cilium.HttpNetworkPolicyRulesH\x01R\thttpRules\x12B\n" + + "\vkafka_rules\x18e \x01(\v2\x1f.cilium.KafkaNetworkPolicyRulesH\x01R\n" + "kafkaRules\x129\n" + - "\bl7_rules\x18f \x01(\v2\x1c.cilium.L7NetworkPolicyRulesH\x00R\al7RulesB\x04\n" + - "\x02l7J\x04\b\x01\x10\x02\"`\n" + + "\bl7_rules\x18f \x01(\v2\x1c.cilium.L7NetworkPolicyRulesH\x01R\al7RulesB\t\n" + + "\averdictB\x04\n" + + "\x02l7\"`\n" + "\x16HttpNetworkPolicyRules\x12F\n" + "\n" + "http_rules\x18\x01 \x03(\v2\x1d.cilium.HttpNetworkPolicyRuleB\b\xfaB\x05\x92\x01\x02\b\x01R\thttpRules\"\xd2\x03\n" + @@ -1319,6 +1363,8 @@ func file_cilium_api_npds_proto_init() { return } file_cilium_api_npds_proto_msgTypes[3].OneofWrappers = []any{ + (*PortNetworkPolicyRule_PassPrecedence)(nil), + (*PortNetworkPolicyRule_Deny)(nil), (*PortNetworkPolicyRule_HttpRules)(nil), (*PortNetworkPolicyRule_KafkaRules)(nil), (*PortNetworkPolicyRule_L7Rules)(nil), diff --git a/go/cilium/api/npds.pb.validate.go b/go/cilium/api/npds.pb.validate.go index 88f7dd02c..e1b5b1a69 100644 --- a/go/cilium/api/npds.pb.validate.go +++ b/go/cilium/api/npds.pb.validate.go @@ -531,9 +531,16 @@ func (m *PortNetworkPolicyRule) validate(all bool) error { // no validation rules for Precedence - // no validation rules for Deny - - // no validation rules for ProxyId + if m.GetProxyId() > 65535 { + err := PortNetworkPolicyRuleValidationError{ + field: "ProxyId", + reason: "value must be less than or equal to 65535", + } + if !all { + return err + } + errors = append(errors, err) + } // no validation rules for Name @@ -613,6 +620,34 @@ func (m *PortNetworkPolicyRule) validate(all bool) error { // no validation rules for L7Proto + switch v := m.Verdict.(type) { + case *PortNetworkPolicyRule_PassPrecedence: + if v == nil { + err := PortNetworkPolicyRuleValidationError{ + field: "Verdict", + reason: "oneof value cannot be a typed-nil", + } + if !all { + return err + } + errors = append(errors, err) + } + // no validation rules for PassPrecedence + case *PortNetworkPolicyRule_Deny: + if v == nil { + err := PortNetworkPolicyRuleValidationError{ + field: "Verdict", + reason: "oneof value cannot be a typed-nil", + } + if !all { + return err + } + errors = append(errors, err) + } + // no validation rules for Deny + default: + _ = v // ensures v is used + } switch v := m.L7.(type) { case *PortNetworkPolicyRule_HttpRules: if v == nil { diff --git a/tests/cilium_network_policy_test.cc b/tests/cilium_network_policy_test.cc index 72bc22d6e..91e4ede44 100644 --- a/tests/cilium_network_policy_test.cc +++ b/tests/cilium_network_policy_test.cc @@ -140,10 +140,10 @@ class CiliumNetworkPolicyTest : public ::testing::Test { tls_socket_required = false; raw_socket_allowed = false; Envoy::Ssl::ContextSharedPtr ctx = - !ingress ? port_policy.getClientTlsContext(proxy_id_, remote_id, sni, &config, - raw_socket_allowed) - : port_policy.getServerTlsContext(proxy_id_, remote_id, sni, &config, - raw_socket_allowed); + !ingress + ? port_policy.getClientTlsContext(proxy_id_, remote_id, sni, config, raw_socket_allowed) + : port_policy.getServerTlsContext(proxy_id_, remote_id, sni, config, + raw_socket_allowed); // separate policy lookup for validation bool allowed = policy.allowed(ingress, proxy_id_, remote_id, sni, port); @@ -217,7 +217,7 @@ class CiliumNetworkPolicyTest : public ::testing::Test { NiceMock secret_manager_; std::shared_ptr policy_map_; NiceMock store_; - uint32_t proxy_id_ = 42; + uint16_t proxy_id_ = 42; }; TEST_F(CiliumNetworkPolicyTest, UpdatesRejectedStatName) { @@ -806,7 +806,7 @@ TEST_F(CiliumNetworkPolicyTest, HttpPolicyUpdate) { EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/publicz"}})); // 3rd update with Ingress deny rules - EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "3" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy endpoint_ips: @@ -837,7 +837,7 @@ TEST_F(CiliumNetworkPolicyTest, HttpPolicyUpdate) { google_re2: {} regex: '.*public$' )EOF")); - EXPECT_EQ(version, "2"); + EXPECT_EQ(version, "3"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); expected = R"EOF(ingress: @@ -889,6 +889,280 @@ TEST_F(CiliumNetworkPolicyTest, HttpPolicyUpdate) { EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/publicz"}})); // 4th update with matching proxy_id in policy + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "4" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - remote_policies: [ 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' + - port: 80 + end_port: 10000 + rules: + - proxy_id: 42 + egress_per_port_policies: + - port: 80 + rules: + - remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + safe_regex_match: + google_re2: {} + regex: '.*public$' +)EOF")); + EXPECT_EQ(version, "4"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + expected = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + proxy_id: 42 + [81-10000]: + - rules: + - remotes: [] + proxy_id: 42 +egress: + rules: + [80-80]: + - rules: + - remotes: [43,44] + http_rules: + - headers: + - name: ":path" + regex: +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected)); + + // Allowed remote ID, port, & path: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Matching proxy ID: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Matching proxy ID: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Matching proxy ID: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // Port out of range: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 79, {{":path", "/allowed"}})); + // Port out of range: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 10001, {{":path", "/notallowed"}})); + + // 5th update with non-matching proxy_id in policy + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "5" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - remote_policies: [ 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' + - port: 80 + end_port: 10000 + rules: + - proxy_id: 99 + egress_per_port_policies: + - port: 80 + rules: + - remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + safe_regex_match: + google_re2: {} + regex: '.*public$' +)EOF")); + EXPECT_EQ(version, "5"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + expected = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + proxy_id: 99 + [81-10000]: + - rules: + - remotes: [] + proxy_id: 99 +egress: + rules: + [80-80]: + - rules: + - remotes: [43,44] + http_rules: + - headers: + - name: ":path" + regex: +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected)); + + // Allowed remote ID, port, & path: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Non-matching proxy ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Non-matching proxy ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Non-matching proxy ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // Port out of range: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 79, {{":path", "/allowed"}})); + // Port out of range: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 10001, {{":path", "/notallowed"}})); +} + +TEST_F(CiliumNetworkPolicyTest, Precedence) { + std::string version; + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "0" +)EOF")); + EXPECT_EQ(version, "0"); + EXPECT_FALSE(policy_map_->exists("10.1.2.3")); + // No policy for the pod + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + + // pass_precedence must be lower than precedence + EXPECT_THROW_WITH_MESSAGE( + updateFromYaml(R"EOF(version_info: "1" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 2000 + remote_policies: [ 43 ] +)EOF"), + EnvoyException, + "PortNetworkPolicyRule: pass_precedence 2000 must be lower than precedence 1000"); + + // deny and pass_precedence are mutually exclusive + EXPECT_THROW_WITH_REGEX(updateFromYaml(R"EOF(version_info: "1" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - deny: true + precedence: 1000 + pass_precedence: 100 + remote_policies: [ 43 ] +)EOF"), + EnvoyException, + "Unable to parse JSON as proto.*INVALID_ARGUMENT:.*oneof"); + + // pass rules on the same tier must use a consistent pass_precedence. + // "pass_precedence" defines the last (lowest) precedence on the "tier". + // any pass rules with precedence higher than the previous pass_precedence + // must have the same pass_precedence. + EXPECT_THROW_WITH_MESSAGE(updateFromYaml(R"EOF(version_info: "1" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 100 + remote_policies: [ 43 ] + - precedence: 900 + pass_precedence: 200 + remote_policies: [ 44 ] +)EOF"), + EnvoyException, + "PortNetworkPolicy: Inconsistent pass precedence 200 != 100"); + + // + // 1st update: Default allow rule combining with an HTTP allow rule + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "1" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' + - port: 80 +)EOF")); + EXPECT_EQ(version, "1"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected1 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [] + name: "default allow rule" + - remotes: [] + http_rules: + - headers: + - name: ":path" + value: "/allowed" +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected1)); + + // All remotes allowed on port 80 + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/also-allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 2nd update: Default allow rule combining with a pass rule. + // EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy @@ -896,151 +1170,938 @@ TEST_F(CiliumNetworkPolicyTest, HttpPolicyUpdate) { - "10.1.2.3" endpoint_id: 42 ingress_per_port_policies: + - port: 80 + end_port: 81 + rules: + - precedence: 10 + pass_precedence: 1 + - port: 80 +)EOF")); + EXPECT_EQ(version, "2"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected2 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [] + name: "default allow rule" + precedence: 9 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected2)); + + // All remotes allowed on port 80 + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/also-allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 3rd update: Default allow rule combining with a pass rule on wildcard port. + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "3" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 10 + pass_precedence: 1 + - port: 80 +)EOF")); + EXPECT_EQ(version, "3"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected3 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [] + name: "default allow rule" + precedence: 9 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected3)); + + // All remotes allowed on port 80 + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/also-allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 4th update: higher precedence deny + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "4" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + deny: true + - precedence: 100 + remote_policies: [ 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "4"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected4 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [] + deny: true + precedence: 1000 + - remotes: [43] + precedence: 100 + http_rules: + - headers: + - name: ":path" + value: "/allowed" +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected4)); + + // Denied remote ID, port, & path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Wrong remote ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Wrong path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 5th update: higher precedence deny on wildcard port + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "5" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 1000 + deny: true + - port: 80 + rules: + - precedence: 100 + remote_policies: [ 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "5"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected5 = R"EOF(ingress: + rules: + [0-0]: + - rules: + - remotes: [] + deny: true + precedence: 1000 + [80-80]: + - rules: + - remotes: [] + deny: true + precedence: 1000 + - remotes: [43] + precedence: 100 + http_rules: + - headers: + - name: ":path" + value: "/allowed" +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected5)); + + // Denied remote ID, port, & path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Wrong remote ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Wrong path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 6th update: pass for '43' + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "6" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 501 + remote_policies: [ 43 ] + - precedence: 900 + deny: true + - precedence: 500 + remote_policies: [ 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "6"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected6 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + deny: true + precedence: 900 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected6)); + + // Allowed remote ID, port, & path: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Wrong remote ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Wrong path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 7th update: pass with partial overlap + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "7" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 501 + remote_policies: [ 43 ] + - precedence: 900 + deny: true + - precedence: 500 + remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "7"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected7 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + deny: true + precedence: 900 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected7)); + + // Allowed remote ID, port, & path: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Denied remote ID, port, & path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allowed"}})); + // Wrong remote ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Wrong path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 8th update: wildcard pass + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "8" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 501 + - precedence: 900 + deny: true + - precedence: 500 + remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "8"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected8 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43,44] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allowed" +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected8)); + + // Allowed remote ID, port, & path: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Allowed remote ID, port, & path: + EXPECT_TRUE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allowed"}})); + // Wrong remote ID: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); + // Wrong port: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); + // Wrong path: + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 9th update: split wildcard lower-precedence rule due to pass + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "9" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 501 + remote_policies: [ 43 ] + - precedence: 900 + deny: true + - precedence: 500 + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "9"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected9 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + deny: true + precedence: 900 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected9)); + + // Remote 43 is promoted above deny by pass. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Other remotes are still denied by the deny rule. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allowed"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/allowed"}})); + + // + // 10th update: wildcard-port pass inherited by specific port rules + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "10" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 1000 + pass_precedence: 501 + remote_policies: [ 43 ] + - port: 80 + rules: + - precedence: 900 + deny: true + - precedence: 500 + remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "10"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected10 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + deny: true + precedence: 900 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected10)); + + // Pass from wildcard port should promote remote 43 above deny on port 80. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + // Remote 44 is denied due to only 43 being promoted. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allowed"}})); + // Unspecified remotes remain denied. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/allowed"}})); + + // No egress is allowed: + EXPECT_FALSE(egressAllowed("10.1.2.3", 43, 80, {{":path", "/public"}})); + + // + // 11th update: wildcard-port and specific-port pass rules at equal precedence + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "11" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 1000 + pass_precedence: 501 + remote_policies: [ 44 ] + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 501 + remote_policies: [ 43 ] + - precedence: 900 + deny: true + - precedence: 500 + remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allowed' +)EOF")); + EXPECT_EQ(version, "11"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected11 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43,44] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allowed" + - remotes: [] + deny: true + precedence: 900 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected11)); + + // Both IDs are passed to the lower allow despite the intermediate deny. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allowed"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/allowed"}})); + + // + // 12th update: non-pass rule shadowing inside a pass tier + // + // The pass rule is required to enable tier processing, but it targets only + // remote 45 so the tier is not wildcard-pass and does not pre-shadow 43/44. + // Within this tier: + // - A higher-precedence deny for remote 44 establishes a final verdict for 44. + // - A lower-precedence allow for [43,44] must have 44 removed due to shadowing. + // - A second allow at the same precedence for [43] must keep 43, confirming + // no same-precedence identity shadowing between allow rules. + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "12" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 701 + remote_policies: [ 45 ] + - precedence: 900 + deny: true + remote_policies: [ 44 ] + - precedence: 800 + remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allow-a' + - precedence: 800 + remote_policies: [ 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allow-b' + - precedence: 700 + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/allow-c' +)EOF")); + EXPECT_EQ(version, "12"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected12 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [45] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/allow-c" + - remotes: [44] + deny: true + precedence: 900 + - remotes: [43] + precedence: 800 + http_rules: + - headers: + - name: ":path" + value: "/allow-b" + - remotes: [43] + precedence: 800 + http_rules: + - headers: + - name: ":path" + value: "/allow-a" + - remotes: [] + precedence: 700 + http_rules: + - headers: + - name: ":path" + value: "/allow-c" +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected12)); + + // Remote 43 is not passed, but both same-precedence allow rules remain effective. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allow-a"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allow-b"}})); + // Remote 44 is denied by the higher-precedence deny and removed from allow-a. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allow-a"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allow-b"}})); + // Pass remote 45 does not match /allow-a because only /allow-c is promoted for it. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/allow-a"}})); + // Wildcard allow at precedence 700 is promoted to precedence 999 only for pass remote 45. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/allow-c"}})); + // Non-pass remotes not already denied at higher precedence still match the + // original wildcard rule at precedence 700. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allow-c"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/allow-c"}})); + + // + // 13th update: inherited wildcard current-tier pass fully shadowed + // + // Wildcard port has a current-tier pass for remote 43, and specific port has + // a higher precedence pass for the same remote on the same tier. When the + // wildcard pass is inherited, it is fully shadowed and skipped, as evidenced by the + // precedence of the passed-to rule for remote 43, which is 999 rather than 899. + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "13" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 900 + pass_precedence: 701 + remote_policies: [ 43 ] + - port: 80 + rules: + - precedence: 1000 + pass_precedence: 701 + remote_policies: [ 43 ] + - precedence: 800 + deny: true + - precedence: 700 + remote_policies: [ 43, 44 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/shadowed-inherited-pass' +)EOF")); + EXPECT_EQ(version, "13"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected13 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [43] + precedence: 999 + http_rules: + - headers: + - name: ":path" + value: "/shadowed-inherited-pass" + - remotes: [] + deny: true + precedence: 800 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected13)); + + // Remote 43 is promoted above deny due to the specific-port pass. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/shadowed-inherited-pass"}})); + // Remote 44 remains denied by the intermediate deny. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/shadowed-inherited-pass"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/shadowed-inherited-pass"}})); + + // + // 14th update: multiple wildcard pass tiers inherited by a specific port + // + // Wildcard port contributes two pass tiers: + // Tier boundaries are inclusive. + // - tier 1 pass (1300/1000) for remote 41: tier boundaries [1300..1000] + // - tier 2 pass (900/700) for remote 42: tier boundaries [999..700] + // For port 80: + // - deny at 850 is within tier 2, so it is promoted by tier 1 pass for remote 41 to 1150 + // - allow [41,42,43] at 600 is split and promoted by both tiers: + // - 41 to tier 1 precedence 900 + // - 42 to tier 2 precedence 800 + // - 43 remains at tier 3 at precedence 600 + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "14" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 1300 + pass_precedence: 1000 + remote_policies: [ 41 ] + - precedence: 900 + pass_precedence: 700 + remote_policies: [ 42 ] + - port: 80 + rules: + - precedence: 850 + deny: true + - precedence: 600 + remote_policies: [ 41, 42, 43 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/multi-tier' +)EOF")); + EXPECT_EQ(version, "14"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected14 = R"EOF(ingress: + rules: + [80-80]: + - rules: + - remotes: [41] + deny: true + precedence: 1150 + - remotes: [] + deny: true + precedence: 850 +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected14)); + + // Remote 41 hits the promoted deny from tier 1. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 41, 80, {{":path", "/multi-tier"}})); + // Remote 42 is promoted by the lower wildcard tier, but remains below deny. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 42, 80, {{":path", "/multi-tier"}})); + // Remote 43 is not promoted and is denied. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/multi-tier"}})); + + // + // 15th update: inconsistent pass precedence via inherited wildcard + local pass + // + // Wildcard current-tier pass (900/700) is inherited for port 80 at local + // pass precedence 850. A local pass with pass_precedence 600 on the same tier + // must fail as inconsistent. + EXPECT_THROW_WITH_MESSAGE(updateFromYaml(R"EOF(version_info: "15" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 900 + pass_precedence: 700 + remote_policies: [ 50 ] + - port: 80 + rules: + - precedence: 850 + pass_precedence: 600 + remote_policies: [ 51 ] + - precedence: 500 + remote_policies: [ 50, 51 ] + http_rules: + http_rules: + - headers: + - name: ':path' + exact_match: '/inconsistent-inherited-pass' +)EOF"), + EnvoyException, + "PortNetworkPolicy: Inconsistent pass precedence 600 != 700"); + + // Failed update must leave policy unchanged from version 10. + EXPECT_TRUE(validate("10.1.2.3", expected14)); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 41, 80, {{":path", "/multi-tier"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 42, 80, {{":path", "/multi-tier"}})); + + // + // 16th update: inherited wildcard pass skips remaining rules on that tier + // + // Wildcard port has a wildcard pass (2000/700), which is inherited for port 80. + // Rules in that same tier [1999..700] are skipped; a lower-tier rule at 600 is + // retained and promoted to 1900 by the inherited wildcard pass. + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "16" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - precedence: 2000 + pass_precedence: 700 - port: 80 rules: - - remote_policies: [ 43 ] + - precedence: 1200 + deny: true + remote_policies: [ 43 ] + - precedence: 1100 + remote_policies: [ 44 ] http_rules: http_rules: - headers: - name: ':path' - exact_match: '/allowed' - - port: 80 - end_port: 10000 - rules: - - proxy_id: 42 - egress_per_port_policies: - - port: 80 - rules: - - remote_policies: [ 43, 44 ] + exact_match: '/should-skip' + - precedence: 600 + remote_policies: [ 43, 44 ] http_rules: http_rules: - headers: - name: ':path' - safe_regex_match: - google_re2: {} - regex: '.*public$' + exact_match: '/promoted-after-skip' )EOF")); - EXPECT_EQ(version, "2"); + EXPECT_EQ(version, "16"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); - expected = R"EOF(ingress: - rules: - [80-80]: - - rules: - - remotes: [43] - http_rules: - - headers: - - name: ":path" - value: "/allowed" - - remotes: [] - proxy_id: 42 - [81-10000]: - - rules: - - remotes: [] - proxy_id: 42 -egress: + std::string expected16 = R"EOF(ingress: rules: [80-80]: - rules: - remotes: [43,44] + precedence: 1900 http_rules: - headers: - name: ":path" - regex: + value: "/promoted-after-skip" +egress: + rules: [] )EOF"; - EXPECT_TRUE(validate("10.1.2.3", expected)); - - // Allowed remote ID, port, & path: - EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); - // Matching proxy ID: - EXPECT_TRUE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); - // Matching proxy ID: - EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); - // Matching proxy ID: - EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + EXPECT_TRUE(validate("10.1.2.3", expected16)); - // Port out of range: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 79, {{":path", "/allowed"}})); - // Port out of range: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 10001, {{":path", "/notallowed"}})); + // Both remotes are allowed by the promoted lower-tier rule. + EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/promoted-after-skip"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/promoted-after-skip"}})); + // Tier rule at 800 is skipped by inherited wildcard pass. + EXPECT_FALSE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/should-skip"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/promoted-after-skip"}})); - // 5th update with non-matching proxy_id in policy - EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" + // + // 17th update: Shadowed rules are eliminated + // + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "17" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy endpoint_ips: - "10.1.2.3" endpoint_id: 42 ingress_per_port_policies: + - port: 0 + rules: + - precedence: 1000 + pass_precedence: 901 - port: 80 rules: - - remote_policies: [ 43 ] + - precedence: 900 + deny: true + remote_policies: [ 43 ] + - precedence: 800 + remote_policies: [ 43 ] http_rules: http_rules: - headers: - name: ':path' - exact_match: '/allowed' - - port: 80 - end_port: 10000 - rules: - - proxy_id: 99 - egress_per_port_policies: - - port: 80 - rules: - - remote_policies: [ 43, 44 ] + exact_match: '/should-skip' + - precedence: 600 + remote_policies: [ 43, 44 ] http_rules: http_rules: - headers: - name: ':path' - safe_regex_match: - google_re2: {} - regex: '.*public$' + exact_match: '/partially-skipped' )EOF")); - EXPECT_EQ(version, "2"); + EXPECT_EQ(version, "17"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); - expected = R"EOF(ingress: + std::string expected17 = R"EOF(ingress: rules: [80-80]: - rules: - remotes: [43] + deny: true + precedence: 999 + - remotes: [44] + precedence: 699 http_rules: - headers: - name: ":path" - value: "/allowed" - - remotes: [] - proxy_id: 99 - [81-10000]: - - rules: - - remotes: [] - proxy_id: 99 + value: "/partially-skipped" egress: - rules: - [80-80]: - - rules: - - remotes: [43,44] - http_rules: - - headers: - - name: ":path" - regex: + rules: [] )EOF"; - EXPECT_TRUE(validate("10.1.2.3", expected)); - - // Allowed remote ID, port, & path: - EXPECT_TRUE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/allowed"}})); - // Non-matching proxy ID: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 40, 80, {{":path", "/allowed"}})); - // Non-matching proxy ID: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 8080, {{":path", "/allowed"}})); - // Non-matching proxy ID: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/notallowed"}})); + EXPECT_TRUE(validate("10.1.2.3", expected17)); - // Port out of range: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 79, {{":path", "/allowed"}})); - // Port out of range: - EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 10001, {{":path", "/notallowed"}})); + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/partially-skipped"}})); + EXPECT_TRUE(ingressAllowed("10.1.2.3", 44, 80, {{":path", "/partially-skipped"}})); + // Rule at 800 is shadowed by higher precedence deny + EXPECT_FALSE(ingressAllowed("10.1.2.3", 43, 80, {{":path", "/should-skip"}})); + // inapplicable identity + EXPECT_FALSE(ingressAllowed("10.1.2.3", 45, 80, {{":path", "/partially-skipped"}})); } TEST_F(CiliumNetworkPolicyTest, HttpOverlappingPortRanges) { @@ -1626,6 +2687,7 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { )EOF")); EXPECT_EQ(version, "1"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + // Allowed remote ID & port: EXPECT_TRUE(tlsIngressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, raw_socket_allowed)); @@ -1651,7 +2713,7 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { EXPECT_FALSE(tls_socket_required); EXPECT_FALSE(raw_socket_allowed); - // TLS SNI update + // 2nd update without TLS requirements, with lower precedence wildcard port deny EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy @@ -1659,13 +2721,170 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { - "10.1.2.3" endpoint_id: 42 ingress_per_port_policies: + - port: 0 + rules: + - remote_policies: [ 43 ] + deny: true + precedence: 0 - port: 80 rules: - remote_policies: [ 43 ] - server_names: [ "cilium.io", "example.com" ] + precedence: 1 )EOF")); EXPECT_EQ(version, "2"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + std::string expected2 = R"EOF(ingress: + rules: + [0-0]: + - rules: + - remotes: [43] + deny: true + [80-80]: + - rules: + - remotes: [43] + precedence: 1 + - remotes: [43] + deny: true +egress: + rules: [] +)EOF"; + + EXPECT_TRUE(validate("10.1.2.3", expected2)); + + // Allowed remote ID & port: + EXPECT_TRUE(tlsIngressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_TRUE(raw_socket_allowed); + // SNI does not matter: + EXPECT_TRUE(tlsIngressAllowed("10.1.2.3", 43, 80, "", tls_socket_required, raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_TRUE(raw_socket_allowed); + // Wrong remote ID: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 40, 80, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // Wrong port: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 8080, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + + // No egress is allowed: + EXPECT_FALSE(tlsEgressAllowed("10.1.2.3", 43, 80, "", tls_socket_required, raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + + // 3rd update without TLS requirements, with same precedence wildcard port deny + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "3" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - remote_policies: [ 43 ] + deny: true + precedence: 0 + - port: 80 + rules: + - remote_policies: [ 43 ] + precedence: 0 +)EOF")); + EXPECT_EQ(version, "3"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + // Denied remote ID & port: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // SNI does not matter: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 80, "", tls_socket_required, raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // Wrong remote ID: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 40, 80, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // Wrong port: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 8080, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + + // No egress is allowed: + EXPECT_FALSE(tlsEgressAllowed("10.1.2.3", 43, 80, "", tls_socket_required, raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + + // 4th update without TLS requirements, with higher precedence wildcard port deny + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "4" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 0 + rules: + - remote_policies: [ 43 ] + deny: true + precedence: 1 + - port: 80 + rules: + - remote_policies: [ 43 ] + precedence: 0 +)EOF")); + EXPECT_EQ(version, "4"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + + // Denied remote ID & port: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // SNI does not matter: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 80, "", tls_socket_required, raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // Wrong remote ID: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 40, 80, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + // Wrong port: + EXPECT_FALSE(tlsIngressAllowed("10.1.2.3", 43, 8080, "example.com", tls_socket_required, + raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + + // No egress is allowed: + EXPECT_FALSE(tlsEgressAllowed("10.1.2.3", 43, 80, "", tls_socket_required, raw_socket_allowed)); + EXPECT_FALSE(tls_socket_required); + EXPECT_FALSE(raw_socket_allowed); + + // 5th Update: TLS SNI update + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "5" +resources: +- "@type": type.googleapis.com/cilium.NetworkPolicy + endpoint_ips: + - "10.1.2.3" + endpoint_id: 42 + ingress_per_port_policies: + - port: 80 + rules: + - remote_policies: [ 43 ] + server_names: [ "cilium.io", "example.com" ] +)EOF")); + EXPECT_EQ(version, "5"); + EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + // Allowed remote ID, port, SNI: EXPECT_TRUE(tlsIngressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, raw_socket_allowed)); @@ -1701,8 +2920,8 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { EXPECT_FALSE(tls_socket_required); EXPECT_FALSE(raw_socket_allowed); - // TLS Interception update - EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" + // 6th update: TLS Interception update + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "6" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy endpoint_ips: @@ -1718,8 +2937,9 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { upstream_tls_context: validation_context_sds_secret: "cacerts" )EOF")); - EXPECT_EQ(version, "2"); + EXPECT_EQ(version, "6"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + // Allowed remote ID, port, SNI: EXPECT_TRUE( tlsEgressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, raw_socket_allowed)); @@ -1755,8 +2975,8 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { EXPECT_FALSE(tls_socket_required); EXPECT_FALSE(raw_socket_allowed); - // TLS Termination update - EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" + // 7th update: TLS Termination update + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "7" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy endpoint_ips: @@ -1770,8 +2990,9 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { downstream_tls_context: tls_sds_secret: "secret1" )EOF")); - EXPECT_EQ(version, "2"); + EXPECT_EQ(version, "7"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + // Allowed remote ID, port, SNI: EXPECT_TRUE(tlsIngressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, raw_socket_allowed)); @@ -1807,8 +3028,8 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { EXPECT_FALSE(tls_socket_required); EXPECT_FALSE(raw_socket_allowed); - // TLS Origination update - EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "2" + // 8th update: TLS Origination update + EXPECT_NO_THROW(version = updateFromYaml(R"EOF(version_info: "8" resources: - "@type": type.googleapis.com/cilium.NetworkPolicy endpoint_ips: @@ -1821,8 +3042,9 @@ TEST_F(CiliumNetworkPolicyTest, TlsPolicyUpdate) { upstream_tls_context: validation_context_sds_secret: "cacerts" )EOF")); - EXPECT_EQ(version, "2"); + EXPECT_EQ(version, "8"); EXPECT_TRUE(policy_map_->exists("10.1.2.3")); + // Allowed remote ID, port, SNI: EXPECT_TRUE( tlsEgressAllowed("10.1.2.3", 43, 80, "example.com", tls_socket_required, raw_socket_allowed)); diff --git a/vendor/google.golang.org/protobuf/.gitignore b/vendor/google.golang.org/protobuf/.gitignore deleted file mode 100644 index 698e891b6..000000000 --- a/vendor/google.golang.org/protobuf/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/.cache/ -/.gocache/ -/bin/ -/vendor/ - -# This file includes artifacts of the system test that should not be checked in. -# For files created by specific development environment (e.g. editor), -# use alternative ways to exclude files from git. -# For example, set up .git/info/exclude or use a global .gitignore. diff --git a/vendor/google.golang.org/protobuf/CONTRIBUTING.md b/vendor/google.golang.org/protobuf/CONTRIBUTING.md deleted file mode 100644 index 1f8083bb5..000000000 --- a/vendor/google.golang.org/protobuf/CONTRIBUTING.md +++ /dev/null @@ -1,80 +0,0 @@ -# Contributing to Go Protocol Buffers - -Go protocol buffers is an open source project and accepts contributions. -The source of truth for this repository is at -[go.googlesource.com/protobuf](https://go.googlesource.com/protobuf). -The code review tool used is -[Gerrit Code Review](https://www.gerritcodereview.com/). -At this time, we are unfortunately unable to accept GitHub pull requests. - - -## Becoming a contributor - -The first step is to configure your environment. -Please follow the steps outlined in -["Becoming a contributor" (golang.org)](https://golang.org/doc/contribute.html#contributor) -as the setup for contributing to the `protobuf` project is identical -to that for contributing to the `go` project. - - -## Before contributing code - -The project welcomes submissions, but to make sure things are well coordinated -we ask that contributors discuss any significant changes before starting work. -Best practice is to connect your work to the -[issue tracker](https://github.com/golang/protobuf/issues), -either by filing a new issue or by claiming an existing issue. - - -## Sending a change via Gerrit - -The `protobuf` project performs development in Gerrit. -Below are the steps to send a change using Gerrit. - - -**Step 1:** Clone the Go source code: -``` -$ git clone https://go.googlesource.com/protobuf -``` - -**Step 2:** Setup a Git hook: -Setup a hook to run the tests prior to submitting changes to Gerrit: -``` -$ (cd protobuf/.git/hooks && echo -e '#!/bin/bash\n./test.bash' > pre-push && chmod a+x pre-push) -``` - -**Step 3:** Prepare changes in a new branch, created from the `master` branch. -To commit the changes, use `git codereview change`; -that will create or amend a single commit in the branch. - -``` -$ git checkout -b mybranch -$ [edit files...] -$ git add [files...] -$ git codereview change # create commit in the branch -$ [edit again...] -$ git add [files...] -$ git codereview change # amend the existing commit with new changes -$ [etc.] -``` - -**Step 4:** Send the changes for review to Gerrit using `git codereview mail`. -``` -$ git codereview mail # send changes to Gerrit -``` - -**Step 5:** After a review, there may be changes that are required. -Do so by applying changes to the same commit and mail them to Gerrit again: -``` -$ [edit files...] -$ git add [files...] -$ git codereview change # update same commit -$ git codereview mail # send to Gerrit again -``` - -When calling `git codereview mail`, it will call `git push` under the hood, -which will trigger the test hook that was setup in step 2. - -The [Contribution Guidelines](https://golang.org/doc/contribute.html) for the -Go project provides additional details that are also relevant to -contributing to the Go `protobuf` project. diff --git a/vendor/google.golang.org/protobuf/README.md b/vendor/google.golang.org/protobuf/README.md deleted file mode 100644 index 81d6438f4..000000000 --- a/vendor/google.golang.org/protobuf/README.md +++ /dev/null @@ -1,344 +0,0 @@ -# Go support for Protocol Buffers - -[![Go Reference](https://pkg.go.dev/badge/google.golang.org/protobuf.svg)](https://pkg.go.dev/google.golang.org/protobuf) -[![Build Status](https://travis-ci.org/protocolbuffers/protobuf-go.svg?branch=master)](https://travis-ci.org/protocolbuffers/protobuf-go) - -This project hosts the Go implementation for -[protocol buffers](https://protobuf.dev), which is a -language-neutral, platform-neutral, extensible mechanism for serializing -structured data. The protocol buffer language is a language for specifying the -schema for structured data. This schema is compiled into language specific -bindings. This project provides both a tool to generate Go code for the -protocol buffer language, and also the runtime implementation to handle -serialization of messages in Go. See the -[protocol buffer developer guide](https://protobuf.dev/overview) -for more information about protocol buffers themselves. - -This project is comprised of two components: - -* Code generator: The - [`protoc-gen-go`](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go) - tool is a compiler plugin to `protoc`, the protocol buffer compiler. It - augments the `protoc` compiler so that it knows how to - [generate Go specific code for a given `.proto` file](https://protobuf.dev/reference/go/go-generated). - -* Runtime library: The - [`protobuf`](https://pkg.go.dev/mod/google.golang.org/protobuf) module - contains a set of Go packages that form the runtime implementation of - protobufs in Go. This provides the set of interfaces that - [define what a message is](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect) - and functionality to serialize message in various formats (e.g., - [wire](https://pkg.go.dev/google.golang.org/protobuf/proto), - [JSON](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson), - and - [text](https://pkg.go.dev/google.golang.org/protobuf/encoding/prototext)). - -See the -[developer guide for protocol buffers in Go](https://protobuf.dev/getting-started/gotutorial) -for a general guide for how to get started using protobufs in Go. - -This project is the second major revision of the Go protocol buffer API -implemented by the -[`google.golang.org/protobuf`](https://pkg.go.dev/mod/google.golang.org/protobuf) -module. The first major version is implemented by the -[`github.com/golang/protobuf`](https://pkg.go.dev/mod/github.com/golang/protobuf) -module. - -## Package index - -Summary of the packages provided by this module: - -* [`proto`](https://pkg.go.dev/google.golang.org/protobuf/proto): Package - `proto` provides functions operating on protobuf messages such as cloning, - merging, and checking equality, as well as binary serialization. -* [`encoding/protojson`](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson): - Package `protojson` serializes protobuf messages as JSON. -* [`encoding/prototext`](https://pkg.go.dev/google.golang.org/protobuf/encoding/prototext): - Package `prototext` serializes protobuf messages as the text format. -* [`encoding/protowire`](https://pkg.go.dev/google.golang.org/protobuf/encoding/protowire): - Package `protowire` parses and formats the low-level raw wire encoding. Most - users should use package `proto` to serialize messages in the wire format. -* [`reflect/protoreflect`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect): - Package `protoreflect` provides interfaces to dynamically manipulate - protobuf messages. -* [`reflect/protoregistry`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoregistry): - Package `protoregistry` provides data structures to register and lookup - protobuf descriptor types. -* [`reflect/protodesc`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protodesc): - Package `protodesc` provides functionality for converting - `descriptorpb.FileDescriptorProto` messages to/from the reflective - `protoreflect.FileDescriptor`. -* [`reflect/protopath`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protopath): - Package `protopath` provides a representation of a sequence of - protobuf reflection operations on a message. -* [`reflect/protorange`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protorange): - Package `protorange` provides functionality to traverse a protobuf message. -* [`testing/protocmp`](https://pkg.go.dev/google.golang.org/protobuf/testing/protocmp): - Package `protocmp` provides protobuf specific options for the `cmp` package. -* [`testing/protopack`](https://pkg.go.dev/google.golang.org/protobuf/testing/protopack): - Package `protopack` aids manual encoding and decoding of the wire format. -* [`testing/prototest`](https://pkg.go.dev/google.golang.org/protobuf/testing/prototest): - Package `prototest` exercises the protobuf reflection implementation for - concrete message types. -* [`types/dynamicpb`](https://pkg.go.dev/google.golang.org/protobuf/types/dynamicpb): - Package `dynamicpb` creates protobuf messages at runtime from protobuf - descriptors. -* [`types/known/anypb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/anypb): - Package `anypb` is the generated package for `google/protobuf/any.proto`. -* [`types/known/timestamppb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/timestamppb): - Package `timestamppb` is the generated package for - `google/protobuf/timestamp.proto`. -* [`types/known/durationpb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/durationpb): - Package `durationpb` is the generated package for - `google/protobuf/duration.proto`. -* [`types/known/wrapperspb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/wrapperspb): - Package `wrapperspb` is the generated package for - `google/protobuf/wrappers.proto`. -* [`types/known/structpb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/structpb): - Package `structpb` is the generated package for - `google/protobuf/struct.proto`. -* [`types/known/fieldmaskpb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/fieldmaskpb): - Package `fieldmaskpb` is the generated package for - `google/protobuf/field_mask.proto`. -* [`types/known/apipb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/apipb): - Package `apipb` is the generated package for - `google/protobuf/api.proto`. -* [`types/known/typepb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/typepb): - Package `typepb` is the generated package for - `google/protobuf/type.proto`. -* [`types/known/sourcecontextpb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/sourcecontextpb): - Package `sourcecontextpb` is the generated package for - `google/protobuf/source_context.proto`. -* [`types/known/emptypb`](https://pkg.go.dev/google.golang.org/protobuf/types/known/emptypb): - Package `emptypb` is the generated package for - `google/protobuf/empty.proto`. -* [`types/descriptorpb`](https://pkg.go.dev/google.golang.org/protobuf/types/descriptorpb): - Package `descriptorpb` is the generated package for - `google/protobuf/descriptor.proto`. -* [`types/pluginpb`](https://pkg.go.dev/google.golang.org/protobuf/types/pluginpb): - Package `pluginpb` is the generated package for - `google/protobuf/compiler/plugin.proto`. -* [`compiler/protogen`](https://pkg.go.dev/google.golang.org/protobuf/compiler/protogen): - Package `protogen` provides support for writing protoc plugins. -* [`cmd/protoc-gen-go`](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go): - The `protoc-gen-go` binary is a protoc plugin to generate a Go protocol - buffer package. - -## Reporting issues - -The issue tracker for this project is currently located at -[golang/protobuf](https://github.com/golang/protobuf/issues). - -Please report any issues there with a sufficient description of the bug or -feature request. Bug reports should ideally be accompanied by a minimal -reproduction of the issue. Irreproducible bugs are difficult to diagnose and fix -(and likely to be closed after some period of time). Bug reports must specify -the version of the -[Go protocol buffer module](https://github.com/protocolbuffers/protobuf-go/releases) -and also the version of the -[protocol buffer toolchain](https://github.com/protocolbuffers/protobuf/releases) -being used. - -## Contributing - -This project is open-source and accepts contributions. See the -[contribution guide](https://github.com/protocolbuffers/protobuf-go/blob/master/CONTRIBUTING.md) -for more information. - -## Compatibility - -This module and the generated code are expected to be stable over time. However, -we reserve the right to make breaking changes without notice for the following -reasons: - -* **Security:** A security issue in the specification or implementation may - come to light whose resolution requires breaking compatibility. We reserve - the right to address such issues. -* **Unspecified behavior:** There are some aspects of the protocol buffer - specification that are undefined. Programs that depend on unspecified - behavior may break in future releases. -* **Specification changes:** It may become necessary to address an - inconsistency, incompleteness, or change in the protocol buffer - specification, which may affect the behavior of existing programs. We - reserve the right to address such changes. -* **Bugs:** If a package has a bug that violates correctness, a program - depending on the buggy behavior may break if the bug is fixed. We reserve - the right to fix such bugs. -* **Generated additions**: We reserve the right to add new declarations to - generated Go packages of `.proto` files. This includes declared constants, - variables, functions, types, fields in structs, and methods on types. This - may break attempts at injecting additional code on top of what is generated - by `protoc-gen-go`. Such practice is not supported by this project. -* **Internal changes**: We reserve the right to add, modify, and remove - internal code, which includes all unexported declarations, the - [`protoc-gen-go/internal_gengo`](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go/internal_gengo) - package, the - [`runtime/protoimpl`](https://pkg.go.dev/google.golang.org/protobuf/runtime/protoimpl?tab=doc) - package, and all packages under - [`internal`](https://pkg.go.dev/google.golang.org/protobuf/internal). - -Any breaking changes outside of these will be announced 6 months in advance to -[protobuf@googlegroups.com](https://groups.google.com/forum/#!forum/protobuf). - -Users should use generated code produced by a version of -[`protoc-gen-go`](https://pkg.go.dev/google.golang.org/protobuf/cmd/protoc-gen-go) -that is identical to the runtime version provided by the -[protobuf module](https://pkg.go.dev/mod/google.golang.org/protobuf). This -project promises that the runtime remains compatible with code produced by a -version of the generator that is no older than 1 year from the version of the -runtime used, according to the release dates of the minor version. Generated -code is expected to use a runtime version that is at least as new as the -generator used to produce it. Generated code contains references to -[`protoimpl.EnforceVersion`](https://pkg.go.dev/google.golang.org/protobuf/runtime/protoimpl?tab=doc#EnforceVersion) -to statically ensure that the generated code and runtime do not drift -sufficiently far apart. - -## Historical legacy - -This project is the second major revision -([released in 2020](https://blog.golang.org/a-new-go-api-for-protocol-buffers)) -of the Go protocol buffer API implemented by the -[`google.golang.org/protobuf`](https://pkg.go.dev/mod/google.golang.org/protobuf) -module. The first major version -([released publicly in 2010](https://blog.golang.org/third-party-libraries-goprotobuf-and)) -is implemented by the -[`github.com/golang/protobuf`](https://pkg.go.dev/mod/github.com/golang/protobuf) -module. - -The first version predates the release of Go 1 by several years. It has a long -history as one of the first core pieces of infrastructure software ever written -in Go. As such, the Go protobuf project was one of many pioneers for determining -what the Go language should even look like and what would eventually be -considered good design patterns and “idiomatic” Go (by simultaneously being -both positive and negative examples of it). - -Consider the changing signature of the `proto.Unmarshal` function as an example -of Go language and library evolution throughout the life of this project: - -```go -// 2007/09/25 - Conception of Go - -// 2008/11/12 -export func UnMarshal(r io.Read, pb_e reflect.Empty) *os.Error - -// 2008/11/13 -export func UnMarshal(buf *[]byte, pb_e reflect.Empty) *os.Error - -// 2008/11/24 -export func UnMarshal(buf *[]byte, pb_e interface{}) *os.Error - -// 2008/12/18 -export func UnMarshal(buf []byte, pb_e interface{}) *os.Error - -// 2009/01/20 -func UnMarshal(buf []byte, pb_e interface{}) *os.Error - -// 2009/04/17 -func UnMarshal(buf []byte, pb_e interface{}) os.Error - -// 2009/05/22 -func Unmarshal(buf []byte, pb_e interface{}) os.Error - -// 2011/11/03 -func Unmarshal(buf []byte, pb_e interface{}) error - -// 2012/03/28 - Release of Go 1 - -// 2012/06/12 -func Unmarshal(buf []byte, pb Message) error -``` - -These changes demonstrate the difficulty of determining what the right API is -for any new technology. It takes time multiplied by many users to determine what -is best; even then, “best” is often still somewhere over the horizon. - -The change on June 6th, 2012 added a degree of type-safety to Go protobufs by -declaring a new interface that all protobuf messages were required to implement: - -```go -type Message interface { - Reset() - String() string - ProtoMessage() -} -``` - -This interface reduced the set of types that can be passed to `proto.Unmarshal` -from the universal set of all possible Go types to those with a special -`ProtoMessage` marker method. The intention of this change is to limit the -protobuf API to only operate on protobuf data types (i.e., protobuf messages). -For example, there is no sensible operation if a Go channel were passed to the -protobuf API as a channel cannot be serialized. The restricted interface would -prevent that. - -This interface does not behaviorally describe what a protobuf message is, but -acts as a marker with an undocumented expectation that protobuf messages must be -a Go struct with a specific layout of fields with formatted tags. This -expectation is not statically enforced by the Go language, for it is an -implementation detail checked dynamically at runtime using Go reflection. Back -in 2012, the only types with this marker were those generated by -`protoc-gen-go`. Since `protoc-gen-go` would always generate messages with the -proper layout of fields, this was deemed an acceptable and dramatic improvement -over `interface{}`. - -Over the next 10 years, -[use of Go would skyrocket](https://blog.golang.org/10years) and use of -protobufs in Go would skyrocket as well. With increased popularity also came -more diverse usages and requirements for Go protobufs and an increased number of -custom `proto.Message` implementations that were not generated by -`protoc-gen-go`. - -The increasingly diverse ecosystem of Go types implementing the `proto.Message` -interface led to incompatibilities, which often occurred when: - -* **Passing custom `proto.Message` types to the protobuf APIs**: A concrete - message implementation might work with some top-level functions (e.g., - `proto.Marshal`), but cause others (e.g., `proto.Equal`) to choke and panic. - This occurs because the type only had partial support for being an actual - message by only implementing the `proto.Marshaler` interface or having - malformed struct field tags that happened to work with one function, but not - another. - -* **Using Go reflection on any `proto.Message` types**: A common desire is to - write general-purpose code that operates on any protobuf message. For - example, a microservice might want to populate a `trace_id` field if it is - present in a message. To accomplish this, one would use Go reflection to - introspect the message type, and assume it were a pointer to a Go struct - with a field named `TraceId` (as would be commonly produced by - `protoc-gen-go`). If the concrete message type did not match this - expectation, it either failed to work or even resulted in a panic. Such was - the case for concrete message types that might be backed by a Go map instead - of a Go struct. - -Both of these issues are solved by following the idiom that _interfaces should -describe behavior, not data_. This means that the interface itself should -provide sufficient functionality through its methods that users can introspect -and interact with all aspects of a protobuf message through a principled API. -This feature is called _protobuf reflection_. Just as how Go reflection provides -an API for programmatically interacting with any arbitrary Go value, protobuf -reflection provides an API for programmatically interacting with any arbitrary -protobuf message. - -Since an interface cannot be extended in a backwards compatible way, this -suggested the need for a new major version that defines a new `proto.Message` -interface: - -```go -type Message interface { - ProtoReflect() protoreflect.Message -} -``` - -The new -[`proto.Message`](https://pkg.go.dev/google.golang.org/protobuf/proto?tab=doc#Message) -interface contains a single `ProtoReflect` method that returns a -[`protoreflect.Message`](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect?tab=doc#Message), -which is a reflective view over a protobuf message. In addition to making a -breaking change to the `proto.Message` interface, we took this opportunity to -cleanup the supporting functionality that operate on a `proto.Message`, split up -complicated functionality apart into manageable packages, and to hide -implementation details away from the public API. - -The goal for this major revision is to improve upon all the benefits of, while -addressing all the shortcomings of the old API. We hope that it will serve the -Go ecosystem well for the next 10 years and beyond. diff --git a/vendor/google.golang.org/protobuf/SECURITY.md b/vendor/google.golang.org/protobuf/SECURITY.md deleted file mode 100644 index 4648e5e3a..000000000 --- a/vendor/google.golang.org/protobuf/SECURITY.md +++ /dev/null @@ -1,4 +0,0 @@ -To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). -We use g.co/vulnz for our intake, and do coordination and disclosure here on -GitHub (including using GitHub Security Advisory). The Google Security Team will -respond within 5 working days of your report on g.co/vulnz. diff --git a/vendor/google.golang.org/protobuf/regenerate.bash b/vendor/google.golang.org/protobuf/regenerate.bash deleted file mode 100644 index 44bdacbc7..000000000 --- a/vendor/google.golang.org/protobuf/regenerate.bash +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Copyright 2018 The Go Authors. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -go test google.golang.org/protobuf -run='^TestIntegration$' -v -timeout=60m -count=1 "$@" -regenerate -exit $? diff --git a/vendor/google.golang.org/protobuf/release.bash b/vendor/google.golang.org/protobuf/release.bash deleted file mode 100644 index 0966da1d8..000000000 --- a/vendor/google.golang.org/protobuf/release.bash +++ /dev/null @@ -1,98 +0,0 @@ -#!/bin/bash -# Copyright 2019 The Go Authors. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -cd "$(git rev-parse --show-toplevel)" - -read -p "What is the next release version (e.g., 'v1.26.0')? " VERSION -SEMVER_REGEX='^v\([0-9]*\)[.]\([0-9]*\)[.]\([0-9]*\)\([.a-zA-Z0-9A-Z-]*\)$' -if ! [[ -z $(echo $VERSION | sed -e "s/$SEMVER_REGEX//") ]]; then - echo; echo "invalid: must be a semver string"; exit 1 -fi -VERSION_MAJOR=$(echo $VERSION | sed -e "s/$SEMVER_REGEX/\1/") -VERSION_MINOR=$(echo $VERSION | sed -e "s/$SEMVER_REGEX/\2/") -VERSION_PATCH=$(echo $VERSION | sed -e "s/$SEMVER_REGEX/\3/") -VERSION_PRERELEASE=$(echo $VERSION | sed -e "s/$SEMVER_REGEX/\4/") -if ! [[ "$VERSION_MAJOR" =~ ^1$ ]]; then - echo; echo "invalid: major version must be 1"; exit 1 -fi -if ! [[ -z $VERSION_PRERELEASE ]] && ! [[ "$VERSION_PRERELEASE" =~ ^-rc[.][0-9]+$ ]]; then - echo; echo "invalid: pre-release suffix must be empty or '-rc.X'"; exit 1 -fi -VERSION_PRERELEASE=${VERSION_PRERELEASE#"-"} # trim possible leading dash - -function version_string() { - VERSION_STRING="v${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}" - if ! [[ -z $VERSION_PRERELEASE ]]; then - VERSION_STRING="${VERSION_STRING}-${VERSION_PRERELEASE}" - fi - echo $VERSION_STRING -} - -read -p "Were there any changes to the generator that relies on new runtime functionality? " YN -case $YN in -[Yy]* ) - read -p " What minor version of the runtime is required now? " GEN_VERSION - if ! [[ "$GEN_VERSION" =~ ^[0-9]+$ ]]; then echo; echo "invalid: must be an integer"; exit 1; fi;; -[Nn]* ) ;; -* ) echo; echo "invalid: must be 'yes' or 'no'"; exit 1;; -esac - -read -p "Were there any dropped functionality in the runtime for old generated code? " YN -case $YN in -[Yy]* ) - read -p " What minor version of the runtime is required now? " MIN_VERSION - if ! [[ "$MIN_VERSION" =~ ^[0-9]+$ ]]; then echo; echo "invalid: must be an integer"; exit 1; fi;; -[Nn]* ) ;; -* ) echo; echo "invalid: must be 'yes' or 'no'"; exit 1;; -esac - - -echo -echo "Preparing changes to release $(version_string)." -echo - -set -e - -# Create a new branch to contain the release changes. -if [[ $(git branch --list release) ]]; then - echo "error: release branch already exists"; exit 1 -fi -git codereview change release -git codereview sync - -# Create commit for actual release. -INPLACE='-i ""' # BSD version of sed expects argument after -i -if [[ "$(sed --version)" == *"GNU"* ]]; then - INPLACE="-i" # GNU version of sed does not expect argument after -i -fi -sed $INPLACE -e "s/\(Minor *= *\)[0-9]*/\1$VERSION_MINOR/" internal/version/version.go -sed $INPLACE -e "s/\(Patch *= *\)[0-9]*/\1$VERSION_PATCH/" internal/version/version.go -sed $INPLACE -e "s/\(PreRelease *= *\)\"[^\"]*\"/\1\"$VERSION_PRERELEASE\"/" internal/version/version.go -if ! [[ -z $GEN_VERSION ]]; then - sed $INPLACE -e "s/\(GenVersion *= *\)[0-9]*/\1$GEN_VERSION/" runtime/protoimpl/version.go -fi -if ! [[ -z $MIN_VERSION ]]; then - sed $INPLACE -e "s/\(MinVersion *= *\)[0-9]*/\1$MIN_VERSION/" runtime/protoimpl/version.go -fi -git commit -a -m "all: release $(version_string)" - -# Build release binaries. -go test google.golang.org/protobuf -run='^TestIntegration$' -timeout=60m -count=1 "$@" -buildRelease - -# Create commit to start development after release. -VERSION_PRERELEASE="${VERSION_PRERELEASE}.devel" # append ".devel" -VERSION_PRERELEASE="${VERSION_PRERELEASE#"."}" # trim possible leading "." -sed $INPLACE -e "s/\(PreRelease *= *\)\"[^\"]*\"/\1\"$VERSION_PRERELEASE\"/" internal/version/version.go -git commit -a -m "all: start $(version_string)" - -echo -echo "Release changes prepared. Additional steps:" -echo " 1) Submit the changes:" -echo " a. Mail out the changes: git mail HEAD" -echo " b. Request a review on the changes and merge them." -echo " 2) Tag a new release on GitHub:" -echo " a. Write release notes highlighting notable changes." -echo " b. Attach pre-compiled binaries as assets to the release." -echo diff --git a/vendor/google.golang.org/protobuf/test.bash b/vendor/google.golang.org/protobuf/test.bash deleted file mode 100644 index b2e2f1c38..000000000 --- a/vendor/google.golang.org/protobuf/test.bash +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Copyright 2018 The Go Authors. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. - -go test google.golang.org/protobuf -run='^TestIntegration$' -v -timeout=60m -count=1 -failfast "$@" -exit $? diff --git a/vendor/modules.txt b/vendor/modules.txt index 005308233..8d3eb2f0c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -192,7 +192,6 @@ google.golang.org/grpc/status google.golang.org/grpc/tap # google.golang.org/protobuf v1.36.11 ## explicit; go 1.23 -google.golang.org/protobuf google.golang.org/protobuf/cmd/protoc-gen-go google.golang.org/protobuf/cmd/protoc-gen-go/internal_gengo google.golang.org/protobuf/compiler/protogen