protocol: add transport I/O vtable over TCP/TLS primitives

Introduce ProtocolIoOps (send/recv/has_pending), selected once by
protocol_session_init() and protocol_session_set_ssl(), and dispatch the
send, receive and status-read loops through session->ops instead of
branching on session->ssl at runtime.

Each op performs one transfer attempt and classifies the result
(PROTOCOL_IO_RETRY/CLOSED/ERROR), preserving the WANT_READ/WANT_WRITE
wait_events switching, the SSL_ERROR_SYSCALL/EINTR retry, the
SSL_pending poll gating and the deadline handling. The raw read()/write()
fallback lives in the plaintext ops.

Add unit tests: a socketpair session with a counting ops wrapper proving
the loops dispatch through the vtable, and a worker-thread test that
protocol_current_ssl() resolves the bound session's SSL when io_ssl is NULL.
This commit is contained in:
2026-09-23 00:18:36 +02:00
parent 1f8d60e30d
commit ec6692ac41
3 changed files with 314 additions and 79 deletions
+129
View File
@@ -1,9 +1,13 @@
#include "protocol.h"
#include "test_utils.h"
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <openssl/ssl.h>
#include <poll.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <threads.h>
@@ -786,6 +790,129 @@ static void test_protocol_throttle_bytes_legacy_same_session() {
io_set_fds(-1, -1);
}
/* ------------------------------------------------------------------------- *
* Transport-vtable dispatch tests.
* ------------------------------------------------------------------------- */
static int dispatch_send_calls;
static int dispatch_recv_calls;
static ssize_t counting_send(ProtocolSession* session, const void* data, size_t size,
short* wait_events) {
dispatch_send_calls++;
ssize_t written = write(session->write_fd, data, size);
if (written < 0)
return errno == EINTR ? PROTOCOL_IO_RETRY : PROTOCOL_IO_ERROR;
if (written == 0)
return PROTOCOL_IO_ERROR;
*wait_events = POLLOUT;
return written;
}
static ssize_t counting_recv(ProtocolSession* session, void* data, size_t size,
short* wait_events) {
dispatch_recv_calls++;
ssize_t received = read(session->read_fd, data, size);
if (received < 0)
return errno == EINTR ? PROTOCOL_IO_RETRY : PROTOCOL_IO_ERROR;
if (received == 0)
return PROTOCOL_IO_CLOSED;
*wait_events = POLLIN;
return received;
}
static bool counting_has_pending(const ProtocolSession* session) {
(void)session;
return false;
}
static const ProtocolIoOps counting_ops = {
.send = counting_send,
.recv = counting_recv,
.has_pending = counting_has_pending,
};
/* A plain-TCP socketpair session must route every byte through the ops table:
* installing a counting ops wrapper proves the send/receive loops dispatch via
* session->ops instead of branching on session->ssl. */
static void test_protocol_dispatch_via_ops() {
int sv[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
ProtocolSession sender;
ProtocolSession receiver;
protocol_session_init(&sender, sv[0], sv[0]);
protocol_session_set_bwlimit(&sender, 0);
protocol_session_init(&receiver, sv[1], sv[1]);
protocol_session_set_bwlimit(&receiver, 0);
EXPECT_NOT_NULL(sender.ops);
EXPECT_NOT_NULL(receiver.ops);
dispatch_send_calls = 0;
dispatch_recv_calls = 0;
sender.ops = &counting_ops;
receiver.ops = &counting_ops;
const char payload[] = "dispatch-through-vtable";
EXPECT_TRUE(protocol_send_n_data(&sender, payload, sizeof(payload)));
char received[sizeof(payload)] = {0};
EXPECT_TRUE(protocol_receive_n_data(&receiver, received, sizeof(received)));
EXPECT_EQ_INT(memcmp(payload, received, sizeof(payload)), 0);
EXPECT_TRUE(dispatch_send_calls > 0);
EXPECT_TRUE(dispatch_recv_calls > 0);
close(sv[0]);
close(sv[1]);
}
typedef struct {
ProtocolSession* session;
SSL* expected_ssl;
SSL* resolved_ssl;
SSL* thread_local_ssl;
} SslResolverWorkerArg;
static int ssl_resolver_worker(void* arg) {
SslResolverWorkerArg* worker = arg;
protocol_session_bind(worker->session);
worker->resolved_ssl = protocol_current_ssl();
worker->thread_local_ssl = io_get_ssl();
protocol_session_unbind();
return thrd_success;
}
/* The worker-thread bug fix: a thread that bound a TLS session but never ran
* the handshake has io_ssl == NULL, yet protocol_current_ssl() must return the
* session's SSL so callers pick the TLS path. */
static void test_protocol_current_ssl_prefers_bound_session() {
SSL_CTX* ctx = SSL_CTX_new(TLS_method());
EXPECT_NOT_NULL(ctx);
SSL* ssl = SSL_new(ctx);
EXPECT_NOT_NULL(ssl);
int sv[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv), 0);
ProtocolSession session;
protocol_session_init(&session, sv[0], sv[0]);
protocol_session_set_ssl(&session, ssl);
/* Clear the calling thread's legacy SSL: only the bound session carries it. */
io_set_fds(-1, -1);
SslResolverWorkerArg arg = {
.session = &session, .expected_ssl = ssl, .resolved_ssl = NULL, .thread_local_ssl = ssl};
thrd_t worker;
EXPECT_EQ_INT(thrd_create(&worker, ssl_resolver_worker, &arg), thrd_success);
EXPECT_EQ_INT(thrd_join(worker, NULL), thrd_success);
EXPECT_TRUE(arg.resolved_ssl == ssl);
EXPECT_NULL(arg.thread_local_ssl);
close(sv[0]);
close(sv[1]);
SSL_free(ssl);
SSL_CTX_free(ctx);
}
void test_protocol() {
test_send_receive_n_data();
test_send_receive_n_data_zero();
@@ -819,4 +946,6 @@ void test_protocol() {
test_protocol_throttle_bytes_paces();
test_protocol_throttle_bytes_unlimited();
test_protocol_throttle_bytes_legacy_same_session();
test_protocol_dispatch_via_ops();
test_protocol_current_ssl_prefers_bound_session();
}