From fb542777ca11748d4d9e8f2afcf127eac2d41c81 Mon Sep 17 00:00:00 2001 From: Koda YeenBean Date: Sat, 11 Jul 2026 22:57:35 +0100 Subject: [PATCH] fixing adoption with invalid WiFi Name scheme (#4) Reviewed-on: https://git.ascheu.de/Koda/OpenUniFi/pulls/4 --- src/clients.c | 11 +++- src/clients.h | 4 +- src/inform.c | 48 +++++++++++++-- src/wlan.c | 164 +++++++++++++++++++++++++++++++++++++------------- 4 files changed, 177 insertions(+), 50 deletions(-) diff --git a/src/clients.c b/src/clients.c index 59e25a6..73311eb 100644 --- a/src/clients.c +++ b/src/clients.c @@ -239,7 +239,9 @@ int clients_read_wifi(const char *wlan_iface, struct json_object *clients_build_sta_table(const char *wlan_iface, const char *radio_band, int channel, - const char *vap_name) + const char *vap_name, + int vlan_id, + int is_11r) { sta_info_t stas[MAX_STA]; int n = clients_read_wifi(wlan_iface, radio_band, channel, @@ -266,7 +268,12 @@ struct json_object *clients_build_sta_table(const char *wlan_iface, json_object_object_add(o, "channel", json_object_new_int(s->channel)); json_object_object_add(o, "vap_name", json_object_new_string( vap_name ? vap_name : wlan_iface)); - json_object_object_add(o, "is_11r", json_object_new_boolean(s->is_11r)); + json_object_object_add(o, "is_11r", json_object_new_boolean(is_11r)); + if (vlan_id > 0) { + json_object_object_add(o, "vlan", json_object_new_int(vlan_id)); + json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id)); + } + json_object_object_add(o, "is_wired", json_object_new_boolean(false)); json_object_object_add(o, "ccq", json_object_new_int(s->ccq)); json_object_object_add(o, "idletime", json_object_new_int(0)); json_object_array_add(arr, o); diff --git a/src/clients.h b/src/clients.h index ea6684e..35a2565 100644 --- a/src/clients.h +++ b/src/clients.h @@ -72,7 +72,9 @@ int clients_read_wifi(const char *wlan_iface, struct json_object *clients_build_sta_table(const char *wlan_iface, const char *radio_band, int channel, - const char *vap_name); + const char *vap_name, + int vlan_id, + int is_11r); /* Busca IP en /proc/net/arp dado un MAC. */ int clients_mac_to_ip(const char *mac, char *ip_out, size_t sz); diff --git a/src/inform.c b/src/inform.c index 3f57cc6..11c7a93 100644 --- a/src/inform.c +++ b/src/inform.c @@ -360,21 +360,31 @@ static struct json_object *build_vap_table(const uf_model_t *m) const char *radio = "ng"; const char *bssid = "00:00:00:00:00:00"; const char *vap_id = NULL; + const char *ifname = NULL; + int vlan_id = 0; + int is_11r = 0; if (json_object_object_get_ex(vap, "essid", &v)) essid = json_object_get_string(v); if (json_object_object_get_ex(vap, "name", &v)) vap_name = json_object_get_string(v); if (json_object_object_get_ex(vap, "radio", &v)) radio = json_object_get_string(v); if (json_object_object_get_ex(vap, "bssid", &v)) bssid = json_object_get_string(v); if (json_object_object_get_ex(vap, "id", &v)) vap_id = json_object_get_string(v); + if (json_object_object_get_ex(vap, "ifname", &v)) ifname = json_object_get_string(v); + if (json_object_object_get_ex(vap, "vlan_id", &v)) vlan_id = json_object_get_int(v); + if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v)) + is_11r = json_object_get_boolean(v); /* Mapear banda → interfaz wlan y canal actual */ - char wlan_iface[32] = "wlan0"; + char wlan_iface[32] = "phy0-ap0"; + if (ifname && ifname[0]) + snprintf(wlan_iface, sizeof(wlan_iface), "%s", ifname); int channel = 6; for (int j = 0; j < m->radio_map_len; j++) { if (strcmp(m->radio_map[j].band, radio) == 0) { int idx = 0; sscanf(m->radio_map[j].device, "radio%d", &idx); - snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", idx); + if (!ifname || !ifname[0]) + snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx); radio_stats_t rs; if (sysinfo_radio(wlan_iface, &rs) == 0 && rs.channel) channel = rs.channel; @@ -390,7 +400,8 @@ static struct json_object *build_vap_table(const uf_model_t *m) /* Clientes conectados a esta VAP */ struct json_object *sta_tbl = - clients_build_sta_table(wlan_iface, radio, channel, vap_name); + clients_build_sta_table(wlan_iface, radio, channel, vap_name, + vlan_id, is_11r); int num_sta = json_object_array_length(sta_tbl); /* Calcular tx_power del radio correspondiente */ @@ -408,6 +419,8 @@ static struct json_object *build_vap_table(const uf_model_t *m) json_object_new_string(vap_name)); json_object_object_add(o, "radio", json_object_new_string(radio)); + if (vlan_id > 0) + json_object_object_add(o, "vlan_id", json_object_new_int(vlan_id)); json_object_object_add(o, "up", json_object_new_boolean(iface_st.up)); json_object_object_add(o, "channel", @@ -448,6 +461,25 @@ static struct json_object *build_vap_table(const uf_model_t *m) return arr; } +/* Build the device-level station table UniFi uses for client ownership. */ +static struct json_object *collect_sta_table(struct json_object *vap_table) +{ + struct json_object *all = json_object_new_array(); + int vap_count = json_object_array_length(vap_table); + for (int i = 0; i < vap_count; i++) { + struct json_object *vap = json_object_array_get_idx(vap_table, i); + struct json_object *stations; + if (!json_object_object_get_ex(vap, "sta_table", &stations) || + !json_object_is_type(stations, json_type_array)) + continue; + int count = json_object_array_length(stations); + for (int j = 0; j < count; j++) + json_object_array_add(all, json_object_get( + json_object_array_get_idx(stations, j))); + } + return all; +} + /* ═══════════════════════════════════════════════════════════════════ build_payload — ensamblado completo del JSON inform ═══════════════════════════════════════════════════════════════════ */ @@ -546,8 +578,12 @@ static char *build_payload(const openuf_state_t *st, build_port_table(root, m); build_eth_table(root, m); - /* ── VAPs con clientes WiFi (sta_table anidado) ─────────────── */ - json_object_object_add(root, "vap_table", build_vap_table(m)); + /* Publish both per-VAP and device-level station views. */ + struct json_object *vap_table = build_vap_table(m); + struct json_object *sta_table = collect_sta_table(vap_table); + int station_count = json_object_array_length(sta_table); + json_object_object_add(root, "vap_table", vap_table); + json_object_object_add(root, "sta_table", sta_table); /* ── Vecinos LLDP para topología visual ─────────────────────── */ json_object_object_add(root, "lldp_table", lldp_read_neighbors()); @@ -555,7 +591,7 @@ static char *build_payload(const openuf_state_t *st, /* Contadores globales */ json_object_object_add(root, "bytes_r", json_object_new_int(0)); json_object_object_add(root, "bytes_d", json_object_new_int(0)); - json_object_object_add(root, "num_sta", json_object_new_int(0)); + json_object_object_add(root, "num_sta", json_object_new_int(station_count)); const char *s = json_object_to_json_string(root); diff --git a/src/wlan.c b/src/wlan.c index abf3dda..fbb4204 100644 --- a/src/wlan.c +++ b/src/wlan.c @@ -122,6 +122,36 @@ static int valid_object_id(const char *id) 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); +} + +/* 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); + return !strcasecmp(text, "enabled") || !strcasecmp(text, "true") || + !strcasecmp(text, "on") || !strcmp(text, "1"); + } + return json_object_get_boolean(value) ? 1 : 0; + } + return 0; +} + /* Safe UCI section name (maximum 15 characters). */ static void safe_section_name(const char *ssid, char *out, size_t sz) { @@ -483,13 +513,27 @@ static int apply_vap(struct uci_context *ctx, 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); + 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); + } /* Nombre de sección: openuf__ */ char safe[16] = {0}; @@ -505,7 +549,7 @@ static int apply_vap(struct uci_context *ctx, 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, "network", target_network); UCI_SET(ctx, "wireless", sec_name, "encryption", sec_to_uci(security)); /* @@ -567,25 +611,18 @@ static int apply_vap(struct uci_context *ctx, /* ── 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; + 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])); if (ft) { - UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 1); - UCI_SET_INT(ctx, "wireless", sec_name, "ft_over_ds", 1); + 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); - /* 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"); - } + /* 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); @@ -609,22 +646,9 @@ static int apply_vap(struct uci_context *ctx, 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); - } - } + /* Record the controller VLAN for telemetry and diagnostics. */ + if (vid > 0) + UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid); /* Reassert the binding last and reject incomplete UCI sections. */ if (uci_set_required(ctx, pkg, sec_name, "device", device_name) != 0) { @@ -632,9 +656,14 @@ static int apply_vap(struct uci_context *ctx, essid, device_name); 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 enc=%s ft=%d bs=%d pmf=%d\n", - essid, sec_name, device_name, sec_to_uci(security), + printf("[openuf] VAP '%s' -> %s device=%s network=%s enc=%s ft=%d bs=%d pmf=%d\n", + essid, sec_name, device_name, target_network, sec_to_uci(security), ft, band_steer, pmf); return 0; } @@ -868,6 +897,20 @@ int wlan_apply_system_cfg(const char *system_cfg, 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")) { @@ -950,6 +993,40 @@ int wlan_apply_system_cfg(const char *system_cfg, También intentamos leer el BSSID real de la interfaz wlan desde /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(); @@ -981,6 +1058,7 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model) 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"; @@ -994,11 +1072,12 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model) } } - /* Nombre de la interfaz wlan (wlan0 para radio0, etc.) */ - char wlan_iface[32] = "wlan0"; + /* Resolve the actual netifd interface (for example phy1-ap0). */ + char wlan_iface[32]; int ridx = 0; sscanf(device, "radio%d", &ridx); - snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx); + if (find_runtime_vap(ridx, ssid, wlan_iface, sizeof(wlan_iface)) != 0) + snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", ridx); /* Leer BSSID real desde sysfs */ char bssid[32] = "00:00:00:00:00:00"; @@ -1027,7 +1106,8 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model) 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, "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)); @@ -1036,6 +1116,8 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model) 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)); + 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);