diff --git a/Makefile.am b/Makefile.am index e3f03a8..1f879da 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,7 +8,7 @@ AM_CPPFLAGS = -DSYSCONFDIR=\"@sysconfdir@\" -DRUNSTATEDIR=\"@runstated mini_snmpd_SOURCES = mini-snmpd.c mini-snmpd.h linux.c freebsd.c mib.c \ globals.c protocol.c utils.c compat.h if HAVE_CONFUSE -mini_snmpd_SOURCES += conf.c +mini_snmpd_SOURCES += conf.c linux_ethtool.c endif mini_snmpd_CPPFLAGS = $(AM_CPPFLAGS) mini_snmpd_CFLAGS = -W -Wall -Wextra -std=gnu99 diff --git a/conf.c b/conf.c index 4de936b..9f9c1ef 100644 --- a/conf.c +++ b/conf.c @@ -17,6 +17,7 @@ #include #include #include +#include "ethtool-conf.h" #include "mini-snmpd.h" static cfg_t *cfg = NULL; @@ -66,6 +67,21 @@ static size_t get_list(cfg_t *cfg, const char *key, char **list, size_t len) int read_config(char *file) { int rc = 0; + cfg_opt_t ethtool_opts[] = { + CFG_STR("rx_bytes", NULL, CFGF_NONE), + CFG_STR("rx_mc_packets", NULL, CFGF_NONE), + CFG_STR("rx_bc_packets", NULL, CFGF_NONE), + CFG_STR("rx_packets", NULL, CFGF_NONE), + CFG_STR("rx_errors", NULL, CFGF_NONE), + CFG_STR("rx_drops", NULL, CFGF_NONE), + CFG_STR("tx_bytes", NULL, CFGF_NONE), + CFG_STR("tx_mc_packets", NULL, CFGF_NONE), + CFG_STR("tx_bc_packets", NULL, CFGF_NONE), + CFG_STR("tx_packets", NULL, CFGF_NONE), + CFG_STR("tx_errors", NULL, CFGF_NONE), + CFG_STR("tx_drops", NULL, CFGF_NONE), + CFG_END() + }; cfg_opt_t opts[] = { CFG_STR ("location", NULL, CFGF_NONE), CFG_STR ("contact", NULL, CFGF_NONE), @@ -76,6 +92,7 @@ int read_config(char *file) CFG_STR ("vendor", VENDOR, CFGF_NONE), CFG_STR_LIST("disk-table", "/", CFGF_NONE), CFG_STR_LIST("iface-table", NULL, CFGF_NONE), + CFG_SEC("ethtool", ethtool_opts, CFGF_MULTI | CFGF_TITLE | CFGF_NO_TITLE_DUPES), CFG_END() }; @@ -118,6 +135,8 @@ int read_config(char *file) g_vendor = get_string(cfg, "vendor"); + ethtool_xlate_cfg(cfg); + error: cfg_free(cfg); return rc; diff --git a/configure.ac b/configure.ac index 96d6bfa..da9ce70 100644 --- a/configure.ac +++ b/configure.ac @@ -52,6 +52,10 @@ AC_ARG_ENABLE(ipv6, AS_HELP_STRING([--disable-ipv6], [Disable IPv6 support, enabled by default]), [enable_ipv6=$enableval], [enable_ipv6=yes]) +AC_ARG_ENABLE(ethtool, + AS_HELP_STRING([--enable-ethtool], [Enable ethtool interface stats, disabled by default]), + [enable_ethtool=$enableval], [enable_ethtool=no]) + ### Enable features ########################################################################### AS_IF([test "x$with_vendor" != "xno"],[ AS_IF([test "x$vendor" = "xyes"],[ @@ -74,6 +78,9 @@ AS_IF([test "x$enable_demo" = "xyes"],[ AS_IF([test "x$enable_ipv6" != "xno"],[ AC_DEFINE(CONFIG_ENABLE_IPV6, 1, [Define to enable IPv6 support.])]) +AS_IF([test "x$enable_ethtool" != "xno"],[ + AC_DEFINE(CONFIG_ENABLE_ETHTOOL, 1, [Define to enable ethtool stats.])]) + # Check where to install the systemd .service file AS_IF([test "x$with_systemd" = "xyes" -o "x$with_systemd" = "xauto"], [ def_systemd=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) @@ -124,6 +131,7 @@ cat < + * + * This file may be distributed and/or modified under the terms of the + * GNU General Public License version 2 as published by the Free Software + * Foundation and appearing in the file LICENSE.GPL included in the + * packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * See COPYING for GPL licensing information. + */ + +#ifndef ETHTOOL_CONF_H_ +#define ETHTOOL_CONF_H_ + +#include +#include "config.h" +#include "mini-snmpd.h" + +#ifdef CONFIG_ENABLE_ETHTOOL +void ethtool_xlate_cfg(cfg_t *cfg); +#else +static inline void ethtool_xlate_cfg(cfg_t *cfg) +{ + if (cfg_size(cfg, "ethtool") > 0) + logit(LOG_WARNING, 0, "No ethtool support. Ignoring config section"); +} +#endif + +#endif /* ETHTOOL_CONF_H_ */ + +/* vim: ts=4 sts=4 sw=4 nowrap + */ + diff --git a/linux.c b/linux.c index 86a7085..c726700 100644 --- a/linux.c +++ b/linux.c @@ -262,18 +262,20 @@ void get_netinfo(netinfo_t *netinfo) sll = (struct sockaddr_ll *)ifa->ifa_addr; memcpy(netinfo->mac_addr[i], sll->sll_addr, sizeof(netinfo->mac_addr[i])); - /* XXX: Tx multicast and Rx/Tx broadcast not available atm. */ - fields[i].prefix = g_interface_list[i]; - fields[i].len = 12; - fields[i].value[0] = &netinfo->rx_bytes[i]; - fields[i].value[1] = &netinfo->rx_packets[i]; - fields[i].value[2] = &netinfo->rx_errors[i]; - fields[i].value[3] = &netinfo->rx_drops[i]; - fields[i].value[7] = &netinfo->rx_mc_packets[i]; - fields[i].value[8] = &netinfo->tx_bytes[i]; - fields[i].value[9] = &netinfo->tx_packets[i]; - fields[i].value[10] = &netinfo->tx_errors[i]; - fields[i].value[11] = &netinfo->tx_drops[i]; + if (ethtool_gstats(i, netinfo, &fields[i]) < 0) { + /* XXX: Tx multicast and Rx/Tx broadcast not available atm. */ + fields[i].prefix = g_interface_list[i]; + fields[i].len = 12; + fields[i].value[0] = &netinfo->rx_bytes[i]; + fields[i].value[1] = &netinfo->rx_packets[i]; + fields[i].value[2] = &netinfo->rx_errors[i]; + fields[i].value[3] = &netinfo->rx_drops[i]; + fields[i].value[7] = &netinfo->rx_mc_packets[i]; + fields[i].value[8] = &netinfo->tx_bytes[i]; + fields[i].value[9] = &netinfo->tx_packets[i]; + fields[i].value[10] = &netinfo->tx_errors[i]; + fields[i].value[11] = &netinfo->tx_drops[i]; + } if (-1 == read_file_value(&netinfo->if_mtu[i], "/sys/class/net/%s/mtu", g_interface_list[i])) netinfo->if_mtu[i] = 1500; /* Fallback */ diff --git a/linux_ethtool.c b/linux_ethtool.c new file mode 100644 index 0000000..0e60300 --- /dev/null +++ b/linux_ethtool.c @@ -0,0 +1,260 @@ +/* Linux ethtool helpers + * + * Copyright (C) 2020 Bjørn Mork + * + * This file may be distributed and/or modified under the terms of the + * GNU General Public License version 2 as published by the Free Software + * Foundation and appearing in the file LICENSE.GPL included in the + * packaging of this file. + * + * This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + * WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * See COPYING for GPL licensing information. + */ +#ifdef __linux__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ethtool-conf.h" +#include "mini-snmpd.h" + +#ifdef CONFIG_ENABLE_ETHTOOL + +typedef unsigned long long u64; +typedef uint32_t u32; +typedef uint16_t u16; +typedef uint8_t u8; +typedef int32_t s32; + +/* counter offsets and number of counters per interface */ +static struct ethtool_s { + int n_stats; + int rx_bytes; + int rx_mc_packets; + int rx_bc_packets; + int rx_packets; + int rx_errors; + int rx_drops; + int tx_bytes; + int tx_mc_packets; + int tx_bc_packets; + int tx_packets; + int tx_errors; + int tx_drops; +} ethtool[MAX_NR_INTERFACES]; + +/* ethtool socket */ +static int fd = -1; + +static int ethtool_init() +{ + fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC); + if (fd < 0) + logit(LOG_ERR, errno, "Cannot get control socket"); + return fd; +} + +static struct ethtool_gstrings *get_stringset(const char *iname) +{ + struct ifreq ifr = {}; + struct { + struct ethtool_sset_info hdr; + u32 buf[1]; + } sset_info; + u32 len; + struct ethtool_gstrings *strings; + + sset_info.hdr.cmd = ETHTOOL_GSSET_INFO; + sset_info.hdr.reserved = 0; + sset_info.hdr.sset_mask = 1ULL << ETH_SS_STATS; + ifr.ifr_data = (void *)&sset_info; + strcpy(ifr.ifr_name, iname); + if (ioctl(fd, SIOCETHTOOL, &ifr) == 0) { + const u32 *sset_lengths = sset_info.hdr.data; + + len = sset_info.hdr.sset_mask ? sset_lengths[0] : 0; + } else { + return NULL; + } + + strings = calloc(1, sizeof(*strings) + len * ETH_GSTRING_LEN); + if (!strings) + return NULL; + + strings->cmd = ETHTOOL_GSTRINGS; + strings->string_set = ETH_SS_STATS; + strings->len = len; + ifr.ifr_data = (void *)strings; + if (len != 0 && ioctl(fd, SIOCETHTOOL, &ifr)) { + free(strings); + return NULL; + } + + return strings; +} + +static int ethtool_match_string(const char *key, struct ethtool_gstrings *strings) +{ + unsigned int i; + + if (!key) + return -1; + for (i = 0; i < strings->len; i++) { + if (!strncmp(key, (char *)&strings->data[i * ETH_GSTRING_LEN], ETH_GSTRING_LEN)) { + logit(LOG_DEBUG, 0, "found '%s' match at index %u", key, i); + return i; + } + } + return -1; +} + +#define ethtool_parse_opt(_name) \ + ethtool[intf]._name = ethtool_match_string(cfg_getstr(cfg, #_name), strings); \ + if (ethtool[intf]._name >= 0) \ + found = 1; + +static void ethtool_xlate_intf(cfg_t *cfg, int intf, const char *iname) +{ + struct ethtool_gstrings *strings = get_stringset(iname); + int found = 0; + + if (!strings) + return; + + logit(LOG_DEBUG, 0, "got ethtool stats strings for '%s'", iname); + + ethtool_parse_opt(rx_bytes); + ethtool_parse_opt(rx_mc_packets); + ethtool_parse_opt(rx_bc_packets); + ethtool_parse_opt(rx_packets); + ethtool_parse_opt(rx_errors); + ethtool_parse_opt(rx_drops); + ethtool_parse_opt(tx_bytes); + ethtool_parse_opt(tx_mc_packets); + ethtool_parse_opt(tx_bc_packets); + ethtool_parse_opt(tx_packets); + ethtool_parse_opt(tx_errors); + ethtool_parse_opt(tx_drops); + + /* save the size of the stats table if we found at least one macth */ + if (found) + ethtool[intf].n_stats = strings->len; + else + logit(LOG_DEBUG, 0, "fount no matching string for '%s'", iname); + + free(strings); +} + +void ethtool_xlate_cfg(cfg_t *cfg) +{ + cfg_t *ethtool; + const char *iname; + unsigned int i, j; + int intf; + + if (ethtool_init() < 0) + return; + + for (i = 0; i < cfg_size(cfg, "ethtool"); i++) { + ethtool = cfg_getnsec(cfg, "ethtool", i); + iname = cfg_title(ethtool); + logit(LOG_INFO, 0, "Parsing ethtool section '%s'", iname); + + /* exact match? */ + intf = find_ifname((char *)iname); + if (intf >= 0) { + ethtool_xlate_intf(ethtool, intf, iname); + continue; + } + + /* or wildcard match? */ + if (strcspn(iname, "*?[")) { + for (j = 0; j < g_interface_list_length; j++) { + if (!fnmatch(iname, g_interface_list[j], 0)) + ethtool_xlate_intf(ethtool, j, g_interface_list[j]); + } + } + } +} + +#define set_val(_fieldnum, _name) \ + if (ethtool[intf]._name >= 0 && ethtool[intf]._name < ethtool[intf].n_stats) \ + netinfo->_name[intf] = stats->data[ethtool[intf]._name]; \ + else if (_fieldnum >= 0) { \ + fallback = 1; \ + field->value[_fieldnum] = &netinfo->_name[intf]; \ + } + +int ethtool_gstats(int intf, netinfo_t *netinfo, field_t *field) +{ + struct ifreq ifr = {}; + struct ethtool_stats *stats; + unsigned int sz_stats; + int fallback = 0; + + if (fd < 0) + return fd; + if (!ethtool[intf].n_stats) + return -1; + + sz_stats = ethtool[intf].n_stats * sizeof(u64); + stats = calloc(1, sz_stats + sizeof(struct ethtool_stats)); + if (!stats) { + logit(LOG_ERR, ENOMEM, "cannot allocate mem for ethtool stats"); + return -ENOMEM; + } + + stats->cmd = ETHTOOL_GSTATS; + stats->n_stats = ethtool[intf].n_stats; + strcpy(ifr.ifr_name, g_interface_list[intf]); + ifr.ifr_data = (void *)stats; + if (ioctl(fd, SIOCETHTOOL, &ifr) < 0) { + logit(LOG_ERR, errno, "Cannot get ethtool stats"); + free(stats); + return -errno; + } + + set_val( 0, rx_bytes); + set_val( 7, rx_mc_packets); + set_val(-1, rx_bc_packets); + set_val( 1, rx_packets); + set_val( 2, rx_errors); + set_val( 3, rx_drops); + set_val( 8, tx_bytes); + set_val(-1, tx_mc_packets); + set_val(-1, tx_bc_packets); + set_val( 9, tx_packets); + set_val(10, tx_errors); + set_val(11, tx_drops); + + /* we can avoid parsing values from the dev file if there is no fallback counter */ + if (fallback) { + field->prefix = g_interface_list[intf]; + field->len = 12; + } + free(stats); + + return 0; +} + +#endif /* CONFIG_ENABLE_ETHTOOL */ +#endif /* __linux__ */ + +/* vim: ts=4 sts=4 sw=4 nowrap + */ diff --git a/mini-snmpd.conf b/mini-snmpd.conf index 0e56a1f..4bdc138 100644 --- a/mini-snmpd.conf +++ b/mini-snmpd.conf @@ -18,4 +18,20 @@ timeout = 1 disk-table = { "/", } # Interfaces to monitor, currently only for IF-MIB::ifTable -#iface-table = { "eth0", "eth1" } \ No newline at end of file +#iface-table = { "eth0", "eth1" } + +# Use ethtool statistics +#ethtool "eth*" { +# rx_bytes = ifInOctets +# rx_mc_packets = ifInMulticastPkts +# rx_bc_packets = ifInBroadcastPkts +# rx_packets = ifInUcastPkts +# rx_errors = Jabbers +# rx_drops = dot1dTpPortInDiscards +# tx_bytes = ifOutOctets +# tx_mc_packets = ifOutMulticastPkts +# tx_bc_packets = ifOutBroadcastPkts +# tx_packets = ifOutUcastPkts +# tx_errors = Collisions +# tx_drops = ifOutDiscards +#} diff --git a/mini-snmpd.h b/mini-snmpd.h index 9c3a3a3..d2a9047 100644 --- a/mini-snmpd.h +++ b/mini-snmpd.h @@ -375,6 +375,12 @@ int mib_update (int full); value_t *mib_find (const oid_t *oid, size_t *pos); value_t *mib_findnext (const oid_t *oid); +#ifdef CONFIG_ENABLE_ETHTOOL +int ethtool_gstats(int intf, netinfo_t *netinfo, field_t *field); +#else +#define ethtool_gstats(intf, netinfo, field) (-1) +#endif + #endif /* MINI_SNMPD_H_ */ /* vim: ts=4 sts=4 sw=4 nowrap