| // 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. |
| |
| use anyhow::{Context as _, Error}; |
| use fidl_fuchsia_net as fnet; |
| use fidl_fuchsia_net_debug as fdebug; |
| use fidl_fuchsia_net_dhcp as fdhcp; |
| use fidl_fuchsia_net_ext as fnet_ext; |
| use fidl_fuchsia_net_filter as fnet_filter; |
| use fidl_fuchsia_net_filter_deprecated as ffilter_deprecated; |
| use fidl_fuchsia_net_filter_ext as fnet_filter_ext; |
| use fidl_fuchsia_net_interfaces as finterfaces; |
| use fidl_fuchsia_net_interfaces_admin as finterfaces_admin; |
| use fidl_fuchsia_net_interfaces_ext as finterfaces_ext; |
| use fidl_fuchsia_net_name as fname; |
| use fidl_fuchsia_net_neighbor as fneighbor; |
| use fidl_fuchsia_net_neighbor_ext as fneighbor_ext; |
| use fidl_fuchsia_net_root as froot; |
| use fidl_fuchsia_net_routes as froutes; |
| use fidl_fuchsia_net_routes_ext as froutes_ext; |
| use fidl_fuchsia_net_stack as fstack; |
| use fidl_fuchsia_net_stack_ext::{self as fstack_ext, FidlReturn as _}; |
| use fidl_fuchsia_net_stackmigrationdeprecated as fnet_migration; |
| use fuchsia_zircon_status as zx; |
| use futures::{FutureExt as _, StreamExt as _, TryFutureExt as _, TryStreamExt as _}; |
| use net_types::ip::{Ipv4, Ipv6}; |
| use netfilter::FidlReturn as _; |
| use prettytable::{cell, format, row, Row, Table}; |
| use ser::AddressAssignmentState; |
| use serde_json::{json, value::Value}; |
| use std::collections::hash_map::HashMap; |
| use std::{convert::TryFrom as _, iter::FromIterator as _, pin::pin, str::FromStr as _}; |
| use tracing::{info, warn}; |
| |
| mod opts; |
| pub use opts::{ |
| underlying_user_facing_error, user_facing_error, Command, CommandEnum, UserFacingError, |
| }; |
| |
| mod filter; |
| use crate::filter::{FilteringResources, Namespace}; |
| |
| mod ser; |
| |
| macro_rules! filter_fidl { |
| ($method:expr, $context:expr) => { |
| $method.await.transform_result().context($context) |
| }; |
| } |
| |
| fn add_row(t: &mut Table, row: Row) { |
| let _: &mut Row = t.add_row(row); |
| } |
| |
| /// An interface for acquiring a proxy to a FIDL service. |
| #[async_trait::async_trait] |
| pub trait ServiceConnector<S: fidl::endpoints::ProtocolMarker> { |
| /// Acquires a proxy to the parameterized FIDL interface. |
| async fn connect(&self) -> Result<S::Proxy, Error>; |
| } |
| |
| /// An interface for acquiring all system dependencies required by net-cli. |
| /// |
| /// FIDL dependencies are specified as supertraits. These supertraits are a complete enumeration of |
| /// all FIDL dependencies required by net-cli. |
| pub trait NetCliDepsConnector: |
| ServiceConnector<fdebug::InterfacesMarker> |
| + ServiceConnector<froot::InterfacesMarker> |
| + ServiceConnector<fdhcp::Server_Marker> |
| + ServiceConnector<ffilter_deprecated::FilterMarker> |
| + ServiceConnector<finterfaces::StateMarker> |
| + ServiceConnector<fneighbor::ControllerMarker> |
| + ServiceConnector<fneighbor::ViewMarker> |
| + ServiceConnector<fstack::LogMarker> |
| + ServiceConnector<fstack::StackMarker> |
| + ServiceConnector<froutes::StateV4Marker> |
| + ServiceConnector<froutes::StateV6Marker> |
| + ServiceConnector<fname::LookupMarker> |
| + ServiceConnector<fnet_migration::ControlMarker> |
| + ServiceConnector<fnet_migration::StateMarker> |
| + ServiceConnector<fnet_filter::StateMarker> |
| { |
| } |
| |
| impl<O> NetCliDepsConnector for O where |
| O: ServiceConnector<fdebug::InterfacesMarker> |
| + ServiceConnector<froot::InterfacesMarker> |
| + ServiceConnector<fdhcp::Server_Marker> |
| + ServiceConnector<ffilter_deprecated::FilterMarker> |
| + ServiceConnector<finterfaces::StateMarker> |
| + ServiceConnector<fneighbor::ControllerMarker> |
| + ServiceConnector<fneighbor::ViewMarker> |
| + ServiceConnector<fstack::LogMarker> |
| + ServiceConnector<fstack::StackMarker> |
| + ServiceConnector<froutes::StateV4Marker> |
| + ServiceConnector<froutes::StateV6Marker> |
| + ServiceConnector<fname::LookupMarker> |
| + ServiceConnector<fnet_migration::ControlMarker> |
| + ServiceConnector<fnet_migration::StateMarker> |
| + ServiceConnector<fnet_filter::StateMarker> |
| { |
| } |
| |
| pub async fn do_root<C: NetCliDepsConnector>( |
| mut out: ffx_writer::Writer, |
| Command { cmd }: Command, |
| connector: &C, |
| ) -> Result<(), Error> { |
| match cmd { |
| CommandEnum::If(opts::If { if_cmd: cmd }) => { |
| do_if(&mut out, cmd, connector).await.context("failed during if command") |
| } |
| CommandEnum::Route(opts::Route { route_cmd: cmd }) => { |
| do_route(&mut out, cmd, connector).await.context("failed during route command") |
| } |
| CommandEnum::FilterDeprecated(opts::FilterDeprecated { filter_cmd: cmd }) => { |
| do_filter_deprecated(out, cmd, connector) |
| .await |
| .context("failed during filter-deprecated command") |
| } |
| CommandEnum::Filter(opts::filter::Filter { filter_cmd: cmd }) => { |
| do_filter(out, cmd, connector).await.context("failed during filter command") |
| } |
| CommandEnum::Log(opts::Log { log_cmd: cmd }) => { |
| do_log(cmd, connector).await.context("failed during log command") |
| } |
| CommandEnum::Dhcp(opts::Dhcp { dhcp_cmd: cmd }) => { |
| do_dhcp(cmd, connector).await.context("failed during dhcp command") |
| } |
| CommandEnum::Dhcpd(opts::dhcpd::Dhcpd { dhcpd_cmd: cmd }) => { |
| do_dhcpd(cmd, connector).await.context("failed during dhcpd command") |
| } |
| CommandEnum::Neigh(opts::Neigh { neigh_cmd: cmd }) => { |
| do_neigh(out, cmd, connector).await.context("failed during neigh command") |
| } |
| CommandEnum::Dns(opts::dns::Dns { dns_cmd: cmd }) => { |
| do_dns(out, cmd, connector).await.context("failed during dns command") |
| } |
| CommandEnum::NetstackMigration(opts::NetstackMigration { cmd }) => { |
| do_netstack_migration(out, cmd, connector) |
| .await |
| .context("failed during migration command") |
| } |
| } |
| } |
| |
| fn shortlist_interfaces( |
| name_pattern: &str, |
| interfaces: &mut HashMap<u64, finterfaces_ext::PropertiesAndState<()>>, |
| ) { |
| interfaces.retain(|_: &u64, properties_and_state| { |
| properties_and_state.properties.name.contains(name_pattern) |
| }) |
| } |
| |
| fn write_tabulated_interfaces_info< |
| W: std::io::Write, |
| I: IntoIterator<Item = ser::InterfaceView>, |
| >( |
| mut out: W, |
| interfaces: I, |
| ) -> Result<(), Error> { |
| let mut t = Table::new(); |
| t.set_format(format::FormatBuilder::new().padding(2, 2).build()); |
| for ( |
| i, |
| ser::InterfaceView { |
| nicid, |
| name, |
| device_class, |
| online, |
| addresses, |
| mac, |
| has_default_ipv4_route, |
| has_default_ipv6_route, |
| }, |
| ) in interfaces.into_iter().enumerate() |
| { |
| if i > 0 { |
| let () = add_row(&mut t, row![]); |
| } |
| |
| let () = add_row(&mut t, row!["nicid", nicid]); |
| let () = add_row(&mut t, row!["name", name]); |
| let () = add_row( |
| &mut t, |
| row![ |
| "device class", |
| match device_class { |
| ser::DeviceClass::Loopback => "loopback", |
| ser::DeviceClass::Virtual => "virtual", |
| ser::DeviceClass::Ethernet => "ethernet", |
| ser::DeviceClass::Wlan => "wlan", |
| ser::DeviceClass::Ppp => "ppp", |
| ser::DeviceClass::Bridge => "bridge", |
| ser::DeviceClass::WlanAp => "wlan-ap", |
| } |
| ], |
| ); |
| let () = add_row(&mut t, row!["online", online]); |
| |
| let default_routes: std::borrow::Cow<'_, _> = |
| if has_default_ipv4_route || has_default_ipv6_route { |
| itertools::Itertools::intersperse( |
| has_default_ipv4_route |
| .then_some("IPv4") |
| .into_iter() |
| .chain(has_default_ipv6_route.then_some("IPv6")), |
| ",", |
| ) |
| .collect::<String>() |
| .into() |
| } else { |
| "-".into() |
| }; |
| add_row(&mut t, row!["default routes", default_routes]); |
| |
| for ser::Address { |
| subnet: ser::Subnet { addr, prefix_len }, |
| valid_until, |
| assignment_state, |
| } in addresses.all_addresses() |
| { |
| let valid_until = valid_until.map(|v| { |
| let v = std::time::Duration::from_nanos(v.try_into().unwrap_or_else(|_| 0)) |
| .as_secs_f32(); |
| std::borrow::Cow::Owned(format!("valid until [{v}s]")) |
| }); |
| let assignment_state: Option<std::borrow::Cow<'_, _>> = match assignment_state { |
| AddressAssignmentState::Assigned => None, |
| AddressAssignmentState::Tentative => Some("TENTATIVE".into()), |
| AddressAssignmentState::Unavailable => Some("UNAVAILABLE".into()), |
| }; |
| let extra_bits = itertools::Itertools::intersperse( |
| assignment_state.into_iter().chain(valid_until), |
| " ".into(), |
| ) |
| .collect::<String>(); |
| |
| let () = add_row(&mut t, row!["addr", format!("{addr}/{prefix_len}"), extra_bits]); |
| } |
| match mac { |
| None => add_row(&mut t, row!["mac", "-"]), |
| Some(mac) => add_row(&mut t, row!["mac", mac]), |
| } |
| } |
| writeln!(out, "{}", t)?; |
| Ok(()) |
| } |
| |
| pub(crate) async fn connect_with_context<S, C>(connector: &C) -> Result<S::Proxy, Error> |
| where |
| C: ServiceConnector<S>, |
| S: fidl::endpoints::ProtocolMarker, |
| { |
| connector.connect().await.with_context(|| format!("failed to connect to {}", S::DEBUG_NAME)) |
| } |
| |
| async fn get_control<C>(connector: &C, id: u64) -> Result<finterfaces_ext::admin::Control, Error> |
| where |
| C: ServiceConnector<froot::InterfacesMarker>, |
| { |
| let root_interfaces = connect_with_context::<froot::InterfacesMarker, _>(connector).await?; |
| let (control, server_end) = finterfaces_ext::admin::Control::create_endpoints() |
| .context("create admin control endpoints")?; |
| let () = root_interfaces.get_admin(id, server_end).context("send get admin request")?; |
| Ok(control) |
| } |
| |
| fn configuration_with_ip_forwarding_set( |
| ip_version: fnet::IpVersion, |
| forwarding: bool, |
| ) -> finterfaces_admin::Configuration { |
| match ip_version { |
| fnet::IpVersion::V4 => finterfaces_admin::Configuration { |
| ipv4: Some(finterfaces_admin::Ipv4Configuration { |
| forwarding: Some(forwarding), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| fnet::IpVersion::V6 => finterfaces_admin::Configuration { |
| ipv6: Some(finterfaces_admin::Ipv6Configuration { |
| forwarding: Some(forwarding), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| } |
| } |
| |
| fn extract_ip_forwarding( |
| finterfaces_admin::Configuration { |
| ipv4: ipv4_config, ipv6: ipv6_config, .. |
| }: finterfaces_admin::Configuration, |
| ip_version: fnet::IpVersion, |
| ) -> Result<bool, Error> { |
| match ip_version { |
| fnet::IpVersion::V4 => { |
| let finterfaces_admin::Ipv4Configuration { forwarding, .. } = |
| ipv4_config.context("get IPv4 configuration")?; |
| forwarding.context("get IPv4 forwarding configuration") |
| } |
| fnet::IpVersion::V6 => { |
| let finterfaces_admin::Ipv6Configuration { forwarding, .. } = |
| ipv6_config.context("get IPv6 configuration")?; |
| forwarding.context("get IPv6 forwarding configuration") |
| } |
| } |
| } |
| |
| fn extract_igmp_version( |
| finterfaces_admin::Configuration { ipv4: ipv4_config, .. }: finterfaces_admin::Configuration, |
| ) -> Result<Option<finterfaces_admin::IgmpVersion>, Error> { |
| let finterfaces_admin::Ipv4Configuration { igmp, .. } = |
| ipv4_config.context("get IPv4 configuration")?; |
| let finterfaces_admin::IgmpConfiguration { version: igmp_version, .. } = |
| igmp.context("get IGMP configuration")?; |
| Ok(igmp_version) |
| } |
| |
| fn extract_mld_version( |
| finterfaces_admin::Configuration { ipv6: ipv6_config, .. }: finterfaces_admin::Configuration, |
| ) -> Result<Option<finterfaces_admin::MldVersion>, Error> { |
| let finterfaces_admin::Ipv6Configuration { mld, .. } = |
| ipv6_config.context("get IPv6 configuration")?; |
| let finterfaces_admin::MldConfiguration { version: mld_version, .. } = |
| mld.context("get MLD configuration")?; |
| Ok(mld_version) |
| } |
| |
| fn extract_nud_config( |
| finterfaces_admin::Configuration { ipv4, ipv6, .. }: finterfaces_admin::Configuration, |
| ip_version: fnet::IpVersion, |
| ) -> Result<finterfaces_admin::NudConfiguration, Error> { |
| match ip_version { |
| fnet::IpVersion::V4 => { |
| let finterfaces_admin::Ipv4Configuration { arp, .. } = |
| ipv4.context("get IPv4 configuration")?; |
| let finterfaces_admin::ArpConfiguration { nud, .. } = |
| arp.context("get ARP configuration")?; |
| nud.context("get NUD configuration") |
| } |
| fnet::IpVersion::V6 => { |
| let finterfaces_admin::Ipv6Configuration { ndp, .. } = |
| ipv6.context("get IPv6 configuration")?; |
| let finterfaces_admin::NdpConfiguration { nud, .. } = |
| ndp.context("get NDP configuration")?; |
| nud.context("get NUD configuration") |
| } |
| } |
| } |
| |
| async fn do_if<C: NetCliDepsConnector>( |
| out: &mut ffx_writer::Writer, |
| cmd: opts::IfEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| match cmd { |
| opts::IfEnum::List(opts::IfList { name_pattern }) => { |
| let root_interfaces = |
| connect_with_context::<froot::InterfacesMarker, _>(connector).await?; |
| let interface_state = |
| connect_with_context::<finterfaces::StateMarker, _>(connector).await?; |
| let stream = finterfaces_ext::event_stream_from_state( |
| &interface_state, |
| finterfaces_ext::IncludedAddresses::OnlyAssigned, |
| )?; |
| let mut response = finterfaces_ext::existing( |
| stream, |
| HashMap::<u64, finterfaces_ext::PropertiesAndState<()>>::new(), |
| ) |
| .await?; |
| if let Some(name_pattern) = name_pattern { |
| let () = shortlist_interfaces(&name_pattern, &mut response); |
| } |
| let response = response.into_values().map( |
| |finterfaces_ext::PropertiesAndState { properties, state: () }| async { |
| let mac = root_interfaces |
| .get_mac(properties.id.get()) |
| .await |
| .context("call get_mac")?; |
| Ok::<_, Error>((properties, mac)) |
| }, |
| ); |
| let response = futures::future::try_join_all(response).await?; |
| let mut response: Vec<_> = response |
| .into_iter() |
| .filter_map(|(properties, mac)| match mac { |
| Err(froot::InterfacesGetMacError::NotFound) => None, |
| Ok(mac) => { |
| let mac = mac.map(|box_| *box_); |
| Some((properties, mac).into()) |
| } |
| }) |
| .collect(); |
| let () = response.sort_by_key(|ser::InterfaceView { nicid, .. }| *nicid); |
| if out.is_machine() { |
| out.machine(&response)?; |
| } else { |
| write_tabulated_interfaces_info(out, response.into_iter()) |
| .context("error tabulating interface info")?; |
| } |
| } |
| opts::IfEnum::Get(opts::IfGet { interface }) => { |
| let id = interface.find_nicid(connector).await?; |
| let root_interfaces = |
| connect_with_context::<froot::InterfacesMarker, _>(connector).await?; |
| let interface_state = |
| connect_with_context::<finterfaces::StateMarker, _>(connector).await?; |
| let stream = finterfaces_ext::event_stream_from_state( |
| &interface_state, |
| finterfaces_ext::IncludedAddresses::OnlyAssigned, |
| )?; |
| let response = finterfaces_ext::existing( |
| stream, |
| finterfaces_ext::InterfaceState::<()>::Unknown(id), |
| ) |
| .await?; |
| match response { |
| finterfaces_ext::InterfaceState::Unknown(id) => { |
| return Err(user_facing_error(format!("interface with id={} not found", id))); |
| } |
| finterfaces_ext::InterfaceState::Known(finterfaces_ext::PropertiesAndState { |
| properties, |
| state: _, |
| }) => { |
| let finterfaces_ext::Properties { id, .. } = &properties; |
| let mac = root_interfaces.get_mac(id.get()).await.context("call get_mac")?; |
| match mac { |
| Err(froot::InterfacesGetMacError::NotFound) => { |
| return Err(user_facing_error(format!( |
| "interface with id={} not found", |
| id |
| ))); |
| } |
| Ok(mac) => { |
| let mac = mac.map(|box_| *box_); |
| let view = (properties, mac).into(); |
| write_tabulated_interfaces_info(out, std::iter::once(view)) |
| .context("error tabulating interface info")?; |
| } |
| }; |
| } |
| } |
| } |
| opts::IfEnum::Igmp(opts::IfIgmp { cmd }) => match cmd { |
| opts::IfIgmpEnum::Get(opts::IfIgmpGet { interface }) => { |
| let id = interface.find_nicid(connector).await.context("find nicid")?; |
| let control = get_control(connector, id).await.context("get control")?; |
| let configuration = control |
| .get_configuration() |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("get configuration")?; |
| |
| out.line(format!("IGMP configuration on interface {}:", id))?; |
| out.line(format!( |
| " Version: {:?}", |
| extract_igmp_version(configuration).context("get IGMP version")? |
| ))?; |
| } |
| opts::IfIgmpEnum::Set(opts::IfIgmpSet { interface, version }) => { |
| let id = interface.find_nicid(connector).await.context("find nicid")?; |
| let control = get_control(connector, id).await.context("get control")?; |
| let prev_config = control |
| .set_configuration(&finterfaces_admin::Configuration { |
| ipv4: Some(finterfaces_admin::Ipv4Configuration { |
| igmp: Some(finterfaces_admin::IgmpConfiguration { |
| version, |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("set configuration")?; |
| |
| info!( |
| "IGMP version set to {:?} on interface {}; previously set to {:?}", |
| version, |
| id, |
| extract_igmp_version(prev_config).context("get IGMP version")?, |
| ); |
| } |
| }, |
| opts::IfEnum::Mld(opts::IfMld { cmd }) => match cmd { |
| opts::IfMldEnum::Get(opts::IfMldGet { interface }) => { |
| let id = interface.find_nicid(connector).await.context("find nicid")?; |
| let control = get_control(connector, id).await.context("get control")?; |
| let configuration = control |
| .get_configuration() |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("get configuration")?; |
| |
| out.line(format!("MLD configuration on interface {}:", id))?; |
| out.line(format!( |
| " Version: {:?}", |
| extract_mld_version(configuration).context("get MLD version")? |
| ))?; |
| } |
| opts::IfMldEnum::Set(opts::IfMldSet { interface, version }) => { |
| let id = interface.find_nicid(connector).await.context("find nicid")?; |
| let control = get_control(connector, id).await.context("get control")?; |
| let prev_config = control |
| .set_configuration(&finterfaces_admin::Configuration { |
| ipv6: Some(finterfaces_admin::Ipv6Configuration { |
| mld: Some(finterfaces_admin::MldConfiguration { |
| version, |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("set configuration")?; |
| |
| info!( |
| "MLD version set to {:?} on interface {}; previously set to {:?}", |
| version, |
| id, |
| extract_mld_version(prev_config).context("get MLD version")?, |
| ); |
| } |
| }, |
| opts::IfEnum::IpForward(opts::IfIpForward { cmd }) => match cmd { |
| opts::IfIpForwardEnum::Get(opts::IfIpForwardGet { interface, ip_version }) => { |
| let id = interface.find_nicid(connector).await.context("find nicid")?; |
| let control = get_control(connector, id).await.context("get control")?; |
| let configuration = control |
| .get_configuration() |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("get configuration")?; |
| |
| out.line(format!( |
| "IP forwarding for {:?} is {} on interface {}", |
| ip_version, |
| extract_ip_forwarding(configuration, ip_version) |
| .context("extract IP forwarding configuration")?, |
| id |
| ))?; |
| } |
| opts::IfIpForwardEnum::Set(opts::IfIpForwardSet { interface, ip_version, enable }) => { |
| let id = interface.find_nicid(connector).await.context("find nicid")?; |
| let control = get_control(connector, id).await.context("get control")?; |
| let prev_config = control |
| .set_configuration(&configuration_with_ip_forwarding_set(ip_version, enable)) |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("set configuration")?; |
| info!( |
| "IP forwarding for {:?} set to {} on interface {}; previously set to {}", |
| ip_version, |
| enable, |
| id, |
| extract_ip_forwarding(prev_config, ip_version) |
| .context("extract IP forwarding configuration")? |
| ); |
| } |
| }, |
| opts::IfEnum::Enable(opts::IfEnable { interface }) => { |
| let id = interface.find_nicid(connector).await?; |
| let control = get_control(connector, id).await?; |
| let did_enable = control |
| .enable() |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlEnableError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("error enabling interface")?; |
| if did_enable { |
| info!("Interface {} enabled", id); |
| } else { |
| info!("Interface {} already enabled", id); |
| } |
| } |
| opts::IfEnum::Disable(opts::IfDisable { interface }) => { |
| let id = interface.find_nicid(connector).await?; |
| let control = get_control(connector, id).await?; |
| let did_disable = control |
| .disable() |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlDisableError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("error disabling interface")?; |
| if did_disable { |
| info!("Interface {} disabled", id); |
| } else { |
| info!("Interface {} already disabled", id); |
| } |
| } |
| opts::IfEnum::Addr(opts::IfAddr { addr_cmd }) => match addr_cmd { |
| opts::IfAddrEnum::Add(opts::IfAddrAdd { interface, addr, prefix, no_subnet_route }) => { |
| let id = interface.find_nicid(connector).await?; |
| let control = get_control(connector, id).await?; |
| let addr = fnet_ext::IpAddress::from_str(&addr)?.into(); |
| let subnet = fnet_ext::Subnet { addr, prefix_len: prefix }; |
| let (address_state_provider, server_end) = fidl::endpoints::create_proxy::< |
| finterfaces_admin::AddressStateProviderMarker, |
| >() |
| .context("create proxy")?; |
| let () = control |
| .add_address( |
| &subnet.into(), |
| &finterfaces_admin::AddressParameters { |
| add_subnet_route: Some(!no_subnet_route), |
| ..Default::default() |
| }, |
| server_end, |
| ) |
| .context("call add address")?; |
| |
| let () = address_state_provider.detach().context("detach address lifetime")?; |
| let state_stream = |
| finterfaces_ext::admin::assignment_state_stream(address_state_provider); |
| |
| state_stream |
| .try_filter_map(|state| { |
| futures::future::ok(match state { |
| finterfaces::AddressAssignmentState::Tentative => None, |
| finterfaces::AddressAssignmentState::Assigned => Some(()), |
| finterfaces::AddressAssignmentState::Unavailable => Some(()), |
| }) |
| }) |
| .try_next() |
| .await |
| .context("error after adding address")? |
| .ok_or_else(|| { |
| anyhow::anyhow!( |
| "Address assignment state stream unexpectedly ended \ |
| before reaching Assigned or Unavailable state. \ |
| This is probably a bug." |
| ) |
| })?; |
| |
| info!("Address {}/{} added to interface {}", addr, prefix, id); |
| } |
| opts::IfAddrEnum::Del(opts::IfAddrDel { interface, addr, prefix }) => { |
| let id = interface.find_nicid(connector).await?; |
| let control = get_control(connector, id).await?; |
| let addr = fnet_ext::IpAddress::from_str(&addr)?; |
| let did_remove = { |
| let addr = addr.into(); |
| let subnet = fnet::Subnet { |
| addr, |
| prefix_len: prefix.unwrap_or_else(|| { |
| 8 * u8::try_from(match addr { |
| fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr }) => addr.len(), |
| fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr }) => addr.len(), |
| }) |
| .expect("prefix length doesn't fit u8") |
| }), |
| }; |
| control |
| .remove_address(&subnet) |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlRemoveAddressError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("call remove address")? |
| }; |
| if !did_remove { |
| return Err(user_facing_error(format!( |
| "Address {} not found on interface {}", |
| addr, id |
| ))); |
| } |
| info!("Address {} deleted from interface {}", addr, id); |
| } |
| opts::IfAddrEnum::Wait(opts::IfAddrWait { interface, ipv6 }) => { |
| let id = interface.find_nicid(connector).await?; |
| let interfaces_state = |
| connect_with_context::<finterfaces::StateMarker, _>(connector).await?; |
| let mut state = finterfaces_ext::InterfaceState::<()>::Unknown(id); |
| |
| let assigned_addr = finterfaces_ext::wait_interface_with_id( |
| finterfaces_ext::event_stream_from_state( |
| &interfaces_state, |
| finterfaces_ext::IncludedAddresses::OnlyAssigned, |
| )?, |
| &mut state, |
| |finterfaces_ext::PropertiesAndState { properties, state: _ }| { |
| let finterfaces_ext::Properties { addresses, .. } = properties; |
| let addr = if ipv6 { |
| addresses.iter().find_map( |
| |finterfaces_ext::Address { |
| addr: fnet::Subnet { addr, .. }, |
| .. |
| }| { |
| match addr { |
| fnet::IpAddress::Ipv4(_) => None, |
| fnet::IpAddress::Ipv6(_) => Some(addr), |
| } |
| }, |
| ) |
| } else { |
| addresses.first().map( |
| |finterfaces_ext::Address { |
| addr: fnet::Subnet { addr, .. }, |
| .. |
| }| addr, |
| ) |
| }; |
| addr.map(|addr| { |
| let fnet_ext::IpAddress(addr) = (*addr).into(); |
| addr |
| }) |
| }, |
| ) |
| .await |
| .context("wait for assigned address")?; |
| |
| out.line(format!("{assigned_addr}"))?; |
| info!("Address {} assigned on interface {}", assigned_addr, id); |
| } |
| }, |
| opts::IfEnum::Bridge(opts::IfBridge { interfaces }) => { |
| let stack = connect_with_context::<fstack::StackMarker, _>(connector).await?; |
| let build_name_to_id_map = || async { |
| let interface_state = |
| connect_with_context::<finterfaces::StateMarker, _>(connector).await?; |
| let stream = finterfaces_ext::event_stream_from_state( |
| &interface_state, |
| finterfaces_ext::IncludedAddresses::OnlyAssigned, |
| )?; |
| let response = finterfaces_ext::existing(stream, HashMap::new()).await?; |
| Ok::<HashMap<String, u64>, Error>( |
| response |
| .into_iter() |
| .map( |
| |( |
| id, |
| finterfaces_ext::PropertiesAndState { |
| properties: |
| finterfaces_ext::Properties { |
| name, |
| id: _, |
| device_class: _, |
| online: _, |
| addresses: _, |
| has_default_ipv4_route: _, |
| has_default_ipv6_route: _, |
| }, |
| state: (), |
| }, |
| )| (name, id), |
| ) |
| .collect(), |
| ) |
| }; |
| |
| let num_interfaces = interfaces.len(); |
| |
| let (_name_to_id, ids): (Option<HashMap<String, u64>>, Vec<u64>) = |
| futures::stream::iter(interfaces) |
| .map(Ok::<_, Error>) |
| .try_fold( |
| (None, Vec::with_capacity(num_interfaces)), |
| |(name_to_id, mut ids), interface| async move { |
| let (name_to_id, id) = match interface { |
| opts::InterfaceIdentifier::Id(id) => (name_to_id, id), |
| opts::InterfaceIdentifier::Name(name) => { |
| let name_to_id = match name_to_id { |
| Some(name_to_id) => name_to_id, |
| None => build_name_to_id_map().await?, |
| }; |
| let id = name_to_id.get(&name).copied().ok_or_else(|| { |
| user_facing_error(format!("no interface named {}", name)) |
| })?; |
| (Some(name_to_id), id) |
| } |
| }; |
| ids.push(id); |
| Ok((name_to_id, ids)) |
| }, |
| ) |
| .await?; |
| |
| let (bridge, server_end) = fidl::endpoints::create_proxy().context("create proxy")?; |
| stack.bridge_interfaces(&ids, server_end).context("bridge interfaces")?; |
| let bridge_id = bridge.get_id().await.context("get bridge id")?; |
| // Detach the channel so it won't cause bridge destruction on exit. |
| bridge.detach().context("detach bridge")?; |
| info!("network bridge created with id {}", bridge_id); |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_route<C: NetCliDepsConnector>( |
| out: &mut ffx_writer::Writer, |
| cmd: opts::RouteEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| match cmd { |
| opts::RouteEnum::List(opts::RouteList {}) => do_route_list(out, connector).await?, |
| opts::RouteEnum::Add(route) => { |
| let stack = connect_with_context::<fstack::StackMarker, _>(connector).await?; |
| let nicid = route.interface.find_u32_nicid(connector).await?; |
| let entry = route.into_route_table_entry(nicid); |
| let () = fstack_ext::exec_fidl!( |
| stack.add_forwarding_entry(&entry), |
| "error adding next-hop forwarding entry" |
| )?; |
| } |
| opts::RouteEnum::Del(route) => { |
| let stack = connect_with_context::<fstack::StackMarker, _>(connector).await?; |
| let nicid = route.interface.find_u32_nicid(connector).await?; |
| let entry = route.into_route_table_entry(nicid); |
| let () = fstack_ext::exec_fidl!( |
| stack.del_forwarding_entry(&entry), |
| "error removing forwarding entry" |
| )?; |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_route_list<C: NetCliDepsConnector>( |
| out: &mut ffx_writer::Writer, |
| connector: &C, |
| ) -> Result<(), Error> { |
| let ipv4_route_event_stream = pin!({ |
| let state_v4 = connect_with_context::<froutes::StateV4Marker, _>(connector) |
| .await |
| .context("failed to connect to fuchsia.net.routes/StateV4")?; |
| froutes_ext::event_stream_from_state::<Ipv4>(&state_v4) |
| .context("failed to initialize a `WatcherV4` client")? |
| .fuse() |
| }); |
| let ipv6_route_event_stream = pin!({ |
| let state_v6 = connect_with_context::<froutes::StateV6Marker, _>(connector) |
| .await |
| .context("failed to connect to fuchsia.net.routes/StateV6")?; |
| froutes_ext::event_stream_from_state::<Ipv6>(&state_v6) |
| .context("failed to initialize a `WatcherV6` client")? |
| .fuse() |
| }); |
| let (v4_routes, v6_routes) = futures::future::join( |
| froutes_ext::collect_routes_until_idle::<_, Vec<_>>(ipv4_route_event_stream), |
| froutes_ext::collect_routes_until_idle::<_, Vec<_>>(ipv6_route_event_stream), |
| ) |
| .await; |
| let mut v4_routes = v4_routes.context("failed to collect all existing IPv4 routes")?; |
| let mut v6_routes = v6_routes.context("failed to collect all existing IPv6 routes")?; |
| if out.is_machine() { |
| fn to_ser<I: net_types::ip::Ip>( |
| route: froutes_ext::InstalledRoute<I>, |
| ) -> Option<ser::ForwardingEntry> { |
| route.try_into().map_err(|e| warn!("failed to convert route: {:?}", e)).ok() |
| } |
| let routes = v4_routes |
| .into_iter() |
| .filter_map(to_ser) |
| .chain(v6_routes.into_iter().filter_map(to_ser)) |
| .collect::<Vec<_>>(); |
| out.machine(&routes).context("serialize")?; |
| } else { |
| let mut t = Table::new(); |
| t.set_format(format::FormatBuilder::new().padding(2, 2).build()); |
| |
| t.set_titles(row!["Destination", "Gateway", "NICID", "Metric"]); |
| fn write_route<I: net_types::ip::Ip>(t: &mut Table, route: froutes_ext::InstalledRoute<I>) { |
| let froutes_ext::InstalledRoute { |
| route: froutes_ext::Route { destination, action, properties: _ }, |
| effective_properties: froutes_ext::EffectiveRouteProperties { metric }, |
| } = route; |
| let (device_id, next_hop) = match action { |
| froutes_ext::RouteAction::Forward(froutes_ext::RouteTarget { |
| outbound_interface, |
| next_hop, |
| }) => (outbound_interface, next_hop), |
| froutes_ext::RouteAction::Unknown => { |
| warn!("observed route with unknown RouteAction."); |
| return; |
| } |
| }; |
| let next_hop = next_hop.map(|next_hop| next_hop.to_string()); |
| let next_hop = next_hop.as_ref().map_or("-", |s| s.as_str()); |
| let () = add_row(t, row![destination, next_hop, device_id, metric]); |
| } |
| |
| v4_routes.sort(); |
| for route in v4_routes { |
| write_route(&mut t, route); |
| } |
| v6_routes.sort(); |
| for route in v6_routes { |
| write_route(&mut t, route); |
| } |
| |
| let _lines_printed: usize = t.print(out)?; |
| out.line("")?; |
| } |
| Ok(()) |
| } |
| |
| async fn do_filter_deprecated<C: NetCliDepsConnector, W: std::io::Write>( |
| mut out: W, |
| cmd: opts::FilterDeprecatedEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| let filter = connect_with_context::<ffilter_deprecated::FilterMarker, _>(connector).await?; |
| match cmd { |
| opts::FilterDeprecatedEnum::GetRules(opts::FilterGetRules {}) => { |
| let (rules, generation): (Vec<ffilter_deprecated::Rule>, u32) = |
| filter.get_rules().await?; |
| writeln!(out, "{:?} (generation {})", rules, generation)?; |
| } |
| opts::FilterDeprecatedEnum::SetRules(opts::FilterSetRules { rules }) => { |
| let (_cur_rules, generation) = filter.get_rules().await?; |
| let rules = netfilter::parser_deprecated::parse_str_to_rules(&rules)?; |
| let () = filter_fidl!( |
| filter.update_rules(&rules, generation), |
| "error setting filter rules" |
| )?; |
| info!("successfully set filter rules"); |
| } |
| opts::FilterDeprecatedEnum::GetNatRules(opts::FilterGetNatRules {}) => { |
| let (rules, generation): (Vec<ffilter_deprecated::Nat>, u32) = |
| filter.get_nat_rules().await?; |
| writeln!(out, "{:?} (generation {})", rules, generation)?; |
| } |
| opts::FilterDeprecatedEnum::SetNatRules(opts::FilterSetNatRules { rules }) => { |
| let (_cur_rules, generation) = filter.get_nat_rules().await?; |
| let rules = netfilter::parser_deprecated::parse_str_to_nat_rules(&rules)?; |
| let () = filter_fidl!( |
| filter.update_nat_rules(&rules, generation), |
| "error setting NAT rules" |
| )?; |
| info!("successfully set NAT rules"); |
| } |
| opts::FilterDeprecatedEnum::GetRdrRules(opts::FilterGetRdrRules {}) => { |
| let (rules, generation): (Vec<ffilter_deprecated::Rdr>, u32) = |
| filter.get_rdr_rules().await?; |
| writeln!(out, "{:?} (generation {})", rules, generation)?; |
| } |
| opts::FilterDeprecatedEnum::SetRdrRules(opts::FilterSetRdrRules { rules }) => { |
| let (_cur_rules, generation) = filter.get_rdr_rules().await?; |
| let rules = netfilter::parser_deprecated::parse_str_to_rdr_rules(&rules)?; |
| let () = filter_fidl!( |
| filter.update_rdr_rules(&rules, generation), |
| "error setting RDR rules" |
| )?; |
| info!("successfully set RDR rules"); |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_filter<C: NetCliDepsConnector, W: std::io::Write>( |
| mut out: W, |
| cmd: opts::filter::FilterEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| match cmd { |
| opts::filter::FilterEnum::List(opts::filter::List {}) => { |
| let state = connect_with_context::<fnet_filter::StateMarker, _>(connector).await?; |
| let stream = fnet_filter_ext::event_stream_from_state(state)?; |
| let mut stream = pin!(stream); |
| let resources: FilteringResources = |
| fnet_filter_ext::get_existing_resources(&mut stream).await?; |
| |
| for controller_id in resources.controllers() { |
| writeln!(out, "controller: \"{}\" {{", controller_id.0)?; |
| |
| for namespace @ Namespace { id, domain, .. } in |
| resources.namespaces(controller_id).unwrap() |
| { |
| writeln!(out, " namespace: \"{}\" {{", id.0)?; |
| writeln!(out, " domain: {:?}", domain)?; |
| |
| for routine in namespace.routines() { |
| writeln!(out, " routine: \"{}\" {{", routine.id.name)?; |
| // TODO(https://fxbug.dev/329686169): Improve Routine readability. |
| writeln!(out, " type: {:?}", routine.routine_type)?; |
| |
| for rule in routine.rules() { |
| writeln!(out, " rule: #{} {{", rule.id.index)?; |
| // TODO(https://fxbug.dev/329686745): Improve Matchers and Action |
| // readability. |
| writeln!(out, " matchers: {:?}", rule.matchers)?; |
| writeln!(out, " action: {:?}", rule.action)?; |
| writeln!(out, " }}")?; |
| } |
| |
| writeln!(out, " }}")?; |
| } |
| |
| writeln!(out, " }}")?; |
| } |
| |
| writeln!(out, "}}")?; |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_log<C: NetCliDepsConnector>(cmd: opts::LogEnum, connector: &C) -> Result<(), Error> { |
| let log = connect_with_context::<fstack::LogMarker, _>(connector).await?; |
| match cmd { |
| opts::LogEnum::SetPackets(opts::LogSetPackets { enabled }) => { |
| let () = log.set_log_packets(enabled).await.context("error setting log packets")?; |
| info!("log packets set to {:?}", enabled); |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_dhcp<C: NetCliDepsConnector>(cmd: opts::DhcpEnum, connector: &C) -> Result<(), Error> { |
| let stack = connect_with_context::<fstack::StackMarker, _>(connector).await?; |
| match cmd { |
| opts::DhcpEnum::Start(opts::DhcpStart { interface }) => { |
| let id = interface.find_nicid(connector).await?; |
| let () = fstack_ext::exec_fidl!( |
| stack.set_dhcp_client_enabled(id, true), |
| "error stopping DHCP client" |
| )?; |
| info!("dhcp client started on interface {}", id); |
| } |
| opts::DhcpEnum::Stop(opts::DhcpStop { interface }) => { |
| let id = interface.find_nicid(connector).await?; |
| let () = fstack_ext::exec_fidl!( |
| stack.set_dhcp_client_enabled(id, false), |
| "error stopping DHCP client" |
| )?; |
| info!("dhcp client stopped on interface {}", id); |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_dhcpd<C: NetCliDepsConnector>( |
| cmd: opts::dhcpd::DhcpdEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| let dhcpd_server = connect_with_context::<fdhcp::Server_Marker, _>(connector).await?; |
| match cmd { |
| opts::dhcpd::DhcpdEnum::Start(opts::dhcpd::Start {}) => { |
| Ok(do_dhcpd_start(dhcpd_server).await?) |
| } |
| opts::dhcpd::DhcpdEnum::Stop(opts::dhcpd::Stop {}) => { |
| Ok(do_dhcpd_stop(dhcpd_server).await?) |
| } |
| opts::dhcpd::DhcpdEnum::Get(get_arg) => Ok(do_dhcpd_get(get_arg, dhcpd_server).await?), |
| opts::dhcpd::DhcpdEnum::Set(set_arg) => Ok(do_dhcpd_set(set_arg, dhcpd_server).await?), |
| opts::dhcpd::DhcpdEnum::List(list_arg) => Ok(do_dhcpd_list(list_arg, dhcpd_server).await?), |
| opts::dhcpd::DhcpdEnum::Reset(reset_arg) => { |
| Ok(do_dhcpd_reset(reset_arg, dhcpd_server).await?) |
| } |
| opts::dhcpd::DhcpdEnum::ClearLeases(opts::dhcpd::ClearLeases {}) => { |
| Ok(do_dhcpd_clear_leases(dhcpd_server).await?) |
| } |
| } |
| } |
| |
| async fn do_neigh<C: NetCliDepsConnector>( |
| out: ffx_writer::Writer, |
| cmd: opts::NeighEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| match cmd { |
| opts::NeighEnum::Add(opts::NeighAdd { interface, ip, mac }) => { |
| let interface = interface.find_nicid(connector).await?; |
| let controller = |
| connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?; |
| let () = do_neigh_add(interface, ip.into(), mac.into(), controller) |
| .await |
| .context("failed during neigh add command")?; |
| info!("Added entry ({}, {}) for interface {}", ip, mac, interface); |
| } |
| opts::NeighEnum::Clear(opts::NeighClear { interface, ip_version }) => { |
| let interface = interface.find_nicid(connector).await?; |
| let controller = |
| connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?; |
| let () = do_neigh_clear(interface, ip_version, controller) |
| .await |
| .context("failed during neigh clear command")?; |
| info!("Cleared entries for interface {}", interface); |
| } |
| opts::NeighEnum::Del(opts::NeighDel { interface, ip }) => { |
| let interface = interface.find_nicid(connector).await?; |
| let controller = |
| connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?; |
| let () = do_neigh_del(interface, ip.into(), controller) |
| .await |
| .context("failed during neigh del command")?; |
| info!("Deleted entry {} for interface {}", ip, interface); |
| } |
| opts::NeighEnum::List(opts::NeighList {}) => { |
| let view = connect_with_context::<fneighbor::ViewMarker, _>(connector).await?; |
| let () = print_neigh_entries(out, false /* watch_for_changes */, view) |
| .await |
| .context("error listing neighbor entries")?; |
| } |
| opts::NeighEnum::Watch(opts::NeighWatch {}) => { |
| let view = connect_with_context::<fneighbor::ViewMarker, _>(connector).await?; |
| let () = print_neigh_entries(out, true /* watch_for_changes */, view) |
| .await |
| .context("error watching for changes to the neighbor table")?; |
| } |
| opts::NeighEnum::Config(opts::NeighConfig { neigh_config_cmd }) => match neigh_config_cmd { |
| opts::NeighConfigEnum::Get(opts::NeighGetConfig { interface, ip_version }) => { |
| let interface = interface.find_nicid(connector).await?; |
| let control = get_control(connector, interface).await.context("get control")?; |
| let configuration = control |
| .get_configuration() |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlGetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("get configuration")?; |
| let nud = extract_nud_config(configuration, ip_version)?; |
| println!("{:#?}", nud); |
| } |
| opts::NeighConfigEnum::Update(opts::NeighUpdateConfig { |
| interface, |
| ip_version, |
| base_reachable_time, |
| }) => { |
| let interface = interface.find_nicid(connector).await?; |
| let control = get_control(connector, interface).await.context("get control")?; |
| let nud_config = finterfaces_admin::NudConfiguration { |
| base_reachable_time, |
| ..Default::default() |
| }; |
| let config = match ip_version { |
| fnet::IpVersion::V4 => finterfaces_admin::Configuration { |
| ipv4: Some(finterfaces_admin::Ipv4Configuration { |
| arp: Some(finterfaces_admin::ArpConfiguration { |
| nud: Some(nud_config), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| fnet::IpVersion::V6 => finterfaces_admin::Configuration { |
| ipv6: Some(finterfaces_admin::Ipv6Configuration { |
| ndp: Some(finterfaces_admin::NdpConfiguration { |
| nud: Some(nud_config), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| }; |
| let prev_config = control |
| .set_configuration(&config) |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|res| { |
| res.map_err(|e: finterfaces_admin::ControlSetConfigurationError| { |
| anyhow::anyhow!("{:?}", e) |
| }) |
| }) |
| .context("set configuration")?; |
| let prev_nud = extract_nud_config(prev_config, ip_version)?; |
| info!("Updated config for interface {}; previously was: {:?}", interface, prev_nud); |
| } |
| }, |
| } |
| Ok(()) |
| } |
| |
| async fn do_neigh_add( |
| interface: u64, |
| neighbor: fnet::IpAddress, |
| mac: fnet::MacAddress, |
| controller: fneighbor::ControllerProxy, |
| ) -> Result<(), Error> { |
| controller |
| .add_entry(interface, &neighbor.into(), &mac.into()) |
| .await |
| .context("FIDL error adding neighbor entry")? |
| .map_err(zx::Status::from_raw) |
| .context("error adding neighbor entry") |
| } |
| |
| async fn do_neigh_clear( |
| interface: u64, |
| ip_version: fnet::IpVersion, |
| controller: fneighbor::ControllerProxy, |
| ) -> Result<(), Error> { |
| controller |
| .clear_entries(interface, ip_version) |
| .await |
| .context("FIDL error clearing neighbor table")? |
| .map_err(zx::Status::from_raw) |
| .context("error clearing neighbor table") |
| } |
| |
| async fn do_neigh_del( |
| interface: u64, |
| neighbor: fnet::IpAddress, |
| controller: fneighbor::ControllerProxy, |
| ) -> Result<(), Error> { |
| controller |
| .remove_entry(interface, &neighbor.into()) |
| .await |
| .context("FIDL error removing neighbor entry")? |
| .map_err(zx::Status::from_raw) |
| .context("error removing neighbor entry") |
| } |
| |
| fn unpack_neigh_iter_item( |
| item: fneighbor::EntryIteratorItem, |
| ) -> Result<(&'static str, Option<fneighbor_ext::Entry>), Error> { |
| let displayed_state_change_status = ser::DISPLAYED_NEIGH_ENTRY_VARIANTS.select(&item); |
| |
| Ok(( |
| displayed_state_change_status, |
| match item { |
| fneighbor::EntryIteratorItem::Existing(entry) |
| | fneighbor::EntryIteratorItem::Added(entry) |
| | fneighbor::EntryIteratorItem::Changed(entry) |
| | fneighbor::EntryIteratorItem::Removed(entry) => { |
| Some(fneighbor_ext::Entry::try_from(entry)?) |
| } |
| fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent) => None, |
| }, |
| )) |
| } |
| |
| fn jsonify_neigh_iter_item( |
| item: fneighbor::EntryIteratorItem, |
| include_entry_state: bool, |
| ) -> Result<Value, Error> { |
| let (state_change_status, entry) = unpack_neigh_iter_item(item)?; |
| let entry_json = entry |
| .map(ser::NeighborTableEntry::from) |
| .map(serde_json::to_value) |
| .map(|res| res.map_err(Error::new)) |
| .unwrap_or(Err(anyhow::anyhow!("failed to jsonify NeighborTableEntry")))?; |
| if include_entry_state { |
| Ok(json!({ |
| "state_change_status": state_change_status, |
| "entry": entry_json, |
| })) |
| } else { |
| Ok(entry_json) |
| } |
| } |
| |
| async fn print_neigh_entries( |
| mut out: ffx_writer::Writer, |
| watch_for_changes: bool, |
| view: fneighbor::ViewProxy, |
| ) -> Result<(), Error> { |
| let (it_client, it_server) = |
| fidl::endpoints::create_endpoints::<fneighbor::EntryIteratorMarker>(); |
| let it = it_client.into_proxy().context("error creating proxy to entry iterator")?; |
| |
| let () = view |
| .open_entry_iterator(it_server, &fneighbor::EntryIteratorOptions::default()) |
| .context("error opening a connection to the entry iterator")?; |
| |
| let out_ref = &mut out; |
| if watch_for_changes { |
| neigh_entry_stream(it, watch_for_changes) |
| .map_ok(|item| { |
| write_neigh_entry(out_ref, item, /* include_entry_state= */ watch_for_changes) |
| .context("error writing entry") |
| }) |
| .try_fold((), |(), r| futures::future::ready(r)) |
| .await?; |
| } else { |
| let results: Vec<Result<fneighbor::EntryIteratorItem, _>> = |
| neigh_entry_stream(it, watch_for_changes).collect().await; |
| if out.is_machine() { |
| let jsonified_items: Value = |
| itertools::process_results(results.into_iter(), |items| { |
| itertools::process_results( |
| items.map(|item| { |
| jsonify_neigh_iter_item( |
| item, |
| /* include_entry_state= */ watch_for_changes, |
| ) |
| }), |
| |json_values| Value::from_iter(json_values), |
| ) |
| })??; |
| out.machine(&jsonified_items)?; |
| } else { |
| itertools::process_results(results.into_iter(), |mut items| { |
| items.try_for_each(|item| { |
| write_tabular_neigh_entry( |
| &mut out, |
| item, |
| /* include_entry_state= */ watch_for_changes, |
| ) |
| }) |
| })??; |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| fn neigh_entry_stream( |
| iterator: fneighbor::EntryIteratorProxy, |
| watch_for_changes: bool, |
| ) -> impl futures::Stream<Item = Result<fneighbor::EntryIteratorItem, Error>> { |
| futures::stream::try_unfold(iterator, |iterator| { |
| iterator |
| .get_next() |
| .map_ok(|items| Some((items, iterator))) |
| .map(|r| r.context("error getting items from iterator")) |
| }) |
| .map_ok(|items| futures::stream::iter(items.into_iter().map(Ok))) |
| .try_flatten() |
| .take_while(move |item| { |
| futures::future::ready(item.as_ref().is_ok_and(|item| { |
| if let fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {}) = item { |
| watch_for_changes |
| } else { |
| true |
| } |
| })) |
| }) |
| } |
| |
| fn write_tabular_neigh_entry<W: std::io::Write>( |
| mut f: W, |
| item: fneighbor::EntryIteratorItem, |
| include_entry_state: bool, |
| ) -> Result<(), Error> { |
| let (state_change_status, entry) = unpack_neigh_iter_item(item)?; |
| match entry { |
| Some(entry) => { |
| if include_entry_state { |
| writeln!( |
| &mut f, |
| "{:width$} | {}", |
| state_change_status, |
| entry, |
| width = ser::DISPLAYED_NEIGH_ENTRY_VARIANTS |
| .into_iter() |
| .map(|s| s.len()) |
| .max() |
| .unwrap_or(0), |
| )? |
| } else { |
| writeln!(&mut f, "{}", entry)? |
| } |
| } |
| None => writeln!(&mut f, "{}", state_change_status)?, |
| } |
| Ok(()) |
| } |
| |
| fn write_neigh_entry( |
| f: &mut ffx_writer::Writer, |
| item: fneighbor::EntryIteratorItem, |
| include_entry_state: bool, |
| ) -> Result<(), Error> { |
| if f.is_machine() { |
| let entry = jsonify_neigh_iter_item(item, include_entry_state)?; |
| f.machine(&entry)?; |
| } else { |
| write_tabular_neigh_entry(f, item, include_entry_state)? |
| } |
| Ok(()) |
| } |
| |
| async fn do_dhcpd_start(server: fdhcp::Server_Proxy) -> Result<(), Error> { |
| server.start_serving().await?.map_err(zx::Status::from_raw).context("failed to start server") |
| } |
| |
| async fn do_dhcpd_stop(server: fdhcp::Server_Proxy) -> Result<(), Error> { |
| server.stop_serving().await.context("failed to stop server") |
| } |
| |
| async fn do_dhcpd_get(get_arg: opts::dhcpd::Get, server: fdhcp::Server_Proxy) -> Result<(), Error> { |
| match get_arg.arg { |
| opts::dhcpd::GetArg::Option(opts::dhcpd::OptionArg { name }) => { |
| let res = server |
| .get_option(name.clone().into()) |
| .await? |
| .map_err(zx::Status::from_raw) |
| .with_context(|| format!("get_option({:?}) failed", name))?; |
| println!("{:#?}", res); |
| } |
| opts::dhcpd::GetArg::Parameter(opts::dhcpd::ParameterArg { name }) => { |
| let res = server |
| .get_parameter(name.clone().into()) |
| .await? |
| .map_err(zx::Status::from_raw) |
| .with_context(|| format!("get_parameter({:?}) failed", name))?; |
| println!("{:#?}", res); |
| } |
| }; |
| Ok(()) |
| } |
| |
| async fn do_dhcpd_set(set_arg: opts::dhcpd::Set, server: fdhcp::Server_Proxy) -> Result<(), Error> { |
| match set_arg.arg { |
| opts::dhcpd::SetArg::Option(opts::dhcpd::OptionArg { name }) => { |
| let () = server |
| .set_option(&name.clone().into()) |
| .await? |
| .map_err(zx::Status::from_raw) |
| .with_context(|| format!("set_option({:?}) failed", name))?; |
| } |
| opts::dhcpd::SetArg::Parameter(opts::dhcpd::ParameterArg { name }) => { |
| let () = server |
| .set_parameter(&name.clone().into()) |
| .await? |
| .map_err(zx::Status::from_raw) |
| .with_context(|| format!("set_parameter({:?}) failed", name))?; |
| } |
| }; |
| Ok(()) |
| } |
| |
| async fn do_dhcpd_list( |
| list_arg: opts::dhcpd::List, |
| server: fdhcp::Server_Proxy, |
| ) -> Result<(), Error> { |
| match list_arg.arg { |
| opts::dhcpd::ListArg::Option(opts::dhcpd::OptionToken {}) => { |
| let res = server |
| .list_options() |
| .await? |
| .map_err(zx::Status::from_raw) |
| .context("list_options() failed")?; |
| |
| println!("{:#?}", res); |
| } |
| opts::dhcpd::ListArg::Parameter(opts::dhcpd::ParameterToken {}) => { |
| let res = server |
| .list_parameters() |
| .await? |
| .map_err(zx::Status::from_raw) |
| .context("list_parameters() failed")?; |
| println!("{:#?}", res); |
| } |
| }; |
| Ok(()) |
| } |
| |
| async fn do_dhcpd_reset( |
| reset_arg: opts::dhcpd::Reset, |
| server: fdhcp::Server_Proxy, |
| ) -> Result<(), Error> { |
| match reset_arg.arg { |
| opts::dhcpd::ResetArg::Option(opts::dhcpd::OptionToken {}) => { |
| let () = server |
| .reset_options() |
| .await? |
| .map_err(zx::Status::from_raw) |
| .context("reset_options() failed")?; |
| } |
| opts::dhcpd::ResetArg::Parameter(opts::dhcpd::ParameterToken {}) => { |
| let () = server |
| .reset_parameters() |
| .await? |
| .map_err(zx::Status::from_raw) |
| .context("reset_parameters() failed")?; |
| } |
| }; |
| Ok(()) |
| } |
| |
| async fn do_dhcpd_clear_leases(server: fdhcp::Server_Proxy) -> Result<(), Error> { |
| server.clear_leases().await?.map_err(zx::Status::from_raw).context("clear_leases() failed") |
| } |
| |
| async fn do_dns<W: std::io::Write, C: NetCliDepsConnector>( |
| mut out: W, |
| cmd: opts::dns::DnsEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| let lookup = connect_with_context::<fname::LookupMarker, _>(connector).await?; |
| let opts::dns::DnsEnum::Lookup(opts::dns::Lookup { hostname, ipv4, ipv6, sort }) = cmd; |
| let result = lookup |
| .lookup_ip( |
| &hostname, |
| &fname::LookupIpOptions { |
| ipv4_lookup: Some(ipv4), |
| ipv6_lookup: Some(ipv6), |
| sort_addresses: Some(sort), |
| ..Default::default() |
| }, |
| ) |
| .await? |
| .map_err(|e| anyhow::anyhow!("DNS lookup failed: {:?}", e))?; |
| let fname::LookupResult { addresses, .. } = result; |
| let addrs = addresses.context("`addresses` not set in response from DNS resolver")?; |
| for addr in addrs { |
| writeln!(out, "{}", fnet_ext::IpAddress::from(addr))?; |
| } |
| Ok(()) |
| } |
| |
| async fn do_netstack_migration<W: std::io::Write, C: NetCliDepsConnector>( |
| mut out: W, |
| cmd: opts::NetstackMigrationEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| match cmd { |
| opts::NetstackMigrationEnum::Set(opts::NetstackMigrationSet { version }) => { |
| let control = |
| connect_with_context::<fnet_migration::ControlMarker, _>(connector).await?; |
| control |
| .set_user_netstack_version(Some(&fnet_migration::VersionSetting { version })) |
| .await |
| .context("failed to set stack version") |
| } |
| opts::NetstackMigrationEnum::Clear(opts::NetstackMigrationClear {}) => { |
| let control = |
| connect_with_context::<fnet_migration::ControlMarker, _>(connector).await?; |
| control.set_user_netstack_version(None).await.context("failed to set stack version") |
| } |
| opts::NetstackMigrationEnum::Get(opts::NetstackMigrationGet {}) => { |
| let state = connect_with_context::<fnet_migration::StateMarker, _>(connector).await?; |
| let fnet_migration::InEffectVersion { current_boot, user, automated, .. } = |
| state.get_netstack_version().await.context("failed to get stack version")?; |
| writeln!(out, "current_boot = {current_boot:?}")?; |
| writeln!(out, "user = {user:?}")?; |
| writeln!(out, "automated = {automated:?}")?; |
| Ok(()) |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use assert_matches::assert_matches; |
| use fidl::endpoints::ProtocolMarker; |
| use fidl_fuchsia_hardware_network as fhardware_network; |
| use fidl_fuchsia_net_routes as froutes; |
| use fidl_fuchsia_net_routes_ext as froutes_ext; |
| use fnet_filter_ext::InstalledIpRoutine; |
| use fuchsia_async::{self as fasync, TimeoutExt as _}; |
| use net_declare::{fidl_ip, fidl_ip_v4, fidl_mac, fidl_subnet}; |
| use std::{convert::TryInto as _, fmt::Debug}; |
| use test_case::test_case; |
| |
| const IF_ADDR_V4: fnet::Subnet = fidl_subnet!("192.168.0.1/32"); |
| const IF_ADDR_V6: fnet::Subnet = fidl_subnet!("fd00::1/64"); |
| |
| const MAC_1: fnet::MacAddress = fidl_mac!("01:02:03:04:05:06"); |
| const MAC_2: fnet::MacAddress = fidl_mac!("02:03:04:05:06:07"); |
| |
| #[derive(Default)] |
| struct TestConnector { |
| debug_interfaces: Option<fdebug::InterfacesProxy>, |
| dhcpd: Option<fdhcp::Server_Proxy>, |
| interfaces_state: Option<finterfaces::StateProxy>, |
| stack: Option<fstack::StackProxy>, |
| root_interfaces: Option<froot::InterfacesProxy>, |
| routes_v4: Option<froutes::StateV4Proxy>, |
| routes_v6: Option<froutes::StateV6Proxy>, |
| name_lookup: Option<fname::LookupProxy>, |
| filter: Option<fnet_filter::StateProxy>, |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fdebug::InterfacesMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<fdebug::InterfacesMarker as ProtocolMarker>::Proxy, Error> { |
| self.debug_interfaces |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no dhcp server instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<froot::InterfacesMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<froot::InterfacesMarker as ProtocolMarker>::Proxy, Error> { |
| self.root_interfaces |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no root interfaces instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fdhcp::Server_Marker> for TestConnector { |
| async fn connect(&self) -> Result<<fdhcp::Server_Marker as ProtocolMarker>::Proxy, Error> { |
| self.dhcpd |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no dhcp server instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<ffilter_deprecated::FilterMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<ffilter_deprecated::FilterMarker as ProtocolMarker>::Proxy, Error> { |
| Err(anyhow::anyhow!("connect filter_deprecated unimplemented for test connector")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<finterfaces::StateMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<finterfaces::StateMarker as ProtocolMarker>::Proxy, Error> { |
| self.interfaces_state |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no interfaces state instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fneighbor::ControllerMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<fneighbor::ControllerMarker as ProtocolMarker>::Proxy, Error> { |
| Err(anyhow::anyhow!("connect neighbor controller unimplemented for test connector")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fneighbor::ViewMarker> for TestConnector { |
| async fn connect(&self) -> Result<<fneighbor::ViewMarker as ProtocolMarker>::Proxy, Error> { |
| Err(anyhow::anyhow!("connect neighbor view unimplemented for test connector")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fstack::LogMarker> for TestConnector { |
| async fn connect(&self) -> Result<<fstack::LogMarker as ProtocolMarker>::Proxy, Error> { |
| Err(anyhow::anyhow!("connect log unimplemented for test connector")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fstack::StackMarker> for TestConnector { |
| async fn connect(&self) -> Result<<fstack::StackMarker as ProtocolMarker>::Proxy, Error> { |
| self.stack.as_ref().cloned().ok_or(anyhow::anyhow!("connector has no stack instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<froutes::StateV4Marker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<froutes::StateV4Marker as ProtocolMarker>::Proxy, Error> { |
| self.routes_v4 |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no routes_v4 instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<froutes::StateV6Marker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<froutes::StateV6Marker as ProtocolMarker>::Proxy, Error> { |
| self.routes_v6 |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no routes_v6 instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fname::LookupMarker> for TestConnector { |
| async fn connect(&self) -> Result<<fname::LookupMarker as ProtocolMarker>::Proxy, Error> { |
| self.name_lookup |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no name lookup instance")) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fnet_migration::ControlMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<fnet_migration::ControlMarker as ProtocolMarker>::Proxy, Error> { |
| unimplemented!("stack migration not supported"); |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fnet_migration::StateMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<fnet_migration::StateMarker as ProtocolMarker>::Proxy, Error> { |
| unimplemented!("stack migration not supported"); |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl ServiceConnector<fnet_filter::StateMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<fnet_filter::StateMarker as ProtocolMarker>::Proxy, Error> { |
| self.filter.as_ref().cloned().ok_or(anyhow::anyhow!("connector has no filter instance")) |
| } |
| } |
| |
| fn trim_whitespace_for_comparison(s: &str) -> String { |
| s.trim().lines().map(|s| s.trim()).collect::<Vec<&str>>().join("\n") |
| } |
| |
| fn get_fake_interface( |
| id: u64, |
| name: &'static str, |
| device_class: finterfaces::DeviceClass, |
| octets: Option<[u8; 6]>, |
| ) -> (finterfaces_ext::Properties, Option<fnet::MacAddress>) { |
| ( |
| finterfaces_ext::Properties { |
| id: id.try_into().unwrap(), |
| name: name.to_string(), |
| device_class, |
| online: true, |
| addresses: Vec::new(), |
| has_default_ipv4_route: false, |
| has_default_ipv6_route: false, |
| }, |
| octets.map(|octets| fnet::MacAddress { octets }), |
| ) |
| } |
| |
| fn shortlist_interfaces_by_nicid(name_pattern: &str) -> Vec<u64> { |
| let mut interfaces = [ |
| get_fake_interface( |
| 1, |
| "lo", |
| finterfaces::DeviceClass::Loopback(finterfaces::Empty), |
| None, |
| ), |
| get_fake_interface( |
| 10, |
| "eth001", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Ethernet), |
| Some([1, 2, 3, 4, 5, 6]), |
| ), |
| get_fake_interface( |
| 20, |
| "eth002", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Ethernet), |
| Some([1, 2, 3, 4, 5, 7]), |
| ), |
| get_fake_interface( |
| 30, |
| "eth003", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Ethernet), |
| Some([1, 2, 3, 4, 5, 8]), |
| ), |
| get_fake_interface( |
| 100, |
| "wlan001", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Wlan), |
| Some([2, 2, 3, 4, 5, 6]), |
| ), |
| get_fake_interface( |
| 200, |
| "wlan002", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Wlan), |
| Some([2, 2, 3, 4, 5, 7]), |
| ), |
| get_fake_interface( |
| 300, |
| "wlan003", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Wlan), |
| Some([2, 2, 3, 4, 5, 8]), |
| ), |
| ] |
| .into_iter() |
| .map(|(properties, _): (_, Option<fnet::MacAddress>)| { |
| let finterfaces_ext::Properties { id, .. } = &properties; |
| (id.get(), finterfaces_ext::PropertiesAndState { properties, state: () }) |
| }) |
| .collect(); |
| let () = shortlist_interfaces(name_pattern, &mut interfaces); |
| let mut interfaces: Vec<_> = interfaces.into_keys().collect(); |
| let () = interfaces.sort(); |
| interfaces |
| } |
| |
| #[test] |
| fn test_shortlist_interfaces() { |
| assert_eq!(vec![1, 10, 20, 30, 100, 200, 300], shortlist_interfaces_by_nicid("")); |
| assert_eq!(vec![0_u64; 0], shortlist_interfaces_by_nicid("no such thing")); |
| |
| assert_eq!(vec![1], shortlist_interfaces_by_nicid("lo")); |
| assert_eq!(vec![10, 20, 30], shortlist_interfaces_by_nicid("eth")); |
| assert_eq!(vec![10, 20, 30], shortlist_interfaces_by_nicid("th")); |
| assert_eq!(vec![100, 200, 300], shortlist_interfaces_by_nicid("wlan")); |
| assert_eq!(vec![10, 100], shortlist_interfaces_by_nicid("001")); |
| } |
| |
| #[test_case(fnet::IpVersion::V4, true ; "IPv4 enable routing")] |
| #[test_case(fnet::IpVersion::V4, false ; "IPv4 disable routing")] |
| #[test_case(fnet::IpVersion::V6, true ; "IPv6 enable routing")] |
| #[test_case(fnet::IpVersion::V6, false ; "IPv6 disable routing")] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_ip_forward(ip_version: fnet::IpVersion, enable: bool) { |
| let interface1 = TestInterface { nicid: 1, name: "interface1" }; |
| let (root_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<froot::InterfacesMarker>().unwrap(); |
| let connector = |
| TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() }; |
| |
| let requests_fut = set_configuration_request( |
| &mut requests, |
| interface1.nicid, |
| |c| extract_ip_forwarding(c, ip_version).expect("extract IP forwarding configuration"), |
| enable, |
| ); |
| let mut out = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut out, |
| opts::IfEnum::IpForward(opts::IfIpForward { |
| cmd: opts::IfIpForwardEnum::Set(opts::IfIpForwardSet { |
| interface: interface1.identifier(false /* use_ifname */), |
| ip_version, |
| enable, |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok)) |
| .await |
| .expect("setting interface ip forwarding should succeed"); |
| |
| let requests_fut = get_configuration_request( |
| &mut requests, |
| interface1.nicid, |
| configuration_with_ip_forwarding_set(ip_version, enable), |
| ); |
| let mut output_buf = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut output_buf, |
| opts::IfEnum::IpForward(opts::IfIpForward { |
| cmd: opts::IfIpForwardEnum::Get(opts::IfIpForwardGet { |
| interface: interface1.identifier(false /* use_ifname */), |
| ip_version, |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok)) |
| .await |
| .expect("getting interface ip forwarding should succeed"); |
| let got_output = output_buf.test_output().unwrap(); |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&got_output), |
| trim_whitespace_for_comparison(&format!( |
| "IP forwarding for {:?} is {} on interface {}", |
| ip_version, enable, interface1.nicid |
| )), |
| ) |
| } |
| |
| async fn set_configuration_request< |
| O: Debug + PartialEq, |
| F: FnOnce(finterfaces_admin::Configuration) -> O, |
| >( |
| requests: &mut froot::InterfacesRequestStream, |
| expected_nicid: u64, |
| extract_config: F, |
| expected_config: O, |
| ) { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("root request stream not ended") |
| .expect("root request stream not error") |
| .into_get_admin() |
| .expect("get admin request"); |
| assert_eq!(id, expected_nicid); |
| |
| let mut control: finterfaces_admin::ControlRequestStream = |
| control.into_stream().expect("control request stream"); |
| let (configuration, responder) = control |
| .next() |
| .await |
| .expect("control request stream not ended") |
| .expect("control request stream not error") |
| .into_set_configuration() |
| .expect("set configuration request"); |
| assert_eq!(extract_config(configuration), expected_config); |
| // net-cli does not check the returned configuration so we do not |
| // return a populated one. |
| let () = responder.send(Ok(&Default::default())).expect("responder.send should succeed"); |
| } |
| |
| async fn get_configuration_request( |
| requests: &mut froot::InterfacesRequestStream, |
| expected_nicid: u64, |
| config: finterfaces_admin::Configuration, |
| ) { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("root request stream not ended") |
| .expect("root request stream not error") |
| .into_get_admin() |
| .expect("get admin request"); |
| assert_eq!(id, expected_nicid); |
| |
| let mut control: finterfaces_admin::ControlRequestStream = |
| control.into_stream().expect("control request stream"); |
| let responder = control |
| .next() |
| .await |
| .expect("control request stream not ended") |
| .expect("control request stream not error") |
| .into_get_configuration() |
| .expect("get configuration request"); |
| let () = responder.send(Ok(&config)).expect("responder.send should succeed"); |
| } |
| |
| #[test_case(finterfaces_admin::IgmpVersion::V1)] |
| #[test_case(finterfaces_admin::IgmpVersion::V2)] |
| #[test_case(finterfaces_admin::IgmpVersion::V3)] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_igmp(igmp_version: finterfaces_admin::IgmpVersion) { |
| let interface1 = TestInterface { nicid: 1, name: "interface1" }; |
| let (root_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<froot::InterfacesMarker>().unwrap(); |
| let connector = |
| TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() }; |
| |
| let requests_fut = set_configuration_request( |
| &mut requests, |
| interface1.nicid, |
| |c| extract_igmp_version(c).unwrap(), |
| Some(igmp_version), |
| ); |
| let mut out = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut out, |
| opts::IfEnum::Igmp(opts::IfIgmp { |
| cmd: opts::IfIgmpEnum::Set(opts::IfIgmpSet { |
| interface: interface1.identifier(false /* use_ifname */), |
| version: Some(igmp_version), |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok)) |
| .await |
| .expect("setting interface IGMP configuration should succeed"); |
| |
| let requests_fut = get_configuration_request( |
| &mut requests, |
| interface1.nicid, |
| finterfaces_admin::Configuration { |
| ipv4: Some(finterfaces_admin::Ipv4Configuration { |
| igmp: Some(finterfaces_admin::IgmpConfiguration { |
| version: Some(igmp_version), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| ); |
| let mut output_buf = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut output_buf, |
| opts::IfEnum::Igmp(opts::IfIgmp { |
| cmd: opts::IfIgmpEnum::Get(opts::IfIgmpGet { |
| interface: interface1.identifier(false /* use_ifname */), |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok)) |
| .await |
| .expect("getting interface IGMP configuration should succeed"); |
| let got_output = output_buf.test_output().unwrap(); |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&got_output), |
| trim_whitespace_for_comparison(&format!( |
| "IGMP configuration on interface {}:\n Version: {:?}", |
| interface1.nicid, |
| Some(igmp_version), |
| )), |
| ) |
| } |
| |
| #[test_case(finterfaces_admin::MldVersion::V1)] |
| #[test_case(finterfaces_admin::MldVersion::V2)] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_mld(mld_version: finterfaces_admin::MldVersion) { |
| let interface1 = TestInterface { nicid: 1, name: "interface1" }; |
| let (root_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<froot::InterfacesMarker>().unwrap(); |
| let connector = |
| TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() }; |
| |
| let requests_fut = set_configuration_request( |
| &mut requests, |
| interface1.nicid, |
| |c| extract_mld_version(c).unwrap(), |
| Some(mld_version), |
| ); |
| let mut out = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut out, |
| opts::IfEnum::Mld(opts::IfMld { |
| cmd: opts::IfMldEnum::Set(opts::IfMldSet { |
| interface: interface1.identifier(false /* use_ifname */), |
| version: Some(mld_version), |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok)) |
| .await |
| .expect("setting interface MLD configuration should succeed"); |
| |
| let requests_fut = get_configuration_request( |
| &mut requests, |
| interface1.nicid, |
| finterfaces_admin::Configuration { |
| ipv6: Some(finterfaces_admin::Ipv6Configuration { |
| mld: Some(finterfaces_admin::MldConfiguration { |
| version: Some(mld_version), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| ); |
| let mut output_buf = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut output_buf, |
| opts::IfEnum::Mld(opts::IfMld { |
| cmd: opts::IfMldEnum::Get(opts::IfMldGet { |
| interface: interface1.identifier(false /* use_ifname */), |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut.map(Ok)) |
| .await |
| .expect("getting interface MLD configuration should succeed"); |
| let got_output = output_buf.test_output().unwrap(); |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&got_output), |
| trim_whitespace_for_comparison(&format!( |
| "MLD configuration on interface {}:\n Version: {:?}", |
| interface1.nicid, |
| Some(mld_version), |
| )), |
| ) |
| } |
| |
| async fn always_answer_with_interfaces( |
| interfaces_state_requests: finterfaces::StateRequestStream, |
| interfaces: Vec<finterfaces::Properties>, |
| ) { |
| interfaces_state_requests |
| .try_for_each(|request| { |
| let interfaces = interfaces.clone(); |
| async move { |
| let (finterfaces::WatcherOptions { .. }, server_end, _): ( |
| _, |
| _, |
| finterfaces::StateControlHandle, |
| ) = request.into_get_watcher().expect("request type should be GetWatcher"); |
| |
| let mut watcher_request_stream: finterfaces::WatcherRequestStream = |
| server_end.into_stream().expect("watcher FIDL error"); |
| |
| for event in interfaces |
| .into_iter() |
| .map(finterfaces::Event::Existing) |
| .chain(std::iter::once(finterfaces::Event::Idle(finterfaces::Empty))) |
| { |
| let () = watcher_request_stream |
| .try_next() |
| .await |
| .expect("watcher watch FIDL error") |
| .expect("watcher request stream should not have ended") |
| .into_watch() |
| .expect("request should be of type Watch") |
| .send(&event) |
| .expect("responder.send should succeed"); |
| } |
| |
| assert_matches!( |
| watcher_request_stream.try_next().await.expect("watcher watch FIDL error"), |
| None, |
| "remaining watcher request stream should be empty" |
| ); |
| Ok(()) |
| } |
| }) |
| .await |
| .expect("interfaces state FIDL error") |
| } |
| |
| #[derive(Clone)] |
| struct TestInterface { |
| nicid: u64, |
| name: &'static str, |
| } |
| |
| impl TestInterface { |
| fn identifier(&self, use_ifname: bool) -> opts::InterfaceIdentifier { |
| let Self { nicid, name } = self; |
| if use_ifname { |
| opts::InterfaceIdentifier::Name(name.to_string()) |
| } else { |
| opts::InterfaceIdentifier::Id(*nicid) |
| } |
| } |
| } |
| |
| #[test_case(true, false ; "when interface is up, and adding subnet route")] |
| #[test_case(true, true ; "when interface is up, and not adding subnet route")] |
| #[test_case(false, false ; "when interface is down, and adding subnet route")] |
| #[test_case(false, true ; "when interface is down, and not adding subnet route")] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_addr_add(interface_is_up: bool, no_subnet_route: bool) { |
| const TEST_PREFIX_LENGTH: u8 = 64; |
| |
| let interface1 = TestInterface { nicid: 1, name: "interface1" }; |
| let (root_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<froot::InterfacesMarker>().unwrap(); |
| |
| let connector = |
| TestConnector { root_interfaces: Some(root_interfaces), ..Default::default() }; |
| let mut out = ffx_writer::Writer::new_test(None); |
| let do_if_fut = do_if( |
| &mut out, |
| opts::IfEnum::Addr(opts::IfAddr { |
| addr_cmd: opts::IfAddrEnum::Add(opts::IfAddrAdd { |
| interface: interface1.identifier(false /* use_ifname */), |
| addr: fnet_ext::IpAddress::from(IF_ADDR_V6.addr).to_string(), |
| prefix: TEST_PREFIX_LENGTH, |
| no_subnet_route, |
| }), |
| }), |
| &connector, |
| ) |
| .map(|res| res.expect("success")); |
| |
| let admin_fut = async { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("root request stream not ended") |
| .expect("root request stream not error") |
| .into_get_admin() |
| .expect("get admin request"); |
| assert_eq!(id, interface1.nicid); |
| |
| let mut control: finterfaces_admin::ControlRequestStream = |
| control.into_stream().expect("control request stream"); |
| let ( |
| addr, |
| addr_params, |
| address_state_provider_server_end, |
| _admin_control_control_handle, |
| ) = control |
| .next() |
| .await |
| .expect("control request stream not ended") |
| .expect("control request stream not error") |
| .into_add_address() |
| .expect("add address request"); |
| assert_eq!(addr, IF_ADDR_V6); |
| assert_eq!( |
| addr_params, |
| finterfaces_admin::AddressParameters { |
| add_subnet_route: Some(!no_subnet_route), |
| ..Default::default() |
| } |
| ); |
| |
| let mut address_state_provider_request_stream = address_state_provider_server_end |
| .into_stream() |
| .expect("address state provider FIDL error"); |
| async fn next_request( |
| stream: &mut finterfaces_admin::AddressStateProviderRequestStream, |
| ) -> finterfaces_admin::AddressStateProviderRequest { |
| stream |
| .next() |
| .await |
| .expect("address state provider request stream not ended") |
| .expect("address state provider request stream not error") |
| } |
| |
| let _address_state_provider_control_handle = |
| next_request(&mut address_state_provider_request_stream) |
| .await |
| .into_detach() |
| .expect("detach request"); |
| |
| for _ in 0..3 { |
| let () = next_request(&mut address_state_provider_request_stream) |
| .await |
| .into_watch_address_assignment_state() |
| .expect("watch address assignment state request") |
| .send(finterfaces::AddressAssignmentState::Tentative) |
| .expect("send address assignment state succeeds"); |
| } |
| |
| let () = next_request(&mut address_state_provider_request_stream) |
| .await |
| .into_watch_address_assignment_state() |
| .expect("watch address assignment state request") |
| .send(if interface_is_up { |
| finterfaces::AddressAssignmentState::Assigned |
| } else { |
| finterfaces::AddressAssignmentState::Unavailable |
| }) |
| .expect("send address assignment state succeeds"); |
| }; |
| |
| let ((), ()) = futures::join!(admin_fut, do_if_fut); |
| } |
| |
| #[test_case(false ; "providing nicids")] |
| #[test_case(true ; "providing interface names")] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_del_addr(use_ifname: bool) { |
| let interface1 = TestInterface { nicid: 1, name: "interface1" }; |
| let interface2 = TestInterface { nicid: 2, name: "interface2" }; |
| |
| let (root_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<froot::InterfacesMarker>().unwrap(); |
| let (interfaces_state, interfaces_requests) = |
| fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>().unwrap(); |
| |
| let (interface1_properties, _mac) = get_fake_interface( |
| interface1.nicid, |
| interface1.name, |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Ethernet), |
| None, |
| ); |
| |
| let interfaces_fut = |
| always_answer_with_interfaces(interfaces_requests, vec![interface1_properties.into()]) |
| .fuse(); |
| let mut interfaces_fut = pin!(interfaces_fut); |
| |
| let connector = TestConnector { |
| root_interfaces: Some(root_interfaces), |
| interfaces_state: Some(interfaces_state), |
| ..Default::default() |
| }; |
| |
| let mut out = ffx_writer::Writer::new_test(None); |
| // Make the first request. |
| let succeeds = do_if( |
| &mut out, |
| opts::IfEnum::Addr(opts::IfAddr { |
| addr_cmd: opts::IfAddrEnum::Del(opts::IfAddrDel { |
| interface: interface1.identifier(use_ifname), |
| addr: fnet_ext::IpAddress::from(IF_ADDR_V4.addr).to_string(), |
| prefix: None, // The prefix should be set to the default of 32 for IPv4. |
| }), |
| }), |
| &connector, |
| ) |
| .map(|res| res.expect("success")); |
| let handler_fut = async { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("root request stream not ended") |
| .expect("root request stream not error") |
| .into_get_admin() |
| .expect("get admin request"); |
| assert_eq!(id, interface1.nicid); |
| let mut control = control.into_stream().expect("control request stream"); |
| let (addr, responder) = control |
| .next() |
| .await |
| .expect("control request stream not ended") |
| .expect("control request stream not error") |
| .into_remove_address() |
| .expect("del address request"); |
| assert_eq!(addr, IF_ADDR_V4); |
| let () = responder.send(Ok(true)).expect("responder send"); |
| }; |
| |
| futures::select! { |
| () = interfaces_fut => panic!("interfaces_fut should never complete"), |
| ((), ()) = futures::future::join(handler_fut, succeeds).fuse() => {}, |
| } |
| |
| let mut out = ffx_writer::Writer::new_test(None); |
| // Make the second request. |
| let fails = do_if( |
| &mut out, |
| opts::IfEnum::Addr(opts::IfAddr { |
| addr_cmd: opts::IfAddrEnum::Del(opts::IfAddrDel { |
| interface: interface2.identifier(use_ifname), |
| addr: fnet_ext::IpAddress::from(IF_ADDR_V6.addr).to_string(), |
| prefix: Some(IF_ADDR_V6.prefix_len), |
| }), |
| }), |
| &connector, |
| ) |
| .map(|res| res.expect_err("failure")); |
| |
| if use_ifname { |
| // The caller will have failed to find an interface matching the name, |
| // so we don't expect any requests to make it to us. |
| futures::select! { |
| () = interfaces_fut => panic!("interfaces_fut should never complete"), |
| e = fails.fuse() => { |
| assert_eq!(e.to_string(), format!("No interface with name {}", interface2.name)); |
| }, |
| } |
| } else { |
| let handler_fut = async { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("root request stream not ended") |
| .expect("root request stream not error") |
| .into_get_admin() |
| .expect("get admin request"); |
| assert_eq!(id, interface2.nicid); |
| let mut control = control.into_stream().expect("control request stream"); |
| let (addr, responder) = control |
| .next() |
| .await |
| .expect("control request stream not ended") |
| .expect("control request stream not error") |
| .into_remove_address() |
| .expect("del address request"); |
| assert_eq!(addr, IF_ADDR_V6); |
| let () = responder.send(Ok(false)).expect("responder send"); |
| }; |
| futures::select! { |
| () = interfaces_fut => panic!("interfaces_fut should never complete"), |
| ((), e) = futures::future::join(handler_fut, fails).fuse() => { |
| let fnet_ext::IpAddress(addr) = IF_ADDR_V6.addr.into(); |
| assert_eq!(e.to_string(), format!("Address {} not found on interface {}", addr, interface2.nicid)); |
| }, |
| } |
| } |
| } |
| |
| const INTERFACE_NAME: &str = "if1"; |
| |
| fn interface_properties( |
| addrs: Vec<(fnet::Subnet, finterfaces::AddressAssignmentState)>, |
| ) -> finterfaces::Properties { |
| finterfaces_ext::Properties { |
| id: INTERFACE_ID.try_into().unwrap(), |
| name: INTERFACE_NAME.to_string(), |
| device_class: finterfaces::DeviceClass::Device( |
| fhardware_network::DeviceClass::Ethernet, |
| ), |
| online: true, |
| addresses: addrs |
| .into_iter() |
| .map(|(addr, assignment_state)| finterfaces_ext::Address { |
| addr, |
| assignment_state, |
| valid_until: fuchsia_zircon_types::ZX_TIME_INFINITE, |
| }) |
| .collect(), |
| has_default_ipv4_route: false, |
| has_default_ipv6_route: false, |
| } |
| .into() |
| } |
| |
| #[test_case( |
| false, |
| vec![ |
| finterfaces::Event::Existing(interface_properties(vec![])), |
| finterfaces::Event::Idle(finterfaces::Empty), |
| finterfaces::Event::Changed(interface_properties(vec![ |
| (fidl_subnet!("192.168.0.1/32"), finterfaces::AddressAssignmentState::Assigned) |
| ])), |
| ], |
| "192.168.0.1"; |
| "wait for an address to be assigned" |
| )] |
| #[test_case( |
| false, |
| vec![ |
| finterfaces::Event::Existing(interface_properties(vec![ |
| (fidl_subnet!("192.168.0.1/32"), finterfaces::AddressAssignmentState::Assigned), |
| (fidl_subnet!("fd00::1/64"), finterfaces::AddressAssignmentState::Assigned), |
| ])), |
| ], |
| "192.168.0.1"; |
| "prefer first when any address requested" |
| )] |
| #[test_case( |
| true, |
| vec![ |
| finterfaces::Event::Existing(interface_properties(vec![ |
| (fidl_subnet!("192.168.0.1/32"), finterfaces::AddressAssignmentState::Assigned) |
| ])), |
| finterfaces::Event::Idle(finterfaces::Empty), |
| finterfaces::Event::Changed(interface_properties(vec![ |
| (fidl_subnet!("fd00::1/64"), finterfaces::AddressAssignmentState::Assigned) |
| ])), |
| ], |
| "fd00::1"; |
| "wait for IPv6 when IPv6 address requested" |
| )] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_addr_wait(ipv6: bool, events: Vec<finterfaces::Event>, expected_output: &str) { |
| let interface = TestInterface { nicid: INTERFACE_ID, name: INTERFACE_NAME }; |
| |
| let (interfaces_state, mut request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>().unwrap(); |
| |
| let interfaces_handler = async move { |
| let (finterfaces::WatcherOptions { include_non_assigned_addresses, .. }, server_end, _) = |
| request_stream |
| .next() |
| .await |
| .expect("should call state") |
| .expect("should succeed") |
| .into_get_watcher() |
| .expect("request should be GetWatcher"); |
| assert_eq!(include_non_assigned_addresses, Some(false)); |
| let mut request_stream: finterfaces::WatcherRequestStream = |
| server_end.into_stream().expect("server end into request stream"); |
| for event in events { |
| request_stream |
| .next() |
| .await |
| .expect("should call watcher") |
| .expect("should succeed") |
| .into_watch() |
| .expect("request should be Watch") |
| .send(&event) |
| .expect("send response"); |
| } |
| }; |
| |
| let connector = |
| TestConnector { interfaces_state: Some(interfaces_state), ..Default::default() }; |
| let mut out = ffx_writer::Writer::new_test(None); |
| let run_command = do_if( |
| &mut out, |
| opts::IfEnum::Addr(opts::IfAddr { |
| addr_cmd: opts::IfAddrEnum::Wait(opts::IfAddrWait { |
| interface: interface.identifier(false), |
| ipv6, |
| }), |
| }), |
| &connector, |
| ) |
| .map(|r| r.expect("command should succeed")); |
| |
| let ((), ()) = futures::future::join(interfaces_handler, run_command).await; |
| |
| let output = out.test_output().unwrap(); |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&output), |
| trim_whitespace_for_comparison(expected_output), |
| ); |
| } |
| |
| fn wanted_net_if_list_json() -> String { |
| json!([ |
| { |
| "addresses": { |
| "ipv4": [], |
| "ipv6": [], |
| }, |
| "device_class": "Loopback", |
| "mac": "00:00:00:00:00:00", |
| "name": "lo", |
| "nicid": 1, |
| "online": true, |
| "has_default_ipv4_route": false, |
| "has_default_ipv6_route": false, |
| }, |
| { |
| "addresses": { |
| "ipv4": [], |
| "ipv6": [], |
| }, |
| "device_class": "Ethernet", |
| "mac": "01:02:03:04:05:06", |
| "name": "eth001", |
| "nicid": 10, |
| "online": true, |
| "has_default_ipv4_route": false, |
| "has_default_ipv6_route": false, |
| }, |
| { |
| "addresses": { |
| "ipv4": [], |
| "ipv6": [], |
| }, |
| "device_class": "Virtual", |
| "mac": null, |
| "name": "virt001", |
| "nicid": 20, |
| "online": true, |
| "has_default_ipv4_route": false, |
| "has_default_ipv6_route": false, |
| }, |
| { |
| "addresses": { |
| "ipv4": [ |
| { |
| "addr": "192.168.0.1", |
| "assignment_state": "Tentative", |
| "prefix_len": 24, |
| "valid_until": 2500000000_u64, |
| } |
| ], |
| "ipv6": [], |
| }, |
| "device_class": "Ethernet", |
| "mac": null, |
| "name": "eth002", |
| "nicid": 30, |
| "online": true, |
| "has_default_ipv4_route": false, |
| "has_default_ipv6_route": true, |
| }, |
| { |
| "addresses": { |
| "ipv4": [], |
| "ipv6": [{ |
| "addr": "2001:db8::1", |
| "assignment_state": "Unavailable", |
| "prefix_len": 64, |
| "valid_until": null, |
| }], |
| }, |
| "device_class": "Ethernet", |
| "mac": null, |
| "name": "eth003", |
| "nicid": 40, |
| "online": true, |
| "has_default_ipv4_route": true, |
| "has_default_ipv6_route": true, |
| }, |
| ]) |
| .to_string() |
| } |
| |
| fn wanted_net_if_list_tabular() -> String { |
| String::from( |
| r#" |
| nicid 1 |
| name lo |
| device class loopback |
| online true |
| default routes - |
| mac 00:00:00:00:00:00 |
| |
| nicid 10 |
| name eth001 |
| device class ethernet |
| online true |
| default routes - |
| mac 01:02:03:04:05:06 |
| |
| nicid 20 |
| name virt001 |
| device class virtual |
| online true |
| default routes - |
| mac - |
| |
| nicid 30 |
| name eth002 |
| device class ethernet |
| online true |
| default routes IPv6 |
| addr 192.168.0.1/24 TENTATIVE valid until [2.5s] |
| mac - |
| |
| nicid 40 |
| name eth003 |
| device class ethernet |
| online true |
| default routes IPv4,IPv6 |
| addr 2001:db8::1/64 UNAVAILABLE |
| mac - |
| "#, |
| ) |
| } |
| |
| #[test_case(true, wanted_net_if_list_json() ; "in json format")] |
| #[test_case(false, wanted_net_if_list_tabular() ; "in tabular format")] |
| #[fasync::run_singlethreaded(test)] |
| async fn if_list(json: bool, wanted_output: String) { |
| let (root_interfaces, root_interfaces_stream) = |
| fidl::endpoints::create_proxy_and_stream::<froot::InterfacesMarker>().unwrap(); |
| let (interfaces_state, interfaces_state_stream) = |
| fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>().unwrap(); |
| |
| let mut output = if json { |
| ffx_writer::Writer::new_test(Some(ffx_writer::Format::Json)) |
| } else { |
| ffx_writer::Writer::new_test(None) |
| }; |
| let output_ref = &mut output; |
| |
| let do_if_fut = async { |
| let connector = TestConnector { |
| root_interfaces: Some(root_interfaces), |
| interfaces_state: Some(interfaces_state), |
| ..Default::default() |
| }; |
| do_if(output_ref, opts::IfEnum::List(opts::IfList { name_pattern: None }), &connector) |
| .map(|res| res.expect("if list")) |
| .await |
| }; |
| let watcher_stream = interfaces_state_stream |
| .and_then(|req| match req { |
| finterfaces::StateRequest::GetWatcher { |
| options: _, |
| watcher, |
| control_handle: _, |
| } => futures::future::ready(watcher.into_stream()), |
| }) |
| .try_flatten() |
| .map(|res| res.expect("watcher stream error")); |
| let (interfaces, mac_addresses): (Vec<_>, HashMap<_, _>) = [ |
| get_fake_interface( |
| 1, |
| "lo", |
| finterfaces::DeviceClass::Loopback(finterfaces::Empty), |
| Some([0, 0, 0, 0, 0, 0]), |
| ), |
| get_fake_interface( |
| 10, |
| "eth001", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Ethernet), |
| Some([1, 2, 3, 4, 5, 6]), |
| ), |
| get_fake_interface( |
| 20, |
| "virt001", |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Virtual), |
| None, |
| ), |
| ( |
| finterfaces_ext::Properties { |
| id: 30.try_into().unwrap(), |
| name: "eth002".to_string(), |
| device_class: finterfaces::DeviceClass::Device( |
| fhardware_network::DeviceClass::Ethernet, |
| ), |
| online: true, |
| addresses: vec![finterfaces_ext::Address { |
| addr: fidl_subnet!("192.168.0.1/24"), |
| valid_until: std::time::Duration::from_millis(2500) |
| .as_nanos() |
| .try_into() |
| .unwrap(), |
| assignment_state: finterfaces::AddressAssignmentState::Tentative, |
| }], |
| has_default_ipv4_route: false, |
| has_default_ipv6_route: true, |
| }, |
| None, |
| ), |
| ( |
| finterfaces_ext::Properties { |
| id: 40.try_into().unwrap(), |
| name: "eth003".to_string(), |
| device_class: finterfaces::DeviceClass::Device( |
| fhardware_network::DeviceClass::Ethernet, |
| ), |
| online: true, |
| addresses: vec![finterfaces_ext::Address { |
| addr: fidl_subnet!("2001:db8::1/64"), |
| valid_until: i64::MAX, |
| assignment_state: finterfaces::AddressAssignmentState::Unavailable, |
| }], |
| has_default_ipv4_route: true, |
| has_default_ipv6_route: true, |
| }, |
| None, |
| ), |
| ] |
| .into_iter() |
| .map(|(properties, mac)| { |
| let finterfaces_ext::Properties { id, .. } = &properties; |
| let id = *id; |
| (properties, (id, mac)) |
| }) |
| .unzip(); |
| let interfaces = |
| futures::stream::iter(interfaces.into_iter().map(Some).chain(std::iter::once(None))); |
| let watcher_fut = watcher_stream.zip(interfaces).for_each(|(req, properties)| match req { |
| finterfaces::WatcherRequest::Watch { responder } => { |
| let event = properties.map_or( |
| finterfaces::Event::Idle(finterfaces::Empty), |
| |finterfaces_ext::Properties { |
| id, |
| name, |
| device_class, |
| online, |
| addresses, |
| has_default_ipv4_route, |
| has_default_ipv6_route, |
| }| { |
| finterfaces::Event::Existing(finterfaces::Properties { |
| id: Some(id.get()), |
| name: Some(name), |
| device_class: Some(device_class), |
| online: Some(online), |
| addresses: Some( |
| addresses |
| .into_iter() |
| .map( |
| |finterfaces_ext::Address { |
| addr, |
| valid_until, |
| assignment_state, |
| }| { |
| finterfaces::Address { |
| addr: Some(addr), |
| valid_until: Some(valid_until), |
| assignment_state: Some(assignment_state), |
| ..Default::default() |
| } |
| }, |
| ) |
| .collect(), |
| ), |
| has_default_ipv4_route: Some(has_default_ipv4_route), |
| has_default_ipv6_route: Some(has_default_ipv6_route), |
| ..Default::default() |
| }) |
| }, |
| ); |
| let () = responder.send(&event).expect("send watcher event"); |
| futures::future::ready(()) |
| } |
| }); |
| let root_fut = root_interfaces_stream |
| .map(|res| res.expect("root interfaces stream error")) |
| .for_each_concurrent(None, |req| { |
| let (id, responder) = req.into_get_mac().expect("get_mac request"); |
| let () = responder |
| .send( |
| mac_addresses |
| .get(&id.try_into().unwrap()) |
| .map(Option::as_ref) |
| .ok_or(froot::InterfacesGetMacError::NotFound), |
| ) |
| .expect("send get_mac response"); |
| futures::future::ready(()) |
| }); |
| let ((), (), ()) = futures::future::join3(do_if_fut, watcher_fut, root_fut).await; |
| |
| let got_output = output.test_output().unwrap(); |
| |
| if json { |
| let got: Value = serde_json::from_str(&got_output).unwrap(); |
| let want: Value = serde_json::from_str(&wanted_output).unwrap(); |
| pretty_assertions::assert_eq!(got, want); |
| } else { |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&got_output), |
| trim_whitespace_for_comparison(&wanted_output), |
| ); |
| } |
| } |
| |
| async fn test_do_dhcp(cmd: opts::DhcpEnum) { |
| let (stack, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fstack::StackMarker>().unwrap(); |
| let connector = TestConnector { stack: Some(stack), ..Default::default() }; |
| let op = do_dhcp(cmd.clone(), &connector); |
| let op_succeeds = async move { |
| let (expected_id, expected_enable) = match cmd { |
| opts::DhcpEnum::Start(opts::DhcpStart { interface }) => (interface, true), |
| opts::DhcpEnum::Stop(opts::DhcpStop { interface }) => (interface, false), |
| }; |
| let request = requests |
| .try_next() |
| .await |
| .expect("start FIDL error") |
| .expect("request stream should not have ended"); |
| let (received_id, enable, responder) = request |
| .into_set_dhcp_client_enabled() |
| .expect("request should be of type StopDhcpClient"); |
| assert_eq!(opts::InterfaceIdentifier::Id(u64::from(received_id)), expected_id); |
| assert_eq!(enable, expected_enable); |
| responder.send(Ok(())).map_err(anyhow::Error::new) |
| }; |
| let ((), ()) = |
| futures::future::try_join(op, op_succeeds).await.expect("dhcp command should succeed"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn dhcp_start() { |
| let () = test_do_dhcp(opts::DhcpEnum::Start(opts::DhcpStart { interface: 1.into() })).await; |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn dhcp_stop() { |
| let () = test_do_dhcp(opts::DhcpEnum::Stop(opts::DhcpStop { interface: 1.into() })).await; |
| } |
| |
| async fn test_modify_route(cmd: opts::RouteEnum) { |
| let expected_interface = match &cmd { |
| opts::RouteEnum::List(_) => panic!("test_modify_route should not take a List command"), |
| opts::RouteEnum::Add(opts::RouteAdd { interface, .. }) => interface, |
| opts::RouteEnum::Del(opts::RouteDel { interface, .. }) => interface, |
| } |
| .clone(); |
| let expected_id = match expected_interface { |
| opts::InterfaceIdentifier::Id(ref id) => *id, |
| opts::InterfaceIdentifier::Name(_) => { |
| panic!("expected test to work only with ids") |
| } |
| }; |
| |
| let (stack, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fstack::StackMarker>().unwrap(); |
| let connector = TestConnector { stack: Some(stack), ..Default::default() }; |
| let mut out = ffx_writer::Writer::new_test(None); |
| let op = do_route(&mut out, cmd.clone(), &connector); |
| let op_succeeds = async move { |
| let () = match cmd { |
| opts::RouteEnum::List(opts::RouteList {}) => { |
| panic!("test_modify_route should not take a List command") |
| } |
| opts::RouteEnum::Add(route) => { |
| let expected_entry = route.into_route_table_entry( |
| expected_id.try_into().expect("nicid does not fit in u32"), |
| ); |
| let (entry, responder) = requests |
| .try_next() |
| .await |
| .expect("add route FIDL error") |
| .expect("request stream should not have ended") |
| .into_add_forwarding_entry() |
| .expect("request should be of type AddRoute"); |
| assert_eq!(entry, expected_entry); |
| responder.send(Ok(())) |
| } |
| opts::RouteEnum::Del(route) => { |
| let expected_entry = route.into_route_table_entry( |
| expected_id.try_into().expect("nicid does not fit in u32"), |
| ); |
| let (entry, responder) = requests |
| .try_next() |
| .await |
| .expect("del route FIDL error") |
| .expect("request stream should not have ended") |
| .into_del_forwarding_entry() |
| .expect("request should be of type DelRoute"); |
| assert_eq!(entry, expected_entry); |
| responder.send(Ok(())) |
| } |
| }?; |
| Ok(()) |
| }; |
| let ((), ()) = |
| futures::future::try_join(op, op_succeeds).await.expect("dhcp command should succeed"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn route_add() { |
| // Test arguments have been arbitrarily selected. |
| let () = test_modify_route(opts::RouteEnum::Add(opts::RouteAdd { |
| destination: std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 0)), |
| prefix_len: 24, |
| gateway: None, |
| interface: 2.into(), |
| metric: 100, |
| })) |
| .await; |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn route_del() { |
| // Test arguments have been arbitrarily selected. |
| let () = test_modify_route(opts::RouteEnum::Del(opts::RouteDel { |
| destination: std::net::IpAddr::V4(std::net::Ipv4Addr::new(192, 168, 1, 0)), |
| prefix_len: 24, |
| gateway: None, |
| interface: 2.into(), |
| metric: 100, |
| })) |
| .await; |
| } |
| |
| fn wanted_route_list_json() -> String { |
| json!([ |
| { |
| "destination":{"addr":"1.1.1.0","prefix_len":24}, |
| "gateway":"1.1.1.2", |
| "metric":4, |
| "nicid":3 |
| }, |
| { |
| "destination":{"addr":"10.10.10.0","prefix_len":24}, |
| "gateway":"10.10.10.20", |
| "metric":40, |
| "nicid":30 |
| }, |
| { |
| "destination":{"addr":"fe80::","prefix_len":64}, |
| "gateway":serde_json::Value::Null, |
| "metric":400, |
| "nicid":300 |
| } |
| |
| ]) |
| .to_string() |
| } |
| |
| fn wanted_route_list_tabular() -> String { |
| "Destination Gateway NICID Metric |
| 1.1.1.0/24 1.1.1.2 3 4 |
| 10.10.10.0/24 10.10.10.20 30 40 |
| fe80::/64 - 300 400 |
| " |
| .to_string() |
| } |
| |
| #[test_case(true, wanted_route_list_json() ; "in json format")] |
| #[test_case(false, wanted_route_list_tabular() ; "in tabular format")] |
| #[fasync::run_singlethreaded(test)] |
| async fn route_list(json: bool, wanted_output: String) { |
| let (routes_v4_controller, mut routes_v4_state_stream) = |
| fidl::endpoints::create_proxy_and_stream::<froutes::StateV4Marker>().unwrap(); |
| let (routes_v6_controller, mut routes_v6_state_stream) = |
| fidl::endpoints::create_proxy_and_stream::<froutes::StateV6Marker>().unwrap(); |
| let connector = TestConnector { |
| routes_v4: Some(routes_v4_controller), |
| routes_v6: Some(routes_v6_controller), |
| ..Default::default() |
| }; |
| |
| let mut output = if json { |
| ffx_writer::Writer::new_test(Some(ffx_writer::Format::Json)) |
| } else { |
| ffx_writer::Writer::new_test(None) |
| }; |
| |
| let do_route_fut = |
| do_route(&mut output, opts::RouteEnum::List(opts::RouteList {}), &connector); |
| |
| let v4_route_events = vec![ |
| froutes::EventV4::Existing(froutes::InstalledRouteV4 { |
| route: Some(froutes::RouteV4 { |
| destination: net_declare::fidl_ip_v4_with_prefix!("1.1.1.0/24"), |
| action: froutes::RouteActionV4::Forward(froutes::RouteTargetV4 { |
| outbound_interface: 3, |
| next_hop: Some(Box::new(net_declare::fidl_ip_v4!("1.1.1.2"))), |
| }), |
| properties: froutes::RoutePropertiesV4 { |
| specified_properties: Some(froutes::SpecifiedRouteProperties { |
| metric: Some(froutes::SpecifiedMetric::ExplicitMetric(4)), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| }), |
| effective_properties: Some(froutes::EffectiveRouteProperties { |
| metric: Some(4), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| froutes::EventV4::Existing(froutes::InstalledRouteV4 { |
| route: Some(froutes::RouteV4 { |
| destination: net_declare::fidl_ip_v4_with_prefix!("10.10.10.0/24"), |
| action: froutes::RouteActionV4::Forward(froutes::RouteTargetV4 { |
| outbound_interface: 30, |
| next_hop: Some(Box::new(net_declare::fidl_ip_v4!("10.10.10.20"))), |
| }), |
| properties: froutes::RoutePropertiesV4 { |
| specified_properties: Some(froutes::SpecifiedRouteProperties { |
| metric: Some(froutes::SpecifiedMetric::ExplicitMetric(40)), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| }), |
| effective_properties: Some(froutes::EffectiveRouteProperties { |
| metric: Some(40), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| froutes::EventV4::Idle(froutes::Empty), |
| ]; |
| let v6_route_events = vec![ |
| froutes::EventV6::Existing(froutes::InstalledRouteV6 { |
| route: Some(froutes::RouteV6 { |
| destination: net_declare::fidl_ip_v6_with_prefix!("fe80::/64"), |
| action: froutes::RouteActionV6::Forward(froutes::RouteTargetV6 { |
| outbound_interface: 300, |
| next_hop: None, |
| }), |
| properties: froutes::RoutePropertiesV6 { |
| specified_properties: Some(froutes::SpecifiedRouteProperties { |
| metric: Some(froutes::SpecifiedMetric::ExplicitMetric(400)), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }, |
| }), |
| effective_properties: Some(froutes::EffectiveRouteProperties { |
| metric: Some(400), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }), |
| froutes::EventV6::Idle(froutes::Empty), |
| ]; |
| |
| let route_v4_fut = routes_v4_state_stream.select_next_some().then(|request| { |
| froutes_ext::testutil::serve_state_request::<Ipv4>( |
| request, |
| futures::stream::once(futures::future::ready(v4_route_events)), |
| ) |
| }); |
| let route_v6_fut = routes_v6_state_stream.select_next_some().then(|request| { |
| froutes_ext::testutil::serve_state_request::<Ipv6>( |
| request, |
| futures::stream::once(futures::future::ready(v6_route_events)), |
| ) |
| }); |
| |
| let ((), (), ()) = |
| futures::try_join!(do_route_fut, route_v4_fut.map(Ok), route_v6_fut.map(Ok)) |
| .expect("listing forwarding table entries should succeed"); |
| |
| let got_output = output.test_output().unwrap(); |
| |
| if json { |
| let got: Value = serde_json::from_str(&got_output).unwrap(); |
| let want: Value = serde_json::from_str(&wanted_output).unwrap(); |
| pretty_assertions::assert_eq!(got, want); |
| } else { |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&got_output), |
| trim_whitespace_for_comparison(&wanted_output), |
| ); |
| } |
| } |
| |
| #[test_case(false ; "providing nicids")] |
| #[test_case(true ; "providing interface names")] |
| #[fasync::run_singlethreaded(test)] |
| async fn bridge(use_ifname: bool) { |
| let (stack, mut stack_requests) = |
| fidl::endpoints::create_proxy_and_stream::<fstack::StackMarker>().unwrap(); |
| let (interfaces_state, interfaces_state_requests) = |
| fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>().unwrap(); |
| let connector = TestConnector { |
| interfaces_state: Some(interfaces_state), |
| stack: Some(stack), |
| ..Default::default() |
| }; |
| |
| let bridge_ifs = vec![ |
| TestInterface { nicid: 1, name: "interface1" }, |
| TestInterface { nicid: 2, name: "interface2" }, |
| TestInterface { nicid: 3, name: "interface3" }, |
| ]; |
| |
| let interface_fidls = bridge_ifs |
| .iter() |
| .map(|interface| { |
| let (interface, _mac) = get_fake_interface( |
| interface.nicid, |
| interface.name, |
| finterfaces::DeviceClass::Device(fhardware_network::DeviceClass::Ethernet), |
| None, |
| ); |
| interface.into() |
| }) |
| .collect::<Vec<_>>(); |
| |
| let interfaces_fut = |
| always_answer_with_interfaces(interfaces_state_requests, interface_fidls); |
| |
| let bridge_id = 4; |
| let mut out = ffx_writer::Writer::new_test(None); |
| let bridge = do_if( |
| &mut out, |
| opts::IfEnum::Bridge(opts::IfBridge { |
| interfaces: bridge_ifs |
| .iter() |
| .map(|interface| interface.identifier(use_ifname)) |
| .collect(), |
| }), |
| &connector, |
| ); |
| |
| let bridge_succeeds = async move { |
| let (requested_ifs, bridge_server_end, _control_handle) = stack_requests |
| .try_next() |
| .await |
| .expect("stack requests FIDL error") |
| .expect("request stream should not have ended") |
| .into_bridge_interfaces() |
| .expect("request should be of type BridgeInterfaces"); |
| assert_eq!( |
| requested_ifs, |
| bridge_ifs.iter().map(|interface| interface.nicid).collect::<Vec<_>>() |
| ); |
| let mut bridge_requests = bridge_server_end.into_stream().expect("bridge stream"); |
| let responder = bridge_requests |
| .try_next() |
| .await |
| .expect("bridge requests FIDL error") |
| .expect("request stream should not have ended") |
| .into_get_id() |
| .expect("request should be get_id"); |
| responder.send(bridge_id).expect("responding with bridge ID should succeed"); |
| let _control_handle = bridge_requests |
| .try_next() |
| .await |
| .expect("bridge requests FIDL error") |
| .expect("request stream should not have ended") |
| .into_detach() |
| .expect("request should be detach"); |
| Ok(()) |
| }; |
| futures::select! { |
| () = interfaces_fut.fuse() => panic!("interfaces_fut should never complete"), |
| result = futures::future::try_join(bridge, bridge_succeeds).fuse() => { |
| let ((), ()) = result.expect("if bridge should succeed"); |
| } |
| } |
| } |
| |
| async fn test_get_neigh_entries( |
| watch_for_changes: bool, |
| batches: Vec<Vec<fneighbor::EntryIteratorItem>>, |
| want: String, |
| ) { |
| let (it, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fneighbor::EntryIteratorMarker>().unwrap(); |
| |
| let server = async { |
| for items in batches { |
| let responder = requests |
| .try_next() |
| .await |
| .expect("neigh FIDL error") |
| .expect("request stream should not have ended") |
| .into_get_next() |
| .expect("request should be of type GetNext"); |
| let () = responder.send(&items).expect("responder.send should succeed"); |
| } |
| } |
| .on_timeout(std::time::Duration::from_secs(60), || panic!("server responder timed out")); |
| |
| let client = async { |
| let mut stream = neigh_entry_stream(it, watch_for_changes); |
| |
| let item_to_string = |item| { |
| let mut buf = ffx_writer::Writer::new_test(None); |
| let () = write_neigh_entry(&mut buf, item, watch_for_changes) |
| .expect("write_neigh_entry should succeed"); |
| buf.test_output().expect("string should be UTF-8") |
| }; |
| |
| // Check each string sent by get_neigh_entries |
| for want_line in want.lines() { |
| let got = stream |
| .next() |
| .await |
| .map(|item| item_to_string(item.expect("neigh_entry_stream should succeed"))); |
| assert_eq!(got, Some(format!("{}\n", want_line))); |
| } |
| |
| // When listing entries, the sender should close after sending all existing entries. |
| if !watch_for_changes { |
| match stream.next().await { |
| Some(Ok(item)) => { |
| panic!("unexpected item from stream: {}", item_to_string(item)) |
| } |
| Some(Err(err)) => panic!("unexpected error from stream: {}", err), |
| None => {} |
| } |
| } |
| }; |
| |
| let ((), ()) = futures::future::join(client, server).await; |
| } |
| |
| async fn test_neigh_none(watch_for_changes: bool, want: String) { |
| test_get_neigh_entries( |
| watch_for_changes, |
| vec![vec![fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {})]], |
| want, |
| ) |
| .await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_list_none() { |
| test_neigh_none(false /* watch_for_changes */, "".to_string()).await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_watch_none() { |
| test_neigh_none(true /* watch_for_changes */, "IDLE".to_string()).await |
| } |
| |
| fn timestamp_60s_ago() -> i64 { |
| let now = std::time::SystemTime::now() |
| .duration_since(std::time::SystemTime::UNIX_EPOCH) |
| .expect("failed to get duration since epoch"); |
| let past = now - std::time::Duration::from_secs(60); |
| i64::try_from(past.as_nanos()).expect("failed to convert duration to i64") |
| } |
| |
| async fn test_neigh_one(watch_for_changes: bool, want: fn(fneighbor_ext::Entry) -> String) { |
| fn new_entry(updated_at: i64) -> fneighbor::Entry { |
| fneighbor::Entry { |
| interface: Some(1), |
| neighbor: Some(IF_ADDR_V4.addr), |
| state: Some(fneighbor::EntryState::Reachable), |
| mac: Some(MAC_1), |
| updated_at: Some(updated_at), |
| ..Default::default() |
| } |
| } |
| |
| let updated_at = timestamp_60s_ago(); |
| |
| test_get_neigh_entries( |
| watch_for_changes, |
| vec![vec![ |
| fneighbor::EntryIteratorItem::Existing(new_entry(updated_at)), |
| fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {}), |
| ]], |
| want(fneighbor_ext::Entry::try_from(new_entry(updated_at)).unwrap()), |
| ) |
| .await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_list_one() { |
| test_neigh_one(false /* watch_for_changes */, |entry| format!("{}\n", entry)).await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_watch_one() { |
| test_neigh_one(true /* watch_for_changes */, |entry| { |
| format!( |
| "EXISTING | {}\n\ |
| IDLE\n", |
| entry |
| ) |
| }) |
| .await |
| } |
| |
| async fn test_neigh_many( |
| watch_for_changes: bool, |
| want: fn(fneighbor_ext::Entry, fneighbor_ext::Entry) -> String, |
| ) { |
| fn new_entry( |
| ip: fnet::IpAddress, |
| mac: fnet::MacAddress, |
| updated_at: i64, |
| ) -> fneighbor::Entry { |
| fneighbor::Entry { |
| interface: Some(1), |
| neighbor: Some(ip), |
| state: Some(fneighbor::EntryState::Reachable), |
| mac: Some(mac), |
| updated_at: Some(updated_at), |
| ..Default::default() |
| } |
| } |
| |
| let updated_at = timestamp_60s_ago(); |
| let offset = i64::try_from(std::time::Duration::from_secs(60).as_nanos()) |
| .expect("failed to convert duration to i64"); |
| |
| test_get_neigh_entries( |
| watch_for_changes, |
| vec![vec![ |
| fneighbor::EntryIteratorItem::Existing(new_entry( |
| IF_ADDR_V4.addr, |
| MAC_1, |
| updated_at, |
| )), |
| fneighbor::EntryIteratorItem::Existing(new_entry( |
| IF_ADDR_V6.addr, |
| MAC_2, |
| updated_at - offset, |
| )), |
| fneighbor::EntryIteratorItem::Idle(fneighbor::IdleEvent {}), |
| ]], |
| want( |
| fneighbor_ext::Entry::try_from(new_entry(IF_ADDR_V4.addr, MAC_1, updated_at)) |
| .unwrap(), |
| fneighbor_ext::Entry::try_from(new_entry( |
| IF_ADDR_V6.addr, |
| MAC_2, |
| updated_at - offset, |
| )) |
| .unwrap(), |
| ), |
| ) |
| .await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_list_many() { |
| test_neigh_many(false /* watch_for_changes */, |a, b| format!("{}\n{}\n", a, b)).await |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_watch_many() { |
| test_neigh_many(true /* watch_for_changes */, |a, b| { |
| format!( |
| "EXISTING | {}\n\ |
| EXISTING | {}\n\ |
| IDLE\n", |
| a, b |
| ) |
| }) |
| .await |
| } |
| |
| fn wanted_neigh_list_json() -> String { |
| json!({ |
| "interface": 1, |
| "mac": "01:02:03:04:05:06", |
| "neighbor": "192.168.0.1", |
| "state": "REACHABLE", |
| }) |
| .to_string() |
| } |
| |
| fn wanted_neigh_watch_json() -> String { |
| json!({ |
| "entry": { |
| "interface": 1, |
| "mac": "01:02:03:04:05:06", |
| "neighbor": "192.168.0.1", |
| "state": "REACHABLE", |
| }, |
| "state_change_status": "EXISTING", |
| }) |
| .to_string() |
| } |
| |
| #[test_case(true, false, &wanted_neigh_list_json() ; "in json format, not including entry state")] |
| #[test_case(false, false, "Interface 1 | IP 192.168.0.1 | MAC 01:02:03:04:05:06 | REACHABLE" ; "in tabular format, not including entry state")] |
| #[test_case(true, true, &wanted_neigh_watch_json() ; "in json format, including entry state")] |
| #[test_case(false, true, "EXISTING | Interface 1 | IP 192.168.0.1 | MAC 01:02:03:04:05:06 | REACHABLE" ; "in tabular format, including entry state")] |
| fn neigh_write_entry(json: bool, include_entry_state: bool, wanted_output: &str) { |
| let entry = fneighbor::EntryIteratorItem::Existing(fneighbor::Entry { |
| interface: Some(1), |
| neighbor: Some(IF_ADDR_V4.addr), |
| state: Some(fneighbor::EntryState::Reachable), |
| mac: Some(MAC_1), |
| updated_at: Some(timestamp_60s_ago()), |
| ..Default::default() |
| }); |
| |
| let mut output = if json { |
| ffx_writer::Writer::new_test(Some(ffx_writer::Format::Json)) |
| } else { |
| ffx_writer::Writer::new_test(None) |
| }; |
| write_neigh_entry(&mut output, entry, include_entry_state) |
| .expect("write_neigh_entry should succeed"); |
| let got_output = output.test_output().unwrap(); |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(&got_output), |
| trim_whitespace_for_comparison(wanted_output), |
| ); |
| } |
| |
| const INTERFACE_ID: u64 = 1; |
| const IP_VERSION: fnet::IpVersion = fnet::IpVersion::V4; |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_add() { |
| let (controller, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fneighbor::ControllerMarker>().unwrap(); |
| let neigh = do_neigh_add(INTERFACE_ID, IF_ADDR_V4.addr, MAC_1, controller); |
| let neigh_succeeds = async { |
| let (got_interface_id, got_ip_address, got_mac, responder) = requests |
| .try_next() |
| .await |
| .expect("neigh FIDL error") |
| .expect("request stream should not have ended") |
| .into_add_entry() |
| .expect("request should be of type AddEntry"); |
| assert_eq!(got_interface_id, INTERFACE_ID); |
| assert_eq!(got_ip_address, IF_ADDR_V4.addr); |
| assert_eq!(got_mac, MAC_1); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| }; |
| let ((), ()) = futures::future::try_join(neigh, neigh_succeeds) |
| .await |
| .expect("neigh add should succeed"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_clear() { |
| let (controller, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fneighbor::ControllerMarker>().unwrap(); |
| let neigh = do_neigh_clear(INTERFACE_ID, IP_VERSION, controller); |
| let neigh_succeeds = async { |
| let (got_interface_id, got_ip_version, responder) = requests |
| .try_next() |
| .await |
| .expect("neigh FIDL error") |
| .expect("request stream should not have ended") |
| .into_clear_entries() |
| .expect("request should be of type ClearEntries"); |
| assert_eq!(got_interface_id, INTERFACE_ID); |
| assert_eq!(got_ip_version, IP_VERSION); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| }; |
| let ((), ()) = futures::future::try_join(neigh, neigh_succeeds) |
| .await |
| .expect("neigh clear should succeed"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn neigh_del() { |
| let (controller, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fneighbor::ControllerMarker>().unwrap(); |
| let neigh = do_neigh_del(INTERFACE_ID, IF_ADDR_V4.addr, controller); |
| let neigh_succeeds = async { |
| let (got_interface_id, got_ip_address, responder) = requests |
| .try_next() |
| .await |
| .expect("neigh FIDL error") |
| .expect("request stream should not have ended") |
| .into_remove_entry() |
| .expect("request should be of type RemoveEntry"); |
| assert_eq!(got_interface_id, INTERFACE_ID); |
| assert_eq!(got_ip_address, IF_ADDR_V4.addr); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| }; |
| let ((), ()) = futures::future::try_join(neigh, neigh_succeeds) |
| .await |
| .expect("neigh remove should succeed"); |
| } |
| |
| #[test_case(opts::dhcpd::DhcpdEnum::Get(opts::dhcpd::Get { |
| arg: opts::dhcpd::GetArg::Option( |
| opts::dhcpd::OptionArg { |
| name: opts::dhcpd::Option_::SubnetMask( |
| opts::dhcpd::SubnetMask { mask: None }) }), |
| }); "get option")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Get(opts::dhcpd::Get { |
| arg: opts::dhcpd::GetArg::Parameter(opts::dhcpd::ParameterArg { |
| name: opts::dhcpd::Parameter::LeaseLength( |
| opts::dhcpd::LeaseLength { default: None, max: None }), |
| }), |
| }); "get parameter")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Set(opts::dhcpd::Set { |
| arg: opts::dhcpd::SetArg::Option(opts::dhcpd::OptionArg { |
| name: opts::dhcpd::Option_::SubnetMask(opts::dhcpd::SubnetMask { |
| mask: Some(net_declare::std_ip_v4!("255.255.255.0")), |
| }), |
| }), |
| }); "set option")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Set(opts::dhcpd::Set { |
| arg: opts::dhcpd::SetArg::Parameter(opts::dhcpd::ParameterArg { |
| name: opts::dhcpd::Parameter::LeaseLength( |
| opts::dhcpd::LeaseLength { max: Some(42), default: Some(42) }), |
| }), |
| }); "set parameter")] |
| #[test_case(opts::dhcpd::DhcpdEnum::List(opts::dhcpd::List { arg: |
| opts::dhcpd::ListArg::Option(opts::dhcpd::OptionToken {}) }); "list option")] |
| #[test_case(opts::dhcpd::DhcpdEnum::List( |
| opts::dhcpd::List { arg: opts::dhcpd::ListArg::Parameter(opts::dhcpd::ParameterToken {}) }); |
| "list parameter")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Reset(opts::dhcpd::Reset { |
| arg: opts::dhcpd::ResetArg::Option(opts::dhcpd::OptionToken {}) }); "reset option")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Reset( |
| opts::dhcpd::Reset { |
| arg: opts::dhcpd::ResetArg::Parameter(opts::dhcpd::ParameterToken {}) }); |
| "reset parameter")] |
| #[test_case(opts::dhcpd::DhcpdEnum::ClearLeases(opts::dhcpd::ClearLeases {}); "clear leases")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Start(opts::dhcpd::Start {}); "start")] |
| #[test_case(opts::dhcpd::DhcpdEnum::Stop(opts::dhcpd::Stop {}); "stop")] |
| #[fasync::run_singlethreaded(test)] |
| async fn test_do_dhcpd(cmd: opts::dhcpd::DhcpdEnum) { |
| let (dhcpd, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fdhcp::Server_Marker>() |
| .expect("failed to create proxy and request stream for dhcp server"); |
| |
| let connector = TestConnector { dhcpd: Some(dhcpd), ..Default::default() }; |
| let op = do_dhcpd(cmd.clone(), &connector); |
| let op_succeeds = async move { |
| let req = requests |
| .try_next() |
| .await |
| .expect("receiving request") |
| .expect("request stream should not have ended"); |
| match cmd { |
| opts::dhcpd::DhcpdEnum::Get(opts::dhcpd::Get { arg }) => match arg { |
| opts::dhcpd::GetArg::Option(opts::dhcpd::OptionArg { name }) => { |
| let (code, responder) = |
| req.into_get_option().expect("request should be of type get option"); |
| assert_eq!( |
| <opts::dhcpd::Option_ as Into<fdhcp::OptionCode>>::into(name), |
| code |
| ); |
| // We don't care what the value is here, we just need something to give as |
| // an argument to responder.send(). |
| let dummy_result = fdhcp::Option_::SubnetMask(fidl_ip_v4!("255.255.255.0")); |
| let () = responder |
| .send(Ok(&dummy_result)) |
| .expect("responder.send should succeed"); |
| Ok(()) |
| } |
| opts::dhcpd::GetArg::Parameter(opts::dhcpd::ParameterArg { name }) => { |
| let (param, responder) = req |
| .into_get_parameter() |
| .expect("request should be of type get parameter"); |
| assert_eq!( |
| <opts::dhcpd::Parameter as Into<fdhcp::ParameterName>>::into(name), |
| param |
| ); |
| // We don't care what the value is here, we just need something to give as |
| // an argument to responder.send(). |
| let dummy_result = fdhcp::Parameter::Lease(fdhcp::LeaseLength::default()); |
| let () = responder |
| .send(Ok(&dummy_result)) |
| .expect("responder.send should succeed"); |
| Ok(()) |
| } |
| }, |
| opts::dhcpd::DhcpdEnum::Set(opts::dhcpd::Set { arg }) => match arg { |
| opts::dhcpd::SetArg::Option(opts::dhcpd::OptionArg { name }) => { |
| let (opt, responder) = |
| req.into_set_option().expect("request should be of type set option"); |
| assert_eq!(<opts::dhcpd::Option_ as Into<fdhcp::Option_>>::into(name), opt); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| opts::dhcpd::SetArg::Parameter(opts::dhcpd::ParameterArg { name }) => { |
| let (opt, responder) = req |
| .into_set_parameter() |
| .expect("request should be of type set parameter"); |
| assert_eq!( |
| <opts::dhcpd::Parameter as Into<fdhcp::Parameter>>::into(name), |
| opt |
| ); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| }, |
| opts::dhcpd::DhcpdEnum::List(opts::dhcpd::List { arg }) => match arg { |
| opts::dhcpd::ListArg::Option(opts::dhcpd::OptionToken {}) => { |
| let responder = req |
| .into_list_options() |
| .expect("request should be of type list options"); |
| let () = responder.send(Ok(&[])).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| opts::dhcpd::ListArg::Parameter(opts::dhcpd::ParameterToken {}) => { |
| let responder = req |
| .into_list_parameters() |
| .expect("request should be of type list options"); |
| let () = responder.send(Ok(&[])).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| }, |
| opts::dhcpd::DhcpdEnum::Reset(opts::dhcpd::Reset { arg }) => match arg { |
| opts::dhcpd::ResetArg::Option(opts::dhcpd::OptionToken {}) => { |
| let responder = req |
| .into_reset_options() |
| .expect("request should be of type reset options"); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| opts::dhcpd::ResetArg::Parameter(opts::dhcpd::ParameterToken {}) => { |
| let responder = req |
| .into_reset_parameters() |
| .expect("request should be of type reset parameters"); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| }, |
| opts::dhcpd::DhcpdEnum::ClearLeases(opts::dhcpd::ClearLeases {}) => { |
| let responder = |
| req.into_clear_leases().expect("request should be of type clear leases"); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| opts::dhcpd::DhcpdEnum::Start(opts::dhcpd::Start {}) => { |
| let responder = |
| req.into_start_serving().expect("request should be of type start serving"); |
| let () = responder.send(Ok(())).expect("responder.send should succeed"); |
| Ok(()) |
| } |
| opts::dhcpd::DhcpdEnum::Stop(opts::dhcpd::Stop {}) => { |
| let responder = |
| req.into_stop_serving().expect("request should be of type stop serving"); |
| let () = responder.send().expect("responder.send should succeed"); |
| Ok(()) |
| } |
| } |
| }; |
| let ((), ()) = futures::future::try_join(op, op_succeeds) |
| .await |
| .expect("dhcp server command should succeed"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn dns_lookup() { |
| let (lookup, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fname::LookupMarker>().unwrap(); |
| let connector = TestConnector { name_lookup: Some(lookup), ..Default::default() }; |
| |
| let cmd = opts::dns::DnsEnum::Lookup(opts::dns::Lookup { |
| hostname: "example.com".to_string(), |
| ipv4: true, |
| ipv6: true, |
| sort: true, |
| }); |
| let mut output = Vec::new(); |
| let dns_command = do_dns(&mut output, cmd.clone(), &connector) |
| .map(|result| result.expect("dns command should succeed")); |
| |
| let handle_request = async move { |
| let (hostname, options, responder) = requests |
| .try_next() |
| .await |
| .expect("FIDL error") |
| .expect("request stream should not have ended") |
| .into_lookup_ip() |
| .expect("request should be of type LookupIp"); |
| let opts::dns::DnsEnum::Lookup(opts::dns::Lookup { |
| hostname: want_hostname, |
| ipv4, |
| ipv6, |
| sort, |
| }) = cmd; |
| let want_options = fname::LookupIpOptions { |
| ipv4_lookup: Some(ipv4), |
| ipv6_lookup: Some(ipv6), |
| sort_addresses: Some(sort), |
| ..Default::default() |
| }; |
| assert_eq!( |
| hostname, want_hostname, |
| "received IP lookup request for unexpected hostname" |
| ); |
| assert_eq!(options, want_options, "received unexpected IP lookup options"); |
| |
| responder |
| .send(Ok(&fname::LookupResult { |
| addresses: Some(vec![fidl_ip!("203.0.113.1"), fidl_ip!("2001:db8::1")]), |
| ..Default::default() |
| })) |
| .expect("send response"); |
| }; |
| let ((), ()) = futures::future::join(dns_command, handle_request).await; |
| |
| const WANT_OUTPUT: &str = " |
| 203.0.113.1 |
| 2001:db8::1 |
| "; |
| let got_output = std::str::from_utf8(&output).unwrap(); |
| pretty_assertions::assert_eq!( |
| trim_whitespace_for_comparison(got_output), |
| trim_whitespace_for_comparison(WANT_OUTPUT), |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_do_filter() { |
| const CONTROLLER_A: &str = "controller a"; |
| const CONTROLLER_B: &str = "controller b"; |
| const NAMESPACE_A: &str = "namespace a"; |
| const NAMESPACE_B: &str = "namespace b"; |
| const NAMESPACE_C: &str = "namespace c"; |
| const ROUTINE_A: &str = "routine a"; |
| const ROUTINE_B: &str = "routine b"; |
| const ROUTINE_C: &str = "routine c"; |
| const INDEX_FIRST: u32 = 11; |
| const INDEX_SECOND: u32 = 12; |
| const INDEX_THIRD: u32 = 13; |
| |
| let mut output = Vec::new(); |
| let (filter, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_filter::StateMarker>() |
| .expect("failed to create proxy and request stream for filter server"); |
| |
| let connector = TestConnector { filter: Some(filter), ..Default::default() }; |
| let op = do_filter( |
| &mut output, |
| opts::filter::FilterEnum::List(opts::filter::List {}), |
| &connector, |
| ); |
| let op_succeeds = async move { |
| let req = requests |
| .try_next() |
| .await |
| .expect("receiving request") |
| .expect("request stream should not have ended"); |
| let (_options, server_end, _state_control_handle) = |
| req.into_get_watcher().expect("request should be of type GetWatcher"); |
| |
| let mut watcher_request_stream = server_end.into_stream().expect("watcher FIDL error"); |
| |
| let events = [ |
| // controller a, namespace a |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Namespace(fnet_filter_ext::Namespace { |
| id: fnet_filter_ext::NamespaceId(NAMESPACE_A.to_string()), |
| domain: fnet_filter_ext::Domain::Ipv4, |
| }), |
| ) |
| .into(), |
| // controller a, namespace b |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Namespace(fnet_filter_ext::Namespace { |
| id: fnet_filter_ext::NamespaceId(NAMESPACE_B.to_string()), |
| domain: fnet_filter_ext::Domain::Ipv4, |
| }), |
| ) |
| .into(), |
| // controller b, namespace c |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_B.to_string()), |
| fnet_filter_ext::Resource::Namespace(fnet_filter_ext::Namespace { |
| id: fnet_filter_ext::NamespaceId(NAMESPACE_C.to_string()), |
| domain: fnet_filter_ext::Domain::Ipv4, |
| }), |
| ) |
| .into(), |
| // controller a, namespace a, routine a |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Routine(fnet_filter_ext::Routine { |
| id: fnet_filter_ext::RoutineId { |
| namespace: fnet_filter_ext::NamespaceId(NAMESPACE_A.to_string()), |
| name: ROUTINE_A.to_string(), |
| }, |
| routine_type: fnet_filter_ext::RoutineType::Ip(Some(InstalledIpRoutine { |
| priority: 2, |
| hook: fnet_filter_ext::IpHook::Egress, |
| })), |
| }), |
| ) |
| .into(), |
| // controller a, namespace a, routine b |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Routine(fnet_filter_ext::Routine { |
| id: fnet_filter_ext::RoutineId { |
| namespace: fnet_filter_ext::NamespaceId(NAMESPACE_A.to_string()), |
| name: ROUTINE_B.to_string(), |
| }, |
| routine_type: fnet_filter_ext::RoutineType::Ip(Some(InstalledIpRoutine { |
| priority: 1, |
| hook: fnet_filter_ext::IpHook::Egress, |
| })), |
| }), |
| ) |
| .into(), |
| // controller a, namespace b, routine c |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Routine(fnet_filter_ext::Routine { |
| id: fnet_filter_ext::RoutineId { |
| namespace: fnet_filter_ext::NamespaceId(NAMESPACE_B.to_string()), |
| name: ROUTINE_C.to_string(), |
| }, |
| routine_type: fnet_filter_ext::RoutineType::Ip(None), |
| }), |
| ) |
| .into(), |
| // controller a, namespace a, routine a, rule #1 (11) |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Rule(fnet_filter_ext::Rule { |
| id: fnet_filter_ext::RuleId { |
| routine: fnet_filter_ext::RoutineId { |
| namespace: fnet_filter_ext::NamespaceId(NAMESPACE_A.to_string()), |
| name: ROUTINE_A.to_string(), |
| }, |
| index: INDEX_FIRST, |
| }, |
| matchers: Default::default(), |
| action: fnet_filter_ext::Action::Accept, |
| }), |
| ) |
| .into(), |
| // controller a, namespace a, routine a, rule #2 (12) |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Rule(fnet_filter_ext::Rule { |
| id: fnet_filter_ext::RuleId { |
| routine: fnet_filter_ext::RoutineId { |
| namespace: fnet_filter_ext::NamespaceId(NAMESPACE_A.to_string()), |
| name: ROUTINE_A.to_string(), |
| }, |
| index: INDEX_SECOND, |
| }, |
| matchers: Default::default(), |
| action: fnet_filter_ext::Action::Accept, |
| }), |
| ) |
| .into(), |
| // controller a, namespace a, routine b, rule #3 (13) |
| fnet_filter_ext::Event::Existing( |
| fnet_filter_ext::ControllerId(CONTROLLER_A.to_string()), |
| fnet_filter_ext::Resource::Rule(fnet_filter_ext::Rule { |
| id: fnet_filter_ext::RuleId { |
| routine: fnet_filter_ext::RoutineId { |
| namespace: fnet_filter_ext::NamespaceId(NAMESPACE_A.to_string()), |
| name: ROUTINE_B.to_string(), |
| }, |
| index: INDEX_THIRD, |
| }, |
| matchers: Default::default(), |
| action: fnet_filter_ext::Action::Accept, |
| }), |
| ) |
| .into(), |
| fnet_filter_ext::Event::Idle.into(), |
| ]; |
| |
| let () = watcher_request_stream |
| .try_next() |
| .await |
| .expect("watcher watch FIDL error") |
| .expect("watcher request stream should not have ended") |
| .into_watch() |
| .expect("request should be of type Watch") |
| .send(&events) |
| .expect("responder.send should succeed"); |
| |
| assert_matches!( |
| watcher_request_stream.try_next().await.expect("watcher watch FIDL error"), |
| None, |
| "remaining watcher request stream should be empty because client should close \ |
| watcher channel after observing existing resources" |
| ); |
| |
| Ok(()) |
| }; |
| let ((), ()) = futures::future::try_join(op, op_succeeds) |
| .await |
| .expect("filter server command should succeed"); |
| |
| const WANT_OUTPUT: &str = r#"controller: "controller a" { |
| namespace: "namespace a" { |
| domain: Ipv4 |
| routine: "routine a" { |
| type: Ip(Some(InstalledIpRoutine { hook: Egress, priority: 2 })) |
| rule: #11 { |
| matchers: Matchers { in_interface: None, out_interface: None, src_addr: None, dst_addr: None, transport_protocol: None } |
| action: Accept |
| } |
| rule: #12 { |
| matchers: Matchers { in_interface: None, out_interface: None, src_addr: None, dst_addr: None, transport_protocol: None } |
| action: Accept |
| } |
| } |
| routine: "routine b" { |
| type: Ip(Some(InstalledIpRoutine { hook: Egress, priority: 1 })) |
| rule: #13 { |
| matchers: Matchers { in_interface: None, out_interface: None, src_addr: None, dst_addr: None, transport_protocol: None } |
| action: Accept |
| } |
| } |
| } |
| namespace: "namespace b" { |
| domain: Ipv4 |
| routine: "routine c" { |
| type: Ip(None) |
| } |
| } |
| } |
| controller: "controller b" { |
| namespace: "namespace c" { |
| domain: Ipv4 |
| } |
| } |
| "#; |
| let got_output = std::str::from_utf8(&output).unwrap(); |
| pretty_assertions::assert_eq!(got_output, WANT_OUTPUT,); |
| } |
| } |