Blob Blame History Raw
/* SPDX-License-Identifier: GPL-2.0+ */
/*
 * Copyright (C) 2015 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-lldp-listener.h"

#include <net/ethernet.h>

#include "nm-std-aux/unaligned.h"
#include "platform/nm-platform.h"
#include "nm-glib-aux/nm-c-list.h"
#include "nm-utils.h"

#include "systemd/nm-sd.h"

#define MAX_NEIGHBORS            128
#define MIN_UPDATE_INTERVAL_NSEC (2 * NM_UTILS_NSEC_PER_SEC)

#define LLDP_MAC_NEAREST_BRIDGE \
    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e}}))
#define LLDP_MAC_NEAREST_NON_TPMR_BRIDGE \
    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x03}}))
#define LLDP_MAC_NEAREST_CUSTOMER_BRIDGE \
    (&((struct ether_addr){.ether_addr_octet = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x00}}))

/*****************************************************************************/

NM_GOBJECT_PROPERTIES_DEFINE(NMLldpListener, PROP_NEIGHBORS, );

typedef struct {
    sd_lldp *   lldp_handle;
    GHashTable *lldp_neighbors;
    GVariant *  variant;

    /* the timestamp in nsec until which we delay updates. */
    gint64 ratelimit_next_nsec;
    guint  ratelimit_id;

    int ifindex;
} NMLldpListenerPrivate;

struct _NMLldpListener {
    GObject               parent;
    NMLldpListenerPrivate _priv;
};

struct _NMLldpListenerClass {
    GObjectClass parent;
};

G_DEFINE_TYPE(NMLldpListener, nm_lldp_listener, G_TYPE_OBJECT)

#define NM_LLDP_LISTENER_GET_PRIVATE(self) \
    _NM_GET_PRIVATE(self, NMLldpListener, NM_IS_LLDP_LISTENER)

/*****************************************************************************/

typedef struct {
    GVariant *        variant;
    sd_lldp_neighbor *neighbor_sd;
    char *            chassis_id;
    char *            port_id;
    guint8            chassis_id_type;
    guint8            port_id_type;
} LldpNeighbor;

/*****************************************************************************/

#define _NMLOG_PREFIX_NAME "lldp"
#define _NMLOG_DOMAIN      LOGD_DEVICE
#define _NMLOG(level, ...)                                                                      \
    G_STMT_START                                                                                \
    {                                                                                           \
        const NMLogLevel _level = (level);                                                      \
                                                                                                \
        if (nm_logging_enabled(_level, _NMLOG_DOMAIN)) {                                        \
            char _sbuf[64];                                                                     \
            int  _ifindex = (self) ? NM_LLDP_LISTENER_GET_PRIVATE(self)->ifindex : 0;           \
                                                                                                \
            _nm_log(_level,                                                                     \
                    _NMLOG_DOMAIN,                                                              \
                    0,                                                                          \
                    _ifindex > 0 ? nm_platform_link_get_name(NM_PLATFORM_GET, _ifindex) : NULL, \
                    NULL,                                                                       \
                    "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                                \
                    _NMLOG_PREFIX_NAME,                                                         \
                    ((_ifindex > 0) ? nm_sprintf_buf(_sbuf, "[%p,%d]", (self), _ifindex)        \
                                    : ((self) ? nm_sprintf_buf(_sbuf, "[%p]", (self)) : ""))    \
                        _NM_UTILS_MACRO_REST(__VA_ARGS__));                                     \
        }                                                                                       \
    }                                                                                           \
    G_STMT_END

#define LOG_NEIGH_FMT "CHASSIS=%u/%s PORT=%u/%s"
#define LOG_NEIGH_ARG(neigh) \
    (neigh)->chassis_id_type, (neigh)->chassis_id, (neigh)->port_id_type, (neigh)->port_id

/*****************************************************************************/

static void
lldp_neighbor_get_raw(LldpNeighbor *neigh, const guint8 **out_raw_data, gsize *out_raw_len)
{
    gconstpointer raw_data;
    gsize         raw_len;
    int           r;

    nm_assert(neigh);

    r = sd_lldp_neighbor_get_raw(neigh->neighbor_sd, &raw_data, &raw_len);

    nm_assert(r >= 0);
    nm_assert(raw_data);
    nm_assert(raw_len > 0);

    *out_raw_data = raw_data;
    *out_raw_len  = raw_len;
}

static gboolean
lldp_neighbor_id_get(struct sd_lldp_neighbor *neighbor_sd,
                     guint8 *                 out_chassis_id_type,
                     const guint8 **          out_chassis_id,
                     gsize *                  out_chassis_id_len,
                     guint8 *                 out_port_id_type,
                     const guint8 **          out_port_id,
                     gsize *                  out_port_id_len)
{
    int r;

    r = sd_lldp_neighbor_get_chassis_id(neighbor_sd,
                                        out_chassis_id_type,
                                        (gconstpointer *) out_chassis_id,
                                        out_chassis_id_len);
    if (r < 0)
        return FALSE;

    r = sd_lldp_neighbor_get_port_id(neighbor_sd,
                                     out_port_id_type,
                                     (gconstpointer *) out_port_id,
                                     out_port_id_len);
    if (r < 0)
        return FALSE;

    return TRUE;
}

static guint
lldp_neighbor_id_hash(gconstpointer ptr)
{
    const LldpNeighbor *neigh = ptr;
    guint8              chassis_id_type;
    guint8              port_id_type;
    const guint8 *      chassis_id;
    const guint8 *      port_id;
    gsize               chassis_id_len;
    gsize               port_id_len;
    NMHashState         h;

    if (!lldp_neighbor_id_get(neigh->neighbor_sd,
                              &chassis_id_type,
                              &chassis_id,
                              &chassis_id_len,
                              &port_id_type,
                              &port_id,
                              &port_id_len)) {
        nm_assert_not_reached();
        return 0;
    }

    nm_hash_init(&h, 23423423u);
    nm_hash_update_vals(&h, chassis_id_len, port_id_len, chassis_id_type, port_id_type);
    nm_hash_update(&h, chassis_id, chassis_id_len);
    nm_hash_update(&h, port_id, port_id_len);
    return nm_hash_complete(&h);
}

static int
lldp_neighbor_id_cmp(const LldpNeighbor *a, const LldpNeighbor *b)
{
    guint8        a_chassis_id_type;
    guint8        b_chassis_id_type;
    guint8        a_port_id_type;
    guint8        b_port_id_type;
    const guint8 *a_chassis_id;
    const guint8 *b_chassis_id;
    const guint8 *a_port_id;
    const guint8 *b_port_id;
    gsize         a_chassis_id_len;
    gsize         b_chassis_id_len;
    gsize         a_port_id_len;
    gsize         b_port_id_len;

    NM_CMP_SELF(a, b);

    if (!lldp_neighbor_id_get(a->neighbor_sd,
                              &a_chassis_id_type,
                              &a_chassis_id,
                              &a_chassis_id_len,
                              &a_port_id_type,
                              &a_port_id,
                              &a_port_id_len)) {
        nm_assert_not_reached();
        return FALSE;
    }

    if (!lldp_neighbor_id_get(b->neighbor_sd,
                              &b_chassis_id_type,
                              &b_chassis_id,
                              &b_chassis_id_len,
                              &b_port_id_type,
                              &b_port_id,
                              &b_port_id_len)) {
        nm_assert_not_reached();
        return FALSE;
    }

    NM_CMP_DIRECT(a_chassis_id_type, b_chassis_id_type);
    NM_CMP_DIRECT(a_port_id_type, b_port_id_type);
    NM_CMP_DIRECT(a_chassis_id_len, b_chassis_id_len);
    NM_CMP_DIRECT(a_port_id_len, b_port_id_len);
    NM_CMP_DIRECT_MEMCMP(a_chassis_id, b_chassis_id, a_chassis_id_len);
    NM_CMP_DIRECT_MEMCMP(a_port_id, b_port_id, a_port_id_len);
    return 0;
}

static int
lldp_neighbor_id_cmp_p(gconstpointer a, gconstpointer b, gpointer user_data)
{
    return lldp_neighbor_id_cmp(*((const LldpNeighbor *const *) a),
                                *((const LldpNeighbor *const *) b));
}

static gboolean
lldp_neighbor_id_equal(gconstpointer a, gconstpointer b)
{
    return lldp_neighbor_id_cmp(a, b) == 0;
}

static void
lldp_neighbor_free(LldpNeighbor *neighbor)
{
    if (!neighbor)
        return;

    g_free(neighbor->chassis_id);
    g_free(neighbor->port_id);
    nm_g_variant_unref(neighbor->variant);
    sd_lldp_neighbor_unref(neighbor->neighbor_sd);
    nm_g_slice_free(neighbor);
}

static void
lldp_neighbor_freep(LldpNeighbor **ptr)
{
    lldp_neighbor_free(*ptr);
}

static gboolean
lldp_neighbor_equal(LldpNeighbor *a, LldpNeighbor *b)
{
    const guint8 *raw_data_a;
    const guint8 *raw_data_b;
    gsize         raw_len_a;
    gsize         raw_len_b;

    if (a->neighbor_sd == b->neighbor_sd)
        return TRUE;

    lldp_neighbor_get_raw(a, &raw_data_a, &raw_len_a);
    lldp_neighbor_get_raw(b, &raw_data_b, &raw_len_b);
    return raw_len_a == raw_len_b && (memcmp(raw_data_a, raw_data_b, raw_len_a) == 0);
}

static GVariant *
parse_management_address_tlv(const uint8_t *data, gsize len)
{
    GVariantBuilder builder;
    gsize           addr_len;
    const guint8 *  v_object_id_arr;
    gsize           v_object_id_len;
    const guint8 *  v_address_arr;
    gsize           v_address_len;
    guint32         v_interface_number;
    guint32         v_interface_number_subtype;
    guint32         v_address_subtype;

    /* 802.1AB-2009 - Figure 8-11
     *
     * - TLV type / length        (2 bytes)
     * - address string length    (1 byte)
     * - address subtype          (1 byte)
     * - address                  (1 to 31 bytes)
     * - interface number subtype (1 byte)
     * - interface number         (4 bytes)
     * - OID string length        (1 byte)
     * - OID                      (0 to 128 bytes)
     */

    if (len < 11)
        return NULL;

    nm_assert((data[0] >> 1) == SD_LLDP_TYPE_MGMT_ADDRESS);
    nm_assert((((data[0] & 1) << 8) + data[1]) + 2 == len);

    data += 2;
    len -= 2;
    addr_len = *data; /* length of (address subtype + address) */

    if (addr_len < 2 || addr_len > 32)
        return NULL;
    if (len < (1          /* address stringth length */
               + addr_len /* address subtype + address */
               + 5        /* interface */
               + 1))      /* oid */
        return NULL;

    data++;
    len--;
    v_address_subtype = *data;
    v_address_arr     = &data[1];
    v_address_len     = addr_len - 1;

    data += addr_len;
    len -= addr_len;
    v_interface_number_subtype = *data;

    data++;
    len--;
    v_interface_number = unaligned_read_be32(data);

    data += 4;
    len -= 4;
    v_object_id_len = *data;
    if (len < (1 + v_object_id_len))
        return NULL;
    data++;
    v_object_id_arr = data;

    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
    nm_g_variant_builder_add_sv_uint32(&builder, "address-subtype", v_address_subtype);
    nm_g_variant_builder_add_sv_bytearray(&builder, "address", v_address_arr, v_address_len);
    nm_g_variant_builder_add_sv_uint32(&builder,
                                       "interface-number-subtype",
                                       v_interface_number_subtype);
    nm_g_variant_builder_add_sv_uint32(&builder, "interface-number", v_interface_number);
    if (v_object_id_len > 0)
        nm_g_variant_builder_add_sv_bytearray(&builder,
                                              "object-id",
                                              v_object_id_arr,
                                              v_object_id_len);
    return g_variant_builder_end(&builder);
}

static char *
format_network_address(const guint8 *data, gsize sz)
{
    NMIPAddr a;
    int      family;

    if (sz == 5 && data[0] == 1 /* LLDP_MGMT_ADDR_IP4 */) {
        memcpy(&a, &data[1], sizeof(a.addr4));
        family = AF_INET;
    } else if (sz == 17 && data[0] == 2 /* LLDP_MGMT_ADDR_IP6 */) {
        memcpy(&a, &data[1], sizeof(a.addr6));
        family = AF_INET6;
    } else
        return NULL;

    return nm_utils_inet_ntop_dup(family, &a);
}

static const char *
format_string(const guint8 *data, gsize len, gboolean allow_trim, char **out_to_free)
{
    gboolean is_null_terminated = FALSE;

    nm_assert(out_to_free && !*out_to_free);

    if (allow_trim) {
        while (len > 0 && data[len - 1] == '\0') {
            is_null_terminated = TRUE;
            len--;
        }
    }

    if (len == 0)
        return NULL;

    if (memchr(data, len, '\0'))
        return NULL;

    return nm_utils_buf_utf8safe_escape(data,
                                        is_null_terminated ? -1 : (gssize) len,
                                        NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_CTRL
                                            | NM_UTILS_STR_UTF8_SAFE_FLAG_ESCAPE_NON_ASCII,
                                        out_to_free);
}

static char *
format_string_cp(const guint8 *data, gsize len, gboolean allow_trim)
{
    char *      s_free = NULL;
    const char *s;

    s = format_string(data, len, allow_trim, &s_free);
    nm_assert(!s_free || s == s_free);
    return s ? (s_free ?: g_strdup(s)) : NULL;
}

static LldpNeighbor *
lldp_neighbor_new(sd_lldp_neighbor *neighbor_sd)
{
    LldpNeighbor *neigh;
    guint8        chassis_id_type;
    guint8        port_id_type;
    const guint8 *chassis_id;
    const guint8 *port_id;
    gsize         chassis_id_len;
    gsize         port_id_len;
    gs_free char *s_chassis_id = NULL;
    gs_free char *s_port_id    = NULL;

    if (!lldp_neighbor_id_get(neighbor_sd,
                              &chassis_id_type,
                              &chassis_id,
                              &chassis_id_len,
                              &port_id_type,
                              &port_id,
                              &port_id_len))
        return NULL;

    switch (chassis_id_type) {
    case SD_LLDP_CHASSIS_SUBTYPE_CHASSIS_COMPONENT:
    case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_ALIAS:
    case SD_LLDP_CHASSIS_SUBTYPE_PORT_COMPONENT:
    case SD_LLDP_CHASSIS_SUBTYPE_INTERFACE_NAME:
    case SD_LLDP_CHASSIS_SUBTYPE_LOCALLY_ASSIGNED:
        s_chassis_id = format_string_cp(chassis_id, chassis_id_len, FALSE);
        break;
    case SD_LLDP_CHASSIS_SUBTYPE_MAC_ADDRESS:
        s_chassis_id = nm_utils_hwaddr_ntoa(chassis_id, chassis_id_len);
        break;
    case SD_LLDP_CHASSIS_SUBTYPE_NETWORK_ADDRESS:
        s_chassis_id = format_network_address(chassis_id, chassis_id_len);
        break;
    }
    if (!s_chassis_id) {
        /* Invalid/unsupported chassis_id? Expose as hex string. This format is not stable, and
         * in the future we may add a better string representation for these case (thus
         * changing the API). */
        s_chassis_id = nm_utils_bin2hexstr_full(chassis_id, chassis_id_len, '\0', FALSE, NULL);
    }

    switch (port_id_type) {
    case SD_LLDP_PORT_SUBTYPE_INTERFACE_ALIAS:
    case SD_LLDP_PORT_SUBTYPE_PORT_COMPONENT:
    case SD_LLDP_PORT_SUBTYPE_INTERFACE_NAME:
    case SD_LLDP_PORT_SUBTYPE_LOCALLY_ASSIGNED:
        s_port_id = format_string_cp(port_id, port_id_len, FALSE);
        break;
    case SD_LLDP_PORT_SUBTYPE_MAC_ADDRESS:
        s_port_id = nm_utils_hwaddr_ntoa(port_id, port_id_len);
        break;
    case SD_LLDP_PORT_SUBTYPE_NETWORK_ADDRESS:
        s_port_id = format_network_address(port_id, port_id_len);
        break;
    }
    if (!s_port_id) {
        /* Invalid/unsupported port_id? Expose as hex string. This format is not stable, and
         * in the future we may add a better string representation for these case (thus
         * changing the API). */
        s_port_id = nm_utils_bin2hexstr_full(port_id, port_id_len, '\0', FALSE, NULL);
    }

    neigh  = g_slice_new(LldpNeighbor);
    *neigh = (LldpNeighbor){
        .neighbor_sd     = sd_lldp_neighbor_ref(neighbor_sd),
        .chassis_id_type = chassis_id_type,
        .chassis_id      = g_steal_pointer(&s_chassis_id),
        .port_id_type    = port_id_type,
        .port_id         = g_steal_pointer(&s_port_id),
    };
    return neigh;
}

static GVariant *
lldp_neighbor_to_variant(LldpNeighbor *neigh)
{
    struct ether_addr destination_address;
    GVariantBuilder   builder;
    const char *      str;
    const guint8 *    raw_data;
    gsize             raw_len;
    uint16_t          u16;
    uint8_t *         data8;
    gsize             len;
    int               r;

    if (neigh->variant)
        return neigh->variant;

    lldp_neighbor_get_raw(neigh, &raw_data, &raw_len);

    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));

    nm_g_variant_builder_add_sv_bytearray(&builder, NM_LLDP_ATTR_RAW, raw_data, raw_len);
    nm_g_variant_builder_add_sv_uint32(&builder,
                                       NM_LLDP_ATTR_CHASSIS_ID_TYPE,
                                       neigh->chassis_id_type);
    nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_CHASSIS_ID, neigh->chassis_id);
    nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_PORT_ID_TYPE, neigh->port_id_type);
    nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_ID, neigh->port_id);

    r = sd_lldp_neighbor_get_destination_address(neigh->neighbor_sd, &destination_address);
    if (r < 0)
        str = NULL;
    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_BRIDGE))
        str = NM_LLDP_DEST_NEAREST_BRIDGE;
    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_NON_TPMR_BRIDGE))
        str = NM_LLDP_DEST_NEAREST_NON_TPMR_BRIDGE;
    else if (nm_utils_ether_addr_equal(&destination_address, LLDP_MAC_NEAREST_CUSTOMER_BRIDGE))
        str = NM_LLDP_DEST_NEAREST_CUSTOMER_BRIDGE;
    else
        str = NULL;
    if (str)
        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_DESTINATION, str);

    if (sd_lldp_neighbor_get_port_description(neigh->neighbor_sd, &str) == 0)
        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_PORT_DESCRIPTION, str);

    if (sd_lldp_neighbor_get_system_name(neigh->neighbor_sd, &str) == 0)
        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_NAME, str);

    if (sd_lldp_neighbor_get_system_description(neigh->neighbor_sd, &str) == 0)
        nm_g_variant_builder_add_sv_str(&builder, NM_LLDP_ATTR_SYSTEM_DESCRIPTION, str);

    if (sd_lldp_neighbor_get_system_capabilities(neigh->neighbor_sd, &u16) == 0)
        nm_g_variant_builder_add_sv_uint32(&builder, NM_LLDP_ATTR_SYSTEM_CAPABILITIES, u16);

    r = sd_lldp_neighbor_tlv_rewind(neigh->neighbor_sd);
    if (r < 0)
        nm_assert_not_reached();
    else {
        gboolean        v_management_addresses_has = FALSE;
        GVariantBuilder v_management_addresses;
        GVariant *      v_ieee_802_1_pvid        = NULL;
        GVariant *      v_ieee_802_1_ppvid       = NULL;
        GVariant *      v_ieee_802_1_ppvid_flags = NULL;
        GVariantBuilder v_ieee_802_1_ppvids;
        GVariant *      v_ieee_802_1_vid       = NULL;
        GVariant *      v_ieee_802_1_vlan_name = NULL;
        GVariantBuilder v_ieee_802_1_vlans;
        GVariant *      v_ieee_802_3_mac_phy_conf   = NULL;
        GVariant *      v_ieee_802_3_power_via_mdi  = NULL;
        GVariant *      v_ieee_802_3_max_frame_size = NULL;
        GVariant *      v_mud_url                   = NULL;
        GVariantBuilder tmp_builder;
        GVariant *      tmp_variant;

        do {
            guint8 oui[3];
            guint8 type;
            guint8 subtype;

            if (sd_lldp_neighbor_tlv_get_type(neigh->neighbor_sd, &type) < 0)
                continue;

            if (sd_lldp_neighbor_tlv_get_raw(neigh->neighbor_sd, (void *) &data8, &len) < 0)
                continue;

            switch (type) {
            case SD_LLDP_TYPE_MGMT_ADDRESS:
                tmp_variant = parse_management_address_tlv(data8, len);
                if (tmp_variant) {
                    if (!v_management_addresses_has) {
                        v_management_addresses_has = TRUE;
                        g_variant_builder_init(&v_management_addresses, G_VARIANT_TYPE("aa{sv}"));
                    }
                    g_variant_builder_add_value(&v_management_addresses, tmp_variant);
                }
                continue;
            case SD_LLDP_TYPE_PRIVATE:
                break;
            default:
                continue;
            }

            r = sd_lldp_neighbor_tlv_get_oui(neigh->neighbor_sd, oui, &subtype);
            if (r < 0) {
                if (r == -ENXIO)
                    continue;

                /* in other cases, something is seriously wrong. Abort, but
                 * keep what we parsed so far. */
                break;
            }

            if (len <= 6)
                continue;

                /* skip over leading TLV, OUI and subtype */
#if NM_MORE_ASSERTS > 5
            {
                guint8 check_hdr[] = {0xfe | (((len - 2) >> 8) & 0x01),
                                      ((len - 2) & 0xFF),
                                      oui[0],
                                      oui[1],
                                      oui[2],
                                      subtype};

                nm_assert(len > 2 + 3 + 1);
                nm_assert(memcmp(data8, check_hdr, sizeof check_hdr) == 0);
            }
#endif
            data8 += 6;
            len -= 6;

            if (memcmp(oui, SD_LLDP_OUI_802_1, sizeof(oui)) == 0) {
                switch (subtype) {
                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_VLAN_ID:
                    if (len != 2)
                        continue;
                    if (!v_ieee_802_1_pvid)
                        v_ieee_802_1_pvid = g_variant_new_uint32(unaligned_read_be16(data8));
                    break;
                case SD_LLDP_OUI_802_1_SUBTYPE_PORT_PROTOCOL_VLAN_ID:
                    if (len != 3)
                        continue;
                    if (!v_ieee_802_1_ppvid) {
                        v_ieee_802_1_ppvid_flags = g_variant_new_uint32(data8[0]);
                        v_ieee_802_1_ppvid = g_variant_new_uint32(unaligned_read_be16(&data8[1]));
                        g_variant_builder_init(&v_ieee_802_1_ppvids, G_VARIANT_TYPE("aa{sv}"));
                    }
                    g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
                    nm_g_variant_builder_add_sv_uint32(&tmp_builder,
                                                       "ppvid",
                                                       unaligned_read_be16(&data8[1]));
                    nm_g_variant_builder_add_sv_uint32(&tmp_builder, "flags", data8[0]);
                    g_variant_builder_add_value(&v_ieee_802_1_ppvids,
                                                g_variant_builder_end(&tmp_builder));
                    break;
                case SD_LLDP_OUI_802_1_SUBTYPE_VLAN_NAME:
                {
                    gs_free char *name_to_free = NULL;
                    const char *  name;
                    guint32       vid;
                    gsize         l;

                    if (len <= 3)
                        continue;

                    l = data8[2];
                    if (len != 3 + l)
                        continue;
                    if (l > 32)
                        continue;

                    name = format_string(&data8[3], l, TRUE, &name_to_free);
                    if (!name)
                        continue;

                    vid = unaligned_read_be16(&data8[0]);
                    if (!v_ieee_802_1_vid) {
                        v_ieee_802_1_vid       = g_variant_new_uint32(vid);
                        v_ieee_802_1_vlan_name = g_variant_new_string(name);
                        g_variant_builder_init(&v_ieee_802_1_vlans, G_VARIANT_TYPE("aa{sv}"));
                    }
                    g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
                    nm_g_variant_builder_add_sv_uint32(&tmp_builder, "vid", vid);
                    nm_g_variant_builder_add_sv_str(&tmp_builder, "name", name);
                    g_variant_builder_add_value(&v_ieee_802_1_vlans,
                                                g_variant_builder_end(&tmp_builder));
                    break;
                }
                default:
                    continue;
                }
            } else if (memcmp(oui, SD_LLDP_OUI_802_3, sizeof(oui)) == 0) {
                switch (subtype) {
                case SD_LLDP_OUI_802_3_SUBTYPE_MAC_PHY_CONFIG_STATUS:
                    if (len != 5)
                        continue;

                    if (!v_ieee_802_3_mac_phy_conf) {
                        g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
                        nm_g_variant_builder_add_sv_uint32(&tmp_builder, "autoneg", data8[0]);
                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
                                                           "pmd-autoneg-cap",
                                                           unaligned_read_be16(&data8[1]));
                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
                                                           "operational-mau-type",
                                                           unaligned_read_be16(&data8[3]));
                        v_ieee_802_3_mac_phy_conf = g_variant_builder_end(&tmp_builder);
                    }
                    break;
                case SD_LLDP_OUI_802_3_SUBTYPE_POWER_VIA_MDI:
                    if (len != 3)
                        continue;

                    if (!v_ieee_802_3_power_via_mdi) {
                        g_variant_builder_init(&tmp_builder, G_VARIANT_TYPE("a{sv}"));
                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
                                                           "mdi-power-support",
                                                           data8[0]);
                        nm_g_variant_builder_add_sv_uint32(&tmp_builder,
                                                           "pse-power-pair",
                                                           data8[1]);
                        nm_g_variant_builder_add_sv_uint32(&tmp_builder, "power-class", data8[2]);
                        v_ieee_802_3_power_via_mdi = g_variant_builder_end(&tmp_builder);
                    }
                    break;
                case SD_LLDP_OUI_802_3_SUBTYPE_MAXIMUM_FRAME_SIZE:
                    if (len != 2)
                        continue;
                    if (!v_ieee_802_3_max_frame_size)
                        v_ieee_802_3_max_frame_size =
                            g_variant_new_uint32(unaligned_read_be16(data8));
                    break;
                }
            } else if (memcmp(oui, SD_LLDP_OUI_MUD, sizeof(oui)) == 0) {
                switch (subtype) {
                case SD_LLDP_OUI_SUBTYPE_MUD_USAGE_DESCRIPTION:
                    if (!v_mud_url) {
                        gs_free char *s_free = NULL;
                        const char *  s;

                        s = format_string(data8, len, TRUE, &s_free);
                        if (s)
                            v_mud_url = g_variant_new_string(s);
                    }
                    break;
                }
            }
        } while (sd_lldp_neighbor_tlv_next(neigh->neighbor_sd) > 0);

        if (v_management_addresses_has)
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_MANAGEMENT_ADDRESSES,
                                        g_variant_builder_end(&v_management_addresses));
        if (v_ieee_802_1_pvid)
            nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_IEEE_802_1_PVID, v_ieee_802_1_pvid);
        if (v_ieee_802_1_ppvid) {
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_1_PPVID,
                                        v_ieee_802_1_ppvid);
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_1_PPVID_FLAGS,
                                        v_ieee_802_1_ppvid_flags);
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_1_PPVIDS,
                                        g_variant_builder_end(&v_ieee_802_1_ppvids));
        }
        if (v_ieee_802_1_vid) {
            nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_IEEE_802_1_VID, v_ieee_802_1_vid);
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_1_VLAN_NAME,
                                        v_ieee_802_1_vlan_name);
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_1_VLANS,
                                        g_variant_builder_end(&v_ieee_802_1_vlans));
        }
        if (v_ieee_802_3_mac_phy_conf)
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_3_MAC_PHY_CONF,
                                        v_ieee_802_3_mac_phy_conf);
        if (v_ieee_802_3_power_via_mdi)
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_3_POWER_VIA_MDI,
                                        v_ieee_802_3_power_via_mdi);
        if (v_ieee_802_3_max_frame_size)
            nm_g_variant_builder_add_sv(&builder,
                                        NM_LLDP_ATTR_IEEE_802_3_MAX_FRAME_SIZE,
                                        v_ieee_802_3_max_frame_size);
        if (v_mud_url)
            nm_g_variant_builder_add_sv(&builder, NM_LLDP_ATTR_MUD_URL, v_mud_url);
    }

    return (neigh->variant = g_variant_ref_sink(g_variant_builder_end(&builder)));
}

/*****************************************************************************/

GVariant *
nmtst_lldp_parse_from_raw(const guint8 *raw_data, gsize raw_len)
{
    nm_auto(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *neighbor_sd = NULL;
    nm_auto(lldp_neighbor_freep) LldpNeighbor *        neigh       = NULL;
    GVariant *                                         variant;
    int                                                r;

    g_assert(raw_data);
    g_assert(raw_len > 0);

    r = sd_lldp_neighbor_from_raw(&neighbor_sd, raw_data, raw_len);
    g_assert(r >= 0);

    neigh = lldp_neighbor_new(neighbor_sd);
    g_assert(neigh);

    variant = lldp_neighbor_to_variant(neigh);
    g_assert(variant);

    return g_variant_ref(variant);
}

/*****************************************************************************/

static void
data_changed_notify(NMLldpListener *self, NMLldpListenerPrivate *priv)
{
    nm_clear_g_variant(&priv->variant);
    _notify(self, PROP_NEIGHBORS);
}

static gboolean
data_changed_timeout(gpointer user_data)
{
    NMLldpListener *       self = user_data;
    NMLldpListenerPrivate *priv;

    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), G_SOURCE_REMOVE);

    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);

    priv->ratelimit_id        = 0;
    priv->ratelimit_next_nsec = nm_utils_get_monotonic_timestamp_nsec() + MIN_UPDATE_INTERVAL_NSEC;
    data_changed_notify(self, priv);
    return G_SOURCE_REMOVE;
}

static void
data_changed_schedule(NMLldpListener *self)
{
    NMLldpListenerPrivate *priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
    gint64                 now_nsec;

    if (priv->ratelimit_id != 0)
        return;

    now_nsec = nm_utils_get_monotonic_timestamp_nsec();
    if (now_nsec < priv->ratelimit_next_nsec) {
        priv->ratelimit_id =
            g_timeout_add_full(G_PRIORITY_LOW,
                               NM_UTILS_NSEC_TO_MSEC_CEIL(priv->ratelimit_next_nsec - now_nsec),
                               data_changed_timeout,
                               self,
                               NULL);
        return;
    }

    priv->ratelimit_id = g_idle_add_full(G_PRIORITY_LOW, data_changed_timeout, self, NULL);
}

static void
process_lldp_neighbor(NMLldpListener *self, sd_lldp_neighbor *neighbor_sd, gboolean remove)
{
    NMLldpListenerPrivate *                    priv;
    nm_auto(lldp_neighbor_freep) LldpNeighbor *neigh = NULL;
    LldpNeighbor *                             neigh_old;

    g_return_if_fail(NM_IS_LLDP_LISTENER(self));

    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);

    g_return_if_fail(priv->lldp_handle);
    g_return_if_fail(neighbor_sd);

    nm_assert(priv->lldp_neighbors);

    neigh = lldp_neighbor_new(neighbor_sd);
    if (!neigh) {
        _LOGT("process: failed to parse neighbor");
        return;
    }

    neigh_old = g_hash_table_lookup(priv->lldp_neighbors, neigh);

    if (remove) {
        if (neigh_old) {
            _LOGT("process: %s neigh: " LOG_NEIGH_FMT, "remove", LOG_NEIGH_ARG(neigh));

            g_hash_table_remove(priv->lldp_neighbors, neigh_old);
            goto handle_changed;
        }
        return;
    }

    if (neigh_old && lldp_neighbor_equal(neigh_old, neigh))
        return;

    _LOGD("process: %s neigh: " LOG_NEIGH_FMT, neigh_old ? "update" : "new", LOG_NEIGH_ARG(neigh));

    g_hash_table_add(priv->lldp_neighbors, g_steal_pointer(&neigh));

handle_changed:
    data_changed_schedule(self);
}

static void
lldp_event_handler(sd_lldp *lldp, sd_lldp_event event, sd_lldp_neighbor *n, void *userdata)
{
    process_lldp_neighbor(
        userdata,
        n,
        !NM_IN_SET(event, SD_LLDP_EVENT_ADDED, SD_LLDP_EVENT_UPDATED, SD_LLDP_EVENT_REFRESHED));
}

gboolean
nm_lldp_listener_start(NMLldpListener *self, int ifindex, GError **error)
{
    NMLldpListenerPrivate *priv;
    int                    ret;

    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE);
    g_return_val_if_fail(ifindex > 0, FALSE);
    g_return_val_if_fail(!error || !*error, FALSE);

    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);

    if (priv->lldp_handle) {
        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "already running");
        return FALSE;
    }

    ret = sd_lldp_new(&priv->lldp_handle);
    if (ret < 0) {
        g_set_error_literal(error,
                            NM_DEVICE_ERROR,
                            NM_DEVICE_ERROR_FAILED,
                            "initialization failed");
        return FALSE;
    }

    ret = sd_lldp_set_ifindex(priv->lldp_handle, ifindex);
    if (ret < 0) {
        g_set_error_literal(error,
                            NM_DEVICE_ERROR,
                            NM_DEVICE_ERROR_FAILED,
                            "failed setting ifindex");
        goto err;
    }

    ret = sd_lldp_set_callback(priv->lldp_handle, lldp_event_handler, self);
    if (ret < 0) {
        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "set callback failed");
        goto err;
    }

    ret = sd_lldp_set_neighbors_max(priv->lldp_handle, MAX_NEIGHBORS);
    nm_assert(ret == 0);

    priv->ifindex = ifindex;

    ret = sd_lldp_attach_event(priv->lldp_handle, NULL, 0);
    if (ret < 0) {
        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "attach event failed");
        goto err_free;
    }

    ret = sd_lldp_start(priv->lldp_handle);
    if (ret < 0) {
        g_set_error_literal(error, NM_DEVICE_ERROR, NM_DEVICE_ERROR_FAILED, "start failed");
        goto err;
    }

    priv->lldp_neighbors = g_hash_table_new_full(lldp_neighbor_id_hash,
                                                 lldp_neighbor_id_equal,
                                                 (GDestroyNotify) lldp_neighbor_free,
                                                 NULL);

    _LOGD("start");

    return TRUE;

err:
    sd_lldp_detach_event(priv->lldp_handle);
err_free:
    sd_lldp_unref(priv->lldp_handle);
    priv->lldp_handle = NULL;
    priv->ifindex     = 0;
    return FALSE;
}

void
nm_lldp_listener_stop(NMLldpListener *self)
{
    NMLldpListenerPrivate *priv;
    guint                  size;
    gboolean               changed = FALSE;

    g_return_if_fail(NM_IS_LLDP_LISTENER(self));
    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);

    if (priv->lldp_handle) {
        _LOGD("stop");
        sd_lldp_stop(priv->lldp_handle);
        sd_lldp_detach_event(priv->lldp_handle);
        sd_lldp_unref(priv->lldp_handle);
        priv->lldp_handle = NULL;

        size = g_hash_table_size(priv->lldp_neighbors);
        g_hash_table_remove_all(priv->lldp_neighbors);
        nm_clear_pointer(&priv->lldp_neighbors, g_hash_table_unref);
        if (size > 0 || priv->ratelimit_id != 0)
            changed = TRUE;
    }

    nm_clear_g_source(&priv->ratelimit_id);
    priv->ratelimit_next_nsec = 0;
    priv->ifindex             = 0;

    if (changed)
        data_changed_notify(self, priv);
}

gboolean
nm_lldp_listener_is_running(NMLldpListener *self)
{
    NMLldpListenerPrivate *priv;

    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE);

    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);
    return !!priv->lldp_handle;
}

GVariant *
nm_lldp_listener_get_neighbors(NMLldpListener *self)
{
    NMLldpListenerPrivate *priv;

    g_return_val_if_fail(NM_IS_LLDP_LISTENER(self), FALSE);

    priv = NM_LLDP_LISTENER_GET_PRIVATE(self);

    if (G_UNLIKELY(!priv->variant)) {
        gs_free LldpNeighbor **neighbors = NULL;
        GVariantBuilder        array_builder;
        guint                  i, n;

        g_variant_builder_init(&array_builder, G_VARIANT_TYPE("aa{sv}"));
        neighbors = (LldpNeighbor **)
            nm_utils_hash_keys_to_array(priv->lldp_neighbors, lldp_neighbor_id_cmp_p, NULL, &n);
        for (i = 0; i < n; i++)
            g_variant_builder_add_value(&array_builder, lldp_neighbor_to_variant(neighbors[i]));
        priv->variant = g_variant_ref_sink(g_variant_builder_end(&array_builder));
    }
    return priv->variant;
}

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMLldpListener *self = NM_LLDP_LISTENER(object);

    switch (prop_id) {
    case PROP_NEIGHBORS:
        g_value_set_variant(value, nm_lldp_listener_get_neighbors(self));
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
nm_lldp_listener_init(NMLldpListener *self)
{
    _LOGT("lldp listener created");
}

NMLldpListener *
nm_lldp_listener_new(void)
{
    return g_object_new(NM_TYPE_LLDP_LISTENER, NULL);
}

static void
dispose(GObject *object)
{
    nm_lldp_listener_stop(NM_LLDP_LISTENER(object));

    G_OBJECT_CLASS(nm_lldp_listener_parent_class)->dispose(object);
}

static void
finalize(GObject *object)
{
    NMLldpListener *       self = NM_LLDP_LISTENER(object);
    NMLldpListenerPrivate *priv = NM_LLDP_LISTENER_GET_PRIVATE(self);

    nm_lldp_listener_stop(self);

    nm_clear_g_variant(&priv->variant);

    _LOGT("lldp listener destroyed");

    G_OBJECT_CLASS(nm_lldp_listener_parent_class)->finalize(object);
}

static void
nm_lldp_listener_class_init(NMLldpListenerClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    object_class->dispose      = dispose;
    object_class->finalize     = finalize;
    object_class->get_property = get_property;

    obj_properties[PROP_NEIGHBORS] =
        g_param_spec_variant(NM_LLDP_LISTENER_NEIGHBORS,
                             "",
                             "",
                             G_VARIANT_TYPE("aa{sv}"),
                             NULL,
                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);
}