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}