use std::ffi::c_void;
use std::mem::size_of;
use std::time::{Duration, Instant};
use windows::core::{GUID, HSTRING, PCWSTR};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::NetworkManagement::WiFi::{
dot11_BSS_type_infrastructure, dot11_phy_type_dmg, dot11_phy_type_dsss, dot11_phy_type_eht,
dot11_phy_type_erp, dot11_phy_type_fhss, dot11_phy_type_he, dot11_phy_type_hrdsss,
dot11_phy_type_irbaseband, dot11_phy_type_ofdm, dot11_phy_type_vht,
wlan_connection_mode_profile, wlan_intf_opcode_current_connection,
wlan_interface_state_ad_hoc_network_formed, wlan_interface_state_associating,
wlan_interface_state_authenticating, wlan_interface_state_connected,
wlan_interface_state_disconnected, wlan_interface_state_disconnecting,
wlan_interface_state_discovering, wlan_interface_state_not_ready, WlanCloseHandle,
WlanConnect, WlanDisconnect, WlanEnumInterfaces, WlanFreeMemory, WlanGetAvailableNetworkList,
WlanOpenHandle, WlanQueryInterface, WlanScan, WlanSetProfile, DOT11_AUTH_ALGO_80211_OPEN,
DOT11_AUTH_ALGO_80211_SHARED_KEY, DOT11_AUTH_ALGO_OWE, DOT11_AUTH_ALGO_RSNA,
DOT11_AUTH_ALGO_RSNA_PSK, DOT11_AUTH_ALGO_WPA, DOT11_AUTH_ALGO_WPA3, DOT11_AUTH_ALGO_WPA3_SAE,
DOT11_AUTH_ALGO_WPA_NONE, DOT11_AUTH_ALGO_WPA_PSK, DOT11_CIPHER_ALGO_BIP,
DOT11_CIPHER_ALGO_CCMP, DOT11_CIPHER_ALGO_CCMP_256, DOT11_CIPHER_ALGO_GCMP,
DOT11_CIPHER_ALGO_GCMP_256, DOT11_CIPHER_ALGO_NONE, DOT11_CIPHER_ALGO_TKIP,
DOT11_CIPHER_ALGO_WEP, DOT11_CIPHER_ALGO_WEP104, DOT11_CIPHER_ALGO_WEP40, DOT11_SSID,
WLAN_AVAILABLE_NETWORK_CONNECTED, WLAN_AVAILABLE_NETWORK_LIST, WLAN_CONNECTION_ATTRIBUTES,
WLAN_CONNECTION_PARAMETERS, WLAN_INTERFACE_INFO_LIST,
};
use super::{ConnectOutcome, ConnectionInfo, Interface, InterfaceState, NetworkInfo};
use crate::error::{IwconfError, Result};
const WLAN_CLIENT_VERSION: u32 = 2;
pub struct WlanHandle(HANDLE);
impl WlanHandle {
pub fn open() -> Result<Self> {
let mut negotiated = 0u32;
let mut handle = HANDLE::default();
let status = unsafe {
WlanOpenHandle(
WLAN_CLIENT_VERSION,
None,
&mut negotiated,
&mut handle,
)
};
if status != 0 {
return Err(IwconfError::ServiceUnavailable);
}
Ok(Self(handle))
}
}
impl Drop for WlanHandle {
fn drop(&mut self) {
unsafe {
let _ = WlanCloseHandle(self.0, None);
}
}
}
struct WlanBuf<T>(*mut T);
impl<T> Drop for WlanBuf<T> {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { WlanFreeMemory(self.0 as *const c_void) };
}
}
}
fn wide_to_string(buf: &[u16]) -> String {
let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
String::from_utf16_lossy(&buf[..end])
}
fn ssid_to_string(ssid: &DOT11_SSID) -> String {
let len = (ssid.uSSIDLength as usize).min(ssid.ucSSID.len());
String::from_utf8_lossy(&ssid.ucSSID[..len]).into_owned()
}
fn phy_type_str(phy: windows::Win32::NetworkManagement::WiFi::DOT11_PHY_TYPE) -> &'static str {
match phy {
p if p == dot11_phy_type_fhss => "802.11 FHSS",
p if p == dot11_phy_type_dsss => "802.11 DSSS",
p if p == dot11_phy_type_irbaseband => "802.11 IR",
p if p == dot11_phy_type_ofdm => "802.11a",
p if p == dot11_phy_type_hrdsss => "802.11b",
p if p == dot11_phy_type_erp => "802.11g",
p if p == windows::Win32::NetworkManagement::WiFi::dot11_phy_type_ht => "802.11n",
p if p == dot11_phy_type_vht => "802.11ac",
p if p == dot11_phy_type_dmg => "802.11ad",
p if p == dot11_phy_type_he => "802.11ax",
p if p == dot11_phy_type_eht => "802.11be",
_ => "unknown",
}
}
fn auth_str(auth: windows::Win32::NetworkManagement::WiFi::DOT11_AUTH_ALGORITHM) -> &'static str {
match auth {
a if a == DOT11_AUTH_ALGO_80211_OPEN => "Open",
a if a == DOT11_AUTH_ALGO_80211_SHARED_KEY => "Shared Key",
a if a == DOT11_AUTH_ALGO_WPA => "WPA-Enterprise",
a if a == DOT11_AUTH_ALGO_WPA_PSK => "WPA-Personal",
a if a == DOT11_AUTH_ALGO_WPA_NONE => "WPA-None",
a if a == DOT11_AUTH_ALGO_RSNA => "WPA2-Enterprise",
a if a == DOT11_AUTH_ALGO_RSNA_PSK => "WPA2-Personal",
a if a == DOT11_AUTH_ALGO_WPA3 => "WPA3-Enterprise",
a if a == DOT11_AUTH_ALGO_WPA3_SAE => "WPA3-Personal (SAE)",
a if a == DOT11_AUTH_ALGO_OWE => "OWE (Enhanced Open)",
_ => "Unknown",
}
}
fn cipher_str(
cipher: windows::Win32::NetworkManagement::WiFi::DOT11_CIPHER_ALGORITHM,
) -> &'static str {
match cipher {
c if c == DOT11_CIPHER_ALGO_NONE => "None",
c if c == DOT11_CIPHER_ALGO_WEP40 => "WEP40",
c if c == DOT11_CIPHER_ALGO_TKIP => "TKIP",
c if c == DOT11_CIPHER_ALGO_CCMP => "CCMP (AES)",
c if c == DOT11_CIPHER_ALGO_WEP104 => "WEP104",
c if c == DOT11_CIPHER_ALGO_BIP => "BIP",
c if c == DOT11_CIPHER_ALGO_GCMP => "GCMP",
c if c == DOT11_CIPHER_ALGO_GCMP_256 => "GCMP-256",
c if c == DOT11_CIPHER_ALGO_CCMP_256 => "CCMP-256",
c if c == DOT11_CIPHER_ALGO_WEP => "WEP",
_ => "Unknown",
}
}
fn interface_state_from(
s: windows::Win32::NetworkManagement::WiFi::WLAN_INTERFACE_STATE,
) -> InterfaceState {
match s {
v if v == wlan_interface_state_not_ready => InterfaceState::NotReady,
v if v == wlan_interface_state_connected => InterfaceState::Connected,
v if v == wlan_interface_state_ad_hoc_network_formed => {
InterfaceState::AdHocNetworkFormed
}
v if v == wlan_interface_state_disconnecting => InterfaceState::Disconnecting,
v if v == wlan_interface_state_disconnected => InterfaceState::Disconnected,
v if v == wlan_interface_state_associating => InterfaceState::Associating,
v if v == wlan_interface_state_discovering => InterfaceState::Discovering,
v if v == wlan_interface_state_authenticating => InterfaceState::Authenticating,
_ => InterfaceState::Unknown,
}
}
pub fn list_interfaces(handle: &WlanHandle) -> Result<Vec<Interface>> {
let mut raw: *mut WLAN_INTERFACE_INFO_LIST = std::ptr::null_mut();
let status = unsafe { WlanEnumInterfaces(handle.0, None, &mut raw) };
if status != 0 {
return Err(IwconfError::WlanApi {
call: "WlanEnumInterfaces",
code: status,
});
}
let _guard = WlanBuf(raw);
let list = unsafe { &*raw };
if list.dwNumberOfItems == 0 {
return Err(IwconfError::NoInterfaces);
}
let friendly_names = super::adapters::friendly_names();
let base = list.InterfaceInfo.as_ptr();
let mut out = Vec::with_capacity(list.dwNumberOfItems as usize);
for i in 0..list.dwNumberOfItems as usize {
let info = unsafe { &*base.add(i) };
let guid = info.InterfaceGuid;
let connection = query_current_connection(handle, &guid).unwrap_or(None);
let description = wide_to_string(&info.strInterfaceDescription);
let name = super::adapters::lookup(&friendly_names, &guid)
.unwrap_or_else(|| description.clone());
out.push(Interface {
guid: guid_to_string(&guid),
name,
description,
state: interface_state_from(info.isState),
connection,
});
}
Ok(out)
}
fn guid_to_string(guid: &GUID) -> String {
format!("{:?}", guid)
}
pub fn query_current_connection(
handle: &WlanHandle,
guid: &GUID,
) -> Result<Option<ConnectionInfo>> {
let mut size = 0u32;
let mut data: *mut c_void = std::ptr::null_mut();
let status = unsafe {
WlanQueryInterface(
handle.0,
guid,
wlan_intf_opcode_current_connection,
None,
&mut size,
&mut data,
None,
)
};
if status != 0 {
return Ok(None);
}
let _guard = WlanBuf(data as *mut WLAN_CONNECTION_ATTRIBUTES);
if data.is_null() || (size as usize) < size_of::<WLAN_CONNECTION_ATTRIBUTES>() {
return Ok(None);
}
let attrs = unsafe { &*(data as *const WLAN_CONNECTION_ATTRIBUTES) };
let assoc = &attrs.wlanAssociationAttributes;
let sec = &attrs.wlanSecurityAttributes;
let ssid = ssid_to_string(&assoc.dot11Ssid);
let bssid = assoc
.dot11Bssid
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(":");
let (channel, band_ghz) = bss_channel_lookup(handle, guid, &assoc.dot11Ssid, &assoc.dot11Bssid)
.unwrap_or((None, None));
Ok(Some(ConnectionInfo {
ssid,
bssid,
network_type: "Infrastructure".to_string(),
radio_type: phy_type_str(assoc.dot11PhyType).to_string(),
authentication: auth_str(sec.dot11AuthAlgorithm).to_string(),
cipher: cipher_str(sec.dot11CipherAlgorithm).to_string(),
connection_mode: if attrs.wlanConnectionMode == wlan_connection_mode_profile {
"Auto Connect".to_string()
} else {
"Manual".to_string()
},
channel,
band_ghz,
rx_rate_mbps: Some(assoc.ulRxRate / 1000),
tx_rate_mbps: Some(assoc.ulTxRate / 1000),
signal_quality: Some(assoc.wlanSignalQuality.min(100) as u8),
profile_name: {
let name = wide_to_string(&attrs.strProfileName);
if name.is_empty() { None } else { Some(name) }
},
}))
}
fn freq_to_channel(freq_khz: u32) -> (u32, f32) {
let mhz = freq_khz / 1000;
if mhz == 2484 {
(14, 2.4)
} else if (2412..=2472).contains(&mhz) {
((mhz - 2407) / 5, 2.4)
} else if (5955..=7115).contains(&mhz) {
((mhz - 5950) / 5, 6.0)
} else if (5000..=5895).contains(&mhz) {
((mhz - 5000) / 5, 5.0)
} else {
(0, 0.0)
}
}
fn bss_channel_lookup(
handle: &WlanHandle,
guid: &GUID,
ssid: &DOT11_SSID,
bssid: &[u8; 6],
) -> Result<(Option<u32>, Option<f32>)> {
use windows::Win32::NetworkManagement::WiFi::WLAN_BSS_LIST;
use windows::Win32::NetworkManagement::WiFi::WlanGetNetworkBssList;
let mut raw: *mut WLAN_BSS_LIST = std::ptr::null_mut();
let status = unsafe {
WlanGetNetworkBssList(
handle.0,
guid,
Some(ssid as *const DOT11_SSID),
dot11_BSS_type_infrastructure,
false,
None,
&mut raw,
)
};
if status != 0 || raw.is_null() {
return Ok((None, None));
}
let _guard = WlanBuf(raw);
let list = unsafe { &*raw };
let base = list.wlanBssEntries.as_ptr();
for i in 0..list.dwNumberOfItems as usize {
let entry = unsafe { &*base.add(i) };
if &entry.dot11Bssid == bssid {
let (ch, band) = freq_to_channel(entry.ulChCenterFrequency);
return Ok((Some(ch), Some(band)));
}
}
Ok((None, None))
}
pub fn scan(handle: &WlanHandle, guid: &GUID) -> Result<()> {
let status = unsafe { WlanScan(handle.0, guid, None, None, None) };
if status != 0 {
return Err(IwconfError::WlanApi {
call: "WlanScan",
code: status,
});
}
Ok(())
}
pub fn available_networks(handle: &WlanHandle, guid: &GUID) -> Result<Vec<NetworkInfo>> {
let mut raw: *mut WLAN_AVAILABLE_NETWORK_LIST = std::ptr::null_mut();
let status = unsafe { WlanGetAvailableNetworkList(handle.0, guid, 0, None, &mut raw) };
if status != 0 {
return Err(IwconfError::WlanApi {
call: "WlanGetAvailableNetworkList",
code: status,
});
}
let _guard = WlanBuf(raw);
let list = unsafe { &*raw };
let base = list.Network.as_ptr();
let mut out = Vec::with_capacity(list.dwNumberOfItems as usize);
for i in 0..list.dwNumberOfItems as usize {
let net = unsafe { &*base.add(i) };
let profile = wide_to_string(&net.strProfileName);
out.push(NetworkInfo {
ssid: ssid_to_string(&net.dot11Ssid),
network_type: "Infrastructure".to_string(),
authentication: auth_str(net.dot11DefaultAuthAlgorithm).to_string(),
encryption: cipher_str(net.dot11DefaultCipherAlgorithm).to_string(),
signal_quality: net.wlanSignalQuality.min(100) as u8,
connectable: net.bNetworkConnectable.as_bool(),
already_connected: net.dwFlags & WLAN_AVAILABLE_NETWORK_CONNECTED != 0,
profile_name: if profile.is_empty() { None } else { Some(profile) },
});
}
Ok(out)
}
pub fn disconnect(handle: &WlanHandle, guid: &GUID) -> Result<()> {
let status = unsafe { WlanDisconnect(handle.0, guid, None) };
if status != 0 {
return Err(IwconfError::WlanApi {
call: "WlanDisconnect",
code: status,
});
}
Ok(())
}
fn build_profile_xml(ssid: &str, key: Option<&str>) -> String {
let hex: String = ssid.as_bytes().iter().map(|b| format!("{b:02X}")).collect();
let name = xml_escape(ssid);
match key {
Some(passphrase) => format!(
r#"<?xml version="1.0"?>
<WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
<name>{name}</name>
<SSIDConfig>
<SSID>
<hex>{hex}</hex>
<name>{name}</name>
</SSID>
</SSIDConfig>
<connectionType>ESS</connectionType>
<connectionMode>manual</connectionMode>
<MSM>
<security>
<authEncryption>
<authentication>WPA2PSK</authentication>
<encryption>AES</encryption>
<useOneX>false</useOneX>
</authEncryption>
<sharedKey>
<keyType>passPhrase</keyType>
<protected>false</protected>
<keyMaterial>{}</keyMaterial>
</sharedKey>
</security>
</MSM>
</WLANProfile>"#,
xml_escape(passphrase)
),
None => format!(
r#"<?xml version="1.0"?>
<WLANProfile xmlns="http://www.microsoft.com/networking/WLAN/profile/v1">
<name>{name}</name>
<SSIDConfig>
<SSID>
<hex>{hex}</hex>
<name>{name}</name>
</SSID>
</SSIDConfig>
<connectionType>ESS</connectionType>
<connectionMode>manual</connectionMode>
<MSM>
<security>
<authEncryption>
<authentication>open</authentication>
<encryption>none</encryption>
<useOneX>false</useOneX>
</authEncryption>
</security>
</MSM>
</WLANProfile>"#
),
}
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub fn connect(
handle: &WlanHandle,
guid: &GUID,
ssid: &str,
key: Option<&str>,
profile: Option<&str>,
timeout: Duration,
) -> Result<ConnectOutcome> {
let profile_name = if let Some(p) = profile {
p.to_string()
} else {
let xml = build_profile_xml(ssid, key);
let xml_h = HSTRING::from(xml.as_str());
let mut reason = 0u32;
let status = unsafe {
WlanSetProfile(
handle.0,
guid,
0,
PCWSTR(xml_h.as_ptr()),
PCWSTR::null(),
true,
None,
&mut reason,
)
};
if status != 0 {
return Err(IwconfError::WlanApi {
call: "WlanSetProfile",
code: status,
});
}
ssid.to_string()
};
let profile_h = HSTRING::from(profile_name.as_str());
let params = WLAN_CONNECTION_PARAMETERS {
wlanConnectionMode: wlan_connection_mode_profile,
strProfile: PCWSTR(profile_h.as_ptr()),
pDot11Ssid: std::ptr::null_mut(),
pDesiredBssidList: std::ptr::null_mut(),
dot11BssType: dot11_BSS_type_infrastructure,
dwFlags: 0,
};
let status = unsafe { WlanConnect(handle.0, guid, ¶ms, None) };
if status != 0 {
return Err(IwconfError::WlanApi {
call: "WlanConnect",
code: status,
});
}
let deadline = Instant::now() + timeout;
loop {
std::thread::sleep(Duration::from_millis(400));
if let Ok(Some(info)) = query_current_connection(handle, guid) {
if info.ssid == ssid {
return Ok(ConnectOutcome::Connected);
}
}
if Instant::now() >= deadline {
return Ok(ConnectOutcome::TimedOut);
}
}
}