From e4ffbbd5f9030aaa981e24dd2fc17133e380c28b Mon Sep 17 00:00:00 2001 From: Koda YeenBean Date: Sun, 12 Jul 2026 13:40:07 +0100 Subject: [PATCH] added dynamic radio assignment (#9) fixed band steering Reviewed-on: https://git.ascheu.de/Koda/OpenUniFi/pulls/9 --- .devcontainer/openwrt.config | 3 +- Makefile | 2 +- README.md | 4 +- src/inform.c | 9 +- src/wlan.c | 247 ++++++++++++++++++++++++++++++++--- src/wlan.h | 5 + 6 files changed, 248 insertions(+), 22 deletions(-) diff --git a/.devcontainer/openwrt.config b/.devcontainer/openwrt.config index 5166500..44bdf31 100644 --- a/.devcontainer/openwrt.config +++ b/.devcontainer/openwrt.config @@ -2,4 +2,5 @@ CONFIG_TARGET_mpc85xx=y CONFIG_TARGET_mpc85xx_p1020=y CONFIG_TARGET_mpc85xx_p1020_DEVICE_hpe_msm460=y CONFIG_PACKAGE_kmod-tun=m -CONFIG_PACKAGE_openuf=m \ No newline at end of file +CONFIG_PACKAGE_openuf=m +CONFIG_PACKAGE_usteer=m \ No newline at end of file diff --git a/Makefile b/Makefile index b185858..960d6fe 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ define Package/openuf SECTION := net CATEGORY := Network TITLE := openUF — UniFi bridge daemon for OpenWrt - DEPENDS := +libmbedtls +libuci +libjson-c +kmod-tun + DEPENDS := +libmbedtls +libuci +libjson-c +kmod-tun +usteer URL := https://git.ascheu.de/Koda/OpenUniFi endef diff --git a/README.md b/README.md index 869771e..2202f3d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# openUF — C +# openUF Daemon that makes an OpenWrt router appear as a UniFi AP to UniFi Network controllers. @@ -79,7 +79,7 @@ enable_inform = 1 # Enable logging to /var/log/openuf.log if set t --- -## Glassary +## Glossary ### TNBU diff --git a/src/inform.c b/src/inform.c index 21d9a87..c27efec 100644 --- a/src/inform.c +++ b/src/inform.c @@ -228,11 +228,13 @@ static struct json_object *build_radio_table_stats(const uf_model_t *m) for (int i = 0; i < m->radio_map_len; i++) { const uf_radio_map_t *rm = &m->radio_map[i]; + const char *device = wlan_device_for_band(m, rm->band); + if (!device) device = rm->device; /* Map "radio0" → "wlan0" by OpenWrt convention */ char wlan_iface[32]; int ridx = 0; - sscanf(rm->device, "radio%d", &ridx); + sscanf(device, "radio%d", &ridx); snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx); /* Radio name in the static table */ @@ -388,7 +390,10 @@ static struct json_object *build_vap_table(const uf_model_t *m) 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); + const char *device = wlan_device_for_band( + m, m->radio_map[j].band); + if (!device) device = m->radio_map[j].device; + sscanf(device, "radio%d", &idx); if (!ifname || !ifname[0]) snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx); radio_stats_t rs; diff --git a/src/wlan.c b/src/wlan.c index 23a8f8b..86895e0 100644 --- a/src/wlan.c +++ b/src/wlan.c @@ -82,6 +82,147 @@ #include "wlan.h" #include "ufmodel.h" +#define MAX_RESOLVED_RADIOS 8 + +typedef struct { + char device[32]; + unsigned int bands; +} radio_capability_t; + +static const uf_model_t *resolved_model; +static char resolved_devices[MAX_RESOLVED_RADIOS][32]; + +static unsigned int band_bit(const char *band) +{ + if (!band) return 0; + if (!strcmp(band, "ng") || !strcmp(band, "2g")) return 1u; + if (!strcmp(band, "na") || !strcmp(band, "5g")) return 2u; + if (!strcmp(band, "6g") || !strcmp(band, "6GHz")) return 4u; + return 0; +} + +static int bit_count(unsigned int value) +{ + int count = 0; + while (value) { + count += value & 1u; + value >>= 1; + } + return count; +} + +/* OpenWrt's generated radioN and phyN indices correspond for mac80211 + * devices. Read actual frequencies instead of assuming PHY band order. */ +static unsigned int detect_radio_bands(const char *device) +{ + int phy_index = -1; + char command[96]; + char line[256]; + unsigned int bands = 0; + + if (!device || sscanf(device, "radio%d", &phy_index) != 1 || phy_index < 0) + return 0; + + snprintf(command, sizeof(command), "iw phy phy%d info 2>/dev/null", phy_index); + FILE *pipe = popen(command, "r"); + if (!pipe) return 0; + + while (fgets(line, sizeof(line), pipe)) { + char *mhz = strstr(line, " MHz ["); + if (!mhz || strstr(line, "(disabled)")) + continue; + + char *start = mhz; + while (start > line && + ((start[-1] >= '0' && start[-1] <= '9') || start[-1] == '.')) + start--; + double frequency = strtod(start, NULL); + if (frequency >= 2300.0 && frequency < 3000.0) + bands |= 1u; + else if (frequency >= 4900.0 && frequency < 5925.0) + bands |= 2u; + else if (frequency >= 5925.0 && frequency < 7200.0) + bands |= 4u; + } + pclose(pipe); + return bands; +} + +static void resolve_radio_map(const uf_model_t *model) +{ + if (!model || resolved_model == model) + return; + + memset(resolved_devices, 0, sizeof(resolved_devices)); + resolved_model = model; + + int count = model->radio_map_len; + if (count > MAX_RESOLVED_RADIOS) + count = MAX_RESOLVED_RADIOS; + + radio_capability_t caps[MAX_RESOLVED_RADIOS] = {0}; + int used[MAX_RESOLVED_RADIOS] = {0}; + for (int i = 0; i < count; i++) { + snprintf(caps[i].device, sizeof(caps[i].device), "%s", + model->radio_map[i].device); + caps[i].bands = detect_radio_bands(caps[i].device); + } + + for (int i = 0; i < count; i++) { + unsigned int wanted = band_bit(model->radio_map[i].band); + int best = -1; + int best_band_count = 99; + + for (int j = 0; j < count; j++) { + if (used[j] || !(caps[j].bands & wanted)) + continue; + int supported = bit_count(caps[j].bands); + if (supported < best_band_count) { + best = j; + best_band_count = supported; + } + } + + if (best >= 0) { + used[best] = 1; + snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s", + caps[best].device); + } else { + snprintf(resolved_devices[i], sizeof(resolved_devices[i]), "%s", + model->radio_map[i].device); + } + + printf("[openuf] Radio mapping: %s -> %s%s\n", + model->radio_map[i].band, resolved_devices[i], + best >= 0 ? " (detected)" : " (model fallback)"); + } +} + +const char *wlan_device_for_band(const uf_model_t *model, const char *band) +{ + if (!model || !band) return NULL; + resolve_radio_map(model); + for (int i = 0; i < model->radio_map_len; i++) + if (!strcmp(model->radio_map[i].band, band)) + return i < MAX_RESOLVED_RADIOS && resolved_devices[i][0] + ? resolved_devices[i] : model->radio_map[i].device; + return NULL; +} + +const char *wlan_band_for_device(const uf_model_t *model, const char *device) +{ + if (!model || !device) return NULL; + resolve_radio_map(model); + for (int i = 0; i < model->radio_map_len; i++) { + const char *mapped = i < MAX_RESOLVED_RADIOS && resolved_devices[i][0] + ? resolved_devices[i] + : model->radio_map[i].device; + if (!strcmp(mapped, device)) + return model->radio_map[i].band; + } + return NULL; +} + /* ─── Mapeo de seguridad UniFi → OpenWrt UCI ────────────────────── */ static const char *sec_to_uci(const char *uf) { @@ -430,6 +571,68 @@ static int ensure_vlan_network(int vid) return ok ? 0 : -1; } +/* + * Configure OpenWrt's steering policy engine. The hostapd 802.11k/v flags + * only expose measurements and transition commands; they do not decide when + * a station should move. usteer supplies that missing policy loop. + */ +static int configure_band_steering(int enabled) +{ + struct uci_context *ctx = uci_alloc_context(); + if (!ctx) + return -1; + + struct uci_package *pkg = NULL; + if (uci_load(ctx, "usteer", &pkg) != UCI_OK) { + printf("[openuf] Cannot load /etc/config/usteer\n"); + uci_free_context(ctx); + return -1; + } + + struct uci_section *settings = NULL; + struct uci_element *element; + uci_foreach_element(&pkg->sections, element) { + struct uci_section *section = uci_to_section(element); + if (!strcmp(section->type, "usteer")) { + settings = section; + break; + } + } + + if (!settings) { + if (uci_ensure_section(ctx, pkg, "openuf", "usteer") != 0) { + uci_unload(ctx, pkg); + uci_free_context(ctx); + return -1; + } + settings = uci_lookup_section(ctx, pkg, "openuf"); + } + + if (!settings) { + uci_unload(ctx, pkg); + uci_free_context(ctx); + return -1; + } + + /* + * A zero interval disables higher-band steering. A zero station-count + * threshold is important for small networks: usteer's default of five + * otherwise prevents a lone client from being considered. The signal + * floor avoids pushing a client onto 5 GHz when that link is too weak. + */ + UCI_SET(ctx, "usteer", settings->e.name, "band_steering_interval", + enabled ? "30000" : "0"); + UCI_SET(ctx, "usteer", settings->e.name, "band_steering_threshold", "0"); + UCI_SET(ctx, "usteer", settings->e.name, "band_steering_min_snr", "-65"); + + int ok = uci_commit(ctx, &pkg, false) == UCI_OK; + uci_unload(ctx, pkg); + uci_free_context(ctx); + printf("[openuf] Band steering policy %s (usteer)\n", + enabled ? "enabled" : "disabled"); + return ok ? 0 : -1; +} + /* ═══════════════════════════════════════════════════════════════════ wlan_clear — remove all VAPs before applying controller ownership ═══════════════════════════════════════════════════════════════════ */ @@ -836,12 +1039,11 @@ int wlan_apply_config(struct json_object *config_json, 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; - } + const char *device_name = wlan_device_for_band(model, radio_band); + if (!device_name) { + printf("[openuf] Ignoring settings for unknown radio '%s'\n", + radio_band); + continue; } wlan_apply_radio(r, device_name); } @@ -886,13 +1088,22 @@ int wlan_apply_config(struct json_object *config_json, printf("[openuf] Disabled %d default OpenWrt VAPs\n", disabled_defaults); - /* 3. Create VAPs */ + /* 3. Create VAPs and determine whether any WLAN requests steering. */ + int band_steering_enabled = 0; 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; + const char *band_steer_keys[] = { + "band_steering", "band_steering_enabled", "steering_enabled" + }; + if (json_boolean_any(vap, band_steer_keys, + sizeof(band_steer_keys) / + sizeof(band_steer_keys[0]))) + band_steering_enabled = 1; + /* 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)) @@ -909,7 +1120,9 @@ int wlan_apply_config(struct json_object *config_json, 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, + const char *device = wlan_device_for_band( + model, model->radio_map[j].band); + if (!device || apply_vap(ctx, pkg, vap, device, model->radio_map[j].band, mac_str, section_idx) != 0) { uci_unload(ctx, pkg); @@ -942,6 +1155,9 @@ int wlan_apply_config(struct json_object *config_json, uci_unload(ctx, pkg); uci_free_context(ctx); + if (configure_band_steering(band_steering_enabled) != 0) + printf("[openuf] Failed to configure the band steering policy\n"); + /* * Reload netifd for generated VLAN devices, then bring the radios up one * at a time. Some dual-ath9k devices intermittently fail their first beacon @@ -952,7 +1168,8 @@ int wlan_apply_config(struct json_object *config_json, system("ubus call network reload >/dev/null 2>&1"); for (int i = 0; i < model->radio_map_len; i++) { char command[256]; - const char *device = model->radio_map[i].device; + const char *device = wlan_device_for_band( + model, model->radio_map[i].band); /* Model radio names are internal constants, but validate defensively. */ if (!device || @@ -997,6 +1214,9 @@ int wlan_apply_config(struct json_object *config_json, if (!radio_up) printf("[openuf] %s failed after 2 start attempts\n", device); } + + /* Restart after hostapd has registered both BSSes on ubus. */ + system("/etc/init.d/usteer restart >/dev/null 2>&1"); return 0; } @@ -1271,13 +1491,8 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model) if (!device) device = "radio0"; /* Band of this 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; - } - } + const char *radio_band = wlan_band_for_device(model, device); + if (!radio_band) radio_band = "ng"; /* Resolve the actual netifd interface (for example phy1-ap0). */ char wlan_iface[32]; diff --git a/src/wlan.h b/src/wlan.h index fb20580..33de5d4 100644 --- a/src/wlan.h +++ b/src/wlan.h @@ -19,6 +19,11 @@ void wlan_clear(void); void wlan_apply_radio(struct json_object *radio_json, const char *device_name); +/* Resolve the model's logical UniFi band against the bands advertised by + * the local PHYs. Falls back to the model mapping when discovery fails. */ +const char *wlan_device_for_band(const uf_model_t *model, const char *band); +const char *wlan_band_for_device(const uf_model_t *model, const char *device); + /* Apply full config pushed by controller (setstate). * config_json: decoded setstate JSON object * model : model descriptor for radio_map lookup */