blob: 4b57ace2b6fe2d90b16bbadcb07529c860374a86 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{format_err, Context as _, Error},
fidl::endpoints,
fidl_fuchsia_wlan_common::PowerSaveType,
fidl_fuchsia_wlan_common::{self as fidl_common, WlanMacRole},
fidl_fuchsia_wlan_common_security as fidl_security,
fidl_fuchsia_wlan_device_service::{
self as wlan_service, DeviceMonitorProxy, QueryIfaceResponse,
},
fidl_fuchsia_wlan_ieee80211 as fidl_ieee80211, fidl_fuchsia_wlan_internal as fidl_internal,
fidl_fuchsia_wlan_sme as fidl_sme,
fidl_fuchsia_wlan_sme::ConnectTransactionEvent,
fuchsia_zircon_status as zx_status, fuchsia_zircon_types as zx_sys,
futures::prelude::*,
ieee80211::{Bssid, MacAddr, MacAddrBytes, Ssid, NULL_ADDR},
itertools::Itertools,
std::fmt,
wlan_common::{
bss::{BssDescription, Protection},
scan::ScanResult,
security::{
wep::WepKey,
wpa::credential::{Passphrase, Psk},
SecurityError,
},
},
};
#[cfg(target_os = "fuchsia")]
use {hex::ToHex, wlan_rsn::psk};
pub mod opts;
use crate::opts::*;
type DeviceMonitor = DeviceMonitorProxy;
/// Context for negotiating an `Authentication` (security protocol and credentials).
///
/// This ephemeral type joins a BSS description with credential data to negotiate an
/// `Authentication`. See the `TryFrom` implementation below.
#[derive(Clone, Debug)]
struct SecurityContext {
pub bss: BssDescription,
pub unparsed_password_text: Option<String>,
pub unparsed_psk_text: Option<String>,
}
/// Negotiates an `Authentication` from security information (given credentials and a BSS
/// description). The security protocol is based on the protection information described by the BSS
/// description. This is used to parse and validate the given credentials.
///
/// This is necessary, because `wlandev` communicates directly with SME, which requires more
/// detailed information than the Policy layer.
impl TryFrom<SecurityContext> for fidl_security::Authentication {
type Error = SecurityError;
fn try_from(context: SecurityContext) -> Result<Self, SecurityError> {
/// Interprets the given password and PSK both as WPA credentials and attempts to parse the
/// pair.
///
/// Note that the given password can also represent a WEP key, so this function should only
/// be used in WPA contexts.
fn parse_wpa_credential_pair(
password: Option<String>,
psk: Option<String>,
) -> Result<fidl_security::Credentials, SecurityError> {
match (password, psk) {
(Some(password), None) => Passphrase::try_from(password)
.map(|passphrase| {
fidl_security::Credentials::Wpa(fidl_security::WpaCredentials::Passphrase(
passphrase.into(),
))
})
.map_err(From::from),
(None, Some(psk)) => Psk::parse(psk.as_bytes())
.map(|psk| {
fidl_security::Credentials::Wpa(fidl_security::WpaCredentials::Psk(
psk.into(),
))
})
.map_err(From::from),
_ => Err(SecurityError::Incompatible),
}
}
let SecurityContext { bss, unparsed_password_text, unparsed_psk_text } = context;
match bss.protection() {
// Unsupported.
// TODO(https://fxbug.dev/42174395): Implement conversions for WPA Enterprise.
Protection::Unknown | Protection::Wpa2Enterprise | Protection::Wpa3Enterprise => {
Err(SecurityError::Unsupported)
}
Protection::Open => match (unparsed_password_text, unparsed_psk_text) {
(None, None) => Ok(fidl_security::Authentication {
protocol: fidl_security::Protocol::Open,
credentials: None,
}),
_ => Err(SecurityError::Incompatible),
},
Protection::Wep => unparsed_password_text
.ok_or(SecurityError::Incompatible)
.and_then(|unparsed_password_text| {
WepKey::parse(unparsed_password_text.as_bytes()).map_err(From::from)
})
.map(|key| fidl_security::Authentication {
protocol: fidl_security::Protocol::Wep,
credentials: Some(Box::new(fidl_security::Credentials::Wep(
fidl_security::WepCredentials { key: key.into() },
))),
}),
Protection::Wpa1 => {
parse_wpa_credential_pair(unparsed_password_text, unparsed_psk_text).map(
|credentials| fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa1,
credentials: Some(Box::new(credentials)),
},
)
}
Protection::Wpa1Wpa2PersonalTkipOnly
| Protection::Wpa1Wpa2Personal
| Protection::Wpa2PersonalTkipOnly
| Protection::Wpa2Personal => {
parse_wpa_credential_pair(unparsed_password_text, unparsed_psk_text).map(
|credentials| fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa2Personal,
credentials: Some(Box::new(credentials)),
},
)
}
// Use WPA2 for transitional networks when a PSK is supplied.
Protection::Wpa2Wpa3Personal => {
parse_wpa_credential_pair(unparsed_password_text, unparsed_psk_text).map(
|credentials| match credentials {
fidl_security::Credentials::Wpa(
fidl_security::WpaCredentials::Passphrase(_),
) => fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa3Personal,
credentials: Some(Box::new(credentials)),
},
fidl_security::Credentials::Wpa(fidl_security::WpaCredentials::Psk(_)) => {
fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa2Personal,
credentials: Some(Box::new(credentials)),
}
}
_ => unreachable!(),
},
)
}
Protection::Wpa3Personal => match (unparsed_password_text, unparsed_psk_text) {
(Some(unparsed_password_text), None) => {
Passphrase::try_from(unparsed_password_text)
.map(|passphrase| fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa3Personal,
credentials: Some(Box::new(fidl_security::Credentials::Wpa(
fidl_security::WpaCredentials::Passphrase(passphrase.into()),
))),
})
.map_err(From::from)
}
_ => Err(SecurityError::Incompatible),
},
}
}
}
pub async fn handle_wlantool_command(monitor_proxy: DeviceMonitor, opt: Opt) -> Result<(), Error> {
match opt {
Opt::Phy(cmd) => do_phy(cmd, monitor_proxy).await,
Opt::Iface(cmd) => do_iface(cmd, monitor_proxy).await,
Opt::Client(opts::ClientCmd::Connect(cmd)) => do_client_connect(cmd, monitor_proxy).await,
Opt::Connect(cmd) => do_client_connect(cmd, monitor_proxy).await,
Opt::Client(opts::ClientCmd::Disconnect(cmd)) | Opt::Disconnect(cmd) => {
do_client_disconnect(cmd, monitor_proxy).await
}
Opt::Client(opts::ClientCmd::Scan(cmd)) => do_client_scan(cmd, monitor_proxy).await,
Opt::Scan(cmd) => do_client_scan(cmd, monitor_proxy).await,
Opt::Client(opts::ClientCmd::WmmStatus(cmd)) | Opt::WmmStatus(cmd) => {
do_client_wmm_status(cmd, monitor_proxy, &mut std::io::stdout()).await
}
Opt::Ap(cmd) => do_ap(cmd, monitor_proxy).await,
#[cfg(target_os = "fuchsia")]
Opt::Rsn(cmd) => do_rsn(cmd).await,
Opt::Status(cmd) => do_status(cmd, monitor_proxy).await,
}
}
async fn do_phy(cmd: opts::PhyCmd, monitor_proxy: DeviceMonitor) -> Result<(), Error> {
match cmd {
opts::PhyCmd::List => {
// TODO(tkilbourn): add timeouts to prevent hanging commands
let response = monitor_proxy.list_phys().await.context("error getting response")?;
println!("response: {:?}", response);
}
opts::PhyCmd::Query { phy_id } => {
let mac_roles = monitor_proxy
.get_supported_mac_roles(phy_id)
.await
.context("error querying MAC roles")?;
let device_path =
monitor_proxy.get_dev_path(phy_id).await.context("error querying device path")?;
println!("PHY ID: {}", phy_id);
println!("Device Path: {:?}", device_path);
println!("Supported MAC roles: {:?}", mac_roles);
}
opts::PhyCmd::GetCountry { phy_id } => {
let result =
monitor_proxy.get_country(phy_id).await.context("error getting country")?;
match result {
Ok(country) => {
println!("response: \"{}\"", std::str::from_utf8(&country.alpha2[..])?);
}
Err(status) => {
println!(
"response: Failed with status {:?}",
zx_status::Status::from_raw(status)
);
}
}
}
opts::PhyCmd::SetCountry { phy_id, country } => {
if !is_valid_country_str(&country) {
return Err(format_err!(
"Country string [{}] looks invalid: Should be 2 ASCII characters",
country
));
}
let mut alpha2 = [0u8; 2];
alpha2.copy_from_slice(country.as_bytes());
let req = wlan_service::SetCountryRequest { phy_id, alpha2 };
let response =
monitor_proxy.set_country(&req).await.context("error setting country")?;
println!("response: {:?}", zx_status::Status::from_raw(response));
}
opts::PhyCmd::ClearCountry { phy_id } => {
let req = wlan_service::ClearCountryRequest { phy_id };
let response =
monitor_proxy.clear_country(&req).await.context("error clearing country")?;
println!("response: {:?}", zx_status::Status::from_raw(response));
}
opts::PhyCmd::SetPowerSaveMode { phy_id, mode } => {
println!("SetPSMode: phy_id {:?} ps_mode {:?}", phy_id, mode);
let req = wlan_service::SetPowerSaveModeRequest { phy_id, ps_mode: mode.into() };
let response =
monitor_proxy.set_power_save_mode(&req).await.context("error setting ps mode")?;
println!("response: {:?}", zx_status::Status::from_raw(response));
}
opts::PhyCmd::GetPowerSaveMode { phy_id } => {
let result =
monitor_proxy.get_power_save_mode(phy_id).await.context("error getting ps mode")?;
match result {
Ok(resp) => match resp.ps_mode {
PowerSaveType::PsModePerformance => {
println!("PS Mode Off");
}
PowerSaveType::PsModeBalanced => {
println!("Medium PS Mode");
}
PowerSaveType::PsModeLowPower => {
println!("Low Ps Mode");
}
PowerSaveType::PsModeUltraLowPower => {
println!("Ultra low Ps Mode");
}
},
Err(status) => {
println!(
"response: Failed with status {:?}",
zx_status::Status::from_raw(status)
);
}
}
}
}
Ok(())
}
fn is_valid_country_str(country: &String) -> bool {
country.len() == 2 && country.chars().all(|x| x.is_ascii())
}
async fn do_iface(cmd: opts::IfaceCmd, monitor_proxy: DeviceMonitor) -> Result<(), Error> {
match cmd {
opts::IfaceCmd::New { phy_id, role, sta_addr } => {
let sta_addr = match sta_addr {
Some(s) => s.parse::<MacAddr>()?,
None => NULL_ADDR,
};
let req = wlan_service::CreateIfaceRequest {
phy_id,
role: role.into(),
sta_addr: sta_addr.to_array(),
};
let response =
monitor_proxy.create_iface(&req).await.context("error getting response")?;
println!("response: {:?}", response);
}
opts::IfaceCmd::Delete { iface_id } => {
let req = wlan_service::DestroyIfaceRequest { iface_id };
let response =
monitor_proxy.destroy_iface(&req).await.context("error destroying iface")?;
match zx_status::Status::ok(response) {
Ok(()) => println!("destroyed iface {:?}", iface_id),
Err(s) => println!("error destroying iface: {:?}", s),
}
}
opts::IfaceCmd::List => {
let response = monitor_proxy.list_ifaces().await.context("error getting response")?;
println!("response: {:?}", response);
}
opts::IfaceCmd::Query { iface_id } => {
let result =
monitor_proxy.query_iface(iface_id).await.context("error querying iface")?;
match result {
Ok(response) => println!("response: {}", format_iface_query_response(response)),
Err(err) => println!("error querying Iface {}: {}", iface_id, err),
}
}
opts::IfaceCmd::Minstrel(cmd) => match cmd {
opts::MinstrelCmd::List { iface_id: _ } => {
println!("List minstrel peers is not supported.");
}
opts::MinstrelCmd::Show { iface_id: _, peer_addr: _ } => {
println!("Show minstrel peer is not supported.");
}
},
opts::IfaceCmd::Status(cmd) => do_status(cmd, monitor_proxy).await?,
}
Ok(())
}
fn read_scan_result_vmo(_vmo: fidl::Vmo) -> Result<Vec<fidl_sme::ScanResult>, Error> {
#[cfg(target_os = "fuchsia")]
return wlan_common::scan::read_vmo(_vmo)
.map_err(|e| format_err!("failed to read VMO: {:?}", e));
#[cfg(not(target_os = "fuchsia"))]
return Err(format_err!("cannot read scan result VMO on host"));
}
async fn do_client_connect(
cmd: opts::ClientConnectCmd,
monitor_proxy: DeviceMonitorProxy,
) -> Result<(), Error> {
async fn try_get_bss_desc(
scan_result: fidl_sme::ClientSmeScanResult,
ssid: &Ssid,
) -> Result<fidl_internal::BssDescription, Error> {
let mut bss_description = None;
match scan_result {
Ok(vmo) => {
let scan_result_list = read_scan_result_vmo(vmo)?;
if bss_description.is_none() {
// Write the first matching `BssDescription`. Any additional information is
// ignored.
if let Some(bss_info) = scan_result_list.into_iter().find(|scan_result| {
// TODO(https://fxbug.dev/42164415): Until the error produced by
// `ScanResult::try_from` includes some details about the scan result
// which failed conversion, `scan_result` must be cloned for debug
// logging if conversion fails.
match ScanResult::try_from(scan_result.clone()) {
Ok(scan_result) => scan_result.bss_description.ssid == *ssid,
Err(e) => {
println!("Failed to convert ScanResult: {:?}", e);
println!(" {:?}", scan_result);
false
}
}
}) {
bss_description = Some(bss_info.bss_description);
}
}
}
Err(scan_error_code) => {
return Err(format_err!("failed to fetch scan result: {:?}", scan_error_code));
}
}
bss_description.ok_or_else(|| format_err!("failed to find BSS information for SSID"))
}
println!(
"The `connect` command performs an implicit scan. This behavior is DEPRECATED and in the \
future detailed BSS information will be required to connect! Use the `donut` tool to \
connect to networks using an SSID."
);
let opts::ClientConnectCmd { iface_id, ssid, password, psk, scan_type } = cmd;
let ssid = Ssid::try_from(ssid)?;
let sme = get_client_sme(monitor_proxy, iface_id).await?;
let req = match scan_type {
ScanTypeArg::Active => fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![ssid.to_vec()],
channels: vec![],
}),
ScanTypeArg::Passive => fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}),
};
let scan_result = sme.scan(&req).await.context("error sending scan request")?;
let bss_description = try_get_bss_desc(scan_result, &ssid).await?;
let authentication = match fidl_security::Authentication::try_from(SecurityContext {
unparsed_password_text: password,
unparsed_psk_text: psk,
bss: BssDescription::try_from(bss_description.clone())?,
}) {
Ok(authentication) => authentication,
Err(error) => {
println!("authentication error: {}", error);
return Ok(());
}
};
let (local, remote) = endpoints::create_proxy()?;
let req = fidl_sme::ConnectRequest {
ssid: ssid.to_vec(),
bss_description,
authentication,
deprecated_scan_type: scan_type.into(),
multiple_bss_candidates: false, // only used for metrics, select arbitrary value
};
sme.connect(&req, Some(remote)).context("error sending connect request")?;
handle_connect_transaction(local).await
}
async fn do_client_disconnect(
cmd: opts::ClientDisconnectCmd,
monitor_proxy: DeviceMonitor,
) -> Result<(), Error> {
let opts::ClientDisconnectCmd { iface_id } = cmd;
let sme = get_client_sme(monitor_proxy, iface_id).await?;
sme.disconnect(fidl_sme::UserDisconnectReason::WlanDevTool)
.await
.map_err(|e| format_err!("error sending disconnect request: {}", e))
}
async fn do_client_scan(
cmd: opts::ClientScanCmd,
monitor_proxy: DeviceMonitor,
) -> Result<(), Error> {
let opts::ClientScanCmd { iface_id, scan_type } = cmd;
let sme = get_client_sme(monitor_proxy, iface_id).await?;
let req = match scan_type {
ScanTypeArg::Passive => fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}),
ScanTypeArg::Active => fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
};
let scan_result = sme.scan(&req).await.context("error sending scan request")?;
print_scan_result(scan_result);
Ok(())
}
async fn print_iface_status(iface_id: u16, monitor_proxy: DeviceMonitor) -> Result<(), Error> {
let result = monitor_proxy
.query_iface(iface_id)
.await
.context("querying iface info")?
.map_err(|e| zx_status::Status::from_raw(e))?;
match result.role {
WlanMacRole::Client => {
let client_sme = get_client_sme(monitor_proxy, iface_id).await?;
let client_status_response = client_sme.status().await?;
match client_status_response {
fidl_sme::ClientStatusResponse::Connected(serving_ap_info) => {
println!(
"Iface {}: Connected to '{}' (bssid {}) channel: {:?} rssi: {}dBm snr: {}dB",
iface_id,
String::from_utf8_lossy(&serving_ap_info.ssid),
Bssid::from(serving_ap_info.bssid),
serving_ap_info.channel,
serving_ap_info.rssi_dbm,
serving_ap_info.snr_db,
);
}
fidl_sme::ClientStatusResponse::Connecting(ssid) => {
println!("Connecting to '{}'", String::from_utf8_lossy(&ssid));
}
fidl_sme::ClientStatusResponse::Idle(_) => {
println!("Iface {}: Not connected to a network", iface_id)
}
}
}
WlanMacRole::Ap => {
let sme = get_ap_sme(monitor_proxy, iface_id).await?;
let status = sme.status().await?;
println!(
"Iface {}: Running AP: {:?}",
iface_id,
status.running_ap.map(|ap| {
format!(
"ssid: {}, channel: {}, clients: {}",
String::from_utf8_lossy(&ap.ssid),
ap.channel,
ap.num_clients
)
})
);
}
WlanMacRole::Mesh => println!("Iface {}: Mesh not supported", iface_id),
fidl_fuchsia_wlan_common::WlanMacRoleUnknown!() => {
println!("Iface {}: Unknown WlanMacRole type {:?}", iface_id, result.role);
}
}
Ok(())
}
async fn do_status(cmd: opts::IfaceStatusCmd, monitor_proxy: DeviceMonitor) -> Result<(), Error> {
let ids = get_iface_ids(monitor_proxy.clone(), cmd.iface_id).await?;
if ids.len() == 0 {
return Err(format_err!("No iface found"));
}
for iface_id in ids {
if let Err(e) = print_iface_status(iface_id, monitor_proxy.clone()).await {
println!("Iface {}: Error querying status: {}", iface_id, e);
continue;
}
}
Ok(())
}
async fn do_client_wmm_status(
cmd: opts::ClientWmmStatusCmd,
monitor_proxy: DeviceMonitor,
stdout: &mut dyn std::io::Write,
) -> Result<(), Error> {
let sme = get_client_sme(monitor_proxy, cmd.iface_id).await?;
let wmm_status = sme
.wmm_status()
.await
.map_err(|e| format_err!("error sending WmmStatus request: {}", e))?;
match wmm_status {
Ok(wmm_status) => print_wmm_status(&wmm_status, stdout)?,
Err(code) => writeln!(stdout, "ClientSme::WmmStatus fails with status code: {}", code)?,
}
Ok(())
}
fn print_wmm_status(
wmm_status: &fidl_internal::WmmStatusResponse,
stdout: &mut dyn std::io::Write,
) -> Result<(), Error> {
writeln!(stdout, "apsd={}", wmm_status.apsd)?;
print_wmm_ac_params("ac_be", &wmm_status.ac_be_params, stdout)?;
print_wmm_ac_params("ac_bk", &wmm_status.ac_bk_params, stdout)?;
print_wmm_ac_params("ac_vi", &wmm_status.ac_vi_params, stdout)?;
print_wmm_ac_params("ac_vo", &wmm_status.ac_vo_params, stdout)?;
Ok(())
}
fn print_wmm_ac_params(
ac_name: &str,
ac_params: &fidl_internal::WmmAcParams,
stdout: &mut dyn std::io::Write,
) -> Result<(), Error> {
writeln!(stdout, "{ac_name}: aifsn={aifsn} acm={acm} ecw_min={ecw_min} ecw_max={ecw_max} txop_limit={txop_limit}",
ac_name=ac_name,
aifsn=ac_params.aifsn,
acm=ac_params.acm,
ecw_min=ac_params.ecw_min,
ecw_max=ac_params.ecw_max,
txop_limit=ac_params.txop_limit,
)?;
Ok(())
}
async fn do_ap(cmd: opts::ApCmd, monitor_proxy: DeviceMonitor) -> Result<(), Error> {
match cmd {
opts::ApCmd::Start { iface_id, ssid, password, channel } => {
let sme = get_ap_sme(monitor_proxy, iface_id).await?;
let config = fidl_sme::ApConfig {
ssid: ssid.as_bytes().to_vec(),
password: password.map_or(vec![], |p| p.as_bytes().to_vec()),
radio_cfg: fidl_sme::RadioConfig {
phy: PhyArg::Ht.into(),
channel: fidl_common::WlanChannel {
primary: channel,
cbw: CbwArg::Cbw20.into(),
secondary80: 0,
},
},
};
println!("{:?}", sme.start(&config).await?);
}
opts::ApCmd::Stop { iface_id } => {
let sme = get_ap_sme(monitor_proxy, iface_id).await?;
let r = sme.stop().await;
println!("{:?}", r);
}
}
Ok(())
}
#[cfg(target_os = "fuchsia")]
async fn do_rsn(cmd: opts::RsnCmd) -> Result<(), Error> {
match cmd {
opts::RsnCmd::GeneratePsk { passphrase, ssid } => {
println!("{}", generate_psk(&passphrase, &ssid)?);
}
}
Ok(())
}
#[cfg(target_os = "fuchsia")]
fn generate_psk(passphrase: &str, ssid: &str) -> Result<String, Error> {
let psk = psk::compute(passphrase.as_bytes(), &Ssid::try_from(ssid)?)?;
let mut psk_hex = String::new();
psk.write_hex(&mut psk_hex)?;
return Ok(psk_hex);
}
fn print_scan_result(scan_result: fidl_sme::ClientSmeScanResult) {
match scan_result {
Ok(vmo) => {
let scan_result_list = match read_scan_result_vmo(vmo) {
Ok(list) => list,
Err(e) => {
eprintln!("Failed to read VMO: {:?}", e);
return;
}
};
print_scan_header();
scan_result_list
.into_iter()
.filter_map(
// TODO(https://fxbug.dev/42164415): Until the error produced by
// ScanResult::TryFrom includes some details about the
// scan result which failed conversion, scan_result must
// be cloned for debug logging if conversion fails.
|scan_result| match ScanResult::try_from(scan_result.clone()) {
Ok(scan_result) => Some(scan_result),
Err(e) => {
eprintln!("Failed to convert ScanResult: {:?}", e);
eprintln!(" {:?}", scan_result);
None
}
},
)
.sorted_by(|a, b| a.bss_description.ssid.cmp(&b.bss_description.ssid))
.by_ref()
.for_each(|scan_result| print_one_scan_result(&scan_result));
}
Err(scan_error_code) => {
eprintln!("Error: {:?}", scan_error_code);
}
}
}
fn print_scan_line(
bssid: impl fmt::Display,
dbm: impl fmt::Display,
channel: impl fmt::Display,
protection: impl fmt::Display,
compat: impl fmt::Display,
ssid: impl fmt::Display,
) {
println!("{:17} {:>4} {:>6} {:12} {:10} {}", bssid, dbm, channel, protection, compat, ssid)
}
fn print_scan_header() {
print_scan_line("BSSID", "dBm", "Chan", "Protection", "Compatible", "SSID");
}
fn print_one_scan_result(scan_result: &wlan_common::scan::ScanResult) {
print_scan_line(
scan_result.bss_description.bssid,
scan_result.bss_description.rssi_dbm,
wlan_common::channel::Channel::from(scan_result.bss_description.channel),
scan_result.bss_description.protection(),
if scan_result.is_compatible() { "Y" } else { "N" },
scan_result.bss_description.ssid.to_string_not_redactable(),
);
}
async fn handle_connect_transaction(
connect_txn: fidl_sme::ConnectTransactionProxy,
) -> Result<(), Error> {
let mut events = connect_txn.take_event_stream();
while let Some(evt) = events
.try_next()
.await
.context("failed to receive connect result before the channel was closed")?
{
match evt {
ConnectTransactionEvent::OnConnectResult { result } => {
match (result.code, result.is_credential_rejected) {
(fidl_ieee80211::StatusCode::Success, _) => println!("Connected successfully"),
(fidl_ieee80211::StatusCode::Canceled, _) => {
eprintln!("Connecting was canceled or superseded by another command")
}
(code, true) => eprintln!("Credential rejected, status code: {:?}", code),
(code, false) => eprintln!("Failed to connect to network: {:?}", code),
}
break;
}
evt => {
eprintln!("Expected ConnectTransactionEvent::OnConnectResult event, got {:?}", evt);
}
}
}
Ok(())
}
/// Constructs a `Result<(), Error>` from a `zx::zx_status_t` returned
/// from one of the `get_client_sme` or `get_ap_sme`
/// functions. In particular, when `zx_status::Status::from_raw(raw_status)` does
/// not match `zx_status::Status::OK`, this function will attach the appropriate
/// error message to the returned `Result`. When `zx_status::Status::from_raw(raw_status)`
/// does match `zx_status::Status::OK`, this function returns `Ok()`.
///
/// If this function returns an `Err`, it includes both a cause and a context.
/// The cause is a readable conversion of `raw_status` based on `station_mode`
/// and `iface_id`. The context notes the failed operation and suggests the
/// interface be checked for support of the given `station_mode`.
fn error_from_sme_raw_status(
raw_status: zx_sys::zx_status_t,
station_mode: WlanMacRole,
iface_id: u16,
) -> Error {
match zx_status::Status::from_raw(raw_status) {
zx_status::Status::OK => Error::msg("Unexpected OK error"),
zx_status::Status::NOT_FOUND => Error::msg("invalid interface id"),
zx_status::Status::NOT_SUPPORTED => Error::msg("operation not supported on SME interface"),
zx_status::Status::INTERNAL => {
Error::msg("internal server error sending endpoint to the SME server future")
}
_ => Error::msg("unrecognized error associated with SME interface"),
}
.context(format!(
"Failed to access {:?} for interface id {}. \
Please ensure the selected iface supports {:?} mode.",
station_mode, iface_id, station_mode,
))
}
async fn get_client_sme(
monitor_proxy: DeviceMonitor,
iface_id: u16,
) -> Result<fidl_sme::ClientSmeProxy, Error> {
let (proxy, remote) = endpoints::create_proxy()?;
monitor_proxy
.get_client_sme(iface_id, remote)
.await
.context("error sending GetClientSme request")?
.map_err(|e| error_from_sme_raw_status(e, WlanMacRole::Client, iface_id))?;
Ok(proxy)
}
async fn get_ap_sme(
monitor_proxy: DeviceMonitor,
iface_id: u16,
) -> Result<fidl_sme::ApSmeProxy, Error> {
let (proxy, remote) = endpoints::create_proxy()?;
monitor_proxy
.get_ap_sme(iface_id, remote)
.await
.context("error sending GetApSme request")?
.map_err(|e| error_from_sme_raw_status(e, WlanMacRole::Ap, iface_id))?;
Ok(proxy)
}
async fn get_iface_ids(
monitor_proxy: DeviceMonitor,
iface_id: Option<u16>,
) -> Result<Vec<u16>, Error> {
match iface_id {
Some(id) => Ok(vec![id]),
None => monitor_proxy.list_ifaces().await.context("error listing ifaces"),
}
}
fn format_iface_query_response(resp: QueryIfaceResponse) -> String {
format!(
"QueryIfaceResponse {{ role: {:?}, id: {}, phy_id: {}, phy_assigned_id: {}, sta_addr: {} }}",
resp.role,
resp.id,
resp.phy_id,
resp.phy_assigned_id,
MacAddr::from(resp.sta_addr)
)
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::endpoints::create_proxy,
fidl_fuchsia_wlan_device_service::DeviceMonitorMarker,
fuchsia_async as fasync,
futures::task::Poll,
ieee80211::SsidError,
std::pin::pin,
wlan_common::{assert_variant, fake_bss_description},
};
#[fuchsia::test]
fn negotiate_authentication() {
let bss = fake_bss_description!(Open);
assert_eq!(
fidl_security::Authentication::try_from(SecurityContext {
unparsed_password_text: None,
unparsed_psk_text: None,
bss
}),
Ok(fidl_security::Authentication {
protocol: fidl_security::Protocol::Open,
credentials: None
}),
);
let bss = fake_bss_description!(Wpa1);
assert_eq!(
fidl_security::Authentication::try_from(SecurityContext {
unparsed_password_text: Some(String::from("password")),
unparsed_psk_text: None,
bss,
}),
Ok(fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa1,
credentials: Some(Box::new(fidl_security::Credentials::Wpa(
fidl_security::WpaCredentials::Passphrase(b"password".to_vec())
))),
}),
);
let bss = fake_bss_description!(Wpa2);
let psk = String::from("f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e");
assert_eq!(
fidl_security::Authentication::try_from(SecurityContext {
unparsed_password_text: None,
unparsed_psk_text: Some(psk),
bss
}),
Ok(fidl_security::Authentication {
protocol: fidl_security::Protocol::Wpa2Personal,
credentials: Some(Box::new(fidl_security::Credentials::Wpa(
fidl_security::WpaCredentials::Psk([
0xf4, 0x2c, 0x6f, 0xc5, 0x2d, 0xf0, 0xeb, 0xef, 0x9e, 0xbb, 0x4b, 0x90,
0xb3, 0x8a, 0x5f, 0x90, 0x2e, 0x83, 0xfe, 0x1b, 0x13, 0x5a, 0x70, 0xe2,
0x3a, 0xed, 0x76, 0x2e, 0x97, 0x10, 0xa1, 0x2e,
])
))),
}),
);
let bss = fake_bss_description!(Wpa2);
let psk = String::from("f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e");
assert!(matches!(
fidl_security::Authentication::try_from(SecurityContext {
unparsed_password_text: Some(String::from("password")),
unparsed_psk_text: Some(psk),
bss,
}),
Err(_),
));
}
#[fuchsia::test]
fn destroy_iface() {
let mut exec = fasync::TestExecutor::new();
let (monitor_svc_local, monitor_svc_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_svc_stream =
monitor_svc_remote.into_stream().expect("failed to create stream");
let del_fut = do_iface(IfaceCmd::Delete { iface_id: 5 }, monitor_svc_local);
let mut del_fut = pin!(del_fut);
assert_variant!(exec.run_until_stalled(&mut del_fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut monitor_svc_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::DestroyIface {
req, responder
}))) => {
assert_eq!(req.iface_id, 5);
responder.send(zx_status::Status::OK.into_raw()).expect("failed to send response");
}
);
}
#[fuchsia::test]
fn test_country_input() {
assert!(is_valid_country_str(&"RS".to_string()));
assert!(is_valid_country_str(&"00".to_string()));
assert!(is_valid_country_str(&"M1".to_string()));
assert!(is_valid_country_str(&"-M".to_string()));
assert!(!is_valid_country_str(&"ABC".to_string()));
assert!(!is_valid_country_str(&"X".to_string()));
assert!(!is_valid_country_str(&"❤".to_string()));
}
#[fuchsia::test]
fn test_get_country() {
let mut exec = fasync::TestExecutor::new();
let (monitor_svc_local, monitor_svc_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_svc_stream =
monitor_svc_remote.into_stream().expect("failed to create stream");
let fut = do_phy(PhyCmd::GetCountry { phy_id: 45 }, monitor_svc_local);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut monitor_svc_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::GetCountry {
phy_id, responder,
}))) => {
assert_eq!(phy_id, 45);
responder.send(
Ok(&fidl_fuchsia_wlan_device_service::GetCountryResponse {
alpha2: [40u8, 40u8],
})).expect("failed to send response");
}
);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(Ok(())));
}
#[fuchsia::test]
fn test_set_country() {
let mut exec = fasync::TestExecutor::new();
let (monitor_svc_local, monitor_svc_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_svc_stream =
monitor_svc_remote.into_stream().expect("failed to create stream");
let fut =
do_phy(PhyCmd::SetCountry { phy_id: 45, country: "RS".to_string() }, monitor_svc_local);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut monitor_svc_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::SetCountry {
req, responder,
}))) => {
assert_eq!(req.phy_id, 45);
assert_eq!(req.alpha2, "RS".as_bytes());
responder.send(zx_status::Status::OK.into_raw()).expect("failed to send response");
}
);
}
#[fuchsia::test]
fn test_clear_country() {
let mut exec = fasync::TestExecutor::new();
let (monitor_svc_local, monitor_svc_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_svc_stream =
monitor_svc_remote.into_stream().expect("failed to create stream");
let fut = do_phy(PhyCmd::ClearCountry { phy_id: 45 }, monitor_svc_local);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut monitor_svc_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::ClearCountry {
req, responder,
}))) => {
assert_eq!(req.phy_id, 45);
responder.send(zx_status::Status::OK.into_raw()).expect("failed to send response");
}
);
}
#[fuchsia::test]
fn test_get_power_save_mode() {
let mut exec = fasync::TestExecutor::new();
let (monitor_svc_local, monitor_svc_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_svc_stream =
monitor_svc_remote.into_stream().expect("failed to create stream");
let fut = do_phy(PhyCmd::GetPowerSaveMode { phy_id: 45 }, monitor_svc_local);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut monitor_svc_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::GetPowerSaveMode {
phy_id, responder,
}))) => {
assert_eq!(phy_id, 45);
responder.send(
Ok(&fidl_fuchsia_wlan_device_service::GetPowerSaveModeResponse {
ps_mode: PowerSaveType::PsModePerformance,
})).expect("failed to send response");
}
);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(Ok(())));
}
#[fuchsia::test]
fn test_set_power_save_mode() {
let mut exec = fasync::TestExecutor::new();
let (monitor_svc_local, monitor_svc_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_svc_stream =
monitor_svc_remote.into_stream().expect("failed to create stream");
let fut = do_phy(
PhyCmd::SetPowerSaveMode { phy_id: 45, mode: PsModeArg::PsModeBalanced },
monitor_svc_local,
);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut monitor_svc_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::SetPowerSaveMode {
req, responder,
}))) => {
assert_eq!(req.phy_id, 45);
assert_eq!(req.ps_mode, PowerSaveType::PsModeBalanced);
responder.send(zx_status::Status::OK.into_raw()).expect("failed to send response");
}
);
}
#[fuchsia::test]
fn test_generate_psk() {
assert_eq!(
generate_psk("12345678", "coolnet").unwrap(),
"1ec9ee30fdff1961a9abd083f571464cc0fe27f62f9f59992bd39f8e625e9f52"
);
assert!(generate_psk("short", "coolnet").is_err());
}
fn has_expected_cause(error: Error, message: &str) -> bool {
error.chain().any(|cause| cause.to_string() == message)
}
#[fuchsia::test]
fn test_error_from_sme_raw_status() {
let not_found = error_from_sme_raw_status(
zx_status::Status::NOT_FOUND.into_raw(),
WlanMacRole::Mesh,
1,
);
let not_supported = error_from_sme_raw_status(
zx_status::Status::NOT_SUPPORTED.into_raw(),
WlanMacRole::Ap,
2,
);
let internal_error = error_from_sme_raw_status(
zx_status::Status::INTERNAL.into_raw(),
WlanMacRole::Client,
3,
);
let unrecognized_error = error_from_sme_raw_status(
zx_status::Status::INTERRUPTED_RETRY.into_raw(),
WlanMacRole::Mesh,
4,
);
assert!(has_expected_cause(not_found, "invalid interface id"));
assert!(has_expected_cause(not_supported, "operation not supported on SME interface"));
assert!(has_expected_cause(
internal_error,
"internal server error sending endpoint to the SME server future"
));
assert!(has_expected_cause(
unrecognized_error,
"unrecognized error associated with SME interface"
));
}
#[fuchsia::test]
fn reject_connect_ssid_too_long() {
let mut exec = fasync::TestExecutor::new();
let (monitor_local, monitor_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_stream = monitor_remote.into_stream().expect("failed to create stream");
// SSID is one byte too long.
let cmd = opts::ClientConnectCmd {
iface_id: 0,
ssid: String::from_utf8(vec![65; 33]).unwrap(),
password: None,
psk: None,
scan_type: opts::ScanTypeArg::Passive,
};
let connect_fut = do_client_connect(cmd, monitor_local.clone());
let mut connect_fut = pin!(connect_fut);
assert_variant!(exec.run_until_stalled(&mut connect_fut), Poll::Ready(Err(e)) => {
assert_eq!(format!("{}", e), format!("{}", SsidError::Size(33)));
});
// No connect request is sent to SME because the command is invalid and rejected.
assert_variant!(exec.run_until_stalled(&mut monitor_stream.next()), Poll::Pending);
}
#[fuchsia::test]
fn test_wmm_status() {
let mut exec = fasync::TestExecutor::new();
let (monitor_local, monitor_remote) =
create_proxy::<DeviceMonitorMarker>().expect("failed to create DeviceMonitor service");
let mut monitor_stream = monitor_remote.into_stream().expect("failed to create stream");
let mut stdout = Vec::new();
{
let fut = do_client_wmm_status(
ClientWmmStatusCmd { iface_id: 11 },
monitor_local,
&mut stdout,
);
let mut fut = pin!(fut);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
let mut fake_sme_server_stream = assert_variant!(
exec.run_until_stalled(&mut monitor_stream.next()),
Poll::Ready(Some(Ok(wlan_service::DeviceMonitorRequest::GetClientSme {
iface_id, sme_server, responder,
}))) => {
assert_eq!(iface_id, 11);
responder.send(Ok(())).expect("failed to send GetClientSme response");
sme_server.into_stream().expect("sme server stream failed")
}
);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Pending);
assert_variant!(
exec.run_until_stalled(&mut fake_sme_server_stream.next()),
Poll::Ready(Some(Ok(fidl_sme::ClientSmeRequest::WmmStatus { responder }))) => {
let wmm_status_resp = fidl_internal::WmmStatusResponse {
apsd: true,
ac_be_params: fidl_internal::WmmAcParams {
aifsn: 1,
acm: false,
ecw_min: 2,
ecw_max: 3,
txop_limit: 4,
},
ac_bk_params: fidl_internal::WmmAcParams {
aifsn: 5,
acm: false,
ecw_min: 6,
ecw_max: 7,
txop_limit: 8,
},
ac_vi_params: fidl_internal::WmmAcParams {
aifsn: 9,
acm: true,
ecw_min: 10,
ecw_max: 11,
txop_limit: 12,
},
ac_vo_params: fidl_internal::WmmAcParams {
aifsn: 13,
acm: true,
ecw_min: 14,
ecw_max: 15,
txop_limit: 16,
},
};
responder.send(Ok(&wmm_status_resp)).expect("failed to send WMM status response");
}
);
assert_variant!(exec.run_until_stalled(&mut fut), Poll::Ready(Ok(())));
}
assert_eq!(
String::from_utf8(stdout).expect("expect valid UTF8"),
"apsd=true\n\
ac_be: aifsn=1 acm=false ecw_min=2 ecw_max=3 txop_limit=4\n\
ac_bk: aifsn=5 acm=false ecw_min=6 ecw_max=7 txop_limit=8\n\
ac_vi: aifsn=9 acm=true ecw_min=10 ecw_max=11 txop_limit=12\n\
ac_vo: aifsn=13 acm=true ecw_min=14 ecw_max=15 txop_limit=16\n"
);
}
}