bigywater.tech
微服务 · 26

请求头的改写:X-Forwarded-For、Host 与真实 IP

X-Forwarded-For 该追加还是覆盖?多层网关怎么还原真实 IP,信任范围放多大?Host 头用 $host 还是 $http_host?这篇用两个环回地址(127.0.0.1 与 ::1)模拟不同来源,把它们逐项量出来。

6.6.6.6
默认配置下,client 伪造的 X-Forwarded-For 原样到了后端;用 $remote_addr 覆盖后才是 127.0.0.1
::1 对 6.6.6.6
L2 只信 L1 的地址与信任范围包含 client 时,还原出的真实 IP(client 真实来源是 ::1,伪造值是 6.6.6.6)
u / shop.test / Shop.Test:8443
Host: Shop.Test:8443 的请求,默认、$host、$http_host 下后端看到的 Host

X-Forwarded-For

实测 · 本机执行输出
  写法                             | client 伪造了 XFF: 6.6.6.6 时后端看到 | client 没带 XFF 时后端看到
  -------------------------------+-------------------------------+--------------------
  不写 proxy_set_header(默认)        | 6.6.6.6                       | (没有)               
  $proxy_add_x_forwarded_for(追加) | 6.6.6.6, 127.0.0.1            | 127.0.0.1          
  $remote_addr(覆盖)               | 127.0.0.1                     | 127.0.0.1          
  置空(不转发)                        | (没有)                          | (没有)               
  默认:client 发什么就原样转给后端(伪造的 6.6.6.6 照单全收);$proxy_add_x_forwarded_for 在 client 带的值后面追加 client 的地址,最左边仍然是伪造的值;直接面对公网的最外层网关应该用 $remote_addr 覆盖,不要相信 client 自己带的 XFF

默认原样转发 client 带的值;追加保留伪造值在最左边;最外层网关应该覆盖。

realip 的信任

实测 · 本机执行输出
  L2 信任谁                | client(来源 ::1)走哪条路 | L2 还原出来的“真实 IP” | L2 收到的 X-Forwarded-For
  ----------------------+--------------------+-----------------+-----------------------
  只信 127.0.0.1(L1)      | 经过 L1              | ::1             | 6.6.6.6, ::1          
                        | 直连 L2              | ::1             | 6.6.6.6               
  信 127.0.0.1 和 ::1     | 经过 L1              | 6.6.6.6         | 6.6.6.6, ::1          
                        | 直连 L2              | 6.6.6.6         | 6.6.6.6               
  信所有人 0.0.0.0/0 与 ::/0 | 经过 L1              | 6.6.6.6         | 6.6.6.6, ::1          
                        | 直连 L2              | 6.6.6.6         | 6.6.6.6               
  “真实 IP”应该是 ::1(client 的真实来源)。信任范围太大时,直连 L2 的 client 只要自己带一个 XFF,L2 就把它当成真实 IP(冒充成功);只信 L1 时,不是 L1 发来的连接,XFF 被忽略,真实 IP 就是连接的对端地址;经过 L1 时,real_ip_recursive on 从右往左跳过受信任的地址,取到第一个不受信任的(L1 追加的 client 地址)

只信上一跳网关的地址,client 伪造的 XFF 才会被忽略;信任范围包含 client 就会被冒充。

Host

实测 · 本机执行输出
  写法                     | Host: Shop.Test:8443 | Host: shop.test  | HTTP/1.0,没有 Host
  -----------------------+----------------------+------------------+-----------------
  (a) 默认                 | 'u'                  | 'u'              | 'u'             
  (b) $host              | 'shop.test'          | 'shop.test'      | 'shop.test'     
  (c) $http_host         | 'Shop.Test:8443'     | 'shop.test'      | 'u'             
  (d) $host:$server_port | 'shop.test:9165'     | 'shop.test:9165' | 'shop.test:9165'
  $host 是小写、去掉端口的主机名,没带 Host 时取 server_name;$http_host 是 client 原样发来的(含端口、大小写),没带 Host 时它是空值,而 proxy_set_header 对空值的头不会发送,于是后端看到的是默认的 u;$host:$server_port 里的端口是 nginx 自己监听的端口;后端要拼绝对 URL 或做虚拟主机路由,要选对。nginx 自己按 Host 选 server 块时大小写不敏感

$host、$http_host 和默认值给后端的 Host 不一样,没带 Host 的请求尤其不同。

分步演示

请求头的改写
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。nginx 是 Homebrew 的 1.31.6;本机只有 127.0.0.1 和 ::1 两个环回地址,“不同来源”用这两个地址区分(L1 到 L2 用 127.0.0.1,client 用 ::1);后端是本进程里回显请求头的 Python 服务;Host 实验里 server_name 是 shop.test,端口是 nginx 随机选的
    • 没有验证:真实公网 IP 与 CDN 的 X-Forwarded-For 链;Forwarded 头(RFC 7239);PROXY protocol;X-Forwarded-Proto / X-Forwarded-Port 的写法;realip 与 limit_req 等模块的先后关系(realip 在 post-read 阶段改 $remote_addr)
    京ICP备2026067502号-1京公网安备11010502063487号