From 8c8708e43aeae394787d0d1aa71ee22dca635bbe Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Sun, 31 May 2026 23:29:00 +0200 Subject: Add LLDP - dispatched by ethertype, no IP layer at all Real switches broadcast this every ~30s, but this project had zero treatment for it (0x88CC was previously the test suite's own example of an "unhandled ethertype"). Dispatched by ethertype the same way ARP is, since LLDP sits directly on Ethernet. TLV-encoded; only the three mandatory TLVs (Chassis ID, Port ID, TTL) plus System Name are rendered, while every other TLV is still walked over correctly so nothing after it is lost. Live-verified two ways, since this machine is on WiFi (LLDP isn't relayed to wireless clients even when a real switch sends it) with no LLDP daemon installed to generate traffic locally either: a 15-second passive capture confirmed no organic LLDP traffic exists to accidentally rely on, then a real 802.1AB frame was sent via a raw AF_PACKET socket onto the actual NIC (not fed directly to parse_lldp() in a unit test) and captured through the full pipeline, decoding correctly. --- include/packeteer/net/lldp.hpp | 92 +++++++++++++++++++++++++++++++++++++++++ include/packeteer/summarize.hpp | 6 +++ 2 files changed, 98 insertions(+) create mode 100644 include/packeteer/net/lldp.hpp (limited to 'include') diff --git a/include/packeteer/net/lldp.hpp b/include/packeteer/net/lldp.hpp new file mode 100644 index 0000000..ccf9f13 --- /dev/null +++ b/include/packeteer/net/lldp.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "packeteer/byteio.hpp" + +// IEEE 802.1AB LLDP. Sent directly on Ethernet (ethertype 0x88CC), no +// IP layer at all - the same reasoning ARP is dispatched by ethertype +// in summarize.hpp rather than through anything IP-based. TLV-encoded: +// each TLV is a 2-byte header (7-bit type, 9-bit length) followed by +// that many bytes of value, terminated by an End of LLDPDU TLV (type +// 0). Only the three mandatory TLVs (Chassis ID, Port ID, TTL) plus +// System Name - usually the single most useful, human-readable field +// in the whole frame - are decoded; the rest (Port/System +// Description, Capabilities, Management Address, and any +// organizationally-specific TLVs) are walked over correctly (so +// nothing after them is missed) but not rendered. +namespace packeteer::net { + +inline constexpr std::uint16_t kEthertypeLldp = 0x88CC; + +struct LldpInfo { + std::optional chassis_id; + std::optional port_id; + std::optional ttl; + std::optional system_name; +}; + +// Chassis ID and Port ID share the same subtype+value shape. Subtype 4 +// (MAC address) is rendered as hex-colon; everything else (interface +// name/alias, component, locally-assigned string, etc.) is treated as +// ASCII text - true for every subtype except "network address" +// (subtype 5 for Chassis ID), which has its own AFI-prefixed encoding +// this doesn't attempt to decode specially and would render as +// mangled text instead. Uncommon enough in practice not to be worth a +// separate code path for a one-line summary. +inline std::string format_lldp_id(std::uint8_t subtype, std::span value) { + if (subtype == 4 && value.size() == 6) { + char buf[18]; + std::snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x", value[0], value[1], + value[2], value[3], value[4], value[5]); + return buf; + } + return std::string(reinterpret_cast(value.data()), value.size()); +} + +inline std::optional parse_lldp(std::span bytes) { + LldpInfo info{}; + std::size_t pos = 0; + constexpr int kMaxTlvs = 32; // real LLDPDUs rarely carry more than a handful + + for (int i = 0; i < kMaxTlvs; ++i) { + if (pos + 2 > bytes.size()) break; + std::uint16_t tlv_header = read_be16(bytes, pos); + std::uint8_t type = static_cast(tlv_header >> 9); + std::uint16_t length = tlv_header & 0x01FF; + pos += 2; + if (pos + length > bytes.size()) break; // truncated: stop, keep what was decoded + std::span value = bytes.subspan(pos, length); + + if (type == 0) break; // End of LLDPDU + if (type == 1 && length >= 1) { + info.chassis_id = format_lldp_id(value[0], value.subspan(1)); + } else if (type == 2 && length >= 1) { + info.port_id = format_lldp_id(value[0], value.subspan(1)); + } else if (type == 3 && length == 2) { + info.ttl = read_be16(value, 0); + } else if (type == 5 && length >= 1) { + info.system_name = std::string(reinterpret_cast(value.data()), value.size()); + } + + pos += length; + } + + // The three mandatory TLVs (802.1AB 9.2) - anything missing one + // of these isn't really a well-formed LLDPDU. + if (!info.chassis_id || !info.port_id || !info.ttl) return std::nullopt; + return info; +} + +inline std::string lldp_summary(const LldpInfo& info) { + std::string out = "LLDP chassis=" + *info.chassis_id + " port=" + *info.port_id + + " ttl=" + std::to_string(*info.ttl); + if (info.system_name) out += " name=" + *info.system_name; + return out; +} + +} // namespace packeteer::net diff --git a/include/packeteer/summarize.hpp b/include/packeteer/summarize.hpp index 4bb2d24..c261083 100644 --- a/include/packeteer/summarize.hpp +++ b/include/packeteer/summarize.hpp @@ -28,6 +28,7 @@ #include "packeteer/net/igmp.hpp" #include "packeteer/net/ipv4.hpp" #include "packeteer/net/ipv6.hpp" +#include "packeteer/net/lldp.hpp" #include "packeteer/net/rtcp.hpp" #include "packeteer/net/rtp.hpp" #include "packeteer/net/tcp.hpp" @@ -301,6 +302,11 @@ inline std::string summarize_packet(std::span bytes, int da return out; } + if (vlan.ethertype == net::kEthertypeLldp) { + if (auto lldp = net::parse_lldp(vlan.payload)) out += " | " + net::lldp_summary(*lldp); + return out; + } + if (vlan.ethertype != net::kEthertypeIPv4 && vlan.ethertype != net::kEthertypeIPv6) { return out; } -- cgit v1.2.3