| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use std::{ |
| collections::HashMap, |
| convert::{TryFrom as _, TryInto as _}, |
| sync::Arc, |
| }; |
| |
| use assert_matches::assert_matches; |
| use fidl_fuchsia_hardware_network::{self as fhardware_network, FrameType}; |
| use fidl_fuchsia_net as fnet; |
| use fidl_fuchsia_net_interfaces as fnet_interfaces; |
| use fuchsia_async as fasync; |
| use fuchsia_zircon as zx; |
| |
| use futures::{lock::Mutex, FutureExt as _, TryStreamExt as _}; |
| use net_types::{ |
| ethernet::Mac, |
| ip::{Ip, IpVersion, Ipv4, Ipv6, Ipv6Addr, Mtu, Subnet}, |
| UnicastAddr, |
| }; |
| use netstack3_core::{ |
| device::{ |
| DeviceProvider, EthernetCreationProperties, EthernetDeviceId, EthernetLinkDevice, |
| EthernetWeakDeviceId, MaxEthernetFrameSize, PureIpDevice, PureIpDeviceCreationProperties, |
| PureIpDeviceId, PureIpDeviceReceiveFrameMetadata, PureIpWeakDeviceId, |
| RecvEthernetFrameMeta, |
| }, |
| ip::{ |
| IpDeviceConfigurationUpdate, Ipv4DeviceConfigurationUpdate, Ipv6DeviceConfigurationUpdate, |
| SlaacConfiguration, TemporarySlaacAddressConfiguration, STABLE_IID_SECRET_KEY_BYTES, |
| }, |
| routes::RawMetric, |
| sync::RwLock as CoreRwLock, |
| }; |
| use rand::Rng as _; |
| |
| use crate::bindings::{ |
| devices, interfaces_admin, routes, trace_duration, BindingId, BindingsCtx, Ctx, DeviceId, |
| DeviceIdExt as _, Ipv6DeviceConfiguration, Netstack, StaticNetdeviceInfo, |
| DEFAULT_INTERFACE_METRIC, |
| }; |
| |
| /// Like [`DeviceId`], but restricted to netdevice devices. |
| enum NetdeviceId { |
| Ethernet(EthernetDeviceId<BindingsCtx>), |
| PureIp(PureIpDeviceId<BindingsCtx>), |
| } |
| |
| impl NetdeviceId { |
| fn netdevice_info(&self) -> &StaticNetdeviceInfo { |
| match self { |
| NetdeviceId::Ethernet(eth) => ð.external_state().netdevice, |
| NetdeviceId::PureIp(ip) => &ip.external_state().netdevice, |
| } |
| } |
| } |
| |
| /// Like [`WeakDeviceId`], but restricted to netdevice devices. |
| #[derive(Clone, Debug)] |
| enum WeakNetdeviceId { |
| Ethernet(EthernetWeakDeviceId<BindingsCtx>), |
| PureIp(PureIpWeakDeviceId<BindingsCtx>), |
| } |
| |
| impl WeakNetdeviceId { |
| fn upgrade(&self) -> Option<NetdeviceId> { |
| match self { |
| WeakNetdeviceId::Ethernet(eth) => eth.upgrade().map(NetdeviceId::Ethernet), |
| WeakNetdeviceId::PureIp(ip) => ip.upgrade().map(NetdeviceId::PureIp), |
| } |
| } |
| } |
| |
| #[derive(Clone)] |
| struct Inner { |
| device: netdevice_client::Client, |
| session: netdevice_client::Session, |
| state: Arc<Mutex<netdevice_client::PortSlab<WeakNetdeviceId>>>, |
| } |
| |
| /// The worker that receives messages from the ethernet device, and passes them |
| /// on to the main event loop. |
| pub(crate) struct NetdeviceWorker { |
| ctx: Ctx, |
| task: netdevice_client::Task, |
| inner: Inner, |
| } |
| |
| #[derive(thiserror::Error, Debug)] |
| pub(crate) enum Error { |
| #[error("failed to create system resources: {0}")] |
| SystemResource(fidl::Error), |
| #[error("client error: {0}")] |
| Client(#[from] netdevice_client::Error), |
| #[error("port {0:?} already installed")] |
| AlreadyInstalled(netdevice_client::Port), |
| #[error("failed to connect to port: {0}")] |
| CantConnectToPort(fidl::Error), |
| #[error("port closed")] |
| PortClosed, |
| #[error("invalid port info: {0}")] |
| InvalidPortInfo(netdevice_client::client::PortInfoValidationError), |
| #[error("unsupported configuration")] |
| ConfigurationNotSupported, |
| #[error("mac {mac} on port {port:?} is not a valid unicast address")] |
| MacNotUnicast { mac: net_types::ethernet::Mac, port: netdevice_client::Port }, |
| #[error("interface named {0} already exists")] |
| DuplicateName(String), |
| #[error("{port_type:?} port received unexpected frame type: {frame_type:?}")] |
| MismatchedRxFrameType { port_type: PortWireFormat, frame_type: fhardware_network::FrameType }, |
| } |
| |
| const DEFAULT_BUFFER_LENGTH: usize = 2048; |
| |
| // TODO(https://fxbug.dev/42052114): Decorate *all* logging with human-readable |
| // device debug information to disambiguate. |
| impl NetdeviceWorker { |
| pub(crate) async fn new( |
| ctx: Ctx, |
| device: fidl::endpoints::ClientEnd<fhardware_network::DeviceMarker>, |
| ) -> Result<Self, Error> { |
| let device = |
| netdevice_client::Client::new(device.into_proxy().expect("must be in executor")); |
| let (session, task) = device |
| .primary_session("netstack3", DEFAULT_BUFFER_LENGTH) |
| .await |
| .map_err(Error::Client)?; |
| Ok(Self { ctx, inner: Inner { device, session, state: Default::default() }, task }) |
| } |
| |
| pub(crate) fn new_handler(&self) -> DeviceHandler { |
| DeviceHandler { inner: self.inner.clone() } |
| } |
| |
| pub(crate) async fn run(self) -> Result<std::convert::Infallible, Error> { |
| let Self { mut ctx, inner: Inner { device: _, session, state }, task } = self; |
| // Allow buffer shuttling to happen in other threads. |
| let mut task = fuchsia_async::Task::spawn(task).fuse(); |
| |
| let mut buff = [0u8; DEFAULT_BUFFER_LENGTH]; |
| let mut last_wifi_drop_log = fasync::Time::INFINITE_PAST; |
| loop { |
| // Extract result into an enum to avoid too much code in macro. |
| let rx: netdevice_client::Buffer<_> = futures::select! { |
| r = session.recv().fuse() => r.map_err(Error::Client)?, |
| r = task => match r { |
| Ok(()) => panic!("task should never end cleanly"), |
| Err(e) => return Err(Error::Client(e)) |
| } |
| }; |
| let port = rx.port(); |
| let id = if let Some(id) = state.lock().await.get(&port) { |
| id.clone() |
| } else { |
| tracing::debug!("dropping frame for port {:?}, no device mapping available", port); |
| continue; |
| }; |
| |
| trace_duration!(c"netdevice::recv"); |
| |
| let frame_length = rx.len(); |
| // TODO(https://fxbug.dev/42051635): pass strongly owned buffers down |
| // to the stack instead of copying it out. |
| rx.read_at(0, &mut buff[..frame_length]).map_err(|e| { |
| tracing::error!("failed to read from buffer {:?}", e); |
| Error::Client(e) |
| })?; |
| |
| let Some(id) = id.upgrade() else { |
| // This is okay because we hold a weak reference; the device may |
| // be removed under us. Note that when the device removal has |
| // completed, the interface's `PortHandler` will be uninstalled |
| // from the port slab (table of ports for this network device). |
| tracing::debug!( |
| "received frame for device after it has been removed; device_id={id:?}" |
| ); |
| // We continue because even though we got frames for a removed |
| // device, this network device may have other ports that will |
| // receive and handle frames. |
| continue; |
| }; |
| |
| match workaround_drop_ssh_over_wlan( |
| &id.netdevice_info().handler.device_class, |
| &buff[..frame_length], |
| ) { |
| FilterResult::Drop => { |
| // This being hardcoded in Netstack is possibly surprising. |
| // Log a loud warning with some throttling to warn users. |
| let now = fasync::Time::now(); |
| if now - last_wifi_drop_log >= zx::Duration::from_seconds(5) { |
| tracing::warn!( |
| "Dropping frame destined to TCP port 22 on WiFi interface. \ |
| See https://fxbug.dev/42084174." |
| ); |
| last_wifi_drop_log = now; |
| } |
| continue; |
| } |
| FilterResult::Accept => (), |
| } |
| let buf = packet::Buf::new(&mut buff[..frame_length], ..); |
| let frame_type = rx.frame_type().map_err(Error::Client)?; |
| match id { |
| NetdeviceId::Ethernet(id) => { |
| match frame_type { |
| FrameType::Ethernet => {} |
| f @ FrameType::Ipv4 | f @ FrameType::Ipv6 => { |
| // NB: When the port was attached, `Ethernet` was |
| // the only permitted frame type; anything else here |
| // indicates a bug in `netdevice_client` or the core |
| // netdevice driver. |
| return Err(Error::MismatchedRxFrameType { |
| port_type: PortWireFormat::Ethernet, |
| frame_type: f, |
| }); |
| } |
| } |
| ctx.api() |
| .device::<EthernetLinkDevice>() |
| .receive_frame(RecvEthernetFrameMeta { device_id: id.clone() }, buf) |
| } |
| NetdeviceId::PureIp(id) => { |
| let ip_version = match frame_type { |
| FrameType::Ipv4 => IpVersion::V4, |
| FrameType::Ipv6 => IpVersion::V6, |
| f @ FrameType::Ethernet => { |
| // NB: When the port was attached, `IPv4` & `Ipv6` |
| // were the only permitted frame types; anything |
| // else here indicates a bug in `netdevice_client` or |
| // the core netdevice driver. |
| return Err(Error::MismatchedRxFrameType { |
| port_type: PortWireFormat::Ip, |
| frame_type: f, |
| }); |
| } |
| }; |
| ctx.api().device::<PureIpDevice>().receive_frame( |
| PureIpDeviceReceiveFrameMetadata { device_id: id.clone(), ip_version }, |
| buf, |
| ) |
| } |
| } |
| } |
| } |
| } |
| |
| #[derive(Debug, Eq, PartialEq)] |
| enum FilterResult { |
| Accept, |
| Drop, |
| } |
| |
| const SSH_PORT: u16 = 22; |
| |
| /// Implements a hardcoded filter to drop all incoming TCP frames with |
| /// destination port 22 on WiFi interfaces because we don't have a filtering |
| /// engine yet. This allows us to test Netstack3 on some devices without |
| /// incurring the security risk of remote access before we have the filtering |
| /// engine in place. |
| /// |
| /// TODO(https://fxbug.dev/42084174): Remove this and replace with real filtering. |
| fn workaround_drop_ssh_over_wlan( |
| device_class: &fhardware_network::DeviceClass, |
| buffer: &[u8], |
| ) -> FilterResult { |
| use packet::ParsablePacket; |
| use packet_formats::{ |
| error::{IpParseError, ParseError, ParseResult}, |
| ethernet::{EtherType, EthernetFrame, EthernetFrameLengthCheck}, |
| ip::{IpProto, Ipv4Proto, Ipv6Proto}, |
| ipv4::{Ipv4Header as _, Ipv4PacketRaw}, |
| ipv6::{ExtHdrParseError, Ipv6PacketRaw}, |
| tcp::TcpSegmentRaw, |
| }; |
| |
| match device_class { |
| fhardware_network::DeviceClass::Wlan | fhardware_network::DeviceClass::WlanAp => {} |
| fhardware_network::DeviceClass::Ppp |
| | fhardware_network::DeviceClass::Bridge |
| | fhardware_network::DeviceClass::Virtual |
| | fhardware_network::DeviceClass::Ethernet => return FilterResult::Accept, |
| } |
| // Attempt to parse the buffer as loosely as we can, that means we're as |
| // strict as possible here in dropping SSH. We just need to parse deep |
| // enough to get to the TCP header. |
| fn extract_tcp_ports(data: &[u8]) -> ParseResult<Option<(u16, u16)>> { |
| let mut bv = &data[..]; |
| let ethernet = EthernetFrame::parse(&mut bv, EthernetFrameLengthCheck::NoCheck)?; |
| let tcp_hdr = match ethernet.ethertype().ok_or(ParseError::Format)? { |
| EtherType::Ipv4 => { |
| let ipv4 = Ipv4PacketRaw::parse(&mut bv, ()) |
| .map_err(|_: IpParseError<_>| ParseError::Format)?; |
| |
| match ipv4.proto() { |
| Ipv4Proto::Proto(IpProto::Tcp) => (), |
| _ => return Ok(None), |
| }; |
| TcpSegmentRaw::parse(&mut ipv4.body().into_inner(), ())?.flow_header() |
| } |
| EtherType::Ipv6 => { |
| let ipv6 = Ipv6PacketRaw::parse(&mut bv, ()) |
| .map_err(|_: IpParseError<_>| ParseError::Format)?; |
| let (body, proto) = |
| ipv6.body_proto().map_err(|_: ExtHdrParseError| ParseError::Format)?; |
| |
| match proto { |
| Ipv6Proto::Proto(IpProto::Tcp) => (), |
| _ => return Ok(None), |
| }; |
| TcpSegmentRaw::parse(&mut body.into_inner(), ())?.flow_header() |
| } |
| EtherType::Arp | EtherType::Other(_) => return Ok(None), |
| }; |
| Ok(Some(tcp_hdr.src_dst())) |
| } |
| |
| match extract_tcp_ports(buffer) { |
| Ok(Some((_src, dst))) => { |
| if dst == SSH_PORT { |
| FilterResult::Drop |
| } else { |
| FilterResult::Accept |
| } |
| } |
| Ok(None) => FilterResult::Accept, |
| Result::<_, ParseError>::Err(_) => { |
| // Just pass packets that are not parsable in. |
| FilterResult::Accept |
| } |
| } |
| } |
| |
| pub(crate) struct InterfaceOptions { |
| pub(crate) name: Option<String>, |
| pub(crate) metric: Option<u32>, |
| } |
| |
| pub(crate) struct DeviceHandler { |
| inner: Inner, |
| } |
| |
| /// The wire format for packets sent to and received on a port. |
| #[derive(Debug, Eq, PartialEq)] |
| pub(crate) enum PortWireFormat { |
| /// The port supports sending/receiving Ethernet frames. |
| Ethernet, |
| /// The port supports sending/receiving IPv4 and IPv6 packets. |
| Ip, |
| } |
| |
| impl PortWireFormat { |
| fn frame_types(&self) -> &[fhardware_network::FrameType] { |
| const ETHERNET_FRAMES: [fhardware_network::FrameType; 1] = |
| [fhardware_network::FrameType::Ethernet]; |
| const IP_FRAMES: [fhardware_network::FrameType; 2] = |
| [fhardware_network::FrameType::Ipv4, fhardware_network::FrameType::Ipv6]; |
| match self { |
| Self::Ethernet => ÐERNET_FRAMES, |
| Self::Ip => &IP_FRAMES, |
| } |
| } |
| } |
| |
| /// Error returned for ports with unsupported wire formats. |
| #[derive(Debug)] |
| pub(crate) enum PortWireFormatError<'a> { |
| InvalidRxFrameTypes { _frame_types: Vec<&'a fhardware_network::FrameType> }, |
| InvalidTxFrameTypes { _frame_types: Vec<&'a fhardware_network::FrameType> }, |
| MismatchedRxTx { _rx: PortWireFormat, _tx: PortWireFormat }, |
| } |
| |
| impl PortWireFormat { |
| fn new_from_port_info( |
| info: &netdevice_client::client::PortBaseInfo, |
| ) -> Result<PortWireFormat, PortWireFormatError<'_>> { |
| let netdevice_client::client::PortBaseInfo { port_class: _, rx_types, tx_types } = info; |
| |
| // Verify the wire format in a single direction (tx/rx). |
| fn wire_format_from_frame_types<'a>( |
| frame_types: impl Iterator<Item = &'a fhardware_network::FrameType> + Clone, |
| ) -> Result<PortWireFormat, impl Iterator<Item = &'a fhardware_network::FrameType>> |
| { |
| struct SupportedFormats { |
| ethernet: bool, |
| ipv4: bool, |
| ipv6: bool, |
| } |
| let SupportedFormats { ethernet, ipv4, ipv6 } = frame_types.clone().fold( |
| SupportedFormats { ethernet: false, ipv4: false, ipv6: false }, |
| |mut sf, frame_type| { |
| match frame_type { |
| fhardware_network::FrameType::Ethernet => sf.ethernet = true, |
| fhardware_network::FrameType::Ipv4 => sf.ipv4 = true, |
| fhardware_network::FrameType::Ipv6 => sf.ipv6 = true, |
| } |
| sf |
| }, |
| ); |
| // Disallow devices with mixed frame types, and require that IP |
| // Devices support both IPv4 and IPv6. |
| if ethernet && !ipv4 && !ipv6 { |
| Ok(PortWireFormat::Ethernet) |
| } else if !ethernet && ipv4 && ipv6 { |
| Ok(PortWireFormat::Ip) |
| } else { |
| Err(frame_types) |
| } |
| } |
| |
| // Ignore the superfluous information included with the tx frame types. |
| let tx_iterator = || { |
| tx_types.iter().map( |
| |fhardware_network::FrameTypeSupport { |
| type_: frame_type, |
| features: _, |
| supported_flags: _, |
| }| { frame_type }, |
| ) |
| }; |
| |
| // Verify each direction independently, and then ensure the port is |
| // symmetrical. |
| let rx_wire_format = wire_format_from_frame_types(rx_types.iter()).map_err(|rx_types| { |
| PortWireFormatError::InvalidRxFrameTypes { _frame_types: rx_types.collect() } |
| })?; |
| let tx_wire_format = wire_format_from_frame_types(tx_iterator()).map_err(|tx_types| { |
| PortWireFormatError::InvalidTxFrameTypes { _frame_types: tx_types.collect() } |
| })?; |
| if rx_wire_format == tx_wire_format { |
| Ok(rx_wire_format) |
| } else { |
| Err(PortWireFormatError::MismatchedRxTx { _rx: rx_wire_format, _tx: tx_wire_format }) |
| } |
| } |
| } |
| |
| impl DeviceHandler { |
| pub(crate) async fn add_port( |
| &self, |
| ns: &mut Netstack, |
| InterfaceOptions { name, metric }: InterfaceOptions, |
| port: fhardware_network::PortId, |
| control_hook: futures::channel::mpsc::Sender<interfaces_admin::OwnedControlHandle>, |
| ) -> Result< |
| ( |
| BindingId, |
| impl futures::Stream<Item = netdevice_client::Result<netdevice_client::PortStatus>>, |
| fuchsia_async::Task<()>, |
| ), |
| Error, |
| > { |
| let port = netdevice_client::Port::try_from(port)?; |
| |
| let DeviceHandler { inner: Inner { state, device, session: _ } } = self; |
| let port_proxy = device.connect_port(port)?; |
| let netdevice_client::client::PortInfo { id: _, base_info } = port_proxy |
| .get_info() |
| .await |
| .map_err(Error::CantConnectToPort)? |
| .try_into() |
| .map_err(Error::InvalidPortInfo)?; |
| |
| let mut status_stream = |
| netdevice_client::client::new_port_status_stream(&port_proxy, None)?; |
| |
| let wire_format = PortWireFormat::new_from_port_info(&base_info).map_err( |
| |e: PortWireFormatError<'_>| { |
| tracing::warn!("not installing port with invalid wire format: {:?}", e); |
| Error::ConfigurationNotSupported |
| }, |
| )?; |
| |
| let netdevice_client::client::PortStatus { flags, mtu } = |
| status_stream.try_next().await?.ok_or_else(|| Error::PortClosed)?; |
| let phy_up = flags.contains(fhardware_network::StatusFlags::ONLINE); |
| let netdevice_client::client::PortBaseInfo { |
| port_class: device_class, |
| rx_types: _, |
| tx_types: _, |
| } = base_info; |
| |
| enum DeviceProperties { |
| Ethernet { |
| max_frame_size: MaxEthernetFrameSize, |
| mac: UnicastAddr<Mac>, |
| mac_proxy: fhardware_network::MacAddressingProxy, |
| }, |
| Ip { |
| max_frame_size: Mtu, |
| }, |
| } |
| let properties = match wire_format { |
| PortWireFormat::Ethernet => { |
| let max_frame_size = |
| MaxEthernetFrameSize::new(mtu).ok_or(Error::ConfigurationNotSupported)?; |
| let (mac, mac_proxy) = get_mac(&port_proxy, &port).await?; |
| DeviceProperties::Ethernet { max_frame_size, mac, mac_proxy } |
| } |
| PortWireFormat::Ip => DeviceProperties::Ip { max_frame_size: Mtu::new(mtu) }, |
| }; |
| |
| let mut state = state.lock().await; |
| let state_entry = match state.entry(port) { |
| netdevice_client::port_slab::Entry::Occupied(occupied) => { |
| tracing::warn!( |
| "attempted to install port {:?} which is already installed for {:?}", |
| port, |
| occupied.get() |
| ); |
| return Err(Error::AlreadyInstalled(port)); |
| } |
| netdevice_client::port_slab::Entry::SaltMismatch(stale) => { |
| tracing::warn!( |
| "attempted to install port {:?} which is already has a stale entry: {:?}", |
| port, |
| stale |
| ); |
| return Err(Error::AlreadyInstalled(port)); |
| } |
| netdevice_client::port_slab::Entry::Vacant(e) => e, |
| }; |
| let Netstack { interfaces_event_sink, neighbor_event_sink, ctx } = ns; |
| |
| // Check if there already exists an interface with this name. |
| // Interface names are unique. |
| let name = name |
| .map(|name| { |
| if let Some(_device_info) = ctx.bindings_ctx().devices.get_device_by_name(&name) { |
| return Err(Error::DuplicateName(name)); |
| }; |
| Ok(name) |
| }) |
| .transpose()?; |
| |
| let binding_id = ctx.bindings_ctx().devices.alloc_new_id(); |
| |
| let name = name.unwrap_or_else(|| match wire_format { |
| PortWireFormat::Ethernet => format!("eth{}", binding_id), |
| PortWireFormat::Ip => format!("ip{}", binding_id), |
| }); |
| |
| let static_netdevice_info = devices::StaticNetdeviceInfo { |
| handler: PortHandler { |
| id: binding_id, |
| port_id: port, |
| inner: self.inner.clone(), |
| device_class: device_class.clone(), |
| wire_format, |
| }, |
| }; |
| let dynamic_netdevice_info_builder = |mtu: Mtu| devices::DynamicNetdeviceInfo { |
| phy_up, |
| common_info: devices::DynamicCommonInfo { |
| mtu, |
| admin_enabled: false, |
| events: crate::bindings::create_interface_event_producer( |
| interfaces_event_sink, |
| binding_id, |
| crate::bindings::InterfaceProperties { |
| name: name.clone(), |
| device_class: fnet_interfaces::DeviceClass::Device(device_class), |
| }, |
| ), |
| control_hook: control_hook, |
| addresses: HashMap::new(), |
| }, |
| }; |
| |
| let core_id = match properties { |
| DeviceProperties::Ethernet { max_frame_size, mac, mac_proxy } => { |
| let info = devices::EthernetInfo { |
| mac, |
| _mac_proxy: mac_proxy, |
| netdevice: static_netdevice_info, |
| common_info: Default::default(), |
| dynamic_info: CoreRwLock::new(devices::DynamicEthernetInfo { |
| netdevice: dynamic_netdevice_info_builder(max_frame_size.as_mtu()), |
| neighbor_event_sink: neighbor_event_sink.clone(), |
| }), |
| } |
| .into(); |
| let core_ethernet_id = ctx.api().device::<EthernetLinkDevice>().add_device( |
| devices::DeviceIdAndName { id: binding_id, name: name.clone() }, |
| EthernetCreationProperties { mac, max_frame_size }, |
| RawMetric(metric.unwrap_or(DEFAULT_INTERFACE_METRIC)), |
| info, |
| ); |
| state_entry.insert(WeakNetdeviceId::Ethernet(core_ethernet_id.downgrade())); |
| DeviceId::from(core_ethernet_id) |
| } |
| DeviceProperties::Ip { max_frame_size } => { |
| let info = devices::PureIpDeviceInfo { |
| common_info: Default::default(), |
| netdevice: static_netdevice_info, |
| dynamic_info: CoreRwLock::new(dynamic_netdevice_info_builder(max_frame_size)), |
| } |
| .into(); |
| let core_pure_ip_id = ctx.api().device::<PureIpDevice>().add_device( |
| devices::DeviceIdAndName { id: binding_id, name: name.clone() }, |
| PureIpDeviceCreationProperties { mtu: max_frame_size }, |
| RawMetric(metric.unwrap_or(DEFAULT_INTERFACE_METRIC)), |
| info, |
| ); |
| state_entry.insert(WeakNetdeviceId::PureIp(core_pure_ip_id.downgrade())); |
| DeviceId::from(core_pure_ip_id) |
| } |
| }; |
| |
| let binding_id = core_id.bindings_id().id; |
| let external_state = core_id.external_state(); |
| let devices::StaticCommonInfo { tx_notifier, authorization_token: _ } = |
| external_state.static_common_info(); |
| let task = |
| crate::bindings::devices::spawn_tx_task(&tx_notifier, ctx.clone(), core_id.clone()); |
| netstack3_core::for_any_device_id!(DeviceId, DeviceProvider, D, &core_id, device => { |
| ctx.api().transmit_queue::<D>().set_configuration( |
| device, |
| netstack3_core::device::TransmitQueueConfiguration::Fifo, |
| ); |
| }); |
| add_initial_routes(ctx.bindings_ctx(), &core_id).await; |
| |
| // TODO(https://fxbug.dev/42148800): Use a different secret key (not this |
| // one) to generate stable opaque interface identifiers. |
| let mut secret_key = [0; STABLE_IID_SECRET_KEY_BYTES]; |
| ctx.rng().fill(&mut secret_key); |
| |
| let ip_config = IpDeviceConfigurationUpdate { |
| ip_enabled: Some(false), |
| forwarding_enabled: Some(false), |
| gmp_enabled: Some(true), |
| }; |
| |
| let _: Ipv6DeviceConfigurationUpdate = ctx |
| .api() |
| .device_ip::<Ipv6>() |
| .update_configuration( |
| &core_id, |
| Ipv6DeviceConfigurationUpdate { |
| dad_transmits: Some(Some( |
| Ipv6DeviceConfiguration::DEFAULT_DUPLICATE_ADDRESS_DETECTION_TRANSMITS, |
| )), |
| max_router_solicitations: Some(Some( |
| Ipv6DeviceConfiguration::DEFAULT_MAX_RTR_SOLICITATIONS, |
| )), |
| slaac_config: Some(SlaacConfiguration { |
| enable_stable_addresses: true, |
| temporary_address_configuration: Some( |
| TemporarySlaacAddressConfiguration::default_with_secret_key(secret_key), |
| ), |
| }), |
| ip_config, |
| }, |
| ) |
| .unwrap(); |
| let _: Ipv4DeviceConfigurationUpdate = ctx |
| .api() |
| .device_ip::<Ipv4>() |
| .update_configuration(&core_id, Ipv4DeviceConfigurationUpdate { ip_config }) |
| .unwrap(); |
| |
| tracing::info!("created interface {:?}", core_id); |
| ctx.bindings_ctx().devices.add_device(binding_id, core_id); |
| |
| Ok((binding_id, status_stream, task)) |
| } |
| } |
| |
| /// Connect to the Port's `MacAddressingProxy`, and fetch the MAC address. |
| async fn get_mac( |
| port_proxy: &fhardware_network::PortProxy, |
| port: &netdevice_client::Port, |
| ) -> Result<(UnicastAddr<Mac>, fhardware_network::MacAddressingProxy), Error> { |
| let (mac_proxy, mac_server) = |
| fidl::endpoints::create_proxy::<fhardware_network::MacAddressingMarker>() |
| .map_err(Error::SystemResource)?; |
| let () = port_proxy.get_mac(mac_server).map_err(Error::CantConnectToPort)?; |
| |
| let mac_addr = { |
| let fnet::MacAddress { octets } = mac_proxy.get_unicast_address().await.map_err(|e| { |
| tracing::warn!("failed to get unicast address, sending not supported: {:?}", e); |
| Error::ConfigurationNotSupported |
| })?; |
| let mac = net_types::ethernet::Mac::new(octets); |
| net_types::UnicastAddr::new(mac).ok_or_else(|| { |
| tracing::warn!("{} is not a valid unicast address", mac); |
| Error::MacNotUnicast { mac, port: *port } |
| })? |
| }; |
| // Always set the interface to multicast promiscuous mode because we |
| // don't really plumb through multicast filtering. |
| // TODO(https://fxbug.dev/42136929): Remove this when multicast filtering |
| // is available. |
| fuchsia_zircon::Status::ok( |
| mac_proxy |
| .set_mode(fhardware_network::MacFilterMode::MulticastPromiscuous) |
| .await |
| .map_err(Error::CantConnectToPort)?, |
| ) |
| .unwrap_or_else(|e| { |
| tracing::warn!("failed to set multicast promiscuous for new interface: {:?}", e) |
| }); |
| |
| Ok((mac_addr, mac_proxy)) |
| } |
| |
| /// Adds the IPv4 and IPv6 multicast subnet routes, the IPv6 link-local subnet |
| /// route, and the IPv4 limited broadcast subnet route. |
| /// |
| /// Note that if an error is encountered while installing a route, any routes |
| /// that were successfully installed prior to the error will not be removed. |
| async fn add_initial_routes(bindings_ctx: &BindingsCtx, device: &DeviceId<BindingsCtx>) { |
| use netstack3_core::routes::{AddableEntry, AddableMetric}; |
| const LINK_LOCAL_SUBNET: Subnet<Ipv6Addr> = net_declare::net_subnet_v6!("fe80::/64"); |
| |
| let v4_changes = [ |
| AddableEntry::without_gateway( |
| Ipv4::MULTICAST_SUBNET, |
| device.downgrade(), |
| AddableMetric::MetricTracksInterface, |
| ), |
| AddableEntry::without_gateway( |
| crate::bindings::IPV4_LIMITED_BROADCAST_SUBNET, |
| device.downgrade(), |
| AddableMetric::ExplicitMetric(RawMetric(crate::bindings::DEFAULT_LOW_PRIORITY_METRIC)), |
| ), |
| ] |
| .into_iter() |
| .map(|entry| { |
| routes::Change::RouteOp( |
| routes::RouteOp::Add(entry), |
| routes::SetMembership::InitialDeviceRoutes, |
| ) |
| }) |
| .map(Into::into); |
| |
| let v6_changes = [ |
| AddableEntry::without_gateway( |
| LINK_LOCAL_SUBNET, |
| device.downgrade(), |
| AddableMetric::MetricTracksInterface, |
| ), |
| AddableEntry::without_gateway( |
| Ipv6::MULTICAST_SUBNET, |
| device.downgrade(), |
| AddableMetric::MetricTracksInterface, |
| ), |
| ] |
| .into_iter() |
| .map(|entry| { |
| routes::Change::RouteOp( |
| routes::RouteOp::Add(entry), |
| routes::SetMembership::InitialDeviceRoutes, |
| ) |
| }) |
| .map(Into::into); |
| |
| for change in v4_changes.chain(v6_changes) { |
| bindings_ctx |
| .apply_route_change_either(change) |
| .await |
| .map(|outcome| assert_matches!(outcome, routes::ChangeOutcome::Changed)) |
| .expect("adding initial routes should succeed"); |
| } |
| } |
| |
| pub(crate) struct PortHandler { |
| id: BindingId, |
| port_id: netdevice_client::Port, |
| inner: Inner, |
| device_class: fhardware_network::DeviceClass, |
| wire_format: PortWireFormat, |
| } |
| |
| #[derive(thiserror::Error, Debug)] |
| pub(crate) enum SendError { |
| #[error("no buffers available")] |
| NoTxBuffers, |
| #[error("device error: {0}")] |
| Device(#[from] netdevice_client::Error), |
| } |
| |
| impl PortHandler { |
| pub(crate) fn device_class(&self) -> fhardware_network::DeviceClass { |
| self.device_class |
| } |
| |
| pub(crate) async fn attach(&self) -> Result<(), netdevice_client::Error> { |
| let Self { port_id, inner: Inner { session, .. }, wire_format, .. } = self; |
| session.attach(*port_id, wire_format.frame_types()).await |
| } |
| |
| pub(crate) async fn detach(&self) -> Result<(), netdevice_client::Error> { |
| let Self { port_id, inner: Inner { session, .. }, .. } = self; |
| session.detach(*port_id).await |
| } |
| |
| pub(crate) fn send( |
| &self, |
| frame: &[u8], |
| frame_type: fhardware_network::FrameType, |
| ) -> Result<(), SendError> { |
| trace_duration!(c"netdevice::send"); |
| |
| let Self { port_id, inner: Inner { session, .. }, .. } = self; |
| // NB: We currently send on a dispatcher, so we can't wait for new |
| // buffers to become available. If that ends up being the long term way |
| // of enqueuing outgoing buffers we might want to fix this impedance |
| // mismatch here. |
| let mut tx = |
| session.alloc_tx_buffer(frame.len()).now_or_never().ok_or(SendError::NoTxBuffers)??; |
| tx.set_port(*port_id); |
| tx.set_frame_type(frame_type); |
| tx.write_at(0, frame)?; |
| session.send(tx)?; |
| Ok(()) |
| } |
| |
| pub(crate) async fn uninstall(self) -> Result<(), netdevice_client::Error> { |
| let Self { port_id, inner: Inner { session, state, .. }, .. } = self; |
| let _: WeakNetdeviceId = assert_matches!( |
| state.lock().await.remove(&port_id), |
| netdevice_client::port_slab::RemoveOutcome::Removed(core_id) => core_id |
| ); |
| session.detach(port_id).await |
| } |
| |
| pub(crate) fn connect_port( |
| &self, |
| port: fidl::endpoints::ServerEnd<fhardware_network::PortMarker>, |
| ) -> Result<(), netdevice_client::Error> { |
| let Self { port_id, inner: Inner { device, .. }, .. } = self; |
| device.connect_port_server_end(*port_id, port) |
| } |
| } |
| |
| impl std::fmt::Debug for PortHandler { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| let Self { id, port_id, inner: _, device_class, wire_format } = self; |
| f.debug_struct("PortHandler") |
| .field("id", id) |
| .field("port_id", port_id) |
| .field("device_class", device_class) |
| .field("wire_format", wire_format) |
| .finish() |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use ip_test_macro::ip_test; |
| use net_declare::net_mac; |
| use net_types::Witness as _; |
| use packet::{Buf, InnerPacketBuilder as _, Serializer as _}; |
| use packet_formats::{ |
| ethernet::EthernetFrameBuilder, |
| ip::{IpExt, IpPacketBuilder as _, IpProto}, |
| tcp::TcpSegmentBuilder, |
| }; |
| use std::num::NonZeroU16; |
| |
| #[ip_test] |
| fn wlan_ssh_workaround<I: Ip + IpExt>() { |
| fn make_packet<I: IpExt>(ip_proto: IpProto, dst: u16) -> Buf<Vec<u8>> { |
| let addr = I::LOOPBACK_ADDRESS.get(); |
| (&[1u8, 2, 3, 4]) |
| .into_serializer() |
| .encapsulate(TcpSegmentBuilder::new( |
| addr, |
| addr, |
| NonZeroU16::new(1234).unwrap(), |
| NonZeroU16::new(dst).unwrap(), |
| 1, |
| None, |
| 1024, |
| )) |
| .encapsulate(I::PacketBuilder::new(addr, addr, 1, ip_proto.into())) |
| .encapsulate(EthernetFrameBuilder::new( |
| net_mac!("02:00:00:00:00:01"), |
| net_mac!("02:00:00:00:00:02"), |
| I::ETHER_TYPE, |
| 0, |
| )) |
| .serialize_vec_outer() |
| .unwrap() |
| .unwrap_b() |
| } |
| |
| // Check that we're only dropping for WLAN. |
| for device_class in [ |
| fhardware_network::DeviceClass::Virtual, |
| fhardware_network::DeviceClass::Ethernet, |
| fhardware_network::DeviceClass::Ppp, |
| fhardware_network::DeviceClass::Bridge, |
| ] { |
| assert_matches!( |
| workaround_drop_ssh_over_wlan( |
| &device_class, |
| make_packet::<I>(IpProto::Tcp, SSH_PORT).as_ref(), |
| ), |
| FilterResult::Accept |
| ); |
| } |
| |
| for (proto, dst, expect) in [ |
| (IpProto::Udp, 1234, FilterResult::Accept), |
| (IpProto::Udp, SSH_PORT, FilterResult::Accept), |
| (IpProto::Tcp, 1234, FilterResult::Accept), |
| (IpProto::Tcp, SSH_PORT, FilterResult::Drop), |
| ] { |
| assert_eq!( |
| workaround_drop_ssh_over_wlan( |
| &fhardware_network::DeviceClass::Wlan, |
| make_packet::<I>(proto, dst).as_ref() |
| ), |
| expect, |
| "proto={proto:?}, dst={dst}" |
| ); |
| } |
| } |
| } |