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_summarize.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_summarize.cpp')
| -rw-r--r-- | tests/test_summarize.cpp | 185 |
1 files changed, 185 insertions, 0 deletions
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 <doctest/doctest.h> +#include <pcap.h> + +#include <string_view> +#include <vector> + +#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<unsigned char> ethernet_ipv4_udp_dns_frame() { + std::vector<unsigned char> 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<unsigned char> 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<std::uint16_t>(8 + dns.size()); + udp[4] = static_cast<unsigned char>(udp_len >> 8); + udp[5] = static_cast<unsigned char>(udp_len & 0xFF); + + std::vector<unsigned char> 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<unsigned char> eth = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac + 0x08, 0x00, // ethertype IPv4 + }; + + std::vector<unsigned char> 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<unsigned char> 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<unsigned char> http(request.begin(), request.end()); + + std::vector<unsigned char> 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<unsigned char> 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<unsigned char> eth = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac + 0x08, 0x00, // ethertype IPv4 + }; + + std::vector<unsigned char> 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<unsigned char> ethernet_ipv6_hopbyhop_tcp_frame() { + std::vector<unsigned char> 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<unsigned char> hop_by_hop = { + static_cast<unsigned char>(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<unsigned char> ip6(40, 0); + ip6[0] = 0x60; // version 6 + std::uint16_t payload_len = static_cast<std::uint16_t>(hop_by_hop.size() + tcp.size()); + ip6[4] = static_cast<unsigned char>(payload_len >> 8); + ip6[5] = static_cast<unsigned char>(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<unsigned char> eth = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac + 0x86, 0xDD, // ethertype IPv6 + }; + + std::vector<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> bytes(20, 0); + for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<unsigned char>(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); +} |