easy_tun/
tun.rs

1use std::{
2    fs::{File, OpenOptions},
3    mem::size_of,
4    os::fd::AsRawFd,
5};
6
7use ioctl_gen::{ioc, iow};
8use libc::{
9    __c_anonymous_ifr_ifru, ifreq, ioctl, IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IF_NAMESIZE,
10};
11
12/// Architecture / target environment specific definitions
13mod arch {
14    #[cfg(all(target_os = "linux", target_env = "gnu"))]
15    pub type IoctlRequestType = libc::c_ulong;
16    #[cfg(all(target_os = "linux", target_env = "musl"))]
17    pub type IoctlRequestType = libc::c_int;
18    #[cfg(not(any(target_env = "gnu", target_env = "musl")))]
19    compile_error!(
20        "Unsupported target environment. Only gnu and musl targets are currently supported."
21    );
22}
23
24/// A TUN device
25pub struct Tun {
26    /// All internal file descriptors
27    fds: Vec<File>,
28    /// Device name
29    name: String,
30}
31
32impl Tun {
33    /// Creates a new Tun device with the given name.
34    ///
35    /// The `name` argument must be less than the system's `IFNAMSIZ` constant,
36    /// and may contain a `%d` format specifier to allow for multiple devices with the same name.
37    #[allow(clippy::cast_possible_truncation)]
38    #[allow(clippy::cast_lossless)]
39    pub fn new(dev: &str, queues: usize) -> Result<Self, std::io::Error> {
40        log::debug!(
41            "Creating new TUN device with requested name: {} ({} queues)",
42            dev,
43            queues
44        );
45
46        // Create all needed file descriptors for `/dev/net/tun`
47        log::trace!("Opening /dev/net/tun");
48        let mut fds = Vec::with_capacity(queues);
49        for _ in 0..queues {
50            let fd = OpenOptions::new()
51                .read(true)
52                .write(true)
53                .open("/dev/net/tun")?;
54            fds.push(fd);
55        }
56
57        // Copy the device name into a C string with padding
58        // NOTE: No zero padding is needed because we pre-init the array to all 0s
59        let mut dev_cstr: [libc::c_char; IF_NAMESIZE] = [0; IF_NAMESIZE];
60        let dev_bytes: Vec<libc::c_char> = dev.chars().map(|c| c as libc::c_char).collect();
61        let dev_len = dev_bytes.len().min(IF_NAMESIZE);
62        log::trace!("Device name length after truncation: {}", dev_len);
63        dev_cstr[..dev_len].copy_from_slice(&dev_bytes[..dev_len]);
64
65        // Build an `ifreq` struct to send to the kernel
66        let mut ifr = ifreq {
67            ifr_name: dev_cstr,
68            ifr_ifru: __c_anonymous_ifr_ifru {
69                ifru_flags: (IFF_TUN | IFF_NO_PI | IFF_MULTI_QUEUE) as i16,
70            },
71        };
72
73        // Each FD needs to be configured separately
74        for fd in &mut fds {
75            // Make an ioctl call to create the TUN device
76            log::trace!("Calling ioctl to create TUN device");
77            let err = unsafe {
78                ioctl(
79                    fd.as_raw_fd(),
80                    iow!('T', 202, size_of::<libc::c_int>()) as arch::IoctlRequestType,
81                    &mut ifr,
82                )
83            };
84            log::trace!("ioctl returned: {}", err);
85
86            // Check for errors
87            if err < 0 {
88                log::error!("ioctl failed: {}", err);
89                return Err(std::io::Error::last_os_error());
90            }
91        }
92
93        // Get the name of the device
94        let name = unsafe { std::ffi::CStr::from_ptr(ifr.ifr_name.as_ptr()) }
95            .to_str()
96            .unwrap()
97            .to_string();
98
99        // Log the success
100        log::debug!("Created TUN device: {}", name);
101
102        // Build the TUN struct
103        Ok(Self { fds, name })
104    }
105
106    /// Get the name of the TUN device
107    #[must_use]
108    pub fn name(&self) -> &str {
109        &self.name
110    }
111
112    /// Get the underlying file descriptor
113    #[must_use]
114    pub fn fd(&self, queue_id: usize) -> Option<&File> {
115        self.fds.get(queue_id)
116    }
117
118    /// Get mutable access to the underlying file descriptor
119    #[must_use]
120    pub fn fd_mut(&mut self, queue_id: usize) -> Option<&mut File> {
121        self.fds.get_mut(queue_id).map(|fd| &mut *fd)
122    }
123}