blob: e571d0e469016b5352baf7af2e7563d807e2c1a1 [file] [log] [blame]
// Copyright 2018 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 {
crate::client::{DeviceInfo, Ssid},
fidl_fuchsia_wlan_common as fidl_common,
fidl_fuchsia_wlan_mlme::{self as fidl_mlme, BssDescription, ScanRequest, ScanResultCodes},
fidl_fuchsia_wlan_sme as fidl_sme,
log::error,
std::{
collections::{hash_map, HashMap, HashSet},
mem,
sync::Arc,
},
wlan_common::{
bss::BssDescriptionExt,
channel::{Cbw, Channel},
ie,
},
};
const PASSIVE_SCAN_CHANNEL_MS: u32 = 200;
const ACTIVE_SCAN_PROBE_DELAY_MS: u32 = 5;
const ACTIVE_SCAN_CHANNEL_MS: u32 = 75;
type BssId = [u8; 6];
// Scans can be performed for two different purposes:
// 1) Discover available wireless networks. These scans are initiated by the "user",
// i.e. a client process that talks to SME, and thus have 'tokens' to identify
// a specific user request.
// 2) Join a specific network. These scans can only be initiated by the SME state machine
// itself.
// An SME-initiated scan request for the purpose of joining a network
#[derive(Debug, PartialEq)]
pub struct JoinScan<T> {
pub ssid: Ssid,
pub token: T,
pub scan_request: fidl_sme::ScanRequest,
}
// A "user"-initiated scan request for the purpose of discovering available networks
#[derive(Debug, PartialEq)]
pub struct DiscoveryScan<T> {
tokens: Vec<T>,
scan_request: fidl_sme::ScanRequest,
}
impl<T> DiscoveryScan<T> {
pub fn new(token: T, scan_request: fidl_sme::ScanRequest) -> Self {
Self { tokens: vec![token], scan_request }
}
pub fn matches(&self, scan: &DiscoveryScan<T>) -> bool {
self.scan_request == scan.scan_request
}
pub fn merges(&mut self, mut scan: DiscoveryScan<T>) {
self.tokens.append(&mut scan.tokens)
}
}
pub struct ScanScheduler<D, J> {
// The currently running scan. We assume that MLME can handle a single concurrent scan
// regardless of its own state.
current: ScanState<D, J>,
// A pending scan request for the purpose of joining a network. This type of request
// always takes priority over discovery scans. There can only be one such pending request:
// if SME requests another one, the newer one wins and overwrites an existing request.
pending_join: Option<JoinScan<J>>,
// Pending discovery requests from the user
pending_discovery: Vec<DiscoveryScan<D>>,
device_info: Arc<DeviceInfo>,
last_mlme_txn_id: u64,
}
#[derive(Debug, PartialEq)]
enum ScanState<D, J> {
NotScanning,
// Join scan is canceled, but we are still waiting for it to complete. This state is to make
// sure that we don't schedule another scan until the lower layer finishes scanning.
StaleJoinScan {
mlme_txn_id: u64,
},
ScanningToJoin {
cmd: JoinScan<J>,
mlme_txn_id: u64,
bss_map: HashMap<BssId, BssDescription>,
},
ScanningToDiscover {
cmd: DiscoveryScan<D>,
mlme_txn_id: u64,
bss_map: HashMap<BssId, BssDescription>,
},
}
// A reaction to MLME's ScanConfirm event
pub enum ScanResult<D, J> {
// No reaction: the scan results are not relevant anymore, or we received
// an unexpected ScanConfirm.
None,
// "Join" scan has finished, either successfully or not.
// The SME state machine can now send a JoinRequest to MLME if it desires so.
JoinScanFinished { token: J, result: Result<Vec<BssDescription>, ScanResultCodes> },
// "Discovery" scan has finished, either successfully or not.
// SME is expected to forward the result to the user.
DiscoveryFinished { tokens: Vec<D>, result: Result<Vec<BssDescription>, ScanResultCodes> },
}
impl<D, J> ScanScheduler<D, J> {
pub fn new(device_info: Arc<DeviceInfo>) -> Self {
ScanScheduler {
current: ScanState::NotScanning,
pending_join: None,
pending_discovery: Vec::new(),
device_info,
last_mlme_txn_id: 0,
}
}
// Initiate a "join" scan.
//
// If there is an existing pending "join" scan request, it will be discarded and replaced
// with the new one. In this case, the token of the discarded request will be returned.
//
// If there is a currently running "join" scan, its results will be discarded, too.
//
// The scan might or might not begin immediately.
// If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
pub fn enqueue_scan_to_join(&mut self, s: JoinScan<J>) -> (Option<J>, Option<ScanRequest>) {
let old_ongoing_token = self.mark_stale_ongoing_join_scan();
let old_pending_token = mem::replace(&mut self.pending_join, Some(s)).map(|p| p.token);
// There should never be both old ongoing token and pending token. This is because if
// a JoinScan is in pending slot, then either there's no ongoing scan or the ongoing
// scan was already marked stale previously
let old_token = old_ongoing_token.or(old_pending_token);
(old_token, self.start_next_scan())
}
// Mark a join scan as stale. This is effectively a "cancel" operation when it comes to scan
// scheduler's internal state (though no cancel scan request is forwarded to lower layer)
fn mark_stale_ongoing_join_scan(&mut self) -> Option<J> {
if let ScanState::ScanningToJoin { mlme_txn_id, .. } = self.current {
let old = mem::replace(&mut self.current, ScanState::StaleJoinScan { mlme_txn_id });
if let ScanState::ScanningToJoin { cmd, .. } = old {
return Some(cmd.token);
}
}
None
}
// Initiate a "discovery" scan. The scan might or might not begin immediately.
// The request can be merged with any pending or ongoing requests.
// If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
pub fn enqueue_scan_to_discover(&mut self, s: DiscoveryScan<D>) -> Option<ScanRequest> {
if let ScanState::ScanningToDiscover { cmd, .. } = &mut self.current {
if cmd.matches(&s) {
cmd.merges(s);
return None;
}
}
if let Some(scan_cmd) = self.pending_discovery.iter_mut().find(|cmd| cmd.matches(&s)) {
scan_cmd.merges(s);
return None;
}
self.pending_discovery.push(s);
self.start_next_scan()
}
// Should be called for every OnScanResult event received from MLME.
pub fn on_mlme_scan_result(&mut self, msg: fidl_mlme::ScanResult) {
if !self.matching_mlme_txn_id(msg.txn_id) {
error!("rejecting scan result with wrong txn id");
return;
}
match &mut self.current {
ScanState::NotScanning | ScanState::StaleJoinScan { .. } => {}
ScanState::ScanningToJoin { cmd, bss_map, .. } => {
if cmd.ssid == msg.bss.ssid {
maybe_insert_bss(bss_map, msg.bss);
}
}
ScanState::ScanningToDiscover { bss_map, .. } => {
maybe_insert_bss(bss_map, msg.bss);
}
}
}
// Should be called for every OnScanEnd event received from MLME.
// The caller is expected to take action based on the returned ScanResult.
// If a ScanRequest is returned, the caller is responsible for forwarding it to MLME.
pub fn on_mlme_scan_end(
&mut self,
msg: fidl_mlme::ScanEnd,
) -> (ScanResult<D, J>, Option<ScanRequest>) {
if !self.matching_mlme_txn_id(msg.txn_id) {
return (ScanResult::None, None);
}
let old_state = mem::replace(&mut self.current, ScanState::NotScanning);
let result = match old_state {
ScanState::NotScanning | ScanState::StaleJoinScan { .. } => ScanResult::None,
ScanState::ScanningToJoin { cmd, bss_map, .. } => {
let result = match msg.code {
ScanResultCodes::Success => Ok(bss_map.into_iter().map(|(_, v)| v).collect()),
other => Err(other),
};
ScanResult::JoinScanFinished { token: cmd.token, result }
}
ScanState::ScanningToDiscover { cmd, bss_map, .. } => ScanResult::DiscoveryFinished {
tokens: cmd.tokens,
result: match msg.code {
ScanResultCodes::Success => Ok(bss_map.into_iter().map(|(_, v)| v).collect()),
other => Err(other),
},
},
};
let request = self.start_next_scan();
(result, request)
}
// Returns the most recent join scan request, if there is one.
pub fn get_join_scan(&self) -> Option<&JoinScan<J>> {
if let Some(s) = &self.pending_join {
Some(s)
} else if let ScanState::ScanningToJoin { cmd, .. } = &self.current {
Some(cmd)
} else {
None
}
}
pub fn is_scanning_to_join(&self) -> bool {
match self.current {
ScanState::ScanningToJoin { .. } | ScanState::StaleJoinScan { .. } => true,
_ => false,
}
}
fn matching_mlme_txn_id(&self, incoming_txn_id: u64) -> bool {
match &self.current {
ScanState::NotScanning => false,
ScanState::StaleJoinScan { mlme_txn_id }
| ScanState::ScanningToJoin { mlme_txn_id, .. }
| ScanState::ScanningToDiscover { mlme_txn_id, .. } => *mlme_txn_id == incoming_txn_id,
}
}
fn start_next_scan(&mut self) -> Option<ScanRequest> {
match &self.current {
ScanState::NotScanning => {
if let Some(join_scan) = self.pending_join.take() {
self.last_mlme_txn_id += 1;
let request =
new_join_scan_request(self.last_mlme_txn_id, &join_scan, &self.device_info);
self.current = ScanState::ScanningToJoin {
cmd: join_scan,
mlme_txn_id: self.last_mlme_txn_id,
bss_map: HashMap::new(),
};
Some(request)
} else if !self.pending_discovery.is_empty() {
self.last_mlme_txn_id += 1;
let scan_cmd = self.pending_discovery.remove(0);
let request = new_discovery_scan_request(
self.last_mlme_txn_id,
&scan_cmd,
&self.device_info,
);
self.current = ScanState::ScanningToDiscover {
cmd: scan_cmd,
mlme_txn_id: self.last_mlme_txn_id,
bss_map: HashMap::new(),
};
Some(request)
} else {
None
}
}
_ => None,
}
}
}
fn maybe_insert_bss(bss_map: &mut HashMap<BssId, BssDescription>, bss: BssDescription) {
match bss_map.entry(bss.bssid) {
hash_map::Entry::Occupied(mut entry) => {
let existing_bss = entry.get_mut();
let existing_has_manufacturer = existing_bss.has_wsc_attr(ie::wsc::Id::MANUFACTURER);
let new_has_manufacturer = bss.has_wsc_attr(ie::wsc::Id::MANUFACTURER);
// Do not replace the BSS in the BSS map if we are going to lose information.
// It's likely that the new scan result comes from a beacon while the old scan
// result comes from a probe response.
if existing_has_manufacturer && !new_has_manufacturer {
return;
}
*existing_bss = bss;
}
hash_map::Entry::Vacant(entry) => {
entry.insert(bss);
}
}
}
const WILDCARD_BSS_ID: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
fn new_scan_request(
mlme_txn_id: u64,
scan_request: fidl_sme::ScanRequest,
ssid: Vec<u8>,
device_info: &DeviceInfo,
) -> ScanRequest {
let scan_req = ScanRequest {
txn_id: mlme_txn_id,
bss_type: fidl_mlme::BssTypes::Infrastructure,
bssid: WILDCARD_BSS_ID.clone(),
ssid,
scan_type: fidl_mlme::ScanTypes::Passive,
probe_delay: 0,
channel_list: Some(get_channels_to_scan(&device_info, &scan_request)),
min_channel_time: PASSIVE_SCAN_CHANNEL_MS,
max_channel_time: PASSIVE_SCAN_CHANNEL_MS,
ssid_list: None,
};
match scan_request {
fidl_sme::ScanRequest::Active(active_scan_params) => ScanRequest {
scan_type: fidl_mlme::ScanTypes::Active,
probe_delay: ACTIVE_SCAN_PROBE_DELAY_MS,
min_channel_time: ACTIVE_SCAN_CHANNEL_MS,
max_channel_time: ACTIVE_SCAN_CHANNEL_MS,
ssid_list: if active_scan_params.ssids.len() > 0 {
Some(active_scan_params.ssids)
} else {
None
},
..scan_req
},
fidl_sme::ScanRequest::Passive(_) => scan_req,
}
}
fn new_join_scan_request<T>(
mlme_txn_id: u64,
join_scan: &JoinScan<T>,
device_info: &DeviceInfo,
) -> ScanRequest {
new_scan_request(
mlme_txn_id,
join_scan.scan_request.clone(),
join_scan.ssid.clone(),
device_info,
)
}
fn new_discovery_scan_request<T>(
mlme_txn_id: u64,
discovery_scan: &DiscoveryScan<T>,
device_info: &DeviceInfo,
) -> ScanRequest {
new_scan_request(mlme_txn_id, discovery_scan.scan_request.clone(), vec![], device_info)
}
/// Get channels to scan depending on device's capability and scan type. If scan type is passive,
/// or if scan type is active but the device handles DFS channels, then the channels returned by
/// this function are the intersection of device's supported channels and Fuchsia supported
/// channels. If scan type is active and the device doesn't handle DFS channels, then the return
/// value excludes DFS channels.
///
/// Example:
///
/// Suppose that Fuchsia supported channels are [1, 2, 52], and 1, 2 are non-DFS channels while
/// 112 is DFS channel. Also suppose that device's supported channels are [1, 52] as parameter
/// to DeviceInfo below.
///
/// ScanType | Device handles DFS | Return values
/// ---------+--------------------+-----------------
/// Passive | Y | [1, 52]
/// Passive | N | [1, 52]
/// Active | Y | [1, 52]
/// Active | N | [1]
fn get_channels_to_scan(device_info: &DeviceInfo, scan_request: &fidl_sme::ScanRequest) -> Vec<u8> {
let mut device_supported_channels: HashSet<u8> = HashSet::new();
for band in &device_info.bands {
device_supported_channels.extend(&band.channels);
}
let supports_dfs = device_info.driver_features.contains(&fidl_common::DriverFeature::Dfs);
SUPPORTED_CHANNELS
.iter()
.filter(|chan| device_supported_channels.contains(chan))
.filter(|chan| {
if let &fidl_sme::ScanRequest::Passive(_) = scan_request {
return true;
};
if supports_dfs {
return true;
};
!Channel::new(**chan, Cbw::Cbw20).is_dfs()
})
.filter(|chan| {
// If this is an active scan and there are any channels specified by the caller,
// only include those channels.
if let &fidl_sme::ScanRequest::Active(ref options) = scan_request {
if !options.channels.is_empty() {
return options.channels.contains(chan);
}
}
true
})
.map(|chan| *chan)
.collect()
}
const SUPPORTED_CHANNELS: &[u8] = &[
// 5GHz UNII-1
36, 40, 44, 48, // 5GHz UNII-2 Middle
52, 56, 60, 64, // 5GHz UNII-2 Extended
100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 144, // 5GHz UNII-3
149, 153, 157, 161, 165, // 2GHz
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
];
#[cfg(test)]
mod tests {
use super::*;
use crate::client::test_utils::{
fake_bss_with_bssid, fake_bss_with_vendor_ies, fake_unprotected_bss_description,
};
use crate::clone_utils::clone_bss_desc;
use crate::test_utils;
use wlan_common::assert_variant;
const CLIENT_ADDR: [u8; 6] = [0x7A, 0xE7, 0x76, 0xD9, 0xF2, 0x67];
fn passive_discovery_scan(token: i32) -> DiscoveryScan<i32> {
DiscoveryScan::new(token, fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}))
}
#[test]
fn discovery_scan() {
let mut sched = create_sched();
let req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = req.txn_id;
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id: txn_id + 100, // mismatching transaction id
bss: fake_unprotected_bss_description(b"bar".to_vec()),
});
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_bss_with_bssid(b"qux".to_vec(), [3; 6]),
});
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
assert!(req.is_none());
let (tokens, result) = assert_variant!(result,
ScanResult::DiscoveryFinished { tokens, result } => (tokens, result),
"expected discovery scan to be completed"
);
assert_eq!(vec![10], tokens);
let mut ssid_list = result
.expect("expected a successful scan result")
.into_iter()
.map(|bss| bss.ssid)
.collect::<Vec<_>>();
ssid_list.sort();
assert_eq!(vec![b"foo".to_vec(), b"qux".to_vec()], ssid_list);
}
#[test]
fn discovery_scan_deduplicate_bssid() {
let mut sched = create_sched();
let req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = req.txn_id;
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"bar".to_vec()),
});
// A new scan result with the same BSSID replaces the previous result.
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"baz".to_vec()),
});
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
assert!(req.is_none());
let (tokens, result) = assert_variant!(result,
ScanResult::DiscoveryFinished { tokens, result } => (tokens, result),
"expected discovery scan to be completed"
);
assert_eq!(vec![10], tokens);
let mut ssid_list = result
.expect("expected a successful scan result")
.into_iter()
.map(|bss| bss.ssid)
.collect::<Vec<_>>();
ssid_list.sort();
assert_eq!(vec![b"baz".to_vec()], ssid_list);
}
#[test]
fn test_passive_discovery_scan_args() {
let mut sched = create_sched();
let req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
assert_eq!(req.ssid, Vec::<u8>::new());
}
#[test]
fn test_active_discovery_scan_args_empty() {
let device_info = device_info_with_chan(vec![1, 36, 165]);
let mut sched: ScanScheduler<i32, i32> = ScanScheduler::new(Arc::new(device_info));
let scan_cmd = DiscoveryScan::new(
10,
fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
);
let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(req.ssid, Vec::<u8>::new());
assert_eq!(req.ssid_list, None);
assert_eq!(req.channel_list, Some(vec![36, 165, 1]));
}
#[test]
fn test_active_discovery_scan_args_filled() {
let device_info = device_info_with_chan(vec![1, 36, 165]);
let mut sched: ScanScheduler<i32, i32> = ScanScheduler::new(Arc::new(device_info));
let ssid1 = "ssid1".as_bytes().to_vec();
let ssid2 = "ssid2".as_bytes().to_vec();
let scan_cmd = DiscoveryScan::new(
10,
fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![ssid1.clone(), ssid2.clone()],
channels: vec![1, 20, 100],
}),
);
let req = sched.enqueue_scan_to_discover(scan_cmd).expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(req.ssid, Vec::<u8>::new());
assert_eq!(req.ssid_list, Some(vec![ssid1, ssid2]));
assert_eq!(req.channel_list, Some(vec![1]));
}
#[test]
fn test_discovery_scans_dedupe_single_group() {
let mut sched = create_sched();
// Post one scan command, expect a message to MLME
let mlme_req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = mlme_req.txn_id;
// Report a scan result
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
// Post another command. It should not issue another request to the MLME since
// there is already an on-going one
assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(20)).is_none());
// Report another scan result and the end of the scan transaction
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_bss_with_bssid(b"bar".to_vec(), [3; 6]),
});
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// We don't expect another request to the MLME
assert!(req.is_none());
// Expect a discovery result with both tokens and both SSIDs
assert_discovery_scan_result(result, vec![10, 20], vec![b"bar".to_vec(), b"foo".to_vec()]);
}
#[test]
fn test_discovery_scans_dedupe_multiple_groups() {
let mut sched = create_sched();
// Post a passive scan command, expect a message to MLME
let mlme_req = sched
.enqueue_scan_to_discover(passive_discovery_scan(10))
.expect("expected a ScanRequest");
let txn_id = mlme_req.txn_id;
// Post an active scan command, which should be enqueued until the previous one finishes
let scan_cmd = DiscoveryScan::new(
20,
fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
);
assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
// Post a passive scan command. It should be merged with the ongoing one and so should not
// issue another request to MLME
assert!(sched.enqueue_scan_to_discover(passive_discovery_scan(30)).is_none());
// Post an active scan command. It should be merged with the active scan command that's
// still enqueued, and so should not issue another request to MLME
let scan_cmd = DiscoveryScan::new(
40,
fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
);
assert!(sched.enqueue_scan_to_discover(scan_cmd).is_none());
// Report scan result and scan end
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
let (result, mlme_req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// Expect discovery result with 1st and 3rd tokens
assert_discovery_scan_result(result, vec![10, 30], vec![b"foo".to_vec()]);
// Next mlme_req should be an active scan request
assert!(mlme_req.is_some());
let mlme_req = mlme_req.unwrap();
assert_eq!(mlme_req.scan_type, fidl_mlme::ScanTypes::Active);
let txn_id = mlme_req.txn_id;
// Report scan result and scan end
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"bar".to_vec()),
});
let (result, mlme_req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// Expect discovery result with 2nd and 3rd tokens
assert_discovery_scan_result(result, vec![20, 40], vec![b"bar".to_vec()]);
// We don't expect another request to the MLME
assert!(mlme_req.is_none());
}
fn assert_discovery_scan_result(
result: ScanResult<i32, i32>,
expected_tokens: Vec<i32>,
expected_ssids: Vec<Vec<u8>>,
) {
let (tokens, result) = assert_variant!(result,
ScanResult::DiscoveryFinished { tokens, result } => (tokens, result),
"expected discovery scan to be completed"
);
assert_eq!(tokens, expected_tokens);
let mut ssid_list = result
.expect("expected a successful scan result")
.into_iter()
.map(|bss| bss.ssid)
.collect::<Vec<_>>();
ssid_list.sort();
assert_eq!(ssid_list, expected_ssids);
}
#[test]
fn join_scan() {
let mut sched = create_sched();
let (discarded_token, req) =
sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
assert!(discarded_token.is_none());
let txn_id = req.expect("expected a ScanRequest").txn_id;
// Matching BSS
let bss1 = fake_bss_with_bssid(b"foo".to_vec(), [1, 1, 1, 1, 1, 1]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id, bss: clone_bss_desc(&bss1) });
// Mismatching transaction ID
let bss2 = fake_bss_with_bssid(b"foo".to_vec(), [2, 2, 2, 2, 2, 2]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id: txn_id + 100, bss: bss2 });
// Mismatching SSID
let bss3 = fake_bss_with_bssid(b"bar".to_vec(), [3, 3, 3, 3, 3, 3]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id, bss: bss3 });
// Matching BSS
let bss4 = fake_bss_with_bssid(b"foo".to_vec(), [4, 4, 4, 4, 4, 4]);
sched.on_mlme_scan_result(fidl_mlme::ScanResult { txn_id, bss: clone_bss_desc(&bss4) });
let (result, req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
assert!(req.is_none());
assert_variant!(
result,
ScanResult::JoinScanFinished { token: 10, result } => {
let mut bss_list = result.expect("bss_list is Err");
bss_list.sort_by(|a, b| a.bssid.cmp(&b.bssid));
assert_eq!(bss_list, vec![bss1, bss4]);
},
"expected join scan to be completed"
);
}
#[test]
fn test_stale_join_scan() {
let mut sched = create_sched();
let (_discarded_token, req) =
sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
let txn_id = req.expect("expected a ScanRequest").txn_id;
// Schedule a new one, which should make the existing one stale
let (discarded_token, req) =
sched.enqueue_scan_to_join(passive_join_scan(b"bar".to_vec(), 20));
// Verify that token for existing scan is discarded (which indicates it's marked stale)
assert!(discarded_token.is_some());
// Although we have marked previous scan stale, we cannot send out a new scan request
// yet since previous scan is still ongoing in lower layer
assert!(req.is_none());
// When stale scan finishes, the new one is ready to be sent
let (stale_scan_result, next_req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
match stale_scan_result {
ScanResult::None => (), // expected path
_ => panic!("expected ScanResult::None"),
}
assert!(next_req.is_some());
assert_eq!(next_req.unwrap().ssid, b"bar".to_vec());
}
fn passive_join_scan(ssid: Vec<u8>, token: i32) -> JoinScan<i32> {
JoinScan {
ssid,
token,
scan_request: fidl_sme::ScanRequest::Passive(fidl_sme::PassiveScanRequest {}),
}
}
#[test]
fn test_passive_join_scan_args() {
let mut sched = create_sched();
let (_discarded_token, req) =
sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
let req = req.expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Passive);
assert_eq!(req.ssid, b"foo".to_vec());
}
#[test]
fn test_active_join_scan_args() {
let mut sched = create_sched();
let (_discarded_token, req) = sched.enqueue_scan_to_join(JoinScan {
ssid: b"foo".to_vec(),
token: 10,
scan_request: fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
});
let req = req.expect("expected a ScanRequest");
assert_eq!(req.scan_type, fidl_mlme::ScanTypes::Active);
assert_eq!(req.ssid, b"foo".to_vec());
}
#[test]
fn get_join_scan() {
let mut sched = create_sched();
assert_eq!(None, sched.get_join_scan());
sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
// Make sure the scanner is in the state we expect it to be: the request is
// 'current', not 'pending'
assert_eq!(
ScanState::ScanningToJoin {
cmd: passive_join_scan(b"foo".to_vec(), 10),
mlme_txn_id: 1,
bss_map: HashMap::new(),
},
sched.current
);
assert_eq!(None, sched.pending_join);
assert_eq!(Some(&passive_join_scan(b"foo".to_vec(), 10)), sched.get_join_scan());
sched.enqueue_scan_to_join(passive_join_scan(b"bar".to_vec(), 20));
// Again, make sure the state is what we expect. "Foo" should still be the current request,
// while "bar" should be pending
assert_eq!(ScanState::StaleJoinScan { mlme_txn_id: 1 }, sched.current);
assert_eq!(Some(passive_join_scan(b"bar".to_vec(), 20)), sched.pending_join);
// Expect the pending request to be returned since the current one will be discarded
// once the scan finishes
assert_eq!(Some(&passive_join_scan(b"bar".to_vec(), 20)), sched.get_join_scan());
}
#[test]
fn test_scan_channels_arg_when_dfs_channel_handling_supported() {
let mut device_info = device_info_with_chan(vec![1, 52]);
device_info.driver_features = vec![fidl_common::DriverFeature::Dfs];
let mut sched: ScanScheduler<i32, i32> = ScanScheduler::new(Arc::new(device_info));
// Passive scan request should always include all channels supported by device
let (_, req) = sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
let req = req.expect("expect ScanRequest");
assert_eq!(req.channel_list, Some(vec![52, 1]));
let _ = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id: req.txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// Active scan request should include both DFS and non-DFS channels since device handles
// DFS channel
let (_, req) = sched.enqueue_scan_to_join(JoinScan {
ssid: b"foo".to_vec(),
token: 10,
scan_request: fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
});
assert_eq!(req.expect("expect ScanRequest").channel_list, Some(vec![52, 1]));
}
#[test]
fn test_scan_channels_arg_when_dfs_channel_handling_not_supported() {
let mut device_info = device_info_with_chan(vec![1, 52]);
device_info.driver_features = vec![];
let mut sched: ScanScheduler<i32, i32> = ScanScheduler::new(Arc::new(device_info));
// Passive scan request should always include all channels supported by device
let (_, req) = sched.enqueue_scan_to_join(passive_join_scan(b"foo".to_vec(), 10));
let req = req.expect("expect ScanRequest");
assert_eq!(req.channel_list, Some(vec![52, 1]));
let _ = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id: req.txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
// Active scan request should exclude DFS channels since device does not handle them
let (_, req) = sched.enqueue_scan_to_join(JoinScan {
ssid: b"foo".to_vec(),
token: 10,
scan_request: fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
});
assert_eq!(req.expect("expect ScanRequest").channel_list, Some(vec![1]));
}
#[test]
fn test_bss_map_retain_information() {
let mut sched = create_sched();
let req = sched
.enqueue_scan_to_discover(DiscoveryScan::new(
10,
fidl_sme::ScanRequest::Active(fidl_sme::ActiveScanRequest {
ssids: vec![],
channels: vec![],
}),
))
.expect("expected a ScanRequest");
let txn_id = req.txn_id;
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_bss_with_vendor_ies(b"foo".to_vec(), probe_resp_wsc_ie()),
});
sched.on_mlme_scan_result(fidl_mlme::ScanResult {
txn_id,
bss: fake_unprotected_bss_description(b"foo".to_vec()),
});
let (result, _req) = sched.on_mlme_scan_end(fidl_mlme::ScanEnd {
txn_id,
code: fidl_mlme::ScanResultCodes::Success,
});
let (_tokens, result) = assert_variant!(result,
ScanResult::DiscoveryFinished { tokens, result } => (tokens, result),
"expected discovery scan to be completed"
);
let scan_list = result.expect("expected a successful scan result");
assert_eq!(scan_list.len(), 1);
// The second scan result should be retained since it contains the AP manufacturer
// information, which the other ones don't.
assert_eq!(scan_list[0].vendor_ies, Some(probe_resp_wsc_ie()));
}
fn probe_resp_wsc_ie() -> Vec<u8> {
#[rustfmt::skip]
let ie = vec![
0xdd, 0x8c, // Vendor IE + Length
0x00, 0x50, 0xf2, 0x04, // OUI type for WSC
0x10, 0x4a, 0x00, 0x01, 0x10, // Version
0x10, 0x44, 0x00, 0x01, 0x02, // WiFi Protected Setup State
0x10, 0x57, 0x00, 0x01, 0x01, // AP Setup Locked
0x10, 0x3b, 0x00, 0x01, 0x03, // Response Type
// UUID-E
0x10, 0x47, 0x00, 0x10,
0x3b, 0x3b, 0xe3, 0x66, 0x80, 0x84, 0x4b, 0x03,
0xbb, 0x66, 0x45, 0x2a, 0xf3, 0x00, 0x59, 0x22,
// Manufacturer
0x10, 0x21, 0x00, 0x15,
0x41, 0x53, 0x55, 0x53, 0x54, 0x65, 0x6b, 0x20, 0x43, 0x6f, 0x6d, 0x70,
0x75, 0x74, 0x65, 0x72, 0x20, 0x49, 0x6e, 0x63, 0x2e,
// Model name
0x10, 0x23, 0x00, 0x08, 0x52, 0x54, 0x2d, 0x41, 0x43, 0x35, 0x38, 0x55,
// Model number
0x10, 0x24, 0x00, 0x03, 0x31, 0x32, 0x33,
// Serial number
0x10, 0x42, 0x00, 0x05, 0x31, 0x32, 0x33, 0x34, 0x35,
// Primary device type
0x10, 0x54, 0x00, 0x08, 0x00, 0x06, 0x00, 0x50, 0xf2, 0x04, 0x00, 0x01,
// Device name
0x10, 0x11, 0x00, 0x0b,
0x41, 0x53, 0x55, 0x53, 0x20, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x72,
// Config methods
0x10, 0x08, 0x00, 0x02, 0x20, 0x0c,
// Vendor extension
0x10, 0x49, 0x00, 0x06, 0x00, 0x37, 0x2a, 0x00, 0x01, 0x20,
];
ie
}
fn create_sched() -> ScanScheduler<i32, i32> {
ScanScheduler::new(Arc::new(test_utils::fake_device_info(CLIENT_ADDR)))
}
fn device_info_with_chan(channels: Vec<u8>) -> DeviceInfo {
DeviceInfo {
bands: vec![fidl_mlme::BandCapabilities {
channels,
..test_utils::fake_5ghz_band_capabilities()
}],
..test_utils::fake_device_info(CLIENT_ADDR)
}
}
}