OSSL-GUIDE-QUIC-CLIENT-BLOCK(7SSL)
π OSSL-GUIDE-QUIC-CLIENT-BLOCK(7SSL) π
okay bestie, welcome to OpenSSL's guide page ossl-guide-quic-client-block β it's the one in the 7SSL manual, and it's literally about writing a simple blocking QUIC client. grab a lil snack, we're doing networking β¨
π NAME π
ossl-guide-quic-client-block β the OpenSSL Guide: Writing a simple blocking QUIC client π¦
πΈ SIMPLE BLOCKING QUIC CLIENT EXAMPLE πΈ
okay so this page is gonna hand you a bunch of source code samples showing how to write a simple blocking QUIC client app. what does it do? it connects to a server, sends it an HTTP/1.0 request, and reads the response back. simple, cute, iconic π
β οΈ real talk: HTTP/1.0 over QUIC is non-standard and real world servers will not support it. this is for demonstration purposes only. like, we're doing this for the vibes and the learning, not for production, no cap.
so here's what we're assuming about you before we start:
- you already have OpenSSL installed on your system π«Ά
- you've got some fundamental understanding of OpenSSL concepts, TLS and QUIC (go peek at
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 how to link it against the
libcryptoandlibssllibraries that OpenSSL provides - you've got a basic understanding of UDP/IP and sockets
- you've run through and understood the blocking TLS client example in
ossl-guide-tls-client-block(7)β because the example code we build here literally amends that one
the way that this works: we're only discussing the differences between that TLS client and this QUIC one. so if you skipped the TLS tutorial, bestie, go back, we're building on it fr fr.
for this tutorial our client is using a single QUIC stream. a subsequent tutorial will discuss how to write a multi-stream client (see ossl-guide-quic-multi-stream(7)) β that's a whole other episode π
the complete source code for this example blocking QUIC client lives in the "demos/guide" directory of the OpenSSL source distribution in the file "quic-client-block.c". it's also 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 that represents the TLS connection. a QUIC connection works in exactly the same way. first we create an SSL_CTX object, then we use it to create an SSL object to represent the QUIC connection. same energy, different protocol π«
like in the TLS example, step one is creating an SSL_CTX object for our client. this is done in the same way 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, except it will additionally create a background thread for handling time based events (this is known as
"thread assisted mode", seeossl-guide-quic-introduction(7)).
for this tutorial we're using OSSL_QUIC_client_method(3) because we will not be leaving the QUIC connection idle in our application, and so thread assisted mode is not needed π
/*
* 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 that 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. slay.
π Creating the socket and BIO π
a major difference between TLS and QUIC is the underlying transport protocol:
- TLS uses TCP
- QUIC uses UDP
that's the plot twist of this whole page ngl π«
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 like we did in the previous tutorial, except 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 have noticed 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. yes, this may be surprising, considering we're literally trying to write a blocking client β but despite this, the SSL object will still have blocking behaviour. see ossl-guide-quic-introduction(7) for further info on this. it's giving "and yet, she blocks" π
Secondly, we take note of the IP address of the peer we're connecting to. we store that information away, because we will need it later. foreshadowing β¨
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);
not me using BIO_s_datagram(3) here instead of 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. iykyk π«Ά
π 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. so yeah, it's mandatory now, no skipping π€
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 chars (it is not a NUL terminated string β important!!). our original TLS blocking client demo was using HTTP/1.0, and we will use the same for this example. unlike most OpenSSL functions, SSL_set_alpn_protos(3) returns zero for success and nonzero for failure. yes it's backwards from the rest of OpenSSL, yes it trips everyone up, we move π
π Setting the peer address π
an OpenSSL QUIC application must specify the target address of the server that is being connected to. remember in "Creating the socket and BIO" above where we saved that address away for future use? this is the future. we now 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);
don't forget it bestie, that's a leak otherwise π«§
β 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. same vibe, same call π
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 β dead stream, living connection. keep that distinction in your head, it's the whole point, periodt β¨
Okay so first things first, bestie: we gotta write the whole request to the stream. And we also need to let the server know we're done writing, periodt. 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 we're gonna slay with that one. π
/* 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;
}
Next up: reading the server's response. Let's get into it. π¦
/*
* 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 block above, you can see that SSL_ERROR_SSL is giving stream fatal error vibes. We can use SSL_get_stream_read_state(3) to figure out if the stream got reset, or if some other fatal error happened. Lowkey important to know. π
π Shutting down the connection π
So, in the TLS tutorial, 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. Same energy with QUIC, but the interpretation is a little different, bestie. With TLS, that meant the server sent a "close_notify" alert. No more data from the server on that connection, periodt.
With QUIC, it means the server sent "FIN" on the stream, so it won't send any more data on that specific stream. But β and this is key β that only tells us about the stream itself, not the whole underlying connection. The server could still send more data on some other stream. And even though the server won't send more data to us, it doesn't stop us from sending more data to the server. It's giving 'we're not done yet, actually'. π
In this tutorial, once we've finished reading data from the server on the one stream we're using, we're gonna close the whole connection down. Just like before, we use the SSL_shutdown(3) function. This QUIC example is super similar to the TLS version, but heads up: SSL_shutdown(3) needs to be called more than once. Not a one-and-done situation. π
/*
* 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 has two stages, okay? 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. Then we send a CONNECTION_CLOSE to the peer to say the connection is no longer usable. This immediately closes the connection β no more data can be sent or received. SSL_shutdown(3) returns 0 once stage one is done. π
Stage two: the connection enters a "closing" state. In this state, application data can't be sent or received, but late-arriving packets from the peer will still be handled appropriately. Once this stage completes successfully, SSL_shutdown(3) returns 1 to indicate success. We love a happy ending. β¨
π₯ FURTHER READING π₯
If you wanna level up and make this client support multiple streams, check out ossl-guide-quic-multi-stream(7). It's a whole tutorial on how to modify the client we just built. Obsessed. π₯
π 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>. π
That's the whole vibe, bestie. This page is from OpenSSL 4.0.3, dated 2026-09-29. Stay slaying! πβ¨
π¦ where this page comes from
- ubuntu stonking: openssl 4.0.3-1ubuntu1
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms