mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-31 21:13:00 +02:00
Merge pull request #1366 from kernelkit/misc
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
Reference in New Issue
Block a user