Skip to content

Commit

Permalink
local rate limit: add new rate_limits support to the filter (#36099)
Browse files Browse the repository at this point in the history
Commit Message: local rate limit: add new rate_limits api to the
filter's api
Additional Description:

In the previous local rate limit, the
[rate_limits](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-virtualhost-rate-limits)
field of route is used to generate the descriptor entries. Then the
generated entries will be used to match a token bucket which is
configured in the filter configs (route level, vhost level, etc).

However, it make the configuration very complex, and cannot cover some
common scenarios easily. For example, give a specific virtual host X and
a special route Y that under this virtual host X.

We want to provides a virtual host level rate limit for the specific
virtual host X, and a route level rate limit for the specific route Y.
We hope the configuration of virtual host could works for all routes
except the Y.

For most filters, this requirement could be achieved by getting the most
specific filter config and applying it. But for the local rate limit,
thing become very complex. Because the rate limit configuration is split
into `rate_limits` field of route and the filter config. The local rate
limit need to handle these relationship carefully.

This PR try to simplify it.

Risk Level: low.
Testing: n/a.
Docs Changes: n/a.
Release Notes: n/a.
Platform Specific Features: n/a.

---------

Signed-off-by: wangbaiping <[email protected]>
Signed-off-by: code <[email protected]>
Co-authored-by: Matt Klein <[email protected]>
  • Loading branch information
wbpcode and mattklein123 authored Oct 5, 2024
1 parent 33679e4 commit e486663
Show file tree
Hide file tree
Showing 17 changed files with 1,594 additions and 77 deletions.
1 change: 1 addition & 0 deletions api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2
api_proto_package(
deps = [
"//envoy/config/core/v3:pkg",
"//envoy/config/route/v3:pkg",
"//envoy/extensions/common/ratelimit/v3:pkg",
"//envoy/type/v3:pkg",
"@com_github_cncf_xds//udpa/annotations:pkg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ syntax = "proto3";
package envoy.extensions.filters.http.local_ratelimit.v3;

import "envoy/config/core/v3/base.proto";
import "envoy/config/route/v3/route_components.proto";
import "envoy/extensions/common/ratelimit/v3/ratelimit.proto";
import "envoy/type/v3/http_status.proto";
import "envoy/type/v3/token_bucket.proto";
Expand All @@ -22,7 +23,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// Local Rate limit :ref:`configuration overview <config_http_filters_local_rate_limit>`.
// [#extension: envoy.filters.http.local_ratelimit]

// [#next-free-field: 17]
// [#next-free-field: 18]
message LocalRateLimit {
// The human readable prefix to use when emitting stats.
string stat_prefix = 1 [(validate.rules).string = {min_len: 1}];
Expand Down Expand Up @@ -147,4 +148,23 @@ message LocalRateLimit {
// of the default ``UNAVAILABLE`` gRPC code for a rate limited gRPC call. The
// HTTP code will be 200 for a gRPC response.
bool rate_limited_as_resource_exhausted = 15;

// Rate limit configuration that is used to generate a list of descriptor entries based on
// the request context. The generated entries will be used to find one or multiple matched rate
// limit rule from the ``descriptors``.
// If this is set, then
// :ref:`VirtualHost.rate_limits<envoy_v3_api_field_config.route.v3.VirtualHost.rate_limits>` or
// :ref:`RouteAction.rate_limits<envoy_v3_api_field_config.route.v3.RouteAction.rate_limits>` fields
// will be ignored.
//
// .. note::
// Not all configuration fields of
// :ref:`rate limit config <envoy_v3_api_msg_config.route.v3.RateLimit>` is supported at here.
// Following fields are not supported:
//
// 1. :ref:`rate limit stage <envoy_v3_api_field_config.route.v3.RateLimit.stage>`.
// 2. :ref:`dynamic metadata <envoy_v3_api_field_config.route.v3.RateLimit.Action.dynamic_metadata>`.
// 3. :ref:`disable_key <envoy_v3_api_field_config.route.v3.RateLimit.disable_key>`.
// 4. :ref:`override limit <envoy_v3_api_field_config.route.v3.RateLimit.limit>`.
repeated config.route.v3.RateLimit rate_limits = 17;
}
8 changes: 8 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,14 @@ new_features:
change: |
Added two new methods ``oidsPeerCertificate()`` and ``oidsLocalCertificate()`` to SSL
connection object API :ref:`SSL connection info object <config_http_filters_lua_ssl_socket_info>`.
- area: local_ratelimit
change: |
Add the :ref:`rate_limits
<envoy_v3_api_field_extensions.filters.http.local_ratelimit.v3.LocalRateLimit.rate_limits>`
field to generate rate limit descriptors. If this field is set, the
:ref:`VirtualHost.rate_limits<envoy_v3_api_field_config.route.v3.VirtualHost.rate_limits>` or
:ref:`RouteAction.rate_limits<envoy_v3_api_field_config.route.v3.RouteAction.rate_limits>` fields
will be ignored.
- area: basic_auth
change: |
Added support to provide an override
Expand Down
62 changes: 21 additions & 41 deletions source/common/router/router_ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
#include "source/common/common/empty_string.h"
#include "source/common/config/metadata.h"
#include "source/common/config/utility.h"
#include "source/common/http/matching/data_impl.h"
#include "source/common/matcher/matcher.h"
#include "source/common/protobuf/utility.h"

namespace Envoy {
Expand All @@ -39,44 +37,24 @@ bool populateDescriptor(const std::vector<RateLimit::DescriptorProducerPtr>& act
return result;
}

class RateLimitDescriptorValidationVisitor
: public Matcher::MatchTreeValidationVisitor<Http::HttpMatchingData> {
public:
absl::Status performDataInputValidation(const Matcher::DataInputFactory<Http::HttpMatchingData>&,
absl::string_view) override {
return absl::OkStatus();
} // namespace

// Ratelimit::DescriptorProducer
bool MatchInputRateLimitDescriptor::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry,
const std::string&,
const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& info) const {
Http::Matching::HttpMatchingDataImpl data(info);
data.onRequestHeaders(headers);
auto result = data_input_->get(data);
if (!absl::holds_alternative<std::string>(result.data_)) {
return false;
}
};

class MatchInputRateLimitDescriptor : public RateLimit::DescriptorProducer {
public:
MatchInputRateLimitDescriptor(const std::string& descriptor_key,
Matcher::DataInputPtr<Http::HttpMatchingData>&& data_input)
: descriptor_key_(descriptor_key), data_input_(std::move(data_input)) {}

// Ratelimit::DescriptorProducer
bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&,
const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& info) const override {
Http::Matching::HttpMatchingDataImpl data(info);
data.onRequestHeaders(headers);
auto result = data_input_->get(data);
if (absl::holds_alternative<absl::monostate>(result.data_)) {
return false;
}
const std::string& str = absl::get<std::string>(result.data_);
if (!str.empty()) {
descriptor_entry = {descriptor_key_, str};
}
return true;
if (absl::string_view str = absl::get<std::string>(result.data_); !str.empty()) {
descriptor_entry = {descriptor_key_, std::string(str)};
}

private:
const std::string descriptor_key_;
Matcher::DataInputPtr<Http::HttpMatchingData> data_input_;
};

} // namespace
return true;
}

const uint64_t RateLimitPolicyImpl::MAX_STAGE_NUMBER = 10UL;

Expand Down Expand Up @@ -256,7 +234,8 @@ QueryParameterValueMatchAction::QueryParameterValueMatchAction(
: descriptor_value_(action.descriptor_value()),
descriptor_key_(!action.descriptor_key().empty() ? action.descriptor_key() : "query_match"),
expect_match_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(action, expect_match, true)),
action_query_parameters_(buildQueryParameterMatcherVector(action, context)) {}
action_query_parameters_(
buildQueryParameterMatcherVector(action.query_parameters(), context)) {}

bool QueryParameterValueMatchAction::populateDescriptor(
RateLimit::DescriptorEntry& descriptor_entry, const std::string&,
Expand All @@ -274,10 +253,11 @@ bool QueryParameterValueMatchAction::populateDescriptor(

std::vector<ConfigUtility::QueryParameterMatcherPtr>
QueryParameterValueMatchAction::buildQueryParameterMatcherVector(
const envoy::config::route::v3::RateLimit::Action::QueryParameterValueMatch& action,
const Protobuf::RepeatedPtrField<envoy::config::route::v3::QueryParameterMatcher>&
query_parameters,
Server::Configuration::CommonFactoryContext& context) {
std::vector<ConfigUtility::QueryParameterMatcherPtr> ret;
for (const auto& query_parameter : action.query_parameters()) {
for (const auto& query_parameter : query_parameters) {
ret.push_back(std::make_unique<ConfigUtility::QueryParameterMatcher>(query_parameter, context));
}
return ret;
Expand Down
31 changes: 30 additions & 1 deletion source/common/router/router_ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

#include "source/common/config/metadata.h"
#include "source/common/http/header_utility.h"
#include "source/common/http/matching/data_impl.h"
#include "source/common/matcher/matcher.h"
#include "source/common/network/cidr_range.h"
#include "source/common/protobuf/utility.h"
#include "source/common/router/config_utility.h"
Expand Down Expand Up @@ -146,6 +148,7 @@ class MetaDataAction : public RateLimit::DescriptorProducer {
MetaDataAction(const envoy::config::route::v3::RateLimit::Action::MetaData& action);
// for maintaining backward compatibility with the deprecated DynamicMetaData action
MetaDataAction(const envoy::config::route::v3::RateLimit::Action::DynamicMetaData& action);

// Ratelimit::DescriptorProducer
bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry,
const std::string& local_service_cluster,
Expand Down Expand Up @@ -198,7 +201,8 @@ class QueryParameterValueMatchAction : public RateLimit::DescriptorProducer {
const StreamInfo::StreamInfo& info) const override;

std::vector<ConfigUtility::QueryParameterMatcherPtr> buildQueryParameterMatcherVector(
const envoy::config::route::v3::RateLimit::Action::QueryParameterValueMatch& action,
const Protobuf::RepeatedPtrField<envoy::config::route::v3::QueryParameterMatcher>&
query_parameters,
Server::Configuration::CommonFactoryContext& context);

private:
Expand All @@ -208,6 +212,31 @@ class QueryParameterValueMatchAction : public RateLimit::DescriptorProducer {
const std::vector<ConfigUtility::QueryParameterMatcherPtr> action_query_parameters_;
};

class RateLimitDescriptorValidationVisitor
: public Matcher::MatchTreeValidationVisitor<Http::HttpMatchingData> {
public:
absl::Status performDataInputValidation(const Matcher::DataInputFactory<Http::HttpMatchingData>&,
absl::string_view) override {
return absl::OkStatus();
}
};

class MatchInputRateLimitDescriptor : public RateLimit::DescriptorProducer {
public:
MatchInputRateLimitDescriptor(const std::string& descriptor_key,
Matcher::DataInputPtr<Http::HttpMatchingData>&& data_input)
: descriptor_key_(descriptor_key), data_input_(std::move(data_input)) {}

// Ratelimit::DescriptorProducer
bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&,
const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& info) const override;

private:
const std::string descriptor_key_;
Matcher::DataInputPtr<Http::HttpMatchingData> data_input_;
};

/*
* Implementation of RateLimitPolicyEntry that holds the action for the configuration.
*/
Expand Down
22 changes: 22 additions & 0 deletions source/extensions/filters/common/ratelimit_config/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_library",
"envoy_extension_package",
)

licenses(["notice"]) # Apache 2

envoy_extension_package()

envoy_cc_library(
name = "ratelimit_config_lib",
srcs = ["ratelimit_config.cc"],
hdrs = ["ratelimit_config.h"],
deps = [
"//envoy/ratelimit:ratelimit_interface",
"//source/common/router:router_ratelimit_lib",
"@com_google_absl//absl/container:inlined_vector",
"@com_google_absl//absl/strings",
"@envoy_api//envoy/config/route/v3:pkg_cc_proto",
],
)
143 changes: 143 additions & 0 deletions source/extensions/filters/common/ratelimit_config/ratelimit_config.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#include "source/extensions/filters/common/ratelimit_config/ratelimit_config.h"

#include "source/common/config/utility.h"
#include "source/common/http/matching/data_impl.h"
#include "source/common/matcher/matcher.h"

namespace Envoy {
namespace Extensions {
namespace Filters {
namespace Common {
namespace RateLimit {

RateLimitPolicy::RateLimitPolicy(const ProtoRateLimit& config,
Server::Configuration::CommonFactoryContext& context,
absl::Status& creation_status, bool no_limit) {
if (config.has_stage() || !config.disable_key().empty()) {
creation_status =
absl::InvalidArgumentError("'stage' field and 'disable_key' field are not supported");
return;
}

if (config.has_limit()) {
if (no_limit) {
creation_status = absl::InvalidArgumentError("'limit' field is not supported");
return;
}
}

for (const ProtoRateLimit::Action& action : config.actions()) {
switch (action.action_specifier_case()) {
case ProtoRateLimit::Action::ActionSpecifierCase::kSourceCluster:
actions_.emplace_back(new Envoy::Router::SourceClusterAction());
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kDestinationCluster:
actions_.emplace_back(new Envoy::Router::DestinationClusterAction());
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kRequestHeaders:
actions_.emplace_back(new Envoy::Router::RequestHeadersAction(action.request_headers()));
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kRemoteAddress:
actions_.emplace_back(new Envoy::Router::RemoteAddressAction());
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kGenericKey:
actions_.emplace_back(new Envoy::Router::GenericKeyAction(action.generic_key()));
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kMetadata:
actions_.emplace_back(new Envoy::Router::MetaDataAction(action.metadata()));
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kHeaderValueMatch:
actions_.emplace_back(
new Envoy::Router::HeaderValueMatchAction(action.header_value_match(), context));
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kExtension: {
ProtobufMessage::ValidationVisitor& validator = context.messageValidationVisitor();
auto* factory =
Envoy::Config::Utility::getFactory<Envoy::RateLimit::DescriptorProducerFactory>(
action.extension());
if (!factory) {
// If no descriptor extension is found, fallback to using HTTP matcher
// input functions. Note that if the same extension name or type was
// dual registered as an extension descriptor and an HTTP matcher input
// function, the descriptor extension takes priority.
Router::RateLimitDescriptorValidationVisitor validation_visitor;
Matcher::MatchInputFactory<Http::HttpMatchingData> input_factory(validator,
validation_visitor);
Matcher::DataInputFactoryCb<Http::HttpMatchingData> data_input_cb =
input_factory.createDataInput(action.extension());
actions_.emplace_back(std::make_unique<Router::MatchInputRateLimitDescriptor>(
action.extension().name(), data_input_cb()));
break;
}
auto message = Envoy::Config::Utility::translateAnyToFactoryConfig(
action.extension().typed_config(), validator, *factory);
Envoy::RateLimit::DescriptorProducerPtr producer =
factory->createDescriptorProducerFromProto(*message, context);
if (producer) {
actions_.emplace_back(std::move(producer));
} else {
creation_status = absl::InvalidArgumentError(
absl::StrCat("Rate limit descriptor extension failed: ", action.extension().name()));
return;
}
break;
}
case ProtoRateLimit::Action::ActionSpecifierCase::kMaskedRemoteAddress:
actions_.emplace_back(new Router::MaskedRemoteAddressAction(action.masked_remote_address()));
break;
case ProtoRateLimit::Action::ActionSpecifierCase::kQueryParameterValueMatch:
actions_.emplace_back(new Router::QueryParameterValueMatchAction(
action.query_parameter_value_match(), context));
break;
default:
creation_status = absl::InvalidArgumentError(fmt::format(
"Unsupported rate limit action: {}", static_cast<int>(action.action_specifier_case())));
return;
}
}
}

void RateLimitPolicy::populateDescriptors(const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& stream_info,
const std::string& local_service_cluster,
RateLimitDescriptors& descriptors) const {
Envoy::RateLimit::LocalDescriptor descriptor;
for (const Envoy::RateLimit::DescriptorProducerPtr& action : actions_) {
Envoy::RateLimit::DescriptorEntry entry;
if (!action->populateDescriptor(entry, local_service_cluster, headers, stream_info)) {
return;
}
if (!entry.key_.empty()) {
descriptor.entries_.emplace_back(std::move(entry));
}
}
descriptors.emplace_back(std::move(descriptor));
}

RateLimitConfig::RateLimitConfig(const Protobuf::RepeatedPtrField<ProtoRateLimit>& configs,
Server::Configuration::CommonFactoryContext& context,
absl::Status& creation_status, bool no_limit) {
for (const ProtoRateLimit& config : configs) {
auto descriptor_generator =
std::make_unique<RateLimitPolicy>(config, context, creation_status, no_limit);
if (!creation_status.ok()) {
return;
}
rate_limit_policies_.emplace_back(std::move(descriptor_generator));
}
}

void RateLimitConfig::populateDescriptors(const Http::RequestHeaderMap& headers,
const StreamInfo::StreamInfo& stream_info,
const std::string& local_service_cluster,
RateLimitDescriptors& descriptors) const {
for (const auto& generator : rate_limit_policies_) {
generator->populateDescriptors(headers, stream_info, local_service_cluster, descriptors);
}
}

} // namespace RateLimit
} // namespace Common
} // namespace Filters
} // namespace Extensions
} // namespace Envoy
Loading

0 comments on commit e486663

Please sign in to comment.