Files
OpenUniFi/src/wlan.c
T
Koda 696d4ac215 fix band steering (#12)
added git autocompletion to devcontainer

Reviewed-on: #12
2026-07-12 17:52:48 +01:00

1552 lines
61 KiB
C

/*
* openuf - wlan.c
*
* Traduce la configuración WiFi del controlador UniFi en settings
* UCI de OpenWrt usando libuci directamente (sin shell).
*
* ── CÓMO SE APLICA LA CONFIGURACIÓN ────────────────────────────────
*
* El controlador envía "setstate" con:
* radio_table[] → configuración de las radios (canal, potencia, HT)
* vap_table[] → configuración de las redes WiFi (SSID, clave, roaming...)
*
* Este módulo:
* 1. Borra todas las wifi-iface UCI con prefijo "openuf_"
* 2. Aplica radio_table → wireless.<device>.channel/txpower/htmode
* 3. Crea nuevas wifi-iface por cada VAP con su configuración
* 4. Ejecuta "wifi reload" para aplicar sin reiniciar
*
* ── MAPEO DE SEGURIDAD ──────────────────────────────────────────────
*
* UniFi OpenWrt UCI Descripción
* ─────────────────────────────────────────────
* open none Sin contraseña
* wpapsk psk WPA Personal
* wpa2psk psk2 WPA2 Personal
* wpapskwpa2psk psk-mixed WPA/WPA2 mixto
* wpa3 sae WPA3 Personal
* wpa3transition sae-mixed WPA2+WPA3 transición
* wpa2enterprise wpa2 WPA2 Enterprise (RADIUS)
* wpa3enterprise wpa3 WPA3 Enterprise
*
* ── BAND STEERING (802.11k/v) ──────────────────────────────────────
*
* Cuando UniFi activa band_steering, configuramos en UCI:
* ieee80211k = 1 → Neighbor Reports (AP informa a cliente de otros APs)
* ieee80211v = 1 → BSS Transition Management (AP puede pedir que el
* cliente se mueva a otro AP/radio)
* rrm_neighbor_report = 1
* bss_transition = 1
*
* El hostapd de OpenWrt usa estos flags para implementar 802.11k/v.
* Band steering real requiere lógica adicional (daemon externo o
* script que monitoriza RSSI y envía BTM Request).
*
* ── FAST ROAMING (802.11r) ─────────────────────────────────────────
*
* Cuando UniFi activa fast_roaming_enabled:
* ieee80211r = 1 → FT (Fast BSS Transition)
* ft_over_ds = 1 → FT sobre Distribution System (más compatible)
* mobility_domain = XXXX → Mismo dominio en todos los APs del site
* ft_psk_generate_local = 1 → PSK sin servidor FT externo
*
* El mobility_domain se deriva de los primeros 2 bytes del MAC del AP.
* Todos los APs del mismo site deben usar el mismo mobility_domain.
*
* ── PMF (Protected Management Frames / 802.11w) ─────────────────────
*
* pmf_mode → ieee80211w:
* "disabled" → 0 (sin PMF)
* "optional" → 1 (PMF opcional, compatible con clientes sin PMF)
* "required" → 2 (PMF obligatorio, solo clientes con PMF)
*
* WPA3 siempre requiere PMF=2.
*
* ── LECTURA DE VAPs DESDE UCI ───────────────────────────────────────
*
* wlan_get_vap_table() itera todas las wifi-iface de /etc/config/wireless
* que tengan prefijo "openuf_" y construye el JSON vap_table para
* incluirlo en el payload inform.
*
* Para cada VAP leemos: ssid, device, bssid, encryption, key, disabled
* y los traducimos al formato que espera el controlador.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
#define MAX_RESOLVED_RADIOS 8
typedef struct {
char device[32];
unsigned int bands;
} radio_capability_t;
static const uf_model_t *resolved_model;
static char resolved_devices[MAX_RESOLVED_RADIOS][32];
static unsigned int band_bit(const char *band)
{
if (!band) return 0;
if (!strcmp(band, "ng") || !strcmp(band, "2g")) return 1u;
if (!strcmp(band, "na") || !strcmp(band, "5g")) return 2u;
if (!strcmp(band, "6g") || !strcmp(band, "6GHz")) return 4u;
return 0;
}
static int bit_count(unsigned int value)
{
int count = 0;
while (value) {
count += value & 1u;
value >>= 1;
}
return count;
}
/* OpenWrt's generated radioN and phyN indices correspond for mac80211
* devices. Read actual frequencies instead of assuming PHY band order. */
static unsigned int detect_radio_bands(const char *device)
{
int phy_index = -1;
char command[96];
char line[256];
unsigned int bands = 0;
if (!device || sscanf(device, "radio%d", &phy_index) != 1 || phy_index < 0)
return 0;
snprintf(command, sizeof(command), "iw phy phy%d info 2>/dev/null", phy_index);
FILE *pipe = popen(command, "r");
if (!pipe) return 0;
while (fgets(line, sizeof(line), pipe)) {
char *mhz = strstr(line, " MHz [");
if (!mhz || strstr(line, "(disabled)"))
continue;
char *start = mhz;
while (start > line &&
((start[-1] >= '0' && start[-1] <= '9') || start[-1] == '.'))
start--;
double frequency = strtod(start, NULL);
if (frequency >= 2300.0 && frequency < 3000.0)
bands |= 1u;
else if (frequency >= 4900.0 && frequency < 5925.0)
bands |= 2u;
else if (frequency >= 5925.0 && frequency < 7200.0)
bands |= 4u;
}
pclose(pipe);
return bands;
}
static void resolve_radio_map(const uf_model_t *model)
{
if (!model || resolved_model == model)
return;
memset(resolved_devices, 0, sizeof(resolved_devices));
resolved_model = model;
int count = model->radio_map_len;
if (count > MAX_RESOLVED_RADIOS)
count = MAX_RESOLVED_RADIOS;
radio_capability_t caps[MAX_RESOLVED_RADIOS] = {0};
int used[MAX_RESOLVED_RADIOS] = {0};
for (int i = 0; i < count; i++) {
snprintf(caps[i].device, sizeof(caps[i].device), "%s",
model->radio_map[i].device);
caps[i].bands = detect_radio_bands(caps[i].device);
}
for (int i = 0; i < count; i++) {
unsigned int wanted = band_bit(model->radio_map[i].band);
int best = -1;
int best_band_count = 99;
for (int j = 0; j < count; j++) {
if (used[j] || !(caps[j].bands & wanted))
continue;
int supported = bit_count(caps[j].bands);
if (supported < best_band_count) {
best = j;
best_band_count = supported;
}
}
if (best >= 0) {
used[best] = 1;
snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s",
caps[best].device);
} else {
snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s",
model->radio_map[i].device);
}
printf("[openuf] Radio mapping: %s -> %s%s\n",
model->radio_map[i].band, resolved_devices[i],
best >= 0 ? " (detected)" : " (model fallback)");
}
}
const char *wlan_device_for_band(const uf_model_t *model, const char *band)
{
if (!model || !band) return NULL;
resolve_radio_map(model);
for (int i = 0; i < model->radio_map_len; i++)
if (!strcmp(model->radio_map[i].band, band))
return i < MAX_RESOLVED_RADIOS && resolved_devices[i][0]
? resolved_devices[i] : model->radio_map[i].device;
return NULL;
}
const char *wlan_band_for_device(const uf_model_t *model, const char *device)
{
if (!model || !device) return NULL;
resolve_radio_map(model);
for (int i = 0; i < model->radio_map_len; i++) {
const char *mapped = i < MAX_RESOLVED_RADIOS && resolved_devices[i][0]
? resolved_devices[i]
: model->radio_map[i].device;
if (!strcmp(mapped, device))
return model->radio_map[i].band;
}
return NULL;
}
/* ─── Mapeo de seguridad UniFi → OpenWrt UCI ────────────────────── */
static const char *sec_to_uci(const char *uf)
{
if (!uf || !strcmp(uf,"open")) return "none";
if (!strcmp(uf,"wpapsk")) return "psk";
if (!strcmp(uf,"wpa2psk")) return "psk2";
if (!strcmp(uf,"wpapskwpa2psk")) return "psk-mixed";
if (!strcmp(uf,"wpa3")) return "sae";
if (!strcmp(uf,"wpa3transition")) return "sae-mixed";
if (!strcmp(uf,"wpa2enterprise")) return "wpa2";
if (!strcmp(uf,"wpa3enterprise")) return "wpa3";
return "psk2"; /* default */
}
/* Mapeo inverso: UCI → UniFi (para wlan_get_vap_table) */
static const char *sec_to_unifi(const char *uci)
{
if (!uci || !strcmp(uci,"none")) return "open";
if (!strcmp(uci,"psk")) return "wpapsk";
if (!strcmp(uci,"psk2")) return "wpa2psk";
if (!strcmp(uci,"psk-mixed")) return "wpapskwpa2psk";
if (!strcmp(uci,"sae")) return "wpa3";
if (!strcmp(uci,"sae-mixed")) return "wpa3transition";
if (!strcmp(uci,"wpa2")) return "wpa2enterprise";
if (!strcmp(uci,"wpa3")) return "wpa3enterprise";
return "wpa2psk";
}
/* Return true only for the 24-character hexadecimal IDs used by UniFi. */
static int valid_object_id(const char *id)
{
if (!id || strlen(id) != 24)
return 0;
for (size_t i = 0; i < 24; i++)
if (!((id[i] >= '0' && id[i] <= '9') ||
(id[i] >= 'a' && id[i] <= 'f') ||
(id[i] >= 'A' && id[i] <= 'F')))
return 0;
return 1;
}
/* Build one stable 802.11r mobility domain shared by every AP for an SSID. */
static void mobility_domain_for_ssid(const char *ssid, char out[5])
{
unsigned int hash = 2166136261u;
const unsigned char *p = (const unsigned char *)(ssid ? ssid : "");
while (*p) {
hash ^= *p++;
hash *= 16777619u;
}
snprintf(out, 5, "%04x", (hash ^ (hash >> 16)) & 0xffffu);
}
/* Return true when an OpenWrt radio is backed by the ath9k kernel driver. */
static int radio_uses_ath9k(const char *device_name)
{
int phy_index;
if (!device_name || sscanf(device_name, "radio%d", &phy_index) != 1)
return 0;
char path[128];
char target[256];
snprintf(path, sizeof(path),
"/sys/class/ieee80211/phy%d/device/driver", phy_index);
ssize_t length = readlink(path, target, sizeof(target) - 1);
if (length < 0)
return 0;
target[length] = '\0';
return strstr(target, "ath9k") != NULL;
}
/* Read a UniFi boolean while accepting names used by controller versions. */
static int json_boolean_any(struct json_object *object,
const char *const *keys, size_t key_count)
{
struct json_object *value;
for (size_t i = 0; i < key_count; i++) {
if (!json_object_object_get_ex(object, keys[i], &value))
continue;
if (json_object_is_type(value, json_type_string)) {
const char *text = json_object_get_string(value);
if (!text || !text[0] || !strcasecmp(text, "disabled") ||
!strcasecmp(text, "false") || !strcasecmp(text, "off") ||
!strcasecmp(text, "none") || !strcmp(text, "0"))
return 0;
/* Also accepts controller modes such as "prefer_5g". */
return 1;
}
return json_object_get_boolean(value) ? 1 : 0;
}
return 0;
}
/* Interpret Boolean text and UniFi feature modes such as "prefer_5g". */
static int feature_text_enabled(const char *text)
{
return text && text[0] && strcasecmp(text, "disabled") &&
strcasecmp(text, "false") && strcasecmp(text, "off") &&
strcasecmp(text, "none") && strcmp(text, "0");
}
/* Safe UCI identifier fragment (maximum 15 characters). */
static void safe_section_name(const char *ssid, char *out, size_t sz)
{
size_t j = 0;
for (size_t i = 0; ssid[i] && j < sz-1 && j < 15; i++) {
char c = ssid[i];
if ((c>='a'&&c<='z')||(c>='A'&&c<='Z')||
(c>='0'&&c<='9')||c=='_')
out[j++] = c;
else
out[j++] = '_';
}
out[j] = '\0';
}
/* ─── libuci: set un valor en wireless ─────────────────────────── */
static int uci_set_val(struct uci_context *ctx,
const char *path, const char *val)
{
struct uci_ptr ptr;
char *p = malloc(strlen(path) + strlen(val) + 2);
if (!p) return -1;
sprintf(p, "%s=%s", path, val);
int ret = uci_lookup_ptr(ctx, &ptr, p, true);
if (ret == UCI_OK)
ret = uci_set(ctx, &ptr);
/* ptr.value may point inside p, so free it only after uci_set(). */
free(p);
return ret == UCI_OK ? 0 : -1;
}
/* Set and verify an option whose absence would make a VAP unusable. */
static int uci_set_required(struct uci_context *ctx,
struct uci_package *pkg,
const char *section_name,
const char *option_name,
const char *value)
{
char path[256];
snprintf(path, sizeof(path), "%s.%s.%s",
pkg->e.name, section_name, option_name);
if (uci_set_val(ctx, path, value) != 0)
return -1;
struct uci_section *section =
uci_lookup_section(ctx, pkg, section_name);
const char *stored = section ?
uci_lookup_option_string(ctx, section, option_name) : NULL;
return stored && !strcmp(stored, value) ? 0 : -1;
}
/* Add one value to a UCI list option. */
static int uci_add_list_val(struct uci_context *ctx,
const char *path, const char *val)
{
struct uci_ptr ptr;
char *assignment = malloc(strlen(path) + strlen(val) + 2);
if (!assignment) return -1;
sprintf(assignment, "%s=%s", path, val);
int ret = uci_lookup_ptr(ctx, &ptr, assignment, true);
if (ret == UCI_OK)
ret = uci_add_list(ctx, &ptr);
/* ptr.value may point inside assignment. */
free(assignment);
return ret == UCI_OK ? 0 : -1;
}
/* Wrapper que formatea path y value en printf style */
#define UCI_SET(ctx, pkg, sec, opt, val) do { \
char _path[256]; \
snprintf(_path, sizeof(_path), "%s.%s.%s", pkg, sec, opt); \
uci_set_val(ctx, _path, val); \
} while(0)
#define UCI_SET_INT(ctx, pkg, sec, opt, ival) do { \
char _v[32]; snprintf(_v, sizeof(_v), "%d", ival); \
UCI_SET(ctx, pkg, sec, opt, _v); \
} while(0)
/* ─── Encontrar/crear sección UCI ──────────────────────────────── */
static int uci_ensure_section(struct uci_context *ctx,
struct uci_package *pkg,
const char *sec_name,
const char *sec_type)
{
struct uci_element *e;
uci_foreach_element(&pkg->sections, e) {
struct uci_section *s = uci_to_section(e);
if (!strcmp(s->e.name, sec_name) && !strcmp(s->type, sec_type))
return 0; /* ya existe */
}
/* Create a named section: wireless.<name>=<type>. */
char *p = malloc(strlen(pkg->e.name) + strlen(sec_name) +
strlen(sec_type) + 3);
if (!p) return -1;
sprintf(p, "%s.%s=%s", pkg->e.name, sec_name, sec_type);
struct uci_ptr ptr;
int ret = uci_lookup_ptr(ctx, &ptr, p, true);
if (ret == UCI_OK)
ret = uci_set(ctx, &ptr);
free(p);
return ret == UCI_OK ? 0 : -1;
}
/*
* Resolve the physical port below network.lan's bridge. VLAN tagging must
* happen on that port (for example eth0.11), not above the management bridge.
*/
static void find_vlan_uplink(struct uci_context *ctx,
struct uci_package *pkg,
char *out, size_t out_size)
{
const char *lan_device = "br-lan";
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
if (!strcmp(section->type, "interface") &&
!strcmp(section->e.name, "lan")) {
const char *device = uci_lookup_option_string(ctx, section,
"device");
if (device && device[0]) lan_device = device;
break;
}
}
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
const char *name;
struct uci_option *ports;
if (strcmp(section->type, "device")) continue;
name = uci_lookup_option_string(ctx, section, "name");
if (!name || strcmp(name, lan_device)) continue;
ports = uci_lookup_option(ctx, section, "ports");
if (!ports) break;
if (ports->type == UCI_TYPE_STRING) {
snprintf(out, out_size, "%s", ports->v.string);
return;
}
if (ports->type == UCI_TYPE_LIST && !uci_list_empty(&ports->v.list)) {
struct uci_element *port =
list_to_element(ports->v.list.next);
snprintf(out, out_size, "%s", port->name);
return;
}
break;
}
snprintf(out, out_size, "eth0");
}
static int ensure_vlan_network(int vid)
{
struct uci_context *ctx = uci_alloc_context();
if (!ctx) return -1;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "network", &pkg) != UCI_OK) {
uci_free_context(ctx);
return -1;
}
char vlan_section[48], bridge_section[48], interface_section[32];
char vlan_uplink[32], vlan_device[32], bridge_device[32], vid_string[16];
find_vlan_uplink(ctx, pkg, vlan_uplink, sizeof(vlan_uplink));
snprintf(vlan_section, sizeof(vlan_section),
"openuf_vlan%d", vid);
snprintf(bridge_section, sizeof(bridge_section), "openuf_br%d", vid);
snprintf(interface_section, sizeof(interface_section),
"vlan%d", vid);
snprintf(vlan_device, sizeof(vlan_device), "%s.%d", vlan_uplink, vid);
snprintf(bridge_device, sizeof(bridge_device), "br-openuf-%d", vid);
snprintf(vid_string, sizeof(vid_string), "%d", vid);
int ok = uci_ensure_section(ctx, pkg, vlan_section, "device") == 0 &&
uci_ensure_section(ctx, pkg, bridge_section, "device") == 0 &&
uci_ensure_section(ctx, pkg, interface_section, "interface") == 0;
if (ok) {
UCI_SET(ctx, "network", vlan_section, "type", "8021q");
UCI_SET(ctx, "network", vlan_section, "ifname", vlan_uplink);
UCI_SET(ctx, "network", vlan_section, "vid", vid_string);
UCI_SET(ctx, "network", vlan_section, "name", vlan_device);
/* A VAP needs a bridge containing the tagged wired device. */
UCI_SET(ctx, "network", bridge_section, "type", "bridge");
UCI_SET(ctx, "network", bridge_section, "name", bridge_device);
char ports_path[128];
snprintf(ports_path, sizeof(ports_path), "network.%s.ports",
bridge_section);
/* Replace the list so repeated provisioning never duplicates ports. */
struct uci_ptr ports_ptr;
char ports_lookup[128];
snprintf(ports_lookup, sizeof(ports_lookup), "%s", ports_path);
if (uci_lookup_ptr(ctx, &ports_ptr, ports_lookup, true) == UCI_OK &&
ports_ptr.o)
uci_delete(ctx, &ports_ptr);
ok = uci_add_list_val(ctx, ports_path, vlan_device) == 0;
UCI_SET(ctx, "network", interface_section, "proto", "none");
UCI_SET(ctx, "network", interface_section, "device", bridge_device);
ok = ok && uci_commit(ctx, &pkg, false) == UCI_OK;
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
if (ok)
printf("[openuf] Configured VLAN %d on uplink %s as network '%s'\n",
vid, vlan_uplink, interface_section);
return ok ? 0 : -1;
}
/*
* Configure OpenWrt's steering policy engine. The hostapd 802.11k/v flags
* only expose measurements and transition commands; they do not decide when
* a station should move. usteer supplies that missing policy loop.
*/
static int configure_band_steering(int enabled)
{
struct uci_context *ctx = uci_alloc_context();
if (!ctx)
return -1;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "usteer", &pkg) != UCI_OK) {
printf("[openuf] Cannot load /etc/config/usteer\n");
uci_free_context(ctx);
return -1;
}
struct uci_section *settings = NULL;
struct uci_element *element;
uci_foreach_element(&pkg->sections, element) {
struct uci_section *section = uci_to_section(element);
if (!strcmp(section->type, "usteer")) {
settings = section;
break;
}
}
if (!settings) {
if (uci_ensure_section(ctx, pkg, "openuf", "usteer") != 0) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
settings = uci_lookup_section(ctx, pkg, "openuf");
}
if (!settings) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
/*
* A zero interval disables higher-band steering. A zero station-count
* threshold is important for small networks: usteer's default of five
* otherwise prevents a lone client from being considered. The signal
* floor avoids pushing a client onto 5 GHz when that link is too weak.
*/
UCI_SET(ctx, "usteer", settings->e.name, "band_steering_interval",
enabled ? "30000" : "0");
UCI_SET(ctx, "usteer", settings->e.name, "band_steering_threshold", "0");
UCI_SET(ctx, "usteer", settings->e.name, "band_steering_min_snr", "-65");
int ok = uci_commit(ctx, &pkg, false) == UCI_OK;
uci_unload(ctx, pkg);
uci_free_context(ctx);
printf("[openuf] Band steering policy %s (usteer)\n",
enabled ? "enabled" : "disabled");
return ok ? 0 : -1;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_clear — remove all VAPs before applying controller ownership
═══════════════════════════════════════════════════════════════════ */
void wlan_clear(void)
{
struct uci_context *ctx = uci_alloc_context();
if (!ctx) return;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
uci_free_context(ctx);
return;
}
/* Collect sections to remove (do not modify during iteration) */
char *to_del[64];
int ndel = 0;
struct uci_element *e;
uci_foreach_element(&pkg->sections, e) {
struct uci_section *s = uci_to_section(e);
if (!strcmp(s->type, "wifi-iface") && ndel < 64) {
to_del[ndel++] = strdup(s->e.name);
}
}
for (int i = 0; i < ndel; i++) {
struct uci_ptr ptr;
char path[128];
snprintf(path, sizeof(path), "wireless.%s", to_del[i]);
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_delete(ctx, &ptr);
free(to_del[i]);
}
if (ndel > 0) {
uci_commit(ctx, &pkg, false);
printf("[openuf] wlan_clear: removed %d existing VAPs\n", ndel);
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_radio — apply radio config (channel, HT, power)
═══════════════════════════════════════════════════════════════════
Reading parameters from the controller's JSON:
channel → wireless.<device>.channel
ht → wireless.<device>.htmode ("HT20" / "HT40" / "HT80" / "HE80")
tx_power → wireless.<device>.txpower
min_rssi → not mapped to UCI (requires an external daemon)
*/
void wlan_apply_radio(struct json_object *radio_json,
const char *device_name)
{
struct uci_context *ctx = uci_alloc_context();
if (!ctx) return;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
uci_free_context(ctx); return;
}
struct json_object *v;
char path[256];
/* Map UniFi band names to OpenWrt mac80211 band names. */
if (json_object_object_get_ex(radio_json, "radio", &v)) {
const char *radio = json_object_get_string(v);
const char *band = !strcmp(radio, "ng") ? "2g" :
!strcmp(radio, "na") ? "5g" :
!strcmp(radio, "6g") ? "6g" : NULL;
if (band) {
snprintf(path, sizeof(path), "wireless.%s.band=%s",
device_name, band);
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
}
}
#define RP(key, uci_opt) \
if (json_object_object_get_ex(radio_json, key, &v)) { \
snprintf(path, sizeof(path), "wireless.%s.%s=%s", \
device_name, uci_opt, json_object_get_string(v)); \
struct uci_ptr ptr; \
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK) \
uci_set(ctx, &ptr); \
}
RP("ht", "htmode");
/* Channel: 0 = auto in UniFi */
if (json_object_object_get_ex(radio_json, "channel", &v)) {
int ch = json_object_get_int(v);
if (ch == 0) {
snprintf(path, sizeof(path), "wireless.%s.channel=auto", device_name);
} else {
snprintf(path, sizeof(path), "wireless.%s.channel=%d", device_name, ch);
}
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
}
/* tx_power */
if (json_object_object_get_ex(radio_json, "tx_power", &v)) {
snprintf(path, sizeof(path), "wireless.%s.txpower=%d",
device_name, json_object_get_int(v));
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
}
/* Enable the radio */
snprintf(path, sizeof(path), "wireless.%s.disabled=0", device_name);
struct uci_ptr ptr;
if (uci_lookup_ptr(ctx, &ptr, path, true) == UCI_OK)
uci_set(ctx, &ptr);
#undef RP
uci_commit(ctx, &pkg, false);
uci_unload(ctx, pkg);
uci_free_context(ctx);
}
/* ═══════════════════════════════════════════════════════════════════
Create a VAP (wifi-iface UCI) from a controller VAP JSON
═══════════════════════════════════════════════════════════════════
Controller parameters we read and how we map them:
essid → wireless.openuf_X.ssid
x_passphrase → wireless.openuf_X.key
security → wireless.openuf_X.encryption (via sec_to_uci)
hide_ssid → wireless.openuf_X.hidden
guest_policy → wireless.openuf_X.isolate (client isolation)
fast_roaming_enabled → ieee80211r, ft_over_ds, mobility_domain, ft_psk_generate_local
band_steering → ieee80211k, ieee80211v, rrm_neighbor_report, bss_transition
pmf_mode → ieee80211w (0/1/2)
wpa3_support → add "sae-mixed" if WPA2+WPA3
uapsd → uapsd (U-APSD power saving)
vlan_id → wireless.openuf_X.vlan_id (if ≠ 0)
*/
static int apply_vap(struct uci_context *ctx,
struct uci_package *pkg,
struct json_object *vap_json,
const char *device_name,
const char *radio_band,
const char *mac_str,
int vap_idx)
{
struct json_object *v;
(void)mac_str;
const char *essid = "";
const char *security = "wpa2psk";
const char *pass = "";
if (json_object_object_get_ex(vap_json, "essid", &v)) essid = json_object_get_string(v);
if (json_object_object_get_ex(vap_json, "security", &v)) security = json_object_get_string(v);
if (json_object_object_get_ex(vap_json, "x_passphrase",&v)) pass = json_object_get_string(v);
/* Resolve the final network before creating the VAP; never fail open. */
int vid = 0;
char target_network[32] = "lan";
if (json_object_object_get_ex(vap_json, "vlan_id", &v))
vid = json_object_get_int(v);
if (vid > 0) {
if (ensure_vlan_network(vid) != 0) {
printf("[openuf] Failed to configure VLAN network %d\n", vid);
return -1;
}
snprintf(target_network, sizeof(target_network), "vlan%d", vid);
}
/* Section name: openuf_<idx>_<ssid_safe> */
char safe[16] = {0};
safe_section_name(essid, safe, sizeof(safe));
char sec_name[48];
snprintf(sec_name, sizeof(sec_name), "openuf_%d_%s", vap_idx, safe);
if (uci_ensure_section(ctx, pkg, sec_name, "wifi-iface") != 0) {
printf("[openuf] Failed to create VAP section '%s'\n", sec_name);
return -1;
}
UCI_SET(ctx, "wireless", sec_name, "device", device_name);
UCI_SET(ctx, "wireless", sec_name, "mode", "ap");
UCI_SET(ctx, "wireless", sec_name, "ssid", essid);
UCI_SET(ctx, "wireless", sec_name, "network", target_network);
UCI_SET(ctx, "wireless", sec_name, "encryption", sec_to_uci(security));
/*
* Preserve the controller's WLAN configuration ID. Inform telemetry must
* refer to this ObjectId; a label such as "user" is not a valid VAP ID.
* Controller versions use different keys, so accept the known variants.
*/
const char *vap_id = NULL;
const char *id_keys[] = { "_id", "id", "wlanconf_id" };
for (size_t i = 0; i < sizeof(id_keys) / sizeof(id_keys[0]); i++) {
if (json_object_object_get_ex(vap_json, id_keys[i], &v)) {
const char *candidate = json_object_get_string(v);
if (valid_object_id(candidate)) {
vap_id = candidate;
break;
}
}
}
if (vap_id)
UCI_SET(ctx, "wireless", sec_name, "openuf_vap_id", vap_id);
/* Password */
if (pass && pass[0] && strcmp(security,"open") != 0)
UCI_SET(ctx, "wireless", sec_name, "key", pass);
/* hidden SSID */
int hidden = 0;
if (json_object_object_get_ex(vap_json, "hide_ssid", &v))
hidden = json_object_get_boolean(v) ? 1 : 0;
UCI_SET_INT(ctx, "wireless", sec_name, "hidden", hidden);
/* Client isolation (guest network) */
int isolate = 0;
if (json_object_object_get_ex(vap_json, "guest_policy", &v))
isolate = json_object_get_boolean(v) ? 1 : 0;
UCI_SET_INT(ctx, "wireless", sec_name, "isolate", isolate);
/* U-APSD (power saving for mobile clients) */
int uapsd = 1;
if (json_object_object_get_ex(vap_json, "uapsd", &v))
uapsd = json_object_get_boolean(v) ? 1 : 0;
UCI_SET_INT(ctx, "wireless", sec_name, "uapsd", uapsd);
/* ── PMF (Protected Management Frames / 802.11w) ──────────────
* "disabled" → 0, "optional" → 1, "required" → 2
* WPA3 (sae/sae-mixed) always requires "optional" or " required" ieee80211w=2 */
int pmf = 0;
if (json_object_object_get_ex(vap_json, "pmf_mode", &v)) {
const char *pm = json_object_get_string(v);
if (!strcmp(pm, "optional")) pmf = 1;
if (!strcmp(pm, "required")) pmf = 2;
}
/* WPA3 forces PMF=2 */
if (!strcmp(security,"wpa3") || !strcmp(security,"wpa3transition") ||
!strcmp(security,"wpa3enterprise"))
pmf = 2;
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211w", pmf);
/* ── Fast Roaming (802.11r FT) ────────────────────────────────
* Allows clients to move between APs without re-authentication
* complete. The FT handshake only takes ~50ms vs ~200-300ms for a normal one. */
const char *ft_keys[] = {
"fast_roaming_enabled", "fast_roaming", "ft_enabled", "ieee80211r"
};
int ft = json_boolean_any(vap_json, ft_keys,
sizeof(ft_keys) / sizeof(ft_keys[0]));
/*
* This legacy 2.4 GHz ath9k PHY rejects every FT beacon tested, including
* WPA2 with PMF disabled. Preserve the controller request for telemetry,
* but disable 802.11r on this one unsupported PHY so the BSS can start.
*/
if (ft && radio_band && !strcmp(radio_band, "ng") &&
radio_uses_ath9k(device_name)) {
UCI_SET_INT(ctx, "wireless", sec_name, "openuf_ft_requested", 1);
ft = 0;
printf("[openuf] Disabled FT on unsupported 2.4 GHz ath9k radio %s\n",
device_name);
}
if (ft) {
char mdomain[5];
mobility_domain_for_ssid(essid, mdomain);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "ft_over_ds", 0);
UCI_SET_INT(ctx, "wireless", sec_name, "ft_psk_generate_local", 1);
/* Local key generation avoids external R0KH/R1KH dependencies. */
UCI_SET(ctx, "wireless", sec_name, "mobility_domain", mdomain);
} else {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 0);
}
/* Enable the hostapd capabilities used by steering and 802.11v hints. */
const char *band_steer_keys[] = {
"band_steering", "band_steering_enabled", "band_steering_mode",
"steering_enabled"
};
const char *handoff_keys[] = {
"bss_transition", "bss_transition_enabled",
"bss_transition_management", "handoff_suggestions",
"handoff_suggestions_enabled", "ieee80211v"
};
int band_steer = json_boolean_any(
vap_json, band_steer_keys,
sizeof(band_steer_keys) / sizeof(band_steer_keys[0]));
int handoff = json_boolean_any(
vap_json, handoff_keys,
sizeof(handoff_keys) / sizeof(handoff_keys[0]));
int rrm = band_steer || handoff;
int bss_transition_requested = band_steer || handoff;
UCI_SET_INT(ctx, "wireless", sec_name, "openuf_band_steering",
band_steer);
UCI_SET_INT(ctx, "wireless", sec_name, "openuf_handoff_suggestions",
handoff);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", rrm);
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", rrm);
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_beacon_report", rrm);
/* Enable 802.11v at runtime after hostapd starts. Putting this option
* in UCI makes builds without CONFIG_WNM_AP reject the entire BSS. */
if (bss_transition_requested)
UCI_SET_INT(ctx, "wireless", sec_name,
"openuf_bss_transition_requested", 1);
/* Record the controller VLAN for telemetry and diagnostics. */
if (vid > 0)
UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid);
/* Reassert and validate every option required to start a secure AP. */
if (uci_set_required(ctx, pkg, sec_name, "device", device_name) != 0) {
printf("[openuf] Failed to bind VAP '%s' to %s\n",
essid, device_name);
return -1;
}
if (uci_set_required(ctx, pkg, sec_name, "mode", "ap") != 0 ||
uci_set_required(ctx, pkg, sec_name, "ssid", essid) != 0 ||
uci_set_required(ctx, pkg, sec_name, "encryption",
sec_to_uci(security)) != 0) {
printf("[openuf] Refusing incomplete VAP '%s': core AP options "
"could not be stored\n", essid);
return -1;
}
if (pass && pass[0] && strcmp(security, "open") != 0 &&
uci_set_required(ctx, pkg, sec_name, "key", pass) != 0) {
printf("[openuf] Refusing unsecured VAP '%s': key could not be stored\n",
essid);
return -1;
}
if (uci_set_required(ctx, pkg, sec_name, "network", target_network) != 0) {
printf("[openuf] Refusing unsafe VAP '%s': cannot bind to %s\n",
essid, target_network);
return -1;
}
printf("[openuf] VAP '%s' -> %s device=%s network=%s enc=%s "
"ft=%d bs=%d handoff=%d pmf=%d\n",
essid, sec_name, device_name, target_network, sec_to_uci(security),
ft, band_steer, handoff, pmf);
return 0;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_config — apply the controller's full configuration
═══════════════════════════════════════════════════════════════════
Called from inform.c → handle_response() when _type=="setstate".
config_json is the controller's complete JSON.
Process:
1. Remove old VAPs (openuf_ prefix)
2. Apply radio_table (channel, power, htmode) per radio
3. Create one VAP for each entry in vap_table
4. Commit UCI
5. Run "wifi reload" to apply without rebooting the AP
*/
int wlan_apply_config(struct json_object *config_json,
const uf_model_t *model)
{
struct json_object *rt_arr = NULL, *vt_arr = NULL, *v;
json_object_object_get_ex(config_json, "radio_table", &rt_arr);
json_object_object_get_ex(config_json, "vap_table", &vt_arr);
/* Get the AP's MAC for mobility_domain */
char mac_str[32] = "00:00:00:00:00:00";
{
char path[128];
snprintf(path, sizeof(path), "/sys/class/net/eth0/address");
FILE *f = fopen(path, "r");
if (f) { fgets(mac_str, sizeof(mac_str), f); fclose(f); }
mac_str[strcspn(mac_str, "\r\n")] = '\0';
}
/* Stop hostapd so a deleted BSS cannot survive a netifd reload race. */
printf("[openuf] Stopping Wi-Fi before controller provisioning...\n");
system("wifi down >/dev/null 2>&1");
/* Remove every existing VAP so UniFi becomes the sole Wi-Fi owner. */
wlan_clear();
/* 2. Apply radio_table */
if (rt_arr && json_object_is_type(rt_arr, json_type_array)) {
int nr = json_object_array_length(rt_arr);
for (int i = 0; i < nr; i++) {
struct json_object *r = json_object_array_get_idx(rt_arr, i);
if (!r) continue;
/* Find the UCI device corresponding to this band */
const char *radio_band = "";
if (json_object_object_get_ex(r, "radio", &v))
radio_band = json_object_get_string(v);
const char *device_name = wlan_device_for_band(model, radio_band);
if (!device_name) {
printf("[openuf] Ignoring settings for unknown radio '%s'\n",
radio_band);
continue;
}
wlan_apply_radio(r, device_name);
}
}
/*
* Load the package after the per-radio commits, otherwise this context
* contains a stale copy that can overwrite those changes on commit.
*/
struct uci_context *ctx = uci_alloc_context();
if (!ctx) {
printf("[openuf] Failed to allocate UCI context\n");
return -1;
}
struct uci_package *pkg = NULL;
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
char *uci_error = NULL;
uci_get_errorstr(ctx, &uci_error, "wireless");
printf("[openuf] Failed to load UCI wireless configuration: %s\n",
uci_error ? uci_error : "unknown UCI error");
free(uci_error);
uci_free_context(ctx);
return -1;
}
/*
* Once UniFi provisioning owns Wi-Fi, disable OpenWrt's generated
* default VAPs. Leaving them enabled keeps broadcasting "OpenWrt"
* alongside the controller-managed SSIDs.
*/
int disabled_defaults = 0;
struct uci_element *default_element;
uci_foreach_element(&pkg->sections, default_element) {
struct uci_section *section = uci_to_section(default_element);
if (!strcmp(section->type, "wifi-iface") &&
!strncmp(section->e.name, "default_radio", 13)) {
UCI_SET(ctx, "wireless", section->e.name, "disabled", "1");
disabled_defaults++;
}
}
if (disabled_defaults)
printf("[openuf] Disabled %d default OpenWrt VAPs\n",
disabled_defaults);
/* 3. Create VAPs and determine whether any WLAN requests steering. */
int steering_policy_enabled = 0;
if (vt_arr && json_object_is_type(vt_arr, json_type_array)) {
int nv = json_object_array_length(vt_arr);
for (int i = 0; i < nv; i++) {
struct json_object *vap = json_object_array_get_idx(vt_arr, i);
if (!vap) continue;
const char *band_steer_keys[] = {
"band_steering", "band_steering_enabled",
"band_steering_mode", "steering_enabled"
};
const char *handoff_keys[] = {
"bss_transition", "bss_transition_enabled",
"bss_transition_management", "handoff_suggestions",
"handoff_suggestions_enabled", "ieee80211v"
};
if (json_boolean_any(vap, band_steer_keys,
sizeof(band_steer_keys) /
sizeof(band_steer_keys[0])) ||
json_boolean_any(vap, handoff_keys,
sizeof(handoff_keys) /
sizeof(handoff_keys[0])))
steering_policy_enabled = 1;
/* A VAP without an explicit band is a model-wide WLAN. */
const char *radio_band = NULL;
if (json_object_object_get_ex(vap, "radio", &v))
radio_band = json_object_get_string(v);
if (radio_band && !strcmp(radio_band, "2g")) radio_band = "ng";
if (radio_band && !strcmp(radio_band, "5g")) radio_band = "na";
if (radio_band && !strcmp(radio_band, "6GHz")) radio_band = "6g";
int all_radios = !radio_band || !radio_band[0] ||
!strcmp(radio_band, "both") ||
!strcmp(radio_band, "all");
int applied = 0;
for (int j = 0; j < model->radio_map_len; j++) {
if (!all_radios &&
strcmp(model->radio_map[j].band, radio_band))
continue;
int section_idx = i * model->radio_map_len + j;
const char *device = wlan_device_for_band(
model, model->radio_map[j].band);
if (!device || apply_vap(ctx, pkg, vap, device,
model->radio_map[j].band,
mac_str, section_idx) != 0) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
applied++;
}
if (!applied) {
printf("[openuf] Ignoring VAP with unknown radio '%s'\n",
radio_band ? radio_band : "");
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
}
}
/* 4. Commit UCI */
if (uci_commit(ctx, &pkg, false) != UCI_OK) {
char *uci_error = NULL;
uci_get_errorstr(ctx, &uci_error, "wireless");
printf("[openuf] Failed to commit UCI wireless configuration: %s\n",
uci_error ? uci_error : "unknown UCI error");
free(uci_error);
uci_unload(ctx, pkg);
uci_free_context(ctx);
return -1;
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
if (configure_band_steering(steering_policy_enabled) != 0)
printf("[openuf] Failed to configure the band steering policy\n");
/*
* Reload netifd for generated VLAN devices, then bring the radios up one
* at a time. Some dual-ath9k devices intermittently fail their first beacon
* setup after ACS. Start and verify each PHY independently, retrying a failed radio so
* provisioning cannot leave one band visible but unusable.
*/
printf("[openuf] Starting controller-managed Wi-Fi sequentially...\n");
system("ubus call network reload >/dev/null 2>&1");
for (int i = 0; i < model->radio_map_len; i++) {
char command[256];
const char *device = wlan_device_for_band(
model, model->radio_map[i].band);
/* Model radio names are internal constants, but validate defensively. */
if (!device ||
strspn(device,
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") !=
strlen(device)) {
printf("[openuf] Refusing invalid radio name\n");
continue;
}
int radio_up = 0;
for (int attempt = 1; attempt <= 2 && !radio_up; attempt++) {
printf("[openuf] Starting %s (%s), attempt %d...\n",
device, model->radio_map[i].band, attempt);
snprintf(command, sizeof(command),
"wifi up %s >/dev/null 2>&1", device);
system(command);
/* ACS normally takes 6-8 seconds on this ath9k hardware. */
sleep(10);
int phy_index = -1;
if (sscanf(device, "radio%d", &phy_index) != 1)
phy_index = -1;
snprintf(command, sizeof(command),
"iw dev phy%d-ap0 info 2>/dev/null | "
"grep -q '^[[:space:]]*ssid ' && echo true",
phy_index);
FILE *status = popen(command, "r");
if (status) {
char value[16] = {0};
if (fgets(value, sizeof(value), status) &&
!strncmp(value, "true", 4))
radio_up = 1;
pclose(status);
}
if (!radio_up)
printf("[openuf] %s did not reach the up state; retrying\n",
device);
}
if (!radio_up)
printf("[openuf] %s failed after 2 start attempts\n", device);
}
/* Enable management features only after hostapd has registered each BSS.
* Unsupported WNM methods fail harmlessly without preventing AP startup. */
if (steering_policy_enabled) {
for (int i = 0; i < model->radio_map_len; i++) {
const char *device = wlan_device_for_band(
model, model->radio_map[i].band);
int phy_index = -1;
if (!device || sscanf(device, "radio%d", &phy_index) != 1)
continue;
char command[256];
snprintf(command, sizeof(command),
"ubus -S call hostapd.phy%d-ap0 bss_mgmt_enable "
"%c{ \"neighbor_report\": true, "
"\"beacon_report\": true, "
"\"bss_transition\": true }%c >/dev/null 2>&1",
phy_index, 39, 39);
if (system(command) != 0)
printf("[openuf] hostapd on phy%d lacks runtime 802.11v "
"support; continuing without BSS Transition\n",
phy_index);
}
}
/* Restart after hostapd has registered both BSSes on ubus. */
system("/etc/init.d/usteer restart >/dev/null 2>&1");
return 0;
}
static int system_cfg_get(const char *cfg, const char *key,
char *out, size_t out_size)
{
size_t key_len = strlen(key);
const char *line = cfg;
while (line && *line) {
const char *end = strchr(line, '\n');
size_t line_len = end ? (size_t)(end - line) : strlen(line);
if (line_len > key_len && !strncmp(line, key, key_len) &&
line[key_len] == '=') {
size_t value_len = line_len - key_len - 1;
if (value_len >= out_size) value_len = out_size - 1;
memcpy(out, line + key_len + 1, value_len);
out[value_len] = '\0';
return 1;
}
line = end ? end + 1 : NULL;
}
return 0;
}
int wlan_apply_system_cfg(const char *system_cfg,
const uf_model_t *model)
{
if (!system_cfg || !system_cfg[0])
return -1;
struct json_object *root = json_object_new_object();
struct json_object *radios = json_object_new_array();
struct json_object *vaps = json_object_new_array();
char key[64], value[256];
for (int i = 1; i <= 4; i++) {
snprintf(key, sizeof(key), "radio.%d.ieee_mode", i);
if (!system_cfg_get(system_cfg, key, value, sizeof(value)))
continue;
struct json_object *radio = json_object_new_object();
const char *band = strstr(value, "11na") ? "na" : "ng";
json_object_object_add(radio, "radio",
json_object_new_string(band));
const char *ht = strstr(value, "ht80") ? "HT80" :
strstr(value, "ht40") ? "HT40" : "HT20";
json_object_object_add(radio, "ht", json_object_new_string(ht));
snprintf(key, sizeof(key), "radio.%d.channel", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(radio, "channel",
json_object_new_int(!strcmp(value, "auto") ? 0 : atoi(value)));
snprintf(key, sizeof(key), "radio.%d.txpower", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
strcmp(value, "auto"))
json_object_object_add(radio, "tx_power",
json_object_new_int(atoi(value)));
json_object_array_add(radios, radio);
}
for (int i = 1; i <= 32; i++) {
snprintf(key, sizeof(key), "aaa.%d.ssid", i);
if (!system_cfg_get(system_cfg, key, value, sizeof(value)))
continue;
struct json_object *vap = json_object_new_object();
json_object_object_add(vap, "essid",
json_object_new_string(value));
/* Preserve the WLAN ObjectId used to attach clients in topology. */
const char *id_suffixes[] = { "id", "_id", "wlanconf_id" };
for (size_t id_index = 0;
id_index < sizeof(id_suffixes) / sizeof(id_suffixes[0]);
id_index++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
id_suffixes[id_index]);
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
valid_object_id(value)) {
json_object_object_add(vap, "id", json_object_new_string(value));
break;
}
}
snprintf(key, sizeof(key), "aaa.%d.status", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
strcmp(value, "enabled")) {
json_object_put(vap);
continue;
}
snprintf(key, sizeof(key), "wireless.%d.parent", i);
const char *band = "ng";
if (system_cfg_get(system_cfg, key, value, sizeof(value)) &&
!strcmp(value, "wifi1"))
band = "na";
json_object_object_add(vap, "radio", json_object_new_string(band));
snprintf(key, sizeof(key), "aaa.%d.wpa.psk", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "security",
json_object_new_string("wpa2psk"));
json_object_object_add(vap, "x_passphrase",
json_object_new_string(value));
} else {
json_object_object_add(vap, "security",
json_object_new_string("open"));
}
snprintf(key, sizeof(key), "aaa.%d.hide_ssid", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "hide_ssid",
json_object_new_boolean(!strcmp(value, "true")));
snprintf(key, sizeof(key), "aaa.%d.ft.status", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value)))
json_object_object_add(vap, "fast_roaming_enabled",
json_object_new_boolean(!strcmp(value, "enabled")));
const char *band_steer_suffixes[] = {
"band_steering", "band_steering_enabled", "band_steering_mode",
"steering"
};
for (size_t n = 0;
n < sizeof(band_steer_suffixes) /
sizeof(band_steer_suffixes[0]); n++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
band_steer_suffixes[n]);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "band_steering",
json_object_new_boolean(feature_text_enabled(value)));
break;
}
}
const char *handoff_suffixes[] = {
"bss_transition", "bss_transition_enabled",
"handoff_suggestions", "handoff_suggestions_enabled"
};
for (size_t n = 0;
n < sizeof(handoff_suffixes) / sizeof(handoff_suffixes[0]); n++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
handoff_suffixes[n]);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "bss_transition",
json_object_new_boolean(feature_text_enabled(value)));
break;
}
}
snprintf(key, sizeof(key), "aaa.%d.pmf.mode", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
const char *pmf = !strcmp(value, "2") ? "required" :
!strcmp(value, "1") ? "optional" : "disabled";
json_object_object_add(vap, "pmf_mode",
json_object_new_string(pmf));
}
snprintf(key, sizeof(key), "aaa.%d.br.devname", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
const char *dot = strrchr(value, '.');
if (dot && atoi(dot + 1) > 0)
json_object_object_add(vap, "vlan_id",
json_object_new_int(atoi(dot + 1)));
}
json_object_array_add(vaps, vap);
}
json_object_object_add(root, "radio_table", radios);
json_object_object_add(root, "vap_table", vaps);
printf("[openuf] Parsed legacy system_cfg: %zu radios, %zu VAPs\n",
json_object_array_length(radios), json_object_array_length(vaps));
int result = wlan_apply_config(root, model);
json_object_put(root);
return result;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_get_vap_table — read active VAPs from UCI
═══════════════════════════════════════════════════════════════════
Iterates over all wifi-iface entries with the "openuf_" prefix in
/etc/config/wireless and builds the vap_table JSON to include in
the inform payload.
Fields we read from UCI → fields in the JSON:
ssid → essid
device → (used to look up radio and BSSID)
encryption → security (via sec_to_unifi)
hidden → hide_ssid
ieee80211r → fast_roaming_enabled
ieee80211k → band_steering
ieee80211w → pmf_mode ("disabled"/"optional"/"required")
disabled → up (inverse)
We also try to read the actual BSSID of the wlan interface
from /sys/class/net/<iface>/address.
*/
/* Resolve a configured VAP to the live interface reported by nl80211. */
static int find_runtime_vap(int phy_index, const char *ssid,
char *out, size_t out_size)
{
FILE *pipe = popen("iw dev 2>/dev/null", "r");
if (!pipe) return -1;
int phy = -1;
char candidate[32] = "";
char line[256];
while (fgets(line, sizeof(line), pipe)) {
int parsed_phy;
char value[128];
if (sscanf(line, "phy#%d", &parsed_phy) == 1) {
phy = parsed_phy;
candidate[0] = '\0';
continue;
}
if (sscanf(line, " Interface %31s", value) == 1) {
snprintf(candidate, sizeof(candidate), "%s", value);
continue;
}
if (phy == phy_index && candidate[0] &&
sscanf(line, " ssid %127[^\n]", value) == 1 &&
!strcmp(value, ssid)) {
snprintf(out, out_size, "%s", candidate);
pclose(pipe);
return 0;
}
}
pclose(pipe);
return -1;
}
struct json_object *wlan_get_vap_table(const uf_model_t *model)
{
struct json_object *arr = json_object_new_array();
struct uci_context *ctx = uci_alloc_context();
if (!ctx) return arr;
struct uci_package *pkg = NULL;
if (uci_load(ctx, "wireless", &pkg) != UCI_OK) {
uci_free_context(ctx);
return arr;
}
struct uci_element *e;
uci_foreach_element(&pkg->sections, e) {
struct uci_section *sec = uci_to_section(e);
if (strcmp(sec->type, "wifi-iface") != 0) continue;
/* Only report VAPs managed by openuf */
if (strncmp(sec->e.name, "openuf_", 7) != 0) continue;
#define UCI_GET(opt) uci_lookup_option_string(ctx, sec, opt)
const char *ssid = UCI_GET("ssid");
const char *device = UCI_GET("device");
const char *enc = UCI_GET("encryption");
const char *dis = UCI_GET("disabled");
const char *r11 = UCI_GET("ieee80211r");
const char *ft_req = UCI_GET("openuf_ft_requested");
const char *k11 = UCI_GET("ieee80211k");
const char *btm = UCI_GET("bss_transition");
const char *bs_req = UCI_GET("openuf_band_steering");
const char *ho_req = UCI_GET("openuf_handoff_suggestions");
const char *w11 = UCI_GET("ieee80211w");
const char *hidden = UCI_GET("hidden");
const char *vap_id = UCI_GET("openuf_vap_id");
const char *vlan = UCI_GET("vlan_id");
if (!ssid) ssid = "";
if (!device) device = "radio0";
/* Band of this radio */
const char *radio_band = wlan_band_for_device(model, device);
if (!radio_band) radio_band = "ng";
/* Resolve the actual netifd interface (for example phy1-ap0). */
char wlan_iface[32];
int ridx = 0;
sscanf(device, "radio%d", &ridx);
if (find_runtime_vap(ridx, ssid, wlan_iface, sizeof(wlan_iface)) != 0)
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", ridx);
/* Read the actual BSSID from sysfs */
char bssid[32] = "00:00:00:00:00:00";
{
char path[128];
snprintf(path, sizeof(path), "/sys/class/net/%s/address", wlan_iface);
FILE *f = fopen(path, "r");
if (f) {
fgets(bssid, sizeof(bssid), f); fclose(f);
bssid[strcspn(bssid, "\r\n")] = '\0';
}
}
/* PMF: ieee80211w → "disabled"/"optional"/"required" */
const char *pmf = "disabled";
if (w11) {
if (!strcmp(w11,"1")) pmf = "optional";
if (!strcmp(w11,"2")) pmf = "required";
}
bool ft_on = (r11 && !strcmp(r11,"1")) ||
(ft_req && !strcmp(ft_req,"1"));
bool bs_on = bs_req ? !strcmp(bs_req, "1") :
(k11 && !strcmp(k11, "1"));
bool handoff_on = ho_req ? !strcmp(ho_req, "1") :
(btm && !strcmp(btm, "1"));
bool hid = (hidden && !strcmp(hidden,"1"));
bool up = !(dis && !strcmp(dis,"1"));
struct json_object *o = json_object_new_object();
json_object_object_add(o, "essid", json_object_new_string(ssid));
json_object_object_add(o, "bssid", json_object_new_string(bssid));
json_object_object_add(o, "name", json_object_new_string(wlan_iface));
json_object_object_add(o, "ifname", json_object_new_string(wlan_iface));
json_object_object_add(o, "radio", json_object_new_string(radio_band));
json_object_object_add(o, "security", json_object_new_string(sec_to_unifi(enc)));
json_object_object_add(o, "up", json_object_new_boolean(up));
json_object_object_add(o, "hide_ssid", json_object_new_boolean(hid));
json_object_object_add(o, "fast_roaming_enabled",json_object_new_boolean(ft_on));
json_object_object_add(o, "band_steering", json_object_new_boolean(bs_on));
json_object_object_add(o, "bss_transition", json_object_new_boolean(handoff_on));
json_object_object_add(o, "handoff_suggestions", json_object_new_boolean(handoff_on));
json_object_object_add(o, "pmf_mode", json_object_new_string(pmf));
json_object_object_add(o, "num_sta", json_object_new_int(0));
if (vlan && atoi(vlan) > 0)
json_object_object_add(o, "vlan_id", json_object_new_int(atoi(vlan)));
if (valid_object_id(vap_id))
json_object_object_add(o, "id", json_object_new_string(vap_id));
json_object_array_add(arr, o);
#undef UCI_GET
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
return arr;
}