/* SPDX-License-Identifier: LGPL-2.1-or-later */ #include "libnm-glib-aux/nm-default-glib-i18n-lib.h" #include "nm-lldp-neighbor.h" #include #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(lldp_rx); 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(NMLldpRX *lldp_rx, NMLldpNeighbor *n) { struct ether_header h; const uint8_t *p; size_t left; int r; nm_assert(lldp_rx); nm_assert(n); nm_assert(!n->lldp_rx); if (n->raw_size < sizeof(struct ether_header)) { _LOG2D(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(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(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(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(lldp_rx, "TLV truncated, ignoring datagram."); return -NME_UNSPEC; } switch (type) { case NM_LLDP_TYPE_END: if (length != 0) { _LOG2D(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(lldp_rx, "Chassis ID field size out of range, ignoring datagram."); return -NME_UNSPEC; } if (n->id.chassis_id) { _LOG2D(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(lldp_rx, "Port ID field size out of range, ignoring datagram."); return -NME_UNSPEC; } if (n->id.port_id) { _LOG2D(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(lldp_rx, "TTL field has wrong size, ignoring datagram."); return -NME_UNSPEC; } if (n->has_ttl) { _LOG2D(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(lldp_rx, &n->port_description, p, length); if (r < 0) return r; break; case NM_LLDP_TYPE_SYSTEM_NAME: r = parse_string(lldp_rx, &n->system_name, p, length); if (r < 0) return r; break; case NM_LLDP_TYPE_SYSTEM_DESCRIPTION: r = parse_string(lldp_rx, &n->system_description, p, length); if (r < 0) return r; break; case NM_LLDP_TYPE_SYSTEM_CAPABILITIES: if (length != 4) { _LOG2D(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(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(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(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(subtype, -EINVAL); nm_assert(oui); 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; n->prioq_idx = NM_PRIOQ_IDX_NULL; return n; } NMLldpNeighbor * nm_lldp_neighbor_new_from_raw(NMLldpRX *lldp_rx, 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(lldp_rx, 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; }