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 /include | |
| 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 'include')
| -rw-r--r-- | include/packeteer/net/ethernet.hpp | 37 | ||||
| -rw-r--r-- | include/packeteer/summarize.hpp | 25 |
2 files changed, 57 insertions, 5 deletions
diff --git a/include/packeteer/net/ethernet.hpp b/include/packeteer/net/ethernet.hpp index 5b851bc..77d2927 100644 --- a/include/packeteer/net/ethernet.hpp +++ b/include/packeteer/net/ethernet.hpp @@ -5,6 +5,7 @@ #include <cstdint> #include <optional> #include <span> +#include <vector> #include "packeteer/byteio.hpp" @@ -14,6 +15,8 @@ inline constexpr std::size_t kEthernetHeaderLen = 14; inline constexpr std::uint16_t kEthertypeIPv4 = 0x0800; inline constexpr std::uint16_t kEthertypeIPv6 = 0x86DD; inline constexpr std::uint16_t kEthertypeArp = 0x0806; +inline constexpr std::uint16_t kEthertypeVlan = 0x8100; // IEEE 802.1Q +inline constexpr std::uint16_t kEthertypeVlanQinQ = 0x88A8; // IEEE 802.1ad, stacked tags struct MacAddress { std::array<unsigned char, 6> bytes; @@ -41,4 +44,38 @@ inline std::optional<EthernetFrame> parse_ethernet(std::span<const unsigned char return EthernetFrame{header, bytes.subspan(kEthernetHeaderLen)}; } +struct VlanWalkResult { + std::vector<std::uint16_t> vlan_ids; // outer to inner; usually 0 or 1, 2 for QinQ + std::uint16_t ethertype; // the real ethertype once every tag is stripped + std::span<const unsigned char> payload; +}; + +// A VLAN-tagged frame inserts a 4-byte tag (2-byte TPID + 2-byte TCI, +// the low 12 bits of which are the VLAN ID) right where the ethertype +// field would otherwise be, pushing the real ethertype 4 bytes deeper +// - parse_ethernet() alone has no way to know this, since it always +// reads a fixed 14-byte header and hands back whatever follows as +// "payload" regardless of whether that's really IP/ARP or another +// tag. Composable and separate from parse_ethernet() the same way +// walk_ipv6_extension_headers() is separate from parse_ipv6(): the +// base parse stays an unconditional fixed-header decode, and this +// walk is what a caller uses when it actually needs the protocol +// underneath. Bounded at 4 tags so a corrupt/hostile frame claiming +// tag-of-a-tag-of-a-tag indefinitely can't spin - real QinQ stacks +// are 2 deep at most. +inline VlanWalkResult walk_vlan_tags(std::uint16_t ethertype, + std::span<const unsigned char> payload) { + VlanWalkResult result{{}, ethertype, payload}; + constexpr int kMaxTags = 4; + for (int i = 0; i < kMaxTags; ++i) { + if (result.ethertype != kEthertypeVlan && result.ethertype != kEthertypeVlanQinQ) break; + if (result.payload.size() < 4) break; // truncated tag + std::uint16_t tci = read_be16(result.payload, 0); + result.vlan_ids.push_back(tci & 0x0FFF); + result.ethertype = read_be16(result.payload, 2); + result.payload = result.payload.subspan(4); + } + return result; +} + } // namespace packeteer::net diff --git a/include/packeteer/summarize.hpp b/include/packeteer/summarize.hpp index 4143e44..302b380 100644 --- a/include/packeteer/summarize.hpp +++ b/include/packeteer/summarize.hpp @@ -220,16 +220,31 @@ inline std::string summarize_packet(std::span<const unsigned char> bytes, int da std::snprintf(eth_buf, sizeof(eth_buf), " ethertype=0x%04x", eth->header.ethertype); out += eth_buf; - if (eth->header.ethertype == net::kEthertypeArp) { - if (auto arp = net::parse_arp(eth->payload)) out += " | " + arp_summary(*arp); + // Unwraps any 802.1Q/802.1ad VLAN tags between the Ethernet + // header and the real protocol - without this, every tagged + // frame's actual ethertype reads as 0x8100 and everything + // below (ARP/IPv4/IPv6 and everything built on them) would be + // invisible on any VLAN-tagged network. The line still shows + // the literal on-the-wire outer ethertype above; dispatch from + // here on uses the walked (real) one. + auto vlan = net::walk_vlan_tags(eth->header.ethertype, eth->payload); + if (!vlan.vlan_ids.empty()) { + out += " vlan="; + for (std::size_t i = 0; i < vlan.vlan_ids.size(); ++i) { + if (i > 0) out += ","; + out += std::to_string(vlan.vlan_ids[i]); + } + } + + if (vlan.ethertype == net::kEthertypeArp) { + if (auto arp = net::parse_arp(vlan.payload)) out += " | " + arp_summary(*arp); return out; } - if (eth->header.ethertype != net::kEthertypeIPv4 && - eth->header.ethertype != net::kEthertypeIPv6) { + if (vlan.ethertype != net::kEthertypeIPv4 && vlan.ethertype != net::kEthertypeIPv6) { return out; } - ip_bytes = eth->payload; + ip_bytes = vlan.payload; } if (ip_bytes.empty()) { |