diff --git a/.env.example b/.env.example index 3d6d65a93..2e5fa5e4c 100644 --- a/.env.example +++ b/.env.example @@ -111,11 +111,9 @@ ADMIN_USERNAME=admin123456 # AETHER_BARK_ALLOW_HTTP=false # AETHER_BARK_ALLOW_PRIVATE_TARGETS=false -# 普通 Provider 反代(包括 Provider OAuth)默认不按 DNS 地址过滤上游,兼容任意 -# Fake-IP 域名及内网 DNS。仅信任管理员配置的上游;需要防 DNS 重绑定时设为 true。 -# 开启后使用内置及系统配置的 DNS 域名白名单;隧道中继、登录 OAuth 的校验不受影响。 -# 此开关不放宽 URL 协议、字面 IP 或 TLS 证书校验;修改后需重启网关。 -# AETHER_PROVIDER_DNS_ADDRESS_FILTER_ENABLED=false +# 普通 Provider 反代(包括 Provider OAuth)不按 DNS 地址过滤上游,兼容任意 +# Fake-IP 域名及内网 DNS。仅信任管理员配置的上游;没有严格 DNS 过滤开关。 +# URL 协议、字面 IP、TLS 证书,以及隧道中继和登录 OAuth 的校验仍保留。 # 可选 Provider OAuth 客户端。Gemini CLI 授权及刷新必须配置 client secret。 # Antigravity 默认使用内置 native-app 客户端凭据;自定义 client ID 时必须同时配置 diff --git a/apps/aether-gateway/src/execution_runtime/transport.rs b/apps/aether-gateway/src/execution_runtime/transport.rs index 0f099fbc7..d4eb01785 100644 --- a/apps/aether-gateway/src/execution_runtime/transport.rs +++ b/apps/aether-gateway/src/execution_runtime/transport.rs @@ -22,8 +22,8 @@ use aether_contracts::{ }; use aether_data::repository::proxy_nodes::ProxyNodeTrafficMutation; use aether_http::{ - apply_http_client_config, is_https_or_loopback_http_url, is_ipv4_benchmarking_fake_ip, - is_private_or_reserved_ip, HttpClientConfig, + apply_http_client_config, is_https_or_loopback_http_url, is_private_or_reserved_ip, + HttpClientConfig, }; use aether_runtime::{MetricKind, MetricSample}; use axum::body::Bytes; @@ -63,8 +63,6 @@ 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; @@ -448,199 +446,6 @@ struct ExecutionSafeDnsResolver; #[derive(Debug, Clone, Copy, Default)] struct ExecutionSafeHyperDnsResolver; -static PROVIDER_DNS_ADDRESS_FILTER_ENABLED: LazyLock = LazyLock::new(|| { - std::env::var("AETHER_PROVIDER_DNS_ADDRESS_FILTER_ENABLED") - .ok() - .is_some_and(|value| matches_truthy_env_value(&value)) -}); - -// Local DNS interception tools may use RFC 2544's 198.18.0.0/15 range for -// synthetic answers. This exception is deliberately an allowlist rather -// than a property of the address range itself: a custom provider hostname -// must not be able to turn a local synthetic mapping into an SSRF primitive. -// Keep this list limited to origins that Aether constructs as built-in -// provider/model-fetch targets. In particular, do not use a -// suffix match for ordinary hosts (for example, `evil.chatgpt.com`). -const TRUSTED_EXECUTION_BENCHMARKING_DNS_EXACT_HOSTS: &[&str] = &[ - "aiplatform.googleapis.com", - "antigravity.googleapis.com", - "api.openai.com", - "api.anthropic.com", - "api.deepseek.com", - "chatgpt.com", - "cloudcode-pa.googleapis.com", - "daily-cloudcode-pa.googleapis.com", - "daily-cloudcode-pa.sandbox.googleapis.com", - "dashscope.aliyuncs.com", - "generativelanguage.googleapis.com", - "grok.com", - "oauth2.googleapis.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", - "www.googleapis.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>> = - 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::>() - }) - }) - .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, -) -> bool { - let host = host.trim().trim_end_matches('.').to_ascii_lowercase(); - if extra_hosts.contains(&host) - || TRUSTED_EXECUTION_BENCHMARKING_DNS_EXACT_HOSTS - .iter() - .any(|trusted| *trusted == host) - { - return true; - } - - // Vertex service-account requests use `-aiplatform.googleapis.com`. - // Keep this compatibility exception limited to known provider regions. - if let Some(region) = host.strip_suffix("-aiplatform.googleapis.com") { - return TRUSTED_EXECUTION_VERTEX_DNS_REGIONS.contains(®ion); - } - - // Kiro uses a small, fixed set of regional service origins. Match each - // supported AWS partition explicitly; never use a broad suffix check that - // could accept an attacker-controlled subdomain. - matches_regional_service_host(&host, "q", ".amazonaws.com") - || matches_regional_service_host(&host, "q-fips", ".amazonaws.com") - || 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") -} - -fn matches_regional_service_host(host: &str, service: &str, suffix: &str) -> bool { - let Some(region) = host - .strip_prefix(service) - .and_then(|value| value.strip_prefix('.')) - .and_then(|value| value.strip_suffix(suffix)) - else { - return false; - }; - TRUSTED_EXECUTION_AWS_DNS_REGIONS.contains(®ion) -} - fn dns_host_explicitly_allows_loopback(host: &str) -> bool { let host = host.trim_end_matches('.'); host.eq_ignore_ascii_case("localhost") @@ -650,17 +455,10 @@ fn dns_host_explicitly_allows_loopback(host: &str) -> bool { .unwrap_or(false) } -fn validate_execution_dns_answers( +fn validate_resolved_execution_addresses( host: &str, addresses: Vec, -) -> Result, std::io::Error> { - validate_execution_dns_answers_with_policy(host, addresses, true) -} - -fn validate_execution_dns_answers_with_policy( - host: &str, - addresses: Vec, - allow_trusted_benchmarking_dns_answer: bool, + provider_execution: bool, ) -> Result, std::io::Error> { if addresses.is_empty() { return Err(std::io::Error::new( @@ -668,25 +466,22 @@ fn validate_execution_dns_answers_with_policy( "upstream DNS resolution returned no addresses", )); } - + if provider_execution { + return Ok(addresses); + } let allows_loopback = dns_host_explicitly_allows_loopback(host); - let allows_benchmarking_dns_answer = allow_trusted_benchmarking_dns_answer - && execution_host_allows_benchmarking_dns_answer(host); - let unsafe_answer = addresses.iter().any(|address| { + if addresses.iter().any(|address| { if allows_loopback { !address.ip().is_loopback() } else { is_private_or_reserved_ip(address.ip()) - && !(allows_benchmarking_dns_answer && is_ipv4_benchmarking_fake_ip(address.ip())) } - }); - if unsafe_answer { + }) { return Err(std::io::Error::new( std::io::ErrorKind::PermissionDenied, - "upstream DNS resolution returned a private or reserved address", + "tunnel relay DNS resolution returned a private or reserved address", )); } - Ok(addresses) } @@ -697,7 +492,7 @@ async fn resolve_execution_dns_addresses(host: &str) -> Result, async fn resolve_execution_target_addresses_with_policy( host: &str, port: u16, - allow_trusted_benchmarking_dns_answer: bool, + provider_execution: bool, ) -> Result, std::io::Error> { let addresses = if let Ok(ip) = host.parse::() { vec![SocketAddr::new(ip, port)] @@ -705,24 +500,7 @@ async fn resolve_execution_target_addresses_with_policy( aether_http::lookup_host_with_limits(host, port, aether_http::DEFAULT_DNS_LOOKUP_TIMEOUT) .await? }; - validate_resolved_execution_addresses( - host, - addresses, - allow_trusted_benchmarking_dns_answer, - *PROVIDER_DNS_ADDRESS_FILTER_ENABLED, - ) -} - -fn validate_resolved_execution_addresses( - host: &str, - addresses: Vec, - provider_execution: bool, - provider_dns_address_filter_enabled: bool, -) -> Result, std::io::Error> { - if provider_execution && !provider_dns_address_filter_enabled && !addresses.is_empty() { - return Ok(addresses); - } - validate_execution_dns_answers_with_policy(host, addresses, provider_execution) + validate_resolved_execution_addresses(host, addresses, provider_execution) } impl reqwest::dns::Resolve for ExecutionSafeDnsResolver { @@ -3448,10 +3226,6 @@ async fn resolve_relay_target_addresses( let port = url.port_or_known_default().ok_or_else(|| { ExecutionRuntimeTransportError::RelayError("tunnel relay URL has no port".to_string()) })?; - // Relay destinations remain strict even when their hostname happens to be - // an official provider origin. The RFC-2544 compatibility exception is - // only for direct provider execution; allowing it here would weaken the - // relay SSRF guard. let addresses = resolve_execution_target_addresses_with_policy(host, port, false) .await .map_err(|error| match error.kind() { @@ -5659,115 +5433,14 @@ mod tests { } #[test] - fn execution_dns_answers_reject_private_addresses_and_allow_explicit_loopback() { - let public = "93.184.216.34:443".parse().unwrap(); - let private = "10.0.0.8:443".parse().unwrap(); - let loopback_v4 = "127.0.0.1:8080".parse().unwrap(); - let loopback_v6 = "[::1]:8080".parse().unwrap(); - - assert!(super::validate_execution_dns_answers("api.example.test", vec![public]).is_ok()); - assert!(super::validate_execution_dns_answers("api.example.test", vec![private]).is_err()); - assert!( - super::validate_execution_dns_answers("localhost", vec![loopback_v4, loopback_v6]) - .is_ok() - ); - assert!(super::validate_execution_dns_answers("localhost", vec![private]).is_err()); - assert!(super::validate_execution_dns_answers("api.example.test", Vec::new()).is_err()); - } - - #[test] - fn execution_dns_answers_allow_benchmarking_range_only_for_fixed_provider_hosts() { - let fake = "198.18.75.234:443".parse().unwrap(); - for host in [ - "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", - "oidc.us-east-1.amazonaws.com", - "prod.us-east-1.auth.desktop.kiro.dev", - "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", - ] { - assert!( - super::validate_execution_dns_answers(host, vec![fake]).is_ok(), - "fixed provider host should accept a benchmarking DNS answer: {host}" - ); - } - - for host in [ - "api.example.test", - "evil.chatgpt.com", - "api.openai.com.evil.test", - "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", - "prod.us-east-1.auth.desktop.kiro.dev.attacker.test", - "prod.us-east-1.evil.auth.desktop.kiro.dev", - "q.us-iso-east-1.evil.c2s.ic.gov", - "q.us-iso-east-1.c2s.ic.gov.attacker.test", - "q.us-iso-east-1.c2s.ic.gov.evil", - "198.18.75.234", - ] { - assert!( - super::validate_execution_dns_answers(host, vec![fake]).is_err(), - "untrusted or lookalike host must reject a benchmarking DNS answer: {host}" - ); - } - } - - #[test] - fn google_oauth_execution_dns_allows_only_exact_hosts_and_fake_ip_answers() { - let fake = "198.18.78.41:443".parse().unwrap(); - for host in ["oauth2.googleapis.com", "www.googleapis.com"] { - assert!(super::validate_execution_dns_answers(host, vec![fake]).is_ok()); - assert!( - super::validate_execution_dns_answers_with_policy(host, vec![fake], false).is_err() - ); - for private in [ - "127.0.0.1:443", - "10.0.0.1:443", - "169.254.169.254:443", - "[::1]:443", - ] { - let private = private.parse().unwrap(); - assert!(super::validate_execution_dns_answers(host, vec![private]).is_err()); - assert!(super::validate_execution_dns_answers(host, vec![fake, private]).is_err()); - } - } - for host in [ - "oauth2.googleapis.com.attacker.test", - "www.googleapis.com.attacker.test", - "evil.oauth2.googleapis.com", - "evil.googleapis.com", - ] { - assert!(super::validate_execution_dns_answers(host, vec![fake]).is_err()); - } - } - - #[test] - fn execution_dns_address_filter_is_optional_only_for_provider_connections() { + fn execution_dns_answers_allow_all_provider_hosts_without_address_filtering() { let addresses = vec![ "198.18.78.41:443".parse().unwrap(), "10.0.0.8:443".parse().unwrap(), "127.0.0.1:443".parse().unwrap(), + "169.254.169.254:443".parse().unwrap(), "[fd00::1]:443".parse().unwrap(), + "93.184.216.34:443".parse().unwrap(), ]; for host in [ "oauth2.googleapis.com", @@ -5775,72 +5448,60 @@ mod tests { "custom.example.test", ] { assert_eq!( - super::validate_resolved_execution_addresses(host, addresses.clone(), true, false) - .expect("provider DNS answers should pass through without filtering"), - addresses, + super::validate_resolved_execution_addresses(host, addresses.clone(), true) + .expect("provider DNS answers should pass through"), + addresses ); - assert!(super::validate_resolved_execution_addresses( - host, - addresses.clone(), - true, - true - ) - .is_err()); - for filter_enabled in [false, true] { - assert!(super::validate_resolved_execution_addresses( - host, - addresses.clone(), - false, - filter_enabled - ) - .is_err()); - assert!(super::validate_resolved_execution_addresses( - host, - Vec::new(), - true, - filter_enabled - ) - .is_err()); - } } } #[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(); + fn execution_dns_answers_keep_relay_address_filtering() { let public = "93.184.216.34:443".parse().unwrap(); - let private = "10.0.0.8:443".parse().unwrap(); - - // A trusted host may have a synthetic answer alongside a genuine public - // answer, but any real private answer still fails closed. + for host in ["oauth2.googleapis.com", "custom.example.test"] { + assert!( + super::validate_resolved_execution_addresses(host, vec![public], false).is_ok() + ); + for blocked in [ + "198.18.78.41:443", + "10.0.0.8:443", + "127.0.0.1:443", + "169.254.169.254:443", + "[fd00::1]:443", + ] { + let blocked = blocked.parse().unwrap(); + assert!( + super::validate_resolved_execution_addresses(host, vec![blocked], false) + .is_err() + ); + assert!(super::validate_resolved_execution_addresses( + host, + vec![public, blocked], + false + ) + .is_err()); + } + } + let loopback = vec![ + "127.0.0.1:443".parse().unwrap(), + "[::1]:443".parse().unwrap(), + ]; + assert!(super::validate_resolved_execution_addresses("localhost", loopback, false).is_ok()); assert!( - super::validate_execution_dns_answers("api.openai.com", vec![fake, public]).is_ok() + super::validate_resolved_execution_addresses("localhost", vec![public], false).is_err() ); - assert!( - super::validate_execution_dns_answers("api.openai.com", vec![fake, private]).is_err() - ); - - // Tunnel relay resolution opts out of the compatibility exception. - assert!(super::validate_execution_dns_answers_with_policy( - "api.openai.com", - vec![fake], - false, - ) - .is_err()); + for provider_execution in [false, true] { + assert_eq!( + super::validate_resolved_execution_addresses( + "custom.example.test", + Vec::new(), + provider_execution + ) + .expect_err("empty DNS answers must fail") + .kind(), + std::io::ErrorKind::NotFound + ); + } } #[test] diff --git a/apps/aether-gateway/src/main.rs b/apps/aether-gateway/src/main.rs index 2341a493e..0721a9d5d 100644 --- a/apps/aether-gateway/src/main.rs +++ b/apps/aether-gateway/src/main.rs @@ -2408,10 +2408,6 @@ async fn run() -> Result<(), Box> { ); } } - 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 { diff --git a/apps/aether-gateway/src/state/core.rs b/apps/aether-gateway/src/state/core.rs index 236073550..44731f40d 100644 --- a/apps/aether-gateway/src/state/core.rs +++ b/apps/aether-gateway/src/state/core.rs @@ -154,15 +154,6 @@ 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, ) -> Option> { @@ -778,18 +769,6 @@ 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 @@ -809,7 +788,6 @@ impl AppState { SYSTEM_CONFIG_CACHE_MAX_STALENESS, ) .await?; - self.refresh_execution_extra_trusted_dns_hosts(key, value.as_ref()); Ok(value) } @@ -822,7 +800,6 @@ impl AppState { .find_system_config_value_strong(key) .await .map_err(|err| GatewayError::Internal(err.to_string()))?; - self.refresh_execution_extra_trusted_dns_hosts(key, value.as_ref()); Ok(value) } @@ -961,7 +938,6 @@ 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(); } @@ -1043,7 +1019,6 @@ impl AppState { } fn remember_system_config_write(&self, key: &str, value: Option) { - 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) { @@ -1091,7 +1066,6 @@ 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) diff --git a/crates/aether-admin/src/system.rs b/crates/aether-admin/src/system.rs index 9cafb2975..3e276f34b 100644 --- a/crates/aether-admin/src/system.rs +++ b/crates/aether-admin/src/system.rs @@ -67,17 +67,6 @@ 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; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExecutionExtraTrustedDnsHostsConfigError { - InvalidValue, - TooManyEntries, - InvalidHost, -} - 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]; @@ -2289,7 +2278,6 @@ pub fn admin_system_config_default_value(key: &str) -> Option "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 @@ -2329,58 +2317,6 @@ pub fn admin_system_config_default_value(key: &str) -> Option } } -pub fn normalize_execution_extra_trusted_dns_hosts_config_value( - value: serde_json::Value, -) -> Result { - let values = match value { - Value::Null => Vec::new(), - Value::Array(values) => values, - _ => return Err(ExecutionExtraTrustedDnsHostsConfigError::InvalidValue), - }; - if values.len() > EXECUTION_EXTRA_TRUSTED_DNS_HOSTS_MAX_ENTRIES { - return Err(ExecutionExtraTrustedDnsHostsConfigError::TooManyEntries); - } - - let mut hosts = BTreeSet::new(); - for value in values { - let host = value - .as_str() - .map(str::trim) - .ok_or(ExecutionExtraTrustedDnsHostsConfigError::InvalidHost)?; - let host = host.trim_end_matches('.').to_ascii_lowercase(); - if !execution_extra_trusted_dns_host_is_valid(&host) { - return Err(ExecutionExtraTrustedDnsHostsConfigError::InvalidHost); - } - 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::().is_ok() - { - return false; - } - - let labels = host.split('.').collect::>(); - 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 { @@ -2872,15 +2808,6 @@ 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(|_| { ( @@ -4621,36 +4548,6 @@ 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!( diff --git a/frontend/src/i18n/legacy-ui-messages.ts b/frontend/src/i18n/legacy-ui-messages.ts index be7f97167..3edc6fffc 100644 --- a/frontend/src/i18n/legacy-ui-messages.ts +++ b/frontend/src/i18n/legacy-ui-messages.ts @@ -422,8 +422,6 @@ export const legacyUiEnglishMessages: Record = { '至少选择一个清理范围': 'Select at least one cleanup scope', '等待后台返回结果': 'Waiting for the server to return results', '默认代理节点': 'Default proxy node', - '额外可信 Fake-IP 域名': 'Additional trusted Fake-IP domains', - '每行填写一个精确 hostname;不支持通配符、后缀、端口、路径或 IP。仅允许这些域名返回 198.18.0.0/15 Fake-IP,内置提供商域名不受影响。': 'Enter one exact hostname per line. Wildcards, suffixes, ports, paths, and IP addresses are not supported. Only these domains may return Fake-IP addresses in 198.18.0.0/15. Built-in provider domains are unaffected.', '配置系统后台定时任务': 'Configure scheduled background tasks', '显示在导航栏、登录页标题和邮件中': 'Shown in the navigation bar, sign-in page title, and emails', '显示在导航栏品牌名称下方': 'Shown below the brand name in the navigation bar', diff --git a/frontend/src/views/admin/SystemSettings.vue b/frontend/src/views/admin/SystemSettings.vue index 515749539..4940022cb 100644 --- a/frontend/src/views/admin/SystemSettings.vue +++ b/frontend/src/views/admin/SystemSettings.vue @@ -50,14 +50,12 @@ @@ -353,7 +351,6 @@ const { hasCleanupConfigChanges, sensitiveHeadersStr, turnstileAllowedHostnamesStr, - extraTrustedDnsHostsStr, loadSystemConfig, loadSystemVersion, saveSiteInfo, diff --git a/frontend/src/views/admin/system-settings/ProxyConfigSection.vue b/frontend/src/views/admin/system-settings/ProxyConfigSection.vue index 29eb6a625..df8e22e62 100644 --- a/frontend/src/views/admin/system-settings/ProxyConfigSection.vue +++ b/frontend/src/views/admin/system-settings/ProxyConfigSection.vue @@ -40,25 +40,6 @@ 对未单独配置代理的提供商生效,覆盖大模型 API 请求、余额查询、OAuth 刷新等。不影响系统内部接口。

-
- -