Commit 90afdb14ed for openssl.org

commit 90afdb14ed35910f3311f08fa1f4301340164e4d
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Fri Jun 12 13:52:16 2026 +0200

    Add BIO bss_conn test

    Assisted-by: Claude:claude-opus-4-8

    Reviewed-by: Richard Levitte <levitte@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    MergeDate: Thu Jul  9 17:39:19 2026
    (Merged from https://github.com/openssl/openssl/pull/30788)

diff --git a/crypto/bio/bio_local.h b/crypto/bio/bio_local.h
index 36f72f5df1..d272eb1861 100644
--- a/crypto/bio/bio_local.h
+++ b/crypto/bio/bio_local.h
@@ -122,6 +122,46 @@ struct bio_st {
 };

 #ifndef OPENSSL_NO_SOCK
+
+typedef struct bio_connect_st {
+    int state;
+    int connect_family;
+    int connect_sock_type;
+    char *param_hostname;
+    char *param_service;
+    int connect_mode;
+#ifndef OPENSSL_NO_KTLS
+    unsigned char record_type;
+#endif
+    int tfo_first;
+
+    BIO_ADDRINFO *addr_first;
+    const BIO_ADDRINFO *addr_iter;
+    /*
+     * int socket; this will be kept in bio->num so that it is compatible
+     * with the bss_sock bio
+     */
+    /*
+     * called when the connection is initially made callback(BIO,state,ret);
+     * The callback should return 'ret'.  state is for compatibility with the
+     * ssl info_callback
+     */
+    BIO_info_cb *info_callback;
+    /*
+     * Used when connect_sock_type is SOCK_DGRAM. Owned by us; we forward
+     * read/write(mmsg) calls to this if present.
+     */
+    BIO *dgram_bio;
+} BIO_CONNECT;
+
+#define BIO_CONN_S_BEFORE 1
+#define BIO_CONN_S_GET_ADDR 2
+#define BIO_CONN_S_CREATE_SOCKET 3
+#define BIO_CONN_S_CONNECT 4
+#define BIO_CONN_S_OK 5
+#define BIO_CONN_S_BLOCKED_CONNECT 6
+#define BIO_CONN_S_CONNECT_ERROR 7
+
 #ifdef OPENSSL_SYS_VMS
 typedef unsigned int socklen_t;
 #endif
diff --git a/crypto/bio/bss_conn.c b/crypto/bio/bss_conn.c
index 0e84e959d8..c4355392e4 100644
--- a/crypto/bio/bss_conn.c
+++ b/crypto/bio/bss_conn.c
@@ -16,37 +16,6 @@

 #ifndef OPENSSL_NO_SOCK

-typedef struct bio_connect_st {
-    int state;
-    int connect_family;
-    int connect_sock_type;
-    char *param_hostname;
-    char *param_service;
-    int connect_mode;
-#ifndef OPENSSL_NO_KTLS
-    unsigned char record_type;
-#endif
-    int tfo_first;
-
-    BIO_ADDRINFO *addr_first;
-    const BIO_ADDRINFO *addr_iter;
-    /*
-     * int socket; this will be kept in bio->num so that it is compatible
-     * with the bss_sock bio
-     */
-    /*
-     * called when the connection is initially made callback(BIO,state,ret);
-     * The callback should return 'ret'.  state is for compatibility with the
-     * ssl info_callback
-     */
-    BIO_info_cb *info_callback;
-    /*
-     * Used when connect_sock_type is SOCK_DGRAM. Owned by us; we forward
-     * read/write(mmsg) calls to this if present.
-     */
-    BIO *dgram_bio;
-} BIO_CONNECT;
-
 static int conn_write(BIO *h, const char *buf, int num);
 static int conn_read(BIO *h, char *buf, int size);
 static int conn_puts(BIO *h, const char *str);
@@ -65,14 +34,6 @@ static void conn_close_socket(BIO *data);
 static BIO_CONNECT *BIO_CONNECT_new(void);
 static void BIO_CONNECT_free(BIO_CONNECT *a);

-#define BIO_CONN_S_BEFORE 1
-#define BIO_CONN_S_GET_ADDR 2
-#define BIO_CONN_S_CREATE_SOCKET 3
-#define BIO_CONN_S_CONNECT 4
-#define BIO_CONN_S_OK 5
-#define BIO_CONN_S_BLOCKED_CONNECT 6
-#define BIO_CONN_S_CONNECT_ERROR 7
-
 static const BIO_METHOD methods_connectp = {
     BIO_TYPE_CONNECT,
     "socket connect",
diff --git a/test/unit/build.info b/test/unit/build.info
index befcc67279..cf2a748418 100644
--- a/test/unit/build.info
+++ b/test/unit/build.info
@@ -6,6 +6,14 @@ INCLUDE[crypto/bio/test_bio_addr]=../../include ../../crypto/bio
 DEPEND[crypto/bio/test_bio_addr]=../../libcrypto.a
 WRAP[crypto/bio/test_bio_addr]=BIO_sock_init getnameinfo freeaddrinfo

+PROGRAMS{noinst}=crypto/bio/test_bss_conn
+SOURCE[crypto/bio/test_bss_conn]=crypto/bio/test_bss_conn.c
+WRAP[crypto/bio/test_bss_conn]=BIO_lookup BIO_socket BIO_connect \
+    BIO_sock_should_retry BIO_closesocket BIO_socket_wait BIO_sock_error \
+    read write
+INCLUDE[crypto/bio/test_bss_conn]=../../include ../../crypto/bio
+DEPEND[crypto/bio/test_bss_conn]=../../libcrypto.a
+
 PROGRAMS{noinst}=crypto/bio/test_bss_fd
 SOURCE[crypto/bio/test_bss_fd]=crypto/bio/test_bss_fd.c
 INCLUDE[crypto/bio/test_bss_fd]=../../include ../../crypto/bio
diff --git a/test/unit/crypto/bio/test_bss_conn.c b/test/unit/crypto/bio/test_bss_conn.c
new file mode 100644
index 0000000000..992a1b781a
--- /dev/null
+++ b/test/unit/crypto/bio/test_bss_conn.c
@@ -0,0 +1,1590 @@
+/*
+ * Copyright 2026 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 "internal/sockets.h"
+
+#ifndef OPENSSL_NO_SOCK
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <string.h>
+#include <errno.h>
+#include <time.h>
+#include <netinet/in.h>
+#include <cmocka.h>
+#include "bio_local.h"
+
+#define FAKE_SOCKET 42
+
+/*
+ * Fake addrinfo used across state machine tests.  g_addrinfo1.bai_next is
+ * NULL by default.  Tests needing a second address temporarily set it to
+ * &g_addrinfo2 and restore it afterwards.
+ */
+static struct sockaddr_in g_sin;
+static BIO_ADDRINFO g_addrinfo1;
+static BIO_ADDRINFO g_addrinfo2;
+
+/* prototypes for __wrap_* (required by -Wmissing-prototypes) */
+ssize_t __wrap_read(int fd, void *buf, size_t count);
+ssize_t __wrap_write(int fd, const void *buf, size_t count);
+int __wrap_BIO_lookup(const char *host, const char *service,
+    enum BIO_lookup_type lookup_type,
+    int family, int socktype, BIO_ADDRINFO **res);
+int __wrap_BIO_socket(int domain, int socktype, int protocol, int options);
+int __wrap_BIO_connect(int sock, const BIO_ADDR *addr, int options);
+int __wrap_BIO_sock_should_retry(int i);
+int __wrap_BIO_closesocket(int sock);
+int __wrap_BIO_socket_wait(int fd, int for_write, time_t max_time);
+int __wrap_BIO_sock_error(int sock);
+
+/* wraps */
+
+ssize_t __wrap_read(int fd, void *buf, size_t count)
+{
+    function_called();
+    check_expected(fd);
+    check_expected_ptr(buf);
+    check_expected(count);
+    return mock_type(ssize_t);
+}
+
+ssize_t __wrap_write(int fd, const void *buf, size_t count)
+{
+    function_called();
+    check_expected(fd);
+    check_expected_ptr(buf);
+    check_expected(count);
+    return mock_type(ssize_t);
+}
+
+int __wrap_BIO_lookup(const char *host, const char *service,
+    enum BIO_lookup_type lookup_type,
+    int family, int socktype, BIO_ADDRINFO **res)
+{
+    int rc;
+
+    function_called();
+    check_expected(host);
+    check_expected(service);
+    check_expected(lookup_type);
+    check_expected(family);
+    check_expected(socktype);
+    rc = mock_type(int);
+    if (rc == 1)
+        *res = mock_ptr_type(BIO_ADDRINFO *);
+    return rc;
+}
+
+int __wrap_BIO_socket(int domain, int socktype, int protocol, int options)
+{
+    function_called();
+    check_expected(domain);
+    check_expected(socktype);
+    check_expected(protocol);
+    check_expected(options);
+    return mock_type(int);
+}
+
+int __wrap_BIO_connect(int sock, const BIO_ADDR *addr, int options)
+{
+    function_called();
+    check_expected(sock);
+    check_expected_ptr(addr);
+    check_expected(options);
+    return mock_type(int);
+}
+
+int __wrap_BIO_sock_should_retry(int i)
+{
+    function_called();
+    check_expected(i);
+    return mock_type(int);
+}
+
+int __wrap_BIO_closesocket(int sock)
+{
+    function_called();
+    check_expected(sock);
+    return mock_type(int);
+}
+
+int __wrap_BIO_socket_wait(int fd, int for_write, time_t max_time)
+{
+    function_called();
+    check_expected(fd);
+    check_expected(for_write);
+    (void)max_time; /* derived from time(NULL): not checked */
+    return mock_type(int);
+}
+
+int __wrap_BIO_sock_error(int sock)
+{
+    function_called();
+    check_expected(sock);
+    return mock_type(int);
+}
+
+/*
+ * A minimal fake dgram BIO.  conn_read/conn_write/conn_sendmmsg/conn_recvmmsg
+ * delegate to the public BIO_* calls on data->dgram_bio, so the leaf needs
+ * read/write/sendmmsg/recvmmsg to dispatch.  Everything the connect layer
+ * forwards (the buffer, message array, stride, count and flags) is verified.
+ */
+
+static int fake_dgram_read(BIO *b, char *buf, size_t size, size_t *readbytes);
+static int fake_dgram_write(BIO *b, const char *buf, size_t size,
+    size_t *written);
+static long fake_dgram_ctrl(BIO *b, int cmd, long arg1, void *arg2);
+static int fake_dgram_sendmmsg(BIO *b, BIO_MSG *m, size_t s, size_t n,
+    uint64_t f, size_t *mp);
+static int fake_dgram_recvmmsg(BIO *b, BIO_MSG *m, size_t s, size_t n,
+    uint64_t f, size_t *mp);
+
+static int fake_dgram_read(BIO *b, char *buf, size_t size, size_t *readbytes)
+{
+    int ret;
+
+    (void)b;
+    function_called();
+    check_expected_ptr(buf);
+    check_expected(size);
+    ret = mock_type(int);
+    if (ret > 0) {
+        *readbytes = (size_t)ret;
+        return 1;
+    }
+    *readbytes = 0;
+    return ret;
+}
+
+static int fake_dgram_write(BIO *b, const char *buf, size_t size,
+    size_t *written)
+{
+    int ret;
+
+    (void)b;
+    function_called();
+    check_expected_ptr(buf);
+    check_expected(size);
+    ret = mock_type(int);
+    if (ret > 0) {
+        *written = (size_t)ret;
+        return 1;
+    }
+    *written = 0;
+    return ret;
+}
+
+static long fake_dgram_ctrl(BIO *b, int cmd, long arg1, void *arg2)
+{
+    (void)b;
+    (void)arg1;
+    (void)arg2;
+    if (cmd == BIO_CTRL_FLUSH)
+        return 1;
+    return 0;
+}
+
+static int fake_dgram_sendmmsg(BIO *b, BIO_MSG *m, size_t s, size_t n,
+    uint64_t f, size_t *mp)
+{
+    (void)b;
+    function_called();
+    check_expected_ptr(m);
+    check_expected(s);
+    check_expected(n);
+    check_expected(f);
+    *mp = mock_type(size_t);
+    return mock_type(int);
+}
+
+static int fake_dgram_recvmmsg(BIO *b, BIO_MSG *m, size_t s, size_t n,
+    uint64_t f, size_t *mp)
+{
+    (void)b;
+    function_called();
+    check_expected_ptr(m);
+    check_expected(s);
+    check_expected(n);
+    check_expected(f);
+    *mp = mock_type(size_t);
+    return mock_type(int);
+}
+
+static BIO_METHOD *fake_dgram_method = NULL;
+
+static BIO_METHOD *make_fake_dgram_method(void)
+{
+    BIO_METHOD *m = BIO_meth_new(BIO_TYPE_DGRAM | 0xff, "fake dgram");
+
+    assert_non_null(m);
+    assert_true(BIO_meth_set_read_ex(m, fake_dgram_read));
+    assert_true(BIO_meth_set_write_ex(m, fake_dgram_write));
+    assert_true(BIO_meth_set_ctrl(m, fake_dgram_ctrl));
+    assert_true(BIO_meth_set_sendmmsg(m, fake_dgram_sendmmsg));
+    assert_true(BIO_meth_set_recvmmsg(m, fake_dgram_recvmmsg));
+    return m;
+}
+
+static BIO *make_fake_dgram(void)
+{
+    BIO *d = BIO_new(fake_dgram_method);
+
+    assert_non_null(d);
+    BIO_set_init(d, 1);
+    return d;
+}
+
+/* expectations */
+
+static void expect_read(int fd, const void *buf, size_t count, ssize_t rc)
+{
+    expect_function_call(__wrap_read);
+    expect_value(__wrap_read, fd, fd);
+    expect_value(__wrap_read, buf, buf);
+    expect_value(__wrap_read, count, count);
+    will_return(__wrap_read, rc);
+}
+
+static void expect_write(int fd, const void *buf, size_t count, ssize_t rc)
+{
+    expect_function_call(__wrap_write);
+    expect_value(__wrap_write, fd, fd);
+    expect_value(__wrap_write, buf, buf);
+    expect_value(__wrap_write, count, count);
+    will_return(__wrap_write, rc);
+}
+
+static void expect_BIO_lookup(BIO_ADDRINFO *res, int rc)
+{
+    expect_function_call(__wrap_BIO_lookup);
+    expect_any(__wrap_BIO_lookup, host);
+    expect_any(__wrap_BIO_lookup, service);
+    expect_any(__wrap_BIO_lookup, lookup_type);
+    expect_any(__wrap_BIO_lookup, family);
+    expect_any(__wrap_BIO_lookup, socktype);
+    will_return(__wrap_BIO_lookup, rc);
+    if (rc == 1)
+        will_return(__wrap_BIO_lookup, res);
+}
+
+/* options is the literal the state machine passes (always 0 here) */
+static void expect_BIO_socket(int domain, int socktype, int protocol,
+    int options, int rc)
+{
+    expect_function_call(__wrap_BIO_socket);
+    expect_value(__wrap_BIO_socket, domain, domain);
+    expect_value(__wrap_BIO_socket, socktype, socktype);
+    expect_value(__wrap_BIO_socket, protocol, protocol);
+    expect_value(__wrap_BIO_socket, options, options);
+    will_return(__wrap_BIO_socket, rc);
+}
+
+static void expect_BIO_connect(int sock, const BIO_ADDR *addr, int options,
+    int rc)
+{
+    expect_function_call(__wrap_BIO_connect);
+    expect_value(__wrap_BIO_connect, sock, sock);
+    expect_value(__wrap_BIO_connect, addr, addr);
+    expect_value(__wrap_BIO_connect, options, options);
+    will_return(__wrap_BIO_connect, rc);
+}
+
+static void expect_BIO_sock_should_retry(int i, int rc)
+{
+    expect_function_call(__wrap_BIO_sock_should_retry);
+    expect_value(__wrap_BIO_sock_should_retry, i, i);
+    will_return(__wrap_BIO_sock_should_retry, rc);
+}
+
+static void expect_BIO_closesocket(int sock, int rc)
+{
+    expect_function_call(__wrap_BIO_closesocket);
+    expect_value(__wrap_BIO_closesocket, sock, sock);
+    will_return(__wrap_BIO_closesocket, rc);
+}
+
+static void expect_BIO_socket_wait(int fd, int for_write, int rc)
+{
+    expect_function_call(__wrap_BIO_socket_wait);
+    expect_value(__wrap_BIO_socket_wait, fd, fd);
+    expect_value(__wrap_BIO_socket_wait, for_write, for_write);
+    will_return(__wrap_BIO_socket_wait, rc);
+}
+
+static void expect_BIO_sock_error(int sock, int rc)
+{
+    expect_function_call(__wrap_BIO_sock_error);
+    expect_value(__wrap_BIO_sock_error, sock, sock);
+    will_return(__wrap_BIO_sock_error, rc);
+}
+
+static void expect_fake_dgram_read(const void *buf, size_t size, int rc)
+{
+    expect_function_call(fake_dgram_read);
+    expect_value(fake_dgram_read, buf, buf);
+    expect_value(fake_dgram_read, size, size);
+    will_return(fake_dgram_read, rc);
+}
+
+static void expect_fake_dgram_write(const void *buf, size_t size, int rc)
+{
+    expect_function_call(fake_dgram_write);
+    expect_value(fake_dgram_write, buf, buf);
+    expect_value(fake_dgram_write, size, size);
+    will_return(fake_dgram_write, rc);
+}
+
+static void expect_fake_dgram_sendmmsg(const BIO_MSG *m, size_t s, size_t n,
+    uint64_t f, size_t processed, int rc)
+{
+    expect_function_call(fake_dgram_sendmmsg);
+    expect_value(fake_dgram_sendmmsg, m, m);
+    expect_value(fake_dgram_sendmmsg, s, s);
+    expect_value(fake_dgram_sendmmsg, n, n);
+    expect_value(fake_dgram_sendmmsg, f, f);
+    will_return(fake_dgram_sendmmsg, processed);
+    will_return(fake_dgram_sendmmsg, rc);
+}
+
+static void expect_fake_dgram_recvmmsg(const BIO_MSG *m, size_t s, size_t n,
+    uint64_t f, size_t processed, int rc)
+{
+    expect_function_call(fake_dgram_recvmmsg);
+    expect_value(fake_dgram_recvmmsg, m, m);
+    expect_value(fake_dgram_recvmmsg, s, s);
+    expect_value(fake_dgram_recvmmsg, n, n);
+    expect_value(fake_dgram_recvmmsg, f, f);
+    will_return(fake_dgram_recvmmsg, processed);
+    will_return(fake_dgram_recvmmsg, rc);
+}
+
+/* helpers */
+
+static BIO_CONNECT *get_data(BIO *bio)
+{
+    return (BIO_CONNECT *)bio->ptr;
+}
+
+/*
+ * Call at the end of any test that sets bio->num or addr_first to prevent
+ * unexpected BIO_closesocket or BIO_ADDRINFO_free invocations in teardown.
+ */
+static void reset_for_teardown(BIO *bio)
+{
+    BIO_CONNECT *data = get_data(bio);
+
+    bio->num = (int)INVALID_SOCKET;
+    data->addr_first = NULL;
+    data->addr_iter = NULL;
+    data->state = BIO_CONN_S_BEFORE;
+}
+
+/* setup / teardown */
+
+static int setup(void **state)
+{
+    BIO *bio = BIO_new(BIO_s_connect());
+
+    assert_non_null(bio);
+    *state = bio;
+    return 0;
+}
+
+static int teardown(void **state)
+{
+    if (*state != NULL)
+        BIO_free(*state);
+    return 0;
+}
+
+/* I/O tests pre-establish state=OK so the state machine is not entered. */
+static int setup_io(void **state)
+{
+    BIO *bio;
+    BIO_CONNECT *data;
+
+    if (setup(state) != 0)
+        return -1;
+    bio = *state;
+    data = get_data(bio);
+    data->state = BIO_CONN_S_OK;
+    bio->num = FAKE_SOCKET;
+    bio->init = 1;
+    return 0;
+}
+
+static int teardown_io(void **state)
+{
+    if (*state != NULL)
+        reset_for_teardown(*state);
+    return teardown(state);
+}
+
+static int group_setup(void **state)
+{
+    (void)state;
+
+    fake_dgram_method = make_fake_dgram_method();
+
+    memset(&g_sin, 0, sizeof(g_sin));
+    g_sin.sin_family = AF_INET;
+    g_sin.sin_port = htons(443);
+    g_sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+    memset(&g_addrinfo1, 0, sizeof(g_addrinfo1));
+    g_addrinfo1.bai_family = AF_INET;
+    g_addrinfo1.bai_socktype = SOCK_STREAM;
+    g_addrinfo1.bai_protocol = IPPROTO_TCP;
+    g_addrinfo1.bai_addrlen = sizeof(g_sin);
+    g_addrinfo1.bai_addr = (struct sockaddr *)&g_sin;
+    g_addrinfo1.bai_next = NULL;
+
+    memcpy(&g_addrinfo2, &g_addrinfo1, sizeof(g_addrinfo1));
+    g_addrinfo2.bai_next = NULL;
+
+    return 0;
+}
+
+static int group_teardown(void **state)
+{
+    (void)state;
+    BIO_meth_free(fake_dgram_method);
+    fake_dgram_method = NULL;
+    return 0;
+}
+
+/* conn_new */
+
+static void test_conn_new(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    assert_non_null(data);
+    assert_int_equal(data->state, BIO_CONN_S_BEFORE);
+    assert_int_equal(data->connect_family, BIO_FAMILY_IPANY);
+    assert_int_equal(data->connect_sock_type, SOCK_STREAM);
+    assert_null(data->param_hostname);
+    assert_null(data->param_service);
+    assert_null(data->addr_first);
+    assert_null(data->dgram_bio);
+    assert_int_equal(bio->num, (int)INVALID_SOCKET);
+    assert_int_equal(bio->init, 0);
+}
+
+/* conn_free */
+
+static void test_conn_free_no_shutdown(void **state)
+{
+    /* shutdown=0: conn_close_socket and BIO_CONNECT_free are both skipped */
+    BIO *bio = BIO_new(BIO_s_connect());
+
+    assert_non_null(bio);
+    bio->shutdown = BIO_NOCLOSE;
+    BIO_free(bio);
+    *state = NULL;
+}
+
+/* conn_close_socket (via BIO_CTRL_RESET) */
+
+static void test_close_socket_none(void **state)
+{
+    /* bio->num == INVALID_SOCKET: no calls expected */
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_RESET, 0, NULL), 0);
+}
+
+static void test_close_socket_non_ok_state(void **state)
+{
+    /* Socket open but state != OK: BIO_closesocket called, no shutdown */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    bio->num = FAKE_SOCKET;
+    data->state = BIO_CONN_S_BLOCKED_CONNECT;
+
+    expect_BIO_closesocket(FAKE_SOCKET, 0);
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_RESET, 0, NULL), 0);
+    assert_int_equal(bio->num, (int)INVALID_SOCKET);
+    assert_int_equal(data->state, BIO_CONN_S_BEFORE);
+}
+
+static void test_close_socket_ok_state(void **state)
+{
+    /* Socket open and state=OK: shutdown first, then BIO_closesocket */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    bio->num = FAKE_SOCKET;
+    data->state = BIO_CONN_S_OK;
+
+    expect_BIO_closesocket(FAKE_SOCKET, 0);
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_RESET, 0, NULL), 0);
+    assert_int_equal(bio->num, (int)INVALID_SOCKET);
+    assert_int_equal(data->state, BIO_CONN_S_BEFORE);
+}
+
+/* conn_state (via BIO_C_DO_STATE_MACHINE) */
+
+static void test_conn_state_no_hostname(void **state)
+{
+    /* BEFORE with no hostname and no service */
+    assert_true(BIO_ctrl(*state, BIO_C_DO_STATE_MACHINE, 0, NULL) <= 0);
+}
+
+static void test_conn_state_unsupported_family(void **state)
+{
+    /* BEFORE -> GET_ADDR -> unrecognised connect_family -> error */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+    data->connect_family = 9999;
+
+    assert_true(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL) <= 0);
+}
+
+static void test_conn_state_lookup_fails(void **state)
+{
+    /* BEFORE -> GET_ADDR -> BIO_lookup returns 0 */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+
+    expect_BIO_lookup(NULL, 0);
+    assert_true(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL) <= 0);
+}
+
+static void test_conn_state_socket_fails(void **state)
+{
+    /* Pre-set CREATE_SOCKET: BIO_socket returns INVALID_SOCKET */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->state = BIO_CONN_S_CREATE_SOCKET;
+    data->addr_iter = &g_addrinfo1;
+
+    expect_BIO_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0,
+        (int)INVALID_SOCKET);
+    assert_true(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL) <= 0);
+
+    data->addr_iter = NULL;
+    data->state = BIO_CONN_S_BEFORE;
+}
+
+static void test_conn_state_connect_succeeds(void **state)
+{
+    /* Full happy path: BEFORE -> GET_ADDR -> CREATE_SOCKET -> CONNECT -> OK */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+
+    expect_BIO_lookup(&g_addrinfo1, 1);
+    expect_BIO_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, FAKE_SOCKET);
+    /* STREAM addrinfo adds KEEPALIVE to the default (zero) connect_mode */
+    expect_BIO_connect(FAKE_SOCKET, (BIO_ADDR *)&g_sin, BIO_SOCK_KEEPALIVE, 1);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 1);
+    assert_int_equal(data->state, BIO_CONN_S_OK);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_state_already_ok(void **state)
+{
+    /* State already OK: returns 1 with no external calls */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->state = BIO_CONN_S_OK;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 1);
+    data->state = BIO_CONN_S_BEFORE;
+}
+
+static void test_conn_state_connect_retry(void **state)
+{
+    /* Pre-set CONNECT: BIO_connect fails with retry -> BLOCKED_CONNECT */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->state = BIO_CONN_S_CONNECT;
+    data->addr_iter = &g_addrinfo1;
+    bio->num = FAKE_SOCKET;
+
+    expect_BIO_connect(FAKE_SOCKET, (BIO_ADDR *)&g_sin, BIO_SOCK_KEEPALIVE, 0);
+    expect_BIO_sock_should_retry(0, 1);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 0);
+    assert_int_equal(data->state, BIO_CONN_S_BLOCKED_CONNECT);
+    assert_int_equal(bio->retry_reason, BIO_RR_CONNECT);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_state_connect_error(void **state)
+{
+    /* Pre-set CONNECT: fails, no retry, no more addresses -> CONNECT_ERROR */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->state = BIO_CONN_S_CONNECT;
+    data->addr_iter = &g_addrinfo1; /* bai_next == NULL */
+    bio->num = FAKE_SOCKET;
+
+    expect_BIO_connect(FAKE_SOCKET, (BIO_ADDR *)&g_sin, BIO_SOCK_KEEPALIVE, 0);
+    expect_BIO_sock_should_retry(0, 0);
+    /* loop continues to CONNECT_ERROR which exits immediately */
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 0);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_state_connect_next_addr(void **state)
+{
+    /*
+     * Pre-set CONNECT with two addresses: first connect fails, iterator
+     * advances, second CREATE_SOCKET fails so we get a clean exit.
+     */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    g_addrinfo1.bai_next = &g_addrinfo2;
+    data->state = BIO_CONN_S_CONNECT;
+    data->addr_iter = &g_addrinfo1;
+    bio->num = FAKE_SOCKET;
+
+    expect_BIO_connect(FAKE_SOCKET, (BIO_ADDR *)&g_sin, BIO_SOCK_KEEPALIVE, 0);
+    expect_BIO_sock_should_retry(0, 0);
+    expect_BIO_closesocket(FAKE_SOCKET, 0);
+    /* CREATE_SOCKET for g_addrinfo2 */
+    expect_BIO_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0,
+        (int)INVALID_SOCKET);
+
+    assert_true(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL) <= 0);
+
+    g_addrinfo1.bai_next = NULL;
+    reset_for_teardown(bio);
+}
+
+static void test_conn_state_blocked_ok(void **state)
+{
+    /* Pre-set BLOCKED_CONNECT: socket becomes writable, no error -> OK */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->state = BIO_CONN_S_BLOCKED_CONNECT;
+    data->addr_iter = &g_addrinfo1;
+    bio->num = FAKE_SOCKET;
+
+    expect_BIO_socket_wait(FAKE_SOCKET, 0, 1);
+    expect_BIO_sock_error(FAKE_SOCKET, 0);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 1);
+    assert_int_equal(data->state, BIO_CONN_S_OK);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_state_blocked_error(void **state)
+{
+    /* Pre-set BLOCKED_CONNECT: socket error, no more addresses -> error */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->state = BIO_CONN_S_BLOCKED_CONNECT;
+    data->addr_iter = &g_addrinfo1; /* bai_next == NULL */
+    bio->num = FAKE_SOCKET;
+
+    expect_BIO_socket_wait(FAKE_SOCKET, 0, 1);
+    expect_BIO_sock_error(FAKE_SOCKET, ECONNREFUSED);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 0);
+
+    reset_for_teardown(bio);
+}
+
+#ifndef OPENSSL_NO_DGRAM
+static void test_conn_state_connect_dgram_ok(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->connect_sock_type = SOCK_DGRAM;
+    data->state = BIO_CONN_S_CONNECT;
+    data->addr_iter = &g_addrinfo1;
+    bio->num = FAKE_SOCKET;
+
+    /* DGRAM addrinfo means opts stays at the default connect_mode (0) */
+    g_addrinfo1.bai_socktype = SOCK_DGRAM;
+
+    expect_BIO_connect(FAKE_SOCKET, (BIO_ADDR *)&g_sin, 0, 1);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_DO_STATE_MACHINE, 0, NULL), 1);
+    assert_int_equal(data->state, BIO_CONN_S_OK);
+    assert_non_null(data->dgram_bio);
+
+    g_addrinfo1.bai_socktype = SOCK_STREAM;
+
+    assert_int_equal(BIO_set_close(data->dgram_bio, BIO_NOCLOSE), 1);
+    BIO_free(data->dgram_bio);
+    data->dgram_bio = NULL;
+    reset_for_teardown(bio);
+}
+#endif
+
+/* conn_read */
+
+static void test_conn_read_success(void **state)
+{
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+
+    expect_read(FAKE_SOCKET, buf, 8, 8);
+    assert_int_equal(BIO_read(bio, buf, 8), 8);
+    assert_false(BIO_should_retry(bio));
+    assert_false(BIO_eof(bio));
+}
+
+static void test_conn_read_eof(void **state)
+{
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+
+    expect_read(FAKE_SOCKET, buf, 8, 0);
+    expect_BIO_sock_should_retry(0, 0);
+    assert_true(BIO_read(bio, buf, 8) <= 0);
+    assert_true(BIO_eof(bio));
+}
+
+static void test_conn_read_retry(void **state)
+{
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+
+    expect_read(FAKE_SOCKET, buf, 8, -1);
+    expect_BIO_sock_should_retry(-1, 1);
+    assert_true(BIO_read(bio, buf, 8) <= 0);
+    assert_true(BIO_should_read(bio));
+}
+
+static void test_conn_read_error(void **state)
+{
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+
+    expect_read(FAKE_SOCKET, buf, 8, -1);
+    expect_BIO_sock_should_retry(-1, 0);
+    assert_true(BIO_read(bio, buf, 8) <= 0);
+    assert_false(BIO_should_retry(bio));
+    assert_false(BIO_eof(bio));
+}
+
+static void test_conn_read_enters_state_machine(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    char buf[8] = { 0 };
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+    bio->init = 1;
+
+    expect_BIO_lookup(&g_addrinfo1, 1);
+    expect_BIO_socket(AF_INET, SOCK_STREAM, IPPROTO_TCP, 0, FAKE_SOCKET);
+    expect_BIO_connect(FAKE_SOCKET, (BIO_ADDR *)&g_sin, BIO_SOCK_KEEPALIVE, 1);
+    expect_read(FAKE_SOCKET, buf, 8, 8);
+
+    assert_int_equal(BIO_read(bio, buf, 8), 8);
+    assert_int_equal(data->state, BIO_CONN_S_OK);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_read_state_machine_fails(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    char buf[8] = { 0 };
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+    bio->init = 1;
+
+    expect_BIO_lookup(NULL, 0); /* conn_state <= 0, no socket touched */
+
+    assert_true(BIO_read(bio, buf, 8) <= 0);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_read_dgram_delegates(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO *dg = make_fake_dgram();
+    char buf[8] = { 0 };
+
+    data->dgram_bio = dg;
+
+    expect_fake_dgram_read(buf, 8, 4);
+    assert_int_equal(BIO_read(bio, buf, 8), 4);
+
+    data->dgram_bio = NULL;
+    BIO_free(dg);
+}
+
+/* conn_write */
+
+static void test_conn_write_success(void **state)
+{
+    BIO *bio = *state;
+    const char buf[] = "hello";
+
+    expect_write(FAKE_SOCKET, buf, 5, 5);
+    assert_int_equal(BIO_write(bio, buf, 5), 5);
+    assert_false(BIO_should_retry(bio));
+}
+
+static void test_conn_write_retry(void **state)
+{
+    BIO *bio = *state;
+    const char buf[] = "hello";
+
+    expect_write(FAKE_SOCKET, buf, 5, -1);
+    expect_BIO_sock_should_retry(-1, 1);
+    assert_true(BIO_write(bio, buf, 5) <= 0);
+    assert_true(BIO_should_write(bio));
+}
+
+static void test_conn_write_error(void **state)
+{
+    BIO *bio = *state;
+    const char buf[] = "hello";
+
+    expect_write(FAKE_SOCKET, buf, 5, -1);
+    expect_BIO_sock_should_retry(-1, 0);
+    assert_true(BIO_write(bio, buf, 5) <= 0);
+    assert_false(BIO_should_retry(bio));
+}
+
+static void test_conn_write_dgram_delegates(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO *dg = make_fake_dgram();
+    const char buf[] = "hello";
+
+    data->dgram_bio = dg;
+
+    expect_fake_dgram_write(buf, 5, 5);
+    assert_int_equal(BIO_write(bio, buf, 5), 5);
+
+    data->dgram_bio = NULL;
+    BIO_free(dg);
+}
+
+/* conn_gets
+ *
+ * conn_gets is non-static (unlike the other method functions) so it is a
+ * public symbol, but we exercise it via BIO_gets to stay in-interface.
+ */
+
+static void test_conn_gets_null_buf(void **state)
+{
+    assert_true(BIO_gets(*state, NULL, 8) <= 0);
+}
+
+static void test_conn_gets_zero_size(void **state)
+{
+    char buf[8] = { 0 };
+
+    assert_true(BIO_gets(*state, buf, 0) <= 0);
+}
+
+static void test_conn_gets_null_ptr(void **state)
+{
+    /* bio->ptr == NULL is caught before any field access */
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+    void *saved = bio->ptr;
+
+    bio->ptr = NULL;
+    assert_true(BIO_gets(bio, buf, sizeof(buf)) <= 0);
+    bio->ptr = saved;
+}
+
+static void test_conn_gets_dgram_bio_set(void **state)
+{
+    /* dgram_bio present is an error for gets */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    char buf[8] = { 0 };
+    BIO fake_dgram;
+
+    data->dgram_bio = &fake_dgram;
+    assert_int_equal(BIO_gets(bio, buf, sizeof(buf)), -1);
+    data->dgram_bio = NULL;
+}
+
+static void test_conn_gets_newline(void **state)
+{
+    BIO *bio = *state;
+    char buf[8] = { 'h', 'i', '\n' };
+
+    expect_read(FAKE_SOCKET, buf, 1, 1);
+    expect_read(FAKE_SOCKET, buf + 1, 1, 1);
+    expect_read(FAKE_SOCKET, buf + 2, 1, 1);
+
+    assert_int_equal(BIO_gets(bio, buf, sizeof(buf)), 3);
+}
+
+static void test_conn_gets_fills_buffer(void **state)
+{
+    /* size=4 allows at most 3 chars before the terminating NUL */
+    BIO *bio = *state;
+    char buf[4] = { 'a', 'b', 'c' };
+
+    expect_read(FAKE_SOCKET, buf, 1, 1);
+    expect_read(FAKE_SOCKET, buf + 1, 1, 1);
+    expect_read(FAKE_SOCKET, buf + 2, 1, 1);
+
+    assert_int_equal(BIO_gets(bio, buf, 4), 3);
+}
+
+static void test_conn_gets_eof_mid(void **state)
+{
+    /* One char read, then EOF: returns the char count */
+    BIO *bio = *state;
+    char buf[8] = { 'z' };
+
+    expect_read(FAKE_SOCKET, buf, 1, 1);
+    expect_read(FAKE_SOCKET, buf + 1, 1, 0);
+    expect_BIO_sock_should_retry(0, 0);
+
+    assert_int_equal(BIO_gets(bio, buf, sizeof(buf)), 1);
+}
+
+static void test_conn_gets_immediate_eof(void **state)
+{
+    /* First read returns 0: EOF flag set, returns 0 */
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+
+    expect_read(FAKE_SOCKET, buf, 1, 0);
+    expect_BIO_sock_should_retry(0, 0);
+
+    assert_int_equal(BIO_gets(bio, buf, sizeof(buf)), 0);
+    assert_int_equal(buf[0], '\0');
+}
+
+static void test_conn_gets_retry(void **state)
+{
+    BIO *bio = *state;
+    char buf[8] = { 0 };
+
+    expect_read(FAKE_SOCKET, buf, 1, -1);
+    expect_BIO_sock_should_retry(-1, 1);
+
+    assert_int_equal(BIO_gets(bio, buf, sizeof(buf)), -1);
+    assert_true(BIO_should_retry(bio));
+}
+
+/* conn_puts */
+
+static void test_conn_puts_success(void **state)
+{
+    BIO *bio = *state;
+    const char *str = "hello";
+
+    expect_write(FAKE_SOCKET, str, 5, 5);
+    assert_int_equal(BIO_puts(bio, str), 5);
+}
+
+static void test_conn_puts_write_fails(void **state)
+{
+    BIO *bio = *state;
+    const char *str = "hello";
+
+    expect_write(FAKE_SOCKET, str, 5, -1);
+    expect_BIO_sock_should_retry(-1, 0);
+    assert_true(BIO_puts(bio, str) <= 0);
+}
+
+/* conn_ctrl */
+
+static void test_conn_ctrl_reset_no_socket(void **state)
+{
+    /* INVALID_SOCKET -> conn_close_socket is a no-op */
+    assert_int_equal(BIO_ctrl(*state, BIO_CTRL_RESET, 0, NULL), 0);
+    assert_int_equal(get_data(*state)->state, BIO_CONN_S_BEFORE);
+}
+
+static void test_conn_ctrl_reset_clears_addrs(void **state)
+{
+    /* RESET also nulls the iterators and clears flags */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->addr_first = NULL; /* keep BIO_ADDRINFO_free a no-op */
+    data->addr_iter = &g_addrinfo1;
+    bio->flags = BIO_FLAGS_IN_EOF;
+    bio->num = (int)INVALID_SOCKET;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_RESET, 0, NULL), 0);
+    assert_null(data->addr_first);
+    assert_null(data->addr_iter);
+    assert_int_equal(bio->flags, 0);
+    assert_int_equal(data->state, BIO_CONN_S_BEFORE);
+}
+
+static void test_conn_ctrl_get_connect(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    const char *out = NULL;
+
+    /* NULL ptr always returns 0 regardless of num */
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 0, NULL), 0);
+
+    data->param_hostname = OPENSSL_strdup("host.example");
+    data->param_service = OPENSSL_strdup("443");
+    data->connect_mode = BIO_SOCK_KEEPALIVE;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 0, &out), 1);
+    assert_string_equal(out, "host.example");
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 1, &out), 1);
+    assert_string_equal(out, "443");
+
+    /* num==4: connect_mode; ptr just needs to be non-NULL */
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 4, &out),
+        BIO_SOCK_KEEPALIVE);
+
+    /* unknown num with non-NULL ptr -> 0 */
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 99, &out), 0);
+}
+
+static void test_conn_ctrl_get_connect_address(void **state)
+{
+    /* num==2: address pointer from addr_iter */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    const char *out = NULL;
+
+    data->addr_iter = &g_addrinfo1;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 2, &out), 1);
+    assert_non_null(out);
+    data->addr_iter = NULL;
+}
+
+static void test_conn_ctrl_get_connect_family(void **state)
+{
+    /* num==3: AF_INET addr_iter maps to BIO_FAMILY_IPV4 */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    const char *out = NULL;
+
+    data->addr_iter = &g_addrinfo1;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_CONNECT, 3, &out),
+        BIO_FAMILY_IPV4);
+    data->addr_iter = NULL;
+}
+
+static void test_conn_ctrl_set_connect_null_ptr(void **state)
+{
+    /* ptr == NULL: no-op, init left untouched */
+    BIO *bio = *state;
+
+    assert_int_equal(bio->init, 0);
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_CONNECT, 0, NULL), 1);
+    assert_int_equal(bio->init, 0);
+}
+
+static void test_conn_ctrl_set_connect_hostname(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_CONNECT, 0, "host.example:443"),
+        1);
+    assert_int_equal(bio->init, 1);
+    assert_string_equal(data->param_hostname, "host.example");
+    assert_string_equal(data->param_service, "443");
+}
+
+static void test_conn_ctrl_set_connect_port(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_CONNECT, 1, "8443"), 1);
+    assert_string_equal(data->param_service, "8443");
+}
+
+static void test_conn_ctrl_set_connect_address(void **state)
+{
+    /* num==2: derive host/service from a BIO_ADDR */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_ADDR *addr = BIO_ADDR_new();
+
+    assert_non_null(addr);
+    assert_true(BIO_ADDR_rawmake(addr, AF_INET, &g_sin.sin_addr,
+        sizeof(g_sin.sin_addr), g_sin.sin_port));
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_CONNECT, 2, addr), 1);
+    assert_non_null(data->param_hostname);
+    assert_non_null(data->param_service);
+    assert_null(data->addr_first);
+
+    BIO_ADDR_free(addr);
+}
+
+static void test_conn_ctrl_set_connect_family(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    int family = BIO_FAMILY_IPV4;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_CONNECT, 3, &family), 1);
+    assert_int_equal(data->connect_family, BIO_FAMILY_IPV4);
+}
+
+static void test_conn_ctrl_set_sock_type(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_SOCK_TYPE, SOCK_DGRAM, NULL), 1);
+    assert_int_equal(data->connect_sock_type, SOCK_DGRAM);
+
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_SOCK_TYPE, SOCK_STREAM, NULL), 1);
+    assert_int_equal(data->connect_sock_type, SOCK_STREAM);
+
+    /* Invalid socktype */
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_SOCK_TYPE, 9999, NULL), 0);
+
+    /* Too late once past BEFORE */
+    data->state = BIO_CONN_S_GET_ADDR;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_SET_SOCK_TYPE, SOCK_DGRAM, NULL), 0);
+    data->state = BIO_CONN_S_BEFORE;
+}
+
+static void test_conn_ctrl_get_sock_type(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    data->connect_sock_type = SOCK_DGRAM;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_SOCK_TYPE, 0, NULL), SOCK_DGRAM);
+    data->connect_sock_type = SOCK_STREAM;
+}
+
+static void test_conn_ctrl_get_dgram_bio(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO *out = NULL;
+    BIO fake_dgram;
+
+    /* dgram_bio NULL -> 0 */
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_DGRAM_BIO, 0, &out), 0);
+
+    data->dgram_bio = &fake_dgram;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_DGRAM_BIO, 0, &out), 1);
+    assert_ptr_equal(out, &fake_dgram);
+    data->dgram_bio = NULL;
+}
+
+static void test_conn_ctrl_nbio(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    BIO_ctrl(bio, BIO_C_SET_NBIO, 1, NULL);
+    assert_true(data->connect_mode & BIO_SOCK_NONBLOCK);
+
+    BIO_ctrl(bio, BIO_C_SET_NBIO, 0, NULL);
+    assert_false(data->connect_mode & BIO_SOCK_NONBLOCK);
+}
+
+static void test_conn_ctrl_nbio_dgram(void **state)
+{
+    /* with a dgram_bio attached the mode flips and the call delegates */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO *dg = make_fake_dgram();
+
+    data->dgram_bio = dg;
+    BIO_ctrl(bio, BIO_C_SET_NBIO, 1, NULL);
+    assert_true(data->connect_mode & BIO_SOCK_NONBLOCK);
+
+    data->dgram_bio = NULL;
+    BIO_free(dg);
+}
+
+static void test_conn_ctrl_connect_mode(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+
+    BIO_ctrl(bio, BIO_C_SET_CONNECT_MODE, BIO_SOCK_KEEPALIVE, NULL);
+    assert_int_equal(data->connect_mode, BIO_SOCK_KEEPALIVE);
+    assert_int_equal(data->tfo_first, 0);
+}
+
+static void test_conn_ctrl_get_fd(void **state)
+{
+    BIO *bio = *state;
+    int fd = -1;
+
+    /* init==0 -> -1 */
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_FD, 0, &fd), -1);
+
+    bio->init = 1;
+    bio->num = FAKE_SOCKET;
+    assert_int_equal(BIO_ctrl(bio, BIO_C_GET_FD, 0, &fd), FAKE_SOCKET);
+    assert_int_equal(fd, FAKE_SOCKET);
+
+    bio->init = 0;
+    bio->num = (int)INVALID_SOCKET;
+}
+
+static void test_conn_ctrl_get_set_close(void **state)
+{
+    BIO *bio = *state;
+
+    bio->shutdown = BIO_NOCLOSE;
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_GET_CLOSE, 0, NULL), BIO_NOCLOSE);
+
+    BIO_ctrl(bio, BIO_CTRL_SET_CLOSE, BIO_CLOSE, NULL);
+    assert_int_equal(bio->shutdown, BIO_CLOSE);
+}
+
+static void test_conn_ctrl_pending_flush(void **state)
+{
+    BIO *bio = *state;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_PENDING, 0, NULL), 0);
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_WPENDING, 0, NULL), 0);
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_FLUSH, 0, NULL), 1);
+}
+
+static void test_conn_ctrl_eof(void **state)
+{
+    BIO *bio = *state;
+
+    bio->flags &= ~BIO_FLAGS_IN_EOF;
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), 0);
+
+    bio->flags |= BIO_FLAGS_IN_EOF;
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_EOF, 0, NULL), 1);
+    bio->flags &= ~BIO_FLAGS_IN_EOF;
+}
+
+static void test_conn_ctrl_set_callback_defers(void **state)
+{
+    /* BIO_CTRL_SET_CALLBACK via conn_ctrl returns 0 (use callback ctrl) */
+    BIO *bio = *state;
+
+    assert_int_equal(BIO_ctrl(bio, BIO_CTRL_SET_CALLBACK, 0, NULL), 0);
+}
+
+static void test_conn_ctrl_default(void **state)
+{
+    assert_int_equal(BIO_ctrl(*state, 9999, 0, NULL), 0);
+}
+
+/* conn_callback_ctrl */
+
+static int dummy_cb(BIO *b, int s, int res)
+{
+    (void)b;
+    (void)s;
+    return res;
+}
+
+static void test_conn_ctrl_get_callback(void **state)
+{
+    /* BIO_CTRL_GET_CALLBACK returns the stored info_callback */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_info_cb *fp = NULL;
+
+    data->info_callback = dummy_cb;
+    BIO_ctrl(bio, BIO_CTRL_GET_CALLBACK, 0, &fp);
+    assert_ptr_equal(fp, dummy_cb);
+    data->info_callback = NULL;
+}
+
+static void test_conn_callback_ctrl_set(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_info_cb *retrieved;
+
+    assert_int_equal(BIO_set_info_callback(bio, dummy_cb), 1);
+    assert_ptr_equal(data->info_callback, dummy_cb);
+
+    assert_int_equal(BIO_get_info_callback(bio, &retrieved), 1);
+    assert_ptr_equal(retrieved, dummy_cb);
+
+    assert_int_equal(BIO_set_info_callback(bio, NULL), 1);
+}
+
+static void test_conn_callback_ctrl_default(void **state)
+{
+    assert_int_equal(BIO_callback_ctrl(*state, BIO_CTRL_SET_CALLBACK, dummy_cb), 1);
+}
+
+static void test_conn_callback_ctrl_invalid(void **state)
+{
+    /* If cmd different than BIO_CTRL_SET_CALLBACK, return -2. */
+    assert_int_equal(BIO_callback_ctrl(*state, 9999, dummy_cb), -2);
+}
+
+/* conn_sendmmsg / conn_recvmmsg */
+
+static void test_conn_sendmmsg_no_dgram(void **state)
+{
+    /* State OK, dgram_bio NULL -> error */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_MSG msg = { 0 };
+    size_t processed = 1;
+
+    data->state = BIO_CONN_S_OK;
+    bio->num = FAKE_SOCKET;
+
+    assert_int_equal(
+        BIO_sendmmsg(bio, &msg, sizeof(msg), 1, 0, &processed), 0);
+    assert_int_equal(processed, 0);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_sendmmsg_state_fails(void **state)
+{
+    /* state != OK and conn_state fails -> 0, processed zeroed */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_MSG msg = { 0 };
+    size_t processed = 5;
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+    bio->init = 1; /* BIO_sendmmsg rejects !init before dispatch */
+
+    expect_BIO_lookup(NULL, 0);
+
+    assert_int_equal(
+        BIO_sendmmsg(bio, &msg, sizeof(msg), 1, 0, &processed), 0);
+    assert_int_equal(processed, 0);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_sendmmsg_dgram_delegates(void **state)
+{
+    /* state OK with dgram_bio -> delegates to BIO_sendmmsg on it */
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO *dg = make_fake_dgram();
+    BIO_MSG msg = { 0 };
+    size_t processed = 0;
+
+    data->dgram_bio = dg;
+
+    expect_fake_dgram_sendmmsg(&msg, sizeof(msg), 1, 0, 1, 1);
+
+    assert_int_equal(
+        BIO_sendmmsg(bio, &msg, sizeof(msg), 1, 0, &processed), 1);
+    assert_int_equal(processed, 1);
+
+    data->dgram_bio = NULL;
+    BIO_free(dg);
+    reset_for_teardown(bio);
+}
+
+static void test_conn_recvmmsg_no_dgram(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_MSG msg = { 0 };
+    size_t processed = 1;
+
+    data->state = BIO_CONN_S_OK;
+    bio->num = FAKE_SOCKET;
+
+    assert_int_equal(
+        BIO_recvmmsg(bio, &msg, sizeof(msg), 1, 0, &processed), 0);
+    assert_int_equal(processed, 0);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_recvmmsg_state_fails(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO_MSG msg = { 0 };
+    size_t processed = 5;
+
+    data->param_hostname = OPENSSL_strdup("host");
+    data->param_service = OPENSSL_strdup("443");
+    bio->init = 1;
+
+    expect_BIO_lookup(NULL, 0);
+
+    assert_int_equal(
+        BIO_recvmmsg(bio, &msg, sizeof(msg), 1, 0, &processed), 0);
+    assert_int_equal(processed, 0);
+
+    reset_for_teardown(bio);
+}
+
+static void test_conn_recvmmsg_dgram_delegates(void **state)
+{
+    BIO *bio = *state;
+    BIO_CONNECT *data = get_data(bio);
+    BIO *dg = make_fake_dgram();
+    BIO_MSG msg = { 0 };
+    size_t processed = 0;
+
+    data->dgram_bio = dg;
+
+    expect_fake_dgram_recvmmsg(&msg, sizeof(msg), 1, 0, 1, 1);
+
+    assert_int_equal(
+        BIO_recvmmsg(bio, &msg, sizeof(msg), 1, 0, &processed), 1);
+    assert_int_equal(processed, 1);
+
+    data->dgram_bio = NULL;
+    BIO_free(dg);
+    reset_for_teardown(bio);
+}
+
+/* main */
+
+#define CONN_TEST(name) \
+    cmocka_unit_test_setup_teardown(name, setup, teardown)
+
+#define CONN_TEST_IO(name) \
+    cmocka_unit_test_setup_teardown(name, setup_io, teardown_io)
+
+int main(void)
+{
+    const struct CMUnitTest tests[] = {
+        /* conn_new */
+        CONN_TEST(test_conn_new),
+        /* conn_free */
+        CONN_TEST(test_conn_free_no_shutdown),
+        /* conn_close_socket */
+        CONN_TEST(test_close_socket_none),
+        CONN_TEST(test_close_socket_non_ok_state),
+        CONN_TEST(test_close_socket_ok_state),
+        /* conn_state */
+        CONN_TEST(test_conn_state_no_hostname),
+        CONN_TEST(test_conn_state_unsupported_family),
+        CONN_TEST(test_conn_state_lookup_fails),
+        CONN_TEST(test_conn_state_socket_fails),
+        CONN_TEST(test_conn_state_connect_succeeds),
+        CONN_TEST(test_conn_state_already_ok),
+        CONN_TEST(test_conn_state_connect_retry),
+        CONN_TEST(test_conn_state_connect_error),
+        CONN_TEST(test_conn_state_connect_next_addr),
+        CONN_TEST(test_conn_state_blocked_ok),
+        CONN_TEST(test_conn_state_blocked_error),
+#ifndef OPENSSL_NO_DGRAM
+        CONN_TEST(test_conn_state_connect_dgram_ok),
+#endif
+        /* conn_read */
+        CONN_TEST_IO(test_conn_read_success),
+        CONN_TEST_IO(test_conn_read_eof),
+        CONN_TEST_IO(test_conn_read_retry),
+        CONN_TEST_IO(test_conn_read_error),
+        CONN_TEST(test_conn_read_enters_state_machine),
+        CONN_TEST(test_conn_read_state_machine_fails),
+        CONN_TEST_IO(test_conn_read_dgram_delegates),
+        /* conn_write */
+        CONN_TEST_IO(test_conn_write_success),
+        CONN_TEST_IO(test_conn_write_retry),
+        CONN_TEST_IO(test_conn_write_error),
+        CONN_TEST_IO(test_conn_write_dgram_delegates),
+        /* conn_gets */
+        CONN_TEST(test_conn_gets_null_buf),
+        CONN_TEST(test_conn_gets_zero_size),
+        CONN_TEST(test_conn_gets_null_ptr),
+        CONN_TEST_IO(test_conn_gets_dgram_bio_set),
+        CONN_TEST_IO(test_conn_gets_newline),
+        CONN_TEST_IO(test_conn_gets_fills_buffer),
+        CONN_TEST_IO(test_conn_gets_eof_mid),
+        CONN_TEST_IO(test_conn_gets_immediate_eof),
+        CONN_TEST_IO(test_conn_gets_retry),
+        /* conn_puts */
+        CONN_TEST_IO(test_conn_puts_success),
+        CONN_TEST_IO(test_conn_puts_write_fails),
+        /* conn_ctrl */
+        CONN_TEST(test_conn_ctrl_reset_no_socket),
+        CONN_TEST(test_conn_ctrl_reset_clears_addrs),
+        CONN_TEST(test_conn_ctrl_get_connect),
+        CONN_TEST(test_conn_ctrl_get_connect_address),
+        CONN_TEST(test_conn_ctrl_get_connect_family),
+        CONN_TEST(test_conn_ctrl_set_connect_null_ptr),
+        CONN_TEST(test_conn_ctrl_set_connect_hostname),
+        CONN_TEST(test_conn_ctrl_set_connect_port),
+        CONN_TEST(test_conn_ctrl_set_connect_address),
+        CONN_TEST(test_conn_ctrl_set_connect_family),
+        CONN_TEST(test_conn_ctrl_set_sock_type),
+        CONN_TEST(test_conn_ctrl_get_sock_type),
+        CONN_TEST(test_conn_ctrl_get_dgram_bio),
+        CONN_TEST(test_conn_ctrl_nbio),
+        CONN_TEST_IO(test_conn_ctrl_nbio_dgram),
+        CONN_TEST(test_conn_ctrl_connect_mode),
+        CONN_TEST(test_conn_ctrl_get_fd),
+        CONN_TEST(test_conn_ctrl_get_set_close),
+        CONN_TEST(test_conn_ctrl_pending_flush),
+        CONN_TEST(test_conn_ctrl_eof),
+        CONN_TEST(test_conn_ctrl_set_callback_defers),
+        CONN_TEST(test_conn_ctrl_default),
+        /* conn_callback_ctrl */
+        CONN_TEST(test_conn_ctrl_get_callback),
+        CONN_TEST(test_conn_callback_ctrl_set),
+        CONN_TEST(test_conn_callback_ctrl_default),
+        CONN_TEST(test_conn_callback_ctrl_invalid),
+        /* conn_sendmmsg / conn_recvmmsg */
+        CONN_TEST(test_conn_sendmmsg_no_dgram),
+        CONN_TEST(test_conn_sendmmsg_state_fails),
+        CONN_TEST_IO(test_conn_sendmmsg_dgram_delegates),
+        CONN_TEST(test_conn_recvmmsg_no_dgram),
+        CONN_TEST(test_conn_recvmmsg_state_fails),
+        CONN_TEST_IO(test_conn_recvmmsg_dgram_delegates),
+    };
+
+    cmocka_set_message_output(CM_OUTPUT_TAP);
+
+    return cmocka_run_group_tests(tests, group_setup, group_teardown);
+}
+
+#else
+
+int main(void)
+{
+    return 0;
+}
+
+#endif /* OPENSSL_NO_SOCK */