Limit the number of port migrations a QUIC client will attempt upon path degrading signals. This is to prevent the client from migrating port too frequently, which may cause port exhaustion.

Also add CODE_COUNT for case where path degrading happens after the upper limit has been reached.

PiperOrigin-RevId: 640248232
diff --git a/quiche/common/quiche_protocol_flags_list.h b/quiche/common/quiche_protocol_flags_list.h
index bcdec6b..7e67582 100644
--- a/quiche/common/quiche_protocol_flags_list.h
+++ b/quiche/common/quiche_protocol_flags_list.h
@@ -263,4 +263,8 @@
 QUICHE_PROTOCOL_FLAG(int64_t, quic_default_delayed_ack_time_ms, 25,
                      "Default maximum delayed ack time, in ms.")
 
+QUICHE_PROTOCOL_FLAG(
+    uint32_t, quic_max_num_path_degrading_to_mitigate, 5,
+    "The maximum number of path degrading to mitigate with port migration. Any "
+    "further path degrading will not kick off port migration.")
 #endif
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index f3388eb..ed788bb 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -5470,6 +5470,59 @@
   EXPECT_GT(pto_count + 4, GetClientConnection()->GetStats().pto_count);
 }
 
+TEST_P(EndToEndTest, ClientLimitPortMigrationOnPathDegrading) {
+  connect_to_server_on_initialize_ = false;
+  Initialize();
+  if (!version_.HasIetfQuicFrames()) {
+    CreateClientWithWriter();
+    return;
+  }
+  const uint32_t max_num_path_degrading_to_mitigate =
+      GetQuicFlag(quic_max_num_path_degrading_to_mitigate);
+
+  delete client_writer_;
+  client_.reset(EndToEndTest::CreateQuicClient(nullptr));
+  client_->client()->EnablePortMigrationUponPathDegrading(std::nullopt);
+  ASSERT_TRUE(client_->client()->WaitForHandshakeConfirmed());
+  QuicConnection* client_connection = GetClientConnection();
+  Http2HeaderBlock headers;
+  headers[":method"] = "POST";
+  headers[":path"] = "/bar";
+  headers[":scheme"] = "https";
+  headers[":authority"] = server_hostname_;
+  // Manually trigger path degrading 5 times and expect they should all trigger
+  // port migration.
+  for (uint32_t i = 0; i < max_num_path_degrading_to_mitigate; ++i) {
+    client_->SendMessage(headers, "aaaa", false);
+    QuicSocketAddress original_self_addr = client_connection->self_address();
+    WaitForNewConnectionIds();
+    client_connection->OnPathDegradingDetected();
+    client_->SendData("bbbb", true);
+    // By the time the response is received, path validation should have been
+    // finished.
+    client_->WaitForResponse();
+    QuicSocketAddress new_self_addr = client_connection->self_address();
+    EXPECT_NE(original_self_addr, new_self_addr);
+  }
+
+  EXPECT_EQ(max_num_path_degrading_to_mitigate,
+            GetClientConnection()->GetStats().num_path_degrading);
+  EXPECT_EQ(max_num_path_degrading_to_mitigate,
+            GetClientConnection()->GetStats().num_path_response_received);
+
+  // The next path degrading shouldn't trigger port migration.
+  WaitForNewConnectionIds();
+  QuicSocketAddress original_self_addr = client_connection->self_address();
+  client_connection->OnPathDegradingDetected();
+  EXPECT_FALSE(client_->client()->HasPendingPathValidation());
+  client_->SendSynchronousRequest("/eep");
+  EXPECT_EQ(original_self_addr, client_connection->self_address());
+  EXPECT_EQ(max_num_path_degrading_to_mitigate + 1,
+            GetClientConnection()->GetStats().num_path_degrading);
+  EXPECT_EQ(max_num_path_degrading_to_mitigate,
+            GetClientConnection()->GetStats().num_path_response_received);
+}
+
 TEST_P(EndToEndTest, ClientMultiPortMigrationOnPathDegrading) {
   client_config_.SetClientConnectionOptions(QuicTagVector{kMPQC, kMPQM});
   ASSERT_TRUE(Initialize());
diff --git a/quiche/quic/tools/quic_client_base.cc b/quiche/quic/tools/quic_client_base.cc
index 03001fd..2e834bc 100644
--- a/quiche/quic/tools/quic_client_base.cc
+++ b/quiche/quic/tools/quic_client_base.cc
@@ -27,6 +27,7 @@
 #include "quiche/quic/core/quic_utils.h"
 #include "quiche/quic/core/quic_versions.h"
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
+#include "quiche/quic/platform/api/quic_flag_utils.h"
 #include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/quic/platform/api/quic_ip_address.h"
 #include "quiche/quic/platform/api/quic_logging.h"
@@ -237,6 +238,7 @@
     session()->connection()->SetVersionNegotiated();
   }
   set_connected_or_attempting_connect(true);
+  num_path_degrading_handled_ = 0;
 }
 
 void QuicClientBase::InitializeSession() { session()->Initialize(); }
@@ -573,6 +575,11 @@
       config_.DisableConnectionMigration()) {
     return;
   }
+  if (num_path_degrading_handled_ >=
+      GetQuicFlag(quic_max_num_path_degrading_to_mitigate)) {
+    QUIC_CODE_COUNT(reached_port_migration_upper_limit);
+    return;
+  }
   const auto self_address = session_->self_address();
   if (network_helper_ == nullptr ||
       !network_helper_->CreateUDPSocketAndBind(session_->peer_address(),
@@ -583,6 +590,7 @@
   if (writer == nullptr) {
     return;
   }
+  ++num_path_degrading_handled_;
   session()->ValidatePath(
       std::make_unique<PathMigrationContext>(
           std::unique_ptr<QuicPacketWriter>(writer),
diff --git a/quiche/quic/tools/quic_client_base.h b/quiche/quic/tools/quic_client_base.h
index f425bfa..c707176 100644
--- a/quiche/quic/tools/quic_client_base.h
+++ b/quiche/quic/tools/quic_client_base.h
@@ -8,6 +8,7 @@
 #ifndef QUICHE_QUIC_TOOLS_QUIC_CLIENT_BASE_H_
 #define QUICHE_QUIC_TOOLS_QUIC_CLIENT_BASE_H_
 
+#include <cstdint>
 #include <memory>
 #include <string>
 
@@ -492,6 +493,7 @@
       kQuicDefaultConnectionIdLength};
 
   bool allow_port_migration_{false};
+  uint32_t num_path_degrading_handled_{0};
 };
 
 }  // namespace quic