213 lines
6.9 KiB
Rust
213 lines
6.9 KiB
Rust
//! 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<String>,
|
|
pub endpoint: String,
|
|
pub endpoint_bypass: bool,
|
|
pub physical_exit: Option<PhysicalExit>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub(crate) struct PhysicalExit {
|
|
pub interface: String,
|
|
pub gateway: Option<String>,
|
|
}
|
|
|
|
#[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<NativePlan>,
|
|
handshake_unix: Option<u64>,
|
|
rx_bytes: Option<u64>,
|
|
tx_bytes: Option<u64>,
|
|
}
|
|
|
|
pub(crate) struct NativeProcess {
|
|
child: Child,
|
|
input: ChildStdin,
|
|
events: Receiver<String>,
|
|
}
|
|
|
|
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<NativeCounters, String> {
|
|
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<NativePlan, String> {
|
|
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::<std::net::Ipv4Addr>().is_ok()
|
|
}
|
|
|
|
fn ipv4_cidr(value: &str) -> bool {
|
|
let Some((ip, prefix)) = value.split_once('/') else {
|
|
return false;
|
|
};
|
|
ipv4(ip) && prefix.parse::<u8>().is_ok_and(|bits| bits <= 32)
|
|
}
|
|
|
|
fn decode_counters(line: &str) -> Result<NativeCounters, String> {
|
|
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());
|
|
}
|
|
}
|