Files
opsx-vpn/client/src-tauri/src/native_tunnel.rs
T

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());
}
}