#OpenSSL TLS Backend
#define FIO_IO
#include "fio-stl/include.h"OpenSSL 3.x IO-layer TLS backend for the facil.io reactor. When OpenSSL is
available at build time this module auto-registers itself as the default TLS
transport. You don't have to call anything — just define FIO_IO and link
against OpenSSL 3.x.
For IO-layer TLS context overview see [./400 io-overview.md](./400 io-overview.md).
For the sibling native TLS 1.3 backend see [./405 tls13.md](./405 tls13.md) (planned).
For standalone TLS 1.3 crypto (key schedule, record layer) see [./190 tls13.md](./190 tls13.md).
#Activation
The module compiles automatically when all of the following are true:
| Condition | Notes |
|---|---|
FIO_IO is defined |
pulls in the IO reactor |
FIO_NO_TLS is not defined |
opt-out guard |
| OpenSSL 3.x headers are present | HAVE_OPENSSL or __has_include("openssl/ssl.h") |
If OpenSSL headers are found but the version is older than 3.x
(OPENSSL_VERSION_MAJOR < 3), the module emits a compiler warning and
makes fio_openssl_io_functions() return fio_io_tls_default_functions(NULL)
(the current default, usually a no-op). Everything compiles; TLS simply does nothing.
A module-level constructor runs before main():
- Initialises the custom BIO methods.
- Calls
fio_io_tls_default_functions(&openssl_io_funcs)so every subsequentfio_io_listen/fio_io_connectcall with atlsargument uses OpenSSL. - Registers a
SIGPIPEmonitor (OpenSSL can trigger SIGPIPE on broken sockets). - Registers a
FIO_CALL_AT_EXITcleanup for the custom BIO method objects.
#Public API
#fio_openssl_io_functions
fio_io_functions_s fio_openssl_io_functions(void);Returns the fio_io_functions_s vtable that wires OpenSSL into the IO reactor:
| Field | Role |
|---|---|
build_context |
Converts fio_io_tls_s into an SSL_CTX wrapper |
free_context |
Deferred free of SSL_CTX and the fio_io_tls_s reference |
start |
Per-connection: SSL_new, custom BIO setup, initial handshake |
read |
Non-blocking decrypt: advances handshake if needed, then SSL_read_ex |
write |
Non-blocking encrypt: SSL_write_ex → encrypt → socket |
flush |
Sends any pending encrypted bytes from the internal buffer |
finish |
Sends TLS close_notify before the TCP close |
cleanup |
Frees the per-connection SSL object |
Normally you never call this directly — the constructor handles registration. Use it explicitly only when overriding the default or building a custom setup:
/* Override: switch back to OpenSSL after something else changed the default */
fio_io_functions_s openssl_funcs = fio_openssl_io_functions();
fio_io_tls_default_functions(&openssl_funcs);Or to set it on a specific protocol without touching the global default
(the io_functions field lives on fio_io_protocol_s):
fio_io_functions_s openssl_funcs = fio_openssl_io_functions();
MY_PROTOCOL.io_functions = openssl_funcs; /* per-protocol override */
fio_io_listen(.url = "0.0.0.0:8443",
.protocol = &MY_PROTOCOL,
.tls = tls);#Configuring TLS — fio_io_tls_s
TLS parameters are held in a fio_io_tls_s object defined in 401 io api.h.
Build one before calling fio_io_listen or fio_io_connect:
/* Allocate (reference counted) */
fio_io_tls_s *tls = fio_io_tls_new();
/* Certificate — PEM files */
fio_io_tls_cert_add(tls,
"www.example.com", /* server_name (SNI) */
"cert.pem", /* public certificate or chain */
"key.pem", /* private key */
NULL); /* PEM password, or NULL */
/* ALPN protocol negotiation */
fio_io_tls_alpn_add(tls, "h2", on_http2_selected);
fio_io_tls_alpn_add(tls, "http/1.1", on_http1_selected);
/* Peer certificate verification */
fio_io_tls_trust_add(tls, NULL); /* use system trust store */
fio_io_tls_trust_add(tls, "ca.pem"); /* or a specific CA bundle */
/* Listen */
fio_io_listen(.url = "0.0.0.0:443",
.protocol = &MY_PROTOCOL,
.tls = tls);
fio_io_tls_free(tls); /* release your reference; the listener holds its own */
fio_io_start(0);fio_io_tls_s is reference-counted (fio_io_tls_dup / fio_io_tls_free).
The backend duplicates the reference during build_context; freeing yours
after fio_io_listen is always safe.
#URL shorthand
The URL query string can also configure TLS without building the object manually:
fio_io_listen(.url = "0.0.0.0:443/?tls=./certs/", .protocol = &MY_PROTOCOL);See fio_io_tls_from_url in 401 io api.h for the query syntax.
#Certificates
#Loading from PEM files
When public_cert_file and private_key_file are both provided to
fio_io_tls_cert_add, the backend:
- Sets a PEM password callback if
pk_passwordis non-NULL. - Loads the certificate chain via
SSL_CTX_use_certificate_chain_file. - Loads the private key via
SSL_CTX_use_PrivateKey_file(PEM format). - Verifies key-cert consistency with
SSL_CTX_check_private_key.
Errors are logged with FIO_LOG_ERROR and the context build fails.
#Self-signed fallback
When a server has no certificates configured (or fio_io_tls_cert_add is
called with both file arguments as NULL), the backend generates a self-signed
ECDSA P-256 certificate on the fly:
| Property | Value |
|---|---|
| Key algorithm | ECDSA P-256 (128-bit security ≈ RSA-3072) |
| Key generation time | ~10 ms |
| Signature | SHA-256 |
| Validity | 180 days |
| Serial number | 128-bit cryptographically random |
| X.509 extensions | Basic Constraints (CA:FALSE), Key Usage, Extended Key Usage (serverAuth), SAN |
The private key is generated once per process (thread-safe, lock-protected) and
freed at exit via a FIO_CALL_AT_EXIT callback.
Self-signed certificates are fine for development. Browsers will warn. Use a CA-issued certificate (e.g. Let's Encrypt) in production.
#ALPN
Register protocols with fio_io_tls_alpn_add before listen/connect. The first
registered protocol is the preferred default.
When a client sends an ALPN extension, the backend walks the offered names in
order and picks the first that matches a registered protocol. On match it calls
fio_io_tls_alpn_select, which fires the on_selected callback for that
connection. If nothing matches, the handshake fails with a fatal alert.
Protocol names must be 1–255 bytes. The internal wire-format list is capped at 1 023 bytes total; more protocols than that will log an overflow error.
#Trust and Peer Verification
Trust configured with fio_io_tls_trust_add always refers to peer
verification: a client context verifies the server certificate, a server
context requests, requires, and verifies the client certificate (mutual TLS).
An empty trust list means no peer verification.
| Scenario | Behaviour |
|---|---|
fio_io_tls_trust_add called (any argument) |
SSL_VERIFY_PEER enabled |
NULL passed to fio_io_tls_trust_add |
system trust store is loaded (X509_STORE_set_default_paths) |
| Trust configured, server mode | SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT (client certificate required) |
| No trust certs added, client mode | SSL_VERIFY_NONE + FIO_LOG_SECURITY warning |
| No trust certs added, server mode | SSL_VERIFY_NONE (no client certificate requested) |
After the handshake, inspect the peer's certificate chain with
fio_io_peer_info_next(io, &info) (see the IO API docs). Each call
re-encodes the next chain certificate into a fixed per-connection staging
buffer (no allocation) and parses it into a fio_x509_cert_s, exposing the
subject, issuer, SAN entries, public key, validity, and a SHA-256
fingerprint. info.verified reflects OpenSSL's chain verification result
(SSL_get_verify_result).
#SSL Context Modes
The SSL_CTX is configured for non-blocking use:
SSL_MODE_ENABLE_PARTIAL_WRITE — SSL_write may return before all data encrypted
SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER — buffer pointer may differ between retries
SSL_MODE_RELEASE_BUFFERS — free idle 34 KB per-connection OpenSSL buffers
SSL_MODE_AUTO_RETRY — CLEARED (return immediately on WANT_READ/WRITE)TLS 1.3 session tickets: 2 tickets are configured for session resumption (saves ~30–50 % of handshake CPU on reconnects).
#Internal Design — Custom BIOs
Instead of BIO_s_mem() (which bounces data through a private BUF_MEM),
the backend uses custom BIO_METHOD objects that give OpenSSL direct access to
the per-connection buffers:
- rbio — OpenSSL reads raw socket bytes directly from a 64 KB receive
buffer that was filled by a single
fio_sock_readcall. - wbio — OpenSSL appends encrypted output directly into a ~66 KB encrypted-output buffer (4 × max TLS record).
This eliminates all intermediate copies while retaining full control over buffering and partial writes.
Handshake responses (ServerHello, etc.) are flushed to the socket from inside
the read callback — the IO layer's on_ready loop doesn't run until the
handshake completes, so the backend must send them itself.
#Lifecycle and Error Behavior
fio_io_listen / fio_io_connect
│
└─► build_context (SSL_CTX built once per listener/connector)
│
└─► per-connection start (SSL_new + BIO setup + SSL_accept/SSL_connect)
│
├─ read (advances handshake until SSL_is_init_finished,
│ then SSL_read_ex)
├─ write (SSL_write_ex, encrypt into wbio buffer, flush socket)
├─ flush (send remaining enc_buf bytes)
│
├─ finish (SSL_shutdown + best-effort send of close_notify)
└─ cleanup (SSL_free — also frees both BIOs)Error codes surfaced to the IO layer:
return 0— peer closed cleanly (SSL_ERROR_ZERO_RETURN) or fatal SSL error.return -1witherrno = EWOULDBLOCK— not enough data yet (normal event-loop signalling; the reactor will retry on next readable event).
All non-fatal internal issues are logged via FIO_LOG_ERROR /
FIO_LOG_WARNING / FIO_LOG_SECURITY. Debug-level detail is at
FIO_LOG_DDEBUG2.
#Minimal Server Example
#define FIO_LOG
#define FIO_IO
#include "fio-stl/include.h"
static void on_data(fio_io_s *io) {
char buf[4096];
size_t n = fio_io_read(io, buf, sizeof(buf));
if (n)
fio_io_write(io, buf, n); /* echo */
}
static fio_io_protocol_s ECHO_PROTO = {
.on_data = on_data,
.on_timeout = fio_io_touch,
};
int main(void) {
/* No certificate configured → self-signed ECDSA P-256 generated automatically */
fio_io_tls_s *tls = fio_io_tls_new();
fio_io_listen(.url = "0.0.0.0:8443",
.protocol = &ECHO_PROTO,
.tls = tls);
fio_io_tls_free(tls);
FIO_LOG_INFO("TLS echo server on :8443 (test: openssl s_client -connect localhost:8443)");
fio_io_start(0);
}For a production server, load real certificates:
fio_io_tls_cert_add(tls, "example.com", "cert.pem", "key.pem", NULL);#Disambiguation
| Document | Scope |
|---|---|
| This document | OpenSSL 3.x IO-layer backend — plugs TLS into the reactor |
| [./405 tls13.md](./405 tls13.md) (planned) | Native TLS 1.3 IO backend (same fio_io_functions_s interface, no OpenSSL dependency) |
| [./190 tls13.md](./190 tls13.md) | Standalone TLS 1.3 crypto library (key schedule, record layer, raw machinery) |
| [./400 io-overview.md](./400 io-overview.md) | IO + TLS stack overview |