mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-10 19:29:50 +08:00
feat(security): harden gateway boundaries and usage policies
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change. Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
@@ -2,28 +2,23 @@ use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use aether_contracts::tunnel::{
|
||||
resolve_tunnel_request_timeouts, try_decode_tunnel_relay_request_meta,
|
||||
TUNNEL_RELAY_FORWARDED_BY_HEADER,
|
||||
};
|
||||
use aether_contracts::tunnel::{resolve_tunnel_request_timeouts, TUNNEL_RELAY_FORWARDED_BY_HEADER};
|
||||
use aether_runtime::{maybe_hold_axum_response_permit, AdmissionPermit};
|
||||
use async_stream::stream;
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::extract::{ConnectInfo, Path, Request, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, Response, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use bytes::BytesMut;
|
||||
use futures_util::StreamExt;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::api::response::apply_streaming_response_headers;
|
||||
use crate::headers::should_skip_response_header;
|
||||
use crate::maintenance::record_proxy_upgrade_traffic_success;
|
||||
use crate::maintenance::record_proxy_upgrade_traffic_success_for_generation;
|
||||
|
||||
use super::hub::{LocalBodyEvent, LocalStream};
|
||||
use super::protocol;
|
||||
use super::AppState;
|
||||
use super::{AppState, RelayRequestAuthenticated};
|
||||
|
||||
pub const TUNNEL_ERROR_HEADER: &str = "x-aether-tunnel-error";
|
||||
|
||||
@@ -86,8 +81,7 @@ pub(crate) async fn open_direct_relay_stream(
|
||||
.await
|
||||
.map_err(map_request_admission_error)?;
|
||||
let stream = state
|
||||
.hub
|
||||
.open_local_stream(node_id, &meta)
|
||||
.open_authorized_local_stream(node_id, &meta)
|
||||
.await
|
||||
.map_err(|error| format!("connect: {error}"))?;
|
||||
if let Err(error) = state
|
||||
@@ -107,7 +101,13 @@ pub(crate) async fn open_direct_relay_stream(
|
||||
return Err(format!("timeout: {error}"));
|
||||
}
|
||||
};
|
||||
if let Err(error) = record_proxy_upgrade_traffic_success(state.data.as_ref(), node_id).await {
|
||||
if let Err(error) = record_proxy_upgrade_traffic_success_for_generation(
|
||||
state.data.as_ref(),
|
||||
node_id,
|
||||
stream.tunnel_generation(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
node_id = %node_id,
|
||||
error = %error,
|
||||
@@ -171,7 +171,7 @@ fn is_rollout_probe_request(headers: &HeaderMap, forwarded_by_gateway: bool) ->
|
||||
pub async fn relay_request(
|
||||
Path(node_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
ConnectInfo(_addr): ConnectInfo<SocketAddr>,
|
||||
request: Request,
|
||||
) -> impl IntoResponse {
|
||||
let forwarded_by_gateway = request
|
||||
@@ -181,13 +181,10 @@ pub async fn relay_request(
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let rollout_probe = is_rollout_probe_request(request.headers(), forwarded_by_gateway);
|
||||
if !addr.ip().is_loopback() && !forwarded_by_gateway {
|
||||
return tunnel_error_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"forbidden",
|
||||
"local relay only accepts loopback requests",
|
||||
);
|
||||
}
|
||||
let already_authenticated = request
|
||||
.extensions()
|
||||
.get::<RelayRequestAuthenticated>()
|
||||
.is_some();
|
||||
|
||||
let request_permit = match state.try_acquire_request_permit().await {
|
||||
Ok(permit) => permit,
|
||||
@@ -226,109 +223,77 @@ pub async fn relay_request(
|
||||
}
|
||||
};
|
||||
|
||||
let mut body_stream = request.into_body().into_data_stream();
|
||||
let mut envelope_buf = BytesMut::new();
|
||||
let mut meta: Option<protocol::RequestMeta> = None;
|
||||
let mut stream: Option<std::sync::Arc<LocalStream>> = None;
|
||||
if !already_authenticated {
|
||||
return release_permit_response(
|
||||
tunnel_error_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"forbidden",
|
||||
"relay request integrity must be verified before local dispatch",
|
||||
),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
|
||||
while let Some(chunk_result) = body_stream.next().await {
|
||||
let chunk = match chunk_result {
|
||||
Ok(chunk) => chunk,
|
||||
let Some(spool) = request
|
||||
.extensions()
|
||||
.get::<crate::tunnel::VerifiedRelaySpool>()
|
||||
.cloned()
|
||||
else {
|
||||
return release_permit_response(
|
||||
tunnel_error_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"forbidden",
|
||||
"verified relay payload is missing",
|
||||
),
|
||||
request_permit,
|
||||
);
|
||||
};
|
||||
let meta = spool.meta().clone();
|
||||
|
||||
let stream = match state.open_authorized_local_stream(&node_id, &meta).await {
|
||||
Ok(stream) => stream,
|
||||
Err(error) => {
|
||||
return release_permit_response(
|
||||
tunnel_error_response(StatusCode::SERVICE_UNAVAILABLE, "connect", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
};
|
||||
let body_stream = match spool.body_stream().await {
|
||||
Ok(stream) => stream,
|
||||
Err(error) => {
|
||||
state.hub.cancel_local_stream(stream.id, &error);
|
||||
return release_permit_response(
|
||||
tunnel_error_response(StatusCode::BAD_GATEWAY, "relay", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
};
|
||||
futures_util::pin_mut!(body_stream);
|
||||
while let Some(chunk) = futures_util::StreamExt::next(&mut body_stream).await {
|
||||
let (chunk, end) = match chunk {
|
||||
Ok(chunk) => (chunk, false),
|
||||
Err(error) => {
|
||||
if let Some(active_stream) = &stream {
|
||||
state
|
||||
.hub
|
||||
.cancel_local_stream(active_stream.id, "failed to read relay request body");
|
||||
}
|
||||
warn!(error = %error, "failed to read local relay request body");
|
||||
let error = error.to_string();
|
||||
state.hub.cancel_local_stream(stream.id, &error);
|
||||
return release_permit_response(
|
||||
tunnel_error_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"relay",
|
||||
"failed to read relay request body",
|
||||
),
|
||||
tunnel_error_response(StatusCode::BAD_GATEWAY, "relay", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if stream.is_none() {
|
||||
envelope_buf.extend_from_slice(&chunk);
|
||||
let Some((parsed_meta, body_offset)) =
|
||||
(match try_decode_tunnel_relay_request_meta(&envelope_buf) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
return release_permit_response(
|
||||
tunnel_error_response(StatusCode::BAD_REQUEST, "bad_request", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
})
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let opened_stream = match state.hub.open_local_stream(&node_id, &parsed_meta).await {
|
||||
Ok(stream) => stream,
|
||||
Err(error) => {
|
||||
return release_permit_response(
|
||||
tunnel_error_response(StatusCode::SERVICE_UNAVAILABLE, "connect", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if envelope_buf.len() > body_offset {
|
||||
let first_body_chunk = Bytes::copy_from_slice(&envelope_buf[body_offset..]);
|
||||
if let Err(error) = state
|
||||
.hub
|
||||
.push_local_request_body(opened_stream.id, first_body_chunk, false)
|
||||
.await
|
||||
{
|
||||
state.hub.cancel_local_stream(opened_stream.id, &error);
|
||||
return release_permit_response(
|
||||
tunnel_error_response(StatusCode::SERVICE_UNAVAILABLE, "connect", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
envelope_buf.clear();
|
||||
meta = Some(parsed_meta);
|
||||
stream = Some(opened_stream);
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(active_stream) = &stream else {
|
||||
continue;
|
||||
};
|
||||
if let Err(error) = state
|
||||
.hub
|
||||
.push_local_request_body(active_stream.id, chunk, false)
|
||||
.push_local_request_body(stream.id, chunk, end)
|
||||
.await
|
||||
{
|
||||
state.hub.cancel_local_stream(active_stream.id, &error);
|
||||
state.hub.cancel_local_stream(stream.id, &error);
|
||||
return release_permit_response(
|
||||
tunnel_error_response(StatusCode::SERVICE_UNAVAILABLE, "connect", &error),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let (meta, stream) = match (meta, stream) {
|
||||
(Some(meta), Some(stream)) => (meta, stream),
|
||||
_ => {
|
||||
return release_permit_response(
|
||||
tunnel_error_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"bad_request",
|
||||
"relay envelope metadata truncated",
|
||||
),
|
||||
request_permit,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = state
|
||||
.hub
|
||||
.push_local_request_body(stream.id, Bytes::new(), true)
|
||||
@@ -359,8 +324,12 @@ pub async fn relay_request(
|
||||
}
|
||||
};
|
||||
if !rollout_probe {
|
||||
if let Err(error) =
|
||||
record_proxy_upgrade_traffic_success(state.data.as_ref(), &node_id).await
|
||||
if let Err(error) = record_proxy_upgrade_traffic_success_for_generation(
|
||||
state.data.as_ref(),
|
||||
&node_id,
|
||||
stream.tunnel_generation(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(
|
||||
node_id = %node_id,
|
||||
@@ -436,8 +405,16 @@ fn release_permit_response(
|
||||
}
|
||||
|
||||
fn append_headers(target: &mut HeaderMap, headers: &[(String, String)]) {
|
||||
let connection_declared = aether_http::connection_declared_header_names(
|
||||
headers
|
||||
.iter()
|
||||
.filter(|(name, _)| name.eq_ignore_ascii_case(http::header::CONNECTION.as_str()))
|
||||
.map(|(_, value)| value.as_str()),
|
||||
);
|
||||
for (name, value) in headers {
|
||||
if should_skip_local_relay_response_header(name) {
|
||||
if should_skip_local_relay_response_header(name)
|
||||
|| connection_declared.contains(&name.to_ascii_lowercase())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Ok(name) = HeaderName::from_bytes(name.as_bytes()) else {
|
||||
@@ -454,12 +431,21 @@ fn should_skip_local_relay_response_header(name: &str) -> bool {
|
||||
should_skip_response_header(name) || name.eq_ignore_ascii_case("content-length")
|
||||
}
|
||||
|
||||
fn tunnel_error_response(status: StatusCode, kind: &str, message: &str) -> Response<Body> {
|
||||
fn tunnel_error_response(status: StatusCode, kind: &str, _message: &str) -> Response<Body> {
|
||||
let kind = safe_tunnel_error_kind(kind);
|
||||
let message = match kind {
|
||||
"overloaded" => "hub relay overloaded",
|
||||
"forbidden" => "relay request forbidden",
|
||||
"connect" => "tunnel connection failed",
|
||||
"timeout" => "tunnel request timed out",
|
||||
"unavailable" => "tunnel unavailable",
|
||||
_ => "tunnel relay failed",
|
||||
};
|
||||
let mut builder = Response::builder().status(status);
|
||||
if let Some(headers) = builder.headers_mut() {
|
||||
headers.insert(
|
||||
HeaderName::from_static(TUNNEL_ERROR_HEADER),
|
||||
HeaderValue::from_str(kind).unwrap_or_else(|_| HeaderValue::from_static("relay")),
|
||||
HeaderValue::from_static(kind),
|
||||
);
|
||||
headers.insert(
|
||||
axum::http::header::CONTENT_TYPE,
|
||||
@@ -471,17 +457,32 @@ fn tunnel_error_response(status: StatusCode, kind: &str, message: &str) -> Respo
|
||||
.unwrap_or_else(|_| Response::new(Body::from("relay error")))
|
||||
}
|
||||
|
||||
fn safe_tunnel_error_kind(kind: &str) -> &'static str {
|
||||
match kind.trim().to_ascii_lowercase().as_str() {
|
||||
"overloaded" => "overloaded",
|
||||
"forbidden" => "forbidden",
|
||||
"connect" => "connect",
|
||||
"timeout" => "timeout",
|
||||
"unavailable" => "unavailable",
|
||||
"relay" => "relay",
|
||||
_ => "relay",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::hub::ProxyConn;
|
||||
use super::super::{protocol, AppState, ConnConfig, ControlPlaneClient};
|
||||
use super::super::{
|
||||
protocol, AppState, ConnConfig, ControlPlaneClient, RelayRequestAuthenticated,
|
||||
};
|
||||
use super::{
|
||||
is_rollout_probe_request, relay_header_timeout, relay_request, Body, HeaderMap, Request,
|
||||
SocketAddr, StatusCode, TUNNEL_ERROR_HEADER,
|
||||
is_rollout_probe_request, relay_header_timeout, relay_request, tunnel_error_response, Body,
|
||||
HeaderMap, Request, SocketAddr, StatusCode, TUNNEL_ERROR_HEADER,
|
||||
};
|
||||
use crate::data::GatewayDataState;
|
||||
use crate::maintenance::start_proxy_upgrade_rollout;
|
||||
use aether_contracts::tunnel::TUNNEL_RELAY_FORWARDED_BY_HEADER;
|
||||
use aether_contracts::tunnel_security::TUNNEL_SECURITY_NON_TLS_REQUIRED;
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::proxy_nodes::{
|
||||
InMemoryProxyNodeRepository, ProxyNodeHeartbeatMutation, ProxyNodeWriteRepository,
|
||||
StoredProxyNode,
|
||||
@@ -496,6 +497,34 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
use tokio::sync::watch;
|
||||
|
||||
const LOCAL_TUNNEL_TEST_PSK: &str = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=";
|
||||
const LOCAL_TUNNEL_TEST_GENERATION: &str = "local-relay-test-generation-1";
|
||||
|
||||
#[tokio::test]
|
||||
async fn relay_error_response_drops_internal_and_peer_details() {
|
||||
let response = tunnel_error_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"https://attacker.invalid/?token=header-secret",
|
||||
"Bearer body-secret at http://10.0.0.8/private\r\nx-injected: true",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(TUNNEL_ERROR_HEADER)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("relay")
|
||||
);
|
||||
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("relay error response body should read");
|
||||
assert_eq!(body.as_ref(), b"tunnel relay failed");
|
||||
let body = String::from_utf8_lossy(&body);
|
||||
assert!(!body.contains("body-secret"));
|
||||
assert!(!body.contains("10.0.0.8"));
|
||||
assert!(!body.contains("x-injected"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollout_probe_marker_is_only_trusted_from_a_forwarding_gateway() {
|
||||
let mut headers = HeaderMap::new();
|
||||
@@ -522,6 +551,18 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
async fn authenticated_request(envelope: Vec<u8>) -> Request {
|
||||
let spool = crate::tunnel::prepare_owner_relay_request_body(Body::from(envelope))
|
||||
.await
|
||||
.expect("relay envelope should prepare");
|
||||
let mut request = Request::builder()
|
||||
.body(Body::empty())
|
||||
.expect("request should build");
|
||||
request.extensions_mut().insert(RelayRequestAuthenticated);
|
||||
request.extensions_mut().insert(spool);
|
||||
request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_header_timeout_ignores_request_timeout_for_stream_requests() {
|
||||
let meta = protocol::RequestMeta {
|
||||
@@ -591,7 +632,12 @@ mod tests {
|
||||
None,
|
||||
Some(1_800_000_000),
|
||||
None,
|
||||
None,
|
||||
Some(json!({
|
||||
"tunnel_security": {
|
||||
"mode": TUNNEL_SECURITY_NON_TLS_REQUIRED,
|
||||
"encryption_key": LOCAL_TUNNEL_TEST_PSK,
|
||||
}
|
||||
})),
|
||||
None,
|
||||
None,
|
||||
Some(1_800_000_000),
|
||||
@@ -599,6 +645,17 @@ mod tests {
|
||||
Some(1_800_000_000),
|
||||
Some(1_800_000_000),
|
||||
)
|
||||
.with_tunnel_generation(LOCAL_TUNNEL_TEST_GENERATION.to_string())
|
||||
}
|
||||
|
||||
async fn recv_tunnel_test_frame(
|
||||
proxy_rx: &mut aether_runtime::BoundedQueueReceiver<Message>,
|
||||
description: &str,
|
||||
) -> Message {
|
||||
tokio::time::timeout(Duration::from_secs(5), proxy_rx.recv())
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("timed out waiting for {description}"))
|
||||
.unwrap_or_else(|| panic!("proxy channel closed before {description}"))
|
||||
}
|
||||
|
||||
fn encode_relay_envelope(meta: &protocol::RequestMeta, body: &[u8]) -> Vec<u8> {
|
||||
@@ -611,9 +668,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn relay_rejects_non_loopback_without_forwarded_header() {
|
||||
async fn relay_rejects_unsigned_request_even_from_loopback() {
|
||||
let request = Request::builder()
|
||||
.body(Body::empty())
|
||||
.body(Body::from(encode_relay_envelope(
|
||||
&protocol::RequestMeta {
|
||||
provider_id: None,
|
||||
endpoint_id: None,
|
||||
key_id: None,
|
||||
method: "GET".to_string(),
|
||||
url: "https://example.com/".to_string(),
|
||||
headers: HashMap::new(),
|
||||
stream: false,
|
||||
request_timeout_ms: None,
|
||||
stream_first_byte_timeout_ms: None,
|
||||
timeout: 30,
|
||||
follow_redirects: None,
|
||||
http1_only: false,
|
||||
transport_profile: None,
|
||||
},
|
||||
&[],
|
||||
)))
|
||||
.expect("request should build");
|
||||
let response = relay_request(
|
||||
Path("node-123".to_string()),
|
||||
@@ -625,13 +699,40 @@ mod tests {
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(TUNNEL_ERROR_HEADER)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("forbidden")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn relay_accepts_forwarded_gateway_request_from_non_loopback() {
|
||||
async fn relay_rejects_forged_forwarded_gateway_header() {
|
||||
let request = Request::builder()
|
||||
.header(TUNNEL_RELAY_FORWARDED_BY_HEADER, "gateway-a")
|
||||
.body(Body::empty())
|
||||
.header(
|
||||
aether_contracts::tunnel::TUNNEL_RELAY_FORWARDED_BY_HEADER,
|
||||
"gateway-a",
|
||||
)
|
||||
.body(Body::from(encode_relay_envelope(
|
||||
&protocol::RequestMeta {
|
||||
provider_id: None,
|
||||
endpoint_id: None,
|
||||
key_id: None,
|
||||
method: "GET".to_string(),
|
||||
url: "https://example.com/".to_string(),
|
||||
headers: HashMap::new(),
|
||||
stream: false,
|
||||
request_timeout_ms: None,
|
||||
stream_first_byte_timeout_ms: None,
|
||||
timeout: 30,
|
||||
follow_redirects: None,
|
||||
http1_only: false,
|
||||
transport_profile: None,
|
||||
},
|
||||
&[],
|
||||
)))
|
||||
.expect("request should build");
|
||||
let response = relay_request(
|
||||
Path("node-123".to_string()),
|
||||
@@ -642,24 +743,29 @@ mod tests {
|
||||
.await
|
||||
.into_response();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(TUNNEL_ERROR_HEADER)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("bad_request")
|
||||
Some("forbidden")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn relay_records_real_traffic_confirmation_for_upgrade_rollout() {
|
||||
let mut node = sample_connected_proxy_node("node-123");
|
||||
node.proxy_metadata = Some(json!({"version": "1.0.0"}));
|
||||
node.proxy_metadata
|
||||
.as_mut()
|
||||
.and_then(serde_json::Value::as_object_mut)
|
||||
.expect("proxy metadata should be an object")
|
||||
.insert("version".to_string(), json!("1.0.0"));
|
||||
let repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![node]));
|
||||
let data = Arc::new(
|
||||
GatewayDataState::with_proxy_node_repository_for_tests(Arc::clone(&repository))
|
||||
.with_system_config_values_for_tests(Vec::<(String, serde_json::Value)>::new()),
|
||||
.with_system_config_values_for_tests(Vec::<(String, serde_json::Value)>::new())
|
||||
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
);
|
||||
|
||||
let started = start_proxy_upgrade_rollout(data.as_ref(), "2.0.0".to_string(), 1, 0, None)
|
||||
@@ -670,6 +776,7 @@ mod tests {
|
||||
repository
|
||||
.apply_heartbeat(&ProxyNodeHeartbeatMutation {
|
||||
node_id: "node-123".to_string(),
|
||||
expected_tunnel_generation: None,
|
||||
heartbeat_interval: None,
|
||||
active_connections: Some(1),
|
||||
total_requests_delta: Some(1),
|
||||
@@ -677,7 +784,13 @@ mod tests {
|
||||
failed_requests_delta: Some(0),
|
||||
dns_failures_delta: Some(0),
|
||||
stream_errors_delta: Some(0),
|
||||
proxy_metadata: Some(json!({"version": "2.0.0"})),
|
||||
proxy_metadata: Some(json!({
|
||||
"version": "2.0.0",
|
||||
"tunnel_security": {
|
||||
"mode": TUNNEL_SECURITY_NON_TLS_REQUIRED,
|
||||
"encryption_key": LOCAL_TUNNEL_TEST_PSK,
|
||||
}
|
||||
})),
|
||||
proxy_version: Some("2.0.0".to_string()),
|
||||
})
|
||||
.await
|
||||
@@ -692,15 +805,19 @@ mod tests {
|
||||
let state = test_app_state().with_data(Arc::clone(&data));
|
||||
let (proxy_tx, mut proxy_rx) = aether_runtime::bounded_queue(8);
|
||||
let (proxy_close_tx, _) = watch::channel(false);
|
||||
state.hub.register_proxy(Arc::new(ProxyConn::new(
|
||||
500,
|
||||
"node-123".to_string(),
|
||||
"Node 123".to_string(),
|
||||
proxy_tx,
|
||||
proxy_close_tx,
|
||||
16,
|
||||
2,
|
||||
)));
|
||||
state.hub.register_proxy(Arc::new(
|
||||
ProxyConn::new(
|
||||
500,
|
||||
"node-123".to_string(),
|
||||
"Node 123".to_string(),
|
||||
proxy_tx,
|
||||
proxy_close_tx,
|
||||
16,
|
||||
2,
|
||||
)
|
||||
.with_tunnel_generation(LOCAL_TUNNEL_TEST_GENERATION.to_string())
|
||||
.with_authenticated_key(LOCAL_TUNNEL_TEST_PSK.to_string()),
|
||||
));
|
||||
|
||||
let meta = protocol::RequestMeta {
|
||||
provider_id: None,
|
||||
@@ -717,9 +834,7 @@ mod tests {
|
||||
http1_only: false,
|
||||
transport_profile: None,
|
||||
};
|
||||
let request = Request::builder()
|
||||
.body(Body::from(encode_relay_envelope(&meta, &[])))
|
||||
.expect("request should build");
|
||||
let request = authenticated_request(encode_relay_envelope(&meta, &[])).await;
|
||||
|
||||
let relay_state = state.clone();
|
||||
let relay_task = tokio::spawn(async move {
|
||||
@@ -733,7 +848,7 @@ mod tests {
|
||||
.into_response()
|
||||
});
|
||||
|
||||
let request_headers = match proxy_rx.recv().await.expect("headers frame should arrive") {
|
||||
let request_headers = match recv_tunnel_test_frame(&mut proxy_rx, "headers frame").await {
|
||||
Message::Binary(data) => data,
|
||||
other => panic!("unexpected message: {other:?}"),
|
||||
};
|
||||
@@ -741,7 +856,7 @@ mod tests {
|
||||
.expect("request header frame should parse");
|
||||
assert_eq!(request_header.msg_type, protocol::REQUEST_HEADERS);
|
||||
|
||||
let request_body = match proxy_rx.recv().await.expect("body frame should arrive") {
|
||||
let request_body = match recv_tunnel_test_frame(&mut proxy_rx, "body frame").await {
|
||||
Message::Binary(data) => data,
|
||||
other => panic!("unexpected message: {other:?}"),
|
||||
};
|
||||
@@ -807,18 +922,29 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn relay_strips_hop_by_hop_and_stale_length_headers_from_proxy_response() {
|
||||
let state = test_app_state();
|
||||
let repository = Arc::new(InMemoryProxyNodeRepository::seed(vec![
|
||||
sample_connected_proxy_node("node-123"),
|
||||
]));
|
||||
let data = Arc::new(
|
||||
GatewayDataState::with_proxy_node_repository_for_tests(repository)
|
||||
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
|
||||
);
|
||||
let state = test_app_state().with_data(data);
|
||||
let (proxy_tx, mut proxy_rx) = aether_runtime::bounded_queue(8);
|
||||
let (proxy_close_tx, _) = watch::channel(false);
|
||||
state.hub.register_proxy(Arc::new(ProxyConn::new(
|
||||
501,
|
||||
"node-123".to_string(),
|
||||
"Node 123".to_string(),
|
||||
proxy_tx,
|
||||
proxy_close_tx,
|
||||
16,
|
||||
2,
|
||||
)));
|
||||
state.hub.register_proxy(Arc::new(
|
||||
ProxyConn::new(
|
||||
501,
|
||||
"node-123".to_string(),
|
||||
"Node 123".to_string(),
|
||||
proxy_tx,
|
||||
proxy_close_tx,
|
||||
16,
|
||||
2,
|
||||
)
|
||||
.with_tunnel_generation(LOCAL_TUNNEL_TEST_GENERATION.to_string())
|
||||
.with_authenticated_key(LOCAL_TUNNEL_TEST_PSK.to_string()),
|
||||
));
|
||||
|
||||
let meta = protocol::RequestMeta {
|
||||
provider_id: None,
|
||||
@@ -835,9 +961,7 @@ mod tests {
|
||||
http1_only: false,
|
||||
transport_profile: None,
|
||||
};
|
||||
let request = Request::builder()
|
||||
.body(Body::from(encode_relay_envelope(&meta, &[])))
|
||||
.expect("request should build");
|
||||
let request = authenticated_request(encode_relay_envelope(&meta, &[])).await;
|
||||
|
||||
let relay_state = state.clone();
|
||||
let relay_task = tokio::spawn(async move {
|
||||
@@ -851,7 +975,7 @@ mod tests {
|
||||
.into_response()
|
||||
});
|
||||
|
||||
let request_headers = match proxy_rx.recv().await.expect("headers frame should arrive") {
|
||||
let request_headers = match recv_tunnel_test_frame(&mut proxy_rx, "headers frame").await {
|
||||
Message::Binary(data) => data,
|
||||
other => panic!("unexpected message: {other:?}"),
|
||||
};
|
||||
@@ -859,7 +983,7 @@ mod tests {
|
||||
.expect("request header frame should parse");
|
||||
assert_eq!(request_header.msg_type, protocol::REQUEST_HEADERS);
|
||||
|
||||
let request_body = match proxy_rx.recv().await.expect("body frame should arrive") {
|
||||
let request_body = match recv_tunnel_test_frame(&mut proxy_rx, "body frame").await {
|
||||
Message::Binary(data) => data,
|
||||
other => panic!("unexpected message: {other:?}"),
|
||||
};
|
||||
@@ -873,6 +997,17 @@ mod tests {
|
||||
("content-length".to_string(), "999".to_string()),
|
||||
("transfer-encoding".to_string(), "chunked".to_string()),
|
||||
("connection".to_string(), "keep-alive".to_string()),
|
||||
(
|
||||
"connection".to_string(),
|
||||
"x-hop-private, x-accel-redirect".to_string(),
|
||||
),
|
||||
("x-hop-private".to_string(), "secret".to_string()),
|
||||
("x-accel-redirect".to_string(), "/internal".to_string()),
|
||||
("set-cookie".to_string(), "session=attacker".to_string()),
|
||||
(
|
||||
"x-aether-future-control".to_string(),
|
||||
"attacker".to_string(),
|
||||
),
|
||||
("content-type".to_string(), "text/plain".to_string()),
|
||||
(
|
||||
"x-proxy-timing".to_string(),
|
||||
@@ -905,6 +1040,10 @@ mod tests {
|
||||
assert!(response.headers().get("content-length").is_none());
|
||||
assert!(response.headers().get("transfer-encoding").is_none());
|
||||
assert!(response.headers().get("connection").is_none());
|
||||
assert!(response.headers().get("x-hop-private").is_none());
|
||||
assert!(response.headers().get("x-accel-redirect").is_none());
|
||||
assert!(response.headers().get("set-cookie").is_none());
|
||||
assert!(response.headers().get("x-aether-future-control").is_none());
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
|
||||
Reference in New Issue
Block a user