added dynamic radio assignment
fixed band steering
This commit is contained in:
@@ -2,4 +2,5 @@ CONFIG_TARGET_mpc85xx=y
|
|||||||
CONFIG_TARGET_mpc85xx_p1020=y
|
CONFIG_TARGET_mpc85xx_p1020=y
|
||||||
CONFIG_TARGET_mpc85xx_p1020_DEVICE_hpe_msm460=y
|
CONFIG_TARGET_mpc85xx_p1020_DEVICE_hpe_msm460=y
|
||||||
CONFIG_PACKAGE_kmod-tun=m
|
CONFIG_PACKAGE_kmod-tun=m
|
||||||
CONFIG_PACKAGE_openuf=m
|
CONFIG_PACKAGE_openuf=m
|
||||||
|
CONFIG_PACKAGE_usteer=m
|
||||||
@@ -13,7 +13,7 @@ define Package/openuf
|
|||||||
SECTION := net
|
SECTION := net
|
||||||
CATEGORY := Network
|
CATEGORY := Network
|
||||||
TITLE := openUF — UniFi bridge daemon for OpenWrt
|
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
|
URL := https://git.ascheu.de/Koda/OpenUniFi
|
||||||
endef
|
endef
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# openUF — C
|
# openUF
|
||||||
|
|
||||||
Daemon that makes an OpenWrt router appear as a UniFi AP to UniFi Network controllers.
|
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
|
### TNBU
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -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++) {
|
for (int i = 0; i < m->radio_map_len; i++) {
|
||||||
const uf_radio_map_t *rm = &m->radio_map[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 */
|
/* Map "radio0" → "wlan0" by OpenWrt convention */
|
||||||
char wlan_iface[32];
|
char wlan_iface[32];
|
||||||
int ridx = 0;
|
int ridx = 0;
|
||||||
sscanf(rm->device, "radio%d", &ridx);
|
sscanf(device, "radio%d", &ridx);
|
||||||
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
|
snprintf(wlan_iface, sizeof(wlan_iface), "wlan%d", ridx);
|
||||||
|
|
||||||
/* Radio name in the static table */
|
/* 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++) {
|
for (int j = 0; j < m->radio_map_len; j++) {
|
||||||
if (strcmp(m->radio_map[j].band, radio) == 0) {
|
if (strcmp(m->radio_map[j].band, radio) == 0) {
|
||||||
int idx = 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])
|
if (!ifname || !ifname[0])
|
||||||
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx);
|
snprintf(wlan_iface, sizeof(wlan_iface), "phy%d-ap0", idx);
|
||||||
radio_stats_t rs;
|
radio_stats_t rs;
|
||||||
|
|||||||
+231
-16
@@ -82,6 +82,147 @@
|
|||||||
#include "wlan.h"
|
#include "wlan.h"
|
||||||
#include "ufmodel.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 ────────────────────── */
|
/* ─── Mapeo de seguridad UniFi → OpenWrt UCI ────────────────────── */
|
||||||
static const char *sec_to_uci(const char *uf)
|
static const char *sec_to_uci(const char *uf)
|
||||||
{
|
{
|
||||||
@@ -430,6 +571,68 @@ static int ensure_vlan_network(int vid)
|
|||||||
return ok ? 0 : -1;
|
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
|
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 = "";
|
const char *radio_band = "";
|
||||||
if (json_object_object_get_ex(r, "radio", &v))
|
if (json_object_object_get_ex(r, "radio", &v))
|
||||||
radio_band = json_object_get_string(v);
|
radio_band = json_object_get_string(v);
|
||||||
const char *device_name = "radio0";
|
const char *device_name = wlan_device_for_band(model, radio_band);
|
||||||
for (int j = 0; j < model->radio_map_len; j++) {
|
if (!device_name) {
|
||||||
if (!strcmp(model->radio_map[j].band, radio_band)) {
|
printf("[openuf] Ignoring settings for unknown radio '%s'\n",
|
||||||
device_name = model->radio_map[j].device;
|
radio_band);
|
||||||
break;
|
continue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
wlan_apply_radio(r, device_name);
|
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",
|
printf("[openuf] Disabled %d default OpenWrt VAPs\n",
|
||||||
disabled_defaults);
|
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)) {
|
if (vt_arr && json_object_is_type(vt_arr, json_type_array)) {
|
||||||
int nv = json_object_array_length(vt_arr);
|
int nv = json_object_array_length(vt_arr);
|
||||||
for (int i = 0; i < nv; i++) {
|
for (int i = 0; i < nv; i++) {
|
||||||
struct json_object *vap = json_object_array_get_idx(vt_arr, i);
|
struct json_object *vap = json_object_array_get_idx(vt_arr, i);
|
||||||
if (!vap) continue;
|
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. */
|
/* A VAP without an explicit band is a model-wide WLAN. */
|
||||||
const char *radio_band = NULL;
|
const char *radio_band = NULL;
|
||||||
if (json_object_object_get_ex(vap, "radio", &v))
|
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))
|
strcmp(model->radio_map[j].band, radio_band))
|
||||||
continue;
|
continue;
|
||||||
int section_idx = i * model->radio_map_len + j;
|
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,
|
model->radio_map[j].band,
|
||||||
mac_str, section_idx) != 0) {
|
mac_str, section_idx) != 0) {
|
||||||
uci_unload(ctx, pkg);
|
uci_unload(ctx, pkg);
|
||||||
@@ -942,6 +1155,9 @@ int wlan_apply_config(struct json_object *config_json,
|
|||||||
uci_unload(ctx, pkg);
|
uci_unload(ctx, pkg);
|
||||||
uci_free_context(ctx);
|
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
|
* Reload netifd for generated VLAN devices, then bring the radios up one
|
||||||
* at a time. Some dual-ath9k devices intermittently fail their first beacon
|
* 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");
|
system("ubus call network reload >/dev/null 2>&1");
|
||||||
for (int i = 0; i < model->radio_map_len; i++) {
|
for (int i = 0; i < model->radio_map_len; i++) {
|
||||||
char command[256];
|
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. */
|
/* Model radio names are internal constants, but validate defensively. */
|
||||||
if (!device ||
|
if (!device ||
|
||||||
@@ -997,6 +1214,9 @@ int wlan_apply_config(struct json_object *config_json,
|
|||||||
if (!radio_up)
|
if (!radio_up)
|
||||||
printf("[openuf] %s failed after 2 start attempts\n", device);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1271,13 +1491,8 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
|
|||||||
if (!device) device = "radio0";
|
if (!device) device = "radio0";
|
||||||
|
|
||||||
/* Band of this radio */
|
/* Band of this radio */
|
||||||
const char *radio_band = "ng";
|
const char *radio_band = wlan_band_for_device(model, device);
|
||||||
for (int j = 0; j < model->radio_map_len; j++) {
|
if (!radio_band) radio_band = "ng";
|
||||||
if (!strcmp(model->radio_map[j].device, device)) {
|
|
||||||
radio_band = model->radio_map[j].band;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resolve the actual netifd interface (for example phy1-ap0). */
|
/* Resolve the actual netifd interface (for example phy1-ap0). */
|
||||||
char wlan_iface[32];
|
char wlan_iface[32];
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ void wlan_clear(void);
|
|||||||
void wlan_apply_radio(struct json_object *radio_json,
|
void wlan_apply_radio(struct json_object *radio_json,
|
||||||
const char *device_name);
|
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).
|
/* Apply full config pushed by controller (setstate).
|
||||||
* config_json: decoded setstate JSON object
|
* config_json: decoded setstate JSON object
|
||||||
* model : model descriptor for radio_map lookup */
|
* model : model descriptor for radio_map lookup */
|
||||||
|
|||||||
Reference in New Issue
Block a user