blob: 3380a57b764c47ad87395309c4243ef68cca98ca [file] [log] [blame]
// 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))
}