summary refs log tree commit diff
path: root/src/libnm-lldp
diff options
context:
space:
mode:
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 00000000..0d2065e8
--- /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 00000000..f1e2d42e
--- /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 00000000..1adc967e
--- /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 00000000..811c3a72
--- /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 00000000..431ac60f
--- /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 00000000..47d063ae
--- /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 00000000..6d0f4a18
--- /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 00000000..a3f38053
--- /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 00000000..55e7de29
--- /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__ */