mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
340 lines
12 KiB
Rust
340 lines
12 KiB
Rust
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<HubRouter>,
|
|
pub proxy_conn_cfg: ConnConfig,
|
|
pub max_streams: usize,
|
|
data: Arc<GatewayDataState>,
|
|
request_gate: Option<Arc<ConcurrencyGate>>,
|
|
distributed_request_gate: Option<Arc<RuntimeSemaphore>>,
|
|
secure_tunnel_keys: Arc<DashMap<String, String>>,
|
|
}
|
|
|
|
#[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<String>,
|
|
key: impl Into<String>,
|
|
) {
|
|
self.secure_tunnel_keys.insert(node_id.into(), key.into());
|
|
}
|
|
|
|
pub(crate) fn secure_tunnel_key(&self, node_id: &str) -> Option<String> {
|
|
self.secure_tunnel_keys
|
|
.get(node_id)
|
|
.map(|entry| entry.value().clone())
|
|
}
|
|
|
|
async fn secure_tunnel_key_for_node(&self, node_id: &str) -> Option<String> {
|
|
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<GatewayDataState>) -> Self {
|
|
self.data = data;
|
|
self
|
|
}
|
|
|
|
pub fn with_request_concurrency_limit(mut self, limit: Option<usize>) -> 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<ConcurrencySnapshot> {
|
|
self.request_gate.as_ref().map(|gate| gate.snapshot())
|
|
}
|
|
|
|
async fn distributed_request_concurrency_snapshot(
|
|
&self,
|
|
) -> Result<Option<RuntimeSemaphoreSnapshot>, RuntimeSemaphoreError> {
|
|
match self.distributed_request_gate.as_ref() {
|
|
Some(gate) => gate.snapshot().await.map(Some),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
async fn metric_samples(&self) -> Vec<MetricSample> {
|
|
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<Option<AdmissionPermit>, 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<AppState>) -> 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<AppState>) -> impl IntoResponse {
|
|
Json(state.hub.stats())
|
|
}
|
|
|
|
async fn metrics(State(state): State<AppState>) -> impl IntoResponse {
|
|
prometheus_response(&state.metric_samples().await)
|
|
}
|
|
|
|
pub async fn ws_proxy(
|
|
ws: WebSocketUpgrade,
|
|
State(state): State<AppState>,
|
|
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()
|
|
}
|