§7.4
Chapter 07 · Transport Parameterization and Negotiation

§7.4Version Negotiation and Compatibility Handling

RFC 9000RFC 8999RFC 9368

Learning objective. Understand how a client and server agree on a QUIC version: the invariant version field every packet carries, the Version Negotiation packet for the incompatible case, and the modern compatible negotiation that upgrades within the first flight, plus how both are protected against downgrade.

The one field every QUIC version shares #

Almost nothing about QUIC is guaranteed across versions, but a tiny invariant core is ([RFC 8999]). Every long-header packet carries a 32-bit Version field, and the connection-ID fields around it have a fixed shape, so any endpoint, even one that doesn't speak the version, can read the version and the connection IDs ([RFC 8999 §5.1]). Two version values are special:

  • 0x00000001 is QUIC v1 ([RFC 9000 §15]); 0x6b3343cf is QUIC v2 ([RFC 9369 §3.1]) (constants.md §7a).
  • 0x00000000 is reserved: it marks a Version Negotiation packet ([RFC 8999 §5.4, §6]).

Because the version field is invariant, version negotiation can happen before any version-specific parsing — the server reads the client's version from the first Initial and decides whether it can proceed.

The incompatible case: a Version Negotiation packet #

If the server does not support the client's chosen version, it replies with a Version Negotiation packet listing the versions it does support ([RFC 9000 §6.1]):

ServerClientIncompatible — server doesn't supportthe client's versionpick a supportedversion, start over(adds a round trip)Compatible — negotiate inside the firstflight (no extra RTT)server supports v2and v1's flight iscompatible →switch to v2v2 was in myavailable list →accept · elseVERSION_NEGOT-IATION_ERRORInitial · Version = 0xff00abcd(unsupported)Version Negotiation packet ·Version=0 · supports [0x00000001,0x6b3343cf]Initial · Version = 0x00000001 (v1)Initial v1 · version_information{chosen=v1, available=[v2, v1] }handshake in v2 ·version_information{ chosen=v2,available=[v2, v1] }
Fig. 7.4-1Two paths. Top — the server doesn't support the client's version and returns a Version Negotiation packet (Version 0) listing what it supports; the client restarts with a supported version, costing a round trip. Bottom — compatible negotiation carries a version_information transport parameter in the first flight, letting the server upgrade v1→v2 with no extra round trip; the chosen version is validated against the client's advertised list to block downgrades.RFC 9000 §6; RFC 9368 §2.3, §4

The Version Negotiation packet has its Version field set to 0 and carries a list of Supported Version values ([RFC 8999 §6]). It is stateless (the server retains no state) and correspondingly not authenticated, which is the important caveat: an on-path attacker could forge one. RFC 9000 acknowledges this directly, noting the packet has no built-in downgrade protection and that a client "MUST discard a Version Negotiation packet that lists the QUIC version selected by the client" and MUST ignore VN packets once it has processed any other packet ([RFC 9000 §6.2]). A client that acts on one abandons its attempt and restarts with a version from the list — costing a full round trip.

The compatible case: negotiate inside the first flight #

The Version Negotiation packet's round-trip cost and lack of authentication motivated Compatible Version Negotiation ([RFC 9368]). The idea: two versions are compatible if a first flight of one "can [be converted] into a first flight" of the other ([RFC 9368 §2.2]), so a server that receives a client's first flight in version A but prefers a compatible version B can just continue the handshake in B — no VN packet, no extra round trip ([RFC 9368 §2.3]).

The mechanism is a transport parameter, version_information (0x11), that each side sends in the handshake ([RFC 9368 §3]). It contains:

  • a Chosen Version, the version the sender is using; and
  • a list of Available Versions: for the client, "all the versions that this first flight is compatible with, ordered by descending preference"; for the server, the versions its deployment offers ([RFC 9368 §3]).

Because this rides in the authenticated handshake (§7.1), it does what the VN packet cannot: prevent downgrades.

Downgrade prevention #

The version_information exchange is validated on both ends ([RFC 9368 §4]):

  • The server "MUST validate that the client's Chosen Version matches the version in use for the connection"; a mismatch is a VERSION_NEGOTIATION_ERROR (0x11, constants.md §7).
  • The client, if the server's Chosen Version "was not sent by the client as part of its Available Versions," MUST close with the same error.

An attacker who forges a Version Negotiation packet to force a weaker version therefore fails: the authenticated version_information in the eventual handshake won't match what an untampered negotiation would have produced, and one side detects it. Clients also "MUST ignore any received Version Negotiation packets that contain the Original Version" ([RFC 9368 §4]).

GREASE versions #

To keep the negotiation machinery exercised, versions matching the pattern 0x?a?a?a?a are reserved and will never be selected ([RFC 9000 §15], constants.md §7a). A client may advertise one (in its available_versions list or even as its chosen version) specifically to check that the server correctly ignores unknown versions or triggers negotiation, rather than mishandling them. It is the version-layer analogue of the GREASE frame types and settings seen elsewhere (§5.1).

Worked example: a v1→v2 upgrade with no round trip #

Follow the two paths in Figure 7.4-1. In the incompatible path, the client sends an Initial with an unsupported version; the server returns a Version Negotiation packet (Version 0) advertising 0x00000001 and 0x6b3343cf; the client picks one and starts a fresh handshake: one wasted round trip. In the compatible path, the client sends its first flight in v1 but includes version_information with chosen = v1, available = [v2, v1]. The server supports v2 and knows v1's flight is compatible, so it simply continues the handshake in v2 and echoes chosen = v2, available = [v2, v1]. The client checks that v2 was in the list it advertised (it was) and proceeds. The upgrade cost nothing, and any tampering would have tripped VERSION_NEGOTIATION_ERROR.

Note

The unauthenticated Version Negotiation packet and the authenticated version_information transport parameter solve different halves of the problem. The VN packet is a discovery tool for the case where the server can't parse the client's version at all. It is necessarily stateless and unauthenticated because there is no shared secret yet. Compatible negotiation is an optimization and hardening for versions the server can parse, moving the decision into the authenticated handshake. A deployment supporting v1 and v2 uses the second and rarely needs the first.

In practice

editorial Don't hand-roll version handling: enable your library's compatible-negotiation support and let it manage version_information. The failure to watch for is a middlebox or a peer that mishandles an unknown version field: if you plan to deploy a new version, test with a GREASE version first to confirm both your stack and the paths you care about either negotiate or cleanly ignore it, rather than dropping the connection. And treat a VERSION_NEGOTIATION_ERROR in logs as a possible downgrade attempt rather than a routine config mismatch. It is precisely the signal the mechanism exists to raise.

Takeaways #

Every QUIC packet carries an invariant 32-bit version field, with 0x00000000 marking the unauthenticated, stateless Version Negotiation packet used when the server can't speak the client's version at all. Modern deployments prefer compatible negotiation: a version_information transport parameter carried in the authenticated handshake lets a server upgrade within the first flight and lets both ends detect downgrade attempts via VERSION_NEGOTIATION_ERROR. GREASE versions (0x?a?a?a?a) keep the machinery honest. With the connection's version and parameters all chosen, §7.5 works a complete example: selecting a full parameter set for a target network profile.