diff --git a/README.md b/README.md index 45363c9..e249330 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,16 @@ You need to edit the `collectd.conf` configuration file. The collectd service should start automatically on boot. + +brcm_wl module +================ + +The inclusion of the `brcm_wl` plugin enables collection of detailed statistics +of wireless clients, such as association time, idle time, TX/RX bytes and +packets, RSSI levels, etc. + +This plugin only works on (certain?) Broadcom wireless chipsets, because it +communicates directly with the driver to retrieve the information. + +I have tested it to work on the *ASUS RT-AC68U* and the *D-Link DIR-868L*. + diff --git a/build.sh b/build.sh index 1da6a5b..132328a 100755 --- a/build.sh +++ b/build.sh @@ -7,7 +7,7 @@ set -e PKGVER=5.9.0 -PKGREL=1 +PKGREL=2 PKGURL="https://collectd.org/files/collectd-$PKGVER.tar.bz2" SHA256SUM=7b220f8898a061f6e7f29a8c16697d1a198277f813da69474a67911097c0626b @@ -48,6 +48,10 @@ extract() { prepare() { cd collectd-$PKGVER + + patch -p1 -t -i $srcdir/files/001-brcm_wl.patch + cp $srcdir/files/brcm_wl.c src/ + cp $srcdir/files/wl_ioctl.h src/ } build() { @@ -78,6 +82,10 @@ package() { find installroot -type f -path '*/sbin/collectdmon' -exec rm -f {} \; find installroot -depth -empty -exec rmdir {} \; + # config files + mv installroot/opt/etc/collectd.conf installroot/opt/etc/collectd.conf.def + install -m0644 -D $srcdir/files/collectd.conf installroot/opt/etc/collectd.conf + # add in our files install -m0644 -D $srcdir/files/collectd.control installroot/opt/lib/ipkg/info/collectd.control install -m0755 -D $srcdir/files/S99collectd installroot/opt/etc/init.d/S99collectd diff --git a/files/001-brcm_wl.patch b/files/001-brcm_wl.patch new file mode 100644 index 0000000..e058638 --- /dev/null +++ b/files/001-brcm_wl.patch @@ -0,0 +1,60 @@ +diff --git a/Makefile.am b/Makefile.am +index 85f8da8a..ae2a8f29 100644 +--- a/Makefile.am ++++ b/Makefile.am +@@ -752,6 +752,12 @@ bind_la_LDFLAGS = $(PLUGIN_LDFLAGS) + bind_la_LIBADD = $(BUILD_WITH_LIBCURL_LIBS) $(BUILD_WITH_LIBXML2_LIBS) + endif + ++if BUILD_PLUGIN_BRCM_WL ++pkglib_LTLIBRARIES += brcm_wl.la ++brcm_wl_la_SOURCES = src/brcm_wl.c ++brcm_wl_la_LDFLAGS = $(PLUGIN_LDFLAGS) ++endif ++ + if BUILD_PLUGIN_CEPH + pkglib_LTLIBRARIES += ceph.la + ceph_la_SOURCES = src/ceph.c +diff --git a/configure.ac b/configure.ac +index c95422f4..d8ef9c0d 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -6367,6 +6367,7 @@ plugin_ascent="no" + plugin_barometer="no" + plugin_battery="no" + plugin_bind="no" ++plugin_brcm_wl="yes" + plugin_ceph="no" + plugin_cgroups="no" + plugin_conntrack="no" +@@ -6790,6 +6791,7 @@ AC_PLUGIN([ascent], [$plugin_ascent], [AscentEmu player + AC_PLUGIN([barometer], [$plugin_barometer], [Barometer sensor on I2C]) + AC_PLUGIN([battery], [$plugin_battery], [Battery statistics]) + AC_PLUGIN([bind], [$plugin_bind], [ISC Bind nameserver statistics]) ++AC_PLUGIN([brcm_wl], [$plugin_brcm_wl], [Broadcom wireless stats]) + AC_PLUGIN([ceph], [$plugin_ceph], [Ceph daemon statistics]) + AC_PLUGIN([cgroups], [$plugin_cgroups], [CGroups CPU usage accounting]) + AC_PLUGIN([chrony], [yes], [Chrony statistics]) +@@ -7217,6 +7219,7 @@ AC_MSG_RESULT([ ascent . . . . . . . $enable_ascent]) + AC_MSG_RESULT([ barometer . . . . . . $enable_barometer]) + AC_MSG_RESULT([ battery . . . . . . . $enable_battery]) + AC_MSG_RESULT([ bind . . . . . . . . $enable_bind]) ++AC_MSG_RESULT([ brcm_wl . . . . . . $enable_brcm_wl]) + AC_MSG_RESULT([ ceph . . . . . . . . $enable_ceph]) + AC_MSG_RESULT([ cgroups . . . . . . . $enable_cgroups]) + AC_MSG_RESULT([ chrony. . . . . . . . $enable_chrony]) +diff --git a/src/types.db b/src/types.db +index 69f59b06..cfaad6c1 100644 +--- a/src/types.db ++++ b/src/types.db +@@ -282,6 +282,10 @@ vs_memory value:GAUGE:0:9223372036854775807 + vs_processes value:GAUGE:0:65535 + vs_threads value:GAUGE:0:65535 + ++wl_traffic tx_ucast_pkts:GAUGE:0:U, tx_ucast_bytes:GAUGE:0:U, tx_mcast_pkts:GAUGE:0:U, tx_mcast_bytes:GAUGE:0:U, rx_ucast_pkts:GAUGE:0:U, rx_ucast_bytes:GAUGE:0:U, rx_mcast_pkts:GAUGE:0:U, rx_mcast_bytes:GAUGE:0:U, decrypt_failures:GAUGE:0:U ++wl_stats assoctime:GAUGE:0:U, idletime:GAUGE:0:U, rssi0:GAUGE:-100:0, rssi1:GAUGE:-100:0, rssi2:GAUGE:-100:0, rssi3:GAUGE:-100:0 ++ ++ + # + # Legacy types + # (required for the v5 upgrade target) diff --git a/files/brcm_wl.c b/files/brcm_wl.c new file mode 100644 index 0000000..da4d448 --- /dev/null +++ b/files/brcm_wl.c @@ -0,0 +1,334 @@ +/* + * brcm_wl.c + * Broadcom wireless clients stats plugin for collectd + * + * Copyright (C) 2019 Darell Tan + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "collectd.h" +#include "plugin.h" +#include "utils/common/common.h" + +#include +#include +#include + +#include "wl_ioctl.h" + + +#define PLUGIN_NAME "wireless_stations" + +#define IOCTL_BUF_SIZE 8192 + +// 64 interfaces ought to be enough for everyone +static char *interfaces[64] = {NULL}; + +static bool secondsResolution = true; +static bool normalizeTimestamps = true; + +static bool warnedStructSize = false; + +static const char *config_keys[] = { + "Interface", + "SecondsResolution", + "NormalizeTimestamps", +}; + +int sockfd = -1; +void *ioctl_buf = NULL; + +static int init_sock() { + sockfd = socket(AF_INET, SOCK_DGRAM, 0); + if (sockfd < 0) { + ERROR("cant open sock: %s", STRERRNO); + } else { + ioctl_buf = malloc(IOCTL_BUF_SIZE); + if (ioctl_buf == NULL) { + close(sockfd); + sockfd = -1; + } + } + + return sockfd; +} + +static void close_sock() { + if (sockfd >= 0) { + close(sockfd); + sockfd = -1; + } + + if (ioctl_buf != NULL) { + free(ioctl_buf); + ioctl_buf = NULL; + } +} + +static int wl_ioctl(const char *ifname, uint32_t cmd, uint32_t len) { + struct ifreq ifr; + wl_ioctl_t ioc; + int r; + + if (sockfd < 0) + return -1; + + ioc.cmd = cmd; + ioc.buf = ioctl_buf; + ioc.len = len; + ioc.set = 0; // false + + strcpy(ifr.ifr_name, ifname); + ifr.ifr_data = (char *) &ioc; + + r = ioctl(sockfd, SIOCDEVPRIVATE, &ifr); + return r; +} + +static void *_memdup(void *src, size_t len) { + void *dst = malloc(len); + if (dst == NULL) + return NULL; + + return memcpy(dst, src, len); +} + +static int brcm_wl_config(const char *key, const char *value) { + if (strcasecmp(key, "Interface") == 0) { + int i; + for (i = 0; i < STATIC_ARRAY_SIZE(interfaces); i++) { + if (interfaces[i] == NULL) { + interfaces[i] = sstrdup(value); + return 0; + } + } + ERROR("cant add more interfaces"); + } else if (strcasecmp(key, "SecondsResolution") == 0) { + secondsResolution = IS_TRUE(value) ? true : false; + return 0; + } else if (strcasecmp(key, "NormalizeTimestamps") == 0) { + normalizeTimestamps = IS_TRUE(value) ? true : false; + return 0; + } + + return -1; +} + +static inline void handleTimestamp(cdtime_t time, value_list_t *vl) { + vl->time = normalizeTimestamps ? time : cdtime(); + if (secondsResolution) { + vl->time >>= 30; + vl->time <<= 30; + } +} + +static void brcm_wl_submit_stats(cdtime_t time, const char *plugin_instance, + const char *type_instance, sta_info_t *info) { + value_list_t vl = VALUE_LIST_INIT; + + value_t values[] = { + {.gauge = info->in}, + {.gauge = info->idle}, + + {.gauge = info->rssi[0]}, + {.gauge = info->rssi[1]}, + {.gauge = info->rssi[2]}, + {.gauge = info->rssi[3]}, + }; + + handleTimestamp(time, &vl); + + vl.values = values; + vl.values_len = STATIC_ARRAY_SIZE(values); + + sstrncpy(vl.plugin, PLUGIN_NAME, sizeof(vl.plugin)); + sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance)); + sstrncpy(vl.type, "wl_stats", sizeof(vl.type)); + sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance)); + + plugin_dispatch_values(&vl); +} + +static void brcm_wl_submit_traffic(cdtime_t time, const char *plugin_instance, + const char *type_instance, sta_info_t *info) { + value_list_t vl = VALUE_LIST_INIT; + value_t values[] = { + {.gauge = info->tx_pkts}, + {.gauge = info->tx_ucast_bytes}, + {.gauge = info->tx_mcast_pkts}, + {.gauge = info->tx_mcast_bytes}, + + {.gauge = info->rx_ucast_pkts}, + {.gauge = info->rx_ucast_bytes}, + {.gauge = info->rx_mcast_pkts}, + {.gauge = info->rx_mcast_bytes}, + + {.gauge = info->rx_decrypt_failures}, + }; + + handleTimestamp(time, &vl); + + vl.values = values; + vl.values_len = STATIC_ARRAY_SIZE(values); + + sstrncpy(vl.plugin, PLUGIN_NAME, sizeof(vl.plugin)); + sstrncpy(vl.plugin_instance, plugin_instance, sizeof(vl.plugin_instance)); + sstrncpy(vl.type, "wl_traffic", sizeof(vl.type)); + sstrncpy(vl.type_instance, type_instance, sizeof(vl.type_instance)); + + plugin_dispatch_values(&vl); +} + +static sta_info_t *get_sta_info(const char *ifname, struct ether_addr *sta_addr) { + int r; + + // format request + strcpy(ioctl_buf, "sta_info"); + memcpy(ioctl_buf + strlen("sta_info") + 1, sta_addr->ether_addr_octet, + sizeof(sta_addr->ether_addr_octet)); + + r = wl_ioctl(ifname, WLC_GET_VAR, IOCTL_BUF_SIZE); + if (r < 0) { + ERROR("cant get sta_info: %s", STRERRNO); + return NULL; + } + + sta_info_t *info = (sta_info_t *) ioctl_buf; + + // warn user about differing struct sizes + if (info->len != sizeof(sta_info_t) && !warnedStructSize) { + WARNING("unexpected sta_info_t size. wanted %d got %d", + sizeof(sta_info_t), info->len); + warnedStructSize = true; + } + + if (info->len >= sizeof(sta_info_t)) { + sta_info_t *new_info = _memdup(info, sizeof(sta_info_t)); + if (new_info == NULL) + ERROR("cant dup sta_info_t"); + return new_info; + } + + return NULL; +} + +static maclist_t *get_assoclist(const char *ifname) { + int max_count = (IOCTL_BUF_SIZE - sizeof(int)) / sizeof(struct ether_addr); + + ((maclist_t *) ioctl_buf)->count = max_count; + int r = wl_ioctl(ifname, WLC_GET_ASSOCLIST, IOCTL_BUF_SIZE); + if (r < 0) { + ERROR("cant get associated clients: %s", STRERRNO); + } else { + maclist_t *list = (maclist_t *) ioctl_buf; + list = _memdup(ioctl_buf, list->count * sizeof(struct ether_addr) + sizeof(uint32_t)); + if (list == NULL) { + ERROR("cant dup assoc list"); + return NULL; + } + + return list; + } + + return NULL; +} + +static int process_assoclist(const char *iface) { + int processed = 0; + char mac_str[12+1]; + + maclist_t *assoclist = get_assoclist(iface); + if (assoclist == NULL) { + return -1; + } + + cdtime_t now = cdtime(); + + if (assoclist->count > 0) + INFO("processing %d clients on %s...", assoclist->count, iface); + + for (int i = 0; i < assoclist->count; i++) { + struct ether_addr *addr = &assoclist->addr[i]; + // convert MAC address to string + snprintf(mac_str, 12+1, "%02x%02x%02x%02x%02x%02x", + addr->ether_addr_octet[0], + addr->ether_addr_octet[1], + addr->ether_addr_octet[2], + addr->ether_addr_octet[3], + addr->ether_addr_octet[4], + addr->ether_addr_octet[5]); + + sta_info_t *info = get_sta_info(iface, addr); + if (info == NULL) + continue; + + INFO("submitting stats for %s", mac_str); + brcm_wl_submit_stats(now, iface, mac_str, info); + brcm_wl_submit_traffic(now, iface, mac_str, info); + + free(info); + processed++; + } + + free(assoclist); + + return processed; +} + +static int brcm_wl_read(void) { + if (init_sock() < 0) { + return -1; + } + + int i, processed = 0; + for (i = 0; i < STATIC_ARRAY_SIZE(interfaces); i++) { + char *iface = interfaces[i]; + if (iface == NULL) + break; + + int r = wl_ioctl(iface, WLC_GET_MAGIC, sizeof(uint32_t)); + if (r < 0 || * (uint32_t *) ioctl_buf != WLC_IOCTL_MAGIC) { + ERROR("interface %s is not a brcm_wl dev", iface); + continue; + } + + if (process_assoclist(iface) < 0) { + ERROR("errror processing clients for %s", iface); + continue; + } + + processed++; + } + + close_sock(); + + // could not query any interface, this will throttle us + if (processed == 0) + return -1; + + return 0; +} + +void module_register(void) { + plugin_register_config("brcm_wl", brcm_wl_config, + config_keys, STATIC_ARRAY_SIZE(config_keys)); + plugin_register_read("brcm_wl", brcm_wl_read); +} + diff --git a/files/collectd.conf b/files/collectd.conf new file mode 100644 index 0000000..68184b2 --- /dev/null +++ b/files/collectd.conf @@ -0,0 +1,28 @@ + +#Hostname "my-wireless-router" +BaseDir "/tmp" +ReadThreads 2 +WriteThreads 1 + +Interval 30 +LoadPlugin cpu +LoadPlugin load +LoadPlugin memory + +# network +LoadPlugin interface + + + + +# wireless clients +LoadPlugin brcm_wl + + + + +LoadPlugin network + + #Server "192.168.0.1" + + diff --git a/files/wl_ioctl.h b/files/wl_ioctl.h new file mode 100644 index 0000000..dbb951c --- /dev/null +++ b/files/wl_ioctl.h @@ -0,0 +1,94 @@ +// +// Broadcom ioctl defines +// https://github.com/RMerl/asuswrt-merlin.ng/blob/master/release/src-rt-6.x.4708/include/wlioctl.h +// + +#ifndef __WLIOCTL_H__ +#define __WLIOCTL_H__ + +#include + +#define WLC_IOCTL_MAGIC 0x14e46c77 + +// commands +#define WLC_GET_MAGIC 0 +#define WLC_GET_VERSION 1 + +#define WLC_GET_ASSOCLIST 159 +#define WLC_GET_VAR 262 + +// ioctl packet for driver communication +typedef struct wl_ioctl { + uint32_t cmd; + void *buf; + uint32_t len; + uint8_t set; + uint32_t used; + uint32_t needed; +} wl_ioctl_t; + + +// a list of MAC addresses +typedef struct maclist { + uint32_t count; + struct ether_addr addr[]; +} maclist_t; + + +#define WL_MAXRATES_IN_SET 16 /* max # of rates in a rateset */ +typedef struct wl_rateset { + uint32_t count; /* # rates in this set */ + uint8_t rates[WL_MAXRATES_IN_SET]; /* rates in 500kbps units w/hi bit set if basic */ +} wl_rateset_t; + + +#define WL_STA_ANT_MAX 4 /* max possible rx antennas */ + +typedef struct { + uint16_t ver; /* version of this struct */ + uint16_t len; /* length in bytes of this structure */ + uint16_t cap; /* sta's advertised capabilities */ + uint32_t flags; /* flags defined below */ + uint32_t idle; /* time since data pkt rx'd from sta */ + struct ether_addr ea; /* Station address */ + wl_rateset_t rateset; /* rateset in use */ + uint32_t in; /* seconds elapsed since associated */ + uint32_t listen_interval_inms; /* Min Listen interval in ms for this STA */ + uint32_t tx_pkts; /* # of packets transmitted */ + uint32_t tx_failures; /* # of packets failed */ + uint32_t rx_ucast_pkts; /* # of unicast packets received */ + uint32_t rx_mcast_pkts; /* # of multicast packets received */ + uint32_t tx_rate; /* Rate of last successful tx frame */ + uint32_t rx_rate; /* Rate of last successful rx frame */ + uint32_t rx_decrypt_succeeds; /* # of packet decrypted successfully */ + uint32_t rx_decrypt_failures; /* # of packet decrypted unsuccessfully */ + uint32_t tx_tot_pkts; /* # of tx pkts (ucast + mcast) */ + uint32_t rx_tot_pkts; /* # of data packets recvd (uni + mcast) */ + uint32_t tx_mcast_pkts; /* # of mcast pkts txed */ + uint64_t tx_tot_bytes; /* data bytes txed (ucast + mcast) */ + uint64_t rx_tot_bytes; /* data bytes recvd (ucast + mcast) */ + uint64_t tx_ucast_bytes; /* data bytes txed (ucast) */ + uint64_t tx_mcast_bytes; /* # data bytes txed (mcast) */ + uint64_t rx_ucast_bytes; /* data bytes recvd (ucast) */ + uint64_t rx_mcast_bytes; /* data bytes recvd (mcast) */ + int8_t rssi[WL_STA_ANT_MAX]; /* average rssi per antenna of data + * frames + */ + int8_t nf[WL_STA_ANT_MAX]; /* per antenna noise floor */ + uint16_t aid; /* association ID */ + uint16_t ht_capabilities; /* advertised ht caps */ + uint16_t vht_flags; /* converted vht flags */ + uint32_t tx_pkts_retried; /* # of frames where a retry was + * necessary + */ + uint32_t tx_pkts_retry_exhausted; /* # of frames where a retry was + * exhausted + */ + int8_t rx_lastpkt_rssi[WL_STA_ANT_MAX]; /* Per antenna RSSI of last + * received data frame + */ +} sta_info_t; + + +#endif/*!__WLIOCTL_H__*/ +