Skip to main content

yip_device/
lib.rs

1//! L3 (TUN) and L2 (TAP) tunnel endpoints behind one trait. Real device
2//! I/O lands in M4; this milestone fixes the public surface.
3#![deny(unsafe_op_in_unsafe_fn)]
4
5#[cfg(target_env = "musl")]
6type IoctlReq = libc::c_int;
7#[cfg(not(target_env = "musl"))]
8type IoctlReq = libc::c_ulong;
9
10use std::io;
11use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
12
13const TUN_PATH: &str = "/dev/net/tun";
14const IFF_TUN: libc::c_short = 0x0001;
15const IFF_TAP: libc::c_short = 0x0002;
16/// Support multiple independent queue file descriptors for the same interface.
17const IFF_MULTI_QUEUE: libc::c_short = 0x0100;
18const IFF_NO_PI: libc::c_short = 0x1000;
19/// Prefix every TUN read/write with a `virtio_net_hdr` (GSO/GRO framing).
20const IFF_VNET_HDR: libc::c_short = 0x4000;
21// _IOW('T', 202, int) on Linux.
22const TUNSETIFF: libc::c_ulong = 0x4004_54ca;
23// _IOW('T', 208, unsigned int) on Linux.
24const TUNSETOFFLOAD: libc::c_ulong = 0x4004_54d0;
25// _IOW('T', 216, int) on Linux.
26pub const TUNSETVNETHDRSZ: libc::c_ulong = 0x4004_54d8;
27/// Alias for `TUNSETVNETHDRSZ`.
28pub const TUNSETVNETHDR: libc::c_ulong = TUNSETVNETHDRSZ;
29const TUN_F_CSUM: libc::c_uint = 0x01;
30const TUN_F_TSO4: libc::c_uint = 0x02;
31const TUN_F_TSO6: libc::c_uint = 0x04;
32
33/// Length (bytes) of the `virtio_net_hdr` prefix on every TUN read/write when
34/// [`TunTap::vnet_hdr_len`] is `Some`. Kept as its own constant (rather than
35/// depending on `yip-io`) so `yip-device` has no dependency on `yip-io`.
36pub const VNET_HDR_LEN: usize = 10;
37
38/// Errors creating or configuring a tunnel device.
39#[derive(Debug, thiserror::Error)]
40pub enum DeviceError {
41    /// Interface name does not fit in `IFNAMSIZ` (including the NUL terminator).
42    #[error("interface name too long")]
43    NameTooLong,
44    /// Underlying OS error (open / ioctl / read / write).
45    #[error("device io error: {0}")]
46    Io(#[from] io::Error),
47}
48
49/// Encode an interface name into a NUL-padded `IFNAMSIZ` buffer.
50fn encode_ifname(name: &str) -> Result<[u8; libc::IFNAMSIZ], DeviceError> {
51    let bytes = name.as_bytes();
52    if bytes.len() >= libc::IFNAMSIZ {
53        return Err(DeviceError::NameTooLong);
54    }
55    let mut buf = [0u8; libc::IFNAMSIZ];
56    buf[..bytes.len()].copy_from_slice(bytes);
57    Ok(buf)
58}
59
60/// Decode a NUL-padded `IFNAMSIZ` buffer into a `String`.
61fn decode_ifname(buf: &[u8; libc::IFNAMSIZ]) -> Result<String, DeviceError> {
62    let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
63    let s = std::str::from_utf8(&buf[..nul_pos])
64        .map_err(|e| DeviceError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))?;
65    Ok(s.to_owned())
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69enum OffloadMode {
70    Full,
71    CsumOnly,
72}
73
74fn negotiate_offload(fd: std::os::fd::RawFd) -> Option<OffloadMode> {
75    // SAFETY: `fd` is a valid open /dev/net/tun fd on which TUNSETIFF (with IFF_VNET_HDR)
76    // just succeeded. TUNSETOFFLOAD takes its flags as an integer argument passed by value.
77    let full = unsafe {
78        libc::ioctl(
79            fd,
80            TUNSETOFFLOAD as IoctlReq,
81            libc::c_ulong::from(TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6),
82        )
83    };
84    if full == 0 {
85        return Some(OffloadMode::Full);
86    }
87
88    // SAFETY: same rationale as the previous TUNSETOFFLOAD call.
89    let csum_only = unsafe {
90        libc::ioctl(
91            fd,
92            TUNSETOFFLOAD as IoctlReq,
93            libc::c_ulong::from(TUN_F_CSUM),
94        )
95    };
96    if csum_only == 0 {
97        return Some(OffloadMode::CsumOnly);
98    }
99
100    None
101}
102
103fn apply_offload(fd: std::os::fd::RawFd, mode: OffloadMode) -> Result<(), DeviceError> {
104    let flags = match mode {
105        OffloadMode::Full => libc::c_ulong::from(TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6),
106        OffloadMode::CsumOnly => libc::c_ulong::from(TUN_F_CSUM),
107    };
108    // SAFETY: `fd` is a valid open /dev/net/tun fd. TUNSETOFFLOAD takes its flags as an integer passed by value.
109    let rc = unsafe { libc::ioctl(fd, TUNSETOFFLOAD as IoctlReq, flags) };
110    if rc != 0 {
111        return Err(DeviceError::Io(io::Error::last_os_error()));
112    }
113    Ok(())
114}
115
116/// Whether a device operates at L3 (IP) or L2 (Ethernet).
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum DeviceKind {
119    /// L3 IP tunnel (`/dev/net/tun`, TUN mode).
120    Tun,
121    /// L2 Ethernet tap (`/dev/net/tun`, TAP mode) with MAC learning.
122    Tap,
123}
124
125/// A tunnel endpoint that yields and accepts inner frames. Implemented in M4.
126pub trait Device {
127    /// Whether this is an L3 (TUN) or L2 (TAP) device.
128    fn kind(&self) -> DeviceKind;
129    /// Read one inner frame into `buf`, returning its length.
130    fn read_frame(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
131    /// Write one inner frame, returning the number of bytes written.
132    fn write_frame(&mut self, frame: &[u8]) -> std::io::Result<usize>;
133}
134
135/// Bring a network interface up (set IFF_UP | IFF_RUNNING).
136///
137/// Opens a temporary AF_INET socket to issue `SIOCGIFFLAGS` / `SIOCSIFFLAGS`.
138fn bring_up(ifname: &[u8; libc::IFNAMSIZ]) -> Result<(), DeviceError> {
139    // struct ifreq layout for SIOCGIFFLAGS / SIOCSIFFLAGS: name then ifr_flags (c_short).
140    #[repr(C)]
141    struct IfReqFlags {
142        name: [u8; libc::IFNAMSIZ],
143        flags: libc::c_short,
144        _pad: [u8; 22],
145    }
146    let mut req = IfReqFlags {
147        name: *ifname,
148        flags: 0,
149        _pad: [0; 22],
150    };
151
152    // SAFETY: `sock_fd` is a valid AF_INET socket returned by `socket(2)`; `req` is a
153    // correctly-laid-out `ifreq` for SIOCGIFFLAGS / SIOCSIFFLAGS, exclusively owned here.
154    // We check both ioctl return values and close the socket before returning.
155    let err = unsafe {
156        let sock_fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
157        if sock_fd < 0 {
158            return Err(DeviceError::Io(io::Error::last_os_error()));
159        }
160        let rc_get = libc::ioctl(sock_fd, libc::SIOCGIFFLAGS as IoctlReq, &raw mut req);
161        if rc_get == 0 {
162            // FFI: libc exposes IFF_UP/IFF_RUNNING as c_int, but `ifreq.ifr_flags`
163            // is c_short; the combined value (0x41) fits trivially.
164            req.flags |= (libc::IFF_UP | libc::IFF_RUNNING) as libc::c_short;
165            let rc_set = libc::ioctl(sock_fd, libc::SIOCSIFFLAGS as IoctlReq, &raw mut req);
166            libc::close(sock_fd);
167            if rc_set != 0 {
168                Some(io::Error::last_os_error())
169            } else {
170                None
171            }
172        } else {
173            let e = io::Error::last_os_error();
174            libc::close(sock_fd);
175            Some(e)
176        }
177    };
178    match err {
179        Some(e) => Err(DeviceError::Io(e)),
180        None => Ok(()),
181    }
182}
183
184/// Set the MTU of a network interface via `SIOCSIFMTU`.
185fn set_device_mtu(ifname: &[u8; libc::IFNAMSIZ], mtu: u32) -> Result<(), DeviceError> {
186    #[repr(C)]
187    struct IfReqMtu {
188        name: [u8; libc::IFNAMSIZ],
189        mtu: libc::c_int,
190        _pad: [u8; 20],
191    }
192    let req = IfReqMtu {
193        name: *ifname,
194        mtu: mtu as libc::c_int,
195        _pad: [0; 20],
196    };
197
198    // SAFETY: `sock_fd` is a valid AF_INET socket returned by `socket(2)`; `req` is a
199    // correctly-laid-out `ifreq` for SIOCSIFMTU, exclusively owned here. We check both
200    // socket creation and ioctl return values, and close `sock_fd` before returning.
201    let err = unsafe {
202        let sock_fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
203        if sock_fd < 0 {
204            return Err(DeviceError::Io(io::Error::last_os_error()));
205        }
206        let rc = libc::ioctl(sock_fd, libc::SIOCSIFMTU as IoctlReq, &req);
207        let e = if rc != 0 {
208            Some(io::Error::last_os_error())
209        } else {
210            None
211        };
212        libc::close(sock_fd);
213        e
214    };
215    match err {
216        Some(e) => Err(DeviceError::Io(e)),
217        None => Ok(()),
218    }
219}
220
221// struct ifreq: name[IFNAMSIZ] then a union; we only set ifr_flags.
222#[repr(C)]
223struct IfReq {
224    name: [u8; libc::IFNAMSIZ],
225    flags: libc::c_short,
226    _pad: [u8; 22],
227}
228
229/// A TUN (L3) or TAP (L2) tunnel device.
230pub struct TunTap {
231    file: std::fs::File,
232    kind: DeviceKind,
233    name: String,
234    /// `Some(VNET_HDR_LEN)` iff `IFF_VNET_HDR` framing + kernel GSO/GRO
235    /// offload are both active on this fd; `None` for a plain device.
236    vnet_hdr_len: Option<usize>,
237}
238
239impl TunTap {
240    /// Create a tunnel device of `kind` named `name`. Requires `CAP_NET_ADMIN`.
241    ///
242    /// When `want_vnet_hdr` is set, first attempts to open with `IFF_VNET_HDR`
243    /// framing and kernel GSO/GRO offload (`TUNSETOFFLOAD`) enabled. If the
244    /// kernel or driver doesn't support one of the two, this falls back
245    /// transparently to a plain device (no vnet_hdr framing) with a fresh fd
246    /// — see [`TunTap::vnet_hdr_len`] to check which mode was actually
247    /// negotiated.
248    pub fn create(
249        name: &str,
250        kind: DeviceKind,
251        want_vnet_hdr: bool,
252    ) -> Result<TunTap, DeviceError> {
253        let ifname = encode_ifname(name)?;
254
255        if want_vnet_hdr {
256            if let Some(tun) = Self::open_with_offload(kind, &ifname)? {
257                return Ok(tun);
258            }
259        }
260
261        Self::open_plain(kind, &ifname)
262    }
263
264    /// Create a multi-queue TUN/TAP device with `queue_count` independent queue fds.
265    ///
266    /// When `queue_count == 1`, delegates transparently to [`TunTap::create`].
267    /// When `queue_count > 1`, opens `/dev/net/tun` `queue_count` times with
268    /// `IFF_MULTI_QUEUE` set, returning one [`TunTap`] per queue.
269    ///
270    /// All returned queue fds are placed in non-blocking mode (`O_NONBLOCK`).
271    /// If `want_vnet_hdr` is requested, `IFF_VNET_HDR` and kernel offload
272    /// (`TUNSETOFFLOAD`) are negotiated across all queues, falling back
273    /// transparently to plain queues if unsupported.
274    pub fn create_multi_queue(
275        name: &str,
276        kind: DeviceKind,
277        queue_count: usize,
278        want_vnet_hdr: bool,
279    ) -> Result<Vec<TunTap>, DeviceError> {
280        if queue_count == 0 {
281            return Err(DeviceError::Io(io::Error::new(
282                io::ErrorKind::InvalidInput,
283                "queue_count must be at least 1",
284            )));
285        }
286
287        if queue_count == 1 {
288            let tun = Self::create(name, kind, want_vnet_hdr)?;
289            tun.set_nonblocking()?;
290            return Ok(vec![tun]);
291        }
292
293        let ifname = encode_ifname(name)?;
294
295        if want_vnet_hdr {
296            if let Some(queues) = Self::open_multi_queue_with_offload(kind, &ifname, queue_count)? {
297                return Ok(queues);
298            }
299        }
300
301        Self::open_multi_queue_plain(kind, &ifname, queue_count)
302    }
303
304    /// Attempt to open with `IFF_VNET_HDR` framing and kernel GSO/GRO
305    /// offload (`TUNSETOFFLOAD`) enabled. Returns `Ok(Some(_))` only when
306    /// `TUNSETIFF` (with `IFF_VNET_HDR`) *and* `TUNSETOFFLOAD` both succeed;
307    /// `Ok(None)` on any unsupported step (the freshly-opened fd is dropped/
308    /// closed here), so the caller falls back to [`Self::open_plain`] with a
309    /// fresh fd — clean framing, no half-enabled state. A hard I/O failure
310    /// that would fail identically on the plain path (e.g. `/dev/net/tun`
311    /// cannot be opened at all) is propagated as `Err`.
312    fn open_with_offload(
313        kind: DeviceKind,
314        ifname: &[u8; libc::IFNAMSIZ],
315    ) -> Result<Option<TunTap>, DeviceError> {
316        let file = std::fs::OpenOptions::new()
317            .read(true)
318            .write(true)
319            .open(TUN_PATH)?;
320
321        let type_flag = match kind {
322            DeviceKind::Tun => IFF_TUN,
323            DeviceKind::Tap => IFF_TAP,
324        };
325        let mut req = IfReq {
326            name: *ifname,
327            flags: type_flag | IFF_NO_PI | IFF_VNET_HDR,
328            _pad: [0; 22],
329        };
330
331        // SAFETY: `req` is a correctly-sized, properly-initialized `ifreq` for
332        // TUNSETIFF; the fd is a freshly-opened /dev/net/tun. The kernel reads
333        // `req` and writes back the resolved name into the same buffer, which
334        // we own exclusively here.
335        let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
336        if rc != 0 {
337            // Driver rejected IFF_VNET_HDR (or the type/name flags altogether).
338            // `file` drops here, closing the fd; caller reopens plain.
339            return Ok(None);
340        }
341
342        // Set or verify vnet header size to VNET_HDR_LEN (10).
343        let mut hdr_sz: libc::c_int = VNET_HDR_LEN as libc::c_int;
344        // SAFETY: `file` is a valid open /dev/net/tun fd. TUNSETVNETHDRSZ takes an int pointer
345        // specifying the virtio_net_hdr byte size.
346        let _ = unsafe {
347            libc::ioctl(
348                file.as_raw_fd(),
349                TUNSETVNETHDRSZ as IoctlReq,
350                &raw mut hdr_sz,
351            )
352        };
353
354        if let Some(_mode) = negotiate_offload(file.as_raw_fd()) {
355            bring_up(&req.name)?;
356            let actual_name = decode_ifname(&req.name)?;
357            return Ok(Some(TunTap {
358                file,
359                kind,
360                name: actual_name,
361                vnet_hdr_len: Some(VNET_HDR_LEN),
362            }));
363        }
364
365        // Neither offload attempt was accepted: the fd has vnet_hdr framing
366        // but no GSO/GRO — not a state we run with. `file` drops here
367        // (closing the fd); the caller reopens plain.
368        Ok(None)
369    }
370
371    /// Open without `IFF_VNET_HDR` framing (today's plain TUN/TAP path).
372    fn open_plain(kind: DeviceKind, ifname: &[u8; libc::IFNAMSIZ]) -> Result<TunTap, DeviceError> {
373        let file = std::fs::OpenOptions::new()
374            .read(true)
375            .write(true)
376            .open(TUN_PATH)?;
377
378        let type_flag = match kind {
379            DeviceKind::Tun => IFF_TUN,
380            DeviceKind::Tap => IFF_TAP,
381        };
382        let mut req = IfReq {
383            name: *ifname,
384            flags: type_flag | IFF_NO_PI,
385            _pad: [0; 22],
386        };
387
388        // SAFETY: `req` is a correctly-sized, properly-initialized `ifreq` for TUNSETIFF;
389        // the fd is a freshly-opened /dev/net/tun. The kernel reads `req` and writes back
390        // the resolved name into the same buffer, which we own exclusively here.
391        let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
392        if rc != 0 {
393            return Err(DeviceError::Io(io::Error::last_os_error()));
394        }
395
396        // Bring the interface up so that reads and writes work immediately.
397        bring_up(&req.name)?;
398        let actual_name = decode_ifname(&req.name)?;
399
400        Ok(TunTap {
401            file,
402            kind,
403            name: actual_name,
404            vnet_hdr_len: None,
405        })
406    }
407
408    fn open_multi_queue_with_offload(
409        kind: DeviceKind,
410        ifname: &[u8; libc::IFNAMSIZ],
411        queue_count: usize,
412    ) -> Result<Option<Vec<TunTap>>, DeviceError> {
413        let file0 = std::fs::OpenOptions::new()
414            .read(true)
415            .write(true)
416            .open(TUN_PATH)?;
417
418        let type_flag = match kind {
419            DeviceKind::Tun => IFF_TUN,
420            DeviceKind::Tap => IFF_TAP,
421        };
422        let mut req0 = IfReq {
423            name: *ifname,
424            flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE | IFF_VNET_HDR,
425            _pad: [0; 22],
426        };
427
428        // SAFETY: req0 is a correctly-sized ifreq for TUNSETIFF; file0 is a freshly-opened /dev/net/tun.
429        let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req0) };
430        if rc != 0 {
431            return Ok(None);
432        }
433
434        let mut hdr_sz0: libc::c_int = VNET_HDR_LEN as libc::c_int;
435        // SAFETY: file0 is a valid open /dev/net/tun fd. TUNSETVNETHDRSZ takes an int pointer.
436        let _ = unsafe {
437            libc::ioctl(
438                file0.as_raw_fd(),
439                TUNSETVNETHDRSZ as IoctlReq,
440                &raw mut hdr_sz0,
441            )
442        };
443
444        let offload_mode = match negotiate_offload(file0.as_raw_fd()) {
445            Some(mode) => mode,
446            None => return Ok(None),
447        };
448
449        bring_up(&req0.name)?;
450        let actual_name = decode_ifname(&req0.name)?;
451
452        let q0 = TunTap {
453            file: file0,
454            kind,
455            name: actual_name.clone(),
456            vnet_hdr_len: Some(VNET_HDR_LEN),
457        };
458        q0.set_nonblocking()?;
459
460        let mut queues = Vec::with_capacity(queue_count);
461        queues.push(q0);
462
463        for _ in 1..queue_count {
464            let file = std::fs::OpenOptions::new()
465                .read(true)
466                .write(true)
467                .open(TUN_PATH)?;
468
469            let mut req = IfReq {
470                name: req0.name,
471                flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE | IFF_VNET_HDR,
472                _pad: [0; 22],
473            };
474
475            // SAFETY: req is a correctly-sized ifreq for TUNSETIFF; file is a freshly-opened /dev/net/tun.
476            let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
477            if rc != 0 {
478                return Err(DeviceError::Io(io::Error::last_os_error()));
479            }
480
481            let mut hdr_sz: libc::c_int = VNET_HDR_LEN as libc::c_int;
482            // SAFETY: file is a valid open /dev/net/tun fd. TUNSETVNETHDRSZ takes an int pointer.
483            let _ = unsafe {
484                libc::ioctl(
485                    file.as_raw_fd(),
486                    TUNSETVNETHDRSZ as IoctlReq,
487                    &raw mut hdr_sz,
488                )
489            };
490
491            apply_offload(file.as_raw_fd(), offload_mode)?;
492
493            let q = TunTap {
494                file,
495                kind,
496                name: actual_name.clone(),
497                vnet_hdr_len: Some(VNET_HDR_LEN),
498            };
499            q.set_nonblocking()?;
500            queues.push(q);
501        }
502
503        Ok(Some(queues))
504    }
505
506    fn open_multi_queue_plain(
507        kind: DeviceKind,
508        ifname: &[u8; libc::IFNAMSIZ],
509        queue_count: usize,
510    ) -> Result<Vec<TunTap>, DeviceError> {
511        let file0 = std::fs::OpenOptions::new()
512            .read(true)
513            .write(true)
514            .open(TUN_PATH)?;
515
516        let type_flag = match kind {
517            DeviceKind::Tun => IFF_TUN,
518            DeviceKind::Tap => IFF_TAP,
519        };
520        let mut req0 = IfReq {
521            name: *ifname,
522            flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE,
523            _pad: [0; 22],
524        };
525
526        // SAFETY: req0 is a correctly-sized ifreq for TUNSETIFF; file0 is a freshly-opened /dev/net/tun.
527        let rc = unsafe { libc::ioctl(file0.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req0) };
528        if rc != 0 {
529            return Err(DeviceError::Io(io::Error::last_os_error()));
530        }
531
532        bring_up(&req0.name)?;
533        let actual_name = decode_ifname(&req0.name)?;
534
535        let q0 = TunTap {
536            file: file0,
537            kind,
538            name: actual_name.clone(),
539            vnet_hdr_len: None,
540        };
541        q0.set_nonblocking()?;
542
543        let mut queues = Vec::with_capacity(queue_count);
544        queues.push(q0);
545
546        for _ in 1..queue_count {
547            let file = std::fs::OpenOptions::new()
548                .read(true)
549                .write(true)
550                .open(TUN_PATH)?;
551
552            let mut req = IfReq {
553                name: req0.name,
554                flags: type_flag | IFF_NO_PI | IFF_MULTI_QUEUE,
555                _pad: [0; 22],
556            };
557
558            // SAFETY: req is a correctly-sized ifreq for TUNSETIFF; file is a freshly-opened /dev/net/tun.
559            let rc = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETIFF as IoctlReq, &raw mut req) };
560            if rc != 0 {
561                return Err(DeviceError::Io(io::Error::last_os_error()));
562            }
563
564            let q = TunTap {
565                file,
566                kind,
567                name: actual_name.clone(),
568                vnet_hdr_len: None,
569            };
570            q.set_nonblocking()?;
571            queues.push(q);
572        }
573
574        Ok(queues)
575    }
576
577    /// The interface name.
578    pub fn name(&self) -> &str {
579        &self.name
580    }
581
582    /// The `virtio_net_hdr` prefix length (bytes) on every TUN read/write
583    /// when kernel GSO/GRO offload is active, or `None` for a plain device
584    /// (see [`TunTap::create`]'s `want_vnet_hdr` parameter).
585    pub fn vnet_hdr_len(&self) -> Option<usize> {
586        self.vnet_hdr_len
587    }
588
589    /// Return the raw file descriptor for this TUN/TAP device.
590    ///
591    /// The fd is bidirectional: `read` pulls frames injected by the kernel
592    /// (egress path for the tunnel), and `write` injects frames into the
593    /// kernel network stack (ingress path for the tunnel).
594    ///
595    /// The fd is valid as long as this `TunTap` value lives.
596    pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
597        self.file.as_raw_fd()
598    }
599
600    /// Set the TUN/TAP fd non-blocking via `fcntl(F_SETFL, O_NONBLOCK)`.
601    ///
602    /// Required before passing the fd to an `epoll`-based event loop that
603    /// drains with `MSG_DONTWAIT` / non-blocking `read`.
604    pub fn set_nonblocking(&self) -> Result<(), DeviceError> {
605        // SAFETY: `self.file` is a valid open fd owned by this `TunTap`
606        // instance, which outlives this call.  `fcntl(F_SETFL, O_NONBLOCK)`
607        // is a pure flag-set on the open-file description and cannot cause UB.
608        let rc = unsafe { libc::fcntl(self.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK) };
609        if rc < 0 {
610            Err(DeviceError::Io(io::Error::last_os_error()))
611        } else {
612            Ok(())
613        }
614    }
615
616    /// Set the MTU of this device. Requires `CAP_NET_ADMIN`.
617    pub fn set_mtu(&self, mtu: u32) -> Result<(), DeviceError> {
618        let ifname = encode_ifname(&self.name)?;
619        set_device_mtu(&ifname, mtu)
620    }
621}
622
623/// The read half of a split [`TunTap`].
624pub struct TunReader {
625    file: std::fs::File,
626}
627
628/// The write half of a split [`TunTap`].
629pub struct TunWriter {
630    file: std::fs::File,
631}
632
633impl TunReader {
634    /// Read one inner frame.
635    pub fn read_frame(&mut self, buf: &mut [u8]) -> io::Result<usize> {
636        use std::io::Read;
637        self.file.read(buf)
638    }
639
640    /// Return the raw fd for this reader half.
641    pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
642        self.file.as_raw_fd()
643    }
644}
645
646impl TunWriter {
647    /// Write one inner frame.
648    pub fn write_frame(&mut self, frame: &[u8]) -> io::Result<usize> {
649        use std::io::Write;
650        self.file.write(frame)
651    }
652
653    /// Return the raw fd for this writer half.
654    pub fn as_raw_fd(&self) -> std::os::fd::RawFd {
655        self.file.as_raw_fd()
656    }
657}
658
659impl TunTap {
660    /// Split into independent reader/writer halves backed by duplicated fds,
661    /// so one thread can read while another writes the same device.
662    pub fn split(self) -> Result<(TunReader, TunWriter), DeviceError> {
663        let raw = self.file.as_raw_fd();
664        // SAFETY: `raw` is a valid open fd owned by `self.file`; `dup` returns a new
665        // independent fd referring to the same TUN/TAP device, or -1 on error.
666        let dup = unsafe { libc::dup(raw) };
667        if dup < 0 {
668            return Err(DeviceError::Io(io::Error::last_os_error()));
669        }
670        // SAFETY: `dup` is a fresh, valid, exclusively-owned fd from `dup`.
671        let dup_file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(dup) });
672        let reader = TunReader { file: dup_file };
673        let writer = TunWriter { file: self.file };
674        Ok((reader, writer))
675    }
676}
677
678// NOTE: a TAP device yields raw Ethernet frames. MAC learning and L2 forwarding
679// (bridging frames between peers by destination MAC) belong to the data-plane
680// forwarding loop wired in M6, not to the device itself, which is a dumb fd.
681impl Device for TunTap {
682    fn kind(&self) -> DeviceKind {
683        self.kind
684    }
685    fn read_frame(&mut self, buf: &mut [u8]) -> io::Result<usize> {
686        use std::io::Read;
687        self.file.read(buf)
688    }
689    fn write_frame(&mut self, frame: &[u8]) -> io::Result<usize> {
690        use std::io::Write;
691        self.file.write(frame)
692    }
693}
694
695impl AsRawFd for TunTap {
696    fn as_raw_fd(&self) -> std::os::fd::RawFd {
697        self.file.as_raw_fd()
698    }
699}
700
701impl AsRawFd for TunReader {
702    fn as_raw_fd(&self) -> std::os::fd::RawFd {
703        self.file.as_raw_fd()
704    }
705}
706
707impl AsRawFd for TunWriter {
708    fn as_raw_fd(&self) -> std::os::fd::RawFd {
709        self.file.as_raw_fd()
710    }
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716
717    #[test]
718    fn device_kinds_are_distinct() {
719        assert_ne!(DeviceKind::Tun, DeviceKind::Tap);
720    }
721
722    #[test]
723    fn ifname_encodes_and_rejects_too_long() {
724        let enc = encode_ifname("yip0").unwrap();
725        assert_eq!(&enc[..4], b"yip0");
726        assert_eq!(enc[4], 0, "NUL-padded");
727        let long = "x".repeat(libc::IFNAMSIZ); // == IFNAMSIZ chars, no room for NUL
728        assert!(matches!(
729            encode_ifname(&long),
730            Err(DeviceError::NameTooLong)
731        ));
732    }
733
734    /// Returns true if we can create tunnel devices (root / CAP_NET_ADMIN).
735    fn can_create_devices() -> bool {
736        TunTap::create("yipcap0", DeviceKind::Tun, false)
737            .map(drop)
738            .is_ok()
739    }
740
741    #[test]
742    fn tun_create_roundtrips_a_write() {
743        if !can_create_devices() {
744            eprintln!("SKIP tun_create_roundtrips_a_write: needs CAP_NET_ADMIN (run under sudo)");
745            return;
746        }
747        let mut dev = TunTap::create("yiptun0", DeviceKind::Tun, false).unwrap();
748        assert_eq!(dev.kind(), DeviceKind::Tun);
749        assert_eq!(dev.name(), "yiptun0");
750        // Writing a minimal IPv4 packet to the device must not error (kernel accepts the inject).
751        let pkt = [
752            0x45u8, 0, 0, 20, 0, 0, 0, 0, 64, 17, 0, 0, 10, 9, 9, 1, 10, 9, 9, 2,
753        ];
754        let n = dev.write_frame(&pkt).unwrap();
755        assert_eq!(n, pkt.len());
756    }
757
758    #[test]
759    fn tap_create_reports_l2_kind() {
760        if !can_create_devices() {
761            eprintln!("SKIP tap_create_reports_l2_kind: needs CAP_NET_ADMIN (run under sudo)");
762            return;
763        }
764        let dev = TunTap::create("yiptap0", DeviceKind::Tap, false).unwrap();
765        assert_eq!(dev.kind(), DeviceKind::Tap);
766        assert_eq!(dev.name(), "yiptap0");
767    }
768
769    #[test]
770    fn split_yields_independent_reader_writer() {
771        if !can_create_devices() {
772            eprintln!("SKIP split_yields_independent_reader_writer: needs CAP_NET_ADMIN");
773            return;
774        }
775        let dev = TunTap::create("yipsplit0", DeviceKind::Tun, false).unwrap();
776        let (_reader, mut writer) = dev.split().unwrap();
777        // the writer half can still inject a frame
778        let pkt = [
779            0x45u8, 0, 0, 20, 0, 0, 0, 0, 64, 17, 0, 0, 10, 9, 9, 1, 10, 9, 9, 2,
780        ];
781        assert_eq!(writer.write_frame(&pkt).unwrap(), pkt.len());
782    }
783
784    #[test]
785    fn plain_create_reports_no_vnet_hdr() {
786        if !can_create_devices() {
787            eprintln!(
788                "SKIP plain_create_reports_no_vnet_hdr: needs CAP_NET_ADMIN (run under sudo)"
789            );
790            return;
791        }
792        let dev = TunTap::create("yipvnh0", DeviceKind::Tun, false).unwrap();
793        assert_eq!(
794            dev.vnet_hdr_len(),
795            None,
796            "want_vnet_hdr=false must never activate vnet_hdr framing"
797        );
798    }
799
800    #[test]
801    fn set_mtu_fails_on_too_long_name() {
802        let dummy_file = std::fs::File::open("/dev/null").unwrap();
803        let dev = TunTap {
804            file: dummy_file,
805            kind: DeviceKind::Tun,
806            name: "x".repeat(libc::IFNAMSIZ),
807            vnet_hdr_len: None,
808        };
809        assert!(matches!(dev.set_mtu(1500), Err(DeviceError::NameTooLong)));
810    }
811
812    #[test]
813    fn set_device_mtu_nonexistent_interface() {
814        let enc = encode_ifname("nonexistent0").unwrap();
815        let res = set_device_mtu(&enc, 1500);
816        assert!(matches!(res, Err(DeviceError::Io(_))));
817    }
818
819    #[test]
820    fn set_mtu_nonexistent_interface() {
821        let dummy_file = std::fs::File::open("/dev/null").unwrap();
822        let dev = TunTap {
823            file: dummy_file,
824            kind: DeviceKind::Tun,
825            name: "nonexistent0".to_owned(),
826            vnet_hdr_len: None,
827        };
828        assert!(matches!(dev.set_mtu(1500), Err(DeviceError::Io(_))));
829    }
830
831    #[test]
832    fn tun_set_mtu_succeeds_when_privileged() {
833        if !can_create_devices() {
834            eprintln!("SKIP tun_set_mtu_succeeds_when_privileged: needs CAP_NET_ADMIN");
835            return;
836        }
837        let dev = TunTap::create("yipmtu0", DeviceKind::Tun, false).unwrap();
838        assert!(dev.set_mtu(1400).is_ok());
839        assert!(dev.set_mtu(1500).is_ok());
840    }
841}