Mastering QUIC: A Beginner’s Guide to Understanding and Capturing QUIC Packet Flow with Wireshark

Last week, I posted an article about s2n-quic, and a reader asked how to learn about network protocols like QUIC. For most internet professionals, although everyone interacts with networks daily, few may need to, or even care to, understand the specifics of network protocols beneath HTTP. Most of the time, just having a general understanding and knowing how to use them suffices. If you have no concept at all of QUIC, the image below will help you understand QUIC’s place in the HTTP/3 ecosystem well:

QUIC />

So, if you want to thoroughly understand QUIC, how should you begin?

As a former developer of network protocols and devices, my own insight is to start with an RFC and supplement it with Wireshark packet capturing to quickly grasp the target protocol.

For QUIC, the first document you need to read is RFC9000. Reading protocols can be very tedious, requiring some patience. If your English is not great, you can use Google Translate to convert it into your native language for a quick overview (skim read). In the first pass, focus on the main concepts and key processes.

Afterward, you can write programs using the QUIC protocol, then capture packets with Wireshark, analyzing the actual messages and comparing them against the RFC protocol content (close read) for a deeper understanding of the protocol’s essence.

We will use the code from the previous article as a base to build an echo client and server. For easy reading, I’ll also paste the code here. Interested readers can clone my repo and run the client/server code themselves.

Client code (see Github: tyrchen/rust-training/live_coding/quic-test/examples/client.rs):

QUIC />

Server code (see Github: tyrchen/rust-training/live_coding/quic-test/examples/server.rs):

These two code snippets establish a simple echo server. We can use Wireshark to monitor UDP packets on the local loopback interface. Note that for traffic using the QUIC protocol with TLS, even if packets are captured, only the initial few may be readable as the rest are encrypted. Therefore, when constructing the client/server, it is necessary to capture the session key negotiated between the server and client for Wireshark decryption. Typically, SSL/TLS libraries provide this functionality. For example, with Rustls, you can use key_log in the TLS config. If you look closely at the server code above, you’ll see the line:

代码语言:javascript复制

let config = Builder::new()    .with_certificate(CERT_PEM, KEY_PEM)?    .with_key_logging()? # Enable keylogging    .build()?;

After using key_log, when starting the server, you only need to specify the SSLKEYLOGFILE:

代码语言:javascript复制

SSLKEYLOGFILE=session.log cargo run --example server

Upon completing the packet capture, open Wireshark’s preference, choose the TLS protocol, and input the path of the log file:

Below is a complete packet capture of the client-server interaction, where you can see that all “protected payloads” are properly displayed:

Since our echo client performs the most basic tasks (opening only a bidirectional stream), through this capture, we can focus on studying the process of establishing a connection using the QUIC protocol.

“First Packet Sent by the Client via QUIC”

Let’s look at the first packet sent by the client:

This packet is very informative. First, unlike the TCP handshake, the initial QUIC packet is quite large, with 1200 bytes (the protocol requires UDP payloads to be at least 1200 bytes), containing a QUIC header, a 255-byte CRYPTO frame, and an 890-byte PADDING frame. From the header, we can see that this QUIC packet type is Initial.

QUIC Packet Types

For QUIC packets, the header is in plaintext; all subsequent frame payloads are encrypted (except for the first few packets). This initial packet is a Long Header packet, as defined in section 17.2 of RFC9000:

代码语言:javascript复制

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 (..), }

Those interested can read the relevant RFC chapter. There are several types of Long Header packets:

Since there is a Long Header packet, there is also a Short Header packet, which currently has only one type:

代码语言:javascript复制

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..),}

Why is a Connection ID Needed?

In the packet header we captured, there’s a new concept of Source Connection ID (SCID) and Destination Connection ID (DCID). You might wonder: Isn’t QUIC a protocol based on UDP/IP with a quintuple (src IP / src port / dst IP / dst port / protocol) describing a connection? Why introduce a new concept like connection ID?

This is to accommodate the increasing number of mobile scenarios. With a QUIC layer connection ID, changes in the underlying network (UDP/IP) won’t interrupt the QUIC connection, meaning that even as your mobile network switches from Wi-Fi (IP assigned by your fixed network operator) to a cellular network (IP assigned by your mobile operator) as you leave home, your QUIC application will only experience minor delays without needing to reestablish the QUIC connection.

Given this use case, using a connectionless UDP at the underlying level in QUIC is very necessary.

Does the Initial Packet Include TLS Hello?

Next, let’s look at the CRYPTO frame:

We can see that QUIC includes the TLS Client Hello in the CRYPTO frame within the initial connection packet and uses TLS version 1.3. In the Client Hello’s extension, the QUIC protocol uses a quic_transport_parameters extension to negotiate QUIC’s initial values, like the number of streams supported and the maximum number of active connection IDs within this connection.

Which Frames Does QUIC Support?

We’ve now seen two types of Frames: CRYPTO and PADDING. The table below lists all the frames supported by QUIC:

Server Response Packet

Let’s look at the server’s response packet:

There are some new elements here. Firstly, a single UDP packet can contain multiple QUIC payloads; we see the server responded with a QUIC Initial packet and a QUIC Handshake packet. In the Initial packet, we observed an ACK frame, indicating that even though QUIC is built on UDP, it incorporates a TCP-like acknowledgment mechanism within the QUIC protocol.

Previously, we saw in the Initial packet’s CRYPTO frame that the client sent the TLS Client Hello; similarly, the server sends the TLS Server Hello in the Initial packet’s CRYPTO frame. We’ll skip examining this part.

Within the Handshake packet:

The server sends its certificate and completes the TLS handshake.

Client Completes the Handshake

Let’s examine the third packet where the client completes the TLS handshake with the server:

This packet still contains two QUIC packets; the first is an ACK frame acknowledging the server’s QUIC packet with Server Hello, and the second includes an ACK frame acknowledging the server’s Handshake, followed by a CRYPTO frame concluding the TLS handshake from the client.

After the TLS handshake, the client and server begin application-layer data exchange, at which point all data is encrypted.

Client Sends a “hello” Message

In our echo client/server code, after connecting to the server, the client can wait for user input from stdin to send to the server. Once the server receives the data from the client, it sends it back unchanged, and the client outputs it to stdout. During this exchange, there are several QUIC packets for connection management, such as PING. We will skip those and only examine the packets transmitting application-layer data. Below is the packet from the client containing the “hello” message:

You can see that here the QUIC packet is a Short Header packet, containing a STREAM frame in addition to the ACK frame. The stream’s ID’s two least significant bits are 00, indicating it was initiated by the client and is a bidirectional stream. With two bits used to describe the type, QUIC has the following stream types:

The STREAM frame’s length is 6, and Data contains (68 65 6c 6c 6f 0a). If the content in Data is viewed in ASCII, it corresponds to “hello”, with a length of 6 bytes.

Server Replies with “hello” Message

Finally, the server’s echo back:

This is almost identical to the previous packet, so it won’t be explained again.

Moments of Insight

I hope that through this introduction to QUIC with packet captures using Wireshark, you now have a preliminary understanding of the QUIC protocol. In the last article, we mentioned that QUIC supports multiplexing and solves the problem of head-of-line blocking at the transport layer. Can you answer the following two questions from this article’s overview?

  1. Which frame type does QUIC use for multiplexing?
  2. How does QUIC solve the transport layer head-of-line blocking?