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};
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
47pub type PathMtuLookup = Arc<RwLock<HashMap<FipsAddress, u16>>>;
53
54#[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 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum TunState {
184 Disabled,
186 Configured,
188 Active,
190 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#[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 pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
227 if platform::is_ipv6_disabled() {
229 return Err(TunError::Ipv6Disabled);
230 }
231
232 let name = config.name();
233 let mtu = config.mtu();
234
235 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 let mut tun_config = tun::Configuration::default();
245
246 #[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 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 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 pub fn name(&self) -> &str {
281 &self.name
282 }
283
284 pub fn mtu(&self) -> u16 {
286 self.mtu
287 }
288
289 pub fn address(&self) -> &FipsAddress {
291 &self.address
292 }
293
294 pub fn device(&self) -> &tun::Device {
296 &self.device
297 }
298
299 pub fn device_mut(&mut self) -> &mut tun::Device {
301 &mut self.device
302 }
303
304 pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
316 self.device.read(buf)
317 }
318
319 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 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 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#[cfg(target_os = "macos")]
374const UTUN_AF_INET6: u32 = 30;
375
376#[cfg(target_os = "macos")]
383#[inline]
384fn utun_af_inet6_header() -> [u8; 4] {
385 UTUN_AF_INET6.to_be_bytes()
386}
387
388#[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#[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 #[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 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 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 #[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 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#[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 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#[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#[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 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 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 if libc::FD_ISSET(shutdown_fd, &read_fds) {
630 debug!(name = %name, "TUN reader received shutdown signal");
631 break;
632 }
633 }
634
635 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; }
650 }
651 Ok(_) => break, Err(e) => {
653 if e.kind() == std::io::ErrorKind::WouldBlock {
654 break; }
656 if e.raw_os_error() != Some(libc::EBADF) {
658 error!(name = %name, error = %e, "TUN read error");
659 }
660 return; }
662 }
663 }
664 }
665 }
667
668#[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#[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 if packet.len() < 40 || packet[0] >> 4 != 6 {
713 return true;
714 }
715
716 if packet[24] == crate::identity::FIPS_ADDRESS_PREFIX {
718 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, }
728 } else {
729 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
754pub 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#[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#[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;