1use 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
40pub type PathMtuLookup = Arc<RwLock<HashMap<FipsAddress, u16>>>;
46
47#[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 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
140pub type TunTx = mpsc::Sender<Vec<u8>>;
142
143pub type TunOutboundTx = tokio::sync::mpsc::Sender<Vec<u8>>;
145pub type TunOutboundRx = tokio::sync::mpsc::Receiver<Vec<u8>>;
147
148#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
184pub enum TunState {
185 Disabled,
187 Configured,
189 Active,
191 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#[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 pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
228 if platform::is_ipv6_disabled() {
230 return Err(TunError::Ipv6Disabled);
231 }
232
233 let name = config.name();
234 let mtu = config.mtu();
235
236 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 let mut tun_config = tun::Configuration::default();
246
247 #[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 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 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 pub fn name(&self) -> &str {
282 &self.name
283 }
284
285 pub fn mtu(&self) -> u16 {
287 self.mtu
288 }
289
290 pub fn address(&self) -> &FipsAddress {
292 &self.address
293 }
294
295 pub fn device(&self) -> &tun::Device {
297 &self.device
298 }
299
300 pub fn device_mut(&mut self) -> &mut tun::Device {
302 &mut self.device
303 }
304
305 pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
317 self.device.read(buf)
318 }
319
320 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 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 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#[cfg(target_os = "macos")]
375const UTUN_AF_INET6: u32 = 30;
376
377#[cfg(target_os = "macos")]
384#[inline]
385fn utun_af_inet6_header() -> [u8; 4] {
386 UTUN_AF_INET6.to_be_bytes()
387}
388
389#[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#[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 #[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 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 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 #[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 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#[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 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#[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#[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 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 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 if libc::FD_ISSET(shutdown_fd, &read_fds) {
627 debug!(name = %name, "TUN reader received shutdown signal");
628 break;
629 }
630 }
631
632 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; }
647 }
648 Ok(_) => break, Err(e) => {
650 if e.kind() == std::io::ErrorKind::WouldBlock {
651 break; }
653 if e.raw_os_error() != Some(libc::EBADF) {
655 error!(name = %name, error = %e, "TUN read error");
656 }
657 return; }
659 }
660 }
661 }
662 }
664
665#[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#[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 if packet.len() < 40 || packet[0] >> 4 != 6 {
710 return true;
711 }
712
713 if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
715 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; }
724 } else {
725 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
750pub 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#[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#[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;