srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--PLAN.md23
-rw-r--r--include/packeteer/net/lldp.hpp92
-rw-r--r--include/packeteer/summarize.hpp6
-rw-r--r--tests/test_lldp.cpp98
-rw-r--r--tests/test_summarize.cpp24
6 files changed, 242 insertions, 2 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 20fc70e..3588095 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -99,6 +99,7 @@ add_executable(packeteer_tests
tests/test_net.cpp
tests/test_arp.cpp
tests/test_igmp.cpp
+ tests/test_lldp.cpp
tests/test_rtcp.cpp
tests/test_rtp.cpp
tests/test_ipv6.cpp
diff --git a/PLAN.md b/PLAN.md
index 64b49e3..f63e477 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -727,3 +727,26 @@ None currently open.
triggered the fragment-id bug above) but not live-verified: this
machine has no real global IPv6 connectivity to generate genuine
IPv6-fragmented traffic against, only link-local addresses.
+- LLDP (net/lldp.hpp), dispatched by ethertype (0x88CC) the same way
+ ARP is - no IP layer at all. TLV-encoded (7-bit type + 9-bit length
+ packed into each TLV's 2-byte header); only the three mandatory TLVs
+ (Chassis ID, Port ID, TTL) plus System Name - usually the single
+ most human-readable field in the whole frame - are rendered, while
+ every other TLV type is still walked over correctly so nothing after
+ it is lost. Chassis/Port ID's MAC-address subtype renders as
+ hex-colon; every other subtype (interface name, locally-assigned
+ string, etc.) as plain text, except "network address" (its own
+ AFI-prefixed encoding, not decoded specially - uncommon enough in
+ practice not to be worth a separate path).
+ Live-verified two ways given this machine is on WiFi, where LLDP
+ isn't relayed to wireless clients even when a real switch upstream
+ sends it, and no LLDP daemon (lldpd et al.) is installed here to
+ generate real Linux-side traffic either: first, a 15-second passive
+ capture confirmed no organic LLDP traffic exists on this network
+ segment to accidentally rely on; then a real, wire-format-correct
+ 802.1AB frame was sent via a raw AF_PACKET socket onto the actual
+ wlp1s0 NIC (not fed directly to parse_lldp() in a unit test) and
+ captured through the full real pipeline - libpcap capture,
+ Ethernet decode, TLV walk - correctly decoding "LLDP
+ chassis=de:ad:be:ef:00:01 port=eth0 ttl=120 name=packeteer-test-host",
+ an exact match for what was actually sent.
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 <cstdint>
+#include <cstdio>
+#include <optional>
+#include <span>
+#include <string>
+
+#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<std::string> chassis_id;
+ std::optional<std::string> port_id;
+ std::optional<std::uint16_t> ttl;
+ std::optional<std::string> 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<const unsigned char> 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<const char*>(value.data()), value.size());
+}
+
+inline std::optional<LldpInfo> parse_lldp(std::span<const unsigned char> 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<std::uint8_t>(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<const unsigned char> 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<const char*>(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<const unsigned char> 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;
}
diff --git a/tests/test_lldp.cpp b/tests/test_lldp.cpp
new file mode 100644
index 0000000..3d48970
--- /dev/null
+++ b/tests/test_lldp.cpp
@@ -0,0 +1,98 @@
+#include <doctest/doctest.h>
+
+#include <vector>
+
+#include "packeteer/net/lldp.hpp"
+
+using namespace packeteer::net;
+
+namespace {
+
+std::vector<unsigned char> tlv(std::uint8_t type, const std::vector<unsigned char>& value) {
+ std::uint16_t header = static_cast<std::uint16_t>((type << 9) | value.size());
+ std::vector<unsigned char> out = {static_cast<unsigned char>(header >> 8),
+ static_cast<unsigned char>(header & 0xFF)};
+ out.insert(out.end(), value.begin(), value.end());
+ return out;
+}
+
+std::vector<unsigned char> lldpdu(std::optional<std::vector<unsigned char>> extra_tlv = {}) {
+ std::vector<unsigned char> bytes;
+ auto chassis_id = tlv(1, {4, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}); // subtype 4: MAC
+ auto port_id = tlv(2, {7, 'e', 't', 'h', '0'}); // subtype 7: locally assigned
+ auto ttl = tlv(3, {0x00, 0x78}); // 120 seconds
+ bytes.insert(bytes.end(), chassis_id.begin(), chassis_id.end());
+ bytes.insert(bytes.end(), port_id.begin(), port_id.end());
+ bytes.insert(bytes.end(), ttl.begin(), ttl.end());
+ if (extra_tlv) bytes.insert(bytes.end(), extra_tlv->begin(), extra_tlv->end());
+ auto end = tlv(0, {});
+ bytes.insert(bytes.end(), end.begin(), end.end());
+ return bytes;
+}
+
+} // namespace
+
+TEST_CASE("parse_lldp decodes the three mandatory TLVs") {
+ auto msg = parse_lldp(lldpdu());
+ REQUIRE(msg.has_value());
+ REQUIRE(msg->chassis_id.has_value());
+ CHECK(*msg->chassis_id == "aa:bb:cc:dd:ee:ff");
+ REQUIRE(msg->port_id.has_value());
+ CHECK(*msg->port_id == "eth0");
+ REQUIRE(msg->ttl.has_value());
+ CHECK(*msg->ttl == 120);
+ CHECK_FALSE(msg->system_name.has_value());
+}
+
+TEST_CASE("parse_lldp decodes the System Name TLV when present") {
+ auto msg = parse_lldp(lldpdu(tlv(5, {'s', 'w', 'i', 't', 'c', 'h', '1'})));
+ REQUIRE(msg.has_value());
+ REQUIRE(msg->system_name.has_value());
+ CHECK(*msg->system_name == "switch1");
+}
+
+TEST_CASE("parse_lldp skips over an unrecognized TLV without losing later ones") {
+ // Type 6 (System Description) isn't decoded, but must not break
+ // parsing of the End TLV that follows it.
+ auto msg = parse_lldp(lldpdu(tlv(6, {'d', 'e', 's', 'c'})));
+ REQUIRE(msg.has_value());
+ REQUIRE(msg->chassis_id.has_value());
+ REQUIRE(msg->port_id.has_value());
+ REQUIRE(msg->ttl.has_value());
+}
+
+TEST_CASE("parse_lldp rejects a message missing a mandatory TLV") {
+ // Chassis ID and Port ID only, no TTL - not a well-formed LLDPDU.
+ std::vector<unsigned char> bytes;
+ auto chassis_id = tlv(1, {4, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF});
+ auto port_id = tlv(2, {7, 'e', 't', 'h', '0'});
+ bytes.insert(bytes.end(), chassis_id.begin(), chassis_id.end());
+ bytes.insert(bytes.end(), port_id.begin(), port_id.end());
+ auto end = tlv(0, {});
+ bytes.insert(bytes.end(), end.begin(), end.end());
+
+ CHECK_FALSE(parse_lldp(bytes).has_value());
+}
+
+TEST_CASE("parse_lldp rejects an empty buffer") {
+ std::vector<unsigned char> bytes;
+ CHECK_FALSE(parse_lldp(bytes).has_value());
+}
+
+TEST_CASE("format_lldp_id renders a MAC-address subtype as hex-colon") {
+ std::vector<unsigned char> mac = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
+ CHECK(format_lldp_id(4, mac) == "aa:bb:cc:dd:ee:ff");
+}
+
+TEST_CASE("format_lldp_id renders a non-MAC subtype as plain text") {
+ std::vector<unsigned char> text = {'e', 't', 'h', '0'};
+ CHECK(format_lldp_id(7, text) == "eth0");
+}
+
+TEST_CASE("lldp_summary formats chassis/port/ttl, and name only when present") {
+ LldpInfo info{"aa:bb:cc:dd:ee:ff", "eth0", 120, std::nullopt};
+ CHECK(lldp_summary(info) == "LLDP chassis=aa:bb:cc:dd:ee:ff port=eth0 ttl=120");
+
+ info.system_name = "switch1";
+ CHECK(lldp_summary(info) == "LLDP chassis=aa:bb:cc:dd:ee:ff port=eth0 ttl=120 name=switch1");
+}
diff --git a/tests/test_summarize.cpp b/tests/test_summarize.cpp
index baf4ed4..0cb0f98 100644
--- a/tests/test_summarize.cpp
+++ b/tests/test_summarize.cpp
@@ -166,10 +166,30 @@ TEST_CASE("summarize_packet reports a truncated Ethernet frame without decoding
TEST_CASE("summarize_packet stops after the Ethernet line for an unhandled ethertype") {
std::vector<unsigned char> bytes = {
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
- 0x88, 0xCC, // LLDP, not IPv4/IPv6/ARP
+ 0x80, 0x35, // RARP - real ethertype, just not one this project decodes
};
auto line = packeteer::summarize_packet(bytes, DLT_EN10MB);
- CHECK(line == "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x88cc");
+ CHECK(line == "ETH aa:bb:cc:dd:ee:ff -> 11:22:33:44:55:66 ethertype=0x8035");
+}
+
+TEST_CASE("summarize_packet decodes an LLDP frame end to end") {
+ std::vector<unsigned char> bytes = {
+ 0x01, 0x80, 0xC2, 0x00, 0x00, 0x0E, // dst: LLDP multicast
+ 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, // src mac
+ 0x88, 0xCC, // ethertype: LLDP
+ // Chassis ID TLV: subtype=4 (MAC), value=aa:bb:cc:dd:ee:ff
+ 0x02, 0x07, 4, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
+ // Port ID TLV: subtype=7 (locally assigned), value="eth0"
+ 0x04, 0x05, 7, 'e', 't', 'h', '0',
+ // TTL TLV: 120 seconds
+ 0x06, 0x02, 0x00, 0x78,
+ // End of LLDPDU
+ 0x00, 0x00,
+ };
+ auto line = packeteer::summarize_packet(bytes, DLT_EN10MB);
+ CHECK(line ==
+ "ETH aa:bb:cc:dd:ee:ff -> 01:80:c2:00:00:0e ethertype=0x88cc | "
+ "LLDP chassis=aa:bb:cc:dd:ee:ff port=eth0 ttl=120");
}
TEST_CASE("summarize_packet decodes an ARP request end to end") {