Skip to content

Latest commit

 

History

History
294 lines (233 loc) · 12 KB

ossl-guide-quic-client-block.pod

File metadata and controls

294 lines (233 loc) · 12 KB

NAME

ossl-guide-quic-client-block - OpenSSL Guide: Writing a simple blocking QUIC client

SIMPLE BLOCKING QUIC CLIENT EXAMPLE

This page will present various source code samples demonstrating how to write a simple blocking QUIC client application which connects to a server, sends an HTTP/1.0 request to it, and reads back the response. Note that HTTP/1.0 over QUIC is non-standard and will not typically be supported by real world servers. This is for demonstration purposes only.

We assume that you already have OpenSSL installed on your system; that you already have some fundamental understanding of OpenSSL concepts, TLS and QUIC (see crypto(7), ossl-guide-tls-introduction(7) and ossl-guide-quic-introduction(7)); and that you know how to write and build C code and link it against the libcrypto and libssl libraries that are provided by OpenSSL. It also assumes that you have a basic understanding of UDP/IP and sockets. The example code that we build in this tutorial will amend the blocking TLS client example that is covered in ossl-guide-tls-client-block(7). Only the differences between that client and this one will be discussed so we also assume that you have run through and understand that tutorial.

For this tutorial our client will be using a single QUIC stream. A subsequent tutorial will discuss how to write a multi-stream client.

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 is also available online at https://github.com/openssl/openssl/blob/master/demos/guide/quic-client-block.c.

Creating the SSL_CTX and SSL objects

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. We first create an SSL_CTX object and 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. This is done in the same way as before except that 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 will be 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 that we are 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 that 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, 0, 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)) {
        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 that we used for TLS.

Firstly, we set the socket into nonblocking mode. This must always be done for an OpenSSL QUIC application. This may be surprising considering that we are trying to write a blocking client. Despite this the SSL object will still have blocking behaviour. See ossl-guide-quic-introduction(7) for further information on this.

Secondly, we take note of the IP address of the peer that we are 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);

/*
 * 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.

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.

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). Our original TLS blocking client demo was using HTTP/1.0. 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.

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_set_initial_peer_addr(3) function.

if (!SSL_set_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 exactly the same way as for TLS. Again, we won't repeat it here.

Shutting down the connection

As in the TLS tutorial, once we have finished reading data from the server then we are ready to close the connection down. As before we do this via the SSL_shutdown(3) function. This example for QUIC is very similar to the TLS version. However the SSL_shutdown(3) function may need 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 in two stages. First we gracefully shutdown the connection for sending and secondly we shutdown the connection for receiving. SSL_shutdown(3) returns 0 once the first stage has been completed, and 1 once the second stage is finished. This two stage process applies to TLS as well. However in our blocking TLS client example code we knew that the peer had already partially closed down due to the SSL_ERROR_ZERO_RETURN that we had obtained via SSL_get_error(3) after the final SSL_read_ex(3) call. Due to that return value we knew that the connection was already closed down for receiving data and hence SSL_shutdown(3) should only need to be called once.

However, with QUIC, the SSL_ERROR_ZERO_RETURN value only tells us that the stream (not the connection) is partially closed down. Therefore we need to call SSL_shutdown(3) more than once to ensure that we progress through both stages of the shutdown process.

SEE ALSO

crypto(7), ossl-guide-tls-introduction(7), ossl-guide-tls-client-block(7), ossl-guide-quic-introduction(7)

COPYRIGHT

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.