#include #include #include "packeteer/net/ethernet.hpp" #include "packeteer/net/ipv4.hpp" #include "packeteer/net/tcp.hpp" #include "packeteer/net/udp.hpp" using namespace packeteer::net; TEST_CASE("parse_ethernet decodes header fields and leaves the right payload") { std::vector bytes = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, // dst mac 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac 0x08, 0x00, // ethertype: IPv4 0xDE, 0xAD, 0xBE, 0xEF, // payload }; auto frame = parse_ethernet(bytes); REQUIRE(frame.has_value()); CHECK(frame->header.dst.bytes == std::array{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}); CHECK(frame->header.src.bytes == std::array{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}); CHECK(frame->header.ethertype == kEthertypeIPv4); REQUIRE(frame->payload.size() == 4); CHECK(frame->payload[0] == 0xDE); } TEST_CASE("parse_ethernet rejects a frame shorter than the header") { std::vector bytes(10, 0); // header is 14 bytes CHECK_FALSE(parse_ethernet(bytes).has_value()); } TEST_CASE("walk_vlan_tags passes an untagged ethertype through unchanged") { std::vector payload = {0xDE, 0xAD, 0xBE, 0xEF}; auto result = walk_vlan_tags(kEthertypeIPv4, payload); CHECK(result.vlan_ids.empty()); CHECK(result.ethertype == kEthertypeIPv4); REQUIRE(result.payload.size() == 4); CHECK(result.payload[0] == 0xDE); } TEST_CASE("walk_vlan_tags unwraps a single 802.1Q tag") { std::vector payload = { 0x00, 42, // TCI: VLAN ID 42 (PCP/DEI bits left zero) 0x08, 0x00, // real ethertype: IPv4 0xDE, 0xAD, // real payload }; auto result = walk_vlan_tags(kEthertypeVlan, payload); REQUIRE(result.vlan_ids.size() == 1); CHECK(result.vlan_ids[0] == 42); CHECK(result.ethertype == kEthertypeIPv4); REQUIRE(result.payload.size() == 2); CHECK(result.payload[0] == 0xDE); } TEST_CASE("walk_vlan_tags unwraps a stacked QinQ pair, outer to inner") { std::vector payload = { 0x00, 100, // outer TCI: VLAN 100 0x81, 0x00, // inner tag's TPID 0x00, 42, // inner TCI: VLAN 42 0x08, 0x00, // real ethertype: IPv4 0xBE, 0xEF, }; auto result = walk_vlan_tags(kEthertypeVlanQinQ, payload); REQUIRE(result.vlan_ids.size() == 2); CHECK(result.vlan_ids[0] == 100); CHECK(result.vlan_ids[1] == 42); CHECK(result.ethertype == kEthertypeIPv4); } TEST_CASE("walk_vlan_tags stops at a truncated tag rather than reading past it") { std::vector payload = {0x00, 42}; // 2 bytes: not a full 4-byte tag auto result = walk_vlan_tags(kEthertypeVlan, payload); CHECK(result.vlan_ids.empty()); CHECK(result.ethertype == kEthertypeVlan); // unchanged: nothing was actually unwrapped } TEST_CASE("walk_vlan_tags is bounded against a claimed unbounded tag chain") { // Each 4-byte block claims "the next ethertype is another VLAN // tag" - a corrupt/hostile frame that never actually reaches a // real ethertype. Must stop, not loop indefinitely. std::vector payload; for (int i = 0; i < 100; ++i) { payload.insert(payload.end(), {0x00, 0x01, 0x81, 0x00}); } auto result = walk_vlan_tags(kEthertypeVlan, payload); CHECK(result.vlan_ids.size() <= 4); } TEST_CASE("parse_ipv4 decodes header fields and leaves the right payload") { std::vector bytes(20, 0); bytes[0] = 0x45; // version 4, IHL 5 (20-byte header, no options) bytes[2] = 0x00; bytes[3] = 0x28; // total_length = 40 bytes[8] = 64; // ttl bytes[9] = kProtoTcp; bytes[12] = 10; bytes[13] = 0; bytes[14] = 0; bytes[15] = 1; // src 10.0.0.1 bytes[16] = 10; bytes[17] = 0; bytes[18] = 0; bytes[19] = 2; // dst 10.0.0.2 bytes.push_back(0x01); bytes.push_back(0x02); auto ip = parse_ipv4(bytes); REQUIRE(ip.has_value()); CHECK(ip->header.version == 4); CHECK(ip->header.ihl == 5); CHECK(ip->header.total_length == 40); CHECK(ip->header.ttl == 64); CHECK(ip->header.protocol == kProtoTcp); CHECK(ip->header.src.bytes == std::array{10, 0, 0, 1}); CHECK(ip->header.dst.bytes == std::array{10, 0, 0, 2}); REQUIRE(ip->payload.size() == 2); CHECK(ip->payload[0] == 0x01); } TEST_CASE("parse_ipv4 rejects a non-IPv4 version") { std::vector bytes(20, 0); bytes[0] = 0x65; // version 6 CHECK_FALSE(parse_ipv4(bytes).has_value()); } TEST_CASE("parse_ipv4 rejects a buffer shorter than the header") { std::vector bytes(10, 0); CHECK_FALSE(parse_ipv4(bytes).has_value()); } TEST_CASE("parse_ipv4 honors IHL > 5 (options present)") { std::vector bytes(24, 0); // IHL=6 -> 24-byte header bytes[0] = 0x46; bytes[9] = kProtoUdp; auto ip = parse_ipv4(bytes); REQUIRE(ip.has_value()); CHECK(ip->header.ihl == 6); CHECK(ip->payload.empty()); } TEST_CASE("parse_tcp decodes header fields and flags") { std::vector bytes(20, 0); bytes[0] = 0x00; bytes[1] = 0x50; // src port 80 bytes[2] = 0x1F; bytes[3] = 0x90; // dst port 8080 bytes[4] = 0; bytes[5] = 0; bytes[6] = 0; bytes[7] = 1; // seq = 1 bytes[8] = 0; bytes[9] = 0; bytes[10] = 0; bytes[11] = 2; // ack = 2 bytes[12] = 5 << 4; // data_offset = 5 (20-byte header, no options) bytes[13] = 0x12; // SYN | ACK bytes[14] = 0xFF; bytes[15] = 0xFF; // window 65535 auto tcp = parse_tcp(bytes); REQUIRE(tcp.has_value()); CHECK(tcp->header.src_port == 80); CHECK(tcp->header.dst_port == 8080); CHECK(tcp->header.seq == 1); CHECK(tcp->header.ack == 2); CHECK(tcp->header.data_offset == 5); CHECK((tcp->header.flags & kTcpSyn) != 0); CHECK((tcp->header.flags & kTcpAck) != 0); CHECK((tcp->header.flags & kTcpFin) == 0); CHECK(tcp->header.window == 65535); CHECK(tcp->payload.empty()); } TEST_CASE("parse_tcp rejects a buffer shorter than the header") { std::vector bytes(10, 0); CHECK_FALSE(parse_tcp(bytes).has_value()); } TEST_CASE("parse_udp decodes header fields and leaves the right payload") { std::vector bytes = {0x00, 0x35, 0x1F, 0x90, 0x00, 0x0A, 0x00, 0x00, 'h', 'i'}; auto udp = parse_udp(bytes); REQUIRE(udp.has_value()); CHECK(udp->header.src_port == 53); CHECK(udp->header.dst_port == 8080); CHECK(udp->header.length == 10); REQUIRE(udp->payload.size() == 2); CHECK(udp->payload[0] == 'h'); } TEST_CASE("parse_udp rejects a buffer shorter than the header") { std::vector bytes(4, 0); CHECK_FALSE(parse_udp(bytes).has_value()); }