避开这些致命坑点:Nginx 四层代理用 proxy_protocol 获取真实 IP 落地实践
在现代网络架构中,为了兼顾性能与弹性,我们经常会在应用前端部署四层(TCP)负载均衡器,然后再透传给后端的 Nginx 或应用服务。
然而,四层代理有一个天然的痛点:在传输层(TCP)完成握手后,后端服务拿到的连接源 IP,变成了四层代理服务器的内网 IP,而不是客户端的真实公网 IP。
为了解决这个问题,HAProxy 开发了 Proxy Protocol(代理协议)并成为了行业标准。它通过在 TCP 握手后、实际应用层数据发送前,插入一个携带客户端真实 IP/Port 的数据块,完美解决了 IP 丢失的问题。
原理听起来很简单,但在实际落地中,如果对它的工作机制理解不够透彻,极易引发生产事故。本文将为你梳理在 Nginx 环境下配置 proxy_protocol 时的四大致命坑点,并给出标准的避坑配置方案。
坑点一:协议握手不匹配,导致服务直接崩溃
这是最常见、最惨烈的事故。
Proxy Protocol 不是一种能够自动协商的协议。它是一个强强制性的协议。
- 如果发送端开启了
proxy_protocol,而接收端没有配置解析:
接收端(比如未配置该协议的 Nginx 或 Go 服务)会把 Proxy Protocol 的头部数据(如PROXY TCP4 1.2.3.4 ...)直接当成应用层协议(如 HTTP 请求方法)来解析。结果就是,Nginx 会报400 Bad Request错误,Go 服务可能会直接报错断开连接。 - 如果发送端没有开启,而接收端配置了
proxy_protocol:
接收端会一直死等那个PROXY协议头,直到超时断开。正常的用户 HTTP 请求发过来,直接被拒之门外。
避坑指南
上线时,必须严格遵守**“先配置接收端,后配置发送端”**的顺序。
- 先在后端 Nginx(接收端)上配置监听
proxy_protocol,此时它能兼容并正确处理带协议头的流量(注意:一旦接收端开启,它在这个端口上就只能接收带协议头的流量了,所以通常需要启用新端口来平滑过渡)。 - 再在前置代理(发送端)开启
proxy_protocol往后端发送。 - 验证无误后,下线旧的无协议端口。
坑点二:健康检查(Health Check)直接报异常
当你在四层负载均衡(如阿里云 CLB、腾讯云 CLB、AWS NLB)上开启了 proxy_protocol,并指向后端的 Nginx 端口时,你可能会发现:用户的正常请求可以访问,但负载均衡器的健康检查却全部变红(异常)。
原因分析
大多数云厂商的四层健康检查,默认只是简单地向后端端口发起一个 TCP 三次握手(SYN-SYN/ACK-ACK),握手成功就认为服务正常,然后直接发送 RST 断开连接。
但是,一旦你的后端 Nginx 端口启用了 proxy_protocol,Nginx 在 TCP 握手后会强制等待接收 PROXY 协议头部。而云厂商的健康检查包里根本没有这个头部,Nginx 等不到就会主动关闭连接,或者记为一次异常。在云负载均衡看来,就是“后端服务响应异常”,从而把该节点剔除。
避坑指南
有两种主流的解决方案:
方案 A:云厂商原生支持(推荐)
在云控制台的监听器设置中,检查是否有“健康检查支持 Proxy Protocol”或“使用 HTTP 健康检查”的选项。如果有,开启它,让健康检查流量也带上协议头或走七层检查。
方案 B:双端口分流
在后端 Nginx 上配置两个监听端口,一个专门处理带 proxy_protocol 的业务流量,另一个作为普通端口专门供健康检查使用。
server {
# 业务流量端口,开启 proxy_protocol
listen 8080 proxy_protocol;
# 健康检查端口,不开启 proxy_protocol
listen 8081;
location /health {
access_log off;
return 200 "OK";
}
location / {
# 业务逻辑
}
}
坑点三:多级代理下的伪造 IP 漏洞(安全隐患)
一旦你在 Nginx 中配置了 real_ip_header proxy_protocol;,Nginx 就会信任协议头里的 IP 并且将其覆盖到 $remote_addr 变量中。
此时,如果你的前置四层代理没有做好限制,或者任何人都可以直接绕过四层代理直接连接你的后端 Nginx 端口,攻击者就可以在客户端连接时,自己伪造一个 PROXY TCP4 8.8.8.8 ... 的协议头发送给你的 Nginx。 Nginx 会毫无防备地认为这个客户端的 IP 就是 8.8.8.8。这在安全审计、防刷、限流场景下是致命的。
避坑指南
必须严格配置 set_real_ip_from,限制只有受信任的代理服务器 IP 才能传递 proxy_protocol 头。
http {
# 启用 real_ip 模块
# 只信任内网负载均衡器的 IP 段(比如 192.168.0.0/16 段内的代理)
set_real_ip_from 192.168.0.0/16;
# 声明从 proxy_protocol 中提取真实 IP
real_ip_header proxy_protocol;
# 级联修正,使 $realip_remote_addr 也能正确表现
real_ip_recursive on;
}
配置后,如果非 192.168.0.0/16 范围内的 IP 直接连接该端口并发送 proxy_protocol 头,Nginx 将不予理睬,直接将其拒之门外或忽略该头部,从而保障了 IP 的不可篡改性。
坑点四:Stream 模块(四层)与 HTTP 模块(七层)配置混淆
Nginx 既可以作为四层代理(使用 stream 模块),也可以作为七层代理(使用 http 模块)。在这两个模块中,proxy_protocol 的配置语法极为相似,但逻辑完全相反,极易混淆。
- 在
stream模块中: 你通常是作为发送端,需要配置proxy_protocol on;。 - 在
http模块中: 你通常是作为接收端,需要在listen指令后加上proxy_protocol参数。
标准配置模板
为了让大家少走弯路,这里提供一套完整的、经过生产验证的标准配置。
1. 前置 Nginx(四层代理,发送端)
修改 nginx.conf 中的 stream 块:
stream {
upstream backend_http {
server 192.168.1.50:8080; # 后端真实 Web 服务器 IP
}
server {
listen 80;
listen 443;
# 关键配置:往下游后端发送时,带上 Proxy Protocol 头
proxy_pass backend_http;
proxy_protocol on;
# 适当调大超时时间,防止因握手等待导致连接断开
proxy_connect_timeout 5s;
}
}
2. 后端 Nginx(七层 Web 服务,接收端)
修改后端服务器的 nginx.conf 中的 http 块:
http {
# 日志格式中引入 $proxy_protocol_addr 方便排查
log_format main '$proxy_protocol_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$realip_remote_addr"';
access_log /var/log/nginx/access.log main;
server {
# 关键配置 1:监听端口后面必须加上 proxy_protocol
listen 8080 proxy_protocol;
# 关键配置 2:设置受信的四层代理 IP(即前置四层代理的内网 IP)
set_real_ip_from 192.168.1.10; # 如果是集群,可以写网段如 192.168.1.0/24
# 关键配置 3:指定从 Proxy Protocol 头部解析出真实客户端 IP
real_ip_header proxy_protocol;
location / {
# 此时 $remote_addr 已经是客户端的真实公网 IP 了
# 如果后面还要传给 Java/Go 等应用,直接照常传入 X-Real-IP 即可
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
root /usr/share/nginx/html;
index index.html;
}
}
}
总结
Proxy Protocol 是解决跨四层代理获取真实 IP 的利器,但其强耦合的特性也带来了极高的部署要求。在落地时,务必牢记:
- 端口隔离:不要让常规流量和
proxy_protocol流量共享同一个后端端口。 - 安全防护:必须用
set_real_ip_from限制受信的代理 IP,防范 IP 伪造。 - 健康检查:为负载均衡器的健康检查单开无协议端口。
- 平滑上线:严格按照“先接收端、后发送端”的节奏发布变更。