Support bundling retransmitted CRYPTO frames via the BCFR connection option. When the BCFR connection option is enabled on the client, QuicCryptoStream and QuicSession use ScopedPacketFlusher and ScopedEncryptionLevelContext during crypto retransmissions. This allows multiple pending or lost CRYPTO frames within the same packet number space to be bundled together into single packets at the appropriate encryption level. Protected by quic_reloadable_flag_quic_bundle_crypto_retransmits. PiperOrigin-RevId: 992089912
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index 1a689b4..fe4de6c 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -20,6 +20,7 @@ QUICHE_FLAG(bool, quiche_reloadable_flag_quic_bbr_always_exit_startup_on_loss, false, false, "Lets BBRv1 exit STARTUP if a large number of packets was lost even if there is no non-app-limited samples. Similar to applying connection option B1AL to all connection.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_bbr_exit_startup_on_loss_network_param, false, true, "Enables adjustment of BBRv1's exit-startup-on-loss behavior on established connections via NetworkParams::enable_bbr_exit_startup_on_loss.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_block_until_settings_received_copt, true, true, "If enabled and a BSUS connection is received, blocks server connections until SETTINGS frame is received.") +QUICHE_FLAG(bool, quiche_reloadable_flag_quic_bundle_crypto_retransmits, false, false, "When true, retransmissions of CRYPTO frames will be bundled in a single packet when possible.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_clear_packet_on_serialization_failure, false, false, "If true, clear QuicPacketCreator state when serialization failure.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_client_check_blockage_before_on_can_write, false, false, "If true, quic clients will only call OnCanWrite() upon write events if the writer is unblocked.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_close_on_idle_timeout, false, false, "If true, closes the connection if it has exceeded the idle timeout when deciding whether to open a stream.")
diff --git a/quiche/quic/core/crypto/crypto_protocol.h b/quiche/quic/core/crypto/crypto_protocol.h index 18ea114..c56eefc 100644 --- a/quiche/quic/core/crypto/crypto_protocol.h +++ b/quiche/quic/core/crypto/crypto_protocol.h
@@ -514,6 +514,7 @@ // Client Hello Padding tags, for experiments. DEFINE_STATIC_QUIC_TAG(CHP1); // 1-packet padding to CHLO. DEFINE_STATIC_QUIC_TAG(CHP2); // 2-packet padding to CHLO. +DEFINE_STATIC_QUIC_TAG(BCFR); // Bundle CRYPTO frames when retransmitting. // Stats collection tags DEFINE_STATIC_QUIC_TAG(EPID); // Endpoint identifier.
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc index fe0c746..c29d91e 100644 --- a/quiche/quic/core/http/end_to_end_test.cc +++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -1139,7 +1139,8 @@ } void TestMultiPacketChaosProtection(int num_packets, bool drop_first_packet, - bool kyber = false); + bool kyber = false, + bool zero_rtt = false); quiche::test::ScopedEnvironmentForThreads environment_; bool initialized_; @@ -7392,6 +7393,13 @@ /*drop_first_packet=*/true); } +TEST_P(EndToEndTest, ZeroRttTwoPacketChaosProtectionWithRetransmission) { + TestMultiPacketChaosProtection(/*num_packets=*/2, + /*drop_first_packet=*/true, + /*kyber=*/false, + /*zero_rtt=*/true); +} + TEST_P(EndToEndTest, ThreePacketChaosProtection) { TestMultiPacketChaosProtection(/*num_packets=*/3, /*drop_first_packet=*/false); @@ -7415,7 +7423,7 @@ void EndToEndTest::TestMultiPacketChaosProtection(int num_packets, bool drop_first_packet, - bool kyber) { + bool kyber, bool zero_rtt) { if (!version_.IsIetfQuic()) { ASSERT_TRUE(Initialize()); return; @@ -7427,14 +7435,34 @@ discard_length = 1216; enable_mlkem_in_client_ = true; } else { - discard_length = 1000 * num_packets; + discard_length = (zero_rtt ? 750 : 1000) * num_packets; client_config_.SetDiscardLengthToSend(discard_length); } + if (zero_rtt) { + client_config_.SetClientConnectionOptions({kBCFR}); + } ASSERT_TRUE(Initialize()); - auto copying_writer = new ChaosPacketWriter(version_, drop_first_packet); - delete client_writer_; - client_writer_ = copying_writer; - client_.reset(CreateQuicClient(client_writer_, /*connect=*/false)); + ChaosPacketWriter* copying_writer = nullptr; + if (zero_rtt) { + client_.reset(CreateQuicClient(client_writer_, /*connect=*/true)); + MockableQuicClient* client = client_->client(); + QuicConnection* client_connection = GetClientConnection(); + client_writer_->Initialize( + QuicConnectionPeer::GetHelper(client_connection), + QuicConnectionPeer::GetAlarmFactory(client_connection), + std::make_unique<ClientDelegate>(client)); + EXPECT_TRUE(client->connected()); + EXPECT_TRUE(SendSynchronousFooRequestAndCheckResponse()); + client_->Disconnect(); + copying_writer = new ChaosPacketWriter(version_, drop_first_packet); + client_writer_ = copying_writer; + client_->UseWriter(client_writer_); + } else { + copying_writer = new ChaosPacketWriter(version_, drop_first_packet); + delete client_writer_; + client_writer_ = copying_writer; + client_.reset(CreateQuicClient(client_writer_, /*connect=*/false)); + } client_->UseConnectionId(TestConnectionId()); client_->Connect(); MockableQuicClient* client = client_->client(); @@ -7446,6 +7474,9 @@ EXPECT_TRUE(client->connected()); // Make sure application data can be sent. EXPECT_TRUE(SendSynchronousFooRequestAndCheckResponse()); + if (zero_rtt) { + EXPECT_TRUE(client->EarlyDataAccepted()); + } // Make sure the first flight contains the entire client hello. QuicIntervalSet<QuicStreamOffset> crypto_data_intervals;
diff --git a/quiche/quic/core/quic_crypto_stream.cc b/quiche/quic/core/quic_crypto_stream.cc index 867b934..d59e1b3 100644 --- a/quiche/quic/core/quic_crypto_stream.cc +++ b/quiche/quic/core/quic_crypto_stream.cc
@@ -7,6 +7,7 @@ #include <algorithm> #include <cstdint> #include <memory> +#include <optional> #include <string> #include <vector> @@ -319,15 +320,32 @@ QUIC_BUG_IF(quic_bug_12573_3, !VersionIsIetfQuic(session()->transport_version())) << "Versions less than 47 don't write CRYPTO frames"; + std::optional<QuicConnection::ScopedPacketFlusher> retransmission_flusher; + if (session()->bundle_retransmitted_crypto_frames()) { + QUIC_RELOADABLE_FLAG_COUNT_N(quic_bundle_crypto_retransmits, 3, 3); + retransmission_flusher.emplace(session()->connection()); + } for (uint8_t i = INITIAL_DATA; i <= APPLICATION_DATA; ++i) { auto packet_number_space = static_cast<PacketNumberSpace>(i); QuicStreamSendBufferInlining* send_buffer = &substreams_[packet_number_space].send_buffer; + if (!send_buffer->HasPendingRetransmission()) { + continue; + } + EncryptionLevel retransmission_encryption_level = + GetEncryptionLevelToSendCryptoDataOfSpace(packet_number_space); + std::optional<QuicConnection::ScopedEncryptionLevelContext> + encryption_level_context; + if (session()->bundle_retransmitted_crypto_frames()) { + QUIC_RELOADABLE_FLAG_COUNT_N(quic_bundle_crypto_retransmits, 2, 3); + encryption_level_context.emplace(session()->connection(), + retransmission_encryption_level); + } while (send_buffer->HasPendingRetransmission()) { auto pending = send_buffer->NextPendingRetransmission(); size_t bytes_consumed = stream_delegate()->SendCryptoData( - GetEncryptionLevelToSendCryptoDataOfSpace(packet_number_space), - pending.length, pending.offset, HANDSHAKE_RETRANSMISSION); + retransmission_encryption_level, pending.length, pending.offset, + HANDSHAKE_RETRANSMISSION); send_buffer->OnStreamDataRetransmitted(pending.offset, bytes_consumed); if (bytes_consumed < pending.length) { return; @@ -504,12 +522,12 @@ if (retransmission.Empty()) { return true; } - for (const auto& interval : retransmission) { + EncryptionLevel retransmission_encryption_level = + GetEncryptionLevelToSendCryptoDataOfSpace( + QuicUtils::GetPacketNumberSpace(crypto_frame->level)); + for (const QuicInterval<QuicStreamOffset>& interval : retransmission) { size_t retransmission_offset = interval.min(); size_t retransmission_length = interval.max() - interval.min(); - EncryptionLevel retransmission_encryption_level = - GetEncryptionLevelToSendCryptoDataOfSpace( - QuicUtils::GetPacketNumberSpace(crypto_frame->level)); size_t bytes_consumed = stream_delegate()->SendCryptoData( retransmission_encryption_level, retransmission_length, retransmission_offset, type);
diff --git a/quiche/quic/core/quic_crypto_stream_test.cc b/quiche/quic/core/quic_crypto_stream_test.cc index fdf9109..a9437da 100644 --- a/quiche/quic/core/quic_crypto_stream_test.cc +++ b/quiche/quic/core/quic_crypto_stream_test.cc
@@ -29,7 +29,6 @@ using testing::_; using testing::InSequence; using testing::Invoke; -using testing::InvokeWithoutArgs; using testing::Return; namespace quic { @@ -837,6 +836,91 @@ stream_->OnCryptoFrameLost(&frame); } +TEST_F(QuicCryptoStreamTest, + RetransmitFramesBundlesCryptoFramesWhenOptionEnabled) { + if (!VersionIsIetfQuic(connection_->transport_version())) { + return; + } + session_.config()->SetClientConnectionOptions({kBCFR}); + session_.Initialize(); + EXPECT_TRUE(session_.bundle_retransmitted_crypto_frames()); + + InSequence s; + std::string data(1000, 'a'); + EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 1000, 0)) + .WillOnce(Invoke(connection_, + &MockQuicConnection::QuicConnection_SendCryptoData)); + stream_->WriteCryptoData(ENCRYPTION_INITIAL, data); + + connection_->SetEncrypter( + ENCRYPTION_ZERO_RTT, + std::make_unique<NullEncrypter>(Perspective::IS_CLIENT)); + connection_->SetDefaultEncryptionLevel(ENCRYPTION_ZERO_RTT); + EXPECT_EQ(ENCRYPTION_ZERO_RTT, connection_->encryption_level()); + + QuicCryptoFrame frame1(ENCRYPTION_INITIAL, 0, 74); + QuicCryptoFrame frame2(ENCRYPTION_INITIAL, 500, 500); + QuicFrames frames; + frames.push_back(QuicFrame(&frame1)); + frames.push_back(QuicFrame(&frame2)); + + EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 74, 0)) + .WillOnce(Invoke(connection_, + &MockQuicConnection::QuicConnection_SendCryptoData)); + EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 500, 500)) + .WillOnce(Invoke(connection_, + &MockQuicConnection::QuicConnection_SendCryptoData)); + // Both CRYPTO frames should be bundled into a single packet write. + EXPECT_CALL(*static_cast<MockPacketWriter*>(connection_->writer()), + WritePacket(_, _, _, _, _, _)) + .WillOnce(Return(WriteResult(WRITE_STATUS_OK, 0))); + EXPECT_TRUE(session_.RetransmitFrames(frames, PTO_RETRANSMISSION)); + EXPECT_EQ(ENCRYPTION_ZERO_RTT, connection_->encryption_level()); +} + +TEST_F(QuicCryptoStreamTest, + WritePendingCryptoRetransmissionBundlesCryptoFramesWhenOptionEnabled) { + if (!VersionIsIetfQuic(connection_->transport_version())) { + return; + } + session_.config()->SetClientConnectionOptions({kBCFR}); + session_.Initialize(); + EXPECT_TRUE(session_.bundle_retransmitted_crypto_frames()); + + InSequence s; + std::string data(1000, 'a'); + EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 1000, 0)) + .WillOnce(Invoke(connection_, + &MockQuicConnection::QuicConnection_SendCryptoData)); + stream_->WriteCryptoData(ENCRYPTION_INITIAL, data); + + connection_->SetEncrypter( + ENCRYPTION_ZERO_RTT, + std::make_unique<NullEncrypter>(Perspective::IS_CLIENT)); + connection_->SetDefaultEncryptionLevel(ENCRYPTION_ZERO_RTT); + EXPECT_EQ(ENCRYPTION_ZERO_RTT, connection_->encryption_level()); + + QuicCryptoFrame lost_frame1(ENCRYPTION_INITIAL, 0, 74); + QuicCryptoFrame lost_frame2(ENCRYPTION_INITIAL, 500, 500); + stream_->OnCryptoFrameLost(&lost_frame1); + stream_->OnCryptoFrameLost(&lost_frame2); + EXPECT_TRUE(stream_->HasPendingCryptoRetransmission()); + + EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 74, 0)) + .WillOnce(Invoke(connection_, + &MockQuicConnection::QuicConnection_SendCryptoData)); + EXPECT_CALL(*connection_, SendCryptoData(ENCRYPTION_INITIAL, 500, 500)) + .WillOnce(Invoke(connection_, + &MockQuicConnection::QuicConnection_SendCryptoData)); + // Both lost CRYPTO frames should be bundled into a single packet write. + EXPECT_CALL(*static_cast<MockPacketWriter*>(connection_->writer()), + WritePacket(_, _, _, _, _, _)) + .WillOnce(Return(WriteResult(WRITE_STATUS_OK, 0))); + stream_->WritePendingCryptoRetransmission(); + EXPECT_FALSE(stream_->HasPendingCryptoRetransmission()); + EXPECT_EQ(ENCRYPTION_ZERO_RTT, connection_->encryption_level()); +} + } // namespace } // namespace test } // namespace quic
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc index c8c28c7..01c10e2 100644 --- a/quiche/quic/core/quic_session.cc +++ b/quiche/quic/core/quic_session.cc
@@ -234,6 +234,9 @@ connection_->version().IsIetfQuic()) { config()->SetMinAckDelayDraft10Ms(kDefaultMinAckDelayTimeMs); } + if (config()->HasClientRequestedIndependentOption(kBCFR, perspective_)) { + bundle_retransmitted_crypto_frames_ = true; + } if (GetQuicReloadableFlag(quic_active_connection_id_limit) && connection_->version().IsIetfQuic()) { if (config()->HasClientRequestedIndependentOption(k3CID, perspective_)) { @@ -2661,17 +2664,32 @@ bool QuicSession::RetransmitFrames(const QuicFrames& frames, TransmissionType type) { QuicConnection::ScopedPacketFlusher retransmission_flusher(connection_); + std::optional<QuicConnection::ScopedEncryptionLevelContext> + encryption_level_context; for (const QuicFrame& frame : frames) { if (frame.type == DATAGRAM_FRAME) { // Do not retransmit DATAGRAM frames. continue; } if (frame.type == CRYPTO_FRAME) { + if (bundle_retransmitted_crypto_frames_ && + (!encryption_level_context.has_value() || + connection_->encryption_level() != frame.crypto_frame->level)) { + QUIC_RELOADABLE_FLAG_COUNT_N(quic_bundle_crypto_retransmits, 1, 3); + // Crypto frames are only retransmitted in the same encryption level + // as the original frame, and adding an encryption level context ensures + // they are bundled together if possible. + encryption_level_context.emplace( + connection_, + GetCryptoStream()->GetEncryptionLevelToSendCryptoDataOfSpace( + QuicUtils::GetPacketNumberSpace(frame.crypto_frame->level))); + } if (!GetMutableCryptoStream()->RetransmitData(frame.crypto_frame, type)) { return false; } continue; } + encryption_level_context.reset(); if (frame.type != STREAM_FRAME) { if (!control_frame_manager_.RetransmitControlFrame(frame, type)) { return false;
diff --git a/quiche/quic/core/quic_session.h b/quiche/quic/core/quic_session.h index 839638d..6faed44 100644 --- a/quiche/quic/core/quic_session.h +++ b/quiche/quic/core/quic_session.h
@@ -871,6 +871,10 @@ return notify_stream_soon_to_destroy_; } + bool bundle_retransmitted_crypto_frames() const { + return bundle_retransmitted_crypto_frames_; + } + protected: using StreamMap = absl::flat_hash_map<QuicStreamId, std::unique_ptr<QuicStream>>; @@ -1305,6 +1309,9 @@ UberQuicStreamIdManager ietf_streamid_manager_; SavedConfig saved_config_; + + bool bundle_retransmitted_crypto_frames_ = + GetQuicReloadableFlag(quic_bundle_crypto_retransmits); }; } // namespace quic