diff options
Diffstat (limited to 'src/daemon/lldpd.c')
-rw-r--r-- | src/daemon/lldpd.c | 2020 |
1 files changed, 2020 insertions, 0 deletions
diff --git a/src/daemon/lldpd.c b/src/daemon/lldpd.c new file mode 100644 index 0000000..4859fb8 --- /dev/null +++ b/src/daemon/lldpd.c @@ -0,0 +1,2020 @@ +/* -*- mode: c; c-file-style: "openbsd" -*- */ +/* + * Copyright (c) 2008 Vincent Bernat <bernat@luffy.cx> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "lldpd.h" +#include "trace.h" + +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <limits.h> +#include <signal.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <time.h> +#include <libgen.h> +#include <assert.h> +#include <sys/utsname.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <sys/socket.h> +#include <sys/select.h> +#include <sys/time.h> +#include <sys/ioctl.h> +#include <arpa/inet.h> +#include <netinet/if_ether.h> +#include <pwd.h> +#include <grp.h> + +#if HAVE_VFORK_H +# include <vfork.h> +#endif +#if HAVE_WORKING_FORK +# define vfork fork +#endif + +static void usage(void); + +static struct protocol protos[] = { + { LLDPD_MODE_LLDP, 1, "LLDP", 'l', lldp_send, lldp_decode, NULL, + { LLDP_ADDR_NEAREST_BRIDGE, LLDP_ADDR_NEAREST_NONTPMR_BRIDGE, + LLDP_ADDR_NEAREST_CUSTOMER_BRIDGE } }, +#ifdef ENABLE_CDP + { LLDPD_MODE_CDPV1, 0, "CDPv1", 'c', cdpv1_send, cdp_decode, cdpv1_guess, + { CDP_MULTICAST_ADDR } }, + { LLDPD_MODE_CDPV2, 0, "CDPv2", 'c', cdpv2_send, cdp_decode, cdpv2_guess, + { CDP_MULTICAST_ADDR } }, +#endif +#ifdef ENABLE_SONMP + { LLDPD_MODE_SONMP, 0, "SONMP", 's', sonmp_send, sonmp_decode, NULL, + { SONMP_MULTICAST_ADDR } }, +#endif +#ifdef ENABLE_EDP + { LLDPD_MODE_EDP, 0, "EDP", 'e', edp_send, edp_decode, NULL, + { EDP_MULTICAST_ADDR } }, +#endif +#ifdef ENABLE_FDP + { LLDPD_MODE_FDP, 0, "FDP", 'f', fdp_send, cdp_decode, NULL, + { FDP_MULTICAST_ADDR } }, +#endif + { 0, 0, "any", ' ', NULL, NULL, NULL, { { 0, 0, 0, 0, 0, 0 } } } +}; + +static char **saved_argv; +#ifdef HAVE___PROGNAME +extern const char *__progname; +#else +# define __progname "lldpd" +#endif + +static void +usage(void) +{ + fprintf(stderr, "Usage: %s [OPTIONS ...]\n", __progname); + fprintf(stderr, "Version: %s\n", PACKAGE_STRING); + + fprintf(stderr, "\n"); + + fprintf(stderr, "-d Do not daemonize.\n"); + fprintf(stderr, "-r Receive-only mode\n"); + fprintf(stderr, "-i Disable LLDP-MED inventory TLV transmission.\n"); + fprintf(stderr, + "-k Disable advertising of kernel release, version, machine.\n"); + fprintf(stderr, "-S descr Override the default system description.\n"); + fprintf(stderr, "-P name Override the default hardware platform.\n"); + fprintf(stderr, + "-m IP Specify the IP management addresses of this system.\n"); + fprintf(stderr, + "-u file Specify the Unix-domain socket used for communication with lldpctl(8).\n"); + fprintf(stderr, + "-H mode Specify the behaviour when detecting multiple neighbors.\n"); + fprintf(stderr, "-I iface Limit interfaces to use.\n"); + fprintf(stderr, "-C iface Limit interfaces to use for computing chassis ID.\n"); + fprintf(stderr, "-L path Override path for lldpcli command.\n"); + fprintf(stderr, + "-O file Override default configuration locations processed by lldpcli(8) at start.\n"); +#ifdef ENABLE_LLDPMED + fprintf(stderr, + "-M class Enable emission of LLDP-MED frame. 'class' should be one of:\n"); + fprintf(stderr, " 1 Generic Endpoint (Class I)\n"); + fprintf(stderr, " 2 Media Endpoint (Class II)\n"); + fprintf(stderr, " 3 Communication Device Endpoints (Class III)\n"); + fprintf(stderr, " 4 Network Connectivity Device\n"); +#endif +#ifdef USE_SNMP + fprintf(stderr, "-x Enable SNMP subagent.\n"); + fprintf(stderr, "-X sock Specify the SNMP subagent socket.\n"); +#endif + fprintf(stderr, "\n"); + +#if defined ENABLE_CDP || defined ENABLE_EDP || defined ENABLE_FDP || \ + defined ENABLE_SONMP + fprintf(stderr, "Additional protocol support.\n"); +# ifdef ENABLE_CDP + fprintf(stderr, "-c Enable the support of CDP protocol. (Cisco)\n"); +# endif +# ifdef ENABLE_EDP + fprintf(stderr, "-e Enable the support of EDP protocol. (Extreme)\n"); +# endif +# ifdef ENABLE_FDP + fprintf(stderr, "-f Enable the support of FDP protocol. (Foundry)\n"); +# endif +# ifdef ENABLE_SONMP + fprintf(stderr, "-s Enable the support of SONMP protocol. (Nortel)\n"); +# endif + + fprintf(stderr, "\n"); +#endif + + fprintf(stderr, "See manual page lldpd(8) for more information\n"); + exit(1); +} + +struct lldpd_hardware * +lldpd_get_hardware(struct lldpd *cfg, char *name, int index) +{ + struct lldpd_hardware *hardware; + TAILQ_FOREACH (hardware, &cfg->g_hardware, h_entries) { + if (strcmp(hardware->h_ifname, name) == 0) { + if (hardware->h_flags == 0) { + if (hardware->h_ifindex != 0 && + hardware->h_ifindex != index) { + log_debug("interfaces", + "%s changed index: from %d to %d", + hardware->h_ifname, hardware->h_ifindex, + index); + hardware->h_ifindex_changed = 1; + } + hardware->h_ifindex = index; + break; + } + if (hardware->h_ifindex == index) break; + } + } + return hardware; +} + +/** + * Allocate the default local port. This port will be cloned each time we need a + * new local port. + */ +static void +lldpd_alloc_default_local_port(struct lldpd *cfg) +{ + struct lldpd_port *port; + + if ((port = (struct lldpd_port *)calloc(1, sizeof(struct lldpd_port))) == NULL) + fatal("main", NULL); + +#ifdef ENABLE_DOT1 + TAILQ_INIT(&port->p_vlans); + TAILQ_INIT(&port->p_ppvids); + TAILQ_INIT(&port->p_pids); +#endif +#ifdef ENABLE_CUSTOM + TAILQ_INIT(&port->p_custom_list); +#endif + cfg->g_default_local_port = port; +} + +/** + * Clone a given port. The destination needs to be already allocated. + */ +static int +lldpd_clone_port(struct lldpd_port *destination, struct lldpd_port *source) +{ + + u_int8_t *output = NULL; + ssize_t output_len; + struct lldpd_port *cloned = NULL; + output_len = lldpd_port_serialize(source, (void **)&output); + if (output_len == -1 || + lldpd_port_unserialize(output, output_len, &cloned) <= 0) { + log_warnx("alloc", "unable to clone default port"); + free(output); + return -1; + } + memcpy(destination, cloned, sizeof(struct lldpd_port)); + free(cloned); + free(output); +#ifdef ENABLE_DOT1 + marshal_repair_tailq(lldpd_vlan, &destination->p_vlans, v_entries); + marshal_repair_tailq(lldpd_ppvid, &destination->p_ppvids, p_entries); + marshal_repair_tailq(lldpd_pi, &destination->p_pids, p_entries); +#endif +#ifdef ENABLE_CUSTOM + marshal_repair_tailq(lldpd_custom, &destination->p_custom_list, next); +#endif + return 0; +} + +struct lldpd_hardware * +lldpd_alloc_hardware(struct lldpd *cfg, char *name, int index) +{ + struct lldpd_hardware *hardware; + + log_debug("alloc", "allocate a new local port (%s)", name); + + if ((hardware = (struct lldpd_hardware *)calloc(1, + sizeof(struct lldpd_hardware))) == NULL) + return NULL; + + /* Clone default local port */ + if (lldpd_clone_port(&hardware->h_lport, cfg->g_default_local_port) == -1) { + log_warnx("alloc", "unable to clone default port"); + free(hardware); + return NULL; + } + + hardware->h_cfg = cfg; + strlcpy(hardware->h_ifname, name, sizeof(hardware->h_ifname)); + hardware->h_ifindex = index; + hardware->h_lport.p_chassis = LOCAL_CHASSIS(cfg); + hardware->h_lport.p_chassis->c_refcount++; + TAILQ_INIT(&hardware->h_rports); + +#ifdef ENABLE_LLDPMED + if (LOCAL_CHASSIS(cfg)->c_med_cap_available) { + hardware->h_lport.p_med_cap_enabled = LLDP_MED_CAP_CAP; + if (!cfg->g_config.c_noinventory) + hardware->h_lport.p_med_cap_enabled |= LLDP_MED_CAP_IV; + } +#endif + + levent_hardware_init(hardware); + return hardware; +} + +struct lldpd_mgmt * +lldpd_alloc_mgmt(int family, void *addrptr, size_t addrsize, u_int32_t iface) +{ + struct lldpd_mgmt *mgmt; + + log_debug("alloc", "allocate a new management address (family: %d)", family); + + if (family <= LLDPD_AF_UNSPEC || family >= LLDPD_AF_LAST) { + errno = EAFNOSUPPORT; + return NULL; + } + if (addrsize > LLDPD_MGMT_MAXADDRSIZE) { + errno = EOVERFLOW; + return NULL; + } + mgmt = calloc(1, sizeof(struct lldpd_mgmt)); + if (mgmt == NULL) { + errno = ENOMEM; + return NULL; + } + mgmt->m_family = family; + memcpy(&mgmt->m_addr, addrptr, addrsize); + mgmt->m_addrsize = addrsize; + mgmt->m_iface = iface; + return mgmt; +} + +void +lldpd_hardware_cleanup(struct lldpd *cfg, struct lldpd_hardware *hardware) +{ + log_debug("alloc", "cleanup hardware port %s", hardware->h_ifname); + + free(hardware->h_lport_previous); + free(hardware->h_lchassis_previous_id); + free(hardware->h_lport_previous_id); + free(hardware->h_ifdescr_previous); + lldpd_port_cleanup(&hardware->h_lport, 1); + if (hardware->h_ops && hardware->h_ops->cleanup) + hardware->h_ops->cleanup(cfg, hardware); + levent_hardware_release(hardware); + free(hardware); +} + +static void +lldpd_ifdescr_neighbors(struct lldpd *cfg) +{ + if (!cfg->g_config.c_set_ifdescr) return; + struct lldpd_hardware *hardware; + TAILQ_FOREACH (hardware, &cfg->g_hardware, h_entries) { + struct lldpd_port *port; + char *description; + const char *neighbor = NULL; + unsigned neighbors = 0; + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) { + if (SMART_HIDDEN(port)) continue; + neighbors++; + neighbor = port->p_chassis->c_name; + } + if (neighbors == 0) + description = strdup(""); + else if (neighbors == 1 && neighbor && *neighbor != '\0') { + if (asprintf(&description, "%s", neighbor) == -1) { + continue; + } + } else { + if (asprintf(&description, "%d neighbor%s", neighbors, + (neighbors > 1) ? "s" : "") == -1) { + continue; + } + } + if (hardware->h_ifdescr_previous == NULL || + strcmp(hardware->h_ifdescr_previous, description)) { + priv_iface_description(hardware->h_ifname, description); + free(hardware->h_ifdescr_previous); + hardware->h_ifdescr_previous = description; + } else + free(description); + } +} + +static void +lldpd_count_neighbors(struct lldpd *cfg) +{ +#if HAVE_SETPROCTITLE + struct lldpd_chassis *chassis; + const char *neighbor; + unsigned neighbors = 0; + TAILQ_FOREACH (chassis, &cfg->g_chassis, c_entries) { + neighbors++; + neighbor = chassis->c_name; + } + neighbors--; + if (neighbors == 0) + setproctitle("no neighbor."); + else if (neighbors == 1 && neighbor && *neighbor != '\0') + setproctitle("connected to %s.", neighbor); + else + setproctitle("%d neighbor%s.", neighbors, (neighbors > 1) ? "s" : ""); +#endif + lldpd_ifdescr_neighbors(cfg); +} + +static void +notify_clients_deletion(struct lldpd_hardware *hardware, struct lldpd_port *rport) +{ + TRACE(LLDPD_NEIGHBOR_DELETE(hardware->h_ifname, rport->p_chassis->c_name, + rport->p_descr)); + levent_ctl_notify(hardware->h_ifname, NEIGHBOR_CHANGE_DELETED, rport); +#ifdef USE_SNMP + agent_notify(hardware, NEIGHBOR_CHANGE_DELETED, rport); +#endif +} + +static void +lldpd_reset_timer(struct lldpd *cfg) +{ + /* Reset timer for ports that have been changed. */ + struct lldpd_hardware *hardware; + TAILQ_FOREACH (hardware, &cfg->g_hardware, h_entries) { + /* We keep a flat copy of the local port to see if there is any + * change. To do this, we zero out fields that are not + * significant, marshal the port, then restore. */ + struct lldpd_port *port = &hardware->h_lport; + /* Take the current flags into account to detect a change. */ + port->_p_hardware_flags = hardware->h_flags; + u_int8_t *output = NULL; + ssize_t output_len; + char save[LLDPD_PORT_START_MARKER]; + memcpy(save, port, sizeof(save)); + /* coverity[sizeof_mismatch] + We intentionally partially memset port */ + memset(port, 0, sizeof(save)); + output_len = lldpd_port_serialize(port, (void **)&output); + memcpy(port, save, sizeof(save)); + if (output_len == -1) { + log_warnx("localchassis", + "unable to serialize local port %s to check for differences", + hardware->h_ifname); + continue; + } + + /* Compare with the previous value */ + if (!hardware->h_ifindex_changed && hardware->h_lport_previous && + output_len == hardware->h_lport_previous_len && + !memcmp(output, hardware->h_lport_previous, output_len)) { + log_debug("localchassis", "no change detected for port %s", + hardware->h_ifname); + } else { + log_debug("localchassis", + "change detected for port %s, resetting its timer", + hardware->h_ifname); + hardware->h_ifindex_changed = 0; + levent_schedule_pdu(hardware); + } + + /* Update the value */ + free(hardware->h_lport_previous); + hardware->h_lport_previous = output; + hardware->h_lport_previous_len = output_len; + } +} + +static void +lldpd_all_chassis_cleanup(struct lldpd *cfg) +{ + struct lldpd_chassis *chassis, *chassis_next; + log_debug("localchassis", "cleanup all chassis"); + + for (chassis = TAILQ_FIRST(&cfg->g_chassis); chassis; chassis = chassis_next) { + chassis_next = TAILQ_NEXT(chassis, c_entries); + if (chassis->c_refcount == 0) { + TAILQ_REMOVE(&cfg->g_chassis, chassis, c_entries); + lldpd_chassis_cleanup(chassis, 1); + } + } +} + +void +lldpd_cleanup(struct lldpd *cfg) +{ + struct lldpd_hardware *hardware, *hardware_next; + + log_debug("localchassis", "cleanup all ports"); + + for (hardware = TAILQ_FIRST(&cfg->g_hardware); hardware != NULL; + hardware = hardware_next) { + hardware_next = TAILQ_NEXT(hardware, h_entries); + if (!hardware->h_flags) { + int m = cfg->g_config.c_perm_ifaces ? + pattern_match(hardware->h_ifname, + cfg->g_config.c_perm_ifaces, 0) : + 0; + switch (m) { + case PATTERN_MATCH_DENIED: + log_debug("localchassis", + "delete non-permanent interface %s", + hardware->h_ifname); + TRACE(LLDPD_INTERFACES_DELETE(hardware->h_ifname)); + TAILQ_REMOVE(&cfg->g_hardware, hardware, h_entries); + lldpd_remote_cleanup(hardware, notify_clients_deletion, + 1); + lldpd_hardware_cleanup(cfg, hardware); + break; + case PATTERN_MATCH_ALLOWED: + case PATTERN_MATCH_ALLOWED_EXACT: + log_debug("localchassis", "do not delete %s, permanent", + hardware->h_ifname); + lldpd_remote_cleanup(hardware, notify_clients_deletion, + 1); + break; + } + } else { + lldpd_remote_cleanup(hardware, notify_clients_deletion, + !(hardware->h_flags & IFF_RUNNING)); + } + } + + levent_schedule_cleanup(cfg); + lldpd_all_chassis_cleanup(cfg); + lldpd_count_neighbors(cfg); +} + +/* Update chassis `ochassis' with values from `chassis'. The later one is not + expected to be part of a list! It will also be wiped from memory. */ +static void +lldpd_move_chassis(struct lldpd_chassis *ochassis, struct lldpd_chassis *chassis) +{ + struct lldpd_mgmt *mgmt, *mgmt_next; + + /* We want to keep refcount, index and list stuff from the current + * chassis */ + TAILQ_ENTRY(lldpd_chassis) entries; + int refcount = ochassis->c_refcount; + int index = ochassis->c_index; + memcpy(&entries, &ochassis->c_entries, sizeof(entries)); + lldpd_chassis_cleanup(ochassis, 0); + + /* Make the copy. */ + /* WARNING: this is a kludgy hack, we need in-place copy and cannot use + * marshaling. */ + memcpy(ochassis, chassis, sizeof(struct lldpd_chassis)); + TAILQ_INIT(&ochassis->c_mgmt); + + /* Copy of management addresses */ + for (mgmt = TAILQ_FIRST(&chassis->c_mgmt); mgmt != NULL; mgmt = mgmt_next) { + mgmt_next = TAILQ_NEXT(mgmt, m_entries); + TAILQ_REMOVE(&chassis->c_mgmt, mgmt, m_entries); + TAILQ_INSERT_TAIL(&ochassis->c_mgmt, mgmt, m_entries); + } + + /* Restore saved values */ + ochassis->c_refcount = refcount; + ochassis->c_index = index; + memcpy(&ochassis->c_entries, &entries, sizeof(entries)); + + /* Get rid of the new chassis */ + free(chassis); +} + +static int +lldpd_guess_type(struct lldpd *cfg, char *frame, int s) +{ + size_t i, j; + if (s < ETHER_ADDR_LEN) return -1; + for (i = 0; cfg->g_protocols[i].mode != 0; i++) { + if (!cfg->g_protocols[i].enabled) continue; + if (cfg->g_protocols[i].guess == NULL) { + for (j = 0; j < sizeof(cfg->g_protocols[0].mac) / + sizeof(cfg->g_protocols[0].mac[0]); + j++) { + if (memcmp(frame, cfg->g_protocols[i].mac[j], + ETHER_ADDR_LEN) == 0) { + log_debug("decode", + "guessed protocol is %s (from MAC address)", + cfg->g_protocols[i].name); + return cfg->g_protocols[i].mode; + } + } + } else { + if (cfg->g_protocols[i].guess(frame, s)) { + log_debug("decode", + "guessed protocol is %s (from detector function)", + cfg->g_protocols[i].name); + return cfg->g_protocols[i].mode; + } + } + } + return -1; +} + +static void +lldpd_decode(struct lldpd *cfg, char *frame, int s, struct lldpd_hardware *hardware) +{ + int i; + struct lldpd_chassis *chassis, *ochassis = NULL; + struct lldpd_port *port, *oport = NULL, *aport; + int guess = LLDPD_MODE_LLDP; + + log_debug("decode", "decode a received frame on %s", hardware->h_ifname); + + if (s < sizeof(struct ether_header) + 4) { + /* Too short, just discard it */ + hardware->h_rx_discarded_cnt++; + return; + } + + /* Decapsulate VLAN frames */ + struct ether_header eheader; + memcpy(&eheader, frame, sizeof(struct ether_header)); + if (eheader.ether_type == htons(ETHERTYPE_VLAN)) { + /* VLAN decapsulation means to shift 4 bytes left the frame from + * offset 2*ETHER_ADDR_LEN */ + memmove(frame + 2 * ETHER_ADDR_LEN, frame + 2 * ETHER_ADDR_LEN + 4, + s - 2 * ETHER_ADDR_LEN); + s -= 4; + } + + TAILQ_FOREACH (oport, &hardware->h_rports, p_entries) { + if ((oport->p_lastframe != NULL) && (oport->p_lastframe->size == s) && + (memcmp(oport->p_lastframe->frame, frame, s) == 0)) { + /* Already received the same frame */ + log_debug("decode", "duplicate frame, no need to decode"); + oport->p_lastupdate = time(NULL); + return; + } + } + + guess = lldpd_guess_type(cfg, frame, s); + for (i = 0; cfg->g_protocols[i].mode != 0; i++) { + if (!cfg->g_protocols[i].enabled) continue; + if (cfg->g_protocols[i].mode == guess) { + log_debug("decode", "using decode function for %s protocol", + cfg->g_protocols[i].name); + if (cfg->g_protocols[i].decode(cfg, frame, s, hardware, + &chassis, &port) == -1) { + log_debug("decode", + "function for %s protocol did not decode this frame", + cfg->g_protocols[i].name); + hardware->h_rx_discarded_cnt++; + return; + } + chassis->c_protocol = port->p_protocol = + cfg->g_protocols[i].mode; + break; + } + } + if (cfg->g_protocols[i].mode == 0) { + log_debug("decode", "unable to guess frame type on %s", + hardware->h_ifname); + return; + } + TRACE(LLDPD_FRAME_DECODED(hardware->h_ifname, cfg->g_protocols[i].name, + chassis->c_name, port->p_descr)); + + /* Do we already have the same MSAP somewhere? */ + int count = 0; + log_debug("decode", "search for the same MSAP"); + TAILQ_FOREACH (oport, &hardware->h_rports, p_entries) { + if (port->p_protocol == oport->p_protocol) { + count++; + if ((port->p_id_subtype == oport->p_id_subtype) && + (port->p_id_len == oport->p_id_len) && + (memcmp(port->p_id, oport->p_id, port->p_id_len) == 0) && + (chassis->c_id_subtype == oport->p_chassis->c_id_subtype) && + (chassis->c_id_len == oport->p_chassis->c_id_len) && + (memcmp(chassis->c_id, oport->p_chassis->c_id, + chassis->c_id_len) == 0)) { + ochassis = oport->p_chassis; + log_debug("decode", "MSAP is already known"); + break; + } + } + } + /* Do we have room for a new MSAP? */ + if (!oport && cfg->g_config.c_max_neighbors) { + if (count == (cfg->g_config.c_max_neighbors - 1)) { + log_debug("decode", + "max neighbors %d reached for port %s, " + "dropping any new ones silently", + cfg->g_config.c_max_neighbors, hardware->h_ifname); + } else if (count > cfg->g_config.c_max_neighbors - 1) { + log_debug("decode", + "too many neighbors for port %s, drop this new one", + hardware->h_ifname); + lldpd_port_cleanup(port, 1); + lldpd_chassis_cleanup(chassis, 1); + free(port); + return; + } + } + /* No, but do we already know the system? */ + if (!oport) { + log_debug("decode", "MSAP is unknown, search for the chassis"); + TAILQ_FOREACH (ochassis, &cfg->g_chassis, c_entries) { + if ((chassis->c_protocol == ochassis->c_protocol) && + (chassis->c_id_subtype == ochassis->c_id_subtype) && + (chassis->c_id_len == ochassis->c_id_len) && + (memcmp(chassis->c_id, ochassis->c_id, chassis->c_id_len) == + 0)) + break; + } + } + + if (oport) { + /* The port is known, remove it before adding it back */ + TAILQ_REMOVE(&hardware->h_rports, oport, p_entries); + lldpd_port_cleanup(oport, 1); + free(oport); + } + if (ochassis) { + if (port->p_ttl == 0) { + /* Shutdown LLDPDU is special. We do not want to replace + * the chassis. Free the new chassis (which is mostly empty) */ + log_debug("decode", "received a shutdown LLDPDU"); + lldpd_chassis_cleanup(chassis, 1); + } else { + lldpd_move_chassis(ochassis, chassis); + } + chassis = ochassis; + } else { + /* Chassis not known, add it */ + log_debug("decode", "unknown chassis, add it to the list"); + chassis->c_index = ++cfg->g_lastrid; + chassis->c_refcount = 0; + TAILQ_INSERT_TAIL(&cfg->g_chassis, chassis, c_entries); + i = 0; + TAILQ_FOREACH (ochassis, &cfg->g_chassis, c_entries) + i++; + log_debug("decode", "%d different systems are known", i); + } + /* Add port */ + port->p_lastchange = port->p_lastupdate = time(NULL); + if ((port->p_lastframe = (struct lldpd_frame *)malloc( + s + sizeof(struct lldpd_frame))) != NULL) { + port->p_lastframe->size = s; + memcpy(port->p_lastframe->frame, frame, s); + } + TAILQ_INSERT_TAIL(&hardware->h_rports, port, p_entries); + port->p_chassis = chassis; + port->p_chassis->c_refcount++; + /* Several cases are possible : + 1. chassis is new, its refcount was 0. It is now attached + to this port, its refcount is 1. + 2. chassis already exists and was attached to another + port, we increase its refcount accordingly. + 3. chassis already exists and was attached to the same + port, its refcount was decreased with + lldpd_port_cleanup() and is now increased again. + + In all cases, if the port already existed, it has been + freed with lldpd_port_cleanup() and therefore, the refcount + of the chassis that was attached to it is decreased. + */ + i = 0; + /* coverity[use_after_free] + TAILQ_REMOVE does the right thing */ + TAILQ_FOREACH (aport, &hardware->h_rports, p_entries) + i++; + log_debug("decode", "%d neighbors for %s", i, hardware->h_ifname); + + if (!oport) hardware->h_insert_cnt++; + + /* Notify */ + log_debug("decode", "send notifications for changes on %s", hardware->h_ifname); + if (oport) { + TRACE(LLDPD_NEIGHBOR_UPDATE(hardware->h_ifname, chassis->c_name, + port->p_descr, i)); + levent_ctl_notify(hardware->h_ifname, NEIGHBOR_CHANGE_UPDATED, port); +#ifdef USE_SNMP + agent_notify(hardware, NEIGHBOR_CHANGE_UPDATED, port); +#endif + } else { + TRACE(LLDPD_NEIGHBOR_NEW(hardware->h_ifname, chassis->c_name, + port->p_descr, i)); + levent_ctl_notify(hardware->h_ifname, NEIGHBOR_CHANGE_ADDED, port); +#ifdef USE_SNMP + agent_notify(hardware, NEIGHBOR_CHANGE_ADDED, port); +#endif + } + +#ifdef ENABLE_LLDPMED + if (!oport && port->p_chassis->c_med_type) { + /* New neighbor, fast start */ + if (hardware->h_cfg->g_config.c_enable_fast_start && + !hardware->h_tx_fast) { + log_debug("decode", + "%s: entering fast start due to " + "new neighbor", + hardware->h_ifname); + hardware->h_tx_fast = hardware->h_cfg->g_config.c_tx_fast_init; + } + + levent_schedule_pdu(hardware); + } +#endif + + return; +} + +/* Get the output of lsb_release -s -d. This is a slow function. It should be + called once. It return NULL if any problem happens. Otherwise, this is a + statically allocated buffer. The result includes the trailing \n */ +static char * +lldpd_get_lsb_release() +{ + static char release[1024]; + char cmd[][12] = { "lsb_release", "-s", "-d" }; + char *const command[] = { cmd[0], cmd[1], cmd[2], NULL }; + int pid, status, devnull, count; + int pipefd[2]; + + log_debug("localchassis", "grab LSB release"); + + if (pipe(pipefd)) { + log_warn("localchassis", "unable to get a pair of pipes"); + return NULL; + } + + pid = vfork(); + switch (pid) { + case -1: + log_warn("localchassis", "unable to fork"); + return NULL; + case 0: + /* Child, exec lsb_release */ + close(pipefd[0]); + if ((devnull = open("/dev/null", O_RDWR, 0)) != -1) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDERR_FILENO); + dup2(pipefd[1], STDOUT_FILENO); + if (devnull > 2) close(devnull); + if (pipefd[1] > 2) close(pipefd[1]); + execvp("lsb_release", command); + } + _exit(127); + break; + default: + /* Father, read the output from the children */ + close(pipefd[1]); + count = 0; + do { + status = + read(pipefd[0], release + count, sizeof(release) - count); + if ((status == -1) && (errno == EINTR)) continue; + if (status > 0) count += status; + } while (count < sizeof(release) && (status > 0)); + if (status < 0) { + log_info("localchassis", "unable to read from lsb_release"); + close(pipefd[0]); + waitpid(pid, &status, 0); + return NULL; + } + close(pipefd[0]); + if (count >= sizeof(release)) { + log_info("localchassis", "output of lsb_release is too large"); + waitpid(pid, &status, 0); + return NULL; + } + status = -1; + if (waitpid(pid, &status, 0) != pid) return NULL; + if (!WIFEXITED(status) || (WEXITSTATUS(status) != 0)) { + log_info("localchassis", + "lsb_release information not available"); + return NULL; + } + if (!count) { + log_info("localchassis", + "lsb_release returned an empty string"); + return NULL; + } + release[count] = '\0'; + return release; + } + /* Should not be here */ + return NULL; +} + +/* Same like lldpd_get_lsb_release but reads /etc/os-release for PRETTY_NAME=. */ +static char * +lldpd_get_os_release() +{ + static char release[1024]; + char line[1024]; + char *key, *val; + char *ptr1 = release; + + log_debug("localchassis", "grab OS release"); + FILE *fp = fopen("/etc/os-release", "r"); + if (!fp) { + log_debug("localchassis", "could not open /etc/os-release"); + fp = fopen("/usr/lib/os-release", "r"); + } + if (!fp) { + log_info("localchassis", + "could not open either /etc/os-release or /usr/lib/os-release"); + return NULL; + } + + while ((fgets(line, sizeof(line), fp) != NULL)) { + key = strtok(line, "="); + val = strtok(NULL, "="); + + if (strncmp(key, "PRETTY_NAME", sizeof(line)) == 0) { + strlcpy(release, val, sizeof(line)); + break; + } + } + fclose(fp); + + /* Remove trailing newline and all " in the string. */ + ptr1 = release + strlen(release) - 1; + while (ptr1 != release && ((*ptr1 == '"') || (*ptr1 == '\n'))) { + *ptr1 = '\0'; + ptr1--; + } + if (release[0] == '"') return release + 1; + return release; +} + +static void +lldpd_hide_ports(struct lldpd *cfg, struct lldpd_hardware *hardware, int mask) +{ + struct lldpd_port *port; + int protocols[LLDPD_MODE_MAX + 1]; + char buffer[256]; + int i, j, k, found; + unsigned int min; + + log_debug("smartfilter", "apply smart filter for port %s", hardware->h_ifname); + + /* Compute the number of occurrences of each protocol */ + for (i = 0; i <= LLDPD_MODE_MAX; i++) + protocols[i] = 0; + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) + protocols[port->p_protocol]++; + + /* Turn the protocols[] array into an array of + enabled/disabled protocols. 1 means enabled, 0 + means disabled. */ + min = (unsigned int)-1; + for (i = 0; i <= LLDPD_MODE_MAX; i++) + if (protocols[i] && (protocols[i] < min)) min = protocols[i]; + found = 0; + for (i = 0; i <= LLDPD_MODE_MAX; i++) + if ((protocols[i] == min) && !found) { + /* If we need a tie breaker, we take + the first protocol only */ + if (cfg->g_config.c_smart & mask & + (SMART_OUTGOING_ONE_PROTO | SMART_INCOMING_ONE_PROTO)) + found = 1; + protocols[i] = 1; + } else + protocols[i] = 0; + + /* We set the p_hidden flag to 1 if the protocol is disabled */ + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) { + if (mask == SMART_OUTGOING) + port->p_hidden_out = protocols[port->p_protocol] ? 0 : 1; + else + port->p_hidden_in = protocols[port->p_protocol] ? 0 : 1; + } + + /* If we want only one neighbor, we take the first one */ + if (cfg->g_config.c_smart & mask & + (SMART_OUTGOING_ONE_NEIGH | SMART_INCOMING_ONE_NEIGH)) { + found = 0; + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) { + if (mask == SMART_OUTGOING) { + if (found) port->p_hidden_out = 1; + if (!port->p_hidden_out) found = 1; + } + if (mask == SMART_INCOMING) { + if (found) port->p_hidden_in = 1; + if (!port->p_hidden_in) found = 1; + } + } + } + + /* Print a debug message summarizing the operation */ + for (i = 0; i <= LLDPD_MODE_MAX; i++) + protocols[i] = 0; + k = j = 0; + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) { + if (!(((mask == SMART_OUTGOING) && port->p_hidden_out) || + ((mask == SMART_INCOMING) && port->p_hidden_in))) { + k++; + protocols[port->p_protocol] = 1; + } + j++; + } + buffer[0] = '\0'; + for (i = 0; cfg->g_protocols[i].mode != 0; i++) { + if (cfg->g_protocols[i].enabled && + protocols[cfg->g_protocols[i].mode]) { + if (strlen(buffer) + strlen(cfg->g_protocols[i].name) + 3 > + sizeof(buffer)) { + /* Unlikely, our buffer is too small */ + memcpy(buffer + sizeof(buffer) - 4, "...", 4); + break; + } + if (buffer[0]) + strncat(buffer, ", ", + sizeof(buffer) - strlen(buffer) - 1); + strncat(buffer, cfg->g_protocols[i].name, + sizeof(buffer) - strlen(buffer) - 1); + } + } + log_debug("smartfilter", "%s: %s: %d visible neighbors (out of %d)", + hardware->h_ifname, (mask == SMART_OUTGOING) ? "out filter" : "in filter", + k, j); + log_debug("smartfilter", "%s: protocols: %s", hardware->h_ifname, + buffer[0] ? buffer : "(none)"); +} + +/* Hide unwanted ports depending on smart mode set by the user */ +static void +lldpd_hide_all(struct lldpd *cfg) +{ + struct lldpd_hardware *hardware; + + if (!cfg->g_config.c_smart) return; + log_debug("smartfilter", "apply smart filter results on all ports"); + TAILQ_FOREACH (hardware, &cfg->g_hardware, h_entries) { + if (cfg->g_config.c_smart & SMART_INCOMING_FILTER) + lldpd_hide_ports(cfg, hardware, SMART_INCOMING); + if (cfg->g_config.c_smart & SMART_OUTGOING_FILTER) + lldpd_hide_ports(cfg, hardware, SMART_OUTGOING); + } +} + +/* If PD device and PSE allocated power, echo back this change. If we have + * several LLDP neighbors, we use the latest updated. */ +static void +lldpd_dot3_power_pd_pse(struct lldpd_hardware *hardware) +{ +#ifdef ENABLE_DOT3 + struct lldpd_port *port, *selected_port = NULL; + /* Are we a PD device? */ + if (hardware->h_lport.p_power.devicetype != LLDP_DOT3_POWER_PD) return; + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) { + if (port->p_hidden_in) continue; + + if (port->p_protocol != LLDPD_MODE_LLDP && + port->p_protocol != LLDPD_MODE_CDPV2) + continue; + + if (port->p_power.devicetype != LLDP_DOT3_POWER_PSE) continue; + if (!selected_port || port->p_lastupdate > selected_port->p_lastupdate) + selected_port = port; + } + if (selected_port && + selected_port->p_power.allocated != hardware->h_lport.p_power.allocated) { + log_info("receive", + "for %s, PSE told us allocated is now %d instead of %d", + hardware->h_ifname, selected_port->p_power.allocated, + hardware->h_lport.p_power.allocated); + hardware->h_lport.p_power.allocated = selected_port->p_power.allocated; + levent_schedule_pdu(hardware); + } + +# ifdef ENABLE_CDP + if (selected_port && + selected_port->p_cdp_power.management_id != + hardware->h_lport.p_cdp_power.management_id) { + hardware->h_lport.p_cdp_power.management_id = + selected_port->p_cdp_power.management_id; + } +# endif + +#endif +} + +void +lldpd_recv(struct lldpd *cfg, struct lldpd_hardware *hardware, int fd) +{ + char *buffer = NULL; + int n; + log_debug("receive", "receive a frame on %s", hardware->h_ifname); + if ((buffer = (char *)malloc(hardware->h_mtu)) == NULL) { + log_warn("receive", "failed to alloc reception buffer"); + return; + } + if ((n = hardware->h_ops->recv(cfg, hardware, fd, buffer, hardware->h_mtu)) == + -1) { + log_debug("receive", "discard frame received on %s", + hardware->h_ifname); + free(buffer); + return; + } + if (hardware->h_lport.p_disable_rx) { + log_debug("receive", "RX disabled, ignore the frame on %s", + hardware->h_ifname); + free(buffer); + return; + } + if (cfg->g_config.c_paused) { + log_debug("receive", "paused, ignore the frame on %s", + hardware->h_ifname); + free(buffer); + return; + } + hardware->h_rx_cnt++; + log_debug("receive", "decode received frame on %s", hardware->h_ifname); + TRACE(LLDPD_FRAME_RECEIVED(hardware->h_ifname, buffer, (size_t)n)); + lldpd_decode(cfg, buffer, n, hardware); + lldpd_hide_all(cfg); /* Immediatly hide */ + lldpd_dot3_power_pd_pse(hardware); + lldpd_count_neighbors(cfg); + free(buffer); +} + +static void +lldpd_send_shutdown(struct lldpd_hardware *hardware) +{ + struct lldpd *cfg = hardware->h_cfg; + if (cfg->g_config.c_receiveonly || cfg->g_config.c_paused) return; + if (hardware->h_lport.p_disable_tx) return; + if ((hardware->h_flags & IFF_RUNNING) == 0) return; + + /* It's safe to call `lldp_send_shutdown()` because shutdown LLDPU will + * only be emitted if LLDP was sent on that port. */ + if (lldp_send_shutdown(hardware->h_cfg, hardware) != 0) + log_warnx("send", "unable to send shutdown LLDPDU on %s", + hardware->h_ifname); +} + +void +lldpd_send(struct lldpd_hardware *hardware) +{ + struct lldpd *cfg = hardware->h_cfg; + struct lldpd_port *port; + int i, sent; + + if (cfg->g_config.c_receiveonly || cfg->g_config.c_paused) return; + if (hardware->h_lport.p_disable_tx) return; + if ((hardware->h_flags & IFF_RUNNING) == 0) return; + + log_debug("send", "send PDU on %s", hardware->h_ifname); + sent = 0; + for (i = 0; cfg->g_protocols[i].mode != 0; i++) { + if (!cfg->g_protocols[i].enabled) continue; + /* We send only if we have at least one remote system + * speaking this protocol or if the protocol is forced */ + if (cfg->g_protocols[i].enabled > 1) { + cfg->g_protocols[i].send(cfg, hardware); + sent++; + continue; + } + TAILQ_FOREACH (port, &hardware->h_rports, p_entries) { + /* If this remote port is disabled, we don't + * consider it */ + if (port->p_hidden_out) continue; + if (port->p_protocol == cfg->g_protocols[i].mode) { + TRACE(LLDPD_FRAME_SEND(hardware->h_ifname, + cfg->g_protocols[i].name)); + log_debug("send", "send PDU on %s with protocol %s", + hardware->h_ifname, cfg->g_protocols[i].name); + cfg->g_protocols[i].send(cfg, hardware); + hardware->h_lport.p_protocol = cfg->g_protocols[i].mode; + sent++; + break; + } + } + } + + if (!sent) { + /* Nothing was sent for this port, let's speak the first + * available protocol. */ + for (i = 0; cfg->g_protocols[i].mode != 0; i++) { + if (!cfg->g_protocols[i].enabled) continue; + TRACE(LLDPD_FRAME_SEND(hardware->h_ifname, + cfg->g_protocols[i].name)); + log_debug("send", "fallback to protocol %s for %s", + cfg->g_protocols[i].name, hardware->h_ifname); + cfg->g_protocols[i].send(cfg, hardware); + break; + } + if (cfg->g_protocols[i].mode == 0) + log_warnx("send", "no protocol enabled, dunno what to send"); + } +} + +#ifdef ENABLE_LLDPMED +static void +lldpd_med(struct lldpd *cfg, struct utsname *un) +{ + static short int once = 0; + if (!once && cfg) { + LOCAL_CHASSIS(cfg)->c_med_hw = dmi_hw(); + LOCAL_CHASSIS(cfg)->c_med_fw = dmi_fw(); + LOCAL_CHASSIS(cfg)->c_med_sn = dmi_sn(); + LOCAL_CHASSIS(cfg)->c_med_manuf = dmi_manuf(); + LOCAL_CHASSIS(cfg)->c_med_model = dmi_model(); + LOCAL_CHASSIS(cfg)->c_med_asset = dmi_asset(); + if (un) { + if (LOCAL_CHASSIS(cfg)->c_med_sw) + free(LOCAL_CHASSIS(cfg)->c_med_sw); + + if (cfg->g_config.c_advertise_version) + LOCAL_CHASSIS(cfg)->c_med_sw = strdup(un->release); + else + LOCAL_CHASSIS(cfg)->c_med_sw = strdup("Unknown"); + } + once = 1; + } +} +#endif + +static int +lldpd_routing_enabled(struct lldpd *cfg) +{ + int routing; + + if ((LOCAL_CHASSIS(cfg)->c_cap_available & LLDP_CAP_ROUTER) == 0) return 0; + + if ((routing = interfaces_routing_enabled(cfg)) == -1) { + log_debug("localchassis", "unable to check if routing is enabled"); + return 0; + } + return routing; +} + +void +lldpd_update_localchassis(struct lldpd *cfg) +{ + struct utsname un; + char *hp; + + log_debug("localchassis", "update information for local chassis"); + assert(LOCAL_CHASSIS(cfg) != NULL); + + /* Set system name and description */ + if (uname(&un) < 0) fatal("localchassis", "failed to get system information"); + if (cfg->g_config.c_hostname) { + log_debug("localchassis", "use overridden system name `%s`", + cfg->g_config.c_hostname); + hp = cfg->g_config.c_hostname; + } else { + if ((hp = priv_gethostname()) == NULL) + fatal("localchassis", "failed to get system name"); + } + free(LOCAL_CHASSIS(cfg)->c_name); + free(LOCAL_CHASSIS(cfg)->c_descr); + if ((LOCAL_CHASSIS(cfg)->c_name = strdup(hp)) == NULL) + fatal("localchassis", NULL); + if (cfg->g_config.c_description) { + log_debug("localchassis", "use overridden description `%s`", + cfg->g_config.c_description); + if (asprintf(&LOCAL_CHASSIS(cfg)->c_descr, "%s", + cfg->g_config.c_description) == -1) + fatal("localchassis", "failed to set full system description"); + } else { + if (cfg->g_config.c_advertise_version) { + log_debug("localchassis", "advertise system version"); + if (asprintf(&LOCAL_CHASSIS(cfg)->c_descr, "%s %s %s %s %s", + cfg->g_lsb_release ? cfg->g_lsb_release : "", + un.sysname, un.release, un.version, un.machine) == -1) + fatal("localchassis", + "failed to set full system description"); + } else { + log_debug("localchassis", "do not advertise system version"); + if (asprintf(&LOCAL_CHASSIS(cfg)->c_descr, "%s", + cfg->g_lsb_release ? cfg->g_lsb_release : un.sysname) == + -1) + fatal("localchassis", + "failed to set minimal system description"); + } + } + if (cfg->g_config.c_platform == NULL) + cfg->g_config.c_platform = strdup(un.sysname); + + if (!cfg->g_config.c_cap_override) { + /* Check routing */ + if (lldpd_routing_enabled(cfg)) { + log_debug("localchassis", + "routing is enabled, enable router capability"); + LOCAL_CHASSIS(cfg)->c_cap_enabled |= LLDP_CAP_ROUTER; + } else + LOCAL_CHASSIS(cfg)->c_cap_enabled &= ~LLDP_CAP_ROUTER; + +#ifdef ENABLE_LLDPMED + if (LOCAL_CHASSIS(cfg)->c_cap_available & LLDP_CAP_TELEPHONE) + LOCAL_CHASSIS(cfg)->c_cap_enabled |= LLDP_CAP_TELEPHONE; + lldpd_med(cfg, &un); +#endif + if ((LOCAL_CHASSIS(cfg)->c_cap_available & LLDP_CAP_STATION) && + (LOCAL_CHASSIS(cfg)->c_cap_enabled == 0)) + LOCAL_CHASSIS(cfg)->c_cap_enabled = LLDP_CAP_STATION; + else if (LOCAL_CHASSIS(cfg)->c_cap_enabled != LLDP_CAP_STATION) + LOCAL_CHASSIS(cfg)->c_cap_enabled &= ~LLDP_CAP_STATION; + } + + /* Set chassis ID if needed. This is only done if chassis ID + has not been set previously (with the MAC address of an + interface for example) + */ + if (cfg->g_config.c_cid_string != NULL) { + log_debug("localchassis", "use specified chassis ID string"); + free(LOCAL_CHASSIS(cfg)->c_id); + if (!(LOCAL_CHASSIS(cfg)->c_id = strdup(cfg->g_config.c_cid_string))) + fatal("localchassis", NULL); + LOCAL_CHASSIS(cfg)->c_id_len = strlen(cfg->g_config.c_cid_string); + LOCAL_CHASSIS(cfg)->c_id_subtype = LLDP_CHASSISID_SUBTYPE_LOCAL; + } + if (LOCAL_CHASSIS(cfg)->c_id == NULL) { + log_debug("localchassis", + "no chassis ID is currently set, use chassis name"); + if (!(LOCAL_CHASSIS(cfg)->c_id = strdup(LOCAL_CHASSIS(cfg)->c_name))) + fatal("localchassis", NULL); + LOCAL_CHASSIS(cfg)->c_id_len = strlen(LOCAL_CHASSIS(cfg)->c_name); + LOCAL_CHASSIS(cfg)->c_id_subtype = LLDP_CHASSISID_SUBTYPE_LOCAL; + } +} + +void +lldpd_update_localports(struct lldpd *cfg) +{ + struct lldpd_hardware *hardware; + + log_debug("localchassis", "update information for local ports"); + + /* h_flags is set to 0 for each port. If the port is updated, h_flags + * will be set to a non-zero value. This will allow us to clean up any + * non up-to-date port */ + TAILQ_FOREACH (hardware, &cfg->g_hardware, h_entries) + hardware->h_flags = 0; + + TRACE(LLDPD_INTERFACES_UPDATE()); + interfaces_update(cfg); + lldpd_cleanup(cfg); + lldpd_reset_timer(cfg); +} + +void +lldpd_loop(struct lldpd *cfg) +{ + /* Main loop. + 1. Update local ports information + 2. Update local chassis information + */ + log_debug("loop", "start new loop"); + if (!cfg->g_config.c_cap_override) LOCAL_CHASSIS(cfg)->c_cap_enabled = 0; + /* Information for local ports is triggered even when it is possible to + * update them on some other event because we want to refresh them if we + * missed something. */ + log_debug("loop", "update information for local ports"); + lldpd_update_localports(cfg); + log_debug("loop", "update information for local chassis"); + lldpd_update_localchassis(cfg); + lldpd_count_neighbors(cfg); +} + +static void +lldpd_exit(struct lldpd *cfg) +{ + char *lockname = NULL; + struct lldpd_hardware *hardware, *hardware_next; + log_debug("main", "exit lldpd"); + + TAILQ_FOREACH (hardware, &cfg->g_hardware, h_entries) + lldpd_send_shutdown(hardware); + + if (asprintf(&lockname, "%s.lock", cfg->g_ctlname) != -1) { + priv_ctl_cleanup(lockname); + free(lockname); + } + close(cfg->g_ctl); + priv_ctl_cleanup(cfg->g_ctlname); + log_debug("main", "cleanup hardware information"); + for (hardware = TAILQ_FIRST(&cfg->g_hardware); hardware != NULL; + hardware = hardware_next) { + hardware_next = TAILQ_NEXT(hardware, h_entries); + log_debug("main", "cleanup interface %s", hardware->h_ifname); + lldpd_remote_cleanup(hardware, NULL, 1); + lldpd_hardware_cleanup(cfg, hardware); + } + interfaces_cleanup(cfg); + lldpd_port_cleanup(cfg->g_default_local_port, 1); + lldpd_all_chassis_cleanup(cfg); + free(cfg->g_default_local_port); + free(cfg->g_config.c_platform); + levent_shutdown(cfg); +} + +/** + * Run lldpcli to configure lldpd. + * + * @return PID of running lldpcli or -1 if error. + */ +static pid_t +lldpd_configure(int use_syslog, int debug, const char *path, const char *ctlname, + const char *config_path) +{ + pid_t lldpcli = vfork(); + int devnull; + + char sdebug[debug + 4]; + if (use_syslog) + strlcpy(sdebug, "-s", 3); + else { + /* debug = 0 -> -sd */ + /* debug = 1 -> -sdd */ + /* debug = 2 -> -sddd */ + memset(sdebug, 'd', sizeof(sdebug)); + sdebug[debug + 3] = '\0'; + sdebug[0] = '-'; + sdebug[1] = 's'; + } + log_debug("main", "invoke %s %s", path, sdebug); + + switch (lldpcli) { + case -1: + log_warn("main", "unable to fork"); + return -1; + case 0: + /* Child, exec lldpcli */ + if ((devnull = open("/dev/null", O_RDWR, 0)) != -1) { + dup2(devnull, STDIN_FILENO); + dup2(devnull, STDOUT_FILENO); + if (devnull > 2) close(devnull); + + if (config_path) { + execl(path, "lldpcli", sdebug, "-u", ctlname, "-C", + config_path, "resume", (char *)NULL); + } else { + execl(path, "lldpcli", sdebug, "-u", ctlname, "-C", + SYSCONFDIR "/lldpd.conf", "-C", + SYSCONFDIR "/lldpd.d", "resume", (char *)NULL); + } + + log_warn("main", "unable to execute %s", path); + log_warnx("main", + "configuration is incomplete, lldpd needs to be unpaused"); + } + _exit(127); + break; + default: + /* Father, don't do anything stupid */ + return lldpcli; + } + /* Should not be here */ + return -1; +} + +struct intint { + int a; + int b; +}; +static const struct intint filters[] = { { 0, 0 }, + { 1, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | SMART_OUTGOING_FILTER | + SMART_OUTGOING_ONE_PROTO }, + { 2, SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO }, + { 3, SMART_OUTGOING_FILTER | SMART_OUTGOING_ONE_PROTO }, + { 4, SMART_INCOMING_FILTER | SMART_OUTGOING_FILTER }, + { 5, SMART_INCOMING_FILTER }, { 6, SMART_OUTGOING_FILTER }, + { 7, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | + SMART_INCOMING_ONE_NEIGH | SMART_OUTGOING_FILTER | + SMART_OUTGOING_ONE_PROTO }, + { 8, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | + SMART_INCOMING_ONE_NEIGH }, + { 9, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_NEIGH | SMART_OUTGOING_FILTER | + SMART_OUTGOING_ONE_PROTO }, + { 10, SMART_OUTGOING_FILTER | SMART_OUTGOING_ONE_NEIGH }, + { 11, SMART_INCOMING_FILTER | SMART_INCOMING_ONE_NEIGH }, + { 12, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_NEIGH | SMART_OUTGOING_FILTER | + SMART_OUTGOING_ONE_NEIGH }, + { 13, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_NEIGH | SMART_OUTGOING_FILTER }, + { 14, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | SMART_OUTGOING_FILTER | + SMART_OUTGOING_ONE_NEIGH }, + { 15, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | SMART_OUTGOING_FILTER }, + { 16, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | + SMART_INCOMING_ONE_NEIGH | SMART_OUTGOING_FILTER | + SMART_OUTGOING_ONE_NEIGH }, + { 17, + SMART_INCOMING_FILTER | SMART_INCOMING_ONE_PROTO | + SMART_INCOMING_ONE_NEIGH | SMART_OUTGOING_FILTER }, + { 18, + SMART_INCOMING_FILTER | SMART_OUTGOING_FILTER | SMART_OUTGOING_ONE_NEIGH }, + { 19, + SMART_INCOMING_FILTER | SMART_OUTGOING_FILTER | SMART_OUTGOING_ONE_PROTO }, + { -1, 0 } }; + +#ifndef HOST_OS_OSX +/** + * Tell if we have been started by systemd. + */ +static int +lldpd_started_by_systemd() +{ +# ifdef HOST_OS_LINUX + int fd = -1; + const char *notifysocket = getenv("NOTIFY_SOCKET"); + if (!notifysocket || !strchr("@/", notifysocket[0]) || strlen(notifysocket) < 2) + return 0; + + log_debug("main", "running with systemd, don't fork but signal ready"); + if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) { + log_warn("main", "unable to open systemd notification socket %s", + notifysocket); + return 0; + } + + struct sockaddr_un su = { .sun_family = AF_UNIX }; + strlcpy(su.sun_path, notifysocket, sizeof(su.sun_path)); + if (notifysocket[0] == '@') su.sun_path[0] = 0; + + char ready[] = "READY=1"; + struct iovec iov = { .iov_base = ready, .iov_len = sizeof ready - 1 }; + struct msghdr hdr = { .msg_name = &su, + .msg_namelen = + offsetof(struct sockaddr_un, sun_path) + strlen(notifysocket), + .msg_iov = &iov, + .msg_iovlen = 1 }; + unsetenv("NOTIFY_SOCKET"); + if (sendmsg(fd, &hdr, MSG_NOSIGNAL) < 0) { + log_warn("main", "unable to send notification to systemd"); + close(fd); + return 0; + } + close(fd); + return 1; +# else + return 0; +# endif +} +#endif + +#ifdef HOST_OS_LINUX +static void +version_convert(const char *sversion, unsigned iversion[], size_t n) +{ + const char *p = sversion; + char *end; + for (size_t i = 0; i < n; i++) { + iversion[i] = strtol(p, &end, 10); + if (*end != '.') break; + p = end + 1; + } +} + +static void +version_check(void) +{ + struct utsname uts; + if (uname(&uts) == -1) return; + unsigned version_min[3] = {}; + unsigned version_cur[3] = {}; + version_convert(uts.release, version_cur, 3); + version_convert(MIN_LINUX_KERNEL_VERSION, version_min, 3); + if (version_min[0] > version_cur[0] || + (version_min[0] == version_cur[0] && version_min[1] > version_cur[1]) || + (version_min[0] == version_cur[0] && version_min[1] == version_cur[1] && + version_min[2] > version_cur[2])) { + log_warnx("lldpd", "minimal kernel version required is %s, got %s", + MIN_LINUX_KERNEL_VERSION, uts.release); + log_warnx("lldpd", + "lldpd may be unable to detect bonds and bridges correctly"); +# ifndef ENABLE_OLDIES + log_warnx("lldpd", "consider recompiling with --enable-oldies option"); +# endif + } +} +#else +static void +version_check(void) +{ +} +#endif + +int +lldpd_main(int argc, char *argv[], char *envp[]) +{ + struct lldpd *cfg; + struct lldpd_chassis *lchassis; + int ch, debug = 0, use_syslog = 1, daemonize = 1; + const char *errstr; +#ifdef USE_SNMP + int snmp = 0; + const char *agentx = NULL; /* AgentX socket */ +#endif + const char *ctlname = NULL; + char *mgmtp = NULL; + char *cidp = NULL; + char *interfaces = NULL; + /* We do not want more options here. Please add them in lldpcli instead + * unless there is a very good reason. Most command-line options will + * get deprecated at some point. */ + char *popt, + opts[] = "H:vhkrdD:p:xX:m:u:4:6:I:C:p:M:P:S:iL:O:@ "; + int i, found, advertise_version = 1; +#ifdef ENABLE_LLDPMED + int lldpmed = 0, noinventory = 0; + int enable_fast_start = 1; +#endif + char *descr_override = NULL; + char *platform_override = NULL; + char *lsb_release = NULL; + const char *lldpcli = LLDPCLI_PATH; + const char *pidfile = LLDPD_PID_FILE; + int smart = 15; + int receiveonly = 0, version = 0; + int ctl; + const char *config_file = NULL; + +#ifdef ENABLE_PRIVSEP + /* Non privileged user */ + struct passwd *user; + struct group *group; + uid_t uid; + gid_t gid; +#endif + + saved_argv = argv; + +#if HAVE_SETPROCTITLE_INIT + setproctitle_init(argc, argv, envp); +#endif + + /* + * Get and parse command line options + */ + if ((popt = strchr(opts, '@')) != NULL) { + for (i = 0; protos[i].mode != 0 && *popt != '\0'; i++) + *(popt++) = protos[i].arg; + *popt = '\0'; + } + while ((ch = getopt(argc, argv, opts)) != -1) { + switch (ch) { + case 'h': + usage(); + break; + case 'v': + version++; + break; + case 'd': + if (daemonize) + daemonize = 0; + else if (use_syslog) + use_syslog = 0; + else + debug++; + break; + case 'D': + log_accept(optarg); + break; + case 'p': + pidfile = optarg; + break; + case 'r': + receiveonly = 1; + break; + case 'm': + if (mgmtp) { + fprintf(stderr, "-m can only be used once\n"); + usage(); + } + mgmtp = strdup(optarg); + break; + case 'u': + if (ctlname) { + fprintf(stderr, "-u can only be used once\n"); + usage(); + } + ctlname = optarg; + break; + case 'I': + if (interfaces) { + fprintf(stderr, "-I can only be used once\n"); + usage(); + } + interfaces = strdup(optarg); + break; + case 'C': + if (cidp) { + fprintf(stderr, "-C can only be used once\n"); + usage(); + } + cidp = strdup(optarg); + break; + case 'L': + if (strlen(optarg)) + lldpcli = optarg; + else + lldpcli = NULL; + break; + case 'k': + advertise_version = 0; + break; +#ifdef ENABLE_LLDPMED + case 'M': + lldpmed = strtonum(optarg, 1, 4, &errstr); + if (errstr) { + fprintf(stderr, + "-M requires an argument between 1 and 4\n"); + usage(); + } + break; + case 'i': + noinventory = 1; + break; +#else + case 'M': + case 'i': + fprintf(stderr, "LLDP-MED support is not built-in\n"); + usage(); + break; +#endif +#ifdef USE_SNMP + case 'x': + snmp = 1; + break; + case 'X': + if (agentx) { + fprintf(stderr, "-X can only be used once\n"); + usage(); + } + snmp = 1; + agentx = optarg; + break; +#else + case 'x': + case 'X': + fprintf(stderr, "SNMP support is not built-in\n"); + usage(); +#endif + break; + case 'S': + if (descr_override) { + fprintf(stderr, "-S can only be used once\n"); + usage(); + } + descr_override = strdup(optarg); + break; + case 'P': + if (platform_override) { + fprintf(stderr, "-P can only be used once\n"); + usage(); + } + platform_override = strdup(optarg); + break; + case 'H': + smart = strtonum(optarg, 0, + sizeof(filters) / sizeof(filters[0]), &errstr); + if (errstr) { + fprintf(stderr, + "-H requires an int between 0 and %zu\n", + sizeof(filters) / sizeof(filters[0])); + usage(); + } + break; + case 'O': + if (config_file) { + fprintf(stderr, "-O can only be used once\n"); + usage(); + } + config_file = optarg; + break; + default: + found = 0; + for (i = 0; protos[i].mode != 0; i++) { + if (ch == protos[i].arg) { + found = 1; + protos[i].enabled++; + } + } + if (!found) usage(); + } + } + + if (version) { + version_display(stdout, "lldpd", version > 1); + exit(0); + } + + if (ctlname == NULL) ctlname = LLDPD_CTL_SOCKET; + + /* Set correct smart mode */ + for (i = 0; (filters[i].a != -1) && (filters[i].a != smart); i++) + ; + if (filters[i].a == -1) { + fprintf(stderr, "Incorrect mode for -H\n"); + usage(); + } + smart = filters[i].b; + + log_init(use_syslog, debug, __progname); + tzset(); /* Get timezone info before chroot */ + if (use_syslog && daemonize) { + /* So, we use syslog and we daemonize (or we are started by + * systemd). No need to continue writing to stdout. */ + int fd; + /* coverity[resource_leak] + fd may be leaked if < 2, it's expected */ + if ((fd = open("/dev/null", O_RDWR, 0)) != -1) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > 2) close(fd); + } + } + log_debug("main", "lldpd " PACKAGE_VERSION " starting..."); + version_check(); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + fatalx("main", "fuzzing enabled, unsafe for production"); +#endif + + /* Grab uid and gid to use for priv sep */ +#ifdef ENABLE_PRIVSEP + if ((user = getpwnam(PRIVSEP_USER)) == NULL) + fatalx("main", + "no " PRIVSEP_USER + " user for privilege separation, please create it"); + uid = user->pw_uid; + if ((group = getgrnam(PRIVSEP_GROUP)) == NULL) + fatalx("main", + "no " PRIVSEP_GROUP + " group for privilege separation, please create it"); + gid = group->gr_gid; +#endif + + /* Create and setup socket */ + int retry = 1; + log_debug("main", "creating control socket"); + while ((ctl = ctl_create(ctlname)) == -1) { + if (retry-- && errno == EADDRINUSE) { + /* Check if a daemon is really listening */ + int tfd; + log_info("main", + "unable to create control socket because it already exists"); + log_info("main", "check if another instance is running"); + if ((tfd = ctl_connect(ctlname)) != -1) { + /* Another instance is running */ + close(tfd); + log_warnx("main", + "another instance is running, please stop it"); + fatalx("main", "giving up"); + } else if (errno == ECONNREFUSED) { + /* Nobody is listening */ + log_info("main", + "old control socket is present, clean it"); + ctl_cleanup(ctlname); + continue; + } + log_warn("main", + "cannot determine if another daemon is already running"); + fatalx("main", "giving up"); + } + log_warn("main", "unable to create control socket at %s", ctlname); + fatalx("main", "giving up"); + } +#ifdef ENABLE_PRIVSEP + if (chown(ctlname, uid, gid) == -1) + log_warn("main", "unable to chown control socket"); + if (chmod(ctlname, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP) == + -1) + log_warn("main", "unable to chmod control socket"); +#endif + + /* Create associated advisory lock file */ + char *lockname = NULL; + int fd; + if (asprintf(&lockname, "%s.lock", ctlname) == -1) + fatal("main", "cannot build lock name"); + if ((fd = open(lockname, O_CREAT | O_RDWR, 0000)) == -1) + fatal("main", "cannot create lock file for control socket"); + close(fd); +#ifdef ENABLE_PRIVSEP + if (chown(lockname, uid, gid) == -1) + log_warn("main", "unable to chown control socket lock"); + if (chmod(lockname, + S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP) == -1) + log_warn("main", "unable to chmod control socket lock"); +#endif + free(lockname); + + /* Disable SIGPIPE */ + signal(SIGPIPE, SIG_IGN); + + /* Disable SIGHUP, until handlers are installed */ + signal(SIGHUP, SIG_IGN); + + /* Daemonization, unless started by systemd or launchd or debug */ +#ifndef HOST_OS_OSX + if (!lldpd_started_by_systemd() && daemonize) { + int pid; + char *spid; + log_debug("main", "going into background"); + if (daemon(0, 1) != 0) fatal("main", "failed to detach daemon"); + if ((pid = open(pidfile, O_TRUNC | O_CREAT | O_WRONLY, 0666)) == -1) + fatal("main", + "unable to open pid file " LLDPD_PID_FILE + " (or the specified one)"); + if (asprintf(&spid, "%d\n", getpid()) == -1) + fatal("main", + "unable to create pid file " LLDPD_PID_FILE + " (or the specified one)"); + if (write(pid, spid, strlen(spid)) == -1) + fatal("main", + "unable to write pid file " LLDPD_PID_FILE + " (or the specified one)"); + free(spid); + close(pid); + } +#endif + + /* Configuration with lldpcli */ + if (lldpcli) { + if (!config_file) { + log_debug("main", + "invoking lldpcli for default configuration locations"); + } else { + log_debug("main", + "invoking lldpcli for user supplied configuration location"); + } + if (lldpd_configure(use_syslog, debug, lldpcli, ctlname, config_file) == + -1) + fatal("main", "unable to spawn lldpcli"); + } + + /* Try to read system information from /etc/os-release if possible. + Fall back to lsb_release for compatibility. */ + log_debug("main", "get OS/LSB release information"); + lsb_release = lldpd_get_os_release(); + if (!lsb_release) { + lsb_release = lldpd_get_lsb_release(); + } + + log_debug("main", "initialize privilege separation"); +#ifdef ENABLE_PRIVSEP + priv_init(PRIVSEP_CHROOT, ctl, uid, gid); +#else + priv_init(); +#endif + + /* Initialization of global configuration */ + if ((cfg = (struct lldpd *)calloc(1, sizeof(struct lldpd))) == NULL) + fatal("main", NULL); + + lldpd_alloc_default_local_port(cfg); + cfg->g_ctlname = ctlname; + cfg->g_ctl = ctl; + cfg->g_config.c_mgmt_pattern = mgmtp; + cfg->g_config.c_cid_pattern = cidp; + cfg->g_config.c_iface_pattern = interfaces; + cfg->g_config.c_smart = smart; + if (lldpcli) cfg->g_config.c_paused = 1; + cfg->g_config.c_receiveonly = receiveonly; + cfg->g_config.c_tx_interval = LLDPD_TX_INTERVAL * 1000; + cfg->g_config.c_tx_hold = LLDPD_TX_HOLD; + cfg->g_config.c_ttl = cfg->g_config.c_tx_interval * cfg->g_config.c_tx_hold; + cfg->g_config.c_ttl = (cfg->g_config.c_ttl + 999) / 1000; + cfg->g_config.c_max_neighbors = LLDPD_MAX_NEIGHBORS; +#ifdef ENABLE_LLDPMED + cfg->g_config.c_enable_fast_start = enable_fast_start; + cfg->g_config.c_tx_fast_init = LLDPD_FAST_INIT; + cfg->g_config.c_tx_fast_interval = LLDPD_FAST_TX_INTERVAL; +#endif +#ifdef USE_SNMP + cfg->g_snmp = snmp; + cfg->g_snmp_agentx = agentx; +#endif /* USE_SNMP */ + cfg->g_config.c_bond_slave_src_mac_type = + LLDP_BOND_SLAVE_SRC_MAC_TYPE_LOCALLY_ADMINISTERED; + + /* Get ioctl socket */ + log_debug("main", "get an ioctl socket"); + if ((cfg->g_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) + fatal("main", "failed to get ioctl socket"); + + /* Description */ + if (!(cfg->g_config.c_advertise_version = advertise_version) && lsb_release && + lsb_release[strlen(lsb_release) - 1] == '\n') + lsb_release[strlen(lsb_release) - 1] = '\0'; + cfg->g_lsb_release = lsb_release; + if (descr_override) cfg->g_config.c_description = descr_override; + + if (platform_override) cfg->g_config.c_platform = platform_override; + + /* Set system capabilities */ + log_debug("main", "set system capabilities"); + if ((lchassis = (struct lldpd_chassis *)calloc(1, + sizeof(struct lldpd_chassis))) == NULL) + fatal("localchassis", NULL); + cfg->g_config.c_cap_advertise = 1; + cfg->g_config.c_cap_override = 0; + lchassis->c_cap_available = + LLDP_CAP_BRIDGE | LLDP_CAP_WLAN | LLDP_CAP_ROUTER | LLDP_CAP_STATION; + cfg->g_config.c_mgmt_advertise = 1; + TAILQ_INIT(&lchassis->c_mgmt); +#ifdef ENABLE_LLDPMED + if (lldpmed > 0) { + if (lldpmed == LLDP_MED_CLASS_III) + lchassis->c_cap_available |= LLDP_CAP_TELEPHONE; + lchassis->c_med_type = lldpmed; + lchassis->c_med_cap_available = LLDP_MED_CAP_CAP | LLDP_MED_CAP_IV | + LLDP_MED_CAP_LOCATION | LLDP_MED_CAP_POLICY | LLDP_MED_CAP_MDI_PSE | + LLDP_MED_CAP_MDI_PD; + cfg->g_config.c_noinventory = noinventory; + } else + cfg->g_config.c_noinventory = 1; +#endif + + log_debug("main", "initialize protocols"); + cfg->g_protocols = protos; + for (i = 0; protos[i].mode != 0; i++) { + + /* With -ll, disable LLDP */ + if (protos[i].mode == LLDPD_MODE_LLDP) protos[i].enabled %= 3; + /* With -ccc force CDPV2, enable CDPV1 */ + if (protos[i].mode == LLDPD_MODE_CDPV1 && protos[i].enabled == 3) { + protos[i].enabled = 1; + } + /* With -cc force CDPV1, enable CDPV2 */ + if (protos[i].mode == LLDPD_MODE_CDPV2 && protos[i].enabled == 2) { + protos[i].enabled = 1; + } + + /* With -cccc disable CDPV1, enable CDPV2 */ + if (protos[i].mode == LLDPD_MODE_CDPV1 && protos[i].enabled >= 4) { + protos[i].enabled = 0; + } + + /* With -cccc disable CDPV1, enable CDPV2; -ccccc will force CDPv2 */ + if (protos[i].mode == LLDPD_MODE_CDPV2 && protos[i].enabled == 4) { + protos[i].enabled = 1; + } + + if (protos[i].enabled > 1) + log_info("main", "protocol %s enabled and forced", + protos[i].name); + else if (protos[i].enabled) + log_info("main", "protocol %s enabled", protos[i].name); + else + log_info("main", "protocol %s disabled", protos[i].name); + } + + TAILQ_INIT(&cfg->g_hardware); + TAILQ_INIT(&cfg->g_chassis); + TAILQ_INSERT_TAIL(&cfg->g_chassis, lchassis, c_entries); + lchassis->c_refcount++; /* We should always keep a reference to local chassis */ + + /* Main loop */ + log_debug("main", "start main loop"); + levent_loop(cfg); + lchassis->c_refcount--; + lldpd_exit(cfg); + free(cfg); + + return (0); +} |