diff --git a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg index a4959b993..fcf456f01 100644 --- a/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg +++ b/board/aarch64/raspberrypi-rpi64/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg @@ -55,7 +55,8 @@ }, { "name": "wifi0", - "type": "infix-if-type:wifi" + "type": "infix-if-type:wifi", + "infix-interfaces:wifi": {} } ] }, diff --git a/board/common/qemu/qemu.sh b/board/common/qemu/qemu.sh index d72d147b6..0b518d654 100755 --- a/board/common/qemu/qemu.sh +++ b/board/common/qemu/qemu.sh @@ -360,6 +360,7 @@ run_qemu() $(serial_args) \ $(rw_args) \ $(usb_args) \ + -device usb-host,vendorid=0x0bda,productid=0xc820 \ $(host_args) \ $(net_args) \ $(wdt_args) \ diff --git a/board/common/rootfs/etc/finit.d/available/hostapd@.conf b/board/common/rootfs/etc/finit.d/available/hostapd@.conf new file mode 100644 index 000000000..87e7e55aa --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/hostapd@.conf @@ -0,0 +1,4 @@ +service name:hostapd :%i \ + [2345] hostapd -P/var/run/hostapd-%i.pid /etc/hostapd-%i.conf \ + -- Hostapd (Wi-Fi AccessPoint, 802.1X) @%i + diff --git a/board/common/rootfs/etc/udev/rules.d/70-rename-wifi.rules b/board/common/rootfs/etc/udev/rules.d/70-rename-wifi.rules index 4251fadf8..6789cb54d 100644 --- a/board/common/rootfs/etc/udev/rules.d/70-rename-wifi.rules +++ b/board/common/rootfs/etc/udev/rules.d/70-rename-wifi.rules @@ -1 +1,3 @@ -SUBSYSTEM=="net", ACTION=="add", TEST=="/sys/class/net/$name/wireless", NAME="wifi%n" +# Only rename physical interfaces, skip virtual ones created by hostapd/iw +# Virtual interfaces created by iw have name_assign_type=3 (userspace assigned) +SUBSYSTEM=="net", ACTION=="add", TEST=="/sys/class/net/$name/wireless", ATTR{name_assign_type}!="3", NAME="wifi%n" diff --git a/board/common/rootfs/usr/libexec/infix/wifi-ap-stations b/board/common/rootfs/usr/libexec/infix/wifi-ap-stations new file mode 100755 index 000000000..dbf26e02b --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/wifi-ap-stations @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import sys +import subprocess +import json +import re + +def get_stations(interface): + """Get connected stations for an AP interface""" + try: + result = subprocess.run(['iw', 'dev', interface, 'station', 'dump'], + capture_output=True, text=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + return [] + + stations = [] + station = None + + for line in result.stdout.split('\n'): + line = line.strip() + + if line.startswith('Station'): + if station: + stations.append(station) + mac = re.search(r'([a-fA-F0-9:]{17})', line) + station = { + 'mac-address': mac.group(1) if mac else 'unknown', + 'rssi': 0, + 'tx-speed': 'unknown', + 'rx-speed': 'unknown', + 'connected-time': 'unknown' + } + + elif station: + if 'signal:' in line and 'avg' not in line: + sig = re.search(r'signal:\s*(-?\d+)', line) + if sig: + station['rssi'] = int(sig.group(1)) + elif 'tx bitrate:' in line: + bitrate = re.search(r'tx bitrate:\s*(\d+\.?\d*)\s*(MBit/s|Gbit/s)', line) + if bitrate: + station['tx-speed'] = f"{bitrate.group(1)} {bitrate.group(2).replace('Bit/s', 'bps')}" + elif 'rx bitrate:' in line: + bitrate = re.search(r'rx bitrate:\s*(\d+\.?\d*)\s*(MBit/s|Gbit/s)', line) + if bitrate: + station['rx-speed'] = f"{bitrate.group(1)} {bitrate.group(2).replace('Bit/s', 'bps')}" + elif 'connected time:' in line: + time = re.search(r'connected time:\s*(\d+\s+\w+)', line) + if time: + station['connected-time'] = time.group(1) + + if station: + stations.append(station) + + return stations + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(1) + + print(json.dumps(get_stations(sys.argv[1]), indent=2)) diff --git a/package/feature-wifi/Config.in b/package/feature-wifi/Config.in index 87ed3852a..97f4e09a3 100644 --- a/package/feature-wifi/Config.in +++ b/package/feature-wifi/Config.in @@ -6,6 +6,10 @@ config BR2_PACKAGE_FEATURE_WIFI select BR2_PACKAGE_WPA_SUPPLICANT_AUTOSCAN select BR2_PACKAGE_WPA_SUPPLICANT_CLI select BR2_PACKAGE_WIRELESS_REGDB + select BR2_PACKAGE_HOSTAPD + select BR2_PACKAGE_HOSTAPD_DRIVER_NL80211 + select BR2_PACKAGE_HOSTAPD_WPA3 + select BR2_PACKAGE_HOSTAPD_WPS select BR2_PACKAGE_IW help Enables WiFi in Infix. Enables all requried applications. diff --git a/package/feature-wifi/feature-wifi.mk b/package/feature-wifi/feature-wifi.mk index 90d5bdd3a..08cdd6627 100644 --- a/package/feature-wifi/feature-wifi.mk +++ b/package/feature-wifi/feature-wifi.mk @@ -12,7 +12,6 @@ define FEATURE_WIFI_LINUX_CONFIG_FIXUPS $(call KCONFIG_ENABLE_OPT,CONFIG_RFKILL) $(call KCONFIG_SET_OPT,CONFIG_MAC80211,m) $(call KCONFIG_SET_OPT,CONFIG_CFG80211,m) - $(if $(filter y,$(BR2_PACKAGE_FEATURE_WIFI_DONGLE_REALTEK)), $(call KCONFIG_ENABLE_OPT,CONFIG_WLAN_VENDOR_REALTEK) $(call KCONFIG_ENABLE_OPT,CONFIG_RTW88) diff --git a/patches/hostapd/0001-skip-mask-check.patch b/patches/hostapd/0001-skip-mask-check.patch new file mode 100644 index 000000000..e61f25ec0 --- /dev/null +++ b/patches/hostapd/0001-skip-mask-check.patch @@ -0,0 +1,26 @@ +We create the interfaces on our own, and set their MAC addresses +remove this check (which are for hostapd created ap interfaces) + +diff --git a/src/ap/hostapd.c b/src/ap/hostapd.c +index a57a151fa..43d31d16c 100644 +--- a/src/ap/hostapd.c ++++ b/src/ap/hostapd.c +@@ -940,18 +940,6 @@ skip_mask_ext: + if (!auto_addr) + return 0; + +- for (i = 0; i < ETH_ALEN; i++) { +- if ((hapd->own_addr[i] & mask[i]) != hapd->own_addr[i]) { +- wpa_printf(MSG_ERROR, "Invalid BSSID mask " MACSTR +- " for start address " MACSTR ".", +- MAC2STR(mask), MAC2STR(hapd->own_addr)); +- wpa_printf(MSG_ERROR, "Start address must be the " +- "first address in the block (i.e., addr " +- "AND mask == addr)."); +- return -1; +- } +- } +- + return 0; + } + diff --git a/src/confd/src/core.c b/src/confd/src/core.c index e24dca9e3..9e38171be 100644 --- a/src/confd/src/core.c +++ b/src/confd/src/core.c @@ -138,6 +138,55 @@ static confd_dependency_t handle_dependencies(struct lyd_node **diff, struct lyd } } + /* Check if any wifi-ap interface changed, add all wifi-ap interfaces and their radios to diff */ + struct ly_set *diff_ifaces = lydx_find_xpathf(*diff, "/ietf-interfaces:interfaces/interface[type='infix-if-type:wifi-ap']"); + if (diff_ifaces) { + struct ly_set *wifi_ap_ifaces = lydx_find_xpathf(config, "/ietf-interfaces:interfaces/interface[type='infix-if-type:wifi-ap']"); + size_t i; + + for (i = 0; wifi_ap_ifaces && i < wifi_ap_ifaces->count; i++) { + struct lyd_node *iface = wifi_ap_ifaces->dnodes[i]; + struct lyd_node *wifi_node, *radio_node; + const char *ifname, *radio_name; + char xpath[256]; + + ifname = lydx_get_cattr(iface, "name"); + wifi_node = lydx_get_child(iface, "wifi"); + + if (wifi_node) { + /* Add the radio attribute to the diff */ + radio_node = lydx_get_child(wifi_node, "radio"); + radio_name = lyd_get_value(radio_node); + /* Add radio attribute to wifi-ap interface in diff */ + snprintf(xpath, sizeof(xpath), + "/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi/radio", + ifname); + result = add_dependencies(diff, xpath, radio_name); + if (result == CONFD_DEP_ERROR) { + ERROR("Failed to add radio attribute to wifi-ap %s in diff", ifname); + ly_set_free(wifi_ap_ifaces, NULL); + goto err; + } + + /* Add the referenced radio interface (type wifi) to diff */ + snprintf(xpath, sizeof(xpath), + "/ietf-interfaces:interfaces/interface[name='%s']/infix-interfaces:wifi", + radio_name); + result = add_dependencies(diff, xpath, ""); + if (result == CONFD_DEP_ERROR) { + ERROR("Failed to add radio interface %s to diff", radio_name); + ly_set_free(wifi_ap_ifaces, NULL); + goto err; + } + } + } + + if (wifi_ap_ifaces) + ly_set_free(wifi_ap_ifaces, NULL); + } +err: + ly_set_free(diff_ifaces, NULL); + return result; } @@ -180,7 +229,7 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod } max_dep--; } -#if 0 +#if 1 /* Debug: print diff to file */ FILE *f = fopen("/tmp/confd-diff.json", "w"); if (f) { diff --git a/src/confd/src/if-wifi.c b/src/confd/src/if-wifi.c index ab934a841..6eb0cd577 100644 --- a/src/confd/src/if-wifi.c +++ b/src/confd/src/if-wifi.c @@ -1,18 +1,43 @@ /* SPDX-License-Identifier: BSD-3-Clause */ #include +#include #include "interfaces.h" #define WPA_SUPPLICANT_FINIT_CONF "/etc/finit.d/available/wpa_supplicant-%s.conf" #define WPA_SUPPLICANT_CONF "/etc/wpa_supplicant-%s.conf" +#define HOSTAPD_SUPPLICANT_CONF "/etc/hostapd-%s.conf" -static int wifi_gen_config(const char *ifname, const char *ssid, const char *country, const char *secret, const char* encryption, struct dagger *net) +/* Comparison function for sorting AP interfaces by name (ascending) */ +static int compare_ap_interfaces(const void *a, const void *b) +{ + struct lyd_node *node_a = *(struct lyd_node **)a; + struct lyd_node *node_b = *(struct lyd_node **)b; + const char *name_a = lydx_get_cattr(node_a, "name"); + const char *name_b = lydx_get_cattr(node_b, "name"); + return strcmp(name_a, name_b); +} + +struct lyd_node *wifi_ap_get_radio(struct lyd_node *cif) { + struct lyd_node *wifi = lydx_get_child(cif, "wifi"); + if (wifi) { + const char *radio = lydx_get_cattr(wifi, "radio"); + if (radio) { + struct lyd_node *radio_if = lydx_get_xpathf(cif, "../interface[name='%s']", radio); + if (radio_if) + return radio_if; + } + } + return NULL; +} +static int wifi_gen_station_config(const char *ifname, const char *ssid, const char *country, const char *secret, const char* encryption, struct dagger *net, int counter) { FILE *wpa_supplicant = NULL, *wpa = NULL; char *encryption_str; int rc = SR_ERR_OK; - if (!secret && (ssid && country && encryption)) { + ERROR("%s", __func__); + if (!secret && (ssid && encryption)) { /* Not an error, updated from two ways, interface cb and keystore cb. */ return 0; } @@ -38,7 +63,7 @@ static int wifi_gen_config(const char *ifname, const char *ssid, const char *cou goto out; } - if (!secret || !ssid || !country || !encryption) { + if (!ssid || !secret) { fprintf(wpa_supplicant, "ctrl_interface=/run/wpa_supplicant\n" "autoscan=periodic:10\n" @@ -49,17 +74,19 @@ static int wifi_gen_config(const char *ifname, const char *ssid, const char *cou } else { asprintf(&encryption_str, "key_mgmt=SAE WPA-PSK\npsk=\"%s\"", secret); } - fprintf(wpa_supplicant, - "country=%s\n" - "ctrl_interface=/run/wpa_supplicant\n" - "autoscan=periodic:10\n" - "ap_scan=1\n" - "network={\n" + if (!counter) { /* First SSID */ + fprintf(wpa_supplicant, + "country=%s\n" + "ctrl_interface=/run/wpa_supplicant\n" + "autoscan=periodic:10\n" + "ap_scan=1\n\n", country); + } + fprintf(wpa_supplicant, "network={\n" "bgscan=\"simple: 30:-45:300\"\n" "ssid=\"%s\"\n" "%s\n" - "}\n", country, ssid, encryption_str); - free(encryption_str); + "}\n\n", ssid, encryption_str); + free(encryption_str); } fclose(wpa_supplicant); @@ -67,44 +94,513 @@ static int wifi_gen_config(const char *ifname, const char *ssid, const char *cou return rc; } -int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net) +static void disable_wifi_station(const char *ifname, FILE *fp) { - const char *ssid, *secret_name, *secret, *ifname, *country, *encryption; - struct lyd_node *wifi, *secret_node; + ERROR("%s", __func__); + fprintf(fp, "# Generated by Infix confd\n"); + fprintf(fp, "iw dev %s disconnect\n", ifname); + fprintf(fp, "initctl -bfqn disable wifi@%s\n", ifname); + erasef(WPA_SUPPLICANT_CONF, ifname); +} +static void disable_wifi_ap(const char *ifname, FILE *fp) +{ + ERROR("%s", __func__); + fprintf(fp, "# Generated by Infix confd\n"); + fprintf(fp, "initctl -bfqn disable hostapd@%s\n", ifname); + erasef(HOSTAPD_SUPPLICANT_CONF, ifname); +} +int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net) { + struct lyd_node *cwifi, *dwifi = NULL, *cmode, *dmode = NULL; + const char *ifname = lydx_get_cattr(cif, "name"); + FILE *fp; + cwifi = lydx_get_child(cif, "wifi"); + ERROR("%s", __func__); + if (!cwifi) + return SR_ERR_OK; + cmode = lydx_get_child(cwifi, "mode"); + + fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "disable-wifi.sh"); + if (dif) { + dwifi = lydx_get_child(dif, "wifi"); + if (dwifi) + dmode = lydx_get_child(dwifi, "mode"); + } + if (!lydx_get_cattr(cif, "enabled")) { + if (dwifi) { + if (dmode && !strcmp(lyd_get_value(dmode), "accesspoint")) + disable_wifi_ap(ifname, fp); + else + disable_wifi_station(ifname, fp); + } + goto out; + } + + if (cmode && !strcmp(lyd_get_value(cmode), "accesspoint")) { + if (dmode && strcmp(lyd_get_value(dmode), "accesspoint")) + disable_wifi_station(ifname, fp); + wifi_ap_gen(cif, net); + + } else { + if (dmode && strcmp(lyd_get_value(dmode), "station")) + disable_wifi_ap(ifname, fp); + /* Client */ + wifi_station_gen(cif, net); + } +out: + fclose(fp); + return SR_ERR_OK; +} +int wifi_station_gen(struct lyd_node *cif, struct dagger *net) +{ + const char *ssid_name, *secret_name, *secret = NULL, *ifname, *country; + const char *encryption, *mode; + struct lyd_node *wifi, *secret_node; bool enabled; + int counter = 0; + FILE *fp; + ERROR("%s", __func__); ifname = lydx_get_cattr(cif, "name"); + fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "disable-wifi.sh"); - if (cif && !lydx_get_child(cif, "wifi")) { - return wifi_gen_config(ifname, NULL, NULL, NULL, NULL, net); + if (!fp) { + ERROR("Could not open disable-wifi.sh"); + return SR_ERR_INTERNAL; } - enabled = lydx_get_bool(cif, "enabled"); wifi = lydx_get_child(cif, "wifi"); - ssid = lydx_get_cattr(wifi, "ssid"); + if (!enabled || !wifi) { + disable_wifi_station(ifname, fp); + goto out; + } + + mode = lydx_get_cattr(wifi, "mode"); + if (mode && !strcmp(mode, "accesspoint")) { + /* Interface is in access point mode - handle radio setup */ + disable_wifi_station(ifname, fp); + fclose(fp); + return wifi_ap_gen(cif, net); + } + + /* Clean up any existing station configuration */ + erasef(WPA_SUPPLICANT_CONF, ifname); + + country = lydx_get_cattr(wifi, "country-code"); + if (!country) + country = "00"; + + if (!lydx_get_child(wifi, "ssid")) { + /* Only the presence container is set - radio only, no station connection */ + wifi_gen_station_config(ifname, NULL, country, NULL, NULL, net, 0); + goto out; + } + + ssid_name = lydx_get_cattr(wifi, "ssid"); secret_name = lydx_get_cattr(wifi, "secret"); - country = lydx_get_cattr(wifi, "country-code"); encryption = lydx_get_cattr(wifi, "encryption"); - secret_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name); - secret = lydx_get_cattr(secret_node, "cleartext-key"); + if (secret_name) { + secret_node = lydx_get_xpathf(cif, "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name); + secret = lydx_get_cattr(secret_node, "cleartext-key"); + } + wifi_gen_station_config(ifname, ssid_name, country, secret, encryption, net, counter); + counter++; + +out: + fclose(fp); + return SR_ERR_OK; +} + +int wifi_ap_del_iface(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net) +{ + const char *ifname, *radio; + struct lyd_node *wifi; + FILE *iw; + bool is_last_ap = false; + + ifname = lydx_get_cattr(dif, "name"); + ERROR("%s: %s", __func__, ifname); + + wifi = lydx_get_child(dif, "wifi"); + if (wifi) { + radio = lydx_get_cattr(wifi, "radio"); + ERROR("Found radio: %s for interface %s", radio, ifname); + if (radio) { + struct lyd_node *iface, *sibling; + struct lyd_node **matching; + uint32_t match_count = 0, alloc_count = 8; + + /* Iterate through sibling interfaces, filter for + * non-created wifi-ap interfaces with matching radio. + */ + matching = calloc(alloc_count, sizeof(struct lyd_node *)); + if (!matching) + goto skip_position_check; + + sibling = lyd_first_sibling(dif); + LYX_LIST_FOR_EACH(sibling, iface, "interface") { + struct lyd_node *iface_radio_node; + const char *iface_radio; - if (!enabled) - return wifi_gen_del(cif, net); + /* Skip created interfaces - only consider deleted/modified */ + if (lydx_get_op(iface) == LYDX_OP_CREATE) + continue; - return wifi_gen_config(ifname, ssid, country, secret, encryption, net); + /* Check wifi/radio to identify wifi-ap interfaces */ + iface_radio_node = lydx_get_descendant(lyd_child(iface), "wifi", "radio", NULL); + if (!iface_radio_node) + continue; + + iface_radio = lyd_get_value(iface_radio_node); + if (!iface_radio || strcmp(iface_radio, radio)) + continue; + + if (match_count >= alloc_count) { + alloc_count *= 2; + matching = realloc(matching, alloc_count * sizeof(struct lyd_node *)); + } + matching[match_count++] = iface; + } + + ERROR("Found %u non-created interfaces matching radio %s", match_count, radio); + if (match_count > 0) { + /* Sort in normal order to find the last AP (renamed radio) */ + qsort(matching, match_count, sizeof(struct lyd_node *), compare_ap_interfaces); + + for (uint32_t i = 0; i < match_count; i++) { + const char *ap_ifname = lydx_get_cattr(matching[i], "name"); + ERROR(" AP[%d]: %s", i, ap_ifname); + if (!strcmp(ap_ifname, ifname)) { + is_last_ap = (i == match_count - 1); + ERROR(" MATCH! is_last_ap=%d", is_last_ap); + break; + } + } + } + + free(matching); + } + } +skip_position_check: + + iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_POST, "exit-iw.sh"); + + if (is_last_ap) { + /* Last AP (e.g., wifi0-ap2) is the renamed radio - restore original radio name and MAC */ + fprintf(iw, "# Last AP is renamed radio, restore original radio name and MAC\n"); + fprintf(iw, "logger -t confd -p daemon.info \"Restoring radio name from %s to %s\"\n", ifname, radio); + fprintf(iw, "ip link set dev %s down\n", ifname); + fprintf(iw, "ip link property del dev %s altname %s 2>/dev/null || true\n", ifname, radio); + fprintf(iw, "ip link set dev %s name %s\n", ifname, radio); + /* Restore original MAC address from permaddr */ + fprintf(iw, "permaddr=$(ip -d -j link show dev %s | jq -rM '.[].permaddr // empty')\n", radio); + fprintf(iw, "if [ -n \"$permaddr\" ]; then\n"); + fprintf(iw, " logger -t confd -p daemon.info \"Restoring original MAC $permaddr on %s\"\n", radio); + fprintf(iw, " ip link set dev %s address $permaddr\n", radio); + fprintf(iw, "fi\n"); + } else { + /* Not last AP - delete virtual interface */ + fprintf(iw, "# Virtual AP interface, delete it\n"); + fprintf(iw, "logger -t confd -p daemon.info \"Deleting virtual AP interface %s\"\n", ifname); + fprintf(iw, "iw dev %s del\n", ifname); + } + + fclose(iw); + + return 0; } -int wifi_gen_del(struct lyd_node *dif, struct dagger *net) +int wifi_ap_add_iface(struct lyd_node *cif,struct dagger *net) { - const char *ifname = lydx_get_cattr(dif, "name"); - FILE *iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_PRE, "iw.sh"); + const char *ifname, *radio; + struct lyd_node *wifi; + struct ly_set *ap_interfaces = NULL; + FILE *iw; + int rc; + + ifname = lydx_get_cattr(cif, "name"); + + wifi = lydx_get_child(cif, "wifi"); + if (!wifi) { + ERROR("wifi-ap interface %s missing wifi configuration", ifname); + return SR_ERR_INVAL_ARG; + } + + radio = lydx_get_cattr(wifi, "radio"); + if (!radio) { + ERROR("wifi-ap interface %s missing radio reference", ifname); + return SR_ERR_INVAL_ARG; + } + + dagger_add_dep(&confd.netdag, ifname, radio); + + /* Find all wifi-ap interfaces that reference the same radio */ + rc = lyd_find_xpath(cif, "../interface[derived-from-or-self(type, 'infix-if-type:wifi-ap') and wifi/radio = current()/wifi/radio]", &ap_interfaces); + if (rc != LY_SUCCESS || !ap_interfaces || ap_interfaces->count == 0) { + ERROR("Failed to find wifi-ap interfaces for radio %s", radio); + return SR_ERR_INTERNAL; + } + + /* Sort interfaces by name to ensure consistent ordering */ + qsort(ap_interfaces->dnodes, ap_interfaces->count, sizeof(struct lyd_node *), compare_ap_interfaces); + + /* Find our position in the AP list */ + bool is_last_ap = false; + uint32_t last_idx = ap_interfaces->count - 1; + for (uint32_t i = 0; i < ap_interfaces->count; i++) { + if (ap_interfaces->dnodes[i] == cif) { + is_last_ap = (i == last_idx); + /* If not first, add dependency to previous AP for sequential creation */ + if (i > 0) { + const char *prev_ap_name = lydx_get_cattr(ap_interfaces->dnodes[i-1], "name"); + dagger_add_dep(&confd.netdag, ifname, prev_ap_name); + ERROR("Adding dependency: %s depends on %s (AP sequence)", ifname, prev_ap_name); + } + break; + } + } + ly_set_free(ap_interfaces, NULL); + + iw = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PRE, "init-iw.sh"); + + if (is_last_ap) { + /* Last AP interface - rename radio to AP name and preserve radio name as altname */ + fprintf(iw, "# Last AP interface, rename radio to AP name\n"); + fprintf(iw, "logger -t confd -p daemon.info \"Renaming radio %s to AP interface %s\"\n", radio, ifname); + fprintf(iw, "ip link set dev %s down\n", radio); + fprintf(iw, "ip link set dev %s name %s\n", radio, ifname); + fprintf(iw, "ip link property add dev %s altname %s\n", ifname, radio); + } else { + /* Not last AP - create virtual interface on the radio */ + fprintf(iw, "# Virtual AP interface, create on radio\n"); + fprintf(iw, "logger -t confd -p daemon.info \"Creating virtual AP interface %s on %s\"\n", ifname, radio); + fprintf(iw, "iw dev %s interface add %s type __ap\n", radio, ifname); + } - fprintf(iw, "# Generated by Infix confd\n"); - fprintf(iw, "iw dev %s disconnect\n", ifname); - fprintf(iw, "initctl -bfqn disable wifi@%s\n", ifname); fclose(iw); - erasef(WPA_SUPPLICANT_CONF, ifname); + return 0; +} + +int wifi_is_accesspoint(struct lyd_node *cif) { + struct lyd_node *wifi; + const char *mode; + ERROR("%s", __func__); + wifi = lydx_get_child(cif, "wifi"); + if (wifi) { + mode = lydx_get_cattr(wifi, "mode"); + ERROR("Accesspoint?: %d", !!strcmp(mode, "accesspoint")); + if (mode) + return !!strcmp(mode, "accesspoint"); + } + ERROR("NOT ACCESSPOINT"); + return 0; +} +int wifi_ap_gen(struct lyd_node *cif, struct dagger *net) +{ + struct lyd_node *wifi, *ap_interface; + struct ly_set *ap_interfaces = NULL; + FILE *hostapd_conf, *hostapd_finit; + const char *country, *band, *channel, *ifname; + const char *main_interface_name; + bool freq_24GHz; + int rc = SR_ERR_OK; + + ERROR("%s", __func__); + ERROR("GENERATE AP"); + ifname = lydx_get_cattr(cif, "name"); + wifi = lydx_get_child(cif, "wifi"); + + country = lydx_get_cattr(wifi, "country-code"); + band = lydx_get_cattr(wifi, "band"); + channel = lydx_get_cattr(wifi, "channel"); + freq_24GHz = !strcmp(band, "2.4GHz"); + + if (!channel || !strcmp(channel, "auto")) + channel = freq_24GHz ? "6" : "149"; + + ERROR("Searching radio"); + /* Find all wifi-ap interfaces that reference this radio */ + rc = lyd_find_xpath(cif, "../interface[derived-from-or-self(type, 'infix-if-type:wifi-ap') and wifi/radio = current()/name]", &ap_interfaces); + if (rc != LY_SUCCESS || !ap_interfaces || ap_interfaces->count == 0) { + ERROR("No wifi-ap interfaces reference radio %s", ifname); + return SR_ERR_OK; + } + + /* Sort interfaces by name to ensure consistent ordering */ + qsort(ap_interfaces->dnodes, ap_interfaces->count, sizeof(struct lyd_node *), compare_ap_interfaces); + + /* The last AP interface becomes the main interface (radio gets renamed to this) */ + ap_interface = ap_interfaces->dnodes[ap_interfaces->count - 1]; + main_interface_name = lydx_get_cattr(ap_interface, "name"); + + ERROR("Generating hostapd config for radio %s, main interface %s with %d total APs", + ifname, main_interface_name, ap_interfaces->count); + + /* Clean up any existing AP configuration */ + erasef(HOSTAPD_SUPPLICANT_CONF, ifname); + + hostapd_conf = fopenf("w", HOSTAPD_SUPPLICANT_CONF, ifname); + if (!hostapd_conf) { + ly_set_free(ap_interfaces, NULL); + return SR_ERR_INTERNAL; + } + + fprintf(hostapd_conf, "# Generated by Infix confd for radio %s (main interface %s)\n", + ifname, main_interface_name); + + /* Basic hostapd configuration using the main AP interface name */ + fprintf(hostapd_conf, + "interface=%s\n" + "driver=nl80211\n" + "hw_mode=%c\n" + "wmm_enabled=1\n" + "channel=%s\n" + "logger_syslog=-1\n" + "logger_syslog_level=0\n" + "logger_stdout=0\n" + "ctrl_interface=/var/run/hostapd\n" + "ctrl_interface_group=0\n\n", + main_interface_name, freq_24GHz ? 'g' : 'a', channel); + + if (strcmp(country, "00")) + fprintf(hostapd_conf, "country_code=%s\n", country); + + if (freq_24GHz) + fprintf(hostapd_conf, "ieee80211n=1\n"); + else + fprintf(hostapd_conf, "ieee80211ac=1\n"); + + /* Configure last AP interface as main SSID (it's the renamed radio) */ + struct lyd_node *main_wifi = lydx_get_child(ap_interface, "wifi"); + if (main_wifi) { + const char *ssid = lydx_get_cattr(main_wifi, "ssid"); + const char *secret_name = lydx_get_cattr(main_wifi, "secret"); + const char *encryption = lydx_get_cattr(main_wifi, "encryption"); + const char *secret = NULL; + + if (encryption && secret_name) { + struct lyd_node *secret_node = lydx_get_xpathf(ap_interface, + "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name); + secret = lydx_get_cattr(secret_node, "cleartext-key"); + } + + fprintf(hostapd_conf, "\n# Main SSID: %s\n", ssid); + fprintf(hostapd_conf, "ssid=%s\n", ssid); + + if (encryption && !strcmp(encryption, "mixed-wpa2-wpa3") && secret) { + fprintf(hostapd_conf, "wpa_key_mgmt=WPA-PSK SAE\n"); + fprintf(hostapd_conf, "wpa_passphrase=%s\n", secret); + fprintf(hostapd_conf, "sae_password=%s\n", secret); + fputs("wpa_pairwise=CCMP\n", hostapd_conf); + fputs("rsn_pairwise=CCMP\n", hostapd_conf); + fputs("ieee80211w=1\n", hostapd_conf); + fputs("wpa=2\n", hostapd_conf); + } + fputs("ignore_broadcast_ssid=0\n", hostapd_conf); + } + + /* Add other AP interfaces as BSS entries (all except the last one) */ + for (uint32_t i = 0; i < ap_interfaces->count - 1; i++) { + ap_interface = ap_interfaces->dnodes[i]; + const char *ap_ifname = lydx_get_cattr(ap_interface, "name"); + struct lyd_node *ap_wifi = lydx_get_child(ap_interface, "wifi"); + + if (!ap_wifi) continue; + + const char *ssid = lydx_get_cattr(ap_wifi, "ssid"); + const char *secret_name = lydx_get_cattr(ap_wifi, "secret"); + const char *encryption = lydx_get_cattr(ap_wifi, "encryption"); + const char *secret = NULL; + + if (encryption && secret_name) { + struct lyd_node *secret_node = lydx_get_xpathf(ap_interface, + "../../keystore/symmetric-keys/symmetric-key[name='%s']", secret_name); + secret = lydx_get_cattr(secret_node, "cleartext-key"); + if (!secret) { + ERROR("Could not retrieve secret key '%s' for BSS %s", secret_name, ap_ifname); + continue; + } + } + + /* Add as BSS entry */ + fprintf(hostapd_conf, "\nbss=%s\n", ap_ifname); + fprintf(hostapd_conf, "# SSID: %s\n", ssid); + fprintf(hostapd_conf, "ssid=%s\n", ssid); + + if (encryption && !strcmp(encryption, "mixed-wpa2-wpa3") && secret) { + fprintf(hostapd_conf, "wpa_key_mgmt=WPA-PSK SAE\n"); + fprintf(hostapd_conf, "wpa_passphrase=%s\n", secret); + fprintf(hostapd_conf, "sae_password=%s\n", secret); + fputs("wpa_pairwise=CCMP\n", hostapd_conf); + fputs("rsn_pairwise=CCMP\n", hostapd_conf); + fputs("ieee80211w=1\n", hostapd_conf); + fputs("wpa=2\n", hostapd_conf); + } + fputs("ignore_broadcast_ssid=0\n", hostapd_conf); + } + + ly_set_free(ap_interfaces, NULL); + fclose(hostapd_conf); + + /* Generate init script for the main interface */ + hostapd_finit = dagger_fopen_net_init(net, main_interface_name, NETDAG_INIT_POST, "hostapd.sh"); + if (!hostapd_finit) + return SR_ERR_INTERNAL; + + fprintf(hostapd_finit, "# Generated by Infix confd\n"); + fprintf(hostapd_finit, "if [ -f '/etc/finit.d/enabled/hostapd@%s.conf' ];then\n", ifname); + fprintf(hostapd_finit, "initctl -bfqn touch hostapd@%s\n", ifname); + fprintf(hostapd_finit, "else\n"); + fprintf(hostapd_finit, "initctl -bfqn enable hostapd@%s\n", ifname); + fprintf(hostapd_finit, "fi\n"); + fclose(hostapd_finit); + + return rc; +} + +bool wifi_ap_must_delete(struct lyd_node *dif) +{ + + struct lyd_node *cwifi; + const char *radio_name; + struct lyd_node *radio_dif; + ERROR("%s", __func__); + + /* Get the wifi container from the current interface */ + cwifi = lydx_get_child(dif, "wifi"); + if (!cwifi) + return false; + + /* Get the radio reference */ + radio_name = lydx_get_cattr(cwifi, "radio"); + if (!radio_name) + return false; + + /* Look for the radio interface in dif to see if it's being deleted */ + radio_dif = lydx_get_xpathf(dif, "../interface[name='%s']", radio_name); + if (radio_dif) { + ERROR("%s must delete, radio change", lydx_get_cattr(dif, "name")); + return true; + } + + return false; +} + +int wifi_gen_del(struct lyd_node *iface, struct dagger *net) +{ + const char *ifname; + FILE *fp; + ERROR("%s", __func__); + ifname = lydx_get_cattr(iface, "name"); + fp = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT_PRE, "disable-wifi.sh"); + if (!fp) { + ERROR("Failed to open disable-wifi.sh"); + return SR_ERR_INTERNAL; + } + /* Disable both station and AP services for this interface */ + disable_wifi_station(ifname, fp); + disable_wifi_ap(ifname, fp); + fclose(fp); return SR_ERR_OK; } diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index 0a0cc0326..9f85e644e 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -80,6 +80,8 @@ static int ifchange_cand_infer_type(sr_session_ctx_t *session, const char *path) if (!fnmatch("wifi+([0-9])", ifname, FNM_EXTMATCH)) inferred.data.string_val = "infix-if-type:wifi"; + if (!fnmatch("wifi([0-9])-ap+([0-9])", ifname, FNM_EXTMATCH)) + inferred.data.string_val = "infix-if-type:wifi-ap"; else if (iface_is_phys(ifname)) inferred.data.string_val = "infix-if-type:ethernet"; else if (!fnmatch("br+([0-9])", ifname, FNM_EXTMATCH)) @@ -419,6 +421,8 @@ static int netdag_gen_afspec_add(sr_session_ctx_t *session, struct dagger *net, return vxlan_gen(NULL, cif, ip); case IFT_WIFI: return wifi_gen(NULL, cif, net); + case IFT_WIFI_AP: + return wifi_ap_add_iface(cif, net); case IFT_ETH: return netdag_gen_ethtool(net, cif, dif); case IFT_LO: @@ -449,6 +453,7 @@ static int netdag_gen_afspec_set(sr_session_ctx_t *session, struct dagger *net, return netdag_gen_ethtool(net, cif, dif); case IFT_WIFI: return wifi_gen(dif, cif, net); + case IFT_WIFI_AP: /* Is generated from radio interface */ case IFT_DUMMY: case IFT_GRE: case IFT_GRETAP: @@ -477,7 +482,8 @@ static bool netdag_must_del(struct lyd_node *dif, struct lyd_node *cif) case IFT_WIFI: case IFT_ETH: return lydx_get_child(dif, "custom-phys-address"); - + case IFT_WIFI_AP: + return lydx_get_child(dif, "custom-phys-address") || lydx_get_child((dif), "wifi"); case IFT_GRE: case IFT_GRETAP: return lydx_get_descendant(lyd_child(dif), "gre", NULL); @@ -523,6 +529,7 @@ static int eth_gen_del(struct lyd_node *dif, FILE *ip) static int link_gen_del(struct lyd_node *dif, FILE *ip) { + ERROR("%s: Called with %s", __func__, lydx_get_cattr(dif, "name")); fprintf(ip, "link del dev %s\n", lydx_get_cattr(dif, "name")); return 0; } @@ -566,9 +573,13 @@ static int netdag_gen_iface_del(struct dagger *net, struct lyd_node *dif, eth_gen_del(dif, ip); wifi_gen_del(dif, net); break; + case IFT_WIFI_AP: + wifi_ap_del_iface(dif, cif, net); + break; case IFT_VETH: veth_gen_del(dif, ip); break; + case IFT_BRIDGE: case IFT_DUMMY: case IFT_GRE: @@ -609,11 +620,13 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net int err = 0; FILE *ip; - err = netdag_gen_iface_timeout(net, ifname, iftype); if (err) goto err; + if (!strcmp(iftype, "infix-if-type:wifi") && wifi_is_accesspoint(cif)) + return SR_ERR_OK; + if ((err = cni_netdag_gen_iface(net, ifname, dif, cif))) { /* error or managed by CNI/podman */ if (err > 0) @@ -738,6 +751,7 @@ static int netdag_init_iface(struct lyd_node *cif) case IFT_DUMMY: case IFT_ETH: case IFT_WIFI: + case IFT_WIFI_AP: case IFT_GRE: case IFT_GRETAP: case IFT_LO: diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index aaa583688..a3414d5b8 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -26,6 +26,7 @@ _map(IFT_DUMMY, "infix-if-type:dummy") \ _map(IFT_ETH, "infix-if-type:ethernet") \ _map(IFT_WIFI, "infix-if-type:wifi") \ + _map(IFT_WIFI_AP, "infix-if-type:wifi-ap") \ _map(IFT_GRE, "infix-if-type:gre") \ _map(IFT_GRETAP, "infix-if-type:gretap") \ _map(IFT_LAG, "infix-if-type:lag") \ @@ -123,7 +124,14 @@ int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); /* if-wifi.c */ int wifi_gen(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net); -int wifi_gen_del(struct lyd_node *dif, struct dagger *net); +int wifi_station_gen(struct lyd_node *cif, struct dagger *net); +int wifi_ap_add_iface(struct lyd_node *cif,struct dagger *net); +int wifi_ap_del_iface(struct lyd_node *dif, struct lyd_node *cif, struct dagger *net); +int wifi_ap_gen(struct lyd_node *cif, struct dagger *net); +int wifi_gen_del(struct lyd_node *iface, struct dagger *net); +int wifi_is_accesspoint(struct lyd_node *cif); +bool wifi_ap_must_delete(struct lyd_node *dif); +struct lyd_node *wifi_ap_get_radio(struct lyd_node *cif); /* if-gre.c */ int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); diff --git a/src/confd/yang/confd/infix-if-type.yang b/src/confd/yang/confd/infix-if-type.yang index 8e2ea0d6f..73930fcbb 100644 --- a/src/confd/yang/confd/infix-if-type.yang +++ b/src/confd/yang/confd/infix-if-type.yang @@ -110,6 +110,12 @@ module infix-if-type { if-feature wifi; base infix-interface-type; base ianaift:ieee80211; - description "WiFi interface"; + description "WiFi radio interface for station connections"; + } + identity wifi-ap { + if-feature wifi; + base infix-interface-type; + base ianaift:ieee80211; + description "WiFi Access Point interface"; } } diff --git a/src/confd/yang/confd/infix-if-wifi.yang b/src/confd/yang/confd/infix-if-wifi.yang index 85a3ba34b..df3192bb5 100644 --- a/src/confd/yang/confd/infix-if-wifi.yang +++ b/src/confd/yang/confd/infix-if-wifi.yang @@ -31,26 +31,39 @@ submodule infix-if-wifi { "WiFi-specific extensions to the standard IETF interfaces model. This submodule defines configuration and operational data relevant to - WiFi interfaces, including security settings, network + WiFi radio interfaces, including security settings, network discovery, and regulatory compliance. - It supports WiFi client mode and enables comprehensive management of + It supports WiFi station mode and enables comprehensive management of wireless connections, including encryption, country codes, and scanning."; + revision 2025-09-26 { + description "Refactor module and add support for Accesspoint mode."; + reference "internal"; + } revision 2025-05-27 { description "Initial revision."; reference "internal"; } feature wifi { - description "WiFi support is an optional build-time feature in Infix."; + description + "WiFi support is an optional build-time feature in Infix."; } + typedef mode { + type enumeration { + enum station { + } + enum accesspoint { + } + } + } typedef encryption { type enumeration { - enum auto { + enum mixed-wpa2-wpa3 { description - "Enables WPA/WPA2/WPA3 encryption with automatic protocol + "Enables WPA2/WPA3 mixed mode encryption with automatic protocol negotiation. The system uses the strongest supported variant supported by Access Point."; } enum disabled { @@ -64,45 +77,135 @@ submodule infix-if-wifi { description "Encryption modes available for WiFi connections. - - auto: Secure connection using WPA3/WPA2/WPA (auto-selected) + - mixed-wpa2-wpa3: Secure connection using WPA2/WPA3 (auto-selected) - disabled: Open network (unencrypted)"; } + typedef band { + type enumeration { + enum "2.4GHz" { + description "2.4 GHz frequency band."; + } + enum "5GHz" { + description "5 GHz frequency band."; + } + } + description "WiFi frequency bands."; + } + + typedef channel { + type union { + type enumeration { + enum "auto" { + description "Automatic channel selection (recommended)."; + } + } + type uint8 { + range "1..14 | 36 | 40 | 44 | 48 | 149 | 153 | 157 | 161 | 165"; + } + } + description "WiFi channel: 'auto' for automatic selection or specific channel number."; + } augment "/if:interfaces/if:interface" { - when "derived-from-or-self(if:type, 'infixift:wifi')" { + when "derived-from-or-self(if:type, 'infixift:wifi') or derived-from-or-self(if:type, 'infixift:wifi-ap')" { description - "Applies only to interfaces of type 'wifi'."; + "Applies to interfaces of type 'wifi' or 'wifi-ap'."; } + description + "WiFi interface extensions with MAC address validation for AP interfaces."; + container wifi { if-feature wifi; presence "Configure Wi-Fi settings"; description - "WiFi-specific configuration and operational data."; + "WiFi radio configuration and operational data."; + + leaf radio { + when "derived-from-or-self(../../if:type, 'infixift:wifi-ap')" { + description "Only available for wifi-ap interfaces."; + } + type if:interface-ref; + must "derived-from-or-self(/if:interfaces/if:interface[if:name=current()]/if:type, 'infixift:wifi')" { + error-message "Referenced interface must be of type 'wifi'."; + } + must "/if:interfaces/if:interface[if:name=current()]/infix-if:wifi/mode = 'accesspoint'" { + error-message "Referenced wifi interface must be in accesspoint mode."; + } + mandatory true; + description + "Reference to the underlying WiFi radio interface. + + Example: 'wifi0' - the radio interface this AP uses."; + } leaf country-code { type iwcc:country-code; - mandatory true; + default '00'; description "Two-letter ISO 3166-1 country code for regulatory compliance. - Examples: 'US', 'DE', 'JP'. + Examples: 'SE', 'US', 'DE', 'JP'. WARNING: Incorrect values may violate local laws."; + } + + leaf mode { + type mode; + must "not(. = 'accesspoint' and derived-from-or-self(../../if:type, 'infixift:wifi')) or ../band" { + error-message "Band must be specified when mode is accesspoint for wifi interfaces."; + } + description + "WiFi interface operating mode. + + - station: Connect to external WiFi networks + - accesspoint: Operate as WiFi access point radio"; + } + leaf band { + type band; + when "derived-from-or-self(../../if:type, 'infixift:wifi') and ../mode = 'accesspoint'" { + description "Only available for wifi interfaces in accesspoint mode."; + } + description + "WiFi frequency band for radio operation. + If not specified, radio supports all available bands."; + } + leaf channel { + type channel; + default "auto"; + when "derived-from-or-self(../../if:type, 'infixift:wifi') and ../mode = 'accesspoint'" { + description "Only available for wifi interfaces in accesspoint mode."; + } + must ". = 'auto' or not(../band) or " + + "(../band = '2.4GHz' and . >= 1 and . <= 14) or " + + "(../band = '5GHz' and (. = 36 or . = 40 or . = 44 or . = 48 or . >= 149 and . <= 165))" { + error-message "Channel must be valid for selected band or use 'auto'."; + } + description + "WiFi channel selection for radio. + 'auto' (default) - automatic channel selection + Manual selection available for common channels."; } leaf encryption { - default auto; + default mixed-wpa2-wpa3; type encryption; - + when "(derived-from-or-self(../../if:type, 'infixift:wifi') and ../mode = 'station') or derived-from-or-self(../../if:type, 'infixift:wifi-ap')" { + description "Available for wifi interfaces in station mode or wifi-ap interfaces."; + } description "WiFi encryption method. - - auto (default): Enables WPA2/WPA3 auto-negotiation + For station interfaces: + - mixed-wpa2-wpa3 (default): Enables WPA2/WPA3 auto-negotiation + - disabled: Disables encryption (open network) + + For AP interfaces: + - mixed-wpa2-wpa3: Enables WPA2/WPA3 encryption - disabled: Disables encryption (open network)"; } @@ -110,24 +213,27 @@ submodule infix-if-wifi { type string { length "1..32"; } - mandatory true; - + when "(derived-from-or-self(../../if:type, 'infixift:wifi') and ../mode = 'station') or derived-from-or-self(../../if:type, 'infixift:wifi-ap')" { + description "Available for wifi interfaces in station mode or wifi-ap interfaces."; + } description "WiFi network name (SSID). - Case-sensitive, must match the target network. + For station interfaces: SSID to connect to (must match target network) + For AP interfaces: SSID to broadcast - Length: 1–32 characters."; + Case-sensitive. Length: 1–32 characters."; } leaf secret { type ks:symmetric-key-ref; - mandatory true; + when "(derived-from-or-self(../../if:type, 'infixift:wifi') and ../mode = 'station') or derived-from-or-self(../../if:type, 'infixift:wifi-ap')" { + description "Available for wifi interfaces in station mode or wifi-ap interfaces."; + } must "../encryption != 'disabled'" { error-message "Pre-shared key required unless encryption is disabled."; } - description "Pre-shared key (PSK) for WPA-secured networks."; } @@ -137,7 +243,7 @@ submodule infix-if-wifi { type int16; units "dBm"; description - "Current received signal strength (RSSI) in dBm. + "Current received signal strength (RSSI) in dBm for station connection. Lower (more negative) values indicate stronger signals."; } @@ -180,6 +286,47 @@ submodule infix-if-wifi { "Human-readable description of the detected security."; } } + + list connected-stations { + when "derived-from-or-self(../../../if:type, 'infixift:wifi-ap')" { + description "Only available for wifi-ap interfaces."; + } + config false; + key mac-address; + description + "List of stations currently connected to this access point."; + + leaf mac-address { + type yang:phys-address; + description + "MAC address of connected station."; + } + + leaf rssi { + type int16; + units "dBm"; + description + "Signal strength for connected station."; + } + + leaf tx-speed { + type string; + description + "Transmit bitrate (e.g., '65.0 Mbps', '1.2 Gbps')."; + } + + leaf rx-speed { + type string; + description + "Receive bitrate (e.g., '65.0 Mbps', '1.2 Gbps')."; + } + + leaf connected-time { + type string; + description + "Time since station connected (e.g., '120 seconds', '5 minutes')."; + } + } } } } diff --git a/src/confd/yang/confd/infix-if-wifi@2025-05-27.yang b/src/confd/yang/confd/infix-if-wifi@2025-09-26.yang similarity index 100% rename from src/confd/yang/confd/infix-if-wifi@2025-05-27.yang rename to src/confd/yang/confd/infix-if-wifi@2025-09-26.yang diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index 9fea0434e..e78926703 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -183,6 +183,11 @@ module infix-interfaces { base infix-ift:infix-interface-type; } } + deviate add { + must "not(derived-from-or-self(., 'infix-ift:wifi-ap')) or (../infix-if:custom-phys-address/infix-if:static or ../infix-if:custom-phys-address/infix-if:chassis)" { + error-message "WiFi AP interfaces must have a custom physical address configured."; + } + } } deviation "/if:interfaces/if:interface/if:name" { diff --git a/src/confd/yang/confd/infix-wifi-country-codes.yang b/src/confd/yang/confd/infix-wifi-country-codes.yang index 58bee7701..4bb7569a4 100644 --- a/src/confd/yang/confd/infix-wifi-country-codes.yang +++ b/src/confd/yang/confd/infix-wifi-country-codes.yang @@ -17,6 +17,12 @@ module infix-wifi-country-codes { The regulatory domain configuration follows the principles established in IETF RFCs for wireless access point management."; + revision 2025-09-15 { + description + "Added missing 00 - World (Global regulatory domain)"; + reference + "internal"; + } revision 2025-06-02 { description "Initial revision for WiFi country code support."; @@ -27,6 +33,7 @@ module infix-wifi-country-codes { typedef country-code { type enumeration { + enum "00" { description "World (Global regulatory domain)"; } enum "AD" { description "Andorra"; } enum "AE" { description "United Arab Emirates"; } enum "AF" { description "Afghanistan"; } diff --git a/src/confd/yang/confd/infix-wifi-country-codes@2025-06-02.yang b/src/confd/yang/confd/infix-wifi-country-codes@2025-09-15.yang similarity index 100% rename from src/confd/yang/confd/infix-wifi-country-codes@2025-06-02.yang rename to src/confd/yang/confd/infix-wifi-country-codes@2025-09-15.yang diff --git a/src/statd/python/cli_pretty/cli_pretty.py b/src/statd/python/cli_pretty/cli_pretty.py index ff617c950..98b786a7d 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -201,6 +201,13 @@ class PadWifiScan: encryption = 30 signal = 9 +class PadWifiStations: + mac = 20 + signal = 9 + tx_speed = 11 + rx_speed = 11 + connected_time = 16 + class PadLldp: interface = 16 @@ -334,15 +341,15 @@ def title(txt, len=None, bold=True): print(txt) -def rssi_to_status(rssi): +def rssi_to_status(rssi, width=0): if rssi <= -75: - status = Decore.bright_green("excellent") + status = Decore.bright_green(f"{'excellent':<{width}}" if width else "excellent") elif rssi <= -65: - status = Decore.green("good") + status = Decore.green(f"{'good':<{width}}" if width else "good") elif rssi <= -50: - status = Decore.yellow("poor") + status = Decore.yellow(f"{'poor':<{width}}" if width else "poor") else: - status = Decore.red("bad") + status = Decore.red(f"{'bad':<{width}}" if width else "bad") return status @@ -868,7 +875,7 @@ def __init__(self, data): self.lower_if = '' def is_wifi(self): - return self.type == "infix-if-type:wifi" + return self.type == "infix-if-type:wifi" or self.type == "infix-if-type:wifi-ap" def is_vlan(self): return self.type == "infix-if-type:vlan" @@ -965,13 +972,36 @@ def pr_proto_loopack(self, pipe=''): row = self._pr_proto_common("loopback", False, pipe); print(row) - def pr_wifi_ssids(self): - hdr = (f"{'SSID':<{PadWifiScan.ssid}}" - f"{'ENCRYPTION':<{PadWifiScan.encryption}}" - f"{'SIGNAL':<{PadWifiScan.signal}}") + def pr_wifi_stations(self): + hdr = "\nCONNECTED STATIONS" + print(Decore.invert(hdr)) + hdr = (f"{'MAC':<{PadWifiStations.mac}}" + f"{'SIGNAL':<{PadWifiStations.signal}}" + f"{'TX speed':<{PadWifiStations.tx_speed}}" + f"{'RX speed':<{PadWifiStations.rx_speed}}" + f"{'Connected':<{PadWifiStations.connected_time}}" + ) + print(Decore.invert(hdr)) + stations=self.wifi.get("connected-stations", {}) + for station in stations: + status=rssi_to_status(station["rssi"], PadWifiStations.signal) + row = f"{station['mac-address']:<{PadWifiStations.mac}}" + row += status + row += f"{station['tx-speed']:<{PadWifiStations.tx_speed}}" + row += f"{station['rx-speed']:<{PadWifiStations.rx_speed}}" + row += f"{station['connected-time']:<{PadWifiStations.connected_time}}" + print(row) + + def pr_wifi_ssids(self): + hdr = "\nSCAN RESULTS" print(Decore.invert(hdr)) - results = self.wifi.get("scan-results", {}) + hdr = (f"{'SSID':<{PadWifiScan.ssid}}" + f"{'ENCRYPTION':<{PadWifiScan.encryption}}" + f"{'SIGNAL':<{PadWifiScan.signal}}") + print(Decore.invert(hdr)) + + results=self.wifi.get("scan-results", {}) for result in results: encstr = ", ".join(result["encryption"]) status = rssi_to_status(result["rssi"]) @@ -987,22 +1017,21 @@ def pr_proto_wifi(self, pipe=''): print(row) ssid = None rssi = None - + status_str="" if self.wifi: - rssi=self.wifi.get("rssi") - ssid=self.wifi.get("ssid") - if ssid is None: - ssid="------" - - if rssi is None: - signal="------" - else: - signal=rssi_to_status(rssi) - data_str = f"ssid: {ssid}, signal: {signal}" - + if self.wifi.get("mode", "") == "client": + ssid=self.wifi.get("active-ssid") + if ssid is not None: + rssi=self.wifi.get("active-rssi") + signal=rssi_to_status(rssi) + + status_str = f"ssid: {ssid}, signal: {signal}" + elif self.wifi.get("mode", "") == "accesspoint": + stations=self.wifi.get("connected-stations", {}) + status_str = f"Connected stations: {len(stations)}" row = f"{'':<{Pad.iface}}" row += f"{'wifi':<{Pad.proto}}" - row += f"{'':<{Pad.state}}{data_str}" + row += f"{'':<{Pad.state}}{status_str}" print(row) def pr_proto_br(self, br_vlans): @@ -1260,14 +1289,6 @@ def pr_iface(self): else: print(f"{'ipv6 addresses':<{20}}:") - if self.wifi: - ssid=self.wifi.get('ssid', "----") - rssi=self.wifi.get('rssi', "----") - print(f"{'SSID':<{20}}: {ssid}") - print(f"{'Signal':<{20}}: {rssi}") - print("") - self.pr_wifi_ssids() - if self.gre: print(f"{'local address':<{20}}: {self.gre['local']}") print(f"{'remote address':<{20}}: {self.gre['remote']}") @@ -1288,6 +1309,18 @@ def pr_iface(self): for key, val in frame.items(): key = remove_yang_prefix(key) print(f"eth-{key:<{25}}: {val}") + if self.wifi: + mode=self.wifi.get('mode') + if mode == "client": + ssid=self.wifi.get('active-ssid', "") + rssi=self.wifi.get('active-rssi', "") + print(f"{'SSID':<{20}}: {ssid}") + print(f"{'Signal':<{20}}: {rssi}") + print("") + self.pr_wifi_ssids() + if mode == "accesspoint": + self.pr_wifi_stations() + def pr_mdb(self, bridge): for group in self.br_mdb.get("multicast-filter", {}): diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index fd7c5d8c7..710d469d3 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -35,6 +35,10 @@ def iplink2yang_type(iplink): case "ether": data = HOST.run(tuple(f"ls /sys/class/net/{ifname}/wireless/".split()), default="no") if data != "no": + iw_data=HOST.run(tuple(f"iw dev {ifname} info".split()), default="") + for line in iw_data.splitlines(): + if line.strip() == "type AP": + return "infix-if-type:wifi-ap" return "infix-if-type:wifi" case _: return "infix-if-type:other" @@ -138,7 +142,7 @@ def interface(iplink, ipaddr): case "infix-if-type:vlan": if v := vlan.vlan(iplink): interface["infix-interfaces:vlan"] = v - case "infix-if-type:wifi": + case "infix-if-type:wifi-ap" | "infix-if-type:wifi": if w := wifi.wifi(iplink["ifname"]): interface["infix-interfaces:wifi"] = w diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index b220f9df8..0e0ccd364 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -6,52 +6,41 @@ def wifi(ifname): wifi_data={} try: - data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="") - - if data != "": - for line in data.splitlines(): - try: - if "=" not in line: - continue - k,v = line.split("=", 1) - if k == "ssid": - wifi_data["ssid"] = v - if k == "wpa_state" and v == "DISCONNECTED": # wpa_suppicant has most likely restarted, restart scanning - HOST.run(tuple(f"wpa_cli -i {ifname} scan".split()), default="") - except ValueError: - # Skip malformed lines - continue - - try: - data=HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL") - - # signal_poll return FAIL not connected - if data.strip() != "FAIL": - for line in data.splitlines(): - try: - if "=" not in line: - continue - k,v = line.strip().split("=", 1) + iw_data=HOST.run(tuple(f"iw dev {ifname} info".split()), default="") + if iw_data != "": + for line in iw_data.splitlines(): + line=line.strip() # Fix crazy output from iw. + if line == "type AP": + wifi_data["mode"] = "accesspoint" + break + else: + wifi_data["mode"] = "station" + + if wifi_data["mode"] == "station": + client_data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="") + if client_data != "": + for line in client_data.splitlines(): + k,v = line.split("=") + if k == "ssid": + wifi_data["active-ssid"] = v + + data=HOST.run(tuple(f"wpa_cli -i {ifname} signal_poll".split()), default="FAIL") + # signal_poll return FAIL not connected + if data.strip() != "FAIL": + for line in data.splitlines(): + k,v = line.strip().split("=") if k == "RSSI": - wifi_data["rssi"]=int(v) - except (ValueError, KeyError): - # Skip malformed lines or invalid integers - continue - except Exception: - # If signal_poll fails, continue without RSSI - pass + wifi_data["active-rssi"]=int(v) + data=HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL") + if data != "FAIL": + wifi_data["scan-results"] = parse_wpa_scan_result(data) + elif wifi_data["mode"] == "accesspoint": + stations=HOST.run_json(tuple(f"/usr/libexec/infix/wifi-ap-stations {ifname}".split()), default=[]) + wifi_data["connected-stations"] = stations except Exception: # If status query fails entirely, continue with scan results pass - try: - data=HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL") - if data != "FAIL": - wifi_data["scan-results"] = parse_wpa_scan_result(data) - except Exception: - # If scan results fail, just omit them - pass - return wifi_data