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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions api/envoy/config/core/v3/protocol.proto
Original file line number Diff line number Diff line change
Expand Up @@ -486,10 +486,10 @@ message Http2ProtocolOptions {
// Allows proxying Websocket and other upgrades over H2 connect.
bool allow_connect = 5;

// [#not-implemented-hide:] Hiding until envoy has full metadata support.
// [#not-implemented-hide:] Hiding until Envoy has full metadata support.
// Still under implementation. DO NOT USE.
//
// Allows metadata. See [metadata
// Allows sending and receiving HTTP/2 METADATA frames. See [metadata
// docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more
// information.
bool allow_metadata = 6;
Expand Down Expand Up @@ -618,7 +618,7 @@ message GrpcProtocolOptions {
}

// A message which allows using HTTP/3.
// [#next-free-field: 6]
// [#next-free-field: 7]
message Http3ProtocolOptions {
QuicProtocolOptions quic_protocol_options = 1;

Expand All @@ -637,6 +637,14 @@ message Http3ProtocolOptions {
// <https://datatracker.ietf.org/doc/draft-ietf-httpbis-h3-websockets/>`_
// Note that HTTP/3 CONNECT is not yet an RFC.
bool allow_extended_connect = 5 [(xds.annotations.v3.field_status).work_in_progress = true];

// [#not-implemented-hide:] Hiding until Envoy has full metadata support.
// Still under implementation. DO NOT USE.
//
// Allows sending and receiving HTTP/3 METADATA frames. See [metadata
// docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more
// information.
bool allow_metadata = 6;
}

// A message to control transformations to the :scheme header
Expand Down
3 changes: 3 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ new_features:
change: |
Added :ref:`host_rewrite <envoy_v3_api_field_extensions.filters.http.aws_lambda.v3.Config.host_rewrite>` config to be used
during signature.
- area: http3
change: |
Added experimental support for sending and receiving HTTP/3 METADATA frames.
- area: ext_proc
change: |
added
Expand Down
14 changes: 13 additions & 1 deletion source/common/quic/envoy_quic_client_stream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ EnvoyQuicClientStream::EnvoyQuicClientStream(
const envoy::config::core::v3::Http3ProtocolOptions& http3_options)
: quic::QuicSpdyClientStream(id, client_session, type),
EnvoyQuicStream(
*this,
*this, *client_session,
// 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<uint32_t>(GetReceiveWindow().value()), *filterManagerConnection(),
[this]() { runLowWatermarkCallbacks(); }, [this]() { runHighWatermarkCallbacks(); },
stats, http3_options) {
ASSERT(static_cast<uint32_t>(GetReceiveWindow().value()) > 8 * 1024,
"Send buffer limit should be larger than 8KB.");
RegisterMetadataVisitor(this);
}

Http::Status EnvoyQuicClientStream::encodeHeaders(const Http::RequestHeaderMap& headers,
Expand Down Expand Up @@ -411,6 +412,17 @@ QuicFilterManagerConnectionImpl* EnvoyQuicClientStream::filterManagerConnection(
return dynamic_cast<QuicFilterManagerConnectionImpl*>(session());
}

void EnvoyQuicClientStream::OnMetadataComplete(size_t /*frame_len*/,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in H3, can metadata arrive before headers? I think it's an invariant of Envoy that headers arrive first and I'm not sure if it's an invariant of the quiche libraries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

METADATA is an HTTP/3 frame which is delivered on the QUIC stream, so it's guaranteed to be delivered in the order it was sent. I think this is true for HTTP/2 as well. But if a peer sent a METADATA frame before sending HEADERS I think QUICHE will expose it in that order. I think that's also the same for HTTP/2. But we could make this an error of some here, if you prefer?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should either make it an error, or add an e2e test it doesn't cause problems in Envoy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh! I think we have a test for this already! ProxyMetadataInResponse does:

  // Sends metadata before response header.                                                                                                                                                 
  const std::string key = "key";
  std::string value = std::string(80 * 1024, '1');
  Http::MetadataMap metadata_map = {{key, value}};
  Http::MetadataMapPtr metadata_map_ptr = std::make_unique<Http::MetadataMap>(metadata_map);
  Http::MetadataMapVector metadata_map_vector;
  metadata_map_vector.push_back(std::move(metadata_map_ptr));
  upstream_request_->encodeMetadata(metadata_map_vector);
  upstream_request_->encodeHeaders(default_response_headers_, false);
  upstream_request_->encodeData(12, true);

const quic::QuicHeaderList& header_list) {
if (mustRejectMetadata(header_list.uncompressed_header_bytes())) {
onStreamError(true, quic::QUIC_HEADERS_TOO_LARGE);
return;
}
if (!header_list.empty()) {
response_decoder_->decodeMetadata(metadataMapFromHeaderList(header_list));
}
}

void EnvoyQuicClientStream::onStreamError(absl::optional<bool> should_close_connection,
quic::QuicRstStreamErrorCode rst_code) {
if (details_.empty()) {
Expand Down
8 changes: 7 additions & 1 deletion source/common/quic/envoy_quic_client_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
#endif
#include "quiche/common/simple_buffer_allocator.h"
#include "quiche/quic/core/http/quic_spdy_client_stream.h"
#include "quiche/quic/core/qpack/qpack_encoder.h"
#include "quiche/quic/core/qpack/qpack_instruction_encoder.h"

namespace Envoy {
namespace Quic {

// This class is a quic stream and also a request encoder.
class EnvoyQuicClientStream : public quic::QuicSpdyClientStream,
public EnvoyQuicStream,
public Http::RequestEncoder {
public Http::RequestEncoder,
public quic::QuicSpdyStream::MetadataVisitor {
public:
EnvoyQuicClientStream(quic::QuicStreamId id, quic::QuicSpdyClientSession* client_session,
quic::StreamType type, Http::Http3::CodecStats& stats,
Expand Down Expand Up @@ -52,6 +55,9 @@ class EnvoyQuicClientStream : public quic::QuicSpdyClientStream,

void clearWatermarkBuffer();

// quic::QuicSpdyStream::MetadataVisitor
void OnMetadataComplete(size_t frame_len, const quic::QuicHeaderList& header_list) override;

protected:
// EnvoyQuicStream
void switchStreamBlockState() override;
Expand Down
14 changes: 13 additions & 1 deletion source/common/quic/envoy_quic_server_stream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ EnvoyQuicServerStream::EnvoyQuicServerStream(
headers_with_underscores_action)
: quic::QuicSpdyServerStreamBase(id, session, type),
EnvoyQuicStream(
*this,
*this, *session,
// Flow control receive window should be larger than 8k to fully utilize congestion
// control window before it reaches the high watermark.
static_cast<uint32_t>(GetReceiveWindow().value()), *filterManagerConnection(),
Expand All @@ -42,6 +42,7 @@ EnvoyQuicServerStream::EnvoyQuicServerStream(

stats_gatherer_ = new QuicStatsGatherer(&filterManagerConnection()->dispatcher().timeSource());
set_ack_listener(stats_gatherer_);
RegisterMetadataVisitor(this);
}

void EnvoyQuicServerStream::encode1xxHeaders(const Http::ResponseHeaderMap& headers) {
Expand Down Expand Up @@ -449,6 +450,17 @@ EnvoyQuicServerStream::validateHeader(absl::string_view header_name,
return result;
}

void EnvoyQuicServerStream::OnMetadataComplete(size_t /*frame_len*/,
const quic::QuicHeaderList& header_list) {
if (mustRejectMetadata(header_list.uncompressed_header_bytes())) {
onStreamError(true, quic::QUIC_HEADERS_TOO_LARGE);
return;
}
if (!header_list.empty()) {
request_decoder_->decodeMetadata(metadataMapFromHeaderList(header_list));
}
}

void EnvoyQuicServerStream::onStreamError(absl::optional<bool> should_close_connection,
quic::QuicRstStreamErrorCode rst) {
if (details_.empty()) {
Expand Down
8 changes: 7 additions & 1 deletion source/common/quic/envoy_quic_server_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@

#include "quiche/common/platform/api/quiche_reference_counted.h"
#include "quiche/quic/core/http/quic_spdy_server_stream_base.h"
#include "quiche/quic/core/qpack/qpack_encoder.h"
#include "quiche/quic/core/qpack/qpack_instruction_encoder.h"

namespace Envoy {
namespace Quic {

// This class is a quic stream and also a response encoder.
class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase,
public EnvoyQuicStream,
public Http::ResponseEncoder {
public Http::ResponseEncoder,
public quic::QuicSpdyStream::MetadataVisitor {
public:
EnvoyQuicServerStream(quic::QuicStreamId id, quic::QuicSpdySession* session,
quic::StreamType type, Http::Http3::CodecStats& stats,
Expand Down Expand Up @@ -78,6 +81,9 @@ class EnvoyQuicServerStream : public quic::QuicSpdyServerStreamBase,
Http::HeaderUtility::HeaderValidationResult
validateHeader(absl::string_view header_name, absl::string_view header_value) override;

// quic::QuicSpdyStream::MetadataVisitor
void OnMetadataComplete(size_t frame_len, const quic::QuicHeaderList& header_list) override;

protected:
// EnvoyQuicStream
void switchStreamBlockState() override;
Expand Down
85 changes: 81 additions & 4 deletions source/common/quic/envoy_quic_stream.cc
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,87 @@ void EnvoyQuicStream::encodeTrailersImpl(spdy::Http2HeaderBlock&& trailers) {
onLocalEndStream();
}

void EnvoyQuicStream::encodeMetadata(const Http::MetadataMapVector& /*metadata_map_vector*/) {
// Metadata Frame is not supported in QUICHE.
ENVOY_STREAM_LOG(debug, "METADATA is not supported in Http3.", *this);
stats_.metadata_not_supported_error_.inc();
std::unique_ptr<Http::MetadataMap>
EnvoyQuicStream::metadataMapFromHeaderList(const quic::QuicHeaderList& header_list) {
auto metadata_map = std::make_unique<Http::MetadataMap>();
for (const auto& [key, value] : header_list) {
(*metadata_map)[key] = value;
}
return metadata_map;
}

namespace {

// Returns a new `unique_ptr<char[]>` containing the characters copied from `str`.
std::unique_ptr<char[]> dataFromString(const std::string& str) {
auto data = std::make_unique<char[]>(str.length());
memcpy(&data[0], str.data(), str.length()); // NOLINT(safe-memcpy)
return data;
}

void serializeMetadata(const Http::MetadataMapPtr& metadata, quic::QuicStreamId id,
absl::InlinedVector<quiche::QuicheMemSlice, 2>& slices) {
quic::NoopDecoderStreamErrorDelegate decoder_stream_error_delegate;
quic::QpackEncoder qpack_encoder(&decoder_stream_error_delegate,
quic::HuffmanEncoding::kDisabled);

spdy::Http2HeaderBlock header_block;
for (const auto& [key, value] : *metadata) {
header_block.AppendValueOrAddHeader(key, value);
}

// The METADATA frame consist of a frame header, which includes payload
// length, and a payload, which is the QPACK-encoded metadata block. In order
// to generate the frame header, the payload needs to be generated first.
std::string metadata_frame_payload =
qpack_encoder.EncodeHeaderList(id, header_block,
/* encoder_stream_sent_byte_count = */ nullptr);
std::string metadata_frame_header =
quic::HttpEncoder::SerializeMetadataFrameHeader(metadata_frame_payload.size());

slices.emplace_back(dataFromString(metadata_frame_header), metadata_frame_header.length());
slices.emplace_back(dataFromString(metadata_frame_payload), metadata_frame_payload.length());
}

} // namespace

void EnvoyQuicStream::encodeMetadata(const Http::MetadataMapVector& metadata_map_vector) {
if (!http3_options_.allow_metadata()) {
ENVOY_STREAM_LOG(debug, "METADATA not supported by config.", *this);
stats_.metadata_not_supported_error_.inc();
return;
}
if (quic_stream_.write_side_closed()) {
return;
}
ASSERT(!local_end_stream_);
Comment thread
alyssawilk marked this conversation as resolved.

for (const Http::MetadataMapPtr& metadata : metadata_map_vector) {
absl::InlinedVector<quiche::QuicheMemSlice, 2> quic_slices;
quic_slices.reserve(2);
serializeMetadata(metadata, quic_stream_.id(), quic_slices);
absl::Span<quiche::QuicheMemSlice> metadata_frame(quic_slices);

SendBufferMonitor::ScopedWatermarkBufferUpdater updater(&quic_stream_, this);
quic::QuicConsumedData result{0, false};
{
IncrementalBytesSentTracker tracker(quic_stream_, *mutableBytesMeter(), false);
result = quic_stream_.WriteMemSlices(metadata_frame, /*end_stream=*/false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Connection might be closed during write loop. Maybe early return in that case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

}
// QUIC stream must take all.
if (result.bytes_consumed == 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can this be true if the metadata passed in is empty?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No, because the METADATA frame header will always be present.

IS_ENVOY_BUG(fmt::format("Send buffer didn't take all the data. Stream is write {} with {} "
"bytes in send buffer. Current write was rejected.",
quic_stream_.write_side_closed() ? "closed" : "open",
quic_stream_.BufferedDataBytes()));
quic_stream_.Reset(quic::QUIC_ERROR_PROCESSING_STREAM);
return;
}
if (!quic_session_.connection()->connected()) {
// Return early if sending METADATA caused the connection to close.
return;
}
}
}

} // namespace Quic
Expand Down
21 changes: 17 additions & 4 deletions source/common/quic/envoy_quic_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder,
public:
// |buffer_limit| is the high watermark of the stream send buffer, and the low
// watermark will be half of it.
EnvoyQuicStream(quic::QuicSpdyStream& quic_stream, uint32_t buffer_limit,
QuicFilterManagerConnectionImpl& filter_manager_connection,
EnvoyQuicStream(quic::QuicSpdyStream& quic_stream, quic::QuicSession& quic_session,
uint32_t buffer_limit, QuicFilterManagerConnectionImpl& filter_manager_connection,
std::function<void()> below_low_watermark,
std::function<void()> above_high_watermark, Http::Http3::CodecStats& stats,
const envoy::config::core::v3::Http3ProtocolOptions& http3_options)
: Http::MultiplexedStreamImplBase(filter_manager_connection.dispatcher()), stats_(stats),
http3_options_(http3_options), quic_stream_(quic_stream),
http3_options_(http3_options), quic_stream_(quic_stream), quic_session_(quic_session),
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),
Expand Down Expand Up @@ -180,6 +180,17 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder,

void encodeTrailersImpl(spdy::Http2HeaderBlock&& trailers);

// Converts `header_list` into a new `Http::MetadataMap`.
std::unique_ptr<Http::MetadataMap>
metadataMapFromHeaderList(const quic::QuicHeaderList& header_list);

// Returns true if the cumulative limit on METADATA headers has been reached
// after adding `bytes`.
bool mustRejectMetadata(size_t bytes) {
received_metadata_bytes_ += bytes;
return received_metadata_bytes_ > 1 << 20;
}

#ifdef ENVOY_ENABLE_HTTP_DATAGRAMS
// Setting |http_datagram_handler_| enables HTTP Datagram support.
std::unique_ptr<HttpDatagramHandler> http_datagram_handler_;
Expand Down Expand Up @@ -210,8 +221,9 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder,
bool saw_regular_headers_{false};

private:
// QUIC stream that this EnvoyQuicStream wraps.
// QUIC stream and session that this EnvoyQuicStream wraps.
quic::QuicSpdyStream& quic_stream_;
quic::QuicSession& quic_session_;

// Keeps track of bytes buffered in the stream send buffer in QUICHE and reacts
// upon crossing high and low watermarks.
Expand All @@ -232,6 +244,7 @@ class EnvoyQuicStream : public virtual Http::StreamEncoder,
absl::optional<size_t> content_length_;
size_t received_content_bytes_{0};
http2::adapter::HeaderValidator header_validator_;
size_t received_metadata_bytes_{0};
};

// Object used for updating a BytesMeter to track bytes sent on a QuicStream since this object was
Expand Down
2 changes: 2 additions & 0 deletions source/common/quic/platform/quiche_flags_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
/* Envoy only supports RFC-v1 in the long term, so disable IETF draft 29 implementation by \
* default. */ \
KEY_VALUE_PAIR(quic_disable_version_draft_29, true) \
/* Enable support for HTTP/3 metadata decoding in QUICHE. */ \
KEY_VALUE_PAIR(quic_enable_http3_metadata_decoding, true) \
/* This flag enables BBR, otherwise QUIC will use Cubic which is less performant */ \
KEY_VALUE_PAIR(quic_default_to_bbr, true)

Expand Down
2 changes: 1 addition & 1 deletion source/common/quic/platform/quiche_flags_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ FlagRegistry::FlagRegistry() : reloadable_flags_(makeReloadableFlagMap()) {}
FlagRegistry& FlagRegistry::getInstance() {
static auto* instance = new FlagRegistry();
ASSERT(sizeof(quiche_reloadable_flag_overrides) / sizeof(std::pair<absl::string_view, bool>) ==
2);
3);
ASSERT(sizeof(quiche_protocol_flag_overrides) /
sizeof(std::pair<absl::string_view, absl::variant<bool, uint32_t>>) ==
3);
Expand Down
1 change: 1 addition & 0 deletions test/integration/fake_upstream.h
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ struct FakeUpstreamConfig {
http2_options_.set_allow_connect(true);
http2_options_.set_allow_metadata(true);
http3_options_.set_allow_extended_connect(true);
http3_options_.set_allow_metadata(true);
}

Event::TestTimeSystem& time_system_;
Expand Down
11 changes: 6 additions & 5 deletions test/integration/http_integration.cc
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,14 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection(
.value();
http2_options.value().set_allow_connect(true);
http2_options.value().set_allow_metadata(true);
}
#ifdef ENVOY_ENABLE_QUIC
} else {
cluster->http3_options_ = ConfigHelper::http2ToHttp3ProtocolOptions(
http2_options.value(), quic::kStreamReceiveWindowLimit);
cluster->http3_options_.set_allow_extended_connect(true);
cluster->http3_options_ = ConfigHelper::http2ToHttp3ProtocolOptions(
http2_options.value(), quic::kStreamReceiveWindowLimit);
cluster->http3_options_.set_allow_extended_connect(true);
cluster->http3_options_.set_allow_metadata(true);
#endif
}

cluster->http2_options_ = http2_options.value();
cluster->http1_settings_.enable_trailers_ = true;

Expand Down
Loading