Wi-Fi: Implement mesh 802.11s

This commit is contained in:
Mattias Walström
2026-06-03 11:16:11 +02:00
parent 9defc4a7f5
commit ebab0c01da
8 changed files with 720 additions and 27 deletions
+46 -2
View File
@@ -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
+294 -13
View File
@@ -529,10 +529,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");
@@ -542,6 +808,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);
@@ -620,14 +889,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 */
@@ -653,13 +921,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");
@@ -667,7 +940,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) {
@@ -677,7 +954,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) {
@@ -757,7 +1038,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++) {
+212 -10
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;
}
/*
@@ -227,6 +238,191 @@ 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;
}
/* HT40 secondary channel direction for mesh: returns "+" or "-" */
static const char *wifi_mesh_ht40_dir(int ch)
{
return ((ch / 4) % 2) ? "+" : "-";
}
/*
* 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;
unsigned char *secret = NULL;
FILE *wpa_supplicant = NULL;
const char *country, *band, *width;
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, "../../hardware/component[name='%s']/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 (!band || !channel) {
ERROR("%s: mesh requires radio band and channel", ifname);
return SR_ERR_INVAL_ARG;
}
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);
fprintf(wpa_supplicant, "\nnetwork={\n");
fprintf(wpa_supplicant, " mode=5\n");
fprintf(wpa_supplicant, " mesh_id=\"%s\"\n", mesh_id);
fprintf(wpa_supplicant, " frequency=%d\n", freq);
/*
* Channel width configuration for mesh.
* wpa_supplicant uses mesh_ht_mode instead of hostapd's ht_capab/vht_oper_chwidth.
* For 6GHz, use HE modes; for 5GHz, use VHT modes.
* vht_center_freq1 takes frequency in MHz (not channel number).
*/
if (width && strcmp(width, "auto")) {
if (!strcmp(width, "20MHz")) {
fprintf(wpa_supplicant, " mesh_ht_mode=HT20\n");
} else if (!strcmp(width, "40MHz")) {
fprintf(wpa_supplicant, " mesh_ht_mode=HT40%s\n", wifi_mesh_ht40_dir(channel));
} else if (!strcmp(width, "80MHz")) {
int center = wifi_center_chan_80(channel);
if (!strcmp(band, "6GHz")) {
fprintf(wpa_supplicant, " mesh_ht_mode=HE80\n");
fprintf(wpa_supplicant, " he=1\n");
} else {
fprintf(wpa_supplicant, " mesh_ht_mode=VHT\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);
if (!strcmp(band, "6GHz")) {
fprintf(wpa_supplicant, " mesh_ht_mode=HE160\n");
fprintf(wpa_supplicant, " he=1\n");
} else {
fprintf(wpa_supplicant, " mesh_ht_mode=VHT\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 +479,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 +513,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 wifi@%s\n", ifname);
fprintf(iw, "initctl -bfq touch wifi@%s\n", ifname);
break;
default:
ERROR("WiFi mode %d unknown", mode);
rc = SR_ERR_INVAL_ARG;
+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 */
+149 -2
View File
@@ -48,6 +48,12 @@ submodule infix-if-wifi {
- Security: WPA2/WPA3 with keystore integration
- Operational state: Connection status, RSSI, client lists";
revision 2026-03-06 {
description
"Add 802.11s mesh point mode support.";
reference "internal";
}
revision 2026-03-04 {
description
"Add band-steering (MBO) support and OKC configuration.";
@@ -160,12 +166,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 {
@@ -636,6 +644,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.";
}
}
}
}
}
}
}
}
@@ -58,6 +58,17 @@ def wifi_ap(ifname):
return {'access-point': ap_data} if ap_data else {}
def wifi_mesh(ifname):
"""Get operational data for mesh point mode using iw"""
mesh_data = {}
# iw station dump works for mesh peers (same nl80211 API as AP stations)
peers = get_iw_stations(ifname)
if peers:
mesh_data['peers'] = {'peer': peers}
return {'mesh-point': mesh_data} if mesh_data else {}
def wifi_station(ifname):
"""Get operational data for Station mode using iw + wpa_cli for scanning"""
station_data = {}
@@ -100,6 +111,8 @@ def wifi(ifname):
if mode == 'ap':
result.update(wifi_ap(ifname))
elif mode == 'mesh point':
result.update(wifi_mesh(ifname))
else:
result.update(wifi_station(ifname))