RFC 9000
QUIC: A UDP-Based Multiplexed and Secure Transport
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 Notice ๐
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, andCONNECTION_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 longx (i): Indicates that x holds an integer value using the variable-length encoding described in Section 16x (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 boundaryx (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 abovex (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 Lx (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:STREAMframes that show up after you've sentSTOP_SENDINGstill 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
STREAMframes 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_addresstransport 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_idtransport parameter is missing from the server after it received a Retry packet, - the
retry_source_connection_idtransport 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_limitinitial_max_datainitial_max_stream_data_bidi_localinitial_max_stream_data_bidi_remoteinitial_max_stream_data_uniinitial_max_streams_bidiinitial_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_CLOSEframe 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_CLOSEframe in a 0-RTT packet cannot be assured that the server has accepted 0-RTT. Sending aCONNECTION_CLOSEframe 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_CLOSEframe in both Handshake and 1-RTT packets. A server SHOULD also send aCONNECTION_CLOSEframe 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 type0x1ccan 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 (type0x1d) 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 theack_delay_exponenttransport 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 typeFRAME_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 typeFRAME_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 typeSTREAM_LIMIT_ERRORorFRAME_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 typeFRAME_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 ๐- an endpoint got a STREAM frame with data past the final size that was already established
- an endpoint got a STREAM frame or a RESET_STREAM frame with a final size lower than the stream data it already received
- 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 advertisedactive_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 ๐:
- 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 ๐ซ.
- 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 ๐ .
- An on-path attacker cannot prevent a client from migrating to a path for which the attacker is not on-path ๐โโ๏ธ๐จ.
- An on-path attacker can reduce the throughput of a connection by delaying packets or dropping them ๐.
- 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 ๐ :
- An off-path attacker can race packets and attempt to become a "limited" on-path attacker ๐๏ธ.
- 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 ๐ถ.
- An off-path attacker cannot cause a connection to close once the handshake has completed ๐.
- An off-path attacker cannot cause migration to a new path to fail if it cannot observe the new path ๐.
- 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 ๐.
- 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 ๐ :
- A limited on-path attacker cannot cause a connection to close once the handshake has completed ๐.
- A limited on-path attacker cannot cause an idle connection to close if the client is first to resume activity ๐.
- 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 ๐
- [BCP38] Ferguson, P. and D. Senie, "Network Ingress Filtering: Defeating Denial of Service Attacks which employ IP Source Address Spoofing", BCP 38, RFC 2827, May 2000.
- [DPLPMTUD] Fairhurst, G., Jones, T., Tรผxen, M., Rรผngeler, I., and T. Vรถlker, "Packetization Layer Path MTU Discovery for Datagram Transports", RFC 8899, DOI 10.17487/RFC8899, September 2020.
- [EARLY-ASSIGN] Cotton, M., "Early IANA Allocation of Standards Track Code Points", BCP 100, RFC 7120, DOI 10.17487/RFC7120, January 2014.
- [IPv4] Postel, J., "Internet Protocol", STD 5, RFC 791, DOI 10.17487/RFC0791, September 1981.
- [QUIC-INVARIANTS] Thomson, M., "Version-Independent Properties of QUIC", RFC 8999, DOI 10.17487/RFC8999, May 2021.
- [QUIC-RECOVERY] Iyengar, J., Ed. and I. Swett, Ed., "QUIC Loss Detection and Congestion Control", RFC 9002, DOI 10.17487/RFC9002, May 2021.
- [QUIC-TLS] Thomson, M., Ed. and S. Turner, Ed., "Using TLS to Secure QUIC", RFC 9001, DOI 10.17487/RFC9001, May 2021.
- [RFC1191] Mogul, J. and S. Deering, "Path MTU discovery", RFC 1191, DOI 10.17487/RFC1191, November 1990.
- [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997.
- [RFC3168] Ramakrishnan, K., Floyd, S., and D. Black, "The Addition of Explicit Congestion Notification (ECN) to IP", RFC 3168, DOI 10.17487/RFC3168, September 2001.
- [RFC3629] Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, November 2003.
- [RFC6437] Amante, S., Carpenter, B., Jiang, S., and J. Rajahalme, "IPv6 Flow Label Specification", RFC 6437, DOI 10.17487/RFC6437, November 2011.
- [RFC8085] Eggert, L., Fairhurst, G., and G. Shepherd, "UDP Usage Guidelines", BCP 145, RFC 8085, DOI 10.17487/RFC8085, March 2017.
- [RFC8126] Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, June 2017.
- [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017.
- [RFC8201] McCann, J., Deering, S., Mogul, J., and R. Hinden, Ed., "Path MTU Discovery for IP version 6", STD 87, RFC 8201, DOI 10.17487/RFC8201, July 2017.
- [RFC8311] Black, D., "Relaxing Restrictions on Explicit Congestion Notification (ECN) Experimentation", RFC 8311, DOI 10.17487/RFC8311, January 2018.
- [TLS13] Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, August 2018.
- [UDP] Postel, J., "User Datagram Protocol", STD 6, RFC 768, DOI 10.17487/RFC0768, August 1980.
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 ๐ธ
- [AEAD] McGrew, D., "An Interface and Algorithms for Authenticated Encryption", RFC 5116, DOI 10.17487/RFC5116, January 2008.
- [ALPN] Friedl, S., Popov, A., Langley, A., and E. Stephan, "Transport Layer Security (TLS) Application-Layer Protocol Negotiation Extension", RFC 7301, DOI 10.17487/RFC7301, July 2014.
- [ALTSVC] Nottingham, M., McManus, P., and J. Reschke, "HTTP Alternative Services", RFC 7838, DOI 10.17487/RFC7838, April 2016.
- [COOKIE] Barth, A., "HTTP State Management Mechanism", RFC 6265, DOI 10.17487/RFC6265, April 2011.
- [CSRF] Barth, A., Jackson, C., and J. Mitchell, "Robust defenses for cross-site request forgery", Proceedings of the 15th ACM conference on Computer and communications security - CCS '08, DOI 10.1145/1455770.1455782, 2008.
- [EARLY-DESIGN] Roskind, J., "QUIC: Multiplexed Stream Transport Over UDP", 2 December 2013.
- [GATEWAY] Hรคtรถnen, S., Nyrhinen, A., Eggert, L., Strowes, S., Sarolahti, P., and M. Kojo, "An experimental study of home gateway characteristics", Proceedings of the 10th ACM SIGCOMM conference on Internet measurement - IMC '10, DOI 10.1145/1879141.1879174, November 2010.
- [HTTP2] Belshe, M., Peon, R., and M. Thomson, Ed., "Hypertext Transfer Protocol Version 2 (HTTP/2)", RFC 7540, DOI 10.17487/RFC7540, May 2015.
- [IPv6] Deering, S. and R. Hinden, "Internet Protocol, Version 6 (IPv6) Specification", STD 86, RFC 8200, DOI 10.17487/RFC8200, July 2017.
- [QUIC-MANAGEABILITY] Kuehlewind, M. and B. Trammell, "Manageability of the QUIC Transport Protocol", Work in Progress, Internet-Draft, draft-ietf-quic-manageability-11, 21 April 2021.
- [RANDOM] Eastlake 3rd, D., Schiller, J., and S. Crocker, "Randomness Requirements for Security", BCP 106, RFC 4086, DOI 10.17487/RFC4086, June 2005.
- [RFC1812] Baker, F., Ed., "Requirements for IP Version 4 Routers", RFC 1812, DOI 10.17487/RFC1812, June 1995.
- [RFC1918] Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. J., and E. Lear, "Address Allocation for Private Internets", BCP 5, RFC 1918, DOI 10.17487/RFC1918, February 1996.
- [RFC2018] Mathis, M., Mahdavi, J., Floyd, S., and A. Romanow, "TCP Selective Acknowledgment Options", RFC 2018, DOI 10.17487/RFC2018, October 1996.
- [RFC2104] Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, DOI 10.17487/RFC2104, February 1997.
- [RFC3449] Balakrishnan, H., Padmanabhan, V., Fairhurst, G., and M. Sooriyabandara, "TCP Performance Implications of Network Path Asymmetry", BCP 69, RFC 3449, DOI 10.17487/RFC3449, December 2002.
- [RFC4193] Hinden, R. and B. Haberman, "Unique Local IPv6 Unicast Addresses", RFC 4193, DOI 10.17487/RFC4193, October 2005.
- [RFC4291] Hinden, R. and S. Deering, "IP Version 6 Addressing Architecture", RFC 4291, DOI 10.17487/RFC4291, February 2006.
- [RFC4443] Conta, A., Deering, S., and M. Gupta, Ed., "Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification", STD 89, RFC 4443, DOI 10.17487/RFC4443, March 2006.
- [RFC4787] Audet, F., Ed. and C. Jennings, "Network Address Translation (NAT) Behavioral Requirements for Unicast UDP", BCP 127, RFC 4787, DOI 10.17487/RFC4787, January 2007.
- [RFC5681] Allman, M., Paxson, V., and E. Blanton, "TCP Congestion Control", RFC 5681, DOI 10.17487/RFC5681, September 2009.
- [RFC5869] Krawczyk, H. and P. Eronen, "HMAC-based Extract-and-Expand Key Derivation Function (HKDF)", RFC 5869, DOI 10.17487/RFC5869, May 2010.
- [RFC7983] Petit-Huguenin, M. and G. Salgueiro, "Multiplexing Scheme Updates for Secure Real-time Transport Protocol (SRTP) Extension for Datagram Transport Layer Security (DTLS)", RFC 7983, DOI 10.17487/RFC7983, September 2016.
- [RFC8087] Fairhurst, G. and M. Welzl, "The Benefits of Using Explicit Congestion Notification (ECN)", RFC 8087, DOI 10.17487/RFC8087, March 2017.
- [RFC8981] Gont, F., Krishnan, S., Narten, T., and R. Draves, "Temporary Address Extensions for Stateless Address Autoconfiguration in IPv6", RFC 8981, DOI 10.17487/RFC8981, February 2021.
- [SEC-CONS] Rescorla, E. and B. Korver, "Guidelines for Writing RFC Text on Security Considerations", BCP 72, RFC 3552, DOI 10.17487/RFC3552, July 2003.
- [SLOWLORIS] "RSnake" Hansen, R., "Welcome to Slowloris - the low bandwidth, yet greedy and poisonous HTTP client!", June 2009.
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
- ietf: rfc9000 2021-05
- ๐ girlified by claude-haiku-5-5 on : a language model's rewrite of the original, under its terms