git-subtree-dir: components/mycelium git-subtree-split: afb32e0cdb2d4cdd17f22a5693278068d061f08c
348 lines
11 KiB
Rust
348 lines
11 KiB
Rust
//! 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
|
|
);
|