//! The unprivileged Go data-plane process. This module deliberately does not //! configure OS routes or report an application-level connected state. #![cfg(target_os = "macos")] #![allow(dead_code)] // Connected after the privileged network helper is installed. use std::{ io::{BufRead, BufReader, Write}, path::Path, process::{Child, ChildStdin, Command, Stdio}, sync::mpsc::{self, Receiver}, time::{Duration, Instant}, }; use serde::Deserialize; #[derive(Debug, Deserialize)] pub(crate) struct NativePlan { pub interface: String, pub client_address: String, pub routes: Vec, pub endpoint: String, pub endpoint_bypass: bool, pub physical_exit: Option, } #[derive(Debug, Deserialize)] pub(crate) struct PhysicalExit { pub interface: String, pub gateway: Option, } #[derive(Debug, Deserialize)] pub(crate) struct NativeCounters { pub handshake_unix: u64, pub rx_bytes: u64, pub tx_bytes: u64, } #[derive(Deserialize)] struct Event { status: String, plan: Option, handshake_unix: Option, rx_bytes: Option, tx_bytes: Option, } pub(crate) struct NativeProcess { child: Child, input: ChildStdin, events: Receiver, } impl NativeProcess { pub(crate) fn start(binary: &Path, record: &Path) -> Result<(Self, NativePlan), String> { let mut child = Command::new(binary) .arg("tun") .arg(record) .stdin(Stdio::piped()) .stdout(Stdio::piped()) // Upstream errors might mention addresses; never forward process output to the UI. .stderr(Stdio::null()) .spawn() .map_err(|_| "无法启动内置 Go 隧道组件".to_string())?; let input = child.stdin.take().ok_or("无法控制 Go 隧道组件")?; let output = child.stdout.take().ok_or("无法读取 Go 隧道组件")?; let (sender, events) = mpsc::sync_channel(4); std::thread::spawn(move || { for line in BufReader::new(output).lines() { let Ok(line) = line else { break }; if sender.send(line).is_err() { break; } } }); let process = Self { child, input, events, }; let line = process .events .recv_timeout(Duration::from_secs(10)) .map_err(|_| "内置隧道没有及时创建本机接口".to_string())?; let plan = decode_plan(&line)?; Ok((process, plan)) } pub(crate) fn counters(&mut self) -> Result { if self .child .try_wait() .map_err(|_| "无法检查内置隧道状态")? .is_some() { return Err("内置 Go 隧道已退出".into()); } self.input .write_all(b"status\n") .map_err(|_| "内置隧道已断开")?; let line = self .events .recv_timeout(Duration::from_secs(3)) .map_err(|_| "无法获取内置隧道握手状态".to_string())?; decode_counters(&line) } pub(crate) fn stop(mut self) { let _ = self.input.write_all(b"stop\n"); let deadline = Instant::now() + Duration::from_secs(2); while Instant::now() < deadline { if self.child.try_wait().ok().flatten().is_some() { return; } std::thread::sleep(Duration::from_millis(50)); } let _ = self.child.kill(); let _ = self.child.wait(); } } impl Drop for NativeProcess { fn drop(&mut self) { if self.child.try_wait().ok().flatten().is_none() { let _ = self.child.kill(); let _ = self.child.wait(); } } } fn decode_plan(line: &str) -> Result { let event: Event = serde_json::from_str(line).map_err(|_| "内置隧道接口事件无效")?; if event.status != "interface_created" { return Err("内置隧道未创建接口".into()); } let plan = event.plan.ok_or("内置隧道没有路由计划")?; if !plan.interface.starts_with("utun") || plan.interface[4..].is_empty() || !plan.interface[4..].bytes().all(|b| b.is_ascii_digit()) || !ipv4_cidr(&plan.client_address) || plan.routes.is_empty() || !plan.routes.iter().all(|r| ipv4_cidr(r)) || !ipv4(&plan.endpoint) || (plan.endpoint_bypass && plan.physical_exit.is_none()) { return Err("内置隧道网络计划无效".into()); } if let Some(exit) = &plan.physical_exit { if exit.interface.starts_with("utun") || exit.interface == "lo0" || exit.interface.len() < 2 || exit.interface.len() > 16 || !exit.interface.bytes().all(|b| b.is_ascii_alphanumeric()) || exit.gateway.as_ref().is_some_and(|gateway| !ipv4(gateway)) { return Err("内置隧道物理出口无效".into()); } } Ok(plan) } fn ipv4(value: &str) -> bool { value.parse::().is_ok() } fn ipv4_cidr(value: &str) -> bool { let Some((ip, prefix)) = value.split_once('/') else { return false; }; ipv4(ip) && prefix.parse::().is_ok_and(|bits| bits <= 32) } fn decode_counters(line: &str) -> Result { let event: Event = serde_json::from_str(line).map_err(|_| "内置隧道状态无效")?; if event.status != "device_status" { return Err("内置隧道状态类型无效".into()); } Ok(NativeCounters { handshake_unix: event.handshake_unix.ok_or("缺少握手时间")?, rx_bytes: event.rx_bytes.ok_or("缺少接收计数")?, tx_bytes: event.tx_bytes.ok_or("缺少发送计数")?, }) } #[cfg(test)] mod tests { use super::*; #[test] fn interface_event_does_not_claim_connection() { let json = r#"{"status":"interface_created","plan":{"interface":"utun12","client_address":"10.88.0.3/32","routes":["10.88.0.0/24"],"endpoint":"121.4.111.208","endpoint_bypass":false}}"#; let plan = decode_plan(json).unwrap(); assert_eq!(plan.interface, "utun12"); assert!(decode_counters(json).is_err()); assert!(decode_plan(&json.replace("utun12", "utun1;whoami")).is_err()); assert!(decode_plan(&json.replace("10.88.0.0/24", "::/0")).is_err()); assert!(decode_plan(r#"{"status":"ready"}"#).is_err()); } #[test] fn handshake_and_bytes_require_explicit_status_event() { let status = decode_counters( r#"{"status":"device_status","handshake_unix":123,"rx_bytes":2,"tx_bytes":3}"#, ) .unwrap(); assert_eq!( (status.handshake_unix, status.rx_bytes, status.tx_bytes), (123, 2, 3) ); assert!(decode_counters(r#"{"status":"device_status","handshake_unix":123}"#).is_err()); } }