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