diff --git a/api/envoy/config/rbac/v3/BUILD b/api/envoy/config/rbac/v3/BUILD index c5246439c7b55..c289def1f11d2 100644 --- a/api/envoy/config/rbac/v3/BUILD +++ b/api/envoy/config/rbac/v3/BUILD @@ -10,6 +10,7 @@ api_proto_package( "//envoy/config/core/v3:pkg", "//envoy/config/route/v3:pkg", "//envoy/type/matcher/v3:pkg", + "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", "@com_google_googleapis//google/api/expr/v1alpha1:checked_proto", "@com_google_googleapis//google/api/expr/v1alpha1:syntax_proto", diff --git a/api/envoy/config/rbac/v3/rbac.proto b/api/envoy/config/rbac/v3/rbac.proto index 44b3cf7cee6ec..d66f9be2b4981 100644 --- a/api/envoy/config/rbac/v3/rbac.proto +++ b/api/envoy/config/rbac/v3/rbac.proto @@ -7,6 +7,7 @@ import "envoy/config/route/v3/route_components.proto"; import "envoy/type/matcher/v3/metadata.proto"; import "envoy/type/matcher/v3/path.proto"; import "envoy/type/matcher/v3/string.proto"; +import "envoy/type/v3/range.proto"; import "google/api/expr/v1alpha1/checked.proto"; import "google/api/expr/v1alpha1/syntax.proto"; @@ -145,7 +146,7 @@ message Policy { } // Permission defines an action (or actions) that a principal can take. -// [#next-free-field: 11] +// [#next-free-field: 12] message Permission { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Permission"; @@ -185,6 +186,9 @@ message Permission { // A port number that describes the destination port connecting to. uint32 destination_port = 6 [(validate.rules).uint32 = {lte: 65535}]; + // A port number range that describes a range of destination ports connecting to. + type.v3.Int32Range destination_port_range = 11; + // Metadata that describes additional information about the action. type.matcher.v3.MetadataMatcher metadata = 7; diff --git a/api/envoy/config/rbac/v4alpha/BUILD b/api/envoy/config/rbac/v4alpha/BUILD index f5683a61a2867..090d01f3cd17c 100644 --- a/api/envoy/config/rbac/v4alpha/BUILD +++ b/api/envoy/config/rbac/v4alpha/BUILD @@ -10,6 +10,7 @@ api_proto_package( "//envoy/config/rbac/v3:pkg", "//envoy/config/route/v4alpha:pkg", "//envoy/type/matcher/v4alpha:pkg", + "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", "@com_google_googleapis//google/api/expr/v1alpha1:checked_proto", "@com_google_googleapis//google/api/expr/v1alpha1:syntax_proto", diff --git a/api/envoy/config/rbac/v4alpha/rbac.proto b/api/envoy/config/rbac/v4alpha/rbac.proto index bd56c0c3dc326..6fbd5a90f37db 100644 --- a/api/envoy/config/rbac/v4alpha/rbac.proto +++ b/api/envoy/config/rbac/v4alpha/rbac.proto @@ -7,6 +7,7 @@ import "envoy/config/route/v4alpha/route_components.proto"; import "envoy/type/matcher/v4alpha/metadata.proto"; import "envoy/type/matcher/v4alpha/path.proto"; import "envoy/type/matcher/v4alpha/string.proto"; +import "envoy/type/v3/range.proto"; import "google/api/expr/v1alpha1/checked.proto"; import "google/api/expr/v1alpha1/syntax.proto"; @@ -143,7 +144,7 @@ message Policy { } // Permission defines an action (or actions) that a principal can take. -// [#next-free-field: 11] +// [#next-free-field: 12] message Permission { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v3.Permission"; @@ -183,6 +184,9 @@ message Permission { // A port number that describes the destination port connecting to. uint32 destination_port = 6 [(validate.rules).uint32 = {lte: 65535}]; + // A port number range that describes a range of destination ports connecting to. + type.v3.Int32Range destination_port_range = 11; + // Metadata that describes additional information about the action. type.matcher.v4alpha.MetadataMatcher metadata = 7; diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 9877d650b1233..6abcdac747fe2 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -53,8 +53,8 @@ New Features ------------ * http: added :ref:`string_match ` in the header matcher. * http: added support for :ref:`max_requests_per_connection ` for both upstream and downstream connections. - * jwt_authn: added support for :ref:`Jwt Cache ` and its size can be specified by :ref:`jwt_cache_size `. +* rbac: added :ref:`destination_port_range ` for matching range of destination ports. Deprecated ---------- diff --git a/generated_api_shadow/envoy/config/rbac/v3/BUILD b/generated_api_shadow/envoy/config/rbac/v3/BUILD index c5246439c7b55..c289def1f11d2 100644 --- a/generated_api_shadow/envoy/config/rbac/v3/BUILD +++ b/generated_api_shadow/envoy/config/rbac/v3/BUILD @@ -10,6 +10,7 @@ api_proto_package( "//envoy/config/core/v3:pkg", "//envoy/config/route/v3:pkg", "//envoy/type/matcher/v3:pkg", + "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", "@com_google_googleapis//google/api/expr/v1alpha1:checked_proto", "@com_google_googleapis//google/api/expr/v1alpha1:syntax_proto", diff --git a/generated_api_shadow/envoy/config/rbac/v3/rbac.proto b/generated_api_shadow/envoy/config/rbac/v3/rbac.proto index 44b3cf7cee6ec..d66f9be2b4981 100644 --- a/generated_api_shadow/envoy/config/rbac/v3/rbac.proto +++ b/generated_api_shadow/envoy/config/rbac/v3/rbac.proto @@ -7,6 +7,7 @@ import "envoy/config/route/v3/route_components.proto"; import "envoy/type/matcher/v3/metadata.proto"; import "envoy/type/matcher/v3/path.proto"; import "envoy/type/matcher/v3/string.proto"; +import "envoy/type/v3/range.proto"; import "google/api/expr/v1alpha1/checked.proto"; import "google/api/expr/v1alpha1/syntax.proto"; @@ -145,7 +146,7 @@ message Policy { } // Permission defines an action (or actions) that a principal can take. -// [#next-free-field: 11] +// [#next-free-field: 12] message Permission { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v2.Permission"; @@ -185,6 +186,9 @@ message Permission { // A port number that describes the destination port connecting to. uint32 destination_port = 6 [(validate.rules).uint32 = {lte: 65535}]; + // A port number range that describes a range of destination ports connecting to. + type.v3.Int32Range destination_port_range = 11; + // Metadata that describes additional information about the action. type.matcher.v3.MetadataMatcher metadata = 7; diff --git a/generated_api_shadow/envoy/config/rbac/v4alpha/BUILD b/generated_api_shadow/envoy/config/rbac/v4alpha/BUILD index ddf34cc1032bc..2b205e7373632 100644 --- a/generated_api_shadow/envoy/config/rbac/v4alpha/BUILD +++ b/generated_api_shadow/envoy/config/rbac/v4alpha/BUILD @@ -11,6 +11,7 @@ api_proto_package( "//envoy/config/rbac/v3:pkg", "//envoy/config/route/v4alpha:pkg", "//envoy/type/matcher/v4alpha:pkg", + "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", "@com_google_googleapis//google/api/expr/v1alpha1:checked_proto", "@com_google_googleapis//google/api/expr/v1alpha1:syntax_proto", diff --git a/generated_api_shadow/envoy/config/rbac/v4alpha/rbac.proto b/generated_api_shadow/envoy/config/rbac/v4alpha/rbac.proto index 3b27e68bba1dc..bff8576a27c84 100644 --- a/generated_api_shadow/envoy/config/rbac/v4alpha/rbac.proto +++ b/generated_api_shadow/envoy/config/rbac/v4alpha/rbac.proto @@ -7,6 +7,7 @@ import "envoy/config/route/v4alpha/route_components.proto"; import "envoy/type/matcher/v4alpha/metadata.proto"; import "envoy/type/matcher/v4alpha/path.proto"; import "envoy/type/matcher/v4alpha/string.proto"; +import "envoy/type/v3/range.proto"; import "google/api/expr/v1alpha1/checked.proto"; import "google/api/expr/v1alpha1/syntax.proto"; @@ -144,7 +145,7 @@ message Policy { } // Permission defines an action (or actions) that a principal can take. -// [#next-free-field: 11] +// [#next-free-field: 12] message Permission { option (udpa.annotations.versioning).previous_message_type = "envoy.config.rbac.v3.Permission"; @@ -184,6 +185,9 @@ message Permission { // A port number that describes the destination port connecting to. uint32 destination_port = 6 [(validate.rules).uint32 = {lte: 65535}]; + // A port number range that describes a range of destination ports connecting to. + type.v3.Int32Range destination_port_range = 11; + // Metadata that describes additional information about the action. type.matcher.v4alpha.MetadataMatcher metadata = 7; diff --git a/source/extensions/filters/common/rbac/matchers.cc b/source/extensions/filters/common/rbac/matchers.cc index 25d8e2de63b54..fc010bc46c68e 100644 --- a/source/extensions/filters/common/rbac/matchers.cc +++ b/source/extensions/filters/common/rbac/matchers.cc @@ -23,6 +23,8 @@ MatcherConstSharedPtr Matcher::create(const envoy::config::rbac::v3::Permission& IPMatcher::Type::DownstreamLocal); case envoy::config::rbac::v3::Permission::RuleCase::kDestinationPort: return std::make_shared(permission.destination_port()); + case envoy::config::rbac::v3::Permission::RuleCase::kDestinationPortRange: + return std::make_shared(permission.destination_port_range()); case envoy::config::rbac::v3::Permission::RuleCase::kAny: return std::make_shared(); case envoy::config::rbac::v3::Permission::RuleCase::kMetadata: @@ -159,6 +161,34 @@ bool PortMatcher::matches(const Network::Connection&, const Envoy::Http::Request return ip && ip->port() == port_; } +PortRangeMatcher::PortRangeMatcher(const ::envoy::type::v3::Int32Range& range) + : start_(range.start()), end_(range.end()) { + auto start = range.start(); + auto end = range.end(); + if (start < 0 || start > 65536) { + throw EnvoyException(fmt::format("range start {} is out of bounds", start)); + } + if (end < 0 || end > 65536) { + throw EnvoyException(fmt::format("range end {} is out of bounds", end)); + } + if (start >= end) { + throw EnvoyException( + fmt::format("range start {} cannot be greater or equal than range end {}", start, end)); + } +} + +bool PortRangeMatcher::matches(const Network::Connection&, const Envoy::Http::RequestHeaderMap&, + const StreamInfo::StreamInfo& info) const { + const Envoy::Network::Address::Ip* ip = + info.downstreamAddressProvider().localAddress().get()->ip(); + if (ip) { + const auto port = ip->port(); + return start_ <= port && port < end_; + } else { + return false; + } +} + bool AuthenticatedMatcher::matches(const Network::Connection& connection, const Envoy::Http::RequestHeaderMap&, const StreamInfo::StreamInfo&) const { diff --git a/source/extensions/filters/common/rbac/matchers.h b/source/extensions/filters/common/rbac/matchers.h index 472b4a2c9c17e..79d43fb59f0c2 100644 --- a/source/extensions/filters/common/rbac/matchers.h +++ b/source/extensions/filters/common/rbac/matchers.h @@ -163,6 +163,18 @@ class PortMatcher : public Matcher { const uint32_t port_; }; +class PortRangeMatcher : public Matcher { +public: + PortRangeMatcher(const ::envoy::type::v3::Int32Range& range); + + bool matches(const Network::Connection&, const Envoy::Http::RequestHeaderMap&, + const StreamInfo::StreamInfo& info) const override; + +private: + const uint32_t start_; + const uint32_t end_; +}; + /** * Matches the principal name as described in the peer certificate. Uses the URI SAN first. If that * field is not present, uses the subject instead. diff --git a/test/extensions/filters/common/rbac/matchers_test.cc b/test/extensions/filters/common/rbac/matchers_test.cc index 87d22517dd428..0979445343df8 100644 --- a/test/extensions/filters/common/rbac/matchers_test.cc +++ b/test/extensions/filters/common/rbac/matchers_test.cc @@ -4,6 +4,7 @@ #include "envoy/config/route/v3/route_components.pb.h" #include "envoy/type/matcher/v3/metadata.pb.h" +#include "source/common/network/address_impl.h" #include "source/common/network/utility.h" #include "source/extensions/filters/common/expr/evaluator.h" #include "source/extensions/filters/common/rbac/matchers.h" @@ -33,6 +34,10 @@ void checkMatcher( EXPECT_EQ(expected, matcher.matches(connection, headers, info)); } +PortRangeMatcher createPortRangeMatcher(envoy::type::v3::Int32Range range) { + return PortRangeMatcher(range); +} + TEST(AlwaysMatcher, AlwaysMatches) { checkMatcher(RBAC::AlwaysMatcher(), true); } TEST(AndMatcher, Permission_Set) { @@ -101,6 +106,12 @@ TEST(OrMatcher, Permission_Set) { checkMatcher(RBAC::OrMatcher(set), false, conn, headers, info); + perm = set.add_rules(); + perm->mutable_destination_port_range()->set_start(123); + perm->mutable_destination_port_range()->set_end(456); + + checkMatcher(RBAC::OrMatcher(set), false, conn, headers, info); + perm = set.add_rules(); perm->set_any(true); @@ -233,6 +244,58 @@ TEST(PortMatcher, PortMatcher) { checkMatcher(PortMatcher(456), false, conn, headers, info); } +// Test valid and invalid destination_port_range permission rule in RBAC. +TEST(PortRangeMatcher, PortRangeMatcher) { + Envoy::Network::MockConnection conn; + Envoy::Http::TestRequestHeaderMapImpl headers; + NiceMock info; + Envoy::Network::Address::InstanceConstSharedPtr addr = + Envoy::Network::Utility::parseInternetAddress("1.2.3.4", 456, false); + info.downstream_address_provider_->setLocalAddress(addr); + + // IP address with port 456 is in range [123, 789) and [456, 789), but not in range [123, 456) or + // [12, 34). + envoy::type::v3::Int32Range range; + range.set_start(123); + range.set_end(789); + checkMatcher(PortRangeMatcher(range), true, conn, headers, info); + + range.set_start(456); + range.set_end(789); + checkMatcher(PortRangeMatcher(range), true, conn, headers, info); + + range.set_start(123); + range.set_end(456); + checkMatcher(PortRangeMatcher(range), false, conn, headers, info); + + range.set_start(12); + range.set_end(34); + checkMatcher(PortRangeMatcher(range), false, conn, headers, info); + + // Only IP address is valid for the permission rule. + NiceMock info2; + Envoy::Network::Address::InstanceConstSharedPtr addr2 = + std::make_shared("test"); + info2.downstream_address_provider_->setLocalAddress(addr2); + checkMatcher(PortRangeMatcher(range), false, conn, headers, info2); + + // Invalid rule will cause an exception. + range.set_start(-1); + range.set_end(80); + EXPECT_THROW_WITH_REGEX(createPortRangeMatcher(range), EnvoyException, + "range start .* is out of bounds"); + + range.set_start(80); + range.set_end(65537); + EXPECT_THROW_WITH_REGEX(createPortRangeMatcher(range), EnvoyException, + "range end .* is out of bounds"); + + range.set_start(80); + range.set_end(80); + EXPECT_THROW_WITH_REGEX(createPortRangeMatcher(range), EnvoyException, + "range start .* cannot be greater or equal than range end .*"); +} + TEST(AuthenticatedMatcher, uriSanPeerCertificate) { Envoy::Network::MockConnection conn; auto ssl = std::make_shared();