fixed fast roaming issue (#6)

added Handoff Suggestion & Band Steering
updated Firmware version
cleaned up README

Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
2026-07-12 11:59:34 +01:00
parent 59274da4e5
commit 4ced23b93d
5 changed files with 307 additions and 89 deletions
+12
View File
@@ -363,6 +363,8 @@ static struct json_object *build_vap_table(const uf_model_t *m)
const char *ifname = NULL;
int vlan_id = 0;
int is_11r = 0;
int band_steering = 0;
int handoff_suggestions = 0;
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);
@@ -373,6 +375,10 @@ static struct json_object *build_vap_table(const uf_model_t *m)
if (json_object_object_get_ex(vap, "vlan_id", &v)) vlan_id = json_object_get_int(v);
if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v))
is_11r = json_object_get_boolean(v);
if (json_object_object_get_ex(vap, "band_steering", &v))
band_steering = json_object_get_boolean(v);
if (json_object_object_get_ex(vap, "handoff_suggestions", &v))
handoff_suggestions = json_object_get_boolean(v);
/* Map band → wlan interface and current channel */
char wlan_iface[32] = "phy0-ap0";
@@ -427,6 +433,12 @@ static struct json_object *build_vap_table(const uf_model_t *m)
json_object_new_int(channel));
json_object_object_add(o, "tx_power",
json_object_new_int(tx_pwr));
json_object_object_add(o, "band_steering",
json_object_new_boolean(band_steering));
json_object_object_add(o, "bss_transition",
json_object_new_boolean(handoff_suggestions));
json_object_object_add(o, "handoff_suggestions",
json_object_new_boolean(handoff_suggestions));
json_object_object_add(o, "num_sta",
json_object_new_int(num_sta));
json_object_object_add(o, "rx_bytes",
+2 -2
View File
@@ -158,8 +158,8 @@ static const uf_radio_map_t uapg2aclr_rmap[] = {
const uf_model_t model_uapg2aclr = {
.model="U2IW", .model_display="UAP-AC-LR", .display_name="UAP-AC-LR",
.platform="U2IW", .board_rev=2, .has_eth1=false,
.fw_pre="U2IW.qca956x.v", .fw_ver="6.7.54.15663",
.fw_buildtime="260615.1200", .fw_factoryver="6.7.54.15663",
.fw_pre="U2IW.qca956x.v", .fw_ver="5.43.31.15663",
.fw_buildtime="260615.1200", .fw_factoryver="5.43.31.15663",
.radio_table=uapg2aclr_radios, .radio_table_len=2,
.port_table=uapg2aclr_ports, .port_table_len=1,
.ethernet_table=uapg2aclr_eth, .ethernet_table_len=1,
+245 -33
View File
@@ -75,6 +75,7 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <uci.h>
#include <json-c/json.h>
@@ -134,6 +135,60 @@ static void mobility_domain_for_ssid(const char *ssid, char out[5])
snprintf(out, 5, "%04x", (hash ^ (hash >> 16)) & 0xffffu);
}
/* Return true when an OpenWrt radio is backed by the ath9k kernel driver. */
static int 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;
}
/* Detect a full hostapd/wpad package built with 802.11v WNM support. */
static int hostapd_supports_bss_transition(void)
{
static int cached = -1;
if (cached >= 0)
return cached;
cached = 0;
/*
* Do not search binary strings: basic wpad contains the option name in
* ancillary code even though its configuration parser rejects it.
*/
FILE *probe = popen(
"(apk info -e wpad >/dev/null 2>&1 || "
"apk info -e wpad-mbedtls >/dev/null 2>&1 || "
"apk info -e wpad-openssl >/dev/null 2>&1 || "
"apk info -e wpad-wolfssl >/dev/null 2>&1 || "
"apk info -e hostapd-mbedtls >/dev/null 2>&1 || "
"apk info -e hostapd-openssl >/dev/null 2>&1 || "
"apk info -e hostapd-wolfssl >/dev/null 2>&1 || "
"opkg status wpad 2>/dev/null | grep -q 'install ok installed' || "
"opkg status wpad-mbedtls 2>/dev/null | grep -q 'install ok installed' || "
"opkg status wpad-openssl 2>/dev/null | grep -q 'install ok installed' || "
"opkg status wpad-wolfssl 2>/dev/null | grep -q 'install ok installed') "
"&& echo true",
"r");
if (probe) {
char value[16] = {0};
if (fgets(value, sizeof(value), probe) &&
!strncmp(value, "true", 4))
cached = 1;
pclose(probe);
}
return cached;
}
/* Read a UniFi boolean while accepting names used by controller versions. */
static int json_boolean_any(struct json_object *object,
const char *const *keys, size_t key_count)
@@ -144,22 +199,34 @@ static int json_boolean_any(struct json_object *object,
continue;
if (json_object_is_type(value, json_type_string)) {
const char *text = json_object_get_string(value);
return !strcasecmp(text, "enabled") || !strcasecmp(text, "true") ||
!strcasecmp(text, "on") || !strcmp(text, "1");
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;
}
/* Safe UCI section name (maximum 15 characters). */
/* Interpret Boolean text and UniFi feature modes such as "prefer_5g". */
static int feature_text_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). */
static void 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=='_'||c=='-')
(c>='0'&&c<='9')||c=='_')
out[j++] = c;
else
out[j++] = '_';
@@ -176,9 +243,11 @@ static int uci_set_val(struct uci_context *ctx,
if (!p) return -1;
sprintf(p, "%s=%s", path, val);
int ret = uci_lookup_ptr(ctx, &ptr, p, true);
if (ret == UCI_OK)
ret = uci_set(ctx, &ptr);
/* ptr.value may point inside p, so free it only after uci_set(). */
free(p);
if (ret != UCI_OK) return -1;
return (uci_set(ctx, &ptr) == UCI_OK) ? 0 : -1;
return ret == 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,
@@ -209,9 +278,11 @@ static int uci_add_list_val(struct uci_context *ctx,
if (!assignment) return -1;
sprintf(assignment, "%s=%s", path, val);
int ret = uci_lookup_ptr(ctx, &ptr, assignment, true);
if (ret == UCI_OK)
ret = uci_add_list(ctx, &ptr);
/* ptr.value may point inside assignment. */
free(assignment);
if (ret != UCI_OK) return -1;
return uci_add_list(ctx, &ptr) == UCI_OK ? 0 : -1;
return ret == UCI_OK ? 0 : -1;
}
@@ -509,6 +580,7 @@ static int apply_vap(struct uci_context *ctx,
struct uci_package *pkg,
struct json_object *vap_json,
const char *device_name,
const char *radio_band,
const char *mac_str,
int vap_idx)
{
@@ -616,6 +688,20 @@ static int apply_vap(struct uci_context *ctx,
};
int ft = json_boolean_any(vap_json, ft_keys,
sizeof(ft_keys) / sizeof(ft_keys[0]));
/*
* This legacy 2.4 GHz ath9k PHY rejects every FT beacon tested, including
* WPA2 with PMF disabled. Preserve the controller request for telemetry,
* but disable 802.11r on this one unsupported PHY so the BSS can start.
*/
if (ft && radio_band && !strcmp(radio_band, "ng") &&
radio_uses_ath9k(device_name)) {
UCI_SET_INT(ctx, "wireless", sec_name, "openuf_ft_requested", 1);
ft = 0;
printf("[openuf] Disabled FT on unsupported 2.4 GHz ath9k radio %s\n",
device_name);
}
if (ft) {
char mdomain[5];
mobility_domain_for_ssid(essid, mdomain);
@@ -628,43 +714,77 @@ static int apply_vap(struct uci_context *ctx,
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 0);
}
/* ── Band Steering (802.11k/v) ────────────────────────────────
* 802.11k: Neighbor Reports → the AP tells the client what other
* APs exist to facilitate roaming.
* 802.11v: BSS Transition Management → the AP can "suggest" to the
* client to move to another AP with better signal.*/
int band_steer = 0;
if (json_object_object_get_ex(vap_json, "band_steering", &v))
band_steer = json_object_get_boolean(v) ? 1 : 0;
if (band_steer) {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", 1);
UCI_SET_INT(ctx, "wireless", sec_name, "bss_transition", 1);
} else {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 0);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 0);
}
/* Enable the hostapd capabilities used by steering and 802.11v hints. */
const char *band_steer_keys[] = {
"band_steering", "band_steering_enabled", "steering_enabled"
};
const char *handoff_keys[] = {
"bss_transition", "bss_transition_enabled",
"bss_transition_management", "handoff_suggestions",
"handoff_suggestions_enabled", "ieee80211v"
};
int band_steer = json_boolean_any(
vap_json, band_steer_keys,
sizeof(band_steer_keys) / sizeof(band_steer_keys[0]));
int handoff = json_boolean_any(
vap_json, handoff_keys,
sizeof(handoff_keys) / sizeof(handoff_keys[0]));
int rrm = band_steer || handoff;
int bss_transition_requested = band_steer || handoff;
int bss_transition = bss_transition_requested &&
hostapd_supports_bss_transition();
UCI_SET_INT(ctx, "wireless", sec_name, "openuf_band_steering",
band_steer);
UCI_SET_INT(ctx, "wireless", sec_name, "openuf_handoff_suggestions",
handoff);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", rrm);
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", rrm);
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_beacon_report", rrm);
/*
* The ucode Wi-Fi backend serializes even bss_transition=0. Reduced wpad
* rejects the unknown option, so omit it completely unless WNM exists.
*/
if (bss_transition)
UCI_SET_INT(ctx, "wireless", sec_name, "bss_transition", 1);
if (bss_transition_requested && !bss_transition)
printf("[openuf] Handoff Suggestions requested, but installed "
"hostapd lacks 802.11v WNM support; install wpad-mbedtls\n");
/* Record the controller VLAN for telemetry and diagnostics. */
if (vid > 0)
UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid);
/* Reassert the binding last and reject incomplete UCI sections. */
/* Reassert and validate every option required to start a secure AP. */
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;
}
if (uci_set_required(ctx, pkg, sec_name, "mode", "ap") != 0 ||
uci_set_required(ctx, pkg, sec_name, "ssid", essid) != 0 ||
uci_set_required(ctx, pkg, sec_name, "encryption",
sec_to_uci(security)) != 0) {
printf("[openuf] Refusing incomplete VAP '%s': core AP options "
"could not be stored\n", essid);
return -1;
}
if (pass && pass[0] && strcmp(security, "open") != 0 &&
uci_set_required(ctx, pkg, sec_name, "key", pass) != 0) {
printf("[openuf] Refusing unsecured VAP '%s': key could not be stored\n",
essid);
return -1;
}
if (uci_set_required(ctx, pkg, sec_name, "network", target_network) != 0) {
printf("[openuf] Refusing unsafe VAP '%s': cannot bind to %s\n",
essid, target_network);
return -1;
}
printf("[openuf] VAP '%s' -> %s device=%s network=%s enc=%s ft=%d bs=%d pmf=%d\n",
printf("[openuf] VAP '%s' -> %s device=%s network=%s enc=%s "
"ft=%d bs=%d handoff=%d pmf=%d\n",
essid, sec_name, device_name, target_network, sec_to_uci(security),
ft, band_steer, pmf);
ft, band_steer, handoff, pmf);
return 0;
}
@@ -790,6 +910,7 @@ int wlan_apply_config(struct json_object *config_json,
continue;
int section_idx = i * model->radio_map_len + j;
if (apply_vap(ctx, pkg, vap, model->radio_map[j].device,
model->radio_map[j].band,
mac_str, section_idx) != 0) {
uci_unload(ctx, pkg);
uci_free_context(ctx);
@@ -821,10 +942,61 @@ int wlan_apply_config(struct json_object *config_json,
uci_unload(ctx, pkg);
uci_free_context(ctx);
/* Reload netifd for generated VLAN devices, then start only the new VAPs. */
printf("[openuf] Starting controller-managed Wi-Fi...\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
* setup after ACS. Start and verify each PHY independently, retrying a failed radio so
* provisioning cannot leave one band visible but unusable.
*/
printf("[openuf] Starting controller-managed Wi-Fi sequentially...\n");
system("ubus call network reload >/dev/null 2>&1");
system("wifi up >/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;
/* Model radio names are internal constants, but validate defensively. */
if (!device ||
strspn(device,
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") !=
strlen(device)) {
printf("[openuf] Refusing invalid radio name\n");
continue;
}
int radio_up = 0;
for (int attempt = 1; attempt <= 2 && !radio_up; attempt++) {
printf("[openuf] Starting %s (%s), attempt %d...\n",
device, model->radio_map[i].band, attempt);
snprintf(command, sizeof(command),
"wifi up %s >/dev/null 2>&1", device);
system(command);
/* ACS normally takes 6-8 seconds on this ath9k hardware. */
sleep(10);
int phy_index = -1;
if (sscanf(device, "radio%d", &phy_index) != 1)
phy_index = -1;
snprintf(command, sizeof(command),
"iw dev phy%d-ap0 info 2>/dev/null | "
"grep -q '^[[:space:]]*ssid ' && echo true",
phy_index);
FILE *status = popen(command, "r");
if (status) {
char value[16] = {0};
if (fgets(value, sizeof(value), status) &&
!strncmp(value, "true", 4))
radio_up = 1;
pclose(status);
}
if (!radio_up)
printf("[openuf] %s did not reach the up state; retrying\n",
device);
}
if (!radio_up)
printf("[openuf] %s failed after 2 start attempts\n", device);
}
return 0;
}
@@ -946,6 +1118,36 @@ int wlan_apply_system_cfg(const char *system_cfg,
json_object_object_add(vap, "fast_roaming_enabled",
json_object_new_boolean(!strcmp(value, "enabled")));
const char *band_steer_suffixes[] = {
"band_steering", "band_steering_enabled", "steering"
};
for (size_t n = 0;
n < sizeof(band_steer_suffixes) /
sizeof(band_steer_suffixes[0]); n++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
band_steer_suffixes[n]);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "band_steering",
json_object_new_boolean(feature_text_enabled(value)));
break;
}
}
const char *handoff_suffixes[] = {
"bss_transition", "bss_transition_enabled",
"handoff_suggestions", "handoff_suggestions_enabled"
};
for (size_t n = 0;
n < sizeof(handoff_suffixes) / sizeof(handoff_suffixes[0]); n++) {
snprintf(key, sizeof(key), "aaa.%d.%s", i,
handoff_suffixes[n]);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
json_object_object_add(vap, "bss_transition",
json_object_new_boolean(feature_text_enabled(value)));
break;
}
}
snprintf(key, sizeof(key), "aaa.%d.pmf.mode", i);
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
const char *pmf = !strcmp(value, "2") ? "required" :
@@ -1055,7 +1257,11 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
const char *enc = UCI_GET("encryption");
const char *dis = UCI_GET("disabled");
const char *r11 = UCI_GET("ieee80211r");
const char *ft_req = UCI_GET("openuf_ft_requested");
const char *k11 = UCI_GET("ieee80211k");
const char *btm = UCI_GET("bss_transition");
const char *bs_req = UCI_GET("openuf_band_steering");
const char *ho_req = UCI_GET("openuf_handoff_suggestions");
const char *w11 = UCI_GET("ieee80211w");
const char *hidden = UCI_GET("hidden");
const char *vap_id = UCI_GET("openuf_vap_id");
@@ -1099,8 +1305,12 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
if (!strcmp(w11,"2")) pmf = "required";
}
bool ft_on = (r11 && !strcmp(r11,"1"));
bool bs_on = (k11 && !strcmp(k11,"1"));
bool ft_on = (r11 && !strcmp(r11,"1")) ||
(ft_req && !strcmp(ft_req,"1"));
bool bs_on = bs_req ? !strcmp(bs_req, "1") :
(k11 && !strcmp(k11, "1"));
bool handoff_on = ho_req ? !strcmp(ho_req, "1") :
(btm && !strcmp(btm, "1"));
bool hid = (hidden && !strcmp(hidden,"1"));
bool up = !(dis && !strcmp(dis,"1"));
@@ -1115,6 +1325,8 @@ struct json_object *wlan_get_vap_table(const uf_model_t *model)
json_object_object_add(o, "hide_ssid", json_object_new_boolean(hid));
json_object_object_add(o, "fast_roaming_enabled",json_object_new_boolean(ft_on));
json_object_object_add(o, "band_steering", json_object_new_boolean(bs_on));
json_object_object_add(o, "bss_transition", json_object_new_boolean(handoff_on));
json_object_object_add(o, "handoff_suggestions", json_object_new_boolean(handoff_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 (vlan && atoi(vlan) > 0)