| // Copyright 2018 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. |
| |
| mod devices; |
| mod dhcpv4; |
| mod dhcpv6; |
| mod dns; |
| mod errors; |
| mod interface; |
| mod virtualization; |
| |
| use dhcp::protocol::FromFidlExt as _; |
| use std::collections::{hash_map::Entry, HashMap, HashSet}; |
| use std::convert::TryInto as _; |
| use std::fs; |
| use std::io; |
| use std::path; |
| use std::pin::Pin; |
| use std::str::FromStr; |
| |
| use fidl::endpoints::RequestStream as _; |
| use fidl_fuchsia_io as fio; |
| use fidl_fuchsia_net as fnet; |
| use fidl_fuchsia_net_debug as fnet_debug; |
| use fidl_fuchsia_net_dhcp as fnet_dhcp; |
| use fidl_fuchsia_net_dhcpv6 as fnet_dhcpv6; |
| use fidl_fuchsia_net_ext::{self as fnet_ext, DisplayExt as _, IpExt as _}; |
| use fidl_fuchsia_net_filter as fnet_filter; |
| use fidl_fuchsia_net_interfaces as fnet_interfaces; |
| use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin; |
| use fidl_fuchsia_net_interfaces_ext::{self as fnet_interfaces_ext, Update as _}; |
| use fidl_fuchsia_net_name as fnet_name; |
| use fidl_fuchsia_net_stack as fnet_stack; |
| use fidl_fuchsia_netstack as fnetstack; |
| use fuchsia_async::DurationExt as _; |
| use fuchsia_component::client::{clone_namespace_svc, new_protocol_connector_in_dir}; |
| use fuchsia_component::server::{ServiceFs, ServiceFsDir}; |
| use fuchsia_vfs_watcher as fvfs_watcher; |
| use fuchsia_zircon::{self as zx, DurationNum as _}; |
| |
| use anyhow::{anyhow, Context as _}; |
| use async_trait::async_trait; |
| use async_utils::stream::TryFlattenUnorderedExt as _; |
| use dns_server_watcher::{DnsServers, DnsServersUpdateSource, DEFAULT_DNS_PORT}; |
| use fuchsia_fs::{open_directory_in_namespace, OpenFlags}; |
| use futures::{StreamExt as _, TryFutureExt as _, TryStreamExt as _}; |
| use net_declare::fidl_ip_v4; |
| use serde::Deserialize; |
| use tracing::{debug, error, info, trace, warn}; |
| |
| use self::devices::DeviceInfo; |
| use self::errors::ContextExt as _; |
| |
| /// Interface metrics. |
| /// |
| /// Interface metrics are used to sort the route table. An interface with a |
| /// lower metric is favored over one with a higher metric. |
| #[derive(Clone, Copy, Debug, Deserialize, PartialEq)] |
| pub struct Metric(u32); |
| |
| impl Default for Metric { |
| // A default value of 600 is chosen for Metric: this provides plenty of space for routing |
| // policy on devices with many interfaces (physical or logical) while remaining in the same |
| // magnitude as our current default ethernet metric. |
| fn default() -> Self { |
| Self(600) |
| } |
| } |
| |
| impl std::fmt::Display for Metric { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let Metric(u) = self; |
| write!(f, "{}", u) |
| } |
| } |
| |
| impl From<Metric> for u32 { |
| fn from(Metric(u): Metric) -> u32 { |
| u |
| } |
| } |
| |
| /// File that stores persistent interface configurations. |
| const PERSISTED_INTERFACE_CONFIG_FILEPATH: &str = "/data/net_interfaces.cfg.json"; |
| |
| /// A node that represents the directory it is in. |
| /// |
| /// `/dir` and `/dir/.` point to the same directory. |
| const THIS_DIRECTORY: &str = "."; |
| |
| /// The prefix length for the address assigned to a WLAN AP interface. |
| const WLAN_AP_PREFIX_LEN: u8 = 29; |
| |
| /// The address for the network the WLAN AP interface is a part of. |
| const WLAN_AP_NETWORK_ADDR: fnet::Ipv4Address = fidl_ip_v4!("192.168.255.248"); |
| |
| /// The lease time for a DHCP lease. |
| /// |
| /// 1 day in seconds. |
| const WLAN_AP_DHCP_LEASE_TIME_SECONDS: u32 = 24 * 60 * 60; |
| |
| /// The maximum number of times to attempt to add a device. |
| const MAX_ADD_DEVICE_ATTEMPTS: u8 = 3; |
| |
| /// A map of DNS server watcher streams that yields `DnsServerWatcherEvent` as DNS |
| /// server updates become available. |
| /// |
| /// DNS server watcher streams may be added or removed at runtime as the watchers |
| /// are started or stopped. |
| type DnsServerWatchers<'a> = async_utils::stream::StreamMap< |
| DnsServersUpdateSource, |
| futures::stream::BoxStream< |
| 'a, |
| (DnsServersUpdateSource, Result<Vec<fnet_name::DnsServer_>, anyhow::Error>), |
| >, |
| >; |
| |
| /// Defines log levels. |
| #[derive(Debug, Copy, Clone)] |
| pub struct LogLevel(diagnostics_log::Severity); |
| |
| impl Default for LogLevel { |
| fn default() -> Self { |
| Self(diagnostics_log::Severity::Info) |
| } |
| } |
| |
| impl FromStr for LogLevel { |
| type Err = anyhow::Error; |
| |
| fn from_str(s: &str) -> Result<Self, anyhow::Error> { |
| match s.to_uppercase().as_str() { |
| "TRACE" => Ok(Self(diagnostics_log::Severity::Trace)), |
| "DEBUG" => Ok(Self(diagnostics_log::Severity::Debug)), |
| "INFO" => Ok(Self(diagnostics_log::Severity::Info)), |
| "WARN" => Ok(Self(diagnostics_log::Severity::Warn)), |
| "ERROR" => Ok(Self(diagnostics_log::Severity::Error)), |
| "FATAL" => Ok(Self(diagnostics_log::Severity::Fatal)), |
| _ => Err(anyhow::anyhow!("unrecognized log level = {}", s)), |
| } |
| } |
| } |
| |
| /// Network Configuration tool. |
| /// |
| /// Configures network components in response to events. |
| #[derive(argh::FromArgs, Debug)] |
| struct Opt { |
| // TODO(https://fxbug.dev/85683): remove once we use netdevice and netcfg no |
| // longer has to differentiate between virtual or physical devices. |
| /// should netemul specific configurations be used? |
| #[argh(switch)] |
| allow_virtual_devices: bool, |
| |
| /// minimum severity for logs |
| #[argh(option, default = "Default::default()")] |
| min_severity: LogLevel, |
| |
| /// config file to use |
| #[argh(option, default = "\"/config/data/default.json\".to_string()")] |
| config_data: String, |
| } |
| |
| #[derive(Debug, Deserialize)] |
| #[serde(deny_unknown_fields)] |
| pub struct DnsConfig { |
| pub servers: Vec<std::net::IpAddr>, |
| } |
| |
| #[derive(Debug, Deserialize)] |
| #[serde(deny_unknown_fields)] |
| pub struct FilterConfig { |
| pub rules: Vec<String>, |
| pub nat_rules: Vec<String>, |
| pub rdr_rules: Vec<String>, |
| } |
| |
| #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Deserialize)] |
| #[serde(deny_unknown_fields, rename_all = "lowercase")] |
| enum InterfaceType { |
| Ethernet, |
| Wlan, |
| } |
| |
| #[cfg_attr(test, derive(PartialEq))] |
| #[derive(Debug, Default, Deserialize)] |
| #[serde(deny_unknown_fields)] |
| pub struct InterfaceMetrics { |
| #[serde(default)] |
| pub wlan_metric: Metric, |
| #[serde(default)] |
| pub eth_metric: Metric, |
| } |
| |
| #[derive(Debug, PartialEq, Eq, Hash, Deserialize)] |
| #[serde(deny_unknown_fields, rename_all = "lowercase")] |
| pub enum DeviceClass { |
| Virtual, |
| Ethernet, |
| Wlan, |
| Ppp, |
| Bridge, |
| WlanAp, |
| } |
| |
| impl From<fidl_fuchsia_hardware_network::DeviceClass> for DeviceClass { |
| fn from(device_class: fidl_fuchsia_hardware_network::DeviceClass) -> Self { |
| match device_class { |
| fidl_fuchsia_hardware_network::DeviceClass::Virtual => DeviceClass::Virtual, |
| fidl_fuchsia_hardware_network::DeviceClass::Ethernet => DeviceClass::Ethernet, |
| fidl_fuchsia_hardware_network::DeviceClass::Wlan => DeviceClass::Wlan, |
| fidl_fuchsia_hardware_network::DeviceClass::Ppp => DeviceClass::Ppp, |
| fidl_fuchsia_hardware_network::DeviceClass::Bridge => DeviceClass::Bridge, |
| fidl_fuchsia_hardware_network::DeviceClass::WlanAp => DeviceClass::WlanAp, |
| } |
| } |
| } |
| |
| #[derive(Debug, PartialEq, Deserialize)] |
| #[serde(transparent)] |
| struct AllowedDeviceClasses(HashSet<DeviceClass>); |
| |
| impl Default for AllowedDeviceClasses { |
| fn default() -> Self { |
| // When new variants are added, this exhaustive match will cause a compilation failure as a |
| // reminder to add the new variant to the default array. |
| match DeviceClass::Virtual { |
| DeviceClass::Virtual |
| | DeviceClass::Ethernet |
| | DeviceClass::Wlan |
| | DeviceClass::Ppp |
| | DeviceClass::Bridge |
| | DeviceClass::WlanAp => {} |
| } |
| Self(HashSet::from([ |
| DeviceClass::Virtual, |
| DeviceClass::Ethernet, |
| DeviceClass::Wlan, |
| DeviceClass::Ppp, |
| DeviceClass::Bridge, |
| DeviceClass::WlanAp, |
| ])) |
| } |
| } |
| |
| // TODO(https://github.com/serde-rs/serde/issues/368): use an inline literal for the default value |
| // rather than defining a one-off function. |
| fn dhcpv6_enabled_default() -> bool { |
| true |
| } |
| |
| #[derive(Debug, Default, Deserialize, PartialEq)] |
| #[serde(deny_unknown_fields, rename_all = "lowercase")] |
| struct ForwardedDeviceClasses { |
| #[serde(default)] |
| pub ipv4: HashSet<DeviceClass>, |
| #[serde(default)] |
| pub ipv6: HashSet<DeviceClass>, |
| } |
| |
| #[derive(Debug, Deserialize)] |
| #[serde(deny_unknown_fields)] |
| struct Config { |
| pub dns_config: DnsConfig, |
| pub filter_config: FilterConfig, |
| pub filter_enabled_interface_types: HashSet<InterfaceType>, |
| #[serde(default)] |
| pub interface_metrics: InterfaceMetrics, |
| #[serde(default)] |
| pub allowed_upstream_device_classes: AllowedDeviceClasses, |
| // TODO(https://fxbug.dev/92096): default to false. |
| #[serde(default = "dhcpv6_enabled_default")] |
| pub enable_dhcpv6: bool, |
| #[serde(default)] |
| pub forwarded_device_classes: ForwardedDeviceClasses, |
| } |
| |
| impl Config { |
| pub fn load<P: AsRef<path::Path>>(path: P) -> Result<Self, anyhow::Error> { |
| let path = path.as_ref(); |
| let file = fs::File::open(path) |
| .with_context(|| format!("could not open the config file {}", path.display()))?; |
| let config = serde_json::from_reader(io::BufReader::new(file)) |
| .with_context(|| format!("could not deserialize the config file {}", path.display()))?; |
| Ok(config) |
| } |
| } |
| |
| #[derive(Clone, Debug)] |
| struct InterfaceConfig { |
| name: String, |
| metric: u32, |
| } |
| |
| // We use Compare-And-Swap (CAS) protocol to update filter rules. $get_rules returns the current |
| // generation number. $update_rules will send it with new rules to make sure we are updating the |
| // intended generation. If the generation number doesn't match, $update_rules will return a |
| // GenerationMismatch error, then we have to restart from $get_rules. |
| |
| const FILTER_CAS_RETRY_MAX: i32 = 3; |
| const FILTER_CAS_RETRY_INTERVAL_MILLIS: i64 = 500; |
| |
| macro_rules! cas_filter_rules { |
| ($filter:expr, $get_rules:ident, $update_rules:ident, $rules:expr, $error_type:ident) => { |
| for retry in 0..FILTER_CAS_RETRY_MAX { |
| let generation = match $filter.$get_rules().await? { |
| Ok((_rules, generation)) => generation, |
| Err(e) => { |
| return Err(anyhow::anyhow!("{} failed: {:?}", stringify!($get_rules), e)); |
| } |
| }; |
| match $filter.$update_rules(&mut $rules.iter_mut(), generation).await.with_context( |
| || format!("error getting response from {}", stringify!($update_rules)), |
| )? { |
| Ok(()) => { |
| break; |
| } |
| Err(fnet_filter::$error_type::GenerationMismatch) |
| if retry < FILTER_CAS_RETRY_MAX - 1 => |
| { |
| fuchsia_async::Timer::new( |
| FILTER_CAS_RETRY_INTERVAL_MILLIS.millis().after_now(), |
| ) |
| .await; |
| } |
| Err(e) => { |
| let () = Err(anyhow::anyhow!("{} failed: {:?}", stringify!($update_rules), e))?; |
| } |
| } |
| } |
| }; |
| } |
| |
| // This is a placeholder macro while some update operations are not supported. |
| macro_rules! no_update_filter_rules { |
| ($filter:expr, $get_rules:ident, $update_rules:ident, $rules:expr, $error_type:ident) => { |
| let generation = match $filter.$get_rules().await? { |
| Ok((_rules, generation)) => generation, |
| Err(e) => { |
| return Err(anyhow::anyhow!("{} failed: {:?}", stringify!($get_rules), e)); |
| } |
| }; |
| match $filter |
| .$update_rules(&mut $rules.iter_mut(), generation) |
| .await |
| .with_context(|| format!("error getting response from {}", stringify!($update_rules)))? |
| { |
| Ok(()) => {} |
| Err(fnet_filter::$error_type::NotSupported) => { |
| error!("{} not supported", stringify!($update_rules)); |
| } |
| } |
| }; |
| } |
| |
| fn should_enable_filter( |
| filter_enabled_interface_types: &HashSet<InterfaceType>, |
| info: &DeviceInfo, |
| ) -> bool { |
| filter_enabled_interface_types.contains(&info.interface_type()) |
| } |
| |
| #[derive(Debug)] |
| struct InterfaceState { |
| // Hold on to control to enforce interface ownership, even if unused. |
| control: fidl_fuchsia_net_interfaces_ext::admin::Control, |
| config: InterfaceConfigState, |
| } |
| |
| /// State for an interface. |
| #[derive(Debug)] |
| enum InterfaceConfigState { |
| Host(HostInterfaceState), |
| WlanAp(WlanApInterfaceState), |
| } |
| |
| #[derive(Debug)] |
| struct HostInterfaceState { |
| dhcpv6_client_addr: Option<fnet::Ipv6SocketAddress>, |
| } |
| |
| #[derive(Debug)] |
| struct WlanApInterfaceState {} |
| |
| impl InterfaceState { |
| fn new_host(control: fidl_fuchsia_net_interfaces_ext::admin::Control) -> Self { |
| Self { |
| control, |
| config: InterfaceConfigState::Host(HostInterfaceState { dhcpv6_client_addr: None }), |
| } |
| } |
| |
| fn new_wlan_ap(control: fidl_fuchsia_net_interfaces_ext::admin::Control) -> Self { |
| Self { control, config: InterfaceConfigState::WlanAp(WlanApInterfaceState {}) } |
| } |
| |
| fn is_wlan_ap(&self) -> bool { |
| let Self { control: _, config } = self; |
| match config { |
| InterfaceConfigState::Host(_) => false, |
| InterfaceConfigState::WlanAp(_) => true, |
| } |
| } |
| |
| /// Handles the interface being discovered. |
| async fn on_discovery( |
| &mut self, |
| properties: &fnet_interfaces_ext::Properties, |
| dhcpv6_client_provider: Option<&fnet_dhcpv6::ClientProviderProxy>, |
| watchers: &mut DnsServerWatchers<'_>, |
| ) -> Result<(), errors::Error> { |
| let Self { control: _, config } = self; |
| let fnet_interfaces_ext::Properties { online, .. } = properties; |
| match config { |
| InterfaceConfigState::Host(HostInterfaceState { dhcpv6_client_addr }) => { |
| if !online { |
| return Ok(()); |
| } |
| let dhcpv6_client_provider = match dhcpv6_client_provider { |
| Some(p) => p, |
| None => return Ok(()), |
| }; |
| *dhcpv6_client_addr = |
| start_dhcpv6_client(properties, dhcpv6_client_provider, watchers)?; |
| } |
| InterfaceConfigState::WlanAp(WlanApInterfaceState {}) => {} |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| /// Network Configuration state. |
| pub struct NetCfg<'a> { |
| stack: fnet_stack::StackProxy, |
| netstack: fnetstack::NetstackProxy, |
| lookup_admin: fnet_name::LookupAdminProxy, |
| filter: fnet_filter::FilterProxy, |
| interface_state: fnet_interfaces::StateProxy, |
| installer: fidl_fuchsia_net_interfaces_admin::InstallerProxy, |
| // TODO(https://fxbug.dev/74532): We won't need to reach out to debug once |
| // we don't have Ethernet interfaces anymore. |
| debug: fidl_fuchsia_net_debug::InterfacesProxy, |
| dhcp_server: Option<fnet_dhcp::Server_Proxy>, |
| dhcpv6_client_provider: Option<fnet_dhcpv6::ClientProviderProxy>, |
| |
| allow_virtual_devices: bool, |
| |
| persisted_interface_config: interface::FileBackedConfig<'a>, |
| |
| filter_enabled_interface_types: HashSet<InterfaceType>, |
| |
| // TODO(https://fxbug.dev/67407): These hashmaps are all indexed by |
| // interface ID and store per-interface state, and should be merged. |
| interface_states: HashMap<u64, InterfaceState>, |
| interface_properties: HashMap<u64, fnet_interfaces_ext::Properties>, |
| interface_metrics: InterfaceMetrics, |
| |
| dns_servers: DnsServers, |
| |
| forwarded_device_classes: ForwardedDeviceClasses, |
| } |
| |
| /// Returns a [`fnet_name::DnsServer_`] with a static source from a [`std::net::IpAddr`]. |
| fn static_source_from_ip(f: std::net::IpAddr) -> fnet_name::DnsServer_ { |
| let socket_addr = match fnet_ext::IpAddress(f).into() { |
| fnet::IpAddress::Ipv4(addr) => fnet::SocketAddress::Ipv4(fnet::Ipv4SocketAddress { |
| address: addr, |
| port: DEFAULT_DNS_PORT, |
| }), |
| fnet::IpAddress::Ipv6(addr) => fnet::SocketAddress::Ipv6(fnet::Ipv6SocketAddress { |
| address: addr, |
| port: DEFAULT_DNS_PORT, |
| zone_index: 0, |
| }), |
| }; |
| |
| fnet_name::DnsServer_ { |
| address: Some(socket_addr), |
| source: Some(fnet_name::DnsServerSource::StaticSource( |
| fnet_name::StaticDnsServerSource::EMPTY, |
| )), |
| ..fnet_name::DnsServer_::EMPTY |
| } |
| } |
| |
| /// Connect to a service, returning an error if the service does not exist in |
| /// the service directory. |
| async fn svc_connect<S: fidl::endpoints::DiscoverableProtocolMarker>( |
| svc_dir: &fio::DirectoryProxy, |
| ) -> Result<S::Proxy, anyhow::Error> { |
| optional_svc_connect::<S>(svc_dir).await?.ok_or(anyhow::anyhow!("service does not exist")) |
| } |
| |
| /// Attempt to connect to a service, returning `None` if the service does not |
| /// exist in the service directory. |
| async fn optional_svc_connect<S: fidl::endpoints::DiscoverableProtocolMarker>( |
| svc_dir: &fio::DirectoryProxy, |
| ) -> Result<Option<S::Proxy>, anyhow::Error> { |
| let req = new_protocol_connector_in_dir::<S>(&svc_dir); |
| if !req.exists().await.context("error checking for service existence")? { |
| Ok(None) |
| } else { |
| req.connect().context("error connecting to service").map(Some) |
| } |
| } |
| |
| /// Start a DHCPv6 client if there is a unicast link-local IPv6 address in `addresses` to use as |
| /// the address. |
| fn start_dhcpv6_client( |
| fnet_interfaces_ext::Properties { id, name, addresses, .. }: &fnet_interfaces_ext::Properties, |
| dhcpv6_client_provider: &fnet_dhcpv6::ClientProviderProxy, |
| watchers: &mut DnsServerWatchers<'_>, |
| ) -> Result<Option<fnet::Ipv6SocketAddress>, errors::Error> { |
| let sockaddr = addresses.iter().find_map( |
| |&fnet_interfaces_ext::Address { |
| addr: fnet::Subnet { addr, prefix_len: _ }, |
| valid_until: _, |
| }| match addr { |
| fnet::IpAddress::Ipv6(address) => { |
| if address.is_unicast_link_local() { |
| Some(fnet::Ipv6SocketAddress { |
| address, |
| port: fnet_dhcpv6::DEFAULT_CLIENT_PORT, |
| zone_index: *id, |
| }) |
| } else { |
| None |
| } |
| } |
| fnet::IpAddress::Ipv4(_) => None, |
| }, |
| ); |
| |
| if let Some(sockaddr) = sockaddr { |
| let () = dhcpv6::start_client(dhcpv6_client_provider, *id, sockaddr, watchers) |
| .with_context(|| { |
| format!( |
| "failed to start DHCPv6 client on interface {} (id={}) w/ sockaddr {}", |
| name, |
| id, |
| sockaddr.display_ext() |
| ) |
| })?; |
| info!( |
| "started DHCPv6 client on host interface {} (id={}) w/ sockaddr {}", |
| name, |
| id, |
| sockaddr.display_ext(), |
| ); |
| } |
| Ok(sockaddr) |
| } |
| |
| impl<'a> NetCfg<'a> { |
| async fn new( |
| allow_virtual_devices: bool, |
| filter_enabled_interface_types: HashSet<InterfaceType>, |
| interface_metrics: InterfaceMetrics, |
| enable_dhcpv6: bool, |
| forwarded_device_classes: ForwardedDeviceClasses, |
| ) -> Result<NetCfg<'a>, anyhow::Error> { |
| let svc_dir = clone_namespace_svc().context("error cloning svc directory handle")?; |
| let stack = svc_connect::<fnet_stack::StackMarker>(&svc_dir) |
| .await |
| .context("could not connect to stack")?; |
| let netstack = svc_connect::<fnetstack::NetstackMarker>(&svc_dir) |
| .await |
| .context("could not connect to netstack")?; |
| let lookup_admin = svc_connect::<fnet_name::LookupAdminMarker>(&svc_dir) |
| .await |
| .context("could not connect to lookup admin")?; |
| let filter = svc_connect::<fnet_filter::FilterMarker>(&svc_dir) |
| .await |
| .context("could not connect to filter")?; |
| let interface_state = svc_connect::<fnet_interfaces::StateMarker>(&svc_dir) |
| .await |
| .context("could not connect to interfaces state")?; |
| let dhcp_server = optional_svc_connect::<fnet_dhcp::Server_Marker>(&svc_dir) |
| .await |
| .context("could not connect to DHCP Server")?; |
| let dhcpv6_client_provider = if enable_dhcpv6 { |
| let dhcpv6_client_provider = svc_connect::<fnet_dhcpv6::ClientProviderMarker>(&svc_dir) |
| .await |
| .context("could not connect to DHCPv6 client provider")?; |
| Some(dhcpv6_client_provider) |
| } else { |
| None |
| }; |
| let installer = svc_connect::<fnet_interfaces_admin::InstallerMarker>(&svc_dir) |
| .await |
| .context("could not connect to installer")?; |
| let debug = svc_connect::<fnet_debug::InterfacesMarker>(&svc_dir) |
| .await |
| .context("could not connect to debug")?; |
| let persisted_interface_config = |
| interface::FileBackedConfig::load(&PERSISTED_INTERFACE_CONFIG_FILEPATH) |
| .context("error loading persistent interface configurations")?; |
| |
| Ok(NetCfg { |
| stack, |
| netstack, |
| lookup_admin, |
| filter, |
| interface_state, |
| dhcp_server, |
| installer, |
| debug, |
| dhcpv6_client_provider, |
| allow_virtual_devices, |
| persisted_interface_config, |
| filter_enabled_interface_types, |
| interface_properties: Default::default(), |
| interface_states: Default::default(), |
| interface_metrics, |
| dns_servers: Default::default(), |
| forwarded_device_classes, |
| }) |
| } |
| |
| /// Updates the network filter configurations. |
| async fn update_filters(&mut self, config: FilterConfig) -> Result<(), anyhow::Error> { |
| let FilterConfig { rules, nat_rules, rdr_rules } = config; |
| |
| if !rules.is_empty() { |
| let mut rules = netfilter::parser::parse_str_to_rules(&rules.join("")) |
| .context("error parsing filter rules")?; |
| cas_filter_rules!(self.filter, get_rules, update_rules, rules, FilterUpdateRulesError); |
| } |
| |
| if !nat_rules.is_empty() { |
| let mut nat_rules = netfilter::parser::parse_str_to_nat_rules(&nat_rules.join("")) |
| .context("error parsing NAT rules")?; |
| cas_filter_rules!( |
| self.filter, |
| get_nat_rules, |
| update_nat_rules, |
| nat_rules, |
| FilterUpdateNatRulesError |
| ); |
| } |
| |
| if !rdr_rules.is_empty() { |
| let mut rdr_rules = netfilter::parser::parse_str_to_rdr_rules(&rdr_rules.join("")) |
| .context("error parsing RDR rules")?; |
| // TODO(https://fxbug.dev/68279): Change this to cas_filter_rules once update is supported. |
| no_update_filter_rules!( |
| self.filter, |
| get_rdr_rules, |
| update_rdr_rules, |
| rdr_rules, |
| FilterUpdateRdrRulesError |
| ); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Updates the DNS servers used by the DNS resolver. |
| async fn update_dns_servers( |
| &mut self, |
| source: DnsServersUpdateSource, |
| servers: Vec<fnet_name::DnsServer_>, |
| ) -> Result<(), errors::Error> { |
| dns::update_servers(&self.lookup_admin, &mut self.dns_servers, source, servers).await |
| } |
| |
| /// Handles the completion of the DNS server watcher associated with `source`. |
| /// |
| /// Clears the servers for `source` and removes the watcher from `dns_watchers`. |
| async fn handle_dns_server_watcher_done( |
| &mut self, |
| source: DnsServersUpdateSource, |
| dns_watchers: &mut DnsServerWatchers<'_>, |
| ) -> Result<(), anyhow::Error> { |
| match source { |
| DnsServersUpdateSource::Default => { |
| return Err(anyhow::anyhow!( |
| "should not have a DNS server watcher for the default source" |
| )); |
| } |
| DnsServersUpdateSource::Netstack => {} |
| DnsServersUpdateSource::Dhcpv6 { interface_id } => { |
| let InterfaceState { control: _, config } = self |
| .interface_states |
| .get_mut(&interface_id) |
| .ok_or(anyhow::anyhow!("no interface state found for id={}", interface_id))?; |
| |
| match config { |
| InterfaceConfigState::Host(HostInterfaceState { dhcpv6_client_addr }) => { |
| let _: fnet::Ipv6SocketAddress = |
| dhcpv6_client_addr.take().ok_or(anyhow::anyhow!( |
| "DHCPv6 was not being performed on host interface with id={}", |
| interface_id |
| ))?; |
| } |
| InterfaceConfigState::WlanAp(WlanApInterfaceState {}) => { |
| return Err(anyhow::anyhow!( |
| "should not have a DNS watcher for a WLAN AP interface with id={}", |
| interface_id |
| )); |
| } |
| } |
| } |
| } |
| |
| let () = self |
| .update_dns_servers(source, vec![]) |
| .await |
| .with_context(|| { |
| format!("error clearing DNS servers for {:?} after completing DNS watcher", source) |
| }) |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| error!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })?; |
| |
| // The watcher stream may have already been removed if it was exhausted so we don't |
| // care what the return value is. At the end of this function, it is guaranteed that |
| // `dns_watchers` will not have a stream for `source` anyways. |
| let _: Option<Pin<Box<futures::stream::BoxStream<'_, _>>>> = dns_watchers.remove(&source); |
| |
| Ok(()) |
| } |
| |
| /// Run the network configuration eventloop. |
| /// |
| /// The device directory will be monitored for device events and the netstack will be |
| /// configured with a new interface on new device discovery. |
| async fn run( |
| &mut self, |
| mut virtualization_handler: impl virtualization::Handler, |
| ) -> Result<(), anyhow::Error> { |
| let ethdev_stream = self |
| .create_device_stream::<devices::EthernetDevice>() |
| .await |
| .context("create ethernet device stream")? |
| .fuse(); |
| futures::pin_mut!(ethdev_stream); |
| |
| let netdev_stream = self |
| .create_device_stream::<devices::NetworkDevice>() |
| .await |
| .context("create netdevice stream")? |
| .fuse(); |
| futures::pin_mut!(netdev_stream); |
| |
| let if_watcher_event_stream = |
| fnet_interfaces_ext::event_stream_from_state(&self.interface_state) |
| .context("error creating interface watcher event stream")? |
| .fuse(); |
| futures::pin_mut!(if_watcher_event_stream); |
| |
| let (dns_server_watcher, dns_server_watcher_req) = |
| fidl::endpoints::create_proxy::<fnet_name::DnsServerWatcherMarker>() |
| .context("error creating dns server watcher")?; |
| let () = self |
| .stack |
| .get_dns_server_watcher(dns_server_watcher_req) |
| .context("get dns server watcher")?; |
| let netstack_dns_server_stream = dns_server_watcher::new_dns_server_stream( |
| DnsServersUpdateSource::Netstack, |
| dns_server_watcher, |
| ) |
| .map(move |(s, r)| (s, r.context("error getting next Netstack DNS server update event"))) |
| .boxed(); |
| |
| let dns_watchers = DnsServerWatchers::empty(); |
| // `Fuse` (the return of `fuse`) guarantees that once the underlying stream is |
| // exhausted, future attempts to poll the stream will return `None`. This would |
| // be undesirable if we needed to support a scenario where all streams are |
| // exhausted before adding a new stream to the `StreamMap`. However, |
| // `netstack_dns_server_stream` is not expected to end so we can fuse the |
| // `StreamMap` without issue. |
| let mut dns_watchers = dns_watchers.fuse(); |
| assert!( |
| dns_watchers |
| .get_mut() |
| .insert(DnsServersUpdateSource::Netstack, netstack_dns_server_stream) |
| .is_none(), |
| "dns watchers should be empty" |
| ); |
| |
| // Serve fuchsia.net.virtualization/Control. |
| let mut fs = ServiceFs::new_local(); |
| let _: &mut ServiceFsDir<'_, _> = |
| fs.dir("svc").add_fidl_service(virtualization::Event::ControlRequestStream); |
| let _: &mut ServiceFs<_> = |
| fs.take_and_serve_directory_handle().context("take and serve directory handle")?; |
| |
| // Maintain a queue of virtualization events to be dispatched to the virtualization handler. |
| let mut virtualization_events = futures::stream::SelectAll::new(); |
| virtualization_events.push(fs.boxed_local()); |
| |
| // Lifecycle handle takes no args, must be set to zero. |
| // See zircon/processargs.h. |
| const LIFECYCLE_HANDLE_ARG: u16 = 0; |
| let lifecycle = fuchsia_runtime::take_startup_handle(fuchsia_runtime::HandleInfo::new( |
| fuchsia_runtime::HandleType::Lifecycle, |
| LIFECYCLE_HANDLE_ARG, |
| )) |
| .ok_or_else(|| anyhow::anyhow!("lifecycle handle not present"))?; |
| let lifecycle = fuchsia_async::Channel::from_channel(lifecycle.into()) |
| .context("lifecycle async channel")?; |
| let mut lifecycle = fidl_fuchsia_process_lifecycle::LifecycleRequestStream::from_channel( |
| fidl::AsyncChannel::from(lifecycle), |
| ); |
| |
| debug!("starting eventloop..."); |
| |
| loop { |
| let () = futures::select! { |
| ethdev_res = ethdev_stream.try_next() => { |
| let instance = ethdev_res |
| .context("error retrieving ethernet instance")? |
| .ok_or_else(|| anyhow::anyhow!("ethdev instance watcher stream ended unexpectedly"))?; |
| self |
| .handle_device_instance::<devices::EthernetDevice>(instance) |
| .await |
| .context("handle ethdev instance")? |
| } |
| netdev_res = netdev_stream.try_next() => { |
| let instance = netdev_res |
| .context("error retrieving netdev instance")? |
| .ok_or_else(|| anyhow::anyhow!("netdev instance watcher stream ended unexpectedly"))?; |
| self |
| .handle_device_instance::<devices::NetworkDevice>(instance) |
| .await |
| .context("handle netdev instance")? |
| } |
| if_watcher_res = if_watcher_event_stream.try_next() => { |
| let event = if_watcher_res |
| .context("error watching interface property changes")? |
| .ok_or( |
| anyhow::anyhow!("interface watcher event stream ended unexpectedly") |
| )?; |
| trace!("got interfaces watcher event = {:?}", event); |
| |
| self |
| .handle_interface_watcher_event( |
| event, dns_watchers.get_mut(), &mut virtualization_handler, |
| ) |
| .await |
| .context("handle interface watcher event") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| error!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })? |
| } |
| dns_watchers_res = dns_watchers.next() => { |
| let (source, res) = dns_watchers_res |
| .ok_or(anyhow::anyhow!("dns watchers stream should never be exhausted"))?; |
| let servers = match res { |
| Ok(s) => s, |
| Err(e) => { |
| // TODO(fxbug.dev/57484): Restart the DNS server watcher. |
| error!("non-fatal error getting next event \ |
| from DNS server watcher stream with source = {:?}: {:?}", source, e); |
| let () = self |
| .handle_dns_server_watcher_done(source, dns_watchers.get_mut()) |
| .await |
| .with_context(|| { |
| format!("error handling completion of DNS server watcher for \ |
| {:?}", source) |
| })?; |
| continue; |
| } |
| }; |
| |
| self.update_dns_servers(source, servers).await.with_context(|| { |
| format!("error handling DNS servers update from {:?}", source) |
| }).or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| error!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })? |
| } |
| event = virtualization_events.select_next_some() => { |
| virtualization_handler |
| .handle_event(event, &mut virtualization_events) |
| .await |
| .context("handle virtualization event") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| error!("non-fatal error handling virtualization: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })? |
| } |
| req = lifecycle.try_next() => { |
| let req = req |
| .context("lifecycle request")? |
| .ok_or_else( |
| || anyhow::anyhow!("LifecycleRequestStream ended unexpectedly") |
| )?; |
| match req { |
| fidl_fuchsia_process_lifecycle::LifecycleRequest::Stop { |
| control_handle |
| } => { |
| info!("received shutdown request"); |
| // Shutdown request is acknowledged by the lifecycle |
| // channel shutting down. Intentionally leak the |
| // channel so it'll only be closed on process |
| // termination, allowing clean process termination |
| // to always be observed. |
| |
| // Must drop the control_handle to unwrap the |
| // lifecycle channel. |
| std::mem::drop(control_handle); |
| let (inner, _terminated): (_, bool) = lifecycle.into_inner(); |
| let inner = std::sync::Arc::try_unwrap(inner) |
| .map_err(|_: std::sync::Arc<_>| { |
| anyhow::anyhow!("failed to retrieve lifecycle channel") |
| })?; |
| let inner: zx::Channel = inner.into_channel().into_zx_channel(); |
| std::mem::forget(inner); |
| |
| return Ok(()); |
| } |
| } |
| } |
| complete => break, |
| }; |
| } |
| |
| Err(anyhow::anyhow!("eventloop ended unexpectedly")) |
| } |
| |
| /// Handles an interface watcher event (existing, added, changed, or removed). |
| async fn handle_interface_watcher_event( |
| &mut self, |
| event: fnet_interfaces::Event, |
| watchers: &mut DnsServerWatchers<'_>, |
| virtualization_handler: &mut impl virtualization::Handler, |
| ) -> Result<(), errors::Error> { |
| let Self { |
| interface_properties, |
| dns_servers, |
| interface_states, |
| lookup_admin, |
| dhcp_server, |
| dhcpv6_client_provider, |
| .. |
| } = self; |
| let update_result = interface_properties |
| .update(event) |
| .context("failed to update interface properties with watcher event") |
| .map_err(errors::Error::Fatal)?; |
| Self::handle_interface_update_result( |
| &update_result, |
| watchers, |
| dns_servers, |
| interface_states, |
| lookup_admin, |
| dhcp_server, |
| dhcpv6_client_provider, |
| ) |
| .await |
| .context("handle interface update")?; |
| virtualization_handler |
| .handle_interface_update_result(&update_result) |
| .await |
| .context("handle interface update for virtualization")?; |
| Ok(()) |
| } |
| |
| // This method takes mutable references to several fields of `NetCfg` separately as parameters, |
| // rather than `&mut self` directly, because `update_result` already holds a reference into |
| // `self.interface_properties`. |
| async fn handle_interface_update_result( |
| update_result: &fnet_interfaces_ext::UpdateResult<'_>, |
| watchers: &mut DnsServerWatchers<'_>, |
| dns_servers: &mut DnsServers, |
| interface_states: &mut HashMap<u64, InterfaceState>, |
| lookup_admin: &fnet_name::LookupAdminProxy, |
| dhcp_server: &Option<fnet_dhcp::Server_Proxy>, |
| dhcpv6_client_provider: &Option<fnet_dhcpv6::ClientProviderProxy>, |
| ) -> Result<(), errors::Error> { |
| match update_result { |
| fnet_interfaces_ext::UpdateResult::Added(properties) => { |
| match interface_states.get_mut(&properties.id) { |
| Some(state) => state |
| .on_discovery(properties, dhcpv6_client_provider.as_ref(), watchers) |
| .await |
| .context("failed to handle interface added event"), |
| // An interface netcfg won't be configuring was added, do nothing. |
| None => Ok(()), |
| } |
| } |
| fnet_interfaces_ext::UpdateResult::Existing(properties) => { |
| match interface_states.get_mut(&properties.id) { |
| Some(state) => state |
| .on_discovery(properties, dhcpv6_client_provider.as_ref(), watchers) |
| .await |
| .context("failed to handle existing interface event"), |
| // An interface netcfg won't be configuring was discovered, do nothing. |
| None => Ok(()), |
| } |
| } |
| fnet_interfaces_ext::UpdateResult::Changed { |
| previous: fnet_interfaces::Properties { online: previous_online, .. }, |
| current: current_properties, |
| } => { |
| let &fnet_interfaces_ext::Properties { |
| id, ref name, online, ref addresses, .. |
| } = current_properties; |
| match interface_states.get_mut(&id) { |
| // An interface netcfg is not configuring was changed, do nothing. |
| None => return Ok(()), |
| Some(InterfaceState { |
| control: _, |
| config: |
| InterfaceConfigState::Host(HostInterfaceState { dhcpv6_client_addr }), |
| }) => { |
| let dhcpv6_client_provider = |
| if let Some(dhcpv6_client_provider) = dhcpv6_client_provider { |
| dhcpv6_client_provider |
| } else { |
| return Ok(()); |
| }; |
| |
| // Stop DHCPv6 client if interface went down. |
| if !online { |
| let sockaddr = match dhcpv6_client_addr.take() { |
| Some(s) => s, |
| None => return Ok(()), |
| }; |
| |
| info!( |
| "host interface {} (id={}) went down \ |
| so stopping DHCPv6 client w/ sockaddr = {}", |
| name, |
| id, |
| sockaddr.display_ext(), |
| ); |
| |
| return dhcpv6::stop_client(&lookup_admin, dns_servers, *id, watchers) |
| .await |
| .with_context(|| { |
| format!( |
| "error stopping DHCPv6 client on down interface {} (id={})", |
| name, id |
| ) |
| }); |
| } |
| |
| // Stop the DHCPv6 client if its address can no longer be found on the |
| // interface. |
| if let Some(sockaddr) = dhcpv6_client_addr { |
| let &mut fnet::Ipv6SocketAddress { address, port: _, zone_index: _ } = |
| sockaddr; |
| if !addresses.iter().any( |
| |&fnet_interfaces_ext::Address { |
| addr: fnet::Subnet { addr, prefix_len: _ }, |
| valid_until: _, |
| }| { |
| addr == fnet::IpAddress::Ipv6(address) |
| }, |
| ) { |
| let sockaddr = *sockaddr; |
| *dhcpv6_client_addr = None; |
| |
| info!( |
| "stopping DHCPv6 client on host interface {} (id={}) \ |
| w/ removed sockaddr = {}", |
| name, |
| id, |
| sockaddr.display_ext(), |
| ); |
| |
| let () = |
| dhcpv6::stop_client(&lookup_admin, dns_servers, *id, watchers) |
| .await |
| .with_context(|| { |
| format!( |
| "error stopping DHCPv6 client on interface {} (id={}) \ |
| since sockaddr {} was removed", |
| name, id, sockaddr.display_ext() |
| ) |
| })?; |
| } |
| } |
| |
| // Start a DHCPv6 client if there isn't one. |
| if dhcpv6_client_addr.is_none() { |
| *dhcpv6_client_addr = start_dhcpv6_client( |
| current_properties, |
| &dhcpv6_client_provider, |
| watchers, |
| )?; |
| } |
| Ok(()) |
| } |
| Some(InterfaceState { |
| control: _, |
| config: InterfaceConfigState::WlanAp(WlanApInterfaceState {}), |
| }) => { |
| // TODO(fxbug.dev/55879): Stop the DHCP server when the address it is |
| // listening on is removed. |
| let dhcp_server = if let Some(dhcp_server) = dhcp_server { |
| dhcp_server |
| } else { |
| return Ok(()); |
| }; |
| |
| if previous_online |
| .map_or(true, |previous_online| previous_online == *online) |
| { |
| return Ok(()); |
| } |
| |
| if *online { |
| info!( |
| "WLAN AP interface {} (id={}) came up so starting DHCP server", |
| name, id |
| ); |
| dhcpv4::start_server(&dhcp_server) |
| .await |
| .context("error starting DHCP server") |
| } else { |
| info!( |
| "WLAN AP interface {} (id={}) went down so stopping DHCP server", |
| name, id |
| ); |
| dhcpv4::stop_server(&dhcp_server) |
| .await |
| .context("error stopping DHCP server") |
| } |
| } |
| } |
| } |
| fnet_interfaces_ext::UpdateResult::Removed(fnet_interfaces_ext::Properties { |
| id, |
| name, |
| .. |
| }) => { |
| match interface_states.remove(&id) { |
| // An interface netcfg was not responsible for configuring was removed, do |
| // nothing. |
| None => Ok(()), |
| Some(InterfaceState { control: _, config }) => { |
| match config { |
| InterfaceConfigState::Host(HostInterfaceState { |
| mut dhcpv6_client_addr, |
| }) => { |
| let sockaddr = match dhcpv6_client_addr.take() { |
| Some(s) => s, |
| None => return Ok(()), |
| }; |
| |
| info!( |
| "host interface {} (id={}) removed \ |
| so stopping DHCPv6 client w/ sockaddr = {}", |
| name, |
| id, |
| sockaddr.display_ext() |
| ); |
| |
| dhcpv6::stop_client( |
| &lookup_admin, |
| dns_servers, |
| *id, |
| watchers, |
| ) |
| .await |
| .with_context(|| { |
| format!( |
| "error stopping DHCPv6 client on removed interface {} (id={})", |
| name, id |
| ) |
| }) |
| } |
| InterfaceConfigState::WlanAp(WlanApInterfaceState {}) => { |
| if let Some(dhcp_server) = dhcp_server { |
| // The DHCP server should only run on the WLAN AP interface, so stop it |
| // since the AP interface is removed. |
| info!( |
| "WLAN AP interface {} (id={}) is removed, stopping DHCP server", |
| name, id |
| ); |
| dhcpv4::stop_server(&dhcp_server) |
| .await |
| .context("error stopping DHCP server") |
| } else { |
| Ok(()) |
| } |
| } |
| } |
| .context("failed to handle interface removed event") |
| } |
| } |
| } |
| fnet_interfaces_ext::UpdateResult::NoChange => Ok(()), |
| } |
| } |
| |
| /// Handle an event from `D`'s device directory. |
| async fn create_device_stream<D: devices::Device>( |
| &self, |
| ) -> Result<impl futures::Stream<Item = Result<D::DeviceInstance, anyhow::Error>>, anyhow::Error> |
| { |
| let installer = self.installer.clone(); |
| let stream_of_streams = fvfs_watcher::Watcher::new( |
| open_directory_in_namespace(D::PATH, OpenFlags::RIGHT_READABLE) |
| .with_context(|| format!("error opening {} directory", D::NAME))?, |
| ) |
| .await |
| .with_context(|| format!("creating watcher for {}", D::PATH))? |
| .err_into() |
| .try_filter_map(move |fvfs_watcher::WatchMessage { event, filename }| { |
| let installer = installer.clone(); |
| async move { |
| trace!("got {:?} {} event for {}", event, D::NAME, filename.display()); |
| |
| if filename == path::PathBuf::from(THIS_DIRECTORY) { |
| debug!("skipping {} w/ filename = {}", D::NAME, filename.display()); |
| return Ok(None); |
| } |
| |
| match event { |
| fvfs_watcher::WatchEvent::ADD_FILE | fvfs_watcher::WatchEvent::EXISTING => { |
| let filepath = path::Path::new(D::PATH).join(filename); |
| info!("found new {} at {:?}", D::NAME, filepath); |
| match D::get_instance_stream(&installer, &filepath) |
| .await |
| .context("create instance stream") |
| { |
| Ok(stream) => Ok(Some(stream.filter_map(move |r| { |
| futures::future::ready(match r { |
| Ok(instance) => Some(Ok(instance)), |
| Err(errors::Error::NonFatal(nonfatal)) => { |
| error!( |
| "non-fatal error operating device stream {} for {:?}: {:?}", |
| D::NAME, |
| filepath, |
| nonfatal |
| ); |
| None |
| } |
| Err(errors::Error::Fatal(fatal)) => Some(Err(fatal)), |
| }) |
| }))), |
| Err(errors::Error::NonFatal(nonfatal)) => { |
| error!( |
| "non-fatal error fetching device stream {} for {:?}: {:?}", |
| D::NAME, |
| filepath, |
| nonfatal |
| ); |
| Ok(None) |
| } |
| Err(errors::Error::Fatal(fatal)) => Err(fatal), |
| } |
| } |
| fvfs_watcher::WatchEvent::IDLE | fvfs_watcher::WatchEvent::REMOVE_FILE => { |
| Ok(None) |
| } |
| event => Err(anyhow::anyhow!( |
| "unrecognized event {:?} for {} filename {}", |
| event, |
| D::NAME, |
| filename.display() |
| )), |
| } |
| } |
| }) |
| .fuse() |
| .try_flatten_unordered(); |
| Ok(stream_of_streams) |
| } |
| |
| async fn handle_device_instance<D: devices::Device>( |
| &mut self, |
| instance: D::DeviceInstance, |
| ) -> Result<(), anyhow::Error> { |
| let mut i = 0; |
| loop { |
| // TODO(fxbug.dev/56559): The same interface may flap so instead of using a |
| // temporary name, try to determine if the interface flapped and wait |
| // for the teardown to complete. |
| match self.add_new_device::<D>(&instance, i == 0 /* stable_name */).await { |
| Ok(()) => { |
| break Ok(()); |
| } |
| Err(devices::AddDeviceError::AlreadyExists) => { |
| i += 1; |
| if i == MAX_ADD_DEVICE_ATTEMPTS { |
| error!( |
| "failed to add {} {:?} after {} attempts due to already exists error", |
| D::NAME, |
| instance, |
| MAX_ADD_DEVICE_ATTEMPTS, |
| ); |
| |
| break Ok(()); |
| } |
| |
| warn!( |
| "got already exists error on attempt {} of adding {} {:?}, trying again...", |
| i, |
| D::NAME, |
| instance, |
| ); |
| } |
| Err(devices::AddDeviceError::Other(errors::Error::NonFatal(e))) => { |
| error!("non-fatal error adding {} {:?}: {:?}", D::NAME, instance, e); |
| |
| break Ok(()); |
| } |
| Err(devices::AddDeviceError::Other(errors::Error::Fatal(e))) => { |
| break Err(e.context(format!("error adding new {} {:?}", D::NAME, instance))); |
| } |
| } |
| } |
| } |
| |
| /// Add a device at `filepath` to the netstack. |
| async fn add_new_device<D: devices::Device>( |
| &mut self, |
| device_instance: &D::DeviceInstance, |
| stable_name: bool, |
| ) -> Result<(), devices::AddDeviceError> { |
| let info = D::get_device_info(&device_instance) |
| .await |
| .context("error getting device info and MAC")?; |
| |
| let DeviceInfo { mac, is_synthetic, device_class: _, topological_path } = &info; |
| |
| if !self.allow_virtual_devices && *is_synthetic { |
| warn!("{} {:?} is not a physical device, skipping", D::NAME, device_instance); |
| return Ok(()); |
| } |
| |
| let mac = mac.ok_or_else(|| { |
| warn!("devices without mac address not supported yet"); |
| devices::AddDeviceError::Other(errors::Error::NonFatal(anyhow::anyhow!( |
| "device without mac not supported" |
| ))) |
| })?; |
| |
| let interface_type = info.interface_type(); |
| let metric = match interface_type { |
| InterfaceType::Wlan => self.interface_metrics.wlan_metric, |
| InterfaceType::Ethernet => self.interface_metrics.eth_metric, |
| } |
| .into(); |
| let interface_name = if stable_name { |
| match self.persisted_interface_config.generate_stable_name( |
| &topological_path, /* TODO(tamird): we can probably do |
| * better with std::borrow::Cow. */ |
| mac, |
| interface_type, |
| ) { |
| Ok(name) => name.to_string(), |
| Err(interface::NameGenerationError::FileUpdateError { name, err }) => { |
| warn!("failed to update interface (topo path = {}, mac = {}, interface_type = {:?}) with new name = {}: {}", topological_path, mac, interface_type, name, err); |
| name.to_string() |
| } |
| Err(interface::NameGenerationError::GenerationError(e)) => { |
| return Err(devices::AddDeviceError::Other(errors::Error::Fatal( |
| e.context("error getting stable name"), |
| ))) |
| } |
| } |
| } else { |
| self.persisted_interface_config.generate_temporary_name(interface_type) |
| }; |
| |
| info!("adding {} {:?} to stack with name = {}", D::NAME, device_instance, interface_name); |
| |
| let (interface_id, control) = D::add_to_stack( |
| self, |
| InterfaceConfig { name: interface_name.clone(), metric }, |
| device_instance, |
| ) |
| .await |
| .context("error adding to stack")?; |
| |
| self.configure_eth_interface(interface_id, control, interface_name, &info) |
| .await |
| .context("error configuring ethernet interface") |
| .map_err(devices::AddDeviceError::Other) |
| } |
| |
| /// Configure an ethernet interface. |
| /// |
| /// If the device is a WLAN AP, it will be configured as a WLAN AP (see |
| /// `configure_wlan_ap_and_dhcp_server` for more details). Otherwise, it |
| /// will be configured as a host (see `configure_host` for more details). |
| async fn configure_eth_interface( |
| &mut self, |
| interface_id: u64, |
| control: fidl_fuchsia_net_interfaces_ext::admin::Control, |
| interface_name: String, |
| info: &DeviceInfo, |
| ) -> Result<(), errors::Error> { |
| let class: DeviceClass = info.device_class.into(); |
| let ForwardedDeviceClasses { ipv4, ipv6 } = &self.forwarded_device_classes; |
| let config: fnet_interfaces_admin::Configuration = control |
| .set_configuration(fnet_interfaces_admin::Configuration { |
| ipv6: Some(fnet_interfaces_admin::Ipv6Configuration { |
| forwarding: Some(ipv6.contains(&class)), |
| ..fnet_interfaces_admin::Ipv6Configuration::EMPTY |
| }), |
| ipv4: Some(fnet_interfaces_admin::Ipv4Configuration { |
| forwarding: Some(ipv4.contains(&class)), |
| ..fnet_interfaces_admin::Ipv4Configuration::EMPTY |
| }), |
| ..fnet_interfaces_admin::Configuration::EMPTY |
| }) |
| .await |
| .context("setting configuration") |
| .and_then(|res| { |
| res.map_err(|e: fnet_interfaces_admin::ControlSetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .map_err(errors::Error::Fatal)?; |
| info!("installed configuration with result {:?}", config); |
| |
| if info.is_wlan_ap() { |
| if let Some(id) = self.interface_states.iter().find_map(|(id, state)| { |
| if state.is_wlan_ap() { |
| Some(id) |
| } else { |
| None |
| } |
| }) { |
| return Err(errors::Error::NonFatal(anyhow::anyhow!("multiple WLAN AP interfaces are not supported, have WLAN AP interface with id = {}", id))); |
| } |
| let InterfaceState { control, config: _ } = match self |
| .interface_states |
| .entry(interface_id) |
| { |
| Entry::Occupied(entry) => { |
| return Err(errors::Error::Fatal(anyhow::anyhow!("multiple interfaces with the same ID = {}; attempting to add state for a WLAN AP, existing state = {:?}", entry.key(), entry.get()))); |
| } |
| Entry::Vacant(entry) => entry.insert(InterfaceState::new_wlan_ap(control)), |
| }; |
| |
| info!("discovered WLAN AP (interface ID={})", interface_id); |
| |
| if let Some(dhcp_server) = &self.dhcp_server { |
| info!("configuring DHCP server for WLAN AP (interface ID={})", interface_id); |
| let () = Self::configure_wlan_ap_and_dhcp_server( |
| interface_id, |
| dhcp_server, |
| control, |
| &self.stack, |
| interface_name, |
| ) |
| .await |
| .context("error configuring wlan ap and dhcp server")?; |
| } else { |
| warn!("cannot configure DHCP server for WLAN AP (interface ID={}) since DHCP server service is not available", interface_id); |
| } |
| } else { |
| let InterfaceState { control, config: _ } = match self |
| .interface_states |
| .entry(interface_id) |
| { |
| Entry::Occupied(entry) => { |
| return Err(errors::Error::Fatal(anyhow::anyhow!("multiple interfaces with the same ID = {}; attempting to add state for a host, existing state = {:?}", entry.key(), entry.get()))); |
| } |
| Entry::Vacant(entry) => entry.insert(InterfaceState::new_host(control)), |
| }; |
| |
| info!("discovered host interface with id={}, configuring interface", interface_id); |
| |
| let () = Self::configure_host( |
| &self.filter_enabled_interface_types, |
| &self.filter, |
| &self.netstack, |
| interface_id, |
| info, |
| ) |
| .await |
| .context("error configuring host")?; |
| |
| let _did_enable: bool = control |
| .enable() |
| .await |
| .context("error sending enable request") |
| .and_then(|res| { |
| // ControlEnableError is an empty *flexible* enum, so we can't match on it, but |
| // the operation is infallible at the time of writing. |
| res.map_err(|e: fidl_fuchsia_net_interfaces_admin::ControlEnableError| { |
| anyhow::anyhow!("enable interface: {:?}", e) |
| }) |
| }) |
| .map_err(errors::Error::Fatal)?; |
| } |
| |
| Ok(()) |
| } |
| |
| /// Configure host interface. |
| async fn configure_host( |
| filter_enabled_interface_types: &HashSet<InterfaceType>, |
| filter: &fnet_filter::FilterProxy, |
| netstack: &fnetstack::NetstackProxy, |
| interface_id: u64, |
| info: &DeviceInfo, |
| ) -> Result<(), errors::Error> { |
| if should_enable_filter(filter_enabled_interface_types, info) { |
| info!("enable filter for nic {}", interface_id); |
| let () = filter |
| .enable_interface(interface_id) |
| .await |
| .with_context(|| { |
| format!("error sending enable filter request on nic {}", interface_id) |
| }) |
| .map_err(errors::Error::Fatal)? |
| .map_err(|e| { |
| anyhow::anyhow!( |
| "failed to enable filter on nic {} with error = {:?}", |
| interface_id, |
| e |
| ) |
| }) |
| .map_err(errors::Error::NonFatal)?; |
| } else { |
| info!("disable filter for nic {}", interface_id); |
| let () = filter |
| .disable_interface(interface_id) |
| .await |
| .with_context(|| { |
| format!("error sending disable filter request on nic {}", interface_id) |
| }) |
| .map_err(errors::Error::Fatal)? |
| .map_err(|e| { |
| anyhow::anyhow!( |
| "failed to disable filter on nic {} with error = {:?}", |
| interface_id, |
| e |
| ) |
| }) |
| .map_err(errors::Error::NonFatal)?; |
| }; |
| |
| let interface_id: u32 = interface_id.try_into().expect("NIC ID should fit in a u32"); |
| |
| // Enable DHCP. |
| let (dhcp_client, server_end) = fidl::endpoints::create_proxy::<fnet_dhcp::ClientMarker>() |
| .context("dhcp client: failed to create fidl endpoints") |
| .map_err(errors::Error::Fatal)?; |
| let () = netstack |
| .get_dhcp_client(interface_id, server_end) |
| .await |
| .context("failed to call netstack.get_dhcp_client") |
| .map_err(errors::Error::Fatal)? |
| .map_err(zx::Status::from_raw) |
| .context("failed to get dhcp client") |
| .map_err(errors::Error::NonFatal)?; |
| let () = dhcp_client |
| .start() |
| .await |
| .context("error sending start DHCP client request") |
| .map_err(errors::Error::Fatal)? |
| .map_err(zx::Status::from_raw) |
| .context("failed to start dhcp client") |
| .map_err(errors::Error::NonFatal)?; |
| |
| Ok(()) |
| } |
| |
| /// Configure the WLAN AP and the DHCP server to serve requests on its network. |
| /// |
| /// Note, this method will not start the DHCP server, it will only be configured |
| /// with the parameters so it is ready to be started when an interface UP event |
| /// is received for the WLAN AP. |
| async fn configure_wlan_ap_and_dhcp_server( |
| interface_id: u64, |
| dhcp_server: &fnet_dhcp::Server_Proxy, |
| control: &fidl_fuchsia_net_interfaces_ext::admin::Control, |
| stack: &fidl_fuchsia_net_stack::StackProxy, |
| name: String, |
| ) -> Result<(), errors::Error> { |
| let (address_state_provider, server_end) = fidl::endpoints::create_proxy::< |
| fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker, |
| >() |
| .context("address state provider: failed to create fidl endpoints") |
| .map_err(errors::Error::Fatal)?; |
| |
| // Calculate and set the interface address based on the network address. |
| // The interface address should be the first available address. |
| let network_addr_as_u32 = u32::from_be_bytes(WLAN_AP_NETWORK_ADDR.addr); |
| let interface_addr_as_u32 = network_addr_as_u32 + 1; |
| let ipv4 = fnet::Ipv4Address { addr: interface_addr_as_u32.to_be_bytes() }; |
| let addr = fidl_fuchsia_net::Subnet { |
| addr: fnet::IpAddress::Ipv4(ipv4.clone()), |
| prefix_len: WLAN_AP_PREFIX_LEN, |
| }; |
| |
| let () = control |
| .add_address( |
| &mut addr.clone(), |
| fidl_fuchsia_net_interfaces_admin::AddressParameters::EMPTY, |
| server_end, |
| ) |
| .map_err(|e| { |
| let severity = match e { |
| fidl_fuchsia_net_interfaces_ext::admin::TerminalError::Fidl(_) => { |
| errors::Error::Fatal |
| }, |
| fidl_fuchsia_net_interfaces_ext::admin::TerminalError::Terminal(e) => match e { |
| fidl_fuchsia_net_interfaces_admin::InterfaceRemovedReason::DuplicateName |
| | fidl_fuchsia_net_interfaces_admin::InterfaceRemovedReason::PortAlreadyBound |
| | fidl_fuchsia_net_interfaces_admin::InterfaceRemovedReason::BadPort => { |
| errors::Error::Fatal |
| } |
| fidl_fuchsia_net_interfaces_admin::InterfaceRemovedReason::PortClosed |
| | fidl_fuchsia_net_interfaces_admin::InterfaceRemovedReason::User | _ => { |
| errors::Error::NonFatal |
| } |
| }, |
| }; |
| severity(anyhow::Error::new(e).context("error sending add address request")) |
| })?; |
| |
| // Allow the address to outlive this scope. At the time of writing its lifetime is |
| // identical to the interface's lifetime and no updates to its properties are made. We may |
| // wish to retain the handle in the future to allow external address removal (e.g. by a |
| // user) to be observed so that an error can be emitted (as such removal would break a |
| // critical user journey). |
| let () = address_state_provider |
| .detach() |
| .context("error sending detach request") |
| .map_err(errors::Error::Fatal)?; |
| |
| // Enable the interface to allow DAD to proceed. |
| let _did_enable: bool = control |
| .enable() |
| .await |
| .context("error sending enable request") |
| .and_then(|res| { |
| // ControlEnableError is an empty *flexible* enum, so we can't match on it, but the |
| // operation is infallible at the time of writing. |
| res.map_err(|e: fidl_fuchsia_net_interfaces_admin::ControlEnableError| { |
| anyhow::anyhow!("enable interface: {:?}", e) |
| }) |
| }) |
| .map_err(errors::Error::Fatal)?; |
| |
| let state_stream = |
| fidl_fuchsia_net_interfaces_ext::admin::assignment_state_stream(address_state_provider); |
| futures::pin_mut!(state_stream); |
| let () = fidl_fuchsia_net_interfaces_ext::admin::wait_assignment_state( |
| &mut state_stream, |
| fidl_fuchsia_net_interfaces_admin::AddressAssignmentState::Assigned, |
| ) |
| .await |
| .map_err(|e| { |
| let severity = match e { |
| fidl_fuchsia_net_interfaces_ext::admin::AddressStateProviderError::AddressRemoved( |
| reason, |
| ) => { |
| match reason { |
| fnet_interfaces_admin::AddressRemovalReason::Invalid => errors::Error::Fatal, |
| fnet_interfaces_admin::AddressRemovalReason::AlreadyAssigned |
| | fnet_interfaces_admin::AddressRemovalReason::DadFailed |
| | fnet_interfaces_admin::AddressRemovalReason::InterfaceRemoved |
| | fnet_interfaces_admin::AddressRemovalReason::UserRemoved => { |
| errors::Error::NonFatal |
| } |
| } |
| } |
| fidl_fuchsia_net_interfaces_ext::admin::AddressStateProviderError::Fidl(_) |
| | fidl_fuchsia_net_interfaces_ext::admin::AddressStateProviderError::ChannelClosed => { |
| // TODO(https://fxbug.dev/89290): Reconsider whether this should be a fatal |
| // error, as it can be caused by a netstack bug. |
| errors::Error::Fatal |
| } |
| }; |
| severity(anyhow::Error::new(e).context("failed to add interface address for WLAN AP")) |
| })?; |
| |
| let subnet = fidl_fuchsia_net_ext::apply_subnet_mask(addr); |
| let () = stack |
| .add_forwarding_entry(&mut fidl_fuchsia_net_stack::ForwardingEntry { |
| subnet, |
| device_id: interface_id, |
| next_hop: None, |
| metric: 0, |
| }) |
| .await |
| .context("error sending add route request") |
| .map_err(errors::Error::Fatal)? |
| .map_err(|e| { |
| let severity = match e { |
| fidl_fuchsia_net_stack::Error::InvalidArgs => { |
| // Do not consider this a fatal error because the interface could have been |
| // removed after it was added, but before we reached this point. |
| // |
| // NB: this error is returned by Netstack2 when the interface doesn't |
| // exist. 🤷 |
| errors::Error::NonFatal |
| } |
| fidl_fuchsia_net_stack::Error::Internal |
| | fidl_fuchsia_net_stack::Error::NotSupported |
| | fidl_fuchsia_net_stack::Error::BadState |
| | fidl_fuchsia_net_stack::Error::TimeOut |
| | fidl_fuchsia_net_stack::Error::NotFound |
| | fidl_fuchsia_net_stack::Error::AlreadyExists |
| | fidl_fuchsia_net_stack::Error::Io => errors::Error::Fatal, |
| }; |
| severity(anyhow::anyhow!("adding route: {:?}", e)) |
| })?; |
| |
| // First we clear any leases that the server knows about since the server |
| // will be used on a new interface. If leases exist, configuring the DHCP |
| // server parameters may fail (AddressPool). |
| debug!("clearing DHCP leases"); |
| let () = dhcp_server |
| .clear_leases() |
| .await |
| .context("error sending clear DHCP leases request") |
| .map_err(errors::Error::NonFatal)? |
| .map_err(zx::Status::from_raw) |
| .context("error clearing DHCP leases request") |
| .map_err(errors::Error::NonFatal)?; |
| |
| // Configure the DHCP server. |
| let v = vec![ipv4]; |
| debug!("setting DHCP IpAddrs parameter to {:?}", v); |
| let () = dhcp_server |
| .set_parameter(&mut fnet_dhcp::Parameter::IpAddrs(v)) |
| .await |
| .context("error sending set DHCP IpAddrs parameter request") |
| .map_err(errors::Error::NonFatal)? |
| .map_err(zx::Status::from_raw) |
| .context("error setting DHCP IpAddrs parameter") |
| .map_err(errors::Error::NonFatal)?; |
| |
| let v = vec![name]; |
| debug!("setting DHCP BoundDeviceNames parameter to {:?}", v); |
| let () = dhcp_server |
| .set_parameter(&mut fnet_dhcp::Parameter::BoundDeviceNames(v)) |
| .await |
| .context("error sending set DHCP BoundDeviceName parameter request") |
| .map_err(errors::Error::NonFatal)? |
| .map_err(zx::Status::from_raw) |
| .context("error setting DHCP BoundDeviceNames parameter") |
| .map_err(errors::Error::NonFatal)?; |
| |
| let v = fnet_dhcp::LeaseLength { |
| default: Some(WLAN_AP_DHCP_LEASE_TIME_SECONDS), |
| max: Some(WLAN_AP_DHCP_LEASE_TIME_SECONDS), |
| ..fnet_dhcp::LeaseLength::EMPTY |
| }; |
| debug!("setting DHCP LeaseLength parameter to {:?}", v); |
| let () = dhcp_server |
| .set_parameter(&mut fnet_dhcp::Parameter::Lease(v)) |
| .await |
| .context("error sending set DHCP LeaseLength parameter request") |
| .map_err(errors::Error::NonFatal)? |
| .map_err(zx::Status::from_raw) |
| .context("error setting DHCP LeaseLength parameter") |
| .map_err(errors::Error::NonFatal)?; |
| |
| let host_mask = dhcp::configuration::SubnetMask::new(WLAN_AP_PREFIX_LEN) |
| .ok_or_else(|| anyhow!("error creating host mask from prefix length")) |
| .map_err(errors::Error::NonFatal)?; |
| let broadcast_addr = |
| host_mask.broadcast_of(&std::net::Ipv4Addr::from_fidl(WLAN_AP_NETWORK_ADDR)); |
| let broadcast_addr_as_u32: u32 = broadcast_addr.into(); |
| // The start address of the DHCP pool should be the first address after the WLAN AP |
| // interface address. |
| let dhcp_pool_start = |
| fnet::Ipv4Address { addr: (interface_addr_as_u32 + 1).to_be_bytes() }.into(); |
| // The last address of the DHCP pool should be the last available address |
| // in the interface's network. This is the address immediately before the |
| // network's broadcast address. |
| let dhcp_pool_end = fnet::Ipv4Address { addr: (broadcast_addr_as_u32 - 1).to_be_bytes() }; |
| |
| let v = fnet_dhcp::AddressPool { |
| prefix_length: Some(WLAN_AP_PREFIX_LEN), |
| range_start: Some(dhcp_pool_start), |
| range_stop: Some(dhcp_pool_end), |
| ..fnet_dhcp::AddressPool::EMPTY |
| }; |
| debug!("setting DHCP AddressPool parameter to {:?}", v); |
| dhcp_server |
| .set_parameter(&mut fnet_dhcp::Parameter::AddressPool(v)) |
| .await |
| .context("error sending set DHCP AddressPool parameter request") |
| .map_err(errors::Error::NonFatal)? |
| .map_err(zx::Status::from_raw) |
| .context("error setting DHCP AddressPool parameter") |
| .map_err(errors::Error::NonFatal) |
| } |
| } |
| |
| pub async fn run<M: Mode>() -> Result<(), anyhow::Error> { |
| let opt: Opt = argh::from_env(); |
| let Opt { allow_virtual_devices, min_severity: LogLevel(min_severity), config_data } = &opt; |
| |
| // Use the diagnostics_log library directly rather than e.g. the #[fuchsia::main] macro on |
| // the main function, so that we can specify the logging severity level at runtime based on a |
| // command line argument. |
| diagnostics_log::init!( |
| &[], |
| diagnostics_log::Interest { |
| min_severity: Some(*min_severity), |
| ..diagnostics_log::Interest::EMPTY |
| } |
| ); |
| info!("starting"); |
| debug!("starting with options = {:?}", opt); |
| |
| let Config { |
| dns_config: DnsConfig { servers }, |
| filter_config, |
| filter_enabled_interface_types, |
| interface_metrics, |
| allowed_upstream_device_classes: AllowedDeviceClasses(allowed_upstream_device_classes), |
| enable_dhcpv6, |
| forwarded_device_classes, |
| } = Config::load(config_data)?; |
| |
| let mut netcfg = NetCfg::new( |
| *allow_virtual_devices, |
| filter_enabled_interface_types, |
| interface_metrics, |
| enable_dhcpv6, |
| forwarded_device_classes, |
| ) |
| .await |
| .context("error creating new netcfg instance")?; |
| |
| let () = |
| netcfg.update_filters(filter_config).await.context("update filters based on config")?; |
| |
| let servers = servers.into_iter().map(static_source_from_ip).collect(); |
| debug!("updating default servers to {:?}", servers); |
| let () = netcfg |
| .update_dns_servers(DnsServersUpdateSource::Default, servers) |
| .await |
| .context("error updating default DNS servers") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| error!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })?; |
| |
| M::run(netcfg, allowed_upstream_device_classes) |
| .map_err(|e| { |
| let err_str = format!("fatal error running main: {:?}", e); |
| error!("{}", err_str); |
| anyhow!(err_str) |
| }) |
| .await |
| } |
| |
| /// Allows callers of `netcfg::run` to configure at compile time which features |
| /// should be enabled. |
| /// |
| /// This trait may be expanded to support combinations of features that can be |
| /// assembled together for specific netcfg builds. |
| #[async_trait(?Send)] |
| pub trait Mode { |
| async fn run( |
| netcfg: NetCfg<'_>, |
| allowed_upstream_device_classes: HashSet<DeviceClass>, |
| ) -> Result<(), anyhow::Error>; |
| } |
| |
| /// In this configuration, netcfg acts as the policy manager for netstack, |
| /// watching for device events and configuring the netstack with new interfaces |
| /// as needed on new device discovery. It does not implement any FIDL protocols. |
| pub enum BasicMode {} |
| |
| #[async_trait(?Send)] |
| impl Mode for BasicMode { |
| async fn run( |
| mut netcfg: NetCfg<'_>, |
| _allowed_upstream_device_classes: HashSet<DeviceClass>, |
| ) -> Result<(), anyhow::Error> { |
| netcfg.run(virtualization::Stub).await.context("event loop") |
| } |
| } |
| |
| /// In this configuration, netcfg implements the base functionality included in |
| /// `BasicMode`, and also serves the `fuchsia.net.virtualization/Control` |
| /// protocol, allowing clients to create virtual networks. |
| pub enum VirtualizationEnabled {} |
| |
| #[async_trait(?Send)] |
| impl Mode for VirtualizationEnabled { |
| async fn run( |
| mut netcfg: NetCfg<'_>, |
| allowed_upstream_device_classes: HashSet<DeviceClass>, |
| ) -> Result<(), anyhow::Error> { |
| let handler = virtualization::Virtualization::new( |
| allowed_upstream_device_classes, |
| virtualization::BridgeHandlerImpl::new( |
| netcfg.stack.clone(), |
| netcfg.netstack.clone(), |
| netcfg.debug.clone(), |
| ), |
| netcfg.installer.clone(), |
| ); |
| netcfg.run(handler).await.context("event loop") |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use futures::future::{self, FutureExt as _, TryFutureExt as _}; |
| use futures::stream::TryStreamExt as _; |
| use net_declare::{fidl_ip, fidl_ip_v6}; |
| use test_case::test_case; |
| |
| use super::*; |
| |
| impl Config { |
| pub fn load_str(s: &str) -> Result<Self, anyhow::Error> { |
| let config = serde_json::from_str(s) |
| .with_context(|| format!("could not deserialize the config data {}", s))?; |
| Ok(config) |
| } |
| } |
| |
| struct ServerEnds { |
| lookup_admin: fnet_name::LookupAdminRequestStream, |
| dhcpv6_client_provider: fnet_dhcpv6::ClientProviderRequestStream, |
| } |
| |
| impl Into<anyhow::Error> for errors::Error { |
| fn into(self) -> anyhow::Error { |
| match self { |
| errors::Error::NonFatal(e) => e, |
| errors::Error::Fatal(e) => e, |
| } |
| } |
| } |
| |
| fn test_netcfg<'a>() -> Result<(NetCfg<'a>, ServerEnds), anyhow::Error> { |
| let (stack, _stack_server) = fidl::endpoints::create_proxy::<fnet_stack::StackMarker>() |
| .context("error creating stack endpoints")?; |
| let (netstack, _netstack_server) = |
| fidl::endpoints::create_proxy::<fnetstack::NetstackMarker>() |
| .context("error creating netstack endpoints")?; |
| let (lookup_admin, lookup_admin_server) = |
| fidl::endpoints::create_proxy::<fnet_name::LookupAdminMarker>() |
| .context("error creating lookup_admin endpoints")?; |
| let (filter, _filter_server) = fidl::endpoints::create_proxy::<fnet_filter::FilterMarker>() |
| .context("create filter endpoints")?; |
| let (interface_state, _interface_state_server) = |
| fidl::endpoints::create_proxy::<fnet_interfaces::StateMarker>() |
| .context("create interface state endpoints")?; |
| let (dhcp_server, _dhcp_server_server) = |
| fidl::endpoints::create_proxy::<fnet_dhcp::Server_Marker>() |
| .context("error creating dhcp_server endpoints")?; |
| let (dhcpv6_client_provider, dhcpv6_client_provider_server) = |
| fidl::endpoints::create_proxy::<fnet_dhcpv6::ClientProviderMarker>() |
| .context("error creating dhcpv6_client_provider endpoints")?; |
| let (installer, _installer_server) = |
| fidl::endpoints::create_proxy::<fidl_fuchsia_net_interfaces_admin::InstallerMarker>() |
| .context("error creating installer endpoints")?; |
| let (debug, _debug_server) = |
| fidl::endpoints::create_proxy::<fidl_fuchsia_net_debug::InterfacesMarker>() |
| .context("error creating installer endpoints")?; |
| let persisted_interface_config = |
| interface::FileBackedConfig::load(&PERSISTED_INTERFACE_CONFIG_FILEPATH) |
| .context("error loading persistent interface configurations")?; |
| |
| Ok(( |
| NetCfg { |
| stack, |
| netstack, |
| lookup_admin, |
| filter, |
| interface_state, |
| installer, |
| debug, |
| dhcp_server: Some(dhcp_server), |
| dhcpv6_client_provider: Some(dhcpv6_client_provider), |
| persisted_interface_config, |
| allow_virtual_devices: false, |
| filter_enabled_interface_types: Default::default(), |
| interface_properties: Default::default(), |
| interface_states: Default::default(), |
| interface_metrics: Default::default(), |
| dns_servers: Default::default(), |
| forwarded_device_classes: Default::default(), |
| }, |
| ServerEnds { |
| lookup_admin: lookup_admin_server |
| .into_stream() |
| .context("error converting lookup_admin server to stream")?, |
| dhcpv6_client_provider: dhcpv6_client_provider_server |
| .into_stream() |
| .context("error converting dhcpv6_client_provider server to stream")?, |
| }, |
| )) |
| } |
| |
| const INTERFACE_ID: u64 = 1; |
| const DHCPV6_DNS_SOURCE: DnsServersUpdateSource = |
| DnsServersUpdateSource::Dhcpv6 { interface_id: INTERFACE_ID }; |
| const LINK_LOCAL_SOCKADDR1: fnet::Ipv6SocketAddress = fnet::Ipv6SocketAddress { |
| address: fidl_ip_v6!("fe80::1"), |
| port: fnet_dhcpv6::DEFAULT_CLIENT_PORT, |
| zone_index: INTERFACE_ID, |
| }; |
| const LINK_LOCAL_SOCKADDR2: fnet::Ipv6SocketAddress = fnet::Ipv6SocketAddress { |
| address: fidl_ip_v6!("fe80::2"), |
| port: fnet_dhcpv6::DEFAULT_CLIENT_PORT, |
| zone_index: INTERFACE_ID, |
| }; |
| const GLOBAL_ADDR: fnet::Subnet = fnet::Subnet { addr: fidl_ip!("2000::1"), prefix_len: 64 }; |
| const DNS_SERVER1: fnet::SocketAddress = fnet::SocketAddress::Ipv6(fnet::Ipv6SocketAddress { |
| address: fidl_ip_v6!("2001::1"), |
| port: fnet_dhcpv6::DEFAULT_CLIENT_PORT, |
| zone_index: 0, |
| }); |
| const DNS_SERVER2: fnet::SocketAddress = fnet::SocketAddress::Ipv6(fnet::Ipv6SocketAddress { |
| address: fidl_ip_v6!("2001::2"), |
| port: fnet_dhcpv6::DEFAULT_CLIENT_PORT, |
| zone_index: 0, |
| }); |
| |
| fn ipv6addrs(a: Option<fnet::Ipv6SocketAddress>) -> Vec<fnet_interfaces::Address> { |
| // The DHCPv6 client will only use a link-local address but we include a global address |
| // and expect it to not be used. |
| std::iter::once(fnet_interfaces::Address { |
| addr: Some(GLOBAL_ADDR), |
| valid_until: Some(fuchsia_zircon::Time::INFINITE.into_nanos()), |
| ..fnet_interfaces::Address::EMPTY |
| }) |
| .chain(a.map(|fnet::Ipv6SocketAddress { address, port: _, zone_index: _ }| { |
| fnet_interfaces::Address { |
| addr: Some(fnet::Subnet { addr: fnet::IpAddress::Ipv6(address), prefix_len: 64 }), |
| valid_until: Some(fuchsia_zircon::Time::INFINITE.into_nanos()), |
| ..fnet_interfaces::Address::EMPTY |
| } |
| })) |
| .collect() |
| } |
| |
| /// Handle receiving a netstack interface changed event. |
| async fn handle_interface_changed_event( |
| netcfg: &mut NetCfg<'_>, |
| dns_watchers: &mut DnsServerWatchers<'_>, |
| online: Option<bool>, |
| addresses: Option<Vec<fnet_interfaces::Address>>, |
| ) -> Result<(), errors::Error> { |
| let event = fnet_interfaces::Event::Changed(fnet_interfaces::Properties { |
| id: Some(INTERFACE_ID), |
| name: None, |
| device_class: None, |
| online, |
| addresses, |
| has_default_ipv4_route: None, |
| has_default_ipv6_route: None, |
| ..fnet_interfaces::Properties::EMPTY |
| }); |
| netcfg.handle_interface_watcher_event(event, dns_watchers, &mut virtualization::Stub).await |
| } |
| |
| /// Make sure that a new DHCPv6 client was requested, and verify its parameters. |
| async fn check_new_client( |
| server: &mut fnet_dhcpv6::ClientProviderRequestStream, |
| sockaddr: fnet::Ipv6SocketAddress, |
| dns_watchers: &mut DnsServerWatchers<'_>, |
| ) -> Result<fnet_dhcpv6::ClientRequestStream, anyhow::Error> { |
| let evt = |
| server.try_next().await.context("error getting next dhcpv6 client provider event")?; |
| let client_server = |
| match evt.ok_or(anyhow::anyhow!("expected dhcpv6 client provider request"))? { |
| fnet_dhcpv6::ClientProviderRequest::NewClient { |
| params, |
| request, |
| control_handle: _, |
| } => { |
| assert_eq!( |
| params, |
| fnet_dhcpv6::NewClientParams { |
| interface_id: Some(INTERFACE_ID), |
| address: Some(sockaddr), |
| config: Some(fnet_dhcpv6::ClientConfig { |
| information_config: Some(fnet_dhcpv6::InformationConfig { |
| dns_servers: Some(true), |
| ..fnet_dhcpv6::InformationConfig::EMPTY |
| }), |
| ..fnet_dhcpv6::ClientConfig::EMPTY |
| }), |
| ..fnet_dhcpv6::NewClientParams::EMPTY |
| } |
| ); |
| |
| request.into_stream().context("error converting client server end to stream")? |
| } |
| }; |
| assert!(dns_watchers.contains_key(&DHCPV6_DNS_SOURCE), "should have a watcher"); |
| Ok(client_server) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_stopping_dhcpv6_with_down_lookup_admin() -> Result<(), anyhow::Error> { |
| let (mut netcfg, ServerEnds { lookup_admin, mut dhcpv6_client_provider }) = |
| test_netcfg().context("error creating test netcfg")?; |
| let mut dns_watchers = DnsServerWatchers::empty(); |
| |
| // Mock a new interface being discovered by NetCfg (we only need to make NetCfg aware of a |
| // NIC with ID `INTERFACE_ID` to test DHCPv6). |
| let (control, _control_server_end) = |
| fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints() |
| .expect("create endpoints"); |
| assert_matches::assert_matches!( |
| netcfg.interface_states.insert(INTERFACE_ID, InterfaceState::new_host(control)), |
| None |
| ); |
| |
| // Should start the DHCPv6 client when we get an interface changed event that shows the |
| // interface as up with an link-local address. |
| let () = netcfg |
| .handle_interface_watcher_event( |
| fnet_interfaces::Event::Added(fnet_interfaces::Properties { |
| id: Some(INTERFACE_ID), |
| name: Some("testif01".to_string()), |
| device_class: Some(fnet_interfaces::DeviceClass::Loopback( |
| fnet_interfaces::Empty {}, |
| )), |
| online: Some(true), |
| addresses: Some(ipv6addrs(Some(LINK_LOCAL_SOCKADDR1))), |
| has_default_ipv4_route: Some(false), |
| has_default_ipv6_route: Some(false), |
| ..fnet_interfaces::Properties::EMPTY |
| }), |
| &mut dns_watchers, |
| &mut virtualization::Stub, |
| ) |
| .await |
| .context("error handling interface added event with interface up and sockaddr1") |
| .map_err::<anyhow::Error, _>(Into::into)?; |
| let _: fnet_dhcpv6::ClientRequestStream = |
| check_new_client(&mut dhcpv6_client_provider, LINK_LOCAL_SOCKADDR1, &mut dns_watchers) |
| .await |
| .context("error checking for new client with sockaddr1")?; |
| |
| // Drop the server-end of the lookup admin to simulate a down lookup admin service. |
| let () = std::mem::drop(lookup_admin); |
| |
| // Not having any more link local IPv6 addresses should terminate the client. |
| let () = handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| None, |
| Some(ipv6addrs(None)), |
| ) |
| .await |
| .context("error handling interface changed event with sockaddr1 removed") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| println!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })?; |
| |
| // Another update without any link-local IPv6 addresses should do nothing |
| // since the DHCPv6 client was already stopped. |
| let () = |
| handle_interface_changed_event(&mut netcfg, &mut dns_watchers, None, Some(Vec::new())) |
| .await |
| .context("error handling interface changed event with sockaddr1 removed") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| println!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })?; |
| |
| // Update interface with a link-local address to create a new DHCPv6 client. |
| let () = handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| None, |
| Some(ipv6addrs(Some(LINK_LOCAL_SOCKADDR1))), |
| ) |
| .await |
| .context("error handling interface changed event with sockaddr1 removed") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| println!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })?; |
| let _: fnet_dhcpv6::ClientRequestStream = |
| check_new_client(&mut dhcpv6_client_provider, LINK_LOCAL_SOCKADDR1, &mut dns_watchers) |
| .await |
| .context("error checking for new client with sockaddr1")?; |
| |
| // Update offline status to down to stop DHCPv6 client. |
| let () = handle_interface_changed_event(&mut netcfg, &mut dns_watchers, Some(false), None) |
| .await |
| .context("error handling interface changed event with sockaddr1 removed") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| println!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| })?; |
| |
| // Update interface with new addresses but leave offline status as down. |
| handle_interface_changed_event(&mut netcfg, &mut dns_watchers, None, Some(ipv6addrs(None))) |
| .await |
| .context("error handling interface changed event with sockaddr1 removed") |
| .or_else(|e| match e { |
| errors::Error::NonFatal(e) => { |
| println!("non-fatal error: {:?}", e); |
| Ok(()) |
| } |
| errors::Error::Fatal(e) => Err(e), |
| }) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dhcpv6() -> Result<(), anyhow::Error> { |
| /// Waits for a `SetDnsServers` request with the specified servers. |
| async fn run_lookup_admin_once( |
| server: &mut fnet_name::LookupAdminRequestStream, |
| expected_servers: &Vec<fnet::SocketAddress>, |
| ) -> Result<(), anyhow::Error> { |
| let req = server |
| .try_next() |
| .await |
| .context("error getting next lookup admin request")? |
| .ok_or(anyhow::anyhow!("expected lookup admin request"))?; |
| if let fnet_name::LookupAdminRequest::SetDnsServers { servers, responder } = req { |
| if expected_servers != &servers { |
| return Err(anyhow::anyhow!( |
| "got SetDnsServers servers = {:?}, want = {:?}", |
| servers, |
| expected_servers |
| )); |
| } |
| responder.send(&mut Ok(())).context("error sending set dns servers response") |
| } else { |
| Err(anyhow::anyhow!("unknown request = {:?}", req)) |
| } |
| } |
| |
| let (mut netcfg, mut servers) = test_netcfg().context("error creating test netcfg")?; |
| let mut dns_watchers = DnsServerWatchers::empty(); |
| |
| // Mock a fake DNS update from the netstack. |
| let netstack_servers = vec![DNS_SERVER1]; |
| let ((), ()) = future::try_join( |
| netcfg |
| .update_dns_servers( |
| DnsServersUpdateSource::Netstack, |
| vec![fnet_name::DnsServer_ { |
| address: Some(DNS_SERVER1), |
| source: Some(fnet_name::DnsServerSource::StaticSource( |
| fnet_name::StaticDnsServerSource::EMPTY, |
| )), |
| ..fnet_name::DnsServer_::EMPTY |
| }], |
| ) |
| .map(|r| r.context("error updating netstack DNS servers").map_err(Into::into)), |
| run_lookup_admin_once(&mut servers.lookup_admin, &netstack_servers) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error setting netstack DNS servers")?; |
| |
| // Mock a new interface being discovered by NetCfg (we only need to make NetCfg aware of a |
| // NIC with ID `INTERFACE_ID` to test DHCPv6). |
| let (control, _control_server_end) = |
| fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints() |
| .expect("create endpoints"); |
| assert_matches::assert_matches!( |
| netcfg.interface_states.insert(INTERFACE_ID, InterfaceState::new_host(control)), |
| None |
| ); |
| |
| // Should start the DHCPv6 client when we get an interface changed event that shows the |
| // interface as up with an link-local address. |
| let () = netcfg |
| .handle_interface_watcher_event( |
| fnet_interfaces::Event::Added(fnet_interfaces::Properties { |
| id: Some(INTERFACE_ID), |
| name: Some("testif01".to_string()), |
| device_class: Some(fnet_interfaces::DeviceClass::Loopback( |
| fnet_interfaces::Empty {}, |
| )), |
| online: Some(true), |
| addresses: Some(ipv6addrs(Some(LINK_LOCAL_SOCKADDR1))), |
| has_default_ipv4_route: Some(false), |
| has_default_ipv6_route: Some(false), |
| ..fnet_interfaces::Properties::EMPTY |
| }), |
| &mut dns_watchers, |
| &mut virtualization::Stub, |
| ) |
| .await |
| .context("error handling interface added event with interface up and sockaddr1") |
| .map_err::<anyhow::Error, _>(Into::into)?; |
| let mut client_server = check_new_client( |
| &mut servers.dhcpv6_client_provider, |
| LINK_LOCAL_SOCKADDR1, |
| &mut dns_watchers, |
| ) |
| .await |
| .context("error checking for new client with sockaddr1")?; |
| |
| // Mock a fake DNS update from the DHCPv6 client. |
| let ((), ()) = future::try_join( |
| netcfg |
| .update_dns_servers( |
| DHCPV6_DNS_SOURCE, |
| vec![fnet_name::DnsServer_ { |
| address: Some(DNS_SERVER2), |
| source: Some(fnet_name::DnsServerSource::Dhcpv6( |
| fnet_name::Dhcpv6DnsServerSource { |
| source_interface: Some(INTERFACE_ID), |
| ..fnet_name::Dhcpv6DnsServerSource::EMPTY |
| }, |
| )), |
| ..fnet_name::DnsServer_::EMPTY |
| }], |
| ) |
| .map(|r| r.context("error updating netstack DNS servers").map_err(Into::into)), |
| run_lookup_admin_once(&mut servers.lookup_admin, &vec![DNS_SERVER2, DNS_SERVER1]) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error setting dhcpv6 DNS servers")?; |
| |
| // Not having any more link local IPv6 addresses should terminate the client. |
| let ((), ()) = future::try_join( |
| handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| None, |
| Some(ipv6addrs(None)), |
| ) |
| .map(|r| r.context("error handling interface changed event with sockaddr1 removed")) |
| .map_err(Into::into), |
| run_lookup_admin_once(&mut servers.lookup_admin, &netstack_servers) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error handling client termination due to empty addresses")?; |
| assert!(!dns_watchers.contains_key(&DHCPV6_DNS_SOURCE), "should not have a watcher"); |
| assert_matches::assert_matches!(client_server.try_next().await, Ok(None)); |
| |
| // Should start a new DHCPv6 client when we get an interface changed event that shows the |
| // interface as up with an link-local address. |
| let () = handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| None, |
| Some(ipv6addrs(Some(LINK_LOCAL_SOCKADDR2))), |
| ) |
| .await |
| .context("error handling netstack event with sockaddr2 added") |
| .map_err(Into::<anyhow::Error>::into)?; |
| let mut client_server = check_new_client( |
| &mut servers.dhcpv6_client_provider, |
| LINK_LOCAL_SOCKADDR2, |
| &mut dns_watchers, |
| ) |
| .await |
| .context("error checking for new client with sockaddr2")?; |
| |
| // Interface being down should terminate the client. |
| let ((), ()) = future::try_join( |
| handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| Some(false), /* down */ |
| None, |
| ) |
| .map(|r| r.context("error handling interface changed event with interface down")) |
| .map_err(Into::into), |
| run_lookup_admin_once(&mut servers.lookup_admin, &netstack_servers) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error handling client termination due to interface down")?; |
| assert!(!dns_watchers.contains_key(&DHCPV6_DNS_SOURCE), "should not have a watcher"); |
| assert_matches::assert_matches!(client_server.try_next().await, Ok(None)); |
| |
| // Should start a new DHCPv6 client when we get an interface changed event that shows the |
| // interface as up with an link-local address. |
| let () = handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| Some(true), /* up */ |
| None, |
| ) |
| .await |
| .context("error handling interface up event") |
| .map_err(Into::<anyhow::Error>::into)?; |
| let mut client_server = check_new_client( |
| &mut servers.dhcpv6_client_provider, |
| LINK_LOCAL_SOCKADDR2, |
| &mut dns_watchers, |
| ) |
| .await |
| .context("error checking for new client with sockaddr2 after interface up again")?; |
| |
| // Should start a new DHCPv6 client when we get an interface changed event that shows the |
| // interface as up with a new link-local address. |
| let ((), ()) = future::try_join( |
| handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| None, |
| Some(ipv6addrs(Some(LINK_LOCAL_SOCKADDR1))), |
| ) |
| .map(|r| { |
| r.context( |
| "error handling interface change event with sockaddr1 replacing sockaddr2", |
| ) |
| }) |
| .map_err(Into::into), |
| run_lookup_admin_once(&mut servers.lookup_admin, &netstack_servers) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error handling client termination due to address change")?; |
| assert_matches::assert_matches!(client_server.try_next().await, Ok(None)); |
| let _client_server = check_new_client( |
| &mut servers.dhcpv6_client_provider, |
| LINK_LOCAL_SOCKADDR1, |
| &mut dns_watchers, |
| ) |
| .await |
| .context("error checking for new client with sockaddr1 after address change")?; |
| |
| // Complete the DNS server watcher then start a new one. |
| let ((), ()) = future::try_join( |
| netcfg |
| .handle_dns_server_watcher_done(DHCPV6_DNS_SOURCE, &mut dns_watchers) |
| .map(|r| r.context("error handling completion of dns server watcher")), |
| run_lookup_admin_once(&mut servers.lookup_admin, &netstack_servers) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error handling DNS server watcher completion")?; |
| assert!(!dns_watchers.contains_key(&DHCPV6_DNS_SOURCE), "should not have a watcher"); |
| let () = handle_interface_changed_event( |
| &mut netcfg, |
| &mut dns_watchers, |
| None, |
| Some(ipv6addrs(Some(LINK_LOCAL_SOCKADDR2))), |
| ) |
| .await |
| .context("error handling interface change event with sockaddr2 replacing sockaddr1") |
| .map_err(Into::<anyhow::Error>::into)?; |
| let mut client_server = check_new_client( |
| &mut servers.dhcpv6_client_provider, |
| LINK_LOCAL_SOCKADDR2, |
| &mut dns_watchers, |
| ) |
| .await |
| .context("error checking for new client with sockaddr2 after completing dns watcher")?; |
| |
| // An event that indicates the interface is removed should stop the client. |
| let ((), ()) = future::try_join( |
| netcfg |
| .handle_interface_watcher_event( |
| fnet_interfaces::Event::Removed(INTERFACE_ID), |
| &mut dns_watchers, |
| &mut virtualization::Stub, |
| ) |
| .map(|r| r.context("error handling interface removed event")) |
| .map_err(Into::into), |
| run_lookup_admin_once(&mut servers.lookup_admin, &netstack_servers) |
| .map(|r| r.context("error running lookup admin")), |
| ) |
| .await |
| .context("error handling client termination due to interface removal")?; |
| assert!(!dns_watchers.contains_key(&DHCPV6_DNS_SOURCE), "should not have a watcher"); |
| assert_matches::assert_matches!(client_server.try_next().await, Ok(None)); |
| assert!(!netcfg.interface_states.contains_key(&INTERFACE_ID)); |
| |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_config() { |
| let config_str = r#" |
| { |
| "dns_config": { |
| "servers": ["8.8.8.8"] |
| }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": ["wlan"], |
| "interface_metrics": { |
| "wlan_metric": 100, |
| "eth_metric": 10 |
| }, |
| "allowed_upstream_device_classes": ["ethernet", "wlan"], |
| "enable_dhcpv6": true, |
| "forwarded_device_classes": { "ipv4": [ "ethernet" ], "ipv6": [ "wlan" ] } |
| } |
| "#; |
| |
| let Config { |
| dns_config: DnsConfig { servers }, |
| filter_config, |
| filter_enabled_interface_types, |
| interface_metrics, |
| allowed_upstream_device_classes, |
| enable_dhcpv6, |
| forwarded_device_classes, |
| } = Config::load_str(config_str).unwrap(); |
| |
| assert_eq!(vec!["8.8.8.8".parse::<std::net::IpAddr>().unwrap()], servers); |
| let FilterConfig { rules, nat_rules, rdr_rules } = filter_config; |
| assert_eq!(Vec::<String>::new(), rules); |
| assert_eq!(Vec::<String>::new(), nat_rules); |
| assert_eq!(Vec::<String>::new(), rdr_rules); |
| |
| assert_eq!(HashSet::from([InterfaceType::Wlan]), filter_enabled_interface_types); |
| |
| let expected_metrics = |
| InterfaceMetrics { wlan_metric: Metric(100), eth_metric: Metric(10) }; |
| assert_eq!(interface_metrics, expected_metrics); |
| |
| assert_eq!( |
| AllowedDeviceClasses(HashSet::from([DeviceClass::Ethernet, DeviceClass::Wlan])), |
| allowed_upstream_device_classes |
| ); |
| |
| assert_eq!(enable_dhcpv6, true); |
| |
| let expected_classes = ForwardedDeviceClasses { |
| ipv4: HashSet::from([DeviceClass::Ethernet]), |
| ipv6: HashSet::from([DeviceClass::Wlan]), |
| }; |
| assert_eq!(forwarded_device_classes, expected_classes); |
| } |
| |
| #[test] |
| fn test_config_defaults() { |
| let config_str = r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": [] |
| } |
| "#; |
| |
| let Config { |
| dns_config: _, |
| filter_config: _, |
| filter_enabled_interface_types: _, |
| allowed_upstream_device_classes, |
| interface_metrics, |
| enable_dhcpv6, |
| forwarded_device_classes: _, |
| } = Config::load_str(config_str).unwrap(); |
| |
| assert_eq!(allowed_upstream_device_classes, Default::default()); |
| assert_eq!(interface_metrics, Default::default()); |
| assert_eq!(enable_dhcpv6, true); |
| } |
| |
| #[test_case( |
| "eth_metric", Default::default(), Metric(1), Metric(1); |
| "wlan assumes default metric when unspecified")] |
| #[test_case("wlan_metric", Metric(1), Default::default(), Metric(1); |
| "eth assumes default metric when unspecified")] |
| fn test_config_metric_individual_defaults( |
| metric_name: &'static str, |
| wlan_metric: Metric, |
| eth_metric: Metric, |
| expect_metric: Metric, |
| ) { |
| let config_str = format!( |
| r#" |
| {{ |
| "dns_config": {{ "servers": [] }}, |
| "filter_config": {{ |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }}, |
| "filter_enabled_interface_types": [], |
| "interface_metrics": {{ "{}": {} }} |
| }} |
| "#, |
| metric_name, expect_metric |
| ); |
| |
| let Config { |
| dns_config: _, |
| filter_config: _, |
| filter_enabled_interface_types: _, |
| allowed_upstream_device_classes: _, |
| enable_dhcpv6: _, |
| interface_metrics, |
| forwarded_device_classes: _, |
| } = Config::load_str(&config_str).unwrap(); |
| |
| let expected_metrics = InterfaceMetrics { wlan_metric, eth_metric }; |
| assert_eq!(interface_metrics, expected_metrics); |
| } |
| |
| #[test] |
| fn test_config_denies_unknown_fields() { |
| let config_str = r#"{ |
| "filter_enabled_interface_types": ["wlan"], |
| "foobar": "baz" |
| }"#; |
| |
| let err = Config::load_str(config_str).expect_err("config shouldn't accept unknown fields"); |
| let err = err.downcast::<serde_json::Error>().expect("downcast error"); |
| assert_eq!(err.classify(), serde_json::error::Category::Data); |
| assert_eq!(err.line(), 3); |
| // Ensure the error is complaining about unknown field. |
| assert!(format!("{:?}", err).contains("foobar")); |
| } |
| |
| #[test] |
| fn test_config_denies_unknown_fields_nested() { |
| let bad_configs = vec![ |
| r#" |
| { |
| "dns_config": { "speling": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": [] |
| } |
| "#, |
| r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "speling": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": [] |
| } |
| "#, |
| r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": ["speling"] |
| } |
| "#, |
| r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "interface_metrics": { |
| "eth_metric": 1, |
| "wlan_metric": 2, |
| "speling": 3, |
| }, |
| "filter_enabled_interface_types": [] |
| } |
| "#, |
| r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": ["speling"] |
| } |
| "#, |
| r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": [], |
| "allowed_upstream_device_classes": ["speling"] |
| } |
| "#, |
| r#" |
| { |
| "dns_config": { "servers": [] }, |
| "filter_config": { |
| "rules": [], |
| "nat_rules": [], |
| "rdr_rules": [] |
| }, |
| "filter_enabled_interface_types": [], |
| "allowed_upstream_device_classes": [], |
| "forwarded_device_classes": { "ipv4": [], "ipv6": [], "speling": [] } |
| } |
| "#, |
| ]; |
| |
| for config_str in bad_configs { |
| let err = |
| Config::load_str(config_str).expect_err("config shouldn't accept unknown fields"); |
| let err = err.downcast::<serde_json::Error>().expect("downcast error"); |
| assert_eq!(err.classify(), serde_json::error::Category::Data); |
| // Ensure the error is complaining about unknown field. |
| assert!(format!("{:?}", err).contains("speling")); |
| } |
| } |
| |
| #[test] |
| fn test_should_enable_filter() { |
| let types_empty: HashSet<InterfaceType> = [].iter().cloned().collect(); |
| let types_ethernet: HashSet<InterfaceType> = |
| [InterfaceType::Ethernet].iter().cloned().collect(); |
| let types_wlan: HashSet<InterfaceType> = [InterfaceType::Wlan].iter().cloned().collect(); |
| |
| let make_info = |device_class| DeviceInfo { |
| device_class, |
| mac: None, |
| is_synthetic: false, |
| topological_path: "".to_string(), |
| }; |
| |
| let wlan_info = make_info(fidl_fuchsia_hardware_network::DeviceClass::Wlan); |
| let wlan_ap_info = make_info(fidl_fuchsia_hardware_network::DeviceClass::WlanAp); |
| let ethernet_info = make_info(fidl_fuchsia_hardware_network::DeviceClass::Ethernet); |
| |
| assert_eq!(should_enable_filter(&types_empty, &wlan_info), false); |
| assert_eq!(should_enable_filter(&types_empty, &wlan_ap_info), false); |
| assert_eq!(should_enable_filter(&types_empty, ðernet_info), false); |
| |
| assert_eq!(should_enable_filter(&types_ethernet, &wlan_info), false); |
| assert_eq!(should_enable_filter(&types_ethernet, &wlan_ap_info), false); |
| assert_eq!(should_enable_filter(&types_ethernet, ðernet_info), true); |
| |
| assert_eq!(should_enable_filter(&types_wlan, &wlan_info), true); |
| assert_eq!(should_enable_filter(&types_wlan, &wlan_ap_info), true); |
| assert_eq!(should_enable_filter(&types_wlan, ðernet_info), false); |
| } |
| } |