refactor(gateway): 将 CF 头剥离中间件下移至各 router 构建函数并重构为前缀匹配

This commit is contained in:
fawney19
2026-04-22 21:10:16 +08:00
parent cf6228f525
commit c8d7dbd8d6
6 changed files with 166 additions and 56 deletions

View File

@@ -20,6 +20,7 @@ use thiserror::Error;
use crate::execution_runtime::{
build_direct_execution_frame_stream, DirectSyncExecutionRuntime, ExecutionRuntimeTransportError,
};
use crate::middleware;
const EXECUTION_RUNTIME_COMPONENT: &str = "aether-gateway-execution-runtime";
const REQUEST_GATE_NAME: &str = "execution_runtime_requests";
@@ -128,12 +129,14 @@ pub fn build_execution_runtime_router_with_request_gates(
.with_distributed_request_gate(gate),
None => ExecutionRuntimeAppState::with_request_concurrency_limit(limit),
};
Router::new()
.route("/health", get(health))
.route("/metrics", get(metrics))
.route("/v1/execute/sync", post(execute_sync))
.route("/v1/execute/stream", post(execute_stream))
.with_state(state)
middleware::apply_cf_header_stripping(
Router::new()
.route("/health", get(health))
.route("/metrics", get(metrics))
.route("/v1/execute/sync", post(execute_sync))
.route("/v1/execute/stream", post(execute_stream))
.with_state(state),
)
}
pub async fn serve_execution_runtime_tcp(

View File

@@ -959,20 +959,14 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
let frontdoor_health_url = format!("{public_base_url}/_gateway/health");
let api_router = build_router_with_state(state);
// Compose the final router: API routes + optional static file serving + CF header stripping
// Compose the final router: API routes + optional static file serving.
let router = if let Some(ref static_dir) = args.static_dir {
use tower_http::compression::CompressionLayer;
info!(static_dir = %static_dir, "serving frontend static files");
attach_static_frontend(api_router, static_dir)
.layer(CompressionLayer::new())
.layer(axum::middleware::from_fn(
aether_gateway::strip_cf_headers_middleware,
))
attach_static_frontend(api_router, static_dir).layer(CompressionLayer::new())
} else {
api_router.layer(axum::middleware::from_fn(
aether_gateway::strip_cf_headers_middleware,
))
api_router
};
info!(

View File

@@ -6,4 +6,5 @@ pub(crate) use access_log::{
access_log_middleware, should_downgrade_access_log, RequestLogEmitted,
};
pub(crate) use frontdoor_cors::frontdoor_cors_middleware;
pub(crate) use strip_cf_headers::apply_cf_header_stripping;
pub use strip_cf_headers::strip_cf_headers_middleware;

View File

@@ -1,36 +1,146 @@
use axum::{extract::Request, middleware::Next, response::Response};
use http::header::HeaderName;
use axum::{extract::Request, middleware::Next, response::Response, Router};
use http::{header::HeaderName, HeaderMap};
/// Cloudflare headers to strip from incoming requests and outgoing responses.
/// Prevents leaking CF metadata to upstream providers or back to clients.
static CF_HEADERS: &[&str] = &[
"cf-connecting-ip",
"cf-ipcountry",
"cf-ray",
"cf-visitor",
"cdn-loop",
"true-client-ip",
"cf-worker",
"cf-ew-via",
"cf-warp-tag-id",
];
/// Cloudflare-specific headers that are not part of the `cf-*` prefix family.
const CF_EXACT_HEADERS: &[&str] = &["cdn-loop", "true-client-ip"];
fn should_strip_cf_header(name: &HeaderName) -> bool {
let normalized = name.as_str();
normalized.starts_with("cf-") || CF_EXACT_HEADERS.contains(&normalized)
}
fn strip_cf_headers(headers: &mut HeaderMap) {
let to_remove: Vec<_> = headers
.keys()
.filter(|name| should_strip_cf_header(name))
.cloned()
.collect();
for name in to_remove {
headers.remove(name);
}
}
pub(crate) fn apply_cf_header_stripping(router: Router) -> Router {
router.layer(axum::middleware::from_fn(strip_cf_headers_middleware))
}
pub async fn strip_cf_headers_middleware(mut request: Request, next: Next) -> Response {
// Strip CF headers from the incoming request
for name in CF_HEADERS {
if let Ok(header) = HeaderName::from_bytes(name.as_bytes()) {
request.headers_mut().remove(&header);
}
}
strip_cf_headers(request.headers_mut());
let mut response = next.run(request).await;
// Strip CF headers from the outgoing response
for name in CF_HEADERS {
if let Ok(header) = HeaderName::from_bytes(name.as_bytes()) {
response.headers_mut().remove(&header);
}
}
strip_cf_headers(response.headers_mut());
response
}
#[cfg(test)]
mod tests {
use axum::body::{to_bytes, Body};
use axum::routing::any;
use axum::Router;
use http::{HeaderValue, Request, Response};
use tower::ServiceExt;
use super::apply_cf_header_stripping;
#[tokio::test]
async fn strips_cf_prefixed_and_exact_headers_from_request_and_response() {
let app = apply_cf_header_stripping(Router::new().route(
"/",
any(|headers: http::HeaderMap| async move {
let leaked = headers.contains_key("cf-ipcity")
|| headers.contains_key("cf-ray")
|| headers.contains_key("true-client-ip")
|| headers.contains_key("cdn-loop");
let mut response =
Response::new(Body::from(if leaked { "leaked" } else { "clean" }));
response.headers_mut().insert(
http::header::HeaderName::from_static("cf-ipcity"),
HeaderValue::from_static("Shanghai"),
);
response.headers_mut().insert(
http::header::HeaderName::from_static("cf-cache-status"),
HeaderValue::from_static("HIT"),
);
response.headers_mut().insert(
http::header::HeaderName::from_static("true-client-ip"),
HeaderValue::from_static("1.1.1.1"),
);
response.headers_mut().insert(
http::header::HeaderName::from_static("cdn-loop"),
HeaderValue::from_static("cloudflare"),
);
response
}),
));
let response = app
.oneshot(
Request::builder()
.uri("/")
.header("cf-ipcity", "Shanghai")
.header("cf-ray", "abc123")
.header("true-client-ip", "1.1.1.1")
.header("cdn-loop", "cloudflare")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert!(response.headers().get("cf-ipcity").is_none());
assert!(response.headers().get("cf-cache-status").is_none());
assert!(response.headers().get("true-client-ip").is_none());
assert!(response.headers().get("cdn-loop").is_none());
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body should be readable");
assert_eq!(body.as_ref(), b"clean");
}
#[tokio::test]
async fn preserves_non_cf_headers() {
let app = apply_cf_header_stripping(Router::new().route(
"/",
any(|headers: http::HeaderMap| async move {
let mut response = Response::new(Body::from(
headers
.get("x-custom-header")
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string(),
));
response.headers_mut().insert(
http::header::HeaderName::from_static("x-custom-response"),
HeaderValue::from_static("kept"),
);
response
}),
));
let response = app
.oneshot(
Request::builder()
.uri("/")
.header("x-custom-header", "kept")
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(
response
.headers()
.get("x-custom-response")
.and_then(|value| value.to_str().ok()),
Some("kept")
);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("body should be readable");
assert_eq!(body.as_ref(), b"kept");
}
}

View File

@@ -43,19 +43,19 @@ pub fn build_router_with_state(state: AppState) -> Router {
middleware::frontdoor_cors_middleware,
));
}
router
middleware::apply_cf_header_stripping(router)
}
pub fn attach_static_frontend(router: Router, static_dir: impl Into<PathBuf>) -> Router {
let static_dir = static_dir.into();
let index_html = static_dir.join("index.html");
router.layer(axum::middleware::from_fn_with_state(
middleware::apply_cf_header_stripping(router.layer(axum::middleware::from_fn_with_state(
FrontendStaticState {
static_dir,
index_html,
},
frontend_static_middleware,
))
)))
}
async fn frontend_static_middleware(

View File

@@ -19,7 +19,7 @@ use axum::routing::{get, post};
use axum::Router;
use tracing::warn;
use crate::data::GatewayDataState;
use crate::{data::GatewayDataState, middleware};
pub use control_plane::ControlPlaneClient;
pub use hub::{ConnConfig, HubRouter, LocalBodyEvent, ProxyConn};
@@ -138,16 +138,18 @@ impl AppState {
}
pub fn build_router_with_state(state: AppState) -> Router {
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)
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 {