访客真实 IP 在 CF-Connecting-IP 请求头里,让 Web 服务器读这个头即可。但必须先限定只信任来自 Cloudflare IP 段的连接,否则任何人都能伪造它。
把域名切成橙云之后,第一个被打乱的往往不是网站本身,而是那些依赖访客地址的功能:登录风控开始误判、评论限频形同虚设、后台的封 IP 按钮点了没反应、统计里所有人都来自美国。翻开访问日志一看,来源地址翻来覆去就那么十几个网段。
这不是配错了,是反向代理的必然结果。下面按「为什么会这样 — 真实地址在哪 — 怎么取 — 取之前要先做什么」的顺序过一遍,最后那一步最常被跳过,也最容易出事。
源站看到的是 Cloudflare,这是对的
代理状态 打开之后,访客的 DNS 查询拿到的是 Cloudflare 的地址,TCP 连接也建立在离访客最近的 Cloudflare 节点上。节点处理完缓存、WAF 这些活儿,再由它自己向你的源站发起一条新的连接。对源站的操作系统来说,这条连接的对端就是那个节点,没有别的可能。
所以一切从连接层取地址的代码都会失真:Nginx 的 $remote_addr、PHP 的 $_SERVER['REMOTE_ADDR']、各种框架里的 request.remote_ip,拿到的都是 Cloudflare 的 IP。这些地址来自一份公开的清单,目前是 15 个 IPv4 网段加 7 个 IPv6 网段,全体 Cloudflare 用户共用,列在 Cloudflare IP 地址页。
要强调的是:失真只发生在源站这一侧。Cloudflare 自己那一层看到的一直是访客的真实地址——WAF 规则里的 ip.src、安全事件列表、流量分析,都不需要任何额外配置。
真实地址在哪个头里
Cloudflare 在转发时会往请求上加几个头,各有各的用途。下面这张表按「你到底该读哪个」排序,细节以官方的 HTTP 头文档 为准。
| 请求头 | 装的是什么 | 该不该用 |
|---|---|---|
CF-Connecting-IP | 访客连到 Cloudflare 时用的地址,永远是单个 IP | 首选。所有套餐都有,格式恒定 |
X-Forwarded-For | 访客地址加上沿途每一跳代理的地址,逗号分隔 | 能用但别用,见下一节 |
True-Client-IP | 内容与 CF-Connecting-IP 完全相同,只是换了个名字 | 企业版专属,且要在托管转换里开启 |
CF-Connecting-IPv6 | 开启伪 IPv4 的覆盖模式时,访客原本的 IPv6 地址 | 只在那一种配置下出现 |
CF-IPCountry | 访客所在国家或地区的两位代码 | 做地域判断时用它,别自己反查 IP 库 |
如果源站压根收不到 CF-Connecting-IP,先去检查 托管转换 里的「移除访客 IP 头」有没有被打开——这个开关会一并去掉 cf-connecting-ip、true-client-ip 和 x-forwarded-for 里的访客地址;其次检查有没有哪条转换规则把它改掉了。
X-Forwarded-For 为什么不建议直接用
当请求在到达 Cloudflare 之前没有经过其它代理时,X-Forwarded-For 的值确实和 CF-Connecting-IP 一模一样,很多人就是这么用过来的,也一直没出事。问题出在有中间代理的时候: Cloudflare 会把它收到的那一跳地址追加到已有的列表后面,于是这个头变成一串地址。取第一个,那一段完全由客户端提供,可以随便填;取最后一个,取到的是代理而不是访客。
官方文档在这一点上说得很直白:日志和应用应当去读 CF-Connecting-IP 或 True-Client-IP,因为它们的格式一致、只含一个地址。
IPv6 访客与伪 IPv4
Cloudflare 对所有域名默认支持 IPv6,这意味着 CF-Connecting-IP 里可能出现一个 IPv6 地址。老一些的风控和统计程序只认得点分十进制,遇到冒号就崩,这时可以用 伪 IPv4:它把 IPv6 地址哈希成一个 E 类 IPv4 地址(形如 240.16.0.1),有三档可选——关闭、加一个 CF-Pseudo-IPv4 头、或者直接覆盖 CF-Connecting-IP 与 X-Forwarded-For,并把真正的 IPv6 地址挪到 CF-Connecting-IPv6 里。
选覆盖模式的话源站一行代码都不用改,代价是日志里记下的不再是真实地址,而是一个只在你这套系统里有意义的映射值。
配置:把真实地址写回日志
思路对所有 Web 服务器都一样:告诉它「这些来源是可信的代理」,再告诉它「去哪个头里取真实地址」。以 Nginx 为例:
# 逐条列出 Cloudflare 的 IP 段(这里只截取开头几条)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 2400:cb00::/32;
# ……其余网段照抄 cloudflare.com/ips
real_ip_header CF-Connecting-IP;重载之后 $remote_addr 与访问日志就换回访客地址了。想同时保留原始头做对账,可以在 log_format 里加上 $http_cf_connecting_ip 和 $http_x_forwarded_for。模块本身的参数说明见 ngx_http_realip_module 文档。
其它常见环境的对应做法:
| 环境 | 怎么做 |
|---|---|
| Apache 2.4 | 启用 mod_remoteip,配 RemoteIPHeader CF-Connecting-IP 与逐条的 RemoteIPTrustedProxy,再把日志格式里的 %h 换成 %a |
| Caddy | 在 reverse_proxy 里用 header_up 把 X-Forwarded-For 覆盖成 CF-Connecting-IP 的值,并只放行 Cloudflare 的来源 |
| HAProxy | 关掉 option forwardfor,用一条 acl 匹配 Cloudflare 来源后再重写 X-Forwarded-For |
| LiteSpeed | 后台勾上「Use Client IP in Header」,日志与 PHP 变量会一起恢复 |
| IIS 8.5 及以上 | 在日志里加一个 CF-Connecting-IP 自定义字段。注意它只影响日志,不会恢复应用层拿到的地址 |
另外,Apache 的 mod_cloudflare 早已停止维护,从 Debian 9 与 Ubuntu 18.04 起官方就不再更新它,改推 mod_remoteip;网上那些老教程仍在教装它,照做只会白折腾。完整清单见官方的 恢复访客原始 IP 一页。
最关键的一步:先划出信任边界
上面每一段配置里都有「可信来源」这半边,很多教程会把它一笔带过,甚至写成 set_real_ip_from 0.0.0.0/0 图个省事。这一步偷懒的后果比不恢复真实 IP 严重得多。
道理很简单:HTTP 头是请求方随手就能写的东西。如果服务器对任何来源的连接都采信 CF-Connecting-IP,那么任何一个知道你源站地址的人,只要在请求里自己带上这个头,就能把自己伪装成任意地址——你按 IP 做的封禁、限流、白名单、审计日志,全部一起作废。Cloudflare 的文档在讲 Caddy 配置时特意点了这一条:不限定来源,头就是可以被伪造的。
所以恢复真实 IP 这件事,要和下面两件一起做:
- 把可信代理列表限定为 Cloudflare 的网段,并且逐条列全。清单在 cloudflare.com/ips;想写脚本定期同步,用
GET /client/v4/ips这个接口,它不需要鉴权,返回里带etag方便判断有没有变。脚本要处理好取不到的情况,别在请求失败时把列表清空。 - 在防火墙上只放行 Cloudflare 的网段,让直连源站的请求根本到不了 Web 服务器。官方也是这么建议的,具体的 iptables 写法在 Cloudflare IP 地址 这一页。
想更进一步,可以开 Authenticated Origin Pulls,让源站校验 Cloudflare 出示的客户端证书。它在所有套餐上都能用,但有两个前提要知道:全局模式用的是所有账号共享的证书,只能证明「来自 Cloudflare 网络」而不是「来自你的账号」,要更强的保证得自己上传证书;另外它和 Tunnel 不兼容,因为隧道场景下源站根本没有对外监听的端口。
国内环境里额外要注意的几件事
走中国网络的话,回源不来自那 15 个网段
这一条会让不少人排查半天。 Cloudflare 中国网络 的境内节点由京东云运营,它们回源时用的是京东云的地址,不在公开那份列表里。只按那 15 个网段配白名单,境内节点的回源会被你自己的防火墙全部挡掉,恢复真实 IP 也一并失效。这些网段要用 GET /client/v4/ips?networks=jdcloud 单独取,返回字段是 jdcloud_cidrs,数量比全球那份多得多。
用得上这条的人不多:中国网络是企业版的单独订阅项,且要求每个顶级域都已完成 ICP 备案。免费和 Pro 方案的大陆访客仍走境外节点,回源地址还是那 15 个网段。
面板环境常见的双层代理
国内主机上装的一体化面板,很多是 Nginx 在前、Apache 或 PHP-FPM 在后。这种结构下真实 IP 要在最外层那一跳恢复:Nginx 配 set_real_ip_from 认 Cloudflare,后端再信任来自 Nginx 的本地地址。顺序反了,后端日志里就会整齐地记满 127.0.0.1,看上去像是恢复失败,其实是恢复了两次。
地域判断别再对着连接地址反查
国内业务常用 IP 库判断省份、做分流或者限制注册。代理开启后这类逻辑必须改成读 CF-Connecting-IP,否则查到的全是 Cloudflare 节点,结论一律是境外。只要国家或地区代码就够的话,更省事的是直接读 CF-IPCountry——开启 IP 地理位置 后 Cloudflare 就会带上它,需要城市、经纬度等更细的字段则要在 托管转换 里打开「添加访客位置头」。
留存的日志得记得住人
境内运营的站点普遍有网络日志留存的要求。一份把所有访客都记成同十几个地址的访问日志,事后追溯时基本没有价值。恢复真实 IP 在国内因此多了一层现实理由。
怎么确认配好了
最直接的办法:从一台你知道出口地址的机器上访问一次网站,再去看服务器的访问日志,第一列应该是那台机器的地址,不是 104.16 开头的。
信任边界那一半要单独验。如果源站还能被直连(比如你只做了头的配置、没做防火墙),试着直接对源站地址发一个带伪造 CF-Connecting-IP 的请求,看日志里记下的是什么。记成了你伪造的那个值,说明可信来源列表写得太宽,等于把封禁开关交给了对方。理想结果是这个请求压根连不上。
另外有个容易踩的边角:如果站点前面还挂着 Worker,同区域的子请求里 CF-Connecting-IP 的取值会跟随 x-real-ip,而后者是脚本里可以改的;跨区域的子请求则会被替换成一个固定地址。
常见问题
Cloudflare 怎么获取访客真实 IP?
代理开着的时候,Cloudflare 会在转发给源站的请求上加一个 CF-Connecting-IP 头,值就是访客连到 Cloudflare 时用的那个地址,格式固定为单个 IP。源站这一侧要做的是让 Web 服务器读这个头,而不是读 TCP 连接的来源地址:Nginx 用 ngx_http_realip_module,Apache 2.4 用 mod_remoteip。
Nginx 后面接 Cloudflare,日志里全是 Cloudflare 的 IP 怎么办?
在 http 或 server 段里用 set_real_ip_from 把 Cloudflare 的全部 IP 段列出来,再加一行 real_ip_header CF-Connecting-IP,重载之后 $remote_addr 与访问日志就会变回访客地址。注意 set_real_ip_from 必须逐条列全,漏掉的段仍然会记成 Cloudflare 的 IP;也别图省事写成 0.0.0.0/0,那等于允许任何人伪造自己的地址。
CF-Connecting-IP 会被伪造吗?
会,只要源站还能被直接访问。HTTP 头是请求方随手就能写的东西,如果你的服务器对任何来源的连接都信任 CF-Connecting-IP,那么知道源站地址的人只要自己带上这个头,就能把自己伪装成任意 IP,绕过按 IP 做的封禁与限流。所以真实 IP 的恢复必须和信任边界一起做:只对来自 Cloudflare IP 段的连接采信这些头,其余的直接拒绝。
X-Forwarded-For 和 CF-Connecting-IP 用哪个?
用 CF-Connecting-IP。当请求到达 Cloudflare 之前没有经过其它代理时,两者的值确实一样;但只要访客那一侧还有别的代理或 CDN,X-Forwarded-For 就会变成一串用逗号分隔的地址列表,取第一个可能取到伪造值,取最后一个又会取到代理的地址。官方文档也明确建议日志和应用去读 CF-Connecting-IP。
为什么开了 Cloudflare 之后,网站的 IP 封禁和限流都失效了?
因为源站看到的来源地址已经全部变成 Cloudflare 的那十几个网段,你的封禁规则要么命中不了任何人,要么一命中就把所有访客一起挡在门外。恢复真实 IP 之后这些功能才会重新有意义。更省事的做法是把这类规则搬到 Cloudflare 那一侧去做:WAF 规则里的 ip.src 本来就是访客的真实地址,不需要任何额外配置。