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) --- include/wireframe/l7/dns.hpp | 108 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 include/wireframe/l7/dns.hpp (limited to 'include/wireframe/l7/dns.hpp') diff --git a/include/wireframe/l7/dns.hpp b/include/wireframe/l7/dns.hpp new file mode 100644 index 0000000..5c1ab36 --- /dev/null +++ b/include/wireframe/l7/dns.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wireframe/byteio.hpp" +#include "wireframe/l7/dissector.hpp" + +// Hand-rolled DNS message parsing: header + the first question record. +// Answer/authority/additional records aren't decoded (not needed for a +// one-line summary), so name-compression pointers there are never +// followed - a pointer in the question section itself is rejected +// rather than chased, keeping this a pure forward scan with no risk of +// a pointer loop. +namespace wireframe::net { + +inline constexpr std::uint16_t kDnsPort = 53; + +struct DnsHeader { + std::uint16_t id; + bool is_response; + std::uint8_t opcode; + std::uint8_t rcode; + std::uint16_t qdcount; + std::uint16_t ancount; +}; + +struct DnsQuestion { + std::string name; + std::uint16_t qtype; +}; + +struct DnsMessage { + DnsHeader header; + std::optional question; // first question only +}; + +// Reads a (possibly multi-label) dotted name starting at offset. +// Returns the name and the offset just past it, or nullopt on +// truncation or a compression pointer (0xC0 prefix - valid in +// answer/authority records, not supported here). +inline std::optional> read_dns_name( + std::span bytes, std::size_t offset) { + std::string name; + while (true) { + if (offset >= bytes.size()) return std::nullopt; + std::uint8_t len = bytes[offset]; + if (len == 0) { + ++offset; + break; + } + if ((len & 0xC0) == 0xC0) return std::nullopt; // compression pointer: unsupported + ++offset; + if (offset + len > bytes.size()) return std::nullopt; + if (!name.empty()) name += '.'; + for (std::uint8_t i = 0; i < len; ++i) name += static_cast(bytes[offset + i]); + offset += len; + } + return std::make_pair(std::move(name), offset); +} + +inline std::optional parse_dns(std::span bytes) { + if (bytes.size() < 12) return std::nullopt; + + DnsHeader header{}; + header.id = read_be16(bytes, 0); + std::uint16_t flags = read_be16(bytes, 2); + header.is_response = (flags & 0x8000) != 0; + header.opcode = static_cast((flags >> 11) & 0x0F); + header.rcode = static_cast(flags & 0x0F); + header.qdcount = read_be16(bytes, 4); + header.ancount = read_be16(bytes, 6); + + DnsMessage msg{header, std::nullopt}; + if (header.qdcount >= 1) { + if (auto result = read_dns_name(bytes, 12)) { + auto& [name, next_offset] = *result; + if (next_offset + 4 <= bytes.size()) { + msg.question = DnsQuestion{std::move(name), read_be16(bytes, next_offset)}; + } + } + } + return msg; +} + +class DnsDissector : public L7Dissector { +public: + std::uint16_t port() const override { return kDnsPort; } + + std::optional summarize(std::span payload) const override { + auto msg = parse_dns(payload); + if (!msg) return std::nullopt; + + std::string out = "DNS "; + out += msg->header.is_response ? "response" : "query"; + out += " id=" + std::to_string(msg->header.id); + if (msg->header.is_response) out += " ancount=" + std::to_string(msg->header.ancount); + if (msg->question) { + out += " " + msg->question->name + " type=" + std::to_string(msg->question->qtype); + } + return out; + } +}; + +} // namespace wireframe::net -- cgit v1.2.3