diff options
| author | srdusr <[email protected]> | 2025-06-26 14:59:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2025-06-26 14:59:00 +0200 |
| commit | 9046e6a10fd2d987cf6f6dc601ed3a75d286793f (patch) | |
| tree | 2f28a977e4bf8143a4a8468bc474bbab725b950c /tests | |
| parent | d9bedcec1bce8d15de6403377702d51d1bcb862f (diff) | |
| download | packeteer-9046e6a10fd2d987cf6f6dc601ed3a75d286793f.tar.gz packeteer-9046e6a10fd2d987cf6f6dc601ed3a75d286793f.zip | |
Unwrap VLAN (802.1Q/802.1ad) tags before protocol dispatch
The single highest-value coverage gap so far, and structural rather
than a new dissector: a VLAN-tagged frame's ethertype reads as 0x8100,
so every existing decoder - ARP, IPv4, IPv6, and everything built on
top of them - was completely invisible on any tagged network.
walk_vlan_tags() (ethernet.hpp) is composable and separate from
parse_ethernet(), the same relationship walk_ipv6_extension_headers()
has to parse_ipv6(): the base parse stays an unconditional fixed-header
decode, and this is what a caller reaches for when it needs the real
protocol underneath. Handles stacked (QinQ) tags, bounded at 4 levels
against a corrupt/hostile frame claiming an unbounded chain.
Live-verified with genuine kernel-tagged frames, not synthetic bytes:
a dummy0 interface with an 802.1Q dummy0.42 sub-interface (VLAN 42),
captured on the parent while pinging out the sub-interface. Both
interfaces and the kernel modules they pulled in were torn down
afterward.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_net.cpp | 57 | ||||
| -rw-r--r-- | tests/test_summarize.cpp | 16 |
2 files changed, 73 insertions, 0 deletions
diff --git a/tests/test_net.cpp b/tests/test_net.cpp index 19667da..2702758 100644 --- a/tests/test_net.cpp +++ b/tests/test_net.cpp @@ -30,6 +30,63 @@ TEST_CASE("parse_ethernet rejects a frame shorter than the header") { CHECK_FALSE(parse_ethernet(bytes).has_value()); } +TEST_CASE("walk_vlan_tags passes an untagged ethertype through unchanged") { + std::vector<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> 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<unsigned char> bytes(20, 0); bytes[0] = 0x45; // version 4, IHL 5 (20-byte header, no options) diff --git a/tests/test_summarize.cpp b/tests/test_summarize.cpp index 27e0dd3..d7b223a 100644 --- a/tests/test_summarize.cpp +++ b/tests/test_summarize.cpp @@ -186,6 +186,22 @@ TEST_CASE("summarize_packet decodes an ARP request end to end") { "ARP who-has 10.0.0.2 tell 10.0.0.1 (aa:bb:cc:dd:ee:ff)"); } +TEST_CASE("summarize_packet unwraps a VLAN tag to reach the real ARP payload underneath") { + std::vector<unsigned char> bytes = { + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, + 0x81, 0x00, // ethertype: 802.1Q + 0x00, 42, // TCI: VLAN 42 + 0x08, 0x06, // real ethertype: ARP + 0x00, 0x01, 0x08, 0x00, 0x06, 0x04, 0x00, 0x01, + 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 10, 0, 0, 1, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 10, 0, 0, 2, + }; + auto line = packeteer::summarize_packet(bytes, DLT_EN10MB); + CHECK(line == + "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x8100 vlan=42 | " + "ARP who-has 10.0.0.2 tell 10.0.0.1 (aa:bb:cc:dd:ee:ff)"); +} + 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); |