三层代理下的真实 IP 穿透战:PVE + Nginx 反代 + Cloudflare 的完整解决方案
问题背景
服务器架构是这样的:
用户 → Cloudflare CDN → 宿主机 Nginx 反代 → 虚拟机 :2020宿主机是 PVE,桥接 IP 192.168.1.1,虚拟机 IP 192.168.1.8,网站跑在虚拟机上。宿主机上配了 Nginx 反向代理到虚拟机的 2020 端口。
问题:网站访问日志里记录的 IP 全是 192.168.1.1(宿主机网关),没有真实访客 IP。
第一轮排查:为什么全是网关 IP?
宿主机 iptables 配置
宿主机上有一条 MASQUERADE 规则:
post-up iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o vmbr0 -j MASQUERADE这条规则的作用是让虚拟机通过宿主机上网(共享上网)。但它通常不会影响入站流量的源 IP,因为入站流量是外部主动发起的,conntrack 会记录原始方向,回复包自动取消 SNAT。
所以在 PVE 网桥模式下,理论上外部流量直接到达虚拟机,应该能看到真实 IP。但用户看到的是全是网关 IP,说明流量经过了宿主机的 Nginx 反向代理。
确认:宿主机上有 Nginx 反代
配置文件:
location ^~ /
{
proxy_pass http://192.168.1.8:2020;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
...
}问题清楚了:宿主机 Nginx 作为反向代理,后端服务器(虚拟机)看到的连接来源就是宿主机自己(192.168.1.1)。但是宿主机 Nginx 已经设置了 X-Real-IP 和 X-Forwarded-For 头部传递真实 IP,所以虚拟机只需要配置从这些头部读取即可。
第二轮:虚拟机 Nginx 配置 realip 模块
在虚拟机的 Nginx 中添加:
set_real_ip_from 192.168.1.1; # 信任宿主机
real_ip_header X-Real-IP; # 从 X-Real-IP 提取配置后,日志显示公网 IP 了!但都是 Cloudflare 的 IP,不是真实访客 IP。
第三轮:Cloudflare 的 CF-Connecting-IP
因为网站套了 Cloudflare CDN,所有流量都经过 Cloudflare 节点。Cloudflare 会把真实 IP 放在 CF-Connecting-IP 请求头中。
于是把虚拟机 Nginx 配置改为:
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
# ... 所有 Cloudflare IP 段
real_ip_header CF-Connecting-IP;
real_ip_recursive on;结果:所有 IP 又变回 192.168.1.1 了。
倒退原因分析
请求链路:
用户 → Cloudflare → 宿主机 Nginx → 虚拟机 Nginx当请求到达虚拟机时,来源 IP 是 宿主机 192.168.1.1。但虚拟机的 set_real_ip_from 只列出了 Cloudflare IP 段,没有包含宿主机 IP。
Nginx realip 模块的规则是:只有当请求来源 IP 在信任列表中时,才会从指定头部提取真实 IP。 宿主机 IP 不在信任列表,所以 Nginx 直接使用 $remote_addr(即 192.168.1.1),CF-Connecting-IP 头被无视了。
另外还有一个问题:宿主机 Nginx 并没有把 CF-Connecting-IP 头转发给虚拟机。宿主机配置只传递了 X-Real-IP 和 X-Forwarded-For,没有传递 CF-Connecting-IP。所以虚拟机根本收不到这个头。
第四轮:最终解决方案
需要同时修改宿主机和虚拟机的 Nginx 配置。
宿主机 Nginx:透传 CF-Connecting-IP
location ^~ /
{
proxy_pass http://192.168.1.8:2020;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip; # 新增:透传真实 IP
proxy_set_header REMOTE-HOST $remote_addr;
# ... 其他配置不变
}$http_cf_connecting_ip 是 Nginx 变量,对应请求头中的 CF-Connecting-IP 字段,由 Cloudflare 自动添加。
虚拟机 Nginx:信任宿主机 + Cloudflare IP
# 信任直接连接的宿主机
set_real_ip_from 192.168.1.1;
# 信任所有 Cloudflare IP 段
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
# 从 CF 的专用头提取真实 IP
real_ip_header CF-Connecting-IP;
# 开启递归,自动去除已知代理 IP
real_ip_recursive on;关键点:
- 必须包含 192.168.1.1,否则不信任来自宿主机的头部
- 同时保留 Cloudflare IP 段,形成完整的信任链
real_ip_recursive on安全保留,它会自动去除所有信任的代理 IP
最终验证
修改后,从外部(如手机 4G/5G)访问网站,虚拟机 Nginx 日志显示的是真实访客公网 IP,不再是网关 IP 或 Cloudflare IP。
架构总结
用户 1.2.3.4
↓ CF-Connecting-IP: 1.2.3.4
Cloudflare CDN
↓ CF-Connecting-IP: 1.2.3.4
宿主机 Nginx (反代)
↓ 透传 CF-Connecting-IP 到后端
虚拟机 Nginx
↓ set_real_ip_from + real_ip_header
访问日志: 真实 IP ✅踩坑记录
| 坑 | 表现 | 原因 | 解决 |
|---|---|---|---|
| 全网关 IP | 日志都是 192.168.1.1 | 宿主机反代,未配置 realip | 虚拟机加 realip 配置 |
| 全 CF IP | 日志都是 Cloudflare 节点 IP | 从 X-Real-IP 提取,但那是 CF 的 IP | 改用 CF-Connecting-IP |
| 又变回网关 IP | 改 CF 配置后回到 192.168.1.1 | 信任列表没包含宿主机 + 宿主机没透传头 | 宿主机透传 + 虚拟机信任宿主机 |
关键知识点
1. Nginx realip 模块的信任规则
set_real_ip_from 不仅指定从哪个头部提取,更是安全机制——只有来自信任 IP 的请求,才会从指定头部提取真实 IP。非信任 IP 发来的头部会被忽略,直接使用 $remote_addr。
2. 反向代理的头部透传
Nginx proxy_pass 默认不会透传所有请求头,需要用 proxy_set_header 显式传递需要的头。常见需要透传的头:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;对于 Cloudflare 特有的头(CF-Connecting-IP、CF-Ray、CF-IPCountry 等),也需要显式透传。
3. Cloudflare IP 段需要定期更新
Cloudflare 的 IP 范围会变化,建议:
- 每 1-3 个月检查更新
- 或写 cron 脚本自动从
https://www.cloudflare.com/ips-v4和https://www.cloudflare.com/ips-v6下载
# 自动更新脚本示例
curl -s https://www.cloudflare.com/ips-v4 -o /etc/nginx/cloudflare-ipv4.txt
curl -s https://www.cloudflare.com/ips-v6 -o /etc/nginx/cloudflare-ipv6.txt然后在 Nginx 中 include 这些文件:
include /etc/nginx/cloudflare-ipv4.txt;
include /etc/nginx/cloudflare-ipv6.txt;一句话总结
三层代理下获取真实 IP 的黄金法则:每一层代理都要透传真实 IP 头,最后一层要信任所有上游代理的 IP。 少一个环节,IP 就断一截。