| // 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(ðx7, 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(ðt1, 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(()) |
| } |