§1.5
Chapter 01 · Foundations of QUIC and HTTP3

§1.5Practical Lab Setup for Capturing Traces and Verifying Behavior

RFC 9000Runnable lab

Learning objective. Stand up the book's capture toolchain and read a real QUIC handshake from a trace, so every later chapter can be checked against observable behavior instead of taken on faith.

Why QUIC needs cooperative observation #

Because QUIC encrypts almost everything past the first header byte, you cannot learn much by pointing a packet sniffer at the wire alone: the packet numbers, frames, and payload are ciphertext (§1.4). Observing QUIC therefore means getting help from an endpoint, in one of two ways:

  • qlog. The endpoint emits a structured JSON log of exactly what it did: packets sent and received, frames, RTT estimates, congestion state. This is the highest-signal view and needs no decryption (schema: draft-ietf-quic-qlog-*).
  • packet capture + keylog. Capture raw UDP with tcpdump, and have the endpoint write its TLS secrets to a keylog file so Wireshark can decrypt the capture after the fact. QUIC reuses the TLS 1.3 keylog mechanism ([RFC 9001 §6] key schedule; the NSS SSLKEYLOGFILE format).

UDP · encrypted

qlog events

qlog events

TLS secrets

aioquic client

aioquic server

client .qlog

server .qlog

keys.keylog

qvis / jq
event timeline · metrics

pcap
(tcpdump)

Wireshark

decrypted
packet view

Fig. 1.5-1Two observation paths. qlog is emitted directly by the endpoints; a pcap must be paired with the TLS keylog before Wireshark can decrypt it.draft-ietf-quic-qlog; RFC 9001 §6

The reference stack #

Every lab in this book uses Python + aioquic, pinned to the version in labs/requirements.txt:

labs/requirements.txtTEXT
aioquic==1.3.0

aioquic is a good teaching stack: it is readable Python, implements QUIC v1 and HTTP/3, and has qlog and keylog support built in. The capture lab runs a minimal server and client on loopback (no external network), completes a handshake, sends a PING, and writes both a qlog and a keylog. The part that turns capture on is just two configuration fields:

labs/01-5-capture/run_capture.pyPYTHON
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.logger import QuicFileLogger

client_cfg = QuicConfiguration(
    is_client=True, alpn_protocols=["hq-interop"],
    quic_logger=QuicFileLogger("qlogs"),      # -> qlogs/<odcid>.qlog
)
client_cfg.secrets_log_file = open("keys.keylog", "w")   # -> Wireshark decryption

Run it with the pinned toolchain:

BASH
uv run labs/01-5-capture/run_capture.py

Reading a real handshake #

The run below is genuine output from the lab, not a mock-up. The packet timeline is reconstructed from the client's qlog; timestamps are relative to the first packet:

captured output — uv run labs/01-5-capture/run_capture.pyTEXT
handshake:    complete
alpn:         hq-interop
cipher:       AES_256_GCM_SHA384 (0x1302)
ping RTT:     1.39 ms (loopback)

packet timeline (client vantage, t relative to first packet):
  t=  0.00ms  TX ->  initial    [crypto]
  t=  2.41ms  RX <-  initial    [ack,crypto]
  t=  2.64ms  RX <-  handshake  [crypto]
  t=  2.76ms  TX ->  initial    [ack]
  t=  2.76ms  TX ->  handshake  [ack]
  t=  2.80ms  RX <-  handshake  [crypto]
  t=  3.54ms  TX ->  handshake  [ack,crypto]
  t=  3.55ms  TX ->  1RTT       [new_connection_id]
  t=  3.70ms  TX ->  1RTT       [padding,ping]
  t=  3.91ms  RX <-  1RTT       [handshake_done,new_connection_id]
  t=  5.00ms  TX ->  1RTT       [ack]
  t=  5.09ms  TX ->  1RTT       [connection_close]

Read against Chapter 1, every line is predictable. The client opens with an Initial carrying a CRYPTO frame (the TLS ClientHello). The server answers in one flight — Initial (ServerHello) plus Handshake packets (EncryptedExtensions, Certificate, Finished) — using the separate Initial and Handshake number spaces (§1.2); note the client ACKs each space independently. Once the client sends its Handshake CRYPTO (Finished), it immediately promotes to 1-RTT packets: a NEW_CONNECTION_ID (supplying spare CIDs for migration, §1.4), then our PADDING+PING. The server confirms with HANDSHAKE_DONE, and the connection closes. The cipher line shows the negotiated AEAD, and four secrets are written to the keylog, one per key phase Wireshark needs.

Note

The padding,ping packet is the lab's own probe, and handshake_done is the server telling the client the handshake is confirmed (a QUIC-only frame with no TLS analogue). Seeing exactly these frames, in this order, is how you verify an implementation is behaving, the same technique the debugging labs in §2.5 and §6.5 use for failures.

In practice

editorial A keylog file is a set of decryption keys: anyone who has it can read the capture. Enable SSLKEYLOGFILE and qlog only in development or with synthetic traffic, never for real users, and treat any captured *.keylog / *.qlog as sensitive. In production, prefer sampled qlog with payloads omitted. For interactive exploration, load a qlog into qvis (qvis.quictools.info, run locally) or filter events with jq; open a pcap in Wireshark with the keylog set under Preferences → Protocols → TLS. See §11.3.

Takeaways #

You now have a reproducible way to make QUIC visible: qlog for a structured account of what an endpoint did, and pcap-plus-keylog for a decrypted packet view. The handshake you just traced is the subject of the next chapter, which opens up each CRYPTO exchange and the keys derived from it, starting with the message flow and state transitions in §2.1.