fixed fast roaming issue #6

Merged
Koda merged 1 commits from fast-roaming into main 2026-07-12 11:59:34 +01:00
5 changed files with 307 additions and 89 deletions
+48 -52
View File
@@ -1,6 +1,6 @@
# openUF — C
Daemon that makes an OpenWrt router appear as a **UniFi U6 InWall** to the UniFi Network controller.
Daemon that makes an OpenWrt router appear as a UniFi AP to UniFi Network controllers.
## Implemented Features
@@ -20,75 +20,71 @@ Daemon that makes an OpenWrt router appear as a **UniFi U6 InWall** to the UniFi
| **LLDP Send** | Custom frames via AF_PACKET raw socket | `lldp.c``lldp_send_frame()` |
| **LLDP Read** | Neighbors for UniFi topology | `lldp.c``lldpctl -f json` |
## Quick Installation
## Using the dev container
```sh
# On the OpenWrt device:
opkg update
opkg install gcc make libmbedtls-dev libuci-dev libjson-c-dev
```shell
# OpenWRT build directory
cd /home/openwrt/openwrt
# Compile and install
make -f Makefile.standalone install
# clean Previous builds
make clean
# Configure
vi /etc/openuf/openuf.conf # adjust controller_ip and lan_if
# Compile
make
```
# Start
/etc/init.d/openuf start
/etc/init.d/openuf enable # start on boot
## Changing Compiler Settings
```shell
# OpenWRT build directory
cd /home/openwrt/openwrt
# Change settings
make menuconfig
# export settings
./scripts/diffconfig.sh
# Paste settings into .devcontainer/openwrt.config file in the code
# Then rebuild the dev container by pressing F1 in VScode
# and typing rebuild and selecting rebuild dev container
```
## Configuration
On The Accesspoint Run this to install the package:
```shell
# Remove old installation
apk -r del openuf
# Install new version
apk add openuf-0.4.0-r1.apk --allow-untrusted
# Configure
vi /etc/openuf/openuf.conf
```
Example Configuration
```ini
controller_ip = 192.168.1.1 # UniFi controller IP
lan_if = br-lan # LAN interface (for MAC and IP)
lan_if = br-lan # Default Lan interface Name
ufmodel = u6-inwall # emulated model
inform_interval = 10 # seconds between informs
inform_interval = 10 # inform interval
enable_announce = 1
enable_inform = 1
enable_inform = 1 # Enable logging to /var/log/openuf.log if set to 1
```
## U6 InWall Model
---
A **U6 IW** is emulated because it has 5 GbE ports, which covers most OpenWrt routers. The model reports:
## Glassary
* 5 ethernet ports (eth0-eth4)
* 2.4 GHz WiFi 6 Radio (HE/802.11ax)
* 5 GHz WiFi 6 Radio (HE/802.11ax)
### TNBU
## LLDP
TNBU is the magic string/identifier at the start of the binary packet format used in this custom Inform protocol implementation.
For visual topology in UniFi:
### CCQ
```sh
opkg install lldpd
/etc/init.d/lldpd start
/etc/init.d/lldpd enable
```
openuf sends its own LLDP frames even without lldpd (raw socket).
With lldpd installed, it also reports upstream neighbors (switches).
## Adoption
The process is automatic:
1. The AP appears as "Pending" in UniFi.
2. Click on "Adopt" → the controller sends a new key.
3. The AP applies the key and becomes "Connected".
4. The controller pushes the WiFi configuration (SSIDs, channels, etc.).
5. Make sure your WiFi Name doesn't have characters like " - " cause of bad JSON! Grrr
To reset: `rm /etc/openuf/state.json && reboot`
## Dependencies
```sh
opkg install libmbedtls libuci libjson-c
opkg install lldpd # optional, for topology
```
Client Connection Quality
-2
View File
@@ -1,2 +0,0 @@
TNBU - TNBU is the magic string/identifier at the start of the binary packet format used in this custom Inform protocol implementation.
CCQ Client Connection Quality
+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,
+244 -32
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);
/* 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);
} else {
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 0);
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 0);
}
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)