From e15ea0d5d316de4a9ea849f08c95cedaeab7e61e Mon Sep 17 00:00:00 2001
From: elky
Date: Sat, 5 Sep 2026 14:05:07 +0800
Subject: [PATCH] fix(security): configure trusted Fake-IP DNS hosts
---
.../src/execution_runtime/transport.rs | 179 +++++++++++++++---
apps/aether-gateway/src/main.rs | 4 +
apps/aether-gateway/src/state/core.rs | 46 ++++-
crates/aether-admin/src/system.rs | 92 +++++++++
frontend/src/views/admin/SystemSettings.vue | 3 +
.../system-settings/ProxyConfigSection.vue | 22 +++
.../__tests__/useSystemConfig.spec.ts | 27 +++
.../composables/useSystemConfig.ts | 36 +++-
8 files changed, 371 insertions(+), 38 deletions(-)
diff --git a/apps/aether-gateway/src/execution_runtime/transport.rs b/apps/aether-gateway/src/execution_runtime/transport.rs
index 8bc6bd113..411c69fcf 100644
--- a/apps/aether-gateway/src/execution_runtime/transport.rs
+++ b/apps/aether-gateway/src/execution_runtime/transport.rs
@@ -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>> =
+ 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 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 `-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(®ion);
}
// 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(®ion)
}
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();
diff --git a/apps/aether-gateway/src/main.rs b/apps/aether-gateway/src/main.rs
index 46ae58ac3..571dee507 100644
--- a/apps/aether-gateway/src/main.rs
+++ b/apps/aether-gateway/src/main.rs
@@ -2434,6 +2434,10 @@ 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 0c455e544..236073550 100644
--- a/apps/aether-gateway/src/state/core.rs
+++ b/apps/aether-gateway/src/state/core.rs
@@ -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,
) -> Option> {
@@ -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
+
+
+
+
+ 每行填写一个精确 hostname;不支持通配符、后缀、端口、路径或 IP。仅允许这些域名返回 198.18.0.0/15 Fake-IP,内置提供商域名不受影响。
+
+
@@ -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(() => {
diff --git a/frontend/src/views/admin/system-settings/__tests__/useSystemConfig.spec.ts b/frontend/src/views/admin/system-settings/__tests__/useSystemConfig.spec.ts
index e54952728..939fc00e2 100644
--- a/frontend/src/views/admin/system-settings/__tests__/useSystemConfig.spec.ts
+++ b/frontend/src/views/admin/system-settings/__tests__/useSystemConfig.spec.ts
@@ -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)
+ })
})
diff --git a/frontend/src/views/admin/system-settings/composables/useSystemConfig.ts b/frontend/src/views/admin/system-settings/composables/useSystemConfig.ts
index 6c2c86bf9..d6f8f17e4 100644
--- a/frontend/src/views/admin/system-settings/composables/useSystemConfig.ts
+++ b/frontend/src/views/admin/system-settings/composables/useSystemConfig.ts
@@ -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,