| // 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"]); |
| } |
| } |