diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index a666d9a762f6b..4109b19a4abd3 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -418,9 +418,15 @@ message GrpcProtocolOptions { // [#not-implemented-hide:] // -// A message which allows using HTTP/3 as an upstream protocol. -// -// Eventually this will include configuration for tuning HTTP/3. +// A message which allows using HTTP/3. message Http3ProtocolOptions { QuicProtocolOptions quic_protocol_options = 1; + + // Allows invalid HTTP messaging and headers. When this option is disabled (default), then + // the whole HTTP/3 connection is terminated upon receiving invalid HEADERS frame. However, + // when this option is enabled, only the offending stream is terminated. + // + // If set, this overrides any HCM :ref:`stream_error_on_invalid_http_messaging + // `. + google.protobuf.BoolValue override_stream_error_on_invalid_http_message = 2; } diff --git a/api/envoy/config/core/v4alpha/protocol.proto b/api/envoy/config/core/v4alpha/protocol.proto index 7cc09a5fbadbd..1e14ae5ee999a 100644 --- a/api/envoy/config/core/v4alpha/protocol.proto +++ b/api/envoy/config/core/v4alpha/protocol.proto @@ -412,12 +412,18 @@ message GrpcProtocolOptions { // [#not-implemented-hide:] // -// A message which allows using HTTP/3 as an upstream protocol. -// -// Eventually this will include configuration for tuning HTTP/3. +// A message which allows using HTTP/3. message Http3ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.Http3ProtocolOptions"; QuicProtocolOptions quic_protocol_options = 1; + + // Allows invalid HTTP messaging and headers. When this option is disabled (default), then + // the whole HTTP/3 connection is terminated upon receiving invalid HEADERS frame. However, + // when this option is enabled, only the offending stream is terminated. + // + // If set, this overrides any HCM :ref:`stream_error_on_invalid_http_messaging + // `. + google.protobuf.BoolValue override_stream_error_on_invalid_http_message = 2; } 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 d28a3b0271161..51c4e229f1375 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 @@ -34,7 +34,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 44] +// [#next-free-field: 45] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -325,6 +325,10 @@ message HttpConnectionManager { config.core.v3.Http2ProtocolOptions http2_protocol_options = 9 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // Additional HTTP/3 settings that are passed directly to the HTTP/3 codec. + // [#not-implemented-hide:] + config.core.v3.Http3ProtocolOptions http3_protocol_options = 44; + // An optional override that the connection manager will write to the server // header in responses. If not set, the default is *envoy*. string server_name = 10 diff --git a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 03f940625b1f9..d442451cdae62 100644 --- a/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/api/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -33,7 +33,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 44] +// [#next-free-field: 45] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; @@ -328,6 +328,10 @@ message HttpConnectionManager { config.core.v4alpha.Http2ProtocolOptions http2_protocol_options = 9 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // Additional HTTP/3 settings that are passed directly to the HTTP/3 codec. + // [#not-implemented-hide:] + config.core.v4alpha.Http3ProtocolOptions http3_protocol_options = 44; + // An optional override that the connection manager will write to the server // header in responses. If not set, the default is *envoy*. string server_name = 10 diff --git a/docs/root/configuration/http/http_conn_man/response_code_details.rst b/docs/root/configuration/http/http_conn_man/response_code_details.rst index 54d6e10eb2cd2..ebca3a321f2e7 100644 --- a/docs/root/configuration/http/http_conn_man/response_code_details.rst +++ b/docs/root/configuration/http/http_conn_man/response_code_details.rst @@ -99,3 +99,16 @@ All http2 details are rooted at *http2.* http2.unexpected_underscore, Envoy was configured to drop requests with header keys beginning with underscores. http2.unknown.nghttp2.error, An unknown error was encountered by nghttp2 http2.violation.of.messaging.rule, The stream was in violation of a HTTP/2 messaging rule. + +Http3 details +~~~~~~~~~~~~~ + +All http3 details are rooted at *http3.* + +.. csv-table:: + :header: Name, Description + :widths: 1, 2 + + http3.invalid_header_field, One of the HTTP/3 headers was invalid + http3.headers_too_large, The size of headers (or trailers) exceeded the configured limits + http3.unexpected_underscore, Envoy was configured to drop or reject requests with header keys beginning with underscores. diff --git a/generated_api_shadow/envoy/config/core/v3/protocol.proto b/generated_api_shadow/envoy/config/core/v3/protocol.proto index a666d9a762f6b..4109b19a4abd3 100644 --- a/generated_api_shadow/envoy/config/core/v3/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v3/protocol.proto @@ -418,9 +418,15 @@ message GrpcProtocolOptions { // [#not-implemented-hide:] // -// A message which allows using HTTP/3 as an upstream protocol. -// -// Eventually this will include configuration for tuning HTTP/3. +// A message which allows using HTTP/3. message Http3ProtocolOptions { QuicProtocolOptions quic_protocol_options = 1; + + // Allows invalid HTTP messaging and headers. When this option is disabled (default), then + // the whole HTTP/3 connection is terminated upon receiving invalid HEADERS frame. However, + // when this option is enabled, only the offending stream is terminated. + // + // If set, this overrides any HCM :ref:`stream_error_on_invalid_http_messaging + // `. + google.protobuf.BoolValue override_stream_error_on_invalid_http_message = 2; } diff --git a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto index 72e00ac0ff90d..85032f6637351 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto @@ -424,12 +424,18 @@ message GrpcProtocolOptions { // [#not-implemented-hide:] // -// A message which allows using HTTP/3 as an upstream protocol. -// -// Eventually this will include configuration for tuning HTTP/3. +// A message which allows using HTTP/3. message Http3ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.Http3ProtocolOptions"; QuicProtocolOptions quic_protocol_options = 1; + + // Allows invalid HTTP messaging and headers. When this option is disabled (default), then + // the whole HTTP/3 connection is terminated upon receiving invalid HEADERS frame. However, + // when this option is enabled, only the offending stream is terminated. + // + // If set, this overrides any HCM :ref:`stream_error_on_invalid_http_messaging + // `. + google.protobuf.BoolValue override_stream_error_on_invalid_http_message = 2; } diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto index df419eefb8e7e..66a75d3aefcbe 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto @@ -36,7 +36,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 44] +// [#next-free-field: 45] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager"; @@ -331,6 +331,10 @@ message HttpConnectionManager { config.core.v3.Http2ProtocolOptions http2_protocol_options = 9 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // Additional HTTP/3 settings that are passed directly to the HTTP/3 codec. + // [#not-implemented-hide:] + config.core.v3.Http3ProtocolOptions http3_protocol_options = 44; + // An optional override that the connection manager will write to the server // header in responses. If not set, the default is *envoy*. string server_name = 10 diff --git a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto index 03f940625b1f9..d442451cdae62 100644 --- a/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto +++ b/generated_api_shadow/envoy/extensions/filters/network/http_connection_manager/v4alpha/http_connection_manager.proto @@ -33,7 +33,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 44] +// [#next-free-field: 45] message HttpConnectionManager { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"; @@ -328,6 +328,10 @@ message HttpConnectionManager { config.core.v4alpha.Http2ProtocolOptions http2_protocol_options = 9 [(udpa.annotations.security).configure_for_untrusted_downstream = true]; + // Additional HTTP/3 settings that are passed directly to the HTTP/3 codec. + // [#not-implemented-hide:] + config.core.v4alpha.Http3ProtocolOptions http3_protocol_options = 44; + // An optional override that the connection manager will write to the server // header in responses. If not set, the default is *envoy*. string server_name = 10 diff --git a/include/envoy/http/codec.h b/include/envoy/http/codec.h index cd003751430c9..70544787ca44d 100644 --- a/include/envoy/http/codec.h +++ b/include/envoy/http/codec.h @@ -27,6 +27,10 @@ namespace Http2 { struct CodecStats; } +namespace Http3 { +struct CodecStats; +} + // Legacy default value of 60K is safely under both codec default limits. static constexpr uint32_t DEFAULT_MAX_REQUEST_HEADERS_KB = 60; // Default maximum number of headers. diff --git a/include/envoy/upstream/upstream.h b/include/envoy/upstream/upstream.h index 6a5cb1a151c21..dc628e6139dbc 100644 --- a/include/envoy/upstream/upstream.h +++ b/include/envoy/upstream/upstream.h @@ -995,6 +995,11 @@ class ClusterInfo { */ virtual Http::Http2::CodecStats& http2CodecStats() const PURE; + /** + * @return the Http3 Codec Stats. + */ + virtual Http::Http3::CodecStats& http3CodecStats() const PURE; + protected: /** * Invoked by extensionProtocolOptionsTyped. diff --git a/source/common/http/BUILD b/source/common/http/BUILD index 796eb0e53228a..33cb754e6aa58 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -436,6 +436,7 @@ envoy_cc_library( "//source/common/common:utility_lib", "//source/common/protobuf:utility_lib", "//source/common/runtime:runtime_features_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", "@envoy_api//envoy/type/v3:pkg_cc_proto", ], diff --git a/source/common/http/codec_client.cc b/source/common/http/codec_client.cc index d6c2d43d4fad1..9110962936cd8 100644 --- a/source/common/http/codec_client.cc +++ b/source/common/http/codec_client.cc @@ -186,7 +186,9 @@ CodecClientProd::CodecClientProd(Type type, Network::ClientConnectionPtr&& conne case Type::HTTP3: { #ifdef ENVOY_ENABLE_QUIC codec_ = std::make_unique( - dynamic_cast(*connection_), *this); + dynamic_cast(*connection_), *this, + host->cluster().http3CodecStats(), host->cluster().http3Options(), + Http::DEFAULT_MAX_REQUEST_HEADERS_KB); break; #else // Should be blocked by configuration checking at an earlier point. diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index 05482e6ad2c8d..5d8dc4f3a8996 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -333,5 +333,57 @@ bool HeaderUtility::isModifiableHeader(absl::string_view header) { !absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get())); } +HeaderUtility::HeaderValidationResult HeaderUtility::checkHeaderNameForUnderscores( + const std::string& header_name, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action, + Stats::Counter& dropped_headers_with_underscores, + Stats::Counter& requests_rejected_with_underscores_in_headers) { + if (headers_with_underscores_action == envoy::config::core::v3::HttpProtocolOptions::ALLOW || + !HeaderUtility::headerNameContainsUnderscore(header_name)) { + return HeaderValidationResult::ACCEPT; + } + if (headers_with_underscores_action == + envoy::config::core::v3::HttpProtocolOptions::DROP_HEADER) { + ENVOY_LOG_MISC(debug, "Dropping header with invalid characters in its name: {}", header_name); + dropped_headers_with_underscores.inc(); + return HeaderValidationResult::DROP; + } + ENVOY_LOG_MISC(debug, "Rejecting request due to header name with underscores: {}", header_name); + requests_rejected_with_underscores_in_headers.inc(); + return HeaderUtility::HeaderValidationResult::REJECT; +} + +HeaderUtility::HeaderValidationResult +HeaderUtility::validateContentLength(absl::string_view header_value, + bool override_stream_error_on_invalid_http_message, + bool& should_close_connection) { + should_close_connection = false; + std::vector values = absl::StrSplit(header_value, ','); + absl::optional content_length; + for (const absl::string_view& value : values) { + uint64_t new_value; + if (!absl::SimpleAtoi(value, &new_value) || + !std::all_of(value.begin(), value.end(), absl::ascii_isdigit)) { + ENVOY_LOG_MISC(debug, "Content length was either unparseable or negative"); + should_close_connection = !override_stream_error_on_invalid_http_message; + return HeaderValidationResult::REJECT; + } + if (!content_length.has_value()) { + content_length = new_value; + continue; + } + if (new_value != content_length.value()) { + ENVOY_LOG_MISC( + debug, + "Parsed content length {} is inconsistent with previously detected content length {}", + new_value, content_length.value()); + should_close_connection = !override_stream_error_on_invalid_http_message; + return HeaderValidationResult::REJECT; + } + } + return HeaderValidationResult::ACCEPT; +} + } // namespace Http } // namespace Envoy diff --git a/source/common/http/header_utility.h b/source/common/http/header_utility.h index 5fa5b66fc8677..e5d58f39ca8e4 100644 --- a/source/common/http/header_utility.h +++ b/source/common/http/header_utility.h @@ -3,6 +3,7 @@ #include #include "envoy/common/regex.h" +#include "envoy/config/core/v3/protocol.pb.h" #include "envoy/config/route/v3/route_components.pb.h" #include "envoy/http/header_map.h" #include "envoy/http/protocol.h" @@ -209,6 +210,35 @@ class HeaderUtility { * may not be modified. */ static bool isModifiableHeader(absl::string_view header); + + enum class HeaderValidationResult { + ACCEPT = 0, + DROP, + REJECT, + }; + + /** + * Check if the given header_name has underscore. + * Return HeaderValidationResult and populate the given counters based on + * headers_with_underscores_action. + */ + static HeaderValidationResult checkHeaderNameForUnderscores( + const std::string& header_name, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action, + Stats::Counter& dropped_headers_with_underscores, + Stats::Counter& requests_rejected_with_underscores_in_headers); + + /** + * Check if header_value represents a valid value for HTTP content-length header. + * Return HeaderValidationResult and populate should_close_connection + * according to override_stream_error_on_invalid_http_message. + */ + static HeaderValidationResult + validateContentLength(absl::string_view header_value, + bool override_stream_error_on_invalid_http_message, + bool& should_close_connection); }; + } // namespace Http } // namespace Envoy diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index e0505d3265cd2..3b245d4f8712e 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -474,6 +474,7 @@ Status ConnectionImpl::completeLastHeader() { ENVOY_CONN_LOG(trace, "completed header: key={} value={}", connection_, current_header_field_.getStringView(), current_header_value_.getStringView()); + // TODO(10646): Switch to use HeaderUtility::checkHeaderNameForUnderscores(). RETURN_IF_ERROR(checkHeaderNameForUnderscores()); auto& headers_or_trailers = headersOrTrailers(); if (!current_header_field_.empty()) { diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index 125a837ce814d..41c083acf2acb 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -1085,6 +1085,7 @@ int ConnectionImpl::saveHeader(const nghttp2_frame* frame, HeaderString&& name, return 0; } + // TODO(10646): Switch to use HeaderUtility::checkHeaderNameForUnderscores(). auto should_return = checkHeaderNameForUnderscores(name.getStringView()); if (should_return) { stream->setDetails(Http2ResponseCodeDetails::get().invalid_underscore); diff --git a/source/common/http/http3/BUILD b/source/common/http/http3/BUILD index a89c441ad794c..62021588859a8 100644 --- a/source/common/http/http3/BUILD +++ b/source/common/http/http3/BUILD @@ -26,3 +26,13 @@ envoy_cc_library( name = "quic_client_connection_factory_lib", hdrs = ["quic_client_connection_factory.h"], ) + +envoy_cc_library( + name = "codec_stats_lib", + hdrs = ["codec_stats.h"], + deps = [ + "//include/envoy/stats:stats_interface", + "//include/envoy/stats:stats_macros", + "//source/common/common:thread_lib", + ], +) diff --git a/source/common/http/http3/codec_stats.h b/source/common/http/http3/codec_stats.h new file mode 100644 index 0000000000000..bb720172907cd --- /dev/null +++ b/source/common/http/http3/codec_stats.h @@ -0,0 +1,44 @@ +#pragma once + +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +#include "common/common/thread.h" + +namespace Envoy { +namespace Http { +namespace Http3 { + +/** + * All stats for the HTTP/3 codec. @see stats_macros.h + * TODO(danzh) populate all of them in codec. + */ +#define ALL_HTTP3_CODEC_STATS(COUNTER, GAUGE) \ + COUNTER(dropped_headers_with_underscores) \ + COUNTER(header_overflow) \ + COUNTER(requests_rejected_with_underscores_in_headers) \ + COUNTER(rx_messaging_error) \ + COUNTER(rx_reset) \ + COUNTER(trailers) \ + COUNTER(tx_reset) \ + GAUGE(streams_active, Accumulate) + +/** + * Wrapper struct for the HTTP/3 codec stats. @see stats_macros.h + */ +struct CodecStats { + using AtomicPtr = Thread::AtomicPtr; + + static CodecStats& atomicGet(AtomicPtr& ptr, Stats::Scope& scope) { + return *ptr.get([&scope]() -> CodecStats* { + return new CodecStats{ALL_HTTP3_CODEC_STATS(POOL_COUNTER_PREFIX(scope, "http3."), + POOL_GAUGE_PREFIX(scope, "http3."))}; + }); + } + + ALL_HTTP3_CODEC_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT) +}; + +} // namespace Http3 +} // namespace Http +} // namespace Envoy diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index cd466c0506e7d..2ee0b2f7fce34 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -201,6 +201,29 @@ initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions } // namespace Utility } // namespace Http2 +namespace Http3 { +namespace Utility { +envoy::config::core::v3::Http3ProtocolOptions +initializeAndValidateOptions(const envoy::config::core::v3::Http3ProtocolOptions& options, + bool hcm_stream_error_set, + const Protobuf::BoolValue& hcm_stream_error) { + if (options.has_override_stream_error_on_invalid_http_message()) { + return options; + } + envoy::config::core::v3::Http3ProtocolOptions options_clone(options); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.hcm_stream_error_on_invalid_message") && + hcm_stream_error_set) { + options_clone.mutable_override_stream_error_on_invalid_http_message()->set_value( + hcm_stream_error.value()); + } else { + options_clone.mutable_override_stream_error_on_invalid_http_message()->set_value(false); + } + return options_clone; +} + +} // namespace Utility +} // namespace Http3 namespace Http { diff --git a/source/common/http/utility.h b/source/common/http/utility.h index e2e08b510edc5..64d85613ee1a3 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -116,7 +116,15 @@ initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions const Protobuf::BoolValue& hcm_stream_error); } // namespace Utility } // namespace Http2 +namespace Http3 { +namespace Utility { +envoy::config::core::v3::Http3ProtocolOptions +initializeAndValidateOptions(const envoy::config::core::v3::Http3ProtocolOptions& options, + bool hcm_stream_error_set, + const Protobuf::BoolValue& hcm_stream_error); +} // namespace Utility +} // namespace Http3 namespace Http { namespace Utility { diff --git a/source/common/quic/BUILD b/source/common/quic/BUILD index 7bbc12b188ee7..506ee80b4294f 100644 --- a/source/common/quic/BUILD +++ b/source/common/quic/BUILD @@ -128,11 +128,13 @@ envoy_cc_library( tags = ["nofips"], deps = [ ":envoy_quic_simulated_watermark_buffer_lib", + ":envoy_quic_utils_lib", ":quic_filter_manager_connection_lib", ":send_buffer_monitor_lib", "//include/envoy/event:dispatcher_interface", "//include/envoy/http:codec_interface", "//source/common/http:codec_helper_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) @@ -187,6 +189,7 @@ envoy_cc_library( "//source/common/common:assert_lib", "//source/common/common:empty_string", "//source/common/http:header_map_lib", + "//source/common/http/http3:codec_stats_lib", "//source/common/network:connection_base_lib", "//source/common/stream_info:stream_info_lib", ], @@ -347,6 +350,7 @@ envoy_cc_library( deps = [ "//include/envoy/http:codec_interface", "//source/common/http:header_map_lib", + "//source/common/http:header_utility_lib", "//source/common/network:address_lib", "//source/common/network:listen_socket_lib", "//source/common/network:socket_option_factory_lib", diff --git a/source/common/quic/codec_impl.cc b/source/common/quic/codec_impl.cc index 86ded99aa1799..2dba873f8b880 100644 --- a/source/common/quic/codec_impl.cc +++ b/source/common/quic/codec_impl.cc @@ -19,8 +19,16 @@ EnvoyQuicClientStream* quicStreamToEnvoyClientStream(quic::QuicStream* stream) { bool QuicHttpConnectionImplBase::wantsToWrite() { return quic_session_.bytesToSend() > 0; } QuicHttpServerConnectionImpl::QuicHttpServerConnectionImpl( - EnvoyQuicServerSession& quic_session, Http::ServerConnectionCallbacks& callbacks) - : QuicHttpConnectionImplBase(quic_session), quic_server_session_(quic_session) { + EnvoyQuicServerSession& quic_session, Http::ServerConnectionCallbacks& callbacks, + Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + const uint32_t /*max_request_headers_kb*/, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action) + : QuicHttpConnectionImplBase(quic_session, stats), quic_server_session_(quic_session) { + quic_session.setCodecStats(stats); + quic_session.setHttp3Options(http3_options); + quic_session.setHeadersWithUnderscoreAction(headers_with_underscores_action); quic_session.setHttpConnectionCallbacks(callbacks); } @@ -56,9 +64,14 @@ void QuicHttpServerConnectionImpl::goAway() { } } -QuicHttpClientConnectionImpl::QuicHttpClientConnectionImpl(EnvoyQuicClientSession& session, - Http::ConnectionCallbacks& callbacks) - : QuicHttpConnectionImplBase(session), quic_client_session_(session) { +QuicHttpClientConnectionImpl::QuicHttpClientConnectionImpl( + EnvoyQuicClientSession& session, Http::ConnectionCallbacks& callbacks, + Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + const uint32_t /*max_request_headers_kb*/) + : QuicHttpConnectionImplBase(session, stats), quic_client_session_(session) { + session.setCodecStats(stats); + session.setHttp3Options(http3_options); session.setHttpConnectionCallbacks(callbacks); } diff --git a/source/common/quic/codec_impl.h b/source/common/quic/codec_impl.h index 240e7e7dd29e6..efd38ea18c429 100644 --- a/source/common/quic/codec_impl.h +++ b/source/common/quic/codec_impl.h @@ -17,8 +17,9 @@ namespace Quic { class QuicHttpConnectionImplBase : public virtual Http::Connection, protected Logger::Loggable { public: - QuicHttpConnectionImplBase(QuicFilterManagerConnectionImpl& quic_session) - : quic_session_(quic_session) {} + QuicHttpConnectionImplBase(QuicFilterManagerConnectionImpl& quic_session, + Http::Http3::CodecStats& stats) + : quic_session_(quic_session), stats_(stats) {} // Http::Connection Http::Status dispatch(Buffer::Instance& /*data*/) override { @@ -32,13 +33,19 @@ class QuicHttpConnectionImplBase : public virtual Http::Connection, protected: QuicFilterManagerConnectionImpl& quic_session_; + Http::Http3::CodecStats& stats_; }; class QuicHttpServerConnectionImpl : public QuicHttpConnectionImplBase, public Http::ServerConnection { public: - QuicHttpServerConnectionImpl(EnvoyQuicServerSession& quic_session, - Http::ServerConnectionCallbacks& callbacks); + QuicHttpServerConnectionImpl( + EnvoyQuicServerSession& quic_session, Http::ServerConnectionCallbacks& callbacks, + Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + const uint32_t max_request_headers_kb, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action); // Http::Connection void goAway() override; @@ -54,7 +61,9 @@ class QuicHttpClientConnectionImpl : public QuicHttpConnectionImplBase, public Http::ClientConnection { public: QuicHttpClientConnectionImpl(EnvoyQuicClientSession& session, - Http::ConnectionCallbacks& callbacks); + Http::ConnectionCallbacks& callbacks, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + const uint32_t max_request_headers_kb); // Http::ClientConnection Http::RequestEncoder& newStream(Http::ResponseDecoder& response_decoder) override; diff --git a/source/common/quic/envoy_quic_client_session.cc b/source/common/quic/envoy_quic_client_session.cc index 68b2309a123a4..381711f938289 100644 --- a/source/common/quic/envoy_quic_client_session.cc +++ b/source/common/quic/envoy_quic_client_session.cc @@ -86,8 +86,10 @@ void EnvoyQuicClientSession::SetDefaultEncryptionLevel(quic::EncryptionLevel lev } std::unique_ptr EnvoyQuicClientSession::CreateClientStream() { + ASSERT(codec_stats_.has_value() && http3_options_.has_value()); return std::make_unique(GetNextOutgoingBidirectionalStreamId(), this, - quic::BIDIRECTIONAL); + quic::BIDIRECTIONAL, codec_stats_.value(), + http3_options_.value()); } quic::QuicSpdyStream* EnvoyQuicClientSession::CreateIncomingStream(quic::QuicStreamId /*id*/) { diff --git a/source/common/quic/envoy_quic_client_stream.cc b/source/common/quic/envoy_quic_client_stream.cc index b9aae50c0c249..eae882079a78e 100644 --- a/source/common/quic/envoy_quic_client_stream.cc +++ b/source/common/quic/envoy_quic_client_stream.cc @@ -29,25 +29,29 @@ namespace Envoy { namespace Quic { -EnvoyQuicClientStream::EnvoyQuicClientStream(quic::QuicStreamId id, - quic::QuicSpdyClientSession* client_session, - quic::StreamType type) +EnvoyQuicClientStream::EnvoyQuicClientStream( + quic::QuicStreamId id, quic::QuicSpdyClientSession* client_session, quic::StreamType type, + Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options) : quic::QuicSpdyClientStream(id, client_session, type), EnvoyQuicStream( // Flow control receive window should be larger than 8k so that the send buffer can fully // utilize congestion control window before it reaches the high watermark. static_cast(GetReceiveWindow().value()), *filterManagerConnection(), - [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }) { + [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }, + stats, http3_options) { ASSERT(GetReceiveWindow() > 8 * 1024, "Send buffer limit should be larger than 8KB."); } -EnvoyQuicClientStream::EnvoyQuicClientStream(quic::PendingStream* pending, - quic::QuicSpdyClientSession* client_session, - quic::StreamType type) +EnvoyQuicClientStream::EnvoyQuicClientStream( + quic::PendingStream* pending, quic::QuicSpdyClientSession* client_session, + quic::StreamType type, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options) : quic::QuicSpdyClientStream(pending, client_session, type), EnvoyQuicStream( static_cast(GetReceiveWindow().value()), *filterManagerConnection(), - [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }) {} + [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }, + stats, http3_options) {} Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& headers, bool end_stream) { @@ -127,7 +131,7 @@ void EnvoyQuicClientStream::OnInitialHeadersComplete(bool fin, size_t frame_len, } quic::QuicSpdyStream::OnInitialHeadersComplete(fin, frame_len, header_list); if (!headers_decompressed() || header_list.empty()) { - Reset(quic::QUIC_BAD_APPLICATION_PAYLOAD); + onStreamError(!http3_options_.override_stream_error_on_invalid_http_message().value()); return; } @@ -136,11 +140,16 @@ void EnvoyQuicClientStream::OnInitialHeadersComplete(bool fin, size_t frame_len, end_stream_decoded_ = true; } std::unique_ptr headers = - quicHeadersToEnvoyHeaders(header_list); + quicHeadersToEnvoyHeaders(header_list, *this); + if (headers == nullptr) { + onStreamError(close_connection_upon_invalid_header_); + return; + } const absl::optional optional_status = Http::Utility::getResponseStatusNoThrow(*headers); if (!optional_status.has_value()) { - Reset(quic::QUIC_BAD_APPLICATION_PAYLOAD); + details_ = Http3ResponseCodeDetailValues::invalid_http_header; + onStreamError(!http3_options_.override_stream_error_on_invalid_http_message().value()); return; } const uint64_t status = optional_status.value(); @@ -258,7 +267,9 @@ void EnvoyQuicClientStream::Reset(quic::QuicRstStreamErrorCode error) { void EnvoyQuicClientStream::OnConnectionClosed(quic::QuicErrorCode error, quic::ConnectionCloseSource source) { if (!end_stream_decoded_) { - runResetCallbacks(quicErrorCodeToEnvoyResetReason(error)); + runResetCallbacks(source == quic::ConnectionCloseSource::FROM_SELF + ? quicErrorCodeToEnvoyLocalResetReason(error) + : quicErrorCodeToEnvoyRemoteResetReason(error)); } quic::QuicSpdyClientStream::OnConnectionClosed(error, source); } @@ -296,5 +307,23 @@ QuicFilterManagerConnectionImpl* EnvoyQuicClientStream::filterManagerConnection( return dynamic_cast(session()); } +void EnvoyQuicClientStream::onStreamError(absl::optional should_close_connection) { + if (details_.empty()) { + details_ = Http3ResponseCodeDetailValues::invalid_http_header; + } + bool close_connection_upon_invalid_header; + if (should_close_connection != absl::nullopt) { + close_connection_upon_invalid_header = should_close_connection.value(); + } else { + close_connection_upon_invalid_header = + !http3_options_.override_stream_error_on_invalid_http_message().value(); + } + if (close_connection_upon_invalid_header) { + stream_delegate()->OnStreamError(quic::QUIC_HTTP_FRAME_ERROR, "Invalid headers"); + } else { + Reset(quic::QUIC_BAD_APPLICATION_PAYLOAD); + } +} + } // namespace Quic } // namespace Envoy diff --git a/source/common/quic/envoy_quic_client_stream.h b/source/common/quic/envoy_quic_client_stream.h index 3ce7322ba3455..73b162feabe85 100644 --- a/source/common/quic/envoy_quic_client_stream.h +++ b/source/common/quic/envoy_quic_client_stream.h @@ -23,9 +23,11 @@ class EnvoyQuicClientStream : public quic::QuicSpdyClientStream, public Http::RequestEncoder { public: EnvoyQuicClientStream(quic::QuicStreamId id, quic::QuicSpdyClientSession* client_session, - quic::StreamType type); + quic::StreamType type, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options); EnvoyQuicClientStream(quic::PendingStream* pending, quic::QuicSpdyClientSession* client_session, - quic::StreamType type); + quic::StreamType type, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options); void setResponseDecoder(Http::ResponseDecoder& decoder) { response_decoder_ = &decoder; } @@ -74,6 +76,10 @@ class EnvoyQuicClientStream : public quic::QuicSpdyClientStream, // Deliver awaiting trailers if body has been delivered. void maybeDecodeTrailers(); + // Either reset the stream or close the connection according to + // should_close_connection and configured http3 options. + void onStreamError(absl::optional should_close_connection); + Http::ResponseDecoder* response_decoder_{nullptr}; bool decoded_100_continue_{false}; diff --git a/source/common/quic/envoy_quic_server_session.cc b/source/common/quic/envoy_quic_server_session.cc index ccd90c2a35f2d..d13da33e4110a 100644 --- a/source/common/quic/envoy_quic_server_session.cc +++ b/source/common/quic/envoy_quic_server_session.cc @@ -43,7 +43,15 @@ quic::QuicSpdyStream* EnvoyQuicServerSession::CreateIncomingStream(quic::QuicStr if (!ShouldCreateIncomingStream(id)) { return nullptr; } - auto stream = new EnvoyQuicServerStream(id, this, quic::BIDIRECTIONAL); + if (!codec_stats_.has_value() || !http3_options_.has_value()) { + ENVOY_BUG(false, + fmt::format( + "Quic session {} attempts to create stream {} before HCM filter is initialized.", + this->id(), id)); + return nullptr; + } + auto stream = new EnvoyQuicServerStream(id, this, quic::BIDIRECTIONAL, codec_stats_.value(), + http3_options_.value(), headers_with_underscores_action_); ActivateStream(absl::WrapUnique(stream)); if (aboveHighWatermark()) { stream->runHighWatermarkCallbacks(); diff --git a/source/common/quic/envoy_quic_server_session.h b/source/common/quic/envoy_quic_server_session.h index 682b841d9fab7..e80e42c9e62fc 100644 --- a/source/common/quic/envoy_quic_server_session.h +++ b/source/common/quic/envoy_quic_server_session.h @@ -64,6 +64,12 @@ class EnvoyQuicServerSession : public quic::QuicServerSessionBase, const spdy::SpdyStreamPrecedence& precedence, quic::QuicReferenceCountedPointer ack_listener) override; + void setHeadersWithUnderscoreAction( + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action) { + headers_with_underscores_action_ = headers_with_underscores_action; + } + using quic::QuicSession::PerformActionOnActiveStreams; protected: @@ -91,6 +97,9 @@ class EnvoyQuicServerSession : public quic::QuicServerSessionBase, // These callbacks are owned by network filters and quic session should out live // them. Http::ServerConnectionCallbacks* http_connection_callbacks_{nullptr}; + + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action_; }; } // namespace Quic diff --git a/source/common/quic/envoy_quic_server_stream.cc b/source/common/quic/envoy_quic_server_stream.cc index 16a120280cb3e..1f7a668b0e1dd 100644 --- a/source/common/quic/envoy_quic_server_stream.cc +++ b/source/common/quic/envoy_quic_server_stream.cc @@ -31,26 +31,38 @@ namespace Envoy { namespace Quic { -EnvoyQuicServerStream::EnvoyQuicServerStream(quic::QuicStreamId id, quic::QuicSpdySession* session, - quic::StreamType type) +EnvoyQuicServerStream::EnvoyQuicServerStream( + quic::QuicStreamId id, quic::QuicSpdySession* session, quic::StreamType type, + Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action) : quic::QuicSpdyServerStreamBase(id, session, type), EnvoyQuicStream( // Flow control receive window should be larger than 8k to fully utilize congestion // control window before it reaches the high watermark. static_cast(GetReceiveWindow().value()), *filterManagerConnection(), - [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }) { + [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }, + stats, http3_options), + headers_with_underscores_action_(headers_with_underscores_action) { ASSERT(GetReceiveWindow() > 8 * 1024, "Send buffer limit should be larger than 8KB."); } -EnvoyQuicServerStream::EnvoyQuicServerStream(quic::PendingStream* pending, - quic::QuicSpdySession* session, quic::StreamType type) +EnvoyQuicServerStream::EnvoyQuicServerStream( + quic::PendingStream* pending, quic::QuicSpdySession* session, quic::StreamType type, + Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action) : quic::QuicSpdyServerStreamBase(pending, session, type), EnvoyQuicStream( // This should be larger than 8k to fully utilize congestion control // window. And no larger than the max stream flow control window for // the stream to buffer all the data. static_cast(GetReceiveWindow().value()), *filterManagerConnection(), - [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }) {} + [this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); }, + stats, http3_options), + headers_with_underscores_action_(headers_with_underscores_action) {} void EnvoyQuicServerStream::encode100ContinueHeaders(const Http::ResponseHeaderMap& headers) { ASSERT(headers.Status()->value() == "100"); @@ -135,15 +147,24 @@ void EnvoyQuicServerStream::OnInitialHeadersComplete(bool fin, size_t frame_len, return; } quic::QuicSpdyServerStreamBase::OnInitialHeadersComplete(fin, frame_len, header_list); - ASSERT(headers_decompressed() && !header_list.empty()); + if (!headers_decompressed() || header_list.empty()) { + onStreamError(absl::nullopt); + return; + } + ENVOY_STREAM_LOG(debug, "Received headers: {}.", *this, header_list.DebugString()); if (fin) { end_stream_decoded_ = true; } std::unique_ptr headers = - quicHeadersToEnvoyHeaders(header_list); + quicHeadersToEnvoyHeaders(header_list, *this); + if (headers == nullptr) { + onStreamError(close_connection_upon_invalid_header_); + return; + } if (Http::HeaderUtility::requestHeadersValid(*headers) != absl::nullopt) { - stream_delegate()->OnStreamError(quic::QUIC_HTTP_FRAME_ERROR, "Invalid headers"); + details_ = Http3ResponseCodeDetailValues::invalid_http_header; + onStreamError(absl::nullopt); return; } request_decoder_->decodeHeaders(std::move(headers), @@ -220,6 +241,7 @@ void EnvoyQuicServerStream::OnTrailingHeadersComplete(bool fin, size_t frame_len void EnvoyQuicServerStream::OnHeadersTooLarge() { ENVOY_STREAM_LOG(debug, "Headers too large.", *this); + details_ = Http3ResponseCodeDetailValues::headers_too_large; quic::QuicSpdyServerStreamBase::OnHeadersTooLarge(); } @@ -259,7 +281,9 @@ void EnvoyQuicServerStream::OnConnectionClosed(quic::QuicErrorCode error, // Run reset callback before closing the stream so that the watermark change will not trigger // callbacks. if (!local_end_stream_) { - runResetCallbacks(quicErrorCodeToEnvoyResetReason(error)); + runResetCallbacks(source == quic::ConnectionCloseSource::FROM_SELF + ? quicErrorCodeToEnvoyLocalResetReason(error) + : quicErrorCodeToEnvoyRemoteResetReason(error)); } quic::QuicSpdyServerStreamBase::OnConnectionClosed(error, source); } @@ -294,5 +318,42 @@ QuicFilterManagerConnectionImpl* EnvoyQuicServerStream::filterManagerConnection( return dynamic_cast(session()); } +Http::HeaderUtility::HeaderValidationResult +EnvoyQuicServerStream::validateHeader(const std::string& header_name, + absl::string_view header_value) { + Http::HeaderUtility::HeaderValidationResult result = + EnvoyQuicStream::validateHeader(header_name, header_value); + if (result != Http::HeaderUtility::HeaderValidationResult::ACCEPT) { + return result; + } + // Do request specific checks. + result = Http::HeaderUtility::checkHeaderNameForUnderscores( + header_name, headers_with_underscores_action_, stats_.dropped_headers_with_underscores_, + stats_.requests_rejected_with_underscores_in_headers_); + if (result != Http::HeaderUtility::HeaderValidationResult::ACCEPT) { + details_ = Http3ResponseCodeDetailValues::invalid_underscore; + } + return result; +} + +void EnvoyQuicServerStream::onStreamError(absl::optional should_close_connection) { + if (details_.empty()) { + details_ = Http3ResponseCodeDetailValues::invalid_http_header; + } + + bool close_connection_upon_invalid_header; + if (should_close_connection != absl::nullopt) { + close_connection_upon_invalid_header = should_close_connection.value(); + } else { + close_connection_upon_invalid_header = + !http3_options_.override_stream_error_on_invalid_http_message().value(); + } + if (close_connection_upon_invalid_header) { + stream_delegate()->OnStreamError(quic::QUIC_HTTP_FRAME_ERROR, "Invalid headers"); + } else { + Reset(quic::QUIC_BAD_APPLICATION_PAYLOAD); + } +} + } // namespace Quic } // namespace Envoy diff --git a/source/common/quic/envoy_quic_server_stream.h b/source/common/quic/envoy_quic_server_stream.h index 36844780c74b8..e9059861c5e28 100644 --- a/source/common/quic/envoy_quic_server_stream.h +++ b/source/common/quic/envoy_quic_server_stream.h @@ -23,10 +23,16 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, public Http::ResponseEncoder { public: EnvoyQuicServerStream(quic::QuicStreamId id, quic::QuicSpdySession* session, - quic::StreamType type); + quic::StreamType type, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action); EnvoyQuicServerStream(quic::PendingStream* pending, quic::QuicSpdySession* session, - quic::StreamType type); + quic::StreamType type, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options, + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action); void setRequestDecoder(Http::RequestDecoder& decoder) { request_decoder_ = &decoder; } @@ -39,7 +45,9 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, Http::Http1StreamEncoderOptionsOptRef http1StreamEncoderOptions() override { return absl::nullopt; } - bool streamErrorOnInvalidHttpMessage() const override { return false; } + bool streamErrorOnInvalidHttpMessage() const override { + return http3_options_.override_stream_error_on_invalid_http_message().value(); + } // Http::Stream void resetStream(Http::StreamResetReason reason) override; @@ -58,6 +66,10 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, void clearWatermarkBuffer(); + // EnvoyQuicStream + Http::HeaderUtility::HeaderValidationResult + validateHeader(const std::string& header_name, absl::string_view header_value) override; + protected: // EnvoyQuicStream void switchStreamBlockState(bool should_block) override; @@ -77,7 +89,13 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase, // Deliver awaiting trailers if body has been delivered. void maybeDecodeTrailers(); + // Either reset the stream or close the connection according to + // should_close_connection and configured http3 options. + void onStreamError(absl::optional should_close_connection); + Http::RequestDecoder* request_decoder_{nullptr}; + envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction + headers_with_underscores_action_; }; } // namespace Quic diff --git a/source/common/quic/envoy_quic_stream.h b/source/common/quic/envoy_quic_stream.h index 84f61a9efbcab..53e2623291b07 100644 --- a/source/common/quic/envoy_quic_stream.h +++ b/source/common/quic/envoy_quic_stream.h @@ -1,29 +1,51 @@ #pragma once +#include "envoy/config/core/v3/protocol.pb.h" #include "envoy/event/dispatcher.h" #include "envoy/http/codec.h" #include "common/http/codec_helper.h" #include "common/quic/envoy_quic_simulated_watermark_buffer.h" +#include "common/quic/envoy_quic_utils.h" #include "common/quic/quic_filter_manager_connection_impl.h" #include "common/quic/send_buffer_monitor.h" namespace Envoy { namespace Quic { +// Changes or additions to details should be reflected in +// docs/root/configuration/http/http_conn_man/response_code_details_details.rst +class Http3ResponseCodeDetailValues { +public: + // Invalid HTTP header field was received and stream is going to be + // closed. + static constexpr absl::string_view invalid_http_header = "http3.invalid_header_field"; + // The size of headers (or trailers) exceeded the configured limits. + static constexpr absl::string_view headers_too_large = "http3.headers_too_large"; + // Envoy was configured to drop requests with header keys beginning with underscores. + static constexpr absl::string_view invalid_underscore = "http3.unexpected_underscore"; + // The peer refused the stream. + static constexpr absl::string_view remote_refused = "http3.remote_refuse"; + // The peer reset the stream. + static constexpr absl::string_view remote_reset = "http3.remote_reset"; +}; + // Base class for EnvoyQuicServer|ClientStream. class EnvoyQuicStream : public virtual Http::StreamEncoder, public Http::Stream, public Http::StreamCallbackHelper, public SendBufferMonitor, + public HeaderValidator, protected Logger::Loggable { public: // |buffer_limit| is the high watermark of the stream send buffer, and the low // watermark will be half of it. EnvoyQuicStream(uint32_t buffer_limit, QuicFilterManagerConnectionImpl& filter_manager_connection, std::function below_low_watermark, - std::function above_high_watermark) - : send_buffer_simulation_(buffer_limit / 2, buffer_limit, std::move(below_low_watermark), + std::function above_high_watermark, Http::Http3::CodecStats& stats, + const envoy::config::core::v3::Http3ProtocolOptions& http3_options) + : stats_(stats), http3_options_(http3_options), + send_buffer_simulation_(buffer_limit / 2, buffer_limit, std::move(below_low_watermark), std::move(above_high_watermark), ENVOY_LOGGER()), filter_manager_connection_(filter_manager_connection) {} @@ -102,6 +124,20 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, filter_manager_connection_.updateBytesBuffered(old_buffered_bytes, new_buffered_bytes); } + Http::HeaderUtility::HeaderValidationResult + validateHeader(const std::string& header_name, absl::string_view header_value) override { + bool override_stream_error_on_invalid_http_message = + http3_options_.override_stream_error_on_invalid_http_message().value(); + if (header_name == "content-length") { + return Http::HeaderUtility::validateContentLength( + header_value, override_stream_error_on_invalid_http_message, + close_connection_upon_invalid_header_); + } + return Http::HeaderUtility::HeaderValidationResult::ACCEPT; + } + + absl::string_view responseDetails() override { return details_; } + protected: virtual void switchStreamBlockState(bool should_block) PURE; @@ -118,6 +154,11 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder, // becomes false. bool in_decode_data_callstack_{false}; + Http::Http3::CodecStats& stats_; + const envoy::config::core::v3::Http3ProtocolOptions& http3_options_; + bool close_connection_upon_invalid_header_{false}; + absl::string_view details_; + private: // Keeps track of bytes buffered in the stream send buffer in QUICHE and reacts // upon crossing high and low watermarks. diff --git a/source/common/quic/envoy_quic_utils.cc b/source/common/quic/envoy_quic_utils.cc index cf2d4c83b9a97..3da9c12702ea9 100644 --- a/source/common/quic/envoy_quic_utils.cc +++ b/source/common/quic/envoy_quic_utils.cc @@ -100,11 +100,25 @@ Http::StreamResetReason quicRstErrorToEnvoyRemoteResetReason(quic::QuicRstStream } } -Http::StreamResetReason quicErrorCodeToEnvoyResetReason(quic::QuicErrorCode error) { - if (error == quic::QUIC_NO_ERROR) { +Http::StreamResetReason quicErrorCodeToEnvoyLocalResetReason(quic::QuicErrorCode error) { + switch (error) { + case quic::QUIC_HANDSHAKE_FAILED: + case quic::QUIC_HANDSHAKE_TIMEOUT: + return Http::StreamResetReason::ConnectionFailure; + case quic::QUIC_HTTP_FRAME_ERROR: + return Http::StreamResetReason::ProtocolError; + default: return Http::StreamResetReason::ConnectionTermination; - } else { + } +} + +Http::StreamResetReason quicErrorCodeToEnvoyRemoteResetReason(quic::QuicErrorCode error) { + switch (error) { + case quic::QUIC_HANDSHAKE_FAILED: + case quic::QUIC_HANDSHAKE_TIMEOUT: return Http::StreamResetReason::ConnectionFailure; + default: + return Http::StreamResetReason::ConnectionTermination; } } diff --git a/source/common/quic/envoy_quic_utils.h b/source/common/quic/envoy_quic_utils.h index 209b1b0c21ae3..8898bd1e4971d 100644 --- a/source/common/quic/envoy_quic_utils.h +++ b/source/common/quic/envoy_quic_utils.h @@ -24,6 +24,7 @@ #include "quiche/quic/core/quic_error_codes.h" #include "quiche/quic/platform/api/quic_ip_address.h" #include "quiche/quic/platform/api/quic_socket_address.h" +#include "common/http/header_utility.h" #include "openssl/ssl.h" @@ -37,19 +38,36 @@ quicAddressToEnvoyAddressInstance(const quic::QuicSocketAddress& quic_address); quic::QuicSocketAddress envoyIpAddressToQuicSocketAddress(const Network::Address::Ip* envoy_ip); +class HeaderValidator { +public: + virtual ~HeaderValidator() = default; + virtual Http::HeaderUtility::HeaderValidationResult + validateHeader(const std::string& header_name, absl::string_view header_value) = 0; +}; + // The returned header map has all keys in lower case. template -std::unique_ptr quicHeadersToEnvoyHeaders(const quic::QuicHeaderList& header_list) { +std::unique_ptr quicHeadersToEnvoyHeaders(const quic::QuicHeaderList& header_list, + HeaderValidator& validator) { auto headers = T::create(); for (const auto& entry : header_list) { - auto key = Http::LowerCaseString(entry.first); - if (key != Http::Headers::get().Cookie) { - // TODO(danzh): Avoid copy by referencing entry as header_list is already validated by QUIC. - headers->addCopy(key, entry.second); - } else { - // QUICHE breaks "cookie" header into crumbs. Coalesce them by appending current one to - // existing one if there is any. - headers->appendCopy(key, entry.second); + Http::HeaderUtility::HeaderValidationResult result = + validator.validateHeader(entry.first, entry.second); + switch (result) { + case Http::HeaderUtility::HeaderValidationResult::REJECT: + return nullptr; + case Http::HeaderUtility::HeaderValidationResult::DROP: + continue; + case Http::HeaderUtility::HeaderValidationResult::ACCEPT: + auto key = Http::LowerCaseString(entry.first); + if (key != Http::Headers::get().Cookie) { + // TODO(danzh): Avoid copy by referencing entry as header_list is already validated by QUIC. + headers->addCopy(key, entry.second); + } else { + // QUICHE breaks "cookie" header into crumbs. Coalesce them by appending current one to + // existing one if there is any. + headers->appendCopy(key, entry.second); + } } } return headers; @@ -81,8 +99,11 @@ Http::StreamResetReason quicRstErrorToEnvoyLocalResetReason(quic::QuicRstStreamE // Called when a QUIC stack reset the stream. Http::StreamResetReason quicRstErrorToEnvoyRemoteResetReason(quic::QuicRstStreamErrorCode rst_err); -// Called when underlying QUIC connection is closed either locally or by peer. -Http::StreamResetReason quicErrorCodeToEnvoyResetReason(quic::QuicErrorCode error); +// Called when underlying QUIC connection is closed locally. +Http::StreamResetReason quicErrorCodeToEnvoyLocalResetReason(quic::QuicErrorCode error); + +// Called when underlying QUIC connection is closed by peer. +Http::StreamResetReason quicErrorCodeToEnvoyRemoteResetReason(quic::QuicErrorCode error); // Called when a GOAWAY frame is received. ABSL_MUST_USE_RESULT diff --git a/source/common/quic/quic_filter_manager_connection_impl.h b/source/common/quic/quic_filter_manager_connection_impl.h index 601555288577a..e897042b6de79 100644 --- a/source/common/quic/quic_filter_manager_connection_impl.h +++ b/source/common/quic/quic_filter_manager_connection_impl.h @@ -1,10 +1,13 @@ #pragma once +#include + #include "envoy/event/dispatcher.h" #include "envoy/network/connection.h" #include "common/common/empty_string.h" #include "common/common/logger.h" +#include "common/http/http3/codec_stats.h" #include "common/network/connection_impl_base.h" #include "common/quic/envoy_quic_connection.h" #include "common/quic/envoy_quic_simulated_watermark_buffer.h" @@ -117,6 +120,15 @@ class QuicFilterManagerConnectionImpl : public Network::ConnectionImplBase, uint32_t bytesToSend() { return bytes_to_send_; } + void setHttp3Options(const envoy::config::core::v3::Http3ProtocolOptions& http3_options) { + http3_options_ = + std::reference_wrapper(http3_options); + } + + void setCodecStats(Http::Http3::CodecStats& stats) { + codec_stats_ = std::reference_wrapper(stats); + } + protected: // Propagate connection close to network_connection_callbacks_. void onConnectionCloseEvent(const quic::QuicConnectionCloseFrame& frame, @@ -128,6 +140,10 @@ class QuicFilterManagerConnectionImpl : public Network::ConnectionImplBase, EnvoyQuicConnection* quic_connection_{nullptr}; + absl::optional> codec_stats_; + absl::optional> + http3_options_; + private: friend class Envoy::TestPauseFilterForQuic; diff --git a/source/common/upstream/BUILD b/source/common/upstream/BUILD index 850e6c5eaadf9..b970eacd34199 100644 --- a/source/common/upstream/BUILD +++ b/source/common/upstream/BUILD @@ -556,6 +556,7 @@ envoy_cc_library( "//source/common/config:well_known_names", "//source/common/http/http1:codec_stats_lib", "//source/common/http/http2:codec_stats_lib", + "//source/common/http/http3:codec_stats_lib", "//source/common/init:manager_lib", "//source/common/shared_pool:shared_pool_lib", "//source/common/stats:isolated_store_lib", diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 83917fb6fc394..5d70b2f90fabf 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -1256,6 +1256,10 @@ Http::Http2::CodecStats& ClusterInfoImpl::http2CodecStats() const { return Http::Http2::CodecStats::atomicGet(http2_codec_stats_, *stats_scope_); } +Http::Http3::CodecStats& ClusterInfoImpl::http3CodecStats() const { + return Http::Http3::CodecStats::atomicGet(http3_codec_stats_, *stats_scope_); +} + std::pair, absl::optional> ClusterInfoImpl::getRetryBudgetParams( const envoy::config::cluster::v3::CircuitBreakers::Thresholds& thresholds) { constexpr double default_budget_percent = 20.0; diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index eb38961922c33..ec98ca4146d25 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -45,6 +45,7 @@ #include "common/config/well_known_names.h" #include "common/http/http1/codec_stats.h" #include "common/http/http2/codec_stats.h" +#include "common/http/http3/codec_stats.h" #include "common/init/manager_impl.h" #include "common/network/utility.h" #include "common/shared_pool/shared_pool.h" @@ -654,6 +655,7 @@ class ClusterInfoImpl : public ClusterInfo, Http::Http1::CodecStats& http1CodecStats() const override; Http::Http2::CodecStats& http2CodecStats() const override; + Http::Http3::CodecStats& http3CodecStats() const override; protected: // Gets the retry budget percent/concurrency from the circuit breaker thresholds. If the retry @@ -732,6 +734,7 @@ class ClusterInfoImpl : public ClusterInfo, std::vector filter_factories_; mutable Http::Http1::CodecStats::AtomicPtr http1_codec_stats_; mutable Http::Http2::CodecStats::AtomicPtr http2_codec_stats_; + mutable Http::Http3::CodecStats::AtomicPtr http3_codec_stats_; }; /** diff --git a/source/extensions/filters/network/http_connection_manager/BUILD b/source/extensions/filters/network/http_connection_manager/BUILD index aa55666bb72ef..590a4a10a44a3 100644 --- a/source/extensions/filters/network/http_connection_manager/BUILD +++ b/source/extensions/filters/network/http_connection_manager/BUILD @@ -44,6 +44,7 @@ envoy_cc_extension( "//source/common/http/http1:codec_lib", "//source/common/http/http1:settings_lib", "//source/common/http/http2:codec_lib", + "//source/common/http/http3:codec_stats_lib", "//source/common/json:json_loader_lib", "//source/common/local_reply:local_reply_lib", "//source/common/protobuf:utility_lib", diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index e7ebcb3ff2c6e..03562a77568ea 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -207,6 +207,9 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( route_config_provider_manager_(route_config_provider_manager), scoped_routes_config_provider_manager_(scoped_routes_config_provider_manager), filter_config_provider_manager_(filter_config_provider_manager), + http3_options_(Http3::Utility::initializeAndValidateOptions( + config.http3_protocol_options(), config.has_stream_error_on_invalid_http_message(), + config.stream_error_on_invalid_http_message())), http2_options_(Http2::Utility::initializeAndValidateOptions( config.http2_protocol_options(), config.has_stream_error_on_invalid_http_message(), config.stream_error_on_invalid_http_message())), @@ -586,7 +589,9 @@ HttpConnectionManagerConfig::createCodec(Network::Connection& connection, case CodecType::HTTP3: #ifdef ENVOY_ENABLE_QUIC return std::make_unique( - dynamic_cast(connection), callbacks); + dynamic_cast(connection), callbacks, + Http::Http3::CodecStats::atomicGet(http3_codec_stats_, context_.scope()), http3_options_, + maxRequestHeadersKb(), headersWithUnderscoresAction()); #else // Should be blocked by configuration checking at an earlier point. NOT_REACHED_GCOVR_EXCL_LINE; diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index 639e3f6ac9a58..d2c889a12aa98 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -23,6 +23,7 @@ #include "common/http/date_provider_impl.h" #include "common/http/http1/codec_stats.h" #include "common/http/http2/codec_stats.h" +#include "common/http/http3/codec_stats.h" #include "common/json/json_loader.h" #include "common/local_reply/local_reply.h" #include "common/router/rds_impl.h" @@ -212,6 +213,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, Http::ConnectionManagerStats stats_; mutable Http::Http1::CodecStats::AtomicPtr http1_codec_stats_; mutable Http::Http2::CodecStats::AtomicPtr http2_codec_stats_; + mutable Http::Http3::CodecStats::AtomicPtr http3_codec_stats_; Http::ConnectionManagerTracingStats tracing_stats_; const bool use_remote_address_{}; const std::unique_ptr internal_address_config_; @@ -224,6 +226,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, Config::ConfigProviderManager& scoped_routes_config_provider_manager_; Filter::Http::FilterConfigProviderManager& filter_config_provider_manager_; CodecType codec_type_; + envoy::config::core::v3::Http3ProtocolOptions http3_options_; envoy::config::core::v3::Http2ProtocolOptions http2_options_; const Http::Http1Settings http1_settings_; HttpConnectionManagerProto::ServerHeaderTransformation server_transformation_{ diff --git a/test/common/http/header_utility_test.cc b/test/common/http/header_utility_test.cc index 15d02fc00343b..ca358e2a7a05d 100644 --- a/test/common/http/header_utility_test.cc +++ b/test/common/http/header_utility_test.cc @@ -722,5 +722,52 @@ TEST(RequiredHeaders, IsModifiableHeader) { EXPECT_TRUE(HeaderUtility::isModifiableHeader("Content-Type")); } +TEST(ValidateHeaders, HeaderNameWithUnderscores) { + Stats::MockCounter dropped; + Stats::MockCounter rejected; + EXPECT_CALL(dropped, inc()); + EXPECT_CALL(rejected, inc()).Times(0u); + EXPECT_EQ(HeaderUtility::HeaderValidationResult::DROP, + HeaderUtility::checkHeaderNameForUnderscores( + "header_with_underscore", envoy::config::core::v3::HttpProtocolOptions::DROP_HEADER, + dropped, rejected)); + + EXPECT_CALL(dropped, inc()).Times(0u); + EXPECT_CALL(rejected, inc()); + EXPECT_EQ(HeaderUtility::HeaderValidationResult::REJECT, + HeaderUtility::checkHeaderNameForUnderscores( + "header_with_underscore", + envoy::config::core::v3::HttpProtocolOptions::REJECT_REQUEST, dropped, rejected)); + + EXPECT_EQ(HeaderUtility::HeaderValidationResult::ACCEPT, + HeaderUtility::checkHeaderNameForUnderscores( + "header_with_underscore", envoy::config::core::v3::HttpProtocolOptions::ALLOW, + dropped, rejected)); + + EXPECT_EQ(HeaderUtility::HeaderValidationResult::ACCEPT, + HeaderUtility::checkHeaderNameForUnderscores( + "header", envoy::config::core::v3::HttpProtocolOptions::REJECT_REQUEST, dropped, + rejected)); +} + +TEST(ValidateHeaders, ContentLength) { + bool should_close_connection; + EXPECT_EQ(HeaderUtility::HeaderValidationResult::ACCEPT, + HeaderUtility::validateContentLength("1,1", true, should_close_connection)); + EXPECT_FALSE(should_close_connection); + + EXPECT_EQ(HeaderUtility::HeaderValidationResult::REJECT, + HeaderUtility::validateContentLength("1,2", true, should_close_connection)); + EXPECT_FALSE(should_close_connection); + + EXPECT_EQ(HeaderUtility::HeaderValidationResult::REJECT, + HeaderUtility::validateContentLength("1,2", false, should_close_connection)); + EXPECT_TRUE(should_close_connection); + + EXPECT_EQ(HeaderUtility::HeaderValidationResult::REJECT, + HeaderUtility::validateContentLength("-1", false, should_close_connection)); + EXPECT_TRUE(should_close_connection); +} + } // namespace Http } // namespace Envoy diff --git a/test/common/quic/envoy_quic_client_session_test.cc b/test/common/quic/envoy_quic_client_session_test.cc index 5f01d110d6dda..8617d8953e83a 100644 --- a/test/common/quic/envoy_quic_client_session_test.cc +++ b/test/common/quic/envoy_quic_client_session_test.cc @@ -115,7 +115,10 @@ class EnvoyQuicClientSessionTest : public testing::TestWithParam { quic::QuicServerId("example.com", 443, false), &crypto_config_, nullptr, *dispatcher_, /*send_buffer_limit*/ 1024 * 1024), - http_connection_(envoy_quic_session_, http_connection_callbacks_) { + stats_({ALL_HTTP3_CODEC_STATS(POOL_COUNTER_PREFIX(scope_, "http3."), + POOL_GAUGE_PREFIX(scope_, "http3."))}), + http_connection_(envoy_quic_session_, http_connection_callbacks_, stats_, http3_options_, + 64 * 1024) { EXPECT_EQ(time_system_.systemTime(), envoy_quic_session_.streamInfo().startTime()); EXPECT_EQ(EMPTY_STRING, envoy_quic_session_.nextProtocol()); EXPECT_EQ(Http::Protocol::Http3, http_connection_.protocol()); @@ -178,6 +181,9 @@ class EnvoyQuicClientSessionTest : public testing::TestWithParam { testing::StrictMock read_current_; testing::StrictMock write_total_; testing::StrictMock write_current_; + Stats::IsolatedStoreImpl scope_; + Http::Http3::CodecStats stats_; + envoy::config::core::v3::Http3ProtocolOptions http3_options_; QuicHttpClientConnectionImpl http_connection_; }; diff --git a/test/common/quic/envoy_quic_client_stream_test.cc b/test/common/quic/envoy_quic_client_stream_test.cc index 62acbef678b68..a07b58dd36b30 100644 --- a/test/common/quic/envoy_quic_client_stream_test.cc +++ b/test/common/quic/envoy_quic_client_stream_test.cc @@ -39,7 +39,10 @@ class EnvoyQuicClientStreamTest : public testing::TestWithParam { quic_session_(quic_config_, {quic_version_}, quic_connection_, *dispatcher_, quic_config_.GetInitialStreamFlowControlWindowToSend() * 2), stream_id_(quic::VersionUsesHttp3(quic_version_.transport_version) ? 4u : 5u), - quic_stream_(new EnvoyQuicClientStream(stream_id_, &quic_session_, quic::BIDIRECTIONAL)), + stats_({ALL_HTTP3_CODEC_STATS(POOL_COUNTER_PREFIX(scope_, "http3."), + POOL_GAUGE_PREFIX(scope_, "http3."))}), + quic_stream_(new EnvoyQuicClientStream(stream_id_, &quic_session_, quic::BIDIRECTIONAL, + stats_, http3_options_)), request_headers_{{":authority", host_}, {":method", "POST"}, {":path", "/"}}, request_trailers_{{"trailer-key", "trailer-value"}} { quic_stream_->setResponseDecoder(stream_decoder_); @@ -136,6 +139,9 @@ class EnvoyQuicClientStreamTest : public testing::TestWithParam { EnvoyQuicClientConnection* quic_connection_; MockEnvoyQuicClientSession quic_session_; quic::QuicStreamId stream_id_; + Stats::IsolatedStoreImpl scope_; + Http::Http3::CodecStats stats_; + envoy::config::core::v3::Http3ProtocolOptions http3_options_; EnvoyQuicClientStream* quic_stream_; Http::MockResponseDecoder stream_decoder_; Http::MockStreamCallbacks stream_callbacks_; diff --git a/test/common/quic/envoy_quic_server_session_test.cc b/test/common/quic/envoy_quic_server_session_test.cc index 2972504184729..c2c93111a1ddf 100644 --- a/test/common/quic/envoy_quic_server_session_test.cc +++ b/test/common/quic/envoy_quic_server_session_test.cc @@ -1,3 +1,5 @@ +#include + #if defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" @@ -162,7 +164,9 @@ class EnvoyQuicServerSessionTest : public testing::TestWithParam { &compressed_certs_cache_, *dispatcher_, /*send_buffer_limit*/ quic::kDefaultFlowControlSendWindow * 1.5, listener_config_), - read_filter_(new Network::MockReadFilter()) { + stats_({ALL_HTTP3_CODEC_STATS( + POOL_COUNTER_PREFIX(listener_config_.listenerScope(), "http3."), + POOL_GAUGE_PREFIX(listener_config_.listenerScope(), "http3."))}) { EXPECT_EQ(time_system_.systemTime(), envoy_quic_session_.streamInfo().startTime()); EXPECT_EQ(EMPTY_STRING, envoy_quic_session_.nextProtocol()); @@ -210,6 +214,7 @@ class EnvoyQuicServerSessionTest : public testing::TestWithParam { bool installReadFilter() { // Setup read filter. + read_filter_ = std::make_shared(), envoy_quic_session_.addReadFilter(read_filter_); EXPECT_EQ(Http::Protocol::Http3, read_filter_->callbacks_->connection().streamInfo().protocol().value()); @@ -221,8 +226,9 @@ class EnvoyQuicServerSessionTest : public testing::TestWithParam { EXPECT_EQ(&read_total_, &quic_connection_->connectionStats().read_total_); EXPECT_CALL(*read_filter_, onNewConnection()).WillOnce(Invoke([this]() { // Create ServerConnection instance and setup callbacks for it. - http_connection_ = std::make_unique(envoy_quic_session_, - http_connection_callbacks_); + http_connection_ = std::make_unique( + envoy_quic_session_, http_connection_callbacks_, stats_, http3_options_, 64 * 1024, + envoy::config::core::v3::HttpProtocolOptions::ALLOW); EXPECT_EQ(Http::Protocol::Http3, http_connection_->protocol()); // Stop iteration to avoid calling getRead/WriteBuffer(). return Network::FilterStatus::StopIteration; @@ -279,11 +285,26 @@ class EnvoyQuicServerSessionTest : public testing::TestWithParam { testing::StrictMock write_total_; testing::StrictMock write_current_; Http::ServerConnectionPtr http_connection_; + Http::Http3::CodecStats stats_; + envoy::config::core::v3::Http3ProtocolOptions http3_options_; }; INSTANTIATE_TEST_SUITE_P(EnvoyQuicServerSessionTests, EnvoyQuicServerSessionTest, testing::ValuesIn({true, false})); +TEST_P(EnvoyQuicServerSessionTest, NewStreamBeforeInitializingFilter) { + quic::QuicStreamId stream_id = + quic::VersionUsesHttp3(quic_version_[0].transport_version) ? 4u : 5u; + EXPECT_ENVOY_BUG(envoy_quic_session_.GetOrCreateStream(stream_id), + fmt::format("attempts to create stream", envoy_quic_session_.id(), stream_id)); + EXPECT_CALL(*quic_connection_, + SendConnectionClosePacket(quic::QUIC_NO_ERROR, _, "Closed by application")); + EXPECT_CALL(*quic_connection_, SendControlFrame(_)) + .Times(testing::AtMost(1)) + .WillOnce(Invoke([](const quic::QuicFrame&) { return false; })); + envoy_quic_session_.close(Network::ConnectionCloseType::NoFlush); +} + TEST_P(EnvoyQuicServerSessionTest, NewStream) { installReadFilter(); @@ -780,6 +801,7 @@ TEST_P(EnvoyQuicServerSessionTest, GoAway) { } TEST_P(EnvoyQuicServerSessionTest, InitializeFilterChain) { + read_filter_ = std::make_shared(); Network::MockFilterChain filter_chain; crypto_stream_->setProofSourceDetails( std::make_unique(filter_chain)); diff --git a/test/common/quic/envoy_quic_server_stream_test.cc b/test/common/quic/envoy_quic_server_stream_test.cc index fc7bcfbf85214..25f570e74572e 100644 --- a/test/common/quic/envoy_quic_server_stream_test.cc +++ b/test/common/quic/envoy_quic_server_stream_test.cc @@ -57,7 +57,12 @@ class EnvoyQuicServerStreamTest : public testing::TestWithParam { quic_session_(quic_config_, {quic_version_}, &quic_connection_, *dispatcher_, quic_config_.GetInitialStreamFlowControlWindowToSend() * 2), stream_id_(VersionUsesHttp3(quic_version_.transport_version) ? 4u : 5u), - quic_stream_(new EnvoyQuicServerStream(stream_id_, &quic_session_, quic::BIDIRECTIONAL)), + stats_( + {ALL_HTTP3_CODEC_STATS(POOL_COUNTER_PREFIX(listener_config_.listenerScope(), "http3."), + POOL_GAUGE_PREFIX(listener_config_.listenerScope(), "http3."))}), + quic_stream_(new EnvoyQuicServerStream( + stream_id_, &quic_session_, quic::BIDIRECTIONAL, stats_, http3_options_, + envoy::config::core::v3::HttpProtocolOptions::ALLOW)), response_headers_{{":status", "200"}, {"response-key", "response-value"}}, response_trailers_{{"trailer-key", "trailer-value"}} { quic_stream_->setRequestDecoder(stream_decoder_); @@ -163,6 +168,8 @@ class EnvoyQuicServerStreamTest : public testing::TestWithParam { EnvoyQuicServerConnection quic_connection_; MockEnvoyQuicSession quic_session_; quic::QuicStreamId stream_id_; + Http::Http3::CodecStats stats_; + envoy::config::core::v3::Http3ProtocolOptions http3_options_; EnvoyQuicServerStream* quic_stream_; Http::MockRequestDecoder stream_decoder_; Http::MockStreamCallbacks stream_callbacks_; diff --git a/test/common/quic/envoy_quic_utils_test.cc b/test/common/quic/envoy_quic_utils_test.cc index c2252592e4083..2ba153ba9a507 100644 --- a/test/common/quic/envoy_quic_utils_test.cc +++ b/test/common/quic/envoy_quic_utils_test.cc @@ -45,13 +45,20 @@ TEST(EnvoyQuicUtilsTest, ConversionBetweenQuicAddressAndEnvoyAddress) { } } +class MockHeaderValidator : public HeaderValidator { +public: + ~MockHeaderValidator() override = default; + MOCK_METHOD(Http::HeaderUtility::HeaderValidationResult, validateHeader, + (const std::string& header_name, absl::string_view header_value)); +}; + TEST(EnvoyQuicUtilsTest, HeadersConversion) { spdy::SpdyHeaderBlock headers_block; headers_block[":authority"] = "www.google.com"; headers_block[":path"] = "/index.hml"; headers_block[":scheme"] = "https"; // "value1" and "value2" should be coalesced into one header by QUICHE and split again while - // converting to Envoy headers.. + // converting to Envoy headers. headers_block.AppendValueOrAddHeader("key", "value1"); headers_block.AppendValueOrAddHeader("key", "value2"); auto envoy_headers = spdyHeaderBlockToEnvoyHeaders(headers_block); @@ -70,9 +77,36 @@ TEST(EnvoyQuicUtilsTest, HeadersConversion) { quic_headers.OnHeader(":scheme", "https"); quic_headers.OnHeader("key", "value1"); quic_headers.OnHeader("key", "value2"); + quic_headers.OnHeader("key-to-drop", ""); quic_headers.OnHeaderBlockEnd(0, 0); - auto envoy_headers2 = quicHeadersToEnvoyHeaders(quic_headers); + MockHeaderValidator validator; + EXPECT_CALL(validator, validateHeader(_, _)) + .WillRepeatedly([](const std::string& header_name, absl::string_view) { + if (header_name == "key-to-drop") { + return Http::HeaderUtility::HeaderValidationResult::DROP; + } + return Http::HeaderUtility::HeaderValidationResult::ACCEPT; + }); + auto envoy_headers2 = + quicHeadersToEnvoyHeaders(quic_headers, validator); EXPECT_EQ(*envoy_headers, *envoy_headers2); + + quic::QuicHeaderList quic_headers2; + quic_headers2.OnHeaderBlockStart(); + quic_headers2.OnHeader(":authority", "www.google.com"); + quic_headers2.OnHeader(":path", "/index.hml"); + quic_headers2.OnHeader(":scheme", "https"); + quic_headers2.OnHeader("invalid_key", ""); + quic_headers2.OnHeaderBlockEnd(0, 0); + EXPECT_CALL(validator, validateHeader(_, _)) + .WillRepeatedly([](const std::string& header_name, absl::string_view) { + if (header_name == "invalid_key") { + return Http::HeaderUtility::HeaderValidationResult::REJECT; + } + return Http::HeaderUtility::HeaderValidationResult::ACCEPT; + }); + EXPECT_EQ(nullptr, + quicHeadersToEnvoyHeaders(quic_headers2, validator)); } } // namespace Quic diff --git a/test/integration/BUILD b/test/integration/BUILD index 447e24e93fcb0..2b9b5b5dec4e5 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -656,6 +656,7 @@ envoy_cc_test_library( "//source/common/common:callback_impl_lib", "//source/common/common:linked_object", "//source/common/common:lock_guard_lib", + "//source/common/http/http3:codec_stats_lib", "//source/common/common:thread_lib", "//source/common/config:utility_lib", "//source/common/grpc:codec_lib", diff --git a/test/integration/fake_upstream.cc b/test/integration/fake_upstream.cc index 941b7b1b59e3f..7c47e739c3c07 100644 --- a/test/integration/fake_upstream.cc +++ b/test/integration/fake_upstream.cc @@ -343,8 +343,10 @@ FakeHttpConnection::FakeHttpConnection( } else { ASSERT(type == Type::HTTP3); #ifdef ENVOY_ENABLE_QUIC + Http::Http3::CodecStats& stats = fake_upstream.http3CodecStats(); codec_ = std::make_unique( - dynamic_cast(shared_connection_.connection()), *this); + dynamic_cast(shared_connection_.connection()), *this, stats, + fake_upstream.http3Options(), max_request_headers_kb, headers_with_underscores_action); #else ASSERT(false, "running a QUIC integration test without compiling QUIC"); #endif @@ -518,6 +520,7 @@ FakeUpstream::FakeUpstream(Network::TransportSocketFactoryPtr&& transport_socket FakeUpstream::FakeUpstream(Network::TransportSocketFactoryPtr&& transport_socket_factory, Network::SocketPtr&& listen_socket, const FakeUpstreamConfig& config) : http_type_(config.upstream_protocol_), http2_options_(config.http2_options_), + http3_options_(config.http3_options_), socket_(Network::SocketSharedPtr(listen_socket.release())), socket_factory_(std::make_shared(socket_)), api_(Api::createApiForTest(stats_store_)), time_system_(config.time_system_), diff --git a/test/integration/fake_upstream.h b/test/integration/fake_upstream.h index 04776906694e5..ce92bcb06f5ee 100644 --- a/test/integration/fake_upstream.h +++ b/test/integration/fake_upstream.h @@ -27,6 +27,7 @@ #include "common/grpc/common.h" #include "common/http/http1/codec_impl.h" #include "common/http/http2/codec_impl.h" +#include "common/http/http3/codec_stats.h" #include "common/network/connection_balancer_impl.h" #include "common/network/filter_impl.h" #include "common/network/listen_socket_impl.h" @@ -551,6 +552,7 @@ struct FakeUpstreamConfig { bool enable_half_close_{}; absl::optional udp_fake_upstream_; envoy::config::core::v3::Http2ProtocolOptions http2_options_; + envoy::config::core::v3::Http3ProtocolOptions http3_options_; uint32_t max_request_headers_kb_ = Http::DEFAULT_MAX_REQUEST_HEADERS_KB; uint32_t max_request_headers_count_ = Http::DEFAULT_MAX_HEADERS_COUNT; envoy::config::core::v3::HttpProtocolOptions::HeadersWithUnderscoresAction @@ -651,6 +653,10 @@ class FakeUpstream : Logger::Loggable, return Http::Http2::CodecStats::atomicGet(http2_codec_stats_, stats_store_); } + Http::Http3::CodecStats& http3CodecStats() { + return Http::Http3::CodecStats::atomicGet(http3_codec_stats_, stats_store_); + } + // Write into the outbound buffer of the network connection at the specified index. // Note: that this write bypasses any processing by the upstream codec. ABSL_MUST_USE_RESULT @@ -659,6 +665,7 @@ class FakeUpstream : Logger::Loggable, std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); const envoy::config::core::v3::Http2ProtocolOptions& http2Options() { return http2_options_; } + const envoy::config::core::v3::Http3ProtocolOptions& http3Options() { return http3_options_; } protected: Stats::IsolatedStoreImpl stats_store_; @@ -785,6 +792,7 @@ class FakeUpstream : Logger::Loggable, std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); const envoy::config::core::v3::Http2ProtocolOptions http2_options_; + const envoy::config::core::v3::Http3ProtocolOptions http3_options_; Network::SocketSharedPtr socket_; Network::ListenSocketFactorySharedPtr socket_factory_; ConditionalInitializer server_initialized_; @@ -811,6 +819,7 @@ class FakeUpstream : Logger::Loggable, std::list received_datagrams_ ABSL_GUARDED_BY(lock_); Http::Http1::CodecStats::AtomicPtr http1_codec_stats_; Http::Http2::CodecStats::AtomicPtr http2_codec_stats_; + Http::Http3::CodecStats::AtomicPtr http3_codec_stats_; }; using FakeUpstreamPtr = std::unique_ptr; diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index ef0759c806ae0..b5706c7edc6e7 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -247,13 +247,20 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection( absl::optional http2_options) { std::shared_ptr cluster{new NiceMock()}; cluster->max_response_headers_count_ = 200; + envoy::config::core::v3::Http3ProtocolOptions http3_options; if (!http2_options.has_value()) { http2_options = Http2::Utility::initializeAndValidateOptions( envoy::config::core::v3::Http2ProtocolOptions()); http2_options.value().set_allow_connect(true); http2_options.value().set_allow_metadata(true); + } else if (http2_options.value().has_override_stream_error_on_invalid_http_message()) { + http3_options.mutable_override_stream_error_on_invalid_http_message()->set_value( + http2_options.value().override_stream_error_on_invalid_http_message().value()); + } else if (http2_options.value().stream_error_on_invalid_http_messaging()) { + http3_options.mutable_override_stream_error_on_invalid_http_message()->set_value(true); } cluster->http2_options_ = http2_options.value(); + cluster->http3_options_ = http3_options; cluster->http1_settings_.enable_trailers_ = true; Upstream::HostDescriptionConstSharedPtr host_description{Upstream::makeTestHostDescription( cluster, fmt::format("tcp://{}:80", Network::Test::getLoopbackAddressUrlString(version_)), diff --git a/test/integration/protocol_integration_test.cc b/test/integration/protocol_integration_test.cc index 074614b73e347..5c17d664443e1 100644 --- a/test/integration/protocol_integration_test.cc +++ b/test/integration/protocol_integration_test.cc @@ -379,15 +379,14 @@ TEST_P(DownstreamProtocolIntegrationTest, DownstreamRequestWithFaultyFilter) { } TEST_P(DownstreamProtocolIntegrationTest, FaultyFilterWithConnect) { + // TODO(danzh) re-enable after adding http3 option "allow_connect". + EXCLUDE_DOWNSTREAM_HTTP3; if (upstreamProtocol() == FakeHttpConnection::Type::HTTP3) { // For QUIC, even through the headers are not sent upstream, the stream will // be created. Use the autonomous upstream and allow incomplete streams. autonomous_allow_incomplete_streams_ = true; autonomous_upstream_ = true; } - // TODO(danzh) re-enable after plumbing through http2 option - // "allow_connect". - EXCLUDE_DOWNSTREAM_HTTP3; // Faulty filter that removed host in a CONNECT request. config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& @@ -1109,8 +1108,6 @@ TEST_P(ProtocolIntegrationTest, MaxStreamDurationWithRetryPolicyWhenRetryUpstrea // Verify that headers with underscores in their names are dropped from client requests // but remain in upstream responses. TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresDropped) { - // TODO(danzh) treat underscore in headers according to the config. - EXCLUDE_DOWNSTREAM_HTTP3 config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { @@ -1144,14 +1141,12 @@ TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresDropped) { stat_name = "http2.dropped_headers_with_underscores"; break; case Http::CodecClient::Type::HTTP3: - // TODO(danzh) add stats for H3. + stat_name = "http3.dropped_headers_with_underscores"; break; default: RELEASE_ASSERT(false, fmt::format("Unknown downstream protocol {}", downstream_protocol_)); }; - if (downstream_protocol_ != Http::CodecClient::Type::HTTP3) { - EXPECT_EQ(1L, TestUtility::findCounter(stats, stat_name)->value()); - } + EXPECT_EQ(1L, TestUtility::findCounter(stats, stat_name)->value()); } // Verify that by default headers with underscores in their names remain in both requests and @@ -1178,8 +1173,6 @@ TEST_P(ProtocolIntegrationTest, HeadersWithUnderscoresRemainByDefault) { // Verify that request with headers containing underscores is rejected when configured. TEST_P(DownstreamProtocolIntegrationTest, HeadersWithUnderscoresCauseRequestRejectedByDefault) { - // TODO(danzh) pass headers_with_underscores_action config into QUIC stream. - EXCLUDE_DOWNSTREAM_HTTP3 useAccessLog("%RESPONSE_FLAGS% %RESPONSE_CODE_DETAILS%"); config_helper_.addConfigModifier( [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& @@ -1290,12 +1283,7 @@ TEST_P(ProtocolIntegrationTest, OverflowingResponseCode) { default_response_headers_.setStatus( "11111111111111111111111111111111111111111111111111111111111111111"); upstream_request_->encodeHeaders(default_response_headers_, false); - if (upstreamProtocol() == FakeHttpConnection::Type::HTTP2) { - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); - } else { - // TODO(#14829) QUIC won't disconnect on invalid messaging. - ASSERT_TRUE(upstream_request_->waitForReset()); - } + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); } response->waitForEndStream(); @@ -1331,11 +1319,11 @@ TEST_P(ProtocolIntegrationTest, MissingStatus) { ASSERT_TRUE(fake_upstream_connection->write(std::string(missing_status))); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); } else { + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); waitForNextUpstreamRequest(); default_response_headers_.removeStatus(); upstream_request_->encodeHeaders(default_response_headers_, false); - // TODO(#14829) QUIC won't disconnect on invalid messaging. - ASSERT_TRUE(upstream_request_->waitForReset()); + ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); } response->waitForEndStream(); @@ -1400,8 +1388,6 @@ TEST_P(DownstreamProtocolIntegrationTest, LargeCookieParsingMany) { } TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLength) { - // TODO(danzh) Add content length validation. - EXCLUDE_DOWNSTREAM_HTTP3 initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); @@ -1426,11 +1412,12 @@ TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLength) { } TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLengthAllowed) { - // TODO(danzh) Add override_stream_error_on_invalid_http_message to http3 protocol options. - EXCLUDE_DOWNSTREAM_HTTP3 config_helper_.addConfigModifier( [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { + hcm.mutable_http3_protocol_options() + ->mutable_override_stream_error_on_invalid_http_message() + ->set_value(true); hcm.mutable_http2_protocol_options() ->mutable_override_stream_error_on_invalid_http_message() ->set_value(true); @@ -1467,8 +1454,6 @@ TEST_P(DownstreamProtocolIntegrationTest, InvalidContentLengthAllowed) { } TEST_P(DownstreamProtocolIntegrationTest, MultipleContentLengths) { - // TODO(danzh) Add content length validation. - EXCLUDE_DOWNSTREAM_HTTP3 initialize(); codec_client_ = makeHttpConnection(lookupPort("http")); auto encoder_decoder = @@ -1491,11 +1476,12 @@ TEST_P(DownstreamProtocolIntegrationTest, MultipleContentLengths) { // TODO(PiotrSikora): move this HTTP/2 only variant to http2_integration_test.cc. TEST_P(DownstreamProtocolIntegrationTest, MultipleContentLengthsAllowed) { - // override_stream_error_on_invalid_http_message not supported yet. - EXCLUDE_DOWNSTREAM_HTTP3 config_helper_.addConfigModifier( [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { + hcm.mutable_http3_protocol_options() + ->mutable_override_stream_error_on_invalid_http_message() + ->set_value(true); hcm.mutable_http2_protocol_options() ->mutable_override_stream_error_on_invalid_http_message() ->set_value(true); @@ -2283,12 +2269,17 @@ TEST_P(DownstreamProtocolIntegrationTest, ConnectIsBlocked) { // Make sure that with override_stream_error_on_invalid_http_message true, CONNECT // results in stream teardown not connection teardown. TEST_P(DownstreamProtocolIntegrationTest, ConnectStreamRejection) { + // TODO(danzh) add "allow_connect" to http3 options. + EXCLUDE_DOWNSTREAM_HTTP3; if (downstreamProtocol() == Http::CodecClient::Type::HTTP1) { return; } config_helper_.addConfigModifier( [](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { + hcm.mutable_http3_protocol_options() + ->mutable_override_stream_error_on_invalid_http_message() + ->set_value(true); hcm.mutable_http2_protocol_options() ->mutable_override_stream_error_on_invalid_http_message() ->set_value(true); @@ -2300,8 +2291,6 @@ TEST_P(DownstreamProtocolIntegrationTest, ConnectStreamRejection) { {":method", "CONNECT"}, {":path", "/"}, {":authority", "host"}}); response->waitForReset(); - // TODO(danzh) plumb through stream_error_on_invalid_http_message. - EXCLUDE_DOWNSTREAM_HTTP3; EXPECT_FALSE(codec_client_->disconnected()); } diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index 063dfc76f6cdf..cd0cd7572ec5b 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -131,5 +131,9 @@ Http::Http2::CodecStats& MockClusterInfo::http2CodecStats() const { return Http::Http2::CodecStats::atomicGet(http2_codec_stats_, statsScope()); } +Http::Http3::CodecStats& MockClusterInfo::http3CodecStats() const { + return Http::Http3::CodecStats::atomicGet(http3_codec_stats_, statsScope()); +} + } // namespace Upstream } // namespace Envoy diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index 31a941bb53ffa..640d5bf4f6044 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -16,6 +16,7 @@ #include "common/common/thread.h" #include "common/http/http1/codec_stats.h" #include "common/http/http2/codec_stats.h" +#include "common/http/http3/codec_stats.h" #include "common/upstream/upstream_impl.h" #include "test/mocks/runtime/mocks.h" @@ -149,6 +150,7 @@ class MockClusterInfo : public ClusterInfo { Http::Http1::CodecStats& http1CodecStats() const override; Http::Http2::CodecStats& http2CodecStats() const override; + Http::Http3::CodecStats& http3CodecStats() const override; std::string name_{"fake_cluster"}; std::string observability_name_{"observability_name"}; @@ -196,6 +198,7 @@ class MockClusterInfo : public ClusterInfo { absl::optional max_stream_duration_; mutable Http::Http1::CodecStats::AtomicPtr http1_codec_stats_; mutable Http::Http2::CodecStats::AtomicPtr http2_codec_stats_; + mutable Http::Http3::CodecStats::AtomicPtr http3_codec_stats_; }; class MockIdleTimeEnabledClusterInfo : public MockClusterInfo {