added devcontainer

This commit is contained in:
2026-07-10 15:44:09 +02:00
parent 5da43ff0f3
commit 356837ee0a
31 changed files with 4828 additions and 1 deletions
+885
View File
@@ -0,0 +1,885 @@
/*
* 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 <uci.h>
#include <json-c/json.h>
#include "wlan.h"
#include "ufmodel.h"
/* ─── 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";
}
/* ─── Nombre de sección UCI seguro (máx 15 chars) ──────────────── */
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=='_'||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);
free(p);
if (ret != UCI_OK) return -1;
return (uci_set(ctx, &ptr) == 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;
}
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 device_section[48], interface_section[32];
char device_name[32], vid_string[16];
snprintf(device_section, sizeof(device_section),
"openuf_vlan%d", vid);
snprintf(interface_section, sizeof(interface_section),
"vlan%d", vid);
snprintf(device_name, sizeof(device_name), "br-lan.%d", vid);
snprintf(vid_string, sizeof(vid_string), "%d", vid);
int ok = uci_ensure_section(ctx, pkg, device_section, "device") == 0 &&
uci_ensure_section(ctx, pkg, interface_section, "interface") == 0;
if (ok) {
UCI_SET(ctx, "network", device_section, "type", "8021q");
UCI_SET(ctx, "network", device_section, "ifname", "br-lan");
UCI_SET(ctx, "network", device_section, "vid", vid_string);
UCI_SET(ctx, "network", device_section, "name", device_name);
UCI_SET(ctx, "network", interface_section, "proto", "none");
UCI_SET(ctx, "network", interface_section, "device", device_name);
ok = uci_commit(ctx, &pkg, false) == UCI_OK;
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
if (ok)
printf("[openuf] Configured VLAN %d as network '%s' on br-lan\n",
vid, interface_section);
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;
}
/* Recopilar secciones a eliminar (no modificar durante iteración) */
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 — aplicar config de radio (canal, HT, potencia)
═══════════════════════════════════════════════════════════════════
Lectura de parámetros del JSON del controlador:
channel → wireless.<device>.channel
ht → wireless.<device>.htmode ("HT20" / "HT40" / "HT80" / "HE80")
tx_power → wireless.<device>.txpower
min_rssi → no se mapea a UCI (requiere daemon externo)
*/
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");
/* Canal: 0 = auto en 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);
}
/* Habilitar el 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);
}
/* ═══════════════════════════════════════════════════════════════════
Crear una VAP (wifi-iface UCI) desde un JSON VAP del controlador
═══════════════════════════════════════════════════════════════════
Parámetros del controlador que leemos y cómo los mapeamos:
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 (aislamiento de clientes)
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 → añadir "sae-mixed" si WPA2+WPA3
uapsd → uapsd (U-APSD power saving)
vlan_id → wireless.openuf_X.vlan_id (si ≠ 0)
*/
static int apply_vap(struct uci_context *ctx,
struct uci_package *pkg,
struct json_object *vap_json,
const char *device_name,
const char *mac_str,
int vap_idx)
{
struct json_object *v;
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);
/* Nombre de sección: 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", "lan");
UCI_SET(ctx, "wireless", sec_name, "encryption", sec_to_uci(security));
/* Contraseña */
if (pass && pass[0] && strcmp(security,"open") != 0)
UCI_SET(ctx, "wireless", sec_name, "key", pass);
/* SSID oculto */
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);
/* Aislamiento de clientes (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 (ahorro de energía para clientes móviles) */
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) siempre requiere 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 obliga 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) ────────────────────────────────
* Permite que los clientes se muevan entre APs sin re-autenticación
* completa. El handshake FT sólo tarda ~50ms vs ~200-300ms normal. */
int ft = 0;
if (json_object_object_get_ex(vap_json, "fast_roaming_enabled", &v))
ft = json_object_get_boolean(v) ? 1 : 0;
if (ft) {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "ft_over_ds", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "ft_psk_generate_local", 1);
/* mobility_domain: derivar de MAC del AP (2 bytes) */
char mdomain[8] = {0};
if (mac_str && strlen(mac_str) >= 5) {
/* Usar bytes 0 y 1 de la MAC como dominio */
char b0[3]={mac_str[0],mac_str[1],0};
char b1[3]={mac_str[3],mac_str[4],0};
unsigned int v0=0,v1=0;
sscanf(b0,"%x",&v0); sscanf(b1,"%x",&v1);
snprintf(mdomain, sizeof(mdomain), "%02x%02x", v0, v1);
} else {
strcpy(mdomain, "1234");
}
UCI_SET(ctx, "wireless", sec_name, "mobility_domain", mdomain);
} else {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 0);
}
/* ── Band Steering (802.11k/v) ────────────────────────────────
* 802.11k: Neighbor Reports → el AP informa al cliente qué otros
* APs existen para facilitar el roaming.
* 802.11v: BSS Transition Management → el AP puede "sugerir" al
* cliente que se mueva a otro AP con mejor señal. */
int band_steer = 0;
if (json_object_object_get_ex(vap_json, "band_steering", &v))
band_steer = json_object_get_boolean(v) ? 1 : 0;
if (band_steer) {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "bss_transition", 1);
} else {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 0);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 0);
}
/* ── VLAN ──────────────────────────────────────────────────────
* Si vlan_id ≠ 0, configurar la interfaz con VLAN tagging. */
if (json_object_object_get_ex(vap_json, "vlan_id", &v)) {
int 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;
}
UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid);
/* Establecer network a vlanXXX si existe */
char vlan_net[32];
snprintf(vlan_net, sizeof(vlan_net), "vlan%d", vid);
UCI_SET(ctx, "wireless", sec_name, "network", vlan_net);
}
}
printf("[openuf] VAP '%s' → %s enc=%s ft=%d bs=%d pmf=%d\n",
essid, sec_name, sec_to_uci(security), ft, band_steer, pmf);
return 0;
}
/* ═══════════════════════════════════════════════════════════════════
wlan_apply_config — aplicar configuración completa del controlador
═══════════════════════════════════════════════════════════════════
Llamado desde inform.c → handle_response() cuando _type=="setstate".
config_json es el JSON completo del controlador.
Proceso:
1. Eliminar VAPs antiguas (prefijo openuf_)
2. Aplicar radio_table (canal, potencia, htmode) por radio
3. Crear una VAP por cada entrada en vap_table
4. Hacer commit UCI
5. Ejecutar "wifi reload" para aplicar sin reiniciar el 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);
/* Obtener MAC del AP para 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';
}
/* 1. Limpiar VAPs antiguas */
wlan_clear();
/* 2. Aplicar 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;
/* Buscar el device UCI correspondiente a esta banda */
const char *radio_band = "";
if (json_object_object_get_ex(r, "radio", &v))
radio_band = json_object_get_string(v);
const char *device_name = "radio0";
for (int j = 0; j < model->radio_map_len; j++) {
if (!strcmp(model->radio_map[j].band, radio_band)) {
device_name = model->radio_map[j].device;
break;
}
}
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. Crear VAPs */
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;
/* Buscar device UCI para este VAP */
const char *radio_band = "ng";
if (json_object_object_get_ex(vap, "radio", &v))
radio_band = json_object_get_string(v);
const char *device_name = "radio0";
for (int j = 0; j < model->radio_map_len; j++) {
if (!strcmp(model->radio_map[j].band, radio_band)) {
device_name = model->radio_map[j].device;
break;
}
}
if (apply_vap(ctx, pkg, vap, device_name, mac_str, i) != 0) {
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);
/* 5. Aplicar cambios sin reiniciar (wifi reload recarga hostapd) */
printf("[openuf] Running wifi reload...\n");
system("ubus call network reload >/dev/null 2>&1");
system("wifi reload 2>/dev/null &");
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));
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")));
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 — leer VAPs activas desde UCI
═══════════════════════════════════════════════════════════════════
Itera todas las wifi-iface con prefijo "openuf_" en /etc/config/wireless
y construye el JSON vap_table para incluir en el payload inform.
Campos que leemos de UCI → campos en el JSON:
ssid → essid
device → (usado para buscar radio y 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 (inverso)
También intentamos leer el BSSID real de la interfaz wlan
desde /sys/class/net/<iface>/address.
*/
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;
/* Solo reportar VAPs gestionadas por 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 *k11 = UCI_GET("ieee80211k");
const char *w11 = UCI_GET("ieee80211w");
const char *hidden = UCI_GET("hidden");
if (!ssid) ssid = "";
if (!device) device = "radio0";
/* Banda de este radio */
const char *radio_band = "ng";
for (int j = 0; j < model->radio_map_len; j++) {
if (!strcmp(model->radio_map[j].device, device)) {
radio_band = model->radio_map[j].band;
break;
}
}
/* Nombre de la interfaz wlan (wlan0 para radio0, etc.) */
char wlan_iface[32] = "wlan0";
int ridx = 0;
sscanf(device, "radio%d", &ridx);
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
/* Leer BSSID real desde 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"));
bool bs_on = (k11 && !strcmp(k11,"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(sec->e.name));
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, "pmf_mode", json_object_new_string(pmf));
json_object_object_add(o, "num_sta", json_object_new_int(0));
json_object_array_add(arr, o);
#undef UCI_GET
}
uci_unload(ctx, pkg);
uci_free_context(ctx);
return arr;
}