Files
OpenUniFi/src/clients.c
T
2026-07-14 22:08:41 +00:00

297 lines
13 KiB
C

/*
* openuf - clients.c
*
*
* Enumerates clients for the inform payload → sta_table.
*
* ── Parsing `iw dev station dump` Output ────────────────────────────
*
* The output is organized into one block per client:
*
*
* Station aa:bb:cc:dd:ee:ff (on wlan0)
* inactive time: 120 ms
* rx bytes: 2000000
* rx packets: 2000
* tx bytes: 5000000
* tx packets: 5000
* signal: -62 [-62, -65] dBm
* tx bitrate: 144.4 MBit/s MCS 15
* rx bitrate: 108.0 MBit/s
* connected time: 1800 seconds
*
* We detect the start of each client by looking for "Station XX:XX:..."
* and populate its fields until the next client entry is encountered.
*
* ── ARP: /proc/net/arp ─────────────────────────────────────────────
*
* IP HW type Flags HW addr Mask Device
* 192.168.1.x 0x1 0x2 aa:bb:cc:dd:ee:ff * br-lan
*
* Flags 0x2 = complete entry (reachable).
* Flags 0x0 = incomplete (no ARP responce), ignore.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <json-c/json.h>
#include "clients.h"
/* ─── Convert MAC address to lowercase ─────────────────────────────────── */
static void mac_lower(const char *src, char *dst, size_t sz)
{
for (size_t i = 0; src[i] && i < sz-1; i++)
dst[i] = tolower((unsigned char)src[i]);
dst[strlen(src) < sz ? strlen(src) : sz-1] = '\0';
}
/* ═══════════════════════════════════════════════════════════════════
/proc/net/arp — MAC → IP
═══════════════════════════════════════════════════════════════════ */
int clients_mac_to_ip(const char *mac, char *ip_out, size_t sz)
{
ip_out[0] = '\0';
FILE *f = fopen("/proc/net/arp", "r");
if (!f) return -1;
char line[256];
fgets(line, sizeof(line), f); /* skip header */
char ml[32] = {0};
mac_lower(mac, ml, sizeof(ml));
while (fgets(line, sizeof(line), f)) {
char ip[64], hw_type[16], flags[16], hw[32], mask[16], dev[32];
if (sscanf(line, "%63s %15s %15s %31s %15s %31s",
ip, hw_type, flags, hw, mask, dev) != 6) continue;
if (strcmp(flags, "0x2") != 0) continue;
char hl[32] = {0};
mac_lower(hw, hl, sizeof(hl));
if (strcmp(ml, hl) == 0) {
strncpy(ip_out, ip, sz-1);
fclose(f); return 0;
}
}
fclose(f);
return -1;
}
/* ═══════════════════════════════════════════════════════════════════
/tmp/dhcp.leases — MAC → hostname
═══════════════════════════════════════════════════════════════════ */
int clients_mac_to_hostname(const char *mac, char *out, size_t sz)
{
out[0] = '\0';
static const char *files[] = {
"/tmp/dhcp.leases",
"/var/lib/misc/dnsmasq.leases",
NULL
};
char ml[32] = {0};
mac_lower(mac, ml, sizeof(ml));
for (int fi = 0; files[fi]; fi++) {
FILE *f = fopen(files[fi], "r");
if (!f) continue;
char line[256];
while (fgets(line, sizeof(line), f)) {
long ts;
char lm[32], lip[64], lh[64], lcid[64];
if (sscanf(line, "%ld %31s %63s %63s %63s",
&ts, lm, lip, lh, lcid) < 4) continue;
char ll[32] = {0};
mac_lower(lm, ll, sizeof(ll));
if (strcmp(ml, ll) == 0 && strcmp(lh, "*") != 0) {
strncpy(out, lh, sz-1);
fclose(f); return 0;
}
}
fclose(f);
}
return -1;
}
/* ─── Parse bitrate "144.4 MBit/s ..." → kbps ───────────── */
static long parse_rate_kbps(const char *s)
{
float r = 0;
sscanf(s, "%f MBit/s", &r);
return (long)(r * 1000.0f);
}
/* ═══════════════════════════════════════════════════════════════════
iw dev <iface> station dump → array sta_info_t
═══════════════════════════════════════════════════════════════════ */
int clients_read_wifi(const char *wlan_iface,
const char *radio_band,
int channel,
sta_info_t *out,
int max_out)
{
char cmd[128];
snprintf(cmd, sizeof(cmd),
"iw dev %s station dump 2>/dev/null", wlan_iface);
FILE *p = popen(cmd, "r");
if (!p) return 0;
int count = 0;
sta_info_t *cur = NULL;
char line[256];
while (fgets(line, sizeof(line), p)) {
line[strcspn(line, "\r\n")] = '\0';
/* ── New station ──────────────────────────────────────── */
char mac[32], on_iface[32];
if (sscanf(line, "Station %31s (on %31[^)])", mac, on_iface) == 2) {
if (count >= max_out) break;
cur = &out[count++];
memset(cur, 0, sizeof(*cur));
strncpy(cur->mac, mac, sizeof(cur->mac)-1);
strncpy(cur->vap_name, wlan_iface, sizeof(cur->vap_name)-1);
strncpy(cur->radio, radio_band, sizeof(cur->radio)-1);
cur->channel = channel;
cur->noise = -95;
continue;
}
if (!cur) continue;
/* ── Counters ──────────────────────────────────────────── */
long long llv;
if (sscanf(line, " rx bytes: %lld", &llv) == 1) { cur->rx_bytes = llv; continue; }
if (sscanf(line, " tx bytes: %lld", &llv) == 1) { cur->tx_bytes = llv; continue; }
if (sscanf(line, " rx packets: %lld", &llv) == 1) { cur->rx_packets = llv; continue; }
if (sscanf(line, " tx packets: %lld", &llv) == 1) { cur->tx_packets = llv; continue; }
if (sscanf(line, " tx retries: %lld", &llv) == 1) { cur->tx_retries = llv; continue; }
if (sscanf(line, " tx failed: %lld", &llv) == 1) { cur->tx_failed = llv; continue; }
if (sscanf(line, " rx drop misc: %lld", &llv) == 1) { cur->rx_dropped = llv; continue; }
if (sscanf(line, " tx duration: %lld us", &llv) == 1) { cur->tx_duration = llv; continue; }
if (sscanf(line, " rx duration: %lld us", &llv) == 1) { cur->rx_duration = llv; continue; }
/* ── Signal ───────────────────────────────────────────────── */
int sig;
if (sscanf(line, " signal: %d", &sig) == 1) { cur->signal = sig; continue; }
/* ── Bitrate ─────────────────────────────────────────────── */
char rest[128];
if (sscanf(line, " tx bitrate: %127[^\n]", rest) == 1) {
cur->tx_rate = parse_rate_kbps(rest); continue;
}
if (sscanf(line, " rx bitrate: %127[^\n]", rest) == 1) {
cur->rx_rate = parse_rate_kbps(rest); continue;
}
/* ── Connection time ────────────────────────────────────── */
int upt;
if (sscanf(line, " connected time: %d seconds", &upt) == 1) {
cur->uptime = upt; continue;
}
}
pclose(p);
/* ── Enrich with IP address, hostname, RSSI, and CCQ.─────────────────────── */
for (int i = 0; i < count; i++) {
sta_info_t *s = &out[i];
clients_mac_to_ip(s->mac, s->ip, sizeof(s->ip));
clients_mac_to_hostname(s->mac, s->hostname, sizeof(s->hostname));
if (!s->hostname[0])
strncpy(s->hostname, s->mac, sizeof(s->hostname)-1);
/* Estimated SNR (signal - noise). */
s->rssi = s->signal - s->noise;
if (s->rssi < 0) s->rssi = 0;
/* CCQ: 0-1000 quality metric
* -50 dBm → 1000 (excellent)
* -90 dBm → 0 (very poor)
* Linear mapping: (signal + 90) * 25, clamped to the range 0-1000.
*/
int ccq = (s->signal + 90) * 25;
s->ccq = (ccq < 0) ? 0 : (ccq > 1000) ? 1000 : ccq;
}
return count;
}
/* ═══════════════════════════════════════════════════════════════════
Build the sta_table JSON array for a VAP.
═══════════════════════════════════════════════════════════════════
The resulting JSON array is embedded in vap_table[i].sta_table
within the inform payload. Example input:
{
"mac": "aa:bb:cc:dd:ee:ff",
"ip": "192.168.1.100",
"hostname": "mi-movil",
"signal": -62,
"rssi": 33,
"noise": -95,
"tx_rate": 144000,
"rx_rate": 108000,
"tx_bytes": 5000000,
"rx_bytes": 2000000,
"tx_packets": 5000,
"rx_packets": 2000,
"uptime": 1800,
"radio": "ng",
"channel": 6,
"vap_name": "ath0",
"is_11r": false,
"ccq": 700
}
*/
struct json_object *clients_build_sta_table(const char *wlan_iface,
const char *radio_band,
int channel,
const char *vap_name,
int vlan_id,
int is_11r)
{
sta_info_t stas[MAX_STA];
int n = clients_read_wifi(wlan_iface, radio_band, channel,
stas, MAX_STA);
struct json_object *arr = json_object_new_array();
for (int i = 0; i < n; i++) {
sta_info_t *s = &stas[i];
struct json_object *o = json_object_new_object();
json_object_object_add(o, "mac", json_object_new_string(s->mac));
json_object_object_add(o, "ip", json_object_new_string(s->ip));
json_object_object_add(o, "hostname", json_object_new_string(s->hostname));
json_object_object_add(o, "signal", json_object_new_int(s->signal));
json_object_object_add(o, "rssi", json_object_new_int(s->rssi));
json_object_object_add(o, "noise", json_object_new_int(s->noise));
json_object_object_add(o, "tx_rate", json_object_new_int64(s->tx_rate));
json_object_object_add(o, "rx_rate", json_object_new_int64(s->rx_rate));
json_object_object_add(o, "tx_bytes", json_object_new_int64(s->tx_bytes));
json_object_object_add(o, "rx_bytes", json_object_new_int64(s->rx_bytes));
json_object_object_add(o, "tx_packets", json_object_new_int64(s->tx_packets));
json_object_object_add(o, "rx_packets", json_object_new_int64(s->rx_packets));
json_object_object_add(o, "tx_retries", json_object_new_int64(s->tx_retries));
json_object_object_add(o, "tx_failed", json_object_new_int64(s->tx_failed));
json_object_object_add(o, "tx_dropped", json_object_new_int64(s->tx_failed));
json_object_object_add(o, "rx_dropped", json_object_new_int64(s->rx_dropped));
json_object_object_add(o, "tx_duration", json_object_new_int64(s->tx_duration));
json_object_object_add(o, "rx_duration", json_object_new_int64(s->rx_duration));
json_object_object_add(o, "uptime", json_object_new_int(s->uptime));
json_object_object_add(o, "radio", json_object_new_string(s->radio));
json_object_object_add(o, "channel", json_object_new_int(s->channel));
json_object_object_add(o, "vap_name", json_object_new_string(
vap_name ? vap_name : wlan_iface));
json_object_object_add(o, "is_11r", json_object_new_boolean(is_11r));
if (vlan_id > 0) {
json_object_object_add(o, "vlan", json_object_new_int(vlan_id));
json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id));
}
json_object_object_add(o, "is_wired", json_object_new_boolean(false));
json_object_object_add(o, "ccq", json_object_new_int(s->ccq));
json_object_object_add(o, "idletime", json_object_new_int(0));
json_object_array_add(arr, o);
}
return arr;
}