diff --git a/codebuild/spec/buildspec_ktls.yml b/codebuild/spec/buildspec_ktls.yml
index 5958b9c6711..ddece2a356e 100644
--- a/codebuild/spec/buildspec_ktls.yml
+++ b/codebuild/spec/buildspec_ktls.yml
@@ -1,5 +1,8 @@
 ---
 version: 0.2
+env:
+  variables:
+    S2N_KTLS_TESTING_EXPECTED: true
 phases:
   install:
     commands:
diff --git a/tests/testlib/s2n_io_testlib.c b/tests/testlib/s2n_io_testlib.c
new file mode 100644
index 00000000000..cf5313412ee
--- /dev/null
+++ b/tests/testlib/s2n_io_testlib.c
@@ -0,0 +1,84 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+#include "testlib/s2n_testlib.h"
+
+S2N_CLEANUP_RESULT s2n_test_iovecs_free(struct s2n_test_iovecs *in)
+{
+    RESULT_ENSURE_REF(in);
+    for (size_t i = 0; i < in->iovecs_count; i++) {
+        RESULT_GUARD_POSIX(s2n_free_object((uint8_t **) &in->iovecs[i].iov_base,
+                in->iovecs[i].iov_len));
+    }
+    RESULT_GUARD_POSIX(s2n_free_object((uint8_t **) &in->iovecs,
+            sizeof(struct iovec) * in->iovecs_count));
+    return S2N_RESULT_OK;
+}
+
+static S2N_RESULT s2n_test_split_data(struct s2n_test_iovecs *iovecs, struct s2n_blob *data)
+{
+    RESULT_ENSURE_REF(iovecs);
+    RESULT_ENSURE_REF(data);
+
+    struct s2n_stuffer in = { 0 };
+    RESULT_GUARD_POSIX(s2n_stuffer_init_written(&in, data));
+
+    for (size_t i = 0; i < iovecs->iovecs_count; i++) {
+        if (iovecs->iovecs[i].iov_len == 0) {
+            continue;
+        }
+        struct s2n_blob mem = { 0 };
+        RESULT_GUARD_POSIX(s2n_alloc(&mem, iovecs->iovecs[i].iov_len));
+        RESULT_GUARD_POSIX(s2n_stuffer_read(&in, &mem));
+        iovecs->iovecs[i].iov_base = mem.data;
+    }
+    RESULT_ENSURE_EQ(s2n_stuffer_data_available(&in), 0);
+    return S2N_RESULT_OK;
+}
+
+S2N_RESULT s2n_test_new_iovecs(struct s2n_test_iovecs *iovecs,
+        struct s2n_blob *data, const size_t *lens, size_t lens_count)
+{
+    RESULT_ENSURE_REF(iovecs);
+    RESULT_ENSURE_REF(data);
+    RESULT_ENSURE_REF(lens);
+
+    size_t len_total = 0;
+    for (size_t i = 0; i < lens_count; i++) {
+        len_total += lens[i];
+    }
+    RESULT_ENSURE_LTE(len_total, data->size);
+
+    size_t iovecs_count = lens_count;
+    if (len_total < data->size) {
+        iovecs_count++;
+    }
+
+    struct s2n_blob iovecs_mem = { 0 };
+    RESULT_GUARD_POSIX(s2n_alloc(&iovecs_mem, sizeof(struct iovec) * iovecs_count));
+    RESULT_GUARD_POSIX(s2n_blob_zero(&iovecs_mem));
+    iovecs->iovecs = (struct iovec *) (void *) iovecs_mem.data;
+    iovecs->iovecs_count = iovecs_count;
+
+    for (size_t i = 0; i < lens_count; i++) {
+        iovecs->iovecs[i].iov_len = lens[i];
+    }
+    if (lens_count < iovecs_count) {
+        iovecs->iovecs[lens_count].iov_len = data->size - len_total;
+    }
+
+    RESULT_GUARD(s2n_test_split_data(iovecs, data));
+    return S2N_RESULT_OK;
+}
diff --git a/tests/testlib/s2n_testlib.h b/tests/testlib/s2n_testlib.h
index 0a5aeeacfc4..e67aaff6a7c 100644
--- a/tests/testlib/s2n_testlib.h
+++ b/tests/testlib/s2n_testlib.h
@@ -208,6 +208,18 @@ int s2n_shutdown_test_server_and_client(struct s2n_connection *server_conn, stru
 S2N_RESULT s2n_negotiate_test_server_and_client_with_early_data(struct s2n_connection *server_conn,
         struct s2n_connection *client_conn, struct s2n_blob *early_data_to_send, struct s2n_blob *early_data_received);
 
+/* Testing only with easily constructed contiguous data buffers could hide errors.
+ * We should use iovecs where every buffer is allocated separately.
+ * These test methods construct separate io buffers from one contiguous buffer.
+ */
+struct s2n_test_iovecs {
+    struct iovec *iovecs;
+    size_t iovecs_count;
+};
+S2N_RESULT s2n_test_new_iovecs(struct s2n_test_iovecs *iovecs,
+        struct s2n_blob *data, const size_t *lens, size_t lens_count);
+S2N_CLEANUP_RESULT s2n_test_iovecs_free(struct s2n_test_iovecs *in);
+
 struct s2n_kem_kat_test_vector {
     const struct s2n_kem *kem;
     const char *kat_file;
diff --git a/tests/unit/s2n_ktls_io_test.c b/tests/unit/s2n_ktls_io_test.c
index 3f437d92e4d..65dc4c8eca4 100644
--- a/tests/unit/s2n_ktls_io_test.c
+++ b/tests/unit/s2n_ktls_io_test.c
@@ -72,82 +72,6 @@ ssize_t s2n_test_ktls_recvmsg_io_stuffer_and_ctrunc(void *io_context, struct msg
     return ret;
 }
 
-struct s2n_test_iovecs {
-    struct iovec *iovecs;
-    size_t iovecs_count;
-};
-
-static S2N_CLEANUP_RESULT s2n_test_iovecs_free(struct s2n_test_iovecs *in)
-{
-    RESULT_ENSURE_REF(in);
-    for (size_t i = 0; i < in->iovecs_count; i++) {
-        RESULT_GUARD_POSIX(s2n_free_object((uint8_t **) &in->iovecs[i].iov_base,
-                in->iovecs[i].iov_len));
-    }
-    RESULT_GUARD_POSIX(s2n_free_object((uint8_t **) &in->iovecs,
-            sizeof(struct iovec) * in->iovecs_count));
-    return S2N_RESULT_OK;
-}
-
-/* Testing only with contiguous data could hide errors.
- * We should use iovecs where every buffer is allocated separately.
- */
-static S2N_RESULT s2n_test_split_data(struct s2n_test_iovecs *iovecs, struct s2n_blob *data)
-{
-    RESULT_ENSURE_REF(iovecs);
-    RESULT_ENSURE_REF(data);
-
-    struct s2n_stuffer in = { 0 };
-    RESULT_GUARD_POSIX(s2n_stuffer_init_written(&in, data));
-
-    for (size_t i = 0; i < iovecs->iovecs_count; i++) {
-        if (iovecs->iovecs[i].iov_len == 0) {
-            continue;
-        }
-        struct s2n_blob mem = { 0 };
-        RESULT_GUARD_POSIX(s2n_alloc(&mem, iovecs->iovecs[i].iov_len));
-        RESULT_GUARD_POSIX(s2n_stuffer_read(&in, &mem));
-        iovecs->iovecs[i].iov_base = mem.data;
-    }
-    RESULT_ENSURE_EQ(s2n_stuffer_data_available(&in), 0);
-    return S2N_RESULT_OK;
-}
-
-static S2N_RESULT s2n_test_new_iovecs(struct s2n_test_iovecs *iovecs,
-        struct s2n_blob *data, const size_t *lens, size_t lens_count)
-{
-    RESULT_ENSURE_REF(iovecs);
-    RESULT_ENSURE_REF(data);
-    RESULT_ENSURE_REF(lens);
-
-    size_t len_total = 0;
-    for (size_t i = 0; i < lens_count; i++) {
-        len_total += lens[i];
-    }
-    RESULT_ENSURE_LTE(len_total, data->size);
-
-    size_t iovecs_count = lens_count;
-    if (len_total < data->size) {
-        iovecs_count++;
-    }
-
-    struct s2n_blob iovecs_mem = { 0 };
-    RESULT_GUARD_POSIX(s2n_alloc(&iovecs_mem, sizeof(struct iovec) * iovecs_count));
-    RESULT_GUARD_POSIX(s2n_blob_zero(&iovecs_mem));
-    iovecs->iovecs = (struct iovec *) iovecs_mem.data;
-    iovecs->iovecs_count = iovecs_count;
-
-    for (size_t i = 0; i < lens_count; i++) {
-        iovecs->iovecs[i].iov_len = lens[i];
-    }
-    if (lens_count < iovecs_count) {
-        iovecs->iovecs[lens_count].iov_len = data->size - len_total;
-    }
-
-    RESULT_GUARD(s2n_test_split_data(iovecs, data));
-    return S2N_RESULT_OK;
-}
-
 int main(int argc, char **argv)
 {
     BEGIN_TEST();
diff --git a/tests/unit/s2n_self_talk_inet_socket_test.c b/tests/unit/s2n_self_talk_inet_socket_test.c
deleted file mode 100644
index 4399d617790..00000000000
--- a/tests/unit/s2n_self_talk_inet_socket_test.c
+++ /dev/null
@@ -1,204 +0,0 @@
-/*
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-#include <netinet/in.h>
-#include <sys/socket.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#include "s2n_test.h"
-#include "testlib/s2n_testlib.h"
-
-/* There are issues with MacOS and FreeBSD so we define the constant ourselves.
- * https://stackoverflow.com/a/34042435 */
-#define S2N_TEST_INADDR_LOOPBACK 0x7f000001 /* 127.0.0.1 */
-
-const char CHAR_A = 'a';
-const char CHAR_B = 'b';
-
-/* A collection of callbacks run during inet socket self talk tests */
-struct self_talk_inet_socket_callbacks {
-    S2N_RESULT (*server_post_handshake_cb)(struct s2n_connection *conn);
-    S2N_RESULT (*client_post_handshake_cb)(struct s2n_connection *conn);
-};
-
-S2N_RESULT s2n_noop_inet_socket_cb(struct s2n_connection *conn)
-{
-    return S2N_RESULT_OK;
-}
-
-static S2N_RESULT start_client(int fd, int read_pipe, const struct self_talk_inet_socket_callbacks *socket_cb)
-{
-    /* Setup connections */
-    DEFER_CLEANUP(struct s2n_connection *client_conn = s2n_connection_new(S2N_CLIENT),
-            s2n_connection_ptr_free);
-    DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free);
-
-    DEFER_CLEANUP(struct s2n_cert_chain_and_key * chain_and_key, s2n_cert_chain_and_key_ptr_free);
-    RESULT_GUARD_POSIX(s2n_test_cert_chain_and_key_new(&chain_and_key,
-            S2N_DEFAULT_TEST_CERT_CHAIN, S2N_DEFAULT_TEST_PRIVATE_KEY));
-
-    /* Setup config */
-    RESULT_GUARD_POSIX(s2n_connection_set_blinding(client_conn, S2N_SELF_SERVICE_BLINDING));
-    RESULT_GUARD_POSIX(s2n_connection_set_fd(client_conn, fd));
-    RESULT_GUARD_POSIX(s2n_config_set_cipher_preferences(config, "default"));
-    RESULT_GUARD_POSIX(s2n_config_set_unsafe_for_testing(config));
-    RESULT_GUARD_POSIX(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key));
-    RESULT_GUARD_POSIX(s2n_connection_set_config(client_conn, config));
-
-    /* Complete the handshake */
-    s2n_blocked_status blocked = S2N_NOT_BLOCKED;
-    RESULT_GUARD_POSIX(s2n_negotiate(client_conn, &blocked));
-    RESULT_ENSURE_EQ(client_conn->actual_protocol_version, S2N_TLS12);
-
-    RESULT_GUARD(socket_cb->client_post_handshake_cb(client_conn));
-
-    char sync = 0;
-    char recv_buffer[1] = { 0 };
-
-    RESULT_GUARD_POSIX(read(read_pipe, &sync, 1));
-    RESULT_GUARD_POSIX(s2n_recv(client_conn, recv_buffer, 1, &blocked));
-    RESULT_ENSURE_EQ(memcmp(&CHAR_A, &recv_buffer[0], 1), 0);
-
-    RESULT_GUARD_POSIX(read(read_pipe, &sync, 1));
-    RESULT_GUARD_POSIX(s2n_recv(client_conn, recv_buffer, 1, &blocked));
-    RESULT_ENSURE_EQ(memcmp(&CHAR_B, &recv_buffer[0], 1), 0);
-
-    return S2N_RESULT_OK;
-}
-
-static S2N_RESULT start_server(int fd, int write_pipe, const struct self_talk_inet_socket_callbacks *socket_cb)
-{
-    /* Setup connections */
-    DEFER_CLEANUP(struct s2n_connection *server_conn = s2n_connection_new(S2N_SERVER),
-            s2n_connection_ptr_free);
-    DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free);
-
-    DEFER_CLEANUP(struct s2n_cert_chain_and_key * chain_and_key, s2n_cert_chain_and_key_ptr_free);
-    RESULT_GUARD_POSIX(s2n_test_cert_chain_and_key_new(&chain_and_key,
-            S2N_DEFAULT_TEST_CERT_CHAIN, S2N_DEFAULT_TEST_PRIVATE_KEY));
-
-    /* Setup config */
-    RESULT_GUARD_POSIX(s2n_connection_set_blinding(server_conn, S2N_SELF_SERVICE_BLINDING));
-    RESULT_ENSURE_EQ(s2n_connection_get_delay(server_conn), 0);
-    RESULT_GUARD_POSIX(s2n_connection_set_fd(server_conn, fd));
-    RESULT_GUARD_POSIX(s2n_config_set_cipher_preferences(config, "default"));
-    RESULT_GUARD_POSIX(s2n_config_set_unsafe_for_testing(config));
-    RESULT_GUARD_POSIX(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key));
-    RESULT_GUARD_POSIX(s2n_connection_set_config(server_conn, config));
-
-    /* Complete the handshake */
-    s2n_blocked_status blocked = S2N_NOT_BLOCKED;
-    RESULT_GUARD_POSIX(s2n_negotiate(server_conn, &blocked));
-    RESULT_ENSURE_EQ(server_conn->actual_protocol_version, S2N_TLS12);
-
-    RESULT_GUARD(socket_cb->server_post_handshake_cb(server_conn));
-
-    char sync = 0;
-    char send_buffer[1] = { 0 };
-
-    send_buffer[0] = CHAR_A;
-    RESULT_GUARD_POSIX(s2n_send(server_conn, send_buffer, 1, &blocked));
-    RESULT_GUARD_POSIX(write(write_pipe, &sync, 1));
-
-    send_buffer[0] = CHAR_B;
-    RESULT_GUARD_POSIX(s2n_send(server_conn, send_buffer, 1, &blocked));
-    RESULT_GUARD_POSIX(write(write_pipe, &sync, 1));
-
-    return S2N_RESULT_OK;
-}
-
-static S2N_RESULT launch_test(const struct self_talk_inet_socket_callbacks *socket_cb)
-{
-    /* configure real socket */
-    int listener = socket(AF_INET, SOCK_STREAM, 0);
-    RESULT_GUARD_POSIX(listener);
-    struct sockaddr_in saddr = { 0 };
-    saddr.sin_family = AF_INET;
-    saddr.sin_addr.s_addr = htonl(S2N_TEST_INADDR_LOOPBACK);
-    saddr.sin_port = 0;
-
-    /* listen on socket address */
-    socklen_t addrlen = sizeof(saddr);
-    RESULT_GUARD_POSIX(bind(listener, (struct sockaddr *) &saddr, addrlen));
-    RESULT_GUARD_POSIX(getsockname(listener, (struct sockaddr *) &saddr, &addrlen));
-
-    /* Used for synchronizing read and writes between client and server.
-     *
-     * The purpose of this mechanism is to prevent the client from reading
-     * before the server has sent it. The client blocks on reading a single
-     * char while the server writes a char after it has sent data and wishes
-     * to unblock the client. */
-    int sync_pipe[2] = { 0 };
-    RESULT_GUARD_POSIX(pipe(sync_pipe));
-
-    pid_t child = fork();
-    RESULT_ENSURE(child >= 0, S2N_ERR_SAFETY);
-    int status = 0;
-    int fd = 0;
-    if (child) {
-        /* server */
-        RESULT_GUARD_POSIX(listen(listener, 1));
-        fd = accept(listener, NULL, NULL);
-        RESULT_GUARD_POSIX(fd);
-
-        RESULT_GUARD_POSIX(close(sync_pipe[0]));
-        RESULT_GUARD(start_server(fd, sync_pipe[1], socket_cb));
-
-        RESULT_ENSURE_EQ(waitpid(-1, &status, 0), child);
-        RESULT_ENSURE_EQ(status, 0);
-    } else {
-        /* client */
-        fd = socket(AF_INET, SOCK_STREAM, 0);
-        RESULT_GUARD_POSIX(fd);
-
-        /* wait for server to start up */
-        sleep(1);
-        RESULT_GUARD_POSIX(connect(fd, (struct sockaddr *) &saddr, addrlen));
-
-        RESULT_GUARD_POSIX(close(sync_pipe[1]));
-        RESULT_GUARD(start_client(fd, sync_pipe[0], socket_cb));
-        exit(0);
-    }
-
-    return S2N_RESULT_OK;
-}
-
-/* This test is unique compared to our other self talk tests because it's testing
- * AF_INET socket functionality.
- *
- * When enabled, kTLS offloads the TLS protocol to the socket. It's not possible to
- * enable kTLS on an AF_UNIX socket, which is what our other self talk tests use.
- * Instead we need to use AF_INET sockets to test kTLS functionality. The use of
- * AF_INET sockets also drives the need to `fork` and create two processes (client and
- * server) to establish a TLS connection.
- */
-int main(int argc, char **argv)
-{
-    BEGIN_TEST();
-
-    /* SIGPIPE is received when a process tries to write to a socket which
-     * has been shutdown. Ignore it and handle it gracefully. */
-    signal(SIGPIPE, SIG_IGN);
-
-    /* A regular connection */
-    const struct self_talk_inet_socket_callbacks noop_inet_cb = {
-        .server_post_handshake_cb = s2n_noop_inet_socket_cb,
-        .client_post_handshake_cb = s2n_noop_inet_socket_cb,
-    };
-    EXPECT_OK(launch_test(&noop_inet_cb));
-
-    END_TEST();
-}
diff --git a/tests/unit/s2n_self_talk_ktls_test.c b/tests/unit/s2n_self_talk_ktls_test.c
new file mode 100644
index 00000000000..45743ccb182
--- /dev/null
+++ b/tests/unit/s2n_self_talk_ktls_test.c
@@ -0,0 +1,271 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+#include <fcntl.h>
+#include <netinet/in.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "s2n_test.h"
+#include "testlib/s2n_testlib.h"
+#include "tls/s2n_ktls.h"
+#include "utils/s2n_random.h"
+
+/* There are issues with MacOS and FreeBSD so we define the constant ourselves.
+ * https://stackoverflow.com/a/34042435 */
+#define S2N_TEST_INADDR_LOOPBACK 0x7f000001 /* 127.0.0.1 */
+
+/* Unlike our other self-talk tests, this test cannot use AF_UNIX / AF_LOCAL.
+ * For a real self-talk test we need real kernel support for kTLS, and only
+ * AF_INET sockets support kTLS.
+ */
+static S2N_RESULT s2n_new_inet_socket_pair(struct s2n_test_io_pair *io_pair)
+{
+    RESULT_ENSURE_REF(io_pair);
+
+    int listener = socket(AF_INET, SOCK_STREAM, 0);
+    RESULT_ENSURE_GT(listener, 0);
+
+    struct sockaddr_in saddr = { 0 };
+    saddr.sin_family = AF_INET;
+    saddr.sin_addr.s_addr = htonl(S2N_TEST_INADDR_LOOPBACK);
+    saddr.sin_port = 0;
+
+    socklen_t addrlen = sizeof(saddr);
+    RESULT_ENSURE_EQ(bind(listener, (struct sockaddr *) &saddr, addrlen), 0);
+    RESULT_ENSURE_EQ(getsockname(listener, (struct sockaddr *) &saddr, &addrlen), 0);
+    RESULT_ENSURE_EQ(listen(listener, 1), 0);
+
+    io_pair->client = socket(AF_INET, SOCK_STREAM, 0);
+    RESULT_ENSURE_GT(io_pair->client, 0);
+
+    fflush(stdout);
+    pid_t pid = fork();
+    RESULT_ENSURE_GTE(pid, 0);
+    if (pid == 0) {
+        RESULT_ENSURE_EQ(connect(io_pair->client, (struct sockaddr *) &saddr, addrlen), 0);
+        ZERO_TO_DISABLE_DEFER_CLEANUP(io_pair);
+        exit(0);
+    }
+    io_pair->server = accept(listener, NULL, NULL);
+    RESULT_ENSURE_GT(io_pair->server, 0);
+    return S2N_RESULT_OK;
+}
+
+int main(int argc, char **argv)
+{
+    BEGIN_TEST();
+
+    /* ktls is complicated to enable. We should ensure that it's actually enabled
+     * where we think we're testing it.
+     */
+    bool ktls_expected = (getenv("S2N_KTLS_TESTING_EXPECTED") != NULL);
+
+    if (!s2n_ktls_is_supported_on_platform() && !ktls_expected) {
+        END_TEST();
+    }
+
+    const s2n_mode modes[] = { S2N_CLIENT, S2N_SERVER };
+
+    DEFER_CLEANUP(struct s2n_cert_chain_and_key *chain_and_key = NULL,
+            s2n_cert_chain_and_key_ptr_free);
+    EXPECT_SUCCESS(s2n_test_cert_chain_and_key_new(&chain_and_key,
+            S2N_DEFAULT_TEST_CERT_CHAIN, S2N_DEFAULT_TEST_PRIVATE_KEY));
+
+    uint8_t test_data[100] = { 0 };
+    struct s2n_blob test_data_blob = { 0 };
+    EXPECT_SUCCESS(s2n_blob_init(&test_data_blob, test_data, sizeof(test_data)));
+    EXPECT_OK(s2n_get_public_random_data(&test_data_blob));
+
+    DEFER_CLEANUP(struct s2n_test_iovecs test_iovecs = { 0 }, s2n_test_iovecs_free);
+    size_t test_iovecs_lens[20] = { 5, 6, 1, 10, 0 };
+    EXPECT_OK(s2n_test_new_iovecs(&test_iovecs, &test_data_blob, test_iovecs_lens,
+            s2n_array_len(test_iovecs_lens)));
+
+    const size_t test_offsets[] = {
+        0,
+        test_iovecs_lens[0],
+        test_iovecs_lens[0] + 1,
+        test_iovecs_lens[0] + test_iovecs_lens[1],
+        sizeof(test_data) - 1,
+        sizeof(test_data),
+    };
+
+    uint8_t file_test_data[100] = { 0 };
+    int file = open(argv[0], O_RDONLY);
+    EXPECT_TRUE(file > 0);
+    int file_read = pread(file, file_test_data, sizeof(file_test_data), 0);
+    EXPECT_EQUAL(file_read, sizeof(file_test_data));
+
+    DEFER_CLEANUP(struct s2n_config *config = s2n_config_new(), s2n_config_ptr_free);
+    EXPECT_SUCCESS(s2n_config_add_cert_chain_and_key_to_store(config, chain_and_key));
+    EXPECT_SUCCESS(s2n_config_set_unsafe_for_testing(config));
+    EXPECT_SUCCESS(s2n_config_set_cipher_preferences(config, "default"));
+
+    /* Test enabling ktls for sending */
+    {
+        DEFER_CLEANUP(struct s2n_connection *client = s2n_connection_new(S2N_CLIENT),
+                s2n_connection_ptr_free);
+        EXPECT_NOT_NULL(client);
+        EXPECT_SUCCESS(s2n_connection_set_config(client, config));
+
+        DEFER_CLEANUP(struct s2n_connection *server = s2n_connection_new(S2N_SERVER),
+                s2n_connection_ptr_free);
+        EXPECT_NOT_NULL(client);
+        EXPECT_SUCCESS(s2n_connection_set_config(server, config));
+
+        DEFER_CLEANUP(struct s2n_test_io_pair io_pair = { 0 }, s2n_io_pair_close);
+        if (s2n_result_is_error(s2n_new_inet_socket_pair(&io_pair))) {
+            /* We should be able to setup AF_INET sockets everywhere, but if
+             * we can't, don't block the build unless the build explicitly expects
+             * to be able to test ktls.
+             */
+            EXPECT_FALSE(ktls_expected);
+            END_TEST();
+        }
+        EXPECT_SUCCESS(s2n_connections_set_io_pair(client, server, &io_pair));
+
+        /* The test negotiate method assumes non-blocking sockets */
+        EXPECT_SUCCESS(s2n_fd_set_non_blocking(io_pair.server));
+        EXPECT_SUCCESS(s2n_fd_set_non_blocking(io_pair.client));
+        EXPECT_SUCCESS(s2n_negotiate_test_server_and_client(server, client));
+
+        if (s2n_connection_ktls_enable_send(client) != S2N_SUCCESS) {
+            /* Even if we detected ktls support at compile time, enabling ktls
+             * can fail at runtime if the system is not properly configured.
+             */
+            EXPECT_FALSE(ktls_expected);
+            END_TEST();
+        }
+        EXPECT_SUCCESS(s2n_connection_ktls_enable_send(server));
+    };
+
+    /* Test sending with ktls */
+    for (size_t mode_i = 0; mode_i < s2n_array_len(modes); mode_i++) {
+        const s2n_mode mode = modes[mode_i];
+
+        DEFER_CLEANUP(struct s2n_connection *client = s2n_connection_new(S2N_CLIENT),
+                s2n_connection_ptr_free);
+        EXPECT_NOT_NULL(client);
+        EXPECT_SUCCESS(s2n_connection_set_config(client, config));
+
+        DEFER_CLEANUP(struct s2n_connection *server = s2n_connection_new(S2N_SERVER),
+                s2n_connection_ptr_free);
+        EXPECT_NOT_NULL(server);
+        EXPECT_SUCCESS(s2n_connection_set_config(server, config));
+
+        DEFER_CLEANUP(struct s2n_test_io_pair io_pair = { 0 }, s2n_io_pair_close);
+        EXPECT_OK(s2n_new_inet_socket_pair(&io_pair));
+        EXPECT_SUCCESS(s2n_connections_set_io_pair(client, server, &io_pair));
+
+        /* The test negotiate method assumes non-blocking sockets */
+        EXPECT_SUCCESS(s2n_fd_set_non_blocking(io_pair.server));
+        EXPECT_SUCCESS(s2n_fd_set_non_blocking(io_pair.client));
+        EXPECT_SUCCESS(s2n_negotiate_test_server_and_client(server, client));
+
+        struct s2n_connection *conns[] = {
+            [S2N_CLIENT] = client,
+            [S2N_SERVER] = server,
+        };
+        struct s2n_connection *writer = conns[mode];
+        struct s2n_connection *reader = conns[S2N_PEER_MODE(mode)];
+        EXPECT_SUCCESS(s2n_connection_ktls_enable_send(writer));
+
+        s2n_blocked_status blocked = S2N_NOT_BLOCKED;
+
+        /* Our IO methods are more predictable if they use blocking sockets. */
+        EXPECT_SUCCESS(s2n_fd_set_blocking(io_pair.server));
+        EXPECT_SUCCESS(s2n_fd_set_blocking(io_pair.client));
+
+        /* Test: s2n_send */
+        for (size_t i = 0; i < 5; i++) {
+            int written = s2n_send(writer, test_data, sizeof(test_data), &blocked);
+            EXPECT_EQUAL(written, sizeof(test_data));
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            uint8_t buffer[sizeof(test_data)] = { 0 };
+            int read = s2n_recv(reader, buffer, sizeof(buffer), &blocked);
+            EXPECT_EQUAL(read, sizeof(test_data));
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            EXPECT_BYTEARRAY_EQUAL(test_data, buffer, read);
+        }
+
+        /* Test: s2n_sendv */
+        for (size_t i = 0; i < 5; i++) {
+            int written = s2n_sendv(writer,
+                    test_iovecs.iovecs, test_iovecs.iovecs_count, &blocked);
+            EXPECT_EQUAL(written, sizeof(test_data));
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            uint8_t buffer[sizeof(test_data)] = { 0 };
+            int read = s2n_recv(reader, buffer, sizeof(buffer), &blocked);
+            EXPECT_EQUAL(read, sizeof(test_data));
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            EXPECT_BYTEARRAY_EQUAL(test_data, buffer, read);
+        }
+
+        /* Test: s2n_sendv_with_offset */
+        for (size_t offset_i = 0; offset_i < s2n_array_len(test_offsets); offset_i++) {
+            const size_t offset = test_offsets[offset_i];
+            const size_t expected_written = sizeof(test_data) - offset;
+
+            int written = s2n_sendv_with_offset(writer,
+                    test_iovecs.iovecs, test_iovecs.iovecs_count, offset, &blocked);
+            EXPECT_EQUAL(written, expected_written);
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            uint8_t buffer[sizeof(test_data)] = { 0 };
+            int read = s2n_recv(reader, buffer, expected_written, &blocked);
+            EXPECT_EQUAL(read, expected_written);
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            EXPECT_BYTEARRAY_EQUAL(test_data + offset, buffer, read);
+        };
+
+        /* Test: s2n_sendfile */
+        for (size_t offset_i = 0; offset_i < s2n_array_len(test_offsets); offset_i++) {
+            const size_t offset = test_offsets[offset_i];
+            const size_t expected_written = sizeof(test_data) - offset;
+
+            size_t written = 0;
+            EXPECT_SUCCESS(s2n_sendfile(writer, file, offset, expected_written,
+                    &written, &blocked));
+            EXPECT_EQUAL(written, expected_written);
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            uint8_t buffer[sizeof(file_test_data)] = { 0 };
+            int read = s2n_recv(reader, buffer, expected_written, &blocked);
+            EXPECT_EQUAL(read, expected_written);
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            EXPECT_BYTEARRAY_EQUAL(file_test_data + offset, buffer, read);
+        }
+
+        /* Test: s2n_shutdown */
+        {
+            EXPECT_SUCCESS(s2n_shutdown_send(writer, &blocked));
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+
+            EXPECT_SUCCESS(s2n_shutdown(reader, &blocked));
+            EXPECT_EQUAL(blocked, S2N_NOT_BLOCKED);
+        };
+    };
+
+    END_TEST();
+}