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 NSSSSLKEYLOGFILEformat).
The reference stack #
Every lab in this book uses Python + aioquic, pinned to the version in
labs/requirements.txt:
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:
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:
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:
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.
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.
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.