Skip to content

mod_http3: bind the QUIC socket in the parent, one SO_REUSEPORT socket per listener bucket - #949

Open
xl32 wants to merge 5 commits into
apache:trunkfrom
xl32:h3-parent-socket
Open

xl32 wants to merge 5 commits into
apache:trunkfrom
xl32:h3-parent-socket

Conversation

@xl32

@xl32 xl32 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Depends on #948. This branch is stacked on it, so until #948 is merged the diff also shows its two commits. The three commits of this PR are the last three.

1. Bind the QUIC socket in the parent, before the privilege drop

Each child binds the UDP port itself after httpd drops root. With the usual Listen 443 and children running as daemon/apache, that bind fails unless the httpd binary has CAP_NET_BIND_SERVICE, so HTTP/3 does not come up on 443.

On Linux the parent now binds the socket in post_config, while it still runs as root, and keeps it in the process pool, so it stays bound across graceful restarts. Children inherit it. One child at a time serves it: the child that binds an abstract Unix socket name owns the port, and the kernel releases the name when that child exits, so the next child takes over. Other platforms keep the per-child bind.

2. Bind one SO_REUSEPORT QUIC socket per httpd listener bucket

With ListenCoresBucketsRatio, httpd splits its TCP listeners into buckets. The parent now binds one SO_REUSEPORT UDP socket per bucket, and each child serves the first bucket no other child owns, so more than one child serves HTTP/3. The parent keeps the sockets while children start and stop, so the kernel's reuseport hash does not change and each peer stays on the same socket. Without buckets there is one socket, as before.

3. Count the QUIC buckets from ListenCoresBucketsRatio in post_config

Some MPMs count their listener buckets in the run step, after post_config, so ap_num_listen_buckets is still 1 there. The module uses the same count as httpd: online cores divided by the ratio, capped at the MPM daemon limit, because a bucket with no child would lose its peers.

Tested on EL9 (OpenSSL 4.0 with QUIC, nghttp3 1.19), httpd trunk built with --enable-http3=shared, no new warnings. Children run as daemon, key 0600 root:

  • Listen 443, no H3Port: HTTP/3 GET returns 200, and again after each of two graceful restarts.
  • 16 cores, ListenCoresBucketsRatio 4, StartServers 4: 16 of 16 HTTP/3 requests return 200, accepted by all four children (6/2/6/2). After a graceful restart, 16 of 16 again, spread over all four new children (4/2/6/4). No errors in the log.

A client that changes address can be hashed to another bucket's socket. The connection-ID based routing that keeps such a connection in its child is not part of this PR.

xl32 added 5 commits October 9, 2026 13:09
A host with "h3" in Protocols now serves HTTP/3 with the certificate
mod_ssl already resolved for it (SSLCertificateFile and mod_md alike),
the way mod_http2 rides mod_ssl. The certificate and key are loaded in
the ap_ssl_add_cert_files hook, during startup while httpd still runs
privileged, so a key readable only by root loads for QUIC as it does for
mod_ssl instead of failing in the unprivileged child.

H3CertificatePath and H3CertificateKeyPath are removed; drop them from
existing configurations.

Each virtual host on a shared port now presents its own certificate over
HTTP/3, chosen by the client SNI; an unmatched name gets the listener
default. Names come from ServerName, ServerAlias and the wildcard
ServerAlias names that httpd keeps in wild_names, and every entry is
matched with ap_strcasecmp_match, the matcher httpd uses for its own
virtual hosts. A cert_cb on the listener context applies the matched
host certificate, key and chain to the connection: SSL_set_SSL_CTX does
not switch the certificate of a QUIC connection.

Landing it exposed that the module ran with AP_MODULE_FLAG_NONE, so a host
without H3 directives shared the main server configuration; per-host
H3AltSvc and H3AltSvcMaxAge were silently ignored. Set ALWAYS_MERGE.
httpd prepends virtual hosts to the server list, so the first host with
an HTTP/3 context in that list is the last one defined. The listener took
that host's settings and certificate, and a client whose SNI matched no
host got that certificate, where mod_ssl serves the first virtual host
defined for the address. Take the last match in both h3_post_config and
find_h3_server instead.
Each child bound the UDP port itself after httpd dropped root, so a port
below 1024 needed CAP_NET_BIND_SERVICE on the httpd binary.

On Linux the parent now binds the socket in post_config, while it still
runs as root, and keeps it in the process pool, so it stays bound across
graceful restarts. The children inherit it. One child at a time serves
it: the child that binds an abstract Unix socket name owns the port, and
the kernel releases the name when that child exits. Other platforms keep
the old per-child bind.
With ListenCoresBucketsRatio, httpd splits its TCP listeners into
buckets. The parent now binds one SO_REUSEPORT UDP socket per bucket,
and each child serves the first bucket that no other child owns. So
more than one child serves HTTP/3.

The parent keeps the sockets while children start and stop, so the
kernel hash does not change and each peer stays on the same socket.
Without buckets, there is one socket as before.
Some MPMs count their listener buckets in the run step, after
post_config, so ap_num_listen_buckets was still 1 there. The module now
uses the same count as httpd: online cores / ratio, capped at the MPM
daemon limit, because a bucket with no child loses its peers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant