--- /dev/null
+/*#############################################################################
+# #
+# telemetryd - The IPFire Telemetry Collection Service #
+# Copyright (C) 2025 IPFire Development Team #
+# #
+# 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 3 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 <http://www.gnu.org/licenses/>. #
+# #
+#############################################################################*/
+
+#include <errno.h>
+#include <net/ethernet.h>
+#include <netinet/ether.h>
+
+#include "../command.h"
+#include "../ctx.h"
+#include "../file.h"
+#include "../source.h"
+#include "hostapd.h"
+
+typedef struct hostapd_station {
+ td_source* source;
+
+ // MAC Address
+ struct ether_addr address;
+
+ // Packets
+ size_t rx_packets;
+ size_t tx_packets;
+
+ // Bytes
+ size_t rx_bytes;
+ size_t tx_bytes;
+
+ // Signal
+ long signal;
+ long last_ack_signal;
+
+ // RX Rate Info
+ unsigned long rx_rate;
+ unsigned long rx_mcs;
+ unsigned long rx_vhtmcs;
+ unsigned long rx_vhtnss;
+
+ // TX Rate Info
+ unsigned long tx_rate;
+ unsigned long tx_mcs;
+ unsigned long tx_vhtmcs;
+ unsigned long tx_vhtnss;
+
+ // Connected Time
+ unsigned long connected_time;
+
+ // Inactive Time
+ unsigned long inactive_msec;
+
+ // Set if this struct contains some useful data
+ int initialized;
+} hostapd_station;
+
+// Check if a MAC address is all zero
+static int ether_is_zero(const struct ether_addr* address) {
+ for (unsigned int i = 0; i < ETH_ALEN; i++) {
+ if (address->ether_addr_octet[i])
+ return 0;
+ }
+
+ return 1;
+}
+
+static int hostapd_submit_station(td_ctx* ctx, hostapd_station* station) {
+ char address[ETHER_MAX_LEN];
+ char* p = NULL;
+
+ // Skip if we don't have a MAC address
+ if (ether_is_zero(&station->address))
+ return 0;
+
+ // Format the address
+ p = ether_ntoa_r(&station->address, address);
+ if (!p) {
+ DEBUG(ctx, "Failed to format MAC address: %m\n");
+ return -errno;
+ }
+
+ // Submit the station
+ return td_source_submit(station->source, address,
+ "%lu:%lu:%ld:%ld:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu:%lu",
+ station->connected_time, station->inactive_msec,
+ station->signal, station->last_ack_signal,
+ station->rx_packets, station->tx_packets, station->rx_bytes, station->tx_bytes,
+ station->rx_rate * 100, station->rx_mcs, station->rx_vhtmcs, station->rx_vhtnss,
+ station->tx_rate * 100, station->tx_mcs, station->tx_vhtmcs, station->tx_vhtnss);
+}
+
+static int hostapd_parse(td_ctx* ctx, td_file* stdout, unsigned long lineno,
+ char* line, size_t length, void* data) {
+ hostapd_station* station = data;
+ struct ether_addr address = {};
+ struct ether_addr* a = NULL;
+ int r;
+
+ // The format of the output is as follows. Each station's block starts with
+ // the MAC address of the station followed by attributes in key-value format.
+
+ // Try parsing an ethernet address
+ a = ether_aton_r(line, &address);
+ if (a) {
+ // Submit the previous station
+ r = hostapd_submit_station(ctx, station);
+ if (r < 0)
+ return r;
+
+ // Store the new MAC address
+ station->address = address;
+
+ // Mark as initialized
+ station->initialized = 1;
+
+ // Done processing this line
+ return 0;
+ }
+
+ // Parse all fields
+ td_file_parser parser[] = {
+ PARSE1("rx_packets=%lu", &station->rx_packets),
+ PARSE1("tx_packets=%lu", &station->tx_packets),
+ PARSE1("rx_bytes=%lu", &station->rx_bytes),
+ PARSE1("tx_bytes=%lu", &station->tx_bytes),
+
+ // Signal
+ PARSE1("signal=%ld", &station->signal),
+ PARSE1("last_ack_signal=%lu", &station->last_ack_signal),
+
+ // RX Rate Info
+ PARSE3("rx_rate_info=%lu vhtmcs %lu vhtnss %lu",
+ &station->rx_rate, &station->rx_vhtmcs, &station->rx_vhtnss),
+ PARSE2("rx_rate_info=%lu mcs %lu",
+ &station->rx_rate, &station->rx_mcs),
+ PARSE1("rx_rate_info=%lu",
+ &station->rx_rate),
+
+ // TX Rate Info
+ PARSE3("tx_rate_info=%lu vhtmcs %lu vhtnss %lu",
+ &station->tx_rate, &station->tx_vhtmcs, &station->tx_vhtnss),
+ PARSE2("tx_rate_info=%lu mcs %lu",
+ &station->tx_rate, &station->tx_mcs),
+ PARSE1("tx_rate_info=%lu",
+ &station->tx_rate),
+
+ // Connected Time
+ PARSE1("connected_time=%lu", &station->connected_time),
+
+ // Inactive Time
+ PARSE1("inactive_msec=%lu", &station->inactive_msec),
+
+ { NULL },
+ };
+
+ // Try parsing the line
+ return td_file_parse_line(stdout, parser, line, length);
+}
+
+static int hostapd_on_success(td_ctx* ctx,
+ int rc, td_file* stdout, void* data) {
+ td_source* source = data;
+ int r;
+
+ // Gather station information
+ hostapd_station station = {
+ .source = source,
+ };
+
+ // Parse the output
+ r = td_file_walk(stdout, hostapd_parse, &station);
+ if (r < 0)
+ return r;
+
+ // Submit the last station
+ return hostapd_submit_station(ctx, &station);
+}
+
+static int hostapd_heartbeat(td_ctx* ctx, td_source* source) {
+ td_command* command = NULL;
+ int r;
+
+ // Run hostapd_cli to fetch station information
+ const char* argv[] = { "hostapd_cli", "all_sta", NULL };
+
+ // Create a new command
+ r = td_source_create_command(source, &command);
+ if (r < 0)
+ goto ERROR;
+
+ // Register the success callback
+ td_command_on_success(command, hostapd_on_success, source);
+
+ // Execute the command
+ r = td_command_execute(command, argv);
+
+ERROR:
+ if (command)
+ td_command_unref(command);
+
+ return r;
+}
+
+const td_source_impl hostapd_source = {
+ .name = "hostapd",
+
+ // RRD Data Sources
+ .rrd_dss = {
+ { "connected_time", "COUNTER", 0, -1, },
+ { "inactive_msec", "GAUGE", 0, -1, },
+
+ // Signal
+ { "signal", "GAUGE", -1, 0, },
+ { "last_ack_signal", "GAUGE", -1, 0, },
+
+ // Packets
+ { "rx_packets", "DERIVE", 0, -1, },
+ { "tx_packets", "DERIVE", 0, -1, },
+
+ // Bytes
+ { "rx_bytes", "DERIVE", 0, -1, },
+ { "tx_bytes", "DERIVE", 0, -1, },
+
+ // RX Rate Info
+ { "rx_rate", "GAUGE", 0, -1, },
+ { "rx_mcs", "GAUGE", 0, -1, },
+ { "rx_vhtmcs", "GAUGE", 0, -1, },
+ { "rx_vhtnss", "GAUGE", 0, -1, },
+
+ // TX Rate Info
+ { "tx_rate", "GAUGE", 0, -1, },
+ { "tx_mcs", "GAUGE", 0, -1, },
+ { "tx_vhtmcs", "GAUGE", 0, -1, },
+ { "tx_vhtnss", "GAUGE", 0, -1, },
+
+ { NULL },
+ },
+
+ // Methods
+ .heartbeat = hostapd_heartbeat,
+};