Skip to content

Instantly share code, notes, and snippets.

Last active September 9, 2023 15:21
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save martinthomson/744d04cbcec9be554f2f8e7bae2715b8 to your computer and use it in GitHub Desktop.
Save martinthomson/744d04cbcec9be554f2f8e7bae2715b8 to your computer and use it in GitHub Desktop.
QUIC header format proposal

There are two forms of QUIC common header: long and short. Long form packets are used for the initial exchange - until both 1-RTT packet protection can be started AND version negotiation is complete. Short form packets carry the bulk of the data.

This removes a lot of the flexibility that was the source of most of the objections to the current format. Fields are aligned on four octet boundaries. All long-form header variations have the exact same form. The connection ID is in the same place in both short and long form. The long form clearly identifies the role of the sender in the first octet and it identifies the packet as a QUIC packet.

The cost is that it makes occasional packets a little larger (the long header is 20 octets, whereas the existing form uses between 14 and 19 octets for initial handshake packets). This is an acceptable trade-off given that only a few of these packets are ever exchanged on a connection.

It makes most packets (the short header) 12 octets where it is possible to have 10 octet packets. However, a 10 octet packet header assumes an 8 bit packet number, this is 30.

Long Header

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|S|Typ|  Next   |              Magic "uic"/"UIC"                |
|                                                               |
+                         Connection ID                         +
|                                                               |
|                            Version                            |
|                         Packet Number                         |
|                       [Header Extensions]                   ...
|                           Payload                           ...

The first four octets:

  • Octet 0: Special
    • Bit 7 (i.e., 0x80): SHORT_HEADER (set to 0 here)
    • Bit 6-5: Type
      • 11 - client packet
      • 10 - server packet
      • 01 - public reset
      • 00 - version negotiation
    • Bits 4-0: Next protocol
      • 0b10001 indicates that it is QUIC handshake data
      • 0b01111 indicates that it is QUIC 0-RTT data
      • other values mean that the payload following the packet number contains an IPv6-style extension header
  • Octets 1-3: Magic (this can be short given that we will have a MAC as well)
    • 0x756963 for a client,
    • 0x554943 for a server

Note: A client packet starts with "quic", server starts with "QUIC", 0-RTT starts with "ouic".

Note(2): We might consider the second set of 7 bits to be a single code space, rather than use a 2+5 bit partitioning. That gives us a bit more flexibility and avoids meaningless combinations like public reset + 0-RTT.

The remainder of the packet layout is the same regardless of type, the difference being what rules for how to fill the values out and their semantics.

A client packet then contains:

  • Octets 4-11: connection ID (initially all zeroes/random)
  • Octets 12-15: version
  • Octets 16-19: packet number (low 4 octets, starts at a random 32-bit value)
  • Octets 20+: payload

Note: I'm not sure whether the client should pack the connection ID with random values. They would have no semantic value, though they might serve to provide proof that the server received the packet if we require echoing, see below.

A server packet contains a connection ID:

  • Octets 4-11: connection ID (server-selected value)
  • Octets 12-15: version (echoed)
  • Octets 16-19: packet number (low 4 octets, random 32-bit initial value)
  • Octets 20+: payload

A version negotiation packet contains:

  • Octets 4-11: connection ID (echoed)
  • Octets 12-15: version received (echoed)
  • Octets 16-19: packet number (echoed)
  • Octets 20+: payload = version list

A public reset packet contains:

  • Octets 4-11: connection ID (echoed)
  • Octets 12-15: version (echoed)
  • Octets 16-19: rejected packet number (echoed)
  • Octets 20+: payload = authentication data

Echoing details from the packet in both version negotiation and provides return routeability on version negotiation (#244), while maintaining a consistent header shape for all packets.

These long-form packets are used for anything that doesn't have 1-RTT packet protection and prior to the completion of version negotiation. Once both conditions are met, switch to sending short-form packets. I haven't defined any 5/7-bit code for protected long-form packets, but that's easy to do if we need to provide for them (see below for more on this).

Extension headers

An 8-bit space (like IPv6) carries an identifier for the protocol extension.

Each header extension takes the form:

  • Octet 1: next identifier
  • Octet 2: length

Since only the lower 5 (or 7) bits of this space is accessible from the outset, 0x00 is reserved for a null extension header, allowing the full space to be unlocked. 0x?a can be used for greasing.

I haven't talked to IP-layer people about whether they consider the IPv6 scheme this is based on to be successful. Either way, this should at least have plenty of hardware support.

FWIW, I'm not sure that we need the complexity that this adds. It's not clear that there is a motivating use case. However, on balance it's probably worth putting something in for the moment. If it turns out that we don't use it and can't find even a potential use case, then removing it is simple enough.

That said, this form could be used to pack a TLS Finished into 1-RTT packets if we define a 5- or 7-bit code for "small bit of handshake, followed by more", after which we can include a 1-RTT payload.

Short Header

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|S|K|                Packet Number (30)                         |
|                                                               |
+                         Connection ID                         +
|                                                               |

The short form header is defined to be specific to a version. Anything can change between protocol versions.

A short packet header - in this version - reserves two bits from the first octet:

  • SHORT_HEADER bit 7 (0x80) = 1,
  • KEY_PHASE bit 6 (0x40) = 0 initially

The remainder of the first four octets contain the packet number. 30 bits should be plenty. If a need is found for more flags, those can steal upper bits from the packet number. (Frankly, I suspect that 14 bits might be enough, but Ian was a little leery of that when I suggested it, and this keeps the connection ID in the same place in every packet.)

The connection ID follows on the next 8 octets.

If we need more bits, then we can steal them from the packet number.

Copy link

thanks for your help . I want to create regex pattern that can find quic protocol in packets data with python . can someone help me ?

Copy link

example for dns

Copy link

badbeef commented Jan 6, 2023

There is supposed to be Payload after the Short Header, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment