Squashed 'components/mycelium/' content from commit afb32e0

git-subtree-dir: components/mycelium
git-subtree-split: afb32e0cdb2d4cdd17f22a5693278068d061f08c
This commit is contained in:
2025-08-16 21:12:34 +02:00
commit 10025f9fa5
132 changed files with 50951 additions and 0 deletions

347
mycelium/src/tun/darwin.rs Normal file
View File

@@ -0,0 +1,347 @@
//! macos specific tun interface setup.
use std::{
ffi::CString,
io::{self, IoSlice},
net::IpAddr,
os::fd::AsRawFd,
str::FromStr,
};
use futures::{Sink, Stream};
use nix::sys::socket::SockaddrIn6;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
select,
sync::mpsc,
};
use tracing::{debug, error, info, warn};
use crate::crypto::PacketBuffer;
use crate::subnet::Subnet;
use crate::tun::TunConfig;
// TODO
const LINK_MTU: i32 = 1400;
/// The 4 byte packet header written before a packet is sent on the TUN
// TODO: figure out structure and values, but for now this seems to work.
const HEADER: [u8; 4] = [0, 0, 0, 30];
const IN6_IFF_NODAD: u32 = 0x0020; // netinet6/in6_var.h
const IN6_IFF_SECURED: u32 = 0x0400; // netinet6/in6_var.h
const ND6_INFINITE_LIFETIME: u32 = 0xFFFFFFFF; // netinet6/nd6.h
/// Wrapper for an OS-specific interface name
// Allways hold the max size of an interface. This includes the 0 byte for termination.
// repr transparent so this can be used with libc calls.
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct IfaceName([libc::c_char; libc::IFNAMSIZ as _]);
/// Wrapped interface handle.
#[derive(Clone, Copy)]
struct Iface {
/// Name of the interface
iface_name: IfaceName,
}
/// Struct to add IPv6 route to interface
#[repr(C)]
pub struct IfaliasReq {
ifname: IfaceName,
addr: SockaddrIn6,
dst_addr: SockaddrIn6,
mask: SockaddrIn6,
flags: u32,
lifetime: AddressLifetime,
}
#[repr(C)]
pub struct AddressLifetime {
/// Not used for userspace -> kernel space
expire: libc::time_t,
/// Not used for userspace -> kernel space
preferred: libc::time_t,
vltime: u32,
pltime: u32,
}
/// Create a new tun interface and set required routes
///
/// # Panics
///
/// This function will panic if called outside of the context of a tokio runtime.
pub async fn new(
tun_config: TunConfig,
) -> Result<
(
impl Stream<Item = io::Result<PacketBuffer>>,
impl Sink<PacketBuffer, Error = impl std::error::Error> + Clone,
),
Box<dyn std::error::Error>,
> {
let tun_name = find_available_utun_name(&tun_config.name)?;
let mut tun = match create_tun_interface(&tun_name) {
Ok(tun) => tun,
Err(e) => {
error!(tun_name=%tun_name, err=%e, "Could not create TUN device. Make sure the name is not yet in use, and you have sufficient privileges to create a network device");
return Err(e);
}
};
let iface = Iface::by_name(&tun_name)?;
iface.add_address(tun_config.node_subnet, tun_config.route_subnet)?;
let (tun_sink, mut sink_receiver) = mpsc::channel::<PacketBuffer>(1000);
let (tun_stream, stream_receiver) = mpsc::unbounded_channel();
// Spawn a single task to manage the TUN interface
tokio::spawn(async move {
let mut buf_hold = None;
loop {
let mut buf: PacketBuffer = buf_hold.take().unwrap_or_default();
select! {
data = sink_receiver.recv() => {
match data {
None => return,
Some(data) => {
// We need to append a 4 byte header here
if let Err(e) = tun.write_vectored(&[IoSlice::new(&HEADER), IoSlice::new(&data)]).await {
error!("Failed to send data to tun interface {e}");
}
}
}
// Save the buffer as we didn't use it
buf_hold = Some(buf);
}
read_result = tun.read(buf.buffer_mut()) => {
let rr = read_result.map(|n| {
buf.set_size(n);
// Trim header
buf.buffer_mut().copy_within(4.., 0);
buf.set_size(n-4);
buf
});
if tun_stream.send(rr).is_err() {
error!("Could not forward data to tun stream, receiver is gone");
break;
};
}
}
}
info!("Stop reading from / writing to tun interface");
});
Ok((
tokio_stream::wrappers::UnboundedReceiverStream::new(stream_receiver),
tokio_util::sync::PollSender::new(tun_sink),
))
}
/// Checks if a name is valid for a utun interface
///
/// Rules:
/// - must start with "utun"
/// - followed by only digits
/// - 15 chars total at most
fn validate_utun_name(input: &str) -> bool {
if input.len() > 15 {
return false;
}
if !input.starts_with("utun") {
return false;
}
input
.strip_prefix("utun")
.expect("We just checked that name starts with 'utun' so this is always some")
.parse::<u64>()
.is_ok()
}
/// Validates the user-supplied TUN interface name
///
/// - If the name is valid and not in use, it will be the TUN name
/// - If the name is valid but already in use, an error will be thrown
/// - If the name is not valid, we try to find the first freely available TUN name
fn find_available_utun_name(preferred_name: &str) -> Result<String, io::Error> {
// Get the list of existing utun interfaces.
let interfaces = netdev::get_interfaces();
let utun_interfaces: Vec<_> = interfaces
.iter()
.filter_map(|iface| {
if iface.name.starts_with("utun") {
Some(iface.name.as_str())
} else {
None
}
})
.collect();
// Check if the preferred name is valid and not in use.
if validate_utun_name(preferred_name) && !utun_interfaces.contains(&preferred_name) {
return Ok(preferred_name.to_string());
}
// If the preferred name is invalid or already in use, find the first available utun name.
if !validate_utun_name(preferred_name) {
warn!(tun_name=%preferred_name, "Invalid TUN name. Looking for the first available TUN name");
} else {
warn!(tun_name=%preferred_name, "TUN name already in use. Looking for the next available TUN name.");
}
// Extract and sort the utun numbers.
let mut utun_numbers = utun_interfaces
.iter()
.filter_map(|iface| iface[4..].parse::<usize>().ok())
.collect::<Vec<_>>();
utun_numbers.sort_unstable();
// Find the first available utun index.
let mut first_free_index = 0;
for (i, &num) in utun_numbers.iter().enumerate() {
if num != i {
first_free_index = i;
break;
}
first_free_index = i + 1;
}
// Create new utun name based on the first free index.
let new_utun_name = format!("utun{}", first_free_index);
if validate_utun_name(&new_utun_name) {
info!(tun_name=%new_utun_name, "Automatically assigned TUN name.");
Ok(new_utun_name)
} else {
error!("No available TUN name found");
Err(io::Error::new(
io::ErrorKind::Other,
"No available TUN name",
))
}
}
/// Create a new TUN interface
fn create_tun_interface(name: &str) -> Result<tun::AsyncDevice, Box<dyn std::error::Error>> {
let mut config = tun::Configuration::default();
config
.name(name)
.layer(tun::Layer::L3)
.mtu(LINK_MTU)
.queues(1)
.up();
let tun = tun::create_as_async(&config)?;
Ok(tun)
}
impl IfaceName {
fn as_ptr(&self) -> *const libc::c_char {
self.0.as_ptr()
}
}
impl FromStr for IfaceName {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Equal len is not allowed because we need to add the 0 byte terminator.
if s.len() >= libc::IFNAMSIZ {
return Err("Interface name too long");
}
// TODO: Is this err possible in a &str?
let raw_name = CString::new(s).map_err(|_| "Interface name contains 0 byte")?;
let mut backing = [0; libc::IFNAMSIZ];
let name_bytes = raw_name.to_bytes_with_nul();
backing[..name_bytes.len()].copy_from_slice(name_bytes);
// SAFETY: This doesn't do any weird things with the bits when converting from u8 to i8
let backing = unsafe { std::mem::transmute::<[u8; 16], [i8; 16]>(backing) };
Ok(Self(backing))
}
}
impl Iface {
/// Retrieve the link index of an interface with the given name
fn by_name(name: &str) -> Result<Iface, Box<dyn std::error::Error>> {
let iface_name: IfaceName = name.parse()?;
match unsafe { libc::if_nametoindex(iface_name.as_ptr()) } {
0 => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"interface not found",
))?,
_ => Ok(Iface { iface_name }),
}
}
/// Add an address to an interface.
///
/// # Panics
///
/// Only IPv6 is supported, this function will panic when adding an IPv4 subnet.
fn add_address(
&self,
subnet: Subnet,
route_subnet: Subnet,
) -> Result<(), Box<dyn std::error::Error>> {
let addr = if let IpAddr::V6(addr) = subnet.address() {
addr
} else {
panic!("IPv4 subnets are not supported");
};
let mask_addr = if let IpAddr::V6(mask) = route_subnet.mask() {
mask
} else {
// We already know we are IPv6 here
panic!("IPv4 routes are not supported");
};
let sock_addr = SockaddrIn6::from(std::net::SocketAddrV6::new(addr, 0, 0, 0));
let mask = SockaddrIn6::from(std::net::SocketAddrV6::new(mask_addr, 0, 0, 0));
let req = IfaliasReq {
ifname: self.iface_name,
addr: sock_addr,
// SAFETY: kernel expects this to be fully zeroed
dst_addr: unsafe { std::mem::zeroed() },
mask,
flags: IN6_IFF_NODAD | IN6_IFF_SECURED,
lifetime: AddressLifetime {
expire: 0,
preferred: 0,
vltime: ND6_INFINITE_LIFETIME,
pltime: ND6_INFINITE_LIFETIME,
},
};
let sock = random_socket()?;
match unsafe { siocaifaddr_in6(sock.as_raw_fd(), &req) } {
Err(e) => {
error!("Failed to add ipv6 addresst to interface {e}");
Err(std::io::Error::last_os_error())?
}
Ok(_) => {
debug!("Added {subnet} to tun interfacel");
Ok(())
}
}
}
}
// Create a socket to talk to the kernel.
fn random_socket() -> Result<std::net::UdpSocket, std::io::Error> {
std::net::UdpSocket::bind("[::1]:0")
}
nix::ioctl_write_ptr!(
/// Add an IPv6 subnet to an interface.
siocaifaddr_in6,
b'i',
26,
IfaliasReq
);