OSSL-GUIDE-QUIC-MULTI-STREAM(7SSL)

πŸ’« ossl-guide-quic-multi-stream (7SSL) πŸ’«

hey bestie 🫢 welcome to ossl-guide-quic-multi-stream β€” the OpenSSL Guide: Writing a simple multi-stream QUIC client. this is part one, so buckle up, we're doing concepts first πŸ’–


🌸 NAME 🌸

ossl-guide-quic-multi-stream β€” OpenSSL Guide: Writing a simple multi-stream QUIC client ✨

πŸ¦‹ INTRODUCTION πŸ¦‹

okay so this page is here to introduce some important concepts you need before writing a simple QUIC multi-stream application πŸ’… it assumes you already have a basic understanding of QUIC and how it gets used in OpenSSL. like, don't come in cold, bestie 😭

go read ossl-guide-quic-introduction(7) and ossl-guide-quic-client-block(7) first, fr fr πŸ“š


πŸ’œ QUIC STREAMS πŸ’œ

okay so in a QUIC multi-stream application we separate out the concepts of a QUIC "connection" and a QUIC "stream". two different things, two different vibes, do not conflate them bestie πŸ™ˆ

A connection object represents the overarching details of the connection between a client and a server, including all its negotiated and configured parameters. In an OpenSSL application we use the SSL object for that β€” it's known as the connection SSL object β€” and it gets created when an application calls SSL_new(3) πŸ’«

Separately, a connection can have zero or more streams associated with it (although a connection with zero streams is probably not very useful, so normally you'd have at least one, no cap). A stream is used to send and receive data between the two peers. Each stream is also represented by an SSL object πŸ‘‘

A stream is logically independent of all the other streams associated with the same connection. Data sent on a stream is guaranteed to be delivered in the order that it was sent within that stream. That guarantee does NOT hold across streams though β€” like, if an application sends data on stream 1 first and then sends some more data on stream 2 second, the remote peer may receive the data sent on stream 2 before it receives the data sent on stream 1 😳 so yeah, don't assume ordering across streams, that's not it.

Once the connection SSL object has completed its handshake (i.e. SSL_connect(3) has returned 1 βœ…), stream SSL objects get created by the application calling SSL_new_stream(3) or SSL_accept_stream(3) β€” see "CREATING NEW STREAMS" below πŸ‘‡

The same threading rules apply to SSL objects as for most OpenSSL objects (see ossl-guide-libraries-introduction(7)). In particular, most OpenSSL functions are thread safe, but the SSL object is not 🚨

What that means in practice:

  • you can use an SSL object representing one stream at the same time as another thread is using a different SSL object for a different stream on the same connection πŸŽ‰
  • you cannot use the same SSL object on two different threads at the same time (not without additional application level locking anyway) πŸ’€
thread safety reminder bestie: different streams, different threads = fine. same stream, two threads = absolutely not, no cap πŸ™…β€β™€οΈ

πŸŽ€ THE DEFAULT STREAM πŸŽ€

A connection SSL object may also (optionally) be associated with a stream. That stream is known as the default stream ✨

The default stream is automatically created and associated with the SSL object when the application calls SSL_read_ex(3), SSL_read(3), SSL_write_ex(3) or SSL_write(3) and passes the connection SSL object as a parameter. like, you didn't ask for it, it just showed up 🫠

Here's how it picks which stream to be:

  • if a client application calls SSL_write_ex(3) or SSL_write(3) first, then (by default) the default stream will be a client-initiated bi-directional stream πŸ”
  • if a client application calls SSL_read_ex(3) or SSL_read(3) first, then the first stream initiated by the server will be used as the default stream β€” whether it's bi-directional or uni-directional, doesn't matter πŸ’…

This behaviour can be controlled via the default stream mode. See SSL_set_default_stream_mode(3) for further details πŸ‘€

It is recommended that new multi-stream applications should not use a default stream at all, and instead should use a separate stream SSL object for each stream that is used. To do that you call SSL_set_default_stream_mode(3) and set the mode to SSL_DEFAULT_STREAM_MODE_NONE. periodt πŸ’…

🌈 CREATING NEW STREAMS 🌈

An endpoint can create a new stream by calling SSL_new_stream(3) ✨ This creates a locally initiated stream.

To do so you must pass the QUIC connection SSL object as a parameter. You can also specify whether you want a bi-directional or a uni-directional stream πŸ¦‹

The function returns a new QUIC stream SSL object for sending and receiving data on that stream πŸ“¨

The peer may also initiate streams, and honestly? iconic of them. An application can use the function SSL_get_accept_stream_queue_len(3) to determine the number of streams that the peer has initiated that are waiting for the application to handle πŸ‘€

An application can call SSL_accept_stream(3) to create a new SSL object for a remotely initiated stream. If the peer has not initiated any, then this call will block until one is available if the connection object is in blocking mode (see SSL_set_blocking_mode(3)) 😴

⚠️ heads up bestie: when using a default stream, OpenSSL will prevent new streams from being accepted. To override this behaviour you must call SSL_set_incoming_stream_policy(3) to set the policy to SSL_INCOMING_STREAM_POLICY_ACCEPT. See the man page for further details. This is not relevant if the default stream has been disabled as described in "THE DEFAULT STREAM" above πŸ’–

Any stream may be bi-directional or uni-directional πŸ”βž‘οΈ

  • if it is uni-directional, then the initiator can write to it but not read from it
  • and vice-versa for the peer β€” the peer can read from it but not write to it πŸ‘―

You can determine what type of stream an SSL object represents by calling SSL_get_stream_type(3). See the man page for further details πŸ”

πŸ’… USING A STREAM TO SEND AND RECEIVE DATA πŸ’…

Once you have a stream SSL object (which includes the connection SSL object if a default stream is in use), then you can send and receive data over it using the SSL_write_ex(3), SSL_write(3), SSL_read_ex(3) or SSL_read(3) functions πŸ“€πŸ“₯ See the man pages for further details πŸ‘‘

In the event of one of these functions not returning a success code, then you should call SSL_get_error(3) to find out further details about the error πŸ”Ž

In blocking mode, this will either be:

  • a fatal error (e.g. SSL_ERROR_SYSCALL or SSL_ERROR_SSL), or
  • SSL_ERROR_ZERO_RETURN, which can occur when attempting to read data from a stream and the peer has indicated that the stream is concluded (i.e. "FIN" has been signalled on the stream). This means that the peer will send no more data on that stream πŸ™…
🚨 careful bestie, this trips people up: the interpretation of SSL_ERROR_ZERO_RETURN is slightly different for a QUIC application compared to a TLS application. In TLS it occurs when the connection has been shutdown by the peer. In QUIC this only tells you that the current stream has been concluded by the peer. It tells you nothing about the underlying connection πŸ’€

If the peer has concluded the stream then no more data will be received on it β€” however, an application can still send data to the peer until the send side of the stream has also been concluded. That can happen by the application calling SSL_stream_conclude(3) ✨

⚠️ it is an error to attempt to send more data on a stream after SSL_stream_conclude(3) has been called. don't do it bestie πŸ™ˆ

It is also possible to abandon a stream abnormally by calling SSL_stream_reset(3) 😱

Once a stream object is no longer needed it should be freed via a call to SSL_free(3) 🧹 An application should not call SSL_shutdown(3) on it, since this is only meaningful for connection level SSL objects. Freeing the stream will automatically signal STOP_SENDING to the peer πŸ›‘

πŸ¦„ STREAMS AND CONNECTIONS πŸ¦„

Given a stream object, it is possible to get the SSL object corresponding to the connection via a call to SSL_get0_connection(3) πŸ”—

Multi-threaded restrictions apply, so care should be taken when using the returned connection object 🚨 Specifically, if you are handling each of your stream objects in a different thread and call SSL_get0_connection(3) from within that thread, then you must be careful to not call any function that uses the connection object at the same time as one of the other threads is also using that connection object 😭

The only exceptions β€” the two functions that are thread-safe β€” are:

A stream object does not inherit all its settings and values from its parent SSL connection object 😳 Therefore certain function calls that are relevant to the connection as a whole will not work on a stream πŸ‘Ž

For example, the function SSL_get_certificate(3) can be used to obtain a handle on the peer certificate when called with a connection SSL object. But when called with a stream SSL object it will return NULL πŸ’€

✨ SIMPLE MULTI-STREAM QUIC CLIENT EXAMPLE ✨

okay bestie, this section is where we get our hands dirty with actual source code samples πŸ’… we're gonna build a simple multi-stream QUIC client app that connects to a server, sends it some HTTP/1.0 requests, and reads the responses back. but bestie, listen β€” HTTP/1.0 over QUIC is non-standard and real world servers will NOT support it, so this is for demonstration purposes only, no cap 🫢

We're building on top of the example code for the simple blocking QUIC client that's covered over on the ossl-guide-quic-client-block(7) page, and we're assuming you already know it. So we're only gonna describe the diffs between the simple blocking QUIC client and the multi-stream QUIC client, periodt. And even though the example code uses blocking SSL objects, you can lowkey use nonblocking SSL objects just the same ✨ peek at ossl-guide-quic-client-non-block(7) for more info on writing a nonblocking QUIC client.

The complete source code for this example multi-stream QUIC client lives in the "demos/guide" directory of the OpenSSL source distribution in the file "quic-multi-stream.c". It's also available online at <https://github.com/openssl/openssl/blob/master/demos/guide/quic-multi-stream.c>. πŸ’–


🌈 Disabling the default stream

Like we talked about up in "THE DEFAULT STREAM", we're gonna follow the recommendation and disable the default stream for our multi-stream client. To do that we call the SSL_set_default_stream_mode(3) function, passing in our connection SSL object and the value SSL_DEFAULT_STREAM_MODE_NONE πŸ’…

/*
 * We will use multiple streams so we will disable the default stream mode.
 * This is not a requirement for using multiple streams but is recommended.
 */
if (!SSL_set_default_stream_mode(ssl, SSL_DEFAULT_STREAM_MODE_NONE)) {
    printf("Failed to disable the default stream mode\n");
    goto end;
}

πŸ¦‹ Creating the request streams

For this example we're gonna create two different streams to send two different HTTP requests to the server. Just for the demo, the first one is a bi-directional stream and the second one is a uni-directional one πŸ‘‘

/*
 * We create two new client initiated streams. The first will be
 * bi-directional, and the second will be uni-directional.
 */
stream1 = SSL_new_stream(ssl, 0);
stream2 = SSL_new_stream(ssl, SSL_STREAM_FLAG_UNI);
if (stream1 == NULL || stream2 == NULL) {
    printf("Failed to create streams\n");
    goto end;
}

🌸 Writing data to the streams

Once the streams are created successfully we can start writing data to them πŸ’œ in this example we'll be sending a different HTTP request on each stream. To avoid repeating too much code we write a simple helper function to send an HTTP request to a stream:

int write_a_request(SSL *stream, const char *request_start,
                    const char *hostname)
{
    const char *request_end = "\r\n\r\n";
    size_t written;

    if (!SSL_write_ex(stream, request_start, strlen(request_start), &written))
        return 0;
    if (!SSL_write_ex(stream, hostname, strlen(hostname), &written))
        return 0;
    if (!SSL_write_ex(stream, request_end, strlen(request_end), &written))
        return 0;

    return 1;
}

We're assuming the strings request1_start and request2_start hold the appropriate HTTP requests. Then we can call our helper function above to send the requests on the two streams πŸ’« for simplicity's sake this example does it sequentially β€” writing to stream1 first, and when that succeeds, writing to stream2 second. Remember our client is blocking, so these calls will only return once they've been successfully completed. A real application would NOT need to do these writes sequentially or in any particular order bestie β€” for example we could start two threads (one for each stream) and write the requests to each stream simultaneously πŸ”₯

/* Write an HTTP GET request on each of our streams to the peer */
if (!write_a_request(stream1, request1_start, hostname)) {
    printf("Failed to write HTTP request on stream 1\n");
    goto end;
}

if (!write_a_request(stream2, request2_start, hostname)) {
    printf("Failed to write HTTP request on stream 2\n");
    goto end;
}

πŸŽ€ Reading data from a stream

In this example stream1 is a bi-directional stream, so once we've sent the request on it we can attempt to read the response back from the server πŸ‘€ here we just repeatedly call SSL_read_ex(3) until that function fails (which means either there's been a problem, or the peer has signalled the stream as concluded).

printf("Stream 1 data:\n");
/*
 * Get up to sizeof(buf) bytes of the response from stream 1 (which is a
 * bidirectional stream). We keep reading until the server closes the
 * connection.
 */
while (SSL_read_ex(stream1, buf, sizeof(buf), &readbytes)) {
    /*
    * OpenSSL does not guarantee that the returned data is a string or
    * that it is NUL terminated so we use fwrite() to write the exact
    * number of bytes that we read. The data could be non-printable or
    * have NUL characters in the middle of it. For this simple example
    * we're going to print it to stdout anyway.
    */
    fwrite(buf, 1, readbytes, stdout);
}
/* In case the response didn't finish with a newline we add one now */
printf("\n");

In a blocking application like this one, calls to SSL_read_ex(3) will either succeed immediately returning data that's already available, or they'll block waiting for more data to become available and return it when it does, or they'll fail with a 0 response code 🫠

Once we exit the while loop above we know that the last call to SSL_read_ex(3) gave a 0 response code, so we call the SSL_get_error(3) function to find out more details. Since this is a blocking application, this will either return SSL_ERROR_SYSCALL or SSL_ERROR_SSL β€” indicating a fundamental problem β€” or it'll return SSL_ERROR_ZERO_RETURN, which indicates that the stream is concluded and there'll be no more data available to read from it. Care must be taken to distinguish between an error at the stream level (i.e. a stream reset) and an error at the connection level (i.e. a connection closed). The SSL_get_stream_read_state(3) function can be used to distinguish between these different cases πŸ’…

/*
 * Check whether we finished the while loop above normally or as the
 * result of an error. The 0 argument to SSL_get_error() is the return
 * code we received from the SSL_read_ex() call. It must be 0 in order
 * to get here. Normal completion is indicated by SSL_ERROR_ZERO_RETURN. In
 * QUIC terms this means that the peer has sent FIN on the stream to
 * indicate that no further data will be sent.
 */
switch (SSL_get_error(stream1, 0)) {
case SSL_ERROR_ZERO_RETURN:
    /* Normal completion of the stream */
    break;

case SSL_ERROR_SSL:
    /*
     * Some stream fatal error occurred. This could be because of a stream
     * reset - or some failure occurred on the underlying connection.
     */
    switch (SSL_get_stream_read_state(stream1)) {
    case SSL_STREAM_STATE_RESET_REMOTE:
        printf("Stream reset occurred\n");
        /* The stream has been reset but the connection is still healthy. */
        break;

    case SSL_STREAM_STATE_CONN_CLOSED:
        printf("Connection closed\n");
        /* Connection is already closed. Skip SSL_shutdown() */
        goto end;

    default:
        printf("Unknown stream failure\n");
        break;
    }
    break;

default:
    /* Some other unexpected error occurred */
    printf ("Failed reading remaining data\n");
    break;
}

🧚 Accepting an incoming stream

Our stream2 object that we created above was a uni-directional stream, so it can't be used to receive data from the server. In this hypothetical example we're assuming the server initiates a new stream to send us back the data we requested πŸ’« to do that we call SSL_accept_stream(3). Since this is a blocking application it'll wait indefinitely until the new stream has arrived and is available for us to accept. In the event of an error it'll return NULL πŸ’€

/*
 * In our hypothetical HTTP/1.0 over QUIC protocol that we are using we
 * assume that the server will respond with a server initiated stream
 * containing the data requested in our uni-directional stream. This doesn't
 * really make sense to do in a real protocol, but its just for
 * demonstration purposes.
 *
 * We're using blocking mode so this will block until a stream becomes
 * available. We could override this behaviour if we wanted to by setting
 * the SSL_ACCEPT_STREAM_NO_BLOCK flag in the second argument below.
 */
stream3 = SSL_accept_stream(ssl, 0);
if (stream3 == NULL) {
    printf("Failed to accept a new stream\n");
    goto end;
}

Now we can read data from the stream the same way we did for stream1 above ✨ we won't repeat that here, bestie, you got this.

πŸ“ Cleaning up the streams

Once we're done using our streams we can simply free them by calling SSL_free(3) 🫢 optionally we could call SSL_stream_conclude(3) on them if we want to tell the peer that we won't be sending them any more data, but we don't do that in this example because we're assuming the HTTP application protocol supplies sufficient info for the peer to know when we've finished sending request data πŸ’…

We should NOT call SSL_shutdown(3) or SSL_shutdown_ex(3) on the stream objects since those calls should not be used for streams πŸ™ˆ

SSL_free(stream1);
SSL_free(stream2);
SSL_free(stream3);

πŸ‘‘ SEE ALSO

ossl-guide-introduction(7), ossl-guide-libraries-introduction(7), ossl-guide-libssl-introduction(7), ossl-guide-quic-introduction(7), ossl-guide-quic-client-block(7)

Copyright 2023 The OpenSSL Project Authors. All Rights Reserved.

Licensed under the Apache License 2.0 (the "License"). You may not use this file except in compliance with the License. You can obtain a copy in the file LICENSE in the source distribution or at <https://www.openssl.org/source/license.html>.


and that's a wrap on part 2, bestie πŸŽ€ this page is from OpenSSL version 4.0.3, dated 2026-09-29, and it's the OSSL-GUIDE-QUIC-MULTI-STREAM(7SSL) girlies. now go forth and stream responsibly πŸ’…βœ¨πŸ¦‹ no cap.

πŸ“¦ where this page comes from