mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-30 04:33:00 +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():
|
for line in output.splitlines():
|
||||||
stripped = line.strip()
|
stripped = line.strip()
|
||||||
|
|
||||||
# Interface type
|
# Interface type (can be multi-word, e.g. "mesh point")
|
||||||
if stripped.startswith('type '):
|
if stripped.startswith('type '):
|
||||||
result['iftype'] = stripped.split()[1]
|
result['iftype'] = ' '.join(stripped.split()[1:])
|
||||||
|
|
||||||
# MAC address
|
# MAC address
|
||||||
elif stripped.startswith('addr '):
|
elif stripped.startswith('addr '):
|
||||||
result['mac'] = stripped.split()[1]
|
result['mac'] = stripped.split()[1]
|
||||||
|
|
||||||
# SSID
|
# SSID (AP mode) or mesh-id (mesh point mode) — kernel uses same attr
|
||||||
elif stripped.startswith('ssid '):
|
elif stripped.startswith('ssid '):
|
||||||
result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:]))
|
result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:]))
|
||||||
|
|
||||||
@@ -538,6 +538,43 @@ def parse_link(ifname):
|
|||||||
return result
|
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():
|
def main():
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
@@ -548,7 +585,8 @@ def main():
|
|||||||
'info': 'Get PHY or interface information (requires device)',
|
'info': 'Get PHY or interface information (requires device)',
|
||||||
'survey': 'Get channel survey data (requires interface)',
|
'survey': 'Get channel survey data (requires interface)',
|
||||||
'station': 'Get connected stations in AP mode (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': [
|
'examples': [
|
||||||
'iw.py list',
|
'iw.py list',
|
||||||
@@ -557,7 +595,8 @@ def main():
|
|||||||
'iw.py info wlan0',
|
'iw.py info wlan0',
|
||||||
'iw.py station wifi0',
|
'iw.py station wifi0',
|
||||||
'iw.py link wlan0',
|
'iw.py link wlan0',
|
||||||
'iw.py survey wlan0'
|
'iw.py survey wlan0',
|
||||||
|
'iw.py caps radio0'
|
||||||
]
|
]
|
||||||
}, indent=2))
|
}, indent=2))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -594,6 +633,11 @@ def main():
|
|||||||
data = {'error': 'survey command requires interface argument'}
|
data = {'error': 'survey command requires interface argument'}
|
||||||
else:
|
else:
|
||||||
data = parse_survey(sys.argv[2])
|
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:
|
else:
|
||||||
data = {'error': f'Unknown command: {command}'}
|
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.
|
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
|
[v26.05.0][] - 2026-05-29
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ regression test system solely relies on NETCONF and RESTCONF.
|
|||||||
- [Introduction](introduction.md)
|
- [Introduction](introduction.md)
|
||||||
- [System Configuration](system.md)
|
- [System Configuration](system.md)
|
||||||
- [Network Configuration](networking.md)
|
- [Network Configuration](networking.md)
|
||||||
|
- [Wi-Fi](wifi.md)
|
||||||
- [DHCP Server](dhcp.md)
|
- [DHCP Server](dhcp.md)
|
||||||
- [Syslog Support](syslog.md)
|
- [Syslog Support](syslog.md)
|
||||||
- **Infix In-Depth**
|
- **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`,
|
- `channel-width`: AP channel bandwidth. Supported values are `auto`, `20MHz`,
|
||||||
`40MHz`, `80MHz`, and `160MHz`. Wider channels require matching hardware,
|
`40MHz`, `80MHz`, and `160MHz`. Wider channels require matching hardware,
|
||||||
regulatory approval, and are only available on 5GHz/6GHz where supported.
|
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
|
- `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
|
to a non-zero value (e.g., 30) for USB WiFi dongles that are slow to
|
||||||
initialize due to firmware loading
|
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
|
> constraints and hardware capabilities. Channel width can now be set
|
||||||
> explicitly for AP mode, or left at `auto` to let the driver choose.
|
> 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 Support
|
||||||
|
|
||||||
WiFi 6 (802.11ax) is always enabled in AP mode on all bands, providing improved
|
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
|
> 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).
|
> 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
|
Fast roaming enables seamless client handoff between access points through
|
||||||
into your LAN:
|
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>
|
<pre class="cli"><code>admin@example:/> <b>configure</b>
|
||||||
admin@example:/config/> <b>edit interface br0</b>
|
admin@example:/config/> <b>edit hardware component radio1 wifi-radio</b>
|
||||||
admin@example:/config/interface/br0/> <b>set type bridge</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/> <b>edit interface wifi0</b>
|
admin@example:/config/hardware/component/radio1/wifi-radio/> <b>set channel 36</b>
|
||||||
admin@example:/config/interface/wifi0/> <b>set bridge-port bridge br0</b>
|
admin@example:/config/hardware/component/radio1/wifi-radio/> <b>leave</b>
|
||||||
admin@example:/config/interface/wifi0/> <b>leave</b>
|
|
||||||
</code></pre>
|
</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
|
## Troubleshooting
|
||||||
|
|
||||||
Use `show interface wifi0` to verify signal strength and connection status.
|
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_DEBUG_SYSLOG
|
||||||
select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN
|
select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN
|
||||||
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
|
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_WIRELESS_REGDB
|
||||||
select BR2_PACKAGE_HOSTAPD
|
select BR2_PACKAGE_HOSTAPD
|
||||||
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
|
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
|
||||||
select BR2_PACKAGE_HOSTAPD_WPA3
|
select BR2_PACKAGE_HOSTAPD_WPA3
|
||||||
select BR2_PACKAGE_HOSTAPD_WPS
|
select BR2_PACKAGE_HOSTAPD_WPS
|
||||||
|
select BR2_PACKAGE_HOSTAPD_WNM
|
||||||
|
select BR2_PACKAGE_HOSTAPD_MBO
|
||||||
select BR2_PACKAGE_IW
|
select BR2_PACKAGE_IW
|
||||||
help
|
help
|
||||||
Enables WiFi in Infix. Enables all requried applications.
|
Enables WiFi in Infix. Enables all requried applications.
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ define FEATURE_WIFI_LINUX_CONFIG_FIXUPS
|
|||||||
endef
|
endef
|
||||||
|
|
||||||
define FEATURE_WIFI_INSTALL_IN_ROMFS
|
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)/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)/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
|
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([sysrepo], [sysrepo >= 4.2.10])
|
||||||
PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2])
|
PKG_CHECK_MODULES([libyang], [libyang >= 4.2.2])
|
||||||
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
|
PKG_CHECK_MODULES([libsrx], [libsrx >= 1.0.0])
|
||||||
|
PKG_CHECK_MODULES([libcrypto], [libcrypto])
|
||||||
|
|
||||||
AC_CHECK_HEADER([ev.h],
|
AC_CHECK_HEADER([ev.h],
|
||||||
[saved_LIBS="$LIBS"
|
[saved_LIBS="$LIBS"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ confd_plugin_la_CFLAGS = \
|
|||||||
$(glib_CFLAGS) \
|
$(glib_CFLAGS) \
|
||||||
$(jansson_CFLAGS) \
|
$(jansson_CFLAGS) \
|
||||||
$(libite_CFLAGS) \
|
$(libite_CFLAGS) \
|
||||||
|
$(libcrypto_CFLAGS) \
|
||||||
$(sysrepo_CFLAGS) \
|
$(sysrepo_CFLAGS) \
|
||||||
$(libsrx_CFLAGS) \
|
$(libsrx_CFLAGS) \
|
||||||
$(CFLAGS)
|
$(CFLAGS)
|
||||||
@@ -26,6 +27,7 @@ confd_plugin_la_LIBADD = \
|
|||||||
$(glib_LIBS) \
|
$(glib_LIBS) \
|
||||||
$(jansson_LIBS) \
|
$(jansson_LIBS) \
|
||||||
$(libite_LIBS) \
|
$(libite_LIBS) \
|
||||||
|
$(libcrypto_LIBS) \
|
||||||
$(sysrepo_LIBS) \
|
$(sysrepo_LIBS) \
|
||||||
$(libsrx_LIBS)
|
$(libsrx_LIBS)
|
||||||
|
|
||||||
|
|||||||
+537
-38
@@ -1,9 +1,12 @@
|
|||||||
/* SPDX-License-Identifier: BSD-3-Clause */
|
/* SPDX-License-Identifier: BSD-3-Clause */
|
||||||
#include <fnmatch.h>
|
#include <fnmatch.h>
|
||||||
#include <ftw.h>
|
#include <ftw.h>
|
||||||
|
#include <glob.h>
|
||||||
#include <jansson.h>
|
#include <jansson.h>
|
||||||
#include <libgen.h>
|
#include <libgen.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
|
||||||
#include <srx/common.h>
|
#include <srx/common.h>
|
||||||
#include <srx/lyx.h>
|
#include <srx/lyx.h>
|
||||||
@@ -17,6 +20,7 @@
|
|||||||
#define XPATH_BASE_ "/ietf-hardware:hardware"
|
#define XPATH_BASE_ "/ietf-hardware:hardware"
|
||||||
#define HOSTAPD_CONF "/etc/hostapd-%s.conf"
|
#define HOSTAPD_CONF "/etc/hostapd-%s.conf"
|
||||||
#define HOSTAPD_CONF_NEXT HOSTAPD_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 "/etc/finit.d/available/gpsd.conf"
|
||||||
#define GPSD_CONF_NEXT GPSD_CONF"+"
|
#define GPSD_CONF_NEXT GPSD_CONF"+"
|
||||||
#define GPSD_MAX_DEVICES 4
|
#define GPSD_MAX_DEVICES 4
|
||||||
@@ -177,6 +181,89 @@ out_free_xpath:
|
|||||||
return err;
|
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,
|
static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio_name,
|
||||||
struct lyd_node ***iface_list, int *count)
|
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)
|
if (lydx_get_op(iface) == LYDX_OP_DELETE)
|
||||||
continue;
|
continue;
|
||||||
list = realloc(list, sizeof(struct lyd_node *) * n + 1);
|
list = realloc(list, sizeof(struct lyd_node *) * (n + 1));
|
||||||
list[n++] = iface;
|
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");
|
ap = lydx_get_child(wifi, "access-point");
|
||||||
if (!ap)
|
if (!ap)
|
||||||
continue;
|
continue;
|
||||||
list = realloc(list, sizeof(char *) *n+1);
|
list = realloc(list, sizeof(char *) * (n + 1));
|
||||||
|
|
||||||
|
|
||||||
ifname = lydx_get_cattr(cif, "name");
|
ifname = lydx_get_cattr(cif, "name");
|
||||||
list[n++] = strdup(ifname);
|
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) */
|
/* 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)
|
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;
|
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;
|
unsigned char *secret = NULL;
|
||||||
const char *ifname;
|
const char *ifname;
|
||||||
|
char nas_identifier_default[300];
|
||||||
|
char hostname[64] = "localhost";
|
||||||
char bssid[18];
|
char bssid[18];
|
||||||
|
|
||||||
ifname = lydx_get_cattr(cif, "name");
|
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);
|
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 */
|
/* Set BSSID if custom MAC is configured */
|
||||||
if (!interface_get_phys_addr(cif, bssid))
|
if (!interface_get_phys_addr(cif, bssid))
|
||||||
fprintf(hostapd, "bssid=%s\n", 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");
|
ssid = lydx_get_cattr(ap, "ssid");
|
||||||
hidden = lydx_get_cattr(ap, "hidden");
|
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)
|
if (ssid)
|
||||||
fprintf(hostapd, "ssid=%s\n", ssid);
|
fprintf(hostapd, "ssid=%s\n", ssid);
|
||||||
if (hidden && !strcmp(hidden, "true"))
|
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")) {
|
} else if (!strcmp(security_mode, "wpa2-personal")) {
|
||||||
fprintf(hostapd, "wpa=2\n");
|
fprintf(hostapd, "wpa=2\n");
|
||||||
/* WPA-PSK: Pre-shared key authentication */
|
/* 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 */
|
/* CCMP: AES-based encryption, mandatory for WPA2 */
|
||||||
fprintf(hostapd, "wpa_pairwise=CCMP\n");
|
fprintf(hostapd, "wpa_pairwise=CCMP\n");
|
||||||
if (secret)
|
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")) {
|
} else if (!strcmp(security_mode, "wpa3-personal")) {
|
||||||
fprintf(hostapd, "wpa=2\n");
|
fprintf(hostapd, "wpa=2\n");
|
||||||
/* SAE: Simultaneous Authentication of Equals, resistant to offline dictionary attacks */
|
/* 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");
|
fprintf(hostapd, "rsn_pairwise=CCMP\n");
|
||||||
if (secret)
|
if (secret)
|
||||||
fprintf(hostapd, "sae_password=%s\n", 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")) {
|
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) {
|
||||||
fprintf(hostapd, "wpa=2\n");
|
fprintf(hostapd, "wpa=2\n");
|
||||||
/* Allow both PSK (WPA2) and SAE (WPA3) authentication */
|
/* 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");
|
fprintf(hostapd, "rsn_pairwise=CCMP\n");
|
||||||
if (secret) {
|
if (secret) {
|
||||||
fprintf(hostapd, "wpa_passphrase=%s\n", 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");
|
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);
|
free(secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,19 +600,290 @@ static const char *wifi_ht40_dir(int ch)
|
|||||||
return ((ch / 4) % 2) ? "[HT40+]" : "[HT40-]";
|
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 */
|
/* 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;
|
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;
|
int ch = 0;
|
||||||
|
bool legacy_rates;
|
||||||
|
|
||||||
country = lydx_get_cattr(radio_node, "country-code");
|
country = lydx_get_cattr(radio_node, "country-code");
|
||||||
band = lydx_get_cattr(radio_node, "band");
|
band = lydx_get_cattr(radio_node, "band");
|
||||||
channel = lydx_get_cattr(radio_node, "channel");
|
channel = lydx_get_cattr(radio_node, "channel");
|
||||||
width = lydx_get_cattr(radio_node, "channel-width");
|
width = lydx_get_cattr(radio_node, "channel-width");
|
||||||
|
legacy_rates = lydx_is_enabled(radio_node, "legacy-rates");
|
||||||
if (channel && strcmp(channel, "auto"))
|
if (channel && strcmp(channel, "auto"))
|
||||||
ch = atoi(channel);
|
ch = atoi(channel);
|
||||||
|
|
||||||
|
/* Read HT/VHT hardware capabilities from PHY */
|
||||||
|
wifi_read_phy_caps(radio_name, &ht_cap, &vht_cap);
|
||||||
|
|
||||||
if (country)
|
if (country)
|
||||||
fprintf(hostapd, "country_code=%s\n", 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");
|
fprintf(hostapd, "hw_mode=g\n");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Disable legacy 802.11b rates (1, 2, 5.5, 11 Mbps).
|
* Disable legacy 802.11b rates (1, 2, 5.5, 11 Mbps)
|
||||||
* Slow clients using these rates consume excessive
|
* unless explicitly enabled via 'legacy-rates'. Slow
|
||||||
* airtime, degrading performance for all clients.
|
* 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.
|
* Rates in 0.5 Mbps units: 60=6M, 90=9M, etc.
|
||||||
*/
|
*/
|
||||||
fprintf(hostapd, "supported_rates=60 90 120 180 240 360 480 540\n");
|
if (!legacy_rates) {
|
||||||
fprintf(hostapd, "basic_rates=60 120 240\n");
|
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")) {
|
} else if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz")) {
|
||||||
/* hw_mode=a: 5GHz/6GHz with 802.11a (OFDM) as baseline */
|
/* hw_mode=a: 5GHz/6GHz with 802.11a (OFDM) as baseline */
|
||||||
fprintf(hostapd, "hw_mode=a\n");
|
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 requires explicit ht_capab and vht_capab strings
|
||||||
* hostapd ignores he_oper_chwidth on 6GHz. No VHT/HT.
|
* that match hardware capabilities. Without vht_capab, 160MHz
|
||||||
*
|
* support is not advertised in beacons, causing clients to
|
||||||
* 5GHz: requires explicit ht_capab/vht_capab strings
|
* connect at 80MHz. Read capabilities from hardware via iw.py
|
||||||
* matching hardware. Without vht_capab, 160MHz is not
|
* and build the capability strings from the bitmasks.
|
||||||
* advertised in beacons.
|
|
||||||
*/
|
*/
|
||||||
if (!strcmp(band, "6GHz")) {
|
if (!strcmp(band, "6GHz")) {
|
||||||
int op_class = 133; /* default 80MHz for 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")) {
|
} else if (width && strcmp(width, "auto")) {
|
||||||
if (!strcmp(width, "20MHz")) {
|
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")) {
|
if (strcmp(band, "2.4GHz")) {
|
||||||
fprintf(hostapd, "vht_oper_chwidth=0\n");
|
fprintf(hostapd, "vht_oper_chwidth=0\n");
|
||||||
fprintf(hostapd, "he_oper_chwidth=0\n");
|
fprintf(hostapd, "he_oper_chwidth=0\n");
|
||||||
}
|
}
|
||||||
} else if (!strcmp(width, "40MHz")) {
|
} 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")) {
|
if (strcmp(band, "2.4GHz")) {
|
||||||
fprintf(hostapd, "vht_oper_chwidth=0\n");
|
fprintf(hostapd, "vht_oper_chwidth=0\n");
|
||||||
fprintf(hostapd, "he_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) {
|
} else if (!strcmp(width, "80MHz") && ch) {
|
||||||
int center = wifi_center_chan_80(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, "vht_oper_chwidth=1\n");
|
||||||
fprintf(hostapd, "he_oper_chwidth=1\n");
|
fprintf(hostapd, "he_oper_chwidth=1\n");
|
||||||
if (center) {
|
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) {
|
} else if (!strcmp(width, "160MHz") && ch) {
|
||||||
int center = wifi_center_chan_160(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, "vht_oper_chwidth=2\n");
|
||||||
fprintf(hostapd, "he_oper_chwidth=2\n");
|
fprintf(hostapd, "he_oper_chwidth=2\n");
|
||||||
if (center) {
|
if (center) {
|
||||||
@@ -648,7 +1116,7 @@ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs,
|
|||||||
fprintf(hostapd, "\n");
|
fprintf(hostapd, "\n");
|
||||||
|
|
||||||
/* Radio-specific configuration */
|
/* 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) */
|
/* Add BSS sections for secondary APs (multi-SSID) */
|
||||||
for (i = 1; i < ap_count; i++) {
|
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;
|
struct lyd_node *difs = NULL, *dif = NULL;
|
||||||
int rc = SR_ERR_OK;
|
int rc = SR_ERR_OK;
|
||||||
int gps_changed = 0;
|
int gps_changed = 0;
|
||||||
|
int wifi_changed = 0;
|
||||||
|
|
||||||
if (!lydx_find_xpathf(diff, XPATH_BASE_))
|
if (!lydx_find_xpathf(diff, XPATH_BASE_))
|
||||||
return SR_ERR_OK;
|
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_find_interfaces_on_radio(interfaces_diff, name,
|
||||||
&wifi_iface_list, &wifi_iface_count);
|
&wifi_iface_list, &wifi_iface_count);
|
||||||
if (wifi_iface_count > 0) {
|
if (wifi_iface_count > 0) {
|
||||||
bool running, enabled;
|
|
||||||
|
|
||||||
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
|
ap = lydx_get_descendant(wifi_iface_list[0], "interface", "wifi", "access-point", NULL);
|
||||||
if (ap && lydx_get_op(ap) != LYDX_OP_DELETE) {
|
if (ap && lydx_get_op(ap) != LYDX_OP_DELETE) {
|
||||||
/* AP mode - activate hostapd for radio */
|
|
||||||
snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name);
|
snprintf(src, sizeof(src), HOSTAPD_CONF_NEXT, name);
|
||||||
snprintf(dst, sizeof(dst), HOSTAPD_CONF, name);
|
snprintf(dst, sizeof(dst), HOSTAPD_CONF, name);
|
||||||
|
|
||||||
running = !systemf("initctl -bfq status hostapd:%s", name);
|
if (fexistf(HOSTAPD_CONF_NEXT, name)) {
|
||||||
enabled = fexistf(HOSTAPD_CONF_NEXT, name);
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
(void)rename(src, dst);
|
(void)rename(src, dst);
|
||||||
ap_interfaces++;
|
ap_interfaces++;
|
||||||
|
|
||||||
if (running)
|
|
||||||
finit_reloadf("hostapd@%s", name);
|
|
||||||
else
|
|
||||||
finit_enablef("hostapd@%s", name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ap_interfaces) {
|
if (!ap_interfaces) {
|
||||||
finit_disablef("hostapd@%s", name);
|
|
||||||
erasef(HOSTAPD_CONF, name);
|
erasef(HOSTAPD_CONF, name);
|
||||||
erasef(HOSTAPD_CONF_NEXT, name);
|
erasef(HOSTAPD_CONF_NEXT, name);
|
||||||
}
|
}
|
||||||
free(wifi_iface_list);
|
free(wifi_iface_list);
|
||||||
|
/* All radios share one hostapd process; the service is
|
||||||
|
* (re)generated after the component loop below. */
|
||||||
|
wifi_changed = 1;
|
||||||
continue;
|
continue;
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
@@ -909,6 +1369,45 @@ int hardware_change(sr_session_ctx_t *session, struct lyd_node *config, struct l
|
|||||||
break;
|
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:
|
err:
|
||||||
|
|
||||||
return rc;
|
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)
|
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");
|
wifi = lydx_get_child(iface, "wifi");
|
||||||
if (!wifi)
|
if (!wifi)
|
||||||
return wifi_unknown;
|
return wifi_unknown;
|
||||||
|
|
||||||
|
|
||||||
ap = lydx_get_child(wifi, "access-point");
|
ap = lydx_get_child(wifi, "access-point");
|
||||||
if (ap) {
|
if (ap) {
|
||||||
if (lydx_get_op(ap) != LYDX_OP_DELETE)
|
if (lydx_get_op(ap) != LYDX_OP_DELETE)
|
||||||
return wifi_ap;
|
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,
|
* Need to return station even if "station" also is false,
|
||||||
* because station is the default scanning mode.
|
* 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)
|
int wifi_mode_changed(struct lyd_node *wifi)
|
||||||
{
|
{
|
||||||
enum lydx_op ap_op = LYDX_OP_DELETE;
|
enum lydx_op op = LYDX_OP_DELETE;
|
||||||
struct lyd_node *ap;
|
struct lyd_node *node;
|
||||||
|
|
||||||
if (!wifi)
|
if (!wifi)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
ap = lydx_get_child(wifi, "access-point");
|
node = lydx_get_child(wifi, "access-point");
|
||||||
if (ap)
|
if (node)
|
||||||
ap_op = lydx_get_op(ap);
|
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;
|
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");
|
country = lydx_get_cattr(radio_node, "country-code");
|
||||||
|
|
||||||
if (secret_name && strcmp(security_mode, "disabled") != 0) {
|
if (secret_name && strcmp(security_mode, "disabled") != 0) {
|
||||||
@@ -171,10 +183,8 @@ int wifi_gen_station(struct lyd_node *cif)
|
|||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* autoscan drives scanning every 10s while unassociated; once
|
||||||
* Background scanning every 10 seconds while not associated, when we
|
* associated no background scanning runs (bgscan disabled below). */
|
||||||
* have an SSID (below), bgscan assumes this task.
|
|
||||||
*/
|
|
||||||
fprintf(wpa_supplicant,
|
fprintf(wpa_supplicant,
|
||||||
"ctrl_interface=/run/wpa_supplicant\n"
|
"ctrl_interface=/run/wpa_supplicant\n"
|
||||||
"autoscan=periodic:10\n"
|
"autoscan=periodic:10\n"
|
||||||
@@ -190,12 +200,15 @@ int wifi_gen_station(struct lyd_node *cif)
|
|||||||
asprintf(&security_str, "key_mgmt=NONE");
|
asprintf(&security_str, "key_mgmt=NONE");
|
||||||
else if (secret)
|
else if (secret)
|
||||||
asprintf(&security_str,
|
asprintf(&security_str,
|
||||||
"key_mgmt=SAE WPA-PSK\n"
|
"key_mgmt=FT-SAE FT-PSK SAE WPA-PSK\n"
|
||||||
" psk=\"%s\"", secret);
|
" 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,
|
fprintf(wpa_supplicant,
|
||||||
"network={\n"
|
"network={\n"
|
||||||
" bgscan=\"simple: 30:-45:300\"\n"
|
" bgscan=\"\"\n"
|
||||||
" ssid=\"%s\"\n"
|
" ssid=\"%s\"\n"
|
||||||
" %s\n"
|
" %s\n"
|
||||||
"}\n", ssid, security_str);
|
"}\n", ssid, security_str);
|
||||||
@@ -227,6 +240,195 @@ out:
|
|||||||
return rc;
|
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.
|
* Get probe-timeout for a radio from sysrepo config.
|
||||||
* Returns 0 if not set.
|
* 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, "# Generated by Infix confd - WiFi Interface Creation\n");
|
||||||
fprintf(iw, "# Create %s interface %s on radio %s\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) */
|
/* Wait for PHY if probe-timeout is set (slow USB dongles) */
|
||||||
if (probe_timeout > 0) {
|
if (probe_timeout > 0) {
|
||||||
@@ -317,6 +519,12 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
|
|||||||
case wifi_ap:
|
case wifi_ap:
|
||||||
fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname);
|
fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname);
|
||||||
break;
|
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:
|
default:
|
||||||
ERROR("WiFi mode %d unknown", mode);
|
ERROR("WiFi mode %d unknown", mode);
|
||||||
rc = SR_ERR_INVAL_ARG;
|
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);
|
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");
|
wifi = lydx_get_child(dif, "wifi");
|
||||||
if (wifi && wifi_get_mode(wifi) != wifi_ap) {
|
if (wifi) {
|
||||||
erasef(WPA_SUPPLICANT_CONF, ifname);
|
int mode = wifi_get_mode(wifi);
|
||||||
fprintf(iw, "initctl -bfq disable wifi@%s\n", ifname);
|
|
||||||
|
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);
|
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)
|
if (wifi_get_mode(cif) == wifi_station)
|
||||||
return wifi_validate_secret(session, cif)
|
return wifi_validate_secret(session, cif)
|
||||||
? : wifi_gen_station(cif);
|
? : wifi_gen_station(cif);
|
||||||
|
if (wifi_get_mode(cif) == wifi_mesh)
|
||||||
|
return wifi_gen_mesh(cif);
|
||||||
return 0;
|
return 0;
|
||||||
case IFT_DUMMY:
|
case IFT_DUMMY:
|
||||||
case IFT_GRE:
|
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 {
|
typedef enum wifi_mode_t {
|
||||||
wifi_station,
|
wifi_station,
|
||||||
wifi_ap,
|
wifi_ap,
|
||||||
|
wifi_mesh,
|
||||||
wifi_unknown
|
wifi_unknown
|
||||||
} wifi_mode_t;
|
} 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_del_iface(struct lyd_node *dif, struct dagger *net);
|
||||||
int wifi_mode_changed(struct lyd_node *wifi);
|
int wifi_mode_changed(struct lyd_node *wifi);
|
||||||
int wifi_gen_station(struct lyd_node *cif);
|
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);
|
wifi_mode_t wifi_get_mode(struct lyd_node *wifi);
|
||||||
|
|
||||||
/* if-gre.c */
|
/* if-gre.c */
|
||||||
|
|||||||
@@ -345,6 +345,20 @@ module infix-hardware {
|
|||||||
Only applicable in Access Point mode.";
|
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 {
|
leaf probe-timeout {
|
||||||
type uint8;
|
type uint8;
|
||||||
description
|
description
|
||||||
|
|||||||
@@ -939,8 +939,8 @@ submodule infix-if-bridge {
|
|||||||
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
|
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
|
||||||
error-message "Bridge ports cannot have IP addresses configured.";
|
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" {
|
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.";
|
error-message "WiFi interfaces can only be bridge ports when configured as Access Points or Mesh Points.";
|
||||||
}
|
}
|
||||||
description "Bridge association and port specific settings.";
|
description "Bridge association and port specific settings.";
|
||||||
uses bridge-port-common;
|
uses bridge-port-common;
|
||||||
|
|||||||
@@ -48,6 +48,15 @@ submodule infix-if-wifi {
|
|||||||
- Security: WPA2/WPA3 with keystore integration
|
- Security: WPA2/WPA3 with keystore integration
|
||||||
- Operational state: Connection status, RSSI, client lists";
|
- 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 {
|
revision 2025-12-17 {
|
||||||
description
|
description
|
||||||
"Major refactoring for AP mode support (BREAKING CHANGE):
|
"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.";
|
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" {
|
augment "/if:interfaces/if:interface" {
|
||||||
when "derived-from-or-self(if:type, 'infixift:wifi')" {
|
when "derived-from-or-self(if:type, 'infixift:wifi')" {
|
||||||
description
|
description
|
||||||
@@ -127,12 +157,14 @@ submodule infix-if-wifi {
|
|||||||
Once you've identified a network, configure either:
|
Once you've identified a network, configure either:
|
||||||
- Station mode: Connect to an existing WiFi network
|
- Station mode: Connect to an existing WiFi network
|
||||||
- Access Point mode: Create a WiFi network for clients
|
- Access Point mode: Create a WiFi network for clients
|
||||||
|
- Mesh Point mode: Create an 802.11s mesh link
|
||||||
|
|
||||||
Note: A radio can host either:
|
Note: A radio can host either:
|
||||||
- Multiple AP interfaces (multi-SSID), OR
|
- 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 {
|
case station {
|
||||||
container 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 */
|
/* Operational state */
|
||||||
|
|
||||||
container stations {
|
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()
|
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=''):
|
def pr_proto_wifi(self, pipe=''):
|
||||||
if self.wifi and (ap := self.wifi.get("access-point", {})):
|
ssid = None
|
||||||
ssid = ap.get("ssid", "------")
|
signal = None
|
||||||
stations = ap.get("stations", {}).get("station", [])
|
mode = None
|
||||||
data_str = f"access-point ssid: {ssid} stations: {len(stations)}"
|
|
||||||
elif self.wifi and (station := self.wifi.get("station", {})):
|
if self.wifi:
|
||||||
ssid = station.get("ssid", "------")
|
if "access-point" in self.wifi:
|
||||||
data_str = f"station ssid: {ssid}"
|
ap = self.wifi["access-point"]
|
||||||
if (signal := station.get("signal-strength")) is not None:
|
ssid = ap.get("ssid", "------")
|
||||||
data_str += f" signal: {signal_to_status(signal)}"
|
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:
|
else:
|
||||||
data_str = "ssid: ------"
|
data_str = "ssid: ------"
|
||||||
|
|
||||||
@@ -1664,9 +1732,8 @@ class Iface:
|
|||||||
print(f"{'out-octets':<{19}}: {self.out_octets}")
|
print(f"{'out-octets':<{19}}: {self.out_octets}")
|
||||||
|
|
||||||
if self.wifi:
|
if self.wifi:
|
||||||
# Detect mode: AP has "stations", Station has "signal-strength" or "scan-results"
|
if "access-point" in self.wifi:
|
||||||
ap = self.wifi.get('access-point')
|
ap = self.wifi['access-point']
|
||||||
if ap:
|
|
||||||
mode = "access-point"
|
mode = "access-point"
|
||||||
ssid = ap.get('ssid', "----")
|
ssid = ap.get('ssid', "----")
|
||||||
stations_data = ap.get("stations", {})
|
stations_data = ap.get("stations", {})
|
||||||
@@ -1675,6 +1742,16 @@ class Iface:
|
|||||||
print(f"{'ssid':<{19}}: {ssid}")
|
print(f"{'ssid':<{19}}: {ssid}")
|
||||||
print(f"{'connected stations':<{19}}: {len(stations)}")
|
print(f"{'connected stations':<{19}}: {len(stations)}")
|
||||||
self.pr_wifi_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:
|
else:
|
||||||
mode = "station"
|
mode = "station"
|
||||||
station = self.wifi.get('station', {})
|
station = self.wifi.get('station', {})
|
||||||
|
|||||||
@@ -58,6 +58,21 @@ def wifi_ap(ifname):
|
|||||||
return {'access-point': ap_data} if ap_data else {}
|
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):
|
def wifi_station(ifname):
|
||||||
"""Get operational data for Station mode using iw + wpa_cli for scanning"""
|
"""Get operational data for Station mode using iw + wpa_cli for scanning"""
|
||||||
station_data = {}
|
station_data = {}
|
||||||
@@ -100,6 +115,8 @@ def wifi(ifname):
|
|||||||
|
|
||||||
if mode == 'ap':
|
if mode == 'ap':
|
||||||
result.update(wifi_ap(ifname))
|
result.update(wifi_ap(ifname))
|
||||||
|
elif mode == 'mesh point':
|
||||||
|
result.update(wifi_mesh(ifname, info))
|
||||||
else:
|
else:
|
||||||
result.update(wifi_station(ifname))
|
result.update(wifi_station(ifname))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user