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_capture_session.cpp | 138 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 tests/test_capture_session.cpp (limited to 'tests/test_capture_session.cpp') diff --git a/tests/test_capture_session.cpp b/tests/test_capture_session.cpp new file mode 100644 index 0000000..691131a --- /dev/null +++ b/tests/test_capture_session.cpp @@ -0,0 +1,138 @@ +#include +#include +#include + +#include +#include +#include + +#include "wireframe/capture_session.hpp" +#include "wireframe/pcapng/writer.hpp" + +using namespace wireframe; + +namespace { + +// A real, named pcapng file on disk - CaptureSession::open() takes a +// path, not a FILE*, so a std::tmpfile() (unnamed) doesn't work here +// the way it does in test_pcapng.cpp. Cleans itself up via RAII. +struct TempPcapngFile { + std::string path; + + explicit TempPcapngFile(int link_type, const std::vector>& packets) { + char path_template[] = "/tmp/wireframe_test_XXXXXX"; + int fd = mkstemp(path_template); + REQUIRE(fd != -1); + path = path_template; + + std::FILE* f = fdopen(fd, "wb"); + REQUIRE(f != nullptr); + pcapng::Writer writer(f); + writer.write_section_header(); + writer.write_interface_description(65535, static_cast(link_type)); + std::uint32_t ts = 1700000000; + for (const auto& packet : packets) { + writer.write_packet(0, ts++, 0, packet, + static_cast(packet.size())); + } + std::fclose(f); + } + + ~TempPcapngFile() { std::remove(path.c_str()); } +}; + +} // namespace + +TEST_CASE("is_supported_datalink accepts EN10MB and RAW, rejects others") { + CHECK(is_supported_datalink(DLT_EN10MB)); + CHECK(is_supported_datalink(DLT_RAW)); + CHECK_FALSE(is_supported_datalink(DLT_IEEE802_11)); +} + +TEST_CASE("CaptureSession::open reports an error for a nonexistent device, without needing root") { + CaptureSession session; + CaptureSessionOptions options; + options.device = "this-device-does-not-exist-0xdeadbeef"; + + auto err = session.open(options); + REQUIRE(err.has_value()); + CHECK_FALSE(err->empty()); +} + +TEST_CASE("CaptureSession::stats returns nullopt before open()") { + CaptureSession session; + CHECK_FALSE(session.stats().has_value()); +} + +TEST_CASE("CaptureSession::open replays a pcapng file without needing root or a live device") { + TempPcapngFile file(DLT_EN10MB, {{0xDE, 0xAD}, {0xBE, 0xEF}}); + + CaptureSession session; + CaptureSessionOptions options; + options.replay_input_path = file.path; + + CHECK_FALSE(session.open(options).has_value()); + CHECK(session.is_replay()); + CHECK(session.datalink() == DLT_EN10MB); + CHECK(session.device() == file.path); + CHECK_FALSE(session.stats().has_value()); // pcap_stats() needs a live handle; replay has none +} + +TEST_CASE("CaptureSession::open rejects combining -r (replay) with -f (capture filter)") { + TempPcapngFile file(DLT_EN10MB, {{0x01}}); + + CaptureSession session; + CaptureSessionOptions options; + options.replay_input_path = file.path; + options.filter_expr = "tcp"; + + auto err = session.open(options); + REQUIRE(err.has_value()); + CHECK(err->find("-r") != std::string::npos); +} + +TEST_CASE("CaptureSession::open reports an error for a nonexistent replay file") { + CaptureSession session; + CaptureSessionOptions options; + options.replay_input_path = "/tmp/this-file-does-not-exist-0xdeadbeef.pcapng"; + + auto err = session.open(options); + REQUIRE(err.has_value()); + CHECK_FALSE(err->empty()); +} + +TEST_CASE("CaptureSession::open reports an error for a pcapng file with no packets") { + TempPcapngFile file(DLT_EN10MB, {}); // just SHB + IDB, no EPBs + + CaptureSession session; + CaptureSessionOptions options; + options.replay_input_path = file.path; + + auto err = session.open(options); + REQUIRE(err.has_value()); +} + +TEST_CASE("replayed packets reach the CaptureQueue in order, and the thread stops on its own") { + TempPcapngFile file(DLT_RAW, {{0x01, 0x02}, {0x03, 0x04}, {0x05, 0x06}}); + + CaptureSession session; + CaptureSessionOptions options; + options.replay_input_path = file.path; + REQUIRE_FALSE(session.open(options).has_value()); + CHECK(session.datalink() == DLT_RAW); + + CaptureQueue queue(4096); + auto capture_thread = session.start_capture_thread(queue); + + std::vector first_bytes; + int count = 0; + while (auto packet = queue.pop()) { + if (count == 0) first_bytes = packet->data; + ++count; + } + capture_thread.join(); + + CHECK(count == 3); + REQUIRE(first_bytes.size() == 2); + CHECK(first_bytes[0] == 0x01); +} -- cgit v1.2.3