blob: 514e7db425d49f133e4db9b6f99e4b7cf581a875 [file] [log] [blame]
// 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 fidl::endpoints::ProtocolMarker as _;
use fidl_fuchsia_hardware_network as fhardware_network;
use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
use fuchsia_async as fasync;
use fuchsia_zircon as zx;
use futures::{FutureExt as _, StreamExt as _, TryFutureExt as _, TryStreamExt as _};
use crate::bindings::{
devices, netdevice_worker, BindingId, InterfaceControl as _, Netstack, NetstackContext,
};
pub(crate) fn serve(
ns: Netstack,
req: fnet_interfaces_admin::InstallerRequestStream,
) -> impl futures::Stream<Item = Result<fasync::Task<()>, fidl::Error>> {
req.map_ok(
move |fnet_interfaces_admin::InstallerRequest::InstallDevice {
device,
device_control,
control_handle: _,
}| {
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| log::warn!("device control finished with {:?}", e))),
)
},
)
}
#[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(
ns: Netstack,
device: fidl::endpoints::ClientEnd<fhardware_network::DeviceMarker>,
device_control: 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_trigger, stop_fut) = futures::channel::oneshot::channel::<()>();
let stop_fut = stop_fut.map(|r| r.expect("closed all cancellation senders")).shared();
let control_stream = device_control
.take_until(stop_fut.clone())
.map_err(DeviceControlError::Fidl)
.try_filter_map(|req| match req {
fnet_interfaces_admin::DeviceControlRequest::CreateInterface {
port,
control,
options,
control_handle: _,
} => create_interface(port, control, options, &ns, &handler, &stop_fut),
fnet_interfaces_admin::DeviceControlRequest::Detach { control_handle: _ } => {
todo!("https://fxbug.dev/100867 support detach");
}
});
futures::pin_mut!(worker_fut);
futures::pin_mut!(control_stream);
let mut tasks = futures::stream::FuturesUnordered::new();
let res = loop {
let mut tasks_fut = if tasks.is_empty() {
futures::future::pending().left_future()
} else {
tasks.by_ref().next().right_future()
};
let result = futures::select! {
r = control_stream.try_next() => r,
r = worker_fut => match r {
Ok(never) => match never {},
Err(e) => Err(e)
},
ready_task = tasks_fut => {
let () = ready_task.unwrap_or_else(|| ());
continue;
}
};
match result {
Ok(Some(task)) => tasks.push(task),
Ok(None) => break Ok(()),
Err(e) => break Err(e),
}
};
// Send a stop signal to all tasks.
stop_trigger.send(()).expect("receiver should not be gone");
match &res {
// Control stream has finished, don't need to drain it.
Ok(()) | Err(DeviceControlError::Fidl(_)) => (),
Err(DeviceControlError::Worker(_)) => {
// Drain control stream to make sure we have all the tasks. The stop
// trigger will make it stop operating on new requests.
control_stream
.try_for_each(|t| futures::future::ok(tasks.push(t)))
.await
.unwrap_or_else(|e| log::warn!("failed to accumulate remaining tasks: {:?}", e));
}
}
// Run all the tasks to completion. We sent the stop signal, they should all
// complete and perform interface cleanup.
tasks.collect::<()>().await;
res
}
/// Operates a fuchsia.net.interfaces.admin/DeviceControl.CreateInterface
/// request.
///
/// Returns `Ok(Some(fuchsia_async::Task))` if an interface was created
/// successfully. The returned `Task` must be polled to completion and is 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: &Netstack,
handler: &netdevice_worker::DeviceHandler,
stop_fut: &(impl futures::Future<Output = ()>
+ futures::future::FusedFuture
+ Clone
+ Send
+ 'static),
) -> Result<Option<fuchsia_async::Task<()>>, DeviceControlError> {
log::debug!("creating interface from {:?} with {:?}", port, options);
let fnet_interfaces_admin::Options { name, metric: _, .. } = options;
match handler.add_port(&ns, netdevice_worker::InterfaceOptions { name }, port).await {
Ok(binding_id) => Ok(Some(fasync::Task::spawn(run_interface_control(
ns.ctx.clone(),
binding_id,
stop_fut.clone(),
control,
)))),
Err(e) => {
log::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::PeerClosed(_)
| 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::Attach(_, _)
| netdevice_client::Error::Detach(_, _) => None,
},
netdevice_worker::Error::AlreadyInstalled(_) => {
Some(fnet_interfaces_admin::InterfaceRemovedReason::PortAlreadyBound)
}
netdevice_worker::Error::CantConnectToPort(_) => {
Some(fnet_interfaces_admin::InterfaceRemovedReason::PortClosed)
}
netdevice_worker::Error::ConfigurationNotSupported
| netdevice_worker::Error::MacNotUnicast { .. } => {
Some(fnet_interfaces_admin::InterfaceRemovedReason::BadPort)
}
netdevice_worker::Error::SystemResource(_)
| netdevice_worker::Error::InvalidPortInfo(_)
| netdevice_worker::Error::InvalidPortStatus(_) => None,
};
if let Some(removed_reason) = removed_reason {
let (_stream, control) =
control.into_stream_and_control_handle().expect("failed to acquire stream");
control.send_on_interface_removed(removed_reason).unwrap_or_else(|e| {
log::warn!("failed to send removed reason: {:?}", e);
});
}
Ok(None)
}
}
}
async fn run_interface_control<
F: Send + 'static + futures::Future<Output = ()> + futures::future::FusedFuture,
>(
ctx: NetstackContext,
id: BindingId,
cancel: F,
server_end: fidl::endpoints::ServerEnd<fnet_interfaces_admin::ControlMarker>,
) {
let (mut stream, control_handle) =
server_end.into_stream_and_control_handle().expect("failed to create stream");
let stream_fut = async {
while let Some(req) = stream.try_next().await? {
log::debug!("serving {:?}", req);
let () = match req {
fnet_interfaces_admin::ControlRequest::AddAddress {
address: _,
parameters: _,
address_state_provider: _,
control_handle: _,
} => todo!("https://fxbug.dev/100870 support add address"),
fnet_interfaces_admin::ControlRequest::RemoveAddress {
address: _,
responder: _,
} => {
todo!("https://fxbug.dev/100870 support remove address")
}
fnet_interfaces_admin::ControlRequest::GetId { responder } => responder.send(id),
fnet_interfaces_admin::ControlRequest::SetConfiguration {
config: _,
responder: _,
} => {
todo!("https://fxbug.dev/76987 support enable/disable forwarding")
}
fnet_interfaces_admin::ControlRequest::GetConfiguration { responder: _ } => {
todo!("https://fxbug.dev/76987 support enable/disable forwarding")
}
fnet_interfaces_admin::ControlRequest::Enable { responder } => {
responder.send(&mut Ok(set_interface_enabled(&ctx, true, id).await))
}
fnet_interfaces_admin::ControlRequest::Disable { responder } => {
responder.send(&mut Ok(set_interface_enabled(&ctx, false, id).await))
}
fnet_interfaces_admin::ControlRequest::Detach { control_handle: _ } => {
todo!("https://fxbug.dev/100867 support detach");
}
}?;
}
Result::<_, fidl::Error>::Ok(())
}
.fuse();
enum Outcome {
Cancelled,
StreamEnded(Result<(), fidl::Error>),
}
futures::pin_mut!(stream_fut);
futures::pin_mut!(cancel);
let outcome = futures::select! {
o = stream_fut => Outcome::StreamEnded(o),
() = cancel => Outcome::Cancelled,
};
match outcome {
Outcome::Cancelled => {
// Device has been removed from under us, inform the user that's the
// case.
control_handle
.send_on_interface_removed(
fnet_interfaces_admin::InterfaceRemovedReason::PortClosed,
)
.unwrap_or_else(|e| {
if !e.is_closed() {
log::error!("failed to send terminal event: {:?}", e)
}
});
}
Outcome::StreamEnded(Err(e)) => {
log::error!(
"error operating {} stream: {:?}",
fnet_interfaces_admin::ControlMarker::DEBUG_NAME,
e
);
}
Outcome::StreamEnded(Ok(())) => (),
}
// Cleanup and remove the interface.
// TODO(https://fxbug.dev/88797): We're not supposed to cleanup if this is a
// debug channel.
// TODO(https://fxbug.dev/100867): We're not supposed to cleanup if we're
// detached.
let _: devices::DeviceInfo = ctx
.lock()
.await
.dispatcher
.devices
.remove_device(id)
.expect("device lifetime should be tied to channel lifetime");
}
/// Sets interface with `id` to `admin_enabled = enabled`.
///
/// Returns `true` if the value of `admin_enabled` changed in response to
/// this call.
async fn set_interface_enabled(ctx: &NetstackContext, enabled: bool, id: BindingId) -> bool {
let mut ctx = ctx.lock().await;
let (common_info, port_handler) =
match ctx.dispatcher.devices.get_device_mut(id).expect("device not present").info_mut() {
devices::DeviceSpecificInfo::Ethernet(devices::EthernetInfo {
common_info,
// NB: In theory we should also start and stop the ethernet
// device when we enable and disable, we'll skip that because
// it's work and Ethernet is going to be deleted soon.
client: _,
mac: _,
features: _,
phy_up: _,
})
| devices::DeviceSpecificInfo::Loopback(devices::LoopbackInfo { common_info }) => {
(common_info, None)
}
devices::DeviceSpecificInfo::Netdevice(devices::NetdeviceInfo {
common_info,
handler,
mac: _,
phy_up: _,
}) => (common_info, Some(handler)),
};
// Already set to expected value.
if common_info.admin_enabled == enabled {
return false;
}
common_info.admin_enabled = enabled;
if let Some(handler) = port_handler {
let r = if enabled { handler.attach().await } else { handler.detach().await };
match r {
Ok(()) => (),
Err(e) => {
log::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
),
}
}
}
}
if enabled {
ctx.enable_interface(id).expect("failed to enable interface");
} else {
ctx.disable_interface(id).expect("failed to disable interface");
}
true
}