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 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): def parse_survey(ifname):
""" """
Parse 'iw dev <name> survey dump' output Parse 'iw dev <name> survey dump' output
@@ -396,6 +463,66 @@ def parse_dev():
return result 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(): def main():
if len(sys.argv) < 2: if len(sys.argv) < 2:
print(json.dumps({ print(json.dumps({
@@ -404,14 +531,17 @@ def main():
'list': 'List all PHY devices', 'list': 'List all PHY devices',
'dev': 'List all interfaces grouped by PHY', 'dev': 'List all interfaces grouped by PHY',
'info': 'Get PHY or interface information (requires device)', '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': [ 'examples': [
'iw.py list', 'iw.py list',
'iw.py dev', 'iw.py dev',
'iw.py info radio0', 'iw.py info radio0',
'iw.py info phy4',
'iw.py info wlan0', 'iw.py info wlan0',
'iw.py station wifi0',
'iw.py link wlan0',
'iw.py survey wlan0' 'iw.py survey wlan0'
] ]
}, indent=2)) }, indent=2))
@@ -434,12 +564,21 @@ def main():
data = parse_phy_info(device) data = parse_phy_info(device)
else: else:
data = parse_interface_info(device) 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': elif command == 'survey':
if len(sys.argv) < 3: if len(sys.argv) < 3:
data = {'error': 'survey command requires device argument'} data = {'error': 'survey command requires interface argument'}
else: else:
device = sys.argv[2] data = parse_survey(sys.argv[2])
data = parse_survey(device)
else: else:
data = {'error': f'Unknown command: {command}'} 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:** **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.** - `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 - `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] > [!NOTE]
> TX power and channel width are automatically determined by the driver based on regulatory constraints, PHY mode, and hardware capabilities. > 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) Support
WiFi 6 (802.11ax) provides improved performance in congested environments through WiFi 6 (802.11ax) is always enabled in AP mode on all bands, providing improved
features like OFDMA, Target Wake Time, and BSS Coloring. By default, WiFi 6 is performance through features like OFDMA, BSS Coloring, and beamforming.
only enabled on the 6GHz band (WiFi 6E requirement).
To enable WiFi 6 on 2.4GHz or 5GHz bands: **WiFi 6 Features (always enabled):**
```
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:**
- **OFDMA**: Better multi-user efficiency in dense environments - **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 - **BSS Coloring**: Reduced interference from neighboring networks
- **Beamforming**: Improved signal quality and range
**Requirements:** **Requirements:**
- Hardware must support 802.11ax - Hardware must support 802.11ax
- Client devices must support WiFi 6 for full benefits - Client devices must support WiFi 6 for full benefits
- Older WiFi 5/4 clients can still connect but won't use WiFi 6 features - 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) ## Discovering Available Networks (Scanning)
Before connecting to a WiFi network, you need to discover which networks 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) static int copy(const char *src, const char *dst)
{ {
char *srcpath = NULL, *dstpath = NULL; char *srcpath = NULL, *dstpath = NULL;
const struct infix_ds *srcds, *dstds; const struct infix_ds *srcds = NULL, *dstds = NULL;
bool rmsrc = false; bool rmsrc = false;
mode_t oldmask; mode_t oldmask;
int err = 1; 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; return 0;
} }
/* Generate BSS section for secondary AP (multi-SSID) */ /* Helper: Write SSID and security configuration (shared between primary and BSS) */
static int wifi_gen_bss_section(FILE *hostapd, struct lyd_node *cifs, const char *ifname, struct lyd_node *config) 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; 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]; char bssid[18];
/* Find the interface node for this BSS */ ifname = lydx_get_cattr(cif, "name");
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;
}
wifi = lydx_get_child(cif, "wifi"); wifi = lydx_get_child(cif, "wifi");
ap = lydx_get_child(wifi, "access-point"); ap = lydx_get_child(wifi, "access-point");
fprintf(hostapd, "\n# BSS %s\n", ifname); if (is_bss) {
fprintf(hostapd, "bss=%s\n", ifname); fprintf(hostapd, "\n# BSS %s\n", ifname);
fprintf(hostapd, "bss=%s\n", ifname);
}
/* Set BSSID if custom MAC is configured */ /* Set BSSID if custom MAC is configured */
if (!interface_get_phys_addr(cif, bssid)) 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")) if (hidden && !strcmp(hidden, "true"))
fprintf(hostapd, "ignore_broadcast_ssid=1\n"); 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"); 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 = lydx_get_child(ap, "security");
security_mode = lydx_get_cattr(security, "mode"); 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")) { if (!strcmp(security_mode, "open")) {
fprintf(hostapd, "# Open network\n"); /* auth_algs=1: Open System authentication (unencrypted) */
fprintf(hostapd, "auth_algs=1\n"); fprintf(hostapd, "auth_algs=1\n");
} else if (!strcmp(security_mode, "wpa2-personal")) { } 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"); fprintf(hostapd, "wpa=2\n");
/* WPA-PSK: Pre-shared key authentication */
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n"); fprintf(hostapd, "wpa_key_mgmt=WPA-PSK\n");
/* CCMP: AES-based encryption, mandatory for WPA2 */
fprintf(hostapd, "wpa_pairwise=CCMP\n"); fprintf(hostapd, "wpa_pairwise=CCMP\n");
if (secret) if (secret)
fprintf(hostapd, "wpa_passphrase=%s\n", secret); fprintf(hostapd, "wpa_passphrase=%s\n", secret);
} else if (!strcmp(security_mode, "wpa3-personal")) { } 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"); fprintf(hostapd, "wpa=2\n");
/* SAE: Simultaneous Authentication of Equals, resistant to offline dictionary attacks */
fprintf(hostapd, "wpa_key_mgmt=SAE\n"); fprintf(hostapd, "wpa_key_mgmt=SAE\n");
fprintf(hostapd, "rsn_pairwise=CCMP\n"); fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret) if (secret)
fprintf(hostapd, "sae_password=%s\n", secret); fprintf(hostapd, "sae_password=%s\n", secret);
/* ieee80211w=2: Management Frame Protection required for WPA3 */
fprintf(hostapd, "ieee80211w=2\n"); fprintf(hostapd, "ieee80211w=2\n");
} else if (!strcmp(security_mode, "wpa2-wpa3-personal")) { } 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"); fprintf(hostapd, "wpa=2\n");
/* Allow both PSK (WPA2) and SAE (WPA3) authentication */
fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n"); fprintf(hostapd, "wpa_key_mgmt=WPA-PSK SAE\n");
fprintf(hostapd, "rsn_pairwise=CCMP\n"); fprintf(hostapd, "rsn_pairwise=CCMP\n");
if (secret) { if (secret) {
fprintf(hostapd, "wpa_passphrase=%s\n", secret); fprintf(hostapd, "wpa_passphrase=%s\n", secret);
fprintf(hostapd, "sae_password=%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"); 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) */ /* 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, static int wifi_gen_aps_on_radio(const char *radio_name, struct lyd_node *cifs,
struct lyd_node *radio_node, struct lyd_node *config) 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_cif, *cif;
struct lyd_node *primary_wifi, *primary_ap;
struct lyd_node *security, *secret_node;
const char *country, *channel, *band;
const char *primary_ifname; const char *primary_ifname;
char hostapd_conf[256]; char hostapd_conf[256];
char **ap_list = NULL; char **ap_list = NULL;
FILE *hostapd = NULL; FILE *hostapd = NULL;
bool ax_enabled;
int ap_count = 0; int ap_count = 0;
char bssid[18];
int i;
int rc = SR_ERR_OK; int rc = SR_ERR_OK;
int i;
wifi_find_radio_aps(cifs, radio_name, &ap_list, &ap_count); 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; 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); snprintf(hostapd_conf, sizeof(hostapd_conf), HOSTAPD_CONF_NEXT, radio_name);
hostapd = fopen(hostapd_conf, "w"); 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, "interface=%s\n", primary_ifname);
fprintf(hostapd, "driver=nl80211\n"); 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 */ /* Primary AP SSID and security configuration */
if (!interface_get_phys_addr(primary_cif, bssid)) wifi_gen_ssid_config(hostapd, primary_cif, config, false);
fprintf(hostapd, "bssid=%s\n", bssid);
fprintf(hostapd, "\n"); fprintf(hostapd, "\n");
fprintf(hostapd, "ssid=%s\n", ssid); /* Radio-specific configuration */
if (hidden && !strcmp(hidden, "true")) wifi_gen_radio_config(hostapd, radio_node);
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");
}
/* Add BSS sections for secondary APs (multi-SSID) */ /* Add BSS sections for secondary APs (multi-SSID) */
for (i = 1; i < ap_count; i++) { for (i = 1; i < ap_count; i++) {
DEBUG("Adding BSS section for secondary AP %s", ap_list[i]); struct lyd_node *bss_cif = NULL;
rc = wifi_gen_bss_section(hostapd, cifs, ap_list[i], config);
if (rc != SR_ERR_OK) { LYX_LIST_FOR_EACH(cifs, cif, "interface") {
ERROR("Failed to generate BSS section for %s", ap_list[i]); 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); fclose(hostapd);
rc = SR_ERR_INVAL_ARG;
goto cleanup; goto cleanup;
} }
DEBUG("Adding BSS section for secondary AP %s", ap_list[i]);
wifi_gen_ssid_config(hostapd, bss_cif, config, true);
} }
fclose(hostapd); fclose(hostapd);
-21
View File
@@ -262,27 +262,6 @@ module infix-hardware {
congestion in most environments."; 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 * 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 json
import re 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: try:
output = HOST.run(tuple(f"iw dev {ifname} info".split()), default="") data = HOST.run(('/usr/libexec/infix/iw.py', 'info', ifname), default='{}')
for line in output.splitlines(): return json.loads(data)
if 'type' in line.lower():
if 'ap' in line.lower():
return 'ap'
else:
return 'station'
except Exception: except Exception:
pass pass
return {}
# Default to station mode
return 'station'
def find_primary_interface_from_config(ifname): def get_iw_stations(ifname):
"""Find primary interface by reading hostapd config files""" """Get connected stations via iw.py (AP mode)"""
try: try:
file_list = HOST.run(("sh", "-c", "ls /etc/hostapd-*.conf"), default="") data = HOST.run(('/usr/libexec/infix/iw.py', 'station', ifname), default='[]')
if not file_list: return json.loads(data)
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
except Exception: except Exception:
pass 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): def wifi_ap(ifname):
"""Get operational data for AP mode using hostapd_cli""" """Get operational data for AP mode using iw"""
ap_data = {} ap_data = {}
try: # Get interface info (includes SSID for AP mode)
primary_if = find_primary_interface_from_config(ifname) info = get_iw_info(ifname)
if not primary_if:
return {}
data = HOST.run(tuple(f"hostapd_cli -i {primary_if} status".split()), default="") if info.get('ssid'):
if not data: ap_data['ssid'] = info['ssid']
return {}
# Find our interface's SSID, different for bss and primary, because it is # Get connected stations
if ifname == primary_if: stations = get_iw_stations(ifname)
# Primary interface - get ssid[0] or ssid if stations:
for line in data.splitlines(): ap_data['stations'] = {'station': stations}
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: return {'access-point': ap_data} if ap_data else {}
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
def wifi_station(ifname): 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 = {} 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: try:
data = HOST.run(tuple(f"wpa_cli -i {ifname} status".split()), default="") data = HOST.run(('wpa_cli', '-i', ifname, 'scan_result'), default='FAIL')
if data and data != 'FAIL':
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":
scan_results = parse_wpa_scan_result(data) scan_results = parse_wpa_scan_result(data)
if scan_results: if scan_results:
station_data["scan-results"] = scan_results station_data['scan-results'] = scan_results
except Exception: except Exception:
# If scan results fail, just omit them
pass pass
# Always nest data inside station container to match YANG schema return {'station': station_data} if station_data else {}
# In scan-only mode, this will be just scan-results with no ssid/rssi
return {"station": station_data} if station_data else {}
def wifi(ifname): def wifi(ifname):
"""Main entry point - detect mode and return appropriate data""" """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': if mode == 'ap':
return wifi_ap(ifname) return wifi_ap(ifname)
@@ -245,11 +96,10 @@ def wifi(ifname):
def parse_wpa_scan_result(scan_output): def parse_wpa_scan_result(scan_output):
"""Parse wpa_cli scan_result output"""
networks = {} networks = {}
lines = scan_output.strip().split('\n')
# Skip header line and any empty lines for line in scan_output.strip().split('\n'):
for line in lines:
try: try:
line = line.strip() line = line.strip()
if not line or 'bssid / frequency' in line.lower(): 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()) frequency = int(parts[1].strip())
rssi = int(parts[2].strip()) rssi = int(parts[2].strip())
except ValueError: except ValueError:
# Skip lines with invalid frequency or RSSI
continue continue
flags = parts[3].strip() flags = parts[3].strip()
ssid = parts[4].strip() if len(parts) > 4 else "" ssid = parts[4].strip() if len(parts) > 4 else ""
# Skip hidden SSIDs (empty or whitespace only) # Skip hidden SSIDs (empty or null-filled)
if not ssid or ssid.isspace() or '\\x00' in ssid: if not ssid or ssid.isspace() or '\\x00' in ssid:
continue continue
# Extract encryption information from flags
encryption = extract_encryption(flags) encryption = extract_encryption(flags)
# Convert frequency to channel
channel = frequency_to_channel(frequency) channel = frequency_to_channel(frequency)
# Keep only the network with best (highest) RSSI per SSID # Keep best RSSI per SSID
if ssid not in networks or rssi < networks[ssid]['rssi']: if ssid not in networks or rssi > networks[ssid]['rssi']:
networks[ssid] = { networks[ssid] = {
'bssid': bssid, 'bssid': bssid,
'ssid': ssid, 'ssid': ssid,
@@ -290,21 +136,21 @@ def parse_wpa_scan_result(scan_output):
'channel': channel 'channel': channel
} }
except Exception: except Exception:
# Skip any malformed scan result lines
continue continue
# Convert to list and sort by RSSI (best first) # Sort by RSSI (best first)
result = list(networks.values()) result = list(networks.values())
result.sort(key=lambda x: x['rssi'], reverse=True) result.sort(key=lambda x: x['rssi'], reverse=True)
return result return result
def frequency_to_channel(frequency): def frequency_to_channel(frequency):
"""Convert frequency (MHz) to WiFi channel number""" """Convert frequency (MHz) to WiFi channel number"""
freq = int(frequency) freq = int(frequency)
# 2.4 GHz band (channels 1-14) # 2.4 GHz band (channels 1-14)
if 2412 <= freq <= 2484: # Channel 14 is special if 2412 <= freq <= 2484:
if freq == 2484: if freq == 2484:
return 14 return 14
return (freq - 2412) // 5 + 1 return (freq - 2412) // 5 + 1
@@ -317,16 +163,15 @@ def frequency_to_channel(frequency):
elif 5955 <= freq <= 7115: elif 5955 <= freq <= 7115:
return (freq - 5950) // 5 return (freq - 5950) // 5
else: return 0
return f"Unknown ({freq} MHz)"
def extract_encryption(flags): def extract_encryption(flags):
"""Extract detailed encryption information from flags string""" """Extract encryption information from flags string"""
flags = flags.upper() flags = flags.upper()
encryption_info = { encryption_info = {
'protocols': [], 'protocols': [],
'key_mgmt': [], 'key_mgmt': [],
'ciphers': [],
'auth_type': 'Unknown' 'auth_type': 'Unknown'
} }
@@ -345,22 +190,12 @@ def extract_encryption(flags):
if 'EAP' in flags: if 'EAP' in flags:
encryption_info['key_mgmt'].append('EAP') encryption_info['key_mgmt'].append('EAP')
encryption_info['auth_type'] = 'Enterprise' encryption_info['auth_type'] = 'Enterprise'
if 'SAE' in flags: # WPA3 Personal if 'SAE' in flags:
encryption_info['key_mgmt'].append('SAE') encryption_info['key_mgmt'].append('SAE')
encryption_info['auth_type'] = 'Personal' encryption_info['auth_type'] = 'Personal'
if 'OWE' in flags: # Enhanced Open (WPA3) if 'OWE' in flags:
encryption_info['key_mgmt'].append('OWE') encryption_info['key_mgmt'].append('OWE')
encryption_info['auth_type'] = 'Enhanced Open' 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 # Handle special cases
if 'WEP' in flags: if 'WEP' in flags:
@@ -369,7 +204,7 @@ def extract_encryption(flags):
if not encryption_info['protocols'] and 'ESS' in flags: if not encryption_info['protocols'] and 'ESS' in flags:
return ['Open'] return ['Open']
# Return array of supported protocols with auth type # Return protocols with auth type
result = [] result = []
for protocol in encryption_info['protocols']: for protocol in encryption_info['protocols']:
if encryption_info['auth_type'] == 'Enterprise': if encryption_info['auth_type'] == 'Enterprise':