srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_net.cpp
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-06-26 14:59:00 +0200
committersrdusr <[email protected]>2025-06-26 14:59:00 +0200
commit9046e6a10fd2d987cf6f6dc601ed3a75d286793f (patch)
tree2f28a977e4bf8143a4a8468bc474bbab725b950c /tests/test_net.cpp
parentd9bedcec1bce8d15de6403377702d51d1bcb862f (diff)
downloadpacketeer-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/test_net.cpp')
-rw-r--r--tests/test_net.cpp57
1 files changed, 57 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)