Skip to main content

fips_core/upper/
tun_runtime.rs

1/// TUN packet reader loop (Linux).
2///
3/// Reads IPv6 packets from the TUN device. Packets destined for FIPS addresses
4/// (fd::/8) are forwarded to the Node via the outbound channel for session
5/// encapsulation and routing. Non-FIPS packets receive ICMPv6 Destination
6/// Unreachable responses.
7///
8/// Also performs TCP MSS clamping on SYN packets to prevent oversized segments.
9///
10/// This is designed to run in a dedicated thread since TUN reads are blocking.
11/// The loop exits when the TUN interface is deleted (EFAULT) or an unrecoverable
12/// error occurs.
13#[cfg(not(target_os = "macos"))]
14#[cfg(any(target_os = "linux", target_os = "macos"))]
15pub(crate) fn run_tun_reader(runtime: TunReaderRuntime) {
16    #[cfg(target_os = "linux")]
17    if runtime.device.vnet_hdr() {
18        run_linux_vnet_tun_reader(runtime);
19        return;
20    }
21
22    let TunReaderRuntime {
23        mut device,
24        mtu,
25        our_addr,
26        tun_tx,
27        outbound_tx,
28        transport_mtu,
29        path_mtu_lookup,
30    } = runtime;
31    let read_buffer_len = device.read_buffer_len(mtu);
32    let (name, mut buf, max_mss) =
33        tun_reader_setup_with_buffer_len(device.name(), mtu, transport_mtu, read_buffer_len);
34
35    loop {
36        match device.read_packet(&mut buf) {
37            Ok(n) if n > 0 => {
38                crate::perf_profile::record_tun_read_packet(n);
39                if !handle_tun_packet(
40                    &mut buf[..n],
41                    max_mss,
42                    &name,
43                    our_addr,
44                    &tun_tx,
45                    &outbound_tx,
46                    &path_mtu_lookup,
47                ) {
48                    break;
49                }
50            }
51            Ok(_) => {}
52            Err(e) => {
53                // EFAULT ("Bad address") is expected during shutdown when the interface is deleted
54                if e.raw_os_error() != Some(libc::EFAULT) {
55                    error!(name = %name, error = %e, "TUN read error");
56                }
57                break;
58            }
59        }
60    }
61}
62
63#[cfg(target_os = "linux")]
64fn run_linux_vnet_tun_reader(runtime: TunReaderRuntime) {
65    let TunReaderRuntime {
66        mut device,
67        mtu,
68        our_addr,
69        tun_tx,
70        outbound_tx,
71        transport_mtu,
72        path_mtu_lookup,
73    } = runtime;
74    let read_buffer_len = device.read_buffer_len(mtu);
75    let (name, mut buf, max_mss) =
76        tun_reader_setup_with_buffer_len(device.name(), mtu, transport_mtu, read_buffer_len);
77    let mut packets = Vec::with_capacity(64);
78
79    loop {
80        packets.clear();
81        match device.read_vnet_packets_into(&mut buf, &mut packets) {
82            Ok(n) if n > 0 => {
83                debug_assert_eq!(n, packets.len());
84                for packet in packets.drain(..) {
85                    crate::perf_profile::record_tun_read_packet(packet.len());
86                    if !handle_tun_packet_owned(
87                        packet,
88                        max_mss,
89                        &name,
90                        our_addr,
91                        &tun_tx,
92                        &outbound_tx,
93                        &path_mtu_lookup,
94                    ) {
95                        return;
96                    }
97                }
98            }
99            Ok(_) => {}
100            Err(e) => {
101                if e.raw_os_error() != Some(libc::EFAULT) {
102                    error!(name = %name, error = %e, "Linux vnet TUN read error");
103                }
104                break;
105            }
106        }
107    }
108}
109
110/// RAII wrapper that closes a raw fd on drop.
111///
112/// Used to ensure the shutdown pipe read-end is always closed when
113/// `run_tun_reader` returns, regardless of which exit path is taken.
114#[cfg(target_os = "macos")]
115struct ShutdownFd(std::os::unix::io::RawFd);
116
117#[cfg(target_os = "macos")]
118impl Drop for ShutdownFd {
119    fn drop(&mut self) {
120        unsafe {
121            libc::close(self.0);
122        }
123    }
124}
125
126/// TUN packet reader loop (macOS).
127///
128/// Uses `select()` to multiplex between the TUN fd and a shutdown pipe,
129/// avoiding the need to close the TUN fd externally (which would cause a
130/// double-close when `TunDevice` drops).
131#[cfg(target_os = "macos")]
132pub(crate) fn run_tun_reader(runtime: TunReaderRuntime, shutdown_fd: std::os::unix::io::RawFd) {
133    let TunReaderRuntime {
134        mut device,
135        mtu,
136        our_addr,
137        tun_tx,
138        outbound_tx,
139        transport_mtu,
140        path_mtu_lookup,
141    } = runtime;
142    let _shutdown_fd = ShutdownFd(shutdown_fd);
143    let tun_fd = device.device().as_raw_fd();
144    let (name, mut buf, max_mss) = tun_reader_setup(device.name(), mtu, transport_mtu);
145
146    // Set TUN fd to non-blocking so we can use select + read without blocking
147    // past the point where select returns readable.
148    unsafe {
149        let flags = libc::fcntl(tun_fd, libc::F_GETFL);
150        if flags >= 0 {
151            libc::fcntl(tun_fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
152        }
153    }
154
155    let nfds = tun_fd.max(shutdown_fd) + 1;
156
157    loop {
158        // Wait for either TUN data or shutdown signal
159        unsafe {
160            let mut read_fds: libc::fd_set = std::mem::zeroed();
161            libc::FD_ZERO(&mut read_fds);
162            libc::FD_SET(tun_fd, &mut read_fds);
163            libc::FD_SET(shutdown_fd, &mut read_fds);
164
165            let ret = libc::select(
166                nfds,
167                &mut read_fds,
168                std::ptr::null_mut(),
169                std::ptr::null_mut(),
170                std::ptr::null_mut(),
171            );
172            if ret < 0 {
173                let err = std::io::Error::last_os_error();
174                if err.kind() == std::io::ErrorKind::Interrupted {
175                    continue;
176                }
177                error!(name = %name, error = %err, "TUN select error");
178                break;
179            }
180
181            // Shutdown signal received
182            if libc::FD_ISSET(shutdown_fd, &read_fds) {
183                debug!(name = %name, "TUN reader received shutdown signal");
184                break;
185            }
186        }
187
188        // TUN fd is readable — drain all available packets
189        loop {
190            match device.read_packet(&mut buf) {
191                Ok(n) if n > 0 => {
192                    crate::perf_profile::record_tun_read_packet(n);
193                    if !handle_tun_packet(
194                        &mut buf[..n],
195                        max_mss,
196                        &name,
197                        our_addr,
198                        &tun_tx,
199                        &outbound_tx,
200                        &path_mtu_lookup,
201                    ) {
202                        return; // _shutdown_fd closes on drop
203                    }
204                }
205                Ok(_) => break, // No more data
206                Err(e) => {
207                    if e.kind() == std::io::ErrorKind::WouldBlock {
208                        break; // Done for this select round
209                    }
210                    // EBADF is expected during shutdown when the fd is closed
211                    if e.raw_os_error() != Some(libc::EBADF) {
212                        error!(name = %name, error = %e, "TUN read error");
213                    }
214                    return; // _shutdown_fd closes on drop
215                }
216            }
217        }
218    }
219    // _shutdown_fd closes on drop
220}
221
222/// Common setup for TUN reader: allocates buffer, computes max MSS.
223#[cfg(any(target_os = "macos", windows))]
224fn tun_reader_setup(device_name: &str, mtu: u16, transport_mtu: u16) -> (String, Vec<u8>, u16) {
225    tun_reader_setup_with_buffer_len(
226        device_name,
227        mtu,
228        transport_mtu,
229        default_tun_read_buffer_len(mtu),
230    )
231}
232
233#[cfg(any(target_os = "linux", target_os = "macos", windows))]
234fn tun_reader_setup_with_buffer_len(
235    device_name: &str,
236    mtu: u16,
237    transport_mtu: u16,
238    read_buffer_len: usize,
239) -> (String, Vec<u8>, u16) {
240    use super::icmp::effective_ipv6_mtu;
241
242    let name = device_name.to_string();
243    let buf = vec![0u8; read_buffer_len];
244
245    const IPV6_HEADER: u16 = 40;
246    const TCP_HEADER: u16 = 20;
247    let effective_mtu = effective_ipv6_mtu(transport_mtu);
248    let max_mss = effective_mtu
249        .saturating_sub(IPV6_HEADER)
250        .saturating_sub(TCP_HEADER);
251
252    debug!(
253        name = %name,
254        tun_mtu = mtu,
255        transport_mtu = transport_mtu,
256        effective_mtu = effective_mtu,
257        max_mss = max_mss,
258        "TUN reader starting"
259    );
260
261    (name, buf, max_mss)
262}
263
264#[cfg(any(target_os = "linux", target_os = "macos", windows))]
265fn default_tun_read_buffer_len(mtu: u16) -> usize {
266    mtu as usize + 100
267}
268
269/// Process a single TUN packet. Returns `false` if the reader should exit.
270#[cfg(any(target_os = "linux", target_os = "macos", windows))]
271fn handle_tun_packet(
272    packet: &mut [u8],
273    max_mss: u16,
274    name: &str,
275    our_addr: FipsAddress,
276    tun_tx: &TunTx,
277    outbound_tx: &TunOutboundTx,
278    path_mtu_lookup: &PathMtuLookup,
279) -> bool {
280    match prepare_tun_packet(packet, max_mss, name, our_addr, path_mtu_lookup) {
281        TunPacketAction::Forward => {
282            if outbound_tx
283                .admit_from_tun_reader(tun_outbound_packet(packet))
284                .is_err()
285            {
286                return false; // Channel closed, shutdown
287            }
288        }
289        TunPacketAction::Icmp(response) => {
290            if tun_tx.send(response).is_err() {
291                return false;
292            }
293        }
294        TunPacketAction::Ignore => {}
295    }
296    true
297}
298
299#[cfg(target_os = "linux")]
300fn handle_tun_packet_owned(
301    mut packet: Vec<u8>,
302    max_mss: u16,
303    name: &str,
304    our_addr: FipsAddress,
305    tun_tx: &TunTx,
306    outbound_tx: &TunOutboundTx,
307    path_mtu_lookup: &PathMtuLookup,
308) -> bool {
309    match prepare_tun_packet(&mut packet, max_mss, name, our_addr, path_mtu_lookup) {
310        TunPacketAction::Forward => {
311            if outbound_tx
312                .admit_from_tun_reader(tun_outbound_packet_owned(packet))
313                .is_err()
314            {
315                return false;
316            }
317        }
318        TunPacketAction::Icmp(response) => {
319            if tun_tx.send(response).is_err() {
320                return false;
321            }
322        }
323        TunPacketAction::Ignore => {}
324    }
325    true
326}
327
328#[cfg(any(target_os = "linux", target_os = "macos", windows))]
329enum TunPacketAction {
330    Forward,
331    Icmp(Vec<u8>),
332    Ignore,
333}
334
335#[cfg(any(target_os = "linux", target_os = "macos", windows))]
336fn prepare_tun_packet(
337    packet: &mut [u8],
338    max_mss: u16,
339    name: &str,
340    our_addr: FipsAddress,
341    path_mtu_lookup: &PathMtuLookup,
342) -> TunPacketAction {
343    use super::icmp::{DestUnreachableCode, build_dest_unreachable, should_send_icmp_error};
344    use super::tcp_mss::clamp_tcp_mss;
345
346    log_ipv6_packet(packet);
347
348    if packet.len() < 40 || packet[0] >> 4 != 6 {
349        return TunPacketAction::Ignore;
350    }
351
352    if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
353        let effective_max_mss = per_flow_max_mss(path_mtu_lookup, &packet[24..40], max_mss);
354        if clamp_tcp_mss(packet, effective_max_mss) {
355            trace!(name = %name, max_mss = effective_max_mss, "Clamped TCP MSS in SYN packet");
356        }
357        return TunPacketAction::Forward;
358    }
359
360    if should_send_icmp_error(packet)
361        && let Some(response) =
362            build_dest_unreachable(packet, DestUnreachableCode::NoRoute, our_addr.to_ipv6())
363    {
364        trace!(name = %name, len = response.len(), "Sending ICMPv6 Destination Unreachable (non-FIPS destination)");
365        return TunPacketAction::Icmp(response);
366    }
367
368    TunPacketAction::Ignore
369}
370
371#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
372fn tun_outbound_packet(packet: &[u8]) -> Vec<u8> {
373    let mut outbound = Vec::with_capacity(
374        packet
375            .len()
376            .saturating_add(TUN_OUTBOUND_PACKET_TAIL_RESERVE),
377    );
378    outbound.extend_from_slice(packet);
379    outbound
380}
381
382#[cfg(any(test, target_os = "linux"))]
383fn tun_outbound_packet_owned(mut packet: Vec<u8>) -> Vec<u8> {
384    let needed = packet
385        .len()
386        .saturating_add(TUN_OUTBOUND_PACKET_TAIL_RESERVE);
387    if packet.capacity() < needed {
388        packet.reserve(needed - packet.capacity());
389    }
390    packet
391}
392
393#[cfg(any(target_os = "linux", target_os = "macos"))]
394impl std::fmt::Debug for TunDevice {
395    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
396        f.debug_struct("TunDevice")
397            .field("name", &self.name)
398            .field("mtu", &self.mtu)
399            .field("address", &self.address)
400            .finish()
401    }
402}
403
404/// Log basic information about an IPv6 packet at TRACE level.
405pub fn log_ipv6_packet(packet: &[u8]) {
406    if packet.len() < 40 {
407        debug!(len = packet.len(), "Received undersized packet");
408        return;
409    }
410
411    let version = packet[0] >> 4;
412    if version != 6 {
413        debug!(version, len = packet.len(), "Received non-IPv6 packet");
414        return;
415    }
416
417    let payload_len = u16::from_be_bytes([packet[4], packet[5]]);
418    let next_header = packet[6];
419    let hop_limit = packet[7];
420
421    let src = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[8..24]).unwrap());
422    let dst = Ipv6Addr::from(<[u8; 16]>::try_from(&packet[24..40]).unwrap());
423
424    let protocol = match next_header {
425        6 => "TCP",
426        17 => "UDP",
427        58 => "ICMPv6",
428        _ => "other",
429    };
430
431    trace!("TUN packet received:");
432    trace!("      src: {}", src);
433    trace!("      dst: {}", dst);
434    trace!(" protocol: {} ({})", protocol, next_header);
435    trace!("  payload: {} bytes, hop_limit: {}", payload_len, hop_limit);
436}
437
438/// Shutdown and delete a TUN interface by name.
439///
440/// This deletes the interface, which will cause any blocking reads
441/// to return an error. Use this for graceful shutdown when the TUN device
442/// has been moved to another thread.
443#[cfg(any(target_os = "linux", target_os = "macos"))]
444pub async fn shutdown_tun_interface(name: &str) -> Result<(), TunError> {
445    debug!("Shutting down TUN interface {}", name);
446    platform::delete_interface(name).await?;
447    debug!("TUN interface {} stopped", name);
448    Ok(())
449}
450
451// ============================================================================
452// Platform-specific system TUN modules
453// ============================================================================
454
455#[cfg(windows)]
456pub(crate) use windows::run_tun_reader;
457#[cfg(windows)]
458pub use windows::{TunDevice, TunWriter, shutdown_tun_interface};
459
460#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
461pub(crate) use unsupported::run_tun_reader;
462#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
463pub use unsupported::{TunDevice, TunWriter, shutdown_tun_interface};
464
465pub(crate) struct TunReaderRuntime {
466    pub(crate) device: TunDevice,
467    pub(crate) mtu: u16,
468    pub(crate) our_addr: FipsAddress,
469    pub(crate) tun_tx: TunTx,
470    pub(crate) outbound_tx: TunOutboundTx,
471    pub(crate) transport_mtu: u16,
472    pub(crate) path_mtu_lookup: PathMtuLookup,
473}