Current section

Files

Jump to
hackney c_src quic_conn.c
Raw

c_src/quic_conn.c

/**
* quic_conn.c - QUIC connection resource management using lsquic
*
* This file is part of hackney released under the Apache 2 license.
* See the NOTICE for more information.
*
* Copyright (c) 2024-2026 Benoit Chesneau
*/
#include "quic_conn.h"
#include "atoms.h"
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <lsxpack_header.h>
/* Maximum UDP packet size */
#define MAX_UDP_PAYLOAD 1500
/* Buffer for incoming packets */
#define RECV_BUF_SIZE 65536
/* Global resource type for QUIC connections */
ErlNifResourceType *QUIC_CONN_RESOURCE = NULL;
/* Global initialization flag - use atomic to prevent race conditions */
static volatile int g_lsquic_initialized = 0;
static ErlNifMutex *g_init_mutex = NULL;
/* Forward declarations for lsquic callbacks */
static lsquic_conn_ctx_t *on_new_conn(void *stream_if_ctx, lsquic_conn_t *conn);
static void on_conn_closed(lsquic_conn_t *conn);
static lsquic_stream_ctx_t *on_new_stream(void *stream_if_ctx, lsquic_stream_t *s);
static void on_read(lsquic_stream_t *s, lsquic_stream_ctx_t *h);
static void on_write(lsquic_stream_t *s, lsquic_stream_ctx_t *h);
static void on_close(lsquic_stream_t *s, lsquic_stream_ctx_t *h);
static void on_hsk_done(lsquic_conn_t *conn, enum lsquic_hsk_status status);
static void on_goaway_received(lsquic_conn_t *conn);
/* Packet output callback */
static int packets_out(void *ctx, const struct lsquic_out_spec *specs, unsigned n_specs);
/* SSL context callback */
static SSL_CTX *get_ssl_ctx(void *peer_ctx, const struct sockaddr *local);
/* Header set interface callbacks */
static void *hsi_create_header_set(void *ctx, lsquic_stream_t *s, int is_push);
static struct lsxpack_header *hsi_prepare_decode(void *hdr_set,
struct lsxpack_header *hdr,
size_t space);
static int hsi_process_header(void *hdr_set, struct lsxpack_header *hdr);
static void hsi_discard_header_set(void *hdr_set);
/* Stream interface for lsquic */
static const struct lsquic_stream_if stream_if = {
.on_new_conn = on_new_conn,
.on_conn_closed = on_conn_closed,
.on_new_stream = on_new_stream,
.on_read = on_read,
.on_write = on_write,
.on_close = on_close,
.on_hsk_done = on_hsk_done,
.on_goaway_received = on_goaway_received,
};
/* Header set interface */
static const struct lsquic_hset_if hset_if = {
.hsi_create_header_set = hsi_create_header_set,
.hsi_prepare_decode = hsi_prepare_decode,
.hsi_process_header = hsi_process_header,
.hsi_discard_header_set = hsi_discard_header_set,
.hsi_flags = 0,
};
/*===========================================================================
* Utility functions
*===========================================================================*/
/* Get current time in microseconds */
static uint64_t get_time_us(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return (uint64_t)tv.tv_sec * 1000000 + tv.tv_usec;
}
/* Set socket to non-blocking mode */
static int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/* Create UDP socket and bind */
static int create_udp_socket(int family, struct sockaddr_storage *local_addr,
socklen_t *local_addrlen) {
int fd = socket(family, SOCK_DGRAM, 0);
if (fd < 0) return -1;
if (set_nonblocking(fd) < 0) {
close(fd);
return -1;
}
/* Enable address reuse */
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
/* Bind to any available port */
if (family == AF_INET) {
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = 0;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
} else {
struct sockaddr_in6 addr = {0};
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;
addr.sin6_port = 0;
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
}
/* Get the bound address */
*local_addrlen = sizeof(*local_addr);
if (getsockname(fd, (struct sockaddr *)local_addr, local_addrlen) < 0) {
close(fd);
return -1;
}
return fd;
}
/* Resolve hostname to sockaddr */
static int resolve_hostname(const char *hostname, uint16_t port,
struct sockaddr_storage *addr, socklen_t *addrlen) {
struct addrinfo hints = {0};
struct addrinfo *res;
char port_str[6];
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
snprintf(port_str, sizeof(port_str), "%u", port);
int ret = getaddrinfo(hostname, port_str, &hints, &res);
if (ret != 0) {
return -1;
}
memcpy(addr, res->ai_addr, res->ai_addrlen);
*addrlen = res->ai_addrlen;
freeaddrinfo(res);
return 0;
}
/*===========================================================================
* Global initialization
*===========================================================================*/
int quic_global_init(void) {
/* Fast path - already initialized */
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_ACQUIRE)) return 0;
/* Create mutex on first call - this is safe because NIF load is single-threaded */
if (!g_init_mutex) {
g_init_mutex = enif_mutex_create("quic_global_init_mutex");
if (!g_init_mutex) return -1;
}
/* Double-checked locking */
enif_mutex_lock(g_init_mutex);
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_RELAXED)) {
enif_mutex_unlock(g_init_mutex);
return 0;
}
if (lsquic_global_init(LSQUIC_GLOBAL_CLIENT) != 0) {
enif_mutex_unlock(g_init_mutex);
return -1;
}
__atomic_store_n(&g_lsquic_initialized, 1, __ATOMIC_RELEASE);
enif_mutex_unlock(g_init_mutex);
return 0;
}
void quic_global_cleanup(void) {
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_ACQUIRE)) {
if (g_init_mutex) {
enif_mutex_lock(g_init_mutex);
}
if (__atomic_load_n(&g_lsquic_initialized, __ATOMIC_RELAXED)) {
lsquic_global_cleanup();
__atomic_store_n(&g_lsquic_initialized, 0, __ATOMIC_RELEASE);
}
if (g_init_mutex) {
enif_mutex_unlock(g_init_mutex);
enif_mutex_destroy(g_init_mutex);
g_init_mutex = NULL;
}
}
}
/*===========================================================================
* SSL context
*===========================================================================*/
static SSL_CTX *create_ssl_ctx(void) {
SSL_CTX *ctx = SSL_CTX_new(TLS_client_method());
if (!ctx) return NULL;
/* Set minimum TLS version to 1.3 (required for QUIC) */
SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION);
SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);
/* Set default verify paths */
SSL_CTX_set_default_verify_paths(ctx);
/* For now, skip verification (TODO: make configurable) */
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
return ctx;
}
static SSL_CTX *get_ssl_ctx(void *peer_ctx, const struct sockaddr *local) {
UNUSED(local);
QuicConn *conn = (QuicConn *)peer_ctx;
return conn->ssl_ctx;
}
/*===========================================================================
* lsquic stream interface callbacks
*===========================================================================*/
static lsquic_conn_ctx_t *on_new_conn(void *stream_if_ctx, lsquic_conn_t *c) {
QuicConn *conn = (QuicConn *)stream_if_ctx;
conn->conn = c;
return (lsquic_conn_ctx_t *)conn;
}
static void on_conn_closed(lsquic_conn_t *c) {
lsquic_conn_ctx_t *ctx = lsquic_conn_get_ctx(c);
if (!ctx) return;
QuicConn *conn = (QuicConn *)ctx;
/* Note: Don't use mutex here - called from I/O thread which may hold it */
conn->state = QUIC_CONN_CLOSED;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (!env) return;
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_CLOSED, enif_make_atom(env, "peer_closed")));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
static void on_hsk_done(lsquic_conn_t *c, enum lsquic_hsk_status status) {
lsquic_conn_ctx_t *ctx = lsquic_conn_get_ctx(c);
if (!ctx) return;
QuicConn *conn = (QuicConn *)ctx;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (!env) return;
if (status == LSQ_HSK_OK || status == LSQ_HSK_RESUMED_OK) {
/* Note: Don't use mutex here - we're called from I/O thread which holds it */
conn->state = QUIC_CONN_CONNECTED;
/* Notify owner of successful connection */
ERL_NIF_TERM info = enif_make_new_map(env);
enif_make_map_put(env, info,
enif_make_atom(env, "resumed"),
status == LSQ_HSK_RESUMED_OK ? ATOM_TRUE : ATOM_FALSE,
&info);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_CONNECTED, info));
enif_send(NULL, &conn->owner_pid, env, msg);
} else {
/* Handshake failed */
/* Note: Don't use mutex here - we're called from I/O thread which holds it */
conn->state = QUIC_CONN_CLOSED;
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_CLOSED,
enif_make_atom(env, "handshake_failed")));
enif_send(NULL, &conn->owner_pid, env, msg);
}
enif_free_env(env);
}
static void on_goaway_received(lsquic_conn_t *c) {
lsquic_conn_ctx_t *ctx = lsquic_conn_get_ctx(c);
if (!ctx) return;
QuicConn *conn = (QuicConn *)ctx;
/* Note: Don't use mutex here - called from I/O thread which may hold it */
conn->state = QUIC_CONN_DRAINING;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (!env) return;
ErlNifBinary empty_bin;
enif_alloc_binary(0, &empty_bin);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_GOAWAY,
enif_make_int64(env, 0), /* LastStreamId - not available in callback */
enif_make_int64(env, 0), /* ErrorCode */
enif_make_binary(env, &empty_bin)));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
static lsquic_stream_ctx_t *on_new_stream(void *stream_if_ctx, lsquic_stream_t *s) {
QuicConn *conn = (QuicConn *)stream_if_ctx;
if (!s) return NULL;
/* Create stream context */
QuicStream *stream = enif_alloc(sizeof(QuicStream));
if (!stream) return NULL;
memset(stream, 0, sizeof(QuicStream));
stream->stream_id = lsquic_stream_id(s);
stream->stream = s; /* Store lsquic stream handle */
stream->conn = conn;
/* Add to connection's stream list (no mutex - called from I/O thread) */
stream->next = conn->streams;
conn->streams = stream;
/* Want to read headers/data */
lsquic_stream_wantread(s, 1);
/* Notify Erlang of the new stream */
ErlNifEnv *env = enif_alloc_env();
if (env) {
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_STREAM_OPENED,
enif_make_int64(env, stream->stream_id)));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
return (lsquic_stream_ctx_t *)stream;
}
static void on_read(lsquic_stream_t *s, lsquic_stream_ctx_t *h) {
if (!h) return;
QuicStream *stream = (QuicStream *)h;
QuicConn *conn = stream->conn;
/* Check if we need to read headers first */
if (!stream->headers_received) {
void *hset = lsquic_stream_get_hset(s);
if (hset) {
QuicHeaderSet *header_set = (QuicHeaderSet *)hset;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (env) {
/* Copy headers list from header_set env to new env */
ERL_NIF_TERM headers = enif_make_copy(env, header_set->headers_list);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_STREAM_HEADERS,
enif_make_int64(env, stream->stream_id),
headers,
ATOM_FALSE)); /* fin flag */
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
/* Clean up header set */
enif_free_env(header_set->env);
enif_free(header_set);
stream->headers_received = true;
}
}
/* Read data */
unsigned char buf[16384];
ssize_t nread;
while ((nread = lsquic_stream_read(s, buf, sizeof(buf))) > 0) {
ErlNifEnv *env = enif_alloc_env();
if (!env) continue;
ErlNifBinary bin;
enif_alloc_binary(nread, &bin);
memcpy(bin.data, buf, nread);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_STREAM_DATA,
enif_make_int64(env, stream->stream_id),
enif_make_binary(env, &bin),
ATOM_FALSE));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
if (nread == 0) {
/* EOF - FIN received */
stream->fin_received = true;
ErlNifEnv *env = enif_alloc_env();
if (env) {
ErlNifBinary empty_bin;
enif_alloc_binary(0, &empty_bin);
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple4(env, ATOM_STREAM_DATA,
enif_make_int64(env, stream->stream_id),
enif_make_binary(env, &empty_bin),
ATOM_TRUE));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
lsquic_stream_wantread(s, 0);
} else if (nread < 0 && errno != EWOULDBLOCK) {
/* Error */
lsquic_stream_wantread(s, 0);
}
}
static void on_write(lsquic_stream_t *s, lsquic_stream_ctx_t *h) {
if (!h) return;
QuicStream *stream = (QuicStream *)h;
QuicConn *conn = stream->conn;
/* Use a fresh environment for thread-safe message sending */
ErlNifEnv *env = enif_alloc_env();
if (env) {
ERL_NIF_TERM msg = enif_make_tuple3(env,
ATOM_QUIC,
enif_make_resource(env, conn),
enif_make_tuple2(env, ATOM_SEND_READY,
enif_make_int64(env, stream->stream_id)));
enif_send(NULL, &conn->owner_pid, env, msg);
enif_free_env(env);
}
/* Don't want write events until explicitly requested */
lsquic_stream_wantwrite(s, 0);
}
static void on_close(lsquic_stream_t *s, lsquic_stream_ctx_t *h) {
if (!h) return;
QuicStream *stream = (QuicStream *)h;
QuicConn *conn = stream->conn;
/* Remove from connection's stream list (no mutex - called from I/O thread) */
QuicStream **pp = &conn->streams;
while (*pp) {
if (*pp == stream) {
*pp = stream->next;
break;
}
pp = &(*pp)->next;
}
enif_free(stream);
}
/*===========================================================================
* Header set interface callbacks
*===========================================================================*/
static void *hsi_create_header_set(void *ctx, lsquic_stream_t *s, int is_push) {
UNUSED(is_push);
QuicConn *conn = (QuicConn *)ctx;
QuicHeaderSet *hset = enif_alloc(sizeof(QuicHeaderSet));
if (!hset) return NULL;
hset->env = enif_alloc_env();
if (!hset->env) {
enif_free(hset);
return NULL;
}
hset->headers_list = enif_make_list(hset->env, 0);
hset->conn = conn;
hset->stream_id = s ? (int64_t)lsquic_stream_id(s) : (int64_t)-1;
return hset;
}
static struct lsxpack_header *hsi_prepare_decode(void *hdr_set,
struct lsxpack_header *hdr,
size_t space) {
UNUSED(hdr_set);
if (hdr) {
/* Resize existing header - use enif_alloc since we can't realloc */
if (space > LSXPACK_MAX_STRLEN) return NULL;
char *new_buf = enif_alloc(space);
if (!new_buf) return NULL;
/* Copy existing data if any */
if (hdr->buf && hdr->val_len > 0) {
size_t copy_len = hdr->val_len < space ? hdr->val_len : space;
memcpy(new_buf, hdr->buf, copy_len);
enif_free(hdr->buf);
}
hdr->buf = new_buf;
hdr->val_len = space;
return hdr;
}
/* Create new header */
struct lsxpack_header *new_hdr = enif_alloc(sizeof(struct lsxpack_header));
if (!new_hdr) return NULL;
memset(new_hdr, 0, sizeof(*new_hdr));
new_hdr->buf = enif_alloc(space);
if (!new_hdr->buf) {
enif_free(new_hdr);
return NULL;
}
lsxpack_header_prepare_decode(new_hdr, new_hdr->buf, 0, space);
return new_hdr;
}
static int hsi_process_header(void *hdr_set, struct lsxpack_header *hdr) {
if (!hdr) {
return 0; /* End of headers */
}
QuicHeaderSet *hs = (QuicHeaderSet *)hdr_set;
/* Extract name and value */
const char *name = lsxpack_header_get_name(hdr);
size_t name_len = hdr->name_len;
const char *value = lsxpack_header_get_value(hdr);
size_t value_len = hdr->val_len;
/* Create Erlang binaries */
ErlNifBinary name_bin, value_bin;
enif_alloc_binary(name_len, &name_bin);
enif_alloc_binary(value_len, &value_bin);
memcpy(name_bin.data, name, name_len);
memcpy(value_bin.data, value, value_len);
/* Create {Name, Value} tuple and prepend to list */
ERL_NIF_TERM tuple = enif_make_tuple2(hs->env,
enif_make_binary(hs->env, &name_bin),
enif_make_binary(hs->env, &value_bin));
hs->headers_list = enif_make_list_cell(hs->env, tuple, hs->headers_list);
/* Free the header buffer */
enif_free(hdr->buf);
enif_free(hdr);
return 0;
}
static void hsi_discard_header_set(void *hdr_set) {
if (!hdr_set) return;
QuicHeaderSet *hs = (QuicHeaderSet *)hdr_set;
if (hs->env) enif_free_env(hs->env);
enif_free(hs);
}
/*===========================================================================
* Packet output
*===========================================================================*/
static int packets_out(void *ctx, const struct lsquic_out_spec *specs,
unsigned n_specs) {
QuicConn *conn = (QuicConn *)ctx;
unsigned n;
for (n = 0; n < n_specs; n++) {
struct msghdr msg = {0};
msg.msg_name = (void *)specs[n].dest_sa;
msg.msg_namelen = specs[n].dest_sa->sa_family == AF_INET ?
sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
msg.msg_iov = specs[n].iov;
msg.msg_iovlen = specs[n].iovlen;
ssize_t sent = sendmsg(conn->sockfd, &msg, 0);
if (sent < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
return n;
}
return -1;
}
}
return n;
}
/*===========================================================================
* Connection processing (called from dirty scheduler)
*===========================================================================*/
int quic_conn_process(QuicConn *conn, ErlNifEnv *env) {
UNUSED(env);
if (!conn || __atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
return -1;
}
/* Mark select as not armed (since enif_select is one-shot) */
conn->select_armed = false;
enif_mutex_lock(conn->mutex);
if (!conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Receive any pending packets */
unsigned char buf[RECV_BUF_SIZE];
struct sockaddr_storage peer_addr;
socklen_t peer_addrlen;
ssize_t nread;
while ((nread = recvfrom(conn->sockfd, buf, sizeof(buf), MSG_DONTWAIT,
(struct sockaddr *)&peer_addr, &peer_addrlen)) > 0) {
peer_addrlen = sizeof(peer_addr); /* Reset for next iteration */
lsquic_engine_packet_in(conn->engine, buf, nread,
(struct sockaddr *)&conn->local_addr,
(struct sockaddr *)&peer_addr,
conn, 0);
}
/* Process connections - triggers callbacks */
lsquic_engine_process_conns(conn->engine);
/* Get next timeout */
int diff = 0;
int has_tick = lsquic_engine_earliest_adv_tick(conn->engine, &diff);
if (has_tick) {
conn->next_timeout_us = get_time_us() + (uint64_t)diff;
} else {
conn->next_timeout_us = 0;
}
/* Send any pending packets */
if (lsquic_engine_has_unsent_packets(conn->engine)) {
lsquic_engine_send_unsent_packets(conn->engine);
}
enif_mutex_unlock(conn->mutex);
/* Return timeout in ms, or -1 for infinity */
return has_tick ? (diff / 1000) : -1;
}
/*===========================================================================
* Resource management
*===========================================================================*/
void quic_conn_resource_dtor(ErlNifEnv *env, void *obj) {
UNUSED(env);
QuicConn *conn = (QuicConn *)obj;
quic_conn_destroy(conn);
}
int quic_conn_resource_init(ErlNifEnv *env) {
ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
QUIC_CONN_RESOURCE = enif_open_resource_type(
env,
NULL,
"quic_conn",
quic_conn_resource_dtor,
flags,
NULL
);
if (!QUIC_CONN_RESOURCE) return 0;
/* Initialize lsquic globally */
return quic_global_init() == 0;
}
QuicConn *quic_conn_create(ErlNifEnv *env, ErlNifPid owner_pid) {
UNUSED(env);
QuicConn *conn = enif_alloc_resource(QUIC_CONN_RESOURCE, sizeof(QuicConn));
if (!conn) return NULL;
memset(conn, 0, sizeof(QuicConn));
conn->sockfd = -1;
conn->owner_pid = owner_pid;
conn->state = QUIC_CONN_IDLE;
conn->ref_count = 1;
conn->mutex = enif_mutex_create("quic_conn_mutex");
if (!conn->mutex) {
enif_release_resource(conn);
return NULL;
}
return conn;
}
int quic_conn_connect(QuicConn *conn, const char *hostname, uint16_t port,
int sockfd, const struct sockaddr *local_addr, socklen_t local_addrlen) {
if (!conn || !hostname) return -1;
/* Store hostname */
conn->hostname = strdup(hostname);
if (!conn->hostname) return -1;
conn->port = port;
/* Resolve hostname */
if (resolve_hostname(hostname, port, &conn->remote_addr, &conn->remote_addrlen) < 0) {
return -1;
}
/* Use provided socket or create new one */
if (sockfd >= 0) {
/* Use external socket - ensure non-blocking */
set_nonblocking(sockfd);
conn->sockfd = sockfd;
/* Use provided local address or get from socket */
if (local_addr && local_addrlen > 0) {
memcpy(&conn->local_addr, local_addr, local_addrlen);
conn->local_addrlen = local_addrlen;
} else {
conn->local_addrlen = sizeof(conn->local_addr);
if (getsockname(sockfd, (struct sockaddr *)&conn->local_addr,
&conn->local_addrlen) < 0) {
return -1;
}
}
} else {
/* Create new UDP socket */
int family = conn->remote_addr.ss_family;
conn->sockfd = create_udp_socket(family, &conn->local_addr, &conn->local_addrlen);
if (conn->sockfd < 0) {
return -1;
}
}
/* Create SSL context */
conn->ssl_ctx = create_ssl_ctx();
if (!conn->ssl_ctx) {
close(conn->sockfd);
conn->sockfd = -1;
return -1;
}
/* Create lsquic engine */
struct lsquic_engine_settings settings;
lsquic_engine_init_settings(&settings, LSENG_HTTP);
/* Use only IETF QUIC v1 */
settings.es_versions = (1 << LSQVER_I001);
struct lsquic_engine_api api = {0};
api.ea_settings = &settings;
api.ea_stream_if = &stream_if;
api.ea_stream_if_ctx = conn;
api.ea_packets_out = packets_out;
api.ea_packets_out_ctx = conn;
api.ea_get_ssl_ctx = get_ssl_ctx;
api.ea_hsi_if = &hset_if;
api.ea_hsi_ctx = conn;
conn->engine = lsquic_engine_new(LSENG_HTTP, &api);
if (!conn->engine) {
SSL_CTX_free(conn->ssl_ctx);
conn->ssl_ctx = NULL;
close(conn->sockfd);
conn->sockfd = -1;
return -1;
}
/* Initiate connection */
conn->state = QUIC_CONN_HANDSHAKING;
lsquic_conn_t *lconn = lsquic_engine_connect(
conn->engine,
LSQVER_I001, /* QUIC v1 (RFC 9000) */
(struct sockaddr *)&conn->local_addr,
(struct sockaddr *)&conn->remote_addr,
conn, /* peer_ctx */
(lsquic_conn_ctx_t *)conn, /* conn_ctx */
hostname, /* SNI */
0, /* base_plpmtu - let engine decide */
NULL, 0, /* session resumption */
NULL, 0 /* token */
);
if (!lconn) {
lsquic_engine_destroy(conn->engine);
conn->engine = NULL;
SSL_CTX_free(conn->ssl_ctx);
conn->ssl_ctx = NULL;
close(conn->sockfd);
conn->sockfd = -1;
conn->state = QUIC_CONN_IDLE;
return -1;
}
conn->conn = lconn;
/* Process to send initial packets */
lsquic_engine_process_conns(conn->engine);
/* Connection is ready - caller will use enif_select() for I/O notifications */
/* and call quic_conn_process() from a dirty scheduler */
return 0;
}
void quic_conn_keep(QuicConn *conn) {
if (!conn) return;
/* Don't keep a destroyed connection */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
return;
}
enif_mutex_lock(conn->mutex);
conn->ref_count++;
enif_mutex_unlock(conn->mutex);
enif_keep_resource(conn);
}
void quic_conn_release(QuicConn *conn) {
if (!conn) return;
/* Check if already destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) {
enif_release_resource(conn);
return;
}
enif_mutex_lock(conn->mutex);
int ref = --conn->ref_count;
enif_mutex_unlock(conn->mutex);
if (ref <= 0) {
/* Don't call destroy here - let the Erlang GC call the destructor */
/* The destructor will call quic_conn_destroy */
}
enif_release_resource(conn);
}
void quic_conn_destroy(QuicConn *conn) {
if (!conn) return;
/* Atomically check and set destroyed flag to prevent double-free */
int expected = 0;
if (!__atomic_compare_exchange_n(&conn->destroyed, &expected, 1,
0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
/* Already destroyed or being destroyed */
return;
}
/* Stop select notifications if armed */
if (conn->select_armed && conn->sockfd >= 0) {
/* Note: enif_select with STOP flag cancels notifications */
/* This is called from destructor, so env may not be valid */
conn->select_armed = false;
}
/* Close lsquic connection */
if (conn->conn && conn->engine) {
lsquic_conn_close(conn->conn);
lsquic_engine_process_conns(conn->engine);
conn->conn = NULL;
}
/* Destroy lsquic engine */
if (conn->engine) {
lsquic_engine_destroy(conn->engine);
conn->engine = NULL;
}
/* Free SSL context */
if (conn->ssl_ctx) {
SSL_CTX_free(conn->ssl_ctx);
conn->ssl_ctx = NULL;
}
/* Close socket */
if (conn->sockfd >= 0) {
close(conn->sockfd);
conn->sockfd = -1;
}
/* Free hostname */
if (conn->hostname) {
free(conn->hostname);
conn->hostname = NULL;
}
/* Free session ticket */
if (conn->session_ticket) {
free(conn->session_ticket);
conn->session_ticket = NULL;
}
/* Free streams */
QuicStream *stream = conn->streams;
while (stream) {
QuicStream *next = stream->next;
enif_free(stream);
stream = next;
}
conn->streams = NULL;
/* Free mutex */
if (conn->mutex) {
enif_mutex_destroy(conn->mutex);
conn->mutex = NULL;
}
}
/*===========================================================================
* Stream operations
*===========================================================================*/
/* Find a stream by its ID */
static QuicStream *find_stream(QuicConn *conn, int64_t stream_id) {
QuicStream *stream = conn->streams;
while (stream) {
if (stream->stream_id == stream_id) {
return stream;
}
stream = stream->next;
}
return NULL;
}
int64_t quic_conn_open_stream(QuicConn *conn) {
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Remember the current head of streams list */
QuicStream *prev_head = conn->streams;
/* Request a new stream - callback will be triggered synchronously */
lsquic_conn_make_stream(conn->conn);
lsquic_engine_process_conns(conn->engine);
/* on_new_stream adds new stream at head of list */
int64_t stream_id = -1;
if (conn->streams && conn->streams != prev_head) {
stream_id = conn->streams->stream_id;
}
enif_mutex_unlock(conn->mutex);
return stream_id;
}
int quic_conn_close(QuicConn *conn) {
if (!conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->conn && conn->engine && conn->state != QUIC_CONN_CLOSED) {
conn->state = QUIC_CONN_DRAINING;
lsquic_conn_close(conn->conn);
lsquic_engine_process_conns(conn->engine);
}
enif_mutex_unlock(conn->mutex);
return 0;
}
/* Maximum headers we support in a single send */
#define MAX_HEADERS 64
int quic_conn_send_headers(QuicConn *conn, int64_t stream_id,
ErlNifEnv *env, ERL_NIF_TERM headers_list, bool fin) {
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
QuicStream *stream = find_stream(conn, stream_id);
if (!stream || !stream->stream) {
enif_mutex_unlock(conn->mutex);
return -1;
}
if (stream->headers_sent) {
enif_mutex_unlock(conn->mutex);
return -1; /* Headers already sent */
}
/* Count headers and calculate buffer size */
unsigned count = 0;
size_t total_buf_size = 0;
ERL_NIF_TERM list = headers_list;
ERL_NIF_TERM head;
while (enif_get_list_cell(env, list, &head, &list)) {
int arity;
const ERL_NIF_TERM *tuple;
if (!enif_get_tuple(env, head, &arity, &tuple) || arity != 2) {
enif_mutex_unlock(conn->mutex);
return -1;
}
ErlNifBinary name_bin, value_bin;
if (!enif_inspect_binary(env, tuple[0], &name_bin) &&
!enif_inspect_iolist_as_binary(env, tuple[0], &name_bin)) {
enif_mutex_unlock(conn->mutex);
return -1;
}
if (!enif_inspect_binary(env, tuple[1], &value_bin) &&
!enif_inspect_iolist_as_binary(env, tuple[1], &value_bin)) {
enif_mutex_unlock(conn->mutex);
return -1;
}
total_buf_size += name_bin.size + value_bin.size;
count++;
if (count >= MAX_HEADERS) break;
}
if (count == 0) {
enif_mutex_unlock(conn->mutex);
return -1; /* Empty headers */
}
/* Allocate buffer and headers array */
char *buf = enif_alloc(total_buf_size);
struct lsxpack_header *headers = enif_alloc(count * sizeof(struct lsxpack_header));
if (!buf || !headers) {
if (buf) enif_free(buf);
if (headers) enif_free(headers);
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Fill in headers */
list = headers_list;
size_t offset = 0;
unsigned i = 0;
while (enif_get_list_cell(env, list, &head, &list) && i < count) {
int arity;
const ERL_NIF_TERM *tuple;
enif_get_tuple(env, head, &arity, &tuple);
ErlNifBinary name_bin, value_bin;
enif_inspect_iolist_as_binary(env, tuple[0], &name_bin);
enif_inspect_iolist_as_binary(env, tuple[1], &value_bin);
/* Copy name and value to buffer */
size_t name_offset = offset;
memcpy(buf + offset, name_bin.data, name_bin.size);
offset += name_bin.size;
size_t val_offset = offset;
memcpy(buf + offset, value_bin.data, value_bin.size);
offset += value_bin.size;
/* Set up the header */
lsxpack_header_set_offset2(&headers[i], buf,
name_offset, name_bin.size,
val_offset, value_bin.size);
i++;
}
/* Create HTTP headers struct */
lsquic_http_headers_t http_headers = {
.count = count,
.headers = headers
};
/* Send headers */
int ret = lsquic_stream_send_headers(stream->stream, &http_headers, fin);
enif_free(buf);
enif_free(headers);
if (ret == 0) {
stream->headers_sent = true;
if (fin) {
stream->fin_sent = true;
/* Shutdown write side to send FIN */
lsquic_stream_shutdown(stream->stream, 1); /* 1 = write side */
}
/* Flush the stream to ensure headers are sent */
lsquic_stream_flush(stream->stream);
/* Re-enable read after sending request headers */
lsquic_stream_wantread(stream->stream, 1);
}
/* Process to send the headers packet */
lsquic_engine_process_conns(conn->engine);
/* Send any packets immediately */
if (lsquic_engine_has_unsent_packets(conn->engine)) {
lsquic_engine_send_unsent_packets(conn->engine);
}
enif_mutex_unlock(conn->mutex);
return ret;
}
int quic_conn_send_data(QuicConn *conn, int64_t stream_id,
const uint8_t *data, size_t len, bool fin) {
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
QuicStream *stream = find_stream(conn, stream_id);
if (!stream || !stream->stream) {
enif_mutex_unlock(conn->mutex);
return -1;
}
ssize_t written = lsquic_stream_write(stream->stream, data, len);
if (written < 0) {
enif_mutex_unlock(conn->mutex);
return -1;
}
if (fin) {
lsquic_stream_shutdown(stream->stream, 1); /* Shutdown write side */
stream->fin_sent = true;
}
lsquic_engine_process_conns(conn->engine);
enif_mutex_unlock(conn->mutex);
return (int)written;
}
int quic_conn_reset_stream(QuicConn *conn, int64_t stream_id, uint64_t error_code) {
UNUSED(error_code); /* lsquic doesn't have a reset with error code API */
if (!conn || !conn->conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
if (conn->state != QUIC_CONN_CONNECTED || !conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
QuicStream *stream = find_stream(conn, stream_id);
if (!stream || !stream->stream) {
enif_mutex_unlock(conn->mutex);
return -1;
}
/* Close the stream (lsquic doesn't have a reset with error code) */
lsquic_stream_close(stream->stream);
lsquic_engine_process_conns(conn->engine);
enif_mutex_unlock(conn->mutex);
return 0;
}
int64_t quic_conn_handle_timeout(QuicConn *conn) {
if (!conn || !conn->engine) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
/* Double-check engine after acquiring mutex */
if (!conn->engine) {
enif_mutex_unlock(conn->mutex);
return -1;
}
lsquic_engine_process_conns(conn->engine);
int diff;
int has_tick = lsquic_engine_earliest_adv_tick(conn->engine, &diff);
int64_t result = has_tick ? diff / 1000 : -1; /* Convert us to ms */
enif_mutex_unlock(conn->mutex);
return result;
}
int quic_conn_peername(QuicConn *conn, struct sockaddr_storage *addr, socklen_t *addrlen) {
if (!conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
memcpy(addr, &conn->remote_addr, conn->remote_addrlen);
*addrlen = conn->remote_addrlen;
enif_mutex_unlock(conn->mutex);
return 0;
}
int quic_conn_sockname(QuicConn *conn, struct sockaddr_storage *addr, socklen_t *addrlen) {
if (!conn) return -1;
/* Check if connection is destroyed */
if (__atomic_load_n(&conn->destroyed, __ATOMIC_ACQUIRE)) return -1;
enif_mutex_lock(conn->mutex);
memcpy(addr, &conn->local_addr, conn->local_addrlen);
*addrlen = conn->local_addrlen;
enif_mutex_unlock(conn->mutex);
return 0;
}