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 {