From a038e01cd5e885c9dceec38d40431f120b81da01 Mon Sep 17 00:00:00 2001 From: wonfen Date: Fri, 30 Oct 2026 09:15:19 +0800 Subject: [PATCH] fix(service): stage file-based ssh private keys for the service core The service core runs with its own home directory, so relative and absolute private-key paths no longer resolved. Stage them as runtime assets under an opaque name; UNC and device paths are left to the core. Refs #8445, #8229 --- Changelog.md | 16 ++++++ src-tauri/src/core/runtime_bundle.rs | 80 +++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index 9c2f97ae1..69314efb6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -25,6 +25,22 @@ 🐞 修复问题 - 修复全局模式缺少 `GLOBAL` 策略组,无法单独选择全局出口 +- 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题 +- 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口 +- 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置 +- 修复 首页 网站测速失败后卡片一直显示测试中 +- 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新 +- 修复 代理 在全局模式下无法展开节点组 +- 修复 自定义 CSS 无法修改编辑器字体和字号的问题 +- 修复服务模式下,SSH 节点的 `private-key` 使用私钥文件时内核无法启动的问题 + +**🍎 macOS** + +- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题 + +**🐧 Linux** + +- Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题 **🖥️ Windows** diff --git a/src-tauri/src/core/runtime_bundle.rs b/src-tauri/src/core/runtime_bundle.rs index 9987d669c..67f405c3d 100644 --- a/src-tauri/src/core/runtime_bundle.rs +++ b/src-tauri/src/core/runtime_bundle.rs @@ -3,8 +3,8 @@ use clash_verge_logging::{Type, logging}; use clash_verge_service_ipc::{RemoteProvider, RuntimeAsset, RuntimeBundle}; use serde_yaml_ng::{Mapping, Value}; use sha2::{Digest as _, Sha256}; -use std::collections::{BTreeMap, HashSet}; -use std::path::{Component, Path, PathBuf}; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::path::{Component, Path, PathBuf, Prefix}; pub(crate) const GEO_ASSETS: &[&str] = &[ "Country.mmdb", @@ -165,6 +165,7 @@ fn collect_runtime_parts(mut config: Value, config_root: &Path) -> Result, + assets: &mut Vec, +) { + let Some(config) = config.as_mapping_mut() else { + return; + }; + // mihomo parses `payload` for every provider type, eagerly as fallback proxies for file and http. + let mut proxy_lists = Vec::new(); + for (key, value) in config.iter_mut() { + match key.as_str() { + Some("proxies") => proxy_lists.extend(value.as_sequence_mut()), + Some("proxy-providers") => proxy_lists.extend( + value + .as_mapping_mut() + .into_iter() + .flat_map(Mapping::values_mut) + .filter_map(|provider| provider.get_mut("payload").and_then(Value::as_sequence_mut)), + ), + _ => {} + } + } + + let mut staged = HashMap::new(); + for proxy in proxy_lists.into_iter().flatten().filter_map(Value::as_mapping_mut) { + if proxy.get("type").and_then(Value::as_str) != Some("ssh") { + continue; + } + // mihomo reads any value containing "PRIVATE KEY" as inline key material. + let Some(raw_path) = proxy + .get("private-key") + .and_then(Value::as_str) + .filter(|key| !key.is_empty() && !key.contains("PRIVATE KEY") && is_local_path(key)) + else { + continue; + }; + let Some(source) = local_provider_source(config_root, raw_path) + .ok() + .filter(|source| source.is_file()) + else { + let name = proxy.get("name").and_then(Value::as_str).unwrap_or_default(); + logging!( + warn, + Type::Config, + "ssh proxy {name:?} private key is unavailable below the config root; leaving it to the core" + ); + continue; + }; + // A root-level alias keeps the user's path away from service name rules and mihomo's inline-key test. + let destination = staged + .entry(source.to_string_lossy().into_owned()) + .or_insert_with_key(|source| { + let destination = allocate_provider_destination("ssh-private-key", source, destinations); + assets.push(RuntimeAsset { + source: source.clone(), + destination: destination.clone(), + }); + destination + }) + .clone(); + proxy.insert(Value::String("private-key".to_owned()), Value::String(destination)); + } +} + +/// Resolving a UNC or device path would authenticate to its server, so those stay with the core. +fn is_local_path(raw_path: &str) -> bool { + match Path::new(raw_path).components().next() { + Some(Component::Prefix(prefix)) => matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)), + _ => true, + } +} + fn local_provider_source(config_root: &Path, raw_path: &str) -> Result { let path = Path::new(raw_path); let source = if path.is_absolute() {