diff options
Diffstat (limited to 'src/libsystemd-network/sd-lldp.c')
-rw-r--r-- | src/libsystemd-network/sd-lldp.c | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/src/libsystemd-network/sd-lldp.c b/src/libsystemd-network/sd-lldp.c new file mode 100644 index 0000000..969fc71 --- /dev/null +++ b/src/libsystemd-network/sd-lldp.c @@ -0,0 +1,496 @@ +/* SPDX-License-Identifier: LGPL-2.1+ */ + +#include <arpa/inet.h> +#include <linux/sockios.h> +#include <sys/ioctl.h> + +#include "sd-lldp.h" + +#include "alloc-util.h" +#include "ether-addr-util.h" +#include "event-util.h" +#include "fd-util.h" +#include "lldp-internal.h" +#include "lldp-neighbor.h" +#include "lldp-network.h" +#include "socket-util.h" +#include "string-table.h" + +#define LLDP_DEFAULT_NEIGHBORS_MAX 128U + +static const char * const lldp_event_table[_SD_LLDP_EVENT_MAX] = { + [SD_LLDP_EVENT_ADDED] = "added", + [SD_LLDP_EVENT_REMOVED] = "removed", + [SD_LLDP_EVENT_UPDATED] = "updated", + [SD_LLDP_EVENT_REFRESHED] = "refreshed", +}; + +DEFINE_STRING_TABLE_LOOKUP(lldp_event, sd_lldp_event); + +static void lldp_flush_neighbors(sd_lldp *lldp) { + assert(lldp); + + hashmap_clear(lldp->neighbor_by_id); +} + +static void lldp_callback(sd_lldp *lldp, sd_lldp_event event, sd_lldp_neighbor *n) { + assert(lldp); + assert(event >= 0 && event < _SD_LLDP_EVENT_MAX); + + if (!lldp->callback) { + log_lldp("Received '%s' event.", lldp_event_to_string(event)); + return; + } + + log_lldp("Invoking callback for '%s' event.", lldp_event_to_string(event)); + lldp->callback(lldp, event, n, lldp->userdata); +} + +static int lldp_make_space(sd_lldp *lldp, size_t extra) { + usec_t t = USEC_INFINITY; + bool changed = false; + + assert(lldp); + + /* Remove all entries that are past their TTL, and more until at least the specified number of extra entries + * are free. */ + + for (;;) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL; + + n = prioq_peek(lldp->neighbor_by_expiry); + if (!n) + break; + + sd_lldp_neighbor_ref(n); + + if (hashmap_size(lldp->neighbor_by_id) > LESS_BY(lldp->neighbors_max, extra)) + goto remove_one; + + if (t == USEC_INFINITY) + t = now(clock_boottime_or_monotonic()); + + if (n->until > t) + break; + + remove_one: + lldp_neighbor_unlink(n); + lldp_callback(lldp, SD_LLDP_EVENT_REMOVED, n); + changed = true; + } + + return changed; +} + +static bool lldp_keep_neighbor(sd_lldp *lldp, sd_lldp_neighbor *n) { + assert(lldp); + assert(n); + + /* Don't keep data with a zero TTL */ + if (n->ttl <= 0) + return false; + + /* Filter out data from the filter address */ + if (!ether_addr_is_null(&lldp->filter_address) && + ether_addr_equal(&lldp->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->capability_mask) == 0) + return false; + + /* Keep everything else */ + return true; +} + +static int lldp_start_timer(sd_lldp *lldp, sd_lldp_neighbor *neighbor); + +static int lldp_add_neighbor(sd_lldp *lldp, sd_lldp_neighbor *n) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *old = NULL; + bool keep; + int r; + + assert(lldp); + assert(n); + assert(!n->lldp); + + keep = lldp_keep_neighbor(lldp, n); + + /* First retrieve the old entry for this MSAP */ + old = hashmap_get(lldp->neighbor_by_id, &n->id); + if (old) { + sd_lldp_neighbor_ref(old); + + if (!keep) { + lldp_neighbor_unlink(old); + lldp_callback(lldp, SD_LLDP_EVENT_REMOVED, old); + return 0; + } + + if (lldp_neighbor_equal(n, old)) { + /* Is this equal, then restart the TTL counter, but don't do anything else. */ + old->timestamp = n->timestamp; + lldp_start_timer(lldp, old); + lldp_callback(lldp, SD_LLDP_EVENT_REFRESHED, old); + return 0; + } + + /* Data changed, remove the old entry, and add a new one */ + lldp_neighbor_unlink(old); + + } else if (!keep) + return 0; + + /* Then, make room for at least one new neighbor */ + lldp_make_space(lldp, 1); + + r = hashmap_put(lldp->neighbor_by_id, &n->id, n); + if (r < 0) + goto finish; + + r = prioq_put(lldp->neighbor_by_expiry, n, &n->prioq_idx); + if (r < 0) { + assert_se(hashmap_remove(lldp->neighbor_by_id, &n->id) == n); + goto finish; + } + + n->lldp = lldp; + + lldp_start_timer(lldp, n); + lldp_callback(lldp, old ? SD_LLDP_EVENT_UPDATED : SD_LLDP_EVENT_ADDED, n); + + return 1; + +finish: + if (old) + lldp_callback(lldp, SD_LLDP_EVENT_REMOVED, old); + + return r; +} + +static int lldp_handle_datagram(sd_lldp *lldp, sd_lldp_neighbor *n) { + int r; + + assert(lldp); + assert(n); + + r = lldp_neighbor_parse(n); + if (r == -EBADMSG) /* Ignore bad messages */ + return 0; + if (r < 0) + return r; + + r = lldp_add_neighbor(lldp, n); + if (r < 0) { + log_lldp_errno(r, "Failed to add datagram. Ignoring."); + return 0; + } + + log_lldp("Successfully processed LLDP datagram."); + return 0; +} + +static int lldp_receive_datagram(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(sd_lldp_neighbor_unrefp) sd_lldp_neighbor *n = NULL; + ssize_t space, length; + sd_lldp *lldp = userdata; + struct timespec ts; + + assert(fd >= 0); + assert(lldp); + + space = next_datagram_size_fd(fd); + if (space < 0) + return log_lldp_errno(space, "Failed to determine datagram size to read: %m"); + + n = lldp_neighbor_new(space); + if (!n) + return -ENOMEM; + + length = recv(fd, LLDP_NEIGHBOR_RAW(n), n->raw_size, MSG_DONTWAIT); + if (length < 0) { + if (IN_SET(errno, EAGAIN, EINTR)) + return 0; + + return log_lldp_errno(errno, "Failed to read LLDP datagram: %m"); + } + + if ((size_t) length != n->raw_size) { + log_lldp("Packet size mismatch."); + return -EINVAL; + } + + /* Try to get the timestamp of this packet if it is known */ + if (ioctl(fd, SIOCGSTAMPNS, &ts) >= 0) + triple_timestamp_from_realtime(&n->timestamp, timespec_load(&ts)); + else + triple_timestamp_get(&n->timestamp); + + return lldp_handle_datagram(lldp, n); +} + +static void lldp_reset(sd_lldp *lldp) { + assert(lldp); + + (void) event_source_disable(lldp->timer_event_source); + lldp->io_event_source = sd_event_source_unref(lldp->io_event_source); + lldp->fd = safe_close(lldp->fd); +} + +_public_ int sd_lldp_start(sd_lldp *lldp) { + int r; + + assert_return(lldp, -EINVAL); + assert_return(lldp->event, -EINVAL); + assert_return(lldp->ifindex > 0, -EINVAL); + + if (lldp->fd >= 0) + return 0; + + assert(!lldp->io_event_source); + + lldp->fd = lldp_network_bind_raw_socket(lldp->ifindex); + if (lldp->fd < 0) + return lldp->fd; + + r = sd_event_add_io(lldp->event, &lldp->io_event_source, lldp->fd, EPOLLIN, lldp_receive_datagram, lldp); + if (r < 0) + goto fail; + + r = sd_event_source_set_priority(lldp->io_event_source, lldp->event_priority); + if (r < 0) + goto fail; + + (void) sd_event_source_set_description(lldp->io_event_source, "lldp-io"); + + log_lldp("Started LLDP client"); + return 1; + +fail: + lldp_reset(lldp); + return r; +} + +_public_ int sd_lldp_stop(sd_lldp *lldp) { + assert_return(lldp, -EINVAL); + + if (lldp->fd < 0) + return 0; + + log_lldp("Stopping LLDP client"); + + lldp_reset(lldp); + lldp_flush_neighbors(lldp); + + return 1; +} + +_public_ int sd_lldp_attach_event(sd_lldp *lldp, sd_event *event, int64_t priority) { + int r; + + assert_return(lldp, -EINVAL); + assert_return(lldp->fd < 0, -EBUSY); + assert_return(!lldp->event, -EBUSY); + + if (event) + lldp->event = sd_event_ref(event); + else { + r = sd_event_default(&lldp->event); + if (r < 0) + return r; + } + + lldp->event_priority = priority; + + return 0; +} + +_public_ int sd_lldp_detach_event(sd_lldp *lldp) { + + assert_return(lldp, -EINVAL); + assert_return(lldp->fd < 0, -EBUSY); + + lldp->event = sd_event_unref(lldp->event); + return 0; +} + +_public_ sd_event* sd_lldp_get_event(sd_lldp *lldp) { + assert_return(lldp, NULL); + + return lldp->event; +} + +_public_ int sd_lldp_set_callback(sd_lldp *lldp, sd_lldp_callback_t cb, void *userdata) { + assert_return(lldp, -EINVAL); + + lldp->callback = cb; + lldp->userdata = userdata; + + return 0; +} + +_public_ int sd_lldp_set_ifindex(sd_lldp *lldp, int ifindex) { + assert_return(lldp, -EINVAL); + assert_return(ifindex > 0, -EINVAL); + assert_return(lldp->fd < 0, -EBUSY); + + lldp->ifindex = ifindex; + return 0; +} + +static sd_lldp* lldp_free(sd_lldp *lldp) { + assert(lldp); + + lldp->timer_event_source = sd_event_source_unref(lldp->timer_event_source); + + lldp_reset(lldp); + sd_lldp_detach_event(lldp); + lldp_flush_neighbors(lldp); + + hashmap_free(lldp->neighbor_by_id); + prioq_free(lldp->neighbor_by_expiry); + return mfree(lldp); +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(sd_lldp, sd_lldp, lldp_free); + +_public_ int sd_lldp_new(sd_lldp **ret) { + _cleanup_(sd_lldp_unrefp) sd_lldp *lldp = NULL; + int r; + + assert_return(ret, -EINVAL); + + lldp = new(sd_lldp, 1); + if (!lldp) + return -ENOMEM; + + *lldp = (sd_lldp) { + .n_ref = 1, + .fd = -1, + .neighbors_max = LLDP_DEFAULT_NEIGHBORS_MAX, + .capability_mask = (uint16_t) -1, + }; + + lldp->neighbor_by_id = hashmap_new(&lldp_neighbor_hash_ops); + if (!lldp->neighbor_by_id) + return -ENOMEM; + + r = prioq_ensure_allocated(&lldp->neighbor_by_expiry, lldp_neighbor_prioq_compare_func); + if (r < 0) + return r; + + *ret = TAKE_PTR(lldp); + + return 0; +} + +static int neighbor_compare_func(sd_lldp_neighbor * const *a, sd_lldp_neighbor * const *b) { + return lldp_neighbor_id_compare_func(&(*a)->id, &(*b)->id); +} + +static int on_timer_event(sd_event_source *s, uint64_t usec, void *userdata) { + sd_lldp *lldp = userdata; + int r; + + r = lldp_make_space(lldp, 0); + if (r < 0) + return log_lldp_errno(r, "Failed to make space: %m"); + + r = lldp_start_timer(lldp, NULL); + if (r < 0) + return log_lldp_errno(r, "Failed to restart timer: %m"); + + return 0; +} + +static int lldp_start_timer(sd_lldp *lldp, sd_lldp_neighbor *neighbor) { + sd_lldp_neighbor *n; + + assert(lldp); + + if (neighbor) + lldp_neighbor_start_ttl(neighbor); + + n = prioq_peek(lldp->neighbor_by_expiry); + if (!n) + return event_source_disable(lldp->timer_event_source); + + if (!lldp->event) + return 0; + + return event_reset_time(lldp->event, &lldp->timer_event_source, + clock_boottime_or_monotonic(), + n->until, 0, + on_timer_event, lldp, + lldp->event_priority, "lldp-timer", true); +} + +_public_ int sd_lldp_get_neighbors(sd_lldp *lldp, sd_lldp_neighbor ***ret) { + sd_lldp_neighbor **l = NULL, *n; + Iterator i; + int k = 0, r; + + assert_return(lldp, -EINVAL); + assert_return(ret, -EINVAL); + + if (hashmap_isempty(lldp->neighbor_by_id)) { /* Special shortcut */ + *ret = NULL; + return 0; + } + + l = new0(sd_lldp_neighbor*, hashmap_size(lldp->neighbor_by_id)); + if (!l) + return -ENOMEM; + + r = lldp_start_timer(lldp, NULL); + if (r < 0) { + free(l); + return r; + } + + HASHMAP_FOREACH(n, lldp->neighbor_by_id, i) + l[k++] = sd_lldp_neighbor_ref(n); + + assert((size_t) k == hashmap_size(lldp->neighbor_by_id)); + + /* Return things in a stable order */ + typesafe_qsort(l, k, neighbor_compare_func); + *ret = l; + + return k; +} + +_public_ int sd_lldp_set_neighbors_max(sd_lldp *lldp, uint64_t m) { + assert_return(lldp, -EINVAL); + assert_return(m <= 0, -EINVAL); + + lldp->neighbors_max = m; + lldp_make_space(lldp, 0); + + return 0; +} + +_public_ int sd_lldp_match_capabilities(sd_lldp *lldp, uint16_t mask) { + assert_return(lldp, -EINVAL); + assert_return(mask != 0, -EINVAL); + + lldp->capability_mask = mask; + + return 0; +} + +_public_ int sd_lldp_set_filter_address(sd_lldp *lldp, const struct ether_addr *addr) { + assert_return(lldp, -EINVAL); + + /* 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. */ + + if (addr) + lldp->filter_address = *addr; + else + zero(lldp->filter_address); + + return 0; +} |