Skip to main content

fips_core/upper/
tun.rs

1//! FIPS TUN Interface
2//!
3//! Manages the TUN device for sending and receiving IPv6 packets.
4//! The TUN interface presents FIPS addresses to the local system,
5//! allowing standard socket applications to communicate over the mesh.
6//!
7//! Platform-specific implementations:
8//! - Linux: Uses the `tun` crate with `rtnetlink` for interface configuration
9//! - macOS: Uses the `tun` crate with `ifconfig`/`route` for interface configuration
10//! - Windows: Uses the `wintun` crate for TUN device support
11
12use crate::FipsAddress;
13#[cfg(any(
14    target_os = "linux",
15    target_os = "macos",
16    not(any(target_os = "linux", target_os = "macos", windows))
17))]
18use crate::TunConfig;
19use std::collections::HashMap;
20#[cfg(any(target_os = "linux", target_os = "macos"))]
21use std::fs::File;
22#[cfg(any(target_os = "linux", target_os = "macos"))]
23use std::io::Read;
24#[cfg(not(target_os = "macos"))]
25#[cfg(any(target_os = "linux", target_os = "macos"))]
26use std::io::Write;
27use std::net::Ipv6Addr;
28#[cfg(any(target_os = "linux", target_os = "macos"))]
29use std::os::unix::io::{AsRawFd, FromRawFd};
30use std::sync::{Arc, RwLock};
31use thiserror::Error;
32#[cfg(any(target_os = "linux", target_os = "macos"))]
33use tracing::error;
34use tracing::{debug, trace};
35#[cfg(windows)]
36use tracing::{error, warn};
37#[cfg(any(target_os = "linux", target_os = "macos"))]
38use tun::Layer;
39
40#[cfg(target_os = "linux")]
41use self::linux_vnet::{LinuxVnetTun, linux_vnet_tun_enabled};
42
43pub(crate) use super::tun_outbound::tun_outbound_channel;
44pub use super::tun_outbound::{TunOutboundRx, TunOutboundTx};
45#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
46pub(crate) use super::tun_write::TunRx;
47pub use super::tun_write::TunTx;
48#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
49pub(crate) use super::tun_write::write_channel;
50
51/// Read-only handle to the per-destination path MTU map. Populated by
52/// the discovery handler on `LookupResponse`; read by the TUN reader
53/// (outbound clamp) and writer (inbound clamp) at TCP MSS clamp time.
54/// Keyed by [`FipsAddress`] (16 bytes, the IPv6 form of a fips peer
55/// address).
56pub type PathMtuLookup = Arc<RwLock<HashMap<FipsAddress, u16>>>;
57
58#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
59const TUN_OUTBOUND_PACKET_TAIL_RESERVE: usize = 128;
60
61/// Compute the effective TCP MSS ceiling for a packet given its peer
62/// address bytes (a 16-byte IPv6 destination on outbound, source on
63/// inbound). Returns `min(global_max_mss, learned_path_max_mss)` when
64/// the per-destination path MTU is known via discovery; otherwise
65/// returns `min(global_max_mss, ipv6_minimum_safe_max_mss)`, the
66/// conservative IPv6-minimum-derived ceiling.
67///
68/// The conservative empty-lookup fallback exists because there is a
69/// race window between TCP-SYN-out and discovery-completes-with-path-
70/// MTU on cold flows. Without the floor, the first SYN exits at the
71/// kernel-natural MSS (TUN MTU minus IPv6/TCP headers), which can
72/// exceed what some downstream forwarder hop is willing to carry.
73/// The drop is silent (no PTB feedback through the userspace TUN to
74/// the kernel TCP stack), so TCP retransmits at the same too-large
75/// MSS and the application's first connection wedges before discovery
76/// completes for a corrected second SYN to fire.
77///
78/// RFC 8200 mandates every IPv6 path accepts at least 1280-byte
79/// packets, so a SYN clamped to the IPv6-minimum-derived MSS fits
80/// any compliant path. Subsequent flows pick up the actual learned
81/// per-destination value, which can be larger (when path supports
82/// it) or smaller (when path is observed-tighter than the IPv6 min).
83///
84/// Path MTU bytes-on-wire to TCP MSS: subtract 77 bytes of FIPS encap
85/// overhead, then 40 bytes IPv6 + 20 bytes TCP headers.
86#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
87pub(crate) fn per_flow_max_mss(
88    lookup: &PathMtuLookup,
89    addr_bytes: &[u8],
90    global_max_mss: u16,
91) -> u16 {
92    use super::icmp::effective_ipv6_mtu;
93
94    // RFC 8200 IPv6-minimum MTU (1280) → effective FIPS-encapsulated
95    // payload (1203) → TCP segment after IPv6+TCP headers (1143).
96    // Used as the conservative ceiling for empty-lookup destinations.
97    const IPV6_MIN_MTU: u16 = 1280;
98    let conservative_max_mss = effective_ipv6_mtu(IPV6_MIN_MTU)
99        .saturating_sub(40)
100        .saturating_sub(20);
101    let empty_lookup_ceiling = std::cmp::min(global_max_mss, conservative_max_mss);
102
103    if addr_bytes.len() != 16 {
104        trace!(
105            len = addr_bytes.len(),
106            global_max_mss,
107            empty_lookup_ceiling,
108            "per_flow_max_mss: addr_bytes wrong length, fall back to conservative ceiling"
109        );
110        return empty_lookup_ceiling;
111    }
112    let Ok(fips_addr) = FipsAddress::from_slice(addr_bytes) else {
113        trace!(
114            global_max_mss,
115            empty_lookup_ceiling,
116            "per_flow_max_mss: FipsAddress::from_slice rejected (non-fd::/8 prefix), fall back to conservative ceiling"
117        );
118        return empty_lookup_ceiling;
119    };
120    let Ok(map) = lookup.read() else {
121        trace!(
122            fips_addr = %fips_addr,
123            global_max_mss,
124            empty_lookup_ceiling,
125            "per_flow_max_mss: lookup read lock poisoned, fall back to conservative ceiling"
126        );
127        return empty_lookup_ceiling;
128    };
129    let Some(&path_mtu) = map.get(&fips_addr) else {
130        trace!(
131            fips_addr = %fips_addr,
132            global_max_mss,
133            empty_lookup_ceiling,
134            map_len = map.len(),
135            "per_flow_max_mss: no path_mtu_lookup entry for destination, fall back to conservative ceiling"
136        );
137        return empty_lookup_ceiling;
138    };
139    let path_max_mss = effective_ipv6_mtu(path_mtu)
140        .saturating_sub(40)
141        .saturating_sub(20);
142    let result = std::cmp::min(global_max_mss, path_max_mss);
143    trace!(
144        fips_addr = %fips_addr,
145        path_mtu,
146        path_max_mss,
147        global_max_mss,
148        result,
149        "per_flow_max_mss: per-destination clamp applied"
150    );
151    result
152}
153
154/// Errors that can occur with TUN operations.
155#[derive(Debug, Error)]
156pub enum TunError {
157    #[error("failed to create TUN device: {0}")]
158    Create(#[source] Box<dyn std::error::Error + Send + Sync>),
159
160    #[error("failed to configure TUN device: {0}")]
161    Configure(String),
162
163    #[cfg(target_os = "linux")]
164    #[error("netlink error: {0}")]
165    Netlink(#[from] rtnetlink::Error),
166
167    #[error("interface not found: {0}")]
168    InterfaceNotFound(String),
169
170    #[error("permission denied: {0}")]
171    PermissionDenied(String),
172
173    #[cfg(any(target_os = "linux", target_os = "macos"))]
174    #[error("IPv6 is disabled (set net.ipv6.conf.all.disable_ipv6=0)")]
175    Ipv6Disabled,
176
177    #[error("system TUN is not supported on this platform")]
178    UnsupportedPlatform,
179}
180
181#[cfg(any(target_os = "linux", target_os = "macos"))]
182impl From<tun::Error> for TunError {
183    fn from(e: tun::Error) -> Self {
184        TunError::Create(Box::new(e))
185    }
186}
187
188/// TUN device state.
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum TunState {
191    /// TUN is disabled in configuration.
192    Disabled,
193    /// TUN is configured but not yet created.
194    Configured,
195    /// TUN device is active and ready.
196    Active,
197    /// TUN device failed to initialize.
198    Failed,
199}
200
201impl std::fmt::Display for TunState {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        match self {
204            TunState::Disabled => write!(f, "disabled"),
205            TunState::Configured => write!(f, "configured"),
206            TunState::Active => write!(f, "active"),
207            TunState::Failed => write!(f, "failed"),
208        }
209    }
210}
211
212// ============================================================================
213// Unix (Linux + macOS) TUN implementation
214// ============================================================================
215
216/// FIPS TUN device wrapper.
217#[cfg(any(target_os = "linux", target_os = "macos"))]
218pub struct TunDevice {
219    #[cfg(target_os = "linux")]
220    device: LinuxTunDevice,
221    #[cfg(target_os = "macos")]
222    device: tun::Device,
223    name: String,
224    mtu: u16,
225    address: FipsAddress,
226}
227
228#[cfg(target_os = "linux")]
229enum LinuxTunDevice {
230    Plain(tun::Device),
231    Vnet(LinuxVnetTun),
232}
233
234#[cfg(target_os = "linux")]
235impl LinuxTunDevice {
236    fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
237        match self {
238            Self::Plain(device) => device.as_raw_fd(),
239            Self::Vnet(device) => device.as_raw_fd(),
240        }
241    }
242
243    fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
244        match self {
245            Self::Plain(device) => {
246                let n = device.read(buf)?;
247                if n > 0 {
248                    crate::perf_profile::record_tun_read_frame(n);
249                }
250                Ok(n)
251            }
252            Self::Vnet(device) => device.read_packet(buf),
253        }
254    }
255
256    fn read_vnet_packets_into(
257        &mut self,
258        buf: &mut [u8],
259        packets: &mut Vec<Vec<u8>>,
260    ) -> Result<usize, std::io::Error> {
261        match self {
262            Self::Plain(_) => unreachable!("Linux vnet packet batching requires a vnet TUN"),
263            Self::Vnet(device) => device.read_packets_into(buf, packets),
264        }
265    }
266
267    fn read_buffer_len(&self, mtu: u16) -> usize {
268        match self {
269            Self::Plain(_) => default_tun_read_buffer_len(mtu),
270            Self::Vnet(device) => device.read_buffer_len(),
271        }
272    }
273
274    fn vnet_hdr(&self) -> bool {
275        matches!(self, Self::Vnet(_))
276    }
277}
278
279#[cfg(any(target_os = "linux", target_os = "macos"))]
280impl TunDevice {
281    /// Create or open a TUN device.
282    ///
283    /// If the interface already exists, opens it and reconfigures it.
284    /// Otherwise, creates a new TUN device.
285    ///
286    /// This requires CAP_NET_ADMIN capability (run with sudo or setcap).
287    pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
288        // Check if IPv6 is enabled
289        if platform::is_ipv6_disabled() {
290            return Err(TunError::Ipv6Disabled);
291        }
292
293        let name = config.name();
294        let mtu = config.mtu();
295
296        // Delete existing interface if present (TUN devices are exclusive)
297        if platform::interface_exists(name).await {
298            debug!(name, "Deleting existing TUN interface");
299            if let Err(e) = platform::delete_interface(name).await {
300                debug!(name, error = %e, "Failed to delete existing interface");
301            }
302        }
303
304        #[cfg(target_os = "linux")]
305        let (device, actual_name) = {
306            if linux_vnet_tun_enabled() {
307                let device =
308                    LinuxVnetTun::create(name).map_err(|e| TunError::Create(Box::new(e)))?;
309                let actual_name = device.name().to_string();
310                (LinuxTunDevice::Vnet(device), actual_name)
311            } else {
312                let mut tun_config = tun::Configuration::default();
313                tun_config.tun_name(name).layer(Layer::L3).mtu(mtu);
314                let device = tun::create(&tun_config)?;
315                let actual_name = {
316                    use tun::AbstractDevice;
317                    device.tun_name().map_err(|e| {
318                        TunError::Configure(format!("failed to get device name: {}", e))
319                    })?
320                };
321                (LinuxTunDevice::Plain(device), actual_name)
322            }
323        };
324
325        #[cfg(target_os = "macos")]
326        let (device, actual_name) = {
327            // On macOS, utun devices get kernel-assigned names (utun0, utun1, ...),
328            // so we skip setting the name and read it back after creation.
329            let mut tun_config = tun::Configuration::default();
330            tun_config.layer(Layer::L3).mtu(mtu);
331            let device = tun::create(&tun_config)?;
332            let actual_name = {
333                use tun::AbstractDevice;
334                device
335                    .tun_name()
336                    .map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
337            };
338            (device, actual_name)
339        };
340
341        // Configure address and bring up via platform-specific method
342        platform::configure_interface(&actual_name, address.to_ipv6(), mtu).await?;
343
344        Ok(Self {
345            device,
346            name: actual_name,
347            mtu,
348            address,
349        })
350    }
351
352    /// Get the device name.
353    pub fn name(&self) -> &str {
354        &self.name
355    }
356
357    /// Get the configured MTU.
358    pub fn mtu(&self) -> u16 {
359        self.mtu
360    }
361
362    /// Get the FIPS address assigned to this device.
363    pub fn address(&self) -> &FipsAddress {
364        &self.address
365    }
366
367    /// Get a reference to the underlying tun::Device.
368    #[cfg(target_os = "macos")]
369    pub fn device(&self) -> &tun::Device {
370        &self.device
371    }
372
373    /// Get a mutable reference to the underlying tun::Device.
374    #[cfg(target_os = "macos")]
375    pub fn device_mut(&mut self) -> &mut tun::Device {
376        &mut self.device
377    }
378
379    /// Read a packet from the TUN device.
380    ///
381    /// Returns the number of bytes read into the buffer, or an `io::Error`.
382    /// The buffer should be at least MTU + header size (typically 1500+ bytes).
383    ///
384    /// The tun crate's `Read` impl transparently strips the macOS utun
385    /// packet information header, so this returns a raw IP packet on all
386    /// platforms.
387    ///
388    /// The raw `io::Error` is returned so callers can inspect `ErrorKind`
389    /// (e.g. `WouldBlock`) or `raw_os_error()` without string matching.
390    pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
391        #[cfg(target_os = "linux")]
392        {
393            self.device.read_packet(buf)
394        }
395
396        #[cfg(target_os = "macos")]
397        self.device.read(buf)
398    }
399
400    #[cfg(target_os = "linux")]
401    fn read_vnet_packets_into(
402        &mut self,
403        buf: &mut [u8],
404        packets: &mut Vec<Vec<u8>>,
405    ) -> Result<usize, std::io::Error> {
406        self.device.read_vnet_packets_into(buf, packets)
407    }
408
409    #[cfg(target_os = "linux")]
410    fn read_buffer_len(&self, mtu: u16) -> usize {
411        self.device.read_buffer_len(mtu)
412    }
413
414    #[cfg(target_os = "linux")]
415    fn vnet_hdr(&self) -> bool {
416        self.device.vnet_hdr()
417    }
418
419    #[cfg(any(target_os = "linux", target_os = "macos"))]
420    fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
421        #[cfg(target_os = "linux")]
422        {
423            self.device.as_raw_fd()
424        }
425
426        #[cfg(target_os = "macos")]
427        self.device.as_raw_fd()
428    }
429
430    /// Shutdown and delete the TUN device.
431    ///
432    /// This deletes the interface entirely.
433    pub async fn shutdown(&self) -> Result<(), TunError> {
434        debug!(name = %self.name, "Deleting TUN device");
435        platform::delete_interface(&self.name).await
436    }
437
438    /// Create a TunWriter for this device.
439    ///
440    /// This duplicates the underlying file descriptor so that reads and writes
441    /// can happen independently on separate threads. Returns the writer and
442    /// a channel sender for submitting packets to be written.
443    ///
444    /// `max_mss` is the global TCP MSS ceiling derived from the local
445    /// `transport_mtu()` floor. `path_mtu_lookup` is a read-only handle to
446    /// the per-destination path MTU map populated by discovery; the writer
447    /// reads it on each inbound SYN-ACK to compute a per-flow ceiling that
448    /// honors learned narrow paths through the mesh.
449    pub fn create_writer(
450        &self,
451        max_mss: u16,
452        path_mtu_lookup: PathMtuLookup,
453    ) -> Result<(TunWriter, TunTx), TunError> {
454        let fd = self.as_raw_fd();
455
456        // Duplicate the file descriptor for writing
457        let write_fd = unsafe { libc::dup(fd) };
458        if write_fd < 0 {
459            return Err(TunError::Configure(format!(
460                "failed to dup fd: {}",
461                std::io::Error::last_os_error()
462            )));
463        }
464
465        let write_file = unsafe { File::from_raw_fd(write_fd) };
466        let (tx, rx) = write_channel();
467
468        Ok((
469            TunWriter {
470                file: write_file,
471                rx,
472                name: self.name.clone(),
473                max_mss,
474                path_mtu_lookup,
475                #[cfg(target_os = "linux")]
476                vnet_hdr: self.vnet_hdr(),
477            },
478            tx,
479        ))
480    }
481}
482
483/// macOS utun protocol family value for IPv6 (matches `<sys/socket.h>`
484/// `AF_INET6` on Darwin). Used as the 4-byte big-endian packet-info
485/// header prepended to every utun frame.
486#[cfg(target_os = "macos")]
487const UTUN_AF_INET6: u32 = 30;
488
489/// Build the 4-byte big-endian utun packet-info header for an IPv6 frame.
490///
491/// utun devices on macOS require a 4-byte address-family prefix on every
492/// frame: a single big-endian `u32` carrying the protocol family. For
493/// IPv6 traffic (the only family FIPS sends) this is `AF_INET6 = 30`,
494/// which serializes as `[0x00, 0x00, 0x00, 0x1e]`.
495#[cfg(target_os = "macos")]
496#[inline]
497fn utun_af_inet6_header() -> [u8; 4] {
498    UTUN_AF_INET6.to_be_bytes()
499}
500
501/// Parse the 4-byte big-endian utun packet-info header.
502///
503/// Returns the address-family value (`AF_INET6 = 30` for IPv6 frames),
504/// or `None` if the buffer is shorter than the 4-byte header. The `tun`
505/// crate's `Read` impl strips this transparently for us in the read
506/// path; this helper exists for round-trip testability with
507/// [`utun_af_inet6_header`] and for any future code path that reads
508/// from the dup'd fd directly.
509#[cfg(all(test, target_os = "macos"))]
510#[inline]
511fn parse_utun_af_prefix(buf: &[u8]) -> Option<u32> {
512    if buf.len() < 4 {
513        return None;
514    }
515    Some(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
516}
517
518/// Writer thread for TUN device.
519///
520/// Services a queue of outbound packets and writes them to the TUN device.
521/// Multiple producers can send packets via the TunTx channel.
522///
523/// Also performs TCP MSS clamping on inbound SYN-ACK packets.
524#[cfg(any(target_os = "linux", target_os = "macos"))]
525pub struct TunWriter {
526    file: File,
527    rx: TunRx,
528    name: String,
529    max_mss: u16,
530    path_mtu_lookup: PathMtuLookup,
531    #[cfg(target_os = "linux")]
532    vnet_hdr: bool,
533}
534
535#[cfg(any(target_os = "linux", target_os = "macos"))]
536impl TunWriter {
537    fn clamp_inbound_packet(&self, packet: &mut super::tun_write::TunWritePacket) {
538        use super::tcp_mss::clamp_tcp_mss;
539
540        // Per-destination clamp: peer IPv6 source address (bytes 8..24)
541        // identifies the flow's remote end. If discovery has learned a
542        // smaller path MTU for that peer, tighten the ceiling.
543        let effective_max_mss = if packet.len() >= 24 {
544            per_flow_max_mss(
545                &self.path_mtu_lookup,
546                &packet.as_slice()[8..24],
547                self.max_mss,
548            )
549        } else {
550            self.max_mss
551        };
552        // Clamp TCP MSS on inbound SYN-ACK packets
553        if clamp_tcp_mss(packet.as_mut_slice(), effective_max_mss) {
554            trace!(
555                name = %self.name,
556                max_mss = effective_max_mss,
557                "Clamped TCP MSS in inbound SYN-ACK packet"
558            );
559        }
560    }
561
562    #[cfg(target_os = "linux")]
563    fn run_linux_vnet(mut self) {
564        use std::sync::mpsc::TryRecvError;
565
566        const LINUX_VNET_TUN_WRITE_BATCH_CAP: usize = 256;
567
568        debug!(name = %self.name, max_mss = self.max_mss, "Linux vnet TUN writer starting");
569
570        let mut batch = Vec::with_capacity(LINUX_VNET_TUN_WRITE_BATCH_CAP);
571        let mut write_preparer = linux_vnet::LinuxVnetWritePreparer::new();
572
573        while let Some(mut packet) = self.rx.recv() {
574            self.clamp_inbound_packet(&mut packet);
575            batch.push(packet);
576
577            while batch.len() < LINUX_VNET_TUN_WRITE_BATCH_CAP {
578                match self.rx.try_recv_packet() {
579                    Ok(mut packet) => {
580                        self.clamp_inbound_packet(&mut packet);
581                        batch.push(packet);
582                    }
583                    Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
584                }
585            }
586
587            let write_result = {
588                linux_vnet::write_packet_slices_to_tun(
589                    &mut self.file,
590                    batch.iter().map(|packet| packet.as_slice()),
591                    &mut write_preparer,
592                )
593            };
594
595            if let Err(e) = write_result {
596                let err_str = e.to_string();
597                if err_str.contains("Bad address") {
598                    break;
599                }
600                error!(name = %self.name, error = %e, "Linux vnet TUN write error");
601            } else {
602                for packet in &batch {
603                    crate::perf_profile::record_tun_write_packet(packet.len());
604                    debug_ipv4_icmp_packet("Linux vnet TUN packet written", packet.as_slice());
605                    trace!(name = %self.name, len = packet.len(), "TUN packet written");
606                }
607            }
608
609            batch.clear();
610        }
611    }
612
613    #[cfg(target_os = "macos")]
614    fn write_packet(&self, packet: &super::tun_write::TunWritePacket) -> std::io::Result<()> {
615        use std::os::unix::io::AsRawFd;
616
617        let af_header = utun_af_inet6_header();
618        let iov = [
619            libc::iovec {
620                iov_base: af_header.as_ptr() as *mut libc::c_void,
621                iov_len: 4,
622            },
623            libc::iovec {
624                iov_base: packet.as_slice().as_ptr() as *mut libc::c_void,
625                iov_len: packet.len(),
626            },
627        ];
628        let ret = unsafe { libc::writev(self.file.as_raw_fd(), iov.as_ptr(), 2) };
629        if ret < 0 {
630            return Err(std::io::Error::last_os_error());
631        }
632        let expected = 4 + packet.len();
633        if (ret as usize) < expected {
634            return Err(std::io::Error::new(
635                std::io::ErrorKind::WriteZero,
636                format!("short writev: {} of {} bytes", ret, expected),
637            ));
638        }
639        Ok(())
640    }
641
642    #[cfg(not(target_os = "macos"))]
643    fn write_packet(&mut self, packet: &super::tun_write::TunWritePacket) -> std::io::Result<()> {
644        self.file.write_all(packet.as_slice())
645    }
646
647    /// Run the writer loop.
648    ///
649    /// Blocks forever, reading packets from the channel and writing them
650    /// to the TUN device. Returns when the channel is closed (all senders dropped).
651    pub fn run(self) {
652        #[cfg(target_os = "linux")]
653        let mut writer = self;
654        #[cfg(target_os = "macos")]
655        let writer = self;
656
657        debug!(name = %writer.name, max_mss = writer.max_mss, "TUN writer starting");
658
659        #[cfg(target_os = "linux")]
660        if writer.vnet_hdr {
661            writer.run_linux_vnet();
662            return;
663        }
664
665        while let Some(mut packet) = writer.rx.recv() {
666            writer.clamp_inbound_packet(&mut packet);
667            let write_result = writer.write_packet(&packet);
668
669            if let Err(e) = write_result {
670                // "Bad address" is expected during shutdown when interface is deleted
671                let err_str = e.to_string();
672                if err_str.contains("Bad address") {
673                    break;
674                }
675                error!(name = %writer.name, error = %e, "TUN write error");
676            } else {
677                crate::perf_profile::record_tun_write_packet(packet.len());
678                debug_ipv4_icmp_packet("TUN packet written", packet.as_slice());
679                trace!(name = %writer.name, len = packet.len(), "TUN packet written");
680            }
681        }
682    }
683}
684
685#[cfg(any(target_os = "linux", target_os = "macos"))]
686fn debug_ipv4_icmp_packet(message: &'static str, packet: &[u8]) {
687    let Some((src, dst, icmp_type, icmp_id, icmp_seq)) = ipv4_icmp_echo(packet) else {
688        return;
689    };
690    debug!(
691        src = %src,
692        dst = %dst,
693        icmp_type,
694        icmp_id,
695        icmp_seq,
696        message
697    );
698}
699
700#[cfg(any(target_os = "linux", target_os = "macos"))]
701fn ipv4_icmp_echo(packet: &[u8]) -> Option<(std::net::Ipv4Addr, std::net::Ipv4Addr, u8, u16, u16)> {
702    if packet.len() < 28 || packet[0] >> 4 != 4 || packet[9] != 1 {
703        return None;
704    }
705    let header_len = usize::from(packet[0] & 0x0f).checked_mul(4)?;
706    if header_len < 20 || packet.len() < header_len.saturating_add(8) {
707        return None;
708    }
709    let icmp_type = packet[header_len];
710    if !matches!(icmp_type, 0 | 8) {
711        return None;
712    }
713    let src = std::net::Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]);
714    let dst = std::net::Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]);
715    let icmp_id = u16::from_be_bytes([packet[header_len + 4], packet[header_len + 5]]);
716    let icmp_seq = u16::from_be_bytes([packet[header_len + 6], packet[header_len + 7]]);
717    Some((src, dst, icmp_type, icmp_id, icmp_seq))
718}
719
720#[cfg(any(target_os = "linux", target_os = "macos"))]
721mod platform;
722
723#[cfg(windows)]
724mod windows;
725
726#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
727mod unsupported;
728
729#[cfg(target_os = "linux")]
730mod linux_vnet;
731
732#[cfg(test)]
733mod tests;
734
735include!("tun_runtime.rs");