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