diff options
| author | srdusr <[email protected]> | 2026-05-31 23:29:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-05-31 23:29:00 +0200 |
| commit | 8c8708e43aeae394787d0d1aa71ee22dca635bbe (patch) | |
| tree | d1e168d081ecf52fd9a62802a214d4263824331b /tests | |
| parent | 719b7f439c1c8c76d0573b34daa329061c9ad8a6 (diff) | |
| download | packeteer-8c8708e43aeae394787d0d1aa71ee22dca635bbe.tar.gz packeteer-8c8708e43aeae394787d0d1aa71ee22dca635bbe.zip | |
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.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test_lldp.cpp | 98 | ||||
| -rw-r--r-- | tests/test_summarize.cpp | 24 |
2 files changed, 120 insertions, 2 deletions
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") { |