srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/PLAN.md
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-11-16 22:17:00 +0200
committersrdusr <[email protected]>2025-11-16 22:17:00 +0200
commit3af3e356d6fdf43e6772dc2e91b322f8e8148f62 (patch)
tree3bc5697412a50818ef633caee151a01927b96053 /PLAN.md
parent407249eb5d654b5a951c43bc1722fd397d5d922c (diff)
downloadpacketeer-3af3e356d6fdf43e6772dc2e91b322f8e8148f62.tar.gz
packeteer-3af3e356d6fdf43e6772dc2e91b322f8e8148f62.zip
Add a cleartext-only QUIC dissector; fix a port-collision bug in L7Registry
QUIC decodes only what RFC 9000 sends in cleartext at the framing level: long/short header form, version, long-packet type, and both connection IDs. Everything past that is encrypted from the first protected byte onward, even for Initial packets - decrypting that is real crypto machinery this project deliberately doesn't take on, the same call already made for TLS's SNI-only extraction. Caught a real, previously-latent bug while wiring this in, by design review rather than live-traffic debugging: L7Registry::dissect() returned on the first dissector whose port() matched, even if that dissector's summarize() then failed. Harmless while every registered port was unique, but QUIC is the first protocol here to genuinely share a well-known port with something already registered (443: TLS over TCP, QUIC over UDP - the registry has no transport dimension). Without the fix, tls_dissector would silently claim every port-443 lookup and QUIC would never be reachable. Fixed to try each same-port dissector until one actually succeeds, locked in with stub-dissector tests independent of any real protocol's parsing. Live-verified thoroughly: real HTTP/3 traffic to google.com via `curl --http3-only`, captured on wlp1s0, correctly decoding the full connection lifecycle against Google's actual production QUIC implementation - Initial packets (including a genuine connection ID migration mid-handshake), Handshake packets, and 1-RTT short-header packets. This also confirms the L7Registry fix live: without it none of this would have decoded at all.
Diffstat (limited to 'PLAN.md')
-rw-r--r--PLAN.md44
1 files changed, 42 insertions, 2 deletions
diff --git a/PLAN.md b/PLAN.md
index 3fa8c1d..392e02e 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -30,8 +30,10 @@ unowned buffers) via a real-world capture pipeline.
3. [done] pcapng read/write
4. [done] Bounded channel + drop-on-backpressure between capture and render
5. [in progress] L7 dissector interface, add protocols incrementally --
- interface + DNS + HTTP + TLS SNI + mDNS + SSH banner done
- (packeteer/l7/); more protocols can still be added incrementally,
+ interface + DNS + HTTP + TLS SNI + mDNS + SSH banner + NTP + DHCP +
+ FTP + SMTP + TFTP + QUIC done (packeteer/l7/); ARP/VLAN/IGMP done
+ at the L2/L3 level too (packeteer/net/); more protocols can still
+ be added incrementally,
by design
6. [done] Filtering (-f <expr>, libpcap's own BPF compiler - see Decisions)
7. [done] Drop privileges after opening the capture handle (see Decisions)
@@ -500,3 +502,41 @@ None currently open.
re-verified against the same live traffic afterward, confirmed
clean, and a regression test locks in the exact byte pattern that
triggered it.
+- QUIC (l7/quic.hpp), decoding only what RFC 9000 actually sends in
+ cleartext at the framing level: long vs. short header form, version,
+ long-packet type (Initial/0-RTT/Handshake/Retry/Version
+ Negotiation), and both connection IDs. Everything past that --
+ packet numbers, frames, the payload - is encrypted from the first
+ protected byte onward, even for Initial packets (whose keys derive
+ via HKDF from a public per-version salt, then AES-GCM); actually
+ decrypting that is real crypto machinery this project deliberately
+ doesn't take on, the same call already made for TLS's SNI-only
+ extraction. A short-header packet's destination connection ID has no
+ length field in the packet itself - the receiver already knows it
+ from earlier connection state a passive observer doesn't have - so
+ short-header packets are reported by form alone.
+ Discovered and fixed a real, previously-latent bug while wiring this
+ in, caught by design review before it ever touched live traffic:
+ L7Registry::dissect() returned on the *first* dissector whose
+ port() matched, even if that dissector's summarize() then failed --
+ harmless while every registered port was unique, but QUIC is the
+ first protocol here to genuinely share a well-known port with
+ something else already registered (443: TLS over TCP, QUIC over
+ UDP; the registry has no transport dimension, only a port number).
+ Without the fix, tls_dissector (registered first) would silently
+ claim every port-443 lookup and return nullopt for all QUIC traffic,
+ forever, regardless of registration order past it. Fixed to try each
+ same-port dissector until one actually succeeds; locked in with
+ tests/test_dissector.cpp using two stub dissectors on a shared port,
+ independent of any real protocol's parsing logic.
+ Live-verified about as thoroughly as anything in this project: real
+ HTTP/3 traffic to google.com via `curl --http3-only` (curl here
+ links ngtcp2/nghttp3), captured on wlp1s0, correctly decoding the
+ full connection lifecycle - Initial packets (including a genuine
+ connection ID migration mid-handshake, dcid changing from
+ a2d81abd... to e2d81abd..., real QUIC behavior, not a parsing
+ artifact), Handshake packets, and finally 1-RTT short-header
+ packets - against Google's actual production QUIC implementation.
+ This also serves as a real-traffic confirmation that the
+ L7Registry fix works: without it, none of this would have decoded
+ at all, since tls_dissector claims port 443 first.