| // 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 ffilter; |
| 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_stack as fstack; |
| use fidl_fuchsia_net_stack_ext::{self as fstack_ext, FidlReturn as _}; |
| use fidl_fuchsia_netstack as fnetstack; |
| use fuchsia_zircon_status as zx; |
| use futures::{FutureExt as _, StreamExt as _, TryFutureExt as _, TryStreamExt as _}; |
| use log::info; |
| use netfilter::FidlReturn as _; |
| use prettytable::{cell, format, row, Row, Table}; |
| use serde_json::{json, value::Value}; |
| use std::collections::hash_map::HashMap; |
| use std::{convert::TryFrom as _, iter::FromIterator as _, str::FromStr as _}; |
| |
| mod opts; |
| pub use opts::{ |
| underlying_user_facing_error, user_facing_error, Command, CommandEnum, UserFacingError, |
| }; |
| |
| 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<fdhcp::Server_Marker> |
| + ServiceConnector<ffilter::FilterMarker> |
| + ServiceConnector<finterfaces::StateMarker> |
| + ServiceConnector<fneighbor::ControllerMarker> |
| + ServiceConnector<fneighbor::ViewMarker> |
| + ServiceConnector<fnetstack::NetstackMarker> |
| + ServiceConnector<fstack::LogMarker> |
| + ServiceConnector<fstack::StackMarker> |
| + ServiceConnector<fname::LookupMarker> |
| { |
| } |
| |
| impl<O> NetCliDepsConnector for O where |
| O: ServiceConnector<fdebug::InterfacesMarker> |
| + ServiceConnector<fdhcp::Server_Marker> |
| + ServiceConnector<ffilter::FilterMarker> |
| + ServiceConnector<finterfaces::StateMarker> |
| + ServiceConnector<fneighbor::ControllerMarker> |
| + ServiceConnector<fneighbor::ViewMarker> |
| + ServiceConnector<fnetstack::NetstackMarker> |
| + ServiceConnector<fstack::LogMarker> |
| + ServiceConnector<fstack::StackMarker> |
| + ServiceConnector<fname::LookupMarker> |
| { |
| } |
| |
| 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::Filter(opts::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") |
| } |
| } |
| } |
| |
| fn shortlist_interfaces( |
| name_pattern: &str, |
| interfaces: &mut HashMap<u64, finterfaces_ext::Properties>, |
| ) { |
| interfaces.retain( |
| |_: &u64, |
| finterfaces_ext::Properties { |
| id: _, |
| name, |
| device_class: _, |
| online: _, |
| addresses: _, |
| has_default_ipv4_route: _, |
| has_default_ipv6_route: _, |
| }| 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 }) 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 ser::Addresses { ipv4, ipv6 } = addresses; |
| for ser::Subnet { addr, prefix_len } in ipv4 { |
| let () = add_row(&mut t, row!["addr", format!("{}/{}", addr, prefix_len)]); |
| } |
| for ser::Subnet { addr, prefix_len } in ipv6 { |
| let () = add_row(&mut t, row!["addr", format!("{}/{}", addr, prefix_len)]); |
| } |
| 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<fdebug::InterfacesMarker>, |
| { |
| let debug_interfaces = connect_with_context::<fdebug::InterfacesMarker, _>(connector).await?; |
| let (control, server_end) = finterfaces_ext::admin::Control::create_endpoints() |
| .context("create admin control endpoints")?; |
| let () = debug_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), |
| ..finterfaces_admin::Ipv4Configuration::EMPTY |
| }), |
| ..finterfaces_admin::Configuration::EMPTY |
| }, |
| fnet::IpVersion::V6 => finterfaces_admin::Configuration { |
| ipv6: Some(finterfaces_admin::Ipv6Configuration { |
| forwarding: Some(forwarding), |
| ..finterfaces_admin::Ipv6Configuration::EMPTY |
| }), |
| ..finterfaces_admin::Configuration::EMPTY |
| }, |
| } |
| } |
| |
| 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") |
| } |
| } |
| } |
| |
| 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 debug_interfaces = |
| connect_with_context::<fdebug::InterfacesMarker, _>(connector).await?; |
| let interface_state = |
| connect_with_context::<finterfaces::StateMarker, _>(connector).await?; |
| let stream = finterfaces_ext::event_stream_from_state(&interface_state)?; |
| let mut response = finterfaces_ext::existing(stream, HashMap::new()).await?; |
| if let Some(name_pattern) = name_pattern { |
| let () = shortlist_interfaces(&name_pattern, &mut response); |
| } |
| let response = response.into_values().map(|properties| async { |
| let finterfaces_ext::Properties { id, .. } = &properties; |
| let mac = debug_interfaces.get_mac(*id).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(fdebug::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 debug_interfaces = |
| connect_with_context::<fdebug::InterfacesMarker, _>(connector).await?; |
| let interface_state = |
| connect_with_context::<finterfaces::StateMarker, _>(connector).await?; |
| let stream = finterfaces_ext::event_stream_from_state(&interface_state)?; |
| 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(properties) => { |
| let finterfaces_ext::Properties { id, .. } = &properties; |
| let mac = debug_interfaces.get_mac(*id).await.context("call get_mac")?; |
| match mac { |
| Err(fdebug::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::IpForward(opts::IfIpForward { cmd }) => match cmd { |
| opts::IfIpForwardEnum::Show(opts::IfIpForwardShow { 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")?; |
| // TODO(https://fxbug.dev/93439): Call `detach` at the end |
| // (after `add_forwarding_entry` below). This will ensure that |
| // the address is only added if all intermediate operations |
| // succeed. In the meantime, `detach` is called before the |
| // `assignment_state_stream` is operated on to ensure that it is |
| // not racy. |
| let () = address_state_provider.detach().context("detach address lifetime")?; |
| let () = control |
| .add_address( |
| &mut subnet.into(), |
| finterfaces_admin::AddressParameters::EMPTY, |
| server_end, |
| ) |
| .context("call add address")?; |
| let state_stream = |
| finterfaces_ext::admin::assignment_state_stream(address_state_provider); |
| |
| state_stream |
| .try_filter_map(|state| { |
| futures::future::ok(match state { |
| finterfaces_admin::AddressAssignmentState::Tentative => None, |
| finterfaces_admin::AddressAssignmentState::Assigned => Some(()), |
| finterfaces_admin::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." |
| ) |
| })?; |
| |
| if !no_subnet_route { |
| let stack: fstack::StackProxy = |
| connect_with_context::<fstack::StackMarker, _>(connector).await?; |
| let mut forwarding_entry = fstack::ForwardingEntry { |
| subnet: fnet_ext::apply_subnet_mask(subnet.into()), |
| device_id: id, |
| next_hop: None, |
| metric: 0, |
| }; |
| let () = fstack_ext::exec_fidl!( |
| stack.add_forwarding_entry(&mut forwarding_entry), |
| "error adding forwarding entry" |
| )?; |
| info!("Added forwarding entry {:?}", forwarding_entry); |
| } |
| |
| 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 mut 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(&mut 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::IfEnum::Bridge(opts::IfBridge { interfaces }) => { |
| let netstack: fnetstack::NetstackProxy = |
| connect_with_context::<fnetstack::NetstackMarker, _>(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)?; |
| let response = finterfaces_ext::existing(stream, HashMap::new()).await?; |
| Ok::<HashMap<String, u64>, Error>( |
| response |
| .into_iter() |
| .map( |
| |( |
| id, |
| finterfaces_ext::Properties { |
| name, |
| id: _, |
| device_class: _, |
| online: _, |
| addresses: _, |
| has_default_ipv4_route: _, |
| has_default_ipv6_route: _, |
| }, |
| )| (name, id), |
| ) |
| .collect(), |
| ) |
| }; |
| |
| let num_interfaces = interfaces.len(); |
| |
| let (_name_to_id, ids): (Option<HashMap<String, u64>>, Vec<u32>) = |
| 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( |
| u32::try_from(id) |
| .with_context(|| format!("nicid {} does not fit in u32", id))?, |
| ); |
| Ok((name_to_id, ids)) |
| }, |
| ) |
| .await?; |
| |
| let bridge_id = netstack |
| .bridge_interfaces(&ids) |
| .await |
| .map_err(anyhow::Error::new) |
| .and_then(|result| match result { |
| fnetstack::Result_::Message(message) => Err(anyhow::Error::msg(message)), |
| fnetstack::Result_::Nicid(id) => Ok(id), |
| }) |
| .with_context(|| format!("bridge_interfaces({:?}", ids))?; |
| 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> { |
| let stack = connect_with_context::<fstack::StackMarker, _>(connector).await?; |
| match cmd { |
| opts::RouteEnum::List(opts::RouteList {}) => { |
| let response = |
| stack.get_forwarding_table().await.context("error retrieving forwarding table")?; |
| if out.is_machine() { |
| let response: Vec<_> = response |
| .into_iter() |
| .map(fstack_ext::ForwardingEntry::from) |
| .map(ser::ForwardingEntry::from) |
| .collect(); |
| out.machine(&response).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"]); |
| for entry in response { |
| let fstack_ext::ForwardingEntry { subnet, device_id, next_hop, metric } = |
| entry.into(); |
| 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(&mut t, row![subnet, next_hop, device_id, metric]); |
| } |
| |
| let _lines_printed: usize = t.print(out)?; |
| out.line("")?; |
| } |
| } |
| opts::RouteEnum::Add(route) => { |
| let nicid = route.interface.find_u32_nicid(connector).await?; |
| let mut entry = route.into_route_table_entry(nicid); |
| let () = fstack_ext::exec_fidl!( |
| stack.add_forwarding_entry(&mut entry), |
| "error adding next-hop forwarding entry" |
| )?; |
| } |
| opts::RouteEnum::Del(route) => { |
| let nicid = route.interface.find_u32_nicid(connector).await?; |
| let mut entry = route.into_route_table_entry(nicid); |
| let () = fstack_ext::exec_fidl!( |
| stack.del_forwarding_entry(&mut entry), |
| "error removing forwarding entry" |
| )?; |
| } |
| } |
| Ok(()) |
| } |
| |
| async fn do_filter<C: NetCliDepsConnector, W: std::io::Write>( |
| mut out: W, |
| cmd: opts::FilterEnum, |
| connector: &C, |
| ) -> Result<(), Error> { |
| let filter = connect_with_context::<ffilter::FilterMarker, _>(connector).await?; |
| match cmd { |
| opts::FilterEnum::GetRules(opts::FilterGetRules {}) => { |
| let (rules, generation): (Vec<ffilter::Rule>, u32) = |
| filter_fidl!(filter.get_rules(), "error getting filter rules")?; |
| writeln!(out, "{:?} (generation {})", rules, generation)?; |
| } |
| opts::FilterEnum::SetRules(opts::FilterSetRules { rules }) => { |
| let (_cur_rules, generation) = |
| filter_fidl!(filter.get_rules(), "error getting filter rules")?; |
| let mut rules = netfilter::parser::parse_str_to_rules(&rules)?; |
| let () = filter_fidl!( |
| filter.update_rules(&mut rules.iter_mut(), generation), |
| "error setting filter rules" |
| )?; |
| info!("successfully set filter rules"); |
| } |
| opts::FilterEnum::GetNatRules(opts::FilterGetNatRules {}) => { |
| let (rules, generation): (Vec<ffilter::Nat>, u32) = |
| filter_fidl!(filter.get_nat_rules(), "error getting NAT rules")?; |
| writeln!(out, "{:?} (generation {})", rules, generation)?; |
| } |
| opts::FilterEnum::SetNatRules(opts::FilterSetNatRules { rules }) => { |
| let (_cur_rules, generation) = |
| filter_fidl!(filter.get_nat_rules(), "error getting NAT rules")?; |
| let mut rules = netfilter::parser::parse_str_to_nat_rules(&rules)?; |
| let () = filter_fidl!( |
| filter.update_nat_rules(&mut rules.iter_mut(), generation), |
| "error setting NAT rules" |
| )?; |
| info!("successfully set NAT rules"); |
| } |
| opts::FilterEnum::GetRdrRules(opts::FilterGetRdrRules {}) => { |
| let (rules, generation): (Vec<ffilter::Rdr>, u32) = |
| filter_fidl!(filter.get_rdr_rules(), "error getting RDR rules")?; |
| writeln!(out, "{:?} (generation {})", rules, generation)?; |
| } |
| opts::FilterEnum::SetRdrRules(opts::FilterSetRdrRules { rules }) => { |
| let (_cur_rules, generation) = |
| filter_fidl!(filter.get_rdr_rules(), "error getting RDR rules")?; |
| let mut rules = netfilter::parser::parse_str_to_rdr_rules(&rules)?; |
| let () = filter_fidl!( |
| filter.update_rdr_rules(&mut rules.iter_mut(), generation), |
| "error setting RDR rules" |
| )?; |
| info!("successfully set RDR rules"); |
| } |
| } |
| 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 view = connect_with_context::<fneighbor::ViewMarker, _>(connector).await?; |
| let () = print_neigh_config(interface, ip_version, view) |
| .await |
| .context("failed during neigh config get command")?; |
| } |
| opts::NeighConfigEnum::Update(opts::NeighUpdateConfig { |
| interface, |
| ip_version, |
| base_reachable_time, |
| learn_base_reachable_time, |
| min_random_factor, |
| max_random_factor, |
| retransmit_timer, |
| learn_retransmit_timer, |
| delay_first_probe_time, |
| max_multicast_probes, |
| max_unicast_probes, |
| max_anycast_delay_time, |
| max_reachability_confirmations, |
| }) => { |
| let updates = fneighbor::UnreachabilityConfig { |
| base_reachable_time, |
| learn_base_reachable_time, |
| min_random_factor, |
| max_random_factor, |
| retransmit_timer, |
| learn_retransmit_timer, |
| delay_first_probe_time, |
| max_multicast_probes, |
| max_unicast_probes, |
| max_anycast_delay_time, |
| max_reachability_confirmations, |
| ..fneighbor::UnreachabilityConfig::EMPTY |
| }; |
| let interface = interface.find_nicid(connector).await?; |
| let controller = |
| connect_with_context::<fneighbor::ControllerMarker, _>(connector).await?; |
| let () = update_neigh_config(interface, ip_version, updates, controller) |
| .await |
| .context("failed during neigh config update command")?; |
| info!("Updated config for interface {}", interface); |
| } |
| }, |
| } |
| Ok(()) |
| } |
| |
| async fn do_neigh_add( |
| interface: u64, |
| neighbor: fnet::IpAddress, |
| mac: fnet::MacAddress, |
| controller: fneighbor::ControllerProxy, |
| ) -> Result<(), Error> { |
| controller |
| .add_entry(interface, &mut neighbor.into(), &mut 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, &mut 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>() |
| .context("error creating channel for entry iterator")?; |
| let it = it_client.into_proxy().context("error creating proxy to entry iterator")?; |
| |
| let () = view |
| .open_entry_iterator(it_server, fneighbor::EntryIteratorOptions::EMPTY) |
| .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(()) |
| } |
| |
| async fn print_neigh_config( |
| interface: u64, |
| version: fnet::IpVersion, |
| view: fneighbor::ViewProxy, |
| ) -> Result<(), Error> { |
| let config = view |
| .get_unreachability_config(interface, version) |
| .await |
| .context("get_unreachability_config FIDL error")? |
| .map_err(zx::Status::from_raw) |
| .context("get_unreachability_config failed")?; |
| |
| println!("{:#?}", config); |
| Ok(()) |
| } |
| |
| async fn update_neigh_config( |
| interface: u64, |
| version: fnet::IpVersion, |
| updates: fneighbor::UnreachabilityConfig, |
| controller: fneighbor::ControllerProxy, |
| ) -> Result<(), Error> { |
| controller |
| .update_unreachability_config(interface, version, updates) |
| .await |
| .context("update_unreachability_config FIDL error")? |
| .map_err(zx::Status::from_raw) |
| .context("update_unreachability_config failed") |
| } |
| |
| 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().map_or(false, |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(&mut 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(&mut 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), |
| ..fname::LookupIpOptions::EMPTY |
| }, |
| ) |
| .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(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use anyhow::Error; |
| use assert_matches::assert_matches; |
| use fidl::endpoints::ProtocolMarker; |
| use fidl_fuchsia_hardware_network as fhardware_network; |
| use fuchsia_async::{self as fasync, TimeoutExt as _}; |
| use net_declare::{fidl_ip, fidl_ip_v4}; |
| use std::convert::TryInto as _; |
| use test_case::test_case; |
| |
| const IF_ADDR_V4: fnet::Subnet = net_declare::fidl_subnet!("192.168.0.1/32"); |
| const IF_ADDR_V6: fnet::Subnet = net_declare::fidl_subnet!("fd00::1/64"); |
| |
| const MAC_1: fnet::MacAddress = net_declare::fidl_mac!("01:02:03:04:05:06"); |
| const MAC_2: fnet::MacAddress = net_declare::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>, |
| netstack: Option<fnetstack::NetstackProxy>, |
| stack: Option<fstack::StackProxy>, |
| name_lookup: Option<fname::LookupProxy>, |
| } |
| |
| #[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<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::FilterMarker> for TestConnector { |
| async fn connect(&self) -> Result<<ffilter::FilterMarker as ProtocolMarker>::Proxy, Error> { |
| Err(anyhow::anyhow!("connect filter 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<fnetstack::NetstackMarker> for TestConnector { |
| async fn connect( |
| &self, |
| ) -> Result<<fnetstack::NetstackMarker as ProtocolMarker>::Proxy, Error> { |
| self.netstack |
| .as_ref() |
| .cloned() |
| .ok_or(anyhow::anyhow!("connector has no netstack instance")) |
| } |
| } |
| |
| #[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<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")) |
| } |
| } |
| |
| 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, |
| 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, properties) |
| }) |
| .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 (debug_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fdebug::InterfacesMarker>().unwrap(); |
| let connector = |
| TestConnector { debug_interfaces: Some(debug_interfaces), ..Default::default() }; |
| |
| let requests_fut = async { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("debug request stream not ended") |
| .expect("debug 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 (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_ip_forwarding(configuration, ip_version) |
| .expect("extract IP forwarding configuration"), |
| enable |
| ); |
| // net-cli does not check the returned configuration so we do not |
| // return a populated one. |
| let () = responder |
| .send(&mut Ok(finterfaces_admin::Configuration::EMPTY)) |
| .expect("responder.send should succeed"); |
| Ok(()) |
| }; |
| 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) |
| .await |
| .expect("setting interface ip forwarding should succeed"); |
| |
| let requests_fut = async { |
| let (id, control, _control_handle) = requests |
| .next() |
| .await |
| .expect("debug request stream not ended") |
| .expect("debug 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 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(&mut Ok(configuration_with_ip_forwarding_set(ip_version, enable))) |
| .expect("responder.send should succeed"); |
| Ok(()) |
| }; |
| 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::Show(opts::IfIpForwardShow { |
| interface: interface1.identifier(false /* use_ifname */), |
| ip_version, |
| }), |
| }), |
| &connector, |
| ); |
| let ((), ()) = futures::future::try_join(do_if_fut, requests_fut) |
| .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 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 mut 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(&mut 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 (debug_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fdebug::InterfacesMarker>().unwrap(); |
| |
| let (stack_proxy, stack_stream) = match (!no_subnet_route) |
| .then(|| fidl::endpoints::create_proxy_and_stream::<fstack::StackMarker>().unwrap()) |
| { |
| Some((proxy, stream)) => (Some(proxy), Some(stream)), |
| None => (None, None), |
| }; |
| |
| let connector = TestConnector { |
| debug_interfaces: Some(debug_interfaces), |
| stack: stack_proxy, |
| ..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("debug request stream not ended") |
| .expect("debug 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); |
| |
| 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_admin::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_admin::AddressAssignmentState::Assigned |
| } else { |
| finterfaces_admin::AddressAssignmentState::Unavailable |
| }) |
| .expect("send address assignment state succeeds"); |
| }; |
| |
| let stack_fut = async { |
| if let Some(mut stack_requests) = stack_stream { |
| let (entry, responder) = stack_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, |
| fstack::ForwardingEntry { |
| subnet: fnet_ext::apply_subnet_mask(fnet::Subnet { |
| addr: IF_ADDR_V6.addr, |
| prefix_len: TEST_PREFIX_LENGTH |
| }), |
| device_id: interface1.nicid, |
| next_hop: None, |
| metric: 0, |
| } |
| ); |
| responder.send(&mut Ok(())).expect("responder.send should succeed"); |
| } |
| }; |
| |
| let ((), (), ()) = futures::join!(admin_fut, do_if_fut, stack_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 (debug_interfaces, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fdebug::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(); |
| futures::pin_mut!(interfaces_fut); |
| |
| let connector = TestConnector { |
| debug_interfaces: Some(debug_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("debug request stream not ended") |
| .expect("debug 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(&mut 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("debug request stream not ended") |
| .expect("debug 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(&mut 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)); |
| }, |
| } |
| } |
| } |
| |
| 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, |
| }, |
| { |
| "addresses": { |
| "ipv4": [], |
| "ipv6": [], |
| }, |
| "device_class": "Ethernet", |
| "mac": "01:02:03:04:05:06", |
| "name": "eth001", |
| "nicid": 10, |
| "online": true, |
| }, |
| { |
| "addresses": { |
| "ipv4": [], |
| "ipv6": [], |
| }, |
| "device_class": "Virtual", |
| "mac": null, |
| "name": "virt001", |
| "nicid": 20, |
| "online": true, |
| }, |
| ]) |
| .to_string() |
| } |
| |
| fn wanted_net_if_list_tabular() -> String { |
| String::from( |
| r#" |
| nicid 1 |
| name lo |
| device class loopback |
| online true |
| mac 00:00:00:00:00:00 |
| |
| nicid 10 |
| name eth001 |
| device class ethernet |
| online true |
| mac 01:02:03:04:05:06 |
| |
| nicid 20 |
| name virt001 |
| device class virtual |
| online true |
| 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 (debug_interfaces, debug_interfaces_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fdebug::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 { |
| debug_interfaces: Some(debug_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, |
| ), |
| ] |
| .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 mut 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), |
| name: Some(name), |
| device_class: Some(device_class), |
| online: Some(online), |
| addresses: Some( |
| addresses |
| .into_iter() |
| .map(|finterfaces_ext::Address { addr, valid_until }| { |
| finterfaces::Address { |
| addr: Some(addr), |
| valid_until: Some(valid_until), |
| ..finterfaces::Address::EMPTY |
| } |
| }) |
| .collect(), |
| ), |
| has_default_ipv4_route: Some(has_default_ipv4_route), |
| has_default_ipv6_route: Some(has_default_ipv6_route), |
| ..finterfaces::Properties::EMPTY |
| }) |
| }, |
| ); |
| let () = responder.send(&mut event).expect("send watcher event"); |
| futures::future::ready(()) |
| } |
| }); |
| let debug_fut = debug_interfaces_stream |
| .map(|res| res.expect("debug interfaces stream error")) |
| .for_each_concurrent(None, |req| { |
| let (id, responder) = req.into_get_mac().expect("get_mac request"); |
| let () = responder |
| .send( |
| &mut mac_addresses |
| .get(&id) |
| .copied() |
| .map(|option| option.map(Box::new)) |
| .ok_or(fdebug::InterfacesGetMacError::NotFound), |
| ) |
| .expect("send get_mac response"); |
| futures::future::ready(()) |
| }); |
| let ((), (), ()) = futures::future::join3(do_if_fut, watcher_fut, debug_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(&mut 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(&mut 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(&mut 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.1","prefix_len":24}, |
| "gateway":"1.1.1.2", |
| "metric":4, |
| "nicid":3 |
| }, |
| { |
| "destination":{"addr":"10.10.10.10","prefix_len":24}, |
| "gateway":"10.10.10.20", |
| "metric":40, |
| "nicid":30 |
| } |
| ]) |
| .to_string() |
| } |
| |
| fn wanted_route_list_tabular() -> String { |
| "Destination Gateway NICID Metric |
| 1.1.1.1/24 1.1.1.2 3 4 |
| 10.10.10.10/24 10.10.10.20 30 40" |
| .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 (stack_controller, mut stack_requests) = |
| fidl::endpoints::create_proxy_and_stream::<fstack::StackMarker>().unwrap(); |
| let connector = TestConnector { stack: Some(stack_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 requests_fut = async { |
| let responder = stack_requests |
| .try_next() |
| .await |
| .expect("stack FIDL error") |
| .expect("request stream should not have ended") |
| .into_get_forwarding_table() |
| .expect("request should be of type GetForwardingTable"); |
| let () = responder |
| .send( |
| &mut vec![ |
| fstack::ForwardingEntry { |
| subnet: fnet::Subnet { |
| addr: fnet_ext::IpAddress::from_str("1.1.1.1")?.into(), |
| prefix_len: 24, |
| }, |
| device_id: 3, |
| next_hop: Some(Box::new( |
| fnet_ext::IpAddress::from_str("1.1.1.2")?.into(), |
| )), |
| metric: 4, |
| }, |
| fstack::ForwardingEntry { |
| subnet: fnet::Subnet { |
| addr: fnet_ext::IpAddress::from_str("10.10.10.10")?.into(), |
| prefix_len: 24, |
| }, |
| device_id: 30, |
| next_hop: Some(Box::new( |
| fnet_ext::IpAddress::from_str("10.10.10.20")?.into(), |
| )), |
| metric: 40, |
| }, |
| ] |
| .iter_mut(), |
| ) |
| .expect("responder.send should succeed"); |
| Ok(()) |
| }; |
| let ((), ()) = futures::future::try_join(do_route_fut, requests_fut) |
| .await |
| .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 (netstack, mut requests) = |
| fidl::endpoints::create_proxy_and_stream::<fnetstack::NetstackMarker>().unwrap(); |
| let (interfaces_state, interfaces_state_requests) = |
| fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>().unwrap(); |
| let connector = TestConnector { |
| netstack: Some(netstack), |
| interfaces_state: Some(interfaces_state), |
| ..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, netstack_responder) = requests |
| .try_next() |
| .await |
| .expect("bridge_interfaces 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| u32::try_from(interface.nicid).unwrap_or_else(|_| panic!( |
| "nicid {} does not fit in u32", |
| interface.nicid |
| ))) |
| .collect::<Vec<_>>() |
| ); |
| let () = netstack_responder |
| .send(&mut fnetstack::Result_::Nicid(bridge_id)) |
| .expect("responder.send should succeed"); |
| 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 mut 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(&mut items.iter_mut()).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), |
| ..fneighbor::Entry::EMPTY |
| } |
| } |
| |
| 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), |
| ..fneighbor::Entry::EMPTY |
| } |
| } |
| |
| 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()), |
| ..fneighbor::Entry::EMPTY |
| }); |
| |
| 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(&mut 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 <
|