Enforce limits on sending ECN.

QuicConnection only permits ECN via a state variable accessible only by QuicConnectionPeer.

The send limits will be relaxed as new code meets the requirements.

There is no code that allows sending anything other than NOT_ECT, so this is not flag-protected.

PiperOrigin-RevId: 523154100
diff --git a/quiche/quic/core/http/end_to_end_test.cc b/quiche/quic/core/http/end_to_end_test.cc
index 4e2e5cf..bfa2c5c 100644
--- a/quiche/quic/core/http/end_to_end_test.cc
+++ b/quiche/quic/core/http/end_to_end_test.cc
@@ -7182,10 +7182,11 @@
   server_thread_->Resume();
 }
 
-TEST_P(EndToEndTest, EcnMarksReportedCorrectly) {
+TEST_P(EndToEndTest, ServerReportsEcn) {
   // Client connects using not-ECT.
   ASSERT_TRUE(Initialize());
   QuicConnection* client_connection = GetClientConnection();
+  QuicConnectionPeer::DisableEcnCodepointValidation(client_connection);
   QuicEcnCounts* ecn = QuicSentPacketManagerPeer::GetPeerEcnCounts(
       QuicConnectionPeer::GetSentPacketManager(client_connection),
       APPLICATION_DATA);
@@ -7229,6 +7230,32 @@
   client_->Disconnect();
 }
 
+TEST_P(EndToEndTest, ClientReportsEcn) {
+  ASSERT_TRUE(Initialize());
+  QuicConnection* server_connection = GetServerConnection();
+  QuicConnectionPeer::DisableEcnCodepointValidation(server_connection);
+  QuicEcnCounts* ecn = QuicSentPacketManagerPeer::GetPeerEcnCounts(
+      QuicConnectionPeer::GetSentPacketManager(server_connection),
+      APPLICATION_DATA);
+  TestPerPacketOptions options;
+  options.ecn_codepoint = ECN_ECT1;
+  server_connection->set_per_packet_options(&options);
+  client_->SendSynchronousRequest("/foo");
+  // A second request provides a packet for the client ACKs to go with.
+  client_->SendSynchronousRequest("/foo");
+  EXPECT_EQ(ecn->ect0, 0);
+  EXPECT_EQ(ecn->ce, 0);
+  if (!GetQuicRestartFlag(quic_receive_ecn) ||
+      !GetQuicRestartFlag(quic_quiche_ecn_sockets) ||
+      !VersionHasIetfQuicFrames(version_.transport_version)) {
+    EXPECT_EQ(ecn->ect1, 0);
+  } else {
+    EXPECT_GT(ecn->ect1, 0);
+  }
+  server_connection->set_per_packet_options(nullptr);
+  client_->Disconnect();
+}
+
 TEST_P(EndToEndTest, ClientMigrationAfterHalfwayServerMigration) {
   use_preferred_address_ = true;
   ASSERT_TRUE(Initialize());
diff --git a/quiche/quic/core/quic_connection.cc b/quiche/quic/core/quic_connection.cc
index 271f172..0d42b43 100644
--- a/quiche/quic/core/quic_connection.cc
+++ b/quiche/quic/core/quic_connection.cc
@@ -3115,7 +3115,6 @@
 
 void QuicConnection::WriteQueuedPackets() {
   QUICHE_DCHECK(!writer_->IsWriteBlocked());
-
   QUIC_CLIENT_HISTOGRAM_COUNTS("QuicSession.NumQueuedPacketsBeforeWrite",
                                buffered_packets_.size(), 1, 1000, 50, "");
 
@@ -3124,7 +3123,7 @@
       break;
     }
     const BufferedPacket& packet = buffered_packets_.front();
-    WriteResult result = writer_->WritePacket(
+    WriteResult result = SendPacketToWriter(
         packet.data.get(), packet.length, packet.self_address.host(),
         packet.peer_address, per_packet_options_);
     QUIC_DVLOG(1) << ENDPOINT << "Sending buffered packet, result: " << result;
@@ -3491,9 +3490,9 @@
       //
       // writer_->WritePacket transfers buffer ownership back to the writer.
       packet->release_encrypted_buffer = nullptr;
-      result = writer_->WritePacket(packet->encrypted_buffer, encrypted_length,
-                                    send_from_address.host(), send_to_address,
-                                    per_packet_options_);
+      result = SendPacketToWriter(packet->encrypted_buffer, encrypted_length,
+                                  send_from_address.host(), send_to_address,
+                                  per_packet_options_);
       // This is a work around for an issue with linux UDP GSO batch writers.
       // When sending a GSO packet with 2 segments, if the first segment is
       // larger than the path MTU, instead of EMSGSIZE, the linux kernel returns
@@ -4148,6 +4147,38 @@
                    address) != known_server_addresses_.cend();
 }
 
+void QuicConnection::ClearEcnCodepoint() {
+  if (per_packet_options_ != nullptr) {
+    per_packet_options_->ecn_codepoint = ECN_NOT_ECT;
+  }
+}
+
+WriteResult QuicConnection::SendPacketToWriter(
+    const char* buffer, size_t buf_len, const QuicIpAddress& self_address,
+    const QuicSocketAddress& peer_address, PerPacketOptions* options) {
+  if (!disable_ecn_codepoint_validation_) {
+    switch (GetNextEcnCodepoint()) {
+      case ECN_NOT_ECT:
+        break;
+      case ECN_ECT0:
+        if (!sent_packet_manager_.GetSendAlgorithm()->SupportsECT0()) {
+          ClearEcnCodepoint();
+        }
+        break;
+      case ECN_ECT1:
+        if (!sent_packet_manager_.GetSendAlgorithm()->SupportsECT1()) {
+          ClearEcnCodepoint();
+        }
+        break;
+      case ECN_CE:
+        ClearEcnCodepoint();
+        break;
+    }
+  }
+  return writer_->WritePacket(buffer, buf_len, self_address, peer_address,
+                              options);
+}
+
 void QuicConnection::OnRetransmissionTimeout() {
   ScopedRetransmissionTimeoutIndicator indicator(this);
 #ifndef NDEBUG
@@ -5966,7 +5997,7 @@
         buffer, static_cast<QuicPacketLength>(length),
         coalesced_packet_.self_address(), coalesced_packet_.peer_address());
   } else {
-    WriteResult result = writer_->WritePacket(
+    WriteResult result = SendPacketToWriter(
         buffer, length, coalesced_packet_.self_address().host(),
         coalesced_packet_.peer_address(), per_packet_options_);
     if (IsWriteError(result.status)) {
diff --git a/quiche/quic/core/quic_connection.h b/quiche/quic/core/quic_connection.h
index 79de604..1e1f9b6 100644
--- a/quiche/quic/core/quic_connection.h
+++ b/quiche/quic/core/quic_connection.h
@@ -1961,6 +1961,22 @@
   // Returns true if |address| is known server address.
   bool IsKnownServerAddress(const QuicSocketAddress& address) const;
 
+  // Retrieves the ECN codepoint to be sent on the next packet.
+  QuicEcnCodepoint GetNextEcnCodepoint() const {
+    return (per_packet_options_ != nullptr) ? per_packet_options_->ecn_codepoint
+                                            : ECN_NOT_ECT;
+  }
+
+  // Sets the ECN codepoint to Not-ECT.
+  void ClearEcnCodepoint();
+
+  // Writes the packet to the writer and clears the ECN codepoint in |options|
+  // if it is invalid.
+  WriteResult SendPacketToWriter(const char* buffer, size_t buf_len,
+                                 const QuicIpAddress& self_address,
+                                 const QuicSocketAddress& peer_address,
+                                 PerPacketOptions* options);
+
   QuicConnectionContext context_;
 
   QuicFramer framer_;
@@ -2357,6 +2373,14 @@
   // original address.
   QuicLRUCache<QuicSocketAddress, bool, QuicSocketAddressHash>
       received_client_addresses_cache_;
+
+  // Endpoints should never mark packets with Congestion Experienced (CE), as
+  // this is only done by routers. Endpoints cannot send ECT(0) or ECT(1) if
+  // their congestion control cannot respond to these signals in accordance with
+  // the spec, or if the QUIC implementation doesn't validate ECN feedback. When
+  // true, the connection will not verify that the requested codepoint adheres
+  // to these policies. This is only accessible through QuicConnectionPeer.
+  bool disable_ecn_codepoint_validation_ = false;
 };
 
 }  // namespace quic
diff --git a/quiche/quic/core/quic_connection_test.cc b/quiche/quic/core/quic_connection_test.cc
index 27cf706..f0fa10b 100644
--- a/quiche/quic/core/quic_connection_test.cc
+++ b/quiche/quic/core/quic_connection_test.cc
@@ -17465,6 +17465,57 @@
   EXPECT_EQ(kSelfAddress, connection_.self_address());
 }
 
+TEST_P(QuicConnectionTest, EcnCodepointsRejected) {
+  TestPerPacketOptions per_packet_options;
+  connection_.set_per_packet_options(&per_packet_options);
+  for (QuicEcnCodepoint ecn : {ECN_NOT_ECT, ECN_ECT0, ECN_ECT1, ECN_CE}) {
+    per_packet_options.ecn_codepoint = ecn;
+    if (ecn == ECN_ECT0) {
+      EXPECT_CALL(*send_algorithm_, SupportsECT0()).WillOnce(Return(false));
+    } else if (ecn == ECN_ECT1) {
+      EXPECT_CALL(*send_algorithm_, SupportsECT1()).WillOnce(Return(false));
+    }
+    EXPECT_CALL(connection_, OnSerializedPacket(_));
+    SendPing();
+    EXPECT_EQ(per_packet_options.ecn_codepoint, ECN_NOT_ECT);
+    EXPECT_EQ(writer_->last_ecn_sent(), ECN_NOT_ECT);
+  }
+}
+
+TEST_P(QuicConnectionTest, EcnCodepointsAccepted) {
+  TestPerPacketOptions per_packet_options;
+  connection_.set_per_packet_options(&per_packet_options);
+  for (QuicEcnCodepoint ecn : {ECN_NOT_ECT, ECN_ECT0, ECN_ECT1, ECN_CE}) {
+    per_packet_options.ecn_codepoint = ecn;
+    if (ecn == ECN_ECT0) {
+      EXPECT_CALL(*send_algorithm_, SupportsECT0()).WillOnce(Return(true));
+    } else if (ecn == ECN_ECT1) {
+      EXPECT_CALL(*send_algorithm_, SupportsECT1()).WillOnce(Return(true));
+    }
+    EXPECT_CALL(connection_, OnSerializedPacket(_));
+    SendPing();
+    QuicEcnCodepoint expected_codepoint = ecn;
+    if (ecn == ECN_CE) {
+      expected_codepoint = ECN_NOT_ECT;
+    }
+    EXPECT_EQ(per_packet_options.ecn_codepoint, expected_codepoint);
+    EXPECT_EQ(writer_->last_ecn_sent(), expected_codepoint);
+  }
+}
+
+TEST_P(QuicConnectionTest, EcnValidationDisabled) {
+  TestPerPacketOptions per_packet_options;
+  connection_.set_per_packet_options(&per_packet_options);
+  QuicConnectionPeer::DisableEcnCodepointValidation(&connection_);
+  for (QuicEcnCodepoint ecn : {ECN_NOT_ECT, ECN_ECT0, ECN_ECT1, ECN_CE}) {
+    per_packet_options.ecn_codepoint = ecn;
+    EXPECT_CALL(connection_, OnSerializedPacket(_));
+    SendPing();
+    EXPECT_EQ(per_packet_options.ecn_codepoint, ecn);
+    EXPECT_EQ(writer_->last_ecn_sent(), ecn);
+  }
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_types.cc b/quiche/quic/core/quic_types.cc
index e046092..9dae069 100644
--- a/quiche/quic/core/quic_types.cc
+++ b/quiche/quic/core/quic_types.cc
@@ -446,6 +446,19 @@
   return os;
 }
 
+std::string EcnCodepointToString(QuicEcnCodepoint ecn) {
+  switch (ecn) {
+    case ECN_NOT_ECT:
+      return "Not-ECT";
+    case ECN_ECT0:
+      return "ECT(0)";
+    case ECN_ECT1:
+      return "ECT(1)";
+    case ECN_CE:
+      return "CE";
+  }
+}
+
 #undef RETURN_STRING_LITERAL  // undef for jumbo builds
 
 }  // namespace quic
diff --git a/quiche/quic/core/quic_types.h b/quiche/quic/core/quic_types.h
index 6e2531b..b2a100b 100644
--- a/quiche/quic/core/quic_types.h
+++ b/quiche/quic/core/quic_types.h
@@ -885,6 +885,8 @@
   ECN_CE = 3,
 };
 
+QUICHE_EXPORT std::string EcnCodepointToString(QuicEcnCodepoint ecn);
+
 // 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.
diff --git a/quiche/quic/core/quic_utils_test.cc b/quiche/quic/core/quic_utils_test.cc
index a4a194f..543c8ea 100644
--- a/quiche/quic/core/quic_utils_test.cc
+++ b/quiche/quic/core/quic_utils_test.cc
@@ -233,6 +233,13 @@
   EXPECT_FALSE(QuicUtils::AreStatelessResetTokensEqual(token1a, token2));
 }
 
+TEST_F(QuicUtilsTest, EcnCodepointToString) {
+  EXPECT_EQ(EcnCodepointToString(ECN_NOT_ECT), "Not-ECT");
+  EXPECT_EQ(EcnCodepointToString(ECN_ECT0), "ECT(0)");
+  EXPECT_EQ(EcnCodepointToString(ECN_ECT1), "ECT(1)");
+  EXPECT_EQ(EcnCodepointToString(ECN_CE), "CE");
+}
+
 enum class TestEnumClassBit : uint8_t {
   BIT_ZERO = 0,
   BIT_ONE,