Changelog
View SourceAll notable changes to this project will be documented in this file.
[Unreleased]
[1.3.0] - 2026-04-25
QUIC and HTTP/3 protocol-conformance hardening: closes the silent-drop of CONNECTION_CLOSE at handshake-time violations, replaces the unmaintained h3spec runner with an in-tree RFC 9114 / 9204 compliance suite, and fixes two externally-reported stream-API bugs.
Added
close_with_error/6emits CONNECTION_CLOSE at the right encryption level (initial / handshake / app), with fallback to the lower available level. (#111)- Server-side validation of peer transport parameters per RFC 9000 §18.2: server-only ids (
original_dcid,preferred_address,retry_scid,stateless_reset_token) and numeric ranges (max_udp_payload_size≥ 1200,ack_delay_exponent≤ 20,max_ack_delay< 2^14). (#111) - Frame-pipeline guards: zero-frame packet →
PROTOCOL_VIOLATION; unknown frame type →FRAME_ENCODING_ERROR. (#111) - HTTP/3 RFC 9114 + 9204 conformance: 30 in-tree unit tests covering control-stream rules, pseudo-headers, stream-type uniqueness, push-id bounds, CONNECT validation, QPACK static-index and capacity limits, RFC 9218 priority signal, RFC 9297 SETTINGS_H3_DATAGRAM. (#112)
docs/h3_compliance.md: RFC 9114 / 9204 / 9218 / 9297 matrix mapping every MUST and SHOULD to its test. (#112)
Fixed
- Reject request streams carrying
:statuspseudo-header (RFC 9114 §4.3.1). (#112) quic_qpack:set_dynamic_capacity/2clamps tomax_allowed_capacityper RFC 9204 §4.3. (#112)quic:reset_stream/3keeps the stream entry alive so subsequentquic:stop_sending/3emits STOP_SENDING instead of returning{error, unknown_stream}. (#113, #115)quic:close/2with an integer reason propagates that integer as the application error code; previously every input fell through to?QUIC_APPLICATION_ERROR(0x0c). (#114, #116)- NEW_TOKEN received by a server and HANDSHAKE_DONE at the wrong level now route through
close_with_error/6so the CLOSE frame reaches the peer when app keys are absent. (#111)
Removed
quic_h3_h3spec_SUITEanddocker/h3spec/. The corpus is ported intoquic_h3_compliance_testsas deterministic state-machine tests. (#112)
[1.2.0] - 2026-04-21
Post-1.1.0 work split across three tracks: a client-side socket-backend opt-in, a round of hot-path micro-optimisations on the send and receive paths, and a migration fix for the default gen_udp client.
Added
- Opt-in
socket_backend => socketfor client connections. Routes the client throughquic_socket:open_for_send/2so it picks up the OTP socket NIF on Linux with GSO available per-message via cmsg, instead of thegen_udpport driver. +18% download throughput on arm64 Linux docker (10 MB bench); upload is neutral. (#88, #91) - Client migration (
quic:migrate/1) now works on the opt-in socket backend. Rebind closes the old OTP socket, stops its dedicated receiver process, opens a fresh one, and threads the new handle through the connection state. (#90) quic_socket:start_client_receiver/2/stop_client_receiver/1: dedicated receiver process for the socket-backend client path (the OTP socket NIF has no{active, N}mode). (#88)quic_socket:set_socket/2swaps the underlying socket handle inside a#socket_state{}while preserving batching configuration. Used by the migration rebind path. (#93)- Instrumentation counters
ack_sentandretransmitsonquic_connection:get_stats/1and the throughput bench output (Phase 0a). (#77, #78)
Fixed
quic:migrate/1on the default gen_udp client no longer drops post-migrate traffic. Rebinding previously left#state.socket_statepointing at the just-closed old socket; every send went through the dead handle and was silently dropped. Also flushes any pending batch to the old socket before rebind so pre-migrate packets reach the server under their original CID. (#93)quic_dist: simultaneous-connect deadlock in the accept path. Two nodes dialling each other within a tight window wedged bothnet_kernel:connect_node/1calls indefinitely. The old accept path ran the dist worker through a nine-hop handoff (register_pending / controller rendezvous in acceptor_loop) before reachingdist_util:mark_pending, so net_kernel's tie-breaker arbitration never ran in time. Collapsed to the TCP-dist shape:accept_connection/5runsset_supervisor+start_timer+handshake_other_startedinline. Docker 5-node regression now passes 5/5. (#106)quic_dist: batch-yield path ininput_handler_loopcould lose or reorder buffered dist bytes when the mailbox had backlog. Yield now threads the buffer remnant through the normal return channel instead of piggybacking on the self-message. (#104)quic_dist_user_stream_SUITE/accept_user_streams/2doc: refreshed to match the auto-assign / direct{quic_dist_stream, _, {data, _, _}}delivery shape. (#105)docker/dist: 3+ node cluster mesh formation. Each node now dials only higher-named peers and boots with-connect_all false, soglobaldoes not re-introduce cross-dials behind the explicit test topology. (#95, #106)- h3: preserve WebTransport and unknown SETTINGS identifiers in the peer settings map so extension-stream hooks can read them. (#96)
quic_socket: client migrate path opens the new socket before closing the old one, avoiding a window where the client has no valid send handle. (#97)quic_socket:client_recv_loopexits cleanly on unexpected socket errors instead of spinning. (#98)quic_socket: clear the pending batch buffer on flush error so stale frames do not get retried on the next flush. (#99)quic:connect/4: reject thesocket+{socket_backend, socket}option combination with a clear error instead of silently overriding one. (#100)- Client connection: treat receiver-process exit as a fatal error and close the connection, matching server behaviour. (#101)
- Server: build a per-connection sender even when
server_send_batchingisfalseso the direct-send path uses the samequic_socketshape as the batched path. (#102, #103)
Performance
- Fuse per-packet cwnd + pacing check into
quic_cc:send_check/3(one BIF call and one record match instead of the previous four). (#79) - Hoist per-chunk lookups (
stream_urgency,max_stream_data_per_packet, pre-computed stream-frame header prefix) out of the chunked send loop. (#80, #85) - ACK 1-RTT packets immediately on reorder (RFC 9002 §6.2) while keeping the decimation window for in-order traffic. (#81)
- Fast-path single-stream-frame in
contains_ack_eliciting_frames/1on the bulk-upload hot path. (#82) - Thread the updated
socket_stateback fromdo_socket_sendvia the return value, dropping the process-dictionary roundtrip. (#83) - Replace the
crypto:exor/2NIF call with inline Erlang XOR for the 1-4 byte header-protection mask. (#84) - Inline the
?QLOG_ENABLEDcheck at packet/frame event call sites so the event-map is never built when qlog is off. (#86) - Coalesce the
monotonic_timesamples on the receive hot path (one BIF call per received datagram instead of three). (#87) - Flush the pending stream-data batch before emitting an ACK-only packet so it does not break GSO uniformity on the opt-in socket backend. +6.4% upload throughput on arm64 Linux docker. (#92)
- Re-enable GSO on the opt-in socket-backend client: drop the
socket-level
UDP_SEGMENTsetsockopt and rely on per-message cmsg viaflush_gso/1. (#91)
[1.1.0] - 2026-04-18
Server-side throughput work. Per-connection send batching over the shared listener socket on Linux + socket backend coalesces outgoing packets into sendmsg super-datagrams via UDP_SEGMENT (GSO); on macOS / gen_udp it is functionally neutral. Several GSO correctness fixes after CI surfaced a handshake stall. Extra observability so tests and operators can see the batching win directly.
Added
- Per-connection send batching on the server. Each server connection
owns a
quic_socketbatch buffer that reuses the listener's UDP socket. Gated by the newserver_send_batchingoption onstart_server/3(defaulttrue); set tofalseto fall back to the previous directgen_udp:send/4path. (#66) quic_socket:info/1— map withbackend,gso_supported,gso_size,gro_enabled,batching_enabled,max_batch_packets, and the newbatch_flushes/packets_coalescedcounters.quic_socket:send_immediate/4— public wrapper that bypasses the per-connection batch for one-shot control-plane sends.quic_socket:new_sender/2— build a per-connection sender that inherits backend + GSO capability from the listener without owning the socket.quic_connection:get_stats/1now returnsbatch_flushesandpackets_coalescedso tests and benchmarks can assert batching behaviour rather than just wiring.quic_server_batching_SUITE— behaviour-level regression: real 256 KB server-to-client downloads assertpackets_coalesced > 1when batching is on, and both counters stay at 0 when disabled.docker/gso-debug/— Erlang 28 + tcpdump + strace container that reproduces the GSO handshake stall against a bind-mounted tree. (#74)bench/run_download_bench.erlandquic_throughput_bench:run_download_sink/0,1drive server-to-client bulk transfers and report MB/s alongsidebatch_flushes/packets_coalescedso the batching effect is visible next to throughput.
Changed
- Stream send path is iovec-native.
quic_frame:encode_iodata/1returns[Header, Data]and threads iodata through header protection andquic_aeadwithout copyingDatainto a fresh binary. AEAD specs relaxed to accept iodata. - 1-RTT ACKs delayed to every 2nd packet or
max_ack_delayper RFC 9002 §6.2. Halves receiver ACK traffic on the server and sender event-processing on the client. Measured on macOS gen_udp: 10 MB upload 45 → 56 MB/s. (#69) quic_lossswitched to a singlequeue:queue(#sent_packet{})for outstanding packets. Per-ACK work scales with the ACK window, not the full outstanding queue. Measured on macOS gen_udp: 10 MB upload 55 → 59 MB/s, 5 MB download 34 → 50 MB/s. (#72)flush_gso/1passes the batch as an iov list directly tosocket:sendmsg/2with the UDP_SEGMENT cmsg, saving up to ~76 KB of user-space copy per flush on a 64-packet batch. (#70)send_app_packet_internal/3samplesmonotonic_timeonce per packet and reuses it for loss tracking andlast_activity. (#71)- Per-packet overhead on the bulk-send path reduced: single
#state{}update, PTO timer reschedule skipped when within tolerance,process_send_queueand pacing timeout short-circuit on empty queue, stream data normalised to binary once at the fragmentation boundary. state_to_map/1replaces the coarsesend_batchingboolean with three explicit fields:send_backend(direct|gen_udp|socket),send_batching_enabled,send_gso_supported.
Fixed
- Server connection crashed with
function_clausewhen the listener was onsocket_backend => socketbecauseinet:sockname/1rejects{'$socket', Ref}handles. Branch on socket shape:socket:sockname/1for OTP socket handles,inet:sockname/1forgen_udpports. - UDP_SEGMENT
setsockoptnow usessizeof(int)(32-bit native) instead of u16, which Linux rejected withEINVAL; GSO capability detection silently returned false and the GSO CT job was skipping. The cmsg path already used u16 correctly. (#67) - GSO skipped for single-packet batches: UDP_SEGMENT with a
sub-
gso_sizesingle-packet payload drops silently on ubuntu-24.04.batch_count == 1has no segmentation work; fall through toflush_individual. (#73) - Listener no longer sets UDP_SEGMENT at socket level. A socket-wide
UDP_SEGMENT forces segmentation on every outbound datagram,
including short handshake packets that can't be segmented. GSO is
now applied only via the per-message cmsg in
flush_gso. (#73) - GSO bypassed when a batch mixes packet sizes (padded 1200-byte
Initial + ~400-byte Handshake). UDP_SEGMENT requires every segment
except the last to be exactly
gso_size, otherwise the client sees undecodable datagrams and stalls atawaiting_encrypted_extensions.flush/1checks uniformity and falls through toflush_individualwhen it fails. (#75) - Listener self-send:
send_packet/6was callingquic_socket:send/4and dropping the returned state, so version-negotiation / retry / stateless-reset packets were buffered then lost on the socket backend withbatching_enabled=true. Switched tosend_immediate/4. send_queue_bytesaccounting leaked on ACK-coalesce dequeues and could eventually trip?MAX_SEND_QUEUE_BYTESon long-lived connections. Addedsend_queue_countas an explicit O(1) emptiness predicate so zero-byte FIN-only sends enqueued under pacing are no longer stranded.examples/echo_server.erl:handle_connection/2expects a DCID binary, not an info map; returns{ok, HandlerPid}so the listener transfers ownership; peer address fetched viaquic:peername/1. (#65)examples/qlog_example.erl: added aconnection_handlerso the server echoes client data; waits for the client connection to terminate before returning so the qlog writer flushes. (#68)
[1.0.2] - 2026-04-16
Fixed
- h3: thread FIN through the peer uni stream-type dispatch so a
STREAM frame carrying type-varint + payload + FIN surfaces as one
{stream_type_data, uni, _, _, true}event to claimed-stream owners (#64)
[1.0.1] - 2026-04-15
Fixed
- h3: consult
stream_type_handleron fresh peer-initiated bidi streams so extensions can claim them before default request handling (#62) - docs:
rebar3 ex_docnow runs clean (#63)
[1.0.0] - 2026-04-15
First release with HTTP/3. Brings full client + server HTTP/3 (RFC 9114) with QPACK (RFC 9204), HTTP Datagrams (RFC 9297), Server Push, Extensible Priorities, Extended CONNECT, and the extension-stream hooks WebTransport needs. Also a critical flow-control deadlock fix in the QUIC core, a BBR loopback throughput fix, and the H3 server owner default change.
HTTP/3 (quic_h3, new module)
Added
- HTTP/3 client and server (RFC 9114) with QPACK header compression
(RFC 9204): request/response, body data, trailers, GOAWAY,
cancellation, CLI tools (
bin/quic_h3c,bin/quic_h3d) - Server Push (RFC 9114 §4.6):
push/3,send_push_response/4,send_push_data/4,set_max_push_id/2,cancel_push/2 - Extensible Priorities (RFC 9218):
priorityrequest option, PRIORITY_UPDATE frames, urgency / incremental hints - Extended CONNECT (RFC 9220) for WebTransport-style upgrades
- HTTP Datagrams (RFC 9297):
send_datagram/3,h3_datagrams_enabled/1,max_datagram_size/2, capsule framing - Extension-stream hook:
stream_type_handleroption onstart_server/3claims peer-initiated uni and bidi streams whose first varint matches a caller-supplied filter; claimed bytes are delivered as{stream_type_data, ...}owner messages instead of being parsed as HTTP/3 requests. Owner also receivesstream_type_open,stream_type_closed,stream_type_reset,stream_type_stop_sendingevents - Client-initiated extension streams:
quic_h3:open_bidi_stream/1,2pre-claims a bidi stream with a signal-type varint (e.g. WebTransport's0x41) so inbound bytes route through the claimed-bidi path - Per-connection owner override via
connection_handlercallback onstart_server/3for hosting many sessions per listener - Per-stream handler registration:
set_stream_handler/3,4,unset_stream_handler/2to redirect body data to a worker pid - Query API:
get_settings/1,get_peer_settings/1,get_quic_conn/1 - Documentation:
docs/HTTP3.mdreference + benchmarks section - E2E test infrastructure:
quic_h3_e2e_SUITE,quic_h3_h3spec_SUITE,quic_h3_owner_SUITE; dedicated CI job - Performance benchmark:
quic_h3_bench
Changed
- Server connection owner now defaults to the listener gen_server
(long-lived, trap_exit'ed) instead of the
start_servercaller pid; durable owners for datagram / stream-type events should be supplied via the per-connectionconnection_handlercallback - SETTINGS directionality validation tightened to RFC 9114
Fixed
- Server connections wedged with
connect_timeoutwhen the process that calledstart_server/3exited before a client arrived and eitherh3_datagram_enabledorstream_type_handlerwas set - Discard unknown unidirectional stream payload (RFC 9114 §6.2 unknown-stream-type rule) instead of erroring the connection
- Emit trailing empty DATA event when response carries FIN so owners
always see
Fin = trueexactly once - Strict PRIORITY_UPDATE frame parsing per RFC 9218
- DoS hardening on header / capsule / frame parsing
- Header / trailer /
:path/:statussymmetry between client and server validation - GOAWAY drain enforcement: reject new requests after a GOAWAY is sent or received
- Server push lifecycle correctness (PUSH_PROMISE pairing, duplicate detection, MAX_PUSH_ID enforcement)
- Tighten RFC 9114 / 9204 compliance across multiple parsers
syncoption onconnect/3resolves an E2E race where the client tried to send before SETTINGS exchange completed- Improved frame error handling and header validation
- aioquic SETTINGS compatibility
- QPACK: encoder eviction guard prevents references to
unacknowledged dynamic-table entries; rejects
Increment = 0
QUIC transport
Added
- Spin bit (RFC 9000 §17.4)
- Stateless reset support (RFC 9000 §10.3)
- Full NEW_TOKEN issuance and validation loop
RESET_STREAM_ATtransport parameter and frame plumbingquic:set_congestion_control/2runtime CC switch APIquic:get_peer_transport_params/1introspection API
Changed
- BBR internal clock switched to microseconds; loopback transfers no longer pin to the InitialRtt fallback
Fixed
- Stream-level
MAX_STREAM_DATAwindow stopped sliding oncerecv_max_datareachedfc_max_receive_window(8 MB default). Past the cap, the auto-tune re-sent the same value forever and the sender stalled at 8 MB lifetime per stream. The window now slides pastrecv_offsetlike the connection-level window already does - BBR loopback throughput regression: ms-precision clock collapsed delivery-rate intervals to 0/1 ms and clamped BDP to the 4-packet minimum, holding throughput at ~0.03 Mbps. Microsecond-precision internal clock restores expected behavior
- Send
MAX_STREAMSas peer-initiated streams complete (RFC 9000 §4.6); previously peers could exhaust the stream-id space
Distribution (quic_dist)
Added
- User-accessible streams API:
quic_dist:open_stream/1,2,send/3,close_stream/1,reset_stream/1,2,controlling_process/2,list_streams/0,1, with acceptor pool and stream priorities - Connection migration logging
- Distributed Erlang benchmarks + multi-node test scripts
- Per-iteration latency stats in throughput benchmark (min/p50/p99/max
- timeout counts)
Changed
- Test runner logs each test's results as it returns rather than at the end, so a stalled middle test no longer hides the others
Tests and infrastructure
quic_e2e_*_SUITEandquic_h3_e2e_SUITErun against in-process servers; Docker no longer required for these jobs
[0.11.0] - 2026-04-09
Added
- Full QUIC connection migration support (RFC 9000 Section 9)
- Server-side address change detection (NAT rebinding vs active migration)
- Path validation with PATH_CHALLENGE/PATH_RESPONSE
- CID rotation for path unlinkability
disable_active_migrationtransport parameter
- Application error code support for CONNECTION_CLOSE frames
- Client certificate support (
verifyserver option) - CUBIC congestion control (RFC 9438)
- BBR congestion control
- HyStart++ slow start (RFC 9406) for all CC algorithms
- UDP packet batching with GSO/GRO support
- Configurable UDP buffer sizing (recbuf/sndbuf options)
- QLOG tracing for debug visibility
- Pluggable congestion control behavior
- Stream deadlines for per-stream timeout control
- STOP_SENDING API (
quic:stop_sending/3) max_udp_payload_sizetransport parameter- Async send API and socket receive optimizations
- Throughput benchmarks (
quic_throughput_bench,quic_batch_bench) - QUIC-based Erlang distribution (
quic_dist) for node communication over QUIC - Distribution modules:
quic_dist,quic_dist_controller,quic_dist_sup - EPMD replacement module (
quic_epmd) for QUIC-based node discovery - Discovery backends:
quic_discovery_static(static config),quic_discovery_dns(DNS SRV) - Session ticket storage (
quic_dist_tickets) for 0-RTT reconnection - Stream prioritization for distribution: control stream (urgency 0), data streams (urgency 4-6)
- Backpressure mechanism for distribution congestion control
- Keep-alive PING frames for transport-level liveness (configurable via
keep_alive_interval) quic:get_stats/1API for connection packet counts (used for liveness detection)quic:send_ping/1API for transport-level PING frames- RTT-based flow control auto-tuning for improved throughput
- Packet pacing (RFC 9002 Section 7.7) to prevent bursts
Changed
- ConnRef is now connection PID (simpler API)
- Improved ACK processing performance (O(n^2) to O(n) with gb_sets)
- Timer batching for reduced overhead
- Zero-copy packet processing optimizations
- Distribution liveness detection now uses QUIC packet counts instead of application ticks
- Improved congestion control with quic-go-inspired settings (larger initial cwnd)
- Flow control windows auto-tune based on RTT measurements
Fixed
- Throughput regression in connection migration (wasteful binary allocation)
- CUBIC cwnd collapse issue
- BBR delivery rate interval causing cwnd collapse
- BBR initial pacing rate causing transfer hangs
- Pacing precision loss causing transfer stalls
- Various RFC compliance fixes for QUIC connection migration
net_tick_timeouterrors under heavy load by using QUIC-level activity as liveness proof- Stream flow control
recv_max_datausing wrong limits - Distribution controller backpressure data loss
- Congestion control protocol compliance issues
- Recovery exit when only non-ack-eliciting packets are ACKed
- Tick timeout issues in distribution controller
- Flow control blocking that caused deadlocks
- Message framing for large message transfers
Removed
- NAT traversal support from
quic_dist(use standard QUIC connection migration instead)
[0.10.2] - 2026-02-21
Fixed
- Deprecated
catchexpressions replaced withtry...catch...end - Undefined
dynamic()type replaced withterm()in type specs - CI workflow consolidated with separate unit-tests, e2e, and interop jobs
[0.10.1] - 2026-02-21
Fixed
- ACK range encoding crash for out-of-order packets: when packets arrived out
of order (e.g., 10, 5, 6), ACK ranges were not properly maintained in
descending order or merged, causing negative Gap values that crashed
quic_varint:encode/1withbadarg
[0.10.0] - 2026-02-21
Added
- RFC 9312 QUIC-LB Connection ID encoding support for load balancer routing
- New
quic_lbmodule with three encoding algorithms:- Plaintext: server_id visible in CID (no encryption)
- Stream Cipher: AES-128-CTR encryption of server_id
- Block Cipher: 4-round Feistel network for <16 bytes, AES-CTR for 16 bytes, truncated cipher for >16 bytes
#lb_config{}record for LB configuration (algorithm, server_id, key, nonce_len)#cid_config{}record for CID generation configurationlb_configoption inquic_listenerto enable LB-aware CID generation- Variable DCID length support in short header packet parsing
- LB-aware CID generation in
quic_connectionfor NEW_CONNECTION_ID frames - E2E test suite
quic_lb_e2e_SUITEwith 21 integration tests quic:server_spec/3to get a child spec for embedding QUIC servers in custom supervision trees- Stream reassembly test suite
quic_stream_reassembly_SUITEfor ordered delivery verification
Changed
quic:set_owner/2is now asynchronous (cast instead of call)
Fixed
quic:get_server_port/1now returns the actual OS-assigned port when server was started with port 0 (ephemeral port), instead of returning 0quic:get_server_connections/1now correctly returns connection PIDs; was returning empty list due toget_listeners/1returning supervisor pids instead of actual listener processes- Removed redundant
link/1call in listener (connection already linked viagen_statem:start_link) - Unhandled calls in connection state machine now return
{error, {invalid_state, State}}instead of silently timing out - Server-side connection termination no longer closes shared listener socket: previously when a server connection terminated, it would close the UDP socket shared with the listener, breaking all subsequent connections
- Cancel delayed ACK timer in connection terminate to prevent timer messages to dead processes
- Session ticket table now has TTL (7 days) and size limit (10,000 entries) to prevent unbounded memory growth
- Listener now properly cleans up ETS tables on terminate (standalone mode only, pool mode tables are managed by the pool manager)
- Draining state now uses calculated
3 * PTOtimeout per RFC 9000 Section 10.2 instead of hardcoded 3 seconds - Pre-connection pending data queue now has size limit (1000 entries) to prevent memory exhaustion from slow handshakes
- Buffer contiguity calculation now has iteration limit to prevent stack overflow with highly fragmented receive buffers
- Stream data is now properly reassembled before delivery: previously data was delivered immediately as received, causing corruption when packets arrived out of order during large file transfers. Data is still streamed incrementally as contiguous chunks become available
- Server connections no longer modify listener's socket active state: server-side
connections were calling
inet:setopts(Socket, [{active, once}])on the shared listener socket, overriding the listener's{active, N}configuration and causing the socket to go passive after receiving packets
[0.9.0] - 2026-02-20
Added
- Multi-pool server support with ranch-style named server pools
quic:start_server/3to start named server with connection poolingquic:stop_server/1to stop named serverquic:get_server_info/1to get server information (pid, port, opts, started_at)quic:get_server_port/1to get server listening portquic:get_server_connections/1to get server connection PIDsquic:which_servers/0to list all running servers- Application supervision structure (
quic_app,quic_sup,quic_server_sup) - ETS-based server registry (
quic_server_registry) with process monitoring pool_sizeoption for listener process pooling with SO_REUSEPORT- FreeBSD CI testing workflow
- Expanded Linux CI matrix (Ubuntu 22.04/24.04, OTP 26-28)
Changed
quic.app.srcnow includes{mod, {quic_app, []}}for OTP application behaviour- Listener supervisor registers with server registry on init for restart recovery
[0.8.0] - 2026-02-20
Added
- Stream prioritization (RFC 9218): urgency-based scheduling with 8 priority levels (0-7) and incremental delivery flag
quic:set_stream_priority/4andquic:get_stream_priority/2API- Bucket-based priority queue for O(1) stream scheduling
- Preferred address handling (RFC 9000 Section 9.6): server can advertise a preferred address during handshake, client validates via PATH_CHALLENGE and automatically migrates to validated preferred address
preferred_ipv4andpreferred_ipv6listener options for server configuration#preferred_address{}record for IPv4/IPv6 addresses, CID, and reset tokenquic_tls:encode_preferred_address/1andquic_tls:decode_preferred_address/1- Idle timeout enforcement (RFC 9000 Section 10.1): when
idle_timeoutoption is set, internal timer automatically closes connection after timeout with no activity (set to 0 to disable) - Persistent congestion detection (RFC 9002 Section 7.6): detects prolonged packet loss spanning > PTO * 3 and resets cwnd to minimum window
- Frame coalescing: ACK frames are coalesced with small pending stream data (< 500 bytes) for more efficient packet utilization
[0.7.1] - 2026-02-20
Fixed
- Packet number reconstruction per RFC 9000 Appendix A: truncated packet numbers are now properly reconstructed using the largest received PN, fixing decryption failures for large responses (>255 packets with 1-byte PN encoding)
[0.7.0] - 2026-02-20
Added
- Docker interop runner integration (client and server images)
- Session resumption interop test (
resumption) - 0-RTT early data interop test (
zerortt) - Connection migration interop test (
connectionmigration) quic:migrate/1API for triggering active path migration- All 10 QUIC Interop Runner test cases now pass:
- handshake, transfer, retry, keyupdate, chacha20, multiconnect, v2, resumption, zerortt, connectionmigration
Fixed
- Connection-level flow control: now properly tracks
data_receivedand sends MAX_DATA frames when 50% of connection window is consumed (RFC 9000 Section 4.1) - Large downloads: interop client now writes to disk incrementally (streaming) instead of accumulating in memory
- Server DCID initialization: server now correctly sets DCID from client's Initial packet SCID field, fixing short header packet alignment
- Key update HP key preservation: header protection keys are no longer rotated during key updates per RFC 9001 Section 6.6
- Fixed bit validation: skip padding bytes (0x00) and invalid short headers (fixed bit not set) in coalesced packets
- Role-based key selection in 1-RTT packet decryption
[0.6.5] - 2026-02-19
Added
quic_listener:start/2for unlinked listener processesset_ownercall handling in idle and handshaking states
Fixed
- IPv4/IPv6 address family matching when opening client sockets
- Race condition: transfer socket ownership before sending packet
- Handle header unprotection errors gracefully in packet decryption
- Removed verbose debug logging from listener
[0.6.4] - 2026-02-17
Fixed
- Server now selects correct signature algorithm based on key type (EC vs RSA)
[0.6.3] - 2026-02-17
Fixed
- Fixed transport params parsing in ClientHello - properly unwrap {ok, Map} result
[0.6.2] - 2026-02-17
Fixed
- Fixed key selection for all packet types based on role (server vs client)
- Server now uses correct keys for both sending and receiving packets
- Fixed Initial, Handshake, and 1-RTT packet encryption/decryption
[0.6.1] - 2026-02-17
Fixed
- Server-side packet decryption now uses correct keys (client keys for Initial/Handshake packets received from clients)
[0.6.0] - 2026-02-17
Added
- DATAGRAM frame support (RFC 9221) for unreliable data transmission
quic:set_owner/2to transfer connection ownership (like gen_tcp:controlling_process/2)quic:peercert/1to retrieve peer certificate (DER-encoded)quic:send_datagram/2to send QUIC datagrams- Connection handler callback in
quic_listenerfor custom connection handling - ACK delay for datagram-only packets per RFC 9221 Section 5.2
- Proper ACK generation at packet level for all ack-eliciting frames
Fixed
- Datagrams are not retransmitted on loss (RFC 9221 compliance)
- ACKs now sent for all ack-eliciting frames, not just stream data
[0.5.1] - 2026-02-17
Fixed
- Pad payload for header protection sampling to prevent crashes during PTO timeout
[0.5.0] - 2026-02-17
Added
- Retry packet handling (RFC 9000 Section 8.1)
- Stateless reset support (RFC 9000 Section 10.3)
- Connection ID limit enforcement (RFC 9000 Section 5.1.1)
- ECN support for congestion control (RFC 9002 Section 7.1)
- RFC 9000/9001 test vectors
- Interoperability test suite with quic-go server
- E2E tests in CI pipeline
Fixed
- CI compatibility with OTP 28 (use rebar3 nightly)
- quic-go Docker build (pin to v0.48.2)
[0.4.0] - 2025-02-17
Changed
- Moved
doc/todocs/to prevent ex_doc from overwriting documentation - Consolidated
hash_len/1andcipher_to_hash/1functions inquic_cryptomodule - Refactored key derivation in
quic_keysusingcipher_params/1helper - Improved socket cleanup on initialization failure in
quic_connection
Removed
- Removed
send_headers/4API (HTTP/3 functionality, not core QUIC transport)
Fixed
- Added bounds checking for header protection sample extraction in
quic_aead - Added CID length validation (max 20 bytes per RFC 9000) in
quic_packet - Added token length validation in
quic_packet - Added frame data length limits in
quic_frameto prevent memory exhaustion - Added ACK range limits in
quic_ackto prevent DoS attacks - Fixed weak random: use
crypto:strong_rand_bytes/1for ticket age_add - Fixed dialyzer warning in
quic_tlsby adding error handling todecode_transport_params/1
[0.3.0] - 2025-02-16
Added
- Server mode with
quic_listenermodule - 0-RTT early data support (RFC 9001 Section 4.6)
- Connection migration support (RFC 9000 Section 9)
- Key update support (RFC 9001 Section 6)
[0.2.0] - 2025-02-15
Added
- Stream multiplexing (bidirectional and unidirectional)
- Flow control (connection and stream level)
- Congestion control (NewReno)
- Loss detection and packet retransmission (RFC 9002)
[0.1.0] - 2025-02-14
Added
- Initial release
- TLS 1.3 handshake (RFC 8446)
- Basic QUIC transport (RFC 9000)
- AEAD packet protection (RFC 9001)