mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 12:40:20 +08:00
fix(tunnel): guard private owner relay targets
This commit is contained in:
@@ -106,6 +106,9 @@ ADMIN_USERNAME=admin123456
|
|||||||
# AETHER_INTERNAL_GATEWAY_AUTH_SECRET=
|
# AETHER_INTERNAL_GATEWAY_AUTH_SECRET=
|
||||||
# 远程 relay 地址必须使用 HTTPS;HTTP 仅允许 localhost 或回环 IP。
|
# 远程 relay 地址必须使用 HTTPS;HTTP 仅允许 localhost 或回环 IP。
|
||||||
# AETHER_TUNNEL_RELAY_BASE_URL=https://gateway-a.example.com
|
# AETHER_TUNNEL_RELAY_BASE_URL=https://gateway-a.example.com
|
||||||
|
# 跨网关 relay 解析到受控私有地址时才显式开启;默认关闭以防止被篡改的 attachment
|
||||||
|
# 记录诱导网关向内网转发 relay 凭据。该开关不放宽普通 provider 的目标地址策略。
|
||||||
|
# AETHER_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS=false
|
||||||
# Bark 自建服务默认仅允许公网 HTTPS。确需明文 HTTP 或内网目标时分别显式开启:
|
# Bark 自建服务默认仅允许公网 HTTPS。确需明文 HTTP 或内网目标时分别显式开启:
|
||||||
# AETHER_BARK_ALLOW_HTTP=false
|
# AETHER_BARK_ALLOW_HTTP=false
|
||||||
# AETHER_BARK_ALLOW_PRIVATE_TARGETS=false
|
# AETHER_BARK_ALLOW_PRIVATE_TARGETS=false
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ Aether Tunnel 是配套的正向代理节点,部署在海外 VPS 上,为墙
|
|||||||
- `AETHER_MAX_REQUEST_BODY_MB`:单请求解压后请求体上限,默认 `256MB`;显式设为 `0` 表示不再收紧默认值,但仍受 `256MB` 安全硬上限约束
|
- `AETHER_MAX_REQUEST_BODY_MB`:单请求解压后请求体上限,默认 `256MB`;显式设为 `0` 表示不再收紧默认值,但仍受 `256MB` 安全硬上限约束
|
||||||
- `AETHER_MAX_INTERNAL_BUFFERED_BODY_MB`:heartbeat、管理探测等内部整包响应体上限,默认 `64MB`;显式设为 `0` 表示不再收紧默认值,但仍受 `256MB` 安全硬上限约束
|
- `AETHER_MAX_INTERNAL_BUFFERED_BODY_MB`:heartbeat、管理探测等内部整包响应体上限,默认 `64MB`;显式设为 `0` 表示不再收紧默认值,但仍受 `256MB` 安全硬上限约束
|
||||||
- `AETHER_TUNNEL_NODE_STATUS_QUEUE_CAPACITY`:隧道节点状态上报队列容量,默认 `1024`;满载时拒绝新事件,避免控制面故障导致无界内存增长
|
- `AETHER_TUNNEL_NODE_STATUS_QUEUE_CAPACITY`:隧道节点状态上报队列容量,默认 `1024`;满载时拒绝新事件,避免控制面故障导致无界内存增长
|
||||||
|
- `AETHER_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS`:跨网关 owner relay 解析到私有/保留地址时的显式运维开关,默认关闭;仅当多网关 relay URL 是受控的内网 HTTPS 地址时设置为 `true`。它不改变普通 provider 请求的 DNS/代理策略,也不允许明文 HTTP 非 loopback relay
|
||||||
- `AETHER_INTERNAL_GATEWAY_AUTH_SECRET`:旧版 `/api/internal/gateway/*` 高权限控制面的独立 HMAC 密钥,至少 `32` 字节;未配置时该控制面返回 `404`。不要复用 JWT、数据加密或 tunnel relay 密钥,多节点必须使用同一值及共享 Redis 防重放
|
- `AETHER_INTERNAL_GATEWAY_AUTH_SECRET`:旧版 `/api/internal/gateway/*` 高权限控制面的独立 HMAC 密钥,至少 `32` 字节;未配置时该控制面返回 `404`。不要复用 JWT、数据加密或 tunnel relay 密钥,多节点必须使用同一值及共享 Redis 防重放
|
||||||
- `AETHER_GATEWAY_SECURITY_CACHE_TTL_MS`:IP 黑白名单本地缓存时间,默认 `1000ms`,写操作会主动失效相关缓存
|
- `AETHER_GATEWAY_SECURITY_CACHE_TTL_MS`:IP 黑白名单本地缓存时间,默认 `1000ms`,写操作会主动失效相关缓存
|
||||||
- `AETHER_MAX_REDACTED_SYNC_RESPONSE_BODY_MB`:PII 恢复同步响应缓冲上限,默认 `64MB`;显式设为 `0` 表示不再收紧默认值,但仍受 `256MB` 安全硬上限约束
|
- `AETHER_MAX_REDACTED_SYNC_RESPONSE_BODY_MB`:PII 恢复同步响应缓冲上限,默认 `64MB`;显式设为 `0` 表示不再收紧默认值,但仍受 `256MB` 安全硬上限约束
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ const TUNNEL_ATTACHMENT_KEY_PREFIX: &str = "tunnel.attachments.";
|
|||||||
const TUNNEL_ATTACHMENT_REDIS_KEY_PREFIX: &str = "tunnel:attachments:";
|
const TUNNEL_ATTACHMENT_REDIS_KEY_PREFIX: &str = "tunnel:attachments:";
|
||||||
const TUNNEL_INSTANCE_ID_ENV: &str = "AETHER_GATEWAY_INSTANCE_ID";
|
const TUNNEL_INSTANCE_ID_ENV: &str = "AETHER_GATEWAY_INSTANCE_ID";
|
||||||
const TUNNEL_RELAY_BASE_URL_ENV: &str = "AETHER_TUNNEL_RELAY_BASE_URL";
|
const TUNNEL_RELAY_BASE_URL_ENV: &str = "AETHER_TUNNEL_RELAY_BASE_URL";
|
||||||
|
// Owner-forward relay URLs are deployment metadata, but a corrupted or
|
||||||
|
// stale attachment record must not turn the gateway into a generic internal
|
||||||
|
// network client. Private HTTPS relay targets therefore require an explicit
|
||||||
|
// operator opt-in, just like the tunnel's private upstream target escape
|
||||||
|
// hatch. Loopback HTTP remains supported for the local single-process path.
|
||||||
|
const TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS_ENV: &str = "AETHER_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS";
|
||||||
const TUNNEL_ATTACHMENT_TTL_ENV: &str = "AETHER_TUNNEL_ATTACHMENT_TTL_SECS";
|
const TUNNEL_ATTACHMENT_TTL_ENV: &str = "AETHER_TUNNEL_ATTACHMENT_TTL_SECS";
|
||||||
const TUNNEL_RELAY_AUTH_SECRET_ENV: &str = "AETHER_TUNNEL_RELAY_AUTH_SECRET";
|
const TUNNEL_RELAY_AUTH_SECRET_ENV: &str = "AETHER_TUNNEL_RELAY_AUTH_SECRET";
|
||||||
const TUNNEL_HEARTBEAT_STATE_KEY_PREFIX: &str = "tunnel:heartbeat:session:";
|
const TUNNEL_HEARTBEAT_STATE_KEY_PREFIX: &str = "tunnel:heartbeat:session:";
|
||||||
@@ -233,10 +239,26 @@ async fn resolve_owner_forward_target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_owner_forward_target(
|
fn build_owner_forward_target(
|
||||||
|
scheme: &str,
|
||||||
|
host: &str,
|
||||||
|
port: u16,
|
||||||
|
addresses: Vec<SocketAddr>,
|
||||||
|
) -> Result<ResolvedOwnerForwardTarget, String> {
|
||||||
|
build_owner_forward_target_with_policy(
|
||||||
|
scheme,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
addresses,
|
||||||
|
tunnel_relay_allows_private_targets(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_owner_forward_target_with_policy(
|
||||||
scheme: &str,
|
scheme: &str,
|
||||||
host: &str,
|
host: &str,
|
||||||
port: u16,
|
port: u16,
|
||||||
mut addresses: Vec<SocketAddr>,
|
mut addresses: Vec<SocketAddr>,
|
||||||
|
allow_private_targets: bool,
|
||||||
) -> Result<ResolvedOwnerForwardTarget, String> {
|
) -> Result<ResolvedOwnerForwardTarget, String> {
|
||||||
let host = host.trim();
|
let host = host.trim();
|
||||||
if host.is_empty() {
|
if host.is_empty() {
|
||||||
@@ -266,6 +288,19 @@ fn build_owner_forward_target(
|
|||||||
{
|
{
|
||||||
return Err("loopback HTTP owner gateway resolved to a non-loopback address".to_string());
|
return Err("loopback HTTP owner gateway resolved to a non-loopback address".to_string());
|
||||||
}
|
}
|
||||||
|
if !allow_private_targets
|
||||||
|
&& addresses.iter().any(|address| {
|
||||||
|
aether_http::is_private_or_reserved_ip(address.ip())
|
||||||
|
// The existing local-development exception is deliberately
|
||||||
|
// narrow: HTTPS never gets an implicit loopback/private
|
||||||
|
// exemption, while literal/localhost HTTP is checked above.
|
||||||
|
&& !(scheme.eq_ignore_ascii_case("http") && address.ip().is_loopback())
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"owner gateway DNS resolution returned a private or reserved address; set {TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS_ENV}=true only for a trusted internal relay"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(ResolvedOwnerForwardTarget {
|
Ok(ResolvedOwnerForwardTarget {
|
||||||
scheme: scheme.to_ascii_lowercase(),
|
scheme: scheme.to_ascii_lowercase(),
|
||||||
@@ -276,6 +311,17 @@ fn build_owner_forward_target(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn tunnel_relay_allows_private_targets() -> bool {
|
||||||
|
std::env::var(TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS_ENV)
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|value| {
|
||||||
|
matches!(
|
||||||
|
value.trim().to_ascii_lowercase().as_str(),
|
||||||
|
"1" | "true" | "yes" | "on"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn build_owner_forward_pinned_client(
|
fn build_owner_forward_pinned_client(
|
||||||
target: &ResolvedOwnerForwardTarget,
|
target: &ResolvedOwnerForwardTarget,
|
||||||
) -> Result<reqwest::Client, String> {
|
) -> Result<reqwest::Client, String> {
|
||||||
@@ -2695,9 +2741,9 @@ fn parse_embedded_tunnel_heartbeat_request(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
apply_embedded_tunnel_heartbeat, apply_embedded_tunnel_node_status,
|
apply_embedded_tunnel_heartbeat, apply_embedded_tunnel_node_status,
|
||||||
build_owner_forward_target, build_tunnel_affinity_auth_metadata,
|
build_owner_forward_target, build_owner_forward_target_with_policy,
|
||||||
build_tunnel_owner_relay_url, build_tunnel_probe_meta, current_unix_secs,
|
build_tunnel_affinity_auth_metadata, build_tunnel_owner_relay_url, build_tunnel_probe_meta,
|
||||||
encode_tunnel_relay_envelope, owner_forward_client_for_url,
|
current_unix_secs, encode_tunnel_relay_envelope, owner_forward_client_for_url,
|
||||||
prepare_owner_relay_request_body, prepare_owner_relay_request_body_with_limits,
|
prepare_owner_relay_request_body, prepare_owner_relay_request_body_with_limits,
|
||||||
resolve_owner_forward_target, resolve_tunnel_relay_auth_secret_value,
|
resolve_owner_forward_target, resolve_tunnel_relay_auth_secret_value,
|
||||||
tunnel_attachment_key, validate_tunnel_relay_transport_url, AppState, GatewayDataState,
|
tunnel_attachment_key, validate_tunnel_relay_transport_url, AppState, GatewayDataState,
|
||||||
@@ -2797,9 +2843,9 @@ mod tests {
|
|||||||
"gateway.example.com",
|
"gateway.example.com",
|
||||||
8443,
|
8443,
|
||||||
vec![
|
vec![
|
||||||
"[2001:db8::2]:8443".parse().unwrap(),
|
"[2001:4860:4860::8888]:8443".parse().unwrap(),
|
||||||
"192.0.2.10:8443".parse().unwrap(),
|
"8.8.8.8:8443".parse().unwrap(),
|
||||||
"192.0.2.10:8443".parse().unwrap(),
|
"8.8.8.8:8443".parse().unwrap(),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.expect("valid DNS answers should produce a target");
|
.expect("valid DNS answers should produce a target");
|
||||||
@@ -2851,13 +2897,22 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
|
|
||||||
// Private HTTPS owner deployments remain supported.
|
// Private HTTPS owner deployments require an explicit deployment
|
||||||
|
// opt-in; the default path must not be an SSRF primitive.
|
||||||
assert!(build_owner_forward_target(
|
assert!(build_owner_forward_target(
|
||||||
"https",
|
"https",
|
||||||
"gateway.internal",
|
"gateway.internal",
|
||||||
8443,
|
8443,
|
||||||
vec!["10.0.0.8:8443".parse().unwrap()],
|
vec!["10.0.0.8:8443".parse().unwrap()],
|
||||||
)
|
)
|
||||||
|
.is_err());
|
||||||
|
assert!(build_owner_forward_target_with_policy(
|
||||||
|
"https",
|
||||||
|
"gateway.internal",
|
||||||
|
8443,
|
||||||
|
vec!["10.0.0.8:8443".parse().unwrap()],
|
||||||
|
true,
|
||||||
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user