use std::io;
use std::net::Ipv4Addr;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use splicetcp::utun::{AF_HEADER_SIZE, utun_af_header};
#[allow(dead_code)]
const MAX_PACKET_SIZE: usize = 65535 + AF_HEADER_SIZE;
const SIOCSIFADDR: libc::c_ulong = 0x8020_690c;
const SIOCSIFDSTADDR: libc::c_ulong = 0x8020_690e;
const SIOCSIFFLAGS: libc::c_ulong = 0x8020_6910;
const SIOCGIFFLAGS: libc::c_ulong = 0xc020_6911;
const SIOCSIFNETMASK: libc::c_ulong = 0x8020_6916;
pub struct DarwinTun {
fd: OwnedFd,
name: String,
}
impl DarwinTun {
pub fn new() -> io::Result<Self> {
let fd = unsafe { libc::socket(libc::PF_SYSTEM, libc::SOCK_DGRAM, libc::SYSPROTO_CONTROL) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
let mut ctl_info: libc::ctl_info = unsafe { std::mem::zeroed() };
let ctl_name = b"com.apple.net.utun_control\0";
unsafe {
std::ptr::copy_nonoverlapping(
ctl_name.as_ptr(),
ctl_info.ctl_name.as_mut_ptr().cast::<u8>(),
ctl_name.len(),
);
}
let ret = unsafe { libc::ioctl(fd.as_raw_fd(), libc::CTLIOCGINFO, &mut ctl_info) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
let addr = libc::sockaddr_ctl {
sc_len: std::mem::size_of::<libc::sockaddr_ctl>() as u8,
sc_family: libc::AF_SYSTEM as u8,
ss_sysaddr: libc::AF_SYS_CONTROL as u16,
sc_id: ctl_info.ctl_id,
sc_unit: 0,
sc_reserved: [0; 5],
};
let ret = unsafe {
libc::connect(
fd.as_raw_fd(),
(&raw const addr).cast::<libc::sockaddr>(),
std::mem::size_of::<libc::sockaddr_ctl>() as libc::socklen_t,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
let name = Self::get_interface_name(fd.as_raw_fd())?;
tracing::info!(interface = %name, "Created utun device");
Ok(Self { fd, name })
}
fn get_interface_name(fd: RawFd) -> io::Result<String> {
const UTUN_OPT_IFNAME: libc::c_int = 2;
let mut name_buf = [0u8; libc::IFNAMSIZ];
let mut name_len: libc::socklen_t = name_buf.len() as libc::socklen_t;
let ret = unsafe {
libc::getsockopt(
fd,
libc::SYSPROTO_CONTROL,
UTUN_OPT_IFNAME,
name_buf.as_mut_ptr().cast(),
&raw mut name_len,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
let name = std::str::from_utf8(&name_buf[..name_len as usize])
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.trim_end_matches('\0')
.to_string();
Ok(name)
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub fn as_raw_fd(&self) -> RawFd {
self.fd.as_raw_fd()
}
pub fn bring_up(&self) -> io::Result<()> {
let ctl_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if ctl_fd < 0 {
return Err(io::Error::last_os_error());
}
let ctl_fd = unsafe { OwnedFd::from_raw_fd(ctl_fd) };
let mut ifr_name = [0u8; libc::IFNAMSIZ];
let name_bytes = self.name.as_bytes();
if name_bytes.len() >= libc::IFNAMSIZ {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"interface name too long",
));
}
ifr_name[..name_bytes.len()].copy_from_slice(name_bytes);
Self::set_if_up(ctl_fd.as_raw_fd(), &ifr_name)?;
tracing::info!(interface = %self.name, "utun interface brought UP");
Ok(())
}
pub fn configure(
&self,
local_ip: Ipv4Addr,
peer_ip: Ipv4Addr,
netmask: Ipv4Addr,
) -> io::Result<()> {
let ctl_fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0) };
if ctl_fd < 0 {
return Err(io::Error::last_os_error());
}
let ctl_fd = unsafe { OwnedFd::from_raw_fd(ctl_fd) };
let mut ifr_name = [0u8; libc::IFNAMSIZ];
let name_bytes = self.name.as_bytes();
if name_bytes.len() >= libc::IFNAMSIZ {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"interface name too long",
));
}
ifr_name[..name_bytes.len()].copy_from_slice(name_bytes);
Self::set_ifaddr(ctl_fd.as_raw_fd(), &ifr_name, SIOCSIFADDR, local_ip)?;
Self::set_ifaddr(ctl_fd.as_raw_fd(), &ifr_name, SIOCSIFDSTADDR, peer_ip)?;
Self::set_ifaddr(ctl_fd.as_raw_fd(), &ifr_name, SIOCSIFNETMASK, netmask)?;
Self::set_if_up(ctl_fd.as_raw_fd(), &ifr_name)?;
tracing::info!(
interface = %self.name,
local = %local_ip,
peer = %peer_ip,
netmask = %netmask,
"Configured utun interface"
);
Ok(())
}
fn set_ifaddr(
ctl_fd: RawFd,
ifr_name: &[u8; libc::IFNAMSIZ],
ioctl_cmd: libc::c_ulong,
addr: Ipv4Addr,
) -> io::Result<()> {
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
unsafe {
std::ptr::copy_nonoverlapping(
ifr_name.as_ptr(),
ifr.ifr_name.as_mut_ptr().cast::<u8>(),
libc::IFNAMSIZ,
);
}
let sin = Self::make_sockaddr_in(addr);
unsafe {
std::ptr::copy_nonoverlapping(
(&raw const sin).cast::<u8>(),
(&raw mut ifr.ifr_ifru).cast::<u8>(),
std::mem::size_of::<libc::sockaddr_in>(),
);
}
let ret = unsafe { libc::ioctl(ctl_fd, ioctl_cmd, &ifr) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn set_if_up(ctl_fd: RawFd, ifr_name: &[u8; libc::IFNAMSIZ]) -> io::Result<()> {
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
unsafe {
std::ptr::copy_nonoverlapping(
ifr_name.as_ptr(),
ifr.ifr_name.as_mut_ptr().cast::<u8>(),
libc::IFNAMSIZ,
);
}
let ret = unsafe { libc::ioctl(ctl_fd, SIOCGIFFLAGS, &mut ifr) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
unsafe {
let flags = ifr.ifr_ifru.ifru_flags;
ifr.ifr_ifru.ifru_flags = flags | (libc::IFF_UP as i16) | (libc::IFF_RUNNING as i16);
}
let ret = unsafe { libc::ioctl(ctl_fd, SIOCSIFFLAGS, &ifr) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn make_sockaddr_in(addr: Ipv4Addr) -> libc::sockaddr_in {
let mut sin: libc::sockaddr_in = unsafe { std::mem::zeroed() };
sin.sin_len = std::mem::size_of::<libc::sockaddr_in>() as u8;
sin.sin_family = libc::AF_INET as u8;
sin.sin_addr.s_addr = u32::from(addr).to_be();
sin
}
pub fn send_packet(&self, packet: &[u8]) -> io::Result<usize> {
if packet.is_empty() {
return Ok(0);
}
let mut af_bytes = utun_af_header(packet)?;
let iov = [
libc::iovec {
iov_base: af_bytes.as_mut_ptr().cast::<libc::c_void>(),
iov_len: AF_HEADER_SIZE,
},
libc::iovec {
iov_base: packet.as_ptr().cast_mut().cast::<libc::c_void>(),
iov_len: packet.len(),
},
];
let n = unsafe { libc::writev(self.fd.as_raw_fd(), iov.as_ptr(), 2) };
if n < 0 {
return Err(io::Error::last_os_error());
}
let written = (n as usize).saturating_sub(AF_HEADER_SIZE);
Ok(written)
}
pub fn recv_packet(&self, buf: &mut [u8]) -> io::Result<usize> {
let mut af_header = [0u8; AF_HEADER_SIZE];
let iov = [
libc::iovec {
iov_base: af_header.as_mut_ptr().cast(),
iov_len: AF_HEADER_SIZE,
},
libc::iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
},
];
let n = unsafe { libc::readv(self.fd.as_raw_fd(), iov.as_ptr(), 2) };
if n < 0 {
return Err(io::Error::last_os_error());
}
let total = n as usize;
if total <= AF_HEADER_SIZE {
return Ok(0);
}
Ok(total - AF_HEADER_SIZE)
}
pub fn set_nonblocking(&self, nonblocking: bool) -> io::Result<()> {
let flags = unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
let new_flags = if nonblocking {
flags | libc::O_NONBLOCK
} else {
flags & !libc::O_NONBLOCK
};
let ret = unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_SETFL, new_flags) };
if ret < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
}
impl std::fmt::Debug for DarwinTun {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DarwinTun")
.field("name", &self.name)
.field("fd", &self.fd.as_raw_fd())
.finish()
}
}
impl crate::nat_engine::HostNetIO for DarwinTun {
fn send_packet(&self, packet: &[u8]) -> io::Result<usize> {
self.send_packet(packet)
}
fn recv_packet(&self, buf: &mut [u8]) -> io::Result<usize> {
self.recv_packet(buf)
}
fn has_data(&self) -> bool {
let mut pfd = libc::pollfd {
fd: self.fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&raw mut pfd, 1, 0) };
ret > 0 && (pfd.revents & libc::POLLIN) != 0
}
fn name(&self) -> &str {
self.name()
}
}
unsafe impl Send for DarwinTun {}
unsafe impl Sync for DarwinTun {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_sockaddr_in() {
let addr = Ipv4Addr::new(192, 168, 64, 1);
let sin = DarwinTun::make_sockaddr_in(addr);
assert_eq!(sin.sin_family, libc::AF_INET as u8);
assert_eq!(sin.sin_len, std::mem::size_of::<libc::sockaddr_in>() as u8);
assert_eq!(sin.sin_addr.s_addr, u32::from(addr).to_be());
}
#[test]
#[ignore = "requires macOS with utun device creation privileges"]
fn test_create_utun() {
let tun = DarwinTun::new().expect("Failed to create utun device");
assert!(
tun.name().starts_with("utun"),
"Expected utun prefix, got: {}",
tun.name()
);
assert!(tun.as_raw_fd() >= 0);
println!("Created interface: {}", tun.name());
}
#[test]
#[ignore = "requires macOS with utun device creation privileges"]
fn test_configure_utun() {
let tun = DarwinTun::new().expect("Failed to create utun device");
let result = tun.configure(
Ipv4Addr::new(192, 168, 64, 1),
Ipv4Addr::new(192, 168, 64, 2),
Ipv4Addr::new(255, 255, 255, 0),
);
if let Err(e) = &result {
println!("Configure failed (may need privileges): {}", e);
} else {
println!("Configured {} successfully", tun.name());
}
}
#[test]
#[ignore = "requires macOS with utun device creation privileges"]
fn test_nonblocking_mode() {
let tun = DarwinTun::new().expect("Failed to create utun device");
tun.set_nonblocking(true)
.expect("Failed to set nonblocking");
let mut buf = [0u8; 1500];
match tun.recv_packet(&mut buf) {
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
}
Err(e) => panic!("Unexpected error: {}", e),
Ok(n) => println!("Unexpectedly received {} bytes", n),
}
}
#[test]
#[ignore = "requires macOS with utun device creation privileges"]
fn test_send_recv_loopback() {
let tun = DarwinTun::new().expect("Failed to create utun device");
tun.configure(
Ipv4Addr::new(10, 200, 0, 1),
Ipv4Addr::new(10, 200, 0, 2),
Ipv4Addr::new(255, 255, 255, 0),
)
.expect("Failed to configure");
tun.set_nonblocking(true)
.expect("Failed to set nonblocking");
let mut packet = vec![0u8; 28 + 4]; packet[0] = 0x45; packet[2..4].copy_from_slice(&32u16.to_be_bytes()); packet[8] = 64; packet[9] = 17; packet[12..16].copy_from_slice(&[10, 200, 0, 1]); packet[16..20].copy_from_slice(&[10, 200, 0, 2]); packet[20..22].copy_from_slice(&12345u16.to_be_bytes());
packet[22..24].copy_from_slice(&54321u16.to_be_bytes());
packet[24..26].copy_from_slice(&12u16.to_be_bytes());
packet[28..32].copy_from_slice(b"test");
let written = tun.send_packet(&packet).expect("Failed to send");
assert_eq!(written, packet.len());
println!("Sent {} bytes through {}", written, tun.name());
}
}