diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index 47e7db128..737f9bc7c 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -286,6 +286,73 @@ def parse_interface_info(ifname): return result +def parse_stations(ifname): + """ + Parse 'iw dev station dump' output + Returns: list of connected stations with stats + """ + output = run_iw('dev', ifname, 'station', 'dump') + if not output: + return [] + + stations = [] + current = None + + for line in output.splitlines(): + stripped = line.strip() + + # New station entry: "Station aa:bb:cc:dd:ee:ff (on wifiX)" + if stripped.startswith('Station '): + if current: + stations.append(current) + parts = stripped.split() + if len(parts) >= 2: + current = {'mac-address': parts[1].lower()} + else: + current = None + continue + + if not current or ':' not in stripped: + continue + + key, _, value = stripped.partition(':') + key = key.strip() + value = value.strip() + + try: + if key == 'signal': + # Format: "-42 dBm" or "-42 [-44, -45] dBm" + current['rssi'] = int(value.split()[0]) + elif key == 'connected time': + # Format: "123 seconds" + current['connected-time'] = int(value.split()[0]) + elif key == 'rx bytes': + current['rx-bytes'] = value # counter64: string-encoded + elif key == 'tx bytes': + current['tx-bytes'] = value # counter64: string-encoded + elif key == 'rx packets': + current['rx-packets'] = value # counter64: string-encoded + elif key == 'tx packets': + current['tx-packets'] = value # counter64: string-encoded + elif key == 'tx bitrate': + # Format: "866.7 MBit/s ..." - convert to 100kbit/s units + speed_mbps = float(value.split()[0]) + current['tx-speed'] = int(speed_mbps * 10) + elif key == 'rx bitrate': + speed_mbps = float(value.split()[0]) + current['rx-speed'] = int(speed_mbps * 10) + elif key == 'inactive time': + # Format: "1234 ms" + current['inactive-time'] = int(value.split()[0]) + except (ValueError, IndexError): + continue + + if current: + stations.append(current) + + return stations + + def parse_survey(ifname): """ Parse 'iw dev survey dump' output @@ -396,6 +463,66 @@ def parse_dev(): return result +def parse_link(ifname): + """ + Parse 'iw dev link' output for station mode + Returns: {connected, bssid, ssid, frequency, signal, tx_bitrate, rx_bitrate} + """ + output = run_iw('dev', ifname, 'link') + if not output: + return {'connected': False} + + if 'Not connected' in output: + return {'connected': False} + + result = {'connected': True} + + for line in output.splitlines(): + stripped = line.strip() + + # Connected to aa:bb:cc:dd:ee:ff + if stripped.startswith('Connected to '): + parts = stripped.split() + if len(parts) >= 3: + result['bssid'] = parts[2].lower() + + # SSID: NetworkName + elif stripped.startswith('SSID: '): + result['ssid'] = stripped[6:] + + # freq: 5180 + elif stripped.startswith('freq: '): + try: + result['frequency'] = int(stripped[6:]) + except ValueError: + pass + + # signal: -42 dBm + elif stripped.startswith('signal: '): + try: + result['rssi'] = int(stripped.split()[1]) + except (ValueError, IndexError): + pass + + # tx bitrate: 866.7 MBit/s ... + elif stripped.startswith('tx bitrate: '): + try: + speed = float(stripped.split()[2]) + result['tx-speed'] = int(speed * 10) # 100kbit/s units + except (ValueError, IndexError): + pass + + # rx bitrate: 780.0 MBit/s ... + elif stripped.startswith('rx bitrate: '): + try: + speed = float(stripped.split()[2]) + result['rx-speed'] = int(speed * 10) + except (ValueError, IndexError): + pass + + return result + + def main(): if len(sys.argv) < 2: print(json.dumps({ @@ -404,14 +531,17 @@ def main(): 'list': 'List all PHY devices', 'dev': 'List all interfaces grouped by PHY', 'info': 'Get PHY or interface information (requires device)', - 'survey': 'Get channel survey data (requires interface name)' + 'survey': 'Get channel survey data (requires interface)', + 'station': 'Get connected stations in AP mode (requires interface)', + 'link': 'Get link info in station mode (requires interface)' }, 'examples': [ 'iw.py list', 'iw.py dev', 'iw.py info radio0', - 'iw.py info phy4', 'iw.py info wlan0', + 'iw.py station wifi0', + 'iw.py link wlan0', 'iw.py survey wlan0' ] }, indent=2)) @@ -434,12 +564,21 @@ def main(): data = parse_phy_info(device) else: data = parse_interface_info(device) + elif command == 'station': + if len(sys.argv) < 3: + data = {'error': 'station command requires interface argument'} + else: + data = parse_stations(sys.argv[2]) + elif command == 'link': + if len(sys.argv) < 3: + data = {'error': 'link command requires interface argument'} + else: + data = parse_link(sys.argv[2]) elif command == 'survey': if len(sys.argv) < 3: - data = {'error': 'survey command requires device argument'} + data = {'error': 'survey command requires interface argument'} else: - device = sys.argv[2] - data = parse_survey(device) + data = parse_survey(sys.argv[2]) else: data = {'error': f'Unknown command: {command}'} diff --git a/doc/wifi.md b/doc/wifi.md index 9cf1e07aa..e01d85214 100644 --- a/doc/wifi.md +++ b/doc/wifi.md @@ -84,46 +84,27 @@ admin@example:/config/hardware/component/radio0/wifi-radio/> leave **Key radio parameters:** - `country-code`: Two-letter ISO 3166-1 code - determines allowed channels and maximum power. Examples: US, DE, GB, SE, FR, JP. **Must match your physical location for legal compliance.** -- `band`: 2.4GHz, 5GHz, or 6GHz (required for AP mode). Band selection automatically enables appropriate WiFi standards (2.4GHz: 802.11n, 5GHz: 802.11n/ac, 6GHz: 802.11n/ac/ax) +- `band`: 2.4GHz, 5GHz, or 6GHz (required for AP mode). Automatically enables appropriate WiFi standards (2.4GHz: 802.11n/ax, 5GHz: 802.11n/ac/ax, 6GHz: 802.11ax) - `channel`: Channel number (1-196) or "auto" (required for AP mode). When set to "auto", defaults to channel 6 for 2.4GHz, channel 36 for 5GHz, or channel 109 for 6GHz -- `enable-80211ax`: Boolean (default: false). Opt-in to enable 802.11ax (WiFi 6) on 2.4GHz and 5GHz bands. The 6GHz band always uses 802.11ax regardless of this setting > [!NOTE] > TX power and channel width are automatically determined by the driver based on regulatory constraints, PHY mode, and hardware capabilities. ### WiFi 6 (802.11ax) Support -WiFi 6 (802.11ax) provides improved performance in congested environments through -features like OFDMA, Target Wake Time, and BSS Coloring. By default, WiFi 6 is -only enabled on the 6GHz band (WiFi 6E requirement). +WiFi 6 (802.11ax) is always enabled in AP mode on all bands, providing improved +performance through features like OFDMA, BSS Coloring, and beamforming. -To enable WiFi 6 on 2.4GHz or 5GHz bands: - -``` -admin@example:/> configure -admin@example:/config/> edit hardware component radio0 wifi-radio -admin@example:/config/hardware/component/radio0/wifi-radio/> set country-code DE -admin@example:/config/hardware/component/radio0/wifi-radio/> set band 5GHz -admin@example:/config/hardware/component/radio0/wifi-radio/> set channel 36 -admin@example:/config/hardware/component/radio0/wifi-radio/> set enable-80211ax true -admin@example:/config/hardware/component/radio0/wifi-radio/> leave -``` - -**WiFi 6 Benefits:** +**WiFi 6 Features (always enabled):** - **OFDMA**: Better multi-user efficiency in dense environments -- **Target Wake Time**: Improved battery life for client devices -- **1024-QAM**: Higher throughput with strong signal conditions - **BSS Coloring**: Reduced interference from neighboring networks +- **Beamforming**: Improved signal quality and range **Requirements:** - Hardware must support 802.11ax - Client devices must support WiFi 6 for full benefits - Older WiFi 5/4 clients can still connect but won't use WiFi 6 features -> [!NOTE] -> The 6GHz band always uses WiFi 6 (802.11ax) regardless of the `enable-80211ax` -> setting, as WiFi 6E requires 802.11ax support. - ## Discovering Available Networks (Scanning) Before connecting to a WiFi network, you need to discover which networks diff --git a/src/bin/copy.c b/src/bin/copy.c index 3c37cc3a2..2a561d41d 100644 --- a/src/bin/copy.c +++ b/src/bin/copy.c @@ -580,7 +580,7 @@ static int resolve_dst(const char **dst, const struct infix_ds **ds, char **path static int copy(const char *src, const char *dst) { char *srcpath = NULL, *dstpath = NULL; - const struct infix_ds *srcds, *dstds; + const struct infix_ds *srcds = NULL, *dstds = NULL; bool rmsrc = false; mode_t oldmask; int err = 1; diff --git a/src/confd/src/hardware.c b/src/confd/src/hardware.c index 8bbff3fa7..ac36adeed 100644 --- a/src/confd/src/hardware.c +++ b/src/confd/src/hardware.c @@ -231,30 +231,22 @@ static int wifi_find_radio_aps(struct lyd_node *cifs, const char *radio_name, return 0; } -/* Generate BSS section for secondary AP (multi-SSID) */ -static int wifi_gen_bss_section(FILE *hostapd, struct lyd_node *cifs, const char *ifname, struct lyd_node *config) +/* Helper: Write SSID and security configuration (shared between primary and BSS) */ +static void wifi_gen_ssid_config(FILE *hostapd, struct lyd_node *cif, struct lyd_node *config, bool is_bss) { const char *ssid, *hidden, *security_mode, *secret_name, *secret; - struct lyd_node *cif, *wifi, *ap, *security, *secret_node; + struct lyd_node *wifi, *ap, *security, *secret_node; + const char *ifname; char bssid[18]; - /* Find the interface node for this BSS */ - LYX_LIST_FOR_EACH(cifs, cif, "interface") { - const char *name = lydx_get_cattr(cif, "name"); - if (strcmp(name, ifname) == 0) - break; - } - - if (!cif) { - ERROR("Failed to find interface %s for BSS section", ifname); - return SR_ERR_INVAL_ARG; - } - + ifname = lydx_get_cattr(cif, "name"); wifi = lydx_get_child(cif, "wifi"); ap = lydx_get_child(wifi, "access-point"); - fprintf(hostapd, "\n# BSS %s\n", ifname); - fprintf(hostapd, "bss=%s\n", ifname); + if (is_bss) { + fprintf(hostapd, "\n# BSS %s\n", ifname); + fprintf(hostapd, "bss=%s\n", ifname); + } /* Set BSSID if custom MAC is configured */ if (!interface_get_phys_addr(cif, bssid)) @@ -269,8 +261,31 @@ static int wifi_gen_bss_section(FILE *hostapd, struct lyd_node *cifs, const char if (hidden && !strcmp(hidden, "true")) fprintf(hostapd, "ignore_broadcast_ssid=1\n"); + /* + * Sane defaults for AP operation + */ + + /* WMM (Wi-Fi Multimedia) required for 802.11n/ac/ax and proper QoS */ fprintf(hostapd, "wmm_enabled=1\n"); - /* Security configuration */ + + /* Allow UTF-8 characters in SSID for international network names */ + fprintf(hostapd, "utf8_ssid=1\n"); + + /* Use short preamble for better throughput on modern clients */ + fprintf(hostapd, "preamble=1\n"); + + /* Disconnect clients with poor link quality to free up airtime */ + fprintf(hostapd, "disassoc_low_ack=1\n"); + + /* Poll inactive clients before disconnecting to detect sleepy devices */ + fprintf(hostapd, "skip_inactivity_poll=0\n"); + + /* Disable opportunistic key caching, reduces roaming attack surface */ + fprintf(hostapd, "okc=0\n"); + + /* Disable PMKSA caching, forces fresh authentication for better security */ + fprintf(hostapd, "disable_pmksa_caching=1\n"); + security = lydx_get_child(ap, "security"); security_mode = lydx_get_cattr(security, "mode"); @@ -291,57 +306,160 @@ static int wifi_gen_bss_section(FILE *hostapd, struct lyd_node *cifs, const char } if (!strcmp(security_mode, "open")) { - fprintf(hostapd, "# Open network\n"); + /* auth_algs=1: Open System authentication (unencrypted) */ fprintf(hostapd, "auth_algs=1\n"); } else if (!strcmp(security_mode, "wpa2-personal")) { - fprintf(hostapd, "# WPA2-Personal\n"); + /* wpa=2: WPA2 only (RSN), no legacy WPA1 */ fprintf(hostapd, "wpa=2\n"); + /* WPA-PSK: Pre-shared key authentication */ fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n"); + /* CCMP: AES-based encryption, mandatory for WPA2 */ fprintf(hostapd, "wpa_pairwise=CCMP\n"); if (secret) fprintf(hostapd, "wpa_passphrase=%s\n", secret); } else if (!strcmp(security_mode, "wpa3-personal")) { - fprintf(hostapd, "# WPA3-Personal\n"); + /* wpa=2: Uses RSN (WPA2) frame format for WPA3 */ fprintf(hostapd, "wpa=2\n"); + /* SAE: Simultaneous Authentication of Equals, resistant to offline dictionary attacks */ fprintf(hostapd, "wpa_key_mgmt=SAE\n"); fprintf(hostapd, "rsn_pairwise=CCMP\n"); if (secret) fprintf(hostapd, "sae_password=%s\n", secret); + /* ieee80211w=2: Management Frame Protection required for WPA3 */ fprintf(hostapd, "ieee80211w=2\n"); } else if (!strcmp(security_mode, "wpa2-wpa3-personal")) { - fprintf(hostapd, "# WPA2/WPA3 Mixed\n"); + /* Transition mode: supports both WPA2 and WPA3 clients */ fprintf(hostapd, "wpa=2\n"); + /* Allow both PSK (WPA2) and SAE (WPA3) authentication */ fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n"); fprintf(hostapd, "rsn_pairwise=CCMP\n"); if (secret) { fprintf(hostapd, "wpa_passphrase=%s\n", secret); fprintf(hostapd, "sae_password=%s\n", secret); } + /* ieee80211w=1: MFP capable but optional, for WPA2 client compatibility */ fprintf(hostapd, "ieee80211w=1\n"); } +} - return 0; +/* Helper: Write radio-specific configuration */ +static void wifi_gen_radio_config(FILE *hostapd, struct lyd_node *radio_node) +{ + const char *country, *channel, *band; + + country = lydx_get_cattr(radio_node, "country-code"); + band = lydx_get_cattr(radio_node, "band"); + channel = lydx_get_cattr(radio_node, "channel"); + + if (country) + fprintf(hostapd, "country_code=%s\n", country); + + /* + * 802.11d broadcasts country info in beacons, required for clients + * to know which channels/power levels are legal in this region. + */ + fprintf(hostapd, "ieee80211d=1\n"); + + /* + * 802.11h enables DFS (radar detection) and TPC (power control), + * required for 5GHz operation in most regulatory domains. + */ + fprintf(hostapd, "ieee80211h=1\n"); + + fprintf(hostapd, "beacon_int=100\n"); + + if (band) { + if (!strcmp(band, "2.4GHz")) { + /* hw_mode=g: 2.4GHz with 802.11g (OFDM) as baseline */ + fprintf(hostapd, "hw_mode=g\n"); + + /* + * Disable legacy 802.11b rates (1, 2, 5.5, 11 Mbps). + * Slow clients using these rates consume excessive + * airtime, degrading performance for all clients. + * Rates in 0.5 Mbps units: 60=6M, 90=9M, etc. + */ + fprintf(hostapd, "supported_rates=60 90 120 180 240 360 480 540\n"); + fprintf(hostapd, "basic_rates=60 120 240\n"); + } else if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz")) { + /* hw_mode=a: 5GHz/6GHz with 802.11a (OFDM) as baseline */ + fprintf(hostapd, "hw_mode=a\n"); + } + + if (channel) { + if (strcmp(channel, "auto") == 0) { + /* + * Default channels when "auto" selected: + * - Ch 6: Center of 2.4GHz, least overlap with 1 and 11 + * - Ch 36: First UNII-1 channel, indoor use, no DFS + * - Ch 109: 6GHz PSC channel, preferred for discovery + * TODO: Replace with ACS (channel=acs_survey) when driver support is verified. + */ + /* set to channel=acs_survey, if succeed to find a survey: + iw dev wlan0 survey dump + good: + Survey data from wlan0 + frequency: 2412 MHz + noise: -95 dBm + channel active time: 154 ms + channel busy time: 0 ms + channel receive time: 0 ms + channel transmit time: 0 ms + bad: + Survey data from wlan0 + frequency: 2412 MHz + [in use] + + */ + if (!strcmp(band, "2.4GHz")) { + fprintf(hostapd, "channel=6\n"); + } else if (!strcmp(band, "5GHz")) { + fprintf(hostapd, "channel=36\n"); + } else if (!strcmp(band, "6GHz")) { + fprintf(hostapd, "channel=109\n"); + } else { + fprintf(hostapd, "channel=0\n"); + } + } else { + fprintf(hostapd, "channel=%s\n", channel); + } + } + + /* + * Enable high-throughput modes per band: + * - 802.11n (HT): Required for speeds >54Mbps, all bands + * - 802.11ac (VHT): 5GHz only, enables 80/160MHz and MU-MIMO + * - 802.11ax (HE): Always enabled, improves dense deployments with OFDMA + */ + if (!strcmp(band, "2.4GHz")) { + fprintf(hostapd, "ieee80211n=1\n"); + } else if (!strcmp(band, "5GHz")) { + fprintf(hostapd, "ieee80211n=1\n"); + fprintf(hostapd, "ieee80211ac=1\n"); + } + /* 802.11ax (WiFi 6) always enabled for better performance */ + fprintf(hostapd, "ieee80211ax=1\n"); + /* BSS coloring reduces interference in dense deployments */ + fprintf(hostapd, "he_bss_color=1\n"); + /* Beamforming improves signal quality and range */ + fprintf(hostapd, "he_su_beamformer=1\n"); + fprintf(hostapd, "he_su_beamformee=1\n"); + } + fprintf(hostapd, "\n"); } /* Generate hostapd config for all APs on a radio (multi-SSID support) */ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs, struct lyd_node *radio_node, struct lyd_node *config) { - const char *ssid, *hidden, *security_mode, *secret_name, *secret; struct lyd_node *primary_cif, *cif; - struct lyd_node *primary_wifi, *primary_ap; - struct lyd_node *security, *secret_node; - const char *country, *channel, *band; const char *primary_ifname; char hostapd_conf[256]; char **ap_list = NULL; FILE *hostapd = NULL; - bool ax_enabled; int ap_count = 0; - char bssid[18]; - int i; - int rc = SR_ERR_OK; + int i; wifi_find_radio_aps(cifs, radio_name, &ap_list, &ap_count); @@ -367,34 +485,6 @@ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs, goto cleanup; } - primary_wifi = lydx_get_child(primary_cif, "wifi"); - primary_ap = lydx_get_child(primary_wifi, "access-point"); - - /* Get AP configuration */ - ssid = lydx_get_cattr(primary_ap, "ssid"); - hidden = lydx_get_cattr(primary_ap, "hidden"); - security = lydx_get_child(primary_ap, "security"); - security_mode = lydx_get_cattr(security, "mode"); - secret_name = lydx_get_cattr(security, "secret"); - secret = NULL; - - /* Get radio configuration */ - country = lydx_get_cattr(radio_node, "country-code"); - band = lydx_get_cattr(radio_node, "band"); - channel = lydx_get_cattr(radio_node, "channel"); - ax_enabled = lydx_get_bool(radio_node, "enable-80211ax"); - - /* Get secret from keystore if not open network */ - if (secret_name && strcmp(security_mode, "open") != 0) { - secret_node = lydx_get_xpathf(config, - "/keystore/symmetric-keys/symmetric-key[name='%s']/symmetric-key", - secret_name); - if (secret_node) { - secret = lyd_get_value(secret_node); - - } - } - snprintf(hostapd_conf, sizeof(hostapd_conf), HOSTAPD_CONF_NEXT, radio_name); hostapd = fopen(hostapd_conf, "w"); @@ -413,122 +503,35 @@ static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs, fprintf(hostapd, "interface=%s\n", primary_ifname); fprintf(hostapd, "driver=nl80211\n"); - fprintf(hostapd, "ctrl_interface=/run/hostapd\n"); + fprintf(hostapd, "ctrl_interface=/run/hostapd\n\n"); - /* Set BSSID if custom MAC is configured */ - if (!interface_get_phys_addr(primary_cif, bssid)) - fprintf(hostapd, "bssid=%s\n", bssid); - fprintf(hostapd, "\n"); - - fprintf(hostapd, "ssid=%s\n", ssid); - if (hidden && !strcmp(hidden, "true")) - fprintf(hostapd, "ignore_broadcast_ssid=1\n"); + /* Primary AP SSID and security configuration */ + wifi_gen_ssid_config(hostapd, primary_cif, config, false); fprintf(hostapd, "\n"); - if (country) - fprintf(hostapd, "country_code=%s\n", country); - - /* Enable 802.11d (regulatory domain) and 802.11h (spectrum management/DFS) */ - fprintf(hostapd, "ieee80211d=1\n"); - fprintf(hostapd, "ieee80211h=1\n"); - fprintf(hostapd, "wmm_enabled=1\n"); - - /* Band and channel configuration */ - if (band) { - /* Set hardware mode based on band */ - if (!strcmp(band, "2.4GHz")) { - fprintf(hostapd, "hw_mode=g\n"); - - /* Disable 802.11b rates, ancient devices. This will improve range. */ - fprintf(hostapd, "supported_rates=60 90 120 180 240 360 480 540\n"); - fprintf(hostapd, "basic_rates=60 120 240\n"); - } else if (!strcmp(band, "5GHz") || !strcmp(band, "6GHz")) { - fprintf(hostapd, "hw_mode=a\n"); - } - - /* Set channel */ - if (channel) { - if (strcmp(channel, "auto") == 0) { - /* - Use default channels: 6 for 2.4GHz, 36 for 5GHz, 109 for 6GHz, this - is a temporary hack, replace with logic for finding best free channel. - */ - if (!strcmp(band, "2.4GHz")) { - fprintf(hostapd, "channel=6\n"); - } else if (!strcmp(band, "5GHz")) { - fprintf(hostapd, "channel=36\n"); - } else if (!strcmp(band, "6GHz")) { - fprintf(hostapd, "channel=109\n"); - } else { - /* Unknown band - use ACS */ - fprintf(hostapd, "channel=0\n"); - } - } else { - fprintf(hostapd, "channel=%s\n", channel); - } - } - } + /* Radio-specific configuration */ + wifi_gen_radio_config(hostapd, radio_node); + /* Add BSS sections for secondary APs (multi-SSID) */ + for (i = 1; i < ap_count; i++) { + struct lyd_node *bss_cif = NULL; - if (band) { - if (!strcmp(band, "2.4GHz")) { - /* 2.4GHz: Enable 802.11n (HT), optionally 802.11ax */ - fprintf(hostapd, "ieee80211n=1\n"); - if (ax_enabled) { - fprintf(hostapd, "ieee80211ax=1\n"); - } - } else if (!strcmp(band, "5GHz")) { - /* 5GHz: Enable 802.11n and 802.11ac, optionally 802.11ax */ - fprintf(hostapd, "ieee80211n=1\n"); - fprintf(hostapd, "ieee80211ac=1\n"); - if (ax_enabled) { - fprintf(hostapd, "ieee80211ax=1\n"); + LYX_LIST_FOR_EACH(cifs, cif, "interface") { + if (!strcmp(lydx_get_cattr(cif, "name"), ap_list[i])) { + bss_cif = cif; + break; } - } else if (!strcmp(band, "6GHz")) { - /* 6GHz: Enable 802.11ax (required for 6GHz) */ - fprintf(hostapd, "ieee80211n=1\n"); - fprintf(hostapd, "ieee80211ac=1\n"); - fprintf(hostapd, "ieee80211ax=1\n"); } - } - fprintf(hostapd, "\n"); - /* Security configuration */ - if (!strcmp(security_mode, "open")) { - fprintf(hostapd, "# Open network (no encryption)\n"); - fprintf(hostapd, "auth_algs=1\n"); - } else if (!strcmp(security_mode, "wpa2-personal")) { - fprintf(hostapd, "# WPA2-Personal\n"); - fprintf(hostapd, "wpa=2\n"); - fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n"); - fprintf(hostapd, "wpa_pairwise=CCMP\n"); - fprintf(hostapd, "wpa_passphrase=%s\n", secret); - } else if (!strcmp(security_mode, "wpa3-personal")) { - fprintf(hostapd, "# WPA3-Personal\n"); - fprintf(hostapd, "wpa=2\n"); - fprintf(hostapd, "wpa_key_mgmt=SAE\n"); - fprintf(hostapd, "rsn_pairwise=CCMP\n"); - fprintf(hostapd, "sae_password=%s\n", secret); - fprintf(hostapd, "ieee80211w=2\n"); - } else if (!strcmp(security_mode, "wpa2-wpa3-personal")) { - fprintf(hostapd, "# WPA2/WPA3 Mixed Mode\n"); - fprintf(hostapd, "wpa=2\n"); - fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n"); - fprintf(hostapd, "rsn_pairwise=CCMP\n"); - fprintf(hostapd, "wpa_passphrase=%s\n", secret); - fprintf(hostapd, "sae_password=%s\n", secret); - fprintf(hostapd, "ieee80211w=1\n"); - } - - /* Add BSS sections for secondary APs (multi-SSID) */ - for (i = 1; i < ap_count; i++) { - DEBUG("Adding BSS section for secondary AP %s", ap_list[i]); - rc = wifi_gen_bss_section(hostapd, cifs, ap_list[i], config); - if (rc != SR_ERR_OK) { - ERROR("Failed to generate BSS section for %s", ap_list[i]); + if (!bss_cif) { + ERROR("Failed to find interface %s for BSS section", ap_list[i]); fclose(hostapd); + rc = SR_ERR_INVAL_ARG; goto cleanup; } + + DEBUG("Adding BSS section for secondary AP %s", ap_list[i]); + wifi_gen_ssid_config(hostapd, bss_cif, config, true); } fclose(hostapd); diff --git a/src/confd/yang/confd/infix-hardware.yang b/src/confd/yang/confd/infix-hardware.yang index 164f94e62..36079e638 100644 --- a/src/confd/yang/confd/infix-hardware.yang +++ b/src/confd/yang/confd/infix-hardware.yang @@ -262,27 +262,6 @@ module infix-hardware { congestion in most environments."; } - leaf enable-80211ax { - type boolean; - description - "Enable 802.11ax (WiFi 6) on 2.4GHz and 5GHz bands. - - By default, 802.11ax is enabled only on 6GHz (WiFi 6E). - Set to 'true' to enable 802.11ax on 2.4GHz and 5GHz bands. - - 802.11ax provides: - - OFDMA (better multi-user efficiency) - - Target Wake Time (better battery life) - - 1024-QAM (higher throughput) - - BSS Coloring (reduced interference) - - Requires: - - Hardware support for 802.11ax - - Compatible clients for full benefits - - Note: 6GHz band always uses 802.11ax regardless of this setting."; - } - /* * Operational state */ diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index 2d1afdb88..f6af9069f 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -1,242 +1,93 @@ -from ..host import HOST +""" +WiFi operational state provider using iw.py for interface data. +Scanning still uses wpa_supplicant for better compatibility. +""" import json import re +from ..host import HOST + -def detect_wifi_mode(ifname): - """Detect if interface is in AP or Station mode""" +def get_iw_info(ifname): + """Get interface info via iw.py on target""" try: - output = HOST.run(tuple(f"iw dev {ifname} info".split()), default="") - for line in output.splitlines(): - if 'type' in line.lower(): - if 'ap' in line.lower(): - return 'ap' - else: - return 'station' + data = HOST.run(('/usr/libexec/infix/iw.py', 'info', ifname), default='{}') + return json.loads(data) except Exception: pass - - # Default to station mode - return 'station' + return {} -def find_primary_interface_from_config(ifname): - """Find primary interface by reading hostapd config files""" +def get_iw_stations(ifname): + """Get connected stations via iw.py (AP mode)""" try: - file_list = HOST.run(("sh", "-c", "ls /etc/hostapd-*.conf"), default="") - if not file_list: - return None - - for config_file in file_list.splitlines(): - config_file = config_file.strip() - if not config_file: - continue + data = HOST.run(('/usr/libexec/infix/iw.py', 'station', ifname), default='[]') + return json.loads(data) + except Exception: + pass + return [] - try: - content = HOST.run(tuple(f"cat {config_file}".split()), default="") - if not content: - continue - if f"interface={ifname}" in content or f"bss={ifname}" in content: - for line in content.splitlines(): - if line.startswith("interface="): - return line.split("=", 1)[1].strip() - except Exception: - continue +def get_iw_link(ifname): + """Get link info via iw.py (station mode)""" + try: + data = HOST.run(('/usr/libexec/infix/iw.py', 'link', ifname), default='{}') + result = json.loads(data) + if result: + return result except Exception: pass - return None + return {'connected': False} def wifi_ap(ifname): - """Get operational data for AP mode using hostapd_cli""" + """Get operational data for AP mode using iw""" ap_data = {} - try: - primary_if = find_primary_interface_from_config(ifname) - if not primary_if: - return {} - - data = HOST.run(tuple(f"hostapd_cli -i {primary_if} status".split()), default="") - if not data: - return {} - - # Find our interface's SSID, different for bss and primary, because it is - if ifname == primary_if: - # Primary interface - get ssid[0] or ssid - for line in data.splitlines(): - if "=" in line: - try: - k, v = line.split("=", 1) - if k in ("ssid[0]", "ssid"): - ap_data["ssid"] = v - break - except ValueError: - continue - else: - # Secondary BSS - find in BSS array - bss_idx = None - for line in data.splitlines(): - if "=" in line: - try: - k, v = line.split("=", 1) - if v == ifname and k.startswith("bss["): - bss_idx = k[4:-1] # Extract index from bss[N] - break - except ValueError: - continue - - if bss_idx: - for line in data.splitlines(): - if "=" in line: - try: - k, v = line.split("=", 1) - if k == f"ssid[{bss_idx}]": - ap_data["ssid"] = v - break - except ValueError: - continue - - stations_data = HOST.run(tuple(f"iw dev {ifname} station dump".split()), default="") - stations = parse_iw_stations(stations_data) - - if stations: - ap_data["stations"] = { - "station": stations - } + # Get interface info (includes SSID for AP mode) + info = get_iw_info(ifname) - except Exception: - pass + if info.get('ssid'): + ap_data['ssid'] = info['ssid'] - # Nest data inside access-point container to match YANG schema - return { - "access-point": ap_data - } if ap_data else {} - - -def parse_iw_stations(output): - """Parse iw station dump output to get connected stations""" - stations = [] - current_station = None - - for line in output.splitlines(): - line = line.strip() - - # Station line: "Station aa:bb:cc:dd:ee:ff (on wifiX)" - if line.startswith("Station "): - if current_station: - stations.append(current_station) - # Extract MAC address - parts = line.split() - if len(parts) >= 2: - current_station = { - "mac-address": parts[1].lower() - } - elif current_station: - # Parse station attributes - try: - # Lines are in format "key: value" with tabs - if ":" not in line: - continue + # Get connected stations + stations = get_iw_stations(ifname) + if stations: + ap_data['stations'] = {'station': stations} - parts = line.split(":", 1) - key = parts[0].strip() - value = parts[1].strip() - - if key == "signal": - # Format: "-42 dBm" or "-42 [-44] dBm" - rssi = int(value.split()[0]) - current_station["rssi"] = rssi - elif key == "connected time": - # Format: "123 seconds" - seconds = int(value.split()[0]) - current_station["connected-time"] = seconds - elif key == "rx packets": - current_station["rx-packets"] = value - elif key == "tx packets": - current_station["tx-packets"] = value - elif key == "rx bytes": - current_station["rx-bytes"] = value - elif key == "tx bytes": - current_station["tx-bytes"] = value - elif key == "tx bitrate": - # Format: "866.7 MBit/s ..." - extract speed and convert to 100kbit/s units - speed_mbps = float(value.split()[0]) - current_station["tx-speed"] = int(speed_mbps * 10) - elif key == "rx bitrate": - # Format: "780.0 MBit/s ..." - extract speed and convert to 100kbit/s units - speed_mbps = float(value.split()[0]) - current_station["rx-speed"] = int(speed_mbps * 10) - except (ValueError, KeyError, IndexError): - # Skip invalid values - continue - - # Add last station - if current_station: - stations.append(current_station) - - return stations + return {'access-point': ap_data} if ap_data else {} def wifi_station(ifname): - """Get operational data for Station mode using wpa_cli""" + """Get operational data for Station mode using iw + wpa_cli for scanning""" station_data = {} - try: - data = HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="") + # Get link info (includes SSID and RSSI when connected) + link = get_iw_link(ifname) - if data != "": - for line in data.splitlines(): - try: - if "=" not in line: - continue - k, v = line.split("=", 1) - if k == "ssid": - station_data["ssid"] = v - 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 if not connected - if data.strip() != "FAIL": - for line in data.splitlines(): - try: - if "=" not in line: - continue - k, v = line.strip().split("=", 1) - if k == "RSSI": - station_data["rssi"] = int(v) - except (ValueError, KeyError): - # Skip malformed lines or invalid integers - continue - except Exception: - # If signal_poll fails, continue without RSSI - pass - except Exception: - # If status query fails entirely, continue with scan results - pass + if link.get('connected'): + if link.get('ssid'): + station_data['ssid'] = link['ssid'] + if link.get('rssi') is not None: + station_data['rssi'] = link['rssi'] + # Get scan results from wpa_supplicant (better scan support) try: - data = HOST.run(tuple(f"wpa_cli -i {ifname} scan_result".split()), default="FAIL") - if data != "FAIL": + data = HOST.run(('wpa_cli', '-i', ifname, 'scan_result'), default='FAIL') + if data and data != 'FAIL': scan_results = parse_wpa_scan_result(data) if scan_results: - station_data["scan-results"] = scan_results + station_data['scan-results'] = scan_results except Exception: - # If scan results fail, just omit them pass - # Always nest data inside station container to match YANG schema - # In scan-only mode, this will be just scan-results with no ssid/rssi - return {"station": station_data} if station_data else {} + return {'station': station_data} if station_data else {} def wifi(ifname): """Main entry point - detect mode and return appropriate data""" - mode = detect_wifi_mode(ifname) + info = get_iw_info(ifname) + mode = info.get('iftype', '').lower() if mode == 'ap': return wifi_ap(ifname) @@ -245,11 +96,10 @@ def wifi(ifname): def parse_wpa_scan_result(scan_output): + """Parse wpa_cli scan_result output""" networks = {} - lines = scan_output.strip().split('\n') - # Skip header line and any empty lines - for line in lines: + for line in scan_output.strip().split('\n'): try: line = line.strip() if not line or 'bssid / frequency' in line.lower(): @@ -264,24 +114,20 @@ def parse_wpa_scan_result(scan_output): frequency = int(parts[1].strip()) rssi = int(parts[2].strip()) except ValueError: - # Skip lines with invalid frequency or RSSI continue flags = parts[3].strip() ssid = parts[4].strip() if len(parts) > 4 else "" - # Skip hidden SSIDs (empty or whitespace only) - if not ssid or ssid.isspace() or '\\x00' in ssid: + # Skip hidden SSIDs (empty or null-filled) + if not ssid or ssid.isspace() or '\\x00' in ssid: continue - # Extract encryption information from flags encryption = extract_encryption(flags) - - # Convert frequency to channel channel = frequency_to_channel(frequency) - # Keep only the network with best (highest) RSSI per SSID - if ssid not in networks or rssi < networks[ssid]['rssi']: + # Keep best RSSI per SSID + if ssid not in networks or rssi > networks[ssid]['rssi']: networks[ssid] = { 'bssid': bssid, 'ssid': ssid, @@ -290,21 +136,21 @@ def parse_wpa_scan_result(scan_output): 'channel': channel } except Exception: - # Skip any malformed scan result lines continue - # Convert to list and sort by RSSI (best first) + # Sort by RSSI (best first) result = list(networks.values()) result.sort(key=lambda x: x['rssi'], reverse=True) return result + def frequency_to_channel(frequency): """Convert frequency (MHz) to WiFi channel number""" freq = int(frequency) # 2.4 GHz band (channels 1-14) - if 2412 <= freq <= 2484: # Channel 14 is special + if 2412 <= freq <= 2484: if freq == 2484: return 14 return (freq - 2412) // 5 + 1 @@ -317,16 +163,15 @@ def frequency_to_channel(frequency): elif 5955 <= freq <= 7115: return (freq - 5950) // 5 - else: - return f"Unknown ({freq} MHz)" + return 0 + def extract_encryption(flags): - """Extract detailed encryption information from flags string""" + """Extract encryption information from flags string""" flags = flags.upper() encryption_info = { 'protocols': [], 'key_mgmt': [], - 'ciphers': [], 'auth_type': 'Unknown' } @@ -345,22 +190,12 @@ def extract_encryption(flags): if 'EAP' in flags: encryption_info['key_mgmt'].append('EAP') encryption_info['auth_type'] = 'Enterprise' - if 'SAE' in flags: # WPA3 Personal + if 'SAE' in flags: encryption_info['key_mgmt'].append('SAE') encryption_info['auth_type'] = 'Personal' - if 'OWE' in flags: # Enhanced Open (WPA3) + if 'OWE' in flags: encryption_info['key_mgmt'].append('OWE') encryption_info['auth_type'] = 'Enhanced Open' - if 'FT' in flags: - encryption_info['key_mgmt'].append('FT') - - # Extract cipher suites - if 'CCMP' in flags: - encryption_info['ciphers'].append('CCMP') - if 'TKIP' in flags: - encryption_info['ciphers'].append('TKIP') - if 'GCMP' in flags: - encryption_info['ciphers'].append('GCMP') # Handle special cases if 'WEP' in flags: @@ -369,7 +204,7 @@ def extract_encryption(flags): if not encryption_info['protocols'] and 'ESS' in flags: return ['Open'] - # Return array of supported protocols with auth type + # Return protocols with auth type result = [] for protocol in encryption_info['protocols']: if encryption_info['auth_type'] == 'Enterprise':