diff options
Diffstat (limited to 'src/daemon/priv-linux.c')
-rw-r--r-- | src/daemon/priv-linux.c | 337 |
1 files changed, 337 insertions, 0 deletions
diff --git a/src/daemon/priv-linux.c b/src/daemon/priv-linux.c new file mode 100644 index 0000000..9a6f90a --- /dev/null +++ b/src/daemon/priv-linux.c @@ -0,0 +1,337 @@ +/* -*- 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 <unistd.h> +#include <inttypes.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <limits.h> +#include <fcntl.h> +#include <errno.h> +#include <regex.h> +#include <sys/ioctl.h> +#include <netpacket/packet.h> /* For sockaddr_ll */ +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdocumentation" +#endif +#include <linux/filter.h> /* For BPF filtering */ +#include <linux/sockios.h> +#include <linux/if_ether.h> +#include <linux/ethtool.h> +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + +/* Defined in linux/pkt_sched.h */ +#define TC_PRIO_CONTROL 7 +/* Defined in sysfs/libsysfs.h */ +#define SYSFS_PATH_MAX 256 + +/* Proxy for open */ +int +priv_open(const char *file) +{ + int len, rc; + enum priv_cmd cmd = PRIV_OPEN; + must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd)); + len = strlen(file); + must_write(PRIV_UNPRIVILEGED, &len, sizeof(int)); + must_write(PRIV_UNPRIVILEGED, file, len); + priv_wait(); + must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int)); + if (rc == -1) return rc; + return receive_fd(PRIV_UNPRIVILEGED); +} + +void +asroot_open() +{ + const char *authorized[] = { PROCFS_SYS_NET "ipv4/ip_forward", + PROCFS_SYS_NET "ipv6/conf/all/forwarding", + "/proc/net/bonding/[^.][^/]*", "/proc/self/net/bonding/[^.][^/]*", +#ifdef ENABLE_OLDIES + SYSFS_CLASS_NET "[^.][^/]*/brforward", + SYSFS_CLASS_NET "[^.][^/]*/brport", + SYSFS_CLASS_NET "[^.][^/]*/brif/[^.][^/]*/port_no", +#endif + SYSFS_CLASS_DMI "product_version", SYSFS_CLASS_DMI "product_serial", + SYSFS_CLASS_DMI "product_name", SYSFS_CLASS_DMI "bios_version", + SYSFS_CLASS_DMI "sys_vendor", SYSFS_CLASS_DMI "chassis_asset_tag", + NULL }; + const char **f; + char *file; + int fd, len, rc; + regex_t preg; + + must_read(PRIV_PRIVILEGED, &len, sizeof(len)); + if (len < 0 || len > PATH_MAX) fatalx("privsep", "too large value requested"); + if ((file = (char *)malloc(len + 1)) == NULL) fatal("privsep", NULL); + must_read(PRIV_PRIVILEGED, file, len); + file[len] = '\0'; + + for (f = authorized; *f != NULL; f++) { + if (regcomp(&preg, *f, REG_NOSUB) != 0) /* Should not happen */ + fatal("privsep", "unable to compile a regex"); + if (regexec(&preg, file, 0, NULL, 0) == 0) { + regfree(&preg); + break; + } + regfree(&preg); + } + if (*f == NULL) { + log_warnx("privsep", "not authorized to open %s", file); + rc = -1; + must_write(PRIV_PRIVILEGED, &rc, sizeof(int)); + free(file); + return; + } + if ((fd = open(file, O_RDONLY)) == -1) { + rc = -1; + must_write(PRIV_PRIVILEGED, &rc, sizeof(int)); + free(file); + return; + } + free(file); + must_write(PRIV_PRIVILEGED, &fd, sizeof(int)); + send_fd(PRIV_PRIVILEGED, fd); + close(fd); +} + +/* Quirks needed by some additional interfaces. Currently, this is limited to + * disabling LLDP firmware for i40e. */ +static void +asroot_iface_init_quirks(int ifindex, char *name) +{ + int s = -1; + int fd = -1; + + /* Check driver. */ + struct ethtool_drvinfo ethc = { .cmd = ETHTOOL_GDRVINFO }; + struct ifreq ifr = { .ifr_data = (caddr_t)ðc }; + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { + log_warn("privsep", "unable to open a socket"); + goto end; + } + strlcpy(ifr.ifr_name, name, IFNAMSIZ); + if (ioctl(s, SIOCETHTOOL, &ifr) != 0 || + strncmp("i40e", ethc.driver, sizeof(ethc.driver))) { + /* Not i40e */ + goto end; + } + log_info("interfaces", + "i40e driver detected for %s, disabling LLDP in firmware", name); + + /* We assume debugfs is mounted. Otherwise, we would need to check if it + * is mounted, then unshare a new mount namespace, mount it, issues the + * command, leave the namespace. Let's see if there is such a need. */ + + /* Alternative is to use ethtool (ethtool --set-priv-flags ens5f0 + * disable-fw-lldp on). However, this requires a recent firmware (from + * i40e_ethtool.c): + * + * If the driver detected FW LLDP was disabled on init, this flag could + * be set, however we do not support _changing_ the flag: + * - on XL710 if NPAR is enabled or FW API version < 1.7 + * - on X722 with FW API version < 1.6 + */ + + char command[] = "lldp stop"; + char sysfs_path[SYSFS_PATH_MAX + 1]; + if (snprintf(sysfs_path, SYSFS_PATH_MAX, "/sys/kernel/debug/i40e/%.*s/command", + (int)sizeof(ethc.bus_info), ethc.bus_info) >= SYSFS_PATH_MAX) { + log_warnx("interfaces", "path truncated"); + goto end; + } + if ((fd = open(sysfs_path, O_WRONLY)) == -1) { + if (errno == ENOENT) { + log_info("interfaces", + "%s does not exist, " + "cannot disable LLDP in firmware for %s", + sysfs_path, name); + goto end; + } + log_warn("interfaces", + "cannot open %s to disable LLDP in firmware for %s", sysfs_path, + name); + goto end; + } + if (write(fd, command, sizeof(command) - 1) == -1) { + log_warn("interfaces", "cannot disable LLDP in firmware for %s", name); + goto end; + } +end: + if (s != -1) close(s); + if (fd != -1) close(fd); +} + +int +asroot_iface_init_os(int ifindex, char *name, int *fd) +{ + int rc; + /* Open listening socket to receive/send frames */ + if ((*fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) { + rc = errno; + return rc; + } + + struct sockaddr_ll sa = { .sll_family = AF_PACKET, .sll_ifindex = ifindex }; + if (bind(*fd, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + rc = errno; + log_warn("privsep", "unable to bind to raw socket for interface %s", + name); + return rc; + } + + /* Set filter */ + log_debug("privsep", "set BPF filter for %s", name); + static struct sock_filter lldpd_filter_f[] = { LLDPD_FILTER_F }; + struct sock_fprog prog = { .filter = lldpd_filter_f, + .len = sizeof(lldpd_filter_f) / sizeof(struct sock_filter) }; + if (setsockopt(*fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog)) < 0) { + rc = errno; + log_warn("privsep", "unable to change filter for %s", name); + return rc; + } + + /* Set priority to TC_PRIO_CONTROL for ice Intel cards. See #444. */ + int prio = TC_PRIO_CONTROL; + if (setsockopt(*fd, SOL_SOCKET, SO_PRIORITY, &prio, sizeof(prio)) < 0) { + rc = errno; + log_warn("privsep", + "unable to set priority \"control\" to socket for interface %s", + name); + return rc; + } + +#ifdef SO_LOCK_FILTER + int lock = 1; + if (setsockopt(*fd, SOL_SOCKET, SO_LOCK_FILTER, &lock, sizeof(lock)) < 0) { + if (errno != ENOPROTOOPT) { + rc = errno; + log_warn("privsep", "unable to lock filter for %s", name); + return rc; + } + } +#endif +#ifdef PACKET_IGNORE_OUTGOING + int ignore = 1; + if (setsockopt(*fd, SOL_PACKET, PACKET_IGNORE_OUTGOING, &ignore, + sizeof(ignore)) < 0) { + if (errno != ENOPROTOOPT) { + rc = errno; + log_warn("privsep", + "unable to set packet direction for BPF filter on %s", + name); + return rc; + } + } +#endif + + asroot_iface_init_quirks(ifindex, name); + return 0; +} + +int +asroot_iface_description_os(const char *name, const char *description) +{ + /* We could use netlink but this is a lot to do in a privileged + * process. Just write to /sys/class/net/XXXX/ifalias. */ + char *file; + char descr[IFALIASZ]; + FILE *fp; + int rc; + if (name[0] == '\0' || name[0] == '.') { + log_warnx("privsep", "odd interface name %s", name); + return -1; + } + if (asprintf(&file, SYSFS_CLASS_NET "%s/ifalias", name) == -1) { + log_warn("privsep", + "unable to allocate memory for setting description"); + return -1; + } + if ((fp = fopen(file, "r+")) == NULL) { + rc = errno; + log_debug("privsep", "cannot open interface description for %s: %s", + name, strerror(errno)); + free(file); + return rc; + } + free(file); + if (strlen(description) == 0 && fgets(descr, sizeof(descr), fp) != NULL) { + if (strncmp(descr, "lldpd: ", 7) == 0) { + if (strncmp(descr + 7, "was ", 4) == 0) { + /* Already has an old neighbor */ + fclose(fp); + return 0; + } else { + /* Append was */ + memmove(descr + 11, descr + 7, sizeof(descr) - 11); + memcpy(descr, "lldpd: was ", 11); + } + } else { + /* No description, no neighbor */ + strlcpy(descr, "lldpd: no neighbor", sizeof(descr)); + } + } else + snprintf(descr, sizeof(descr), "lldpd: connected to %s", description); + if (fputs(descr, fp) == EOF) { + log_debug("privsep", "cannot set interface description for %s", name); + fclose(fp); + return -1; + } + fclose(fp); + return 0; +} + +int +asroot_iface_promisc_os(const char *name) +{ + int s, rc; + if ((s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) { + rc = errno; + log_warn("privsep", "unable to open raw socket"); + return rc; + } + + struct ifreq ifr = {}; + strlcpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); + + if (ioctl(s, SIOCGIFFLAGS, &ifr) == -1) { + rc = errno; + log_warn("privsep", "unable to get interface flags for %s", name); + close(s); + return rc; + } + + if (ifr.ifr_flags & IFF_PROMISC) { + close(s); + return 0; + } + ifr.ifr_flags |= IFF_PROMISC; + if (ioctl(s, SIOCSIFFLAGS, &ifr) == -1) { + rc = errno; + log_warn("privsep", "unable to set promisc mode for %s", name); + close(s); + return rc; + } + log_info("privsep", "promiscuous mode enabled for %s", name); + close(s); + return 0; +} |