fix(network): allow manual proxy overrides when system proxy is detected
This commit is contained in:
1 parent
7cde8942d3
commit
de01219cc8
10 files changed
+145
-31
No files matched your search
@@ -194,7 +194,7 @@ fn handle_configuration_file_changes(
|
||||
if preserve_invalid_gui_file {
|
||||
gui_state.replace_core_settings_external(&settings)?;
|
||||
} else {
|
||||
gui_state.sync_core_settings(&settings)?;
|
||||
gui_state.sync_core_settings_external(&settings)?;
|
||||
}
|
||||
cache.clear()?;
|
||||
Ok(())
|
||||
|
||||
@@ -160,18 +160,20 @@ pub(crate) fn save_network_endpoint_settings(
|
||||
next.host = host;
|
||||
next.allow_lan = !is_loopback_host(&next.host);
|
||||
next.port = settings.port;
|
||||
if let Some(proxy_url) = settings.proxy_url {
|
||||
let proxy_url = network_proxy::normalize_optional_proxy_url(&proxy_url)?;
|
||||
let detected = if proxy_url.is_empty() {
|
||||
Some(network_proxy::detect())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
next.proxy_override = !proxy_url.is_empty();
|
||||
if settings.proxy_url.is_some() || settings.proxy_override.is_some() {
|
||||
let proxy_url = network_proxy::normalize_optional_proxy_url(
|
||||
settings.proxy_url.as_deref().unwrap_or(&next.proxy_url),
|
||||
)?;
|
||||
// Explicit mode selection lets an empty manual URL mean direct access.
|
||||
// Calls from older frontends keep the legacy behavior where an empty URL
|
||||
// means following the system proxy.
|
||||
next.proxy_override = settings
|
||||
.proxy_override
|
||||
.unwrap_or_else(|| !proxy_url.is_empty());
|
||||
next.proxy_url = if next.proxy_override {
|
||||
proxy_url
|
||||
} else {
|
||||
detected.unwrap_or_else(network_proxy::detect)
|
||||
network_proxy::detect()
|
||||
};
|
||||
}
|
||||
validate_gui_config(&next)?;
|
||||
|
||||
@@ -1152,6 +1152,7 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
if core_is_newer && !gui_parse_failed {
|
||||
if let Ok(core_settings) = read_installed_core_config_settings() {
|
||||
apply_core_settings_to_gui_config(&mut config, &core_settings);
|
||||
apply_external_core_proxy_override(&mut config, &core_settings)?;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
@@ -1239,8 +1240,9 @@ pub(crate) fn load_or_create_gui_config() -> Result<GuiConfigFile, String> {
|
||||
if presence.routing_strategy.is_none() {
|
||||
config.routing_strategy = core_settings.routing_strategy;
|
||||
}
|
||||
if presence.proxy_url.is_none() && config.proxy_override {
|
||||
if presence.proxy_url.is_none() {
|
||||
config.proxy_url = core_settings.proxy_url;
|
||||
config.proxy_override = !config.proxy_url.trim().is_empty();
|
||||
}
|
||||
if presence.routing_session_affinity.is_none() {
|
||||
config.routing_session_affinity = core_settings.routing_session_affinity;
|
||||
@@ -1372,6 +1374,20 @@ pub(crate) fn apply_core_settings_to_gui_config(
|
||||
config.streaming_bootstrap_retries = core_settings.streaming_bootstrap_retries;
|
||||
}
|
||||
|
||||
pub(crate) fn apply_external_core_proxy_override(
|
||||
config: &mut GuiConfigFile,
|
||||
core_settings: &CoreConfigSettings,
|
||||
) -> Result<(), String> {
|
||||
if core_settings.proxy_url == config.proxy_url {
|
||||
return Ok(());
|
||||
}
|
||||
config.proxy_url = network_proxy::normalize_optional_proxy_url(&core_settings.proxy_url)?;
|
||||
// Changes made directly in the core YAML are user choices. Persist them as
|
||||
// manual overrides so the system-proxy monitor does not revert them.
|
||||
config.proxy_override = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn default_api_key_entry() -> GuiApiKeyEntry {
|
||||
GuiApiKeyEntry {
|
||||
key: DEFAULT_API_KEY.to_string(),
|
||||
@@ -1890,7 +1906,7 @@ pub(crate) fn validate_gui_config(config: &GuiConfigFile) -> Result<(), String>
|
||||
validate_strong_management_secret_key(&config.management_secret_key)?;
|
||||
validate_routing_strategy(config.routing_strategy.trim())?;
|
||||
if config.proxy_override {
|
||||
network_proxy::normalize_proxy_url(&config.proxy_url)?;
|
||||
network_proxy::normalize_optional_proxy_url(&config.proxy_url)?;
|
||||
} else if config.proxy_url.chars().any(char::is_control) {
|
||||
return Err("代理 URL 不能包含控制字符".to_string());
|
||||
}
|
||||
|
||||
+25
-1
@@ -1489,6 +1489,8 @@ struct GuiNetworkEndpointSettings {
|
||||
port: u16,
|
||||
#[serde(default)]
|
||||
proxy_url: Option<String>,
|
||||
#[serde(default)]
|
||||
proxy_override: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -1589,6 +1591,7 @@ struct CoreConfigView {
|
||||
plugins_enabled: bool,
|
||||
routing_strategy: String,
|
||||
proxy_url: String,
|
||||
proxy_override: bool,
|
||||
routing_session_affinity: bool,
|
||||
routing_session_affinity_ttl: String,
|
||||
disable_cooling: bool,
|
||||
@@ -1974,6 +1977,7 @@ impl GuiConfigState {
|
||||
.map_err(|_| "GUI 配置状态锁已损坏".to_string())?;
|
||||
let mut config = current.clone();
|
||||
apply_core_settings_to_gui_config(&mut config, settings);
|
||||
apply_external_core_proxy_override(&mut config, settings)?;
|
||||
sanitize_gui_config(&mut config)?;
|
||||
validate_gui_config(&config)?;
|
||||
*current = config.clone();
|
||||
@@ -2143,13 +2147,29 @@ impl GuiConfigState {
|
||||
}
|
||||
|
||||
fn sync_core_settings(&self, settings: &CoreConfigSettings) -> Result<GuiConfigFile, String> {
|
||||
self.sync_core_settings_with_api_key(settings, None)
|
||||
self.sync_core_settings_internal(settings, None, false)
|
||||
}
|
||||
|
||||
fn sync_core_settings_external(
|
||||
&self,
|
||||
settings: &CoreConfigSettings,
|
||||
) -> Result<GuiConfigFile, String> {
|
||||
self.sync_core_settings_internal(settings, None, true)
|
||||
}
|
||||
|
||||
fn sync_core_settings_with_api_key(
|
||||
&self,
|
||||
settings: &CoreConfigSettings,
|
||||
added_api_key: Option<GuiApiKeyEntry>,
|
||||
) -> Result<GuiConfigFile, String> {
|
||||
self.sync_core_settings_internal(settings, added_api_key, false)
|
||||
}
|
||||
|
||||
fn sync_core_settings_internal(
|
||||
&self,
|
||||
settings: &CoreConfigSettings,
|
||||
added_api_key: Option<GuiApiKeyEntry>,
|
||||
apply_external_proxy: bool,
|
||||
) -> Result<GuiConfigFile, String> {
|
||||
self.update(|config| {
|
||||
config.api_keys = merge_core_api_keys_with_gui_metadata(
|
||||
@@ -2187,6 +2207,9 @@ impl GuiConfigState {
|
||||
config.max_retry_credentials = settings.max_retry_credentials;
|
||||
config.max_retry_interval = settings.max_retry_interval;
|
||||
config.streaming_bootstrap_retries = settings.streaming_bootstrap_retries;
|
||||
if apply_external_proxy {
|
||||
apply_external_core_proxy_override(config, settings)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -2277,6 +2300,7 @@ impl From<&GuiConfigFile> for CoreConfigView {
|
||||
plugins_enabled: config.plugins_enabled,
|
||||
routing_strategy: config.routing_strategy.clone(),
|
||||
proxy_url: config.proxy_url.clone(),
|
||||
proxy_override: config.proxy_override,
|
||||
routing_session_affinity: config.routing_session_affinity,
|
||||
routing_session_affinity_ttl: config.routing_session_affinity_ttl.clone(),
|
||||
disable_cooling: config.disable_cooling,
|
||||
|
||||
@@ -492,11 +492,10 @@ pub(crate) fn initialize_override(config: &mut GuiConfigFile, had_override: bool
|
||||
if had_override {
|
||||
return false;
|
||||
}
|
||||
if config.proxy_override {
|
||||
config.proxy_override = false;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
// Before proxy-override existed, every non-empty proxy-url was explicitly
|
||||
// entered by the user. Preserve that choice when migrating the config.
|
||||
config.proxy_override = !config.proxy_url.trim().is_empty();
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_optional_proxy_url(value: &str) -> Result<String, String> {
|
||||
@@ -877,11 +876,18 @@ mod tests {
|
||||
};
|
||||
assert!(!initialize_override(&mut leftover, true));
|
||||
assert!(leftover.proxy_override);
|
||||
let mut legacy = GuiConfigFile {
|
||||
proxy_url: "http://127.0.0.1:7890".into(),
|
||||
..GuiConfigFile::default()
|
||||
};
|
||||
assert!(initialize_override(&mut legacy, false));
|
||||
assert!(legacy.proxy_override);
|
||||
assert_eq!(resolve(&legacy), "http://127.0.0.1:7890");
|
||||
let mut auto = GuiConfigFile {
|
||||
proxy_url: "http://127.0.0.1:7890".into(),
|
||||
..GuiConfigFile::default()
|
||||
};
|
||||
assert!(!initialize_override(&mut auto, false));
|
||||
assert!(!initialize_override(&mut auto, true));
|
||||
assert!(!auto.proxy_override);
|
||||
assert_eq!(resolve(&auto), detect());
|
||||
assert_eq!(normalize_optional_proxy_url("").unwrap(), "");
|
||||
|
||||
@@ -403,6 +403,40 @@ fn legacy_gui_config_can_seed_managed_core_settings() {
|
||||
assert!(config.run_on_startup);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_core_proxy_changes_become_manual_overrides() {
|
||||
let settings_with_proxy = |proxy_url: &str| {
|
||||
let input = format!("proxy-url: \"{proxy_url}\"\n");
|
||||
let document = serde_norway::from_str::<serde_norway::Value>(&input).unwrap();
|
||||
core_config_settings_from_value(&document).unwrap()
|
||||
};
|
||||
|
||||
let detected_url = "http://127.0.0.1:7890";
|
||||
let mut unchanged = GuiConfigFile {
|
||||
proxy_url: detected_url.to_string(),
|
||||
proxy_override: false,
|
||||
..GuiConfigFile::default()
|
||||
};
|
||||
apply_external_core_proxy_override(&mut unchanged, &settings_with_proxy(detected_url)).unwrap();
|
||||
assert!(!unchanged.proxy_override);
|
||||
|
||||
let mut custom = unchanged.clone();
|
||||
apply_external_core_proxy_override(
|
||||
&mut custom,
|
||||
&settings_with_proxy("socks5://127.0.0.1:1080"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(custom.proxy_override);
|
||||
assert_eq!(custom.proxy_url, "socks5://127.0.0.1:1080");
|
||||
|
||||
let mut direct = unchanged;
|
||||
apply_external_core_proxy_override(&mut direct, &settings_with_proxy("")).unwrap();
|
||||
assert!(direct.proxy_override);
|
||||
assert!(direct.proxy_url.is_empty());
|
||||
ensure_strong_management_secret(&mut direct).unwrap();
|
||||
assert!(validate_gui_config(&direct).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_api_keys_are_not_persisted_as_gui_settings() {
|
||||
let input = "api-keys:\n - your-api-key-1\n - real-key\nremote-management:\n secret-key: plain-management-secret\nplugins:\n enabled: true\nrouting:\n strategy: fill-first\n";
|
||||
|
||||
+3
-1
@@ -688,7 +688,9 @@ export const jaOverrides = {
|
||||
'config.network.listenHostHint': '例: 127.0.0.1、0.0.0.0、または LAN IP。変更すると実行中のコアが再起動します',
|
||||
'config.network.proxyUrl': 'プロキシ URL',
|
||||
'config.network.proxyPlaceholder': '例:socks5://127.0.0.1:7890',
|
||||
'config.network.proxyHint': '手動入力できます。空欄の場合はシステムプロキシを使います。未検出なら空欄のままです。HTTP と SOCKS に対応しています。',
|
||||
'config.network.proxyHint': '「システムに従う」をオフにすると編集できます。手動設定が空欄の場合は直接接続します。HTTP と SOCKS に対応しています。',
|
||||
'config.network.systemProxy': 'システムに従う',
|
||||
'config.network.systemProxyHint': 'オンの場合は検出したシステムプロキシを、オフの場合は下の手動設定を使用します',
|
||||
'config.network.sessionAffinity': 'セッションアフィニティ',
|
||||
'config.network.sessionAffinityHint': '同じセッションでは以前選択した認証情報を優先します',
|
||||
'config.network.sessionTtl': 'セッションアフィニティ TTL',
|
||||
|
||||
@@ -719,7 +719,9 @@ export const en: Record<MessageKey, string> = {
|
||||
'config.network.listenHostHint': 'For example, 127.0.0.1, 0.0.0.0, or a LAN IP; changing it restarts a running core',
|
||||
'config.network.proxyUrl': 'Proxy URL',
|
||||
'config.network.proxyPlaceholder': 'For example, socks5://127.0.0.1:7890',
|
||||
'config.network.proxyHint': 'You can enter a proxy URL. Leave it empty to use the system proxy, or none if none is detected. Supports HTTP and SOCKS.',
|
||||
'config.network.proxyHint': 'Turn off “Follow system” to customize it. An empty manual value means a direct connection. Supports HTTP and SOCKS.',
|
||||
'config.network.systemProxy': 'Follow system',
|
||||
'config.network.systemProxyHint': 'Use the detected system proxy when enabled; use the manual value below when disabled',
|
||||
'config.network.sessionAffinity': 'Session Affinity',
|
||||
'config.network.sessionAffinityHint': 'Prefer the previously selected credential for the same session',
|
||||
'config.network.sessionTtl': 'Session Affinity TTL',
|
||||
|
||||
@@ -718,7 +718,9 @@ export const zhCN = {
|
||||
'config.network.listenHostHint': '例如 127.0.0.1、0.0.0.0 或局域网 IP;修改后会重启运行中的内核',
|
||||
'config.network.proxyUrl': '代理 URL',
|
||||
'config.network.proxyPlaceholder': '例如 socks5://127.0.0.1:7890',
|
||||
'config.network.proxyHint': '可手动填写;留空则自动读取系统代理,没有就不填。支持 HTTP 与 SOCKS。',
|
||||
'config.network.proxyHint': '关闭“跟随系统”后可自定义;手动留空表示直连。支持 HTTP 与 SOCKS。',
|
||||
'config.network.systemProxy': '跟随系统',
|
||||
'config.network.systemProxyHint': '开启时自动使用系统代理;关闭后使用下方的手动配置',
|
||||
'config.network.sessionAffinity': '会话粘性路由',
|
||||
'config.network.sessionAffinityHint': '同一会话优先复用已选择的凭据',
|
||||
'config.network.sessionTtl': '会话粘性 TTL',
|
||||
|
||||
@@ -50,6 +50,7 @@ type CoreConfigSettings = {
|
||||
allowLan: boolean;
|
||||
routingStrategy: string;
|
||||
proxyUrl: string;
|
||||
proxyOverride: boolean;
|
||||
routingSessionAffinity: boolean;
|
||||
routingSessionAffinityTtl: string;
|
||||
disableCooling: boolean;
|
||||
@@ -83,6 +84,7 @@ type NetworkDraftField =
|
||||
| 'port'
|
||||
| 'host'
|
||||
| 'proxyUrl'
|
||||
| 'proxyOverride'
|
||||
| 'sessionAffinity'
|
||||
| 'sessionTtl'
|
||||
| 'disableCooling'
|
||||
@@ -118,6 +120,7 @@ const cleanNetworkDraft = (): NetworkDraftDirty => ({
|
||||
port: false,
|
||||
host: false,
|
||||
proxyUrl: false,
|
||||
proxyOverride: false,
|
||||
sessionAffinity: false,
|
||||
sessionTtl: false,
|
||||
disableCooling: false,
|
||||
@@ -190,6 +193,7 @@ export function ConfigPanelPage() {
|
||||
const [portDraft, setPortDraft] = useState('8317');
|
||||
const [hostDraft, setHostDraft] = useState('127.0.0.1');
|
||||
const [proxyUrlDraft, setProxyUrlDraft] = useState('');
|
||||
const [proxyOverrideDraft, setProxyOverrideDraft] = useState(false);
|
||||
const [sessionAffinityDraft, setSessionAffinityDraft] = useState(false);
|
||||
const [sessionTtlDraft, setSessionTtlDraft] = useState('');
|
||||
const [disableCoolingDraft, setDisableCoolingDraft] = useState(false);
|
||||
@@ -236,6 +240,7 @@ export function ConfigPanelPage() {
|
||||
if (!dirty.port) setPortDraft(String(result.port));
|
||||
if (!dirty.host) setHostDraft(result.host);
|
||||
if (!dirty.proxyUrl) setProxyUrlDraft(result.proxyUrl);
|
||||
if (!dirty.proxyOverride) setProxyOverrideDraft(result.proxyOverride);
|
||||
if (!dirty.sessionAffinity) setSessionAffinityDraft(result.routingSessionAffinity);
|
||||
if (!dirty.sessionTtl) setSessionTtlDraft(result.routingSessionAffinityTtl);
|
||||
if (!dirty.disableCooling) setDisableCoolingDraft(result.disableCooling);
|
||||
@@ -261,6 +266,7 @@ export function ConfigPanelPage() {
|
||||
setPortDraft(String(result.port));
|
||||
setHostDraft(result.host);
|
||||
setProxyUrlDraft(result.proxyUrl);
|
||||
setProxyOverrideDraft(result.proxyOverride);
|
||||
setSessionAffinityDraft(result.routingSessionAffinity);
|
||||
setSessionTtlDraft(result.routingSessionAffinityTtl);
|
||||
setDisableCoolingDraft(result.disableCooling);
|
||||
@@ -720,13 +726,14 @@ export function ConfigPanelPage() {
|
||||
setBusyAction('network');
|
||||
try {
|
||||
const result = await invoke<CoreConfigSettings>('save_network_endpoint_settings', {
|
||||
settings: networkDraftDirtyRef.current.proxyUrl
|
||||
? { host, port, proxyUrl }
|
||||
settings: networkDraftDirtyRef.current.proxyUrl || networkDraftDirtyRef.current.proxyOverride
|
||||
? { host, port, proxyUrl, proxyOverride: proxyOverrideDraft }
|
||||
: { host, port },
|
||||
});
|
||||
clearDraftDirty('host');
|
||||
clearDraftDirty('port');
|
||||
clearDraftDirty('proxyUrl');
|
||||
clearDraftDirty('proxyOverride');
|
||||
applySettings(result, 'preserve');
|
||||
setLoadError('');
|
||||
|
||||
@@ -872,6 +879,7 @@ export function ConfigPanelPage() {
|
||||
hostDraft.trim() !== settings?.host
|
||||
|| portDraft !== String(settings?.port)
|
||||
|| proxyUrlDraft.trim() !== settings?.proxyUrl
|
||||
|| proxyOverrideDraft !== settings?.proxyOverride
|
||||
);
|
||||
const sessionRoutingDirty = Boolean(settings) && (
|
||||
sessionAffinityDraft !== settings?.routingSessionAffinity
|
||||
@@ -1502,16 +1510,34 @@ export function ConfigPanelPage() {
|
||||
<small>{t('config.network.listenHostHint')}</small>
|
||||
</label>
|
||||
|
||||
<label className="config-network-field">
|
||||
<span className="config-network-label">
|
||||
<Link2 size={15} aria-hidden="true" />
|
||||
{t('config.network.proxyUrl')}
|
||||
</span>
|
||||
<div className="config-network-field config-proxy-field">
|
||||
<div className="config-proxy-heading">
|
||||
<label className="config-network-label" htmlFor="config-network-proxy-url">
|
||||
<Link2 size={15} aria-hidden="true" />
|
||||
{t('config.network.proxyUrl')}
|
||||
</label>
|
||||
<label className="config-proxy-system-toggle" title={t('config.network.systemProxyHint')}>
|
||||
<span>{t('config.network.systemProxy')}</span>
|
||||
<span className="switch-control">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!proxyOverrideDraft}
|
||||
disabled={controlsDisabled}
|
||||
onChange={(event) => {
|
||||
markDraftDirty('proxyOverride');
|
||||
setProxyOverrideDraft(!event.currentTarget.checked);
|
||||
}}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
id="config-network-proxy-url"
|
||||
className="config-network-input"
|
||||
type="text"
|
||||
value={proxyUrlDraft}
|
||||
disabled={controlsDisabled}
|
||||
disabled={controlsDisabled || !proxyOverrideDraft}
|
||||
placeholder={t('config.network.proxyPlaceholder')}
|
||||
onChange={(event) => {
|
||||
markDraftDirty('proxyUrl');
|
||||
@@ -1526,7 +1552,7 @@ export function ConfigPanelPage() {
|
||||
}}
|
||||
/>
|
||||
<small>{t('config.network.proxyHint')}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in new issue
Block a user