/* * 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 #include #include #include #include #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 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; }