diff options
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 |