| // 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. |
| |
| //! FIDL Worker for the `fuchsia.net.interfaces.admin` API, including the |
| //! `DeviceControl`, `Control` and `AddressStateProvider` Protocols. |
| //! |
| //! Each instance of these protocols is tied to the lifetime of a particular |
| //! entity in the Netstack: |
| //! `DeviceControl` => device |
| //! `Control` => interface |
| //! `AddressStateProvider` => address |
| //! meaning the entity is removed if the protocol is closed, and the protocol is |
| //! closed if the entity is removed. Some protocols expose a `detach` method |
| //! that allows the lifetimes to be decoupled. |
| //! |
| //! These protocols (and their corresponding entities) are nested: |
| //! `DeviceControl` allows a new `Control` protocol to be connected (creating a |
| //! new interface on the device), while `Control` allows a new |
| //! `AddressStateProvider` protocol to be connected (creating a new address on |
| //! the interface). |
| //! |
| //! In general, each protocol is served by a "worker", either a |
| //! [`fuchsia_async::Task`] or a bare [`futures::future::Future`], that handles |
| //! incoming requests, spawns the workers for the nested protocols, and tears |
| //! down its associated entity if canceled. |
| //! |
| //! The `fuchsia.net.debug/Interfaces.GetAdmin` method exposes a backdoor that |
| //! allows clients to deviate from the behavior described above. Clients can |
| //! attach a new `Control` protocol to an existing interface with one-way |
| //! ownership semantics (removing the interface closes the protocol; closing |
| //! protocol does not remove the interface). |
| |
| use std::{collections::hash_map, num::NonZeroU16, ops::DerefMut as _, pin::pin}; |
| |
| use assert_matches::assert_matches; |
| use fidl::endpoints::{ProtocolMarker, ServerEnd}; |
| use fidl_fuchsia_hardware_network as fhardware_network; |
| use fidl_fuchsia_net as fnet; |
| use fidl_fuchsia_net_interfaces as fnet_interfaces; |
| use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin; |
| use fnet_interfaces_admin::GrantForInterfaceAuthorization; |
| use fuchsia_async as fasync; |
| use fuchsia_zircon::{self as zx, HandleBased, Rights}; |
| use futures::{ |
| future::FusedFuture as _, stream::FusedStream as _, FutureExt as _, SinkExt as _, |
| StreamExt as _, TryFutureExt as _, TryStreamExt as _, |
| }; |
| use net_types::{ |
| ip::{AddrSubnetEither, IpAddr, Ipv4, Ipv6}, |
| SpecifiedAddr, Witness, |
| }; |
| use netstack3_core::{ |
| device::{ |
| DeviceConfiguration, DeviceConfigurationUpdate, DeviceConfigurationUpdateError, DeviceId, |
| NdpConfiguration, NdpConfigurationUpdate, |
| }, |
| ip::{ |
| AddIpAddrSubnetError, AddrSubnetAndManualConfigEither, IpDeviceConfiguration, |
| IpDeviceConfigurationUpdate, Ipv4AddrConfig, Ipv4DeviceConfigurationUpdate, |
| Ipv6AddrManualConfig, Ipv6DeviceConfiguration, Ipv6DeviceConfigurationAndFlags, |
| Ipv6DeviceConfigurationUpdate, Lifetime, SetIpAddressPropertiesError, |
| UpdateIpConfigurationError, |
| }, |
| }; |
| |
| use crate::bindings::{ |
| devices::{self, EthernetInfo, StaticCommonInfo}, |
| netdevice_worker, |
| routes::{self, admin::RouteSet}, |
| util::{ |
| IllegalNonPositiveValueError, IntoCore as _, IntoFidl, RemoveResourceResultExt as _, |
| TryIntoCore, |
| }, |
| BindingId, Ctx, DeviceIdExt as _, Netstack, StackTime, |
| }; |
| |
| pub(crate) async fn serve(ns: Netstack, req: fnet_interfaces_admin::InstallerRequestStream) { |
| req.filter_map(|req| { |
| let req = match req { |
| Ok(req) => req, |
| Err(e) => { |
| if !e.is_closed() { |
| tracing::error!( |
| "{} request error {e:?}", |
| fnet_interfaces_admin::InstallerMarker::DEBUG_NAME |
| ); |
| } |
| return futures::future::ready(None); |
| } |
| }; |
| match req { |
| fnet_interfaces_admin::InstallerRequest::InstallDevice { |
| device, |
| device_control, |
| control_handle: _, |
| } => futures::future::ready(Some(fasync::Task::spawn( |
| run_device_control( |
| ns.clone(), |
| device, |
| device_control.into_stream().expect("failed to obtain stream"), |
| ) |
| .map(|r| { |
| r.unwrap_or_else(|e| tracing::warn!("device control finished with {:?}", e)) |
| }), |
| ))), |
| } |
| }) |
| .for_each_concurrent(None, |task| { |
| // Wait for all created devices on this installer to finish. |
| task |
| }) |
| .await; |
| } |
| |
| #[derive(thiserror::Error, Debug)] |
| enum DeviceControlError { |
| #[error("worker error: {0}")] |
| Worker(#[from] netdevice_worker::Error), |
| #[error("fidl error: {0}")] |
| Fidl(#[from] fidl::Error), |
| } |
| |
| async fn run_device_control( |
| mut ns: Netstack, |
| device: fidl::endpoints::ClientEnd<fhardware_network::DeviceMarker>, |
| req_stream: fnet_interfaces_admin::DeviceControlRequestStream, |
| ) -> Result<(), DeviceControlError> { |
| let worker = netdevice_worker::NetdeviceWorker::new(ns.ctx.clone(), device).await?; |
| let handler = worker.new_handler(); |
| let worker_fut = worker.run().map_err(DeviceControlError::Worker); |
| let stop_event = async_utils::event::Event::new(); |
| let req_stream = |
| req_stream.take_until(stop_event.wait_or_dropped()).map_err(DeviceControlError::Fidl); |
| let mut worker_fut = pin!(worker_fut); |
| let mut req_stream = pin!(req_stream); |
| let mut detached = false; |
| let mut tasks = futures::stream::FuturesUnordered::new(); |
| let res = loop { |
| let result = futures::select! { |
| req = req_stream.try_next() => req, |
| r = worker_fut => match r { |
| Ok(never) => match never {}, |
| Err(e) => Err(e) |
| }, |
| ready_task = tasks.next() => { |
| let () = ready_task.unwrap_or_else(|| ()); |
| continue; |
| } |
| }; |
| match result { |
| Ok(None) => { |
| // The client hung up; stop serving if not detached. |
| if !detached { |
| break Ok(()); |
| } |
| } |
| Ok(Some(req)) => match req { |
| fnet_interfaces_admin::DeviceControlRequest::CreateInterface { |
| port, |
| control, |
| options, |
| control_handle: _, |
| } => { |
| if let Some(interface_tasks) = create_interface( |
| port, |
| control, |
| options, |
| &mut ns, |
| &handler, |
| stop_event.wait_or_dropped(), |
| ) |
| .await |
| { |
| tasks.extend(interface_tasks); |
| } |
| } |
| fnet_interfaces_admin::DeviceControlRequest::Detach { control_handle: _ } => { |
| detached = true; |
| } |
| }, |
| Err(e) => break Err(e), |
| } |
| }; |
| |
| // Send a stop signal to all tasks. |
| assert!(stop_event.signal(), "event was already signaled"); |
| // Run all the tasks to completion. We sent the stop signal, they should all |
| // complete and perform interface cleanup. |
| tasks.collect::<()>().await; |
| |
| res |
| } |
| |
| const INTERFACES_ADMIN_CHANNEL_SIZE: usize = 16; |
| /// A wrapper over `fuchsia.net.interfaces.admin/Control` handles to express ownership semantics. |
| /// |
| /// If `owns_interface` is true, this handle 'owns' the interfaces, and when the handle closes the |
| /// interface should be removed. |
| pub(crate) struct OwnedControlHandle { |
| request_stream: fnet_interfaces_admin::ControlRequestStream, |
| control_handle: fnet_interfaces_admin::ControlControlHandle, |
| owns_interface: bool, |
| } |
| |
| impl OwnedControlHandle { |
| pub(crate) fn new_unowned( |
| handle: fidl::endpoints::ServerEnd<fnet_interfaces_admin::ControlMarker>, |
| ) -> OwnedControlHandle { |
| let (stream, control) = |
| handle.into_stream_and_control_handle().expect("failed to decompose control handle"); |
| OwnedControlHandle { |
| request_stream: stream, |
| control_handle: control, |
| owns_interface: false, |
| } |
| } |
| |
| // Constructs a new channel of `OwnedControlHandle` with no owner. |
| pub(crate) fn new_channel() -> ( |
| futures::channel::mpsc::Sender<OwnedControlHandle>, |
| futures::channel::mpsc::Receiver<OwnedControlHandle>, |
| ) { |
| futures::channel::mpsc::channel(INTERFACES_ADMIN_CHANNEL_SIZE) |
| } |
| |
| // Constructs a new channel of `OwnedControlHandle` with the given handle as the owner. |
| // TODO(https://fxbug.dev/42169142): This currently enforces that there is only ever one owner, |
| // which will need to be revisited to implement `Clone`. |
| pub(crate) async fn new_channel_with_owned_handle( |
| handle: fidl::endpoints::ServerEnd<fnet_interfaces_admin::ControlMarker>, |
| ) -> ( |
| futures::channel::mpsc::Sender<OwnedControlHandle>, |
| futures::channel::mpsc::Receiver<OwnedControlHandle>, |
| ) { |
| let (mut sender, receiver) = Self::new_channel(); |
| let (stream, control) = |
| handle.into_stream_and_control_handle().expect("failed to decompose control handle"); |
| sender |
| .send(OwnedControlHandle { |
| request_stream: stream, |
| control_handle: control, |
| owns_interface: true, |
| }) |
| .await |
| .expect("failed to attach initial control handle"); |
| (sender, receiver) |
| } |
| |
| // Consumes the OwnedControlHandle and returns its `control_handle`. |
| pub(crate) fn into_control_handle(self) -> fnet_interfaces_admin::ControlControlHandle { |
| self.control_handle |
| } |
| } |
| |
| /// Operates a fuchsia.net.interfaces.admin/DeviceControl.CreateInterface |
| /// request. |
| /// |
| /// Returns `Some([fuchsia_async::Task;2])` if an interface was created |
| /// successfully. The returned `Task`s must be polled to completion and are tied |
| /// to the created interface's lifetime. |
| async fn create_interface( |
| port: fhardware_network::PortId, |
| control: fidl::endpoints::ServerEnd<fnet_interfaces_admin::ControlMarker>, |
| options: fnet_interfaces_admin::Options, |
| ns: &mut Netstack, |
| handler: &netdevice_worker::DeviceHandler, |
| device_stopped_fut: async_utils::event::EventWaitResult, |
| ) -> Option<[fuchsia_async::Task<()>; 2]> { |
| tracing::debug!("creating interface from {:?} with {:?}", port, options); |
| let fnet_interfaces_admin::Options { name, metric, .. } = options; |
| let (control_sender, mut control_receiver) = |
| OwnedControlHandle::new_channel_with_owned_handle(control).await; |
| match handler |
| .add_port(ns, netdevice_worker::InterfaceOptions { name, metric }, port, control_sender) |
| .await |
| { |
| Ok((binding_id, status_stream, tx_task)) => { |
| let interface_control_task = fasync::Task::spawn(run_netdevice_interface_control( |
| ns.ctx.clone(), |
| binding_id, |
| status_stream, |
| device_stopped_fut, |
| control_receiver, |
| )); |
| Some([interface_control_task, tx_task]) |
| } |
| Err(e) => { |
| tracing::warn!("failed to add port {:?} to device: {:?}", port, e); |
| let removed_reason = match e { |
| netdevice_worker::Error::Client(e) => match e { |
| // Assume any fidl errors are port closed |
| // errors. |
| netdevice_client::Error::Fidl(_) => { |
| Some(fnet_interfaces_admin::InterfaceRemovedReason::PortClosed) |
| } |
| netdevice_client::Error::RxFlags(_) |
| | netdevice_client::Error::FrameType(_) |
| | netdevice_client::Error::NoProgress |
| | netdevice_client::Error::Config(_) |
| | netdevice_client::Error::LargeChain(_) |
| | netdevice_client::Error::Index(_, _) |
| | netdevice_client::Error::Pad(_, _) |
| | netdevice_client::Error::TxLength |
| | netdevice_client::Error::Open(_, _) |
| | netdevice_client::Error::Vmo(_, _) |
| | netdevice_client::Error::Fifo(_, _, _) |
| | netdevice_client::Error::VmoSize(_, _) |
| | netdevice_client::Error::Map(_, _) |
| | netdevice_client::Error::DeviceInfo(_) |
| | netdevice_client::Error::PortStatus(_) |
| | netdevice_client::Error::InvalidPortId(_) |
| | netdevice_client::Error::Attach(_, _) |
| | netdevice_client::Error::Detach(_, _) |
| | netdevice_client::Error::TooSmall { size: _, offset: _, length: _ } => None, |
| }, |
| netdevice_worker::Error::AlreadyInstalled(_) => { |
| Some(fnet_interfaces_admin::InterfaceRemovedReason::PortAlreadyBound) |
| } |
| netdevice_worker::Error::CantConnectToPort(_) |
| | netdevice_worker::Error::PortClosed => { |
| Some(fnet_interfaces_admin::InterfaceRemovedReason::PortClosed) |
| } |
| netdevice_worker::Error::ConfigurationNotSupported |
| | netdevice_worker::Error::MacNotUnicast { .. } |
| | netdevice_worker::Error::MismatchedRxFrameType { .. } => { |
| Some(fnet_interfaces_admin::InterfaceRemovedReason::BadPort) |
| } |
| netdevice_worker::Error::DuplicateName(_) => { |
| Some(fnet_interfaces_admin::InterfaceRemovedReason::DuplicateName) |
| } |
| netdevice_worker::Error::SystemResource(_) |
| | netdevice_worker::Error::InvalidPortInfo(_) => None, |
| }; |
| if let Some(removed_reason) = removed_reason { |
| // Retrieve the original control handle from the receiver. |
| let OwnedControlHandle { request_stream: _, control_handle, owns_interface: _ } = |
| control_receiver |
| .try_next() |
| .expect("expected control handle to be ready in the receiver") |
| .expect("expected receiver to not be closed/empty"); |
| control_handle.send_on_interface_removed(removed_reason).unwrap_or_else(|e| { |
| tracing::warn!("failed to send removed reason: {:?}", e); |
| }); |
| } |
| None |
| } |
| } |
| } |
| |
| /// Manages the lifetime of a newly created Netdevice interface, including spawning an |
| /// interface control worker, spawning a link state worker, and cleaning up the interface on |
| /// deletion. |
| async fn run_netdevice_interface_control< |
| S: futures::Stream<Item = netdevice_client::Result<netdevice_client::client::PortStatus>>, |
| >( |
| ctx: Ctx, |
| id: BindingId, |
| status_stream: S, |
| mut device_stopped_fut: async_utils::event::EventWaitResult, |
| control_receiver: futures::channel::mpsc::Receiver<OwnedControlHandle>, |
| ) { |
| let status_stream = status_stream.scan((), |(), status| { |
| futures::future::ready(match status { |
| Ok(netdevice_client::client::PortStatus { flags, mtu: _ }) => { |
| Some(DeviceState { online: flags.contains(fhardware_network::StatusFlags::ONLINE) }) |
| } |
| Err(e) => { |
| match e { |
| netdevice_client::Error::Fidl(e) if e.is_closed() => { |
| tracing::warn!( |
| "error operating port state stream {:?} for interface {}", |
| e, |
| id |
| ) |
| } |
| e => { |
| tracing::error!( |
| "error operating port state stream {:?} for interface {}", |
| e, |
| id |
| ) |
| } |
| } |
| // Terminate the stream in case of any errors. |
| None |
| } |
| }) |
| }); |
| |
| let (interface_control_stop_sender, interface_control_stop_receiver) = |
| futures::channel::oneshot::channel(); |
| |
| // Device-backed interfaces are always removable. |
| let removable = true; |
| let interface_control_fut = run_interface_control( |
| ctx.clone(), |
| id, |
| interface_control_stop_receiver, |
| control_receiver, |
| removable, |
| status_stream, |
| ) |
| .fuse(); |
| let mut interface_control_fut = pin!(interface_control_fut); |
| futures::select! { |
| o = device_stopped_fut => { |
| o.expect("event was orphaned"); |
| }, |
| () = interface_control_fut => (), |
| }; |
| if !interface_control_fut.is_terminated() { |
| // Cancel interface control and drive it to completion, allowing it to terminate each |
| // control handle. |
| interface_control_stop_sender |
| .send(fnet_interfaces_admin::InterfaceRemovedReason::PortClosed) |
| .expect("failed to cancel interface control"); |
| interface_control_fut.await |
| } |
| } |
| |
| pub(crate) struct DeviceState { |
| online: bool, |
| } |
| |
| /// Runs a worker to serve incoming `fuchsia.net.interfaces.admin/Control` |
| /// handles. |
| pub(crate) async fn run_interface_control<S: futures::Stream<Item = DeviceState>>( |
| ctx: Ctx, |
| id: BindingId, |
| mut stop_receiver: futures::channel::oneshot::Receiver< |
| fnet_interfaces_admin::InterfaceRemovedReason, |
| >, |
| control_receiver: futures::channel::mpsc::Receiver<OwnedControlHandle>, |
| removable: bool, |
| device_state: S, |
| ) { |
| // An event indicating that the individual control request streams should stop. |
| let cancel_request_streams = async_utils::event::Event::new(); |
| |
| let enabled_controller = enabled::InterfaceEnabledController::new(ctx.clone(), id); |
| let enabled_controller = &enabled_controller; |
| // A struct to retain per-handle state of the individual request streams in `control_receiver`. |
| struct ReqStreamState { |
| owns_interface: bool, |
| control_handle: fnet_interfaces_admin::ControlControlHandle, |
| ctx: Ctx, |
| id: BindingId, |
| } |
| |
| // Convert `control_receiver` (a stream-of-streams) into a stream of futures, where each future |
| // represents the termination of an inner `ControlRequestStream`. |
| let stream_of_fut = control_receiver.map( |
| |OwnedControlHandle { request_stream, control_handle, owns_interface }| { |
| let initial_state = |
| ReqStreamState { owns_interface, control_handle, ctx: ctx.clone(), id }; |
| // Attach `cancel_request_streams` as a short-circuit mechanism to stop handling new |
| // `ControlRequest`. |
| let request_stream = |
| request_stream.take_until(cancel_request_streams.wait_or_dropped()); |
| |
| // Convert the request stream into a future, dispatching each incoming |
| // `ControlRequest` and retaining the `ReqStreamState` along the way. |
| async_utils::fold::fold_while( |
| request_stream, |
| initial_state, |
| |mut state, request| async move { |
| let ReqStreamState { ctx, id, owns_interface, control_handle: _ } = &mut state; |
| match request { |
| Err(e) => { |
| tracing::error!( |
| "error operating {} stream for interface {}: {:?}", |
| fnet_interfaces_admin::ControlMarker::DEBUG_NAME, |
| id, |
| e |
| ); |
| async_utils::fold::FoldWhile::Continue(state) |
| } |
| Ok(req) => { |
| match dispatch_control_request( |
| req, |
| ctx, |
| *id, |
| removable, |
| owns_interface, |
| enabled_controller, |
| ) |
| .await |
| { |
| Err(e) => { |
| tracing::error!( |
| "failed to handle request for interface {}: {:?}", |
| id, |
| e |
| ); |
| async_utils::fold::FoldWhile::Continue(state) |
| } |
| Ok(ControlRequestResult::Continue) => { |
| async_utils::fold::FoldWhile::Continue(state) |
| } |
| Ok(ControlRequestResult::Remove) => { |
| // Short-circuit the stream, user called |
| // remove. |
| async_utils::fold::FoldWhile::Done(state.control_handle) |
| } |
| } |
| } |
| } |
| }, |
| ) |
| }, |
| ); |
| // Enable the stream of futures to be polled concurrently. |
| let mut stream_of_fut = stream_of_fut.buffer_unordered(std::usize::MAX); |
| |
| let device_state = { |
| device_state |
| .then(|DeviceState { online }| async move { |
| enabled_controller.set_phy_up(online).await; |
| }) |
| .fuse() |
| .collect::<()>() |
| }; |
| |
| let (remove_reason, removal_requester) = { |
| // Drive the `ControlRequestStreams` to completion, short-circuiting if |
| // an owner terminates or if interface removal is requested. |
| let mut interface_control_stream = stream_of_fut.by_ref().filter_map(|stream_result| { |
| futures::future::ready(match stream_result { |
| async_utils::fold::FoldResult::StreamEnded(ReqStreamState { |
| owns_interface, |
| control_handle: _, |
| ctx: _, |
| id: _, |
| }) => { |
| // If we own the interface, return `Some(None)` to stop |
| // operating on the request streams. |
| owns_interface.then(|| None) |
| } |
| async_utils::fold::FoldResult::ShortCircuited(control_handle) => { |
| // If it was short-circuited by a call to remove, return |
| // `Some(Some(control_handle))` so we stop operating on |
| // the request streams and inform the requester of |
| // removal when done. |
| Some(Some(control_handle)) |
| } |
| }) |
| }); |
| |
| let mut device_state = pin!(device_state); |
| futures::select! { |
| // One of the interface's owning channels hung up or `Remove` was |
| // called; inform the other channels. |
| removal_requester = interface_control_stream.next() => { |
| (fnet_interfaces_admin::InterfaceRemovedReason::User, removal_requester.flatten()) |
| } |
| // Cancelation event was received with a specified remove reason. |
| reason = stop_receiver => (reason.expect("failed to receive stop"), None), |
| // Device state stream ended, assume device was removed. |
| () = device_state => (fnet_interfaces_admin::InterfaceRemovedReason::PortClosed, None), |
| } |
| }; |
| |
| // Close the control_receiver, preventing new RequestStreams from attaching. |
| stream_of_fut.get_mut().get_mut().close(); |
| // Cancel the active request streams, and drive the remaining `ControlRequestStreams` to |
| // completion, allowing each handle to send termination events. |
| assert!(cancel_request_streams.signal(), "expected the event to be unsignaled"); |
| let control_handles = if !stream_of_fut.is_terminated() { |
| // Accumulate all the control handles first so we stop operating on |
| // them. |
| stream_of_fut |
| .map(|fold_result| match fold_result { |
| async_utils::fold::FoldResult::StreamEnded(ReqStreamState { |
| owns_interface: _, |
| control_handle, |
| ctx: _, |
| id: _, |
| }) => control_handle, |
| async_utils::fold::FoldResult::ShortCircuited(control_handle) => control_handle, |
| }) |
| // Append the client that requested that the interface be removed |
| // (if there is one) so it receives the terminal event too. |
| .chain(futures::stream::iter(removal_requester)) |
| .collect::<Vec<_>>() |
| .await |
| } else { |
| // Drop the terminated stream of fut to get rid of context borrows. |
| std::mem::drop(stream_of_fut); |
| Vec::new() |
| }; |
| // Cancel the `AddressStateProvider` workers and drive them to completion. |
| let address_state_providers = { |
| let core_id = |
| ctx.bindings_ctx().devices.get_core_id(id).expect("missing device info for interface"); |
| core_id.external_state().with_common_info_mut(|i| { |
| futures::stream::FuturesUnordered::from_iter(i.addresses.values_mut().map( |
| |devices::AddressInfo { |
| address_state_provider: devices::FidlWorkerInfo { worker, cancelation_sender }, |
| assignment_state_sender: _, |
| }| { |
| if let Some(cancelation_sender) = cancelation_sender.take() { |
| cancelation_sender |
| .send(AddressStateProviderCancellationReason::InterfaceRemoved) |
| .expect("failed to stop AddressStateProvider"); |
| } |
| worker.clone() |
| }, |
| )) |
| }) |
| }; |
| address_state_providers.collect::<()>().await; |
| |
| // Nothing else should be borrowing ctx by now. Moving to a mutable bind |
| // proves this. |
| let mut ctx = ctx; |
| remove_interface(&mut ctx, id).await; |
| |
| // Send the termination reason for all handles we had on removal. |
| control_handles.into_iter().for_each(|control_handle| { |
| control_handle.send_on_interface_removed(remove_reason).unwrap_or_else(|e| { |
| tracing::error!("failed to send terminal event: {:?} for interface {}", e, id) |
| }); |
| }); |
| } |
| |
| enum ControlRequestResult { |
| Continue, |
| Remove, |
| } |
| |
| /// Serves a `fuchsia.net.interfaces.admin/Control` Request. |
| async fn dispatch_control_request( |
| req: fnet_interfaces_admin::ControlRequest, |
| ctx: &mut Ctx, |
| id: BindingId, |
| removable: bool, |
| owns_interface: &mut bool, |
| enabled_controller: &enabled::InterfaceEnabledController, |
| ) -> Result<ControlRequestResult, fidl::Error> { |
| tracing::debug!("serving {:?}", req); |
| |
| match req { |
| fnet_interfaces_admin::ControlRequest::AddAddress { |
| address, |
| parameters, |
| address_state_provider, |
| control_handle: _, |
| } => Ok(add_address(ctx, id, address, parameters, address_state_provider)), |
| fnet_interfaces_admin::ControlRequest::RemoveAddress { address, responder } => { |
| responder.send(Ok(remove_address(ctx, id, address).await)) |
| } |
| fnet_interfaces_admin::ControlRequest::GetId { responder } => responder.send(id.get()), |
| fnet_interfaces_admin::ControlRequest::SetConfiguration { config, responder } => { |
| responder.send(set_configuration(ctx, id, config).as_ref().map_err(|e| *e)) |
| } |
| fnet_interfaces_admin::ControlRequest::GetConfiguration { responder } => { |
| responder.send(Ok(&get_configuration(ctx, id))) |
| } |
| fnet_interfaces_admin::ControlRequest::Enable { responder } => { |
| responder.send(Ok(enabled_controller.set_admin_enabled(true).await)) |
| } |
| fnet_interfaces_admin::ControlRequest::Disable { responder } => { |
| responder.send(Ok(enabled_controller.set_admin_enabled(false).await)) |
| } |
| fnet_interfaces_admin::ControlRequest::Detach { control_handle: _ } => { |
| *owns_interface = false; |
| Ok(()) |
| } |
| fnet_interfaces_admin::ControlRequest::Remove { responder } => { |
| if removable { |
| return responder.send(Ok(())).map(|()| ControlRequestResult::Remove); |
| } |
| responder.send(Err(fnet_interfaces_admin::ControlRemoveError::NotAllowed)) |
| } |
| fnet_interfaces_admin::ControlRequest::GetAuthorizationForInterface { responder } => { |
| responder.send(grant_for_interface(ctx, id)) |
| } |
| } |
| .map(|()| ControlRequestResult::Continue) |
| } |
| |
| /// Cleans up and removes the specified NetDevice interface. |
| /// |
| /// # Panics |
| /// |
| /// Panics if `id` points to a loopback device. |
| async fn remove_interface(ctx: &mut Ctx, id: BindingId) { |
| let (devices::StaticNetdeviceInfo { handler }, weak_id) = { |
| let core_id = ctx |
| .bindings_ctx() |
| .devices |
| .remove_device(id) |
| .expect("device was not removed since retrieval"); |
| // Keep a weak ID around to debug pending destruction. |
| let weak_id = core_id.downgrade(); |
| match core_id { |
| DeviceId::Ethernet(core_id) => { |
| // We want to remove the routes on the device _after_ we mark |
| // the device for deletion (by calling `remove_device`) so |
| // that we don't race with any new routes being added through |
| // that device. |
| let result = ctx.api().device().remove_device(core_id); |
| ctx.bindings_ctx().remove_routes_on_device(&weak_id).await; |
| let EthernetInfo { netdevice, .. } = result |
| .map_deferred(|d| d.into_future("ethernet device", &id)) |
| .into_future() |
| .await; |
| (netdevice, weak_id) |
| } |
| DeviceId::Loopback(core_id) => { |
| // We want to remove the routes on the device _after_ we mark |
| // the device for deletion (by calling `remove_device`) so |
| // that we don't race with any new routes being added through |
| // that device. |
| let result = ctx.api().device().remove_device(core_id); |
| ctx.bindings_ctx().remove_routes_on_device(&weak_id).await; |
| let devices::LoopbackInfo { |
| static_common_info: _, |
| dynamic_common_info: _, |
| rx_notifier: _, |
| } = result |
| .map_deferred(|d| d.into_future("loopback device", &id)) |
| .into_future() |
| .await; |
| // Allow the loopback interface to be removed as part of clean |
| // shutdown, but emit a warning about it. |
| tracing::warn!("loopback interface was removed"); |
| return; |
| } |
| DeviceId::PureIp(core_id) => { |
| // We want to remove the routes on the device _after_ we mark |
| // the device for deletion (by calling `remove_device`) so |
| // that we don't race with any new routes being added through |
| // that device. |
| let result = ctx.api().device().remove_device(core_id); |
| ctx.bindings_ctx().remove_routes_on_device(&weak_id).await; |
| let devices::PureIpDeviceInfo { netdevice, .. } = result |
| .map_deferred(|d| d.into_future("pure ip device", &id)) |
| .into_future() |
| .await; |
| (netdevice, weak_id) |
| } |
| } |
| }; |
| |
| let id = weak_id.bindings_id(); |
| handler.uninstall().await.unwrap_or_else(|e| { |
| tracing::warn!("error uninstalling netdevice handler for interface {:?}: {:?}", id, e) |
| }); |
| tracing::info!("device {id:?} removal complete"); |
| } |
| |
| /// Removes the given `address` from the interface with the given `id`. |
| /// |
| /// Returns `true` if the address existed and was removed; otherwise `false`. |
| async fn remove_address(ctx: &mut Ctx, id: BindingId, address: fnet::Subnet) -> bool { |
| let specified_addr = match address.addr.try_into_core() { |
| Ok(addr) => addr, |
| Err(e) => { |
| tracing::warn!("not removing unspecified address {:?}: {:?}", address.addr, e); |
| return false; |
| } |
| }; |
| let core_id = |
| ctx.bindings_ctx().devices.get_core_id(id).expect("missing device info for interface"); |
| let Some((worker, cancelation_sender)) = ({ |
| core_id.external_state().with_common_info_mut(|i| { |
| i.addresses.get_mut(&specified_addr).map( |
| |devices::AddressInfo { |
| address_state_provider: devices::FidlWorkerInfo { worker, cancelation_sender }, |
| assignment_state_sender: _, |
| }| (worker.clone(), cancelation_sender.take()), |
| ) |
| }) |
| }) else { |
| // Even if the address is not in the bindings HashMap of addresses, |
| // it could still be in core (e.g. if it's a loopback address or a |
| // SLAAC address). |
| // TODO(https://fxbug.dev/42084781): Rather than falling back in this way, |
| // make it impossible to make this mistake by centralizing the |
| // "source of truth" for addresses on a device. |
| return match ctx.api().device_ip_any().del_ip_addr(&core_id, specified_addr) { |
| Ok(result) => { |
| let _: AddrSubnetEither = result |
| .map_deferred(|d| { |
| d.map_left(|l| { |
| l.into_future("device addr", &specified_addr).map(Into::into) |
| }) |
| .map_right(|r| { |
| r.into_future("device addr", &specified_addr).map(Into::into) |
| }) |
| }) |
| .into_future() |
| .await; |
| true |
| } |
| Err(netstack3_core::error::NotFoundError) => false, |
| }; |
| }; |
| let did_cancel_worker = match cancelation_sender { |
| Some(cancelation_sender) => { |
| cancelation_sender |
| .send(AddressStateProviderCancellationReason::UserRemoved) |
| .expect("failed to stop AddressStateProvider"); |
| true |
| } |
| // The worker was already canceled by some other task. |
| None => false, |
| }; |
| // Wait for the worker to finish regardless of if we were the task to cancel |
| // it. Doing so prevents us from prematurely returning while the address is |
| // pending cleanup. |
| worker.await; |
| // Because the worker removes the address on teardown, `did_cancel_worker` |
| // is a suitable proxy for `did_remove_addr`. |
| return did_cancel_worker; |
| } |
| |
| /// Sets the provided `config` on the interface with the given `id`. |
| /// |
| /// Returns the previously set configuration on the interface. |
| fn set_configuration( |
| ctx: &mut Ctx, |
| id: BindingId, |
| config: fnet_interfaces_admin::Configuration, |
| ) -> fnet_interfaces_admin::ControlSetConfigurationResult { |
| let core_id = ctx |
| .bindings_ctx() |
| .devices |
| .get_core_id(id) |
| .expect("device lifetime should be tied to channel lifetime"); |
| |
| let fnet_interfaces_admin::Configuration { ipv4, ipv6, .. } = config; |
| |
| let (ipv4_update, arp) = match ipv4 { |
| Some(fnet_interfaces_admin::Ipv4Configuration { |
| igmp, |
| multicast_forwarding, |
| forwarding, |
| arp, |
| __source_breaking, |
| }) => { |
| if let Some(_) = igmp { |
| tracing::warn!( |
| "TODO(https://fxbug.dev/42071402): support IGMP configuration changes" |
| ) |
| } |
| if let Some(_) = multicast_forwarding { |
| tracing::warn!( |
| "TODO(https://fxbug.dev/323052525): setting multicast_forwarding not yet supported" |
| ) |
| } |
| ( |
| Some(Ipv4DeviceConfigurationUpdate { |
| ip_config: IpDeviceConfigurationUpdate { |
| forwarding_enabled: forwarding, |
| ..Default::default() |
| }, |
| ..Default::default() |
| }), |
| arp.map(TryIntoCore::try_into_core).transpose().map_err(|e| match e { |
| IllegalNonPositiveValueError::Zero => { |
| fnet_interfaces_admin::ControlSetConfigurationError::IllegalZeroValue |
| } |
| IllegalNonPositiveValueError::Negative => { |
| fnet_interfaces_admin::ControlSetConfigurationError::IllegalNegativeValue |
| } |
| })?, |
| ) |
| } |
| None => (None, None), |
| }; |
| |
| let (ipv6_update, ndp) = match ipv6 { |
| Some(fnet_interfaces_admin::Ipv6Configuration { |
| mld, |
| multicast_forwarding, |
| forwarding, |
| ndp, |
| __source_breaking, |
| }) => { |
| if let Some(_) = mld { |
| tracing::warn!( |
| "TODO(https://fxbug.dev/42071402): support MLD configuration changes" |
| ) |
| } |
| if let Some(_) = multicast_forwarding { |
| tracing::warn!( |
| "TODO(https://fxbug.dev/323052525): setting multicast_forwarding not yet supported" |
| ) |
| } |
| |
| let fnet_interfaces_admin::NdpConfiguration { nud, dad, __source_breaking } = |
| ndp.unwrap_or_default(); |
| |
| let fnet_interfaces_admin::DadConfiguration { |
| transmits: dad_transmits, |
| __source_breaking, |
| } = dad.unwrap_or_default(); |
| |
| ( |
| Some(Ipv6DeviceConfigurationUpdate { |
| ip_config: IpDeviceConfigurationUpdate { |
| forwarding_enabled: forwarding, |
| ..Default::default() |
| }, |
| dad_transmits: dad_transmits.map(|v| NonZeroU16::new(v)), |
| ..Default::default() |
| }), |
| nud.map(|nud| Ok(NdpConfigurationUpdate { nud: Some(nud.try_into_core()?) })) |
| .transpose() |
| .map_err(|e| match e { |
| IllegalNonPositiveValueError::Zero => { |
| fnet_interfaces_admin::ControlSetConfigurationError::IllegalZeroValue |
| } |
| IllegalNonPositiveValueError::Negative => { |
| fnet_interfaces_admin::ControlSetConfigurationError::IllegalNegativeValue |
| } |
| })?, |
| ) |
| } |
| None => (None, None), |
| }; |
| |
| let ipv4_update = ipv4_update |
| .map(|ipv4_update| { |
| ctx.api().device_ip::<Ipv4>().new_configuration_update(&core_id, ipv4_update) |
| }) |
| .transpose() |
| .map_err(|e| match e { |
| UpdateIpConfigurationError::ForwardingNotSupported => { |
| fnet_interfaces_admin::ControlSetConfigurationError::Ipv4ForwardingUnsupported |
| } |
| })?; |
| let ipv6_update = ipv6_update |
| .map(|ipv6_update| { |
| ctx.api().device_ip::<Ipv6>().new_configuration_update(&core_id, ipv6_update) |
| }) |
| .transpose() |
| .map_err(|e| match e { |
| UpdateIpConfigurationError::ForwardingNotSupported => { |
| fnet_interfaces_admin::ControlSetConfigurationError::Ipv6ForwardingUnsupported |
| } |
| })?; |
| let device_update = ctx |
| .api() |
| .device_any() |
| .new_configuration_update(&core_id, DeviceConfigurationUpdate { arp, ndp }) |
| .map_err(|e| match e { |
| DeviceConfigurationUpdateError::ArpNotSupported => { |
| fnet_interfaces_admin::ControlSetConfigurationError::ArpNotSupported |
| } |
| DeviceConfigurationUpdateError::NdpNotSupported => { |
| fnet_interfaces_admin::ControlSetConfigurationError::NdpNotSupported |
| } |
| })?; |
| |
| let DeviceConfigurationUpdate { arp, ndp } = |
| ctx.api().device_any().apply_configuration(device_update); |
| let nud = ndp.and_then(|NdpConfigurationUpdate { nud }| nud); |
| |
| // Apply both updates now that we have checked for errors and get the deltas |
| // back. If we didn't apply updates, use the default struct to construct the |
| // delta responses. |
| let ipv4 = ipv4_update.map(|u| { |
| let Ipv4DeviceConfigurationUpdate { ip_config } = |
| ctx.api().device_ip::<Ipv4>().apply_configuration(u); |
| let IpDeviceConfigurationUpdate { forwarding_enabled, ip_enabled: _, gmp_enabled: _ } = |
| ip_config; |
| fnet_interfaces_admin::Ipv4Configuration { |
| forwarding: forwarding_enabled, |
| multicast_forwarding: None, |
| igmp: None, |
| arp: arp.map(IntoFidl::into_fidl), |
| __source_breaking: fidl::marker::SourceBreaking, |
| } |
| }); |
| |
| let ipv6 = ipv6_update.map(|u| { |
| let Ipv6DeviceConfigurationUpdate { |
| ip_config, |
| dad_transmits, |
| max_router_solicitations: _, |
| slaac_config: _, |
| } = ctx.api().device_ip::<Ipv6>().apply_configuration(u); |
| |
| let dad = dad_transmits.map(|transmits| fnet_interfaces_admin::DadConfiguration { |
| transmits: Some(transmits.map_or(0, NonZeroU16::get)), |
| __source_breaking: fidl::marker::SourceBreaking, |
| }); |
| |
| let ndp = fnet_interfaces_admin::NdpConfiguration { |
| nud: nud.map(IntoFidl::into_fidl), |
| dad, |
| __source_breaking: fidl::marker::SourceBreaking, |
| }; |
| let ndp = (ndp != Default::default()).then_some(ndp); |
| |
| let IpDeviceConfigurationUpdate { forwarding_enabled, ip_enabled: _, gmp_enabled: _ } = |
| ip_config; |
| fnet_interfaces_admin::Ipv6Configuration { |
| forwarding: forwarding_enabled, |
| multicast_forwarding: None, |
| mld: None, |
| ndp, |
| __source_breaking: fidl::marker::SourceBreaking, |
| } |
| }); |
| Ok(fnet_interfaces_admin::Configuration { |
| ipv4, |
| ipv6, |
| __source_breaking: fidl::marker::SourceBreaking, |
| }) |
| } |
| |
| /// Returns the configuration used for the interface with the given `id`. |
| fn get_configuration(ctx: &mut Ctx, id: BindingId) -> fnet_interfaces_admin::Configuration { |
| let core_id = ctx |
| .bindings_ctx() |
| .devices |
| .get_core_id(id) |
| .expect("device lifetime should be tied to channel lifetime"); |
| |
| let DeviceConfiguration { arp, ndp } = ctx.api().device_any().get_configuration(&core_id); |
| |
| let ipv4 = Some(fnet_interfaces_admin::Ipv4Configuration { |
| forwarding: Some( |
| ctx.api() |
| .device_ip::<Ipv4>() |
| .get_configuration(&core_id) |
| .config |
| .ip_config |
| .forwarding_enabled, |
| ), |
| // TODO(https://fxbug.dev/42071402): Support IGMP configuration |
| // changes. |
| igmp: None, |
| // TODO(https://fxbug.dev/323052525): Support multicast forwarding |
| // configuration changes. |
| multicast_forwarding: None, |
| arp: arp.map(IntoFidl::into_fidl), |
| __source_breaking: fidl::marker::SourceBreaking, |
| }); |
| |
| let Ipv6DeviceConfigurationAndFlags { |
| config: |
| Ipv6DeviceConfiguration { |
| dad_transmits, |
| max_router_solicitations: _, |
| slaac_config: _, |
| ip_config: IpDeviceConfiguration { gmp_enabled: _, forwarding_enabled }, |
| }, |
| flags: _, |
| } = ctx.api().device_ip::<Ipv6>().get_configuration(&core_id); |
| |
| let nud = ndp.map(|NdpConfiguration { nud }| nud); |
| |
| let ndp = Some(fnet_interfaces_admin::NdpConfiguration { |
| nud: nud.map(IntoFidl::into_fidl), |
| dad: Some(fnet_interfaces_admin::DadConfiguration { |
| transmits: Some(dad_transmits.map_or(0, NonZeroU16::get)), |
| __source_breaking: fidl::marker::SourceBreaking, |
| }), |
| __source_breaking: fidl::marker::SourceBreaking, |
| }); |
| |
| let ipv6 = Some(fnet_interfaces_admin::Ipv6Configuration { |
| forwarding: Some(forwarding_enabled), |
| // TODO(https://fxbug.dev/42071402): Support MLD configuration |
| // changes. |
| mld: None, |
| // TODO(https://fxbug.dev/323052525): Support multicast forwarding |
| // configuration changes. |
| multicast_forwarding: None, |
| ndp, |
| __source_breaking: fidl::marker::SourceBreaking, |
| }); |
| |
| fnet_interfaces_admin::Configuration { |
| ipv4, |
| ipv6, |
| __source_breaking: fidl::marker::SourceBreaking, |
| } |
| } |
| |
| /// Adds the given `address` to the interface with the given `id`. |
| /// |
| /// If the address cannot be added, the appropriate removal reason will be sent |
| /// to the address_state_provider. |
| fn add_address( |
| ctx: &mut Ctx, |
| id: BindingId, |
| address: fnet::Subnet, |
| params: fnet_interfaces_admin::AddressParameters, |
| address_state_provider: ServerEnd<fnet_interfaces_admin::AddressStateProviderMarker>, |
| ) { |
| let (req_stream, control_handle) = address_state_provider |
| .into_stream_and_control_handle() |
| .expect("failed to decompose AddressStateProvider handle"); |
| let addr_subnet_either: AddrSubnetEither = match address.try_into_core() { |
| Ok(addr) => addr, |
| Err(e) => { |
| tracing::warn!("not adding invalid address {:?} to interface {}: {:?}", address, id, e); |
| send_address_removal_event( |
| address.addr.into_core(), |
| id, |
| control_handle, |
| fnet_interfaces_admin::AddressRemovalReason::Invalid, |
| ); |
| return; |
| } |
| }; |
| let specified_addr = addr_subnet_either.addr(); |
| |
| if params.temporary.unwrap_or(false) { |
| todo!("https://fxbug.dev/42056881: support adding temporary addresses"); |
| } |
| const INFINITE_NANOS: i64 = zx::Time::INFINITE.into_nanos(); |
| let initial_properties = |
| params.initial_properties.unwrap_or(fnet_interfaces_admin::AddressProperties::default()); |
| let valid_lifetime_end = initial_properties.valid_lifetime_end.unwrap_or(INFINITE_NANOS); |
| |
| match initial_properties |
| .preferred_lifetime_info |
| .unwrap_or(fnet_interfaces::PreferredLifetimeInfo::PreferredUntil(INFINITE_NANOS)) |
| { |
| fnet_interfaces::PreferredLifetimeInfo::Deprecated(_) => { |
| todo!("https://fxbug.dev/42056881: support adding deprecated addresses") |
| } |
| fnet_interfaces::PreferredLifetimeInfo::PreferredUntil(preferred_until) => { |
| if preferred_until != INFINITE_NANOS { |
| tracing::warn!( |
| "TODO(https://fxbug.dev/42056881): ignoring preferred_until: {:?}", |
| preferred_until |
| ); |
| } |
| } |
| } |
| |
| let valid_until = if valid_lifetime_end == INFINITE_NANOS { |
| Lifetime::Infinite |
| } else { |
| Lifetime::Finite(StackTime(fasync::Time::from_nanos(valid_lifetime_end))) |
| }; |
| |
| let addr_subnet_either = match addr_subnet_either { |
| AddrSubnetEither::V4(addr_subnet) => { |
| AddrSubnetAndManualConfigEither::V4(addr_subnet, Ipv4AddrConfig { valid_until }) |
| } |
| AddrSubnetEither::V6(addr_subnet) => { |
| AddrSubnetAndManualConfigEither::V6(addr_subnet, Ipv6AddrManualConfig { valid_until }) |
| } |
| }; |
| |
| let core_id = |
| ctx.bindings_ctx().devices.get_core_id(id).expect("missing device info for interface"); |
| core_id.external_state().with_common_info_mut(|i| { |
| let vacant_address_entry = match i.addresses.entry(specified_addr) { |
| hash_map::Entry::Occupied(_occupied) => { |
| send_address_removal_event( |
| address.addr.into_core(), |
| id, |
| control_handle, |
| fnet_interfaces_admin::AddressRemovalReason::AlreadyAssigned, |
| ); |
| return; |
| } |
| hash_map::Entry::Vacant(vacant) => vacant, |
| }; |
| // Cancelation mechanism for the `AddressStateProvider` worker. |
| let (cancelation_sender, cancelation_receiver) = futures::channel::oneshot::channel(); |
| // Sender/receiver for updates in `AddressAssignmentState`, as |
| // published by Core. |
| let (assignment_state_sender, assignment_state_receiver) = |
| futures::channel::mpsc::unbounded(); |
| // Spawn the `AddressStateProvider` worker, which during |
| // initialization, will add the address to Core. |
| let worker = fasync::Task::spawn(run_address_state_provider( |
| ctx.clone(), |
| addr_subnet_either, |
| params.add_subnet_route.unwrap_or(false), |
| id, |
| control_handle, |
| req_stream, |
| assignment_state_receiver, |
| cancelation_receiver, |
| )) |
| .shared(); |
| let _: &mut devices::AddressInfo = vacant_address_entry.insert(devices::AddressInfo { |
| address_state_provider: devices::FidlWorkerInfo { |
| worker, |
| cancelation_sender: Some(cancelation_sender), |
| }, |
| assignment_state_sender, |
| }); |
| }) |
| } |
| |
| fn grant_for_interface(ctx: &mut Ctx, id: BindingId) -> GrantForInterfaceAuthorization { |
| let core_id = ctx |
| .bindings_ctx() |
| .devices |
| .get_core_id(id) |
| .expect("device lifetime should be tied to channel lifetime"); |
| |
| let external_state = core_id.external_state(); |
| let StaticCommonInfo { authorization_token, tx_notifier: _ } = |
| external_state.static_common_info(); |
| |
| GrantForInterfaceAuthorization { |
| interface_id: id.get(), |
| token: authorization_token |
| .duplicate_handle(Rights::TRANSFER | Rights::DUPLICATE) |
| .expect("failed to duplicate handle"), |
| } |
| } |
| |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
| pub(crate) enum AddressStateProviderCancellationReason { |
| UserRemoved, |
| DadFailed, |
| InterfaceRemoved, |
| } |
| |
| impl From<AddressStateProviderCancellationReason> for fnet_interfaces_admin::AddressRemovalReason { |
| fn from(value: AddressStateProviderCancellationReason) -> Self { |
| match value { |
| AddressStateProviderCancellationReason::UserRemoved => { |
| fnet_interfaces_admin::AddressRemovalReason::UserRemoved |
| } |
| AddressStateProviderCancellationReason::DadFailed => { |
| fnet_interfaces_admin::AddressRemovalReason::DadFailed |
| } |
| AddressStateProviderCancellationReason::InterfaceRemoved => { |
| fnet_interfaces_admin::AddressRemovalReason::InterfaceRemoved |
| } |
| } |
| } |
| } |
| |
| /// A worker for `fuchsia.net.interfaces.admin/AddressStateProvider`. |
| async fn run_address_state_provider( |
| mut ctx: Ctx, |
| addr_subnet_and_config: AddrSubnetAndManualConfigEither<StackTime>, |
| add_subnet_route: bool, |
| id: BindingId, |
| control_handle: fnet_interfaces_admin::AddressStateProviderControlHandle, |
| req_stream: fnet_interfaces_admin::AddressStateProviderRequestStream, |
| mut assignment_state_receiver: futures::channel::mpsc::UnboundedReceiver< |
| fnet_interfaces::AddressAssignmentState, |
| >, |
| mut stop_receiver: futures::channel::oneshot::Receiver<AddressStateProviderCancellationReason>, |
| ) { |
| let addr_subnet_either = addr_subnet_and_config.addr_subnet_either(); |
| let (address, subnet) = addr_subnet_either.addr_subnet(); |
| struct StateInCore { |
| address: bool, |
| subnet_route: |
| Option<(routes::admin::UserRouteSet<Ipv4>, routes::admin::UserRouteSet<Ipv6>)>, |
| } |
| |
| // Add the address to Core. Note that even though we verified the address |
| // was absent from `ctx` before spawning this worker, it's still possible |
| // for the address to exist in core (e.g. auto-configured addresses such as |
| // loopback or SLAAC). |
| let add_to_core_result = { |
| let device_id = ctx.bindings_ctx().devices.get_core_id(id).expect("interface not found"); |
| ctx.api().device_ip_any().add_ip_addr_subnet(&device_id, addr_subnet_and_config) |
| }; |
| let (state_to_remove_from_core, removal_reason) = match add_to_core_result { |
| Err(e) => { |
| let removal_reason = match e { |
| AddIpAddrSubnetError::Exists => { |
| fnet_interfaces_admin::AddressRemovalReason::AlreadyAssigned |
| } |
| AddIpAddrSubnetError::InvalidAddr => { |
| fnet_interfaces_admin::AddressRemovalReason::Invalid |
| } |
| }; |
| send_address_removal_event(*address, id, control_handle.clone(), removal_reason); |
| // The address wasn't added, so don't attempt to remove it. In the |
| // `AlreadyAssigned` case, this ensures we don't accidentally remove |
| // an address we didn't add. |
| (StateInCore { address: false, subnet_route: None }, None) |
| } |
| Ok(()) => { |
| let state_to_remove = if add_subnet_route { |
| let bindings_ctx = ctx.bindings_ctx(); |
| let core_id = bindings_ctx |
| .devices |
| .get_core_id(id) |
| .expect("missing device info for interface") |
| .downgrade(); |
| |
| let route_set_v4 = routes::admin::UserRouteSet::from_main_table(ctx.clone()); |
| let route_set_v6 = routes::admin::UserRouteSet::from_main_table(ctx.clone()); |
| |
| let add_route_result = match subnet { |
| net_types::ip::SubnetEither::V4(subnet) => { |
| let entry = netstack3_core::routes::AddableEntry { |
| subnet, |
| device: core_id, |
| metric: netstack3_core::routes::AddableMetric::MetricTracksInterface, |
| gateway: None, |
| }; |
| route_set_v4.apply_route_op(routes::RouteOp::Add(entry)).await |
| } |
| net_types::ip::SubnetEither::V6(subnet) => { |
| let entry = netstack3_core::routes::AddableEntry { |
| subnet, |
| device: core_id, |
| metric: netstack3_core::routes::AddableMetric::MetricTracksInterface, |
| gateway: None, |
| }; |
| route_set_v6.apply_route_op(routes::RouteOp::Add(entry)).await |
| } |
| }; |
| |
| match add_route_result { |
| Err(e) => { |
| tracing::warn!("failed to add subnet route {:?}: {:?}", subnet, e); |
| route_set_v4.close().await; |
| route_set_v6.close().await; |
| StateInCore { address: true, subnet_route: None } |
| } |
| Ok(outcome) => { |
| assert_matches!( |
| outcome, |
| routes::ChangeOutcome::Changed, |
| "subnet route for {subnet:?} should be new to \ |
| route set, since route set was created especially \ |
| for this" |
| ); |
| |
| StateInCore { |
| address: true, |
| subnet_route: Some((route_set_v4, route_set_v6)), |
| } |
| } |
| } |
| } else { |
| StateInCore { address: true, subnet_route: None } |
| }; |
| |
| control_handle.send_on_address_added().unwrap_or_else(|e| { |
| tracing::error!( |
| "failed to send address added event for addr {:?} on interface {}: {:?}", |
| address, |
| id, |
| e |
| ); |
| }); |
| |
| // Receive the initial assignment state from Core. The message |
| // must already be in the channel, so don't await. |
| let initial_assignment_state = assignment_state_receiver |
| .next() |
| .now_or_never() |
| .expect("receiver unexpectedly empty") |
| .expect("sender unexpectedly closed"); |
| |
| // Run the `AddressStateProvider` main loop. |
| // Pass in the `assignment_state_receiver` and `stop_receiver` by |
| // ref so that they don't get dropped after the main loop exits |
| // (before the senders are removed from `ctx`). |
| let (needs_removal, removal_reason) = address_state_provider_main_loop( |
| &mut ctx, |
| address, |
| id, |
| req_stream, |
| &mut assignment_state_receiver, |
| initial_assignment_state, |
| &mut stop_receiver, |
| ) |
| .await; |
| |
| ( |
| match needs_removal { |
| AddressNeedsExplicitRemovalFromCore::Yes => state_to_remove, |
| AddressNeedsExplicitRemovalFromCore::No => { |
| StateInCore { address: false, ..state_to_remove } |
| } |
| }, |
| removal_reason, |
| ) |
| } |
| }; |
| |
| // Remove the address. |
| let bindings_ctx = ctx.bindings_ctx(); |
| let core_id = bindings_ctx.devices.get_core_id(id).expect("missing device info for interface"); |
| // Don't drop the worker yet; it's what's driving THIS function. |
| let _worker: futures::future::Shared<fuchsia_async::Task<()>> = |
| match core_id.external_state().with_common_info_mut(|i| i.addresses.remove(&address)) { |
| Some(devices::AddressInfo { |
| address_state_provider: devices::FidlWorkerInfo { worker, cancelation_sender: _ }, |
| assignment_state_sender: _, |
| }) => worker, |
| None => { |
| panic!("`AddressInfo` unexpectedly missing for {:?}", address) |
| } |
| }; |
| |
| let StateInCore { address: remove_address, subnet_route } = state_to_remove_from_core; |
| let device_id = bindings_ctx.devices.get_core_id(id).expect("interface not found"); |
| |
| // The presence of this `route_set` indicates that we added a subnet route |
| // previously due to the `add_subnet_route` AddressParameters option. |
| // Closing `route_set` will correctly remove this route. |
| if let Some((route_set_v4, route_set_v6)) = subnet_route { |
| route_set_v4.close().await; |
| route_set_v6.close().await; |
| } |
| |
| if remove_address { |
| let result = |
| ctx.api().device_ip_any().del_ip_addr(&device_id, address).expect("address must exist"); |
| let _: AddrSubnetEither = result |
| .map_deferred(|d| { |
| d.map_left(|l| l.into_future("device addr", &address).map(Into::into)) |
| .map_right(|r| r.into_future("device addr", &address).map(Into::into)) |
| }) |
| .into_future() |
| .await; |
| } |
| |
| if let Some(removal_reason) = removal_reason { |
| send_address_removal_event(address.get(), id, control_handle, removal_reason.into()); |
| } |
| } |
| |
| enum AddressNeedsExplicitRemovalFromCore { |
| /// The caller is expected to delete the address from Core. |
| Yes, |
| /// The caller need not delete the address from Core (e.g. interface removal, |
| /// which implicitly removes all addresses.) |
| No, |
| } |
| |
| async fn address_state_provider_main_loop( |
| ctx: &mut Ctx, |
| address: SpecifiedAddr<IpAddr>, |
| id: BindingId, |
| req_stream: fnet_interfaces_admin::AddressStateProviderRequestStream, |
| assignment_state_receiver: &mut futures::channel::mpsc::UnboundedReceiver< |
| fnet_interfaces::AddressAssignmentState, |
| >, |
| initial_assignment_state: fnet_interfaces::AddressAssignmentState, |
| stop_receiver: &mut futures::channel::oneshot::Receiver<AddressStateProviderCancellationReason>, |
| ) -> (AddressNeedsExplicitRemovalFromCore, Option<AddressStateProviderCancellationReason>) { |
| // When detached, the lifetime of `req_stream` should not be tied to the |
| // lifetime of `address`. |
| let mut detached = false; |
| let mut watch_state = AddressAssignmentWatcherState { |
| fsm: AddressAssignmentWatcherStateMachine::UnreportedUpdate(initial_assignment_state), |
| last_response: None, |
| }; |
| enum AddressStateProviderEvent { |
| Request(Result<Option<fnet_interfaces_admin::AddressStateProviderRequest>, fidl::Error>), |
| AssignmentStateChange(fnet_interfaces::AddressAssignmentState), |
| Canceled(AddressStateProviderCancellationReason), |
| } |
| let mut req_stream = pin!(req_stream); |
| let mut stop_receiver = pin!(stop_receiver); |
| let mut assignment_state_receiver = pin!(assignment_state_receiver); |
| let cancelation_reason = loop { |
| let next_event = futures::select! { |
| req = req_stream.try_next() => AddressStateProviderEvent::Request(req), |
| state = assignment_state_receiver.next() => { |
| AddressStateProviderEvent::AssignmentStateChange( |
| // It's safe to `expect` here, because the |
| // AddressStateProvider worker is responsible for |
| // cleaning up the sender, and only does so after this |
| // function exits. |
| state.expect("sender unexpectedly closed")) |
| }, |
| reason = stop_receiver => AddressStateProviderEvent::Canceled(reason.expect("failed to receive stop")), |
| }; |
| |
| match next_event { |
| AddressStateProviderEvent::Request(req) => match req { |
| // The client hung up, stop serving. |
| Ok(None) => { |
| // If detached, wait to be canceled before exiting. |
| if detached { |
| // N.B. The `Canceled` arm of this match exits the loop, |
| // meaning we can't already be canceled here. |
| debug_assert!(!stop_receiver.is_terminated()); |
| break Some(stop_receiver.await.expect("failed to receive stop")); |
| } |
| break None; |
| } |
| Ok(Some(request)) => match dispatch_address_state_provider_request( |
| ctx, |
| request, |
| address, |
| id, |
| &mut detached, |
| &mut watch_state, |
| ) { |
| Ok(Some(UserRemove)) => { |
| break Some(AddressStateProviderCancellationReason::UserRemoved) |
| } |
| Ok(None) => {} |
| Err(err @ AddressStateProviderError::PreviousPendingWatchRequest) => { |
| tracing::warn!( |
| "failed to handle request for address {:?} on interface {}: {}", |
| address, |
| id, |
| err |
| ); |
| break None; |
| } |
| Err(err @ AddressStateProviderError::Fidl(_)) => tracing::error!( |
| "failed to handle request for address {:?} on interface {}: {}", |
| address, |
| id, |
| err |
| ), |
| }, |
| Err(e) => { |
| tracing::error!( |
| "error operating {} stream for address {:?} on interface {}: {:?}", |
| fnet_interfaces_admin::AddressStateProviderMarker::DEBUG_NAME, |
| address, |
| id, |
| e |
| ); |
| break None; |
| } |
| }, |
| AddressStateProviderEvent::AssignmentStateChange(state) => { |
| watch_state.on_new_assignment_state(state).unwrap_or_else(|e|{ |
| tracing::error!( |
| "failed to respond to pending watch request for address {:?} on interface {}: {:?}", |
| address, |
| id, |
| e |
| ) |
| }); |
| } |
| AddressStateProviderEvent::Canceled(reason) => { |
| break Some(reason); |
| } |
| } |
| }; |
| |
| ( |
| match cancelation_reason { |
| Some(AddressStateProviderCancellationReason::DadFailed) |
| | Some(AddressStateProviderCancellationReason::InterfaceRemoved) => { |
| AddressNeedsExplicitRemovalFromCore::No |
| } |
| Some(AddressStateProviderCancellationReason::UserRemoved) | None => { |
| AddressNeedsExplicitRemovalFromCore::Yes |
| } |
| }, |
| cancelation_reason, |
| ) |
| } |
| |
| #[derive(thiserror::Error, Debug)] |
| pub(crate) enum AddressStateProviderError { |
| #[error( |
| "received a `WatchAddressAssignmentState` request while a previous request is pending" |
| )] |
| PreviousPendingWatchRequest, |
| #[error("FIDL error: {0}")] |
| Fidl(fidl::Error), |
| } |
| |
| // State Machine for the `WatchAddressAssignmentState` "Hanging-Get" FIDL API. |
| #[derive(Debug)] |
| enum AddressAssignmentWatcherStateMachine { |
| // Holds the new assignment state waiting to be sent. |
| UnreportedUpdate(fnet_interfaces::AddressAssignmentState), |
| // Holds the hanging responder waiting for a new assignment state to send. |
| HangingRequest(fnet_interfaces_admin::AddressStateProviderWatchAddressAssignmentStateResponder), |
| Idle, |
| } |
| struct AddressAssignmentWatcherState { |
| // The state of the `WatchAddressAssignmentState` "Hanging-Get" FIDL API. |
| fsm: AddressAssignmentWatcherStateMachine, |
| // The last response to a `WatchAddressAssignmentState` FIDL request. |
| // `None` until the first request, after which it will always be `Some`. |
| last_response: Option<fnet_interfaces::AddressAssignmentState>, |
| } |
| |
| impl AddressAssignmentWatcherState { |
| // Handle a change in `AddressAssignmentState` as published by Core. |
| fn on_new_assignment_state( |
| &mut self, |
| new_state: fnet_interfaces::AddressAssignmentState, |
| ) -> Result<(), fidl::Error> { |
| use AddressAssignmentWatcherStateMachine::*; |
| let Self { fsm, last_response } = self; |
| // Use `Idle` as a placeholder value to take ownership of `fsm`. |
| let old_fsm = std::mem::replace(fsm, Idle); |
| let (new_fsm, result) = match old_fsm { |
| UnreportedUpdate(old_state) => { |
| if old_state == new_state { |
| tracing::warn!("received duplicate AddressAssignmentState event from Core."); |
| } |
| if self.last_response == Some(new_state) { |
| // Return to `Idle` because we've coalesced |
| // multiple updates and no-longer have new state to send. |
| (Idle, Ok(())) |
| } else { |
| (UnreportedUpdate(new_state), Ok(())) |
| } |
| } |
| HangingRequest(responder) => { |
| *last_response = Some(new_state); |
| (Idle, responder.send(new_state)) |
| } |
| Idle => (UnreportedUpdate(new_state), Ok(())), |
| }; |
| assert_matches!(std::mem::replace(fsm, new_fsm), Idle); |
| result |
| } |
| |
| // Handle a new `WatchAddressAssignmentState` FIDL request. |
| fn on_new_watch_req( |
| &mut self, |
| responder: fnet_interfaces_admin::AddressStateProviderWatchAddressAssignmentStateResponder, |
| ) -> Result<(), AddressStateProviderError> { |
| use AddressAssignmentWatcherStateMachine::*; |
| let Self { fsm, last_response } = self; |
| // Use `Idle` as a placeholder value to take ownership of `fsm`. |
| let old_fsm = std::mem::replace(fsm, Idle); |
| let (new_fsm, result) = match old_fsm { |
| UnreportedUpdate(state) => { |
| *last_response = Some(state); |
| (Idle, responder.send(state).map_err(AddressStateProviderError::Fidl)) |
| } |
| HangingRequest(_existing_responder) => ( |
| HangingRequest(responder), |
| Err(AddressStateProviderError::PreviousPendingWatchRequest), |
| ), |
| Idle => (HangingRequest(responder), Ok(())), |
| }; |
| assert_matches!(std::mem::replace(fsm, new_fsm), Idle); |
| result |
| } |
| } |
| |
| struct UserRemove; |
| |
| /// Serves a `fuchsia.net.interfaces.admin/AddressStateProvider` request. |
| /// |
| /// If `Ok(Some(UserRemove))` is returned, the client has explicitly |
| /// requested removal of the address, but the address has not been removed yet. |
| fn dispatch_address_state_provider_request( |
| ctx: &mut Ctx, |
| req: fnet_interfaces_admin::AddressStateProviderRequest, |
| address: SpecifiedAddr<IpAddr>, |
| id: BindingId, |
| detached: &mut bool, |
| watch_state: &mut AddressAssignmentWatcherState, |
| ) -> Result<Option<UserRemove>, AddressStateProviderError> { |
| tracing::debug!("serving {:?}", req); |
| match req { |
| fnet_interfaces_admin::AddressStateProviderRequest::UpdateAddressProperties { |
| address_properties: |
| fnet_interfaces_admin::AddressProperties { |
| valid_lifetime_end: valid_lifetime_end_nanos, |
| preferred_lifetime_info, |
| .. |
| }, |
| responder, |
| } => { |
| if preferred_lifetime_info.is_some() { |
| tracing::warn!("Updating preferred lifetime info is not yet supported (https://fxbug.dev/42056194)"); |
| } |
| let device_id = |
| ctx.bindings_ctx().devices.get_core_id(id).expect("interface not found"); |
| let valid_lifetime_end = valid_lifetime_end_nanos |
| .map(|nanos| Lifetime::Finite(StackTime(fasync::Time::from_nanos(nanos)))) |
| .unwrap_or(Lifetime::Infinite); |
| let result = match address.into() { |
| IpAddr::V4(address) => ctx.api().device_ip::<Ipv4>().set_addr_properties( |
| &device_id, |
| address, |
| valid_lifetime_end, |
| ), |
| IpAddr::V6(address) => ctx.api().device_ip::<Ipv6>().set_addr_properties( |
| &device_id, |
| address, |
| valid_lifetime_end, |
| ), |
| }; |
| match result { |
| Ok(()) => { |
| responder.send().map_err(AddressStateProviderError::Fidl)?; |
| Ok(None) |
| } |
| Err(SetIpAddressPropertiesError::NotFound( |
| netstack3_core::error::NotFoundError, |
| )) => { |
| panic!("address not found") |
| } |
| Err(SetIpAddressPropertiesError::NotManual) => { |
| panic!("address not manual") |
| } |
| } |
| } |
| fnet_interfaces_admin::AddressStateProviderRequest::WatchAddressAssignmentState { |
| responder, |
| } => { |
| watch_state.on_new_watch_req(responder)?; |
| Ok(None) |
| } |
| fnet_interfaces_admin::AddressStateProviderRequest::Detach { control_handle: _ } => { |
| *detached = true; |
| Ok(None) |
| } |
| fnet_interfaces_admin::AddressStateProviderRequest::Remove { control_handle: _ } => { |
| Ok(Some(UserRemove)) |
| } |
| } |
| } |
| |
| fn send_address_removal_event( |
| addr: IpAddr, |
| id: BindingId, |
| control_handle: fnet_interfaces_admin::AddressStateProviderControlHandle, |
| reason: fnet_interfaces_admin::AddressRemovalReason, |
| ) { |
| control_handle.send_on_address_removed(reason).unwrap_or_else(|e| { |
| tracing::error!( |
| "failed to send address removal reason for addr {:?} on interface {}: {:?}", |
| addr, |
| id, |
| e |
| ); |
| }) |
| } |
| |
| mod enabled { |
| use super::*; |
| use crate::bindings::{DeviceSpecificInfo, DynamicEthernetInfo, DynamicNetdeviceInfo}; |
| use futures::lock::Mutex as AsyncMutex; |
| |
| /// A helper that provides interface enabling and disabling under an |
| /// interface-scoped lock. |
| /// |
| /// All interface enabling and disabling must go through this controller to |
| /// guarantee that futures are not racing to act on the same status that is |
| /// protected by locks owned by device structures in core. |
| pub(super) struct InterfaceEnabledController { |
| id: BindingId, |
| ctx: AsyncMutex<Ctx>, |
| } |
| |
| impl InterfaceEnabledController { |
| pub(super) fn new(ctx: Ctx, id: BindingId) -> Self { |
| Self { id, ctx: AsyncMutex::new(ctx) } |
| } |
| |
| /// Sets this controller's interface to `admin_enabled = enabled`. |
| /// |
| /// Returns `true` if the value of `admin_enabled` changed in response |
| /// to this call. |
| pub(super) async fn set_admin_enabled(&self, enabled: bool) -> bool { |
| let Self { id, ctx } = self; |
| let mut ctx = ctx.lock().await; |
| enum Info<A, B, C> { |
| Loopback(A), |
| Ethernet(B), |
| PureIp(C), |
| } |
| |
| let core_id = ctx.bindings_ctx().devices.get_core_id(*id).expect("device not present"); |
| let port_handler = { |
| let (mut info, port_handler) = match core_id.external_state() { |
| devices::DeviceSpecificInfo::Loopback(devices::LoopbackInfo { |
| static_common_info: _, |
| dynamic_common_info, |
| rx_notifier: _, |
| }) => (Info::Loopback(dynamic_common_info.write()), None), |
| devices::DeviceSpecificInfo::Ethernet(devices::EthernetInfo { |
| netdevice, |
| common_info: _, |
| dynamic_info, |
| mac: _, |
| _mac_proxy: _, |
| }) => (Info::Ethernet(dynamic_info.write()), Some(&netdevice.handler)), |
| devices::DeviceSpecificInfo::PureIp(devices::PureIpDeviceInfo { |
| netdevice, |
| common_info: _, |
| dynamic_info, |
| }) => (Info::PureIp(dynamic_info.write()), Some(&netdevice.handler)), |
| }; |
| let common_info = match info { |
| Info::Loopback(ref mut common_info) => common_info.deref_mut(), |
| Info::Ethernet(ref mut dynamic) => &mut dynamic.netdevice.common_info, |
| Info::PureIp(ref mut dynamic) => &mut dynamic.common_info, |
| }; |
| |
| // Already set to expected value. |
| if common_info.admin_enabled == enabled { |
| return false; |
| } |
| common_info.admin_enabled = enabled; |
| |
| port_handler |
| }; |
| |
| if let Some(handler) = port_handler { |
| let r = if enabled { handler.attach().await } else { handler.detach().await }; |
| match r { |
| Ok(()) => (), |
| Err(e) => { |
| tracing::warn!("failed to set port {:?} to {}: {:?}", handler, enabled, e); |
| // NB: There might be other errors here to consider in the |
| // future, we start with a very strict set of known errors to |
| // allow and panic on anything that is unexpected. |
| match e { |
| // We can race with the port being removed or the device |
| // being destroyed. |
| netdevice_client::Error::Attach(_, zx::Status::NOT_FOUND) |
| | netdevice_client::Error::Detach(_, zx::Status::NOT_FOUND) => (), |
| netdevice_client::Error::Fidl(e) if e.is_closed() => (), |
| e => panic!( |
| "unexpected error setting enabled={} on port {:?}: {:?}", |
| enabled, handler, e |
| ), |
| } |
| } |
| } |
| } |
| Self::update_enabled_state(ctx.deref_mut(), *id); |
| true |
| } |
| |
| /// Sets this controller's interface to `phy_up = online`. |
| pub(super) async fn set_phy_up(&self, online: bool) { |
| let Self { id, ctx } = self; |
| let mut ctx = ctx.lock().await; |
| let core_id = ctx.bindings_ctx().devices.get_core_id(*id).expect("device not present"); |
| let original_state = match core_id.external_state() { |
| devices::DeviceSpecificInfo::Ethernet(i) => { |
| i.with_dynamic_info_mut(|i| std::mem::replace(&mut i.netdevice.phy_up, online)) |
| } |
| i @ devices::DeviceSpecificInfo::Loopback(_) => { |
| unreachable!("unexpected device info {:?} for interface {}", i, *id) |
| } |
| devices::DeviceSpecificInfo::PureIp(i) => { |
| i.with_dynamic_info_mut(|i| std::mem::replace(&mut i.phy_up, online)) |
| } |
| }; |
| |
| match (original_state, online) { |
| (true, true) | (false, false) => { |
| tracing::debug!( |
| "observed no-op port status update on interface {:?}. online = {}", |
| core_id, |
| online |
| ); |
| } |
| (true, false) => { |
| tracing::warn!( |
| "observed port status change to offline on interface {:?}", |
| core_id |
| ) |
| } |
| (false, true) => { |
| tracing::info!( |
| "observed port status change to online on interface {:?}", |
| core_id |
| ) |
| } |
| } |
| |
| // Enable or disable interface with context depending on new |
| // online status. The helper functions take care of checking if |
| // admin enable is the expected value. |
| Self::update_enabled_state(&mut ctx, *id); |
| } |
| |
| /// Commits interface enabled state to core. |
| /// |
| /// # Panics |
| /// |
| /// Panics if `id` is not a valid installed interface identifier. |
| /// |
| /// Panics if `should_enable` is `false` but the device state reflects |
| /// that it should be enabled. |
| fn update_enabled_state(ctx: &mut Ctx, id: BindingId) { |
| let core_id = ctx |
| .bindings_ctx() |
| .devices |
| .get_core_id(id) |
| .expect("tried to enable/disable nonexisting device"); |
| |
| let dev_enabled = match core_id.external_state() { |
| DeviceSpecificInfo::Ethernet(i) => i.with_dynamic_info( |
| |DynamicEthernetInfo { |
| netdevice: DynamicNetdeviceInfo { phy_up, common_info }, |
| neighbor_event_sink: _, |
| }| *phy_up && common_info.admin_enabled, |
| ), |
| DeviceSpecificInfo::Loopback(i) => { |
| i.with_dynamic_info(|common_info| common_info.admin_enabled) |
| } |
| DeviceSpecificInfo::PureIp(i) => { |
| i.with_dynamic_info(|DynamicNetdeviceInfo { phy_up, common_info }| { |
| *phy_up && common_info.admin_enabled |
| }) |
| } |
| }; |
| |
| let ip_config = |
| IpDeviceConfigurationUpdate { ip_enabled: Some(dev_enabled), ..Default::default() }; |
| |
| // The update functions from core are already capable of identifying |
| // deltas and return the previous values for us. Log the deltas for |
| // info. |
| let was_v4_previously_enabled = ctx |
| .api() |
| .device_ip::<Ipv4>() |
| .update_configuration( |
| &core_id, |
| Ipv4DeviceConfigurationUpdate { ip_config, ..Default::default() }, |
| ) |
| .expect("changing ip_enabled should never fail") |
| .ip_config |
| .ip_enabled |
| .expect("ip enabled must be informed"); |
| |
| let was_v6_previously_enabled = ctx |
| .api() |
| .device_ip::<Ipv6>() |
| .update_configuration( |
| &core_id, |
| Ipv6DeviceConfigurationUpdate { ip_config, ..Default::default() }, |
| ) |
| .expect("changing ip_enabled should never fail") |
| .ip_config |
| .ip_enabled |
| .expect("ip enabled must be informed"); |
| |
| tracing::info!( |
| "updated core IPv4 and IPv6 enabled state to {dev_enabled} on {core_id:?}, \ |
| prev v4={was_v4_previously_enabled},v6={was_v6_previously_enabled}" |
| ); |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use net_declare::fidl_subnet; |
| |
| use crate::bindings::{ |
| integration_tests::{StackSetupBuilder, TestSetup, TestSetupBuilder}, |
| interfaces_watcher::{InterfaceEvent, InterfaceUpdate}, |
| }; |
| |
| // Verifies that when an an interface is removed, its addresses are |
| // implicitly removed, rather then explicitly removed one-by-one. Explicit |
| // removal would be redundant and is unnecessary. |
| #[fixture::teardown(TestSetup::shutdown)] |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn implicit_address_removal_on_interface_removal() { |
| const ENDPOINT: &'static str = "endpoint"; |
| |
| let (spy_sink, event_receiver) = futures::channel::mpsc::unbounded(); |
| let mut t = TestSetupBuilder::new() |
| .add_named_endpoint(ENDPOINT) |
| .add_stack(StackSetupBuilder::new().spy_interface_events(spy_sink)) |
| .build() |
| .await; |
| |
| // We just want to add and remove an interface from the stack. Since loopback interfaces |
| // are never removed (except for during stack teardown), it's better to use a netdevice |
| // interface instead. |
| let (endpoint, port_id) = t.get_endpoint(ENDPOINT).await; |
| |
| let test_stack = t.get(0); |
| |
| let (device_control_proxy, device_control_request_stream) = |
| fidl::endpoints::create_proxy_and_stream::<fnet_interfaces_admin::DeviceControlMarker>( |
| ) |
| .expect("create address proxy and stream"); |
| |
| let device_control_task = fuchsia_async::Task::spawn(run_device_control( |
| test_stack.netstack(), |
| endpoint, |
| device_control_request_stream, |
| )); |
| |
| let (interface_control, control_server_end) = |
| fidl::endpoints::create_proxy::<fnet_interfaces_admin::ControlMarker>() |
| .expect("create interface control proxy"); |
| |
| device_control_proxy |
| .create_interface( |
| &port_id, |
| control_server_end, |
| &fnet_interfaces_admin::Options::default(), |
| ) |
| .expect("create interface"); |
| |
| let binding_id = interface_control.get_id().await.expect("get ID"); |
| |
| // Filter for only events on this interface. |
| let event_receiver = event_receiver |
| .filter(|event| match event { |
| InterfaceEvent::Added { id, properties: _ } |
| | InterfaceEvent::Changed { id, event: _ } |
| | InterfaceEvent::Removed(id) => futures::future::ready(id.get() == binding_id), |
| }) |
| .fuse(); |
| let mut event_receiver = pin!(event_receiver); |
| |
| // We should see the interface get added. |
| let event = event_receiver.next().await; |
| assert_matches!(event, Some(InterfaceEvent::Added { |
| id, |
| properties: _, |
| }) if id.get() == binding_id); |
| |
| // Add an address. |
| let (asp_client_end, asp_server_end) = |
| fidl::endpoints::create_proxy::<fnet_interfaces_admin::AddressStateProviderMarker>() |
| .expect("create ASP proxy"); |
| let addr = fidl_subnet!("1.1.1.1/32"); |
| interface_control |
| .add_address( |
| &addr, |
| &fnet_interfaces_admin::AddressParameters::default(), |
| asp_server_end, |
| ) |
| .expect("failed to add address"); |
| |
| // Observe the `AddressAdded` event. |
| let event = event_receiver.next().await; |
| assert_matches!(event, Some(InterfaceEvent::Changed { |
| id, |
| event: InterfaceUpdate::AddressAdded { |
| addr: address, |
| assignment_state: _, |
| valid_until: _, |
| } |
| }) if (id.get() == binding_id && address.into_fidl() == addr )); |
| let mut asp_event_stream = asp_client_end.take_event_stream(); |
| let event = asp_event_stream |
| .try_next() |
| .await |
| .expect("read AddressStateProvider event") |
| .expect("AddressStateProvider event stream unexpectedly empty"); |
| assert_matches!(event, fnet_interfaces_admin::AddressStateProviderEvent::OnAddressAdded {}); |
| |
| // Drop the device control handle and expect the device task to exit. |
| // This will cause the interface to be removed as well. |
| drop(device_control_proxy); |
| device_control_task.await.expect("should not get error"); |
| |
| // Expect that the event receiver observes the interface being removed |
| // without ever seeing an `AddressRemoved` event, which would indicate |
| // the address was explicitly removed. |
| assert_matches!(event_receiver.next().await, |
| Some(InterfaceEvent::Removed(id)) if id.get() == binding_id); |
| |
| // Verify the ASP closed for the correct reason. |
| let event = asp_event_stream |
| .try_next() |
| .await |
| .expect("read AddressStateProvider event") |
| .expect("AddressStateProvider event stream unexpectedly empty"); |
| assert_matches!( |
| event, |
| fnet_interfaces_admin::AddressStateProviderEvent::OnAddressRemoved { error } => { |
| assert_eq!(error, fnet_interfaces_admin::AddressRemovalReason::InterfaceRemoved) |
| } |
| ); |
| |
| t |
| } |
| } |