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);