erlang_quic Features
View SourceCore Protocol (RFC 9000)
Connection Management
- [x] Connection establishment with TLS 1.3 handshake
- [x] Connection close (immediate and draining states)
- [x] Idle timeout enforcement (configurable via
idle_timeoutoption) - [x] Lazy idle and keep-alive timers: armed once and re-armed only when they
fire, using the
last_activitytimestamp, so steady-state traffic does not cancel and reschedule a timer on every packet - [x] Version negotiation: server emits a Version Negotiation packet for an unknown version; a client that receives one and shares no version closes with
{version_negotiation, Versions}(RFC 9000 §6.2) - [x] Retry packets for address validation
- [x] IPv6 client connections: hostname, IP-literal (bracketed or bare), or
inet:ip_address()tuple host - [x] Happy Eyeballs v2 (RFC 8305): dual-stack hostnames race IPv6-first, with
happy_eyeballs,family,connection_attempt_delayandconnect_timeoutoptions onquic:connect/4 [x] Latency spin bit (RFC 9000 §17.4) with
spin_bit => true | false- [x] NEW_TOKEN frame dispatch (server rejects peer-received tokens per §8.1.3); client caches received tokens keyed by
{Host, Port}and reuses them in the Initial of the next connect to the same endpoint - [x] Stateless reset (RFC 9000 §10.3): listener emits resets for orphan packets; per-connection
NEW_CONNECTION_IDtokens share the listener's HMAC secret so they match orphan-path tokens - [x] Server-side address validation (RFC 9000 §8.1): opt in with
address_validation => alwaysonquic:start_server/3. Listener emits a Retry packet with an HMAC-signed retry token when a client Initial arrives without one; subsequent Initials carrying a valid token skip retry and spawn a connection that echoesretry_source_connection_id. Server issues a NEW_TOKEN after handshake so the next reconnect skips retry entirely
Streams
- [x] Bidirectional streams (client and server initiated)
- [x] Unidirectional streams
- [x] Stream prioritization (RFC 9218) with 8 urgency levels
- [x] Incremental delivery flag support
- [x] RESET_STREAM_AT extension (draft-ietf-quic-reliable-stream-reset-07)
Flow Control
- [x] Connection-level flow control (MAX_DATA)
- [x] Stream-level flow control (MAX_STREAM_DATA)
- [x] MAX_STREAMS limits (bidirectional and unidirectional)
Packet Handling
- [x] Initial, Handshake, and 1-RTT packet types
- [x] Short header (1-RTT) packets
- [x] Packet number encoding (1-4 bytes)
- [x] Packet number reconstruction per RFC 9000 Appendix A
- [x] Coalesced packets
- [x] Frame coalescing (ACK + small stream data in single packet)
- [x] HTTP/3 response HEADERS coalesced with the first DATA frame so the response headers and first body bytes ride in one 1-RTT packet (a large body still fragments; only the standalone HEADERS packet is removed)
Connection Migration (RFC 9000 Section 9)
- [x] PATH_CHALLENGE / PATH_RESPONSE validation
- [x] Active connection migration (
quic:migrate/1,quic:migrate/2) - [x] Preferred address handling (RFC 9000 Section 9.6)
- [x] Server-side address change detection (NAT rebinding and active migration)
- [x] Congestion control reset on path change (RFC 9002 Section 9.4)
- [x] Per-path connection IDs, bound before the path's first packet, with the
replaced one retired (RFC 9000 Section 9.5). A migration with no unused CID
is refused rather than reusing one:
quic:migrate/1,2returns{error, no_available_connection_id} - [x]
disable_active_migrationtransport parameter support - [x] Path validation timeout with retry (3 * PTO, up to 3 attempts)
Not yet enforced: the Section 9.3 limit on bytes sent to an unvalidated path, and the Section 9.4 rule that old-path packets must not be charged to the new path's congestion controller.
Connection ID Management
- [x] Multiple connection IDs
- [x] NEW_CONNECTION_ID frames
- [x] RETIRE_CONNECTION_ID frames
- [x] Active connection ID limit
Loss Detection & Congestion Control (RFC 9002)
Loss Detection
- [x] Packet loss detection
- [x] Per-packet-number-space recovery (RFC 9002 Appendix A): each space keeps its own sent packets and loss state, while the RTT estimate, the probe backoff and bytes in flight stay connection-wide
- [x] Probe timeout (PTO), armed for one space at a time and probing at that encryption level. The application space is not armed until the handshake is confirmed (Section 6.2.1), an endpoint over its anti-amplification limit arms nothing, and a client with nothing in flight and an unvalidated address arms the anti-deadlock probe
- [x] Packet number space discard on key discard (Section 6.4), removing those packets from both the loss tracker and the congestion controller
- [x] Handshake packets are admitted against the congestion window like any other (Section 7). A refusal holds the frames rather than the encoded packet, so no packet number is spent, and probes are exempt
- [x] Recovery and congestion state reset on Retry (Section 6.3), preserving the data to resend and the controller's configuration
- [x] RTT measurement (smoothed RTT, RTT variance)
The probe interval is capped at 5 seconds, which RFC 9002's unbounded exponential backoff does not require; this is a deliberate bound on how late a probe may arrive. Time-threshold loss detection runs on acknowledgement rather than from its own timer.
Congestion Control
- [x] Pluggable congestion control behavior
- [x] NewReno (default, RFC 9002)
- [x] CUBIC (RFC 9438)
- [x] BBR (Bottleneck Bandwidth and RTT)
- [x] HyStart++ slow start (RFC 9406) for all algorithms
- [x] Slow start with improved exit detection
- [x] Congestion avoidance
- [x] Recovery on packet loss
- [x] Persistent congestion detection, over the RFC 9002 Section 7.6.1 duration: the PTO without its exponential backoff, times 3. Section 7.6.2's rule about packets sent before the first RTT sample is not applied
- [x] ECN support (ECN-CE triggers congestion response)
- [x] Packet pacing (RFC 9002 Section 7.7) to prevent bursts
- [x] RTT-based flow control auto-tuning
Path MTU Discovery (RFC 8899 - DPLPMTUD)
- [x] Binary search probing for optimal MTU
- [x] Integration with peer's
max_udp_payload_sizetransport parameter - [x] Black hole detection and recovery
- [x] Automatic MTU reset on connection migration
- [x] Periodic re-probing for MTU increases
- [x] Congestion control integration (updates cwnd-related parameters)
TLS 1.3 Integration (RFC 9001)
Handshake
- [x] Full TLS 1.3 handshake
- [x] ALPN negotiation
- [x] Transport parameters exchange
- [x] Certificate verification
- [x] HelloRetryRequest (RFC 8446 §4.1.4)
Key-exchange groups
- [x] x25519 (default)
- [x] secp256r1, secp384r1 (opt-in via
groups) - [x] x25519mlkem768 post-quantum hybrid (draft-ietf-tls-ecdhe-mlkem;
opt-in via `groups`, needs the ML-KEM APIs in `crypto`, OTP 28.1+) - [x] Multi-group
key_share+supported_groupsnegotiation - [ ] secp521r1, x448 (constants only; see Roadmap)
Signature algorithms
- [x] ECDSA secp256r1-SHA256, secp384r1-SHA384
- [x] RSA-PSS-RSAE SHA256/384/512
- [x] Ed25519
- [x] Per-handshake
signature_algorithmsnegotiation (signature_algs) - [ ] Ed448, ECDSA secp521r1-SHA512 (constants only; see Roadmap)
Encryption
- [x] AES-128-GCM cipher suite
- [x] AES-256-GCM cipher suite
- [x] ChaCha20-Poly1305 cipher suite
- [x] Header protection
- [x] Key derivation (HKDF)
Key Management
- [x] Initial secrets derivation
- [x] Handshake secrets
- [x] Application secrets
- [x] Key updates (RFC 9001 Section 6)
Session Resumption
- [x] Session tickets (NewSessionTicket)
- [x] PSK-based resumption
- [x] 0-RTT early data
External PSK (RFC 8446 §4.2.11)
- [x]
psk_dhe_kemode (forward-secret) - [x]
psk_kemode (no DHE) - [x] Server-side binder verification (constant-time)
- [x] Identity lookup via callback and/or static map
- [x] Cert + PSK coexistence on the same listener
- [x] Client downgrade protection
See docs/PSK.md. Pending follow-ups tracked in the Roadmap below.
QUIC Version 2 (RFC 9369)
- [x] Version 2 (0x6b3343cf) support
- [x] Updated initial salt
- [x] Updated retry integrity tag key
QUIC-LB Load Balancer Support (RFC 9312)
- [x] Server ID encoding in Connection IDs for LB routing
- [x] Config rotation bits for LB coordination
- [x] Variable CID length support (1-20 bytes)
- [x] Three encoding algorithms:
- Plaintext: Server ID visible in CID (no encryption)
- Stream Cipher: AES-128-CTR encryption
- Block Cipher: Feistel network for variable lengths
- [x] LB-aware CID generation in listener and connection
Reliable Stream Reset (draft-ietf-quic-reliable-stream-reset-07)
RESET_STREAM_AT allows resetting a stream while ensuring data up to a specified offset is reliably delivered. Required for WebTransport where stream headers must be received even if the stream is immediately reset.
Features
- [x] Frame type 0x24 (RESET_STREAM_AT) encode/decode
- [x] Transport parameter negotiation (0x17f7586d2cb571)
- [x] Reliable delivery guarantee up to ReliableSize
- [x] Retransmission filtering (data beyond ReliableSize not retransmitted)
- [x] Validation: ReliableSize cannot exceed FinalSize
- [x] Validation: ReliableSize cannot be increased after initial reset
- [x] Validation: ErrorCode cannot change after initial reset
Usage
%% Enable in connection options (both client and server)
Opts = #{reset_stream_at => true, alpn => [<<"webtransport">>]},
{ok, Conn} = quic:connect(Host, Port, Opts, self()),
%% Send stream header (e.g., WebTransport session ID)
{ok, StreamId} = quic:open_stream(Conn),
ok = quic:send_data(Conn, StreamId, Header, false),
%% Reset stream but ensure header is delivered
ok = quic:reset_stream_at(Conn, StreamId, ErrorCode, byte_size(Header)).API
Connection
quic:connect/4- Connect to serverquic:close/1,2,3- Close connection (with optional app error code)quic:peername/1- Get peer addressquic:sockname/1- Get local addressquic:peercert/1- Get peer certificatequic:migrate/1,2- Trigger connection migration (with optional timeout)quic:has_early_keys/1- Whether 0-RTT (early data) keys are availablequic:early_data_accepted/1- Whether the server accepted 0-RTT early data
Datagrams (RFC 9221)
quic:send_datagram/2- Send unreliable datagramquic:datagram_max_size/1- Get max datagram size (0 if unsupported)quic:datagram_stats/1- Delivered / dropped / sent counters (backpressure)
HTTP Datagrams (RFC 9297)
quic_h3:send_datagram/3- Send an HTTP datagram bound to a request streamquic_h3:h3_datagrams_enabled/1- Whether both sides negotiated supportquic_h3:max_datagram_size/2- Max payload per datagram for a given stream- Owner event:
{quic_h3, Conn, {datagram, StreamId, Payload}} - Set
h3_datagram_enabled => trueonconnect/3/start_server/3to enable. CONNECT-UDP (RFC 9298) builds on this in a separate library.
Capsule Protocol (RFC 9297 §3.2)
quic_h3_capsule:encode/2- EncodeType | Length | Valueas an iolistquic_h3_capsule:decode/1- Decode one capsule, or report that more bytes are needed- Constants in
include/quic_h3.hrl:?H3_CAPSULE_DATAGRAM(0x00),?H3_CAPSULE_LEGACY_DATAGRAM(0xff37a0) - A pure codec with no callers inside this library: extension libraries
drive it. Same wire format as
h1_capsuleandh2_capsule.
HTTP/3 Extension Streams
quic_h3:open_bidi_stream/1,2- Open a client-initiated bidi stream; with a non-negativeSignalTypevarint the stream is pre-claimed and inbound bytes route as{stream_type_data, bidi, ...}owner messages instead of HTTP/3 request frames (e.g. WebTransport's0x41)stream_type_handleroption onstart_server/3claims peer-initiated uni / bidi streams whose first varint matches a caller-supplied filter- Owner events:
{stream_type_open, Direction, StreamId, VarintType},{stream_type_data, Direction, StreamId, Data, Fin},{stream_type_closed, Direction, StreamId},{stream_type_reset, Direction, StreamId, ErrorCode},{stream_type_stop_sending, Direction, StreamId, ErrorCode} - Per-connection owner override via
connection_handlercallback onstart_server/3for hosting many sessions on one listener
Streams
quic:open_stream/1- Open bidirectional streamquic:open_unidirectional_stream/1- Open unidirectional streamquic:send_data/4,5- Send data on stream (Fin = truecloses the send side)quic:send_data_async/4- Send without waiting for the connection processquic:stop_sending/3- Ask the peer to stop sending on a streamquic:reset_stream/3- Reset stream with error codequic:reset_stream_at/4- Reset stream with reliable delivery up to specified sizequic:set_stream_priority/4- Set stream priority (urgency, incremental)quic:get_stream_priority/2- Get stream priority
Flow Control Accounting
quic_flow:new/0,1- Create accounting statequic_flow:can_send/2,on_data_sent/2,on_max_data_received/2,send_blocked/1- Send sidequic_flow:on_data_received/2,should_send_max_data/1,generate_max_data/1- Receive sidequic_flow:bytes_sent/1,bytes_received/1,send_limit/1,recv_limit/1,send_window/1,recv_window/1- Queries- Connection-level only, and it emits no frames.
quic_connectiondoes not use it: the flow control that runs on the wire is inline there, auto-tuned from the RTT and with per-stream limits.
Server / Multi-Pool Server Management
quic:start_server/3- Start named server poolquic:stop_server/1- Stop named serverquic:get_server_info/1- Get server informationquic:get_server_port/1- Get server listening portquic:get_server_sockname/1- Get server bound address ({IP, Port})quic:get_server_connections/1- Get server connection PIDsquic:which_servers/0- List all running servers
Load Balancer (RFC 9312)
quic_lb:new_config/1- Create LB configuration from options mapquic_lb:new_cid_config/1- Create CID generation configurationquic_lb:generate_cid/1- Generate CID with encoded server_idquic_lb:decode_server_id/2- Extract server_id from CIDquic_lb:is_lb_routable/1- Check if CID has valid LB routing bitsquic_lb:get_config_rotation/1- Get config rotation bits from CIDquic_lb:expected_cid_len/1- Calculate expected CID length from config
Options
idle_timeout- Connection idle timeout in milliseconds (0 to disable)max_data- Connection-level flow control limitmax_stream_data_bidi_local- Stream-level limit for streams we openmax_stream_data_bidi_remote- Stream-level limit for streams the peer opensmax_stream_data_uni- Stream-level limit for unidirectional streamsmax_receive_window- Largest window the receive limits grow to (default: 8 MiB, minimum: 16 KiB)max_datagram_frame_size- Max datagram size to accept (0 = disabled, default: 0)datagram_recv_queue_len- Bounded receive queue for inbound datagrams (default:infinity; drops oldest on overflow, tracked viadatagram_stats/1)reset_stream_at- Enable RESET_STREAM_AT extension (default: false)active_connection_id_limit- How many of the peer's connection IDs to hold, counting the handshake CID (default: 2). Advertised to the peer and enforced on inbound NEW_CONNECTION_ID; exceeding it closes with CONNECTION_ID_LIMIT_ERRORalpn- ALPN protocols listverify- Server certificate verification on the client (default:true; verifies the CertificateVerify signature, the chain, and the hostname). The hostname check follows the RFC 6125 HTTPS rules, so a leftmost-label wildcard SAN such as*.example.commatcheshost.example.com. Setfalseto accept any certificate, e.g. a self-signed test server.cacerts- Trust anchors for client chain validation, as a list of DER-encoded certificates (default: the operating system trust store)cacertfile- Path to a PEM file holding those anchors, read once when the connection or server starts. Ignored whencacertsis given; an unreadable file, or one with no certificate in it, fails the callpreferred_ipv4- Server preferred IPv4 addresspreferred_ipv6- Server preferred IPv6 addresspool_size- Number of listener processes for server pools (default: 1)connection_handler- Callback for handling new connectionsmonitor_owner- Stop the connection with{shutdown, owner_down}and send CONNECTION_CLOSE when its owner process exits (default:truefor accepted connections,falsefor client connections, which are linked to their caller unless supervised). Setfalsein the listener options to keep an accepted connection alive after its handler exitslb_config- QUIC-LB configuration map for load balancer routingkeep_alive_interval- Keep-alive PING interval (disabled,auto, or milliseconds)pmtu_enabled- Enable Path MTU Discovery (default: true)pmtu_max_mtu- Maximum MTU to probe (default: 1500)max_udp_payload_size- Largest UDP payload this endpoint is willing to receive, advertised as the RFC 9000 §18.2 transport parameter (minimum 1200; default: what a 1500-byte path carries for the address family, 1472 over IPv4 and 1452 over IPv6)recbuf- UDP receive buffer size in bytes (default: 7MB)sndbuf- UDP send buffer size in bytes (default: 7MB)server_send_batching- Per-connection send batching on the server (default: true). On Linux +socket_backend => socketwith UDP_SEGMENT, outgoing packets are coalesced into GSO super-datagrams viasendmsgcmsg; neutral on macOS / gen_udp. Set tofalseto fall back to directgen_udp:send/4hibernate_after- Hibernate an idle connection process after this many milliseconds, running a fullsweep so handshake garbage is not pinned to a heap that never collects (default: 5000;infinityopts out)versions- QUIC versions this connection also accepts, for RFC 9368 compatible version negotiation (default: the negotiatedversionalone)ciphers- TLS 1.3 cipher suites to offer or accept, in preference order (default:[aes_128_gcm, aes_256_gcm, chacha20_poly1305]on a CPU with AES instructions,[chacha20_poly1305, aes_128_gcm, aes_256_gcm]without them, per/proc/cpuinfo). A server honours a client that listschacha20_poly1305first whenever its own list allows the suite, so a peer without AES hardware gets ChaCha from either sidemax_burst_packets- Packets allowed to leave per send drain, so a large queued write cannot monopolise the scheduler (default: 64)ack_packet_tolerance- Ack-eliciting packets received before a 1-RTT ACK is sent, the RFC 9000 section 13.2.1 decimation threshold (default: 2)disconnect_timeout- Milliseconds without a response from the peer before the connection is given up on, checked on its own timer (default: 16000)pmtu_raise_interval- Milliseconds between PMTU raise probes (default: 600000)delivery_coalescing- Merge consecutive same-streamstream_datadeliveries of one receive pass into a single owner message (default:false). Arrival order across streams is preserved and astream_resetnever overtakes data delivered before it, but an owner that counts messages rather than bytes sees fewer of them
PMTU Discovery
quic:get_mtu/1- Get current effective MTU for a connection
Erlang Distribution (quic_dist)
QUIC-based Erlang distribution protocol implementation.
Features
- [x] Full distribution protocol over QUIC transport
- [x] TLS 1.3 encryption built-in (no separate SSL setup)
- [x] 0-RTT session resumption for fast reconnection
- [x] Multiple streams: control (urgency 0) + data (urgency 4-6)
- [x] Stream prioritization for tick/control messages
- [x] QUIC-level liveness detection (packet counts, not blocked by flow control)
- [x] Keep-alive PING frames for transport liveness
- [x] Backpressure mechanism for congestion control
- [x] Session ticket storage for 0-RTT
Modules
quic_dist- Distribution protocol callbacksquic_dist_controller- Per-connection state machinequic_dist_sup- Distribution supervisorquic_dist_tickets- Session ticket storagequic_epmd- EPMD replacement modulequic_dist_auth- Optional auth-handshake behaviour
Discovery Backends
quic_discovery_static- Static node configurationquic_discovery_dns- DNS SRV-based discovery- Custom backends via
quic_discoverybehaviour
Distribution API
quic:get_stats/1- Get packet counts for liveness detectionquic:send_ping/1- Send transport-level PING frame
Extension Hooks
auth_callback(defaultundefined):{Mod, Fun}orfun/3invoked on both sides after the QUIC handshake but before the distutil handshake. Returning `{error, }closes the connection without the node ever joining. Seequic_dist_authand the Configuration Reference indocs/QUIC_DIST.md`.register_with_epmd(defaultfalse): whentrue, the listener registers its port via the configuredepmd_moduleso external tooling (e.g.epmd -names) can resolve the node.
Roadmap
Tracked future work. Items here are not committed deliverables; they mark known gaps that may land in a later release.
TLS 1.3 external PSK follow-ups
- [ ] 0-RTT on external PSK. The server currently ignores
early_dataon PSK handshakes; full EndOfEarlyData state-machine support is deferred. - [ ]
NewSessionTicketon PSK-authenticated handshakes. Suppressed in v1 to avoid binding-identity ambiguity; revisit when a concrete use case appears. - [ ] RFC 9258 PSK Importer. The current API consumes the secret as
raw IKM; an importer layer would derive an
epsk -> pskmapping bound to a target protocol/KDF.
TLS 1.3 negotiation follow-ups
- [ ] secp521r1 / x448 key-exchange groups (constants defined; key generation and key_share wiring deferred).
- [ ] Ed448 / ECDSA secp521r1-SHA512 signature schemes (constants defined; sign/verify branches deferred).
- [ ] PSK + HelloRetryRequest in one handshake. Currently the client aborts if HRR follows a PSK ClientHello (binder recompute over the synthetic transcript is not implemented).
Interop Runner Compliance
All 10 QUIC Interop Runner test cases pass against the external runner.
The Covered by column says which of them quic_interop_SUITE also
checks here, so the table is not taken on trust:
| Test Case | Status | Covered by quic_interop_SUITE |
|---|---|---|
| handshake | Pass | yes |
| transfer | Pass | yes |
| retry | Pass | no |
| keyupdate | Pass | no, nothing exposes the key phase to assert on |
| chacha20 | Pass | yes |
| multiconnect | Pass | yes |
| v2 | Pass | yes |
| resumption | Pass | yes |
| zerortt | Pass | yes, early data sent, accepted and answered |
| connectionmigration | Pass | yes, data echoed over the new path |