Erlang QUIC 1.8.0 update

I have not posted an update here since 1.3.0, so here is a short one. Most of the work has been around interoperability with real QUIC/HTTP3 servers, security, and distribution.

One important fix landed in 1.4.4. The client was not actually authenticating the server: verify was effectively a no-op and any certificate was accepted. This is fixed as CVE-2026-49457, reported by benmmurphy. It now verifies the CertificateVerify signature, validates the chain using the trust store (OS store by default), and checks the hostname. Verification is enabled by default.

The same release also brought a more complete security pass: anti-amplification limits, Retry token validation, PSK binder verification, single-use 0-RTT, AEAD usage-limit key updates, connection and CRYPTO buffer limits, and stricter HTTP/3 and QPACK decoding. It also supports mutual TLS on the server side, including an option to require a client certificate.

A lot of less visible work went into interoperability. Issues around QPACK encoding with nghttp3 have been fixed, TLS flight segmentation with Chromium, cross-signed chains, wildcard SANs, Version Negotiation, Retry and max_udp_payload_size, among others. In practice, the client can now fetch sites like www.google.com or cloudflare.com directly over HTTP/3.

0-RTT now works on both client and server, with quic:early_data_accepted/1 to know if it was accepted. It supports IPv6 for listeners and clients, including Happy Eyeballs for dual-stack hostnames. Stateless reset is implemented too, so after a server restart the peer does not need to wait for the idle timeout to discover that the connection is gone.

On the TLS side, it now supports external PSK, HelloRetryRequest with x25519/secp256r1/secp384r1, and per-handshake signature selection including Ed25519.

HTTP/3 also got some simpler APIs. quic_h3:respond/5 sends status, headers and body in one call. SNI callbacks allow selecting certificates per hostname, small responses coalesce HEADERS with the first DATA frame, and received bodies are no longer accumulated in the stream state.

Erlang distribution changed quite a bit too. It now spreads traffic over 16 QUIC streams, hashed by {From, To}. A large message should not block unrelated traffic while ordering between a sender/receiver pair is preserved. It also has an auth_callback between the QUIC and distribution handshakes, priv/bin/quic_call.sh as an erl_call equivalent for QUIC clusters, and keep-alives paced using net_ticktime.

One change to notice when upgrading to 1.8.0: handshake failures now reach the owner as {quic, Conn, {error, Reason}}, using the connection handle like the other QUIC events. This also means quic_h3:connect/3 can return the real error, for example {error, {certificate_invalid, _}}, instead of eventually timing out.

erlang_quic is also starting to be used by other projects. It is now the default HTTP/3 layer in Livery, Hackney and erlang-webtransport. This is useful to exercise it with different real-world usage patterns.

Thanks to sstrollo, ycastorium, benmmurphy and maslowalex for the patches and reports.

Repository: https://github.com/benoitc/erlang_quic
1.8.0: https://github.com/benoitc/erlang_quic/releases/tag/1.8.0
Changelog: https://github.com/benoitc/erlang_quic/blob/main/CHANGELOG.md

Issues, interoperability reports and pull requests are welcome.

3 Likes