ECN support for several QuicPacketWriter implementations.

Protected by quic_send_ect1.

PiperOrigin-RevId: 537933737
diff --git a/quiche/quic/core/batch_writer/quic_gso_batch_writer.cc b/quiche/quic/core/batch_writer/quic_gso_batch_writer.cc
index 512e51f..77218d4 100644
--- a/quiche/quic/core/batch_writer/quic_gso_batch_writer.cc
+++ b/quiche/quic/core/batch_writer/quic_gso_batch_writer.cc
@@ -65,7 +65,8 @@
   // [2] It won't cause this batch to exceed kMaxGsoPacketSize.
   // [3] Already buffered writes all have the same length.
   // [4] Length of already buffered writes must >= length of the new write.
-  // [5] The new packet can be released without delay, or it has the same
+  // [5] The ECN markings match.
+  // [6] The new packet can be released without delay, or it has the same
   //     release time as buffered writes.
   const BufferedWrite& first = buffered_writes().front();
   const BufferedWrite& last = buffered_writes().back();
@@ -81,7 +82,8 @@
       batch_buffer().SizeInUse() + buf_len <= kMaxGsoPacketSize &&  // [2]
       first.buf_len == last.buf_len &&                              // [3]
       first.buf_len >= buf_len &&                                   // [4]
-      (can_burst || first.release_time == release_time);            // [5]
+      first.params.ecn_codepoint == params.ecn_codepoint &&         // [5]
+      (can_burst || first.release_time == release_time);            // [6]
 
   // A flush is required if any of the following is true:
   // [a] The new write can't be batched.
@@ -138,7 +140,8 @@
 // static
 void QuicGsoBatchWriter::BuildCmsg(QuicMsgHdr* hdr,
                                    const QuicIpAddress& self_address,
-                                   uint16_t gso_size, uint64_t release_time) {
+                                   uint16_t gso_size, uint64_t release_time,
+                                   QuicEcnCodepoint ecn_codepoint) {
   hdr->SetIpInNextCmsg(self_address);
   if (gso_size > 0) {
     *hdr->GetNextCmsgData<uint16_t>(SOL_UDP, UDP_SEGMENT) = gso_size;
@@ -146,6 +149,16 @@
   if (release_time != 0) {
     *hdr->GetNextCmsgData<uint64_t>(SOL_SOCKET, SO_TXTIME) = release_time;
   }
+  if (ecn_codepoint != ECN_NOT_ECT && GetQuicReloadableFlag(quic_send_ect1)) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 8, 8);
+    if (self_address.IsIPv4()) {
+      *hdr->GetNextCmsgData<int>(IPPROTO_IP, IP_TOS) =
+          static_cast<int>(ecn_codepoint);
+    } else {
+      *hdr->GetNextCmsgData<int>(IPPROTO_IPV6, IPV6_TCLASS) =
+          static_cast<int>(ecn_codepoint);
+    }
+  }
 }
 
 QuicGsoBatchWriter::FlushImplResult QuicGsoBatchWriter::FlushImpl() {
diff --git a/quiche/quic/core/batch_writer/quic_gso_batch_writer.h b/quiche/quic/core/batch_writer/quic_gso_batch_writer.h
index 96ccc76..492e446 100644
--- a/quiche/quic/core/batch_writer/quic_gso_batch_writer.h
+++ b/quiche/quic/core/batch_writer/quic_gso_batch_writer.h
@@ -21,6 +21,10 @@
 
   bool SupportsReleaseTime() const final { return supports_release_time_; }
 
+  bool SupportsEcn() const override {
+    return GetQuicReloadableFlag(quic_send_ect1);
+  }
+
   CanBatchResult CanBatch(const char* buffer, size_t buf_len,
                           const QuicIpAddress& self_address,
                           const QuicSocketAddress& peer_address,
@@ -52,10 +56,11 @@
     return gso_size <= 2 ? 16 : 45;
   }
 
-  static const int kCmsgSpace =
-      kCmsgSpaceForIp + kCmsgSpaceForSegmentSize + kCmsgSpaceForTxTime;
+  static const int kCmsgSpace = kCmsgSpaceForIp + kCmsgSpaceForSegmentSize +
+                                kCmsgSpaceForTxTime + kCmsgSpaceForTOS;
   static void BuildCmsg(QuicMsgHdr* hdr, const QuicIpAddress& self_address,
-                        uint16_t gso_size, uint64_t release_time);
+                        uint16_t gso_size, uint64_t release_time,
+                        QuicEcnCodepoint ecn_codepoint);
 
   template <size_t CmsgSpace, typename CmsgBuilderT>
   FlushImplResult InternalFlushImpl(CmsgBuilderT cmsg_builder) {
@@ -73,7 +78,8 @@
                    sizeof(cbuf));
 
     uint16_t gso_size = buffered_writes().size() > 1 ? first.buf_len : 0;
-    cmsg_builder(&hdr, first.self_address, gso_size, first.release_time);
+    cmsg_builder(&hdr, first.self_address, gso_size, first.release_time,
+                 first.params.ecn_codepoint);
 
     write_result = QuicLinuxSocketUtils::WritePacket(fd(), hdr);
     QUIC_DVLOG(1) << "Write GSO packet result: " << write_result
diff --git a/quiche/quic/core/batch_writer/quic_gso_batch_writer_test.cc b/quiche/quic/core/batch_writer/quic_gso_batch_writer_test.cc
index c4c0406..b21c5e7 100644
--- a/quiche/quic/core/batch_writer/quic_gso_batch_writer_test.cc
+++ b/quiche/quic/core/batch_writer/quic_gso_batch_writer_test.cc
@@ -4,6 +4,8 @@
 
 #include "quiche/quic/core/batch_writer/quic_gso_batch_writer.h"
 
+#include <sys/socket.h>
+
 #include <cstdint>
 #include <limits>
 #include <memory>
@@ -61,12 +63,6 @@
   uint64_t forced_release_time_ms_ = 1;
 };
 
-struct QUIC_EXPORT_PRIVATE TestPerPacketOptions : public PerPacketOptions {
-  std::unique_ptr<quic::PerPacketOptions> Clone() const override {
-    return std::make_unique<TestPerPacketOptions>(*this);
-  }
-};
-
 // TestBufferedWrite is a copy-constructible BufferedWrite.
 struct QUIC_EXPORT_PRIVATE TestBufferedWrite : public BufferedWrite {
   using BufferedWrite::BufferedWrite;
@@ -453,6 +449,97 @@
   EXPECT_EQ(result.send_time_offset, QuicTime::Delta::Zero());
 }
 
+TEST_F(QuicGsoBatchWriterTest, EcnCodepoint) {
+  const WriteResult write_buffered(WRITE_STATUS_OK, 0);
+
+  auto writer = TestQuicGsoBatchWriter::NewInstanceWithReleaseTimeSupport();
+
+  QuicPacketWriterParams params;
+  EXPECT_TRUE(params.release_time_delay.IsZero());
+  EXPECT_FALSE(params.allow_burst);
+  params.ecn_codepoint = ECN_ECT0;
+
+  // The 1st packet has no delay.
+  WriteResult result = WritePacketWithParams(writer.get(), params);
+  ASSERT_EQ(write_buffered, result);
+  EXPECT_EQ(MillisToNanos(1), writer->buffered_writes().back().release_time);
+  EXPECT_EQ(result.send_time_offset, QuicTime::Delta::Zero());
+
+  // The 2nd packet should be buffered.
+  params.allow_burst = true;
+  result = WritePacketWithParams(writer.get(), params);
+  ASSERT_EQ(write_buffered, result);
+
+  // The 3rd packet changes the ECN codepoint.
+  // The first 2 packets are flushed due to different codepoint.
+  params.ecn_codepoint = ECN_ECT1;
+  EXPECT_CALL(mock_syscalls_, Sendmsg(_, _, _))
+      .WillOnce(Invoke([](int /*sockfd*/, const msghdr* msg, int /*flags*/) {
+        const int kEct0 = 0x01;
+        EXPECT_EQ(2700u, PacketLength(msg));
+        msghdr mutable_msg;
+        memcpy(&mutable_msg, msg, sizeof(*msg));
+        for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&mutable_msg); cmsg != NULL;
+             cmsg = CMSG_NXTHDR(&mutable_msg, cmsg)) {
+          if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_TOS) {
+            EXPECT_EQ(*reinterpret_cast<int*> CMSG_DATA(cmsg), kEct0);
+            break;
+          }
+        }
+        errno = 0;
+        return 0;
+      }));
+  result = WritePacketWithParams(writer.get(), params);
+  ASSERT_EQ(WriteResult(WRITE_STATUS_OK, 2700), result);
+}
+
+TEST_F(QuicGsoBatchWriterTest, EcnCodepointIPv6) {
+  const WriteResult write_buffered(WRITE_STATUS_OK, 0);
+
+  self_address_ = QuicIpAddress::Any6();
+  peer_address_ = QuicSocketAddress(QuicIpAddress::Any6(), 443);
+  auto writer = TestQuicGsoBatchWriter::NewInstanceWithReleaseTimeSupport();
+
+  QuicPacketWriterParams params;
+  EXPECT_TRUE(params.release_time_delay.IsZero());
+  EXPECT_FALSE(params.allow_burst);
+  params.ecn_codepoint = ECN_ECT0;
+
+  // The 1st packet has no delay.
+  WriteResult result = WritePacketWithParams(writer.get(), params);
+  ASSERT_EQ(write_buffered, result);
+  EXPECT_EQ(MillisToNanos(1), writer->buffered_writes().back().release_time);
+  EXPECT_EQ(result.send_time_offset, QuicTime::Delta::Zero());
+
+  // The 2nd packet should be buffered.
+  params.allow_burst = true;
+  result = WritePacketWithParams(writer.get(), params);
+  ASSERT_EQ(write_buffered, result);
+
+  // The 3rd packet changes the ECN codepoint.
+  // The first 2 packets are flushed due to different codepoint.
+  params.ecn_codepoint = ECN_ECT1;
+  EXPECT_CALL(mock_syscalls_, Sendmsg(_, _, _))
+      .WillOnce(Invoke([](int /*sockfd*/, const msghdr* msg, int /*flags*/) {
+        const int kEct0 = 0x01;
+        EXPECT_EQ(2700u, PacketLength(msg));
+        msghdr mutable_msg;
+        memcpy(&mutable_msg, msg, sizeof(*msg));
+        for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&mutable_msg); cmsg != NULL;
+             cmsg = CMSG_NXTHDR(&mutable_msg, cmsg)) {
+          if (cmsg->cmsg_level == IPPROTO_IPV6 &&
+              cmsg->cmsg_type == IPV6_TCLASS) {
+            EXPECT_EQ(*reinterpret_cast<int*> CMSG_DATA(cmsg), kEct0);
+            break;
+          }
+        }
+        errno = 0;
+        return 0;
+      }));
+  result = WritePacketWithParams(writer.get(), params);
+  ASSERT_EQ(WriteResult(WRITE_STATUS_OK, 2700), result);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 8737453..f78f11a 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -3915,7 +3915,7 @@
   // Only packets on the default path are in-flight.
   if (!default_path_.ecn_marked_packet_acked) {
     QUIC_DVLOG(1) << ENDPOINT << "First ECT packet acked on active path.";
-    QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 2, 3);
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 2, 8);
     default_path_.ecn_marked_packet_acked = true;
   }
 }
@@ -7354,7 +7354,7 @@
   if (!GetQuicReloadableFlag(quic_send_ect1)) {
     return false;
   }
-  QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 3, 3);
+  QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 3, 8);
   if (disable_ecn_codepoint_validation_ || ecn_codepoint == ECN_NOT_ECT) {
     packet_writer_params_.ecn_codepoint = ecn_codepoint;
     return true;
diff --git a/quiche/quic/core/quic_linux_socket_utils.h b/quiche/quic/core/quic_linux_socket_utils.h
index 41be945..36c2dd7 100644
--- a/quiche/quic/core/quic_linux_socket_utils.h
+++ b/quiche/quic/core/quic_linux_socket_utils.h
@@ -57,6 +57,8 @@
 
 const int kCmsgSpaceForTTL = CMSG_SPACE(sizeof(int));
 
+const int kCmsgSpaceForTOS = CMSG_SPACE(sizeof(int));
+
 // QuicMsgHdr is used to build msghdr objects that can be used send packets via
 // ::sendmsg.
 //
diff --git a/quiche/quic/core/quic_sent_packet_manager.cc b/quiche/quic/core/quic_sent_packet_manager.cc
index 2f030ec..0357120 100644
--- a/quiche/quic/core/quic_sent_packet_manager.cc
+++ b/quiche/quic/core/quic_sent_packet_manager.cc
@@ -1421,7 +1421,7 @@
   // Validate ECN feedback.
   absl::optional<QuicEcnCounts> valid_ecn_counts;
   if (GetQuicReloadableFlag(quic_send_ect1)) {
-    QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 1, 3);
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_send_ect1, 1, 8);
     if (IsEcnFeedbackValid(acked_packet_number_space, ecn_counts,
                            newly_acked_ect0, newly_acked_ect1)) {
       valid_ecn_counts = ecn_counts;