From 3b462f7ecf0941867bd148e1532d69817cb1f76a Mon Sep 17 00:00:00 2001 From: Zyy Date: Wed, 8 Jul 2026 15:22:23 +0800 Subject: [PATCH] refactor: remove trusted proxy config Co-authored-by: Codex --- backend/src/config/schema.rs | 3 - backend/src/http/middleware/request_id.rs | 106 +++------------------ backend/src/http/router.rs | 25 +---- backend/src/runtime/bootstrap.rs | 4 +- backend/tests/config/mod.rs | 26 +---- backend/tests/http/trace_middleware/mod.rs | 102 ++------------------ backend/tests/support/config.rs | 1 - deploy/README.md | 4 +- deploy/config.example.yaml | 5 - 9 files changed, 27 insertions(+), 249 deletions(-) diff --git a/backend/src/config/schema.rs b/backend/src/config/schema.rs index f126c2e10..73b0af122 100644 --- a/backend/src/config/schema.rs +++ b/backend/src/config/schema.rs @@ -57,9 +57,6 @@ pub struct ServerConfig { pub host: String, /// 监听端口。 pub port: u16, - /// 可信反向代理 IP 或 CIDR。非空时只有这些 peer 的转发 IP 头会被采信。 - #[serde(default)] - pub trusted_proxies: Vec, } /// 上游 API 地址配置。 diff --git a/backend/src/http/middleware/request_id.rs b/backend/src/http/middleware/request_id.rs index 6a16c901b..82f21edde 100644 --- a/backend/src/http/middleware/request_id.rs +++ b/backend/src/http/middleware/request_id.rs @@ -4,7 +4,7 @@ use std::net::{IpAddr, SocketAddr}; use std::sync::LazyLock; use axum::{ - extract::{connect_info::ConnectInfo, Request, State}, + extract::{connect_info::ConnectInfo, Request}, http::{HeaderMap, HeaderValue}, middleware::Next, response::Response, @@ -19,7 +19,7 @@ const CF_CONNECTING_IP_HEADER: &str = "cf-connecting-ip"; const REAL_IP_HEADER: &str = "x-real-ip"; const X_FORWARDED_FOR_HEADER: &str = "x-forwarded-for"; -/// 私有 / 回环 / 唯一本地地址网段,用于在自动模式下从 `X-Forwarded-For` 中跳过内网跳板 IP。 +/// 私有 / 回环 / 唯一本地地址网段,用于从 `X-Forwarded-For` 中跳过内网跳板 IP。 static PRIVATE_NETWORKS: LazyLock> = LazyLock::new(|| { [ "10.0.0.0/8", @@ -38,40 +38,6 @@ fn is_private_ip(ip: IpAddr) -> bool { PRIVATE_NETWORKS.iter().any(|network| network.contains(&ip)) } -/// 可信反向代理配置。 -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct TrustedProxyConfig { - networks: Vec, -} - -impl TrustedProxyConfig { - /// 从 IP 或 CIDR 字符串构造可信代理配置。 - pub fn from_entries(entries: &[String]) -> Result { - let networks = entries - .iter() - .map(|entry| parse_trusted_proxy_entry(entry)) - .collect::, _>>()?; - Ok(Self { networks }) - } - - fn is_trusted(&self, ip: IpAddr) -> bool { - self.networks.iter().any(|network| network.contains(&ip)) - } - - /// 是否未配置任何可信代理(决定是否启用自动真实 IP 探测)。 - fn is_empty(&self) -> bool { - self.networks.is_empty() - } -} - -/// 可信代理配置错误。 -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum TrustedProxyConfigError { - /// 配置项不是 IP 地址或 CIDR。 - #[error("invalid trusted proxy entry `{entry}`; expected an IP address or CIDR network")] - InvalidEntry { entry: String }, -} - /// 请求 ID。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct RequestId(String); @@ -97,7 +63,7 @@ impl RequestId { } } -/// 由连接来源和可信代理链解析出的客户端 IP。 +/// 由连接来源和转发头解析出的客户端 IP。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientIp(String); @@ -118,17 +84,7 @@ fn valid_request_id(value: &str) -> bool { /// 为请求附加请求 ID,并在响应头中回写。 pub async fn attach_request_id(mut request: Request, next: Next) -> Response { - attach_request_context(&TrustedProxyConfig::default(), &mut request); - run_with_request_id(request, next).await -} - -/// 为请求附加请求上下文,并按可信代理配置解析真实客户端 IP。 -pub async fn attach_request_id_with_proxy_config( - State(trusted_proxies): State, - mut request: Request, - next: Next, -) -> Response { - attach_request_context(&trusted_proxies, &mut request); + attach_request_context(&mut request); run_with_request_id(request, next).await } @@ -147,17 +103,16 @@ async fn run_with_request_id(mut request: Request, next: Next) -> Response { response } -fn attach_request_context(trusted_proxies: &TrustedProxyConfig, request: &mut Request) { - attach_client_ip_from_connection(trusted_proxies, request); +fn attach_request_context(request: &mut Request) { + attach_client_ip_from_connection(request); } -fn attach_client_ip_from_connection(trusted_proxies: &TrustedProxyConfig, request: &mut Request) { +fn attach_client_ip_from_connection(request: &mut Request) { let peer_ip = request .extensions() .get::>() .map(|ConnectInfo(addr)| addr.ip()); - let client_ip = - peer_ip.map(|peer_ip| client_ip_from_peer(peer_ip, request.headers(), trusted_proxies)); + let client_ip = peer_ip.map(|peer_ip| client_ip_from_peer(peer_ip, request.headers())); let Some(client_ip) = client_ip else { return; @@ -165,28 +120,12 @@ fn attach_client_ip_from_connection(trusted_proxies: &TrustedProxyConfig, reques request.extensions_mut().insert(ClientIp(client_ip)); } -fn client_ip_from_peer( - peer_ip: IpAddr, - headers: &HeaderMap, - trusted_proxies: &TrustedProxyConfig, -) -> String { - if trusted_proxies.is_empty() { - // 未配置可信代理:自动模式,按 CF-Connecting-IP → X-Real-IP → - // X-Forwarded-For(首个公网 IP)→ peer IP 的优先级解析真实客户端 IP。 - return auto_forwarded_client_ip(headers).unwrap_or_else(|| peer_ip.to_string()); - } - if trusted_proxies.is_trusted(peer_ip) { - // 已配置可信代理且直连来源可信:只采信转发头。 - trusted_forwarded_client_ip(headers).unwrap_or_else(|| peer_ip.to_string()) - } else { - // 已配置可信代理但直连来源不可信:忽略转发头,使用 peer IP。 - peer_ip.to_string() - } +fn client_ip_from_peer(peer_ip: IpAddr, headers: &HeaderMap) -> String { + auto_forwarded_client_ip(headers).unwrap_or_else(|| peer_ip.to_string()) } -/// 自动模式下解析转发头中的真实客户端 IP。 +/// 解析转发头中的真实客户端 IP。 /// -/// 未配置可信代理时使用,对齐常见反代(Cloudflare / Nginx)默认行为: /// `CF-Connecting-IP` → `X-Real-IP` → `X-Forwarded-For` 首个公网 IP。 fn auto_forwarded_client_ip(headers: &HeaderMap) -> Option { if let Some(cf_ip) = header_string(headers, CF_CONNECTING_IP_HEADER) { @@ -212,13 +151,6 @@ fn forwarded_for_public_ip(headers: &HeaderMap) -> Option { .map(ToString::to_string) } -fn trusted_forwarded_client_ip(headers: &HeaderMap) -> Option { - header_string(headers, CF_CONNECTING_IP_HEADER) - .or_else(|| header_string(headers, REAL_IP_HEADER)) - .or_else(|| forwarded_for_public_ip(headers)) - .or_else(|| first_forwarded_for_value(headers)) -} - fn header_string(headers: &HeaderMap, name: &str) -> Option { headers .get(name) @@ -237,19 +169,3 @@ fn first_forwarded_for_value(headers: &HeaderMap) -> Option { .filter(|value| !value.is_empty()) .map(ToString::to_string) } - -fn parse_trusted_proxy_entry(entry: &str) -> Result { - if let Ok(network) = entry.parse::() { - return Ok(network); - } - entry - .parse::() - .map(IpNet::from) - .map_err(|_| invalid_trusted_proxy_entry(entry)) -} - -fn invalid_trusted_proxy_entry(entry: &str) -> TrustedProxyConfigError { - TrustedProxyConfigError::InvalidEntry { - entry: entry.to_string(), - } -} diff --git a/backend/src/http/router.rs b/backend/src/http/router.rs index f5f380ad7..3ca1025f1 100644 --- a/backend/src/http/router.rs +++ b/backend/src/http/router.rs @@ -5,10 +5,7 @@ use std::path::Path; use axum::{extract::State, http::StatusCode, middleware, routing::get, Router}; use crate::admin; -use crate::http::middleware::{ - request_id::{attach_request_id_with_proxy_config, TrustedProxyConfig}, - trace::http_trace_layer, -}; +use crate::http::middleware::{request_id::attach_request_id, trace::http_trace_layer}; use crate::proxy; use crate::runtime::state::AppState; use crate::web; @@ -18,34 +15,18 @@ pub const DEFAULT_ASSET_DIST_DIR: &str = "web/dist"; /// 构造整个 HTTP 服务路由。 pub fn router() -> Router { - router_with_trusted_proxies(TrustedProxyConfig::default()) -} - -/// 使用指定可信代理配置构造整个 HTTP 服务路由。 -pub fn router_with_trusted_proxies(trusted_proxies: TrustedProxyConfig) -> Router { - router_with_assets_and_trusted_proxies(DEFAULT_ASSET_DIST_DIR, trusted_proxies) + router_with_assets(DEFAULT_ASSET_DIST_DIR) } /// 使用指定前端构建产物目录构造整个 HTTP 服务路由。 pub fn router_with_assets(dist_dir: impl AsRef) -> Router { - router_with_assets_and_trusted_proxies(dist_dir, TrustedProxyConfig::default()) -} - -/// 使用指定前端构建产物目录和可信代理配置构造整个 HTTP 服务路由。 -pub fn router_with_assets_and_trusted_proxies( - dist_dir: impl AsRef, - trusted_proxies: TrustedProxyConfig, -) -> Router { Router::new() .route("/healthz", get(healthz)) .merge(proxy::router::router()) .merge(admin::router::router()) .fallback_service(web::assets::spa_router(dist_dir)) .layer(http_trace_layer()) - .layer(middleware::from_fn_with_state( - trusted_proxies, - attach_request_id_with_proxy_config, - )) + .layer(middleware::from_fn(attach_request_id)) } async fn healthz(State(state): State) -> StatusCode { diff --git a/backend/src/runtime/bootstrap.rs b/backend/src/runtime/bootstrap.rs index 5c044bc26..933d3ab6a 100644 --- a/backend/src/runtime/bootstrap.rs +++ b/backend/src/runtime/bootstrap.rs @@ -5,7 +5,6 @@ use chrono::Utc; use crate::config::schema::AppConfig; use crate::config::settings::RuntimeSettingsService; -use crate::http::middleware::request_id::TrustedProxyConfig; use crate::infra::database::connect_sqlite; use crate::infra::logging::{init_tracing, LogGuard, RotationConfig}; use crate::infra::paths::{ensure_data_dir, load_or_create_installation_id}; @@ -113,11 +112,10 @@ async fn build_application( ); let task_coordinator = TaskCoordinator::start(&runtime_config, &services); - let trusted_proxies = TrustedProxyConfig::from_entries(&config.server.trusted_proxies)?; let app_state = AppState { services }; Ok(( - crate::http::router::router_with_trusted_proxies(trusted_proxies).with_state(app_state), + crate::http::router::router().with_state(app_state), task_coordinator, )) } diff --git a/backend/tests/config/mod.rs b/backend/tests/config/mod.rs index fa27c7bc6..1512e2007 100644 --- a/backend/tests/config/mod.rs +++ b/backend/tests/config/mod.rs @@ -8,7 +8,6 @@ const DEFAULT_CONFIG_YAML: &str = r#" server: host: 0.0.0.0 port: 8080 - trusted_proxies: [] api: base_url: https://chatgpt.com/backend-api database: @@ -80,7 +79,6 @@ logging: fn default_config_keeps_only_codex_backend() { let cfg: AppConfig = parse_yaml_config(DEFAULT_CONFIG_YAML).unwrap(); assert_eq!(cfg.server.host, "0.0.0.0"); - assert!(cfg.server.trusted_proxies.is_empty()); assert_eq!(cfg.api.base_url, "https://chatgpt.com/backend-api"); assert!(cfg.model_aliases.is_empty()); assert_eq!(cfg.auth.refresh_margin_seconds, 300); @@ -259,7 +257,6 @@ ws_pool: assert_eq!(cfg.server.host, "127.0.0.1"); assert_eq!(cfg.server.port, 8080); - assert!(cfg.server.trusted_proxies.is_empty()); assert_eq!( cfg.database.url, "sqlite://.runtime/data/codex-proxy-rs.sqlite" @@ -280,30 +277,15 @@ ws_pool: assert_eq!(cfg.logging.retention_days, 14); } -#[test] -fn config_should_parse_trusted_proxy_entries() { - let cfg: AppConfig = parse_yaml_config(&DEFAULT_CONFIG_YAML.replace( - "trusted_proxies: []", - "trusted_proxies:\n - 127.0.0.1\n - 10.0.0.0/8", - )) - .unwrap(); - - assert_eq!( - cfg.server.trusted_proxies, - vec!["127.0.0.1".to_string(), "10.0.0.0/8".to_string()] - ); -} - #[test] fn server_config_should_reject_unknown_fields() { - let err = parse_yaml_config::(&DEFAULT_CONFIG_YAML.replace( - "trusted_proxies: []", - "trusted_proxies: []\n trusted_proxy: 127.0.0.1", - )) + let err = parse_yaml_config::( + &DEFAULT_CONFIG_YAML.replace(" port: 8080", " port: 8080\n trusted_proxies: []"), + ) .unwrap_err(); assert!( - err.to_string().contains("trusted_proxy"), + err.to_string().contains("trusted_proxies"), "expected unknown server field to be rejected, got {err}" ); } diff --git a/backend/tests/http/trace_middleware/mod.rs b/backend/tests/http/trace_middleware/mod.rs index eeeb519cf..f0511eed4 100644 --- a/backend/tests/http/trace_middleware/mod.rs +++ b/backend/tests/http/trace_middleware/mod.rs @@ -9,14 +9,12 @@ use axum::{ extract::connect_info::ConnectInfo, http::HeaderMap, http::{Request, StatusCode}, - middleware::{from_fn, from_fn_with_state}, + middleware::from_fn, routing::get, Extension, Router, }; use codex_proxy_rs::http::middleware::{ - request_id::{ - attach_request_id, attach_request_id_with_proxy_config, ClientIp, TrustedProxyConfig, - }, + request_id::{attach_request_id, ClientIp}, trace::http_trace_layer, }; use tokio::time::{sleep, Duration}; @@ -285,7 +283,7 @@ async fn request_context_should_auto_detect_forwarded_ip_without_trusted_proxy_c let response = app.oneshot(request).await.expect("response"); let bytes = to_bytes(response.into_body(), 1024).await.unwrap(); - // 未配置可信代理时进入自动模式:CF-Connecting-IP 优先于 X-Real-IP / X-Forwarded-For。 + // CF-Connecting-IP 优先于 X-Real-IP / X-Forwarded-For。 assert_eq!( std::str::from_utf8(&bytes).unwrap(), "203.0.113.43|cf=203.0.113.43|xff=203.0.113.42, 10.0.0.2|real=203.0.113.44" @@ -319,45 +317,7 @@ async fn request_context_should_skip_private_forwarded_hops_in_auto_mode() { } #[tokio::test] -async fn request_context_should_accept_forwarded_ip_from_trusted_proxy() { - let trusted_proxies = - TrustedProxyConfig::from_entries(&["127.0.0.0/8".to_string()]).expect("trusted proxy"); - let app = Router::new() - .route( - "/ip", - get( - |Extension(client_ip): Extension, headers: HeaderMap| async move { - let forwarded_present = headers.contains_key("x-forwarded-for"); - format!("{}|forwarded={forwarded_present}", client_ip.as_str()) - }, - ), - ) - .layer(from_fn_with_state( - trusted_proxies, - attach_request_id_with_proxy_config, - )); - let mut request = Request::builder() - .uri("/ip") - .header("x-forwarded-for", "203.0.113.42, 10.0.0.2") - .body(Body::empty()) - .expect("request should build"); - request - .extensions_mut() - .insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 61234)))); - - let response = app.oneshot(request).await.expect("response"); - let bytes = to_bytes(response.into_body(), 1024).await.unwrap(); - - assert_eq!( - std::str::from_utf8(&bytes).unwrap(), - "203.0.113.42|forwarded=true" - ); -} - -#[tokio::test] -async fn request_context_should_accept_real_ip_from_trusted_proxy_when_forwarded_for_missing() { - let trusted_proxies = - TrustedProxyConfig::from_entries(&["127.0.0.1".to_string()]).expect("trusted proxy"); +async fn request_context_should_accept_real_ip_when_forwarded_for_missing() { let app = Router::new() .route( "/ip", @@ -365,10 +325,7 @@ async fn request_context_should_accept_real_ip_from_trusted_proxy_when_forwarded client_ip.as_str().to_string() }), ) - .layer(from_fn_with_state( - trusted_proxies, - attach_request_id_with_proxy_config, - )); + .layer(from_fn(attach_request_id)); let mut request = Request::builder() .uri("/ip") .header("x-real-ip", "198.51.100.23") @@ -385,9 +342,7 @@ async fn request_context_should_accept_real_ip_from_trusted_proxy_when_forwarded } #[tokio::test] -async fn request_context_should_prefer_real_ip_over_forwarded_for_from_trusted_proxy() { - let trusted_proxies = - TrustedProxyConfig::from_entries(&["127.0.0.1".to_string()]).expect("trusted proxy"); +async fn request_context_should_prefer_real_ip_over_forwarded_for() { let app = Router::new() .route( "/ip", @@ -395,10 +350,7 @@ async fn request_context_should_prefer_real_ip_over_forwarded_for_from_trusted_p client_ip.as_str().to_string() }), ) - .layer(from_fn_with_state( - trusted_proxies, - attach_request_id_with_proxy_config, - )); + .layer(from_fn(attach_request_id)); let mut request = Request::builder() .uri("/ip") .header("x-forwarded-for", "198.51.100.99, 203.0.113.42") @@ -415,36 +367,6 @@ async fn request_context_should_prefer_real_ip_over_forwarded_for_from_trusted_p assert_eq!(std::str::from_utf8(&bytes).unwrap(), "203.0.113.42"); } -#[tokio::test] -async fn request_context_should_ignore_forwarded_ip_from_untrusted_peer() { - let trusted_proxies = - TrustedProxyConfig::from_entries(&["10.0.0.0/8".to_string()]).expect("trusted proxy"); - let app = Router::new() - .route( - "/ip", - get(|Extension(client_ip): Extension| async move { - client_ip.as_str().to_string() - }), - ) - .layer(from_fn_with_state( - trusted_proxies, - attach_request_id_with_proxy_config, - )); - let mut request = Request::builder() - .uri("/ip") - .header("x-forwarded-for", "203.0.113.42") - .body(Body::empty()) - .expect("request should build"); - request - .extensions_mut() - .insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 61234)))); - - let response = app.oneshot(request).await.expect("response"); - let bytes = to_bytes(response.into_body(), 1024).await.unwrap(); - - assert_eq!(std::str::from_utf8(&bytes).unwrap(), "127.0.0.1"); -} - #[tokio::test] async fn request_context_should_not_inject_real_ip_header() { let app = Router::new() @@ -474,13 +396,3 @@ async fn request_context_should_not_inject_real_ip_header() { assert_eq!(std::str::from_utf8(&bytes).unwrap(), "missing"); } - -#[test] -fn trusted_proxy_config_should_reject_invalid_entries() { - let error = TrustedProxyConfig::from_entries(&["not-an-ip".to_string()]).unwrap_err(); - - assert!( - error.to_string().contains("not-an-ip"), - "unexpected error: {error}" - ); -} diff --git a/backend/tests/support/config.rs b/backend/tests/support/config.rs index b95ba37ad..cd479bb95 100644 --- a/backend/tests/support/config.rs +++ b/backend/tests/support/config.rs @@ -8,7 +8,6 @@ pub(crate) fn test_config(database_url: String) -> AppConfig { server: ServerConfig { host: "127.0.0.1".to_string(), port: 0, - trusted_proxies: Vec::new(), }, api: ApiConfig { base_url: "https://chatgpt.com/backend-api".to_string(), diff --git a/deploy/README.md b/deploy/README.md index b55972971..53638d092 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -19,9 +19,7 @@ cp deploy/config.example.yaml .runtime/config.yaml 复制后必须设置 `admin.default_password`。空值、长度不足 12 位或常见弱口令会被后端拒绝。 -如果前面还有 Nginx、Caddy、Cloudflare Tunnel 等反向代理,把后端实际看到的代理 peer IP 或 CIDR 写入 `server.trusted_proxies`。经宿主机反代访问 Docker 端口时,这个 peer 通常是 Compose 网络网关,例如 `172.18.0.1/32`,不是宿主 `127.0.0.1/32`。 - -`server.trusted_proxies` 非空时,只有这些 peer 的 `CF-Connecting-IP` / `X-Real-IP` / `X-Forwarded-For` 会被采信;为空时进入自动解析模式,按同一组头解析真实 IP。 +如果前面还有 Nginx、Caddy、Cloudflare Tunnel 等反向代理,确保反代正常传递 `CF-Connecting-IP`、`X-Real-IP` 或 `X-Forwarded-For`。后端会按这组头自动解析真实客户端 IP;没有这些头时回落到直连 peer IP。 构建并启动: diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index 2fa1dbb8d..70b69b528 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -3,11 +3,6 @@ server: host: '0.0.0.0' # 容器内服务端口,需与 docker-compose.yml 的 target port 一致。 port: 8080 - # 可信反向代理 IP 或 CIDR。非空时只有这些 peer 的转发 IP 头会被采信。 - # Docker 端口映射经宿主反代访问时,后端看到的 peer 通常是 Compose 网络网关 - # (例如 172.18.0.1),不是宿主 127.0.0.1。 - # 为空时进入自动解析模式,按 CF-Connecting-IP / X-Real-IP / X-Forwarded-For 解析。 - trusted_proxies: [] api: # ChatGPT / Codex 上游 API 地址。