blob: 870effbb62a9a1314c1803d11dccfa7a4c62c20c [file] [log] [blame]
// Copyright 2020 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.
#![cfg(test)]
use std::collections::HashMap;
use fidl_fuchsia_net as net;
use fidl_fuchsia_net_dhcp as dhcp;
use fidl_fuchsia_net_ext::IntoExt as _;
use fidl_fuchsia_net_interfaces as net_interfaces;
use fidl_fuchsia_netemul_network as netemul_network;
use fidl_fuchsia_netstack as netstack;
use fuchsia_async::{DurationExt as _, TimeoutExt as _};
use fuchsia_zircon as zx;
use anyhow::Context as _;
use futures::future::{FutureExt as _, TryFutureExt as _};
use futures::stream::{self, StreamExt as _};
use net_declare::fidl_ip_v4;
use net_types::ip as net_types_ip;
use netstack_testing_common::environments::{
KnownServices, Manager, NetCfg, Netstack2, TestSandboxExt as _,
};
use netstack_testing_common::{
try_all, try_any, wait_for_non_loopback_interface_up, Result,
ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
};
use netstack_testing_macros::variants_test;
/// Test that NetCfg discovers a newly added device and it adds the device
/// to the Netstack.
// TODO(fxbug.dev/54025): Enable this test for NetworkManager.
#[variants_test]
async fn test_oir<E: netemul::Endpoint>(name: &str) -> Result {
let sandbox = netemul::TestSandbox::new().context("create sandbox")?;
// Create an environment with the LookupAdmin service as NetCfg tries to configure
// it. NetCfg will fail if it can't send the LookupAdmin a request.
let environment = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(name, &[KnownServices::LookupAdmin])
.context("create netstack environment")?;
// Start the network manager.
let launcher = environment.get_launcher().context("get launcher")?;
let mut netmgr = fuchsia_component::client::launch(
&launcher,
NetCfg::PKG_URL.to_string(),
NetCfg::testing_args(),
)
.context("launch the network manager")?;
// Add a device to the environment.
let endpoint = sandbox.create_endpoint::<E, _>(name).await.context("create endpoint")?;
let () = endpoint.set_link_up(true).await.context("set link up")?;
let endpoint_mount_path = E::dev_path("ep");
let endpoint_mount_path = endpoint_mount_path.as_path();
let () = environment
.add_virtual_device(&endpoint, endpoint_mount_path)
.with_context(|| format!("add virtual device {}", endpoint_mount_path.display()))?;
// Make sure the Netstack got the new device added.
let mut wait_for_netmgr = netmgr.wait().fuse();
let interface_state = environment
.connect_to_service::<net_interfaces::StateMarker>()
.context("connect to fuchsia.net.interfaces/State service")?;
let _: (u64, String) = wait_for_non_loopback_interface_up(
&interface_state,
&mut wait_for_netmgr,
None,
ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
)
.await
.context("wait for non loopback interface")?;
environment
.remove_virtual_device(endpoint_mount_path)
.with_context(|| format!("remove virtual device {}", endpoint_mount_path.display()))
}
/// Tests that stable interface name conflicts are handled gracefully.
// TODO(fxbug.dev/54025): Enable this test for NetworkManager.
#[variants_test]
async fn test_oir_interface_name_conflict<E: netemul::Endpoint>(name: &str) -> Result {
let sandbox = netemul::TestSandbox::new().context("create sandbox")?;
let environment = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(name, &[KnownServices::LookupAdmin])
.context("create netstack environment")?;
// Start the network manager.
let launcher = environment.get_launcher().context("get launcher")?;
let mut netmgr = fuchsia_component::client::launch(
&launcher,
NetCfg::PKG_URL.to_string(),
NetCfg::testing_args(),
)
.context("launch the network manager")?;
let mut wait_for_netmgr = netmgr.wait().fuse();
let netstack = environment
.connect_to_service::<netstack::NetstackMarker>()
.context("connect to netstack service")?;
// Add a device to the environment and wait for it to be added to the netstack.
//
// Non PCI and USB devices get their interface names from their MAC addresses.
// Using the same MAC address for different devices will result in the same
// interface name.
let mac = || Some(Box::new(net::MacAddress { octets: [2, 3, 4, 5, 6, 7] }));
let ethx7 = sandbox
.create_endpoint_with(
"ep1",
netemul_network::EndpointConfig { mtu: 1500, mac: mac(), backing: E::NETEMUL_BACKING },
)
.await
.context("create ethx7")?;
let () = ethx7.set_link_up(true).await.context("set link up")?;
let endpoint_mount_path = E::dev_path("ep1");
let endpoint_mount_path = endpoint_mount_path.as_path();
let () = environment
.add_virtual_device(&ethx7, endpoint_mount_path)
.with_context(|| format!("add virtual device1 {}", endpoint_mount_path.display()))?;
let interface_state = environment
.connect_to_service::<net_interfaces::StateMarker>()
.context("connect to fuchsia.net.interfaces/State service")?;
let (id_ethx7, name_ethx7) = wait_for_non_loopback_interface_up(
&interface_state,
&mut wait_for_netmgr,
None,
ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
)
.await
.context("wait for first non loopback interface")?;
assert_eq!(
&name_ethx7, "ethx7",
"first interface should use a stable name based on its MAC address"
);
// Create an interface that the network manager does not know about that will cause a
// name conflict with the first temporary name.
let etht0 =
sandbox.create_endpoint::<netemul::Ethernet, _>("etht0").await.context("create eth0")?;
let () = etht0.set_link_up(true).await.context("set link up")?;
let name = "etht0";
let netstack_id_etht0 = netstack
.add_ethernet_device(
name,
&mut netstack::InterfaceConfig {
name: name.to_string(),
filepath: "/fake/filepath/for_test".to_string(),
metric: 0,
},
etht0
.get_ethernet()
.await
.context("netstack.add_ethernet_device requires an Ethernet endpoint")?,
)
.await
.context("add_ethernet_device FIDL error")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("add_ethernet_device error")?;
let () = netstack
.set_interface_status(netstack_id_etht0, true /* enabled */)
.context("set interface status FIDL error")?;
let (id_etht0, name_etht0) = wait_for_non_loopback_interface_up(
&interface_state,
&mut wait_for_netmgr,
Some(&std::iter::once(id_ethx7).collect()),
ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
)
.await
.context("wait for second non loopback interface")?;
assert_eq!(id_etht0, u64::from(netstack_id_etht0));
assert_eq!(&name_etht0, "etht0");
// Add another device from the network manager with the same MAC address and wait for it
// to be added to the netstack. Its first two attempts at adding a name should conflict
// with the above two devices.
let etht1 = sandbox
.create_endpoint_with(
"ep2",
netemul_network::EndpointConfig { mtu: 1500, mac: mac(), backing: E::NETEMUL_BACKING },
)
.await
.context("create etht1")?;
let () = etht1.set_link_up(true).await.context("set link up")?;
let endpoint_mount_path = E::dev_path("ep2");
let endpoint_mount_path = endpoint_mount_path.as_path();
let () = environment
.add_virtual_device(&etht1, endpoint_mount_path)
.with_context(|| format!("add virtual device2 {}", endpoint_mount_path.display()))?;
let (id_etht1, name_etht1) = wait_for_non_loopback_interface_up(
&interface_state,
&mut wait_for_netmgr,
Some(&vec![id_ethx7, id_etht0].into_iter().collect()),
ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
)
.await
.context("wait for third non loopback interface")?;
assert_ne!(id_ethx7, id_etht1, "interface IDs should be different");
assert_ne!(id_etht0, id_etht1, "interface IDs should be different");
assert_eq!(
&name_etht1, "etht1",
"second interface from network manager should use a temporary name"
);
Ok(())
}
/// Make sure the DHCP server is configured to start serving requests when NetCfg discovers
/// a WLAN AP interface and stops serving when the interface is removed.
///
/// Also make sure that a new WLAN AP interface may be added after a previous interface has been
/// removed from the netstack.
#[variants_test]
async fn test_wlan_ap_dhcp_server<E: netemul::Endpoint>(name: &str) -> Result {
// Use a large timeout to check for resolution.
//
// These values effectively result in a large timeout of 60s which should avoid
// flakes. This test was run locally 100 times without flakes.
/// Duration to sleep between polls.
const POLL_WAIT: fuchsia_zircon::Duration = fuchsia_zircon::Duration::from_seconds(1);
/// Maximum number of times we'll poll the DHCP server to check its parameters.
const RETRY_COUNT: u64 = 120;
/// Check if the DHCP server is started.
async fn check_dhcp_status(dhcp_server: &dhcp::Server_Proxy, started: bool) -> Result {
for _ in 0..RETRY_COUNT {
let () = fuchsia_async::Timer::new(POLL_WAIT.after_now()).await;
if started == dhcp_server.is_serving().await.context("query server status request")? {
return Ok(());
}
}
Err(anyhow::anyhow!("timed out checking DHCP server status"))
}
/// Make sure the DHCP server is configured to start serving requests when NetCfg discovers
/// a WLAN AP interface and stops serving when the interface is removed.
///
/// When `wlan_ap_dhcp_server_inner` returns successfully, the interface that it creates will
/// have been removed.
async fn wlan_ap_dhcp_server_inner<'a, E: netemul::Endpoint>(
sandbox: &'a netemul::TestSandbox,
environment: &netemul::TestEnvironment<'a>,
offset: u8,
) -> Result {
// These constants are all hard coded in NetCfg for the WLAN AP interface and
// the DHCP server.
const DHCP_LEASE_TIME: u32 = 24 * 60 * 60; // 1 day in seconds.
const NETWORK_ADDR: net::Ipv4Address = fidl_ip_v4!("192.168.255.248");
const NETWORK_PREFIX_LEN: u8 = 29;
const INTERFACE_ADDR: net::Ipv4Address = fidl_ip_v4!("192.168.255.249");
const DHCP_POOL_START_ADDR: net::Ipv4Address = fidl_ip_v4!("192.168.255.250");
const DHCP_POOL_END_ADDR: net::Ipv4Address = fidl_ip_v4!("192.168.255.254");
const NETWORK_ADDR_SUBNET: net_types_ip::Subnet<net_types_ip::Ipv4Addr> = unsafe {
net_types_ip::Subnet::new_unchecked(
net_types_ip::Ipv4Addr::new(NETWORK_ADDR.addr),
NETWORK_PREFIX_LEN,
)
};
// Add a device to the environment that looks like a WLAN AP from the perspective
// of NetCfg. The topological path for the interface must include "wlanif-ap" as
// that is how NetCfg identifies a WLAN AP interface.
let network = sandbox
.create_network(format!("dhcp-server-{}", offset))
.await
.context("create network")?;
let wlan_ap = network
.create_endpoint::<E, _>(format!("wlanif-ap-dhcp-server-{}", offset))
.await
.context("create wlan ap")?;
let path = E::dev_path(&format!("dhcp-server-ep-{}", offset));
let () = environment
.add_virtual_device(&wlan_ap, path.as_path())
.with_context(|| format!("add WLAN AP virtual device {}", path.display()))?;
let () = wlan_ap.set_link_up(true).await.context("set wlan ap link up")?;
// Make sure the WLAN AP interface is added to the Netstack and is brought up with
// the right IP address.
let interface_state = environment
.connect_to_service::<net_interfaces::StateMarker>()
.context("connect to fuchsia.net.interfaces/State service")?;
let (watcher, watcher_server) =
::fidl::endpoints::create_proxy::<net_interfaces::WatcherMarker>()?;
let () = interface_state
.get_watcher(net_interfaces::WatcherOptions::EMPTY, watcher_server)
.context("failed to initialize interface watcher")?;
let mut if_map = HashMap::new();
let (wlan_ap_id, wlan_ap_name) = fidl_fuchsia_net_interfaces_ext::wait_interface(
fidl_fuchsia_net_interfaces_ext::event_stream(watcher.clone()),
&mut if_map,
|if_map| {
if_map.iter().find_map(
|(
id,
fidl_fuchsia_net_interfaces_ext::Properties {
name, online, addresses, ..
},
)| {
(*online
&& addresses.iter().any(
|&fidl_fuchsia_net_interfaces_ext::Address {
addr: fidl_fuchsia_net::Subnet { addr, prefix_len: _ },
}| {
addr == INTERFACE_ADDR.into_ext()
},
))
.then(|| (*id, name.clone()))
},
)
},
)
.map_err(anyhow::Error::from)
.on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || {
Err(anyhow::anyhow!("timed out"))
})
.await
.context("failed to wait for presence of a WLAN AP interface")?;
// Check the DHCP server's configured parameters.
let dhcp_server = environment
.connect_to_service::<dhcp::Server_Marker>()
.context("connect to DHCP server service")?;
let checks = [
(dhcp::ParameterName::IpAddrs, dhcp::Parameter::IpAddrs(vec![INTERFACE_ADDR])),
(
dhcp::ParameterName::LeaseLength,
dhcp::Parameter::Lease(dhcp::LeaseLength {
default: Some(DHCP_LEASE_TIME),
max: Some(DHCP_LEASE_TIME),
..dhcp::LeaseLength::EMPTY
}),
),
(
dhcp::ParameterName::BoundDeviceNames,
dhcp::Parameter::BoundDeviceNames(vec![wlan_ap_name]),
),
(
dhcp::ParameterName::AddressPool,
dhcp::Parameter::AddressPool(dhcp::AddressPool {
prefix_length: Some(NETWORK_PREFIX_LEN),
range_start: Some(DHCP_POOL_START_ADDR),
range_stop: Some(DHCP_POOL_END_ADDR),
..dhcp::AddressPool::EMPTY
}),
),
];
let dhcp_server_ref = &dhcp_server;
let checks_ref = &checks;
if !try_any(stream::iter(0..RETRY_COUNT).then(|i| async move {
let () = fuchsia_async::Timer::new(POLL_WAIT.after_now()).await;
try_all(stream::iter(checks_ref.iter()).then(|(param_name, param_value)| async move {
Ok(dhcp_server_ref
.get_parameter(*param_name)
.await
.with_context(|| format!("get {:?} paramter request", param_name))?
.map_err(zx::Status::from_raw)
.with_context(|| format!("error getting {:?} paramter", param_name))?
== *param_value)
}))
.await
.with_context(|| format!("{}-th iteration checking DHCP parameters", i))
}))
.await
.context("checking DHCP parameters")?
{
// Too many retries.
return Err(anyhow::anyhow!("timed out waiting for DHCP server configurations"));
}
// The DHCP server should be started.
let () = check_dhcp_status(&dhcp_server, true)
.await
.context("check DHCP server started after interface added")?;
// Add a host endpoint to the network. It should be configured by the DHCP server.
let host = network
.create_endpoint::<E, _>(format!("host-dhcp-client-{}", offset))
.await
.context("create host")?;
let path = E::dev_path(&format!("dhcp-client-ep-{}", offset));
let () = environment
.add_virtual_device(&host, path.as_path())
.with_context(|| format!("add host virtual device {}", path.display()))?;
let () = host.set_link_up(true).await.context("set host link up")?;
let () = fidl_fuchsia_net_interfaces_ext::wait_interface(
fidl_fuchsia_net_interfaces_ext::event_stream(watcher.clone()),
&mut if_map,
|if_map| {
if_map.iter().find_map(
|(
id,
fidl_fuchsia_net_interfaces_ext::Properties { online, addresses, .. },
)| {
// TODO(https://github.com/rust-lang/rust/issues/80967): use bool::then_some.
(*id != wlan_ap_id
&& *online
&& addresses.iter().any(
|&fidl_fuchsia_net_interfaces_ext::Address {
addr: fidl_fuchsia_net::Subnet { addr, prefix_len: _ },
}| match addr {
net::IpAddress::Ipv4(net::Ipv4Address { addr }) => {
NETWORK_ADDR_SUBNET
.contains(&net_types_ip::Ipv4Addr::new(addr))
}
net::IpAddress::Ipv6(net::Ipv6Address { addr: _ }) => false,
},
))
.then(|| ())
},
)
},
)
.map_err(anyhow::Error::from)
.on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || {
Err(anyhow::anyhow!("timed out"))
})
.await
.context("wait for host interface to be configured")?;
// Take the interface down, the DHCP server should be stopped.
let () = wlan_ap.set_link_up(false).await.context("set wlan ap link down")?;
let () = check_dhcp_status(&dhcp_server, false)
.await
.context("check DHCP server stopped after interface down")?;
// Bring the interface back up, the DHCP server should be started.
let () = wlan_ap.set_link_up(true).await.context("set wlan ap link up")?;
let () = check_dhcp_status(&dhcp_server, true)
.await
.context("check DHCP server started after interface up")?;
// Remove the interface, the DHCP server should be stopped.
std::mem::drop(wlan_ap);
let () = check_dhcp_status(&dhcp_server, false)
.await
.context("check DHCP server stopped after interface removed")?;
Ok(())
}
let sandbox = netemul::TestSandbox::new().context("create sandbox")?;
let environment = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(
name,
vec![
KnownServices::LookupAdmin.into_launch_service(),
KnownServices::DhcpServer.into_launch_service(),
KnownServices::SecureStash.into_launch_service(),
],
)
.context("create netstack environment")?;
// Start NetCfg.
let launcher = environment.get_launcher().context("get launcher")?;
let mut netcfg = fuchsia_component::client::launch(
&launcher,
NetCfg::PKG_URL.to_string(),
NetCfg::testing_args(),
)
.context("launch netcfg")?;
let mut wait_for_netcfg_fut = netcfg.wait().fuse();
// Add a WLAN AP, make sure the DHCP server gets configurd and starts or stops when the
// interface is added and brought up or brought down/removed.
for i in 0..=1 {
let test_fut = wlan_ap_dhcp_server_inner::<E>(&sandbox, &environment, i).fuse();
futures::pin_mut!(test_fut);
let () = futures::select! {
test_res = test_fut => test_res,
wait_for_netcfg_res = wait_for_netcfg_fut => {
Err(anyhow::anyhow!("NetCfg unexpectedly exited with exit status = {:?}", wait_for_netcfg_res?))
}
}
.with_context(|| format!("test {}-th interface", i))?;
}
Ok(())
}