Bundle TAP-Windows installer and persist startup config
This commit is contained in:
+164
-18
@@ -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
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user