Add support for sending immediate GOAWAYs in HTTP/3.

Protected by quic_reloadable_flag_quic_enforce_immediate_goaway.

PiperOrigin-RevId: 848433923
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h
index 82603bc..cde6e5c 100755
--- a/quiche/common/quiche_feature_flags_list.h
+++ b/quiche/common/quiche_feature_flags_list.h
@@ -40,6 +40,7 @@
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_mtu_discovery_at_server, false, false, "If true, QUIC will default enable MTU discovery at server, with a target of 1450 bytes.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_server_on_wire_ping, true, true, "If true, enable server retransmittable on wire PING.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enable_version_rfcv2, false, false, "When true, support RFC9369.")
+QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enforce_immediate_goaway, false, false, "If true, QUIC will support sending immediate GOAWAYS and will refuse streams above the limit.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_enobufs_blocked, true, true, "If true, ENOBUFS socket errors are reported as socket blocked instead of socket failure.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fin_before_completed_http_headers, false, true, "If true, close the connection with error if FIN is received before finish receiving the whole HTTP headers.")
 QUICHE_FLAG(bool, quiche_reloadable_flag_quic_fix_timeouts, true, true, "If true, postpone setting handshake timeout to infinite to handshake complete.")
diff --git a/quiche/quic/core/http/quic_spdy_session.cc b/quiche/quic/core/http/quic_spdy_session.cc
index dd370c0..479abfa 100644
--- a/quiche/quic/core/http/quic_spdy_session.cc
+++ b/quiche/quic/core/http/quic_spdy_session.cc
@@ -897,6 +897,12 @@
 
 void QuicSpdySession::SendHttp3GoAway(QuicErrorCode error_code,
                                       const std::string& reason) {
+  SendHttp3GoAway(error_code, reason, /*immediate=*/false);
+}
+
+void QuicSpdySession::SendHttp3GoAway(QuicErrorCode error_code,
+                                      const std::string& reason,
+                                      bool immediate) {
   QUICHE_DCHECK(VersionIsIetfQuic(transport_version()));
   if (!IsEncryptionEstablished()) {
     QUIC_CODE_COUNT(quic_h3_goaway_before_encryption_established);
@@ -908,8 +914,7 @@
   ietf_streamid_manager().StopIncreasingIncomingMaxStreams();
 
   QuicStreamId stream_id =
-      QuicUtils::GetMaxClientInitiatedBidirectionalStreamId(
-          transport_version());
+      GetStreamIdForHttp3Goaway(immediate && enforce_immediate_goaway());
   if (last_sent_http3_goaway_id_.has_value() &&
       *last_sent_http3_goaway_id_ <= stream_id) {
     // Do not send GOAWAY frame with a higher id, because it is forbidden.
@@ -1657,6 +1662,26 @@
   }
 }
 
+QuicStreamId QuicSpdySession::GetStreamIdForHttp3Goaway(bool immediate) const {
+  if (!immediate) {
+    return QuicUtils::GetMaxClientInitiatedBidirectionalStreamId(
+        transport_version());
+  }
+
+  QUIC_RELOADABLE_FLAG_COUNT_N(quic_enforce_immediate_goaway, 3, 3);
+  QuicStreamId stream_id =
+      GetLargestPeerCreatedStreamId(/*unidirectional = */ false);
+
+  if (stream_id == QuicUtils::GetInvalidStreamId(transport_version())) {
+    // No client-initiated bidirectional streams received yet.
+    // Send 0 to let client know that all requests can be retried.
+    return 0;
+  }
+  // Tell client that streams starting with the next after the largest
+  // received one can be retried.
+  return stream_id + QuicUtils::StreamIdDelta(transport_version());
+}
+
 void QuicSpdySession::BeforeConnectionCloseSent() {
   if (!VersionIsIetfQuic(transport_version()) || !IsEncryptionEstablished()) {
     return;
@@ -1664,18 +1689,8 @@
 
   QUICHE_DCHECK_EQ(perspective(), Perspective::IS_SERVER);
 
-  QuicStreamId stream_id =
-      GetLargestPeerCreatedStreamId(/*unidirectional = */ false);
+  QuicStreamId stream_id = GetStreamIdForHttp3Goaway(/*immediate=*/true);
 
-  if (stream_id == QuicUtils::GetInvalidStreamId(transport_version())) {
-    // No client-initiated bidirectional streams received yet.
-    // Send 0 to let client know that all requests can be retried.
-    stream_id = 0;
-  } else {
-    // Tell client that streams starting with the next after the largest
-    // received one can be retried.
-    stream_id += QuicUtils::StreamIdDelta(transport_version());
-  }
   if (last_sent_http3_goaway_id_.has_value() &&
       *last_sent_http3_goaway_id_ <= stream_id) {
     // Do not send GOAWAY frame with a higher id, because it is forbidden.
@@ -2043,6 +2058,11 @@
   }
 }
 
+bool QuicSpdySession::ShouldRefuseIncomingStream(QuicStreamId stream_id) {
+  return last_sent_http3_goaway_id_.has_value() &&
+         *last_sent_http3_goaway_id_ <= stream_id;
+}
+
 #undef ENDPOINT  // undef for jumbo builds
 
 }  // namespace quic
diff --git a/quiche/quic/core/http/quic_spdy_session.h b/quiche/quic/core/http/quic_spdy_session.h
index 1a3736f..acee841 100644
--- a/quiche/quic/core/http/quic_spdy_session.h
+++ b/quiche/quic/core/http/quic_spdy_session.h
@@ -258,6 +258,9 @@
   // before encryption gets established.
   void SendHttp3GoAway(QuicErrorCode error_code, const std::string& reason);
 
+  void SendHttp3GoAway(QuicErrorCode error_code, const std::string& reason,
+                       bool immediate);
+
   QpackEncoder* qpack_encoder();
   QpackDecoder* qpack_decoder();
   QuicHeadersStream* headers_stream() { return headers_stream_; }
@@ -494,6 +497,7 @@
   // ensure that all data streams are QuicSpdyStreams.
   QuicSpdyStream* CreateIncomingStream(QuicStreamId id) override = 0;
   QuicSpdyStream* CreateIncomingStream(PendingStream* pending) override = 0;
+  bool ShouldRefuseIncomingStream(QuicStreamId id) override;
   // Called to create a new outgoing bidirectional stream.
   virtual QuicSpdyStream* CreateOutgoingBidirectionalStream() = 0;
 
@@ -632,6 +636,8 @@
 
   bool ValidateWebTransportSettingsConsistency();
 
+  QuicStreamId GetStreamIdForHttp3Goaway(bool immediate) const;
+
   std::unique_ptr<QpackDecoder> qpack_decoder_;
   http2::Http2DecoderAdapter h2_deframer_;
   bool fin_;
diff --git a/quiche/quic/core/http/quic_spdy_session_test.cc b/quiche/quic/core/http/quic_spdy_session_test.cc
index 82b5322..e6bef69 100644
--- a/quiche/quic/core/http/quic_spdy_session_test.cc
+++ b/quiche/quic/core/http/quic_spdy_session_test.cc
@@ -1174,6 +1174,48 @@
   session_->SendHttp3GoAway(QUIC_PEER_GOING_AWAY, "Goaway");
 }
 
+TEST_P(QuicSpdySessionTestServer, SendHttp3ImmediateGoAway) {
+  Initialize();
+  if (!VersionIsIetfQuic(transport_version())) {
+    return;
+  }
+
+  CompleteHandshake();
+  if (!session_.has_value()) {
+    FAIL();
+    return;
+  }
+  TestSession& session = *session_;
+  StrictMock<MockHttp3DebugVisitor> debug_visitor;
+  session.set_debug_visitor(&debug_visitor);
+
+  EXPECT_CALL(*writer_, WritePacket(_, _, _, _, _, _))
+      .WillOnce(Return(WriteResult(WRITE_STATUS_OK, 0)));
+  if (!GetQuicReloadableFlag(quic_enforce_immediate_goaway)) {
+    // Send max stream id (currently 32 bits).
+    EXPECT_CALL(debug_visitor, OnGoAwayFrameSent(/* stream_id = */ 0xfffffffc));
+  } else {
+    EXPECT_CALL(debug_visitor, OnGoAwayFrameSent(/* stream_id = */ 0));
+  }
+  session.SendHttp3GoAway(QUIC_PEER_GOING_AWAY, "Goaway", true);
+  EXPECT_TRUE(session.goaway_sent());
+
+  if (!GetQuicReloadableFlag(quic_enforce_immediate_goaway)) {
+    // New incoming stream is not reset.
+    const QuicStreamId kTestStreamId =
+        GetNthClientInitiatedBidirectionalStreamId(transport_version(), 0);
+    EXPECT_CALL(*connection_, OnStreamReset(kTestStreamId, _)).Times(0);
+    EXPECT_TRUE(session.GetOrCreateStream(kTestStreamId));
+    return;
+  }
+  // New incoming stream is refused.
+  const QuicStreamId kTestStreamId =
+      GetNthClientInitiatedBidirectionalStreamId(transport_version(), 0);
+  EXPECT_CALL(*connection_, OnStreamReset(kTestStreamId, _));
+  EXPECT_CALL(*connection_, SendControlFrame(_));
+  EXPECT_FALSE(session.GetOrCreateStream(kTestStreamId));
+}
+
 TEST_P(QuicSpdySessionTestServer, SendHttp3GoAwayAndNoMoreMaxStreams) {
   Initialize();
   if (!VersionIsIetfQuic(transport_version())) {
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc
index 99cc1a9..1b6e90d 100644
--- a/quiche/quic/core/quic_session.cc
+++ b/quiche/quic/core/quic_session.cc
@@ -2245,6 +2245,16 @@
     return nullptr;
   }
 
+  if (enforce_immediate_goaway_) {
+    QUIC_RELOADABLE_FLAG_COUNT_N(quic_enforce_immediate_goaway, 1, 3);
+    if (ShouldRefuseIncomingStream(stream_id)) {
+      QUIC_RELOADABLE_FLAG_COUNT_N(quic_enforce_immediate_goaway, 2, 3);
+      // Refuse to open the stream.
+      ResetStream(stream_id, QUIC_REFUSED_STREAM);
+      return nullptr;
+    }
+  }
+
   return CreateIncomingStream(stream_id);
 }
 
diff --git a/quiche/quic/core/quic_session.h b/quiche/quic/core/quic_session.h
index 562b2db..0518f4a 100644
--- a/quiche/quic/core/quic_session.h
+++ b/quiche/quic/core/quic_session.h
@@ -884,6 +884,8 @@
   // Returns nullptr and does error handling if the stream can not be created.
   virtual QuicStream* CreateIncomingStream(QuicStreamId id) = 0;
   virtual QuicStream* CreateIncomingStream(PendingStream* pending) = 0;
+  // Return true if the specified stream should be refused.
+  virtual bool ShouldRefuseIncomingStream(QuicStreamId id) { return false; }
 
   // Return the reserved crypto stream.
   virtual QuicCryptoStream* GetMutableCryptoStream() = 0;
@@ -1057,6 +1059,8 @@
     max_streams_accepted_per_loop_ = max_streams_accepted_per_loop;
   }
 
+  bool enforce_immediate_goaway() const { return enforce_immediate_goaway_; }
+
  private:
   friend class test::QuicSessionPeer;
 
@@ -1212,6 +1216,9 @@
   const bool notify_stream_soon_to_destroy_ =
       GetQuicReloadableFlag(quic_notify_stream_soon_to_destroy);
 
+  const bool enforce_immediate_goaway_ =
+      GetQuicReloadableFlag(quic_enforce_immediate_goaway);
+
   // Whether a transport layer GOAWAY frame has been received.
   // Such a frame only exists in Google QUIC, therefore
   // |transport_goaway_received_| is always false when using IETF QUIC.
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc
index 974a79d..35e490f 100644
--- a/quiche/quic/core/quic_session_test.cc
+++ b/quiche/quic/core/quic_session_test.cc
@@ -355,6 +355,8 @@
     return consumed;
   }
 
+  MOCK_METHOD(bool, ShouldRefuseIncomingStream, (QuicStreamId stream_id),
+              (override));
   MOCK_METHOD(void, OnCanCreateNewOutgoingStream, (bool unidirectional),
               (override));
 
@@ -727,6 +729,18 @@
                   GetNthClientInitiatedBidirectionalId(1)) != nullptr);
 }
 
+TEST_P(QuicSessionTestServer, StreamRefused) {
+  QuicStreamId stream_id = GetNthClientInitiatedBidirectionalId(3);
+  if (!GetQuicReloadableFlag(quic_enforce_immediate_goaway)) {
+    ASSERT_TRUE(session_.GetOrCreateStream(stream_id) != nullptr);
+    return;
+  }
+  EXPECT_CALL(session_, ShouldRefuseIncomingStream(stream_id))
+      .WillOnce(Return(true));
+  EXPECT_CALL(*connection_, OnStreamReset(stream_id, _));
+  ASSERT_TRUE(session_.GetOrCreateStream(stream_id) == nullptr);
+}
+
 TEST_P(QuicSessionTestServer, AvailableUnidirectionalStreams) {
   ASSERT_TRUE(session_.GetOrCreateStream(
                   GetNthClientInitiatedUnidirectionalId(3)) != nullptr);