AkurAI Build
Menu

Public repository / 5c07c5e644744ad612da6efa3e039c5eb30df6f9

AkurAI-VPN / crates/akurai-sys/src/tun.rs

Latest change 739c4c06884e4cff0fcc99b9d72ca5da56a066a5 - feat(node): real Windows Wintun data plane + netsh setup + BCryptGenRandom RNG by Claude Code

//! Cross-platform TUN device — layer-3 packet I/O behind one small handle.
//!
//! [`TunDevice`] hides every OS difference behind three calls: [`TunDevice::recv`]
//! returns exactly one **bare IPv4/IPv6 packet** (no platform framing) and
//! [`TunDevice::send`] writes exactly one. `recv`/`send` take `&self`, so a single
//! device wrapped in an `Arc` can be read by one thread and written by another at
//! the same time — the kernel serializes the underlying read/write per fd.
//!
//! - **Linux** — open `/dev/net/tun`, bind it with one `ioctl(TUNSETIFF)` using
//!   `IFF_TUN | IFF_NO_PI` (so frames have NO header). `recv`/`send` are plain
//!   `File` read/write. The only `unsafe` is the raw syscall in [`crate::raw`].
//! - **macOS** — a REAL `utun` interface created with raw `extern "C"` calls into
//!   libSystem (always linked, never a crate dependency). utun frames carry a
//!   4-byte big-endian address-family header, which this module adds on `send` and
//!   strips on `recv`, so callers still see bare packets.
//! - **Windows** — a REAL Wintun interface: `wintun.dll` is loaded at runtime via
//!   `LoadLibraryW`/`GetProcAddress` (no crate dependency), an adapter + ring
//!   session are created, and `recv`/`send` shuttle packets through the ring.
//!   Wintun frames are already bare IP (no AF header, unlike utun).
//!
//! The interface name is exposed via [`TunDevice::name`] so the caller can
//! configure the right interface — the requested name on Linux and Windows, the
//! kernel-assigned `utunN` on macOS.

use std::io;

// ---------------------------------------------------------------------------
// Linux — the original /dev/net/tun seam, now wrapped in TunDevice.
// ---------------------------------------------------------------------------
#[cfg(target_os = "linux")]
mod platform {
    use std::fs::{File, OpenOptions};
    use std::io::{self, Read, Write};
    use std::os::unix::io::AsRawFd;

    /// Path to the TUN/TAP clone device.
    const TUN_CLONE: &str = "/dev/net/tun";

    /// A Linux layer-3 TUN interface (`IFF_TUN | IFF_NO_PI`): bare IPv4/IPv6
    /// packets, no packet-info header.
    pub struct TunDevice {
        file: File,
        name: String,
    }

    impl TunDevice {
        /// Create a layer-3 TUN named `name`. The interface starts **down** with no
        /// address; the caller brings it up (safe `ip` commands). Requires
        /// `CAP_NET_ADMIN`.
        ///
        /// # Errors
        /// Surfaces the OS error if `/dev/net/tun` cannot be opened (missing module,
        /// `EPERM`) or the `ioctl` fails (name in use/invalid). Never panics.
        pub fn create(name: &str) -> io::Result<TunDevice> {
            let file = OpenOptions::new().read(true).write(true).open(TUN_CLONE)?;
            crate::raw::tunsetiff(file.as_raw_fd(), name)?;
            Ok(TunDevice {
                file,
                name: name.to_string(),
            })
        }

        /// Read one bare packet. `&self`-safe to call concurrently with [`send`].
        ///
        /// [`send`]: TunDevice::send
        pub fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
            // `Read for &File` lets us read through a shared reference; the fd's
            // read/write are independently safe to call from two threads.
            let mut f: &File = &self.file;
            f.read(buf)
        }

        /// Write one bare packet. `&self`-safe to call concurrently with [`recv`].
        ///
        /// [`recv`]: TunDevice::recv
        pub fn send(&self, pkt: &[u8]) -> io::Result<usize> {
            let mut f: &File = &self.file;
            f.write(pkt)
        }

        /// The interface name (the requested name on Linux — the kernel honors it).
        pub fn name(&self) -> &str {
            &self.name
        }
    }
}

// ---------------------------------------------------------------------------
// macOS — a real utun interface via raw libSystem FFI (no crate dependency).
// ---------------------------------------------------------------------------
#[cfg(target_os = "macos")]
mod platform {
    //! macOS `utun` data plane.
    //!
    //! A utun interface is a `PF_SYSTEM` kernel-control socket: resolve the utun
    //! control id by name, `connect` to it (the kernel allocates `utunN`), then
    //! `read`/`write` frames. Each frame is prefixed with a 4-byte big-endian
    //! address-family word, which we add on send and strip on recv.

    use std::io;
    use std::os::raw::{c_char, c_int, c_uint, c_ulong, c_void};

    /// `socklen_t` on macOS (LP64): a 32-bit unsigned int.
    type Socklen = c_uint;

    // Kernel-control / utun constants (from <sys/sys_domain.h>, <net/if_utun.h>).
    const PF_SYSTEM: c_int = 32;
    const SOCK_DGRAM: c_int = 2;
    const SYSPROTO_CONTROL: c_int = 2;
    const AF_SYSTEM: u8 = 32;
    const AF_SYS_CONTROL: u16 = 2;
    /// `CTLIOCGINFO` = `_IOWR('N', 3, struct ctl_info)` (ctl_info is 100 bytes).
    const CTLIOCGINFO: c_ulong = 0xc064_4e03;
    /// `getsockopt` name for the kernel-assigned interface name (`utunN`).
    const UTUN_OPT_IFNAME: c_int = 2;
    /// The well-known utun kernel-control name.
    const UTUN_CONTROL_NAME: &[u8] = b"com.apple.net.utun_control";

    /// `struct ctl_info { u_int32_t ctl_id; char ctl_name[MAX_KCTL_NAME=96]; }`.
    #[repr(C)]
    struct CtlInfo {
        ctl_id: u32,
        ctl_name: [c_char; 96],
    }

    /// `struct sockaddr_ctl` — 32 bytes total.
    #[repr(C)]
    struct SockaddrCtl {
        sc_len: u8,
        sc_family: u8,
        ss_sysaddr: u16,
        sc_id: u32,
        sc_unit: u32,
        sc_reserved: [u32; 5],
    }

    extern "C" {
        fn socket(domain: c_int, ty: c_int, protocol: c_int) -> c_int;
        // ioctl is variadic in C; we always pass exactly one pointer argument.
        fn ioctl(fd: c_int, request: c_ulong, ...) -> c_int;
        fn connect(fd: c_int, addr: *const c_void, len: Socklen) -> c_int;
        fn getsockopt(
            fd: c_int,
            level: c_int,
            optname: c_int,
            optval: *mut c_void,
            optlen: *mut Socklen,
        ) -> c_int;
        fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize;
        fn write(fd: c_int, buf: *const c_void, count: usize) -> isize;
        fn close(fd: c_int) -> c_int;
    }

    /// Closes a raw fd on drop unless disarmed via [`FdGuard::take`]. Guarantees the
    /// socket is released if any step of `create` fails after the `socket` call.
    struct FdGuard(c_int);

    impl Drop for FdGuard {
        fn drop(&mut self) {
            if self.0 >= 0 {
                // SAFETY: `self.0` is an fd we opened and still own; closed once.
                unsafe { close(self.0) };
            }
        }
    }

    impl FdGuard {
        /// Take ownership of the fd, disarming the guard so it won't close it.
        fn take(mut self) -> c_int {
            let fd = self.0;
            self.0 = -1;
            fd
        }
    }

    /// A macOS `utun` layer-3 interface. Owns one fd (closed on drop) plus the
    /// kernel-assigned name (`utunN`). The fd is a plain `i32`, so the handle is
    /// `Send + Sync`; concurrent `recv`/`send` map to independent `read`/`write`
    /// syscalls the kernel serializes per fd.
    pub struct TunDevice {
        fd: c_int,
        name: String,
    }

    impl TunDevice {
        /// Create a real `utun` interface. `name` is ignored — the kernel assigns
        /// `utunN`; the chosen name is available via [`TunDevice::name`].
        ///
        /// # Errors
        /// Surfaces the OS error from `socket`/`ioctl`/`connect` (e.g. `EPERM`
        /// without root). Never panics.
        pub fn create(_name: &str) -> io::Result<TunDevice> {
            // 1. Open a kernel-control socket.
            // SAFETY: a plain syscall with constant args; returns < 0 on error.
            let fd = unsafe { socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL) };
            if fd < 0 {
                return Err(io::Error::last_os_error());
            }
            let guard = FdGuard(fd);

            // 2. Resolve the utun control id by name into ctl_info.ctl_id.
            let mut info = CtlInfo {
                ctl_id: 0,
                ctl_name: [0; 96],
            };
            for (slot, &b) in info.ctl_name.iter_mut().zip(UTUN_CONTROL_NAME) {
                *slot = b as c_char;
            }
            // SAFETY: `info` is a valid, correctly-sized ctl_info that outlives the
            // call; the kernel reads ctl_name and writes ctl_id, both in-bounds.
            let rc = unsafe { ioctl(fd, CTLIOCGINFO, &mut info as *mut CtlInfo) };
            if rc < 0 {
                return Err(io::Error::last_os_error());
            }

            // 3. Connect to the control; sc_unit = 0 => kernel picks utunN.
            let addr = SockaddrCtl {
                sc_len: core::mem::size_of::<SockaddrCtl>() as u8,
                sc_family: AF_SYSTEM,
                ss_sysaddr: AF_SYS_CONTROL,
                sc_id: info.ctl_id,
                sc_unit: 0,
                sc_reserved: [0; 5],
            };
            // SAFETY: `addr` is a valid sockaddr_ctl of exactly `sc_len` bytes.
            let rc = unsafe {
                connect(
                    fd,
                    &addr as *const SockaddrCtl as *const c_void,
                    core::mem::size_of::<SockaddrCtl>() as Socklen,
                )
            };
            if rc < 0 {
                return Err(io::Error::last_os_error());
            }

            // 4. Learn the kernel-assigned interface name (utunN).
            let name = ifname(fd).unwrap_or_default();

            Ok(TunDevice {
                fd: guard.take(),
                name,
            })
        }

        /// Read one bare IP packet, stripping the 4-byte utun AF header.
        /// `&self`-safe to call concurrently with [`send`](TunDevice::send).
        pub fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
            // utun delivers one whole frame per read: a 4-byte big-endian AF header
            // followed by the IP packet. Read into a temp, then hand back [4..].
            let mut frame = [0u8; 4 + 4096];
            let want = core::cmp::min(buf.len() + 4, frame.len());
            // SAFETY: writing at most `want` bytes into `frame`, which is `want`-big.
            let n = unsafe { read(self.fd, frame.as_mut_ptr() as *mut c_void, want) };
            if n < 0 {
                return Err(io::Error::last_os_error());
            }
            let n = n as usize;
            if n == 0 {
                return Ok(0); // EOF — device closed.
            }
            if n < 4 {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    "utun frame shorter than its 4-byte header",
                ));
            }
            let payload = n - 4;
            buf[..payload].copy_from_slice(&frame[4..n]);
            Ok(payload)
        }

        /// Write one bare IP packet, prepending the 4-byte utun AF header and
        /// writing the whole frame in ONE `write`. `&self`-safe to call
        /// concurrently with [`recv`](TunDevice::recv).
        pub fn send(&self, pkt: &[u8]) -> io::Result<usize> {
            // Big-endian address family: AF_INET (2) for IPv4, AF_INET6 (30) for v6,
            // chosen from the IP version nibble. Header bytes [0..3] stay zero.
            const AF_INET: u8 = 2;
            const AF_INET6: u8 = 30;
            let mut frame = [0u8; 4 + 4096];
            if pkt.len() > frame.len() - 4 {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    "packet larger than the utun frame buffer",
                ));
            }
            frame[3] = if !pkt.is_empty() && (pkt[0] >> 4) == 6 {
                AF_INET6
            } else {
                AF_INET
            };
            frame[4..4 + pkt.len()].copy_from_slice(pkt);
            let total = 4 + pkt.len();
            // SAFETY: writing exactly `total` bytes from `frame`, which is `total`-big.
            let n = unsafe { write(self.fd, frame.as_ptr() as *const c_void, total) };
            if n < 0 {
                return Err(io::Error::last_os_error());
            }
            // The kernel counts the 4-byte header; report inner-packet bytes only.
            Ok((n as usize).saturating_sub(4))
        }

        /// The kernel-assigned interface name, e.g. `utun4`.
        pub fn name(&self) -> &str {
            &self.name
        }
    }

    impl Drop for TunDevice {
        fn drop(&mut self) {
            if self.fd >= 0 {
                // SAFETY: we own this fd and close it exactly once.
                unsafe { close(self.fd) };
            }
        }
    }

    /// Fetch the kernel-assigned interface name (e.g. `utun4`) for a utun fd via
    /// `getsockopt(SYSPROTO_CONTROL, UTUN_OPT_IFNAME)`.
    fn ifname(fd: c_int) -> Option<String> {
        let mut buf = [0u8; 64];
        let mut len: Socklen = buf.len() as Socklen;
        // SAFETY: getsockopt writes up to `len` bytes into `buf` and updates `len`.
        let rc = unsafe {
            getsockopt(
                fd,
                SYSPROTO_CONTROL,
                UTUN_OPT_IFNAME,
                buf.as_mut_ptr() as *mut c_void,
                &mut len,
            )
        };
        if rc < 0 {
            return None;
        }
        let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
        Some(String::from_utf8_lossy(&buf[..end]).into_owned())
    }
}

// ---------------------------------------------------------------------------
// Windows — a real Wintun interface via runtime-loaded `wintun.dll` (no crate).
// ---------------------------------------------------------------------------
#[cfg(target_os = "windows")]
mod platform {
    //! Windows `utun`-equivalent data plane on the Wintun driver.
    //!
    //! IMPLEMENTED. `wintun.dll` is loaded at runtime — no crate dependency, no
    //! import-time link to wintun — preserving the zero-dependency promise:
    //!
    //! 1. `LoadLibraryW(L"wintun.dll")` then `GetProcAddress` for every export.
    //! 2. `WintunCreateAdapter(name, "AkurAI", NULL)` to make the adapter.
    //! 3. `WintunStartSession(adapter, 4 MiB)` to get a ring-buffer session.
    //! 4. `recv` -> `WintunReceivePacket` (+ `WintunReleaseReceivePacket`),
    //!    blocking on `WintunGetReadWaitEvent` via `WaitForSingleObject` when the
    //!    ring is empty; `send` -> `WintunAllocateSendPacket` + memcpy +
    //!    `WintunSendPacket`. Wintun packets are already bare IP — no 4-byte AF
    //!    header (unlike utun), so packets pass straight through.
    //! 5. Drop -> `WintunEndSession` + `WintunCloseAdapter`.
    //!
    //! Only `kernel32` (`LoadLibraryW`/`GetProcAddress`/`GetLastError`/
    //! `WaitForSingleObject`) is statically imported — that is the bootstrap that
    //! cannot itself be dynamically loaded. Everything Wintun is resolved by name.

    use core::ffi::c_void;
    use std::io;

    /// Wintun ring capacity: 4 MiB (must be a power of two between 128 KiB and 64
    /// MiB per the Wintun API).
    const WINTUN_RING_CAPACITY: u32 = 0x0040_0000;
    /// `WaitForSingleObject` — wait with no timeout.
    const INFINITE: u32 = 0xFFFF_FFFF;
    /// `GetLastError`: the receive ring is currently empty (block on the event).
    const ERROR_NO_MORE_ITEMS: u32 = 259;
    /// `GetLastError`: the send ring is full (back-pressure → `WouldBlock`).
    const ERROR_BUFFER_OVERFLOW: u32 = 111;

    // --- Wintun exported function-pointer types (all `__stdcall` = extern system).
    /// Opaque `WINTUN_ADAPTER_HANDLE` / `WINTUN_SESSION_HANDLE` / `HANDLE`.
    type Handle = *mut c_void;
    /// `WINTUN_ADAPTER_HANDLE WintunCreateAdapter(LPCWSTR, LPCWSTR, const GUID*)`.
    type WintunCreateAdapterFn =
        unsafe extern "system" fn(*const u16, *const u16, *const c_void) -> Handle;
    /// `void WintunCloseAdapter(WINTUN_ADAPTER_HANDLE)`.
    type WintunCloseAdapterFn = unsafe extern "system" fn(Handle);
    /// `WINTUN_SESSION_HANDLE WintunStartSession(WINTUN_ADAPTER_HANDLE, DWORD)`.
    type WintunStartSessionFn = unsafe extern "system" fn(Handle, u32) -> Handle;
    /// `void WintunEndSession(WINTUN_SESSION_HANDLE)`.
    type WintunEndSessionFn = unsafe extern "system" fn(Handle);
    /// `BYTE* WintunAllocateSendPacket(WINTUN_SESSION_HANDLE, DWORD)`.
    type WintunAllocateSendPacketFn = unsafe extern "system" fn(Handle, u32) -> *mut u8;
    /// `void WintunSendPacket(WINTUN_SESSION_HANDLE, const BYTE*)`.
    type WintunSendPacketFn = unsafe extern "system" fn(Handle, *const u8);
    /// `BYTE* WintunReceivePacket(WINTUN_SESSION_HANDLE, DWORD* PacketSize)`.
    type WintunReceivePacketFn = unsafe extern "system" fn(Handle, *mut u32) -> *mut u8;
    /// `void WintunReleaseReceivePacket(WINTUN_SESSION_HANDLE, const BYTE*)`.
    type WintunReleaseReceivePacketFn = unsafe extern "system" fn(Handle, *const u8);
    /// `HANDLE WintunGetReadWaitEvent(WINTUN_SESSION_HANDLE)`.
    type WintunGetReadWaitEventFn = unsafe extern "system" fn(Handle) -> Handle;

    // kernel32 — the irreducible static imports. `LoadLibraryW`/`GetProcAddress`
    // cannot themselves be dynamically loaded (chicken-and-egg), so they, plus the
    // error/wait helpers, are linked directly. Everything Wintun goes through them.
    #[link(name = "kernel32")]
    extern "system" {
        fn LoadLibraryW(lp_lib_file_name: *const u16) -> *mut c_void;
        fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const u8) -> *mut c_void;
        fn GetLastError() -> u32;
        fn WaitForSingleObject(h_handle: *mut c_void, dw_milliseconds: u32) -> u32;
    }

    /// The resolved Wintun entry points for one loaded `wintun.dll`.
    struct Wintun {
        create_adapter: WintunCreateAdapterFn,
        close_adapter: WintunCloseAdapterFn,
        start_session: WintunStartSessionFn,
        end_session: WintunEndSessionFn,
        allocate_send_packet: WintunAllocateSendPacketFn,
        send_packet: WintunSendPacketFn,
        receive_packet: WintunReceivePacketFn,
        release_receive_packet: WintunReleaseReceivePacketFn,
        get_read_wait_event: WintunGetReadWaitEventFn,
    }

    /// Resolve one Wintun export by name and transmute it to its typed fn pointer,
    /// returning `Unsupported` from the enclosing function if the export is absent
    /// (an old or incomplete `wintun.dll`). Must be expanded inside an `unsafe`
    /// block — `GetProcAddress` and `transmute` are unsafe.
    macro_rules! load_sym {
        ($module:expr, $name:literal, $ty:ty) => {{
            let sym = GetProcAddress($module, concat!($name, "\0").as_ptr());
            if sym.is_null() {
                return Err(io::Error::new(
                    io::ErrorKind::Unsupported,
                    concat!("wintun.dll is missing export ", $name),
                ));
            }
            core::mem::transmute::<*mut c_void, $ty>(sym)
        }};
    }

    /// A Windows layer-3 interface backed by a Wintun ring session. Holds the raw
    /// adapter + session handles, the read-ready event, the requested name, and the
    /// resolved Wintun entry points.
    pub struct TunDevice {
        adapter: Handle,
        session: Handle,
        read_wait: Handle,
        name: String,
        api: Wintun,
    }

    // SAFETY: a Wintun session is documented as thread-safe for concurrent
    // `WintunSendPacket` and `WintunReceivePacket` from different threads, so
    // sharing one `TunDevice` across the send and receive pump threads (via `Arc`)
    // is sound. The stored handles are process-stable pointers with no thread
    // affinity, and the resolved fn pointers are immutable after `create`.
    unsafe impl Send for TunDevice {}
    unsafe impl Sync for TunDevice {}

    /// UTF-16, NUL-terminated — the form `LoadLibraryW`/`WintunCreateAdapter` want.
    fn to_wide(s: &str) -> Vec<u16> {
        s.encode_utf16().chain(core::iter::once(0)).collect()
    }

    impl TunDevice {
        /// Create a real Wintun adapter named `name` (with tunnel type `AkurAI`) and
        /// start a 4 MiB ring session. Unlike macOS, Wintun honors the requested
        /// name, so [`TunDevice::name`] returns `name` and the caller configures
        /// that interface.
        ///
        /// # Errors
        /// Returns `ErrorKind::Unsupported` if `wintun.dll` cannot be loaded (driver
        /// not installed) or is missing an export; otherwise surfaces the Win32
        /// error from `WintunCreateAdapter`/`WintunStartSession`. Never panics.
        pub fn create(name: &str) -> io::Result<TunDevice> {
            let name_w = to_wide(name);
            let tunnel_w = to_wide("AkurAI");
            let dll_w = to_wide("wintun.dll");

            // SAFETY: every call below is FFI into kernel32 / wintun.dll matching the
            // documented signatures. The wide-string buffers outlive their calls,
            // and every returned handle is null-checked before use.
            unsafe {
                let module = LoadLibraryW(dll_w.as_ptr());
                if module.is_null() {
                    return Err(io::Error::new(
                        io::ErrorKind::Unsupported,
                        "wintun.dll not found — install the Wintun driver",
                    ));
                }
                let api = Wintun {
                    create_adapter: load_sym!(module, "WintunCreateAdapter", WintunCreateAdapterFn),
                    close_adapter: load_sym!(module, "WintunCloseAdapter", WintunCloseAdapterFn),
                    start_session: load_sym!(module, "WintunStartSession", WintunStartSessionFn),
                    end_session: load_sym!(module, "WintunEndSession", WintunEndSessionFn),
                    allocate_send_packet: load_sym!(
                        module,
                        "WintunAllocateSendPacket",
                        WintunAllocateSendPacketFn
                    ),
                    send_packet: load_sym!(module, "WintunSendPacket", WintunSendPacketFn),
                    receive_packet: load_sym!(module, "WintunReceivePacket", WintunReceivePacketFn),
                    release_receive_packet: load_sym!(
                        module,
                        "WintunReleaseReceivePacket",
                        WintunReleaseReceivePacketFn
                    ),
                    get_read_wait_event: load_sym!(
                        module,
                        "WintunGetReadWaitEvent",
                        WintunGetReadWaitEventFn
                    ),
                };

                // NULL requested GUID ⇒ Wintun derives a stable GUID from the name.
                let adapter =
                    (api.create_adapter)(name_w.as_ptr(), tunnel_w.as_ptr(), core::ptr::null());
                if adapter.is_null() {
                    return Err(io::Error::last_os_error());
                }
                let session = (api.start_session)(adapter, WINTUN_RING_CAPACITY);
                if session.is_null() {
                    let err = io::Error::last_os_error();
                    (api.close_adapter)(adapter);
                    return Err(err);
                }
                let read_wait = (api.get_read_wait_event)(session);

                Ok(TunDevice {
                    adapter,
                    session,
                    read_wait,
                    name: name.to_string(),
                    api,
                })
            }
        }

        /// Read one bare IP packet from the ring. Blocks on the read-ready event
        /// when the ring is empty, so this never busy-spins. `&self`-safe to call
        /// concurrently with [`send`](TunDevice::send).
        pub fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
            loop {
                let mut size: u32 = 0;
                // SAFETY: `self.session` is a live Wintun session. `receive_packet`
                // returns either a pointer to `size` readable bytes (released below)
                // or NULL with a reason in `GetLastError`.
                let pkt = unsafe { (self.api.receive_packet)(self.session, &mut size) };
                if !pkt.is_null() {
                    let n = (size as usize).min(buf.len());
                    // SAFETY: `pkt` is valid for `size` bytes until we release it; we
                    // copy `n <= size` into the caller's buffer (no overlap), then
                    // hand the ring slot straight back.
                    unsafe {
                        core::ptr::copy_nonoverlapping(pkt, buf.as_mut_ptr(), n);
                        (self.api.release_receive_packet)(self.session, pkt);
                    }
                    return Ok(n);
                }
                // SAFETY: kernel32 call, no pointer arguments.
                let err = unsafe { GetLastError() };
                if err == ERROR_NO_MORE_ITEMS {
                    // SAFETY: `read_wait` is this session's read-ready event handle.
                    unsafe { WaitForSingleObject(self.read_wait, INFINITE) };
                    continue;
                }
                return Err(io::Error::from_raw_os_error(err as i32));
            }
        }

        /// Write one bare IP packet into the send ring. Returns `WouldBlock` if the
        /// ring is full. `&self`-safe to call concurrently with
        /// [`recv`](TunDevice::recv).
        pub fn send(&self, pkt: &[u8]) -> io::Result<usize> {
            if pkt.is_empty() {
                return Ok(0);
            }
            // SAFETY: `self.session` is live; `allocate_send_packet` returns a
            // writable buffer of exactly `pkt.len()` bytes, or NULL with a reason in
            // `GetLastError`.
            let slot = unsafe { (self.api.allocate_send_packet)(self.session, pkt.len() as u32) };
            if slot.is_null() {
                // SAFETY: kernel32 call, no pointer arguments.
                let err = unsafe { GetLastError() };
                if err == ERROR_BUFFER_OVERFLOW {
                    return Err(io::Error::new(
                        io::ErrorKind::WouldBlock,
                        "wintun send ring full",
                    ));
                }
                return Err(io::Error::from_raw_os_error(err as i32));
            }
            // SAFETY: `slot` is `pkt.len()` writable bytes we own until handed to the
            // ring by `send_packet`; source and destination do not overlap.
            unsafe {
                core::ptr::copy_nonoverlapping(pkt.as_ptr(), slot, pkt.len());
                (self.api.send_packet)(self.session, slot);
            }
            Ok(pkt.len())
        }

        /// The interface name — the requested name (Wintun honors it).
        pub fn name(&self) -> &str {
            &self.name
        }
    }

    impl Drop for TunDevice {
        fn drop(&mut self) {
            // SAFETY: `session`/`adapter` came from `WintunStartSession`/
            // `WintunCreateAdapter` and are still owned here; each is ended/closed
            // exactly once, session before adapter (Wintun's required order).
            unsafe {
                if !self.session.is_null() {
                    (self.api.end_session)(self.session);
                }
                if !self.adapter.is_null() {
                    (self.api.close_adapter)(self.adapter);
                }
            }
        }
    }
}

pub use platform::TunDevice;

/// Create a layer-3 TUN interface named `name` and return a cross-platform handle.
///
/// On Linux and Windows the requested `name` is honored; on macOS the kernel
/// assigns `utunN` (read it back via [`TunDevice::name`]). On Windows a missing
/// `wintun.dll` surfaces as `ErrorKind::Unsupported`.
///
/// # Errors
/// Surfaces the underlying OS error. Never panics.
pub fn create(name: &str) -> io::Result<TunDevice> {
    TunDevice::create(name)
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
    use std::fs::OpenOptions;
    use std::io;

    #[test]
    fn rejects_empty_and_overlong_names() {
        // The ioctl seam validates the name before touching the kernel; an empty or
        // >15-byte name is an InvalidInput error, never a panic.
        assert_eq!(
            crate::raw::tunsetiff(-1, "").unwrap_err().kind(),
            io::ErrorKind::InvalidInput
        );
        assert_eq!(
            crate::raw::tunsetiff(-1, "a_very_long_iface_name")
                .unwrap_err()
                .kind(),
            io::ErrorKind::InvalidInput
        );
    }

    #[test]
    fn missing_device_is_an_error_not_a_panic() {
        // Opening a non-existent clone device must surface as an io::Error.
        let r = OpenOptions::new()
            .read(true)
            .write(true)
            .open("/dev/net/tun-does-not-exist");
        assert!(r.is_err());
    }
}