blob: 615dd6eba5446c24a71dc93e20535d8a12e76715 [file] [log] [blame]
// Copyright 2024 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.
//! This file contains code for creating and serving tun/tap devices.
use fidl::endpoints::Proxy as _;
use starnix_core::mm::MemoryAccessorExt;
use starnix_core::security;
use starnix_core::signals::RunState;
use starnix_core::task::{CurrentTask, WaiterRef};
use starnix_core::vfs::socket::IfReqPtr;
use starnix_core::vfs::{FileObject, FileOps, default_ioctl};
use starnix_logging::{log_info, log_warn};
use starnix_sync::{Locked, Mutex, Unlocked};
use starnix_uapi::errors::Errno;
use std::num::NonZeroU64;
use std::sync::Arc;
use {
fidl_fuchsia_hardware_network as fhardware_network, fidl_fuchsia_net as fnet,
fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin,
fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext, fidl_fuchsia_net_tun as fnet_tun,
fuchsia_async as fasync,
};
#[derive(Debug, Clone, Copy)]
enum DevKind {
Tun,
Tap,
}
impl DevKind {
fn rx_types(&self) -> impl IntoIterator<Item = fhardware_network::FrameType> {
match self {
DevKind::Tun => itertools::Either::Left(
[fhardware_network::FrameType::Ipv4, fhardware_network::FrameType::Ipv6]
.into_iter(),
),
DevKind::Tap => {
itertools::Either::Right(std::iter::once(fhardware_network::FrameType::Ethernet))
}
}
}
fn tx_types(&self) -> impl IntoIterator<Item = fhardware_network::FrameTypeSupport> {
self.rx_types().into_iter().map(|frame_type| fhardware_network::FrameTypeSupport {
type_: frame_type,
features: 0,
supported_flags: fhardware_network::TxFlags::empty(),
})
}
}
fn random_mac() -> fnet::MacAddress {
let mut octets = [0u8; 6];
zx::cprng_draw(&mut octets[..]);
// Ensure the least-significant-bit of the first byte of the address is 0,
// indicating that it is a unicast address.
// https://en.wikipedia.org/wiki/MAC_address#Unicast_vs._multicast
octets[0] = octets[0] & !1;
// Ensure the second-least-significant bit of the first byte of the address
// is 1, indicating the address is locally administered (i.e. assigned by
// software and not by a device manufacturer).
// https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local_(U/L_bit)
octets[0] = octets[0] | 0b10;
fnet::MacAddress { octets }
}
#[derive(Debug)]
struct CreateTunRequest {
name: String,
kind: DevKind,
// If true, will report frame metadata on receiving frames.
report_metadata: bool,
}
// Give back `ClientEnd`s so that clients can use synchronous proxies for
// reading/writing operations on the files.
struct CreateTunResponse {
device: fidl::endpoints::ClientEnd<fidl_fuchsia_net_tun::DeviceMarker>,
port: fidl::endpoints::ClientEnd<fidl_fuchsia_net_tun::PortMarker>,
port_info: fhardware_network::PortInfo,
interface_id: NonZeroU64,
}
const ARBITRARY_PORT_ID: u8 = 2;
const ETHERNET_MTU: u32 = 1500;
const MAX_ETHERNET_HEADER_SIZE: u32 = 18;
macro_rules! errno_from_interfaces_admin_error {
($err:expr) => {
match $err {
fnet_interfaces_ext::admin::TerminalError::Terminal(err) => match err {
fnet_interfaces_admin::InterfaceRemovedReason::DuplicateName => {
starnix_uapi::errno!(
EEXIST,
"tried to create tuntap interface with duplicate name"
)
}
fnet_interfaces_admin::InterfaceRemovedReason::PortAlreadyBound => {
starnix_uapi::errno!(EBUSY, "tuntap port already bound to an interface")
}
fnet_interfaces_admin::InterfaceRemovedReason::BadPort => {
starnix_uapi::errno!(ENOENT, "tried to create tuntap interface from bad port")
}
fnet_interfaces_admin::InterfaceRemovedReason::PortClosed => {
starnix_uapi::errno!(
ENOENT,
"tried to create tuntap interface from closed port"
)
}
fnet_interfaces_admin::InterfaceRemovedReason::User => {
starnix_uapi::errno!(
ENOENT,
"tuntap interface was removed out from under starnix"
)
}
fnet_interfaces_admin::InterfaceRemovedReasonUnknown!() => {
starnix_uapi::errno!(ENOENT, "unknown interface removed reason")
}
},
fnet_interfaces_ext::admin::TerminalError::Fidl(e) => {
starnix_uapi::errno!(ENOENT, format!("interfaces admin FIDL error: {e:?}"))
}
}
};
}
struct TunWorker {
tun_control: fidl_fuchsia_net_tun::ControlProxy,
installer: fidl_fuchsia_net_interfaces_admin::InstallerProxy,
}
impl TunWorker {
async fn handle_create_request(
&mut self,
request: CreateTunRequest,
) -> Result<CreateTunResponse, Errno> {
let CreateTunRequest { name, kind, report_metadata } = request;
if report_metadata {
log_warn!(
"TODO(https://fxbug.dev/332317144): frame-metadata reporting for
tuntap interfaces in starnix is not implemented yet"
);
}
let report_metadata = false;
let Self { tun_control, installer } = self;
let (tun_device, server_end) =
fidl::endpoints::create_endpoints::<fnet_tun::DeviceMarker>();
tun_control
.create_device(
&fnet_tun::DeviceConfig {
blocking: Some(false),
base: Some(fnet_tun::BaseDeviceConfig {
report_metadata: Some(report_metadata),
min_tx_buffer_length: None,
min_rx_buffer_length: None,
..Default::default()
}),
..Default::default()
},
server_end,
)
.map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("creating fuchsia.net.tun Device failed: {e:?}")
)
})?;
let (tun_port, server_end) = fidl::endpoints::create_endpoints::<fnet_tun::PortMarker>();
let tun_device = tun_device.into_proxy();
tun_device
.add_port(
&fnet_tun::DevicePortConfig {
base: Some(fnet_tun::BasePortConfig {
id: Some(ARBITRARY_PORT_ID),
// Even though this field is named `mtu`, it's actually
// the maximum frame size of whatever layer the
// interface operates at. So if we want to get to the
// typical 1500 MTU that we usually have above the
// Ethernet layer, for TAP devices we need to add space
// to account for the Ethernet header itself.
mtu: Some(
ETHERNET_MTU
+ match kind {
DevKind::Tun => 0,
DevKind::Tap => MAX_ETHERNET_HEADER_SIZE,
},
),
rx_types: Some(kind.rx_types().into_iter().collect::<Vec<_>>()),
tx_types: Some(kind.tx_types().into_iter().collect::<Vec<_>>()),
port_class: None,
..Default::default()
}),
online: Some(true),
mac: match kind {
DevKind::Tun => None,
DevKind::Tap => Some(random_mac()),
},
..Default::default()
},
server_end,
)
.map_err(|e| {
starnix_uapi::errno!(ENOENT, format!("adding fuchsia.net.tun Port failed, {e:?}"))
})?;
let tun_port = tun_port.into_proxy();
let (hw_port, server_end) =
fidl::endpoints::create_endpoints::<fhardware_network::PortMarker>();
tun_port.get_port(server_end).map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("getting fuchsia.hardware.networkn Port failed: {e:?}")
)
})?;
let hw_port = hw_port.into_proxy();
let port_info = hw_port.get_info().await.map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("getting fuchsia.hardware.network PortInfo failed: {e:?}")
)
})?;
let (hw_device, server_end) =
fidl::endpoints::create_endpoints::<fhardware_network::DeviceMarker>();
tun_device.get_device(server_end).map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("getting fuchsia.hardware.network Device failed: {e:?}")
)
})?;
let (device_control, server_end) =
fidl::endpoints::create_endpoints::<fnet_interfaces_admin::DeviceControlMarker>();
installer.install_device(hw_device, server_end).map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("installing fuchsia.hardware.network Device failed: {e:?}")
)
})?;
let (interface_control, server_end) =
fidl::endpoints::create_endpoints::<fnet_interfaces_admin::ControlMarker>();
let device_control = device_control.into_proxy();
device_control
.create_interface(
&port_info
.id
.ok_or_else(|| starnix_uapi::errno!(ENOENT, "got PortInfo with no ID"))?,
server_end,
fnet_interfaces_admin::Options {
name: Some(name.clone()),
metric: None,
..Default::default()
},
)
.map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("creating fuchsia.net.interfaces.admin Control failed: {e:?}")
)
})?;
let interface_control = interface_control.into_proxy();
let interface_control = fnet_interfaces_ext::admin::Control::new(interface_control);
// Get the NICID that the netstack allocates for this interface. This
// serves as a way to wait for the interface to be successfully
// installed, verifying that there's no duplicate-name clash.
let interface_id = interface_control
.get_id()
.await
.map_err(|err| errno_from_interfaces_admin_error!(err))?;
let interface_id = NonZeroU64::new(interface_id).expect("interface IDs must be nonzero");
let _enabled: bool = interface_control
.enable()
.await
.map_err(|err| errno_from_interfaces_admin_error!(err))?
.map_err(|err: fnet_interfaces_admin::ControlEnableError| {
starnix_uapi::errno!(
ENOENT,
format!("enabling fuchsia.net.interfaces.admin Control failed: {err:?}")
)
})?;
let tun_device =
tun_device.into_client_end().expect("should not have cloned tun_device proxy");
let tun_port = tun_port.into_client_end().expect("should not have cloned tun_port proxy");
// Detach the fnet_interfaces_admin DeviceControl to avoid it being
// uninstalled once we drop the proxy. NB: we don't want to do this
// until we're done doing any async work so that we clean up properly if
// we're interrupted.
device_control.detach().map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("detaching fuchsia.net.interfaces.admin DeviceControl failed: {e:?}")
)
})?;
// Same for the fnet_interfaces_admin Control.
interface_control.detach().map_err(|e| {
starnix_uapi::errno!(
ENOENT,
format!("detaching fuchsia.net.interfaces.admin Control failed: {e:?}")
)
})?;
Ok(CreateTunResponse { device: tun_device, port: tun_port, port_info, interface_id })
}
}
#[derive(Default)]
pub struct DevTun(Mutex<Option<DevTunInner>>);
struct DevTunInner {
_tun_device: fnet_tun::DeviceSynchronousProxy,
_tun_port: fnet_tun::PortSynchronousProxy,
_port_info: fhardware_network::PortInfo,
_interface_id: NonZeroU64,
}
impl FileOps for DevTun {
starnix_core::fileops_impl_nonseekable!();
starnix_core::fileops_impl_noop_sync!();
fn write(
&self,
_locked: &mut Locked<starnix_sync::FileOpsCore>,
_file: &FileObject,
_current_task: &CurrentTask,
_offset: usize,
_data: &mut dyn starnix_core::vfs::InputBuffer,
) -> Result<usize, Errno> {
// TODO(https://fxbug.dev/332317144): Implement writing to a TUN/TAP
// device.
starnix_uapi::error!(EOPNOTSUPP)
}
fn read(
&self,
_locked: &mut Locked<starnix_sync::FileOpsCore>,
_file: &FileObject,
_current_task: &CurrentTask,
_offset: usize,
_data: &mut dyn starnix_core::vfs::OutputBuffer,
) -> Result<usize, Errno> {
// TODO(https://fxbug.dev/332317144): Implement reading from a TUN/TAP
// device.
starnix_uapi::error!(EOPNOTSUPP)
}
fn ioctl(
&self,
locked: &mut Locked<Unlocked>,
file: &FileObject,
current_task: &CurrentTask,
request: u32,
arg: starnix_syscalls::SyscallArg,
) -> Result<starnix_syscalls::SyscallResult, Errno> {
match request {
starnix_uapi::TUNSETIFF => {
let mut inner = self.0.lock();
security::check_tun_dev_create_access(current_task)?;
log_info!("handling TUNSETIFF for /dev/tun");
let user_addr = IfReqPtr::new(current_task, arg);
let in_ifreq = current_task.read_multi_arch_object(user_addr)?;
let name = in_ifreq.name_as_str()?.to_string();
let flags = in_ifreq.ifru_flags() as u32;
let iff_tun = flags & starnix_uapi::IFF_TUN != 0;
let iff_tap = flags & starnix_uapi::IFF_TAP != 0;
let iff_no_pi = flags & starnix_uapi::IFF_NO_PI != 0;
let kind = match (iff_tun, iff_tap) {
(true, false) => DevKind::Tun,
(false, true) => DevKind::Tap,
_ => return starnix_uapi::error!(EINVAL),
};
let request = CreateTunRequest { name, kind, report_metadata: !iff_no_pi };
log_info!("adding /dev/tun interface {request:?}");
let (abort_handle, abort_registration) = futures::stream::AbortHandle::new_pair();
let abort_handle = Arc::new(abort_handle);
let CreateTunResponse { device, port, port_info, interface_id } = current_task
.run_in_state(
RunState::Waiter(WaiterRef::from_abort_handle(&abort_handle)),
|| {
let mut executor = fasync::LocalExecutor::default();
let tun_control = fuchsia_component::client::connect_to_protocol::<
fnet_tun::ControlMarker,
>()
.map_err(|_| starnix_uapi::errno!(ENOENT))?;
let installer = fuchsia_component::client::connect_to_protocol::<
fnet_interfaces_admin::InstallerMarker,
>()
.map_err(|_| starnix_uapi::errno!(ENOENT))?;
let mut worker = TunWorker { tun_control, installer };
executor
.run_singlethreaded(futures::stream::Abortable::new(
worker.handle_create_request(request),
abort_registration,
))
.map_err(|futures::stream::Aborted| starnix_uapi::errno!(EINTR))?
},
)?;
*inner = Some(DevTunInner {
_tun_device: device.into_sync_proxy(),
_tun_port: port.into_sync_proxy(),
_port_info: port_info,
_interface_id: interface_id,
});
Ok(starnix_syscalls::SUCCESS)
}
_ => default_ioctl(file, locked, current_task, request, arg),
}
}
}