三层代理下的真实 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-IPX-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-IPX-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-IPCF-RayCF-IPCountry 等),也需要显式透传。

3. Cloudflare IP 段需要定期更新

Cloudflare 的 IP 范围会变化,建议:

  • 每 1-3 个月检查更新
  • 或写 cron 脚本自动从 https://www.cloudflare.com/ips-v4https://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 就断一截。

Last modification:May 22nd, 2026 at 01:50 am
如果觉得我的文章对你有用,请随意赞赏