fixed vlan config + 5Ghz Radio #2

Merged
Koda merged 1 commits from vlans into main 2026-07-11 19:57:01 +01:00
5 changed files with 200 additions and 32 deletions
+1
View File
@@ -0,0 +1 @@
**/.env
+2 -2
View File
@@ -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
+2 -1
View File
@@ -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;
+5 -2
View File
@@ -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",
+190 -27
View File
@@ -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
}