fixed fast roaming issue #6
@@ -1,6 +1,6 @@
|
|||||||
# openUF — C
|
# 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
|
## 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 Send** | Custom frames via AF_PACKET raw socket | `lldp.c` → `lldp_send_frame()` |
|
||||||
| **LLDP Read** | Neighbors for UniFi topology | `lldp.c` → `lldpctl -f json` |
|
| **LLDP Read** | Neighbors for UniFi topology | `lldp.c` → `lldpctl -f json` |
|
||||||
|
|
||||||
## Quick Installation
|
## Using the dev container
|
||||||
|
|
||||||
```sh
|
```shell
|
||||||
# On the OpenWrt device:
|
# OpenWRT build directory
|
||||||
opkg update
|
cd /home/openwrt/openwrt
|
||||||
opkg install gcc make libmbedtls-dev libuci-dev libjson-c-dev
|
|
||||||
|
|
||||||
# Compile and install
|
# clean Previous builds
|
||||||
make -f Makefile.standalone install
|
make clean
|
||||||
|
|
||||||
# Configure
|
# Compile
|
||||||
vi /etc/openuf/openuf.conf # adjust controller_ip and lan_if
|
make
|
||||||
|
```
|
||||||
|
|
||||||
# Start
|
## Changing Compiler Settings
|
||||||
/etc/init.d/openuf start
|
|
||||||
/etc/init.d/openuf enable # start on boot
|
|
||||||
|
|
||||||
|
```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
|
## 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
|
```ini
|
||||||
controller_ip = 192.168.1.1 # UniFi controller IP
|
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
|
ufmodel = u6-inwall # emulated model
|
||||||
inform_interval = 10 # seconds between informs
|
inform_interval = 10 # inform interval
|
||||||
enable_announce = 1
|
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)
|
### TNBU
|
||||||
* 2.4 GHz WiFi 6 Radio (HE/802.11ax)
|
|
||||||
* 5 GHz WiFi 6 Radio (HE/802.11ax)
|
|
||||||
|
|
||||||
## 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
|
Client Connection Quality
|
||||||
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
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
@@ -363,6 +363,8 @@ static struct json_object *build_vap_table(const uf_model_t *m)
|
|||||||
const char *ifname = NULL;
|
const char *ifname = NULL;
|
||||||
int vlan_id = 0;
|
int vlan_id = 0;
|
||||||
int is_11r = 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, "essid", &v)) essid = json_object_get_string(v);
|
||||||
if (json_object_object_get_ex(vap, "name", &v)) vap_name = 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, "vlan_id", &v)) vlan_id = json_object_get_int(v);
|
||||||
if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v))
|
if (json_object_object_get_ex(vap, "fast_roaming_enabled", &v))
|
||||||
is_11r = json_object_get_boolean(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 */
|
/* Map band → wlan interface and current channel */
|
||||||
char wlan_iface[32] = "phy0-ap0";
|
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_new_int(channel));
|
||||||
json_object_object_add(o, "tx_power",
|
json_object_object_add(o, "tx_power",
|
||||||
json_object_new_int(tx_pwr));
|
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_object_add(o, "num_sta",
|
||||||
json_object_new_int(num_sta));
|
json_object_new_int(num_sta));
|
||||||
json_object_object_add(o, "rx_bytes",
|
json_object_object_add(o, "rx_bytes",
|
||||||
|
|||||||
+2
-2
@@ -158,8 +158,8 @@ static const uf_radio_map_t uapg2aclr_rmap[] = {
|
|||||||
const uf_model_t model_uapg2aclr = {
|
const uf_model_t model_uapg2aclr = {
|
||||||
.model="U2IW", .model_display="UAP-AC-LR", .display_name="UAP-AC-LR",
|
.model="U2IW", .model_display="UAP-AC-LR", .display_name="UAP-AC-LR",
|
||||||
.platform="U2IW", .board_rev=2, .has_eth1=false,
|
.platform="U2IW", .board_rev=2, .has_eth1=false,
|
||||||
.fw_pre="U2IW.qca956x.v", .fw_ver="6.7.54.15663",
|
.fw_pre="U2IW.qca956x.v", .fw_ver="5.43.31.15663",
|
||||||
.fw_buildtime="260615.1200", .fw_factoryver="6.7.54.15663",
|
.fw_buildtime="260615.1200", .fw_factoryver="5.43.31.15663",
|
||||||
.radio_table=uapg2aclr_radios, .radio_table_len=2,
|
.radio_table=uapg2aclr_radios, .radio_table_len=2,
|
||||||
.port_table=uapg2aclr_ports, .port_table_len=1,
|
.port_table=uapg2aclr_ports, .port_table_len=1,
|
||||||
.ethernet_table=uapg2aclr_eth, .ethernet_table_len=1,
|
.ethernet_table=uapg2aclr_eth, .ethernet_table_len=1,
|
||||||
|
|||||||
+244
-32
@@ -75,6 +75,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
#include <uci.h>
|
#include <uci.h>
|
||||||
#include <json-c/json.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);
|
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. */
|
/* Read a UniFi boolean while accepting names used by controller versions. */
|
||||||
static int json_boolean_any(struct json_object *object,
|
static int json_boolean_any(struct json_object *object,
|
||||||
const char *const *keys, size_t key_count)
|
const char *const *keys, size_t key_count)
|
||||||
@@ -144,22 +199,34 @@ static int json_boolean_any(struct json_object *object,
|
|||||||
continue;
|
continue;
|
||||||
if (json_object_is_type(value, json_type_string)) {
|
if (json_object_is_type(value, json_type_string)) {
|
||||||
const char *text = json_object_get_string(value);
|
const char *text = json_object_get_string(value);
|
||||||
return !strcasecmp(text, "enabled") || !strcasecmp(text, "true") ||
|
if (!text || !text[0] || !strcasecmp(text, "disabled") ||
|
||||||
!strcasecmp(text, "on") || !strcmp(text, "1");
|
!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 json_object_get_boolean(value) ? 1 : 0;
|
||||||
}
|
}
|
||||||
return 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)
|
static void safe_section_name(const char *ssid, char *out, size_t sz)
|
||||||
{
|
{
|
||||||
size_t j = 0;
|
size_t j = 0;
|
||||||
for (size_t i = 0; ssid[i] && j < sz-1 && j < 15; i++) {
|
for (size_t i = 0; ssid[i] && j < sz-1 && j < 15; i++) {
|
||||||
char c = ssid[i];
|
char c = ssid[i];
|
||||||
if ((c>='a'&&c<='z')||(c>='A'&&c<='Z')||
|
if ((c>='a'&&c<='z')||(c>='A'&&c<='Z')||
|
||||||
(c>='0'&&c<='9')||c=='_'||c=='-')
|
(c>='0'&&c<='9')||c=='_')
|
||||||
out[j++] = c;
|
out[j++] = c;
|
||||||
else
|
else
|
||||||
out[j++] = '_';
|
out[j++] = '_';
|
||||||
@@ -176,9 +243,11 @@ static int uci_set_val(struct uci_context *ctx,
|
|||||||
if (!p) return -1;
|
if (!p) return -1;
|
||||||
sprintf(p, "%s=%s", path, val);
|
sprintf(p, "%s=%s", path, val);
|
||||||
int ret = uci_lookup_ptr(ctx, &ptr, p, true);
|
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);
|
free(p);
|
||||||
if (ret != UCI_OK) return -1;
|
return ret == UCI_OK ? 0 : -1;
|
||||||
return (uci_set(ctx, &ptr) == UCI_OK) ? 0 : -1;
|
|
||||||
}
|
}
|
||||||
/* Set and verify an option whose absence would make a VAP unusable. */
|
/* Set and verify an option whose absence would make a VAP unusable. */
|
||||||
static int uci_set_required(struct uci_context *ctx,
|
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;
|
if (!assignment) return -1;
|
||||||
sprintf(assignment, "%s=%s", path, val);
|
sprintf(assignment, "%s=%s", path, val);
|
||||||
int ret = uci_lookup_ptr(ctx, &ptr, assignment, true);
|
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);
|
free(assignment);
|
||||||
if (ret != UCI_OK) return -1;
|
return ret == UCI_OK ? 0 : -1;
|
||||||
return uci_add_list(ctx, &ptr) == UCI_OK ? 0 : -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -509,6 +580,7 @@ static int apply_vap(struct uci_context *ctx,
|
|||||||
struct uci_package *pkg,
|
struct uci_package *pkg,
|
||||||
struct json_object *vap_json,
|
struct json_object *vap_json,
|
||||||
const char *device_name,
|
const char *device_name,
|
||||||
|
const char *radio_band,
|
||||||
const char *mac_str,
|
const char *mac_str,
|
||||||
int vap_idx)
|
int vap_idx)
|
||||||
{
|
{
|
||||||
@@ -616,6 +688,20 @@ static int apply_vap(struct uci_context *ctx,
|
|||||||
};
|
};
|
||||||
int ft = json_boolean_any(vap_json, ft_keys,
|
int ft = json_boolean_any(vap_json, ft_keys,
|
||||||
sizeof(ft_keys) / sizeof(ft_keys[0]));
|
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) {
|
if (ft) {
|
||||||
char mdomain[5];
|
char mdomain[5];
|
||||||
mobility_domain_for_ssid(essid, mdomain);
|
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);
|
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211r", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Band Steering (802.11k/v) ────────────────────────────────
|
/* Enable the hostapd capabilities used by steering and 802.11v hints. */
|
||||||
* 802.11k: Neighbor Reports → the AP tells the client what other
|
const char *band_steer_keys[] = {
|
||||||
* APs exist to facilitate roaming.
|
"band_steering", "band_steering_enabled", "steering_enabled"
|
||||||
* 802.11v: BSS Transition Management → the AP can "suggest" to the
|
};
|
||||||
* client to move to another AP with better signal.*/
|
const char *handoff_keys[] = {
|
||||||
int band_steer = 0;
|
"bss_transition", "bss_transition_enabled",
|
||||||
if (json_object_object_get_ex(vap_json, "band_steering", &v))
|
"bss_transition_management", "handoff_suggestions",
|
||||||
band_steer = json_object_get_boolean(v) ? 1 : 0;
|
"handoff_suggestions_enabled", "ieee80211v"
|
||||||
if (band_steer) {
|
};
|
||||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 1);
|
int band_steer = json_boolean_any(
|
||||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 1);
|
vap_json, band_steer_keys,
|
||||||
UCI_SET_INT(ctx, "wireless", sec_name, "rrm_neighbor_report", 1);
|
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);
|
UCI_SET_INT(ctx, "wireless", sec_name, "bss_transition", 1);
|
||||||
} else {
|
if (bss_transition_requested && !bss_transition)
|
||||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211k", 0);
|
printf("[openuf] Handoff Suggestions requested, but installed "
|
||||||
UCI_SET_INT(ctx, "wireless", sec_name, "ieee80211v", 0);
|
"hostapd lacks 802.11v WNM support; install wpad-mbedtls\n");
|
||||||
}
|
|
||||||
|
|
||||||
/* Record the controller VLAN for telemetry and diagnostics. */
|
/* Record the controller VLAN for telemetry and diagnostics. */
|
||||||
if (vid > 0)
|
if (vid > 0)
|
||||||
UCI_SET_INT(ctx, "wireless", sec_name, "vlan_id", vid);
|
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) {
|
if (uci_set_required(ctx, pkg, sec_name, "device", device_name) != 0) {
|
||||||
printf("[openuf] Failed to bind VAP '%s' to %s\n",
|
printf("[openuf] Failed to bind VAP '%s' to %s\n",
|
||||||
essid, device_name);
|
essid, device_name);
|
||||||
return -1;
|
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) {
|
if (uci_set_required(ctx, pkg, sec_name, "network", target_network) != 0) {
|
||||||
printf("[openuf] Refusing unsafe VAP '%s': cannot bind to %s\n",
|
printf("[openuf] Refusing unsafe VAP '%s': cannot bind to %s\n",
|
||||||
essid, target_network);
|
essid, target_network);
|
||||||
return -1;
|
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),
|
essid, sec_name, device_name, target_network, sec_to_uci(security),
|
||||||
ft, band_steer, pmf);
|
ft, band_steer, handoff, pmf);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,6 +910,7 @@ int wlan_apply_config(struct json_object *config_json,
|
|||||||
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,
|
if (apply_vap(ctx, pkg, vap, model->radio_map[j].device,
|
||||||
|
model->radio_map[j].band,
|
||||||
mac_str, section_idx) != 0) {
|
mac_str, section_idx) != 0) {
|
||||||
uci_unload(ctx, pkg);
|
uci_unload(ctx, pkg);
|
||||||
uci_free_context(ctx);
|
uci_free_context(ctx);
|
||||||
@@ -821,10 +942,61 @@ 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);
|
||||||
|
|
||||||
/* 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("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;
|
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_object_add(vap, "fast_roaming_enabled",
|
||||||
json_object_new_boolean(!strcmp(value, "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);
|
snprintf(key, sizeof(key), "aaa.%d.pmf.mode", i);
|
||||||
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
|
if (system_cfg_get(system_cfg, key, value, sizeof(value))) {
|
||||||
const char *pmf = !strcmp(value, "2") ? "required" :
|
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 *enc = UCI_GET("encryption");
|
||||||
const char *dis = UCI_GET("disabled");
|
const char *dis = UCI_GET("disabled");
|
||||||
const char *r11 = UCI_GET("ieee80211r");
|
const char *r11 = UCI_GET("ieee80211r");
|
||||||
|
const char *ft_req = UCI_GET("openuf_ft_requested");
|
||||||
const char *k11 = UCI_GET("ieee80211k");
|
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 *w11 = UCI_GET("ieee80211w");
|
||||||
const char *hidden = UCI_GET("hidden");
|
const char *hidden = UCI_GET("hidden");
|
||||||
const char *vap_id = UCI_GET("openuf_vap_id");
|
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";
|
if (!strcmp(w11,"2")) pmf = "required";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ft_on = (r11 && !strcmp(r11,"1"));
|
bool ft_on = (r11 && !strcmp(r11,"1")) ||
|
||||||
bool bs_on = (k11 && !strcmp(k11,"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 hid = (hidden && !strcmp(hidden,"1"));
|
||||||
bool up = !(dis && !strcmp(dis,"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, "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, "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, "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, "pmf_mode", json_object_new_string(pmf));
|
||||||
json_object_object_add(o, "num_sta", json_object_new_int(0));
|
json_object_object_add(o, "num_sta", json_object_new_int(0));
|
||||||
if (vlan && atoi(vlan) > 0)
|
if (vlan && atoi(vlan) > 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user