refactor: remove trusted proxy config

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-07-08 15:22:23 +08:00
co-authored by Codex
parent 5682d4fbe9
commit 3b462f7ecf
9 changed files with 27 additions and 249 deletions
-3
View File
@@ -57,9 +57,6 @@ pub struct ServerConfig {
pub host: String,
/// 监听端口。
pub port: u16,
/// 可信反向代理 IP 或 CIDR。非空时只有这些 peer 的转发 IP 头会被采信。
#[serde(default)]
pub trusted_proxies: Vec<String>,
}
/// 上游 API 地址配置。
+11 -95
View File
@@ -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<Vec<IpNet>> = 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<IpNet>,
}
impl TrustedProxyConfig {
/// 从 IP 或 CIDR 字符串构造可信代理配置。
pub fn from_entries(entries: &[String]) -> Result<Self, TrustedProxyConfigError> {
let networks = entries
.iter()
.map(|entry| parse_trusted_proxy_entry(entry))
.collect::<Result<Vec<_>, _>>()?;
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<TrustedProxyConfig>,
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::<ConnectInfo<SocketAddr>>()
.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<String> {
if let Some(cf_ip) = header_string(headers, CF_CONNECTING_IP_HEADER) {
@@ -212,13 +151,6 @@ fn forwarded_for_public_ip(headers: &HeaderMap) -> Option<String> {
.map(ToString::to_string)
}
fn trusted_forwarded_client_ip(headers: &HeaderMap) -> Option<String> {
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<String> {
headers
.get(name)
@@ -237,19 +169,3 @@ fn first_forwarded_for_value(headers: &HeaderMap) -> Option<String> {
.filter(|value| !value.is_empty())
.map(ToString::to_string)
}
fn parse_trusted_proxy_entry(entry: &str) -> Result<IpNet, TrustedProxyConfigError> {
if let Ok(network) = entry.parse::<IpNet>() {
return Ok(network);
}
entry
.parse::<IpAddr>()
.map(IpNet::from)
.map_err(|_| invalid_trusted_proxy_entry(entry))
}
fn invalid_trusted_proxy_entry(entry: &str) -> TrustedProxyConfigError {
TrustedProxyConfigError::InvalidEntry {
entry: entry.to_string(),
}
}
+3 -22
View File
@@ -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<AppState> {
router_with_trusted_proxies(TrustedProxyConfig::default())
}
/// 使用指定可信代理配置构造整个 HTTP 服务路由。
pub fn router_with_trusted_proxies(trusted_proxies: TrustedProxyConfig) -> Router<AppState> {
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<Path>) -> Router<AppState> {
router_with_assets_and_trusted_proxies(dist_dir, TrustedProxyConfig::default())
}
/// 使用指定前端构建产物目录和可信代理配置构造整个 HTTP 服务路由。
pub fn router_with_assets_and_trusted_proxies(
dist_dir: impl AsRef<Path>,
trusted_proxies: TrustedProxyConfig,
) -> Router<AppState> {
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<AppState>) -> StatusCode {
+1 -3
View File
@@ -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,
))
}
+4 -22
View File
@@ -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::<AppConfig>(&DEFAULT_CONFIG_YAML.replace(
"trusted_proxies: []",
"trusted_proxies: []\n trusted_proxy: 127.0.0.1",
))
let err = parse_yaml_config::<AppConfig>(
&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}"
);
}
+7 -95
View File
@@ -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<ClientIp>, 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<ClientIp>| 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}"
);
}
-1
View File
@@ -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(),
+1 -3
View File
@@ -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。
构建并启动:
-5
View File
@@ -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 地址。