Overview • Platforms • Installation • Errors • Opening • Privileges • Offload
The cross-platform TUN/TAP backend of
Tunnel Lattice, built on
tun-rs (minimum and verified version
2.8.11). It implements tunnel-lattice-platform's provider traits and turns
every tun-rs/OS error into a typed tunnel_lattice_core::Error.
Not used directly: the
tunnel-latticefacade selects this backend by default through itstun-rsfeature. Depend on it directly only to drive the provider traits without the facade.
What it adds on top of tun-rs:
- Typed errors with the same meaning on every OS (
Disconnected,DriverUnavailable,AlreadyExists,BufferTooSmall, ...). - Device removal always ends
recv, including the Linux Tokio and async macOS TAP cases wheretun-rs's own wait never returns. - No truncation: an oversize packet is reported, not cut.
- Honest open: names the OS would not honor exactly are rejected up front, and an existing interface is never adopted and then destroyed.
- Linux TUN segmentation offload with its own header parsing,
segmentation, and coalescing, plus
send_batchon every OS.
The rules every backend shares (name and open checks, the apply step
order, the recv_batch drain, the Linux errno table, and the offload codec
and engine) are not private to this crate: they come from the non-default
backend (and, on Linux targets, offload) support modules of
tunnel-lattice-model and tunnel-lattice-platform. This crate keeps only
the tun-rs calls, its error classifiers, and the trait implementations.
Main surface:
TunRsBackend(#[non_exhaustive]; build withnew()/default()):DeviceProvider::openand a host-levelCapabilityProvider.TunRsDevice:PacketIo(recv,recv_batch,send,send_batch),DeviceObserver,DeviceMutator(MTU, TAP MAC, admin state),CapabilityProvider, and with an async featureAsyncPacketIo(Capability::NATIVE_ASYNC). On Linux only,PersistentDeviceandMultiQueueProvider; elsewhere they are not implemented at all.
Tested in CI on real devices (TUN and TAP, sync, Tokio, async-io):
- Linux —
/dev/net/tun; persistence, multi-queue, TUN offload. - Windows — TUN via Wintun (
wintun.dll), TAP via tap-windows6. - macOS — TUN via
utun, TAP viafethpairs and BPF.
Admin state is read from the host on every snapshot; Windows applies a
change asynchronously, so a snapshot right after apply may lag briefly.
Host-level capabilities (no device opened, no privilege needed):
DEVICE_MUTATION always; PERSISTENT_DEVICES and MULTI_QUEUE on Linux;
NATIVE_ASYNC with an async feature; TAP_DEVICES on Linux and macOS, and
on Windows only when the tap-windows6 driver is installed (detected once per
process, advisory only). MAC_MUTATION (TAP on Linux and macOS) and
SEGMENTATION_OFFLOAD are reported only by an open handle.
use tunnel_lattice_backend_tunrs::TunRsBackend;
use tunnel_lattice_platform::{Capability, CapabilityProvider};
let host = TunRsBackend::new().capabilities();
if host.contains(Capability::TAP_DEVICES) {
// a TAP device can be requested on this host
}[dependencies]
tunnel-lattice-backend-tunrs = "0.6"
# or, with the native async path (mutually exclusive):
tunnel-lattice-backend-tunrs = { version = "0.6", features = ["tokio"] }
tunnel-lattice-backend-tunrs = { version = "0.6", features = ["async-io"] }async-io and tokio select tun-rs's two async backends; enabling both
is a compile error from tun-rs itself, so --all-features is not valid.
- Every
io::Erroris mapped byio::ErrorKindfirst:PermissionDenied,NotFound,AlreadyExists,Unsupported, andBrokenPipe/UnexpectedEof/NotConnected→Disconnected. Anything else becomesPlatform(PlatformErrorCode), tagged by OS, orUnknownwhen there is no OS code (never a fabricated0). recvnever truncates: an oversize packet is discarded asBufferTooSmalland the device stays usable.EINTRand a transient empty macOS TAP read are retried internally.recv_batchon Linux waits for the first packet likerecv, then drains what is already queued without waiting again and without togglingO_NONBLOCK(preadv2withRWF_NOWAITin the blocking build). An empty queue ends the batch quietly,EINTRrepeats the read, and an offloadEINVALor invalid frame is dropped; any other error after the first packet ends the batch and is left for the next call. A packet after the first that is too long for its buffer makes the nextrecv/recv_batchon that handle returnBufferTooSmallfirst. A dropped asyncrecv_batchhas received nothing. Windows and macOS return one packet per call.- Deleting a Linux device, or the peer
fethof a macOS TAP device, ends a waitingrecvwithDisconnectedin every feature set. A device that is down or disabled reportsInvalidState, with two exceptions: a down Linux device makesrecvwait instead, and a down macOS device still acceptssend. ApplyingDesiredAdminState::Upon the same handle recovers it, except for a Windows adapter disabled outside this crate. DriverUnavailablecomes only fromopen: a missingwintun.dllor tap-windows6 driver on Windows, or a missingtunmodule on Linux.
Per-OS rules, native codes, and the retry details are in ARCHITECTURE.md, "Error model" and the API reference.
- A name or MTU the OS cannot honor exactly is rejected with
InvalidStatebefore any native call: Linux names are at most 15 bytes with no%; macOS TAP names arefeth<N>(N ≤ 32767) and TUN namesutun<N>; Windows names are at most 255 UTF-16 units; MTU is at mostu16::MAX. Leave the name unset to let the OS choose. opennever adopts an existing interface and then deletes it. An existing name givesAlreadyExists, except two documented attach cases that are never deleted on drop: a Linux persistent or multi-queue device of the same kind and setting, and an existing Wintun adapter on Windows. A WindowsTunopen fails withPlatform(Windows(1247))while another handle holds that Wintun adapter's session (the other handle keeps working), and withPlatform(Windows(code))when the existing adapter of that name is not a Wintun adapter.opendoes not report whether it attached or created. Joining another user's Linux multi-queue device requiresCAP_NET_ADMIN.DeviceConfig::with_macsets a TAP MAC address at open on every OS; if it was not applied,openreturnsUnsupportedand removes the device.
- Opening needs
CAP_NET_ADMIN(Linux), Administrator (Windows), or root (macOS); without itopenreportsError::PermissionDenied. - Windows TUN loads
wintun.dllat runtime (not linked or vendored): ship it from wintun.net next to the executable or onPATH. Windows TAP needs the tap-windows6 driver (tap0901) staged, e.g.pnputil /add-driver OemVista.inf; adapters are created and removed byopenand drop. - With
tokio,openmust run inside an entered Tokio runtime, and the blockingPacketIo::recv/sendneed a multi-threaded runtime: oncurrent_threadthe first blocking call hangs forever. Never call them from async code (Tokio panics;async-ioparks the executor); useAsyncPacketIothere, which works on any runtime flavor. - To detect device removal, a Linux handle with
tokio, and a macOS TAP handle with either async feature, holds one extra file descriptor.
PersistentDevice::persist/unpersistset and clearIFF_PERSIST(idempotent, from any queue). A later process re-attaches by opening the same name, kind, and multi-queue setting.DeviceConfig::with_multi_queue(true)enablesadditional_queue, an independent kernel-scheduled queue; without it the call returnsError::Unsupported. Sharing a handle across threads needs no extra queue:recv/sendtake&self.
DeviceConfig::with_offload(true)is a request, off by default, ignored without error on Windows, macOS, and TAP.Capability::SEGMENTATION_OFFLOADon the open handle says whether this queue really uses offload; the backend reads the queue's real framing back after every open.recvreturns one IP packet per call: super-packets (up to 64 KiB) are staged in a per-queue buffer and split with lengths and checksums completed. A dropped asyncrecvloses no segment.recv_batchreturns the segments of one or more super-packets in one call.send_batchsends at most 128 packets per call and coalesces adjacent packets of one TCP flow (or UDP flow, when the kernel supports it) into one write without copying payload; a refused super-packet is resent packet by packet. The prefix contract (Ok(n)/Err= nothing sent) holds.- Device-wide side effect: the kernel's offload setting covers every queue and process; any later open without offload turns it off again, and nothing restores it on drop.
- Offload needs no privilege beyond opening the device. A big-endian host whose device was switched to little-endian headers by another program is not supported (and not detected).
Details: ARCHITECTURE.md, "Segmentation offload".
- API reference: docs.rs/tunnel-lattice-backend-tunrs
- Design and backend replacement plan: ARCHITECTURE.md
- Facade:
tunnel-lattice
Licensed under the Mozilla Public License 2.0.