diff --git a/CHANGELOG.md b/CHANGELOG.md index 538d4b3..dc06c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- IPv6 client connections: `quic:connect/4` accepts a hostname, an IP-literal string (IPv4 or IPv6, optionally bracketed), or an `inet:ip_address()` tuple. Dual-stack hostnames use RFC 8305 Happy Eyeballs (IPv6-first racing) with `happy_eyeballs`, `family`, `connection_attempt_delay` and `connect_timeout` options. A hostname that fails to resolve returns `{error, Reason}` instead of dialing a default address. (#150) +- Listeners can bind to IPv6: pass `inet6` or an IPv6 `{ip, Addr}` in `extra_socket_opts`; the address family is inferred from those options. (#149) + ## [1.4.5] - 2026-05-28 ### Fixed diff --git a/docs/CLIENT_GUIDE.md b/docs/CLIENT_GUIDE.md index aef6b38..a9bf035 100644 --- a/docs/CLIENT_GUIDE.md +++ b/docs/CLIENT_GUIDE.md @@ -88,6 +88,15 @@ transparently. | `socket` | gen_udp:socket() | - | Pre-opened UDP socket | | `extra_socket_opts` | list() | `[]` | Options for socket creation | +### Address Resolution Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `happy_eyeballs` | boolean | true | Race IPv6/IPv4 for dual-stack hostnames (RFC 8305) | +| `family` | `inet \| inet6 \| any` | `any` | Restrict resolution to one address family | +| `connection_attempt_delay` | integer | 250 | Happy Eyeballs stagger between attempts (ms) | +| `connect_timeout` | integer | 5000 | Overall Happy Eyeballs deadline (ms) | + ### Advanced Options | Option | Type | Default | Description | @@ -300,6 +309,35 @@ To prevent migration (e.g., for server-side load balancing): %% You must close it yourself after the connection terminates. ``` +### IPv6 and Happy Eyeballs (RFC 8305) + +`Host` may be a hostname, an IP-literal string (IPv4 or IPv6, optionally +bracketed), or an `inet:ip_address()` tuple. + +```erlang +quic:connect("example.com", 443, #{}, self()). % hostname (Happy Eyeballs) +quic:connect("[2606:4700::1111]", 443, #{}, self()). % IPv6 literal +quic:connect({2606,17008,16#1000,0,0,0,0,1}, 443, #{}, self()). % address tuple +``` + +When a hostname resolves to both IPv4 and IPv6 addresses, the addresses +are raced IPv6-first and the first to complete its handshake is used. In +that case `connect/4` blocks until the first attempt connects (or all +fail / time out), then returns `{ok, Conn}`; the owner still receives the +`{connected, Info}` message. A single resolved address, an IP +literal/tuple, or a pre-opened `socket` keeps the immediate, asynchronous +return. + +```erlang +%% Force IPv6, or disable racing for the legacy IPv4-first resolver. +quic:connect("example.com", 443, #{family => inet6}, self()). +quic:connect("example.com", 443, #{happy_eyeballs => false}, self()). +``` + +With `happy_eyeballs => false`, a hostname resolves IPv4-first (then IPv6) +unless `family => inet6` is set. A hostname that fails to resolve returns +`{error, Reason}` rather than connecting to a default address. + ### 0-RTT Session Resumption ```erlang diff --git a/docs/SERVER_GUIDE.md b/docs/SERVER_GUIDE.md index eb21519..b42908f 100644 --- a/docs/SERVER_GUIDE.md +++ b/docs/SERVER_GUIDE.md @@ -55,6 +55,24 @@ also advertise `secp256r1`. | `max_streams_uni` | integer | 100 | Max unidirectional streams | | `max_datagram_frame_size` | integer | 0 | Datagram support (0 = disabled, RFC 9221) | +### Socket Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `extra_socket_opts` | list() | `[]` | Extra options for the listener socket | + +Pass `inet6` (or an IPv6 `{ip, Addr}`) in `extra_socket_opts` to listen on +IPv6. The address family is inferred from these options; the default is IPv4. + +```erlang +%% Listen on the IPv6 wildcard +quic:start_server(my_server, 4433, Opts#{extra_socket_opts => [inet6]}). + +%% Bind to a specific IPv6 address +quic:start_server(my_server, 4433, + Opts#{extra_socket_opts => [{ip, {16#2001, 16#db8, 0, 0, 0, 0, 0, 1}}]}). +``` + ### Server Pool Options | Option | Type | Default | Description | diff --git a/docs/features.md b/docs/features.md index a8da5a5..f780693 100644 --- a/docs/features.md +++ b/docs/features.md @@ -11,6 +11,8 @@ cancel and reschedule a timer on every packet - [x] Version negotiation - [x] Retry packets for address validation +- [x] IPv6 client connections: hostname, IP-literal (bracketed or bare), or `inet:ip_address()` tuple host +- [x] Happy Eyeballs v2 (RFC 8305): dual-stack hostnames race IPv6-first, with `happy_eyeballs`, `family`, `connection_attempt_delay` and `connect_timeout` options on `quic:connect/4` - [x] Latency spin bit (RFC 9000 §17.4) with `spin_bit => true | false` - [x] NEW_TOKEN frame dispatch (server rejects peer-received tokens per §8.1.3); client caches received tokens keyed by `{Host, Port}` and reuses them in the Initial of the next connect to the same endpoint - [x] Stateless reset (RFC 9000 §10.3): listener emits resets for orphan packets; per-connection `NEW_CONNECTION_ID` tokens share the listener's HMAC secret so they match orphan-path tokens