机柜里成排的光纤配线架与黄色光纤跳线
运维 5 分钟

Nginx 反代后拿不到真实 IP

访问日志里所有访客都变成了 127.0.0.1。要让真实 IP 走完这一路,代理层和应用层各有一件事必须做。

给博客加了一个访问统计的小脚本,跑了两天,发现一件怪事:所有访客的 IP 都是 127.0.0.1。

不是「大部分是」,是「全部都是」。连我自己从家里访问,记录里也是本机地址。这说明拿到的根本不是访客的 IP,而是某一层代理自己的地址。

一、先确认在哪一层丢的

这套结构很常见:Nginx 监听 443 做反向代理,把请求转发给本机 127.0.0.1:2368 上的后端程序。我在两个位置分别打日志:

Nginx 访问日志        →  正常,是真实公网 IP
后端拿到的 remote_addr →  127.0.0.1

既然 Nginx 自己是知道真实 IP 的,那问题就只在「它转发的时有没有告诉后端」。

二、根因:默认配置不会透传客户端地址

一开始我下意识地认为反代会自动带上原始 IP,其实不会。TCP 是点对点的:Nginx 和后端之间新开了一条连接,在这条连接里,客户端就是 Nginx 自己。

如果不显式加请求头,后端看到的源地址只能是 127.0.0.1。真实 IP 想传过去,只能靠在 HTTP 头里捎带,而这件事 Nginx 默认不做。

访客 1.2.3.4  ──►  Nginx  ──►  127.0.0.1:2368
                     │
                     └─ 不加头,后端只知道「Nginx 找我」

三、加上两个标准头

在 location 里补两行,这是最通行的做法:

location / {
    proxy_pass http://127.0.0.1:2368;

    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;
}

这两个头的作用不太一样,值得分清楚:

  • X-Real-IP 只放一个地址,就是直连 Nginx 的那个客户端的 IP。简单直接,但前面要是还套了一层 CDN,它就是 CDN 的地址。
  • X-Forwarded-For 是一个逗号分隔的列表,每一层代理都会往后追加自己的上游地址,所以它是「一路走过来的完整链条」。

$proxy_add_x_forwarded_for 这个变量的含义是:如果请求里已经有 X-Forwarded-For,就在它后面接上当前直连方的地址;如果没有,就直接写当前地址。所以链条是自然累加出来的:

X-Forwarded-For: 1.2.3.4, 10.0.0.7, 127.0.0.1
                 ↑访客    ↑CDN     ↑本机 Nginx 追加的

四、加了头,后端还是拿不到

改完配置重载 Nginx,日志里还是 127.0.0.1。这一步卡了最久,因为头明明已经发过去了。

用 curl 从外部请求一次,看后端收到的原始头:

curl -H 'X-Forwarded-For: 9.9.9.9' http://127.0.0.1:2368/debug

X-Forwarded-For: 9.9.9.9      ← 头确实到了
remote_addr:     127.0.0.1    ← 但变量没变

原因是:HTTP 头只是数据,程序不会自动去读它。绝大多数框架出于安全考虑,默认不信任任何客户端发来的转发头——否则任何人加一个头就能把自己伪装成别的 IP。

所以还有第二步,得在后端侧显式开启信任。Node / Express 里是一行:

app.set('trust proxy', true);

开了之后,框架才会在计算 req.ip 时去读 X-Forwarded-For。其他语言/框架也有对应的开关,只是叫法不同(比如 Flask 的 ProxyFix、Django 的 USE_X_FORWARDED_HOST 系列配置)。

五、别忘了它本身可以被伪造

这一步做完 IP 就正常了,但顺手补上安全上的收尾。

X-Forwarded-For 是一个普通的请求头,客户端可以自己伪造。如果你直接把「链条里第一个地址」当真实 IP,那访客只要发一个 X-Forwarded-For: 1.1.1.1 就能污染你的统计、甚至绕过基于 IP 的限流。

所以只应该信任自己控制的代理转发过来的那些地址。Nginx 侧可以用 realip 模块把「可信来源」和「取值规则」写死:

set_real_ip_from 10.0.0.0/8;      # 只信内网代理
set_real_ip_from 172.16.0.0/12;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;             # 从右往左跳过可信地址,取第一个不可信的

开了之后,Nginx 的 $remote_addr 自己就变成了真实客户端 IP——上面那段 proxy_set_header 也就自动正确了。但注意:只把你自己那几台代理的网段写进去,范围开太大会等于没做防护。

六、小结

这个问题其实横跨了两层,每次只在其中一层改都不够:

  • 代理层负责把头带上(X-Real-IP / X-Forwarded-For),默认配置不会带
  • 应用层负责选择相信这个头,默认不信任;两件事都做完,IP 才会正确
  • X-Forwarded-For 是会被追加的列表,取第一个地址在多级代理下才正确,但它可被伪造
  • 用 set_real_ip_from 把可信网段收窄,是这套方案能不能安全用的前提

以后再看到日志里清一色的 127.0.0.1,基本可以直接往「转发头没带」或「后端没开信任」这两个方向查,前面那些环节都是对的。

评论