Merge pull request #1527 from kernelkit/wifi-roaming

Wi-Fi: Add mesh (802.11s) and roaming (802.11k,v,r)
This commit is contained in:
Joachim Wiberg
2026-06-05 16:29:35 +02:00
committed by GitHub
22 changed files with 2025 additions and 93 deletions
@@ -1,3 +0,0 @@
service <!> name:hostapd :%i \
[2345] hostapd -P/var/run/hostapd-%i.pid /etc/hostapd-%i.conf \
-- Wi-Fi Access Point @%i
@@ -0,0 +1,3 @@
service name:wpa_supplicant :%i \
[2345] wpa_supplicant -s -i %i -c /etc/wpa_supplicant-%i.conf -P/var/run/wpa_supplicant-%i.pid \
-- Wi-Fi Mesh @%i
+49 -5
View File
@@ -257,15 +257,15 @@ def parse_interface_info(ifname):
for line in output.splitlines():
stripped = line.strip()
# Interface type
# Interface type (can be multi-word, e.g. "mesh point")
if stripped.startswith('type '):
result['iftype'] = stripped.split()[1]
result['iftype'] = ' '.join(stripped.split()[1:])
# MAC address
elif stripped.startswith('addr '):
result['mac'] = stripped.split()[1]
# SSID
# SSID (AP mode) or mesh-id (mesh point mode) — kernel uses same attr
elif stripped.startswith('ssid '):
result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:]))
@@ -538,6 +538,43 @@ def parse_link(ifname):
return result
def parse_phy_caps(phy_name):
"""
Parse 'iw phy <name> info' for HT and VHT capability bitmasks.
Returns: {ht_cap: int, vht_cap: int}
iw phy info output format:
Capabilities: 0x1ef
...
VHT Capabilities (0x339071b2):
...
"""
actual_phy = normalize_phy_name(phy_name)
output = run_iw('phy', actual_phy, 'info')
if not output:
output = run_iw(actual_phy, 'info')
if not output:
return {'ht_cap': 0, 'vht_cap': 0}
ht_cap = 0
vht_cap = 0
for line in output.splitlines():
stripped = line.strip()
# HT Capabilities: "Capabilities: 0x1ef"
ht_match = re.match(r'Capabilities:\s+(0x[0-9a-fA-F]+)', stripped)
if ht_match:
ht_cap = int(ht_match.group(1), 16)
# VHT Capabilities: "VHT Capabilities (0x339071b2):"
vht_match = re.match(r'VHT Capabilities\s+\((0x[0-9a-fA-F]+)\)', stripped)
if vht_match:
vht_cap = int(vht_match.group(1), 16)
return {'ht_cap': ht_cap, 'vht_cap': vht_cap}
def main():
if len(sys.argv) < 2:
print(json.dumps({
@@ -548,7 +585,8 @@ def main():
'info': 'Get PHY or interface information (requires device)',
'survey': 'Get channel survey data (requires interface)',
'station': 'Get connected stations in AP mode (requires interface)',
'link': 'Get link info in station mode (requires interface)'
'link': 'Get link info in station mode (requires interface)',
'caps': 'Get HT/VHT capability bitmasks (requires PHY/radio)'
},
'examples': [
'iw.py list',
@@ -557,7 +595,8 @@ def main():
'iw.py info wlan0',
'iw.py station wifi0',
'iw.py link wlan0',
'iw.py survey wlan0'
'iw.py survey wlan0',
'iw.py caps radio0'
]
}, indent=2))
sys.exit(1)
@@ -594,6 +633,11 @@ def main():
data = {'error': 'survey command requires interface argument'}
else:
data = parse_survey(sys.argv[2])
elif command == 'caps':
if len(sys.argv) < 3:
data = {'error': 'caps command requires PHY/radio argument'}
else:
data = parse_phy_caps(sys.argv[2])
else:
data = {'error': f'Unknown command: {command}'}
+17
View File
@@ -3,6 +3,23 @@ Change Log
All notable changes to the project are documented in this file.
[v26.06.0][UNRELEASED]
--------------
### Changes
- Add Wi-Fi roaming for fast, seamless handoff between access points that
share an SSID: 802.11k, 802.11v and 802.11r (over-the-air FT). See the
[Wi-Fi][wifi] guide for details
- Add Wi-Fi 802.11s mesh support, letting access points form a wireless
backhaul between each other without cabling
- Add band steering for dual-band access points, nudging dual-band
clients onto the faster 5/6 GHz band
- Add `legacy-rates` option to re-enable 802.11b rates on 2.4 GHz for
old IoT devices (disabled by default)
[wifi]: wifi.md
[v26.05.0][] - 2026-05-29
-------------------------
+1
View File
@@ -21,6 +21,7 @@ regression test system solely relies on NETCONF and RESTCONF.
- [Introduction](introduction.md)
- [System Configuration](system.md)
- [Network Configuration](networking.md)
- [Wi-Fi](wifi.md)
- [DHCP Server](dhcp.md)
- [Syslog Support](syslog.md)
- **Infix In-Depth**
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

+310 -9
View File
@@ -156,6 +156,11 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> <b>leave</b>
- `channel-width`: AP channel bandwidth. Supported values are `auto`, `20MHz`,
`40MHz`, `80MHz`, and `160MHz`. Wider channels require matching hardware,
regulatory approval, and are only available on 5GHz/6GHz where supported.
- `legacy-rates`: Allow legacy 802.11b rates (1, 2, 5.5, 11 Mbps) on 2.4GHz
(default: disabled). Slow 802.11b clients consume excessive airtime and
degrade throughput for all stations, so the rates are normally suppressed.
Enable only when old 2.4GHz-only IoT devices need them to associate. No
effect on 5GHz/6GHz.
- `probe-timeout`: Seconds to wait for PHY detection at boot (default: 0). Set
to a non-zero value (e.g., 30) for USB WiFi dongles that are slow to
initialize due to firmware loading
@@ -165,6 +170,69 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> <b>leave</b>
> constraints and hardware capabilities. Channel width can now be set
> explicitly for AP mode, or left at `auto` to let the driver choose.
### Bands and Channels
Each band strikes a different balance between range and capacity. The
`country-code` decides which channels are legal in your location; the
lists below are the common allocations, and your regulatory domain may
allow fewer.
**2.4 GHz**
Channels 1-13 are available in most of the world, 1-11 in the US and
Canada, and 14 in Japan (802.11b only). At 20 MHz only three channels
avoid overlap: 1, 6, and 11. A 40 MHz channel takes up most of the
band, so it is seldom worth using here.
Drawbacks:
- This is the most crowded band. It is shared with Bluetooth, Zigbee,
cordless phones, microwave ovens, and most of the neighboring Wi-Fi.
- Narrow channels and constant contention hold real throughput well
below 5 and 6 GHz.
- The upside is range: 2.4 GHz reaches further and passes through walls
better, which keeps it useful for distant clients and 2.4 GHz-only
IoT devices.
**5 GHz**
UNII-1 (channels 36-48) and UNII-3 (149-165) need no radar checks.
UNII-2 (channels 52-64 and 100-144) shares spectrum with radar and
requires DFS. ETSI regions such as the EU do not include UNII-3, so the
only non-DFS 5 GHz channels there are 36-48. This band supports 20, 40,
80, and 160 MHz, so it is the one to use for wide, fast channels.
Drawbacks:
- Shorter range than 2.4 GHz, and a weaker signal through walls and
floors.
- A DFS channel must be monitored for radar for 60 seconds (up to 10
minutes near some weather radars) before the AP may transmit, which
delays start-up. If radar appears later, the AP has to leave the
channel within 10 seconds and avoid it for 30 minutes, dropping
clients during the move.
- The widest 80 and 160 MHz channels almost always sit on DFS spectrum,
so the same radar rules apply to them.
**6 GHz**
The FCC regions open 59 channels (1, 5, 9 ... 233) across 5925-7125 MHz.
ETSI regions, including the EU, currently open only the lower part,
5945-6425 MHz (channels 1-93), for indoor use. Clients find networks on
the 15 Preferred Scanning Channels (5, 21, 37 ... 229) spaced every
80 MHz, and `auto` selects channel 37. There is no DFS in 6 GHz, so
there is no radar start-up delay.
Drawbacks:
- The shortest range and the weakest wall penetration of the three
bands.
- Only Wi-Fi 6E and newer clients can use it; older phones and IoT
devices cannot see the band at all.
- AP operation requires WPA3-Personal (SAE) with management frame
protection, so WPA2-only and open networks are rejected.
- Indoor power limits cap coverage further.
### WiFi 6 Support
WiFi 6 (802.11ax) is always enabled in AP mode on all bands, providing improved
@@ -486,20 +554,253 @@ radio settings, and `show interface` to see all active AP interfaces.
> AP and Station modes cannot be mixed on the same radio. All virtual interfaces
> on a radio must be the same mode (all APs or all Stations).
### AP as Bridge Port
## Fast Roaming Between Access Points
WiFi AP interfaces can be added to bridges to integrate wireless devices
into your LAN:
Fast roaming enables seamless client handoff between access points through
802.11k/r/v standards. These features can be enabled individually based on
your requirements.
### 802.11r - Fast BSS Transition
Enable 802.11r for fast handoff (<50ms) between APs with the same SSID:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain 4f57
```
**Requirements:**
- All APs in roaming group must have **identical** SSID
- All APs must have **identical** passphrase (same keystore secret)
- All APs must use the **same mobility-domain** identifier
**Mobility Domain Options:**
- Explicit 4-character hex value (e.g., `4f57`) - default if not specified
- `hash` - Automatically derive from SSID using MD5 (OpenWrt-compatible)
Using `hash` allows multiple APs with the same SSID to automatically share
the same mobility domain without manual configuration:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain hash
```
The NAS-Identifier (Network Access Server Identifier) is a string that
uniquely identifies each AP within the 802.11r mobility domain. APs
exchange this identifier during fast BSS transition so they can look up
the correct PMK-R1 key for the roaming client. It must be unique per AP
BSS and stable across reboots.
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r nas-identifier auto
```
`auto` derives the identifier as:
`<interface-name>-<hostname>.<mobility-domain>`
Or set an explicit string:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r nas-identifier ap01.wifi0.4f57
```
### 802.11k - Radio Resource Management
Enable 802.11k for client neighbor discovery and better roaming decisions:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k
```
Enables neighbor reports and beacon reports, allowing clients to discover
nearby APs before roaming.
### 802.11v - BSS Transition Management
Enable 802.11v for network-assisted roaming:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
```
Allows APs to suggest better APs to clients, improving roaming decisions.
#### Band Steering (MBO)
Enabling `dot11v` also turns on MBO (Multi-Band Operation), advertised in
beacons and association responses. MBO lets a dual-band client see that
the same SSID exists on another band and decide for itself when to move,
while 802.11v BSS Transition Management lets the AP suggest a better
target.
On top of the client-cooperative hints, the AP applies active steering:
on a 2.4 GHz access-point it suppresses probe responses to clients that
were recently seen on the same SSID on the 5/6 GHz band, nudging
dual-band clients onto the higher band. MBO is **enabled by default**
whenever `dot11v` is enabled:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
```
To turn it off while keeping BSS Transition Management:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v band-steering false
```
> [!NOTE]
> Band steering only matters when the same SSID is offered on two or more
> bands (one access-point per radio). On a single-band network there is
> no other band to move to, so it has no effect.
### Opportunistic Key Caching (OKC)
OKC reduces re-authentication time for roaming clients that do not
support 802.11r. The AP caches the PMK from previous associations and
shares it with other APs in the same mobility group. It is **enabled by
default** and only activates when both AP and client support it:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming okc false
```
### Recommended Configuration
For optimal roaming experience, enable all three features:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain 4f57
```
Or use `hash` for automatic mobility domain derivation from SSID:
```
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11k
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11v
admin@example:/config/interface/wifi0/> set wifi access-point roaming dot11r mobility-domain hash
```
Repeat for all APs that should participate in the roaming group.
> [!NOTE]
> Not all client devices support all roaming features. Modern devices typically
> support 802.11k/r/v, but older devices may only support basic roaming without
> fast transition.
## 802.11s Mesh Point Mode
IEEE 802.11s is a wireless mesh networking standard operating at Layer 2.
Mesh nodes form peer links directly with each other and route traffic
using HWMP (Hybrid Wireless Mesh Protocol), which is built into the
Linux mac80211 subsystem. There is no central controller; nodes
discover peers and find paths on their own.
The standard defines two node roles:
- **Mesh Point (MP)** - a basic mesh node that forwards traffic within
the mesh
- **Mesh Portal (MPP)** - a mesh node that bridges traffic between the
mesh and an external network (e.g., a wired LAN)
In practice, a node bridging the mesh interface to a LAN acts as a mesh
portal.
> [!NOTE]
> Not all WiFi hardware supports 802.11s mesh. The driver must implement
> mesh point mode in mac80211. Check your adapter's capabilities with
> `iw phy <phy> info` and look for "mesh point" under "Supported interface
> modes".
### 802.11s vs EasyMesh
| | **802.11s** | **EasyMesh** |
|-----------------------------|--------------------------|--------------------------------|
| **Standard** | IEEE (open, ratified) | Wi-Fi Alliance (certification) |
| **Topology** | Peer-to-peer, any-to-any | Controller-based tree |
| **Single point of failure** | None | Controller |
| **Multi-hop** | True N-hop | Limited (1-2 hops) |
| **Vendor lock-in** | None | Common |
| **Linux support** | Kernel-native (mac80211) | Requires proprietary firmware |
Infix uses 802.11s because it runs entirely in the kernel with no
proprietary components.
### Mesh configuration
A mesh point requires the radio to have `band`, `channel`, and a valid
`country-code` configured. Mesh and AP modes cannot coexist on the same
radio.
**Step 1: Configure the radio**
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit interface br0</b>
admin@example:/config/interface/br0/> <b>set type bridge</b>
admin@example:/config/> <b>edit interface wifi0</b>
admin@example:/config/interface/wifi0/> <b>set bridge-port bridge br0</b>
admin@example:/config/interface/wifi0/> <b>leave</b>
admin@example:/config/> <b>edit hardware component radio1 wifi-radio</b>
admin@example:/config/hardware/component/radio1/wifi-radio/> <b>set country-code DE</b>
admin@example:/config/hardware/component/radio1/wifi-radio/> <b>set band 5GHz</b>
admin@example:/config/hardware/component/radio1/wifi-radio/> <b>set channel 36</b>
admin@example:/config/hardware/component/radio1/wifi-radio/> <b>leave</b>
</code></pre>
**Step 2: Create keystore entry for mesh security**
All mesh links use WPA3-SAE encryption. All nodes in the same mesh
network must share the same passphrase:
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit keystore symmetric-key mesh-secret</b>
admin@example:/config/keystore/…/mesh-secret/> <b>set key-format passphrase-key-format</b>
admin@example:/config/keystore/…/mesh-secret/> <b>change cleartext-symmetric-key</b>
Passphrase: ************
Retype passphrase: ************
admin@example:/config/keystore/…/mesh-secret/> <b>end</b>
</code></pre>
**Step 3: Configure the mesh interface**
<pre class="cli"><code>admin@example:/config/> <b>edit interface wifi-mesh</b>
admin@example:/config/interface/wifi-mesh/> <b>set type wifi</b>
admin@example:/config/interface/wifi-mesh/> <b>set wifi radio radio1</b>
admin@example:/config/interface/wifi-mesh/> <b>set wifi mesh-point mesh-id my-mesh</b>
admin@example:/config/interface/wifi-mesh/> <b>set wifi mesh-point security secret mesh-secret</b>
admin@example:/config/interface/wifi-mesh/> <b>leave</b>
</code></pre>
**Mesh parameters:**
- `mesh-id`: Network identifier, 1-32 characters. All nodes in the mesh
must use the same mesh ID
- `forwarding`: L2 mesh forwarding (default: true). When enabled, the
interface can be added to a bridge as a mesh portal
- `security secret`: Keystore reference for the WPA3-SAE passphrase
### Mesh portal (bridge integration)
To connect the wireless mesh to a wired LAN, add the mesh interface to
a bridge:
<pre class="cli"><code>admin@example:/config/> <b>edit interface wifi-mesh</b>
admin@example:/config/interface/wifi-mesh/> <b>set bridge-port bridge br0</b>
admin@example:/config/interface/wifi-mesh/> <b>leave</b>
</code></pre>
### Mesh with roaming APs
You can combine 802.11s mesh backhaul with roaming-enabled access
points. Each node has a mesh interface for backhaul on one radio and
AP interfaces for clients on another:
![802.11s mesh backhaul with roaming-enabled access points](img/wifi-mesh-roaming.svg)
With 802.11r/k/v roaming enabled on the APs (same SSID, same
passphrase, same mobility domain), clients hand off between nodes while
the mesh carries backhaul traffic.
## Troubleshooting
Use `show interface wifi0` to verify signal strength and connection status.
+4
View File
@@ -5,11 +5,15 @@ config BR2_PACKAGE_FEATURE_WIFI
select BR2_PACKAGE_WPA_SUPPLICANT_DEBUG_SYSLOG
select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
select BR2_PACKAGE_WPA_SUPPLICANT_AP_SUPPORT
select BR2_PACKAGE_WPA_SUPPLICANT_MESH_NETWORKING
select BR2_PACKAGE_WIRELESS_REGDB
select BR2_PACKAGE_HOSTAPD
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
select BR2_PACKAGE_HOSTAPD_WPA3
select BR2_PACKAGE_HOSTAPD_WPS
select BR2_PACKAGE_HOSTAPD_WNM
select BR2_PACKAGE_HOSTAPD_MBO
select BR2_PACKAGE_IW
help
Enables WiFi in Infix. Enables all requried applications.
+1 -1
View File
@@ -69,7 +69,7 @@ define FEATURE_WIFI_LINUX_CONFIG_FIXUPS
endef
define FEATURE_WIFI_INSTALL_IN_ROMFS
mkdir -p $(TARGET_DIR)/etc/modprobe.d $(TARGET_DIR)/etc/udev/rules.d
mkdir -p $(TARGET_DIR)/etc/modprobe.d $(TARGET_DIR)/etc/udev/rules.d $(TARGET_DIR)/usr/libexec
cp $(FEATURE_WIFI_PKGDIR)/mt7915e.conf $(TARGET_DIR)/etc/modprobe.d/mt7915e.conf
cp $(FEATURE_WIFI_PKGDIR)/60-rename-wifi-phy.rules $(TARGET_DIR)/etc/udev/rules.d/60-rename-wifi-phy.rules
cp $(FEATURE_WIFI_PKGDIR)/70-remove-virtual-wifi-interfaces.rules $(TARGET_DIR)/etc/udev/rules.d/70-remove-virtual-wifi-interfaces.rules
@@ -0,0 +1,54 @@
From 9899169d6dcb07aaecdde09c77ef59b56f66e3e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
Date: Fri, 5 Jun 2026 09:10:15 +0200
Subject: [PATCH 3/3] fix(functions): do not touch global ip_forward sysctl
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
Infix manages IPv4/IPv6 forwarding per-interface via the sysctls
net.ipv4.conf.<iface>.forwarding and net.ipv6.conf.<iface>.forwarding.
firewalld's enable_ip_forwarding() instead writes the *global*
net.ipv4.ip_forward and net.ipv6.conf.all.forwarding knobs. The kernel
propagates those global values to every interface, which clobbers the
per-interface forwarding configuration Infix sets.
Make enable_ip_forwarding() a no-op. The backends still install the
masquerade and forward-port nftables rules; the per-packet forwarding
decision is governed by the inbound interface's forwarding flag, so
routing keeps working on the interfaces where Infix enabled it while
the global knob is left untouched.
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
src/firewall/functions.py | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/src/firewall/functions.py b/src/firewall/functions.py
index 1b8a32c..cd87f5d 100644
--- a/src/firewall/functions.py
+++ b/src/firewall/functions.py
@@ -495,11 +495,14 @@ def writefile(filename, line):
def enable_ip_forwarding(ipv):
- if ipv == "ipv4":
- return writefile("/proc/sys/net/ipv4/ip_forward", "1\n")
- elif ipv == "ipv6":
- return writefile("/proc/sys/net/ipv6/conf/all/forwarding", "1\n")
- return False
+ # Infix manages IP forwarding per-interface via the sysctls
+ # net.ipv4.conf.<iface>.forwarding and net.ipv6.conf.<iface>.forwarding.
+ # Writing the global net.ipv4.ip_forward / net.ipv6.conf.all.forwarding
+ # knobs would clobber those per-interface settings, since the kernel
+ # propagates the global value to every interface. Make this a no-op:
+ # firewalld still installs the masquerade and forward-port nft rules, and
+ # forwarding works on the interfaces where Infix has enabled it.
+ return True
def get_nf_conntrack_short_name(module):
--
2.43.0
+1
View File
@@ -89,6 +89,7 @@ PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
PKG_CHECK_MODULES([sysrepo], [sysrepo >= 4.2.10])
PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2])
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
PKG_CHECK_MODULES([libcrypto], [libcrypto])
AC_CHECK_HEADER([ev.h],
[saved_LIBS="$LIBS"
+2
View File
@@ -17,6 +17,7 @@ confd_plugin_la_CFLAGS = \
$(glib_CFLAGS) \
$(jansson_CFLAGS) \
$(libite_CFLAGS) \
$(libcrypto_CFLAGS) \
$(sysrepo_CFLAGS) \
$(libsrx_CFLAGS) \
$(CFLAGS)
@@ -26,6 +27,7 @@ confd_plugin_la_LIBADD = \
$(glib_LIBS) \
$(jansson_LIBS) \
$(libite_LIBS) \
$(libcrypto_LIBS) \
$(sysrepo_LIBS) \
$(libsrx_LIBS)
+537 -38
View File
@@ -1,9 +1,12 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <fnmatch.h>
#include <ftw.h>
#include <glob.h>
#include <jansson.h>
#include <libgen.h>
#include <limits.h>
#include <unistd.h>
#include <openssl/evp.h>
#include <srx/common.h>
#include <srx/lyx.h>
@@ -17,6 +20,7 @@
#define XPATH_BASE_ "/ietf-hardware:hardware"
#define HOSTAPD_CONF "/etc/hostapd-%s.conf"
#define HOSTAPD_CONF_NEXT HOSTAPD_CONF"+"
#define HOSTAPD_SERVICE "/etc/finit.d/available/hostapd.conf"
#define GPSD_CONF "/etc/finit.d/available/gpsd.conf"
#define GPSD_CONF_NEXT GPSD_CONF"+"
#define GPSD_MAX_DEVICES 4
@@ -177,6 +181,89 @@ out_free_xpath:
return err;
}
/* Resolve mobility domain - if "hash", derive from SSID using MD5 (OpenWrt-compatible) */
static const char *resolve_mobility_domain(const char *mobility_domain, const char *ssid)
{
static char hash_result[5]; /* 4 hex chars + null */
unsigned char md5_digest[EVP_MAX_MD_SIZE];
unsigned int md5_len;
EVP_MD_CTX *ctx;
if (strcmp(mobility_domain, "hash"))
return mobility_domain;
if (!ssid) {
ERROR("Cannot derive mobility domain from NULL SSID");
return "4f57"; /* Fallback to default */
}
/* Compute MD5 hash using EVP API (OpenSSL 3.0+) */
ctx = EVP_MD_CTX_new();
if (!ctx) {
ERROR("Failed to create EVP context");
return "4f57";
}
if (EVP_DigestInit_ex(ctx, EVP_md5(), NULL) != 1 ||
EVP_DigestUpdate(ctx, ssid, strlen(ssid)) != 1 ||
EVP_DigestFinal_ex(ctx, md5_digest, &md5_len) != 1) {
ERROR("Failed to compute MD5 hash");
EVP_MD_CTX_free(ctx);
return "4f57";
}
EVP_MD_CTX_free(ctx);
/* Extract first 4 hex characters (first 2 bytes) */
snprintf(hash_result, sizeof(hash_result), "%02x%02x", md5_digest[0], md5_digest[1]);
DEBUG("Derived mobility domain '%s' from SSID '%s'", hash_result, ssid);
return hash_result;
}
/*
* Find an AP interface on a higher-band radio (5/6 GHz) advertising the
* same SSID as the caller's 2.4 GHz BSS, for no_probe_resp_if_seen_on=.
* hostapd suppresses a 2.4 GHz probe response only once it has actually
* seen that MAC on the listed interface, so 2.4-only clients always get a
* response while dual-band clients are nudged to the higher band. Note:
* sta_track spans bands only when both radios run in one hostapd process.
*
* Returns a pointer into the config tree (do not free) or NULL.
*/
static const char *wifi_find_higher_band_twin(struct lyd_node *config,
const char *current_band,
const char *current_ssid)
{
struct lyd_node *cifs, *cif;
if (strcmp(current_band, "2.4GHz"))
return NULL;
cifs = lydx_get_descendant(config, "interfaces", "interface", NULL);
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
struct lyd_node *wifi, *ap, *radio_node;
const char *radio, *ssid, *band;
wifi = lydx_get_child(cif, "wifi");
if (!wifi)
continue;
ap = lydx_get_child(wifi, "access-point");
if (!ap)
continue;
ssid = lydx_get_cattr(ap, "ssid");
if (strcmp(ssid, current_ssid))
continue;
radio = lydx_get_cattr(wifi, "radio");
radio_node = lydx_get_xpathf(config,
"/hardware/component[name='%s']/wifi-radio", radio);
band = lydx_get_cattr(radio_node, "band");
if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz"))
return lydx_get_cattr(cif, "name");
}
return NULL;
}
static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio_name,
struct lyd_node ***iface_list, int *count)
@@ -200,7 +287,7 @@ static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio
if (lydx_get_op(iface) == LYDX_OP_DELETE)
continue;
list = realloc(list, sizeof(struct lyd_node *) * n + 1);
list = realloc(list, sizeof(struct lyd_node *) * (n + 1));
list[n++] = iface;
}
@@ -230,8 +317,7 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
ap = lydx_get_child(wifi, "access-point");
if (!ap)
continue;
list = realloc(list, sizeof(char *) *n+1);
list = realloc(list, sizeof(char *) * (n + 1));
ifname = lydx_get_cattr(cif, "name");
list[n++] = strdup(ifname);
@@ -256,10 +342,16 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
/* Helper: Write SSID and security configuration (shared between primary and BSS) */
static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd_node *config, bool is_bss, const char *band)
{
struct lyd_node *wifi, *ap, *security, *secret_node, *roaming;
struct lyd_node *dot11k, *dot11r, *dot11v;
const char *ssid, *hidden, *security_mode, *secret_name;
struct lyd_node *wifi, *ap, *security, *secret_node;
const char *mobility_domain, *mobility_domain_raw;
const char *nas_identifier_cfg;
bool enable_80211k, enable_80211r, enable_80211v, enable_mbo;
unsigned char *secret = NULL;
const char *ifname;
char nas_identifier_default[300];
char hostname[64] = "localhost";
char bssid[18];
ifname = lydx_get_cattr(cif, "name");
@@ -271,6 +363,24 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
fprintf(hostapd, "bss=%s\n", ifname);
}
/* Check 802.11k/r/v configuration */
roaming = lydx_get_child(ap, "roaming");
dot11k = roaming ? lydx_get_child(roaming, "dot11k") : NULL;
dot11r = roaming ? lydx_get_child(roaming, "dot11r") : NULL;
dot11v = roaming ? lydx_get_child(roaming, "dot11v") : NULL;
enable_80211k = dot11k != NULL;
enable_80211r = dot11r != NULL;
enable_80211v = dot11v != NULL;
if (dot11v) {
const char *band_steering = lydx_get_cattr(dot11v, "band-steering");
enable_mbo = !band_steering || !strcmp(band_steering, "true");
} else {
enable_mbo = false;
}
/* Set BSSID if custom MAC is configured */
if (!interface_get_phys_addr(cif, bssid))
fprintf(hostapd, "bssid=%s\n", bssid);
@@ -279,6 +389,19 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
ssid = lydx_get_cattr(ap, "ssid");
hidden = lydx_get_cattr(ap, "hidden");
if (enable_80211r) {
mobility_domain_raw = lydx_get_cattr(dot11r, "mobility-domain");
mobility_domain = resolve_mobility_domain(mobility_domain_raw, ssid);
nas_identifier_cfg = lydx_get_cattr(dot11r, "nas-identifier");
if (!nas_identifier_cfg || !strcmp(nas_identifier_cfg, "auto")) {
if (gethostname(hostname, sizeof(hostname)) != 0)
snprintf(hostname, sizeof(hostname), "localhost");
hostname[sizeof(hostname) - 1] = '\0';
snprintf(nas_identifier_default, sizeof(nas_identifier_default),
"%s-%s.%s", ifname, hostname, mobility_domain);
nas_identifier_cfg = nas_identifier_default;
}
}
if (ssid)
fprintf(hostapd, "ssid=%s\n", ssid);
if (hidden && !strcmp(hidden, "true"))
@@ -330,7 +453,7 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
} else if (!strcmp(security_mode, "wpa2-personal")) {
fprintf(hostapd, "wpa=2\n");
/* WPA-PSK: Pre-shared key authentication */
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
fprintf(hostapd, "wpa_key_mgmt=%sWPA-PSK\n", enable_80211r ? "FT-PSK " : "");
/* CCMP: AES-based encryption, mandatory for WPA2 */
fprintf(hostapd, "wpa_pairwise=CCMP\n");
if (secret)
@@ -338,7 +461,7 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
} else if (!strcmp(security_mode, "wpa3-personal")) {
fprintf(hostapd, "wpa=2\n");
/* SAE: Simultaneous Authentication of Equals, resistant to offline dictionary attacks */
fprintf(hostapd, "wpa_key_mgmt=SAE\n");
fprintf(hostapd, "wpa_key_mgmt=%sSAE\n", enable_80211r ? "FT-SAE " : "");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret)
fprintf(hostapd, "sae_password=%s\n", secret);
@@ -346,7 +469,8 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
fprintf(hostapd, "wpa=2\n");
/* Allow both PSK (WPA2) and SAE (WPA3) authentication */
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
fprintf(hostapd, "wpa_key_mgmt=%sWPA-PSK SAE\n",
enable_80211r ? "FT-PSK FT-SAE " : "");
fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret) {
fprintf(hostapd, "wpa_passphrase=%s\n", secret);
@@ -356,6 +480,62 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
fprintf(hostapd, "ieee80211w=1\n");
}
/* 802.11r: Fast BSS Transition */
if (enable_80211r) {
fprintf(hostapd, "# Fast BSS Transition (802.11r)\n");
fprintf(hostapd, "mobility_domain=%s\n", mobility_domain);
/* Over-the-air FT: the client authenticates directly with the
* target AP. Over-DS would relay the exchange through the wireless
* mesh backhaul, adding latency and packet loss, so it is not used. */
fprintf(hostapd, "ft_over_ds=0\n");
fprintf(hostapd, "ft_psk_generate_local=1\n");
fprintf(hostapd, "nas_identifier=%s\n", nas_identifier_cfg);
}
/* 802.11k: Radio Resource Management */
if (enable_80211k) {
fprintf(hostapd, "# Radio Resource Management (802.11k)\n");
fprintf(hostapd, "rrm_neighbor_report=1\n");
fprintf(hostapd, "rrm_beacon_report=1\n");
}
/* 802.11v: BSS Transition Management */
if (enable_80211v) {
fprintf(hostapd, "# BSS Transition Management (802.11v)\n");
fprintf(hostapd, "bss_transition=1\n");
}
/* OKC: Opportunistic Key Caching */
if (roaming) {
const char *okc = lydx_get_cattr(roaming, "okc");
if (!okc || !strcmp(okc, "true"))
fprintf(hostapd, "okc=1\n");
}
/* MBO: Multi-Band Operation. Advertises multi-band capability so
* MBO-aware clients make their own band-steering decisions; pairs
* with 802.11v BSS Transition Management above. */
if (enable_mbo) {
const char *twin;
fprintf(hostapd, "mbo=1\n");
/* Required for no_probe_resp_if_seen_on below: without it
* hostapd keeps no sta_track list, so the twin radio never
* knows which clients it has seen. Radio-level, emit once
* in the main section, never per BSS. */
if (!is_bss)
fprintf(hostapd, "track_sta_max_num=100\n");
/* Active band steering: on a 2.4 GHz BSS, suppress probe
* responses to clients recently seen on the same-SSID 5/6
* GHz BSS, nudging dual-band clients to the higher band. */
twin = wifi_find_higher_band_twin(config, band, ssid);
if (twin)
fprintf(hostapd, "no_probe_resp_if_seen_on=%s\n", twin);
}
free(secret);
}
@@ -420,19 +600,290 @@ static const char *wifi_ht40_dir(int ch)
return ((ch / 4) % 2) ? "[HT40+]" : "[HT40-]";
}
/*
* Read HT/VHT capability bitmasks from hardware via iw.py.
* Returns 0 on success, -1 on failure.
*/
static int wifi_read_phy_caps(const char *radio_name, unsigned int *ht_cap, unsigned int *vht_cap)
{
json_error_t jerr;
json_t *root, *jht, *jvht;
char buf[256];
size_t len;
FILE *pp;
*ht_cap = 0;
*vht_cap = 0;
pp = popenf("r", "/usr/libexec/infix/iw.py caps %s", radio_name);
if (!pp)
return -1;
len = fread(buf, 1, sizeof(buf) - 1, pp);
pclose(pp);
buf[len] = '\0';
/*
* Parse JSON output: {"ht_cap": NNN, "vht_cap": NNN}
* Use jansson since hardware.c already includes it.
*/
root = json_loads(buf, 0, &jerr);
if (!root)
return -1;
jht = json_object_get(root, "ht_cap");
jvht = json_object_get(root, "vht_cap");
if (json_is_integer(jht))
*ht_cap = (unsigned int)json_integer_value(jht);
if (json_is_integer(jvht))
*vht_cap = (unsigned int)json_integer_value(jvht);
json_decref(root);
return 0;
}
/*
* Build hostapd ht_capab string from HT capability bitmask.
* IEEE 802.11-2016 Table 9-153 — HT Capabilities Info field
*
* The ht40_dir string ("[HT40+]" or "[HT40-]") is prepended when
* the configured channel width is 40MHz or wider.
*/
static void wifi_build_ht_capab(char *out, size_t sz, unsigned int ht_cap,
const char *ht40_dir, int want_ht40)
{
char *p = out;
size_t rem = sz;
int n;
*p = '\0';
if (want_ht40 && ht40_dir) {
n = snprintf(p, rem, "%s", ht40_dir);
p += n;
rem -= n;
}
if (ht_cap & 0x0001) {
n = snprintf(p, rem, "[LDPC]");
p += n;
rem -= n;
}
if (ht_cap & 0x0020) {
n = snprintf(p, rem, "[SHORT-GI-20]");
p += n;
rem -= n;
}
if (ht_cap & 0x0040) {
n = snprintf(p, rem, "[SHORT-GI-40]");
p += n;
rem -= n;
}
if (ht_cap & 0x0080) {
n = snprintf(p, rem, "[TX-STBC]");
p += n;
rem -= n;
}
/* RX-STBC: bits 8-9 */
switch ((ht_cap >> 8) & 0x3) {
case 1:
n = snprintf(p, rem, "[RX-STBC1]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[RX-STBC12]");
p += n;
rem -= n;
break;
case 3:
n = snprintf(p, rem, "[RX-STBC123]");
p += n;
rem -= n;
break;
}
/* Max A-MSDU Length: bit 11 */
if (ht_cap & 0x0800) {
n = snprintf(p, rem, "[MAX-AMSDU-7935]");
p += n;
rem -= n;
}
}
/*
* Build hostapd vht_capab string from VHT capability bitmask.
* IEEE 802.11-2016 Table 9-250 — VHT Capabilities Info field
*
* When the configured width is less than 160MHz, the VHT160 and
* SHORT-GI-160 flags are masked out to prevent hostapd from
* advertising capabilities the configuration does not use.
*/
static void wifi_build_vht_capab(char *out, size_t sz, unsigned int vht_cap, int chwidth)
{
char *p = out;
size_t rem = sz;
int n;
*p = '\0';
/* Max MPDU Length: bits 0-1 */
switch (vht_cap & 0x3) {
case 1:
n = snprintf(p, rem, "[MAX-MPDU-7991]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[MAX-MPDU-11454]");
p += n;
rem -= n;
break;
}
/* Supported Channel Width: bits 2-3 */
if (chwidth >= 2) {
switch ((vht_cap >> 2) & 0x3) {
case 1:
n = snprintf(p, rem, "[VHT160]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[VHT160-80PLUS80]");
p += n;
rem -= n;
break;
}
}
/* RXLDPC: bit 4 */
if (vht_cap & 0x10) {
n = snprintf(p, rem, "[RXLDPC]");
p += n;
rem -= n;
}
/* Short GI for 80MHz: bit 5 */
if (vht_cap & 0x20) {
n = snprintf(p, rem, "[SHORT-GI-80]");
p += n;
rem -= n;
}
/* Short GI for 160MHz: bit 6 — only when configured for 160MHz */
if (chwidth >= 2 && (vht_cap & 0x40)) {
n = snprintf(p, rem, "[SHORT-GI-160]");
p += n;
rem -= n;
}
/* TX STBC: bit 7 */
if (vht_cap & 0x80) {
n = snprintf(p, rem, "[TX-STBC-2BY1]");
p += n;
rem -= n;
}
/* RX STBC: bits 8-10 */
switch ((vht_cap >> 8) & 0x7) {
case 1:
n = snprintf(p, rem, "[RX-STBC-1]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[RX-STBC-12]");
p += n;
rem -= n;
break;
case 3:
n = snprintf(p, rem, "[RX-STBC-123]");
p += n;
rem -= n;
break;
case 4:
n = snprintf(p, rem, "[RX-STBC-1234]");
p += n;
rem -= n;
break;
}
/* SU Beamformer: bit 11 */
if (vht_cap & 0x800) {
n = snprintf(p, rem, "[SU-BEAMFORMER]");
p += n;
rem -= n;
}
/* SU Beamformee: bit 12 */
if (vht_cap & 0x1000) {
n = snprintf(p, rem, "[SU-BEAMFORMEE]");
p += n;
rem -= n;
}
/* MU Beamformer: bit 19 */
if (vht_cap & 0x80000) {
n = snprintf(p, rem, "[MU-BEAMFORMER]");
p += n;
rem -= n;
}
/* MU Beamformee: bit 20 */
if (vht_cap & 0x100000) {
n = snprintf(p, rem, "[MU-BEAMFORMEE]");
p += n;
rem -= n;
}
/* VHT TXOP PS: bit 21 */
if (vht_cap & 0x200000) {
n = snprintf(p, rem, "[VHT-TXOP-PS]");
p += n;
rem -= n;
}
/* HTC-VHT: bit 22 */
if (vht_cap & 0x400000) {
n = snprintf(p, rem, "[HTC-VHT]");
p += n;
rem -= n;
}
/* Max A-MPDU Length Exponent: bits 23-25 */
n = (vht_cap >> 23) & 0x7;
if (n) {
int wrote = snprintf(p, rem, "[MAX-A-MPDU-LEN-EXP%d]", n);
p += wrote;
rem -= wrote;
}
}
/* Helper: Write radio-specific configuration */
static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
static void wifi_gen_radio_config(FILE *hostapd, const char *radio_name,
struct lyd_node *radio_node)
{
const char *country, *channel, *band, *width;
unsigned int ht_cap = 0, vht_cap = 0;
char ht_capab[512], vht_capab[512];
int chwidth = 0; /* 0=20/40, 1=80, 2=160 */
int ch = 0;
bool legacy_rates;
country = lydx_get_cattr(radio_node, "country-code");
band = lydx_get_cattr(radio_node, "band");
channel = lydx_get_cattr(radio_node, "channel");
width = lydx_get_cattr(radio_node, "channel-width");
legacy_rates = lydx_is_enabled(radio_node, "legacy-rates");
if (channel && strcmp(channel, "auto"))
ch = atoi(channel);
/* Read HT/VHT hardware capabilities from PHY */
wifi_read_phy_caps(radio_name, &ht_cap, &vht_cap);
if (country)
fprintf(hostapd, "country_code=%s\n", country);
@@ -459,13 +910,18 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
fprintf(hostapd, "hw_mode=g\n");
/*
* Disable legacy 802.11b rates (1, 2, 5.5, 11 Mbps).
* Slow clients using these rates consume excessive
* airtime, degrading performance for all clients.
* Disable legacy 802.11b rates (1, 2, 5.5, 11 Mbps)
* unless explicitly enabled via 'legacy-rates'. Slow
* 802.11b clients consume excessive airtime, degrading
* performance for all clients. When enabled, hostapd
* keeps its default rate set so old 2.4GHz-only IoT
* devices can still associate.
* Rates in 0.5 Mbps units: 60=6M, 90=9M, etc.
*/
fprintf(hostapd, "supported_rates=60 90 120 180 240 360 480 540\n");
fprintf(hostapd, "basic_rates=60 120 240\n");
if (!legacy_rates) {
fprintf(hostapd, "supported_rates=60 90 120 180 240 360 480 540\n");
fprintf(hostapd, "basic_rates=60 120 240\n");
}
} else if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz")) {
/* hw_mode=a: 5GHz/6GHz with 802.11a (OFDM) as baseline */
fprintf(hostapd, "hw_mode=a\n");
@@ -511,14 +967,13 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
}
/*
* Channel width configuration.
* Channel width + HT/VHT capability configuration.
*
* 6GHz: bandwidth is determined by op_class (131-134),
* hostapd ignores he_oper_chwidth on 6GHz. No VHT/HT.
*
* 5GHz: requires explicit ht_capab/vht_capab strings
* matching hardware. Without vht_capab, 160MHz is not
* advertised in beacons.
* hostapd requires explicit ht_capab and vht_capab strings
* that match hardware capabilities. Without vht_capab, 160MHz
* support is not advertised in beacons, causing clients to
* connect at 80MHz. Read capabilities from hardware via iw.py
* and build the capability strings from the bitmasks.
*/
if (!strcmp(band, "6GHz")) {
int op_class = 133; /* default 80MHz for 6GHz */
@@ -544,13 +999,18 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
}
} else if (width && strcmp(width, "auto")) {
if (!strcmp(width, "20MHz")) {
fprintf(hostapd, "ht_capab=\n");
chwidth = 0;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap, NULL, 0);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
if (strcmp(band, "2.4GHz")) {
fprintf(hostapd, "vht_oper_chwidth=0\n");
fprintf(hostapd, "he_oper_chwidth=0\n");
}
} else if (!strcmp(width, "40MHz")) {
fprintf(hostapd, "ht_capab=%s\n", ch ? wifi_ht40_dir(ch) : "[HT40+]");
chwidth = 0;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap,
ch ? wifi_ht40_dir(ch) : "[HT40+]", 1);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
if (strcmp(band, "2.4GHz")) {
fprintf(hostapd, "vht_oper_chwidth=0\n");
fprintf(hostapd, "he_oper_chwidth=0\n");
@@ -558,7 +1018,11 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
} else if (!strcmp(width, "80MHz") && ch) {
int center = wifi_center_chan_80(ch);
fprintf(hostapd, "ht_capab=%s\n", wifi_ht40_dir(ch));
chwidth = 1;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap, wifi_ht40_dir(ch), 1);
wifi_build_vht_capab(vht_capab, sizeof(vht_capab), vht_cap, chwidth);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
fprintf(hostapd, "vht_capab=%s\n", vht_capab);
fprintf(hostapd, "vht_oper_chwidth=1\n");
fprintf(hostapd, "he_oper_chwidth=1\n");
if (center) {
@@ -568,7 +1032,11 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
} else if (!strcmp(width, "160MHz") && ch) {
int center = wifi_center_chan_160(ch);
fprintf(hostapd, "ht_capab=%s\n", wifi_ht40_dir(ch));
chwidth = 2;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap, wifi_ht40_dir(ch), 1);
wifi_build_vht_capab(vht_capab, sizeof(vht_capab), vht_cap, chwidth);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
fprintf(hostapd, "vht_capab=%s\n", vht_capab);
fprintf(hostapd, "vht_oper_chwidth=2\n");
fprintf(hostapd, "he_oper_chwidth=2\n");
if (center) {
@@ -648,7 +1116,7 @@ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs,
fprintf(hostapd, "\n");
/* Radio-specific configuration */
wifi_gen_radio_config(hostapd, radio_node);
wifi_gen_radio_config(hostapd, radio_name, radio_node);
/* Add BSS sections for secondary APs (multi-SSID) */
for (i = 1; i < ap_count; i++) {
@@ -725,6 +1193,7 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
struct lyd_node *difs = NULL, *dif = NULL;
int rc = SR_ERR_OK;
int gps_changed = 0;
int wifi_changed = 0;
if (!lydx_find_xpathf(diff, XPATH_BASE_))
return SR_ERR_OK;
@@ -786,34 +1255,25 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
wifi_find_interfaces_on_radio(interfaces_diff, name,
&wifi_iface_list, &wifi_iface_count);
if (wifi_iface_count > 0) {
bool running, enabled;
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
if (ap && lydx_get_op(ap) != LYDX_OP_DELETE) {
/* AP mode - activate hostapd for radio */
snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name);
snprintf(dst, sizeof(dst), HOSTAPD_CONF, name);
running = !systemf("initctl -bfq status hostapd:%s", name);
enabled = fexistf(HOSTAPD_CONF_NEXT, name);
if (enabled) {
if (fexistf(HOSTAPD_CONF_NEXT, name)) {
(void)rename(src, dst);
ap_interfaces++;
if (running)
finit_reloadf("hostapd@%s", name);
else
finit_enablef("hostapd@%s", name);
}
}
}
if (!ap_interfaces) {
finit_disablef("hostapd@%s", name);
erasef(HOSTAPD_CONF, name);
erasef(HOSTAPD_CONF_NEXT, name);
}
free(wifi_iface_list);
/* All radios share one hostapd process; the service is
* (re)generated after the component loop below. */
wifi_changed = 1;
continue;
default:
continue;
@@ -909,6 +1369,45 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
break;
}
}
/*
* All AP radios run in a single hostapd process so that
* cross-radio directives (e.g. no_probe_resp_if_seen_on for band
* steering) resolve: those only consult interfaces managed by the
* same process. Regenerate the combined service from every staged
* /etc/hostapd-<radio>.conf and (re)start it. A restart re-reads
* all radio configs, so band edits and radio add/remove both apply.
*/
if (wifi_changed && event == SR_EV_DONE) {
glob_t gl = { 0 };
if (glob("/etc/hostapd-*.conf", 0, NULL, &gl) == 0 && gl.gl_pathc > 0) {
FILE *fp;
fp = fopen(HOSTAPD_SERVICE, "w");
if (!fp) {
ERRNO("Could not open " HOSTAPD_SERVICE);
rc = SR_ERR_INTERNAL;
} else {
size_t i;
fprintf(fp, "# Generated by confd, do not edit.\n");
fprintf(fp, "service <!> name:hostapd \\\n");
fprintf(fp, "\t[2345] hostapd -P /run/hostapd.pid");
for (i = 0; i < gl.gl_pathc; i++)
fprintf(fp, " %s", gl.gl_pathv[i]);
fprintf(fp, " \\\n\t-- Wi-Fi Access Points\n");
fclose(fp);
finit_enable("hostapd");
finit_reload("hostapd");
}
} else {
unlink(HOSTAPD_SERVICE);
finit_disable("hostapd");
}
globfree(&gl);
}
err:
return rc;
+235 -20
View File
@@ -79,19 +79,24 @@ int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif)
wifi_mode_t wifi_get_mode(struct lyd_node *iface)
{
struct lyd_node *ap, *wifi;
struct lyd_node *ap, *mesh, *wifi;
wifi = lydx_get_child(iface, "wifi");
if (!wifi)
return wifi_unknown;
ap = lydx_get_child(wifi, "access-point");
if (ap) {
if (lydx_get_op(ap) != LYDX_OP_DELETE)
return wifi_ap;
}
mesh = lydx_get_child(wifi, "mesh-point");
if (mesh) {
if (lydx_get_op(mesh) != LYDX_OP_DELETE)
return wifi_mesh;
}
/*
* Need to return station even if "station" also is false,
* because station is the default scanning mode.
@@ -101,19 +106,25 @@ wifi_mode_t wifi_get_mode(struct lyd_node *iface)
int wifi_mode_changed(struct lyd_node *wifi)
{
enum lydx_op ap_op = LYDX_OP_DELETE;
struct lyd_node *ap;
enum lydx_op op = LYDX_OP_DELETE;
struct lyd_node *node;
if (!wifi)
return 0;
ap = lydx_get_child(wifi, "access-point");
if (ap)
ap_op = lydx_get_op(ap);
node = lydx_get_child(wifi, "access-point");
if (node)
op = lydx_get_op(node);
if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE))
return 1;
DEBUG("MODE CHANGED: %d", ap && (ap_op == LYDX_OP_CREATE || ap_op == LYDX_OP_DELETE));
node = lydx_get_child(wifi, "mesh-point");
if (node)
op = lydx_get_op(node);
if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE))
return 1;
return (ap && (ap_op == LYDX_OP_CREATE || ap_op == LYDX_OP_DELETE));
return 0;
}
/*
@@ -150,7 +161,8 @@ int wifi_gen_station(struct lyd_node *cif)
secret_name = NULL;
}
radio_node = lydx_get_xpathf(cif, "../../hardware/component[name='%s']/wifi-radio", radio);
radio_node = lydx_get_xpathf(cif,
"/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio);
country = lydx_get_cattr(radio_node, "country-code");
if (secret_name && strcmp(security_mode, "disabled") != 0) {
@@ -171,10 +183,8 @@ int wifi_gen_station(struct lyd_node *cif)
goto out;
}
/*
* Background scanning every 10 seconds while not associated, when we
* have an SSID (below), bgscan assumes this task.
*/
/* autoscan drives scanning every 10s while unassociated; once
* associated no background scanning runs (bgscan disabled below). */
fprintf(wpa_supplicant,
"ctrl_interface=/run/wpa_supplicant\n"
"autoscan=periodic:10\n"
@@ -190,12 +200,15 @@ int wifi_gen_station(struct lyd_node *cif)
asprintf(&security_str, "key_mgmt=NONE");
else if (secret)
asprintf(&security_str,
"key_mgmt=SAE WPA-PSK\n"
"key_mgmt=FT-SAE FT-PSK SAE WPA-PSK\n"
" psk=\"%s\"", secret);
/* bgscan="" disables background scanning once associated: on a
* fixed backhaul link the periodic off-channel sweep stalls
* traffic and buys no roaming benefit. */
fprintf(wpa_supplicant,
"network={\n"
" bgscan=\"simple: 30:-45:300\"\n"
" bgscan=\"\"\n"
" ssid=\"%s\"\n"
" %s\n"
"}\n", ssid, security_str);
@@ -227,6 +240,195 @@ out:
return rc;
}
/*
* Center channel for 80MHz VHT/HE operation.
* 5GHz 80MHz channel groups and their center channels:
* 36-48(42), 52-64(58), 100-112(106),
* 116-128(122), 132-144(138), 149-161(155)
*/
static int wifi_center_chan_80(int ch)
{
static const int grp[][2] = {
{36, 42}, {52, 58}, {100, 106}, {116, 122}, {132, 138}, {149, 155}
};
int i;
for (i = 0; i < 6; i++)
if (ch >= grp[i][0] && ch < grp[i][0] + 16)
return grp[i][1];
return 0;
}
/*
* Center channel for 160MHz VHT/HE operation.
* 5GHz 160MHz groups: 36-64(50), 100-128(114)
*/
static int wifi_center_chan_160(int ch)
{
if (ch >= 36 && ch <= 64)
return 50;
if (ch >= 100 && ch <= 128)
return 114;
return 0;
}
/*
* Convert WiFi channel number to frequency in MHz.
* Band is determined from channel range:
* 2.4GHz: channels 1-14
* 5GHz: channels 32-177
* 6GHz: channels 1-233 (identified by band string)
*/
static int wifi_chan_to_freq(int channel, const char *band)
{
if (!strcmp(band, "6GHz"))
return 5950 + channel * 5;
if (channel >= 1 && channel <= 13)
return 2407 + channel * 5;
if (channel == 14)
return 2484;
/* 5GHz */
return 5000 + channel * 5;
}
/*
* Generate wpa_supplicant config for 802.11s mesh mode
*/
int wifi_gen_mesh(struct lyd_node *cif)
{
const char *ifname, *mesh_id, *secret_name, *radio;
struct lyd_node *mesh, *security, *secret_node, *radio_node, *wifi;
const char *country, *band, *width;
FILE *wpa_supplicant = NULL;
unsigned char *secret = NULL;
int rc = SR_ERR_OK;
int channel, freq;
mode_t oldmask;
bool forwarding;
ifname = lydx_get_cattr(cif, "name");
wifi = lydx_get_child(cif, "wifi");
if (!wifi)
return SR_ERR_OK;
radio = lydx_get_cattr(wifi, "radio");
mesh = lydx_get_child(wifi, "mesh-point");
if (!mesh)
return SR_ERR_OK;
mesh_id = lydx_get_cattr(mesh, "mesh-id");
forwarding = lydx_is_enabled(mesh, "forwarding");
security = lydx_get_child(mesh, "security");
secret_name = lydx_get_cattr(security, "secret");
radio_node = lydx_get_xpathf(cif,
"/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio);
country = lydx_get_cattr(radio_node, "country-code");
band = lydx_get_cattr(radio_node, "band");
width = lydx_get_cattr(radio_node, "channel-width");
channel = atoi(lydx_get_cattr(radio_node, "channel") ? : "0");
if (!channel && band && width && !strcmp(width, "auto")) {
if (!strcmp(band, "2.4GHz"))
channel = 6;
else if (!strcmp(band, "5GHz"))
channel = 36;
else if (!strcmp(band, "6GHz"))
channel = 1;
}
freq = wifi_chan_to_freq(channel, band);
if (secret_name) {
const char *b64;
secret_node = lydx_get_xpathf(cif,
"../../keystore/symmetric-keys/symmetric-key[name='%s']",
secret_name);
b64 = lydx_get_cattr(secret_node, "cleartext-symmetric-key");
if (b64)
secret = base64_decode((const unsigned char *)b64, strlen(b64), NULL);
}
oldmask = umask(0077);
wpa_supplicant = fopenf("w", WPA_SUPPLICANT_CONF, ifname);
if (!wpa_supplicant) {
rc = SR_ERR_INTERNAL;
goto out;
}
fprintf(wpa_supplicant, "ctrl_interface=/run/wpa_supplicant\n");
if (country)
fprintf(wpa_supplicant, "country=%s\n", country);
/* Global scope only (maps to peer_link_timeout): raise the 300s
* default so a fixed backhaul peer is not idled out and re-peered
* (MESH_CLOSE, reason 55), which briefly breaks forwarding. */
fprintf(wpa_supplicant, "mesh_max_inactivity=3600\n");
fprintf(wpa_supplicant, "\nnetwork={\n");
fprintf(wpa_supplicant, " mode=5\n");
fprintf(wpa_supplicant, " ssid=\"%s\"\n", mesh_id);
fprintf(wpa_supplicant, " frequency=%d\n", freq);
if (band && !strcmp(band, "6GHz") && (!width || !strcmp(width, "auto"))) {
int center = wifi_center_chan_80(channel);
fprintf(wpa_supplicant, " ht40=1\n");
fprintf(wpa_supplicant, " he=1\n");
fprintf(wpa_supplicant, " max_oper_chwidth=1\n");
if (center)
fprintf(wpa_supplicant, " vht_center_freq1=%d\n", wifi_chan_to_freq(center, band));
} else if (width && strcmp(width, "auto")) {
if (!strcmp(width, "20MHz")) {
fprintf(wpa_supplicant, " disable_ht40=1\n");
} else if (!strcmp(width, "40MHz")) {
fprintf(wpa_supplicant, " ht40=1\n");
} else if (!strcmp(width, "80MHz")) {
int center = wifi_center_chan_80(channel);
fprintf(wpa_supplicant, " ht40=1\n");
if (!strcmp(band, "6GHz"))
fprintf(wpa_supplicant, " he=1\n");
else
fprintf(wpa_supplicant, " vht=1\n");
fprintf(wpa_supplicant, " max_oper_chwidth=1\n");
if (center)
fprintf(wpa_supplicant, " vht_center_freq1=%d\n", wifi_chan_to_freq(center, band));
} else if (!strcmp(width, "160MHz")) {
int center = wifi_center_chan_160(channel);
fprintf(wpa_supplicant, " ht40=1\n");
if (!strcmp(band, "6GHz"))
fprintf(wpa_supplicant, " he=1\n");
else
fprintf(wpa_supplicant, " vht=1\n");
fprintf(wpa_supplicant, " max_oper_chwidth=2\n");
if (center)
fprintf(wpa_supplicant, " vht_center_freq1=%d\n", wifi_chan_to_freq(center, band));
}
}
fprintf(wpa_supplicant, " mesh_fwding=%d\n", forwarding ? 1 : 0);
fprintf(wpa_supplicant, " key_mgmt=SAE\n");
fprintf(wpa_supplicant, " ieee80211w=2\n");
if (secret)
fprintf(wpa_supplicant, " sae_password=\"%s\"\n", secret);
fprintf(wpa_supplicant, "}\n");
out:
free(secret);
if (wpa_supplicant)
fclose(wpa_supplicant);
umask(oldmask);
return rc;
}
/*
* Get probe-timeout for a radio from sysrepo config.
* Returns 0 if not set.
@@ -283,7 +485,7 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n");
fprintf(iw, "# Create %s interface %s on radio %s\n",
mode == wifi_station ? "station" : "access point", ifname, radio);
mode == wifi_station ? "station" : (mode == wifi_mesh ? "mesh" : "access point"), ifname, radio);
/* Wait for PHY if probe-timeout is set (slow USB dongles) */
if (probe_timeout > 0) {
@@ -317,6 +519,12 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
case wifi_ap:
fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname);
break;
case wifi_mesh:
fprintf(iw, "iw phy %s interface add %s type mesh\n", radio, ifname);
wifi_gen_mesh(cif);
fprintf(iw, "initctl -bfq enable mesh@%s\n", ifname);
fprintf(iw, "initctl -bfq touch mesh@%s\n", ifname);
break;
default:
ERROR("WiFi mode %d unknown", mode);
rc = SR_ERR_INVAL_ARG;
@@ -350,9 +558,16 @@ int wifi_del_iface(struct lyd_node *dif, struct dagger *net)
fprintf(iw, "iw dev %s del 2>/dev/null || ip link del %s 2>/dev/null || true\n", ifname, ifname);
wifi = lydx_get_child(dif, "wifi");
if (wifi && wifi_get_mode(wifi) != wifi_ap) {
erasef(WPA_SUPPLICANT_CONF, ifname);
fprintf(iw, "initctl -bfq disable wifi@%s\n", ifname);
if (wifi) {
int mode = wifi_get_mode(wifi);
if (mode == wifi_mesh) {
erasef(WPA_SUPPLICANT_CONF, ifname);
fprintf(iw, "initctl -bfq disable mesh@%s\n", ifname);
} else if (mode != wifi_ap) {
erasef(WPA_SUPPLICANT_CONF, ifname);
fprintf(iw, "initctl -bfq disable wifi@%s\n", ifname);
}
}
fclose(iw);
+2
View File
@@ -428,6 +428,8 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
if (wifi_get_mode(cif) == wifi_station)
return wifi_validate_secret(session, cif)
? : wifi_gen_station(cif);
if (wifi_get_mode(cif) == wifi_mesh)
return wifi_gen_mesh(cif);
return 0;
case IFT_DUMMY:
case IFT_GRE:
+2
View File
@@ -130,6 +130,7 @@ int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
typedef enum wifi_mode_t {
wifi_station,
wifi_ap,
wifi_mesh,
wifi_unknown
} wifi_mode_t;
@@ -138,6 +139,7 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net);
int wifi_del_iface(struct lyd_node *dif, struct dagger *net);
int wifi_mode_changed(struct lyd_node *wifi);
int wifi_gen_station(struct lyd_node *cif);
int wifi_gen_mesh(struct lyd_node *cif);
wifi_mode_t wifi_get_mode(struct lyd_node *wifi);
/* if-gre.c */
+14
View File
@@ -345,6 +345,20 @@ module infix-hardware {
Only applicable in Access Point mode.";
}
leaf legacy-rates {
type boolean;
default false;
description
"Allow legacy 802.11b rates (1, 2, 5.5, 11 Mbps) on 2.4 GHz.
Disabled by default: slow 802.11b clients consume excessive
airtime and degrade throughput for all associated stations.
Enable only when old 2.4 GHz-only IoT devices require these
rates to associate.
No effect on 5 GHz or 6 GHz radios.";
}
leaf probe-timeout {
type uint8;
description
+2 -2
View File
@@ -939,8 +939,8 @@ submodule infix-if-bridge {
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
error-message "Bridge ports cannot have IP addresses configured.";
}
must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point" {
error-message "WiFi interfaces can only be bridge ports when configured as Access Points.";
must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point or ../infix-if:wifi/infix-if:mesh-point" {
error-message "WiFi interfaces can only be bridge ports when configured as Access Points or Mesh Points.";
}
description "Bridge association and port specific settings.";
uses bridge-port-common;
+288 -2
View File
@@ -48,6 +48,15 @@ submodule infix-if-wifi {
- Security: WPA2/WPA3 with keystore integration
- Operational state: Connection status, RSSI, client lists";
revision 2026-03-06 {
description
"Adding mesh support and roaming.
- Add 802.11s mesh point mode support.
- Add band-steering (MBO) support and OKC configuration.
- Add support for roaming, by adding support for 802.11v/k/r";
reference "internal";
}
revision 2025-12-17 {
description
"Major refactoring for AP mode support (BREAKING CHANGE):
@@ -72,6 +81,27 @@ submodule infix-if-wifi {
description "WiFi support is an optional build-time feature in Infix.";
}
typedef mobility-domain {
type union {
type string {
pattern '[0-9a-fA-F]{4}';
}
type enumeration {
enum hash {
description
"Derive mobility domain by hashing the SSID.
OpenWrt-compatible: md5(ssid), first 4 hex chars.";
}
}
}
default "4f57";
description
"802.11r Mobility Domain identifier (4 hex digits).
Use 'hash' to automatically derive it from the SSID.";
}
augment "/if:interfaces/if:interface" {
when "derived-from-or-self(if:type, 'infixift:wifi')" {
description
@@ -127,12 +157,14 @@ submodule infix-if-wifi {
Once you've identified a network, configure either:
- Station mode: Connect to an existing WiFi network
- Access Point mode: Create a WiFi network for clients
- Mesh Point mode: Create an 802.11s mesh link
Note: A radio can host either:
- Multiple AP interfaces (multi-SSID), OR
- A single Station interface
- A single Station interface, OR
- A single Mesh Point interface
Mixing AP and Station on the same radio is not supported.";
Mixing AP and Mesh Point on the same radio is not supported.";
case station {
container station {
@@ -421,6 +453,121 @@ submodule infix-if-wifi {
}
}
container roaming {
description
"Fast roaming and seamless handoff configuration.
802.11k: Radio Resource Management (neighbor discovery)
802.11r: Fast BSS Transition (fast handoff)
802.11v: BSS Transition Management (network-assisted roaming)";
container dot11k {
presence "Enable 802.11k Radio Resource Management";
description
"Enable 802.11k Radio Resource Management.
Allows clients to discover neighboring APs for better
roaming decisions. Enables:
- Neighbor reports
- Beacon reports";
}
container dot11r {
presence "Enable 802.11r Fast BSS Transition";
must "../../security/mode != 'open'" {
error-message "802.11r requires WPA2/WPA3 security and cannot be used with open networks.";
}
description
"Enable 802.11r Fast BSS Transition.
Reduces handoff latency from ~1s to <50ms through
pre-authentication. Required for seamless roaming.";
leaf mobility-domain {
type mobility-domain;
default "4f57";
description
"802.11r Mobility Domain identifier.
All APs that clients should roam between MUST use the
same mobility domain ID.
Can be either:
- A 4-character hex string (e.g., '4f57')
- 'hash' to auto-derive from SSID (OpenWrt-compatible)";
}
leaf nas-identifier {
type union {
type enumeration {
enum auto {
description
"Automatically derive NAS-Identifier as:
<interface-name>-<hostname>.<mobility-domain>.";
}
}
type string {
length "1..253";
}
}
default auto;
description
"Override NAS-Identifier used for 802.11r.
Set to 'auto' to derive:
<interface-name>-<hostname>.<mobility-domain>.
This value should be unique per AP BSS and stable.";
}
}
container dot11v {
presence "Enable 802.11v BSS Transition Management";
description
"Enable 802.11v BSS Transition Management.
Allows APs to suggest better APs to clients, improving
network-assisted roaming decisions.";
leaf band-steering {
type boolean;
default true;
description
"Multi-Band Operation (MBO) band steering.
Signals band preferences to capable clients,
encouraging dual-band devices to prefer 5GHz
over 2.4GHz when signal quality permits.
MBO steers clients via 802.11v BSS Transition
Management, hence its placement under dot11v;
enabling dot11v enables band steering by default.
Only meaningful when the same SSID is offered on
two or more bands (an access-point per radio); a
single-band BSS has no other band to steer toward.
Safe to leave enabled (default) - only activates
when both AP and client support MBO.";
}
}
leaf okc {
type boolean;
default true;
description
"Opportunistic Key Caching (OKC).
Reduces re-authentication time for roaming clients
that don't support 802.11r. The AP caches PMK from
previous associations and shares them with other APs
in the same mobility group.
Safe to leave enabled (default) - only activates when
both AP and client support it.";
}
}
/* Operational state */
container stations {
@@ -496,6 +643,145 @@ submodule infix-if-wifi {
}
}
}
case mesh-point {
container mesh-point {
presence "Configure 802.11s mesh point mode";
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:band" {
error-message "Parent radio must have 'band' configured for mesh mode";
}
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:channel" {
error-message "Parent radio must have 'channel' configured for mesh mode";
}
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:country-code != '00'" {
error-message "Country code '00' is not allowed for mesh mode.";
}
must "not(/if:interfaces/if:interface[wifi/access-point][wifi/radio = current()/../radio])" {
error-message "Mesh point and access point cannot coexist on the same radio";
}
description
"802.11s Mesh Point mode configuration.
In mesh mode, the interface creates a peer-to-peer mesh
link with other mesh points sharing the same mesh ID.
Only one mesh point interface is allowed per radio.
Mesh point and access point cannot share the same radio.
Example use case: Wireless backhaul between APs.";
leaf mesh-id {
type string {
length "1..32";
}
mandatory true;
description
"Mesh network identifier.
All mesh points that should form a mesh network
must use the same mesh ID.";
}
leaf forwarding {
type boolean;
default true;
description
"Enable layer-2 mesh forwarding (mesh_fwding).
When true, the mesh interface can be added to a Linux
bridge for transparent L2 connectivity (mesh portal).
When false, only locally destined traffic is received.";
}
container security {
description
"Mesh security configuration.
All mesh links use WPA3-SAE encryption.";
leaf secret {
type ks:central-symmetric-key-ref;
mandatory true;
description
"Pre-shared key (PSK) reference for SAE mesh.
References a symmetric key in the keystore.
All mesh points in the same mesh must share
the same key.";
}
}
/* Operational state */
container peers {
list peer {
config false;
key mac-address;
description
"List of currently connected mesh peers.";
leaf mac-address {
type yang:mac-address;
description "Mesh peer MAC address.";
}
leaf signal-strength {
type int16;
units "dBm";
description "Peer signal strength in dBm.";
}
leaf connected-time {
type uint32;
units "seconds";
description "Time since peer connected, in seconds.";
}
leaf rx-packets {
type yang:counter64;
description "Packets received from this peer.";
}
leaf tx-packets {
type yang:counter64;
description "Packets transmitted to this peer.";
}
leaf rx-bytes {
type yang:counter64;
units "octets";
description "Bytes received from this peer.";
}
leaf tx-bytes {
type yang:counter64;
units "octets";
description "Bytes transmitted to this peer.";
}
leaf rx-speed {
type uint32;
units "100 kbps";
description
"Last received data rate from this peer in 100 kbps.";
}
leaf tx-speed {
type uint32;
units "100 kbps";
description
"Last transmitted data rate to this peer in 100 kbps.";
}
}
}
}
}
}
}
}
+89 -12
View File
@@ -1382,17 +1382,85 @@ class Iface:
stations_table.print()
def pr_wifi_peers(self):
if not self.wifi:
return
mesh = self.wifi.get("mesh-point", {})
peers_data = mesh.get("peers", {})
peers = peers_data.get("peer", [])
if not peers:
return
print("\nCONNECTED PEERS:")
peers_table = SimpleTable([
Column('MAC'),
Column('SIGNAL'),
Column('TIME'),
Column('RX PKT'),
Column('TX PKT'),
Column('RX BYTES'),
Column('TX BYTES'),
Column('RX SPEED'),
Column('TX SPEED')
])
for peer in peers:
mac = peer.get("mac-address", "unknown")
signal = peer.get("signal-strength")
signal_str = signal_to_status(signal) if signal is not None else "------"
conn_time = peer.get("connected-time", 0)
time_str = f"{conn_time}s"
rx_pkt = peer.get("rx-packets", 0)
tx_pkt = peer.get("tx-packets", 0)
rx_bytes = peer.get("rx-bytes", 0)
tx_bytes = peer.get("tx-bytes", 0)
rx_speed = peer.get("rx-speed", 0)
tx_speed = peer.get("tx-speed", 0)
rx_speed_str = f"{rx_speed / 10:.1f}" if rx_speed else "-"
tx_speed_str = f"{tx_speed / 10:.1f}" if tx_speed else "-"
peers_table.row(mac, signal_str, time_str, rx_pkt, tx_pkt,
rx_bytes, tx_bytes, rx_speed_str, tx_speed_str)
peers_table.print()
def pr_proto_wifi(self, pipe=''):
if self.wifi and (ap := self.wifi.get("access-point", {})):
ssid = ap.get("ssid", "------")
stations = ap.get("stations", {}).get("station", [])
data_str = f"access-point ssid: {ssid} stations: {len(stations)}"
elif self.wifi and (station := self.wifi.get("station", {})):
ssid = station.get("ssid", "------")
data_str = f"station ssid: {ssid}"
if (signal := station.get("signal-strength")) is not None:
data_str += f" signal: {signal_to_status(signal)}"
ssid = None
signal = None
mode = None
if self.wifi:
if "access-point" in self.wifi:
ap = self.wifi["access-point"]
ssid = ap.get("ssid", "------")
mode = "AP"
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
station_count = len(stations)
data_str = f"{mode}, ssid: {ssid}, stations: {station_count}"
elif "mesh-point" in self.wifi:
mesh = self.wifi["mesh-point"]
mesh_id = mesh.get("mesh-id", "------")
mode = "Mesh"
peers_data = mesh.get("peers", {})
peers = peers_data.get("peer", [])
data_str = f"{mode}, mesh-id: {mesh_id}, peers: {len(peers)}"
else:
station=self.wifi.get("station", {})
ssid = station.get("ssid", "------")
signal = station.get("signal-strength")
mode = "Station"
if signal is not None:
signal_str = signal_to_status(signal)
data_str = f"{mode}, ssid: {ssid}, signal: {signal_str}"
else:
data_str = f"{mode}, ssid: {ssid}"
else:
data_str = "ssid: ------"
@@ -1664,9 +1732,8 @@ class Iface:
print(f"{'out-octets':<{19}}: {self.out_octets}")
if self.wifi:
# Detect mode: AP has "stations", Station has "signal-strength" or "scan-results"
ap = self.wifi.get('access-point')
if ap:
if "access-point" in self.wifi:
ap = self.wifi['access-point']
mode = "access-point"
ssid = ap.get('ssid', "----")
stations_data = ap.get("stations", {})
@@ -1675,6 +1742,16 @@ class Iface:
print(f"{'ssid':<{19}}: {ssid}")
print(f"{'connected stations':<{19}}: {len(stations)}")
self.pr_wifi_stations()
elif "mesh-point" in self.wifi:
mesh = self.wifi['mesh-point']
mode = "mesh-point"
mesh_id = mesh.get('mesh-id', "----")
peers_data = mesh.get("peers", {})
peers = peers_data.get("peer", [])
print(f"{'mode':<{20}}: {mode}")
print(f"{'mesh-id':<{20}}: {mesh_id}")
print(f"{'connected peers':<{20}}: {len(peers)}")
self.pr_wifi_peers()
else:
mode = "station"
station = self.wifi.get('station', {})
@@ -58,6 +58,21 @@ def wifi_ap(ifname):
return {'access-point': ap_data} if ap_data else {}
def wifi_mesh(ifname, info=None):
"""Get operational data for mesh point mode using iw"""
mesh_data = {}
if info is None:
info = get_iw_info(ifname)
if info.get('ssid'):
mesh_data['mesh-id'] = info['ssid']
peers = get_iw_stations(ifname)
if peers:
mesh_data['peers'] = {'peer': peers}
return {'mesh-point': mesh_data}
def wifi_station(ifname):
"""Get operational data for Station mode using iw + wpa_cli for scanning"""
station_data = {}
@@ -100,6 +115,8 @@ def wifi(ifname):
if mode == 'ap':
result.update(wifi_ap(ifname))
elif mode == 'mesh point':
result.update(wifi_mesh(ifname, info))
else:
result.update(wifi_station(ifname))