| // Copyright 2019 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. |
| |
| //! A simple port manager. |
| |
| use { |
| crate::address::{subnet_mask_to_prefix_length, to_ip_addr, LifIpAddr}, |
| crate::error, |
| crate::lifmgr::{self, LIFProperties}, |
| crate::oir, |
| crate::DnsPolicy, |
| anyhow::{Context as _, Error}, |
| fidl_fuchsia_net as fnet, |
| fidl_fuchsia_net_name::{LookupAdminMarker, LookupAdminProxy}, |
| fidl_fuchsia_net_stack::{ |
| self as stack, ForwardingDestination, ForwardingEntry, InterfaceInfo, StackMarker, |
| StackProxy, |
| }, |
| fidl_fuchsia_net_stack_ext::FidlReturn, |
| fidl_fuchsia_netstack::{self as netstack, NetstackMarker, NetstackProxy}, |
| fidl_fuchsia_router_config as netconfig, |
| fuchsia_component::client::connect_to_service, |
| fuchsia_zircon as zx, |
| std::convert::TryFrom, |
| std::net::IpAddr, |
| }; |
| |
| /// The port ID's used by the netstack. |
| /// |
| /// This is what is passed in the stack FIDL to denote the port or nic id. |
| #[derive(Eq, PartialEq, Hash, Debug, Copy, Clone)] |
| pub struct StackPortId(u64); |
| impl From<u64> for StackPortId { |
| fn from(p: u64) -> StackPortId { |
| StackPortId(p) |
| } |
| } |
| impl From<PortId> for StackPortId { |
| fn from(p: PortId) -> StackPortId { |
| // TODO(dpradilla): This should be based on the mapping between physical location and |
| // logical (from management plane point of view) port id. |
| StackPortId::from(p.to_u64()) |
| } |
| } |
| impl StackPortId { |
| /// Performs the conversion to `u64`, some FIDL interfaces need the ID as a `u64`. |
| pub fn to_u64(self) -> u64 { |
| self.0 |
| } |
| /// Performs the conversion to `u32`, some FIDL interfaces need the ID as a `u32`. |
| pub fn to_u32(self) -> u32 { |
| match u32::try_from(self.0) { |
| Ok(v) => v, |
| e => { |
| warn!("overflow converting StackPortId {:?}: {:?}", self.0, e); |
| self.0 as u32 |
| } |
| } |
| } |
| } |
| |
| #[derive(Eq, PartialEq, Hash, Debug, Copy, Clone)] |
| pub struct PortId(u64); |
| impl From<u64> for PortId { |
| fn from(p: u64) -> PortId { |
| PortId(p) |
| } |
| } |
| impl From<StackPortId> for PortId { |
| // TODO(dpradilla): This should be based on the mapping between physical location and |
| // logical (from management plane point of view) port id. |
| fn from(p: StackPortId) -> PortId { |
| PortId(p.to_u64()) |
| } |
| } |
| impl PortId { |
| /// Performs the conversion to `u64`, some FIDL interfaces need the ID as a `u64`. |
| pub fn to_u64(self) -> u64 { |
| self.0 |
| } |
| |
| /// Performs the conversion to `u32`, some FIDL interfaces need the ID as a `u32`. |
| pub fn to_u32(self) -> u32 { |
| match u32::try_from(self.0) { |
| Ok(v) => v, |
| e => { |
| warn!("overflow converting StackPortId {:?}: {:?}", self.0, e); |
| self.0 as u32 |
| } |
| } |
| } |
| } |
| |
| /// Route table entry. |
| #[derive(PartialEq, Debug, Clone)] |
| pub struct Route { |
| /// Target network address. |
| pub target: LifIpAddr, |
| /// Next hop to reach `target`. |
| pub gateway: Option<IpAddr>, |
| /// Port to reach `gateway`. |
| pub port_id: Option<PortId>, |
| /// Represents the route priority. |
| pub metric: Option<u32>, |
| } |
| |
| impl From<&ForwardingEntry> for Route { |
| fn from(r: &ForwardingEntry) -> Self { |
| let (gateway, port_id) = match r.destination { |
| ForwardingDestination::DeviceId(id) => (None, Some(PortId::from(id))), |
| ForwardingDestination::NextHop(gateway) => (Some(to_ip_addr(gateway)), None), |
| }; |
| Route { |
| target: LifIpAddr { address: to_ip_addr(r.subnet.addr), prefix: r.subnet.prefix_len }, |
| gateway, |
| port_id, |
| metric: None, |
| } |
| } |
| } |
| |
| impl From<&netstack::RouteTableEntry2> for Route { |
| fn from(r: &netstack::RouteTableEntry2) -> Self { |
| Route { |
| target: LifIpAddr { |
| address: to_ip_addr(r.destination), |
| prefix: subnet_mask_to_prefix_length(r.netmask), |
| }, |
| gateway: r.gateway.as_ref().map(|g| to_ip_addr(**g)), |
| port_id: Some(StackPortId::from(r.nicid as u64).into()), |
| metric: Some(r.metric), |
| } |
| } |
| } |
| |
| pub struct NetCfg { |
| stack: StackProxy, |
| netstack: NetstackProxy, |
| lookup_admin: LookupAdminProxy, |
| } |
| |
| #[derive(Debug)] |
| pub struct Port { |
| pub id: PortId, |
| pub path: String, |
| } |
| |
| #[derive(Debug, Eq, PartialEq)] |
| pub enum InterfaceAddress { |
| Unknown(LifIpAddr), |
| Static(LifIpAddr), |
| Dhcp(LifIpAddr), |
| } |
| |
| #[derive(Debug, Eq, PartialEq)] |
| pub enum InterfaceState { |
| Unknown, |
| Up, |
| Down, |
| } |
| |
| #[derive(Debug, Eq, PartialEq)] |
| pub struct Interface { |
| pub id: PortId, |
| pub topo_path: String, |
| pub name: String, |
| pub ipv4_addr: Option<InterfaceAddress>, |
| pub ipv6_addr: Vec<LifIpAddr>, |
| pub enabled: bool, |
| pub state: InterfaceState, |
| pub dhcp_client_enabled: bool, |
| } |
| |
| impl Interface { |
| pub fn get_address_v4(&self) -> Option<LifIpAddr> { |
| self.ipv4_addr.as_ref().map(|a| match a { |
| InterfaceAddress::Unknown(a) => *a, |
| InterfaceAddress::Static(a) => *a, |
| InterfaceAddress::Dhcp(a) => *a, |
| }) |
| } |
| |
| pub fn get_address_v6(&self) -> Vec<LifIpAddr> { |
| self.ipv6_addr.clone() |
| } |
| } |
| |
| fn address_is_valid_unicast(addr: &IpAddr) -> bool { |
| // TODO(guzt): This should also check for special-purpose addresses as defined by rfc6890. |
| !addr.is_loopback() && !addr.is_unspecified() && !addr.is_multicast() |
| } |
| |
| impl From<&InterfaceInfo> for Interface { |
| fn from(iface: &InterfaceInfo) -> Self { |
| Interface { |
| id: iface.id.into(), |
| topo_path: iface.properties.topopath.clone(), |
| name: iface.properties.name.clone(), |
| ipv4_addr: iface |
| .properties |
| .addresses |
| .iter() |
| .filter_map(|a| match a.ip_address { |
| // Only return interfaces with an IPv4 address |
| // TODO(dpradilla) support interfaces with multiple IPs? (is there |
| // a use case given this context?) |
| fnet::IpAddress::Ipv4(_) => { |
| if address_is_valid_unicast(&LifIpAddr::from(&a.ip_address).address) { |
| Some(InterfaceAddress::Unknown(LifIpAddr::from(a))) |
| } else { |
| None |
| } |
| } |
| _ => None, |
| }) |
| .next(), |
| ipv6_addr: iface |
| .properties |
| .addresses |
| .iter() |
| .filter_map(|a| match a.ip_address { |
| // Only return Ipv6 addresses |
| fnet::IpAddress::Ipv6(_) => Some(LifIpAddr::from(a)), |
| _ => None, |
| }) |
| .collect(), |
| |
| enabled: match iface.properties.administrative_status { |
| stack::AdministrativeStatus::Enabled => true, |
| stack::AdministrativeStatus::Disabled => false, |
| }, |
| state: match iface.properties.physical_status { |
| stack::PhysicalStatus::Up => InterfaceState::Up, |
| stack::PhysicalStatus::Down => InterfaceState::Down, |
| }, |
| dhcp_client_enabled: false, |
| } |
| } |
| } |
| |
| fn valid_unicast_address_or_none(addr: LifIpAddr) -> Option<LifIpAddr> { |
| if address_is_valid_unicast(&addr.address) { |
| Some(addr) |
| } else { |
| None |
| } |
| } |
| |
| impl From<&netstack::NetInterface> for Interface { |
| fn from(iface: &netstack::NetInterface) -> Self { |
| let dhcp = iface.flags & netstack::NET_INTERFACE_FLAG_DHCP != 0; |
| let addr = valid_unicast_address_or_none(LifIpAddr { |
| address: to_ip_addr(iface.addr), |
| prefix: subnet_mask_to_prefix_length(iface.netmask), |
| }); |
| Interface { |
| id: PortId(iface.id.into()), |
| topo_path: iface.name.clone(), |
| name: iface.name.clone(), |
| ipv4_addr: addr.map(|a| { |
| if dhcp { |
| InterfaceAddress::Dhcp(a) |
| } else { |
| InterfaceAddress::Static(a) |
| } |
| }), |
| ipv6_addr: iface.ipv6addrs.iter().map(|a| LifIpAddr::from(a)).collect(), |
| enabled: (iface.flags & netstack::NET_INTERFACE_FLAG_UP) != 0, |
| state: InterfaceState::Unknown, |
| dhcp_client_enabled: dhcp, |
| } |
| } |
| } |
| |
| impl Into<LIFProperties> for Interface { |
| fn into(self) -> LIFProperties { |
| LIFProperties { |
| dhcp: if self.dhcp_client_enabled { lifmgr::Dhcp::Client } else { lifmgr::Dhcp::None }, |
| dhcp_config: None, |
| address_v4: self.get_address_v4(), |
| address_v6: self.get_address_v6(), |
| enabled: self.enabled, |
| } |
| } |
| } |
| |
| impl NetCfg { |
| pub fn new() -> Result<Self, Error> { |
| let stack = connect_to_service::<StackMarker>() |
| .context("network_manager failed to connect to netstack")?; |
| let netstack = connect_to_service::<NetstackMarker>() |
| .context("network_manager failed to connect to netstack")?; |
| let lookup_admin = connect_to_service::<LookupAdminMarker>() |
| .context("network_manager failed to connect to lookup admin")?; |
| Ok(NetCfg { stack, netstack, lookup_admin }) |
| } |
| |
| /// Returns event streams for fuchsia.fnet.stack and fuchsia.netstack. |
| pub fn take_event_streams( |
| &mut self, |
| ) -> (stack::StackEventStream, netstack::NetstackEventStream) { |
| (self.stack.take_event_stream(), self.netstack.take_event_stream()) |
| } |
| |
| /// Returns the interface associated with the specified port. |
| pub async fn get_interface(&mut self, port: u64) -> Option<Interface> { |
| match self.stack.get_interface_info(port).await { |
| Ok(Ok(info)) => Some((&info).into()), |
| _ => None, |
| } |
| } |
| |
| /// Returns all physical ports in the system. |
| pub async fn ports(&self) -> error::Result<Vec<Port>> { |
| let ports = self.stack.list_interfaces().await.map_err(|_| error::Hal::OperationFailed)?; |
| let p = ports |
| .into_iter() |
| .filter(|x| x.properties.topopath != "") |
| .map(|x| Port { id: StackPortId::from(x.id).into(), path: x.properties.topopath }) |
| .collect::<Vec<Port>>(); |
| Ok(p) |
| } |
| |
| /// Returns all L3 interfaces with valid, non-local IPs in the system. |
| pub async fn interfaces(&mut self) -> error::Result<Vec<Interface>> { |
| let ifs = self |
| .stack |
| .list_interfaces() |
| .await |
| .map_err(|_| error::Hal::OperationFailed)? |
| .iter() |
| .map(|i| i.into()) |
| .filter(|i: &Interface| i.ipv4_addr.is_some()) |
| .collect(); |
| Ok(ifs) |
| } |
| |
| /// Creates a new interface, bridging the given ports. |
| pub async fn create_bridge(&mut self, ports: Vec<PortId>) -> error::Result<Interface> { |
| let (error, bridge_id) = self |
| .netstack |
| .bridge_interfaces( |
| &ports.into_iter().map(|id| StackPortId::from(id).to_u32()).collect::<Vec<_>>(), |
| ) |
| .await |
| .map_err(|e| { |
| error!("Failed creating bridge {:?}", e); |
| error::NetworkManager::Hal(error::Hal::OperationFailed) |
| })?; |
| if error.status != netstack::Status::Ok { |
| error!("Failed creating bridge {:?}", error); |
| return Err(error::NetworkManager::Hal(error::Hal::OperationFailed)); |
| } |
| info!("bridge created {:?}", bridge_id); |
| if let Some(i) = self.get_interface(bridge_id.into()).await { |
| Ok(i) |
| } else { |
| Err(error::NetworkManager::Hal(error::Hal::BridgeNotFound)) |
| } |
| } |
| |
| /// Deletes the given bridge. |
| pub async fn delete_bridge(&mut self, id: PortId) -> error::Result<()> { |
| // TODO(dpradilla): what is the API for deleting a bridge? Call it |
| info!("delete_bridge {:?} - Noop for now", id); |
| Ok(()) |
| } |
| |
| /// Configures an IP address. |
| pub async fn set_ip_address<'a>( |
| &'a mut self, |
| pid: PortId, |
| addr: &'a LifIpAddr, |
| ) -> error::Result<()> { |
| let r = self |
| .stack |
| .add_interface_address(StackPortId::from(pid).to_u64(), &mut addr.into()) |
| .await; |
| info!("set_ip_address {:?}: {:?}: {:?}", pid, addr, r); |
| r.squash_result().map_err(|_| error::NetworkManager::Hal(error::Hal::OperationFailed)) |
| } |
| |
| /// Removes an IP address. |
| pub async fn unset_ip_address<'a>( |
| &'a mut self, |
| pid: PortId, |
| addr: &'a LifIpAddr, |
| ) -> error::Result<()> { |
| let a: netconfig::CidrAddress = addr.into(); |
| // TODO(dpradilla): this needs to be changed to use the stack fidl once |
| // this functionality is moved there. the u32 conversion won't be needed. |
| let r = self |
| .netstack |
| .remove_interface_address( |
| pid.to_u32(), |
| &mut a.address.unwrap(), |
| a.prefix_length.unwrap(), |
| ) |
| .await; |
| info!("unset_ip_address {:?}: {:?}: {:?}", pid, addr, r); |
| r.map_err(|_| error::NetworkManager::Hal(error::Hal::OperationFailed))?; |
| |
| Ok(()) |
| } |
| |
| /// Sets the state of an interface. |
| /// |
| /// `state` controls whether the given `PortId` is enabled or disabled. |
| pub async fn set_interface_state(&mut self, pid: PortId, state: bool) -> error::Result<()> { |
| let r = if state { |
| self.stack.enable_interface(StackPortId::from(pid).to_u64()) |
| } else { |
| self.stack.disable_interface(StackPortId::from(pid).to_u64()) |
| } |
| .await; |
| |
| r.squash_result() |
| .with_context(|| "failed setting interface state".to_string()) |
| .map_err(|_| error::NetworkManager::Hal(error::Hal::OperationFailed)) |
| } |
| |
| /// Sets the state of the DHCP client on the specified interface. |
| /// |
| /// `enable` controls whether the given `PortId` has a DHCP client started or stopped. |
| pub async fn set_dhcp_client_state(&mut self, pid: PortId, enable: bool) -> error::Result<()> { |
| let (dhcp_client, server_end) = |
| fidl::endpoints::create_proxy::<fidl_fuchsia_net_dhcp::ClientMarker>() |
| .context("dhcp client: failed to create fidl endpoints")?; |
| if let Err(e) = self.netstack.get_dhcp_client(pid.to_u32(), server_end).await { |
| warn!("failed to create fidl endpoint for dhch client: {:?}", e); |
| return Err(error::NetworkManager::Hal(error::Hal::OperationFailed)); |
| } |
| let r = if enable { |
| dhcp_client.start().await.context("failed to start dhcp client")? |
| } else { |
| dhcp_client.stop().await.context("failed to stop dhcp client")? |
| }; |
| if let Err(e) = r { |
| warn!("failed to start dhcp client: {:?}", e); |
| return Err(error::NetworkManager::Hal(error::Hal::OperationFailed)); |
| } |
| info!("DHCP client on nicid: {}, enabled: {}", pid.to_u32(), enable); |
| Ok(()) |
| } |
| |
| /// Sets the state of the DHCP server on the specified interface. |
| /// |
| /// `enable` controls whether the given `PortId` has a DHCP client started or stopped. |
| pub async fn set_dhcp_server_state(&mut self, pid: PortId, enable: bool) -> error::Result<()> { |
| // TODO(dpradilla): call API here when ready. |
| info!("set_dhcp_server_state pid: {:?} enable: {}", pid, enable); |
| Ok(()) |
| } |
| |
| /// Sets the configuration of the DHCP server on the specified interface. |
| /// |
| pub async fn set_dhcp_server_config( |
| &mut self, |
| pid: PortId, |
| _old: &Option<lifmgr::DhcpServerConfig>, |
| desired: &Option<lifmgr::DhcpServerConfig>, |
| ) -> error::Result<()> { |
| // TODO(dpradilla): call API here when ready. |
| info!("set_dhcp_server_config pid: {:?} config: {:?}", pid, desired); |
| Ok(()) |
| } |
| |
| /// Sets the state of IP forwarding. |
| /// |
| /// `enabled` controls whether IP forwarding is enabled or disabled. |
| pub async fn set_ip_forwarding(&self, enabled: bool) -> error::Result<()> { |
| let r = if enabled { |
| self.stack.enable_ip_forwarding() |
| } else { |
| self.stack.disable_ip_forwarding() |
| } |
| .await; |
| r.map_err(|_| error::NetworkManager::Hal(error::Hal::OperationFailed)) |
| } |
| |
| /// Updates a configured IP address. |
| async fn apply_manual_ip<'a>( |
| &'a mut self, |
| pid: PortId, |
| current: &'a Option<LifIpAddr>, |
| desired: &'a Option<LifIpAddr>, |
| ) -> error::Result<()> { |
| match (current, desired) { |
| (Some(current_ip), Some(desired_ip)) => { |
| if current_ip != desired_ip { |
| // There has been a change. |
| // Remove the old one and add the new one. |
| self.unset_ip_address(pid, ¤t_ip).await?; |
| self.set_ip_address(pid, &desired_ip).await?; |
| } |
| } |
| (None, Some(desired_ip)) => { |
| self.set_ip_address(pid, &desired_ip).await?; |
| } |
| (Some(current_ip), None) => { |
| self.unset_ip_address(pid, ¤t_ip).await?; |
| } |
| // Nothing to do. |
| (None, None) => {} |
| }; |
| Ok(()) |
| } |
| |
| /// Applies the given LIF properties. |
| pub async fn apply_properties<'a>( |
| &'a mut self, |
| pid: PortId, |
| old: &'a lifmgr::LIFProperties, |
| properties: &'a lifmgr::LIFProperties, |
| ) -> error::Result<()> { |
| match (&old.dhcp, &properties.dhcp) { |
| // dhcp is still disabled, check for manual IP address changes. |
| (lifmgr::Dhcp::None, lifmgr::Dhcp::None) => { |
| self.apply_manual_ip(pid, &old.address_v4, &properties.address_v4).await?; |
| } |
| // No changes to dhcp configuration, it is still enabled, nothing to do. |
| (lifmgr::Dhcp::Client, lifmgr::Dhcp::Client) => {} |
| (lifmgr::Dhcp::Server, lifmgr::Dhcp::Server) => { |
| self.apply_manual_ip(pid, &old.address_v4, &properties.address_v4).await?; |
| self.set_dhcp_server_config(pid, &old.dhcp_config, &properties.dhcp_config).await?; |
| } |
| |
| (lifmgr::Dhcp::Server, lifmgr::Dhcp::None) => { |
| self.apply_manual_ip(pid, &old.address_v4, &properties.address_v4).await?; |
| self.set_dhcp_server_state(pid, false).await?; |
| } |
| (lifmgr::Dhcp::None, lifmgr::Dhcp::Server) => { |
| self.apply_manual_ip(pid, &old.address_v4, &properties.address_v4).await?; |
| self.set_dhcp_server_config(pid, &old.dhcp_config, &properties.dhcp_config).await?; |
| self.set_dhcp_server_state(pid, true).await?; |
| } |
| |
| // dhcp configuration transitions from client enabled |
| (lifmgr::Dhcp::Client, lifmgr::Dhcp::None) => { |
| // Disable dhcp and apply manual address configuration. |
| self.set_dhcp_client_state(pid, false).await?; |
| self.apply_manual_ip(pid, &old.address_v4, &properties.address_v4).await?; |
| } |
| (lifmgr::Dhcp::Client, lifmgr::Dhcp::Server) => { |
| // Disable dhcp and apply manual address configuration. |
| self.set_dhcp_client_state(pid, false).await?; |
| self.apply_manual_ip(pid, &old.address_v4, &properties.address_v4).await?; |
| self.set_dhcp_server_config(pid, &old.dhcp_config, &properties.dhcp_config).await?; |
| self.set_dhcp_server_state(pid, true).await?; |
| } |
| // dhcp configuration transitions from disabled to enabled. |
| (_, lifmgr::Dhcp::Client) => { |
| // Remove any manual IP address and enable dhcp client. |
| self.apply_manual_ip(pid, &old.address_v4, &None).await?; |
| self.set_dhcp_client_state(pid, properties.dhcp == lifmgr::Dhcp::Client).await?; |
| self.set_dhcp_server_state(pid, false).await?; |
| } |
| } |
| |
| // TODO(dpradilla) apply ipv6 address |
| |
| if old.enabled != properties.enabled { |
| info!("id {:?} updating state {:?}", pid, properties.enabled); |
| self.set_interface_state(pid, properties.enabled).await?; |
| } |
| Ok(()) |
| } |
| |
| /// Returns the running routing table (as seen by the network stack). |
| pub async fn routes(&mut self) -> Option<Vec<Route>> { |
| let table = self.netstack.get_route_table2().await; |
| match table { |
| Ok(entries) => Some( |
| entries |
| .iter() |
| .map(Route::from) |
| .filter(|r| !r.target.address.is_loopback()) |
| .collect(), |
| ), |
| _ => { |
| info!("no entries present in forwarding table."); |
| None |
| } |
| } |
| } |
| |
| /// Sets the DNS resolver. |
| pub async fn set_dns_resolver( |
| &mut self, |
| servers: &mut [fidl_fuchsia_net::IpAddress], |
| _domains: Option<String>, |
| _policy: DnsPolicy, |
| ) -> error::Result<()> { |
| self.lookup_admin |
| .set_default_dns_servers(&mut servers.iter_mut()) |
| .await |
| .with_context(|| "failed setting interface state".to_string()) |
| .and_then(|r| { |
| r.map_err(|status| { |
| anyhow::anyhow!( |
| "set_default_dns_servers returned {:?}", |
| zx::Status::from_raw(status) |
| ) |
| }) |
| }) |
| .map_err(|e| { |
| error!("set_dns_resolver error {:?}", e); |
| error::NetworkManager::Hal(error::Hal::OperationFailed) |
| }) |
| } |
| |
| /// Adds an ethernet device to netstack. |
| pub async fn add_ethernet_device( |
| &self, |
| channel: fuchsia_zircon::Channel, |
| port: oir::PortDevice, |
| ) -> error::Result<PortId> { |
| info!("Adding port: {:#?}", port); |
| let nic_id = self |
| .netstack |
| .add_ethernet_device( |
| &port.topological_path, |
| &mut netstack::InterfaceConfig { |
| name: port.name, |
| metric: port.metric, |
| filepath: port.file_path, |
| ip_address_config: netstack::IpAddressConfig::Dhcp(false), |
| }, |
| fidl::endpoints::ClientEnd::<fidl_fuchsia_hardware_ethernet::DeviceMarker>::new( |
| channel, |
| ), |
| ) |
| .await |
| .map_err(|_| error::NetworkManager::Hal(error::Hal::OperationFailed))?; |
| info!("added port with id {:?}", nic_id); |
| |
| Ok(StackPortId::from(u64::from(nic_id)).into()) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| fn interface_info_with_addrs(addrs: Vec<stack::InterfaceAddress>) -> InterfaceInfo { |
| InterfaceInfo { |
| id: 42, |
| properties: stack::InterfaceProperties { |
| topopath: "test/interface/info".to_string(), |
| addresses: addrs, |
| administrative_status: stack::AdministrativeStatus::Enabled, |
| name: "ethtest".to_string(), |
| filepath: "/some/file".to_string(), |
| mac: None, |
| mtu: 0, |
| features: 0, |
| physical_status: stack::PhysicalStatus::Up, |
| }, |
| } |
| } |
| |
| fn interface_with_addr(addr: Option<LifIpAddr>) -> Interface { |
| Interface { |
| id: 42.into(), |
| topo_path: "test/interface/info".to_string(), |
| name: "ethtest".to_string(), |
| ipv4_addr: addr.map(|a| InterfaceAddress::Unknown(a)), |
| ipv6_addr: Vec::new(), |
| enabled: true, |
| state: InterfaceState::Up, |
| dhcp_client_enabled: false, |
| } |
| } |
| |
| fn sample_addresses() -> Vec<stack::InterfaceAddress> { |
| vec![ |
| // Unspecified addresses are skipped. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [0, 0, 0, 0] }), |
| prefix_len: 24, |
| }, |
| // Multicast addresses are skipped. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [224, 0, 0, 5] }), |
| prefix_len: 24, |
| }, |
| // Loopback addresses are skipped. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [127, 0, 0, 1] }), |
| prefix_len: 24, |
| }, |
| // IPv6 addresses are skipped. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], |
| }), |
| prefix_len: 8, |
| }, |
| // First valid address, should be picked. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [4, 3, 2, 1] }), |
| prefix_len: 24, |
| }, |
| // A valid address is already available, so this address should be skipped. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 4] }), |
| prefix_len: 24, |
| }, |
| ] |
| } |
| |
| #[test] |
| fn test_net_interface_info_into_hal_interface() { |
| let info = interface_info_with_addrs(sample_addresses()); |
| let iface: Interface = (&info).into(); |
| assert_eq!(iface.topo_path, "test/interface/info"); |
| assert_eq!(iface.enabled, true); |
| assert_eq!( |
| iface.get_address_v4(), |
| Some(LifIpAddr { address: IpAddr::from([4, 3, 2, 1]), prefix: 24 }) |
| ); |
| assert_eq!(iface.id.to_u64(), 42); |
| } |
| |
| async fn handle_list_interfaces(request: stack::StackRequest) { |
| match request { |
| stack::StackRequest::ListInterfaces { responder } => { |
| responder |
| .send( |
| &mut sample_addresses() |
| .into_iter() |
| .map(|addr| interface_info_with_addrs(vec![addr])) |
| .collect::<Vec<InterfaceInfo>>() |
| .iter_mut(), |
| ) |
| .unwrap(); |
| } |
| _ => { |
| panic!("unexpected stack request: {:?}", request); |
| } |
| } |
| } |
| |
| async fn handle_with_panic<Request: std::fmt::Debug>(request: Request) { |
| panic!("unexpected request: {:?}", request); |
| } |
| |
| #[fuchsia_async::run_until_stalled(test)] |
| async fn test_ignore_interface_without_ip() { |
| let stack: StackProxy = |
| fidl::endpoints::spawn_stream_handler(handle_list_interfaces).unwrap(); |
| let netstack: NetstackProxy = |
| fidl::endpoints::spawn_stream_handler(handle_with_panic).unwrap(); |
| let lookup_admin: LookupAdminProxy = |
| fidl::endpoints::spawn_stream_handler(handle_with_panic).unwrap(); |
| let mut netcfg = NetCfg { stack, netstack, lookup_admin }; |
| assert_eq!( |
| netcfg.interfaces().await.unwrap(), |
| // Should return only interfaces with a valid address. |
| vec![ |
| interface_with_addr(Some(LifIpAddr { |
| address: IpAddr::from([4, 3, 2, 1]), |
| prefix: 24 |
| })), |
| interface_with_addr(Some(LifIpAddr { |
| address: IpAddr::from([1, 2, 3, 4]), |
| prefix: 24 |
| })), |
| ] |
| ); |
| } |
| |
| #[test] |
| fn test_valid_address() { |
| let f = |addr| { |
| valid_unicast_address_or_none(LifIpAddr { address: IpAddr::from(addr), prefix: 24 }) |
| }; |
| assert!(f([0, 0, 0, 0]).is_none()); |
| assert!(f([127, 0, 0, 1]).is_none()); |
| assert!(f([224, 0, 0, 5]).is_none()); |
| assert_eq!( |
| f([1, 2, 3, 4]), |
| Some(LifIpAddr { address: IpAddr::from([1, 2, 3, 4]), prefix: 24 }) |
| ); |
| } |
| |
| #[test] |
| fn test_hal_interface_from_netstack_net_interface() { |
| for (test, net_interface, want) in [ |
| ( |
| "ipv4 /24", |
| netstack::NetInterface { |
| id: 5, |
| flags: netstack::NET_INTERFACE_FLAG_UP | netstack::NET_INTERFACE_FLAG_DHCP, |
| features: 0, |
| configuration: 0, |
| name: "test_if".to_string(), |
| addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 4] }), |
| netmask: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [255, 255, 255, 0] }), |
| broadaddr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 255] }), |
| ipv6addrs: vec![], |
| hwaddr: vec![1, 2, 3, 4, 5, 6], |
| }, |
| Interface { |
| id: PortId(5), |
| topo_path: "test_if".to_string(), |
| name: "test_if".to_string(), |
| ipv4_addr: Some(InterfaceAddress::Dhcp(LifIpAddr { |
| address: IpAddr::from([1, 2, 3, 4]), |
| prefix: 24, |
| })), |
| ipv6_addr: Vec::new(), |
| enabled: true, |
| state: InterfaceState::Unknown, |
| dhcp_client_enabled: true, |
| }, |
| ), |
| ( |
| "ipv4 /25", |
| netstack::NetInterface { |
| id: 5, |
| flags: netstack::NET_INTERFACE_FLAG_UP | netstack::NET_INTERFACE_FLAG_DHCP, |
| features: 0, |
| configuration: 0, |
| name: "test_if".to_string(), |
| addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 4] }), |
| netmask: fnet::IpAddress::Ipv4(fnet::Ipv4Address { |
| addr: [255, 255, 255, 128], |
| }), |
| broadaddr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 127] }), |
| ipv6addrs: vec![], |
| hwaddr: vec![1, 2, 3, 4, 5, 6], |
| }, |
| Interface { |
| id: PortId(5), |
| topo_path: "test_if".to_string(), |
| name: "test_if".to_string(), |
| ipv4_addr: Some(InterfaceAddress::Dhcp(LifIpAddr { |
| address: IpAddr::from([1, 2, 3, 4]), |
| prefix: 25, |
| })), |
| ipv6_addr: Vec::new(), |
| enabled: true, |
| state: InterfaceState::Unknown, |
| dhcp_client_enabled: true, |
| }, |
| ), |
| ( |
| "ipv6 /64", |
| netstack::NetInterface { |
| id: 5, |
| flags: netstack::NET_INTERFACE_FLAG_UP | netstack::NET_INTERFACE_FLAG_DHCP, |
| features: 0, |
| configuration: 0, |
| name: "test_if".to_string(), |
| addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 4] }), |
| netmask: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [255, 255, 255, 0] }), |
| broadaddr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 255] }), |
| ipv6addrs: vec![fnet::Subnet { |
| addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], |
| }), |
| prefix_len: 64, |
| }], |
| hwaddr: vec![1, 2, 3, 4, 5, 6], |
| }, |
| Interface { |
| id: PortId(5), |
| topo_path: "test_if".to_string(), |
| name: "test_if".to_string(), |
| ipv4_addr: Some(InterfaceAddress::Dhcp(LifIpAddr { |
| address: IpAddr::from([1, 2, 3, 4]), |
| prefix: 24, |
| })), |
| ipv6_addr: vec![LifIpAddr { |
| address: IpAddr::from([ |
| 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
| ]), |
| prefix: 64, |
| }], |
| enabled: true, |
| state: InterfaceState::Unknown, |
| dhcp_client_enabled: true, |
| }, |
| ), |
| ( |
| "ipv6 /72", |
| netstack::NetInterface { |
| id: 5, |
| flags: netstack::NET_INTERFACE_FLAG_UP | netstack::NET_INTERFACE_FLAG_DHCP, |
| features: 0, |
| configuration: 0, |
| name: "test_if".to_string(), |
| addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 4] }), |
| netmask: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [255, 255, 255, 0] }), |
| broadaddr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 255] }), |
| ipv6addrs: vec![fnet::Subnet { |
| addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], |
| }), |
| prefix_len: 72, |
| }], |
| hwaddr: vec![1, 2, 3, 4, 5, 6], |
| }, |
| Interface { |
| id: PortId(5), |
| topo_path: "test_if".to_string(), |
| name: "test_if".to_string(), |
| ipv4_addr: Some(InterfaceAddress::Dhcp(LifIpAddr { |
| address: IpAddr::from([1, 2, 3, 4]), |
| prefix: 24, |
| })), |
| ipv6_addr: vec![LifIpAddr { |
| address: IpAddr::from([ |
| 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
| ]), |
| prefix: 72, |
| }], |
| enabled: true, |
| state: InterfaceState::Unknown, |
| dhcp_client_enabled: true, |
| }, |
| ), |
| ( |
| "2 ipv6 /64", |
| netstack::NetInterface { |
| id: 5, |
| flags: netstack::NET_INTERFACE_FLAG_UP | netstack::NET_INTERFACE_FLAG_DHCP, |
| features: 0, |
| configuration: 0, |
| name: "test_if".to_string(), |
| addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 4] }), |
| netmask: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [255, 255, 255, 0] }), |
| broadaddr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [1, 2, 3, 255] }), |
| ipv6addrs: vec![ |
| fnet::Subnet { |
| addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], |
| }), |
| prefix_len: 64, |
| }, |
| fnet::Subnet { |
| addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], |
| }), |
| prefix_len: 48, |
| }, |
| ], |
| hwaddr: vec![1, 2, 3, 4, 5, 6], |
| }, |
| Interface { |
| id: PortId(5), |
| topo_path: "test_if".to_string(), |
| name: "test_if".to_string(), |
| ipv4_addr: Some(InterfaceAddress::Dhcp(LifIpAddr { |
| address: IpAddr::from([1, 2, 3, 4]), |
| prefix: 24, |
| })), |
| ipv6_addr: vec![ |
| LifIpAddr { |
| address: IpAddr::from([ |
| 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
| ]), |
| prefix: 64, |
| }, |
| LifIpAddr { |
| address: IpAddr::from([ |
| 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
| ]), |
| prefix: 48, |
| }, |
| ], |
| enabled: true, |
| state: InterfaceState::Unknown, |
| dhcp_client_enabled: true, |
| }, |
| ), |
| ] |
| .iter() |
| { |
| let got = Interface::from(net_interface); |
| assert_eq!(got, *want, "{} Got {:?}, want {:?}", test, got, want) |
| } |
| } |
| |
| #[test] |
| fn test_hal_interface_from_interfaceinfo() { |
| for (test, net_interface, want) in [( |
| "multiple v4 and v6 addresses", |
| InterfaceInfo { |
| id: 5, |
| properties: stack::InterfaceProperties { |
| topopath: "test/interface/info".to_string(), |
| addresses: vec![ |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], |
| }), |
| prefix_len: 8, |
| }, |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv6(fnet::Ipv6Address { |
| addr: [1, 1, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], |
| }), |
| prefix_len: 64, |
| }, |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { |
| addr: [4, 3, 2, 1], |
| }), |
| prefix_len: 23, |
| }, |
| // A valid address is already available, so this address should be skipped. |
| stack::InterfaceAddress { |
| ip_address: fnet::IpAddress::Ipv4(fnet::Ipv4Address { |
| addr: [1, 2, 3, 4], |
| }), |
| prefix_len: 27, |
| }, |
| ], |
| administrative_status: stack::AdministrativeStatus::Enabled, |
| name: "test_if".to_string(), |
| filepath: "/some/file".to_string(), |
| mac: None, |
| mtu: 1234, |
| features: 0, |
| physical_status: stack::PhysicalStatus::Up, |
| }, |
| }, |
| Interface { |
| id: PortId(5), |
| topo_path: "test/interface/info".to_string(), |
| name: "test_if".to_string(), |
| ipv4_addr: Some(InterfaceAddress::Unknown(LifIpAddr { |
| address: IpAddr::from([4, 3, 2, 1]), |
| prefix: 23, |
| })), |
| ipv6_addr: vec![ |
| LifIpAddr { |
| address: IpAddr::from([ |
| 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, |
| ]), |
| prefix: 8, |
| }, |
| LifIpAddr { |
| address: IpAddr::from([ |
| 1, 1, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, |
| ]), |
| prefix: 64, |
| }, |
| ], |
| enabled: true, |
| state: InterfaceState::Up, |
| dhcp_client_enabled: false, |
| }, |
| )] |
| .iter() |
| { |
| let got = Interface::from(net_interface); |
| assert_eq!(got, *want, "{} Got {:?}, want {:?}", test, got, want) |
| } |
| } |
| #[test] |
| fn test_route_from_forwarding_entry() { |
| assert_eq!( |
| Route::from(&ForwardingEntry { |
| subnet: fidl_fuchsia_net::Subnet { |
| addr: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [1, 2, 3, 0], |
| }), |
| prefix_len: 23, |
| }, |
| destination: stack::ForwardingDestination::NextHop( |
| fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [1, 2, 3, 4] |
| },) |
| ), |
| }), |
| Route { |
| target: LifIpAddr { address: "1.2.3.0".parse().unwrap(), prefix: 23 }, |
| gateway: Some("1.2.3.4".parse().unwrap()), |
| metric: None, |
| port_id: None, |
| }, |
| "valid IPv4 entry, with gateway" |
| ); |
| |
| assert_eq!( |
| Route::from(&ForwardingEntry { |
| subnet: fidl_fuchsia_net::Subnet { |
| addr: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [1, 2, 3, 0], |
| }), |
| prefix_len: 23, |
| }, |
| destination: stack::ForwardingDestination::DeviceId(3) |
| }), |
| Route { |
| target: LifIpAddr { address: "1.2.3.0".parse().unwrap(), prefix: 23 }, |
| gateway: None, |
| metric: None, |
| port_id: Some(PortId(3)) |
| }, |
| "valid IPv4 entry, no gateway" |
| ); |
| |
| assert_eq!( |
| Route::from(&ForwardingEntry { |
| subnet: fidl_fuchsia_net::Subnet { |
| addr: fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [0x26, 0x20, 0, 0, 0x10, 0, 0x50, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| }), |
| prefix_len: 64, |
| }, |
| destination: stack::ForwardingDestination::NextHop( |
| fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [ |
| 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x0, 0x5e, 0xff, 0xfe, 0x0, 0x02, |
| 0x65 |
| ], |
| }) |
| ), |
| }), |
| Route { |
| target: LifIpAddr { address: "2620:0:1000:5000::".parse().unwrap(), prefix: 64 }, |
| gateway: Some("fe80::200:5eff:fe00:265".parse().unwrap()), |
| metric: None, |
| port_id: None, |
| }, |
| "valid IPv6 entry, with gateway" |
| ); |
| |
| assert_eq!( |
| Route::from(&ForwardingEntry { |
| subnet: fidl_fuchsia_net::Subnet { |
| addr: fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [0x26, 0x20, 0, 0, 0x10, 0, 0x50, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| }), |
| prefix_len: 58, |
| }, |
| destination: stack::ForwardingDestination::DeviceId(3) |
| }), |
| Route { |
| target: LifIpAddr { address: "2620:0:1000:5000::".parse().unwrap(), prefix: 58 }, |
| gateway: None, |
| metric: None, |
| port_id: Some(PortId(3)) |
| }, |
| "valid IPv6 entry, no gateway" |
| ); |
| } |
| |
| #[test] |
| fn test_route_from_routetableentry2() { |
| assert_eq!( |
| Route::from(&netstack::RouteTableEntry2 { |
| destination: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [1, 2, 3, 0], |
| }), |
| netmask: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [255, 255, 254, 0], |
| }), |
| gateway: Some(Box::new(fidl_fuchsia_net::IpAddress::Ipv4( |
| fidl_fuchsia_net::Ipv4Address { addr: [1, 2, 3, 1] } |
| ))), |
| nicid: 1, |
| metric: 100, |
| }), |
| Route { |
| target: LifIpAddr { address: "1.2.3.0".parse().unwrap(), prefix: 23 }, |
| gateway: Some("1.2.3.1".parse().unwrap()), |
| metric: Some(100), |
| port_id: Some(PortId(1)), |
| }, |
| "valid IPv4 entry, with gateway" |
| ); |
| |
| assert_eq!( |
| Route::from(&netstack::RouteTableEntry2 { |
| destination: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [1, 2, 3, 0], |
| }), |
| netmask: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { |
| addr: [255, 255, 254, 0], |
| }), |
| gateway: None, |
| nicid: 3, |
| metric: 50, |
| }), |
| Route { |
| target: LifIpAddr { address: "1.2.3.0".parse().unwrap(), prefix: 23 }, |
| gateway: None, |
| metric: Some(50), |
| port_id: Some(PortId(3)) |
| }, |
| "valid IPv4 entry, no gateway" |
| ); |
| |
| assert_eq!( |
| Route::from(&netstack::RouteTableEntry2 { |
| destination: fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [0x26, 0x20, 0, 0, 0x10, 0, 0x50, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| }), |
| netmask: fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0], |
| }), |
| gateway: Some(Box::new(fidl_fuchsia_net::IpAddress::Ipv6( |
| fidl_fuchsia_net::Ipv6Address { |
| addr: [ |
| 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0x02, 0x0, 0x5e, 0xff, 0xfe, 0x0, 0x02, |
| 0x65 |
| ], |
| } |
| ))), |
| nicid: 9, |
| metric: 500, |
| }), |
| Route { |
| target: LifIpAddr { address: "2620:0:1000:5000::".parse().unwrap(), prefix: 64 }, |
| gateway: Some("fe80::200:5eff:fe00:265".parse().unwrap()), |
| metric: Some(500), |
| port_id: Some(PortId(9)), |
| }, |
| "valid IPv6 entry, with gateway" |
| ); |
| |
| assert_eq!( |
| Route::from(&netstack::RouteTableEntry2 { |
| destination: fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [0x26, 0x20, 0, 0, 0x10, 0, 0x50, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| }), |
| netmask: fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address { |
| addr: [255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0], |
| }), |
| gateway: None, |
| nicid: 3, |
| metric: 50, |
| }), |
| Route { |
| target: LifIpAddr { address: "2620:0:1000:5000::".parse().unwrap(), prefix: 56 }, |
| gateway: None, |
| metric: Some(50), |
| port_id: Some(PortId(3)) |
| }, |
| "valid IPv6 entry, no gateway" |
| ); |
| } |
| } |