Bundle TAP-Windows installer and persist startup config

This commit is contained in:
2026-09-01 22:16:19 +08:00
parent 57e6ba8e1e
commit a8d803168f
12 changed files with 691 additions and 44 deletions
+164 -18
View File
@@ -89,6 +89,19 @@ struct TapAdapter {
guid: String,
}
const CONFIG_FILE_NAME: &str = "super-n2n.config.json";
const TAP_INSTALLER_FILE_NAME: &str = "tap-windows-9.24.7-I601-Win10.exe";
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct StartupStatus {
config_exists: bool,
first_launch: bool,
config_path: String,
tap_driver_present: bool,
tap_installer_path: Option<String>,
}
fn now_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -122,6 +135,30 @@ fn app_directory() -> Result<std::path::PathBuf, String> {
.ok_or_else(|| "application executable has no parent directory".to_string())
}
fn config_file_path() -> Result<PathBuf, String> {
Ok(app_directory()?.join(CONFIG_FILE_NAME))
}
fn bundled_tap_installer_path() -> Result<PathBuf, String> {
Ok(app_directory()?
.join("drivers")
.join(TAP_INSTALLER_FILE_NAME))
}
fn tap_driver_present() -> bool {
#[cfg(target_os = "windows")]
{
return windows_tap_adapters()
.map(|adapters| !adapters.is_empty())
.unwrap_or(false);
}
#[cfg(not(target_os = "windows"))]
{
true
}
}
fn downloads_directory() -> PathBuf {
#[cfg(target_os = "windows")]
if let Some(profile) = std::env::var_os("USERPROFILE") {
@@ -257,8 +294,14 @@ fn windows_tap_adapters() -> Result<Vec<TapAdapter>, String> {
}
#[tauri::command]
fn start_service(request: StartRequest, state: State<'_, AppState>) -> Result<ServiceStatus, String> {
let mut processes = state.processes.lock().map_err(|_| "process state is unavailable".to_string())?;
fn start_service(
request: StartRequest,
state: State<'_, AppState>,
) -> Result<ServiceStatus, String> {
let mut processes = state
.processes
.lock()
.map_err(|_| "process state is unavailable".to_string())?;
if let Some(existing) = processes.get_mut(&request.service) {
if existing.try_wait().map_err(|e| e.to_string())?.is_none() {
return Ok(ServiceStatus {
@@ -279,7 +322,10 @@ fn start_service(request: StartRequest, state: State<'_, AppState>) -> Result<Se
let working_directory = app_directory()?;
let binary = working_directory.join(relative_binary);
if !binary.is_file() {
return Err(format!("required n2n binary is missing: {}", binary.display()));
return Err(format!(
"required n2n binary is missing: {}",
binary.display()
));
}
let mut child = Command::new(relative_binary)
@@ -310,9 +356,14 @@ fn start_service(request: StartRequest, state: State<'_, AppState>) -> Result<Se
#[tauri::command]
fn stop_service(service: String, state: State<'_, AppState>) -> Result<ServiceStatus, String> {
let mut processes = state.processes.lock().map_err(|_| "process state is unavailable".to_string())?;
let mut processes = state
.processes
.lock()
.map_err(|_| "process state is unavailable".to_string())?;
if let Some(mut child) = processes.remove(&service) {
child.kill().map_err(|e| format!("unable to stop {}: {}", service, e))?;
child
.kill()
.map_err(|e| format!("unable to stop {}: {}", service, e))?;
let _ = child.wait();
return Ok(empty_status(&service, "stopped"));
}
@@ -321,7 +372,10 @@ fn stop_service(service: String, state: State<'_, AppState>) -> Result<ServiceSt
#[tauri::command]
fn service_status(service: String, state: State<'_, AppState>) -> Result<ServiceStatus, String> {
let mut processes = state.processes.lock().map_err(|_| "process state is unavailable".to_string())?;
let mut processes = state
.processes
.lock()
.map_err(|_| "process state is unavailable".to_string())?;
if let Some(child) = processes.get_mut(&service) {
if child.try_wait().map_err(|e| e.to_string())?.is_none() {
return Ok(ServiceStatus {
@@ -366,7 +420,8 @@ fn management_query(request: ManagementRequest) -> Result<Vec<Value>, String> {
if let Ok(text) = std::str::from_utf8(&buffer[..size]) {
for line in text.lines() {
if let Ok(value) = serde_json::from_str::<Value>(line) {
let finished = value.get("_type").and_then(Value::as_str) == Some("end");
let finished =
value.get("_type").and_then(Value::as_str) == Some("end");
packets.push(value);
if finished {
return Ok(packets);
@@ -375,7 +430,10 @@ fn management_query(request: ManagementRequest) -> Result<Vec<Value>, String> {
}
}
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock || error.kind() == std::io::ErrorKind::TimedOut => {
Err(error)
if error.kind() == std::io::ErrorKind::WouldBlock
|| error.kind() == std::io::ErrorKind::TimedOut =>
{
break;
}
Err(error) => return Err(error.to_string()),
@@ -406,7 +464,11 @@ fn run_ping(request: PingRequest) -> Result<ToolResult, String> {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok(ToolResult {
ok: output.status.success(),
summary: if output.status.success() { "目标可达".to_string() } else { "目标不可达".to_string() },
summary: if output.status.success() {
"目标可达".to_string()
} else {
"目标不可达".to_string()
},
output: if stdout.is_empty() { stderr } else { stdout },
latency_ms: Some(started.elapsed().as_millis()),
})
@@ -420,11 +482,15 @@ fn run_tcping(request: TcpingRequest) -> Result<ToolResult, String> {
let started = Instant::now();
match TcpStream::connect_timeout(&address, Duration::from_millis(request.timeout_ms.max(100))) {
Ok(_) => Ok(ToolResult {
ok: true,
summary: "TCP 端口可连接".to_string(),
output: format!("connected to {} in {} ms", address, started.elapsed().as_millis()),
latency_ms: Some(started.elapsed().as_millis()),
}),
ok: true,
summary: "TCP 端口可连接".to_string(),
output: format!(
"connected to {} in {} ms",
address,
started.elapsed().as_millis()
),
latency_ms: Some(started.elapsed().as_millis()),
}),
Err(error) => Ok(ToolResult {
ok: false,
summary: "TCP 端口不可连接".to_string(),
@@ -451,7 +517,9 @@ fn tap_traffic(request: TapTrafficRequest) -> Result<TapTraffic, String> {
#[cfg(target_os = "linux")]
{
let path = std::path::Path::new("/sys/class/net").join(tap_device).join("statistics");
let path = std::path::Path::new("/sys/class/net")
.join(tap_device)
.join("statistics");
let received_bytes = fs::read_to_string(path.join("rx_bytes"))
.map_err(|error| format!("unable to read TAP adapter {tap_device}: {error}"))?
.trim()
@@ -514,6 +582,71 @@ fn resolve_tap_adapter(request: TapAdapterRequest) -> Result<TapAdapter, String>
}
}
#[tauri::command]
fn startup_status() -> Result<StartupStatus, String> {
let config_path = config_file_path()?;
let installer_path = bundled_tap_installer_path()?;
let config_exists = config_path.is_file();
Ok(StartupStatus {
config_exists,
first_launch: !config_exists,
config_path: config_path.display().to_string(),
tap_driver_present: tap_driver_present(),
tap_installer_path: installer_path
.is_file()
.then(|| installer_path.display().to_string()),
})
}
#[tauri::command]
fn read_config() -> Result<Option<String>, String> {
let path = config_file_path()?;
if !path.is_file() {
return Ok(None);
}
fs::read_to_string(&path)
.map(Some)
.map_err(|error| format!("unable to read configuration {}: {error}", path.display()))
}
#[tauri::command]
fn write_config(contents: String) -> Result<String, String> {
let path = config_file_path()?;
if contents.len() > 64 * 1024 {
return Err("configuration is too large".to_string());
}
serde_json::from_str::<Value>(&contents)
.map_err(|error| format!("configuration must be valid JSON: {error}"))?;
fs::write(&path, contents)
.map_err(|error| format!("unable to write configuration {}: {error}", path.display()))?;
Ok(path.display().to_string())
}
#[tauri::command]
fn open_tap_installer() -> Result<String, String> {
let path = bundled_tap_installer_path()?;
if !path.is_file() {
return Err(format!(
"随包 TAP-Windows 安装程序不存在:{}",
path.display()
));
}
#[cfg(target_os = "windows")]
{
Command::new(&path)
.spawn()
.map_err(|error| format!("无法打开 TAP-Windows 安装程序:{error}"))?;
return Ok(path.display().to_string());
}
#[cfg(not(target_os = "windows"))]
{
let _ = path;
Err("TAP-Windows 安装程序仅支持 Windows".to_string())
}
}
#[tauri::command]
fn firewall_status() -> Result<ToolResult, String> {
#[cfg(target_os = "windows")]
@@ -523,7 +656,10 @@ fn firewall_status() -> Result<ToolResult, String> {
.map_err(|e| e.to_string())?;
#[cfg(target_os = "linux")]
let output = Command::new("sh")
.args(["-c", "ufw status verbose 2>/dev/null || firewall-cmd --state 2>/dev/null || true"])
.args([
"-c",
"ufw status verbose 2>/dev/null || firewall-cmd --state 2>/dev/null || true",
])
.output()
.map_err(|e| e.to_string())?;
#[cfg(target_os = "macos")]
@@ -544,10 +680,16 @@ fn firewall_status() -> Result<ToolResult, String> {
|| normalized.contains("status: active")
|| normalized.contains("firewall is enabled")
|| normalized.contains("state = 1")
|| normalized.split_whitespace().any(|token| token == "running");
|| normalized
.split_whitespace()
.any(|token| token == "running");
Ok(ToolResult {
ok: output.status.success(),
summary: if enabled { "本地防火墙已开启".to_string() } else { "未检测到防火墙开启".to_string() },
summary: if enabled {
"本地防火墙已开启".to_string()
} else {
"未检测到防火墙开启".to_string()
},
output: stdout,
latency_ms: None,
})
@@ -619,6 +761,10 @@ pub fn run() {
run_tcping,
tap_traffic,
resolve_tap_adapter,
startup_status,
read_config,
write_config,
open_tap_installer,
firewall_status,
export_log
])