715 lines
25 KiB
Rust
715 lines
25 KiB
Rust
#![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<String>,
|
||
}
|
||
|
||
// ── 防火墙规则管理 ────────────────────────────────────────────────────────────
|
||
|
||
fn rule_display_name(tool_name: &str) -> String {
|
||
format!("DevMgr_Block_{}", tool_name.replace(' ', "_"))
|
||
}
|
||
|
||
fn run_ps(script: &str) -> Result<String, String> {
|
||
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<bool, String> {
|
||
let conn = pool().get().map_err(|e| e.to_string())?;
|
||
let val: Option<String> = 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::<Result<Vec<_>, _>>()
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
// 应用防火墙规则
|
||
let mut errors: Vec<String> = 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<Vec<MonitoredTool>, 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::<Result<Vec<_>, _>>()
|
||
.map_err(|e| e.to_string())?;
|
||
Ok(tools)
|
||
}
|
||
|
||
/// 添加受监控工具
|
||
#[tauri::command]
|
||
pub fn add_monitored_tool(name: String, exe_path: String) -> Result<String, String> {
|
||
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<String> = 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<Vec<ConnectionInfo>, 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<String> = stmt
|
||
.query_map([], |row| row.get(0))
|
||
.map_err(|e| e.to_string())?
|
||
.collect::<Result<Vec<_>, _>>()
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
if paths.is_empty() {
|
||
return Ok(vec![]);
|
||
}
|
||
|
||
// 提取文件名(不含后缀)用于匹配进程名
|
||
let process_names: Vec<String> = 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<ConnectionInfo> = 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<std::process::Output, String> {
|
||
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<String, String> {
|
||
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::<std::net::IpAddr>().is_ok()
|
||
}
|
||
|
||
/// 获取 WSL 对外 IP(用于从 Windows 侧访问 WSL 服务)
|
||
/// WSL2 NAT 模式下 localhost 不通,需用 WSL 实际 IP
|
||
#[tauri::command]
|
||
pub fn get_wsl_ip() -> Result<String, String> {
|
||
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<String>) -> Result<String, String> {
|
||
// 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<Vec<Inbound>>,
|
||
}
|
||
|
||
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<serde_json::Value, String> {
|
||
// 单次调用输出 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::<Vec<_>>()
|
||
{
|
||
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<u16> = 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<String, String> {
|
||
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<String, String> {
|
||
// 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<i64>) -> Result<Vec<ModeLogEntry>, 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::<Result<Vec<_>, _>>()
|
||
.map_err(|e| e.to_string())?;
|
||
Ok(rows)
|
||
}
|