§11.3
Chapter 11 · Observability, Measurement, and Performance Engineering

§11.3Interpreting Traces with Wireshark and Key Log Files

RFC 9001

Learning objective. Turn an opaque QUIC packet capture into readable frames: why a capture alone shows nothing, what the TLS key log contains, how Wireshark uses it to derive the packet-protection keys, and how the decrypted view complements qlog and passive observation.

Why a raw capture is opaque #

Capture QUIC with tcpdump and open it, and you see UDP datagrams full of ciphertext. Everything of interest is encrypted: the payload, of course, but also (because of header protection) the packet numbers and several header bits, which are "protected using a key that is derived separately from the packet protection key" ([RFC 9001 §5.4]). You cannot even reliably tell one packet's number from the next. To read the capture you need the connection's secrets, and QUIC's whole security model means those live only at the endpoints.

What the key log contains #

The bridge is a TLS key log file, the same SSLKEYLOGFILE mechanism used to debug TLS over TCP. It is a text file, one secret per line, that an endpoint you control writes during the handshake. Two things to be clear about:

  • It is a tooling convention, not an RFC. The key-log file format and its label names (CLIENT_TRAFFIC_SECRET_0, etc.) are an NSS/Wireshark convention; RFC 9001 defines the secrets but says nothing about logging them to a file (§11.1). Every major QUIC library can emit one; the labs in this book do, via aioquic's secrets_log_file.
  • The secrets are the TLS 1.3 traffic secrets. Each encryption level and direction has its own: the handshake secrets (CLIENT_HANDSHAKE_TRAFFIC_SECRET, SERVER_HANDSHAKE_TRAFFIC_SECRET), the 1-RTT application secrets (CLIENT_TRAFFIC_SECRET_0, SERVER_TRAFFIC_SECRET_0), and, for 0-RTT, CLIENT_EARLY_TRAFFIC_SECRET. These "traffic secrets are derived by TLS" and are what QUIC keys its protection from ([RFC 9001 §5.1]). (Initial-packet keys need no log: they are derivable from the client's Destination Connection ID, [RFC 9001 §5.2].)

From secret to readable frame #

Wireshark, given the pcap and the key log, reproduces exactly what the endpoints did ([RFC 9001 §5.1]): from each traffic secret it runs HKDF-Expand-Label with the labels quic key, quic iv, and quic hp to derive the AEAD key, the IV, and the header-protection key for that level and direction. With the header-protection key it un-masks the packet number and header bits; with the AEAD key and IV it decrypts the payload. The result is a fully dissected QUIC connection.

traffic secrets

tcpdump / tshark

derive quic key / iv / hp
HKDF-Expand-Label

TLS 1.3 handshake

keys.keylog
CLIENT/SERVER _TRAFFIC_SECRET

encrypted UDP datagrams

capture.pcap

Wireshark

decrypted frames
HEADERS · STREAM · ACK
both directions

Fig. 11.3-1Decrypting a capture. The TLS handshake's traffic secrets are written to a key log; the encrypted datagrams are captured to a pcap. Wireshark reads both, derives the quic key / iv / hp for each level and direction (HKDF-Expand-Label, RFC 9001 §5.1), and dissects the frames in both directions.RFC 9001 §5.1, §5.4

The workflow #

In practice the steps are:

  1. Capture the packets. tcpdump -i any -w capture.pcap 'udp port 443' (or tshark) records the raw datagrams (encrypted, but complete).
  2. Export the key log. Set SSLKEYLOGFILE=keys.keylog for a browser or curl, or configure your library to write it (the §1.5 lab does this with aioquic). The endpoint appends its traffic secrets as the handshake progresses.
  3. Point Wireshark at the key log. In Preferences → Protocols → TLS → (Pre)-Master-Secret log filename, select keys.keylog. Wireshark links each capture to its secrets by the TLS ClientHello random and decrypts.
  4. Read the frames. Now the QUIC dissector shows every packet's number, every frame (HEADERS, STREAM, ACK, MAX_DATA, NEW_CONNECTION_ID, …), the stream IDs and offsets, and the HTTP/3 layer on top, filterable with display filters like quic and http3.

The book's §1.5 lab already produces both artifacts — a qlog and a keys.keylog — from a loopback handshake, so you can practice this decryption on a capture you generated yourself.

What the decrypted view adds #

The decrypted capture is the byte-exact ground truth that the other vantage points (§11.1) cannot give:

  • It is both directions, on the wire, exactly as sent, where qlog is one endpoint's belief about what it did.
  • It shows the actual frames and their order within packets, which resolves ambiguities a metric cannot: whether a MAX_DATA was really sent, whether a stream was reset, whether two implementations disagree about framing.
  • It is the arbiter for interop bugs (Chapter 13): when two stacks won't talk, the decrypted capture shows which one sent something the other rejected.

Its limits are equally clear: it needs the keys (so you must control an endpoint or have its log), it is after-the-fact rather than a live metric, and it does not show congestion-control state: cwnd and bytes_in_flight live only in the endpoint's qlog (§11.2), never on the wire.

Worked example: correlating three views #

A stream stalls intermittently. The three vantage points converge. qlog shows bytes_in_flight well below cwnd (not congestion-limited), with the stalls aligned to gaps in received data. A passive spin-bit trace shows the path RTT steady, ruling out a path problem. The decrypted capture then shows the smoking gun: the peer's MAX_STREAM_DATA updates arrive in bursts, not steadily, so the sender repeatedly hits the flow-control limit and waits — a flow-control autotuning bug on the peer (§7.2). No single view proves it: qlog says "not congestion," passive says "not the path," and only the byte-exact capture reveals the sporadic MAX_STREAM_DATA. Correlating all three, lined up by timestamp, is how hard cases are cracked.

Note

A key log is a live decryption key for the connection. Treat it as a secret. Anyone with the pcap and the key log can read everything the connection carried, including credentials in headers or bodies. So export key logs only for connections and environments you intend to expose (test traffic, your own staging), never production user traffic, and delete them after use. The same property that makes the key log invaluable for debugging makes it a liability if it leaks; RFC 9001 goes to considerable length to keep these secrets off the wire, and a key log deliberately writes them to disk.

In practice

editorial Wire up the capture-plus-keylog path once, in a test harness, before you need it in an incident. Have a script that runs tcpdump alongside your client with SSLKEYLOGFILE set, so any repro produces a decryptable capture automatically. Hunting for keys after the fact is too late. In Wireshark, learn three filters: quic (all QUIC), quic.frame_type (isolate a frame kind, e.g. flow-control or reset frames), and http3 (the application layer). And remember the division of labor: reach for the decrypted capture to answer "what exact bytes were exchanged," and stay in qlog for "what was my congestion-control state": the capture cannot answer the second.

Takeaways #

A QUIC capture is opaque because payload and packet numbers are encrypted, so reading it requires the connection's TLS traffic secrets, exported to a key-log file (a tooling convention). Wireshark derives the quic key/iv/hp from those secrets and dissects every frame in both directions, the byte-exact ground truth that resolves what metrics and one-sided qlogs leave ambiguous, at the cost of needing the keys and showing no congestion-control state. To make such captures reproducible under controlled network conditions, §11.4 turns to network emulation.