From cecd716e0686c24c02863255f7cf8915b3e0f922 Mon Sep 17 00:00:00 2001 From: Kamil Bienkiewicz Date: Thu, 15 Oct 2026 15:04:27 +0200 Subject: [PATCH] snmpd-openwrt-metrics: add new package Stock OpenWrt reports nothing useful about its wireless side or its sensors over SNMP. The ieee802dot11 MIB module is compiled into net-snmp but produces no data on mac80211 drivers, and net-snmp's lmSensors module cannot be enabled because it still targets the libsensors 2 API that lm-sensors 3 removed. Add a small AgentX subagent serving three subtrees: .1.3.6.1.4.1.66510.1.1 OPENWRT-MIB device metadata: release version, build revision, target and model, so a manager can identify the device without extend scripts .1.3.6.1.4.1.66510.1.10 OPENWRT-WIRELESS-MIB, under OpenWrt's own IANA enterprise number, indexed by ifIndex so rows join IF-MIB: clients, frequency, noise floor, tx/rx rate range, SNR range, channel utilisation, tx power, and (for 802.11be Multi-Link Operation interfaces, which have no single value for those columns) a separate per-radio-link table .1.3.6.1.4.1.2021.13.16 LM-SENSORS-MIB temperature and fan tables Wireless data comes from libiwinfo, MLO per-link data from a direct nl80211 query (libiwinfo has no MLO awareness), and sensor data from hwmon and thermal zones, taking the union of the two so a sensor with no thermal zone (an MDIO PHY die sensor, for example) is still reported. snmpd enables the AgentX master by default and listens on a known socket, so the package needs no snmpd configuration at all: no pass registration, no uci-defaults, no removal hook. If the socket was moved with the agentxsocket option of the snmpd agentx section, the init script reads it from there and passes it to the agent. Radios, VAPs and sensors are discovered at request time, so there is nothing per-interface to maintain either. The SSID of each interface is read from hostapd over ubus, with libiwinfo as the fallback, because libiwinfo returns no SSID for some VAPs on a tri-band phy. --dump also lists the temperature and fan sensors the agent finds, which is what to check when a device reports no LM-SENSORS data. Tested on a GL-BE9300 (qualcommbe/ipq53xx), including its 802.11be multi-link (5GHz+6GHz) AP, and cross-checked against `iwinfo`/`iw`/sysfs output and a live AgentX registration against snmpd with `snmpwalk`. OPENWRT-MIB also defines openwrtDevice (.1.3.6.1.4.1.66510.3.1) as a sysObjectID value for snmpd to report; setting it is left to snmpd's own config. Init ordering: the AgentX master lives in snmpd, which starts at START=99, the top of the two-digit range (rc.common's glob-based enable/disable needs two digits, see rc.common), so nothing can be ordered strictly after it. The agent therefore starts at START=95 and relies on net-snmp's AgentX subagent retrying the master connection on its own schedule (agentx_reopen_session in agentx/subagent.c). The init script only checks that snmpd is enabled and logs when it is not. If the startup order of the base system ever changes, this is the assumption to re-check. Signed-off-by: Kamil Bienkiewicz --- net/snmpd-openwrt-metrics/Makefile | 67 ++ net/snmpd-openwrt-metrics/files/OPENWRT-MIB | 154 ++++ .../files/OPENWRT-WIRELESS-MIB | 513 ++++++++++++ .../files/openwrt-snmp-agent.init | 36 + net/snmpd-openwrt-metrics/src/LICENSE | 338 ++++++++ net/snmpd-openwrt-metrics/src/Makefile | 18 + net/snmpd-openwrt-metrics/src/agent.c | 504 ++++++++++++ net/snmpd-openwrt-metrics/src/collect.c | 774 ++++++++++++++++++ net/snmpd-openwrt-metrics/src/collect.h | 106 +++ net/snmpd-openwrt-metrics/src/mlo.c | 201 +++++ net/snmpd-openwrt-metrics/src/mlo.h | 29 + 11 files changed, 2740 insertions(+) create mode 100644 net/snmpd-openwrt-metrics/Makefile create mode 100644 net/snmpd-openwrt-metrics/files/OPENWRT-MIB create mode 100644 net/snmpd-openwrt-metrics/files/OPENWRT-WIRELESS-MIB create mode 100755 net/snmpd-openwrt-metrics/files/openwrt-snmp-agent.init create mode 100644 net/snmpd-openwrt-metrics/src/LICENSE create mode 100644 net/snmpd-openwrt-metrics/src/Makefile create mode 100644 net/snmpd-openwrt-metrics/src/agent.c create mode 100644 net/snmpd-openwrt-metrics/src/collect.c create mode 100644 net/snmpd-openwrt-metrics/src/collect.h create mode 100644 net/snmpd-openwrt-metrics/src/mlo.c create mode 100644 net/snmpd-openwrt-metrics/src/mlo.h diff --git a/net/snmpd-openwrt-metrics/Makefile b/net/snmpd-openwrt-metrics/Makefile new file mode 100644 index 0000000000000..1299c6ab4fc32 --- /dev/null +++ b/net/snmpd-openwrt-metrics/Makefile @@ -0,0 +1,67 @@ +# +# Copyright (C) 2026 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=snmpd-openwrt-metrics +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=Kamil Bienkiewicz +PKG_LICENSE:=GPL-2.0-or-later +PKG_LICENSE_FILES:=LICENSE + +include $(INCLUDE_DIR)/package.mk + +define Package/snmpd-openwrt-metrics + SECTION:=net + CATEGORY:=Network + TITLE:=OpenWrt wireless and sensor metrics over SNMP + URL:=https://openwrt.org + DEPENDS:=+snmpd +libnetsnmp +libiwinfo +libnl-tiny +libubus +libubox +endef + +define Package/snmpd-openwrt-metrics/description + An AgentX subagent that exports per-interface wireless metrics under + OpenWrt's IANA enterprise number 66510 (OPENWRT-WIRELESS-MIB: clients, + frequency, noise floor, tx/rx rates, SNR, channel utilisation, tx power, + indexed by ifIndex so the table joins IF-MIB), the device's OpenWrt + release, build revision, target and model (OPENWRT-MIB), and hwmon + temperature and fan readings as LM-SENSORS-MIB, which net-snmp cannot + serve on OpenWrt because its lmSensors module predates the libsensors 3 + API. + + Wireless data comes from libiwinfo (SSIDs are asked of hostapd over + ubus first), so nothing needs configuring per radio or per VAP; + a multi-link (MLO) interface's per-link channel/tx power is queried + directly over nl80211 instead, since libiwinfo has no MLO + awareness. snmpd enables the AgentX master by default, so no snmpd + configuration is required either. +endef + +TARGET_CFLAGS += \ + -I$(STAGING_DIR)/usr/include/libnl-tiny \ + -D_GNU_SOURCE + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./src/* $(PKG_BUILD_DIR)/ +endef + +define Package/snmpd-openwrt-metrics/install + $(INSTALL_DIR) $(1)/usr/sbin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/openwrt-snmp-agent $(1)/usr/sbin/ + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/openwrt-snmp-agent.init $(1)/etc/init.d/openwrt-snmp-agent + + $(INSTALL_DIR) $(1)/usr/share/snmp/mibs + $(INSTALL_DATA) ./files/OPENWRT-MIB $(1)/usr/share/snmp/mibs/OPENWRT-MIB + $(INSTALL_DATA) ./files/OPENWRT-WIRELESS-MIB $(1)/usr/share/snmp/mibs/OPENWRT-WIRELESS-MIB +endef + +$(eval $(call BuildPackage,snmpd-openwrt-metrics)) diff --git a/net/snmpd-openwrt-metrics/files/OPENWRT-MIB b/net/snmpd-openwrt-metrics/files/OPENWRT-MIB new file mode 100644 index 0000000000000..882a014a857e7 --- /dev/null +++ b/net/snmpd-openwrt-metrics/files/OPENWRT-MIB @@ -0,0 +1,154 @@ +-- ********************************************************** +-- OPENWRT-MIB +-- +-- Base registration module for OpenWrt metrics exported by the +-- OpenWrt SNMP metrics agent (a net-snmp AgentX subagent). +-- +-- This module establishes the OpenWrt OID structure, defines the device +-- metadata scalars under openwrtSystem, and the openwrtProducts values an +-- agent may report as sysObjectID. The metrics themselves live in +-- per-subsystem modules that register beneath openwrtObjects, for example +-- OPENWRT-WIRELESS-MIB at { openwrtObjects 10 }. +-- +-- Enterprise Number: 66510 (OpenWrt, IANA PEN) +-- OID Base: .1.3.6.1.4.1.66510 +-- +-- openwrtMIB is rooted directly under OpenWrt's own IANA-assigned enterprise +-- number, with no intermediate node. +-- +-- Temperature/thermal data is exported separately via LM-SENSORS-MIB. +-- ********************************************************** + +OPENWRT-MIB DEFINITIONS ::= BEGIN + +IMPORTS + MODULE-IDENTITY, OBJECT-TYPE, enterprises + FROM SNMPv2-SMI + DisplayString + FROM SNMPv2-TC + MODULE-COMPLIANCE, OBJECT-GROUP + FROM SNMPv2-CONF; + +openwrtMIB MODULE-IDENTITY + LAST-UPDATED "202609220000Z" + ORGANIZATION "OpenWrt" + CONTACT-INFO + " OpenWrt Project + https://openwrt.org + https://github.com/openwrt/openwrt + " + DESCRIPTION + "Base module describing the OpenWrt OID structure used by the + OpenWrt SNMP metrics agent, the device metadata scalars under + openwrtSystem, and the sysObjectID values under openwrtProducts. + Per-subsystem metrics register under openwrtObjects (wireless at + { openwrtObjects 10 }); conformance registers under + openwrtConformance." + REVISION "202609220000Z" + DESCRIPTION + "Added openwrtSystem device metadata scalars (release version, + build revision, target and device model) and openwrtProducts, so + a manager can identify an OpenWrt device and its release without + extend scripts." + REVISION "202608130000Z" + DESCRIPTION + "Initial version: base registration nodes for the OpenWrt subtree." + ::= { enterprises 66510 } + +-- ********************************************************** +-- Structure +-- ********************************************************** + +openwrtObjects OBJECT IDENTIFIER ::= { openwrtMIB 1 } +openwrtConformance OBJECT IDENTIFIER ::= { openwrtMIB 2 } +openwrtProducts OBJECT IDENTIFIER ::= { openwrtMIB 3 } + +-- ********************************************************** +-- Products (sysObjectID values) +-- ********************************************************** + +-- sysObjectID for a device running OpenWrt, whatever the hardware; the +-- device model is in openwrtSysModel. snmpd reports this only when +-- configured to (net-snmp's own default is its generic Linux value). +openwrtDevice OBJECT IDENTIFIER ::= { openwrtProducts 1 } + +-- ********************************************************** +-- Device metadata +-- ********************************************************** + +openwrtSystem OBJECT IDENTIFIER ::= { openwrtObjects 1 } + +openwrtSysVersion OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "OpenWrt release version, for example 24.10.2, or SNAPSHOT for a + development build: the VERSION field of /etc/os-release. + Not instantiated if the device does not provide it." + ::= { openwrtSystem 1 } + +openwrtSysRevision OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Source revision the running firmware was built from, for example + r28427-6df0e3d02a: the BUILD_ID field of /etc/os-release. This is + what tells one SNAPSHOT build from another. Not instantiated if the + device does not provide it." + ::= { openwrtSystem 2 } + +openwrtSysTarget OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "OpenWrt target and subtarget the firmware was built for, for + example mediatek/filogic: the OPENWRT_BOARD field of + /etc/os-release. Not instantiated if the device does not provide + it." + ::= { openwrtSystem 3 } + +openwrtSysModel OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Human-readable device model, for example Linksys WRT1200AC, as + OpenWrt determined it at boot (/tmp/sysinfo/model). Not + instantiated if the device does not provide it." + ::= { openwrtSystem 4 } + +-- ********************************************************** +-- Conformance +-- ********************************************************** + +openwrtCompliances OBJECT IDENTIFIER ::= { openwrtConformance 1 } +openwrtGroups OBJECT IDENTIFIER ::= { openwrtConformance 2 } + +openwrtSystemGroup OBJECT-GROUP + OBJECTS { + openwrtSysVersion, + openwrtSysRevision, + openwrtSysTarget, + openwrtSysModel + } + STATUS current + DESCRIPTION + "OpenWrt device metadata." + ::= { openwrtGroups 1 } + +openwrtCompliance MODULE-COMPLIANCE + STATUS current + DESCRIPTION + "Compliance statement for OpenWrt SNMP agents." + MODULE -- this module + GROUP openwrtSystemGroup + DESCRIPTION + "Optional: each object in this group is instantiated only when the + device provides the underlying value, so an agent may implement + any subset of it." + ::= { openwrtCompliances 1 } + +END diff --git a/net/snmpd-openwrt-metrics/files/OPENWRT-WIRELESS-MIB b/net/snmpd-openwrt-metrics/files/OPENWRT-WIRELESS-MIB new file mode 100644 index 0000000000000..ec930dcee37a3 --- /dev/null +++ b/net/snmpd-openwrt-metrics/files/OPENWRT-WIRELESS-MIB @@ -0,0 +1,513 @@ +-- ********************************************************** +-- OPENWRT-WIRELESS-MIB +-- +-- OpenWrt wireless metrics exported by the OpenWrt SNMP metrics agent. +-- The agent discovers radios/VAPs live (hostapd/iwinfo) at request time and +-- serves this subtree as a net-snmp AgentX subagent, so no per-interface or +-- per-metric snmpd configuration is required. +-- +-- Registers under OPENWRT-MIB at { openwrtObjects 10 } +-- -> OID base .1.3.6.1.4.1.66510.1.10 +-- +-- Rows in openwrtWirelessInterfaceTable are indexed by IF-MIB ifIndex so the +-- table joins directly onto the standard ifTable/ifXTable for the same VAP. +-- ********************************************************** + +OPENWRT-WIRELESS-MIB DEFINITIONS ::= BEGIN + +IMPORTS + MODULE-IDENTITY, OBJECT-TYPE, + Integer32, Unsigned32 + FROM SNMPv2-SMI + DisplayString, PhysAddress, TEXTUAL-CONVENTION + FROM SNMPv2-TC + MODULE-COMPLIANCE, OBJECT-GROUP + FROM SNMPv2-CONF + InterfaceIndex + FROM IF-MIB + openwrtObjects, openwrtConformance + FROM OPENWRT-MIB; + +openwrtWirelessMIB MODULE-IDENTITY + LAST-UPDATED "202610170000Z" + ORGANIZATION "OpenWrt" + CONTACT-INFO + " OpenWrt Project + https://openwrt.org + https://github.com/openwrt/openwrt + " + DESCRIPTION + "MIB module describing OpenWrt wireless metrics (aggregate scalars, + a per-interface table, and a per-radio-link table for multi-link + interfaces) exported by the OpenWrt SNMP metrics agent's + AgentX subagent." + REVISION "202610170000Z" + DESCRIPTION + "Bitrates and channel utilisation are now reported in tenths of + their unit (OpenwrtTenths), so Wi-Fi rates such as 72.2 Mbit/s + and utilisation such as 89.6 percent keep their fractional part + instead of being truncated to a whole number." + REVISION "202609150000Z" + DESCRIPTION + "Added openwrtWirelessLinkTable: per-radio-link channel, transmit + power and channel utilisation for 802.11be Multi-Link Operation + (MLO/MLD) interfaces, queried directly over nl80211 rather than + through the same path as the single-radio columns below, which + cannot represent more than one radio per row." + REVISION "202609120000Z" + DESCRIPTION + "Initial version: wireless aggregate scalars and per-interface table, + including per-interface channel utilisation and transmit power. + Radio-scoped objects are defined as absent on interfaces that span + several radio links, so 802.11be multi-link devices are representable + without reporting a misleading single value." + ::= { openwrtObjects 10 } + +-- ********************************************************** +-- Textual conventions +-- ********************************************************** + +OpenwrtTenths ::= TEXTUAL-CONVENTION + DISPLAY-HINT "d-1" + STATUS current + DESCRIPTION + "A non-negative quantity transmitted as an integer count of tenths + of the unit given by the object's UNITS clause, so 722 for an object + measured in Mbit/s is 72.2 Mbit/s. A manager should divide by 10." + SYNTAX Unsigned32 + +-- ********************************************************** +-- Aggregate scalars +-- ********************************************************** + +openwrtWirelessInterfaceCount OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of active wireless interfaces exported in + openwrtWirelessInterfaceTable." + ::= { openwrtWirelessMIB 1 } + +openwrtWirelessClientCount OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Aggregate count of unique wireless clients served by this device + across its AP/master interfaces. MLO/MLD-related addresses are + de-duplicated so a single client associated over multiple links + counts once. Upstream associations on client/STA-mode interfaces are + excluded (this device's own uplink is not one of its clients)." + ::= { openwrtWirelessMIB 2 } + +-- ********************************************************** +-- Wireless interface table +-- ********************************************************** + +openwrtWirelessInterfaceTable OBJECT-TYPE + SYNTAX SEQUENCE OF OpenwrtWirelessInterfaceEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "OpenWrt wireless interfaces and their per-interface metrics." + ::= { openwrtWirelessMIB 3 } + +openwrtWirelessInterfaceEntry OBJECT-TYPE + SYNTAX OpenwrtWirelessInterfaceEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "Metrics for one OpenWrt wireless interface. Rows are indexed by the + corresponding IF-MIB ifIndex, so that this table joins directly onto + the standard interface table for the same VAP." + INDEX { openwrtWlIfaceIfIndex } + ::= { openwrtWirelessInterfaceTable 1 } + +OpenwrtWirelessInterfaceEntry ::= SEQUENCE { + openwrtWlIfaceIfIndex InterfaceIndex, + openwrtWlIfaceName DisplayString, + openwrtWlIfaceLabel DisplayString, + openwrtWlIfaceClients Unsigned32, + openwrtWlIfaceFrequency Unsigned32, + openwrtWlIfaceNoiseFloor Integer32, + openwrtWlIfaceTxRateMin Unsigned32, + openwrtWlIfaceTxRateAvg Unsigned32, + openwrtWlIfaceTxRateMax Unsigned32, + openwrtWlIfaceRxRateMin Unsigned32, + openwrtWlIfaceRxRateAvg Unsigned32, + openwrtWlIfaceRxRateMax Unsigned32, + openwrtWlIfaceSnrMin Integer32, + openwrtWlIfaceSnrAvg Integer32, + openwrtWlIfaceSnrMax Integer32, + openwrtWlIfaceChannelUtil Unsigned32, + openwrtWlIfaceTxPower Integer32 +} + +openwrtWlIfaceIfIndex OBJECT-TYPE + SYNTAX InterfaceIndex + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "The kernel interface index (IF-MIB ifIndex) of this wireless + interface, taken from /sys/class/net//ifindex. Used as the + table index so rows align with the standard ifTable/ifXTable." + ::= { openwrtWirelessInterfaceEntry 1 } + +openwrtWlIfaceName OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "OpenWrt wireless interface name, for example wlan0 or phy0-ap0." + ::= { openwrtWirelessInterfaceEntry 2 } + +openwrtWlIfaceLabel OBJECT-TYPE + SYNTAX DisplayString + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Display label for the wireless interface: its SSID, or the + interface name itself if it has none (a monitor interface, or a + VAP that is currently down). Not necessarily unique - two VAPs + broadcasting the same SSID on different bands get identical + labels here; use openwrtWlIfaceIfIndex to tell them apart." + ::= { openwrtWirelessInterfaceEntry 3 } + +openwrtWlIfaceClients OBJECT-TYPE + SYNTAX Unsigned32 + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Number of stations associated with this wireless interface. + For an AP/master interface this is the count of connected clients. + For a client/STA-mode interface (e.g. a wireless uplink or backhaul) + this is the number of upstream associations, which is its peer count: + 1 while associated to an AP, 0 when there is no peer (not associated / + link down) - so it doubles as a backhaul link-up indicator." + ::= { openwrtWirelessInterfaceEntry 4 } + +openwrtWlIfaceFrequency OBJECT-TYPE + SYNTAX Unsigned32 + UNITS "MHz" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Current channel centre frequency in MHz. + + Absent for an interface that spans several radio links (an + 802.11be multi-link device), which has no single value for + this object; agents report noSuchInstance in that case. See + openwrtWirelessLinkTable for that interface's per-link frequency + instead." + ::= { openwrtWirelessInterfaceEntry 5 } + +openwrtWlIfaceNoiseFloor OBJECT-TYPE + SYNTAX Integer32 + UNITS "dBm" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Noise floor for this wireless interface in dBm (typically + negative). + + Absent if the driver reports no noise measurement, or for an + interface that spans several radio links (an 802.11be multi-link + device), which has no single value for this object; agents report + noSuchInstance in those cases. Unlike frequency, transmit power + and channel utilisation, this has no per-link equivalent in + openwrtWirelessLinkTable: nl80211 has no per-link noise attribute, + only a device-wide one." + ::= { openwrtWirelessInterfaceEntry 6 } + +openwrtWlIfaceTxRateMin OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "Mbit/s" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Minimum transmit bitrate across associated stations, in tenths of + Mbit/s (722 is 72.2 Mbit/s). Expressed in Mbit/s rather than + bit/s so that Wi-Fi 6E/7 rates (multi-Gbit/s) do not overflow + Unsigned32." + ::= { openwrtWirelessInterfaceEntry 7 } + +openwrtWlIfaceTxRateAvg OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "Mbit/s" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average transmit bitrate across associated stations, in tenths of + Mbit/s (722 is 72.2 Mbit/s)." + ::= { openwrtWirelessInterfaceEntry 8 } + +openwrtWlIfaceTxRateMax OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "Mbit/s" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Maximum transmit bitrate across associated stations, in tenths of + Mbit/s (722 is 72.2 Mbit/s)." + ::= { openwrtWirelessInterfaceEntry 9 } + +openwrtWlIfaceRxRateMin OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "Mbit/s" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Minimum receive bitrate across associated stations, in tenths of + Mbit/s (722 is 72.2 Mbit/s)." + ::= { openwrtWirelessInterfaceEntry 10 } + +openwrtWlIfaceRxRateAvg OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "Mbit/s" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average receive bitrate across associated stations, in tenths of + Mbit/s (722 is 72.2 Mbit/s)." + ::= { openwrtWirelessInterfaceEntry 11 } + +openwrtWlIfaceRxRateMax OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "Mbit/s" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Maximum receive bitrate across associated stations, in tenths of + Mbit/s (722 is 72.2 Mbit/s)." + ::= { openwrtWirelessInterfaceEntry 12 } + +openwrtWlIfaceSnrMin OBJECT-TYPE + SYNTAX Integer32 + UNITS "dB" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Minimum signal-to-noise ratio across associated stations, in dB." + ::= { openwrtWirelessInterfaceEntry 13 } + +openwrtWlIfaceSnrAvg OBJECT-TYPE + SYNTAX Integer32 + UNITS "dB" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Average signal-to-noise ratio across associated stations, in dB." + ::= { openwrtWirelessInterfaceEntry 14 } + +openwrtWlIfaceSnrMax OBJECT-TYPE + SYNTAX Integer32 + UNITS "dB" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Maximum signal-to-noise ratio across associated stations, in dB." + ::= { openwrtWirelessInterfaceEntry 15 } + +openwrtWlIfaceChannelUtil OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "percent" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Channel airtime utilisation for this interface's radio, in tenths + of a percent (0-1000; 896 is 89.6 percent). This is the fraction of time the channel is + observed busy. Absent if the driver reports no survey data for + the operating channel, or for a multi-link interface, which has no + single radio; agents report noSuchInstance in those cases. See + openwrtWirelessLinkTable for a multi-link interface's per-link + channel utilisation instead." + ::= { openwrtWirelessInterfaceEntry 16 } + +openwrtWlIfaceTxPower OBJECT-TYPE + SYNTAX Integer32 + UNITS "dBm" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Current transmit power for this wireless interface in dBm. + + Absent if the driver reports no transmit power, or for an + interface that spans several radio links (an 802.11be multi-link + device), which has no single value for this object; agents report + noSuchInstance in those cases. See openwrtWirelessLinkTable for + that interface's per-link transmit power instead." + ::= { openwrtWirelessInterfaceEntry 17 } + +-- ********************************************************** +-- Wireless link table (802.11be Multi-Link Operation) +-- ********************************************************** + +openwrtWirelessLinkTable OBJECT-TYPE + SYNTAX SEQUENCE OF OpenwrtWirelessLinkEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "Per-radio-link metrics for wireless interfaces using 802.11be + Multi-Link Operation (MLO/MLD), where a single netdev spans + several radio links and so has no single channel, transmit power + or channel utilisation of its own - see openwrtWlIfaceFrequency + and the related objects in openwrtWirelessInterfaceTable, which + are defined as absent for such an interface. Each row here is one + link of one such interface; several rows share one ifIndex. This + table has no rows at all on a device with no multi-link + interface, rather than being present but empty." + ::= { openwrtWirelessMIB 4 } + +openwrtWirelessLinkEntry OBJECT-TYPE + SYNTAX OpenwrtWirelessLinkEntry + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "Metrics for one radio link of one multi-link wireless interface." + INDEX { openwrtWlLinkIfIndex, openwrtWlLinkId } + ::= { openwrtWirelessLinkTable 1 } + +OpenwrtWirelessLinkEntry ::= SEQUENCE { + openwrtWlLinkIfIndex InterfaceIndex, + openwrtWlLinkId Integer32, + openwrtWlLinkAddress PhysAddress, + openwrtWlLinkFrequency Unsigned32, + openwrtWlLinkTxPower Integer32, + openwrtWlLinkChannelUtil Unsigned32 +} + +openwrtWlLinkIfIndex OBJECT-TYPE + SYNTAX InterfaceIndex + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "The ifIndex of the parent multi-link wireless interface - the same + value as openwrtWlIfaceIfIndex for that interface's row in + openwrtWirelessInterfaceTable." + ::= { openwrtWirelessLinkEntry 1 } + +openwrtWlLinkId OBJECT-TYPE + SYNTAX Integer32 (0..15) + MAX-ACCESS not-accessible + STATUS current + DESCRIPTION + "The link ID nl80211 assigns this radio link within its MLD + (NL80211_ATTR_MLO_LINK_ID), stable for the life of the link." + ::= { openwrtWirelessLinkEntry 2 } + +openwrtWlLinkAddress OBJECT-TYPE + SYNTAX PhysAddress + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "The MAC address this specific link transmits from. Distinct from + the MLD address associations use, and from the other links' + addresses on the same interface." + ::= { openwrtWirelessLinkEntry 3 } + +openwrtWlLinkFrequency OBJECT-TYPE + SYNTAX Unsigned32 + UNITS "MHz" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Current channel centre frequency in MHz for this link." + ::= { openwrtWirelessLinkEntry 4 } + +openwrtWlLinkTxPower OBJECT-TYPE + SYNTAX Integer32 + UNITS "dBm" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Current transmit power for this link in dBm. Absent if the + kernel did not report a transmit power for this link; agents + report noSuchInstance in that case." + ::= { openwrtWirelessLinkEntry 5 } + +openwrtWlLinkChannelUtil OBJECT-TYPE + SYNTAX OpenwrtTenths + UNITS "percent" + MAX-ACCESS read-only + STATUS current + DESCRIPTION + "Channel airtime utilisation for this link's radio, in tenths of a + percent (0-1000), from the same survey data as + openwrtWlIfaceChannelUtil. Absent if the driver reports no survey + data for this link's operating channel; agents report + noSuchInstance in that case." + ::= { openwrtWirelessLinkEntry 6 } + +-- ********************************************************** +-- Conformance +-- ********************************************************** + +openwrtWirelessConformance OBJECT IDENTIFIER ::= { openwrtConformance 10 } +openwrtWirelessCompliances OBJECT IDENTIFIER ::= { openwrtWirelessConformance 1 } +openwrtWirelessGroups OBJECT IDENTIFIER ::= { openwrtWirelessConformance 2 } + +openwrtWirelessScalarGroup OBJECT-GROUP + OBJECTS { + openwrtWirelessInterfaceCount, + openwrtWirelessClientCount + } + STATUS current + DESCRIPTION + "Aggregate wireless scalars." + ::= { openwrtWirelessGroups 1 } + +openwrtWirelessInterfaceGroup OBJECT-GROUP + OBJECTS { + openwrtWlIfaceName, + openwrtWlIfaceLabel, + openwrtWlIfaceClients, + openwrtWlIfaceFrequency, + openwrtWlIfaceNoiseFloor, + openwrtWlIfaceTxRateMin, + openwrtWlIfaceTxRateAvg, + openwrtWlIfaceTxRateMax, + openwrtWlIfaceRxRateMin, + openwrtWlIfaceRxRateAvg, + openwrtWlIfaceRxRateMax, + openwrtWlIfaceSnrMin, + openwrtWlIfaceSnrAvg, + openwrtWlIfaceSnrMax, + openwrtWlIfaceChannelUtil, + openwrtWlIfaceTxPower + } + STATUS current + DESCRIPTION + "Per-interface wireless metrics." + ::= { openwrtWirelessGroups 2 } + +openwrtWirelessLinkGroup OBJECT-GROUP + OBJECTS { + openwrtWlLinkAddress, + openwrtWlLinkFrequency, + openwrtWlLinkTxPower, + openwrtWlLinkChannelUtil + } + STATUS current + DESCRIPTION + "Per-radio-link metrics for 802.11be Multi-Link Operation + interfaces. Optional: a device with no multi-link interface has + no rows to report and so cannot implement this group." + ::= { openwrtWirelessGroups 3 } + +openwrtWirelessCompliance MODULE-COMPLIANCE + STATUS current + DESCRIPTION + "Compliance statement for OpenWrt wireless monitoring agents." + MODULE -- this module + MANDATORY-GROUPS { + openwrtWirelessScalarGroup, + openwrtWirelessInterfaceGroup + } + GROUP openwrtWirelessLinkGroup + DESCRIPTION + "Implemented only by agents that have at least one multi-link + (802.11be MLO/MLD) wireless interface to report." + ::= { openwrtWirelessCompliances 1 } + +END diff --git a/net/snmpd-openwrt-metrics/files/openwrt-snmp-agent.init b/net/snmpd-openwrt-metrics/files/openwrt-snmp-agent.init new file mode 100755 index 0000000000000..253be67c9fc94 --- /dev/null +++ b/net/snmpd-openwrt-metrics/files/openwrt-snmp-agent.init @@ -0,0 +1,36 @@ +#!/bin/sh /etc/rc.common +# SPDX-License-Identifier: GPL-2.0-or-later + +START=95 +USE_PROCD=1 + +PROG=/usr/sbin/openwrt-snmp-agent + +AGENTX_SOCKET=/var/run/agentx.sock + +agentx_socket() { + local sock + + config_get sock "$1" agentxsocket + [ -n "$sock" ] && AGENTX_SOCKET="$sock" +} + +start_service() { + if ! /etc/init.d/snmpd enabled; then + logger -t openwrt-snmp-agent -p daemon.notice "snmpd is not enabled, not starting" + return 0 + fi + + config_load snmpd + config_foreach agentx_socket agentx + + procd_open_instance + procd_set_param command "$PROG" -f -x "$AGENTX_SOCKET" + procd_set_param respawn + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "snmpd" +} diff --git a/net/snmpd-openwrt-metrics/src/LICENSE b/net/snmpd-openwrt-metrics/src/LICENSE new file mode 100644 index 0000000000000..9efa6fbc96283 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/LICENSE @@ -0,0 +1,338 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Moe Ghoul, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/net/snmpd-openwrt-metrics/src/Makefile b/net/snmpd-openwrt-metrics/src/Makefile new file mode 100644 index 0000000000000..c9628cb554036 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/Makefile @@ -0,0 +1,18 @@ +PROG = openwrt-snmp-agent +OBJS = agent.o collect.o mlo.o +LIBS = -lnetsnmpagent -lnetsnmp -liwinfo -lnl-tiny -lubus -lubox + +CFLAGS += -Wall -Wextra + +all: $(PROG) + +$(PROG): $(OBJS) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(OBJS) $(LIBS) + +%.o: %.c collect.h mlo.h + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f $(PROG) $(OBJS) + +.PHONY: all clean diff --git a/net/snmpd-openwrt-metrics/src/agent.c b/net/snmpd-openwrt-metrics/src/agent.c new file mode 100644 index 0000000000000..6b1b4c94155d6 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/agent.c @@ -0,0 +1,504 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * agent.c - AgentX subagent exporting OpenWrt metrics over SNMP. + * + * Three subtrees are served: + * .1.3.6.1.4.1.66510.1.1 OPENWRT-MIB device metadata (OpenWrt IANA PEN 66510) + * .1.3.6.1.4.1.66510.1.10 OPENWRT-WIRELESS-MIB + * .1.3.6.1.4.1.2021.13.16 LM-SENSORS-MIB temperature and fan tables + * + * OpenWrt's snmpd enables the AgentX master by default and listens on + * /var/run/agentx.sock, so no snmpd configuration is required. Any + * pass_persist line previously registering either subtree must be removed, + * or snmpd has two claimants for the same OIDs. + */ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "collect.h" + +#define AGENT_VERSION "1.0.0" + +static volatile sig_atomic_t running = 1; + +static oid sys_scalar_oid[] = { 1,3,6,1,4,1,66510,1,1,0 }; /* last arc set per field */ +static oid wl_scalar_ifcount[] = { 1,3,6,1,4,1,66510,1,10,1 }; +static oid wl_scalar_clients[] = { 1,3,6,1,4,1,66510,1,10,2 }; +static oid wl_table_oid[] = { 1,3,6,1,4,1,66510,1,10,3 }; +static oid wl_link_table_oid[] = { 1,3,6,1,4,1,66510,1,10,4 }; +static oid lm_temp_table_oid[] = { 1,3,6,1,4,1,2021,13,16,2 }; +static oid lm_fan_table_oid[] = { 1,3,6,1,4,1,2021,13,16,3 }; + +static void on_signal(int sig) { (void)sig; running = 0; } + +/* ------------------------------------------------------------------ */ +/* Scalars */ +/* ------------------------------------------------------------------ */ + +static int scalar_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + const struct snapshot *s = snapshot_get(); + uint32_t v; + + (void)handler; + if (reqinfo->mode != MODE_GET) + return SNMP_ERR_NOERROR; + + v = (reginfo->rootoid[9] == 1) ? (uint32_t)s->wl_count : s->client_total; + snmp_set_var_typed_value(requests->requestvb, ASN_GAUGE, + (u_char *)&v, sizeof(v)); + return SNMP_ERR_NOERROR; +} + +/* Device metadata, indexed by enum sys_field. Only fields the device + * provides are registered, so an absent one is simply not instantiated. */ +static char sys_values[SYS_FIELD_MAX + 1][SYS_VALUE_MAX]; + +static int sys_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + const char *v = sys_values[reginfo->rootoid[9]]; + + (void)handler; + if (reqinfo->mode != MODE_GET) + return SNMP_ERR_NOERROR; + + snmp_set_var_typed_value(requests->requestvb, ASN_OCTET_STR, + (const u_char *)v, strlen(v)); + return SNMP_ERR_NOERROR; +} + +static void register_sysinfo(void) +{ + static const char *const names[SYS_FIELD_MAX + 1] = { + [SYS_VERSION] = "openwrtSysVersion", + [SYS_REVISION] = "openwrtSysRevision", + [SYS_TARGET] = "openwrtSysTarget", + [SYS_MODEL] = "openwrtSysModel", + }; + int f; + + for (f = 1; f <= SYS_FIELD_MAX; f++) { + if (!sysinfo_read(f, sys_values[f], sizeof(sys_values[f]))) + continue; + /* netsnmp copies the OID into the registration */ + sys_scalar_oid[9] = f; + netsnmp_register_read_only_scalar( + netsnmp_create_handler_registration( + names[f], sys_handler, sys_scalar_oid, + OID_LENGTH(sys_scalar_oid), HANDLER_CAN_RONLY)); + } +} + +/* ------------------------------------------------------------------ */ +/* Wireless interface table */ +/* ------------------------------------------------------------------ */ + +static netsnmp_variable_list *wl_next(void **loop, void **data, + netsnmp_variable_list *idx, + netsnmp_iterator_info *ii) +{ + const struct snapshot *s = snapshot_get(); + size_t i = (size_t)(intptr_t)*loop; + + (void)ii; + if (i >= s->wl_count) + return NULL; + *data = (void *)&s->wl[i]; + *loop = (void *)(intptr_t)(i + 1); + snmp_set_var_typed_integer(idx, ASN_INTEGER, s->wl[i].ifindex); + return idx; +} + +static netsnmp_variable_list *wl_first(void **loop, void **data, + netsnmp_variable_list *idx, + netsnmp_iterator_info *ii) +{ + *loop = (void *)(intptr_t)0; + return wl_next(loop, data, idx, ii); +} + +static void set_gauge(netsnmp_request_info *req, uint32_t v) +{ + snmp_set_var_typed_value(req->requestvb, ASN_GAUGE, + (u_char *)&v, sizeof(v)); +} + +static void set_int(netsnmp_request_info *req, int32_t v) +{ + snmp_set_var_typed_integer(req->requestvb, ASN_INTEGER, v); +} + +static int wl_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + netsnmp_request_info *req; + + (void)handler; (void)reginfo; + if (reqinfo->mode != MODE_GET) + return SNMP_ERR_NOERROR; + + for (req = requests; req; req = req->next) { + const struct wl_iface *w = netsnmp_extract_iterator_context(req); + netsnmp_table_request_info *ti = netsnmp_extract_table_info(req); + + if (!w || !ti) + continue; + + /* An MLD netdev spans several radio links, so the per-radio + * columns have no single answer. Reporting zero would look + * like a real measurement. */ + if (!w->has_radio && (ti->colnum == 5 || ti->colnum == 6 || + ti->colnum == 16 || ti->colnum == 17)) { + netsnmp_request_set_error(req, SNMP_NOSUCHINSTANCE); + continue; + } + /* Even on a single-radio vif, libiwinfo's noise/txpower/survey + * calls can individually fail while channel/frequency succeed; + * a bare 0 would then look like a real dBm/percent reading. */ + if (ti->colnum == 6 && !w->has_noise) { + netsnmp_request_set_error(req, SNMP_NOSUCHINSTANCE); + continue; + } + if (ti->colnum == 16 && !w->has_chanutil) { + netsnmp_request_set_error(req, SNMP_NOSUCHINSTANCE); + continue; + } + if (ti->colnum == 17 && !w->has_txpower) { + netsnmp_request_set_error(req, SNMP_NOSUCHINSTANCE); + continue; + } + + switch (ti->colnum) { + case 2: + snmp_set_var_typed_value(req->requestvb, ASN_OCTET_STR, + (const u_char *)w->name, + strlen(w->name)); + break; + case 3: + snmp_set_var_typed_value(req->requestvb, ASN_OCTET_STR, + (const u_char *)w->ssid, + strlen(w->ssid)); + break; + case 4: set_gauge(req, w->clients); break; + case 5: set_gauge(req, w->frequency); break; + case 6: set_int(req, w->noise); break; + case 7: set_gauge(req, w->tx_min); break; + case 8: set_gauge(req, w->tx_avg); break; + case 9: set_gauge(req, w->tx_max); break; + case 10: set_gauge(req, w->rx_min); break; + case 11: set_gauge(req, w->rx_avg); break; + case 12: set_gauge(req, w->rx_max); break; + case 13: set_int(req, w->snr_min); break; + case 14: set_int(req, w->snr_avg); break; + case 15: set_int(req, w->snr_max); break; + case 16: set_gauge(req, w->chanutil); break; + case 17: set_int(req, w->txpower); break; + default: + netsnmp_request_set_error(req, SNMP_NOSUCHOBJECT); + break; + } + } + return SNMP_ERR_NOERROR; +} + +/* ------------------------------------------------------------------ */ +/* Wireless link table (802.11be Multi-Link Operation) */ +/* ------------------------------------------------------------------ */ + +/* Rows are (ifIndex, linkId) pairs flattened across every interface's + * links[]; *loop is that flat position. wl[] is ifindex-sorted and each + * iface's links[] is link_id-sorted (collect_mlo_links()), so walking flat + * positions in order yields rows in index order as GETNEXT requires. */ +static bool link_row_at(const struct snapshot *s, size_t flat, + const struct wl_iface **out_w, size_t *out_j) +{ + size_t i; + + for (i = 0; i < s->wl_count; i++) { + const struct wl_iface *w = &s->wl[i]; + + if (flat < w->link_count) { + *out_w = w; + *out_j = flat; + return true; + } + flat -= w->link_count; + } + return false; +} + +static netsnmp_variable_list *link_next(void **loop, void **data, + netsnmp_variable_list *idx, + netsnmp_iterator_info *ii) +{ + const struct snapshot *s = snapshot_get(); + size_t flat = (size_t)(intptr_t)*loop; + const struct wl_iface *w; + size_t j; + + (void)ii; + if (!link_row_at(s, flat, &w, &j)) + return NULL; + + *data = (void *)&w->links[j]; + *loop = (void *)(intptr_t)(flat + 1); + snmp_set_var_typed_integer(idx, ASN_INTEGER, w->ifindex); + snmp_set_var_typed_integer(idx->next_variable, ASN_INTEGER, + w->links[j].link_id); + return idx; +} + +static netsnmp_variable_list *link_first(void **loop, void **data, + netsnmp_variable_list *idx, + netsnmp_iterator_info *ii) +{ + *loop = (void *)(intptr_t)0; + return link_next(loop, data, idx, ii); +} + +static int link_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + netsnmp_request_info *req; + + (void)handler; (void)reginfo; + if (reqinfo->mode != MODE_GET) + return SNMP_ERR_NOERROR; + + for (req = requests; req; req = req->next) { + const struct wl_link *l = netsnmp_extract_iterator_context(req); + netsnmp_table_request_info *ti = netsnmp_extract_table_info(req); + + if (!l || !ti) + continue; + + if (ti->colnum == 5 && !l->has_txpower) { + netsnmp_request_set_error(req, SNMP_NOSUCHINSTANCE); + continue; + } + if (ti->colnum == 6 && !l->has_chanutil) { + netsnmp_request_set_error(req, SNMP_NOSUCHINSTANCE); + continue; + } + + switch (ti->colnum) { + case 3: + snmp_set_var_typed_value(req->requestvb, ASN_OCTET_STR, + (const u_char *)l->addr, + sizeof(l->addr)); + break; + case 4: set_gauge(req, l->frequency); break; + case 5: set_int(req, l->txpower); break; + case 6: set_gauge(req, l->chanutil); break; + default: + netsnmp_request_set_error(req, SNMP_NOSUCHOBJECT); + break; + } + } + return SNMP_ERR_NOERROR; +} + +/* ------------------------------------------------------------------ */ +/* LM-SENSORS temperature and fan tables */ +/* ------------------------------------------------------------------ */ + +/* Both tables have the same shape, so one pair of hooks serves them; the + * iterator info's my_loop_context selects which list to walk. */ +struct lm_ctx { bool fan; }; +static struct lm_ctx lm_temp_ctx = { .fan = false }; +static struct lm_ctx lm_fan_ctx = { .fan = true }; + +static netsnmp_variable_list *lm_next(void **loop, void **data, + netsnmp_variable_list *idx, + netsnmp_iterator_info *ii) +{ + const struct snapshot *s = snapshot_get(); + const struct lm_ctx *c = ii->myvoid; + const struct sensor *list = c->fan ? s->fan : s->temp; + size_t count = c->fan ? s->fan_count : s->temp_count; + size_t i = (size_t)(intptr_t)*loop; + + if (i >= count) + return NULL; + *data = (void *)&list[i]; + *loop = (void *)(intptr_t)(i + 1); + snmp_set_var_typed_integer(idx, ASN_INTEGER, list[i].index); + return idx; +} + +static netsnmp_variable_list *lm_first(void **loop, void **data, + netsnmp_variable_list *idx, + netsnmp_iterator_info *ii) +{ + *loop = (void *)(intptr_t)0; + return lm_next(loop, data, idx, ii); +} + +static int lm_handler(netsnmp_mib_handler *handler, + netsnmp_handler_registration *reginfo, + netsnmp_agent_request_info *reqinfo, + netsnmp_request_info *requests) +{ + netsnmp_request_info *req; + + (void)handler; (void)reginfo; + if (reqinfo->mode != MODE_GET) + return SNMP_ERR_NOERROR; + + for (req = requests; req; req = req->next) { + const struct sensor *sn = netsnmp_extract_iterator_context(req); + netsnmp_table_request_info *ti = netsnmp_extract_table_info(req); + + if (!sn || !ti) + continue; + switch (ti->colnum) { + case 1: set_int(req, sn->index); break; + case 2: + snmp_set_var_typed_value(req->requestvb, ASN_OCTET_STR, + (const u_char *)sn->device, + strlen(sn->device)); + break; + case 3: set_gauge(req, sn->value); break; + default: + netsnmp_request_set_error(req, SNMP_NOSUCHOBJECT); + break; + } + } + return SNMP_ERR_NOERROR; +} + +/* ------------------------------------------------------------------ */ + +static void register_table_n(const char *name, oid *base, size_t baselen, + Netsnmp_Node_Handler *handler, + Netsnmp_First_Data_Point *first, + Netsnmp_Next_Data_Point *next, + int min_col, int max_col, void *myvoid, + int n_index) +{ + netsnmp_handler_registration *reg; + netsnmp_table_registration_info *tinfo; + netsnmp_iterator_info *iinfo; + + reg = netsnmp_create_handler_registration(name, handler, base, baselen, + HANDLER_CAN_RONLY); + tinfo = SNMP_MALLOC_TYPEDEF(netsnmp_table_registration_info); + if (n_index == 2) + netsnmp_table_helper_add_indexes(tinfo, ASN_INTEGER, + ASN_INTEGER, 0); + else + netsnmp_table_helper_add_indexes(tinfo, ASN_INTEGER, 0); + tinfo->min_column = min_col; + tinfo->max_column = max_col; + + iinfo = SNMP_MALLOC_TYPEDEF(netsnmp_iterator_info); + iinfo->get_first_data_point = first; + iinfo->get_next_data_point = next; + iinfo->table_reginfo = tinfo; + iinfo->myvoid = myvoid; + + netsnmp_register_table_iterator(reg, iinfo); +} + +static void register_table(const char *name, oid *base, size_t baselen, + Netsnmp_Node_Handler *handler, + Netsnmp_First_Data_Point *first, + Netsnmp_Next_Data_Point *next, + int min_col, int max_col, void *myvoid) +{ + register_table_n(name, base, baselen, handler, first, next, + min_col, max_col, myvoid, 1); +} + +int main(int argc, char **argv) +{ + /* OpenWrt's snmpd writes "agentXSocket /var/run/agentx.sock"; net-snmp's + * compiled-in default is elsewhere, so connect where snmpd listens. */ + const char *sock = "/var/run/agentx.sock"; + int use_syslog = 1; + int i; + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-f")) + use_syslog = 0; + else if (!strcmp(argv[i], "-x") && i + 1 < argc) + sock = argv[++i]; + else if (!strcmp(argv[i], "--dump")) { + collect_dump(); + return 0; + } else if (!strcmp(argv[i], "--version") || + !strcmp(argv[i], "-V") || + !strcmp(argv[i], "--help")) { + printf("openwrt-snmp-agent " AGENT_VERSION "\n"); + return 0; + } + } + + if (use_syslog) + snmp_enable_calllog(); + else + snmp_enable_stderrlog(); + + /* Run as an AgentX subagent, not a standalone master. */ + netsnmp_ds_set_boolean(NETSNMP_DS_APPLICATION_ID, + NETSNMP_DS_AGENT_ROLE, 1); + netsnmp_ds_set_string(NETSNMP_DS_APPLICATION_ID, + NETSNMP_DS_AGENT_X_SOCKET, sock); + + init_agent("openwrt-metrics"); + + register_sysinfo(); + + netsnmp_register_read_only_scalar( + netsnmp_create_handler_registration( + "openwrtWirelessInterfaceCount", scalar_handler, + wl_scalar_ifcount, OID_LENGTH(wl_scalar_ifcount), + HANDLER_CAN_RONLY)); + netsnmp_register_read_only_scalar( + netsnmp_create_handler_registration( + "openwrtWirelessClientCount", scalar_handler, + wl_scalar_clients, OID_LENGTH(wl_scalar_clients), + HANDLER_CAN_RONLY)); + + register_table("openwrtWirelessInterfaceTable", wl_table_oid, + OID_LENGTH(wl_table_oid), wl_handler, wl_first, wl_next, + 2, 17, NULL); + register_table_n("openwrtWirelessLinkTable", wl_link_table_oid, + OID_LENGTH(wl_link_table_oid), link_handler, + link_first, link_next, 3, 6, NULL, 2); + register_table("lmTempSensorsTable", lm_temp_table_oid, + OID_LENGTH(lm_temp_table_oid), lm_handler, lm_first, + lm_next, 1, 3, &lm_temp_ctx); + register_table("lmFanSensorsTable", lm_fan_table_oid, + OID_LENGTH(lm_fan_table_oid), lm_handler, lm_first, + lm_next, 1, 3, &lm_fan_ctx); + + init_snmp("openwrt-metrics"); + + signal(SIGTERM, on_signal); + signal(SIGINT, on_signal); + + while (running) + agent_check_and_process(1); + + snmp_shutdown("openwrt-metrics"); + snapshot_cleanup(); + return 0; +} diff --git a/net/snmpd-openwrt-metrics/src/collect.c b/net/snmpd-openwrt-metrics/src/collect.c new file mode 100644 index 0000000000000..36ef6ca09b103 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/collect.c @@ -0,0 +1,774 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * collect.c - wireless metrics via libiwinfo, sensors via sysfs, device + * metadata from /etc/os-release and /tmp/sysinfo. + * + * Wireless interfaces are found by looking for a phy80211 link under + * /sys/class/net; that covers every mac80211 netdev including MLD interfaces, + * which carry their own SSID and their own associations. + * + * Temperatures are the union of two sysfs sources. hwmon exposes sensors that + * have no thermal zone at all (an MDIO PHY die sensor, for example), while + * thermal zones exist on targets built without CONFIG_THERMAL_HWMON. A zone + * already reachable through hwmon is not counted twice. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "collect.h" +#include "mlo.h" + +#define SYS_NET "/sys/class/net" +#define SYS_HWMON "/sys/class/hwmon" +#define SYS_THERMAL "/sys/class/thermal" +#define DEFAULT_TTL 20 +/* sysfs paths used here are short and fully bounded */ +#define SYSFS_PATH 128 + +static struct snapshot snap; +static bool snap_valid; + +static int ttl_seconds(void) +{ + const char *v = getenv("OPENWRT_SNMP_TTL"); + int t = v ? atoi(v) : 0; + return (t > 0) ? t : DEFAULT_TTL; +} + +/* Build "/" only if it fits, so sysfs path construction cannot + * silently truncate. */ +static bool join(char *out, size_t len, const char *dir, const char *name) +{ + size_t dl = strlen(dir), nl = strlen(name); + + if (dl + nl + 2 > len) + return false; + memcpy(out, dir, dl); + out[dl] = '/'; + memcpy(out + dl + 1, name, nl + 1); + return true; +} + +/* Read a small text file; returns length or -1. Trailing newline removed. */ +static ssize_t slurp(const char *path, char *buf, size_t len) +{ + FILE *f = fopen(path, "r"); + size_t n; + + if (!f) + return -1; + n = fread(buf, 1, len - 1, f); + fclose(f); + buf[n] = '\0'; + while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\t' || buf[n - 1] == ' ')) + buf[--n] = '\0'; + return (ssize_t)n; +} + +static bool read_u32(const char *path, uint32_t *out) +{ + char buf[32]; + long v; + + if (slurp(path, buf, sizeof(buf)) < 0) + return false; + /* hwmon/thermal-zone temperatures are signed and can be sub-zero; + * Gauge32 cannot represent that, so clamp rather than let strtoul() + * wrap a negative reading into a multi-billion milli-degree value. */ + v = strtol(buf, NULL, 10); + *out = (v < 0) ? 0 : (uint32_t)v; + return true; +} + +/* ------------------------------------------------------------------ */ +/* Wireless */ +/* ------------------------------------------------------------------ */ + +struct mac { uint8_t a[6]; }; + +static bool mac_seen(const struct mac *set, size_t n, const uint8_t *m) +{ + size_t i; + + for (i = 0; i < n; i++) + if (!memcmp(set[i].a, m, 6)) + return true; + return false; +} + +/* Channel utilisation, in tenths of a percent, from the survey entry + * matching the operating channel. iwinfo_survey_entry has no in-use flag, + * so the frequency is the key. */ +static bool survey_utilisation(const struct iwinfo_ops *iw, const char *ifname, + uint32_t mhz, uint32_t *pct) +{ + char buf[IWINFO_BUFSIZE]; + int len = 0, i; + + if (!iw->survey || iw->survey(ifname, buf, &len) || len <= 0) + return false; + + for (i = 0; i < len / (int)sizeof(struct iwinfo_survey_entry); i++) { + struct iwinfo_survey_entry *e = + (struct iwinfo_survey_entry *)&buf[i * sizeof(*e)]; + + if (e->mhz != mhz || e->active_time == 0) + continue; + if (e->busy_time > e->active_time) + return false; + *pct = (uint32_t)((e->busy_time * 1000ULL) / e->active_time); + return true; + } + return false; +} + +/* GETNEXT requires rows in index order; readdir() order is arbitrary. */ +static int cmp_ifindex(const void *a, const void *b) +{ + const struct wl_iface *x = a, *y = b; + + return (x->ifindex > y->ifindex) - (x->ifindex < y->ifindex); +} + +static int cmp_link_id(const void *a, const void *b) +{ + const struct wl_link *x = a, *y = b; + + return (x->link_id > y->link_id) - (x->link_id < y->link_id); +} + +/* Queries this vif's MLO links directly over nl80211 (see mlo.c) and fills + * in channel utilisation for each via the same survey data the single-radio + * path uses, keyed by that link's own frequency. Returns the link count; + * zero means ifname is not an MLD. */ +static size_t collect_mlo_links(const struct iwinfo_ops *iw, const char *ifname, + struct wl_link *out, size_t max) +{ + struct mlo_link raw[WL_MAX_LINKS]; + size_t n, i; + + n = mlo_get_links(ifname, raw, max < WL_MAX_LINKS ? max : WL_MAX_LINKS); + if (n > max) + n = max; + + for (i = 0; i < n; i++) { + out[i].link_id = raw[i].link_id; + memcpy(out[i].addr, raw[i].addr, sizeof(out[i].addr)); + out[i].frequency = raw[i].frequency; + out[i].has_txpower = raw[i].has_txpower; + out[i].txpower = raw[i].txpower; + out[i].has_chanutil = raw[i].frequency && + survey_utilisation(iw, ifname, raw[i].frequency, + &out[i].chanutil); + } + + qsort(out, n, sizeof(out[0]), cmp_link_id); + return n; +} + +/* hostapd reports the SSID it is actually serving over ubus. libiwinfo + * derives it from scan results or hostapd's generated config, which comes + * back empty for some VAPs (seen on the second radio of a tri-band phy), so + * ask hostapd first and keep iwinfo as the fallback. */ +static struct ubus_context *ubus_ctx; +static bool ubus_tried; + +static void hostapd_ssid_cb(struct ubus_request *req, int type, + struct blob_attr *msg) +{ + static const struct blobmsg_policy pol[1] = { + { .name = "ssid", .type = BLOBMSG_TYPE_STRING }, + }; + struct blob_attr *tb[1]; + const char *v; + + (void)type; + blobmsg_parse(pol, 1, tb, blob_data(msg), blob_len(msg)); + if (!tb[0]) + return; + v = blobmsg_get_string(tb[0]); + if (strlen(v) <= IWINFO_ESSID_MAX_SIZE) + strcpy(req->priv, v); +} + +static int hostapd_ssid(const char *ifname, char *buf) +{ + char obj[64]; + uint32_t id; + + if (!ubus_ctx && !ubus_tried) { + ubus_tried = true; + ubus_ctx = ubus_connect(NULL); + } + if (!ubus_ctx) + return -1; + if (snprintf(obj, sizeof(obj), "hostapd.%s", ifname) >= (int)sizeof(obj)) + return -1; + if (ubus_lookup_id(ubus_ctx, obj, &id)) + return -1; + buf[0] = '\0'; + if (ubus_invoke(ubus_ctx, id, "get_status", NULL, hostapd_ssid_cb, + buf, 500) || !buf[0]) + return -1; + return 0; +} + +static int iface_ssid(const struct iwinfo_ops *iw, const char *ifname, + char *buf) +{ + if (!hostapd_ssid(ifname, buf)) + return 0; + buf[0] = '\0'; + return iw->ssid ? iw->ssid(ifname, buf) : -1; +} + +static void ubus_release(void) +{ + if (ubus_ctx) + ubus_free(ubus_ctx); + ubus_ctx = NULL; + ubus_tried = false; +} + +static void collect_wireless(struct snapshot *s) +{ + struct mac seen[512]; + size_t seen_n = 0; + DIR *d; + struct dirent *de; + + s->wl_count = 0; + s->client_total = 0; + + d = opendir(SYS_NET); + if (!d) + return; + + while ((de = readdir(d)) && s->wl_count < WL_MAX_IFACES) { + char path[PATH_MAX], abuf[IWINFO_BUFSIZE]; + const struct iwinfo_ops *iw; + struct wl_iface *w; + char ssid_buf[IWINFO_ESSID_MAX_SIZE + 1] = { 0 }; + int val = 0, mode = 0, alen = 0, i, n; + uint64_t tx_sum = 0, rx_sum = 0; + int64_t snr_sum = 0; + + if (de->d_name[0] == '.') + continue; + if (!join(path, sizeof(path) - 10, SYS_NET, de->d_name)) + continue; + strcat(path, "/phy80211"); + if (access(path, F_OK)) + continue; + + iw = iwinfo_backend(de->d_name); + if (!iw) + continue; + if (iface_ssid(iw, de->d_name, ssid_buf)) + ssid_buf[0] = '\0'; + + mode = 0; + if (iw->mode) + iw->mode(de->d_name, &mode); + + w = &s->wl[s->wl_count]; + memset(w, 0, sizeof(*w)); + if (strlen(de->d_name) >= sizeof(w->name)) + continue; + memcpy(w->name, de->d_name, strlen(de->d_name) + 1); + w->ifindex = (int)if_nametoindex(de->d_name); + if (w->ifindex <= 0) + continue; + + /* iwinfo writes at most IWINFO_ESSID_MAX_SIZE + 1 bytes. An + * interface with no SSID (a monitor or a down VAP) falls back + * to its name so the row is still identifiable. */ + if (ssid_buf[0]) + memcpy(w->ssid, ssid_buf, strlen(ssid_buf) + 1); + else + memcpy(w->ssid, w->name, strlen(w->name) + 1); + + /* An MLD netdev spans several links; query them directly over + * nl80211 rather than through libiwinfo, which has no MLO + * awareness and reports whatever single link the kernel's + * legacy top-level compat attributes happen to carry (seen + * live: an arbitrary link's channel, and no tx power at + * all). A non-MLD vif gets zero links here and falls through + * to the existing single-radio path below unchanged. */ + w->link_count = collect_mlo_links(iw, de->d_name, w->links, + WL_MAX_LINKS); + + /* Radio-scoped values. An MLD netdev cannot answer these as a + * single value; leave has_radio false so the agent reports + * noSuchInstance rather than a misleading zero, and rely on + * links[] above for its real per-radio data instead. */ + if (w->link_count == 0) { + val = 0; + if (iw->channel && iw->channel(de->d_name, &val)) + val = 0; + if (val > 0) { + val = 0; + if (iw->frequency && iw->frequency(de->d_name, &val)) + val = 0; + } + if (val > 0) { + w->frequency = (uint32_t)val; + w->has_radio = true; + val = 0; + w->has_noise = iw->noise && !iw->noise(de->d_name, &val); + if (w->has_noise) + w->noise = val; + val = 0; + w->has_txpower = iw->txpower && !iw->txpower(de->d_name, &val); + if (w->has_txpower) + w->txpower = val; + w->has_chanutil = survey_utilisation(iw, de->d_name, + w->frequency, + &w->chanutil); + } + } + + /* Associations: rates, SNR and the deduplicated client total. */ + if (iw->assoclist && !iw->assoclist(de->d_name, abuf, &alen) && alen > 0) { + n = alen / (int)sizeof(struct iwinfo_assoclist_entry); + for (i = 0; i < n; i++) { + struct iwinfo_assoclist_entry *e = + (struct iwinfo_assoclist_entry *) + &abuf[i * sizeof(*e)]; + uint32_t tx = e->tx_rate.rate / 100; + uint32_t rx = e->rx_rate.rate / 100; + int32_t snr = (int32_t)e->signal - (int32_t)e->noise; + + if (!w->has_rates) { + w->tx_min = w->tx_max = tx; + w->rx_min = w->rx_max = rx; + w->snr_min = w->snr_max = snr; + w->has_rates = true; + } else { + if (tx < w->tx_min) w->tx_min = tx; + if (tx > w->tx_max) w->tx_max = tx; + if (rx < w->rx_min) w->rx_min = rx; + if (rx > w->rx_max) w->rx_max = rx; + if (snr < w->snr_min) w->snr_min = snr; + if (snr > w->snr_max) w->snr_max = snr; + } + tx_sum += tx; + rx_sum += rx; + snr_sum += snr; + w->clients++; + + /* openwrtWirelessClientCount is documented to exclude + * this device's own upstream associations, so only + * an AP/master-mode row's peers count as clients; + * a STA-mode uplink's one "peer" is the AP above us, + * not a client of ours. */ + if (mode == IWINFO_OPMODE_MASTER && + seen_n < sizeof(seen) / sizeof(seen[0]) && + !mac_seen(seen, seen_n, e->mac)) { + memcpy(seen[seen_n].a, e->mac, 6); + seen_n++; + } + } + if (w->clients) { + w->tx_avg = (uint32_t)(tx_sum / w->clients); + w->rx_avg = (uint32_t)(rx_sum / w->clients); + w->snr_avg = (int32_t)(snr_sum / (int64_t)w->clients); + } + } + + s->wl_count++; + iwinfo_finish(); + } + closedir(d); + ubus_release(); + qsort(s->wl, s->wl_count, sizeof(s->wl[0]), cmp_ifindex); + s->client_total = (uint32_t)seen_n; +} + +/* ------------------------------------------------------------------ */ +/* Sensors */ +/* ------------------------------------------------------------------ */ + +/* Records which thermal zones are already exposed through hwmon so the + * thermal-zone pass does not report them a second time. */ +struct zone_cover { int zone[SENSOR_MAX]; size_t n; }; + +static bool zone_covered(const struct zone_cover *c, int z) +{ + size_t i; + + for (i = 0; i < c->n; i++) + if (c->zone[i] == z) + return true; + return false; +} + +static void label_for(const char *dir, const char *input, char *out, size_t len) +{ + char path[SYSFS_PATH * 2]; + char *suffix; + + if (!join(path, sizeof(path), dir, input)) { + snprintf(out, len, "unknown"); + return; + } + suffix = strstr(path, "_input"); + if (suffix) { + memcpy(suffix, "_label", 6); + if (slurp(path, out, len) > 0) + return; + } + if (join(path, sizeof(path), dir, "name") && slurp(path, out, len) > 0) + return; + snprintf(out, len, "unknown"); +} + +static void collect_sensors(struct snapshot *s) +{ + struct zone_cover cover = { .n = 0 }; + DIR *d, *hd; + struct dirent *de, *he; + + s->temp_count = 0; + s->fan_count = 0; + + d = opendir(SYS_HWMON); + if (d) { + while ((de = readdir(d))) { + char dir[SYSFS_PATH], real[PATH_MAX], *zp; + + if (strncmp(de->d_name, "hwmon", 5)) + continue; + if (!join(dir, sizeof(dir), SYS_HWMON, de->d_name)) + continue; + + /* Note any thermal zone backing this hwmon device. */ + if (realpath(dir, real) && (zp = strstr(real, "thermal_zone"))) + if (cover.n < SENSOR_MAX) + cover.zone[cover.n++] = atoi(zp + 12); + + hd = opendir(dir); + if (!hd) + continue; + while ((he = readdir(hd))) { + char path[SYSFS_PATH * 2]; + uint32_t v; + bool is_temp = !strncmp(he->d_name, "temp", 4); + bool is_fan = !strncmp(he->d_name, "fan", 3); + + if ((!is_temp && !is_fan) || !strstr(he->d_name, "_input")) + continue; + if (!join(path, sizeof(path), dir, he->d_name)) + continue; + if (!read_u32(path, &v)) + continue; + + if (is_temp && s->temp_count < SENSOR_MAX) { + struct sensor *t = &s->temp[s->temp_count]; + t->index = (int)s->temp_count + 1; + t->value = v; + label_for(dir, he->d_name, t->device, + sizeof(t->device)); + s->temp_count++; + } else if (is_fan && s->fan_count < SENSOR_MAX) { + struct sensor *f = &s->fan[s->fan_count]; + f->index = (int)s->fan_count + 1; + f->value = v; + label_for(dir, he->d_name, f->device, + sizeof(f->device)); + s->fan_count++; + } + } + closedir(hd); + } + closedir(d); + } + + /* Thermal zones with no hwmon representation. */ + d = opendir(SYS_THERMAL); + if (!d) + return; + while ((de = readdir(d))) { + char path[PATH_MAX]; + uint32_t v; + int z; + + if (strncmp(de->d_name, "thermal_zone", 12)) + continue; + z = atoi(de->d_name + 12); + if (zone_covered(&cover, z) || s->temp_count >= SENSOR_MAX) + continue; + if (!join(path, sizeof(path) - 6, SYS_THERMAL, de->d_name)) + continue; + strcat(path, "/temp"); + if (!read_u32(path, &v)) + continue; + struct sensor *t = &s->temp[s->temp_count]; + t->index = (int)s->temp_count + 1; + t->value = v; + if (join(path, sizeof(path) - 6, SYS_THERMAL, de->d_name)) + strcat(path, "/type"); + if (slurp(path, t->device, sizeof(t->device)) <= 0) + t->device[0] = '\0'; + if (!t->device[0]) + snprintf(t->device, sizeof(t->device), "zone%d", z); + s->temp_count++; + } + closedir(d); +} + +/* ------------------------------------------------------------------ */ +/* Device metadata */ +/* ------------------------------------------------------------------ */ + +#define OS_RELEASE "/etc/os-release" +#define SYSINFO_MODEL "/tmp/sysinfo/model" + +/* Value of KEY in an os-release file: KEY=value lines, value optionally + * wrapped in double or single quotes. OpenWrt's own file never uses the + * backslash escapes the format allows, so they are not interpreted. */ +static bool os_release_value(const char *key, char *out, size_t len) +{ + char buf[4096], *line, *save = NULL; + size_t kl = strlen(key); + + if (slurp(OS_RELEASE, buf, sizeof(buf)) < 0) + return false; + + for (line = strtok_r(buf, "\n", &save); line; + line = strtok_r(NULL, "\n", &save)) { + char *v; + size_t vl; + + if (strncmp(line, key, kl) || line[kl] != '=') + continue; + v = line + kl + 1; + vl = strlen(v); + if (vl > 0 && v[vl - 1] == '\r') + v[--vl] = '\0'; + if (vl >= 2 && (v[0] == '"' || v[0] == '\'') && v[vl - 1] == v[0]) { + v++; + vl -= 2; + } + if (vl == 0 || vl >= len) + return false; + memcpy(out, v, vl); + out[vl] = '\0'; + return true; + } + return false; +} + +/* /tmp is world-writable and the agent runs as root, so only trust a + * regular, root-owned file there and never follow a symlink to one. Like + * os_release_value(), a value that does not fit is rejected, not cut. */ +static bool read_tmp_value(const char *path, char *out, size_t len) +{ + char buf[SYS_VALUE_MAX + 2]; + struct stat st; + ssize_t n; + int fd; + + /* buf only proves a value is too long up to this bound */ + if (len > SYS_VALUE_MAX) + len = SYS_VALUE_MAX; + + fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return false; + if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_uid != 0) { + close(fd); + return false; + } + n = read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <= 0) + return false; + while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || + buf[n - 1] == '\t' || buf[n - 1] == ' ')) + n--; + if (n == 0 || (size_t)n >= len) + return false; + memcpy(out, buf, n); + out[n] = '\0'; + return true; +} + +bool sysinfo_read(enum sys_field f, char *out, size_t len) +{ + bool ok = false; + char *p; + + switch (f) { + case SYS_VERSION: ok = os_release_value("VERSION", out, len); break; + case SYS_REVISION: ok = os_release_value("BUILD_ID", out, len); break; + case SYS_TARGET: ok = os_release_value("OPENWRT_BOARD", out, len); break; + case SYS_MODEL: ok = read_tmp_value(SYSINFO_MODEL, out, len); break; + } + /* Served as DisplayString: keep control characters out of it. */ + for (p = out; ok && *p; p++) + if ((unsigned char)*p < 0x20 || *p == 0x7f) + *p = '?'; + return ok; +} + +/* ------------------------------------------------------------------ */ + +const struct snapshot *snapshot_get(void) +{ + time_t now = time(NULL); + + if (snap_valid && (now - snap.taken) < ttl_seconds()) + return &snap; + + memset(&snap, 0, sizeof(snap)); + collect_wireless(&snap); + collect_sensors(&snap); + snap.taken = now; + snap_valid = true; + return &snap; +} + +void snapshot_cleanup(void) +{ + iwinfo_finish(); + mlo_cleanup(); + snap_valid = false; +} + +/* ------------------------------------------------------------------ */ +/* Diagnostic dump */ +/* ------------------------------------------------------------------ */ + +/* Mirrors collect_wireless()'s discovery and call order exactly, including + * the per-interface iwinfo_finish(), so whatever state dependency causes + * the CLI/in-process divergence is reproduced rather than avoided. */ +void collect_dump(void) +{ + DIR *d; + struct dirent *de; + int f; + + printf("== system ==\n"); + for (f = 1; f <= SYS_FIELD_MAX; f++) { + char v[SYS_VALUE_MAX]; + + if (sysinfo_read(f, v, sizeof(v))) + printf(" field %d: \"%s\"\n", f, v); + else + printf(" field %d: (absent)\n", f); + } + + d = opendir(SYS_NET); + if (!d) { + printf("cannot open %s\n", SYS_NET); + return; + } + + while ((de = readdir(d))) { + char path[PATH_MAX], abuf[IWINFO_BUFSIZE]; + const struct iwinfo_ops *iw; + char ssid_buf[IWINFO_ESSID_MAX_SIZE + 1] = { 0 }; + int mode_v = 0, chan_v = 0, freq_v = 0, noise_v = 0, txp_v = 0; + int mode_r, ssid_r, chan_r, freq_r, noise_r, txp_r; + int alen = 0, assoc_r; + + if (de->d_name[0] == '.') + continue; + if (!join(path, sizeof(path) - 10, SYS_NET, de->d_name)) + continue; + strcat(path, "/phy80211"); + if (access(path, F_OK)) + continue; + + iw = iwinfo_backend(de->d_name); + printf("== %s ==\n", de->d_name); + if (!iw) { + printf(" iwinfo_backend: NULL (no backend claims this iface)\n"); + continue; + } + printf(" backend: %s\n", iw->name ? iw->name : "(null name)"); + + mode_r = iw->mode ? iw->mode(de->d_name, &mode_v) : -1; + printf(" mode() ret=%d val=%d\n", mode_r, mode_v); + + ssid_r = iface_ssid(iw, de->d_name, ssid_buf); + printf(" ssid() ret=%d val=\"%s\"\n", ssid_r, ssid_buf); + + chan_r = iw->channel ? iw->channel(de->d_name, &chan_v) : -1; + printf(" channel() ret=%d val=%d\n", chan_r, chan_v); + + freq_r = iw->frequency ? iw->frequency(de->d_name, &freq_v) : -1; + printf(" frequency() ret=%d val=%d\n", freq_r, freq_v); + + noise_r = iw->noise ? iw->noise(de->d_name, &noise_v) : -1; + printf(" noise() ret=%d val=%d\n", noise_r, noise_v); + + txp_r = iw->txpower ? iw->txpower(de->d_name, &txp_v) : -1; + printf(" txpower() ret=%d val=%d\n", txp_r, txp_v); + + assoc_r = iw->assoclist ? iw->assoclist(de->d_name, abuf, &alen) : -1; + printf(" assoclist() ret=%d len=%d (%d entries)\n", assoc_r, alen, + alen > 0 ? (int)(alen / (int)sizeof(struct iwinfo_assoclist_entry)) : 0); + + { + struct mlo_link links[WL_MAX_LINKS]; + size_t n = mlo_get_links(de->d_name, links, WL_MAX_LINKS); + size_t k; + + printf(" mlo_get_links() n=%zu\n", n); + for (k = 0; k < n; k++) { + const struct mlo_link *l = &links[k]; + uint32_t cu; + bool has_cu = l->frequency && + survey_utilisation(iw, de->d_name, + l->frequency, &cu); + + printf(" link id=%d addr=%02x:%02x:%02x:%02x:%02x:%02x " + "freq=%u txpower=%s%d chanutil=%s%u\n", + l->link_id, + l->addr[0], l->addr[1], l->addr[2], + l->addr[3], l->addr[4], l->addr[5], + l->frequency, + l->has_txpower ? "" : "(absent)", + l->has_txpower ? l->txpower : 0, + has_cu ? "" : "(absent)", + has_cu ? cu : 0); + } + } + + if (getenv("OPENWRT_SNMP_DUMP_NO_FINISH")) { + printf(" -- iwinfo_finish() SKIPPED (OPENWRT_SNMP_DUMP_NO_FINISH) --\n"); + } else { + iwinfo_finish(); + printf(" -- iwinfo_finish() called --\n"); + } + } + closedir(d); + ubus_release(); + { + static struct snapshot ds; + size_t k; + + memset(&ds, 0, sizeof(ds)); + collect_sensors(&ds); + printf("== sensors ==\n temp: %zu fan: %zu\n", (size_t)ds.temp_count, + (size_t)ds.fan_count); + for (k = 0; k < ds.temp_count; k++) + printf(" temp %d \"%s\" %u\n", ds.temp[k].index, + ds.temp[k].device, (unsigned)ds.temp[k].value); + for (k = 0; k < ds.fan_count; k++) + printf(" fan %d \"%s\" %u\n", ds.fan[k].index, + ds.fan[k].device, (unsigned)ds.fan[k].value); + } +} diff --git a/net/snmpd-openwrt-metrics/src/collect.h b/net/snmpd-openwrt-metrics/src/collect.h new file mode 100644 index 0000000000000..683955a7be9b6 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/collect.h @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * collect.h - data collection for the OpenWrt SNMP subagent. + * + * A single snapshot holds every metric the agent serves. It is rebuilt at most + * once per SNAPSHOT_TTL seconds: an SNMP walk drives one getnext per OID, and + * re-running netlink queries for each would overrun the AgentX timeout. + */ +#ifndef OPENWRT_SNMP_COLLECT_H +#define OPENWRT_SNMP_COLLECT_H + +#include +#include +#include +#include + +#define WL_MAX_IFACES 32 +#define WL_MAX_LINKS 4 +#define SENSOR_MAX 64 +#define LABEL_MAX 64 + +/* One radio link of a multi-link (MLD) interface. A regular single-link vif + * has none of these; an MLD's own channel/noise/txpower/chanutil columns are + * absent for the same reason (see has_radio below) and these rows are where + * that per-radio data actually lives instead, one row per link. */ +struct wl_link { + int link_id; + uint8_t addr[6]; + uint32_t frequency; /* MHz */ + bool has_txpower; + int32_t txpower; /* dBm */ + bool has_chanutil; + uint32_t chanutil; /* tenths of a percent */ +}; + +/* Columns an MLD (multi-link) interface cannot answer are marked absent + * rather than reported as zero; the agent returns noSuchInstance for them. + * Their real per-radio data is in links[] / link_count instead. */ +struct wl_iface { + int ifindex; + char name[32]; + char ssid[LABEL_MAX]; + + uint32_t clients; + + bool has_radio; /* frequency valid */ + uint32_t frequency; /* MHz */ + bool has_noise; + int32_t noise; /* dBm */ + bool has_txpower; + int32_t txpower; /* dBm */ + bool has_chanutil; + uint32_t chanutil; /* tenths of a percent */ + + struct wl_link links[WL_MAX_LINKS]; + size_t link_count; /* >0 marks this vif an MLD */ + + bool has_rates; /* false when no clients are associated */ + uint32_t tx_min, tx_avg, tx_max; /* tenths of Mbit/s */ + uint32_t rx_min, rx_avg, rx_max; /* tenths of Mbit/s */ + int32_t snr_min, snr_avg, snr_max; /* dB */ +}; + +struct sensor { + int index; + char device[LABEL_MAX]; + uint32_t value; /* milli-degrees C, or RPM */ +}; + +struct snapshot { + time_t taken; + struct wl_iface wl[WL_MAX_IFACES]; + size_t wl_count; + uint32_t client_total; /* deduplicated across all VAPs */ + struct sensor temp[SENSOR_MAX]; + size_t temp_count; + struct sensor fan[SENSOR_MAX]; + size_t fan_count; +}; + +/* Device metadata (OPENWRT-MIB openwrtSystem), numbered as its sub-OIDs. */ +enum sys_field { + SYS_VERSION = 1, + SYS_REVISION, + SYS_TARGET, + SYS_MODEL, + SYS_FIELD_MAX = SYS_MODEL +}; + +#define SYS_VALUE_MAX 256 /* DisplayString SIZE (0..255) + NUL */ + +/* Reads one metadata field into out. False if the device does not provide + * it, or it is empty. None of these change without a reboot, so the agent + * reads them once at startup. */ +bool sysinfo_read(enum sys_field f, char *out, size_t len); + +/* Returns the current snapshot, refreshing it if older than the TTL. */ +const struct snapshot *snapshot_get(void); +void snapshot_cleanup(void); + +/* Prints the raw return code and value of every wireless op, per interface, + * to stdout. Diagnostic tool for cases where libiwinfo behaves differently + * in-process than via the iwinfo CLI (see agent.c --dump). */ +void collect_dump(void); + +#endif /* OPENWRT_SNMP_COLLECT_H */ diff --git a/net/snmpd-openwrt-metrics/src/mlo.c b/net/snmpd-openwrt-metrics/src/mlo.c new file mode 100644 index 0000000000000..518d815bd3c6a --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/mlo.c @@ -0,0 +1,201 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * mlo.c - direct nl80211 query for Multi-Link Operation per-link data. + * + * libiwinfo has no MLO awareness: its channel/frequency/txpower calls read + * the legacy top-level NL80211_ATTR_WIPHY_FREQ / NL80211_ATTR_WIPHY_TX_POWER_LEVEL + * from a plain NL80211_CMD_GET_INTERFACE, attributes the kernel does not + * reliably populate for an MLD wdev (confirmed live on a two-link AP: the + * top-level frequency picked one arbitrary link's channel, and the top-level + * tx power was absent entirely). The real per-link values live nested under + * NL80211_ATTR_MLO_LINKS, one entry per link, which nothing in libiwinfo's + * public API exposes - so this talks to nl80211 directly for that one + * attribute instead, mirroring the same request/parse pattern libiwinfo + * itself uses internally. + */ +#include +#include + +#include +#include +#include + +#include + +#include "mlo.h" + +struct mlo_state { + struct nl_sock *sock; + int family_id; +}; + +static struct mlo_state st; +static bool st_valid; + +static bool mlo_init(void) +{ + if (st_valid) + return true; + + st.sock = nl_socket_alloc(); + if (!st.sock) + return false; + + if (genl_connect(st.sock)) { + nl_socket_free(st.sock); + st.sock = NULL; + return false; + } + + st.family_id = genl_ctrl_resolve(st.sock, "nl80211"); + if (st.family_id < 0) { + nl_socket_free(st.sock); + st.sock = NULL; + return false; + } + + st_valid = true; + return true; +} + +struct mlo_cb_ctx { + struct mlo_link *out; + size_t max; + size_t n; +}; + +static int mlo_valid_cb(struct nl_msg *msg, void *arg) +{ + struct mlo_cb_ctx *ctx = arg; + struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg)); + struct nlattr *tb[NL80211_ATTR_MAX + 1]; + struct nlattr *link; + int rem; + + nla_parse(tb, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0), + genlmsg_attrlen(gnlh, 0), NULL); + + if (!tb[NL80211_ATTR_MLO_LINKS]) + return NL_SKIP; + + nla_for_each_nested(link, tb[NL80211_ATTR_MLO_LINKS], rem) + { + struct nlattr *lt[NL80211_ATTR_MAX + 1]; + struct mlo_link *l; + + if (ctx->n >= ctx->max) + break; + + nla_parse_nested(lt, NL80211_ATTR_MAX, link, NULL); + + l = &ctx->out[ctx->n]; + memset(l, 0, sizeof(*l)); + + if (lt[NL80211_ATTR_MLO_LINK_ID] && + nla_len(lt[NL80211_ATTR_MLO_LINK_ID]) >= (int)sizeof(uint8_t)) + l->link_id = (int)nla_get_u8(lt[NL80211_ATTR_MLO_LINK_ID]); + if (lt[NL80211_ATTR_MAC] && + nla_len(lt[NL80211_ATTR_MAC]) >= (int)sizeof(l->addr)) + memcpy(l->addr, nla_data(lt[NL80211_ATTR_MAC]), sizeof(l->addr)); + if (lt[NL80211_ATTR_WIPHY_FREQ] && + nla_len(lt[NL80211_ATTR_WIPHY_FREQ]) >= (int)sizeof(uint32_t)) + l->frequency = nla_get_u32(lt[NL80211_ATTR_WIPHY_FREQ]); + if (lt[NL80211_ATTR_WIPHY_TX_POWER_LEVEL] && + nla_len(lt[NL80211_ATTR_WIPHY_TX_POWER_LEVEL]) >= (int)sizeof(uint32_t)) { + l->has_txpower = true; + /* mBm -> dBm, same conversion libiwinfo applies */ + l->txpower = (int32_t)nla_get_u32( + lt[NL80211_ATTR_WIPHY_TX_POWER_LEVEL]) / 100; + } + + ctx->n++; + } + + return NL_SKIP; +} + +static int mlo_err_cb(struct sockaddr_nl *nla, struct nlmsgerr *err, void *arg) +{ + int *ret = arg; + (void)nla; + *ret = err->error; + return NL_STOP; +} + +static int mlo_finish_cb(struct nl_msg *msg, void *arg) +{ + int *ret = arg; + (void)msg; + *ret = 0; + return NL_SKIP; +} + +static int mlo_ack_cb(struct nl_msg *msg, void *arg) +{ + int *ret = arg; + (void)msg; + *ret = 0; + return NL_STOP; +} + +size_t mlo_get_links(const char *ifname, struct mlo_link *out, size_t max) +{ + struct mlo_cb_ctx ctx = { .out = out, .max = max, .n = 0 }; + struct nl_msg *msg; + struct nl_cb *cb; + unsigned int ifidx; + int err = 1; + + if (!mlo_init()) + return 0; + + ifidx = if_nametoindex(ifname); + if (!ifidx) + return 0; + + msg = nlmsg_alloc(); + if (!msg) + return 0; + + cb = nl_cb_alloc(NL_CB_DEFAULT); + if (!cb) { + nlmsg_free(msg); + return 0; + } + + genlmsg_put(msg, 0, 0, st.family_id, 0, 0, NL80211_CMD_GET_INTERFACE, 0); + + if (nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifidx) < 0) { + nlmsg_free(msg); + nl_cb_put(cb); + return 0; + } + + nl_cb_set(cb, NL_CB_VALID, NL_CB_CUSTOM, mlo_valid_cb, &ctx); + nl_cb_err(cb, NL_CB_CUSTOM, mlo_err_cb, &err); + nl_cb_set(cb, NL_CB_FINISH, NL_CB_CUSTOM, mlo_finish_cb, &err); + nl_cb_set(cb, NL_CB_ACK, NL_CB_CUSTOM, mlo_ack_cb, &err); + + if (nl_send_auto_complete(st.sock, msg) < 0) { + nlmsg_free(msg); + nl_cb_put(cb); + return 0; + } + + while (err > 0) { + if (nl_recvmsgs(st.sock, cb) < 0) + break; + } + + nlmsg_free(msg); + nl_cb_put(cb); + + return ctx.n; +} + +void mlo_cleanup(void) +{ + if (st_valid && st.sock) + nl_socket_free(st.sock); + st_valid = false; +} diff --git a/net/snmpd-openwrt-metrics/src/mlo.h b/net/snmpd-openwrt-metrics/src/mlo.h new file mode 100644 index 0000000000000..eec368dfd3be1 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/mlo.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * mlo.h - direct nl80211 query for Multi-Link Operation per-link data. + */ +#ifndef OPENWRT_SNMP_MLO_H +#define OPENWRT_SNMP_MLO_H + +#include +#include +#include + +#define MLO_MAX_LINKS 4 + +struct mlo_link { + int link_id; + uint8_t addr[6]; + uint32_t frequency; /* MHz, 0 if the kernel didn't report one */ + bool has_txpower; + int32_t txpower; /* dBm */ +}; + +/* Queries NL80211_CMD_GET_INTERFACE for ifname and extracts + * NL80211_ATTR_MLO_LINKS. Returns the number of links found: 0 for a + * non-MLD interface, a query failure, or an interface with no links. */ +size_t mlo_get_links(const char *ifname, struct mlo_link *out, size_t max); + +void mlo_cleanup(void); + +#endif /* OPENWRT_SNMP_MLO_H */