OSSL-GUIDE-QU...ENT-NON-BLOCK(7SSL)

πŸ’– OSSL-GUIDE-QUIC-CLIENT-NON-BLOCK πŸ’–

okay bestie, welcome to ossl-guide-quic-client-non-block(7SSL) β€” the OpenSSL Guide page for writing a simple nonblocking QUIC client. this is part one of the glow-up, so grab a strawberry drink πŸ“ and let's get into it.


✨ NAME ✨

ossl-guide-quic-client-non-block β€” OpenSSL Guide: Writing a simple nonblocking QUIC client. periodt.


🌈 SIMPLE NONBLOCKING QUIC CLIENT EXAMPLE 🌈

so this page is us building on top of the example we already cooked up over on the ossl-guide-quic-client-block(7) page, which shows how to write a simple blocking QUIC client. here we're gonna take that demo code and amend it so it supports nonblocking functionality. it's giving sequel energy πŸ’…

the complete source code for this example nonblocking QUIC client lives in the demos/guide directory of the OpenSSL source distribution, in the file quic-client-non-block.c. it's also online at https://github.com/openssl/openssl/blob/master/demos/guide/quic-client-non-block.c.

so, like we saw in the previous example: an OpenSSL QUIC application always uses a nonblocking socket. but β€” plot twist β€” despite that, the SSL object still has blocking behaviour. when the SSL object has blocking behaviour, that means it waits (blocks) until data is available to read if you try to read from it when there's no data yet. same vibe when writing: it waits if the SSL object currently can't write at that moment. this can make your life easier ngl, because you don't have to worry about what to do in those cases β€” the code just... stops until it can continue. no thoughts, head empty, it just waits. 🫢

but in many cases you do not want this behaviour. instead of stopping and waiting, your application might need to go do other tasks while the SSL object can't read/write β€” like updating a GUI, or doing operations on some other connection or stream. main character multitasking fr.

we'll see later in this tutorial how to change the SSL object so it has nonblocking behaviour. with a nonblocking SSL object, functions like SSL_read_ex(3) or SSL_write_ex(3) will return immediately with a non-fatal error if they currently can't read or write respectively.

and since this page builds on the example developed on the ossl-guide-quic-client-block(7) page, we're assuming you're already familiar with it, and we're only explaining how this example differs. noted? noted. πŸ“

πŸ¦‹ Performing work while waiting for the socket πŸ¦‹

in a nonblocking application you're gonna need work to perform in the event that you want to read or write to the SSL object but currently can't. actually, that's literally the whole point of using a nonblocking SSL object: to give the application the chance to go do something else. whatever that something else is, the app must also be prepared to come back and retry the operation it previously attempted periodically, to see if it can now complete. ideally it would only do this when something has changed such that it might succeed on the retry attempt, but honestly? it doesn't have to be that way. it can retry at any time. no pressure.

important bestie: you must retry exactly the same operation that you tried last time. you cannot start something new. like, if you were attempting to write the text "Hello World" and it failed because the SSL object currently can't write, then you cannot go and attempt to write some other text when you retry. that's not the vibe. πŸ’€

in this demo application we're going to create a helper function that simulates doing other work. in fact, for the sake of simplicity, it does nothing except wait for the state of the underlying socket to change, or until a timeout expires, after which the state of the SSL object might have changed. we're calling our function wait_for_activity().

static void wait_for_activity(SSL *ssl)
{
    fd_set wfds, rfds;
    int width, sock, isinfinite;
    struct timeval tv;
    struct timeval *tvp = NULL;

    /* Get hold of the underlying file descriptor for the socket */
    sock = SSL_get_fd(ssl);

    FD_ZERO(&wfds);
    FD_ZERO(&rfds);

    /*
     * Find out if we would like to write to the socket, or read from it (or
     * both)
     */
    if (SSL_net_write_desired(ssl))
        FD_SET(sock, &wfds);
    if (SSL_net_read_desired(ssl))
        FD_SET(sock, &rfds);
    width = sock + 1;

    /*
     * Find out when OpenSSL would next like to be called, regardless of
     * whether the state of the underlying socket has changed or not.
     */
    if (SSL_get_event_timeout(ssl, &tv, &isinfinite) && !isinfinite)
        tvp = &tv;

    /*
     * Wait until the socket is writeable or readable. We use select here
     * for the sake of simplicity and portability, but you could equally use
     * poll/epoll or similar functions
     *
     * NOTE: For the purposes of this demonstration code this effectively
     * makes this demo block until it has something more useful to do. In a
     * real application you probably want to go and do other work here (e.g.
     * update a GUI, or service other connections).
     *
     * Let's say for example that you want to update the progress counter on
     * a GUI every 100ms. One way to do that would be to use the timeout in
     * the last parameter to "select" below. If the tvp value is greater
     * than 100ms then use 100ms instead. Then, when select returns, you
     * check if it did so because of activity on the file descriptors or
     * because of the timeout. If the 100ms GUI timeout has expired but the
     * tvp timeout has not then go and update the GUI and then restart the
     * "select" (with updated timeouts).
     */

    select(width, &rfds, &wfds, NULL, tvp);
}

okay so if you already know how to write nonblocking apps in OpenSSL for TLS (peep ossl-guide-tls-client-non-block(7)), you should note there's an important difference here between how a QUIC app and a TLS app work. with a TLS application, if we try to read or write something to the SSL object and get a "retry" response (SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE), we can assume that's because OpenSSL attempted to read or write to the underlying socket and the socket signalled the "retry". with QUIC? not the case. OpenSSL may signal retry as a result of an SSL_read_ex(3) or SSL_write_ex(3) call (or similar), which indicates the state of the stream. this is entirely independent of whether the underlying socket needs to retry or not. two totally separate dramas, bestie. 😭

to figure out whether OpenSSL currently wants to read or write to the underlying socket for a QUIC application, we must call SSL_net_read_desired(3) and SSL_net_write_desired(3).

it's also super important with QUIC that we periodically call an I/O function (or otherwise call the SSL_handle_events(3) function) to make sure the QUIC connection stays healthy. this is especially important with a nonblocking application because you're likely to leave the SSL object idle for a while while the app goes off to do other work. the SSL_get_event_timeout(3) function can be used to determine the deadline for the next time we need to call an I/O function (or call SSL_handle_events(3)).

an alternative to using SSL_get_event_timeout(3) to find the next deadline at which OpenSSL must be called again is to use "thread assisted" mode. in "thread assisted" mode, OpenSSL spawns an additional thread that periodically calls SSL_handle_events(3) automatically β€” meaning the application can leave the connection idle, safe in the knowledge that the connection is still being maintained in a healthy state. ✨ see "Creating the SSL_CTX and SSL objects" below for more details on that.

in this example we're using the "select" function to check the readability/writeability of the socket because it's extremely simple to use and it's available on most Operating Systems. but you could use any other similar function to do the same thing, no gatekeeping. "select" waits for the state of the underlying socket(s) to become readable/writeable, or until the timeout has expired, before returning.

πŸŽ€ Handling errors from OpenSSL I/O functions πŸŽ€

a QUIC application configured for nonblocking behaviour needs to be prepared to handle errors returned from OpenSSL I/O functions like SSL_read_ex(3) or SSL_write_ex(3). errors may be fatal for the stream (for example, because the stream has been reset, or because the underlying connection has failed), or non-fatal (for example, because we're trying to read from the stream but no data has arrived yet from the peer for that stream).

SSL_read_ex(3) and SSL_write_ex(3) will return 0 to indicate an error, and SSL_read(3) and SSL_write(3) will return 0 or a negative value to indicate an error. SSL_shutdown(3) will return a negative value to indicate an error.

in the event of an error, an application should call SSL_get_error(3) to find out what type of error has occurred. if the error is non-fatal and can be retried, then SSL_get_error(3) will return SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE, depending on whether OpenSSL wanted to read from or write to the stream but was unable to. note that a call to SSL_read_ex(3) or SSL_read(3) can still generate SSL_ERROR_WANT_WRITE. similarly, calls to SSL_write_ex(3) or SSL_write(3) might generate SSL_ERROR_WANT_READ. it's giving chaos but it's fine. πŸ™ˆ

another type of non-fatal error that may occur is SSL_ERROR_ZERO_RETURN. this indicates an EOF (End-Of-File), which can happen if you attempt to read data from an SSL object but the peer has indicated that it will not send any more data on the stream. in this case you may still want to write data to the stream, but you will not receive any more data.

fatal errors that may occur are SSL_ERROR_SYSCALL and SSL_ERROR_SSL. these indicate that the stream is no longer usable. for example, this could be because the stream has been reset by the peer, or because the underlying connection has failed. you can consult the OpenSSL error stack for further details (for example, by calling ERR_print_errors(3) to print out details of errors that have occurred). you can also consult the return value of SSL_get_stream_read_state(3) to determine whether the error is local to the stream, or whether the underlying connection has also failed. a return value of SSL_STREAM_STATE_RESET_REMOTE tells you that the stream has been reset by the peer, and SSL_STREAM_STATE_CONN_CLOSED tells you that the underlying connection has closed. ✨

in our demo application we will write a function to handle these errors from OpenSSL I/O functions:


more coming in part 2, don't go anywhere bestie 🫢

okay bestie, here's handle_io_failure() in full β€” the nesting is a lot, but she's doing her best πŸ’…:

static int handle_io_failure(SSL *ssl, int res)
{
    switch (SSL_get_error(ssl, res)) {
    case SSL_ERROR_WANT_READ:
    case SSL_ERROR_WANT_WRITE:
        /* Temporary failure. Wait until we can read/write and try again */
        wait_for_activity(ssl);
        return 1;

    case SSL_ERROR_ZERO_RETURN:
        /* EOF */
        return 0;

    case SSL_ERROR_SYSCALL:
        return -1;

    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(ssl)) {
        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. */
            break;

        default:
            printf("Unknown stream failure\n");
            break;
        }
        /*
         * If the failure is due to a verification error we can get more
         * information about it from SSL_get_verify_result().
         */
        if (SSL_get_verify_result(ssl) != X509_V_OK)
            printf("Verify error: %s\n",
                X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
        return -1;

    default:
        return -1;
    }
}

So what's she doing? You hand her two things: the SSL object that represents the connection, and the return code from the I/O function that just failed. If the failure is non-fatal, she waits until a retry of the I/O operation might actually succeed β€” using the wait_for_activity() function we built in the previous section, iconic. Returns: 1 for a non-fatal error (except EOF), 0 for EOF, or -1 if a fatal error happened. Clean, no notes. ✨


πŸ’– Creating the SSL_CTX and SSL objects πŸ’–

To connect to a server you gotta make SSL_CTX and SSL objects first, that's just the law. Most of the steps for that are the same as for a blocking client and they're written up on the ossl-guide-quic-client-block(7) page, and we are not repeating that info here. Go read it, bestie.

One key difference though: you must put the SSL object into nonblocking mode, because the default is blocking mode. That's a SSL_set_blocking_mode(3) moment:

/*
 * The underlying socket is always nonblocking with QUIC, but the default
 * behaviour of the SSL object is still to block. We set it for nonblocking
 * mode in this demo.
 */
if (!SSL_set_blocking_mode(ssl, 0)) {
    printf("Failed to turn off blocking mode\n");
    goto end;
}

Even though the demo application we're building here doesn't use it, it is possible to use "thread assisted mode" when developing QUIC applications. Here's the tea: normally, when writing an OpenSSL QUIC application, it is important that SSL_handle_events(3) (or alternatively any I/O function) is called on the connection SSL object periodically to maintain the connection in a healthy state. See "Performing work while waiting for the socket" for more discussion on this. This is particularly important to keep in mind when writing a nonblocking QUIC application, because it's common to leave the SSL connection object idle for some time when using nonblocking mode. By using "thread assisted mode" a separate thread is created by OpenSSL to do this automatically, which means the application developer does not need to handle this aspect. To do that we must use OSSL_QUIC_client_thread_method(3) when we construct the SSL_CTX, like so:

ctx = SSL_CTX_new(OSSL_QUIC_client_thread_method());
if (ctx == NULL) {
    printf("Failed to create the SSL_CTX\n");
    goto end;
}

🌸 Performing the handshake 🌸

Same as in the demo for a blocking QUIC client: SSL_connect(3) is how we do the handshake with the server. But since we're using a nonblocking SSL object, it's very likely those calls will fail with a non-fatal error while we're waiting for the server to respond to our handshake messages. And when that happens? You retry the exact same SSL_connect(3) call at a later time. In this demo we do it in a loop:

/* Do the handshake with the server */
while ((ret = SSL_connect(ssl)) != 1) {
    if (handle_io_failure(ssl, ret) == 1)
        continue; /* Retry */
    printf("Failed to connect to server\n");
    goto end; /* Cannot retry: error */
}

We keep calling SSL_connect(3) until it gives us a success response. Otherwise we use the handle_io_failure() function we made earlier to work out what we should do next. Oh and btw: we do not expect an EOF to occur at this stage, so that kind of response gets treated the same way as a fatal error. πŸ’€


πŸ¦‹ Sending and receiving data πŸ¦‹

As with the blocking QUIC client demo, SSL_write_ex(3) is how we send data to the server. And as with SSL_connect(3) up there, because we're on a nonblocking SSL object, this call could fail with a non-fatal error β€” in which case you should retry exactly the same SSL_write_ex(3) call again. And I mean exactly: the parameters must be exactly the same, i.e. the same pointer to the buffer to write with the same length. You must not attempt to send different data on a retry. An optional mode does exist (SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER) which will configure OpenSSL to allow the buffer being written to change from one retry to the next β€” but even in that case, you must still retry exactly the same data, even though the buffer that contains that data may change location. See SSL_CTX_set_mode(3) for further details. And as in the TLS tutorials (ossl-guide-tls-client-block(7)), we write the request in three chunks.

First, we write the entire request to the stream. We also have to make sure we signal to the server that we've finished writing. You can do that by passing the SSL_WRITE_FLAG_CONCLUDE flag to SSL_write_ex2(3) or by calling SSL_stream_conclude(3). The first way is more efficient, so that's the one we choose:

/* Write an HTTP GET request to the peer */
while (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
    if (handle_io_failure(ssl, 0) == 1)
        continue; /* Retry */
    printf("Failed to write start of HTTP request\n");
    goto end; /* Cannot retry: error */
}
while (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
    if (handle_io_failure(ssl, 0) == 1)
        continue; /* Retry */
    printf("Failed to write hostname in HTTP request\n");
    goto end; /* Cannot retry: error */
}
while (!SSL_write_ex2(ssl, request_end, strlen(request_end),
       SSL_WRITE_FLAG_CONCLUDE, &written)) {
    if (handle_io_failure(ssl, 0) == 1)
        continue; /* Retry */
    printf("Failed to write end of HTTP request\n");
    goto end; /* Cannot retry: error */
}
On a write we do not expect to see an EOF response, so we treat that case in the same way as a fatal error. 😀

Reading a response back from the server is similar:

do {
    /*
     * Get up to sizeof(buf) bytes of the response. We keep reading until
     * the server closes the connection.
     */
    while (!eof && !SSL_read_ex(ssl, buf, sizeof(buf), &readbytes)) {
        switch (handle_io_failure(ssl, 0)) {
        case 1:
            continue; /* Retry */
        case 0:
            eof = 1;
            continue;
        case -1:
        default:
            printf("Failed reading remaining data\n");
            goto end; /* Cannot retry: error */
        }
    }
    /*
     * 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.
     */
    if (!eof)
        fwrite(buf, 1, readbytes, stdout);
} while (!eof);
/* In case the response didn't finish with a newline we add one now */
printf("\n");

The main difference this time is that it's valid for us to receive an EOF response when trying to read data from the server. That's what happens when the server closes down the connection after sending all the data in its response.

In this demo we just print out all the data we've received back from the server. We keep going around the loop until we either hit a fatal error, or we get an EOF, which means a graceful finish. πŸŽ‰


🫢 Shutting down the connection 🫢

As in the QUIC blocking example, we must shut the connection down when we're finished with it.

And even though we received EOF on the stream we were reading from above, that tells us literally nothing about the state of the underlying connection. Our demo application initiates the connection shutdown process via SSL_shutdown(3).

Since our application is the one initiating the shutdown, we might expect SSL_shutdown(3) to give a return value of 0, and then we should keep calling it until we receive a return value of 1 (which means we've successfully completed the shutdown). And since we're using a nonblocking SSL object, we might expect to have to retry this operation several times. If SSL_shutdown(3) returns a negative result, then we must call SSL_get_error(3) to work out what to do next β€” and we use our handle_io_failure() function from earlier for that:

/*
 * Repeatedly call SSL_shutdown() until the connection is fully
 * closed.
 */
while ((ret = SSL_shutdown(ssl)) != 1) {
    if (ret < 0 && handle_io_failure(ssl, ret) == 1)
        continue; /* Retry */
}

πŸŽ€ Final clean up πŸŽ€

As with the blocking QUIC client example, once our connection is finished with we must free it. The steps to do this for this example are the same as for the blocking example, so we won't repeat it here.

πŸ’– FURTHER READING πŸ’–

okay bestie, you ate that whole non-blocking client and now you want more? valid. here's your next hyperfixation list:

  • Wanna see how the same thing looks when you just wanna sit there and wait for the socket like a patient little angel instead of bouncing around checking SSL_want() all the time? Go read ossl-guide-quic-client-block(7) β€” that's the tutorial on how to write a blocking QUIC client.
  • Wanna level up and have multiple streams going on at once like the main character you are? ossl-guide-quic-multi-stream(7) is right there, and it shows you how to write a multi-stream QUIC client.

no cap, read both. it's giving ✨well-rounded✨.


πŸ” SEE ALSO πŸ”

the whole friend group, fr fr πŸ‘―β€β™€οΈ:

go say hi. they don't bite. πŸ¦‹


the legal girlies have entered the chat and honestly? they're serving. πŸ’…

Copyright 2023-2025 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.

okay so real talk: that's not a vibe suggestion, that's the actual rule. read the License, comply with the License, and if you need a copy it's literally chilling in the LICENSE file in the source distro or at the URL above. periodt. 🌸

and that's the whole page bestie!! πŸŽ€ you made it to the end, which is honestly iconic of you. this page ships with OpenSSL 4.0.3 and carries the date 2026-09-2 β€” so if you're reading it in some other OpenSSL era, check the docs that match your version. now go write your non-blocking QUIC client and go be the main character of your own event loop. πŸ§šβ€β™€οΈβœ¨πŸ¦‹

πŸ“¦ where this page comes from