No public description

PiperOrigin-RevId: 568864507
diff --git a/quiche/quic/core/quic_buffered_packet_store.cc b/quiche/quic/core/quic_buffered_packet_store.cc
index df028f0..650a08f 100644
--- a/quiche/quic/core/quic_buffered_packet_store.cc
+++ b/quiche/quic/core/quic_buffered_packet_store.cc
@@ -292,9 +292,11 @@
 
 bool QuicBufferedPacketStore::IngestPacketForTlsChloExtraction(
     const QuicConnectionId& connection_id, const ParsedQuicVersion& version,
-    const QuicReceivedPacket& packet, std::vector<std::string>* out_alpns,
-    std::string* out_sni, bool* out_resumption_attempted,
-    bool* out_early_data_attempted, absl::optional<uint8_t>* tls_alert) {
+    const QuicReceivedPacket& packet,
+    std::vector<uint16_t>* out_supported_groups,
+    std::vector<std::string>* out_alpns, std::string* out_sni,
+    bool* out_resumption_attempted, bool* out_early_data_attempted,
+    absl::optional<uint8_t>* tls_alert) {
   QUICHE_DCHECK_NE(out_alpns, nullptr);
   QUICHE_DCHECK_NE(out_sni, nullptr);
   QUICHE_DCHECK_NE(tls_alert, nullptr);
@@ -311,6 +313,7 @@
     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();
diff --git a/quiche/quic/core/quic_buffered_packet_store.h b/quiche/quic/core/quic_buffered_packet_store.h
index 734980f..0973d5c 100644
--- a/quiche/quic/core/quic_buffered_packet_store.h
+++ b/quiche/quic/core/quic_buffered_packet_store.h
@@ -114,18 +114,21 @@
   // Ingests this packet into the corresponding TlsChloExtractor. This should
   // only be called when HasBufferedPackets(connection_id) is true.
   // Returns whether we've now parsed a full multi-packet TLS CHLO.
-  // When this returns true, |out_alpns| is populated with the list of ALPNs
-  // extracted from the CHLO. |out_sni| is populated with the SNI tag in CHLO.
-  // |out_resumption_attempted| is populated if the CHLO has the
-  // 'pre_shared_key' TLS extension. |out_early_data_attempted| is populated if
-  // the CHLO has the 'early_data' TLS extension.
-  // When this returns false, and an unrecoverable error happened due to a TLS
-  // alert, |*tls_alert| will be set to the alert value.
+  // When this returns true, |out_supported_groups| is populated with the list
+  // of groups in the CHLO's 'supported_groups' TLS extension. |out_alpns| is
+  // populated with the list of ALPNs extracted from the CHLO. |out_sni| is
+  // populated with the SNI tag in CHLO. |out_resumption_attempted| is populated
+  // if the CHLO has the 'pre_shared_key' TLS extension.
+  // |out_early_data_attempted| is populated if the CHLO has the 'early_data'
+  // TLS extension. When this returns false, and an unrecoverable error happened
+  // due to a TLS alert, |*tls_alert| will be set to the alert value.
   bool IngestPacketForTlsChloExtraction(
       const QuicConnectionId& connection_id, const ParsedQuicVersion& version,
-      const QuicReceivedPacket& packet, std::vector<std::string>* out_alpns,
-      std::string* out_sni, bool* out_resumption_attempted,
-      bool* out_early_data_attempted, absl::optional<uint8_t>* tls_alert);
+      const QuicReceivedPacket& packet,
+      std::vector<uint16_t>* out_supported_groups,
+      std::vector<std::string>* out_alpns, std::string* out_sni,
+      bool* out_resumption_attempted, bool* out_early_data_attempted,
+      absl::optional<uint8_t>* tls_alert);
 
   // 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
diff --git a/quiche/quic/core/quic_buffered_packet_store_test.cc b/quiche/quic/core/quic_buffered_packet_store_test.cc
index 3b32300..562b516 100644
--- a/quiche/quic/core/quic_buffered_packet_store_test.cc
+++ b/quiche/quic/core/quic_buffered_packet_store_test.cc
@@ -463,6 +463,7 @@
 TEST_F(QuicBufferedPacketStoreTest, IngestPacketForTlsChloExtraction) {
   QuicConnectionId connection_id = TestConnectionId(1);
   std::vector<std::string> alpns;
+  std::vector<uint16_t> supported_groups;
   std::string sni;
   bool resumption_attempted = false;
   bool early_data_attempted = false;
@@ -476,7 +477,7 @@
 
   // The packet in 'packet_' is not a TLS CHLO packet.
   EXPECT_FALSE(store_.IngestPacketForTlsChloExtraction(
-      connection_id, valid_version_, packet_, &alpns, &sni,
+      connection_id, valid_version_, packet_, &supported_groups, &alpns, &sni,
       &resumption_attempted, &early_data_attempted, &tls_alert));
 
   store_.DiscardPackets(connection_id);
@@ -497,13 +498,18 @@
 
   EXPECT_TRUE(store_.HasBufferedPackets(connection_id));
   EXPECT_FALSE(store_.IngestPacketForTlsChloExtraction(
-      connection_id, valid_version_, *packets[0], &alpns, &sni,
-      &resumption_attempted, &early_data_attempted, &tls_alert));
+      connection_id, valid_version_, *packets[0], &supported_groups, &alpns,
+      &sni, &resumption_attempted, &early_data_attempted, &tls_alert));
   EXPECT_TRUE(store_.IngestPacketForTlsChloExtraction(
-      connection_id, valid_version_, *packets[1], &alpns, &sni,
-      &resumption_attempted, &early_data_attempted, &tls_alert));
+      connection_id, valid_version_, *packets[1], &supported_groups, &alpns,
+      &sni, &resumption_attempted, &early_data_attempted, &tls_alert));
 
   EXPECT_THAT(alpns, ElementsAre(AlpnForVersion(valid_version_)));
+  if (GetQuicReloadableFlag(quic_extract_supported_groups_early)) {
+    EXPECT_FALSE(supported_groups.empty());
+  } else {
+    EXPECT_TRUE(supported_groups.empty());
+  }
   EXPECT_EQ(sni, TestHostname());
 
   EXPECT_FALSE(resumption_attempted);
diff --git a/quiche/quic/core/quic_dispatcher.cc b/quiche/quic/core/quic_dispatcher.cc
index bdc339a..09ece44 100644
--- a/quiche/quic/core/quic_dispatcher.cc
+++ b/quiche/quic/core/quic_dispatcher.cc
@@ -678,6 +678,7 @@
   if (packet_info.version.UsesTls()) {
     bool has_full_tls_chlo = false;
     std::string sni;
+    std::vector<uint16_t> supported_groups;
     std::vector<std::string> alpns;
     bool resumption_attempted = false, early_data_attempted = false;
     if (buffered_packets_.HasBufferedPackets(
@@ -686,8 +687,8 @@
       // use the associated TlsChloExtractor to parse this packet.
       has_full_tls_chlo = buffered_packets_.IngestPacketForTlsChloExtraction(
           packet_info.destination_connection_id, packet_info.version,
-          packet_info.packet, &alpns, &sni, &resumption_attempted,
-          &early_data_attempted, &result.tls_alert);
+          packet_info.packet, &supported_groups, &alpns, &sni,
+          &resumption_attempted, &early_data_attempted, &result.tls_alert);
     } else {
       // If we do not have a BufferedPacketList for this connection ID,
       // create a single-use one to check whether this packet contains a
@@ -697,6 +698,7 @@
       if (tls_chlo_extractor.HasParsedFullChlo()) {
         // This packet contains a full single-packet CHLO.
         has_full_tls_chlo = true;
+        supported_groups = tls_chlo_extractor.supported_groups();
         alpns = tls_chlo_extractor.alpns();
         sni = tls_chlo_extractor.server_name();
         resumption_attempted = tls_chlo_extractor.resumption_attempted();
@@ -723,6 +725,7 @@
 
     ParsedClientHello& parsed_chlo = result.parsed_chlo.emplace();
     parsed_chlo.sni = std::move(sni);
+    parsed_chlo.supported_groups = std::move(supported_groups);
     parsed_chlo.alpns = std::move(alpns);
     if (packet_info.retry_token.has_value()) {
       parsed_chlo.retry_token = std::string(*packet_info.retry_token);
diff --git a/quiche/quic/core/quic_dispatcher_test.cc b/quiche/quic/core/quic_dispatcher_test.cc
index 6cd5390..35b96ff 100644
--- a/quiche/quic/core/quic_dispatcher_test.cc
+++ b/quiche/quic/core/quic_dispatcher_test.cc
@@ -64,11 +64,16 @@
 #include "quiche/common/test_tools/quiche_test_utils.h"
 
 using testing::_;
+using testing::AllOf;
 using testing::ByMove;
+using testing::ElementsAreArray;
 using testing::Eq;
+using testing::Field;
 using testing::InSequence;
 using testing::Invoke;
+using testing::IsEmpty;
 using testing::NiceMock;
+using testing::Not;
 using testing::Ref;
 using testing::Return;
 using testing::ReturnRef;
@@ -517,11 +522,18 @@
 
   std::string ExpectedAlpn() { return ExpectedAlpnForVersion(version_); }
 
-  ParsedClientHello ParsedClientHelloForTest() {
-    ParsedClientHello parsed_chlo;
-    parsed_chlo.alpns = {ExpectedAlpn()};
-    parsed_chlo.sni = TestHostname();
-    return parsed_chlo;
+  auto MatchParsedClientHello() {
+    if (version_.UsesQuicCrypto() ||
+        !GetQuicReloadableFlag(quic_extract_supported_groups_early)) {
+      return AllOf(
+          Field(&ParsedClientHello::alpns, ElementsAreArray({ExpectedAlpn()})),
+          Field(&ParsedClientHello::sni, Eq(TestHostname())),
+          Field(&ParsedClientHello::supported_groups, IsEmpty()));
+    }
+    return AllOf(
+        Field(&ParsedClientHello::alpns, ElementsAreArray({ExpectedAlpn()})),
+        Field(&ParsedClientHello::sni, Eq(TestHostname())),
+        Field(&ParsedClientHello::supported_groups, Not(IsEmpty())));
   }
 
   void MarkSession1Deleted() { session1_ = nullptr; }
@@ -612,10 +624,10 @@
 
   QuicSocketAddress client_address(QuicIpAddress::Loopback4(), 1);
 
-  EXPECT_CALL(*dispatcher_,
-              CreateQuicSession(TestConnectionId(1), _, client_address,
-                                Eq(ExpectedAlpn()), _,
-                                Eq(ParsedClientHelloForTest()), _))
+  EXPECT_CALL(
+      *dispatcher_,
+      CreateQuicSession(TestConnectionId(1), _, client_address,
+                        Eq(ExpectedAlpn()), _, MatchParsedClientHello(), _))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, TestConnectionId(1), client_address,
           &mock_helper_, &mock_alarm_factory_, &crypto_config_,
@@ -643,9 +655,9 @@
   ConnectionIdGeneratorInterface& expected_generator =
       mock_connection_id_generator;
   EXPECT_CALL(*dispatcher_,
-              CreateQuicSession(
-                  TestConnectionId(1), _, client_address, Eq(ExpectedAlpn()), _,
-                  Eq(ParsedClientHelloForTest()), Ref(expected_generator)))
+              CreateQuicSession(TestConnectionId(1), _, client_address,
+                                Eq(ExpectedAlpn()), _, MatchParsedClientHello(),
+                                Ref(expected_generator)))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, TestConnectionId(1), client_address,
           &mock_helper_, &mock_alarm_factory_, &crypto_config_,
@@ -667,7 +679,7 @@
   QuicSocketAddress client_address(QuicIpAddress::Loopback4(), 1);
   EXPECT_CALL(*dispatcher_,
               CreateQuicSession(new_id, _, client_address, Eq(ExpectedAlpn()),
-                                _, Eq(ParsedClientHelloForTest()), _))
+                                _, MatchParsedClientHello(), _))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, new_id, client_address, &mock_helper_,
           &mock_alarm_factory_, &crypto_config_,
@@ -732,10 +744,10 @@
       << "No session should be created before the rest of the CHLO arrives.";
 
   // Processing the second packet should create the new session.
-  EXPECT_CALL(*dispatcher_,
-              CreateQuicSession(new_connection_id, _, client_address,
-                                Eq(ExpectedAlpn()), _,
-                                Eq(ParsedClientHelloForTest()), _))
+  EXPECT_CALL(
+      *dispatcher_,
+      CreateQuicSession(new_connection_id, _, client_address,
+                        Eq(ExpectedAlpn()), _, MatchParsedClientHello(), _))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, new_connection_id, client_address,
           &mock_helper_, &mock_alarm_factory_, &crypto_config_,
@@ -773,10 +785,10 @@
 TEST_P(QuicDispatcherTestAllVersions, ProcessPackets) {
   QuicSocketAddress client_address(QuicIpAddress::Loopback4(), 1);
 
-  EXPECT_CALL(*dispatcher_,
-              CreateQuicSession(TestConnectionId(1), _, client_address,
-                                Eq(ExpectedAlpn()), _,
-                                Eq(ParsedClientHelloForTest()), _))
+  EXPECT_CALL(
+      *dispatcher_,
+      CreateQuicSession(TestConnectionId(1), _, client_address,
+                        Eq(ExpectedAlpn()), _, MatchParsedClientHello(), _))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, TestConnectionId(1), client_address,
           &mock_helper_, &mock_alarm_factory_, &crypto_config_,
@@ -788,10 +800,10 @@
       })));
   ProcessFirstFlight(client_address, TestConnectionId(1));
 
-  EXPECT_CALL(*dispatcher_,
-              CreateQuicSession(TestConnectionId(2), _, client_address,
-                                Eq(ExpectedAlpn()), _,
-                                Eq(ParsedClientHelloForTest()), _))
+  EXPECT_CALL(
+      *dispatcher_,
+      CreateQuicSession(TestConnectionId(2), _, client_address,
+                        Eq(ExpectedAlpn()), _, MatchParsedClientHello(), _))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, TestConnectionId(2), client_address,
           &mock_helper_, &mock_alarm_factory_, &crypto_config_,
@@ -2553,7 +2565,7 @@
       .WillOnce(Return(absl::nullopt));
   EXPECT_CALL(*dispatcher_,
               CreateQuicSession(conn_id, _, client_addr_, Eq(ExpectedAlpn()), _,
-                                Eq(ParsedClientHelloForTest()), _))
+                                MatchParsedClientHello(), _))
       .WillOnce(Return(ByMove(CreateSession(
           dispatcher_.get(), config_, conn_id, client_addr_, &mock_helper_,
           &mock_alarm_factory_, &crypto_config_,
@@ -2754,10 +2766,10 @@
       EXPECT_CALL(connection_id_generator_,
                   MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
           .WillOnce(Return(absl::nullopt));
-      EXPECT_CALL(*dispatcher_,
-                  CreateQuicSession(TestConnectionId(conn_id), _, client_addr_,
-                                    Eq(ExpectedAlpn()), _,
-                                    Eq(ParsedClientHelloForTest()), _))
+      EXPECT_CALL(
+          *dispatcher_,
+          CreateQuicSession(TestConnectionId(conn_id), _, client_addr_,
+                            Eq(ExpectedAlpn()), _, MatchParsedClientHello(), _))
           .WillOnce(Return(ByMove(CreateSession(
               dispatcher_.get(), config_, TestConnectionId(conn_id),
               client_addr_, &mock_helper_, &mock_alarm_factory_,
@@ -2794,10 +2806,10 @@
     EXPECT_CALL(connection_id_generator_,
                 MaybeReplaceConnectionId(TestConnectionId(conn_id), version_))
         .WillOnce(Return(absl::nullopt));
-    EXPECT_CALL(*dispatcher_,
-                CreateQuicSession(TestConnectionId(conn_id), _, client_addr_,
-                                  Eq(ExpectedAlpn()), _,
-                                  Eq(ParsedClientHelloForTest()), _))
+    EXPECT_CALL(
+        *dispatcher_,
+        CreateQuicSession(TestConnectionId(conn_id), _, client_addr_,
+                          Eq(ExpectedAlpn()), _, MatchParsedClientHello(), _))
         .WillOnce(Return(ByMove(CreateSession(
             dispatcher_.get(), config_, TestConnectionId(conn_id), client_addr_,
             &mock_helper_, &mock_alarm_factory_, &crypto_config_,
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index 1d11804..87b37ef 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -25,6 +25,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_ignore_gquic_probing, true)
 // If true, QUIC will default enable MTU discovery at server, with a target of 1450 bytes.
 QUIC_FLAG(quic_reloadable_flag_quic_enable_mtu_discovery_at_server, false)
+// If true, QUIC will extract supported_groups from ClientHello before creating QuicSession.
+QUIC_FLAG(quic_reloadable_flag_quic_extract_supported_groups_early, true)
 // If true, QuicGsoBatchWriter will support release time if it is available and the process has the permission to do so.
 QUIC_FLAG(quic_restart_flag_quic_support_release_time_for_gso, false)
 // If true, a duplicate NEW_CID frame will be ignore during QUIC packet processing.
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index 168d699..b7b2b26 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -417,7 +417,8 @@
 }
 
 bool operator==(const ParsedClientHello& a, const ParsedClientHello& b) {
-  return a.sni == b.sni && a.uaid == b.uaid && a.alpns == b.alpns &&
+  return a.sni == b.sni && a.uaid == b.uaid &&
+         a.supported_groups == b.supported_groups && a.alpns == b.alpns &&
          a.retry_token == b.retry_token &&
          a.resumption_attempted == b.resumption_attempted &&
          a.early_data_attempted == b.early_data_attempted;
@@ -427,6 +428,10 @@
                          const ParsedClientHello& parsed_chlo) {
   os << "{ sni:" << parsed_chlo.sni << ", uaid:" << parsed_chlo.uaid
      << ", alpns:" << quiche::PrintElements(parsed_chlo.alpns)
+     << ", supported_groups:"
+     << quiche::PrintElements(parsed_chlo.supported_groups)
+     << ", resumption_attempted:" << parsed_chlo.resumption_attempted
+     << ", early_data_attempted:" << parsed_chlo.early_data_attempted
      << ", len(retry_token):" << parsed_chlo.retry_token.size() << " }";
   return os;
 }
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index e58d0d6..727eb73 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -861,9 +861,10 @@
 // ParsedClientHello contains client hello information extracted from a fully
 // received client hello.
 struct QUICHE_EXPORT ParsedClientHello {
-  std::string sni;                 // QUIC crypto and TLS.
-  std::string uaid;                // QUIC crypto only.
-  std::vector<std::string> alpns;  // QUIC crypto and TLS.
+  std::string sni;                         // QUIC crypto and TLS.
+  std::string uaid;                        // QUIC crypto only.
+  std::vector<uint16_t> supported_groups;  // TLS only.
+  std::vector<std::string> alpns;          // QUIC crypto and TLS.
   // The unvalidated retry token from the last received packet of a potentially
   // multi-packet client hello. TLS only.
   std::string retry_token;
diff --git a/quiche/quic/core/tls_chlo_extractor.cc b/quiche/quic/core/tls_chlo_extractor.cc
index 9a741d0..7cfcf25 100644
--- a/quiche/quic/core/tls_chlo_extractor.cc
+++ b/quiche/quic/core/tls_chlo_extractor.cc
@@ -4,8 +4,10 @@
 
 #include "quiche/quic/core/tls_chlo_extractor.h"
 
+#include <cstdint>
 #include <cstring>
 #include <memory>
+#include <vector>
 
 #include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
@@ -18,6 +20,7 @@
 #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_flags.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 
 namespace quic {
@@ -30,6 +33,41 @@
                                                    &unused_extension_bytes,
                                                    &unused_extension_len);
 }
+
+std::vector<uint16_t> GetSupportedGroups(const SSL_CLIENT_HELLO* client_hello) {
+  const uint8_t* extension_data;
+  size_t extension_len;
+  int rv = SSL_early_callback_ctx_extension_get(
+      client_hello, TLSEXT_TYPE_supported_groups, &extension_data,
+      &extension_len);
+  if (rv != 1) {
+    return {};
+  }
+
+  // See https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.7 for the
+  // format of this extension.
+  QuicDataReader named_groups_reader(
+      reinterpret_cast<const char*>(extension_data), extension_len);
+  uint16_t named_groups_len;
+  if (!named_groups_reader.ReadUInt16(&named_groups_len) ||
+      named_groups_len + sizeof(uint16_t) != extension_len) {
+    QUIC_CODE_COUNT(quic_chlo_supported_groups_invalid_length);
+    return {};
+  }
+
+  std::vector<uint16_t> named_groups;
+  while (!named_groups_reader.IsDoneReading()) {
+    uint16_t named_group;
+    if (!named_groups_reader.ReadUInt16(&named_group)) {
+      QUIC_CODE_COUNT(quic_chlo_supported_groups_odd_length);
+      QUIC_LOG_FIRST_N(WARNING, 10) << "Failed to read named groups";
+      break;
+    }
+    named_groups.push_back(named_group);
+  }
+  return named_groups;
+}
+
 }  // namespace
 
 TlsChloExtractor::TlsChloExtractor()
@@ -60,6 +98,7 @@
   error_details_ = std::move(other.error_details_);
   parsed_crypto_frame_in_this_packet_ =
       other.parsed_crypto_frame_in_this_packet_;
+  supported_groups_ = std::move(other.supported_groups_);
   alpns_ = std::move(other.alpns_);
   server_name_ = std::move(other.server_name_);
   client_hello_bytes_ = std::move(other.client_hello_bytes_);
@@ -323,6 +362,11 @@
     }
   }
 
+  if (GetQuicReloadableFlag(quic_extract_supported_groups_early)) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_extract_supported_groups_early, 1, 3);
+    supported_groups_ = GetSupportedGroups(client_hello);
+  }
+
   // Update our state now that we've parsed a full CHLO.
   if (state_ == State::kInitial) {
     state_ = State::kParsedFullSinglePacketChlo;
diff --git a/quiche/quic/core/tls_chlo_extractor.h b/quiche/quic/core/tls_chlo_extractor.h
index 55b0142..324365e 100644
--- a/quiche/quic/core/tls_chlo_extractor.h
+++ b/quiche/quic/core/tls_chlo_extractor.h
@@ -5,6 +5,7 @@
 #ifndef QUICHE_QUIC_CORE_TLS_CHLO_EXTRACTOR_H_
 #define QUICHE_QUIC_CORE_TLS_CHLO_EXTRACTOR_H_
 
+#include <cstdint>
 #include <memory>
 #include <string>
 #include <vector>
@@ -49,6 +50,9 @@
   std::string server_name() const { return server_name_; }
   bool resumption_attempted() const { return resumption_attempted_; }
   bool early_data_attempted() const { return early_data_attempted_; }
+  const std::vector<uint16_t>& supported_groups() const {
+    return supported_groups_;
+  }
   absl::Span<const uint8_t> client_hello_bytes() const {
     return client_hello_bytes_;
   }
@@ -253,6 +257,8 @@
   std::string error_details_;
   // Whether a CRYPTO frame was parsed in this packet.
   bool parsed_crypto_frame_in_this_packet_;
+  // Array of NamedGroups parsed from the CHLO's supported_groups extension.
+  std::vector<uint16_t> supported_groups_;
   // Array of ALPNs parsed from the CHLO.
   std::vector<std::string> alpns_;
   // SNI parsed from the CHLO.
diff --git a/quiche/quic/core/tls_chlo_extractor_test.cc b/quiche/quic/core/tls_chlo_extractor_test.cc
index 44a1e63..34fe798 100644
--- a/quiche/quic/core/tls_chlo_extractor_test.cc
+++ b/quiche/quic/core/tls_chlo_extractor_test.cc
@@ -29,18 +29,26 @@
   TlsChloExtractorTest() : version_(GetParam()), server_id_(TestServerId()) {}
 
   void Initialize() {
+    tls_chlo_extractor_ = std::make_unique<TlsChloExtractor>();
     AnnotatedPackets packets =
         GetAnnotatedFirstFlightOfPackets(version_, config_);
     packets_ = std::move(packets.packets);
     crypto_stream_size_ = packets.crypto_stream_size;
+    QUIC_DLOG(INFO) << "Initialized with " << packets_.size()
+                    << " packets with crypto_stream_size:"
+                    << crypto_stream_size_;
   }
 
   void Initialize(std::unique_ptr<QuicCryptoClientConfig> crypto_config) {
+    tls_chlo_extractor_ = std::make_unique<TlsChloExtractor>();
     AnnotatedPackets packets = GetAnnotatedFirstFlightOfPackets(
         version_, config_, TestConnectionId(), EmptyQuicConnectionId(),
         std::move(crypto_config));
     packets_ = std::move(packets.packets);
     crypto_stream_size_ = packets.crypto_stream_size;
+    QUIC_DLOG(INFO) << "Initialized with " << packets_.size()
+                    << " packets with crypto_stream_size:"
+                    << crypto_stream_size_;
   }
 
   // Perform a full handshake in order to insert a SSL_SESSION into
@@ -107,14 +115,15 @@
           &packet_info.destination_connection_id,
           &packet_info.source_connection_id, &retry_token, &detailed_error);
       ASSERT_THAT(error, IsQuicNoError()) << detailed_error;
-      tls_chlo_extractor_.IngestPacket(packet_info.version, packet_info.packet);
+      tls_chlo_extractor_->IngestPacket(packet_info.version,
+                                        packet_info.packet);
     }
     packets_.clear();
   }
 
   void ValidateChloDetails(const TlsChloExtractor* extractor = nullptr) const {
     if (extractor == nullptr) {
-      extractor = &tls_chlo_extractor_;
+      extractor = tls_chlo_extractor_.get();
     }
 
     EXPECT_TRUE(extractor->HasParsedFullChlo());
@@ -147,7 +156,7 @@
 
   ParsedQuicVersion version_;
   QuicServerId server_id_;
-  TlsChloExtractor tls_chlo_extractor_;
+  std::unique_ptr<TlsChloExtractor> tls_chlo_extractor_;
   QuicConfig config_;
   std::vector<std::unique_ptr<QuicReceivedPacket>> packets_;
   uint64_t crypto_stream_size_;
@@ -162,13 +171,13 @@
   EXPECT_EQ(packets_.size(), 1u);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullSinglePacketChlo);
-  EXPECT_FALSE(tls_chlo_extractor_.resumption_attempted());
-  EXPECT_FALSE(tls_chlo_extractor_.early_data_attempted());
+  EXPECT_FALSE(tls_chlo_extractor_->resumption_attempted());
+  EXPECT_FALSE(tls_chlo_extractor_->early_data_attempted());
 }
 
-TEST_P(TlsChloExtractorTest, TlsExtentionInfo_ResumptionOnly) {
+TEST_P(TlsChloExtractorTest, TlsExtensionInfo_ResumptionOnly) {
   auto crypto_client_config = std::make_unique<QuicCryptoClientConfig>(
       crypto_test_utils::ProofVerifierForTesting(),
       std::make_unique<SimpleSessionCache>());
@@ -179,13 +188,13 @@
   EXPECT_GE(packets_.size(), 1u);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullSinglePacketChlo);
-  EXPECT_TRUE(tls_chlo_extractor_.resumption_attempted());
-  EXPECT_FALSE(tls_chlo_extractor_.early_data_attempted());
+  EXPECT_TRUE(tls_chlo_extractor_->resumption_attempted());
+  EXPECT_FALSE(tls_chlo_extractor_->early_data_attempted());
 }
 
-TEST_P(TlsChloExtractorTest, TlsExtentionInfo_ZeroRtt) {
+TEST_P(TlsChloExtractorTest, TlsExtensionInfo_ZeroRtt) {
   auto crypto_client_config = std::make_unique<QuicCryptoClientConfig>(
       crypto_test_utils::ProofVerifierForTesting(),
       std::make_unique<SimpleSessionCache>());
@@ -196,10 +205,34 @@
   EXPECT_GE(packets_.size(), 1u);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullMultiPacketChlo);
-  EXPECT_TRUE(tls_chlo_extractor_.resumption_attempted());
-  EXPECT_TRUE(tls_chlo_extractor_.early_data_attempted());
+  EXPECT_TRUE(tls_chlo_extractor_->resumption_attempted());
+  EXPECT_TRUE(tls_chlo_extractor_->early_data_attempted());
+}
+
+TEST_P(TlsChloExtractorTest, TlsExtensionInfo_SupportedGroups) {
+  const std::vector<std::vector<uint16_t>> preferred_groups_to_test = {
+      // Only one group
+      {SSL_GROUP_X25519},
+      // Two groups
+      {SSL_GROUP_X25519_KYBER768_DRAFT00, SSL_GROUP_X25519},
+  };
+  for (const std::vector<uint16_t>& preferred_groups :
+       preferred_groups_to_test) {
+    auto crypto_client_config = std::make_unique<QuicCryptoClientConfig>(
+        crypto_test_utils::ProofVerifierForTesting());
+    crypto_client_config->set_preferred_groups(preferred_groups);
+
+    Initialize(std::move(crypto_client_config));
+    IngestPackets();
+    ValidateChloDetails();
+    if (GetQuicReloadableFlag(quic_extract_supported_groups_early)) {
+      EXPECT_EQ(tls_chlo_extractor_->supported_groups(), preferred_groups);
+    } else {
+      EXPECT_TRUE(tls_chlo_extractor_->supported_groups().empty());
+    }
+  }
 }
 
 TEST_P(TlsChloExtractorTest, MultiPacket) {
@@ -208,7 +241,7 @@
   EXPECT_EQ(packets_.size(), 2u);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullMultiPacketChlo);
 }
 
@@ -216,11 +249,11 @@
   IncreaseSizeOfChlo();
   Initialize();
   ASSERT_EQ(packets_.size(), 2u);
-  // Artifically reorder both packets.
+  // Artificially reorder both packets.
   std::swap(packets_[0], packets_[1]);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullMultiPacketChlo);
 }
 
@@ -228,10 +261,10 @@
   Initialize();
   EXPECT_EQ(packets_.size(), 1u);
   TlsChloExtractor other_extractor;
-  tls_chlo_extractor_ = std::move(other_extractor);
+  *tls_chlo_extractor_ = std::move(other_extractor);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullSinglePacketChlo);
 }
 
@@ -240,10 +273,10 @@
   EXPECT_EQ(packets_.size(), 1u);
   IngestPackets();
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullSinglePacketChlo);
 
-  TlsChloExtractor other_extractor = std::move(tls_chlo_extractor_);
+  TlsChloExtractor other_extractor = std::move(*tls_chlo_extractor_);
 
   EXPECT_EQ(other_extractor.state(),
             TlsChloExtractor::State::kParsedFullSinglePacketChlo);
@@ -276,13 +309,13 @@
   EXPECT_EQ(packets_.size(), 1u);
 
   // Move the extractor.
-  tls_chlo_extractor_ = std::move(other_extractor);
+  *tls_chlo_extractor_ = std::move(other_extractor);
 
   // Have |tls_chlo_extractor_| parse the second packet.
   IngestPackets();
 
   ValidateChloDetails();
-  EXPECT_EQ(tls_chlo_extractor_.state(),
+  EXPECT_EQ(tls_chlo_extractor_->state(),
             TlsChloExtractor::State::kParsedFullMultiPacketChlo);
 }