use std::io;
use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use ipnetwork::Ipv4Network;
use super::netlink::NetlinkHandle;
use crate::backend::NetworkBackend;
use crate::error::{NetError, Result};
#[derive(Debug, Clone)]
pub struct TapConfig {
pub name: Option<String>,
pub mac: Option<[u8; 6]>,
pub mtu: u16,
pub vnet_hdr: bool,
}
impl Default for TapConfig {
fn default() -> Self {
Self {
name: None,
mac: None,
mtu: 1500,
vnet_hdr: false,
}
}
}
impl TapConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn with_mac(mut self, mac: [u8; 6]) -> Self {
self.mac = Some(mac);
self
}
#[must_use]
pub fn with_mtu(mut self, mtu: u16) -> Self {
self.mtu = mtu;
self
}
#[must_use]
pub fn with_vnet_hdr(mut self, enabled: bool) -> Self {
self.vnet_hdr = enabled;
self
}
}
pub struct LinuxTap {
fd: OwnedFd,
name: String,
mac: [u8; 6],
mtu: u16,
nonblocking: bool,
vnet_hdr: bool,
}
const TUNSETIFF: libc::c_ulong = 0x400454ca;
const TUNSETOFFLOAD: libc::c_ulong = 0x400454d0;
const TUNSETVNETHDRSZ: libc::c_ulong = 0x400454d8;
const IFF_TAP: libc::c_short = 0x0002;
const IFF_NO_PI: libc::c_short = 0x1000;
const IFF_VNET_HDR: libc::c_short = 0x4000;
const TUN_F_CSUM: u32 = 0x01;
const TUN_F_TSO4: u32 = 0x02;
const TUN_F_TSO6: u32 = 0x04;
#[repr(C)]
struct Ifreq {
ifr_name: [libc::c_char; libc::IFNAMSIZ],
ifr_flags: libc::c_short,
_padding: [u8; 22],
}
impl LinuxTap {
pub fn new(config: TapConfig) -> Result<Self> {
let fd = unsafe {
libc::open(
b"/dev/net/tun\0".as_ptr().cast::<libc::c_char>(),
libc::O_RDWR | libc::O_CLOEXEC,
)
};
if fd < 0 {
return Err(NetError::Tap(format!(
"failed to open /dev/net/tun: {}",
io::Error::last_os_error()
)));
}
let mut ifr = Ifreq {
ifr_name: [0; libc::IFNAMSIZ],
ifr_flags: IFF_TAP | IFF_NO_PI,
_padding: [0; 22],
};
if config.vnet_hdr {
ifr.ifr_flags |= IFF_VNET_HDR;
}
if let Some(ref dev_name) = config.name {
let name_bytes = dev_name.as_bytes();
let len = name_bytes.len().min(libc::IFNAMSIZ - 1);
for (i, &b) in name_bytes[..len].iter().enumerate() {
ifr.ifr_name[i] = b as libc::c_char;
}
}
let ret = unsafe { libc::ioctl(fd, TUNSETIFF, &ifr) };
if ret < 0 {
unsafe { libc::close(fd) };
return Err(NetError::Tap(format!(
"TUNSETIFF failed: {}",
io::Error::last_os_error()
)));
}
let name = {
let len = ifr
.ifr_name
.iter()
.position(|&c| c == 0)
.unwrap_or(libc::IFNAMSIZ);
let bytes: Vec<u8> = ifr.ifr_name[..len].iter().map(|&c| c as u8).collect();
String::from_utf8_lossy(&bytes).into_owned()
};
let fd = unsafe { OwnedFd::from_raw_fd(fd) };
if config.vnet_hdr {
let hdr_sz: i32 = 12; let ret = unsafe { libc::ioctl(fd.as_raw_fd(), TUNSETVNETHDRSZ, &hdr_sz) };
if ret < 0 {
return Err(NetError::Tap(format!(
"TUNSETVNETHDRSZ failed: {}",
io::Error::last_os_error()
)));
}
let offload = TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6;
let ret = unsafe { libc::ioctl(fd.as_raw_fd(), TUNSETOFFLOAD, offload) };
if ret < 0 {
tracing::warn!(
"TUNSETOFFLOAD failed (non-fatal): {}",
io::Error::last_os_error()
);
}
}
let mac = Self::get_mac_address(&name)?;
tracing::info!("Created TAP device: {}", name);
Ok(Self {
fd,
name,
mac,
mtu: config.mtu,
nonblocking: false,
vnet_hdr: config.vnet_hdr,
})
}
fn get_mac_address(ifname: &str) -> Result<[u8; 6]> {
let path = format!("/sys/class/net/{}/address", ifname);
let mac_str = std::fs::read_to_string(&path)
.map_err(|e| NetError::Tap(format!("failed to read MAC address: {}", e)))?;
let mac_str = mac_str.trim();
let bytes: Vec<u8> = mac_str
.split(':')
.filter_map(|s| u8::from_str_radix(s, 16).ok())
.collect();
if bytes.len() != 6 {
return Err(NetError::Tap(format!("invalid MAC address: {}", mac_str)));
}
let mut mac = [0u8; 6];
mac.copy_from_slice(&bytes);
Ok(mac)
}
pub fn set_nonblocking(&mut self, nonblocking: bool) -> Result<()> {
let flags = unsafe { libc::fcntl(self.fd.as_raw_fd(), libc::F_GETFL) };
if flags < 0 {
return Err(NetError::Tap(format!(
"F_GETFL failed: {}",
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(NetError::Tap(format!(
"F_SETFL failed: {}",
io::Error::last_os_error()
)));
}
self.nonblocking = nonblocking;
Ok(())
}
#[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) -> Result<()> {
let mut netlink = NetlinkHandle::new()?;
let ifindex = netlink.get_ifindex(&self.name)?;
netlink.set_link_state(ifindex, true)?;
tracing::debug!("Brought up TAP device {}", self.name);
Ok(())
}
pub fn bring_down(&self) -> Result<()> {
let mut netlink = NetlinkHandle::new()?;
let ifindex = netlink.get_ifindex(&self.name)?;
netlink.set_link_state(ifindex, false)?;
tracing::debug!("Brought down TAP device {}", self.name);
Ok(())
}
pub fn set_ip(&self, addr: Ipv4Network) -> Result<()> {
let mut netlink = NetlinkHandle::new()?;
let ifindex = netlink.get_ifindex(&self.name)?;
netlink.add_address(ifindex, ipnetwork::IpNetwork::V4(addr))?;
tracing::debug!("Set IP {} on TAP device {}", addr, self.name);
Ok(())
}
pub fn ifindex(&self) -> Result<u32> {
let netlink = NetlinkHandle::new()?;
netlink.get_ifindex(&self.name)
}
#[must_use]
pub fn has_vnet_hdr(&self) -> bool {
self.vnet_hdr
}
pub fn send_packet(&mut self, data: &[u8]) -> Result<usize> {
let ret = unsafe { libc::write(self.fd.as_raw_fd(), data.as_ptr().cast(), data.len()) };
if ret < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
return Ok(0);
}
return Err(NetError::Io(err));
}
Ok(ret as usize)
}
pub fn recv_packet(&mut self, buf: &mut [u8]) -> Result<usize> {
let ret = unsafe { libc::read(self.fd.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) };
if ret < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
return Ok(0);
}
return Err(NetError::Io(err));
}
Ok(ret as usize)
}
#[must_use]
pub fn has_data(&self) -> bool {
let mut pollfd = libc::pollfd {
fd: self.fd.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pollfd, 1, 0) };
ret > 0 && (pollfd.revents & libc::POLLIN) != 0
}
}
impl NetworkBackend for LinuxTap {
fn send(&self, data: &[u8]) -> Result<usize> {
let ret = unsafe { libc::write(self.fd.as_raw_fd(), data.as_ptr().cast(), data.len()) };
if ret < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
return Ok(0);
}
return Err(NetError::Io(err));
}
Ok(ret as usize)
}
fn recv(&self, buf: &mut [u8]) -> Result<usize> {
let ret = unsafe { libc::read(self.fd.as_raw_fd(), buf.as_mut_ptr().cast(), buf.len()) };
if ret < 0 {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::WouldBlock {
return Ok(0);
}
return Err(NetError::Io(err));
}
Ok(ret as usize)
}
fn mac(&self) -> [u8; 6] {
self.mac
}
fn mtu(&self) -> u16 {
self.mtu
}
}
impl Drop for LinuxTap {
fn drop(&mut self) {
tracing::debug!("Closing TAP device {}", self.name);
}
}
#[must_use]
pub fn generate_mac() -> [u8; 6] {
let mut mac = [0u8; 6];
if let Ok(mut file) = std::fs::File::open("/dev/urandom") {
use std::io::Read;
let _ = file.read_exact(&mut mac);
}
mac[0] = (mac[0] & 0xfe) | 0x02;
mac[0] &= 0xfe;
mac
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tap_config_default() {
let config = TapConfig::default();
assert!(config.name.is_none());
assert!(config.mac.is_none());
assert_eq!(config.mtu, 1500);
assert!(!config.vnet_hdr);
}
#[test]
fn test_tap_config_builder() {
let mac = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
let config = TapConfig::new()
.with_name("tap-test")
.with_mac(mac)
.with_mtu(9000)
.with_vnet_hdr(true);
assert_eq!(config.name, Some("tap-test".to_string()));
assert_eq!(config.mac, Some(mac));
assert_eq!(config.mtu, 9000);
assert!(config.vnet_hdr);
}
#[test]
fn test_generate_mac() {
let mac = generate_mac();
assert_eq!(mac[0] & 0x02, 0x02);
assert_eq!(mac[0] & 0x01, 0x00);
}
#[test]
fn test_tap_creation_requires_root() {
if unsafe { libc::geteuid() } != 0 {
eprintln!("Skipping test: requires root privileges");
return;
}
let config = TapConfig::new().with_name("arcbox-tap-test");
let tap = LinuxTap::new(config);
assert!(tap.is_ok());
let tap = tap.unwrap();
assert_eq!(tap.name(), "arcbox-tap-test");
assert_eq!(tap.mtu(), 1500);
}
}