OSSL-GUIDE-QUIC-CLIENT-BLOCK(7ossl)
π NAME π
ossl-guide-quic-client-block β the OpenSSL Guide, aka: how to write a simple blocking QUIC client, bestie edition π¦
β¨ SIMPLE BLOCKING QUIC CLIENT EXAMPLE β¨
okay so this page is literally gonna walk you through source code samples showing how to write a simple blocking QUIC client app that connects to a server, sends it an HTTP/1.0 request, and reads the response back. π
β οΈ real talk before we start: HTTP/1.0 over QUIC is non-standard and real-world servers will not support it. This is for demonstration purposes only, fr fr. don't @ me.
We're assuming a few things about you, bestie:
- you already have OpenSSL installed on your system π
- you already have some fundamental understanding of OpenSSL concepts, TLS and QUIC (go read
ossl-guide-libraries-introduction(7),ossl-guide-tls-introduction(7)andossl-guide-quic-introduction(7)) - you know how to write and build C code and link it against the
libcryptoandlibssllibraries that OpenSSL provides - you have a basic understanding of UDP/IP and sockets
The example code we build here amends the blocking TLS client example covered in ossl-guide-tls-client-block(7). Like, we're only discussing the differences between that client and this one, so we also assume you've run through that tutorial and understood it. iykyk π§
For this tutorial our client will be using a single QUIC stream. A subsequent tutorial will discuss how to write a multi-stream client (see ossl-guide-quic-multi-stream(7)).
The complete source code for this example blocking QUIC client is available in the demos/guide directory of the OpenSSL source distribution in the file quic-client-block.c. It's also available online at <https://github.com/openssl/openssl/blob/master/demos/guide/quic-client-block.c>.
πΈ Creating the SSL_CTX and SSL objects πΈ
So in the TLS tutorial (ossl-guide-tls-client-block(7)) we created an SSL_CTX object for our client and used it to create an SSL object to represent the TLS connection. A QUIC connection works in exactly the same way β no cap. We first create an SSL_CTX object, then use it to create an SSL object to represent the QUIC connection. π
As in the TLS example, the first step is to create an SSL_CTX object for our client. Same as before, except we use a different "method". OpenSSL offers two different QUIC client methods, i.e. OSSL_QUIC_client_method(3) and OSSL_QUIC_client_thread_method(3).
- The first one is the equivalent of
TLS_client_method(3)but for the QUIC protocol. - The second one is the same, but it will additionally create a background thread for handling time based events (known as "thread assisted mode", see
ossl-guide-quic-introduction(7)).
For this tutorial we'll be using OSSL_QUIC_client_method(3) because we will not be leaving the QUIC connection idle in our application, so thread assisted mode is not needed. lowkey efficient π
/*
* Create an SSL_CTX which we can use to create SSL objects from. We
* want an SSL_CTX for creating clients so we use OSSL_QUIC_client_method()
* here.
*/
ctx = SSL_CTX_new(OSSL_QUIC_client_method());
if (ctx == NULL) {
printf("Failed to create the SSL_CTX\n");
goto end;
}
The other setup steps we applied to the SSL_CTX for TLS also apply to QUIC β except for restricting the TLS versions we're willing to accept. The QUIC protocol implementation in OpenSSL currently only supports TLSv1.3. There is no need to call SSL_CTX_set_min_proto_version(3) or SSL_CTX_set_max_proto_version(3) in an OpenSSL QUIC application, and any such call will be ignored. π«Ά
Once the SSL_CTX is created, the SSL object is constructed in exactly the same way as for the TLS application.
π¦ Creating the socket and BIO π¦
A major difference between TLS and QUIC is the underlying transport protocol. TLS uses TCP while QUIC uses UDP. The way the QUIC socket is created in our example code is much the same as for TLS. We use the BIO_lookup_ex(3) and BIO_socket(3) helper functions as we did in the previous tutorial, except that we pass SOCK_DGRAM as an argument to indicate UDP (instead of SOCK_STREAM for TCP). β¨
/*
* Lookup IP address info for the server.
*/
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_DGRAM, 0,
&res))
return NULL;
/*
* Loop through all the possible addresses for the server and find one
* we can connect to.
*/
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
/*
* Create a TCP socket. We could equally use non-OpenSSL calls such
* as "socket" here for this and the subsequent connect and close
* functions. But for portability reasons and also so that we get
* errors on the OpenSSL stack in the event of a failure we use
* OpenSSL's versions of these functions.
*/
sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_DGRAM, 0, 0);
if (sock == -1)
continue;
/* Connect the socket to the server's address */
if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), 0)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
/* Set to nonblocking mode */
if (!BIO_socket_nbio(sock, 1)) {
BIO_closesocket(sock);
sock = -1;
continue;
}
break;
}
if (sock != -1) {
*peer_addr = BIO_ADDR_dup(BIO_ADDRINFO_address(ai));
if (*peer_addr == NULL) {
BIO_closesocket(sock);
return NULL;
}
}
/* Free the address information resources we allocated earlier */
BIO_ADDRINFO_free(res);
You may notice a couple of other differences between this code and the version we used for TLS. π
- Firstly, we set the socket into nonblocking mode. This must always be done for an OpenSSL QUIC application. And yeah, that may be surprising considering we're trying to write a blocking client β but despite this, the
SSLobject will still have blocking behaviour. Seeossl-guide-quic-introduction(7)for further information on this. it's giving "trust the process" π - Secondly, we take note of the IP address of the peer that we're connecting to. We store that information away. We will need it later. π
See BIO_lookup_ex(3), BIO_socket(3), BIO_connect(3), BIO_closesocket(3), BIO_ADDRINFO_next(3), BIO_ADDRINFO_address(3), BIO_ADDRINFO_free(3) and BIO_ADDR_dup(3) for further information on the functions used here. In the above example code the hostname and port variables are strings, e.g. "www.example.com" and "443".
As for our TLS client, once the socket has been created and connected we need to associate it with a BIO object:
BIO *bio;
/* Create a BIO to wrap the socket */
bio = BIO_new(BIO_s_datagram());
if (bio == NULL) {
BIO_closesocket(sock);
return NULL;
}
/*
* Associate the newly created BIO with the underlying socket. By
* passing BIO_CLOSE here the socket will be automatically closed when
* the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which
* case you must close the socket explicitly when it is no longer
* needed.
*/
BIO_set_fd(bio, sock, BIO_CLOSE);
Note the use of BIO_s_datagram(3) here as opposed to BIO_s_socket(3) that we used for our TLS client. This is again due to the fact that QUIC uses UDP instead of TCP for its transport layer. See BIO_new(3), BIO_s_datagram(3) and BIO_set_fd(3) for further information on these functions. π
π Setting the server's hostname π
As in the TLS tutorial we need to set the server's hostname both for SNI (Server Name Indication) and for certificate validation purposes. The steps for this are identical to the TLS tutorial and won't be repeated here. periodt. βοΈ
π Setting the ALPN π
ALPN (Application-Layer Protocol Negotiation) is a feature of TLS that enables the application to negotiate which protocol will be used over the connection. For example, if you intend to use HTTP/3 over the connection then the ALPN value for that is "h3" (see <https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xml#alpn-protocol-ids>).
OpenSSL provides the ability for a client to specify the ALPN to use via the SSL_set_alpn_protos(3) function. This is optional for a TLS client, and so our simple client that we developed in ossl-guide-tls-client-block(7) did not use it. However β QUIC mandates that the TLS handshake used in establishing a QUIC connection must use ALPN. no skipping, bestie π₯
unsigned char alpn[] = { 8, 'h', 't', 't', 'p', '/', '1', '.', '0' };
/* SSL_set_alpn_protos returns 0 for success! */
if (SSL_set_alpn_protos(ssl, alpn, sizeof(alpn)) != 0) {
printf("Failed to set the ALPN for the connection\n");
goto end;
}
The ALPN is specified using a length prefixed array of unsigned char (it is not a NUL terminated string). Our original TLS blocking client demo was using HTTP/1.0. We'll use the same for this example. And heads up β unlike most OpenSSL functions, SSL_set_alpn_protos(3) returns zero for success and nonzero for failure. yes it's backwards, yes we know, just go with it π
π Setting the peer address π
An OpenSSL QUIC application must specify the target address of the server that is being connected to. In "Creating the socket and BIO" above we saved that address away for future use. Now we need to use it via the SSL_set1_initial_peer_addr(3) function. π
/* Set the IP address of the remote peer */
if (!SSL_set1_initial_peer_addr(ssl, peer_addr)) {
printf("Failed to set the initial peer address\n");
goto end;
}
Note that we will need to free the peer_addr value that we allocated via BIO_ADDR_dup(3) earlier:
BIO_ADDR_free(peer_addr);
π The handshake and application data transfer π
Once initial setup of the SSL object is complete, then we perform the handshake via SSL_connect(3) in exactly the same way as we did for the TLS client, so we won't repeat it here. β¨
We can also perform data transfer using a default QUIC stream that is automatically associated with the SSL object for us. We can transmit data using SSL_write_ex(3), and receive data using SSL_read_ex(3) in the same way as for TLS. The main difference is that we have to account for failures slightly differently. With QUIC the stream can be reset by the peer (which is fatal for that stream), but the underlying connection itself may still be healthy. π«Ά so like, don't panic and throw the whole connection away, ok?
Okay so boom β we're writing the whole request to the stream now. And here's the lil detail that matters: we gotta tell the server we're done writing. Two ways to do that, bestie: pass the SSL_WRITE_FLAG_CONCLUDE flag to SSL_write_ex2(3), or call SSL_stream_conclude(3). The first one is the more efficient route, so that's the icon we're going with. π
/* Write an HTTP GET request to the peer */
if (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
printf("Failed to write start of HTTP request\n");
goto end;
}
if (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
printf("Failed to write hostname in HTTP request\n");
goto end;
}
if (!SSL_write_ex2(ssl, request_end, strlen(request_end),
SSL_WRITE_FLAG_CONCLUDE, &written)) {
printf("Failed to write end of HTTP request\n");
goto end;
}
Then boom β we read the response back from the server. π
/*
* Get up to sizeof(buf) bytes of the response. We keep reading until the
* server closes the connection.
*/
while (SSL_read_ex(ssl, 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");
/*
* 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(ssl, 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(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. 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;
}
So in that code up there you can see how SSL_ERROR_SSL is giving stream fatal error energy π. We hit up SSL_get_stream_read_state(3) to figure out whether the stream got reset, or whether some other fatal error happened. That's the whole vibe check right there. π₯
π¦ Shutting down the connection π¦
Okay so remember in the TLS tutorial how we knew the server was done sending data? Because SSL_read_ex(3) returned 0 and SSL_get_error(3) returned SSL_ERROR_ZERO_RETURN. Yeah β same deal with QUIC, except SSL_ERROR_ZERO_RETURN lowkey means something a lil different now.
With TLS, that meant the server sent a "close_notify" alert, and no more data is coming from the server on that connection. Simple, done, slay.
With QUIC? It means the server indicated "FIN" on the stream β so it won't send more data on that stream. But bestie, that's only tea about the stream itself, not the underlying connection. More data could still come from the server on some other stream. And also β even though the server won't send us more data, nothing's stopping us from sending more data to the server. π
In this tutorial, once we're done reading data from the server on the one single stream we're using, we close the connection down. Same as before, we do it with SSL_shutdown(3). This QUIC example is super similar to the TLS version, BUT β and this is the plot twist β SSL_shutdown(3) has to be called more than once:
/*
* Repeatedly call SSL_shutdown() until the connection is fully
* closed.
*/
do {
ret = SSL_shutdown(ssl);
if (ret < 0) {
printf("Error shutting down: %d\n", ret);
goto end;
}
} while (ret != 1);
The shutdown process is a whole two-stage saga, ngl:
- Stage one β we wait until all the data we've buffered for sending on any stream has been successfully sent and acknowledged by the peer, and then we send a
CONNECTION_CLOSEto the peer to say "this connection is no longer usable." That immediately closes the connection β no more data can be sent or received, periodt.SSL_shutdown(3)returns 0 once this first stage is complete. - Stage two β the connection enters a
"closing"state. Application data can't be sent or received in this state, but late-arriving packets coming from the peer will be handled appropriately. Once this stage finishes successfully,SSL_shutdown(3)returns 1 to signal success. β¨
π FURTHER READING π
Wanna level up? Go read ossl-guide-quic-multi-stream(7) β it's the tutorial on how to modify the client we built on this page so it supports multiple streams. Main character energy upgrade, fr. π
π SEE ALSO π
ossl-guide-introduction(7)ossl-guide-libraries-introduction(7)ossl-guide-libssl-introduction(7)ossl-guide-tls-introduction(7)ossl-guide-tls-client-block(7)ossl-guide-quic-introduction(7)
π COPYRIGHT π
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>.
And that's the whole page, bestie β this one's from OpenSSL 3.5.9, dated 2026-09-2, and honestly? It ate. Go build something iconic with it. π β¨π«Ά
π¦ where this page comes from
- alpine 3.23: openssl-doc 3.5.9-r0 Β· Apache-2.0
- alpine 3.24: openssl-doc 3.5.9-r0 Β· Apache-2.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms