QUIC Version 2 and Compatible Version Negotiation
QUIC Version 2 and Compatible Version Negotiation
QUIC version 1 (RFC 9000) won the Internet. That success is also a risk: middleboxes that only ever see 0x00000001 in the long-header Version field, or that hard-code the version-1 Initial salt, will quietly break the next version. QUIC version 2 (RFC 9369) is deliberately almost identical to version 1 — same streams, same TLS, same congestion control — with a handful of wire-image changes whose only job is to break that ossification and to exercise the negotiation machinery. Compatible version negotiation (RFC 9368) is how endpoints switch between v1 and v2 (or any pair of compatible versions) without paying an extra round trip. This chapter teaches the Version field, Compatible VN versus Incompatible VN, when v2 actually matters on the wire, and how to observe it in a modern browser stack.
Learning goals
By the end of this chapter you can:
- State what QUICv2 changes (and what it deliberately does not change)
- Decode a long-header Version field:
0x00000001vs0x6b3343cf
- Explain Compatible VN vs Incompatible VN, and why v1 ↔︎ v2 is Compatible
- Predict which packets keep the Original Version and which must use the Negotiated Version
- Name the
version_informationtransport parameter (0x11) and why v2 endpoints MUST use it
- Observe negotiated version in Firefox (Nightly 154+, all channels from 155) and reason about server support
Concepts — why a “version 2” that does nothing new?
RFC 9369 is explicit: QUICv2 exists to mitigate ossification and to exercise version negotiation. Application capabilities are unchanged. ALPN tokens defined for QUICv1 (h3, doq, …) also work over v2. Extensions that work with v1 work with v2 unless they say otherwise.
What does change:
| Knob | QUICv1 (RFC 9000 / 9001) | QUICv2 (RFC 9369) |
|---|---|---|
| Long-header Version field | 0x00000001 |
0x6b3343cf |
| Long-header Type bits | Initial 0b00, 0-RTT 0b01, Handshake 0b10, Retry 0b11 |
Initial 0b01, 0-RTT 0b10, Handshake 0b11, Retry 0b00 |
| Initial salt | v1 salt | 0x0dede3def700a6db819381be6e269dcbf9bd2ed9 |
| HKDF labels | quic key / quic iv / quic hp / quic ku |
quicv2 key / quicv2 iv / quicv2 hp / quicv2 ku |
| Retry Integrity Tag keying | v1 secret | v2 secret / key / nonce (RFC 9369 §3.3.3) |
The version number is not the integer 2. It is the first four bytes of sha256("QUICv2 version number"), chosen randomly on purpose so middleboxes cannot guess “the next number”. A provisional draft codepoint 0x709a50c4 also sits in the IANA registry for historical draft traffic.
Long header (RFC 8999 invariants):
Header Form (1) = 1
Fixed Bit (1) = 1
Long Packet Type (2) ← different encoding in v2
Type-Specific (4)
Version (32) ← 0x00000001 or 0x6b3343cf
DCID Len / DCID / SCID Len / SCID / …
Concepts — Compatible VN vs Incompatible VN
RFC 9000 gave servers a Version Negotiation packet that says “I do not speak the version you chose”, but it never defined a safe client reaction. RFC 9368 fills that hole with two mechanisms:
Incompatible VN. Server cannot parse the client’s first flight. It sends a Version Negotiation packet listing Offered Versions. Client starts a new connection attempt with a mutually supported version. Cost: one extra RTT. Works between any versions.
Compatible VN. Server can parse the first flight, extracts the client’s Version Information, and may switch to another version that (1) it supports and (2) it knows is compatible with the client’s Chosen Version. It converts the first flight and replies as if that converted flight had arrived. Cost: zero extra RTT, but only when the versions share a convertible first-flight format.
RFC 9369 states that v2 is compatible with v1 and v1 is compatible with v2. Endpoints that speak both SHOULD use Compatible VN.
Compatible VN (happy path, client offers v1+v2, prefers v1 as Chosen):
Client Server
Chosen=v1, Available=(v1,v2) --------------->
(server prefers v2, converts first flight)
<--------------- CRYPTO in v2 long headers
(client sees Version 0x6b3343cf ≠ Chosen) → Negotiated = v2
subsequent Initial / Handshake / 1-RTT use v2
Version Information (transport parameter 0x11)
Version Information {
Chosen Version (32),
Available Versions (32) ...,
}
- Client Available Versions: every version this first flight is compatible with, preference order descending. Chosen Version MUST appear in the list.
- Server Available Versions: the deployment’s Fully Deployed Versions (may be empty during rollouts; ordering carries no meaning).
- QUICv2 endpoints MUST send, process, and validate this parameter (downgrade defence). QUICv1 endpoints that only ever speak v1 may omit it; RFC 9368 §8 special-cases that.
Packet-version rules after a Compatible switch (v1 ↔︎ v2)
| Packet class | Rule |
|---|---|
| Retry | Always Original Version; client must not switch version in the next Initial that carries the Retry token |
| Server Initial before client TP processed | May use Original Version |
| All CRYPTO frames from the server | Negotiated Version |
| Handshake and 1-RTT | MUST be Negotiated Version; other versions dropped |
| Client 0-RTT | MUST stay on Original Version even after seeing Negotiated Version |
| Session tickets / NEW_TOKEN | Version-scoped; do not resume v1 tickets on a v2 connection (and vice versa). After Compatible VN, issued tokens belong to the Negotiated version |
The client learns the Negotiated Version by watching the first long-header Version field that differs from the Original Version. If the server answers with CRYPTO still in the Original Version, negotiation landed on Original == Negotiated.
When v2 matters vs when it does not
| Situation | Prefer |
|---|---|
| Fighting middleboxes that ossify on v1 Initial crypto / Version bytes | Offer v2; some clients start with v2 when they know the server supports it (accepts Incompatible VN cost on miss) |
Maximising reach to old clients / Alt-Svc h3 without version signalling |
Keep speaking v1; RFC 9369 says HTTP servers SHOULD support multiple versions |
| Measuring deployment health | Log negotiated version per handshake (Firefox Glean: networking.http_3_version) |
| Application features (streams, datagrams, HTTP/3) | Irrelevant — v2 adds none |
v2 does not deprecate v1. Treating “we enabled v2” as “we can drop v1” breaks clients that only offer v1, and Alt-Svc still does not distinguish versions.
Worked observation — Firefox and Compatible VN
As of mid-2026, Mozilla’s neqo stack can offer QUICv2. Bug 2040450 enabled Compatible VN on Firefox Nightly 154 via the pref network.http.http3.version_negotiation.enabled (Nightly-only at first). Bug 2059947 later set that pref to true on all channels (Firefox 155). When the pref is true, the client offers Version::all() (v2 preferred, then v1); when false, v1 only. The server still chooses:
Client offers: [v2, v1] Server speaks only v1 → connection uses v1
Client offers: [v2, v1] Server prefers v2 → Compatible VN → v2
Client offers: [v1] Server prefers v2 → stays v1 (client never offered v2)
Try this (browser)
- Firefox 155+ (or Nightly 154+ with the pref true).
- Visit an HTTP/3 origin you control that advertises QUICv2 (or a public origin known to prefer v2).
about:networking#httpconn(or your TLS/QUIC debugger) — confirm HTTP/3 and note the negotiated QUIC version if exposed.
- On Nightly builds that still gate the pref, toggle
network.http.http3.version_negotiation.enabledand compare.
Server-side (conceptual checklist, library-specific):
- Advertise both version codepoints in Compatible VN.
- Emit
version_informationon every v2 (and dual-stack) handshake.
- Log Original vs Negotiated Version.
- Keep issuing version-scoped session tickets.
- Roll versions through Acceptable → Offered → Fully Deployed with ≥1 MSL between steps (RFC 9368 §5) so downgrade detection does not false-positive during fleet upgrades.
Libraries commonly cited for dual-version support include quiche, quic-go, and browser stacks built on neqo / NSS-adjacent HTTP/3 plumbing. Always check the current release notes for your pin — Compatible VN and v2 landed at different times in different stacks.
Mental model — one diagram
┌─ Incompatible VN (VN packet, +1 RTT)
first flight ──────┤
(Chosen Version) └─ Compatible VN (convert flight, 0 RTT)
│
▼
Negotiated Version
(Handshake + 1-RTT live here)
The trap
- Assuming the Version field integer “2”. The wire value is
0x6b3343cf. Fingerprinting scripts that look forversion == 2will miss every real v2 packet.
- Enabling v2 only on the client. Without servers that select v2 from Available Versions, you negotiate v1 forever and learn nothing.
- Reusing tickets across versions. A v1 session ticket on a v2 attempt must be rejected (full handshake, no 0-RTT).
- Skipping
version_informationon a v2 endpoint. Mandatory; without it you lose downgrade protection and violate RFC 9369.
- Expecting application-visible differences. If your HTTP/3 metrics are identical on v1 and v2, that is success — the change is at the transport wire image.
The boring rule
Offer QUICv1 and QUICv2 together. Use Compatible VN so the upgrade is free when both sides agree. Keep v1 around for reachability. Authenticate negotiation with version_information. Measure which version you actually land on.
Sources
- RFC 9369 — QUIC Version 2: https://www.rfc-editor.org/rfc/rfc9369.html
- RFC 9368 — Compatible Version Negotiation for QUIC: https://www.rfc-editor.org/rfc/rfc9368.html
- RFC 9000 — QUIC: https://www.rfc-editor.org/rfc/rfc9000.html
- RFC 8999 — Version-Independent Properties of QUIC: https://www.rfc-editor.org/rfc/rfc8999.html
- Firefox Nightly 154 notes (Bug 2040450): https://www.firefox.com/en-US/firefox/154.0a1/releasenotes/
- Bug 2040450 — Enable QUIC version negotiation: https://bugzilla.mozilla.org/show_bug.cgi?id=2040450
- Bug 2059947 — Enable QUIC v2 VN on all channels (Firefox 155): https://bugzilla.mozilla.org/show_bug.cgi?id=2059947