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, mpsc};
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/// Read-only handle to the per-destination path MTU map. Populated by
41/// the discovery handler on `LookupResponse`; read by the TUN reader
42/// (outbound clamp) and writer (inbound clamp) at TCP MSS clamp time.
43/// Keyed by [`FipsAddress`] (16 bytes, the IPv6 form of a fips peer
44/// address).
45pub type PathMtuLookup = Arc<RwLock<HashMap<FipsAddress, u16>>>;
46
47/// Compute the effective TCP MSS ceiling for a packet given its peer
48/// address bytes (a 16-byte IPv6 destination on outbound, source on
49/// inbound). Returns `min(global_max_mss, learned_path_max_mss)` when
50/// the per-destination path MTU is known via discovery; otherwise
51/// returns `min(global_max_mss, ipv6_minimum_safe_max_mss)`, the
52/// conservative IPv6-minimum-derived ceiling.
53///
54/// The conservative empty-lookup fallback exists because there is a
55/// race window between TCP-SYN-out and discovery-completes-with-path-
56/// MTU on cold flows. Without the floor, the first SYN exits at the
57/// kernel-natural MSS (TUN MTU minus IPv6/TCP headers), which can
58/// exceed what some downstream forwarder hop is willing to carry.
59/// The drop is silent (no PTB feedback through the userspace TUN to
60/// the kernel TCP stack), so TCP retransmits at the same too-large
61/// MSS and the application's first connection wedges before discovery
62/// completes for a corrected second SYN to fire.
63///
64/// RFC 8200 mandates every IPv6 path accepts at least 1280-byte
65/// packets, so a SYN clamped to the IPv6-minimum-derived MSS fits
66/// any compliant path. Subsequent flows pick up the actual learned
67/// per-destination value, which can be larger (when path supports
68/// it) or smaller (when path is observed-tighter than the IPv6 min).
69///
70/// Path MTU bytes-on-wire to TCP MSS: subtract 77 bytes of FIPS encap
71/// overhead, then 40 bytes IPv6 + 20 bytes TCP headers.
72#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
73pub(crate) fn per_flow_max_mss(
74    lookup: &PathMtuLookup,
75    addr_bytes: &[u8],
76    global_max_mss: u16,
77) -> u16 {
78    use super::icmp::effective_ipv6_mtu;
79
80    // RFC 8200 IPv6-minimum MTU (1280) → effective FIPS-encapsulated
81    // payload (1203) → TCP segment after IPv6+TCP headers (1143).
82    // Used as the conservative ceiling for empty-lookup destinations.
83    const IPV6_MIN_MTU: u16 = 1280;
84    let conservative_max_mss = effective_ipv6_mtu(IPV6_MIN_MTU)
85        .saturating_sub(40)
86        .saturating_sub(20);
87    let empty_lookup_ceiling = std::cmp::min(global_max_mss, conservative_max_mss);
88
89    if addr_bytes.len() != 16 {
90        trace!(
91            len = addr_bytes.len(),
92            global_max_mss,
93            empty_lookup_ceiling,
94            "per_flow_max_mss: addr_bytes wrong length, fall back to conservative ceiling"
95        );
96        return empty_lookup_ceiling;
97    }
98    let Ok(fips_addr) = FipsAddress::from_slice(addr_bytes) else {
99        trace!(
100            global_max_mss,
101            empty_lookup_ceiling,
102            "per_flow_max_mss: FipsAddress::from_slice rejected (non-fd::/8 prefix), fall back to conservative ceiling"
103        );
104        return empty_lookup_ceiling;
105    };
106    let Ok(map) = lookup.read() else {
107        trace!(
108            fips_addr = %fips_addr,
109            global_max_mss,
110            empty_lookup_ceiling,
111            "per_flow_max_mss: lookup read lock poisoned, fall back to conservative ceiling"
112        );
113        return empty_lookup_ceiling;
114    };
115    let Some(&path_mtu) = map.get(&fips_addr) else {
116        trace!(
117            fips_addr = %fips_addr,
118            global_max_mss,
119            empty_lookup_ceiling,
120            map_len = map.len(),
121            "per_flow_max_mss: no path_mtu_lookup entry for destination, fall back to conservative ceiling"
122        );
123        return empty_lookup_ceiling;
124    };
125    let path_max_mss = effective_ipv6_mtu(path_mtu)
126        .saturating_sub(40)
127        .saturating_sub(20);
128    let result = std::cmp::min(global_max_mss, path_max_mss);
129    trace!(
130        fips_addr = %fips_addr,
131        path_mtu,
132        path_max_mss,
133        global_max_mss,
134        result,
135        "per_flow_max_mss: per-destination clamp applied"
136    );
137    result
138}
139
140/// Channel sender for packets to be written to TUN.
141pub type TunTx = mpsc::Sender<Vec<u8>>;
142
143/// Channel sender for outbound packets from TUN reader to Node.
144pub type TunOutboundTx = tokio::sync::mpsc::Sender<Vec<u8>>;
145/// Channel receiver for outbound packets (consumed by Node's RX loop).
146pub type TunOutboundRx = tokio::sync::mpsc::Receiver<Vec<u8>>;
147
148/// Errors that can occur with TUN operations.
149#[derive(Debug, Error)]
150pub enum TunError {
151    #[error("failed to create TUN device: {0}")]
152    Create(#[source] Box<dyn std::error::Error + Send + Sync>),
153
154    #[error("failed to configure TUN device: {0}")]
155    Configure(String),
156
157    #[cfg(target_os = "linux")]
158    #[error("netlink error: {0}")]
159    Netlink(#[from] rtnetlink::Error),
160
161    #[error("interface not found: {0}")]
162    InterfaceNotFound(String),
163
164    #[error("permission denied: {0}")]
165    PermissionDenied(String),
166
167    #[cfg(any(target_os = "linux", target_os = "macos"))]
168    #[error("IPv6 is disabled (set net.ipv6.conf.all.disable_ipv6=0)")]
169    Ipv6Disabled,
170
171    #[error("system TUN is not supported on this platform")]
172    UnsupportedPlatform,
173}
174
175#[cfg(any(target_os = "linux", target_os = "macos"))]
176impl From<tun::Error> for TunError {
177    fn from(e: tun::Error) -> Self {
178        TunError::Create(Box::new(e))
179    }
180}
181
182/// TUN device state.
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum TunState {
185    /// TUN is disabled in configuration.
186    Disabled,
187    /// TUN is configured but not yet created.
188    Configured,
189    /// TUN device is active and ready.
190    Active,
191    /// TUN device failed to initialize.
192    Failed,
193}
194
195impl std::fmt::Display for TunState {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        match self {
198            TunState::Disabled => write!(f, "disabled"),
199            TunState::Configured => write!(f, "configured"),
200            TunState::Active => write!(f, "active"),
201            TunState::Failed => write!(f, "failed"),
202        }
203    }
204}
205
206// ============================================================================
207// Unix (Linux + macOS) TUN implementation
208// ============================================================================
209
210/// FIPS TUN device wrapper.
211#[cfg(any(target_os = "linux", target_os = "macos"))]
212pub struct TunDevice {
213    device: tun::Device,
214    name: String,
215    mtu: u16,
216    address: FipsAddress,
217}
218
219#[cfg(any(target_os = "linux", target_os = "macos"))]
220impl TunDevice {
221    /// Create or open a TUN device.
222    ///
223    /// If the interface already exists, opens it and reconfigures it.
224    /// Otherwise, creates a new TUN device.
225    ///
226    /// This requires CAP_NET_ADMIN capability (run with sudo or setcap).
227    pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
228        // Check if IPv6 is enabled
229        if platform::is_ipv6_disabled() {
230            return Err(TunError::Ipv6Disabled);
231        }
232
233        let name = config.name();
234        let mtu = config.mtu();
235
236        // Delete existing interface if present (TUN devices are exclusive)
237        if platform::interface_exists(name).await {
238            debug!(name, "Deleting existing TUN interface");
239            if let Err(e) = platform::delete_interface(name).await {
240                debug!(name, error = %e, "Failed to delete existing interface");
241            }
242        }
243
244        // Create the TUN device
245        let mut tun_config = tun::Configuration::default();
246
247        // On macOS, utun devices get kernel-assigned names (utun0, utun1, ...),
248        // so we skip setting the name and read it back after creation.
249        #[cfg(target_os = "linux")]
250        #[allow(deprecated)]
251        tun_config.name(name).layer(Layer::L3).mtu(mtu);
252
253        #[cfg(target_os = "macos")]
254        {
255            #[allow(deprecated)]
256            tun_config.layer(Layer::L3).mtu(mtu);
257        }
258
259        let device = tun::create(&tun_config)?;
260
261        // Read the actual device name (on macOS this is the kernel-assigned utun* name)
262        let actual_name = {
263            use tun::AbstractDevice;
264            device
265                .tun_name()
266                .map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
267        };
268
269        // Configure address and bring up via platform-specific method
270        platform::configure_interface(&actual_name, address.to_ipv6(), mtu).await?;
271
272        Ok(Self {
273            device,
274            name: actual_name,
275            mtu,
276            address,
277        })
278    }
279
280    /// Get the device name.
281    pub fn name(&self) -> &str {
282        &self.name
283    }
284
285    /// Get the configured MTU.
286    pub fn mtu(&self) -> u16 {
287        self.mtu
288    }
289
290    /// Get the FIPS address assigned to this device.
291    pub fn address(&self) -> &FipsAddress {
292        &self.address
293    }
294
295    /// Get a reference to the underlying tun::Device.
296    pub fn device(&self) -> &tun::Device {
297        &self.device
298    }
299
300    /// Get a mutable reference to the underlying tun::Device.
301    pub fn device_mut(&mut self) -> &mut tun::Device {
302        &mut self.device
303    }
304
305    /// Read a packet from the TUN device.
306    ///
307    /// Returns the number of bytes read into the buffer, or an `io::Error`.
308    /// The buffer should be at least MTU + header size (typically 1500+ bytes).
309    ///
310    /// The tun crate's `Read` impl transparently strips the macOS utun
311    /// packet information header, so this returns a raw IP packet on all
312    /// platforms.
313    ///
314    /// The raw `io::Error` is returned so callers can inspect `ErrorKind`
315    /// (e.g. `WouldBlock`) or `raw_os_error()` without string matching.
316    pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
317        self.device.read(buf)
318    }
319
320    /// Shutdown and delete the TUN device.
321    ///
322    /// This deletes the interface entirely.
323    pub async fn shutdown(&self) -> Result<(), TunError> {
324        debug!(name = %self.name, "Deleting TUN device");
325        platform::delete_interface(&self.name).await
326    }
327
328    /// Create a TunWriter for this device.
329    ///
330    /// This duplicates the underlying file descriptor so that reads and writes
331    /// can happen independently on separate threads. Returns the writer and
332    /// a channel sender for submitting packets to be written.
333    ///
334    /// `max_mss` is the global TCP MSS ceiling derived from the local
335    /// `transport_mtu()` floor. `path_mtu_lookup` is a read-only handle to
336    /// the per-destination path MTU map populated by discovery; the writer
337    /// reads it on each inbound SYN-ACK to compute a per-flow ceiling that
338    /// honors learned narrow paths through the mesh.
339    pub fn create_writer(
340        &self,
341        max_mss: u16,
342        path_mtu_lookup: PathMtuLookup,
343    ) -> Result<(TunWriter, TunTx), TunError> {
344        let fd = self.device.as_raw_fd();
345
346        // Duplicate the file descriptor for writing
347        let write_fd = unsafe { libc::dup(fd) };
348        if write_fd < 0 {
349            return Err(TunError::Configure(format!(
350                "failed to dup fd: {}",
351                std::io::Error::last_os_error()
352            )));
353        }
354
355        let write_file = unsafe { File::from_raw_fd(write_fd) };
356        let (tx, rx) = mpsc::channel();
357
358        Ok((
359            TunWriter {
360                file: write_file,
361                rx,
362                name: self.name.clone(),
363                max_mss,
364                path_mtu_lookup,
365            },
366            tx,
367        ))
368    }
369}
370
371/// macOS utun protocol family value for IPv6 (matches `<sys/socket.h>`
372/// `AF_INET6` on Darwin). Used as the 4-byte big-endian packet-info
373/// header prepended to every utun frame.
374#[cfg(target_os = "macos")]
375const UTUN_AF_INET6: u32 = 30;
376
377/// Build the 4-byte big-endian utun packet-info header for an IPv6 frame.
378///
379/// utun devices on macOS require a 4-byte address-family prefix on every
380/// frame: a single big-endian `u32` carrying the protocol family. For
381/// IPv6 traffic (the only family FIPS sends) this is `AF_INET6 = 30`,
382/// which serializes as `[0x00, 0x00, 0x00, 0x1e]`.
383#[cfg(target_os = "macos")]
384#[inline]
385fn utun_af_inet6_header() -> [u8; 4] {
386    UTUN_AF_INET6.to_be_bytes()
387}
388
389/// Parse the 4-byte big-endian utun packet-info header.
390///
391/// Returns the address-family value (`AF_INET6 = 30` for IPv6 frames),
392/// or `None` if the buffer is shorter than the 4-byte header. The `tun`
393/// crate's `Read` impl strips this transparently for us in the read
394/// path; this helper exists for round-trip testability with
395/// [`utun_af_inet6_header`] and for any future code path that reads
396/// from the dup'd fd directly.
397#[cfg(all(test, target_os = "macos"))]
398#[inline]
399fn parse_utun_af_prefix(buf: &[u8]) -> Option<u32> {
400    if buf.len() < 4 {
401        return None;
402    }
403    Some(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
404}
405
406/// Writer thread for TUN device.
407///
408/// Services a queue of outbound packets and writes them to the TUN device.
409/// Multiple producers can send packets via the TunTx channel.
410///
411/// Also performs TCP MSS clamping on inbound SYN-ACK packets.
412#[cfg(any(target_os = "linux", target_os = "macos"))]
413pub struct TunWriter {
414    file: File,
415    rx: mpsc::Receiver<Vec<u8>>,
416    name: String,
417    max_mss: u16,
418    path_mtu_lookup: PathMtuLookup,
419}
420
421#[cfg(any(target_os = "linux", target_os = "macos"))]
422impl TunWriter {
423    /// Run the writer loop.
424    ///
425    /// Blocks forever, reading packets from the channel and writing them
426    /// to the TUN device. Returns when the channel is closed (all senders dropped).
427    #[cfg_attr(target_os = "macos", allow(unused_mut))]
428    pub fn run(mut self) {
429        use super::tcp_mss::clamp_tcp_mss;
430
431        debug!(name = %self.name, max_mss = self.max_mss, "TUN writer starting");
432
433        for mut packet in self.rx {
434            // Per-destination clamp: peer IPv6 source address (bytes 8..24)
435            // identifies the flow's remote end. If discovery has learned a
436            // smaller path MTU for that peer, tighten the ceiling.
437            let effective_max_mss = if packet.len() >= 24 {
438                per_flow_max_mss(&self.path_mtu_lookup, &packet[8..24], self.max_mss)
439            } else {
440                self.max_mss
441            };
442            // Clamp TCP MSS on inbound SYN-ACK packets
443            if clamp_tcp_mss(&mut packet, effective_max_mss) {
444                trace!(
445                    name = %self.name,
446                    max_mss = effective_max_mss,
447                    "Clamped TCP MSS in inbound SYN-ACK packet"
448                );
449            }
450
451            // On macOS, utun devices require a 4-byte packet information header
452            // prepended to each packet. The tun crate handles this for its own
453            // Read/Write impl, but we use a dup'd fd directly. We use writev
454            // to avoid allocating a buffer on every packet.
455            #[cfg(target_os = "macos")]
456            let write_result = {
457                use std::os::unix::io::AsRawFd;
458                let af_header = utun_af_inet6_header();
459                let iov = [
460                    libc::iovec {
461                        iov_base: af_header.as_ptr() as *mut libc::c_void,
462                        iov_len: 4,
463                    },
464                    libc::iovec {
465                        iov_base: packet.as_ptr() as *mut libc::c_void,
466                        iov_len: packet.len(),
467                    },
468                ];
469                let ret = unsafe { libc::writev(self.file.as_raw_fd(), iov.as_ptr(), 2) };
470                if ret < 0 {
471                    Err(std::io::Error::last_os_error())
472                } else {
473                    let expected = 4 + packet.len();
474                    if (ret as usize) < expected {
475                        Err(std::io::Error::new(
476                            std::io::ErrorKind::WriteZero,
477                            format!("short writev: {} of {} bytes", ret, expected),
478                        ))
479                    } else {
480                        Ok(())
481                    }
482                }
483            };
484            #[cfg(not(target_os = "macos"))]
485            let write_result = self.file.write_all(&packet);
486
487            if let Err(e) = write_result {
488                // "Bad address" is expected during shutdown when interface is deleted
489                let err_str = e.to_string();
490                if err_str.contains("Bad address") {
491                    break;
492                }
493                error!(name = %self.name, error = %e, "TUN write error");
494            } else {
495                trace!(name = %self.name, len = packet.len(), "TUN packet written");
496            }
497        }
498    }
499}
500
501/// TUN packet reader loop (Linux).
502///
503/// Reads IPv6 packets from the TUN device. Packets destined for FIPS addresses
504/// (fd::/8) are forwarded to the Node via the outbound channel for session
505/// encapsulation and routing. Non-FIPS packets receive ICMPv6 Destination
506/// Unreachable responses.
507///
508/// Also performs TCP MSS clamping on SYN packets to prevent oversized segments.
509///
510/// This is designed to run in a dedicated thread since TUN reads are blocking.
511/// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable
512/// error occurs.
513#[cfg(not(target_os = "macos"))]
514#[cfg(any(target_os = "linux", target_os = "macos"))]
515pub fn run_tun_reader(
516    mut device: TunDevice,
517    mtu: u16,
518    our_addr: FipsAddress,
519    tun_tx: TunTx,
520    outbound_tx: TunOutboundTx,
521    transport_mtu: u16,
522    path_mtu_lookup: PathMtuLookup,
523) {
524    let (name, mut buf, max_mss) = tun_reader_setup(device.name(), mtu, transport_mtu);
525
526    loop {
527        match device.read_packet(&mut buf) {
528            Ok(n) if n > 0 => {
529                if !handle_tun_packet(
530                    &mut buf[..n],
531                    max_mss,
532                    &name,
533                    our_addr,
534                    &tun_tx,
535                    &outbound_tx,
536                    &path_mtu_lookup,
537                ) {
538                    break;
539                }
540            }
541            Ok(_) => {}
542            Err(e) => {
543                // EFAULT ("Bad address") is expected during shutdown when the interface is deleted
544                if e.raw_os_error() != Some(libc::EFAULT) {
545                    error!(name = %name, error = %e, "TUN read error");
546                }
547                break;
548            }
549        }
550    }
551}
552
553/// RAII wrapper that closes a raw fd on drop.
554///
555/// Used to ensure the shutdown pipe read-end is always closed when
556/// `run_tun_reader` returns, regardless of which exit path is taken.
557#[cfg(target_os = "macos")]
558struct ShutdownFd(std::os::unix::io::RawFd);
559
560#[cfg(target_os = "macos")]
561impl Drop for ShutdownFd {
562    fn drop(&mut self) {
563        unsafe {
564            libc::close(self.0);
565        }
566    }
567}
568
569/// TUN packet reader loop (macOS).
570///
571/// Uses `select()` to multiplex between the TUN fd and a shutdown pipe,
572/// avoiding the need to close the TUN fd externally (which would cause a
573/// double-close when `TunDevice` drops).
574#[cfg(target_os = "macos")]
575#[allow(clippy::too_many_arguments)]
576pub fn run_tun_reader(
577    mut device: TunDevice,
578    mtu: u16,
579    our_addr: FipsAddress,
580    tun_tx: TunTx,
581    outbound_tx: TunOutboundTx,
582    transport_mtu: u16,
583    path_mtu_lookup: PathMtuLookup,
584    shutdown_fd: std::os::unix::io::RawFd,
585) {
586    let _shutdown_fd = ShutdownFd(shutdown_fd);
587    let tun_fd = device.device().as_raw_fd();
588    let (name, mut buf, max_mss) = tun_reader_setup(device.name(), mtu, transport_mtu);
589
590    // Set TUN fd to non-blocking so we can use select + read without blocking
591    // past the point where select returns readable.
592    unsafe {
593        let flags = libc::fcntl(tun_fd, libc::F_GETFL);
594        if flags >= 0 {
595            libc::fcntl(tun_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
596        }
597    }
598
599    let nfds = tun_fd.max(shutdown_fd) + 1;
600
601    loop {
602        // Wait for either TUN data or shutdown signal
603        unsafe {
604            let mut read_fds: libc::fd_set = std::mem::zeroed();
605            libc::FD_ZERO(&mut read_fds);
606            libc::FD_SET(tun_fd, &mut read_fds);
607            libc::FD_SET(shutdown_fd, &mut read_fds);
608
609            let ret = libc::select(
610                nfds,
611                &mut read_fds,
612                std::ptr::null_mut(),
613                std::ptr::null_mut(),
614                std::ptr::null_mut(),
615            );
616            if ret < 0 {
617                let err = std::io::Error::last_os_error();
618                if err.kind() == std::io::ErrorKind::Interrupted {
619                    continue;
620                }
621                error!(name = %name, error = %err, "TUN select error");
622                break;
623            }
624
625            // Shutdown signal received
626            if libc::FD_ISSET(shutdown_fd, &read_fds) {
627                debug!(name = %name, "TUN reader received shutdown signal");
628                break;
629            }
630        }
631
632        // TUN fd is readable — drain all available packets
633        loop {
634            match device.read_packet(&mut buf) {
635                Ok(n) if n > 0 => {
636                    if !handle_tun_packet(
637                        &mut buf[..n],
638                        max_mss,
639                        &name,
640                        our_addr,
641                        &tun_tx,
642                        &outbound_tx,
643                        &path_mtu_lookup,
644                    ) {
645                        return; // _shutdown_fd closes on drop
646                    }
647                }
648                Ok(_) => break, // No more data
649                Err(e) => {
650                    if e.kind() == std::io::ErrorKind::WouldBlock {
651                        break; // Done for this select round
652                    }
653                    // EBADF is expected during shutdown when the fd is closed
654                    if e.raw_os_error() != Some(libc::EBADF) {
655                        error!(name = %name, error = %e, "TUN read error");
656                    }
657                    return; // _shutdown_fd closes on drop
658                }
659            }
660        }
661    }
662    // _shutdown_fd closes on drop
663}
664
665/// Common setup for TUN reader: allocates buffer, computes max MSS.
666#[cfg(any(target_os = "linux", target_os = "macos", windows))]
667fn tun_reader_setup(device_name: &str, mtu: u16, transport_mtu: u16) -> (String, Vec<u8>, u16) {
668    use super::icmp::effective_ipv6_mtu;
669
670    let name = device_name.to_string();
671    let buf = vec![0u8; mtu as usize + 100];
672
673    const IPV6_HEADER: u16 = 40;
674    const TCP_HEADER: u16 = 20;
675    let effective_mtu = effective_ipv6_mtu(transport_mtu);
676    let max_mss = effective_mtu
677        .saturating_sub(IPV6_HEADER)
678        .saturating_sub(TCP_HEADER);
679
680    debug!(
681        name = %name,
682        tun_mtu = mtu,
683        transport_mtu = transport_mtu,
684        effective_mtu = effective_mtu,
685        max_mss = max_mss,
686        "TUN reader starting"
687    );
688
689    (name, buf, max_mss)
690}
691
692/// Process a single TUN packet. Returns `false` if the reader should exit.
693#[cfg(any(target_os = "linux", target_os = "macos", windows))]
694fn handle_tun_packet(
695    packet: &mut [u8],
696    max_mss: u16,
697    name: &str,
698    our_addr: FipsAddress,
699    tun_tx: &TunTx,
700    outbound_tx: &TunOutboundTx,
701    path_mtu_lookup: &PathMtuLookup,
702) -> bool {
703    use super::icmp::{DestUnreachableCode, build_dest_unreachable, should_send_icmp_error};
704    use super::tcp_mss::clamp_tcp_mss;
705
706    log_ipv6_packet(packet);
707
708    // Must be a valid IPv6 packet
709    if packet.len() < 40 || packet[0] >> 4 != 6 {
710        return true;
711    }
712
713    // Check if destination is a FIPS address (fd::/8 prefix)
714    if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
715        // Per-destination clamp: if discovery has learned a smaller path
716        // MTU for this destination, tighten the ceiling for this flow.
717        let effective_max_mss = per_flow_max_mss(path_mtu_lookup, &packet[24..40], max_mss);
718        if clamp_tcp_mss(packet, effective_max_mss) {
719            trace!(name = %name, max_mss = effective_max_mss, "Clamped TCP MSS in SYN packet");
720        }
721        if outbound_tx.blocking_send(packet.to_vec()).is_err() {
722            return false; // Channel closed, shutdown
723        }
724    } else {
725        // Non-FIPS destination: send ICMPv6 Destination Unreachable
726        if should_send_icmp_error(packet)
727            && let Some(response) =
728                build_dest_unreachable(packet, DestUnreachableCode::NoRoute, our_addr.to_ipv6())
729        {
730            trace!(name = %name, len = response.len(), "Sending ICMPv6 Destination Unreachable (non-FIPS destination)");
731            if tun_tx.send(response).is_err() {
732                return false;
733            }
734        }
735    }
736    true
737}
738
739#[cfg(any(target_os = "linux", target_os = "macos"))]
740impl std::fmt::Debug for TunDevice {
741    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
742        f.debug_struct("TunDevice")
743            .field("name", &self.name)
744            .field("mtu", &self.mtu)
745            .field("address", &self.address)
746            .finish()
747    }
748}
749
750/// Log basic information about an IPv6 packet at TRACE level.
751pub fn log_ipv6_packet(packet: &[u8]) {
752    if packet.len() < 40 {
753        debug!(len = packet.len(), "Received undersized packet");
754        return;
755    }
756
757    let version = packet[0] >> 4;
758    if version != 6 {
759        debug!(version, len = packet.len(), "Received non-IPv6 packet");
760        return;
761    }
762
763    let payload_len = u16::from_be_bytes([packet[4], packet[5]]);
764    let next_header = packet[6];
765    let hop_limit = packet[7];
766
767    let src = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[8..24]).unwrap());
768    let dst = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[24..40]).unwrap());
769
770    let protocol = match next_header {
771        6 => "TCP",
772        17 => "UDP",
773        58 => "ICMPv6",
774        _ => "other",
775    };
776
777    trace!("TUN packet received:");
778    trace!("      src: {}", src);
779    trace!("      dst: {}", dst);
780    trace!(" protocol: {} ({})", protocol, next_header);
781    trace!("  payload: {} bytes, hop_limit: {}", payload_len, hop_limit);
782}
783
784/// Shutdown and delete a TUN interface by name.
785///
786/// This deletes the interface, which will cause any blocking reads
787/// to return an error. Use this for graceful shutdown when the TUN device
788/// has been moved to another thread.
789#[cfg(any(target_os = "linux", target_os = "macos"))]
790pub async fn shutdown_tun_interface(name: &str) -> Result<(), TunError> {
791    debug!("Shutting down TUN interface {}", name);
792    platform::delete_interface(name).await?;
793    debug!("TUN interface {} stopped", name);
794    Ok(())
795}
796
797// ============================================================================
798// Platform-specific system TUN modules
799// ============================================================================
800
801#[cfg(windows)]
802mod windows;
803#[cfg(windows)]
804pub use windows::{TunDevice, TunWriter, run_tun_reader, shutdown_tun_interface};
805
806#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
807mod unsupported;
808#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
809pub use unsupported::{TunDevice, TunWriter, run_tun_reader, shutdown_tun_interface};
810
811#[cfg(any(target_os = "linux", target_os = "macos"))]
812mod platform;
813
814#[cfg(test)]
815mod tests;