blob: cf0e3ee5783e186348d2e01d99f5d5d20a1f18e0 [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.
#![feature(async_await, await_macro, futures_api)]
#![deny(warnings)]
// Explicitly added due to conflict using custom_attribute and async_await above.
#[macro_use]
extern crate serde_derive;
mod opts;
use crate::opts::Opt;
use connectivity_testing::wlan_service_util;
use failure::{bail, Error, ResultExt};
use fidl_fuchsia_net_oldhttp::{self as http, HttpServiceProxy};
use fidl_fuchsia_net_stack::{self as netstack, StackMarker, StackProxy};
use fidl_fuchsia_wlan_device_service::{DeviceServiceMarker, DeviceServiceProxy};
use fidl_fuchsia_wlan_sme as fidl_sme;
use fuchsia_app::client::connect_to_service;
use fuchsia_async as fasync;
use fuchsia_syslog::{self as syslog, fx_log_info};
use fuchsia_zircon as zx;
use futures::io::{AllowStdIo, AsyncReadExt};
use std::collections::HashMap;
use std::process;
use std::{thread, time};
use structopt::StructOpt;
#[allow(dead_code)]
type WlanService = DeviceServiceProxy;
fn main() -> Result<(), Error> {
syslog::init_with_tags(&["wlan-smoke-test"]).expect("should not fail");
let opt = Opt::from_args();
fx_log_info!("{:?}", opt);
// create objects to hold test objects and results
let mut test_results: TestResults = Default::default();
let mut test_pass = true;
if let Err(e) = run_test(opt, &mut test_results) {
test_pass = false;
test_results.error_message = e.to_string();
}
report_results(&mut test_results);
if !test_pass {
process::exit(1);
}
Ok(())
}
fn run_test(opt: Opt, test_results: &mut TestResults) -> Result<(), Error> {
let mut test_pass = false;
let mut exec = fasync::Executor::new().context("error creating event loop")?;
let wlan_svc =
connect_to_service::<DeviceServiceMarker>().context("Failed to connect to wlan_service")?;
test_results.connect_to_wlan_service = true;
let http_svc = connect_to_service::<http::HttpServiceMarker>()?;
test_results.connect_to_http_service = true;
let network_svc = connect_to_service::<StackMarker>()?;
test_results.connect_to_netstack_service = true;
let fut = async {
let wlan_iface_ids = await!(wlan_service_util::get_iface_list(&wlan_svc))
.context("wlan-smoke-test: failed to query wlanservice iface list")?;
test_results.query_wlan_service_iface_list = true;
if wlan_iface_ids.is_empty() {
bail!("Did not find wlan interfaces");
};
test_results.wlan_discovered = true;
// note: interface discovery is marked false at the time of failure
test_results.interface_status = true;
for iface in wlan_iface_ids {
let sme_proxy = await!(wlan_service_util::get_iface_sme_proxy(&wlan_svc, iface))?;
let status_response = match await!(sme_proxy.status()) {
Ok(status) => status,
Err(_) => {
test_results.interface_status = false;
continue;
}
};
let iface_object = WlanIface::new(sme_proxy, status_response);
test_results.iface_objects.insert(iface, iface_object);
}
// now that we have interfaces... let's try to use them!
for (iface_id, wlan_iface) in test_results.iface_objects.iter_mut() {
// first check if we can get scan results
let scan_result = await!(wlan_service_util::perform_scan(&wlan_iface.sme_proxy));
match scan_result {
Ok(results) => {
wlan_iface.scan_success = true;
for entry in results.into_iter() {
if entry.best_bss.ssid == opt.target_ssid.as_bytes().to_vec() {
wlan_iface.scan_found_target_ssid = true;
}
}
}
_ => println!("scan failed"),
};
let mut requires_disconnect = false;
// first check if we are connected to the target network already
if is_connect_to_target_network_needed(
opt.stay_connected,
&opt.target_ssid,
&wlan_iface.initial_status,
) {
let connect_result = await!(wlan_service_util::connect_to_network(
&wlan_iface.sme_proxy,
opt.target_ssid.as_bytes().to_vec(),
opt.target_pwd.as_bytes().to_vec()
));
match connect_result {
Ok(true) => {
wlan_iface.connection_success = true;
requires_disconnect = true;
}
_ => continue,
};
} else {
// connection already established, mark as successful
wlan_iface.connection_success = true;
}
let mut dhcp_check_attempts = 0;
while dhcp_check_attempts < 3 && !wlan_iface.dhcp_success {
// check if there is a non-zero ip addr as a first check for dhcp success
let ip_addrs =
match await!(get_ip_addrs_for_wlan_iface(&wlan_svc, &network_svc, *iface_id)) {
Ok(result) => result,
Err(_) => continue,
};
if check_dhcp_complete(ip_addrs) {
wlan_iface.dhcp_success = true;
} else {
// dhcp takes some time... loop again to give it a chance
dhcp_check_attempts += 1;
thread::sleep(time::Duration::from_millis(4000));
}
}
// after testing, check if we need to disconnect
if requires_disconnect {
match await!(wlan_service_util::disconnect_from_network(&wlan_iface.sme_proxy)) {
Err(_) => wlan_iface.disconnect_success = false,
_ => wlan_iface.disconnect_success = true,
};
} else {
wlan_iface.disconnect_success = true;
}
// if any of the checks failed, throw an error to indicate a part of
// the test failure
if wlan_iface.connection_success
&& wlan_iface.dhcp_success
&& wlan_iface.data_transfer
&& wlan_iface.disconnect_success
{
// note: failures are logged at the point of the failure,
// simply checking here to return overall test status
test_pass = true;
} else {
test_pass = false;
}
// TODO(NET-1095): add ping check to verify connectivity
// TODO(NET-1095): add http get to verify data when we can specify this interface
}
// create url (TODO(NET-1095): add command line option)
let url_string = "http://ovh.net/files/1Mb.dat";
let url_request = create_url_request(url_string);
// NOTE: this is intended to loop over each wlan iface. For now,
// make a single request to make sure that mechanism works and we have not broken
// connectivity with connection changes
await!(fetch_and_discard_url(http_svc, url_request))?;
test_results.base_data_transfer = true;
Ok(())
};
exec.run_singlethreaded(fut)?;
if !test_pass {
bail!("Saw a failure on at least one interface");
}
Ok(())
}
// Object to hold overall test status
#[derive(Default, Serialize)]
struct TestResults {
connect_to_wlan_service: bool,
connect_to_http_service: bool,
connect_to_netstack_service: bool,
query_wlan_service_iface_list: bool,
wlan_discovered: bool,
interface_status: bool,
base_data_transfer: bool,
#[serde(flatten)]
iface_objects: HashMap<u16, WlanIface>,
error_message: String,
}
// Object to hold test specific status
#[derive(Serialize)]
struct WlanIface {
#[serde(skip_serializing)]
sme_proxy: fidl_sme::ClientSmeProxy,
#[serde(skip_serializing)]
initial_status: fidl_sme::ClientStatusResponse,
scan_success: bool,
scan_found_target_ssid: bool,
connection_success: bool,
disconnect_success: bool,
dhcp_success: bool,
data_transfer: bool,
}
impl WlanIface {
pub fn new(
sme_proxy: fidl_sme::ClientSmeProxy,
status: fidl_sme::ClientStatusResponse,
) -> WlanIface {
WlanIface {
sme_proxy: sme_proxy,
initial_status: status,
scan_success: false,
scan_found_target_ssid: false,
connection_success: false,
disconnect_success: false,
dhcp_success: false,
data_transfer: false,
}
}
}
fn report_results(test_results: &TestResults) {
println!("{}", serde_json::to_string_pretty(&test_results).unwrap());
}
fn is_connect_to_target_network_needed<T: AsRef<[u8]>>(
stay_connected: bool,
target_ssid: T,
status: &fidl_sme::ClientStatusResponse,
) -> bool {
if !stay_connected {
// doesn't matter if we are connected, we will force a reconnection
return true;
}
// are we already connected? if so, check the current ssid
match status.connected_to {
Some(ref bss) if bss.ssid.as_slice() == target_ssid.as_ref() => false,
_ => true,
}
}
fn create_url_request<T: Into<String>>(url_string: T) -> http::UrlRequest {
http::UrlRequest {
url: url_string.into(),
method: String::from("GET"),
headers: None,
body: None,
response_body_buffer_size: 0,
auto_follow_redirects: true,
cache_mode: http::CacheMode::Default,
response_body_mode: http::ResponseBodyMode::Stream,
}
}
async fn fetch_and_discard_url(
http_service: HttpServiceProxy,
mut url_request: http::UrlRequest,
) -> Result<(), Error> {
// Create a UrlLoader instance
let (s, p) = zx::Channel::create().context("failed to create zx channel")?;
let proxy = fasync::Channel::from_channel(p).context("failed to make async channel")?;
let loader_server = fidl::endpoints::ServerEnd::<http::UrlLoaderMarker>::new(s);
http_service.create_url_loader(loader_server)?;
let loader_proxy = http::UrlLoaderProxy::new(proxy);
let response = await!(loader_proxy.start(&mut url_request))?;
if let Some(e) = response.error {
bail!("UrlLoaderProxy error - code:{} ({})", e.code, e.description.unwrap_or("".into()))
}
let mut socket = match response.body.map(|x| *x) {
Some(http::UrlBody::Stream(s)) => fasync::Socket::from_socket(s)?,
_ => return Err(Error::from(zx::Status::BAD_STATE)),
};
// discard the bytes
let mut stdio_sink = AllowStdIo::new(::std::io::sink());
let bytes_received = await!(socket.copy_into(&mut stdio_sink))?;
fx_log_info!("Received {:?} bytes", bytes_received);
Ok(())
}
async fn get_ip_addrs_for_wlan_iface<'a>(
wlan_svc: &'a DeviceServiceProxy,
network_svc: &'a StackProxy,
wlan_iface_id: u16,
) -> Result<Vec<netstack::InterfaceAddress>, Error> {
// temporary implementation for getting the ip addrs for a wlan iface. A more robust
// lookup will be designed and implemented in the future (TODO: <bug already filed?>)
let mut iface_path = String::new();
//first get info on the wlan iface
let response = await!(wlan_svc.list_ifaces())?;
for iface in response.ifaces {
if wlan_iface_id == iface.iface_id {
// trim off any leading '@'s
iface_path = iface.path.trim_start_matches('@').to_string();
}
}
//now, if we got a valid path, we can check the netstack iface info
if iface_path.is_empty() {
// could not find a path... throw an error
bail!("Could not find the path for iface {}", wlan_iface_id);
}
let mut net_iface_response = await!(network_svc.list_interfaces())?;
let mut wlan_iface_ip_addrs = Vec::new();
for net_iface in net_iface_response.iter_mut() {
if net_iface.properties.path.is_empty() {
continue;
}
// trim off any leading '@'s
let net_path = net_iface.properties.path.trim_start_matches('@').to_string();
if net_path.starts_with(&iface_path) {
// now get the ip addrs
wlan_iface_ip_addrs.append(&mut net_iface.properties.addresses);
// Note: Until proper interface mappings between wlanstack and netstack,
// we return all ip_addrs that match the device path to handle
// multiple interfaces on a single device.
}
}
Ok(wlan_iface_ip_addrs)
}
fn check_dhcp_complete(ip_addrs: Vec<netstack::InterfaceAddress>) -> bool {
for ip_addr in ip_addrs {
// for now, assume a valid address if we see anything that isn't a 0
fx_log_info!("checking validity of ip address: {:?}", ip_addr.ip_address);
match ip_addr.ip_address {
fidl_fuchsia_net::IpAddress::Ipv4(address) => {
for &a in address.addr.iter() {
if a != 0 as u8 {
return true;
}
}
}
fidl_fuchsia_net::IpAddress::Ipv6(address) => {
for &a in address.addr.iter() {
if a != 0 as u8 {
return true;
}
}
}
};
}
return false;
}