For IETF QUIC client: When 0-RTT is rejected, check the server-sent transport params and close connection if server's limit exceeds what the client has already used. Client side change only. not protected. PiperOrigin-RevId: 315406554 Change-Id: I6e313df0ed10ef64c3f72edb5e4d1433804b8e87
diff --git a/quic/core/http/quic_spdy_client_session_test.cc b/quic/core/http/quic_spdy_client_session_test.cc index 1a6ed6c..ca9b1d5 100644 --- a/quic/core/http/quic_spdy_client_session_test.cc +++ b/quic/core/http/quic_spdy_client_session_test.cc
@@ -194,6 +194,18 @@ session_->GetMutableCryptoStream()); } + void CompleteFirstConnection() { + CompleteCryptoHandshake(); + EXPECT_FALSE(session_->GetCryptoStream()->IsResumption()); + if (session_->version().UsesHttp3()) { + SettingsFrame settings; + settings.values[SETTINGS_QPACK_MAX_TABLE_CAPACITY] = 2; + settings.values[SETTINGS_MAX_HEADER_LIST_SIZE] = 5; + settings.values[256] = 4; // unknown setting + session_->OnSettingsFrame(settings); + } + } + // Owned by |session_|. QuicCryptoClientStream* crypto_stream_; std::unique_ptr<QuicCryptoServerConfig> server_crypto_config_; @@ -979,15 +991,7 @@ return; } - CompleteCryptoHandshake(); - EXPECT_FALSE(session_->GetCryptoStream()->IsResumption()); - if (session_->version().UsesHttp3()) { - SettingsFrame settings; - settings.values[SETTINGS_QPACK_MAX_TABLE_CAPACITY] = 2; - settings.values[SETTINGS_MAX_HEADER_LIST_SIZE] = 5; - settings.values[256] = 4; // unknown setting - session_->OnSettingsFrame(settings); - } + CompleteFirstConnection(); CreateConnection(); // Session configs should be in initial state. @@ -1064,15 +1068,7 @@ return; } - CompleteCryptoHandshake(); - EXPECT_FALSE(session_->GetCryptoStream()->IsResumption()); - if (session_->version().UsesHttp3()) { - SettingsFrame settings; - settings.values[SETTINGS_QPACK_MAX_TABLE_CAPACITY] = 2; - settings.values[SETTINGS_MAX_HEADER_LIST_SIZE] = 5; - settings.values[256] = 4; // unknown setting - session_->OnSettingsFrame(settings); - } + CompleteFirstConnection(); // Create a second connection, but disable 0-RTT on the server. CreateConnection(); @@ -1108,6 +1104,136 @@ EXPECT_TRUE(session_->GetCryptoStream()->IsResumption()); } +// When IETF QUIC 0-RTT is rejected, a server-sent fresh transport params is +// available. If the new transport params reduces stream/flow control limit to +// lower than what the client has already used, connection will be closed. +TEST_P(QuicSpdyClientSessionTest, ZeroRttRejectReducesStreamLimitTooMuch) { + // This feature is TLS-only. + if (session_->version().UsesQuicCrypto()) { + return; + } + + CompleteFirstConnection(); + + // Create a second connection, but disable 0-RTT on the server. + CreateConnection(); + QuicConfig config = DefaultQuicConfig(); + // Server doesn't allow any bidirectional streams. + config.SetMaxBidirectionalStreamsToSend(0); + SSL_CTX_set_early_data_enabled(server_crypto_config_->ssl_ctx(), false); + session_->CryptoConnect(); + EXPECT_TRUE(session_->IsEncryptionEstablished()); + QuicSpdyClientStream* stream = session_->CreateOutgoingBidirectionalStream(); + ASSERT_TRUE(stream); + + if (session_->version().UsesHttp3()) { + EXPECT_CALL( + *connection_, + CloseConnection( + QUIC_INTERNAL_ERROR, + "Server rejected 0-RTT, aborting because new bidirectional initial " + "stream limit 0 is less than current open streams: 1", + _)) + .WillOnce(testing::Invoke(connection_, + &MockQuicConnection::ReallyCloseConnection)); + } else { + EXPECT_CALL( + *connection_, + CloseConnection(QUIC_INTERNAL_ERROR, + "Server rejected 0-RTT, aborting because new stream " + "limit 0 is less than current open streams: 1", + _)) + .WillOnce(testing::Invoke(connection_, + &MockQuicConnection::ReallyCloseConnection)); + } + EXPECT_CALL(*connection_, CloseConnection(QUIC_HANDSHAKE_FAILED, _, _)); + + crypto_test_utils::HandshakeWithFakeServer( + &config, server_crypto_config_.get(), &helper_, &alarm_factory_, + connection_, crypto_stream_, AlpnForVersion(connection_->version())); +} + +TEST_P(QuicSpdyClientSessionTest, + ZeroRttRejectReducesStreamFlowControlTooMuch) { + // This feature is TLS-only. + if (session_->version().UsesQuicCrypto()) { + return; + } + + CompleteFirstConnection(); + + // Create a second connection, but disable 0-RTT on the server. + CreateConnection(); + QuicConfig config = DefaultQuicConfig(); + // Server doesn't allow any outgoing streams. + config.SetInitialMaxStreamDataBytesIncomingBidirectionalToSend(1); + config.SetInitialMaxStreamDataBytesUnidirectionalToSend(1); + SSL_CTX_set_early_data_enabled(server_crypto_config_->ssl_ctx(), false); + session_->CryptoConnect(); + EXPECT_TRUE(session_->IsEncryptionEstablished()); + QuicSpdyClientStream* stream = session_->CreateOutgoingBidirectionalStream(); + ASSERT_TRUE(stream); + // Let the stream write more than 1 byte of data. + stream->WriteOrBufferData("hello", true, nullptr); + + if (session_->version().UsesHttp3()) { + // Both control stream and the request stream will report errors. + EXPECT_CALL(*connection_, CloseConnection(QUIC_INTERNAL_ERROR, _, _)) + .Times(2) + .WillOnce(testing::Invoke(connection_, + &MockQuicConnection::ReallyCloseConnection)); + } else { + EXPECT_CALL(*connection_, + CloseConnection( + QUIC_INTERNAL_ERROR, + "Server rejected 0-RTT, aborting because new stream max " + "data 1 for stream 3 is less than currently used: 5", + _)) + .Times(1) + .WillOnce(testing::Invoke(connection_, + &MockQuicConnection::ReallyCloseConnection)); + } + EXPECT_CALL(*connection_, CloseConnection(QUIC_HANDSHAKE_FAILED, _, _)); + + crypto_test_utils::HandshakeWithFakeServer( + &config, server_crypto_config_.get(), &helper_, &alarm_factory_, + connection_, crypto_stream_, AlpnForVersion(connection_->version())); +} + +TEST_P(QuicSpdyClientSessionTest, + ZeroRttRejectReducesSessionFlowControlTooMuch) { + // This feature is TLS-only. + if (session_->version().UsesQuicCrypto()) { + return; + } + + CompleteFirstConnection(); + + // Create a second connection, but disable 0-RTT on the server. + CreateConnection(); + QuicConfig config = DefaultQuicConfig(); + // Server doesn't allow minimum data in session. + config.SetInitialSessionFlowControlWindowToSend( + kMinimumFlowControlSendWindow); + SSL_CTX_set_early_data_enabled(server_crypto_config_->ssl_ctx(), false); + session_->CryptoConnect(); + EXPECT_TRUE(session_->IsEncryptionEstablished()); + QuicSpdyClientStream* stream = session_->CreateOutgoingBidirectionalStream(); + ASSERT_TRUE(stream); + std::string data_to_send(kMinimumFlowControlSendWindow + 1, 'x'); + // Let the stream write some data. + stream->WriteOrBufferData(data_to_send, true, nullptr); + + EXPECT_CALL(*connection_, CloseConnection(QUIC_INTERNAL_ERROR, _, _)) + .WillOnce(testing::Invoke(connection_, + &MockQuicConnection::ReallyCloseConnection)); + EXPECT_CALL(*connection_, CloseConnection(QUIC_HANDSHAKE_FAILED, _, _)); + + crypto_test_utils::HandshakeWithFakeServer( + &config, server_crypto_config_.get(), &helper_, &alarm_factory_, + connection_, crypto_stream_, AlpnForVersion(connection_->version())); +} + } // namespace } // namespace test } // namespace quic
diff --git a/quic/core/quic_flow_controller.h b/quic/core/quic_flow_controller.h index 29c3c02..daad3d9 100644 --- a/quic/core/quic_flow_controller.h +++ b/quic/core/quic_flow_controller.h
@@ -91,6 +91,8 @@ QuicByteCount bytes_consumed() const { return bytes_consumed_; } + QuicByteCount bytes_sent() const { return bytes_sent_; } + QuicStreamOffset send_window_offset() const { return send_window_offset_; } QuicStreamOffset highest_received_byte_offset() const {
diff --git a/quic/core/quic_session.cc b/quic/core/quic_session.cc index 912b0cb..a697dc7 100644 --- a/quic/core/quic_session.cc +++ b/quic/core/quic_session.cc
@@ -97,7 +97,8 @@ supported_versions_(supported_versions), use_http2_priority_write_scheduler_(false), is_configured_(false), - enable_round_robin_scheduling_(false) { + enable_round_robin_scheduling_(false), + was_zero_rtt_rejected_(false) { closed_streams_clean_up_alarm_ = QuicWrapUnique<QuicAlarm>(connection_->alarm_factory()->CreateAlarm( new ClosedStreamsCleanUpDelegate(this))); @@ -1022,6 +1023,19 @@ if (config_.HasReceivedMaxBidirectionalStreams()) { max_streams = config_.ReceivedMaxBidirectionalStreams(); } + if (was_zero_rtt_rejected_ && + max_streams < + v99_streamid_manager_.outgoing_bidirectional_stream_count()) { + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, + quiche::QuicheStrCat( + "Server rejected 0-RTT, aborting because new bidirectional " + "initial stream limit ", + max_streams, " is less than current open streams: ", + v99_streamid_manager_.outgoing_bidirectional_stream_count()), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } QUIC_DVLOG(1) << ENDPOINT << "Setting Bidirectional outgoing_max_streams_ to " << max_streams; @@ -1046,6 +1060,21 @@ if (config_.HasReceivedMaxUnidirectionalStreams()) { max_streams = config_.ReceivedMaxUnidirectionalStreams(); } + + if (was_zero_rtt_rejected_ && + max_streams < + v99_streamid_manager_.outgoing_unidirectional_stream_count()) { + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, + quiche::QuicheStrCat( + "Server rejected 0-RTT, aborting because new unidirectional " + "initial stream limit ", + max_streams, " is less than current open streams: ", + v99_streamid_manager_.outgoing_unidirectional_stream_count()), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } + if (max_streams < v99_streamid_manager_.max_outgoing_unidirectional_streams()) { connection_->CloseConnection( @@ -1071,6 +1100,17 @@ } QUIC_DVLOG(1) << ENDPOINT << "Setting max_open_outgoing_streams_ to " << max_streams; + if (was_zero_rtt_rejected_ && + max_streams < stream_id_manager_.num_open_outgoing_streams()) { + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, + quiche::QuicheStrCat( + "Server rejected 0-RTT, aborting because new stream limit ", + max_streams, " is less than current open streams: ", + stream_id_manager_.num_open_outgoing_streams()), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } stream_id_manager_.set_max_open_outgoing_streams(max_streams); } @@ -1281,6 +1321,18 @@ } QUIC_DVLOG(1) << ENDPOINT << "Informing unidirectional stream " << id << " of new stream flow control window " << new_window; + if (was_zero_rtt_rejected_ && + new_window < kv.second->flow_controller()->bytes_sent()) { + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, + quiche::QuicheStrCat( + "Server rejected 0-RTT, aborting because new stream max data ", + new_window, " for stream ", kv.first, + " is less than currently used: ", + kv.second->flow_controller()->bytes_sent()), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } if (!kv.second->ConfigSendWindowOffset(new_window)) { return; } @@ -1305,6 +1357,18 @@ } QUIC_DVLOG(1) << ENDPOINT << "Informing outgoing bidirectional stream " << id << " of new stream flow control window " << new_window; + if (was_zero_rtt_rejected_ && + new_window < kv.second->flow_controller()->bytes_sent()) { + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, + quiche::QuicheStrCat( + "Server rejected 0-RTT, aborting because new stream max data ", + new_window, " for stream ", kv.first, + " is less than currently used: ", + kv.second->flow_controller()->bytes_sent()), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } if (!kv.second->ConfigSendWindowOffset(new_window)) { return; } @@ -1329,6 +1393,18 @@ } QUIC_DVLOG(1) << ENDPOINT << "Informing incoming bidirectional stream " << id << " of new stream flow control window " << new_window; + if (was_zero_rtt_rejected_ && + new_window < kv.second->flow_controller()->bytes_sent()) { + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, + quiche::QuicheStrCat( + "Server rejected 0-RTT, aborting because new stream max data ", + new_window, " for stream ", kv.first, + " is less than currently used: ", + kv.second->flow_controller()->bytes_sent()), + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } if (!kv.second->ConfigSendWindowOffset(new_window)) { return; } @@ -1337,28 +1413,40 @@ void QuicSession::OnNewSessionFlowControlWindow(QuicStreamOffset new_window) { QUIC_DVLOG(1) << ENDPOINT << "OnNewSessionFlowControlWindow " << new_window; - bool close_connection = false; - if (!connection_->version().AllowsLowFlowControlLimits()) { - if (new_window < kMinimumFlowControlSendWindow) { - close_connection = true; - QUIC_LOG_FIRST_N(ERROR, 1) - << "Peer sent us an invalid session flow control send window: " - << new_window << ", below default: " << kMinimumFlowControlSendWindow; - } - } else if (perspective_ == Perspective::IS_CLIENT && - new_window < flow_controller_.send_window_offset()) { + + if (was_zero_rtt_rejected_ && new_window < flow_controller_.bytes_sent()) { + std::string error_details = quiche::QuicheStrCat( + "Server rejected 0-RTT. Aborting because the client received session " + "flow control send window: ", + new_window, + ", which is below currently used: ", flow_controller_.bytes_sent()); + QUIC_LOG(ERROR) << error_details; + connection_->CloseConnection( + QUIC_INTERNAL_ERROR, error_details, + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } + if (!connection_->version().AllowsLowFlowControlLimits() && + new_window < kMinimumFlowControlSendWindow) { + std::string error_details = quiche::QuicheStrCat( + "Peer sent us an invalid session flow control send window: ", + new_window, ", below minimum: ", kMinimumFlowControlSendWindow); + QUIC_LOG_FIRST_N(ERROR, 1) << error_details; + connection_->CloseConnection( + QUIC_FLOW_CONTROL_INVALID_WINDOW, error_details, + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } + if (perspective_ == Perspective::IS_CLIENT && + new_window < flow_controller_.send_window_offset()) { // The client receives a lower limit than remembered, violating // https://tools.ietf.org/html/draft-ietf-quic-transport-27#section-7.3.1 - QUIC_LOG_FIRST_N(ERROR, 1) - << "Peer sent us an invalid session flow control send window: " - << new_window - << ", below current: " << flow_controller_.send_window_offset(); - close_connection = true; - } - if (close_connection) { + std::string error_details = quiche::QuicheStrCat( + "Peer sent us an invalid session flow control send window: ", + new_window, ", below current: ", flow_controller_.send_window_offset()); + QUIC_LOG(ERROR) << error_details; connection_->CloseConnection( - QUIC_FLOW_CONTROL_INVALID_WINDOW, - quiche::QuicheStrCat("New connection window too low: ", new_window), + QUIC_FLOW_CONTROL_INVALID_WINDOW, error_details, ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); return; } @@ -1486,9 +1574,7 @@ } void QuicSession::OnZeroRttRejected() { - // TODO(b/153726130): Read stream limit and flow control limit from server - // transport params, and close the connection proactively if client has used - // too many. + was_zero_rtt_rejected_ = true; connection_->RetransmitZeroRttPackets(); if (connection_->encryption_level() == ENCRYPTION_FORWARD_SECURE) { QUIC_BUG << "1-RTT keys already available when 0-RTT is rejected.";
diff --git a/quic/core/quic_session.h b/quic/core/quic_session.h index 61019a4..33abcfc 100644 --- a/quic/core/quic_session.h +++ b/quic/core/quic_session.h
@@ -795,6 +795,9 @@ // If true, enables round robin scheduling. bool enable_round_robin_scheduling_; + + // Whether the session has received a 0-RTT rejection (QUIC+TLS only). + bool was_zero_rtt_rejected_; }; } // namespace quic
diff --git a/quic/core/uber_quic_stream_id_manager.cc b/quic/core/uber_quic_stream_id_manager.cc index 6951b2c..64cbd58 100644 --- a/quic/core/uber_quic_stream_id_manager.cc +++ b/quic/core/uber_quic_stream_id_manager.cc
@@ -162,4 +162,14 @@ return unidirectional_stream_id_manager_.incoming_advertised_max_streams(); } +QuicStreamCount UberQuicStreamIdManager::outgoing_bidirectional_stream_count() + const { + return bidirectional_stream_id_manager_.outgoing_stream_count(); +} + +QuicStreamCount UberQuicStreamIdManager::outgoing_unidirectional_stream_count() + const { + return unidirectional_stream_id_manager_.outgoing_stream_count(); +} + } // namespace quic
diff --git a/quic/core/uber_quic_stream_id_manager.h b/quic/core/uber_quic_stream_id_manager.h index b1fc126..aafe0b6 100644 --- a/quic/core/uber_quic_stream_id_manager.h +++ b/quic/core/uber_quic_stream_id_manager.h
@@ -87,6 +87,9 @@ QuicStreamCount advertised_max_incoming_bidirectional_streams() const; QuicStreamCount advertised_max_incoming_unidirectional_streams() const; + QuicStreamCount outgoing_bidirectional_stream_count() const; + QuicStreamCount outgoing_unidirectional_stream_count() const; + private: friend class test::QuicSessionPeer; friend class test::UberQuicStreamIdManagerPeer;