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 {