This PR adds the support to probe alternative path on RTO instead of receiving a new connection ID. If RTO triggers while the client is not actively validating a path, it will probe for the multipath on RTO. ``` - The PR adds the connection option "MPR1". - Probe for alternative path on RTO. - Add kMPR1 option to enable probing on RTO. - Add unit test and e2e test. ``` Protected by QUIC connection option for multiport. PiperOrigin-RevId: 772554253
diff --git a/quiche/quic/core/crypto/crypto_protocol.h b/quiche/quic/core/crypto/crypto_protocol.h index 61f1212..244321f 100644 --- a/quiche/quic/core/crypto/crypto_protocol.h +++ b/quiche/quic/core/crypto/crypto_protocol.h
@@ -524,6 +524,7 @@ DEFINE_STATIC_QUIC_TAG(PDE2); // 2 PTOs for path degradation. DEFINE_STATIC_QUIC_TAG(PDE3); // 3 PTOs for path degradation. DEFINE_STATIC_QUIC_TAG(PDE5); // 5 PTOs for path degradation. +DEFINE_STATIC_QUIC_TAG(MPR1); // Probe for multi-port path on RTO. // Experiment for explicit connection close packet for silent idle timeout from // server.
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc index a589f49..6ae847b 100644 --- a/quiche/quic/core/http/end_to_end_test.cc +++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -5771,6 +5771,110 @@ stream->Reset(QuicRstStreamErrorCode::QUIC_STREAM_NO_ERROR); } +TEST_P(EndToEndTest, ClientMultiPortProbeOnRto) { + client_config_.SetClientConnectionOptions(QuicTagVector{kMPQC, kMPR1}); + ASSERT_TRUE(Initialize()); + if (!version_.HasIetfQuicFrames()) { + return; + } + client_.reset(EndToEndTest::CreateQuicClient(nullptr)); + ASSERT_TRUE(client_->client()->WaitForHandshakeConfirmed()); + + QuicConnection* client_connection = GetClientConnection(); + QuicSpdyClientStream* stream = client_->GetOrCreateStream(); + ASSERT_TRUE(stream); + + // Increase the probing frequency to speed up this test. + client_connection->SetMultiPortProbingInterval( + QuicTime::Delta::FromMilliseconds(100)); + + SendSynchronousFooRequestAndCheckResponse(); + + // Verify that no multiport connection is established before RTO. + EXPECT_TRUE(QuicConnectionPeer::GetServerConnectionIdOnAlternativePath( + client_connection) + .IsEmpty() || + client_connection->GetStats().pto_count > 0); + + // If no multiport connection is established, simulate a RTO and verify that + // the probing on RTO is triggered. + if (client_connection->multi_port_stats()->num_multi_port_paths_created == + 0) { + server_writer_->set_fake_packet_loss_percentage(100); + EXPECT_TRUE(client_->WaitUntil( + 1000, [&]() { return client_->client()->HasPendingPathValidation(); })); + server_writer_->set_fake_packet_loss_percentage(0); + // Now wait for path validation to complete. + EXPECT_TRUE(client_->WaitUntil(2000, [&]() { + return !client_->client()->HasPendingPathValidation(); + })); + } + + // Verify that a multiport connection is established. + EXPECT_EQ(client_connection->multi_port_stats()->num_multi_port_paths_created, + 1); + + // Verify that the probing is triggered after multiport connection is + // established. + EXPECT_TRUE(client_->WaitUntil(1000, [&]() { + return 1u == client_connection->GetStats().num_path_response_received; + })); + + // Verify that the alternative path keeps sending probes periodically. + EXPECT_TRUE(client_->WaitUntil(1000, [&]() { + return 2u == client_connection->GetStats().num_path_response_received; + })); + + // This will cause the next periodic probing to fail. + server_writer_->set_fake_packet_loss_percentage(100); + EXPECT_TRUE(client_->WaitUntil( + 1000, [&]() { return client_->client()->HasPendingPathValidation(); })); + // Now wait for path validation to timeout. + EXPECT_TRUE(client_->WaitUntil( + 2000, [&]() { return !client_->client()->HasPendingPathValidation(); })); + server_writer_->set_fake_packet_loss_percentage(0); + // Verify no new path response received on alternate path + EXPECT_TRUE(client_->WaitUntil(1000, [&]() { + return 2u == client_connection->GetStats().num_path_response_received; + })); + + // Verify that the previous path is retired after path validation times out. + EXPECT_TRUE(client_->WaitUntil(1000, [&]() { + return 1u == client_connection->GetStats().num_retire_connection_id_sent; + })); + + // Wait for new connection id to be received before new multiport connection + // is established. + WaitForNewConnectionIds(); + + // Send another request to make sure the server will have a chance to + // establish new multiport connection on RTO. + SendSynchronousFooRequestAndCheckResponse(); + + // Simulate another RTO and verify that the probing on RTO is triggered again. + server_writer_->set_fake_packet_loss_percentage(100); + + // Verify that a new multiport connection is established on RTO. + EXPECT_TRUE(client_->WaitUntil(2000, [&]() { + return client_connection->multi_port_stats() + ->num_multi_port_paths_created == 2; + })); + EXPECT_TRUE(client_->WaitUntil( + 2000, [&]() { return client_->client()->HasPendingPathValidation(); })); + server_writer_->set_fake_packet_loss_percentage(0); + // Now wait for path validation to complete. + EXPECT_TRUE(client_->WaitUntil( + 1000, [&]() { return !client_->client()->HasPendingPathValidation(); })); + + // Verify new path is validated after establishing a new multiport connection. + // Sometimes the path validation is trigerred more than 3 times. + EXPECT_TRUE(client_->WaitUntil(2000, [&]() { + return 3u >= client_connection->GetStats().num_path_response_received; + })); + + stream->Reset(QuicRstStreamErrorCode::QUIC_STREAM_NO_ERROR); +} + TEST_P(EndToEndTest, ClientPortMigrationOnPathDegrading) { connect_to_server_on_initialize_ = false; Initialize();
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc index f52a3f6..78a463f 100644 --- a/quiche/quic/core/quic_connection.cc +++ b/quiche/quic/core/quic_connection.cc
@@ -644,6 +644,9 @@ if (config.HasClientRequestedIndependentOption(kMPQM, perspective_)) { multi_port_migration_enabled_ = true; } + if (config.HasClientRequestedIndependentOption(kMPR1, perspective_)) { + multi_port_probing_on_rto_ = true; + } } if (config.HasMinAckDelayDraft10ToSend()) { @@ -2001,7 +2004,7 @@ NewConnectionIdResult result = OnNewConnectionIdFrameInner(frame); switch (result) { case NewConnectionIdResult::kOk: - if (multi_port_stats_ != nullptr) { + if (multi_port_stats_ != nullptr && !multi_port_probing_on_rto_) { MaybeCreateMultiPortPath(); } break; @@ -4277,6 +4280,16 @@ return result; } +// If self_issued_cid_manager_ or peer_issued_cid_manager_ are nullptr, +// then there is unused connection ID. Otherwise, check if there is unused +// connection ID in self_issued_cid_manager_ and peer_issued_cid_manager_. +bool QuicConnection::HasUnusedConnectionId() const { + return (self_issued_cid_manager_ == nullptr || + self_issued_cid_manager_->HasConnectionIdToConsume()) && + (peer_issued_cid_manager_ == nullptr || + peer_issued_cid_manager_->HasUnusedConnectionId()); +} + void QuicConnection::OnRetransmissionAlarm() { QUICHE_DCHECK(connected()); ScopedRetransmissionTimeoutIndicator indicator(this); @@ -4380,6 +4393,14 @@ << ", writer is blocked: " << writer_->IsWriteBlocked() << ", pending_timer_transmission_count: " << sent_packet_manager_.pending_timer_transmission_count(); + if (multi_port_probing_on_rto_ && IsHandshakeConfirmed() && + HasUnusedConnectionId()) { + QUICHE_DCHECK(multi_port_stats_ != nullptr && + version().HasIetfQuicFrames()); + + QUIC_VLOG(1) << "Maybe creating multiport path on PTO."; + MaybeCreateMultiPortPath(); + } } // Ensure the retransmission alarm is always set if there are unacked packets @@ -6865,10 +6886,7 @@ return; } } - if ((self_issued_cid_manager_ != nullptr && - !self_issued_cid_manager_->HasConnectionIdToConsume()) || - (peer_issued_cid_manager_ != nullptr && - !peer_issued_cid_manager_->HasUnusedConnectionId())) { + if (!HasUnusedConnectionId()) { QUIC_DVLOG(1) << "Client cannot start new path validation as there is no " "requried connection ID is available."; result_delegate->OnPathValidationFailure(std::move(context));
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h index 83646d4..8c7055d 100644 --- a/quiche/quic/core/quic_connection.h +++ b/quiche/quic/core/quic_connection.h
@@ -1357,6 +1357,8 @@ bool HasPendingPathValidation() const; + bool HasUnusedConnectionId() const; + QuicPathValidationContext* GetPathValidationContext() const; void CancelPathValidation(); @@ -2567,6 +2569,12 @@ // If true, connection will migrate to multi-port path upon path degrading. bool multi_port_migration_enabled_ = false; + // If true, connection will probe for multi-port path on RTO. This is only + // used on the client side. If false, connection will probe for multi-port + // path on receiving a new connection ID frame. See OnNewConnectionIdFrame() + // for more details. + bool multi_port_probing_on_rto_ = false; + // Client side only. bool active_migration_disabled_ = false; @@ -2632,7 +2640,6 @@ bool enable_black_hole_avoidance_via_flow_label_ = false; // If true, fixes a off-by-one error in the least unacked packet calculation. bool least_unacked_plus_1_; - const bool quic_limit_new_streams_per_loop_2_ = GetQuicReloadableFlag(quic_limit_new_streams_per_loop_2);