/* * 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..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 #include #include #include #include #include #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.=. */ 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..channel ht → wireless..htmode ("HT20" / "HT40" / "HT80" / "HE80") tx_power → wireless..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__ */ 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//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; }