#![allow(dead_code)] use crate::db::pool; use rusqlite::params; use serde::{Deserialize, Serialize}; use uuid::Uuid; // ── 数据结构 ────────────────────────────────────────────────────────────────── #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MonitoredTool { pub id: String, pub name: String, pub exe_path: String, pub enabled: bool, } #[derive(Debug, Serialize, Clone)] pub struct ConnectionInfo { pub process_name: String, pub process_path: String, pub remote_address: String, pub remote_port: u16, } #[derive(Debug, Serialize, Clone)] pub struct ModeLogEntry { pub id: i64, pub mode: String, pub created_at: Option, } // ── 防火墙规则管理 ──────────────────────────────────────────────────────────── fn rule_display_name(tool_name: &str) -> String { format!("DevMgr_Block_{}", tool_name.replace(' ', "_")) } fn run_ps(script: &str) -> Result { let output = crate::silent_cmd("powershell") .args(["-NonInteractive", "-NoProfile", "-Command", script]) .output() .map_err(|e| format!("无法启动 PowerShell: {e}"))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } else { let err = String::from_utf8_lossy(&output.stderr).trim().to_string(); Err(err) } } /// 添加出站封锁规则 fn firewall_block(tool_name: &str, exe_path: &str) -> Result<(), String> { let name = rule_display_name(tool_name); // 先尝试删除旧规则(幂等) let _ = run_ps(&format!( "Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue" )); run_ps(&format!( "New-NetFirewallRule -DisplayName '{name}' -Direction Outbound \ -Program '{exe_path}' -Action Block -Profile Any -ErrorAction Stop" )) .map(|_| ()) .map_err(|e| { if e.contains("access") || e.contains("denied") || e.contains("privilege") || e.contains("Privilege") { "需要管理员权限才能修改防火墙规则,请以管理员身份运行本程序".to_string() } else { e } }) } /// 删除出站封锁规则(恢复放行) fn firewall_unblock(tool_name: &str) -> Result<(), String> { let name = rule_display_name(tool_name); run_ps(&format!( "Remove-NetFirewallRule -DisplayName '{name}' -ErrorAction SilentlyContinue" )) .map(|_| ()) .map_err(|e| { if e.contains("access") || e.contains("denied") || e.contains("privilege") || e.contains("Privilege") { "需要管理员权限才能修改防火墙规则,请以管理员身份运行本程序".to_string() } else { e } }) } // ── Tauri 命令 ──────────────────────────────────────────────────────────────── /// 获取当前开发模式状态 #[tauri::command] pub fn get_dev_mode() -> Result { let conn = pool().get().map_err(|e| e.to_string())?; let val: Option = conn .query_row( "SELECT value FROM settings WHERE key = 'dev_mode'", [], |row| row.get(0), ) .ok(); // 默认为 false(非开发模式) Ok(val.as_deref() == Some("1")) } /// 切换开发模式(同时应用/撤销防火墙规则) #[tauri::command] pub fn set_dev_mode(enabled: bool) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 获取所有已启用的受监控工具 let mut stmt = conn .prepare("SELECT name, exe_path FROM network_monitored_tools WHERE enabled = 1") .map_err(|e| e.to_string())?; let tools: Vec<(String, String)> = stmt .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; // 应用防火墙规则 let mut errors: Vec = Vec::new(); for (name, path) in &tools { let result = if enabled { firewall_unblock(name) } else { firewall_block(name, path) }; if let Err(e) = result { errors.push(format!("{name}: {e}")); break; // 权限错误无需继续尝试 } } if !errors.is_empty() { return Err(errors.join("\n")); } // 保存模式到设置 conn.execute( "INSERT INTO settings (key, value) VALUES ('dev_mode', ?1) ON CONFLICT(key) DO UPDATE SET value = excluded.value", params![if enabled { "1" } else { "0" }], ) .map_err(|e| e.to_string())?; // 记录切换历史 conn.execute( "INSERT INTO network_mode_log (mode, created_at) VALUES (?1, datetime('now'))", params![if enabled { "dev" } else { "non_dev" }], ) .map_err(|e| e.to_string())?; Ok(()) } /// 获取受监控工具列表 #[tauri::command] pub fn get_monitored_tools() -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare("SELECT id, name, exe_path, enabled FROM network_monitored_tools ORDER BY name") .map_err(|e| e.to_string())?; let tools = stmt .query_map([], |row| { Ok(MonitoredTool { id: row.get(0)?, name: row.get(1)?, exe_path: row.get(2)?, enabled: row.get::<_, i64>(3)? == 1, }) }) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; Ok(tools) } /// 添加受监控工具 #[tauri::command] pub fn add_monitored_tool(name: String, exe_path: String) -> Result { let id = Uuid::new_v4().to_string(); let conn = pool().get().map_err(|e| e.to_string())?; conn.execute( "INSERT INTO network_monitored_tools (id, name, exe_path, enabled) VALUES (?1, ?2, ?3, 1)", params![id, name, exe_path], ) .map_err(|e| e.to_string())?; // 如果当前是非开发模式,立即对新工具应用封锁规则 let dev_mode: bool = conn .query_row( "SELECT value FROM settings WHERE key = 'dev_mode'", [], |row| row.get::<_, String>(0), ) .ok() .as_deref() == Some("1"); if !dev_mode { let _ = firewall_block(&name, &exe_path); // 非关键失败,忽略 } Ok(id) } /// 移除受监控工具(同时清除其防火墙规则) #[tauri::command] pub fn remove_monitored_tool(id: String) -> Result<(), String> { let conn = pool().get().map_err(|e| e.to_string())?; // 先查出名称,用于删除防火墙规则 let name: Option = conn .query_row( "SELECT name FROM network_monitored_tools WHERE id = ?1", params![id], |row| row.get(0), ) .ok(); conn.execute( "DELETE FROM network_monitored_tools WHERE id = ?1", params![id], ) .map_err(|e| e.to_string())?; // 清除对应防火墙规则(不管当前是什么模式都清理干净) if let Some(n) = name { let _ = firewall_unblock(&n); } Ok(()) } /// 获取当前活跃的出站网络连接(只返回受监控工具相关的进程) #[tauri::command] pub fn get_active_connections() -> Result, String> { // 获取受监控工具的进程名(exe 文件名,不含路径) let conn = pool().get().map_err(|e| e.to_string())?; let mut stmt = conn .prepare("SELECT exe_path FROM network_monitored_tools WHERE enabled = 1") .map_err(|e| e.to_string())?; let paths: Vec = stmt .query_map([], |row| row.get(0)) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; if paths.is_empty() { return Ok(vec![]); } // 提取文件名(不含后缀)用于匹配进程名 let process_names: Vec = paths .iter() .filter_map(|p| { std::path::Path::new(p) .file_stem() .map(|s| s.to_string_lossy().to_lowercase()) }) .collect(); // PowerShell 脚本:获取已建立的出站连接,每行 "进程名||远端IP|端口" // 不读取 MainModule.FileName(极慢,需枚举每个进程的模块列表,且对系统进程易超时) let ps_script = r#" $results = @() try { Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue | Where-Object { $_.RemoteAddress -notmatch '^(0\.0\.0\.0|127\.|::1$|::$)' } | ForEach-Object { $proc = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue if ($proc) { $results += "$($proc.ProcessName)||$($_.RemoteAddress)|$($_.RemotePort)" } } } catch {} $results -join "`n" "#; let output = run_ps(ps_script).unwrap_or_default(); let mut connections: Vec = Vec::new(); for line in output.lines() { let parts: Vec<&str> = line.splitn(4, '|').collect(); if parts.len() < 4 { continue; } let proc_name = parts[0].to_lowercase(); // 只返回受监控工具的连接 if !process_names.iter().any(|n| proc_name.contains(n.as_str())) { continue; } let port: u16 = parts[3].parse().unwrap_or(0); connections.push(ConnectionInfo { process_name: parts[0].to_string(), process_path: parts[1].to_string(), remote_address: parts[2].to_string(), remote_port: port, }); } Ok(connections) } // ── WSL v2rayA 代理控制 ──────────────────────────────────────────────────────── fn get_wsl_distro() -> String { use crate::db::pool; use rusqlite::params; pool() .get() .ok() .and_then(|conn| { conn.query_row( "SELECT value FROM settings WHERE key = 'wsl_distro'", params![], |row| row.get::<_, String>(0), ) .ok() }) .filter(|s| !s.is_empty()) .unwrap_or_else(|| "Ubuntu".into()) } fn run_wsl_root(cmd: &str) -> Result { let distro = get_wsl_distro(); crate::silent_cmd("wsl") .args(["-d", &distro, "-u", "root", "-e", "bash", "-c", cmd]) .output() .map_err(|e| format!("WSL 不可用: {e}")) } /// run_wsl_root + 检查 exit code,用于关键写操作 fn run_wsl_root_ok(cmd: &str) -> Result { let output = run_wsl_root(cmd)?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); return Err(if stderr.is_empty() { format!("WSL 命令失败 (exit {})", output.status.code().unwrap_or(-1)) } else { let first_line = stderr.lines().next().unwrap_or(&stderr); format!("WSL 命令失败: {}", &first_line[..first_line.len().min(120)]) }); } Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } /// 验证字符串是合法 IPv4/IPv6 地址 fn is_valid_ip(s: &str) -> bool { s.parse::().is_ok() } /// 获取 WSL 对外 IP(用于从 Windows 侧访问 WSL 服务) /// WSL2 NAT 模式下 localhost 不通,需用 WSL 实际 IP #[tauri::command] pub fn get_wsl_ip() -> Result { let output = run_wsl_root("hostname -I | awk '{print $1}'").map_err(|_| "WSL 不可用".to_string())?; let ip = String::from_utf8_lossy(&output.stdout).trim().to_string(); if ip.is_empty() { Err("无法获取 WSL IP".to_string()) } else { Ok(ip) } } /// 将 v2rayN 代理端口同步写入 WSL /etc/profile.d/v2rayn-proxy.sh #[tauri::command] pub fn sync_wsl_proxy(db_path: Option) -> Result { // 1. 定位 guiNConfig.json let db_file = match db_path.filter(|s| !s.is_empty()) { Some(p) => std::path::PathBuf::from(p), None => { let saved = crate::db::pool() .get() .ok() .and_then(|conn| { conn.query_row( "SELECT value FROM settings WHERE key = 'v2rayn_db_path'", rusqlite::params![], |row| row.get::<_, String>(0), ) .ok() }) .filter(|s| !s.is_empty()); match saved { Some(p) => std::path::PathBuf::from(p), None => return Err("未配置 v2rayN 数据库路径,请先在「Win 代理配置」中设置".into()), } } }; let config_file = db_file .parent() .ok_or("无法获取 v2rayN 配置目录")? .join("guiNConfig.json"); // 2. 读取代理端口(Inbound[0].LocalPort,默认 10808) let config_str = std::fs::read_to_string(&config_file) .map_err(|e| format!("读取 guiNConfig.json 失败: {e}"))?; #[derive(serde::Deserialize)] struct Inbound { #[serde(rename = "LocalPort")] local_port: u16, } #[derive(serde::Deserialize)] struct GuiConfig { #[serde(rename = "Inbound")] inbound: Option>, } let cfg: GuiConfig = serde_json::from_str(&config_str).map_err(|e| format!("解析 guiNConfig.json 失败: {e}"))?; let port = cfg .inbound .as_ref() .and_then(|v| v.first()) .map(|i| i.local_port) .unwrap_or(10808); // 3. 生成脚本内容并用 base64 写入 WSL,避免 shell 引号转义问题 let script = format!( "# v2rayN proxy - managed by dev-manager (port {port})\n\ _WIN_HOST=$(ip route 2>/dev/null | grep default | awk '{{print $3}}' | head -1)\n\ if [ -n \"$_WIN_HOST\" ]; then\n\ export http_proxy=\"http://$_WIN_HOST:{port}\"\n\ export https_proxy=\"http://$_WIN_HOST:{port}\"\n\ export HTTP_PROXY=\"http://$_WIN_HOST:{port}\"\n\ export HTTPS_PROXY=\"http://$_WIN_HOST:{port}\"\n\ export ALL_PROXY=\"socks5://$_WIN_HOST:{port}\"\n\ export no_proxy=\"localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16\"\n\ export NO_PROXY=\"localhost,127.0.0.1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16\"\n\ fi\n" ); let encoded = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &script); // rm -f 先删除旧文件(包括符号链接本身,不跟随),再创建新普通文件,防止链接劫持 let cmd = format!( "rm -f /etc/profile.d/v2rayn-proxy.sh && echo '{encoded}' | base64 -d > /etc/profile.d/v2rayn-proxy.sh && chmod 644 /etc/profile.d/v2rayn-proxy.sh" ); run_wsl_root_ok(&cmd)?; // 让非登录 shell(如 VSCode 终端)也能加载代理变量 run_wsl_root_ok( r#"grep -qF 'v2rayn-proxy.sh' /etc/bash.bashrc 2>/dev/null || \ echo '[ -f /etc/profile.d/v2rayn-proxy.sh ] && source /etc/profile.d/v2rayn-proxy.sh' >> /etc/bash.bashrc"#, )?; // 为 WSL 默认登录用户写入 ~/.bashrc(root 的 ~ 是 /root,不是用户目录) // 优先读 /etc/wsl.conf [user] default=xxx,否则 fallback 到 UID=1000 run_wsl_root_ok( r#"_DEFAULT_USER=$(grep -oP '(?<=default=)\S+' /etc/wsl.conf 2>/dev/null | head -1); \ [ -z "$_DEFAULT_USER" ] && _DEFAULT_USER=$(getent passwd 1000 | cut -d: -f1); \ _USER_HOME=$(getent passwd "$_DEFAULT_USER" 2>/dev/null | cut -d: -f6); \ [ -n "$_USER_HOME" ] && { \ grep -qF 'v2rayn-proxy.sh' "$_USER_HOME/.bashrc" 2>/dev/null || \ echo '[ -f /etc/profile.d/v2rayn-proxy.sh ] && source /etc/profile.d/v2rayn-proxy.sh' >> "$_USER_HOME/.bashrc"; \ }"#, )?; // 将当前 Windows host IP 写入 /etc/environment(PAM 全局,覆盖所有 shell 类型) // /etc/environment 不支持 shell 展开,必须用当前实际 IP let win_host = { let out = run_wsl_root("ip route 2>/dev/null | grep default | awk '{print $3}' | head -1")?; String::from_utf8_lossy(&out.stdout).trim().to_string() }; if win_host.is_empty() { // WSL 网络异常时 L4(/etc/environment)无法写入,仅 L1 动态脚本生效 // PAM 类会话(GUI 程序)代理将不可用,给用户明确提示 return Ok(format!( "WSL 代理脚本已写入(端口 {port}),但无法获取 Windows 网关 IP,\ /etc/environment 未更新(重启 WSL 后可重新同步)" )); } if !win_host.is_empty() { let no_proxy_val = "localhost,127.0.0.1,::1"; let env_block = format!( "http_proxy=http://{win_host}:{port}\n\ https_proxy=http://{win_host}:{port}\n\ HTTP_PROXY=http://{win_host}:{port}\n\ HTTPS_PROXY=http://{win_host}:{port}\n\ ALL_PROXY=socks5://{win_host}:{port}\n\ no_proxy={no_proxy_val}\n\ NO_PROXY={no_proxy_val}" ); let env_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &env_block); // 先删除旧的 proxy 条目,再追加新的 let update_env_cmd = format!( r#"sed -i -E '/^https?_proxy|^HTTP_PROXY|^HTTPS_PROXY|^ALL_PROXY|^no_proxy|^NO_PROXY/Id' /etc/environment 2>/dev/null; \ echo '{env_b64}' | base64 -d >> /etc/environment"# ); run_wsl_root_ok(&update_env_cmd)?; } Ok(format!( "WSL 代理已配置(端口 {port} → {win_host}),新开终端自动生效" )) } /// 读取 WSL 当前代理配置状态,并检测 /etc/environment 中的 IP 是否已过时 /// 4 项检测合并为 1 次 WSL 调用,降低延迟约 75% #[tauri::command] pub fn get_wsl_proxy_status() -> Result { // 单次调用输出 4 段,以 __SEP1__ ~ __SEP3__ 分隔: // 段0: profile.d 脚本内容(缺失则输出 __MISSING__) // 段1: /etc/environment 中的 http_proxy 主机 IP // 段2: 当前网关 IP // 段3: bash.bashrc 是否包含 source 行(yes/no) let output = run_wsl_root( r#"if [ -f /etc/profile.d/v2rayn-proxy.sh ]; then cat /etc/profile.d/v2rayn-proxy.sh; else echo __MISSING__; fi; \ echo __SEP1__; \ grep -i '^http_proxy=' /etc/environment 2>/dev/null | head -1 | sed 's|.*://||;s|:.*||'; \ echo __SEP2__; \ ip route 2>/dev/null | grep default | awk '{print $3}' | head -1; \ echo __SEP3__; \ grep -qF 'v2rayn-proxy.sh' /etc/bash.bashrc 2>/dev/null && echo yes || echo no"# ).map_err(|_| "WSL 不可用".to_string())?; let raw = String::from_utf8_lossy(&output.stdout).to_string(); // 手动按分隔符切割 let seg: Vec<&str> = { let mut v = Vec::new(); let mut rest = raw.as_str(); // 兼容 Windows 管道可能插入的 \r\n for sep in &[ "__SEP1__\r\n", "__SEP1__\n", "__SEP2__\r\n", "__SEP2__\n", "__SEP3__\r\n", "__SEP3__\n", ] .chunks(2) .map(|pair| { if raw.contains(pair[0]) { pair[0] } else { pair[1] } }) .collect::>() { if let Some(pos) = rest.find(sep) { v.push(rest[..pos].trim()); rest = &rest[pos + sep.len()..]; } else { v.push(rest.trim()); rest = ""; } } v.push(rest.trim()); v }; let script_content = seg.first().copied().unwrap_or(""); if script_content.is_empty() || script_content == "__MISSING__" { return Ok(serde_json::json!({ "configured": false })); } // 从注释行提取端口 "# ... (port NNNN)" let port: Option = script_content .lines() .find(|l| l.contains("(port ")) .and_then(|l| l.split("(port ").nth(1)) .and_then(|s| s.trim_end_matches(')').trim().parse().ok()); let configured_ip = seg.get(1).copied().unwrap_or("").to_string(); let configured_ip = if is_valid_ip(&configured_ip) { Some(configured_ip) } else { None }; let current_ip = seg.get(2).copied().unwrap_or("").to_string(); let current_ip = if is_valid_ip(¤t_ip) { Some(current_ip) } else { None }; // 若无法提取 configured_ip(如 /etc/environment 被手动修改),保守地标为 stale let stale = match (&configured_ip, ¤t_ip) { (Some(a), Some(b)) => a != b, (None, Some(_)) => true, // 无法确认,提示重新同步 _ => false, }; let bashrc_ok = seg.get(3).copied().unwrap_or("no") == "yes"; Ok(serde_json::json!({ "configured": true, "port": port, "stale": stale, "configured_ip": configured_ip, "current_ip": current_ip, "bashrc_ok": bashrc_ok, })) } /// 清除 WSL 代理配置 #[tauri::command] pub fn clear_wsl_proxy() -> Result<(), String> { run_wsl_root_ok("rm -f /etc/profile.d/v2rayn-proxy.sh")?; run_wsl_root_ok(r#"sed -i '/v2rayn-proxy\.sh/d' /etc/bash.bashrc 2>/dev/null; true"#)?; // 同时清理 WSL 默认登录用户的 ~/.bashrc run_wsl_root_ok( r#"_DEFAULT_USER=$(grep -oP '(?<=default=)\S+' /etc/wsl.conf 2>/dev/null | head -1); \ [ -z "$_DEFAULT_USER" ] && _DEFAULT_USER=$(getent passwd 1000 | cut -d: -f1); \ _USER_HOME=$(getent passwd "$_DEFAULT_USER" 2>/dev/null | cut -d: -f6); \ [ -n "$_USER_HOME" ] && sed -i '/v2rayn-proxy\.sh/d' "$_USER_HOME/.bashrc" 2>/dev/null; true"#, )?; run_wsl_root_ok( r#"sed -i -E '/^https?_proxy|^HTTP_PROXY|^HTTPS_PROXY|^ALL_PROXY|^no_proxy|^NO_PROXY/Id' /etc/environment 2>/dev/null; true"#, )?; Ok(()) } /// 测试 WSL 侧出口 IP(通过 v2rayn-proxy.sh 代理) #[tauri::command] pub fn test_wsl_proxy_ip() -> Result { let output = run_wsl_root( "source /etc/profile.d/v2rayn-proxy.sh 2>/dev/null && \ curl -s --max-time 10 --proxy \"$http_proxy\" https://api.ipify.org 2>&1", ) .map_err(|_| "WSL 不可用".to_string())?; let ip = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !is_valid_ip(&ip) { let hint = if ip.is_empty() { "无输出".to_string() } else { ip.lines().next().unwrap_or(&ip).chars().take(80).collect() }; Err(format!("获取失败({hint}),请确认代理已配置且节点可用")) } else { Ok(ip) } } /// 测试 Windows 侧出口 IP(通过系统代理) #[tauri::command] pub fn test_win_proxy_ip() -> Result { // 1. 读取系统代理地址 let reg_out = crate::silent_cmd("powershell") .args(["-NonInteractive", "-NoProfile", "-Command", r#"(Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -ErrorAction SilentlyContinue).ProxyServer"#]) .output() .map_err(|e| format!("读取系统代理失败: {e}"))?; let proxy_server = String::from_utf8_lossy(®_out.stdout).trim().to_string(); if proxy_server.is_empty() { return Err("Windows 未配置系统代理(v2rayN 可能未启动)".to_string()); } // 2. 用 curl.exe 通过代理获取 IP let output = crate::silent_cmd("curl.exe") .args([ "--proxy", &format!("http://{}", proxy_server), "-s", "--max-time", "10", "https://api.ipify.org", ]) .output() .map_err(|e| format!("curl.exe 执行失败: {e}"))?; let ip = String::from_utf8_lossy(&output.stdout).trim().to_string(); if !is_valid_ip(&ip) { let hint = if ip.is_empty() { "无输出".to_string() } else { ip.lines().next().unwrap_or(&ip).chars().take(80).collect() }; Err(format!("获取失败(代理 {proxy_server},{hint})")) } else { Ok(ip) } } /// 获取模式切换历史 #[tauri::command] pub fn get_mode_log(limit: Option) -> Result, String> { let conn = pool().get().map_err(|e| e.to_string())?; let n = limit.unwrap_or(20); let mut stmt = conn .prepare( "SELECT id, mode, created_at FROM network_mode_log ORDER BY created_at DESC LIMIT ?1", ) .map_err(|e| e.to_string())?; let rows = stmt .query_map(params![n], |row| { Ok(ModeLogEntry { id: row.get(0)?, mode: row.get(1)?, created_at: row.get(2)?, }) }) .map_err(|e| e.to_string())? .collect::, _>>() .map_err(|e| e.to_string())?; Ok(rows) }