diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index 534aefc2f..55dc08977 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -585,13 +585,13 @@ def parse_phy_caps(phy_name): def parse_mesh_param(ifname): """ - Parse 'iw dev get mesh_param' output for mesh point mode + Parse 'iw dev mesh_param dump' output for mesh point mode Returns: {param: value}, values as int where they are numeric Lines are 'mesh_fwding = 1' or 'mesh_retry_timeout = 100 milliseconds', the unit is dropped. """ - output = run_iw('dev', ifname, 'get', 'mesh_param') + output = run_iw('dev', ifname, 'mesh_param', 'dump') if not output: return {} diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index 7911e476e..61373f1d0 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -106,6 +106,15 @@ All notable changes to the project are documented in this file. ### Fixes +- Fix #1655: restrict the allowed characters in interface names +- Constrain Wi-Fi mesh-id and NAS identifier, and validate access-point and mesh passphrases as strictly as station +- Apply syslog configuration changes at runtime, not only after reboot +- Restrict the allowed characters in keystore key and certificate names +- Restrict the allowed characters in syslog property-filter value and pattern-match +- Fix #1657: restrict the allowed characters in DHCP client option values, + which are written into the DHCP client service file +- Restrict the allowed characters in DHCP server static-host match values +- Restrict the allowed characters in a hardware component `name` - Fix #1619: Raspberry Pi kernel panic when configure Wi-Fi - WebUI: "Save" in the interface editor and "OK" in Add Interface did nothing for Wi-Fi and WireGuard interfaces. The inline "+ New" diff --git a/doc/iface.md b/doc/iface.md index 679fe5021..0cbdb9d95 100644 --- a/doc/iface.md +++ b/doc/iface.md @@ -9,9 +9,10 @@ sections for [Bridging](bridging.md), [Link Aggregation](lag.md), ## Interface Name The interface name is limited to 1-15 characters due to Linux kernel -constraints. Physical interfaces use their system-assigned names (e.g., -`eth0`, `eth1`), while user-created interfaces can be named freely within -this limit. +constraints, and may only contain letters, digits and the characters +`_ . : + -`, not starting with `.` or `-`. Physical interfaces use their +system-assigned names (e.g., `eth0`, `eth1`), while user-created +interfaces can be named freely within these limits. > [!TIP] > Naming conventions like `br0`, `lag0`, `vlan10`, or `eth0.20` allow diff --git a/src/confd/src/core.c b/src/confd/src/core.c index 86343ae82..bd705cce5 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -874,6 +874,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv) ERROR("Failed to subscribe to ietf-system"); goto err; } + rc = subscribe_model("ietf-syslog", &confd, 0); + if (rc) { + ERROR("Failed to subscribe to ietf-syslog"); + goto err; + } rc = subscribe_model("ieee802-dot1ab-lldp", &confd, 0); if (rc) { ERROR("Failed to subscribe to ieee802-dot1ab-lldp"); diff --git a/src/confd/src/dhcp-client.c b/src/confd/src/dhcp-client.c index 2ef8d1cc4..8d1f1ffab 100644 --- a/src/confd/src/dhcp-client.c +++ b/src/confd/src/dhcp-client.c @@ -101,7 +101,7 @@ static void add(const char *ifname, struct lyd_node *cfg) char *cid = NULL, *options = NULL; int ena = 0; const char *vendor_class; - char vendor[128] = { 0 }; + char vendor[272] = { 0 }; char do_arp[20] = { 0 }; bool arping; FILE *fp; diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index 84653dfa1..8b9fca53a 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -18,27 +18,28 @@ #define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf" -int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif) +/* + * Validate one wifi security block's referenced keystore secret. The + * decoded passphrase is written verbatim into wpa_supplicant/hostapd + * config, so it must be 8-63 printable characters (no newline, which + * would inject an unrelated directive). Applies to station, access + * point and mesh alike. + */ +static int validate_wifi_secret(sr_session_ctx_t *session, const char *ifname, + struct lyd_node *cif, struct lyd_node *security) { - struct lyd_node *wifi, *station, *security, *secret_node; - const char *ifname, *secret_name, *security_mode, *b64; + const char *secret_name, *security_mode, *b64; + struct lyd_node *secret_node; unsigned char *decoded; size_t len; - ifname = lydx_get_cattr(cif, "name"); - wifi = lydx_get_child(cif, "wifi"); - if (!wifi) - return SR_ERR_OK; - - station = lydx_get_child(wifi, "station"); - if (!station) + if (!security) return SR_ERR_OK; - security = lydx_get_child(station, "security"); security_mode = lydx_get_cattr(security, "mode"); secret_name = lydx_get_cattr(security, "secret"); - if (!secret_name || !strcmp(security_mode, "disabled")) + if (!secret_name || (security_mode && !strcmp(security_mode, "disabled"))) return SR_ERR_OK; secret_node = lydx_get_xpathf(cif, @@ -77,6 +78,35 @@ int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif) return SR_ERR_OK; } +int wifi_validate_secret(sr_session_ctx_t *session, struct lyd_node *cif) +{ + static const char *const modes[] = { + "station", "access-point", "mesh-point" + }; + const char *ifname; + struct lyd_node *wifi; + + ifname = lydx_get_cattr(cif, "name"); + wifi = lydx_get_child(cif, "wifi"); + if (!wifi) + return SR_ERR_OK; + + for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) { + struct lyd_node *node = lydx_get_child(wifi, modes[i]); + int rc; + + if (!node) + continue; + + rc = validate_wifi_secret(session, ifname, cif, + lydx_get_child(node, "security")); + if (rc) + return rc; + } + + return SR_ERR_OK; +} + wifi_mode_t wifi_get_mode(struct lyd_node *iface) { struct lyd_node *ap, *mesh, *wifi; diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index db00e3b45..68d88dfbf 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -24,10 +24,10 @@ MODULES=( # NOTE: ietf-tls-client must be version matched with ietf-tls-server, used by netopeer2! # "ietf-tls-client@2023-12-28.yang" "ietf-syslog@2024-03-21.yang -e file-action -e file-limit-size -e remote-action -e select-adv-compare -e select-match" - "infix-syslog@2026-09-15.yang" + "infix-syslog@2026-09-24.yang" "iana-hardware@2018-03-13.yang" "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" - "infix-hardware@2026-07-02.yang" + "infix-hardware@2026-09-24.yang" "ieee802-dot1q-types@2022-10-29.yang" "infix-ip@2026-04-28.yang" "infix-if-type@2026-01-07.yang" @@ -35,9 +35,9 @@ MODULES=( "ieee802-dot1ab-lldp@2022-03-15.yang" "infix-lldp@2025-05-05.yang" "infix-dhcp-common@2025-12-21.yang" - "infix-dhcp-client@2025-11-09.yang" + "infix-dhcp-client@2026-09-28.yang" "infix-dhcpv6-client@2025-11-09.yang" - "infix-dhcp-server@2026-09-18.yang" + "infix-dhcp-server@2026-09-24.yang" "infix-firewall@2026-07-02.yang" "infix-firewall-services@2025-04-26.yang" "infix-firewall-icmp-types@2025-04-26.yang" @@ -48,12 +48,12 @@ MODULES=( "ieee802-ethernet-phy-type@2025-09-10.yang" "infix-ethernet-interface@2026-05-21.yang" "infix-factory-default@2023-06-28.yang" - "infix-interfaces@2026-06-11.yang -e vlan-filtering" + "infix-interfaces@2026-09-28.yang -e vlan-filtering" "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" "infix-ntp@2026-06-11.yang" - "infix-keystore@2025-12-17.yang" + "infix-keystore@2026-09-24.yang" "ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction" "ieee802-dot1as-gptp@2025-12-10.yang" "infix-ptp@2026-04-07.yang" diff --git a/src/confd/yang/confd/infix-dhcp-client.yang b/src/confd/yang/confd/infix-dhcp-client.yang index 1279d5e62..1751eb9bc 100644 --- a/src/confd/yang/confd/infix-dhcp-client.yang +++ b/src/confd/yang/confd/infix-dhcp-client.yang @@ -16,6 +16,11 @@ module infix-dhcp-client { contact "kernelkit@googlegroups.com"; description "This module implements a DHCPv4 client"; + revision 2026-09-28 { + description "Constrain the character set and length of option values."; + reference "internal"; + } + revision 2025-11-09 { description "Fix namespace to use infix instead of ietf."; reference "internal"; @@ -128,7 +133,12 @@ module infix-dhcp-client { which the server can use for static host matching. For the 'hostname' option the 'auto' keyword can be used to send the hostname part from IETF system."; - type string; + reference "RFC 2132, sec. 2: the option length field is one + octet, so a value is at most 255 bytes."; + type string { + length "1..255"; + pattern '[a-zA-Z0-9 _.:/@=,+-]+'; + } must "../id != 'hostname' or re-match(., '[a-zA-Z0-9\\-_]{1,64}')"; } } diff --git a/src/confd/yang/confd/infix-dhcp-client@2025-11-09.yang b/src/confd/yang/confd/infix-dhcp-client@2026-09-28.yang similarity index 100% rename from src/confd/yang/confd/infix-dhcp-client@2025-11-09.yang rename to src/confd/yang/confd/infix-dhcp-client@2026-09-28.yang diff --git a/src/confd/yang/confd/infix-dhcp-server.yang b/src/confd/yang/confd/infix-dhcp-server.yang index 68f847cdd..072ff3c5a 100644 --- a/src/confd/yang/confd/infix-dhcp-server.yang +++ b/src/confd/yang/confd/infix-dhcp-server.yang @@ -20,6 +20,12 @@ module infix-dhcp-server { contact "kernelkit@googlegroups.com"; description "This module implements a DHCPv4 server"; + revision 2026-09-24 { + description "Constrain the character set and length of static-host + match hostname and client-id string."; + reference "internal"; + } + revision 2026-09-18 { description "Add network boot parameters (BOOTP siaddr/file, option 66/67) at global, subnet, and host scope."; @@ -283,7 +289,13 @@ module infix-dhcp-server { case hostname { leaf hostname { description "Match on client hostname, DHCP option 12."; - type string; + reference "RFC 2132, sec. 3.14: Host Name Option. The + option length field is one octet, so the value + is at most 255 bytes."; + type string { + length "1..255"; + pattern '[a-zA-Z0-9_.-]+'; + } } } @@ -298,7 +310,14 @@ module infix-dhcp-server { description "String value for text-based client-id. Example: xyzzy"; - type string; + reference "RFC 2132, sec. 9.14: Client-identifier + (option 61). The option length field is + one octet, so the value is at most 255 + bytes."; + type string { + length "1..255"; + pattern '[a-zA-Z0-9_.:+-]+'; + } } } case hex { diff --git a/src/confd/yang/confd/infix-dhcp-server@2026-09-18.yang b/src/confd/yang/confd/infix-dhcp-server@2026-09-24.yang similarity index 100% rename from src/confd/yang/confd/infix-dhcp-server@2026-09-18.yang rename to src/confd/yang/confd/infix-dhcp-server@2026-09-24.yang diff --git a/src/confd/yang/confd/infix-hardware.yang b/src/confd/yang/confd/infix-hardware.yang index 630bf570e..6769774b6 100644 --- a/src/confd/yang/confd/infix-hardware.yang +++ b/src/confd/yang/confd/infix-hardware.yang @@ -21,6 +21,11 @@ module infix-hardware { contact "kernelkit@googlegroups.com"; description "Vital Product Data augmentation of ieee-hardware and deviations."; + revision 2026-09-24 { + description "Constrain the character set of hardware component names."; + reference "internal"; + } + revision 2026-07-02 { description "Widen wifi max-interfaces ap/station to uint16, virtual radios (mac80211_hwsim) report combinations up to 2048."; @@ -163,6 +168,16 @@ module infix-hardware { description "GPS/GNSS receiver for time synchronization"; } + deviation "/iehw:hardware/iehw:component/iehw:name" { + deviate replace { + type string { + pattern '[a-zA-Z0-9_][a-zA-Z0-9_.:+@-]*'; + } + } + description "Component names are plain identifiers, including + device-tree unit addresses such as 'sfp@9'."; + } + deviation "/iehw:hardware/iehw:component/iehw:state/iehw:admin-state" { deviate add { must ". = 'locked' or . = 'unlocked'" { diff --git a/src/confd/yang/confd/infix-hardware@2026-07-02.yang b/src/confd/yang/confd/infix-hardware@2026-09-24.yang similarity index 100% rename from src/confd/yang/confd/infix-hardware@2026-07-02.yang rename to src/confd/yang/confd/infix-hardware@2026-09-24.yang diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 4f037e54f..078e50204 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -48,6 +48,12 @@ submodule infix-if-wifi { - Security: WPA2/WPA3 with keystore integration - Operational state: Connection status, RSSI, client lists"; + revision 2026-09-24 { + description + "Constrain the character set of mesh-id and nas-identifier."; + reference "internal"; + } + revision 2026-07-01 { description "Add station 'bssid' operational leaf: the BSSID the station is @@ -527,6 +533,7 @@ submodule infix-if-wifi { } type string { length "1..253"; + pattern '[a-zA-Z0-9_.:+-]+'; } } default auto; @@ -697,6 +704,9 @@ submodule infix-if-wifi { leaf mesh-id { type string { length "1..32"; + pattern '[^\x00-\x1f\x22\x5c\x7f]*' { + error-message "Mesh ID must not contain control characters, double quotes, or backslashes."; + } } mandatory true; description diff --git a/src/confd/yang/confd/infix-if-wifi@2026-07-01.yang b/src/confd/yang/confd/infix-if-wifi@2026-09-24.yang similarity index 100% rename from src/confd/yang/confd/infix-if-wifi@2026-07-01.yang rename to src/confd/yang/confd/infix-if-wifi@2026-09-24.yang diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index d86a9789d..2c11068fb 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -41,6 +41,11 @@ module infix-interfaces { contact "kernelkit@googlegroups.com"; description "Linux bridge and lag extensions for ietf-interfaces."; + revision 2026-09-28 { + description "Constrain the character set of interface names."; + reference "internal"; + } + revision 2026-06-11 { description "Fix WireGuard key-format must expressions, see infix-if-wireguard@2026-06-11."; @@ -227,8 +232,11 @@ module infix-interfaces { deviate replace { type string { length "1..15"; + pattern '[a-zA-Z0-9_][a-zA-Z0-9_.:+-]*'; } } + description "Interface names are plain identifiers, at most 15 + characters (Linux IFNAMSIZ)."; } deviation "/if:interfaces/if:interface/if:description" { diff --git a/src/confd/yang/confd/infix-interfaces@2026-06-11.yang b/src/confd/yang/confd/infix-interfaces@2026-09-28.yang similarity index 100% rename from src/confd/yang/confd/infix-interfaces@2026-06-11.yang rename to src/confd/yang/confd/infix-interfaces@2026-09-28.yang diff --git a/src/confd/yang/confd/infix-keystore.yang b/src/confd/yang/confd/infix-keystore.yang index c27fbcf81..0cea8dc82 100644 --- a/src/confd/yang/confd/infix-keystore.yang +++ b/src/confd/yang/confd/infix-keystore.yang @@ -9,6 +9,12 @@ module infix-keystore { prefix infix-ct; } + revision 2026-09-24 { + description "Constrain the character set of asymmetric-key and + certificate names."; + reference "internal"; + } + revision 2025-12-17 { description "Add WireGuard support, see infix-crypto-types.yang"; } @@ -21,4 +27,22 @@ module infix-keystore { revision 2025-02-04 { description "Initial"; } + + deviation "/ks:keystore/ks:asymmetric-keys/ks:asymmetric-key/ks:name" { + deviate replace { + type string { + pattern '[a-zA-Z0-9_][a-zA-Z0-9_.:+@-]*'; + } + } + description "Key names are plain identifiers."; + } + + deviation "/ks:keystore/ks:asymmetric-keys/ks:asymmetric-key/ks:certificates/ks:certificate/ks:name" { + deviate replace { + type string { + pattern '[a-zA-Z0-9_][a-zA-Z0-9_.:+@-]*'; + } + } + description "Certificate names are plain identifiers."; + } } diff --git a/src/confd/yang/confd/infix-keystore@2025-12-17.yang b/src/confd/yang/confd/infix-keystore@2026-09-24.yang similarity index 100% rename from src/confd/yang/confd/infix-keystore@2025-12-17.yang rename to src/confd/yang/confd/infix-keystore@2026-09-24.yang diff --git a/src/confd/yang/confd/infix-syslog.yang b/src/confd/yang/confd/infix-syslog.yang index b245d68b7..37bd6e908 100644 --- a/src/confd/yang/confd/infix-syslog.yang +++ b/src/confd/yang/confd/infix-syslog.yang @@ -20,6 +20,12 @@ module infix-syslog { contact "kernelkit@googlegroups.com"; description "Infix augments and deviations to ietf-syslog, draft 32."; + revision 2026-09-24 { + description "Constrain the character set of the property-filter value + and the select pattern-match."; + reference "internal"; + } + revision 2026-09-15 { description "Add log RPC for injecting messages in the system log."; reference "internal"; @@ -251,7 +257,9 @@ module infix-syslog { } leaf value { - type string; + type string { + pattern '[a-zA-Z0-9 _.:+*?|^$()\[\]{}/@=,-]+'; + } mandatory true; description "The value to compare against."; } @@ -308,6 +316,26 @@ module infix-syslog { deviate not-supported; } + deviation "/syslog:syslog/syslog:actions/syslog:file/syslog:log-file/syslog:pattern-match" { + description "Restrict the character set, POSIX regular expression + syntax remains available."; + deviate replace { + type string { + pattern '[a-zA-Z0-9 _.:+*?|^$()\[\]{}/@=,-]+'; + } + } + } + + deviation "/syslog:syslog/syslog:actions/syslog:remote/syslog:destination/syslog:pattern-match" { + description "Restrict the character set, POSIX regular expression + syntax remains available."; + deviate replace { + type string { + pattern '[a-zA-Z0-9 _.:+*?|^$()\[\]{}/@=,-]+'; + } + } + } + /* * RPCs */ diff --git a/src/confd/yang/confd/infix-syslog@2026-09-15.yang b/src/confd/yang/confd/infix-syslog@2026-09-24.yang similarity index 100% rename from src/confd/yang/confd/infix-syslog@2026-09-15.yang rename to src/confd/yang/confd/infix-syslog@2026-09-24.yang diff --git a/src/confd/yang/confd/infix-system.yang b/src/confd/yang/confd/infix-system.yang index c3415ee11..5d902ac9c 100644 --- a/src/confd/yang/confd/infix-system.yang +++ b/src/confd/yang/confd/infix-system.yang @@ -383,6 +383,7 @@ module infix-system { augment "/sys:system" { description "Advanced, low-level system customization."; container advanced { + nacm:default-deny-write; description "Advanced system customization, for debugging and development. Settings in this container reach below the abstractions of the diff --git a/test/case/interfaces/wifi_mesh_roaming/test.adoc b/test/case/interfaces/wifi_mesh_roaming/test.adoc index b99e3c5f0..77290bed3 100644 --- a/test/case/interfaces/wifi_mesh_roaming/test.adoc +++ b/test/case/interfaces/wifi_mesh_roaming/test.adoc @@ -20,7 +20,8 @@ so the mesh is a transparent layer-2 backhaul. A fourth node is the client. The test checks the claims the whitepaper makes: - 1. the three nodes form a mesh (each sees its two peers); + 1. the three nodes form a mesh (each lists the other two as peers, and + reports the mesh id and forwarding it runs with); 2. the client associates to the "campus" SSID; 3. traffic reaches the client across the mesh backhaul (host behind gw1 pings the client, which is attached to some gw's AP); @@ -51,7 +52,8 @@ image::topology.svg[WiFi Mesh backhaul with roaming Access Points topology, alig . Set up topology and attach to gw1, gw2, gw3 and the client . Configure gw1, gw2, gw3 as mesh nodes with a roaming AP . Configure the client as a station for the 'campus' SSID -. Verify the three nodes form the mesh backhaul +. Verify each node lists the other two nodes as mesh peers +. Verify each node reports mesh-id 'backhaul' with forwarding enabled . Verify the client associates to the 'campus' SSID . Verify the client is connected to one of the campus APs . Verify the client is reachable across the mesh diff --git a/test/case/interfaces/wifi_mesh_roaming/test.py b/test/case/interfaces/wifi_mesh_roaming/test.py index 2e959aa5d..a2c1ecc06 100755 --- a/test/case/interfaces/wifi_mesh_roaming/test.py +++ b/test/case/interfaces/wifi_mesh_roaming/test.py @@ -18,7 +18,8 @@ The test checks the claims the whitepaper makes: - 1. the three nodes form a mesh (each sees its two peers); + 1. the three nodes form a mesh (each lists the other two as peers, and + reports the mesh id and forwarding it runs with); 2. the client associates to the "campus" SSID; 3. traffic reaches the client across the mesh backhaul (host behind gw1 pings the client, which is attached to some gw's AP); @@ -149,11 +150,20 @@ def gw_config(mesh_mac, ap_mac, uplink=None): ]}}, }) - with test.step("Verify the three nodes form the mesh backhaul"): - for name, dut, _, _ in gws: - until(lambda dut=dut: len(wifi.mesh_peers(dut)) >= 2, + with test.step("Verify each node lists the other two nodes as mesh peers"): + mesh_macs = {mesh_mac.lower() for _, mesh_mac, _ in GWS} + for name, dut, mesh_mac, _ in gws: + until(lambda dut=dut, mesh_mac=mesh_mac: + wifi.mesh_peer_macs(dut) == mesh_macs - {mesh_mac.lower()}, attempts=60, interval=2) + with test.step("Verify each node reports mesh-id 'backhaul' with forwarding enabled"): + for name, dut, _, _ in gws: + mp = wifi.mesh_point(dut) + if mp.get("mesh-id") != MESH_ID or mp.get("forwarding") is not True: + print(f"{name}: {mp}") + test.fail() + with test.step("Verify the client associates to the 'campus' SSID"): until(lambda: wifi.associated(client, SSID), attempts=60, interval=2) diff --git a/test/infamy/wifi.py b/test/infamy/wifi.py index 19dec66b7..942c838b0 100644 --- a/test/infamy/wifi.py +++ b/test/infamy/wifi.py @@ -91,7 +91,16 @@ def ap_stations(target, ifname="wifi0"): return {sta.get("mac-address", "").lower() for sta in stations} +def mesh_point(target, ifname="wifi0"): + """Operational mesh-point container of ifname, empty if not a mesh point.""" + return _wifi(target.get_iface(ifname)).get("mesh-point") or {} + + def mesh_peers(target, ifname="wifi0"): """Peers of the mesh point on ifname.""" - mp = _wifi(target.get_iface(ifname)).get("mesh-point") or {} - return (mp.get("peers") or {}).get("peer") or [] + return (mesh_point(target, ifname).get("peers") or {}).get("peer") or [] + + +def mesh_peer_macs(target, ifname="wifi0"): + """MACs of the mesh peers on ifname, lowercase.""" + return {peer.get("mac-address", "").lower() for peer in mesh_peers(target, ifname)}