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 /include/wireframe/pcapng/reader.hpp | |
| 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 'include/wireframe/pcapng/reader.hpp')
| -rw-r--r-- | include/wireframe/pcapng/reader.hpp | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/include/wireframe/pcapng/reader.hpp b/include/wireframe/pcapng/reader.hpp new file mode 100644 index 0000000..d01b431 --- /dev/null +++ b/include/wireframe/pcapng/reader.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include <array> +#include <cstdint> +#include <cstdio> +#include <optional> +#include <span> +#include <vector> + +// Minimal pcapng reader, paired with writer.hpp: reads Enhanced Packet +// Blocks sequentially, skipping the Section Header Block, Interface +// Description Block, and any other block type transparently. +// +// Assumes little-endian block encoding (checked against the Section +// Header Block's byte-order magic, not just assumed) since that's what +// writer.hpp emits and what pcapng writers on this class of hardware +// (tcpdump, dumpcap) produce. A big-endian file is out of scope - this +// pairs with our own writer, not general pcapng interop. +namespace wireframe::pcapng { + +struct PacketRecord { + std::uint32_t interface_id; + std::uint64_t timestamp_us; + std::uint32_t original_len; + std::vector<unsigned char> data; +}; + +class Reader { +public: + explicit Reader(std::FILE* file) : file_(file) {} + + // Returns the next packet, or nullopt once the file is exhausted or + // a malformed/unsupported block is hit - treated as end of stream + // rather than a hard error, to keep this reader small. + std::optional<PacketRecord> next_packet() { + for (;;) { + std::array<std::uint8_t, 4> field{}; + if (std::fread(field.data(), 1, 4, file_) != 4) return std::nullopt; + std::uint32_t type = get_u32(field); + + if (std::fread(field.data(), 1, 4, file_) != 4) return std::nullopt; + std::uint32_t total_len = get_u32(field); + if (total_len < 12) return std::nullopt; + + std::size_t body_len = total_len - 12; + // total_len is an untrusted 32-bit value straight from the + // file; without a cap, a corrupted/hostile file can claim + // a multi-gigabyte block and OOM the process on the + // allocation below before a single byte is even read to + // check whether the file actually contains that much data + // (found by fuzzing fuzz_pcapng_reader.cpp - real crash, + // not theoretical). Bounded well above any block our own + // writer produces (packets capped at a 65535 snaplen; this + // reader is explicitly scoped to pair with that writer, + // not arbitrary pcapng interop). + if (body_len > kMaxBlockBodyLen) return std::nullopt; + std::vector<std::uint8_t> body(body_len); + if (body_len > 0 && std::fread(body.data(), 1, body_len, file_) != body_len) { + return std::nullopt; + } + + if (std::fread(field.data(), 1, 4, file_) != 4) return std::nullopt; + if (get_u32(field) != total_len) return std::nullopt; // corrupt trailer + + if (type == kBlockTypeShb) { + if (body_len < 4 || get_u32({body.data(), 4}) != kByteOrderMagic) { + return std::nullopt; // not little-endian, or malformed + } + continue; + } + if (type == kBlockTypeIdb) { + // LinkType is the first 2 bytes of the IDB body (see + // writer.hpp's write_interface_description). Only the + // first IDB is captured - correct for a file our own + // writer produced, which only ever writes one + // interface, matching this reader's documented scope. + if (!link_type_ && body_len >= 2) { + link_type_ = static_cast<std::uint16_t>(body[0] | (body[1] << 8)); + } + continue; + } + if (type != kBlockTypeEpb) continue; // anything else: skip + + if (body_len < 20) return std::nullopt; + + PacketRecord record; + record.interface_id = get_u32({body.data() + 0, 4}); + std::uint32_t ts_high = get_u32({body.data() + 4, 4}); + std::uint32_t ts_low = get_u32({body.data() + 8, 4}); + record.timestamp_us = (static_cast<std::uint64_t>(ts_high) << 32) | ts_low; + std::uint32_t caplen = get_u32({body.data() + 12, 4}); + record.original_len = get_u32({body.data() + 16, 4}); + + if (body_len < 20 + caplen) return std::nullopt; + record.data.assign(body.begin() + 20, body.begin() + 20 + caplen); + return record; + } + } + + // The interface's link type, learned from the Interface + // Description Block once next_packet() has read past it (which + // happens before it ever returns the first EPB, so this is + // populated by the time the first successful next_packet() call + // returns). nullopt if no IDB has been seen yet. + std::optional<std::uint16_t> link_type() const { return link_type_; } + +private: + static std::uint32_t get_u32(std::span<const std::uint8_t> b) { + return static_cast<std::uint32_t>(b[0]) | (static_cast<std::uint32_t>(b[1]) << 8) | + (static_cast<std::uint32_t>(b[2]) << 16) | (static_cast<std::uint32_t>(b[3]) << 24); + } + + static constexpr std::uint32_t kBlockTypeShb = 0x0A0D0D0A; + static constexpr std::uint32_t kBlockTypeIdb = 0x00000001; + static constexpr std::uint32_t kBlockTypeEpb = 0x00000006; + static constexpr std::uint32_t kByteOrderMagic = 0x1A2B3C4D; + static constexpr std::size_t kMaxBlockBodyLen = 1 << 20; // 1 MiB + + std::FILE* file_; + std::optional<std::uint16_t> link_type_; +}; + +} // namespace wireframe::pcapng |