Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions api/envoy/api/v2/core/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ message HttpProtocolOptions {
// maximum number of request headers allowed is 100. Requests that exceed this limit will receive
// a 431 response for HTTP/1.x and cause a stream reset for HTTP/2.
google.protobuf.UInt32Value max_headers_count = 2 [(validate.rules).uint32 = {gte: 1}];

// Total duration to keep alive an HTTP request/response stream. If the time limit is reached the stream will be
// reset independent of any other timeouts. If not specified, this value is not set.
// The current implementation implements this timeout on downstream connections only.
// [#comment:TODO(shikugawa): add this functionality to upstream.]
google.protobuf.Duration max_stream_duration = 4;
}

// [#next-free-field: 6]
Expand Down
6 changes: 6 additions & 0 deletions api/envoy/config/core/v3/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ message HttpProtocolOptions {
// maximum number of request headers allowed is 100. Requests that exceed this limit will receive
// a 431 response for HTTP/1.x and cause a stream reset for HTTP/2.
google.protobuf.UInt32Value max_headers_count = 2 [(validate.rules).uint32 = {gte: 1}];

// Total duration to keep alive an HTTP request/response stream. If the time limit is reached the stream will be
// reset independent of any other timeouts. If not specified, this value is not set.
// The current implementation implements this timeout on downstream connections only.
// [#comment:TODO(shikugawa): add this functionality to upstream.]
google.protobuf.Duration max_stream_duration = 4;
}

// [#next-free-field: 6]
Expand Down
1 change: 1 addition & 0 deletions docs/root/configuration/http/http_conn_man/stats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ statistics:
downstream_rq_ws_on_non_ws_route, Counter, Total WebSocket upgrade requests rejected by non WebSocket routes
downstream_rq_time, Histogram, Total time for request and response (milliseconds)
downstream_rq_idle_timeout, Counter, Total requests closed due to idle timeout
downstream_rq_max_duration_reached, Counter, Total requests closed due to max duration reached
downstream_rq_timeout, Counter, Total requests closed due to a timeout on the request path
downstream_rq_overload_close, Counter, Total requests closed due to Envoy overload
rs_too_large, Counter, Total response errors due to buffering an overly large body
Expand Down
10 changes: 10 additions & 0 deletions docs/root/faq/configuration/timeouts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ context request/stream is interchangeable.
is the amount of time that the connection manager will allow a stream to exist with no upstream
or downstream activity. The default stream idle timeout is *5 minutes*. This timeout is strongly
recommended for streaming APIs (requests or responses that never end).
* The HTTP protocol :ref:`max_stream_duration <envoy_api_field_core.HttpProtocolOptions.max_stream_duration>`
is defined in a generic message used by the HTTP connection manager. The max stream duration is the
maximum time that a stream's lifetime will span. You can use this functionality when you want to reset
HTTP request/response streams periodically. You can't use :ref:`request_timeout
<envoy_api_field_config.filter.network.http_connection_manager.v2.HttpConnectionManager.request_timeout>`
in this situation because this timer will be disarmed if a response header is received on the request/response streams.

.. attention::

The current implementation implements this timeout on downstream connections only.

Route timeouts
^^^^^^^^^^^^^^
Expand Down
1 change: 1 addition & 0 deletions docs/root/intro/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Version history
* http: fixing a bug in HTTP/1.0 responses where Connection: keep-alive was not appended for connections which were kept alive.
* http: fixed a bug that could send extra METADATA frames and underflow memory when encoding METADATA frames on a connection that was dispatching data.
* http: connection header sanitizing has been modified to always sanitize if there is no upgrade, including when an h2c upgrade attempt has been removed.
* http: added :ref:`max_stream_duration <envoy_api_field_core.HttpProtocolOptions.max_stream_duration>` to specify the duration of existing streams. See :ref:`connection and stream timeouts <faq_configuration_timeouts>`.
* listener filters: listener filter extensions use the "envoy.filters.listener" name space. A
mapping of extension names is available in the :ref:`deprecated <deprecated>` documentation.
* listeners: fixed issue where :ref:`TLS inspector listener filter <config_listener_filters_tls_inspector>` could have been bypassed by a client using only TLS 1.3.
Expand Down
6 changes: 6 additions & 0 deletions generated_api_shadow/envoy/api/v2/core/protocol.proto

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

6 changes: 6 additions & 0 deletions generated_api_shadow/envoy/config/core/v3/protocol.proto

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

6 changes: 6 additions & 0 deletions source/common/http/conn_manager_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ namespace Http {
COUNTER(downstream_rq_too_large) \
COUNTER(downstream_rq_total) \
COUNTER(downstream_rq_tx_reset) \
COUNTER(downstream_rq_max_duration_reached) \
COUNTER(downstream_rq_ws_on_non_ws_route) \
COUNTER(rs_too_large) \
GAUGE(downstream_cx_active, Accumulate) \
Expand Down Expand Up @@ -282,6 +283,11 @@ class ConnectionManagerConfig {
*/
virtual std::chrono::milliseconds delayedCloseTimeout() const PURE;

/**
* @return maximum duration time to keep alive stream
*/
virtual absl::optional<std::chrono::milliseconds> maxStreamDuration() const PURE;

/**
* @return Router::RouteConfigProvider* the configuration provider used to acquire a route
* config for each request flow. Pointer ownership is _not_ transferred to the caller of
Expand Down
19 changes: 19 additions & 0 deletions source/common/http/conn_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ void ConnectionManagerImpl::doEndStream(ActiveStream& stream) {
}

void ConnectionManagerImpl::doDeferredStreamDestroy(ActiveStream& stream) {
if (stream.max_stream_duration_timer_) {
stream.max_stream_duration_timer_->disableTimer();
stream.max_stream_duration_timer_ = nullptr;
}
if (stream.stream_idle_timer_ != nullptr) {
stream.stream_idle_timer_->disableTimer();
stream.stream_idle_timer_ = nullptr;
Expand Down Expand Up @@ -602,6 +606,15 @@ ConnectionManagerImpl::ActiveStream::ActiveStream(ConnectionManagerImpl& connect
request_timer_->enableTimer(request_timeout_ms_, this);
}

const auto max_stream_duration = connection_manager_.config_.maxStreamDuration();
if (max_stream_duration.has_value() && max_stream_duration.value().count()) {
max_stream_duration_timer_ =
connection_manager.read_callbacks_->connection().dispatcher().createTimer(
[this]() -> void { onStreamMaxDurationReached(); });
max_stream_duration_timer_->enableTimer(connection_manager_.config_.maxStreamDuration().value(),
this);
}

stream_info_.setRequestedServerName(
connection_manager_.read_callbacks_->connection().requestedServerName());
}
Expand Down Expand Up @@ -673,6 +686,12 @@ void ConnectionManagerImpl::ActiveStream::onRequestTimeout() {
absl::nullopt, StreamInfo::ResponseCodeDetails::get().RequestOverallTimeout);
}

void ConnectionManagerImpl::ActiveStream::onStreamMaxDurationReached() {
ENVOY_STREAM_LOG(debug, "Stream max duration time reached", *this);
connection_manager_.stats_.named_.downstream_rq_max_duration_reached_.inc();
connection_manager_.doEndStream(*this);
}

void ConnectionManagerImpl::ActiveStream::addStreamDecoderFilterWorker(
StreamDecoderFilterSharedPtr filter, bool dual_filter) {
ActiveStreamDecoderFilterPtr wrapper(new ActiveStreamDecoderFilter(*this, filter, dual_filter));
Expand Down
5 changes: 4 additions & 1 deletion source/common/http/conn_manager_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,8 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
void resetIdleTimer();
// Per-stream request timeout callback
void onRequestTimeout();

// Per-stream alive duration reached.
void onStreamMaxDurationReached();
bool hasCachedRoute() { return cached_route_.has_value() && cached_route_.value(); }

friend std::ostream& operator<<(std::ostream& os, const ActiveStream& s) {
Expand Down Expand Up @@ -702,6 +703,8 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
Event::TimerPtr stream_idle_timer_;
// Per-stream request timeout.
Event::TimerPtr request_timer_;
// Per-stream alive duration.
Event::TimerPtr max_stream_duration_timer_;
std::chrono::milliseconds idle_timeout_ms_{};
State state_;
StreamInfo::StreamInfoImpl stream_info_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig(
idle_timeout_(PROTOBUF_GET_OPTIONAL_MS(config.common_http_protocol_options(), idle_timeout)),
max_connection_duration_(
PROTOBUF_GET_OPTIONAL_MS(config.common_http_protocol_options(), max_connection_duration)),
max_stream_duration_(
PROTOBUF_GET_OPTIONAL_MS(config.common_http_protocol_options(), max_stream_duration)),
stream_idle_timeout_(
PROTOBUF_GET_MS_OR_DEFAULT(config, stream_idle_timeout, StreamIdleTimeoutMs)),
request_timeout_(PROTOBUF_GET_MS_OR_DEFAULT(config, request_timeout, RequestTimeoutMs)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ class HttpConnectionManagerConfig : Logger::Loggable<Logger::Id::config>,
}
std::chrono::milliseconds streamIdleTimeout() const override { return stream_idle_timeout_; }
std::chrono::milliseconds requestTimeout() const override { return request_timeout_; }
absl::optional<std::chrono::milliseconds> maxStreamDuration() const override {
return max_stream_duration_;
}
Router::RouteConfigProvider* routeConfigProvider() override {
return route_config_provider_.get();
}
Expand Down Expand Up @@ -198,6 +201,7 @@ class HttpConnectionManagerConfig : Logger::Loggable<Logger::Id::config>,
const uint32_t max_request_headers_count_;
absl::optional<std::chrono::milliseconds> idle_timeout_;
absl::optional<std::chrono::milliseconds> max_connection_duration_;
absl::optional<std::chrono::milliseconds> max_stream_duration_;
std::chrono::milliseconds stream_idle_timeout_;
std::chrono::milliseconds request_timeout_;
Router::RouteConfigProviderSharedPtr route_config_provider_;
Expand Down
4 changes: 4 additions & 0 deletions source/server/http/admin.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ class AdminImpl : public Admin,
std::chrono::milliseconds streamIdleTimeout() const override { return {}; }
std::chrono::milliseconds requestTimeout() const override { return {}; }
std::chrono::milliseconds delayedCloseTimeout() const override { return {}; }
absl::optional<std::chrono::milliseconds> maxStreamDuration() const override {
return max_stream_duration_;
}
Router::RouteConfigProvider* routeConfigProvider() override { return &route_config_provider_; }
Config::ConfigProvider* scopedRouteConfigProvider() override {
return &scoped_route_config_provider_;
Expand Down Expand Up @@ -457,6 +460,7 @@ class AdminImpl : public Admin,
const uint32_t max_request_headers_count_{Http::DEFAULT_MAX_HEADERS_COUNT};
absl::optional<std::chrono::milliseconds> idle_timeout_;
absl::optional<std::chrono::milliseconds> max_connection_duration_;
absl::optional<std::chrono::milliseconds> max_stream_duration_;
absl::optional<std::string> user_agent_;
Http::SlowDateProviderImpl date_provider_;
std::vector<Http::ClientCertDetailsType> set_current_client_cert_details_;
Expand Down
4 changes: 4 additions & 0 deletions test/common/http/conn_manager_impl_fuzz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ class FuzzConfig : public ConnectionManagerConfig {
absl::optional<std::chrono::milliseconds> maxConnectionDuration() const override {
return max_connection_duration_;
}
absl::optional<std::chrono::milliseconds> maxStreamDuration() const override {
return max_stream_duration_;
}
std::chrono::milliseconds streamIdleTimeout() const override { return stream_idle_timeout_; }
std::chrono::milliseconds requestTimeout() const override { return request_timeout_; }
std::chrono::milliseconds delayedCloseTimeout() const override { return delayed_close_timeout_; }
Expand Down Expand Up @@ -170,6 +173,7 @@ class FuzzConfig : public ConnectionManagerConfig {
uint32_t max_request_headers_count_{Http::DEFAULT_MAX_HEADERS_COUNT};
absl::optional<std::chrono::milliseconds> idle_timeout_;
absl::optional<std::chrono::milliseconds> max_connection_duration_;
absl::optional<std::chrono::milliseconds> max_stream_duration_;
std::chrono::milliseconds stream_idle_timeout_{};
std::chrono::milliseconds request_timeout_{};
std::chrono::milliseconds delayed_close_timeout_{};
Expand Down
72 changes: 72 additions & 0 deletions test/common/http/conn_manager_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan
std::chrono::milliseconds streamIdleTimeout() const override { return stream_idle_timeout_; }
std::chrono::milliseconds requestTimeout() const override { return request_timeout_; }
std::chrono::milliseconds delayedCloseTimeout() const override { return delayed_close_timeout_; }
absl::optional<std::chrono::milliseconds> maxStreamDuration() const override {
return max_stream_duration_;
}
bool use_srds_{};
Router::RouteConfigProvider* routeConfigProvider() override {
if (use_srds_) {
Expand Down Expand Up @@ -375,6 +378,7 @@ class HttpConnectionManagerImplTest : public testing::Test, public ConnectionMan
std::chrono::milliseconds stream_idle_timeout_{};
std::chrono::milliseconds request_timeout_{};
std::chrono::milliseconds delayed_close_timeout_{};
absl::optional<std::chrono::milliseconds> max_stream_duration_{};
NiceMock<Runtime::MockRandomGenerator> random_;
NiceMock<LocalInfo::MockLocalInfo> local_info_;
NiceMock<Server::Configuration::MockFactoryContext> factory_context_;
Expand Down Expand Up @@ -2397,6 +2401,74 @@ TEST_F(HttpConnectionManagerImplTest, RequestTimeoutIsDisarmedOnConnectionTermin
EXPECT_EQ(0U, stats_.named_.downstream_rq_timeout_.value());
}

TEST_F(HttpConnectionManagerImplTest, MaxStreamDurationDisabledIfSetToZero) {
max_stream_duration_ = std::chrono::milliseconds(0);
setup(false, "");

EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> void {
EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, createTimer_).Times(0);
conn_manager_->newStream(response_encoder_);
}));

Buffer::OwnedImpl fake_input("1234");
conn_manager_->onData(fake_input, false); // kick off request
}

TEST_F(HttpConnectionManagerImplTest, MaxStreamDurationValidlyConfigured) {
max_stream_duration_ = std::chrono::milliseconds(10);
setup(false, "");

EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> void {
Event::MockTimer* duration_timer = setUpTimer();

EXPECT_CALL(*duration_timer, enableTimer(max_stream_duration_.value(), _));
conn_manager_->newStream(response_encoder_);
}));

Buffer::OwnedImpl fake_input("1234");
conn_manager_->onData(fake_input, false); // kick off request
}

TEST_F(HttpConnectionManagerImplTest, MaxStreamDurationCallbackResetStream) {
max_stream_duration_ = std::chrono::milliseconds(10);
setup(false, "");
Event::MockTimer* duration_timer = setUpTimer();

EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> void {
EXPECT_CALL(*duration_timer, enableTimer(max_stream_duration_.value(), _)).Times(1);
conn_manager_->newStream(response_encoder_);
}));

Buffer::OwnedImpl fake_input("1234");
conn_manager_->onData(fake_input, false); // kick off request

EXPECT_CALL(*duration_timer, disableTimer());
duration_timer->invokeCallback();

EXPECT_EQ(1U, stats_.named_.downstream_rq_max_duration_reached_.value());
EXPECT_EQ(1U, stats_.named_.downstream_rq_rx_reset_.value());
}

TEST_F(HttpConnectionManagerImplTest, MaxStreamDurationCallbackNotCalledIfResetStreamValidly) {
max_stream_duration_ = std::chrono::milliseconds(5000);
setup(false, "");
Event::MockTimer* duration_timer = setUpTimer();

EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance&) -> void {
EXPECT_CALL(*duration_timer, enableTimer(max_stream_duration_.value(), _)).Times(1);
conn_manager_->newStream(response_encoder_);
}));

Buffer::OwnedImpl fake_input("1234");
conn_manager_->onData(fake_input, false); // kick off request

EXPECT_CALL(*duration_timer, disableTimer());
conn_manager_->onEvent(Network::ConnectionEvent::RemoteClose);

EXPECT_EQ(0U, stats_.named_.downstream_rq_max_duration_reached_.value());
EXPECT_EQ(1U, stats_.named_.downstream_rq_rx_reset_.value());
}

TEST_F(HttpConnectionManagerImplTest, RejectWebSocketOnNonWebSocketRoute) {
setup(false, "");
RequestDecoder* decoder = nullptr;
Expand Down
1 change: 1 addition & 0 deletions test/common/http/conn_manager_utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class MockConnectionManagerConfig : public ConnectionManagerConfig {
MOCK_METHOD(absl::optional<std::chrono::milliseconds>, idleTimeout, (), (const));
MOCK_METHOD(bool, isRoutable, (), (const));
MOCK_METHOD(absl::optional<std::chrono::milliseconds>, maxConnectionDuration, (), (const));
MOCK_METHOD(absl::optional<std::chrono::milliseconds>, maxStreamDuration, (), (const));
MOCK_METHOD(std::chrono::milliseconds, streamIdleTimeout, (), (const));
MOCK_METHOD(std::chrono::milliseconds, requestTimeout, (), (const));
MOCK_METHOD(std::chrono::milliseconds, delayedCloseTimeout, (), (const));
Expand Down
10 changes: 10 additions & 0 deletions test/config/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,16 @@ void ConfigHelper::setDownstreamMaxConnectionDuration(std::chrono::milliseconds
});
}

void ConfigHelper::setDownstreamMaxStreamDuration(std::chrono::milliseconds timeout) {
addConfigModifier(
[timeout](
envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) {
hcm.mutable_common_http_protocol_options()->mutable_max_stream_duration()->MergeFrom(
ProtobufUtil::TimeUtil::MillisecondsToDuration(timeout.count()));
});
}

void ConfigHelper::setConnectTimeout(std::chrono::milliseconds timeout) {
RELEASE_ASSERT(!finalized_, "");

Expand Down
3 changes: 3 additions & 0 deletions test/config/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ class ConfigHelper {
// Set the max connection duration for downstream connections through the HttpConnectionManager.
void setDownstreamMaxConnectionDuration(std::chrono::milliseconds max_connection_duration);

// Set the max stream duration for downstream connections through the HttpConnectionManager.
void setDownstreamMaxStreamDuration(std::chrono::milliseconds max_stream_duration);

// Set the connect timeout on upstream connections.
void setConnectTimeout(std::chrono::milliseconds timeout);

Expand Down
Loading