blob: 0238267236b45bab5d1088595c8a995d13402647 [file] [log] [blame]
// Copyright 2022 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.
use anyhow::{anyhow, Context as _};
use fidl_fuchsia_data as fdata;
use fidl_fuchsia_net_ext as fnet_ext;
use fidl_fuchsia_net_interfaces as fnet_interfaces;
use fidl_fuchsia_netemul as fnetemul;
use fidl_fuchsia_netemul_network as fnetemul_network;
use fidl_fuchsia_sys2 as fsys2;
use std::{
collections::{hash_map, HashMap, HashSet},
str::FromStr,
};
use tracing::{debug, info};
#[derive(Debug, PartialEq)]
pub(crate) struct Network {
name: String,
endpoints: Vec<Endpoint>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct Endpoint {
name: String,
mac: Option<fnet_ext::MacAddress>,
mtu: u16,
up: bool,
}
impl Endpoint {
const fn default_mtu() -> u16 {
netemul::DEFAULT_MTU
}
const fn default_link_up() -> bool {
true
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct Netstack {
name: String,
interfaces: Vec<Interface>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct Interface {
name: String,
without_autogenerated_addresses: bool,
static_ips: Vec<fnet_ext::Subnet>,
gateway: Option<fnet_ext::IpAddress>,
enable_ipv4_forwarding: bool,
enable_ipv6_forwarding: bool,
}
// Represents a configuration that has been deserialized but not yet validated.
//
// To produce a valid `Config`, pass an `UnvalidatedConfig` to
// `Config::validate`.
#[derive(Debug, PartialEq)]
struct UnvalidatedConfig {
networks: Vec<Network>,
netstacks: Vec<Netstack>,
eager_components: Vec<String>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct Config {
networks: Vec<Network>,
netstacks: Vec<Netstack>,
eager_components: Vec<String>,
}
#[derive(Debug)]
struct Dictionary(HashMap<String, Box<fdata::DictionaryValue>>);
// NB(https://fxbug.dev/42178304): `fuchsia.data/Dictionary` is produced from a
// restricted subset of JSON that appears in parts of the component manifest,
// such as the `program` section. As a result, there are some undesirable
// properties of the `Dictionary` type that arise from its JSON source. For
// example:
// * the value of a given key is optional, given that in JSON, it could be
// `null`;
// * duplicate keys are allowed, and later instances of a key overwrites any
// previous instances, as specified in the ECMA standard.
impl TryFrom<fdata::Dictionary> for Dictionary {
type Error = anyhow::Error;
fn try_from(dict: fdata::Dictionary) -> Result<Self, anyhow::Error> {
let fdata::Dictionary { entries, .. } = dict;
let entries = entries
.context("`entries` not set in dictionary")?
.into_iter()
.map(|fdata::DictionaryEntry { key, value }| {
let value = value.with_context(|| format!("value not set for key `{}`", key))?;
Ok((key, value))
})
.collect::<Result<HashMap<_, _>, anyhow::Error>>()?;
Ok(Self(entries))
}
}
impl Dictionary {
fn try_take_str(&mut self, key: &str) -> Option<Result<String, fdata::DictionaryValue>> {
let Self(entries) = self;
entries.remove(key).map(|value| match *value {
fdata::DictionaryValue::Str(string) => Ok(string),
other => Err(other),
})
}
fn try_take_str_into<T: FromStr>(
&mut self,
key: &str,
) -> Option<Result<Result<T, <T as FromStr>::Err>, fdata::DictionaryValue>> {
self.try_take_str(key).map(|result| result.map(|s| s.parse::<T>()))
}
fn try_take_str_vec(
&mut self,
key: &str,
) -> Option<Result<Vec<String>, fdata::DictionaryValue>> {
let Self(entries) = self;
entries.remove(key).map(|value| match *value {
fdata::DictionaryValue::StrVec(strings) => Ok(strings),
other => Err(other),
})
}
fn try_take_obj_vec(
&mut self,
key: &str,
) -> Option<Result<Vec<fdata::Dictionary>, fdata::DictionaryValue>> {
let Self(entries) = self;
entries.remove(key).map(|value| {
match *value {
fdata::DictionaryValue::ObjVec(values) => Ok(values),
// The component manifest parser assumes an empty list to be a list of strings,
// so accept an empty list of strings as a valid list of objects.
fdata::DictionaryValue::StrVec(v) if v.is_empty() => Ok(vec![]),
other => Err(other),
}
})
}
fn into_empty(self) -> Result<(), Vec<String>> {
let Self(entries) = self;
if !entries.is_empty() {
Err(entries.into_keys().collect::<Vec<_>>())
} else {
Ok(())
}
}
}
impl TryFrom<Dictionary> for Network {
type Error = anyhow::Error;
fn try_from(mut dict: Dictionary) -> Result<Self, Self::Error> {
let name = dict
.try_take_str("name")
.context("could not find `name` in network")?
.map_err(|e| anyhow!("`name` is not a string: {:?}", e))?;
let endpoints = dict
.try_take_obj_vec("endpoints")
.with_context(|| format!("could not find `endpoints` in network `{}`", name))?
.map_err(|e| anyhow!("`endpoints` is not a list of objects: {:?}", e))?
.into_iter()
.map(|dict| Dictionary::try_from(dict).and_then(Endpoint::try_from))
.collect::<Result<Vec<_>, _>>()?;
dict.into_empty()
.map_err(|e| anyhow!("unrecognized fields in network `{}`: {:?}", name, e))?;
Ok(Network { name, endpoints })
}
}
impl TryFrom<Dictionary> for Endpoint {
type Error = anyhow::Error;
fn try_from(mut dict: Dictionary) -> Result<Self, Self::Error> {
let name = dict
.try_take_str("name")
.context("could not find `name` in endpoint")?
.map_err(|e| anyhow!("`name` is not a string: {:?}", e))?;
let mac = dict
.try_take_str_into::<fnet_ext::MacAddress>("mac")
.transpose()
.map_err(|e| anyhow!("`mac` is not a string: {:?}", e))?
.transpose()
.context("parse `mac`")?;
let mtu = dict
.try_take_str_into::<u16>("mtu")
.unwrap_or_else(|| Ok(Ok(Endpoint::default_mtu())))
.map_err(|e| anyhow!("`mtu` is not a string: {:?}", e))?
.context("parse `mtu`")?;
let up = dict
.try_take_str_into::<bool>("up")
.unwrap_or_else(|| Ok(Ok(Endpoint::default_link_up())))
.map_err(|e| anyhow!("`up` is not a string: {:?}", e))?
.context("parse `up`")?;
dict.into_empty()
.map_err(|e| anyhow!("unrecognized fields in endpoint `{}`: {:?}", name, e))?;
Ok(Endpoint { name, mac, mtu, up })
}
}
impl TryFrom<Dictionary> for Netstack {
type Error = anyhow::Error;
fn try_from(mut dict: Dictionary) -> Result<Self, Self::Error> {
let name = dict
.try_take_str("name")
.context("could not find `name` in netstack")?
.map_err(|e| anyhow!("`name` is not a string: {:?}", e))?;
let interfaces = dict
.try_take_obj_vec("interfaces")
.with_context(|| format!("could not find `interfaces` in netstack `{}`", name))?
.map_err(|e| anyhow!("`interfaces` is not a list of objects: {:?}", e))?
.into_iter()
.map(|dict| Dictionary::try_from(dict).and_then(Interface::try_from))
.collect::<Result<Vec<_>, _>>()?;
dict.into_empty()
.map_err(|e| anyhow!("unrecognized fields in netstack `{}`: {:?}", name, e))?;
Ok(Netstack { name, interfaces })
}
}
impl TryFrom<Dictionary> for Interface {
type Error = anyhow::Error;
fn try_from(mut dict: Dictionary) -> Result<Self, Self::Error> {
let name = dict
.try_take_str("name")
.context("could not find `name` in interface")?
.map_err(|e| anyhow!("`name` is not a string: {:?}", e))?;
let without_autogenerated_addresses = dict
.try_take_str_into::<bool>("without_autogenerated_addresses")
.unwrap_or_else(|| Ok(Ok(false)))
.map_err(|e| anyhow!("`without_autogenerated_addresses` is not a string: {:?}", e))?
.context("parse `without_autogenerated_addresses`")?;
let static_ips = dict
.try_take_str_vec("static_ips")
.with_context(|| format!("could not find `static_ips` in interface `{}`", name))?
.map_err(|e| anyhow!("`static_ips` is not a list of strings: {:?}", e))?
.into_iter()
.map(|s| s.parse::<fnet_ext::Subnet>())
.collect::<Result<Vec<_>, _>>()
.context("`static_ips` from strings")?;
let gateway = dict
.try_take_str_into::<fnet_ext::IpAddress>("gateway")
.transpose()
.map_err(|e| anyhow!("`gateway` is not a string: {:?}", e))?
.transpose()
.context("parse `gateway`")?;
let enable_ipv4_forwarding = dict
.try_take_str_into::<bool>("enable_ipv4_forwarding")
.unwrap_or_else(|| Ok(Ok(false)))
.map_err(|e| anyhow!("`enable_ipv4_forwarding` is not a string: {:?}", e))?
.context("parse `enable_ipv4_forwarding`")?;
let enable_ipv6_forwarding = dict
.try_take_str_into::<bool>("enable_ipv6_forwarding")
.unwrap_or_else(|| Ok(Ok(false)))
.map_err(|e| anyhow!("`enable_ipv6_forwarding` is not a string: {:?}", e))?
.context("parse `enable_ipv6_forwarding`")?;
dict.into_empty()
.map_err(|e| anyhow!("unrecognized fields in interface `{}`: {:?}", name, e))?;
Ok(Interface {
name,
without_autogenerated_addresses,
static_ips,
gateway,
enable_ipv4_forwarding,
enable_ipv6_forwarding,
})
}
}
impl TryFrom<Dictionary> for UnvalidatedConfig {
type Error = anyhow::Error;
fn try_from(mut program: Dictionary) -> Result<Self, Self::Error> {
let networks = program
.try_take_obj_vec("networks")
.context("could not find `networks`")?
.map_err(|e| anyhow!("`networks` is not a list of objects: {:?}", e))?
.into_iter()
.map(|dict| Dictionary::try_from(dict).and_then(Network::try_from))
.collect::<Result<Vec<_>, _>>()?;
let netstacks = program
.try_take_obj_vec("netstacks")
.context("could not find `netstacks`")?
.map_err(|e| anyhow!("`netstacks` is not a list of objects: {:?}", e))?
.into_iter()
.map(|dict| Dictionary::try_from(dict).and_then(Netstack::try_from))
.collect::<Result<Vec<_>, _>>()?;
let eager_components = program
.try_take_str_vec("start")
.transpose()
.map_err(|e| anyhow!("`start` is not a list of strings: {:?}", e))?
.unwrap_or_default();
program.into_empty().map_err(|e| anyhow!("unrecognized fields in `program`: {:?}", e))?;
Ok(Self { networks, netstacks, eager_components })
}
}
#[derive(thiserror::Error, Debug, PartialEq)]
pub(crate) enum Error {
#[error("duplicate network `{0}`, network names must be unique")]
DuplicateNetwork(String),
#[error("duplicate netstack `{0}`, netstack names must be unique")]
DuplicateNetstack(String),
#[error("duplicate endpoint `{0}`, endpoint names must be unique")]
DuplicateEndpoint(String),
#[error("endpoint `{0}` assigned to a netstack multiple times")]
EndpointAssignedMultipleTimes(String),
#[error(
"endpoint name '{0}' exceeds maximum interface name length of {}",
fnet_interfaces::INTERFACE_NAME_LENGTH
)]
EndpointNameExceedsMaximumLength(String),
#[error("unknown endpoint `{0}`, must be declared on a network")]
UnknownEndpoint(String),
#[error("duplicate eager component `{0}`, component names must be unique")]
DuplicateEagerComponent(String),
}
impl UnvalidatedConfig {
fn validate(self) -> Result<Config, Error> {
let Self { networks, netstacks, eager_components } = &self;
let mut network_names = HashSet::new();
let mut installed_endpoints = HashMap::new();
for Network { name, endpoints } in networks {
if !network_names.insert(name) {
return Err(Error::DuplicateNetwork(name.to_string()));
}
for Endpoint { name, .. } in endpoints {
if let Some(_) = installed_endpoints.insert(name, false) {
return Err(Error::DuplicateEndpoint(name.to_string()));
}
if name.len() > fnet_interfaces::INTERFACE_NAME_LENGTH.into() {
return Err(Error::EndpointNameExceedsMaximumLength(name.to_string()));
}
}
}
let mut netstack_names = HashSet::new();
for Netstack { name, interfaces } in netstacks {
if !netstack_names.insert(name) {
return Err(Error::DuplicateNetstack(name.to_string()));
}
for Interface { name, .. } in interfaces {
match installed_endpoints.entry(&name) {
hash_map::Entry::Occupied(mut entry) => {
if *entry.get() {
return Err(Error::EndpointAssignedMultipleTimes(name.to_string()));
} else {
*entry.get_mut() = true;
}
}
hash_map::Entry::Vacant(_) => {
return Err(Error::UnknownEndpoint(name.to_string()));
}
}
}
}
let mut eager_component_names = HashSet::new();
for name in eager_components {
if !eager_component_names.insert(name) {
return Err(Error::DuplicateEagerComponent(name.to_string()));
}
}
let Self { networks, netstacks, eager_components } = self;
Ok(Config { networks, netstacks, eager_components })
}
}
/// A handle to the virtual network environment created for the test.
///
/// This encodes the lifetime of the network environment, as the netemul sandbox
/// and any networks or endpoints it contains are garbage-collected when the
/// client end of their control channel is dropped.
pub(crate) struct NetworkEnvironment {
// We only start a test sandbox if the requested configuration requires it.
_sandbox: Option<netemul::TestSandbox>,
_networks: HashMap<String, fnetemul_network::NetworkProxy>,
_endpoints: HashMap<String, fnetemul_network::EndpointProxy>,
}
impl Config {
/// Loads the virtual network configuration that the test should be run in.
pub(crate) fn load_from_program(program: fdata::Dictionary) -> Result<Self, anyhow::Error> {
let program = Dictionary::try_from(program).context("program into dictionary")?;
UnvalidatedConfig::try_from(program)
.context("parsing config from `program`")?
.validate()
.context("validating config")
}
/// Applies the virtual network configuration.
///
/// A netemul sandbox is used to create virtual networks and endpoints, and the
/// netstacks to be configured are connected to with `connect_to_netstack`.
///
/// Returns a handle to the network environment.
pub(crate) async fn apply<F>(
self,
mut connect_to_netstack: F,
lifecycle_controller: fsys2::LifecycleControllerProxy,
) -> Result<NetworkEnvironment, anyhow::Error>
where
F: FnMut(String) -> Result<fnetemul::ConfigurableNetstackProxy, anyhow::Error>,
{
info!("configuring environment for test: {:#?}", self);
let Self { networks, netstacks, eager_components } = self;
// Create the networks and endpoints in a netemul sandbox.
let mut maybe_sandbox = None;
let mut network_handles = HashMap::new();
let mut endpoint_handles = HashMap::new();
for Network { name, endpoints } in networks {
let sandbox = if let Some(sandbox) = maybe_sandbox.as_ref() {
sandbox
} else {
maybe_sandbox.insert(netemul::TestSandbox::new().context("create test sandbox")?)
};
let network = sandbox
.create_network(name.clone())
.await
.with_context(|| format!("create network `{}`", name))?;
for Endpoint { name, mac, mtu, up } in endpoints {
let endpoint = network
.create_endpoint_with(
name.clone(),
fnetemul_network::EndpointConfig {
mac: mac.map(Into::into).map(Box::new),
mtu,
port_class: fidl_fuchsia_hardware_network::PortClass::Virtual,
},
)
.await
.with_context(|| format!("create endpoint `{}`", name))?;
if up {
endpoint.set_link_up(true).await.context("set link up")?;
}
assert!(endpoint_handles.insert(name, endpoint.into_proxy()).is_none());
}
assert!(network_handles.insert(name, network.into_proxy()).is_none());
}
// Configure the netstacks.
for Netstack { name, interfaces } in netstacks {
debug!("configuring netstack `{}`", name);
let netstack = connect_to_netstack(name).context("connect to configurable netstack")?;
for Interface {
name,
without_autogenerated_addresses,
static_ips,
gateway,
enable_ipv4_forwarding,
enable_ipv6_forwarding,
} in interfaces
{
debug!("configuring interface `{}` with static IPs {:?}", name, static_ips);
let (port, server_end) = fidl::endpoints::create_endpoints();
endpoint_handles
.get(&name)
.with_context(|| format!("could not find endpoint `{}`", name))?
.get_port(server_end)
.with_context(|| format!("retrieve device from test endpoint `{}`", name))?;
let options = fnetemul::InterfaceOptions {
name: Some(name.to_string()),
device: Some(port),
without_autogenerated_addresses: Some(without_autogenerated_addresses),
static_ips: Some(static_ips.into_iter().map(Into::into).collect()),
gateway: gateway.map(Into::into),
enable_ipv4_forwarding: Some(enable_ipv4_forwarding),
enable_ipv6_forwarding: Some(enable_ipv6_forwarding),
..Default::default()
};
netstack
.configure_interface(options)
.await
.context("call configure interface")?
.map_err(|e| anyhow!("error configuring netstack: {:?}", e))?;
}
}
// Start all the eager components now that test setup is complete.
for component in eager_components {
let (_, binder_server) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_component::BinderMarker>();
lifecycle_controller
.start_instance(&format!("./{}", component), binder_server)
.await
.context("call start")?
.map_err(|e| anyhow!("failed to start component '{}': {:?}", component, e))?
}
Ok(NetworkEnvironment {
_sandbox: maybe_sandbox,
_networks: network_handles,
_endpoints: endpoint_handles,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use futures::{channel::mpsc, StreamExt as _, TryStreamExt as _};
use net_declare::{fidl_ip, fidl_mac, fidl_subnet};
use test_case::test_case;
const LOCAL_NETSTACK: &str = "local";
const REMOTE_NETSTACK: &str = "remote";
fn example_config() -> Config {
Config {
netstacks: vec![
Netstack {
name: LOCAL_NETSTACK.to_string(),
interfaces: vec![
Interface {
name: "local-ep1".to_string(),
without_autogenerated_addresses: true,
static_ips: vec![fidl_subnet!("192.168.0.2/24").into()],
gateway: Some(fidl_ip!("192.168.1.1").into()),
enable_ipv4_forwarding: true,
enable_ipv6_forwarding: true,
},
Interface {
name: "local-ep2".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![fidl_subnet!("192.168.0.3/24").into()],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
},
],
},
Netstack {
name: REMOTE_NETSTACK.to_string(),
interfaces: vec![Interface {
name: "remote-ep".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![fidl_subnet!("192.168.0.1/24").into()],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
}],
},
],
networks: vec![Network {
name: "net".to_string(),
endpoints: vec![
Endpoint {
name: "local-ep1".to_string(),
mac: Some(fidl_mac!("aa:bb:cc:dd:ee:ff").into()),
mtu: 999,
up: false,
},
Endpoint {
name: "local-ep2".to_string(),
mac: None,
mtu: Endpoint::default_mtu(),
up: Endpoint::default_link_up(),
},
Endpoint {
name: "remote-ep".to_string(),
mac: None,
mtu: Endpoint::default_mtu(),
up: Endpoint::default_link_up(),
},
],
}],
eager_components: vec!["foo".to_string(), "bar".to_string()],
}
}
fn program_from_str(program: &str) -> fdata::Dictionary {
let object = serde_json::from_str(program).expect("deserialize JSON object");
let cm_rust::ComponentDecl { program, .. } =
cm_rust_testing::new_decl_from_json(object).expect("deserialize component decl");
let cm_rust::ProgramDecl { info, runner: _ } =
program.expect("`program` not set in component decl");
info
}
#[test]
fn valid_config() {
let file = r#"
{
"program": {
"runner": "elf",
"netstacks": [
{
"name": "local",
"interfaces": [
{
"name": "local-ep1",
"without_autogenerated_addresses": "true",
"static_ips": [ "192.168.0.2/24" ],
"gateway": "192.168.1.1",
"enable_ipv4_forwarding": "true",
"enable_ipv6_forwarding": "true"
},
{
"name": "local-ep2",
"static_ips": [ "192.168.0.3/24" ]
}
]
},
{
"name": "remote",
"interfaces": [
{
"name": "remote-ep",
"static_ips": [ "192.168.0.1/24" ]
}
]
}
],
"networks": [
{
"name": "net",
"endpoints": [
{
"name": "local-ep1",
"mac": "aa:bb:cc:dd:ee:ff",
"mtu": "999",
"up": "false"
},
{
"name": "local-ep2"
},
{
"name": "remote-ep"
}
]
}
],
"start": ["foo", "bar"]
}
}
"#;
let program = program_from_str(file);
let config = Config::load_from_program(program).expect("parse network config");
assert_eq!(config, example_config());
}
#[test_case(r#"{ "program": { "runner": "elf", "netstacks": [] } }"#; "missing required field `networks`")]
#[test_case(
r#"{
"program": {
"runner": "elf",
"netstacks": [],
"networks": [],
"endpoints": []
}
}"#;
"unknown field `endpoints`"
)]
#[test_case(
r#"{
"program": {
"runner": "elf",
"netstacks": [],
"networks": [
{
"name": "net",
"endpoints": [
{
"name": "ep",
"mtu": "65536"
}
]
}
]
}
}"#;
"invalid endpoint MTU (larger than `u16::MAX`)"
)]
#[test_case(
r#"{
"program": {
"runner": "elf",
"netstacks": [
{
"name": "ns",
"interfaces": [
{
"name": "if",
"static_ips": ["not an IP address"]
}
]
}
],
"networks": []
}
}"#;
"invalid static interface address"
)]
fn invalid_parse(s: &str) {
let program = program_from_str(s);
let program = Dictionary::try_from(program).expect("program into dictionary");
assert_matches!(UnvalidatedConfig::try_from(program), Err(_));
}
#[test_case(
UnvalidatedConfig {
netstacks: vec![
Netstack {
name: "netstack".to_string(),
interfaces: vec![
Interface {
name: "ep".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
},
],
},
],
networks: vec![],
eager_components: vec![],
},
Error::UnknownEndpoint("ep".to_string());
"netstack interfaces must be declared as endpoints on a network"
)]
#[test_case(
UnvalidatedConfig {
netstacks: vec![
Netstack {
name: "netstack".to_string(),
interfaces: vec![],
},
Netstack {
name: "netstack".to_string(),
interfaces: vec![],
},
],
networks: vec![],
eager_components: vec![],
},
Error::DuplicateNetstack("netstack".to_string());
"netstack names must be unique"
)]
#[test_case(
UnvalidatedConfig {
netstacks: vec![],
networks: vec![
Network {
name: "net".to_string(),
endpoints: vec![],
},
Network {
name: "net".to_string(),
endpoints: vec![],
},
],
eager_components: vec![],
},
Error::DuplicateNetwork("net".to_string());
"network names must be unique"
)]
#[test_case(
UnvalidatedConfig {
netstacks: vec![],
networks: vec![
Network {
name: "net".to_string(),
endpoints: vec![
Endpoint {
name: "ep".to_string(),
mac: None,
mtu: Endpoint::default_mtu(),
up: Endpoint::default_link_up(),
},
Endpoint {
name: "ep".to_string(),
mac: None,
mtu: Endpoint::default_mtu(),
up: Endpoint::default_link_up(),
},
],
},
],
eager_components: vec![],
},
Error::DuplicateEndpoint("ep".to_string());
"endpoint names must be unique"
)]
#[test_case(
UnvalidatedConfig {
netstacks: vec![],
networks: vec![
Network {
name: "net".to_string(),
endpoints: vec![
Endpoint {
name: "overly-long-ep-name".to_string(),
mac: None,
mtu: Endpoint::default_mtu(),
up: Endpoint::default_link_up(),
},
],
},
],
eager_components: vec![],
},
Error::EndpointNameExceedsMaximumLength("overly-long-ep-name".to_string());
"endpoint name must be <= maximum interface name length"
)]
#[test_case(
UnvalidatedConfig {
netstacks: vec![
Netstack {
name: "ns1".to_string(),
interfaces: vec![
Interface {
name: "ep".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
},
],
},
Netstack {
name: "ns2".to_string(),
interfaces: vec![
Interface {
name: "ep".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
},
],
},
],
networks: vec![
Network {
name: "net".to_string(),
endpoints: vec![
Endpoint {
name: "ep".to_string(),
mac: None,
mtu: Endpoint::default_mtu(),
up: Endpoint::default_link_up(),
},
],
},
],
eager_components: vec![],
},
Error::EndpointAssignedMultipleTimes("ep".to_string());
"endpoints may only be assigned once to a single netstack"
)]
#[test_case(
UnvalidatedConfig {
netstacks: vec![],
networks: vec![],
eager_components: vec!["server".to_string(), "server".to_string()],
},
Error::DuplicateEagerComponent("server".to_string());
"eager components must be unique"
)]
fn invalid_config(config: UnvalidatedConfig, error: Error) {
assert_eq!(config.validate(), Err(error));
}
async fn expect_incoming_requests(
rx: &mut mpsc::UnboundedReceiver<(String, fnetemul::ConfigurableNetstackRequestStream)>,
expected_netstack: &str,
expected_configs: Vec<Interface>,
) {
let (netstack, mut stream) =
rx.next().await.expect("no connection requests for mock netstack");
assert_eq!(
netstack, expected_netstack,
"expected request for netstack '{}', got '{}'",
expected_netstack, netstack
);
for expected_config in expected_configs {
let fnetemul::ConfigurableNetstackRequest::ConfigureInterface { payload, responder } =
stream
.next()
.await
.expect("expected request not received by mock configurable netstack")
.expect("FIDL error on request");
let fnetemul::InterfaceOptions {
name,
without_autogenerated_addresses,
static_ips,
gateway,
enable_ipv4_forwarding,
enable_ipv6_forwarding,
device: _,
..
} = payload;
assert_eq!(
Interface {
name: name.expect("missing interface name"),
without_autogenerated_addresses: without_autogenerated_addresses
.unwrap_or_default(),
static_ips: static_ips
.unwrap_or_default()
.into_iter()
.map(Into::into)
.collect(),
gateway: gateway.map(Into::into),
enable_ipv4_forwarding: enable_ipv4_forwarding.unwrap_or_default(),
enable_ipv6_forwarding: enable_ipv6_forwarding.unwrap_or_default(),
},
expected_config
);
responder.send(Ok(())).expect("send response");
}
let remaining = stream
.map_ok(
|fnetemul::ConfigurableNetstackRequest::ConfigureInterface {
payload,
responder: _,
}| payload,
)
.try_collect::<Vec<_>>()
.await
.expect("collect remaining requests");
assert_eq!(remaining, vec![]);
}
#[fuchsia::test]
async fn configurable_netstack() {
let (tx, mut rx) = mpsc::unbounded();
let (controller, server_end) =
fidl::endpoints::create_proxy::<fsys2::LifecycleControllerMarker>()
.expect("create proxy");
drop(server_end);
let configure_environment = async {
Config {
// Don't test eager components here.
eager_components: vec![],
..example_config()
}
.apply(
|name| {
let (proxy, server_end) =
fidl::endpoints::create_proxy::<fnetemul::ConfigurableNetstackMarker>()
.context("create proxy")?;
let stream =
server_end.into_stream().context("server end into request stream")?;
tx.unbounded_send((name, stream))
.expect("request stream receiver should not be closed");
Ok(proxy)
},
controller,
)
.await
.expect("configure network environment for test")
};
let mock_netstack = async {
// Expect netstacks to be configured in the order in which they're declared: the
// "local" netstack first and "remote" second. The same order applies to the
// interfaces that are installed in the netstacks.
expect_incoming_requests(
&mut rx,
LOCAL_NETSTACK,
vec![
Interface {
name: "local-ep1".to_string(),
without_autogenerated_addresses: true,
static_ips: vec![fidl_subnet!("192.168.0.2/24").into()],
gateway: Some(fidl_ip!("192.168.1.1").into()),
enable_ipv4_forwarding: true,
enable_ipv6_forwarding: true,
},
Interface {
name: "local-ep2".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![fidl_subnet!("192.168.0.3/24").into()],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
},
],
)
.await;
expect_incoming_requests(
&mut rx,
REMOTE_NETSTACK,
vec![Interface {
name: "remote-ep".to_string(),
without_autogenerated_addresses: false,
static_ips: vec![fidl_subnet!("192.168.0.1/24").into()],
gateway: None,
enable_ipv4_forwarding: false,
enable_ipv6_forwarding: false,
}],
)
.await;
};
let (NetworkEnvironment { _sandbox: sandbox, _networks, _endpoints }, ()) =
futures::future::join(configure_environment, mock_netstack).await;
let sandbox = sandbox.expect("sandbox was not configured");
let network_manager = sandbox.get_network_manager().expect("get network manager");
let networks = network_manager.list_networks().await.expect("list virtual networks");
assert_eq!(networks, vec!["net".to_string()]);
let endpoint_manager = sandbox.get_endpoint_manager().expect("get endpoint manager");
let mut endpoints =
endpoint_manager.list_endpoints().await.expect("list virtual endpoints");
endpoints.sort();
assert_eq!(
endpoints,
vec!["local-ep1".to_string(), "local-ep2".to_string(), "remote-ep".to_string()],
);
}
#[fuchsia::test]
async fn eager_components() {
let (controller, controller_requests) =
fidl::endpoints::create_proxy_and_stream::<fsys2::LifecycleControllerMarker>()
.expect("create proxy and stream");
let configure_environment = async {
let config = Config {
networks: vec![],
netstacks: vec![],
eager_components: vec!["foo".to_string(), "bar".to_string(), "baz".to_string()],
};
config
.apply(
|_name| {
let (proxy, server_end) =
fidl::endpoints::create_proxy::<fnetemul::ConfigurableNetstackMarker>()
.context("create proxy")?;
drop(server_end);
Ok(proxy)
},
controller,
)
.await
.expect("configure network environment for test")
};
let mock_lifecycle_controller = controller_requests
.map(|request| {
let (moniker, _, responder) = request
.expect("get request")
.into_start_instance()
.expect("unexpected lifecycle controller request");
responder.send(Ok(())).expect("send response");
moniker
})
.collect::<Vec<String>>();
let (_network_environment, started_components) =
futures::future::join(configure_environment, mock_lifecycle_controller).await;
assert_eq!(started_components, vec!["./foo", "./bar", "./baz"]);
}
}