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,