use anyhow::{bail, Context, Result};
use std::mem::size_of;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use tokio::io::unix::AsyncFd;
const UTUN_CONTROL_NAME: &[u8] = b"com.apple.net.utun_control";
const UTUN_OPT_IFNAME: libc::c_int = 2;
pub struct KernelTun {
fd: AsyncFd<OwnedFd>,
name: String,
}
impl KernelTun {
pub fn name(&self) -> &str {
&self.name
}
pub fn open(_requested: &str, cidr: &str, mtu: u32) -> Result<KernelTun> {
let raw = unsafe { libc::socket(libc::PF_SYSTEM, libc::SOCK_DGRAM, libc::SYSPROTO_CONTROL) };
if raw < 0 {
let e = std::io::Error::last_os_error();
if matches!(e.raw_os_error(), Some(libc::EPERM) | Some(libc::EACCES)) {
bail!("open utun control socket: permission denied. L3 on macOS needs root; run the daemon with sudo.");
}
bail!("open utun control socket: {e}");
}
let owned = unsafe { OwnedFd::from_raw_fd(raw) };
let mut info: libc::ctl_info = unsafe { std::mem::zeroed() };
for (i, &b) in UTUN_CONTROL_NAME.iter().enumerate() {
info.ctl_name[i] = b as libc::c_char;
}
if unsafe { libc::ioctl(owned.as_raw_fd(), libc::CTLIOCGINFO, &mut info) } < 0 {
bail!("CTLIOCGINFO utun: {}", std::io::Error::last_os_error());
}
let mut addr: libc::sockaddr_ctl = unsafe { std::mem::zeroed() };
addr.sc_len = size_of::<libc::sockaddr_ctl>() as u8;
addr.sc_family = libc::AF_SYSTEM as u8;
addr.ss_sysaddr = libc::AF_SYS_CONTROL as u16;
addr.sc_id = info.ctl_id;
addr.sc_unit = 0;
let rc = unsafe {
libc::connect(
owned.as_raw_fd(),
&addr as *const libc::sockaddr_ctl as *const libc::sockaddr,
size_of::<libc::sockaddr_ctl>() as libc::socklen_t,
)
};
if rc < 0 {
let e = std::io::Error::last_os_error();
if matches!(e.raw_os_error(), Some(libc::EPERM) | Some(libc::EACCES)) {
bail!("create utun: permission denied. L3 on macOS needs root; run the daemon with sudo.");
}
bail!("create utun: {e}");
}
let mut name_buf = [0u8; 32];
let mut name_len = name_buf.len() as libc::socklen_t;
let rc = unsafe {
libc::getsockopt(
owned.as_raw_fd(),
libc::SYSPROTO_CONTROL,
UTUN_OPT_IFNAME,
name_buf.as_mut_ptr() as *mut libc::c_void,
&mut name_len,
)
};
if rc < 0 {
bail!("read utun name: {}", std::io::Error::last_os_error());
}
let end = name_buf.iter().position(|&b| b == 0).unwrap_or(name_buf.len());
let name = String::from_utf8_lossy(&name_buf[..end]).into_owned();
unsafe {
let flags = libc::fcntl(owned.as_raw_fd(), libc::F_GETFL, 0);
libc::fcntl(owned.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let (addr_str, prefixlen) = split_cidr(cidr);
let fam = if addr_str.contains(':') { "inet6" } else { "inet" };
ifconfig(&[&name, fam, &addr_str, "prefixlen", &prefixlen, "up"])
.with_context(|| format!("ifconfig {name} {fam} {addr_str}"))?;
let _ = ifconfig(&[&name, "mtu", &mtu.to_string()]);
let fd = AsyncFd::new(owned).context("register utun fd with the reactor")?;
Ok(KernelTun { fd, name })
}
pub async fn recv(&self, buf: &mut [u8]) -> Result<usize> {
loop {
let mut guard = self.fd.readable().await?;
match guard.try_io(|inner| {
let mut af = [0u8; 4];
let iov = [
libc::iovec { iov_base: af.as_mut_ptr() as *mut libc::c_void, iov_len: 4 },
libc::iovec { iov_base: buf.as_mut_ptr() as *mut libc::c_void, iov_len: buf.len() },
];
let n = unsafe { libc::readv(inner.as_raw_fd(), iov.as_ptr(), 2) };
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok((n as usize).saturating_sub(4))
}
}) {
Ok(res) => return res.map_err(Into::into),
Err(_would_block) => continue,
}
}
}
pub async fn send(&self, packet: &[u8]) -> Result<usize> {
let af: u32 = match packet.first().map(|b| b >> 4) {
Some(6) => libc::AF_INET6 as u32,
_ => libc::AF_INET as u32,
};
let hdr = af.to_be_bytes();
loop {
let mut guard = self.fd.writable().await?;
match guard.try_io(|inner| {
let iov = [
libc::iovec { iov_base: hdr.as_ptr() as *mut libc::c_void, iov_len: 4 },
libc::iovec { iov_base: packet.as_ptr() as *mut libc::c_void, iov_len: packet.len() },
];
let n = unsafe { libc::writev(inner.as_raw_fd(), iov.as_ptr(), 2) };
if n < 0 {
Err(std::io::Error::last_os_error())
} else {
Ok((n as usize).saturating_sub(4))
}
}) {
Ok(res) => return res.map_err(Into::into),
Err(_would_block) => continue,
}
}
}
}
#[async_trait::async_trait]
impl crate::tun::TunDevice for KernelTun {
fn name(&self) -> &str {
KernelTun::name(self)
}
async fn recv(&self, buf: &mut [u8]) -> Result<usize> {
KernelTun::recv(self, buf).await
}
async fn send(&self, packet: &[u8]) -> Result<usize> {
KernelTun::send(self, packet).await
}
}
fn split_cidr(cidr: &str) -> (String, String) {
match cidr.split_once('/') {
Some((a, p)) => (a.to_string(), p.to_string()),
None => (cidr.to_string(), if cidr.contains(':') { "128".into() } else { "32".into() }),
}
}
pub fn add_route(cidr: &str, dev: &str) -> Result<()> {
let fam = if cidr.contains(':') { "-inet6" } else { "-inet" };
let out = std::process::Command::new("route")
.args(["-q", "-n", "add", fam, cidr, "-interface", dev])
.output()
.context("exec route")?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
if !err.contains("File exists") {
bail!("route add {cidr} -interface {dev}: {}", err.trim());
}
}
Ok(())
}
pub fn add_addr(cidr: &str, dev: &str) -> Result<()> {
let (addr, prefixlen) = split_cidr(cidr);
let fam = if addr.contains(':') { "inet6" } else { "inet" };
let out = std::process::Command::new("ifconfig")
.args([dev, fam, &format!("{addr}/{prefixlen}"), "alias"])
.output()
.context("exec ifconfig")?;
if !out.status.success() {
let err = String::from_utf8_lossy(&out.stderr);
if !err.contains("File exists") && !err.to_lowercase().contains("already") {
bail!("ifconfig {dev} {fam} {addr} alias: {}", err.trim());
}
}
Ok(())
}
pub fn ensure_net_admin_for_l3() -> bool {
if unsafe { libc::geteuid() } == 0 {
return true;
}
eprintln!(" L3 on macOS needs root to create a utun device; run the daemon with sudo.");
false
}
pub fn ensure_hosts_writable() {}
fn ifconfig(args: &[&str]) -> Result<()> {
let out = std::process::Command::new("ifconfig")
.args(args)
.output()
.context("exec ifconfig")?;
if !out.status.success() {
bail!("ifconfig {}: {}", args.join(" "), String::from_utf8_lossy(&out.stderr).trim());
}
Ok(())
}