summaryrefslogtreecommitdiff
path: root/src/libnm-lldp
diff options
context:
space:
mode:
authorThomas Haller <thaller@redhat.com>2022-09-06 13:33:18 +0200
committerThomas Haller <thaller@redhat.com>2022-10-25 10:59:00 +0200
commit630de288d2e4e01d9ed89218722c0f52b2173128 (patch)
treea30287300802a7ee91766460b655e31ab9cf0c02 /src/libnm-lldp
parent8506865345b8ae9c1772f2cfe2f679eef7a87cba (diff)
downloadNetworkManager-630de288d2e4e01d9ed89218722c0f52b2173128.tar.gz
lldp: add libnm-lldp as fork of systemd's sd_lldp_rx
We currently use the systemd LLDP client, which we consume by forking systemd code. That is a maintenance burden, because it's not a self-contained, stable library that we use. Hence there is a need for an individual library or properly integrating the fork in our tree. Optimally, we would create a new nettools project with an LLDP library. That was not done because: - nettools may want to be dual licensed with LGPL-2.1+ and Apache. Systemd code is LGPL-2.1+ so it is fine for NetworkManager but possibly not for nettools. - nettools provides independent librares, as such they don't have an event loop, instead they expose an epoll file descriptor and the user needs to integrate it. Systemd and NetworkManager on the other hand have their established event loop (sd_event and GMainContext, respectively). It's simpler to implement the library on those terms, in particular porting the systemd library from sd_event to GMainContext. - NetworkManager uses glib and has various helper utils. While it's possible to do without them, it's more work. The main reason to not write a new NetworkManager-agnostic library from scratch, is that it's much simpler to fork the systemd library and make it part of NetworkManager, than making it a nettools library. Do it.
Diffstat (limited to 'src/libnm-lldp')
-rw-r--r--src/libnm-lldp/meson.build18
-rw-r--r--src/libnm-lldp/nm-lldp-neighbor.c842
-rw-r--r--src/libnm-lldp/nm-lldp-neighbor.h85
-rw-r--r--src/libnm-lldp/nm-lldp-network.c74
-rw-r--r--src/libnm-lldp/nm-lldp-network.h9
-rw-r--r--src/libnm-lldp/nm-lldp-rx-internal.h55
-rw-r--r--src/libnm-lldp/nm-lldp-rx.c469
-rw-r--r--src/libnm-lldp/nm-lldp-rx.h124
-rw-r--r--src/libnm-lldp/nm-lldp.h110
9 files changed, 1786 insertions, 0 deletions
diff --git a/src/libnm-lldp/meson.build b/src/libnm-lldp/meson.build
new file mode 100644
index 0000000000..0d2065e8ba
--- /dev/null
+++ b/src/libnm-lldp/meson.build
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+libnm_lldp = static_library(
+ 'nm-lldp',
+ sources: [
+ 'nm-lldp-neighbor.c',
+ 'nm-lldp-network.c',
+ 'nm-lldp-rx.c',
+ ],
+ include_directories: [
+ src_inc,
+ top_inc,
+ ],
+ dependencies: [
+ glib_dep,
+ libudev_dep,
+ ],
+)
diff --git a/src/libnm-lldp/nm-lldp-neighbor.c b/src/libnm-lldp/nm-lldp-neighbor.c
new file mode 100644
index 0000000000..f1e2d42eb0
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-neighbor.c
@@ -0,0 +1,842 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-lldp-neighbor.h"
+
+#include <net/ethernet.h>
+
+#include "libnm-std-aux/unaligned.h"
+#include "libnm-glib-aux/nm-time-utils.h"
+#include "nm-lldp-network.h"
+#include "nm-lldp.h"
+#include "nm-lldp-rx-internal.h"
+
+/*****************************************************************************/
+
+guint
+nm_lldp_neighbor_id_hash(const NMLldpNeighborID *id)
+{
+ NMHashState h;
+
+ nm_assert(id);
+
+ nm_hash_init(&h, 1925469911u);
+ nm_hash_update_mem(&h, id->chassis_id, id->chassis_id_size);
+ nm_hash_update_mem(&h, id->port_id, id->port_id_size);
+ return nm_hash_complete(&h);
+}
+
+int
+nm_lldp_neighbor_id_cmp(const NMLldpNeighborID *x, const NMLldpNeighborID *y)
+{
+ nm_assert(x);
+ nm_assert(y);
+
+ NM_CMP_SELF(x, y);
+ NM_CMP_RETURN_DIRECT(
+ nm_memcmp_n(x->chassis_id, x->chassis_id_size, y->chassis_id, y->chassis_id_size, 1));
+ NM_CMP_RETURN_DIRECT(nm_memcmp_n(x->port_id, x->port_id_size, y->port_id, y->port_id_size, 1));
+ return 0;
+}
+
+gboolean
+nm_lldp_neighbor_id_equal(const NMLldpNeighborID *x, const NMLldpNeighborID *y)
+{
+ return nm_lldp_neighbor_id_cmp(x, y) == 0;
+}
+
+int
+nm_lldp_neighbor_prioq_compare_func(const void *a, const void *b)
+{
+ const NMLldpNeighbor *x = a;
+ const NMLldpNeighbor *y = b;
+
+ nm_assert(x);
+ nm_assert(y);
+
+ NM_CMP_FIELD(x, y, until_usec);
+ return 0;
+}
+
+static int
+parse_string(NMLldpRX *lldp_rx, char **s, const void *q, size_t n)
+{
+ const char *p = q;
+ char *k;
+
+ nm_assert(s);
+ nm_assert(p || n == 0);
+
+ if (*s) {
+ _LOG2D(lldp_rx, "Found duplicate string, ignoring field.");
+ return 0;
+ }
+
+ /* Strip trailing NULs, just to be nice */
+ while (n > 0 && p[n - 1] == 0)
+ n--;
+
+ if (n <= 0) /* Ignore empty strings */
+ return 0;
+
+ /* Look for inner NULs */
+ if (memchr(p, 0, n)) {
+ _LOG2D(lldp_rx, "Found inner NUL in string, ignoring field.");
+ return 0;
+ }
+
+ /* Let's escape weird chars, for security reasons */
+ k = nm_utils_buf_utf8safe_escape_cp(p,
+ n,
+ NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
+ | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII);
+
+ g_free(*s);
+ *s = k;
+
+ return 1;
+}
+
+int
+nm_lldp_neighbor_parse(NMLldpNeighbor *n)
+{
+ struct ether_header h;
+ const uint8_t *p;
+ size_t left;
+ int r;
+
+ nm_assert(n);
+
+ if (n->raw_size < sizeof(struct ether_header)) {
+ _LOG2D(n->lldp_rx, "Received truncated packet, ignoring.");
+ return -NME_UNSPEC;
+ }
+
+ memcpy(&h, NM_LLDP_NEIGHBOR_RAW(n), sizeof(h));
+
+ if (h.ether_type != htobe16(NM_ETHERTYPE_LLDP)) {
+ _LOG2D(n->lldp_rx, "Received packet with wrong type, ignoring.");
+ return -NME_UNSPEC;
+ }
+
+ if (h.ether_dhost[0] != 0x01 || h.ether_dhost[1] != 0x80 || h.ether_dhost[2] != 0xc2
+ || h.ether_dhost[3] != 0x00 || h.ether_dhost[4] != 0x00
+ || !NM_IN_SET(h.ether_dhost[5], 0x00, 0x03, 0x0e)) {
+ _LOG2D(n->lldp_rx, "Received packet with wrong destination address, ignoring.");
+ return -NME_UNSPEC;
+ }
+
+ memcpy(&n->source_address, h.ether_shost, sizeof(NMEtherAddr));
+ memcpy(&n->destination_address, h.ether_dhost, sizeof(NMEtherAddr));
+
+ p = (const uint8_t *) NM_LLDP_NEIGHBOR_RAW(n) + sizeof(struct ether_header);
+ left = n->raw_size - sizeof(struct ether_header);
+
+ for (;;) {
+ uint8_t type;
+ uint16_t length;
+
+ if (left < 2) {
+ _LOG2D(n->lldp_rx, "TLV lacks header, ignoring.");
+ return -NME_UNSPEC;
+ }
+
+ type = p[0] >> 1;
+ length = p[1] + (((uint16_t) (p[0] & 1)) << 8);
+ p += 2, left -= 2;
+
+ if (left < length) {
+ _LOG2D(n->lldp_rx, "TLV truncated, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ switch (type) {
+ case NM_LLDP_TYPE_END:
+ if (length != 0) {
+ _LOG2D(n->lldp_rx, "End marker TLV not zero-sized, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ /* Note that after processing the NM_LLDP_TYPE_END left could still be > 0
+ * as the message may contain padding (see IEEE 802.1AB-2016, sec. 8.5.12) */
+
+ goto end_marker;
+
+ case NM_LLDP_TYPE_CHASSIS_ID:
+ if (length < 2 || length > 256) {
+ /* includes the chassis subtype, hence one extra byte */
+ _LOG2D(n->lldp_rx, "Chassis ID field size out of range, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ if (n->id.chassis_id) {
+ _LOG2D(n->lldp_rx, "Duplicate chassis ID field, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ n->id.chassis_id = nm_memdup(p, length);
+ n->id.chassis_id_size = length;
+ break;
+
+ case NM_LLDP_TYPE_PORT_ID:
+ if (length < 2 || length > 256) {
+ /* includes the port subtype, hence one extra byte */
+ _LOG2D(n->lldp_rx, "Port ID field size out of range, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ if (n->id.port_id) {
+ _LOG2D(n->lldp_rx, "Duplicate port ID field, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ n->id.port_id = nm_memdup(p, length);
+ n->id.port_id_size = length;
+ break;
+
+ case NM_LLDP_TYPE_TTL:
+ if (length != 2) {
+ _LOG2D(n->lldp_rx, "TTL field has wrong size, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ if (n->has_ttl) {
+ _LOG2D(n->lldp_rx, "Duplicate TTL field, ignoring datagram.");
+ return -NME_UNSPEC;
+ }
+
+ n->ttl = unaligned_read_be16(p);
+ n->has_ttl = true;
+ break;
+
+ case NM_LLDP_TYPE_PORT_DESCRIPTION:
+ r = parse_string(n->lldp_rx, &n->port_description, p, length);
+ if (r < 0)
+ return r;
+ break;
+
+ case NM_LLDP_TYPE_SYSTEM_NAME:
+ r = parse_string(n->lldp_rx, &n->system_name, p, length);
+ if (r < 0)
+ return r;
+ break;
+
+ case NM_LLDP_TYPE_SYSTEM_DESCRIPTION:
+ r = parse_string(n->lldp_rx, &n->system_description, p, length);
+ if (r < 0)
+ return r;
+ break;
+
+ case NM_LLDP_TYPE_SYSTEM_CAPABILITIES:
+ if (length != 4) {
+ _LOG2D(n->lldp_rx, "System capabilities field has wrong size.");
+ return -NME_UNSPEC;
+ }
+
+ n->system_capabilities = unaligned_read_be16(p);
+ n->enabled_capabilities = unaligned_read_be16(p + 2);
+ n->has_capabilities = true;
+ break;
+
+ case NM_LLDP_TYPE_PRIVATE:
+ if (length < 4) {
+ _LOG2D(n->lldp_rx, "Found private TLV that is too short, ignoring.");
+ return -NME_UNSPEC;
+ }
+
+ /* RFC 8520: MUD URL */
+ if (memcmp(p, NM_LLDP_OUI_IANA_MUD, sizeof(NM_LLDP_OUI_IANA_MUD)) == 0) {
+ r = parse_string(n->lldp_rx,
+ &n->mud_url,
+ p + sizeof(NM_LLDP_OUI_IANA_MUD),
+ length - sizeof(NM_LLDP_OUI_IANA_MUD));
+ if (r < 0)
+ return r;
+ }
+ break;
+ }
+
+ p += length, left -= length;
+ }
+
+end_marker:
+ if (!n->id.chassis_id || !n->id.port_id || !n->has_ttl) {
+ _LOG2D(n->lldp_rx, "One or more mandatory TLV missing in datagram. Ignoring.");
+ return -NME_UNSPEC;
+ }
+
+ n->rindex = sizeof(struct ether_header);
+
+ return 0;
+}
+
+void
+nm_lldp_neighbor_start_ttl(NMLldpNeighbor *n)
+{
+ nm_assert(n);
+
+ if (n->ttl > 0) {
+ /* Use the packet's timestamp if there is one known */
+ if (n->timestamp_usec <= 0) {
+ /* Otherwise, take the current time */
+ n->timestamp_usec = nm_utils_get_monotonic_timestamp_usec();
+ }
+
+ n->until_usec = n->timestamp_usec + (n->ttl * NM_UTILS_USEC_PER_SEC);
+ } else
+ n->until_usec = 0;
+
+ if (n->lldp_rx)
+ nm_prioq_reshuffle(&n->lldp_rx->neighbor_by_expiry, n, &n->prioq_idx);
+}
+
+int
+nm_lldp_neighbor_cmp(const NMLldpNeighbor *a, const NMLldpNeighbor *b)
+{
+ NM_CMP_SELF(a, b);
+ NM_CMP_FIELD(a, b, raw_size);
+ NM_CMP_DIRECT_MEMCMP(NM_LLDP_NEIGHBOR_RAW(a), NM_LLDP_NEIGHBOR_RAW(b), a->raw_size);
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_source_address(NMLldpNeighbor *n, NMEtherAddr *address)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(address, -EINVAL);
+
+ *address = n->source_address;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_destination_address(NMLldpNeighbor *n, NMEtherAddr *address)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(address, -EINVAL);
+
+ *address = n->destination_address;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_raw(NMLldpNeighbor *n, const void **ret, size_t *size)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+ g_return_val_if_fail(size, -EINVAL);
+
+ *ret = NM_LLDP_NEIGHBOR_RAW(n);
+ *size = n->raw_size;
+
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_chassis_id(NMLldpNeighbor *n, uint8_t *type, const void **ret, size_t *size)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(type, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+ g_return_val_if_fail(size, -EINVAL);
+
+ nm_assert(n->id.chassis_id_size > 0);
+
+ *type = *(uint8_t *) n->id.chassis_id;
+ *ret = (uint8_t *) n->id.chassis_id + 1;
+ *size = n->id.chassis_id_size - 1;
+
+ return 0;
+}
+
+static char *
+format_mac_address(const void *data, size_t sz)
+{
+ NMEtherAddr a;
+
+ nm_assert(data || sz <= 0);
+
+ if (sz != 7)
+ return NULL;
+
+ memcpy(&a, (uint8_t *) data + 1, sizeof(a));
+ return nm_ether_addr_to_string_dup(&a);
+}
+
+static char *
+format_network_address(const void *data, size_t sz)
+{
+ int addr_family;
+ NMIPAddr a;
+
+ if (sz == 6 && ((uint8_t *) data)[1] == 1) {
+ memcpy(&a.addr4, (uint8_t *) data + 2, sizeof(a.addr4));
+ addr_family = AF_INET;
+ } else if (sz == 18 && ((uint8_t *) data)[1] == 2) {
+ memcpy(&a.addr6, (uint8_t *) data + 2, sizeof(a.addr6));
+ addr_family = AF_INET6;
+ } else
+ return NULL;
+
+ return nm_inet_ntop_dup(addr_family, &a);
+}
+
+const char *
+nm_lldp_neighbor_get_chassis_id_as_string(NMLldpNeighbor *n)
+{
+ char *k;
+
+ g_return_val_if_fail(n, NULL);
+
+ if (n->chassis_id_as_string)
+ return n->chassis_id_as_string;
+
+ nm_assert(n->id.chassis_id_size > 0);
+
+ switch (*(uint8_t *) n->id.chassis_id) {
+ case NM_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT:
+ case NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS:
+ case NM_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT:
+ case NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME:
+ case NM_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED:
+ k = nm_utils_buf_utf8safe_escape_cp((char *) n->id.chassis_id + 1,
+ n->id.chassis_id_size - 1,
+ NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
+ | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII);
+ goto done;
+
+ case NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS:
+ k = format_mac_address(n->id.chassis_id, n->id.chassis_id_size);
+ if (k)
+ goto done;
+ break;
+
+ case NM_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS:
+ k = format_network_address(n->id.chassis_id, n->id.chassis_id_size);
+ if (k)
+ goto done;
+ break;
+ }
+
+ /* Generic fallback */
+ k = nm_utils_bin2hexstr_full(n->id.chassis_id, n->id.chassis_id_size, '\0', FALSE, NULL);
+
+done:
+ nm_assert(k);
+ return (n->chassis_id_as_string = k);
+}
+
+int
+nm_lldp_neighbor_get_port_id(NMLldpNeighbor *n, uint8_t *type, const void **ret, size_t *size)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(type, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+ g_return_val_if_fail(size, -EINVAL);
+
+ nm_assert(n->id.port_id_size > 0);
+
+ *type = *(uint8_t *) n->id.port_id;
+ *ret = (uint8_t *) n->id.port_id + 1;
+ *size = n->id.port_id_size - 1;
+
+ return 0;
+}
+
+const char *
+nm_lldp_neighbor_get_port_id_as_string(NMLldpNeighbor *n)
+{
+ char *k;
+
+ g_return_val_if_fail(n, NULL);
+
+ if (n->port_id_as_string)
+ return n->port_id_as_string;
+
+ nm_assert(n->id.port_id_size > 0);
+
+ switch (*(uint8_t *) n->id.port_id) {
+ case NM_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS:
+ case NM_LLDP_PORT_SUBTYPE_PORT_COMPONENT:
+ case NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME:
+ case NM_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED:
+ k = nm_utils_buf_utf8safe_escape_cp((char *) n->id.port_id + 1,
+ n->id.port_id_size - 1,
+ NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
+ | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII);
+ goto done;
+
+ case NM_LLDP_PORT_SUBTYPE_MAC_ADDRESS:
+ k = format_mac_address(n->id.port_id, n->id.port_id_size);
+ if (k)
+ goto done;
+ break;
+
+ case NM_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS:
+ k = format_network_address(n->id.port_id, n->id.port_id_size);
+ if (k)
+ goto done;
+ break;
+ }
+
+ /* Generic fallback */
+ k = nm_utils_bin2hexstr_full(n->id.port_id, n->id.port_id_size, '\0', FALSE, NULL);
+
+done:
+ nm_assert(k);
+ return (n->port_id_as_string = k);
+}
+
+int
+nm_lldp_neighbor_get_ttl(NMLldpNeighbor *n, uint16_t *ret_sec)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret_sec, -EINVAL);
+
+ *ret_sec = n->ttl;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_system_name(NMLldpNeighbor *n, const char **ret)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+
+ if (!n->system_name)
+ return -ENODATA;
+
+ *ret = n->system_name;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_system_description(NMLldpNeighbor *n, const char **ret)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+
+ if (!n->system_description)
+ return -ENODATA;
+
+ *ret = n->system_description;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_port_description(NMLldpNeighbor *n, const char **ret)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+
+ if (!n->port_description)
+ return -ENODATA;
+
+ *ret = n->port_description;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_mud_url(NMLldpNeighbor *n, const char **ret)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+
+ if (!n->mud_url)
+ return -ENODATA;
+
+ *ret = n->mud_url;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_system_capabilities(NMLldpNeighbor *n, uint16_t *ret)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+
+ if (!n->has_capabilities)
+ return -ENODATA;
+
+ *ret = n->system_capabilities;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_enabled_capabilities(NMLldpNeighbor *n, uint16_t *ret)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+
+ if (!n->has_capabilities)
+ return -ENODATA;
+
+ *ret = n->enabled_capabilities;
+ return 0;
+}
+
+int
+nm_lldp_neighbor_tlv_rewind(NMLldpNeighbor *n)
+{
+ g_return_val_if_fail(n, -EINVAL);
+
+ nm_assert(n->raw_size >= sizeof(struct ether_header));
+
+ n->rindex = sizeof(struct ether_header);
+
+ return n->rindex < n->raw_size;
+}
+
+int
+nm_lldp_neighbor_tlv_next(NMLldpNeighbor *n)
+{
+ size_t length;
+
+ g_return_val_if_fail(n, -EINVAL);
+
+ if (n->rindex == n->raw_size) /* EOF */
+ return -ESPIPE;
+
+ if (n->rindex + 2 > n->raw_size) /* Truncated message */
+ return -EBADMSG;
+
+ length = NM_LLDP_NEIGHBOR_TLV_LENGTH(n);
+ if (n->rindex + 2 + length > n->raw_size)
+ return -EBADMSG;
+
+ n->rindex += 2 + length;
+ return n->rindex < n->raw_size;
+}
+
+int
+nm_lldp_neighbor_tlv_get_type(NMLldpNeighbor *n, uint8_t *type)
+{
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(type, -EINVAL);
+
+ if (n->rindex == n->raw_size) /* EOF */
+ return -ESPIPE;
+
+ if (n->rindex + 2 > n->raw_size)
+ return -EBADMSG;
+
+ *type = NM_LLDP_NEIGHBOR_TLV_TYPE(n);
+ return 0;
+}
+
+int
+nm_lldp_neighbor_tlv_is_type(NMLldpNeighbor *n, uint8_t type)
+{
+ uint8_t k;
+ int r;
+
+ g_return_val_if_fail(n, -EINVAL);
+
+ r = nm_lldp_neighbor_tlv_get_type(n, &k);
+ if (r < 0)
+ return r;
+
+ return type == k;
+}
+
+int
+nm_lldp_neighbor_tlv_get_oui(NMLldpNeighbor *n, uint8_t oui[static 3], uint8_t *subtype)
+{
+ const uint8_t *d;
+ size_t length;
+ int r;
+
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(oui, -EINVAL);
+ g_return_val_if_fail(subtype, -EINVAL);
+
+ r = nm_lldp_neighbor_tlv_is_type(n, NM_LLDP_TYPE_PRIVATE);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return -ENXIO;
+
+ length = NM_LLDP_NEIGHBOR_TLV_LENGTH(n);
+ if (length < 4)
+ return -EBADMSG;
+
+ if (n->rindex + 2 + length > n->raw_size)
+ return -EBADMSG;
+
+ d = NM_LLDP_NEIGHBOR_TLV_DATA(n);
+ memcpy(oui, d, 3);
+ *subtype = d[3];
+
+ return 0;
+}
+
+int
+nm_lldp_neighbor_tlv_is_oui(NMLldpNeighbor *n, const uint8_t oui[static 3], uint8_t subtype)
+{
+ uint8_t k[3], st;
+ int r;
+
+ r = nm_lldp_neighbor_tlv_get_oui(n, k, &st);
+ if (r == -ENXIO)
+ return 0;
+ if (r < 0)
+ return r;
+
+ return memcmp(k, oui, 3) == 0 && st == subtype;
+}
+
+int
+nm_lldp_neighbor_tlv_get_raw(NMLldpNeighbor *n, const void **ret, size_t *size)
+{
+ size_t length;
+
+ g_return_val_if_fail(n, -EINVAL);
+ g_return_val_if_fail(ret, -EINVAL);
+ g_return_val_if_fail(size, -EINVAL);
+
+ /* Note that this returns the full TLV, including the TLV header */
+
+ if (n->rindex + 2 > n->raw_size)
+ return -EBADMSG;
+
+ length = NM_LLDP_NEIGHBOR_TLV_LENGTH(n);
+ if (n->rindex + 2 + length > n->raw_size)
+ return -EBADMSG;
+
+ *ret = (uint8_t *) NM_LLDP_NEIGHBOR_RAW(n) + n->rindex;
+ *size = length + 2;
+
+ return 0;
+}
+
+int
+nm_lldp_neighbor_get_timestamp_usec(NMLldpNeighbor *n, gint64 *out_usec)
+{
+ g_return_val_if_fail(n, -EINVAL);
+
+ if (n->timestamp_usec == 0)
+ return -ENODATA;
+
+ NM_SET_OUT(out_usec, n->timestamp_usec);
+ return 0;
+}
+
+/*****************************************************************************/
+
+NMLldpNeighbor *
+nm_lldp_neighbor_new(size_t raw_size)
+{
+ NMLldpNeighbor *n;
+
+ nm_assert(raw_size < SIZE_MAX - NM_ALIGN(sizeof(NMLldpNeighbor)));
+
+ n = g_malloc0(NM_ALIGN(sizeof(NMLldpNeighbor)) + raw_size);
+
+ n->raw_size = raw_size;
+ n->ref_count = 1;
+ return n;
+}
+
+NMLldpNeighbor *
+nm_lldp_neighbor_new_from_raw(const void *raw, size_t raw_size)
+{
+ nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *n = NULL;
+ int r;
+
+ g_return_val_if_fail(raw || raw_size <= 0, NULL);
+
+ n = nm_lldp_neighbor_new(raw_size);
+
+ nm_memcpy(NM_LLDP_NEIGHBOR_RAW(n), raw, raw_size);
+
+ r = nm_lldp_neighbor_parse(n);
+ if (r < 0)
+ return NULL;
+
+ return g_steal_pointer(&n);
+}
+
+NMLldpNeighbor *
+nm_lldp_neighbor_ref(NMLldpNeighbor *n)
+{
+ if (!n)
+ return NULL;
+
+ nm_assert(n->ref_count > 0 || n->lldp_rx);
+
+ n->ref_count++;
+ return n;
+}
+
+static void
+_lldp_neighbor_free(NMLldpNeighbor *n)
+{
+ if (!n)
+ return;
+
+ g_free((gpointer) n->id.port_id);
+ g_free((gpointer) n->id.chassis_id);
+ g_free(n->port_description);
+ g_free(n->system_name);
+ g_free(n->system_description);
+ g_free(n->mud_url);
+ g_free(n->chassis_id_as_string);
+ g_free(n->port_id_as_string);
+ g_free(n);
+ return;
+}
+
+NMLldpNeighbor *
+nm_lldp_neighbor_unref(NMLldpNeighbor *n)
+{
+ /* Drops one reference from the neighbor. Note that the object is not freed unless it is already unlinked from
+ * the sd_lldp object. */
+
+ if (!n)
+ return NULL;
+
+ nm_assert(n->ref_count > 0);
+ n->ref_count--;
+
+ if (n->ref_count <= 0 && !n->lldp_rx)
+ _lldp_neighbor_free(n);
+
+ return NULL;
+}
+
+void
+nm_lldp_neighbor_unlink(NMLldpNeighbor *n)
+{
+ gpointer old_key;
+ gpointer old_val;
+
+ /* Removes the neighbor object from the LLDP object, and frees it if it also has no other reference. */
+
+ if (!n)
+ return;
+
+ if (!n->lldp_rx)
+ return;
+
+ /* Only remove the neighbor object from the hash table if it's in there, don't complain if it isn't. This is
+ * because we are used as destructor call for hashmap_clear() and thus sometimes are called to de-register
+ * ourselves from the hashtable and sometimes are called after we already are de-registered. */
+
+ if (g_hash_table_steal_extended(n->lldp_rx->neighbor_by_id, n, &old_key, &old_val)) {
+ nm_assert(NM_IN_SET(old_val, NULL, old_key));
+ if (old_key != n) {
+ /* it wasn't the right key. Add it again. */
+ g_hash_table_add(n->lldp_rx->neighbor_by_id, old_key);
+ }
+ }
+
+ nm_prioq_remove(&n->lldp_rx->neighbor_by_expiry, n, &n->prioq_idx);
+
+ n->lldp_rx = NULL;
+
+ if (n->ref_count <= 0)
+ _lldp_neighbor_free(n);
+
+ return;
+}
diff --git a/src/libnm-lldp/nm-lldp-neighbor.h b/src/libnm-lldp/nm-lldp-neighbor.h
new file mode 100644
index 0000000000..1adc967e7e
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-neighbor.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_LLDP_NEIGHBOR_H__
+#define __NM_LLDP_NEIGHBOR_H__
+
+#include "nm-lldp-rx.h"
+
+struct _NMLldpNeighbor {
+ NMLldpNeighborID id;
+
+ /* Neighbor objects stay around as long as they are linked into an "NMLldpRX" object or n_ref > 0. */
+ struct _NMLldpRX *lldp_rx;
+
+ gint64 timestamp_usec;
+
+ gint64 until_usec;
+
+ int ref_count;
+ unsigned prioq_idx;
+
+ NMEtherAddr source_address;
+ NMEtherAddr destination_address;
+
+ /* The raw packet size. The data is appended to the object, accessible via LLDP_NEIGHBOR_RAW() */
+ size_t raw_size;
+
+ /* The current read index for the iterative TLV interface */
+ size_t rindex;
+
+ /* And a couple of fields parsed out. */
+ bool has_ttl : 1;
+ bool has_capabilities : 1;
+ bool has_port_vlan_id : 1;
+
+ uint16_t ttl;
+
+ uint16_t system_capabilities;
+ uint16_t enabled_capabilities;
+
+ char *port_description;
+ char *system_name;
+ char *system_description;
+ char *mud_url;
+
+ uint16_t port_vlan_id;
+
+ char *chassis_id_as_string;
+ char *port_id_as_string;
+};
+
+static inline void *
+NM_LLDP_NEIGHBOR_RAW(const NMLldpNeighbor *n)
+{
+ return (uint8_t *) n + NM_ALIGN(sizeof(NMLldpNeighbor));
+}
+
+static inline uint8_t
+NM_LLDP_NEIGHBOR_TLV_TYPE(const NMLldpNeighbor *n)
+{
+ return ((uint8_t *) NM_LLDP_NEIGHBOR_RAW(n))[n->rindex] >> 1;
+}
+
+static inline size_t
+NM_LLDP_NEIGHBOR_TLV_LENGTH(const NMLldpNeighbor *n)
+{
+ uint8_t *p;
+
+ p = (uint8_t *) NM_LLDP_NEIGHBOR_RAW(n) + n->rindex;
+ return p[1] + (((size_t) (p[0] & 1)) << 8);
+}
+
+static inline void *
+NM_LLDP_NEIGHBOR_TLV_DATA(const NMLldpNeighbor *n)
+{
+ return ((uint8_t *) NM_LLDP_NEIGHBOR_RAW(n)) + n->rindex + 2;
+}
+
+int nm_lldp_neighbor_prioq_compare_func(const void *a, const void *b);
+
+void nm_lldp_neighbor_unlink(NMLldpNeighbor *n);
+NMLldpNeighbor *nm_lldp_neighbor_new(size_t raw_size);
+int nm_lldp_neighbor_parse(NMLldpNeighbor *n);
+void nm_lldp_neighbor_start_ttl(NMLldpNeighbor *n);
+
+#endif /* __NM_LLDP_NEIGHBOR_H__ */
diff --git a/src/libnm-lldp/nm-lldp-network.c b/src/libnm-lldp/nm-lldp-network.c
new file mode 100644
index 0000000000..811c3a7291
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-network.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-lldp-network.h"
+
+#include <linux/filter.h>
+#include <linux/if_packet.h>
+#include <netinet/if_ether.h>
+
+int
+nm_lldp_network_bind_raw_socket(int ifindex)
+{
+ static const struct sock_filter filter[] = {
+ BPF_STMT(BPF_LD + BPF_W + BPF_ABS,
+ offsetof(struct ethhdr, h_dest)), /* A <- 4 bytes of destination MAC */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0180c200, 1, 0), /* A != 01:80:c2:00 */
+ BPF_STMT(BPF_RET + BPF_K, 0), /* drop packet */
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS,
+ offsetof(struct ethhdr, h_dest)
+ + 4), /* A <- remaining 2 bytes of destination MAC */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0000, 3, 0), /* A != 00:00 */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x0003, 2, 0), /* A != 00:03 */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 0x000e, 1, 0), /* A != 00:0e */
+ BPF_STMT(BPF_RET + BPF_K, 0), /* drop packet */
+ BPF_STMT(BPF_LD + BPF_H + BPF_ABS, offsetof(struct ethhdr, h_proto)), /* A <- protocol */
+ BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, NM_ETHERTYPE_LLDP, 1, 0), /* A != NM_ETHERTYPE_LLDP */
+ BPF_STMT(BPF_RET + BPF_K, 0), /* drop packet */
+ BPF_STMT(BPF_RET + BPF_K, UINT32_MAX), /* accept packet */
+ };
+ static const struct sock_fprog fprog = {
+ .len = G_N_ELEMENTS(filter),
+ .filter = (struct sock_filter *) filter,
+ };
+ struct packet_mreq mreq = {
+ .mr_ifindex = ifindex,
+ .mr_type = PACKET_MR_MULTICAST,
+ .mr_alen = ETH_ALEN,
+ .mr_address = {0x01, 0x80, 0xC2, 0x00, 0x00, 0x00},
+ };
+ struct sockaddr_ll saddrll = {
+ .sll_family = AF_PACKET,
+ .sll_ifindex = ifindex,
+ };
+ nm_auto_close int fd = -1;
+
+ assert(ifindex > 0);
+
+ fd = socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, htobe16(NM_ETHERTYPE_LLDP));
+ if (fd < 0)
+ return -errno;
+
+ if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &fprog, sizeof(fprog)) < 0)
+ return -errno;
+
+ /* customer bridge */
+ if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
+ return -errno;
+
+ /* non TPMR bridge */
+ mreq.mr_address[ETH_ALEN - 1] = 0x03;
+ if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
+ return -errno;
+
+ /* nearest bridge */
+ mreq.mr_address[ETH_ALEN - 1] = 0x0E;
+ if (setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
+ return -errno;
+
+ if (bind(fd, (const struct sockaddr *) &saddrll, sizeof(saddrll)) < 0)
+ return -errno;
+
+ return nm_steal_fd(&fd);
+}
diff --git a/src/libnm-lldp/nm-lldp-network.h b/src/libnm-lldp/nm-lldp-network.h
new file mode 100644
index 0000000000..431ac60f8e
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-network.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef __NM_LLDP_NETWORK_H__
+#define __NM_LLDP_NETWORK_H__
+
+#define NM_ETHERTYPE_LLDP 0x88cc
+
+int nm_lldp_network_bind_raw_socket(int ifindex);
+
+#endif /* __NM_LLDP_NETWORK_H__ */
diff --git a/src/libnm-lldp/nm-lldp-rx-internal.h b/src/libnm-lldp/nm-lldp-rx-internal.h
new file mode 100644
index 0000000000..47d063ae70
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-rx-internal.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#ifndef __NM_LLDP_RX_INTERNAL_H__
+#define __NM_LLDP_RX_INTERNAL_H__
+
+#include "libnm-glib-aux/nm-prioq.h"
+#include "libnm-log-core/nm-logging.h"
+
+#include "nm-lldp-rx.h"
+
+struct _NMLldpRX {
+ int ref_count;
+
+ int fd;
+
+ NMLldpRXConfig config;
+
+ GMainContext *main_context;
+
+ GSource *io_event_source;
+ GSource *timer_event_source;
+
+ GHashTable *neighbor_by_id;
+ NMPrioq neighbor_by_expiry;
+};
+
+/*****************************************************************************/
+
+#define _NMLOG2_DOMAIN LOGD_PLATFORM
+#define _NMLOG2(level, lldp_rx, ...) \
+ G_STMT_START \
+ { \
+ const NMLogLevel _level = (level); \
+ NMLldpRX *_lldp_rx = (lldp_rx); \
+ \
+ if (_NMLOG2_ENABLED(_level)) { \
+ _nm_log(level, \
+ _NMLOG2_DOMAIN, \
+ 0, \
+ _lldp_rx->config.log_ifname, \
+ _lldp_rx->config.log_uuid, \
+ "lldp-rx[" NM_HASH_OBFUSCATE_PTR_FMT \
+ "%s%s]: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__), \
+ NM_HASH_OBFUSCATE_PTR(_lldp_rx), \
+ NM_PRINT_FMT_QUOTED2(_lldp_rx->config.log_ifname, \
+ ", ", \
+ _lldp_rx->config.log_ifname, \
+ "") _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
+ } \
+ } \
+ G_STMT_END
+
+/*****************************************************************************/
+
+#endif /* __NM_LLDP_RX_INTERNAL_H__ */
diff --git a/src/libnm-lldp/nm-lldp-rx.c b/src/libnm-lldp/nm-lldp-rx.c
new file mode 100644
index 0000000000..6d0f4a184e
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-rx.c
@@ -0,0 +1,469 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-lib.h"
+
+#include "nm-lldp-rx.h"
+
+#include <arpa/inet.h>
+#include <linux/sockios.h>
+#include <sys/ioctl.h>
+
+#include "libnm-glib-aux/nm-io-utils.h"
+#include "libnm-glib-aux/nm-time-utils.h"
+#include "nm-lldp-network.h"
+#include "nm-lldp-neighbor.h"
+#include "nm-lldp-rx-internal.h"
+
+#define LLDP_DEFAULT_NEIGHBORS_MAX 128U
+
+/*****************************************************************************/
+
+static void lldp_rx_start_timer(NMLldpRX *lldp_rx, NMLldpNeighbor *neighbor);
+
+/*****************************************************************************/
+
+NM_UTILS_LOOKUP_STR_DEFINE(nm_lldp_rx_event_to_string,
+ NMLldpRXEvent,
+ NM_UTILS_LOOKUP_DEFAULT_WARN("<unknown>"),
+ NM_UTILS_LOOKUP_STR_ITEM(NM_LLDP_RX_EVENT_ADDED, "added"),
+ NM_UTILS_LOOKUP_STR_ITEM(NM_LLDP_RX_EVENT_REMOVED, "removed"),
+ NM_UTILS_LOOKUP_STR_ITEM(NM_LLDP_RX_EVENT_UPDATED, "updated"),
+ NM_UTILS_LOOKUP_STR_ITEM(NM_LLDP_RX_EVENT_REFRESHED, "refreshed"),
+ NM_UTILS_LOOKUP_ITEM_IGNORE_OTHER());
+
+/*****************************************************************************/
+
+#define nm_assert_is_lldp_rx(lldp_rx) \
+ G_STMT_START \
+ { \
+ NMLldpRX *_lldp_rx = (lldp_rx); \
+ \
+ nm_assert(_lldp_rx); \
+ nm_assert(_lldp_rx->ref_count > 0); \
+ } \
+ G_STMT_END
+
+/*****************************************************************************/
+
+/* This needs to be first. Check nm_lldp_rx_get_id(). */
+G_STATIC_ASSERT(G_STRUCT_OFFSET(NMLldpNeighbor, id) == 0);
+
+/*****************************************************************************/
+
+static void
+lldp_rx_callback(NMLldpRX *lldp_rx, NMLldpRXEvent event, NMLldpNeighbor *n)
+{
+ nm_assert_is_lldp_rx(lldp_rx);
+ nm_assert(event >= 0 && event < _NM_LLDP_RX_EVENT_MAX);
+
+ _LOG2D(lldp_rx, "invoking callback for '%s' event", nm_lldp_rx_event_to_string(event));
+ lldp_rx->config.callback(lldp_rx, event, n, lldp_rx->config.userdata);
+}
+
+static gboolean
+lldp_rx_make_space(NMLldpRX *lldp_rx, gboolean flush, size_t extra)
+{
+ nm_auto(nm_lldp_rx_unrefp) NMLldpRX *lldp_rx_alive = NULL;
+ gint64 now_usec = 0;
+ gboolean changed = FALSE;
+ size_t max;
+
+ /* Remove all entries that are past their TTL, and more until at least the specified number of extra entries
+ * are free. */
+
+ max = (!flush && lldp_rx->config.neighbors_max > extra)
+ ? (lldp_rx->config.neighbors_max - extra)
+ : 0u;
+
+ for (;;) {
+ NMLldpNeighbor *n;
+
+ nm_assert(g_hash_table_size(lldp_rx->neighbor_by_id)
+ == nm_prioq_size(&lldp_rx->neighbor_by_expiry));
+
+ n = nm_prioq_peek(&lldp_rx->neighbor_by_expiry);
+ if (!n)
+ break;
+
+ if (nm_prioq_size(&lldp_rx->neighbor_by_expiry) > max) {
+ /* drop it. */
+ } else {
+ if (n->until_usec > nm_utils_get_monotonic_timestamp_usec_cached(&now_usec))
+ break;
+ }
+
+ if (flush) {
+ changed = TRUE;
+ nm_lldp_neighbor_unlink(n);
+ } else {
+ nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *n_alive = NULL;
+
+ if (!changed) {
+ lldp_rx_alive = nm_lldp_rx_ref(lldp_rx);
+ changed = TRUE;
+ }
+ n_alive = nm_lldp_neighbor_ref(n);
+ nm_lldp_neighbor_unlink(n);
+ lldp_rx_callback(lldp_rx, NM_LLDP_RX_EVENT_REMOVED, n);
+ }
+ }
+
+ return changed;
+}
+
+static bool
+lldp_rx_keep_neighbor(NMLldpRX *lldp_rx, NMLldpNeighbor *n)
+{
+ nm_assert_is_lldp_rx(lldp_rx);
+ nm_assert(n);
+
+ /* Don't keep data with a zero TTL */
+ if (n->ttl <= 0)
+ return FALSE;
+
+ /* Filter out data from the filter address */
+ if (!nm_ether_addr_is_zero(&lldp_rx->config.filter_address)
+ && nm_ether_addr_equal(&lldp_rx->config.filter_address, &n->source_address))
+ return FALSE;
+
+ /* Only add if the neighbor has a capability we are interested in. Note that we also store all neighbors with
+ * no caps field set. */
+ if (n->has_capabilities && (n->enabled_capabilities & lldp_rx->config.capability_mask) == 0)
+ return FALSE;
+
+ /* Keep everything else */
+ return TRUE;
+}
+
+static void
+lldp_rx_add_neighbor(NMLldpRX *lldp_rx, NMLldpNeighbor *n)
+{
+ nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *old_alive = NULL;
+ NMLldpNeighbor *old;
+ gboolean keep;
+
+ nm_assert_is_lldp_rx(lldp_rx);
+ nm_assert(n);
+ nm_assert(!n->lldp_rx);
+
+ keep = lldp_rx_keep_neighbor(lldp_rx, n);
+
+ /* First retrieve the old entry for this MSAP */
+ old = g_hash_table_lookup(lldp_rx->neighbor_by_id, n);
+ if (old) {
+ old_alive = nm_lldp_neighbor_ref(old);
+
+ if (!keep) {
+ nm_lldp_neighbor_unlink(old);
+ lldp_rx_callback(lldp_rx, NM_LLDP_RX_EVENT_REMOVED, old);
+ return;
+ }
+
+ if (nm_lldp_neighbor_equal(n, old)) {
+ /* Is this equal, then restart the TTL counter, but don't do anything else. */
+ old->timestamp_usec = n->timestamp_usec;
+ lldp_rx_start_timer(lldp_rx, old);
+ lldp_rx_callback(lldp_rx, NM_LLDP_RX_EVENT_REFRESHED, old);
+ return;
+ }
+
+ /* Data changed, remove the old entry, and add a new one */
+ nm_lldp_neighbor_unlink(old);
+
+ } else if (!keep)
+ return;
+
+ /* Then, make room for at least one new neighbor */
+ lldp_rx_make_space(lldp_rx, FALSE, 1);
+
+ if (!g_hash_table_add(lldp_rx->neighbor_by_id, n))
+ nm_assert_not_reached();
+
+ nm_prioq_put(&lldp_rx->neighbor_by_expiry, n, &n->prioq_idx);
+
+ n->lldp_rx = lldp_rx;
+
+ lldp_rx_start_timer(lldp_rx, n);
+ lldp_rx_callback(lldp_rx, old ? NM_LLDP_RX_EVENT_UPDATED : NM_LLDP_RX_EVENT_ADDED, n);
+}
+
+static gboolean
+lldp_rx_receive_datagram(int fd, GIOCondition condition, gpointer user_data)
+
+{
+ NMLldpRX *lldp_rx = user_data;
+ nm_auto(nm_lldp_neighbor_unrefp) NMLldpNeighbor *n = NULL;
+ ssize_t space;
+ ssize_t length;
+ struct timespec ts;
+ gint64 ts_usec;
+ gint64 now_usec;
+ gint64 now_usec_rt;
+ gint64 now_usec_bt;
+ int r;
+
+ nm_assert_is_lldp_rx(lldp_rx);
+ nm_assert(lldp_rx->fd == fd);
+
+ _LOG2T(lldp_rx, "fd ready");
+
+ space = nm_fd_next_datagram_size(lldp_rx->fd);
+ if (space < 0) {
+ if (!NM_ERRNO_IS_TRANSIENT(space) && !NM_ERRNO_IS_DISCONNECT(space)) {
+ _LOG2D(lldp_rx,
+ "Failed to determine datagram size to read, ignoring: %s",
+ nm_strerror_native(-space));
+ }
+ return G_SOURCE_CONTINUE;
+ }
+
+ n = nm_lldp_neighbor_new(space);
+
+ length = recv(lldp_rx->fd, NM_LLDP_NEIGHBOR_RAW(n), n->raw_size, MSG_DONTWAIT);
+ if (length < 0) {
+ if (!NM_ERRNO_IS_TRANSIENT(errno) && !NM_ERRNO_IS_DISCONNECT(errno)) {
+ _LOG2D(lldp_rx,
+ "Failed to read LLDP datagram, ignoring: %s",
+ nm_strerror_native(errno));
+ }
+ return G_SOURCE_CONTINUE;
+ }
+
+ if ((size_t) length != n->raw_size) {
+ _LOG2D(lldp_rx, "Packet size mismatch, ignoring");
+ return G_SOURCE_CONTINUE;
+ }
+
+ /* Try to get the timestamp of this packet if it is known */
+ if (ioctl(lldp_rx->fd, SIOCGSTAMPNS, &ts) >= 0
+ && (ts_usec = nm_utils_timespec_to_usec(&ts)) < G_MAXINT64
+ && (now_usec_bt = nm_utils_clock_gettime_usec(CLOCK_BOOTTIME)) >= 0
+ && (now_usec_rt = nm_utils_clock_gettime_usec(CLOCK_REALTIME)) >= 0) {
+ gint64 t;
+
+ now_usec = nm_utils_monotonic_timestamp_from_boottime(now_usec_bt, 1000);
+ ts_usec = nm_time_map_clock(ts_usec, now_usec_rt, now_usec_bt);
+
+ t = now_usec;
+ if (ts_usec >= 0) {
+ ts_usec = nm_utils_monotonic_timestamp_from_boottime(ts_usec, 1000);
+ if (ts_usec > NM_UTILS_USEC_PER_SEC && ts_usec < now_usec)
+ t = ts_usec;
+ }
+
+ n->timestamp_usec = t;
+ } else
+ n->timestamp_usec = nm_utils_get_monotonic_timestamp_usec();
+
+ r = nm_lldp_neighbor_parse(n);
+ if (r < 0) {
+ _LOG2D(lldp_rx, "Failure parsing invalid LLDP datagram.");
+ return G_SOURCE_CONTINUE;
+ }
+
+ _LOG2D(lldp_rx, "Successfully processed LLDP datagram.");
+ lldp_rx_add_neighbor(lldp_rx, n);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+lldp_rx_reset(NMLldpRX *lldp_rx)
+{
+ nm_clear_g_source_inst(&lldp_rx->timer_event_source);
+ nm_clear_g_source_inst(&lldp_rx->io_event_source);
+ nm_clear_fd(&lldp_rx->fd);
+
+ lldp_rx_make_space(lldp_rx, TRUE, 0);
+
+ nm_assert(g_hash_table_size(lldp_rx->neighbor_by_id) == 0);
+ nm_assert(nm_prioq_size(&lldp_rx->neighbor_by_expiry) == 0);
+}
+
+gboolean
+nm_lldp_rx_is_running(NMLldpRX *lldp_rx)
+{
+ if (!lldp_rx)
+ return FALSE;
+
+ return lldp_rx->fd >= 0;
+}
+
+int
+nm_lldp_rx_start(NMLldpRX *lldp_rx)
+{
+ int r;
+
+ g_return_val_if_fail(lldp_rx, -EINVAL);
+ nm_assert(lldp_rx->main_context);
+ nm_assert(lldp_rx->config.ifindex > 0);
+
+ if (nm_lldp_rx_is_running(lldp_rx))
+ return 0;
+
+ nm_assert(!lldp_rx->io_event_source);
+
+ r = nm_lldp_network_bind_raw_socket(lldp_rx->config.ifindex);
+ if (r < 0) {
+ _LOG2D(lldp_rx, "start failed to bind socket (%s)", nm_strerror_native(-r));
+ return r;
+ }
+
+ lldp_rx->fd = r;
+
+ lldp_rx->io_event_source = nm_g_source_attach(nm_g_unix_fd_source_new(lldp_rx->fd,
+ G_IO_IN,
+ G_PRIORITY_DEFAULT,
+ lldp_rx_receive_datagram,
+ lldp_rx,
+ NULL),
+ lldp_rx->main_context);
+
+ _LOG2D(lldp_rx, "started (fd %d)", lldp_rx->fd);
+ return 1;
+}
+
+int
+nm_lldp_rx_stop(NMLldpRX *lldp_rx)
+{
+ if (!nm_lldp_rx_is_running(lldp_rx))
+ return 0;
+
+ _LOG2D(lldp_rx, "stopping");
+
+ lldp_rx_reset(lldp_rx);
+ return 1;
+}
+
+static gboolean
+on_timer_event(gpointer user_data)
+{
+ NMLldpRX *lldp_rx = user_data;
+
+ lldp_rx_make_space(lldp_rx, FALSE, 0);
+ lldp_rx_start_timer(lldp_rx, NULL);
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+lldp_rx_start_timer(NMLldpRX *lldp_rx, NMLldpNeighbor *neighbor)
+{
+ NMLldpNeighbor *n;
+ gint64 timeout_msec;
+
+ nm_assert_is_lldp_rx(lldp_rx);
+
+ nm_clear_g_source_inst(&lldp_rx->timer_event_source);
+
+ if (neighbor)
+ nm_lldp_neighbor_start_ttl(neighbor);
+
+ n = nm_prioq_peek(&lldp_rx->neighbor_by_expiry);
+ if (!n)
+ return;
+
+ timeout_msec = (n->until_usec / 1000) - nm_utils_get_monotonic_timestamp_msec();
+
+ lldp_rx->timer_event_source =
+ nm_g_source_attach(nm_g_timeout_source_new(NM_CLAMP(timeout_msec, 0, G_MAXUINT),
+ G_PRIORITY_DEFAULT,
+ on_timer_event,
+ lldp_rx,
+ NULL),
+ lldp_rx->main_context);
+}
+
+static inline int
+neighbor_compare_func(gconstpointer p_a, gconstpointer p_b, gpointer user_data)
+{
+ NMLldpNeighbor *const *a = p_a;
+ NMLldpNeighbor *const *b = p_b;
+
+ nm_assert(a);
+ nm_assert(b);
+ nm_assert(*a);
+ nm_assert(*b);
+
+ return nm_lldp_neighbor_id_cmp(&(*a)->id, &(*b)->id);
+}
+
+NMLldpNeighbor **
+nm_lldp_rx_get_neighbors(NMLldpRX *lldp_rx, guint *out_len)
+{
+ g_return_val_if_fail(lldp_rx, NULL);
+
+ return (NMLldpNeighbor **)
+ nm_utils_hash_keys_to_array(lldp_rx->neighbor_by_id, neighbor_compare_func, NULL, out_len);
+}
+
+/*****************************************************************************/
+
+NMLldpRX *
+nm_lldp_rx_new(const NMLldpRXConfig *config)
+{
+ NMLldpRX *lldp_rx;
+
+ nm_assert(config);
+ nm_assert(config->ifindex > 0);
+ nm_assert(config->callback);
+
+ /* This needs to be first, see neighbor_by_id hash. */
+ G_STATIC_ASSERT_EXPR(G_STRUCT_OFFSET(NMLldpNeighbor, id) == 0);
+
+ lldp_rx = g_slice_new(NMLldpRX);
+ *lldp_rx = (NMLldpRX){
+ .ref_count = 1,
+ .fd = -1,
+ .main_context = g_main_context_ref_thread_default(),
+ .config = *config,
+ .neighbor_by_id = g_hash_table_new((GHashFunc) nm_lldp_neighbor_id_hash,
+ (GEqualFunc) nm_lldp_neighbor_id_equal),
+ };
+ lldp_rx->config.log_ifname = g_strdup(lldp_rx->config.log_ifname);
+ lldp_rx->config.log_uuid = g_strdup(lldp_rx->config.log_uuid);
+ if (lldp_rx->config.neighbors_max == 0)
+ lldp_rx->config.neighbors_max = LLDP_DEFAULT_NEIGHBORS_MAX;
+ if (!lldp_rx->config.has_capability_mask && lldp_rx->config.capability_mask == 0)
+ lldp_rx->config.capability_mask = UINT16_MAX;
+
+ nm_prioq_init(&lldp_rx->neighbor_by_expiry, (GCompareFunc) nm_lldp_neighbor_prioq_compare_func);
+
+ return lldp_rx;
+}
+
+NMLldpRX *
+nm_lldp_rx_ref(NMLldpRX *lldp_rx)
+{
+ if (!lldp_rx)
+ return NULL;
+
+ nm_assert_is_lldp_rx(lldp_rx);
+ nm_assert(lldp_rx->ref_count < G_MAXINT);
+
+ lldp_rx->ref_count++;
+ return lldp_rx;
+}
+
+void
+nm_lldp_rx_unref(NMLldpRX *lldp_rx)
+{
+ if (!lldp_rx)
+ return;
+
+ nm_assert_is_lldp_rx(lldp_rx);
+
+ if (--lldp_rx->ref_count > 0)
+ return;
+
+ lldp_rx_reset(lldp_rx);
+
+ g_hash_table_unref(lldp_rx->neighbor_by_id);
+ nm_prioq_destroy(&lldp_rx->neighbor_by_expiry);
+
+ free((char *) lldp_rx->config.log_ifname);
+ free((char *) lldp_rx->config.log_uuid);
+
+ g_main_context_unref(lldp_rx->main_context);
+
+ nm_g_slice_free(lldp_rx);
+}
diff --git a/src/libnm-lldp/nm-lldp-rx.h b/src/libnm-lldp/nm-lldp-rx.h
new file mode 100644
index 0000000000..a3f3805376
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp-rx.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef __NM_LLDP_RX_H__
+#define __NM_LLDP_RX_H__
+
+#include "nm-lldp.h"
+
+typedef struct _NMLldpRX NMLldpRX;
+typedef struct _NMLldpNeighbor NMLldpNeighbor;
+
+typedef enum {
+ NM_LLDP_RX_EVENT_ADDED,
+ NM_LLDP_RX_EVENT_REMOVED,
+ NM_LLDP_RX_EVENT_UPDATED,
+ NM_LLDP_RX_EVENT_REFRESHED,
+ _NM_LLDP_RX_EVENT_MAX,
+ _NM_LLDP_RX_EVENT_INVALID = -EINVAL,
+} NMLldpRXEvent;
+
+const char *nm_lldp_rx_event_to_string(NMLldpRXEvent e) _nm_pure;
+
+typedef struct {
+ /* The spec calls this an "MSAP identifier" */
+ const void *chassis_id;
+ size_t chassis_id_size;
+
+ const void *port_id;
+ size_t port_id_size;
+} NMLldpNeighborID;
+
+guint nm_lldp_neighbor_id_hash(const NMLldpNeighborID *id);
+int nm_lldp_neighbor_id_cmp(const NMLldpNeighborID *x, const NMLldpNeighborID *y);
+gboolean nm_lldp_neighbor_id_equal(const NMLldpNeighborID *x, const NMLldpNeighborID *y);
+
+typedef void (*NMLldpRXCallback)(NMLldpRX *lldp_rx,
+ NMLldpRXEvent event,
+ NMLldpNeighbor *n,
+ void *userdata);
+
+typedef struct {
+ int ifindex;
+ guint neighbors_max;
+ const char *log_ifname;
+ const char *log_uuid;
+ NMLldpRXCallback callback;
+ void *userdata;
+
+ /* In order to deal nicely with bridges that send back our own packets, allow one address to be filtered, so
+ * that our own can be filtered out here. */
+ NMEtherAddr filter_address;
+
+ uint16_t capability_mask;
+ bool has_capability_mask : 1;
+} NMLldpRXConfig;
+
+NMLldpRX *nm_lldp_rx_new(const NMLldpRXConfig *config);
+NMLldpRX *nm_lldp_rx_ref(NMLldpRX *lldp_rx);
+void nm_lldp_rx_unref(NMLldpRX *lldp_rx);
+
+NM_AUTO_DEFINE_FCN(NMLldpRX *, nm_lldp_rx_unrefp, nm_lldp_rx_unref);
+
+int nm_lldp_rx_start(NMLldpRX *lldp_rx);
+int nm_lldp_rx_stop(NMLldpRX *lldp_rx);
+gboolean nm_lldp_rx_is_running(NMLldpRX *lldp_rx);
+
+/* Controls how much and what to store in the neighbors database */
+
+NMLldpNeighbor **nm_lldp_rx_get_neighbors(NMLldpRX *lldp_rx, guint *out_len);
+
+/*****************************************************************************/
+
+NMLldpNeighbor *nm_lldp_neighbor_new_from_raw(const void *raw, size_t raw_size);
+
+NMLldpNeighbor *nm_lldp_neighbor_ref(NMLldpNeighbor *n);
+NMLldpNeighbor *nm_lldp_neighbor_unref(NMLldpNeighbor *n);
+
+NM_AUTO_DEFINE_FCN(NMLldpNeighbor *, nm_lldp_neighbor_unrefp, nm_lldp_neighbor_unref);
+
+int nm_lldp_neighbor_cmp(const NMLldpNeighbor *a, const NMLldpNeighbor *b);
+
+static inline gboolean
+nm_lldp_neighbor_equal(const NMLldpNeighbor *a, const NMLldpNeighbor *b)
+{
+ return nm_lldp_neighbor_cmp(a, b) == 0;
+}
+
+/*****************************************************************************/
+
+static inline const NMLldpNeighborID *
+nm_lldp_neighbor_get_id(NMLldpNeighbor *lldp_neigbor)
+{
+ return (const NMLldpNeighborID *) ((gconstpointer) lldp_neigbor);
+}
+
+/* Access to LLDP frame metadata */
+int nm_lldp_neighbor_get_source_address(NMLldpNeighbor *n, NMEtherAddr *address);
+int nm_lldp_neighbor_get_destination_address(NMLldpNeighbor *n, NMEtherAddr *address);
+int nm_lldp_neighbor_get_timestamp_usec(NMLldpNeighbor *n, gint64 *out_usec);
+int nm_lldp_neighbor_get_raw(NMLldpNeighbor *n, const void **ret, size_t *size);
+
+/* High-level, direct, parsed out field access. These fields exist at most once, hence may be queried directly. */
+int
+nm_lldp_neighbor_get_chassis_id(NMLldpNeighbor *n, uint8_t *type, const void **ret, size_t *size);
+const char *nm_lldp_neighbor_get_chassis_id_as_string(NMLldpNeighbor *n);
+int nm_lldp_neighbor_get_port_id(NMLldpNeighbor *n, uint8_t *type, const void **ret, size_t *size);
+const char *nm_lldp_neighbor_get_port_id_as_string(NMLldpNeighbor *n);
+int nm_lldp_neighbor_get_ttl(NMLldpNeighbor *n, uint16_t *ret_sec);
+int nm_lldp_neighbor_get_system_name(NMLldpNeighbor *n, const char **ret);
+int nm_lldp_neighbor_get_system_description(NMLldpNeighbor *n, const char **ret);
+int nm_lldp_neighbor_get_port_description(NMLldpNeighbor *n, const char **ret);
+int nm_lldp_neighbor_get_mud_url(NMLldpNeighbor *n, const char **ret);
+int nm_lldp_neighbor_get_system_capabilities(NMLldpNeighbor *n, uint16_t *ret);
+int nm_lldp_neighbor_get_enabled_capabilities(NMLldpNeighbor *n, uint16_t *ret);
+
+/* Low-level, iterative TLV access. This is for everything else, it iteratively goes through all available TLVs
+ * (including the ones covered with the calls above), and allows multiple TLVs for the same fields. */
+int nm_lldp_neighbor_tlv_rewind(NMLldpNeighbor *n);
+int nm_lldp_neighbor_tlv_next(NMLldpNeighbor *n);
+int nm_lldp_neighbor_tlv_get_type(NMLldpNeighbor *n, uint8_t *type);
+int nm_lldp_neighbor_tlv_is_type(NMLldpNeighbor *n, uint8_t type);
+int nm_lldp_neighbor_tlv_get_oui(NMLldpNeighbor *n, uint8_t oui[static 3], uint8_t *subtype);
+int nm_lldp_neighbor_tlv_is_oui(NMLldpNeighbor *n, const uint8_t oui[static 3], uint8_t subtype);
+int nm_lldp_neighbor_tlv_get_raw(NMLldpNeighbor *n, const void **ret, size_t *size);
+
+#endif /* __NM_LLDP_RX_H__ */
diff --git a/src/libnm-lldp/nm-lldp.h b/src/libnm-lldp/nm-lldp.h
new file mode 100644
index 0000000000..55e7de292e
--- /dev/null
+++ b/src/libnm-lldp/nm-lldp.h
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef __NM_LLDP_H__
+#define __NM_LLDP_H__
+
+/* IEEE 802.1AB-2009 Clause 8: TLV Types */
+enum {
+ NM_LLDP_TYPE_END = 0,
+ NM_LLDP_TYPE_CHASSIS_ID = 1,
+ NM_LLDP_TYPE_PORT_ID = 2,
+ NM_LLDP_TYPE_TTL = 3,
+ NM_LLDP_TYPE_PORT_DESCRIPTION = 4,
+ NM_LLDP_TYPE_SYSTEM_NAME = 5,
+ NM_LLDP_TYPE_SYSTEM_DESCRIPTION = 6,
+ NM_LLDP_TYPE_SYSTEM_CAPABILITIES = 7,
+ NM_LLDP_TYPE_MGMT_ADDRESS = 8,
+ NM_LLDP_TYPE_PRIVATE = 127
+};
+
+/* IEEE 802.1AB-2009 Clause 8.5.2: Chassis subtypes */
+enum {
+ NM_LLDP_CHASSIS_SUBTYPE_RESERVED = 0,
+ NM_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT = 1,
+ NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS = 2,
+ NM_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT = 3,
+ NM_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS = 4,
+ NM_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS = 5,
+ NM_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME = 6,
+ NM_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED = 7
+};
+
+/* IEEE 802.1AB-2009 Clause 8.5.3: Port subtype */
+enum {
+ NM_LLDP_PORT_SUBTYPE_RESERVED = 0,
+ NM_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS = 1,
+ NM_LLDP_PORT_SUBTYPE_PORT_COMPONENT = 2,
+ NM_LLDP_PORT_SUBTYPE_MAC_ADDRESS = 3,
+ NM_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS = 4,
+ NM_LLDP_PORT_SUBTYPE_INTERFACE_NAME = 5,
+ NM_LLDP_PORT_SUBTYPE_AGENT_CIRCUIT_ID = 6,
+ NM_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED = 7
+};
+
+/* IEEE 802.1AB-2009 Clause 8.5.8: System capabilities */
+enum {
+ NM_LLDP_SYSTEM_CAPABILITIES_OTHER = 1 << 0,
+ NM_LLDP_SYSTEM_CAPABILITIES_REPEATER = 1 << 1,
+ NM_LLDP_SYSTEM_CAPABILITIES_BRIDGE = 1 << 2,
+ NM_LLDP_SYSTEM_CAPABILITIES_WLAN_AP = 1 << 3,
+ NM_LLDP_SYSTEM_CAPABILITIES_ROUTER = 1 << 4,
+ NM_LLDP_SYSTEM_CAPABILITIES_PHONE = 1 << 5,
+ NM_LLDP_SYSTEM_CAPABILITIES_DOCSIS = 1 << 6,
+ NM_LLDP_SYSTEM_CAPABILITIES_STATION = 1 << 7,
+ NM_LLDP_SYSTEM_CAPABILITIES_CVLAN = 1 << 8,
+ NM_LLDP_SYSTEM_CAPABILITIES_SVLAN = 1 << 9,
+ NM_LLDP_SYSTEM_CAPABILITIES_TPMR = 1 << 10
+};
+
+#define NM_LLDP_SYSTEM_CAPABILITIES_ALL UINT16_MAX
+
+#define NM_LLDP_SYSTEM_CAPABILITIES_ALL_ROUTERS \
+ ((uint16_t) (NM_LLDP_SYSTEM_CAPABILITIES_REPEATER | NM_LLDP_SYSTEM_CAPABILITIES_BRIDGE \
+ | NM_LLDP_SYSTEM_CAPABILITIES_WLAN_AP | NM_LLDP_SYSTEM_CAPABILITIES_ROUTER \
+ | NM_LLDP_SYSTEM_CAPABILITIES_DOCSIS | NM_LLDP_SYSTEM_CAPABILITIES_CVLAN \
+ | NM_LLDP_SYSTEM_CAPABILITIES_SVLAN | NM_LLDP_SYSTEM_CAPABILITIES_TPMR))
+
+#define NM_LLDP_OUI_802_1 \
+ (const uint8_t[]) \
+ { \
+ 0x00, 0x80, 0xc2 \
+ }
+#define NM_LLDP_OUI_802_3 \
+ (const uint8_t[]) \
+ { \
+ 0x00, 0x12, 0x0f \
+ }
+
+#define _SD_LLDP_OUI_IANA 0x00, 0x00, 0x5E
+#define NM_LLDP_OUI_IANA \
+ (const uint8_t[]) \
+ { \
+ _SD_LLDP_OUI_IANA \
+ }
+
+#define NM_LLDP_OUI_IANA_SUBTYPE_MUD 0x01
+#define NM_LLDP_OUI_IANA_MUD \
+ (const uint8_t[]) \
+ { \
+ _SD_LLDP_OUI_IANA, NM_LLDP_OUI_IANA_SUBTYPE_MUD \
+ }
+
+/* IEEE 802.1AB-2009 Annex E */
+enum {
+ NM_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID = 1,
+ NM_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID = 2,
+ NM_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME = 3,
+ NM_LLDP_OUI_802_1_SUBTYPE_PROTOCOL_IDENTITY = 4,
+ NM_LLDP_OUI_802_1_SUBTYPE_VID_USAGE_DIGEST = 5,
+ NM_LLDP_OUI_802_1_SUBTYPE_MANAGEMENT_VID = 6,
+ NM_LLDP_OUI_802_1_SUBTYPE_LINK_AGGREGATION = 7
+};
+
+/* IEEE 802.1AB-2009 Annex F */
+enum {
+ NM_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS = 1,
+ NM_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI = 2,
+ NM_LLDP_OUI_802_3_SUBTYPE_LINK_AGGREGATION = 3,
+ NM_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE = 4
+};
+
+#endif /* __NM_LLDP_H__ */