mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-11 11:49:50 +08:00
fix(tunnel): allow exact private relay hosts
This commit is contained in:
@@ -109,6 +109,8 @@ ADMIN_USERNAME=admin123456
|
|||||||
# 跨网关 relay 解析到受控私有地址时才显式开启;默认关闭以防止被篡改的 attachment
|
# 跨网关 relay 解析到受控私有地址时才显式开启;默认关闭以防止被篡改的 attachment
|
||||||
# 记录诱导网关向内网转发 relay 凭据。该开关不放宽普通 provider 的目标地址策略。
|
# 记录诱导网关向内网转发 relay 凭据。该开关不放宽普通 provider 的目标地址策略。
|
||||||
# AETHER_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS=false
|
# AETHER_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS=false
|
||||||
|
# 更推荐按 relay 主机名精确放行私网部署(逗号分隔,大小写不敏感);不支持通配符/后缀。
|
||||||
|
# AETHER_TUNNEL_RELAY_PRIVATE_HOST_ALLOWLIST=gateway-a.internal,gateway-b.internal
|
||||||
# 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
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ Aether Tunnel 是配套的正向代理节点,部署在海外 VPS 上,为墙
|
|||||||
- `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_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS`:跨网关 owner relay 解析到私有/保留地址时的显式运维开关,默认关闭;仅当多网关 relay URL 是受控的内网 HTTPS 地址时设置为 `true`。它不改变普通 provider 请求的 DNS/代理策略,也不允许明文 HTTP 非 loopback relay
|
||||||
|
- `AETHER_TUNNEL_RELAY_PRIVATE_HOST_ALLOWLIST`:更窄的 owner relay 私网例外,填写逗号分隔的精确主机名(例如 `gateway-a.internal,gateway-b.internal`,忽略大小写和末尾点);仅这些主机解析出的私有地址会被允许,并且请求仍使用解析后地址 pin。不要填写通配符或 `.internal` 这类后缀
|
||||||
- `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` 安全硬上限约束
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ const TUNNEL_RELAY_BASE_URL_ENV: &str = "AETHER_TUNNEL_RELAY_BASE_URL";
|
|||||||
// operator opt-in, just like the tunnel's private upstream target escape
|
// operator opt-in, just like the tunnel's private upstream target escape
|
||||||
// hatch. Loopback HTTP remains supported for the local single-process path.
|
// 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_RELAY_ALLOW_PRIVATE_TARGETS_ENV: &str = "AETHER_TUNNEL_RELAY_ALLOW_PRIVATE_TARGETS";
|
||||||
|
// Prefer this narrow host allowlist for private owner relay deployments. The
|
||||||
|
// legacy *_ALLOW_PRIVATE_TARGETS switch remains available for operators that
|
||||||
|
// intentionally trust every private address in their deployment, but an exact
|
||||||
|
// hostname list limits the blast radius of a stale or corrupted attachment.
|
||||||
|
const TUNNEL_RELAY_PRIVATE_HOST_ALLOWLIST_ENV: &str = "AETHER_TUNNEL_RELAY_PRIVATE_HOST_ALLOWLIST";
|
||||||
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:";
|
||||||
@@ -249,7 +254,7 @@ fn build_owner_forward_target(
|
|||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
addresses,
|
addresses,
|
||||||
tunnel_relay_allows_private_targets(),
|
tunnel_relay_allows_private_targets() || tunnel_relay_private_host_is_allowlisted(host),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +327,33 @@ fn tunnel_relay_allows_private_targets() -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return whether a private owner relay hostname was explicitly allowlisted.
|
||||||
|
///
|
||||||
|
/// Entries are comma-separated exact DNS names (case-insensitive); a trailing
|
||||||
|
/// dot is ignored for DNS canonicalisation. We intentionally do not support
|
||||||
|
/// arbitrary suffix or wildcard matching: allowing `.internal` would let a
|
||||||
|
/// compromised attachment redirect traffic to any sibling service in that
|
||||||
|
/// namespace. The broad `*_ALLOW_PRIVATE_TARGETS=true` switch above remains
|
||||||
|
/// the explicit escape hatch for deployments that need that behaviour.
|
||||||
|
fn tunnel_relay_private_host_is_allowlisted(host: &str) -> bool {
|
||||||
|
let host = host.trim().trim_end_matches('.').to_ascii_lowercase();
|
||||||
|
if host.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::env::var(TUNNEL_RELAY_PRIVATE_HOST_ALLOWLIST_ENV)
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|value| tunnel_relay_private_host_matches_allowlist(&host, &value))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tunnel_relay_private_host_matches_allowlist(host: &str, allowlist: &str) -> bool {
|
||||||
|
let host = host.trim().trim_end_matches('.');
|
||||||
|
!host.is_empty()
|
||||||
|
&& allowlist.split(',').any(|entry| {
|
||||||
|
let entry = entry.trim().trim_end_matches('.');
|
||||||
|
!entry.is_empty() && entry.eq_ignore_ascii_case(host)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn build_owner_forward_pinned_client(
|
fn build_owner_forward_pinned_client(
|
||||||
target: &ResolvedOwnerForwardTarget,
|
target: &ResolvedOwnerForwardTarget,
|
||||||
) -> Result<reqwest::Client, String> {
|
) -> Result<reqwest::Client, String> {
|
||||||
@@ -2746,8 +2778,9 @@ mod tests {
|
|||||||
current_unix_secs, 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, tunnel_relay_private_host_matches_allowlist,
|
||||||
TunnelAttachmentDirectory, TunnelAttachmentRecord, MAX_OWNER_FORWARD_DNS_ADDRESSES,
|
validate_tunnel_relay_transport_url, AppState, GatewayDataState, TunnelAttachmentDirectory,
|
||||||
|
TunnelAttachmentRecord, MAX_OWNER_FORWARD_DNS_ADDRESSES,
|
||||||
};
|
};
|
||||||
use aether_contracts::tunnel::{
|
use aether_contracts::tunnel::{
|
||||||
try_decode_tunnel_relay_request_meta, TUNNEL_RELAY_FORWARDED_BY_HEADER,
|
try_decode_tunnel_relay_request_meta, TUNNEL_RELAY_FORWARDED_BY_HEADER,
|
||||||
@@ -2916,6 +2949,26 @@ mod tests {
|
|||||||
.is_ok());
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_relay_host_allowlist_matches_exact_dns_names_only() {
|
||||||
|
assert!(tunnel_relay_private_host_matches_allowlist(
|
||||||
|
"gateway-a.internal.",
|
||||||
|
"gateway-a.internal, gateway-b.internal"
|
||||||
|
));
|
||||||
|
assert!(tunnel_relay_private_host_matches_allowlist(
|
||||||
|
"GATEWAY-B.INTERNAL",
|
||||||
|
"gateway-a.internal, gateway-b.internal."
|
||||||
|
));
|
||||||
|
assert!(!tunnel_relay_private_host_matches_allowlist(
|
||||||
|
"api.gateway-a.internal",
|
||||||
|
"gateway-a.internal"
|
||||||
|
));
|
||||||
|
assert!(!tunnel_relay_private_host_matches_allowlist(
|
||||||
|
"gateway-a.internal.evil.example",
|
||||||
|
".internal"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn owner_forward_target_marks_literal_ipv4_and_ipv6_hosts() {
|
fn owner_forward_target_marks_literal_ipv4_and_ipv6_hosts() {
|
||||||
let ipv4 = build_owner_forward_target(
|
let ipv4 = build_owner_forward_target(
|
||||||
|
|||||||
Reference in New Issue
Block a user