Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/envoy/config/filter/http/rate_limit/v2/rate_limit.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package envoy.config.filter.http.rate_limit.v2;
option go_package = "v2";

import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this isn't needed anymore


import "validate/validate.proto";
import "gogoproto/gogo.proto";
Expand Down Expand Up @@ -34,4 +35,10 @@ message RateLimit {
// The timeout in milliseconds for the rate limit service RPC. If not
// set, this defaults to 20ms.
google.protobuf.Duration timeout = 4 [(gogoproto.stdduration) = true];

// The filter's behaviour in case the rate limiting service does
// not respond back. When it is set to true, Envoy will also allow traffic in case of
// communication failure between rate limiting service and the proxy.
// Defaults to true.
google.protobuf.BoolValue failure_mode_allow = 5;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's preferable to use bool instead of g.p.BoolValue. Can you change this to type bool and name failure_mode_deny (so that the default value is false)? Or is there a good reason for making this a tri-state?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no other reason except to be consistent with external_authz proto naming. I will change.

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ option go_package = "v2";

import "envoy/api/v2/ratelimit/ratelimit.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/wrappers.proto";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


import "validate/validate.proto";
import "gogoproto/gogo.proto";
Expand All @@ -26,4 +27,10 @@ message RateLimit {
// The timeout in milliseconds for the rate limit service RPC. If not
// set, this defaults to 20ms.
google.protobuf.Duration timeout = 4 [(gogoproto.stdduration) = true];

// The filter's behaviour in case the rate limiting service does
// not respond back. When it is set to true, Envoy will also allow traffic in case of
// communication failure between rate limiting service and the proxy.
// Defaults to true.
google.protobuf.BoolValue failure_mode_allow = 5;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}
5 changes: 5 additions & 0 deletions docs/root/configuration/http_filters/rate_limit_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ apply to a request. Each configuration results in a descriptor being sent to the
If the rate limit service is called, and the response for any of the descriptors is over limit, a
429 response is returned.

If there is an error in calling rate limit service or rate limit service returns an error and :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failure_mode_deny

set to false, a 500 response is returned.

.. _config_http_filters_rate_limit_composing_actions:

Composing Actions
Expand Down Expand Up @@ -108,6 +111,8 @@ The buffer filter outputs statistics in the *cluster.<route target cluster>.rate
ok, Counter, Total under limit responses from the rate limit service
error, Counter, Total errors contacting the rate limit service
over_limit, Counter, total over limit responses from the rate limit service
failure_mode_allowed, Counter, "Total requests that were error(s) but were allowed through because
of :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` set to true."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failure_mode_deny


Runtime
-------
Expand Down
2 changes: 2 additions & 0 deletions docs/root/configuration/network_filters/rate_limit_filter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ following statistics:
ok, Counter, Total under limit responses from the rate limit service
cx_closed, Counter, Total connections closed due to an over limit response from the rate limit service
active, Gauge, Total active requests to the rate limit service
failure_mode_allowed, Counter, "Total requests that were error(s) but were allowed through because

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

failure_mode_deny: I'm fine with the stat being named either way, but the doc link needs to be updated.

of :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` set to true."

Runtime
-------
Expand Down
2 changes: 2 additions & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ Version history
* upstream: require opt-in to use the :ref:`x-envoy-orignal-dst-host <config_http_conn_man_headers_x-envoy-original-dst-host>` header
for overriding destination address when using the :ref:`Original Destination <arch_overview_load_balancing_types_original_destination>`
load balancing policy.
* ratelimit: added :ref:`failure_mode_allow <envoy_api_msg_config.filter.http.rate_limit.v2.RateLimit>` option to control traffic flow in
case of rate limit service error.

1.7.0
===============
Expand Down
4 changes: 3 additions & 1 deletion include/envoy/request_info/request_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ enum ResponseFlag {
RateLimited = 0x800,
// Request was unauthorized by external authorization service.
UnauthorizedExternalService = 0x1000,
// Unable to call Ratelimiting service.
RateLimitingServiceError = 0x1200,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: RateLimitServiceError

// ATTENTION: MAKE SURE THIS REMAINS EQUAL TO THE LAST FLAG.
LastFlag = UnauthorizedExternalService
LastFlag = RateLimitingServiceError
};

/**
Expand Down
2 changes: 1 addition & 1 deletion source/common/request_info/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ void ResponseFlagUtils::appendString(std::string& result, const std::string& app
const std::string ResponseFlagUtils::toShortString(const RequestInfo& request_info) {
std::string result;

static_assert(ResponseFlag::LastFlag == 0x1000, "A flag has been added. Fix this code.");
static_assert(ResponseFlag::LastFlag == 0x1200, "A flag has been added. Fix this code.");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this, and all the other asserts related to the LastFlag, we not only need to fix the value. There is accompanying code that needs to be fixed. For example here we need to add the new flag to the mapping between the enum and the string.

Any suggestions appreciated on how to make this maintenance more obvious.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. Sorry..did not realize that. Will change


if (request_info.hasResponseFlag(ResponseFlag::FailedLocalHealthCheck)) {
appendString(result, FAILED_LOCAL_HEALTH_CHECK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ void HttpGrpcAccessLog::responseFlagsToAccessLogResponseFlags(
envoy::data::accesslog::v2::AccessLogCommon& common_access_log,
const RequestInfo::RequestInfo& request_info) {

static_assert(RequestInfo::ResponseFlag::LastFlag == 0x1000,
static_assert(RequestInfo::ResponseFlag::LastFlag == 0x1200,
"A flag has been added. Fix this code.");

if (request_info.hasResponseFlag(RequestInfo::ResponseFlag::FailedLocalHealthCheck)) {
Expand Down
10 changes: 10 additions & 0 deletions source/extensions/filters/http/ratelimit/ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ void Filter::complete(RateLimit::LimitStatus status) {
state_ = State::Responded;
callbacks_->sendLocalReply(Http::Code::TooManyRequests, "", nullptr);
callbacks_->requestInfo().setResponseFlag(RequestInfo::ResponseFlag::RateLimited);
} else if (status == RateLimit::LimitStatus::Error) {
if (config_->failureModeAllow()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not change the method name in config because it seems more readable like this. LMK if you think otherwise

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine as is

cluster_->statsScope().counter("ratelimit.failure_mode_allowed").inc();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this new counter at all? We already have a counter for ratelimit service failures, and presumably the user knows which way they have this configured (failure_mode_deny value). In one case this counter will always be zero, in the other it will be equal to ratelimit.error. So is there value in this new counter?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would help in general, while looking at stats user do not have to check back on what his configuration is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'm fine with that.

callbacks_->continueDecoding();
} else {
state_ = State::Responded;
callbacks_->sendLocalReply(Http::Code::InternalServerError, "", nullptr);
callbacks_->requestInfo().setResponseFlag(
RequestInfo::ResponseFlag::RateLimitingServiceError);
}
} else if (!initiating_call_) {
callbacks_->continueDecoding();
}
Expand Down
7 changes: 5 additions & 2 deletions source/extensions/filters/http/ratelimit/ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class FilterConfig {
: domain_(config.domain()), stage_(static_cast<uint64_t>(config.stage())),
request_type_(config.request_type().empty() ? stringToType("both")
: stringToType(config.request_type())),
local_info_(local_info), scope_(scope), runtime_(runtime), cm_(cm) {}

local_info_(local_info), scope_(scope), runtime_(runtime), cm_(cm),
failure_mode_allow_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, failure_mode_allow, true)) {}
const std::string& domain() const { return domain_; }
const LocalInfo::LocalInfo& localInfo() const { return local_info_; }
uint64_t stage() const { return stage_; }
Expand All @@ -47,6 +47,8 @@ class FilterConfig {
Upstream::ClusterManager& cm() { return cm_; }
FilterRequestType requestType() const { return request_type_; }

bool failureModeAllow() const { return failure_mode_allow_; }

private:
static FilterRequestType stringToType(const std::string& request_type) {
if (request_type == "internal") {
Expand All @@ -66,6 +68,7 @@ class FilterConfig {
Stats::Scope& scope_;
Runtime::Loader& runtime_;
Upstream::ClusterManager& cm_;
bool failure_mode_allow_;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

};

typedef std::shared_ptr<FilterConfig> FilterConfigSharedPtr;
Expand Down
13 changes: 10 additions & 3 deletions source/extensions/filters/network/ratelimit/ratelimit.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ namespace RateLimitFilter {
Config::Config(const envoy::config::filter::network::rate_limit::v2::RateLimit& config,
Stats::Scope& scope, Runtime::Loader& runtime)
: domain_(config.domain()), stats_(generateStats(config.stat_prefix(), scope)),
runtime_(runtime) {

runtime_(runtime),
failure_mode_allow_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, failure_mode_allow, true)) {
for (const auto& descriptor : config.descriptors()) {
RateLimit::Descriptor new_descriptor;
for (const auto& entry : descriptor.entries()) {
Expand Down Expand Up @@ -85,11 +85,18 @@ void Filter::complete(RateLimit::LimitStatus status) {
break;
}

// We fail open if there is an error contacting the service.
if (status == RateLimit::LimitStatus::OverLimit &&
config_->runtime().snapshot().featureEnabled("ratelimit.tcp_filter_enforcing", 100)) {
config_->stats().cx_closed_.inc();
filter_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush);
} else if (status == RateLimit::LimitStatus::Error) {
if (config_->failureModeAllow()) {
config_->stats().failure_mode_allowed_.inc();
filter_callbacks_->continueReading();
} else {
config_->stats().cx_closed_.inc();
filter_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush);
}
} else {
// We can get completion inline, so only call continue if that isn't happening.
if (!calling_limit_) {
Expand Down
3 changes: 3 additions & 0 deletions source/extensions/filters/network/ratelimit/ratelimit.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ namespace RateLimitFilter {
COUNTER(error) \
COUNTER(over_limit) \
COUNTER(ok) \
COUNTER(failure_mode_allowed) \
COUNTER(cx_closed) \
GAUGE (active)
// clang-format on
Expand All @@ -49,6 +50,7 @@ class Config {
const std::vector<RateLimit::Descriptor>& descriptors() { return descriptors_; }
Runtime::Loader& runtime() { return runtime_; }
const InstanceStats& stats() { return stats_; }
bool failureModeAllow() const { return failure_mode_allow_; };

private:
static InstanceStats generateStats(const std::string& name, Stats::Scope& scope);
Expand All @@ -57,6 +59,7 @@ class Config {
std::vector<RateLimit::Descriptor> descriptors_;
const InstanceStats stats_;
Runtime::Loader& runtime_;
bool failure_mode_allow_;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

};

typedef std::shared_ptr<Config> ConfigSharedPtr;
Expand Down
2 changes: 1 addition & 1 deletion test/common/access_log/access_log_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,7 @@ name: envoy.file_access_log
path: /dev/null
)EOF";

static_assert(RequestInfo::ResponseFlag::LastFlag == 0x1000,
static_assert(RequestInfo::ResponseFlag::LastFlag == 0x1200,
"A flag has been added. Fix this code.");

std::vector<RequestInfo::ResponseFlag> all_response_flags = {
Expand Down
4 changes: 2 additions & 2 deletions test/common/request_info/utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Envoy {
namespace RequestInfo {

TEST(ResponseFlagUtilsTest, toShortStringConversion) {
static_assert(ResponseFlag::LastFlag == 0x1000, "A flag has been added. Fix this code.");
static_assert(ResponseFlag::LastFlag == 0x1200, "A flag has been added. Fix this code.");

std::vector<std::pair<ResponseFlag, std::string>> expected = {
std::make_pair(ResponseFlag::FailedLocalHealthCheck, "LH"),
Expand Down Expand Up @@ -58,7 +58,7 @@ TEST(ResponseFlagUtilsTest, toShortStringConversion) {
}

TEST(ResponseFlagsUtilsTest, toResponseFlagConversion) {
static_assert(ResponseFlag::LastFlag == 0x1000, "A flag has been added. Fix this code.");
static_assert(ResponseFlag::LastFlag == 0x1200, "A flag has been added. Fix this code.");

std::vector<std::pair<std::string, ResponseFlag>> expected = {
std::make_pair("LH", ResponseFlag::FailedLocalHealthCheck),
Expand Down
36 changes: 35 additions & 1 deletion test/extensions/filters/http/ratelimit/ratelimit_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,13 @@ class HttpRateLimitFilterTest : public testing::Test {
.WillByDefault(Return(true));
}

void SetUpTest(const std::string json) {
void SetUpTest(const std::string json, const bool failure_mode = true) {
Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json);
envoy::config::filter::http::rate_limit::v2::RateLimit proto_config{};
Config::FilterJson::translateHttpRateLimitFilter(*json_config, proto_config);
// TODO(ramaraochavali): move filter_config_ to yaml and use a different config for

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go ahead and do this real quick.

// failure_mode.
proto_config.mutable_failure_mode_allow()->set_value(failure_mode);
config_.reset(new FilterConfig(proto_config, local_info_, stats_store_, runtime_, cm_));

client_ = new RateLimit::MockClient();
Expand Down Expand Up @@ -239,6 +242,37 @@ TEST_F(HttpRateLimitFilterTest, ErrorResponse) {
EXPECT_EQ(
1U,
cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ratelimit.error").value());
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
.counter("ratelimit.failure_mode_allowed")
.value());
}

TEST_F(HttpRateLimitFilterTest, ErrorResponseWithFailureAllowModeOff) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: FailureModeAllow for naming consistency

SetUpTest(filter_config_, false);
InSequence s;

EXPECT_CALL(route_rate_limit_, populateDescriptors(_, _, _, _, _))
.WillOnce(SetArgReferee<1>(descriptor_));
EXPECT_CALL(*client_, limit(_, _, _, _))
.WillOnce(WithArgs<0>(Invoke([&](RateLimit::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));

EXPECT_EQ(Http::FilterHeadersStatus::StopIteration,
filter_->decodeHeaders(request_headers_, false));

request_callbacks_->complete(RateLimit::LimitStatus::Error);

EXPECT_CALL(filter_callbacks_.request_info_,
setResponseFlag(RequestInfo::ResponseFlag::RateLimitingServiceError))
.Times(0);

EXPECT_EQ(
1U,
cm_.thread_local_cluster_.cluster_.info_->stats_store_.counter("ratelimit.error").value());
EXPECT_EQ(0U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
.counter("ratelimit.failure_mode_allowed")
.value());
}

TEST_F(HttpRateLimitFilterTest, LimitResponse) {
Expand Down
40 changes: 39 additions & 1 deletion test/extensions/filters/network/ratelimit/ratelimit_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ namespace RateLimitFilter {

class RateLimitFilterTest : public testing::Test {
public:
RateLimitFilterTest() {
RateLimitFilterTest() {}

void SetUp() override {
std::string json = R"EOF(
{
"domain": "foo",
Expand All @@ -53,6 +55,8 @@ class RateLimitFilterTest : public testing::Test {
Json::ObjectSharedPtr json_config = Json::Factory::loadFromString(json);
envoy::config::filter::network::rate_limit::v2::RateLimit proto_config{};
Envoy::Config::FilterJson::translateTcpRateLimitFilter(*json_config, proto_config);
// TODO(ramaraochavali): move the config to yaml and use a different config for failure_mode.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go ahead and do this, and change these tests to use a SetUpTest(const std::string& yaml) setup function. Prevents the subclass addition here, as you point out in the TODO.

proto_config.mutable_failure_mode_allow()->set_value(failure_mode_);
config_.reset(new Config(proto_config, stats_store_, runtime_));
client_ = new RateLimit::MockClient();
filter_.reset(new Filter(config_, RateLimit::ClientPtr{client_}));
Expand All @@ -76,6 +80,16 @@ class RateLimitFilterTest : public testing::Test {
std::unique_ptr<Filter> filter_;
NiceMock<Network::MockReadFilterCallbacks> filter_callbacks_;
RateLimit::RequestCallbacks* request_callbacks_{};
bool failure_mode_{true};
};

class RateLimitFilterFailureModeTest : public RateLimitFilterTest {
public:
RateLimitFilterFailureModeTest() {
failure_mode_ = false;
std::cout << "setting failure mode to false..."
<< "\n";
}
};

TEST_F(RateLimitFilterTest, BadRatelimitConfig) {
Expand Down Expand Up @@ -194,6 +208,7 @@ TEST_F(RateLimitFilterTest, Error) {

EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.total").value());
EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.error").value());
EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.failure_mode_allowed").value());
}

TEST_F(RateLimitFilterTest, Disconnect) {
Expand Down Expand Up @@ -247,6 +262,29 @@ TEST_F(RateLimitFilterTest, RuntimeDisable) {
EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data, false));
}

TEST_F(RateLimitFilterFailureModeTest, ErrorResponseWithFailureAllowModeOff) {
InSequence s;

EXPECT_CALL(*client_, limit(_, "foo", _, _))
.WillOnce(WithArgs<0>(Invoke([&](RateLimit::RequestCallbacks& callbacks) -> void {
request_callbacks_ = &callbacks;
})));

EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onNewConnection());
Buffer::OwnedImpl data("hello");
EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onData(data, false));
request_callbacks_->complete(RateLimit::LimitStatus::Error);

EXPECT_EQ(Network::FilterStatus::Continue, filter_->onData(data, false));

EXPECT_CALL(*client_, cancel()).Times(0);
filter_callbacks_.connection_.raiseEvent(Network::ConnectionEvent::RemoteClose);

EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.total").value());
EXPECT_EQ(1U, stats_store_.counter("ratelimit.name.error").value());
EXPECT_EQ(0U, stats_store_.counter("ratelimit.name.failure_mode_allowed").value());
}

} // namespace RateLimitFilter
} // namespace NetworkFilters
} // namespace Extensions
Expand Down
Loading