mod control_plane; mod hub; mod local_relay; pub mod protocol; mod proxy_conn; use std::sync::Arc; use aether_gateway_tunnel::{ resolve_proxy_max_streams, resolve_proxy_node_name, resolve_proxy_protocol_version, }; use aether_runtime::{ hold_admission_permit_until, prometheus_response, service_up_sample, AdmissionPermit, ConcurrencyError, ConcurrencyGate, ConcurrencySnapshot, MetricKind, MetricLabel, MetricSample, }; use aether_runtime_state::{RuntimeSemaphore, RuntimeSemaphoreError, RuntimeSemaphoreSnapshot}; use axum::extract::ws::WebSocketUpgrade; use axum::extract::State; use axum::http::HeaderMap; use axum::response::{IntoResponse, Json}; use axum::routing::{get, post}; use axum::Router; use dashmap::DashMap; use tracing::warn; use crate::{data::GatewayDataState, middleware}; pub use control_plane::ControlPlaneClient; pub use hub::{ConnConfig, HubRouter, LocalBodyEvent, ProxyConn}; pub use local_relay::relay_request; pub(crate) use local_relay::{open_direct_relay_stream, DirectRelayResponse}; #[derive(Clone)] pub struct AppState { pub hub: Arc, pub proxy_conn_cfg: ConnConfig, pub max_streams: usize, data: Arc, request_gate: Option>, distributed_request_gate: Option>, secure_tunnel_keys: Arc>, } #[derive(Debug)] enum RequestAdmissionError { Local(ConcurrencyError), Distributed(RuntimeSemaphoreError), } impl AppState { pub fn new( control_plane: ControlPlaneClient, proxy_conn_cfg: ConnConfig, max_streams: usize, ) -> Self { Self { hub: HubRouter::new(control_plane), proxy_conn_cfg, max_streams, data: Arc::new(GatewayDataState::disabled()), request_gate: None, distributed_request_gate: None, secure_tunnel_keys: Arc::new(DashMap::new()), } } pub(crate) fn register_secure_tunnel_key( &self, node_id: impl Into, key: impl Into, ) { self.secure_tunnel_keys.insert(node_id.into(), key.into()); } pub(crate) fn secure_tunnel_key(&self, node_id: &str) -> Option { self.secure_tunnel_keys .get(node_id) .map(|entry| entry.value().clone()) } async fn secure_tunnel_key_for_node(&self, node_id: &str) -> Option { if let Some(key) = self.secure_tunnel_key(node_id) { return Some(key); } let key = self .data .find_proxy_node(node_id) .await .ok() .flatten() .and_then(|node| { node.proxy_metadata.and_then(|metadata| { metadata .pointer("/tunnel_security/encryption_key") .and_then(|value| value.as_str()) .map(str::to_string) }) }); if let Some(key) = key.as_ref() { self.register_secure_tunnel_key(node_id.to_string(), key.clone()); } key } pub(crate) fn with_data(mut self, data: Arc) -> Self { self.data = data; self } pub fn with_request_concurrency_limit(mut self, limit: Option) -> Self { self.request_gate = limit .filter(|limit| *limit > 0) .map(|limit| Arc::new(ConcurrencyGate::new("tunnel_requests", limit))); self } pub fn with_distributed_request_gate(mut self, gate: RuntimeSemaphore) -> Self { self.distributed_request_gate = Some(Arc::new(gate)); self } fn request_concurrency_snapshot(&self) -> Option { self.request_gate.as_ref().map(|gate| gate.snapshot()) } async fn distributed_request_concurrency_snapshot( &self, ) -> Result, RuntimeSemaphoreError> { match self.distributed_request_gate.as_ref() { Some(gate) => gate.snapshot().await.map(Some), None => Ok(None), } } async fn metric_samples(&self) -> Vec { let mut samples = vec![service_up_sample("aether-tunnel-standalone")]; if let Some(snapshot) = self.request_concurrency_snapshot() { samples.extend(snapshot.to_metric_samples("tunnel_requests")); } if let Some(gate) = self.distributed_request_gate.as_ref() { match gate.snapshot().await { Ok(snapshot) => { samples.extend(snapshot.to_metric_samples("tunnel_requests_distributed")); } Err(_) => samples.push( MetricSample::new( "concurrency_unavailable", "Whether the distributed concurrency gate is currently unavailable.", MetricKind::Gauge, 1, ) .with_labels(vec![MetricLabel::new( "gate", "tunnel_requests_distributed", )]), ), } } samples.extend(self.hub.stats().to_metric_samples()); samples } async fn try_acquire_request_permit( &self, ) -> Result, RequestAdmissionError> { let local = self .request_gate .as_ref() .map(|gate| gate.try_acquire()) .transpose() .map_err(RequestAdmissionError::Local)?; let distributed = match self.distributed_request_gate.as_ref() { Some(gate) => Some( gate.try_acquire() .await .map_err(RequestAdmissionError::Distributed)?, ), None => None, }; Ok(AdmissionPermit::from_parts(local, distributed)) } } pub fn build_router_with_state(state: AppState) -> Router { middleware::apply_cf_header_stripping( Router::new() .route("/health", get(health)) .route("/metrics", get(metrics)) .route("/stats", get(stats)) .route("/api/internal/proxy-tunnel", get(ws_proxy)) .route( "/api/internal/tunnel/relay/{node_id}", post(local_relay::relay_request), ) .with_state(state), ) } async fn health(State(state): State) -> impl IntoResponse { let request_concurrency = state.request_concurrency_snapshot().map(|snapshot| { serde_json::json!({ "limit": snapshot.limit, "in_flight": snapshot.in_flight, "available_permits": snapshot.available_permits, "high_watermark": snapshot.high_watermark, "rejected": snapshot.rejected, }) }); let distributed_request_concurrency = state .distributed_request_concurrency_snapshot() .await .ok() .flatten() .map(|snapshot| { serde_json::json!({ "limit": snapshot.limit, "in_flight": snapshot.in_flight, "available_permits": snapshot.available_permits, "high_watermark": snapshot.high_watermark, "rejected": snapshot.rejected, }) }); Json(serde_json::json!({ "status": "ok", "request_concurrency": request_concurrency, "distributed_request_concurrency": distributed_request_concurrency, })) } async fn stats(State(state): State) -> impl IntoResponse { Json(state.hub.stats()) } async fn metrics(State(state): State) -> impl IntoResponse { prometheus_response(&state.metric_samples().await) } pub async fn ws_proxy( ws: WebSocketUpgrade, State(state): State, headers: HeaderMap, ) -> impl IntoResponse { let node_id = headers .get("x-node-id") .and_then(|v| v.to_str().ok()) .unwrap_or("") .trim() .to_string(); let node_name = resolve_proxy_node_name(&headers, &node_id); let max_streams = resolve_proxy_max_streams(&headers, state.max_streams); let protocol_version = resolve_proxy_protocol_version(&headers); let tunnel_security = headers .get(aether_contracts::tunnel_security::TUNNEL_SECURITY_HEADER) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string); let security_session = headers .get(aether_contracts::tunnel_security::TUNNEL_SECURITY_SESSION_HEADER) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string); if node_id.is_empty() { warn!("proxy connection rejected: missing X-Node-ID header"); return axum::http::StatusCode::BAD_REQUEST.into_response(); } let stored_security_key = state.secure_tunnel_key_for_node(&node_id).await; let (security_key, security_session) = match tunnel_security.as_deref() { Some(aether_contracts::tunnel_security::TUNNEL_SECURITY_NON_TLS_REQUIRED) => { match stored_security_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 => { warn!(node_id = %node_id, "secure tunnel requested but no PSK is registered"); return axum::http::StatusCode::UNAUTHORIZED.into_response(); } } } Some(_) => return axum::http::StatusCode::BAD_REQUEST.into_response(), None if stored_security_key.is_some() => { warn!(node_id = %node_id, "proxy connection rejected: stored secure tunnel key requires encrypted frames"); return axum::http::StatusCode::UNAUTHORIZED.into_response(); } None => (None, String::new()), }; let request_permit = match state.try_acquire_request_permit().await { Ok(permit) => permit, Err(RequestAdmissionError::Local(ConcurrencyError::Saturated { .. })) | Err(RequestAdmissionError::Distributed(RuntimeSemaphoreError::Saturated { .. })) | Err(RequestAdmissionError::Distributed(RuntimeSemaphoreError::Unavailable { .. })) => { return axum::http::StatusCode::SERVICE_UNAVAILABLE.into_response() } Err(RequestAdmissionError::Local(ConcurrencyError::Closed { gate })) => { warn!( gate = gate, "standalone tunnel relay request concurrency gate is closed" ); return axum::http::StatusCode::SERVICE_UNAVAILABLE.into_response(); } Err(RequestAdmissionError::Distributed(RuntimeSemaphoreError::InvalidConfiguration( message, ))) => { warn!( error = %message, "standalone tunnel relay distributed request gate is invalid" ); return axum::http::StatusCode::SERVICE_UNAVAILABLE.into_response(); } }; ws.max_frame_size(64 * 1024 * 1024) .on_upgrade(move |socket| { hold_admission_permit_until(request_permit, async move { proxy_conn::handle_proxy_connection( socket, state.hub, node_id, node_name, max_streams, protocol_version, security_key, security_session, state.proxy_conn_cfg, ) .await }) }) .into_response() }