From 08332a4195956611db80a2cfe3710d760cbd6acf Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Tue, 14 May 2024 01:42:00 +0200 Subject: Initial commit: wireframe packet capture/analysis tool Terminal packet capture and analysis tool built to learn the C++ memory model (byte layout, alignment, endianness, std::span over unowned buffers) via a real capture pipeline. - Hand-rolled L2-L4 decoders (Ethernet, IPv4, IPv6 with extension header walking, TCP, UDP) over std::span, no struct-casting - L7 dissector interface with DNS, HTTP, and TLS SNI implementations - pcapng read/write for Wireshark-compatible capture files - Bounded capture queue: drop-on-backpressure for live capture, blocking push for faithful file replay - Kernel-level BPF filtering (-f) and a separate display-only search (-g / interactive) that doesn't touch what's captured - Replay mode (-r) reads a saved pcapng file back through the same pipeline as live capture, no root or live device needed - pcap_stats() surfaces kernel/interface drops invisible to the capture queue's own counter - Three frontends sharing one CaptureSession setup path: CLI, TUI (FTXUI, primary), GUI (Dear ImGui + SDL3, secondary) - 89 unit tests (doctest) plus 9 libFuzzer harnesses covering every hand-rolled parser; fuzzing found and fixed a real OOM in the pcapng reader (unbounded allocation from an untrusted length field) --- tests/test_tls.cpp | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tests/test_tls.cpp (limited to 'tests/test_tls.cpp') diff --git a/tests/test_tls.cpp b/tests/test_tls.cpp new file mode 100644 index 0000000..65784a9 --- /dev/null +++ b/tests/test_tls.cpp @@ -0,0 +1,132 @@ +#include + +#include + +#include "wireframe/l7/tls.hpp" + +using namespace wireframe::net; + +namespace { + +void append_be16(std::vector& out, std::uint16_t v) { + out.push_back(static_cast(v >> 8)); + out.push_back(static_cast(v & 0xFF)); +} + +// Builds a real, well-formed TLS record containing a ClientHello with +// (optionally) a single SNI host_name extension. Every length field is +// computed from the actual bytes assembled, not hand-counted - the +// same lesson from this session's earlier UDP-length test typo. +// +// `corrupt_sni_ext_len`, when set, writes an oversized SNI extension +// length instead of the real one (computed here, not via post-hoc +// offset math into the finished buffer - equally fragile). +std::vector build_client_hello(const std::string& sni, + bool corrupt_sni_ext_len = false) { + std::vector body; + body.push_back(0x03); + body.push_back(0x03); // client_version: TLS 1.2 + body.insert(body.end(), 32, 0x00); // random + body.push_back(0x00); // session_id length: 0 + append_be16(body, 2); // cipher_suites length + body.push_back(0x00); + body.push_back(0x2F); // one arbitrary cipher suite + body.push_back(0x01); // compression_methods length: 1 + body.push_back(0x00); // null compression + + std::vector extensions; + if (!sni.empty()) { + std::vector server_name_list; + server_name_list.push_back(0x00); // name_type: host_name + append_be16(server_name_list, static_cast(sni.size())); + server_name_list.insert(server_name_list.end(), sni.begin(), sni.end()); + + std::vector sni_ext_data; + append_be16(sni_ext_data, static_cast(server_name_list.size())); + sni_ext_data.insert(sni_ext_data.end(), server_name_list.begin(), server_name_list.end()); + + append_be16(extensions, kTlsExtensionServerName); + std::uint16_t ext_len = corrupt_sni_ext_len + ? static_cast(0xFFFF) + : static_cast(sni_ext_data.size()); + append_be16(extensions, ext_len); + extensions.insert(extensions.end(), sni_ext_data.begin(), sni_ext_data.end()); + } + append_be16(body, static_cast(extensions.size())); + body.insert(body.end(), extensions.begin(), extensions.end()); + + std::vector handshake; + handshake.push_back(kTlsHandshakeTypeClientHello); + std::uint32_t hs_len = static_cast(body.size()); + handshake.push_back(static_cast((hs_len >> 16) & 0xFF)); + handshake.push_back(static_cast((hs_len >> 8) & 0xFF)); + handshake.push_back(static_cast(hs_len & 0xFF)); + handshake.insert(handshake.end(), body.begin(), body.end()); + + std::vector record; + record.push_back(kTlsContentTypeHandshake); + record.push_back(0x03); + record.push_back(0x01); // record-layer version (legacy compat value) + append_be16(record, static_cast(handshake.size())); + record.insert(record.end(), handshake.begin(), handshake.end()); + + return record; +} + +} // namespace + +TEST_CASE("parse_tls_client_hello extracts a real SNI extension") { + auto record = build_client_hello("example.com"); + auto hello = parse_tls_client_hello(record); + REQUIRE(hello.has_value()); + REQUIRE(hello->server_name.has_value()); + CHECK(*hello->server_name == "example.com"); +} + +TEST_CASE("parse_tls_client_hello succeeds with no SNI when there's no extensions block") { + auto record = build_client_hello(""); + auto hello = parse_tls_client_hello(record); + REQUIRE(hello.has_value()); + CHECK_FALSE(hello->server_name.has_value()); +} + +TEST_CASE("parse_tls_client_hello rejects a non-Handshake record") { + auto record = build_client_hello("example.com"); + record[0] = 0x17; // application_data, not handshake + CHECK_FALSE(parse_tls_client_hello(record).has_value()); +} + +TEST_CASE("parse_tls_client_hello rejects a non-ClientHello handshake type") { + auto record = build_client_hello("example.com"); + record[5] = 0x02; // ServerHello, not ClientHello + CHECK_FALSE(parse_tls_client_hello(record).has_value()); +} + +TEST_CASE("parse_tls_client_hello rejects a truncated record") { + auto record = build_client_hello("example.com"); + record.resize(record.size() - 5); // claims more than it has + CHECK_FALSE(parse_tls_client_hello(record).has_value()); +} + +TEST_CASE("parse_tls_client_hello rejects a buffer shorter than the record header") { + std::vector bytes(4, 0); + CHECK_FALSE(parse_tls_client_hello(bytes).has_value()); +} + +TEST_CASE("parse_tls_client_hello stops gracefully on a malformed extension length") { + auto record = build_client_hello("example.com", /*corrupt_sni_ext_len=*/true); + auto hello = parse_tls_client_hello(record); + REQUIRE(hello.has_value()); // still a structurally valid ClientHello otherwise + CHECK_FALSE(hello->server_name.has_value()); // SNI extension was malformed, so skipped +} + +TEST_CASE("TlsSniDissector claims port 443 and its summary matches parse_tls_client_hello") { + TlsSniDissector dissector; + CHECK(dissector.port() == kTlsPort); + + auto record = build_client_hello("wireframe.test"); + auto summary = dissector.summarize(record); + REQUIRE(summary.has_value()); + CHECK(summary->substr(0, 3) == "TLS"); + CHECK(summary->find("SNI=wireframe.test") != std::string::npos); +} -- cgit v1.2.3