| // Copyright 2019 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. |
| |
| #![deny(missing_docs)] |
| |
| //! Provides utilities for Netstack integration tests. |
| |
| pub mod constants; |
| #[macro_use] |
| pub mod environments; |
| |
| use std::collections::{HashMap, HashSet}; |
| use std::convert::TryFrom; |
| use std::fmt::Debug; |
| |
| use fidl_fuchsia_hardware_ethertap as ethertap; |
| use fidl_fuchsia_net_interfaces as net_interfaces; |
| use fidl_fuchsia_netemul_environment as netemul_environment; |
| use fidl_fuchsia_netstack as netstack; |
| use fuchsia_async::{self as fasync, DurationExt as _, TimeoutExt as _}; |
| use fuchsia_zircon as zx; |
| |
| use anyhow::Context as _; |
| use futures::future::{FusedFuture, Future, FutureExt as _}; |
| use futures::stream::{Stream, StreamExt, TryStreamExt}; |
| use futures::TryFutureExt as _; |
| use net_types::ethernet::Mac; |
| use net_types::ip as net_types_ip; |
| use net_types::Witness as _; |
| use packet::serialize::{InnerPacketBuilder, Serializer}; |
| use packet_formats::ethernet::{EtherType, EthernetFrameBuilder}; |
| use packet_formats::icmp::ndp::{self, options::NdpOption, RouterAdvertisement}; |
| use packet_formats::icmp::{IcmpMessage, IcmpPacketBuilder, IcmpUnusedCode}; |
| use packet_formats::ip::IpProto; |
| use packet_formats::ipv6::Ipv6PacketBuilder; |
| use zerocopy::ByteSlice; |
| |
| use crate::environments::TestSandboxExt as _; |
| |
| /// An alias for `Result<T, anyhow::Error>`. |
| pub type Result<T = ()> = std::result::Result<T, anyhow::Error>; |
| |
| /// Extra time to use when waiting for an async event to occur. |
| /// |
| /// A large timeout to help prevent flakes. |
| pub const ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT: zx::Duration = zx::Duration::from_seconds(120); |
| |
| /// Extra time to use when waiting for an async event to not occur. |
| /// |
| /// Since a negative check is used to make sure an event did not happen, its okay to use a |
| /// smaller timeout compared to the positive case since execution stall in regards to the |
| /// monotonic clock will not affect the expected outcome. |
| pub const ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT: zx::Duration = zx::Duration::from_seconds(5); |
| |
| /// The time to wait between two consecutive checks of an event. |
| pub const ASYNC_EVENT_CHECK_INTERVAL: zx::Duration = zx::Duration::from_seconds(1); |
| |
| /// As per [RFC 4861] sections 4.1-4.5, NDP packets MUST have a hop limit of 255. |
| /// |
| /// [RFC 4861]: https://tools.ietf.org/html/rfc4861 |
| pub const NDP_MESSAGE_TTL: u8 = 255; |
| |
| /// Returns `true` once the stream yields a `true`. |
| /// |
| /// If the stream never yields `true` or never terminates, `try_any` may never resolve. |
| pub async fn try_any<S: Stream<Item = Result<bool>>>(stream: S) -> Result<bool> { |
| futures::pin_mut!(stream); |
| stream.try_filter(|v| futures::future::ready(*v)).next().await.unwrap_or(Ok(false)) |
| } |
| |
| /// Returns `true` if the stream only yields `true`. |
| /// |
| /// If the stream never yields `false` or never terminates, `try_all` may never resolve. |
| pub async fn try_all<S: Stream<Item = Result<bool>>>(stream: S) -> Result<bool> { |
| futures::pin_mut!(stream); |
| stream.try_filter(|v| futures::future::ready(!*v)).next().await.unwrap_or(Ok(true)) |
| } |
| |
| /// A trait that provides an Ethertap compatible name. |
| pub trait EthertapName { |
| /// Returns an Ethertap compatible name. |
| fn ethertap_compatible_name(&self) -> Self; |
| } |
| |
| impl<'a> EthertapName for &'a str { |
| fn ethertap_compatible_name(&self) -> &'a str { |
| let max_len = |
| usize::try_from(ethertap::MAX_NAME_LENGTH).expect("u32 could not fit into usize"); |
| &self[self.len().checked_sub(max_len).unwrap_or(0)..self.len()] |
| } |
| } |
| |
| /// Asynchronously sleeps for specified `secs` seconds. |
| pub async fn sleep(secs: i64) { |
| fasync::Timer::new(zx::Duration::from_seconds(secs).after_now()).await; |
| } |
| |
| /// Writes an NDP message to the provided fake endpoint. |
| /// |
| /// Given the source and destination MAC and IP addresses, NDP message and |
| /// options, the full NDP packet (including IPv6 and Ethernet headers) will be |
| /// transmitted to the fake endpoint's network. |
| pub async fn write_ndp_message< |
| B: ByteSlice + Debug, |
| M: IcmpMessage<net_types_ip::Ipv6, B, Code = IcmpUnusedCode> + Debug, |
| >( |
| src_mac: Mac, |
| dst_mac: Mac, |
| src_ip: net_types_ip::Ipv6Addr, |
| dst_ip: net_types_ip::Ipv6Addr, |
| message: M, |
| options: &[NdpOption<'_>], |
| ep: &netemul::TestFakeEndpoint<'_>, |
| ) -> Result { |
| let ser = ndp::OptionsSerializer::<_>::new(options.iter()) |
| .into_serializer() |
| .encapsulate(IcmpPacketBuilder::<_, B, _>::new(src_ip, dst_ip, IcmpUnusedCode, message)) |
| .encapsulate(Ipv6PacketBuilder::new(src_ip, dst_ip, NDP_MESSAGE_TTL, IpProto::Icmpv6)) |
| .encapsulate(EthernetFrameBuilder::new(src_mac, dst_mac, EtherType::Ipv6)) |
| .serialize_vec_outer() |
| .map_err(|e| anyhow::anyhow!("failed to serialize NDP packet: {:?}", e))? |
| .unwrap_b(); |
| ep.write(ser.as_ref()).await.context("failed to write to fake endpoint") |
| } |
| |
| /// Waits for a non-loopback interface to come up with an ID not in `exclude_ids`. |
| /// |
| /// Useful when waiting for an interface to be discovered and brought up by a |
| /// network manager. |
| /// |
| /// Returns the interface's ID and name. |
| pub async fn wait_for_non_loopback_interface_up< |
| F: Unpin + FusedFuture + Future<Output = Result<fuchsia_component::client::ExitStatus>>, |
| >( |
| interface_state: &net_interfaces::StateProxy, |
| mut wait_for_netmgr: &mut F, |
| exclude_ids: Option<&HashSet<u64>>, |
| timeout: zx::Duration, |
| ) -> Result<(u64, String)> { |
| let mut if_map = HashMap::new(); |
| let wait_for_interface = fidl_fuchsia_net_interfaces_ext::wait_interface( |
| fidl_fuchsia_net_interfaces_ext::event_stream_from_state(interface_state)?, |
| &mut if_map, |
| |if_map| { |
| if_map.iter().find_map( |
| |( |
| id, |
| fidl_fuchsia_net_interfaces_ext::Properties { |
| name, device_class, online, .. |
| }, |
| )| { |
| (*device_class |
| != net_interfaces::DeviceClass::Loopback(net_interfaces::Empty {}) |
| && *online |
| && exclude_ids.map_or(true, |ids| !ids.contains(id))) |
| .then(|| (*id, name.clone())) |
| }, |
| ) |
| }, |
| ) |
| .map_err(anyhow::Error::from) |
| .on_timeout(timeout.after_now(), || Err(anyhow::anyhow!("timed out"))) |
| .map(|r| r.context("failed to wait for non-loopback interface up")) |
| .fuse(); |
| fuchsia_async::pin_mut!(wait_for_interface); |
| futures::select! { |
| wait_for_interface_res = wait_for_interface => { |
| wait_for_interface_res |
| } |
| wait_for_netmgr_res = wait_for_netmgr => { |
| Err(anyhow::anyhow!("the network manager unexpectedly exited with exit status = {:?}", wait_for_netmgr_res?)) |
| } |
| } |
| } |
| |
| /// Gets inspect data in environment. |
| /// |
| /// Returns the resulting inspect data for `component`, filtered by |
| /// `tree_selector` and with inspect file starting with `file_prefix`. |
| pub async fn get_inspect_data<'a>( |
| env: &netemul::TestEnvironment<'a>, |
| component: impl Into<String>, |
| tree_selector: impl Into<String>, |
| file_prefix: &str, |
| ) -> Result<diagnostics_hierarchy::DiagnosticsHierarchy> { |
| let archive = env |
| .connect_to_service::<fidl_fuchsia_diagnostics::ArchiveAccessorMarker>() |
| .context("failed to connect to archive accessor")?; |
| |
| let mut data = diagnostics_reader::ArchiveReader::new() |
| .with_archive(archive) |
| .add_selector( |
| diagnostics_reader::ComponentSelector::new(vec![component.into()]) |
| .with_tree_selector(tree_selector.into()), |
| ) |
| // Enable `retry_if_empty` to prevent races in test environment bringup |
| // where we may end up reaching `ArchiveReader` before it has observed |
| // Netstack starting. |
| // |
| // Eventually there will be support for lifecycle streams, with which |
| // it will be possible to wait on the event of Archivist obtaining a |
| // handle to Netstack diagnostics, and then request the snapshot of |
| // inspect data once that event is received. |
| .retry_if_empty(true) |
| .snapshot::<diagnostics_reader::Inspect>() |
| .await |
| .context("failed to get inspect data")? |
| .into_iter() |
| .filter_map( |
| |diagnostics_data::InspectData { |
| data_source: _, |
| metadata, |
| moniker: _, |
| payload, |
| version: _, |
| }| { |
| if metadata.filename.starts_with(file_prefix) { |
| Some(payload.ok_or_else(|| { |
| anyhow::anyhow!( |
| "empty inspect payload, metadata errors: {:?}", |
| metadata.errors |
| ) |
| })) |
| } else { |
| None |
| } |
| }, |
| ); |
| let datum = data.next().unwrap_or_else(|| Err(anyhow::anyhow!("failed to find inspect data"))); |
| let data: Vec<_> = data.collect(); |
| assert!( |
| data.is_empty(), |
| "expected a single inspect entry; got {:?} and also {:?}", |
| datum, |
| data |
| ); |
| datum |
| } |
| |
| /// Send Router Advertisement NDP message with router lifetime. |
| pub async fn send_ra_with_router_lifetime<'a>( |
| fake_ep: &netemul::TestFakeEndpoint<'a>, |
| lifetime: u16, |
| options: &[NdpOption<'_>], |
| ) -> Result { |
| let ra = RouterAdvertisement::new( |
| 0, /* current_hop_limit */ |
| false, /* managed_flag */ |
| false, /* other_config_flag */ |
| lifetime, /* router_lifetime */ |
| 0, /* reachable_time */ |
| 0, /* retransmit_timer */ |
| ); |
| write_ndp_message::<&[u8], _>( |
| constants::eth::MAC_ADDR, |
| Mac::from(&net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS), |
| constants::ipv6::LINK_LOCAL_ADDR, |
| net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(), |
| ra, |
| options, |
| fake_ep, |
| ) |
| .await |
| } |
| |
| /// Sets up an environment with a network with no required services. |
| pub async fn setup_network<E, S>( |
| sandbox: &netemul::TestSandbox, |
| name: S, |
| ) -> Result<( |
| netemul::TestNetwork<'_>, |
| netemul::TestEnvironment<'_>, |
| netstack::NetstackProxy, |
| netemul::TestInterface<'_>, |
| netemul::TestFakeEndpoint<'_>, |
| )> |
| where |
| E: netemul::Endpoint, |
| S: Copy + Into<String> + EthertapName, |
| { |
| setup_network_with::<E, S, _>( |
| sandbox, |
| name, |
| std::iter::empty::<netemul_environment::LaunchService>(), |
| ) |
| .await |
| } |
| |
| /// Sets up an environment with required services and a network used for tests |
| /// requiring manual packet inspection and transmission. |
| /// |
| /// Returns the network, environment, netstack client, interface (added to the |
| /// netstack and up) and a fake endpoint used to read and write raw ethernet |
| /// packets. |
| pub async fn setup_network_with<E, S, I>( |
| sandbox: &netemul::TestSandbox, |
| name: S, |
| services: I, |
| ) -> Result<( |
| netemul::TestNetwork<'_>, |
| netemul::TestEnvironment<'_>, |
| netstack::NetstackProxy, |
| netemul::TestInterface<'_>, |
| netemul::TestFakeEndpoint<'_>, |
| )> |
| where |
| E: netemul::Endpoint, |
| S: Copy + Into<String> + EthertapName, |
| I: IntoIterator, |
| I::Item: Into<netemul_environment::LaunchService>, |
| { |
| let network = sandbox.create_network(name).await.context("failed to create network")?; |
| let environment = sandbox |
| .create_netstack_environment_with::<environments::Netstack2, _, _>(name, services) |
| .context("failed to create netstack environment")?; |
| // It is important that we create the fake endpoint before we join the |
| // network so no frames transmitted by Netstack are lost. |
| let fake_ep = network.create_fake_endpoint()?; |
| |
| let iface = environment |
| .join_network::<E, _>( |
| &network, |
| name.ethertap_compatible_name(), |
| &netemul::InterfaceConfig::None, |
| ) |
| .await |
| .context("failed to configure networking")?; |
| |
| let netstack = environment |
| .connect_to_service::<netstack::NetstackMarker>() |
| .context("failed to connect to netstack service")?; |
| |
| Ok((network, environment, netstack, iface, fake_ep)) |
| } |