Use detailed error code when client cached 0-RTT stream/flow control limit is reduced by server.
Protected by quic_enable_zero_rtt_for_tls
PiperOrigin-RevId: 316531305
Change-Id: Ia9bf39307b4e19b345605cbd8a13ed4874e7b3b8
diff --git a/quic/core/http/quic_spdy_client_session_test.cc b/quic/core/http/quic_spdy_client_session_test.cc
index 0a0f5e9..5c0fdb8 100644
--- a/quic/core/http/quic_spdy_client_session_test.cc
+++ b/quic/core/http/quic_spdy_client_session_test.cc
@@ -1142,7 +1142,7 @@
EXPECT_CALL(
*connection_,
CloseConnection(
- QUIC_INTERNAL_ERROR,
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE,
"Server rejected 0-RTT, aborting because new bidirectional initial "
"stream limit 0 is less than current open streams: 1",
_))
@@ -1190,14 +1190,15 @@
if (session_->version().UsesHttp3()) {
// Both control stream and the request stream will report errors.
- EXPECT_CALL(*connection_, CloseConnection(QUIC_INTERNAL_ERROR, _, _))
+ EXPECT_CALL(*connection_,
+ CloseConnection(QUIC_ZERO_RTT_UNRETRANSMITTABLE, _, _))
.Times(2)
.WillOnce(testing::Invoke(connection_,
&MockQuicConnection::ReallyCloseConnection));
} else {
EXPECT_CALL(*connection_,
CloseConnection(
- QUIC_INTERNAL_ERROR,
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE,
"Server rejected 0-RTT, aborting because new stream max "
"data 1 for stream 3 is less than currently used: 5",
_))
@@ -1236,7 +1237,8 @@
// Let the stream write some data.
stream->WriteOrBufferData(data_to_send, true, nullptr);
- EXPECT_CALL(*connection_, CloseConnection(QUIC_INTERNAL_ERROR, _, _))
+ EXPECT_CALL(*connection_,
+ CloseConnection(QUIC_ZERO_RTT_UNRETRANSMITTABLE, _, _))
.WillOnce(testing::Invoke(connection_,
&MockQuicConnection::ReallyCloseConnection));
EXPECT_CALL(*connection_, CloseConnection(QUIC_HANDSHAKE_FAILED, _, _));
diff --git a/quic/core/quic_error_codes.cc b/quic/core/quic_error_codes.cc
index 6afccfe..4b2c737 100644
--- a/quic/core/quic_error_codes.cc
+++ b/quic/core/quic_error_codes.cc
@@ -221,6 +221,9 @@
RETURN_STRING_LITERAL(QUIC_HPACK_TRUNCATED_BLOCK);
RETURN_STRING_LITERAL(QUIC_HPACK_FRAGMENT_TOO_LONG);
RETURN_STRING_LITERAL(QUIC_HPACK_COMPRESSED_HEADER_SIZE_EXCEEDS_LIMIT);
+ RETURN_STRING_LITERAL(QUIC_ZERO_RTT_UNRETRANSMITTABLE);
+ RETURN_STRING_LITERAL(QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED);
+ RETURN_STRING_LITERAL(QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED);
RETURN_STRING_LITERAL(QUIC_LAST_ERROR);
// Intentionally have no default case, so we'll break the build
@@ -599,6 +602,12 @@
return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
case QUIC_HPACK_COMPRESSED_HEADER_SIZE_EXCEEDS_LIMIT:
return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
+ case QUIC_ZERO_RTT_UNRETRANSMITTABLE:
+ return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
+ case QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED:
+ return {true, static_cast<uint64_t>(INTERNAL_ERROR)};
+ case QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED:
+ return {true, static_cast<uint64_t>(PROTOCOL_VIOLATION)};
case QUIC_LAST_ERROR:
return {false, static_cast<uint64_t>(QUIC_LAST_ERROR)};
}
diff --git a/quic/core/quic_error_codes.h b/quic/core/quic_error_codes.h
index f057fea..f5c5007 100644
--- a/quic/core/quic_error_codes.h
+++ b/quic/core/quic_error_codes.h
@@ -466,8 +466,20 @@
// Total compressed HPACK data size exceeds limit.
QUIC_HPACK_COMPRESSED_HEADER_SIZE_EXCEEDS_LIMIT = 150,
+ // Stream/flow control limit from 1-RTT handshake is too low to retransmit
+ // 0-RTT data. This is our implentation error. We could in theory keep the
+ // connection alive but chose not to for simplicity.
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE = 161,
+ // Stream/flow control limit from 0-RTT rejection reduces cached limit.
+ // This is our implentation error. We could in theory keep the connection
+ // alive but chose not to for simplicity.
+ QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED = 162,
+ // Stream/flow control limit from 0-RTT resumption reduces cached limit.
+ // This is the peer violating QUIC spec.
+ QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED = 163,
+
// No error. Used as bound while iterating.
- QUIC_LAST_ERROR = 161,
+ QUIC_LAST_ERROR = 164,
};
// QuicErrorCodes is encoded as four octets on-the-wire when doing Google QUIC,
// or a varint62 when doing IETF QUIC. Ensure that its value does not exceed
diff --git a/quic/core/quic_session.cc b/quic/core/quic_session.cc
index 5043f1f..b697457 100644
--- a/quic/core/quic_session.cc
+++ b/quic/core/quic_session.cc
@@ -1027,7 +1027,7 @@
max_streams <
v99_streamid_manager_.outgoing_bidirectional_stream_count()) {
connection_->CloseConnection(
- QUIC_INTERNAL_ERROR,
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE,
quiche::QuicheStrCat(
"Server rejected 0-RTT, aborting because new bidirectional "
"initial stream limit ",
@@ -1043,8 +1043,12 @@
max_streams <
v99_streamid_manager_.max_outgoing_bidirectional_streams()) {
connection_->CloseConnection(
- QUIC_MAX_STREAMS_ERROR,
+ was_zero_rtt_rejected_ ? QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED
+ : QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED,
quiche::QuicheStrCat(
+ was_zero_rtt_rejected_
+ ? "Server rejected 0-RTT, aborting because "
+ : "",
"new bidirectional limit ", max_streams,
" decreases the current limit: ",
v99_streamid_manager_.max_outgoing_bidirectional_streams()),
@@ -1065,7 +1069,7 @@
max_streams <
v99_streamid_manager_.outgoing_unidirectional_stream_count()) {
connection_->CloseConnection(
- QUIC_INTERNAL_ERROR,
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE,
quiche::QuicheStrCat(
"Server rejected 0-RTT, aborting because new unidirectional "
"initial stream limit ",
@@ -1078,8 +1082,12 @@
if (max_streams <
v99_streamid_manager_.max_outgoing_unidirectional_streams()) {
connection_->CloseConnection(
- QUIC_MAX_STREAMS_ERROR,
+ was_zero_rtt_rejected_ ? QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED
+ : QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED,
quiche::QuicheStrCat(
+ was_zero_rtt_rejected_
+ ? "Server rejected 0-RTT, aborting because "
+ : "",
"new unidirectional limit ", max_streams,
" decreases the current limit: ",
v99_streamid_manager_.max_outgoing_unidirectional_streams()),
@@ -1321,16 +1329,7 @@
}
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);
+ if (!ValidateStreamFlowControlLimit(new_window, kv.second.get())) {
return;
}
if (!kv.second->ConfigSendWindowOffset(new_window)) {
@@ -1357,16 +1356,7 @@
}
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);
+ if (!ValidateStreamFlowControlLimit(new_window, kv.second.get())) {
return;
}
if (!kv.second->ConfigSendWindowOffset(new_window)) {
@@ -1393,16 +1383,7 @@
}
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);
+ if (!ValidateStreamFlowControlLimit(new_window, kv.second.get())) {
return;
}
if (!kv.second->ConfigSendWindowOffset(new_window)) {
@@ -1411,6 +1392,41 @@
}
}
+bool QuicSession::ValidateStreamFlowControlLimit(QuicStreamOffset new_window,
+ const QuicStream* stream) {
+ if (was_zero_rtt_rejected_ &&
+ new_window < stream->flow_controller()->bytes_sent()) {
+ QUIC_BUG_IF(perspective() == Perspective::IS_SERVER)
+ << "Server should never receive configs twice.";
+ connection_->CloseConnection(
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE,
+ quiche::QuicheStrCat(
+ "Server rejected 0-RTT, aborting because new stream max data ",
+ new_window, " for stream ", stream->id(),
+ " is less than currently used: ",
+ stream->flow_controller()->bytes_sent()),
+ ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+ return false;
+ }
+
+ if (version().AllowsLowFlowControlLimits() &&
+ new_window < stream->flow_controller()->send_window_offset()) {
+ QUIC_BUG_IF(perspective() == Perspective::IS_SERVER)
+ << "Server should never receive configs twice.";
+ connection_->CloseConnection(
+ was_zero_rtt_rejected_ ? QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED
+ : QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED,
+ quiche::QuicheStrCat(
+ was_zero_rtt_rejected_ ? "Server rejected 0-RTT, aborting because "
+ : "",
+ "new stream max data ", new_window, " decreases current limit: ",
+ stream->flow_controller()->send_window_offset()),
+ ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+ return false;
+ }
+ return true;
+}
+
void QuicSession::OnNewSessionFlowControlWindow(QuicStreamOffset new_window) {
QUIC_DVLOG(1) << ENDPOINT << "OnNewSessionFlowControlWindow " << new_window;
@@ -1422,7 +1438,7 @@
", which is below currently used: ", flow_controller_.bytes_sent());
QUIC_LOG(ERROR) << error_details;
connection_->CloseConnection(
- QUIC_INTERNAL_ERROR, error_details,
+ QUIC_ZERO_RTT_UNRETRANSMITTABLE, error_details,
ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
return;
}
@@ -1442,12 +1458,15 @@
// The client receives a lower limit than remembered, violating
// https://tools.ietf.org/html/draft-ietf-quic-transport-27#section-7.3.1
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());
+ was_zero_rtt_rejected_ ? "Server rejected 0-RTT, aborting because "
+ : "",
+ "new session max data ", new_window,
+ " decreases current limit: ", flow_controller_.send_window_offset());
QUIC_LOG(ERROR) << error_details;
connection_->CloseConnection(
- QUIC_FLOW_CONTROL_INVALID_WINDOW, error_details,
- ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
+ was_zero_rtt_rejected_ ? QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED
+ : QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED,
+ error_details, ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
return;
}
diff --git a/quic/core/quic_session.h b/quic/core/quic_session.h
index e1e5188..81ba099 100644
--- a/quic/core/quic_session.h
+++ b/quic/core/quic_session.h
@@ -691,6 +691,12 @@
QuicRstStreamErrorCode error,
QuicStreamOffset bytes_written);
+ // Closes the connection and returns false if |new_window| is lower than
+ // |stream|'s current flow control window.
+ // Returns true otherwise.
+ bool ValidateStreamFlowControlLimit(QuicStreamOffset new_window,
+ const QuicStream* stream);
+
// Sends a STOP_SENDING frame if the stream type allows.
void MaybeSendStopSendingFrame(QuicStreamId id, QuicRstStreamErrorCode error);
diff --git a/quic/core/quic_session_test.cc b/quic/core/quic_session_test.cc
index 70fda35..a400d97 100644
--- a/quic/core/quic_session_test.cc
+++ b/quic/core/quic_session_test.cc
@@ -2070,8 +2070,12 @@
const uint32_t kInvalidWindow = kMinimumFlowControlSendWindow - 1;
QuicConfigPeer::SetReceivedInitialSessionFlowControlWindow(session_.config(),
kInvalidWindow);
- EXPECT_CALL(*connection_,
- CloseConnection(QUIC_FLOW_CONTROL_INVALID_WINDOW, _, _));
+ EXPECT_CALL(
+ *connection_,
+ CloseConnection(connection_->version().AllowsLowFlowControlLimits()
+ ? QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED
+ : QUIC_FLOW_CONTROL_INVALID_WINDOW,
+ _, _));
connection_->SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
session_.OnConfigNegotiated();
}
@@ -2083,7 +2087,8 @@
}
QuicConfigPeer::SetReceivedMaxBidirectionalStreams(
session_.config(), kDefaultMaxStreamsPerConnection - 1);
- EXPECT_CALL(*connection_, CloseConnection(QUIC_MAX_STREAMS_ERROR, _, _));
+ EXPECT_CALL(*connection_,
+ CloseConnection(QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED, _, _));
connection_->SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
session_.OnConfigNegotiated();
}
@@ -2095,7 +2100,8 @@
}
QuicConfigPeer::SetReceivedMaxUnidirectionalStreams(
session_.config(), kDefaultMaxStreamsPerConnection - 1);
- EXPECT_CALL(*connection_, CloseConnection(QUIC_MAX_STREAMS_ERROR, _, _));
+ EXPECT_CALL(*connection_,
+ CloseConnection(QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED, _, _));
connection_->SetDefaultEncryptionLevel(ENCRYPTION_FORWARD_SECURE);
session_.OnConfigNegotiated();
}
diff --git a/quic/core/quic_stream.cc b/quic/core/quic_stream.cc
index 895cf4c..9903781 100644
--- a/quic/core/quic_stream.cc
+++ b/quic/core/quic_stream.cc
@@ -929,16 +929,11 @@
<< "ConfigSendWindowOffset called on stream without flow control";
return false;
}
- if (perspective_ == Perspective::IS_CLIENT &&
- session()->version().AllowsLowFlowControlLimits() &&
- new_offset < flow_controller_->send_window_offset()) {
- OnUnrecoverableError(
- QUIC_FLOW_CONTROL_INVALID_WINDOW,
- quiche::QuicheStrCat("New stream max data ", new_offset,
- " decreases current limit: ",
- flow_controller_->send_window_offset()));
- return false;
- }
+
+ QUIC_BUG_IF(session()->version().AllowsLowFlowControlLimits() &&
+ new_offset < flow_controller_->send_window_offset())
+ << ENDPOINT << "The new offset " << new_offset
+ << " decreases current offset " << flow_controller_->send_window_offset();
if (flow_controller_->UpdateSendWindowOffset(new_offset)) {
// Let session unblock this stream.
session_->MarkConnectionLevelWriteBlocked(id_);
@@ -1285,6 +1280,22 @@
session_->SendStopSending(code, id_);
}
+QuicFlowController* QuicStream::flow_controller() {
+ if (flow_controller_.has_value()) {
+ return &flow_controller_.value();
+ }
+ QUIC_BUG << "Trying to access non-existent flow controller.";
+ return nullptr;
+}
+
+const QuicFlowController* QuicStream::flow_controller() const {
+ if (flow_controller_.has_value()) {
+ return &flow_controller_.value();
+ }
+ QUIC_BUG << "Trying to access non-existent flow controller.";
+ return nullptr;
+}
+
// static
spdy::SpdyStreamPrecedence QuicStream::CalculateDefaultPriority(
const QuicSession* session) {
diff --git a/quic/core/quic_stream.h b/quic/core/quic_stream.h
index 80b2fb0..7a847f0 100644
--- a/quic/core/quic_stream.h
+++ b/quic/core/quic_stream.h
@@ -229,7 +229,9 @@
int num_frames_received() const;
int num_duplicate_frames_received() const;
- QuicFlowController* flow_controller() { return &*flow_controller_; }
+ QuicFlowController* flow_controller();
+
+ const QuicFlowController* flow_controller() const;
// Called when endpoint receives a frame which could increase the highest
// offset.
diff --git a/quic/core/tls_client_handshaker_test.cc b/quic/core/tls_client_handshaker_test.cc
index 1ed2042..1ec3d39 100644
--- a/quic/core/tls_client_handshaker_test.cc
+++ b/quic/core/tls_client_handshaker_test.cc
@@ -8,6 +8,7 @@
#include "net/third_party/quiche/src/quic/core/crypto/quic_decrypter.h"
#include "net/third_party/quiche/src/quic/core/crypto/quic_encrypter.h"
+#include "net/third_party/quiche/src/quic/core/quic_error_codes.h"
#include "net/third_party/quiche/src/quic/core/quic_packets.h"
#include "net/third_party/quiche/src/quic/core/quic_server_id.h"
#include "net/third_party/quiche/src/quic/core/quic_types.h"
@@ -515,7 +516,8 @@
config.SetMaxBidirectionalStreamsToSend(
config.GetMaxBidirectionalStreamsToSend() - 1);
- EXPECT_CALL(*connection_, CloseConnection(QUIC_MAX_STREAMS_ERROR, _, _))
+ EXPECT_CALL(*connection_,
+ CloseConnection(QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED, _, _))
.WillOnce(testing::Invoke(connection_,
&MockQuicConnection::ReallyCloseConnection));
// Close connection will be called again in the handshaker, but this will be