Merge pull request #1366 from kernelkit/misc

This commit is contained in:
Mattias Walström
2026-01-24 18:02:02 +01:00
committed by GitHub
6 changed files with 389 additions and 452 deletions
+144 -5
View File
@@ -286,6 +286,73 @@ def parse_interface_info(ifname):
return result
def parse_stations(ifname):
"""
Parse 'iw dev <name> 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 <name> survey dump' output
@@ -396,6 +463,66 @@ def parse_dev():
return result
def parse_link(ifname):
"""
Parse 'iw dev <name> 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}'}
+5 -24
View File
@@ -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
+1 -1
View File
@@ -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;
+169 -166
View File
@@ -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);
/* Primary AP SSID and security configuration */
wifi_gen_ssid_config(hostapd, primary_cif, config, false);
fprintf(hostapd, "\n");
fprintf(hostapd, "ssid=%s\n", ssid);
if (hidden && !strcmp(hidden, "true"))
fprintf(hostapd, "ignore_broadcast_ssid=1\n");
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);
}
}
}
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");
}
} 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");
}
/* 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++) {
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]);
struct lyd_node *bss_cif = NULL;
LYX_LIST_FOR_EACH(cifs, cif, "interface") {
if (!strcmp(lydx_get_cattr(cif, "name"), ap_list[i])) {
bss_cif = cif;
break;
}
}
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);
-21
View File
@@ -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
*/
+70 -235
View File
@@ -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
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
data = HOST.run(('/usr/libexec/infix/iw.py', 'station', ifname), default='[]')
return json.loads(data)
except Exception:
pass
return None
return []
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 {'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 {}
# Get interface info (includes SSID for AP mode)
info = get_iw_info(ifname)
data = HOST.run(tuple(f"hostapd_cli -i {primary_if} status".split()), default="")
if not data:
return {}
if info.get('ssid'):
ap_data['ssid'] = info['ssid']
# 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
# Get connected stations
stations = get_iw_stations(ifname)
if stations:
ap_data['stations'] = {'station': stations}
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
}
except Exception:
pass
# 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
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 = {}
# Get link info (includes SSID and RSSI when connected)
link = get_iw_link(ifname)
if link.get('connected'):
if link.get('ssid'):
station_data['ssid'] = link['ssid']
if link.get('rssi') is not None:
station_data['rssi'] = link['rssi']
# Get scan results from wpa_supplicant (better scan support)
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":
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
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':