mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
Fix secure tunnel session handling
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -515,6 +515,7 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"url",
|
"url",
|
||||||
|
"uuid",
|
||||||
"webpki-roots 0.26.11",
|
"webpki-roots 0.26.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub(crate) fn mount_public_support_routes(router: Router<AppState>) -> Router<Ap
|
|||||||
.route("/api/capabilities/model/{*model_path}", get(proxy_request))
|
.route("/api/capabilities/model/{*model_path}", get(proxy_request))
|
||||||
.route("/install/{*install_path}", get(proxy_request))
|
.route("/install/{*install_path}", get(proxy_request))
|
||||||
.route("/install-tunnel/{*install_path}", get(proxy_request))
|
.route("/install-tunnel/{*install_path}", get(proxy_request))
|
||||||
.route("/install-proxy/{*install_path}", get(proxy_request))
|
|
||||||
.route("/i/{*install_path}", get(proxy_request))
|
.route("/i/{*install_path}", get(proxy_request))
|
||||||
.route("/", get(proxy_request))
|
.route("/", get(proxy_request))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -797,7 +797,6 @@ pub(super) fn classify_public_support_route(
|
|||||||
} else if method == http::Method::GET
|
} else if method == http::Method::GET
|
||||||
&& (has_single_segment_after_prefix(normalized_path, "/install/")
|
&& (has_single_segment_after_prefix(normalized_path, "/install/")
|
||||||
|| has_single_segment_after_prefix(normalized_path, "/install-tunnel/")
|
|| has_single_segment_after_prefix(normalized_path, "/install-tunnel/")
|
||||||
|| has_single_segment_after_prefix(normalized_path, "/install-proxy/")
|
|
||||||
|| has_single_segment_after_prefix(normalized_path, "/i/"))
|
|| has_single_segment_after_prefix(normalized_path, "/i/"))
|
||||||
{
|
{
|
||||||
Some(classified(
|
Some(classified(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const INSTALL_SESSION_TTL_SECS: u64 = 15 * 60;
|
|||||||
const INSTALL_SESSION_KEY_PREFIX: &str = "install:session:";
|
const INSTALL_SESSION_KEY_PREFIX: &str = "install:session:";
|
||||||
const TUNNEL_INSTALL_SESSION_KEY_PREFIX: &str = "tunnel-install:session:";
|
const TUNNEL_INSTALL_SESSION_KEY_PREFIX: &str = "tunnel-install:session:";
|
||||||
const TUNNEL_INSTALL_UNIX_SCRIPT_URL: &str =
|
const TUNNEL_INSTALL_UNIX_SCRIPT_URL: &str =
|
||||||
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh";
|
"https://raw.githubusercontent.com/fawney19/Aether/refs/heads/main/apps/aether-tunnel/install.sh";
|
||||||
const TUNNEL_INSTALL_POWERSHELL_SCRIPT_URL: &str =
|
const TUNNEL_INSTALL_POWERSHELL_SCRIPT_URL: &str =
|
||||||
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1";
|
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1";
|
||||||
|
|
||||||
@@ -95,8 +95,7 @@ fn install_code_from_path(request_path: &str) -> Option<(String, bool)> {
|
|||||||
|
|
||||||
fn tunnel_install_code_from_path(request_path: &str) -> Option<(String, bool)> {
|
fn tunnel_install_code_from_path(request_path: &str) -> Option<(String, bool)> {
|
||||||
let raw = request_path
|
let raw = request_path
|
||||||
.strip_prefix("/install-tunnel/")
|
.strip_prefix("/install-tunnel/")?
|
||||||
.or_else(|| request_path.strip_prefix("/install-proxy/"))?
|
|
||||||
.trim()
|
.trim()
|
||||||
.trim_matches('/');
|
.trim_matches('/');
|
||||||
if raw.is_empty() || raw.contains('/') {
|
if raw.is_empty() || raw.contains('/') {
|
||||||
@@ -735,9 +734,7 @@ pub(super) async fn maybe_build_local_install_response(
|
|||||||
if decision.route_family.as_deref() != Some("install") {
|
if decision.route_family.as_deref() != Some("install") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
if request_context.request_path.starts_with("/install-tunnel/")
|
if request_context.request_path.starts_with("/install-tunnel/") {
|
||||||
|| request_context.request_path.starts_with("/install-proxy/")
|
|
||||||
{
|
|
||||||
return Some(maybe_build_local_tunnel_install_response(state, request_context).await);
|
return Some(maybe_build_local_tunnel_install_response(state, request_context).await);
|
||||||
}
|
}
|
||||||
let Some((code, wants_powershell)) = install_code_from_path(&request_context.request_path)
|
let Some((code, wants_powershell)) = install_code_from_path(&request_context.request_path)
|
||||||
@@ -932,10 +929,6 @@ mod tests {
|
|||||||
tunnel_install_code_from_path("/install-tunnel/abc123.ps1"),
|
tunnel_install_code_from_path("/install-tunnel/abc123.ps1"),
|
||||||
Some(("abc123".to_string(), true))
|
Some(("abc123".to_string(), true))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
|
||||||
tunnel_install_code_from_path("/install-proxy/abc123"),
|
|
||||||
Some(("abc123".to_string(), false))
|
|
||||||
);
|
|
||||||
assert_eq!(tunnel_install_code_from_path("/install-tunnel/a/b"), None);
|
assert_eq!(tunnel_install_code_from_path("/install-tunnel/a/b"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,7 +942,7 @@ mod tests {
|
|||||||
assert!(script.contains("export AETHER_TUNNEL_SECURITY='non_tls_required'"));
|
assert!(script.contains("export AETHER_TUNNEL_SECURITY='non_tls_required'"));
|
||||||
assert!(script.contains("export AETHER_TUNNEL_ENCRYPTION_KEY='base64-32-bytes'"));
|
assert!(script.contains("export AETHER_TUNNEL_ENCRYPTION_KEY='base64-32-bytes'"));
|
||||||
assert!(script.contains(
|
assert!(script.contains(
|
||||||
"https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh"
|
"https://raw.githubusercontent.com/fawney19/Aether/refs/heads/main/apps/aether-tunnel/install.sh"
|
||||||
));
|
));
|
||||||
assert!(!script.contains("aether-rust-pioneer"));
|
assert!(!script.contains("aether-rust-pioneer"));
|
||||||
assert!(!script.contains("[[servers]]"));
|
assert!(!script.contains("[[servers]]"));
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ fn frontend_path_bypasses_static(path: &str) -> bool {
|
|||||||
|| path.starts_with("/.well-known/")
|
|| path.starts_with("/.well-known/")
|
||||||
|| path.starts_with("/install/")
|
|| path.starts_with("/install/")
|
||||||
|| path.starts_with("/install-tunnel/")
|
|| path.starts_with("/install-tunnel/")
|
||||||
|| path.starts_with("/install-proxy/")
|
|
||||||
|| path.starts_with("/i/")
|
|| path.starts_with("/i/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -264,18 +264,23 @@ pub async fn ws_proxy(
|
|||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.unwrap_or(node_id.as_str())
|
.map(str::to_string);
|
||||||
.to_string();
|
|
||||||
|
|
||||||
if node_id.is_empty() {
|
if node_id.is_empty() {
|
||||||
warn!("proxy connection rejected: missing X-Node-ID header");
|
warn!("proxy connection rejected: missing X-Node-ID header");
|
||||||
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||||
}
|
}
|
||||||
let stored_security_key = state.secure_tunnel_key_for_node(&node_id).await;
|
let stored_security_key = state.secure_tunnel_key_for_node(&node_id).await;
|
||||||
let security_key = match tunnel_security.as_deref() {
|
let (security_key, security_session) = match tunnel_security.as_deref() {
|
||||||
Some(aether_contracts::tunnel_security::TUNNEL_SECURITY_NON_TLS_REQUIRED) => {
|
Some(aether_contracts::tunnel_security::TUNNEL_SECURITY_NON_TLS_REQUIRED) => {
|
||||||
match stored_security_key {
|
match stored_security_key {
|
||||||
Some(key) => Some(key),
|
Some(key) => {
|
||||||
|
let Some(session) = security_session else {
|
||||||
|
warn!(node_id = %node_id, "secure tunnel requested without a security session");
|
||||||
|
return axum::http::StatusCode::BAD_REQUEST.into_response();
|
||||||
|
};
|
||||||
|
(Some(key), session)
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
warn!(node_id = %node_id, "secure tunnel requested but no PSK is registered");
|
warn!(node_id = %node_id, "secure tunnel requested but no PSK is registered");
|
||||||
return axum::http::StatusCode::UNAUTHORIZED.into_response();
|
return axum::http::StatusCode::UNAUTHORIZED.into_response();
|
||||||
@@ -287,7 +292,7 @@ pub async fn ws_proxy(
|
|||||||
warn!(node_id = %node_id, "proxy connection rejected: stored secure tunnel key requires encrypted frames");
|
warn!(node_id = %node_id, "proxy connection rejected: stored secure tunnel key requires encrypted frames");
|
||||||
return axum::http::StatusCode::UNAUTHORIZED.into_response();
|
return axum::http::StatusCode::UNAUTHORIZED.into_response();
|
||||||
}
|
}
|
||||||
None => None,
|
None => (None, String::new()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let request_permit = match state.try_acquire_request_permit().await {
|
let request_permit = match state.try_acquire_request_permit().await {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
if ($env:AETHER_PROXY_AETHER_URL -and -not $env:AETHER_TUNNEL_AETHER_URL) {
|
|
||||||
$env:AETHER_TUNNEL_AETHER_URL = $env:AETHER_PROXY_AETHER_URL
|
|
||||||
}
|
|
||||||
if ($env:AETHER_PROXY_MANAGEMENT_TOKEN -and -not $env:AETHER_TUNNEL_MANAGEMENT_TOKEN) {
|
|
||||||
$env:AETHER_TUNNEL_MANAGEMENT_TOKEN = $env:AETHER_PROXY_MANAGEMENT_TOKEN
|
|
||||||
}
|
|
||||||
if ($env:AETHER_PROXY_NODE_NAME -and -not $env:AETHER_TUNNEL_NODE_NAME) {
|
|
||||||
$env:AETHER_TUNNEL_NODE_NAME = $env:AETHER_PROXY_NODE_NAME
|
|
||||||
}
|
|
||||||
if ($env:AETHER_PROXY_TUNNEL_SECURITY -and -not $env:AETHER_TUNNEL_SECURITY) {
|
|
||||||
$env:AETHER_TUNNEL_SECURITY = $env:AETHER_PROXY_TUNNEL_SECURITY
|
|
||||||
}
|
|
||||||
if ($env:AETHER_PROXY_TUNNEL_ENCRYPTION_KEY -and -not $env:AETHER_TUNNEL_ENCRYPTION_KEY) {
|
|
||||||
$env:AETHER_TUNNEL_ENCRYPTION_KEY = $env:AETHER_PROXY_TUNNEL_ENCRYPTION_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
irm 'https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.ps1' | iex
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
if [ -n "${AETHER_PROXY_AETHER_URL:-}" ] && [ -z "${AETHER_TUNNEL_AETHER_URL:-}" ]; then
|
|
||||||
export AETHER_TUNNEL_AETHER_URL="${AETHER_PROXY_AETHER_URL}"
|
|
||||||
fi
|
|
||||||
if [ -n "${AETHER_PROXY_MANAGEMENT_TOKEN:-}" ] && [ -z "${AETHER_TUNNEL_MANAGEMENT_TOKEN:-}" ]; then
|
|
||||||
export AETHER_TUNNEL_MANAGEMENT_TOKEN="${AETHER_PROXY_MANAGEMENT_TOKEN}"
|
|
||||||
fi
|
|
||||||
if [ -n "${AETHER_PROXY_NODE_NAME:-}" ] && [ -z "${AETHER_TUNNEL_NODE_NAME:-}" ]; then
|
|
||||||
export AETHER_TUNNEL_NODE_NAME="${AETHER_PROXY_NODE_NAME}"
|
|
||||||
fi
|
|
||||||
if [ -n "${AETHER_PROXY_TUNNEL_SECURITY:-}" ] && [ -z "${AETHER_TUNNEL_SECURITY:-}" ]; then
|
|
||||||
export AETHER_TUNNEL_SECURITY="${AETHER_PROXY_TUNNEL_SECURITY}"
|
|
||||||
fi
|
|
||||||
if [ -n "${AETHER_PROXY_TUNNEL_ENCRYPTION_KEY:-}" ] && [ -z "${AETHER_TUNNEL_ENCRYPTION_KEY:-}" ]; then
|
|
||||||
export AETHER_TUNNEL_ENCRYPTION_KEY="${AETHER_PROXY_TUNNEL_ENCRYPTION_KEY}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
curl -fsSL 'https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh' | sh
|
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
|
||||||
wget -qO- 'https://raw.githubusercontent.com/fawney19/Aether/main/apps/aether-tunnel/install.sh' | sh
|
|
||||||
else
|
|
||||||
printf '%s\n' "[Aether Tunnel] 需要 curl 或 wget 下载安装脚本" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -41,6 +41,7 @@ tar = "0.4"
|
|||||||
socket2 = { version = "0.5", features = ["all"] }
|
socket2 = { version = "0.5", features = ["all"] }
|
||||||
tower-service = "0.3"
|
tower-service = "0.3"
|
||||||
webpki-roots = "0.26"
|
webpki-roots = "0.26"
|
||||||
|
uuid.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
aether-gateway.workspace = true
|
aether-gateway.workspace = true
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ pub async fn connect_and_run(
|
|||||||
);
|
);
|
||||||
let node_id = server.node_id.read().unwrap().clone();
|
let node_id = server.node_id.read().unwrap().clone();
|
||||||
headers.insert("X-Node-Id", http::HeaderValue::from_str(&node_id)?);
|
headers.insert("X-Node-Id", http::HeaderValue::from_str(&node_id)?);
|
||||||
|
let security_session = uuid::Uuid::new_v4().simple().to_string();
|
||||||
if server.tunnel_security == crate::config::TunnelSecurity::NonTlsRequired {
|
if server.tunnel_security == crate::config::TunnelSecurity::NonTlsRequired {
|
||||||
headers.insert(
|
headers.insert(
|
||||||
TUNNEL_SECURITY_HEADER,
|
TUNNEL_SECURITY_HEADER,
|
||||||
@@ -68,7 +69,7 @@ pub async fn connect_and_run(
|
|||||||
);
|
);
|
||||||
headers.insert(
|
headers.insert(
|
||||||
TUNNEL_SECURITY_SESSION_HEADER,
|
TUNNEL_SECURITY_SESSION_HEADER,
|
||||||
http::HeaderValue::from_str(&node_id)?,
|
http::HeaderValue::from_str(&security_session)?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Use dynamic node_name (may be updated by remote config) instead of
|
// Use dynamic node_name (may be updated by remote config) instead of
|
||||||
@@ -142,7 +143,7 @@ pub async fn connect_and_run(
|
|||||||
.ok_or_else(|| anyhow::anyhow!("secure tunnel requires tunnel_encryption_key"))?;
|
.ok_or_else(|| anyhow::anyhow!("secure tunnel requires tunnel_encryption_key"))?;
|
||||||
Some(Arc::new(SecureFrameCodec::new(
|
Some(Arc::new(SecureFrameCodec::new(
|
||||||
key,
|
key,
|
||||||
&node_id,
|
&security_session,
|
||||||
TunnelSecurityRole::Client,
|
TunnelSecurityRole::Client,
|
||||||
)?))
|
)?))
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -242,4 +242,25 @@ mod tests {
|
|||||||
Err(TunnelSecurityError::Decrypt)
|
Err(TunnelSecurityError::Decrypt)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn secure_frame_uses_session_in_key_derivation() {
|
||||||
|
let session_a = "node-1:connection-a";
|
||||||
|
let session_b = "node-1:connection-b";
|
||||||
|
let client_a = SecureFrameCodec::new(&test_key(), session_a, TunnelSecurityRole::Client)
|
||||||
|
.expect("client codec a");
|
||||||
|
let client_b = SecureFrameCodec::new(&test_key(), session_b, TunnelSecurityRole::Client)
|
||||||
|
.expect("client codec b");
|
||||||
|
let frame = Frame::new(
|
||||||
|
7,
|
||||||
|
MsgType::RequestBody,
|
||||||
|
0,
|
||||||
|
Bytes::from_static(b"same payload"),
|
||||||
|
);
|
||||||
|
|
||||||
|
let encrypted_a = client_a.encrypt_frame(frame.clone()).expect("encrypt a");
|
||||||
|
let encrypted_b = client_b.encrypt_frame(frame).expect("encrypt b");
|
||||||
|
|
||||||
|
assert_ne!(encrypted_a, encrypted_b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user