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;