From 3af3e356d6fdf43e6772dc2e91b322f8e8148f62 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Sun, 16 Nov 2025 22:17:00 +0200 Subject: 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. --- tests/test_dissector.cpp | 74 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_dissector.cpp (limited to 'tests/test_dissector.cpp') diff --git a/tests/test_dissector.cpp b/tests/test_dissector.cpp new file mode 100644 index 0000000..9dd4135 --- /dev/null +++ b/tests/test_dissector.cpp @@ -0,0 +1,74 @@ +#include + +#include "packeteer/l7/dissector.hpp" + +using namespace packeteer::net; + +namespace { + +// A dissector that claims a port but never actually matches any +// payload - stands in for e.g. TlsSniDissector receiving QUIC bytes +// on port 443: same port, wrong protocol, never succeeds. +class NeverMatchesDissector : public L7Dissector { +public: + explicit NeverMatchesDissector(std::uint16_t port) : port_(port) {} + std::uint16_t port() const override { return port_; } + std::optional summarize(std::span) const override { + return std::nullopt; + } + +private: + std::uint16_t port_; +}; + +class AlwaysMatchesDissector : public L7Dissector { +public: + AlwaysMatchesDissector(std::uint16_t port, std::string label) + : port_(port), label_(std::move(label)) {} + std::uint16_t port() const override { return port_; } + std::optional summarize(std::span) const override { + return label_; + } + +private: + std::uint16_t port_; + std::string label_; +}; + +} // namespace + +TEST_CASE("L7Registry falls through to a later dissector on the same port " + "when an earlier one fails to match") { + NeverMatchesDissector tls_like(443); + AlwaysMatchesDissector quic_like(443, "QUIC something"); + + L7Registry registry; + registry.add(&tls_like); + registry.add(&quic_like); + + auto result = registry.dissect(443, {}); + REQUIRE(result.has_value()); + CHECK(*result == "QUIC something"); +} + +TEST_CASE("L7Registry still returns the first dissector to succeed, not the last") { + AlwaysMatchesDissector first(80, "first"); + AlwaysMatchesDissector second(80, "second"); + + L7Registry registry; + registry.add(&first); + registry.add(&second); + + auto result = registry.dissect(80, {}); + REQUIRE(result.has_value()); + CHECK(*result == "first"); +} + +TEST_CASE("L7Registry returns nullopt when no dissector on the port matches") { + NeverMatchesDissector only(443); + + L7Registry registry; + registry.add(&only); + + CHECK_FALSE(registry.dissect(443, {}).has_value()); +} -- cgit v1.2.3