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_ipv6.cpp | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 tests/test_ipv6.cpp (limited to 'tests/test_ipv6.cpp') diff --git a/tests/test_ipv6.cpp b/tests/test_ipv6.cpp new file mode 100644 index 0000000..438fadc --- /dev/null +++ b/tests/test_ipv6.cpp @@ -0,0 +1,169 @@ +#include + +#include + +#include "wireframe/net/ipv4.hpp" // for kProtoTcp +#include "wireframe/net/ipv6.hpp" + +using namespace wireframe::net; + +namespace { + +Ipv6Address addr_from_groups(std::array groups) { + Ipv6Address addr{}; + for (std::size_t i = 0; i < 8; ++i) { + addr.bytes[i * 2] = static_cast(groups[i] >> 8); + addr.bytes[i * 2 + 1] = static_cast(groups[i] & 0xFF); + } + return addr; +} + +} // namespace + +TEST_CASE("parse_ipv6 decodes header fields and leaves the right payload") { + std::vector bytes(40, 0); + bytes[0] = 0x60; // version 6, traffic class high nibble 0 + bytes[1] = 0x00; // traffic class low nibble 0, flow label starts 0 + bytes[4] = 0x00; + bytes[5] = 0x04; // payload_length = 4 + bytes[6] = kProtoTcp; + bytes[7] = 64; // hop_limit + // src = 2001:0db8::1 + bytes[8] = 0x20; bytes[9] = 0x01; bytes[10] = 0x0d; bytes[11] = 0xb8; + bytes[23] = 0x01; + // dst = ::1 + bytes[39] = 0x01; + bytes.insert(bytes.end(), {0xAA, 0xBB, 0xCC, 0xDD}); + + auto ip6 = parse_ipv6(bytes); + REQUIRE(ip6.has_value()); + CHECK(ip6->header.version == 6); + CHECK(ip6->header.payload_length == 4); + CHECK(ip6->header.next_header == kProtoTcp); + CHECK(ip6->header.hop_limit == 64); + REQUIRE(ip6->payload.size() == 4); + CHECK(ip6->payload[0] == 0xAA); +} + +TEST_CASE("parse_ipv6 rejects a non-IPv6 version") { + std::vector bytes(40, 0); + bytes[0] = 0x45; // version 4 + CHECK_FALSE(parse_ipv6(bytes).has_value()); +} + +TEST_CASE("parse_ipv6 rejects a buffer shorter than the 40-byte header") { + std::vector bytes(39, 0); + bytes[0] = 0x60; + CHECK_FALSE(parse_ipv6(bytes).has_value()); +} + +TEST_CASE("ipv6_to_string compresses the loopback address") { + CHECK(ipv6_to_string(addr_from_groups({0, 0, 0, 0, 0, 0, 0, 1})) == "::1"); +} + +TEST_CASE("ipv6_to_string compresses the unspecified address") { + CHECK(ipv6_to_string(addr_from_groups({0, 0, 0, 0, 0, 0, 0, 0})) == "::"); +} + +TEST_CASE("ipv6_to_string compresses a zero run in the middle") { + CHECK(ipv6_to_string(addr_from_groups({0x2001, 0x0db8, 0, 0, 0, 0, 0, 1})) == "2001:db8::1"); +} + +TEST_CASE("ipv6_to_string does not compress a lone zero group") { + CHECK(ipv6_to_string(addr_from_groups({0x2001, 0, 0x0db8, 1, 1, 1, 1, 1})) == + "2001:0:db8:1:1:1:1:1"); +} + +TEST_CASE("ipv6_to_string picks the leftmost run when two runs tie in length") { + // Two runs of length 2: groups[1..2] and groups[5..6]. Leftmost wins. + CHECK(ipv6_to_string(addr_from_groups({1, 0, 0, 2, 3, 0, 0, 4})) == "1::2:3:0:0:4"); +} + +TEST_CASE("ipv6_to_string leaves an address with no zero run untouched") { + CHECK(ipv6_to_string(addr_from_groups({1, 2, 3, 4, 5, 6, 7, 8})) == "1:2:3:4:5:6:7:8"); +} + +TEST_CASE("walk_ipv6_extension_headers passes a direct transport protocol through unchanged") { + std::vector payload = {0xAA, 0xBB, 0xCC}; + auto result = walk_ipv6_extension_headers(kProtoTcp, payload); + CHECK(result.final_next_header == kProtoTcp); + CHECK_FALSE(result.stopped_at_esp); + REQUIRE(result.payload.size() == 3); + CHECK(result.payload[0] == 0xAA); +} + +TEST_CASE("walk_ipv6_extension_headers walks a single Hop-by-Hop header to reach TCP") { + // Hop-by-Hop: next_header(1)=TCP, hdr_ext_len(1)=0 -> total len (0+1)*8=8 bytes. + std::vector payload = {static_cast(kProtoTcp), 0x00, + 0, 0, 0, 0, 0, 0}; // 6 bytes of option padding + std::vector tcp_marker = {0xDE, 0xAD}; + payload.insert(payload.end(), tcp_marker.begin(), tcp_marker.end()); + + auto result = walk_ipv6_extension_headers(kNextHeaderHopByHop, payload); + CHECK(result.final_next_header == kProtoTcp); + CHECK_FALSE(result.stopped_at_esp); + REQUIRE(result.payload.size() == 2); + CHECK(result.payload[0] == 0xDE); +} + +TEST_CASE("walk_ipv6_extension_headers walks a chain of two extension headers") { + // Hop-by-Hop (8 bytes) -> Destination Options (8 bytes) -> UDP. + std::vector payload = { + kNextHeaderDestOptions, 0x00, 0, 0, 0, 0, 0, 0, // Hop-by-Hop, len 8 + static_cast(kProtoUdp), 0x00, 0, 0, 0, 0, 0, 0, // Dest Options, len 8 + 0xFE, 0xED, // "UDP header" marker + }; + auto result = walk_ipv6_extension_headers(kNextHeaderHopByHop, payload); + CHECK(result.final_next_header == kProtoUdp); + REQUIRE(result.payload.size() == 2); + CHECK(result.payload[0] == 0xFE); +} + +TEST_CASE("walk_ipv6_extension_headers walks the fixed-size Fragment header") { + std::vector payload = {static_cast(kProtoTcp), 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 8-byte fragment header + 0xCA, 0xFE}; + auto result = walk_ipv6_extension_headers(kNextHeaderFragment, payload); + CHECK(result.final_next_header == kProtoTcp); + REQUIRE(result.payload.size() == 2); + CHECK(result.payload[0] == 0xCA); +} + +TEST_CASE("walk_ipv6_extension_headers applies AH's 4-byte-unit length formula") { + // AH: next_header(1)=TCP, payload_len(1)=1 -> total len (1+2)*4=12 bytes. + std::vector payload(12, 0); + payload[0] = static_cast(kProtoTcp); + payload[1] = 0x01; + payload.push_back(0x11); + payload.push_back(0x22); + + auto result = walk_ipv6_extension_headers(kNextHeaderAh, payload); + CHECK(result.final_next_header == kProtoTcp); + REQUIRE(result.payload.size() == 2); + CHECK(result.payload[0] == 0x11); +} + +TEST_CASE("walk_ipv6_extension_headers stops at ESP without guessing past it") { + std::vector payload = {0x01, 0x02, 0x03, 0x04}; + auto result = walk_ipv6_extension_headers(kNextHeaderEsp, payload); + CHECK(result.stopped_at_esp); + CHECK(result.final_next_header == kNextHeaderEsp); + REQUIRE(result.payload.size() == 4); + CHECK(result.payload[0] == 0x01); // untouched: ESP payload starts right here +} + +TEST_CASE("walk_ipv6_extension_headers stops gracefully on a truncated extension header") { + std::vector payload = {static_cast(kProtoTcp), + 0xFF}; // claims (255+1)*8 bytes; nowhere near present + auto result = walk_ipv6_extension_headers(kNextHeaderHopByHop, payload); + CHECK(result.final_next_header == kNextHeaderHopByHop); // never resolved past it + CHECK_FALSE(result.stopped_at_esp); +} + +TEST_CASE("walk_ipv6_extension_headers passes an unknown next_header through untouched") { + std::vector payload = {0x01, 0x02}; + auto result = walk_ipv6_extension_headers(200, payload); // not a known extension type + CHECK(result.final_next_header == 200); + REQUIRE(result.payload.size() == 2); + CHECK(result.payload[0] == 0x01); +} -- cgit v1.2.3