如何获取客户端真实 IP?从 Gin 的一个 "Bug" 说起
作者:郑伟@石墨文档
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-Ip
if 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 作为客户端IP
if 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 列表的第一个合法 IP
ip, 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 = ipStr
valid = 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
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
亿级流量系统架构演进之路
海量用户同时进行高频访问对任何平台都是难题,也是行业乐此不疲的研究方向。但值得庆幸的是,虽然业务场景不同,设计和优化的思想却是万变不离宗。本文将结合业务与高并发系统设计的核心技术点,对系统架构调优方案进行深度剖析。 文章根据 Authing 身份云高级工程师罗杰林,在又拍云 Open Talk 技术沙龙北京站所作主题演讲《亿级流量系统架构演进之路》整理而成,现场视频及 PPT 可点击阅读原文查看。 相信大家都同意,互联网发展势头的逐渐凶猛改变了我们很多的生活方式。比如网购、银行转账等业务,不再需要我们必须线下办理,这极大方便了我们的生活。这背后当然也对身为互联网从业人员的我们来说,面临的考验也越来越大,在系统架构升级上也会倾注更大的心血。 认识高并发系统 高并发系统拥有高并发、高性能、高可用,分布式、集群化,安全性等特性。 我们首先来看一下高并发、高性能、高可用,也就是我们经常提到的三高系统。当我们流量非常大的情况下,我们一定要保证这三高。这其中高并发是指要支持很多并发用户,高性能是在高并发的前提下保证优秀的性能,高可用则是保证系统在某一节点出现问题时不会整体宕机且继续持续提供服务。由...
- 下一篇
Spring Boot 2.4.11 正式版发布
一、发布说明 9月22日官方发布了Spring Boot 2.4.11版本,此版本包括32个错误修复、文档改进和依赖项升级。 二、更新内容 2.1 bug修复 修复默认情况下,执行器端点不会清理 SPRING_APPLICATION_JSON #28045 当过滤器抛出 NestedServletException 以外的异常时,Web MVC 指标出现错误的状态#27988 并发镜像构建导致删除构建器镜像时出错#27888 运行大于 4GB的 Zip64 jar 文件时出现 IndexOutOfBoundsException #27822 多个线程调用时,Binder 转换会间歇性失败#27813 @MockBean结合@Repeat出现“the field cannot have an existing value”错误的结果#27693 修复当存档文件名包含 URL 中保留的字符时,嵌入式 Undertow 抛出 MalformedURLException #9283 2.2文档 使用 AspectJ weaving 时 devtools 重启不起作用的文档#28071 spri...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS关闭SELinux安全模块
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7设置SWAP分区,小内存服务器的救世主