Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_network_ssl_nonblock.c

Non-blocking (resumable) TLS: both peers drive netw_ssl_handshake_once / netw_ssl_server_handshake_once + the *_once movers from their own poll() loop, never the thread engine.

Non-blocking (resumable) TLS: both peers drive netw_ssl_handshake_once / netw_ssl_server_handshake_once + the *_once movers from their own poll() loop, never the thread engine.

Author
Castagnier Mickael
Version
1.0
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "nilorea/n_log.h"
#include "nilorea/n_str.h"
#include "nilorea/n_x509.h"
#ifndef __windows__
#include <poll.h>
#endif
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <openssl/bio.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#define MSG "nonblock-client->server"
#define REPLY "nonblock-server->client"
#define MSG_LEN 24
static void leave_stale_openssl_error(void) {
static const char garbage[] = "not a certificate";
BIO* b = BIO_new_mem_buf(garbage, (int)(sizeof(garbage) - 1));
if (b) {
X509* x = PEM_read_bio_X509(b, NULL, NULL, NULL); /* fails: queues */
if (x) X509_free(x);
BIO_free(b);
}
}
typedef struct SRV {
SSL_CTX* ctx;
int ok;
} SRV;
/* Parse the -V LOG_LEVEL verbosity argument. */
static void process_args(int argc, char** argv) {
int opt = 0;
while ((opt = getopt(argc, argv, "hvV:")) != EOF) {
switch (opt) {
case 'V':
if (!strncmp("LOG_NULL", optarg, 8))
else if (!strncmp("LOG_NOTICE", optarg, 10))
else if (!strncmp("LOG_INFO", optarg, 8))
else if (!strncmp("LOG_ERR", optarg, 7))
else if (!strncmp("LOG_DEBUG", optarg, 9))
else {
fprintf(stderr, "Unknown log level %s\n", optarg);
exit(1);
}
break;
case 'v':
fprintf(stderr, "ex_network_ssl_nonblock\n");
exit(1);
case 'h':
default:
fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
break;
}
}
}
/* Build a server ctx from an in-memory PEM cert + key, pinned to the newest TLS
* version the linked TLS library can negotiate. That is TLS 1.3 from OpenSSL
* 1.1.1 (LibreSSL 3.5) on; on OpenSSL 1.0.2 neither the version-flexible
* TLS_server_method() nor the min/max_proto_version setters exist, and
* TLSv1_2_server_method() already pins the context to the one version that
* build speaks. What this example
* exercises is the non-blocking handshake and I/O drive loop, which is the
* same either way. */
static SSL_CTX* make_server_ctx(const N_STR* cert_pem, const N_STR* key_pem) {
#if N_OSSL_HAVE_PROTO_VERSION_SETTERS
SSL_CTX* ctx = SSL_CTX_new(TLS_server_method());
if (!ctx)
return NULL;
SSL_CTX_set_min_proto_version(ctx, NETW_TLS_MAX_VERSION);
SSL_CTX_set_max_proto_version(ctx, NETW_TLS_MAX_VERSION);
#else
SSL_CTX* ctx = SSL_CTX_new(TLSv1_2_server_method());
if (!ctx)
return NULL;
#endif
SSL_CTX_set_mode(ctx, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
BIO* cb = BIO_new_mem_buf(cert_pem->data, (int)cert_pem->written);
BIO* kb = BIO_new_mem_buf(key_pem->data, (int)key_pem->written);
X509* x = cb ? PEM_read_bio_X509(cb, NULL, NULL, NULL) : NULL;
EVP_PKEY* pk = kb ? PEM_read_bio_PrivateKey(kb, NULL, NULL, NULL) : NULL;
int ok = x && pk && SSL_CTX_use_certificate(ctx, x) == 1 &&
SSL_CTX_use_PrivateKey(ctx, pk) == 1;
if (x)
X509_free(x);
if (pk)
EVP_PKEY_free(pk);
if (cb)
BIO_free(cb);
if (kb)
BIO_free(kb);
if (!ok) {
SSL_CTX_free(ctx);
return NULL;
}
return ctx;
}
/* Wait for a socket to become readable (want_write==0) or writable.
* poll() is POSIX only. Winsock has no <poll.h>, and its WSAPoll() equivalent
* needs _WIN32_WINNT >= 0x0600 headers while n_windows.h targets 0x0501, so the
* Windows branch waits with select(), which Winsock always provides. A failed
* non-blocking connect is signalled through exceptfds there (not writefds), so
* that set is watched too: the caller learns the real outcome from its next
* netw_connect_check() / SSL attempt. */
static int poll_fd(SOCKET fd, int want_write, int timeout_ms) {
#ifdef __windows__
fd_set rset, wset, eset;
struct timeval tv;
FD_ZERO(&rset);
FD_ZERO(&wset);
FD_ZERO(&eset);
if (want_write) {
FD_SET(fd, &wset);
FD_SET(fd, &eset);
} else {
FD_SET(fd, &rset);
}
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
return select((int)fd + 1, want_write ? NULL : &rset,
want_write ? &wset : NULL, want_write ? &eset : NULL, &tv);
#else
struct pollfd p;
p.fd = fd;
p.events = (short)(want_write ? POLLOUT : POLLIN);
p.revents = 0;
return poll(&p, 1, timeout_ms);
#endif
}
/* Drive a resumable handshake to completion from a poll() loop. */
static int drive_handshake(NETWORK* netw, int is_server, SSL_CTX* sctx) {
int guard;
for (guard = 0; guard < 10000; guard++) {
int r = is_server ? netw_ssl_server_handshake_once(netw, sctx)
if (r == TRUE)
return TRUE;
if (r == NETW_IO_WANT_READ) {
if (poll_fd(netw->link.sock, 0, 2000) <= 0)
return FALSE;
continue;
}
if (r == NETW_IO_WANT_WRITE) {
if (poll_fd(netw->link.sock, 1, 2000) <= 0)
return FALSE;
continue;
}
return FALSE; /* fatal */
}
return FALSE;
}
/* Drive a non-blocking connect to completion from a poll() loop. */
static int drive_connect(NETWORK* netw) {
int guard;
for (guard = 0; guard < 10000; guard++) {
if (r == TRUE)
return TRUE;
if (r == NETW_IO_WANT_WRITE) {
if (poll_fd(netw->link.sock, 1, 2000) <= 0)
return FALSE;
continue;
}
return FALSE; /* all addresses failed */
}
return FALSE;
}
/* Move exactly n bytes with the single-attempt movers, polling on WANT. */
static int io_full(NETWORK* netw, char* buf, uint32_t n, int is_send) {
uint32_t done = 0;
int guard;
for (guard = 0; guard < 100000 && done < n; guard++) {
ssize_t r = is_send ? send_ssl_data_once(netw, buf + done, n - done)
: recv_ssl_data_once(netw, buf + done, n - done);
if (r > 0) {
done += (uint32_t)r;
continue;
}
if (r == NETW_IO_WANT_READ) {
if (poll_fd(netw->link.sock, 0, 2000) <= 0)
return FALSE;
continue;
}
if (r == NETW_IO_WANT_WRITE) {
if (poll_fd(netw->link.sock, 1, 2000) <= 0)
return FALSE;
continue;
}
return FALSE; /* disconnect or error */
}
return done == n ? TRUE : FALSE;
}
/* Server thread: accept one connection (non-blocking), complete the resumable
* handshake, read the probe, echo the reply. */
static void* server_fn(void* p) {
SRV* s = (SRV*)p;
NETWORK* c = NULL;
int guard;
for (guard = 0; guard < 10000 && !c; guard++) {
if (poll_fd(s->listen->link.sock, 0, 2000) <= 0)
break;
}
if (!c)
return NULL;
if (drive_handshake(c, 1, s->ctx) == TRUE) {
char buf[MSG_LEN] = "";
char reply[MSG_LEN];
memcpy(reply, REPLY, MSG_LEN);
if (io_full(c, buf, MSG_LEN, 0) == TRUE && memcmp(buf, MSG, MSG_LEN) == 0) {
n_log(LOG_NOTICE, "server received: %s", buf);
if (io_full(c, reply, MSG_LEN, 1) == TRUE)
s->ok = 1;
}
}
return NULL;
}
/* Bind a loopback listening socket on the first free port in a range. */
static NETWORK* bind_free(char* port_out, size_t port_out_len) {
int p;
for (p = 18500; p < 18600; p++) {
NETWORK* l = NULL;
char ps[16];
snprintf(ps, sizeof(ps), "%d", p);
if (netw_make_listening(&l, "127.0.0.1", ps, 5, NETWORK_IPALL) == TRUE && l) {
snprintf(port_out, port_out_len, "%s", ps);
return l;
}
if (l)
}
return NULL;
}
int main(int argc, char** argv) {
N_STR* cert = NULL;
N_STR* key = NULL;
SSL_CTX* sctx = NULL;
NETWORK* listen = NULL;
NETWORK* cli = NULL;
SRV srv;
pthread_t th;
char port[16] = "";
int failures = 0;
process_args(argc, argv);
/* OpenSSL is initialised once in the main thread: the server and client
* threads both use TLS and the lazy init guard is not synchronized. */
/* A private self-signed CA cert doubles as the server leaf here; the client
* turns verification off (it is not in any trust store). */
if (n_x509_generate_ca("Nilorea NonBlock TLS Test", 3650, &cert, &key) != 0 || !cert || !key) {
n_log(LOG_ERR, "certificate generation failed");
return 1;
}
if (!sctx) {
n_log(LOG_ERR, "server ctx setup failed");
return 1;
}
listen = bind_free(port, sizeof(port));
if (!listen) {
n_log(LOG_ERR, "could not bind a loopback port");
SSL_CTX_free(sctx);
return 1;
}
srv.listen = listen;
srv.ctx = sctx;
srv.ok = 0;
pthread_create(&th, NULL, server_fn, &srv);
/* The whole client path is non-blocking + poll-driven: start the TCP
* connect, drive it to completion, set up the client TLS ctx, then run the
* TLS handshake -- none of it blocks a shared thread. */
if (netw_connect_nonblock(&cli, "127.0.0.1", port, NETWORK_IPALL) != TRUE || !cli) {
n_log(LOG_ERR, "client connect start failed");
} else if (drive_connect(cli) != TRUE) {
n_log(LOG_ERR, "client non-blocking connect failed");
} else if (netw_ssl_start_client(cli) != TRUE) {
n_log(LOG_ERR, "client TLS ctx setup failed");
} else {
n_log(LOG_ERR, "client TLS version pin failed");
}
if (drive_handshake(cli, 0, NULL) != TRUE) {
n_log(LOG_ERR, "client non-blocking handshake failed");
} else {
char msg[MSG_LEN];
char buf[MSG_LEN] = "";
/* A stale entry on the thread's error queue -- left by some
* OTHER connection, e.g. one shut down mid-handshake -- must not
* fail this healthy one: OpenSSL's SSL_get_error() reports
* SSL_ERROR_SSL whenever the queue is non-empty, so the *_once
* movers must clear it first. The server is waiting for our
* message, so the idle read can only be WANT_READ. (A
* single-threaded DarkChan server closed an innocent client this
* way and lost its first frames, 2026-10-08.) */
ssize_t idle = recv_ssl_data_once(cli, buf, MSG_LEN);
if (idle != NETW_IO_WANT_READ) {
n_log(LOG_ERR, "stale error queue: idle recv returned %zd, not WANT_READ", idle);
}
leave_stale_openssl_error(); /* and the send that follows */
memcpy(msg, MSG, MSG_LEN);
if (io_full(cli, msg, MSG_LEN, 1) != TRUE) {
n_log(LOG_ERR, "client send failed");
} else if (io_full(cli, buf, MSG_LEN, 0) != TRUE) {
n_log(LOG_ERR, "client recv failed");
} else if (memcmp(buf, REPLY, MSG_LEN) != 0) {
n_log(LOG_ERR, "client got an unexpected reply");
} else {
n_log(LOG_NOTICE, "client received: %s", buf);
}
}
}
netw_close(&cli);
pthread_join(th, NULL);
if (!srv.ok) {
n_log(LOG_ERR, "server did not complete the exchange");
}
netw_close(&listen);
SSL_CTX_free(sctx);
if (failures) {
n_log(LOG_ERR, "ex_network_ssl_nonblock: %d failure(s)", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_network_ssl_nonblock: all checks passed");
return 0;
}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
NETWORK * netw
Network for server mode, accepting incomming.
Definition ex_network.c:39
static void * server_fn(void *p)
static NETWORK * bind_free(char *port_out, size_t port_out_len)
#define MSG_LEN
fixed exchange message length (both messages are 17 chars + NUL)
volatile sig_atomic_t done
char * cert
char * port
#define MSG
client->server probe (both messages share this fixed length)
SSL_CTX * ctx
shared server ctx (owned by main)
static SSL_CTX * make_server_ctx(const N_STR *cert_pem, const N_STR *key_pem)
static int io_full(NETWORK *netw, char *buf, uint32_t n, int is_send)
static int poll_fd(SOCKET fd, int want_write, int timeout_ms)
static int drive_handshake(NETWORK *netw, int is_server, SSL_CTX *sctx)
NETWORK * listen
the listening socket
#define REPLY
server->client probe
static int drive_connect(NETWORK *netw)
int ok
set to 1 once the server completed the exchange
static void leave_stale_openssl_error(void)
leave a genuine error on this thread's OpenSSL queue (portable: a failed PEM parse,...
server worker context
volatile char key[227]
TRUE while the key is held.
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_DEBUG
debug-level messages
Definition n_log.h:108
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_NULL
no log output
Definition n_log.h:70
#define LOG_INFO
informational
Definition n_log.h:106
size_t written
number of meaningful bytes in data, excluding the null terminator; the size including the null termin...
Definition n_str.h:68
char * data
the string
Definition n_str.h:63
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
Definition n_str.h:203
A box including a string and his lenght.
Definition n_str.h:61
int n_x509_generate_ca(const char *cn, int days, N_STR **ca_cert_pem, N_STR **ca_key_pem)
Generate a self-signed certificate authority (CA) keypair and cert.
Definition n_x509.c:226
N_SOCKET link
networking socket
Definition n_network.h:454
SOCKET sock
a normal socket
Definition n_network.h:356
ssize_t send_ssl_data_once(void *netw, char *buf, uint32_t n)
single-attempt TLS send for non-blocking sockets (reactor use).
Definition n_network.c:5279
#define NETW_IO_WANT_READ
single-attempt I/O (send_data_once / recv_data_once): the operation cannot progress until the socket ...
Definition n_network.h:77
ssize_t recv_ssl_data_once(void *netw, char *buf, uint32_t n)
single-attempt TLS recv for non-blocking sockets (reactor use).
Definition n_network.c:5367
int netw_ssl_set_verify(NETWORK *netw, int enable)
enable or disable SSL peer certificate verification
Definition n_network.c:2053
NETWORK * netw_accept_nonblock_from(NETWORK *from, int blocking)
make a normal blocking 'accept' .
Definition n_network.c:4188
int netw_ssl_server_handshake_once(NETWORK *netw, SSL_CTX *ctx)
non-blocking, resumable TLS server handshake against a shared ctx
Definition n_network.c:3093
int netw_connect_nonblock(NETWORK **netw, char *host, char *port, int ip_version)
begin a non-blocking TCP connect without waiting for it to complete
Definition n_network.c:2651
int netw_ssl_handshake_once(NETWORK *netw, const char *sni_hostname)
non-blocking, resumable TLS client handshake
Definition n_network.c:3050
int netw_init_openssl(void)
Do not directly use, internal api.
Definition n_network.c:1335
#define NETW_TLS_MAX_VERSION
highest TLS version the linked TLS library can negotiate, as a TLS*_VERSION constant.
Definition n_network.h:266
int netw_make_listening(NETWORK **netw, char *addr, char *port, int nbpending, int ip_version)
Make a NETWORK be a Listening network.
Definition n_network.c:3493
int netw_unload_openssl(void)
Do not directly use, internal api.
Definition n_network.c:1368
#define NETWORK_IPALL
Flag for auto detection by OS of ip version to use.
Definition n_network.h:48
int SOCKET
default socket declaration
Definition n_network.h:111
int netw_connect_check(NETWORK *netw)
advance a non-blocking connect started by netw_connect_nonblock()
Definition n_network.c:2713
#define NETW_IO_WANT_WRITE
single-attempt I/O: the operation cannot progress until the socket is WRITABLE.
Definition n_network.h:81
int netw_close(NETWORK **netw)
Closing a specified Network, destroy queues, free the structure.
Definition n_network.c:3258
int netw_ssl_start_client(NETWORK *netw)
Set up a client SSL_CTX on an already-connected NETWORK.
Definition n_network.c:2868
int netw_ssl_set_versions(NETWORK *netw, int min_version, int max_version)
set the minimum (and optionally maximum) TLS protocol version on a connection's SSL context
Definition n_network.c:2085
Structure of a NETWORK.
Definition n_network.h:375
Common headers and low-level functions & define.
Generic log system.
Network Engine.
N_STR and string function declaration.
X.509 helpers: self-signed CA generation and per-host leaf minting.