RFC 9000

QUIC: A UDP-Based Multiplexed and Secure Transport

๐Ÿ“… published May 2021, from ietf

RFC 9000: QUIC ๐Ÿฆ‹

Okay bestie, this is RFC 9000, and it's called QUIC: A UDP-Based Multiplexed and Secure Transport โœจ It's an Internet Standards Track document from May 2021, published by the IETF with ISSN 2070-1721. The editors are J. Iyengar, Ed., at Fastly, and M. Thomson, Ed., at Mozilla ๐Ÿ’œ Those two are literally the main authors of QUIC's core, so it's giving legendary ๐Ÿ‘‘ The header doesn't list anything this RFC obsoletes or updates ๐Ÿงš

Abstract โœจ

So this document defines the core of the QUIC transport protocol ๐ŸŽ€ QUIC gives apps flow-controlled streams for structured communication, low-latency connection setup, and network path migration (basically your connection can follow you when your network changes) ๐ŸŒธ It also has security measures that keep things confidential, intact, and available across lots of different deployment setups ๐Ÿ” Other documents cover how TLS plugs in for key negotiation, how loss detection works, and an example congestion control algorithm ๐Ÿ’…

Status of This Memo ๐ŸŒˆ

This is an Internet Standards Track document ๐ŸŽ€

It's a product of the Internet Engineering Task Force (IETF) and represents the consensus of the IETF community, so a lot of people looked at it ๐Ÿซถ It got public review and was approved for publication by the Internet Engineering Steering Group (IESG) ๐Ÿ’– If you want more on Internet Standards, check out Section 2 of RFC 7841 ๐Ÿฆ‹

For the current status, any errata, or to send feedback, go to https://www.rfc-editor.org/info/rfc9000 ๐Ÿ“

Copyright (c) 2021 IETF Trust and the people listed as document authors, all rights reserved ๐ŸŒธ

This document is covered by BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info), as they stood when it was published ๐Ÿ“œ Please actually read those, because they explain your rights and restrictions ๐Ÿ™ˆ

One important bit: any Code Components you pull out of this document have to include Simplified BSD License text, as described in Section 4.e of the Trust Legal Provisions, and they come with no warranty, as described in the Simplified BSD License โญ


Table of Contents ๐Ÿงš

Here's the full map of the doc, copied exactly so every section number lines up with the references ๐Ÿ’–

1.  Overview
  1.1.  Document Structure
  1.2.  Terms and Definitions
  1.3.  Notational Conventions
2.  Streams
  2.1.  Stream Types and Identifiers
  2.2.  Sending and Receiving Data
  2.3.  Stream Prioritization
  2.4.  Operations on Streams
3.  Stream States
  3.1.  Sending Stream States
  3.2.  Receiving Stream States
  3.3.  Permitted Frame Types
  3.4.  Bidirectional Stream States
  3.5.  Solicited State Transitions
4.  Flow Control
  4.1.  Data Flow Control
  4.2.  Increasing Flow Control Limits
  4.3.  Flow Control Performance
  4.4.  Handling Stream Cancellation
  4.5.  Stream Final Size
  4.6.  Controlling Concurrency
5.  Connections
  5.1.  Connection ID
    5.1.1.  Issuing Connection IDs
    5.1.2.  Consuming and Retiring Connection IDs
  5.2.  Matching Packets to Connections
    5.2.1.  Client Packet Handling
    5.2.2.  Server Packet Handling
    5.2.3.  Considerations for Simple Load Balancers
  5.3.  Operations on Connections
6.  Version Negotiation
  6.1.  Sending Version Negotiation Packets
  6.2.  Handling Version Negotiation Packets
  6.3.  Using Reserved Versions
7.  Cryptographic and Transport Handshake
  7.1.  Example Handshake Flows
  7.2.  Negotiating Connection IDs
  7.3.  Authenticating Connection IDs
  7.4.  Transport Parameters
    7.4.1.  Values of Transport Parameters for 0-RTT
    7.4.2.  New Transport Parameters
  7.5.  Cryptographic Message Buffering
8.  Address Validation
  8.1.  Address Validation during Connection Establishment
    8.1.1.  Token Construction
    8.1.2.  Address Validation Using Retry Packets
    8.1.3.  Address Validation for Future Connections
    8.1.4.  Address Validation Token Integrity
  8.2.  Path Validation
    8.2.1.  Initiating Path Validation
    8.2.2.  Path Validation Responses
    8.2.3.  Successful Path Validation
    8.2.4.  Failed Path Validation
9.  Connection Migration
  9.1.  Probing a New Path
  9.2.  Initiating Connection Migration
  9.3.  Responding to Connection Migration
    9.3.1.  Peer Address Spoofing
    9.3.2.  On-Path Address Spoofing
    9.3.3.  Off-Path Packet Forwarding
  9.4.  Loss Detection and Congestion Control
  9.5.  Privacy Implications of Connection Migration
  9.6.  Server's Preferred Address
    9.6.1.  Communicating a Preferred Address
    9.6.2.  Migration to a Preferred Address
    9.6.3.  Interaction of Client Migration and Preferred Address
  9.7.  Use of IPv6 Flow Label and Migration
10. Connection Termination
  10.1.  Idle Timeout
    10.1.1.  Liveness Testing
    10.1.2.  Deferring Idle Timeout
  10.2.  Immediate Close
    10.2.1.  Closing Connection State
    10.2.2.  Draining Connection State
    10.2.3.  Immediate Close during the Handshake
  10.3.  Stateless Reset
    10.3.1.  Detecting a Stateless Reset
    10.3.2.  Calculating a Stateless Reset Token
    10.3.3.  Looping
11. Error Handling
  11.1.  Connection Errors
  11.2.  Stream Errors
12. Packets and Frames
  12.1.  Protected Packets
  12.2.  Coalescing Packets
  12.3.  Packet Numbers
  12.4.  Frames and Frame Types
  12.5.  Frames and Number Spaces
13. Packetization and Reliability
  13.1.  Packet Processing
  13.2.  Generating Acknowledgments
    13.2.1.  Sending ACK Frames
    13.2.2.  Acknowledgment Frequency
    13.2.3.  Managing ACK Ranges
    13.2.4.  Limiting Ranges by Tracking ACK Frames
    13.2.5.  Measuring and Reporting Host Delay
    13.2.6.  ACK Frames and Packet Protection
    13.2.7.  PADDING Frames Consume Congestion Window
  13.3.  Retransmission of Information
  13.4.  Explicit Congestion Notification
    13.4.1.  Reporting ECN Counts
    13.4.2.  ECN Validation
14. Datagram Size
  14.1.  Initial Datagram Size
  14.2.  Path Maximum Transmission Unit
    14.2.1.  Handling of ICMP Messages by PMTUD
  14.3.  Datagram Packetization Layer PMTU Discovery
    14.3.1.  DPLPMTUD and Initial Connectivity
    14.3.2.  Validating the Network Path with DPLPMTUD
    14.3.3.  Handling of ICMP Messages by DPLPMTUD
  14.4.  Sending QUIC PMTU Probes
    14.4.1.  PMTU Probes Containing Source Connection ID
15. Versions
16. Variable-Length Integer Encoding
17. Packet Formats
  17.1.  Packet Number Encoding and Decoding
  17.2.  Long Header Packets
    17.2.1.  Version Negotiation Packet
    17.2.2.  Initial Packet
    17.2.3.  0-RTT
    17.2.4.  Handshake Packet
    17.2.5.  Retry Packet
  17.3.  Short Header Packets
    17.3.1.  1-RTT Packet
  17.4.  Latency Spin Bit
18. Transport Parameter Encoding
  18.1.  Reserved Transport Parameters
  18.2.  Transport Parameter Definitions
19. Frame Types and Formats
  19.1.  PADDING Frames
  19.2.  PING Frames
  19.3.  ACK Frames
    19.3.1.  ACK Ranges
    19.3.2.  ECN Counts
  19.4.  RESET_STREAM Frames
  19.5.  STOP_SENDING Frames
  19.6.  CRYPTO Frames
  19.7.  NEW_TOKEN Frames
  19.8.  STREAM Frames
  19.9.  MAX_DATA Frames
  19.10. MAX_STREAM_DATA Frames
  19.11. MAX_STREAMS Frames
  19.12. DATA_BLOCKED Frames
  19.13. STREAM_DATA_BLOCKED Frames
  19.14. STREAMS_BLOCKED Frames
  19.15. NEW_CONNECTION_ID Frames
  19.16. RETIRE_CONNECTION_ID Frames
  19.17. PATH_CHALLENGE Frames
  19.18. PATH_RESPONSE Frames
  19.19. CONNECTION_CLOSE Frames
  19.20. HANDSHAKE_DONE Frames
  19.21. Extension Frames
20. Error Codes
  20.1.  Transport Error Codes
  20.2.  Application Protocol Error Codes
21. Security Considerations
  21.1.  Overview of Security Properties
    21.1.1.  Handshake
    21.1.2.  Protected Packets
    21.1.3.  Connection Migration
  21.2.  Handshake Denial of Service
  21.3.  Amplification Attack
  21.4.  Optimistic ACK Attack
  21.5.  Request Forgery Attacks
    21.5.1.  Control Options for Endpoints
    21.5.2.  Request Forgery with Client Initial Packets
    21.5.3.  Request Forgery with Preferred Addresses
    21.5.4.  Request Forgery with Spoofed Migration
    21.5.5.  Request Forgery with Version Negotiation
    21.5.6.  Generic Request Forgery Countermeasures
  21.6.  Slowloris Attacks
  21.7.  Stream Fragmentation and Reassembly Attacks
  21.8.  Stream Commitment Attack
  21.9.  Peer Denial of Service
  21.10. Explicit Congestion Notification Attacks
  21.11. Stateless Reset Oracle
  21.12. Version Downgrade
  21.13. Targeted Attacks by Routing
  21.14. Traffic Analysis
22. IANA Considerations
  22.1.  Registration Policies for QUIC Registries
    22.1.1.  Provisional Registrations
    22.1.2.  Selecting Codepoints
    22.1.3.  Reclaiming Provisional Codepoints
    22.1.4.  Permanent Registrations
  22.2.  QUIC Versions Registry
  22.3.  QUIC Transport Parameters Registry
  22.4.  QUIC Frame Types Registry
  22.5.  QUIC Transport Error Codes Registry
23. References
  23.1.  Normative References
  23.2.  Informative References
Appendix A.  Pseudocode
  A.1.  Sample Variable-Length Integer Decoding
  A.2.  Sample Packet Number Encoding Algorithm
  A.3.  Sample Packet Number Decoding Algorithm
  A.4.  Sample ECN Validation Algorithm
Contributors
Authors' Addresses

1. Overview ๐ŸŒธ

Okay bestie, welcome to the main event โœจ. QUIC is a secure general-purpose transport protocol ๐Ÿ”. This document defines version 1 of QUIC, which conforms to the version-independent properties of QUIC defined in [QUIC-INVARIANTS] ๐Ÿ’–.

QUIC is a connection-oriented protocol, so it creates a stateful interaction between a client and a server ๐Ÿค. It's giving "we're in a committed relationship" energy ๐Ÿ’….

The QUIC handshake combines negotiation of cryptographic and transport parameters in one go ๐Ÿ’. QUIC integrates the TLS handshake [TLS13], although it uses a customized framing for protecting packets ๐ŸŽ€. The integration of TLS and QUIC is described in more detail in [QUIC-TLS] ๐Ÿ“–. The handshake is structured so application data can be exchanged as soon as possible ๐Ÿš€. This includes an option for clients to send data immediately (0-RTT), which requires some form of prior communication or configuration to enable โฉ.

Endpoints talk to each other in QUIC by exchanging QUIC packets ๐Ÿ“ฆ. Most packets contain frames, which carry control information and application data between endpoints ๐Ÿงš. QUIC authenticates the entirety of each packet and encrypts as much of each packet as is practical, so it's locked down tight ๐Ÿ”’. QUIC packets are carried in UDP datagrams [UDP] to make deployment in existing systems and networks way easier ๐ŸŒˆ.

Application protocols exchange information over a QUIC connection via streams, which are ordered sequences of bytes ๐ŸŒŠ. There are two types of streams you can make. Bidirectional streams let both endpoints send data, and unidirectional streams let a single endpoint send data ๐Ÿฆ‹. A credit-based scheme is used to limit stream creation and to bound the amount of data that can be sent, so nobody gets to spam the whole thing ๐Ÿ™ˆ.

QUIC gives you the feedback you need to do reliable delivery and congestion control ๐Ÿ“ฌ. An algorithm for detecting and recovering from loss of data is described in Section 6 of [QUIC-RECOVERY] ๐Ÿ”. QUIC depends on congestion control to avoid network congestion ๐Ÿšฆ. An exemplary congestion control algorithm is described in Section 7 of [QUIC-RECOVERY] โญ.

QUIC connections are not strictly bound to a single network path, which is honestly iconic ๐Ÿฆ‹. Connection migration uses connection identifiers to let connections transfer to a new network path ๐Ÿ›ฃ๏ธ. Only clients are able to migrate in this version of QUIC ๐Ÿ™‹โ€โ™€๏ธ. This design also lets connections keep going after changes in network topology or address mappings, such as the ones that might be caused by NAT rebinding ๐ŸŒธ.

Once a connection is established, there are multiple ways to end it ๐ŸŽฌ. Applications can manage a graceful shutdown, endpoints can negotiate a timeout period, errors can cause an immediate teardown, and a stateless mechanism provides for termination of connections after one endpoint has lost state ๐Ÿ’€.

1.1. Document Structure ๐Ÿ—บ๏ธ

This document describes the core QUIC protocol and is laid out like this:

  • Streams are the basic service abstraction that QUIC provides ๐ŸŒŠ.
    • Section 2 describes core concepts related to streams,
    • Section 3 provides a reference model for stream states, and
    • Section 4 outlines the operation of flow control.
  • Connections are the context in which QUIC endpoints communicate ๐Ÿค.
    • Section 5 describes core concepts related to connections,
    • Section 6 describes version negotiation,
    • Section 7 details the process for establishing connections,
    • Section 8 describes address validation and critical denial-of-service mitigations,
    • Section 9 describes how endpoints migrate a connection to a new network path,
    • Section 10 lists the options for terminating an open connection, and
    • Section 11 provides guidance for stream and connection error handling.
  • Packets and frames are the basic unit QUIC uses to communicate ๐Ÿ“ฆ.
    • Section 12 describes concepts related to packets and frames,
    • Section 13 defines models for the transmission, retransmission, and acknowledgment of data, and
    • Section 14 specifies rules for managing the size of datagrams carrying QUIC packets.
  • Finally, the encoding details of QUIC protocol elements are described in:
    • Section 15 (versions),
    • Section 16 (integer encoding),
    • Section 17 (packet headers),
    • Section 18 (transport parameters),
    • Section 19 (frames), and
    • Section 20 (errors) ๐Ÿงพ.

Accompanying documents describe QUIC's loss detection and congestion control [QUIC-RECOVERY], and the use of TLS and other cryptographic mechanisms [QUIC-TLS] ๐Ÿ”‘.

This document defines QUIC version 1, which conforms to the protocol invariants in [QUIC-INVARIANTS] ๐Ÿ’œ.

To refer to QUIC version 1, cite this document. References to the limited set of version-independent properties of QUIC can cite [QUIC-INVARIANTS] ๐Ÿ“š.

1.2. Terms and Definitions ๐Ÿ“

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here ๐Ÿ” . Those words are very serious, so read them carefully ๐Ÿซถ.

Here are the terms that come up a lot in this doc, bestie ๐Ÿ’…:

  • QUIC: The transport protocol described by this document. QUIC is a name, not an acronym ๐ŸŒˆ.
  • Endpoint: An entity that can participate in a QUIC connection by generating, receiving, and processing QUIC packets. There are only two types of endpoints in QUIC: client and server ๐Ÿ‘ฏ.
  • Client: The endpoint that initiates a QUIC connection ๐Ÿšช.
  • Server: The endpoint that accepts a QUIC connection ๐Ÿ›Ž๏ธ.
  • QUIC packet: A complete processable unit of QUIC that can be encapsulated in a UDP datagram. One or more QUIC packets can be encapsulated in a single UDP datagram ๐Ÿ“ฆ.
  • Ack-eliciting packet: A QUIC packet that contains frames other than ACK, PADDING, and CONNECTION_CLOSE. These cause a recipient to send an acknowledgment; see Section 13.2.1 ๐Ÿ™‹.
  • Frame: A unit of structured protocol information. There are multiple frame types, and each one carries different information. Frames are contained in QUIC packets ๐Ÿงฉ.
  • Address: When used without qualification, the tuple of IP version, IP address, and UDP port number that represents one end of a network path ๐Ÿ“.
  • Connection ID: An identifier that is used to identify a QUIC connection at an endpoint. Each endpoint selects one or more connection IDs for its peer to include in packets sent towards the endpoint. This value is opaque to the peer ๐Ÿ†”.
  • Stream: A unidirectional or bidirectional channel of ordered bytes within a QUIC connection. A QUIC connection can carry multiple simultaneous streams ๐Ÿงต.
  • Application: An entity that uses QUIC to send and receive data ๐Ÿ“ฑ.

This document uses the terms "QUIC packets", "UDP datagrams", and "IP packets" to refer to the units of the respective protocols. That is, one or more QUIC packets can be encapsulated in a UDP datagram, which is in turn encapsulated in an IP packet ๐Ÿช†.

1.3. Notational Conventions ๐ŸŽจ

Packet and frame diagrams in this document use a custom format ๐Ÿ–๏ธ. The purpose of this format is to summarize protocol elements, not define them. Prose defines the complete semantics and details of structures, so always check the words too โœ๏ธ.

Complex fields are named and then followed by a list of fields surrounded by a pair of matching braces. Each field in this list is separated by commas ๐Ÿง.

Individual fields include length information, plus indications about fixed value, optionality, or repetitions. Individual fields use the following notational conventions, with all lengths in bits ๐Ÿ“:

  • x (A): Indicates that x is A bits long
  • x (i): Indicates that x holds an integer value using the variable-length encoding described in Section 16
  • x (A..B): Indicates that x can be any length from A to B. A can be omitted to indicate a minimum of zero bits, and B can be omitted to indicate no set upper limit. Values in this format always end on a byte boundary
  • x (L) = C: Indicates that x has a fixed value of C. The length of x is described by L, which can use any of the length forms above
  • x (L) = C..D: Indicates that x has a value in the range from C to D, inclusive, with the length described by L, as above
  • [x (L)]: Indicates that x is optional and has a length of L
  • x (L) ...: Indicates that x is repeated zero or more times and that each instance has a length of L

This document uses network byte order (that is, big endian) values. Fields are placed starting from the high-order bits of each byte ๐Ÿ”ข.

By convention, individual fields reference a complex field by using the name of the complex field ๐Ÿท๏ธ.

Figure 1 provides an example ๐Ÿ‘€:

Example Structure {
  One-bit Field (1),
  7-bit Field with Fixed Value (7) = 61,
  Field with Variable-Length Integer (i),
  Arbitrary-Length Field (..),
  Variable-Length Field (8..24),
  Field With Minimum Length (16..),
  Field With Maximum Length (..128),
  [Optional Field (64)],
  Repeated Field (8) ...,
}

Figure 1: Example Format ๐Ÿฆ„

When a single-bit field is referenced in prose, the position of that field can be clarified by using the value of the byte that carries the field with the field's value set. For example, the value 0x80 could be used to refer to the single-bit field in the most significant bit of the byte, such as One-bit Field in Figure 1 ๐Ÿ’ซ.

2. Streams ๐ŸŒŠ

Streams in QUIC give an application a lightweight, ordered byte-stream abstraction. Streams can be unidirectional or bidirectional ๐Ÿฆ‹.

Streams can be created by sending data ๐Ÿ“ค. The other stream-management stuff, like ending, canceling, and managing flow control, is all designed to add minimal overhead ๐Ÿชถ. For instance, a single STREAM frame (Section 19.8) can open a stream, carry data for it, and close it, all in one go ๐ŸŽฏ. Streams can also be long-lived and last the entire duration of a connection ๐Ÿ’œ.

Streams can be created by either endpoint, can concurrently send data interleaved with other streams, and can be canceled ๐Ÿ™Œ. QUIC does not provide any means of ensuring ordering between bytes on different streams, so don't expect cross-stream order ๐Ÿ™…โ€โ™€๏ธ.

QUIC allows an arbitrary number of streams to operate concurrently, and an arbitrary amount of data to be sent on any stream, subject to flow control constraints and stream limits; see Section 4 ๐Ÿ‘‘โœจ.

2.1. Stream Types and Identifiers

okay so streams can be unidirectional or bidirectional ๐Ÿ’– unidirectional ones only carry data one way, from whoever opened the stream over to their peer. bidirectional streams let data travel both ways ๐Ÿฆ‹

every stream gets a numeric label called the stream ID โœจ it's a 62-bit integer, so 0 to 2^62-1, and it has to be unique for all streams on a connection. stream IDs get encoded as variable-length integers (see Section 16 ๐Ÿงš). an endpoint MUST NOT reuse a stream ID within a connection. no recycling IDs, bestie ๐Ÿ™…

the least significant bit (0x01) of the stream ID tells you who started the stream ๐Ÿ‘‘ client-initiated streams have even-numbered IDs (the bit is 0), and server-initiated streams have odd-numbered IDs (the bit is 1).

the second least significant bit (0x02) is the bidirectional vs. unidirectional switch ๐Ÿ”ฅ bit 0 means bidirectional, bit 1 means unidirectional.

so the two lowest bits together give you four stream types, all laid out in Table 1 ๐Ÿ’…

              +======+==================================+
              | Bits | Stream Type                      |
              +======+==================================+
              | 0x00 | Client-Initiated, Bidirectional  |
              +------+----------------------------------+
              | 0x01 | Server-Initiated, Bidirectional  |
              +------+----------------------------------+
              | 0x02 | Client-Initiated, Unidirectional |
              +------+----------------------------------+
              | 0x03 | Server-Initiated, Unidirectional |
              +------+----------------------------------+

                        Table 1: Stream ID Types

each type has its own stream space that starts at its minimum value (0x00 through 0x03, in that order) ๐ŸŒธ streams of the same type get created with numerically increasing IDs. and here's the tea: if you use a stream ID out of order, every stream of that type with a lower-numbered ID gets opened too ๐Ÿ‘€ iykyk.

2.2. Sending and Receiving Data

STREAM frames (Section 19.8) are the little envelopes that carry app data ๐Ÿ’Œ an endpoint uses the Stream ID and Offset fields in them to put everything back in order.

endpoints MUST be able to hand stream data to an application as an ordered byte stream ๐Ÿง  getting that order right means buffering any data that shows up out of order, up to the flow control limit that was advertised. that's a lot of holding, but it's the job ๐Ÿ’ช

QUIC doesn't make any special promises about out-of-order delivery ๐Ÿคทโ€โ™€๏ธ however, implementations MAY choose to offer the ability to deliver data out of order to a receiving application. ooh, optional glow-up ๐ŸŒˆ

an endpoint could get data for the same stream at the same offset more than once ๐Ÿ” data it has already received can be thrown away. the data at a given offset MUST NOT change if it gets sent multiple times. and an endpoint MAY treat getting different data at the same offset within a stream as a connection error of type PROTOCOL_VIOLATION ๐Ÿšจ not okay behavior, periodt.

streams are just an ordered byte-stream abstraction, and QUIC can't see any other structure in them โœจ STREAM frame boundaries aren't expected to survive transmission, retransmission after packet loss, or delivery to the receiving application. the bytes are the thing that matters, not the packaging ๐ŸŽ€

an endpoint MUST NOT send data on any stream without making sure it's within the flow control limits its peer set ๐Ÿšง flow control gets the full treatment in Section 4 ๐Ÿ’–

2.3. Stream Prioritization

multiplexing can totally change how an app performs, but only if the resources given to each stream are prioritized correctly ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ

QUIC doesn't have a mechanism for trading priority info ๐Ÿ™ƒ instead, it relies on the application to tell it the priorities.

a QUIC implementation SHOULD provide ways for an application to say how important each stream is relative to the others โญ the implementation then uses that info to decide how to split resources across the active streams. the app calls the shots here, queen ๐Ÿ‘‘

2.4. Operations on Streams

this document doesn't define an API for QUIC ๐Ÿ™ˆ it defines a set of functions on streams that application protocols can count on. an app can assume the implementation gives it an interface that covers these operations, and an implementation built for one specific app might only bother with the ones that app actually uses ๐Ÿงš

on the sending part of a stream, an app can:

  • write data, and know when stream flow control credit (Section 4.1) has successfully been reserved to send what it wrote ๐Ÿ’Œ
  • end the stream (a clean termination), which results in a STREAM frame (Section 19.8) with the FIN bit set ๐Ÿ and
  • reset the stream (an abrupt termination), which results in a RESET_STREAM frame (Section 19.4) as long as the stream wasn't already in a terminal state ๐Ÿ’ฅ

on the receiving part of a stream, an app can:

  • read data; and
  • abort reading the stream and request that it be closed, which might result in a STOP_SENDING frame (Section 19.5) ๐Ÿ›‘

an app can also ask to be told about state changes on streams ๐Ÿ“ฃ that includes when the peer opened or reset a stream, when the peer aborts reading, when new data shows up, and when data can or can't be written because of flow control. main character energy, the app gets all the updates ๐Ÿ’…

3. Stream States

this section describes streams in terms of their send or receive parts ๐Ÿฆ‹ there are two state machines: one for streams where an endpoint transmits data (Section 3.1), and one for streams where an endpoint receives data (Section 3.2).

unidirectional streams use either the sending or the receiving state machine, depending on the stream type and the endpoint's role. bidirectional streams use both state machines at both endpoints. mostly, the rules are the same whether a stream is unidirectional or bidirectional ๐Ÿซถ the only extra drama is that opening a bidirectional stream is a little more complex, because opening either its send side or its receive side opens the stream in both directions.

the state machines in this section are largely informative ๐Ÿ“ the doc uses these stream states to describe when and how different frames can be sent, and what reactions are expected when different frames arrive. they're meant to be helpful for building QUIC, but they're not meant to boss implementations around ๐ŸŒท an implementation can define a different state machine, as long as its behavior is consistent with one that follows these states.

Note: In some cases, a single event or action can cause a transition through multiple states. For instance, sending STREAM with a FIN bit set can cause two state transitions for a sending stream: from the "Ready" state to the "Send" state, and from the "Send" state to the "Data Sent" state. ๐ŸŽ€

3.1. Sending Stream States

here's Figure 2, the states for the part of a stream that sends data to a peer ๐Ÿงš

    o
    | Create Stream (Sending)
    | Peer Creates Bidirectional Stream
    v
+-------+
| Ready | Send RESET_STREAM
|       |-----------------------.
+-------+                       |
    |                           |
    | Send STREAM /             |
    |      STREAM_DATA_BLOCKED  |
    v                           |
+-------+                       |
| Send  | Send RESET_STREAM     |
|       |---------------------->|
+-------+                       |
    |                           |
    | Send STREAM + FIN         |
    v                           v
+-------+                   +-------+
| Data  | Send RESET_STREAM | Reset |
| Sent  |------------------>| Sent  |
+-------+                   +-------+
    |                           |
    | Recv All ACKs             | Recv ACK
    v                           v
+-------+                   +-------+
| Data  |                   | Reset |
| Recvd |                   | Recvd |
+-------+                   +-------+

         Figure 2: States for Sending Parts of Streams

the sending part of a stream that the endpoint starts (types 0 and 2 for clients, 1 and 3 for servers) gets opened by the application ๐Ÿš€ the "Ready" state is a brand-new stream that's ready to take data from the app. stream data might get buffered in this state while it waits to go out ๐Ÿ“ฆ

sending the first STREAM or STREAM_DATA_BLOCKED frame moves the sending part into the "Send" state ๐Ÿ’ซ an implementation might choose to hold off on assigning a stream ID until it sends that first STREAM frame and enters this state, and that can make prioritization work better โญ

the sending part of a bidirectional stream started by a peer (type 0 for a server, type 1 for a client) starts out in the "Ready" state once the receiving part gets created ๐ŸŒธ

in the "Send" state, an endpoint transmits stream data in STREAM frames, and retransmits them as needed ๐Ÿ” it respects the flow control limits its peer sets, and keeps accepting and processing MAX_STREAM_DATA frames. if stream flow control (Section 4.1) is blocking it from sending, the endpoint generates STREAM_DATA_BLOCKED frames ๐Ÿšง

once the app says all the stream data has been sent and a STREAM frame with the FIN bit goes out, the sending part enters the "Data Sent" state ๐Ÿ from here, the endpoint only retransmits stream data as needed. it doesn't have to check flow control limits or send STREAM_DATA_BLOCKED frames for a stream in this state. MAX_STREAM_DATA frames might still arrive until the peer gets the final stream offset, and the endpoint can safely ignore any MAX_STREAM_DATA frames it gets from its peer for a stream in this state ๐Ÿ™ˆ

once all the stream data has been acknowledged, the sending part enters the "Data Recvd" state ๐ŸŽ‰ that one is terminal, so it's the end of the road.

from any of the "Ready", "Send", or "Data Sent" states, an app can signal that it wants to give up on sending the stream data ๐Ÿ›‘ or the endpoint might get a STOP_SENDING frame from its peer. either way, the endpoint sends a RESET_STREAM frame, which moves the stream into the "Reset Sent" state ๐Ÿ’ฅ

an endpoint MAY send a RESET_STREAM as the very first frame that mentions a stream. this opens the sending part of that stream and then immediately moves it to the "Reset Sent" state โšก

once a packet containing a RESET_STREAM has been acknowledged, the sending part enters the "Reset Recvd" state, which is also terminal ๐Ÿซ 

3.2. Receiving Stream States โœจ

okay so the receiving side gets its own little state machine ๐Ÿ“ฌ Figure 3 shows it, and it's giving mirror, but not a perfect one ๐Ÿชž The receiving part only copies SOME of the sending states from the peer. It doesn't track stuff it can't even see, like "Ready" ๐Ÿ™ˆ Instead it keeps tabs on when data actually gets handed over to the application, and the sender has zero visibility into that part ๐Ÿ’€

          o
          | Recv STREAM / STREAM_DATA_BLOCKED / RESET_STREAM
          | Create Bidirectional Stream (Sending)
          | Recv MAX_STREAM_DATA / STOP_SENDING (Bidirectional)
          | Create Higher-Numbered Stream
          v
      +-------+
      | Recv  | Recv RESET_STREAM
      |       |-----------------------.
      +-------+                       |
          |                           |
          | Recv STREAM + FIN         |
          v                           |
      +-------+                       |
      | Size  | Recv RESET_STREAM     |
      | Known |---------------------->|
      +-------+                       |
          |                           |
          | Recv All Data             |
          v                           v
      +-------+ Recv RESET_STREAM +-------+
      | Data  |--- (optional) --->| Reset |
      | Recvd |  Recv All Data    | Recvd |
      +-------+<-- (optional) ----+-------+
          |                           |
          | App Read All Data         | App Read Reset
          v                           v
      +-------+                   +-------+
      | Data  |                   | Reset |
      | Read  |                   | Read  |
      +-------+                   +-------+

              Figure 3: States for Receiving Parts of Streams

The receiving part of a stream that a peer started (types 1 and 3 for a client, or 0 and 2 for a server) gets created the moment the first STREAM, STREAM_DATA_BLOCKED, or RESET_STREAM frame shows up for that stream ๐ŸŽ€ For bidirectional streams a peer started, getting a MAX_STREAM_DATA or STOP_SENDING frame aimed at the sending part also creates the receiving part โœจ The starting state is "Recv" ๐Ÿฆ‹

For a bidirectional stream, the receiving part jumps into "Recv" once the sending part that the endpoint itself started (type 0 for a client, type 1 for a server) hits "Ready" ๐ŸŒธ

An endpoint opens a bidirectional stream when it gets a MAX_STREAM_DATA or STOP_SENDING frame from the peer for that stream ๐Ÿซถ A MAX_STREAM_DATA for a stream that isn't open yet means the peer opened it and is handing over flow control credit ๐Ÿ’œ A STOP_SENDING for an unopened stream means the peer no longer wants to receive data on that stream ๐Ÿšซ Either one can show up before a STREAM or STREAM_DATA_BLOCKED frame if packets get lost or reordered ๐Ÿ™ƒ

Before a stream gets made, every stream of the same type with a lower-numbered stream ID MUST be made first ๐Ÿ“‹ That keeps the creation order matching on both ends ๐Ÿ‘ฏ

In the "Recv" state, the endpoint receives STREAM and STREAM_DATA_BLOCKED frames ๐Ÿ“ฅ Incoming data is buffered and can be reassembled into the correct order for delivery to the application. As the application consumes data and buffer space opens up, the endpoint sends MAX_STREAM_DATA frames so the peer can send more ๐ŸŒŠ

When a STREAM frame with a FIN bit is received, the final size of the stream is known; see Section 4.5. The receiving part then enters the "Size Known" state ๐Ÿ In this state the endpoint no longer needs to send MAX_STREAM_DATA frames. It only receives any retransmissions of stream data ๐Ÿ”

Once all data for the stream has been received, the receiving part enters the "Data Recvd" state ๐ŸŽ‰ This might happen as a result of receiving the same STREAM frame that causes the transition to "Size Known". After all data has been received, any STREAM or STREAM_DATA_BLOCKED frames for the stream can be discarded ๐Ÿ—‘๏ธ

The "Data Recvd" state sticks around until stream data has been delivered to the application โณ Once it's delivered, the stream enters the "Data Read" state, which is a terminal state ๐Ÿชฆโœจ

Receiving a RESET_STREAM frame in the "Recv" or "Size Known" state sends the stream into the "Reset Recvd" state ๐Ÿ˜ค This might interrupt the delivery of stream data to the application.

It's possible that all stream data has already been received when a RESET_STREAM shows up (that is, in the "Data Recvd" state). Similarly, remaining stream data can arrive after a RESET_STREAM frame (the "Reset Recvd" state) ๐ŸŒ€ An implementation is free to manage this situation as it chooses ๐Ÿคทโ€โ™€๏ธ

Sending a RESET_STREAM means that an endpoint cannot guarantee delivery of stream data ๐Ÿ˜ฌ However, there is no requirement that stream data not be delivered if a RESET_STREAM is received. An implementation MAY interrupt delivery of stream data, discard any data that was not consumed, and signal the receipt of the RESET_STREAM. A RESET_STREAM signal might be suppressed or withheld if stream data is completely received and is buffered to be read by the application. If the RESET_STREAM is suppressed, the receiving part of the stream remains in "Data Recvd" ๐Ÿซ 

Once the application gets the signal that the stream was reset, the receiving part transitions to the "Reset Read" state, which is a terminal state ๐Ÿฅ€


3.3. Permitted Frame Types ๐Ÿ“œ

The sender of a stream sends just three frame types that affect the state of a stream at either the sender or the receiver: STREAM (Section 19.8), STREAM_DATA_BLOCKED (Section 19.13), and RESET_STREAM (Section 19.4). That's the whole list, bestie ๐Ÿ’…

A sender MUST NOT send any of these frames from a terminal state ("Data Recvd" or "Reset Recvd") ๐Ÿšซ A sender MUST NOT send a STREAM or STREAM_DATA_BLOCKED frame for a stream in the "Reset Sent" state or any terminal state -- that is, after sending a RESET_STREAM frame ๐Ÿ™…โ€โ™€๏ธ A receiver could still get any of these three frames in any state, because delayed packets carrying them can show up whenever they like ๐Ÿ•ฐ๏ธ

The receiver of a stream sends MAX_STREAM_DATA frames (Section 19.10) and STOP_SENDING frames (Section 19.5) ๐Ÿ“ค

The receiver only sends MAX_STREAM_DATA frames in the "Recv" state ๐Ÿ’– A receiver MAY send a STOP_SENDING frame in any state where it has not received a RESET_STREAM frame -- that is, states other than "Reset Recvd" or "Reset Read". However, there is little value in sending a STOP_SENDING frame in the "Data Recvd" state, since all stream data has already been received ๐Ÿ™„ A sender could receive either of these two types of frames in any state as a result of delayed delivery of packets ๐Ÿข


3.4. Bidirectional Stream States ๐Ÿฆ‹

A bidirectional stream is made of a sending part and a receiving part ๐ŸŽ€ Implementations can represent the state of the whole bidirectional stream as a composite of the sending and receiving stream states. The simplest model says the stream is "open" when either the sending or receiving part is in a non-terminal state, and "closed" when both are in terminal states ๐Ÿ’œ

Table 2 shows a fancier mapping of bidirectional stream states that loosely lines up with the stream states defined in HTTP/2 [HTTP2] ๐Ÿค“ Multiple states on the sending or receiving parts end up mapped to the same composite state, which is the main vibe here โœจ Just know this is only one possible mapping, and it requires that data be acknowledged before the transition to a "closed" or "half-closed" state ๐Ÿ“ฆ

      +===================+=======================+=================+
      | Sending Part      | Receiving Part        | Composite State |
      +===================+=======================+=================+
      | No Stream / Ready | No Stream / Recv (*1) | idle            |
      +-------------------+-----------------------+-----------------+
      | Ready / Send /    | Recv / Size Known     | open            |
      | Data Sent         |                       |                 |
      +-------------------+-----------------------+-----------------+
      | Ready / Send /    | Data Recvd / Data     | half-closed     |
      | Data Sent         | Read                  | (remote)        |
      +-------------------+-----------------------+-----------------+
      | Ready / Send /    | Reset Recvd / Reset   | half-closed     |
      | Data Sent         | Read                  | (remote)        |
      +-------------------+-----------------------+-----------------+
      | Data Recvd        | Recv / Size Known     | half-closed     |
      |                   |                       | (local)         |
      +-------------------+-----------------------+-----------------+
      | Reset Sent /      | Recv / Size Known     | half-closed     |
      | Reset Recvd       |                       | (local)         |
      +-------------------+-----------------------+-----------------+
      | Reset Sent /      | Data Recvd / Data     | closed          |
      | Reset Recvd       | Read                  |                 |
      +-------------------+-----------------------+-----------------+
      | Reset Sent /      | Reset Recvd / Reset   | closed          |
      | Reset Recvd       | Read                  |                 |
      +-------------------+-----------------------+-----------------+
      | Data Recvd        | Data Recvd / Data     | closed          |
      |                   | Read                  |                 |
      +-------------------+-----------------------+-----------------+
      | Data Recvd        | Reset Recvd / Reset   | closed          |
      |                   | Read                  |                 |
      +-------------------+-----------------------+-----------------+

            Table 2: Possible Mapping of Stream States to HTTP/2
      |  Note (*1): A stream is considered "idle" if it has not yet been
      |  created or if the receiving part of the stream is in the "Recv"
      |  state without yet having received any frames.

3.5. Solicited State Transitions โœจ

okay so if an app isn't vibing with the data it's getting on a stream anymore, it can bail on reading and attach an application error code ๐Ÿ™…โ€โ™€๏ธ

if the stream is in "Recv" or "Size Known", the transport SHOULD signal that by sending a STOP_SENDING frame, which basically asks for the other direction to close ๐Ÿšช this usually means the receiving app isn't reading anymore, but it's not a promise that the incoming data will get ignored ๐Ÿ‘€

heads up: STREAM frames that show up after you've sent STOP_SENDING still count toward connection and stream flow control ๐Ÿ’… even though you're allowed to throw them away the second they land.

a STOP_SENDING frame is basically requesting that the other endpoint send a RESET_STREAM frame ๐Ÿ’Œ an endpoint that gets a STOP_SENDING frame MUST send a RESET_STREAM frame if the stream is in "Ready" or "Send". if it's in "Data Sent", the endpoint MAY hold off on the RESET_STREAM until the packets carrying outstanding data get acknowledged or declared lost ๐Ÿซถ and if any of that outstanding data does get declared lost, the endpoint SHOULD send a RESET_STREAM instead of retransmitting it ๐Ÿ”

an endpoint SHOULD copy the error code from the STOP_SENDING frame into the RESET_STREAM frame it sends back ๐ŸŽ€ but it's allowed to use any application error code it wants. an endpoint that sent a STOP_SENDING frame MAY ignore the error code in any RESET_STREAM frames it gets later for that stream ๐Ÿ™ˆ

STOP_SENDING SHOULD only be sent for a stream the peer hasn't reset ๐ŸŒธ it's most useful for streams in the "Recv" or "Size Known" state, periodt.

an endpoint is expected to send another STOP_SENDING if a packet with the previous one gets lost ๐Ÿ”„ but once all the stream data or a RESET_STREAM frame has arrived (aka the stream is in any state other than "Recv" or "Size Known"), sending STOP_SENDING is unnecessary ๐Ÿ’€

if an endpoint wants to kill both directions of a bidirectional stream, it can end one direction with a RESET_STREAM frame ๐Ÿงจ and then nudge the other direction toward closing quickly by sending a STOP_SENDING frame ๐Ÿฆ‹


4. Flow Control โœจ

okay this part is about not letting anyone flood you ๐ŸŒŠ receivers need to cap how much data they have to buffer, so a fast sender can't overwhelm them and a shady sender can't eat all the memory ๐Ÿ˜ค to keep memory commitments in check for a connection, streams get flow controlled individually AND across the whole connection. a QUIC receiver controls the max amount a sender can send on one stream and across all streams at any time, as described in Sections 4.1 and 4.2 ๐Ÿ’–

similarly, to keep concurrency under control, a QUIC endpoint controls the max cumulative number of streams its peer is allowed to start, see Section 4.6 ๐Ÿงš

data in CRYPTO frames doesn't get flow controlled the same way stream data does ๐Ÿซง QUIC leans on the cryptographic protocol implementation to avoid excessive buffering, see [QUIC-TLS]. to avoid excessive buffering across multiple layers, QUIC implementations SHOULD provide an interface that lets the cryptographic protocol implementation communicate its buffering limits ๐Ÿค


4.1. Data Flow Control

QUIC uses a limit-based flow control scheme ๐ŸŽฏ where a receiver advertises the total number of bytes it's ready to take on a given stream or for the whole connection. that gives us two levels of data flow control in QUIC, bestie:

  • Stream flow control ๐ŸŒท stops one stream from hogging the entire receive buffer for a connection, by limiting how much data can be sent on each stream.
  • Connection flow control ๐ŸŒŠ stops senders from going past a receiver's buffer capacity for the connection, by limiting the total bytes of stream data sent in STREAM frames across all streams.

senders MUST NOT send data in excess of either limit ๐Ÿšซ no exceptions, no cap.

a receiver sets the initial limits for all streams through transport parameters during the handshake (Section 7.4) โœจ after that, it sends MAX_STREAM_DATA frames (Section 19.10) or MAX_DATA frames (Section 19.9) to the sender to advertise bigger limits ๐Ÿ“ˆ

a receiver can advertise a larger limit for one stream by sending a MAX_STREAM_DATA frame with that stream's ID ๐Ÿ†” a MAX_STREAM_DATA frame indicates the maximum absolute byte offset of a stream. the receiver could work out the flow control offset to advertise based on the current offset of data it has consumed on that stream ๐Ÿงฎ

a receiver can advertise a larger limit for the connection by sending a MAX_DATA frame ๐Ÿ’ซ which indicates the maximum of the sum of the absolute byte offsets of all streams. the receiver keeps a running total of bytes received on all streams, and uses that to check for violations of the advertised connection or stream data limits ๐Ÿ” it could decide the max data limit to advertise based on the sum of bytes consumed on all streams.

once a receiver advertises a limit for the connection or a stream, advertising a smaller limit isn't an error ๐Ÿ™ƒ but the smaller limit just doesn't do anything.

a receiver MUST close the connection with an error of type FLOW_CONTROL_ERROR if the sender violates the advertised connection or stream data limits ๐Ÿšจ see Section 11 for the details on error handling.

a sender MUST ignore any MAX_STREAM_DATA or MAX_DATA frames that don't actually increase flow control limits ๐Ÿ™„

if a sender has sent data up to the limit, it can't send new data and is considered blocked ๐Ÿงฑ a sender SHOULD send a STREAM_DATA_BLOCKED or DATA_BLOCKED frame to let the receiver know it has data to write but is stuck behind flow control limits ๐Ÿ“ฃ if a sender is blocked for longer than the idle timeout (Section 10.1), the receiver might close the connection even while the sender still has data ready to go ๐Ÿ˜ฑ to keep the connection from closing, a sender that is flow control limited SHOULD periodically send a STREAM_DATA_BLOCKED or DATA_BLOCKED frame when it has no ack-eliciting packets in flight ๐Ÿ’“


4.2. Increasing Flow Control Limits

implementations get to decide when and how much credit to advertise in MAX_STREAM_DATA and MAX_DATA frames ๐ŸŽ€ this section just offers a few things to consider ๐Ÿ’ญ

to avoid blocking a sender, a receiver MAY send a MAX_STREAM_DATA or MAX_DATA frame multiple times within a round trip, or send it early enough to leave time for the frame to get lost and recovered ๐Ÿ›Ÿ

control frames add to connection overhead ๐Ÿ“ฆ so sending MAX_STREAM_DATA and MAX_DATA frames over and over with tiny changes is a no-go ๐Ÿ™…โ€โ™€๏ธ but if updates happen less often, you need bigger increments to avoid blocking a sender, which means the receiver has to commit more resources ๐Ÿซ  there's a trade-off between resource commitment and overhead when you decide how big an advertised limit should be โš–๏ธ

a receiver can use an autotuning mechanism to adjust how often and how much extra credit it advertises, based on a round-trip time estimate and how fast the receiving app consumes data ๐Ÿค– it's similar to what common TCP implementations do ๐Ÿงƒ as an optimization, an endpoint could send flow-control-related frames only when it has other frames to send, so flow control never causes extra packets to get sent ๐Ÿฆ‹

a blocked sender doesn't have to send STREAM_DATA_BLOCKED or DATA_BLOCKED frames ๐Ÿซฅ which is why a receiver MUST NOT wait for a STREAM_DATA_BLOCKED or DATA_BLOCKED frame before sending a MAX_STREAM_DATA or MAX_DATA frame ๐Ÿ™…โ€โ™€๏ธ doing that could leave the sender blocked for the rest of the connection ๐Ÿ˜ญ and even if the sender does send those frames, waiting for them means the sender stays blocked for at least an entire round trip โณ

when a sender gets credit after being blocked, it might be able to send a huge amount of data in response, which causes short-term congestion ๐ŸŒช๏ธ see Section 7.7 of [QUIC-RECOVERY] for a discussion of how a sender can avoid that congestion ๐Ÿง˜โ€โ™€๏ธ


4.3. Flow Control Performance

if an endpoint can't make sure its peer always has available flow control credit greater than the peer's bandwidth-delay product on this connection, its receive throughput is going to get limited by flow control ๐Ÿข

packet loss can create gaps in the receive buffer ๐Ÿ•ณ๏ธ which stops the application from consuming data and freeing up receive buffer space ๐Ÿ˜ค

timely flow control updates can seriously improve performance โšก but sending packets only to deliver flow control updates can pump up network load and hurt performance ๐Ÿ“‰ the move is to send flow control updates along with other frames, like ACK frames, since that cuts the cost of those updates ๐Ÿง โœจ


4.4. Handling Stream Cancellation

endpoints eventually need to agree on how much flow control credit has been used on every stream, so they can account for all the bytes at the connection level ๐Ÿงพ

when an endpoint gets a RESET_STREAM frame, it tears down the state for the matching stream and ignores any further data arriving on that stream ๐Ÿ—‘๏ธ

RESET_STREAM ends one direction of a stream abruptly ๐Ÿ’ฅ on a bidirectional stream, it has no effect on data flow in the opposite direction ๐Ÿ”€ both endpoints MUST maintain flow control state for the stream in the unterminated direction until that direction enters a terminal state ๐Ÿซถ


4.5. Stream Final Size

the final size is the amount of flow control credit a stream consumes ๐Ÿ’ฐ if you assume every contiguous byte on the stream was sent exactly once, the final size is the number of bytes sent. more generally, it's one higher than the offset of the byte with the largest offset sent on the stream, or zero if no bytes were sent ๐Ÿ”ข

a sender always communicates the final size of a stream to the receiver reliably, no matter how the stream ends ๐Ÿ“ฌ the final size is the sum of the Offset and Length fields of a STREAM frame with a FIN flag, and keep in mind those fields might be implicit ๐Ÿซฃ or you can get it from the Final Size field of a RESET_STREAM frame ๐ŸŽฏ either way, both endpoints end up agreeing on how much flow control credit the sender used on that stream ๐Ÿค

an endpoint knows the final size for a stream once the receiving part of the stream enters the "Size Known" or "Reset Recvd" state (Section 3) โœ… the receiver MUST use the final size of the stream to account for all bytes sent on the stream in its connection-level flow controller ๐Ÿ’…

an endpoint MUST NOT send data on a stream at or beyond the final size ๐Ÿšซ

once a final size for a stream is known, it can't change ๐Ÿ”’ if a RESET_STREAM or STREAM frame shows up indicating a change in the final size, an endpoint SHOULD respond with an error of type FINAL_SIZE_ERROR (see Section 11 for error handling) ๐Ÿšจ a receiver SHOULD treat data arriving at or beyond the final size as an error of type FINAL_SIZE_ERROR, even after the stream is closed ๐Ÿซข generating these errors isn't compulsory, though, because requiring it would also mean the endpoint has to keep final size state for closed streams, and that could be a seriously big state commitment ๐Ÿงณ๐Ÿ’œ

4.6. Controlling Concurrency โœจ

okay so an endpoint gets to decide how many incoming streams its peer is allowed to open in total ๐Ÿ’… Only streams with a stream ID less than (max_streams * 4 + first_stream_id_of_type) can be opened, see Table 1 ๐Ÿฆ‹ The starting limits go in the transport parameters (see Section 18.2), and any later ones get sent with MAX_STREAMS frames (see Section 19.11). Unidirectional and bidirectional streams each have their own separate limits, so they don't mess with each other ๐ŸŒธ

If a max_streams transport parameter or a MAX_STREAMS frame comes in with a value greater than 2^60, the maximum stream ID would be one that can't be written as a variable-length integer (see Section 16) ๐Ÿ’€ If that happens, the connection MUST be closed immediately. The error type is TRANSPORT_PARAMETER_ERROR if the bad value came in a transport parameter, or FRAME_ENCODING_ERROR if it came in a frame (see Section 10.2) ๐Ÿ™ˆ

Endpoints MUST NOT exceed the limit their peer set ๐Ÿšซ If an endpoint gets a frame with a stream ID past the limit it has sent, that MUST be treated as a connection error of type STREAM_LIMIT_ERROR. Section 11 has the details on how errors get handled ๐Ÿ”ฅ

Once a receiver advertises a stream limit with a MAX_STREAMS frame, sending a smaller limit does nothing โœจ MAX_STREAMS frames that don't increase the stream limit MUST be ignored, no exceptions ๐Ÿ’…

Just like with stream and connection flow control, this document leaves it up to implementations to decide when and how many streams to advertise to a peer with MAX_STREAMS ๐Ÿงš They might bump the limits as streams close so the number of streams available stays roughly steady ๐ŸŒˆ

An endpoint that can't open a new stream because of the peer's limits SHOULD send a STREAMS_BLOCKED frame (Section 19.14). This signal is handy for debugging ๐Ÿ” An endpoint MUST NOT wait to get this signal before advertising more credit ๐Ÿ˜ญ Waiting would leave the peer blocked for at least a full round trip, and potentially forever if the peer never sends STREAMS_BLOCKED frames ๐Ÿ’€


5. Connections ๐Ÿ”—

A QUIC connection is shared state between a client and a server ๐Ÿซถ

Every connection starts with a handshake phase. During it, both endpoints work out a shared secret using the cryptographic handshake protocol [QUIC-TLS] and agree on the application protocol ๐Ÿค The handshake (Section 7) confirms that both endpoints want to talk (Section 8.1) and sets up the parameters for the connection (Section 7.4) โœจ

An application protocol can use the connection during the handshake phase, with some limits ๐ŸŽ€ 0-RTT lets a client send application data before it hears back from the server. The catch is that 0-RTT has zero protection against replay attacks, so check Section 9.2 of [QUIC-TLS] ๐Ÿ™ˆ A server can also send application data to a client before it gets the final cryptographic handshake messages that confirm the client's identity and that it's alive. These options let an application trade some security guarantees for lower latency โšก

Connection IDs (Section 5.1) let connections migrate to a new network path, whether an endpoint chooses to do it or a middlebox forces it ๐Ÿฆ‹ Section 9 covers how to deal with the security and privacy issues that come with migration ๐Ÿ”

When connections aren't needed anymore, a client and server have several ways to end them, all described in Section 10 ๐Ÿ’…

5.1. Connection ID ๐Ÿชช

Every connection has a set of connection identifiers, or connection IDs, and any one of them can identify that connection ๐ŸŒธ Endpoints pick their connection IDs independently. Each endpoint picks the connection IDs its peer will use โœจ

The main job of a connection ID is to make sure that changes in addressing at the lower layers (UDP, IP) don't send packets for a QUIC connection to the wrong endpoint ๐Ÿงš Each endpoint picks connection IDs with an implementation-specific method (maybe even a deployment-specific one) that lets packets with that ID get routed back to it and recognized when they arrive ๐Ÿ’œ

Multiple connection IDs let endpoints send packets that an observer can't link to the same connection without help from the endpoint. Check Section 9.5 for more on that ๐Ÿ•ต๏ธ

Connection IDs MUST NOT contain any information that an external observer (someone who isn't cooperating with the issuer) could use to link them to other connection IDs for the same connection ๐Ÿšซ As a very simple example, the same connection ID MUST NOT be issued more than once on the same connection ๐Ÿ™…โ€โ™€๏ธ

Packets with long headers have Source Connection ID and Destination Connection ID fields. These fields set the connection IDs for new connections, so see Section 7.2 for the details ๐ŸŽ€

Packets with short headers (Section 17.3) only have the Destination Connection ID, and they leave out the explicit length. Endpoints are expected to already know how long that Destination Connection ID field is ๐Ÿ“ Endpoints using a load balancer that routes by connection ID can agree with the load balancer on a fixed length, or on an encoding scheme. A fixed portion of the ID can encode an explicit length, so the whole ID can vary in length and the load balancer still reads it ๐ŸŒˆ

A Version Negotiation packet (Section 17.2.1) echoes the connection IDs the client picked. That keeps the packet routed back to the client, and it proves the packet is a reply to something the client actually sent ๐ŸŽฏ

A zero-length connection ID works when you don't need one to route to the right endpoint ๐Ÿซง But if you multiplex connections on the same local IP address and port while using zero-length connection IDs, things will break once peer connection migration, NAT rebinding, or client port reuse shows up ๐Ÿ˜ญ An endpoint MUST NOT use the same IP address and port for multiple concurrent connections with zero-length connection IDs, unless it's sure none of those features are in use ๐Ÿ’€

When an endpoint uses a non-zero-length connection ID, it has to make sure its peer has a supply of connection IDs to pick from when sending packets to it. The endpoint provides them with NEW_CONNECTION_ID frames (Section 19.15) ๐Ÿงš

5.1.1. Issuing Connection IDs ๐Ÿ“ฎ

Every connection ID has a sequence number. That helps with spotting when NEW_CONNECTION_ID or RETIRE_CONNECTION_ID frames are talking about the same value ๐Ÿ”ข The first connection ID an endpoint issues goes in the Source Connection ID field of the long packet header (Section 17.2) during the handshake, and its sequence number is 0. If the preferred_address transport parameter is sent, the connection ID it supplies gets sequence number 1 โœจ

Any extra connection IDs get sent to the peer in NEW_CONNECTION_ID frames (Section 19.15). The sequence number of each newly issued connection ID MUST increase by 1 ๐Ÿ“ˆ The connection ID a client picks for its first Destination Connection ID field doesn't get a sequence number, and neither does any connection ID a Retry packet provides ๐Ÿ™…โ€โ™€๏ธ

When an endpoint issues a connection ID, it MUST accept packets that carry that connection ID for as long as the connection lasts, or until its peer retires the ID with a RETIRE_CONNECTION_ID frame (Section 19.16) ๐ŸŽ€ Issued IDs that haven't been retired count as active, and any active connection ID can be used with the current connection at any time, in any packet type ๐Ÿ’… That includes the connection ID the server issues through the preferred_address transport parameter ๐ŸŒธ

An endpoint SHOULD make sure its peer has enough available, unused connection IDs โœจ Endpoints say how many active connection IDs they're willing to keep track of with the active_connection_id_limit transport parameter. An endpoint MUST NOT provide more connection IDs than the peer's limit ๐Ÿšซ An endpoint MAY send connection IDs that go temporarily over the peer's limit, but only if the NEW_CONNECTION_ID frame also makes the peer retire the extras by putting a big enough value in the Retire Prior To field ๐Ÿงน

A NEW_CONNECTION_ID frame can make an endpoint add some active connection IDs and retire others, based on the Retire Prior To field ๐Ÿ”„ After it processes a NEW_CONNECTION_ID frame and adds and retires active connection IDs, if the number of active connection IDs is still bigger than the value in its active_connection_id_limit transport parameter, the endpoint MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR ๐Ÿ’€

An endpoint SHOULD supply a new connection ID when its peer retires one โœจ If an endpoint gave out fewer connection IDs than the peer's active_connection_id_limit, it MAY supply a new one when it gets a packet with a connection ID it hasn't seen before ๐Ÿ‘€ An endpoint MAY cap the total number of connection IDs it issues for each connection, so it doesn't run out of them (see Section 10.3.2) ๐Ÿ›ก๏ธ An endpoint MAY also limit how many connection IDs it issues to cut down on the per-path state it keeps, like path validation status. That matters because the peer might talk to it over as many paths as there are issued connection IDs ๐Ÿ›ค๏ธ

An endpoint that starts a migration and needs non-zero-length connection IDs SHOULD make sure the pool of connection IDs its peer has lets the peer use a new one during migration ๐Ÿฆ‹ Otherwise the peer can't respond once the pool runs dry ๐Ÿ˜ฑ

An endpoint that picks a zero-length connection ID during the handshake can't issue new connection IDs ๐Ÿซ  A zero-length Destination Connection ID field gets used in every packet sent toward that endpoint, over any network path ๐ŸŒ™

5.1.2. Consuming and Retiring Connection IDs โœจ

okay so an endpoint can swap the connection ID it uses for a peer with another available one at literally any point during the connection ๐Ÿฆ‹ It consumes connection IDs when a peer migrates (see Section 9.5 for the deets).

An endpoint keeps a set of connection IDs it got from its peer, and it can use any of them when sending packets ๐Ÿ’… When it wants one of them out of rotation, it sends a RETIRE_CONNECTION_ID frame to its peer. That frame basically says "I'm never using this one again, so please replace it with a new one using a NEW_CONNECTION_ID frame" ๐Ÿซถ

Section 9.5 explains that endpoints limit a connection ID to packets sent from a single local address to a single destination address. So endpoints SHOULD retire connection IDs once they're no longer actively using either the local or the destination address that the ID was used for ๐ŸŒธ

Sometimes an endpoint has to stop accepting connection IDs it already handed out ๐Ÿ™ˆ When that happens, it can make its peer retire them by sending a NEW_CONNECTION_ID frame with an increased Retire Prior To field. The endpoint SHOULD keep accepting the previously issued connection IDs until the peer retires them. If the endpoint can't process the indicated connection IDs anymore, it MAY close the connection ๐Ÿ’€

When the peer gets an increased Retire Prior To field, it MUST stop using the matching connection IDs and retire them with RETIRE_CONNECTION_ID frames before it adds the new connection ID to its active set ๐Ÿ‘‘ This order is the whole point. It lets an endpoint swap out every active connection ID without the peer ever being left with zero usable IDs, and without going over the limit the peer sets in the active_connection_id_limit transport parameter (see Section 18.2). If the peer doesn't stop using the connection IDs when asked, things can break, because the endpoint that issued them might not be able to keep using them on the active connection โš ๏ธ

An endpoint SHOULD limit how many connection IDs it has retired locally that still don't have an acknowledged RETIRE_CONNECTION_ID frame ๐Ÿงš It SHOULD also allow for sending and tracking at least twice the value of the active_connection_id_limit transport parameter in RETIRE_CONNECTION_ID frames. An endpoint MUST NOT forget a connection ID without retiring it ๐Ÿšซ It MAY choose to treat having too many connection IDs in need of retirement as a connection error of type CONNECTION_ID_LIMIT_ERROR, though ๐Ÿซ 

Endpoints SHOULD NOT issue updates to the Retire Prior To field before they've received RETIRE_CONNECTION_ID frames that retire every connection ID indicated by the previous Retire Prior To value ๐ŸŽ€


5.2. Matching Packets to Connections

Okay, so incoming packets get sorted the moment they land ๐Ÿ“ฌ A packet is either attached to a connection that already exists or, if you're a server, it might start a brand-new one โœจ

Endpoints try to match a packet to an existing connection first ๐Ÿฆ‹ If the packet has a non-zero-length Destination Connection ID that belongs to an existing connection, QUIC processes it that way. Also, one connection can be linked to more than one connection ID, so check Section 5.1 for that ๐Ÿ’œ

If the Destination Connection ID is zero length and the addressing info in the packet matches what the endpoint uses to identify a connection with a zero-length connection ID, QUIC processes the packet as part of that connection ๐ŸŒˆ An endpoint can identify connections by destination IP and port, or by both source and destination addresses. Heads up though, this makes connections fragile, as Section 5.1 describes ๐Ÿ™ˆ

Endpoints can send a Stateless Reset (Section 10.3) for any packet that can't be linked to an existing connection ๐Ÿ”ฅ A Stateless Reset helps a peer figure out much faster that a connection has become unusable ๐Ÿ’–

Packets that do match an existing connection get thrown out if they don't fit that connection's state ๐Ÿ—‘๏ธ For example, packets get discarded if they show a different protocol version than the connection's, or if removing packet protection fails once the expected keys are available ๐Ÿ˜ญ

Invalid packets that lack strong integrity protection, like Initial, Retry, or Version Negotiation packets, MAY be discarded ๐Ÿ“ An endpoint MUST generate a connection error if processing these packets' contents reveals an error, or it MUST fully revert any changes it made during that processing. Not optional, bestie ๐Ÿ‘‘

5.2.1. Client Packet Handling

Valid packets sent to clients always carry a Destination Connection ID that matches a value the client picked ๐Ÿ’… Clients that choose to receive zero-length connection IDs can use the local address and port to identify a connection ๐ŸŒธ Packets that don't match an existing connection, whether by Destination Connection ID or (if that value is zero length) by local IP address and port, get discarded ๐Ÿ—‘๏ธ

Thanks to packet reordering or loss, a client might get packets for a connection that are encrypted with a key it hasn't computed yet ๐Ÿคฏ The client MAY drop these packets, or it MAY buffer them while it waits for later packets that let it compute the key ๐Ÿซถ

If a client gets a packet using a different version than the one it originally selected, it MUST discard that packet ๐Ÿšซ

5.2.2. Server Packet Handling

If a server gets a packet that asks for an unsupported version, and the packet is big enough to start a new connection for any supported version, the server SHOULD send a Version Negotiation packet as described in Section 6.1 โœจ A server MAY limit how many packets it answers with a Version Negotiation packet. Servers MUST drop smaller packets that specify unsupported versions ๐Ÿซ 

The first packet for an unsupported version can use different semantics and encodings for version-specific fields ๐Ÿงฉ Different packet protection keys might even be used for different versions. Servers that don't support a particular version probably can't decrypt the payload or read the result properly ๐Ÿ˜ต So servers SHOULD respond with a Version Negotiation packet, as long as the datagram is long enough ๐Ÿ’œ

Packets with a supported version, or with no Version field, get matched to a connection using the connection ID, or (for packets with zero-length connection IDs) the local address and port ๐Ÿฆ‹ Those packets are processed on the selected connection. Otherwise, the server keeps going as described below ๐Ÿ‘€

If the packet is an Initial packet that fully conforms to the specification, the server moves ahead with the handshake (Section 7) ๐Ÿค That commits the server to the version the client selected ๐Ÿ”’

If a server refuses to accept a new connection, it SHOULD send an Initial packet containing a CONNECTION_CLOSE frame with the error code CONNECTION_REFUSED ๐Ÿ™…โ€โ™€๏ธ

If the packet is a 0-RTT packet, the server MAY buffer a limited number of them in case a late Initial packet shows up ๐Ÿ•ฐ๏ธ Clients can't send Handshake packets before they get a server response, so servers SHOULD ignore any such packets ๐Ÿ™ˆ

Servers MUST drop incoming packets in all other circumstances ๐Ÿšซ

5.2.3. Considerations for Simple Load Balancers

A server deployment could load-balance across servers using only source and destination IP addresses and ports ๐ŸŽ  But if the client's IP address or port changes, packets could get forwarded to the wrong server ๐Ÿ˜ฌ A deployment like that can use one of the following methods for connection continuity when the client's address changes:

  • Servers could use an out-of-band mechanism to forward packets to the correct server based on connection ID ๐Ÿ“ฆ
  • If servers can use a dedicated server IP address or port, other than the one the client first connects to, they could use the preferred_address transport parameter to ask clients to move the connection to that dedicated address ๐Ÿงš Just note that clients could choose not to use the preferred address ๐Ÿคทโ€โ™€๏ธ

A server in a deployment that doesn't have a solution for keeping connection continuity when the client address changes SHOULD indicate that migration isn't supported, by using the disable_active_migration transport parameter ๐Ÿšง The disable_active_migration transport parameter doesn't stop connection migration after a client has acted on a preferred_address transport parameter ๐Ÿ’…

Server deployments that use this simple form of load balancing MUST avoid creating a stateless reset oracle (see Section 21.11) ๐Ÿ”ฎ This one is serious, babe ๐Ÿ˜ค


5.3. Operations on Connections

This document doesn't define an API for QUIC ๐Ÿ™…โ€โ™€๏ธ Instead, it defines a set of functions for QUIC connections that application protocols can rely on ๐Ÿซถ An application protocol can assume that a QUIC implementation provides an interface that includes the operations in this section. An implementation built for one specific application protocol might only provide the operations that protocol actually uses ๐Ÿง 

When implementing the client role, an application protocol can:

  • open a connection, which begins the exchange described in Section 7;
  • enable Early Data when it's available; and
  • be informed when a server has accepted or rejected Early Data ๐ŸŒธ

When implementing the server role, an application protocol can:

  • listen for incoming connections, which prepares for the exchange described in Section 7;
  • if Early Data is supported, embed application-controlled data in the TLS resumption ticket sent to the client; and
  • if Early Data is supported, retrieve application-controlled data from the client's resumption ticket and accept or reject Early Data based on that information ๐ŸŽŸ๏ธ

In either role, an application protocol can:

  • configure minimum values for the initial number of permitted streams of each type, as communicated in the transport parameters (Section 7.4);
  • control resource allocation for receive buffers by setting flow control limits for both streams and the connection ๐Ÿชฃ;
  • identify whether the handshake has completed successfully or is still ongoing ๐Ÿค;
  • keep a connection from silently closing, by either generating PING frames (Section 19.2) or requesting that the transport send additional frames before the idle timeout expires (Section 10.1); and
  • immediately close the connection (Section 10.2) ๐Ÿ’ฅ

6. Version Negotiation โœจ

Okay so version negotiation is basically how a server says "sorry bestie, I don't speak that version" ๐Ÿ’…. A server sends a Version Negotiation packet in response to each packet that might start a new connection. See Section 5.2 for the deets ๐Ÿฆ‹.

The size of the first packet a client sends decides whether the server replies with a Version Negotiation packet ๐Ÿ‘€. Clients that support multiple QUIC versions SHOULD make their first UDP datagram as big as the largest minimum datagram size across all the versions they support, using PADDING frames (Section 19.1) when needed ๐ŸŒธ. This is what makes sure the server responds if there's a version you both speak ๐Ÿซถ. A server might not send a Version Negotiation packet if the datagram it gets is smaller than the minimum size another version asks for. See Section 14.1 for that one ๐Ÿ™ˆ.

6.1. Sending Version Negotiation Packets

If the server doesn't accept the version the client picked, it replies with a Version Negotiation packet (see Section 17.2.1) ๐Ÿ’–. That packet includes a list of the versions the server will accept โœจ. An endpoint MUST NOT send a Version Negotiation packet in response to receiving a Version Negotiation packet. Not me sending a Version Negotiation packet back at a Version Negotiation packet ๐Ÿ’€.

This whole system lets a server handle packets with unsupported versions without keeping any state ๐Ÿงš. Sure, the Initial packet or the Version Negotiation packet could get lost on the way, but the client keeps sending new packets until it gets a response or gives up on the connection attempt ๐Ÿ“.

A server MAY limit how many Version Negotiation packets it sends ๐ŸŒˆ. For example, a server that can tell which packets are 0-RTT might decide not to send Version Negotiation packets in reply to 0-RTT packets, because it's expecting an Initial packet to show up eventually ๐ŸŽ€.

6.2. Handling Version Negotiation Packets

Version Negotiation packets are built so that future features can let QUIC negotiate which version to use for a connection ๐Ÿ”ฎ. Future Standards Track specifications might change how implementations that support multiple versions react to Version Negotiation packets received in response to an attempt to set up a connection with this version ๐Ÿ‘‘.

A client that supports only this version of QUIC MUST abandon the current connection attempt if it receives a Version Negotiation packet, with two exceptions ๐Ÿ˜ญ. A client MUST discard any Version Negotiation packet if it has already received and successfully processed any other packet, including an earlier Version Negotiation packet. A client MUST discard a Version Negotiation packet that lists the QUIC version the client selected. Those are the rules, no cap ๐Ÿซถ.

How version negotiation actually works is left as future work, to be defined by future Standards Track specifications โญ. In particular, that future work will make sure it's robust against version downgrade attacks. See Section 21.12 for the scary details ๐Ÿ’€.

6.3. Using Reserved Versions

For a server to start using a new version someday, clients need to handle unsupported versions correctly ๐ŸŒธ. Some version numbers (0x?a?a?a?a, as defined in Section 15) are reserved for use in fields that contain version numbers ๐Ÿฆ‹.

Endpoints MAY add reserved versions to any field where unknown or unsupported versions get ignored, just to check that a peer correctly ignores the value ๐Ÿงช. For example, an endpoint could put a reserved version in a Version Negotiation packet (see Section 17.2.1). Endpoints MAY also send packets with a reserved version to check that a peer correctly discards the packet ๐Ÿ’….


7. Cryptographic and Transport Handshake ๐Ÿ”

QUIC leans on a combined cryptographic and transport handshake so connections get set up super fast โšก. QUIC uses the CRYPTO frame (Section 19.6) to send the cryptographic handshake ๐Ÿ’œ. The version of QUIC defined in this document is identified as 0x00000001 and uses TLS as described in [QUIC-TLS]. A different QUIC version could mean a different cryptographic handshake protocol is in use ๐ŸŒˆ.

QUIC gives you reliable, ordered delivery of the cryptographic handshake data ๐ŸŽ€. QUIC packet protection is used to encrypt as much of the handshake protocol as it can ๐Ÿ”’. The cryptographic handshake MUST provide the following properties:

  • authenticated key exchange, where
    • a server is always authenticated,
    • a client is optionally authenticated,
    • every connection produces distinct and unrelated keys, and
    • keying material is usable for packet protection for both 0-RTT and 1-RTT packets.
  • authenticated exchange of values for transport parameters of both endpoints, and confidentiality protection for server transport parameters (see Section 7.4).
  • authenticated negotiation of an application protocol (TLS uses Application-Layer Protocol Negotiation (ALPN) [ALPN] for this purpose).

The CRYPTO frame can be sent in different packet number spaces (Section 12.3) ๐Ÿงฉ. The offsets CRYPTO frames use to keep the handshake data in order start from zero in each packet number space ๐Ÿ“.

Figure 4 shows a simplified handshake and the exchange of packets and frames that move the handshake along ๐Ÿฆ‹. Where possible, application data can be exchanged during the handshake, shown with an asterisk ("*") โœจ. Once the handshake is complete, endpoints can swap application data freely ๐Ÿฅณ.

   Client                                               Server

   Initial (CRYPTO)
   0-RTT (*)              ---------->
                                              Initial (CRYPTO)
                                            Handshake (CRYPTO)
                          <----------                1-RTT (*)
   Handshake (CRYPTO)
   1-RTT (*)              ---------->
                          <----------   1-RTT (HANDSHAKE_DONE)

   1-RTT                  <=========>                    1-RTT

Figure 4: Simplified QUIC Handshake

Endpoints can use packets sent during the handshake to test for Explicit Congestion Notification (ECN) support (see Section 13.4) ๐ŸŒฟ. An endpoint validates ECN support by checking whether the ACK frames acknowledging its first packets carry ECN counts, as described in Section 13.4.2 ๐Ÿ“Š.

Endpoints MUST explicitly negotiate an application protocol ๐Ÿซถ. This avoids situations where the two sides disagree about which protocol they're using. Nobody wants that drama ๐Ÿ˜ญ.

7.1. Example Handshake Flows

The details of how TLS is integrated with QUIC live in [QUIC-TLS], but here are a few examples ๐Ÿ’–. An extension of this exchange that supports client address validation is shown in Section 8.1.2 ๐Ÿงš.

Once any address validation exchanges are done, the cryptographic handshake is used to agree on cryptographic keys ๐Ÿ”‘. The cryptographic handshake is carried in Initial (Section 17.2.2) and Handshake (Section 17.2.4) packets ๐Ÿ“ฆ.

Figure 5 gives you an overview of the 1-RTT handshake ๐ŸŒธ. Each line shows a QUIC packet, with the packet type and packet number first, followed by the frames that are usually in those packets. For example, the first packet is of type Initial, with packet number 0, and it contains a CRYPTO frame carrying the ClientHello ๐Ÿ‘‘.

Multiple QUIC packets can be coalesced into a single UDP datagram, even if they're different packet types (see Section 12.2) ๐Ÿช†. So this handshake could be as few as four UDP datagrams, or more (subject to limits inherent to the protocol, like congestion control and anti-amplification) ๐Ÿฆ‹. For instance, the server's first flight contains Initial packets, Handshake packets, and "0.5-RTT data" in 1-RTT packets ๐ŸŽ€.

   Client                                                  Server

   Initial[0]: CRYPTO[CH] ->

                                    Initial[0]: CRYPTO[SH] ACK[0]
                          Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
                                    <- 1-RTT[0]: STREAM[1, "..."]

   Initial[1]: ACK[0]
   Handshake[0]: CRYPTO[FIN], ACK[0]
   1-RTT[0]: STREAM[0, "..."], ACK[0] ->

                                             Handshake[1]: ACK[0]
            <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[0]

Figure 5: Example 1-RTT Handshake

Figure 6 shows an example connection with a 0-RTT handshake and a single packet of 0-RTT data ๐ŸŒˆ. As Section 12.3 explains, the server acknowledges 0-RTT data in 1-RTT packets, and the client sends 1-RTT packets in the same packet number space ๐Ÿ’œ.

   Client                                                  Server

   Initial[0]: CRYPTO[CH]
   0-RTT[0]: STREAM[0, "..."] ->

                                    Initial[0]: CRYPTO[SH] ACK[0]
                                     Handshake[0] CRYPTO[EE, FIN]
                             <- 1-RTT[0]: STREAM[1, "..."] ACK[0]

   Initial[1]: ACK[0]
   Handshake[0]: CRYPTO[FIN], ACK[0]
   1-RTT[1]: STREAM[0, "..."] ACK[0] ->

                                             Handshake[1]: ACK[0]
            <- 1-RTT[1]: HANDSHAKE_DONE, STREAM[3, "..."], ACK[1]

Figure 6: Example 0-RTT Handshake

7.2. Negotiating Connection IDs ๐Ÿ”—

okay so connection IDs are how packets find their way home ๐Ÿ  Section 5.1 covers the details, but the TL;DR is that the long header carries two of them. The Destination Connection ID is picked by whoever receives the packet, and it's what keeps routing consistent. The Source Connection ID is how you tell your peer which value to put in their Destination Connection ID field. Iconic teamwork ๐Ÿซถ

During the handshake, the long header packets (Section 17.2) are how both endpoints set up their connection IDs ๐Ÿ’… Each endpoint uses its Source Connection ID field to say "hey, put this value in the Destination Connection ID field of anything you send me." After an endpoint processes the first Initial packet, it sets the Destination Connection ID in everything it sends after that to whatever Source Connection ID it received ๐Ÿ’–

When a client sends an Initial packet and hasn't gotten an Initial or Retry packet from the server yet, it fills the Destination Connection ID field with a value nobody can predict ๐ŸŽฒ This Destination Connection ID MUST be at least 8 bytes long. Until the client hears back from the server, it MUST use that same Destination Connection ID on every packet in the connection. No switching it up early, bestie ๐Ÿ™…โ€โ™€๏ธ

The Destination Connection ID from the client's very first Initial packet is what gets used to figure out the packet protection keys for Initial packets. Those keys change after a Retry packet shows up, so check Section 5.2 of [QUIC-TLS] for the full scoop โœจ

The client fills the Source Connection ID field with a value it picks itself, and it sets the Source Connection ID Length field to say how long that value is ๐ŸŽ€

0-RTT packets in the first flight use the same Destination Connection ID and Source Connection ID values as the client's first Initial packet ๐Ÿงš

When the client gets its first Initial or Retry packet from the server, it switches to using the Source Connection ID the server gave it as the Destination Connection ID for everything after that, 0-RTT packets included ๐Ÿฆ‹ So a client might end up changing its Destination Connection ID twice during setup: once because of a Retry packet, and once because of an Initial packet from the server. Main character handshake era ๐Ÿ˜ญ Once a client has a valid Initial packet from the server, it MUST throw away any later packet on that connection that has a different Source Connection ID. Not me ignoring the imposters ๐Ÿ™ˆ

A client MUST only change the Destination Connection ID it sends packets to in response to the first Initial or Retry packet it receives. A server MUST set the Destination Connection ID it uses for sending packets based on the first Initial packet it receives. After that, the Destination Connection ID can only change if the new values come from NEW_CONNECTION_ID frames. If later Initial packets show up with a different Source Connection ID, they MUST be discarded. This keeps things from getting messy, because processing several Initial packets with different Source Connection IDs statelessly could otherwise lead to unpredictable results ๐ŸŒˆ

The Destination Connection ID an endpoint sends can change over the life of a connection, especially when connection migration happens (Section 9). See Section 5.1.1 for the details ๐ŸŒธ


7.3. Authenticating Connection IDs ๐Ÿ”

Every connection ID choice made during the handshake gets authenticated by putting all of those values in the transport parameters (see Section 7.4). So all the connection IDs used during the handshake are also covered by the cryptographic handshake. Security glow-up ๐Ÿ’œ

Each endpoint puts the Source Connection ID value from the first Initial packet it sent into the initial_source_connection_id transport parameter (see Section 18.2). A server puts the Destination Connection ID from the first Initial packet it received from the client into the original_destination_connection_id transport parameter. If the server sent a Retry packet, this means the first Initial packet it got before it sent that Retry. If a server sends a Retry packet, it also puts the Source Connection ID from the Retry packet into the retry_source_connection_id transport parameter ๐Ÿ“

The values a peer gives for these transport parameters MUST match the values that the endpoint used in the Destination and Source Connection ID fields of the Initial packets it sent (and, for servers, received). Endpoints MUST check that the received transport parameters match the received connection ID values. Putting connection IDs in the transport parameters and checking them is what stops an attacker from picking the connection ID for a successful connection by injecting packets with their own connection IDs during the handshake. Attacker denied, era ended ๐Ÿ™…โ€โ™€๏ธ๐Ÿ’€

An endpoint MUST treat a missing initial_source_connection_id transport parameter (from either endpoint), or a missing original_destination_connection_id transport parameter (from the server), as a connection error of type TRANSPORT_PARAMETER_ERROR. No cap, that's a hard stop ๐Ÿ›‘

An endpoint MUST treat any of the following as a connection error of type TRANSPORT_PARAMETER_ERROR or PROTOCOL_VIOLATION:

  • the retry_source_connection_id transport parameter is missing from the server after it received a Retry packet,
  • the retry_source_connection_id transport parameter is present even though no Retry packet was received, or
  • a value a peer sent in these transport parameters doesn't match the value in the corresponding Destination or Source Connection ID field of the Initial packets.

If a zero-length connection ID is selected, the matching transport parameter still gets included, just with a zero-length value ๐Ÿซง

Figure 7 shows the connection IDs (DCID is the Destination Connection ID, SCID is the Source Connection ID) used in a full handshake. It covers the Initial packet exchange, plus the later 1-RTT packets that use the connection ID set up during the handshake โœจ

   Client                                                  Server

   Initial: DCID=S1, SCID=C1 ->
                                     <- Initial: DCID=C1, SCID=S3
                                ...
   1-RTT: DCID=S3 ->
                                                <- 1-RTT: DCID=C1

               Figure 7: Use of Connection IDs in a Handshake

Figure 8 shows a similar handshake that includes a Retry packet ๐ŸŽ€

   Client                                                  Server

   Initial: DCID=S1, SCID=C1 ->
                                       <- Retry: DCID=C1, SCID=S2
   Initial: DCID=S2, SCID=C1 ->
                                     <- Initial: DCID=C1, SCID=S3
                                ...
   1-RTT: DCID=S3 ->
                                                <- 1-RTT: DCID=C1

         Figure 8: Use of Connection IDs in a Handshake with Retry

In both cases (Figures 7 and 8), the client sets the initial_source_connection_id transport parameter to "C1" ๐Ÿฆ‹

When the handshake doesn't include a Retry (Figure 7), the server sets original_destination_connection_id to "S1" (the client chose this value, so keep that in mind ๐Ÿ‘€) and initial_source_connection_id to "S3". In this case, the server doesn't include a retry_source_connection_id transport parameter.

When the handshake includes a Retry (Figure 8), the server sets original_destination_connection_id to "S1", retry_source_connection_id to "S2", and initial_source_connection_id to "S3" ๐ŸŒˆ


7.4. Transport Parameters โš™๏ธ

During connection setup, both endpoints make authenticated declarations of their transport parameters. Endpoints are required to comply with the restrictions each parameter defines, and each parameter's description includes rules for how to handle it ๐Ÿ’–

Transport parameters are declarations each endpoint makes on its own. Each endpoint can pick its values independently of what its peer picked ๐Ÿงš

The encoding of the transport parameters is detailed in Section 18.

QUIC puts the encoded transport parameters into the cryptographic handshake. Once the handshake completes, each endpoint can see the transport parameters its peer declared, and each endpoint validates the values its peer provided โœจ

The definitions for each of the defined transport parameters are in Section 18.2.

An endpoint MUST treat receiving a transport parameter with an invalid value as a connection error of type TRANSPORT_PARAMETER_ERROR.

An endpoint MUST NOT send a parameter more than once in a given transport parameters extension. An endpoint SHOULD treat receiving duplicate transport parameters as a connection error of type TRANSPORT_PARAMETER_ERROR. Double-dipping is a no-go ๐Ÿ™…โ€โ™€๏ธ

Endpoints use transport parameters to authenticate how connection IDs got negotiated during the handshake (see Section 7.3) ๐Ÿ”

ALPN (see [ALPN]) lets clients offer multiple application protocols during connection setup ๐ŸŒธ The transport parameters a client includes in the handshake apply to all the application protocols it offers. Application protocols can recommend values for transport parameters, like initial flow control limits. However, if an application protocol puts constraints on transport parameter values, those constraints might conflict, and then a client could end up unable to offer multiple application protocols ๐Ÿ˜ญ

7.4.1. Values of Transport Parameters for 0-RTT ๐Ÿงš

okay so 0-RTT only works if both the client and the server are using protocol parameters they already agreed on in a previous connection ๐Ÿซถ. to make that possible, endpoints store the server's transport parameter values alongside any session tickets they get on the connection โœจ. they also store anything else the application protocol or cryptographic handshake needs (see Section 4.6 of [QUIC-TLS]). then when they try 0-RTT with those session tickets, they use those stored values.

the remembered transport parameters apply to the new connection until the handshake finishes and the client starts sending 1-RTT packets ๐Ÿ’…. once the handshake is done, the client switches over to whatever transport parameters were set during the handshake. and not every parameter gets remembered, because some of them don't matter for future connections or don't affect 0-RTT at all ๐Ÿ™ˆ.

the definition of a new transport parameter (Section 7.4.2) MUST specify whether storing that parameter for 0-RTT is mandatory, optional, or prohibited ๐Ÿ“‹. a client doesn't need to store a transport parameter it can't process, which is very valid ๐Ÿ’œ.

a client MUST NOT use remembered values for these parameters: ack_delay_exponent, max_ack_delay, initial_source_connection_id, original_destination_connection_id, preferred_address, retry_source_connection_id, and stateless_reset_token. instead, the client MUST use the server's new values from the handshake. if the server doesn't send new values, the default values are used ๐ŸŒธ.

a client that tries to send 0-RTT data MUST remember all the other transport parameters the server used that it's able to process ๐ŸŽ€. the server can remember these itself, or it can keep an integrity-protected copy of the values in the ticket and pull them back out when it accepts the 0-RTT data. the server uses the transport parameters to decide whether to accept 0-RTT data at all ๐Ÿ‘‘.

if the server accepts 0-RTT data, it MUST NOT reduce any limits or change any values that the client might break with its 0-RTT data. this is a big deal. specifically, a server that accepts 0-RTT data MUST NOT set these parameters (Section 18.2) to anything smaller than the remembered values:
  • active_connection_id_limit
  • initial_max_data
  • initial_max_stream_data_bidi_local
  • initial_max_stream_data_bidi_remote
  • initial_max_stream_data_uni
  • initial_max_streams_bidi
  • initial_max_streams_uni

leaving out a parameter, or setting it to zero, can make 0-RTT data get enabled but still be unusable ๐Ÿ˜ญ. the parameters that let application data get sent SHOULD be set to non-zero values for 0-RTT. that includes initial_max_data, plus either (1) initial_max_streams_bidi and initial_max_stream_data_bidi_remote, or (2) initial_max_streams_uni and initial_max_stream_data_uni ๐Ÿ“.

a server might give bigger initial stream flow control limits than the remembered ones that the client uses when sending 0-RTT. that's fine. once the handshake is done, the client updates the flow control limits on all its sending streams using the new values of initial_max_stream_data_bidi_remote and initial_max_stream_data_uni ๐Ÿฆ‹.

a server MAY store and recover the previously sent values of max_idle_timeout, max_udp_payload_size, and disable_active_migration, and reject 0-RTT if it picks smaller values. lowering these while also accepting 0-RTT data could hurt the connection's performance ๐Ÿ“‰. specifically, lowering max_udp_payload_size could cause dropped packets, which would be worse than just rejecting the 0-RTT data outright ๐Ÿ’€.

a server MUST reject 0-RTT data if the restored transport parameter values can't be supported ๐Ÿšซ.

when a client sends frames in 0-RTT packets, it MUST only use remembered transport parameters. this is the important part: it MUST NOT use updated values it learns from the server's new transport parameters or from frames received in 1-RTT packets ๐Ÿ™…โ€โ™€๏ธ. updated transport parameter values from the handshake only apply to 1-RTT packets. for example, flow control limits from remembered parameters apply to all 0-RTT packets, even if the handshake or 1-RTT frames raise those limits ๐Ÿ’œ. a server MAY treat using updated transport parameters in 0-RTT as a connection error of type PROTOCOL_VIOLATION ๐Ÿ”ฅ.


7.4.2. New Transport Parameters ๐ŸŒˆ

new transport parameters are how endpoints negotiate new protocol behavior โœจ. an endpoint MUST ignore any transport parameter it doesn't support. so if a parameter is missing, any optional feature that would have been negotiated with it is just off ๐Ÿซง. some identifiers are reserved for exactly this purpose, as described in Section 18.1 ๐ŸŽ€.

a client that doesn't understand a transport parameter can throw it away and try 0-RTT on later connections. but if the client adds support for a parameter it had thrown away, it risks breaking the constraints that parameter sets when it tries 0-RTT ๐Ÿ˜ฌ. new transport parameters can avoid this by using a default of the most conservative value. clients can also avoid it by remembering every parameter, even ones they don't support yet ๐Ÿง .

new transport parameters can be registered following the rules in Section 22.3 ๐Ÿ“‹.


7.5. Cryptographic Message Buffering ๐Ÿ“ฆ

implementations need to keep a buffer for CRYPTO data that shows up out of order ๐Ÿงบ. CRYPTO frames don't have flow control, so an endpoint could force its peer to buffer an unlimited amount of data. that's a lot, bestie ๐Ÿ˜ญ.

implementations MUST support buffering at least 4096 bytes of data received in out-of-order CRYPTO frames ๐Ÿ’–. endpoints MAY choose to let more data be buffered during the handshake. a bigger limit during the handshake can make room for larger keys or credentials to be exchanged ๐Ÿ”‘. an endpoint's buffer size doesn't have to stay the same for the whole life of the connection ๐Ÿฆ‹.

not being able to buffer CRYPTO frames during the handshake can end up failing the connection ๐Ÿ’”. if an endpoint's buffer gets exceeded during the handshake, it can temporarily expand the buffer so it can finish the handshake โœจ. if it doesn't expand the buffer, it MUST close the connection with a CRYPTO_BUFFER_EXCEEDED error code ๐Ÿšจ.

once the handshake is done, if an endpoint can't buffer all the data in a CRYPTO frame, it MAY discard that frame and all CRYPTO frames received after it, or it MAY close the connection with a CRYPTO_BUFFER_EXCEEDED error code ๐Ÿ™ˆ. packets that contained discarded CRYPTO frames MUST be acknowledged, because the packet was still received and processed by the transport even though the CRYPTO frame got tossed ๐Ÿซถ.


8. Address Validation ๐Ÿ›ก๏ธ

address validation makes sure an endpoint can't be used for a traffic amplification attack ๐Ÿ˜ค. here's how that attack goes: someone sends a packet to a server with a spoofed source address that points at a victim. if the server sends more or bigger packets back in response, the attacker can use the server to push more data at the victim than they could send on their own. not okay ๐Ÿ’€.

the main defense against amplification is checking that a peer can actually receive packets at the transport address it claims ๐ŸŽฏ. so after getting packets from an address that isn't validated yet, an endpoint MUST limit how much data it sends to that unvalidated address to three times the amount of data it received from that address ๐Ÿ“. this cap on response size is called the anti-amplification limit ๐Ÿงข.

address validation happens both during connection establishment (see Section 8.1) and during connection migration (see Section 8.2) ๐Ÿฆ‹.


8.1. Address Validation during Connection Establishment ๐Ÿค

connection establishment gives both endpoints address validation automatically ๐ŸŽ€. specifically, receiving a packet protected with Handshake keys confirms that the peer successfully processed an Initial packet โœ…. once an endpoint has successfully processed a Handshake packet from its peer, it can consider the peer's address validated ๐Ÿ’….

also, an endpoint MAY consider the peer address validated if the peer uses a connection ID that the endpoint chose and that connection ID has at least 64 bits of entropy ๐ŸŽฒ.

for the client, the Destination Connection ID field in its first Initial packet lets it validate the server address as part of successfully processing any packet ๐Ÿงฉ. Initial packets from the server are protected with keys derived from this value (see Section 5.2 of [QUIC-TLS]). alternatively, the server echoes the value in Version Negotiation packets (Section 6) or includes it in the Integrity Tag in Retry packets (Section 5.8 of [QUIC-TLS]) ๐Ÿ”.

before validating the client address, servers MUST NOT send more than three times as many bytes as they have received ๐Ÿšง. this limits how big any spoofed-address amplification attack can get ๐Ÿ“‰. to avoid amplification before address validation, servers MUST count all of the payload bytes received in datagrams that are uniquely attributed to a single connection. this includes datagrams with packets that get processed successfully and datagrams whose packets all get discarded ๐Ÿงพ.

clients MUST make sure UDP datagrams containing Initial packets have UDP payloads of at least 1200 bytes, adding PADDING frames as needed ๐ŸŒธ. a client that sends padded datagrams lets the server send more data before address validation finishes ๐ŸŽ.

here's a sneaky one: losing an Initial or Handshake packet from the server can cause a deadlock if the client doesn't send any more Initial or Handshake packets ๐Ÿ˜ฑ. this can happen when the server hits its anti-amplification limit and the client has already been acknowledged for everything it sent. then the client has no reason to send more packets, and the server can't send more data either, because it hasn't validated the client's address. to stop this deadlock, clients MUST send a packet when a Probe Timeout (PTO) fires; see Section 6.2 of [QUIC-RECOVERY]. specifically, the client MUST send an Initial packet in a UDP datagram with at least 1200 bytes if it doesn't have Handshake keys, and otherwise it sends a Handshake packet ๐Ÿ†˜.

a server might want to validate the client address before starting the cryptographic handshake ๐Ÿค”. QUIC uses a token in the Initial packet to do address validation before the handshake finishes ๐ŸŽซ. the token gets delivered to the client during connection establishment, either with a Retry packet (see Section 8.1.2) or in an earlier connection using the NEW_TOKEN frame (see Section 8.1.3) ๐Ÿ’Œ.

on top of the limits that apply before address validation, servers are also limited by what the congestion controller allows. clients are only limited by the congestion controller ๐ŸŒŠ.


8.1.1. Token Construction ๐Ÿช™

a token sent in a NEW_TOKEN frame or a Retry packet MUST be built in a way that lets the server tell how it was given to the client ๐Ÿ”. these tokens are carried in the same field, but servers have to handle them differently ๐Ÿ’œ.

8.1.2. Address Validation Using Retry Packets โœจ

Okay so when the server gets the client's Initial packet, it can ask for address validation by sending a Retry packet (Section 17.2.5) that carries a token ๐ŸŽ€. The client MUST repeat that token in every Initial packet it sends for that connection after it gets the Retry packet. No skipping, bestie ๐Ÿ’….

Once the server has processed an Initial packet that has a token from a Retry packet, it cannot send another Retry. Its only options now are to refuse the connection or let it carry on ๐Ÿ™ˆ.

As long as an attacker can't make a valid token for its own address (see Section 8.1.4) and the client can send that token back, returning it proves to the server that the client actually got the token ๐Ÿ”.

A server can also use a Retry packet to put off the state and processing costs of connection establishment ๐Ÿง . If the server gives a different connection ID, along with the original_destination_connection_id transport parameter defined in Section 18.2, it has to prove that it (or some entity it works with) got the original Initial packet from the client ๐Ÿ•ต๏ธ. Giving a different connection ID also gives the server some control over how later packets get routed ๐Ÿ—บ๏ธ. That can be used to send connections to a different server instance ๐Ÿฆ‹.

If a server gets a client Initial with an invalid Retry token that is otherwise valid, it knows the client won't accept another Retry token ๐Ÿ˜ญ. The server could just throw the packet away and let the client time out to find out the handshake failed, but that could cost the client a huge latency penalty. Instead, the server SHOULD immediately close the connection (Section 10.2) with an INVALID_TOKEN error ๐Ÿ’€. Also, at this point the server hasn't set up any state for the connection, so it doesn't enter the closing period.

Here's the Retry flow, Figure 9 ๐ŸŒธ:

   Client                                                  Server

   Initial[0]: CRYPTO[CH] ->

                                                   <- Retry+Token

   Initial+Token[1]: CRYPTO[CH] ->

                                    Initial[0]: CRYPTO[SH] ACK[1]
                          Handshake[0]: CRYPTO[EE, CERT, CV, FIN]
                                    <- 1-RTT[0]: STREAM[1, "..."]

Figure 9: Example Handshake with Retry

8.1.3. Address Validation for Future Connections ๐Ÿ”ฎ

A server MAY give clients an address validation token during one connection that can be used on a later connection ๐ŸŽซ. This is super important with 0-RTT, because a server can end up sending a lot of data to a client in response to 0-RTT data.

The server uses the NEW_TOKEN frame (Section 19.7) to hand the client an address validation token that can validate future connections ๐Ÿ’Œ. On a future connection, the client puts this token in its Initial packets to get address validation. The client MUST include the token in all Initial packets it sends, unless a Retry replaces the token with a newer one. The client MUST NOT use a token provided in a Retry for future connections. Servers MAY discard any Initial packet that doesn't carry the expected token.

Unlike the token made for a Retry packet, which gets used right away, the token in a NEW_TOKEN frame can be used after some time has passed โฐ. So a token SHOULD have an expiration time. That could be an explicit expiration time, or an issued timestamp the server can use to work out the expiration time on the fly. A server can store the expiration time or put it inside the token in encrypted form ๐Ÿ”’.

A token issued with NEW_TOKEN MUST NOT include information that would let an observer link values to the connection where it was issued ๐Ÿ•ต๏ธโ€โ™€๏ธ. For example, it can't include the previous connection ID or addressing information, unless those values are encrypted. A server MUST make sure every NEW_TOKEN frame it sends is unique across all clients, except for ones sent to repair losses of earlier NEW_TOKEN frames. Information that lets the server tell Retry tokens apart from NEW_TOKEN tokens MAY be visible to entities other than the server.

The client port number is very unlikely to be the same on two different connections, so validating the port probably won't work ๐Ÿ™…โ€โ™€๏ธ.

A token received in a NEW_TOKEN frame applies to any server the connection is considered authoritative for (for example, server names in the certificate) ๐Ÿ“œ. When the client connects to a server it has an applicable and unused token for, it SHOULD include that token in the Token field of its Initial packet. Including a token might let the server validate the client address without an extra round trip โšก. A client MUST NOT include a token that doesn't apply to the server it's connecting to, unless the client knows that the server that issued the token and the server it's connecting to are jointly managing tokens. A client MAY use a token from any previous connection to that server.

A token lets a server correlate activity between the connection where the token was issued and any connection where it's used ๐Ÿงฉ. Clients that want to break continuity of identity with a server can throw away tokens provided in NEW_TOKEN frames ๐Ÿซฅ. By comparison, a token from a Retry packet MUST be used immediately during the connection attempt and can't be used in later connection attempts.

A client SHOULD NOT reuse a token from a NEW_TOKEN frame across different connection attempts. Reusing a token lets entities on the network path link connections together (see Section 9.5) ๐Ÿ‘€.

Clients might get several tokens on a single connection. Apart from preventing linkability, any token can be used in any connection attempt ๐ŸŽŸ๏ธ. Servers can send extra tokens to enable address validation for multiple connection attempts, or to replace older tokens that might stop being valid. For a client, that ambiguity means sending the most recent unused token is most likely to work. Saving and using older tokens has no real downside, but clients can think of older tokens as less likely to help the server validate addresses.

When a server gets an Initial packet with an address validation token, it MUST try to validate the token, unless it has already completed address validation. If the token is invalid, the server SHOULD carry on as if the client didn't have a validated address, which could include sending a Retry packet. Servers can tell tokens from NEW_TOKEN frames apart from tokens from Retry packets (see Section 8.1.1), and Retry tokens can be validated more strictly ๐Ÿง. If validation succeeds, the server SHOULD then let the handshake proceed โœ….

๐Ÿ“ Note: The reason the server treats the client as unvalidated, instead of just tossing the packet, is that the client might have gotten the token in an earlier connection through a NEW_TOKEN frame. If the server has lost state, it might not be able to validate the token at all, and dropping the packet would make the connection fail.

In a stateless design, a server can use encrypted and authenticated tokens to pass information to clients, which the server can later recover and use to validate a client address ๐Ÿช„. Tokens aren't part of the cryptographic handshake, so they aren't authenticated. For instance, a client might be able to reuse a token. To guard against attacks that exploit this, a server can limit its use of tokens to only the information it needs to validate client addresses ๐Ÿ›ก๏ธ.

Clients MAY use tokens obtained on one connection for any connection attempt using the same version ๐Ÿ’œ. When choosing a token, clients don't need to think about other properties of the connection being attempted, like the choice of application protocols, session tickets, or other connection properties.

8.1.4. Address Validation Token Integrity ๐Ÿ”

An address validation token MUST be difficult to guess. Putting a random value with at least 128 bits of entropy in the token would be enough, but that depends on the server remembering the value it sent to clients ๐Ÿง .

A token-based scheme lets the server offload any state tied to validation onto the client ๐Ÿ“ฆ. For this design to work, the token MUST be covered by integrity protection, so clients can't modify or fake it. Without integrity protection, malicious clients could generate or guess token values that the server would accept ๐Ÿ˜ˆ. Only the server needs access to the integrity protection key for tokens.

There's no need for one well-defined token format, because the server that generates the token is also the one that consumes it ๐Ÿซถ. Tokens sent in Retry packets SHOULD include information that lets the server check that the source IP address and port in client packets stay the same ๐Ÿ”.

Tokens sent in NEW_TOKEN frames MUST include information that lets the server verify the client IP address hasn't changed since the token was issued. Servers can use tokens from NEW_TOKEN frames when deciding not to send a Retry packet, even if the client address has changed. If the client IP address has changed, the server MUST adhere to the anti-amplification limit; see Section 8. Note that with NAT in the picture, this requirement might not be enough to protect other hosts sharing the NAT from amplification attacks ๐ŸŒ.

Attackers could replay tokens to use servers as amplifiers in DDoS attacks ๐Ÿšจ. To protect against this, servers MUST ensure that token replay is prevented or limited. Servers SHOULD ensure that tokens sent in Retry packets are only accepted for a short time, since clients send them back immediately. Tokens provided in NEW_TOKEN frames (Section 19.7) need to stay valid for longer, but they SHOULD NOT be accepted multiple times. Servers are encouraged to allow tokens to be used only once, if possible. Tokens MAY include additional information about clients to narrow down where they can be applied or reused ๐Ÿ’–.

8.2. Path Validation โœจ

okay bestie, so path validation is what both peers do during connection migration (see Section 9) to check that they can still reach each other after an address changes ๐Ÿฆ‹. Here, endpoints test reachability between a specific local address and a specific peer address, and an address is just the 2-tuple of IP address and port ๐ŸŒธ.

Path validation checks that packets sent on a path to a peer actually get received by that peer ๐Ÿ’…. It's how you make sure a migrating peer isn't sending packets with a spoofed source address ๐Ÿ™ˆ.

Path validation does NOT prove the peer can send back the other way ๐Ÿ˜ญ. Acknowledgments can't be used for return path validation, because they don't have enough entropy and could be spoofed ๐Ÿ’€. Each direction of a path gets figured out independently, so return reachability can only be established by the peer itself ๐Ÿซถ.

Either endpoint can start path validation whenever it wants ๐Ÿ”ฅ. For example, an endpoint might check that its peer still has the address after a stretch of quiet time โญ.

Path validation is not a NAT traversal mechanism, and I need you to hear that clearly ๐Ÿงš. It might help create NAT bindings that support NAT traversal, but the expectation is that one endpoint can receive packets without having sent anything on that path first ๐Ÿ’œ. Real NAT traversal needs extra synchronization that isn't provided here ๐ŸŒˆ.

An endpoint MAY include other frames alongside the PATH_CHALLENGE and PATH_RESPONSE frames used for path validation ๐ŸŽ€. For example, an endpoint can put PADDING frames with a PATH_CHALLENGE frame for Path Maximum Transmission Unit Discovery (PMTUD), see Section 14.2.1 ๐Ÿ“. An endpoint can also include its own PATH_CHALLENGE frame when it sends a PATH_RESPONSE frame ๐Ÿ’–.

An endpoint uses a new connection ID for probes sent from a new local address, see Section 9.5 โœจ. When probing a new path, an endpoint can make sure its peer has an unused connection ID ready for responses ๐Ÿฆ‹. If the peer's active_connection_id_limit allows it, sending NEW_CONNECTION_ID and PATH_CHALLENGE frames in the same packet makes sure an unused connection ID is available to the peer when it sends its response ๐Ÿ‘‘.

An endpoint can choose to probe several paths at once ๐Ÿ’…. The number of simultaneous probe paths is capped by how many extra connection IDs its peer has already supplied, since every new local address used for a probe needs a previously unused connection ID ๐ŸŒธ.

8.2.1. Initiating Path Validation

To start path validation, an endpoint sends a PATH_CHALLENGE frame with an unpredictable payload on the path it wants to validate ๐Ÿ”ฎ.

An endpoint MAY send multiple PATH_CHALLENGE frames to guard against packet loss ๐Ÿ›ก๏ธ. However, an endpoint SHOULD NOT send multiple PATH_CHALLENGE frames in a single packet ๐Ÿ™…โ€โ™€๏ธ.

An endpoint SHOULD NOT probe a new path with packets containing a PATH_CHALLENGE frame more often than it would send an Initial packet ๐ŸŒท. This keeps connection migration from putting more load on a new path than establishing a brand new connection would ๐Ÿ’–.

The endpoint MUST use unpredictable data in every PATH_CHALLENGE frame so it can match the peer's response to the right PATH_CHALLENGE ๐ŸŽฏ.

An endpoint MUST expand datagrams that contain a PATH_CHALLENGE frame to at least the smallest allowed maximum datagram size of 1200 bytes ๐Ÿ“, unless the anti-amplification limit for the path doesn't allow a datagram that size ๐Ÿšง. Sending UDP datagrams this big proves the network path from the endpoint to the peer can carry QUIC, see Section 14 ๐ŸŒˆ.

If the endpoint can't expand the datagram to 1200 bytes because of the anti-amplification limit, the path MTU won't be validated ๐Ÿ˜ฌ. To make sure the path MTU is big enough, the endpoint MUST do a second path validation by sending a PATH_CHALLENGE frame in a datagram of at least 1200 bytes ๐Ÿซ . This extra validation can happen after a PATH_RESPONSE comes back successfully, or once enough bytes have been received on the path that sending the bigger datagram won't go over the anti-amplification limit ๐ŸŽ€.

Unlike other cases where datagrams get expanded, endpoints MUST NOT discard datagrams that look too small when they contain PATH_CHALLENGE or PATH_RESPONSE ๐Ÿšซ.

8.2.2. Path Validation Responses

When an endpoint receives a PATH_CHALLENGE frame, it MUST respond by echoing the data from that PATH_CHALLENGE in a PATH_RESPONSE frame ๐Ÿ”. An endpoint MUST NOT delay sending a packet with a PATH_RESPONSE frame unless congestion control is holding it back ๐Ÿš€.

A PATH_RESPONSE frame MUST be sent on the network path where the PATH_CHALLENGE frame was received ๐Ÿ›ค๏ธ. This means path validation by a peer only works if the path functions in both directions ๐Ÿ’ฏ. This requirement MUST NOT be enforced by the endpoint that starts path validation, because that would open up an attack on migration, see Section 9.3.3 โš ๏ธ.

An endpoint MUST expand datagrams that contain a PATH_RESPONSE frame to at least 1200 bytes ๐Ÿ“ฆ. This checks that the path can carry datagrams of this size in both directions ๐Ÿช„. But an endpoint MUST NOT expand the datagram containing the PATH_RESPONSE if the resulting data goes over the anti-amplification limit ๐Ÿ™ƒ. That's only expected to happen if the PATH_CHALLENGE wasn't sent in an expanded datagram ๐ŸŒธ.

An endpoint MUST NOT send more than one PATH_RESPONSE frame in response to one PATH_CHALLENGE frame, see Section 13.3 ๐Ÿ›‘. The peer is expected to send more PATH_CHALLENGE frames whenever it needs more PATH_RESPONSE frames ๐Ÿ’Œ.

8.2.3. Successful Path Validation

Path validation succeeds when a PATH_RESPONSE frame arrives containing the data from an earlier PATH_CHALLENGE frame ๐Ÿฅณ. A PATH_RESPONSE frame received on any network path validates the path the PATH_CHALLENGE was sent on ๐Ÿฆ‹.

If an endpoint sends a PATH_CHALLENGE frame in a datagram that isn't expanded to at least 1200 bytes, and the response validates the peer address, then the path is validated but the path MTU isn't ๐Ÿ˜ฎ. That means the endpoint can now send more than three times the amount of data it has received ๐Ÿซจ. Even so, the endpoint MUST start another path validation with an expanded datagram to confirm the path supports the required MTU ๐Ÿ“.

Getting an acknowledgment for a packet that contained a PATH_CHALLENGE frame is NOT enough validation, since a malicious peer can spoof the acknowledgment ๐Ÿ•ต๏ธโ€โ™€๏ธ.

8.2.4. Failed Path Validation

Path validation only fails when the endpoint trying to validate the path gives up on it ๐Ÿ™…โ€โ™€๏ธ.

Endpoints SHOULD abandon path validation based on a timer โฐ. When setting this timer, implementations should be warned that the new path could have a longer round-trip time than the original ๐Ÿข. Using three times the larger of the current PTO or the PTO for the new path (using kInitialRtt, as defined in [QUIC-RECOVERY]) is RECOMMENDED ๐Ÿ’….

This timeout gives multiple PTOs time to expire before path validation fails, so losing one PATH_CHALLENGE or PATH_RESPONSE frame doesn't sink it ๐Ÿ›Ÿ.

The endpoint might receive packets with other frames on the new path, but a PATH_RESPONSE frame with the right data is required for path validation to succeed ๐ŸŽฏ.

When an endpoint abandons path validation, it decides the path is unusable ๐Ÿšท. This doesn't necessarily mean the connection failed, since endpoints can keep sending packets over other paths as needed ๐ŸŒˆ. If no paths are available, an endpoint can wait for a new path to show up or close the connection ๐Ÿคทโ€โ™€๏ธ. An endpoint with no valid network path to its peer MAY signal this with the NO_VIABLE_PATH connection error ๐Ÿšจ, but that's only possible if the network path exists and just doesn't support the required MTU (Section 14) ๐Ÿงฉ.

A path validation might be abandoned for other reasons too, not just failure ๐ŸŒ€. Mostly this happens when a connection migration to a new path starts while a path validation on the old path is still in progress ๐Ÿ”„.

9. Connection Migration ๐Ÿฆ‹

okay so the connection ID is what lets connections survive address changes (IP address and port) ๐Ÿซถ, like when an endpoint moves to a new network ๐Ÿšถโ€โ™€๏ธ. This section explains how an endpoint migrates to a new address ๐Ÿ’–.

QUIC's design counts on endpoints keeping a stable address for the whole handshake ๐Ÿ . An endpoint MUST NOT start connection migration before the handshake is confirmed, as defined in Section 4.1.2 of [QUIC-TLS] ๐Ÿšซ.

If the peer sent the disable_active_migration transport parameter, an endpoint also MUST NOT send packets (including probing packets, see Section 9.1) from a local address different from the one the peer used during the handshake ๐Ÿ™…โ€โ™€๏ธ, unless the endpoint has acted on a preferred_address transport parameter from the peer ๐ŸŽ. If the peer breaks this requirement, the endpoint MUST either drop the incoming packets on that path without generating a Stateless Reset, or go ahead with path validation and let the peer migrate ๐Ÿค”. Generating a Stateless Reset or closing the connection would let third parties on the network make connections close by spoofing or messing with traffic they can see ๐Ÿ˜ฑ.

Not every change in peer address is intentional, or an active migration ๐Ÿ™ƒ. The peer could go through NAT rebinding, which is when a middlebox (usually a NAT) gives a flow a new outgoing port or even a new outgoing IP address ๐ŸŒช๏ธ. An endpoint MUST perform path validation (Section 8.2) if it detects any change to a peer's address, unless it has already validated that address ๐Ÿ”.

When an endpoint has no validated path to send packets on, it MAY discard connection state ๐Ÿ—‘๏ธ. An endpoint capable of connection migration MAY wait for a new path to become available before discarding connection state โณ.

This document limits migration to new client addresses, except as described in Section 9.6 ๐Ÿšง. Clients are responsible for starting all migrations ๐Ÿงญ. Servers don't send non-probing packets (see Section 9.1) toward a client address until they see a non-probing packet from that address ๐Ÿ™ˆ. If a client gets packets from an unknown server address, the client MUST discard those packets ๐Ÿ—‘๏ธ.

9.1. Probing a New Path ๐Ÿ”ญ

An endpoint MAY probe for peer reachability from a new local address using path validation (Section 8.2) before migrating the connection to that new local address ๐Ÿ›ธ. If path validation fails, the new path just isn't usable for this connection ๐Ÿ™…โ€โ™€๏ธ. A failed validation doesn't end the connection unless there are no valid alternative paths left ๐Ÿ’ช.

PATH_CHALLENGE, PATH_RESPONSE, NEW_CONNECTION_ID, and PADDING frames are "probing frames", and every other frame is a "non-probing frame" ๐Ÿท๏ธ. A packet containing only probing frames is a "probing packet", and a packet containing any other frame is a "non-probing packet" ๐Ÿ“ฆ.

9.2. Initiating Connection Migration ๐Ÿฆ‹

okay so an endpoint can migrate a connection to a new local address just by sending packets with non-probing frames from that address โœจ

each endpoint validates its peer's address during connection establishment, so a migrating endpoint can send to its peer knowing the peer is willing to receive at the peer's current address ๐Ÿ’… that means an endpoint can migrate to a new local address without first validating the peer's address. iconic ๐Ÿ‘‘

to establish reachability on the new path, an endpoint starts path validation (Section 8.2) on that new path ๐Ÿ›ค๏ธ An endpoint MAY defer path validation until after a peer sends the next non-probing frame to its new address ๐Ÿ™ˆ

when you migrate, the new path might not support your current sending rate ๐Ÿ˜ญ so the endpoint resets its congestion controller and RTT estimate, as described in Section 9.4 ๐Ÿซถ

and the new path might not have the same ECN capability, so the endpoint validates ECN capability as described in Section 13.4 ๐ŸŒˆ


9.3. Responding to Connection Migration ๐Ÿงš

getting a packet from a new peer address that contains a non-probing frame means the peer has migrated to that address ๐Ÿ’œ

if the recipient permits the migration, it MUST send subsequent packets to the new peer address and MUST initiate path validation (Section 8.2) to verify the peer's ownership of the address if validation is not already underway ๐Ÿ”ฅ if the recipient has no unused connection IDs from the peer, it will not be able to send anything on the new path until the peer provides one; see Section 9.5 ๐Ÿ’€

an endpoint only changes the address it sends packets to in response to the highest-numbered non-probing packet. this keeps things safe, so an endpoint doesn't send packets to an old peer address when packets arrive out of order ๐Ÿ“

An endpoint MAY send data to an unvalidated peer address, but it MUST protect against potential attacks as described in Sections 9.3.1 and 9.3.2. An endpoint MAY skip validation of a peer address if that address has been seen recently. in particular, if an endpoint returns to a previously validated path after detecting some form of spurious migration, skipping address validation and restoring loss detection and congestion state can reduce the performance impact of the attack ๐ŸŒธ

after changing the address it sends non-probing packets to, an endpoint can abandon any path validation for other addresses ๐Ÿฆ‹

receiving a packet from a new peer address could be the result of a NAT rebinding at the peer โœจ

after verifying a new client address, the server SHOULD send new address validation tokens (Section 8) to the client ๐ŸŽ€

9.3.1. Peer Address Spoofing ๐Ÿ•ต๏ธ

it is possible that a peer is spoofing its source address to make an endpoint send excessive amounts of data to an unwilling host ๐Ÿ˜ฑ if the endpoint sends significantly more data than the spoofing peer, connection migration might be used to amplify the volume of data an attacker can generate toward a victim. not it ๐Ÿšจ

as described in Section 9.3, an endpoint is required to validate a peer's new address to confirm the peer's possession of the new address. until a peer's address is deemed valid, an endpoint limits the amount of data it sends to that address; see Section 8. without this limit, an endpoint risks being used for a denial-of-service attack against an unsuspecting victim ๐Ÿ’”

if an endpoint skips validation of a peer address as described above, it does not need to limit its sending rate โœจ

9.3.2. On-Path Address Spoofing ๐Ÿงฉ

an on-path attacker could cause a spurious connection migration by copying and forwarding a packet with a spoofed address so that it arrives before the original packet. the packet with the spoofed address will be seen as coming from a migrating connection, and the original packet will be seen as a duplicate and dropped. after a spurious migration, validation of the source address will fail, because the entity at the source address does not have the cryptographic keys to read or respond to the PATH_CHALLENGE frame that is sent to it, even if it wanted to ๐Ÿ”

to protect the connection from failing due to such a spurious migration, an endpoint MUST revert to using the last validated peer address when validation of a new peer address fails. Additionally, receiving packets with higher packet numbers from the legitimate peer address will trigger another connection migration. this cancels validation of the spurious address, so an attacker injecting a single packet can't keep the migrations going ๐Ÿ›ก๏ธ

if an endpoint has no state about the last validated peer address, it MUST close the connection silently by discarding all connection state. this means new packets on the connection get handled generically. For instance, an endpoint MAY send a Stateless Reset in response to any further incoming packets. ๐ŸŒ™

9.3.3. Off-Path Packet Forwarding ๐Ÿ‘€

an off-path attacker that can observe packets might forward copies of genuine packets to endpoints. if the copied packet arrives before the genuine packet, this will look like a NAT rebinding, and any genuine packet will be discarded as a duplicate. if the attacker keeps forwarding packets, it might be able to cause migration to a path via the attacker. that puts the attacker on-path, giving it the ability to observe or drop all subsequent packets ๐Ÿ˜ˆ

this style of attack relies on the attacker using a path with approximately the same characteristics as the direct path between endpoints. the attack is more reliable if relatively few packets are sent or if packet loss coincides with the attempted attack ๐ŸŽฏ

a non-probing packet received on the original path that increases the maximum received packet number will cause the endpoint to move back to that path. eliciting packets on this path increases the likelihood that the attack is unsuccessful. so mitigation of this attack relies on triggering the exchange of packets ๐Ÿƒ

in response to an apparent migration, endpoints MUST validate the previously active path using a PATH_CHALLENGE frame. this induces the sending of new packets on that path. if the path is no longer viable, the validation attempt will time out and fail; if the path is viable but no longer desired, the validation will succeed but only results in probing packets being sent on the path ๐Ÿ”„

an endpoint that receives a PATH_CHALLENGE on an active path SHOULD send a non-probing packet in response. if the non-probing packet arrives before any copy made by an attacker, this results in the connection being migrated back to the original path. any subsequent migration to another path restarts this entire process ๐Ÿ”

this defense is imperfect, but this is not considered a serious problem. if the path via the attack is reliably faster than the original path despite multiple attempts to use that original path, it is not possible to distinguish between an attack and an improvement in routing ๐Ÿคท

an endpoint could also use heuristics to improve detection of this style of attack. For instance, NAT rebinding is improbable if packets were recently received on the old path; similarly, rebinding is rare on IPv6 paths. endpoints can also look for duplicated packets. conversely, a change in connection ID is more likely to indicate an intentional migration rather than an attack ๐Ÿง 


9.4. Loss Detection and Congestion Control ๐ŸŒŠ

the capacity available on the new path might not be the same as the old path. packets sent on the old path MUST NOT contribute to congestion control or RTT estimation for the new path.

on confirming a peer's ownership of its new address, an endpoint MUST immediately reset the congestion controller and round-trip time estimator for the new path to initial values (see Appendices A.3 and B.3 of [QUIC-RECOVERY]) unless the only change in the peer's address is its port number. because port-only changes are commonly the result of NAT rebinding or other middlebox activity, the endpoint MAY instead retain its congestion control state and round-trip estimate in those cases instead of reverting to initial values. in cases where congestion control state retained from an old path is used on a new path with substantially different characteristics, a sender could transmit too aggressively until the congestion controller and the RTT estimator have adapted. generally, implementations are advised to be cautious when using previous values on a new path ๐Ÿซฃ

there could be apparent reordering at the receiver when an endpoint sends data and probes from or to multiple addresses during the migration period, since the two resulting paths could have different round-trip times. a receiver of packets on multiple paths will still send ACK frames covering all received packets โœจ

while multiple paths might be used during connection migration, a single congestion control context and a single loss recovery context (as described in [QUIC-RECOVERY]) could be adequate. for instance, an endpoint might delay switching to a new congestion control context until it is confirmed that an old path is no longer needed (such as the case described in Section 9.3.3) ๐Ÿ’…

a sender can make exceptions for probe packets so that their loss detection is independent and does not unduly cause the congestion controller to reduce its sending rate. an endpoint might set a separate timer when a PATH_CHALLENGE is sent, which is canceled if the corresponding PATH_RESPONSE is received. if the timer fires before the PATH_RESPONSE is received, the endpoint might send a new PATH_CHALLENGE and restart the timer for a longer period of time. this timer SHOULD be set as described in Section 6.2.1 of [QUIC-RECOVERY] and MUST NOT be more aggressive. that's the whole vibe, bestie ๐ŸŒˆ๐Ÿ’–

9.5. Privacy Implications of Connection Migration ๐Ÿฆ‹

okay so bestie, picture this ๐Ÿ’… if you used the SAME stable connection ID on multiple network paths, a sneaky passive observer could literally connect the dots and see that all that activity belongs to one person. not it. so endpoints use DIFFERENT connection IDs when sending from different local addresses, as we talked about in Section 5.1. ๐Ÿซถ but here's the catch: it only works if the connection IDs an endpoint hands out can't be linked by anyone else either ๐Ÿ™ˆ

At any time, endpoints MAY change the Destination Connection ID they transmit with to a value that has not been used on another path. ๐ŸŒธ

An endpoint MUST NOT reuse a connection ID when sending from more than one local address ๐Ÿ’€ -- for example, when initiating connection migration as described in Section 9.2 or when probing a new network path as described in Section 9.1. no reusing, periodt ๐Ÿ™…โ€โ™€๏ธ

Similarly, an endpoint MUST NOT reuse a connection ID when sending to more than one destination address. ๐Ÿ’– Because of network changes outside its peer's control, an endpoint might get packets from a new source address with the same Destination Connection ID field value. in that case it MAY keep using the current connection ID with the new remote address, as long as it's still sending from the same local address โœจ

these reuse rules only cover SENDING, because sometimes the path changes without the connection ID changing, and that's totally possible ๐Ÿฆ‹ for example, after a quiet period, NAT rebinding might push packets onto a new path when the client starts sending again. the endpoint handles that kind of thing the way Section 9.3 describes ๐Ÿ‘‘

Using different connection IDs for packets in both directions on each new network path means the connection ID can't be used to link packets from the same connection across paths ๐ŸŽ€ Header protection makes sure packet numbers can't be used to correlate activity either ๐Ÿงš That said, other stuff like timing and size can STILL be used to correlate activity, so this doesn't stop everything ๐Ÿ˜ญ

An endpoint SHOULD NOT initiate migration with a peer that has requested a zero-length connection ID ๐Ÿ™ˆ, because traffic on the new path might be trivially linkable to traffic on the old one. if the server can still match packets with a zero-length connection ID to the right connection, that means it's using other info to demultiplex packets. for example, a server might give every client a unique address, like with HTTP alternative services [ALTSVC]. and honestly? any info that helps route packets across multiple paths will also let other entities link activity on those paths ๐Ÿ’…

A client might want to lower linkability by switching to a new connection ID, source UDP port, or IP address (see [RFC8981]) when it sends traffic after a period of inactivity ๐ŸŒˆ Changing the sending address at the same time might make the server detect a connection migration. this makes sure the migration mechanisms get exercised even for clients that never saw a NAT rebinding or a real migration โญ Changing address can make a peer reset its congestion control state (see Section 9.4), so addresses SHOULD only be changed infrequently ๐Ÿ“

An endpoint that has run out of available connection IDs can't probe new paths or start migration, and it also can't respond to probes or migration attempts from its peer ๐Ÿ˜ญ So that migration stays possible and packets on different paths can't be correlated, endpoints SHOULD provide new connection IDs before peers migrate; see Section 5.1.1. If a peer might be out of connection IDs, a migrating endpoint could include a NEW_CONNECTION_ID frame in all packets it sends on a new network path ๐Ÿซถ


9.6. Server's Preferred Address ๐ŸŒŸ

okay this one is so cute ๐Ÿฅน QUIC lets servers accept a connection on one IP address and then move it to a more preferred address shortly after the handshake. this is super useful when clients first connect to an address shared by a bunch of servers but would rather use a unicast address for better connection stability ๐Ÿ’œ This section describes how to migrate a connection to a preferred server address ๐Ÿงš

Migrating a connection to a new server address mid-connection is NOT supported by the version of QUIC specified in this document ๐Ÿ™…โ€โ™€๏ธ If a client gets packets from a new server address when it hasn't started a migration to that address, the client SHOULD discard those packets ๐Ÿ’€

9.6.1. Communicating a Preferred Address ๐ŸŽ€

A server shares a preferred address by including the preferred_address transport parameter in the TLS handshake โœจ

Servers MAY communicate a preferred address for each address family (IPv4 and IPv6), so clients can pick whichever one fits their network setup best ๐ŸŒธ

Once the handshake is confirmed, the client SHOULD pick one of the two addresses the server provided and start path validation (see Section 8.2) ๐Ÿฆ‹ The client builds packets using any previously unused active connection ID, taken from either the preferred_address transport parameter or a NEW_CONNECTION_ID frame ๐Ÿงš

As soon as path validation succeeds, the client SHOULD start sending all future packets to the new server address using the new connection ID, and stop using the old server address ๐Ÿ’… If path validation fails, the client MUST keep sending all future packets to the server's original IP address ๐Ÿซถ

9.6.2. Migration to a Preferred Address ๐Ÿ‘‘

A client that migrates to a preferred address MUST validate the address it picks before migrating; see Section 21.5.3 ๐Ÿ”ฅ

A server might get a packet addressed to its preferred IP address at any time after it accepts a connection. If that packet contains a PATH_CHALLENGE frame, the server sends back a packet containing a PATH_RESPONSE frame, as per Section 8.2 ๐ŸŒˆ The server MUST keep sending non-probing packets from its original address until it receives a non-probing packet from the client at its preferred address AND has validated the new path ๐Ÿ™ˆ

The server MUST probe the path toward the client from its preferred address ๐Ÿ’– This helps guard against spurious migration started by an attacker ๐Ÿ˜ญ

Once the server has finished path validation and has received a non-probing packet with a new largest packet number on its preferred address, it starts sending non-probing packets to the client ONLY from its preferred IP address โœจ The server SHOULD drop newer packets for this connection that arrive on the old IP address ๐Ÿ“ The server MAY keep processing delayed packets that arrive on the old IP address ๐ŸŒธ

The addresses a server provides in the preferred_address transport parameter are only valid for the connection they're provided in ๐Ÿ’œ A client MUST NOT use them for other connections, including connections resumed from the current one ๐Ÿšซ

9.6.3. Interaction of Client Migration and Preferred Address ๐Ÿฆ‹

A client might need to migrate connections before it has moved to the server's preferred address. In that case, the client SHOULD do path validation to BOTH the original and preferred server addresses from its new address, at the same time ๐Ÿงš

If path validation of the server's preferred address succeeds, the client MUST abandon validation of the original address and migrate to the server's preferred address ๐Ÿ‘‘ If path validation of the server's preferred address fails but validation of the original address succeeds, the client MAY migrate to its new address and keep sending to the server's original address ๐ŸŽ€

If packets received at the server's preferred address have a different source address than the one seen from the client during the handshake, the server MUST protect against potential attacks as described in Sections 9.3.1 and 9.3.2. Besides intentional simultaneous migration, this can also happen because the client's access network used a different NAT binding for the server's preferred address ๐Ÿ™ˆ

Servers SHOULD start path validation to the client's new address when they get a probe packet from a different address; see Section 8 ๐Ÿ’…

A client that migrates to a new address SHOULD use a preferred address from the same address family for the server ๐Ÿ’–

The connection ID in the preferred_address transport parameter isn't specific to the addresses provided. It's there so the client has a connection ID available for migration, but the client MAY use this connection ID on any path ๐Ÿซถ


9.7. Use of IPv6 Flow Label and Migration ๐ŸŒธ

Endpoints that send data using IPv6 SHOULD apply an IPv6 flow label in compliance with [RFC6437], unless the local API doesn't let them set IPv6 flow labels ๐Ÿ’…

The flow label generation MUST be designed to minimize the chances of linkability with a previously used flow label, because a stable flow label would let someone correlate activity across multiple paths; see Section 9.5 ๐Ÿฆ‹

[RFC6437] suggests deriving values with a pseudorandom function to generate flow labels ๐Ÿงš Including the Destination Connection ID field, along with the source and destination addresses, when generating flow labels keeps changes in sync with changes in other observable identifiers โœจ A cryptographic hash function that combines these inputs with a local secret is one way this might be implemented ๐Ÿ’œ


10. Connection Termination ๐Ÿ’€

An established QUIC connection can be terminated in one of three ways:

  • idle timeout (Section 10.1)
  • immediate close (Section 10.2)
  • stateless reset (Section 10.3)

An endpoint MAY discard connection state if it doesn't have a validated path it can send packets on; see Section 8.2 ๐Ÿซถ

10.1. Idle Timeout โฐ

If a max_idle_timeout is specified by either endpoint in its transport parameters (Section 18.2), the connection is silently closed and its state is discarded when it stays idle for longer than the minimum of the max_idle_timeout values advertised by both endpoints ๐ŸŒˆ

Each endpoint advertises a max_idle_timeout, but the effective value at an endpoint is computed as the minimum of the two advertised values (or the sole advertised value, if only one endpoint advertises a non-zero value) ๐Ÿ“ By announcing a max_idle_timeout, an endpoint commits to initiating an immediate close (Section 10.2) if it abandons the connection before the effective value ๐Ÿ‘‘

An endpoint restarts its idle timer when it receives and successfully processes a packet from its peer ๐Ÿ’– An endpoint also restarts its idle timer when it sends an ack-eliciting packet, as long as no other ack-eliciting packets have been sent since it last received and processed a packet ๐Ÿฆ‹ Restarting the timer when sending a packet makes sure connections don't get closed right after new activity starts โœจ

To avoid excessively small idle timeout periods, endpoints MUST increase the idle timeout period to be at least three times the current Probe Timeout (PTO) ๐Ÿ™…โ€โ™€๏ธ This allows for multiple PTOs to expire, and therefore multiple probes to be sent and lost, before the idle timeout hits ๐ŸŒธ

10.1.1. Liveness Testing ๐Ÿ’–

okay so if an endpoint sends packets right up close to the effective timeout, they might get tossed at the peer ๐Ÿ˜ญ because the peer's idle timeout could already have expired before those packets show up. bestie that's giving "arrived to the party after it ended" ๐Ÿ’€

an endpoint can send a PING or another ack-eliciting frame to check whether the connection is still alive if the peer could time out soon, like within a PTO (see Section 6.2 of [QUIC-RECOVERY]) โœจ. this is especially useful if any available application data can't be safely retried ๐Ÿซถ. and the application is the one that decides what data is safe to retry, not QUIC!! ๐Ÿ’…

10.1.2. Deferring Idle Timeout ๐Ÿฆ‹

ok so sometimes an endpoint needs to send ack-eliciting packets to avoid an idle timeout, like when it's waiting on response data but doesn't have application data to send, or can't send any ๐Ÿฅบ

an implementation of QUIC might give applications an option to defer an idle timeout ๐ŸŽ€. this is clutch when the app wants to hold onto state tied to an open connection but doesn't expect to exchange application data for a while. with that option, an endpoint could send a PING frame (Section 19.2) periodically, which makes the peer restart its idle timeout period. sending a packet containing a PING frame also restarts the idle timeout for this endpoint if it's the first ack-eliciting packet sent since receiving a packet. and sending a PING frame makes the peer respond with an acknowledgment, which also restarts the idle timeout for the endpoint โœจ.

Application protocols that use QUIC SHOULD provide guidance on when deferring an idle timeout is appropriate. unnecessary sending of PING frames could have a detrimental effect on performance ๐Ÿ™…

a connection will time out if no packets are sent or received for a period longer than the time negotiated using the max_idle_timeout transport parameter (see Section 10). however, state in middleboxes might time out earlier than that ๐Ÿ‘€ though REQ-5 in [RFC4787] recommends a 2-minute timeout interval, experience shows that sending packets every 30 seconds is necessary to prevent the majority of middleboxes from losing state for UDP flows [GATEWAY] ๐ŸŒธ.


10.2. Immediate Close ๐Ÿšจ

An endpoint sends a CONNECTION_CLOSE frame (Section 19.19) to terminate the connection immediately. A CONNECTION_CLOSE frame causes all streams to immediately become closed, and open streams can be assumed to be implicitly reset ๐Ÿ’€.

After sending a CONNECTION_CLOSE frame, an endpoint immediately enters the closing state (see Section 10.2.1). After receiving a CONNECTION_CLOSE frame, endpoints enter the draining state (see Section 10.2.2).

Violations of the protocol lead to an immediate close โš ๏ธ.

An immediate close can be used after an application protocol has arranged to close a connection. this might happen after the application protocol negotiates a graceful shutdown. the application protocol can exchange the messages that both application endpoints need in order to agree the connection can be closed, and then the application requests that QUIC close the connection. when QUIC closes the connection this way, a CONNECTION_CLOSE frame carrying an application-supplied error code is used to tell the peer โœจ.

The closing and draining connection states exist to make sure connections close cleanly and that delayed or reordered packets are properly discarded ๐Ÿงน. these states SHOULD persist for at least three times the current PTO interval as defined in [QUIC-RECOVERY].

Disposing of connection state before exiting the closing or draining state could mean an endpoint generates a Stateless Reset unnecessarily when a late packet shows up ๐Ÿ˜ค. endpoints that have some other way to make sure late-arriving packets don't trigger a response, like being able to close the UDP socket, MAY end these states earlier to recover resources faster ๐Ÿƒโ€โ™€๏ธ. servers that keep an open socket for accepting new connections SHOULD NOT end the closing or draining state early ๐Ÿšช.

Once its closing or draining state ends, an endpoint SHOULD discard all connection state. the endpoint MAY send a Stateless Reset in response to any further incoming packets that belong to this connection ๐Ÿฆ‹.

10.2.1. Closing Connection State ๐ŸงŠ

An endpoint enters the closing state after initiating an immediate close.

In the closing state, an endpoint keeps only enough information to generate a packet containing a CONNECTION_CLOSE frame and to identify packets as belonging to the connection. an endpoint in the closing state sends a packet containing a CONNECTION_CLOSE frame in response to any incoming packet that it attributes to the connection ๐Ÿ“ฆ.

An endpoint SHOULD limit the rate at which it generates packets in the closing state. for example, an endpoint could wait for a progressively increasing number of received packets, or an increasing amount of time, before responding to received packets ๐Ÿข.

An endpoint's selected connection ID and the QUIC version are enough information to identify packets for a closing connection, and the endpoint MAY discard all other connection state. an endpoint that is closing is not required to process any received frame ๐Ÿ™ˆ. an endpoint MAY retain packet protection keys for incoming packets so it can read and process a CONNECTION_CLOSE frame ๐Ÿ”‘.

An endpoint MAY drop packet protection keys when entering the closing state and send a packet containing a CONNECTION_CLOSE frame in response to any UDP datagram that is received. however, an endpoint that discards packet protection keys cannot identify and discard invalid packets ๐Ÿ˜ฌ. to avoid being used for an amplification attack, such endpoints MUST limit the cumulative size of packets they send to three times the cumulative size of the packets that are received and attributed to the connection ๐Ÿ›‘. to minimize the state an endpoint maintains for a closing connection, endpoints MAY send the exact same packet in response to any received packet ๐Ÿง .

Note: Allowing retransmission of a closing packet is an exception to the requirement that a new packet number be used for each packet; see Section 12.3. Sending new packet numbers is primarily of advantage to loss recovery and congestion control, which are not expected to be relevant for a closed connection. Retransmitting the final packet requires less state. ๐Ÿค“

While in the closing state, an endpoint could receive packets from a new source address, possibly indicating a connection migration (see Section 9). An endpoint in the closing state MUST either discard packets received from an unvalidated address or limit the cumulative size of packets it sends to an unvalidated address to three times the size of packets it receives from that address ๐Ÿ”.

An endpoint is not expected to handle key updates when it is closing (Section 6 of [QUIC-TLS]). a key update might stop the endpoint from moving from the closing state to the draining state, since it won't be able to process packets received afterward, but otherwise it has no impact ๐ŸŒ€.

10.2.2. Draining Connection State ๐ŸŒ™

The draining state is entered once an endpoint receives a CONNECTION_CLOSE frame, which indicates that its peer is closing or draining. While otherwise identical to the closing state, an endpoint in the draining state MUST NOT send any packets. retaining packet protection keys is unnecessary once a connection is in the draining state ๐Ÿคซ.

An endpoint that receives a CONNECTION_CLOSE frame MAY send a single packet containing a CONNECTION_CLOSE frame before entering the draining state, using a NO_ERROR code if appropriate. An endpoint MUST NOT send further packets. doing so could result in a constant back-and-forth of CONNECTION_CLOSE frames until one of the endpoints exits the closing state ๐Ÿ“.

An endpoint MAY enter the draining state from the closing state if it receives a CONNECTION_CLOSE frame, which indicates that the peer is also closing or draining. in this case, the draining state ends when the closing state would have ended. in other words, the endpoint uses the same end time but stops transmitting any packets on this connection ๐Ÿ›Œ.

10.2.3. Immediate Close during the Handshake ๐Ÿค

When sending a CONNECTION_CLOSE frame, the goal is to make sure the peer will process the frame. generally, this means sending the frame in a packet with the highest level of packet protection so the packet doesn't get discarded ๐Ÿ›ก๏ธ. after the handshake is confirmed (see Section 4.1.2 of [QUIC-TLS]), an endpoint MUST send any CONNECTION_CLOSE frames in a 1-RTT packet. however, before the handshake is confirmed, more advanced packet protection keys might not be available to the peer, so another CONNECTION_CLOSE frame MAY be sent in a packet that uses a lower packet protection level. more specifically:

  • A client will always know whether the server has Handshake keys (see Section 17.2.2.1), but it is possible that a server does not know whether the client has Handshake keys. Under these circumstances, a server SHOULD send a CONNECTION_CLOSE frame in both Handshake and Initial packets to ensure that at least one of them is processable by the client. ๐Ÿ”
  • A client that sends a CONNECTION_CLOSE frame in a 0-RTT packet cannot be assured that the server has accepted 0-RTT. Sending a CONNECTION_CLOSE frame in an Initial packet makes it more likely that the server can receive the close signal, even if the application error code might not be received. ๐Ÿ“ฌ
  • Prior to confirming the handshake, a peer might be unable to process 1-RTT packets, so an endpoint SHOULD send a CONNECTION_CLOSE frame in both Handshake and 1-RTT packets. A server SHOULD also send a CONNECTION_CLOSE frame in an Initial packet. ๐Ÿ’Œ

Sending a CONNECTION_CLOSE of type 0x1d in an Initial or Handshake packet could expose application state or be used to alter application state. A CONNECTION_CLOSE of type 0x1d MUST be replaced by a CONNECTION_CLOSE of type 0x1c when sending the frame in Initial or Handshake packets. otherwise, information about the application state might be revealed. Endpoints MUST clear the value of the Reason Phrase field and SHOULD use the APPLICATION_ERROR code when converting to a CONNECTION_CLOSE of type 0x1c ๐Ÿ™Š.

CONNECTION_CLOSE frames sent in multiple packet types can be coalesced into a single UDP datagram (see Section 12.2) ๐Ÿ“ฆโœจ.

An endpoint can send a CONNECTION_CLOSE frame in an Initial packet. this might happen in response to unauthenticated information received in Initial or Handshake packets. such an immediate close might expose legitimate connections to a denial of service ๐Ÿ˜ฑ. QUIC does not include defensive measures for on-path attacks during the handshake (see Section 21.2). However, at the cost of reducing feedback about errors for legitimate peers, some forms of denial of service can be made more difficult for an attacker if endpoints discard illegal packets rather than terminating a connection with CONNECTION_CLOSE. for this reason, endpoints MAY discard packets rather than immediately close if errors are detected in packets that lack authentication ๐Ÿ›ก๏ธ๐Ÿ’œ.

An endpoint that has not established state, such as a server that detects an error in an Initial packet, does not enter the closing state. An endpoint that has no state for the connection does not enter a closing or draining period on sending a CONNECTION_CLOSE frame ๐Ÿซง.

10.3. Stateless Reset ๐Ÿฆ‹

okay so bestie, a stateless reset is the absolute last-resort option โœจ for an endpoint that has totally lost track of a connection's state (think: a crash or an outage ๐Ÿ’€). The peers keep yeeting data at it like nothing happened, and it's like, hello?? I don't know this connection anymore ๐Ÿ™ˆ. So an endpoint MAY send a Stateless Reset when it gets a packet it can't link to any active connection.

But ngl, it is NOT the move for flagging errors on a connection that's still alive ๐Ÿšซ. If an endpoint wants to say "this connection is cooked" (a fatal error), it MUST use a CONNECTION_CLOSE frame if it's able to ๐Ÿ’….

So how does this even work? An endpoint hands out a stateless reset token, which is a 16-byte value that's basically impossible to guess ๐Ÿ”. Later, if the peer gets a Stateless Reset (which is just a UDP datagram ending in that token ๐ŸŽ€), the peer immediately ends the connection. Iconic, no notes ๐Ÿ‘‘.

Each token belongs to one specific connection ID ๐ŸŒธ. An endpoint gives out the token by putting it in the Stateless Reset Token field of a NEW_CONNECTION_ID frame. Servers can also send a stateless_reset_token transport parameter during the handshake, which applies to the connection ID they picked then ๐Ÿ’–. Everything here is encrypted, so only the client and server know the value ๐Ÿซถ. Clients, though, can't use the stateless_reset_token transport parameter, because their transport parameters don't have confidentiality protection ๐Ÿ™…โ€โ™€๏ธ.

Also, tokens get invalidated the moment their connection ID is retired through a RETIRE_CONNECTION_ID frame (Section 19.16) ๐Ÿงš.

Here's what an endpoint that gets packets it can't process actually sends (see Section 1.3):

Stateless Reset {
  Fixed Bits (2) = 1,
  Unpredictable Bits (38..),
  Stateless Reset Token (128),
}

Figure 10: Stateless Reset ๐Ÿ“

The whole point of this design is that a Stateless Reset looks, as much as possible, exactly like a regular packet with a short header ๐Ÿฆ‹โœจ. Totally sneaky, totally iconic.

A Stateless Reset takes up an entire UDP datagram, and it starts with the first two bits of the packet header ๐Ÿ’…. The rest of the first byte, plus however many bytes come after it, SHOULD be set to values that are indistinguishable from random ๐ŸŽฒ. The last 16 bytes of the datagram hold the stateless reset token ๐Ÿ’œ.

To anyone who isn't the intended recipient, a Stateless Reset will look like a packet with a short header ๐Ÿ‘€. For it to look like a valid QUIC packet, the Unpredictable Bits field needs at least 38 bits of data (so 5 bytes, minus the two fixed bits) ๐Ÿงฎ.

That gives a minimum size of 21 bytes, but that doesn't guarantee it's hard to tell apart from other packets if the recipient needs a connection ID ๐Ÿ™ˆ. To get that, the endpoint SHOULD make sure every packet it sends is at least 22 bytes longer than the minimum connection ID length it asks the peer to use, adding PADDING frames as needed ๐ŸŒˆ. That way, any Stateless Reset the peer sends back looks just like a valid packet to the endpoint โœจ. And if an endpoint is sending a Stateless Reset in response to a packet that's 43 bytes or shorter, it SHOULD make the reset one byte shorter than that packet ๐ŸŽ€.

These numbers assume the stateless reset token is the same length as the minimum expansion of the packet protection AEAD ๐Ÿง . If the endpoint could have negotiated a packet protection scheme with a bigger minimum expansion, it needs extra unpredictable bytes ๐Ÿ’ซ.

An endpoint MUST NOT send a Stateless Reset that is three times or more larger than the packet it receives, so it can't be abused for amplification ๐Ÿšจ. Section 10.3.3 has more limits on Stateless Reset size ๐Ÿ“.

Endpoints MUST discard packets that are too small to be valid QUIC packets ๐Ÿ—‘๏ธ. For example, with the AEAD functions defined in [QUIC-TLS], short header packets smaller than 21 bytes are never valid ๐Ÿ™….

Endpoints MUST send Stateless Resets formatted as a packet with a short header ๐Ÿ’…. But endpoints MUST treat any packet ending in a valid stateless reset token as a Stateless Reset, because other QUIC versions might allow a long header ๐ŸŒธ.

An endpoint MAY send a Stateless Reset in response to a packet with a long header ๐ŸŽ€. Sending one isn't useful until the peer actually has the stateless reset token ๐Ÿ™ƒ. In this QUIC version, long header packets are only used during connection establishment โœจ. And since the token isn't available until establishment is done or nearly done, ignoring an unknown long header packet might work just as well as sending a Stateless Reset ๐Ÿงš.

An endpoint can't figure out the Source Connection ID from a short header packet, so it can't set the Destination Connection ID in the Stateless Reset ๐Ÿ˜ญ. That means the Destination Connection ID will be different from the one used in earlier packets. A random Destination Connection ID makes it look like the peer switched to a new connection ID that was provided with a NEW_CONNECTION_ID frame (see Section 19.15) ๐Ÿฆ‹.

Using a randomized connection ID causes two problems ๐Ÿ’€:

  • The packet might not reach the peer ๐Ÿ“ญ. If the Destination Connection ID is important for routing to the peer, this packet could get routed wrong. That might also trigger another Stateless Reset in response (see Section 10.3.3) ๐Ÿ”. A Stateless Reset that gets misrouted is an ineffective error detection and recovery mechanism ๐Ÿ˜ฉ. So in that case, endpoints will need to rely on other methods, like timers, to notice the connection has failed โฐ.
  • Other entities besides the peer can use the randomly generated connection ID to spot this as a potential Stateless Reset ๐Ÿ‘€. An endpoint that now and then uses different connection IDs might make that a little less certain ๐Ÿคทโ€โ™€๏ธ.

This stateless reset design is specific to QUIC version 1 ๐ŸŒˆ. An endpoint that supports multiple QUIC versions needs to generate a Stateless Reset that peers will accept for any version the endpoint might support (or might have supported before losing state) ๐Ÿซถ. Designers of new QUIC versions need to know this, and either (1) reuse this design or (2) use some part of the packet other than the last 16 bytes to carry data ๐Ÿงฉ.

10.3.1. Detecting a Stateless Reset

okay so detecting one is all about the trailing 16 bytes of the UDP datagram ๐Ÿ”. An endpoint remembers all the stateless reset tokens tied to the connection IDs and remote addresses for datagrams it recently sent ๐Ÿง . That includes Stateless Reset Token field values from NEW_CONNECTION_ID frames and the server's transport parameters, but it leaves out tokens tied to connection IDs that are unused or retired ๐Ÿ—‚๏ธ. To spot a Stateless Reset, the endpoint compares the last 16 bytes of the datagram with all the stateless reset tokens tied to the remote address the datagram came from ๐ŸŽฏ.

This comparison can happen for every inbound datagram ๐Ÿ“ฌ. Endpoints MAY skip the check if any packet in a datagram is processed successfully ๐Ÿ‘. But the comparison MUST be done when the first packet in an incoming datagram either can't be linked to a connection or can't be decrypted ๐Ÿšจ.

An endpoint MUST NOT check for stateless reset tokens tied to connection IDs it hasn't used, or to connection IDs that have been retired ๐Ÿ™…โ€โ™€๏ธ.

When comparing a datagram to stateless reset token values, endpoints MUST do it without leaking info about the token's value ๐Ÿคซ. For example, doing the comparison in constant time protects individual tokens from timing side channels โฑ๏ธ. Another approach is to store and compare transformed versions of the tokens instead of the raw values, where the transformation is a cryptographically secure pseudorandom function using a secret key (for example, a block cipher or a Hashed Message Authentication Code (HMAC) [RFC2104]) ๐Ÿ”. An endpoint isn't expected to protect info about whether a packet was successfully decrypted, or how many valid stateless reset tokens there are ๐Ÿคทโ€โ™€๏ธ.

If the last 16 bytes of the datagram are identical in value to a stateless reset token, the endpoint MUST enter the draining period and MUST NOT send any further packets on this connection ๐Ÿ›‘. Not me, the drama ๐Ÿ’€.

10.3.2. Calculating a Stateless Reset Token

okay this part is giving cryptography main character energy ๐Ÿง™โ€โ™€๏ธ. The stateless reset token MUST be difficult to guess ๐Ÿ”. An endpoint could randomly generate [RANDOM] a secret for every connection it creates ๐ŸŽฒ. But that causes a coordination problem when there are multiple instances in a cluster, or a storage problem for an endpoint that might lose state ๐Ÿ˜ต. Stateless reset exists specifically for the case where state is lost, so this approach is suboptimal ๐Ÿ™ƒ.

A single static key can be used across all connections to the same endpoint ๐Ÿ—๏ธ. You generate the proof with a pseudorandom function that takes the static key and the connection ID the endpoint chose (see Section 5.1) as input ๐ŸŒธ. An endpoint could use HMAC [RFC2104] (for example, HMAC(static_key, connection_id)), or the HMAC-based Key Derivation Function (HKDF) [RFC5869] (for example, using the static key as input keying material, with the connection ID as salt) ๐Ÿง‚. The output gets truncated to 16 bytes to make the stateless reset token for that connection ๐Ÿ’–.

An endpoint that has lost state can use the same method to make a valid stateless reset token again โ™ป๏ธ. The connection ID comes from the packet the endpoint receives ๐Ÿ“ฆ.

This design relies on the peer always sending a connection ID in its packets, so the endpoint can use that connection ID to reset the connection ๐Ÿ”. An endpoint using this design MUST either use the same connection ID length for all connections, or encode the length of the connection ID so it can be recovered without state ๐Ÿ“. Also, it can't provide a zero-length connection ID ๐Ÿšซ.

Revealing the stateless reset token lets anyone terminate the connection, so each value can only be used once ๐Ÿ’ฅ. This method of choosing the token means the combination of connection ID and static key MUST NOT be used for another connection ๐Ÿ™…. A denial-of-service attack is possible if the same connection ID is used by instances that share a static key, or if an attacker can get a packet routed to an instance that has no state but has the same static key (see Section 21.11) ๐Ÿ˜ฑ. A connection ID from a connection that was reset by revealing the stateless reset token MUST NOT be reused for new connections at nodes that share a static key ๐Ÿšง.

The same stateless reset token MUST NOT be used for multiple connection IDs ๐Ÿ’…. Endpoints don't have to compare new values against every previous value, but a duplicate value MAY be treated as a connection error of type PROTOCOL_VIOLATION โš ๏ธ.

Note that Stateless Resets do not have any cryptographic protection ๐Ÿฅบ.


10.3.3. Looping

Okay bestie, this part is lowkey dramatic ๐Ÿ’€. A Stateless Reset is built so that, unless you know the stateless reset token, it looks exactly like a valid packet ๐Ÿ™ˆ. So if a server sends one to another server, that server might send one right back, and now they're stuck in an infinite reply loop ๐Ÿ˜ญ not it.

An endpoint MUST ensure that every Stateless Reset it sends is smaller than the packet that triggered it, unless it keeps enough state to prevent looping. If a loop does happen, the packets eventually get so tiny that they can't trigger a response anymore ๐Ÿคโœจ the way that resolves itself is iconic.

An endpoint can keep count of how many Stateless Resets it has sent and stop making new ones once it hits a limit ๐Ÿ›‘ Using separate limits for different remote addresses means Stateless Resets can still be used to close connections when other peers or connections have run out of their limits. That's main character energy for sure ๐Ÿ’…

A Stateless Reset smaller than 41 bytes might be identifiable as one by an observer, depending on how long the peer's connection IDs are ๐Ÿ‘€ On the flip side, if you don't send a Stateless Reset in response to a small packet, Stateless Resets might not be useful for spotting broken connections where only very tiny packets get sent ๐Ÿ’” Those failures might only get caught some other way, like with timers โฐ


11. Error Handling โœจ

An endpoint that detects an error SHOULD signal that error to its peer ๐Ÿ“ฃ Both transport-level and application-level errors can affect a whole connection; see Section 11.1. Only application-level errors can be isolated to a single stream; see Section 11.2.

The most appropriate error code (Section 20) SHOULD be included in the frame that signals the error ๐ŸŽ€ This spec tells you which error code to use for each error condition. Even though those are worded as requirements, different implementations might end up reporting different errors ๐Ÿคทโ€โ™€๏ธ An endpoint MAY use any applicable error code when it detects an error condition ๐Ÿ’– A generic error code, like PROTOCOL_VIOLATION or INTERNAL_ERROR, can always be used in place of a specific one, so no panic ๐Ÿซถ

A stateless reset (Section 10.3) is not suitable for any error that can be signaled with a CONNECTION_CLOSE or RESET_STREAM frame ๐Ÿ™…โ€โ™€๏ธ An endpoint that has the state it needs to send a frame on the connection MUST NOT use a stateless reset. Rule of the group chat: use the proper tool ๐Ÿงš


11.1. Connection Errors

Errors that make the connection unusable, like an obvious violation of protocol semantics or corrupted state that affects the whole connection, MUST be signaled using a CONNECTION_CLOSE frame (Section 19.19) ๐Ÿ’€ no exceptions, bestie.

Application-specific protocol errors get signaled with a CONNECTION_CLOSE frame whose frame type is 0x1d ๐ŸŒธ Errors specific to the transport, including every one described in this document, are carried in a CONNECTION_CLOSE frame with a frame type of 0x1c ๐Ÿ“

A CONNECTION_CLOSE frame could get sent in a packet that gets lost ๐Ÿ˜ฉ An endpoint SHOULD be ready to retransmit a packet containing a CONNECTION_CLOSE frame if it keeps receiving packets on a terminated connection. Limiting how many retransmissions happen, and how long this final packet keeps getting sent, keeps the effort spent on dead connections in check ๐Ÿฆ‹

An endpoint that decides not to retransmit packets containing a CONNECTION_CLOSE frame risks its peer missing that first packet ๐Ÿ˜ฌ The only option left for an endpoint still getting data for a terminated connection is to try the stateless reset process (Section 10.3) ๐Ÿซ 

As the AEAD for Initial packets doesn't provide strong authentication, an endpoint MAY discard an invalid Initial packet ๐Ÿ—‘๏ธ Discarding an Initial packet is allowed even where this spec would otherwise call for a connection error โœจ An endpoint can only discard a packet if it doesn't process the frames in it, or if it reverts the effects of any processing it already did. Discarding invalid Initial packets might help reduce exposure to denial of service; see Section 21.2 ๐Ÿ›ก๏ธ


11.2. Stream Errors

If an application-level error hits a single stream but otherwise leaves the connection in a recoverable state, the endpoint can send a RESET_STREAM frame (Section 19.4) with an appropriate error code to end just that one affected stream ๐ŸŽฏ

Resetting a stream without the application protocol being involved could leave the application protocol in an unrecoverable state ๐Ÿ˜ฑ RESET_STREAM MUST only be triggered by the application protocol that uses QUIC ๐Ÿ”

The semantics of the application error code inside RESET_STREAM are defined by the application protocol ๐Ÿ’œ Only the application protocol can cause a stream to be terminated. A local instance of the application protocol does it with a direct API call, and a remote instance does it by sending a STOP_SENDING frame, which automatically triggers a RESET_STREAM ๐Ÿฆ‹

Application protocols SHOULD define rules for handling streams that get prematurely canceled by either endpoint ๐Ÿ“œ plan ahead, bestie ๐Ÿ’…


12. Packets and Frames โœจ

QUIC endpoints communicate by swapping packets ๐Ÿ“ฆ Packets have confidentiality and integrity protection; see Section 12.1. Packets are carried in UDP datagrams; see Section 12.2.

This version of QUIC uses the long packet header during connection establishment; see Section 17.2. Packets with the long header are Initial (Section 17.2.2), 0-RTT (Section 17.2.3), Handshake (Section 17.2.4), and Retry (Section 17.2.5) ๐ŸŒˆ Version negotiation uses a version-independent packet with a long header; see Section 17.2.1 ๐Ÿงš

Packets with the short header are built for minimal overhead ๐Ÿƒ They get used after a connection is established and 1-RTT keys are available; see Section 17.3 โœจ


12.1. Protected Packets

QUIC packets have different levels of cryptographic protection depending on their type ๐Ÿ” Details of packet protection live in [QUIC-TLS]. This section gives an overview of the protections that are provided ๐ŸŽ€

Version Negotiation packets have no cryptographic protection; see [QUIC-INVARIANTS] ๐Ÿ™ˆ

Retry packets use an AEAD function [AEAD] to protect against accidental modification ๐Ÿ›ก๏ธ

Initial packets use an AEAD function, and the keys for it are derived from a value that is visible on the wire ๐Ÿ‘€ So Initial packets don't have effective confidentiality protection. Initial protection exists to make sure the sender of the packet is really on the network path ๐Ÿ›ค๏ธ Any entity that receives an Initial packet from a client can recover the keys that let them read the packet's contents and generate Initial packets that will be successfully authenticated at either endpoint ๐Ÿ˜ณ The AEAD also protects Initial packets against accidental modification ๐Ÿ”’

All other packets are protected with keys derived from the cryptographic handshake ๐Ÿค The handshake makes sure only the communicating endpoints get the matching keys for Handshake, 0-RTT, and 1-RTT packets. Packets protected with 0-RTT and 1-RTT keys have strong confidentiality and integrity protection ๐Ÿ’ช

The Packet Number field that shows up in some packet types has alternative confidentiality protection, applied as part of header protection; see Section 5.4 of [QUIC-TLS] for details ๐ŸŽญ The underlying packet number goes up with each packet sent in a given packet number space; see Section 12.3 for details ๐Ÿ”ข


12.2. Coalescing Packets

Initial (Section 17.2.2), 0-RTT (Section 17.2.3), and Handshake (Section 17.2.4) packets contain a Length field that marks where the packet ends ๐Ÿ“ The length covers both the Packet Number and Payload fields. Both of those are confidentiality protected and start out of unknown length, and the Payload length only gets learned once header protection is removed ๐Ÿ™ƒ

Using the Length field, a sender can coalesce multiple QUIC packets into one UDP datagram ๐Ÿงฉ That can cut down the number of UDP datagrams needed to finish the cryptographic handshake and start sending data. It can also be used to construct Path Maximum Transmission Unit (PMTU) probes; see Section 14.4.1 ๐Ÿ“ Receivers MUST be able to process coalesced packets ๐Ÿ’–

Coalescing packets in order of increasing encryption levels (Initial, 0-RTT, Handshake, 1-RTT; see Section 4.1.4 of [QUIC-TLS]) makes it more likely the receiver can process all the packets in a single pass ๐Ÿƒโ€โ™€๏ธ A packet with a short header doesn't include a length, so it can only be the last packet in a UDP datagram ๐Ÿ”š An endpoint SHOULD include multiple frames in a single packet when they're going to be sent at the same encryption level, instead of coalescing multiple packets at that same level ๐Ÿงบ

Receivers MAY route based on the information in the first packet contained in a UDP datagram ๐Ÿ—บ๏ธ Senders MUST NOT coalesce QUIC packets with different connection IDs into a single UDP datagram ๐Ÿšซ Receivers SHOULD ignore any later packets whose Destination Connection ID differs from the first packet in the datagram ๐Ÿ™…โ€โ™€๏ธ

Every QUIC packet coalesced into a single UDP datagram is separate and complete ๐Ÿ’… The receiver of coalesced QUIC packets MUST individually process each QUIC packet and separately acknowledge them, just as if they had arrived as the payloads of different UDP datagrams ๐Ÿ“ฌ For example, if decryption fails (because the keys aren't available or for any other reason), the receiver MAY either discard or buffer the packet for later processing, and MUST attempt to process the remaining packets ๐Ÿซถ

Retry packets (Section 17.2.5), Version Negotiation packets (Section 17.2.1), and packets with a short header (Section 17.3) don't contain a Length field, so they can't be followed by other packets in the same UDP datagram ๐Ÿ™… Also, there's no situation where a Retry or Version Negotiation packet gets coalesced with another packet ๐Ÿ’€

12.3. Packet Numbers ๐Ÿ”ขโœจ

Okay bestie, so a packet number is an integer somewhere in the range 0 to 2^62-1 ๐Ÿงฎ๐Ÿ’–. It's used to work out the cryptographic nonce for packet protection ๐Ÿ”. Each endpoint keeps its own separate packet number for sending and for receiving ๐Ÿฆ‹.

Packet numbers are capped at that range because they have to fit whole in the Largest Acknowledged field of an ACK frame (Section 19.3) ๐Ÿ’…. When a long or short header carries one, though, the packet number gets shrunk and encoded in 1 to 4 bytes, so see Section 17.1 for the deets ๐ŸŒธ.

Version Negotiation (Section 17.2.1) and Retry (Section 17.2.5) packets don't include a packet number at all ๐Ÿ™ˆ.

Packet numbers are split into three spaces in QUIC ๐Ÿงš:

  • Initial space: All Initial packets (Section 17.2.2) live in this space.
  • Handshake space: All Handshake packets (Section 17.2.4) live in this space.
  • Application data space: All 0-RTT (Section 17.2.3) and 1-RTT (Section 17.3.1) packets live in this space.

As described in [QUIC-TLS], each packet type uses different protection keys ๐Ÿ”‘.

Think of a packet number space as the little room where a packet gets processed and acknowledged ๐Ÿ . Initial packets can only be sent with Initial packet protection keys, and they only get acknowledged in packets that are also Initial packets. Handshake packets go out at the Handshake encryption level and only get acknowledged in Handshake packets too ๐Ÿซถ.

This keeps the data in each space cryptographically separated, which is so iconic ๐Ÿ‘‘. Packet numbers in each space start at packet number 0. Subsequent packets sent in the same packet number space MUST increase the packet number by at least one โœจ.

0-RTT and 1-RTT data share the same packet number space, which makes loss recovery algorithms way easier to build between those two packet types ๐Ÿง .

A QUIC endpoint MUST NOT reuse a packet number within the same packet number space in one connection ๐Ÿšซ. If the packet number for sending hits 2^62-1, the sender MUST close the connection without sending a CONNECTION_CLOSE frame or any further packets ๐Ÿ˜ญ. An endpoint MAY send a Stateless Reset (Section 10.3) in response to any further packets it receives ๐Ÿฆ‹.

A receiver MUST discard a newly unprotected packet unless it is certain it hasn't already processed another packet with the same packet number from the same packet number space ๐Ÿง. Duplicate suppression MUST happen after removing packet protection, for the reasons described in Section 9.5 of [QUIC-TLS] ๐Ÿ”๐Ÿ’œ.

Endpoints that track every single individual packet to catch duplicates can end up hoarding way too much state ๐Ÿ’€. The data needed for duplicate detection can be trimmed down by keeping a minimum packet number, and anything below it gets dropped right away ๐Ÿ—‘๏ธ. Any minimum has to account for big swings in round-trip time, including the possibility that a peer probes network paths with much larger round-trip times, so see Section 9 ๐ŸŒˆ.

Packet number encoding at a sender and decoding at a receiver are covered in Section 17.1 ๐ŸŽ€.

12.4. Frames and Frame Types ๐ŸŽ

The payload of QUIC packets, after packet protection is removed, is a sequence of complete frames, as shown in Figure 11 ๐Ÿ“. Version Negotiation, Stateless Reset, and Retry packets don't contain frames at all ๐Ÿ™…โ€โ™€๏ธ.

Packet Payload {
  Frame (8..) ...,
}

                     Figure 11: QUIC Payload

The payload of a packet that contains frames MUST contain at least one frame ๐Ÿซ . It MAY contain multiple frames and multiple frame types too ๐ŸŒธ. An endpoint MUST treat receipt of a packet containing no frames as a connection error of type PROTOCOL_VIOLATION ๐Ÿšจ. Frames always fit within a single QUIC packet and can't span multiple packets, no exceptions ๐Ÿ’….

Each frame starts with a Frame Type that tells you what it is, followed by whatever type-dependent fields it needs ๐Ÿ“ฆ:

Frame {
  Frame Type (i),
  Type-Dependent Fields (..),
}

                 Figure 12: Generic Frame Layout

Table 3 lists and summarizes info about every frame type defined in this spec ๐Ÿ“‹โœจ. There's a summary of what it all means right after the table ๐Ÿ’–.

+============+======================+===============+======+======+
| Type Value | Frame Type Name      | Definition    | Pkts | Spec |
+============+======================+===============+======+======+
| 0x00       | PADDING              | Section 19.1  | IH01 | NP   |
+------------+----------------------+---------------+------+------+
| 0x01       | PING                 | Section 19.2  | IH01 |      |
+------------+----------------------+---------------+------+------+
| 0x02-0x03  | ACK                  | Section 19.3  | IH_1 | NC   |
+------------+----------------------+---------------+------+------+
| 0x04       | RESET_STREAM         | Section 19.4  | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x05       | STOP_SENDING         | Section 19.5  | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x06       | CRYPTO               | Section 19.6  | IH_1 |      |
+------------+----------------------+---------------+------+------+
| 0x07       | NEW_TOKEN            | Section 19.7  | ___1 |      |
+------------+----------------------+---------------+------+------+
| 0x08-0x0f  | STREAM               | Section 19.8  | __01 | F    |
+------------+----------------------+---------------+------+------+
| 0x10       | MAX_DATA             | Section 19.9  | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x11       | MAX_STREAM_DATA      | Section 19.10 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x12-0x13  | MAX_STREAMS          | Section 19.11 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x14       | DATA_BLOCKED         | Section 19.12 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x15       | STREAM_DATA_BLOCKED  | Section 19.13 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x16-0x17  | STREAMS_BLOCKED      | Section 19.14 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x18       | NEW_CONNECTION_ID    | Section 19.15 | __01 | P    |
+------------+----------------------+---------------+------+------+
| 0x19       | RETIRE_CONNECTION_ID | Section 19.16 | __01 |      |
+------------+----------------------+---------------+------+------+
| 0x1a       | PATH_CHALLENGE       | Section 19.17 | __01 | P    |
+------------+----------------------+---------------+------+------+
| 0x1b       | PATH_RESPONSE        | Section 19.18 | ___1 | P    |
+------------+----------------------+---------------+------+------+
| 0x1c-0x1d  | CONNECTION_CLOSE     | Section 19.19 | ih01 | N    |
+------------+----------------------+---------------+------+------+
| 0x1e       | HANDSHAKE_DONE       | Section 19.20 | ___1 |      |
+------------+----------------------+---------------+------+------+

                        Table 3: Frame Types

The full format and semantics of each frame type are in Section 19 ๐Ÿ“–. The rest of this section is a quick rundown of the important general stuff ๐Ÿ’œ.

The Frame Type in ACK, STREAM, MAX_STREAMS, STREAMS_BLOCKED, and CONNECTION_CLOSE frames is used to carry other frame-specific flags ๐Ÿšฉ. For every other frame, the Frame Type field just identifies the frame ๐Ÿท๏ธ.

The "Pkts" column in Table 3 lists which packet types each frame type could show up in, using these little characters ๐Ÿ”ค:

  • I: Initial (Section 17.2.2)
  • H: Handshake (Section 17.2.4)
  • 0: 0-RTT (Section 17.2.3)
  • 1: 1-RTT (Section 17.3.1)
  • ih: Only a CONNECTION_CLOSE frame of type 0x1c can appear in Initial or Handshake packets.

For more on these restrictions, see Section 12.5 ๐Ÿ™Œ. Note that all frames can appear in 1-RTT packets ๐ŸŒˆ. An endpoint MUST treat receipt of a frame in a packet type that isn't permitted as a connection error of type PROTOCOL_VIOLATION ๐Ÿ˜ค.

The "Spec" column in Table 3 summarizes any special rules for processing or generating each frame type, using these characters ๐Ÿช„:

  • N: Packets containing only frames with this marking are not ack-eliciting; see Section 13.2 ๐Ÿ™Š.
  • C: Packets containing only frames with this marking do not count toward bytes in flight for congestion control purposes; see [QUIC-RECOVERY] ๐Ÿšฆ.
  • P: Packets containing only frames with this marking can be used to probe new network paths during connection migration; see Section 9.1 ๐Ÿ›ค๏ธ.
  • F: The contents of frames with this marking are flow controlled; see Section 4 ๐Ÿšฐ.

The "Pkts" and "Spec" columns in Table 3 aren't part of the IANA registry, so see Section 22.4 if you're curious ๐Ÿ“‘.

An endpoint MUST treat the receipt of a frame of unknown type as a connection error of type FRAME_ENCODING_ERROR ๐Ÿšซ.

All frames are idempotent in this version of QUIC, which means a valid frame doesn't cause undesirable side effects or errors when it's received more than once ๐Ÿ”๐Ÿ’–.

The Frame Type field uses a variable-length integer encoding (see Section 16), with one exception ๐ŸŽ€. To keep frame parsing simple and efficient, a frame type MUST use the shortest possible encoding โœ‚๏ธ. For frame types defined in this document, that means a single-byte encoding, even though you could technically encode these values as a two-, four-, or eight-byte variable-length integer ๐Ÿคฏ. For instance, 0x4001 is a legit two-byte encoding for the value 1, but PING frames are always encoded as a single byte with the value 0x01 ๐Ÿ“. This rule applies to all current and future QUIC frame types ๐Ÿ”ฎ. An endpoint MAY treat the receipt of a frame type that uses a longer encoding than necessary as a connection error of type PROTOCOL_VIOLATION ๐Ÿ™ƒ.

12.5. Frames and Number Spaces ๐Ÿ—‚๏ธ

Some frames are prohibited in certain packet number spaces ๐Ÿšง. These rules generalize the ones from TLS: frames tied to establishing the connection can usually show up in packets in any packet number space, while frames tied to transferring data can only appear in the application data packet number space ๐Ÿ’…:

  • PADDING, PING, and CRYPTO frames MAY appear in any packet number space ๐Ÿง.
  • CONNECTION_CLOSE frames signaling errors at the QUIC layer (type 0x1c) MAY appear in any packet number space โš ๏ธ. CONNECTION_CLOSE frames signaling application errors (type 0x1d) MUST only appear in the application data packet number space ๐ŸŽฏ.
  • ACK frames MAY appear in any packet number space, but they can only acknowledge packets that showed up in that same packet number space ๐Ÿงพ. However, as noted below, 0-RTT packets can't contain ACK frames ๐Ÿ™…โ€โ™€๏ธ.
  • All other frame types MUST only be sent in the application data packet number space ๐Ÿก.

Note that it isn't possible to send the following frames in 0-RTT packets, for various reasons: ACK, CRYPTO, HANDSHAKE_DONE, NEW_TOKEN, PATH_RESPONSE, and RETIRE_CONNECTION_ID ๐Ÿ˜ญ. A server MAY treat receipt of these frames in 0-RTT packets as a connection error of type PROTOCOL_VIOLATION ๐Ÿ’œ.

13. Packetization and Reliability โœจ

okay bestie, so a sender puts one or more frames inside a QUIC packet, see Section 12.4 for the full frame lineup ๐Ÿ’…

a sender can keep its per-packet bandwidth and computational costs low by stuffing as many frames as possible into each QUIC packet ๐Ÿ“ฆ it MAY wait a little while to collect more frames before sending a packet that isn't maximally packed, so it doesn't spam out a bunch of tiny packets ๐Ÿซถ an implementation MAY use what it knows about how the application sends data, or some heuristics, to decide whether and how long to wait ๐Ÿง  the waiting period is up to the implementation, but it should be careful to delay conservatively, because any delay is likely to increase latency the application can see ๐Ÿ˜ญ

stream multiplexing happens when STREAM frames from multiple streams get interleaved into one or more QUIC packets ๐Ÿฆ‹ a single QUIC packet can carry multiple STREAM frames, from one stream or from several streams ๐ŸŒˆ

one of the best things about QUIC is that it avoids head-of-line blocking across multiple streams ๐Ÿ‘‘ when a packet gets lost, only the streams that had data in that packet are stuck waiting for a retransmission, and the other streams can keep on making progress ๐Ÿƒโ€โ™€๏ธ just be aware that when data from multiple streams rides in one QUIC packet, losing that packet blocks all of those streams from progressing ๐Ÿ’€ implementations are advised to include as few streams as necessary in outgoing packets, without giving up transmission efficiency by sending underfilled packets ๐Ÿ“


13.1. Packet Processing

a packet MUST NOT be acknowledged until packet protection has been successfully removed and all the frames inside it have been processed ๐Ÿ” for STREAM frames, "processed" means the data has been queued up so the application protocol can receive it, but it does not require that the data actually be delivered and consumed โœจ

once the packet is fully processed, the receiver acknowledges it by sending one or more ACK frames containing the packet number of the received packet ๐Ÿ’Œ

an endpoint SHOULD treat receiving an acknowledgment for a packet it never sent as a connection error of type PROTOCOL_VIOLATION, if it's able to detect that condition ๐Ÿšจ for more on how that might be pulled off, see Section 21.4 ๐Ÿง


13.2. Generating Acknowledgments

endpoints acknowledge all the packets they receive and process ๐Ÿซก however, only ack-eliciting packets cause an ACK frame to be sent within the maximum ack delay ๐ŸŽ€ packets that aren't ack-eliciting only get acknowledged when an ACK frame gets sent for some other reason ๐ŸŒธ

when an endpoint sends a packet for any reason, it SHOULD try to include an ACK frame if one hasn't been sent recently ๐Ÿ’– this helps the peer detect loss in a timely way ๐Ÿ”

generally speaking, frequent feedback from a receiver improves loss and congestion response ๐Ÿ“ˆ but that has to be balanced against the excessive load a receiver creates if it sends an ACK frame in response to every single ack-eliciting packet ๐Ÿ˜ตโ€๐Ÿ’ซ the guidance below is designed to strike that balance โš–๏ธ


13.2.1. Sending ACK Frames

every packet SHOULD be acknowledged at least once, and ack-eliciting packets MUST be acknowledged at least once within the maximum delay an endpoint advertised using the max_ack_delay transport parameter; see Section 18.2 ๐Ÿ•ฐ๏ธ max_ack_delay is basically an explicit contract: an endpoint promises to never intentionally delay acknowledgments of an ack-eliciting packet by more than the value it indicated ๐Ÿค if it does break that promise, any excess accrues to the RTT estimate and could cause spurious or delayed retransmissions from the peer ๐Ÿ˜ฌ a sender uses the receiver's max_ack_delay value when it sets timeouts for timer-based retransmission, as detailed in Section 6.2 of [QUIC-RECOVERY] ๐Ÿงฎ

an endpoint MUST acknowledge all ack-eliciting Initial and Handshake packets immediately, and all ack-eliciting 0-RTT and 1-RTT packets within its advertised max_ack_delay, with one exception ๐Ÿซถ before the handshake is confirmed, an endpoint might not have the packet protection keys it needs to decrypt Handshake, 0-RTT, or 1-RTT packets when they arrive ๐Ÿ”‘ so it might buffer them and acknowledge them once the needed keys show up ๐ŸŽ

since packets containing only ACK frames aren't congestion controlled, an endpoint MUST NOT send more than one such packet in response to receiving an ack-eliciting packet ๐Ÿšซ

an endpoint MUST NOT send a non-ack-eliciting packet in response to a non-ack-eliciting packet, even if there are packet gaps that come before the received packet ๐Ÿ™…โ€โ™€๏ธ this stops an infinite feedback loop of acknowledgments, which could keep the connection from ever going idle ๐Ÿ˜ด non-ack-eliciting packets get acknowledged eventually, when the endpoint sends an ACK frame in response to other events โณ

an endpoint that's only sending ACK frames won't receive acknowledgments from its peer unless those acknowledgments are included in packets carrying ack-eliciting frames ๐Ÿฅบ so an endpoint SHOULD send an ACK frame along with other frames when there are new ack-eliciting packets to acknowledge ๐ŸŽˆ when only non-ack-eliciting packets need acknowledging, an endpoint MAY choose not to send an ACK frame with its outgoing frames until it has received an ack-eliciting packet ๐ŸŒ™

an endpoint that's only sending non-ack-eliciting packets might occasionally add an ack-eliciting frame to those packets, so that it gets an acknowledgment; see Section 13.2.4 ๐Ÿช„ in that case, an endpoint MUST NOT send an ack-eliciting frame in all the packets that would otherwise be non-ack-eliciting, to avoid an infinite feedback loop of acknowledgments ๐Ÿ”

to help loss detection at the sender, an endpoint SHOULD generate and send an ACK frame without delay when it receives an ack-eliciting packet, either:

  • when the received packet has a packet number less than another ack-eliciting packet that has already been received, or
  • when the packet has a packet number larger than the highest-numbered ack-eliciting packet that has been received, and there are missing packets between that packet and this one ๐Ÿ•ต๏ธโ€โ™€๏ธ

similarly, packets marked with the ECN Congestion Experienced (CE) codepoint in the IP header SHOULD be acknowledged immediately, so the peer can respond to congestion events faster ๐Ÿš€

the algorithms in [QUIC-RECOVERY] are expected to be resilient to receivers that don't follow the guidance above ๐Ÿ’ช still, an implementation should only deviate from these requirements after carefully considering the performance implications of the change, both for its own connections and for other users of the network ๐ŸŒ


13.2.2. Acknowledgment Frequency

a receiver decides how often to send acknowledgments in response to ack-eliciting packets ๐Ÿค” this decision is a trade-off, babe ๐Ÿ’…

endpoints rely on timely acknowledgment to detect loss; see Section 6 of [QUIC-RECOVERY] โฐ window-based congestion controllers, like the one described in Section 7 of [QUIC-RECOVERY], rely on acknowledgments to manage their congestion window ๐ŸชŸ in both cases, delaying acknowledgments can hurt performance ๐Ÿ“‰

on the other hand, reducing how often packets carry only acknowledgments cuts down the packet transmission and processing cost at both endpoints ๐Ÿ’ธ it can improve connection throughput on severely asymmetric links and reduce the volume of acknowledgment traffic using return path capacity; see Section 3 of [RFC3449] ๐Ÿ›ฃ๏ธ

a receiver SHOULD send an ACK frame after receiving at least two ack-eliciting packets ๐Ÿ‘ฏโ€โ™€๏ธ this recommendation is general in nature and consistent with the recommendations for TCP endpoint behavior [RFC5681] ๐Ÿค knowledge of network conditions, knowledge of the peer's congestion controller, or further research and experimentation might suggest alternative acknowledgment strategies with better performance characteristics ๐Ÿ”ฌ

a receiver MAY process multiple available packets before deciding whether to send an ACK frame in response ๐Ÿ“ฌ


13.2.3. Managing ACK Ranges

when an ACK frame is sent, it includes one or more ranges of acknowledged packets ๐Ÿ“‹ including acknowledgments for older packets reduces the chance of spurious retransmissions caused by losing previously sent ACK frames, but the cost is larger ACK frames ๐Ÿฐ

ACK frames SHOULD always acknowledge the most recently received packets ๐Ÿ†• and the more out of order the packets are, the more important it is to send an updated ACK frame quickly, so the peer doesn't declare a packet lost and spuriously retransmit the frames it contained ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ an ACK frame is expected to fit within a single QUIC packet ๐Ÿ“ฆ if it doesn't, the older ranges (the ones with the smallest packet numbers) get omitted ๐Ÿ‘‹

a receiver limits the number of ACK Ranges (Section 19.3.1) it remembers and sends in ACK frames ๐Ÿง  this keeps ACK frames small and avoids resource exhaustion ๐Ÿ›ก๏ธ after receiving acknowledgments for an ACK frame, the receiver SHOULD stop tracking those acknowledged ACK Ranges ๐Ÿ—‘๏ธ senders can expect acknowledgments for most packets, but QUIC doesn't guarantee that an acknowledgment will arrive for every packet the receiver processes ๐Ÿซ 

retaining lots of ACK Ranges could make an ACK frame too large ๐Ÿ˜ต a receiver can discard unacknowledged ACK Ranges to limit ACK frame size, but the cost is more retransmissions from the sender ๐Ÿ’ธ this is necessary if an ACK frame would be too large to fit in a packet ๐Ÿ“ receivers MAY also limit ACK frame size further, to preserve space for other frames or to limit the capacity that acknowledgments consume ๐ŸŽ€

a receiver MUST retain an ACK Range unless it can ensure that it won't subsequently accept packets with numbers in that range ๐Ÿ”’ maintaining a minimum packet number that goes up as ranges get discarded is one way to pull that off with minimal state โœจ

receivers can discard all ACK Ranges, but they MUST retain the largest packet number that has been successfully processed, because that's what gets used to recover packet numbers from subsequent packets; see Section 17.1 ๐Ÿ”ข

a receiver SHOULD include an ACK Range containing the largest received packet number in every ACK frame ๐Ÿ‘‘ the Largest Acknowledged field is used in ECN validation at a sender, and including a lower value than what was in a previous ACK frame could cause ECN to be unnecessarily disabled; see Section 13.4.2 ๐Ÿ™ˆ

Section 13.2.4 describes an exemplary approach for deciding which packets to acknowledge in each ACK frame ๐Ÿงฉ the goal of that algorithm is to generate an acknowledgment for every processed packet, but acknowledgments can still get lost ๐Ÿซถ

13.2.4. Limiting Ranges by Tracking ACK Frames ๐ŸŽ€

okay so bestie, here's the trick ๐Ÿงš when a packet carrying an ACK frame gets sent, you can save its Largest Acknowledged field. then, once THAT packet is acknowledged, the receiver can stop acknowledging anything less than or equal to that saved value. it's giving tidy housekeeping ๐Ÿ’…โœจ

ngl, there's a catch ๐Ÿ™ˆ a receiver that only ever sends non-ack-eliciting packets (like ACK frames) might not hear back for a really long time. that means it has to hang onto state for a bunch of ACK frames for ages, and those ACK frames can end up hella big ๐Ÿ’€ so the fix is to occasionally send a PING or some other small ack-eliciting frame, like once per round trip, to get an ACK back from the peer ๐Ÿซถ

the vibe check on reordering: without ACK frame loss, this algorithm gives you a minimum of 1 RTT of reordering tolerance ๐ŸŒธ but with ACK frame loss AND reordering, it does NOT guarantee every acknowledgment reaches the sender before it's dropped from the ACK frame. packets can arrive out of order and then every ACK frame containing them gets lost ๐Ÿ˜ญ in that case the loss recovery might cause spurious retransmissions, but the sender keeps making forward progress, so it's not the end of the world ๐Ÿฆ‹


13.2.5. Measuring and Reporting Host Delay ๐Ÿ“

so an endpoint measures the delays it intentionally adds between receiving the packet with the largest packet number and sending the acknowledgment. it puts that in the ACK Delay field of an ACK frame (see Section 19.3) ๐Ÿ’– this lets the receiver adjust for those intentional delays, which matters a LOT for getting a good path RTT estimate when acks are delayed ๐Ÿ”ฅ

here's the tea โ˜• a packet can sit in the OS kernel or somewhere else on the host before it gets processed. an endpoint MUST NOT include delays it doesn't control in the ACK Delay field. HOWEVER, endpoints SHOULD include buffering delays caused by not having decryption keys yet, since those can be big and usually don't repeat ๐Ÿ‘‘

and when the measured ack delay is bigger than its max_ack_delay, an endpoint SHOULD report the measured delay ๐ŸŒˆ this is especially useful during the handshake, when delays can get big (see Section 13.2.1) ๐Ÿงš


13.2.6. ACK Frames and Packet Protection ๐Ÿฆ‹

okay this one's a must-know ๐Ÿซถ ACK frames MUST only be carried in a packet with the same packet number space as the packet being acknowledged (see Section 12.1). for example, packets protected with 1-RTT keys MUST be acknowledged in packets that are ALSO protected with 1-RTT keys ๐Ÿ’…

the 0-RTT situation is extra spicy ๐ŸŒถ๏ธ packets a client sends with 0-RTT packet protection MUST be acknowledged by the server in packets protected by 1-RTT keys. that means the client might not be able to use those acks if the server's cryptographic handshake messages get delayed or lost ๐Ÿ˜ญ and fr, the same limitation applies to any other data the server sends under 1-RTT keys ๐Ÿ’€


13.2.7. PADDING Frames Consume Congestion Window ๐ŸŒธ

so packets with PADDING frames count as in flight for congestion control purposes [QUIC-RECOVERY] ๐Ÿซง that means packets with ONLY PADDING frames eat up congestion window but don't generate acks that would open it back up. to avoid a deadlock ๐Ÿ™ˆ, a sender SHOULD make sure other frames are sent periodically alongside the PADDING frames, so they draw out acknowledgments from the receiver ๐Ÿ’œ


13.3. Retransmission of Information โœจ

bestie, QUIC packets that get declared lost are NOT resent whole ๐Ÿ’… same goes for the frames inside them. instead, the information that might have been in those frames gets sent again in brand-new frames, as needed ๐ŸŽ€

basically, info gets re-sent when a packet carrying it is declared lost, and it stops once a packet carrying it is acknowledged ๐Ÿฆ‹ here's how it plays out for each kind of info:

  • Data in CRYPTO frames gets retransmitted following the rules in [QUIC-RECOVERY], until ALL of it is acknowledged ๐Ÿ”ฅ CRYPTO data in Initial and Handshake packets gets thrown out once the keys for that packet number space are discarded ๐Ÿงน
  • Application data in STREAM frames gets resent in new STREAM frames, UNLESS the endpoint already sent a RESET_STREAM for that stream ๐Ÿ’– once a RESET_STREAM frame is sent, no more STREAM frames are needed ๐Ÿซถ
  • ACK frames carry the most recent set of acknowledgments and the ack delay from the largest acknowledged packet (see Section 13.2.1) ๐ŸŒธ delaying ACK-carrying packets, or resending old ACK frames, can make the peer generate an inflated RTT sample or unnecessarily disable ECN ๐Ÿ˜ญ
  • Stream transmission cancellation, carried in a RESET_STREAM frame, keeps getting sent until it's acknowledged, OR until all the stream data is acknowledged by the peer (so either the "Reset Recvd" or "Data Recvd" state is reached on the sending part of the stream) ๐Ÿฆ‹ the content of a RESET_STREAM frame MUST NOT change when it's re-sent ๐Ÿ™…โ€โ™€๏ธ
  • A request to cancel stream transmission, encoded in a STOP_SENDING frame, is sent the same way, until the receiving part of the stream enters "Data Recvd" or "Reset Recvd" (see Section 3.5) ๐Ÿ’…
  • Connection close signals, including packets with CONNECTION_CLOSE frames, are NOT sent again when loss is detected ๐Ÿ’€ resending these is covered in Section 10 ๐Ÿงš
  • The current connection maximum data goes out in MAX_DATA frames ๐Ÿ’– an updated value gets sent in a MAX_DATA frame if the packet with the most recent MAX_DATA frame is declared lost, OR when the endpoint decides to update the limit ๐ŸŒˆ careful though, the limit can go up a lot, so don't send this too often or you'll flood the peer with MAX_DATA frames (see Section 4.2) ๐Ÿ‘‘
  • The current maximum stream data offset goes out in MAX_STREAM_DATA frames ๐ŸŽ€ same deal as MAX_DATA: an updated value is sent when the packet with the most recent MAX_STREAM_DATA frame for a stream is lost, or when the limit is updated, with care taken not to send it too often โœจ an endpoint SHOULD stop sending MAX_STREAM_DATA frames once the receiving part of the stream enters a "Size Known" or "Reset Recvd" state ๐Ÿซถ
  • The limit on streams of a given type goes out in MAX_STREAMS frames ๐ŸŒธ like MAX_DATA, an updated value is sent when the packet with the most recent MAX_STREAMS frame for a stream type is declared lost, or when the limit is updated, with care taken not to send it too often ๐Ÿ’œ
  • Blocked signals ride in DATA_BLOCKED, STREAM_DATA_BLOCKED, and STREAMS_BLOCKED frames ๐Ÿฆ‹ DATA_BLOCKED frames have connection scope, STREAM_DATA_BLOCKED frames have stream scope, and STREAMS_BLOCKED frames are scoped to a specific stream type ๐Ÿงš a new frame gets sent if the packet with the most recent frame for a scope is lost, but ONLY while the endpoint is still blocked on the matching limit ๐Ÿ™ˆ these frames always include the limit that's causing the block when they're sent ๐Ÿ”ฅ
  • A liveness or path validation check using PATH_CHALLENGE frames is sent periodically until a matching PATH_RESPONSE frame is received, or until there's no remaining need for liveness or path validation checking ๐ŸŒˆ PATH_CHALLENGE frames carry a different payload each time they're sent ๐Ÿ’–
  • Responses to path validation using PATH_RESPONSE frames are sent just once ๐Ÿ“ the peer is expected to send more PATH_CHALLENGE frames as needed to evoke additional PATH_RESPONSE frames ๐Ÿ’…
  • New connection IDs go out in NEW_CONNECTION_ID frames and get retransmitted if the packet carrying them is lost ๐Ÿซง retransmissions of this frame carry the SAME sequence number value ๐ŸŽ€ likewise, retired connection IDs go out in RETIRE_CONNECTION_ID frames and get retransmitted if their packet is lost ๐Ÿฆ‹
  • NEW_TOKEN frames get retransmitted if the packet carrying them is lost ๐ŸŒธ no special support exists for spotting reordered and duplicated NEW_TOKEN frames, beyond a direct comparison of the frame contents ๐Ÿง
  • PING and PADDING frames contain no information, so lost PING or PADDING frames don't need repair ๐Ÿ’œ no drama, no repair needed โœจ
  • The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged ๐Ÿ‘‘

endpoints SHOULD prioritize retransmitting data over sending new data, unless the priorities the application specifies say otherwise (see Section 2.3) ๐Ÿ”ฅ

even though a sender is encouraged to build frames with up-to-date info every time it sends a packet, it's not forbidden to retransmit copies of frames from lost packets ๐Ÿ™Œ a sender that does this needs to handle decreases in available payload size from changes in packet number length, connection ID length, and path MTU ๐Ÿง  and a receiver MUST accept packets containing an outdated frame, like a MAX_DATA frame carrying a smaller maximum data value than one in an older packet ๐Ÿ’–

a sender SHOULD avoid retransmitting information from packets once they're acknowledged ๐Ÿฆ‹ that includes packets acknowledged AFTER being declared lost, which can happen with network reordering ๐Ÿ™ˆ doing this means senders have to keep info about packets even after they're declared lost ๐Ÿงš a sender can discard that info after enough time passes to allow for reordering, like a PTO (Section 6.2 of [QUIC-RECOVERY]), or based on other events, like hitting a memory limit ๐Ÿซถ

upon detecting losses, a sender MUST take appropriate congestion control action ๐Ÿšจ the details of loss detection and congestion control are in [QUIC-RECOVERY] ๐Ÿ’…


13.4. Explicit Congestion Notification ๐ŸŒˆ

QUIC endpoints can use ECN [RFC3168] to detect and respond to network congestion โšก ECN lets an endpoint set an ECN-Capable Transport (ECT) codepoint in the ECN field of an IP packet ๐ŸŽ€ then a network node can signal congestion by setting the ECN-CE codepoint in that field, instead of dropping the packet [RFC8087] ๐Ÿฆ‹ endpoints react to reported congestion by reducing their sending rate, as described in [QUIC-RECOVERY] ๐Ÿ“‰๐Ÿ’œ

to enable ECN, a sending QUIC endpoint first figures out whether a path supports ECN marking, and whether the peer reports the ECN values in received IP headers (see Section 13.4.2) ๐Ÿ”ฅ

13.4.1. Reporting ECN Counts โœจ

okay so reading the ECN field off an IP packet isn't possible on every platform ๐Ÿ˜ญ which means if an endpoint doesn't do ECN, or can't see the ECN fields on what it receives, it just doesn't report ECN counts for the packets it gets ๐Ÿซถ

Even if an endpoint doesn't set an ECT field in the packets it sends, it MUST provide feedback about the ECN markings it receives, as long as those are accessible. And if it skips reporting the ECN counts, the sender is going to turn off ECN for the connection ๐Ÿ’€

When an ECN-enabled endpoint gets an IP packet with an ECT(0), ECT(1), or ECN-CE codepoint, it reads the ECN field and bumps the matching ECT(0), ECT(1), or ECN-CE count. Those ECN counts ride along in later ACK frames, so peek at Sections 13.2 and 19.3 ๐Ÿฆ‹

Each packet number space keeps its own acknowledgment state and its own ECN counts ๐Ÿ’œ Coalesced QUIC packets (see Section 12.2) share one IP header, so the ECN counts go up once for each coalesced QUIC packet.

For example, if one each of an Initial, Handshake, and 1-RTT QUIC packet get coalesced into a single UDP datagram, the ECN counts for all three packet number spaces go up by one each, based on the ECN field of that one IP header ๐Ÿงš

ECN counts only increase when the QUIC packets from a received IP packet get processed. So duplicate QUIC packets aren't processed and don't increase the ECN counts. Check Section 21.10 for the security concerns that come with this ๐Ÿ”ฅ

13.4.2. ECN Validation ๐Ÿ‘‘

Faulty network devices can corrupt packets or erroneously drop them when they carry a non-zero ECN codepoint ๐Ÿ˜ค So to keep connectivity working even with those devices around, an endpoint validates the ECN counts for each network path, and it turns off ECN on that path if it finds errors.

To perform ECN validation for a new path:

  • The endpoint sets an ECT(0) codepoint in the IP header of early outgoing packets sent on a new path to the peer [RFC8311].
  • The endpoint monitors whether all packets sent with an ECT codepoint are eventually deemed lost (Section 6 of [QUIC-RECOVERY]), which means ECN validation has failed.

If an endpoint has a reason to expect that IP packets with an ECT codepoint might get dropped by a faulty network element, it could set an ECT codepoint on only the first ten outgoing packets on a path, or for a period of three PTOs (see Section 6.2 of [QUIC-RECOVERY]). If all the packets marked with non-zero ECN codepoints get lost afterward, it can turn off marking on the assumption that the marking caused the loss ๐Ÿ™ˆ

So an endpoint tries to use ECN and validates it for each new connection, when switching to a server's preferred address, and on active connection migration to a new path. Appendix A.4 walks through one possible algorithm ๐ŸŒธ

There are other ways to probe paths for ECN support, and different marking strategies are possible too. Implementations MAY use other methods defined in RFCs; see [RFC8311]. Implementations that use the ECT(1) codepoint need to perform ECN validation using the reported ECT(1) counts.

13.4.2.1. Receiving ACK Frames with ECN Counts ๐Ÿ”

Erroneous ECN-CE markings applied by the network can hurt connection performance, so an endpoint that receives an ACK frame with ECN counts validates those counts before using them. It does this by comparing the newly received counts against the counts from the last successfully processed ACK frame. Any increase in the ECN counts gets validated against the ECN markings that were applied to the packets newly acknowledged in that ACK frame.

If an ACK frame newly acknowledges a packet that the endpoint sent with either the ECT(0) or ECT(1) codepoint set, ECN validation fails if the matching ECN counts aren't in the ACK frame. This check catches a network element that zeroes the ECN field, or a peer that doesn't report ECN markings ๐Ÿ˜ฌ

ECN validation also fails if the sum of the increase in ECT(0) and ECN-CE counts is less than the number of newly acknowledged packets that were originally sent with an ECT(0) marking. In the same way, ECN validation fails if the sum of the increases to ECT(1) and ECN-CE counts is less than the number of newly acknowledged packets sent with an ECT(1) marking. These checks can detect the network remarking ECN-CE markings ๐Ÿ‘€

An endpoint could miss acknowledgments for a packet when ACK frames get lost. So the total increase in ECT(0), ECT(1), and ECN-CE counts can end up greater than the number of packets newly acknowledged by an ACK frame. That's exactly why ECN counts are allowed to be larger than the total number of packets acknowledged ๐Ÿ’…

Validating ECN counts from reordered ACK frames can result in failure. An endpoint MUST NOT fail ECN validation as a result of processing an ACK frame that does not increase the largest acknowledged packet number. Reordering shouldn't get the blame ๐Ÿ™…โ€โ™€๏ธ

ECN validation can also fail if the received total count for either ECT(0) or ECT(1) exceeds the total number of packets sent with each corresponding ECT codepoint. Specifically, validation fails when an endpoint receives a non-zero ECN count for an ECT codepoint it never applied. This check detects when packets get remarked to ECT(0) or ECT(1) in the network ๐Ÿ•ต๏ธโ€โ™€๏ธ

13.4.2.2. ECN Validation Outcomes ๐ŸŽ€

If validation fails, then the endpoint MUST disable ECN. It stops setting the ECT codepoint in the IP packets it sends, because it's assuming either the network path or the peer doesn't support ECN.

Even if validation fails, an endpoint MAY revalidate ECN for the same path at any later time in the connection. An endpoint could also keep periodically trying validation ๐Ÿ”

Once validation succeeds, an endpoint MAY keep setting an ECT codepoint in the packets it sends afterward, expecting that the path is ECN capable. Network routing and path elements can change mid-connection, though, so an endpoint MUST disable ECN if validation fails later on โš ๏ธ


14. Datagram Size ๐Ÿ“

A UDP datagram can carry one or more QUIC packets. The datagram size is the total UDP payload size of a single UDP datagram carrying QUIC packets. It includes the one or more QUIC packet headers and protected payloads, but not the UDP or IP headers ๐Ÿงš

The maximum datagram size is the largest UDP payload that can be sent across a network path in a single UDP datagram. QUIC MUST NOT be used if the network path can't support a maximum datagram size of at least 1200 bytes ๐Ÿšซ

QUIC assumes a minimum IP packet size of at least 1280 bytes. That's the IPv6 minimum size [IPv6], and most modern IPv4 networks support it too. If you assume the minimum IP header size of 40 bytes for IPv6 and 20 bytes for IPv4, plus a UDP header size of 8 bytes, you get a maximum datagram size of 1232 bytes for IPv6 and 1252 bytes for IPv4. So modern IPv4 and all IPv6 network paths are expected to be able to support QUIC ๐ŸŒˆ

Note: This requirement to support a UDP payload of 1200 bytes limits the space available for IPv6 extension headers to 32 bytes or IPv4 options to 52 bytes if the path only supports the IPv6 minimum MTU of 1280 bytes. This affects Initial packets and path validation. ๐Ÿ™ˆ

Any maximum datagram size larger than 1200 bytes can be discovered using Path Maximum Transmission Unit Discovery (PMTUD) (see Section 14.2.1) or Datagram Packetization Layer PMTU Discovery (DPLPMTUD) (see Section 14.3) ๐Ÿ”

The max_udp_payload_size transport parameter (Section 18.2) can act as an additional limit on the maximum datagram size. A sender can avoid going over this limit once the value is known. Before the endpoints learn the value of the transport parameter, though, they risk datagrams getting lost if they send anything larger than the smallest allowed maximum datagram size of 1200 bytes ๐Ÿ˜ฐ

UDP datagrams MUST NOT be fragmented at the IP layer. In IPv4 [IPv4], the Don't Fragment (DF) bit MUST be set if possible, so that fragmentation on the path gets prevented ๐Ÿ›‘

Sometimes QUIC needs datagrams to be no smaller than a certain size; Section 8.1 is one example. But the size of a datagram isn't authenticated. That means if an endpoint receives a datagram of a certain size, it can't know the sender sent it at that same size ๐Ÿคทโ€โ™€๏ธ So an endpoint MUST NOT close a connection when it receives a datagram that doesn't meet size constraints; it MAY discard such datagrams instead ๐Ÿ’–

14.1. Initial Datagram Size ๐ŸŒŸ

A client MUST expand the payload of all UDP datagrams carrying Initial packets to at least the smallest allowed maximum datagram size of 1200 bytes. It does this by adding PADDING frames to the Initial packet or by coalescing the Initial packet (see Section 12.2). Initial packets can even be coalesced with invalid packets, which a receiver will discard ๐Ÿ—‘๏ธ Similarly, a server MUST expand the payload of all UDP datagrams carrying ack-eliciting Initial packets to at least the smallest allowed maximum datagram size of 1200 bytes ๐Ÿ’œ

Sending UDP datagrams of this size makes sure the network path supports a reasonable Path Maximum Transmission Unit (PMTU), in both directions ๐Ÿ›ฃ๏ธ Plus, a client that expands its Initial packets helps reduce the amplitude of amplification attacks caused by server responses toward an unverified client address (see Section 8) ๐Ÿซถ

Datagrams containing Initial packets MAY exceed 1200 bytes if the sender believes that the network path and peer both support the size it chooses ๐Ÿฆ‹

A server MUST discard an Initial packet that's carried in a UDP datagram with a payload smaller than the smallest allowed maximum datagram size of 1200 bytes. A server MAY also close the connection right away by sending a CONNECTION_CLOSE frame with an error code of PROTOCOL_VIOLATION (see Section 10.2.3) ๐Ÿ˜ค

The server MUST also limit the number of bytes it sends before validating the address of the client; see Section 8 ๐Ÿ”’

14.2. Path Maximum Transmission Unit ๐Ÿ›ฃ๏ธ

Okay bestie, so the PMTU is the biggest an entire IP packet can be ๐Ÿฆ‹. That count includes the IP header, the UDP header, AND the UDP payload. The payload holds one or more QUIC packet headers plus their protected payloads ๐Ÿ’–. The PMTU can depend on the path and can change over time ๐ŸŒธ, so the largest UDP payload an endpoint is sending at any given moment gets the name maximum datagram size โœจ.

An endpoint SHOULD use DPLPMTUD (Section 14.3) or PMTUD (Section 14.2.1) to check whether the path to a destination can handle a desired maximum datagram size without fragmentation ๐Ÿ”. If it has neither of those, QUIC endpoints SHOULD NOT send datagrams bigger than the smallest allowed maximum datagram size ๐Ÿ™ˆ.

Both DPLPMTUD and PMTUD send datagrams that are bigger than the current maximum datagram size. These are called PMTU probes ๐Ÿงช. Every QUIC packet that is NOT in a PMTU probe SHOULD be sized to fit within the maximum datagram size, so the datagram doesn't get fragmented or dropped [RFC8085] ๐Ÿ’….

If a QUIC endpoint figures out that the PMTU between any pair of local and remote IP addresses can't support the smallest allowed maximum datagram size of 1200 bytes, it MUST immediately stop sending QUIC packets on that affected path ๐Ÿšซ. The only exceptions are packets in PMTU probes and packets containing CONNECTION_CLOSE frames ๐Ÿ’€. An endpoint MAY end the connection if it can't find an alternative path ๐Ÿ‘‹.

Every pair of local and remote addresses could have its own PMTU ๐ŸŒˆ. So QUIC implementations that do any kind of PMTU discovery SHOULD keep a maximum datagram size for each combination of local and remote IP addresses ๐Ÿ—‚๏ธ.

A QUIC implementation MAY be extra cautious when computing the maximum datagram size, to allow for unknown tunnel overheads or IP header options and extensions ๐Ÿ›ก๏ธ.

14.2.1. Handling of ICMP Messages by PMTUD ๐Ÿ“จ

PMTUD [RFC1191] [RFC8201] depends on receiving ICMP messages, specifically IPv6 Packet Too Big (PTB) messages ๐Ÿ“ฌ. Those messages say when an IP packet got dropped because it was bigger than the local router MTU. DPLPMTUD can optionally use these messages too ๐Ÿคทโ€โ™€๏ธ. Here's the tea: this ICMP use is potentially vulnerable to attacks from entities that can't see the packets but might successfully guess the addresses on the path ๐Ÿ˜ฑ. Those attacks could drag the PMTU down to a bandwidth-inefficient value ๐Ÿข.

An endpoint MUST ignore an ICMP message that claims the PMTU has dropped below QUIC's smallest allowed maximum datagram size ๐Ÿ™…โ€โ™€๏ธ.

The rules for generating ICMP [RFC1812] [RFC4443] say the quoted packet should include as much of the original packet as possible without going over the minimum MTU for the IP version ๐Ÿ“ฆ. The quoted packet can actually be smaller, or the info can be unintelligible, as described in Section 1.1 of [DPLPMTUD] ๐Ÿงฉ.

QUIC endpoints using PMTUD SHOULD validate ICMP messages to protect against packet injection, as specified in [RFC8201] and Section 5.2 of [RFC8085] ๐Ÿ”. That validation SHOULD use the quoted packet in the ICMP payload to link the message to a corresponding transport connection (see Section 4.6.1 of [DPLPMTUD]) ๐Ÿ”—. ICMP message validation MUST include matching IP addresses and UDP ports [RFC8085] and, when possible, connection IDs for an active QUIC session ๐Ÿชช. The endpoint SHOULD ignore every ICMP message that fails validation ๐Ÿ—‘๏ธ.

An endpoint MUST NOT increase the PMTU based on ICMP messages. See Item 6 in Section 3 of [DPLPMTUD] ๐Ÿšง. Any reduction in QUIC's maximum datagram size in response to ICMP messages MAY be provisional, until QUIC's loss detection algorithm figures out that the quoted packet was actually lost ๐Ÿ•ต๏ธโ€โ™€๏ธ.

14.3. Datagram Packetization Layer PMTU Discovery ๐Ÿงฌ

DPLPMTUD [DPLPMTUD] depends on tracking the loss or acknowledgment of QUIC packets carried in PMTU probes ๐Ÿ“Š. PMTU probes for DPLPMTUD that use the PADDING frame implement "Probing using padding data," as defined in Section 4.1 of [DPLPMTUD] ๐Ÿงธ.

Endpoints SHOULD set the initial value of BASE_PLPMTU (Section 5.1 of [DPLPMTUD]) to match QUIC's smallest allowed maximum datagram size ๐ŸŽฏ. MIN_PLPMTU is the same as BASE_PLPMTU ๐Ÿ‘ฏโ€โ™€๏ธ.

QUIC endpoints implementing DPLPMTUD keep a DPLPMTUD Maximum Packet Size (MPS) (Section 4.4 of [DPLPMTUD]) for each combination of local and remote IP addresses ๐Ÿ—ƒ๏ธ. The MPS corresponds to the maximum datagram size ๐Ÿค.

14.3.1. DPLPMTUD and Initial Connectivity ๐Ÿค

From DPLPMTUD's point of view, QUIC is an acknowledged Packetization Layer (PL) ๐Ÿ™Œ. So a QUIC sender can enter the DPLPMTUD BASE state (Section 5.2 of [DPLPMTUD]) once the QUIC connection handshake has completed ๐ŸŽ‰.

14.3.2. Validating the Network Path with DPLPMTUD ๐Ÿ›ค๏ธ

QUIC is an acknowledged PL. That means a QUIC sender does not implement a DPLPMTUD CONFIRMATION_TIMER while it's in the SEARCH_COMPLETE state. See Section 5.2 of [DPLPMTUD] ๐Ÿ’ค.

14.3.3. Handling of ICMP Messages by DPLPMTUD ๐Ÿ“ฎ

An endpoint using DPLPMTUD needs to validate any received ICMP PTB message before it uses the PTB information, as defined in Section 4.6 of [DPLPMTUD] โœ…. On top of UDP port validation, QUIC validates an ICMP message using other PL information, like checking connection IDs in the quoted packet of any received ICMP message ๐Ÿช„.

The considerations for processing ICMP messages in Section 14.2.1 also apply if DPLPMTUD uses these messages ๐Ÿ”.

14.4. Sending QUIC PMTU Probes ๐Ÿš€

PMTU probes are ack-eliciting packets ๐Ÿ””.

Endpoints could limit the content of PMTU probes to PING and PADDING frames, since packets bigger than the current maximum datagram size are more likely to get dropped by the network ๐Ÿช‚. So losing a QUIC packet carried in a PMTU probe is NOT a reliable sign of congestion, and it SHOULD NOT trigger a congestion control reaction. See Item 7 in Section 3 of [DPLPMTUD] ๐Ÿ“‰. Still, PMTU probes do eat up congestion window, which could delay later transmission by an application ๐Ÿฐ.

14.4.1. PMTU Probes Containing Source Connection ID ๐Ÿชช

Endpoints that rely on the Destination Connection ID field to route incoming QUIC packets will probably need the connection ID included in PMTU probes, so any resulting ICMP messages (Section 14.2.1) can find their way back to the right endpoint ๐Ÿงญ. The catch: only long header packets (Section 17.2) contain the Source Connection ID field, and long header packets aren't decrypted or acknowledged by the peer once the handshake is complete ๐Ÿ˜ค.

One way to build a PMTU probe is to coalesce (see Section 12.2) a packet with a long header, like a Handshake or 0-RTT packet (Section 17.2), with a short header packet in a single UDP datagram ๐Ÿงฉ. If the resulting PMTU probe reaches the endpoint, the long header packet gets ignored, but the short header packet gets acknowledged ๐Ÿ’œ. If the PMTU probe causes an ICMP message to be sent, the first part of the probe will be quoted in that message ๐Ÿ“ฌ. If the Source Connection ID field is inside the quoted portion of the probe, it could be used for routing or validating the ICMP message ๐Ÿ”Ž.

   |  Note: The purpose of using a packet with a long header is only
   |  to ensure that the quoted packet contained in the ICMP message
   |  contains a Source Connection ID field.  This packet does not
   |  need to be a valid packet, and it can be sent even if there is
   |  no current use for packets of that type.

15. Versions ๐Ÿท๏ธ

QUIC versions are identified by a 32-bit unsigned number ๐Ÿ”ข.

The version 0x00000000 is reserved to represent version negotiation ๐Ÿค–. This version of the specification is identified by the number 0x00000001 โœจ.

Other versions of QUIC might have different properties from this one ๐ŸŒฑ. The properties of QUIC that are guaranteed to stay consistent across all versions are described in [QUIC-INVARIANTS] ๐Ÿ’Ž.

Version 0x00000001 of QUIC uses TLS as its cryptographic handshake protocol, as described in [QUIC-TLS] ๐Ÿ”.

Versions with the most significant 16 bits of the version number cleared are reserved for use in future IETF consensus documents ๐Ÿ“œ.

Versions that follow the pattern 0x?a?a?a?a are reserved for forcing version negotiation to be exercised. That means any version number where the low four bits of every byte is 1010 in binary ๐ŸŽญ. A client MAY advertise support for any of these reserved versions, and so MAY a server ๐ŸŒŸ.

Reserved version numbers will never represent a real protocol ๐Ÿ™ƒ. A client MAY use one of these version numbers, expecting that the server will start version negotiation ๐Ÿคž. A server MAY advertise support for one of these versions, and it can expect clients to ignore the value ๐Ÿซถ.

16. Variable-Length Integer Encoding ๐Ÿงฎ

QUIC packets and frames commonly use a variable-length encoding for non-negative integer values ๐Ÿฌ. This encoding makes sure that smaller integer values need fewer bytes to encode ๐Ÿฃ.

The QUIC variable-length integer encoding reserves the two most significant bits of the first byte to encode the base-2 logarithm of the integer's encoding length in bytes ๐Ÿค“. The integer value gets encoded in the remaining bits, in network byte order ๐Ÿง .

So integers are encoded on 1, 2, 4, or 8 bytes, and they can hold 6-, 14-, 30-, or 62-bit values, respectively ๐Ÿ“. Table 4 sums up the encoding properties ๐Ÿ’….

       +======+========+=============+=======================+
       | 2MSB | Length | Usable Bits | Range                 |
       +======+========+=============+=======================+
       | 00   | 1      | 6           | 0-63                  |
       +------+--------+-------------+-----------------------+
       | 01   | 2      | 14          | 0-16383               |
       +------+--------+-------------+-----------------------+
       | 10   | 4      | 30          | 0-1073741823          |
       +------+--------+-------------+-----------------------+
       | 11   | 8      | 62          | 0-4611686018427387903 |
       +------+--------+-------------+-----------------------+

                Table 4: Summary of Integer Encodings

An example decoding algorithm and sample encodings are shown in Appendix A.1 ๐Ÿ“–.

Values do not need to be encoded on the minimum number of bytes possible, with one exception: the Frame Type field. See Section 12.4 ๐Ÿ™…โ€โ™€๏ธ.

Versions (Section 15), packet numbers sent in the header (Section 17.1), and the length of connection IDs in long header packets (Section 17.2) are described using integers but do NOT use this encoding ๐Ÿšซ.

17. Packet Formats ๐Ÿ“ฆ

All numeric values are encoded in network byte order (that is, big endian), and all field sizes are in bits ๐Ÿ”ข. Hexadecimal notation is used to describe the value of fields ๐Ÿช„.

17.1. Packet Number Encoding and Decoding โœจ

okay so packet numbers are just integers, anywhere from 0 all the way up to 2^62-1 ๐Ÿ”ข (see Section 12.3). When they show up in long or short headers, they get squished down to 1 to 4 bytes ๐Ÿฆ‹. The trick is that we only send the least significant bits of the number, so we don't need all the bits every single time ๐Ÿ’…

The encoded packet number gets protected the way Section 5.4 of [QUIC-TLS] describes ๐Ÿ”’

Before you've received an acknowledgment for a packet number space, the full packet number MUST be included ๐Ÿซถ No truncating it, no cutting corners, periodt. It's not to be truncated, as described below ๐Ÿ˜ญ

Once an acknowledgment comes back for a packet number space, the sender MUST use a packet number size that can represent a range more than twice as large as the difference between the largest acknowledged packet number and the packet number being sent ๐Ÿ”ฅ That way the peer can decode the packet number correctly, unless the packet gets stuck in transit and shows up after a whole bunch of higher-numbered packets have already arrived ๐Ÿ™ˆ Endpoints SHOULD use a large enough packet number encoding so the number can still be recovered even if the packet arrives after packets that were sent later ๐Ÿ’œ

So, the size of the packet number encoding ends up at least one bit more than the base-2 logarithm of the number of contiguous unacknowledged packet numbers, counting the new packet too ๐Ÿงฎ Pseudocode and an example for packet number encoding are in Appendix A.2 ๐ŸŒธ

On the receiving end, packet protection gets removed from the packet number first, and then the full packet number gets rebuilt ๐Ÿงฉ It uses the number of significant bits present, the value of those bits, and the largest packet number received in a successfully authenticated packet ๐Ÿ‘‘ Rebuilding the full packet number is necessary to finish removing packet protection, so it's kind of a big deal ๐Ÿ’€

Once header protection is gone, the packet number is decoded by finding the value closest to the next expected packet ๐ŸŽฏ The next expected packet is the highest received packet number plus one โœจ Pseudocode and an example for packet number decoding are in Appendix A.3 ๐Ÿฆ‹

17.2. Long Header Packets ๐ŸŽ€

Here's the long header packet layout, lowkey the most iconic diagram ever:

Long Header Packet {
  Header Form (1) = 1,
  Fixed Bit (1) = 1,
  Long Packet Type (2),
  Type-Specific Bits (4),
  Version (32),
  Destination Connection ID Length (8),
  Destination Connection ID (0..160),
  Source Connection ID Length (8),
  Source Connection ID (0..160),
  Type-Specific Payload (..),
}

Figure 13: Long Header Packet Format

Long headers are for packets sent before 1-RTT keys have been established ๐Ÿ”‘ Once 1-RTT keys are available, the sender switches over to the short header (Section 17.3) ๐Ÿ” The long form is also what lets special packets, like the Version Negotiation packet, fit into this same uniform fixed-length format ๐Ÿงš Packets that use the long header have these fields:

Header Form: The most significant bit (0x80) of byte 0, the first byte, is set to 1 for long headers ๐Ÿ’–

Fixed Bit: The next bit (0x40) of byte 0 is set to 1, unless the packet is a Version Negotiation packet ๐ŸŒˆ Packets with a zero value for this bit are not valid packets in this version and MUST be discarded ๐Ÿšซ Setting this bit to 1 lets QUIC coexist with other protocols; see [RFC7983] ๐Ÿค

Long Packet Type: The next two bits, the ones with a mask of 0x30, in byte 0 hold a packet type ๐Ÿท๏ธ Packet types are listed in Table 5 ๐Ÿ“‹

Type-Specific Bits: The lower four bits of byte 0, the ones with a mask of 0x0f, mean whatever the packet type says they mean ๐ŸŽญ

Version: The QUIC Version is a 32-bit field right after the first byte โœจ It tells you which version of QUIC is in use and how the rest of the protocol fields should be read ๐Ÿ“–

Destination Connection ID Length: The byte after the version holds the length in bytes of the Destination Connection ID field that comes next ๐Ÿ“ This length is an 8-bit unsigned integer ๐Ÿ”ข In QUIC version 1, this value MUST NOT exceed 20 bytes ๐Ÿ™…โ€โ™€๏ธ Endpoints that get a version 1 long header with a value larger than 20 MUST drop the packet ๐Ÿ—‘๏ธ To properly form a Version Negotiation packet, servers SHOULD be able to read longer connection IDs from other QUIC versions ๐Ÿง 

Destination Connection ID: This field comes right after the Destination Connection ID Length field, which tells you how long it is ๐Ÿ†” Section 7.2 has the deets on how it gets used ๐Ÿ”

Source Connection ID Length: The byte right after the Destination Connection ID holds the length in bytes of the Source Connection ID field that follows ๐Ÿ“ It's also an 8-bit unsigned integer ๐Ÿ”ข In QUIC version 1, this value MUST NOT exceed 20 bytes ๐Ÿ™…โ€โ™€๏ธ Endpoints that get a version 1 long header with a value larger than 20 MUST drop the packet ๐Ÿ—‘๏ธ For properly forming a Version Negotiation packet, servers SHOULD be able to read longer connection IDs from other QUIC versions ๐Ÿง 

Source Connection ID: This field comes right after the Source Connection ID Length field, which tells you its size ๐Ÿ†” Section 7.2 explains how it's used in more detail ๐Ÿ’…

Type-Specific Payload: Whatever's left of the packet, if anything, depends on the type ๐ŸŽ

In this version of QUIC, these long header packet types are defined:

+======+===========+================+
| Type | Name      | Section        |
+======+===========+================+
| 0x00 | Initial   | Section 17.2.2 |
+------+-----------+----------------+
| 0x01 | 0-RTT     | Section 17.2.3 |
+------+-----------+----------------+
| 0x02 | Handshake | Section 17.2.4 |
+------+-----------+----------------+
| 0x03 | Retry     | Section 17.2.5 |
+------+-----------+----------------+

Table 5: Long Header Packet Types

The header form bit, the Destination and Source Connection ID lengths, the Destination and Source Connection ID fields, and the Version field of a long header packet are all version independent ๐ŸŒ The other fields in the first byte are version specific ๐Ÿงฉ See [QUIC-INVARIANTS] for details on how packets from different versions of QUIC get interpreted ๐Ÿ”Ž

The fields and payload meaning depend on the version and packet type ๐ŸŽญ The type-specific semantics for this version are in the sections that follow, but several long header packets in this version also have these extra fields:

Reserved Bits: Two bits, the ones with a mask of 0x0c, in byte 0 are reserved across multiple packet types ๐Ÿšง These bits are protected with header protection; see Section 5.4 of [QUIC-TLS] ๐Ÿ” The value included before protection MUST be set to 0 ๐Ÿ’ฏ An endpoint MUST treat receipt of a packet with a non-zero value for these bits, after removing both packet and header protection, as a connection error of type PROTOCOL_VIOLATION ๐Ÿšจ Tossing out such a packet after only removing header protection can expose the endpoint to attacks, so please don't do that ๐Ÿ˜ฑ See Section 9.5 of [QUIC-TLS] ๐Ÿ›ก๏ธ

Packet Number Length: In packet types that have a Packet Number field, the least significant two bits, the ones with a mask of 0x03, in byte 0 hold the length of the Packet Number field ๐Ÿ“ It's an unsigned two-bit integer that's one less than the length of the Packet Number field in bytes โž– So the actual length is the value of this field plus one โž• These bits are protected with header protection; see Section 5.4 of [QUIC-TLS] ๐Ÿ”’

Length: This is the length in bytes of the rest of the packet, meaning the Packet Number and Payload fields ๐Ÿ“ฆ It's encoded as a variable-length integer (Section 16) ๐Ÿ”ข

Packet Number: This field is 1 to 4 bytes long ๐Ÿ”ข It's protected with header protection; see Section 5.4 of [QUIC-TLS] ๐Ÿ” Its length is encoded in the Packet Number Length bits of byte 0, as noted above โ˜๏ธ

Packet Payload: This is the payload of the packet, which holds a sequence of frames, and it's protected with packet protection ๐ŸŽ๐Ÿ”’

17.2.1. Version Negotiation Packet ๐ŸŒŸ

A Version Negotiation packet isn't version specific at all ๐ŸŒ When a client gets one, it knows it's a Version Negotiation packet because the Version field is 0 ๐Ÿ•ต๏ธโ€โ™€๏ธ

This packet is a response to a client packet that asks for a version the server doesn't support ๐Ÿ™… Only servers send it ๐Ÿ“ค

The layout of a Version Negotiation packet looks like this:

Version Negotiation Packet {
  Header Form (1) = 1,
  Unused (7),
  Version (32) = 0,
  Destination Connection ID Length (8),
  Destination Connection ID (0..2040),
  Source Connection ID Length (8),
  Source Connection ID (0..2040),
  Supported Version (32) ...,
}

Figure 14: Version Negotiation Packet

The server sets the Unused field to any arbitrary value ๐ŸŽฒ Clients MUST ignore the value of this field ๐Ÿ™ˆ If QUIC might be multiplexed with other protocols (see [RFC7983]), servers SHOULD set the most significant bit of this field (0x40) to 1, so Version Negotiation packets look like they have the Fixed Bit field ๐ŸŽ€ Heads up, though: other versions of QUIC might not make the same recommendation ๐Ÿ“

The Version field of a Version Negotiation packet MUST be set to 0x00000000 ๐Ÿ’ฏ

The server MUST include the value from the Source Connection ID field of the packet it received in the Destination Connection ID field ๐Ÿ” The value for Source Connection ID MUST be copied from the Destination Connection ID of the received packet, which the client picks randomly at first ๐ŸŽฒ Echoing both connection IDs gives clients some assurance that the server actually got the packet and that the Version Negotiation packet wasn't made by something that never saw the Initial packet ๐Ÿ•ต๏ธโ€โ™€๏ธโœจ

Future versions of QUIC could have different rules for connection ID lengths ๐Ÿ“ For example, connection IDs might have a smaller minimum length or a bigger maximum length ๐ŸŒฑ So version-specific rules for the connection ID MUST NOT influence the decision about whether to send a Version Negotiation packet ๐Ÿšซ

The rest of the Version Negotiation packet is a list of 32-bit versions that the server supports ๐Ÿ“œ

A Version Negotiation packet doesn't get acknowledged ๐Ÿ™… It's only sent in response to a packet that asks for an unsupported version; see Section 5.2.2 ๐Ÿ”

The Version Negotiation packet doesn't include the Packet Number and Length fields that other long header packets have ๐Ÿซฅ So a Version Negotiation packet takes up an entire UDP datagram all by itself ๐Ÿ“ฆ

A server MUST NOT send more than one Version Negotiation packet in response to a single UDP datagram ๐Ÿ™…โ€โ™€๏ธ

See Section 6 for how the version negotiation process works ๐Ÿฆ‹

okay bestie, let's get into it ๐Ÿ’…๐ŸŽ€

17.2.2. Initial Packet โœจ

17.2.2. Initial Packet

Okay so an Initial packet is a long-header packet with a type value of 0x00. It's the one that carries the very first CRYPTO frames from both the client and the server so they can do the key exchange, and it can carry ACK frames going either way. Main character packet, no cap ๐Ÿ‘‘

Initial Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2) = 0, Reserved Bits (2), Packet Number Length (2), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Token Length (i), Token (..), Length (i), Packet Number (8..32), Packet Payload (8..), }

Figure 15: Initial Packet

Okay so the Initial packet has a long header plus the Length and Packet Number fields (see Section 17.2 ๐Ÿฆ‹). The first byte holds the Reserved and Packet Number Length bits, which you can also read about in Section 17.2. Between the Source Connection ID and Length fields there are two extra fields that only Initial packets get. Iconic ๐ŸŒธ

Token Length ๐Ÿงš: a variable-length integer that says how long the Token field is, in bytes. It's 0 if there's no token. Initial packets sent by the server MUST set the Token Length field to 0. Clients that get an Initial packet with a non-zero Token Length field MUST either discard the packet or raise a connection error of type PROTOCOL_VIOLATION. Not me being strict about this, but it's a hard rule ๐Ÿ’€

Token ๐Ÿ’œ: the token value that was given earlier in a Retry packet or a NEW_TOKEN frame. See Section 8.1.

Okay so, to stop version-unaware middleboxes from messing with things, Initial packets get protected with connection- and version-specific keys (called Initial keys), as described in [QUIC-TLS]. Lowkey important: this protection doesn't give confidentiality or integrity against anyone who can see the packets. What it does do is stop attackers who can't see the packets from spoofing Initial packets. ๐Ÿ™ˆ

The client and server use the Initial packet type for any packet carrying an initial cryptographic handshake message. That includes every case where a new packet with the initial crypto message has to be made, like the packets sent after getting a Retry packet (see Section 17.2.5). Literally everywhere the handshake starts ๐Ÿซถ

A server sends its first Initial packet as a response to a client Initial. A server MAY send multiple Initial packets. The crypto key exchange could take several round trips or retransmissions of this data, so don't panic if it's a whole thing โœจ

The payload of an Initial packet holds a CRYPTO frame (or frames) with a cryptographic handshake message, ACK frames, or both. PING, PADDING, and CONNECTION_CLOSE frames of type 0x1c are also allowed. If an endpoint gets an Initial packet with other frames in it, it can either discard the packet as spurious or treat it as a connection error. Rude ๐Ÿ™„

The first packet a client sends always includes a CRYPTO frame with the start (or all) of the first cryptographic handshake message. The first CRYPTO frame sent always begins at offset 0 (see Section 7). Very organized, very iconic ๐Ÿ’–

Note that if the server sends a TLS HelloRetryRequest (see Section 4.7 of [QUIC-TLS]), the client will send another series of Initial packets. These will keep the handshake going and will contain CRYPTO frames starting at an offset that matches the size of the CRYPTO frames sent in the first flight of Initial packets. It's giving sequel energy ๐ŸŽฌ

17.2.2.1. Abandoning Initial Packets

Okay so a client stops both sending and processing Initial packets once it sends its first Handshake packet. A server stops sending and processing Initial packets once it receives its first Handshake packet. Packets might still be floating around or waiting to be acknowledged, but no more Initial packets need to be exchanged after that point ๐Ÿฆ‹ Initial packet protection keys get discarded (see Section 4.9.1 of [QUIC-TLS]) along with any loss recovery and congestion control state (see Section 6.4 of [QUIC-RECOVERY]).

Any data in CRYPTO frames is discarded, and no longer retransmitted, when Initial keys are discarded. Gone, reduced to atoms ๐Ÿ’€

17.2.3. 0-RTT

A 0-RTT packet uses a long header with a type value of 0x01, followed by the Length and Packet Number fields (see Section 17.2). The first byte holds the Reserved and Packet Number Length bits (see Section 17.2). A 0-RTT packet carries "early" data from the client to the server as part of the first flight, before the handshake finishes. As part of the TLS handshake, the server can accept or reject this early data. Chaotic but make it work ๐ŸŒˆ

See Section 2.3 of [TLS13] for a discussion of 0-RTT data and its limitations.

0-RTT Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2) = 1, Reserved Bits (2), Packet Number Length (2), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Length (i), Packet Number (8..32), Packet Payload (8..), }

Figure 16: 0-RTT Packet

Packet numbers for 0-RTT protected packets use the same space as 1-RTT protected packets. Shared closet, shared rules ๐Ÿงš

Okay so after a client gets a Retry packet, 0-RTT packets are likely to have been lost or discarded by the server. A client SHOULD try to resend data in 0-RTT packets after it sends a new Initial packet. New packet numbers MUST be used for any new packets that get sent. As described in Section 17.2.5.3, reusing packet numbers could compromise packet protection. Never recycle packet numbers, bestie, it's a whole security thing ๐Ÿ”ฅ

A client only receives acknowledgments for its 0-RTT packets once the handshake is complete, as defined in Section 4.1.1 of [QUIC-TLS]. Patience is a virtue ๐ŸŒธ

A client MUST NOT send 0-RTT packets once it starts processing 1-RTT packets from the server. This means 0-RTT packets can't contain any response to frames from 1-RTT packets. For instance, a client can't send an ACK frame in a 0-RTT packet, because that can only acknowledge a 1-RTT packet. An acknowledgment for a 1-RTT packet MUST be carried in a 1-RTT packet. Not allowed to be sneaky about it ๐Ÿ™…โ€โ™€๏ธ

A server SHOULD treat a violation of remembered limits (Section 7.4.1) as a connection error of an appropriate type (for instance, a FLOW_CONTROL_ERROR for exceeding stream data limits). Boundaries matter, periodt ๐Ÿ’…

17.2.4. Handshake Packet

A Handshake packet uses a long header with a type value of 0x02, followed by the Length and Packet Number fields (see Section 17.2). The first byte holds the Reserved and Packet Number Length bits (see Section 17.2). It's used to carry cryptographic handshake messages and acknowledgments from both the server and the client. Handshake era ๐ŸŽ€

Handshake Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2) = 2, Reserved Bits (2), Packet Number Length (2), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Length (i), Packet Number (8..32), Packet Payload (8..), }

Figure 17: Handshake Protected Packet

Once a client has received a Handshake packet from a server, it uses Handshake packets to send later cryptographic handshake messages and acknowledgments back to the server. Call and response ๐Ÿฆ‹

The Destination Connection ID field in a Handshake packet holds a connection ID chosen by the recipient of the packet. The Source Connection ID holds the connection ID that the sender wants to use (see Section 7.2). Two IDs, one vibe ๐Ÿซถ

Handshake packets have their own packet number space, so the first Handshake packet a server sends has a packet number of 0. Fresh start, new numbering ๐ŸŒˆ

The payload of this packet contains CRYPTO frames and could also contain PING, PADDING, or ACK frames. Handshake packets MAY contain CONNECTION_CLOSE frames of type 0x1c. Endpoints MUST treat receipt of Handshake packets with other frames as a connection error of type PROTOCOL_VIOLATION. Strict list, no exceptions ๐Ÿ’…

Like Initial packets (see Section 17.2.2.1), data in CRYPTO frames for Handshake packets is discarded, and no longer retransmitted, when Handshake protection keys are discarded. Same energy as before, gone for good ๐Ÿ’€

17.2.5. Retry Packet

As shown in Figure 18, a Retry packet uses a long packet header with a type value of 0x03. It carries an address validation token created by the server. A server uses it when it wants to do a retry (see Section 8.1). Sending the client back to try again, respectfully ๐Ÿงš

Retry Packet { Header Form (1) = 1, Fixed Bit (1) = 1, Long Packet Type (2) = 3, Unused (4), Version (32), Destination Connection ID Length (8), Destination Connection ID (0..160), Source Connection ID Length (8), Source Connection ID (0..160), Retry Token (..), Retry Integrity Tag (128), }

Figure 18: Retry Packet

A Retry packet doesn't contain any protected fields. The server sets the Unused field to an arbitrary value, and a client MUST ignore these bits. Beyond the long header fields, it has these extra fields:

Retry Token ๐ŸŒธ: an opaque token the server can use to validate the client's address.

Retry Integrity Tag ๐Ÿฆ‹: defined in Section 5.8 ("Retry Packet Integrity") of [QUIC-TLS].

17.2.5.1. Sending a Retry Packet

The server fills the Destination Connection ID with the connection ID the client put in the Source Connection ID of the Initial packet. Easy handoff ๐ŸŽ€

The server includes a connection ID of its choice in the Source Connection ID field. This value MUST NOT be equal to the Destination Connection ID field of the packet the client sent. A client MUST discard a Retry packet whose Source Connection ID field is identical to the Destination Connection ID field of its Initial packet. The client MUST use the value from the Source Connection ID field of the Retry packet in the Destination Connection ID field of the subsequent packets it sends. No copying allowed, bestie ๐Ÿ™ˆ

A server MAY send Retry packets in response to Initial and 0-RTT packets. A server can either discard or buffer the 0-RTT packets it receives. A server can send multiple Retry packets as it receives Initial or 0-RTT packets. A server MUST NOT send more than one Retry packet in response to a single UDP datagram. One per datagram, that's the rule ๐ŸŒˆ

17.2.5.2. Handling a Retry Packet

A client MUST accept and process at most one Retry packet for each connection attempt. After the client has received and processed an Initial or Retry packet from the server, it MUST discard any Retry packets it receives after that. Once is enough ๐Ÿ‘‘

Clients MUST discard Retry packets that have a Retry Integrity Tag that can't be validated (see Section 5.8 of [QUIC-TLS]). This makes it harder for an attacker to inject a Retry packet, and it protects against accidental corruption of Retry packets. A client MUST discard a Retry packet with a zero-length Retry Token field. Bye to the sus ones ๐Ÿ’€

The client responds to a Retry packet with an Initial packet that includes the provided Retry token, so it can keep connection establishment going. โœจ

A client sets the Destination Connection ID field of this Initial packet to the value from the Source Connection ID field in the Retry packet. Changing the Destination Connection ID also changes the keys used to protect the Initial packet ๐Ÿงš It also sets the Token field to the token provided in the Retry packet. The client MUST NOT change the Source Connection ID, because the server could include the connection ID as part of its token validation logic (see Section 8.1.4). Don't touch that field, it's load-bearing ๐Ÿซถ

A Retry packet doesn't include a packet number and can't be explicitly acknowledged by a client. No receipts for this one ๐Ÿ’…


That's the Initial, 0-RTT, Handshake, and Retry packet stuff for this part ๐ŸŽ€ Next up, we keep going ๐ŸŒธ

17.2.5.3. Continuing a Handshake after Retry โœจ

okay so after a Retry, the client's next Initial packets carry the connection ID and token from the Retry packet ๐ŸŽ€. The client copies the Source Connection ID field from the Retry packet into the Destination Connection ID field and keeps using that value until it gets an Initial packet with an updated value (see Section 7.2). The Token field gets copied into every later Initial packet too (see Section 8.1.2).

Apart from updating the Destination Connection ID and Token fields, the Initial packet from the client follows the same rules as the first Initial packet ๐Ÿ’…. A client MUST use the same cryptographic handshake message it included in that packet. A server MAY treat a packet with a different cryptographic handshake message as a connection error or discard it ๐Ÿ˜ญ. Heads up: including a Token field eats into the room left for the cryptographic handshake message, so the client might have to send multiple Initial packets ๐Ÿงš.

A client MAY attempt 0-RTT after getting a Retry packet by sending 0-RTT packets to the connection ID the server gave it ๐Ÿฆ‹.

A client MUST NOT reset the packet number for any packet number space after processing a Retry packet. This is so important, bestie ๐Ÿซถ. 0-RTT packets carry confidential info that will most likely get retransmitted when a Retry arrives. The keys protecting these new 0-RTT packets don't change just because the client responded to a Retry. But the data in them might be different from what was sent earlier. Reusing the same packet number could seriously compromise packet protection for those packets, since the same key and nonce might end up protecting different content ๐Ÿ’€. A server MAY abort the connection if it notices the client reset the packet number ๐Ÿšจ.

The connection IDs used in Initial and Retry packets that go back and forth between client and server get copied into the transport parameters, and they're validated as described in Section 7.3 ๐Ÿ’œ.

17.3. Short Header Packets ๐ŸŒธ

This version of QUIC defines just one packet type that uses the short packet header. Only one, no cap ๐Ÿ’….

17.3.1. 1-RTT Packet ๐Ÿฆ‹

A 1-RTT packet uses a short packet header. It shows up once the version and 1-RTT keys have been negotiated โœจ.

1-RTT Packet {
  Header Form (1) = 0,
  Fixed Bit (1) = 1,
  Spin Bit (1),
  Reserved Bits (2),
  Key Phase (1),
  Packet Number Length (2),
  Destination Connection ID (0..160),
  Packet Number (8..32),
  Packet Payload (8..),
}

                      Figure 19: 1-RTT Packet

Here's what the 1-RTT packet fields are giving ๐Ÿ’–:

  • Header Form: The most significant bit (0x80) of byte 0 is set to 0 for the short header. Main character energy for the short header ๐ŸŽ€.
  • Fixed Bit: The next bit (0x40) of byte 0 is set to 1. Packets with a zero value for this bit are not valid packets in this version and MUST be discarded ๐Ÿšซ. A value of 1 is what lets QUIC coexist with other protocols (see [RFC7983]) ๐ŸŒˆ.
  • Spin Bit: The third most significant bit (0x20) of byte 0 is the latency spin bit, set as described in Section 17.4 โญ.
  • Reserved Bits: The next two bits (the ones with a mask of 0x18) of byte 0 are reserved. They're protected with header protection (see Section 5.4 of [QUIC-TLS]). The value included before protection MUST be set to 0. An endpoint MUST treat receipt of a packet with a non-zero value in these bits, after removing both packet and header protection, as a connection error of type PROTOCOL_VIOLATION. Tossing out such a packet after only removing header protection can expose the endpoint to attacks (see Section 9.5 of [QUIC-TLS]) ๐Ÿ˜ญ๐Ÿ”ฅ.
  • Key Phase: The next bit (0x04) of byte 0 indicates the key phase. This lets the recipient figure out which packet protection keys were used to protect the packet. See [QUIC-TLS] for the deets. This bit is protected with header protection (see Section 5.4 of [QUIC-TLS]) ๐Ÿ”‘.
  • Packet Number Length: The least significant two bits (the ones with a mask of 0x03) of byte 0 hold the length of the Packet Number field. It's encoded as an unsigned two-bit integer that's one less than the Packet Number field length in bytes. So the real length is the value plus one. These bits are protected with header protection (see Section 5.4 of [QUIC-TLS]) ๐Ÿงฎ.
  • Destination Connection ID: This is a connection ID picked by the intended recipient of the packet. See Section 5.1 for more details ๐Ÿชช.
  • Packet Number: The Packet Number field is 1 to 4 bytes long. The packet number is protected with header protection (see Section 5.4 of [QUIC-TLS]). The Packet Number Length field tells you how long this field is. See Section 17.1 for details ๐Ÿ”ข.
  • Packet Payload: 1-RTT packets always include a 1-RTT protected payload ๐ŸŽ.

The header form bit and the Destination Connection ID field of a short header packet are version independent. Everything else is specific to the QUIC version you've selected. See [QUIC-INVARIANTS] for how packets from different QUIC versions get interpreted ๐Ÿง .

17.4. Latency Spin Bit ๐ŸŒ€

The latency spin bit is defined for 1-RTT packets (Section 17.3.1). It lets observers on the network path do passive latency monitoring for the whole life of a connection ๐Ÿ‘€. The server reflects the spin value it receives, while the client "spins" it after one RTT. On-path observers can measure the time between two spin bit toggle events to estimate the end-to-end RTT of a connection. Iconic, honestly ๐Ÿ˜ญโœจ.

The spin bit only shows up in 1-RTT packets. That's because you can measure the initial RTT of a connection just by watching the handshake ๐Ÿค. So the spin bit is available once version negotiation and connection establishment are done. On-path measurement and use of the latency spin bit get more discussion in [QUIC-MANAGEABILITY] ๐Ÿ“š.

The spin bit is an OPTIONAL feature of this version of QUIC. An endpoint that doesn't support this feature MUST disable it, as defined below ๐Ÿ™…โ€โ™€๏ธ.

Each endpoint decides on its own whether the spin bit is enabled or disabled for a connection. Implementations MUST allow administrators of clients and servers to disable the spin bit, either globally or per connection ๐Ÿ› ๏ธ. Even when the administrator hasn't disabled it, endpoints MUST disable their use of the spin bit for a random selection of at least one in every 16 network paths, or for one in every 16 connection IDs. The point is to make sure QUIC connections that disable the spin bit are commonly seen on the network ๐Ÿ‘‘. Since each endpoint disables the spin bit independently, the spin bit signal ends up disabled on roughly one in eight network paths ๐Ÿง.

When the spin bit is disabled, endpoints MAY set the spin bit to any value and MUST ignore any incoming value. It is RECOMMENDED that endpoints set the spin bit to a random value, either picked independently for each packet or independently for each connection ID ๐ŸŽฒ.

If the spin bit is enabled for the connection, the endpoint keeps a spin value for each network path. It sets the spin bit in the packet header to the currently stored value whenever it sends a 1-RTT packet on that path. The spin value starts at 0 in the endpoint for each network path. Each endpoint also remembers the highest packet number it has seen from its peer on each path ๐Ÿ“ˆ.

When a server gets a 1-RTT packet that raises the highest packet number it has seen from the client on a given network path, it sets the spin value for that path equal to the spin bit in the received packet ๐Ÿชž.

When a client gets a 1-RTT packet that raises the highest packet number it has seen from the server on a given network path, it sets the spin value for that path to the inverse of the spin bit in the received packet ๐Ÿ”„.

An endpoint resets the spin value for a network path to 0 when it changes the connection ID it's using on that path ๐Ÿงผ.

18. Transport Parameter Encoding ๐ŸŽ€

The extension_data field of the quic_transport_parameters extension defined in [QUIC-TLS] carries the QUIC transport parameters. They're encoded as a sequence of transport parameters, as shown in Figure 20 ๐Ÿ’…:

Transport Parameters {
  Transport Parameter (..) ...,
}

            Figure 20: Sequence of Transport Parameters

Each transport parameter is encoded as an (identifier, length, value) tuple, as shown in Figure 21 ๐Ÿงฉ:

Transport Parameter {
  Transport Parameter ID (i),
  Transport Parameter Length (i),
  Transport Parameter Value (..),
}

              Figure 21: Transport Parameter Encoding

The Transport Parameter Length field holds the length of the Transport Parameter Value field in bytes ๐Ÿ“.

QUIC encodes transport parameters into a sequence of bytes, and that sequence then gets included in the cryptographic handshake ๐Ÿค.

18.1. Reserved Transport Parameters ๐Ÿฆ„

Transport parameters with an identifier of the form "31 * N + 27" for integer values of N are reserved. They exist to exercise the requirement that unknown transport parameters be ignored ๐Ÿ™ˆ. These transport parameters have no semantics and can carry arbitrary values, so do with that what you will ๐Ÿ“.

18.2. Transport Parameter Definitions โœจ

okay so this is the big list of every transport parameter defined in this doc ๐Ÿ’– bestie, read it slowly because implementers depend on every single word ๐ŸŽ€

Lots of these have integer values. The ones that are integers use a variable-length integer encoding, see Section 16 ๐Ÿฆ‹ Transport parameters have a default value of 0 if they're absent, unless it says otherwise ๐ŸŒธ

Here's the full lineup of what's defined:

original_destination_connection_id (0x00): this is the value of the Destination Connection ID field from the first Initial packet the client sent, see Section 7.3 ๐Ÿงš only a server ever sends this one ๐Ÿซถ

max_idle_timeout (0x01): the maximum idle timeout is a value in milliseconds that gets encoded as an integer, see (Section 10.1) โฐ idle timeout is switched off when both endpoints leave this out or both say 0 ๐Ÿ’€

stateless_reset_token (0x02): a stateless reset token is used to verify a stateless reset, see Section 10.3 ๐Ÿ” this one is a sequence of 16 bytes. A client MUST NOT send this transport parameter, but a server MAY send it ๐Ÿ™ˆ a server that doesn't send it can't use stateless reset (Section 10.3) for the connection ID negotiated during the handshake ๐Ÿ’”

max_udp_payload_size (0x03): this is an integer that limits how big UDP payloads the endpoint is willing to receive ๐Ÿ“ฆ UDP datagrams with payloads bigger than this limit are not likely to get processed by the receiver ๐Ÿ˜ญ

The default for this one is the maximum permitted UDP payload of 65527. Values below 1200 are invalid ๐Ÿšซ

This limit acts as an extra constraint on datagram size, the same way the path MTU does, but it's a property of the endpoint and not the path, see Section 14 ๐Ÿ›ฃ๏ธ it's expected that this is the space an endpoint sets aside for holding incoming packets ๐Ÿงบ

initial_max_data (0x04): an integer containing the initial value for the maximum amount of data that can be sent on the connection ๐Ÿ’ฐ it's the same as sending a MAX_DATA (Section 19.9) for the connection right after the handshake finishes ๐ŸŽ‰

initial_max_stream_data_bidi_local (0x05): an integer setting the initial flow control limit for locally initiated bidirectional streams ๐ŸŒŠ this applies to newly created bidirectional streams opened by the endpoint that sends the parameter. In client transport parameters, it applies to streams whose identifier has the least significant two bits set to 0x00. In server transport parameters, it applies to streams with the least significant two bits set to 0x01 ๐Ÿช„

initial_max_stream_data_bidi_remote (0x06): an integer setting the initial flow control limit for peer-initiated bidirectional streams ๐ŸŒท this applies to newly created bidirectional streams opened by the endpoint that receives the parameter. In client transport parameters, it applies to streams with the least significant two bits set to 0x01. In server transport parameters, it applies to streams with the least significant two bits set to 0x00 ๐ŸŽ€

initial_max_stream_data_uni (0x07): an integer setting the initial flow control limit for unidirectional streams โžก๏ธ this applies to newly created unidirectional streams opened by the endpoint that receives the parameter. In client transport parameters, it applies to streams with the least significant two bits set to 0x03. In server transport parameters, it applies to streams with the least significant two bits set to 0x02 ๐Ÿ“

initial_max_streams_bidi (0x08): an integer holding the initial maximum number of bidirectional streams that the endpoint receiving this parameter is allowed to start ๐Ÿšช if it's absent or zero, the peer can't open bidirectional streams until a MAX_STREAMS frame is sent. Setting this is the same as sending a MAX_STREAMS (Section 19.11) of the matching type with the same value ๐Ÿซถ

initial_max_streams_uni (0x09): an integer holding the initial maximum number of unidirectional streams that the endpoint receiving this parameter is allowed to start ๐Ÿšช if it's absent or zero, the peer can't open unidirectional streams until a MAX_STREAMS frame is sent. Setting this is the same as sending a MAX_STREAMS (Section 19.11) of the matching type with the same value ๐Ÿ’…

ack_delay_exponent (0x0a): an integer exponent used to decode the ACK Delay field in the ACK frame (Section 19.3) ๐Ÿงฎ if it's absent, the default is 3, which means a multiplier of 8 โœจ values above 20 are invalid โŒ

max_ack_delay (0x0b): an integer holding the maximum amount of time in milliseconds that the endpoint will wait before sending acknowledgments โณ this value SHOULD include the receiver's expected delays in alarms firing. Like, if a receiver sets a timer for 5ms and alarms commonly fire up to 1ms late, then it should send a max_ack_delay of 6ms ๐Ÿ•ฐ๏ธ if it's absent, the default is 25 milliseconds. Values of 2^14 or greater are invalid ๐Ÿ™…โ€โ™€๏ธ

disable_active_migration (0x0c): this one shows up if the endpoint doesn't support active connection migration (Section 9) on the address that got used during the handshake ๐Ÿšซ๐Ÿƒโ€โ™€๏ธ an endpoint that receives it MUST NOT use a new local address when sending to the address the peer used during the handshake. It does NOT stop connection migration after a client has acted on a preferred_address transport parameter, so don't panic ๐Ÿ˜Œ it's a zero-length value ๐Ÿซง

preferred_address (0x0d): the server's preferred address is how the server changes its address at the end of the handshake, see Section 9.6 ๐Ÿก only a server sends this one. Servers MAY choose to send a preferred address for only one address family, by sending an all-zero address and port (0.0.0.0:0 or [::]:0) for the other family ๐ŸŒ IP addresses are encoded in network byte order ๐Ÿ”ข

This transport parameter has an address and port for both IPv4 and IPv6. The four-byte IPv4 Address field is followed by the matching two-byte IPv4 Port field. After that comes a 16-byte IPv6 Address field and a two-byte IPv6 Port field ๐Ÿงฉ then a Connection ID Length field tells you how long the Connection ID field after it is. Last of all, a 16-byte Stateless Reset Token field holds the stateless reset token that goes with the connection ID ๐Ÿ”‘ the format is shown in Figure 22 below ๐Ÿ‘‡

Preferred Address {
  IPv4 Address (32),
  IPv4 Port (16),
  IPv6 Address (128),
  IPv6 Port (16),
  Connection ID Length (8),
  Connection ID (..),
  Stateless Reset Token (128),
}

               Figure 22: Preferred Address Format

The Connection ID field and the Stateless Reset Token field hold an alternative connection ID with a sequence number of 1, see Section 5.1.1 ๐Ÿชช sending these alongside the preferred address means there's at least one unused active connection ID when the client starts migrating to the preferred address ๐Ÿ›ซ

The Connection ID and Stateless Reset Token fields of a preferred address are identical in syntax and semantics to the matching fields of a NEW_CONNECTION_ID frame (Section 19.15) ๐Ÿค A server that chooses a zero-length connection ID MUST NOT provide a preferred address ๐Ÿ™… and a server MUST NOT include a zero-length connection ID in this transport parameter either ๐Ÿ™…โ€โ™€๏ธ a client MUST treat any violation of these requirements as a connection error of type TRANSPORT_PARAMETER_ERROR ๐Ÿšจ no cap, this is serious ๐Ÿ˜ค

active_connection_id_limit (0x0e): an integer specifying the maximum number of connection IDs from the peer that an endpoint is willing to store ๐Ÿ—‚๏ธ this count includes the connection ID received during the handshake, the one received in the preferred_address transport parameter, and those received in NEW_CONNECTION_ID frames ๐Ÿง  The value of the active_connection_id_limit parameter MUST be at least 2. An endpoint that receives a value less than 2 MUST close the connection with an error of type TRANSPORT_PARAMETER_ERROR ๐Ÿ›‘ if this transport parameter is absent, the default is 2 ๐Ÿซง and if an endpoint issues a zero-length connection ID, it will never send a NEW_CONNECTION_ID frame, so it just ignores the active_connection_id_limit value it gets from its peer ๐Ÿ™ƒ

initial_source_connection_id (0x0f): this is the value the endpoint put in the Source Connection ID field of the first Initial packet it sends for the connection, see Section 7.3 ๐ŸŒฑ

retry_source_connection_id (0x10): this is the value the server put in the Source Connection ID field of a Retry packet, see Section 7.3 ๐Ÿ” only a server sends this one ๐Ÿซถ

If they're present, the transport parameters that set initial per-stream flow control limits (initial_max_stream_data_bidi_local, initial_max_stream_data_bidi_remote, and initial_max_stream_data_uni) work the same as sending a MAX_STREAM_DATA frame (Section 19.10) on every stream of the matching type right after it opens ๐ŸŒŠ if one of them is absent, streams of that type start with a flow control limit of 0 ๐Ÿงฑ

A client MUST NOT include any server-only transport parameter: original_destination_connection_id, preferred_address, retry_source_connection_id, or stateless_reset_token ๐Ÿ™…โ€โ™€๏ธ and a server MUST treat receiving any of these as a connection error of type TRANSPORT_PARAMETER_ERROR ๐Ÿ’ฅ this is giving strict but it's necessary ๐Ÿ’…


19. Frame Types and Formats ๐ŸŽ€

okay the frames are where the actual stuff happens ๐Ÿ”ฅ As described in Section 12.4, packets contain one or more frames ๐Ÿ“ฆ this section describes the format and semantics of the core QUIC frame types ๐Ÿ’œ

19.1. PADDING Frames ๐ŸŒธ

A PADDING frame (type=0x00) has no semantic value ๐Ÿซฅ but it's still super useful, because PADDING frames can be used to increase the size of a packet ๐Ÿ“ Padding can be used to increase an Initial packet to the minimum required size, or to provide protection against traffic analysis for protected packets ๐Ÿ•ต๏ธโ€โ™€๏ธ bestie, that's a whole vibe ๐Ÿฆ‹

PADDING frames are formatted as shown in Figure 23, which shows that PADDING frames have no content ๐Ÿ™ˆ a PADDING frame is literally just the single byte that identifies it as a PADDING frame ๐Ÿชถ

PADDING Frame {
  Type (i) = 0x00,
}

                 Figure 23: PADDING Frame Format

19.2. PING Frames ๐Ÿ“โœจ

Okay bestie, PING frames are how endpoints check that their peer is still alive or that they can actually reach it ๐Ÿ›ฐ๏ธ. The type is 0x01 and there is literally nothing else inside, just the type ๐Ÿ’….

Figure 24 shows the layout, and it's giving minimal:

PING Frame {
  Type (i) = 0x01,
}

Figure 24: PING Frame Format

All the receiver has to do is acknowledge the packet that carried the PING ๐Ÿซถ.

PING can also keep a connection from timing out when an app or app protocol wants that. It's like a little heartbeat ๐Ÿ’“ (see Section 10.1.2).


19.3. ACK Frames ๐Ÿ“ฌ

Receivers use ACK frames (types 0x02 and 0x03) to tell senders which packets they got and processed. Each ACK frame has one or more ACK Ranges, which are how it spells out exactly which packets were acknowledged ๐Ÿงพ. When the type is 0x03, the ACK frame also carries a running total of QUIC packets with ECN marks that have shown up on the connection so far ๐ŸŒˆ. QUIC implementations MUST properly handle both types, and if they've turned on ECN for the packets they send, they SHOULD use the ECN section's info to manage their congestion state. Bestie, that's a whole glow-up ๐Ÿ’–.

QUIC acknowledgments are irrevocable ๐Ÿšช. Once a packet is acknowledged, it stays acknowledged, even if it's missing from a later ACK frame. This is unlike reneging, which TCP Selective Acknowledgments (SACKs) [RFC2018] can do. Not the vibe ๐Ÿ™…โ€โ™€๏ธ.

Packets from different packet number spaces can share the same numeric value, so an acknowledgment needs to name both a packet number and a packet number space. QUIC handles this by making each ACK frame acknowledge only packet numbers in the same space as the packet the ACK frame is sitting in ๐Ÿงฉ.

Version Negotiation and Retry packets can't be acknowledged because they don't contain a packet number. Instead of ACK frames, these packets get implicitly acknowledged by the client's next Initial packet ๐ŸŒธ.

ACK frames are formatted as shown in Figure 25:

ACK Frame {
  Type (i) = 0x02..0x03,
  Largest Acknowledged (i),
  ACK Delay (i),
  ACK Range Count (i),
  First ACK Range (i),
  ACK Range (..) ...,
  [ECN Counts (..)],
}

Figure 25: ACK Frame Format

ACK frames have these fields:

  • Largest Acknowledged: A variable-length integer for the largest packet number the peer is acknowledging. This is usually the largest packet number the peer received before it made the ACK frame. Unlike the packet number in the QUIC long or short header, the value here is not truncated ๐Ÿ“.
  • ACK Delay: A variable-length integer for the acknowledgment delay in microseconds (see Section 13.2.5) โฑ๏ธ. You decode it by multiplying the value by 2 to the power of the ack_delay_exponent transport parameter that the ACK frame's sender set (see Section 18.2). Compared to just writing the delay as a plain integer, this encoding fits a bigger range of values in the same number of bytes, but you give up some resolution. Trade-offs, babes ๐ŸŽ€.
  • ACK Range Count: A variable-length integer for how many ACK Range fields the frame has ๐Ÿ”ข.
  • First ACK Range: A variable-length integer for how many contiguous packets right before the Largest Acknowledged are being acknowledged too. So the smallest packet acknowledged in that range is the Largest Acknowledged minus the First ACK Range value ๐Ÿงฎ.
  • ACK Ranges: Extra ranges that alternate between not acknowledged (Gap) and acknowledged (ACK Range). See Section 19.3.1 ๐Ÿฆ‹.
  • ECN Counts: The three ECN counts. See Section 19.3.2 ๐Ÿ’œ.

19.3.1. ACK Ranges

Each ACK Range is a pair of alternating Gap and ACK Range Length values, going in descending packet number order. ACK Ranges can be repeated ๐Ÿ”. The ACK Range Count field tells you how many Gap and ACK Range Length values there are, and there's one of each for every value in that count.

ACK Ranges are structured as shown in Figure 26:

ACK Range {
  Gap (i),
  ACK Range Length (i),
}

Figure 26: ACK Ranges

The fields that make up each ACK Range are:

  • Gap: A variable-length integer for how many contiguous unacknowledged packets come right before the packet number one lower than the smallest one in the previous ACK Range ๐Ÿ•ณ๏ธ.
  • ACK Range Length: A variable-length integer for how many contiguous acknowledged packets come right before the largest packet number, as set by the Gap before it ๐Ÿ“ฆ.

Gap and ACK Range Length use a relative integer encoding to save space โœจ. Each encoded value is positive, but the values get subtracted, so each ACK Range describes packets with lower and lower numbers. Chef's kiss ๐Ÿ‘ฉโ€๐Ÿณ.

Each ACK Range acknowledges a contiguous run of packets by saying how many acknowledged packets come before the largest packet number in that run. A value of 0 means only the largest packet number is acknowledged 1๏ธโƒฃ. Bigger ACK Range values mean a bigger range, which means the smallest packet number in it is lower. So, given the largest packet number for a range, you get the smallest with this formula:

smallest = largest - ack_range

An ACK Range acknowledges every packet between the smallest and the largest packet numbers, inclusive ๐Ÿ’ฏ.

To find the largest value for an ACK Range, you cumulatively subtract the size of all the ACK Range Lengths and Gaps before it ๐Ÿง .

Each Gap is a stretch of packets that are not being acknowledged. The number of packets in the gap is one more than the encoded Gap value ๐Ÿ™ˆ.

The Gap value sets the largest packet number for the next ACK Range, using this formula:

largest = previous_smallest - gap - 2

If any computed packet number comes out negative, an endpoint MUST generate a connection error of type FRAME_ENCODING_ERROR ๐Ÿšจ.

19.3.2. ECN Counts

The ACK frame uses the least significant bit of the type value (so, type 0x03) to signal ECN feedback. That's how it reports receipt of QUIC packets with ECN codepoints of ECT(0), ECT(1), or ECN-CE in their IP header ๐ŸŒก๏ธ. ECN counts only show up when the ACK frame type is 0x03.

When they're there, there are three ECN counts, shown in Figure 27:

ECN Counts {
  ECT0 Count (i),
  ECT1 Count (i),
  ECN-CE Count (i),
}

Figure 27: ECN Count Format

The ECN count fields are:

  • ECT0 Count: A variable-length integer for the total number of packets received with the ECT(0) codepoint, in the packet number space of the ACK frame ๐ŸŸฆ.
  • ECT1 Count: A variable-length integer for the total number of packets received with the ECT(1) codepoint, in the packet number space of the ACK frame ๐ŸŸช.
  • ECN-CE Count: A variable-length integer for the total number of packets received with the ECN-CE codepoint, in the packet number space of the ACK frame ๐ŸŸฅ.

ECN counts are kept separately for each packet number space ๐Ÿ’….


19.4. RESET_STREAM Frames ๐Ÿ›‘

An endpoint uses a RESET_STREAM frame (type=0x04) to abruptly end the sending side of a stream ๐Ÿ’ฅ.

After sending a RESET_STREAM, the endpoint stops sending and resending STREAM frames on that stream. A receiver of RESET_STREAM can throw out any data it already got on that stream ๐Ÿ—‘๏ธ.

An endpoint that receives a RESET_STREAM frame for a send-only stream MUST terminate the connection with error STREAM_STATE_ERROR. Big no-no ๐Ÿ™…โ€โ™€๏ธ.

RESET_STREAM frames are formatted as shown in Figure 28:

RESET_STREAM Frame {
  Type (i) = 0x04,
  Stream ID (i),
  Application Protocol Error Code (i),
  Final Size (i),
}

Figure 28: RESET_STREAM Frame Format

RESET_STREAM frames have these fields:

  • Stream ID: A variable-length integer for the ID of the stream being terminated ๐ŸŽฏ.
  • Application Protocol Error Code: A variable-length integer with the application protocol error code (see Section 20.2) that says why the stream is closing ๐Ÿงพ.
  • Final Size: A variable-length integer for the final size of the stream, as set by the RESET_STREAM sender, in bytes (see Section 4.5) ๐Ÿ“.

19.5. STOP_SENDING Frames ๐Ÿ™…โ€โ™€๏ธ

An endpoint uses a STOP_SENDING frame (type=0x05) to say that incoming data is getting thrown away when it arrives, because the application asked for that ๐Ÿšซ. STOP_SENDING asks the peer to stop transmitting on a stream.

A STOP_SENDING frame can be sent for streams in the "Recv" or "Size Known" states (see Section 3.2). Receiving a STOP_SENDING frame for a locally initiated stream that hasn't been created yet MUST be treated as a connection error of type STREAM_STATE_ERROR. An endpoint that receives a STOP_SENDING frame for a receive-only stream MUST terminate the connection with error STREAM_STATE_ERROR. Yikes ๐Ÿ˜ญ.

STOP_SENDING frames are formatted as shown in Figure 29:

STOP_SENDING Frame {
  Type (i) = 0x05,
  Stream ID (i),
  Application Protocol Error Code (i),
}

Figure 29: STOP_SENDING Frame Format

STOP_SENDING frames have these fields:

  • Stream ID: A variable-length integer carrying the ID of the stream being ignored ๐Ÿ”•.
  • Application Protocol Error Code: A variable-length integer with the application-specified reason the sender is ignoring the stream (see Section 20.2) ๐Ÿ’ฌ.

19.6. CRYPTO Frames ๐Ÿ”

A CRYPTO frame (type=0x06) carries cryptographic handshake messages ๐Ÿค. It can go in all packet types except 0-RTT. The CRYPTO frame gives the cryptographic protocol an in-order stream of bytes. CRYPTO frames work basically like STREAM frames, with a few differences: they don't have a stream identifier, they aren't flow controlled, and they don't have markers for optional offset, optional length, or the end of the stream ๐Ÿงƒ.

CRYPTO frames are formatted as shown in Figure 30:

CRYPTO Frame {
  Type (i) = 0x06,
  Offset (i),
  Length (i),
  Crypto Data (..),
}

Figure 30: CRYPTO Frame Format

CRYPTO frames have these fields:

  • Offset: A variable-length integer for the byte offset in the stream where the data in this CRYPTO frame starts ๐Ÿ“.
  • Length: A variable-length integer for the length of the Crypto Data field in this CRYPTO frame ๐Ÿ“.
  • Crypto Data: The cryptographic message data ๐ŸŽ.

Each encryption level has its own separate flow of cryptographic handshake data, and each one starts at an offset of 0 ๐ŸŒฑ. So each encryption level is treated as its own CRYPTO stream of data. Separate lanes, no mixing ๐Ÿ›ฃ๏ธ.

The largest offset delivered on a stream (the offset plus the data length) cannot go over 2^62-1. Receiving a frame that goes past that limit MUST be treated as a connection error of type FRAME_ENCODING_ERROR or CRYPTO_BUFFER_EXCEEDED. Way too big, periodt ๐Ÿšซ.

Unlike STREAM frames, which include a stream ID saying which stream the data belongs to, a CRYPTO frame carries data for a single stream per encryption level. That stream has no explicit end, so CRYPTO frames don't have a FIN bit ๐ŸŒ™. It's giving open-ended, and honestly iconic ๐Ÿ’œ.

19.7. NEW_TOKEN Frames ๐ŸŽŸ๏ธ

okay so a server sends a NEW_TOKEN frame (type=0x07) to give the client a token it can keep and send in the header of an Initial packet for a future connection ๐ŸŽซโœจ basically a little ticket for next time, bestie ๐Ÿ’–

NEW_TOKEN frames are formatted like this ๐Ÿ‘€

NEW_TOKEN Frame {
  Type (i) = 0x07,
  Token Length (i),
  Token (..),
}

Figure 31: NEW_TOKEN Frame Format

NEW_TOKEN frames have these fields:

  • Token Length: a variable-length integer for how many bytes the token is ๐Ÿ“
  • Token: an opaque blob the client can use with a future Initial packet ๐Ÿซถ The token MUST NOT be empty ๐Ÿ™…โ€โ™€๏ธ and if a client gets a NEW_TOKEN frame with an empty Token field, it MUST treat that as a connection error of type FRAME_ENCODING_ERROR ๐Ÿ’€

ngl this part is kinda funny: a client might get the same token twice if packets with the frame get marked as lost by mistake ๐Ÿ˜ญ so clients are on the hook for throwing out duplicate values, since those could be used to link connection attempts (see Section 8.1.3) ๐Ÿ•ต๏ธโ€โ™€๏ธ

Clients MUST NOT send NEW_TOKEN frames ๐Ÿšซ and a server MUST treat receiving one as a connection error of type PROTOCOL_VIOLATION ๐Ÿ™… that's a whole no-go, periodt ๐Ÿ’…

19.8. STREAM Frames ๐ŸŒŠ

STREAM frames do two jobs at once: they implicitly create a stream and they carry the stream's data ๐ŸŽ€ The Type field takes the form 0b00001XXX (or any value from 0x08 to 0x0f), and the three low-order bits decide which fields show up in the frame ๐Ÿ‘‘

  • The OFF bit (0x04) says whether an Offset field is there. When it's set to 1, the Offset field is present. When it's set to 0, the Offset field is absent and the Stream Data starts at offset 0. That means the frame has the very first bytes of the stream, or it's the end of a stream that has no data at all ๐ŸŒธ
  • The LEN bit (0x02) says whether a Length field is there. If it's set to 0, there's no Length field and the Stream Data just runs to the end of the packet. If it's set to 1, the Length field is present ๐Ÿ“ฆ
  • The FIN bit (0x01) marks the end of the stream ๐Ÿ The final size of the stream is the offset plus the length of this frame, so the math is literally offset + length ๐Ÿงฎ

An endpoint MUST terminate the connection with error STREAM_STATE_ERROR if it gets a STREAM frame for a locally initiated stream that hasn't been created yet, or for a send-only stream ๐Ÿ™ˆ no sneaky data there, bestie

STREAM frames are formatted like this ๐Ÿ‘€

STREAM Frame {
  Type (i) = 0x08..0x0f,
  Stream ID (i),
  [Offset (i)],
  [Length (i)],
  Stream Data (..),
}

Figure 32: STREAM Frame Format

STREAM frames have these fields:

  • Stream ID: the stream ID, see Section 2.1 ๐Ÿ†”
  • Offset: a variable-length integer for the byte offset in the stream where this data goes. It's only there when the OFF bit is set to 1, and when it's missing the offset is 0 ๐Ÿงญ
  • Length: a variable-length integer for how long the Stream Data field is. It's only there when the LEN bit is set to 1. When the LEN bit is 0, Stream Data eats all the remaining bytes in the packet ๐Ÿฐ
  • Stream Data: the bytes from that stream that get delivered ๐Ÿ’Œ

When Stream Data has a length of 0, the offset in the STREAM frame is the offset of the next byte that would be sent โœจ the first byte in a stream is at offset 0 ๐Ÿฅฐ

Now the limit: the largest offset delivered on a stream (offset plus data length) can't go past 2^62-1, because it's impossible to give flow control credit for that data ๐Ÿ˜ฑ if a frame goes over that, it MUST be treated as a connection error of type FRAME_ENCODING_ERROR or FLOW_CONTROL_ERROR ๐Ÿšจ

19.9. MAX_DATA Frames ๐Ÿšฐ

A MAX_DATA frame (type=0x10) is part of flow control. It tells the peer the maximum amount of data that can be sent on the connection as a whole ๐Ÿซง

MAX_DATA frames are formatted like this ๐Ÿ‘€

MAX_DATA Frame {
  Type (i) = 0x10,
  Maximum Data (i),
}

Figure 33: MAX_DATA Frame Format

MAX_DATA frames have this field:

  • Maximum Data: a variable-length integer for the maximum amount of data that can be sent on the entire connection, in bytes ๐Ÿ“Š

Every byte sent in STREAM frames counts toward this limit ๐Ÿง The sum of the final sizes on all streams, including streams in terminal states, MUST NOT exceed the value a receiver advertised ๐Ÿ›‘ And if an endpoint gets more data than the maximum data value it has sent, it MUST terminate the connection with an error of type FLOW_CONTROL_ERROR ๐Ÿ˜ค That includes breaking remembered limits in Early Data (see Section 7.4.1) ๐Ÿ•ฐ๏ธ

19.10. MAX_STREAM_DATA Frames ๐ŸŒท

A MAX_STREAM_DATA frame (type=0x11) is also flow control, but this one is per stream: it tells a peer the maximum amount of data that can be sent on one stream ๐Ÿฆ‹

A MAX_STREAM_DATA frame can be sent for streams in the "Recv" state (see Section 3.2). If someone gets a MAX_STREAM_DATA frame for a locally initiated stream that hasn't been created yet, that MUST be treated as a connection error of type STREAM_STATE_ERROR ๐Ÿ˜ต And an endpoint that gets a MAX_STREAM_DATA frame for a receive-only stream MUST terminate the connection with error STREAM_STATE_ERROR ๐Ÿ™…โ€โ™€๏ธ

MAX_STREAM_DATA frames are formatted like this ๐Ÿ‘€

MAX_STREAM_DATA Frame {
  Type (i) = 0x11,
  Stream ID (i),
  Maximum Stream Data (i),
}

Figure 34: MAX_STREAM_DATA Frame Format

MAX_STREAM_DATA frames have these fields:

  • Stream ID: the ID of the affected stream, encoded as a variable-length integer ๐Ÿ†”
  • Maximum Stream Data: a variable-length integer for the maximum amount of data that can be sent on that stream, in bytes ๐ŸŽฏ

When counting data toward this limit, an endpoint looks at the largest received offset of data that's sent or received on the stream ๐Ÿ‘€ Loss or reordering can make that largest offset bigger than the total data actually received, and receiving STREAM frames might not bump the largest offset at all ๐ŸŒ€ that's just how it goes, bestie

The data sent on a stream MUST NOT exceed the largest maximum stream data value the receiver advertised ๐Ÿšซ If an endpoint gets more data than the largest maximum stream data it has sent for that stream, it MUST terminate the connection with an error of type FLOW_CONTROL_ERROR ๐Ÿ’ฅ this includes violations of remembered limits in Early Data (see Section 7.4.1) ๐Ÿ•ฐ๏ธ

19.11. MAX_STREAMS Frames ๐ŸŒˆ

A MAX_STREAMS frame (type=0x12 or 0x13) tells the peer the cumulative number of streams of a given type it's allowed to open ๐Ÿšช A type of 0x12 applies to bidirectional streams, and a type of 0x13 applies to unidirectional streams โ†”๏ธโžก๏ธ

MAX_STREAMS frames are formatted like this ๐Ÿ‘€

MAX_STREAMS Frame {
  Type (i) = 0x12..0x13,
  Maximum Streams (i),
}

Figure 35: MAX_STREAMS Frame Format

MAX_STREAMS frames have this field:

  • Maximum Streams: a count of the cumulative number of streams of that type that can be opened over the connection's whole lifetime ๐ŸŽˆ This value cannot exceed 2^60, since stream IDs can't be encoded above 2^62-1. If a frame allows opening a stream bigger than that limit, it MUST be treated as a connection error of type FRAME_ENCODING_ERROR ๐Ÿ˜ฑ

Loss or reordering can make an endpoint get a MAX_STREAMS frame with a lower limit than one it already got ๐ŸŒ€ MAX_STREAMS frames that don't increase the stream limit MUST be ignored ๐Ÿ™…โ€โ™€๏ธ no backsies

An endpoint MUST NOT open more streams than its peer's current stream limit allows ๐Ÿงฑ For example, a server that gets a unidirectional stream limit of 3 can open streams 3, 7, and 11, but not stream 15 ๐Ÿ”ข If a peer opens more streams than it was permitted, the endpoint MUST terminate the connection with an error of type STREAM_LIMIT_ERROR ๐Ÿšจ this also covers violations of remembered limits in Early Data (see Section 7.4.1).

Note that these frames (and the matching transport parameters) don't count how many streams can be open at the same time ๐Ÿ™… the limit includes streams that have already closed as well as the ones that are open right now ๐Ÿซ 

19.12. DATA_BLOCKED Frames ๐Ÿงฑ

A sender SHOULD send a DATA_BLOCKED frame (type=0x14) when it wants to send data but can't, because of connection-level flow control (see Section 4) ๐Ÿฅบ DATA_BLOCKED frames can be used as input for tuning flow control algorithms (see Section 4.2) ๐Ÿ”ง

DATA_BLOCKED frames are formatted like this ๐Ÿ‘€

DATA_BLOCKED Frame {
  Type (i) = 0x14,
  Maximum Data (i),
}

Figure 36: DATA_BLOCKED Frame Format

DATA_BLOCKED frames have this field:

  • Maximum Data: a variable-length integer for the connection-level limit at which the blocking happened ๐Ÿšง

19.13. STREAM_DATA_BLOCKED Frames ๐Ÿšง

A sender SHOULD send a STREAM_DATA_BLOCKED frame (type=0x15) when it wants to send data but can't, because of stream-level flow control ๐Ÿ’… This frame is the stream version of DATA_BLOCKED (Section 19.12) ๐Ÿ‘ฏโ€โ™€๏ธ

An endpoint that gets a STREAM_DATA_BLOCKED frame for a send-only stream MUST terminate the connection with error STREAM_STATE_ERROR ๐Ÿ™ˆ

STREAM_DATA_BLOCKED frames are formatted like this ๐Ÿ‘€

STREAM_DATA_BLOCKED Frame {
  Type (i) = 0x15,
  Stream ID (i),
  Maximum Stream Data (i),
}

Figure 37: STREAM_DATA_BLOCKED Frame Format

STREAM_DATA_BLOCKED frames have these fields:

  • Stream ID: a variable-length integer for the stream that's blocked by flow control ๐Ÿ†”
  • Maximum Stream Data: a variable-length integer for the offset of the stream where the blocking happened ๐Ÿ“

19.14. STREAMS_BLOCKED Frames ๐Ÿšฆ

A sender SHOULD send a STREAMS_BLOCKED frame (type=0x16 or 0x17) when it wants to open a stream but can't, because the peer's maximum stream limit is in the way (see Section 19.11) ๐Ÿ™ƒ A type of 0x16 means the bidirectional stream limit was reached, and a type of 0x17 means the unidirectional stream limit was reached ๐ŸŽ€

A STREAMS_BLOCKED frame doesn't open the stream ๐Ÿ™… it just tells the peer that a new stream was needed and the stream limit stopped it from being created ๐Ÿซถ

STREAMS_BLOCKED frames are formatted like this ๐Ÿ‘€

STREAMS_BLOCKED Frame {
  Type (i) = 0x16..0x17,
  Maximum Streams (i),
}

Figure 38: STREAMS_BLOCKED Frame Format

STREAMS_BLOCKED frames have this field:

  • Maximum Streams: a variable-length integer for the maximum number of streams allowed at the time the frame was sent. This value cannot exceed 2^60, since stream IDs can't be encoded above 2^62-1. If a frame encodes a larger stream ID, it MUST be treated as a connection error of type STREAM_LIMIT_ERROR or FRAME_ENCODING_ERROR ๐Ÿ’€

19.15. NEW_CONNECTION_ID Frames ๐Ÿฆ‹

okay bestie, so the NEW_CONNECTION_ID frame (type=0x18) is how an endpoint hands its peer a stash of alternative connection IDs ๐Ÿซถ Those are there to break linkability when you migrate a connection, so check out Section 9.5 for the deets ๐Ÿ’…

here's the layout, as shown in Figure 39 ๐ŸŽ€

NEW_CONNECTION_ID Frame {
  Type (i) = 0x18,
  Sequence Number (i),
  Retire Prior To (i),
  Length (8),
  Connection ID (8..160),
  Stateless Reset Token (128),
}
             Figure 39: NEW_CONNECTION_ID Frame Format

so the fields are:

  • Sequence Number: the sequence number the sender gave this connection ID, encoded as a variable-length integer. see Section 5.1.1 โœจ
  • Retire Prior To: a variable-length integer telling you which connection IDs should get retired. see Section 5.1.2 ๐Ÿ“
  • Length: an 8-bit unsigned integer with the length of the connection ID. values less than 1 and greater than 20 are invalid and MUST be treated as a connection error of type FRAME_ENCODING_ERROR ๐Ÿ˜ญ
  • Connection ID: a connection ID of the length you just read ๐ŸŒˆ
  • Stateless Reset Token: a 128-bit value that gets used for a stateless reset whenever the matching connection ID is used. see Section 10.3 ๐Ÿงš

now the rules, and they are strict ๐Ÿ‘‘

An endpoint MUST NOT send this frame if it currently needs its peer to send packets with a zero-length Destination Connection ID. changing a connection ID to or from zero length makes it hard to tell when the value changed, so no cap, don't do it ๐Ÿ™ˆ An endpoint that is sending packets with a zero-length Destination Connection ID MUST treat receipt of a NEW_CONNECTION_ID frame as a connection error of type PROTOCOL_VIOLATION ๐Ÿ’€

retries happen! transmission errors, timeouts, and retransmissions can make the same NEW_CONNECTION_ID frame show up more than once. Receipt of the same frame multiple times MUST NOT be treated as a connection error ๐Ÿซถ the receiver can use the sequence number in the frame to handle the repeat like a champ โœจ

if an endpoint gets a NEW_CONNECTION_ID frame that repeats a connection ID it already handed out but with a different Stateless Reset Token or a different Sequence Number, or if a sequence number ends up being used for different connection IDs, the endpoint MAY treat that receipt as a connection error of type PROTOCOL_VIOLATION ๐Ÿ”ฅ

the Retire Prior To field applies to connection IDs set up during connection setup and to the preferred_address transport parameter. see Section 5.1.2 ๐Ÿฆ‹ The value in the Retire Prior To field MUST be less than or equal to the value in the Sequence Number field. if you receive a Retire Prior To value greater than the Sequence Number, that MUST be treated as a connection error of type FRAME_ENCODING_ERROR ๐Ÿ˜ญ

once a sender has indicated a Retire Prior To value, smaller values in later NEW_CONNECTION_ID frames have no effect. A receiver MUST ignore any Retire Prior To fields that do not increase the largest received Retire Prior To value ๐Ÿ’…

finally, if an endpoint receives a NEW_CONNECTION_ID frame with a sequence number smaller than the Retire Prior To field of a NEW_CONNECTION_ID frame it already received, it MUST send a matching RETIRE_CONNECTION_ID frame that retires the newly received connection ID, unless it already did that for that sequence number ๐ŸŽ€


19.16. RETIRE_CONNECTION_ID Frames ๐ŸŒธ

this one is an endpoint saying "bye, I'm done using this connection ID." the frame is type=0x19, and it covers any connection ID its peer issued, including the one from the handshake ๐Ÿงš Sending a RETIRE_CONNECTION_ID frame also works as a request for the peer to send more connection IDs for later use, see Section 5.1 ๐Ÿ’œ New connection IDs can be delivered to a peer with the NEW_CONNECTION_ID frame (Section 19.15) โœจ

retiring a connection ID also kills the stateless reset token that came with it ๐Ÿ’€

here's the format, as shown in Figure 40 ๐ŸŽ€

RETIRE_CONNECTION_ID Frame {
  Type (i) = 0x19,
  Sequence Number (i),
}
                Figure 40: RETIRE_CONNECTION_ID Frame Format

the one field is:

  • Sequence Number: the sequence number of the connection ID being retired. see Section 5.1.2 ๐Ÿ“

the rules: Receipt of a RETIRE_CONNECTION_ID frame with a sequence number greater than any the peer has been sent MUST be treated as a connection error of type PROTOCOL_VIOLATION ๐Ÿ˜ญ

The sequence number in a RETIRE_CONNECTION_ID frame MUST NOT refer to the Destination Connection ID field of the packet that carries the frame. the peer MAY treat that as a connection error of type PROTOCOL_VIOLATION ๐Ÿ™ˆ

and the zero-length case is its own vibe ๐ŸŒˆ An endpoint cannot send this frame if its peer gave it a zero-length connection ID. An endpoint that provides a zero-length connection ID MUST treat receipt of a RETIRE_CONNECTION_ID frame as a connection error of type PROTOCOL_VIOLATION ๐Ÿ”ฅ


19.17. PATH_CHALLENGE Frames ๐Ÿฆ‹

endpoints use PATH_CHALLENGE frames (type=0x1a) to check that the peer is reachable and to validate a path during connection migration ๐Ÿงš

the layout, from Figure 41 ๐ŸŽ€

PATH_CHALLENGE Frame {
  Type (i) = 0x1a,
  Data (64),
}
                   Figure 41: PATH_CHALLENGE Frame Format

the one field is:

  • Data: this 8-byte field carries arbitrary data ๐Ÿ’…

why 64 bits? because it's basically a secret handshake ๐Ÿซถ Including 64 bits of entropy in a PATH_CHALLENGE frame makes it way easier to receive the packet than to guess the value correctly ๐Ÿ‘‘

the recipient of this frame MUST generate a PATH_RESPONSE frame (Section 19.18) containing the same Data value ๐Ÿ’–


19.18. PATH_RESPONSE Frames ๐ŸŒˆ

a PATH_RESPONSE frame (type=0x1b) is what gets sent back when a PATH_CHALLENGE frame shows up ๐Ÿฆ‹

the format is in Figure 42. It's identical to PATH_CHALLENGE, see Section 19.17 ๐Ÿ’…

PATH_RESPONSE Frame {
  Type (i) = 0x1b,
  Data (64),
}
                   Figure 42: PATH_RESPONSE Frame Format

if a PATH_RESPONSE frame's content doesn't match a PATH_CHALLENGE frame the endpoint sent earlier, the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION ๐Ÿ™ˆ


19.19. CONNECTION_CLOSE Frames ๐Ÿ’€

an endpoint sends a CONNECTION_CLOSE frame (type=0x1c or 0x1d) to tell its peer the connection is closing ๐Ÿ˜ญ Type 0x1c is for errors at only the QUIC layer, or for no errors at all (with the NO_ERROR code). Type 0x1d is for errors coming from the application that uses QUIC ๐ŸŽ€

if there are open streams that were never explicitly closed, they get implicitly closed when the connection closes ๐Ÿซถ

the layout is in Figure 43 ๐ŸŒธ

CONNECTION_CLOSE Frame {
  Type (i) = 0x1c..0x1d,
  Error Code (i),
  [Frame Type (i)],
  Reason Phrase Length (i),
  Reason Phrase (..),
}
                  Figure 43: CONNECTION_CLOSE Frame Format

the fields:

  • Error Code: a variable-length integer for why the connection is closing. A type 0x1c frame uses codes from the space defined in Section 20.1. A type 0x1d frame uses codes defined by the application protocol, see Section 20.2 โœจ
  • Frame Type: a variable-length integer naming the type of frame that caused the error. A value of 0 (same as the PADDING frame) is used when the frame type is unknown. The application-specific variant (type 0x1d) doesn't include this field ๐Ÿงš
  • Reason Phrase Length: a variable-length integer with the length of the reason phrase in bytes. A CONNECTION_CLOSE frame can't be split between packets, so any packet size limits also limit how much room there is for a reason phrase ๐Ÿ“
  • Reason Phrase: extra diagnostic info about the closure. It can be zero length if the sender doesn't want to say more than the Error Code. This SHOULD be a UTF-8 encoded string [RFC3629], though the frame doesn't carry things like language tags that would help anyone except the entity that wrote the text understand it ๐ŸŒˆ

the app-specific version (type 0x1d) can only be sent in 0-RTT or 1-RTT packets, see Section 12.5 ๐Ÿ”ฅ And if an app wants to bail on a connection during the handshake, an endpoint can send a type 0x1c CONNECTION_CLOSE frame with an error code of APPLICATION_ERROR in an Initial or Handshake packet ๐Ÿ™ˆ


19.20. HANDSHAKE_DONE Frames ๐Ÿ‘‘

the server uses a HANDSHAKE_DONE frame (type=0x1e) to confirm the handshake to the client ๐Ÿ’–

this one is simple because it has no content at all, as shown in Figure 44 ๐Ÿฆ‹

HANDSHAKE_DONE Frame {
  Type (i) = 0x1e,
}
                   Figure 44: HANDSHAKE_DONE Frame Format

the rules are short and sweet ๐Ÿ’… A HANDSHAKE_DONE frame can only be sent by the server. Servers MUST NOT send a HANDSHAKE_DONE frame before completing the handshake. A server MUST treat receipt of a HANDSHAKE_DONE frame as a connection error of type PROTOCOL_VIOLATION โœจ


19.21. Extension Frames ๐Ÿงš

QUIC frames don't use a self-describing encoding ๐ŸŒธ So an endpoint has to understand the syntax of every frame before it can process a packet. That's what makes frame encoding efficient, but it also means an endpoint can't send a frame type its peer doesn't know ๐Ÿ’€

an extension that wants a new kind of frame MUST first make sure the peer can understand that frame ๐Ÿซถ An endpoint can use a transport parameter to say it's willing to receive extension frame types. One transport parameter can indicate support for one or more extension frame types ๐ŸŽ€

extensions that change or replace core functionality (including frame types) are hard to combine with other extensions that touch the same functionality, unless the behavior of the combination is explicitly defined ๐Ÿ˜ญ Such extensions SHOULD define how they interact with previously defined extensions that modify the same protocol components ๐Ÿ‘‘

extension frames MUST be congestion controlled and MUST cause an ACK frame to be sent ๐Ÿ”ฅ The exception is extension frames that replace or supplement the ACK frame. Extension frames aren't included in flow control unless the extension says so ๐ŸŒˆ

an IANA registry manages the assignment of frame types, see Section 22.4 ๐Ÿ“


20. Error Codes ๐Ÿ’œ

QUIC transport error codes and application error codes are 62-bit unsigned integers โœจ

20.1. Transport Error Codes โœจ

okay so this section is the official list of QUIC transport error codes ๐Ÿ’… bestie, these are the ones you can send in a CONNECTION_CLOSE frame with a type of 0x1c. Every one of them applies to the whole connection, not just one stream ๐Ÿฆ‹

  • NO_ERROR (0x00): an endpoint uses this with CONNECTION_CLOSE to say it's closing the connection abruptly, but there's no actual error ๐Ÿซถ
  • INTERNAL_ERROR (0x01): the endpoint hit an internal error and can't keep going with the connection ๐Ÿ˜ญ
  • CONNECTION_REFUSED (0x02): the server said no to a new connection ๐Ÿšซ
  • FLOW_CONTROL_ERROR (0x03): the endpoint got more data than it allowed in its advertised data limits. See Section 4 ๐Ÿ’œ
  • STREAM_LIMIT_ERROR (0x04): the endpoint got a frame for a stream ID that went past its advertised stream limit for that stream type ๐ŸŒธ
  • STREAM_STATE_ERROR (0x05): a frame showed up for a stream that wasn't in a state that allowed that frame. See Section 3 ๐ŸŽ€
  • FINAL_SIZE_ERROR (0x06): this one has three flavors ๐Ÿ’€
    1. an endpoint got a STREAM frame with data past the final size that was already established
    2. an endpoint got a STREAM frame or a RESET_STREAM frame with a final size lower than the stream data it already received
    3. an endpoint got a STREAM frame or a RESET_STREAM frame with a final size that's different from the one already established
  • FRAME_ENCODING_ERROR (0x07): a frame was badly formatted. Like, a frame of an unknown type, or an ACK frame with more acknowledgment ranges than the rest of the packet could fit ๐Ÿ™ˆ
  • TRANSPORT_PARAMETER_ERROR (0x08): the endpoint got transport parameters that were badly formatted, had an invalid value, left out a mandatory transport parameter, included a forbidden one, or were just wrong in some other way ๐Ÿ“
  • CONNECTION_ID_LIMIT_ERROR (0x09): the peer gave more connection IDs than the advertised active_connection_id_limit ๐Ÿงš
  • PROTOCOL_VIOLATION (0x0a): the endpoint spotted a protocol compliance error that didn't fit any of the more specific codes โญ
  • INVALID_TOKEN (0x0b): a server got a client Initial with an invalid Token field ๐Ÿ”ฅ
  • APPLICATION_ERROR (0x0c): the application or application protocol is the reason the connection got closed ๐Ÿ‘‘
  • CRYPTO_BUFFER_EXCEEDED (0x0d): the endpoint got more data in CRYPTO frames than it can buffer ๐ŸŒˆ
  • KEY_UPDATE_ERROR (0x0e): the endpoint ran into errors while doing key updates. See Section 6 of [QUIC-TLS] ๐Ÿ”‘
  • AEAD_LIMIT_REACHED (0x0f): the endpoint hit the confidentiality or integrity limit for the AEAD algorithm used on this connection ๐Ÿšจ
  • NO_VIABLE_PATH (0x10): the endpoint decided the network path can't support QUIC. Honestly, an endpoint probably won't get a CONNECTION_CLOSE frame with this code unless the path can't handle a big enough MTU ๐Ÿ›ค๏ธ
  • CRYPTO_ERROR (0x0100-0x01ff): the cryptographic handshake failed ๐Ÿฅ€ a whole range of 256 values is reserved for crypto-handshake-specific errors. If TLS is doing the handshake, check Section 4.8 of [QUIC-TLS] for those codes ๐Ÿ”

Want to register your own error codes? See Section 22.5 ๐Ÿ’…

Okay, but here's the fun part, the principles behind all of this ๐Ÿง  Error conditions that might need a specific action from the recipient get their own unique code. Common conditions get specific codes too. If neither of those applies, the code just names a general part of the stack, like flow control or transport parameter handling. And finally, generic errors exist for when an implementation can't or won't use a more specific code. It's giving organized ๐ŸŽ€

20.2. Application Protocol Error Codes โœจ

The application protocol gets to decide its own error codes ๐Ÿฆ‹ Those are used in three places: the RESET_STREAM frame (Section 19.4), the STOP_SENDING frame (Section 19.5), and the CONNECTION_CLOSE frame when its type is 0x1d (Section 19.19) ๐Ÿ’œ


21. Security Considerations ๐Ÿ”

okay this is the serious part, bestie ๐Ÿ˜ญ The big goal of QUIC is to give you a secure transport connection. Section 21.1 gives an overview of those properties. The sections after it talk about the constraints and caveats around those properties, including known attacks and how to counter them ๐Ÿ›ก๏ธ

21.1. Overview of Security Properties โœจ

ngl, a full security analysis of QUIC is out of scope for this document ๐Ÿ’€ This section is just an informal description of the security properties we want, to help implementers and guide protocol analysis ๐Ÿง

QUIC assumes the threat model described in [SEC-CONS] and protects against a lot of the attacks that come out of that model ๐Ÿซถ

Attacks get split into two groups: passive and active. Passive attackers can read packets off the network. Active attackers can also write packets into the network ๐Ÿ‘€ A passive attack could still involve an attacker who causes a routing change, or some other change in the path packets take through a connection ๐Ÿ›ค๏ธ

Attackers are also either on-path or off-path. An on-path attacker can read, change, or remove any packet it sees, so the packet never reaches its destination. An off-path attacker can see the packets but can't stop the original packet from getting where it's going. Both types can also send any packets they want ๐Ÿ™ƒ This definition is different from the one in Section 3.5 of [SEC-CONS] because an off-path attacker is able to observe packets ๐Ÿฆ‹

Properties of the handshake, protected packets, and connection migration each get looked at separately โœจ

21.1.1. Handshake ๐Ÿค

The QUIC handshake bakes in the TLS 1.3 handshake and inherits the cryptographic properties from Appendix E.1 of [TLS13] ๐Ÿ’œ A lot of QUIC's security depends on the TLS handshake doing its job. Any attack on TLS could hit QUIC too ๐Ÿ˜ญ

Any attack on the TLS handshake that messes with the secrecy or uniqueness of session keys, or with the authentication of the peers, also hurts the other guarantees QUIC gets from those keys. For example, migration (Section 9) relies on confidentiality protections working, both for negotiating keys in the TLS handshake and for QUIC packet protection. That's how you avoid linkability across network paths ๐Ÿ”—

An attack on the integrity of the TLS handshake might let an attacker change which application protocol or QUIC version gets picked ๐Ÿ™ˆ

On top of what TLS provides, the QUIC handshake also gives some defense against DoS attacks on the handshake ๐Ÿ›ก๏ธ

21.1.1.1. Anti-Amplification ๐Ÿšซ

Address validation (Section 8) checks that an entity claiming a given address can actually receive packets there. That limits amplification attack targets to addresses where an attacker can observe packets ๐Ÿ‘€

Before address validation, endpoints are limited in what they can send. An endpoint can't send data toward an unvalidated address that's more than three times the data it received from that address ๐Ÿ›‘

Note: The anti-amplification limit only kicks in when an endpoint is responding to packets from an unvalidated address. It does NOT apply to clients setting up a new connection, or to clients starting connection migration. Main character energy for clients ๐Ÿ’…

21.1.1.2. Server-Side DoS ๐Ÿ–ฅ๏ธ

Computing a server's first flight for a full handshake can be pricey, since it needs both a signature and a key exchange computation ๐Ÿ’ธ To stop computational DoS attacks, the Retry packet gives a cheap token exchange. It lets servers validate a client's IP address before doing any expensive work, and it only costs one extra round trip โฑ๏ธ After a successful handshake, servers can hand out new tokens to a client. Those let the client set up new connections later without paying that cost again ๐ŸŽŸ๏ธ

21.1.1.3. On-Path Handshake Termination ๐Ÿ›‘

An on-path or off-path attacker can force a handshake to fail by replacing or racing Initial packets ๐Ÿ˜ค Once valid Initial packets have been exchanged, later Handshake packets are protected with the Handshake keys. After that, an on-path attacker can't force the handshake to fail, except by dropping packets so the endpoints give up on the attempt ๐Ÿ™…โ€โ™€๏ธ

An on-path attacker can also swap out the addresses on packets from either side, so the client or server ends up with a wrong view of the remote addresses. That kind of attack looks exactly like what a NAT does ๐Ÿคทโ€โ™€๏ธ

21.1.1.4. Parameter Negotiation ๐ŸŽ›๏ธ

The whole handshake is cryptographically protected. Initial packets are encrypted with per-version keys, and Handshake and later packets are encrypted with keys derived from the TLS key exchange ๐Ÿ” Parameter negotiation is folded into the TLS transcript, so it gets the same integrity guarantees as regular TLS negotiation โœจ An attacker can see the client's transport parameters if it knows the version-specific salt, but it can't see the server's transport parameters and can't influence parameter negotiation ๐Ÿ‘‘

Connection IDs are unencrypted, but they're integrity protected in every packet ๐Ÿงš

This version of QUIC doesn't have a version negotiation mechanism. Implementations of incompatible versions will just fail to establish a connection ๐Ÿฅ€

that's the vibe for this part, bestie ๐Ÿ’–

21.1.2. Protected Packets ๐Ÿ”โœจ

Okay bestie, time to talk about the locked-up packets ๐Ÿ’…. Packet protection (Section 12.1) slaps authenticated encryption on basically every packet except Version Negotiation packets ๐Ÿ”’. Initial and Retry packets only get limited protection because they use version-specific keying material ๐Ÿ™ˆ, so check [QUIC-TLS] for the full tea. This section looks at passive and active attacks against protected packets ๐Ÿ•ต๏ธโ€โ™€๏ธ.

First up, passive attacks ๐Ÿ“ผ. Both on-path and off-path attackers can save the packets they observe and run an offline attack against packet protection later on. This is true for any observer of any packet on any network, so ngl it's giving "always assume someone is recording" ๐Ÿ˜ญ.

Next, injection ๐Ÿ’‰. An attacker who injects packets without being able to observe valid packets for a connection is unlikely to succeed. Packet protection makes sure valid packets only come from endpoints that hold the key material established during the handshake ๐Ÿ”‘. See Sections 7 and 21.1.1 for the deets. Similarly, an active attacker who watches packets and tries to insert new data or modify existing data in them should not be able to generate packets that the receiving endpoint deems valid, except for Initial packets ๐Ÿšซ.

Now spoofing ๐ŸŽญ. A spoofing attack, where an active attacker rewrites the unprotected parts of a packet it forwards or injects (like the source or destination address), is only effective if the attacker can forward packets to the original endpoint. Packet protection means the payloads can only be processed by endpoints that completed the handshake, and invalid packets are ignored by those endpoints ๐Ÿ—‘๏ธ.

Last but not least, coalescing ๐Ÿงฉ. An attacker can also mess with the boundaries between packets and UDP datagrams, either smooshing multiple packets into a single datagram or splitting coalesced packets into multiple datagrams. Aside from datagrams containing Initial packets (which require padding), changing how packets are arranged in datagrams has no functional effect on a connection, although it might change some performance characteristics ๐Ÿ“Š.


21.1.3. Connection Migration ๐Ÿฆ‹

Connection migration (Section 9) lets endpoints switch between IP addresses and ports across multiple paths ๐Ÿ›ฃ๏ธ. They use one path at a time for sending and receiving non-probing frames. Path validation (Section 8.2) confirms that a peer is both willing and able to receive packets sent on a particular path โœ…. This helps reduce the effects of address spoofing by limiting the number of packets sent to a spoofed address, which is a very smart move ๐Ÿง .

This section describes the intended security properties of connection migration under various types of DoS attacks ๐Ÿ›ก๏ธ.


21.1.3.1. On-Path Active Attacks ๐Ÿ‘€

An attacker that can stop a packet it observes from reaching its intended destination is considered an on-path attacker ๐Ÿšง. When an attacker sits between a client and server, endpoints are required to send packets through the attacker in order to establish connectivity on a given path ๐Ÿ˜ฉ.

An on-path attacker can:

  • Inspect packets ๐Ÿ”
  • Modify IP and UDP packet headers โœ๏ธ
  • Inject new packets ๐Ÿ’‰
  • Delay packets โณ
  • Reorder packets ๐Ÿ”€
  • Drop packets ๐Ÿ—‘๏ธ
  • Split and merge datagrams along packet boundaries ๐Ÿงฉ

An on-path attacker cannot:

  • Modify an authenticated portion of a packet and cause the recipient to accept that packet ๐Ÿ™…โ€โ™€๏ธ

Okay so an on-path attacker can totally modify the packets it sees ๐Ÿ‘€, but any change to an authenticated part of a packet gets the packet dropped by the receiving endpoint as invalid ๐Ÿ’€. Packet payloads are both authenticated and encrypted, so no cap, it's a big L for the attacker ๐Ÿ˜ค.

QUIC tries to limit what an on-path attacker can do, and here's how ๐Ÿ’–:

  1. An on-path attacker can prevent the use of a path for a connection, causing the connection to fail if it cannot use a different path that does not contain the attacker. This can be achieved by dropping all packets, modifying them so that they fail to decrypt, or other methods ๐Ÿšซ.
  2. An on-path attacker can prevent migration to a new path for which the attacker is also on-path by causing path validation to fail on the new path ๐Ÿ™….
  3. An on-path attacker cannot prevent a client from migrating to a path for which the attacker is not on-path ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ.
  4. An on-path attacker can reduce the throughput of a connection by delaying packets or dropping them ๐ŸŒ.
  5. An on-path attacker cannot cause an endpoint to accept a packet for which it has modified an authenticated portion of that packet ๐Ÿ”.

21.1.3.2. Off-Path Active Attacks ๐Ÿ•ถ๏ธ

An off-path attacker isn't directly on the path between a client and server, but it could be able to get copies of some or all of the packets sent between them ๐Ÿ‘๏ธ. It can also send copies of those packets to either endpoint ๐Ÿ“จ.

An off-path attacker can:

  • Inspect packets ๐Ÿ”
  • Inject new packets ๐Ÿ’‰
  • Reorder injected packets ๐Ÿ”€

An off-path attacker cannot:

  • Modify packets sent by endpoints ๐Ÿšซ
  • Delay packets โณ
  • Drop packets ๐Ÿ—‘๏ธ
  • Reorder original packets ๐Ÿ™…โ€โ™€๏ธ

An off-path attacker can make modified copies of packets it has observed and inject those copies into the network, possibly with spoofed source and destination addresses ๐ŸŽญ.

For the sake of this discussion, it's assumed that an off-path attacker can inject a modified copy of a packet into the network that reaches the destination endpoint before the original packet the attacker observed arrives ๐Ÿ. In other words, the attacker can consistently "win" a race against the legitimate packets between the endpoints, which could cause the original packet to be ignored by the recipient ๐Ÿ˜ณ.

It's also assumed that an attacker has the resources needed to affect NAT state ๐Ÿจ. In particular, an attacker can make an endpoint lose its NAT binding and then grab the same port for its own traffic ๐Ÿซฃ.

QUIC tries to limit what an off-path attacker can do, and here's the rundown ๐Ÿ’…:

  1. An off-path attacker can race packets and attempt to become a "limited" on-path attacker ๐ŸŽ๏ธ.
  2. An off-path attacker can cause path validation to succeed for forwarded packets with the source address listed as the off-path attacker, as long as it can provide improved connectivity between the client and the server ๐Ÿ“ถ.
  3. An off-path attacker cannot cause a connection to close once the handshake has completed ๐Ÿ™Œ.
  4. An off-path attacker cannot cause migration to a new path to fail if it cannot observe the new path ๐Ÿš€.
  5. An off-path attacker can become a limited on-path attacker during migration to a new path for which it is also an off-path attacker ๐Ÿ”„.
  6. An off-path attacker can become a limited on-path attacker by affecting shared NAT state such that it sends packets to the server from the same IP address and port that the client originally used ๐Ÿชž.

21.1.3.3. Limited On-Path Active Attacks ๐ŸŽ€

A limited on-path attacker is an off-path attacker that offers improved routing of packets by duplicating and forwarding original packets between the server and the client ๐Ÿš€. Its copies arrive before the originals, so the original packets get dropped by the destination endpoint ๐Ÿ˜ฌ.

A limited on-path attacker is different from an on-path attacker because it isn't on the original path between the endpoints, so the original packets sent by an endpoint still reach their destination ๐Ÿ™Œ. This means that if the attacker later fails to route the copied packets to the destination faster than their original path, that won't stop the original packets from getting there ๐Ÿƒโ€โ™€๏ธ.

A limited on-path attacker can:

  • Inspect packets ๐Ÿ”
  • Inject new packets ๐Ÿ’‰
  • Modify unencrypted packet headers โœ๏ธ
  • Reorder packets ๐Ÿ”€

A limited on-path attacker cannot:

  • Delay packets so that they arrive later than packets sent on the original path โณ๐Ÿ™…โ€โ™€๏ธ
  • Drop packets ๐Ÿ—‘๏ธ
  • Modify the authenticated and encrypted portion of a packet and cause the recipient to accept that packet ๐Ÿ”

A limited on-path attacker can only delay packets up to the point where the original packets arrive before the duplicate packets. That means it can't offer routing with worse latency than the original path ๐Ÿข. And if a limited on-path attacker drops packets, the original copy still arrives at the destination endpoint ๐Ÿ’–.

QUIC tries to limit what a limited off-path attacker can do ๐Ÿ’…:

  1. A limited on-path attacker cannot cause a connection to close once the handshake has completed ๐Ÿ›‘.
  2. A limited on-path attacker cannot cause an idle connection to close if the client is first to resume activity ๐Ÿ˜Œ.
  3. A limited on-path attacker can cause an idle connection to be deemed lost if the server is the first to resume activity ๐Ÿ’€.

Note that these guarantees are the same guarantees provided for any NAT, for the same reasons ๐Ÿงšโ€โ™€๏ธ.

21.2. Handshake Denial of Service โœจ

okay so QUIC is encrypted AND authenticated, which means it comes with a whole bunch of protections against denial of service ๐Ÿ›ก๏ธ Once the cryptographic handshake is done, QUIC endpoints toss out most packets that aren't authenticated ๐Ÿ—‘๏ธ, so an attacker has a much harder time messing with connections that already exist ๐Ÿ’…

Once a connection is up, endpoints might accept some unauthenticated ICMP packets (see Section 14.2.1), but honestly that's extremely limited ๐Ÿ™ˆ The only other kind of packet an endpoint might accept is a stateless reset (Section 10.3), and that only works because the token stays secret until it's used ๐Ÿคซ

While a connection is being created, QUIC only protects against attacks from off the network path ๐Ÿ›ค๏ธ Every QUIC packet carries proof that the recipient saw a preceding packet from its peer ๐Ÿซถ

Addresses can't change during the handshake, so endpoints can discard any packets that show up on a different network path ๐Ÿšซ

The Source and Destination Connection ID fields are the main protection against off-path attacks during the handshake (see Section 8.1) ๐Ÿ†” These are required to match the values set by a peer. Except for Initial and Stateless Resets, an endpoint only accepts packets that include a Destination Connection ID field matching a value the endpoint previously chose. This is the only protection offered for Version Negotiation packets ๐ŸŽ€

The Destination Connection ID field in an Initial packet is picked by a client to be unpredictable, and that does a second job too ๐Ÿง  The packets carrying the cryptographic handshake are protected with a key derived from this connection ID and a salt specific to the QUIC version. This lets endpoints use the same process for authenticating incoming packets that they use after the handshake completes. Packets that can't be authenticated get discarded ๐Ÿ—‘๏ธ Protecting packets this way gives a strong assurance that the sender saw the Initial packet and understood it ๐Ÿงš

These protections aren't meant to stop an attacker that can receive QUIC packets before the connection is established ๐Ÿ‘€ Such an attacker can potentially send packets that QUIC endpoints will accept. This version of QUIC tries to detect this sort of attack, but it expects endpoints to fail to establish a connection rather than recover from it ๐Ÿ˜ญ For the most part, the cryptographic handshake protocol [QUIC-TLS] is responsible for detecting tampering during the handshake ๐Ÿ”

Endpoints are permitted to use other methods to detect and try to recover from interference with the handshake ๐Ÿ› ๏ธ Invalid packets can be identified and discarded using other methods, but this document doesn't mandate any specific method ๐Ÿ’–

21.3. Amplification Attack ๐ŸŒŠ

An attacker might be able to receive an address validation token (Section 8) from a server and then let go of the IP address it used to get that token ๐ŸŽฃ Later, the attacker can start a 0-RTT connection with a server by spoofing that same address, which might now belong to a different (victim) endpoint ๐Ÿ˜ˆ That means the attacker can potentially make the server send an initial congestion window's worth of data toward the victim ๐Ÿ“ฆ

Servers SHOULD provide mitigations for this attack by limiting the usage and lifetime of address validation tokens; see Section 8.1.3 ๐Ÿ›ก๏ธ

21.4. Optimistic ACK Attack โฉ

An endpoint that acknowledges packets it hasn't received might trick a congestion controller into permitting sending at rates beyond what the network can actually support ๐Ÿ™ƒ An endpoint MAY skip packet numbers when sending packets to detect this behavior ๐Ÿ•ต๏ธโ€โ™€๏ธ If it spots this, the endpoint can immediately close the connection with a connection error of type PROTOCOL_VIOLATION (see Section 10.2) ๐Ÿšช

21.5. Request Forgery Attacks ๐ŸŽญ

A request forgery attack happens when an endpoint makes its peer issue a request toward a victim, with the endpoint controlling what that request says ๐ŸŽญ These attacks aim to give an attacker access to capabilities of its peer that would otherwise be out of reach. For a networking protocol, request forgery is often used to exploit any implicit authorization the victim grants the peer just because of where the peer sits in the network ๐Ÿฐ

For request forgery to work, an attacker needs to be able to influence what packets the peer sends and where those packets go ๐ŸŽฏ If an attacker can target a vulnerable service with a controlled payload, that service might do things that get attributed to the attacker's peer but were actually decided by the attacker ๐Ÿ˜ฌ

For example, cross-site request forgery [CSRF] exploits on the Web make a client issue requests that include authorization cookies [COOKIE]. That lets one site access information and take actions meant to be restricted to a different site ๐Ÿช

Since QUIC runs over UDP, the main attack modality to worry about is one where an attacker can pick the address its peer sends UDP datagrams to and can control some of the unprotected content of those packets ๐Ÿ‘€ Much of the data QUIC endpoints send is protected, but this still includes control over ciphertext. An attack succeeds if an attacker can make a peer send a UDP datagram to a host that then performs some action based on the content of that datagram ๐Ÿ’€

This section talks about ways QUIC might be used for request forgery attacks ๐Ÿ“–

This section also describes limited countermeasures that QUIC endpoints can implement ๐Ÿ›ก๏ธ These mitigations can be used unilaterally by a QUIC implementation or deployment, without the potential targets of request forgery attacks needing to do anything. However, these countermeasures could fall short if UDP-based services don't properly authorize requests ๐Ÿ™…โ€โ™€๏ธ

The migration attack described in Section 21.5.4 is seriously powerful and doesn't have adequate countermeasures, so QUIC server implementations should assume that attackers can make them generate arbitrary UDP payloads to arbitrary destinations ๐Ÿ˜ฑ QUIC servers SHOULD NOT be deployed in networks that don't deploy ingress filtering [BCP38] and also have inadequately secured UDP endpoints ๐Ÿšจ

It's not generally possible to make sure clients aren't co-located with vulnerable endpoints ๐Ÿ  But this version of QUIC doesn't let servers migrate, which prevents spoofed migration attacks on clients ๐Ÿ™Œ Any future extension that allows server migration MUST also define countermeasures for forgery attacks ๐Ÿ”

21.5.1. Control Options for Endpoints ๐ŸŽ›๏ธ

QUIC gives an attacker a few opportunities to influence or control where its peer sends UDP datagrams ๐Ÿ‘€

  • initial connection establishment (Section 7), where a server can choose where a client sends datagrams, for example by populating DNS records ๐ŸŒ
  • preferred addresses (Section 9.6), where a server can choose where a client sends datagrams ๐Ÿงญ
  • spoofed connection migrations (Section 9.3.1), where a client can use source address spoofing to select where a server sends subsequent datagrams ๐ŸŽญ
  • spoofed packets that cause a server to send a Version Negotiation packet (Section 21.5.5) ๐ŸŒ€

In every case, the attacker can make its peer send datagrams to a victim that might not understand QUIC at all ๐Ÿ˜ฌ That is, these packets are sent by the peer before address validation (see Section 8) ๐Ÿ™ˆ

Outside the encrypted portion of packets, QUIC gives an endpoint several ways to control the content of the UDP datagrams its peer sends ๐ŸŽ›๏ธ The Destination Connection ID field offers direct control over bytes that show up early in packets sent by the peer (see Section 5.1). The Token field in Initial packets gives a server control over other bytes of Initial packets (see Section 17.2.2) ๐Ÿงฉ

This version of QUIC has no measures to prevent indirect control over the encrypted portions of packets ๐Ÿ”“ It's necessary to assume that endpoints can control the contents of frames a peer sends, especially frames that carry application data, like STREAM frames ๐Ÿ“จ How much depends on the details of the application protocol, but some control is possible in many protocol usage contexts ๐Ÿคทโ€โ™€๏ธ Since the attacker has access to packet protection keys, they're likely able to predict how a peer will encrypt future packets ๐Ÿ”‘ So controlling datagram content only requires that the attacker can predict the packet number and the placement of frames in packets with some reliability ๐ŸŽฏ

This section assumes that limiting control over datagram content isn't feasible ๐Ÿ™ The focus of the mitigations in the following sections is on limiting the ways that datagrams sent before address validation can be used for request forgery ๐Ÿ›‘

21.5.2. Request Forgery with Client Initial Packets ๐Ÿ“ฌ

An attacker acting as a server can choose the IP address and port it advertises its availability on, so Initial packets from clients are assumed to be usable in this sort of attack ๐ŸŽฃ The address validation built into the handshake makes sure that, for a new connection, a client won't send other types of packets to a destination that doesn't understand QUIC or isn't willing to accept a QUIC connection โœ…

Initial packet protection (Section 5.2 of [QUIC-TLS]) makes it hard for servers to control the content of Initial packets sent by clients ๐Ÿ”’ A client that picks an unpredictable Destination Connection ID makes sure that servers can't control any of the encrypted portion of Initial packets from clients ๐Ÿ™Œ

However, the Token field is open to server control, and it does let a server use clients to mount request forgery attacks ๐Ÿ˜ค Using tokens provided with the NEW_TOKEN frame (Section 8.1.3) is the only option for request forgery during connection establishment ๐Ÿฅฒ

Clients aren't obligated to use the NEW_TOKEN frame, though ๐Ÿ™…โ€โ™€๏ธ Request forgery attacks that rely on the Token field can be avoided if clients send an empty Token field when the server address has changed since the NEW_TOKEN frame was received ๐Ÿ›ก๏ธ

Clients could avoid using NEW_TOKEN if the server address changes ๐Ÿ”„ However, leaving out a Token field could hurt performance. Servers could rely on NEW_TOKEN to allow sending data beyond the three-times limit on sending data (see Section 8.1) ๐Ÿ“ˆ This especially affects cases where clients use 0-RTT to request data from servers โšก

Sending a Retry packet (Section 17.2.5) gives a server the option to change the Token field ๐Ÿ” After sending a Retry, the server can also control the Destination Connection ID field of subsequent Initial packets from the client ๐ŸŽ€ This might also allow indirect control over the encrypted content of Initial packets ๐Ÿ•ต๏ธโ€โ™€๏ธ But the exchange of a Retry packet validates the server's address, which prevents subsequent Initial packets from being used for request forgery ๐Ÿ’…

21.5.3. Request Forgery with Preferred Addresses ๐ŸŽ€

okay so servers can totally pick a preferred address ๐Ÿฆ‹ and clients then move over to it once the handshake is confirmed (see Section 9.6). the Destination Connection ID field on packets the client sends to that preferred address can be used for request forgery, so it's a little sus ngl ๐Ÿ™ˆ

a client MUST NOT send non-probing frames to a preferred address before it has validated that address (see Section 8). this is lowkey a huge deal because it cuts way down on how much control a server can have over the encrypted part of your datagrams ๐Ÿ”โœจ

this document doesn't add any extra countermeasures that are specific to preferred addresses, and endpoints can't really build their own either ๐Ÿ’… the generic measures in Section 21.5.6 could be used as extra backup ๐Ÿซถ

21.5.4. Request Forgery with Spoofed Migration ๐ŸŒธ

so clients are able to fake a source address as part of an apparent connection migration, which can trick a server into sending datagrams to that address ๐Ÿ˜ญ

the Destination Connection ID field in any packets a server later sends to that spoofed address can be used for request forgery, and a client might also be able to influence the ciphertext ๐Ÿ’€

a server that only sends probing packets (Section 9.1) to an address before address validation gives an attacker only limited control over the encrypted part of the datagrams. but, especially for NAT rebinding, this can hurt performance ๐ŸŒˆ if the server sends frames carrying application data, though, an attacker might be able to control most of the content of those datagrams. yikes ๐Ÿ˜ฌ

this document doesn't offer specific countermeasures that endpoints can implement, apart from the generic measures in Section 21.5.6. the real MVPs here are countermeasures for address spoofing at the network level -- like ingress filtering [BCP38] -- which work super well against spoofing attacks that come from an external network ๐Ÿ‘‘

21.5.5. Request Forgery with Version Negotiation ๐Ÿงš

so clients that can fake a source address on a packet can make a server send a Version Negotiation packet (Section 17.2.1) to that address ๐Ÿ™ƒ

the connection ID fields on packets of an unknown version have no size restrictions, which means the client gets to control more of the resulting datagram. the first byte of this packet isn't under client control and the next four bytes are zero, but the client can control up to 512 bytes starting from the fifth byte. so much control, bestie ๐Ÿ˜ณ

no specific countermeasures are provided for this attack, though generic protections (Section 21.5.6) could apply. ingress filtering [BCP38] is also effective here ๐Ÿ’–

21.5.6. Generic Request Forgery Countermeasures ๐Ÿ’œ

the most effective defense against request forgery is to update vulnerable services so they use strong authentication. but that isn't always something a QUIC deployment can control ๐Ÿฅบ so this section lists some other steps that QUIC endpoints could take on their own. all of these are discretionary, because depending on the situation they could get in the way of legitimate uses ๐ŸŒท

services offered over loopback interfaces often lack proper authentication, so endpoints MAY prevent connection attempts or migration to a loopback address ๐Ÿšซ endpoints SHOULD NOT allow connections or migration to a loopback address if the same service was previously available at a different interface, or if the address was handed out by a service at a non-loopback address. endpoints that depend on these capabilities could offer an option to turn these protections off ๐Ÿ”ง

similarly, endpoints could treat a change to a link-local address [RFC4291] or an address in a private-use range [RFC1918] as a possible request forgery attempt if it comes from a global, unique-local [RFC4193], or non-private address. endpoints could refuse to use these addresses entirely, but that carries a significant risk of breaking legitimate uses. so endpoints SHOULD NOT refuse to use an address unless they have specific knowledge about the network showing that sending datagrams to unvalidated addresses in a given range isn't safe ๐Ÿซ 

endpoints MAY choose to reduce the risk of request forgery by not including values from NEW_TOKEN frames in Initial packets, or by only sending probing frames before address validation is complete. just note that this doesn't stop an attacker from using the Destination Connection ID field for an attack ๐Ÿ‘€

endpoints aren't expected to know specifically where vulnerable servers are located. still, over time it might be possible to identify particular UDP ports that are common attack targets, or particular patterns in datagrams used for attacks. endpoints MAY choose to avoid sending datagrams to those ports, or to avoid sending datagrams that match those patterns before validating the destination address ๐Ÿ›ก๏ธ endpoints MAY retire connection IDs containing patterns known to be problematic without using them ๐Ÿ—‘๏ธ

๐Ÿ’ก Note: modifying endpoints to apply these protections is more efficient than deploying network-based protections, because endpoints don't need to do any extra processing when sending to an address that has already been validated. โœจ

21.6. Slowloris Attacks ๐Ÿข

the attacks commonly known as Slowloris [SLOWLORIS] try to keep lots of connections to the target endpoint open for as long as possible. against a QUIC endpoint, these attacks can be run by generating the bare minimum of activity needed to avoid being closed for inactivity ๐Ÿ˜ด this might mean sending tiny bits of data, slowly opening flow control windows to control the sender's rate, or making up ACK frames that pretend there's a high loss rate. sneaky sneaky ๐Ÿฅท

QUIC deployments SHOULD provide mitigations for Slowloris attacks ๐Ÿ›‘ some examples are increasing the maximum number of clients the server will accept, limiting how many connections a single IP address can make, putting restrictions on the minimum transfer speed a connection is allowed to have, and restricting how long an endpoint is allowed to stay connected โฐ

21.7. Stream Fragmentation and Reassembly Attacks ๐Ÿงฉ

an adversarial sender might purposely not send parts of the stream data, which makes the receiver commit resources to data that never shows up. this can lead to a disproportionately large receive buffer memory commitment, and/or the creation of a big, inefficient data structure at the receiver ๐Ÿ˜ตโ€๐Ÿ’ซ

an adversarial receiver might purposely not acknowledge packets that contain stream data, trying to force the sender to keep unacknowledged stream data around for retransmission. rude ๐Ÿ™„

the attack on receivers is mitigated if flow control windows match available memory. however, some receivers overcommit memory and advertise flow control offsets that add up to more than their actual available memory. overcommitting can lead to better performance when endpoints are well behaved, but it leaves endpoints open to the stream fragmentation attack ๐Ÿ’”

QUIC deployments SHOULD provide mitigations for stream fragmentation attacks ๐Ÿ› ๏ธ mitigations could include avoiding overcommitting memory, limiting the size of tracking data structures, delaying reassembly of STREAM frames, implementing heuristics based on the age and duration of reassembly holes, or some mix of these โœจ

21.8. Stream Commitment Attack ๐ŸŽญ

an adversarial endpoint can open a huge number of streams and exhaust the state on an endpoint. it could repeat this across a large number of connections, kind of like SYN flooding in TCP ๐ŸŒŠ

normally, clients open streams in sequence, as explained in Section 2.1. but when several streams get initiated at short intervals, loss or reordering can cause the STREAM frames that open streams to arrive out of sequence. when a receiver gets a higher-numbered stream ID, it is required to open all the intervening streams of the same type (see Section 3.2). so on a new connection, opening stream 4000000 opens 1 million and 1 client-initiated bidirectional streams. wild ๐Ÿคฏ

the number of active streams is limited by the initial_max_streams_bidi and initial_max_streams_uni transport parameters, as updated by any received MAX_STREAMS frames (see Section 4.6). if they're picked wisely, these limits reduce the effect of the stream commitment attack ๐Ÿ™Œ but setting the limit too low could hurt performance when applications expect to open a large number of streams ๐Ÿ˜ฌ

21.9. Peer Denial of Service ๐Ÿšจ

QUIC and TLS both contain frames or messages that have legit uses in some contexts. but those frames or messages can be abused to make a peer spend processing resources without having any observable impact on the connection's state ๐Ÿฅฑ

messages can also be used to change and revert state in small or inconsequential ways, like sending small increments to flow control limits ๐Ÿ”

if the processing costs are disproportionately large compared to bandwidth consumption or effect on state, a malicious peer could exhaust processing capacity. not it ๐Ÿ˜ค

there are legit uses for all messages, but implementations SHOULD track the cost of processing relative to progress, and treat excessive amounts of any non-productive packets as a sign of an attack ๐Ÿ•ต๏ธโ€โ™€๏ธ endpoints MAY respond to this with a connection error or by dropping packets ๐Ÿซก

21.10. Explicit Congestion Notification Attacks โšก

an on-path attacker could change the value of ECN fields in the IP header to influence the sender's rate. [RFC3168] goes into more detail about these manipulations and their effects ๐Ÿ“š

a limited on-path attacker can duplicate packets and send them with modified ECN fields to affect the sender's rate. if a receiver discards duplicate packets, the attacker has to race the duplicate against the original to pull this off ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ so QUIC endpoints ignore the ECN field in an IP packet unless at least one QUIC packet in that IP packet is successfully processed (see Section 13.4). periodt ๐Ÿ’…๐ŸŽ€

21.11. Stateless Reset Oracle ๐Ÿ”ฎ

okay so stateless resets can cause a denial-of-service attack that's kinda like a TCP reset injection ๐Ÿ’€. It's possible if an attacker can make a stateless reset token get generated for a connection with a connection ID they picked. Then they can reset an active connection that has that same connection ID. Scary stuff ๐Ÿ™ˆ

If a packet can get routed to different instances that share a static key (like when an IP address or port changes), then an attacker can make the server send a stateless reset. To block this style of DoS, endpoints that share a static key for stateless resets (see Section 10.3.2) MUST be arranged so that packets with a given connection ID always arrive at an instance that has connection state, unless that connection is no longer active. ๐Ÿ›ก๏ธ

More generally, servers MUST NOT generate a stateless reset if a connection with the corresponding connection ID could be active on any endpoint using the same static key. ๐Ÿšซ

With a cluster that uses dynamic load balancing, a change in load-balancer configuration can happen while an active instance still holds connection state. Even if the instance keeps its state, the change in routing and the resulting stateless reset will end the connection. If there's no chance the packet gets routed to the correct instance, it is better to send a stateless reset than to wait for the connection to time out. โณ But that's only acceptable if an attacker can't influence the routing. ๐Ÿ’…

21.12. Version Downgrade โฌ‡๏ธ

This document defines QUIC Version Negotiation packets (Section 6), which can be used to negotiate the QUIC version between two endpoints. However, this document doesn't spell out how that negotiation works between this version and future versions. Version Negotiation packets don't include any mechanism to prevent version downgrade attacks. So future versions of QUIC that use Version Negotiation packets MUST define a mechanism that is robust against version downgrade attacks. Not a detail to skip ๐Ÿšจ

21.13. Targeted Attacks by Routing ๐ŸŽฏ

Deployments need to limit an attacker's ability to target a new connection to a particular server instance. Ideally, routing decisions are made independently of client-selected values, including addresses. Once an instance is selected, a connection ID can be picked so that later packets get routed to the same instance. ๐Ÿงš

21.14. Traffic Analysis ๐Ÿ‘€

The length of QUIC packets can reveal information about the length of the content inside them. That's why the PADDING frame exists, so endpoints have some ability to obscure the length of packet content; see Section 19.1. ๐ŸŽ€

Defeating traffic analysis is seriously challenging and still the subject of active research. Length isn't the only way information might leak. Endpoints might also reveal sensitive information through other side channels, such as the timing of packets. โฑ๏ธ

22. IANA Considerations ๐Ÿ“š

This document establishes several registries for managing codepoints in QUIC. These registries all operate on a common set of policies, as defined in Section 22.1. ๐ŸŒธ

22.1. Registration Policies for QUIC Registries

All QUIC registries allow for both provisional and permanent registration of codepoints. This section documents the policies that these registries have in common. ๐Ÿฆ‹

22.1.1. Provisional Registrations

Provisional registrations of codepoints are meant for private use and experimenting with QUIC extensions. They only need the codepoint value and contact information. However, provisional registrations could be reclaimed and reassigned for another purpose. ๐Ÿงช

Provisional registrations require Expert Review, as defined in Section 4.5 of [RFC8126]. The designated expert or experts are advised that only registrations taking up an excessive proportion of the remaining codepoint space, or the very first unassigned value (see Section 22.1.2), can be rejected. ๐Ÿง

Provisional registrations include a Date field that shows when the registration was last updated. A request to update the date on any provisional registration can be made without review from the designated expert(s). ๐Ÿ“…

All QUIC registries include the following fields to support provisional registration:

  • Value: The assigned codepoint.
  • Status: "permanent" or "provisional".
  • Specification: A reference to a publicly available specification for the value.
  • Date: The date of the last update to the registration.
  • Change Controller: The entity that is responsible for the definition of the registration.
  • Contact: Contact details for the registrant.
  • Notes: Supplementary notes about the registration.

Provisional registrations MAY omit the Specification and Notes fields, plus any additional fields that might be required for a permanent registration. The Date field isn't required when requesting a registration, since it's set to the date the registration is created or updated. ๐Ÿ“

22.1.2. Selecting Codepoints

New requests for codepoints from QUIC registries SHOULD use a randomly selected codepoint that excludes both existing allocations and the first unallocated codepoint in the selected space. Requests for multiple codepoints MAY use a contiguous range. This keeps different implementations from attaching different meanings to the same codepoint. ๐ŸŽฒ

The first unassigned codepoint is reserved for allocation using the Standards Action policy; see Section 4.9 of [RFC8126]. The early codepoint assignment process [EARLY-ASSIGN] can be used for these values. ๐Ÿ”

For codepoints encoded in variable-length integers (Section 16), such as frame types, codepoints that encode to four or eight bytes (that is, values 2^14 and above) SHOULD be used unless the usage is especially sensitive to having a longer encoding. ๐Ÿ“

Applications to register codepoints in QUIC registries MAY include a requested codepoint as part of the registration. IANA MUST allocate the selected codepoint if the codepoint is unassigned and the requirements of the registration policy are met. โœจ

22.1.3. Reclaiming Provisional Codepoints

Sometimes someone wants to remove an unused provisional registration to free up space in a registry, or a portion of it (such as the 64-16383 range for codepoints that use variable-length encodings). This SHOULD be done only for the codepoints with the earliest recorded date, and entries that have been updated less than a year prior SHOULD NOT be reclaimed. ๐Ÿงน

A request to remove a codepoint MUST be reviewed by the designated experts. The experts MUST attempt to determine whether the codepoint is still in use. Experts are advised to contact the listed contacts for the registration, plus as wide a set of protocol implementers as possible, to find out whether any use of the codepoint is known. The experts are also advised to allow at least four weeks for responses. ๐Ÿ•ต๏ธ

If any use of the codepoints is identified by this search, or a request to update the registration is made, the codepoint MUST NOT be reclaimed. Instead, the date on the registration is updated. A note might be added to the registration recording the relevant information that was learned. ๐Ÿ“Œ

If no use of the codepoint was identified and no request was made to update the registration, the codepoint MAY be removed from the registry. ๐Ÿ—‘๏ธ

This review and consultation process also applies to requests to change a provisional registration into a permanent registration. The difference is that the goal isn't to determine whether there's no use of the codepoint, but to confirm that the registration accurately represents any deployed usage. ๐Ÿ™Œ

22.1.4. Permanent Registrations

Permanent registrations in QUIC registries use the Specification Required policy (Section 4.6 of [RFC8126]), unless otherwise specified. The designated expert or experts verify that a specification exists and is readily accessible. Experts are encouraged to lean towards approving registrations unless they are abusive, frivolous, or actively harmful (not merely aesthetically displeasing or architecturally dubious). The creation of a registry MAY specify additional constraints on permanent registrations. ๐Ÿ’–

The creation of a registry MAY identify a range of codepoints where registrations are governed by a different registration policy. For instance, the "QUIC Frame Types" registry (Section 22.4) has a stricter policy for codepoints in the range from 0 to 63. ๐Ÿงฉ

Any stricter requirements for permanent registrations don't prevent provisional registrations for the affected codepoints. For instance, a provisional registration for a frame type of 61 could be requested. ๐Ÿ“

All registrations made by Standards Track publications MUST be permanent. ๐Ÿ”’

All registrations in this document are assigned a permanent status and list a change controller of the IETF and a contact of the QUIC Working Group (quic@ietf.org). ๐Ÿซถ

22.2. QUIC Versions Registry

IANA has added a registry for "QUIC Versions" under a "QUIC" heading. ๐ŸŽ‰

The "QUIC Versions" registry governs a 32-bit space; see Section 15. This registry follows the registration policy from Section 22.1. Permanent registrations in this registry are assigned using the Specification Required policy (Section 4.6 of [RFC8126]). ๐ŸŒˆ

The codepoint 0x00000001 is assigned with permanent status to the protocol defined in this document. The codepoint 0x00000000 is permanently reserved; the note for this codepoint indicates that this version is reserved for version negotiation. ๐Ÿ‘‘

All codepoints that follow the pattern 0x?a?a?a?a are reserved, MUST NOT be assigned by IANA, and MUST NOT appear in the listing of assigned values. ๐Ÿ™…โ€โ™€๏ธ

22.3. QUIC Transport Parameters Registry ๐ŸŽ€

okay so IANA made a whole registry for "QUIC Transport Parameters" and it lives under a "QUIC" heading ๐Ÿ’… it's giving organized. this one governs a 62-bit space ๐Ÿฆ‹ and it follows the registration policy from Section 22.1. Permanent registrations here are assigned with the Specification Required policy (Section 4.6 of [RFC8126]), except for values between 0x00 and 0x3f (in hexadecimal), inclusive. Those get Standards Action or IESG Approval instead, as defined in Sections 4.9 and 4.10 of [RFC8126].

In addition to the fields listed in Section 22.1.1, permanent registrations in this registry MUST include the following field:

  • Parameter Name: A short mnemonic for the parameter.

The initial contents of this registry are shown in Table 6. ๐ŸŒธ

+=======+=====================================+===============+
| Value | Parameter Name                      | Specification |
+=======+=====================================+===============+
| 0x00  | original_destination_connection_id  | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x01  | max_idle_timeout                    | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x02  | stateless_reset_token               | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x03  | max_udp_payload_size                | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x04  | initial_max_data                    | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x05  | initial_max_stream_data_bidi_local  | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x06  | initial_max_stream_data_bidi_remote | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x07  | initial_max_stream_data_uni         | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x08  | initial_max_streams_bidi            | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x09  | initial_max_streams_uni             | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x0a  | ack_delay_exponent                  | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x0b  | max_ack_delay                       | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x0c  | disable_active_migration            | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x0d  | preferred_address                   | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x0e  | active_connection_id_limit          | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x0f  | initial_source_connection_id        | Section 18.2  |
+-------+-------------------------------------+---------------+
| 0x10  | retry_source_connection_id          | Section 18.2  |
+-------+-------------------------------------+---------------+

Table 6: Initial QUIC Transport Parameters Registry Entries

Each value of the form 31 * N + 27 for integer values of N (that is, 27, 58, 89, ...) are reserved. These values MUST NOT be assigned by IANA and MUST NOT appear in the listing of assigned values. so those numbers are off-limits, no exceptions, periodt ๐Ÿ™ˆ

22.4. QUIC Frame Types Registry ๐Ÿงš

IANA has added a registry for "QUIC Frame Types" under a "QUIC" heading. โœจ

The "QUIC Frame Types" registry governs a 62-bit space. This registry follows the registration policy from Section 22.1. Permanent registrations in this registry are assigned using the Specification Required policy (Section 4.6 of [RFC8126]), except for values between 0x00 and 0x3f (in hexadecimal), inclusive, which are assigned using Standards Action or IESG Approval as defined in Sections 4.9 and 4.10 of [RFC8126].

In addition to the fields listed in Section 22.1.1, permanent registrations in this registry MUST include the following field:

  • Frame Type Name: A short mnemonic for the frame type.

In addition to the advice in Section 22.1, specifications for new permanent registrations SHOULD describe the means by which an endpoint might determine that it can send the identified type of frame. An accompanying transport parameter registration is expected for most registrations; see Section 22.3. Specifications for permanent registrations also need to describe the format and assigned semantics of any fields in the frame. ๐Ÿซถ

The initial contents of this registry are tabulated in Table 3. Note that the registry does not include the "Pkts" and "Spec" columns from Table 3. bestie, read Table 3 carefully, it's doing a lot of work here ๐Ÿ’œ

22.5. QUIC Transport Error Codes Registry ๐Ÿ’€

IANA has added a registry for "QUIC Transport Error Codes" under a "QUIC" heading. ๐Ÿ”ฅ

The "QUIC Transport Error Codes" registry governs a 62-bit space. This space is split into three ranges that are governed by different policies. Permanent registrations in this registry are assigned using the Specification Required policy (Section 4.6 of [RFC8126]), except for values between 0x00 and 0x3f (in hexadecimal), inclusive, which are assigned using Standards Action or IESG Approval as defined in Sections 4.9 and 4.10 of [RFC8126].

In addition to the fields listed in Section 22.1.1, permanent registrations in this registry MUST include the following fields:

  • Code: A short mnemonic for the parameter.
  • Description: A brief description of the error code semantics, which MAY be a summary if a specification reference is provided.

The initial contents of this registry are shown in Table 7. ๐Ÿ‘‘

+=======+===========================+================+==============+
|Value  | Code                      |Description     |Specification |
+=======+===========================+================+==============+
|0x00   | NO_ERROR                  |No error        |Section 20    |
+-------+---------------------------+----------------+--------------+
|0x01   | INTERNAL_ERROR            |Implementation  |Section 20    |
|       |                           |error           |              |
+-------+---------------------------+----------------+--------------+
|0x02   | CONNECTION_REFUSED        |Server refuses a|Section 20    |
|       |                           |connection      |              |
+-------+---------------------------+----------------+--------------+
|0x03   | FLOW_CONTROL_ERROR        |Flow control    |Section 20    |
|       |                           |error           |              |
+-------+---------------------------+----------------+--------------+
|0x04   | STREAM_LIMIT_ERROR        |Too many streams|Section 20    |
|       |                           |opened          |              |
+-------+---------------------------+----------------+--------------+
|0x05   | STREAM_STATE_ERROR        |Frame received  |Section 20    |
|       |                           |in invalid      |              |
|       |                           |stream state    |              |
+-------+---------------------------+----------------+--------------+
|0x06   | FINAL_SIZE_ERROR          |Change to final |Section 20    |
|       |                           |size            |              |
+-------+---------------------------+----------------+--------------+
|0x07   | FRAME_ENCODING_ERROR      |Frame encoding  |Section 20    |
|       |                           |error           |              |
+-------+---------------------------+----------------+--------------+
|0x08   | TRANSPORT_PARAMETER_ERROR |Error in        |Section 20    |
|       |                           |transport       |              |
|       |                           |parameters      |              |
+-------+---------------------------+----------------+--------------+
|0x09   | CONNECTION_ID_LIMIT_ERROR |Too many        |Section 20    |
|       |                           |connection IDs  |              |
|       |                           |received        |              |
+-------+---------------------------+----------------+--------------+
|0x0a   | PROTOCOL_VIOLATION        |Generic protocol|Section 20    |
|       |                           |violation       |              |
+-------+---------------------------+----------------+--------------+
|0x0b   | INVALID_TOKEN             |Invalid Token   |Section 20    |
|       |                           |received        |              |
+-------+---------------------------+----------------+--------------+
|0x0c   | APPLICATION_ERROR         |Application     |Section 20    |
|       |                           |error           |              |
+-------+---------------------------+----------------+--------------+
|0x0d   | CRYPTO_BUFFER_EXCEEDED    |CRYPTO data     |Section 20    |
|       |                           |buffer          |              |
|       |                           |overflowed      |              |
+-------+---------------------------+----------------+--------------+
|0x0e   | KEY_UPDATE_ERROR          |Invalid packet  |Section 20    |
|       |                           |protection      |              |
|       |                           |update          |              |
+-------+---------------------------+----------------+--------------+
|0x0f   | AEAD_LIMIT_REACHED        |Excessive use of|Section 20    |
|       |                           |packet          |              |
|       |                           |protection keys |              |
+-------+---------------------------+----------------+--------------+
|0x10   | NO_VIABLE_PATH            |No viable       |Section 20    |
|       |                           |network path    |              |
|       |                           |exists          |              |
+-------+---------------------------+----------------+--------------+
|0x0100-| CRYPTO_ERROR              |TLS alert code  |Section 20    |
|0x01ff |                           |                |              |
+-------+---------------------------+----------------+--------------+

Table 7: Initial QUIC Transport Error Codes Registry Entries

23. References ๐Ÿ“š

the references are next, bestie ๐ŸŒˆ

23.1. Normative References โœจ

okay bestie, this is the must-read pile ๐Ÿ“š๐Ÿซถ these are the docs the spec leans on, so if you skip them you're gonna be lost fr ๐Ÿ’€


23.2. Informative References ๐Ÿงš

these ones are the extra reading ๐Ÿ“–๐Ÿ’œ not required to get the core spec, but they give you the backstory, the vibes, and the "wait, where did that idea come from?" moments ๐ŸŒธ


Appendix A. Pseudocode ๐Ÿงช

okay so this part is the code-ish bit ๐Ÿ’ปโœจ the pseudocode here is sample algorithms, and the goal is to be correct and clear, NOT to be the fastest thing ever ๐Ÿข๐Ÿ’–

the pseudocode segments in this section are licensed as Code Components, so check the Copyright Notice if you wanna know the deets ๐Ÿ“œ๐ŸŽ€

Okay so this is the final stretch of RFC 9000 and bestie, it's giving appendix energy ๐ŸŽ€โœจ We're picking up right where the Sample Variable-Length Integer Decoding section lives, so let's go!

A.1. Sample Variable-Length Integer Decoding ๐Ÿงš

okay so the pseudocode in Figure 45 shows how you read a variable-length integer out of a stream of bytes ๐Ÿ’– The function ReadVarint takes one argument, which is a sequence of bytes that you read in network byte order (aka the way the bytes are actually laid out on the wire ๐ŸŒธ).

ReadVarint(data):
  // The length of variable-length integers is encoded in the
  // first two bits of the first byte.
  v = data.next_byte()
  prefix = v >> 6
  length = 1 << prefix

  // Once the length is known, remove these bits and read any
  // remaining bytes.
  v = v & 0x3f
  repeat length-1 times:
    v = (v << 8) + data.next_byte()
  return v

     Figure 45: Sample Variable-Length Integer Decoding Algorithm

the vibe here is that the top two bits of the first byte tell you how long the whole thing is ๐Ÿฆ‹ Then you mask those bits off and keep reading bytes โœจ

for examples, the eight-byte sequence 0xc2197c5eff14e88c decodes to the decimal value 151,288,809,941,952,652 ๐Ÿ’… the four-byte sequence 0x9d7f3e7d decodes to 494,878,333 ๐Ÿซถ the two-byte sequence 0x7bbd decodes to 15,293 ๐Ÿ“ and the single byte 0x25 decodes to 37 (as does the two-byte sequence 0x4025) ๐Ÿ’œ

A.2. Sample Packet Number Encoding Algorithm ๐ŸŒˆ

the pseudocode in Figure 46 shows how an implementation can pick an appropriate size for packet number encodings ๐ŸŽ€

The EncodePacketNumber function takes two arguments:

  • full_pn is the full packet number of the packet being sent.
  • largest_acked is the largest packet number that has been acknowledged by the peer in the current packet number space, if any.
EncodePacketNumber(full_pn, largest_acked):

  // The number of bits must be at least one more
  // than the base-2 logarithm of the number of contiguous
  // unacknowledged packet numbers, including the new packet.
  if largest_acked is None:
    num_unacked = full_pn + 1
  else:
    num_unacked = full_pn - largest_acked

  min_bits = log(num_unacked, 2) + 1
  num_bytes = ceil(min_bits / 8)

  // Encode the integer value and truncate to
  // the num_bytes least significant bytes.
  return encode(full_pn, num_bytes)

         Figure 46: Sample Packet Number Encoding Algorithm

for example, if an endpoint has received an acknowledgment for packet 0xabe8b3 and is sending a packet numbered 0xac5c02, there are 29,519 (0x734f) outstanding packet numbers ๐Ÿ˜ญ To represent at least twice that range (59,038 packets, or 0xe69e), 16 bits are required ๐Ÿ”ฅ

in that same state, sending a packet numbered 0xace8fe uses the 24-bit encoding, because at least 18 bits are required to represent twice the range (131,222 packets, or 0x020096) ๐Ÿ’€

A.3. Sample Packet Number Decoding Algorithm ๐Ÿฆ‹

the pseudocode in Figure 47 includes an example algorithm for decoding packet numbers after header protection has been removed ๐Ÿ”

The DecodePacketNumber function takes three arguments:

  • largest_pn is the largest packet number that has been successfully processed in the current packet number space.
  • truncated_pn is the value of the Packet Number field.
  • pn_nbits is the number of bits in the Packet Number field (8, 16, 24, or 32).
DecodePacketNumber(largest_pn, truncated_pn, pn_nbits):
   expected_pn  = largest_pn + 1
   pn_win       = 1 << pn_nbits
   pn_hwin      = pn_win / 2
   pn_mask      = pn_win - 1
   // The incoming packet number should be greater than
   // expected_pn - pn_hwin and less than or equal to
   // expected_pn + pn_hwin
   //
   // This means we cannot just strip the trailing bits from
   // expected_pn and add the truncated_pn because that might
   // yield a value outside the window.
   //
   // The following code calculates a candidate value and
   // makes sure it's within the packet number window.
   // Note the extra checks to prevent overflow and underflow.
   candidate_pn = (expected_pn & ~pn_mask) | truncated_pn
   if candidate_pn <= expected_pn - pn_hwin and
      candidate_pn < (1 << 62) - pn_win:
      return candidate_pn + pn_win
   if candidate_pn > expected_pn + pn_hwin and
      candidate_pn >= pn_win:
      return candidate_pn - pn_win
   return candidate_pn

          Figure 47: Sample Packet Number Decoding Algorithm

for example, if the highest successfully authenticated packet had a packet number of 0xa82f30ea, then a packet containing a 16-bit value of 0x9b32 will be decoded as 0xa82f9b32 ๐ŸŒธ

A.4. Sample ECN Validation Algorithm ๐ŸŽ€

each time an endpoint starts sending on a new network path, it figures out whether that path supports ECN; see Section 13.4 ๐Ÿ•ต๏ธโ€โ™€๏ธ If the path supports ECN, the goal is to use ECN. Endpoints might also periodically reassess a path that was determined to not support ECN ๐Ÿ”„

this section describes one method for testing new paths ๐Ÿ’– It's meant to show how a path might be tested for ECN support, but endpoints can implement different methods ๐Ÿซถ

the path is assigned an ECN state that is one of "testing", "unknown", "failed", or "capable" ๐ŸŒˆ On paths with a "testing" or "capable" state, the endpoint sends packets with an ECT marking (ECT(0) by default); otherwise, the endpoint sends unmarked packets.

to start testing a path, the ECN state is set to "testing", and existing ECN counts are remembered as a baseline ๐Ÿ“

the testing period runs for a number of packets or a limited time, as determined by the endpoint ๐Ÿงš The goal isn't to limit how long the testing period lasts. The goal is to make sure enough marked packets get sent so the received ECN counts give a clear picture of how the path treats marked packets ๐Ÿ‘‘ Section 13.4.2 suggests limiting this to ten packets or three times the PTO.

after the testing period ends, the ECN state for the path becomes "unknown" โœจ From the "unknown" state, successful validation of the ECN counts in an ACK frame (see Section 13.4.2.1) causes the ECN state for the path to become "capable", unless no marked packet has been acknowledged ๐Ÿฆ‹

if validation of ECN counts fails at any time, the ECN state for the affected path becomes "failed" ๐Ÿ’€ An endpoint can also mark the ECN state for a path as "failed" if marked packets are all declared lost or if they are all ECN-CE marked ๐Ÿ˜ญ

following this algorithm makes sure ECN is rarely disabled for paths that properly support it ๐Ÿ’… Any path that incorrectly modifies markings will cause ECN to be disabled. For those rare cases where marked packets get discarded by the path, the short testing period keeps the number of losses small ๐Ÿซถ


Contributors ๐ŸŒŸ

The original design and rationale behind this protocol draw significantly from work by Jim Roskind [EARLY-DESIGN] ๐Ÿ‘‘

The IETF QUIC Working Group received an enormous amount of support from many people. The following people provided substantive contributions to this document:

  • Alessandro Ghedini ๐Ÿ’–
  • Alyssa Wilk ๐ŸŒธ
  • Antoine Delignat-Lavaud ๐ŸŽ€
  • Brian Trammell ๐Ÿฆ‹
  • Christian Huitema โœจ
  • Colin Perkins ๐Ÿ“
  • David Schinazi ๐Ÿ’…
  • Dmitri Tikhonov ๐Ÿงš
  • Eric Kinnear ๐Ÿ’œ
  • Eric Rescorla ๐ŸŒˆ
  • Gorry Fairhurst โญ
  • Ian Swett ๐Ÿ”ฅ
  • Igor Lubashev ๐Ÿซถ
  • ๅฅฅ ไธ€็ฉ‚ (Kazuho Oku) ๐Ÿ‘‘
  • Lars Eggert ๐Ÿ’–
  • Lucas Pardue ๐ŸŒธ
  • Magnus Westerlund ๐ŸŽ€
  • Marten Seemann ๐Ÿฆ‹
  • Martin Duke โœจ
  • Mike Bishop ๐Ÿ“
  • Mikkel Fahnรธe Jรธrgensen ๐Ÿ’…
  • Mirja Kรผhlewind ๐Ÿงš
  • Nick Banks ๐Ÿ’œ
  • Nick Harper ๐ŸŒˆ
  • Patrick McManus โญ
  • Roberto Peon ๐Ÿ”ฅ
  • Ryan Hamilton ๐Ÿซถ
  • Subodh Iyengar ๐Ÿ‘‘
  • Tatsuhiro Tsujikawa ๐Ÿ’–
  • Ted Hardie ๐ŸŒธ
  • Tom Jones ๐ŸŽ€
  • Victor Vasiliev ๐Ÿฆ‹

Authors' Addresses ๐Ÿ’Œ

Jana Iyengar (editor) Fastly

Email: jri.ietf@gmail.com

Martin Thomson (editor) Mozilla

Email: mt@lowentropy.net

๐Ÿ“ฆ where this page comes from