Receive ECN marks on inbound packets and report counts in ACK_ECN frames.
It does not support marking outbound packets except for a minimal capability for test.

QuicPacketReader receives ECN marks from the socket API via QuicUdpPacketInfo.. QuicPacketReader then stores this in a new member of QuicReceivedPacket. This arrives at QuicConnection, which adds to the last_received_packet_info_. When the framer calls QuicConnection::OnPacketHeader(), QuicConnection updates the pending ack frame with the new ECN count via QuicReceivedPacketManager, which updates the counts in the ack frame.

To enable an end-to-end test, this CL also includes minimal changes to mark outbound packets and report the contents of ACK_ECN frames.

QuicConnection::per_packet_options_ is extended to include an ECN codepoint, which QuicDefaultPacketWriter reads, and passes the instruction to the QuicUdpSocketApi via QuicUdpPacketInfo. The test explicitly sets per_packet_options_ to mark ECT(0) after the handshake.

When receiving ACK_ECN, the results are reported to QuicConnection in OnAckFrameEnd(), which then just stores them in a new data structure QuicEcnCounts.

Protected by FLAGS_quic_reloadable_flag_quic_receive_ecn.

PiperOrigin-RevId: 501596181
diff --git a/quiche/quic/core/chlo_extractor.cc b/quiche/quic/core/chlo_extractor.cc
index c3dc584..267823e 100644
--- a/quiche/quic/core/chlo_extractor.cc
+++ b/quiche/quic/core/chlo_extractor.cc
@@ -59,6 +59,7 @@
   bool OnAckRange(QuicPacketNumber start, QuicPacketNumber end) override;
   bool OnAckTimestamp(QuicPacketNumber packet_number,
                       QuicTime timestamp) override;
+  void OnAckEcnCounts(const QuicEcnCounts& ecn_counts) override;
   bool OnAckFrameEnd(QuicPacketNumber start) override;
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override;
   bool OnPingFrame(const QuicPingFrame& frame) override;
@@ -219,6 +220,8 @@
   return true;
 }
 
+void ChloFramerVisitor::OnAckEcnCounts(const QuicEcnCounts& /*ecn_counts*/) {}
+
 bool ChloFramerVisitor::OnAckFrameEnd(QuicPacketNumber /*start*/) {
   return true;
 }
diff --git a/quiche/quic/core/frames/quic_ack_frame.cc b/quiche/quic/core/frames/quic_ack_frame.cc
index cd14d9d..1e42b7d 100644
--- a/quiche/quic/core/frames/quic_ack_frame.cc
+++ b/quiche/quic/core/frames/quic_ack_frame.cc
@@ -42,11 +42,11 @@
     os << p.first << " at " << p.second.ToDebuggingValue() << " ";
   }
   os << " ]";
-  os << ", ecn_counters_populated: " << ack_frame.ecn_counters_populated;
-  if (ack_frame.ecn_counters_populated) {
-    os << ", ect_0_count: " << ack_frame.ect_0_count
-       << ", ect_1_count: " << ack_frame.ect_1_count
-       << ", ecn_ce_count: " << ack_frame.ecn_ce_count;
+  os << ", ecn_counters_populated: " << ack_frame.ecn_counters.has_value();
+  if (ack_frame.ecn_counters.has_value()) {
+    os << ", ect_0_count: " << ack_frame.ecn_counters->ect0
+       << ", ect_1_count: " << ack_frame.ecn_counters->ect1
+       << ", ecn_ce_count: " << ack_frame.ecn_counters->ce;
   }
 
   os << " }\n";
diff --git a/quiche/quic/core/frames/quic_ack_frame.h b/quiche/quic/core/frames/quic_ack_frame.h
index 4a20e66..6828a8c 100644
--- a/quiche/quic/core/frames/quic_ack_frame.h
+++ b/quiche/quic/core/frames/quic_ack_frame.h
@@ -115,12 +115,8 @@
   // Set of packets.
   PacketNumberQueue packets;
 
-  // ECN counters, used only in version 99's ACK frame and valid only when
-  // |ecn_counters_populated| is true.
-  bool ecn_counters_populated = false;
-  QuicPacketCount ect_0_count = 0;
-  QuicPacketCount ect_1_count = 0;
-  QuicPacketCount ecn_ce_count = 0;
+  // ECN counters.
+  absl::optional<QuicEcnCounts> ecn_counters;
 };
 
 // The highest acked packet number we've observed from the peer. If no packets
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index a6e97f9..2524521 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -7116,6 +7116,52 @@
   server_thread_->Resume();
 }
 
+TEST_P(EndToEndTest, EcnMarksReportedCorrectly) {
+  // Client connects using not-ECT.
+  ASSERT_TRUE(Initialize());
+  QuicConnection* client_connection = GetClientConnection();
+  QuicEcnCounts* ecn =
+      QuicConnectionPeer::GetEcnCounts(client_connection, APPLICATION_DATA);
+  EXPECT_EQ(ecn->ect0, 0);
+  EXPECT_EQ(ecn->ect1, 0);
+  EXPECT_EQ(ecn->ce, 0);
+  QuicPacketCount ect0 = 0, ect1 = 0;
+  TestPerPacketOptions options;
+  client_connection->set_per_packet_options(&options);
+  for (QuicEcnCodepoint codepoint : {ECN_NOT_ECT, ECN_ECT0, ECN_ECT1, ECN_CE}) {
+    options.ecn_codepoint = codepoint;
+    client_->SendSynchronousRequest("/foo");
+    if (!GetQuicRestartFlag(quic_receive_ecn) ||
+        !GetQuicRestartFlag(quic_quiche_ecn_sockets) ||
+        !VersionHasIetfQuicFrames(version_.transport_version) ||
+        codepoint == ECN_NOT_ECT) {
+      EXPECT_EQ(ecn->ect0, 0);
+      EXPECT_EQ(ecn->ect1, 0);
+      EXPECT_EQ(ecn->ce, 0);
+      continue;
+    }
+    EXPECT_GT(ecn->ect0, 0);
+    if (codepoint == ECN_CE) {
+      EXPECT_EQ(ect0, ecn->ect0);  // No more ECT(0) arriving
+      EXPECT_GE(ecn->ect1, ect1);  // Late-arriving ECT(1) control packets
+      EXPECT_GT(ecn->ce, 0);
+      continue;
+    }
+    EXPECT_EQ(ecn->ce, 0);
+    if (codepoint == ECN_ECT1) {
+      EXPECT_GE(ecn->ect0, ect0);  // Late-arriving ECT(0) control packets
+      ect0 = ecn->ect0;
+      ect1 = ecn->ect1;
+      EXPECT_GT(ect1, 0);
+      continue;
+    }
+    // codepoint == ECN_ECT0
+    ect0 = ecn->ect0;
+    EXPECT_EQ(ecn->ect1, 0);
+  }
+  client_->Disconnect();
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index f22d8ed..053e33b 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -1328,7 +1328,8 @@
   }
   uber_received_packet_manager_.RecordPacketReceived(
       last_received_packet_info_.decrypted_level,
-      last_received_packet_info_.header, receipt_time);
+      last_received_packet_info_.header, receipt_time,
+      last_received_packet_info_.ecn_codepoint);
   if (EnforceAntiAmplificationLimit() && !IsHandshakeConfirmed() &&
       !header.retry_token.empty() &&
       visitor_->ValidateToken(header.retry_token)) {
@@ -1494,6 +1495,14 @@
   return true;
 }
 
+void QuicConnection::OnAckEcnCounts(const QuicEcnCounts& ecn_counts) {
+  QUIC_DVLOG(1) << ENDPOINT << "OnAckEcnCounts: [" << ecn_counts.ToString()
+                << "]";
+  PacketNumberSpace space = QuicUtils::GetPacketNumberSpace(
+      last_received_packet_info_.decrypted_level);
+  peer_ack_ecn_counts_[space] = ecn_counts;
+}
+
 bool QuicConnection::OnAckFrameEnd(QuicPacketNumber start) {
   QUIC_BUG_IF(quic_bug_12714_7, !connected_)
       << "Processing ACK frame end when connection is closed. Received packet "
@@ -2671,8 +2680,9 @@
   if (debug_visitor_ != nullptr) {
     debug_visitor_->OnPacketReceived(self_address, peer_address, packet);
   }
-  last_received_packet_info_ = ReceivedPacketInfo(
-      self_address, peer_address, packet.receipt_time(), packet.length());
+  last_received_packet_info_ =
+      ReceivedPacketInfo(self_address, peer_address, packet.receipt_time(),
+                         packet.length(), packet.ecn_codepoint());
   current_packet_data_ = packet.data();
 
   if (!default_path_.self_address.IsInitialized()) {
@@ -4807,11 +4817,12 @@
 QuicConnection::ReceivedPacketInfo::ReceivedPacketInfo(
     const QuicSocketAddress& destination_address,
     const QuicSocketAddress& source_address, QuicTime receipt_time,
-    QuicByteCount length)
+    QuicByteCount length, QuicEcnCodepoint ecn_codepoint)
     : destination_address(destination_address),
       source_address(source_address),
       receipt_time(receipt_time),
-      length(length) {}
+      length(length),
+      ecn_codepoint(ecn_codepoint) {}
 
 std::ostream& operator<<(std::ostream& os,
                          const QuicConnection::ReceivedPacketInfo& info) {
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index b71904d..9fb1aaa 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -673,6 +673,7 @@
   bool OnAckRange(QuicPacketNumber start, QuicPacketNumber end) override;
   bool OnAckTimestamp(QuicPacketNumber packet_number,
                       QuicTime timestamp) override;
+  void OnAckEcnCounts(const quic::QuicEcnCounts& ecn_counts) override;
   bool OnAckFrameEnd(QuicPacketNumber start) override;
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override;
   bool OnPaddingFrame(const QuicPaddingFrame& frame) override;
@@ -1482,7 +1483,8 @@
     explicit ReceivedPacketInfo(QuicTime receipt_time);
     ReceivedPacketInfo(const QuicSocketAddress& destination_address,
                        const QuicSocketAddress& source_address,
-                       QuicTime receipt_time, QuicByteCount length);
+                       QuicTime receipt_time, QuicByteCount length,
+                       QuicEcnCodepoint ecn_codepoint);
 
     QuicSocketAddress destination_address;
     QuicSocketAddress source_address;
@@ -1496,6 +1498,7 @@
     EncryptionLevel decrypted_level = ENCRYPTION_INITIAL;
     QuicPacketHeader header;
     absl::InlinedVector<QuicFrameType, 1> frames;
+    QuicEcnCodepoint ecn_codepoint;
   };
 
   QUIC_EXPORT_PRIVATE friend std::ostream& operator<<(
@@ -2316,6 +2319,11 @@
       GetQuicFlag(quic_enforce_strict_amplification_factor);
 
   ConnectionIdGeneratorInterface& connection_id_generator_;
+
+  // Most recent ECN codepoint counts received in ACK_ECN frames sent from the
+  // peer. For now, this is only stored for tests.
+  QuicEcnCounts
+      peer_ack_ecn_counts_[PacketNumberSpace::NUM_PACKET_NUMBER_SPACES];
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index b567587..a3a4144 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -16497,6 +16497,61 @@
   EXPECT_TRUE(alt_path->validated);
 }
 
+TEST_P(QuicConnectionTest, EcnMarksCorrectlyRecorded) {
+  set_perspective(Perspective::IS_SERVER);
+  QuicPacketHeader header = ConstructPacketHeader(1, ENCRYPTION_FORWARD_SECURE);
+  QuicFrames frames;
+  QuicPingFrame ping_frame;
+  QuicPaddingFrame padding_frame;
+  frames.push_back(QuicFrame(ping_frame));
+  frames.push_back(QuicFrame(padding_frame));
+  std::unique_ptr<QuicPacket> packet =
+      BuildUnsizedDataPacket(&peer_framer_, header, frames);
+  char buffer[kMaxOutgoingPacketSize];
+  size_t encrypted_length = peer_framer_.EncryptPayload(
+      ENCRYPTION_FORWARD_SECURE, QuicPacketNumber(1), *packet, buffer,
+      kMaxOutgoingPacketSize);
+  QuicReceivedPacket received_packet(buffer, encrypted_length, clock_.Now(),
+                                     false, 0, true, nullptr, 0, false,
+                                     ECN_ECT0);
+  if (connection_.SupportsMultiplePacketNumberSpaces()) {
+    EXPECT_FALSE(connection_.received_packet_manager()
+                     .GetAckFrame(APPLICATION_DATA)
+                     .ecn_counters.has_value());
+    connection_.ProcessUdpPacket(kSelfAddress, kPeerAddress, received_packet);
+    if (GetQuicRestartFlag(quic_receive_ecn)) {
+      EXPECT_TRUE(connection_.received_packet_manager()
+                      .GetAckFrame(APPLICATION_DATA)
+                      .ecn_counters.has_value());
+      EXPECT_EQ(connection_.received_packet_manager()
+                    .GetAckFrame(APPLICATION_DATA)
+                    .ecn_counters->ect0,
+                1);
+    } else {
+      EXPECT_FALSE(connection_.received_packet_manager()
+                       .GetAckFrame(APPLICATION_DATA)
+                       .ecn_counters.has_value());
+    }
+  } else {
+    EXPECT_FALSE(connection_.received_packet_manager()
+                     .ack_frame()
+                     .ecn_counters.has_value());
+    connection_.ProcessUdpPacket(kSelfAddress, kPeerAddress, received_packet);
+    if (GetQuicRestartFlag(quic_receive_ecn)) {
+      EXPECT_TRUE(connection_.received_packet_manager()
+                      .ack_frame()
+                      .ecn_counters.has_value());
+      EXPECT_EQ(
+          connection_.received_packet_manager().ack_frame().ecn_counters->ect0,
+          1);
+    } else {
+      EXPECT_FALSE(connection_.received_packet_manager()
+                       .ack_frame()
+                       .ecn_counters.has_value());
+    }
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_default_packet_writer.cc b/quiche/quic/core/quic_default_packet_writer.cc
index 3ba64b0..78feee5 100644
--- a/quiche/quic/core/quic_default_packet_writer.cc
+++ b/quiche/quic/core/quic_default_packet_writer.cc
@@ -17,11 +17,12 @@
     const char* buffer, size_t buf_len, const QuicIpAddress& self_address,
     const QuicSocketAddress& peer_address, PerPacketOptions* options) {
   QUICHE_DCHECK(!write_blocked_);
-  QUICHE_DCHECK(nullptr == options)
-      << "QuicDefaultPacketWriter does not accept any options.";
   QuicUdpPacketInfo packet_info;
   packet_info.SetPeerAddress(peer_address);
   packet_info.SetSelfIp(self_address);
+  if (options != nullptr) {
+    packet_info.SetEcnCodepoint(options->ecn_codepoint);
+  }
   WriteResult result =
       QuicUdpSocketApi().WritePacket(fd_, buffer, buf_len, packet_info);
   if (IsWriteBlockedStatus(result.status)) {
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index b205e3e..d9333b2 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -87,6 +87,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_default_to_bbr, false)
 // When true, quiche UDP sockets report Explicit Congestion Notification (ECN) [RFC3168, RFC9330] results.
 QUIC_FLAG(quic_restart_flag_quic_quiche_ecn_sockets, false)
+// When true, report received ECN markings to the peer.
+QUIC_FLAG(quic_restart_flag_quic_receive_ecn, false)
 // When true, support draft-ietf-quic-v2-08
 QUIC_FLAG(quic_reloadable_flag_quic_enable_version_2_draft_08, false)
 // When true, the BB2U copt causes BBR2 to wait two rounds with out draining the queue before exiting PROBE_UP and BB2S has the same effect in STARTUP.
diff --git a/quiche/quic/core/quic_framer.cc b/quiche/quic/core/quic_framer.cc
index b362c51..8a506fd 100644
--- a/quiche/quic/core/quic_framer.cc
+++ b/quiche/quic/core/quic_framer.cc
@@ -22,6 +22,7 @@
 #include "absl/strings/str_cat.h"
 #include "absl/strings/str_split.h"
 #include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
 #include "quiche/quic/core/crypto/crypto_framer.h"
 #include "quiche/quic/core/crypto/crypto_handshake.h"
 #include "quiche/quic/core/crypto/crypto_handshake_message.h"
@@ -392,6 +393,16 @@
                       ":", initial_error_string);
 }
 
+// Return the minimum size of the ECN fields in an ACK frame
+size_t AckEcnCountSize(const QuicAckFrame& ack_frame) {
+  if (!ack_frame.ecn_counters.has_value()) {
+    return 0;
+  }
+  return (QuicDataWriter::GetVarInt62Len(ack_frame.ecn_counters->ect0) +
+          QuicDataWriter::GetVarInt62Len(ack_frame.ecn_counters->ect1) +
+          QuicDataWriter::GetVarInt62Len(ack_frame.ecn_counters->ce));
+}
+
 }  // namespace
 
 QuicFramer::QuicFramer(const ParsedQuicVersionVector& supported_versions,
@@ -499,13 +510,8 @@
     if (use_ietf_ack_with_receive_timestamp) {
       // 0 Timestamp Range Count.
       min_size += QuicDataWriter::GetVarInt62Len(0);
-    } else if (ack_frame.ecn_counters_populated &&
-               (ack_frame.ect_0_count || ack_frame.ect_1_count ||
-                ack_frame.ecn_ce_count)) {
-      // ECN counts.
-      min_size += (QuicDataWriter::GetVarInt62Len(ack_frame.ect_0_count) +
-                   QuicDataWriter::GetVarInt62Len(ack_frame.ect_1_count) +
-                   QuicDataWriter::GetVarInt62Len(ack_frame.ecn_ce_count));
+    } else {
+      min_size += AckEcnCountSize(ack_frame);
     }
     return min_size;
   }
@@ -4113,32 +4119,32 @@
     ack_block_count--;
   }
 
+  QUICHE_DCHECK(!ack_frame->ecn_counters.has_value());
   if (frame_type == IETF_ACK_RECEIVE_TIMESTAMPS) {
     QUICHE_DCHECK(process_timestamps_);
     if (!ProcessIetfTimestampsInAckFrame(ack_frame->largest_acked, reader)) {
       return false;
     }
   } else if (frame_type == IETF_ACK_ECN) {
-    ack_frame->ecn_counters_populated = true;
-    if (!reader->ReadVarInt62(&ack_frame->ect_0_count)) {
+    ack_frame->ecn_counters = QuicEcnCounts();
+    if (!reader->ReadVarInt62(&ack_frame->ecn_counters->ect0)) {
       set_detailed_error("Unable to read ack ect_0_count.");
       return false;
     }
-    if (!reader->ReadVarInt62(&ack_frame->ect_1_count)) {
+    if (!reader->ReadVarInt62(&ack_frame->ecn_counters->ect1)) {
       set_detailed_error("Unable to read ack ect_1_count.");
       return false;
     }
-    if (!reader->ReadVarInt62(&ack_frame->ecn_ce_count)) {
+    if (!reader->ReadVarInt62(&ack_frame->ecn_counters->ce)) {
       set_detailed_error("Unable to read ack ecn_ce_count.");
       return false;
     }
-  } else {
-    ack_frame->ecn_counters_populated = false;
-    ack_frame->ect_0_count = 0;
-    ack_frame->ect_1_count = 0;
-    ack_frame->ecn_ce_count = 0;
+    if (GetQuicRestartFlag(quic_receive_ecn)) {
+      QUIC_RESTART_FLAG_COUNT_N(quic_receive_ecn, 2, 3);
+      visitor_->OnAckEcnCounts(*ack_frame->ecn_counters);
+    }
   }
-  // TODO(fayang): Report ECN counts to visitor when they are actually used.
+
   if (!visitor_->OnAckFrameEnd(QuicPacketNumber(block_low))) {
     set_detailed_error(
         "Error occurs when visitor finishes processing the ACK frame.");
@@ -5103,12 +5109,8 @@
 
   if (UseIetfAckWithReceiveTimestamp(frame)) {
     ack_frame_size += GetIetfAckFrameTimestampSize(frame);
-  } else if (frame.ecn_counters_populated &&
-             (frame.ect_0_count || frame.ect_1_count || frame.ecn_ce_count)) {
-    // ECN counts.
-    ack_frame_size += QuicDataWriter::GetVarInt62Len(frame.ect_0_count);
-    ack_frame_size += QuicDataWriter::GetVarInt62Len(frame.ect_1_count);
-    ack_frame_size += QuicDataWriter::GetVarInt62Len(frame.ecn_ce_count);
+  } else {
+    ack_frame_size += AckEcnCountSize(frame);
   }
 
   return ack_frame_size;
@@ -6040,13 +6042,10 @@
   uint64_t ecn_size = 0;
   if (UseIetfAckWithReceiveTimestamp(frame)) {
     type = IETF_ACK_RECEIVE_TIMESTAMPS;
-  } else if (frame.ecn_counters_populated &&
-             (frame.ect_0_count || frame.ect_1_count || frame.ecn_ce_count)) {
+  } else if (frame.ecn_counters.has_value()) {
     // Change frame type to ACK_ECN if any ECN count is available.
     type = IETF_ACK_ECN;
-    ecn_size = (QuicDataWriter::GetVarInt62Len(frame.ect_0_count) +
-                QuicDataWriter::GetVarInt62Len(frame.ect_1_count) +
-                QuicDataWriter::GetVarInt62Len(frame.ecn_ce_count));
+    ecn_size = AckEcnCountSize(frame);
   }
 
   if (!writer->WriteVarInt62(type)) {
@@ -6141,15 +6140,15 @@
 
   if (type == IETF_ACK_ECN) {
     // Encode the ECN counts.
-    if (!writer->WriteVarInt62(frame.ect_0_count)) {
+    if (!writer->WriteVarInt62(frame.ecn_counters->ect0)) {
       set_detailed_error("No room for ect_0_count in ack frame");
       return false;
     }
-    if (!writer->WriteVarInt62(frame.ect_1_count)) {
+    if (!writer->WriteVarInt62(frame.ecn_counters->ect1)) {
       set_detailed_error("No room for ect_1_count in ack frame");
       return false;
     }
-    if (!writer->WriteVarInt62(frame.ecn_ce_count)) {
+    if (!writer->WriteVarInt62(frame.ecn_counters->ce)) {
       set_detailed_error("No room for ecn_ce_count in ack frame");
       return false;
     }
diff --git a/quiche/quic/core/quic_framer.h b/quiche/quic/core/quic_framer.h
index 3f1d011..5b0e8e2 100644
--- a/quiche/quic/core/quic_framer.h
+++ b/quiche/quic/core/quic_framer.h
@@ -164,6 +164,10 @@
   virtual bool OnAckTimestamp(QuicPacketNumber packet_number,
                               QuicTime timestamp) = 0;
 
+  // Called when an ACK frame arrives that includes Explicit Congestion
+  // Notification (ECN) packet counts.
+  virtual void OnAckEcnCounts(const QuicEcnCounts& ecn_counts) = 0;
+
   // Called after the last ack range in an AckFrame has been parsed.
   // |start| is the starting value of the last ack range.
   virtual bool OnAckFrameEnd(QuicPacketNumber start) = 0;
diff --git a/quiche/quic/core/quic_framer_test.cc b/quiche/quic/core/quic_framer_test.cc
index d2eae77..2c82bcb 100644
--- a/quiche/quic/core/quic_framer_test.cc
+++ b/quiche/quic/core/quic_framer_test.cc
@@ -51,6 +51,7 @@
 const uint64_t kMask = kEpoch - 1;
 const uint8_t kPacket0ByteConnectionId = 0;
 const uint8_t kPacket8ByteConnectionId = 8;
+constexpr size_t kTagSize = 16;
 
 const StatelessResetToken kTestStatelessResetToken{
     0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
@@ -396,6 +397,8 @@
     return true;
   }
 
+  void OnAckEcnCounts(const QuicEcnCounts& /*ecn_counts*/) override {}
+
   bool OnAckFrameEnd(QuicPacketNumber /*start*/) override { return true; }
 
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override {
@@ -10646,10 +10649,7 @@
   ack_frame = MakeAckFrameWithGaps(/*gap_size=*/0xffffffff,
                                    /*max_num_gaps=*/200,
                                    /*largest_acked=*/kMaxIetfVarInt);
-  ack_frame.ecn_counters_populated = true;
-  ack_frame.ect_0_count = 100;
-  ack_frame.ect_1_count = 10000;
-  ack_frame.ecn_ce_count = 1000000;
+  ack_frame.ecn_counters = QuicEcnCounts(100, 10000, 1000000);
   QuicFrames frames = {QuicFrame(&ack_frame)};
   // Build an ACK packet.
   QuicFramerPeer::SetPerspective(&framer_, Perspective::IS_CLIENT);
@@ -16480,6 +16480,67 @@
   EXPECT_EQ(detailed_error, "");
 }
 
+TEST_P(QuicFramerTest, ReportEcnCountsIfPresent) {
+  if (!VersionHasIetfQuicFrames(framer_.transport_version())) {
+    return;
+  }
+  SetDecrypterLevel(ENCRYPTION_FORWARD_SECURE);
+
+  QuicPacketHeader header;
+  header.destination_connection_id = FramerTestConnectionId();
+  header.reset_flag = false;
+  header.version_flag = false;
+  header.packet_number = kPacketNumber;
+
+  for (bool ecn_marks : { false, true }) {
+    // Add some padding, because TestEncrypter doesn't add an authentication
+    // tag. For a small packet, this will cause QuicFramer to fail to get a
+    // header protection sample.
+    QuicPaddingFrame padding_frame(kTagSize);
+    // Create a packet with just an ack.
+    QuicAckFrame ack_frame = InitAckFrame(5);
+    if (ecn_marks) {
+      ack_frame.ecn_counters = QuicEcnCounts(100, 10000, 1000000);
+    } else {
+      ack_frame.ecn_counters = absl::nullopt;
+    }
+    QuicFrames frames = {QuicFrame(padding_frame), QuicFrame(&ack_frame)};
+    // Build an ACK packet.
+    QuicFramerPeer::SetPerspective(&framer_, Perspective::IS_CLIENT);
+    std::unique_ptr<QuicPacket> raw_ack_packet(BuildDataPacket(header, frames));
+    ASSERT_TRUE(raw_ack_packet != nullptr);
+    char buffer[kMaxOutgoingPacketSize];
+    size_t encrypted_length =
+        framer_.EncryptPayload(ENCRYPTION_INITIAL, header.packet_number,
+                               *raw_ack_packet, buffer, kMaxOutgoingPacketSize);
+    ASSERT_NE(0u, encrypted_length);
+    // Now make sure we can turn our ack packet back into an ack frame.
+    QuicFramerPeer::SetPerspective(&framer_, Perspective::IS_SERVER);
+    MockFramerVisitor visitor;
+    framer_.set_visitor(&visitor);
+    EXPECT_CALL(visitor, OnPacket()).Times(1);
+    EXPECT_CALL(visitor, OnUnauthenticatedPublicHeader(_))
+        .Times(1)
+        .WillOnce(Return(true));
+    EXPECT_CALL(visitor, OnUnauthenticatedHeader(_))
+        .Times(1)
+        .WillOnce(Return(true));
+    EXPECT_CALL(visitor, OnPacketHeader(_)).Times(1);
+    EXPECT_CALL(visitor, OnDecryptedPacket(_, _)).Times(1);
+    EXPECT_CALL(visitor, OnAckFrameStart(_, _)).Times(1).WillOnce(Return(true));
+    EXPECT_CALL(visitor, OnAckRange(_, _)).Times(1).WillOnce(Return(true));
+    if (GetQuicRestartFlag(quic_receive_ecn) && ecn_marks) {
+      EXPECT_CALL(visitor, OnAckEcnCounts(_)).Times(1);
+    } else {
+      EXPECT_CALL(visitor, OnAckEcnCounts(_)).Times(0);
+    }
+    EXPECT_CALL(visitor, OnAckFrameEnd(_)).Times(1).WillOnce(Return(true));
+    EXPECT_CALL(visitor, OnPacketComplete()).Times(1);
+    ASSERT_TRUE(framer_.ProcessPacket(
+                    QuicEncryptedPacket(buffer, encrypted_length, false)));
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_packet_reader.cc b/quiche/quic/core/quic_packet_reader.cc
index ec72824..eaa3441 100644
--- a/quiche/quic/core/quic_packet_reader.cc
+++ b/quiche/quic/core/quic_packet_reader.cc
@@ -7,6 +7,8 @@
 #include "absl/base/macros.h"
 #include "quiche/quic/core/quic_packets.h"
 #include "quiche/quic/core/quic_process_packet_interface.h"
+#include "quiche/quic/core/quic_udp_socket.h"
+#include "quiche/quic/core/quic_utils.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"
@@ -47,15 +49,19 @@
   // arriving at the host and now is considered part of the network delay.
   QuicTime now = clock.Now();
 
-  size_t packets_read = socket_api_.ReadMultiplePackets(
-      fd,
-      BitMask64(QuicUdpPacketInfoBit::DROPPED_PACKETS,
-                QuicUdpPacketInfoBit::PEER_ADDRESS,
-                QuicUdpPacketInfoBit::V4_SELF_IP,
-                QuicUdpPacketInfoBit::V6_SELF_IP,
-                QuicUdpPacketInfoBit::RECV_TIMESTAMP, QuicUdpPacketInfoBit::TTL,
-                QuicUdpPacketInfoBit::GOOGLE_PACKET_HEADER),
-      &read_results_);
+  BitMask64 info_bits{QuicUdpPacketInfoBit::DROPPED_PACKETS,
+                      QuicUdpPacketInfoBit::PEER_ADDRESS,
+                      QuicUdpPacketInfoBit::V4_SELF_IP,
+                      QuicUdpPacketInfoBit::V6_SELF_IP,
+                      QuicUdpPacketInfoBit::RECV_TIMESTAMP,
+                      QuicUdpPacketInfoBit::TTL,
+                      QuicUdpPacketInfoBit::GOOGLE_PACKET_HEADER};
+  if (GetQuicRestartFlag(quic_receive_ecn)) {
+    QUIC_RESTART_FLAG_COUNT_N(quic_receive_ecn, 3, 3);
+    info_bits.Set(QuicUdpPacketInfoBit::ECN);
+  }
+  size_t packets_read =
+      socket_api_.ReadMultiplePackets(fd, info_bits, &read_results_);
   for (size_t i = 0; i < packets_read; ++i) {
     auto& result = read_results_[i];
     if (!result.ok) {
@@ -97,8 +103,7 @@
     QuicReceivedPacket packet(
         result.packet_buffer.buffer, result.packet_buffer.buffer_len, now,
         /*owns_buffer=*/false, ttl, has_ttl, headers, headers_length,
-        /*owns_header_buffer=*/false);
-
+        /*owns_header_buffer=*/false, result.packet_info.ecn_codepoint());
     QuicSocketAddress self_address(self_ip, port);
     processor->ProcessPacket(self_address, peer_address, packet);
   }
diff --git a/quiche/quic/core/quic_packet_writer.h b/quiche/quic/core/quic_packet_writer.h
index 95d167b..5ebbfe0 100644
--- a/quiche/quic/core/quic_packet_writer.h
+++ b/quiche/quic/core/quic_packet_writer.h
@@ -34,6 +34,8 @@
   QuicTime::Delta release_time_delay = QuicTime::Delta::Zero();
   // Whether it is allowed to send this packet without |release_time_delay|.
   bool allow_burst = false;
+  // ECN codepoint to use when sending this packet.
+  QuicEcnCodepoint ecn_codepoint;
 };
 
 // An interface between writers and the entity managing the
diff --git a/quiche/quic/core/quic_packets.cc b/quiche/quic/core/quic_packets.cc
index b0f7535..d884adf 100644
--- a/quiche/quic/core/quic_packets.cc
+++ b/quiche/quic/core/quic_packets.cc
@@ -344,7 +344,7 @@
     : quic::QuicReceivedPacket(buffer, length, receipt_time, owns_buffer, ttl,
                                ttl_valid, nullptr /* packet_headers */,
                                0 /* headers_length */,
-                               false /* owns_header_buffer */) {}
+                               false /* owns_header_buffer */, ECN_NOT_ECT) {}
 
 QuicReceivedPacket::QuicReceivedPacket(const char* buffer, size_t length,
                                        QuicTime receipt_time, bool owns_buffer,
@@ -352,12 +352,21 @@
                                        char* packet_headers,
                                        size_t headers_length,
                                        bool owns_header_buffer)
+    : quic::QuicReceivedPacket(buffer, length, receipt_time, owns_buffer, ttl,
+                               ttl_valid, packet_headers, headers_length,
+                               owns_header_buffer, ECN_NOT_ECT) {}
+
+QuicReceivedPacket::QuicReceivedPacket(
+    const char* buffer, size_t length, QuicTime receipt_time, bool owns_buffer,
+    int ttl, bool ttl_valid, char* packet_headers, size_t headers_length,
+    bool owns_header_buffer, QuicEcnCodepoint ecn_codepoint)
     : QuicEncryptedPacket(buffer, length, owns_buffer),
       receipt_time_(receipt_time),
       ttl_(ttl_valid ? ttl : -1),
       packet_headers_(packet_headers),
       headers_length_(headers_length),
-      owns_header_buffer_(owns_header_buffer) {}
+      owns_header_buffer_(owns_header_buffer),
+      ecn_codepoint_(ecn_codepoint) {}
 
 QuicReceivedPacket::~QuicReceivedPacket() {
   if (owns_header_buffer_) {
diff --git a/quiche/quic/core/quic_packets.h b/quiche/quic/core/quic_packets.h
index 263bdb9..a1c743a 100644
--- a/quiche/quic/core/quic_packets.h
+++ b/quiche/quic/core/quic_packets.h
@@ -291,6 +291,10 @@
                      bool owns_buffer, int ttl, bool ttl_valid,
                      char* packet_headers, size_t headers_length,
                      bool owns_header_buffer);
+  QuicReceivedPacket(const char* buffer, size_t length, QuicTime receipt_time,
+                     bool owns_buffer, int ttl, bool ttl_valid,
+                     char* packet_headers, size_t headers_length,
+                     bool owns_header_buffer, QuicEcnCodepoint ecn_codepoint);
   ~QuicReceivedPacket();
   QuicReceivedPacket(const QuicReceivedPacket&) = delete;
   QuicReceivedPacket& operator=(const QuicReceivedPacket&) = delete;
@@ -317,6 +321,8 @@
   QUIC_EXPORT_PRIVATE friend std::ostream& operator<<(
       std::ostream& os, const QuicReceivedPacket& s);
 
+  QuicEcnCodepoint ecn_codepoint() const { return ecn_codepoint_; }
+
  private:
   const QuicTime receipt_time_;
   int ttl_;
@@ -326,6 +332,7 @@
   int headers_length_;
   // Whether owns the buffer for packet headers.
   bool owns_header_buffer_;
+  QuicEcnCodepoint ecn_codepoint_;
 };
 
 // SerializedPacket contains information of a serialized(encrypted) packet.
diff --git a/quiche/quic/core/quic_received_packet_manager.cc b/quiche/quic/core/quic_received_packet_manager.cc
index bb7b87d..0486618 100644
--- a/quiche/quic/core/quic_received_packet_manager.cc
+++ b/quiche/quic/core/quic_received_packet_manager.cc
@@ -10,7 +10,9 @@
 
 #include "quiche/quic/core/congestion_control/rtt_stats.h"
 #include "quiche/quic/core/crypto/crypto_protocol.h"
+#include "quiche/quic/core/quic_config.h"
 #include "quiche/quic/core/quic_connection_stats.h"
+#include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_bug_tracker.h"
 #include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/quic/platform/api/quic_logging.h"
@@ -70,7 +72,8 @@
 }
 
 void QuicReceivedPacketManager::RecordPacketReceived(
-    const QuicPacketHeader& header, QuicTime receipt_time) {
+    const QuicPacketHeader& header, QuicTime receipt_time,
+    const QuicEcnCodepoint ecn) {
   const QuicPacketNumber packet_number = header.packet_number;
   QUICHE_DCHECK(IsAwaitingPacket(packet_number))
       << " packet_number:" << packet_number;
@@ -119,6 +122,27 @@
     }
   }
 
+  if (GetQuicRestartFlag(quic_receive_ecn) && ecn != ECN_NOT_ECT) {
+    QUIC_RESTART_FLAG_COUNT_N(quic_receive_ecn, 1, 3);
+    if (!ack_frame_.ecn_counters.has_value()) {
+      ack_frame_.ecn_counters = QuicEcnCounts();
+    }
+    switch (ecn) {
+      case ECN_NOT_ECT:
+        QUICHE_NOTREACHED();
+        break;  // It's impossible to get here, but the compiler complains.
+      case ECN_ECT0:
+        ack_frame_.ecn_counters->ect0++;
+        break;
+      case ECN_ECT1:
+        ack_frame_.ecn_counters->ect1++;
+        break;
+      case ECN_CE:
+        ack_frame_.ecn_counters->ce++;
+        break;
+    }
+  }
+
   if (least_received_packet_number_.IsInitialized()) {
     least_received_packet_number_ =
         std::min(least_received_packet_number_, packet_number);
diff --git a/quiche/quic/core/quic_received_packet_manager.h b/quiche/quic/core/quic_received_packet_manager.h
index d13e09b..ab298d4 100644
--- a/quiche/quic/core/quic_received_packet_manager.h
+++ b/quiche/quic/core/quic_received_packet_manager.h
@@ -11,6 +11,7 @@
 #include "quiche/quic/core/quic_config.h"
 #include "quiche/quic/core/quic_framer.h"
 #include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_export.h"
 
 namespace quic {
@@ -41,7 +42,8 @@
   // header: the packet header.
   // timestamp: the arrival time of the packet.
   virtual void RecordPacketReceived(const QuicPacketHeader& header,
-                                    QuicTime receipt_time);
+                                    QuicTime receipt_time,
+                                    QuicEcnCodepoint ecn);
 
   // Checks whether |packet_number| is missing and less than largest observed.
   virtual bool IsMissing(QuicPacketNumber packet_number);
diff --git a/quiche/quic/core/quic_received_packet_manager_test.cc b/quiche/quic/core/quic_received_packet_manager_test.cc
index c453707..143654e 100644
--- a/quiche/quic/core/quic_received_packet_manager_test.cc
+++ b/quiche/quic/core/quic_received_packet_manager_test.cc
@@ -13,6 +13,7 @@
 #include "quiche/quic/core/crypto/crypto_protocol.h"
 #include "quiche/quic/core/quic_connection_stats.h"
 #include "quiche/quic/core/quic_constants.h"
+#include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_expect_bug.h"
 #include "quiche/quic/platform/api/quic_flags.h"
 #include "quiche/quic/platform/api/quic_test.h"
@@ -54,9 +55,14 @@
   }
 
   void RecordPacketReceipt(uint64_t packet_number, QuicTime receipt_time) {
+    RecordPacketReceipt(packet_number, receipt_time, ECN_NOT_ECT);
+  }
+
+  void RecordPacketReceipt(uint64_t packet_number, QuicTime receipt_time,
+                           QuicEcnCodepoint ecn_codepoint) {
     QuicPacketHeader header;
     header.packet_number = QuicPacketNumber(packet_number);
-    received_manager_.RecordPacketReceived(header, receipt_time);
+    received_manager_.RecordPacketReceived(header, receipt_time, ecn_codepoint);
   }
 
   bool HasPendingAck() {
@@ -91,9 +97,9 @@
 TEST_F(QuicReceivedPacketManagerTest, DontWaitForPacketsBefore) {
   QuicPacketHeader header;
   header.packet_number = QuicPacketNumber(2u);
-  received_manager_.RecordPacketReceived(header, QuicTime::Zero());
+  received_manager_.RecordPacketReceived(header, QuicTime::Zero(), ECN_NOT_ECT);
   header.packet_number = QuicPacketNumber(7u);
-  received_manager_.RecordPacketReceived(header, QuicTime::Zero());
+  received_manager_.RecordPacketReceived(header, QuicTime::Zero(), ECN_NOT_ECT);
   EXPECT_TRUE(received_manager_.IsAwaitingPacket(QuicPacketNumber(3u)));
   EXPECT_TRUE(received_manager_.IsAwaitingPacket(QuicPacketNumber(6u)));
   received_manager_.DontWaitForPacketsBefore(QuicPacketNumber(4));
@@ -106,7 +112,7 @@
   header.packet_number = QuicPacketNumber(2u);
   QuicTime two_ms = QuicTime::Zero() + QuicTime::Delta::FromMilliseconds(2);
   EXPECT_FALSE(received_manager_.ack_frame_updated());
-  received_manager_.RecordPacketReceived(header, two_ms);
+  received_manager_.RecordPacketReceived(header, two_ms, ECN_NOT_ECT);
   EXPECT_TRUE(received_manager_.ack_frame_updated());
 
   QuicFrame ack = received_manager_.GetUpdatedAckFrame(QuicTime::Zero());
@@ -129,11 +135,11 @@
   EXPECT_EQ(1u, ack.ack_frame->received_packet_times.size());
 
   header.packet_number = QuicPacketNumber(999u);
-  received_manager_.RecordPacketReceived(header, two_ms);
+  received_manager_.RecordPacketReceived(header, two_ms, ECN_NOT_ECT);
   header.packet_number = QuicPacketNumber(4u);
-  received_manager_.RecordPacketReceived(header, two_ms);
+  received_manager_.RecordPacketReceived(header, two_ms, ECN_NOT_ECT);
   header.packet_number = QuicPacketNumber(1000u);
-  received_manager_.RecordPacketReceived(header, two_ms);
+  received_manager_.RecordPacketReceived(header, two_ms, ECN_NOT_ECT);
   EXPECT_TRUE(received_manager_.ack_frame_updated());
   ack = received_manager_.GetUpdatedAckFrame(two_ms);
   received_manager_.ResetAckStates();
@@ -676,6 +682,23 @@
   CheckAckTimeout(clock_.ApproximateNow());
 }
 
+TEST_F(QuicReceivedPacketManagerTest, CountEcnPackets) {
+  EXPECT_FALSE(HasPendingAck());
+  RecordPacketReceipt(3, QuicTime::Zero(), ECN_NOT_ECT);
+  RecordPacketReceipt(4, QuicTime::Zero(), ECN_ECT0);
+  RecordPacketReceipt(5, QuicTime::Zero(), ECN_ECT1);
+  RecordPacketReceipt(6, QuicTime::Zero(), ECN_CE);
+  QuicFrame ack = received_manager_.GetUpdatedAckFrame(QuicTime::Zero());
+  if (GetQuicRestartFlag(quic_receive_ecn)) {
+    EXPECT_TRUE(ack.ack_frame->ecn_counters.has_value());
+    EXPECT_EQ(ack.ack_frame->ecn_counters->ect0, 1);
+    EXPECT_EQ(ack.ack_frame->ecn_counters->ect1, 1);
+    EXPECT_EQ(ack.ack_frame->ecn_counters->ce, 1);
+  } else {
+    EXPECT_FALSE(ack.ack_frame->ecn_counters.has_value());
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index f4a2394..fac4f38 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -878,6 +878,25 @@
   ECN_CE = 3,
 };
 
+// This struct reports the Explicit Congestion Notification (ECN) contents of
+// the ACK_ECN frame. They are the cumulative number of QUIC packets received
+// for that codepoint in a given Packet Number Space.
+struct QUIC_EXPORT_PRIVATE QuicEcnCounts {
+  QuicEcnCounts() = default;
+  QuicEcnCounts(QuicPacketCount ect0, QuicPacketCount ect1, QuicPacketCount ce)
+      : ect0(ect0), ect1(ect1), ce(ce) {}
+
+  std::string ToString() const {
+    return absl::StrFormat("ECT(0): %s, ECT(1): %s, CE: %s",
+                           std::to_string(ect0), std::to_string(ect1),
+                           std::to_string(ce));
+  }
+
+  QuicPacketCount ect0 = 0;
+  QuicPacketCount ect1 = 0;
+  QuicPacketCount ce = 0;
+};
+
 }  // namespace quic
 
 #endif  // QUICHE_QUIC_CORE_QUIC_TYPES_H_
diff --git a/quiche/quic/core/tls_chlo_extractor.h b/quiche/quic/core/tls_chlo_extractor.h
index b48065f..4173953 100644
--- a/quiche/quic/core/tls_chlo_extractor.h
+++ b/quiche/quic/core/tls_chlo_extractor.h
@@ -112,6 +112,7 @@
                       QuicTime /*timestamp*/) override {
     return true;
   }
+  void OnAckEcnCounts(const QuicEcnCounts& /*ecn_counts*/) override {}
   bool OnAckFrameEnd(QuicPacketNumber /*start*/) override { return true; }
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& /*frame*/) override {
     return true;
diff --git a/quiche/quic/core/uber_received_packet_manager.cc b/quiche/quic/core/uber_received_packet_manager.cc
index 2456795..4efe788 100644
--- a/quiche/quic/core/uber_received_packet_manager.cc
+++ b/quiche/quic/core/uber_received_packet_manager.cc
@@ -48,14 +48,15 @@
 
 void UberReceivedPacketManager::RecordPacketReceived(
     EncryptionLevel decrypted_packet_level, const QuicPacketHeader& header,
-    QuicTime receipt_time) {
+    QuicTime receipt_time, QuicEcnCodepoint ecn_codepoint) {
   if (!supports_multiple_packet_number_spaces_) {
-    received_packet_managers_[0].RecordPacketReceived(header, receipt_time);
+    received_packet_managers_[0].RecordPacketReceived(header, receipt_time,
+                                                      ecn_codepoint);
     return;
   }
   received_packet_managers_[QuicUtils::GetPacketNumberSpace(
                                 decrypted_packet_level)]
-      .RecordPacketReceived(header, receipt_time);
+      .RecordPacketReceived(header, receipt_time, ecn_codepoint);
 }
 
 void UberReceivedPacketManager::DontWaitForPacketsBefore(
diff --git a/quiche/quic/core/uber_received_packet_manager.h b/quiche/quic/core/uber_received_packet_manager.h
index 1eb15d9..0e436c0 100644
--- a/quiche/quic/core/uber_received_packet_manager.h
+++ b/quiche/quic/core/uber_received_packet_manager.h
@@ -31,7 +31,8 @@
   // been parsed.
   void RecordPacketReceived(EncryptionLevel decrypted_packet_level,
                             const QuicPacketHeader& header,
-                            QuicTime receipt_time);
+                            QuicTime receipt_time,
+                            QuicEcnCodepoint ecn_codepoint);
 
   // Retrieves a frame containing a QuicAckFrame. The ack frame must be
   // serialized before another packet is received, or it will change.
diff --git a/quiche/quic/core/uber_received_packet_manager_test.cc b/quiche/quic/core/uber_received_packet_manager_test.cc
index ad62d8f..97b2a80 100644
--- a/quiche/quic/core/uber_received_packet_manager_test.cc
+++ b/quiche/quic/core/uber_received_packet_manager_test.cc
@@ -74,8 +74,8 @@
                            uint64_t packet_number, QuicTime receipt_time) {
     QuicPacketHeader header;
     header.packet_number = QuicPacketNumber(packet_number);
-    manager_->RecordPacketReceived(decrypted_packet_level, header,
-                                   receipt_time);
+    manager_->RecordPacketReceived(decrypted_packet_level, header, receipt_time,
+                                   ECN_NOT_ECT);
   }
 
   bool HasPendingAck() {
diff --git a/quiche/quic/test_tools/quic_connection_peer.cc b/quiche/quic/test_tools/quic_connection_peer.cc
index b33bc29..ad58d9c 100644
--- a/quiche/quic/test_tools/quic_connection_peer.cc
+++ b/quiche/quic/test_tools/quic_connection_peer.cc
@@ -576,5 +576,11 @@
   return connection->server_preferred_address_;
 }
 
+// static
+QuicEcnCounts* QuicConnectionPeer::GetEcnCounts(
+    QuicConnection* connection, PacketNumberSpace packet_number_space) {
+  return &connection->peer_ack_ecn_counts_[packet_number_space];
+}
+
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/test_tools/quic_connection_peer.h b/quiche/quic/test_tools/quic_connection_peer.h
index 794e991..fbdedb3 100644
--- a/quiche/quic/test_tools/quic_connection_peer.h
+++ b/quiche/quic/test_tools/quic_connection_peer.h
@@ -12,6 +12,7 @@
 #include "quiche/quic/core/quic_connection_id.h"
 #include "quiche/quic/core/quic_connection_stats.h"
 #include "quiche/quic/core/quic_packets.h"
+#include "quiche/quic/core/quic_path_validator.h"
 #include "quiche/quic/core/quic_types.h"
 #include "quiche/quic/platform/api/quic_socket_address.h"
 
@@ -235,6 +236,9 @@
 
   static QuicSocketAddress GetServerPreferredAddress(
       QuicConnection* connection);
+
+  static QuicEcnCounts* GetEcnCounts(QuicConnection* connection,
+                                     PacketNumberSpace packet_number_space);
 };
 
 }  // namespace test
diff --git a/quiche/quic/test_tools/quic_test_utils.cc b/quiche/quic/test_tools/quic_test_utils.cc
index 1a63557..bc1cf2f 100644
--- a/quiche/quic/test_tools/quic_test_utils.cc
+++ b/quiche/quic/test_tools/quic_test_utils.cc
@@ -348,6 +348,8 @@
   return true;
 }
 
+void NoOpFramerVisitor::OnAckEcnCounts(const QuicEcnCounts& /*ecn_counts*/) {}
+
 bool NoOpFramerVisitor::OnAckFrameEnd(QuicPacketNumber /*start*/) {
   return true;
 }
diff --git a/quiche/quic/test_tools/quic_test_utils.h b/quiche/quic/test_tools/quic_test_utils.h
index d8271e1..8609d5f 100644
--- a/quiche/quic/test_tools/quic_test_utils.h
+++ b/quiche/quic/test_tools/quic_test_utils.h
@@ -310,6 +310,7 @@
   MOCK_METHOD(bool, OnAckRange, (QuicPacketNumber, QuicPacketNumber),
               (override));
   MOCK_METHOD(bool, OnAckTimestamp, (QuicPacketNumber, QuicTime), (override));
+  MOCK_METHOD(void, OnAckEcnCounts, (const QuicEcnCounts&), (override));
   MOCK_METHOD(bool, OnAckFrameEnd, (QuicPacketNumber), (override));
   MOCK_METHOD(bool, OnStopWaitingFrame, (const QuicStopWaitingFrame& frame),
               (override));
@@ -393,6 +394,7 @@
   bool OnAckRange(QuicPacketNumber start, QuicPacketNumber end) override;
   bool OnAckTimestamp(QuicPacketNumber packet_number,
                       QuicTime timestamp) override;
+  void OnAckEcnCounts(const QuicEcnCounts& ecn_counts) override;
   bool OnAckFrameEnd(QuicPacketNumber start) override;
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override;
   bool OnPaddingFrame(const QuicPaddingFrame& frame) override;
@@ -1382,7 +1384,8 @@
   ~MockReceivedPacketManager() override;
 
   MOCK_METHOD(void, RecordPacketReceived,
-              (const QuicPacketHeader& header, QuicTime receipt_time),
+              (const QuicPacketHeader& header, QuicTime receipt_time,
+               const QuicEcnCodepoint ecn),
               (override));
   MOCK_METHOD(bool, IsMissing, (QuicPacketNumber packet_number), (override));
   MOCK_METHOD(bool, IsAwaitingPacket, (QuicPacketNumber packet_number),
@@ -2145,6 +2148,13 @@
   return result;
 }
 
+struct TestPerPacketOptions : PerPacketOptions {
+ public:
+  std::unique_ptr<quic::PerPacketOptions> Clone() const override {
+    return std::make_unique<TestPerPacketOptions>(*this);
+  }
+};
+
 }  // namespace test
 }  // namespace quic
 
diff --git a/quiche/quic/test_tools/simple_quic_framer.cc b/quiche/quic/test_tools/simple_quic_framer.cc
index 793afc4..da9f232 100644
--- a/quiche/quic/test_tools/simple_quic_framer.cc
+++ b/quiche/quic/test_tools/simple_quic_framer.cc
@@ -112,6 +112,8 @@
     return true;
   }
 
+  void OnAckEcnCounts(const QuicEcnCounts& /*ecn_counts*/) override {}
+
   bool OnAckFrameEnd(QuicPacketNumber /*start*/) override { return true; }
 
   bool OnStopWaitingFrame(const QuicStopWaitingFrame& frame) override {
diff --git a/quiche/quic/tools/quic_packet_printer_bin.cc b/quiche/quic/tools/quic_packet_printer_bin.cc
index 740971c..3e8b07b 100644
--- a/quiche/quic/tools/quic_packet_printer_bin.cc
+++ b/quiche/quic/tools/quic_packet_printer_bin.cc
@@ -128,6 +128,9 @@
               << timestamp.ToDebuggingValue() << ")";
     return true;
   }
+  void OnAckEcnCounts(const QuicEcnCounts& ecn_counts) override {
+    std::cerr << "OnAckEcnCounts: " << ecn_counts.ToString();
+  }
   bool OnAckFrameEnd(QuicPacketNumber start) override {
     std::cerr << "OnAckFrameEnd, start: " << start;
     return true;