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,
diff --git a/quiche/quic/test_tools/quic_connection_peer.cc b/quiche/quic/test_tools/quic_connection_peer.cc
index db827aa..eb28e2d 100644
--- a/quiche/quic/test_tools/quic_connection_peer.cc
+++ b/quiche/quic/test_tools/quic_connection_peer.cc
@@ -406,6 +406,12 @@
 }
 
 // static
+QuicAlarm* QuicConnectionPeer::GetMultiPortProbingAlarm(
+    QuicConnection* connection) {
+  return connection->multi_port_probing_alarm_.get();
+}
+
+// static
 void QuicConnectionPeer::SetServerConnectionId(
     QuicConnection* connection, const QuicConnectionId& server_connection_id) {
   connection->default_path_.server_connection_id = server_connection_id;
diff --git a/quiche/quic/test_tools/quic_connection_peer.h b/quiche/quic/test_tools/quic_connection_peer.h
index 7ad551f..169a33c 100644
--- a/quiche/quic/test_tools/quic_connection_peer.h
+++ b/quiche/quic/test_tools/quic_connection_peer.h
@@ -224,6 +224,8 @@
   static QuicCoalescedPacket& GetCoalescedPacket(QuicConnection* connection);
 
   static void FlushCoalescedPacket(QuicConnection* connection);
+
+  static QuicAlarm* GetMultiPortProbingAlarm(QuicConnection* connection);
 };
 
 }  // namespace test
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index af9cf2c..feb5719 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -501,6 +501,8 @@
   MOCK_METHOD(void, BeforeConnectionCloseSent, (), (override));
   MOCK_METHOD(bool, ValidateToken, (absl::string_view), (override));
   MOCK_METHOD(bool, MaybeSendAddressToken, (), (override));
+  MOCK_METHOD(std::unique_ptr<QuicPathValidationContext>,
+              CreateContextForMultiPortPath, (), (override));
 
   bool IsKnownServerAddress(
       const QuicSocketAddress& /*address*/) const override {
diff --git a/quiche/quic/test_tools/simulator/quic_endpoint.h b/quiche/quic/test_tools/simulator/quic_endpoint.h
index 7654d89..99b3915 100644
--- a/quiche/quic/test_tools/simulator/quic_endpoint.h
+++ b/quiche/quic/test_tools/simulator/quic_endpoint.h
@@ -109,6 +109,10 @@
     return false;
   }
   void OnBandwidthUpdateTimeout() override {}
+  std::unique_ptr<QuicPathValidationContext> CreateContextForMultiPortPath()
+      override {
+    return nullptr;
+  }
 
   // End QuicConnectionVisitorInterface implementation.
 
diff --git a/quiche/quic/tools/quic_client.cc b/quiche/quic/tools/quic_client.cc
index c6de357..fca34d2 100644
--- a/quiche/quic/tools/quic_client.cc
+++ b/quiche/quic/tools/quic_client.cc
@@ -96,8 +96,9 @@
     const ParsedQuicVersionVector& supported_versions,
     QuicConnection* connection) {
   return std::make_unique<QuicSimpleClientSession>(
-      *config(), supported_versions, connection, server_id(), crypto_config(),
-      push_promise_index(), drop_response_body(), enable_web_transport());
+      *config(), supported_versions, connection, network_helper(), server_id(),
+      crypto_config(), push_promise_index(), drop_response_body(),
+      enable_web_transport());
 }
 
 QuicClientEpollNetworkHelper* QuicClient::epoll_network_helper() {
diff --git a/quiche/quic/tools/quic_client_base.cc b/quiche/quic/tools/quic_client_base.cc
index 3a7dfd6..6cbfc13 100644
--- a/quiche/quic/tools/quic_client_base.cc
+++ b/quiche/quic/tools/quic_client_base.cc
@@ -18,24 +18,6 @@
 
 namespace quic {
 
-// A path context which owns the writer.
-class QUIC_EXPORT_PRIVATE PathMigrationContext
-    : public QuicPathValidationContext {
- public:
-  PathMigrationContext(std::unique_ptr<QuicPacketWriter> writer,
-                       const QuicSocketAddress& self_address,
-                       const QuicSocketAddress& peer_address)
-      : QuicPathValidationContext(self_address, peer_address),
-        alternative_writer_(std::move(writer)) {}
-
-  QuicPacketWriter* WriterToUse() override { return alternative_writer_.get(); }
-
-  QuicPacketWriter* ReleaseWriter() { return alternative_writer_.release(); }
-
- private:
-  std::unique_ptr<QuicPacketWriter> alternative_writer_;
-};
-
 // Implements the basic feature of a result delegate for path validation for
 // connection migration. If the validation succeeds, migrate to the alternative
 // path. Otherwise, stay on the current path.
diff --git a/quiche/quic/tools/quic_client_base.h b/quiche/quic/tools/quic_client_base.h
index 91c597b..e9700a8 100644
--- a/quiche/quic/tools/quic_client_base.h
+++ b/quiche/quic/tools/quic_client_base.h
@@ -26,6 +26,24 @@
 class QuicServerId;
 class SessionCache;
 
+// A path context which owns the writer.
+class QUIC_EXPORT_PRIVATE PathMigrationContext
+    : public QuicPathValidationContext {
+ public:
+  PathMigrationContext(std::unique_ptr<QuicPacketWriter> writer,
+                       const QuicSocketAddress& self_address,
+                       const QuicSocketAddress& peer_address)
+      : QuicPathValidationContext(self_address, peer_address),
+        alternative_writer_(std::move(writer)) {}
+
+  QuicPacketWriter* WriterToUse() override { return alternative_writer_.get(); }
+
+  QuicPacketWriter* ReleaseWriter() { return alternative_writer_.release(); }
+
+ private:
+  std::unique_ptr<QuicPacketWriter> alternative_writer_;
+};
+
 // QuicClientBase handles establishing a connection to the passed in
 // server id, including ensuring that it supports the passed in versions
 // and config.
diff --git a/quiche/quic/tools/quic_default_client.cc b/quiche/quic/tools/quic_default_client.cc
index de509c5..0f43aa5 100644
--- a/quiche/quic/tools/quic_default_client.cc
+++ b/quiche/quic/tools/quic_default_client.cc
@@ -86,8 +86,9 @@
     const ParsedQuicVersionVector& supported_versions,
     QuicConnection* connection) {
   return std::make_unique<QuicSimpleClientSession>(
-      *config(), supported_versions, connection, server_id(), crypto_config(),
-      push_promise_index(), drop_response_body(), enable_web_transport());
+      *config(), supported_versions, connection, network_helper(), server_id(),
+      crypto_config(), push_promise_index(), drop_response_body(),
+      enable_web_transport());
 }
 
 QuicClientDefaultNetworkHelper* QuicDefaultClient::default_network_helper() {
diff --git a/quiche/quic/tools/quic_simple_client_session.cc b/quiche/quic/tools/quic_simple_client_session.cc
index 1a001fb..013167c 100644
--- a/quiche/quic/tools/quic_simple_client_session.cc
+++ b/quiche/quic/tools/quic_simple_client_session.cc
@@ -10,22 +10,13 @@
 
 QuicSimpleClientSession::QuicSimpleClientSession(
     const QuicConfig& config, const ParsedQuicVersionVector& supported_versions,
-    QuicConnection* connection, const QuicServerId& server_id,
-    QuicCryptoClientConfig* crypto_config,
-    QuicClientPushPromiseIndex* push_promise_index, bool drop_response_body)
-    : QuicSimpleClientSession(config, supported_versions, connection, server_id,
-                              crypto_config, push_promise_index,
-                              drop_response_body,
-                              /*enable_web_transport=*/false) {}
-
-QuicSimpleClientSession::QuicSimpleClientSession(
-    const QuicConfig& config, const ParsedQuicVersionVector& supported_versions,
-    QuicConnection* connection, const QuicServerId& server_id,
-    QuicCryptoClientConfig* crypto_config,
+    QuicConnection* connection, QuicClientBase::NetworkHelper* network_helper,
+    const QuicServerId& server_id, QuicCryptoClientConfig* crypto_config,
     QuicClientPushPromiseIndex* push_promise_index, bool drop_response_body,
     bool enable_web_transport)
     : QuicSpdyClientSession(config, supported_versions, connection, server_id,
                             crypto_config, push_promise_index),
+      network_helper_(network_helper),
       drop_response_body_(drop_response_body),
       enable_web_transport_(enable_web_transport) {}
 
@@ -45,4 +36,24 @@
                                : HttpDatagramSupport::kNone;
 }
 
+std::unique_ptr<QuicPathValidationContext>
+QuicSimpleClientSession::CreateContextForMultiPortPath() {
+  if (!network_helper_ || !connection()->multi_port_enabled()) {
+    return nullptr;
+  }
+  auto self_address = connection()->self_address();
+  auto server_address = connection()->peer_address();
+  if (!network_helper_->CreateUDPSocketAndBind(
+          server_address, self_address.host(), self_address.port() + 1)) {
+    return nullptr;
+  }
+  QuicPacketWriter* writer = network_helper_->CreateQuicPacketWriter();
+  if (writer == nullptr) {
+    return nullptr;
+  }
+  return std::make_unique<PathMigrationContext>(
+      std::unique_ptr<QuicPacketWriter>(writer),
+      network_helper_->GetLatestClientAddress(), peer_address());
+}
+
 }  // namespace quic
diff --git a/quiche/quic/tools/quic_simple_client_session.h b/quiche/quic/tools/quic_simple_client_session.h
index 6b6f4a7..f3231ad 100644
--- a/quiche/quic/tools/quic_simple_client_session.h
+++ b/quiche/quic/tools/quic_simple_client_session.h
@@ -6,6 +6,7 @@
 #define QUICHE_QUIC_TOOLS_QUIC_SIMPLE_CLIENT_SESSION_H_
 
 #include "quiche/quic/core/http/quic_spdy_client_session.h"
+#include "quiche/quic/tools/quic_client_base.h"
 #include "quiche/quic/tools/quic_simple_client_stream.h"
 
 namespace quic {
@@ -15,13 +16,7 @@
   QuicSimpleClientSession(const QuicConfig& config,
                           const ParsedQuicVersionVector& supported_versions,
                           QuicConnection* connection,
-                          const QuicServerId& server_id,
-                          QuicCryptoClientConfig* crypto_config,
-                          QuicClientPushPromiseIndex* push_promise_index,
-                          bool drop_response_body);
-  QuicSimpleClientSession(const QuicConfig& config,
-                          const ParsedQuicVersionVector& supported_versions,
-                          QuicConnection* connection,
+                          QuicClientBase::NetworkHelper* network_helper,
                           const QuicServerId& server_id,
                           QuicCryptoClientConfig* crypto_config,
                           QuicClientPushPromiseIndex* push_promise_index,
@@ -30,8 +25,11 @@
   std::unique_ptr<QuicSpdyClientStream> CreateClientStream() override;
   bool ShouldNegotiateWebTransport() override;
   HttpDatagramSupport LocalHttpDatagramSupport() override;
+  std::unique_ptr<QuicPathValidationContext> CreateContextForMultiPortPath()
+      override;
 
  private:
+  QuicClientBase::NetworkHelper* network_helper_;
   const bool drop_response_body_;
   const bool enable_web_transport_;
 };