1. 背景

请求 IP 作为用户的身份标识属性之一,是一种非常重要的基础数据。在很多场景下,我们会基于客户端请求 IP 去做网络安全攻击防范或访问风险控制。通常我们可以通过 HTTP 协议 Request Headers 中 X-Forwarded-For 头来获取真实 IP。然而通过 X-Forwarded-For 头获取真实 IP 的方式真的可靠么?

2. 概念

X-Forwarded-For 是一个 HTTP 扩展头。HTTP/1.1(RFC 2616)标准中并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP,现在已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。

前段时间石墨文档某 HTTP 服务升级 Gin 框架到 1.7.2 后突然发现一个 『Bug』,升级后服务端无法获正确的客户端 IP,取而代之的是 Kubernetes 集群中 Nginx Ingress IP。于是我们决定从 Gin 获取客户端相应源码来顺藤摸瓜排查一下。

业务方服务之前使用的是 v1.6.3 版本,我们先看看该版本 Context.ClientIP() 方法实现:

// ClientIP 方法可以获取到请求客户端的IP
func (c *Context) ClientIP() string {// 1. ForwardedByClientIP 默认为 true,此处会优先取 X-Forwarded-For 值,// 如果 X-Forwarded-For 为空,则会再尝试取 X-Real-Ipif c.engine.ForwardedByClientIP {clientIP := c.requestHeader("X-Forwarded-For")clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])if clientIP == "" {clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))}if clientIP != "" {return clientIP}}// 2. 如果我们手动配置 ForwardedByClientIP 为 false 且 X-Appengine-Remote-Addr 不为空,则取 X-Appengine-Remote-Addr 作为客户端IPif c.engine.AppEngine {if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {return addr}}// 3. 最终才考虑取对端 IP 兜底if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {return ip
}return ""
}

再看 v1.7.2 版本, Contexnt.ClientIP() 方法实现:

func (c *Context) RemoteIP() (net.IP, bool) {...remoteIP := net.ParseIP(ip) // 获取客户端 IP...// trustedCIDRs 由 engine 启动时配置的 TrustedProxies 数组解析而来,表示可以信任的前置代理 CIDR 列表。只有配置了 engine.TrustedProxies 才有可能解析出正确的可信任 CIDR 列表。// 只有 CIDR 列表不为空,这里才会将 remoteIP 和已配置可信 CIDR 列表进行比对。CIDR 列表中任一 CIDR 包含对端 IP,则将第二个返回值置为 true,表示对端 IP 可信任。if c.engine.trustedCIDRs != nil {for _, cidr := range c.engine.trustedCIDRs {if cidr.Contains(remoteIP) {return remoteIP, true}}}return remoteIP, false
}
func (c *Context) ClientIP() string {// 1. AppEngine 默认为 false,如果应用通过 Google Cloud App Engine 部署,或用户手动设置为 true 且 X-Appengine-Remote-Addr 不为空,则会取 X-Appengine-Remote-Addr 值作为客户端 IP。if c.engine.AppEngine {if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {return addr}}// 2. 否则通过 RemoteIP() 方法判断对端 IP 是否可信,trusted 为 true 表示可信// 详见上文 Context.RemoteIP() 方法内部注释。remoteIP, trusted := c.RemoteIP()if remoteIP == nil {return ""}// 3. 如对端 IP 可信,且 ForwardedByClientIP 为 true(默认为 true),且// RemoteIPHeaders 不为空(默认不为空),则根据 RemoteIPHeaders 中配置的获取 ClientIP 的 Headers 列表中依次获取。默认读取顺序:1. X-Forwarded-For;2. X-Real-IP。if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {for _, headerName := range c.engine.RemoteIPHeaders {// 对header进行处理,先通过","进行分割,并返回分割后 IP 列表的第一个合法 IPip, valid := validateHeader(c.requestHeader(headerName))if valid {return ip}}}// 3. 最终才考虑取对端 IP 兜底。return remoteIP.String()
}
// validateHeader 会对入参header进行校验,先通过","进行分割成 IP 列表后,对每个 IP 进行合法性检查,如果任一 IP 不合法,则此Header不合法;否则返回 IP 列表中第一个 IP。
func validateHeader(header string) (clientIP string, valid bool) {if header == "" {return "", false}items := strings.Split(header, ",")for i, ipStr := range items {ipStr = strings.TrimSpace(ipStr)ip := net.ParseIP(ipStr)...if i == 0 {clientIP = ipStrvalid = true}}return
}

此 『Bug』详细讨论见:https://github.com/gin-gonic/gin/issues/2697。

3. 分析

先介绍几个稍后可能会涉及到的概念/术语:

$remote_addr:是 Nginx 与客户端进行 TCP 连接过程中,获得的客户端真实地址. Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。•X-Client-Real-IP:是一我们在云厂商 WAF/CDN 上自定义 Header,是由云厂商在边缘节点上设置的取值 $remote_addr  的 Header,可以保证我们获取到真实的客户端 IP。这个特性基本上绝大部分云厂商(阿里云、华为云、腾讯云等)都支持。

网络请求通常是浏览器(或其他客户端)发出请求,通过层层网络设备的转发,最终到达服务端。那么每一个环节收到请求中的 $remote_addr 必定是上游环节的真实 IP,这个无法伪造。那从全链路来看,如果需要最终请求的来源,则通过 X-Forwarded-For 来进行追踪,每一环节的 IP( $remote_addr )都添加到 X-Forwarded-For 字段之后,这样 X-Forwarded-For 就能串联全链路了。即:

X-Forwarded-For: client_ip, proxy1_ip, proxy2_ip

3.1. X-Forwarded-For 是否可以被伪造?

客户端是否能伪造 IP,取决于边缘节点(Edge Node)是如何处理 X-Forwarded-For 字段。客户端直接连接的首个 Proxy 节点都叫做边缘节点(Edge Node),无论是网关、CDN、LB 等,只要这一层是直接接入客户端访问的,那么它就是一个边缘节点。

•不重写 X-Forwarded-For 的边缘节点 边缘节点如果是透传 HTTP 的 X-Forwarded-For 头,那么它就是不安全的,客户端可以在 HTTP 请求中伪造 X-Forwarded-For 值,且这个值会被向后透传。

因此不重写 X-Forwarded-For 的边缘节点是不安全的边缘节点,用户可以伪造 X-Forwarded-For 。

# 不安全
X-Forwareded-For:clientX-Forwarded-For(用户请求中的 X-Forwarded-For),proxy1,proxy2,proxy3...

•重写 X-Forwarded-For 的边缘节点 边缘节点如果重写 $remote_addr 到 X-Forwarded-For ,那么这就是安全的。边缘节点获取的 remote_addr 就是客户端的真实 IP。因此重写 X-Forwarded-For 的边缘节点是安全的边缘节点,用户无法伪造 X-Forwarded-For 。

# 边缘节点用 $remote_addr 来覆盖用户请求中的 X-Forwarded-For:
proxy_set_header X-Forwarded-For $remote_addr;
# 安全
X-Forwareded-For:ClientX-Forwarded-For(边缘节点获取的 remote_addr),proxy1,proxy2,proxy3...

3.2. 如何才能获取真实客户端 IP?

我们考虑公有云上常见网络拓扑结构下,能获取真实客户端 IP 的方案。

3.2.1. 客户端->WAF->SLB->Ingress->Pod

3.2.1.1. 使用 Nginx real-ip 模块

使用 Nginx real-ip 模块获取,需在 Ingress 上配置 proxy-real-ip-cidr ,把WAF 和 SLB(7 层) 地址都加上。操作后服务端使用 X-Forwarded-For 可取到真实 IP,通过 X-Original-Forwarded-For 可取到伪造 IP。

这种方案有如下缺点:

•由于 WAF 是云厂商维护,WAF 地址池众多,同时地址会有变化,维护此动态配置难度极大,如更新不及时会导致获取的客户端 IP 不准确。•即使采用此方案,业务方如果要使用新版本的 Gin 的 ctx. ClientIP() 方法,仍然需改动代码,将所有可信代理配置到 TrustedProxies,这会导致基础设施和业务服务耦合,这种方案显然是无法接受的,除非业务方愿意将依赖的 Gin 版本锁死在 v1.6.3。

3.2.1.2. 使用 WAF 自定义 Header

不少云厂商提供了自定义 Header 来获取客户端真实 IP( $remote_addr )能力,我们可以在云厂商 WAF 终端中提前配置好自定义 Header 头,比如 X-Appengine-Remote-Addr 或 X-Client-Real-IP 等,用来获取客户端真实 IP。

这种方案有如下缺点:

•如直接复用 X-Appengine-Remote-Addr 这个 Header,则需设置 engine. AppEngine=true,才可通过 ctx. ClientIP() 方法的前提下获取客户端 IP。•如使用其他 Header,比如 X-Client-Real-IP,则需要自行封装从 X-Client-Real-IP 中获取客户端 IP 方法,同时需要业务配合做改造。

架构大概如下所示:

3.2.2. 客户端->CDN->WAF->SLB->Ingress->Pod

3.2.2.2. 使用 real-ip

使用 real-ip 模块获取,需要在 ingress 上配置 proxy-real-ip-cidr 把 CDN、WAF 和 SLB(7 层)的地址都加上,服务端使用 X-Forwarded-For 可取到真实 IP,通过 X-Original-Forwarded-For 可取到伪造 IP。

此方案优缺点:

•此场景相比 3.2.1 多了层 CDN,CDN 地址池比 WAF 更大,地址池变化频率更高,同时厂商也没有提供 CDN 地址池,维护 Ingress 配置基本不可能。•即使采用此方案,业务方如果要使用新版本的 Gin 的 ctx. ClientIP() 方法,仍然需改动代码,将所有可信代理配置到 TrustedProxies,这会导致基础设施和业务服务耦合,这个肯定无法接受,除非业务方将 Gin 版本锁死在 1.6.3。

3.2.2.1. 使用 CDN 自定义 Header

此方案优缺点:同 3.1.1。架构大概如下所示:

3.2.3. 客户端->SLB->Ingress->Pod

可通过 Ingress 上设置 use-forwarded-headers 来防止 X-Forwarded-For 伪造。

•use-forwarded-headers=false

适用于 Ingress 前无代理层,例如直接挂在 4 层 SLB 上,ingress 默认重写 X-Forwarded-For 为 $remote_addr ,可防止伪造 X-Forwarded-For 。

•use-forwarded-headers=true

适用于 Ingress 前有代理层,例如 7 层 SLB 或 WAF、CDN 等相当于在 nginx.conf 中添加如下配置:

real_ip_header      X-Forwarded-For;
real_ip_recursive   on;
set_real_ip_from    0.0.0.0/0; // 默认信任所有 IP,无法避免伪造 X-Forwarded-For

架构大概如下所示:

4. 总结

从上文中我们不难看出,在云上复杂多变的网络拓扑结构下,我们会频繁地维护 CDN、WAF、SLB、Ingress 等多种网络设施配置。如果需完全保证 X-Forwarded-For 不可伪造,对于要升级 Gin 框架的 Go 服务来说,只有如下两种方案:

•继续尝试通过 X-Forwarded-For 获取客户端真实 IP。•尝试通过其他 Header 获取客户端真实 IP。

4.1. 继续尝试通过 X-Forwarded-For 获取客户端真实 IP

业务中需配置基础设施所有前置代理到 TrustedProxies 中,包含 CDN 地址池、WAF 地址池、Kunernetest Nginx Ingress 地址池,这种方案基本无法落地:

•配置太过复杂,一旦获取 IP 不准,很难排查。•导致业务配置和基础设施耦合,基础设施如果对 CDN、WAF、Ingress 做变动,业务代码必须同步变更。•部分可信代理 IP 根本没法配置,比如 CDN 地址池。

4.2. 尝试通过自定义 Header 获取客户端真实 IP

基础设施团队提供自定义 Header 来获取客户端真实 IP,如 X-Client-Real-IP 或 X-Appengine-Remote-Addr 。这种方案需要基础设施团队在云厂商 CDN 或 WAF 终端上做好相应的配置。这种方案:

•配置简单可靠,维护成本低,仅需在 CDN、WAF 终端配置自定义 Header 即可。•如果使用 X-Appengine-Remote-Addr,对于使用 Google Cloud 的 App Engine 的服务不需做任何修改。对于使用的国内云厂商的服务,则需要显式的配置 engine. AppEngine = true,然后继续通过 ctx.ClientIP() 方法即可。•如果使用其他自定义 Header,如 X-Client-Real-IP 来获取客户端真实 IP,建议可以考虑自行封装 ClientIP(*gin.Context) string 函数,从 X-Client-Real-IP 中获取客户端 IP。

资料链接:

  • https://datatracker.ietf.org/doc/html/rfc7239

  • https://github.com/gin-gonic/gin/issues/2697

实战群

如何获取客户端真实 IP?从 Gin 的一个 Bug 说起相关推荐

  1. 获取客户端真实IP地址

    Java-Web获取客户端真实IP: 发生的场景:服务器端接收客户端请求的时候,一般需要进行签名验证,客户端IP限定等情况,在进行客户端IP限定的时候,需要首先获取该真实的IP. 一般分为两种情况: ...

  2. Java正确获取客户端真实IP方法整理

    转载自 干货:Java正确获取客户端真实IP方法整理 在JSP里,获取客户端的IP地址的方法是:request.getRemoteAddr(),这种方法在大部分情况下都是有效的.但是在通过了Apach ...

  3. vue获取url中ip_Kubernetes 集群中这样获取客户端真实 IP

    Kubernetes 依靠 kube-proxy 组件实现 Service 的通信与负载均衡.在这个过程中,由于使用了 SNAT 对源地址进行了转换,导致 Pod 中的服务拿不到真实的客户端 IP 地 ...

  4. 获取客户端真实 IP

    Tomcat + Nginx 反向代理获取客户端真实IP.域名.协议.端口 Nginx 反向代理后,Servlet 应用通过 request.getRemoteAddr() 取到的 IP 是 Ngin ...

  5. 获取客户端真实ip的方法

    为什么需要获取客户端真实ip ip地址是按地域分布的,服务器获取到客户端ip后可以做流量统计和分析,服务器也可以针对客户端ip做一些定制化的功能,比如限流和黑白名单. 网络环境十分复杂,客户端发出的一 ...

  6. 获取客户端真实IP方法

    2019独角兽企业重金招聘Python工程师标准>>> 我们经常会记录审计日志,那么如何获取客户端真实IP呢?让我们了解一下HTTP协议头吧. 在讨论获取客户端IP 地址前,我们首先 ...

  7. PHP获取客户端真实IP地址的方法

    php获取客户端IP地址有四种方法,这五种方法分别为 1 2 3 4 REMOTE_ADDR HTTP_CLIENT_IP HTTP_X_FORWARDED_FOR HTTP_VIA REMOTE_A ...

  8. asp.net如何获取客户端真实IP地址

    目前网上流行的所谓"取真实IP地址"的方法,都有bug,没有考虑到多层透明代理的情况. 多数代码类似: string IpAddress = (HttpContext.Curren ...

  9. PHP获取客户端真实IP的方法

    摘要: 利用getenv获取获取用户真实ip可以获取相当可观的用户数据,不管是做大数据还是数据备份查找来源,都是不错的选择. 好处: 一个网站,获取用户是非常有必要的,你获得了ip可以干嘛?简单了说, ...

最新文章

  1. 喜欢到底是什么样子呢
  2. 保护1000万篇原创文章,区块链技术如何做到
  3. django-中间件0911-2
  4. 论__AlertDialog自定义布局回调修改的正确方式
  5. Python机器学习 使用sklearn构建决策树复习
  6. python文件操作的方法_python中文件操作的基本方法
  7. HDOJ 1019 Least Common Multiple (数论)
  8. 手机sstv解码软件_关于二维码扫描使用专业扫描设备和手机二维码扫描软件的区别?...
  9. linux下高可用性群集和负载均衡群集的实现
  10. python unpack_Python使用struct处理二进制(pack和unpack用法)
  11. OD调试4----去除NAG窗口的几种方法
  12. 半导体物理学——(一)半导体中的电子状态
  13. 《具体数学》部分习题解答2
  14. idea 导出项目到eclipse
  15. ios 图片合成幻灯片_为iPad构建iOS幻灯片应用程序
  16. .net编程的十大技巧 转载之Jeffery.Sun
  17. qt无边框窗体的移动
  18. shiro的anon部分失效
  19. 对扫描出的图片进行纠偏
  20. 影之刃服务器维护,影之刃无法联机到服务器怎么办 解决办法

热门文章

  1. 实时冷链监测——改进冷链物流技术,提高效率并降低成本
  2. 2021年安全员-B证考试APP及安全员-B证模拟考试
  3. matlab透视变换函数,逆透视变换matlab 代码
  4. 蛇,鳄鱼和狮子,你怎么逃生
  5. 使用Kurento提供公网视频流
  6. ssh服务器拒绝了密码
  7. Oracle date(时间)类型详解
  8. vmware安装镜像
  9. Vue实现购物车页面
  10. python进阶学习