From f6917da95d7a9ffdaf394270a73983ae4bbde1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Wed, 27 May 2026 10:05:25 +0200 Subject: [PATCH] Continues with mesh support --- .../rootfs/etc/finit.d/available/mesh@.conf | 7 ++ board/common/rootfs/usr/libexec/infix/iw.py | 6 +- package/feature-wifi/feature-wifi.mk | 1 - package/feature-wifi/mesh-action.sh | 11 +-- package/feature-wifi/mesh-post.sh | 11 --- src/confd/src/if-wifi.c | 94 +++++++++++------- src/confd/yang/confd/infix-if-bridge.yang | 4 +- src/confd/yang/confd/infix-if-wifi.yang | 13 +++ src/statd/python/cli_pretty/cli_pretty.py | 98 +++++++++++++++++-- .../python/yanger/ietf_interfaces/wifi.py | 5 +- 10 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 board/common/rootfs/etc/finit.d/available/mesh@.conf mode change 100755 => 100644 package/feature-wifi/mesh-action.sh delete mode 100644 package/feature-wifi/mesh-post.sh diff --git a/board/common/rootfs/etc/finit.d/available/mesh@.conf b/board/common/rootfs/etc/finit.d/available/mesh@.conf new file mode 100644 index 00000000..dcc08e4e --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/mesh@.conf @@ -0,0 +1,7 @@ +service name:wpa_cli :%i pid:/run/wpa_cli-%i.pid \ + [2345] wpa_cli -P /run/wpa_cli-%i.pid -i %i -a /usr/libexec/mesh-action.sh -r \ + -- Wi-Fi Mesh action @%i + +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 diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index 4b70918a..66827438 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -265,10 +265,14 @@ def parse_interface_info(ifname): elif stripped.startswith('addr '): result['mac'] = stripped.split()[1] - # SSID + # SSID (AP mode) elif stripped.startswith('ssid '): result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:])) + # Mesh ID (mesh point mode) + elif stripped.startswith('mesh_id '): + result['mesh_id'] = decode_iw_ssid(' '.join(stripped.split()[1:])) + # Channel/frequency elif stripped.startswith('channel '): parts = stripped.split() diff --git a/package/feature-wifi/feature-wifi.mk b/package/feature-wifi/feature-wifi.mk index cd59a0ca..ce9ea3ff 100644 --- a/package/feature-wifi/feature-wifi.mk +++ b/package/feature-wifi/feature-wifi.mk @@ -74,7 +74,6 @@ define FEATURE_WIFI_INSTALL_IN_ROMFS 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 $(INSTALL) -m 0755 $(FEATURE_WIFI_PKGDIR)/mesh-action.sh $(TARGET_DIR)/usr/libexec/mesh-action.sh - $(INSTALL) -m 0755 $(FEATURE_WIFI_PKGDIR)/mesh-post.sh $(TARGET_DIR)/usr/libexec/mesh-post.sh endef FEATURE_WIFI_POST_INSTALL_TARGET_HOOKS += FEATURE_WIFI_INSTALL_IN_ROMFS diff --git a/package/feature-wifi/mesh-action.sh b/package/feature-wifi/mesh-action.sh old mode 100755 new mode 100644 index a6b40ea0..bd7990e6 --- a/package/feature-wifi/mesh-action.sh +++ b/package/feature-wifi/mesh-action.sh @@ -1,14 +1,9 @@ #!/bin/sh -# mesh-action.sh - wpa_cli action script for 802.11s mesh -# Called by wpa_cli with: $1=" " - -IFACE="${1%% *}" -EVENT="${1#* }" +IFACE="$1" +EVENT="$2" case "$EVENT" in MESH-GROUP-STARTED*) - if [ -f "/run/mesh-params-${IFACE}.conf" ]; then - . "/run/mesh-params-${IFACE}.conf" - fi + [ -f "/run/mesh-params-${IFACE}.conf" ] && . "/run/mesh-params-${IFACE}.conf" ;; esac diff --git a/package/feature-wifi/mesh-post.sh b/package/feature-wifi/mesh-post.sh deleted file mode 100644 index 5d19856c..00000000 --- a/package/feature-wifi/mesh-post.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -IFACE="$1" -CTRL="/run/wpa_supplicant/$IFACE" - -i=0 -while [ $i -lt 10 ] && [ ! -S "$CTRL" ]; do - sleep 1 - i=$((i + 1)) -done - -[ -S "$CTRL" ] && wpa_cli -i "$IFACE" -a /usr/libexec/mesh-action.sh -B diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index 351175b2..bb50de01 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -16,6 +16,7 @@ #include "base64.h" #define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf" +#define MESH_PARAMS_CONF "/run/mesh-params-%s.conf" int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif) @@ -161,7 +162,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) { @@ -272,11 +274,6 @@ static int wifi_center_chan_160(int ch) 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: @@ -329,15 +326,19 @@ int wifi_gen_mesh(struct lyd_node *cif) 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); + 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 (!band || !channel) { - ERROR("%s: mesh requires radio band and channel", ifname); - return SR_ERR_INVAL_ARG; + 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); @@ -366,41 +367,41 @@ int wifi_gen_mesh(struct lyd_node *cif) fprintf(wpa_supplicant, "\nnetwork={\n"); fprintf(wpa_supplicant, " mode=5\n"); - fprintf(wpa_supplicant, " mesh_id=\"%s\"\n", mesh_id); + fprintf(wpa_supplicant, " ssid=\"%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 (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, " mesh_ht_mode=HT20\n"); + fprintf(wpa_supplicant, " disable_ht40=1\n"); } else if (!strcmp(width, "40MHz")) { - fprintf(wpa_supplicant, " mesh_ht_mode=HT40%s\n", wifi_mesh_ht40_dir(channel)); + fprintf(wpa_supplicant, " ht40=1\n"); } 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, " ht40=1\n"); + if (!strcmp(band, "6GHz")) fprintf(wpa_supplicant, " he=1\n"); - } else { - fprintf(wpa_supplicant, " mesh_ht_mode=VHT\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); - if (!strcmp(band, "6GHz")) { - fprintf(wpa_supplicant, " mesh_ht_mode=HE160\n"); + fprintf(wpa_supplicant, " ht40=1\n"); + if (!strcmp(band, "6GHz")) fprintf(wpa_supplicant, " he=1\n"); - } else { - fprintf(wpa_supplicant, " mesh_ht_mode=VHT\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)); @@ -415,6 +416,19 @@ int wifi_gen_mesh(struct lyd_node *cif) fprintf(wpa_supplicant, "}\n"); + /* Generate mesh params file, sourced by mesh-action.sh on MESH-GROUP-STARTED */ + if (lydx_is_enabled(mesh, "gate-announcements")) { + FILE *params; + + params = fopenf("w", MESH_PARAMS_CONF, ifname); + if (params) { + fprintf(params, "iw dev %s set mesh_param mesh_gate_announcements 1\n", ifname); + fclose(params); + } + } else { + erasef(MESH_PARAMS_CONF, ifname); + } + out: free(secret); if (wpa_supplicant) @@ -516,8 +530,8 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net) 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); + 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); @@ -552,9 +566,17 @@ 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); + erasef(MESH_PARAMS_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); diff --git a/src/confd/yang/confd/infix-if-bridge.yang b/src/confd/yang/confd/infix-if-bridge.yang index 3e4d32a2..7c5eff40 100644 --- a/src/confd/yang/confd/infix-if-bridge.yang +++ b/src/confd/yang/confd/infix-if-bridge.yang @@ -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; diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 5ca1a297..7e1cbe1d 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -691,6 +691,19 @@ submodule infix-if-wifi { When false, only locally destined traffic is received."; } + leaf gate-announcements { + type boolean; + default false; + description + "Enable mesh gate announcements (GANN frames). + + When true, this node advertises itself as a mesh gate + (i.e., it has external network connectivity). Other mesh + nodes proactively maintain a path to this node. + + Typically enabled on nodes with a wired uplink."; + } + container security { description "Mesh security configuration. diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 234409f2..92919474 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -1382,17 +1382,87 @@ 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: + # Detect mode: AP has "stations", Station has "signal-strength" or "scan-results" + ap=self.wifi.get("access-point", {}) + if ap: + 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}" + else: + mesh = self.wifi.get("mesh-point", {}) + if mesh: + 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: ------" @@ -1675,6 +1745,16 @@ class Iface: print(f"{'ssid':<{19}}: {ssid}") print(f"{'connected stations':<{19}}: {len(stations)}") self.pr_wifi_stations() + elif self.wifi.get('mesh-point'): + mesh = self.wifi.get('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', {}) diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index 046bea81..6365029c 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -62,7 +62,10 @@ 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) + info = get_iw_info(ifname) + if info.get('mesh_id'): + mesh_data['mesh-id'] = info['mesh_id'] + peers = get_iw_stations(ifname) if peers: mesh_data['peers'] = {'peer': peers}