On handshake confirmed, the client connection starts to validate received server preferred address. The connection migrates on path validation succeeds. Otherwise, the connection stays on default path.
This functionality is behind client_connection_option SPAD.
PiperOrigin-RevId: 497030952
diff --git a/quiche/quic/core/crypto/crypto_protocol.h b/quiche/quic/core/crypto/crypto_protocol.h
index e8320a1..08dcc42 100644
--- a/quiche/quic/core/crypto/crypto_protocol.h
+++ b/quiche/quic/core/crypto/crypto_protocol.h
@@ -284,6 +284,8 @@
const QuicTag kRVCM = TAG('R', 'V', 'C', 'M'); // Validate the new address
// upon client address change.
+const QuicTag kSPAD = TAG('S', 'P', 'A', 'D'); // Use server preferred address
+
// Optional support of truncated Connection IDs. If sent by a peer, the value
// is the minimum number of bytes allowed for the connection ID sent to the
// peer.
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 0ad3770..d9f9f6c 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -242,6 +242,58 @@
return false;
}
+// This context stores the path info from client address to server preferred
+// address while client is validating this address.
+class ServerPreferredAddressPathValidationContext
+ : public QuicPathValidationContext {
+ public:
+ ServerPreferredAddressPathValidationContext(
+ const QuicSocketAddress& server_preferred_address,
+ QuicConnection* connection)
+ : QuicPathValidationContext(connection->self_address(),
+ server_preferred_address),
+ connection_(connection) {}
+
+ QuicPacketWriter* WriterToUse() override { return connection_->writer(); }
+
+ private:
+ QuicConnection* connection_;
+};
+
+// Client migrates to server preferred address on path validation suceeds.
+// Otherwise, client cleans up alternative path.
+class ServerPreferredAddressResultDelegate
+ : public QuicPathValidator::ResultDelegate {
+ public:
+ explicit ServerPreferredAddressResultDelegate(QuicConnection* connection)
+ : connection_(connection) {}
+ void OnPathValidationSuccess(
+ std::unique_ptr<QuicPathValidationContext> context,
+ QuicTime /*start_time*/) override {
+ QUIC_DLOG(INFO) << "Server preferred address: " << context->peer_address()
+ << " validated. Migrating path, self_address: "
+ << context->self_address()
+ << ", peer_address: " << context->peer_address();
+ const bool success = connection_->MigratePath(context->self_address(),
+ context->peer_address(),
+ context->WriterToUse(),
+ /*owns_writer*/ false);
+ QUIC_BUG_IF(failed to migrate to server preferred address, !success)
+ << "Failed to migrate to server preferred address: "
+ << context->peer_address() << " after successful validation";
+ }
+
+ void OnPathValidationFailure(
+ std::unique_ptr<QuicPathValidationContext> context) override {
+ QUIC_DLOG(INFO) << "Failed to validate server preferred address : "
+ << context->peer_address();
+ connection_->OnPathValidationFailureAtClient(/*is_multi_port=*/false);
+ }
+
+ private:
+ QuicConnection* connection_;
+};
+
} // namespace
#define ENDPOINT \
@@ -658,6 +710,19 @@
connection_migration_use_new_cid_ =
validate_client_addresses_ &&
GetQuicReloadableFlag(quic_connection_migration_use_new_cid_v2);
+
+ if (connection_migration_use_new_cid_ &&
+ config.HasReceivedPreferredAddressConnectionIdAndToken() &&
+ config.HasClientRequestedIndependentOption(kSPAD, perspective_)) {
+ if (self_address().host().IsIPv4() &&
+ config.HasReceivedIPv4AlternateServerAddress()) {
+ server_preferred_address_ = config.ReceivedIPv4AlternateServerAddress();
+ } else if (self_address().host().IsIPv6() &&
+ config.HasReceivedIPv6AlternateServerAddress()) {
+ server_preferred_address_ = config.ReceivedIPv6AlternateServerAddress();
+ }
+ AddKnownServerAddress(server_preferred_address_);
+ }
if (config.HasReceivedMaxPacketSize()) {
peer_max_packet_size_ = config.ReceivedMaxPacketSize();
packet_creator_.SetMaxPacketLength(
@@ -1217,7 +1282,9 @@
if (!GetLargestReceivedPacket().IsInitialized() ||
header.packet_number > GetLargestReceivedPacket()) {
if (version().HasIetfQuicFrames()) {
- // Do not update server address.
+ // Client processes packets from any known server address. Client only
+ // updates peer address on initialization and/or to validated server
+ // preferred address.
} else {
// Update direct_peer_address_ and default path peer_address immediately
// for client connections.
@@ -2905,8 +2972,6 @@
last_received_packet_info_.source_address.IsInitialized() &&
direct_peer_address_ != last_received_packet_info_.source_address &&
!IsKnownServerAddress(last_received_packet_info_.source_address)) {
- // TODO(haoyuewang) Revisit this when preferred_address transport parameter
- // is used on the client side.
// Discard packets received from unseen server addresses.
return false;
}
@@ -3943,6 +4008,16 @@
// Re-arm ack alarm.
ack_alarm_->Update(uber_received_packet_manager_.GetEarliestAckTimeout(),
kAlarmGranularity);
+ if (server_preferred_address_.IsInitialized()) {
+ QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, perspective_);
+ // Validate received server preferred address.
+ auto context =
+ std::make_unique<ServerPreferredAddressPathValidationContext>(
+ server_preferred_address_, this);
+ auto result_delegate =
+ std::make_unique<ServerPreferredAddressResultDelegate>(this);
+ ValidatePath(std::move(context), std::move(result_delegate));
+ }
}
void QuicConnection::MaybeCreateMultiPortPath() {
@@ -5339,8 +5414,7 @@
last_received_packet_info_.source_address)) {
return connected_;
}
- if (perspective_ == Perspective::IS_SERVER &&
- type == PATH_CHALLENGE_FRAME &&
+ if (type == PATH_CHALLENGE_FRAME &&
!IsAlternativePath(last_received_packet_info_.destination_address,
current_effective_peer_address)) {
QUIC_DVLOG(1)
@@ -6694,7 +6768,10 @@
peer_address_change_type == NO_CHANGE);
SetSelfAddress(self_address);
UpdatePeerAddress(peer_address);
- SetQuicPacketWriter(writer, owns_writer);
+ default_path_.peer_address = peer_address;
+ if (writer_ != writer) {
+ SetQuicPacketWriter(writer, owns_writer);
+ }
MaybeClearQueuedPacketsOnPathChange();
OnSuccessfulMigration(is_port_change);
return true;
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index 02a218b..a3eddff 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -2270,6 +2270,10 @@
// Server addresses that are known to the client.
std::vector<QuicSocketAddress> known_server_addresses_;
+ // Stores received server preferred address in transport param. Client side
+ // only.
+ QuicSocketAddress server_preferred_address_;
+
// If true, throttle sending if next created packet will exceed amplification
// limit.
const bool enforce_strict_amplification_factor_ =
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index 94aa39e..ebc8891 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -1462,6 +1462,46 @@
}
}
+ // Receive server preferred address.
+ void ServerPreferredAddressInit() {
+ ASSERT_EQ(Perspective::IS_CLIENT, connection_.perspective());
+ ASSERT_TRUE(version().HasIetfQuicFrames());
+ ASSERT_TRUE(connection_.self_address().host().IsIPv6());
+ SetQuicReloadableFlag(quic_connection_migration_use_new_cid_v2, true);
+ QuicIpAddress host;
+ host.FromString("2604:31c0::");
+ QuicSocketAddress server_preferred_address(host, 443);
+ const QuicConnectionId connection_id = TestConnectionId(17);
+ const StatelessResetToken reset_token =
+ QuicUtils::GenerateStatelessResetToken(connection_id);
+
+ connection_.CreateConnectionIdManager();
+
+ connection_.SendCryptoStreamData();
+ EXPECT_CALL(*loss_algorithm_, DetectLosses(_, _, _, _, _, _));
+ EXPECT_CALL(*send_algorithm_, OnCongestionEvent(true, _, _, _, _));
+ QuicAckFrame frame = InitAckFrame(1);
+ // Received ACK for packet 1.
+ ProcessFramePacketAtLevel(1, QuicFrame(&frame), ENCRYPTION_INITIAL);
+
+ QuicConfig config;
+ config.SetConnectionOptionsToSend(QuicTagVector{kRVCM});
+ config.SetClientConnectionOptions(QuicTagVector{kSPAD});
+ QuicConfigPeer::SetReceivedStatelessResetToken(&config,
+ kTestStatelessResetToken);
+ QuicConfigPeer::SetReceivedAlternateServerAddress(&config,
+ server_preferred_address);
+ QuicConfigPeer::SetPreferredAddressConnectionIdAndToken(
+ &config, connection_id, reset_token);
+ EXPECT_CALL(*send_algorithm_, SetFromConfig(_, _));
+ connection_.SetFromConfig(config);
+
+ ASSERT_TRUE(QuicConnectionPeer::GetServerPreferredAddress(&connection_)
+ .IsInitialized());
+ EXPECT_EQ(server_preferred_address,
+ QuicConnectionPeer::GetServerPreferredAddress(&connection_));
+ }
+
void TestClientRetryHandling(bool invalid_retry_tag,
bool missing_original_id_in_config,
bool wrong_original_id_in_config,
@@ -15939,6 +15979,165 @@
EXPECT_EQ(connection_.connection_id(), connection_id_);
}
+TEST_P(QuicConnectionTest, ClientValidatedServerPreferredAddress) {
+ // Test the scenario where the client validates server preferred address by
+ // receiving PATH_RESPONSE from server preferred address.
+ if (!connection_.version().HasIetfQuicFrames()) {
+ return;
+ }
+ ServerPreferredAddressInit();
+ const QuicSocketAddress kServerPreferredAddress =
+ QuicConnectionPeer::GetServerPreferredAddress(&connection_);
+ const StatelessResetToken kNewStatelessResetToken =
+ QuicUtils::GenerateStatelessResetToken(TestConnectionId(17));
+ connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+ EXPECT_CALL(visitor_, GetHandshakeState())
+ .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+ // Kick off path validation of server preferred address on handshake
+ // confirmed.
+ connection_.OnHandshakeComplete();
+ EXPECT_TRUE(connection_.HasPendingPathValidation());
+ EXPECT_EQ(TestConnectionId(17),
+ writer_->last_packet_header().destination_connection_id);
+ EXPECT_EQ(kServerPreferredAddress, writer_->last_write_peer_address());
+
+ ASSERT_FALSE(writer_->path_challenge_frames().empty());
+ QuicPathFrameBuffer payload =
+ writer_->path_challenge_frames().front().data_buffer;
+ // Send data packet while path validation is pending.
+ connection_.SendStreamDataWithString(3, "foo", 0, NO_FIN);
+ // While path validation is pending, packet is sent on default path.
+ EXPECT_EQ(TestConnectionId(),
+ writer_->last_packet_header().destination_connection_id);
+ EXPECT_EQ(kPeerAddress, writer_->last_write_peer_address());
+ EXPECT_TRUE(connection_.IsValidStatelessResetToken(kTestStatelessResetToken));
+ EXPECT_FALSE(connection_.IsValidStatelessResetToken(kNewStatelessResetToken));
+
+ // Receive path challenge from server preferred address.
+ QuicFrames frames;
+ frames.push_back(QuicFrame(QuicPathResponseFrame(99, payload)));
+ // Verify send_algorithm gets reset after migration (new sent packet is not
+ // updated to exsting send_algorithm_).
+ EXPECT_CALL(*send_algorithm_, OnPacketSent(_, _, _, _, _)).Times(0);
+ ProcessFramesPacketWithAddresses(
+ frames, kSelfAddress, kServerPreferredAddress, ENCRYPTION_FORWARD_SECURE);
+ ASSERT_FALSE(connection_.HasPendingPathValidation());
+ // Verify stream data is retransmitted on new path.
+ EXPECT_EQ(TestConnectionId(17),
+ writer_->last_packet_header().destination_connection_id);
+ EXPECT_EQ(kServerPreferredAddress, writer_->last_write_peer_address());
+ // Verify stateless reset token gets changed.
+ EXPECT_FALSE(
+ connection_.IsValidStatelessResetToken(kTestStatelessResetToken));
+ EXPECT_TRUE(connection_.IsValidStatelessResetToken(kNewStatelessResetToken));
+
+ auto* retire_peer_issued_cid_alarm =
+ connection_.GetRetirePeerIssuedConnectionIdAlarm();
+ ASSERT_TRUE(retire_peer_issued_cid_alarm->IsSet());
+ // Verify client retires connection ID with sequence number 0.
+ EXPECT_CALL(visitor_, SendRetireConnectionId(/*sequence_number=*/0u));
+ retire_peer_issued_cid_alarm->Fire();
+}
+
+TEST_P(QuicConnectionTest, ClientValidatedServerPreferredAddress2) {
+ // Test the scenario where the client validates server preferred address by
+ // receiving PATH_RESPONSE from original server address.
+ if (!connection_.version().HasIetfQuicFrames()) {
+ return;
+ }
+ ServerPreferredAddressInit();
+ const QuicSocketAddress kServerPreferredAddress =
+ QuicConnectionPeer::GetServerPreferredAddress(&connection_);
+ connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+ EXPECT_CALL(visitor_, GetHandshakeState())
+ .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+ // Kick off path validation of server preferred address on handshake
+ // confirmed.
+ connection_.OnHandshakeComplete();
+ EXPECT_TRUE(connection_.HasPendingPathValidation());
+ ASSERT_FALSE(writer_->path_challenge_frames().empty());
+ QuicPathFrameBuffer payload =
+ writer_->path_challenge_frames().front().data_buffer;
+ // Send data packet while path validation is pending.
+ connection_.SendStreamDataWithString(3, "foo", 0, NO_FIN);
+ EXPECT_EQ(TestConnectionId(),
+ writer_->last_packet_header().destination_connection_id);
+ EXPECT_EQ(kPeerAddress, writer_->last_write_peer_address());
+
+ // Receive path challenge from original server address.
+ QuicFrames frames;
+ frames.push_back(QuicFrame(QuicPathResponseFrame(99, payload)));
+ ProcessFramesPacketWithAddresses(frames, kSelfAddress, kPeerAddress,
+ ENCRYPTION_FORWARD_SECURE);
+ ASSERT_FALSE(connection_.HasPendingPathValidation());
+ // Verify stream data is retransmitted on new path.
+ EXPECT_EQ(TestConnectionId(17),
+ writer_->last_packet_header().destination_connection_id);
+ EXPECT_EQ(kServerPreferredAddress, writer_->last_write_peer_address());
+
+ auto* retire_peer_issued_cid_alarm =
+ connection_.GetRetirePeerIssuedConnectionIdAlarm();
+ ASSERT_TRUE(retire_peer_issued_cid_alarm->IsSet());
+ // Verify client retires connection ID with sequence number 0.
+ EXPECT_CALL(visitor_, SendRetireConnectionId(/*sequence_number=*/0u));
+ retire_peer_issued_cid_alarm->Fire();
+
+ // Verify another packet from original server address gets processed.
+ EXPECT_CALL(visitor_, OnStreamFrame(_)).Times(1);
+ frames.clear();
+ frames.push_back(QuicFrame(frame1_));
+ ProcessFramesPacketWithAddresses(frames, kSelfAddress, kPeerAddress,
+ ENCRYPTION_FORWARD_SECURE);
+}
+
+TEST_P(QuicConnectionTest, ClientFailedToValidateServerPreferredAddress) {
+ // Test the scenario where the client fails to validate server preferred
+ // address.
+ if (!connection_.version().HasIetfQuicFrames()) {
+ return;
+ }
+ ServerPreferredAddressInit();
+ connection_.SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
+ EXPECT_CALL(visitor_, GetHandshakeState())
+ .WillRepeatedly(Return(HANDSHAKE_CONFIRMED));
+ // Kick off path validation of server preferred address on handshake
+ // confirmed.
+ connection_.OnHandshakeComplete();
+ EXPECT_TRUE(connection_.HasPendingPathValidation());
+ ASSERT_FALSE(writer_->path_challenge_frames().empty());
+
+ // Receive mismatched path challenge from original server address.
+ QuicFrames frames;
+ frames.push_back(
+ QuicFrame(QuicPathResponseFrame(99, {0, 1, 2, 3, 4, 5, 6, 7})));
+ ProcessFramesPacketWithAddresses(frames, kSelfAddress, kPeerAddress,
+ ENCRYPTION_FORWARD_SECURE);
+ ASSERT_TRUE(connection_.HasPendingPathValidation());
+
+ // Simluate path validation times out.
+ 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());
+ // Verify stream data is sent on the default path.
+ connection_.SendStreamDataWithString(3, "foo", 0, NO_FIN);
+ EXPECT_EQ(TestConnectionId(),
+ writer_->last_packet_header().destination_connection_id);
+ EXPECT_EQ(kPeerAddress, writer_->last_write_peer_address());
+
+ auto* retire_peer_issued_cid_alarm =
+ connection_.GetRetirePeerIssuedConnectionIdAlarm();
+ ASSERT_TRUE(retire_peer_issued_cid_alarm->IsSet());
+ // Verify client retires connection ID with sequence number 1.
+ EXPECT_CALL(visitor_, SendRetireConnectionId(/*sequence_number=*/1u));
+ retire_peer_issued_cid_alarm->Fire();
+ EXPECT_TRUE(connection_.IsValidStatelessResetToken(kTestStatelessResetToken));
+}
+
} // namespace
} // namespace test
} // namespace quic