Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions docs/CLIENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions docs/SERVER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 2 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading