diff --git a/demos/guide/quic-client-non-block.c b/demos/guide/quic-client-non-block.c index 743c2839c9..870dd1c4fe 100644 --- a/demos/guide/quic-client-non-block.c +++ b/demos/guide/quic-client-non-block.c @@ -164,13 +164,17 @@ static int handle_io_failure(SSL *ssl, int res) 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. + * 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. */ + /* + * The stream has been reset but the connection is still + * healthy. + */ break; case SSL_STREAM_STATE_CONN_CLOSED: @@ -183,9 +187,9 @@ static int handle_io_failure(SSL *ssl, int res) break; } /* - * If the failure is due to a verification error we can get more - * information about it from SSL_get_verify_result(). - */ + * 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))); @@ -300,8 +304,8 @@ int main(void) } /* - * The underlying socket is always non-blocking with QUIC, but the default - * behaviour of the SSL object is still to block. We set it for non-blocking + * 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)) { diff --git a/doc/build.info b/doc/build.info index 7e819cfe31..aec4ae616f 100644 --- a/doc/build.info +++ b/doc/build.info @@ -4779,6 +4779,10 @@ DEPEND[html/man7/ossl-guide-quic-client-block.html]=man7/ossl-guide-quic-client- GENERATE[html/man7/ossl-guide-quic-client-block.html]=man7/ossl-guide-quic-client-block.pod DEPEND[man/man7/ossl-guide-quic-client-block.7]=man7/ossl-guide-quic-client-block.pod GENERATE[man/man7/ossl-guide-quic-client-block.7]=man7/ossl-guide-quic-client-block.pod +DEPEND[html/man7/ossl-guide-quic-client-non-block.html]=man7/ossl-guide-quic-client-non-block.pod +GENERATE[html/man7/ossl-guide-quic-client-non-block.html]=man7/ossl-guide-quic-client-non-block.pod +DEPEND[man/man7/ossl-guide-quic-client-non-block.7]=man7/ossl-guide-quic-client-non-block.pod +GENERATE[man/man7/ossl-guide-quic-client-non-block.7]=man7/ossl-guide-quic-client-non-block.pod DEPEND[html/man7/ossl-guide-quic-introduction.html]=man7/ossl-guide-quic-introduction.pod GENERATE[html/man7/ossl-guide-quic-introduction.html]=man7/ossl-guide-quic-introduction.pod DEPEND[man/man7/ossl-guide-quic-introduction.7]=man7/ossl-guide-quic-introduction.pod @@ -5007,6 +5011,7 @@ html/man7/ossl-guide-libraries-introduction.html \ html/man7/ossl-guide-libssl-introduction.html \ html/man7/ossl-guide-migration.html \ html/man7/ossl-guide-quic-client-block.html \ +html/man7/ossl-guide-quic-client-non-block.html \ html/man7/ossl-guide-quic-introduction.html \ html/man7/ossl-guide-quic-multi-stream.html \ html/man7/ossl-guide-tls-client-block.html \ @@ -5148,6 +5153,7 @@ man/man7/ossl-guide-libraries-introduction.7 \ man/man7/ossl-guide-libssl-introduction.7 \ man/man7/ossl-guide-migration.7 \ man/man7/ossl-guide-quic-client-block.7 \ +man/man7/ossl-guide-quic-client-non-block.7 \ man/man7/ossl-guide-quic-introduction.7 \ man/man7/ossl-guide-quic-multi-stream.7 \ man/man7/ossl-guide-tls-client-block.7 \ diff --git a/doc/man7/ossl-guide-introduction.pod b/doc/man7/ossl-guide-introduction.pod index 02d38de551..b6c1f955bb 100644 --- a/doc/man7/ossl-guide-introduction.pod +++ b/doc/man7/ossl-guide-introduction.pod @@ -85,6 +85,8 @@ The pages in the guide are as follows: =item L: Writing a simple multi-stream QUIC client +=item L: Writing a simple nonblocking QUIC client + =item L: Migrating from older OpenSSL versions =back diff --git a/doc/man7/ossl-guide-quic-client-non-block.pod b/doc/man7/ossl-guide-quic-client-non-block.pod new file mode 100644 index 0000000000..b015a6fbf1 --- /dev/null +++ b/doc/man7/ossl-guide-quic-client-non-block.pod @@ -0,0 +1,440 @@ +=pod + +=begin comment + +NB: Changes to the source code samples in this file should also be reflected in +demos/guide/quic-client-non-block.c + +=end comment + +=head1 NAME + +ossl-guide-quic-client-non-block +- OpenSSL Guide: Writing a simple nonblocking QUIC client + +=head1 SIMPLE NONBLOCKING QUIC CLIENT EXAMPLE + +This page will build on the example developed on the +L page which demonstrates how to write a simple +blocking QUIC client. On this page we will amend that demo code so that it +supports nonblocking functionality. + +The complete source code for this example nonblocking QUIC client is available +in the B directory of the OpenSSL source distribution in the file +B. It is also available online at +L. + +As we saw in the previous example an OpenSSL QUIC application always uses a +nonblocking socket. However, despite this, the B object still has blocking +behaviour. When the B object has blocking behaviour then this means that +it waits (blocks) until data is available to read if you attempt to read from +it when there is no data yet. Similarly it waits when writing if the B +object is currently unable to write at the moment. This can simplify the +development of code because you do not have to worry about what to do in these +cases. The execution of the code will simply stop until it is able to continue. +However in many cases you do not want this behaviour. Rather than stopping and +waiting your application may need to go and do other tasks whilst the B +object is unable to read/write, for example updating a GUI or performing +operations on some other connection or stream. + +We will see later in this tutorial how to change the B object so that it +has nonblocking behaviour. With a nonblocking B object, functions such as +L or L will return immediately with a non-fatal +error if they are currently unable to read or write respectively. + +Since this page is building on the example developed on the +L page we assume that you are familar with it +and we only explain how this example differs. + +=head2 Performing work while waiting for the socket + +In a nonblocking application you will need work to perform in the event that +we want to read or write to the B object but we are currently unable to. +In fact this is the whole point of using a nonblocking B object, i.e. to +give the application the opportunity to do something else. Whatever it is that +the application has to do, it must also be prepared to come back and retry the +operation that it previously attempted periodically to see if it can now +complete. Ideally it would only do this in the event that something has changed +such that it might succeed on the retry attempt, but this does not have to be +the case. It can retry at any time. + +Note that it is important that you retry exactly the same operation that you +tried last time. You cannot start something new. For example if you were +attempting to write the text "Hello World" and the operation failed because the +B object is currently unable to write, then you cannot then attempt to +write some other text when you retry the operation. + +In this demo application we will create a helper function which simulates doing +other work. In fact, for the sake of simplicity, it will do nothing except wait +for the state of the underlying socket to change or until a timeout expires +after which the state of the B object might have changed. We will call our +function C. + + 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. If we have a timeout we use it to + * ensure that OpenSSL is called when it wants to be. + */ + + select(width, &rfds, &wfds, NULL, tvp); +} + +If you are familiar with how to write nonblocking applications in OpenSSL for +TLS (see L) then you should note that there +is an important difference here between the way a QUIC application and a TLS +application works. With a TLS application if we try to read or write something +to the B object and we get a "retry" response (B or +B) then we can assume that is because OpenSSL attempted to +read or write to the underlying socket and the socket signalled the "retry". +With QUIC that is not the case. OpenSSL may signal retry as a result of an +L or L (or similar) call which indicates the +state of the stream. This is entirely independent of whether the underlying +socket needs to retry or not. + +To determine whether OpenSSL currently wants to read or write to the underlying +socket for a QUIC application we must call the L and +L functions. + +It is also important with QUIC that we periodically call an I/O function (or +otherwise call the L function) to ensure that the QUIC +connection remains healthy. This is particularly important with a nonblocking +application because you are likely to leave the B object idle for a while +while the application goes off to do other work. The L +function can be used to determine what the deadline is for the next time we need +to call an I/O function (or call L). + +An alternative to using L to find the next deadline +that OpenSSL must be called again by is to use "thread assisted" mode. In +"thread assisted" mode OpenSSL spawns an additional thread which will +periodically call L automatically, meaning that the +application can leave the connection idle safe in the knowledge that the +connection will still be maintained in a healthy state. See +L below for further details about this. + +In this example we are using the C waits for the state of the underlying +socket(s) to become readable/writeable or until the timeout has expired before +returning. + +=head2 Handling errors from OpenSSL I/O functions + +A QUIC application that has been configured for nonblocking behaviour will need +to be prepared to handle errors returned from OpenSSL I/O functions such as +L or L. 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 are trying to read from the +stream but no data has not yet arrived from the peer for that stream). + +L and L will return 0 to indicate an error and +L and L will return 0 or a negative value to indicate +an error. L will return a negative value to incidate an error. + +In the event of an error an application should call L to find +out what type of error has occurred. If the error is non-fatal and can be +retried then L will return B or +B depending on whether OpenSSL wanted to read to or write +from the stream but was unable to. Note that a call to L or +L can still generate B. Similarly calls to +L or L might generate B. + +Another type of non-fatal error that may occur is B. This +indicates an EOF (End-Of-File) which can occur if you attempt to read data from +an B 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 B and B. 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 L to print out details of errors that have +occurred). You can also consult the return value of +L to determine whether the error is local to the +stream, or whether the underlying connection has also failed. A return value +of B tells you that the stream has been reset by +the peer and B 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: + + 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; + } + } + +This function takes as arguments the B object that represents the +connection, as well as the return code from the I/O function that failed. In +the event of a non-fatal failure, it waits until a retry of the I/O operation +might succeed (by using the C function that we developed +in the previous section). It returns 1 in the event of a non-fatal error +(except EOF), 0 in the event of EOF, or -1 if a fatal error occurred. + +=head2 Creating the SSL_CTX and SSL objects + +In order to connect to a server we must create B and B objects for +this. Most of the steps to do this are the same as for a blocking client and are +explained on the L page. We won't repeat that +information here. + +One key difference is that we must put the B object into nonblocking mode +(the default is blocking mode). To do that we use the +L function: + + /* + * 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; + } + +Although the demo application that we are developing here does not use it, it is +possible to use "thread assisted mode" when developing QUIC applications. +Normally, when writing an OpenSSL QUIC application, it is important that +L (or alternatively any I/O function) is called on the +connection B object periodically to maintain the connection in a healthy +state. See L for more discussion +on this. This is particularly important to keep in mind when writing a +nonblocking QUIC application because it is common to leave the B 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 that the application developer does not need to handle this aspect. To do +this we must use L when we construct the +B as shown below: + + ctx = SSL_CTX_new(OSSL_QUIC_client_thread_method()); + if (ctx == NULL) { + printf("Failed to create the SSL_CTX\n"); + goto end; + } + +=head2 Performing the handshake + +As in the demo for a blocking QUIC client we use the L function +to perform the handshake with the server. Since we are using a nonblocking +B object it is very likely that calls to this function will fail with a +non-fatal error while we are waiting for the server to respond to our handshake +messages. In such a case we must retry the same L call at a +later time. In this demo we do this 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 continually call L until it gives us a success response. +Otherwise we use the C function that we created earlier to +work out what we should do next. Note that we do not expect an EOF to occur at +this stage, so such a response is treated in the same way as a fatal error. + +=head2 Sending and receiving data + +As with the blocking QUIC client demo we use the L function to +send data to the server. As with L above, because we are using +a nonblocking B object, this call could fail with a non-fatal error. In +that case we should retry exactly the same L call again. Note +that the parameters must be I 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 +(B) which will configure OpenSSL to allow +the buffer being written to change from one retry to the next. However, in this +case, you must still retry exactly the same data - even though the buffer that +contains that data may change location. See L for further +details. + + /* Write an HTTP GET request to the peer */ + while (!SSL_write_ex(ssl, request, strlen(request), &written)) { + if (handle_io_failure(ssl, 0) == 1) + continue; /* Retry */ + printf("Failed to write 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 is valid for us to receive an EOF +response when trying to read data from the server. This will occur 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 in the response +from the server. We continue going around the loop until we either encounter a +fatal error, or we receive an EOF (indicating a graceful finish). + +=head2 Shutting down the connection + +As in the QUIC blocking example we must shutdown the connection when we are +finished with it. + +Even though we have received EOF on the stream that we were reading from above, +this tell us nothing about the state of the underlying connection. Our demo +applicaiton will initiate the connection shutdown process via +L. + +Since our application is initiating the shutdown then we might expect to see +L give a return value of 0, and then we should continue to call +it until we recieve a return value of 1 (meaning we have successfully completed +the shutdown). Since we are using a nonblocking B object we might expect to +have to retry this operation several times. If L returns a +negative result then we must call L to work out what to do +next. We use our handle_io_failure() function that we developed earlier for +this: + + /* + * 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 */ + } + +=head2 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. + +=head1 FURTHER READING + +See L to read a tutorial on how to write a +blocking QUIC client. See L to see how to write +a multi-stream QUIC client. + +=head1 SEE ALSO + +L, L, +L, L, +L, L + +=head1 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 +L. + +=cut diff --git a/doc/man7/ossl-guide-quic-multi-stream.pod b/doc/man7/ossl-guide-quic-multi-stream.pod index 4291c30fa7..4e4d852b03 100644 --- a/doc/man7/ossl-guide-quic-multi-stream.pod +++ b/doc/man7/ossl-guide-quic-multi-stream.pod @@ -166,7 +166,10 @@ is for demonstration purposes only. We will build on the example code for the simple blocking QUIC client that is covered on the L page and we assume that you are familiar with it. We will only describe the differences between the simple -blocking QUIC client and the multi-stream QUIC client. +blocking QUIC client and the multi-stream QUIC client. Although the example code +uses blocking B objects, you can equally use nonblocking B objects. +See L for more information about writing a +nonblocking QUIC client. The complete source code for this example multi-stream QUIC client is available in the C directory of the OpenSSL source distribution in the file