c.ClientIP()在线上环境几乎必然返回内网地址,因其默认仅信任127.0.0.1,而生产请求必经Nginx/CDN等代理,导致退化为c.Request.RemoteAddr(如10.0.1.5);若未调用SetTrustedProxies,攻击者可伪造X-Forwarded-For绕过校验。

为什么 c.ClientIP() 在线上环境几乎必然返回内网地址
因为 c.ClientIP() 默认只信任 127.0.0.1,而生产环境几乎都经过 Nginx、CDN 或 Kubernetes Service 转发。一旦请求穿过代理,c.ClientIP() 就会退回到 c.Request.RemoteAddr —— 这通常是上一跳代理的内网 IP(比如 10.0.1.5 或 127.0.0.1),不是用户真实出口 IP。
更危险的是:如果你没调用 engine.SetTrustedProxies(),Gin 会把所有 X-Forwarded-For 头都当真,攻击者发个 X-Forwarded-For: 1.1.1.1 就能绕过所有基于 IP 的校验逻辑。
- 别在没配置信任代理前就用
c.ClientIP()做鉴权或限流 - 别把
c.Request.RemoteAddr直接当真实 IP 解析(它带端口、含 IPv6 方括号、可能为 localhost) - 别在日志里直接打
c.ClientIP()而不验证其来源是否可信
如何正确配置 SetTrustedProxies 并匹配你的部署结构
信任代理列表不是“写几个 IP 就行”,必须和你实际流量路径严格对齐。写错网段或遗漏层级,就会漏掉真实 IP 或引入伪造风险。
- 单层 Nginx 直连 Go:信任 Nginx 所在网段,例如
r.SetTrustedProxies([]string{"192.168.10.0/24"}) - CDN → Nginx → Go:需同时信任 CDN 回源段 + Nginx 内网段。阿里云 SLB 是
100.64.0.0/10,腾讯云 CLB 是100.64.0.0/11,不能笼统写成/16 - Kubernetes NodePort + externalTrafficPolicy: Local:此时请求可能来自节点本机,要加节点 CIDR,比如
"10.0.0.0/8" - 绝对不要传空切片或
nil,那等价于信任所有 IP,形同裸奔
配置后,c.ClientIP() 才会真正解析 X-Forwarded-For 并跳过私有地址;否则它仍只认 127.0.0.1。
从 X-Forwarded-For 提取时为什么必须手动校验 IP 合法性
Gin 的 c.ClientIP() 会取 X-Forwarded-For 最左 IP,但它不做 net.ParseIP() 和 ip.IsGlobalUnicast() 判断。如果上游代理写了个非法值(如 X-Forwarded-For: 127.0.0.1, 1.2.3.4),而你又没校验,就会把 127.0.0.1 当作真实 IP 处理。
- 永远用
net.ParseIP()解析后再判断,别直接字符串比较 - IPv6 地址可能带方括号:
[2001:db8::1],需先strings.Trim(ipStr, "[]") - 跳过所有私有地址:
127.0.0.0/8、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、::1/128等 - 多层代理下,
X-Forwarded-For可能被追加多次,只取第一个全局单播地址即可
健康探针 / 内部调用路径必须豁免 IP 校验
像 /healthz、/metrics、服务间 gRPC HTTP 转发这类路径,如果强制走 IP 黑白名单中间件,会导致探针失败、链路中断甚至雪崩。它们不该参与客户端 IP 校验。
- 在中间件中用
c.Request.URL.Path显式跳过豁免路径,而不是靠 try-catch 或延迟校验 - 别把 IP 校验逻辑塞进全局中间件——至少对
OPTIONS、GET /favicon.ico等也应放行 - 如果用了
atomic.Value热更新黑白名单,确保豁免逻辑在规则加载前就生效,避免热更瞬间误拦
最易被忽略的一点:Kubernetes liveness/readiness probe 默认用 localhost 发起,若你没豁免 /healthz,probe 就永远失败,Pod 持续重启。


















