quic-hq-interop: Allow for retries if we've reached our max stream limit

Several servers defer the sending of max stream frames.  For instance
quic-go uses a go-routine to do the sending after sufficient existing
streams have finished, while mvfst seems to wait for all outstanding
streams to be closed before issuing a new batch.  This result in the
client, if all streams are in use, getting a transient NULL return from
SSL_new_stream().  Check for the stream limit being reached and allow a
number of retries before giving up to give the server a chance to issue
us more streams.  Also dead-reckon the batch count of streams we use in
parallel to be 1/4 of our total number of available streams (generally
hard coded to 100 for most servers) to avoid using all our streams at
once.  It would be really nice to have an api to expose our negotiated
transport parameters so that the application can know what this limit
is, but until then we have to just guess.

Reviewed-by: Matt Caswell <matt@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/26527)
This commit is contained in:
Neil Horman 2025-01-22 13:19:52 -05:00
parent 5569e170ee
commit 17dc32c51b
2 changed files with 36 additions and 5 deletions

View file

@ -529,6 +529,8 @@ static size_t build_request_set(SSL *ssl)
char req_string[REQ_STRING_SZ];
SSL *new_stream;
size_t written;
unsigned long error;
size_t retry_count;
/*
* Free any previous poll list
@ -606,12 +608,29 @@ static size_t build_request_set(SSL *ssl)
new_stream = NULL;
/*
* We don't strictly have to do this check, but our quic client limits
* our max data streams to 100, so we're just batching in groups of 100
* for now
* NOTE: We are doing groups of 25 because thats 1/4 of the initial max
* stream count that most servers advertise. This gives the server an
* opportunity to send us updated MAX_STREAM frames to extend our stream
* allotment before we run out, which many servers defer doing.
*/
if (poll_count <= 99)
new_stream = SSL_new_stream(ssl, 0);
if (poll_count <= 25) {
for (retry_count = 0; retry_count < 10; retry_count++) {
ERR_clear_error();
new_stream = SSL_new_stream(ssl, 0);
if (new_stream == NULL
&& (error = ERR_get_error()) != 0
&& ERR_GET_REASON(error) == SSL_R_STREAM_COUNT_LIMITED) {
/*
* Kick the SSL state machine in the hopes that
* the server has a MAX_STREAM frame for us to process
*/
fprintf(stderr, "Stream limit reached, retrying\n");
SSL_handle_events(ssl);
continue;
}
break;
}
}
if (new_stream == NULL) {
/*

View file

@ -69,6 +69,18 @@ SSL_new_stream() returns a new stream object, or NULL on error.
This function fails if called on a QUIC stream SSL object or on a non-QUIC SSL
object.
SSL_new_stream() may also fail if the connection that the stream is being
allocated in relation to has reached the maximum number of streams allowed by
the connection (as dictated by the B<max_streams_bidi> or B<max_streams_uni>
transport parameter values negotiated by the connection with the server. In
this event the NULL return will be accompanied by an error on the error stack
(obtainable via ERR_get_error()), which will contain a reason code of
B<SSL_R_STREAM_COUNT_LIMITED>. When this error is encountered, The operation
may be retried. It is recommended that, prior to retry, the error stack be
cleared via a call to ERR_clear_error(), and that the TLS state machine be
triggered via a call to SSL_handle_events() to process any potential updates
from the server allowing additional streams to be created.
=head1 SEE ALSO
L<SSL_accept_stream(3)>, L<SSL_free(3)>