mod_http3/src/h3_io.c¶
Functions¶
| Name | |
|---|---|
| int | h3_io_at_connection_limit(h3_io_t * io) |
| void | teardown(h3_io_t * io) |
| apr_status_t | h3_io_listen_start(apr_pool_t * pchild, server_rec * s, h3_server_conf * conf, int udp_fd) |
| void | h3_io_listen_stop(h3_io_t * io) |
| void | wait_for_event(h3_io_t * io) |
| void | remove_pending_handshake(h3_io_t * io, int index, int free_conn) |
| apr_status_t | spawn_serviced_session(h3_io_t * io, quic_conn * conn) |
| void | progress_pending_handshakes(h3_io_t * io) |
| int | prepare_accepted_connection(h3_io_t * io, quic_conn * conn) |
| int | service_session_pass(h3_io_t * io, h3_session * session) |
Attributes¶
| Name | |
|---|---|
| h3_io_t * | child_h3_io |
Functions Documentation¶
function h3_io_at_connection_limit¶
Parameters:
- io The h3_io_t instance to check.
Return: Non-zero if at the limit, zero otherwise.
Check if the active connection limit (H3MaxConnections) is reached.
function teardown¶
function h3_io_listen_start¶
apr_status_t h3_io_listen_start(
apr_pool_t * pchild,
server_rec * s,
h3_server_conf * conf,
int udp_fd
)
Parameters:
- pchild Child process pool.
- s The server_rec this listener is associated with.
- conf The vhost's h3_server_conf (cert/key paths, port).
- udp_fd Pre-opened non-blocking UDP socket bound to the listen port.
Return: APR_SUCCESS on success, APR_EAGAIN if the port is already owned, or another APR error code.
Build the listener, bind the UDP socket via udp_fd, and spawn the event thread. Idempotent on the same port: returns APR_EAGAIN if another child already owns it.
function h3_io_listen_stop¶
Parameters:
- io The h3_io_t to tear down.
Stop the event thread, join all worker threads, and release the UDP fd and engine. Safe to call with NULL.
function wait_for_event¶
Parameters:
- io The listener whose socket and wakeup pipe should be polled.
Wait for network read/write events using poll().
function remove_pending_handshake¶
Parameters:
- io The owning h3_io_t listener instance.
- index The index of the connection in the array.
- free_conn If non-zero, the connection object is freed.
Remove a connection from the pending handshake array.
function spawn_serviced_session¶
function progress_pending_handshakes¶
Parameters:
- io The owning h3_io_t listener instance.
Progress handshakes for all pending connections, timing out stalled connections and adding completed connections to the event loop.
function prepare_accepted_connection¶
Parameters:
- io The owning h3_io_t listener instance.
- conn The newly accepted QUIC connection instance.
Return: 1 on success, 0 otherwise.
Prepare a newly accepted connection before starting the handshake. Sets stream modes, Incoming Stream policies, and pushes it to the pending array.
function service_session_pass¶
Parameters:
- io The owning h3_io_t listener instance.
- session The h3_session to service.
Return: Non-zero when stream input made progress and another pass should run without polling.
Service the newly established session connection. Drives HTTP/3 request processing.
Attributes Documentation¶
variable child_h3_io¶
Source code¶
/*
* Copyright (c) 2026 The mod_http3 Project Authors. All rights reserved.
*
* SPDX-License-Identifier: Apache-2.0
*
* 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.
*/
#include <httpd.h>
#include <http_config.h>
#include <http_log.h>
#include <apr_allocator.h>
#include <apr_atomic.h>
#include <apr_pools.h>
#include <apr_portable.h>
#include <apr_thread_proc.h>
#include <apr_thread_pool.h>
#include <errno.h>
#include <poll.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "h3.h"
#include "h3_check.h"
#include "h3_io.h"
#include "h3_request.h"
#include "h3_session.h"
#include "h3_socket.h"
#include "h3_stream.h"
#include "h3_threads.h"
#include "h3_version.h"
#include "mod_http3.h"
#include "quic.h"
h3_io_t* child_h3_io = NULL;
int h3_io_at_connection_limit(h3_io_t* io)
{
h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module);
apr_uint32_t active = (apr_uint32_t)io->active_sessions->nelts + (apr_uint32_t)io->pending_handshakes->nelts;
return active >= conf->h3_max_connections;
}
static void teardown(h3_io_t* io)
{
CHECK(io);
if (io->event_thread)
{
io->thread_running = 0;
if (io->wakeup_pipe[1])
{
char wake = '1';
apr_size_t len = 1;
(void)apr_file_write(io->wakeup_pipe[1], &wake, &len);
}
apr_status_t status;
apr_thread_join(&status, io->event_thread);
io->event_thread = NULL;
}
if (io->h3_worker_pool)
{
apr_thread_pool_destroy(io->h3_worker_pool);
io->h3_worker_pool = NULL;
}
if (io->active_sessions)
{
apr_time_t next_warning = apr_time_now() + apr_time_from_sec(5);
while (io->active_sessions->nelts > 0)
{
if (apr_time_now() >= next_warning)
{
ap_log_error(APLOG_MARK, APLOG_WARNING, 0, io->server, "teardown waiting for %d connections to finish in-flight streams", io->active_sessions->nelts);
next_warning = apr_time_now() + apr_time_from_sec(5);
}
apr_sleep(50 * 1000);
}
}
if (io->qengine)
{
quic_engine_destroy(io->qengine);
io->qengine = NULL;
}
if (io->udp_fd >= 0)
{
h3_socket_close(io->udp_fd);
io->udp_fd = -1;
}
if (io->wakeup_pipe[0])
{
apr_file_close(io->wakeup_pipe[0]);
apr_file_close(io->wakeup_pipe[1]);
io->wakeup_pipe[0] = NULL;
io->wakeup_pipe[1] = NULL;
}
}
apr_status_t h3_io_listen_start(apr_pool_t* pchild, server_rec* s, h3_server_conf* conf, int udp_fd)
{
CHECK(pchild);
CHECK(s);
CHECK(conf);
h3_io_t* io = apr_pcalloc(pchild, sizeof(*io));
io->pool = pchild;
io->server = s;
io->udp_fd = udp_fd;
io->active_sessions = apr_array_make(pchild, 8, sizeof(h3_session*));
io->pending_handshakes = apr_array_make(pchild, 4, sizeof(h3_pending_handshake));
if (apr_file_pipe_create_ex(&io->wakeup_pipe[0], &io->wakeup_pipe[1], APR_FULL_NONBLOCK, pchild) != APR_SUCCESS)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "apr_file_pipe_create_ex failed");
return APR_EGENERAL;
}
if (apr_thread_pool_create(&io->h3_worker_pool, 16, 64, pchild) != APR_SUCCESS)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "apr_thread_pool_create failed");
return APR_EGENERAL;
}
char qerr[QUIC_ERRLEN] = {0};
quic_config qcfg = {
.cred = {.kind = QUIC_CRED_FILE, .as.file = {.cert_path = conf->h3_cert_path, .key_path = conf->h3_key_path}},
.callbacks = {.stream_acked = h3_session_on_stream_acked},
.io = &io->qio,
};
quic_settings_default(&qcfg.settings);
qcfg.settings.max_idle_timeout_ms = (uint64_t)conf->h3_idle_timeout * 1000;
qcfg.settings.address_validation = (conf->h3_address_validation != H3_FLAG_OFF);
quic_io_udp_init(&io->qio, udp_fd);
io->qengine = quic_engine_create(&qcfg, qerr, sizeof(qerr));
if (!io->qengine)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "QUIC engine initialization failed: %s", qerr);
teardown(io);
return APR_EGENERAL;
}
io->note_conn_added = APR_RETRIEVE_OPTIONAL_FN(ap_mpm_note_extra_connection_added);
io->note_conn_removed = APR_RETRIEVE_OPTIONAL_FN(ap_mpm_note_extra_connection_removed);
if (!io->note_conn_added || !io->note_conn_removed)
{
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, s, "active MPM '%s' lacks connection-count notifications; upgrade your httpd to a version that supports mod_http3", ap_show_mpm());
teardown(io);
return APR_EGENERAL;
}
io->thread_running = 1;
child_h3_io = io;
apr_threadattr_t* attr = NULL;
if (apr_threadattr_create(&attr, pchild) != APR_SUCCESS || apr_thread_create(&io->event_thread, attr, quic_event_thread, io, pchild) != APR_SUCCESS)
{
io->thread_running = 0;
teardown(io);
child_h3_io = NULL;
return APR_EGENERAL;
}
ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "mod_http3 loaded with version: %d (%s) on pid=%d port=%d", MOD_HTTP3_VERSION, MOD_HTTP3_VERSION_STRING, getpid(), (int)conf->h3_port);
return APR_SUCCESS;
}
void h3_io_listen_stop(h3_io_t* io)
{
if (io)
{
teardown(io);
if (child_h3_io == io)
{
child_h3_io = NULL;
}
}
}
void wait_for_event(h3_io_t* io)
{
if (!io)
{
return;
}
int want_read = 0;
int want_write = 0;
int timeout_ms = 1000;
quic_engine_want(io->qengine, &want_read, &want_write, &timeout_ms);
struct pollfd pfds[2] = {{.fd = io->udp_fd, .events = 0}, {.fd = -1, .events = POLLIN}};
nfds_t npfds = 1;
if (io->wakeup_pipe[0])
{
apr_os_file_t wakeup_fd = -1;
apr_os_file_get(&wakeup_fd, io->wakeup_pipe[0]);
pfds[1].fd = wakeup_fd;
npfds = 2;
}
if (want_read)
{
pfds[0].events |= POLLIN;
}
if (want_write)
{
pfds[0].events |= POLLOUT;
}
if (!pfds[0].events)
{
pfds[0].events = POLLIN;
}
if (poll(pfds, npfds, timeout_ms) < 0 && errno == EINTR)
{
return;
}
if (npfds == 2 && (pfds[1].revents & POLLIN))
{
char buf[64];
apr_size_t len = sizeof(buf);
(void)apr_file_read(io->wakeup_pipe[0], buf, &len);
}
}
void remove_pending_handshake(h3_io_t* io, int index, int free_conn)
{
h3_pending_handshake* pending = (h3_pending_handshake*)io->pending_handshakes->elts;
if (free_conn)
{
quic_conn_free(pending[index].conn);
}
if (index < io->pending_handshakes->nelts - 1)
{
pending[index] = pending[io->pending_handshakes->nelts - 1];
}
io->pending_handshakes->nelts--;
}
static apr_status_t spawn_serviced_session(h3_io_t* io, quic_conn* conn)
{
apr_allocator_t* allocator = NULL;
apr_pool_t* session_pool = NULL;
if (apr_allocator_create(&allocator) != APR_SUCCESS || apr_pool_create_ex(&session_pool, io->pool, NULL, allocator) != APR_SUCCESS)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "failed to create session pool for new connection");
if (allocator)
{
apr_allocator_destroy(allocator);
}
quic_conn_free(conn);
return APR_EGENERAL;
}
apr_allocator_owner_set(allocator, session_pool);
apr_pool_tag(session_pool, "h3_session");
h3_session* session = NULL;
if (h3_session_create(&session, io->server, conn, session_pool) != APR_SUCCESS)
{
quic_conn_free(conn);
apr_pool_destroy(session_pool);
return APR_EGENERAL;
}
quic_conn_set_user(conn, session);
if (h3_session_create_control_streams(session) != APR_SUCCESS)
{
h3_session_destroy(session);
return APR_EGENERAL;
}
if (quic_conn_is_closed(conn))
{
h3_session_destroy(session);
return APR_EGENERAL;
}
conn_rec* c = h3_synth_conn(session);
if (!c)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "h3_synth_conn failed");
h3_session_destroy(session);
return APR_EGENERAL;
}
*(h3_session**)apr_array_push(io->active_sessions) = session;
apr_atomic_inc32(&io->active_session_count);
if (io->note_conn_added)
{
io->note_conn_added();
}
return APR_SUCCESS;
}
void progress_pending_handshakes(h3_io_t* io)
{
CHECK(io);
h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module);
CHECK(conf);
apr_time_t now = apr_time_now();
apr_time_t timeout = apr_time_from_sec(conf->h3_handshake_timeout);
for (int i = 0; i < io->pending_handshakes->nelts;)
{
h3_pending_handshake* pending = &((h3_pending_handshake*)io->pending_handshakes->elts)[i];
quic_conn* conn = pending->conn;
if (now - pending->accepted_at >= timeout)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "QUIC handshake timed out after %u second(s)", (unsigned)conf->h3_handshake_timeout);
remove_pending_handshake(io, i, 1);
continue;
}
int finished = 0;
int rv = 0;
const char* why = NULL;
if (quic_conn_is_closed(conn))
{
rv = -1;
why = "peer closed the connection during the handshake";
}
else if (quic_conn_is_handshake_done(conn))
{
rv = 1;
}
if (rv == 1)
{
ap_log_error(APLOG_MARK, APLOG_INFO, 0, io->server, "QUIC handshake complete");
spawn_serviced_session(io, conn);
remove_pending_handshake(io, i, 0);
finished = 1;
}
else if (rv == -1)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "QUIC handshake did not complete: %s", why);
remove_pending_handshake(io, i, 1);
finished = 1;
}
if (!finished)
{
i++;
}
}
}
int prepare_accepted_connection(h3_io_t* io, quic_conn* conn)
{
h3_server_conf* conf = ap_get_module_config(io->server->module_config, &http3_module);
if (!quic_conn_prepare(conn, conf->h3_idle_timeout))
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, io->server, "quic_conn_prepare failed for accepted connection - dropping it");
return 0;
}
h3_pending_handshake* pending = (h3_pending_handshake*)apr_array_push(io->pending_handshakes);
pending->conn = conn;
pending->accepted_at = apr_time_now();
apr_atomic_inc32(&io->total_connections);
return 1;
}
int service_session_pass(h3_io_t* io, h3_session* session)
{
CHECK(io);
CHECK(session);
server_rec* s = session->s;
quic_conn* conn = session->qconn;
if (session->aborted)
{
return 0;
}
if (!io->thread_running)
{
apr_thread_mutex_lock(session->lock);
if (!session->goaway_deadline)
{
nghttp3_conn_submit_shutdown_notice(session->ngh3);
}
flush_nghttp3(session);
int drained = nghttp3_conn_is_drained2(session->ngh3);
apr_thread_mutex_unlock(session->lock);
if (!session->goaway_deadline)
{
session->goaway_deadline = apr_time_now() + apr_time_from_sec(H3_GOAWAY_GRACE_SECS);
ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "sent HTTP/3 GOAWAY; allowing up to %d more second(s) for in-flight streams", H3_GOAWAY_GRACE_SECS);
}
if (drained || apr_time_now() >= session->goaway_deadline)
{
apr_thread_mutex_lock(session->lock);
nghttp3_conn_shutdown(session->ngh3);
flush_nghttp3(session);
apr_thread_mutex_unlock(session->lock);
session->aborted = 1;
return 0;
}
}
if (quic_conn_is_closed(conn))
{
ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "QUIC connection terminated (idle timeout, peer close, or transport error)");
session->aborted = 1;
return 0;
}
apr_pool_t* scratch = NULL;
if (apr_pool_create(&scratch, session->pool) != APR_SUCCESS)
{
return 0;
}
for (quic_stream* s2 = NULL; (s2 = quic_conn_accept_stream(conn)) != NULL;)
{
apr_atomic_inc32(&io->total_streams);
int64_t sid = quic_stream_id(s2);
if (sid < 0)
{
quic_stream_free(s2);
continue;
}
apr_thread_mutex_lock(session->lock);
h3_stream* tracked = track_stream(session, sid, s2);
if (!tracked)
{
ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "track_stream failed for sid=%lld - freeing stream", (long long)sid);
quic_stream_free(s2);
}
apr_thread_mutex_unlock(session->lock);
}
int data_read = 0;
apr_thread_mutex_lock(session->lock);
apr_array_header_t* completed = drain_ready_streams(session, scratch, &data_read);
flush_nghttp3(session);
apr_thread_mutex_unlock(session->lock);
if (session->ngh3_dead)
{
session->aborted = 1;
apr_pool_destroy(scratch);
return 0;
}
for (int i = 0; i < completed->nelts; i++)
{
h3_stream* h3s = ((h3_stream**)completed->elts)[i];
h3_process_request(session, h3s);
}
apr_thread_mutex_lock(session->lock);
flush_nghttp3(session);
apr_thread_mutex_unlock(session->lock);
apr_pool_destroy(scratch);
return data_read;
}