diff options
| author | srdusr <[email protected]> | 2024-05-14 01:42:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-05-14 01:42:00 +0200 |
| commit | 08332a4195956611db80a2cfe3710d760cbd6acf (patch) | |
| tree | 0cb5cdf9fdcfdd8dc8c129a33575ad9b182d5c01 /tests/test_capture_session.cpp | |
| download | packeteer-08332a4195956611db80a2cfe3710d760cbd6acf.tar.gz packeteer-08332a4195956611db80a2cfe3710d760cbd6acf.zip | |
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)
Diffstat (limited to 'tests/test_capture_session.cpp')
| -rw-r--r-- | tests/test_capture_session.cpp | 138 |
1 files changed, 138 insertions, 0 deletions
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 <doctest/doctest.h> +#include <pcap.h> +#include <unistd.h> + +#include <cstdio> +#include <string> +#include <vector> + +#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<std::vector<unsigned char>>& 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<std::uint16_t>(link_type)); + std::uint32_t ts = 1700000000; + for (const auto& packet : packets) { + writer.write_packet(0, ts++, 0, packet, + static_cast<std::uint32_t>(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<unsigned char> 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); +} |