§10.5
Chapter 10 · Connection Migration, Resilience, and Session Continuity

§10.5Practical Example: Verifying Migration with Packet Captures

RFC 9000Runnable lab

Learning objective. Trigger a connection migration on a loopback QUIC connection and confirm, from the server's qlog, that the transport handled it: a new path appears, the server issues a PATH_CHALLENGE, and the client's PATH_RESPONSE echoes the same 8 bytes to validate the path.

Making a migration happen on purpose #

Migration is triggered by an external event (a network change) that a loopback test does not naturally produce. The lab manufactures one the way §10.3 described a NAT: it puts a tiny UDP proxy between client and server, and mid-connection rebinds the proxy's server-facing socket to a new port. The client is unaware and keeps its connection ID; the server sees packets arrive from a new source address and must decide what to do. That is exactly the involuntary-rebinding case (§10.3), and it exercises the server's path-validation machinery for real.

Server (qlog)Proxy (the "NAT")Clientrebind — swapserver-facing socketto port B(involuntary)known connectionID, new address →validate the pathqlog: challenge sent= response received→ path validated,connection survivedhandshake + "before-migration"datafrom server-facing port A"after-migration" STREAM data(non-probing)from NEW port BPATH_CHALLENGE · data =8a94575a0bc4da76(relayed)PATH_RESPONSE · data =8a94575a0bc4da76 (echo)(relayed)
Fig. 10.5-1The lab's setup. A proxy relays between client and server; after the handshake and a first message it rebinds its server-facing socket from port A to port B. The client's next STREAM (non-probing) packet reaches the server from the new port, so the server validates the path with a PATH_CHALLENGE, and the client's PATH_RESPONSE echoes the identical 8 bytes — captured in the server's qlog.RFC 9000 §9.3, §8.2

The key detail is that the client sends a STREAM frame after the rebinding. STREAM is a non-probing frame (§10.1), and a non-probing packet from a new address is precisely what tells the server the peer has moved ([RFC 9000 §9.3]).

Running it #

captured output — uv run --with aioquic==1.3.0 labs/10-5-migration-capture/migration_capture.pyTEXT
handshake complete — server sees client via proxy port 51226
NAT rebinding — server-facing source port 51226 -> 61649

server qlog: a929ea97474928d9.qlog
  server sent PATH_CHALLENGE to new address, data = 8a94575a0bc4da76
  client's PATH_RESPONSE echoed back,        data = 8a94575a0bc4da76
  8 bytes match (proof of reachability)           = True

check: address change → PATH_CHALLENGE/PATH_RESPONSE validate the new path → connection survived : OK

Reading the capture #

Every claim from §10.1 is visible in the server's own trace:

  • The address changed. The proxy relayed the client through source port 51226, then rebound to 61649. To the server this is an address change on a known connection ID: indistinguishable, at first sight, from a deliberate migration (§10.3).
  • The server validated the new path. Its qlog records a packet_sent carrying a PATH_CHALLENGE frame with 8 bytes of unpredictable data (8a94575a0bc4da76): the server refusing to trust the new address until the peer proves it holds it ([RFC 9000 §8.2]).
  • The client proved reachability. The qlog records a packet_received carrying a PATH_RESPONSE with the identical 8 bytes. "Path validation succeeds when a PATH_RESPONSE frame is received that contains the data that was sent in a previous PATH_CHALLENGE" ([RFC 9000 §8.2.3]). The two hex strings match exactly, which is the whole proof: only an endpoint genuinely at the new address could echo bytes it had to receive to know.
  • The connection survived. No handshake, no new connection, no application-visible break. The same connection simply continued from the new address once the path was validated.

The 8-byte echo is worth pausing on: it is the entire security argument of path validation reduced to two lines of a trace. An off-path attacker that spoofed the new address never sees the challenge, so it cannot produce the response; the match is what distinguishes a real peer from a spoofed source ([RFC 9000 §19.17]).

A verification checklist #

The same qlog reading generalizes into how you confirm migration works in any deployment:

  1. Find the address change. In the server trace, look for packets on the connection arriving from a new source address (some tools log a path event; otherwise the datagram source changes).
  2. Confirm a PATH_CHALLENGE was sent to it. If the server sent stream data to the new address without a challenge, address validation is misconfigured — a spoofing risk.
  3. Confirm a matching PATH_RESPONSE returned. Equal challenge and response data is proof of reachability; a missing or mismatched response means validation failed and the server should have reverted to the last validated address ([RFC 9000 §9.3.2]).
  4. Check the congestion state. On a full path change, expect a congestion-control/RTT reset (§10.1); on a port-only change, expect it to be retained (§10.3). A reset on every NAT rebinding is a performance bug.
Note

Driving migration with a rebinding proxy is a test technique, not how migration happens in production, where the client's own address changes as it moves between networks. But the proxy isolates the one variable that matters (the address the server sees) while keeping everything else on loopback and deterministic, so the path-validation exchange can be observed without a real mobile network. The same harness is a useful regression test: assert that a rebinding produces a challenge/response pair and no connection close.

In practice

editorial Make this capture part of how you validate a migration deployment, because migration bugs are silent until a real network change hits them. Keep a proxy-based rebinding test in CI that asserts the PATH_CHALLENGE/PATH_RESPONSE pair appears and the connection stays up; separately, capture a real migration (a mobile client walking off Wi-Fi) once and confirm the same trace shape end to end. The two failure modes to catch are a server that sends data to an unvalidated new address (no challenge, a reflection risk) and one that resets congestion control on a bare port change (needless throughput dips). Both are visible in exactly the qlog fields this lab reads.

Takeaways #

A rebinding proxy triggers migration on demand, and the server's qlog proves the transport handled it: a new source address, a PATH_CHALLENGE of 8 unpredictable bytes, and a PATH_RESPONSE echoing the same 8 bytes (matching data being the reachability proof), after which the connection continues unbroken. The generalized checklist (find the address change, confirm a challenge, confirm a matching response, check the congestion reset) is how you verify migration anywhere. That closes the migration chapter; the chapter quiz checks the model, and Chapter 11 turns to observing QUIC in production — the qlog, metrics, and traces this lab just relied on.