From c0591773d46a534f330f3d564eee3d6adeccc7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Mon, 26 Jan 2026 13:55:19 +0100 Subject: [PATCH 1/5] yang: wifi: Only one virtual interface can have default MAC The others need to set custom-phys-address, if not dagger will fail to take up the interface because "The address is not unique on the network" --- src/confd/yang/confd/infix-if-wifi.yang | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 309c3e40c..53e8cb226 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -100,6 +100,9 @@ submodule infix-if-wifi { must "count(/if:interfaces/if:interface[wifi/radio = current()][not(wifi/access-point)]) <= 1" { error-message "Only one station or scan interface is allowed per radio"; } + must "count(/if:interfaces/if:interface[wifi/radio = current()][not(infix-if:custom-phys-address/*)]) <= 1" { + error-message "Only one interface per radio can use the default MAC address. Configure custom-phys-address on additional interfaces."; + } description "Reference to parent WiFi radio (PHY). From a8dc889c7e22b15552e38c7b7bd783b1f7160d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Mon, 26 Jan 2026 16:35:30 +0100 Subject: [PATCH 2/5] confd: wifi: Update wpa_supplicant config when wifi setting change For example changing SSID --- src/confd/src/if-wifi.c | 21 +++++++++------------ src/confd/src/interfaces.c | 5 ++--- src/confd/src/interfaces.h | 8 ++++++++ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index 39b61c36a..f7d5bef50 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -14,18 +14,15 @@ #define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf" -/* - * Determine WiFi mode from YANG configuration - */ -typedef enum wifi_mode_t { - wifi_station, - wifi_ap, - wifi_unknown -} wifi_mode_t; -static wifi_mode_t wifi_get_mode(struct lyd_node *wifi) +wifi_mode_t wifi_get_mode(struct lyd_node *iface) { - struct lyd_node *ap; + struct lyd_node *ap, *wifi; + + wifi = lydx_get_child(iface, "wifi"); + if (!wifi) + return wifi_unknown; + ap = lydx_get_child(wifi, "access-point"); if (ap) { @@ -60,7 +57,7 @@ int wifi_mode_changed(struct lyd_node *wifi) /* * Generate wpa_supplicant config for station mode */ -static int wifi_gen_station(struct lyd_node *cif) +int wifi_gen_station(struct lyd_node *cif) { const char *ifname, *ssid, *secret_name, *secret, *security_mode, *radio; struct lyd_node *security, *secret_node, *radio_node, *station, *wifi; @@ -213,7 +210,7 @@ int wifi_add_iface(struct lyd_node *cif, struct dagger *net) return SR_ERR_INTERNAL; } - mode = wifi_get_mode(wifi); + mode = wifi_get_mode(cif); probe_timeout = wifi_get_probe_timeout(net->session, radio); fprintf(iw, "# Generated by Infix confd - WiFi Interface Creation\n"); diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index f16aa8eee..ddf426052 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -452,9 +452,8 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net, case IFT_ETH: return netdag_gen_ethtool(net, cif, dif); case IFT_WIFI: - /* WiFi daemon config (hostapd/wpa_supplicant) is handled by - * hardware.c when the radio (phy) is configured. Interface - * creation/deletion is handled in netdag_gen_afspec_add(). */ + if (wifi_get_mode(cif) == wifi_station) + return wifi_gen_station(cif); return 0; case IFT_DUMMY: case IFT_GRE: diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index 3e7bcfd6e..97c59098e 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -124,9 +124,17 @@ int bridge_mcd_gen(struct lyd_node *cifs); int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); /* if-wifi.c */ +typedef enum wifi_mode_t { + wifi_station, + wifi_ap, + wifi_unknown +} wifi_mode_t; + 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); +wifi_mode_t wifi_get_mode(struct lyd_node *wifi); /* if-gre.c */ int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); From 01e82547f75a1e64cdc6f278476ef8a31190df35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Mon, 26 Jan 2026 16:59:30 +0100 Subject: [PATCH 3/5] confd: Wi-Fi: Rename RSSI to signal-strength RSSI is a vendor-specific relative index (0-255), while dBm is an absolute power measurement. Since we report dBm values from iw, rename the leaf to signal-strength for accuracy. --- board/common/rootfs/usr/libexec/infix/iw.py | 4 +-- doc/wifi.md | 4 +-- src/confd/yang/confd/infix-if-wifi.yang | 8 ++--- src/statd/python/cli_pretty/cli_pretty.py | 36 +++++++++---------- .../python/yanger/ietf_interfaces/wifi.py | 16 ++++----- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index 737f9bc7c..0ce76d36d 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -322,7 +322,7 @@ def parse_stations(ifname): try: if key == 'signal': # Format: "-42 dBm" or "-42 [-44, -45] dBm" - current['rssi'] = int(value.split()[0]) + current['signal-strength'] = int(value.split()[0]) elif key == 'connected time': # Format: "123 seconds" current['connected-time'] = int(value.split()[0]) @@ -500,7 +500,7 @@ def parse_link(ifname): # signal: -42 dBm elif stripped.startswith('signal: '): try: - result['rssi'] = int(stripped.split()[1]) + result['signal-strength'] = int(stripped.split()[1]) except (ValueError, IndexError): pass diff --git a/doc/wifi.md b/doc/wifi.md index da47f85fb..73190f3fd 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -238,8 +238,8 @@ CoffeeShop 00:1a:2b:3c:4d:5e Open bad 1 In the CLI, signal strength is reported as: excellent, good, fair or bad. -For precise RSSI values in dBm, use NETCONF or RESTCONF to access the -operational datastore directly. +For precise signal strength values in dBm, use NETCONF or RESTCONF to access +the `signal-strength` leaf in the operational datastore. ## Station Mode (Client) diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 53e8cb226..9d16cfcf7 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -204,12 +204,12 @@ submodule infix-if-wifi { /* Operational state */ - leaf rssi { + leaf signal-strength { config false; type int16; units "dBm"; description - "Current received signal strength indication (RSSI) in dBm. + "Current signal strength in dBm. More negative values indicate weaker signal. @@ -269,7 +269,7 @@ submodule infix-if-wifi { description "BSSID (MAC address) of the AP."; } - leaf rssi { + leaf signal-strength { type int16; units "dBm"; description "Signal strength of the network."; @@ -418,7 +418,7 @@ submodule infix-if-wifi { description "Client MAC address."; } - leaf rssi { + leaf signal-strength { type int16; units "dBm"; description "Client signal strength in dBm."; diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 246d5d38a..5e65d4daf 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -565,12 +565,12 @@ def title(txt, width=None, bold=True): print(txt) -def rssi_to_status(rssi): - if rssi <= -75: +def signal_to_status(signal): + if signal >= -50: status = Decore.bright_green("excellent") - elif rssi <= -65: + elif signal <= -65: status = Decore.green("good") - elif rssi <= -50: + elif signal <= -50: status = Decore.yellow("poor") else: status = Decore.red("bad") @@ -1249,7 +1249,7 @@ def pr_wifi_ssids(self): ssid = result.get('ssid', 'Hidden') bssid = result.get("bssid", "unknown") encstr = ", ".join(result.get("encryption", ["Unknown"])) - status = rssi_to_status(result.get("rssi", -100)) + status = signal_to_status(result.get("signal-strength", -100)) channel = result.get("channel", "?") ssid_table.row(ssid, bssid, encstr, status, channel) @@ -1283,8 +1283,8 @@ def pr_wifi_stations(self): for station in stations: mac = station.get("mac-address", "unknown") - rssi = station.get("rssi") - signal_str = rssi_to_status(rssi) if rssi is not None else "------" + signal = station.get("signal-strength") + signal_str = signal_to_status(signal) if signal is not None else "------" conn_time = station.get("connected-time", 0) time_str = f"{conn_time}s" @@ -1310,11 +1310,11 @@ def pr_proto_wifi(self, pipe=''): row = self._pr_proto_common("ethernet", True, pipe); print(row) ssid = None - rssi = None + signal = None mode = None if self.wifi: - # Detect mode: AP has "stations", Station has "rssi" or "scan-results" + # Detect mode: AP has "stations", Station has "signal-strength" or "scan-results" ap=self.wifi.get("access-point", {}) if ap: ssid = ap.get("ssid", "------") @@ -1326,11 +1326,11 @@ def pr_proto_wifi(self, pipe=''): else: station=self.wifi.get("station", {}) ssid = station.get("ssid", "------") - rssi = station.get("rssi") + signal = station.get("signal-strength") mode = "Station" - if rssi is not None: - signal = rssi_to_status(rssi) - data_str = f"{mode}, ssid: {ssid}, signal: {signal}" + 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: @@ -1618,7 +1618,7 @@ def pr_iface(self): print(f"{'out-octets':<{20}}: {self.out_octets}") if self.wifi: - # Detect mode: AP has "stations", Station has "rssi" or "scan-results" + # Detect mode: AP has "stations", Station has "signal-strength" or "scan-results" ap = self.wifi.get('access-point') if ap: mode = "access-point" @@ -1632,13 +1632,13 @@ def pr_iface(self): else: mode = "station" station = self.wifi.get('station', {}) - rssi = station.get('rssi') + signal = station.get('signal-strength') ssid = station.get('ssid', "----") print(f"{'mode':<{20}}: {mode}") print(f"{'ssid':<{20}}: {ssid}") - if rssi is not None: - signal_status = rssi_to_status(rssi) - print(f"{'signal':<{20}}: {rssi} dBm ({signal_status})") + if signal is not None: + signal_status = signal_to_status(signal) + print(f"{'signal':<{20}}: {signal} dBm ({signal_status})") rx_speed = station.get('rx-speed') tx_speed = station.get('tx-speed') if rx_speed is not None: diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index 56669a600..d7342e33b 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -62,14 +62,14 @@ def wifi_station(ifname): """Get operational data for Station mode using iw + wpa_cli for scanning""" station_data = {} - # Get link info (includes SSID and RSSI when connected) + # Get link info (includes SSID and signal strength when connected) link = get_iw_link(ifname) if link.get('connected'): if link.get('ssid'): station_data['ssid'] = link['ssid'] - if link.get('rssi') is not None: - station_data['rssi'] = link['rssi'] + if link.get('signal-strength') is not None: + station_data['signal-strength'] = link['signal-strength'] if link.get('rx-speed') is not None: station_data['rx-speed'] = link['rx-speed'] if link.get('tx-speed') is not None: @@ -130,21 +130,21 @@ def parse_wpa_scan_result(scan_output): encryption = extract_encryption(flags) channel = frequency_to_channel(frequency) - # Keep best RSSI per SSID - if ssid not in networks or rssi > networks[ssid]['rssi']: + # Keep best signal per SSID + if ssid not in networks or rssi > networks[ssid]['signal-strength']: networks[ssid] = { 'bssid': bssid, 'ssid': ssid, - 'rssi': rssi, + 'signal-strength': rssi, 'encryption': encryption, 'channel': channel } except Exception: continue - # Sort by RSSI (best first) + # Sort by signal strength (best first) result = list(networks.values()) - result.sort(key=lambda x: x['rssi'], reverse=True) + result.sort(key=lambda x: x['signal-strength'], reverse=True) return result From 2b5e4e92f131e47f2cc1a6854c215286411501c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Mon, 26 Jan 2026 16:56:52 +0100 Subject: [PATCH 4/5] cli_pretty: Wi-Fi: signal-strenght to text was inverted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should be like this: ┌────────────┬────────────────────┐ │ RSSI (dBm) │ Status │ ├────────────┼────────────────────┤ │ ≥ -50 │ excellent (strong) │ ├────────────┼────────────────────┤ │ -60 to -51 │ good │ ├────────────┼────────────────────┤ │ -70 to -61 │ poor │ ├────────────┼────────────────────┤ │ < -70 │ bad (weak) │ └────────────┴────────────────────┘ --- src/statd/python/cli_pretty/cli_pretty.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index 5e65d4daf..ffd381a4d 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -568,9 +568,9 @@ def title(txt, width=None, bold=True): def signal_to_status(signal): if signal >= -50: status = Decore.bright_green("excellent") - elif signal <= -65: + elif signal >= -60: status = Decore.green("good") - elif signal <= -50: + elif signal >= -70: status = Decore.yellow("poor") else: status = Decore.red("bad") From fb89d47c90ca55e79bd50503a7478153ee673c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Tue, 27 Jan 2026 08:24:26 +0100 Subject: [PATCH 5/5] yanger: Allow to read back SSIDs encoded with utf-8 --- board/common/rootfs/usr/libexec/infix/iw.py | 13 ++++++++++--- src/confd/yang/confd/infix-if-wifi.yang | 6 ++++++ src/statd/python/yanger/__main__.py | 2 +- src/statd/python/yanger/ietf_interfaces/wifi.py | 8 +++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index 0ce76d36d..035dd53f0 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -13,6 +13,13 @@ import json import subprocess import re +def decode_iw_ssid(ssid): + """Decode iw escaped SSID (\\xHH) to UTF-8, stripping non-printable chars.""" + try: + ssid = ssid.encode().decode('unicode_escape').encode('latin-1').decode('utf-8') + except (UnicodeDecodeError, UnicodeEncodeError): + return ssid + return ''.join(c for c in ssid if c.isprintable()) def run_iw(*args): @@ -260,7 +267,7 @@ def parse_interface_info(ifname): # SSID elif stripped.startswith('ssid '): - result['ssid'] = ' '.join(stripped.split()[1:]) + result['ssid'] = decode_iw_ssid(' '.join(stripped.split()[1:])) # Channel/frequency elif stripped.startswith('channel '): @@ -488,7 +495,7 @@ def parse_link(ifname): # SSID: NetworkName elif stripped.startswith('SSID: '): - result['ssid'] = stripped[6:] + result['ssid'] = decode_iw_ssid(stripped[6:]) # freq: 5180 elif stripped.startswith('freq: '): @@ -582,7 +589,7 @@ def main(): else: data = {'error': f'Unknown command: {command}'} - print(json.dumps(data, indent=2)) + print(json.dumps(data, indent=2, ensure_ascii=False)) except Exception as e: print(json.dumps({'error': str(e)})) diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 9d16cfcf7..d2b8d3500 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -151,6 +151,9 @@ submodule infix-if-wifi { leaf ssid { type string { length "1..32"; + pattern '[^\x00-\x1f\x22\x5c\x7f]*' { + error-message "SSID must not contain control characters, double quotes, or backslashes."; + } } mandatory true; description @@ -322,6 +325,9 @@ submodule infix-if-wifi { leaf ssid { type string { length "1..32"; + pattern '[^\x00-\x1f\x22\x5c\x7f]*' { + error-message "SSID must not contain control characters, double quotes, or backslashes."; + } } mandatory true; description diff --git a/src/statd/python/yanger/__main__.py b/src/statd/python/yanger/__main__.py index 09b76adbf..e156174c4 100644 --- a/src/statd/python/yanger/__main__.py +++ b/src/statd/python/yanger/__main__.py @@ -97,7 +97,7 @@ def dirpath(path): common.LOG.warning("Unsupported model %s", args.model) sys.exit(1) - print(json.dumps(yang_data, indent=2)) + print(json.dumps(yang_data, indent=2, ensure_ascii=False)) if __name__ == "__main__": diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index d7342e33b..ccf092d14 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -122,9 +122,15 @@ def parse_wpa_scan_result(scan_output): flags = parts[3].strip() ssid = parts[4].strip() if len(parts) > 4 else "" + try: + ssid = ssid.encode().decode('unicode_escape').encode('latin-1').decode('utf-8') + except (UnicodeDecodeError, UnicodeEncodeError): + pass + # Strip control chars (terminal injection risk from rogue APs) + ssid = ''.join(c for c in ssid if c.isprintable()) # Skip hidden SSIDs (empty or null-filled) - if not ssid or ssid.isspace() or '\\x00' in ssid: + if not ssid or ssid.isspace(): continue encryption = extract_encryption(flags)