Replace QUIC CID when the first INITIAL packet of a connection is enqueued into the QuicBufferedPacketStore.

This address a review comment of cl/641294174.

Protected by FLAGS_quic_restart_flag_quic_dispatcher_replace_cid_on_first_packet.

PiperOrigin-RevId: 658154062
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index bb0a6da..0e4689b 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -51,6 +51,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_false, false, false, "A testonly reloadable flag that will always default to false.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_testonly_default_true, true, true, "A testonly reloadable flag that will always default to true.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_use_received_client_addresses_cache, true, true, "If true, use a LRU cache to record client addresses of packets received on server's original address.")
+QUICHE_FLAG(bool, quiche_restart_flag_quic_dispatcher_replace_cid_on_first_packet, false, false, "If true, QuicDispatcher will always generate the replaced connection ID when the first INITIAL packet is received, even if it's buffered in the packet store.")
 QUICHE_FLAG(bool, quiche_restart_flag_quic_support_ect1, false, false, "When true, allows sending of QUIC packets marked ECT(1). A different flag (TBD) will actually utilize this capability to send ECT(1).")
 QUICHE_FLAG(bool, quiche_restart_flag_quic_support_release_time_for_gso, false, false, "If true, QuicGsoBatchWriter will support release time if it is available and the process has the permission to do so.")
 QUICHE_FLAG(bool, quiche_restart_flag_quic_testonly_default_false, false, false, "A testonly restart flag that will always default to false.")
diff --git a/quiche/quic/core/quic_buffered_packet_store.cc b/quiche/quic/core/quic_buffered_packet_store.cc
index e2fb225..9d525b2 100644
--- a/quiche/quic/core/quic_buffered_packet_store.cc
+++ b/quiche/quic/core/quic_buffered_packet_store.cc
@@ -26,9 +26,11 @@
 #include "quiche/quic/core/quic_types.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_socket_address.h"
 #include "quiche/common/platform/api/quiche_logging.h"
+#include "quiche/common/print_elements.h"
 
 namespace quic {
 
@@ -63,10 +65,12 @@
 
 BufferedPacket::BufferedPacket(std::unique_ptr<QuicReceivedPacket> packet,
                                QuicSocketAddress self_address,
-                               QuicSocketAddress peer_address)
+                               QuicSocketAddress peer_address,
+                               bool is_ietf_initial_packet)
     : packet(std::move(packet)),
       self_address(self_address),
-      peer_address(peer_address) {}
+      peer_address(peer_address),
+      is_ietf_initial_packet(is_ietf_initial_packet) {}
 
 BufferedPacket::BufferedPacket(BufferedPacket&& other) = default;
 
@@ -105,7 +109,7 @@
 EnqueuePacketResult QuicBufferedPacketStore::EnqueuePacket(
     const ReceivedPacketInfo& packet_info,
     std::optional<ParsedClientHello> parsed_chlo,
-    ConnectionIdGeneratorInterface* connection_id_generator) {
+    ConnectionIdGeneratorInterface& connection_id_generator) {
   QuicConnectionId connection_id = packet_info.destination_connection_id;
   const QuicReceivedPacket& packet = packet_info.packet;
   const QuicSocketAddress& self_address = packet_info.self_address;
@@ -113,6 +117,9 @@
   const ParsedQuicVersion& version = packet_info.version;
   const bool ietf_quic = packet_info.form != GOOGLE_QUIC_PACKET;
   const bool is_chlo = parsed_chlo.has_value();
+  const bool is_ietf_initial_packet =
+      (version.IsKnown() && packet_info.form == IETF_QUIC_LONG_HEADER_PACKET &&
+       packet_info.long_packet_type == INITIAL);
   QUIC_BUG_IF(quic_bug_12410_1, !GetQuicFlag(quic_allow_chlo_buffering))
       << "Shouldn't buffer packets if disabled via flag.";
   QUIC_BUG_IF(quic_bug_12410_2,
@@ -121,52 +128,104 @@
   QUIC_BUG_IF(quic_bug_12410_4, is_chlo && !version.IsKnown())
       << "Should have version for CHLO packet.";
 
-  const bool is_first_packet = !undecryptable_packets_.contains(connection_id);
-  if (is_first_packet) {
-    if (ShouldNotBufferPacket(is_chlo)) {
-      // Drop the packet if the upper limit of undecryptable packets has been
-      // reached or the whole capacity of the store has been reached.
-      return TOO_MANY_CONNECTIONS;
-    }
-    undecryptable_packets_.emplace(
-        std::make_pair(connection_id, BufferedPacketList()));
-    undecryptable_packets_.back().second.ietf_quic = ietf_quic;
-    undecryptable_packets_.back().second.version = version;
-  }
-  QUICHE_CHECK(undecryptable_packets_.contains(connection_id));
-  BufferedPacketList& queue =
-      undecryptable_packets_.find(connection_id)->second;
+  bool is_first_packet;
+  BufferedPacketListNode* node = nullptr;
 
-  if (!is_chlo) {
-    // If current packet is not CHLO, it might not be buffered because store
-    // only buffers certain number of undecryptable packets per connection.
-    size_t num_non_chlo_packets = connections_with_chlo_.contains(connection_id)
-                                      ? (queue.buffered_packets.size() - 1)
-                                      : queue.buffered_packets.size();
-    if (num_non_chlo_packets >= kDefaultMaxUndecryptablePackets) {
+  if (replace_cid_on_first_packet_) {
+    QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 1,
+                              13);
+    auto iter = buffered_session_map_.find(connection_id);
+    is_first_packet = (iter == buffered_session_map_.end());
+    if (is_first_packet) {
+      if (ShouldNotBufferPacket(is_chlo)) {
+        // Drop the packet if the upper limit of undecryptable packets has been
+        // reached or the whole capacity of the store has been reached.
+        return TOO_MANY_CONNECTIONS;
+      }
+      iter = buffered_session_map_.emplace_hint(
+          iter, connection_id, std::make_shared<BufferedPacketListNode>());
+      iter->second->ietf_quic = ietf_quic;
+      iter->second->version = version;
+      iter->second->original_connection_id = connection_id;
+      iter->second->creation_time = clock_->ApproximateNow();
+      buffered_sessions_.push_back(iter->second.get());
+      ++num_buffered_sessions_;
+    }
+    node = iter->second.get();
+    QUICHE_DCHECK(buffered_session_map_.contains(connection_id));
+  } else {
+    is_first_packet = !undecryptable_packets_.contains(connection_id);
+    if (is_first_packet) {
+      if (ShouldNotBufferPacket(is_chlo)) {
+        // Drop the packet if the upper limit of undecryptable packets has been
+        // reached or the whole capacity of the store has been reached.
+        return TOO_MANY_CONNECTIONS;
+      }
+      undecryptable_packets_.emplace(
+          std::make_pair(connection_id, BufferedPacketList()));
+      undecryptable_packets_.back().second.ietf_quic = ietf_quic;
+      undecryptable_packets_.back().second.version = version;
+    }
+    QUICHE_DCHECK(undecryptable_packets_.contains(connection_id));
+  }
+
+  BufferedPacketList& queue =
+      replace_cid_on_first_packet_
+          ? *node
+          : undecryptable_packets_.find(connection_id)->second;
+
+  if (replace_cid_on_first_packet_) {
+    // TODO(wub): Rename kDefaultMaxUndecryptablePackets when deprecating
+    //  --quic_dispatcher_replace_cid_on_first_packet.
+    if (!is_chlo &&
+        queue.buffered_packets.size() >= kDefaultMaxUndecryptablePackets) {
       // If there are kMaxBufferedPacketsPerConnection packets buffered up for
       // this connection, drop the current packet.
       return TOO_MANY_PACKETS;
     }
-  }
+  } else {
+    if (!is_chlo) {
+      // If current packet is not CHLO, it might not be buffered because store
+      // only buffers certain number of undecryptable packets per connection.
+      size_t num_non_chlo_packets =
+          connections_with_chlo_.contains(connection_id)
+              ? (queue.buffered_packets.size() - 1)
+              : queue.buffered_packets.size();
+      if (num_non_chlo_packets >= kDefaultMaxUndecryptablePackets) {
+        // If there are kMaxBufferedPacketsPerConnection packets buffered up for
+        // this connection, drop the current packet.
+        return TOO_MANY_PACKETS;
+      }
+    }
 
-  if (queue.buffered_packets.empty()) {
-    // If this is the first packet arrived on a new connection, initialize the
-    // creation time.
-    queue.creation_time = clock_->ApproximateNow();
+    if (queue.buffered_packets.empty()) {
+      // If this is the first packet arrived on a new connection, initialize the
+      // creation time.
+      queue.creation_time = clock_->ApproximateNow();
+    }
   }
 
   BufferedPacket new_entry(std::unique_ptr<QuicReceivedPacket>(packet.Clone()),
-                           self_address, peer_address);
+                           self_address, peer_address, is_ietf_initial_packet);
   if (is_chlo) {
     // Add CHLO to the beginning of buffered packets so that it can be delivered
     // first later.
     queue.buffered_packets.push_front(std::move(new_entry));
     queue.parsed_chlo = std::move(parsed_chlo);
-    connections_with_chlo_[connection_id] = false;  // Dummy value.
     // Set the version of buffered packets of this connection on CHLO.
     queue.version = version;
-    queue.connection_id_generator = connection_id_generator;
+    if (replace_cid_on_first_packet_) {
+      if (!buffered_sessions_with_chlo_.is_linked(node)) {
+        buffered_sessions_with_chlo_.push_back(node);
+        ++num_buffered_sessions_with_chlo_;
+      } else {
+        QUIC_BUG(quic_store_session_already_has_chlo)
+            << "Buffered session already has CHLO";
+      }
+    } else {
+      queue.connection_id_generator = &connection_id_generator;
+      connections_with_chlo_[connection_id] = false;  // Dummy value.
+    }
   } else {
     // Buffer non-CHLO packets in arrival order.
     queue.buffered_packets.push_back(std::move(new_entry));
@@ -184,86 +243,232 @@
   }
 
   MaybeSetExpirationAlarm();
+
+  if (replace_cid_on_first_packet_ && is_ietf_initial_packet &&
+      version.UsesTls() && !queue.HasAttemptedToReplaceConnectionId()) {
+    QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 2,
+                              13);
+    queue.SetAttemptedToReplaceConnectionId(&connection_id_generator);
+    std::optional<QuicConnectionId> replaced_connection_id =
+        connection_id_generator.MaybeReplaceConnectionId(connection_id,
+                                                         packet_info.version);
+    // Normalize the output of MaybeReplaceConnectionId.
+    if (replaced_connection_id.has_value() &&
+        (replaced_connection_id->IsEmpty() ||
+         *replaced_connection_id == connection_id)) {
+      QUIC_CODE_COUNT(quic_store_replaced_cid_is_empty_or_same_as_original);
+      replaced_connection_id.reset();
+    }
+    QUIC_DVLOG(1) << "MaybeReplaceConnectionId(" << connection_id << ") = "
+                  << (replaced_connection_id.has_value()
+                          ? replaced_connection_id->ToString()
+                          : "nullopt");
+    if (replaced_connection_id.has_value()) {
+      switch (visitor_->HandleConnectionIdCollision(
+          connection_id, *replaced_connection_id, self_address, peer_address,
+          version,
+          queue.parsed_chlo.has_value() ? &queue.parsed_chlo.value()
+                                        : nullptr)) {
+        case VisitorInterface::HandleCidCollisionResult::kOk:
+          queue.replaced_connection_id = *replaced_connection_id;
+          buffered_session_map_.insert(
+              {*replaced_connection_id, node->shared_from_this()});
+          break;
+        case VisitorInterface::HandleCidCollisionResult::kCollision:
+          return CID_COLLISION;
+      }
+    }
+  }
+
   return SUCCESS;
 }
 
 bool QuicBufferedPacketStore::HasBufferedPackets(
     QuicConnectionId connection_id) const {
+  if (replace_cid_on_first_packet_) {
+    return buffered_session_map_.contains(connection_id);
+  }
   return undecryptable_packets_.contains(connection_id);
 }
 
 bool QuicBufferedPacketStore::HasChlosBuffered() const {
+  if (replace_cid_on_first_packet_) {
+    return num_buffered_sessions_with_chlo_ != 0;
+  }
   return !connections_with_chlo_.empty();
 }
 
 BufferedPacketList QuicBufferedPacketStore::DeliverPackets(
     QuicConnectionId connection_id) {
-  BufferedPacketList packets_to_deliver;
-  auto it = undecryptable_packets_.find(connection_id);
-  if (it != undecryptable_packets_.end()) {
-    packets_to_deliver = std::move(it->second);
-    undecryptable_packets_.erase(connection_id);
-    std::list<BufferedPacket> initial_packets;
-    std::list<BufferedPacket> other_packets;
-    for (auto& packet : packets_to_deliver.buffered_packets) {
-      QuicLongHeaderType long_packet_type = INVALID_PACKET_TYPE;
-      PacketHeaderFormat unused_format;
-      bool unused_version_flag;
-      bool unused_use_length_prefix;
-      QuicVersionLabel unused_version_label;
-      ParsedQuicVersion unused_parsed_version = UnsupportedQuicVersion();
-      QuicConnectionId unused_destination_connection_id;
-      QuicConnectionId unused_source_connection_id;
-      std::optional<absl::string_view> unused_retry_token;
-      std::string unused_detailed_error;
+  if (!replace_cid_on_first_packet_) {
+    BufferedPacketList packets_to_deliver;
+    auto it = undecryptable_packets_.find(connection_id);
+    if (it != undecryptable_packets_.end()) {
+      packets_to_deliver = std::move(it->second);
+      undecryptable_packets_.erase(connection_id);
+      std::list<BufferedPacket> initial_packets;
+      std::list<BufferedPacket> other_packets;
+      for (auto& packet : packets_to_deliver.buffered_packets) {
+        QuicLongHeaderType long_packet_type = INVALID_PACKET_TYPE;
+        PacketHeaderFormat unused_format;
+        bool unused_version_flag;
+        bool unused_use_length_prefix;
+        QuicVersionLabel unused_version_label;
+        ParsedQuicVersion unused_parsed_version = UnsupportedQuicVersion();
+        QuicConnectionId unused_destination_connection_id;
+        QuicConnectionId unused_source_connection_id;
+        std::optional<absl::string_view> unused_retry_token;
+        std::string unused_detailed_error;
 
-      // We don't need to pass |generator| because we already got the correct
-      // connection ID length when we buffered the packet and indexed by
-      // connection ID.
-      QuicErrorCode error_code = QuicFramer::ParsePublicHeaderDispatcher(
-          *packet.packet, connection_id.length(), &unused_format,
-          &long_packet_type, &unused_version_flag, &unused_use_length_prefix,
-          &unused_version_label, &unused_parsed_version,
-          &unused_destination_connection_id, &unused_source_connection_id,
-          &unused_retry_token, &unused_detailed_error);
+        // We don't need to pass |generator| because we already got the correct
+        // connection ID length when we buffered the packet and indexed by
+        // connection ID.
+        QuicErrorCode error_code = QuicFramer::ParsePublicHeaderDispatcher(
+            *packet.packet, connection_id.length(), &unused_format,
+            &long_packet_type, &unused_version_flag, &unused_use_length_prefix,
+            &unused_version_label, &unused_parsed_version,
+            &unused_destination_connection_id, &unused_source_connection_id,
+            &unused_retry_token, &unused_detailed_error);
 
-      if (error_code == QUIC_NO_ERROR && long_packet_type == INITIAL) {
-        initial_packets.push_back(std::move(packet));
-      } else {
-        other_packets.push_back(std::move(packet));
+        if (error_code == QUIC_NO_ERROR && long_packet_type == INITIAL) {
+          initial_packets.push_back(std::move(packet));
+        } else {
+          other_packets.push_back(std::move(packet));
+        }
       }
-    }
 
-    initial_packets.splice(initial_packets.end(), other_packets);
-    packets_to_deliver.buffered_packets = std::move(initial_packets);
+      initial_packets.splice(initial_packets.end(), other_packets);
+      packets_to_deliver.buffered_packets = std::move(initial_packets);
+    }
+    return packets_to_deliver;
   }
-  return packets_to_deliver;
+
+  auto it = buffered_session_map_.find(connection_id);
+  if (it == buffered_session_map_.end()) {
+    return BufferedPacketList();
+  }
+  QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 3, 13);
+  std::shared_ptr<BufferedPacketListNode> node = it->second->shared_from_this();
+  RemoveFromStore(*node);
+  std::list<BufferedPacket> initial_packets;
+  std::list<BufferedPacket> other_packets;
+  for (auto& packet : node->buffered_packets) {
+    if (packet.is_ietf_initial_packet) {
+      initial_packets.push_back(std::move(packet));
+    } else {
+      other_packets.push_back(std::move(packet));
+    }
+  }
+  initial_packets.splice(initial_packets.end(), other_packets);
+  node->buffered_packets = std::move(initial_packets);
+  BufferedPacketList& packet_list = *node;
+  return std::move(packet_list);
 }
 
 void QuicBufferedPacketStore::DiscardPackets(QuicConnectionId connection_id) {
-  undecryptable_packets_.erase(connection_id);
-  connections_with_chlo_.erase(connection_id);
+  if (!replace_cid_on_first_packet_) {
+    undecryptable_packets_.erase(connection_id);
+    connections_with_chlo_.erase(connection_id);
+    return;
+  }
+
+  QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 4, 13);
+  auto it = buffered_session_map_.find(connection_id);
+  if (it == buffered_session_map_.end()) {
+    return;
+  }
+
+  RemoveFromStore(*it->second);
+}
+
+void QuicBufferedPacketStore::RemoveFromStore(BufferedPacketListNode& node) {
+  QUICHE_DCHECK(replace_cid_on_first_packet_);
+  QUICHE_DCHECK_EQ(buffered_sessions_with_chlo_.size(),
+                   num_buffered_sessions_with_chlo_);
+  QUICHE_DCHECK_EQ(buffered_sessions_.size(), num_buffered_sessions_);
+
+  // Remove |node| from all lists.
+  QUIC_BUG_IF(quic_store_chlo_state_inconsistent,
+              node.parsed_chlo.has_value() !=
+                  buffered_sessions_with_chlo_.is_linked(&node))
+      << "Inconsistent CHLO state for connection "
+      << node.original_connection_id
+      << ", parsed_chlo.has_value:" << node.parsed_chlo.has_value()
+      << ", is_linked:" << buffered_sessions_with_chlo_.is_linked(&node);
+  if (buffered_sessions_with_chlo_.is_linked(&node)) {
+    buffered_sessions_with_chlo_.erase(&node);
+    --num_buffered_sessions_with_chlo_;
+  }
+
+  if (buffered_sessions_.is_linked(&node)) {
+    buffered_sessions_.erase(&node);
+    --num_buffered_sessions_;
+  } else {
+    QUIC_BUG(quic_store_missing_node_in_main_list)
+        << "Missing node in main buffered session list for connection "
+        << node.original_connection_id;
+  }
+
+  if (node.HasReplacedConnectionId()) {
+    bool erased = buffered_session_map_.erase(*node.replaced_connection_id) > 0;
+    QUIC_BUG_IF(quic_store_missing_replaced_cid_in_map, !erased)
+        << "Node has replaced CID but it's not in the map. original_cid: "
+        << node.original_connection_id
+        << " replaced_cid: " << *node.replaced_connection_id;
+  }
+
+  bool erased = buffered_session_map_.erase(node.original_connection_id) > 0;
+  QUIC_BUG_IF(quic_store_missing_original_cid_in_map, !erased)
+      << "Node missing in the map. original_cid: "
+      << node.original_connection_id;
 }
 
 void QuicBufferedPacketStore::DiscardAllPackets() {
-  undecryptable_packets_.clear();
-  connections_with_chlo_.clear();
+  if (!replace_cid_on_first_packet_) {
+    undecryptable_packets_.clear();
+    connections_with_chlo_.clear();
+  } else {
+    QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 5,
+                              13);
+    buffered_sessions_with_chlo_.clear();
+    num_buffered_sessions_with_chlo_ = 0;
+    buffered_sessions_.clear();
+    num_buffered_sessions_ = 0;
+    buffered_session_map_.clear();
+  }
   expiration_alarm_->Cancel();
 }
 
 void QuicBufferedPacketStore::OnExpirationTimeout() {
   QuicTime expiration_time = clock_->ApproximateNow() - connection_life_span_;
-  while (!undecryptable_packets_.empty()) {
-    auto& entry = undecryptable_packets_.front();
-    if (entry.second.creation_time > expiration_time) {
+  if (!replace_cid_on_first_packet_) {
+    while (!undecryptable_packets_.empty()) {
+      auto& entry = undecryptable_packets_.front();
+      if (entry.second.creation_time > expiration_time) {
+        break;
+      }
+      QuicConnectionId connection_id = entry.first;
+      visitor_->OnExpiredPackets(connection_id, std::move(entry.second));
+      undecryptable_packets_.pop_front();
+      connections_with_chlo_.erase(connection_id);
+    }
+    if (!undecryptable_packets_.empty()) {
+      MaybeSetExpirationAlarm();
+    }
+    return;
+  }
+  QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 6, 13);
+  while (!buffered_sessions_.empty()) {
+    BufferedPacketListNode& node = buffered_sessions_.front();
+    if (node.creation_time > expiration_time) {
       break;
     }
-    QuicConnectionId connection_id = entry.first;
-    visitor_->OnExpiredPackets(connection_id, std::move(entry.second));
-    undecryptable_packets_.pop_front();
-    connections_with_chlo_.erase(connection_id);
+    std::shared_ptr<BufferedPacketListNode> node_ref = node.shared_from_this();
+    QuicConnectionId connection_id = node.original_connection_id;
+    RemoveFromStore(node);
+    visitor_->OnExpiredPackets(connection_id, std::move(node));
   }
-  if (!undecryptable_packets_.empty()) {
+  if (!buffered_sessions_.empty()) {
     MaybeSetExpirationAlarm();
   }
 }
@@ -274,16 +479,27 @@
   }
 }
 
-bool QuicBufferedPacketStore::ShouldNotBufferPacket(bool is_chlo) {
-  bool is_store_full =
-      undecryptable_packets_.size() >= kDefaultMaxConnectionsInStore;
+bool QuicBufferedPacketStore::ShouldNotBufferPacket(bool is_chlo) const {
+  size_t num_connections = replace_cid_on_first_packet_
+                               ? num_buffered_sessions_
+                               : undecryptable_packets_.size();
+
+  bool is_store_full = num_connections >= kDefaultMaxConnectionsInStore;
 
   if (is_chlo) {
     return is_store_full;
   }
 
+  size_t num_connections_with_chlo = replace_cid_on_first_packet_
+                                         ? num_buffered_sessions_with_chlo_
+                                         : connections_with_chlo_.size();
+
+  QUIC_BUG_IF(quic_store_too_many_connections_with_chlo,
+              num_connections < num_connections_with_chlo)
+      << "num_connections: " << num_connections
+      << ", num_connections_with_chlo: " << num_connections_with_chlo;
   size_t num_connections_without_chlo =
-      undecryptable_packets_.size() - connections_with_chlo_.size();
+      num_connections - num_connections_with_chlo;
   bool reach_non_chlo_limit =
       num_connections_without_chlo >= kMaxConnectionsWithoutCHLO;
 
@@ -292,24 +508,48 @@
 
 BufferedPacketList QuicBufferedPacketStore::DeliverPacketsForNextConnection(
     QuicConnectionId* connection_id) {
-  if (connections_with_chlo_.empty()) {
+  if (!replace_cid_on_first_packet_) {
+    if (connections_with_chlo_.empty()) {
+      // Returns empty list if no CHLO has been buffered.
+      return BufferedPacketList();
+    }
+    *connection_id = connections_with_chlo_.front().first;
+    connections_with_chlo_.pop_front();
+
+    BufferedPacketList packets = DeliverPackets(*connection_id);
+    QUICHE_DCHECK(!packets.buffered_packets.empty() &&
+                  packets.parsed_chlo.has_value())
+        << "Try to deliver connectons without CHLO. # packets:"
+        << packets.buffered_packets.size()
+        << ", has_parsed_chlo:" << packets.parsed_chlo.has_value();
+    return packets;
+  }
+
+  QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 7, 13);
+  if (buffered_sessions_with_chlo_.empty()) {
     // Returns empty list if no CHLO has been buffered.
     return BufferedPacketList();
   }
-  *connection_id = connections_with_chlo_.front().first;
-  connections_with_chlo_.pop_front();
 
-  BufferedPacketList packets = DeliverPackets(*connection_id);
-  QUICHE_DCHECK(!packets.buffered_packets.empty() &&
-                packets.parsed_chlo.has_value())
+  *connection_id = buffered_sessions_with_chlo_.front().original_connection_id;
+  BufferedPacketList packet_list = DeliverPackets(*connection_id);
+  QUICHE_DCHECK(!packet_list.buffered_packets.empty() &&
+                packet_list.parsed_chlo.has_value())
       << "Try to deliver connectons without CHLO. # packets:"
-      << packets.buffered_packets.size()
-      << ", has_parsed_chlo:" << packets.parsed_chlo.has_value();
-  return packets;
+      << packet_list.buffered_packets.size()
+      << ", has_parsed_chlo:" << packet_list.parsed_chlo.has_value();
+  return packet_list;
 }
 
 bool QuicBufferedPacketStore::HasChloForConnection(
     QuicConnectionId connection_id) {
+  if (replace_cid_on_first_packet_) {
+    auto it = buffered_session_map_.find(connection_id);
+    if (it == buffered_session_map_.end()) {
+      return false;
+    }
+    return it->second->parsed_chlo.has_value();
+  }
   return connections_with_chlo_.contains(connection_id);
 }
 
@@ -325,18 +565,42 @@
   QUICHE_DCHECK_NE(out_sni, nullptr);
   QUICHE_DCHECK_NE(tls_alert, nullptr);
   QUICHE_DCHECK_EQ(version.handshake_protocol, PROTOCOL_TLS1_3);
-  auto it = undecryptable_packets_.find(connection_id);
-  if (it == undecryptable_packets_.end()) {
+
+  if (!replace_cid_on_first_packet_) {
+    auto it = undecryptable_packets_.find(connection_id);
+    if (it == undecryptable_packets_.end()) {
+      QUIC_BUG(quic_bug_10838_1)
+          << "Cannot ingest packet for unknown connection ID " << connection_id;
+      return false;
+    }
+    it->second.tls_chlo_extractor.IngestPacket(version, packet);
+    if (!it->second.tls_chlo_extractor.HasParsedFullChlo()) {
+      *tls_alert = it->second.tls_chlo_extractor.tls_alert();
+      return false;
+    }
+    const TlsChloExtractor& tls_chlo_extractor = it->second.tls_chlo_extractor;
+    *out_supported_groups = tls_chlo_extractor.supported_groups();
+    *out_alpns = tls_chlo_extractor.alpns();
+    *out_sni = tls_chlo_extractor.server_name();
+    *out_resumption_attempted = tls_chlo_extractor.resumption_attempted();
+    *out_early_data_attempted = tls_chlo_extractor.early_data_attempted();
+    return true;
+  }
+
+  QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 8, 13);
+  auto it = buffered_session_map_.find(connection_id);
+  if (it == buffered_session_map_.end()) {
     QUIC_BUG(quic_bug_10838_1)
         << "Cannot ingest packet for unknown connection ID " << connection_id;
     return false;
   }
-  it->second.tls_chlo_extractor.IngestPacket(version, packet);
-  if (!it->second.tls_chlo_extractor.HasParsedFullChlo()) {
-    *tls_alert = it->second.tls_chlo_extractor.tls_alert();
+  BufferedPacketListNode& node = *it->second;
+  node.tls_chlo_extractor.IngestPacket(version, packet);
+  if (!node.tls_chlo_extractor.HasParsedFullChlo()) {
+    *tls_alert = node.tls_chlo_extractor.tls_alert();
     return false;
   }
-  const TlsChloExtractor& tls_chlo_extractor = it->second.tls_chlo_extractor;
+  const TlsChloExtractor& tls_chlo_extractor = node.tls_chlo_extractor;
   *out_supported_groups = tls_chlo_extractor.supported_groups();
   *out_cert_compression_algos = tls_chlo_extractor.cert_compression_algos();
   *out_alpns = tls_chlo_extractor.alpns();
diff --git a/quiche/quic/core/quic_buffered_packet_store.h b/quiche/quic/core/quic_buffered_packet_store.h
index db9b74f..2555944 100644
--- a/quiche/quic/core/quic_buffered_packet_store.h
+++ b/quiche/quic/core/quic_buffered_packet_store.h
@@ -5,6 +5,7 @@
 #ifndef QUICHE_QUIC_CORE_QUIC_BUFFERED_PACKET_STORE_H_
 #define QUICHE_QUIC_CORE_QUIC_BUFFERED_PACKET_STORE_H_
 
+#include <cstddef>
 #include <cstdint>
 #include <list>
 #include <memory>
@@ -12,6 +13,7 @@
 #include <string>
 #include <vector>
 
+#include "absl/container/flat_hash_map.h"
 #include "quiche/quic/core/connection_id_generator.h"
 #include "quiche/quic/core/quic_alarm.h"
 #include "quiche/quic/core/quic_alarm_factory.h"
@@ -30,6 +32,7 @@
 #include "quiche/quic/platform/api/quic_socket_address.h"
 #include "quiche/common/platform/api/quiche_export.h"
 #include "quiche/common/quiche_buffer_allocator.h"
+#include "quiche/common/quiche_intrusive_list.h"
 #include "quiche/common/quiche_linked_hash_map.h"
 
 namespace quic {
@@ -51,14 +54,18 @@
  public:
   enum EnqueuePacketResult {
     SUCCESS = 0,
-    TOO_MANY_PACKETS,  // Too many packets stored up for a certain connection.
-    TOO_MANY_CONNECTIONS  // Too many connections stored up in the store.
+    // Too many packets stored up for a certain connection.
+    TOO_MANY_PACKETS,
+    // Too many connections stored up in the store.
+    TOO_MANY_CONNECTIONS,
+    // Replaced CID collide with a buffered or active session.
+    CID_COLLISION,
   };
 
   struct QUICHE_EXPORT BufferedPacket {
     BufferedPacket(std::unique_ptr<QuicReceivedPacket> packet,
                    QuicSocketAddress self_address,
-                   QuicSocketAddress peer_address);
+                   QuicSocketAddress peer_address, bool is_ietf_initial_packet);
     BufferedPacket(BufferedPacket&& other);
 
     BufferedPacket& operator=(BufferedPacket&& other);
@@ -68,6 +75,7 @@
     std::unique_ptr<QuicReceivedPacket> packet;
     QuicSocketAddress self_address;
     QuicSocketAddress peer_address;
+    bool is_ietf_initial_packet;
   };
 
   // A queue of BufferedPackets for a connection.
@@ -79,6 +87,20 @@
 
     ~BufferedPacketList();
 
+    bool HasReplacedConnectionId() const {
+      return replaced_connection_id.has_value() &&
+             !replaced_connection_id->IsEmpty();
+    }
+
+    bool HasAttemptedToReplaceConnectionId() const {
+      return connection_id_generator != nullptr;
+    }
+
+    void SetAttemptedToReplaceConnectionId(
+        ConnectionIdGeneratorInterface* generator) {
+      connection_id_generator = generator;
+    }
+
     std::list<BufferedPacket> buffered_packets;
     QuicTime creation_time;
     // |parsed_chlo| is set iff the entire CHLO has been received.
@@ -90,16 +112,36 @@
     ParsedQuicVersion version;
     TlsChloExtractor tls_chlo_extractor;
     // Only one reference to the generator is stored per connection, and this is
-    // stored when the CHLO is buffered. The connection needs a stable,
-    // consistent way to generate IDs. Fixing it on the CHLO is a
-    // straightforward way to enforce that.
+    // stored when the replaced CID is generated.
     ConnectionIdGeneratorInterface* connection_id_generator = nullptr;
+    // The original connection ID of the connection.
+    // Only used when replace_cid_on_first_packet_ is true.
+    QuicConnectionId original_connection_id;
+    // Set to the result of ConnectionIdGenerator::MaybeReplaceConnectionId,
+    // when the first IETF INITIAL packet is enqueued.
+    // Note that std::nullopt indicates one the following cases, you can use
+    // HasAttemptedToReplaceConnectionId() to distinguish them:
+    // 1. No attempt to replace CID has been made.
+    // 2. One attempt to replace CID has been made, but the CID generator does
+    //    not want to replace it.
+    std::optional<QuicConnectionId> replaced_connection_id;
   };
 
   using BufferedPacketMap =
       quiche::QuicheLinkedHashMap<QuicConnectionId, BufferedPacketList,
                                   QuicConnectionIdHash>;
 
+  // Tag type for the list of sessions with full CHLO buffered.
+  struct QUICHE_EXPORT BufferedSessionsWithChloList {};
+
+  // The internal data structure for a buffered session.
+  struct QUICHE_EXPORT BufferedPacketListNode
+      : public quiche::QuicheIntrusiveLink<BufferedPacketListNode>,
+        public quiche::QuicheIntrusiveLink<BufferedPacketListNode,
+                                           BufferedSessionsWithChloList>,
+        public std::enable_shared_from_this<BufferedPacketListNode>,
+        public BufferedPacketList {};
+
   class QUICHE_EXPORT VisitorInterface {
    public:
     virtual ~VisitorInterface() {}
@@ -107,6 +149,25 @@
     // Called for each expired connection when alarm fires.
     virtual void OnExpiredPackets(QuicConnectionId connection_id,
                                   BufferedPacketList early_arrived_packets) = 0;
+
+    enum class HandleCidCollisionResult {
+      kOk,
+      kCollision,
+    };
+    // Check and handle CID collision for |replaced_connection_id|.
+    // This method is called immediately after |replaced_connection_id| is
+    // generated by the connection ID generator, at which time the mapping from
+    // |replaced_connection_id| to the connection is not yet established, which
+    // means if the implementation calls
+    //   store.HasBufferedPackets(replaced_connection_id);
+    // and it returns true, then |replaced_connection_id| has already been
+    // mapped to another connection, i.e. a CID collision.
+    virtual HandleCidCollisionResult HandleConnectionIdCollision(
+        const QuicConnectionId& original_connection_id,
+        const QuicConnectionId& replaced_connection_id,
+        const QuicSocketAddress& self_address,
+        const QuicSocketAddress& peer_address, ParsedQuicVersion version,
+        const ParsedClientHello* parsed_chlo) = 0;
   };
 
   QuicBufferedPacketStore(VisitorInterface* visitor, const QuicClock* clock,
@@ -126,9 +187,10 @@
   EnqueuePacketResult EnqueuePacket(
       const ReceivedPacketInfo& packet_info,
       std::optional<ParsedClientHello> parsed_chlo,
-      ConnectionIdGeneratorInterface* connection_id_generator);
+      ConnectionIdGeneratorInterface& connection_id_generator);
 
   // Returns true if there are any packets buffered for |connection_id|.
+  // |connection_id| can be either original or replaced connection ID.
   bool HasBufferedPackets(QuicConnectionId connection_id) const;
 
   // Ingests this packet into the corresponding TlsChloExtractor. This should
@@ -154,9 +216,11 @@
   // Returns the list of buffered packets for |connection_id| and removes them
   // from the store. Returns an empty list if no early arrived packets for this
   // connection are present.
+  // |connection_id| can be either original or replaced connection ID.
   BufferedPacketList DeliverPackets(QuicConnectionId connection_id);
 
   // Discards packets buffered for |connection_id|, if any.
+  // |connection_id| can be either original or replaced connection ID.
   void DiscardPackets(QuicConnectionId connection_id);
 
   // Discards all the packets.
@@ -177,12 +241,17 @@
   BufferedPacketList DeliverPacketsForNextConnection(
       QuicConnectionId* connection_id);
 
-  // Is given connection already buffered in the store?
+  // Is the given connection in the store and contains the full CHLO?
+  // |connection_id| can be either original or replaced connection ID.
   bool HasChloForConnection(QuicConnectionId connection_id);
 
-  // Is there any CHLO buffered in the store?
+  // Is there any connection in the store that contains a full CHLO?
   bool HasChlosBuffered() const;
 
+  bool replace_cid_on_first_packet() const {
+    return replace_cid_on_first_packet_;
+  }
+
  private:
   friend class test::QuicBufferedPacketStorePeer;
 
@@ -191,11 +260,46 @@
 
   // Return true if add an extra packet will go beyond allowed max connection
   // limit. The limit for non-CHLO packet and CHLO packet is different.
-  bool ShouldNotBufferPacket(bool is_chlo);
+  bool ShouldNotBufferPacket(bool is_chlo) const;
+
+  // Remove |node| from the buffered store. If caller wants to access |node|
+  // after this call, it should use a shared_ptr<BufferedPacketListNode> to keep
+  // |node| alive:
+  //
+  //   BufferedPacketListNode& node = ...;
+  //   auto node_ref = node.shared_from_this();
+  //   RemoveFromStore(node);
+  //   |node| can still be used here.
+  //
+  void RemoveFromStore(BufferedPacketListNode& node);
+
+  const bool replace_cid_on_first_packet_ =
+      GetQuicRestartFlag(quic_dispatcher_replace_cid_on_first_packet);
 
   // A map to store packet queues with creation time for each connection.
+  // Only used when !replace_cid_on_first_packet_.
   BufferedPacketMap undecryptable_packets_;
 
+  // Map from connection ID to the list of buffered packets for that connection.
+  // The key can be either the original or the replaced connection ID.
+  // The value is never nullptr.
+  // Only used when replace_cid_on_first_packet_ is true.
+  absl::flat_hash_map<QuicConnectionId, std::shared_ptr<BufferedPacketListNode>,
+                      QuicConnectionIdHash>
+      buffered_session_map_;
+
+  // Main list of all buffered sessions, in insertion order.
+  // Only used when replace_cid_on_first_packet_ is true.
+  quiche::QuicheIntrusiveList<BufferedPacketListNode> buffered_sessions_;
+  size_t num_buffered_sessions_ = 0;
+
+  // Secondary list of all buffered sessions with full CHLO.
+  // Only used when replace_cid_on_first_packet_ is true.
+  quiche::QuicheIntrusiveList<BufferedPacketListNode,
+                              BufferedSessionsWithChloList>
+      buffered_sessions_with_chlo_;
+  size_t num_buffered_sessions_with_chlo_ = 0;
+
   // The max time the packets of a connection can be buffer in the store.
   const QuicTime::Delta connection_life_span_;
 
@@ -209,6 +313,7 @@
 
   // Keeps track of connection with CHLO buffered up already and the order they
   // arrive.
+  // Only used when !replace_cid_on_first_packet_.
   quiche::QuicheLinkedHashMap<QuicConnectionId, bool, QuicConnectionIdHash>
       connections_with_chlo_;
 };
diff --git a/quiche/quic/core/quic_buffered_packet_store_test.cc b/quiche/quic/core/quic_buffered_packet_store_test.cc
index 85ed960..8ae493d 100644
--- a/quiche/quic/core/quic_buffered_packet_store_test.cc
+++ b/quiche/quic/core/quic_buffered_packet_store_test.cc
@@ -63,7 +63,7 @@
     const QuicReceivedPacket& packet, QuicSocketAddress self_address,
     QuicSocketAddress peer_address, const ParsedQuicVersion& version,
     std::optional<ParsedClientHello> parsed_chlo,
-    ConnectionIdGeneratorInterface* connection_id_generator) {
+    ConnectionIdGeneratorInterface& connection_id_generator) {
   ReceivedPacketInfo packet_info(self_address, peer_address, packet);
   packet_info.destination_connection_id = connection_id;
   packet_info.form = form;
@@ -85,6 +85,15 @@
     last_expired_packet_queue_ = std::move(early_arrived_packets);
   }
 
+  HandleCidCollisionResult HandleConnectionIdCollision(
+      const QuicConnectionId& /*original_connection_id*/,
+      const QuicConnectionId& /*replaced_connection_id*/,
+      const QuicSocketAddress& /*self_address*/,
+      const QuicSocketAddress& /*peer_address*/, ParsedQuicVersion /*version*/,
+      const ParsedClientHello* /*parsed_chlo*/) override {
+    return HandleCidCollisionResult::kOk;
+  }
+
   // The packets queue for most recently expirect connection.
   BufferedPacketList last_expired_packet_queue_;
 };
@@ -120,7 +129,8 @@
   QuicConnectionId connection_id = TestConnectionId(1);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
   auto packets = store_.DeliverPackets(connection_id);
   const std::list<BufferedPacket>& queue = packets.buffered_packets;
@@ -143,11 +153,12 @@
   QuicConnectionId connection_id = TestConnectionId(1);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
                        addr_with_new_port, invalid_version_, kNoParsedChlo,
-                       nullptr);
+                       connection_id_generator_);
   std::list<BufferedPacket> queue =
       store_.DeliverPackets(connection_id).buffered_packets;
   ASSERT_EQ(2u, queue.size());
@@ -161,12 +172,14 @@
   size_t num_connections = 10;
   for (uint64_t conn_id = 1; conn_id <= num_connections; ++conn_id) {
     QuicConnectionId connection_id = TestConnectionId(conn_id);
-    EnqueuePacketToStore(
-        store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
-        self_address_, peer_address_, invalid_version_, kNoParsedChlo, nullptr);
-    EnqueuePacketToStore(
-        store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
-        self_address_, peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+    EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
+                         INVALID_PACKET_TYPE, packet_, self_address_,
+                         peer_address_, invalid_version_, kNoParsedChlo,
+                         connection_id_generator_);
+    EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
+                         INVALID_PACKET_TYPE, packet_, self_address_,
+                         peer_address_, invalid_version_, kNoParsedChlo,
+                         connection_id_generator_);
   }
 
   // Deliver packets in reversed order.
@@ -178,35 +191,37 @@
   }
 }
 
+// Tests that for one connection, only limited number of packets can be
+// buffered.
 TEST_F(QuicBufferedPacketStoreTest,
        FailToBufferTooManyPacketsOnExistingConnection) {
-  // Tests that for one connection, only limited number of packets can be
-  // buffered.
-  size_t num_packets = kDefaultMaxUndecryptablePackets + 1;
+  // Max number of packets that can be buffered per connection.
+  const size_t kMaxPacketsPerConnection =
+      store_.replace_cid_on_first_packet()
+          ? kDefaultMaxUndecryptablePackets
+          : kDefaultMaxUndecryptablePackets + 1;
   QuicConnectionId connection_id = TestConnectionId(1);
-  // Arrived CHLO packet shouldn't affect how many non-CHLO pacekts store can
-  // keep.
   EXPECT_EQ(QuicBufferedPacketStore::SUCCESS,
-            EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
-                                 INVALID_PACKET_TYPE, packet_, self_address_,
-                                 peer_address_, valid_version_,
-                                 kDefaultParsedChlo, nullptr));
-  for (size_t i = 1; i <= num_packets; ++i) {
-    // Only first |kDefaultMaxUndecryptablePackets packets| will be buffered.
+            EnqueuePacketToStore(store_, connection_id,
+                                 IETF_QUIC_LONG_HEADER_PACKET, INITIAL, packet_,
+                                 self_address_, peer_address_, valid_version_,
+                                 kDefaultParsedChlo, connection_id_generator_));
+  for (size_t i = 1; i <= kMaxPacketsPerConnection; ++i) {
+    // All packets will be buffered except the last one.
     EnqueuePacketResult result = EnqueuePacketToStore(
         store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
-        self_address_, peer_address_, invalid_version_, kNoParsedChlo, nullptr);
-    if (i <= kDefaultMaxUndecryptablePackets) {
+        self_address_, peer_address_, invalid_version_, kNoParsedChlo,
+        connection_id_generator_);
+    if (i != kMaxPacketsPerConnection) {
       EXPECT_EQ(EnqueuePacketResult::SUCCESS, result);
     } else {
       EXPECT_EQ(EnqueuePacketResult::TOO_MANY_PACKETS, result);
     }
   }
 
-  // Only first |kDefaultMaxUndecryptablePackets| non-CHLO packets and CHLO are
-  // buffered.
-  EXPECT_EQ(kDefaultMaxUndecryptablePackets + 1,
-            store_.DeliverPackets(connection_id).buffered_packets.size());
+  // Verify |kMaxPacketsPerConnection| packets are buffered.
+  EXPECT_EQ(store_.DeliverPackets(connection_id).buffered_packets.size(),
+            kMaxPacketsPerConnection);
 }
 
 TEST_F(QuicBufferedPacketStoreTest, ReachNonChloConnectionUpperLimit) {
@@ -217,7 +232,8 @@
     QuicConnectionId connection_id = TestConnectionId(conn_id);
     EnqueuePacketResult result = EnqueuePacketToStore(
         store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
-        self_address_, peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+        self_address_, peer_address_, invalid_version_, kNoParsedChlo,
+        connection_id_generator_);
     if (conn_id <= kMaxConnectionsWithoutCHLO) {
       EXPECT_EQ(EnqueuePacketResult::SUCCESS, result);
     } else {
@@ -244,11 +260,12 @@
   size_t num_chlos =
       kDefaultMaxConnectionsInStore - kMaxConnectionsWithoutCHLO + 1;
   for (uint64_t conn_id = 1; conn_id <= num_chlos; ++conn_id) {
-    EXPECT_EQ(EnqueuePacketResult::SUCCESS,
-              EnqueuePacketToStore(
-                  store_, TestConnectionId(conn_id), GOOGLE_QUIC_PACKET,
-                  INVALID_PACKET_TYPE, packet_, self_address_, peer_address_,
-                  valid_version_, kDefaultParsedChlo, nullptr));
+    EXPECT_EQ(
+        EnqueuePacketResult::SUCCESS,
+        EnqueuePacketToStore(store_, TestConnectionId(conn_id),
+                             GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
+                             self_address_, peer_address_, valid_version_,
+                             kDefaultParsedChlo, connection_id_generator_));
   }
 
   // Send data packets on another |kMaxConnectionsWithoutCHLO| connections.
@@ -259,7 +276,7 @@
     EnqueuePacketResult result = EnqueuePacketToStore(
         store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
         self_address_, peer_address_, valid_version_, kDefaultParsedChlo,
-        nullptr);
+        connection_id_generator_);
     if (conn_id <= kDefaultMaxConnectionsInStore) {
       EXPECT_EQ(EnqueuePacketResult::SUCCESS, result);
     } else {
@@ -273,27 +290,17 @@
             EnqueuePacketToStore(
                 store_, TestConnectionId(1), GOOGLE_QUIC_PACKET,
                 INVALID_PACKET_TYPE, packet_, self_address_, peer_address_,
-                valid_version_, kDefaultParsedChlo, &connection_id_generator_));
+                valid_version_, kDefaultParsedChlo, connection_id_generator_));
   QuicConnectionId delivered_conn_id;
   BufferedPacketList packet_list =
       store_.DeliverPacketsForNextConnection(&delivered_conn_id);
   EXPECT_EQ(1u, packet_list.buffered_packets.size());
   EXPECT_EQ(delivered_conn_id, TestConnectionId(1));
-  EXPECT_EQ(&connection_id_generator_, packet_list.connection_id_generator);
-}
-
-TEST_F(QuicBufferedPacketStoreTest, NullGeneratorOk) {
-  EXPECT_EQ(EnqueuePacketResult::SUCCESS,
-            EnqueuePacketToStore(store_, TestConnectionId(1),
-                                 GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE,
-                                 packet_, self_address_, peer_address_,
-                                 valid_version_, kDefaultParsedChlo, nullptr));
-  QuicConnectionId delivered_conn_id;
-  BufferedPacketList packet_list =
-      store_.DeliverPacketsForNextConnection(&delivered_conn_id);
-  EXPECT_EQ(1u, packet_list.buffered_packets.size());
-  EXPECT_EQ(delivered_conn_id, TestConnectionId(1));
-  EXPECT_EQ(packet_list.connection_id_generator, nullptr);
+  if (GetQuicRestartFlag(quic_dispatcher_replace_cid_on_first_packet)) {
+    EXPECT_EQ(packet_list.connection_id_generator, nullptr);
+  } else {
+    EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+  }
 }
 
 TEST_F(QuicBufferedPacketStoreTest, GeneratorIgnoredForNonChlo) {
@@ -302,18 +309,22 @@
             EnqueuePacketToStore(
                 store_, TestConnectionId(1), GOOGLE_QUIC_PACKET,
                 INVALID_PACKET_TYPE, packet_, self_address_, peer_address_,
-                valid_version_, kDefaultParsedChlo, &connection_id_generator_));
+                valid_version_, kDefaultParsedChlo, connection_id_generator_));
   EXPECT_EQ(EnqueuePacketResult::SUCCESS,
             EnqueuePacketToStore(store_, TestConnectionId(1),
                                  GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE,
                                  packet_, self_address_, peer_address_,
-                                 valid_version_, kNoParsedChlo, &generator2));
+                                 valid_version_, kNoParsedChlo, generator2));
   QuicConnectionId delivered_conn_id;
   BufferedPacketList packet_list =
       store_.DeliverPacketsForNextConnection(&delivered_conn_id);
   EXPECT_EQ(2u, packet_list.buffered_packets.size());
   EXPECT_EQ(delivered_conn_id, TestConnectionId(1));
-  EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+  if (GetQuicRestartFlag(quic_dispatcher_replace_cid_on_first_packet)) {
+    EXPECT_EQ(packet_list.connection_id_generator, nullptr);
+  } else {
+    EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+  }
 }
 
 TEST_F(QuicBufferedPacketStoreTest, EnqueueChloOnTooManyDifferentConnections) {
@@ -326,7 +337,7 @@
               EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                                    INVALID_PACKET_TYPE, packet_, self_address_,
                                    peer_address_, invalid_version_,
-                                   kNoParsedChlo, &connection_id_generator_));
+                                   kNoParsedChlo, connection_id_generator_));
   }
 
   // Buffer CHLOs on other connections till store is full.
@@ -336,7 +347,7 @@
     EnqueuePacketResult rs = EnqueuePacketToStore(
         store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE, packet_,
         self_address_, peer_address_, valid_version_, kDefaultParsedChlo,
-        &connection_id_generator_);
+        connection_id_generator_);
     if (i <= kDefaultMaxConnectionsInStore) {
       EXPECT_EQ(EnqueuePacketResult::SUCCESS, rs);
       EXPECT_TRUE(store_.HasChloForConnection(connection_id));
@@ -354,7 +365,7 @@
             EnqueuePacketToStore(
                 store_, TestConnectionId(1), GOOGLE_QUIC_PACKET,
                 INVALID_PACKET_TYPE, packet_, self_address_, peer_address_,
-                valid_version_, kDefaultParsedChlo, &connection_id_generator_));
+                valid_version_, kDefaultParsedChlo, connection_id_generator_));
   EXPECT_TRUE(store_.HasChloForConnection(TestConnectionId(1)));
 
   QuicConnectionId delivered_conn_id;
@@ -372,7 +383,11 @@
       EXPECT_EQ(2u, packet_list.buffered_packets.size());
       EXPECT_EQ(TestConnectionId(1u), delivered_conn_id);
     }
-    EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+    if (GetQuicRestartFlag(quic_dispatcher_replace_cid_on_first_packet)) {
+      EXPECT_EQ(packet_list.connection_id_generator, nullptr);
+    } else {
+      EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+    }
   }
   EXPECT_FALSE(store_.HasChlosBuffered());
 }
@@ -384,18 +399,18 @@
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
                        peer_address_, invalid_version_, kNoParsedChlo,
-                       &connection_id_generator_);
+                       connection_id_generator_);
   EXPECT_EQ(EnqueuePacketResult::SUCCESS,
-            EnqueuePacketToStore(
-                store_, connection_id, GOOGLE_QUIC_PACKET, INVALID_PACKET_TYPE,
-                packet_, self_address_, peer_address_, valid_version_,
-                kDefaultParsedChlo, &connection_id_generator_));
+            EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
+                                 INVALID_PACKET_TYPE, packet_, self_address_,
+                                 peer_address_, valid_version_,
+                                 kDefaultParsedChlo, connection_id_generator_));
   QuicConnectionId connection_id2 = TestConnectionId(2);
   EXPECT_EQ(EnqueuePacketResult::SUCCESS,
             EnqueuePacketToStore(store_, connection_id2, GOOGLE_QUIC_PACKET,
                                  INVALID_PACKET_TYPE, packet_, self_address_,
                                  peer_address_, invalid_version_, kNoParsedChlo,
-                                 &connection_id_generator_));
+                                 connection_id_generator_));
 
   // CHLO on connection 3 arrives 1ms later.
   clock_.AdvanceTime(QuicTime::Delta::FromMilliseconds(1));
@@ -406,7 +421,7 @@
   EnqueuePacketToStore(store_, connection_id3, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
                        another_client_address, valid_version_,
-                       kDefaultParsedChlo, &connection_id_generator_);
+                       kDefaultParsedChlo, connection_id_generator_);
 
   // Advance clock to the time when connection 1 and 2 expires.
   clock_.AdvanceTime(
@@ -431,7 +446,11 @@
 
   // Connection 3 is the next to be delivered as connection 1 already expired.
   EXPECT_EQ(connection_id3, delivered_conn_id);
-  EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+  if (GetQuicRestartFlag(quic_dispatcher_replace_cid_on_first_packet)) {
+    EXPECT_EQ(packet_list.connection_id_generator, nullptr);
+  } else {
+    EXPECT_EQ(packet_list.connection_id_generator, &connection_id_generator_);
+  }
   ASSERT_EQ(1u, packet_list.buffered_packets.size());
   // Packets in connection 3 should use another peer address.
   EXPECT_EQ(another_client_address,
@@ -442,10 +461,12 @@
   QuicConnectionId connection_id4 = TestConnectionId(4);
   EnqueuePacketToStore(store_, connection_id4, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id4, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   clock_.AdvanceTime(
       QuicBufferedPacketStorePeer::expiration_alarm(&store_)->deadline() -
       clock_.ApproximateNow());
@@ -461,10 +482,12 @@
   // Enqueue some packets
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
   EXPECT_FALSE(store_.HasChlosBuffered());
 
@@ -489,14 +512,16 @@
   // Enqueue some packets, which include a CHLO
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
                        peer_address_, valid_version_, kDefaultParsedChlo,
-                       nullptr);
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
   EXPECT_TRUE(store_.HasChlosBuffered());
 
@@ -522,17 +547,19 @@
   // Enqueue some packets for two connection IDs
   EnqueuePacketToStore(store_, connection_id_1, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id_1, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, invalid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, invalid_version_, kNoParsedChlo,
+                       connection_id_generator_);
 
   ParsedClientHello parsed_chlo;
   parsed_chlo.alpns.push_back("h3");
   parsed_chlo.sni = TestHostname();
-  EnqueuePacketToStore(store_, connection_id_2, GOOGLE_QUIC_PACKET,
-                       INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, valid_version_, parsed_chlo, nullptr);
+  EnqueuePacketToStore(store_, connection_id_2, IETF_QUIC_LONG_HEADER_PACKET,
+                       INITIAL, packet_, self_address_, peer_address_,
+                       valid_version_, parsed_chlo, connection_id_generator_);
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id_1));
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id_2));
   EXPECT_TRUE(store_.HasChlosBuffered());
@@ -554,8 +581,12 @@
   EXPECT_EQ(TestHostname(), packets.parsed_chlo->sni);
   // Since connection_id_2's chlo arrives, verify version is set.
   EXPECT_EQ(valid_version_, packets.version);
-  EXPECT_TRUE(store_.HasChlosBuffered());
 
+  if (store_.replace_cid_on_first_packet()) {
+    EXPECT_FALSE(store_.HasChlosBuffered());
+  } else {
+    EXPECT_TRUE(store_.HasChlosBuffered());
+  }
   // Discard the packets for connection 2
   store_.DiscardPackets(connection_id_2);
   EXPECT_FALSE(store_.HasChlosBuffered());
@@ -586,7 +617,8 @@
   EXPECT_FALSE(store_.HasBufferedPackets(connection_id));
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, packet_, self_address_,
-                       peer_address_, valid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, valid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
 
   // The packet in 'packet_' is not a TLS CHLO packet.
@@ -608,10 +640,12 @@
 
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, *packets[0], self_address_,
-                       peer_address_, valid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, valid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, *packets[1], self_address_,
-                       peer_address_, valid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, valid_version_, kNoParsedChlo,
+                       connection_id_generator_);
 
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
   EXPECT_FALSE(store_.IngestPacketForTlsChloExtraction(
@@ -691,13 +725,15 @@
 
   EnqueuePacketToStore(store_, connection_id, packet_format, long_packet_type,
                        packet_, self_address_, peer_address_, valid_version_,
-                       kNoParsedChlo, nullptr);
+                       kNoParsedChlo, connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, IETF_QUIC_LONG_HEADER_PACKET,
                        INITIAL, *initial_packets[0], self_address_,
-                       peer_address_, valid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, valid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   EnqueuePacketToStore(store_, connection_id, IETF_QUIC_LONG_HEADER_PACKET,
                        INITIAL, *initial_packets[1], self_address_,
-                       peer_address_, valid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, valid_version_, kNoParsedChlo,
+                       connection_id_generator_);
 
   BufferedPacketList delivered_packets = store_.DeliverPackets(connection_id);
   EXPECT_THAT(delivered_packets.buffered_packets, SizeIs(3));
@@ -728,7 +764,8 @@
                                  false, ECN_ECT1);
   EnqueuePacketToStore(store_, connection_id, GOOGLE_QUIC_PACKET,
                        INVALID_PACKET_TYPE, ect1_packet, self_address_,
-                       peer_address_, valid_version_, kNoParsedChlo, nullptr);
+                       peer_address_, valid_version_, kNoParsedChlo,
+                       connection_id_generator_);
   BufferedPacketList delivered_packets = store_.DeliverPackets(connection_id);
   EXPECT_THAT(delivered_packets.buffered_packets, SizeIs(1));
   for (const auto& packet : delivered_packets.buffered_packets) {
@@ -736,6 +773,15 @@
   }
 }
 
+TEST_F(QuicBufferedPacketStoreTest, EmptyBufferedPacketList) {
+  BufferedPacketList packet_list;
+  EXPECT_TRUE(packet_list.buffered_packets.empty());
+  EXPECT_FALSE(packet_list.parsed_chlo.has_value());
+  EXPECT_FALSE(packet_list.version.IsKnown());
+  EXPECT_TRUE(packet_list.original_connection_id.IsEmpty());
+  EXPECT_FALSE(packet_list.replaced_connection_id.has_value());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_dispatcher.cc b/quiche/quic/core/quic_dispatcher.cc
index 49ace1c..4b832e8 100644
--- a/quiche/quic/core/quic_dispatcher.cc
+++ b/quiche/quic/core/quic_dispatcher.cc
@@ -447,10 +447,15 @@
   if (buffered_packets_.HasChloForConnection(server_connection_id)) {
     EnqueuePacketResult rs = buffered_packets_.EnqueuePacket(
         packet_info,
-        /*parsed_chlo=*/std::nullopt, /*connection_id_generator=*/nullptr);
+        /*parsed_chlo=*/std::nullopt, ConnectionIdGenerator());
     switch (rs) {
       case EnqueuePacketResult::SUCCESS:
         break;
+      case EnqueuePacketResult::CID_COLLISION:
+        QUICHE_DCHECK(false) << "Connection " << server_connection_id
+                             << " already has a CHLO buffered, but "
+                                "EnqueuePacket returned CID_COLLISION.";
+        ABSL_FALLTHROUGH_INTENDED;
       case EnqueuePacketResult::TOO_MANY_PACKETS:
         ABSL_FALLTHROUGH_INTENDED;
       case EnqueuePacketResult::TOO_MANY_CONNECTIONS:
@@ -658,10 +663,17 @@
       // or it could be a fragment of a multi-packet CHLO.
       EnqueuePacketResult rs = buffered_packets_.EnqueuePacket(
           packet_info,
-          /*parsed_chlo=*/std::nullopt, /*connection_id_generator=*/nullptr);
+          /*parsed_chlo=*/std::nullopt, ConnectionIdGenerator());
       switch (rs) {
         case EnqueuePacketResult::SUCCESS:
           break;
+        case EnqueuePacketResult::CID_COLLISION:
+          QUICHE_DCHECK(buffered_packets_.replace_cid_on_first_packet());
+          QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet,
+                                    9, 13);
+          buffered_packets_.DiscardPackets(
+              packet_info.destination_connection_id);
+          ABSL_FALLTHROUGH_INTENDED;
         case EnqueuePacketResult::TOO_MANY_PACKETS:
           ABSL_FALLTHROUGH_INTENDED;
         case EnqueuePacketResult::TOO_MANY_CONNECTIONS:
@@ -693,10 +705,15 @@
     // Buffer non-CHLO packets.
     EnqueuePacketResult rs = buffered_packets_.EnqueuePacket(
         packet_info,
-        /*parsed_chlo=*/std::nullopt, /*connection_id_generator=*/nullptr);
+        /*parsed_chlo=*/std::nullopt, ConnectionIdGenerator());
     switch (rs) {
       case EnqueuePacketResult::SUCCESS:
         break;
+      case EnqueuePacketResult::CID_COLLISION:
+        // This should never happen; we only replace CID in the packet store
+        // for IETF packets.
+        QUIC_BUG(quic_store_cid_collision_from_gquic_packet);
+        ABSL_FALLTHROUGH_INTENDED;
       case EnqueuePacketResult::TOO_MANY_PACKETS:
         ABSL_FALLTHROUGH_INTENDED;
       case EnqueuePacketResult::TOO_MANY_CONNECTIONS:
@@ -1036,6 +1053,9 @@
   return false;
 }
 
+// TODO(wub): After deprecating --quic_dispatcher_replace_cid_on_first_packet,
+// remove |server_connection_id| because |early_arrived_packets| already
+// contains the original and replaced connection ID.
 void QuicDispatcher::OnExpiredPackets(
     QuicConnectionId server_connection_id,
     BufferedPacketList early_arrived_packets) {
@@ -1083,7 +1103,8 @@
       continue;
     }
     auto session_ptr = CreateSessionFromChlo(
-        server_connection_id, *packet_list.parsed_chlo, packet_list.version,
+        server_connection_id, packet_list.replaced_connection_id,
+        *packet_list.parsed_chlo, packet_list.version,
         packets.front().self_address, packets.front().peer_address,
         packet_list.connection_id_generator);
     if (session_ptr != nullptr) {
@@ -1120,10 +1141,17 @@
     QUIC_BUG_IF(quic_bug_12724_7, buffered_packets_.HasChloForConnection(
                                       packet_info->destination_connection_id));
     EnqueuePacketResult rs = buffered_packets_.EnqueuePacket(
-        *packet_info, std::move(parsed_chlo), &ConnectionIdGenerator());
+        *packet_info, std::move(parsed_chlo), ConnectionIdGenerator());
     switch (rs) {
       case EnqueuePacketResult::SUCCESS:
         break;
+      case EnqueuePacketResult::CID_COLLISION:
+        QUICHE_DCHECK(buffered_packets_.replace_cid_on_first_packet());
+        QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet,
+                                  10, 13);
+        buffered_packets_.DiscardPackets(
+            packet_info->destination_connection_id);
+        ABSL_FALLTHROUGH_INTENDED;
       case EnqueuePacketResult::TOO_MANY_PACKETS:
         ABSL_FALLTHROUGH_INTENDED;
       case EnqueuePacketResult::TOO_MANY_CONNECTIONS:
@@ -1133,10 +1161,46 @@
     return;
   }
 
+  if (buffered_packets_.replace_cid_on_first_packet()) {
+    QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 11,
+                              13);
+    BufferedPacketList packet_list = buffered_packets_.DeliverPackets(
+        packet_info->destination_connection_id);
+    // Get original_connection_id from buffered packets because
+    // destination_connection_id may be replaced connection_id if any packets
+    // have been sent by packet store.
+    QuicConnectionId original_connection_id =
+        packet_list.buffered_packets.empty()
+            ? packet_info->destination_connection_id
+            : packet_list.original_connection_id;
+
+    auto session_ptr = CreateSessionFromChlo(
+        original_connection_id, packet_list.replaced_connection_id, parsed_chlo,
+        packet_info->version, packet_info->self_address,
+        packet_info->peer_address, packet_list.connection_id_generator);
+    if (session_ptr == nullptr) {
+      // The only reason that CreateSessionFromChlo returns nullptr is because
+      // of CID collision, which can only happen if CreateSessionFromChlo
+      // attempted to replace the CID, CreateSessionFromChlo only replaces the
+      // CID when connection_id_generator is nullptr.
+      QUICHE_DCHECK_EQ(packet_list.connection_id_generator, nullptr);
+      return;
+    }
+    // Process the current packet first, then deliver queued-up packets.
+    // Note that multi-packet CHLOs, if received in packet number order, will
+    // not be delivered in the same order. This needs to be fixed.
+    session_ptr->ProcessUdpPacket(packet_info->self_address,
+                                  packet_info->peer_address,
+                                  packet_info->packet);
+    DeliverPacketsToSession(packet_list.buffered_packets, session_ptr.get());
+    --new_sessions_allowed_per_event_loop_;
+    return;
+  }
+
   auto session_ptr = CreateSessionFromChlo(
-      packet_info->destination_connection_id, parsed_chlo, packet_info->version,
-      packet_info->self_address, packet_info->peer_address,
-      &ConnectionIdGenerator());
+      packet_info->destination_connection_id, std::nullopt, parsed_chlo,
+      packet_info->version, packet_info->self_address,
+      packet_info->peer_address, &ConnectionIdGenerator());
   if (session_ptr == nullptr) {
     return;
   }
@@ -1206,53 +1270,93 @@
 
 std::shared_ptr<QuicSession> QuicDispatcher::CreateSessionFromChlo(
     const QuicConnectionId original_connection_id,
+    const std::optional<QuicConnectionId>& replaced_connection_id,
     const ParsedClientHello& parsed_chlo, const ParsedQuicVersion version,
     const QuicSocketAddress self_address, const QuicSocketAddress peer_address,
     ConnectionIdGeneratorInterface* connection_id_generator) {
+  bool should_generate_cid = false;
   if (connection_id_generator == nullptr) {
+    should_generate_cid = true;
     connection_id_generator = &ConnectionIdGenerator();
   }
-  std::optional<QuicConnectionId> server_connection_id =
-      connection_id_generator->MaybeReplaceConnectionId(original_connection_id,
-                                                        version);
-  const bool replaced_connection_id = server_connection_id.has_value();
-  if (!replaced_connection_id) {
+  std::optional<QuicConnectionId> server_connection_id;
+  if (buffered_packets_.replace_cid_on_first_packet()) {
+    if (should_generate_cid) {
+      QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 12,
+                                13);
+      server_connection_id = connection_id_generator->MaybeReplaceConnectionId(
+          original_connection_id, version);
+      // Normalize the output of MaybeReplaceConnectionId.
+      if (server_connection_id.has_value() &&
+          (server_connection_id->IsEmpty() ||
+           *server_connection_id == original_connection_id)) {
+        server_connection_id.reset();
+      }
+      QUIC_DVLOG(1) << "MaybeReplaceConnectionId(" << original_connection_id
+                    << ") = "
+                    << (server_connection_id.has_value()
+                            ? server_connection_id->ToString()
+                            : "nullopt");
+
+      if (server_connection_id.has_value()) {
+        switch (HandleConnectionIdCollision(
+            original_connection_id, *server_connection_id, self_address,
+            peer_address, version, &parsed_chlo)) {
+          case VisitorInterface::HandleCidCollisionResult::kOk:
+            break;
+          case VisitorInterface::HandleCidCollisionResult::kCollision:
+            return nullptr;
+        }
+      }
+    } else {
+      QUIC_RESTART_FLAG_COUNT_N(quic_dispatcher_replace_cid_on_first_packet, 13,
+                                13);
+      server_connection_id = replaced_connection_id;
+    }
+  } else {
+    server_connection_id = connection_id_generator->MaybeReplaceConnectionId(
+        original_connection_id, version);
+  }
+  const bool connection_id_replaced = server_connection_id.has_value();
+  if (!connection_id_replaced) {
     server_connection_id = original_connection_id;
   }
-  QUIC_CODE_COUNT(quic_connection_id_chosen);
-  if (reference_counted_session_map_.count(*server_connection_id) > 0) {
-    // The new connection ID is owned by another session. Avoid creating one
-    // altogether, as this connection attempt cannot possibly succeed.
-    QUIC_CODE_COUNT(quic_connection_id_collision);
-    QuicConnection* other_connection =
-        reference_counted_session_map_[*server_connection_id]->connection();
-    if (other_connection != nullptr) {  // Just make sure there is no crash.
-      QUIC_LOG_EVERY_N_SEC(ERROR, 10)
-          << "QUIC Connection ID collision. original_connection_id:"
-          << original_connection_id.ToString()
-          << " server_connection_id:" << server_connection_id->ToString()
-          << ", version:" << version << ", self_address:" << self_address
-          << ", peer_address:" << peer_address
-          << ", parsed_chlo:" << parsed_chlo
-          << ", other peer address: " << other_connection->peer_address()
-          << ", other CIDs: "
-          << quiche::PrintElements(
-                 other_connection->GetActiveServerConnectionIds())
-          << ", other stats: " << other_connection->GetStats();
+  if (!buffered_packets_.replace_cid_on_first_packet()) {
+    QUIC_CODE_COUNT(quic_connection_id_chosen);
+    if (reference_counted_session_map_.count(*server_connection_id) > 0) {
+      // The new connection ID is owned by another session. Avoid creating one
+      // altogether, as this connection attempt cannot possibly succeed.
+      QUIC_CODE_COUNT(quic_connection_id_collision);
+      QuicConnection* other_connection =
+          reference_counted_session_map_[*server_connection_id]->connection();
+      if (other_connection != nullptr) {  // Just make sure there is no crash.
+        QUIC_LOG_EVERY_N_SEC(ERROR, 10)
+            << "QUIC Connection ID collision. original_connection_id:"
+            << original_connection_id.ToString()
+            << " server_connection_id:" << server_connection_id->ToString()
+            << ", version:" << version << ", self_address:" << self_address
+            << ", peer_address:" << peer_address
+            << ", parsed_chlo:" << parsed_chlo
+            << ", other peer address: " << other_connection->peer_address()
+            << ", other CIDs: "
+            << quiche::PrintElements(
+                   other_connection->GetActiveServerConnectionIds())
+            << ", other stats: " << other_connection->GetStats();
+      }
+      if (connection_id_replaced) {
+        QUIC_CODE_COUNT(quic_replaced_connection_id_collision);
+        // The original connection ID does not correspond to an existing
+        // session. It is safe to send CONNECTION_CLOSE and add to TIME_WAIT.
+        StatelesslyTerminateConnection(
+            self_address, peer_address, original_connection_id,
+            IETF_QUIC_LONG_HEADER_PACKET,
+            /*version_flag=*/true, version.HasLengthPrefixedConnectionIds(),
+            version, QUIC_HANDSHAKE_FAILED,
+            "Connection ID collision, please retry",
+            QuicTimeWaitListManager::SEND_CONNECTION_CLOSE_PACKETS);
+      }
+      return nullptr;
     }
-    if (replaced_connection_id) {
-      QUIC_CODE_COUNT(quic_replaced_connection_id_collision);
-      // The original connection ID does not correspond to an existing
-      // session. It is safe to send CONNECTION_CLOSE and add to TIME_WAIT.
-      StatelesslyTerminateConnection(
-          self_address, peer_address, original_connection_id,
-          IETF_QUIC_LONG_HEADER_PACKET,
-          /*version_flag=*/true, version.HasLengthPrefixedConnectionIds(),
-          version, QUIC_HANDSHAKE_FAILED,
-          "Connection ID collision, please retry",
-          QuicTimeWaitListManager::SEND_CONNECTION_CLOSE_PACKETS);
-    }
-    return nullptr;
   }
   // Creates a new session and process all buffered packets for this connection.
   std::string alpn = SelectAlpn(parsed_chlo.alpns);
@@ -1267,7 +1371,7 @@
     return nullptr;
   }
 
-  if (replaced_connection_id) {
+  if (connection_id_replaced) {
     session->connection()->SetOriginalDestinationConnectionId(
         original_connection_id);
   }
@@ -1283,7 +1387,7 @@
         << *server_connection_id;
   } else {
     ++num_sessions_in_session_map_;
-    if (replaced_connection_id) {
+    if (connection_id_replaced) {
       auto insertion_result2 = reference_counted_session_map_.insert(
           std::make_pair(original_connection_id, session_ptr));
       QUIC_BUG_IF(quic_460317833_02, !insertion_result2.second)
@@ -1297,6 +1401,69 @@
   return session_ptr;
 }
 
+QuicDispatcher::HandleCidCollisionResult
+QuicDispatcher::HandleConnectionIdCollision(
+    const QuicConnectionId& original_connection_id,
+    const QuicConnectionId& replaced_connection_id,
+    const QuicSocketAddress& self_address,
+    const QuicSocketAddress& peer_address, ParsedQuicVersion version,
+    const ParsedClientHello* parsed_chlo) {
+  QUICHE_DCHECK(buffered_packets_.replace_cid_on_first_packet());
+  HandleCidCollisionResult result = HandleCidCollisionResult::kOk;
+  auto existing_session_iter =
+      reference_counted_session_map_.find(replaced_connection_id);
+  if (existing_session_iter != reference_counted_session_map_.end()) {
+    // Collide with an active session in dispatcher.
+    result = HandleCidCollisionResult::kCollision;
+    QUIC_CODE_COUNT(quic_connection_id_collision);
+    QuicConnection* other_connection =
+        existing_session_iter->second->connection();
+    if (other_connection != nullptr) {  // Just make sure there is no crash.
+      QUIC_LOG_EVERY_N_SEC(ERROR, 10)
+          << "QUIC Connection ID collision. original_connection_id:"
+          << original_connection_id
+          << ", replaced_connection_id:" << replaced_connection_id
+          << ", version:" << version << ", self_address:" << self_address
+          << ", peer_address:" << peer_address << ", parsed_chlo:"
+          << (parsed_chlo == nullptr ? "null" : parsed_chlo->ToString())
+          << ", other peer address: " << other_connection->peer_address()
+          << ", other CIDs: "
+          << quiche::PrintElements(
+                 other_connection->GetActiveServerConnectionIds())
+          << ", other stats: " << other_connection->GetStats();
+    }
+  } else if (buffered_packets_.HasBufferedPackets(replaced_connection_id)) {
+    // Collide with a buffered session in packet store.
+    result = HandleCidCollisionResult::kCollision;
+    QUIC_CODE_COUNT(quic_connection_id_collision_with_buffered_session);
+  }
+
+  if (result == HandleCidCollisionResult::kOk) {
+    return result;
+  }
+
+  const bool collide_with_active_session =
+      existing_session_iter != reference_counted_session_map_.end();
+  QUIC_DLOG(INFO) << "QUIC Connection ID collision with "
+                  << (collide_with_active_session ? "active session"
+                                                  : "buffered session")
+                  << " for original_connection_id:" << original_connection_id
+                  << ", replaced_connection_id:" << replaced_connection_id;
+
+  // The original connection ID does not correspond to an existing
+  // session. It is safe to send CONNECTION_CLOSE and add to TIME_WAIT.
+  StatelesslyTerminateConnection(
+      self_address, peer_address, original_connection_id,
+      IETF_QUIC_LONG_HEADER_PACKET,
+      /*version_flag=*/true, version.HasLengthPrefixedConnectionIds(), version,
+      QUIC_HANDSHAKE_FAILED, "Connection ID collision, please retry",
+      QuicTimeWaitListManager::SEND_CONNECTION_CLOSE_PACKETS);
+
+  // Caller is responsible for erasing the connection from the buffered store,
+  // if needed.
+  return result;
+}
+
 void QuicDispatcher::MaybeResetPacketsWithNoVersion(
     const ReceivedPacketInfo& packet_info) {
   QUICHE_DCHECK(!packet_info.version_flag);
diff --git a/quiche/quic/core/quic_dispatcher.h b/quiche/quic/core/quic_dispatcher.h
index 58e9d68..80118b7 100644
--- a/quiche/quic/core/quic_dispatcher.h
+++ b/quiche/quic/core/quic_dispatcher.h
@@ -157,6 +157,12 @@
   void OnExpiredPackets(QuicConnectionId server_connection_id,
                         QuicBufferedPacketStore::BufferedPacketList
                             early_arrived_packets) override;
+  HandleCidCollisionResult HandleConnectionIdCollision(
+      const QuicConnectionId& original_connection_id,
+      const QuicConnectionId& replaced_connection_id,
+      const QuicSocketAddress& self_address,
+      const QuicSocketAddress& peer_address, ParsedQuicVersion version,
+      const ParsedClientHello* parsed_chlo) override;
   void OnPathDegrading() override {}
 
   // Create connections for previously buffered CHLOs as many as allowed.
@@ -383,8 +389,19 @@
   bool IsServerConnectionIdTooShort(QuicConnectionId connection_id) const;
 
   // Core CHLO processing logic.
+  //
+  // |connection_id_generator| != nullptr indicates we have attempted to
+  // call connection_id_generator->MaybeReplaceConnectionId() and the result is
+  // in |replaced_connection_id|.
+  //
+  // |connection_id_generator| == nullptr indicates we have not attempted to
+  // generate a replacement connection ID, in that case
+  // - |replaced_connection_id| should be std::nullopt.
+  // - CreateSessionFromChlo will generate a replacement connection ID using
+  //   ConnectionIdGenerator().MaybeReplaceConnectionId().
   std::shared_ptr<QuicSession> CreateSessionFromChlo(
       QuicConnectionId original_connection_id,
+      const std::optional<QuicConnectionId>& replaced_connection_id,
       const ParsedClientHello& parsed_chlo, ParsedQuicVersion version,
       QuicSocketAddress self_address, QuicSocketAddress peer_address,
       ConnectionIdGeneratorInterface* connection_id_generator);
diff --git a/quiche/quic/core/quic_dispatcher_test.cc b/quiche/quic/core/quic_dispatcher_test.cc
index 21cb7c3..3d081ea 100644
--- a/quiche/quic/core/quic_dispatcher_test.cc
+++ b/quiche/quic/core/quic_dispatcher_test.cc
@@ -2752,10 +2752,23 @@
   const size_t kNumCHLOs =
       kMaxNumSessionsToCreate + kDefaultMaxConnectionsInStore + 1;
   for (uint64_t conn_id = 1; conn_id <= kNumCHLOs; ++conn_id) {
-    if (conn_id <= kMaxNumSessionsToCreate) {
+    const bool should_drop =
+        (conn_id > kMaxNumSessionsToCreate + kDefaultMaxConnectionsInStore);
+    if (store->replace_cid_on_first_packet() && !should_drop) {
+      // MaybeReplaceConnectionId will be called once per connection, whether it
+      // is buffered or not.
       EXPECT_CALL(connection_id_generator_,
                   MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
           .WillOnce(Return(std::nullopt));
+    }
+
+    if (conn_id <= kMaxNumSessionsToCreate) {
+      if (!store->replace_cid_on_first_packet()) {
+        EXPECT_CALL(
+            connection_id_generator_,
+            MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
+            .WillOnce(Return(std::nullopt));
+      }
       EXPECT_CALL(
           *dispatcher_,
           CreateQuicSession(TestConnectionId(conn_id), _, client_addr_,
@@ -2793,9 +2806,13 @@
   for (uint64_t conn_id = kMaxNumSessionsToCreate + 1;
        conn_id <= kMaxNumSessionsToCreate + kDefaultMaxConnectionsInStore;
        ++conn_id) {
-    EXPECT_CALL(connection_id_generator_,
-                MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
-        .WillOnce(Return(std::nullopt));
+    // MaybeReplaceConnectionId should have been called once per buffered
+    // session.
+    if (!store->replace_cid_on_first_packet()) {
+      EXPECT_CALL(connection_id_generator_,
+                  MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
+          .WillOnce(Return(std::nullopt));
+    }
     EXPECT_CALL(
         *dispatcher_,
         CreateQuicSession(TestConnectionId(conn_id), _, client_addr_,
@@ -2862,6 +2879,13 @@
   expect_generator_is_called_ = false;
   EXPECT_CALL(*dispatcher_, ConnectionIdGenerator())
       .WillRepeatedly(ReturnRef(generator2));
+  if (store->replace_cid_on_first_packet()) {
+    // generator2 should be used to replace the connection ID when the first
+    // IETF INITIAL is enqueued.
+    EXPECT_CALL(generator2,
+                MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
+        .WillOnce(Return(std::nullopt));
+  }
   ProcessFirstFlight(TestConnectionId(conn_id));
   EXPECT_TRUE(store->HasChloForConnection(TestConnectionId(conn_id)));
   // Change the generator back so that the session can only access generator2
@@ -2869,11 +2893,13 @@
   EXPECT_CALL(*dispatcher_, ConnectionIdGenerator())
       .WillRepeatedly(ReturnRef(connection_id_generator_));
 
-  // Consume the buffered CHLO. The buffered connection should be
-  // created using generator2.
-  EXPECT_CALL(generator2,
-              MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
-      .WillOnce(Return(std::nullopt));
+  if (!store->replace_cid_on_first_packet()) {
+    // Consume the buffered CHLO. The buffered connection should be
+    // created using generator2.
+    EXPECT_CALL(generator2,
+                MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
+        .WillOnce(Return(std::nullopt));
+  }
   EXPECT_CALL(*dispatcher_, CreateQuicSession(TestConnectionId(conn_id), _,
                                               client_addr_, Eq(ExpectedAlpn()),
                                               _, MatchParsedClientHello(), _))
@@ -2975,11 +3001,11 @@
     ProcessFirstFlight(TestConnectionId(conn_id));
   }
 
-  // Process another |kDefaultMaxUndecryptablePackets| + 1 data packets. The
-  // last one should be dropped.
-  for (uint64_t packet_number = 2;
-       packet_number <= kDefaultMaxUndecryptablePackets + 2; ++packet_number) {
-    ProcessPacket(client_addr_, last_connection_id, true, "data packet");
+  // |last_connection_id| has 1 packet buffered now. Process another
+  // |kDefaultMaxUndecryptablePackets| + 1 data packets to reach max number of
+  // buffered packets per connection.
+  for (uint64_t i = 0; i <= kDefaultMaxUndecryptablePackets; ++i) {
+    ProcessPacket(client_addr_, last_connection_id, false, "data packet");
   }
 
   // Reset counter and process buffered CHLO.
@@ -2990,11 +3016,25 @@
           dispatcher_.get(), config_, last_connection_id, client_addr_,
           &mock_helper_, &mock_alarm_factory_, &crypto_config_,
           QuicDispatcherPeer::GetCache(dispatcher_.get()), &session1_))));
-  // Only CHLO and following |kDefaultMaxUndecryptablePackets| data packets
-  // should be process.
+
+  const QuicBufferedPacketStore* store =
+      QuicDispatcherPeer::GetBufferedPackets(dispatcher_.get());
+  const QuicBufferedPacketStore::BufferedPacketList*
+      last_connection_buffered_packets =
+          QuicBufferedPacketStorePeer::FindBufferedPackets(store,
+                                                           last_connection_id);
+  ASSERT_NE(last_connection_buffered_packets, nullptr);
+  if (store->replace_cid_on_first_packet()) {
+    ASSERT_EQ(last_connection_buffered_packets->buffered_packets.size(),
+              kDefaultMaxUndecryptablePackets);
+  } else {
+    ASSERT_EQ(last_connection_buffered_packets->buffered_packets.size(),
+              kDefaultMaxUndecryptablePackets + 1);
+  }
+  // All buffered packets should be delivered to the session.
   EXPECT_CALL(*reinterpret_cast<MockQuicConnection*>(session1_->connection()),
               ProcessUdpPacket(_, _, _))
-      .Times(kDefaultMaxUndecryptablePackets + 1)
+      .Times(last_connection_buffered_packets->buffered_packets.size())
       .WillRepeatedly(WithArg<2>(
           Invoke([this, last_connection_id](const QuicEncryptedPacket& packet) {
             if (version_.UsesQuicCrypto()) {
@@ -3036,7 +3076,7 @@
                   ValidatePacket(TestConnectionId(conn_id), packet);
                 }
               })));
-    } else {
+    } else if (!store->replace_cid_on_first_packet()) {
       expect_generator_is_called_ = false;
     }
     ProcessFirstFlight(TestConnectionId(conn_id));
@@ -3168,6 +3208,189 @@
   EXPECT_TRUE(got_ce);
 }
 
+class DualCIDBufferedPacketStoreTest : public BufferedPacketStoreTest {
+ protected:
+  void SetUp() override {
+    if (!GetQuicRestartFlag(quic_dispatcher_replace_cid_on_first_packet)) {
+      GTEST_SKIP();
+    }
+
+    BufferedPacketStoreTest::SetUp();
+    QuicDispatcherPeer::set_new_sessions_allowed_per_event_loop(
+        dispatcher_.get(), 0);
+
+    // Prevent |ProcessFirstFlight| from setting up expectations for
+    // MaybeReplaceConnectionId.
+    expect_generator_is_called_ = false;
+    EXPECT_CALL(connection_id_generator_, MaybeReplaceConnectionId(_, _))
+        .WillRepeatedly(Invoke(
+            this, &DualCIDBufferedPacketStoreTest::ReplaceConnectionIdInTest));
+  }
+
+  std::optional<QuicConnectionId> ReplaceConnectionIdInTest(
+      const QuicConnectionId& original, const ParsedQuicVersion& version) {
+    auto it = replaced_cid_map_.find(original);
+    if (it == replaced_cid_map_.end()) {
+      ADD_FAILURE() << "Bad test setup: no replacement CID for " << original
+                    << ", version " << version;
+      return std::nullopt;
+    }
+    return it->second;
+  }
+
+  QuicBufferedPacketStore& store() {
+    return *QuicDispatcherPeer::GetBufferedPackets(dispatcher_.get());
+  }
+
+  using BufferedPacketList = QuicBufferedPacketStore::BufferedPacketList;
+  const BufferedPacketList* FindBufferedPackets(
+      QuicConnectionId connection_id) {
+    return QuicBufferedPacketStorePeer::FindBufferedPackets(&store(),
+                                                            connection_id);
+  }
+
+  absl::flat_hash_map<QuicConnectionId, std::optional<QuicConnectionId>>
+      replaced_cid_map_;
+
+ private:
+  using BufferedPacketStoreTest::expect_generator_is_called_;
+};
+
+INSTANTIATE_TEST_SUITE_P(DualCIDBufferedPacketStoreTests,
+                         DualCIDBufferedPacketStoreTest,
+                         ::testing::ValuesIn(CurrentSupportedVersionsWithTls()),
+                         ::testing::PrintToStringParamName());
+
+TEST_P(DualCIDBufferedPacketStoreTest, CanLookUpByBothCIDs) {
+  replaced_cid_map_[TestConnectionId(1)] = TestConnectionId(2);
+  ProcessFirstFlight(TestConnectionId(1));
+
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(1)));
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(2)));
+
+  const BufferedPacketList* packets1 = FindBufferedPackets(TestConnectionId(1));
+  const BufferedPacketList* packets2 = FindBufferedPackets(TestConnectionId(2));
+  EXPECT_EQ(packets1, packets2);
+  EXPECT_EQ(packets1->original_connection_id, TestConnectionId(1));
+  EXPECT_EQ(packets1->replaced_connection_id, TestConnectionId(2));
+}
+
+TEST_P(DualCIDBufferedPacketStoreTest, DeliverPacketsByOriginalCID) {
+  replaced_cid_map_[TestConnectionId(1)] = TestConnectionId(2);
+  ProcessFirstFlight(TestConnectionId(1));
+
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(1)));
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(2)));
+  ASSERT_TRUE(store().HasChloForConnection(TestConnectionId(1)));
+  ASSERT_TRUE(store().HasChloForConnection(TestConnectionId(2)));
+  ASSERT_TRUE(store().HasChlosBuffered());
+
+  BufferedPacketList packets = store().DeliverPackets(TestConnectionId(1));
+  EXPECT_EQ(packets.original_connection_id, TestConnectionId(1));
+  EXPECT_EQ(packets.replaced_connection_id, TestConnectionId(2));
+
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(2)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(2)));
+  EXPECT_FALSE(store().HasChlosBuffered());
+}
+
+TEST_P(DualCIDBufferedPacketStoreTest, DeliverPacketsByReplacedCID) {
+  replaced_cid_map_[TestConnectionId(1)] = TestConnectionId(2);
+  replaced_cid_map_[TestConnectionId(3)] = TestConnectionId(4);
+  ProcessFirstFlight(TestConnectionId(1));
+  ProcessFirstFlight(TestConnectionId(3));
+
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(1)));
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(3)));
+  ASSERT_TRUE(store().HasChloForConnection(TestConnectionId(1)));
+  ASSERT_TRUE(store().HasChloForConnection(TestConnectionId(3)));
+  ASSERT_TRUE(store().HasChlosBuffered());
+
+  BufferedPacketList packets2 = store().DeliverPackets(TestConnectionId(2));
+  EXPECT_EQ(packets2.original_connection_id, TestConnectionId(1));
+  EXPECT_EQ(packets2.replaced_connection_id, TestConnectionId(2));
+
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(2)));
+  EXPECT_TRUE(store().HasBufferedPackets(TestConnectionId(3)));
+  EXPECT_TRUE(store().HasBufferedPackets(TestConnectionId(4)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(2)));
+  EXPECT_TRUE(store().HasChloForConnection(TestConnectionId(3)));
+  EXPECT_TRUE(store().HasChloForConnection(TestConnectionId(4)));
+  EXPECT_TRUE(store().HasChlosBuffered());
+
+  BufferedPacketList packets4 = store().DeliverPackets(TestConnectionId(4));
+  EXPECT_EQ(packets4.original_connection_id, TestConnectionId(3));
+  EXPECT_EQ(packets4.replaced_connection_id, TestConnectionId(4));
+
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(3)));
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(4)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(3)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(4)));
+  EXPECT_FALSE(store().HasChlosBuffered());
+}
+
+TEST_P(DualCIDBufferedPacketStoreTest, DiscardPacketsByOriginalCID) {
+  replaced_cid_map_[TestConnectionId(1)] = TestConnectionId(2);
+  ProcessFirstFlight(TestConnectionId(1));
+
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(1)));
+
+  store().DiscardPackets(TestConnectionId(1));
+
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(2)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(2)));
+  EXPECT_FALSE(store().HasChlosBuffered());
+}
+
+TEST_P(DualCIDBufferedPacketStoreTest, DiscardPacketsByReplacedCID) {
+  replaced_cid_map_[TestConnectionId(1)] = TestConnectionId(2);
+  replaced_cid_map_[TestConnectionId(3)] = TestConnectionId(4);
+  ProcessFirstFlight(TestConnectionId(1));
+  ProcessFirstFlight(TestConnectionId(3));
+
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(2)));
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(4)));
+
+  store().DiscardPackets(TestConnectionId(2));
+
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(2)));
+  EXPECT_TRUE(store().HasBufferedPackets(TestConnectionId(3)));
+  EXPECT_TRUE(store().HasBufferedPackets(TestConnectionId(4)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(1)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(2)));
+  EXPECT_TRUE(store().HasChloForConnection(TestConnectionId(3)));
+  EXPECT_TRUE(store().HasChloForConnection(TestConnectionId(4)));
+  EXPECT_TRUE(store().HasChlosBuffered());
+
+  store().DiscardPackets(TestConnectionId(4));
+
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(3)));
+  EXPECT_FALSE(store().HasBufferedPackets(TestConnectionId(4)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(3)));
+  EXPECT_FALSE(store().HasChloForConnection(TestConnectionId(4)));
+  EXPECT_FALSE(store().HasChlosBuffered());
+}
+
+TEST_P(DualCIDBufferedPacketStoreTest, CIDCollision) {
+  replaced_cid_map_[TestConnectionId(1)] = TestConnectionId(2);
+  replaced_cid_map_[TestConnectionId(3)] = TestConnectionId(2);
+  ProcessFirstFlight(TestConnectionId(1));
+  ProcessFirstFlight(TestConnectionId(3));
+
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(1)));
+  ASSERT_TRUE(store().HasBufferedPackets(TestConnectionId(2)));
+
+  // QuicDispatcher should discard connection 3 after CID collision.
+  ASSERT_FALSE(store().HasBufferedPackets(TestConnectionId(3)));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index 43b5ace..a180fb1 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -420,6 +420,12 @@
   return os;
 }
 
+std::string ParsedClientHello::ToString() const {
+  std::ostringstream oss;
+  oss << *this;
+  return oss.str();
+}
+
 bool operator==(const ParsedClientHello& a, const ParsedClientHello& b) {
   return a.sni == b.sni && a.uaid == b.uaid &&
          a.supported_groups == b.supported_groups &&
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index b85e079..f3c5ff2 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -882,6 +882,8 @@
   std::string retry_token;
   bool resumption_attempted = false;  // TLS only.
   bool early_data_attempted = false;  // TLS only.
+
+  std::string ToString() const;
 };
 
 QUICHE_EXPORT bool operator==(const ParsedClientHello& a,
diff --git a/quiche/quic/test_tools/quic_buffered_packet_store_peer.cc b/quiche/quic/test_tools/quic_buffered_packet_store_peer.cc
index 4d11806..0e2aa15 100644
--- a/quiche/quic/test_tools/quic_buffered_packet_store_peer.cc
+++ b/quiche/quic/test_tools/quic_buffered_packet_store_peer.cc
@@ -9,17 +9,33 @@
 namespace quic {
 namespace test {
 
-// static
 QuicAlarm* QuicBufferedPacketStorePeer::expiration_alarm(
     QuicBufferedPacketStore* store) {
   return store->expiration_alarm_.get();
 }
 
-// static
 void QuicBufferedPacketStorePeer::set_clock(QuicBufferedPacketStore* store,
                                             const QuicClock* clock) {
   store->clock_ = clock;
 }
 
+const QuicBufferedPacketStore::BufferedPacketList*
+QuicBufferedPacketStorePeer::FindBufferedPackets(
+    const QuicBufferedPacketStore* store, QuicConnectionId connection_id) {
+  if (store->replace_cid_on_first_packet()) {
+    auto it = store->buffered_session_map_.find(connection_id);
+    if (it == store->buffered_session_map_.end()) {
+      return nullptr;
+    }
+    return it->second.get();
+  }
+
+  auto it = store->undecryptable_packets_.find(connection_id);
+  if (it == store->undecryptable_packets_.end()) {
+    return nullptr;
+  }
+  return &it->second;
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/test_tools/quic_buffered_packet_store_peer.h b/quiche/quic/test_tools/quic_buffered_packet_store_peer.h
index 0610274..5bb9c17 100644
--- a/quiche/quic/test_tools/quic_buffered_packet_store_peer.h
+++ b/quiche/quic/test_tools/quic_buffered_packet_store_peer.h
@@ -8,7 +8,9 @@
 #include <memory>
 
 #include "quiche/quic/core/quic_alarm.h"
+#include "quiche/quic/core/quic_buffered_packet_store.h"
 #include "quiche/quic/core/quic_clock.h"
+#include "quiche/quic/core/quic_connection_id.h"
 
 namespace quic {
 
@@ -23,6 +25,9 @@
   static QuicAlarm* expiration_alarm(QuicBufferedPacketStore* store);
 
   static void set_clock(QuicBufferedPacketStore* store, const QuicClock* clock);
+
+  static const QuicBufferedPacketStore::BufferedPacketList* FindBufferedPackets(
+      const QuicBufferedPacketStore* store, QuicConnectionId connection_id);
 };
 
 }  // namespace test