Changelog

View Source

All 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/6 emits 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 :status pseudo-header (RFC 9114 §4.3.1). (#112)
  • quic_qpack:set_dynamic_capacity/2 clamps to max_allowed_capacity per RFC 9204 §4.3. (#112)
  • quic:reset_stream/3 keeps the stream entry alive so subsequent quic:stop_sending/3 emits STOP_SENDING instead of returning {error, unknown_stream}. (#113, #115)
  • quic:close/2 with 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/6 so the CLOSE frame reaches the peer when app keys are absent. (#111)

Removed

  • quic_h3_h3spec_SUITE and docker/h3spec/. The corpus is ported into quic_h3_compliance_tests as 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 => socket for client connections. Routes the client through quic_socket:open_for_send/2 so it picks up the OTP socket NIF on Linux with GSO available per-message via cmsg, instead of the gen_udp port 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/2 swaps the underlying socket handle inside a #socket_state{} while preserving batching configuration. Used by the migration rebind path. (#93)
  • Instrumentation counters ack_sent and retransmits on quic_connection:get_stats/1 and the throughput bench output (Phase 0a). (#77, #78)

Fixed

  • quic:migrate/1 on the default gen_udp client no longer drops post-migrate traffic. Rebinding previously left #state.socket_state pointing 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 both net_kernel:connect_node/1 calls indefinitely. The old accept path ran the dist worker through a nine-hop handoff (register_pending / controller rendezvous in acceptor_loop) before reaching dist_util:mark_pending, so net_kernel's tie-breaker arbitration never ran in time. Collapsed to the TCP-dist shape: accept_connection/5 runs set_supervisor + start_timer + handshake_other_started inline. Docker 5-node regression now passes 5/5. (#106)
  • quic_dist: batch-yield path in input_handler_loop could 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/2 doc: 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, so global does 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_loop exits 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 the socket + {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_batching is false so the direct-send path uses the same quic_socket shape 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/1 on the bulk-upload hot path. (#82)
  • Thread the updated socket_state back from do_socket_send via the return value, dropping the process-dictionary roundtrip. (#83)
  • Replace the crypto:exor/2 NIF call with inline Erlang XOR for the 1-4 byte header-protection mask. (#84)
  • Inline the ?QLOG_ENABLED check at packet/frame event call sites so the event-map is never built when qlog is off. (#86)
  • Coalesce the monotonic_time samples 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_SEGMENT setsockopt and rely on per-message cmsg via flush_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_socket batch buffer that reuses the listener's UDP socket. Gated by the new server_send_batching option on start_server/3 (default true); set to false to fall back to the previous direct gen_udp:send/4 path. (#66)
  • quic_socket:info/1 — map with backend, gso_supported, gso_size, gro_enabled, batching_enabled, max_batch_packets, and the new batch_flushes / packets_coalesced counters.
  • 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/1 now returns batch_flushes and packets_coalesced so 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 assert packets_coalesced > 1 when 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.erl and quic_throughput_bench:run_download_sink/0,1 drive server-to-client bulk transfers and report MB/s alongside batch_flushes / packets_coalesced so the batching effect is visible next to throughput.

Changed

  • Stream send path is iovec-native. quic_frame:encode_iodata/1 returns [Header, Data] and threads iodata through header protection and quic_aead without copying Data into a fresh binary. AEAD specs relaxed to accept iodata.
  • 1-RTT ACKs delayed to every 2nd packet or max_ack_delay per 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_loss switched to a single queue: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/1 passes the batch as an iov list directly to socket:sendmsg/2 with 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/3 samples monotonic_time once per packet and reuses it for loss tracking and last_activity. (#71)
  • Per-packet overhead on the bulk-send path reduced: single #state{} update, PTO timer reschedule skipped when within tolerance, process_send_queue and pacing timeout short-circuit on empty queue, stream data normalised to binary once at the fragmentation boundary.
  • state_to_map/1 replaces the coarse send_batching boolean with three explicit fields: send_backend (direct | gen_udp | socket), send_batching_enabled, send_gso_supported.

Fixed

  • Server connection crashed with function_clause when the listener was on socket_backend => socket because inet:sockname/1 rejects {'$socket', Ref} handles. Branch on socket shape: socket:sockname/1 for OTP socket handles, inet:sockname/1 for gen_udp ports.
  • UDP_SEGMENT setsockopt now uses sizeof(int) (32-bit native) instead of u16, which Linux rejected with EINVAL; 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_size single-packet payload drops silently on ubuntu-24.04. batch_count == 1 has no segmentation work; fall through to flush_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 at awaiting_encrypted_extensions. flush/1 checks uniformity and falls through to flush_individual when it fails. (#75)
  • Listener self-send: send_packet/6 was calling quic_socket:send/4 and dropping the returned state, so version-negotiation / retry / stateless-reset packets were buffered then lost on the socket backend with batching_enabled=true. Switched to send_immediate/4.
  • send_queue_bytes accounting leaked on ACK-coalesce dequeues and could eventually trip ?MAX_SEND_QUEUE_BYTES on long-lived connections. Added send_queue_count as 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/2 expects a DCID binary, not an info map; returns {ok, HandlerPid} so the listener transfers ownership; peer address fetched via quic:peername/1. (#65)
  • examples/qlog_example.erl: added a connection_handler so 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_handler on fresh peer-initiated bidi streams so extensions can claim them before default request handling (#62)
  • docs: rebar3 ex_doc now 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): priority request 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_handler option on start_server/3 claims 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 receives stream_type_open, stream_type_closed, stream_type_reset, stream_type_stop_sending events
  • Client-initiated extension streams: quic_h3:open_bidi_stream/1,2 pre-claims a bidi stream with a signal-type varint (e.g. WebTransport's 0x41) so inbound bytes route through the claimed-bidi path
  • Per-connection owner override via connection_handler callback on start_server/3 for hosting many sessions per listener
  • Per-stream handler registration: set_stream_handler/3,4, unset_stream_handler/2 to redirect body data to a worker pid
  • Query API: get_settings/1, get_peer_settings/1, get_quic_conn/1
  • Documentation: docs/HTTP3.md reference + 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_server caller pid; durable owners for datagram / stream-type events should be supplied via the per-connection connection_handler callback
  • SETTINGS directionality validation tightened to RFC 9114

Fixed

  • Server connections wedged with connect_timeout when the process that called start_server/3 exited before a client arrived and either h3_datagram_enabled or stream_type_handler was 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 = true exactly once
  • Strict PRIORITY_UPDATE frame parsing per RFC 9218
  • DoS hardening on header / capsule / frame parsing
  • Header / trailer / :path / :status symmetry 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
  • sync option on connect/3 resolves 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

Changed

  • BBR internal clock switched to microseconds; loopback transfers no longer pin to the InitialRtt fallback

Fixed

  • Stream-level MAX_STREAM_DATA window stopped sliding once recv_max_data reached fc_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 past recv_offset like 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_STREAMS as 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_*_SUITE and quic_h3_e2e_SUITE run 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_migration transport parameter
  • Application error code support for CONNECTION_CLOSE frames
  • Client certificate support (verify server 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_size transport 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/1 API for connection packet counts (used for liveness detection)
  • quic:send_ping/1 API 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_timeout errors under heavy load by using QUIC-level activity as liveness proof
  • Stream flow control recv_max_data using 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 catch expressions replaced with try...catch...end
  • Undefined dynamic() type replaced with term() 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/1 with badarg

[0.10.0] - 2026-02-21

Added

  • RFC 9312 QUIC-LB Connection ID encoding support for load balancer routing
  • New quic_lb module 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 configuration
  • lb_config option in quic_listener to enable LB-aware CID generation
  • Variable DCID length support in short header packet parsing
  • LB-aware CID generation in quic_connection for NEW_CONNECTION_ID frames
  • E2E test suite quic_lb_e2e_SUITE with 21 integration tests
  • quic:server_spec/3 to get a child spec for embedding QUIC servers in custom supervision trees
  • Stream reassembly test suite quic_stream_reassembly_SUITE for ordered delivery verification

Changed

Fixed

  • quic:get_server_port/1 now returns the actual OS-assigned port when server was started with port 0 (ephemeral port), instead of returning 0
  • quic:get_server_connections/1 now correctly returns connection PIDs; was returning empty list due to get_listeners/1 returning supervisor pids instead of actual listener processes
  • Removed redundant link/1 call in listener (connection already linked via gen_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 * PTO timeout 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/3 to start named server with connection pooling
  • quic:stop_server/1 to stop named server
  • quic:get_server_info/1 to get server information (pid, port, opts, started_at)
  • quic:get_server_port/1 to get server listening port
  • quic:get_server_connections/1 to get server connection PIDs
  • quic:which_servers/0 to 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_size option 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.src now 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/4 and quic:get_stream_priority/2 API
  • 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_ipv4 and preferred_ipv6 listener options for server configuration
  • #preferred_address{} record for IPv4/IPv6 addresses, CID, and reset token
  • quic_tls:encode_preferred_address/1 and quic_tls:decode_preferred_address/1
  • Idle timeout enforcement (RFC 9000 Section 10.1): when idle_timeout option 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/1 API 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_received and 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/2 for unlinked listener processes
  • set_owner call 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/2 to transfer connection ownership (like gen_tcp:controlling_process/2)
  • quic:peercert/1 to retrieve peer certificate (DER-encoded)
  • quic:send_datagram/2 to send QUIC datagrams
  • Connection handler callback in quic_listener for 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/ to docs/ to prevent ex_doc from overwriting documentation
  • Consolidated hash_len/1 and cipher_to_hash/1 functions in quic_crypto module
  • Refactored key derivation in quic_keys using cipher_params/1 helper
  • Improved socket cleanup on initialization failure in quic_connection

Removed

  • Removed send_headers/4 API (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_frame to prevent memory exhaustion
  • Added ACK range limits in quic_ack to prevent DoS attacks
  • Fixed weak random: use crypto:strong_rand_bytes/1 for ticket age_add
  • Fixed dialyzer warning in quic_tls by adding error handling to decode_transport_params/1

[0.3.0] - 2025-02-16

Added

  • Server mode with quic_listener module
  • 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)