Refine WireGuard client routing and authentication UI
This commit is contained in:
1 parent
eabb3e4472
commit
41fb9d5631
33 files changed
+1709
-469
No files matched your search
@@ -0,0 +1,212 @@
|
||||
//! 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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user