#![deny(unsafe_op_in_unsafe_fn)]
#[cfg(target_env = "musl")]
type IoctlReq = libc::c_int;
#[cfg(not(target_env = "musl"))]
type IoctlReq = libc::c_ulong;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
const TUN_PATH: &str = "/dev/net/tun";
const IFF_TUN: libc::c_short = 0x0001;
const IFF_TAP: libc::c_short = 0x0002;
const IFF_MULTI_QUEUE: libc::c_short = 0x0100;
const IFF_NO_PI: libc::c_short = 0x1000;
const IFF_VNET_HDR: libc::c_short = 0x4000;
const TUNSETIFF: libc::c_ulong = 0x4004_54ca;
const TUNSETOFFLOAD: libc::c_ulong = 0x4004_54d0;
pub const TUNSETVNETHDRSZ: libc::c_ulong = 0x4004_54d8;
pub const TUNSETVNETHDR: libc::c_ulong = TUNSETVNETHDRSZ;
const TUN_F_CSUM: libc::c_uint = 0x01;
const TUN_F_TSO4: libc::c_uint = 0x02;
const TUN_F_TSO6: libc::c_uint = 0x04;
pub const VNET_HDR_LEN: usize = 10;
#[derive(Debug, thiserror::Error)]
pub enum DeviceError {
#[error("interface name too long")]
NameTooLong,
#[error("device io error: {0}")]
Io(#[from] io::Error),
}
fn encode_ifname(name: &str) -> Result<[u8; libc::IFNAMSIZ], DeviceError> {
let bytes = name.as_bytes();
if bytes.len() >= libc::IFNAMSIZ {
return Err(DeviceError::NameTooLong);
}
let mut buf = [0u8; libc::IFNAMSIZ];
buf[..bytes.len()].copy_from_slice(bytes);
Ok(buf)
}
fn decode_ifname(buf: &[u8; libc::IFNAMSIZ]) -> Result<String, DeviceError> {
let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
let s = std::str::from_utf8(&buf[..nul_pos])
.map_err(|e| DeviceError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
Ok(s.to_owned())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OffloadMode {
Full,
CsumOnly,
}
fn negotiate_offload(fd: std::os::fd::RawFd) -> Option<OffloadMode> {
let full = unsafe {
libc::ioctl(
fd,
TUNSETOFFLOAD as IoctlReq,
libc::c_ulong::from(TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6),
)
};
if full == 0 {
return Some(OffloadMode::Full);
}
let csum_only = unsafe {
libc::ioctl(
fd,
TUNSETOFFLOAD as IoctlReq,
libc::c_ulong::from(TUN_F_CSUM),
)
};
if csum_only == 0 {
return Some(OffloadMode::CsumOnly);
}
None
}
fn apply_offload(fd: std::os::fd::RawFd, mode: OffloadMode) -> Result<(), DeviceError> {
let flags = match mode {
OffloadMode::Full => libc::c_ulong::from(TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6),
OffloadMode::CsumOnly => libc::c_ulong::from(TUN_F_CSUM),
};
let rc = unsafe { libc::ioctl(fd, TUNSETOFFLOAD as IoctlReq, flags) };
if rc != 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceKind {
Tun,
Tap,
}
pub trait Device {
fn kind(&self) -> DeviceKind;
fn read_frame(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
fn write_frame(&mut self, frame: &[u8]) -> std::io::Result<usize>;
}
fn bring_up(ifname: &[u8; libc::IFNAMSIZ]) -> Result<(), DeviceError> {
#[repr(C)]
struct IfReqFlags {
name: [u8; libc::IFNAMSIZ],
flags: libc::c_short,
_pad: [u8; 22],
}
let mut req = IfReqFlags {
name: *ifname,
flags: 0,
_pad: [0; 22],
};
let err = unsafe {
let sock_fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
if sock_fd < 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
let rc_get = libc::ioctl(sock_fd, libc::SIOCGIFFLAGS as IoctlReq, &raw mut req);
if rc_get == 0 {
req.flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
let rc_set = libc::ioctl(sock_fd, libc::SIOCSIFFLAGS as IoctlReq, &raw mut req);
libc::close(sock_fd);
if rc_set != 0 {
Some(io::Error::last_os_error())
} else {
None
}
} else {
let e = io::Error::last_os_error();
libc::close(sock_fd);
Some(e)
}
};
match err {
Some(e) => Err(DeviceError::Io(e)),
None => Ok(()),
}
}
fn set_device_mtu(ifname: &[u8; libc::IFNAMSIZ], mtu: u32) -> Result<(), DeviceError> {
#[repr(C)]
struct IfReqMtu {
name: [u8; libc::IFNAMSIZ],
mtu: libc::c_int,
_pad: [u8; 20],
}
let req = IfReqMtu {
name: *ifname,
mtu: mtu as libc::c_int,
_pad: [0; 20],
};
let err = unsafe {
let sock_fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
if sock_fd < 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
let rc = libc::ioctl(sock_fd, libc::SIOCSIFMTU as IoctlReq, &req);
let e = if rc != 0 {
Some(io::Error::last_os_error())
} else {
None
};
libc::close(sock_fd);
e
};
match err {
Some(e) => Err(DeviceError::Io(e)),
None => Ok(()),
}
}
#[repr(C)]
struct IfReq {
name: [u8; libc::IFNAMSIZ],
flags: libc::c_short,
_pad: [u8; 22],
}
pub struct TunTap {
file: std::fs::File,
kind: DeviceKind,
name: String,
vnet_hdr_len: Option<usize>,
}
impl TunTap {
pub fn create(
name: &str,
kind: DeviceKind,
want_vnet_hdr: bool,
) -> Result<TunTap, DeviceError> {
let ifname = encode_ifname(name)?;
if want_vnet_hdr {
if let Some(tun) = Self::open_with_offload(kind, &ifname)? {
return Ok(tun);
}
}
Self::open_plain(kind, &ifname)
}
pub fn create_multi_queue(
name: &str,
kind: DeviceKind,
queue_count: usize,
want_vnet_hdr: bool,
) -> Result<Vec<TunTap>, DeviceError> {
if queue_count == 0 {
return Err(DeviceError::Io(io::Error::new(
io::ErrorKind::InvalidInput,
"queue_count must be at least 1",
)));
}
if queue_count == 1 {
let tun = Self::create(name, kind, want_vnet_hdr)?;
tun.set_nonblocking()?;
return Ok(vec![tun]);
}
let ifname = encode_ifname(name)?;
if want_vnet_hdr {
if let Some(queues) = Self::open_multi_queue_with_offload(kind, &ifname, queue_count)? {
return Ok(queues);
}
}
Self::open_multi_queue_plain(kind, &ifname, queue_count)
}
fn open_with_offload(
kind: DeviceKind,
ifname: &[u8; libc::IFNAMSIZ],
) -> Result<Option<TunTap>, DeviceError> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(TUN_PATH)?;
let type_flag = match kind {
DeviceKind::Tun => IFF_TUN,
DeviceKind::Tap => IFF_TAP,
};
let mut req = IfReq {
name: *ifname,
flags: type_flag | IFF_NO_PI | IFF_VNET_HDR,
_pad: [0; 22],
};
let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
if rc != 0 {
return Ok(None);
}
let mut hdr_sz: libc::c_int = VNET_HDR_LEN as libc::c_int;
let _ = unsafe {
libc::ioctl(
file.as_raw_fd(),
TUNSETVNETHDRSZ as IoctlReq,
&raw mut hdr_sz,
)
};
if let Some(_mode) = negotiate_offload(file.as_raw_fd()) {
bring_up(&req.name)?;
let actual_name = decode_ifname(&req.name)?;
return Ok(Some(TunTap {
file,
kind,
name: actual_name,
vnet_hdr_len: Some(VNET_HDR_LEN),
}));
}
Ok(None)
}
fn open_plain(kind: DeviceKind, ifname: &[u8; libc::IFNAMSIZ]) -> Result<TunTap, DeviceError> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(TUN_PATH)?;
let type_flag = match kind {
DeviceKind::Tun => IFF_TUN,
DeviceKind::Tap => IFF_TAP,
};
let mut req = IfReq {
name: *ifname,
flags: type_flag | IFF_NO_PI,
_pad: [0; 22],
};
let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
if rc != 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
bring_up(&req.name)?;
let actual_name = decode_ifname(&req.name)?;
Ok(TunTap {
file,
kind,
name: actual_name,
vnet_hdr_len: None,
})
}
fn open_multi_queue_with_offload(
kind: DeviceKind,
ifname: &[u8; libc::IFNAMSIZ],
queue_count: usize,
) -> Result<Option<Vec<TunTap>>, DeviceError> {
let file0 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(TUN_PATH)?;
let type_flag = match kind {
DeviceKind::Tun => IFF_TUN,
DeviceKind::Tap => IFF_TAP,
};
let mut req0 = IfReq {
name: *ifname,
flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE | IFF_VNET_HDR,
_pad: [0; 22],
};
let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req0) };
if rc != 0 {
return Ok(None);
}
let mut hdr_sz0: libc::c_int = VNET_HDR_LEN as libc::c_int;
let _ = unsafe {
libc::ioctl(
file0.as_raw_fd(),
TUNSETVNETHDRSZ as IoctlReq,
&raw mut hdr_sz0,
)
};
let offload_mode = match negotiate_offload(file0.as_raw_fd()) {
Some(mode) => mode,
None => return Ok(None),
};
bring_up(&req0.name)?;
let actual_name = decode_ifname(&req0.name)?;
let q0 = TunTap {
file: file0,
kind,
name: actual_name.clone(),
vnet_hdr_len: Some(VNET_HDR_LEN),
};
q0.set_nonblocking()?;
let mut queues = Vec::with_capacity(queue_count);
queues.push(q0);
for _ in 1..queue_count {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(TUN_PATH)?;
let mut req = IfReq {
name: req0.name,
flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE | IFF_VNET_HDR,
_pad: [0; 22],
};
let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
if rc != 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
let mut hdr_sz: libc::c_int = VNET_HDR_LEN as libc::c_int;
let _ = unsafe {
libc::ioctl(
file.as_raw_fd(),
TUNSETVNETHDRSZ as IoctlReq,
&raw mut hdr_sz,
)
};
apply_offload(file.as_raw_fd(), offload_mode)?;
let q = TunTap {
file,
kind,
name: actual_name.clone(),
vnet_hdr_len: Some(VNET_HDR_LEN),
};
q.set_nonblocking()?;
queues.push(q);
}
Ok(Some(queues))
}
fn open_multi_queue_plain(
kind: DeviceKind,
ifname: &[u8; libc::IFNAMSIZ],
queue_count: usize,
) -> Result<Vec<TunTap>, DeviceError> {
let file0 = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(TUN_PATH)?;
let type_flag = match kind {
DeviceKind::Tun => IFF_TUN,
DeviceKind::Tap => IFF_TAP,
};
let mut req0 = IfReq {
name: *ifname,
flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE,
_pad: [0; 22],
};
let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req0) };
if rc != 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
bring_up(&req0.name)?;
let actual_name = decode_ifname(&req0.name)?;
let q0 = TunTap {
file: file0,
kind,
name: actual_name.clone(),
vnet_hdr_len: None,
};
q0.set_nonblocking()?;
let mut queues = Vec::with_capacity(queue_count);
queues.push(q0);
for _ in 1..queue_count {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(TUN_PATH)?;
let mut req = IfReq {
name: req0.name,
flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE,
_pad: [0; 22],
};
let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
if rc != 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
let q = TunTap {
file,
kind,
name: actual_name.clone(),
vnet_hdr_len: None,
};
q.set_nonblocking()?;
queues.push(q);
}
Ok(queues)
}
pub fn name(&self) -> &str {
&self.name
}
pub fn vnet_hdr_len(&self) -> Option<usize> {
self.vnet_hdr_len
}
pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
pub fn set_nonblocking(&self) -> Result<(), DeviceError> {
let rc = unsafe { libc::fcntl(self.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) };
if rc < 0 {
Err(DeviceError::Io(io::Error::last_os_error()))
} else {
Ok(())
}
}
pub fn set_mtu(&self, mtu: u32) -> Result<(), DeviceError> {
let ifname = encode_ifname(&self.name)?;
set_device_mtu(&ifname, mtu)
}
}
pub struct TunReader {
file: std::fs::File,
}
pub struct TunWriter {
file: std::fs::File,
}
impl TunReader {
pub fn read_frame(&mut self, buf: &mut [u8]) -> io::Result<usize> {
use std::io::Read;
self.file.read(buf)
}
pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
impl TunWriter {
pub fn write_frame(&mut self, frame: &[u8]) -> io::Result<usize> {
use std::io::Write;
self.file.write(frame)
}
pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
impl TunTap {
pub fn split(self) -> Result<(TunReader, TunWriter), DeviceError> {
let raw = self.file.as_raw_fd();
let dup = unsafe { libc::dup(raw) };
if dup < 0 {
return Err(DeviceError::Io(io::Error::last_os_error()));
}
let dup_file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(dup) });
let reader = TunReader { file: dup_file };
let writer = TunWriter { file: self.file };
Ok((reader, writer))
}
}
impl Device for TunTap {
fn kind(&self) -> DeviceKind {
self.kind
}
fn read_frame(&mut self, buf: &mut [u8]) -> io::Result<usize> {
use std::io::Read;
self.file.read(buf)
}
fn write_frame(&mut self, frame: &[u8]) -> io::Result<usize> {
use std::io::Write;
self.file.write(frame)
}
}
impl AsRawFd for TunTap {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
impl AsRawFd for TunReader {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
impl AsRawFd for TunWriter {
fn as_raw_fd(&self) -> std::os::fd::RawFd {
self.file.as_raw_fd()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_kinds_are_distinct() {
assert_ne!(DeviceKind::Tun, DeviceKind::Tap);
}
#[test]
fn ifname_encodes_and_rejects_too_long() {
let enc = encode_ifname("yip0").unwrap();
assert_eq!(&enc[..4], b"yip0");
assert_eq!(enc[4], 0, "NUL-padded");
let long = "x".repeat(libc::IFNAMSIZ); assert!(matches!(
encode_ifname(&long),
Err(DeviceError::NameTooLong)
));
}
fn can_create_devices() -> bool {
TunTap::create("yipcap0", DeviceKind::Tun, false)
.map(drop)
.is_ok()
}
#[test]
fn tun_create_roundtrips_a_write() {
if !can_create_devices() {
eprintln!("SKIP tun_create_roundtrips_a_write: needs CAP_NET_ADMIN (run under sudo)");
return;
}
let mut dev = TunTap::create("yiptun0", DeviceKind::Tun, false).unwrap();
assert_eq!(dev.kind(), DeviceKind::Tun);
assert_eq!(dev.name(), "yiptun0");
let pkt = [
0x45u8, 0, 0, 20, 0, 0, 0, 0, 64, 17, 0, 0, 10, 9, 9, 1, 10, 9, 9, 2,
];
let n = dev.write_frame(&pkt).unwrap();
assert_eq!(n, pkt.len());
}
#[test]
fn tap_create_reports_l2_kind() {
if !can_create_devices() {
eprintln!("SKIP tap_create_reports_l2_kind: needs CAP_NET_ADMIN (run under sudo)");
return;
}
let dev = TunTap::create("yiptap0", DeviceKind::Tap, false).unwrap();
assert_eq!(dev.kind(), DeviceKind::Tap);
assert_eq!(dev.name(), "yiptap0");
}
#[test]
fn split_yields_independent_reader_writer() {
if !can_create_devices() {
eprintln!("SKIP split_yields_independent_reader_writer: needs CAP_NET_ADMIN");
return;
}
let dev = TunTap::create("yipsplit0", DeviceKind::Tun, false).unwrap();
let (_reader, mut writer) = dev.split().unwrap();
let pkt = [
0x45u8, 0, 0, 20, 0, 0, 0, 0, 64, 17, 0, 0, 10, 9, 9, 1, 10, 9, 9, 2,
];
assert_eq!(writer.write_frame(&pkt).unwrap(), pkt.len());
}
#[test]
fn plain_create_reports_no_vnet_hdr() {
if !can_create_devices() {
eprintln!(
"SKIP plain_create_reports_no_vnet_hdr: needs CAP_NET_ADMIN (run under sudo)"
);
return;
}
let dev = TunTap::create("yipvnh0", DeviceKind::Tun, false).unwrap();
assert_eq!(
dev.vnet_hdr_len(),
None,
"want_vnet_hdr=false must never activate vnet_hdr framing"
);
}
#[test]
fn set_mtu_fails_on_too_long_name() {
let dummy_file = std::fs::File::open("/dev/null").unwrap();
let dev = TunTap {
file: dummy_file,
kind: DeviceKind::Tun,
name: "x".repeat(libc::IFNAMSIZ),
vnet_hdr_len: None,
};
assert!(matches!(dev.set_mtu(1500), Err(DeviceError::NameTooLong)));
}
#[test]
fn set_device_mtu_nonexistent_interface() {
let enc = encode_ifname("nonexistent0").unwrap();
let res = set_device_mtu(&enc, 1500);
assert!(matches!(res, Err(DeviceError::Io(_))));
}
#[test]
fn set_mtu_nonexistent_interface() {
let dummy_file = std::fs::File::open("/dev/null").unwrap();
let dev = TunTap {
file: dummy_file,
kind: DeviceKind::Tun,
name: "nonexistent0".to_owned(),
vnet_hdr_len: None,
};
assert!(matches!(dev.set_mtu(1500), Err(DeviceError::Io(_))));
}
#[test]
fn tun_set_mtu_succeeds_when_privileged() {
if !can_create_devices() {
eprintln!("SKIP tun_set_mtu_succeeds_when_privileged: needs CAP_NET_ADMIN");
return;
}
let dev = TunTap::create("yipmtu0", DeviceKind::Tun, false).unwrap();
assert!(dev.set_mtu(1400).is_ok());
assert!(dev.set_mtu(1500).is_ok());
}
}