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;
48pub(crate) use super::tun_write::write_channel;
49
50/// Read-only handle to the per-destination path MTU map. Populated by
51/// the discovery handler on `LookupResponse`; read by the TUN reader
52/// (outbound clamp) and writer (inbound clamp) at TCP MSS clamp time.
53/// Keyed by [`FipsAddress`] (16 bytes, the IPv6 form of a fips peer
54/// address).
55pub type PathMtuLookup = Arc<RwLock<HashMap<FipsAddress, u16>>>;
56
57#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
58const TUN_OUTBOUND_PACKET_TAIL_RESERVE: usize = 128;
59
60/// Compute the effective TCP MSS ceiling for a packet given its peer
61/// address bytes (a 16-byte IPv6 destination on outbound, source on
62/// inbound). Returns `min(global_max_mss, learned_path_max_mss)` when
63/// the per-destination path MTU is known via discovery; otherwise
64/// returns `min(global_max_mss, ipv6_minimum_safe_max_mss)`, the
65/// conservative IPv6-minimum-derived ceiling.
66///
67/// The conservative empty-lookup fallback exists because there is a
68/// race window between TCP-SYN-out and discovery-completes-with-path-
69/// MTU on cold flows. Without the floor, the first SYN exits at the
70/// kernel-natural MSS (TUN MTU minus IPv6/TCP headers), which can
71/// exceed what some downstream forwarder hop is willing to carry.
72/// The drop is silent (no PTB feedback through the userspace TUN to
73/// the kernel TCP stack), so TCP retransmits at the same too-large
74/// MSS and the application's first connection wedges before discovery
75/// completes for a corrected second SYN to fire.
76///
77/// RFC 8200 mandates every IPv6 path accepts at least 1280-byte
78/// packets, so a SYN clamped to the IPv6-minimum-derived MSS fits
79/// any compliant path. Subsequent flows pick up the actual learned
80/// per-destination value, which can be larger (when path supports
81/// it) or smaller (when path is observed-tighter than the IPv6 min).
82///
83/// Path MTU bytes-on-wire to TCP MSS: subtract 77 bytes of FIPS encap
84/// overhead, then 40 bytes IPv6 + 20 bytes TCP headers.
85#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
86pub(crate) fn per_flow_max_mss(
87    lookup: &PathMtuLookup,
88    addr_bytes: &[u8],
89    global_max_mss: u16,
90) -> u16 {
91    use super::icmp::effective_ipv6_mtu;
92
93    // RFC 8200 IPv6-minimum MTU (1280) → effective FIPS-encapsulated
94    // payload (1203) → TCP segment after IPv6+TCP headers (1143).
95    // Used as the conservative ceiling for empty-lookup destinations.
96    const IPV6_MIN_MTU: u16 = 1280;
97    let conservative_max_mss = effective_ipv6_mtu(IPV6_MIN_MTU)
98        .saturating_sub(40)
99        .saturating_sub(20);
100    let empty_lookup_ceiling = std::cmp::min(global_max_mss, conservative_max_mss);
101
102    if addr_bytes.len() != 16 {
103        trace!(
104            len = addr_bytes.len(),
105            global_max_mss,
106            empty_lookup_ceiling,
107            "per_flow_max_mss: addr_bytes wrong length, fall back to conservative ceiling"
108        );
109        return empty_lookup_ceiling;
110    }
111    let Ok(fips_addr) = FipsAddress::from_slice(addr_bytes) else {
112        trace!(
113            global_max_mss,
114            empty_lookup_ceiling,
115            "per_flow_max_mss: FipsAddress::from_slice rejected (non-fd::/8 prefix), fall back to conservative ceiling"
116        );
117        return empty_lookup_ceiling;
118    };
119    let Ok(map) = lookup.read() else {
120        trace!(
121            fips_addr = %fips_addr,
122            global_max_mss,
123            empty_lookup_ceiling,
124            "per_flow_max_mss: lookup read lock poisoned, fall back to conservative ceiling"
125        );
126        return empty_lookup_ceiling;
127    };
128    let Some(&path_mtu) = map.get(&fips_addr) else {
129        trace!(
130            fips_addr = %fips_addr,
131            global_max_mss,
132            empty_lookup_ceiling,
133            map_len = map.len(),
134            "per_flow_max_mss: no path_mtu_lookup entry for destination, fall back to conservative ceiling"
135        );
136        return empty_lookup_ceiling;
137    };
138    let path_max_mss = effective_ipv6_mtu(path_mtu)
139        .saturating_sub(40)
140        .saturating_sub(20);
141    let result = std::cmp::min(global_max_mss, path_max_mss);
142    trace!(
143        fips_addr = %fips_addr,
144        path_mtu,
145        path_max_mss,
146        global_max_mss,
147        result,
148        "per_flow_max_mss: per-destination clamp applied"
149    );
150    result
151}
152
153/// Errors that can occur with TUN operations.
154#[derive(Debug, Error)]
155pub enum TunError {
156    #[error("failed to create TUN device: {0}")]
157    Create(#[source] Box<dyn std::error::Error + Send + Sync>),
158
159    #[error("failed to configure TUN device: {0}")]
160    Configure(String),
161
162    #[cfg(target_os = "linux")]
163    #[error("netlink error: {0}")]
164    Netlink(#[from] rtnetlink::Error),
165
166    #[error("interface not found: {0}")]
167    InterfaceNotFound(String),
168
169    #[error("permission denied: {0}")]
170    PermissionDenied(String),
171
172    #[cfg(any(target_os = "linux", target_os = "macos"))]
173    #[error("IPv6 is disabled (set net.ipv6.conf.all.disable_ipv6=0)")]
174    Ipv6Disabled,
175
176    #[error("system TUN is not supported on this platform")]
177    UnsupportedPlatform,
178}
179
180#[cfg(any(target_os = "linux", target_os = "macos"))]
181impl From<tun::Error> for TunError {
182    fn from(e: tun::Error) -> Self {
183        TunError::Create(Box::new(e))
184    }
185}
186
187/// TUN device state.
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum TunState {
190    /// TUN is disabled in configuration.
191    Disabled,
192    /// TUN is configured but not yet created.
193    Configured,
194    /// TUN device is active and ready.
195    Active,
196    /// TUN device failed to initialize.
197    Failed,
198}
199
200impl std::fmt::Display for TunState {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            TunState::Disabled => write!(f, "disabled"),
204            TunState::Configured => write!(f, "configured"),
205            TunState::Active => write!(f, "active"),
206            TunState::Failed => write!(f, "failed"),
207        }
208    }
209}
210
211// ============================================================================
212// Unix (Linux + macOS) TUN implementation
213// ============================================================================
214
215/// FIPS TUN device wrapper.
216#[cfg(any(target_os = "linux", target_os = "macos"))]
217pub struct TunDevice {
218    #[cfg(target_os = "linux")]
219    device: LinuxTunDevice,
220    #[cfg(target_os = "macos")]
221    device: tun::Device,
222    name: String,
223    mtu: u16,
224    address: FipsAddress,
225}
226
227#[cfg(target_os = "linux")]
228enum LinuxTunDevice {
229    Plain(tun::Device),
230    Vnet(LinuxVnetTun),
231}
232
233#[cfg(target_os = "linux")]
234impl LinuxTunDevice {
235    fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
236        match self {
237            Self::Plain(device) => device.as_raw_fd(),
238            Self::Vnet(device) => device.as_raw_fd(),
239        }
240    }
241
242    fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
243        match self {
244            Self::Plain(device) => {
245                let n = device.read(buf)?;
246                if n > 0 {
247                    crate::perf_profile::record_tun_read_frame(n);
248                }
249                Ok(n)
250            }
251            Self::Vnet(device) => device.read_packet(buf),
252        }
253    }
254
255    fn read_vnet_packets_into(
256        &mut self,
257        buf: &mut [u8],
258        packets: &mut Vec<Vec<u8>>,
259    ) -> Result<usize, std::io::Error> {
260        match self {
261            Self::Plain(_) => unreachable!("Linux vnet packet batching requires a vnet TUN"),
262            Self::Vnet(device) => device.read_packets_into(buf, packets),
263        }
264    }
265
266    fn read_buffer_len(&self, mtu: u16) -> usize {
267        match self {
268            Self::Plain(_) => default_tun_read_buffer_len(mtu),
269            Self::Vnet(device) => device.read_buffer_len(),
270        }
271    }
272
273    fn vnet_hdr(&self) -> bool {
274        matches!(self, Self::Vnet(_))
275    }
276}
277
278#[cfg(any(target_os = "linux", target_os = "macos"))]
279impl TunDevice {
280    /// Create or open a TUN device.
281    ///
282    /// If the interface already exists, opens it and reconfigures it.
283    /// Otherwise, creates a new TUN device.
284    ///
285    /// This requires CAP_NET_ADMIN capability (run with sudo or setcap).
286    pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
287        // Check if IPv6 is enabled
288        if platform::is_ipv6_disabled() {
289            return Err(TunError::Ipv6Disabled);
290        }
291
292        let name = config.name();
293        let mtu = config.mtu();
294
295        // Delete existing interface if present (TUN devices are exclusive)
296        if platform::interface_exists(name).await {
297            debug!(name, "Deleting existing TUN interface");
298            if let Err(e) = platform::delete_interface(name).await {
299                debug!(name, error = %e, "Failed to delete existing interface");
300            }
301        }
302
303        #[cfg(target_os = "linux")]
304        let (device, actual_name) = {
305            if linux_vnet_tun_enabled() {
306                let device =
307                    LinuxVnetTun::create(name).map_err(|e| TunError::Create(Box::new(e)))?;
308                let actual_name = device.name().to_string();
309                (LinuxTunDevice::Vnet(device), actual_name)
310            } else {
311                let mut tun_config = tun::Configuration::default();
312                tun_config.tun_name(name).layer(Layer::L3).mtu(mtu);
313                let device = tun::create(&tun_config)?;
314                let actual_name = {
315                    use tun::AbstractDevice;
316                    device.tun_name().map_err(|e| {
317                        TunError::Configure(format!("failed to get device name: {}", e))
318                    })?
319                };
320                (LinuxTunDevice::Plain(device), actual_name)
321            }
322        };
323
324        #[cfg(target_os = "macos")]
325        let (device, actual_name) = {
326            // On macOS, utun devices get kernel-assigned names (utun0, utun1, ...),
327            // so we skip setting the name and read it back after creation.
328            let mut tun_config = tun::Configuration::default();
329            tun_config.layer(Layer::L3).mtu(mtu);
330            let device = tun::create(&tun_config)?;
331            let actual_name = {
332                use tun::AbstractDevice;
333                device
334                    .tun_name()
335                    .map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
336            };
337            (device, actual_name)
338        };
339
340        // Configure address and bring up via platform-specific method
341        platform::configure_interface(&actual_name, address.to_ipv6(), mtu).await?;
342
343        Ok(Self {
344            device,
345            name: actual_name,
346            mtu,
347            address,
348        })
349    }
350
351    /// Get the device name.
352    pub fn name(&self) -> &str {
353        &self.name
354    }
355
356    /// Get the configured MTU.
357    pub fn mtu(&self) -> u16 {
358        self.mtu
359    }
360
361    /// Get the FIPS address assigned to this device.
362    pub fn address(&self) -> &FipsAddress {
363        &self.address
364    }
365
366    /// Get a reference to the underlying tun::Device.
367    #[cfg(target_os = "macos")]
368    pub fn device(&self) -> &tun::Device {
369        &self.device
370    }
371
372    /// Get a mutable reference to the underlying tun::Device.
373    #[cfg(target_os = "macos")]
374    pub fn device_mut(&mut self) -> &mut tun::Device {
375        &mut self.device
376    }
377
378    /// Read a packet from the TUN device.
379    ///
380    /// Returns the number of bytes read into the buffer, or an `io::Error`.
381    /// The buffer should be at least MTU + header size (typically 1500+ bytes).
382    ///
383    /// The tun crate's `Read` impl transparently strips the macOS utun
384    /// packet information header, so this returns a raw IP packet on all
385    /// platforms.
386    ///
387    /// The raw `io::Error` is returned so callers can inspect `ErrorKind`
388    /// (e.g. `WouldBlock`) or `raw_os_error()` without string matching.
389    pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
390        #[cfg(target_os = "linux")]
391        {
392            return self.device.read_packet(buf);
393        }
394
395        #[cfg(target_os = "macos")]
396        self.device.read(buf)
397    }
398
399    #[cfg(target_os = "linux")]
400    fn read_vnet_packets_into(
401        &mut self,
402        buf: &mut [u8],
403        packets: &mut Vec<Vec<u8>>,
404    ) -> Result<usize, std::io::Error> {
405        self.device.read_vnet_packets_into(buf, packets)
406    }
407
408    #[cfg(target_os = "linux")]
409    fn read_buffer_len(&self, mtu: u16) -> usize {
410        self.device.read_buffer_len(mtu)
411    }
412
413    #[cfg(target_os = "linux")]
414    fn vnet_hdr(&self) -> bool {
415        self.device.vnet_hdr()
416    }
417
418    #[cfg(any(target_os = "linux", target_os = "macos"))]
419    fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
420        #[cfg(target_os = "linux")]
421        {
422            return self.device.as_raw_fd();
423        }
424
425        #[cfg(target_os = "macos")]
426        self.device.as_raw_fd()
427    }
428
429    /// Shutdown and delete the TUN device.
430    ///
431    /// This deletes the interface entirely.
432    pub async fn shutdown(&self) -> Result<(), TunError> {
433        debug!(name = %self.name, "Deleting TUN device");
434        platform::delete_interface(&self.name).await
435    }
436
437    /// Create a TunWriter for this device.
438    ///
439    /// This duplicates the underlying file descriptor so that reads and writes
440    /// can happen independently on separate threads. Returns the writer and
441    /// a channel sender for submitting packets to be written.
442    ///
443    /// `max_mss` is the global TCP MSS ceiling derived from the local
444    /// `transport_mtu()` floor. `path_mtu_lookup` is a read-only handle to
445    /// the per-destination path MTU map populated by discovery; the writer
446    /// reads it on each inbound SYN-ACK to compute a per-flow ceiling that
447    /// honors learned narrow paths through the mesh.
448    pub fn create_writer(
449        &self,
450        max_mss: u16,
451        path_mtu_lookup: PathMtuLookup,
452    ) -> Result<(TunWriter, TunTx), TunError> {
453        let fd = self.as_raw_fd();
454
455        // Duplicate the file descriptor for writing
456        let write_fd = unsafe { libc::dup(fd) };
457        if write_fd < 0 {
458            return Err(TunError::Configure(format!(
459                "failed to dup fd: {}",
460                std::io::Error::last_os_error()
461            )));
462        }
463
464        let write_file = unsafe { File::from_raw_fd(write_fd) };
465        let (tx, rx) = write_channel();
466
467        Ok((
468            TunWriter {
469                file: write_file,
470                rx,
471                name: self.name.clone(),
472                max_mss,
473                path_mtu_lookup,
474                #[cfg(target_os = "linux")]
475                vnet_hdr: self.vnet_hdr(),
476            },
477            tx,
478        ))
479    }
480}
481
482/// macOS utun protocol family value for IPv6 (matches `<sys/socket.h>`
483/// `AF_INET6` on Darwin). Used as the 4-byte big-endian packet-info
484/// header prepended to every utun frame.
485#[cfg(target_os = "macos")]
486const UTUN_AF_INET6: u32 = 30;
487
488/// Build the 4-byte big-endian utun packet-info header for an IPv6 frame.
489///
490/// utun devices on macOS require a 4-byte address-family prefix on every
491/// frame: a single big-endian `u32` carrying the protocol family. For
492/// IPv6 traffic (the only family FIPS sends) this is `AF_INET6 = 30`,
493/// which serializes as `[0x00, 0x00, 0x00, 0x1e]`.
494#[cfg(target_os = "macos")]
495#[inline]
496fn utun_af_inet6_header() -> [u8; 4] {
497    UTUN_AF_INET6.to_be_bytes()
498}
499
500/// Parse the 4-byte big-endian utun packet-info header.
501///
502/// Returns the address-family value (`AF_INET6 = 30` for IPv6 frames),
503/// or `None` if the buffer is shorter than the 4-byte header. The `tun`
504/// crate's `Read` impl strips this transparently for us in the read
505/// path; this helper exists for round-trip testability with
506/// [`utun_af_inet6_header`] and for any future code path that reads
507/// from the dup'd fd directly.
508#[cfg(all(test, target_os = "macos"))]
509#[inline]
510fn parse_utun_af_prefix(buf: &[u8]) -> Option<u32> {
511    if buf.len() < 4 {
512        return None;
513    }
514    Some(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
515}
516
517/// Writer thread for TUN device.
518///
519/// Services a queue of outbound packets and writes them to the TUN device.
520/// Multiple producers can send packets via the TunTx channel.
521///
522/// Also performs TCP MSS clamping on inbound SYN-ACK packets.
523#[cfg(any(target_os = "linux", target_os = "macos"))]
524pub struct TunWriter {
525    file: File,
526    rx: TunRx,
527    name: String,
528    max_mss: u16,
529    path_mtu_lookup: PathMtuLookup,
530    #[cfg(target_os = "linux")]
531    vnet_hdr: bool,
532}
533
534#[cfg(any(target_os = "linux", target_os = "macos"))]
535impl TunWriter {
536    fn clamp_inbound_packet(&self, packet: &mut super::tun_write::TunWritePacket) {
537        use super::tcp_mss::clamp_tcp_mss;
538
539        // Per-destination clamp: peer IPv6 source address (bytes 8..24)
540        // identifies the flow's remote end. If discovery has learned a
541        // smaller path MTU for that peer, tighten the ceiling.
542        let effective_max_mss = if packet.len() >= 24 {
543            per_flow_max_mss(
544                &self.path_mtu_lookup,
545                &packet.as_slice()[8..24],
546                self.max_mss,
547            )
548        } else {
549            self.max_mss
550        };
551        // Clamp TCP MSS on inbound SYN-ACK packets
552        if clamp_tcp_mss(packet.as_mut_slice(), effective_max_mss) {
553            trace!(
554                name = %self.name,
555                max_mss = effective_max_mss,
556                "Clamped TCP MSS in inbound SYN-ACK packet"
557            );
558        }
559    }
560
561    #[cfg(target_os = "linux")]
562    fn run_linux_vnet(mut self) {
563        use std::sync::mpsc::TryRecvError;
564
565        const LINUX_VNET_TUN_WRITE_BATCH_CAP: usize = 256;
566
567        debug!(name = %self.name, max_mss = self.max_mss, "Linux vnet TUN writer starting");
568
569        let mut batch = Vec::with_capacity(LINUX_VNET_TUN_WRITE_BATCH_CAP);
570        let mut write_preparer = linux_vnet::LinuxVnetWritePreparer::new();
571
572        while let Some(mut packet) = self.rx.recv() {
573            self.clamp_inbound_packet(&mut packet);
574            batch.push(packet);
575
576            while batch.len() < LINUX_VNET_TUN_WRITE_BATCH_CAP {
577                match self.rx.try_recv_packet() {
578                    Ok(mut packet) => {
579                        self.clamp_inbound_packet(&mut packet);
580                        batch.push(packet);
581                    }
582                    Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
583                }
584            }
585
586            let write_result = {
587                linux_vnet::write_packet_slices_to_tun(
588                    &mut self.file,
589                    batch.iter().map(|packet| packet.as_slice()),
590                    &mut write_preparer,
591                )
592            };
593
594            if let Err(e) = write_result {
595                let err_str = e.to_string();
596                if err_str.contains("Bad address") {
597                    break;
598                }
599                error!(name = %self.name, error = %e, "Linux vnet TUN write error");
600            } else {
601                for packet in &batch {
602                    crate::perf_profile::record_tun_write_packet(packet.len());
603                    debug_ipv4_icmp_packet("Linux vnet TUN packet written", packet.as_slice());
604                    trace!(name = %self.name, len = packet.len(), "TUN packet written");
605                }
606            }
607
608            batch.clear();
609        }
610    }
611
612    #[cfg(target_os = "macos")]
613    fn write_packet(&self, packet: &super::tun_write::TunWritePacket) -> std::io::Result<()> {
614        use std::os::unix::io::AsRawFd;
615
616        let af_header = utun_af_inet6_header();
617        let iov = [
618            libc::iovec {
619                iov_base: af_header.as_ptr() as *mut libc::c_void,
620                iov_len: 4,
621            },
622            libc::iovec {
623                iov_base: packet.as_slice().as_ptr() as *mut libc::c_void,
624                iov_len: packet.len(),
625            },
626        ];
627        let ret = unsafe { libc::writev(self.file.as_raw_fd(), iov.as_ptr(), 2) };
628        if ret < 0 {
629            return Err(std::io::Error::last_os_error());
630        }
631        let expected = 4 + packet.len();
632        if (ret as usize) < expected {
633            return Err(std::io::Error::new(
634                std::io::ErrorKind::WriteZero,
635                format!("short writev: {} of {} bytes", ret, expected),
636            ));
637        }
638        Ok(())
639    }
640
641    #[cfg(not(target_os = "macos"))]
642    fn write_packet(&mut self, packet: &super::tun_write::TunWritePacket) -> std::io::Result<()> {
643        self.file.write_all(packet.as_slice())
644    }
645
646    /// Run the writer loop.
647    ///
648    /// Blocks forever, reading packets from the channel and writing them
649    /// to the TUN device. Returns when the channel is closed (all senders dropped).
650    pub fn run(self) {
651        #[cfg(target_os = "linux")]
652        let mut writer = self;
653        #[cfg(target_os = "macos")]
654        let writer = self;
655
656        debug!(name = %writer.name, max_mss = writer.max_mss, "TUN writer starting");
657
658        #[cfg(target_os = "linux")]
659        if writer.vnet_hdr {
660            writer.run_linux_vnet();
661            return;
662        }
663
664        while let Some(mut packet) = writer.rx.recv() {
665            writer.clamp_inbound_packet(&mut packet);
666            let write_result = writer.write_packet(&packet);
667
668            if let Err(e) = write_result {
669                // "Bad address" is expected during shutdown when interface is deleted
670                let err_str = e.to_string();
671                if err_str.contains("Bad address") {
672                    break;
673                }
674                error!(name = %writer.name, error = %e, "TUN write error");
675            } else {
676                crate::perf_profile::record_tun_write_packet(packet.len());
677                debug_ipv4_icmp_packet("TUN packet written", packet.as_slice());
678                trace!(name = %writer.name, len = packet.len(), "TUN packet written");
679            }
680        }
681    }
682}
683
684#[cfg(any(target_os = "linux", target_os = "macos"))]
685fn debug_ipv4_icmp_packet(message: &'static str, packet: &[u8]) {
686    let Some((src, dst, icmp_type, icmp_id, icmp_seq)) = ipv4_icmp_echo(packet) else {
687        return;
688    };
689    debug!(
690        src = %src,
691        dst = %dst,
692        icmp_type,
693        icmp_id,
694        icmp_seq,
695        message
696    );
697}
698
699#[cfg(any(target_os = "linux", target_os = "macos"))]
700fn ipv4_icmp_echo(packet: &[u8]) -> Option<(std::net::Ipv4Addr, std::net::Ipv4Addr, u8, u16, u16)> {
701    if packet.len() < 28 || packet[0] >> 4 != 4 || packet[9] != 1 {
702        return None;
703    }
704    let header_len = usize::from(packet[0] & 0x0f).checked_mul(4)?;
705    if header_len < 20 || packet.len() < header_len.saturating_add(8) {
706        return None;
707    }
708    let icmp_type = packet[header_len];
709    if !matches!(icmp_type, 0 | 8) {
710        return None;
711    }
712    let src = std::net::Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]);
713    let dst = std::net::Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]);
714    let icmp_id = u16::from_be_bytes([packet[header_len + 4], packet[header_len + 5]]);
715    let icmp_seq = u16::from_be_bytes([packet[header_len + 6], packet[header_len + 7]]);
716    Some((src, dst, icmp_type, icmp_id, icmp_seq))
717}
718
719/// TUN packet reader loop (Linux).
720///
721/// Reads IPv6 packets from the TUN device. Packets destined for FIPS addresses
722/// (fd::/8) are forwarded to the Node via the outbound channel for session
723/// encapsulation and routing. Non-FIPS packets receive ICMPv6 Destination
724/// Unreachable responses.
725///
726/// Also performs TCP MSS clamping on SYN packets to prevent oversized segments.
727///
728/// This is designed to run in a dedicated thread since TUN reads are blocking.
729/// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable
730/// error occurs.
731#[cfg(not(target_os = "macos"))]
732#[cfg(any(target_os = "linux", target_os = "macos"))]
733pub(crate) fn run_tun_reader(runtime: TunReaderRuntime) {
734    #[cfg(target_os = "linux")]
735    if runtime.device.vnet_hdr() {
736        run_linux_vnet_tun_reader(runtime);
737        return;
738    }
739
740    let TunReaderRuntime {
741        mut device,
742        mtu,
743        our_addr,
744        tun_tx,
745        outbound_tx,
746        transport_mtu,
747        path_mtu_lookup,
748    } = runtime;
749    let read_buffer_len = device.read_buffer_len(mtu);
750    let (name, mut buf, max_mss) =
751        tun_reader_setup_with_buffer_len(device.name(), mtu, transport_mtu, read_buffer_len);
752
753    loop {
754        match device.read_packet(&mut buf) {
755            Ok(n) if n > 0 => {
756                crate::perf_profile::record_tun_read_packet(n);
757                if !handle_tun_packet(
758                    &mut buf[..n],
759                    max_mss,
760                    &name,
761                    our_addr,
762                    &tun_tx,
763                    &outbound_tx,
764                    &path_mtu_lookup,
765                ) {
766                    break;
767                }
768            }
769            Ok(_) => {}
770            Err(e) => {
771                // EFAULT ("Bad address") is expected during shutdown when the interface is deleted
772                if e.raw_os_error() != Some(libc::EFAULT) {
773                    error!(name = %name, error = %e, "TUN read error");
774                }
775                break;
776            }
777        }
778    }
779}
780
781#[cfg(target_os = "linux")]
782fn run_linux_vnet_tun_reader(runtime: TunReaderRuntime) {
783    let TunReaderRuntime {
784        mut device,
785        mtu,
786        our_addr,
787        tun_tx,
788        outbound_tx,
789        transport_mtu,
790        path_mtu_lookup,
791    } = runtime;
792    let read_buffer_len = device.read_buffer_len(mtu);
793    let (name, mut buf, max_mss) =
794        tun_reader_setup_with_buffer_len(device.name(), mtu, transport_mtu, read_buffer_len);
795    let mut packets = Vec::with_capacity(64);
796
797    loop {
798        packets.clear();
799        match device.read_vnet_packets_into(&mut buf, &mut packets) {
800            Ok(n) if n > 0 => {
801                debug_assert_eq!(n, packets.len());
802                for packet in packets.drain(..) {
803                    crate::perf_profile::record_tun_read_packet(packet.len());
804                    if !handle_tun_packet_owned(
805                        packet,
806                        max_mss,
807                        &name,
808                        our_addr,
809                        &tun_tx,
810                        &outbound_tx,
811                        &path_mtu_lookup,
812                    ) {
813                        return;
814                    }
815                }
816            }
817            Ok(_) => {}
818            Err(e) => {
819                if e.raw_os_error() != Some(libc::EFAULT) {
820                    error!(name = %name, error = %e, "Linux vnet TUN read error");
821                }
822                break;
823            }
824        }
825    }
826}
827
828/// RAII wrapper that closes a raw fd on drop.
829///
830/// Used to ensure the shutdown pipe read-end is always closed when
831/// `run_tun_reader` returns, regardless of which exit path is taken.
832#[cfg(target_os = "macos")]
833struct ShutdownFd(std::os::unix::io::RawFd);
834
835#[cfg(target_os = "macos")]
836impl Drop for ShutdownFd {
837    fn drop(&mut self) {
838        unsafe {
839            libc::close(self.0);
840        }
841    }
842}
843
844/// TUN packet reader loop (macOS).
845///
846/// Uses `select()` to multiplex between the TUN fd and a shutdown pipe,
847/// avoiding the need to close the TUN fd externally (which would cause a
848/// double-close when `TunDevice` drops).
849#[cfg(target_os = "macos")]
850pub(crate) fn run_tun_reader(runtime: TunReaderRuntime, shutdown_fd: std::os::unix::io::RawFd) {
851    let TunReaderRuntime {
852        mut device,
853        mtu,
854        our_addr,
855        tun_tx,
856        outbound_tx,
857        transport_mtu,
858        path_mtu_lookup,
859    } = runtime;
860    let _shutdown_fd = ShutdownFd(shutdown_fd);
861    let tun_fd = device.device().as_raw_fd();
862    let (name, mut buf, max_mss) = tun_reader_setup(device.name(), mtu, transport_mtu);
863
864    // Set TUN fd to non-blocking so we can use select + read without blocking
865    // past the point where select returns readable.
866    unsafe {
867        let flags = libc::fcntl(tun_fd, libc::F_GETFL);
868        if flags >= 0 {
869            libc::fcntl(tun_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
870        }
871    }
872
873    let nfds = tun_fd.max(shutdown_fd) + 1;
874
875    loop {
876        // Wait for either TUN data or shutdown signal
877        unsafe {
878            let mut read_fds: libc::fd_set = std::mem::zeroed();
879            libc::FD_ZERO(&mut read_fds);
880            libc::FD_SET(tun_fd, &mut read_fds);
881            libc::FD_SET(shutdown_fd, &mut read_fds);
882
883            let ret = libc::select(
884                nfds,
885                &mut read_fds,
886                std::ptr::null_mut(),
887                std::ptr::null_mut(),
888                std::ptr::null_mut(),
889            );
890            if ret < 0 {
891                let err = std::io::Error::last_os_error();
892                if err.kind() == std::io::ErrorKind::Interrupted {
893                    continue;
894                }
895                error!(name = %name, error = %err, "TUN select error");
896                break;
897            }
898
899            // Shutdown signal received
900            if libc::FD_ISSET(shutdown_fd, &read_fds) {
901                debug!(name = %name, "TUN reader received shutdown signal");
902                break;
903            }
904        }
905
906        // TUN fd is readable — drain all available packets
907        loop {
908            match device.read_packet(&mut buf) {
909                Ok(n) if n > 0 => {
910                    crate::perf_profile::record_tun_read_packet(n);
911                    if !handle_tun_packet(
912                        &mut buf[..n],
913                        max_mss,
914                        &name,
915                        our_addr,
916                        &tun_tx,
917                        &outbound_tx,
918                        &path_mtu_lookup,
919                    ) {
920                        return; // _shutdown_fd closes on drop
921                    }
922                }
923                Ok(_) => break, // No more data
924                Err(e) => {
925                    if e.kind() == std::io::ErrorKind::WouldBlock {
926                        break; // Done for this select round
927                    }
928                    // EBADF is expected during shutdown when the fd is closed
929                    if e.raw_os_error() != Some(libc::EBADF) {
930                        error!(name = %name, error = %e, "TUN read error");
931                    }
932                    return; // _shutdown_fd closes on drop
933                }
934            }
935        }
936    }
937    // _shutdown_fd closes on drop
938}
939
940/// Common setup for TUN reader: allocates buffer, computes max MSS.
941#[cfg(any(target_os = "macos", windows))]
942fn tun_reader_setup(device_name: &str, mtu: u16, transport_mtu: u16) -> (String, Vec<u8>, u16) {
943    tun_reader_setup_with_buffer_len(
944        device_name,
945        mtu,
946        transport_mtu,
947        default_tun_read_buffer_len(mtu),
948    )
949}
950
951#[cfg(any(target_os = "linux", target_os = "macos", windows))]
952fn tun_reader_setup_with_buffer_len(
953    device_name: &str,
954    mtu: u16,
955    transport_mtu: u16,
956    read_buffer_len: usize,
957) -> (String, Vec<u8>, u16) {
958    use super::icmp::effective_ipv6_mtu;
959
960    let name = device_name.to_string();
961    let buf = vec![0u8; read_buffer_len];
962
963    const IPV6_HEADER: u16 = 40;
964    const TCP_HEADER: u16 = 20;
965    let effective_mtu = effective_ipv6_mtu(transport_mtu);
966    let max_mss = effective_mtu
967        .saturating_sub(IPV6_HEADER)
968        .saturating_sub(TCP_HEADER);
969
970    debug!(
971        name = %name,
972        tun_mtu = mtu,
973        transport_mtu = transport_mtu,
974        effective_mtu = effective_mtu,
975        max_mss = max_mss,
976        "TUN reader starting"
977    );
978
979    (name, buf, max_mss)
980}
981
982#[cfg(any(target_os = "linux", target_os = "macos", windows))]
983fn default_tun_read_buffer_len(mtu: u16) -> usize {
984    mtu as usize + 100
985}
986
987/// Process a single TUN packet. Returns `false` if the reader should exit.
988#[cfg(any(target_os = "linux", target_os = "macos", windows))]
989fn handle_tun_packet(
990    packet: &mut [u8],
991    max_mss: u16,
992    name: &str,
993    our_addr: FipsAddress,
994    tun_tx: &TunTx,
995    outbound_tx: &TunOutboundTx,
996    path_mtu_lookup: &PathMtuLookup,
997) -> bool {
998    match prepare_tun_packet(packet, max_mss, name, our_addr, path_mtu_lookup) {
999        TunPacketAction::Forward => {
1000            if outbound_tx
1001                .admit_from_tun_reader(tun_outbound_packet(packet))
1002                .is_err()
1003            {
1004                return false; // Channel closed, shutdown
1005            }
1006        }
1007        TunPacketAction::Icmp(response) => {
1008            if tun_tx.send(response).is_err() {
1009                return false;
1010            }
1011        }
1012        TunPacketAction::Ignore => {}
1013    }
1014    true
1015}
1016
1017#[cfg(target_os = "linux")]
1018fn handle_tun_packet_owned(
1019    mut packet: Vec<u8>,
1020    max_mss: u16,
1021    name: &str,
1022    our_addr: FipsAddress,
1023    tun_tx: &TunTx,
1024    outbound_tx: &TunOutboundTx,
1025    path_mtu_lookup: &PathMtuLookup,
1026) -> bool {
1027    match prepare_tun_packet(&mut packet, max_mss, name, our_addr, path_mtu_lookup) {
1028        TunPacketAction::Forward => {
1029            if outbound_tx
1030                .admit_from_tun_reader(tun_outbound_packet_owned(packet))
1031                .is_err()
1032            {
1033                return false;
1034            }
1035        }
1036        TunPacketAction::Icmp(response) => {
1037            if tun_tx.send(response).is_err() {
1038                return false;
1039            }
1040        }
1041        TunPacketAction::Ignore => {}
1042    }
1043    true
1044}
1045
1046#[cfg(any(target_os = "linux", target_os = "macos", windows))]
1047enum TunPacketAction {
1048    Forward,
1049    Icmp(Vec<u8>),
1050    Ignore,
1051}
1052
1053#[cfg(any(target_os = "linux", target_os = "macos", windows))]
1054fn prepare_tun_packet(
1055    packet: &mut [u8],
1056    max_mss: u16,
1057    name: &str,
1058    our_addr: FipsAddress,
1059    path_mtu_lookup: &PathMtuLookup,
1060) -> TunPacketAction {
1061    use super::icmp::{DestUnreachableCode, build_dest_unreachable, should_send_icmp_error};
1062    use super::tcp_mss::clamp_tcp_mss;
1063
1064    log_ipv6_packet(packet);
1065
1066    if packet.len() < 40 || packet[0] >> 4 != 6 {
1067        return TunPacketAction::Ignore;
1068    }
1069
1070    if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
1071        let effective_max_mss = per_flow_max_mss(path_mtu_lookup, &packet[24..40], max_mss);
1072        if clamp_tcp_mss(packet, effective_max_mss) {
1073            trace!(name = %name, max_mss = effective_max_mss, "Clamped TCP MSS in SYN packet");
1074        }
1075        return TunPacketAction::Forward;
1076    }
1077
1078    if should_send_icmp_error(packet)
1079        && let Some(response) =
1080            build_dest_unreachable(packet, DestUnreachableCode::NoRoute, our_addr.to_ipv6())
1081    {
1082        trace!(name = %name, len = response.len(), "Sending ICMPv6 Destination Unreachable (non-FIPS destination)");
1083        return TunPacketAction::Icmp(response);
1084    }
1085
1086    TunPacketAction::Ignore
1087}
1088
1089#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
1090fn tun_outbound_packet(packet: &[u8]) -> Vec<u8> {
1091    let mut outbound = Vec::with_capacity(
1092        packet
1093            .len()
1094            .saturating_add(TUN_OUTBOUND_PACKET_TAIL_RESERVE),
1095    );
1096    outbound.extend_from_slice(packet);
1097    outbound
1098}
1099
1100#[cfg(any(test, target_os = "linux"))]
1101fn tun_outbound_packet_owned(mut packet: Vec<u8>) -> Vec<u8> {
1102    let needed = packet
1103        .len()
1104        .saturating_add(TUN_OUTBOUND_PACKET_TAIL_RESERVE);
1105    if packet.capacity() < needed {
1106        packet.reserve(needed - packet.capacity());
1107    }
1108    packet
1109}
1110
1111#[cfg(any(target_os = "linux", target_os = "macos"))]
1112impl std::fmt::Debug for TunDevice {
1113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1114        f.debug_struct("TunDevice")
1115            .field("name", &self.name)
1116            .field("mtu", &self.mtu)
1117            .field("address", &self.address)
1118            .finish()
1119    }
1120}
1121
1122/// Log basic information about an IPv6 packet at TRACE level.
1123pub fn log_ipv6_packet(packet: &[u8]) {
1124    if packet.len() < 40 {
1125        debug!(len = packet.len(), "Received undersized packet");
1126        return;
1127    }
1128
1129    let version = packet[0] >> 4;
1130    if version != 6 {
1131        debug!(version, len = packet.len(), "Received non-IPv6 packet");
1132        return;
1133    }
1134
1135    let payload_len = u16::from_be_bytes([packet[4], packet[5]]);
1136    let next_header = packet[6];
1137    let hop_limit = packet[7];
1138
1139    let src = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[8..24]).unwrap());
1140    let dst = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[24..40]).unwrap());
1141
1142    let protocol = match next_header {
1143        6 => "TCP",
1144        17 => "UDP",
1145        58 => "ICMPv6",
1146        _ => "other",
1147    };
1148
1149    trace!("TUN packet received:");
1150    trace!("      src: {}", src);
1151    trace!("      dst: {}", dst);
1152    trace!(" protocol: {} ({})", protocol, next_header);
1153    trace!("  payload: {} bytes, hop_limit: {}", payload_len, hop_limit);
1154}
1155
1156/// Shutdown and delete a TUN interface by name.
1157///
1158/// This deletes the interface, which will cause any blocking reads
1159/// to return an error. Use this for graceful shutdown when the TUN device
1160/// has been moved to another thread.
1161#[cfg(any(target_os = "linux", target_os = "macos"))]
1162pub async fn shutdown_tun_interface(name: &str) -> Result<(), TunError> {
1163    debug!("Shutting down TUN interface {}", name);
1164    platform::delete_interface(name).await?;
1165    debug!("TUN interface {} stopped", name);
1166    Ok(())
1167}
1168
1169// ============================================================================
1170// Platform-specific system TUN modules
1171// ============================================================================
1172
1173#[cfg(windows)]
1174mod windows;
1175#[cfg(windows)]
1176pub(crate) use windows::run_tun_reader;
1177#[cfg(windows)]
1178pub use windows::{TunDevice, TunWriter, shutdown_tun_interface};
1179
1180#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1181mod unsupported;
1182#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1183pub(crate) use unsupported::run_tun_reader;
1184#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1185pub use unsupported::{TunDevice, TunWriter, shutdown_tun_interface};
1186
1187pub(crate) struct TunReaderRuntime {
1188    pub(crate) device: TunDevice,
1189    pub(crate) mtu: u16,
1190    pub(crate) our_addr: FipsAddress,
1191    pub(crate) tun_tx: TunTx,
1192    pub(crate) outbound_tx: TunOutboundTx,
1193    pub(crate) transport_mtu: u16,
1194    pub(crate) path_mtu_lookup: PathMtuLookup,
1195}
1196
1197#[cfg(target_os = "linux")]
1198mod linux_vnet;
1199#[cfg(any(target_os = "linux", target_os = "macos"))]
1200mod platform;
1201
1202#[cfg(test)]
1203mod tests;