Skip to content

Commit 5091626

Browse files
committed
feat: support rate limit action based on cidr match
Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com>
1 parent 7dc9d36 commit 5091626

File tree

14 files changed

+480
-19
lines changed

14 files changed

+480
-19
lines changed

api/envoy/config/route/v3/route_components.proto

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2492,7 +2492,6 @@ message RateLimit {
24922492
// .. code-block:: cpp
24932493
//
24942494
// ("remote_address_match", "<descriptor_value>")
2495-
// [#not-implemented-hide:]
24962495
message RemoteAddressMatch {
24972496
// Descriptor value of entry.
24982497
//
@@ -2502,13 +2501,13 @@ message RateLimit {
25022501
//
25032502
// .. note::
25042503
//
2505-
// The format string can contain multiple valid substitution fields. If multiple substitution
2506-
// fields are present, their results will be concatenated to form the final descriptor value.
2507-
// If it contains no substitution fields, the value will be used as is.
2508-
// All substitution fields will be evaluated and their results concatenated.
2509-
// If the final concatenated result is empty and ``default_value`` is set, the ``default_value`` will be used.
2510-
// If ``default_value`` is not set and the result is empty, this descriptor will be skipped
2511-
// and not included in the rate limit call.
2504+
// The format string can contain multiple valid substitution fields. If multiple
2505+
// substitution fields are present, their results will be concatenated to form the
2506+
// final descriptor value. If it contains no substitution fields, the value will be
2507+
// used as is. All substitution fields will be evaluated and their results concatenated.
2508+
// If the final concatenated result is empty and ``default_value`` is set, the
2509+
// ``default_value`` will be used. If ``default_value`` is not set and the result is
2510+
// empty, this descriptor will be skipped and not included in the rate limit call.
25122511
//
25132512
// For example, ``static_value`` will be used as is since there are no substitution fields.
25142513
// ``%REQ(:method)%`` will be replaced with the HTTP method, and
@@ -2588,7 +2587,6 @@ message RateLimit {
25882587
// Rate limit on the existence of query parameters.
25892588
QueryParameterValueMatch query_parameter_value_match = 11;
25902589

2591-
// [#not-implemented-hide:]
25922590
// Rate limit on remote address match.
25932591
RemoteAddressMatch remote_address_match = 13;
25942592
}

api/envoy/type/matcher/v3/address.proto

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
2020
message AddressMatcher {
2121
repeated xds.core.v3.CidrRange ranges = 1;
2222

23-
// [#not-implemented-hide:]
2423
// If true, the match result will be inverted. Defaults to false.
24+
//
25+
// * If set to false (default), the matcher will return true if the IP matches any of the CIDR ranges.
26+
// * If set to true, the matcher will return true if the IP does NOT match any of the CIDR ranges.
2527
bool invert_match = 2;
2628
}

changelogs/current.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@ new_features:
126126
Added per-descriptor ``x-ratelimit-*`` headers support. See the
127127
:ref:`x_ratelimit_option <envoy_v3_api_field_config.route.v3.RateLimit.x_ratelimit_option>`
128128
field documentation for more details.
129+
- area: ratelimit
130+
change: |
131+
Added ``RemoteAddressMatch`` action to the rate limit filter. This action matches remote addresses against
132+
CIDR ranges with support for ``invert_match`` to match addresses outside specified ranges.
129133
- area: mcp_router
130134
change: |
131135
Added support for MCP client-to-server notification methods ``notifications/cancelled``

source/common/common/filter_state_object_matchers.cc

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ namespace Envoy {
1313
namespace Matchers {
1414

1515
FilterStateIpRangeMatcher::FilterStateIpRangeMatcher(
16-
std::unique_ptr<Network::Address::IpList>&& ip_list)
17-
: ip_list_(std::move(ip_list)) {}
16+
std::unique_ptr<Network::Address::IpList>&& ip_list, bool invert_match)
17+
: ip_list_(std::move(ip_list)), invert_match_(invert_match) {}
1818

1919
bool FilterStateIpRangeMatcher::match(const StreamInfo::FilterState::Object& object) const {
2020
const Network::Address::InstanceAccessor* ip =
2121
dynamic_cast<const Network::Address::InstanceAccessor*>(&object);
2222
if (ip == nullptr) {
2323
return false;
2424
}
25-
return ip_list_->contains(*ip->getIp());
25+
const bool matches = ip_list_->contains(*ip->getIp());
26+
return invert_match_ ? !matches : matches;
2627
}
2728

2829
FilterStateStringMatcher::FilterStateStringMatcher(StringMatcherPtr&& string_matcher)

source/common/common/filter_state_object_matchers.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ using FilterStateObjectMatcherPtr = std::unique_ptr<FilterStateObjectMatcher>;
2121

2222
class FilterStateIpRangeMatcher : public FilterStateObjectMatcher {
2323
public:
24-
FilterStateIpRangeMatcher(std::unique_ptr<Network::Address::IpList>&& ip_list);
24+
FilterStateIpRangeMatcher(std::unique_ptr<Network::Address::IpList>&& ip_list,
25+
bool invert_match = false);
2526
bool match(const StreamInfo::FilterState::Object& object) const override;
2627

2728
private:
2829
std::unique_ptr<Envoy::Network::Address::IpList> ip_list_;
30+
const bool invert_match_;
2931
};
3032

3133
class FilterStateStringMatcher : public FilterStateObjectMatcher {

source/common/common/matchers.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,8 @@ filterStateObjectMatcherFromProto(const envoy::type::matcher::v3::FilterStateMat
182182
case envoy::type::matcher::v3::FilterStateMatcher::MatcherCase::kAddressMatch: {
183183
auto ip_list = Network::Address::IpList::create(matcher.address_match().ranges());
184184
RETURN_IF_NOT_OK_REF(ip_list.status());
185-
return std::make_unique<FilterStateIpRangeMatcher>(std::move(*ip_list));
185+
return std::make_unique<FilterStateIpRangeMatcher>(std::move(*ip_list),
186+
matcher.address_match().invert_match());
186187
break;
187188
}
188189
default:

source/common/router/router_ratelimit.cc

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,47 @@ QueryParameterValueMatchAction::buildQueryParameterMatcherVector(
342342
return ret;
343343
}
344344

345+
RemoteAddressMatchAction::RemoteAddressMatchAction(
346+
const envoy::config::route::v3::RateLimit::Action::RemoteAddressMatch& action,
347+
Server::Configuration::CommonFactoryContext&)
348+
: descriptor_value_(action.descriptor_value()),
349+
descriptor_key_(!action.descriptor_key().empty() ? action.descriptor_key()
350+
: "remote_address_match"),
351+
default_value_(action.default_value()),
352+
ip_list_(Network::Address::IpList::create(action.address_matcher().ranges()).value()),
353+
invert_match_(action.address_matcher().invert_match()),
354+
descriptor_formatter_(Formatter::FormatterImpl::create(descriptor_value_, true).value()) {}
355+
356+
bool RemoteAddressMatchAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry,
357+
const std::string&,
358+
const Http::RequestHeaderMap& headers,
359+
const StreamInfo::StreamInfo& info) const {
360+
// Check if remote address matches the address matcher
361+
const Network::Address::InstanceConstSharedPtr& remote_address =
362+
info.downstreamAddressProvider().remoteAddress();
363+
if (remote_address->type() != Network::Address::Type::Ip) {
364+
return false;
365+
}
366+
367+
const bool matches = ip_list_->contains(*remote_address);
368+
const bool should_apply = invert_match_ ? !matches : matches;
369+
if (!should_apply) {
370+
return false;
371+
}
372+
373+
// Format the descriptor value
374+
const std::string formatted_value = descriptor_formatter_->format({&headers}, info);
375+
if (!formatted_value.empty()) {
376+
descriptor_entry = {descriptor_key_, formatted_value};
377+
} else if (!default_value_.empty()) {
378+
descriptor_entry = {descriptor_key_, default_value_};
379+
} else {
380+
// If formatting resulted in empty string and no default_value, skip this descriptor
381+
return false;
382+
}
383+
return true;
384+
}
385+
345386
RateLimitPolicyEntryImpl::RateLimitPolicyEntryImpl(
346387
const envoy::config::route::v3::RateLimit& config,
347388
Server::Configuration::CommonFactoryContext& context, absl::Status& creation_status)
@@ -453,8 +494,7 @@ RateLimitPolicyEntryImpl::RateLimitPolicyEntryImpl(
453494
break;
454495
}
455496
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::kRemoteAddressMatch:
456-
// [#not-implemented-hide:] RemoteAddressMatch is not yet implemented.
457-
PANIC("RemoteAddressMatch rate limit action is not yet implemented");
497+
actions_.emplace_back(new RemoteAddressMatchAction(action.remote_address_match(), context));
458498
break;
459499
case envoy::config::route::v3::RateLimit::Action::ActionSpecifierCase::ACTION_SPECIFIER_NOT_SET:
460500
PANIC_DUE_TO_CORRUPT_ENUM;

source/common/router/router_ratelimit.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,30 @@ class QueryParameterValueMatchAction : public RateLimit::DescriptorProducer {
238238
const std::unique_ptr<Formatter::FormatterImpl> descriptor_formatter_;
239239
};
240240

241+
/**
242+
* Action for remote address match rate limiting.
243+
*/
244+
class RemoteAddressMatchAction : public RateLimit::DescriptorProducer {
245+
public:
246+
RemoteAddressMatchAction(
247+
const envoy::config::route::v3::RateLimit::Action::RemoteAddressMatch& action,
248+
Server::Configuration::CommonFactoryContext& context);
249+
250+
// Ratelimit::DescriptorProducer
251+
bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry,
252+
const std::string& local_service_cluster,
253+
const Http::RequestHeaderMap& headers,
254+
const StreamInfo::StreamInfo& info) const override;
255+
256+
private:
257+
const std::string descriptor_value_;
258+
const std::string descriptor_key_;
259+
const std::string default_value_;
260+
const std::unique_ptr<Network::Address::IpList> ip_list_;
261+
const bool invert_match_;
262+
const std::unique_ptr<Formatter::FormatterImpl> descriptor_formatter_;
263+
};
264+
241265
class RateLimitDescriptorValidationVisitor
242266
: public Matcher::MatchTreeValidationVisitor<Http::HttpMatchingData> {
243267
public:

source/extensions/filters/common/ratelimit_config/ratelimit_config.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ RateLimitPolicy::RateLimitPolicy(const ProtoRateLimit& config,
161161
action.query_parameter_value_match(), context, std::move(formatter_or_error.value())));
162162
break;
163163
}
164+
case ProtoRateLimit::Action::ActionSpecifierCase::kRemoteAddressMatch:
165+
actions_.emplace_back(
166+
new Router::RemoteAddressMatchAction(action.remote_address_match(), context));
167+
break;
164168
default:
165169
creation_status = absl::InvalidArgumentError(fmt::format(
166170
"Unsupported rate limit action: {}", static_cast<int>(action.action_specifier_case())));

test/common/common/matchers_test.cc

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,162 @@ TEST_F(FilterStateMatcher, NoMatchFilterStateAddressMatchIpv6) {
788788
EXPECT_FALSE((*filter_state_matcher)->match(filter_state));
789789
}
790790

791+
TEST_F(FilterStateMatcher, MatchFilterStateAddressMatchIpv4InvertMatch) {
792+
const std::string key = "test.key";
793+
envoy::type::matcher::v3::FilterStateMatcher matcher;
794+
matcher.set_key(key);
795+
auto* cidrv4 = matcher.mutable_address_match()->add_ranges();
796+
cidrv4->set_address_prefix("10.0.0.0");
797+
cidrv4->mutable_prefix_len()->set_value(8);
798+
matcher.mutable_address_match()->set_invert_match(true);
799+
800+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
801+
filter_state.setData(
802+
key,
803+
std::make_shared<Network::Address::InstanceAccessor>(
804+
Envoy::Network::Utility::parseInternetAddressNoThrow("192.168.1.1", 456, false)),
805+
StreamInfo::FilterState::StateType::Mutable);
806+
807+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
808+
ASSERT_TRUE(filter_state_matcher.ok());
809+
EXPECT_TRUE((*filter_state_matcher)->match(filter_state));
810+
}
811+
812+
TEST_F(FilterStateMatcher, NoMatchFilterStateAddressMatchIpv4InvertMatch) {
813+
const std::string key = "test.key";
814+
envoy::type::matcher::v3::FilterStateMatcher matcher;
815+
matcher.set_key(key);
816+
auto* cidrv4 = matcher.mutable_address_match()->add_ranges();
817+
cidrv4->set_address_prefix("10.0.0.0");
818+
cidrv4->mutable_prefix_len()->set_value(8);
819+
matcher.mutable_address_match()->set_invert_match(true);
820+
821+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
822+
filter_state.setData(
823+
key,
824+
std::make_shared<Network::Address::InstanceAccessor>(
825+
Envoy::Network::Utility::parseInternetAddressNoThrow("10.0.0.1", 456, false)),
826+
StreamInfo::FilterState::StateType::Mutable);
827+
828+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
829+
ASSERT_TRUE(filter_state_matcher.ok());
830+
EXPECT_FALSE((*filter_state_matcher)->match(filter_state));
831+
}
832+
833+
TEST_F(FilterStateMatcher, MatchFilterStateAddressMatchIpv6InvertMatch) {
834+
const std::string key = "test.key";
835+
envoy::type::matcher::v3::FilterStateMatcher matcher;
836+
matcher.set_key(key);
837+
auto* cidrv6 = matcher.mutable_address_match()->add_ranges();
838+
cidrv6->set_address_prefix("2001:db8::");
839+
cidrv6->mutable_prefix_len()->set_value(32);
840+
matcher.mutable_address_match()->set_invert_match(true);
841+
842+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
843+
filter_state.setData(
844+
key,
845+
std::make_shared<Network::Address::InstanceAccessor>(
846+
Envoy::Network::Utility::parseInternetAddressNoThrow("2001:db7::1", 8080, false)),
847+
StreamInfo::FilterState::StateType::Mutable);
848+
849+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
850+
ASSERT_TRUE(filter_state_matcher.ok());
851+
EXPECT_TRUE((*filter_state_matcher)->match(filter_state));
852+
}
853+
854+
TEST_F(FilterStateMatcher, NoMatchFilterStateAddressMatchIpv6InvertMatch) {
855+
const std::string key = "test.key";
856+
envoy::type::matcher::v3::FilterStateMatcher matcher;
857+
matcher.set_key(key);
858+
auto* cidrv6 = matcher.mutable_address_match()->add_ranges();
859+
cidrv6->set_address_prefix("2001:db8::");
860+
cidrv6->mutable_prefix_len()->set_value(32);
861+
matcher.mutable_address_match()->set_invert_match(true);
862+
863+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
864+
filter_state.setData(
865+
key,
866+
std::make_shared<Network::Address::InstanceAccessor>(
867+
Envoy::Network::Utility::parseInternetAddressNoThrow("2001:db8::1", 8080, false)),
868+
StreamInfo::FilterState::StateType::Mutable);
869+
870+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
871+
ASSERT_TRUE(filter_state_matcher.ok());
872+
EXPECT_FALSE((*filter_state_matcher)->match(filter_state));
873+
}
874+
875+
TEST_F(FilterStateMatcher, MatchFilterStateAddressMatchMultipleRangesInvertMatch) {
876+
const std::string key = "test.key";
877+
envoy::type::matcher::v3::FilterStateMatcher matcher;
878+
matcher.set_key(key);
879+
auto* cidrv4_1 = matcher.mutable_address_match()->add_ranges();
880+
cidrv4_1->set_address_prefix("10.0.0.0");
881+
cidrv4_1->mutable_prefix_len()->set_value(8);
882+
auto* cidrv4_2 = matcher.mutable_address_match()->add_ranges();
883+
cidrv4_2->set_address_prefix("192.168.0.0");
884+
cidrv4_2->mutable_prefix_len()->set_value(16);
885+
matcher.mutable_address_match()->set_invert_match(true);
886+
887+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
888+
filter_state.setData(
889+
key,
890+
std::make_shared<Network::Address::InstanceAccessor>(
891+
Envoy::Network::Utility::parseInternetAddressNoThrow("172.16.0.1", 456, false)),
892+
StreamInfo::FilterState::StateType::Mutable);
893+
894+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
895+
ASSERT_TRUE(filter_state_matcher.ok());
896+
EXPECT_TRUE((*filter_state_matcher)->match(filter_state));
897+
}
898+
899+
TEST_F(FilterStateMatcher, NoMatchFilterStateAddressMatchMultipleRangesInvertMatchFirstRange) {
900+
const std::string key = "test.key";
901+
envoy::type::matcher::v3::FilterStateMatcher matcher;
902+
matcher.set_key(key);
903+
auto* cidrv4_1 = matcher.mutable_address_match()->add_ranges();
904+
cidrv4_1->set_address_prefix("10.0.0.0");
905+
cidrv4_1->mutable_prefix_len()->set_value(8);
906+
auto* cidrv4_2 = matcher.mutable_address_match()->add_ranges();
907+
cidrv4_2->set_address_prefix("192.168.0.0");
908+
cidrv4_2->mutable_prefix_len()->set_value(16);
909+
matcher.mutable_address_match()->set_invert_match(true);
910+
911+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
912+
filter_state.setData(
913+
key,
914+
std::make_shared<Network::Address::InstanceAccessor>(
915+
Envoy::Network::Utility::parseInternetAddressNoThrow("10.0.0.1", 456, false)),
916+
StreamInfo::FilterState::StateType::Mutable);
917+
918+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
919+
ASSERT_TRUE(filter_state_matcher.ok());
920+
EXPECT_FALSE((*filter_state_matcher)->match(filter_state));
921+
}
922+
923+
TEST_F(FilterStateMatcher, NoMatchFilterStateAddressMatchMultipleRangesInvertMatchSecondRange) {
924+
const std::string key = "test.key";
925+
envoy::type::matcher::v3::FilterStateMatcher matcher;
926+
matcher.set_key(key);
927+
auto* cidrv4_1 = matcher.mutable_address_match()->add_ranges();
928+
cidrv4_1->set_address_prefix("10.0.0.0");
929+
cidrv4_1->mutable_prefix_len()->set_value(8);
930+
auto* cidrv4_2 = matcher.mutable_address_match()->add_ranges();
931+
cidrv4_2->set_address_prefix("192.168.0.0");
932+
cidrv4_2->mutable_prefix_len()->set_value(16);
933+
matcher.mutable_address_match()->set_invert_match(true);
934+
935+
StreamInfo::FilterStateImpl filter_state(StreamInfo::FilterState::LifeSpan::Connection);
936+
filter_state.setData(
937+
key,
938+
std::make_shared<Network::Address::InstanceAccessor>(
939+
Envoy::Network::Utility::parseInternetAddressNoThrow("192.168.1.1", 456, false)),
940+
StreamInfo::FilterState::StateType::Mutable);
941+
942+
auto filter_state_matcher = Matchers::FilterStateMatcher::create(matcher, context_);
943+
ASSERT_TRUE(filter_state_matcher.ok());
944+
EXPECT_FALSE((*filter_state_matcher)->match(filter_state));
945+
}
946+
791947
} // namespace
792948
} // namespace Matcher
793949
} // namespace Envoy

0 commit comments

Comments
 (0)