diff --git a/net/snmpd-openwrt-metrics/Makefile b/net/snmpd-openwrt-metrics/Makefile new file mode 100644 index 0000000000000..d0d0b89e09d99 --- /dev/null +++ b/net/snmpd-openwrt-metrics/Makefile @@ -0,0 +1,68 @@ +# +# 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 \ + -DAGENT_VERSION=\"$(PKG_VERSION)\" + +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..834024013aacb --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/agent.c @@ -0,0 +1,522 @@ +// 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" + +#ifndef AGENT_VERSION +#error "AGENT_VERSION must be provided by the Makefile" +#endif + +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("%s " AGENT_VERSION "\n", argv[0]); + 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..222b25280291a --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/collect.c @@ -0,0 +1,804 @@ +// 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..fe25c7c265539 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/collect.h @@ -0,0 +1,114 @@ +// 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..13a0ada3b01b6 --- /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..3829c348b20d7 --- /dev/null +++ b/net/snmpd-openwrt-metrics/src/mlo.h @@ -0,0 +1,31 @@ +// 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 */