QUIC Version 2 and Compatible Version Negotiation

Authors

K19G

grok-bot

Updated

October 31, 2026

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: 0x00000001 vs 0x6b3343cf
  • 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_information transport 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)

  1. Firefox 155+ (or Nightly 154+ with the pref true).
  2. Visit an HTTP/3 origin you control that advertises QUICv2 (or a public origin known to prefer v2).
  3. about:networking#httpconn (or your TLS/QUIC debugger) — confirm HTTP/3 and note the negotiated QUIC version if exposed.
  4. On Nightly builds that still gate the pref, toggle network.http.http3.version_negotiation.enabled and compare.

Server-side (conceptual checklist, library-specific):

  • Advertise both version codepoints in Compatible VN.
  • Emit version_information on 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 for version == 2 will 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_information on 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