fix(security): configure trusted Fake-IP DNS hosts

This commit is contained in:
elky
2026-09-05 14:05:07 +08:00
parent f69b770f5e
commit e15ea0d5d3
8 changed files with 371 additions and 38 deletions
@@ -1,5 +1,5 @@
use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::error::Error as _;
use std::future::Future;
use std::io::Read;
@@ -63,6 +63,8 @@ use crate::upstream_admission::UpstreamTargetAdmissionPermit;
use crate::{AppState, GatewayError};
const HUB_RELAY_CONTENT_TYPE: &str = "application/vnd.aether.tunnel-envelope";
pub(crate) const EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY: &str =
aether_admin::system::EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY;
const HUB_RELAY_ERROR_HEADER: &str = "x-aether-tunnel-error";
const MAX_SAFE_REDIRECTS: usize = 10;
const MAX_UPSTREAM_ERROR_DETAIL_BYTES: usize = 2_048;
@@ -479,28 +481,147 @@ const TRUSTED_EXECUTION_BENCHMARKING_DNS_EXACT_HOSTS: &[&str] = &[
"generativelanguage.googleapis.com",
"grok.com",
"open.bigmodel.cn",
"q.us-iso-east-1.c2s.ic.gov",
"q.us-isob-east-1.sc2s.sgov.gov",
"q.us-isof-east-1.csp.hci.ic.gov",
"q.us-isof-south-1.csp.hci.ic.gov",
"server.codeium.com",
];
const TRUSTED_EXECUTION_VERTEX_DNS_REGIONS: &[&str] = &[
"africa-south1",
"asia-east1",
"asia-east2",
"asia-northeast1",
"asia-northeast2",
"asia-northeast3",
"asia-south1",
"asia-south2",
"asia-southeast1",
"asia-southeast2",
"australia-southeast1",
"australia-southeast2",
"europe-central2",
"europe-north1",
"europe-southwest1",
"europe-west1",
"europe-west2",
"europe-west3",
"europe-west4",
"europe-west6",
"europe-west8",
"europe-west9",
"europe-west10",
"europe-west12",
"me-central1",
"me-central2",
"me-west1",
"northamerica-northeast1",
"northamerica-northeast2",
"southamerica-east1",
"southamerica-west1",
"us-central1",
"us-east1",
"us-east4",
"us-east5",
"us-south1",
"us-west1",
"us-west2",
"us-west3",
"us-west4",
];
const TRUSTED_EXECUTION_AWS_DNS_REGIONS: &[&str] = &[
"af-south-1",
"ap-east-1",
"ap-northeast-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-south-1",
"ap-south-2",
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-3",
"ap-southeast-4",
"ca-central-1",
"ca-west-1",
"eu-central-1",
"eu-central-2",
"eu-north-1",
"eu-south-1",
"eu-south-2",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"il-central-1",
"me-central-1",
"me-south-1",
"mx-central-1",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-gov-east-1",
"us-gov-west-1",
"us-west-1",
"us-west-2",
];
static EXECUTION_EXTRA_TRUSTED_DNS_HOSTS: LazyLock<StdRwLock<BTreeSet<String>>> =
LazyLock::new(|| StdRwLock::new(BTreeSet::new()));
pub(crate) fn refresh_execution_extra_trusted_dns_hosts(value: Option<&Value>) {
let hosts = value
.cloned()
.and_then(|value| {
aether_admin::system::normalize_execution_extra_trusted_dns_hosts_config_value(value)
.ok()
})
.and_then(|value| {
value.as_array().map(|hosts| {
hosts
.iter()
.filter_map(Value::as_str)
.map(ToOwned::to_owned)
.collect::<BTreeSet<_>>()
})
})
.unwrap_or_default();
if let Ok(mut current) = EXECUTION_EXTRA_TRUSTED_DNS_HOSTS.write() {
*current = hosts;
}
}
/// Return whether `host` is one of the fixed provider origins for which a
/// local RFC-2544 synthetic answer can be accepted. The resolver receives only
/// a hostname (not the URL scheme/path), so all policy that can be expressed
/// here is intentionally host based. URL validation still requires HTTPS for
/// non-loopback upstreams before this resolver is used.
fn execution_host_allows_benchmarking_dns_answer(host: &str) -> bool {
let extra_hosts = EXECUTION_EXTRA_TRUSTED_DNS_HOSTS
.read()
.map(|hosts| hosts.clone())
.unwrap_or_default();
execution_host_allows_benchmarking_dns_answer_with_extra_hosts(host, &extra_hosts)
}
fn execution_host_allows_benchmarking_dns_answer_with_extra_hosts(
host: &str,
extra_hosts: &BTreeSet<String>,
) -> bool {
let host = host.trim().trim_end_matches('.').to_ascii_lowercase();
if TRUSTED_EXECUTION_BENCHMARKING_DNS_EXACT_HOSTS
.iter()
.any(|trusted| *trusted == host)
if extra_hosts.contains(&host)
|| TRUSTED_EXECUTION_BENCHMARKING_DNS_EXACT_HOSTS
.iter()
.any(|trusted| *trusted == host)
{
return true;
}
// Vertex service-account requests use `<region>-aiplatform.googleapis.com`.
// Keep the interpolated region to one DNS label and reuse the provider's
// existing conservative region syntax validator.
// Keep this compatibility exception limited to known provider regions.
if let Some(region) = host.strip_suffix("-aiplatform.googleapis.com") {
return looks_like_cloud_region_label(region);
return TRUSTED_EXECUTION_VERTEX_DNS_REGIONS.contains(&region);
}
// Kiro uses a small, fixed set of regional service origins. Match each
@@ -511,9 +632,6 @@ fn execution_host_allows_benchmarking_dns_answer(host: &str) -> bool {
|| matches_regional_service_host(&host, "codewhisperer", ".amazonaws.com")
|| matches_regional_service_host(&host, "oidc", ".amazonaws.com")
|| matches_regional_service_host(&host, "prod", ".auth.desktop.kiro.dev")
|| matches_regional_service_host(&host, "q", ".c2s.ic.gov")
|| matches_regional_service_host(&host, "q", ".sc2s.sgov.gov")
|| matches_regional_service_host(&host, "q", ".csp.hci.ic.gov")
}
fn matches_regional_service_host(host: &str, service: &str, suffix: &str) -> bool {
@@ -524,21 +642,7 @@ fn matches_regional_service_host(host: &str, service: &str, suffix: &str) -> boo
else {
return false;
};
// A single region label is required. This rejects values such as
// `q.us-east-1.evil.amazonaws.com` and suffix lookalikes.
!region.contains('.') && looks_like_cloud_region_label(region)
}
fn looks_like_cloud_region_label(value: &str) -> bool {
value.len() >= 3
&& value.len() <= 63
&& value
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
&& !value.starts_with('-')
&& !value.ends_with('-')
&& value.contains('-')
&& value.bytes().any(|byte| byte.is_ascii_digit())
TRUSTED_EXECUTION_AWS_DNS_REGIONS.contains(&region)
}
fn dns_host_explicitly_allows_loopback(host: &str) -> bool {
@@ -5569,6 +5673,7 @@ mod tests {
"api.openai.com",
"CHATGPT.COM.",
"us-central1-aiplatform.googleapis.com",
"me-central2-aiplatform.googleapis.com",
"q.us-east-1.amazonaws.com",
"q-fips.us-gov-west-1.amazonaws.com",
"codewhisperer.us-west-2.amazonaws.com",
@@ -5591,6 +5696,16 @@ mod tests {
"q.us-east-1.evil.amazonaws.com",
"q.us-east-1.amazonaws.com.attacker.test",
"q.localhost.amazonaws.com",
"evil-1-aiplatform.googleapis.com",
"q.evil-1.amazonaws.com",
"q-fips.evil-1.amazonaws.com",
"codewhisperer.evil-1.amazonaws.com",
"prod.evil-1.auth.desktop.kiro.dev",
"oidc.evil-1.amazonaws.com",
"q.us-central1.amazonaws.com",
"us-east-1-aiplatform.googleapis.com",
"q.us-east-1.c2s.ic.gov",
"q.us-iso-east-1.sc2s.sgov.gov",
"q-fips.us-gov-west-1.evil.amazonaws.com",
"codewhisperer.us-west-2.evil.amazonaws.com",
"oidc.us-east-1.evil.amazonaws.com",
@@ -5608,6 +5723,20 @@ mod tests {
}
}
#[test]
fn execution_dns_answers_allow_benchmarking_range_for_configured_exact_hosts() {
let fake = "198.18.75.234:443".parse().unwrap();
super::refresh_execution_extra_trusted_dns_hosts(Some(&json!(["custom.example.com",])));
assert!(super::validate_execution_dns_answers("custom.example.com", vec![fake]).is_ok());
assert!(
super::validate_execution_dns_answers("api.custom.example.com", vec![fake]).is_err()
);
super::refresh_execution_extra_trusted_dns_hosts(None);
assert!(super::validate_execution_dns_answers("custom.example.com", vec![fake]).is_err());
}
#[test]
fn execution_dns_answers_reject_mixed_private_results_and_strict_relay_policy() {
let fake = "198.18.75.234:443".parse().unwrap();
+4
View File
@@ -2434,6 +2434,10 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
);
}
}
match state.prewarm_execution_extra_trusted_dns_hosts().await {
Ok(_) => info!("prewarmed execution Fake-IP DNS allowlist"),
Err(err) => warn!(error = %err, "failed to prewarm execution Fake-IP DNS allowlist"),
}
match prewarm_direct_h2c_sender_cache_from_env_for_startup().await {
Ok(Some(report)) => {
if report.failed_targets > 0 {
+38 -8
View File
@@ -154,6 +154,15 @@ impl AppState {
.map_err(|err| format!("{err:?}"))
}
pub async fn prewarm_execution_extra_trusted_dns_hosts(&self) -> Result<(), String> {
self.read_system_config_json_value(
aether_admin::system::EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY,
)
.await
.map(|_| ())
.map_err(|err| format!("{err:?}"))
}
fn usage_worker_queue_for(
runtime_state: &Arc<RuntimeState>,
) -> Option<Arc<dyn RuntimeQueueStore>> {
@@ -769,6 +778,18 @@ impl AppState {
.expect("admin monitoring error stats reset cache should lock")
}
fn refresh_execution_extra_trusted_dns_hosts(
&self,
key: &str,
value: Option<&serde_json::Value>,
) {
if key.eq_ignore_ascii_case(
aether_admin::system::EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY,
) {
crate::execution_runtime::transport::refresh_execution_extra_trusted_dns_hosts(value);
}
}
pub(crate) fn mark_admin_monitoring_error_stats_reset(&self, now_unix_secs: u64) {
let mut reset_at = self
.admin_monitoring_error_stats_reset_at
@@ -781,22 +802,28 @@ impl AppState {
&self,
key: &str,
) -> Result<Option<serde_json::Value>, GatewayError> {
self.read_system_config_json_value_with_cache_windows(
key,
SYSTEM_CONFIG_CACHE_TTL,
SYSTEM_CONFIG_CACHE_MAX_STALENESS,
)
.await
let value = self
.read_system_config_json_value_with_cache_windows(
key,
SYSTEM_CONFIG_CACHE_TTL,
SYSTEM_CONFIG_CACHE_MAX_STALENESS,
)
.await?;
self.refresh_execution_extra_trusted_dns_hosts(key, value.as_ref());
Ok(value)
}
pub(crate) async fn read_system_config_json_value_strong(
&self,
key: &str,
) -> Result<Option<serde_json::Value>, GatewayError> {
self.data
let value = self
.data
.find_system_config_value_strong(key)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))
.map_err(|err| GatewayError::Internal(err.to_string()))?;
self.refresh_execution_extra_trusted_dns_hosts(key, value.as_ref());
Ok(value)
}
pub(crate) async fn compare_and_set_system_config_string_value(
@@ -934,6 +961,7 @@ impl AppState {
.map_err(|err| GatewayError::Internal(err.to_string()))?;
self.system_config_cache
.insert(key.to_string(), None, SYSTEM_CONFIG_CACHE_MAX_STALENESS);
self.refresh_execution_extra_trusted_dns_hosts(key, None);
if deleted && system_config_key_affects_scheduler(key) {
self.invalidate_scheduler_affinity_cache();
}
@@ -1015,6 +1043,7 @@ impl AppState {
}
fn remember_system_config_write(&self, key: &str, value: Option<serde_json::Value>) {
self.refresh_execution_extra_trusted_dns_hosts(key, value.as_ref());
self.system_config_cache
.insert(key.to_string(), value, SYSTEM_CONFIG_CACHE_MAX_STALENESS);
if system_config_key_affects_scheduler(key) {
@@ -1062,6 +1091,7 @@ impl AppState {
| aether_data::repository::system::AdminSystemPurgeTarget::Stats
) {
self.system_config_cache.clear();
crate::execution_runtime::transport::refresh_execution_extra_trusted_dns_hosts(None);
self.invalidate_provider_routing_caches();
}
Ok(summary)
+92
View File
@@ -67,6 +67,9 @@ pub fn admin_email_template_html_is_valid(value: &str) -> bool {
pub const ADMIN_SYSTEM_CONFIG_EXPORT_VERSION: &str = "2.3";
pub const ADMIN_SYSTEM_CONFIG_SUPPORTED_VERSIONS: &[&str] =
&["2.0", "2.1", "2.2", ADMIN_SYSTEM_CONFIG_EXPORT_VERSION];
pub const EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY: &str = "execution_extra_trusted_dns_hosts";
pub const EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_MAX_ENTRIES: usize = 128;
pub const EXECUTION_EXTRA_TRUSTED_DNS_HOST_MAX_BYTES: usize = 253;
pub const ADMIN_SYSTEM_USERS_EXPORT_VERSION: &str = "1.6";
pub const ADMIN_SYSTEM_USERS_SUPPORTED_VERSIONS: &[&str] =
&["1.3", "1.4", "1.5", ADMIN_SYSTEM_USERS_EXPORT_VERSION];
@@ -2278,6 +2281,7 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
"email_suffix_mode" => Some(json!("none")),
"email_suffix_list" => Some(json!([])),
"enable_format_conversion" => Some(json!(false)),
EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY => Some(json!([])),
"enable_model_directives" => Some(json!(false)),
// Failover after a provider-side Cyber policy refusal is an explicit
// opt-in. Keep the system-config fallback aligned with the routing
@@ -2317,6 +2321,55 @@ pub fn admin_system_config_default_value(key: &str) -> Option<serde_json::Value>
}
}
pub fn normalize_execution_extra_trusted_dns_hosts_config_value(
value: serde_json::Value,
) -> Result<serde_json::Value, ()> {
let values = match value {
Value::Null => Vec::new(),
Value::Array(values) => values,
_ => return Err(()),
};
if values.len() > EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_MAX_ENTRIES {
return Err(());
}
let mut hosts = BTreeSet::new();
for value in values {
let host = value.as_str().map(str::trim).ok_or(())?;
let host = host.trim_end_matches('.').to_ascii_lowercase();
if !execution_extra_trusted_dns_host_is_valid(&host) {
return Err(());
}
hosts.insert(host);
}
Ok(Value::Array(hosts.into_iter().map(Value::String).collect()))
}
fn execution_extra_trusted_dns_host_is_valid(host: &str) -> bool {
if host.is_empty()
|| host.len() > EXECUTION_EXTRA_TRUSTED_DNS_HOST_MAX_BYTES
|| !host.is_ascii()
|| host.parse::<std::net::IpAddr>().is_ok()
{
return false;
}
let labels = host.split('.').collect::<Vec<_>>();
if labels.len() < 2 {
return false;
}
labels.iter().all(|label| {
!label.is_empty()
&& label.len() <= 63
&& !label.starts_with('-')
&& !label.ends_with('-')
&& label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
})
}
pub fn build_admin_system_configs_payload(
entries: &[StoredSystemConfigEntry],
) -> serde_json::Value {
@@ -2808,6 +2861,15 @@ pub fn parse_admin_system_config_update(
)
})?;
}
EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_CONFIG_KEY => {
value =
normalize_execution_extra_trusted_dns_hosts_config_value(value).map_err(|_| {
(
http::StatusCode::BAD_REQUEST,
json!({ "detail": "额外可信 Fake-IP 域名配置格式无效" }),
)
})?;
}
"module.important_notification.default_channel" => {
value = normalize_notification_channel_value(value).map_err(|_| {
(
@@ -4548,6 +4610,36 @@ mod tests {
.is_err());
}
#[test]
fn extra_trusted_dns_hosts_update_normalizes_exact_hostnames() {
let update = parse_admin_system_config_update(
"execution_extra_trusted_dns_hosts",
br#"{"value":[" API.Example.COM. ","api.example.com"]}"#,
)
.expect("valid extra trusted DNS hosts should parse");
assert_eq!(update.value, json!(["api.example.com"]));
}
#[test]
fn extra_trusted_dns_hosts_update_rejects_non_exact_hostnames() {
for value in [
r#"["*.example.com"]"#,
r#"["example.com:443"]"#,
r#"["https://example.com/path"]"#,
r#"["10.0.0.1"]"#,
r#"["example..com"]"#,
r#"["localhost"]"#,
] {
let body = format!(r#"{{"value":{value}}}"#);
let error = parse_admin_system_config_update(
"execution_extra_trusted_dns_hosts",
body.as_bytes(),
)
.expect_err("non-exact hostname should be rejected");
assert_eq!(error.0, http::StatusCode::BAD_REQUEST);
}
}
#[test]
fn legacy_notification_email_config_key_normalizes_to_important_notification() {
assert_eq!(
@@ -50,12 +50,14 @@
<ProxyConfigSection
id="section-proxy"
:proxy-node-id="systemConfig.system_proxy_node_id"
:extra-trusted-dns-hosts-str="extraTrustedDnsHostsStr"
:online-nodes="proxyNodesStore.onlineNodes"
:all-nodes="proxyNodesStore.nodes"
:loading="systemConfigLoading || proxyConfigLoading"
:has-changes="hasProxyConfigChanges"
@save="saveProxyConfig"
@update:proxy-node-id="systemConfig.system_proxy_node_id = $event"
@update:extra-trusted-dns-hosts-str="extraTrustedDnsHostsStr = $event"
/>
<!-- 基础配置 -->
@@ -351,6 +353,7 @@ const {
hasCleanupConfigChanges,
sensitiveHeadersStr,
turnstileAllowedHostnamesStr,
extraTrustedDnsHostsStr,
loadSystemConfig,
loadSystemVersion,
saveSiteInfo,
@@ -40,6 +40,25 @@
对未单独配置代理的提供商生效,覆盖大模型 API 请求、余额查询、OAuth 刷新等。不影响系统内部接口。
</p>
</div>
<div class="mt-6 max-w-2xl border-t pt-5">
<Label
for="extra-trusted-dns-hosts"
class="block text-sm font-medium"
>
额外可信 Fake-IP 域名
</Label>
<Textarea
id="extra-trusted-dns-hosts"
:model-value="extraTrustedDnsHostsStr"
rows="4"
class="mt-1"
placeholder="api.example.com\nmodels.example.com"
@update:model-value="$emit('update:extraTrustedDnsHostsStr', String($event || ''))"
/>
<p class="mt-1 text-xs text-muted-foreground">
每行填写一个精确 hostname;不支持通配符、后缀、端口、路径或 IP。仅允许这些域名返回 198.18.0.0/15 Fake-IP,内置提供商域名不受影响。
</p>
</div>
</CardSection>
</template>
@@ -47,6 +66,7 @@
import { computed } from 'vue'
import Button from '@/components/ui/button.vue'
import Label from '@/components/ui/label.vue'
import Textarea from '@/components/ui/textarea.vue'
import Select from '@/components/ui/select.vue'
import SelectTrigger from '@/components/ui/select-trigger.vue'
import SelectValue from '@/components/ui/select-value.vue'
@@ -64,6 +84,7 @@ interface ProxyNode {
const props = defineProps<{
proxyNodeId: string | null
extraTrustedDnsHostsStr: string
onlineNodes: ProxyNode[]
allNodes: ProxyNode[]
loading: boolean
@@ -73,6 +94,7 @@ const props = defineProps<{
defineEmits<{
save: []
'update:proxyNodeId': [value: string | null]
'update:extraTrustedDnsHostsStr': [value: string]
}>()
const selectableNodes = computed(() => {
@@ -80,4 +80,31 @@ describe('useSystemConfig', () => {
expect(state.systemConfig.value).not.toHaveProperty('max_request_body_size')
expect(state.systemConfig.value).not.toHaveProperty('max_response_body_size')
})
it('loads and saves the extra trusted Fake-IP DNS hosts with proxy settings', async () => {
getAllSystemConfigsMock.mockResolvedValue([
{ key: 'system_proxy_node_id', value: 'node-1' },
{ key: 'execution_extra_trusted_dns_hosts', value: ['custom.example.com'] },
])
updateSystemConfigMock.mockResolvedValue(undefined)
const state = useSystemConfig()
await state.loadSystemConfig()
expect(state.extraTrustedDnsHostsStr.value).toBe('custom.example.com')
state.extraTrustedDnsHostsStr.value = 'api.example.com\nmodels.example.com'
expect(state.systemConfig.value.execution_extra_trusted_dns_hosts).toEqual([
'api.example.com',
'models.example.com',
])
await state.saveProxyConfig()
expect(updateSystemConfigMock).toHaveBeenCalledWith(
'execution_extra_trusted_dns_hosts',
['api.example.com', 'models.example.com'],
'执行运行时额外可信 Fake-IP 域名'
)
expect(state.hasProxyConfigChanges.value).toBe(false)
})
})
@@ -10,6 +10,7 @@ export interface SystemConfig {
site_subtitle: string
// 网络代理
system_proxy_node_id: string | null
execution_extra_trusted_dns_hosts: string[]
// 基础配置
default_user_initial_gift_usd: number
rate_limit_per_minute: number
@@ -61,6 +62,7 @@ const CONFIG_KEYS = [
'site_subtitle',
// 网络代理
'system_proxy_node_id',
'execution_extra_trusted_dns_hosts',
// 基础配置
'default_user_initial_gift_usd',
'rate_limit_per_minute',
@@ -112,6 +114,7 @@ function createDefaultConfig(): SystemConfig {
site_subtitle: 'AI Gateway',
// 网络代理
system_proxy_node_id: null,
execution_extra_trusted_dns_hosts: [],
// 基础配置
default_user_initial_gift_usd: 10.0,
rate_limit_per_minute: 0,
@@ -188,6 +191,8 @@ export function useSystemConfig() {
if (systemConfigLoading.value) return false
if (!originalConfig.value) return false
return systemConfig.value.system_proxy_node_id !== originalConfig.value.system_proxy_node_id
|| JSON.stringify(systemConfig.value.execution_extra_trusted_dns_hosts) !==
JSON.stringify(originalConfig.value.execution_extra_trusted_dns_hosts)
})
const hasBasicConfigChanges = computed(() => {
@@ -278,6 +283,16 @@ export function useSystemConfig() {
},
})
const extraTrustedDnsHostsStr = computed({
get: () => systemConfig.value.execution_extra_trusted_dns_hosts.join('\n'),
set: (val: string) => {
systemConfig.value.execution_extra_trusted_dns_hosts = val
.split(/[\n,]/)
.map((s) => s.trim().toLowerCase().replace(/\.$/, ''))
.filter((s) => s.length > 0)
},
})
// 加载配置
async function loadSystemConfig() {
systemConfigLoading.value = true
@@ -355,13 +370,23 @@ export function useSystemConfig() {
async function saveProxyConfig() {
proxyConfigLoading.value = true
try {
await adminApi.updateSystemConfig(
'system_proxy_node_id',
systemConfig.value.system_proxy_node_id || null,
'系统默认代理节点 ID'
)
await Promise.all([
adminApi.updateSystemConfig(
'system_proxy_node_id',
systemConfig.value.system_proxy_node_id || null,
'系统默认代理节点 ID'
),
adminApi.updateSystemConfig(
'execution_extra_trusted_dns_hosts',
systemConfig.value.execution_extra_trusted_dns_hosts,
'执行运行时额外可信 Fake-IP 域名'
),
])
if (originalConfig.value) {
originalConfig.value.system_proxy_node_id = systemConfig.value.system_proxy_node_id
originalConfig.value.execution_extra_trusted_dns_hosts = [
...systemConfig.value.execution_extra_trusted_dns_hosts,
]
}
success('网络代理配置已保存')
} catch (err) {
@@ -716,6 +741,7 @@ export function useSystemConfig() {
// 计算属性
sensitiveHeadersStr,
turnstileAllowedHostnamesStr,
extraTrustedDnsHostsStr,
// 加载函数
loadSystemConfig,
loadSystemVersion,