From 5dce6b9282054c5d25f1a818adbf5d96e0a13ce4 Mon Sep 17 00:00:00 2001 From: Koda YeenBean Date: Sun, 12 Jul 2026 10:57:05 +0000 Subject: [PATCH] fixed fast roaming issue added Handoff Suggestion & Band Steering updated Firmware version cleaned up README --- README.md | 100 +++++++++--------- legend.md | 2 - src/inform.c | 12 +++ src/models.c | 4 +- src/wlan.c | 278 +++++++++++++++++++++++++++++++++++++++++++++------ 5 files changed, 307 insertions(+), 89 deletions(-) delete mode 100644 legend.md diff --git a/README.md b/README.md index b281071..869771e 100644 --- a/README.md +++ b/README.md @@ -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 - -``` \ No newline at end of file +Client Connection Quality \ No newline at end of file diff --git a/legend.md b/legend.md deleted file mode 100644 index cc5191f..0000000 --- a/legend.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/src/inform.c b/src/inform.c index ec46dca..21d9a87 100644 --- a/src/inform.c +++ b/src/inform.c @@ -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", diff --git a/src/models.c b/src/models.c index 6fa3263..617180e 100644 --- a/src/models.c +++ b/src/models.c @@ -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, diff --git a/src/wlan.c b/src/wlan.c index 66f09f4..23a8f8b 100644 --- a/src/wlan.c +++ b/src/wlan.c @@ -75,6 +75,7 @@ #include #include #include +#include #include #include @@ -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)