1552 lines
61 KiB
C
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;
|
|
}
|