[QUIC] Do not send multi-port probing when there's no active stream. Limit the max number of multi-port path creations to 5.
PiperOrigin-RevId: 492305120
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index d9bed33..b889402 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -5423,13 +5423,15 @@
}
TEST_P(EndToEndTest, ClientMultiPortConnection) {
- client_extra_copts_.push_back(kMPQC);
+ 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));
@@ -5463,6 +5465,7 @@
}));
// Verify that the previous path was retired.
EXPECT_EQ(1u, client_connection->GetStats().num_retire_connection_id_sent);
+ stream->Reset(QuicRstStreamErrorCode::QUIC_STREAM_NO_ERROR);
}
TEST_P(EndToEndPacketReorderingTest, ReorderedPathChallenge) {
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 93cef1d..85f91a5 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -176,7 +176,7 @@
void OnAlarm() override {
QUICHE_DCHECK(connection_->connected());
QUIC_DLOG(INFO) << "Alternative path probing alarm fired";
- connection_->ProbeMultiPortPath();
+ connection_->MaybeProbeMultiPortPath();
}
};
@@ -673,10 +673,9 @@
UpdateReleaseTimeIntoFuture();
}
- multi_port_enabled_ =
+ if (perspective_ == Perspective::IS_CLIENT &&
connection_migration_use_new_cid_ &&
- config.HasClientSentConnectionOption(kMPQC, perspective_);
- if (multi_port_enabled_) {
+ config.HasClientRequestedIndependentOption(kMPQC, perspective_)) {
multi_port_stats_ = std::make_unique<MultiPortStats>();
}
}
@@ -1994,7 +1993,7 @@
if (!OnNewConnectionIdFrameInner(frame)) {
return false;
}
- if (perspective_ == Perspective::IS_CLIENT && multi_port_enabled_) {
+ if (multi_port_stats_ != nullptr) {
MaybeCreateMultiPortPath();
}
return true;
@@ -3929,14 +3928,21 @@
void QuicConnection::MaybeCreateMultiPortPath() {
QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, perspective_);
+ QUIC_BUG_IF(quic_bug_12714_20, path_validator_.HasPendingPathValidation())
+ << "Pending validation exists when multi-port path is created.";
+ if (multi_port_stats_->num_multi_port_paths_created >=
+ kMaxNumMultiPortPaths) {
+ return;
+ }
auto path_context = visitor_->CreateContextForMultiPortPath();
- if (!path_context || path_validator_.HasPendingPathValidation()) {
+ if (!path_context) {
return;
}
auto multi_port_validation_result_delegate =
std::make_unique<MultiPortPathValidationResultDelegate>(this);
multi_port_probing_alarm_->Cancel();
multi_port_path_context_ = nullptr;
+ multi_port_stats_->num_multi_port_paths_created++;
ValidatePath(std::move(path_context),
std::move(multi_port_validation_result_delegate));
}
@@ -4495,6 +4501,7 @@
process_undecryptable_packets_alarm_->PermanentCancel();
discard_previous_one_rtt_keys_alarm_->PermanentCancel();
discard_zero_rtt_decryption_keys_alarm_->PermanentCancel();
+ multi_port_probing_alarm_->PermanentCancel();
blackhole_detector_.StopDetection(/*permanent=*/true);
idle_network_detector_.StopDetection();
}
@@ -6889,13 +6896,15 @@
}
}
-void QuicConnection::ProbeMultiPortPath() {
+void QuicConnection::MaybeProbeMultiPortPath() {
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()) {
+ multi_port_path_context_->peer_address() ||
+ !visitor_->ShouldKeepConnectionAlive() ||
+ multi_port_probing_alarm_->IsSet()) {
return;
}
auto multi_port_validation_result_delegate =
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index 63d4723..2efd08d 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -493,6 +493,8 @@
size_t num_multi_port_probe_failures_when_path_not_degrading = 0;
// number of multi-port probe failure when path is degrading
size_t num_multi_port_probe_failures_when_path_degrading = 0;
+ // number of total multi-port path creations in a connection
+ size_t num_multi_port_paths_created = 0;
};
// Sets connection parameters from the supplied |config|.
@@ -762,7 +764,7 @@
// Probe the existing alternative path. Does not create a new alternative
// path. This method is the callback for |multi_port_probing_alarm_|.
- void ProbeMultiPortPath();
+ virtual void MaybeProbeMultiPortPath();
// Accessors
void set_visitor(QuicConnectionVisitorInterface* visitor) {
@@ -805,7 +807,6 @@
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_; }
@@ -2253,8 +2254,6 @@
std::unique_ptr<QuicPathValidationContext> multi_port_path_context_;
- bool multi_port_enabled_ = false;
-
QuicTime::Delta multi_port_probing_interval_;
std::unique_ptr<MultiPortStats> multi_port_stats_;
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index fdb0a0d..100dd22 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -13008,10 +13008,11 @@
&connection_, kNewSelfAddress, connection_.peer_address()));
}
-TEST_P(QuicConnectionTest, MultiPortCreation) {
+TEST_P(QuicConnectionTest, MultiPortConnection) {
set_perspective(Perspective::IS_CLIENT);
QuicConfig config;
- config.SetConnectionOptionsToSend(QuicTagVector{kMPQC, kRVCM});
+ config.SetConnectionOptionsToSend(QuicTagVector{kRVCM});
+ config.SetClientConnectionOptions(QuicTagVector{kMPQC});
EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
connection_.SetFromConfig(config);
if (!connection_.connection_migration_use_new_cid()) {
@@ -13030,6 +13031,7 @@
EXPECT_NE(kNewSelfAddress, self_address);
TestPacketWriter new_writer(version(), &clock_, Perspective::IS_CLIENT);
+ EXPECT_CALL(visitor_, ShouldKeepConnectionAlive()).WillOnce(Return(false));
QuicNewConnectionIdFrame frame;
frame.connection_id = TestConnectionId(1234);
ASSERT_NE(frame.connection_id, connection_.connection_id());
@@ -13055,7 +13057,7 @@
QuicFrames frames;
frames.push_back(QuicFrame(QuicPathResponseFrame(
- 99, new_writer.path_challenge_frames().front().data_buffer)));
+ 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.
@@ -13071,6 +13073,46 @@
EXPECT_EQ(kTestRTT,
stats->rtt_stats_when_default_path_degrading.latest_rtt());
+ // When there's no active request, the probing shouldn't happen. But the
+ // probing context should be saved.
+ EXPECT_CALL(visitor_, ShouldKeepConnectionAlive()).WillOnce(Return(false));
+ connection_.GetMultiPortProbingAlarm()->Fire();
+ EXPECT_FALSE(connection_.HasPendingPathValidation());
+ EXPECT_FALSE(connection_.GetMultiPortProbingAlarm()->IsSet());
+
+ // Simulate the situation where a new request stream is created.
+ EXPECT_CALL(visitor_, ShouldKeepConnectionAlive())
+ .WillRepeatedly(Return(true));
+ random_generator_.ChangeValue();
+ connection_.MaybeProbeMultiPortPath();
+ EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath(
+ &connection_, kNewSelfAddress, connection_.peer_address()));
+ EXPECT_TRUE(alt_path->validated);
+ // Fake a response delay.
+ clock_.AdvanceTime(kTestRTT);
+ QuicFrames frames2;
+ frames2.push_back(QuicFrame(QuicPathResponseFrame(
+ 99, new_writer.path_challenge_frames().back().data_buffer)));
+ ProcessFramesPacketWithAddresses(frames2, 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);
+ EXPECT_EQ(1, stats->num_path_degrading);
+ EXPECT_EQ(0, stats->num_multi_port_probe_failures_when_path_degrading);
+ EXPECT_EQ(kTestRTT, stats->rtt_stats.latest_rtt());
+ EXPECT_EQ(kTestRTT,
+ stats->rtt_stats_when_default_path_degrading.latest_rtt());
+
+ EXPECT_TRUE(connection_.GetMultiPortProbingAlarm()->IsSet());
+ // Since there's already a scheduled probing alarm, manual calls won't have
+ // any effect.
+ connection_.MaybeProbeMultiPortPath();
+ EXPECT_FALSE(connection_.HasPendingPathValidation());
+
+ // Simulate the case where the path validation fails after retries.
connection_.GetMultiPortProbingAlarm()->Fire();
EXPECT_TRUE(connection_.HasPendingPathValidation());
EXPECT_TRUE(QuicConnectionPeer::IsAlternativePath(
@@ -13091,6 +13133,116 @@
EXPECT_EQ(0, stats->num_multi_port_probe_failures_when_path_not_degrading);
}
+TEST_P(QuicConnectionTest, TooManyMultiPortPathCreations) {
+ 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();
+
+ EXPECT_CALL(visitor_, OnPathDegrading());
+ connection_.OnPathDegradingDetected();
+
+ 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);
+
+ 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();
+ }
+
+ auto stats = connection_.multi_port_stats();
+ EXPECT_FALSE(connection_.HasPendingPathValidation());
+ EXPECT_FALSE(QuicConnectionPeer::IsAlternativePath(
+ &connection_, kNewSelfAddress, connection_.peer_address()));
+ EXPECT_EQ(1, stats->num_path_degrading);
+ EXPECT_EQ(1, stats->num_multi_port_probe_failures_when_path_degrading);
+
+ uint64_t connection_id = 1235;
+ for (size_t i = 0; i < kMaxNumMultiPortPaths - 1; ++i) {
+ QuicNewConnectionIdFrame frame;
+ frame.connection_id = TestConnectionId(connection_id + i);
+ 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 = i + 2;
+ 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()));
+ EXPECT_FALSE(alt_path->validated);
+
+ for (size_t j = 0; j < QuicPathValidator::kMaxRetryTimes + 1; ++j) {
+ 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()));
+ EXPECT_EQ(1, stats->num_path_degrading);
+ EXPECT_EQ(i + 2, stats->num_multi_port_probe_failures_when_path_degrading);
+ }
+
+ // The 6th attemp should fail.
+ QuicNewConnectionIdFrame frame2;
+ frame2.connection_id = TestConnectionId(1239);
+ ASSERT_NE(frame2.connection_id, connection_.connection_id());
+ frame2.stateless_reset_token =
+ QuicUtils::GenerateStatelessResetToken(frame2.connection_id);
+ frame2.retire_prior_to = 0u;
+ frame2.sequence_number = 6u;
+ EXPECT_TRUE(connection_.OnNewConnectionIdFrame(frame2));
+ EXPECT_FALSE(connection_.HasPendingPathValidation());
+ EXPECT_EQ(kMaxNumMultiPortPaths,
+ stats->num_multi_port_probe_failures_when_path_degrading);
+}
+
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 13fa288..4589189 100644
--- a/quiche/quic/core/quic_constants.h
+++ b/quiche/quic/core/quic_constants.h
@@ -324,6 +324,8 @@
inline constexpr QuicTime::Delta kDefaultMultiPortProbingInterval =
QuicTime::Delta::FromSeconds(3);
+inline constexpr size_t kMaxNumMultiPortPaths = 5;
+
} // namespace quic
#endif // QUICHE_QUIC_CORE_QUIC_CONSTANTS_H_
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc
index 242b9fc..158db55 100644
--- a/quiche/quic/core/quic_session.cc
+++ b/quiche/quic/core/quic_session.cc
@@ -1816,6 +1816,7 @@
}
void QuicSession::ActivateStream(std::unique_ptr<QuicStream> stream) {
+ const bool should_keep_alive = ShouldKeepConnectionAlive();
QuicStreamId stream_id = stream->id();
bool is_static = stream->is_static();
QUIC_DVLOG(1) << ENDPOINT << "num_streams: " << stream_map_.size()
@@ -1831,6 +1832,11 @@
stream_id_manager_.ActivateStream(
/*is_incoming=*/IsIncomingStream(stream_id));
}
+ if (perspective() == Perspective::IS_CLIENT &&
+ connection()->multi_port_stats() != nullptr && !should_keep_alive &&
+ ShouldKeepConnectionAlive()) {
+ connection()->MaybeProbeMultiPortPath();
+ }
}
QuicStreamId QuicSession::GetNextOutgoingBidirectionalStreamId() {
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index a55035f..d663cbd 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -2086,6 +2086,23 @@
&session_, GetNthClientInitiatedBidirectionalId(1)));
}
+TEST_P(QuicSessionTestClient, NewStreamCreationResumesMultiPortProbing) {
+ session_.config()->SetConnectionOptionsToSend({kRVCM});
+ session_.config()->SetClientConnectionOptions({kMPQC});
+ session_.Initialize();
+ connection_->CreateConnectionIdManager();
+ connection_->SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+ connection_->OnHandshakeComplete();
+ session_.OnConfigNegotiated();
+
+ if (!connection_->connection_migration_use_new_cid()) {
+ return;
+ }
+
+ EXPECT_CALL(*connection_, MaybeProbeMultiPortPath());
+ session_.CreateOutgoingBidirectionalStream();
+}
+
TEST_P(QuicSessionTestClient, InvalidSessionFlowControlWindowInHandshake) {
// Test that receipt of an invalid (< default for gQUIC, < current for TLS)
// session flow control window from the peer results in the connection being
diff --git a/quiche/quic/core/quic_stream_test.cc b/quiche/quic/core/quic_stream_test.cc
index 1e77c5a..a0837be 100644
--- a/quiche/quic/core/quic_stream_test.cc
+++ b/quiche/quic/core/quic_stream_test.cc
@@ -113,6 +113,8 @@
stream_ = new StrictMock<TestStream>(kTestStreamId, session_.get(),
BIDIRECTIONAL);
EXPECT_NE(nullptr, stream_);
+ EXPECT_CALL(*session_, ShouldKeepConnectionAlive())
+ .WillRepeatedly(Return(true));
// session_ now owns stream_.
session_->ActivateStream(absl::WrapUnique(stream_));
// Ignore resetting when session_ is terminated.
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index 4c8c9b0..b2d8f08 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -636,6 +636,7 @@
MOCK_METHOD(bool, SendConnectivityProbingPacket,
(QuicPacketWriter*, const QuicSocketAddress& peer_address),
(override));
+ MOCK_METHOD(void, MaybeProbeMultiPortPath, (), (override));
MOCK_METHOD(void, OnSendConnectionState, (const CachedNetworkParameters&),
(override));
diff --git a/quiche/quic/tools/quic_simple_client_session.cc b/quiche/quic/tools/quic_simple_client_session.cc
index 013167c..2350052 100644
--- a/quiche/quic/tools/quic_simple_client_session.cc
+++ b/quiche/quic/tools/quic_simple_client_session.cc
@@ -38,7 +38,7 @@
std::unique_ptr<QuicPathValidationContext>
QuicSimpleClientSession::CreateContextForMultiPortPath() {
- if (!network_helper_ || !connection()->multi_port_enabled()) {
+ if (!network_helper_ || connection()->multi_port_stats() == nullptr) {
return nullptr;
}
auto self_address = connection()->self_address();