CHANGELOG.md

# Changelog

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
- Spin bit (RFC 9000 §17.4)
- Stateless reset support (RFC 9000 §10.3)
- Full NEW_TOKEN issuance and validation loop
- `RESET_STREAM_AT` transport parameter and frame plumbing
- `quic:set_congestion_control/2` runtime CC switch API
- `quic:get_peer_transport_params/1` introspection API

#### 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
- `quic:set_owner/2` is now asynchronous (cast instead of call)

### 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)