diff --git a/board/common/qemu/qemu.sh b/board/common/qemu/qemu.sh index 0de195da3..28481473c 100755 --- a/board/common/qemu/qemu.sh +++ b/board/common/qemu/qemu.sh @@ -333,6 +333,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..933ec6743 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,2 @@ -SUBSYSTEM=="net", ACTION=="add", TEST=="/sys/class/net/$name/wireless", NAME="wifi%n" +# Only rename physical interfaces, skip virtual ones created by hostapd +SUBSYSTEM=="net", ACTION=="add", TEST=="/sys/class/net/$name/wireless", TEST=="/sys/class/net/$name/device", KERNEL!="*_*", 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..5c0d20f97 --- /dev/null +++ b/board/common/rootfs/usr/libexec/infix/wifi-ap-stations @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +import sys +import subprocess +import json +import re +import os + +def check_interface_exists(interface): + """Check if the network interface exists""" + try: + result = subprocess.run(['ip', 'link', 'show', interface], + capture_output=True, check=True) + return True + except subprocess.CalledProcessError: + return False + +def parse_iw_station_dump(interface): + """Parse iw station dump output and return JSON""" + try: + result = subprocess.run(['sudo', 'iw', 'dev', interface, 'station', 'dump'], + capture_output=True, text=True, check=True) + output = result.stdout + except subprocess.CalledProcessError as e: + print(f"Error running iw command: {e}", file=sys.stderr) + return [] + except FileNotFoundError: + print("Error: 'iw' command not found", file=sys.stderr) + return [] + + stations = [] + current_station = {} + + for line in output.split('\n'): + line = line.strip() + + if line.startswith('Station'): + if current_station: + stations.append(current_station) + + mac_match = re.search(r'Station ([a-fA-F0-9:]{17})', line) + current_station = { + 'mac-address': mac_match.group(1) if mac_match else 'unknown', + 'tx-speed': 'unknown', + 'rx-speed': 'unknown', + 'rssi': 0, + 'connected-time': 'unknown' + } + + elif 'tx bitrate:' in line: + bitrate_match = re.search(r'tx bitrate:\s*(\d+\.?\d*)\s*(MBit/s|Gbit/s)', line) + if bitrate_match: + speed = bitrate_match.group(1) + unit = bitrate_match.group(2) + current_station['tx-speed'] = f"{speed} {unit.replace('Bit/s', 'bps')}" + + elif 'rx bitrate:' in line: + bitrate_match = re.search(r'rx bitrate:\s*(\d+\.?\d*)\s*(MBit/s|Gbit/s)', line) + if bitrate_match: + speed = bitrate_match.group(1) + unit = bitrate_match.group(2) + current_station['rx-speed'] = f"{speed} {unit.replace('Bit/s', 'bps')}" + + elif 'signal:' in line and 'avg' not in line: + signal_match = re.search(r'signal:\s*(-?\d+)', line) + if signal_match: + current_station['rssi'] = int(f"{signal_match.group(1)}") + + elif 'connected time:' in line: + time_match = re.search(r'connected time:\s*(\d+\s+\w+)', line) + if time_match: + current_station['connected-time'] = time_match.group(1) + + if current_station: + stations.append(current_station) + + return stations + +def main(): + if len(sys.argv) != 2: + print("Usage: python3 wifi_station_parser.py ") + print("Example: python3 wifi_station_parser.py wifi0_ap2") + sys.exit(1) + + interface = sys.argv[1] + + if not check_interface_exists(interface): + print(f"Error: Interface '{interface}' not found", file=sys.stderr) + sys.exit(1) + + stations = parse_iw_station_dump(interface) + print(json.dumps(stations, indent=2)) + +if __name__ == "__main__": + main() 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/src/board/raspberry-pi-4/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg b/src/board/raspberry-pi-4/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg index 8ebc10a63..bfcc98299 100644 --- a/src/board/raspberry-pi-4/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg +++ b/src/board/raspberry-pi-4/rootfs/usr/share/product/raspberrypi,4-model-b/etc/factory-config.cfg @@ -41,7 +41,8 @@ }, { "name": "wifi0", - "type": "infix-if-type:wifi" + "type": "infix-if-type:wifi", + "infix-interfaces:wifi": {} } ] }, diff --git a/src/confd/src/ietf-interfaces.c b/src/confd/src/ietf-interfaces.c index 28045ff05..dc9e7dca2 100644 --- a/src/confd/src/ietf-interfaces.c +++ b/src/confd/src/ietf-interfaces.c @@ -425,6 +425,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) || wifi_gen(NULL, wifi_ap_get_radio(cif), net); case IFT_ETH: return netdag_gen_ethtool(net, cif, dif); case IFT_LO: @@ -455,6 +457,11 @@ 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: { + struct lyd_node *radio_if = wifi_ap_get_radio(cif); + return wifi_gen(NULL, radio_if, net); + return 0; + } case IFT_DUMMY: case IFT_GRE: case IFT_GRETAP: @@ -483,6 +490,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"); case IFT_GRE: case IFT_GRETAP: @@ -572,9 +581,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, net); + break; case IFT_VETH: veth_gen_del(dif, ip); break; + case IFT_BRIDGE: case IFT_DUMMY: case IFT_GRE: @@ -615,7 +628,6 @@ 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; @@ -744,6 +756,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: @@ -897,18 +910,20 @@ static int keystorecb(sr_session_ctx_t *session, uint32_t sub_id, const char *mo LYX_LIST_FOR_EACH(interfaces, interface, "interface") { const char *name; - - if (iftype_from_iface(interface) != IFT_WIFI) - continue; - - wifi = lydx_get_child(interface, "wifi"); - if (!wifi) - continue; - - name = lydx_get_cattr(wifi, "secret"); - if (!name || strcmp(name, secret_name)) + enum iftype itype = iftype_from_iface(interface); + + if (itype == IFT_WIFI) { + wifi = lydx_get_child(interface, "wifi"); + if (!wifi) + continue; + + name = lydx_get_cattr(wifi, "secret"); + if (!name || strcmp(name, secret_name)) + continue; + wifi_station_gen(interface, &confd->netdag); + } else { continue; - wifi_gen(NULL, interface, &confd->netdag); + } } } diff --git a/src/confd/src/ietf-interfaces.h b/src/confd/src/ietf-interfaces.h index 81892ff26..5c3cb9450 100644 --- a/src/confd/src/ietf-interfaces.h +++ b/src/confd/src/ietf-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") \ @@ -122,7 +123,14 @@ int bridge_port_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); /* infix-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 *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); /* infix-if-gre.c */ int gre_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); diff --git a/src/confd/src/infix-if-wifi.c b/src/confd/src/infix-if-wifi.c index 2ef7bfd2b..aa48ab0cd 100644 --- a/src/confd/src/infix-if-wifi.c +++ b/src/confd/src/infix-if-wifi.c @@ -5,14 +5,27 @@ #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) +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)) { + if (!secret && (ssid && encryption)) { /* Not an error, updated from two ways, interface cb and keystore cb. */ return 0; } @@ -38,7 +51,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 +62,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 +82,450 @@ 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; + 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) +{ + 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"); + if (!cwifi) + return SR_ERR_OK; + cmode = lydx_get_child(cwifi, "mode"); + ERROR("NOW GENERATE WIFI"); + 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")) { + ERROR("INTERFACE DISSABLED"); + 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")) { + ERROR("Should now configure AP"); + if (dmode && strcmp(lyd_get_value(dmode), "accesspoint")) + disable_wifi_station(ifname, fp); + wifi_ap_gen(cif, net); + + } else { + ERROR("Should now configure station"); + 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("GENERATE STATION"); 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++; - if (!enabled) - return wifi_gen_del(cif, net); +out: + fclose(fp); + return SR_ERR_OK; +} + +int wifi_ap_del_iface(struct lyd_node *cif,struct dagger *net) +{ + const char *ifname, *radio; + struct lyd_node *wifi; + struct ly_set *remaining_aps = NULL; + bool is_last_ap = false; + FILE *iw; + int rc; + + ifname = lydx_get_cattr(cif, "name"); + wifi = lydx_get_child(cif, "wifi"); - return wifi_gen_config(ifname, ssid, country, secret, encryption, net); + if (wifi) { + radio = lydx_get_cattr(wifi, "radio"); + + /* Check if this is the last AP interface for this radio */ + if (radio) { + rc = lyd_find_xpath(cif, "../interface[derived-from-or-self(type, 'infix-if-type:wifi-ap') and wifi/radio = current()/wifi/radio and name != current()/name]", &remaining_aps); + if (rc != LY_SUCCESS || !remaining_aps || remaining_aps->count == 0) { + is_last_ap = true; + ERROR("Interface %s is the last AP for radio %s - will restore radio name", ifname, radio); + } else { + ERROR("Interface %s removal leaves %d other APs for radio %s", ifname, remaining_aps->count, radio); + } + + if (remaining_aps) + ly_set_free(remaining_aps, NULL); + } + } + + iw = dagger_fopen_net_exit(net, ifname, NETDAG_EXIT, "exit-iw.sh"); + + if (is_last_ap && radio) { + /* Last AP interface: restore original radio name */ + fprintf(iw, "# Restore radio name from %s back to %s\n", ifname, radio); + fprintf(iw, "ip link set dev %s name %s\n", ifname, radio); + fprintf(iw, "ip link property del dev %s altname %s\n", radio, radio); + } else { + /* Virtual AP interface: delete it */ + fprintf(iw, "# Delete 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 *existing_aps = NULL; + bool is_first_ap = false; + 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; + } + + /* Check if this is the first AP interface for this radio by finding all APs for this radio */ + rc = lyd_find_xpath(cif, "../interface[derived-from-or-self(type, 'infix-if-type:wifi-ap') and wifi/radio = current()/wifi/radio]", &existing_aps); + if (rc == LY_SUCCESS && existing_aps && existing_aps->count > 0) { + /* Check if current interface is the first one in the list */ + const char *first_ap_name = lydx_get_cattr(existing_aps->dnodes[0], "name"); + if (!strcmp(ifname, first_ap_name)) { + is_first_ap = true; + ERROR("Interface %s is the first AP for radio %s - will rename radio", ifname, radio); + } else { + ERROR("Interface %s is additional AP for radio %s - will create virtual interface", ifname, radio); + } + } else { + /* Fallback: if we can't determine, assume first */ + is_first_ap = true; + ERROR("Interface %s assumed to be first AP for radio %s", ifname, radio); + } + + if (existing_aps) + ly_set_free(existing_aps, NULL); + + dagger_add_dep(&confd.netdag, ifname, radio); + iw = dagger_fopen_net_init(net, ifname, NETDAG_INIT_PRE, "init-iw.sh"); + + if (is_first_ap) { + /* First AP interface: rename radio interface to AP name */ + fprintf(iw, "# Rename radio %s to first AP interface %s\n", radio, ifname); + 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 { + /* Additional AP interfaces: create virtual interface as before */ + fprintf(iw, "# Create virtual AP interface %s on radio %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; + + 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("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"; + + /* 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; + } + + /* The first AP interface becomes the main interface (radio gets renamed to this) */ + ap_interface = ap_interfaces->dnodes[0]; + main_interface_name = lydx_get_cattr(ap_interface, "name"); + + ERROR("Generating hostapd config for radio %s, main interface %s with %d total APs", + ifname, ifname, 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 first AP interface as main SSID */ + 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 additional AP interfaces as BSS entries */ + for (uint32_t i = 1; i < ap_interfaces->count; 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; + + + /* 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; + + 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/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..3b9b0977b 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,132 @@ 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'."; + } + 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; + 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."; + } + must "not(derived-from-or-self(../../if:type, 'infixift:wifi') and ../mode = 'accesspoint') or ." { + error-message "Band is mandatory 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 +210,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 +240,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 +283,29 @@ 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."; + } + } } } } 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 592d23183..57dfc5fc4 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -134,6 +134,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 232818ce3..bbf882438 100755 --- a/src/statd/python/cli_pretty/cli_pretty.py +++ b/src/statd/python/cli_pretty/cli_pretty.py @@ -87,6 +87,10 @@ class PadWifiScan: encryption = 30 signal = 9 +class PadWifiStations: + mac = 20 + signal = 9 + class PadLldp: interface = 16 @@ -651,13 +655,30 @@ def pr_proto_loopack(self, pipe=''): row = self._pr_proto_common("loopback", False, pipe); print(row) + def pr_wifi_stations(self): + hdr = "\nCONNECTED STATIONS" + print(Decore.invert(hdr)) + hdr = (f"{'MAC':<{PadWifiStations.mac}}" + f"{'SIGNAL':<{PadWifiStations.signal}}" + ) + print(Decore.invert(hdr)) + + stations=self.wifi.get("connected-stations", {}) + for station in stations: + status=rssi_to_status(station["rssi"]) + row = f"{station['mac-address']:<{PadWifiStations.mac}}" + row += f"{status:<{PadWifiStations.signal}}" + print(row) + def pr_wifi_ssids(self): + hdr = "\nSCAN RESULTS" + print(Decore.invert(hdr)) 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"]) @@ -674,22 +695,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): @@ -947,14 +967,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']}") @@ -975,6 +987,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: + ssid=self.wifi.get('active-ssid', "") + rssi=self.wifi.get('active-rssi', "") + mode=self.wifi.get('mode') + if mode == "client": + 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/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index d373dd5cc..b010bfbb9 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -3,30 +3,41 @@ import re def wifi(ifname): - data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="") + iw_data=HOST.run(tuple(f"iw dev {ifname} info".split()), default="") wifi_data={} - if data != "": - for line in data.splitlines(): - k,v = line.split("=") - 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="") - - 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) - 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) - + if iw_data != "": + for line in iw_data.splitlines(): + line=line.strip() # Fix crazy output from iw. + l=line.split(" ") + if l[0] == "type": + if l[1] == "AP": + wifi_data["mode"] = "accesspoint" + else: + wifi_data["mode"] = "station" + + if wifi_data["mode"] == "station": + station_data=HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="") + if station_data != "": + for line in station_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["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 return wifi_data