可信代理

当 OxPHP 运行在反向代理(Kubernetes Ingress、Cloudflare、AWS ALB、nginx)之后时,每个请求都来自代理的 IP 地址。如果不配置可信代理,限流、访问日志和 $_SERVER['REMOTE_ADDR'] 看到的都会是代理 IP,而不是真实的客户端 IP。

配置

bash
# Comma-separated CIDR list TRUSTED_PROXIES="10.0.0.0/8,172.16.0.0/12,192.168.0.0/16" # Shorthand: all RFC-1918 + loopback + link-local (IPv4 and IPv6) TRUSTED_PROXIES="private" # Keywords and CIDRs combine in one list TRUSTED_PROXIES="private,cloudflare,100.64.0.0/10"

每个以逗号分隔的条目可以是一个 CIDR、一个裸 IP 地址(单个主机),或者关键字 private 和 cloudflare 之一,不区分大小写。不属于以上任何一种的条目会导致启动错误。

Note

未设置时,OxPHP 会忽略所有转发请求头。这是安全的默认行为。

工作原理

当请求来自一个可信 IP 时,OxPHP 会按优先级顺序检查转发请求头:

  1. Forwarded(RFC 7239)—— 标准化的请求头
  2. X-Forwarded-For / X-Forwarded-Proto / X-Forwarded-Host / X-Forwarded-Port —— 事实上的后备方案

如果存在 Forwarded 请求头,则忽略 X-Forwarded-* 请求头。

客户端 IP 提取

OxPHP 采用最右侧非可信算法 —— 与 nginx(real_ip_recursive on)、Caddy、Traefik 和 Apache 使用的方法相同:

text
X-Forwarded-For: 203.0.113.50, 172.16.1.1, 10.0.0.5 TCP peer: 10.0.0.1 (trusted) Walk right-to-left: 10.0.0.5 → trusted → skip 172.16.1.1 → trusted → skip 203.0.113.50 → NOT trusted → client IP

这能防止通过在前面追加伪造值来进行欺骗。攻击者可以在左侧添加虚假 IP,但最右侧的非可信 IP 是由链路中最后一个可信代理设置的。

条目可以携带端口:203.0.113.50:4711 或 [2001:db8::1]:4711,这是某些负载均衡器(其中包括 Azure Application Gateway)写入 X-Forwarded-For 的格式。当客户端地址来自这样的条目时,其端口会成为 REMOTE_PORT。

遍历会在以下位置停止:没有地址的条目,或无法解析为 IP 地址的条目(空元素、unknown、经过混淆的 RFC 7239 标识符),以及 OxPHP 无法作为文本读取的请求头行(例如含有非 ASCII 字节的行)。该位置左侧的任何内容都不会被使用,因为链路的那一部分可能只是客户端自己发送的内容。客户端地址是该位置右侧最右边的非可信条目。如果那里的每个条目都是可信的,则取其中最左边的一个;如果那里根本没有条目,则地址不会被重写,保持为连接对端(端口也一样)。

text
X-Forwarded-For: 198.51.100.9, unknown, 10.0.0.5 TCP peer: 10.0.0.1 (trusted) Walk right-to-left: 10.0.0.5 → trusted → skip unknown → does not parse → stop 198.51.100.9 → never reached Client IP: 10.0.0.5 (every entry right of the stop is trusted → the leftmost of them)

使用 cloudflare 关键字时,如果某个请求的转发链显示它来自 Cloudflare 边缘节点,则其客户端地址改为从 CF-Connecting-IP 请求头中获取。参见位于 CDN 和平台负载均衡器之后。

有哪些变化

当配置了 TRUSTED_PROXIES 且连接的 IP 是可信的时:

组件 未配置可信代理 配置可信代理后
$_SERVER['REMOTE_ADDR'] 代理 IP 真实客户端 IP;当转发请求头得不出地址时为代理 IP
$_SERVER['REMOTE_PORT'] 代理源端口 来自 Forwarded 或 X-Forwarded-For 中 ip:port 条目的客户端端口,否则为 0;地址未被重写时为对端的端口
$_SERVER['HTTPS'] 基于 OxPHP 的 TLS 配置 来自 Forwarded: proto= 或 X-Forwarded-Proto
$_SERVER['REQUEST_SCHEME'] 来自 TLS 的 http 或 https 来自转发的协议
$_SERVER['SERVER_NAME'] 来自 Host 请求头 来自 Forwarded: host= 或 X-Forwarded-Host
$_SERVER['SERVER_PORT'] 来自 Host 请求头 来自 X-Forwarded-Port,否则取 X-Forwarded-Host / Forwarded: host= 的端口,否则为 443/80
限流 按代理 IP 按客户端 IP
访问日志 代理 IP 真实客户端 IP

当客户端地址被重写时,如果地址所来自的条目携带了端口(RFC 7239 的 Forwarded: for="ip:port" 节点,或 X-Forwarded-For 中的 ip:port 条目),REMOTE_PORT 就是客户端的源端口,否则为 0。端口无法重建,因此会被置零而不是猜测。取自 CF-Connecting-IP 的地址同样没有端口,所以此时 REMOTE_PORT 也为 0。当请求头得不出客户端地址、地址未被重写时(如客户端 IP 提取中所述),REMOTE_PORT 保持为连接对端的端口。

private 网络

private 简写包括:

网络 说明
10.0.0.0/8 A 类私有地址
172.16.0.0/12 B 类私有地址
192.168.0.0/16 C 类私有地址
127.0.0.0/8 回环地址
169.254.0.0/16 链路本地地址
::1/128 IPv6 回环地址
fc00::/7 IPv6 唯一本地地址
fe80::/10 IPv6 链路本地地址

共享地址空间(100.64.0.0/10,RFC 6598)不属于 private。如果你的负载均衡器从该地址段发起连接,请把它列在关键字旁边:TRUSTED_PROXIES="private,100.64.0.0/10"。

cloudflare 网络

cloudflare 关键字会加入 Cloudflare 在 cloudflare.com/ips 公布的边缘地址段:15 个 IPv4 段和 7 个 IPv6 段,以 2026-10-05 公布的版本为准。该列表编译在 OxPHP 中,只会随 OxPHP 的发布而变化。当 Cloudflare 的列表先发生变化时:

  • 新增的地址段可以列在关键字旁边:TRUSTED_PROXIES="private,cloudflare,<new range>"。列出的地址段会被视为你自己的代理,而不是 Cloudflare 边缘节点,因此在某个版本把该地址段加入关键字之前,经由它的请求会按最右侧非可信规则取得地址,而不是从 CF-Connecting-IP 获取。
  • 撤回的地址段无法从关键字中减去。如果要在 OxPHP 发布版本移除它之前就把它去掉,请不要使用关键字,而是以 CIDR 形式列出你仍然信任的地址段。这样也就放弃了读取 CF-Connecting-IP,因为它与关键字绑定。

位于 CDN 和平台负载均衡器之后

一种常见的部署方式是在托管平台的负载均衡器前面放一个 CDN:

text
client → Cloudflare → load balancer → OxPHP

使用 TRUSTED_PROXIES=private 时,只有负载均衡器是可信的。于是 X-Forwarded-For 中最右侧的非可信地址就是连接到负载均衡器的那个 Cloudflare 边缘节点,每个请求看起来都来自 Cloudflare —— 无论是在 REMOTE_ADDR、访问日志、限流中,还是在 client.address span 属性中。请同时信任这两跳:

bash
TRUSTED_PROXIES="private,cloudflare"

如果负载均衡器从 private 地址段之外的地址发起连接,也请列出它的地址段:TRUSTED_PROXIES="private,100.64.0.0/10,cloudflare"。

使用 cloudflare 关键字时,OxPHP 分三步解析客户端地址:

  1. 1
    越过你自己的代理。 OxPHP 从连接对端开始,沿转发链(存在 Forwarded 时用它,否则用 X-Forwarded-For)向左遍历,越过你自己的代理:private 地址段以及你列出的 CIDR。位于 Cloudflare 地址段内的地址即使被你列出,也算作 Cloudflare。
  2. 2
    寻找 Cloudflare 跳。 如果第一个不属于你自己代理的跳是 Cloudflare 地址,说明请求经由 Cloudflare 而来,客户端地址就是 CF-Connecting-IP 请求头。该请求头必须恰好出现一次,并且包含一个有效的 IP 地址。
  3. 3
    否则,采用最右侧非可信规则。 如果请求并非经由 Cloudflare 而来,或者 CF-Connecting-IP 缺失、重复或格式错误,则适用最右侧非可信规则,此时 Cloudflare 地址段被视为可信。

遍历停下的那一跳,要么是连接对端,要么是你自己的某个代理记录下的地址。因此,直接访问负载均衡器的客户端无法让 OxPHP 读取它自己发送的 CF-Connecting-IP:负载均衡器会在那里记录客户端自己的地址,而客户端写在更左侧的 Cloudflare 地址永远不会被遍历到。与最右侧非可信规则一样,这依赖于你的代理正确维护 OxPHP 所读取的请求头 —— 追加连接到它们的地址,而不是把客户端的值原样透传。

移除客户端发送的 Forwarded 请求头

你的代理也必须维护 Forwarded。OxPHP 会优先于 X-Forwarded-For 读取它,而 Cloudflare 会按访问者发送的原样转发它,因为它不属于 Cloudflare 设置的请求头。因此,只维护 X-Forwarded-For 的负载均衡器必须从请求中移除 Forwarded。如果它把该请求头透传过来,任何客户端(无论是否经由 Cloudflare)都能决定 OxPHP 看到的地址,不管是否使用了 cloudflare 关键字。

当遍历在到达 Cloudflare 地址之前就停止时 —— 停在没有地址或无法解析的条目上,或停在 OxPHP 无法作为文本读取的行上,如客户端 IP 提取中所述 —— 同样不会读取 CF-Connecting-IP。你的某个代理单独写在一行上的记录,不受客户端在它之前发送的不可读行影响。代理写在与客户端发送的文本同一行上的记录,则会随那一行一起丢失:遍历在那里停止,地址来自你的代理在那一行右侧记录的内容,或者保持为连接对端 —— 绝不会来自客户端发送的任何内容。

Cloudflare Workers

Cloudflare 会把连接到它的一方的地址追加到 X-Forwarded-For,因此对于普通访问者,这两个请求头指向同一个客户端。对于 Cloudflare Workers 发往你源站的请求,二者可能不同;这些请求同样从 Cloudflare 的地址发出,无论该 Worker 属于哪个账户。Cloudflare 的 HTTP 请求头参考没有说明这类请求的 X-Forwarded-For 中可能包含什么。对于 CF-Connecting-IP,它有说明:Worker 只能为发往其自身 zone 中主机名的请求选择该值;对于另一个 Cloudflare zone 中的主机名,该请求头是固定地址 2a06:98c0:3600::103;对于不在 Cloudflare 上的主机名,它是客户端的地址,Worker 无法更改。

信任 Cloudflare 地址段仍然意味着信任所有离开 Cloudflare 网络的流量,而不仅仅是你自己的 zone。如果客户端地址会影响安全决策,请通过你的平台提供的任何手段,让源站只接受来自你自己 zone 的流量。

安全性

  • 安全的默认行为 —— 未设置 TRUSTED_PROXIES 时,不处理任何转发请求头
  • CIDR 校验 —— TRUSTED_PROXIES 中的无效值会导致启动错误
  • 抗欺骗能力 —— 最右侧非可信算法会忽略攻击者在前面追加的值
  • 来自非可信 IP 的请求,其转发请求头会被完全忽略

给你的智能体的提示词

这些提示词假定你的智能体已连接 OxPHP 文档 MCP 服务器——需要哪些页面它会自己去读。

prompt
为我的 OxPHP 部署配置可信代理。通过 OxPHP 文档 MCP 服务器读取 `security/trusted-proxies` 页面。 从我的基础设施文件里判断挡在应用前面的是什么,然后给我确切的配置,并告诉我每个值会放行什么,以及一旦我配错了,它会让客户端伪造出什么。

另请参阅

  • 限流 —— 按 IP 限流使用解析出的客户端 IP
  • 访问日志 —— remote_ip 字段显示解析出的客户端 IP
  • 配置参考 —— 所有环境变量
发现错误?报告问题 →