/* * Shared translations and validators used by Wi-Fi provisioning and * telemetry. This unit does not commit controller configuration itself. */ #include #include #include #include #include #include #include "wlan.h" #include "ufmodel.h" #include "crypto.h" #include "wlan_internal.h" const char *wlan_security_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 */ } /* OpenWrt UCI encryption names to UniFi security names. */ const char *wlan_security_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. */ int wlan_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; } /* * UniFi uses a 24-character hexadecimal MongoDB ObjectId to associate VAP * telemetry and stations with a WLAN. Older legacy system_cfg payloads do * not include that ID, so allocate a local one and keep it stable in UCI. * All radio instances of the same WLAN must report the same ID. */ int wlan_ensure_vap_ids(struct json_object *vaps) { if (!vaps || !json_object_is_type(vaps, json_type_array)) return 0; struct uci_context *ctx = uci_alloc_context(); struct uci_package *pkg = NULL; if (ctx) uci_load(ctx, "wireless", &pkg); int count = json_object_array_length(vaps); for (int i = 0; i < count; i++) { struct json_object *vap = json_object_array_get_idx(vaps, i); struct json_object *value; if (!vap) continue; const char *id = NULL; const char *id_keys[] = { "_id", "id", "wlanconf_id" }; for (size_t n = 0; n < sizeof(id_keys) / sizeof(id_keys[0]); n++) { if (json_object_object_get_ex(vap, id_keys[n], &value)) { const char *candidate = json_object_get_string(value); if (wlan_valid_object_id(candidate)) { id = candidate; break; } } } if (id) continue; const char *ssid = ""; if (json_object_object_get_ex(vap, "essid", &value)) ssid = json_object_get_string(value); /* Reuse an ID already assigned to this WLAN in the same payload. */ for (int j = 0; j < i && !id; j++) { struct json_object *previous = json_object_array_get_idx(vaps, j); struct json_object *previous_ssid; struct json_object *previous_id; if (previous && json_object_object_get_ex(previous, "essid", &previous_ssid) && !strcmp(json_object_get_string(previous_ssid), ssid) && json_object_object_get_ex(previous, "id", &previous_id) && wlan_valid_object_id(json_object_get_string(previous_id))) id = json_object_get_string(previous_id); } /* Reuse the ID committed by an earlier provisioning cycle. */ if (!id && pkg) { struct uci_element *element; uci_foreach_element(&pkg->sections, element) { struct uci_section *section = uci_to_section(element); const char *stored_ssid; const char *stored_id; if (strcmp(section->type, "wifi-iface") || strncmp(section->e.name, "openuf_", 7)) continue; stored_ssid = uci_lookup_option_string(ctx, section, "ssid"); stored_id = uci_lookup_option_string(ctx, section, "openuf_vap_id"); if (stored_ssid && !strcmp(stored_ssid, ssid) && wlan_valid_object_id(stored_id)) { id = stored_id; break; } } } char generated[25]; if (!id) { if (crypto_random_hex((unsigned char *)generated, 12) != 0) { LOGF(stdout, "Failed to generate a VAP ID for '%s'", ssid); if (pkg) uci_unload(ctx, pkg); if (ctx) uci_free_context(ctx); return -1; } id = generated; LOGF(stdout, "Generated persistent VAP ID %s for '%s'", id, ssid); } json_object_object_add(vap, "id", json_object_new_string(id)); json_object_object_add(vap, "wlanconf_id", json_object_new_string(id)); } if (pkg) uci_unload(ctx, pkg); if (ctx) uci_free_context(ctx); return 0; } /* Build one stable 802.11r mobility domain shared by every AP for an SSID. */ void wlan_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. */ int wlan_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. */ int wlan_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". */ int wlan_feature_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). */ void wlan_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'; }