Skip to content

Commit

Permalink
Add support for X-RateLimit-* headers in ratelimit filter (envoyproxy…
Browse files Browse the repository at this point in the history
…#12410)

Adds support for X-RateLimit-* headers described in the draft RFC. The X-RateLimit-Limit header contains the quota-policy per RFC. The descriptor name is included in the quota policy under the name key. X-RateLimit-Reset header is emitted, but it would need a followup in the ratelimit service, which I will do once this is merged.

Signed-off-by: Petr Pchelko <ppchelko@wikimedia.org>
  • Loading branch information
Pchelolo authored and chaoqinli committed Aug 7, 2020
1 parent 39804de commit 021f985
Show file tree
Hide file tree
Showing 28 changed files with 683 additions and 55 deletions.
37 changes: 36 additions & 1 deletion api/envoy/extensions/filters/http/ratelimit/v3/rate_limit.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
// Rate limit :ref:`configuration overview <config_http_filters_rate_limit>`.
// [#extension: envoy.filters.http.ratelimit]

// [#next-free-field: 8]
// [#next-free-field: 9]
message RateLimit {
option (udpa.annotations.versioning).previous_message_type =
"envoy.config.filter.http.rate_limit.v2.RateLimit";

// Defines the version of the standard to use for X-RateLimit headers.
enum XRateLimitHeadersRFCVersion {
// X-RateLimit headers disabled.
OFF = 0;

// Use `draft RFC Version 02 <https://tools.ietf.org/id/draft-polli-ratelimit-headers-02.html>`_.
DRAFT_VERSION_02 = 1;
}

// The rate limit domain to use when calling the rate limit service.
string domain = 1 [(validate.rules).string = {min_bytes: 1}];

Expand Down Expand Up @@ -64,4 +73,30 @@ message RateLimit {
// success.
config.ratelimit.v3.RateLimitServiceConfig rate_limit_service = 7
[(validate.rules).message = {required: true}];

// Defines the standard version to use for X-RateLimit headers emitted by the filter:
//
// * ``X-RateLimit-Limit`` - indicates the request-quota associated to the
// client in the current time-window followed by the description of the
// quota policy. The values are returned by the rate limiting service in
// :ref:`current_limit<envoy_v3_api_field_service.ratelimit.v3.RateLimitResponse.DescriptorStatus.current_limit>`
// field. Example: `10, 10;w=1;name="per-ip", 1000;w=3600`.
// * ``X-RateLimit-Remaining`` - indicates the remaining requests in the
// current time-window. The values are returned by the rate limiting service
// in :ref:`limit_remaining<envoy_v3_api_field_service.ratelimit.v3.RateLimitResponse.DescriptorStatus.limit_remaining>`
// field.
// * ``X-RateLimit-Reset`` - indicates the number of seconds until reset of
// the current time-window. The values are returned by the rate limiting service
// in :ref:`duration_until_reset<envoy_v3_api_field_service.ratelimit.v3.RateLimitResponse.DescriptorStatus.duration_until_reset>`
// field.
//
// In case rate limiting policy specifies more then one time window, the values
// above represent the window that is closest to reaching its limit.
//
// For more information about the headers specification see selected version of
// the `draft RFC <https://tools.ietf.org/id/draft-polli-ratelimit-headers-02.html>`_.
//
// Disabled by default.
XRateLimitHeadersRFCVersion enable_x_ratelimit_headers = 8
[(validate.rules).enum = {defined_only: true}];
}
5 changes: 5 additions & 0 deletions api/envoy/service/ratelimit/v3/rls.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package envoy.service.ratelimit.v3;
import "envoy/config/core/v3/base.proto";
import "envoy/extensions/common/ratelimit/v3/ratelimit.proto";

import "google/protobuf/duration.proto";

import "udpa/annotations/status.proto";
import "udpa/annotations/versioning.proto";
import "validate/validate.proto";
Expand Down Expand Up @@ -110,6 +112,9 @@ message RateLimitResponse {

// The limit remaining in the current time unit.
uint32 limit_remaining = 3;

// Duration until reset of the current limit window.
google.protobuf.Duration duration_until_reset = 4;
}

// The overall response code which takes into account all of the descriptors that were passed
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ New Features
* load balancer: added a :ref:`configuration<envoy_v3_api_msg_config.cluster.v3.Cluster.LeastRequestLbConfig>` option to specify the active request bias used by the least request load balancer.
* lua: added Lua APIs to access :ref:`SSL connection info <config_http_filters_lua_ssl_socket_info>` object.
* postgres network filter: :ref:`metadata <config_network_filters_postgres_proxy_dynamic_metadata>` is produced based on SQL query.
* ratelimit: added :ref:`enable_x_ratelimit_headers <envoy_v3_api_msg_extensions.filters.http.ratelimit.v3.RateLimit>` option to enable `X-RateLimit-*` headers as defined in `draft RFC <https://tools.ietf.org/id/draft-polli-ratelimit-headers-02.html>`_.
* router: added new
:ref:`envoy-ratelimited<config_http_filters_router_retry_policy-envoy-ratelimited>`
retry policy, which allows retrying envoy's own rate limited responses.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions generated_api_shadow/envoy/service/ratelimit/v3/rls.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions source/extensions/filters/common/ratelimit/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ envoy_cc_library(
"//include/envoy/singleton:manager_interface",
"//include/envoy/tracing:http_tracer_interface",
"//source/common/stats:symbol_table_lib",
"@envoy_api//envoy/service/ratelimit/v3:pkg_cc_proto",
],
)

Expand Down
8 changes: 7 additions & 1 deletion source/extensions/filters/common/ratelimit/ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "envoy/common/pure.h"
#include "envoy/ratelimit/ratelimit.h"
#include "envoy/service/ratelimit/v3/rls.pb.h"
#include "envoy/singleton/manager.h"
#include "envoy/tracing/http_tracer.h"

Expand All @@ -30,6 +31,10 @@ enum class LimitStatus {
OverLimit
};

using DescriptorStatusList =
std::vector<envoy::service::ratelimit::v3::RateLimitResponse_DescriptorStatus>;
using DescriptorStatusListPtr = std::unique_ptr<DescriptorStatusList>;

/**
* Async callbacks used during limit() calls.
*/
Expand All @@ -41,7 +46,8 @@ class RequestCallbacks {
* Called when a limit request is complete. The resulting status,
* response headers and request headers to be forwarded to the upstream are supplied.
*/
virtual void complete(LimitStatus status, Http::ResponseHeaderMapPtr&& response_headers_to_add,
virtual void complete(LimitStatus status, DescriptorStatusListPtr&& descriptor_statuses,
Http::ResponseHeaderMapPtr&& response_headers_to_add,
Http::RequestHeaderMapPtr&& request_headers_to_add) PURE;
};

Expand Down
8 changes: 5 additions & 3 deletions source/extensions/filters/common/ratelimit/ratelimit_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

#include "envoy/config/core/v3/grpc_service.pb.h"
#include "envoy/extensions/common/ratelimit/v3/ratelimit.pb.h"
#include "envoy/service/ratelimit/v3/rls.pb.h"
#include "envoy/stats/scope.h"

#include "common/common/assert.h"
Expand Down Expand Up @@ -101,15 +100,18 @@ void GrpcClientImpl::onSuccess(
request_headers_to_add->addCopy(Http::LowerCaseString(h.key()), h.value());
}
}
callbacks_->complete(status, std::move(response_headers_to_add),

DescriptorStatusListPtr descriptor_statuses = std::make_unique<DescriptorStatusList>(
response->statuses().begin(), response->statuses().end());
callbacks_->complete(status, std::move(descriptor_statuses), std::move(response_headers_to_add),
std::move(request_headers_to_add));
callbacks_ = nullptr;
}

void GrpcClientImpl::onFailure(Grpc::Status::GrpcStatus status, const std::string&,
Tracing::Span&) {
ASSERT(status != Grpc::Status::WellKnownGrpcStatus::Ok);
callbacks_->complete(LimitStatus::Error, nullptr, nullptr);
callbacks_->complete(LimitStatus::Error, nullptr, nullptr, nullptr);
callbacks_ = nullptr;
}

Expand Down
11 changes: 11 additions & 0 deletions source/extensions/filters/http/ratelimit/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ envoy_cc_library(
srcs = ["ratelimit.cc"],
hdrs = ["ratelimit.h"],
deps = [
":ratelimit_headers_lib",
"//include/envoy/http:codes_interface",
"//include/envoy/ratelimit:ratelimit_interface",
"//source/common/common:assert_lib",
Expand All @@ -30,6 +31,16 @@ envoy_cc_library(
],
)

envoy_cc_library(
name = "ratelimit_headers_lib",
srcs = ["ratelimit_headers.cc"],
hdrs = ["ratelimit_headers.h"],
deps = [
"//source/common/http:header_map_lib",
"//source/extensions/filters/common/ratelimit:ratelimit_client_interface",
],
)

envoy_cc_extension(
name = "config",
srcs = ["config.cc"],
Expand Down
14 changes: 14 additions & 0 deletions source/extensions/filters/http/ratelimit/ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
#include "common/http/header_utility.h"
#include "common/router/config_impl.h"

#include "extensions/filters/http/ratelimit/ratelimit_headers.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
Expand Down Expand Up @@ -125,6 +127,7 @@ void Filter::onDestroy() {
}

void Filter::complete(Filters::Common::RateLimit::LimitStatus status,
Filters::Common::RateLimit::DescriptorStatusListPtr&& descriptor_statuses,
Http::ResponseHeaderMapPtr&& response_headers_to_add,
Http::RequestHeaderMapPtr&& request_headers_to_add) {
state_ = State::Complete;
Expand Down Expand Up @@ -161,6 +164,17 @@ void Filter::complete(Filters::Common::RateLimit::LimitStatus status,
break;
}

if (config_->enableXRateLimitHeaders()) {
Http::ResponseHeaderMapPtr rate_limit_headers =
XRateLimitHeaderUtils::create(std::move(descriptor_statuses));
if (response_headers_to_add_ == nullptr) {
response_headers_to_add_ = Http::ResponseHeaderMapImpl::create();
}
Http::HeaderUtility::addHeaders(*response_headers_to_add_, *rate_limit_headers);
} else {
descriptor_statuses = nullptr;
}

if (status == Filters::Common::RateLimit::LimitStatus::OverLimit &&
config_->runtime().snapshot().featureEnabled("ratelimit.http_filter_enforcing", 100)) {
state_ = State::Responded;
Expand Down
6 changes: 6 additions & 0 deletions source/extensions/filters/http/ratelimit/ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class FilterConfig {
: stringToType(config.request_type())),
local_info_(local_info), scope_(scope), runtime_(runtime),
failure_mode_deny_(config.failure_mode_deny()),
enable_x_ratelimit_headers_(
config.enable_x_ratelimit_headers() ==
envoy::extensions::filters::http::ratelimit::v3::RateLimit::DRAFT_VERSION_02),
rate_limited_grpc_status_(
config.rate_limited_as_resource_exhausted()
? absl::make_optional(Grpc::Status::WellKnownGrpcStatus::ResourceExhausted)
Expand All @@ -55,6 +58,7 @@ class FilterConfig {
Stats::Scope& scope() { return scope_; }
FilterRequestType requestType() const { return request_type_; }
bool failureModeAllow() const { return !failure_mode_deny_; }
bool enableXRateLimitHeaders() const { return enable_x_ratelimit_headers_; }
const absl::optional<Grpc::Status::GrpcStatus> rateLimitedGrpcStatus() const {
return rate_limited_grpc_status_;
}
Expand All @@ -80,6 +84,7 @@ class FilterConfig {
Stats::Scope& scope_;
Runtime::Loader& runtime_;
const bool failure_mode_deny_;
const bool enable_x_ratelimit_headers_;
const absl::optional<Grpc::Status::GrpcStatus> rate_limited_grpc_status_;
Http::Context& http_context_;
Filters::Common::RateLimit::StatNames stat_names_;
Expand Down Expand Up @@ -117,6 +122,7 @@ class Filter : public Http::StreamFilter, public Filters::Common::RateLimit::Req

// RateLimit::RequestCallbacks
void complete(Filters::Common::RateLimit::LimitStatus status,
Filters::Common::RateLimit::DescriptorStatusListPtr&& descriptor_statuses,
Http::ResponseHeaderMapPtr&& response_headers_to_add,
Http::RequestHeaderMapPtr&& request_headers_to_add) override;

Expand Down
82 changes: 82 additions & 0 deletions source/extensions/filters/http/ratelimit/ratelimit_headers.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include "extensions/filters/http/ratelimit/ratelimit_headers.h"

#include "common/http/header_map_impl.h"

#include "absl/strings/substitute.h"

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace RateLimitFilter {

Http::ResponseHeaderMapPtr XRateLimitHeaderUtils::create(
Filters::Common::RateLimit::DescriptorStatusListPtr&& descriptor_statuses) {
Http::ResponseHeaderMapPtr result = Http::ResponseHeaderMapImpl::create();
if (!descriptor_statuses || descriptor_statuses->empty()) {
descriptor_statuses = nullptr;
return result;
}

absl::optional<envoy::service::ratelimit::v3::RateLimitResponse_DescriptorStatus>
min_remaining_limit_status;
std::string quota_policy;
for (auto&& status : *descriptor_statuses) {
if (!status.has_current_limit()) {
continue;
}
if (!min_remaining_limit_status ||
status.limit_remaining() < min_remaining_limit_status.value().limit_remaining()) {
min_remaining_limit_status.emplace(status);
}
const uint32_t window = convertRateLimitUnit(status.current_limit().unit());
// Constructing the quota-policy per RFC
// https://tools.ietf.org/id/draft-polli-ratelimit-headers-02.html#name-ratelimit-limit
// Example of the result: `, 10;w=1;name="per-ip", 1000;w=3600`
if (window) {
// For each descriptor status append `<LIMIT>;w=<WINDOW_IN_SECONDS>`
absl::SubstituteAndAppend(&quota_policy, ", $0;$1=$2",
status.current_limit().requests_per_unit(),
XRateLimitHeaders::get().QuotaPolicyKeys.Window, window);
if (!status.current_limit().name().empty()) {
// If the descriptor has a name, append `;name="<DESCRIPTOR_NAME>"`
absl::SubstituteAndAppend(&quota_policy, ";$0=\"$1\"",
XRateLimitHeaders::get().QuotaPolicyKeys.Name,
status.current_limit().name());
}
}
}

if (min_remaining_limit_status) {
const std::string rate_limit_limit = absl::StrCat(
min_remaining_limit_status.value().current_limit().requests_per_unit(), quota_policy);
result->addReferenceKey(XRateLimitHeaders::get().XRateLimitLimit, rate_limit_limit);
result->addReferenceKey(XRateLimitHeaders::get().XRateLimitRemaining,
min_remaining_limit_status.value().limit_remaining());
result->addReferenceKey(XRateLimitHeaders::get().XRateLimitReset,
min_remaining_limit_status.value().duration_until_reset().seconds());
}
descriptor_statuses = nullptr;
return result;
}

uint32_t XRateLimitHeaderUtils::convertRateLimitUnit(
const envoy::service::ratelimit::v3::RateLimitResponse::RateLimit::Unit unit) {
switch (unit) {
case envoy::service::ratelimit::v3::RateLimitResponse::RateLimit::SECOND:
return 1;
case envoy::service::ratelimit::v3::RateLimitResponse::RateLimit::MINUTE:
return 60;
case envoy::service::ratelimit::v3::RateLimitResponse::RateLimit::HOUR:
return 60 * 60;
case envoy::service::ratelimit::v3::RateLimitResponse::RateLimit::DAY:
return 24 * 60 * 60;
case envoy::service::ratelimit::v3::RateLimitResponse::RateLimit::UNKNOWN:
default:
return 0;
}
}

} // namespace RateLimitFilter
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
Loading

0 comments on commit 021f985

Please sign in to comment.