From 14525f8074eeca5f4232933f8e910ff61b9876e0 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Thu, 19 Sep 2024 13:47:11 -0700 Subject: [PATCH 01/11] http: add config for max response header size Signed-off-by: Greg Greenway --- api/envoy/config/core/v3/protocol.proto | 11 +++- .../v3/http_connection_manager.proto | 3 ++ changelogs/current.yaml | 4 ++ envoy/upstream/upstream.h | 5 ++ source/common/http/codec_client.cc | 8 +-- source/common/http/http1/codec_impl.cc | 4 +- source/common/http/http1/codec_impl.h | 1 + source/common/protobuf/utility.h | 6 +++ source/common/upstream/upstream_impl.cc | 2 + source/common/upstream/upstream_impl.h | 4 ++ .../network/http_connection_manager/config.cc | 16 ++++-- test/common/http/codec_impl_fuzz_test.cc | 2 +- test/common/http/http1/codec_impl_test.cc | 37 ++++++++++++-- .../http/http1/http1_connection_fuzz_test.cc | 2 +- .../http_connection_manager/config_test.cc | 50 +++++++++++++++++++ .../multiplexed_upstream_integration_test.cc | 27 ++++++++++ test/mocks/upstream/cluster_info.h | 1 + 17 files changed, 168 insertions(+), 15 deletions(-) diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 6dbff8c48f03..138d704d9eaa 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -205,7 +205,7 @@ message AlternateProtocolsCacheOptions { repeated string canonical_suffixes = 5; } -// [#next-free-field: 7] +// [#next-free-field: 8] message HttpProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.HttpProtocolOptions"; @@ -260,6 +260,15 @@ message HttpProtocolOptions { // 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}]; + // The maximum combined size of headers. + // If unconfigured, the default is 60 KiB, except for HTTP/1 response headers which have a default + // of 80KiB. + // Requests that exceed this limit will receive a 431 response. + // If both this setting and :ref:`max_request_headers_kb + // ` + // are set, this setting is used. + google.protobuf.UInt32Value max_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; + // 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. google.protobuf.Duration max_stream_duration = 4; diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 4cbbbc20d3fb..0c0ad980d940 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -494,6 +494,9 @@ message HttpConnectionManager { // The maximum request headers size for incoming connections. // If unconfigured, the default max request headers allowed is 60 KiB. // Requests that exceed this limit will receive a 431 response. + // If both this setting and :ref:`max_headers_kb + // ` are set, + // that setting is used and this one is ignored. google.protobuf.UInt32Value max_request_headers_kb = 29 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 5fd9945a3ad6..a0ba00a5fa3f 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -264,6 +264,10 @@ new_features: change: | Added full feature absl::FormatTime() support to the DateFormatter. This allows the timepoint formatters (like ``%START_TIME%``) to use ``%E#S``, ``%E*S``, ``%E#f`` and ``%E*f`` to format the subsecond part of the timepoint. +- area: http + change: | + Added configuration setting for the :ref:`maximum size of headers + ` in responses. - area: http_11_proxy change: | Added the option to configure the transport socket via locality or endpoint metadata. diff --git a/envoy/upstream/upstream.h b/envoy/upstream/upstream.h index e307de9471f7..ca704d02fdb6 100644 --- a/envoy/upstream/upstream.h +++ b/envoy/upstream/upstream.h @@ -1068,6 +1068,11 @@ class ClusterInfo : public Http::FilterChainFactory { */ virtual uint32_t maxResponseHeadersCount() const PURE; + /** + * @return uint32_t the maximum total size of response headers in KB. + */ + virtual absl::optional maxResponseHeadersKb() const PURE; + /** * @return the human readable name of the cluster. */ diff --git a/source/common/http/codec_client.cc b/source/common/http/codec_client.cc index 4c0cee082880..fb86c869f300 100644 --- a/source/common/http/codec_client.cc +++ b/source/common/http/codec_client.cc @@ -277,13 +277,14 @@ CodecClientProd::CodecClientProd(CodecType type, Network::ClientConnectionPtr&& } codec_ = std::make_unique( *connection_, host->cluster().http1CodecStats(), *this, host->cluster().http1Settings(), - host->cluster().maxResponseHeadersCount(), proxied); + host->cluster().maxResponseHeadersKb(), host->cluster().maxResponseHeadersCount(), proxied); break; } case CodecType::HTTP2: codec_ = std::make_unique( *connection_, *this, host->cluster().http2CodecStats(), random_generator, - host->cluster().http2Options(), Http::DEFAULT_MAX_REQUEST_HEADERS_KB, + host->cluster().http2Options(), + host->cluster().maxResponseHeadersKb().value_or(Http::DEFAULT_MAX_REQUEST_HEADERS_KB), host->cluster().maxResponseHeadersCount(), Http2::ProdNghttp2SessionFactory::get()); break; case CodecType::HTTP3: { @@ -291,7 +292,8 @@ CodecClientProd::CodecClientProd(CodecType type, Network::ClientConnectionPtr&& auto& quic_session = dynamic_cast(*connection_); codec_ = std::make_unique( quic_session, *this, host->cluster().http3CodecStats(), host->cluster().http3Options(), - Http::DEFAULT_MAX_REQUEST_HEADERS_KB, host->cluster().maxResponseHeadersCount()); + host->cluster().maxResponseHeadersKb().value_or(Http::DEFAULT_MAX_REQUEST_HEADERS_KB), + host->cluster().maxResponseHeadersCount()); // Initialize the session after max request header size is changed in above http client // connection creation. quic_session.Initialize(); diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index 5e054058a996..92fb58bb1a40 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -1428,9 +1428,11 @@ void ServerConnectionImpl::ActiveRequest::dumpState(std::ostream& os, int indent ClientConnectionImpl::ClientConnectionImpl(Network::Connection& connection, CodecStats& stats, ConnectionCallbacks&, const Http1Settings& settings, + absl::optional max_response_headers_kb, const uint32_t max_response_headers_count, bool passing_through_proxy) - : ConnectionImpl(connection, stats, settings, MessageType::Response, MAX_RESPONSE_HEADERS_KB, + : ConnectionImpl(connection, stats, settings, MessageType::Response, + max_response_headers_kb.value_or(MAX_RESPONSE_HEADERS_KB), max_response_headers_count), owned_output_buffer_(connection.dispatcher().getWatermarkFactory().createBuffer( [&]() -> void { this->onBelowLowWatermark(); }, diff --git a/source/common/http/http1/codec_impl.h b/source/common/http/http1/codec_impl.h index 26e3bfe82d9f..875a9c6d99d6 100644 --- a/source/common/http/http1/codec_impl.h +++ b/source/common/http/http1/codec_impl.h @@ -590,6 +590,7 @@ class ClientConnectionImpl : public ClientConnection, public ConnectionImpl { public: ClientConnectionImpl(Network::Connection& connection, CodecStats& stats, ConnectionCallbacks& callbacks, const Http1Settings& settings, + absl::optional max_response_headers_kb, const uint32_t max_response_headers_count, bool passing_through_proxy = false); // Http::ClientConnection diff --git a/source/common/protobuf/utility.h b/source/common/protobuf/utility.h index 546fbcd22783..ffd19ad89e78 100644 --- a/source/common/protobuf/utility.h +++ b/source/common/protobuf/utility.h @@ -23,6 +23,12 @@ #define PROTOBUF_GET_WRAPPED_OR_DEFAULT(message, field_name, default_value) \ ((message).has_##field_name() ? (message).field_name().value() : (default_value)) +// Obtain the value of a wrapped field (e.g. google.protobuf.UInt32Value) if set. Otherwise, return +// absl::nullopt. +#define PROTOBUF_GET_OPTIONAL_WRAPPED(message, field_name) \ + ((message).has_##field_name() ? absl::make_optional((message).field_name().value()) \ + : absl::nullopt) + // Obtain the value of a wrapped field (e.g. google.protobuf.UInt32Value) if set. Otherwise, throw // a EnvoyException. diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index bfee8e6b2baa..1f01a0ff5b2e 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -1229,6 +1229,8 @@ ClusterInfoImpl::ClusterInfoImpl( http_protocol_options_->common_http_protocol_options_, max_headers_count, runtime_.snapshot().getInteger(Http::MaxResponseHeadersCountOverrideKey, Http::DEFAULT_MAX_HEADERS_COUNT))), + max_response_headers_kb_(PROTOBUF_GET_OPTIONAL_WRAPPED( + http_protocol_options_->common_http_protocol_options_, max_headers_kb)), type_(config.type()), drain_connections_on_host_removal_(config.ignore_health_on_host_removal()), connection_pool_per_downstream_connection_( diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index 47d32468571d..a3d43f7b969e 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -913,6 +913,9 @@ class ClusterInfoImpl : public ClusterInfo, bool maintenanceMode() const override; uint64_t maxRequestsPerConnection() const override { return max_requests_per_connection_; } uint32_t maxResponseHeadersCount() const override { return max_response_headers_count_; } + absl::optional maxResponseHeadersKb() const override { + return max_response_headers_kb_; + } const std::string& name() const override { return name_; } const std::string& observabilityName() const override { if (observability_name_ != nullptr) { @@ -1126,6 +1129,7 @@ class ClusterInfoImpl : public ClusterInfo, // overhead via alignment const uint32_t per_connection_buffer_limit_bytes_; const uint32_t max_response_headers_count_; + const absl::optional max_response_headers_kb_; const envoy::config::cluster::v3::Cluster::DiscoveryType type_; const bool drain_connections_on_host_removal_ : 1; const bool connection_pool_per_downstream_connection_ : 1; diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 563ece44c917..eb587a82507b 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -369,9 +369,11 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( config.stream_error_on_invalid_http_message(), xff_num_trusted_hops_ == 0 && use_remote_address_)), max_request_headers_kb_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_request_headers_kb, - context.serverFactoryContext().runtime().snapshot().getInteger( - Http::MaxRequestHeadersSizeOverrideKey, Http::DEFAULT_MAX_REQUEST_HEADERS_KB))), + config.common_http_protocol_options(), max_headers_kb, + PROTOBUF_GET_WRAPPED_OR_DEFAULT( + config, max_request_headers_kb, + context.serverFactoryContext().runtime().snapshot().getInteger( + Http::MaxRequestHeadersSizeOverrideKey, Http::DEFAULT_MAX_REQUEST_HEADERS_KB)))), max_request_headers_count_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( config.common_http_protocol_options(), max_headers_count, context.serverFactoryContext().runtime().snapshot().getInteger( @@ -432,6 +434,14 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( return; } + if (config.has_max_request_headers_kb() && + config.common_http_protocol_options().has_max_headers_kb() && + config.max_request_headers_kb().value() != + config.common_http_protocol_options().max_headers_kb().value()) { + ENVOY_LOG(warn, "Both `max_request_headers_kb` and `max_headers_kb` are configured. Ignoring " + "`max_request_headers_kb`."); + } + auto options_or_error = Http2::Utility::initializeAndValidateOptions( config.http2_protocol_options(), config.has_stream_error_on_invalid_http_message(), config.stream_error_on_invalid_http_message()); diff --git a/test/common/http/codec_impl_fuzz_test.cc b/test/common/http/codec_impl_fuzz_test.cc index 8554be417b01..bca32e84542e 100644 --- a/test/common/http/codec_impl_fuzz_test.cc +++ b/test/common/http/codec_impl_fuzz_test.cc @@ -616,7 +616,7 @@ void codecFuzz(const test::common::http::CodecImplFuzzTestCase& input, HttpVersi } else { client = std::make_unique( client_connection, Http1::CodecStats::atomicGet(http1_stats, scope), client_callbacks, - client_http1settings, max_response_headers_count); + client_http1settings, max_request_headers_kb, max_response_headers_count); } if (http2) { diff --git a/test/common/http/http1/codec_impl_test.cc b/test/common/http/http1/codec_impl_test.cc index 1fca99b17e7b..0dca1c8abc16 100644 --- a/test/common/http/http1/codec_impl_test.cc +++ b/test/common/http/http1/codec_impl_test.cc @@ -2544,7 +2544,8 @@ class Http1ClientConnectionImplTest : public Http1CodecTestBase { public: void initialize() { codec_ = std::make_unique( - connection_, http1CodecStats(), callbacks_, codec_settings_, max_response_headers_count_, + connection_, http1CodecStats(), callbacks_, codec_settings_, max_response_headers_kb_, + max_response_headers_count_, /* passing_through_proxy=*/false); } @@ -2564,6 +2565,7 @@ class Http1ClientConnectionImplTest : public Http1CodecTestBase { protected: Stats::TestUtil::TestStore store_; uint32_t max_response_headers_count_{Http::DEFAULT_MAX_HEADERS_COUNT}; + uint32_t max_response_headers_kb_{Http::Http1::MAX_RESPONSE_HEADERS_KB}; }; void Http1ClientConnectionImplTest::testClientAllowChunkedContentLength( @@ -2571,8 +2573,9 @@ void Http1ClientConnectionImplTest::testClientAllowChunkedContentLength( // Response validation is not implemented in UHV yet #ifndef ENVOY_ENABLE_UHV codec_settings_.allow_chunked_length_ = allow_chunked_length; - codec_ = std::make_unique( - connection_, http1CodecStats(), callbacks_, codec_settings_, max_response_headers_count_); + codec_ = std::make_unique(connection_, http1CodecStats(), callbacks_, + codec_settings_, max_response_headers_kb_, + max_response_headers_count_); NiceMock response_decoder; Http::RequestEncoder& request_encoder = codec_->newStream(response_decoder); @@ -3706,6 +3709,28 @@ TEST_P(Http1ClientConnectionImplTest, LargeResponseHeadersAccepted) { std::string long_header = "big: " + std::string(79 * 1024, 'q') + "\r\n"; buffer = Buffer::OwnedImpl(long_header); status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); +} + +// Tests that the size of response headers for HTTP/1 can be configured higher than the default of +// 80kB. +TEST_P(Http1ClientConnectionImplTest, LargeResponseHeadersAcceptedConfigurable) { + constexpr uint32_t size_limit_kb = 85; + max_response_headers_kb_ = size_limit_kb; + initialize(); + + NiceMock response_decoder; + Http::RequestEncoder& request_encoder = codec_->newStream(response_decoder); + TestRequestHeaderMapImpl headers{{":method", "GET"}, {":path", "/"}, {":authority", "host"}}; + EXPECT_TRUE(request_encoder.encodeHeaders(headers, true).ok()); + + Buffer::OwnedImpl buffer("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n"); + auto status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); + std::string long_header = "big: " + std::string((size_limit_kb - 1) * 1024, 'q') + "\r\n"; + buffer = Buffer::OwnedImpl(long_header); + status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); } // Regression test for CVE-2019-18801. Large method headers should not trigger @@ -3792,6 +3817,7 @@ TEST_P(Http1ClientConnectionImplTest, ManyResponseHeadersAccepted) { // Response already contains one header. buffer = Buffer::OwnedImpl(createHeaderOrTrailerFragment(150) + "\r\n"); status = codec_->dispatch(buffer); + EXPECT_TRUE(status.ok()); } TEST_P(Http1ClientConnectionImplTest, TestResponseSplit0) { @@ -3821,8 +3847,9 @@ TEST_P(Http1ClientConnectionImplTest, TestResponseSplitAllowChunkedLength100) { TEST_P(Http1ClientConnectionImplTest, VerifyResponseHeaderTrailerMapMaxLimits) { codec_settings_.allow_chunked_length_ = true; codec_settings_.enable_trailers_ = true; - codec_ = std::make_unique( - connection_, http1CodecStats(), callbacks_, codec_settings_, max_response_headers_count_); + codec_ = std::make_unique(connection_, http1CodecStats(), callbacks_, + codec_settings_, max_response_headers_kb_, + max_response_headers_count_); NiceMock response_decoder; Http::RequestEncoder& request_encoder = codec_->newStream(response_decoder); diff --git a/test/common/http/http1/http1_connection_fuzz_test.cc b/test/common/http/http1/http1_connection_fuzz_test.cc index 9dae7af6c7b9..37eb6fc64e4a 100644 --- a/test/common/http/http1/http1_connection_fuzz_test.cc +++ b/test/common/http/http1/http1_connection_fuzz_test.cc @@ -45,7 +45,7 @@ class Http1Harness { client_ = std::make_unique( mock_client_connection_, Http1::CodecStats::atomicGet(http1_stats_, *stats_store_.rootScope()), - mock_client_callbacks_, client_settings_, Http::DEFAULT_MAX_HEADERS_COUNT); + mock_client_callbacks_, client_settings_, absl::nullopt, Http::DEFAULT_MAX_HEADERS_COUNT); Status status = client_->dispatch(payload); } diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index 5f39f9935a21..599dada4bf20 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -826,6 +826,56 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbConfigured) { EXPECT_EQ(16, config.maxRequestHeadersKb()); } +TEST_F(HttpConnectionManagerConfigTest, MaxHeadersKbConfigured) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + common_http_protocol_options: + max_headers_kb: 16 + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + ASSERT_TRUE(creation_status_.ok()); + EXPECT_EQ(16, config.maxRequestHeadersKb()); +} + +TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbAndMaxHeadersKbConfigured) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + common_http_protocol_options: + max_headers_kb: 16 + max_request_headers_kb: 32 + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + EXPECT_LOG_CONTAINS("warn", + "Both `max_request_headers_kb` and `max_headers_kb` are configured. Ignoring " + "`max_request_headers_kb`.", + { + HttpConnectionManagerConfig config( + parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + + ASSERT_TRUE(creation_status_.ok()); + EXPECT_EQ(16, config.maxRequestHeadersKb()); + }); +} + TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbMaxConfigurable) { const std::string yaml_string = R"EOF( stat_prefix: ingress_http diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index e2270be533a8..0afab76a0f34 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -573,6 +573,33 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersRejected) { EXPECT_EQ("503", response->headers().getStatusValue()); } +// Tests configuration of max response headers size. +TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { + constexpr uint32_t limit_kb = 150; + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); + http_protocol_options->mutable_max_headers_kb()->set_value(limit_kb); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); + Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); + large_headers.addCopy("large", std::string((limit_kb - 1) * 1024, 'a')); + auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + + initialize(); + codec_client_ = makeHttpConnection(lookupPort("http")); + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(response_headers, false); + upstream_request_->encodeData(512, true); + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); +} + TEST_P(MultiplexedUpstreamIntegrationTest, NoInitialStreams) { // Set the fake upstream to start with 0 streams available. upstreamConfig().http2_options_.mutable_max_concurrent_streams()->set_value(0); diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index 8f5a72973f49..978f9c5826aa 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -128,6 +128,7 @@ class MockClusterInfo : public ClusterInfo { (const)); MOCK_METHOD(bool, maintenanceMode, (), (const)); MOCK_METHOD(uint32_t, maxResponseHeadersCount, (), (const)); + MOCK_METHOD(absl::optional, maxResponseHeadersKb, (), (const)); MOCK_METHOD(uint64_t, maxRequestsPerConnection, (), (const)); MOCK_METHOD(const std::string&, name, (), (const)); MOCK_METHOD(const std::string&, observabilityName, (), (const)); From bfa34f5c5a72ad74ac14d26cdfd9e597f1046b35 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Mon, 23 Sep 2024 12:48:05 -0700 Subject: [PATCH 02/11] fix integration test Signed-off-by: Greg Greenway --- test/integration/http_integration.cc | 5 +++++ test/integration/http_integration.h | 10 ++++++---- .../multiplexed_upstream_integration_test.cc | 19 ++++++++++++++++--- test/integration/overload_integration_test.cc | 6 ++++-- .../integration/quic_http_integration_test.cc | 11 +++++++---- test/mocks/upstream/cluster_info.cc | 7 +++++++ 6 files changed, 45 insertions(+), 13 deletions(-) diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index d8bc506c181e..8f37780f87b0 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -266,6 +266,7 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeHttpConnection(uint32_t port) IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection( Network::ClientConnectionPtr&& conn, absl::optional http2_options, + absl::optional common_http_options, bool wait_till_connected) { std::shared_ptr cluster{new NiceMock()}; cluster->max_response_headers_count_ = 200; @@ -286,6 +287,10 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection( cluster->http2_options_ = http2_options.value(); cluster->http1_settings_.enable_trailers_ = true; + if (common_http_options.has_value()) { + cluster->common_http_protocol_options_ = common_http_options.value(); + } + if (!disable_client_header_validation_) { cluster->header_validator_factory_ = IntegrationUtil::makeHeaderValidationFactory( ::envoy::extensions::http::header_validators::envoy_default::v3::HeaderValidatorConfig()); diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 06f2dd15a65c..389953536095 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -158,10 +158,12 @@ class HttpIntegrationTest : public BaseIntegrationTest { IntegrationCodecClientPtr makeHttpConnection(uint32_t port); // Makes a http connection object without checking its connected state. - virtual IntegrationCodecClientPtr - makeRawHttpConnection(Network::ClientConnectionPtr&& conn, - absl::optional http2_options, - bool wait_till_connected = true); + virtual IntegrationCodecClientPtr makeRawHttpConnection( + Network::ClientConnectionPtr&& conn, + absl::optional http2_options, + absl::optional common_http_options = + absl::nullopt, + bool wait_till_connected = true); // Makes a downstream network connection object based on client codec version. Network::ClientConnectionPtr makeClientConnectionWithOptions( uint32_t port, const Network::ConnectionSocket::OptionsSharedPtr& options) override; diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index 0afab76a0f34..db95df464cf3 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -585,14 +585,27 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { }); Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); large_headers.addCopy("large", std::string((limit_kb - 1) * 1024, 'a')); - auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); + + // This test is validating upstream response headers, but the test client will fail to receive the + // request from Envoy if its limits aren't increased. + envoy::config::core::v3::HttpProtocolOptions client_protocol_options; + client_protocol_options.mutable_max_headers_kb()->set_value(limit_kb * 2); + + // nghttp2 test codec fails with a compression error in this test for unknown reasons, but oghttp2 + // works fine, so force its use here, only for the test client. Use the test parameter specified + // http2 codec for the Envoy client and server codecs. + envoy::config::core::v3::Http2ProtocolOptions client_h2_options = + Http2::Utility::initializeAndValidateOptions(envoy::config::core::v3::Http2ProtocolOptions()) + .value(); + client_h2_options.mutable_use_oghttp2_codec()->set_value(true); initialize(); - codec_client_ = makeHttpConnection(lookupPort("http")); + codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), client_h2_options, + client_protocol_options); auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); waitForNextUpstreamRequest(); - upstream_request_->encodeHeaders(response_headers, false); + upstream_request_->encodeHeaders(large_headers, false); upstream_request_->encodeData(512, true); ASSERT_TRUE(response->waitForEndStream()); diff --git a/test/integration/overload_integration_test.cc b/test/integration/overload_integration_test.cc index d3a62cdb2ff9..d5e808a88832 100644 --- a/test/integration/overload_integration_test.cc +++ b/test/integration/overload_integration_test.cc @@ -403,6 +403,7 @@ TEST_P(OverloadScaledTimerIntegrationTest, HTTP3CloseIdleHttpConnectionsDuringHa test_server_->waitForGaugeGe("overload.envoy.overload_actions.reduce_timeouts.scale_percent", 50); // Create an HTTP connection without finishing the handshake. codec_client_ = makeRawHttpConnection(makeClientConnection((lookupPort("http"))), absl::nullopt, + absl::nullopt, /*wait_till_connected=*/false); EXPECT_FALSE(codec_client_->connected()); @@ -418,8 +419,9 @@ TEST_P(OverloadScaledTimerIntegrationTest, HTTP3CloseIdleHttpConnectionsDuringHa 100); // Create another HTTP connection without finishing handshake. - IntegrationCodecClientPtr codec_client2 = makeRawHttpConnection( - makeClientConnection((lookupPort("http"))), absl::nullopt, /*wait_till_connected=*/false); + IntegrationCodecClientPtr codec_client2 = + makeRawHttpConnection(makeClientConnection((lookupPort("http"))), absl::nullopt, + absl::nullopt, /*wait_till_connected=*/false); EXPECT_FALSE(codec_client2->connected()); // Advancing past the minimum time and wait for the proxy to notice and close both connections. timeSystem().advanceTimeWait(std::chrono::seconds(3)); diff --git a/test/integration/quic_http_integration_test.cc b/test/integration/quic_http_integration_test.cc index efc3e6b9224e..8be734d4747b 100644 --- a/test/integration/quic_http_integration_test.cc +++ b/test/integration/quic_http_integration_test.cc @@ -251,12 +251,15 @@ class QuicHttpIntegrationTestBase : public HttpIntegrationTest { return session; } - IntegrationCodecClientPtr - makeRawHttpConnection(Network::ClientConnectionPtr&& conn, - absl::optional http2_options, - bool wait_till_connected = true) override { + IntegrationCodecClientPtr makeRawHttpConnection( + Network::ClientConnectionPtr&& conn, + absl::optional http2_options, + absl::optional common_http_options = + absl::nullopt, + bool wait_till_connected = true) override { ENVOY_LOG(debug, "Creating a new client {}", conn->connectionInfoProvider().localAddress()->asStringView()); + ASSERT(!common_http_options.has_value(), "Not implemented"); return makeRawHttp3Connection(std::move(conn), http2_options, wait_till_connected); } diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index 380c7a7c8bcd..dce4fb332e8a 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -96,6 +96,13 @@ MockClusterInfo::MockClusterInfo() ON_CALL(*this, extensionProtocolOptions(_)).WillByDefault(Return(extension_protocol_options_)); ON_CALL(*this, maxResponseHeadersCount()) .WillByDefault(ReturnPointee(&max_response_headers_count_)); + ON_CALL(*this, maxResponseHeadersKb()).WillByDefault(Invoke([this]() -> absl::optional { + if (common_http_protocol_options_.has_max_headers_kb()) { + return common_http_protocol_options_.max_headers_kb().value(); + } else { + return absl::nullopt; + } + })); ON_CALL(*this, maxRequestsPerConnection()) .WillByDefault(ReturnPointee(&max_requests_per_connection_)); ON_CALL(*this, trafficStats()).WillByDefault(ReturnRef(traffic_stats_)); From 9dce8622c7385ba874753378aae5e8eb9c21386f Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Mon, 23 Sep 2024 13:10:16 -0700 Subject: [PATCH 03/11] improve docs Signed-off-by: Greg Greenway --- api/envoy/config/core/v3/protocol.proto | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 138d704d9eaa..cfc959818270 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -255,15 +255,21 @@ message HttpProtocolOptions { // `. google.protobuf.Duration max_connection_duration = 3; - // The maximum number of headers. If unconfigured, the default - // 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. + // The maximum number of headers (request headers if configured on HttpConnectionManager, + // response headers when configured on a cluster). + // If unconfigured, the default maximum number of headers allowed is 100. + // Downstream requests that exceed this limit will receive a 431 response for HTTP/1.x and cause a stream + // reset for HTTP/2. + // Upstream responses that exceed this limit will result in a 503 response. google.protobuf.UInt32Value max_headers_count = 2 [(validate.rules).uint32 = {gte: 1}]; - // The maximum combined size of headers. + // The maximum size of headers (request headers if configured on HttpConnectionManager, response headers + // when configured on a cluster). // If unconfigured, the default is 60 KiB, except for HTTP/1 response headers which have a default // of 80KiB. - // Requests that exceed this limit will receive a 431 response. + // Downstream requests that exceed this limit will receive a 431 response for HTTP/1.x and cause a stream + // reset for HTTP/2. + // Upstream responses that exceed this limit will result in a 503 response. // If both this setting and :ref:`max_request_headers_kb // ` // are set, this setting is used. From 4f0aefd3ad19d990182b773b20bd1b72c36c70b0 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Mon, 23 Sep 2024 13:15:54 -0700 Subject: [PATCH 04/11] test at the limit Signed-off-by: Greg Greenway --- test/integration/multiplexed_upstream_integration_test.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index db95df464cf3..f0da67eed411 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -575,7 +575,7 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersRejected) { // Tests configuration of max response headers size. TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { - constexpr uint32_t limit_kb = 150; + constexpr uint32_t limit_kb = 8192; config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { ConfigHelper::HttpProtocolOptions protocol_options; auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); @@ -584,7 +584,9 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { protocol_options); }); Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); - large_headers.addCopy("large", std::string((limit_kb - 1) * 1024, 'a')); + large_headers.addCopy( + "large", + std::string((limit_kb * 1024) - 512 /* allow 512 bytes for other response headers */, 'a')); // This test is validating upstream response headers, but the test client will fail to receive the // request from Envoy if its limits aren't increased. From d86bbd8a73468a198de00a49d329d8b76f3992bd Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Mon, 23 Sep 2024 13:27:39 -0700 Subject: [PATCH 05/11] verify that request was successful in new test Signed-off-by: Greg Greenway --- test/integration/multiplexed_upstream_integration_test.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index f0da67eed411..4fdd5253738e 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -613,6 +613,7 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { EXPECT_TRUE(upstream_request_->complete()); EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); } TEST_P(MultiplexedUpstreamIntegrationTest, NoInitialStreams) { From c4f51dc27f695682e4a1cea55332f34dfedec2df Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Mon, 23 Sep 2024 18:36:21 -0700 Subject: [PATCH 06/11] document codec limits Signed-off-by: Greg Greenway --- api/envoy/config/core/v3/protocol.proto | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index cfc959818270..0a7c43df338f 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -273,6 +273,10 @@ message HttpProtocolOptions { // If both this setting and :ref:`max_request_headers_kb // ` // are set, this setting is used. + // + // Note: currently some protocol codecs impose limits on the maximum size of a single header: + // HTTP2 (when using nghttp2) limits a single header to around 100kb. + // HTTP3 limits a single header to around 1024kb. google.protobuf.UInt32Value max_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; // Total duration to keep alive an HTTP request/response stream. If the time limit is reached the stream will be From 31b007078312584585aa29f761caa59a3d235c17 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Mon, 23 Sep 2024 18:39:16 -0700 Subject: [PATCH 07/11] tests Signed-off-by: Greg Greenway --- test/integration/http_integration.cc | 67 ++++++++++++++++++- test/integration/http_integration.h | 6 +- .../multiplexed_upstream_integration_test.cc | 63 +++++++++++++---- test/integration/protocol_integration_test.cc | 43 +++++++++++- 4 files changed, 162 insertions(+), 17 deletions(-) diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 8f37780f87b0..21ab4c6c05cf 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -1453,7 +1453,72 @@ void HttpIntegrationTest::testLargeRequestHeaders(uint32_t size, uint32_t count, } else { IntegrationStreamDecoderPtr response = codec_client_->makeHeaderOnlyRequest(big_headers); RELEASE_ASSERT(response->waitForEndStream(timeout), - fmt::format("unexpected timeout after ", timeout.count(), " ms")); + fmt::format("unexpected timeout after {}ms", timeout.count())); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } + if (count > max_count) { + EXPECT_THAT(waitForAccessLog(access_log_name_), HasSubstr("too_many_headers")); + } +} + +void HttpIntegrationTest::testLargeResponseHeaders(uint32_t size, uint32_t count, uint32_t max_size, + uint32_t max_count, + std::chrono::milliseconds timeout) { + autonomous_upstream_ = true; + useAccessLog("%RESPONSE_CODE_DETAILS%"); + // `size` parameter dictates the size of each header that will be added to the response and + // `count` parameter is the number of headers to be added. The actual request byte size will + // exceed `size` due to the keys and other headers. The actual request header count will exceed + // `count` by four due to default headers. + + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); + http_protocol_options->mutable_max_headers_kb()->set_value(max_size); + http_protocol_options->mutable_max_headers_count()->set_value(max_count); + + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); + + // This test is validating upstream response headers, but the test client will fail to receive the + // request from Envoy if its limits aren't increased. + envoy::config::core::v3::HttpProtocolOptions client_protocol_options; + client_protocol_options.mutable_max_headers_kb()->set_value(max_size); + client_protocol_options.mutable_max_headers_count()->set_value(max_count); + + Http::TestRequestHeaderMapImpl big_headers(default_response_headers_); + + // Already added four headers. + for (unsigned int i = 0; i < count; i++) { + big_headers.addCopy(std::to_string(i), std::string(size * 1024, 'a')); + } + + initialize(); + codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), absl::nullopt, + client_protocol_options); + reinterpret_cast(fake_upstreams_.front().get()) + ->setResponseHeaders(std::make_unique(big_headers)); + + if (size >= max_size || count > max_count) { + // header size includes keys too, so expect rejection when equal + auto encoder_decoder = codec_client_->startRequest(default_request_headers_); + auto response = std::move(encoder_decoder.second); + + if (downstream_protocol_ == Http::CodecType::HTTP1) { + ASSERT_TRUE(codec_client_->waitForDisconnect()); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("431", response->headers().getStatusValue()); + } else { + ASSERT_TRUE(response->waitForReset()); + codec_client_->close(); + } + } else { + IntegrationStreamDecoderPtr response = + codec_client_->makeHeaderOnlyRequest(default_request_headers_); + RELEASE_ASSERT(response->waitForEndStream(timeout), + fmt::format("unexpected timeout after {}ms", timeout.count())); EXPECT_TRUE(response->complete()); EXPECT_EQ("200", response->headers().getStatusValue()); } diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 389953536095..55adabba2eb4 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -273,13 +273,13 @@ class HttpIntegrationTest : public BaseIntegrationTest { void testRouterUpstreamResponseBeforeRequestComplete(uint32_t status_code = 0); void testTwoRequests(bool force_network_backup = false); - void testLargeHeaders(Http::TestRequestHeaderMapImpl request_headers, - Http::TestRequestTrailerMapImpl request_trailers, uint32_t size, - uint32_t max_size); void testLargeRequestUrl(uint32_t url_size, uint32_t max_headers_size); void testLargeRequestHeaders(uint32_t size, uint32_t count, uint32_t max_size = 60, uint32_t max_count = 100, std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); + void testLargeResponseHeaders(uint32_t size, uint32_t count, uint32_t max_size = 60, + uint32_t max_count = 100, + std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); void testLargeRequestTrailers(uint32_t size, uint32_t max_size = 60); void testManyRequestHeaders(std::chrono::milliseconds time = TestUtility::DefaultTimeout); diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index 4fdd5253738e..ca1b603308d7 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -575,34 +575,73 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersRejected) { // Tests configuration of max response headers size. TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { + uint32_t limit_kb = 8192; + if (GetParam().http2_implementation == Http2Impl::Nghttp2) { + // nghttp2 has a hard-coded, unconfigurable limit of 64k for a header in it's header + // decompressor, so this test will always fail when using that codec. + // Reduce the size so that it can pass and receive some test coverage. + limit_kb = 100; + } + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + ConfigHelper::HttpProtocolOptions protocol_options; + auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); + http_protocol_options->mutable_max_headers_kb()->set_value(limit_kb); + + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), + protocol_options); + }); + Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); + large_headers.addCopy("large", std::string((limit_kb - 1) * 1024, 'a')); + + // This test is validating upstream response headers, but the test client will fail to receive the + // request from Envoy if its limits aren't increased. + envoy::config::core::v3::HttpProtocolOptions client_protocol_options; + client_protocol_options.mutable_max_headers_kb()->set_value(limit_kb * 2); + + initialize(); + codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), absl::nullopt, + client_protocol_options); + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + waitForNextUpstreamRequest(); + + upstream_request_->encodeHeaders(large_headers, false); + upstream_request_->encodeData(512, true); + ASSERT_TRUE(response->waitForEndStream()); + + EXPECT_TRUE(upstream_request_->complete()); + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + +// Tests configuration of max response headers size when that limit is consumed by many medium sized +// headers. +TEST_P(MultiplexedUpstreamIntegrationTest, ManyLargeResponseHeadersAccepted) { constexpr uint32_t limit_kb = 8192; + constexpr uint32_t num_hdrs = 100; + constexpr uint32_t hdr_size_kb = + (limit_kb - 1) / num_hdrs; // Subtract 1 to leave some space for other response headers. + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { ConfigHelper::HttpProtocolOptions protocol_options; auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); http_protocol_options->mutable_max_headers_kb()->set_value(limit_kb); + http_protocol_options->mutable_max_headers_count()->set_value(1000); + ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), protocol_options); }); Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); - large_headers.addCopy( - "large", - std::string((limit_kb * 1024) - 512 /* allow 512 bytes for other response headers */, 'a')); + for (uint32_t i = 0; i < num_hdrs; ++i) { + large_headers.addCopy(fmt::format("large{}", i), std::string(hdr_size_kb * 1024, 'a')); + } // This test is validating upstream response headers, but the test client will fail to receive the // request from Envoy if its limits aren't increased. envoy::config::core::v3::HttpProtocolOptions client_protocol_options; client_protocol_options.mutable_max_headers_kb()->set_value(limit_kb * 2); - // nghttp2 test codec fails with a compression error in this test for unknown reasons, but oghttp2 - // works fine, so force its use here, only for the test client. Use the test parameter specified - // http2 codec for the Envoy client and server codecs. - envoy::config::core::v3::Http2ProtocolOptions client_h2_options = - Http2::Utility::initializeAndValidateOptions(envoy::config::core::v3::Http2ProtocolOptions()) - .value(); - client_h2_options.mutable_use_oghttp2_codec()->set_value(true); - initialize(); - codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), client_h2_options, + codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), absl::nullopt, client_protocol_options); auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); waitForNextUpstreamRequest(); diff --git a/test/integration/protocol_integration_test.cc b/test/integration/protocol_integration_test.cc index 3625b0b58d51..921e5ba80925 100644 --- a/test/integration/protocol_integration_test.cc +++ b/test/integration/protocol_integration_test.cc @@ -2380,11 +2380,52 @@ TEST_P(DownstreamProtocolIntegrationTest, LargeRequestHeadersAccepted) { testLargeRequestHeaders(100, 1, 8192, 100); } -TEST_P(DownstreamProtocolIntegrationTest, ManyLargeRequestHeadersAccepted) { +TEST_P(ProtocolIntegrationTest, ManyLargeRequestHeadersAccepted) { // Send 70 headers each of size 100 kB with limit 8192 kB (8 MB) and 100 headers. testLargeRequestHeaders(100, 70, 8192, 100, TestUtility::DefaultTimeout); } +namespace { +uint32_t adjustMaxSingleHeaderSizeForCodecLimits(uint32_t size, + const HttpProtocolTestParams& params) { + if (params.http2_implementation == Http2Impl::Nghttp2 && + (params.downstream_protocol == Http::CodecType::HTTP2 || + params.upstream_protocol == Http::CodecType::HTTP2)) { + // nghttp2 has a hard-coded, unconfigurable limit of 64k for a header in it's header + // decompressor, so this test will always fail when using that codec. + // Reduce the size so that it can pass and receive some test coverage. + return 100; + } else if (params.downstream_protocol == Http::CodecType::HTTP3 || + params.upstream_protocol == Http::CodecType::HTTP3) { + // QUICHE has a hard-coded limit of 1024KiB in it's QPACK decoder. + // Reduce the size so that it can pass and receive some test coverage. + return 1023; + } + + return size; +} +} // namespace + +// Test a single header of the maximum allowed size. +TEST_P(ProtocolIntegrationTest, VeryLargeRequestHeadersAccepted) { + uint32_t size = adjustMaxSingleHeaderSizeForCodecLimits(8191, GetParam()); + + testLargeRequestHeaders(size, 1, 8192, 100, TestUtility::DefaultTimeout); +} + +// Test a single header of the maximum allowed size. +TEST_P(ProtocolIntegrationTest, ManyLargeResponseHeadersAccepted) { + // Send 70 headers each of size 100 kB with limit 8192 kB (8 MB) and 100 headers. + testLargeResponseHeaders(100, 70, 8192, 100, TestUtility::DefaultTimeout); +} + +// Test a single header of the maximum allowed size. +TEST_P(ProtocolIntegrationTest, VeryLargeResponseHeadersAccepted) { + uint32_t size = adjustMaxSingleHeaderSizeForCodecLimits(8191, GetParam()); + + testLargeResponseHeaders(size, 1, 8192, 100, TestUtility::DefaultTimeout); +} + TEST_P(DownstreamProtocolIntegrationTest, ManyRequestHeadersRejected) { // Send 101 empty headers with limit 60 kB and 100 headers. testLargeRequestHeaders(0, 101, 60, 80); From 82a44aec6809e423ac8e81065f2f13d4538f9332 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Tue, 24 Sep 2024 10:45:10 -0700 Subject: [PATCH 08/11] HTTP2 -> HTTP/2 in docs Signed-off-by: Greg Greenway --- api/envoy/config/core/v3/protocol.proto | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 0a7c43df338f..3c142fc924c7 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -275,8 +275,8 @@ message HttpProtocolOptions { // are set, this setting is used. // // Note: currently some protocol codecs impose limits on the maximum size of a single header: - // HTTP2 (when using nghttp2) limits a single header to around 100kb. - // HTTP3 limits a single header to around 1024kb. + // HTTP/2 (when using nghttp2) limits a single header to around 100kb. + // HTTP/3 limits a single header to around 1024kb. google.protobuf.UInt32Value max_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; // Total duration to keep alive an HTTP request/response stream. If the time limit is reached the stream will be From 24fbd128c339328e77df38be1479d666f743d9b1 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Tue, 24 Sep 2024 11:30:35 -0700 Subject: [PATCH 09/11] remove multiplexed-upstream test; it's duplicate or protocol-test Signed-off-by: Greg Greenway --- .../multiplexed_upstream_integration_test.cc | 82 ------------------- 1 file changed, 82 deletions(-) diff --git a/test/integration/multiplexed_upstream_integration_test.cc b/test/integration/multiplexed_upstream_integration_test.cc index ca1b603308d7..e2270be533a8 100644 --- a/test/integration/multiplexed_upstream_integration_test.cc +++ b/test/integration/multiplexed_upstream_integration_test.cc @@ -573,88 +573,6 @@ TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersRejected) { EXPECT_EQ("503", response->headers().getStatusValue()); } -// Tests configuration of max response headers size. -TEST_P(MultiplexedUpstreamIntegrationTest, LargeResponseHeadersAccepted) { - uint32_t limit_kb = 8192; - if (GetParam().http2_implementation == Http2Impl::Nghttp2) { - // nghttp2 has a hard-coded, unconfigurable limit of 64k for a header in it's header - // decompressor, so this test will always fail when using that codec. - // Reduce the size so that it can pass and receive some test coverage. - limit_kb = 100; - } - config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - ConfigHelper::HttpProtocolOptions protocol_options; - auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); - http_protocol_options->mutable_max_headers_kb()->set_value(limit_kb); - - ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), - protocol_options); - }); - Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); - large_headers.addCopy("large", std::string((limit_kb - 1) * 1024, 'a')); - - // This test is validating upstream response headers, but the test client will fail to receive the - // request from Envoy if its limits aren't increased. - envoy::config::core::v3::HttpProtocolOptions client_protocol_options; - client_protocol_options.mutable_max_headers_kb()->set_value(limit_kb * 2); - - initialize(); - codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), absl::nullopt, - client_protocol_options); - auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); - waitForNextUpstreamRequest(); - - upstream_request_->encodeHeaders(large_headers, false); - upstream_request_->encodeData(512, true); - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - EXPECT_EQ("200", response->headers().getStatusValue()); -} - -// Tests configuration of max response headers size when that limit is consumed by many medium sized -// headers. -TEST_P(MultiplexedUpstreamIntegrationTest, ManyLargeResponseHeadersAccepted) { - constexpr uint32_t limit_kb = 8192; - constexpr uint32_t num_hdrs = 100; - constexpr uint32_t hdr_size_kb = - (limit_kb - 1) / num_hdrs; // Subtract 1 to leave some space for other response headers. - - config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { - ConfigHelper::HttpProtocolOptions protocol_options; - auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); - http_protocol_options->mutable_max_headers_kb()->set_value(limit_kb); - http_protocol_options->mutable_max_headers_count()->set_value(1000); - - ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), - protocol_options); - }); - Http::TestResponseHeaderMapImpl large_headers(default_response_headers_); - for (uint32_t i = 0; i < num_hdrs; ++i) { - large_headers.addCopy(fmt::format("large{}", i), std::string(hdr_size_kb * 1024, 'a')); - } - - // This test is validating upstream response headers, but the test client will fail to receive the - // request from Envoy if its limits aren't increased. - envoy::config::core::v3::HttpProtocolOptions client_protocol_options; - client_protocol_options.mutable_max_headers_kb()->set_value(limit_kb * 2); - - initialize(); - codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), absl::nullopt, - client_protocol_options); - auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); - waitForNextUpstreamRequest(); - - upstream_request_->encodeHeaders(large_headers, false); - upstream_request_->encodeData(512, true); - ASSERT_TRUE(response->waitForEndStream()); - - EXPECT_TRUE(upstream_request_->complete()); - EXPECT_TRUE(response->complete()); - EXPECT_EQ("200", response->headers().getStatusValue()); -} - TEST_P(MultiplexedUpstreamIntegrationTest, NoInitialStreams) { // Set the fake upstream to start with 0 streams available. upstreamConfig().http2_options_.mutable_max_concurrent_streams()->set_value(0); From dc84aaeadaed939d3ab99975ac091773b28f2f9f Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Tue, 24 Sep 2024 14:57:58 -0700 Subject: [PATCH 10/11] rename new field to max_response_headers_kb Signed-off-by: Greg Greenway --- api/envoy/config/core/v3/protocol.proto | 15 ++-- .../v3/http_connection_manager.proto | 7 +- changelogs/current.yaml | 4 +- source/common/upstream/upstream_impl.cc | 2 +- .../network/http_connection_manager/config.cc | 22 +++--- .../http_connection_manager/config_test.cc | 71 ++++++------------- test/integration/http_integration.cc | 4 +- test/mocks/upstream/cluster_info.cc | 4 +- 8 files changed, 47 insertions(+), 82 deletions(-) diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 3c142fc924c7..ebee26c54e92 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -263,21 +263,18 @@ message HttpProtocolOptions { // Upstream responses that exceed this limit will result in a 503 response. google.protobuf.UInt32Value max_headers_count = 2 [(validate.rules).uint32 = {gte: 1}]; - // The maximum size of headers (request headers if configured on HttpConnectionManager, response headers - // when configured on a cluster). + // The maximum size of response headers. // If unconfigured, the default is 60 KiB, except for HTTP/1 response headers which have a default // of 80KiB. - // Downstream requests that exceed this limit will receive a 431 response for HTTP/1.x and cause a stream - // reset for HTTP/2. - // Upstream responses that exceed this limit will result in a 503 response. - // If both this setting and :ref:`max_request_headers_kb - // ` - // are set, this setting is used. + // Responses that exceed this limit will result in a 503 response. + // In Envoy, this setting is only valid when configured on an upstream cluster, not on the + // :ref:`HTTP Connection Manager + // `. // // Note: currently some protocol codecs impose limits on the maximum size of a single header: // HTTP/2 (when using nghttp2) limits a single header to around 100kb. // HTTP/3 limits a single header to around 1024kb. - google.protobuf.UInt32Value max_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; + google.protobuf.UInt32Value max_response_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; // 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. diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index 0c0ad980d940..3d438ae87881 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -494,9 +494,10 @@ message HttpConnectionManager { // The maximum request headers size for incoming connections. // If unconfigured, the default max request headers allowed is 60 KiB. // Requests that exceed this limit will receive a 431 response. - // If both this setting and :ref:`max_headers_kb - // ` are set, - // that setting is used and this one is ignored. + // + // Note: currently some protocol codecs impose limits on the maximum size of a single header: + // HTTP/2 (when using nghttp2) limits a single header to around 100kb. + // HTTP/3 limits a single header to around 1024kb. google.protobuf.UInt32Value max_request_headers_kb = 29 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; diff --git a/changelogs/current.yaml b/changelogs/current.yaml index a0ba00a5fa3f..f491052a9026 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -266,8 +266,8 @@ new_features: ``%START_TIME%``) to use ``%E#S``, ``%E*S``, ``%E#f`` and ``%E*f`` to format the subsecond part of the timepoint. - area: http change: | - Added configuration setting for the :ref:`maximum size of headers - ` in responses. + Added configuration setting for the :ref:`maximum size of response headers + ` in responses. - area: http_11_proxy change: | Added the option to configure the transport socket via locality or endpoint metadata. diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 1f01a0ff5b2e..195d2ba59acc 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -1230,7 +1230,7 @@ ClusterInfoImpl::ClusterInfoImpl( runtime_.snapshot().getInteger(Http::MaxResponseHeadersCountOverrideKey, Http::DEFAULT_MAX_HEADERS_COUNT))), max_response_headers_kb_(PROTOBUF_GET_OPTIONAL_WRAPPED( - http_protocol_options_->common_http_protocol_options_, max_headers_kb)), + http_protocol_options_->common_http_protocol_options_, max_response_headers_kb)), type_(config.type()), drain_connections_on_host_removal_(config.ignore_health_on_host_removal()), connection_pool_per_downstream_connection_( diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index eb587a82507b..757c2439d422 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -369,11 +369,9 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( config.stream_error_on_invalid_http_message(), xff_num_trusted_hops_ == 0 && use_remote_address_)), max_request_headers_kb_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config.common_http_protocol_options(), max_headers_kb, - PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_request_headers_kb, - context.serverFactoryContext().runtime().snapshot().getInteger( - Http::MaxRequestHeadersSizeOverrideKey, Http::DEFAULT_MAX_REQUEST_HEADERS_KB)))), + config, max_request_headers_kb, + context.serverFactoryContext().runtime().snapshot().getInteger( + Http::MaxRequestHeadersSizeOverrideKey, Http::DEFAULT_MAX_REQUEST_HEADERS_KB))), max_request_headers_count_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( config.common_http_protocol_options(), max_headers_count, context.serverFactoryContext().runtime().snapshot().getInteger( @@ -434,14 +432,6 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( return; } - if (config.has_max_request_headers_kb() && - config.common_http_protocol_options().has_max_headers_kb() && - config.max_request_headers_kb().value() != - config.common_http_protocol_options().max_headers_kb().value()) { - ENVOY_LOG(warn, "Both `max_request_headers_kb` and `max_headers_kb` are configured. Ignoring " - "`max_request_headers_kb`."); - } - auto options_or_error = Http2::Utility::initializeAndValidateOptions( config.http2_protocol_options(), config.has_stream_error_on_invalid_http_message(), config.stream_error_on_invalid_http_message()); @@ -453,6 +443,12 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( idle_timeout_ = absl::nullopt; } + if (config.common_http_protocol_options().has_max_response_headers_kb()) { + creation_status = absl::InvalidArgumentError( + fmt::format("Error: max_response_headers_kb cannot be set on http_connection_manager.")); + return; + } + if (config.strip_any_host_port() && config.strip_matching_host_port()) { creation_status = absl::InvalidArgumentError(fmt::format( "Error: Only one of `strip_matching_host_port` or `strip_any_host_port` can be set.")); diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index 599dada4bf20..06fa2cf5c194 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -826,56 +826,6 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbConfigured) { EXPECT_EQ(16, config.maxRequestHeadersKb()); } -TEST_F(HttpConnectionManagerConfigTest, MaxHeadersKbConfigured) { - const std::string yaml_string = R"EOF( - stat_prefix: ingress_http - common_http_protocol_options: - max_headers_kb: 16 - route_config: - name: local_route - http_filters: - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - )EOF"; - - HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, - date_provider_, route_config_provider_manager_, - &scoped_routes_config_provider_manager_, tracer_manager_, - filter_config_provider_manager_, creation_status_); - ASSERT_TRUE(creation_status_.ok()); - EXPECT_EQ(16, config.maxRequestHeadersKb()); -} - -TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbAndMaxHeadersKbConfigured) { - const std::string yaml_string = R"EOF( - stat_prefix: ingress_http - common_http_protocol_options: - max_headers_kb: 16 - max_request_headers_kb: 32 - route_config: - name: local_route - http_filters: - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - )EOF"; - - EXPECT_LOG_CONTAINS("warn", - "Both `max_request_headers_kb` and `max_headers_kb` are configured. Ignoring " - "`max_request_headers_kb`.", - { - HttpConnectionManagerConfig config( - parseHttpConnectionManagerFromYaml(yaml_string), context_, - date_provider_, route_config_provider_manager_, - &scoped_routes_config_provider_manager_, tracer_manager_, - filter_config_provider_manager_, creation_status_); - - ASSERT_TRUE(creation_status_.ok()); - EXPECT_EQ(16, config.maxRequestHeadersKb()); - }); -} - TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeadersKbMaxConfigurable) { const std::string yaml_string = R"EOF( stat_prefix: ingress_http @@ -1046,6 +996,27 @@ TEST_F(HttpConnectionManagerConfigTest, MaxRequestHeaderCountConfigurable) { EXPECT_EQ(200, config.maxRequestHeadersCount()); } +// Check that max response header size is invalid on HCM. +TEST_F(HttpConnectionManagerConfigTest, MaxResponseHeaderKbInvalid) { + const std::string yaml_string = R"EOF( + stat_prefix: ingress_http + common_http_protocol_options: + max_response_headers_kb: 200 + route_config: + name: local_route + http_filters: + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + )EOF"; + + HttpConnectionManagerConfig config(parseHttpConnectionManagerFromYaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + &scoped_routes_config_provider_manager_, tracer_manager_, + filter_config_provider_manager_, creation_status_); + EXPECT_FALSE(creation_status_.ok()); +} + // Checking that default max_requests_per_connection is 0. TEST_F(HttpConnectionManagerConfigTest, DefaultMaxRequestPerConnection) { const std::string yaml_string = R"EOF( diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 21ab4c6c05cf..c640f452141b 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -1475,7 +1475,7 @@ void HttpIntegrationTest::testLargeResponseHeaders(uint32_t size, uint32_t count config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { ConfigHelper::HttpProtocolOptions protocol_options; auto* http_protocol_options = protocol_options.mutable_common_http_protocol_options(); - http_protocol_options->mutable_max_headers_kb()->set_value(max_size); + http_protocol_options->mutable_max_response_headers_kb()->set_value(max_size); http_protocol_options->mutable_max_headers_count()->set_value(max_count); ConfigHelper::setProtocolOptions(*bootstrap.mutable_static_resources()->mutable_clusters(0), @@ -1485,7 +1485,7 @@ void HttpIntegrationTest::testLargeResponseHeaders(uint32_t size, uint32_t count // This test is validating upstream response headers, but the test client will fail to receive the // request from Envoy if its limits aren't increased. envoy::config::core::v3::HttpProtocolOptions client_protocol_options; - client_protocol_options.mutable_max_headers_kb()->set_value(max_size); + client_protocol_options.mutable_max_response_headers_kb()->set_value(max_size); client_protocol_options.mutable_max_headers_count()->set_value(max_count); Http::TestRequestHeaderMapImpl big_headers(default_response_headers_); diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index dce4fb332e8a..8177186b0b4e 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -97,8 +97,8 @@ MockClusterInfo::MockClusterInfo() ON_CALL(*this, maxResponseHeadersCount()) .WillByDefault(ReturnPointee(&max_response_headers_count_)); ON_CALL(*this, maxResponseHeadersKb()).WillByDefault(Invoke([this]() -> absl::optional { - if (common_http_protocol_options_.has_max_headers_kb()) { - return common_http_protocol_options_.max_headers_kb().value(); + if (common_http_protocol_options_.has_max_response_headers_kb()) { + return common_http_protocol_options_.max_response_headers_kb().value(); } else { return absl::nullopt; } From 639cfbd19aa9044908ec603b2287187d00bca9a7 Mon Sep 17 00:00:00 2001 From: Greg Greenway Date: Tue, 24 Sep 2024 15:18:22 -0700 Subject: [PATCH 11/11] fix proto format Signed-off-by: Greg Greenway --- api/envoy/config/core/v3/protocol.proto | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index ebee26c54e92..2bf36d704c1e 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -274,7 +274,8 @@ message HttpProtocolOptions { // Note: currently some protocol codecs impose limits on the maximum size of a single header: // HTTP/2 (when using nghttp2) limits a single header to around 100kb. // HTTP/3 limits a single header to around 1024kb. - google.protobuf.UInt32Value max_response_headers_kb = 7 [(validate.rules).uint32 = {lte: 8192 gt: 0}]; + google.protobuf.UInt32Value max_response_headers_kb = 7 + [(validate.rules).uint32 = {lte: 8192 gt: 0}]; // 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.