mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-23 17:33:01 +02:00
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:
@@ -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
|
||||
@@ -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}'}
|
||||
|
||||
|
||||
+1
-1
Submodule buildroot updated: a40d69265e...d758952b3e
@@ -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
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+54
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user