Skip to main content

tunnel_lattice/
lib.rs

1//! Cross-platform Rust library for TUN/TAP tunnel interfaces, designed to
2//! compose with the rest of the Lattice networking stack.
3//!
4//! Start with [`Tunnel::connect`] (available with the default `tun-rs`
5//! feature) to open a device:
6//!
7//! ```no_run
8//! use tunnel_lattice::{DeviceConfig, DeviceKind, Result, Tunnel};
9//!
10//! fn main() -> Result<()> {
11//!     let tunnel = Tunnel::connect();
12//!     let device = tunnel.open(DeviceConfig::new(DeviceKind::Tun).with_mtu(1500))?;
13//!     let mut buf = vec![0u8; 1500];
14//!     let len = device.recv(&mut buf)?;
15//!     println!("{} bytes", len);
16//!     Ok(())
17//! }
18//! ```
19//!
20//! This crate carries no OS-addressing responsibility — it creates and
21//! configures the virtual interface and transfers packets on it; IP address
22//! assignment on the resulting interface is `net-lattice`'s job (see the
23//! `net-lattice` crate in the sibling Lattice ecosystem).
24//!
25//! ## Feature flags
26//!
27//! - `tun-rs` (default): selects `tunnel-lattice-backend-tunrs`,
28//!   implemented on top of the cross-platform `tun-rs` crate. This is a
29//!   Cargo feature rather than a `target_os` cfg gate so a future
30//!   alternative backend can sit alongside it instead of replacing it — see
31//!   the workspace `ARCHITECTURE.md`, "Backend replacement plan."
32//! - `async-io`/`tokio`: mutually exclusive, matching `tun-rs`'s own two
33//!   async backends (enabling both is a compile error). Either one adds
34//!   `Handle::packet_stream`, a `futures::Stream` of received packets. Uses
35//!   a backend's native async I/O path when it reports
36//!   `Capability::NATIVE_ASYNC`; otherwise falls back to
37//!   `tunnel-lattice-async`'s thread-based adapter. No async runtime is
38//!   forced on a caller that enables neither feature. `packet_stream` is
39//!   referenced here as plain text, not an intra-doc link, because it only
40//!   exists under these features and this crate's default `cargo doc`
41//!   build (no features beyond `tun-rs`) cannot resolve it.
42
43#![warn(missing_docs)]
44
45pub use tunnel_lattice_core::{Error, Result};
46pub use tunnel_lattice_model::{
47    AdminState, DesiredAdminState, Device, DeviceConfig, DeviceConfigPatch, DeviceId, DeviceKind,
48};
49#[cfg(feature = "async")]
50use tunnel_lattice_platform::AsyncPacketIo;
51pub use tunnel_lattice_platform::{
52    Capability, CapabilityProvider, MultiQueueProvider, PersistentDevice,
53};
54use tunnel_lattice_platform::{DeviceMutator, DeviceObserver, DeviceProvider, PacketIo};
55
56/// An open device handle bound to this facade's concrete model types.
57///
58/// A single named bound for what `Tunnel::open` accepts and `Handle` wraps,
59/// instead of repeating the same four-trait list on both `impl` blocks —
60/// blanket-implemented for every type that satisfies it, so no backend ever
61/// implements this trait by name.
62pub trait ConnectedDevice:
63    PacketIo
64    + DeviceObserver<Device = Device>
65    + DeviceMutator<DeviceConfigPatch = DeviceConfigPatch>
66    + CapabilityProvider
67{
68}
69
70impl<T> ConnectedDevice for T where
71    T: PacketIo
72        + DeviceObserver<Device = Device>
73        + DeviceMutator<DeviceConfigPatch = DeviceConfigPatch>
74        + CapabilityProvider
75{
76}
77
78/// A connected backend for creating and operating TUN/TAP devices.
79///
80/// Generic over the backend the same way `net_lattice::Lattice<Backend>`
81/// is: `tunnel_lattice_platform::DeviceProvider`'s associated types are
82/// bound to the concrete `tunnel_lattice_model` types here, at the facade
83/// layer, not inside `tunnel-lattice-platform` itself.
84pub struct Tunnel<B> {
85    backend: B,
86}
87
88impl<B> Tunnel<B>
89where
90    B: DeviceProvider<DeviceConfig = DeviceConfig>,
91    B::Device: ConnectedDevice,
92{
93    /// Opens a new device matching `config`.
94    pub fn open(&self, config: DeviceConfig) -> Result<Handle<B::Device>> {
95        Ok(Handle {
96            device: std::sync::Arc::new(self.backend.open(config)?),
97        })
98    }
99}
100
101#[cfg(feature = "tun-rs")]
102impl Tunnel<tunnel_lattice_backend_tunrs::TunRsBackend> {
103    /// Connects the default `tun-rs`-backed backend.
104    ///
105    /// Stateless and infallible: `tun-rs` has no persistent connection step
106    /// analogous to `net_lattice::Lattice::connect`'s Netlink/WFP/
107    /// route-socket handshake — the privileged step is opening a device,
108    /// not connecting the backend.
109    pub fn connect() -> Self {
110        Self {
111            backend: tunnel_lattice_backend_tunrs::TunRsBackend::new(),
112        }
113    }
114}
115
116/// An open TUN/TAP device.
117///
118/// ## Ownership and concurrency contract
119///
120/// `Handle<D>` wraps the backend's device handle in an `Arc<D>` — there is
121/// no single owner in the usual sense; the underlying device stays open for
122/// as long as *any* clone of the `Arc` is alive, and `Handle` is [`Clone`]
123/// specifically to make that sharing explicit and deliberate rather than an
124/// implementation detail only `packet_stream` uses internally.
125///
126/// - **Multiple readers/writers are always safe on every backend.**
127///   [`PacketIo::recv`]/[`PacketIo::send`] take `&self`, not `&mut self` —
128///   this is a property of the trait, not an accident, and every backend
129///   this crate ships (`tunnel-lattice-backend-tunrs`, on Linux, macOS, and
130///   Windows) is verified safe for concurrent `recv`/`send` from multiple
131///   threads sharing one `Handle` clone. This "naive" multiplexing needs no
132///   feature or capability check; see `ARCHITECTURE.md`'s async design
133///   notes for why `tun-rs`'s own `recv`/`send` signatures already commit
134///   to this.
135/// - **`additional_queue` (with `D: MultiQueueProvider`) is a different,
136///   stronger thing**: it returns an independent `Handle` over a *second*
137///   OS-level queue on the same device (Linux `IFF_MULTI_QUEUE` only —
138///   `Capability::MULTI_QUEUE`), for hardware-scheduled per-CPU
139///   distribution instead of every thread contending on one queue. The two
140///   returned handles do not share an `Arc`: dropping one does not affect
141///   the other, and each closes only its own queue on drop.
142/// - **Drop closes the device once every clone is gone.** `D`'s own `Drop`
143///   impl (e.g. `tun_rs::SyncDevice`/`AsyncDevice`'s, which close the
144///   underlying file descriptor) runs when the last `Arc<D>` referencing it
145///   is dropped — which may be a `Handle` clone, a live `PacketStream`,
146///   or both, in any order. No `Handle` method explicitly "closes" a
147///   device; there is nothing to call beyond letting every reference drop.
148/// - **`packet_stream` holds its own `Arc` clone**, independent of the
149///   `Handle` it was created from — dropping the original `Handle` while a
150///   `PacketStream` is still alive does not close the device early, and
151///   vice versa. See `tunnel_lattice_async::PacketStream`'s own docs for
152///   its worker-thread shutdown caveat on `Drop` (a known limitation, not
153///   related to this ownership model).
154///
155/// Referenced as plain text, not an intra-doc link, for the same reason as
156/// the crate-level docs above (`packet_stream` only exists under the
157/// `async-io`/`tokio` features).
158pub struct Handle<D> {
159    device: std::sync::Arc<D>,
160}
161
162impl<D> Clone for Handle<D> {
163    /// Cheap: clones the underlying `Arc<D>`, not the device itself — see
164    /// the type's docs on what sharing a clone means for concurrent access
165    /// and `Drop`.
166    fn clone(&self) -> Self {
167        Handle {
168            device: std::sync::Arc::clone(&self.device),
169        }
170    }
171}
172
173impl<D> Handle<D>
174where
175    D: ConnectedDevice,
176{
177    /// Reads one packet into `buf`, returning the number of bytes written.
178    pub fn recv(&self, buf: &mut [u8]) -> Result<usize> {
179        self.device.recv(buf)
180    }
181
182    /// Writes one packet from `buf`.
183    pub fn send(&self, buf: &[u8]) -> Result<usize> {
184        self.device.send(buf)
185    }
186
187    /// Returns this device's current observed state.
188    pub fn snapshot(&self) -> Result<Device> {
189        self.device.snapshot()
190    }
191
192    /// Applies an MTU or administrative-state patch to this device.
193    pub fn apply(&self, patch: DeviceConfigPatch) -> Result<()> {
194        self.device.apply(patch)
195    }
196
197    /// Returns the runtime-dependent capabilities this device has available.
198    pub fn capabilities(&self) -> Capability {
199        self.device.capabilities()
200    }
201}
202
203impl<D> Handle<D>
204where
205    D: PersistentDevice,
206{
207    /// Marks this device persistent — see [`PersistentDevice`]'s docs.
208    /// Requires `Capability::PERSISTENT_DEVICES`; only `TunRsDevice` on
209    /// Linux implements this today.
210    pub fn persist(&self) -> Result<()> {
211        self.device.persist()
212    }
213}
214
215impl<D> Handle<D>
216where
217    D: MultiQueueProvider,
218{
219    /// Duplicates this device's hardware-scheduled queue for use from
220    /// another thread — see [`MultiQueueProvider`]'s docs. Requires
221    /// `Capability::MULTI_QUEUE` and that the device was opened with
222    /// `DeviceConfig::with_multi_queue(true)`; only `TunRsDevice` on Linux
223    /// implements this today.
224    pub fn additional_queue(&self) -> Result<Handle<D>> {
225        Ok(Handle {
226            device: std::sync::Arc::new(self.device.additional_queue()?),
227        })
228    }
229}
230
231#[cfg(feature = "async")]
232impl<D> Handle<D>
233where
234    D: PacketIo + AsyncPacketIo + CapabilityProvider + Send + Sync + 'static,
235{
236    /// Returns a `futures::Stream` of received packets.
237    ///
238    /// `mtu` bounds the per-packet receive buffer. Uses
239    /// `tunnel-lattice-async::from_async_device` (no worker thread; dropping
240    /// the stream drops the in-flight `recv` future, which is genuine,
241    /// immediate cancellation) when the device reports
242    /// `Capability::NATIVE_ASYNC`; otherwise falls back to
243    /// `from_device`'s thread-based bridge over [`PacketIo`], which cannot
244    /// guarantee prompt shutdown — see that function's rustdoc. Every
245    /// backend `tunnel-lattice` ships as of this method's `D: AsyncPacketIo`
246    /// bound always implements `AsyncPacketIo` whenever this method is
247    /// reachable at all (it requires the `async` feature, which is what
248    /// makes a backend build its async-capable handle in the first place),
249    /// so the fallback path exists for a hypothetical future backend with
250    /// no native async support, not for anything shipped today.
251    pub fn packet_stream(&self, mtu: usize) -> tunnel_lattice_async::PacketStream {
252        if self
253            .device
254            .capabilities()
255            .contains(Capability::NATIVE_ASYNC)
256        {
257            tunnel_lattice_async::from_async_device(std::sync::Arc::clone(&self.device), mtu)
258        } else {
259            tunnel_lattice_async::from_device(std::sync::Arc::clone(&self.device), mtu)
260        }
261    }
262}
263
264/// Privileged, `async`-feature-only tests exercising `Handle::packet_stream`
265/// against a real device — see `tunnel-lattice-backend-tunrs`'s
266/// `privileged_tests` module for why these are `#[ignore]`d and how to run
267/// them, and `tunnel-lattice-async`'s own unit tests for the
268/// cancellation-semantics proof against a mock (no privilege needed there).
269#[cfg(all(test, feature = "async", feature = "tun-rs"))]
270mod privileged_tests {
271    use futures::FutureExt;
272
273    use super::*;
274
275    #[cfg(feature = "tokio")]
276    fn enter_tokio_runtime() -> tokio::runtime::Runtime {
277        tokio::runtime::Builder::new_multi_thread()
278            .enable_io()
279            .build()
280            .expect("build a Tokio runtime")
281    }
282
283    #[test]
284    #[ignore = "requires CAP_NET_ADMIN/Administrator/root to open a TUN device"]
285    fn packet_stream_dispatches_to_the_native_no_thread_path() {
286        #[cfg(feature = "tokio")]
287        let _runtime = enter_tokio_runtime();
288        #[cfg(feature = "tokio")]
289        let _entered = _runtime.enter();
290
291        let tunnel = Tunnel::connect();
292        let device = tunnel
293            .open(DeviceConfig::new(DeviceKind::Tun).with_mtu(1400))
294            .expect("open a TUN device");
295
296        assert!(
297            device.capabilities().contains(Capability::NATIVE_ASYNC),
298            "tunnel-lattice-backend-tunrs reports NATIVE_ASYNC whenever \
299             an async feature is enabled, which is the only way this test \
300             itself gets compiled in"
301        );
302
303        let mut stream = device.packet_stream(1400);
304        // A freshly created Linux TUN device is not actually silent: the
305        // kernel sends IPv6 neighbor-discovery traffic (router
306        // solicitation) onto it almost immediately, confirmed by an
307        // earlier run of this test printing a real received packet here.
308        // So this deliberately does not assert Pending vs. Ready either
309        // way — only that whatever comes back is a well-formed item, not
310        // an error from the dispatch itself.
311        if let Some(item) = futures::StreamExt::next(&mut stream)
312            .now_or_never()
313            .flatten()
314        {
315            item.expect("a resolved item from the native path must be Ok, not a dispatch error");
316        }
317        // Reaching this line at all is the proof: dropping a real device's
318        // in-flight (or just-completed) `AsyncPacketIo::recv` future
319        // completes synchronously, with no worker thread left parked in a
320        // blocking recv the way the pre-0.4 thread-bridge path could leave
321        // one behind.
322        drop(stream);
323    }
324}