fix: allow 0.0.0.0 binding for LAN access (fixes #63)

Allow binding to 0.0.0.0 or :: to enable LAN access, with security
requirements:

1. Add is_valid_bind_host() and is_non_local_bind() helper functions
2. Allow 0.0.0.0 and :: as valid bind addresses
3. Require non-default API key when binding to all interfaces
4. Update validation in:
   - save_config command
   - hot_reload validation
   - bootstrap validation

Security: When using 0.0.0.0 or ::, users must set a custom API key
(not the default "proxy_cast") to prevent unauthorized access.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
coso
2026-01-08 21:29:42 +08:00
co-authored by Claude Opus 4.5
parent c034c97be1
commit 9dbab22624
7 changed files with 98 additions and 26 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.36.2",
"version": "0.36.3",
"type": "module",
"repository": {
"type": "git",
+1 -1
View File
@@ -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"
+22 -14
View File
@@ -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<Config, ConfigError> {
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 配置
+13 -5
View File
@@ -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(),
);
}
+37
View File
@@ -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<char> = 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"), "****");
+23 -4
View File
@@ -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 {
+1 -1
View File
@@ -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",