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);