diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..845959d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/.env \ No newline at end of file diff --git a/files/openuf.conf b/files/openuf.conf index ee77d86..be0d3de 100644 --- a/files/openuf.conf +++ b/files/openuf.conf @@ -6,7 +6,7 @@ # inform_interval: seconds between inform requests (minimum 5) # enable_announce: 1=enable UDP discovery (port 10001) # enable_inform: 1=enable HTTP inform requests (adoption and telemetry) -# enable_logging: 1=enable logging to /var/log/openuf.log +# enable_logging: 1=enable logging to /var/log/openuf.log (default: off) controller_ip = 10.10.10.1 lan_if = br-lan @@ -14,4 +14,4 @@ ufmodel = uapg2-ac-lr inform_interval = 10 enable_announce = 1 enable_inform = 1 -enable_logging = 1 +enable_logging = 0 diff --git a/src/config.c b/src/config.c index f34d9bf..47a0508 100644 --- a/src/config.c +++ b/src/config.c @@ -12,7 +12,8 @@ void config_load(openuf_config_t *cfg) cfg->inform_interval = DEFAULT_INFORM_INTERVAL; cfg->enable_announce = 1; cfg->enable_inform = 1; - cfg->enable_logging = 1; + /* File logging is opt-in to avoid flash wear and unbounded log growth. */ + cfg->enable_logging = 0; FILE *f = fopen(OPENUF_CONF_FILE, "r"); if (!f) return; diff --git a/src/inform.c b/src/inform.c index 5d5ef7b..3f57cc6 100644 --- a/src/inform.c +++ b/src/inform.c @@ -359,11 +359,13 @@ static struct json_object *build_vap_table(const uf_model_t *m) const char *vap_name = ""; const char *radio = "ng"; const char *bssid = "00:00:00:00:00:00"; + const char *vap_id = NULL; 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); /* Mapear banda → interfaz wlan y canal actual */ char wlan_iface[32] = "wlan0"; @@ -430,8 +432,9 @@ static struct json_object *build_vap_table(const uf_model_t *m) json_object_new_int64(iface_st.rx_dropped)); json_object_object_add(o, "tx_dropped", json_object_new_int64(iface_st.tx_dropped)); - json_object_object_add(o, "id", - json_object_new_string("user")); + /* Only controller-issued ObjectIds are valid in this field. */ + if (vap_id) + json_object_object_add(o, "id", json_object_new_string(vap_id)); json_object_object_add(o, "usage", json_object_new_string("user")); json_object_object_add(o, "ccq", diff --git a/src/wlan.c b/src/wlan.c index eb93e17..abf3dda 100644 --- a/src/wlan.c +++ b/src/wlan.c @@ -109,7 +109,20 @@ static const char *sec_to_unifi(const char *uci) return "wpa2psk"; } -/* ─── Nombre de sección UCI seguro (máx 15 chars) ──────────────── */ +/* 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; +} + +/* Safe UCI section name (maximum 15 characters). */ static void safe_section_name(const char *ssid, char *out, size_t sz) { size_t j = 0; @@ -137,6 +150,40 @@ static int uci_set_val(struct uci_context *ctx, if (ret != UCI_OK) return -1; return (uci_set(ctx, &ptr) == 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); + free(assignment); + if (ret != UCI_OK) return -1; + return uci_add_list(ctx, &ptr) == UCI_OK ? 0 : -1; +} + /* Wrapper que formatea path y value en printf style */ #define UCI_SET(ctx, pkg, sec, opt, val) do { \ @@ -175,6 +222,53 @@ static int uci_ensure_section(struct uci_context *ctx, 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(); @@ -186,32 +280,52 @@ static int ensure_vlan_network(int vid) return -1; } - char device_section[48], interface_section[32]; - char device_name[32], vid_string[16]; - snprintf(device_section, sizeof(device_section), + 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(device_name, sizeof(device_name), "br-lan.%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, device_section, "device") == 0 && + 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", 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", 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", device_name); - ok = uci_commit(ctx, &pkg, false) == UCI_OK; + 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 as network '%s' on br-lan\n", - vid, interface_section); + printf("[openuf] Configured VLAN %d on uplink %s as network '%s'\n", + vid, vlan_uplink, interface_section); return ok ? 0 : -1; } @@ -394,6 +508,25 @@ static int apply_vap(struct uci_context *ctx, UCI_SET(ctx, "wireless", sec_name, "network", "lan"); 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); + /* Contraseña */ if (pass && pass[0] && strcmp(security,"open") != 0) UCI_SET(ctx, "wireless", sec_name, "key", pass); @@ -493,8 +626,16 @@ static int apply_vap(struct uci_context *ctx, } } - 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); + /* Reassert the binding last and reject incomplete UCI sections. */ + 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; + } + + 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), + ft, band_steer, pmf); return 0; } @@ -529,7 +670,11 @@ int wlan_apply_config(struct json_object *config_json, mac_str[strcspn(mac_str, "\r\n")] = '\0'; } - /* 1. Limpiar VAPs antiguas */ + /* 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. Aplicar radio_table */ @@ -599,18 +744,33 @@ int wlan_apply_config(struct json_object *config_json, 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"; + /* 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); - const char *device_name = "radio0"; + 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 (!strcmp(model->radio_map[j].band, radio_band)) { - device_name = model->radio_map[j].device; - break; + if (!all_radios && + strcmp(model->radio_map[j].band, radio_band)) + continue; + int section_idx = i * model->radio_map_len + j; + if (apply_vap(ctx, pkg, vap, model->radio_map[j].device, + mac_str, section_idx) != 0) { + uci_unload(ctx, pkg); + uci_free_context(ctx); + return -1; } + applied++; } - if (apply_vap(ctx, pkg, vap, device_name, mac_str, i) != 0) { + 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; @@ -632,10 +792,10 @@ int wlan_apply_config(struct json_object *config_json, uci_unload(ctx, pkg); uci_free_context(ctx); - /* 5. Aplicar cambios sin reiniciar (wifi reload recarga hostapd) */ - printf("[openuf] Running wifi reload...\n"); + /* Reload netifd for generated VLAN devices, then start only the new VAPs. */ + printf("[openuf] Starting controller-managed Wi-Fi...\n"); system("ubus call network reload >/dev/null 2>&1"); - system("wifi reload 2>/dev/null &"); + system("wifi up >/dev/null 2>&1"); return 0; } @@ -820,6 +980,7 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model) const char *k11 = UCI_GET("ieee80211k"); const char *w11 = UCI_GET("ieee80211w"); const char *hidden = UCI_GET("hidden"); + const char *vap_id = UCI_GET("openuf_vap_id"); if (!ssid) ssid = ""; if (!device) device = "radio0"; @@ -875,6 +1036,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 (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 }