srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_dissector.cpp
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-05-26 09:16:00 +0200
committersrdusr <[email protected]>2026-05-26 09:16:00 +0200
commitc098f1742bb04fbe41fc6cf492cd334efef734eb (patch)
tree8af2c79ec321357d2766fdab03d6a870ff7ceb6c /tests/test_dissector.cpp
parent2d010c9f851ea4eb851179db012c8977ce6e4bd5 (diff)
downloadpacketeer-c098f1742bb04fbe41fc6cf492cd334efef734eb.tar.gz
packeteer-c098f1742bb04fbe41fc6cf492cd334efef734eb.zip
Add deeper TLS (ServerHello, ALPN); fix a QUIC/TCP false-positive bug
TLS: ServerHello now reports the negotiated version and cipher suite alongside the existing ClientHello SNI support, plus ClientHello's ALPN extension. ServerHello's version prefers the supported_versions extension over legacy_version when present - TLS 1.3 always sets legacy_version to 0x0303 for middlebox compatibility, so reading only that field would misreport every real TLS 1.3 connection as 1.2. Cipher suite names are hardcoded only for TLS 1.3's five suites (a small closed set); everything else reports as raw hex rather than a guessed name from a "common suites" list. Live-verifying that against a real Cloudflare TLS 1.3 handshake surfaced a real, unrelated bug in the QUIC dissector added earlier: it was also being tried against TCP port-443 payloads (a side effect of the earlier L7Registry port-sharing fix), and produced false "QUIC" labels on TLS ciphertext continuation fragments - large encrypted records split across multiple TCP segments, each fed to the parser independently since this project doesn't reassemble by default, so a later fragment's effectively random bytes occasionally passed as a plausible QUIC header. Fixed in two layers: parse_quic() now enforces RFC 9000's real 20-byte cap on connection ID lengths, closing most of the long-header false- positive surface; and L7Dissector gained a transport() method (defaulting to kAny, so every other dissector's behavior is unchanged) so QuicDissector can declare itself UDP-only - necessary because the length cap alone can't touch QUIC's short-header form, which by design has no structural signal beyond one bit once header protection can't be removed without connection state. Re-verified against the identical live scenario afterward: zero false QUIC labels on the same Cloudflare TCP handshake, and a repeat of the earlier real HTTP/3 capture confirmed genuine QUIC still decodes correctly on UDP.
Diffstat (limited to 'tests/test_dissector.cpp')
-rw-r--r--tests/test_dissector.cpp37
1 files changed, 32 insertions, 5 deletions
diff --git a/tests/test_dissector.cpp b/tests/test_dissector.cpp
index 9dd4135..4517ae7 100644
--- a/tests/test_dissector.cpp
+++ b/tests/test_dissector.cpp
@@ -23,9 +23,10 @@ private:
class AlwaysMatchesDissector : public L7Dissector {
public:
- AlwaysMatchesDissector(std::uint16_t port, std::string label)
- : port_(port), label_(std::move(label)) {}
+ AlwaysMatchesDissector(std::uint16_t port, std::string label, Transport transport = Transport::kAny)
+ : port_(port), label_(std::move(label)), transport_(transport) {}
std::uint16_t port() const override { return port_; }
+ Transport transport() const override { return transport_; }
std::optional<std::string> summarize(std::span<const unsigned char>) const override {
return label_;
}
@@ -33,6 +34,7 @@ public:
private:
std::uint16_t port_;
std::string label_;
+ Transport transport_;
};
} // namespace
@@ -46,7 +48,7 @@ TEST_CASE("L7Registry falls through to a later dissector on the same port "
registry.add(&tls_like);
registry.add(&quic_like);
- auto result = registry.dissect(443, {});
+ auto result = registry.dissect(443, Transport::kTcp, {});
REQUIRE(result.has_value());
CHECK(*result == "QUIC something");
}
@@ -59,7 +61,7 @@ TEST_CASE("L7Registry still returns the first dissector to succeed, not the last
registry.add(&first);
registry.add(&second);
- auto result = registry.dissect(80, {});
+ auto result = registry.dissect(80, Transport::kTcp, {});
REQUIRE(result.has_value());
CHECK(*result == "first");
}
@@ -70,5 +72,30 @@ TEST_CASE("L7Registry returns nullopt when no dissector on the port matches") {
L7Registry registry;
registry.add(&only);
- CHECK_FALSE(registry.dissect(443, {}).has_value());
+ CHECK_FALSE(registry.dissect(443, Transport::kTcp, {}).has_value());
+}
+
+TEST_CASE("L7Registry skips a dissector whose declared transport doesn't match, even on the "
+ "right port") {
+ // The actual fix for QUIC's false positives on TCP:443: a
+ // dissector that only claims UDP must never be tried against a
+ // TCP payload on the same port, regardless of what its own
+ // summarize() would have returned.
+ AlwaysMatchesDissector udp_only(443, "UDP thing", Transport::kUdp);
+
+ L7Registry registry;
+ registry.add(&udp_only);
+
+ CHECK_FALSE(registry.dissect(443, Transport::kTcp, {}).has_value());
+ CHECK(registry.dissect(443, Transport::kUdp, {}).has_value());
+}
+
+TEST_CASE("L7Registry's default kAny transport matches either TCP or UDP") {
+ AlwaysMatchesDissector any(53, "DNS-like"); // default transport: kAny
+
+ L7Registry registry;
+ registry.add(&any);
+
+ CHECK(registry.dissect(53, Transport::kTcp, {}).has_value());
+ CHECK(registry.dissect(53, Transport::kUdp, {}).has_value());
}