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

πŸ’– ossl-guide-quic-server-non-block β€” OpenSSL Guide πŸ’–

okay bestie, welcome to the OSSL-GUIDE-QU...VER-NON-BLOCK(7SSL) page, the OpenSSL one, and we're about to build a whole little server together. grab a snack. πŸ“


✨ NAME ✨

ossl-guide-quic-server-non-block β€” the OpenSSL Guide for writing a simple nonblocking QUIC server. like, a whole guide. for you. no cap. πŸ’…


🌈 SIMPLE NONBLOCKING QUIC SERVER EXAMPLE 🌈

okay so this page is literally just source code samples, and what they show you is how to write a simple, non-concurrent, QUIC "echo" server app. it accepts one client connection at a time and echoes the input from the client right back to that same client. the way that works β€” once the current client disconnects, the next client connection gets accepted. slay. πŸ¦‹

here's the tea though: this server only accepts the "http/1.0" and "hq-interop" ALPN's, and it doesn't actually implement HTTP at all. it just does a simple echo. this is non-standard and will not be supported by real world servers. this is for demonstration purposes only, fr fr. πŸ’€

there are various methods to test this server, and you have options bestie:

  • quic-client-block.c and quic-client-non-block.c will send a basic HTTP/1.0 request, which the server will echo back.
  • you can also test this server by running "openssl s_client -connect localhost:4443 -4 -quic -alpn http/1.0" and entering text that will be echoed back by the server.

both the listening socket and connected socket are "nonblocking". however, we use select() to make the listening socket block when it cannot read/write. and here's the vibe check: rather than stopping and waiting, your application may need to go and do other tasks whilst the SSL object is unable to read/write. for example: updating a GUI or performing operations on some other connection or stream. multitasking icon behavior. πŸ‘‘

the complete source code for this example nonblocking QUIC server is available in the demos/guide directory of the OpenSSL source distribution in the file quic-server-non-block.c. it is also available online at <https://github.com/openssl/openssl/blob/master/demos/guide/quic-server-non-block.c>. obsessed. ✨

we kinda assume some stuff about you before we start (don't worry, it's giving prerequisites, not gatekeeping):

  • you already have OpenSSL installed on your system;
  • you already have some fundamental understanding of OpenSSL concepts and QUIC (see ossl-guide-libraries-introduction(7) and ossl-guide-quic-introduction(7));
  • you know how to write and build C code and link it against the libcrypto and libssl libraries that are provided by OpenSSL;
  • and it also assumes that you have a basic understanding of UDP/IP and sockets.

πŸŽ€ Creating the SSL_CTX and SSL objects πŸŽ€

first step bestie: create an SSL_CTX object for our server. we use the SSL_CTX_new(3) function for this purpose. we pass as an argument the return value of the function OSSL_QUIC_server_method(3). you should use this method whenever you are writing a QUIC server. periodt.

/*
 * An SSL_CTX holds shared configuration information for multiple
 * subsequent per-client SSL connections. We specifically load a QUIC
 * server method here.
 */
ctx = SSL_CTX_new(OSSL_QUIC_server_method());
if (ctx == NULL)
    goto err;

servers need a private key and certificate, obviously. intermediate issuer CA certificates are often required, and both the server (end-entity or EE) certificate and the issuer ("chain") certificates are most easily configured in a single "chain file". below we load such a chain file (the EE certificate must appear first, don't forget that part πŸ’–), and then load the corresponding private key, checking that it matches the server certificate.

no checks are performed to check the integrity of the chain (CA signatures or certificate expiration dates, for example), but we do verify the consistency of the private key with the corresponding certificate. so like, consistent? yes. validated top to bottom? no. πŸ™ˆ

/*
 * Load the server's certificate *chain* file (PEM format), which includes
 * not only the leaf (end-entity) server certificate, but also any
 * intermediate issuer-CA certificates.  The leaf certificate must be the
 * first certificate in the file.
 *
 * In advanced use-cases this can be called multiple times, once per public
 * key algorithm for which the server has a corresponding certificate.
 * However, the corresponding private key (see below) must be loaded first,
 * *before* moving on to the next chain file.
 */
if (SSL_CTX_use_certificate_chain_file(ctx, cert_path) <= 0) {
    fprintf(stderr, "couldn't load certificate file: %s\n", cert_path);
    goto err;
}

/*
 * Load the corresponding private key, this also checks that the private
 * key matches the just loaded end-entity certificate.  It does not check
 * whether the certificate chain is valid, the certificates could be
 * expired, or may otherwise fail to form a chain that a client can
 * validate.
 */
if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) <= 0) {
    fprintf(stderr, "couldn't load key file: %s\n", key_path);
    goto err;
}

most servers, including this one, do not solicit client certificates. we therefore do not need a "trust store" and allow the handshake to complete even when the client does not present a certificate. 🌸

note, bestie: even if a client did present a trusted certificate, for it to be useful, the server application would still need custom code to use the verified identity to grant nondefault access to that particular client. some servers grant access to all clients with certificates from a private CA, and this then requires processing of certificate revocation lists to deauthorise a client. it is often simpler and more secure to instead keep a list of authorised public keys. iykyk. πŸ”₯

though this is the default setting, we explicitly call the SSL_CTX_set_verify(3) function and pass the SSL_VERIFY_NONE value to it. the final argument to this function is a callback that you can optionally supply to override the default handling for certificate verification. most applications do not need to do this, so this can safely be set to NULL to get the default handling. easy. πŸ’…

/*
 * Clients rarely employ certificate-based authentication, and so we don't
 * require "mutual" TLS authentication (indeed there's no way to know
 * whether or how the client authenticated the server, so the term "mutual"
 * is potentially misleading).
 *
 * Since we're not soliciting or processing client certificates, we don't
 * need to configure a trusted-certificate store, so no call to
 * SSL_CTX_set_default_verify_paths() is needed.  The server's own
 * certificate chain is assumed valid.
 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);

QUIC also dictates using Application-Layer Protocol Negotiation (ALPN) to select an application protocol. we use SSL_CTX_set_alpn_select_cb(3) for this purpose. we can pass a callback which will be called for each connection to select an ALPN the server considers acceptable. πŸ‘―

/* Setup ALPN negotiation callback to decide which ALPN is accepted. */
SSL_CTX_set_alpn_select_cb(ctx, select_alpn, NULL);

in this case, we only accept "http/1.0" and "hq-interop". and nothing else. nope. 🚫

/*
 * ALPN strings for TLS handshake. Only 'http/1.0' and 'hq-interop'
 * are accepted.
 */
static const unsigned char alpn_ossltest[] = {
    8,  'h', 't', 't', 'p', '/', '1', '.', '0',
    10, 'h', 'q', '-', 'i', 'n', 't', 'e', 'r', 'o', 'p',
};

static int select_alpn(SSL *ssl, const unsigned char **out,
                       unsigned char *out_len, const unsigned char *in,
                       unsigned int in_len, void *arg)
{
    if (SSL_select_next_proto((unsigned char **)out, out_len, alpn_ossltest,
                              sizeof(alpn_ossltest), in,
                              in_len) == OPENSSL_NPN_NEGOTIATED)
        return SSL_TLSEXT_ERR_OK;
    return SSL_TLSEXT_ERR_ALERT_FATAL;
}

and that is all the setup that we need to do for the SSL_CTX. ate and left no crumbs. next, we create a UDP socket and bind to it on localhost. 🫢

/* Retrieve the file descriptor for a new UDP socket */
if ((fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
    fprintf(stderr, "cannot create socket");
    return -1;
}

sa.sin_family = AF_INET;
sa.sin_port = htons(port);

/* Bind to the new UDP socket on localhost */
if (bind(fd, (const struct sockaddr *)&sa, sizeof(sa)) < 0) {
    fprintf(stderr, "cannot bind to %u\n", port);
    BIO_closesocket(fd);
    return -1;
}

/* Set port to nonblocking mode */
if (BIO_socket_nbio(fd, 1) <= 0) {
    fprintf(stderr, "Unable to set port to nonblocking mode");
    BIO_closesocket(fd);
    return -1;
}

to run the QUIC server, we create an SSL_LISTENER to listen for incoming connections. we provide it with the bound UDP port to then explicitly begin listening for new connections. πŸ’«

/* Create a new QUIC listener */
if ((listener = SSL_new_listener(ctx, 0)) == NULL)
    goto err;

/* Provide the listener with our UDP socket. */
if (!SSL_set_fd(listener, fd))
    goto err;

/* Set the listener mode to nonblocking, which is inherited by
 * child objects.
 */
if (!SSL_set_blocking_mode(listener, 0))
    goto err;

/*
 * Begin listening. Note that is not usually needed as SSL_accept_connection
 * will implicitly start listening. It is only needed if a server wishes to
 * ensure it has started to accept incoming connections but does not wish to
 * actually call SSL_accept_connection yet.
 */
if (!SSL_listen(listener))
    goto err;

πŸ¦‹ Server loop πŸ¦‹

the server now enters a "forever" loop, handling one client connection at a time. before each connection, we clear the OpenSSL error stack so that any error reports are related to just the new connection. clean slate energy. ✨

/* Pristine error stack for each new connection */
ERR_clear_error();

we then wait until a connection is ready for reading. it uses the select function to wait until the socket is either readable or writable, depending on what the SSL connection requires.

we then accept a new connection, in which the handshake will have already occurred. however, since we are in nonblocking mode, SSL_accept_connection(3) will return immediately. therefore, we use a helper function to essentially block until a connection is established. the way that it just… waits. patiently. main character energy honestly. ⭐

printf("Waiting for connection\n");
while ((conn = SSL_accept_connection(listener, 0)) == NULL) {
    wait_for_activity(listener);
}
printf("Accepted new connection\n");

the helper function wait_for_activity uses select() to block until the file descriptor belonging to the passed SSL object is readable. as mentioned earlier, a more real-world application would likely use this time to perform other tasks. like, go be productive bestie. πŸ’œ

/* Initialize the fd_set structure */
FD_ZERO(&read_fd);
FD_ZERO(&write_fd);

πŸ¦‹ this is part 1, and there's more coming, so don't go anywhere β€” we're not done yet. πŸ’–

Ok bestie, we're back in the thick of it β€” this is the part where the sockets actually do something. Strap in. 🫢


/*
 * Determine if we would like to write to the socket, read from it, or both.
 */
if (SSL_net_write_desired(ssl))
    FD_SET(sock, &write_fd);
if (SSL_net_read_desired(ssl))
    FD_SET(sock, &read_fd);

/*
 * 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(sock + 1, &read_fd, &write_fd, NULL, tvp);

So what's happening up there? SSL_net_write_desired(ssl) and SSL_net_read_desired(ssl) are literally just vibe-checking whether OpenSSL wants to write, read, or both β€” and whichever it wants, we drop that socket into write_fd / read_fd accordingly. Then SSL_get_event_timeout(ssl, &tv, &isinfinite) tells us the next moment OpenSSL would like a lil call, no matter if the underlying socket state changed or not, and if that timeout is real (i.e. !isinfinite) we point tvp at it. Then select sits there waiting for the socket to be readable or writeable β€” they used select for simplicity and portability, but poll/epoll or whatever similar function you're obsessed with works too, iykyk. πŸ‘‘

And yes bestie, the NOTE is important: for the purposes of this demo code, this effectively makes the demo block until it has something more useful to do. In a real app you'd go do other work here (update a GUI, service other connections…). Their example: say you wanna update a progress counter on a GUI every 100ms β€” use the timeout in the last parameter to select. If tvp is bigger than 100ms, use 100ms instead. Then when select returns, check if it came back because of activity on the file descriptors or because of the timeout. If the 100ms GUI timeout expired but the tvp one hasn't, go update the GUI and restart the select (with updated timeouts). Chef's kiss. πŸ’…


Handshake? Done. ✨ So now the server reads all the client input.

/* Read from client until the client sends a end of stream packet */
while (!eof) {
    ret = SSL_read_ex(conn, buf + total_read, sizeof(buf) - total_read,
                      &nread);
    total_read += nread;
    if (total_read >= 8192) {
        fprintf(stderr, "Could not fit all data into buffer\n");
        goto err;
    }
    switch (handle_io_failure(conn, ret)) {
    case 1:
        continue; /* Retry */
    case 0:
        /* Reached end of stream */
        if (!SSL_has_pending(conn))
            eof = 1;
        break;
    default:
        fprintf(stderr, "Failed reading remaining data\n");
        goto err;
    }
}

Translation: it loops until eof, calling SSL_read_ex into the buffer and bumping total_read. If total_read ever hits 8192, it's like "nope, could not fit all data into buffer" and bails to err. Then it switch-cases on whatever handle_io_failure(conn, ret) says: 1 means retry (just continue), 0 means we reached end of stream and if there's nothing pending via SSL_has_pending(conn) we set eof = 1, and anything else is giving "Failed reading remaining data" β†’ goto err. πŸ“


Finally, we echo the received data back to the client. And the serve here is that we can use SSL_write_ex2(3) to pass in a special flag SSL_WRITE_FLAG_CONCLUDE that sends a FIN packet once the write has successfully finished writing all the data to the peer. Periodt. πŸ”₯

/* Echo client input */
while (!SSL_write_ex2(conn, buf,
                      total_read,
                      SSL_WRITE_FLAG_CONCLUDE, &total_written)) {
    if (handle_io_failure(conn, 0) == 1)
        continue;
    fprintf(stderr, "Failed to write data\n");
    goto err;
}

Then we shut the connection down with SSL_shutdown(3) β€” and lowkey it may need to be called multiple times to ensure the connection is shutdown completely. Not me looping it, bestie.

/*
 * Shut down the connection. We may need to call this multiple times
 * to ensure the connection is shutdown completely.
 */
while ((ret = SSL_shutdown(conn)) != 1) {
    if (ret < 0 && handle_io_failure(conn, ret) == 1)
        continue; /* Retry */
}

And finally, we free the SSL connection, and the server is now ready to accept the next client connection. Slay, next. πŸ’–

SSL_free(conn);

🧼 Final clean up

If the server somehow manages to break out of the infinite loop and be ready to exit, it would deallocate the constructed SSL. iconic behavior, honestly

SSL_free(listener);

And in the main function, it would deallocate the constructed SSL_CTX.

SSL_CTX_free(ctx);
BIO_closesocket(fd);

πŸ‘€ SEE ALSO

Go read these next, bestie, they're all connected:

Copyright 2024-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>.


That's a wrap on this one, babe! πŸ’œ This page is from OpenSSL 4.0.3, dated 2026-09-2… (the footer got a lil cropped, you know how it is). Now go forth, build your nonblocking QUIC server, and thrive. πŸ¦‹βœ¨

πŸ“¦ where this page comes from