Add multi-port support to a QUIC connection.

Client needs to turn on RVCM and MPQC to enable multi-port connection. The client will also need to introduce configs to set up probing frequency. The current default is every 3 seconds.

Note that this feature will interfere with port migration and early connection migration. I will need to add guards in Chrome once this CL is merged.

Protected by connection options MPQC.

PiperOrigin-RevId: 470073812
diff --git a/quiche/quic/core/crypto/crypto_protocol.h b/quiche/quic/core/crypto/crypto_protocol.h
index 6a78b6d..66db12d 100644
--- a/quiche/quic/core/crypto/crypto_protocol.h
+++ b/quiche/quic/core/crypto/crypto_protocol.h
@@ -439,6 +439,8 @@
 const QuicTag kINVC = TAG('I', 'N', 'V', 'C');   // Send connection close for
                                                  // INVALID_VERSION
 
+const QuicTag kMPQC = TAG('M', 'P', 'Q', 'C');   // Multi-port QUIC connection
+
 // Client Hints triggers.
 const QuicTag kGWCH = TAG('G', 'W', 'C', 'H');
 const QuicTag kYTCH = TAG('Y', 'T', 'C', 'H');
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index 713d7c5..f1db1c4 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -5402,6 +5402,49 @@
   server_thread_->Resume();
 }
 
+TEST_P(EndToEndTest, ClientMultiPortConnection) {
+  client_extra_copts_.push_back(kMPQC);
+  ASSERT_TRUE(Initialize());
+  if (!GetClientConnection()->connection_migration_use_new_cid()) {
+    return;
+  }
+  client_.reset(EndToEndTest::CreateQuicClient(nullptr));
+  QuicConnection* client_connection = GetClientConnection();
+  // Increase the probing frequency to speed up this test.
+  client_connection->SetMultiPortProbingInterval(
+      QuicTime::Delta::FromMilliseconds(100));
+  SendSynchronousFooRequestAndCheckResponse();
+  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;
+  }));
+  server_thread_->Pause();
+  QuicConnection* server_connection = GetServerConnection();
+  // Verify that no migration has happened.
+  if (server_connection != nullptr) {
+    EXPECT_EQ(0u, server_connection->GetStats()
+                      .num_peer_migration_to_proactively_validated_address);
+  }
+  server_thread_->Resume();
+
+  // 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);
+  EXPECT_TRUE(client_->WaitUntil(1000, [&]() {
+    return 3u == client_connection->GetStats().num_path_response_received;
+  }));
+  // Verify that the previous path was retired.
+  EXPECT_EQ(1u, client_connection->GetStats().num_retire_connection_id_sent);
+}
+
 TEST_P(EndToEndPacketReorderingTest, ReorderedPathChallenge) {
   ASSERT_TRUE(Initialize());
   if (!version_.HasIetfQuicFrames()) {
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 27aef7f..73f75a0 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -183,6 +183,17 @@
   }
 };
 
+class MultiPortProbingAlarmDelegate : public QuicConnectionAlarmDelegate {
+ public:
+  using QuicConnectionAlarmDelegate::QuicConnectionAlarmDelegate;
+
+  void OnAlarm() override {
+    QUICHE_DCHECK(connection_->connected());
+    QUIC_DLOG(INFO) << "Alternative path probing alarm fired";
+    connection_->ProbeMultiPortPath();
+  }
+};
+
 // When the clearer goes out of scope, the coalesced packet gets cleared.
 class ScopedCoalescedPacketClearer {
  public:
@@ -298,6 +309,8 @@
       discard_zero_rtt_decryption_keys_alarm_(alarm_factory_->CreateAlarm(
           arena_.New<DiscardZeroRttDecryptionKeysAlarmDelegate>(this),
           &arena_)),
+      multi_port_probing_alarm_(alarm_factory_->CreateAlarm(
+          arena_.New<MultiPortProbingAlarmDelegate>(this), &arena_)),
       visitor_(nullptr),
       debug_visitor_(nullptr),
       packet_creator_(server_connection_id, &framer_, random_generator_, this),
@@ -328,7 +341,8 @@
                              alarm_factory_, &context_),
       path_validator_(alarm_factory_, &arena_, this, random_generator_, clock_,
                       &context_),
-      ping_manager_(perspective, this, &arena_, alarm_factory_, &context_) {
+      ping_manager_(perspective, this, &arena_, alarm_factory_, &context_),
+      multi_port_probing_interval_(kDefaultMultiPortProbingInterval) {
   QUICHE_DCHECK(perspective_ == Perspective::IS_CLIENT ||
                 default_path_.self_address.IsInitialized());
 
@@ -679,6 +693,10 @@
   if (supports_release_time_) {
     UpdateReleaseTimeIntoFuture();
   }
+
+  multi_port_enabled_ =
+      connection_migration_use_new_cid_ &&
+      config.HasClientSentConnectionOption(kMPQC, perspective_);
 }
 
 void QuicConnection::EnableLegacyVersionEncapsulation(
@@ -1800,6 +1818,7 @@
       << "Processing PATH_RESPONSE frame when connection is closed. Received "
          "packet info: "
       << last_received_packet_info_;
+  ++stats_.num_path_response_received;
   if (!UpdatePacketContent(PATH_RESPONSE_FRAME)) {
     return false;
   }
@@ -2021,7 +2040,14 @@
   if (debug_visitor_ != nullptr) {
     debug_visitor_->OnNewConnectionIdFrame(frame);
   }
-  return OnNewConnectionIdFrameInner(frame);
+
+  if (!OnNewConnectionIdFrameInner(frame)) {
+    return false;
+  }
+  if (perspective_ == Perspective::IS_CLIENT && multi_port_enabled_) {
+    MaybeCreateMultiPortPath();
+  }
+  return true;
 }
 
 bool QuicConnection::OnRetireConnectionIdFrame(
@@ -4060,6 +4086,20 @@
                      kAlarmGranularity);
 }
 
+void QuicConnection::MaybeCreateMultiPortPath() {
+  QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, perspective_);
+  auto path_context = visitor_->CreateContextForMultiPortPath();
+  if (!path_context || path_validator_.HasPendingPathValidation()) {
+    return;
+  }
+  auto multi_port_validation_result_delegate =
+      std::make_unique<MultiPortPathValidationResultDelegate>(this);
+  multi_port_probing_alarm_->Cancel();
+  multi_port_path_context_ = nullptr;
+  ValidatePath(std::move(path_context),
+               std::move(multi_port_validation_result_delegate));
+}
+
 void QuicConnection::SendOrQueuePacket(SerializedPacket packet) {
   // The caller of this function is responsible for checking CanWrite().
   WritePacket(&packet);
@@ -7101,6 +7141,47 @@
                                          current_effective_peer_address.host());
 }
 
+void QuicConnection::OnMultiPortPathProbingSuccess(
+    std::unique_ptr<QuicPathValidationContext> context) {
+  multi_port_path_context_ = std::move(context);
+  multi_port_probing_alarm_->Set(clock_->ApproximateNow() +
+                                 multi_port_probing_interval_);
+}
+
+void QuicConnection::ProbeMultiPortPath() {
+  if (!connected_ || path_validator_.HasPendingPathValidation() ||
+      !multi_port_path_context_ ||
+      alternative_path_.self_address !=
+          multi_port_path_context_->self_address() ||
+      alternative_path_.peer_address !=
+          multi_port_path_context_->peer_address()) {
+    return;
+  }
+  auto multi_port_validation_result_delegate =
+      std::make_unique<MultiPortPathValidationResultDelegate>(this);
+  path_validator_.StartPathValidation(
+      std::move(multi_port_path_context_),
+      std::move(multi_port_validation_result_delegate));
+}
+
+QuicConnection::MultiPortPathValidationResultDelegate::
+    MultiPortPathValidationResultDelegate(QuicConnection* connection)
+    : connection_(connection) {
+  QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, connection->perspective());
+}
+
+void QuicConnection::MultiPortPathValidationResultDelegate::
+    OnPathValidationSuccess(std::unique_ptr<QuicPathValidationContext> context,
+                            QuicTime /*start_time*/) {
+  connection_->OnMultiPortPathProbingSuccess(std::move(context));
+}
+
+void QuicConnection::MultiPortPathValidationResultDelegate::
+    OnPathValidationFailure(
+        std::unique_ptr<QuicPathValidationContext> /*context*/) {
+  connection_->OnPathValidationFailureAtClient();
+}
+
 QuicConnection::ReversePathValidationResultDelegate::
     ReversePathValidationResultDelegate(
         QuicConnection* connection,
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index d7fa3e7..5c94c50 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -238,6 +238,10 @@
 
   // When bandwidth update alarms.
   virtual void OnBandwidthUpdateTimeout() = 0;
+
+  // Returns context needed for the connection to probe on the alternative path.
+  virtual std::unique_ptr<QuicPathValidationContext>
+  CreateContextForMultiPortPath() = 0;
 };
 
 // Interface which gets callbacks from the QuicConnection at interesting
@@ -737,6 +741,18 @@
   // TODO(fayang): Add a guard that this only gets called once.
   void OnHandshakeComplete();
 
+  // Creates and probes an multi-port path if none exists.
+  void MaybeCreateMultiPortPath();
+
+  // Called in multi-port QUIC when the alternative path validation succeeds.
+  // Stores the path validation context and prepares for the next validation.
+  void OnMultiPortPathProbingSuccess(
+      std::unique_ptr<QuicPathValidationContext> context);
+
+  // Probe the existing alternative path. Does not create a new alternative
+  // path. This method is the callback for |multi_port_probing_alarm_|.
+  void ProbeMultiPortPath();
+
   // Accessors
   void set_visitor(QuicConnectionVisitorInterface* visitor) {
     visitor_ = visitor;
@@ -778,6 +794,7 @@
   QuicByteCount max_packet_length() const;
   void SetMaxPacketLength(QuicByteCount length);
 
+  bool multi_port_enabled() const { return multi_port_enabled_; }
   size_t mtu_probe_count() const { return mtu_probe_count_; }
 
   bool connected() const { return connected_; }
@@ -799,6 +816,10 @@
   void SetNetworkTimeouts(QuicTime::Delta handshake_timeout,
                           QuicTime::Delta idle_timeout);
 
+  void SetMultiPortProbingInterval(QuicTime::Delta probing_interval) {
+    multi_port_probing_interval_ = probing_interval;
+  }
+
   // Called when the ping alarm fires. Causes a ping frame to be sent only
   // if the retransmission alarm is not running.
   void OnPingTimeout();
@@ -1479,6 +1500,24 @@
     AddressChangeType active_effective_peer_migration_type_;
   };
 
+  // Keeps an ongoing alternative path. The connection will not migrate upon
+  // validation success.
+  class MultiPortPathValidationResultDelegate
+      : public QuicPathValidator::ResultDelegate {
+   public:
+    MultiPortPathValidationResultDelegate(QuicConnection* connection);
+
+    void OnPathValidationSuccess(
+        std::unique_ptr<QuicPathValidationContext> context,
+        QuicTime start_time) override;
+
+    void OnPathValidationFailure(
+        std::unique_ptr<QuicPathValidationContext> context) override;
+
+   private:
+    QuicConnection* connection_;
+  };
+
   // A class which sets and clears in_on_retransmission_time_out_ when entering
   // and exiting OnRetransmissionTimeout, respectively.
   class QUIC_EXPORT_PRIVATE ScopedRetransmissionTimeoutIndicator {
@@ -2020,6 +2059,8 @@
   // first 1-RTT packet has been decrypted. Only used on server connections with
   // TLS handshaker.
   QuicArenaScopedPtr<QuicAlarm> discard_zero_rtt_decryption_keys_alarm_;
+  // An alarm that fires to keep probing the multi-port path.
+  QuicArenaScopedPtr<QuicAlarm> multi_port_probing_alarm_;
   // Neither visitor is owned by this class.
   QuicConnectionVisitorInterface* visitor_;
   QuicConnectionDebugVisitor* debug_visitor_;
@@ -2234,6 +2275,12 @@
   // Records first serialized 1-RTT packet.
   std::unique_ptr<BufferedPacket> first_serialized_one_rtt_packet_;
 
+  std::unique_ptr<QuicPathValidationContext> multi_port_path_context_;
+
+  bool multi_port_enabled_ = false;
+
+  QuicTime::Delta multi_port_probing_interval_;
+
   RetransmittableOnWireBehavior retransmittable_on_wire_behavior_ = DEFAULT;
 
   bool only_send_probing_frames_on_alternative_path_ =
diff --git a/quiche/quic/core/quic_connection_stats.cc b/quiche/quic/core/quic_connection_stats.cc
index bb53ca5..105d957 100644
--- a/quiche/quic/core/quic_connection_stats.cc
+++ b/quiche/quic/core/quic_connection_stats.cc
@@ -48,6 +48,7 @@
   os << " blocked_frames_sent: " << s.blocked_frames_sent;
   os << " num_connectivity_probing_received: "
      << s.num_connectivity_probing_received;
+  os << " num_path_response_received: " << s.num_path_response_received;
   os << " retry_packet_processed: "
      << (s.retry_packet_processed ? "yes" : "no");
   os << " num_coalesced_packets_received: " << s.num_coalesced_packets_received;
diff --git a/quiche/quic/core/quic_connection_stats.h b/quiche/quic/core/quic_connection_stats.h
index 4aaf780..2d25495 100644
--- a/quiche/quic/core/quic_connection_stats.h
+++ b/quiche/quic/core/quic_connection_stats.h
@@ -143,6 +143,9 @@
   // Number of connectivity probing packets received by this connection.
   uint64_t num_connectivity_probing_received = 0;
 
+  // Number of PATH_RESPONSE frame received by this connection.
+  uint64_t num_path_response_received = 0;
+
   // Whether a RETRY packet was successfully processed.
   bool retry_packet_processed = false;
 
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index 9f4e7c6..72530ae 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -468,6 +468,11 @@
         QuicConnectionPeer::GetRetireSelfIssuedConnectionIdAlarm(this));
   }
 
+  TestAlarmFactory::TestAlarm* GetMultiPortProbingAlarm() {
+    return reinterpret_cast<TestAlarmFactory::TestAlarm*>(
+        QuicConnectionPeer::GetMultiPortProbingAlarm(this));
+  }
+
   void PathDegradingTimeout() {
     QUICHE_DCHECK(PathDegradingDetectionInProgress());
     GetBlackholeDetectorAlarm()->Fire();
@@ -13065,6 +13070,68 @@
       &connection_, kNewSelfAddress, connection_.peer_address()));
 }
 
+TEST_P(QuicConnectionTest, MultiPortCreation) {
+  set_perspective(Perspective::IS_CLIENT);
+  QuicConfig config;
+  config.SetConnectionOptionsToSend(QuicTagVector{kMPQC, kRVCM});
+  EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+  connection_.SetFromConfig(config);
+  if (!connection_.connection_migration_use_new_cid()) {
+    return;
+  }
+  connection_.CreateConnectionIdManager();
+  connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+  connection_.OnHandshakeComplete();
+
+  auto self_address = connection_.self_address();
+  const QuicSocketAddress kNewSelfAddress(self_address.host(),
+                                          self_address.port() + 1);
+  EXPECT_NE(kNewSelfAddress, self_address);
+  TestPacketWriter new_writer(version(), &clock_, Perspective::IS_CLIENT);
+
+  QuicNewConnectionIdFrame frame;
+  frame.connection_id = TestConnectionId(1234);
+  ASSERT_NE(frame.connection_id, connection_.connection_id());
+  frame.stateless_reset_token =
+      QuicUtils::GenerateStatelessResetToken(frame.connection_id);
+  frame.retire_prior_to = 0u;
+  frame.sequence_number = 1u;
+  EXPECT_CALL(visitor_, CreateContextForMultiPortPath())
+      .WillRepeatedly(Return(
+          testing::ByMove(std::make_unique<TestQuicPathValidationContext>(
+              kNewSelfAddress, connection_.peer_address(), &new_writer))));
+  connection_.OnNewConnectionIdFrame(frame);
+  EXPECT_TRUE(connection_.HasPendingPathValidation());
+  EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath(
+      &connection_, kNewSelfAddress, connection_.peer_address()));
+
+  QuicFrames frames;
+  frames.push_back(QuicFrame(QuicPathResponseFrame(
+      99, new_writer.path_challenge_frames().front().data_buffer)));
+  ProcessFramesPacketWithAddresses(frames, kNewSelfAddress, kPeerAddress,
+                                   ENCRYPTION_FORWARD_SECURE);
+  // No migration should happen and the alternative path should still be alive.
+  EXPECT_FALSE(connection_.HasPendingPathValidation());
+  EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath(
+      &connection_, kNewSelfAddress, connection_.peer_address()));
+
+  connection_.GetMultiPortProbingAlarm()->Fire();
+  EXPECT_TRUE(connection_.HasPendingPathValidation());
+  EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath(
+      &connection_, kNewSelfAddress, connection_.peer_address()));
+  for (size_t i = 0; i < QuicPathValidator::kMaxRetryTimes + 1; ++i) {
+    clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(3 * kInitialRttMs));
+    static_cast<TestAlarmFactory::TestAlarm*>(
+        QuicPathValidatorPeer::retry_timer(
+            QuicConnectionPeer::path_validator(&connection_)))
+        ->Fire();
+  }
+
+  EXPECT_FALSE(connection_.HasPendingPathValidation());
+  EXPECT_FALSE(QuicConnectionPeer::IsAlternativePath(
+      &connection_, kNewSelfAddress, connection_.peer_address()));
+}
+
 TEST_P(QuicConnectionTest, SingleAckInPacket) {
   EXPECT_CALL(visitor_, OnSuccessfulVersionNegotiation(_));
   EXPECT_CALL(visitor_, OnConnectionClosed(_, _));
diff --git a/quiche/quic/core/quic_constants.h b/quiche/quic/core/quic_constants.h
index 46af2e8..13fa288 100644
--- a/quiche/quic/core/quic_constants.h
+++ b/quiche/quic/core/quic_constants.h
@@ -321,6 +321,9 @@
 
 inline constexpr uint64_t kHttpDatagramStreamIdDivisor = 4;
 
+inline constexpr QuicTime::Delta kDefaultMultiPortProbingInterval =
+    QuicTime::Delta::FromSeconds(3);
+
 }  // namespace quic
 
 #endif  // QUICHE_QUIC_CORE_QUIC_CONSTANTS_H_
diff --git a/quiche/quic/core/quic_session.h b/quiche/quic/core/quic_session.h
index e749abe..e51d77a 100644
--- a/quiche/quic/core/quic_session.h
+++ b/quiche/quic/core/quic_session.h
@@ -178,6 +178,10 @@
     return false;
   }
   void OnBandwidthUpdateTimeout() override {}
+  std::unique_ptr<QuicPathValidationContext> CreateContextForMultiPortPath()
+      override {
+    return nullptr;
+  }
 
   // QuicStreamFrameDataProducer
   WriteStreamDataResult WriteStreamData(QuicStreamId id,