Migrate to multi-port path on path degrading Protected by client side change only. PiperOrigin-RevId: 516611509
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc index 0ee3cf0..75872f4 100644 --- a/quiche/quic/core/http/end_to_end_test.cc +++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -5509,6 +5509,50 @@ stream->Reset(QuicRstStreamErrorCode::QUIC_STREAM_NO_ERROR); } +TEST_P(EndToEndTest, ClientMultiPortMigrationOnPathDegrading) { + client_config_.SetClientConnectionOptions(QuicTagVector{kMPQC}); + ASSERT_TRUE(Initialize()); + if (!GetClientConnection()->connection_migration_use_new_cid()) { + return; + } + client_.reset(EndToEndTest::CreateQuicClient(nullptr)); + 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(); + 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(); + + auto original_self_addr = client_connection->self_address(); + // Trigger client side path degrading + client_connection->OnPathDegradingDetected(); + EXPECT_NE(original_self_addr, client_connection->self_address()); + + // Send another request to trigger connection id retirement. + SendSynchronousFooRequestAndCheckResponse(); + EXPECT_EQ(1u, client_connection->GetStats().num_retire_connection_id_sent); + auto new_alt_path = QuicConnectionPeer::GetAlternativePath(client_connection); + EXPECT_NE(client_connection->self_address(), new_alt_path->self_address); + + stream->Reset(QuicRstStreamErrorCode::QUIC_STREAM_NO_ERROR); +} + TEST_P(EndToEndTest, SimpleServerPreferredAddressTest) { use_preferred_address_ = true; ASSERT_TRUE(Initialize());
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc index dbede25..659821a 100644 --- a/quiche/quic/core/quic_connection.cc +++ b/quiche/quic/core/quic_connection.cc
@@ -1137,9 +1137,11 @@ void QuicConnection::OnSuccessfulMigration(bool is_port_change) { QUICHE_DCHECK_EQ(perspective_, Perspective::IS_CLIENT); - if (IsPathDegrading()) { + if (IsPathDegrading() && !multi_port_stats_) { // If path was previously degrading, and migration is successful after // probing, restart the path degrading and blackhole detection. + // In the case of multi-port, since the alt-path state is inferred from + // historical data, we can't trust it until we receive data on the new path. OnForwardProgressMade(); } if (IsAlternativePath(default_path_.self_address, @@ -6164,10 +6166,49 @@ void QuicConnection::OnPathDegradingDetected() { is_path_degrading_ = true; + visitor_->OnPathDegrading(); if (multi_port_stats_) { multi_port_stats_->num_path_degrading++; + MaybeMigrateToMultiPortPath(); } - visitor_->OnPathDegrading(); +} + +void QuicConnection::MaybeMigrateToMultiPortPath() { + if (!alternative_path_.validated) { + QUIC_CLIENT_HISTOGRAM_ENUM( + "QuicConnection.MultiPortPathStatusWhenMigrating", + MultiPortStatusOnMigration::kNotValidated, + MultiPortStatusOnMigration::kMaxValue, + "Status of the multi port path upon migration"); + return; + } + std::unique_ptr<QuicPathValidationContext> context; + const bool has_pending_validation = + path_validator_.HasPendingPathValidation(); + if (!has_pending_validation) { + // The multi-port path should have just finished the recent probe and + // waiting for the next one. + context = std::move(multi_port_path_context_); + multi_port_probing_alarm_->Cancel(); + QUIC_CLIENT_HISTOGRAM_ENUM( + "QuicConnection.MultiPortPathStatusWhenMigrating", + MultiPortStatusOnMigration::kWaitingForRefreshValidation, + MultiPortStatusOnMigration::kMaxValue, + "Status of the multi port path upon migration"); + } else { + // The multi-port path is currently under probing. + context = path_validator_.ReleaseContext(); + QUIC_CLIENT_HISTOGRAM_ENUM( + "QuicConnection.MultiPortPathStatusWhenMigrating", + MultiPortStatusOnMigration::kPendingRefreshValidation, + MultiPortStatusOnMigration::kMaxValue, + "Status of the multi port path upon migration"); + } + if (context == nullptr) { + QUICHE_BUG(quic_bug_12714_90) << "No multi-port context to migrate to"; + return; + } + visitor_->MigrateToMultiPortPath(std::move(context)); } void QuicConnection::OnBlackholeDetected() {
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h index 8a8717b..bb6de27 100644 --- a/quiche/quic/core/quic_connection.h +++ b/quiche/quic/core/quic_connection.h
@@ -241,6 +241,10 @@ virtual std::unique_ptr<QuicPathValidationContext> CreateContextForMultiPortPath() = 0; + // Migrate to the multi-port path which is identified by |context|. + virtual void MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> context) = 0; + // Called when the client receives a preferred address from its peer. virtual void OnServerPreferredAddressAvailable( const QuicSocketAddress& server_preferred_address) = 0; @@ -1409,6 +1413,13 @@ SEND_RANDOM_BYTES // Send random bytes which is an unprocessable packet. }; + enum class MultiPortStatusOnMigration { + kNotValidated, + kPendingRefreshValidation, + kWaitingForRefreshValidation, + kMaxValue, + }; + struct QUIC_EXPORT_PRIVATE PendingPathChallenge { QuicPathFrameBuffer received_path_challenge; QuicSocketAddress peer_address; @@ -1922,6 +1933,10 @@ // Return true if framer should continue processing the packet. bool OnPathChallengeFrameInternal(const QuicPathChallengeFrame& frame); + // Check the state of the multi-port alternative path and initiate path + // migration. + void MaybeMigrateToMultiPortPath(); + std::unique_ptr<QuicSelfIssuedConnectionIdManager> MakeSelfIssuedConnectionIdManager();
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc index a235314..e0cd6e3 100644 --- a/quiche/quic/core/quic_connection_test.cc +++ b/quiche/quic/core/quic_connection_test.cc
@@ -13568,6 +13568,234 @@ stats->num_multi_port_probe_failures_when_path_degrading); } +// Verify that when multi-port is enabled and path degrading is triggered, if +// the alt-path is not ready, nothing happens. +TEST_P(QuicConnectionTest, PathDegradingWhenAltPathIsNotReady) { + set_perspective(Perspective::IS_CLIENT); + QuicConfig config; + config.SetConnectionOptionsToSend(QuicTagVector{kRVCM}); + config.SetClientConnectionOptions(QuicTagVector{kMPQC}); + 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); + + EXPECT_CALL(visitor_, ShouldKeepConnectionAlive()) + .WillRepeatedly(Return(true)); + + 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)))); + EXPECT_TRUE(connection_.OnNewConnectionIdFrame(frame)); + EXPECT_TRUE(connection_.HasPendingPathValidation()); + EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath( + &connection_, kNewSelfAddress, connection_.peer_address())); + auto* alt_path = QuicConnectionPeer::GetAlternativePath(&connection_); + EXPECT_FALSE(alt_path->validated); + + // The alt path is not ready, path degrading doesn't do anything. + EXPECT_CALL(visitor_, OnPathDegrading()); + EXPECT_CALL(visitor_, MigrateToMultiPortPath(_)).Times(0); + connection_.OnPathDegradingDetected(); + + // 30ms RTT. + const QuicTime::Delta kTestRTT = QuicTime::Delta::FromMilliseconds(30); + // Fake a response delay. + clock_.AdvanceTime(kTestRTT); + + // Even if the alt path is validated after path degrading, nothing should + // happen. + QuicFrames frames; + frames.push_back(QuicFrame(QuicPathResponseFrame( + 99, new_writer.path_challenge_frames().back().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())); + EXPECT_TRUE(alt_path->validated); +} + +// Verify that when multi-port is enabled and path degrading is triggered, if +// the alt-path is ready and not probing, it should be migrated. +TEST_P(QuicConnectionTest, PathDegradingWhenAltPathIsReadyAndNotProbing) { + EXPECT_CALL(visitor_, GetHandshakeState()) + .WillRepeatedly(Return(HANDSHAKE_CONFIRMED)); + set_perspective(Perspective::IS_CLIENT); + QuicConfig config; + config.SetConnectionOptionsToSend(QuicTagVector{kRVCM}); + config.SetClientConnectionOptions(QuicTagVector{kMPQC}); + 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); + + EXPECT_CALL(visitor_, ShouldKeepConnectionAlive()) + .WillRepeatedly(Return(true)); + + 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)))); + EXPECT_TRUE(connection_.OnNewConnectionIdFrame(frame)); + EXPECT_TRUE(connection_.HasPendingPathValidation()); + EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath( + &connection_, kNewSelfAddress, connection_.peer_address())); + auto* alt_path = QuicConnectionPeer::GetAlternativePath(&connection_); + EXPECT_FALSE(alt_path->validated); + + // 30ms RTT. + const QuicTime::Delta kTestRTT = QuicTime::Delta::FromMilliseconds(30); + // Fake a response delay. + clock_.AdvanceTime(kTestRTT); + + // Even if the alt path is validated after path degrading, nothing should + // happen. + QuicFrames frames; + frames.push_back(QuicFrame(QuicPathResponseFrame( + 99, new_writer.path_challenge_frames().back().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())); + EXPECT_TRUE(alt_path->validated); + + // Trigger path degrading and the connection should attempt to migrate. + EXPECT_CALL(visitor_, OnPathDegrading()); + EXPECT_CALL(visitor_, OnForwardProgressMadeAfterPathDegrading()).Times(0); + EXPECT_CALL(visitor_, MigrateToMultiPortPath(_)) + .WillOnce(Invoke([&](std::unique_ptr<QuicPathValidationContext> context) { + EXPECT_EQ(context->self_address(), kNewSelfAddress); + connection_.MigratePath(context->self_address(), + context->peer_address(), context->WriterToUse(), + /*owns_writer=*/false); + })); + connection_.OnPathDegradingDetected(); +} + +// Verify that when multi-port is enabled and path degrading is triggered, if +// the alt-path is probing, the probing should be cancelled and the path should +// be migrated. +TEST_P(QuicConnectionTest, PathDegradingWhenAltPathIsReadyAndProbing) { + EXPECT_CALL(visitor_, GetHandshakeState()) + .WillRepeatedly(Return(HANDSHAKE_CONFIRMED)); + set_perspective(Perspective::IS_CLIENT); + QuicConfig config; + config.SetConnectionOptionsToSend(QuicTagVector{kRVCM}); + config.SetClientConnectionOptions(QuicTagVector{kMPQC}); + 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); + + EXPECT_CALL(visitor_, ShouldKeepConnectionAlive()) + .WillRepeatedly(Return(true)); + + 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)))); + EXPECT_TRUE(connection_.OnNewConnectionIdFrame(frame)); + EXPECT_TRUE(connection_.HasPendingPathValidation()); + EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath( + &connection_, kNewSelfAddress, connection_.peer_address())); + auto* alt_path = QuicConnectionPeer::GetAlternativePath(&connection_); + EXPECT_FALSE(alt_path->validated); + + // 30ms RTT. + const QuicTime::Delta kTestRTT = QuicTime::Delta::FromMilliseconds(30); + // Fake a response delay. + clock_.AdvanceTime(kTestRTT); + + // Even if the alt path is validated after path degrading, nothing should + // happen. + QuicFrames frames; + frames.push_back(QuicFrame(QuicPathResponseFrame( + 99, new_writer.path_challenge_frames().back().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())); + EXPECT_TRUE(alt_path->validated); + + random_generator_.ChangeValue(); + connection_.GetMultiPortProbingAlarm()->Fire(); + EXPECT_TRUE(connection_.HasPendingPathValidation()); + EXPECT_FALSE(connection_.GetMultiPortProbingAlarm()->IsSet()); + + // Trigger path degrading and the connection should attempt to migrate. + EXPECT_CALL(visitor_, OnPathDegrading()); + EXPECT_CALL(visitor_, OnForwardProgressMadeAfterPathDegrading()).Times(0); + EXPECT_CALL(visitor_, MigrateToMultiPortPath(_)) + .WillOnce(Invoke([&](std::unique_ptr<QuicPathValidationContext> context) { + EXPECT_EQ(context->self_address(), kNewSelfAddress); + connection_.MigratePath(context->self_address(), + context->peer_address(), context->WriterToUse(), + /*owns_writer=*/false); + })); + connection_.OnPathDegradingDetected(); + EXPECT_FALSE(connection_.HasPendingPathValidation()); + auto* path_validator = QuicConnectionPeer::path_validator(&connection_); + EXPECT_FALSE(QuicPathValidatorPeer::retry_timer(path_validator)->IsSet()); +} + TEST_P(QuicConnectionTest, SingleAckInPacket) { EXPECT_CALL(visitor_, OnSuccessfulVersionNegotiation(_)); EXPECT_CALL(visitor_, OnConnectionClosed(_, _));
diff --git a/quiche/quic/core/quic_path_validator.cc b/quiche/quic/core/quic_path_validator.cc index da29fac..48c21d2 100644 --- a/quiche/quic/core/quic_path_validator.cc +++ b/quiche/quic/core/quic_path_validator.cc
@@ -114,6 +114,12 @@ return path_context_.get(); } +std::unique_ptr<QuicPathValidationContext> QuicPathValidator::ReleaseContext() { + auto ret = std::move(path_context_); + ResetPathValidation(); + return ret; +} + const QuicPathFrameBuffer& QuicPathValidator::GeneratePathChallengePayload() { probing_data_.emplace_back(clock_->Now()); random_->RandBytes(probing_data_.back().frame_buffer.data(),
diff --git a/quiche/quic/core/quic_path_validator.h b/quiche/quic/core/quic_path_validator.h index fe975b0..1079f21 100644 --- a/quiche/quic/core/quic_path_validator.h +++ b/quiche/quic/core/quic_path_validator.h
@@ -5,6 +5,7 @@ #ifndef QUICHE_QUIC_CORE_QUIC_PATH_VALIDATOR_H_ #define QUICHE_QUIC_CORE_QUIC_PATH_VALIDATOR_H_ +#include <memory> #include <ostream> #include "absl/container/inlined_vector.h" @@ -143,6 +144,10 @@ QuicPathValidationContext* GetContext() const; + // Pass the ownership of path_validation context to the caller and reset the + // validator. + std::unique_ptr<QuicPathValidationContext> ReleaseContext(); + PathValidationReason GetPathValidationReason() const { return reason_; } // Send another PATH_CHALLENGE on the same path. After retrying
diff --git a/quiche/quic/core/quic_session.h b/quiche/quic/core/quic_session.h index c9931fe..e811ed4 100644 --- a/quiche/quic/core/quic_session.h +++ b/quiche/quic/core/quic_session.h
@@ -185,6 +185,8 @@ override { return nullptr; } + void MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> /*context*/) override {} void OnServerPreferredAddressAvailable( const QuicSocketAddress& /*server_preferred_address*/) override;
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h index 2eac913..f33fe49 100644 --- a/quiche/quic/test_tools/quic_test_utils.h +++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -505,6 +505,8 @@ MOCK_METHOD(bool, MaybeSendAddressToken, (), (override)); MOCK_METHOD(std::unique_ptr<QuicPathValidationContext>, CreateContextForMultiPortPath, (), (override)); + MOCK_METHOD(void, MigrateToMultiPortPath, + (std::unique_ptr<QuicPathValidationContext>), (override)); MOCK_METHOD(void, OnServerPreferredAddressAvailable, (const QuicSocketAddress&), (override)); void OnBandwidthUpdateTimeout() override {}
diff --git a/quiche/quic/test_tools/simulator/quic_endpoint.h b/quiche/quic/test_tools/simulator/quic_endpoint.h index 1bb9ce0..6be7bbb 100644 --- a/quiche/quic/test_tools/simulator/quic_endpoint.h +++ b/quiche/quic/test_tools/simulator/quic_endpoint.h
@@ -109,6 +109,8 @@ override { return nullptr; } + void MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> /*context*/) override {} void OnServerPreferredAddressAvailable( const QuicSocketAddress& /*server_preferred_address*/) override {}
diff --git a/quiche/quic/tools/quic_simple_client_session.cc b/quiche/quic/tools/quic_simple_client_session.cc index 26e368b..23601ad 100644 --- a/quiche/quic/tools/quic_simple_client_session.cc +++ b/quiche/quic/tools/quic_simple_client_session.cc
@@ -70,4 +70,13 @@ network_helper_->GetLatestClientAddress(), peer_address()); } +void QuicSimpleClientSession::MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> context) { + auto* path_migration_context = + static_cast<PathMigrationContext*>(context.get()); + MigratePath(path_migration_context->self_address(), + path_migration_context->peer_address(), + path_migration_context->ReleaseWriter(), /*owns_writer=*/true); +} + } // namespace quic
diff --git a/quiche/quic/tools/quic_simple_client_session.h b/quiche/quic/tools/quic_simple_client_session.h index 1beb062..1770c9f 100644 --- a/quiche/quic/tools/quic_simple_client_session.h +++ b/quiche/quic/tools/quic_simple_client_session.h
@@ -37,6 +37,8 @@ HttpDatagramSupport LocalHttpDatagramSupport() override; std::unique_ptr<QuicPathValidationContext> CreateContextForMultiPortPath() override; + void MigrateToMultiPortPath( + std::unique_ptr<QuicPathValidationContext> context) override; bool drop_response_body() const { return drop_response_body_; } private: