dev-manager-tauri/src-tauri/src/commands/network.rs
2026-09-21 12:52:37 +09:00

715 lines
25 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![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(&current_ip) {
Some(current_ip)
} else {
None
};
// 若无法提取 configured_ip(如 /etc/environment 被手动修改),保守地标为 stale
let stale = match (&configured_ip, &current_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(&reg_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)
}