Handle HTTP/3 SETTINGS on 0-RTT client connection.

In IETF QUIC, if 0-RTT is enabled, the client will receive 2 SETTINGS, one from cache and one from the fresh server sent SETTINGS.
If 0-RTT is accepted, the server shouldn't change dynamic table capacity according to https://quicwg.org/base-drafts/draft-ietf-quic-qpack.html#name-maximum-dynamic-table-capac. For other settings, https://www.google.com/url?sa=D&q=https%3A%2F%2Fquicwg.org%2Fbase-drafts%2Fdraft-ietf-quic-http.html%23name-initialization specifies they shouldn't be decreased.
When 0-RTT is rejected, our current approach is to retransmit 0-RTT packets. If the server changes SETTINGS, our implementation doesn't have an easy way to comply with the new SETTINGS, thus we close the connection too.

Protected by quic_enable_zero_rtt_for_tls

PiperOrigin-RevId: 316128955
Change-Id: I84b277b3c57d6a8009aec5a547c6416542122ec6
diff --git a/quic/core/http/quic_receive_control_stream.cc b/quic/core/http/quic_receive_control_stream.cc
index d24e3dd..27de70d 100644
--- a/quic/core/http/quic_receive_control_stream.cc
+++ b/quic/core/http/quic_receive_control_stream.cc
@@ -131,8 +131,7 @@
 bool QuicReceiveControlStream::OnSettingsFrame(const SettingsFrame& frame) {
   QUIC_DVLOG(1) << "Control Stream " << id()
                 << " received settings frame: " << frame;
-  spdy_session_->OnSettingsFrame(frame);
-  return true;
+  return spdy_session_->OnSettingsFrame(frame);
 }
 
 bool QuicReceiveControlStream::OnDataFrameStart(QuicByteCount /*header_length*/,
diff --git a/quic/core/http/quic_spdy_client_session_base.cc b/quic/core/http/quic_spdy_client_session_base.cc
index 86592bb..dfef4db 100644
--- a/quic/core/http/quic_spdy_client_session_base.cc
+++ b/quic/core/http/quic_spdy_client_session_base.cc
@@ -233,8 +233,10 @@
          num_outgoing_draining_streams() > 0;
 }
 
-void QuicSpdyClientSessionBase::OnSettingsFrame(const SettingsFrame& frame) {
-  QuicSpdySession::OnSettingsFrame(frame);
+bool QuicSpdyClientSessionBase::OnSettingsFrame(const SettingsFrame& frame) {
+  if (!QuicSpdySession::OnSettingsFrame(frame)) {
+    return false;
+  }
   std::unique_ptr<char[]> buffer;
   QuicByteCount frame_length =
       HttpEncoder::SerializeSettingsFrame(frame, &buffer);
@@ -242,6 +244,7 @@
       buffer.get(), buffer.get() + frame_length);
   GetMutableCryptoStream()->SetServerApplicationStateForResumption(
       std::move(serialized_data));
+  return true;
 }
 
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_client_session_base.h b/quic/core/http/quic_spdy_client_session_base.h
index 20f4087..3192479 100644
--- a/quic/core/http/quic_spdy_client_session_base.h
+++ b/quic/core/http/quic_spdy_client_session_base.h
@@ -123,7 +123,7 @@
   }
 
   // Override to serialize the settings and pass it down to the handshaker.
-  void OnSettingsFrame(const SettingsFrame& frame) override;
+  bool OnSettingsFrame(const SettingsFrame& frame) override;
 
  private:
   // For QuicSpdyClientStream to detect that a response corresponds to a
diff --git a/quic/core/http/quic_spdy_client_session_test.cc b/quic/core/http/quic_spdy_client_session_test.cc
index fc6343e..0a0f5e9 100644
--- a/quic/core/http/quic_spdy_client_session_test.cc
+++ b/quic/core/http/quic_spdy_client_session_test.cc
@@ -1061,6 +1061,17 @@
     EXPECT_EQ(kDefaultMaxStreamsPerConnection + 1,
               id_manager->max_open_outgoing_streams());
   }
+
+  EXPECT_CALL(*connection_, CloseConnection(_, _, _)).Times(0);
+  // Let the session receive a new SETTINGS frame to complete the second
+  // connection.
+  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);
+  }
 }
 
 TEST_P(QuicSpdyClientSessionTest, RetransmitDataOnZeroRttReject) {
@@ -1348,6 +1359,27 @@
   EXPECT_TRUE(session_->GetCryptoStream()->IsResumption());
 }
 
+TEST_P(QuicSpdyClientSessionTest, BadSettingsInZeroRtt) {
+  if (!session_->version().UsesHttp3()) {
+    return;
+  }
+
+  CompleteFirstConnection();
+
+  CreateConnection();
+  CompleteCryptoHandshake();
+
+  EXPECT_CALL(*connection_, CloseConnection(QUIC_INTERNAL_ERROR, _, _))
+      .WillOnce(testing::Invoke(connection_,
+                                &MockQuicConnection::ReallyCloseConnection));
+  // Let the session receive a different SETTINGS frame.
+  SettingsFrame settings;
+  settings.values[SETTINGS_QPACK_MAX_TABLE_CAPACITY] = 1;
+  settings.values[SETTINGS_MAX_HEADER_LIST_SIZE] = 5;
+  settings.values[256] = 4;  // unknown setting
+  session_->OnSettingsFrame(settings);
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/core/http/quic_spdy_session.cc b/quic/core/http/quic_spdy_session.cc
index 386fb8a..836404f 100644
--- a/quic/core/http/quic_spdy_session.cc
+++ b/quic/core/http/quic_spdy_session.cc
@@ -874,52 +874,85 @@
   return true;
 }
 
-void QuicSpdySession::OnSettingsFrame(const SettingsFrame& frame) {
+bool QuicSpdySession::OnSettingsFrame(const SettingsFrame& frame) {
   DCHECK(VersionUsesHttp3(transport_version()));
   if (debug_visitor_ != nullptr) {
     debug_visitor_->OnSettingsFrameReceived(frame);
   }
   for (const auto& setting : frame.values) {
-    OnSetting(setting.first, setting.second);
+    if (!OnSetting(setting.first, setting.second)) {
+      return false;
+    }
   }
+  return true;
 }
 
-void QuicSpdySession::OnSetting(uint64_t id, uint64_t value) {
+bool QuicSpdySession::OnSetting(uint64_t id, uint64_t value) {
+  // TODO(b/158614287): If cached SETTINGS has SETTINGS_QPACK_MAX_TABLE_CAPACITY
+  // and SETTINGS_MAX_HEADER_LIST_SIZE, and the server accepts 0-RTT connection,
+  // make sure the fresh SETTINGS contains the same values.
   if (VersionUsesHttp3(transport_version())) {
     // SETTINGS frame received on the control stream.
     switch (id) {
-      case SETTINGS_QPACK_MAX_TABLE_CAPACITY:
+      case SETTINGS_QPACK_MAX_TABLE_CAPACITY: {
         QUIC_DVLOG(1)
             << ENDPOINT
             << "SETTINGS_QPACK_MAX_TABLE_CAPACITY received with value "
             << value;
         // Communicate |value| to encoder, because it is used for encoding
         // Required Insert Count.
-        qpack_encoder_->SetMaximumDynamicTableCapacity(value);
+        bool success = qpack_encoder_->SetMaximumDynamicTableCapacity(value);
+        if (GetQuicReloadableFlag(quic_enable_zero_rtt_for_tls) && !success) {
+          // TODO(b/153726130): Use different error code for the case of 0-RTT
+          // rejection.
+          CloseConnectionWithDetails(
+              QUIC_INTERNAL_ERROR,
+              "Server sent an invalid SETTINGS_QPACK_MAX_TABLE_CAPACITY.");
+          return false;
+        }
         // However, limit the dynamic table capacity to
         // |qpack_maximum_dynamic_table_capacity_|.
         qpack_encoder_->SetDynamicTableCapacity(
             std::min(value, qpack_maximum_dynamic_table_capacity_));
         break;
+      }
       case SETTINGS_MAX_HEADER_LIST_SIZE:
         QUIC_DVLOG(1) << ENDPOINT
                       << "SETTINGS_MAX_HEADER_LIST_SIZE received with value "
                       << value;
+        if (GetQuicReloadableFlag(quic_enable_zero_rtt_for_tls) &&
+            max_outbound_header_list_size_ < value) {
+          // TODO(b/153726130): Use different error code for the case of 0-RTT
+          // rejection.
+          CloseConnectionWithDetails(
+              QUIC_INTERNAL_ERROR,
+              "Server sent an invalid SETTINGS_MAX_HEADER_LIST_SIZE.");
+          return false;
+        }
         max_outbound_header_list_size_ = value;
         break;
-      case SETTINGS_QPACK_BLOCKED_STREAMS:
+      case SETTINGS_QPACK_BLOCKED_STREAMS: {
         QUIC_DVLOG(1) << ENDPOINT
                       << "SETTINGS_QPACK_BLOCKED_STREAMS received with value "
                       << value;
-        qpack_encoder_->SetMaximumBlockedStreams(value);
+        bool success = qpack_encoder_->SetMaximumBlockedStreams(value);
+        // TODO(b/153726130): Use different error code for the case of 0-RTT
+        // rejection.
+        if (GetQuicReloadableFlag(quic_enable_zero_rtt_for_tls) && !success) {
+          CloseConnectionWithDetails(
+              QUIC_INTERNAL_ERROR,
+              "Server sent an invalid SETTINGS_QPACK_BLOCKED_STREAMS.");
+          return false;
+        }
         break;
+      }
       default:
         QUIC_DVLOG(1) << ENDPOINT << "Unknown setting identifier " << id
                       << " received with value " << value;
         // Ignore unknown settings.
         break;
     }
-    return;
+    return true;
   }
 
   // SETTINGS frame received on the headers stream.
@@ -942,7 +975,7 @@
                 quiche::QuicheStrCat("Invalid value for SETTINGS_ENABLE_PUSH: ",
                                      value));
           }
-          return;
+          return true;
         }
         QUIC_DVLOG(1) << ENDPOINT << "SETTINGS_ENABLE_PUSH received with value "
                       << value;
@@ -977,6 +1010,7 @@
                                  id));
       }
   }
+  return true;
 }
 
 bool QuicSpdySession::ShouldReleaseHeadersStreamSequencerBuffer() {
diff --git a/quic/core/http/quic_spdy_session.h b/quic/core/http/quic_spdy_session.h
index 013bfc3..b948002 100644
--- a/quic/core/http/quic_spdy_session.h
+++ b/quic/core/http/quic_spdy_session.h
@@ -247,10 +247,14 @@
   bool server_push_enabled() const;
 
   // Called when the control stream receives HTTP/3 SETTINGS.
-  virtual void OnSettingsFrame(const SettingsFrame& frame);
+  // Returns false in case of 0-RTT if received settings are incompatible with
+  // cached values, true otherwise.
+  virtual bool OnSettingsFrame(const SettingsFrame& frame);
 
-  // Called when a setting is parsed from an incoming SETTINGS frame.
-  void OnSetting(uint64_t id, uint64_t value);
+  // Called when a SETTINGS is parsed from an incoming SETTINGS frame.
+  // Returns false in case of 0-RTT if received SETTINGS is incompatible with
+  // cached value, true otherwise.
+  bool OnSetting(uint64_t id, uint64_t value);
 
   // Return true if this session wants to release headers stream's buffer
   // aggressively.
diff --git a/quic/core/qpack/qpack_encoder.cc b/quic/core/qpack/qpack_encoder.cc
index db7ec79..67adf12 100644
--- a/quic/core/qpack/qpack_encoder.cc
+++ b/quic/core/qpack/qpack_encoder.cc
@@ -378,9 +378,10 @@
   return SecondPassEncode(std::move(instructions), required_insert_count);
 }
 
-void QpackEncoder::SetMaximumDynamicTableCapacity(
+bool QpackEncoder::SetMaximumDynamicTableCapacity(
     uint64_t maximum_dynamic_table_capacity) {
-  header_table_.SetMaximumDynamicTableCapacity(maximum_dynamic_table_capacity);
+  return header_table_.SetMaximumDynamicTableCapacity(
+      maximum_dynamic_table_capacity);
 }
 
 void QpackEncoder::SetDynamicTableCapacity(uint64_t dynamic_table_capacity) {
@@ -392,8 +393,12 @@
   DCHECK(success);
 }
 
-void QpackEncoder::SetMaximumBlockedStreams(uint64_t maximum_blocked_streams) {
+bool QpackEncoder::SetMaximumBlockedStreams(uint64_t maximum_blocked_streams) {
+  if (maximum_blocked_streams < maximum_blocked_streams_) {
+    return false;
+  }
   maximum_blocked_streams_ = maximum_blocked_streams;
+  return true;
 }
 
 void QpackEncoder::OnInsertCountIncrement(uint64_t increment) {
diff --git a/quic/core/qpack/qpack_encoder.h b/quic/core/qpack/qpack_encoder.h
index 0f1d14c..8b75097 100644
--- a/quic/core/qpack/qpack_encoder.h
+++ b/quic/core/qpack/qpack_encoder.h
@@ -63,7 +63,10 @@
   // measured in bytes.  Called when SETTINGS_QPACK_MAX_TABLE_CAPACITY is
   // received.  Encoder needs to know this value so that it can calculate
   // MaxEntries, used as a modulus to encode Required Insert Count.
-  void SetMaximumDynamicTableCapacity(uint64_t maximum_dynamic_table_capacity);
+  // Returns true if |maximum_dynamic_table_capacity| is set for the first time
+  // or if it doesn't change current value. The setting is not changed when
+  // returning false.
+  bool SetMaximumDynamicTableCapacity(uint64_t maximum_dynamic_table_capacity);
 
   // Set dynamic table capacity to |dynamic_table_capacity|.
   // |dynamic_table_capacity| must not exceed maximum dynamic table capacity.
@@ -72,7 +75,9 @@
 
   // Set maximum number of blocked streams.
   // Called when SETTINGS_QPACK_BLOCKED_STREAMS is received.
-  void SetMaximumBlockedStreams(uint64_t maximum_blocked_streams);
+  // Returns true if |maximum_blocked_streams| doesn't decrease current value.
+  // The setting is not changed when returning false.
+  bool SetMaximumBlockedStreams(uint64_t maximum_blocked_streams);
 
   // QpackDecoderStreamReceiver::Delegate implementation
   void OnInsertCountIncrement(uint64_t increment) override;
diff --git a/quic/core/qpack/qpack_header_table.cc b/quic/core/qpack/qpack_header_table.cc
index 472db89..29e7148 100644
--- a/quic/core/qpack/qpack_header_table.cc
+++ b/quic/core/qpack/qpack_header_table.cc
@@ -186,16 +186,15 @@
   return true;
 }
 
-void QpackHeaderTable::SetMaximumDynamicTableCapacity(
+bool QpackHeaderTable::SetMaximumDynamicTableCapacity(
     uint64_t maximum_dynamic_table_capacity) {
-  // This method can only be called once: in the decoding context, shortly after
-  // construction; in the encoding context, upon receiving the SETTINGS frame.
-  DCHECK_EQ(0u, dynamic_table_capacity_);
-  DCHECK_EQ(0u, maximum_dynamic_table_capacity_);
-  DCHECK_EQ(0u, max_entries_);
-
-  maximum_dynamic_table_capacity_ = maximum_dynamic_table_capacity;
-  max_entries_ = maximum_dynamic_table_capacity / 32;
+  if (maximum_dynamic_table_capacity_ == 0) {
+    maximum_dynamic_table_capacity_ = maximum_dynamic_table_capacity;
+    max_entries_ = maximum_dynamic_table_capacity / 32;
+    return true;
+  }
+  // If the value is already set, it should not be changed.
+  return maximum_dynamic_table_capacity == maximum_dynamic_table_capacity_;
 }
 
 void QpackHeaderTable::RegisterObserver(uint64_t required_insert_count,
diff --git a/quic/core/qpack/qpack_header_table.h b/quic/core/qpack/qpack_header_table.h
index e3fb975..bed1cc8 100644
--- a/quic/core/qpack/qpack_header_table.h
+++ b/quic/core/qpack/qpack_header_table.h
@@ -97,7 +97,10 @@
   // value can be set upon connection establishment, whereas in the encoding
   // context it can be set when the SETTINGS frame is received.
   // This method must only be called at most once.
-  void SetMaximumDynamicTableCapacity(uint64_t maximum_dynamic_table_capacity);
+  // Returns true if |maximum_dynamic_table_capacity| is set for the first time
+  // or if it doesn't change current value. The setting is not changed when
+  // returning false.
+  bool SetMaximumDynamicTableCapacity(uint64_t maximum_dynamic_table_capacity);
 
   // Get |maximum_dynamic_table_capacity_|.
   uint64_t maximum_dynamic_table_capacity() const {