diff --git a/package.json b/package.json index 9f8676e48..a2d19ec5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.36.2", + "version": "0.36.3", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index bb5e1f5a9..222198cb9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.36.2" +version = "0.36.3" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/app/bootstrap.rs b/src-tauri/src/app/bootstrap.rs index 7f0be9d38..236a5dcc0 100644 --- a/src-tauri/src/app/bootstrap.rs +++ b/src-tauri/src/app/bootstrap.rs @@ -40,7 +40,7 @@ use crate::services::token_cache_service::TokenCacheService; use crate::telemetry; use super::types::{AppState, LogState, TokenCacheServiceState}; -use super::utils::{generate_api_key, is_loopback_host}; +use super::utils::{generate_api_key, is_non_local_bind, is_valid_bind_host}; /// 配置验证错误 #[derive(Debug)] @@ -48,7 +48,7 @@ pub enum ConfigError { LoadFailed(String), SaveFailed(String), InvalidHost, - DefaultApiKey, + DefaultApiKeyWithNonLocalBind, TlsNotSupported, RemoteManagementNotSupported, } @@ -59,9 +59,15 @@ impl std::fmt::Display for ConfigError { ConfigError::LoadFailed(e) => write!(f, "配置加载失败: {}", e), ConfigError::SaveFailed(e) => write!(f, "配置保存失败: {}", e), ConfigError::InvalidHost => { - write!(f, "当前版本仅支持本地监听,请使用 127.0.0.1/localhost/::1") + write!( + f, + "无效的监听地址。允许的地址:127.0.0.1、localhost、::1、0.0.0.0、::" + ) } - ConfigError::DefaultApiKey => write!(f, "检测到使用默认 API key,请配置强密钥"), + ConfigError::DefaultApiKeyWithNonLocalBind => write!( + f, + "监听所有网络接口 (0.0.0.0 或 ::) 时,必须设置非默认的 API Key" + ), ConfigError::TlsNotSupported => write!(f, "当前版本尚未支持 TLS"), ConfigError::RemoteManagementNotSupported => { write!(f, "远程管理需要 TLS 支持,当前版本未启用") @@ -74,22 +80,24 @@ impl std::fmt::Display for ConfigError { pub fn load_and_validate_config() -> Result { let mut config = config::load_config().map_err(|e| ConfigError::LoadFailed(e.to_string()))?; - // 自动生成 API key(如果使用默认值) - if config.server.api_key == config::DEFAULT_API_KEY { + // 验证主机地址 + if !is_valid_bind_host(&config.server.host) { + return Err(ConfigError::InvalidHost); + } + + let is_non_local = is_non_local_bind(&config.server.host); + + // 如果是本地绑定且使用默认 API key,自动生成新密钥 + if !is_non_local && config.server.api_key == config::DEFAULT_API_KEY { let new_key = generate_api_key(); config.server.api_key = new_key; config::save_config(&config).map_err(|e| ConfigError::SaveFailed(e.to_string()))?; tracing::info!("检测到默认 API key,已自动生成并保存新密钥"); } - // 验证主机地址 - if !is_loopback_host(&config.server.host) { - return Err(ConfigError::InvalidHost); - } - - // 再次检查 API key(防止保存失败后继续) - if config.server.api_key == config::DEFAULT_API_KEY { - return Err(ConfigError::DefaultApiKey); + // 如果是非本地绑定,必须使用非默认 API key + if is_non_local && config.server.api_key == config::DEFAULT_API_KEY { + return Err(ConfigError::DefaultApiKeyWithNonLocalBind); } // 检查 TLS 配置 diff --git a/src-tauri/src/app/commands/config.rs b/src-tauri/src/app/commands/config.rs index 72b98ea66..7fd2138b6 100644 --- a/src-tauri/src/app/commands/config.rs +++ b/src-tauri/src/app/commands/config.rs @@ -3,10 +3,11 @@ //! 包含配置读取、保存、Provider 设置等命令。 use crate::app::types::{AppState, LogState}; +use crate::app::utils::{is_non_local_bind, is_valid_bind_host}; use crate::config::{ self, observer::{ConfigChangeEvent, RoutingChangeEvent}, - ConfigChangeSource, GlobalConfigManagerState, + ConfigChangeSource, GlobalConfigManagerState, DEFAULT_API_KEY, }; /// 获取配置 @@ -22,12 +23,19 @@ pub async fn save_config( state: tauri::State<'_, AppState>, config: config::Config, ) -> Result<(), String> { - // P0 安全修复:禁止危险的网络配置 let host = config.server.host.to_lowercase(); - if host == "0.0.0.0" || host == "::" { + + // 验证绑定地址 + if !is_valid_bind_host(&host) { return Err( - "安全限制:不允许监听所有网络接口 (0.0.0.0 或 ::)。请使用 127.0.0.1 或 localhost" - .to_string(), + "无效的监听地址。允许的地址:127.0.0.1、localhost、::1、0.0.0.0、::".to_string(), + ); + } + + // 如果监听所有接口,要求使用强 API Key + if is_non_local_bind(&host) && config.server.api_key == DEFAULT_API_KEY { + return Err( + "安全限制:监听所有网络接口 (0.0.0.0 或 ::) 时,必须设置非默认的 API Key".to_string(), ); } diff --git a/src-tauri/src/app/utils.rs b/src-tauri/src/app/utils.rs index 6f4f61e6f..dffe61f96 100644 --- a/src-tauri/src/app/utils.rs +++ b/src-tauri/src/app/utils.rs @@ -20,6 +20,21 @@ pub fn is_loopback_host(host: &str) -> bool { } } +/// 检查是否为有效的绑定地址 +/// 允许回环地址和 0.0.0.0(监听所有接口) +pub fn is_valid_bind_host(host: &str) -> bool { + if is_loopback_host(host) { + return true; + } + // 允许 0.0.0.0 和 :: (监听所有接口) + host == "0.0.0.0" || host == "::" +} + +/// 检查是否为非本地绑定地址(需要强 API Key) +pub fn is_non_local_bind(host: &str) -> bool { + host == "0.0.0.0" || host == "::" +} + /// 掩码敏感 Token pub fn mask_token(token: &str) -> String { let chars: Vec = token.chars().collect(); @@ -45,6 +60,28 @@ mod tests { assert!(!is_loopback_host("192.168.1.1")); } + #[test] + fn test_is_valid_bind_host() { + // 回环地址 + assert!(is_valid_bind_host("localhost")); + assert!(is_valid_bind_host("127.0.0.1")); + assert!(is_valid_bind_host("::1")); + // 监听所有接口 + assert!(is_valid_bind_host("0.0.0.0")); + assert!(is_valid_bind_host("::")); + // 其他地址不允许 + assert!(!is_valid_bind_host("192.168.1.1")); + assert!(!is_valid_bind_host("10.0.0.1")); + } + + #[test] + fn test_is_non_local_bind() { + assert!(is_non_local_bind("0.0.0.0")); + assert!(is_non_local_bind("::")); + assert!(!is_non_local_bind("127.0.0.1")); + assert!(!is_non_local_bind("localhost")); + } + #[test] fn test_mask_token() { assert_eq!(mask_token("short"), "****"); diff --git a/src-tauri/src/config/hot_reload.rs b/src-tauri/src/config/hot_reload.rs index 3ed26ddc1..cd2062d2a 100644 --- a/src-tauri/src/config/hot_reload.rs +++ b/src-tauri/src/config/hot_reload.rs @@ -375,6 +375,8 @@ impl HotReloadManager { /// 验证配置 fn validate_config(&self, config: &Config) -> Result<(), HotReloadError> { let is_localhost = is_localhost_host(&config.server.host); + let is_valid_host = is_valid_bind_host(&config.server.host); + let is_non_local = is_non_local_bind(&config.server.host); // 验证端口范围 if config.server.port == 0 { @@ -383,9 +385,10 @@ impl HotReloadManager { )); } - if !is_localhost { + // 验证绑定地址 + if !is_valid_host { return Err(HotReloadError::ValidationError( - "当前版本仅支持本地监听,请使用 127.0.0.1/localhost/::1".to_string(), + "无效的监听地址。允许的地址:127.0.0.1、localhost、::1、0.0.0.0、::".to_string(), )); } @@ -415,11 +418,12 @@ impl HotReloadManager { )); } - if (!is_localhost || config.remote_management.allow_remote) + // 非本地绑定或远程管理时,禁止使用默认 API Key + if (is_non_local || !is_localhost || config.remote_management.allow_remote) && is_default_api_key(&config.server.api_key) { return Err(HotReloadError::ValidationError( - "非本地访问场景下禁止使用默认 API Key,请设置强口令".to_string(), + "监听所有网络接口或开启远程管理时,禁止使用默认 API Key,请设置强口令".to_string(), )); } @@ -484,6 +488,21 @@ fn is_localhost_host(host: &str) -> bool { .unwrap_or(false) } +/// 检查是否为有效的绑定地址 +/// 允许回环地址和 0.0.0.0(监听所有接口) +fn is_valid_bind_host(host: &str) -> bool { + if is_localhost_host(host) { + return true; + } + // 允许 0.0.0.0 和 :: (监听所有接口) + host == "0.0.0.0" || host == "::" +} + +/// 检查是否为非本地绑定地址(需要强 API Key) +fn is_non_local_bind(host: &str) -> bool { + host == "0.0.0.0" || host == "::" +} + /// 热重载状态 #[derive(Debug, Clone, serde::Serialize)] pub struct HotReloadStatus { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7ed2e357e..aaf42e43e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.36.2", + "version": "0.36.3", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev",