A whitepaper shaped like a packet capture
Most whitepapers describe a system from above. This one rides along with it. On 2026-07-26 at
13:22:12 UTC the PQ Crypta HTTP/3 Analyzer scanned pqcrypta.com, sent 18 UDP
datagrams, received 24, lost none, and produced a verdict. Every number on this page comes from that
single connection or from the source code that made it. Nothing is illustrative, nothing is
simulated, and the verdict stays sealed until the connection closes at the bottom.
The bar above is the connection's state machine. As you scroll, it advances the way the real
connection did: through address validation, the Initial flight, a post-quantum handshake, 1-RTT
application data, a battery of capability probes, and CONNECTION_CLOSE. The scanner behind all of
this is a Rust client built on noq
(the multipath-capable quinn fork), driven by a single Axum handler,
http3_scanner.rs, that opens a genuine QUIC connection to whatever you type into
the analyzer. It does not parse HTML about protocols. It speaks them.
The capture at a glance: every exchange in this paper, drawn in wire order and replayed on a loop. Probes (phases 6 and 7) run inside the gap before CONNECTION_CLOSE.
Phase 0 · Nothing is trusted, including the target
Before a single datagram leaves, the target string goes through scan_target.rs, a
validation module shared with the PQC readiness scanner: domain syntax, DNS resolution, and an SSRF
guard that refuses private, loopback, and link-local ranges so the scanner cannot be aimed at
internal infrastructure. Only a public, resolvable host reaches the wire.
One deliberate act of amnesia happens here. The scanner installs a NoneTokenStore, so
it forgets every address-validation token servers have ever issued it. That guarantees each scan is
a true cold start: without it, noq would cache the NEW_TOKEN a server issues after its first Retry,
present it on the next scan, and the server would politely skip Retry, making Retry detection report
a different answer on every second run. A scanner that remembers is a scanner that measures its own
memory.
Phase 1 · First flight, and the server says "prove your address"
Initial[0]: CRYPTO(ClientHello) · version 0x00000001 · PADDING to 1200 BRetry: token (integrity-tagged) · new SCIDInitial[0]: CRYPTO(ClientHello) + token
The first packet is an Initial: a long-header packet carrying the ClientHello inside a CRYPTO frame,
padded to at least 1200 bytes because QUIC refuses to let tiny packets trigger big replies. In this
capture the server did not answer with a handshake. It answered with a Retry
(retry_packet_received: true): a stateless challenge that forces the client to prove it
owns its source address before the server commits any memory to it. The scanner echoed the token and
was admitted. It records this as security_features.retry_packets_enabled, a defense most
scanners never notice because most scanners never complete a real handshake.
Two more findings land in this phase. The server honors version negotiation
(version_negotiation_received: true): poked with a reserved version number, it correctly
lists what it speaks, QUIC v1 (RFC 9000) and QUIC v2 (RFC 9369), with GREASE versions sprinkled in
(grease_versions_seen: true) to keep middleboxes from ossifying the list. And the
amplification math is measured, not assumed: the anti-amplification limit caps a server at 3×
the bytes a client has sent until the address is validated. This server's measured factor before
validation: 0.09×. It could legally send 33 times more than it does.
Phase 2 · A handshake from after the quantum threat
ClientHello: key_share X25519MLKEM768 (1216 B) · ALPN h3 · ECHServerHello · EncryptedExtensions · Certificate · FinishedFinished · handshake complete at t = 30 ms
The ClientHello's key share is the tell. X25519MLKEM768 is a hybrid: a 1184-byte
ML-KEM-768 encapsulation key (FIPS 203, the standardized descendant of Kyber) concatenated with
a 32-byte X25519 point, 1216 bytes where classical TLS sends 32. Breaking the session requires
breaking both: the lattice problem and the curve. The scanner records the negotiated group
(pqc_key_exchange: "X25519MLKEM768", pqc_hybrid_offered: true), and this
single boolean later decides whether the top grade is even reachable.
Around it, the rest of the hello is read with the same literalism: ALPN offered and negotiated
(h3), Encrypted Client Hello support detected
(ech_supported: true, the server publishes a real ECH config, so the hostname itself can
travel encrypted), GREASE extensions present, cipher settled as
TLS_AES_256_GCM_SHA384, and the signature algorithm list even shows
ML-DSA-44: a post-quantum signature scheme advertised in a live TLS stack. The
negotiated cipher comes from a small accessor added to the vendored noq
(negotiated_cipher_suite on its rustls HandshakeData): real value or an
explicit unknown, never a guess.
The clock reads 30 ms for the complete handshake, and that number is honest by
construction: an earlier version of this metric accidentally timed DNS resolution and a per-scan
certificate-store load along with the handshake. The store now loads once behind a
OnceLock and the timer starts at connect(), so 30 ms is the protocol, not
the scanner's own overhead.
Phase 3 · The server's confession, in eleven integers
Inside the encrypted handshake, the server hands over its transport parameters: a self-description
of every limit it intends to enforce. The scanner does not infer these from behavior. It reads the
peer's advertisement raw, through a peer_transport_params() accessor maintained in the
vendored noq fork, and republishes them verbatim:
These integers are also a fingerprint. The scanner identifies server implementations in three tiers:
the Server: header when present (95% confidence), known platform domains (85%), and,
when both fail, the characteristic shape of exactly these transport parameters (70%). This capture
needed only tier one: Server: pqcrypta resolved to
"PQCrypta Proxy (noq)" at 0.95 confidence, with characteristics including
"Post-Quantum ready" and "Full multipath QUIC". Sites that hide their server header still get named,
just less confidently, by their integers.
Phase 4 · HTTP/3 clears its throat
SETTINGS: EXTENDED_CONNECT · H3_DATAGRAM · WEBTRANSPORTPRIORITY_UPDATE (RFC 9218): u=3, non-incrementalaccepted · response carries Priority: u=3
With 1-RTT keys installed, both sides open control streams and exchange SETTINGS. This server
enables the modern set: Extended CONNECT (the door WebTransport walks through),
H3_DATAGRAM, and WebTransport itself. Its
max_field_section_size is 4,611,686,018,427,387,903, which is 262−1,
the largest value a QUIC varint can carry. The server is not expecting a 4-exabyte header block; it
is declining to impose a limit, in the most maximal way the encoding allows.
For RFC 9218 Extensible Priorities, the scanner refuses to settle for reading a
header. It sends a real PRIORITY_UPDATE frame (a capability added to the vendored
h3 crate) and watches whether the server accepts it. Here:
priority_update_accepted: true, with the response advertising Priority: u=3.
Passive observation says a feature is configured. An active frame says it works. The capture also
shows 103 Early Hints arriving before the main response
(early_hints_103: true), preload hints sent while the origin is still thinking.
Phase 5 · The request, and the header that decides careers
HEADERS: GET / · :authority pqcrypta.comHEADERS: 200 · alt-svc: h3=":443"; ma=86400, h3=":4434" · 6 ms to first byte
The actual HTTP part is almost anticlimactic: GET /, a 200, first byte in
6 milliseconds, and a server-timing header confessing the proxy spent
4.43 ms of that. But one response header carries more grading weight than any other:
Alt-Svc. It is how browsers discover HTTP/3 exists. A server can run a flawless
QUIC stack on UDP 443, and if this one header is missing, every browser on Earth will quietly use
HTTP/2 forever.
The scanner treats that failure mode as its own grade. Sites whose QUIC handshake succeeds but whose Alt-Svc is absent are graded C: misconfigured, arguably the saddest grade on the scale, a working engine nobody is told about. Historical scan data shows it is not rare. This capture's Alt-Svc advertises HTTP/3 on 443 with a 24-hour lifetime, plus an alternate endpoint on 4434, so the C trapdoor stays shut.
Phase 6 · Four interrogations on one connection
A grade earned by reading headers would be an opinion. The scanner's grades are alibis: each claimed capability is exercised for real. Four probes run against this connection; the entire scan, all of them included, takes 2,128 ms of wall time.
01 WebTransport
A genuine session is established through Extended CONNECT (here on port 443, path /;
4433 and custom ports are also tried). Measured, not asserted: session open in
37 ms, datagrams supported at up to 1,413 bytes, 1,000 incoming
streams, both stream directions, a 16 MiB flow-control window.
02 MASQUE CONNECT-UDP
RFC 9298 proxying is verified by actually relaying UDP over the HTTP/3 connection: the probe
sends a real DNS query through the relay to 127.0.0.53:53 and waits for the answer.
This capture: session accepted, status 200, round trip 5 ms. The server is a
working UDP proxy, proven by a resolved query.
03 Multipath
The scanner calls connection.open_path() and opens a second, data-carrying
network path (draft-ietf-quic-multipath-21). Path 1 validated with its own congestion window
(12,000 B) and MTU (1,200 B), independent of the primary path's
14,400 B and 1,452 B. That story has its own whitepaper.
04 0-RTT
The one probe where the best answer is a refusal. 0-RTT resumes sessions with zero round
trips but makes the first flight replayable by anyone who recorded it. This server declines
early data (supports_0rtt: false), and the grading system rewards exactly that.
Probe 03 as the wire sees it: the numbers on each path are this capture's real per-path PathStats, and their divergence is the proof of independent congestion control.
The connection's own vitals are logged alongside: primary-path congestion window 14,400 bytes, MTU discovered to 1,452, GSO offload confirmed on the socket, ACK-frequency extension negotiated, zero packets lost across all 42.
Phase 6B · What the server saw
Multipath happens inside the transport, which means that unless someone asks, it happens silently.
pqcrypta-proxy asks. It subscribes to noq's path_events() broadcast stream and writes
every path establishment, abandonment, and discard to its logs with that path's own statistics
attached, at a cost of nothing when no extra paths are opened:
// pqcrypta-proxy/src/quic_listener.rs
let mut events = conn.path_events();
while let Some(ev) = events.next().await {
if let Ok(quinn::PathEvent::Established { id, .. }) = ev {
let (rtt, cwnd, mtu) = conn.path_stats(id) /* per-path, not global */ ...;
info!("Multipath: path {} ESTABLISHED for {} (rtt={}ms cwnd={}B mtu={}B)", ...);
}
}
Here is what that produces in the live journal. This is phase 6's probe, seen from the other side of the wire:
On the day this page was written, the US East node logged 26 second paths established, arriving roughly every ten to fifteen minutes. Every single one traces back to the scanner's own scheduled scans returning over the node's public egress address (which is why the RTT rounds to zero: the observer lives on the host it is observing). Zero paths were abandoned and zero discarded, because scans end with CONNECTION_CLOSE, which tears down every path with the connection rather than exercising PATH_ABANDON.
Read plainly: no mainstream browser negotiates draft multipath yet, so the multipath internet currently consists of implementations testing each other. That is exactly what the early life of a protocol extension looks like, and it is why the logging exists: when an unfamiliar peer opens a second path for the first time, it will be a log line, not a mystery.
Phase 7 · Papers, please
The certificate chain presented during the handshake gets its own dissection:
CN=pqcrypta.com, issued by Let's Encrypt (YE1 intermediate), ECDSA P-384 signed with
SHA-384, a four-certificate chain up to ISRG Root X1, two embedded Certificate Transparency SCTs,
48 days until expiry at capture time. OCSP stapling: absent, and recorded as absent, because the
scanner reports what is on the wire, not what would be flattering.
The classifier behind this panel goes further than classical PKI. It explicitly recognizes
post-quantum signed certificates (ML-DSA, SLH-DSA, FN-DSA) and composite
classical+PQC certificates such as mldsa65_rsa3072, because PQ Crypta operates hosts
that actually serve them: pqc.pqcrypta.com terminates TLS with an ML-DSA-87 certificate
chain today. When the WebPKI's post-quantum migration arrives, this panel will not need updating to
notice.
Phase 8 · The wire closes. The verdict unseals.
CONNECTION_CLOSE (0x00): no error · keys discardedEverything measured now flows through a decision tree that is deliberately unsentimental. There are five grades, and the top one is unreachable without post-quantum cryptography:
The requirement doing the real work is the last one. Since July 2026 the A++ tier requires a post-quantum hybrid key exchange, which means the scanner's top grade cannot be earned by any deployment that a future quantum adversary could retroactively decrypt. Grades are stored with the scan; the global grade distribution on the analyzer page is the accumulated verdict history of every domain anyone has ever tested.
Phase 9 · The connection is dead; the data begins its second life
The full scan JSON lands in PostgreSQL (http3_scan_history, with per-grade totals in
http3_scan_totals), joining the dataset behind the analyzer's global statistics. Then
the recommendation engine reads it: a scikit-learn RandomForest of 100 trees
performing multi-label classification over nine features extracted from each scan (grade, protocol
support flags, 0-RTT, WebTransport, Alt-Svc presence), with a GradientBoostingRegressor predicting
each recommendation's effectiveness. Only predictions above 30% confidence surface, ranked by
confidence times effectiveness, with a rule-based fallback if the ML path ever fails.
Even this A++ capture got homework. The top recommendation flagged that HTTP/1.1 is still enabled on the origin: a legacy attack surface (request smuggling, slowloris) that coexists with the quantum-resistant front door. A perfect grade and an open side entrance are not contradictory, which is precisely why the recommendations exist independently of the grade.
Field notes: nine lessons so you do not repeat ours
Everything above works today, but not everything worked the first time. These are the concrete problems we hit building the scanner and the server, and what fixed each one. If you are implementing QUIC tooling or multipath, this list is the part of the paper to steal.
- Force cold starts or Retry detection lies. QUIC servers issue NEW_TOKEN after a Retry;
a client that caches it skips Retry on the next connection. Our Retry detector flip-flopped
true/false on alternating scans until the scanner installed a
NoneTokenStore. A measurement client must forget on purpose. - Both ends must track the same draft revision. During rollout, our older narrow-probe scanner advertised multipath with a pre-standard frame codepoint; the new server's draft-conformant frames corrupted the old client's HTTP/3 stream. The only casualty of deploying multipath was our previous multipath implementation. Draft extensions interoperate by revision, not by name.
- Fold the Path ID into the AEAD nonce. Every path numbers packets from zero, so packet 42
exists on every path. If packet protection ignores the path, two different packets encrypt under the
same nonce and AEAD security fails completely, not gracefully. Our rustls fork implements the
draft's nonce calculation as
encrypt_in_place_for_path(path_id, packet_number, ...). - Multipath deletes the idea of "the" remote address. Any API shaped like
connection.remote_address()breaks when a connection has several. Porting our WebTransport layer needed exactly four code fixes, and this was three of them:remote_address()andrtt()now take aPathId. - Alias the fork, do not rewrite the consumers. One manifest line,
quinn = { package = "noq" }, let two large codebases compile against a different QUIC engine nearly unchanged. Reserve rewriting for real API divergences; there were only a handful. - Time the protocol, not your own overhead. Our handshake metric once silently included
DNS resolution and a per-scan root-store load. The fix: load the certificate store once behind a
OnceLockand start the timer atconnect(). If a number feeds a grade, audit what the timer actually wraps. - Subscribe to path events or multipath is invisible. The transport manages paths
automatically, which means nothing surfaces unless you ask. The server's
path_events()subscription (phase 6B) is a few dozen lines and is the only reason we can say, with logs, who opens paths against us. - Report "indeterminate" instead of guessing. This capture's congestion-controller field
reads
indeterminate: no response body received to measure congestion response, and the second path's RTT reads 0 because observer and target share a host. Both are true statements about measurement conditions. A scanner that fills awkward gaps with plausible values is a fiction generator with a JSON API. - Keep draft features out of the grade. The multipath probe is reported but deliberately not folded into A++ criteria while the draft is unratified. Grades should only reward behavior a site operator can adopt from a standard, not from a moving target.
The evidence manifest: every check, its reason, its use
This is the scanner's complete coverage, taken from the fields it actually returns. Each group states why it is collected and what the result is used for.
Protocol support why: is HTTP/3 real here, and can browsers find it? · feeds the grade directly
http3_supportedbrowsers can reach HTTP/3 (Alt-Svc present + QUIC works)http3_server_capableQUIC answers even if browsers are never told (the grade-C trap)http2_supported/http1_supportedfallback surface, including legacy-risk flagsprotocol_negotiated/browser_protocolwhat actually got used vs what a browser would usealt_svcthe discovery header itself, with endpoints and lifetimestatus_code/response_time_msthe request worked; total scan wall time
QUIC version behavior why: correctness and ossification resistance · feeds diagnostics + fingerprinting
current_version/supported_versionsQUIC v1 (RFC 9000) and v2 (RFC 9369) supportversion_negotiation_receivedthe server correctly rejects unknown versions instead of hanginggrease_versions_seenanti-ossification GREASE in the version listretry_packet_receivedaddress validation before server state commitment
Transport parameters (peer-advertised, read raw) why: the server's own declared limits · feeds capacity analysis + tier-3 fingerprinting
initial_max_data+ per-stream / per-direction flow-control windows: throughput ceilingsinitial_max_streams_bidi/uniconcurrency ceilings for HTTP/3 and WebTransportmax_idle_timeout/max_udp_payload_sizeconnection lifetime and datagram sizingmax_datagram_frame_sizeunreliable datagram support and its capactive_connection_id_limitheadroom for migration and multipathdisable_active_migrationwhether phones may roam networks mid-connectiongrease_quic_bitRFC 9287 fixed-bit greasingpreferred_address/stateless_reset_token/ack_delayparams: failover, clean reset, ACK timing contract
TLS 1.3 and post-quantum why: is the handshake quantum-resistant and private? · gates the A++ tier
pqc_key_exchange/pqc_hybrid_offeredhybrid PQ key exchange (e.g. X25519MLKEM768); required for A++key_share_group(s)/signature_algorithmsnegotiated and offered crypto, including PQ signatures like ML-DSAech_supportedEncrypted Client Hello: can the hostname itself travel encryptedalpn_protocols/supported_versions/ cipher suite: negotiation factsearly_data_accepted/max_early_data_size0-RTT posture (replay risk)grease_extensions_used/grease_key_shares_usedossification hygieneserver_name_indication/certificate_compressionhello completeness
HTTP/3 SETTINGS and headers why: which application-layer capabilities are switched on · feeds feature detection + recommendations
enable_connect_protocolExtended CONNECT, the prerequisite for WebTransportenable_webtransport/h3_datagramWebTransport and datagram flowqpack_*header-compression configuration (table capacity, blocked streams)max_field_section_sizeheader-block limit policypriority_*+priority_update_acceptedRFC 9218: verified by sending a real PRIORITY_UPDATE frame, not by reading a headerearly_hints_103preload hints before the main responseserver_timing/accept_ch/nel/report_toobservability and client-hint posture
Connection metrics (measured, not quoted) why: real performance of this path · feeds performance panel + honesty checks
handshake_time_msprotocol-only handshake duration (timer starts at connect)time_to_first_byte_ms/rtt_estimate/min/maxlatency truthcongestion_window/bytes_in_flight/congestion_controllertransport state; reported indeterminate when unmeasurablepackets_sent/received/lost/loss_rate_percentpath quality of the scan itselfpath_mtu_discovery/current_mtu/max_packet_sizedatagram sizing discovered livegso_supported/ack_frequency*/pto_countsocket offload and ACK economics
Active capability probes why: claims are only counted when exercised · feeds grade (WebTransport) + capability panels
webtransport_capabilitiesreal session opened: latency, datagram size, stream limits, flow-control window, uni/bidiconnect_udpMASQUE RFC 9298: a genuine UDP relay round trip (DNS query through the proxy), with timingmultipath_probea second data-carrying path opened viaopen_path(), with per-path RTT, cwnd, MTU (informational while draft)supports_0rttearly-data acceptance test; refusal is rewarded by the grade
Security posture why: DDoS and spoofing resilience of the QUIC deployment · feeds security panel + recommendations
retry_packets_enabled/retry_token_receivedaddress validation in actionaddress_validation_enabled/token_validation_enabledthe full validation pipelineanti_amplification_limit/amplification_factorthe 3x rule, and the measured reality against itstateless_reset_supportedclean teardown across server restartsrate_limiting_detectedvisible throttling behavior
Certificate chain why: trust, expiry risk, and quantum readiness of the PKI · feeds certificate panel + expiry warnings
subject/issuer/root_ca/chain_lengththe full trust pathvalid_from/until/days_until_expiryoperational expiry riskkey_type/key_size/signature_algorithmincluding PQC-signed (ML-DSA, SLH-DSA, FN-DSA) and composite classical+PQC classificationocsp_stapling/ocsp_must_staple/scts_present+ count: revocation and transparency posturedelegated_credentials/is_valid/validation_errorsedge cases stated plainly
Identification and verdict why: who runs this stack, and what should they do next · feeds fingerprint card, grade, ML recommendations
server_fingerprintimplementation named via three tiers (Server header 95%, known domains 85%, transport-parameter shape 70%) with confidence and characteristicsgradethe five-tier verdict; A++ unreachable without post-quantum key exchangerecommendationsRandomForest multi-label predictions over nine scan features, effectiveness-ranked, rule-based fallback- persistence: every scan saved to PostgreSQL, powering the global adoption statistics on the analyzer page
Provenance
Every figure above comes from one scan of pqcrypta.com executed at
2026-07-26T13:22:12Z through the same public API the analyzer page calls
(POST https://api.pqcrypta.com/http3-scanner/scan), cross-checked against the scanner
source (http3_scanner.rs), the proxy source (pqcrypta-proxy), and the
vendored noq / h3 / wtransport forks. Run your own capture at
the HTTP/3 Analyzer; the numbers will be yours, but the anatomy will be
the same.
References
- RFC 9000 · QUIC: A UDP-Based Multiplexed and Secure Transport
- RFC 9114 · HTTP/3
- RFC 9369 · QUIC Version 2
- RFC 9218 · Extensible Prioritization Scheme for HTTP
- RFC 9298 · Proxying UDP in HTTP (MASQUE CONNECT-UDP)
- RFC 9287 · Greasing the QUIC Bit
- RFC 8446 · TLS 1.3 · RFC 7838 · HTTP Alternative Services
- draft-ietf-quic-multipath-21 · Multipath Extension for QUIC
- FIPS 203 · Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM)
- draft-ietf-tls-esni · TLS Encrypted Client Hello
