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
40#[cfg(target_os = "linux")]
41use self::linux_vnet::{LinuxVnetTun, linux_vnet_tun_enabled};
42
43pub(crate) use super::tun_outbound::tun_outbound_channel;
44pub use super::tun_outbound::{TunOutboundRx, TunOutboundTx};
45#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
46pub(crate) use super::tun_write::TunRx;
47pub use super::tun_write::TunTx;
48#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
49pub(crate) use super::tun_write::write_channel;
50
51pub type PathMtuLookup = Arc<RwLock<HashMap<FipsAddress, u16>>>;
57
58#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
59const TUN_OUTBOUND_PACKET_TAIL_RESERVE: usize = 128;
60
61#[cfg(any(test, target_os = "linux", target_os = "macos", windows))]
87pub(crate) fn per_flow_max_mss(
88 lookup: &PathMtuLookup,
89 addr_bytes: &[u8],
90 global_max_mss: u16,
91) -> u16 {
92 use super::icmp::effective_ipv6_mtu;
93
94 const IPV6_MIN_MTU: u16 = 1280;
98 let conservative_max_mss = effective_ipv6_mtu(IPV6_MIN_MTU)
99 .saturating_sub(40)
100 .saturating_sub(20);
101 let empty_lookup_ceiling = std::cmp::min(global_max_mss, conservative_max_mss);
102
103 if addr_bytes.len() != 16 {
104 trace!(
105 len = addr_bytes.len(),
106 global_max_mss,
107 empty_lookup_ceiling,
108 "per_flow_max_mss: addr_bytes wrong length, fall back to conservative ceiling"
109 );
110 return empty_lookup_ceiling;
111 }
112 let Ok(fips_addr) = FipsAddress::from_slice(addr_bytes) else {
113 trace!(
114 global_max_mss,
115 empty_lookup_ceiling,
116 "per_flow_max_mss: FipsAddress::from_slice rejected (non-fd::/8 prefix), fall back to conservative ceiling"
117 );
118 return empty_lookup_ceiling;
119 };
120 let Ok(map) = lookup.read() else {
121 trace!(
122 fips_addr = %fips_addr,
123 global_max_mss,
124 empty_lookup_ceiling,
125 "per_flow_max_mss: lookup read lock poisoned, fall back to conservative ceiling"
126 );
127 return empty_lookup_ceiling;
128 };
129 let Some(&path_mtu) = map.get(&fips_addr) else {
130 trace!(
131 fips_addr = %fips_addr,
132 global_max_mss,
133 empty_lookup_ceiling,
134 map_len = map.len(),
135 "per_flow_max_mss: no path_mtu_lookup entry for destination, fall back to conservative ceiling"
136 );
137 return empty_lookup_ceiling;
138 };
139 let path_max_mss = effective_ipv6_mtu(path_mtu)
140 .saturating_sub(40)
141 .saturating_sub(20);
142 let result = std::cmp::min(global_max_mss, path_max_mss);
143 trace!(
144 fips_addr = %fips_addr,
145 path_mtu,
146 path_max_mss,
147 global_max_mss,
148 result,
149 "per_flow_max_mss: per-destination clamp applied"
150 );
151 result
152}
153
154#[derive(Debug, Error)]
156pub enum TunError {
157 #[error("failed to create TUN device: {0}")]
158 Create(#[source] Box<dyn std::error::Error + Send + Sync>),
159
160 #[error("failed to configure TUN device: {0}")]
161 Configure(String),
162
163 #[cfg(target_os = "linux")]
164 #[error("netlink error: {0}")]
165 Netlink(#[from] rtnetlink::Error),
166
167 #[error("interface not found: {0}")]
168 InterfaceNotFound(String),
169
170 #[error("permission denied: {0}")]
171 PermissionDenied(String),
172
173 #[cfg(any(target_os = "linux", target_os = "macos"))]
174 #[error("IPv6 is disabled (set net.ipv6.conf.all.disable_ipv6=0)")]
175 Ipv6Disabled,
176
177 #[error("system TUN is not supported on this platform")]
178 UnsupportedPlatform,
179}
180
181#[cfg(any(target_os = "linux", target_os = "macos"))]
182impl From<tun::Error> for TunError {
183 fn from(e: tun::Error) -> Self {
184 TunError::Create(Box::new(e))
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum TunState {
191 Disabled,
193 Configured,
195 Active,
197 Failed,
199}
200
201impl std::fmt::Display for TunState {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 TunState::Disabled => write!(f, "disabled"),
205 TunState::Configured => write!(f, "configured"),
206 TunState::Active => write!(f, "active"),
207 TunState::Failed => write!(f, "failed"),
208 }
209 }
210}
211
212#[cfg(any(target_os = "linux", target_os = "macos"))]
218pub struct TunDevice {
219 #[cfg(target_os = "linux")]
220 device: LinuxTunDevice,
221 #[cfg(target_os = "macos")]
222 device: tun::Device,
223 name: String,
224 mtu: u16,
225 address: FipsAddress,
226}
227
228#[cfg(target_os = "linux")]
229enum LinuxTunDevice {
230 Plain(tun::Device),
231 Vnet(LinuxVnetTun),
232}
233
234#[cfg(target_os = "linux")]
235impl LinuxTunDevice {
236 fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
237 match self {
238 Self::Plain(device) => device.as_raw_fd(),
239 Self::Vnet(device) => device.as_raw_fd(),
240 }
241 }
242
243 fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
244 match self {
245 Self::Plain(device) => {
246 let n = device.read(buf)?;
247 if n > 0 {
248 crate::perf_profile::record_tun_read_frame(n);
249 }
250 Ok(n)
251 }
252 Self::Vnet(device) => device.read_packet(buf),
253 }
254 }
255
256 fn read_vnet_packets_into(
257 &mut self,
258 buf: &mut [u8],
259 packets: &mut Vec<Vec<u8>>,
260 ) -> Result<usize, std::io::Error> {
261 match self {
262 Self::Plain(_) => unreachable!("Linux vnet packet batching requires a vnet TUN"),
263 Self::Vnet(device) => device.read_packets_into(buf, packets),
264 }
265 }
266
267 fn read_buffer_len(&self, mtu: u16) -> usize {
268 match self {
269 Self::Plain(_) => default_tun_read_buffer_len(mtu),
270 Self::Vnet(device) => device.read_buffer_len(),
271 }
272 }
273
274 fn vnet_hdr(&self) -> bool {
275 matches!(self, Self::Vnet(_))
276 }
277}
278
279#[cfg(any(target_os = "linux", target_os = "macos"))]
280impl TunDevice {
281 pub async fn create(config: &TunConfig, address: FipsAddress) -> Result<Self, TunError> {
288 if platform::is_ipv6_disabled() {
290 return Err(TunError::Ipv6Disabled);
291 }
292
293 let name = config.name();
294 let mtu = config.mtu();
295
296 if platform::interface_exists(name).await {
298 debug!(name, "Deleting existing TUN interface");
299 if let Err(e) = platform::delete_interface(name).await {
300 debug!(name, error = %e, "Failed to delete existing interface");
301 }
302 }
303
304 #[cfg(target_os = "linux")]
305 let (device, actual_name) = {
306 if linux_vnet_tun_enabled() {
307 let device =
308 LinuxVnetTun::create(name).map_err(|e| TunError::Create(Box::new(e)))?;
309 let actual_name = device.name().to_string();
310 (LinuxTunDevice::Vnet(device), actual_name)
311 } else {
312 let mut tun_config = tun::Configuration::default();
313 tun_config.tun_name(name).layer(Layer::L3).mtu(mtu);
314 let device = tun::create(&tun_config)?;
315 let actual_name = {
316 use tun::AbstractDevice;
317 device.tun_name().map_err(|e| {
318 TunError::Configure(format!("failed to get device name: {}", e))
319 })?
320 };
321 (LinuxTunDevice::Plain(device), actual_name)
322 }
323 };
324
325 #[cfg(target_os = "macos")]
326 let (device, actual_name) = {
327 let mut tun_config = tun::Configuration::default();
330 tun_config.layer(Layer::L3).mtu(mtu);
331 let device = tun::create(&tun_config)?;
332 let actual_name = {
333 use tun::AbstractDevice;
334 device
335 .tun_name()
336 .map_err(|e| TunError::Configure(format!("failed to get device name: {}", e)))?
337 };
338 (device, actual_name)
339 };
340
341 platform::configure_interface(&actual_name, address.to_ipv6(), mtu).await?;
343
344 Ok(Self {
345 device,
346 name: actual_name,
347 mtu,
348 address,
349 })
350 }
351
352 pub fn name(&self) -> &str {
354 &self.name
355 }
356
357 pub fn mtu(&self) -> u16 {
359 self.mtu
360 }
361
362 pub fn address(&self) -> &FipsAddress {
364 &self.address
365 }
366
367 #[cfg(target_os = "macos")]
369 pub fn device(&self) -> &tun::Device {
370 &self.device
371 }
372
373 #[cfg(target_os = "macos")]
375 pub fn device_mut(&mut self) -> &mut tun::Device {
376 &mut self.device
377 }
378
379 pub fn read_packet(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
391 #[cfg(target_os = "linux")]
392 {
393 self.device.read_packet(buf)
394 }
395
396 #[cfg(target_os = "macos")]
397 self.device.read(buf)
398 }
399
400 #[cfg(target_os = "linux")]
401 fn read_vnet_packets_into(
402 &mut self,
403 buf: &mut [u8],
404 packets: &mut Vec<Vec<u8>>,
405 ) -> Result<usize, std::io::Error> {
406 self.device.read_vnet_packets_into(buf, packets)
407 }
408
409 #[cfg(target_os = "linux")]
410 fn read_buffer_len(&self, mtu: u16) -> usize {
411 self.device.read_buffer_len(mtu)
412 }
413
414 #[cfg(target_os = "linux")]
415 fn vnet_hdr(&self) -> bool {
416 self.device.vnet_hdr()
417 }
418
419 #[cfg(any(target_os = "linux", target_os = "macos"))]
420 fn as_raw_fd(&self) -> std::os::unix::io::RawFd {
421 #[cfg(target_os = "linux")]
422 {
423 self.device.as_raw_fd()
424 }
425
426 #[cfg(target_os = "macos")]
427 self.device.as_raw_fd()
428 }
429
430 pub async fn shutdown(&self) -> Result<(), TunError> {
434 debug!(name = %self.name, "Deleting TUN device");
435 platform::delete_interface(&self.name).await
436 }
437
438 pub fn create_writer(
450 &self,
451 max_mss: u16,
452 path_mtu_lookup: PathMtuLookup,
453 ) -> Result<(TunWriter, TunTx), TunError> {
454 let fd = self.as_raw_fd();
455
456 let write_fd = unsafe { libc::dup(fd) };
458 if write_fd < 0 {
459 return Err(TunError::Configure(format!(
460 "failed to dup fd: {}",
461 std::io::Error::last_os_error()
462 )));
463 }
464
465 let write_file = unsafe { File::from_raw_fd(write_fd) };
466 let (tx, rx) = write_channel();
467
468 Ok((
469 TunWriter {
470 file: write_file,
471 rx,
472 name: self.name.clone(),
473 max_mss,
474 path_mtu_lookup,
475 #[cfg(target_os = "linux")]
476 vnet_hdr: self.vnet_hdr(),
477 },
478 tx,
479 ))
480 }
481}
482
483#[cfg(target_os = "macos")]
487const UTUN_AF_INET6: u32 = 30;
488
489#[cfg(target_os = "macos")]
496#[inline]
497fn utun_af_inet6_header() -> [u8; 4] {
498 UTUN_AF_INET6.to_be_bytes()
499}
500
501#[cfg(all(test, target_os = "macos"))]
510#[inline]
511fn parse_utun_af_prefix(buf: &[u8]) -> Option<u32> {
512 if buf.len() < 4 {
513 return None;
514 }
515 Some(u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]))
516}
517
518#[cfg(any(target_os = "linux", target_os = "macos"))]
525pub struct TunWriter {
526 file: File,
527 rx: TunRx,
528 name: String,
529 max_mss: u16,
530 path_mtu_lookup: PathMtuLookup,
531 #[cfg(target_os = "linux")]
532 vnet_hdr: bool,
533}
534
535#[cfg(any(target_os = "linux", target_os = "macos"))]
536impl TunWriter {
537 fn clamp_inbound_packet(&self, packet: &mut super::tun_write::TunWritePacket) {
538 use super::tcp_mss::clamp_tcp_mss;
539
540 let effective_max_mss = if packet.len() >= 24 {
544 per_flow_max_mss(
545 &self.path_mtu_lookup,
546 &packet.as_slice()[8..24],
547 self.max_mss,
548 )
549 } else {
550 self.max_mss
551 };
552 if clamp_tcp_mss(packet.as_mut_slice(), effective_max_mss) {
554 trace!(
555 name = %self.name,
556 max_mss = effective_max_mss,
557 "Clamped TCP MSS in inbound SYN-ACK packet"
558 );
559 }
560 }
561
562 #[cfg(target_os = "linux")]
563 fn run_linux_vnet(mut self) {
564 use std::sync::mpsc::TryRecvError;
565
566 const LINUX_VNET_TUN_WRITE_BATCH_CAP: usize = 256;
567
568 debug!(name = %self.name, max_mss = self.max_mss, "Linux vnet TUN writer starting");
569
570 let mut batch = Vec::with_capacity(LINUX_VNET_TUN_WRITE_BATCH_CAP);
571 let mut write_preparer = linux_vnet::LinuxVnetWritePreparer::new();
572
573 while let Some(mut packet) = self.rx.recv() {
574 self.clamp_inbound_packet(&mut packet);
575 batch.push(packet);
576
577 while batch.len() < LINUX_VNET_TUN_WRITE_BATCH_CAP {
578 match self.rx.try_recv_packet() {
579 Ok(mut packet) => {
580 self.clamp_inbound_packet(&mut packet);
581 batch.push(packet);
582 }
583 Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
584 }
585 }
586
587 let write_result = {
588 linux_vnet::write_packet_slices_to_tun(
589 &mut self.file,
590 batch.iter().map(|packet| packet.as_slice()),
591 &mut write_preparer,
592 )
593 };
594
595 if let Err(e) = write_result {
596 let err_str = e.to_string();
597 if err_str.contains("Bad address") {
598 break;
599 }
600 error!(name = %self.name, error = %e, "Linux vnet TUN write error");
601 } else {
602 for packet in &batch {
603 crate::perf_profile::record_tun_write_packet(packet.len());
604 debug_ipv4_icmp_packet("Linux vnet TUN packet written", packet.as_slice());
605 trace!(name = %self.name, len = packet.len(), "TUN packet written");
606 }
607 }
608
609 batch.clear();
610 }
611 }
612
613 #[cfg(target_os = "macos")]
614 fn write_packet(&self, packet: &super::tun_write::TunWritePacket) -> std::io::Result<()> {
615 use std::os::unix::io::AsRawFd;
616
617 let af_header = utun_af_inet6_header();
618 let iov = [
619 libc::iovec {
620 iov_base: af_header.as_ptr() as *mut libc::c_void,
621 iov_len: 4,
622 },
623 libc::iovec {
624 iov_base: packet.as_slice().as_ptr() as *mut libc::c_void,
625 iov_len: packet.len(),
626 },
627 ];
628 let ret = unsafe { libc::writev(self.file.as_raw_fd(), iov.as_ptr(), 2) };
629 if ret < 0 {
630 return Err(std::io::Error::last_os_error());
631 }
632 let expected = 4 + packet.len();
633 if (ret as usize) < expected {
634 return Err(std::io::Error::new(
635 std::io::ErrorKind::WriteZero,
636 format!("short writev: {} of {} bytes", ret, expected),
637 ));
638 }
639 Ok(())
640 }
641
642 #[cfg(not(target_os = "macos"))]
643 fn write_packet(&mut self, packet: &super::tun_write::TunWritePacket) -> std::io::Result<()> {
644 self.file.write_all(packet.as_slice())
645 }
646
647 pub fn run(self) {
652 #[cfg(target_os = "linux")]
653 let mut writer = self;
654 #[cfg(target_os = "macos")]
655 let writer = self;
656
657 debug!(name = %writer.name, max_mss = writer.max_mss, "TUN writer starting");
658
659 #[cfg(target_os = "linux")]
660 if writer.vnet_hdr {
661 writer.run_linux_vnet();
662 return;
663 }
664
665 while let Some(mut packet) = writer.rx.recv() {
666 writer.clamp_inbound_packet(&mut packet);
667 let write_result = writer.write_packet(&packet);
668
669 if let Err(e) = write_result {
670 let err_str = e.to_string();
672 if err_str.contains("Bad address") {
673 break;
674 }
675 error!(name = %writer.name, error = %e, "TUN write error");
676 } else {
677 crate::perf_profile::record_tun_write_packet(packet.len());
678 debug_ipv4_icmp_packet("TUN packet written", packet.as_slice());
679 trace!(name = %writer.name, len = packet.len(), "TUN packet written");
680 }
681 }
682 }
683}
684
685#[cfg(any(target_os = "linux", target_os = "macos"))]
686fn debug_ipv4_icmp_packet(message: &'static str, packet: &[u8]) {
687 let Some((src, dst, icmp_type, icmp_id, icmp_seq)) = ipv4_icmp_echo(packet) else {
688 return;
689 };
690 debug!(
691 src = %src,
692 dst = %dst,
693 icmp_type,
694 icmp_id,
695 icmp_seq,
696 message
697 );
698}
699
700#[cfg(any(target_os = "linux", target_os = "macos"))]
701fn ipv4_icmp_echo(packet: &[u8]) -> Option<(std::net::Ipv4Addr, std::net::Ipv4Addr, u8, u16, u16)> {
702 if packet.len() < 28 || packet[0] >> 4 != 4 || packet[9] != 1 {
703 return None;
704 }
705 let header_len = usize::from(packet[0] & 0x0f).checked_mul(4)?;
706 if header_len < 20 || packet.len() < header_len.saturating_add(8) {
707 return None;
708 }
709 let icmp_type = packet[header_len];
710 if !matches!(icmp_type, 0 | 8) {
711 return None;
712 }
713 let src = std::net::Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]);
714 let dst = std::net::Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]);
715 let icmp_id = u16::from_be_bytes([packet[header_len + 4], packet[header_len + 5]]);
716 let icmp_seq = u16::from_be_bytes([packet[header_len + 6], packet[header_len + 7]]);
717 Some((src, dst, icmp_type, icmp_id, icmp_seq))
718}
719
720#[cfg(any(target_os = "linux", target_os = "macos"))]
721mod platform;
722
723#[cfg(windows)]
724mod windows;
725
726#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
727mod unsupported;
728
729#[cfg(target_os = "linux")]
730mod linux_vnet;
731
732#[cfg(test)]
733mod tests;
734
735include!("tun_runtime.rs");