Wi-Fi: Add support for mesh-point (802.11s)

This commit is contained in:
Mattias Walström
2026-06-05 14:54:53 +02:00
parent 44ade8b761
commit 61d4325d2c
12 changed files with 857 additions and 73 deletions
@@ -0,0 +1,3 @@
service name:wpa_supplicant :%i \
[2345] wpa_supplicant -s -i %i -c /etc/wpa_supplicant-%i.conf -P/var/run/wpa_supplicant-%i.pid \
-- Wi-Fi Mesh @%i
+49 -5
View File
@@ -257,15 +257,15 @@ def parse_interface_info(ifname):
for line in output.splitlines():
stripped = line.strip()
# Interface type
# Interface type (can be multi-word, e.g. "mesh point")
if stripped.startswith('type '):
result['iftype'] = stripped.split()[1]
result['iftype'] = ' '.join(stripped.split()[1:])
# MAC address
elif stripped.startswith('addr '):
result['mac'] = stripped.split()[1]
# SSID
# SSID (AP mode) or mesh-id (mesh point mode) — kernel uses same attr
elif stripped.startswith('ssid '):
result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:]))
@@ -538,6 +538,43 @@ def parse_link(ifname):
return result
def parse_phy_caps(phy_name):
"""
Parse 'iw phy <name> info' for HT and VHT capability bitmasks.
Returns: {ht_cap: int, vht_cap: int}
iw phy info output format:
Capabilities: 0x1ef
...
VHT Capabilities (0x339071b2):
...
"""
actual_phy = normalize_phy_name(phy_name)
output = run_iw('phy', actual_phy, 'info')
if not output:
output = run_iw(actual_phy, 'info')
if not output:
return {'ht_cap': 0, 'vht_cap': 0}
ht_cap = 0
vht_cap = 0
for line in output.splitlines():
stripped = line.strip()
# HT Capabilities: "Capabilities: 0x1ef"
ht_match = re.match(r'Capabilities:\s+(0x[0-9a-fA-F]+)', stripped)
if ht_match:
ht_cap = int(ht_match.group(1), 16)
# VHT Capabilities: "VHT Capabilities (0x339071b2):"
vht_match = re.match(r'VHT Capabilities\s+\((0x[0-9a-fA-F]+)\)', stripped)
if vht_match:
vht_cap = int(vht_match.group(1), 16)
return {'ht_cap': ht_cap, 'vht_cap': vht_cap}
def main():
if len(sys.argv) < 2:
print(json.dumps({
@@ -548,7 +585,8 @@ def main():
'info': 'Get PHY or interface information (requires device)',
'survey': 'Get channel survey data (requires interface)',
'station': 'Get connected stations in AP mode (requires interface)',
'link': 'Get link info in station mode (requires interface)'
'link': 'Get link info in station mode (requires interface)',
'caps': 'Get HT/VHT capability bitmasks (requires PHY/radio)'
},
'examples': [
'iw.py list',
@@ -557,7 +595,8 @@ def main():
'iw.py info wlan0',
'iw.py station wifi0',
'iw.py link wlan0',
'iw.py survey wlan0'
'iw.py survey wlan0',
'iw.py caps radio0'
]
}, indent=2))
sys.exit(1)
@@ -594,6 +633,11 @@ def main():
data = {'error': 'survey command requires interface argument'}
else:
data = parse_survey(sys.argv[2])
elif command == 'caps':
if len(sys.argv) < 3:
data = {'error': 'caps command requires PHY/radio argument'}
else:
data = parse_phy_caps(sys.argv[2])
else:
data = {'error': f'Unknown command: {command}'}
+2
View File
@@ -5,6 +5,8 @@ config BR2_PACKAGE_FEATURE_WIFI
select BR2_PACKAGE_WPA_SUPPLICANT_DEBUG_SYSLOG
select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN
select BR2_PACKAGE_WPA_SUPPLICANT_CLI
select BR2_PACKAGE_WPA_SUPPLICANT_AP_SUPPORT
select BR2_PACKAGE_WPA_SUPPLICANT_MESH_NETWORKING
select BR2_PACKAGE_WIRELESS_REGDB
select BR2_PACKAGE_HOSTAPD
select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211
+1 -1
View File
@@ -69,7 +69,7 @@ define FEATURE_WIFI_LINUX_CONFIG_FIXUPS
endef
define FEATURE_WIFI_INSTALL_IN_ROMFS
mkdir -p $(TARGET_DIR)/etc/modprobe.d $(TARGET_DIR)/etc/udev/rules.d
mkdir -p $(TARGET_DIR)/etc/modprobe.d $(TARGET_DIR)/etc/udev/rules.d $(TARGET_DIR)/usr/libexec
cp $(FEATURE_WIFI_PKGDIR)/mt7915e.conf $(TARGET_DIR)/etc/modprobe.d/mt7915e.conf
cp $(FEATURE_WIFI_PKGDIR)/60-rename-wifi-phy.rules $(TARGET_DIR)/etc/udev/rules.d/60-rename-wifi-phy.rules
cp $(FEATURE_WIFI_PKGDIR)/70-remove-virtual-wifi-interfaces.rules $(TARGET_DIR)/etc/udev/rules.d/70-remove-virtual-wifi-interfaces.rules
+308 -24
View File
@@ -221,12 +221,11 @@ static const char *resolve_mobility_domain(const char *mobility_domain, const ch
/*
* Find an AP interface on a higher-band radio (5/6 GHz) advertising the
* same SSID as the caller's 2.4 GHz BSS. Used to emit
* no_probe_resp_if_seen_on=, which suppresses 2.4 GHz probe responses to
* clients that have recently probed the 5/6 GHz twin, forcing dual-band
* clients to discover the higher band. 2.4-only clients are unaffected:
* hostapd only suppresses if it has actually seen the MAC on the listed
* interface, so a 2.4-only client always gets a probe response.
* 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.
*/
@@ -286,7 +285,7 @@ static int wifi_find_interfaces_on_radio(struct lyd_node *ifs, const char *radio
if (lydx_get_op(iface) == LYDX_OP_DELETE)
continue;
list = realloc(list, sizeof(struct lyd_node *) * n + 1);
list = realloc(list, sizeof(struct lyd_node *) * (n + 1));
list[n++] = iface;
}
@@ -316,8 +315,7 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name,
ap = lydx_get_child(wifi, "access-point");
if (!ap)
continue;
list = realloc(list, sizeof(char *) *n+1);
list = realloc(list, sizeof(char *) * (n + 1));
ifname = lydx_get_cattr(cif, "name");
list[n++] = strdup(ifname);
@@ -484,7 +482,10 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
if (enable_80211r) {
fprintf(hostapd, "# Fast BSS Transition (802.11r)\n");
fprintf(hostapd, "mobility_domain=%s\n", mobility_domain);
fprintf(hostapd, "ft_over_ds=1\n");
/* 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);
}
@@ -510,7 +511,9 @@ static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd
fprintf(hostapd, "okc=1\n");
}
/* MBO: Multi-Band Operation (band steering, needs 802.11v above) */
/* 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;
@@ -595,10 +598,276 @@ static const char *wifi_ht40_dir(int ch)
return ((ch / 4) % 2) ? "[HT40+]" : "[HT40-]";
}
/*
* Read HT/VHT capability bitmasks from hardware via iw.py.
* Returns 0 on success, -1 on failure.
*/
static int wifi_read_phy_caps(const char *radio_name, unsigned int *ht_cap, unsigned int *vht_cap)
{
json_error_t jerr;
json_t *root, *jht, *jvht;
char buf[256];
size_t len;
FILE *pp;
*ht_cap = 0;
*vht_cap = 0;
pp = popenf("r", "/usr/libexec/infix/iw.py caps %s", radio_name);
if (!pp)
return -1;
len = fread(buf, 1, sizeof(buf) - 1, pp);
pclose(pp);
buf[len] = '\0';
/*
* Parse JSON output: {"ht_cap": NNN, "vht_cap": NNN}
* Use jansson since hardware.c already includes it.
*/
root = json_loads(buf, 0, &jerr);
if (!root)
return -1;
jht = json_object_get(root, "ht_cap");
jvht = json_object_get(root, "vht_cap");
if (json_is_integer(jht))
*ht_cap = (unsigned int)json_integer_value(jht);
if (json_is_integer(jvht))
*vht_cap = (unsigned int)json_integer_value(jvht);
json_decref(root);
return 0;
}
/*
* Build hostapd ht_capab string from HT capability bitmask.
* IEEE 802.11-2016 Table 9-153 — HT Capabilities Info field
*
* The ht40_dir string ("[HT40+]" or "[HT40-]") is prepended when
* the configured channel width is 40MHz or wider.
*/
static void wifi_build_ht_capab(char *out, size_t sz, unsigned int ht_cap,
const char *ht40_dir, int want_ht40)
{
char *p = out;
size_t rem = sz;
int n;
*p = '\0';
if (want_ht40 && ht40_dir) {
n = snprintf(p, rem, "%s", ht40_dir);
p += n;
rem -= n;
}
if (ht_cap & 0x0001) {
n = snprintf(p, rem, "[LDPC]");
p += n;
rem -= n;
}
if (ht_cap & 0x0020) {
n = snprintf(p, rem, "[SHORT-GI-20]");
p += n;
rem -= n;
}
if (ht_cap & 0x0040) {
n = snprintf(p, rem, "[SHORT-GI-40]");
p += n;
rem -= n;
}
if (ht_cap & 0x0080) {
n = snprintf(p, rem, "[TX-STBC]");
p += n;
rem -= n;
}
/* RX-STBC: bits 8-9 */
switch ((ht_cap >> 8) & 0x3) {
case 1:
n = snprintf(p, rem, "[RX-STBC1]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[RX-STBC12]");
p += n;
rem -= n;
break;
case 3:
n = snprintf(p, rem, "[RX-STBC123]");
p += n;
rem -= n;
break;
}
/* Max A-MSDU Length: bit 11 */
if (ht_cap & 0x0800) {
n = snprintf(p, rem, "[MAX-AMSDU-7935]");
p += n;
rem -= n;
}
}
/*
* Build hostapd vht_capab string from VHT capability bitmask.
* IEEE 802.11-2016 Table 9-250 — VHT Capabilities Info field
*
* When the configured width is less than 160MHz, the VHT160 and
* SHORT-GI-160 flags are masked out to prevent hostapd from
* advertising capabilities the configuration does not use.
*/
static void wifi_build_vht_capab(char *out, size_t sz, unsigned int vht_cap, int chwidth)
{
char *p = out;
size_t rem = sz;
int n;
*p = '\0';
/* Max MPDU Length: bits 0-1 */
switch (vht_cap & 0x3) {
case 1:
n = snprintf(p, rem, "[MAX-MPDU-7991]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[MAX-MPDU-11454]");
p += n;
rem -= n;
break;
}
/* Supported Channel Width: bits 2-3 */
if (chwidth >= 2) {
switch ((vht_cap >> 2) & 0x3) {
case 1:
n = snprintf(p, rem, "[VHT160]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[VHT160-80PLUS80]");
p += n;
rem -= n;
break;
}
}
/* RXLDPC: bit 4 */
if (vht_cap & 0x10) {
n = snprintf(p, rem, "[RXLDPC]");
p += n;
rem -= n;
}
/* Short GI for 80MHz: bit 5 */
if (vht_cap & 0x20) {
n = snprintf(p, rem, "[SHORT-GI-80]");
p += n;
rem -= n;
}
/* Short GI for 160MHz: bit 6 — only when configured for 160MHz */
if (chwidth >= 2 && (vht_cap & 0x40)) {
n = snprintf(p, rem, "[SHORT-GI-160]");
p += n;
rem -= n;
}
/* TX STBC: bit 7 */
if (vht_cap & 0x80) {
n = snprintf(p, rem, "[TX-STBC-2BY1]");
p += n;
rem -= n;
}
/* RX STBC: bits 8-10 */
switch ((vht_cap >> 8) & 0x7) {
case 1:
n = snprintf(p, rem, "[RX-STBC-1]");
p += n;
rem -= n;
break;
case 2:
n = snprintf(p, rem, "[RX-STBC-12]");
p += n;
rem -= n;
break;
case 3:
n = snprintf(p, rem, "[RX-STBC-123]");
p += n;
rem -= n;
break;
case 4:
n = snprintf(p, rem, "[RX-STBC-1234]");
p += n;
rem -= n;
break;
}
/* SU Beamformer: bit 11 */
if (vht_cap & 0x800) {
n = snprintf(p, rem, "[SU-BEAMFORMER]");
p += n;
rem -= n;
}
/* SU Beamformee: bit 12 */
if (vht_cap & 0x1000) {
n = snprintf(p, rem, "[SU-BEAMFORMEE]");
p += n;
rem -= n;
}
/* MU Beamformer: bit 19 */
if (vht_cap & 0x80000) {
n = snprintf(p, rem, "[MU-BEAMFORMER]");
p += n;
rem -= n;
}
/* MU Beamformee: bit 20 */
if (vht_cap & 0x100000) {
n = snprintf(p, rem, "[MU-BEAMFORMEE]");
p += n;
rem -= n;
}
/* VHT TXOP PS: bit 21 */
if (vht_cap & 0x200000) {
n = snprintf(p, rem, "[VHT-TXOP-PS]");
p += n;
rem -= n;
}
/* HTC-VHT: bit 22 */
if (vht_cap & 0x400000) {
n = snprintf(p, rem, "[HTC-VHT]");
p += n;
rem -= n;
}
/* Max A-MPDU Length Exponent: bits 23-25 */
n = (vht_cap >> 23) & 0x7;
if (n) {
int wrote = snprintf(p, rem, "[MAX-A-MPDU-LEN-EXP%d]", n);
p += wrote;
rem -= wrote;
}
}
/* Helper: Write radio-specific configuration */
static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
static void wifi_gen_radio_config(FILE *hostapd, const char *radio_name,
struct lyd_node *radio_node)
{
const char *country, *channel, *band, *width;
unsigned int ht_cap = 0, vht_cap = 0;
char ht_capab[512], vht_capab[512];
int chwidth = 0; /* 0=20/40, 1=80, 2=160 */
int ch = 0;
country = lydx_get_cattr(radio_node, "country-code");
@@ -608,6 +877,9 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
if (channel && strcmp(channel, "auto"))
ch = atoi(channel);
/* Read HT/VHT hardware capabilities from PHY */
wifi_read_phy_caps(radio_name, &ht_cap, &vht_cap);
if (country)
fprintf(hostapd, "country_code=%s\n", country);
@@ -686,14 +958,13 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
}
/*
* Channel width configuration.
* Channel width + HT/VHT capability configuration.
*
* 6GHz: bandwidth is determined by op_class (131-134),
* hostapd ignores he_oper_chwidth on 6GHz. No VHT/HT.
*
* 5GHz: requires explicit ht_capab/vht_capab strings
* matching hardware. Without vht_capab, 160MHz is not
* advertised in beacons.
* hostapd requires explicit ht_capab and vht_capab strings
* that match hardware capabilities. Without vht_capab, 160MHz
* support is not advertised in beacons, causing clients to
* connect at 80MHz. Read capabilities from hardware via iw.py
* and build the capability strings from the bitmasks.
*/
if (!strcmp(band, "6GHz")) {
int op_class = 133; /* default 80MHz for 6GHz */
@@ -719,13 +990,18 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
}
} else if (width && strcmp(width, "auto")) {
if (!strcmp(width, "20MHz")) {
fprintf(hostapd, "ht_capab=\n");
chwidth = 0;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap, NULL, 0);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
if (strcmp(band, "2.4GHz")) {
fprintf(hostapd, "vht_oper_chwidth=0\n");
fprintf(hostapd, "he_oper_chwidth=0\n");
}
} else if (!strcmp(width, "40MHz")) {
fprintf(hostapd, "ht_capab=%s\n", ch ? wifi_ht40_dir(ch) : "[HT40+]");
chwidth = 0;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap,
ch ? wifi_ht40_dir(ch) : "[HT40+]", 1);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
if (strcmp(band, "2.4GHz")) {
fprintf(hostapd, "vht_oper_chwidth=0\n");
fprintf(hostapd, "he_oper_chwidth=0\n");
@@ -733,7 +1009,11 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
} else if (!strcmp(width, "80MHz") && ch) {
int center = wifi_center_chan_80(ch);
fprintf(hostapd, "ht_capab=%s\n", wifi_ht40_dir(ch));
chwidth = 1;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap, wifi_ht40_dir(ch), 1);
wifi_build_vht_capab(vht_capab, sizeof(vht_capab), vht_cap, chwidth);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
fprintf(hostapd, "vht_capab=%s\n", vht_capab);
fprintf(hostapd, "vht_oper_chwidth=1\n");
fprintf(hostapd, "he_oper_chwidth=1\n");
if (center) {
@@ -743,7 +1023,11 @@ static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node)
} else if (!strcmp(width, "160MHz") && ch) {
int center = wifi_center_chan_160(ch);
fprintf(hostapd, "ht_capab=%s\n", wifi_ht40_dir(ch));
chwidth = 2;
wifi_build_ht_capab(ht_capab, sizeof(ht_capab), ht_cap, wifi_ht40_dir(ch), 1);
wifi_build_vht_capab(vht_capab, sizeof(vht_capab), vht_cap, chwidth);
fprintf(hostapd, "ht_capab=%s\n", ht_capab);
fprintf(hostapd, "vht_capab=%s\n", vht_capab);
fprintf(hostapd, "vht_oper_chwidth=2\n");
fprintf(hostapd, "he_oper_chwidth=2\n");
if (center) {
@@ -823,7 +1107,7 @@ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs,
fprintf(hostapd, "\n");
/* Radio-specific configuration */
wifi_gen_radio_config(hostapd, radio_node);
wifi_gen_radio_config(hostapd, radio_name, radio_node);
/* Add BSS sections for secondary APs (multi-SSID) */
for (i = 1; i < ap_count; i++) {
+234 -19
View File
@@ -79,19 +79,24 @@ int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif)
wifi_mode_t wifi_get_mode(struct lyd_node *iface)
{
struct lyd_node *ap, *wifi;
struct lyd_node *ap, *mesh, *wifi;
wifi = lydx_get_child(iface, "wifi");
if (!wifi)
return wifi_unknown;
ap = lydx_get_child(wifi, "access-point");
if (ap) {
if (lydx_get_op(ap) != LYDX_OP_DELETE)
return wifi_ap;
}
mesh = lydx_get_child(wifi, "mesh-point");
if (mesh) {
if (lydx_get_op(mesh) != LYDX_OP_DELETE)
return wifi_mesh;
}
/*
* Need to return station even if "station" also is false,
* because station is the default scanning mode.
@@ -101,19 +106,25 @@ wifi_mode_t wifi_get_mode(struct lyd_node *iface)
int wifi_mode_changed(struct lyd_node *wifi)
{
enum lydx_op ap_op = LYDX_OP_DELETE;
struct lyd_node *ap;
enum lydx_op op = LYDX_OP_DELETE;
struct lyd_node *node;
if (!wifi)
return 0;
ap = lydx_get_child(wifi, "access-point");
if (ap)
ap_op = lydx_get_op(ap);
node = lydx_get_child(wifi, "access-point");
if (node)
op = lydx_get_op(node);
if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE))
return 1;
DEBUG("MODE CHANGED: %d", ap && (ap_op == LYDX_OP_CREATE || ap_op == LYDX_OP_DELETE));
node = lydx_get_child(wifi, "mesh-point");
if (node)
op = lydx_get_op(node);
if (node && (op == LYDX_OP_CREATE || op == LYDX_OP_DELETE))
return 1;
return (ap && (ap_op == LYDX_OP_CREATE || ap_op == LYDX_OP_DELETE));
return 0;
}
/*
@@ -150,7 +161,8 @@ int wifi_gen_station(struct lyd_node *cif)
secret_name = NULL;
}
radio_node = lydx_get_xpathf(cif, "../../hardware/component[name='%s']/wifi-radio", radio);
radio_node = lydx_get_xpathf(cif,
"/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio);
country = lydx_get_cattr(radio_node, "country-code");
if (secret_name && strcmp(security_mode, "disabled") != 0) {
@@ -171,10 +183,8 @@ int wifi_gen_station(struct lyd_node *cif)
goto out;
}
/*
* Background scanning every 10 seconds while not associated, when we
* have an SSID (below), bgscan assumes this task.
*/
/* autoscan drives scanning every 10s while unassociated; once
* associated no background scanning runs (bgscan disabled below). */
fprintf(wpa_supplicant,
"ctrl_interface=/run/wpa_supplicant\n"
"autoscan=periodic:10\n"
@@ -193,9 +203,12 @@ int wifi_gen_station(struct lyd_node *cif)
"key_mgmt=FT-SAE FT-PSK SAE WPA-PSK\n"
" psk=\"%s\"", secret);
/* bgscan="" disables background scanning once associated: on a
* fixed backhaul link the periodic off-channel sweep stalls
* traffic and buys no roaming benefit. */
fprintf(wpa_supplicant,
"network={\n"
" bgscan=\"simple: 30:-45:300\"\n"
" bgscan=\"\"\n"
" ssid=\"%s\"\n"
" %s\n"
"}\n", ssid, security_str);
@@ -227,6 +240,195 @@ out:
return rc;
}
/*
* Center channel for 80MHz VHT/HE operation.
* 5GHz 80MHz channel groups and their center channels:
* 36-48(42), 52-64(58), 100-112(106),
* 116-128(122), 132-144(138), 149-161(155)
*/
static int wifi_center_chan_80(int ch)
{
static const int grp[][2] = {
{36, 42}, {52, 58}, {100, 106}, {116, 122}, {132, 138}, {149, 155}
};
int i;
for (i = 0; i < 6; i++)
if (ch >= grp[i][0] && ch < grp[i][0] + 16)
return grp[i][1];
return 0;
}
/*
* Center channel for 160MHz VHT/HE operation.
* 5GHz 160MHz groups: 36-64(50), 100-128(114)
*/
static int wifi_center_chan_160(int ch)
{
if (ch >= 36 && ch <= 64)
return 50;
if (ch >= 100 && ch <= 128)
return 114;
return 0;
}
/*
* Convert WiFi channel number to frequency in MHz.
* Band is determined from channel range:
* 2.4GHz: channels 1-14
* 5GHz: channels 32-177
* 6GHz: channels 1-233 (identified by band string)
*/
static int wifi_chan_to_freq(int channel, const char *band)
{
if (!strcmp(band, "6GHz"))
return 5950 + channel * 5;
if (channel >= 1 && channel <= 13)
return 2407 + channel * 5;
if (channel == 14)
return 2484;
/* 5GHz */
return 5000 + channel * 5;
}
/*
* Generate wpa_supplicant config for 802.11s mesh mode
*/
int wifi_gen_mesh(struct lyd_node *cif)
{
const char *ifname, *mesh_id, *secret_name, *radio;
struct lyd_node *mesh, *security, *secret_node, *radio_node, *wifi;
const char *country, *band, *width;
FILE *wpa_supplicant = NULL;
unsigned char *secret = NULL;
int rc = SR_ERR_OK;
int channel, freq;
mode_t oldmask;
bool forwarding;
ifname = lydx_get_cattr(cif, "name");
wifi = lydx_get_child(cif, "wifi");
if (!wifi)
return SR_ERR_OK;
radio = lydx_get_cattr(wifi, "radio");
mesh = lydx_get_child(wifi, "mesh-point");
if (!mesh)
return SR_ERR_OK;
mesh_id = lydx_get_cattr(mesh, "mesh-id");
forwarding = lydx_is_enabled(mesh, "forwarding");
security = lydx_get_child(mesh, "security");
secret_name = lydx_get_cattr(security, "secret");
radio_node = lydx_get_xpathf(cif,
"/ietf-hardware:hardware/component[name='%s']/infix-hardware:wifi-radio", radio);
country = lydx_get_cattr(radio_node, "country-code");
band = lydx_get_cattr(radio_node, "band");
width = lydx_get_cattr(radio_node, "channel-width");
channel = atoi(lydx_get_cattr(radio_node, "channel") ? : "0");
if (!channel && band && width && !strcmp(width, "auto")) {
if (!strcmp(band, "2.4GHz"))
channel = 6;
else if (!strcmp(band, "5GHz"))
channel = 36;
else if (!strcmp(band, "6GHz"))
channel = 1;
}
freq = wifi_chan_to_freq(channel, band);
if (secret_name) {
const char *b64;
secret_node = lydx_get_xpathf(cif,
"../../keystore/symmetric-keys/symmetric-key[name='%s']",
secret_name);
b64 = lydx_get_cattr(secret_node, "cleartext-symmetric-key");
if (b64)
secret = base64_decode((const unsigned char *)b64, strlen(b64), NULL);
}
oldmask = umask(0077);
wpa_supplicant = fopenf("w", WPA_SUPPLICANT_CONF, ifname);
if (!wpa_supplicant) {
rc = SR_ERR_INTERNAL;
goto out;
}
fprintf(wpa_supplicant, "ctrl_interface=/run/wpa_supplicant\n");
if (country)
fprintf(wpa_supplicant, "country=%s\n", country);
/* Global scope only (maps to peer_link_timeout): raise the 300s
* default so a fixed backhaul peer is not idled out and re-peered
* (MESH_CLOSE, reason 55), which briefly breaks forwarding. */
fprintf(wpa_supplicant, "mesh_max_inactivity=3600\n");
fprintf(wpa_supplicant, "\nnetwork={\n");
fprintf(wpa_supplicant, " mode=5\n");
fprintf(wpa_supplicant, " ssid=\"%s\"\n", mesh_id);
fprintf(wpa_supplicant, " frequency=%d\n", freq);
if (band && !strcmp(band, "6GHz") && (!width || !strcmp(width, "auto"))) {
int center = wifi_center_chan_80(channel);
fprintf(wpa_supplicant, " ht40=1\n");
fprintf(wpa_supplicant, " he=1\n");
fprintf(wpa_supplicant, " max_oper_chwidth=1\n");
if (center)
fprintf(wpa_supplicant, " vht_center_freq1=%d\n", wifi_chan_to_freq(center, band));
} else if (width && strcmp(width, "auto")) {
if (!strcmp(width, "20MHz")) {
fprintf(wpa_supplicant, " disable_ht40=1\n");
} else if (!strcmp(width, "40MHz")) {
fprintf(wpa_supplicant, " ht40=1\n");
} else if (!strcmp(width, "80MHz")) {
int center = wifi_center_chan_80(channel);
fprintf(wpa_supplicant, " ht40=1\n");
if (!strcmp(band, "6GHz"))
fprintf(wpa_supplicant, " he=1\n");
else
fprintf(wpa_supplicant, " vht=1\n");
fprintf(wpa_supplicant, " max_oper_chwidth=1\n");
if (center)
fprintf(wpa_supplicant, " vht_center_freq1=%d\n", wifi_chan_to_freq(center, band));
} else if (!strcmp(width, "160MHz")) {
int center = wifi_center_chan_160(channel);
fprintf(wpa_supplicant, " ht40=1\n");
if (!strcmp(band, "6GHz"))
fprintf(wpa_supplicant, " he=1\n");
else
fprintf(wpa_supplicant, " vht=1\n");
fprintf(wpa_supplicant, " max_oper_chwidth=2\n");
if (center)
fprintf(wpa_supplicant, " vht_center_freq1=%d\n", wifi_chan_to_freq(center, band));
}
}
fprintf(wpa_supplicant, " mesh_fwding=%d\n", forwarding ? 1 : 0);
fprintf(wpa_supplicant, " key_mgmt=SAE\n");
fprintf(wpa_supplicant, " ieee80211w=2\n");
if (secret)
fprintf(wpa_supplicant, " sae_password=\"%s\"\n", secret);
fprintf(wpa_supplicant, "}\n");
out:
free(secret);
if (wpa_supplicant)
fclose(wpa_supplicant);
umask(oldmask);
return rc;
}
/*
* Get probe-timeout for a radio from sysrepo config.
* Returns 0 if not set.
@@ -283,7 +485,7 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n");
fprintf(iw, "# Create %s interface %s on radio %s\n",
mode == wifi_station ? "station" : "access point", ifname, radio);
mode == wifi_station ? "station" : (mode == wifi_mesh ? "mesh" : "access point"), ifname, radio);
/* Wait for PHY if probe-timeout is set (slow USB dongles) */
if (probe_timeout > 0) {
@@ -317,6 +519,12 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net)
case wifi_ap:
fprintf(iw, "iw phy %s interface add %s type __ap\n", radio, ifname);
break;
case wifi_mesh:
fprintf(iw, "iw phy %s interface add %s type mesh\n", radio, ifname);
wifi_gen_mesh(cif);
fprintf(iw, "initctl -bfq enable mesh@%s\n", ifname);
fprintf(iw, "initctl -bfq touch mesh@%s\n", ifname);
break;
default:
ERROR("WiFi mode %d unknown", mode);
rc = SR_ERR_INVAL_ARG;
@@ -350,9 +558,16 @@ int wifi_del_iface(struct lyd_node *dif, struct dagger *net)
fprintf(iw, "iw dev %s del 2>/dev/null || ip link del %s 2>/dev/null || true\n", ifname, ifname);
wifi = lydx_get_child(dif, "wifi");
if (wifi && wifi_get_mode(wifi) != wifi_ap) {
erasef(WPA_SUPPLICANT_CONF, ifname);
fprintf(iw, "initctl -bfq disable wifi@%s\n", ifname);
if (wifi) {
int mode = wifi_get_mode(wifi);
if (mode == wifi_mesh) {
erasef(WPA_SUPPLICANT_CONF, ifname);
fprintf(iw, "initctl -bfq disable mesh@%s\n", ifname);
} else if (mode != wifi_ap) {
erasef(WPA_SUPPLICANT_CONF, ifname);
fprintf(iw, "initctl -bfq disable wifi@%s\n", ifname);
}
}
fclose(iw);
+2
View File
@@ -428,6 +428,8 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net,
if (wifi_get_mode(cif) == wifi_station)
return wifi_validate_secret(session, cif)
? : wifi_gen_station(cif);
if (wifi_get_mode(cif) == wifi_mesh)
return wifi_gen_mesh(cif);
return 0;
case IFT_DUMMY:
case IFT_GRE:
+2
View File
@@ -130,6 +130,7 @@ int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip);
typedef enum wifi_mode_t {
wifi_station,
wifi_ap,
wifi_mesh,
wifi_unknown
} wifi_mode_t;
@@ -138,6 +139,7 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net);
int wifi_del_iface(struct lyd_node *dif, struct dagger *net);
int wifi_mode_changed(struct lyd_node *wifi);
int wifi_gen_station(struct lyd_node *cif);
int wifi_gen_mesh(struct lyd_node *cif);
wifi_mode_t wifi_get_mode(struct lyd_node *wifi);
/* if-gre.c */
+2 -2
View File
@@ -939,8 +939,8 @@ submodule infix-if-bridge {
must "not(../ip:ipv4/ip:address or ../ip:ipv6/ip:address)" {
error-message "Bridge ports cannot have IP addresses configured.";
}
must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point" {
error-message "WiFi interfaces can only be bridge ports when configured as Access Points.";
must "not(derived-from-or-self(../if:type, 'infix-ift:wifi')) or ../infix-if:wifi/infix-if:access-point or ../infix-if:wifi/infix-if:mesh-point" {
error-message "WiFi interfaces can only be bridge ports when configured as Access Points or Mesh Points.";
}
description "Bridge association and port specific settings.";
uses bridge-port-common;
+148 -10
View File
@@ -48,15 +48,12 @@ submodule infix-if-wifi {
- Security: WPA2/WPA3 with keystore integration
- Operational state: Connection status, RSSI, client lists";
revision 2026-03-04 {
revision 2026-03-06 {
description
"Add band-steering (MBO) support and OKC configuration.";
reference "internal";
}
revision 2026-02-12 {
description
"Add support for roaming, by adding support for 802.11v/k/r"
"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";
}
@@ -160,12 +157,14 @@ submodule infix-if-wifi {
Once you've identified a network, configure either:
- Station mode: Connect to an existing WiFi network
- Access Point mode: Create a WiFi network for clients
- Mesh Point mode: Create an 802.11s mesh link
Note: A radio can host either:
- Multiple AP interfaces (multi-SSID), OR
- A single Station interface
- A single Station interface, OR
- A single Mesh Point interface
Mixing AP and Station on the same radio is not supported.";
Mixing AP and Mesh Point on the same radio is not supported.";
case station {
container station {
@@ -644,6 +643,145 @@ submodule infix-if-wifi {
}
}
}
case mesh-point {
container mesh-point {
presence "Configure 802.11s mesh point mode";
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:band" {
error-message "Parent radio must have 'band' configured for mesh mode";
}
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:channel" {
error-message "Parent radio must have 'channel' configured for mesh mode";
}
must "/iehw:hardware/iehw:component[iehw:name = current()/../radio]/ih:wifi-radio/ih:country-code != '00'" {
error-message "Country code '00' is not allowed for mesh mode.";
}
must "not(/if:interfaces/if:interface[wifi/access-point][wifi/radio = current()/../radio])" {
error-message "Mesh point and access point cannot coexist on the same radio";
}
description
"802.11s Mesh Point mode configuration.
In mesh mode, the interface creates a peer-to-peer mesh
link with other mesh points sharing the same mesh ID.
Only one mesh point interface is allowed per radio.
Mesh point and access point cannot share the same radio.
Example use case: Wireless backhaul between APs.";
leaf mesh-id {
type string {
length "1..32";
}
mandatory true;
description
"Mesh network identifier.
All mesh points that should form a mesh network
must use the same mesh ID.";
}
leaf forwarding {
type boolean;
default true;
description
"Enable layer-2 mesh forwarding (mesh_fwding).
When true, the mesh interface can be added to a Linux
bridge for transparent L2 connectivity (mesh portal).
When false, only locally destined traffic is received.";
}
container security {
description
"Mesh security configuration.
All mesh links use WPA3-SAE encryption.";
leaf secret {
type ks:central-symmetric-key-ref;
mandatory true;
description
"Pre-shared key (PSK) reference for SAE mesh.
References a symmetric key in the keystore.
All mesh points in the same mesh must share
the same key.";
}
}
/* Operational state */
container peers {
list peer {
config false;
key mac-address;
description
"List of currently connected mesh peers.";
leaf mac-address {
type yang:mac-address;
description "Mesh peer MAC address.";
}
leaf signal-strength {
type int16;
units "dBm";
description "Peer signal strength in dBm.";
}
leaf connected-time {
type uint32;
units "seconds";
description "Time since peer connected, in seconds.";
}
leaf rx-packets {
type yang:counter64;
description "Packets received from this peer.";
}
leaf tx-packets {
type yang:counter64;
description "Packets transmitted to this peer.";
}
leaf rx-bytes {
type yang:counter64;
units "octets";
description "Bytes received from this peer.";
}
leaf tx-bytes {
type yang:counter64;
units "octets";
description "Bytes transmitted to this peer.";
}
leaf rx-speed {
type uint32;
units "100 kbps";
description
"Last received data rate from this peer in 100 kbps.";
}
leaf tx-speed {
type uint32;
units "100 kbps";
description
"Last transmitted data rate to this peer in 100 kbps.";
}
}
}
}
}
}
}
}
+89 -12
View File
@@ -1382,17 +1382,85 @@ class Iface:
stations_table.print()
def pr_wifi_peers(self):
if not self.wifi:
return
mesh = self.wifi.get("mesh-point", {})
peers_data = mesh.get("peers", {})
peers = peers_data.get("peer", [])
if not peers:
return
print("\nCONNECTED PEERS:")
peers_table = SimpleTable([
Column('MAC'),
Column('SIGNAL'),
Column('TIME'),
Column('RX PKT'),
Column('TX PKT'),
Column('RX BYTES'),
Column('TX BYTES'),
Column('RX SPEED'),
Column('TX SPEED')
])
for peer in peers:
mac = peer.get("mac-address", "unknown")
signal = peer.get("signal-strength")
signal_str = signal_to_status(signal) if signal is not None else "------"
conn_time = peer.get("connected-time", 0)
time_str = f"{conn_time}s"
rx_pkt = peer.get("rx-packets", 0)
tx_pkt = peer.get("tx-packets", 0)
rx_bytes = peer.get("rx-bytes", 0)
tx_bytes = peer.get("tx-bytes", 0)
rx_speed = peer.get("rx-speed", 0)
tx_speed = peer.get("tx-speed", 0)
rx_speed_str = f"{rx_speed / 10:.1f}" if rx_speed else "-"
tx_speed_str = f"{tx_speed / 10:.1f}" if tx_speed else "-"
peers_table.row(mac, signal_str, time_str, rx_pkt, tx_pkt,
rx_bytes, tx_bytes, rx_speed_str, tx_speed_str)
peers_table.print()
def pr_proto_wifi(self, pipe=''):
if self.wifi and (ap := self.wifi.get("access-point", {})):
ssid = ap.get("ssid", "------")
stations = ap.get("stations", {}).get("station", [])
data_str = f"access-point ssid: {ssid} stations: {len(stations)}"
elif self.wifi and (station := self.wifi.get("station", {})):
ssid = station.get("ssid", "------")
data_str = f"station ssid: {ssid}"
if (signal := station.get("signal-strength")) is not None:
data_str += f" signal: {signal_to_status(signal)}"
ssid = None
signal = None
mode = None
if self.wifi:
if "access-point" in self.wifi:
ap = self.wifi["access-point"]
ssid = ap.get("ssid", "------")
mode = "AP"
stations_data = ap.get("stations", {})
stations = stations_data.get("station", [])
station_count = len(stations)
data_str = f"{mode}, ssid: {ssid}, stations: {station_count}"
elif "mesh-point" in self.wifi:
mesh = self.wifi["mesh-point"]
mesh_id = mesh.get("mesh-id", "------")
mode = "Mesh"
peers_data = mesh.get("peers", {})
peers = peers_data.get("peer", [])
data_str = f"{mode}, mesh-id: {mesh_id}, peers: {len(peers)}"
else:
station=self.wifi.get("station", {})
ssid = station.get("ssid", "------")
signal = station.get("signal-strength")
mode = "Station"
if signal is not None:
signal_str = signal_to_status(signal)
data_str = f"{mode}, ssid: {ssid}, signal: {signal_str}"
else:
data_str = f"{mode}, ssid: {ssid}"
else:
data_str = "ssid: ------"
@@ -1664,9 +1732,8 @@ class Iface:
print(f"{'out-octets':<{19}}: {self.out_octets}")
if self.wifi:
# Detect mode: AP has "stations", Station has "signal-strength" or "scan-results"
ap = self.wifi.get('access-point')
if ap:
if "access-point" in self.wifi:
ap = self.wifi['access-point']
mode = "access-point"
ssid = ap.get('ssid', "----")
stations_data = ap.get("stations", {})
@@ -1675,6 +1742,16 @@ class Iface:
print(f"{'ssid':<{19}}: {ssid}")
print(f"{'connected stations':<{19}}: {len(stations)}")
self.pr_wifi_stations()
elif "mesh-point" in self.wifi:
mesh = self.wifi['mesh-point']
mode = "mesh-point"
mesh_id = mesh.get('mesh-id', "----")
peers_data = mesh.get("peers", {})
peers = peers_data.get("peer", [])
print(f"{'mode':<{20}}: {mode}")
print(f"{'mesh-id':<{20}}: {mesh_id}")
print(f"{'connected peers':<{20}}: {len(peers)}")
self.pr_wifi_peers()
else:
mode = "station"
station = self.wifi.get('station', {})
@@ -58,6 +58,21 @@ def wifi_ap(ifname):
return {'access-point': ap_data} if ap_data else {}
def wifi_mesh(ifname, info=None):
"""Get operational data for mesh point mode using iw"""
mesh_data = {}
if info is None:
info = get_iw_info(ifname)
if info.get('ssid'):
mesh_data['mesh-id'] = info['ssid']
peers = get_iw_stations(ifname)
if peers:
mesh_data['peers'] = {'peer': peers}
return {'mesh-point': mesh_data}
def wifi_station(ifname):
"""Get operational data for Station mode using iw + wpa_cli for scanning"""
station_data = {}
@@ -100,6 +115,8 @@ def wifi(ifname):
if mode == 'ap':
result.update(wifi_ap(ifname))
elif mode == 'mesh point':
result.update(wifi_mesh(ifname, info))
else:
result.update(wifi_station(ifname))