| /* |
| * Copyright 2022 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 |
| */ |
| |
| #include <openssl/bio.h> |
| #include "internal/e_os.h" |
| #include "internal/sockets.h" |
| #include "internal/bio_tfo.h" |
| #include "testutil.h" |
| |
| /* If OS support is added in crypto/bio/bio_tfo.h, add it here */ |
| #if defined(OPENSSL_SYS_LINUX) |
| # define GOOD_OS 1 |
| #elif defined(__FreeBSD__) |
| # define GOOD_OS 1 |
| #elif defined(OPENSSL_SYS_MACOSX) |
| # define GOOD_OS 1 |
| #else |
| # ifdef GOOD_OS |
| # undef GOOD_OS |
| # endif |
| #endif |
| |
| #if !defined(OPENSSL_NO_TFO) && defined(GOOD_OS) |
| |
| /* |
| * This test is to ensure that if TCP Fast Open is configured, that socket |
| * connections will still work. These tests are able to detect if TCP Fast |
| * Open works, but the tests will pass as long as the socket connects. |
| * |
| * The first test function tests the socket interface as implemented as BIOs. |
| * |
| * The second test functions tests the socket interface as implemented as fds. |
| * |
| * The tests are run 5 times. The first time is without TFO. |
| * The second test will create the TCP fast open cookie, |
| * this can be seen in `ip tcp_metrics` and in /proc/net/netstat/ on Linux. |
| * e.g. on Linux 4.15.0-135-generic: |
| * $ grep '^TcpExt:' /proc/net/netstat | cut -d ' ' -f 84-90 | column -t |
| * The third attempt will use the cookie and actually do TCP fast open. |
| * The 4th time is client-TFO only, the 5th time is server-TFO only. |
| */ |
| |
| # define SOCKET_DATA "FooBar" |
| # define SOCKET_DATA_LEN sizeof(SOCKET_DATA) |
| |
| static int test_bio_tfo(int idx) |
| { |
| BIO *cbio = NULL; |
| BIO *abio = NULL; |
| BIO *sbio = NULL; |
| int ret = 0; |
| int sockerr = 0; |
| const char *port; |
| int server_tfo = 0; |
| int client_tfo = 0; |
| size_t bytes; |
| char read_buffer[20]; |
| |
| switch (idx) { |
| default: |
| case 0: |
| break; |
| case 1: |
| case 2: |
| server_tfo = 1; |
| client_tfo = 1; |
| break; |
| case 3: |
| client_tfo = 1; |
| break; |
| case 4: |
| server_tfo = 1; |
| break; |
| } |
| |
| /* ACCEPT SOCKET */ |
| if (!TEST_ptr(abio = BIO_new_accept("localhost:0")) |
| || !TEST_true(BIO_set_nbio_accept(abio, 1)) |
| || !TEST_true(BIO_set_tfo_accept(abio, server_tfo)) |
| || !TEST_int_gt(BIO_do_accept(abio), 0) |
| || !TEST_ptr(port = BIO_get_accept_port(abio))) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| |
| /* Note: first BIO_do_accept will basically do the bind/listen */ |
| |
| /* CLIENT SOCKET */ |
| if (!TEST_ptr(cbio = BIO_new_connect("localhost")) |
| || !TEST_long_gt(BIO_set_conn_port(cbio, port), 0) |
| || !TEST_long_gt(BIO_set_nbio(cbio, 1), 0) |
| || !TEST_long_gt(BIO_set_tfo(cbio, client_tfo), 0)) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| |
| /* FIRST ACCEPT: no connection should be established */ |
| if (BIO_do_accept(abio) <= 0) { |
| if (!BIO_should_retry(abio)) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: failed without EAGAIN\n"); |
| goto err; |
| } |
| } else { |
| sbio = BIO_pop(abio); |
| BIO_printf(bio_err, "Error: accepted unknown connection\n"); |
| goto err; |
| } |
| |
| /* CONNECT ATTEMPT: different behavior based on TFO support */ |
| if (BIO_do_connect(cbio) <= 0) { |
| sockerr = get_last_socket_error(); |
| if (sockerr == EOPNOTSUPP) { |
| BIO_printf(bio_err, "Skip: TFO not enabled/supported for client\n"); |
| goto success; |
| } else if (sockerr != EINPROGRESS) { |
| BIO_printf(bio_err, "Error: failed without EINPROGRESSn"); |
| goto err; |
| } |
| } |
| |
| /* macOS needs some time for this to happen, so put in a select */ |
| if (!TEST_int_ge(BIO_wait(abio, time(NULL) + 2, 0), 0)) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: socket wait failed\n"); |
| goto err; |
| } |
| |
| /* SECOND ACCEPT: if TFO is supported, this will still fail until data is sent */ |
| if (BIO_do_accept(abio) <= 0) { |
| if (!BIO_should_retry(abio)) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: failed without EAGAIN\n"); |
| goto err; |
| } |
| } else { |
| if (idx == 0) |
| BIO_printf(bio_err, "Success: non-TFO connection accepted without data\n"); |
| else if (idx == 1) |
| BIO_printf(bio_err, "Ignore: connection accepted before data, possibly no TFO cookie, or TFO may not be enabled\n"); |
| else if (idx == 4) |
| BIO_printf(bio_err, "Success: connection accepted before data, client TFO is disabled\n"); |
| else |
| BIO_printf(bio_err, "Warning: connection accepted before data, TFO may not be enabled\n"); |
| sbio = BIO_pop(abio); |
| goto success; |
| } |
| |
| /* SEND DATA: this should establish the actual TFO connection */ |
| if (!TEST_true(BIO_write_ex(cbio, SOCKET_DATA, SOCKET_DATA_LEN, &bytes))) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| |
| /* macOS needs some time for this to happen, so put in a select */ |
| if (!TEST_int_ge(BIO_wait(abio, time(NULL) + 2, 0), 0)) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: socket wait failed\n"); |
| goto err; |
| } |
| |
| /* FINAL ACCEPT: if TFO is enabled, socket should be accepted at *this* point */ |
| if (BIO_do_accept(abio) <= 0) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: socket not accepted\n"); |
| goto err; |
| } |
| BIO_printf(bio_err, "Success: Server accepted socket after write\n"); |
| if (!TEST_ptr(sbio = BIO_pop(abio)) |
| || !TEST_true(BIO_read_ex(sbio, read_buffer, sizeof(read_buffer), &bytes)) |
| || !TEST_size_t_eq(bytes, SOCKET_DATA_LEN) |
| || !TEST_strn_eq(read_buffer, SOCKET_DATA, SOCKET_DATA_LEN)) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| |
| success: |
| sockerr = 0; |
| ret = 1; |
| |
| err: |
| if (sockerr != 0) { |
| const char *errstr = strerror(sockerr); |
| |
| if (errstr != NULL) |
| BIO_printf(bio_err, "last errno: %d=%s\n", sockerr, errstr); |
| } |
| BIO_free(cbio); |
| BIO_free(abio); |
| BIO_free(sbio); |
| return ret; |
| } |
| |
| static int test_fd_tfo(int idx) |
| { |
| struct sockaddr_storage sstorage; |
| socklen_t slen; |
| struct addrinfo *ai = NULL; |
| struct addrinfo hints; |
| int ret = 0; |
| int cfd = -1; /* client socket */ |
| int afd = -1; /* accept socket */ |
| int sfd = -1; /* server accepted socket */ |
| BIO_ADDR *baddr = NULL; |
| char read_buffer[20]; |
| int bytes_read; |
| int server_flags = BIO_SOCK_NONBLOCK; |
| int client_flags = BIO_SOCK_NONBLOCK; |
| int sockerr = 0; |
| unsigned short port; |
| void *addr; |
| size_t addrlen; |
| |
| switch (idx) { |
| default: |
| case 0: |
| break; |
| case 1: |
| case 2: |
| server_flags |= BIO_SOCK_TFO; |
| client_flags |= BIO_SOCK_TFO; |
| break; |
| case 3: |
| client_flags |= BIO_SOCK_TFO; |
| break; |
| case 4: |
| server_flags |= BIO_SOCK_TFO; |
| break; |
| } |
| |
| /* ADDRESS SETUP */ |
| memset(&hints, 0, sizeof(hints)); |
| hints.ai_family = AF_UNSPEC; |
| hints.ai_socktype = SOCK_STREAM; |
| if (!TEST_int_eq(getaddrinfo(NULL, "0", &hints, &ai), 0)) |
| goto err; |
| |
| switch (ai->ai_family) { |
| case AF_INET: |
| port = ((struct sockaddr_in *)ai->ai_addr)->sin_port; |
| addr = &((struct sockaddr_in *)ai->ai_addr)->sin_addr; |
| addrlen = sizeof(((struct sockaddr_in *)ai->ai_addr)->sin_addr); |
| BIO_printf(bio_err, "Using IPv4\n"); |
| break; |
| case AF_INET6: |
| port = ((struct sockaddr_in6 *)ai->ai_addr)->sin6_port; |
| addr = &((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr; |
| addrlen = sizeof(((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr); |
| BIO_printf(bio_err, "Using IPv6\n"); |
| break; |
| default: |
| BIO_printf(bio_err, "Unknown address family %d\n", ai->ai_family); |
| goto err; |
| } |
| |
| if (!TEST_ptr(baddr = BIO_ADDR_new()) |
| || !TEST_true(BIO_ADDR_rawmake(baddr, ai->ai_family, addr, addrlen, port))) |
| goto err; |
| |
| /* ACCEPT SOCKET */ |
| |
| if (!TEST_int_ge(afd = BIO_socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol, 0), 0) |
| || !TEST_true(BIO_listen(afd, baddr, server_flags))) |
| goto err; |
| |
| /* UPDATE ADDRESS WITH PORT */ |
| slen = sizeof(sstorage); |
| if (!TEST_int_ge(getsockname(afd, (struct sockaddr *)&sstorage, &slen), 0)) |
| goto err; |
| |
| switch (sstorage.ss_family) { |
| case AF_INET: |
| port = ((struct sockaddr_in *)&sstorage)->sin_port; |
| addr = &((struct sockaddr_in *)&sstorage)->sin_addr; |
| addrlen = sizeof(((struct sockaddr_in *)&sstorage)->sin_addr); |
| break; |
| case AF_INET6: |
| port = ((struct sockaddr_in6 *)&sstorage)->sin6_port; |
| addr = &((struct sockaddr_in6 *)&sstorage)->sin6_addr; |
| addrlen = sizeof(((struct sockaddr_in6 *)&sstorage)->sin6_addr); |
| break; |
| default: |
| goto err; |
| } |
| |
| if(!TEST_true(BIO_ADDR_rawmake(baddr, sstorage.ss_family, addr, addrlen, port))) |
| goto err; |
| |
| /* CLIENT SOCKET */ |
| if (!TEST_int_ge(cfd = BIO_socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol, 0), 0)) |
| goto err; |
| |
| /* FIRST ACCEPT: no connection should be established */ |
| sfd = BIO_accept_ex(afd, NULL, 0); |
| if (sfd == -1) { |
| sockerr = get_last_socket_error(); |
| /* Note: Windows would hit WSAEWOULDBLOCK */ |
| if (sockerr != EAGAIN) { |
| BIO_printf(bio_err, "Error: failed without EAGAIN\n"); |
| goto err; |
| } |
| } else { |
| BIO_printf(bio_err, "Error: accepted unknown connection\n"); |
| goto err; |
| } |
| |
| /* CONNECT ATTEMPT: different behavior based on TFO support */ |
| if (!BIO_connect(cfd, baddr, client_flags)) { |
| sockerr = get_last_socket_error(); |
| if (sockerr == EOPNOTSUPP) { |
| BIO_printf(bio_err, "Skip: TFO not enabled/supported for client\n"); |
| goto success; |
| } else { |
| /* Note: Windows would hit WSAEWOULDBLOCK */ |
| if (sockerr != EINPROGRESS) { |
| BIO_printf(bio_err, "Error: failed without EINPROGRESS\n"); |
| goto err; |
| } |
| } |
| } |
| |
| /* macOS needs some time for this to happen, so put in a select */ |
| if (!TEST_int_ge(BIO_socket_wait(afd, 1, time(NULL) + 2), 0)) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: socket wait failed\n"); |
| goto err; |
| } |
| |
| /* SECOND ACCEPT: if TFO is supported, this will still fail until data is sent */ |
| sfd = BIO_accept_ex(afd, NULL, 0); |
| if (sfd == -1) { |
| sockerr = get_last_socket_error(); |
| /* Note: Windows would hit WSAEWOULDBLOCK */ |
| if (sockerr != EAGAIN) { |
| BIO_printf(bio_err, "Error: failed without EAGAIN\n"); |
| goto err; |
| } |
| } else { |
| if (idx == 0) |
| BIO_printf(bio_err, "Success: non-TFO connection accepted without data\n"); |
| else if (idx == 1) |
| BIO_printf(bio_err, "Ignore: connection accepted before data, possibly no TFO cookie, or TFO may not be enabled\n"); |
| else if (idx == 4) |
| BIO_printf(bio_err, "Success: connection accepted before data, client TFO is disabled\n"); |
| else |
| BIO_printf(bio_err, "Warning: connection accepted before data, TFO may not be enabled\n"); |
| goto success; |
| } |
| |
| /* SEND DATA: this should establish the actual TFO connection */ |
| #ifdef OSSL_TFO_SENDTO |
| if (!TEST_int_ge(sendto(cfd, SOCKET_DATA, SOCKET_DATA_LEN, OSSL_TFO_SENDTO, |
| (struct sockaddr *)&sstorage, slen), 0)) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| #else |
| if (!TEST_int_ge(writesocket(cfd, SOCKET_DATA, SOCKET_DATA_LEN), 0)) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| #endif |
| |
| /* macOS needs some time for this to happen, so put in a select */ |
| if (!TEST_int_ge(BIO_socket_wait(afd, 1, time(NULL) + 2), 0)) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: socket wait failed\n"); |
| goto err; |
| } |
| |
| /* FINAL ACCEPT: if TFO is enabled, socket should be accepted at *this* point */ |
| sfd = BIO_accept_ex(afd, NULL, 0); |
| if (sfd == -1) { |
| sockerr = get_last_socket_error(); |
| BIO_printf(bio_err, "Error: socket not accepted\n"); |
| goto err; |
| } |
| BIO_printf(bio_err, "Success: Server accepted socket after write\n"); |
| bytes_read = readsocket(sfd, read_buffer, sizeof(read_buffer)); |
| if (!TEST_int_eq(bytes_read, SOCKET_DATA_LEN) |
| || !TEST_strn_eq(read_buffer, SOCKET_DATA, SOCKET_DATA_LEN)) { |
| sockerr = get_last_socket_error(); |
| goto err; |
| } |
| |
| success: |
| sockerr = 0; |
| ret = 1; |
| |
| err: |
| if (sockerr != 0) { |
| const char *errstr = strerror(sockerr); |
| |
| if (errstr != NULL) |
| BIO_printf(bio_err, "last errno: %d=%s\n", sockerr, errstr); |
| } |
| BIO_ADDR_free(baddr); |
| BIO_closesocket(cfd); |
| BIO_closesocket(sfd); |
| BIO_closesocket(afd); |
| return ret; |
| } |
| #endif |
| |
| int setup_tests(void) |
| { |
| #if !defined(OPENSSL_NO_TFO) && defined(GOOD_OS) |
| ADD_ALL_TESTS(test_bio_tfo, 5); |
| ADD_ALL_TESTS(test_fd_tfo, 5); |
| #endif |
| return 1; |
| } |