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_summarize.cpp | 185 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 tests/test_summarize.cpp (limited to 'tests/test_summarize.cpp') diff --git a/tests/test_summarize.cpp b/tests/test_summarize.cpp new file mode 100644 index 0000000..d10053d --- /dev/null +++ b/tests/test_summarize.cpp @@ -0,0 +1,185 @@ +#include +#include + +#include +#include + +#include "wireframe/summarize.hpp" + +namespace { + +// Ethernet + IPv4 + UDP + DNS query for "example.com", assembled the +// same way the real capture path hands bytes to summarize_packet: one +// contiguous frame, no struct-casting. +std::vector ethernet_ipv4_udp_dns_frame() { + std::vector dns = { + 0x12, 0x9d, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, + 0x00, 0x01, 0x00, 0x01, + }; + + std::vector udp(8, 0); + udp[0] = 0xD4; udp[1] = 0x31; // src port 54321 + udp[2] = 0x00; udp[3] = 0x35; // dst port 53 + std::uint16_t udp_len = static_cast(8 + dns.size()); + udp[4] = static_cast(udp_len >> 8); + udp[5] = static_cast(udp_len & 0xFF); + + std::vector ip(20, 0); + ip[0] = 0x45; + ip[8] = 64; // ttl + ip[9] = wireframe::net::kProtoUdp; // proto + ip[12] = 10; ip[13] = 0; ip[14] = 0; ip[15] = 1; // src 10.0.0.1 + ip[16] = 10; ip[17] = 0; ip[18] = 0; ip[19] = 2; // dst 10.0.0.2 + + std::vector eth = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac + 0x08, 0x00, // ethertype IPv4 + }; + + std::vector frame = eth; + frame.insert(frame.end(), ip.begin(), ip.end()); + frame.insert(frame.end(), udp.begin(), udp.end()); + frame.insert(frame.end(), dns.begin(), dns.end()); + return frame; +} + +// Ethernet + IPv4 + TCP + an HTTP GET request. This is the only test +// exercising L7Registry's TCP-payload path with a real registered +// dissector - DNS only ever runs over UDP, so summarize_packet's TCP +// branch calling into l7_summarize() was otherwise unverified. +std::vector ethernet_ipv4_tcp_http_frame() { + std::string_view request = "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"; + std::vector http(request.begin(), request.end()); + + std::vector tcp(20, 0); + tcp[0] = 0xC3; tcp[1] = 0x50; // src port 50000 + tcp[2] = 0x00; tcp[3] = 0x50; // dst port 80 + tcp[12] = 5 << 4; // data_offset = 5 (20-byte header) + tcp[13] = 0x18; // PSH | ACK + + std::vector ip(20, 0); + ip[0] = 0x45; + ip[8] = 64; // ttl + ip[9] = wireframe::net::kProtoTcp; // proto + ip[12] = 10; ip[13] = 0; ip[14] = 0; ip[15] = 1; // src 10.0.0.1 + ip[16] = 10; ip[17] = 0; ip[18] = 0; ip[19] = 2; // dst 10.0.0.2 + + std::vector eth = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac + 0x08, 0x00, // ethertype IPv4 + }; + + std::vector frame = eth; + frame.insert(frame.end(), ip.begin(), ip.end()); + frame.insert(frame.end(), tcp.begin(), tcp.end()); + frame.insert(frame.end(), http.begin(), http.end()); + return frame; +} + +// Ethernet + IPv6 + a Hop-by-Hop Options extension header + TCP. Proves +// walk_ipv6_extension_headers() is actually wired into summarize_packet's +// IPv6 branch, not just unit-tested in isolation - without it, this +// packet's TCP layer (and any L7 behind it) would be silently invisible. +std::vector ethernet_ipv6_hopbyhop_tcp_frame() { + std::vector tcp(20, 0); + tcp[0] = 0x00; tcp[1] = 0x50; // src port 80 + tcp[2] = 0x00; tcp[3] = 0x51; // dst port 81 + tcp[12] = 5 << 4; // data_offset = 5 + tcp[13] = 0x02; // SYN + + std::vector hop_by_hop = { + static_cast(wireframe::net::kProtoTcp), + 0x00, // hdr_ext_len = 0 -> total length (0+1)*8 = 8 bytes + 0, 0, 0, 0, 0, 0, // option padding + }; + + std::vector ip6(40, 0); + ip6[0] = 0x60; // version 6 + std::uint16_t payload_len = static_cast(hop_by_hop.size() + tcp.size()); + ip6[4] = static_cast(payload_len >> 8); + ip6[5] = static_cast(payload_len & 0xFF); + ip6[6] = wireframe::net::kNextHeaderHopByHop; + ip6[7] = 64; // hop_limit + ip6[23] = 0x01; // src = ::1 + ip6[39] = 0x01; // dst = ::1 + + std::vector eth = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac + 0x86, 0xDD, // ethertype IPv6 + }; + + std::vector frame = eth; + frame.insert(frame.end(), ip6.begin(), ip6.end()); + frame.insert(frame.end(), hop_by_hop.begin(), hop_by_hop.end()); + frame.insert(frame.end(), tcp.begin(), tcp.end()); + return frame; +} + +} // namespace + +TEST_CASE("summarize_packet walks a Hop-by-Hop extension header to reach TCP") { + auto line = wireframe::summarize_packet(ethernet_ipv6_hopbyhop_tcp_frame(), DLT_EN10MB); + CHECK(line == + "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x86dd" + " | IPv6 ::1 -> ::1 ttl=64 proto=6" + " | TCP 80 -> 81 [S] seq=0 ack=0 win=0"); +} + +TEST_CASE("summarize_packet decodes a full Ethernet/IPv4/TCP/HTTP frame end to end") { + auto line = wireframe::summarize_packet(ethernet_ipv4_tcp_http_frame(), DLT_EN10MB); + CHECK(line == + "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x0800" + " | IPv4 10.0.0.1 -> 10.0.0.2 ttl=64 proto=6" + " | TCP 50000 -> 80 [AP] seq=0 ack=0 win=0" + " | HTTP GET /index.html Host: example.com"); +} + +TEST_CASE("summarize_packet decodes a full Ethernet/IPv4/UDP/DNS frame end to end") { + auto line = wireframe::summarize_packet(ethernet_ipv4_udp_dns_frame(), DLT_EN10MB); + CHECK(line == + "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x0800" + " | IPv4 10.0.0.1 -> 10.0.0.2 ttl=64 proto=17" + " | UDP 54321 -> 53 len=37" + " | DNS query id=4765 example.com type=1"); +} + +TEST_CASE("summarize_packet on DLT_RAW skips the Ethernet line entirely") { + auto frame = ethernet_ipv4_udp_dns_frame(); + std::vector raw(frame.begin() + wireframe::net::kEthernetHeaderLen, frame.end()); + + auto line = wireframe::summarize_packet(raw, DLT_RAW); + CHECK(line.substr(0, 3) == "RAW"); + CHECK(line.find("ETH") == std::string::npos); + CHECK(line.find("IPv4 10.0.0.1 -> 10.0.0.2") != std::string::npos); +} + +TEST_CASE("summarize_packet reports a truncated Ethernet frame without decoding further") { + std::vector bytes(10, 0); // shorter than the 14-byte header + auto line = wireframe::summarize_packet(bytes, DLT_EN10MB); + CHECK(line == "[10 bytes] truncated ethernet frame"); +} + +TEST_CASE("summarize_packet stops after the Ethernet line for a non-IP ethertype") { + std::vector bytes = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, + 0x08, 0x06, // ARP, not IPv4/IPv6 + }; + auto line = wireframe::summarize_packet(bytes, DLT_EN10MB); + CHECK(line == "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x0806"); +} + +TEST_CASE("hex_dump_lines produces one line per 16 bytes, with the right byte count") { + std::vector bytes(20, 0); + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast(i); + + auto lines = wireframe::hex_dump_lines(bytes); + REQUIRE(lines.size() == 2); + CHECK(lines[0].substr(0, 6) == "000000"); + CHECK(lines[1].substr(0, 6) == "000010"); + CHECK(lines[0].find("00 01 02 03") != std::string::npos); + CHECK(lines[0].find('|') != std::string::npos); +} -- cgit v1.2.3