获取客户端真实 IP 地址的最佳实践

这篇具有很好参考价值的文章主要介绍了获取客户端真实 IP 地址的最佳实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、背景

1. 业务上云带来性能收益

公司从去年全面推动业务上云,而以往 IDC 架构部署上,接入层采用典型的 4 层 LVS 多机房容灾架构,在业务高峰时期,扩容困难(受限于物理机资源和 LVS 内网网段的网络规划),且抵挡不住 HTTPS 卸载引发的高 CPU 占用。

而经过压力测试发现,使用腾讯云 7 层 CLB 负载均衡进行 HTTPS 卸载,性能得到极大提升。测试数据也表明,IDC 旧架构中,启用 HTTPS 会带来 90% 以上的性能损耗。

2. 架构调整引发多次故障

引入腾讯云 7 层 CLB 负载均衡产品,带了了巨大的性能提升,却也给业务带来了痛苦,主要核心问题是获取客户端的真实 IP 上。

当前现状是业务语言异构(PHP + Go),多数业务已经历服务化改造,但缺乏服务发现机制,服务与服务之间的调用依赖域名和 DNS 解析,大部分都是 HTTP 服务。

在架构调整后,由于未能 100% 覆盖测试,导致漏测的服务经常拿到错误的客户端 IP 地址,造成的后果是损失大量的用户。这些用户会因为短信验证码发送限制、IP 登录频次过高而无法登录、充值,给公司带来巨大损失。

3. 未来的路应该怎么走?

更进一步讲,当前业务如何抵挡外界的 DDoS 攻击、请求机器人、SQL 注入等等,最简单的是接入高防 IP、WAF 应用防火墙,而请求经过多轮转发,同样也有获取客户端真实 IP 的问题。

再者,业务也在逐步容器化,享受 Kubernetes 弹性扩容的便利,怎么平滑迁移也是非常值得深思的。

假设有一天某个同学,不小心配置有误——应用层拿到的,很有可能是高防 IP 或者 WAF 的 IP,业务绝对无法忍受。

显然,确定一个业务无感知的方案并成功落地迫在眉睫。

然而翻遍整个互联网,几乎没有文章能把这些看起来很简单的事情捋清楚、讲明白,更不用说最佳实践。

大多数人都是抄抄配置,潦潦草草上线,方案并没有普适性。

这篇文章也是我在这段时间的研究中总结出来的宝贵经验,希望对读者能有些许帮助。文章篇幅较长,难免有错误之处,还请各位看官斧正,感激不尽:)

二、名词释义

1. REMOTE-ADDR
  • Nginx + PHP 模式下,REMOTE-ADDR 为远端的 IP 地址,可通过 $_SERVER['REMOTE-ADDR'] 获取;
  • 它代表与上一层建立 TCP 连接的 IP 地址;
  • 网站无代理时(客户端->服务端),WEB服务器(Nginx,Apache等)会设置该值为客户端 IP;
  • 网站存在代理时(客户端->代理->服务端),该值为代理的 IP。
proxy_set_header REMOTE-ADDR $remote_addr;
2. X-Forwarded-For

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

  • 格式为英文逗号 + 空格隔开,例如:X-Forwarded-For: IP0(client), IP1(proxy), IP2(proxy);

  • 中间经过的代理,会逐层追加至末尾;

  • IP0 离服务端最远,然后是每一级代理设备的 IP,IP2 直连服务端。

  • 如果客户端伪造 IP 地址,格式为:X-Forwarded-For: 伪造的 IP 地址 1, [伪造的 IP 地址 2…], IP0(client), IP1(proxy), IP2(proxy)。

3. X-Real-IP

注:CLB <=> SLB,为腾讯云和阿里云不同产品的称呼,均为负载均衡。

典型的调用链路:

client --> ① [CLB-7]gateway --域名--> ② [CLB-7]server(③ nginx + ④ go/php)
  • X-Real-IP 为建立 TCP 连接的上一跳的 IP 地址;
  • 对于 ④ 而言,X-Real-IP 为 ① 网关的 NAT 公网出口 IP 地址,或 gateway 的内网 IP 地址,该结论通过生产环境 tcpdump 抓包验证得到;
  • 公网调用下,① 网关 调用 ② 7 层 CLB,再到应用层 ③④,此时 ④ 拿到的 X-Real-IP 为 ① 的 NAT 公网出口地址(7 层 CLB 会重写 X-Real-IP 头部,并追加 X-Forwarded-For 头部);
  • 内网环境中,原理相似,只不过拿到的是 gateway 的内网 IP 地址;
  • 中间可能被 ③ nginx 重写,此时等同于 REMOTE-ADDR。

比如以下最常见的 nginx 配置:

proxy_set_header  REMOTE-ADDR     $remote_addr;
proxy_set_header  X-Real-IP       $remote_addr;
proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;

REMOTE-ADDR 和 X-Real-IP 都是 nginx 的 $remote_addr 变量,再传递给下游。

三、面临困境

1. 运维侧
  • 业务线配置五花八门,没有统一。具体表现在 nginx.conf 和 vhost 配置在不同的业务线有很大区别;
  • vhost 成千上万,nginx 内部存在多重转发,外部也有网关转发过来的流量,且网关不止一套,捋不清链路容易导致线上故障;
  • 缺乏完善的 QA 验证流程,变更没办法 100% 覆盖测试,最终结果就是尽可能少变更,但这不是长久之计;
  • 存在开发自行维护信任 IP 的情况,所以运维不敢随便变更,因为变更前需要通知开发整改,开发有自己的时间排期,处理起来效率极其低下;
  • 为了尽可能少修改原先的配置,部分机器组接入了腾讯云的 TOA 模块,用来获取客户端真实 IP 地址,而阿里云没有相似的产品,如果没有统一的方案,没办法上线阿里云,实现不了双云双活的目标等等。
2. 开发侧

各个业务线使用的技术栈不统一,存在多种获取客户端 IP 的方案,需要找到一种尽可能少修改代码,或者一点都不需要修改代码的方案。

PHP 以 Laravel 框架为例(底层是 Symfony 框架),发现内部取了 $_SERVER[‘REMOTE_ADDR’] 变量:

public function getClientIp()
{
    $ipAddresses = $this->getClientIps();
    return $ipAddresses[0]; // 1. 取第一个 IP 地址。
}
public function getClientIps()
{
    $ip = $this->server->get('REMOTE_ADDR');
    if (!$this->isFromTrustedProxy()) {
        // 2. 程序在这里返回了 REMOTE_ADDR 头部的值。
        return [$ip];
    }
    // 3. 永远到不了这个分支。
    return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}
public function isFromTrustedProxy()
{
    // 4. 因为生产环境中,$trustedProxies 没有配置。
    return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
}

公司内部有些业务自己实现函数,依赖的是 X-Forwarded-For 头部。

Go 以 Gin 框架为例,准确的说是 Gin@v1.6. 版本,它先取 X-Forwarded-For 的第一个 IP,取不到就取 X-Real-IP 头部:*

func (c *Context) ClientIP() string {
	// 1. ForwardedByClientIP 默认为 true
	if c.engine.ForwardedByClientIP {
		// 2. 优先获取 X-Forwarded-For 头部
		clientIP := c.requestHeader("X-Forwarded-For")
		// 3. 取 X-Forwarded-For 的第一个 IP 地址
		clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
		// 4. 取不到就取 X-Real-Ip 字段
		if clientIP == "" {
			clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
		}
		// 5. 拿到了就直接返回(正常的逻辑)
		if clientIP != "" {
			return clientIP
		}
	}
	// 6. 忽略,该值为 false,除非 build tags 包含 appengine 为 true
	if c.engine.AppEngine {
		if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
			return addr
		}
	}
	// 7. 以上都取不到的话,取 RemoteAddr 字段,走到这个逻辑,程序肯定不正常。
	// 参考 Go 标准库,该值为 TCP 建立连接的远端 IP 地址
	// go1.17.1/src/net/http/server.go:1003
	// req.RemoteAddr = *conn.remoteAddr
	if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
		return ip
	}
	return ""
}

经过调研发现,业务取的是 X-Real-IP 字段,具体原因就不展开了。

至于 Gin@1.7.* 版本,由于 Gin@1.6.* 的实现存在伪造客户端 IP 的问题,被爆 CVE-2020-28483 漏洞,官方为了修复这个问题,换了一种实现修复该漏洞:

func (c *Context) ClientIP() string {
	// 1. 自定义 Header 的情况,可以忽略
	if c.engine.TrustedPlatform != "" {
		if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
			return addr
		}
	}
	if c.engine.AppEngine {
		if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
			return addr
		}
	}
	// 2. 获取 IP 地址,并返回是否可以信任
	remoteIP, trusted := c.RemoteIP()
	if remoteIP == nil {
		return ""
	}
	// 3. 如果信任,检查 IP 地址的合法性,合法就返回
	// 默认值:ForwardedByClientIP=true,RemoteIPHeaders=[X-Forwarded-For(优先), X-Real-IP]
	if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
		for _, headerName := range c.engine.RemoteIPHeaders {
			// c.requestHeader 在头部有效的情况下,也是返回第一个 IP 地址。
			ip, valid := validateHeader(c.requestHeader(headerName))
			if valid {
				return ip
			}
		}
	}
	// 4. 不能信任,那就用 TCP 连接远端 IP 兜底。
	return remoteIP.String()
}
func (c *Context) RemoteIP() (net.IP, bool) {
	ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
	if err != nil {
		return nil, false
	}
	remoteIP := net.ParseIP(ip)
	if remoteIP == nil {
		return nil, false
	}
	// remoteIP = TCP 连接远端 IP 地址
	// 由于业务没有配置 engine.TrustedProxies,所以是不可信任的。
	return remoteIP, c.engine.isTrustedProxy(remoteIP)
}
func (e *Engine) isTrustedProxy(ip net.IP) bool {
	if e.trustedCIDRs != nil {
		for _, cidr := range e.trustedCIDRs {
			if cidr.Contains(ip) {
				return true
			}
		}
	}
	// 业务将会走到这里!
	return false
}
func (e *Engine) validateHeader(header string) (clientIP string, valid bool) {
	if header == "" {
		return "", false
	}
	items := strings.Split(header, ",")
	for i := len(items) - 1; i >= 0; i-- {
		ipStr := strings.TrimSpace(items[i])
		ip := net.ParseIP(ipStr)
		if ip == nil {
			return "", false
		}
		// X-Forwarded-For is appended by proxy
		// Check IPs in reverse order and stop when find untrusted proxy
		if (i == 0) || (!e.isTrustedProxy(ip)) {
			return ipStr, true
		}
	}
	return
}

官方的手法也是简单粗暴,以前是将错就错,这次一下子修复好了,搞得很多人翻车了(https://github.com/gin-gonic/gin/issues/2697)。

原因是新的实现没有兼容 1.6 版本,导致升级框架后获取不到客户端的真实 IP,1.7.7 才解决该问题。

四、三大原则

分析完整个事情的来龙去脉,想必读者们对现状有一定的了解。

我把这套方案,抽象为三大原则,只要理解它,获取客户端真实 IP 的问题,就跟喝水一样简单!

1. 代理必须向下传递客户端 IP 地址

原因:从入口流量开始,经过 N 层代理,如果代理中间不传递客户端的 IP 地址,底层业务必然获取不到客户端的真实 IP 地址

2. 统一使用 nginx 的 realip 模块获取客户端 IP 地址
# nginx.conf
# ...
set_real_ip_from 腾讯云/阿里云 NAT 出口网段;
set_real_ip_from 腾讯云/阿里云高防 IP 网段;
set_real_ip_from 腾讯云/阿里云 WAF 网段;
set_real_ip_from CDN 网段;
set_real_ip_from 内网地址网段; # 按需配置,对于网关进来的请求通过内网到业务机器,需要配置上这个网段。
set_real_ip_from 127.0.0.1;  # 按需配置,主要作用在 nginx 的内部转发。
real_ip_header X-Forwarded-For;
real_ip_recursive on;        # 必须打开该选项,原因见下面分析。
access_by_lua '
    ngx.req.set_header("X-REAL-IP",       ngx.var.remote_addr)
    ngx.req.set_header("X-FORWARDED-FOR", ngx.var.remote_addr)
';

# vhost/*.conf
location ^~ /foo {
    access_log         logs/api_foo.access.log main;
    proxy_pass         http://api_foo;
    proxy_redirect     off;
    proxy_http_version 1.1;
    proxy_set_header   Host            $http_host;
    proxy_set_header   X-NginX-Proxy   true;
    proxy_set_header   Connection      "";
}

此时,X-Real-IP、REMOTE-ADDR、X-Forwarded-For 均统一为 realip 模块重写后的 $remote_addr 变量,业务就可以取到真实的客户端 IP 地址,无需考虑 PHP、Go 等不同语言、同种语言不同框架下的差异。

那问题来了,客户端 IP 是否会被伪造?答案是不会的。

按照 X-Forwarded-For 的定义,该头部每经过一层就追加一个 IP 地址:

X-Forwarded-For: 客户端伪造 IP 地址, IP0(client), IP1(proxy), IP2(proxy)

那么,我们只需启用 realip 模块的 real_ip_recursive 递归模式,将从右往左逐步剔除 IP2,IP1 等信任代理,最后会获取到真实的客户端 IP 地址。

问题二:网上有一种边缘节点的方案,为什么不采用?

边缘节点,指的就是接入层,直接连接客户端的那一层。经过边缘节点转发到下游的,统称为非边缘节点。

按照这个思路,如果边缘节点拿到了客户端 IP,重置 X-FORWARDED-FOR 头部为客户端 IP 地址,并转发到下游,业务只获取第一个 IP 地址,理论上也不会被伪造,业务也简单,为什么不采用?

因为边缘节点方案最大的缺点在于失去了灵活性,譬如你想接入高防 IP 或者 WAF 防火墙,此时它已不再是边缘节点,而是接收高防服务器或 WAF 防火墙清洗的流量,将会拿到错误的 IP 地址。

3. 运维维护信任 IP 列表,开发代码不做处理

由 2 可知,三个头部均为统一的值,对开发可以保证最大的兼容性。原因是不同的语言,同个语言的不同开发框架,同个框架的不同版本,获取客户端 IP 的方式也就这几种。

对开发而言,确实没必要关心自己的代码需要引入 NAT 网关 IP 配置、高防 IP 配置等,并且每个工程可能都要修改,这是不现实的。

本质上,这也是运维的工作。举个例子,如果真的遇到 DDoS 攻击,切换高防 IP 抵御 DDoS 攻击的操作人是运维,开发这个时候去将所有工程配置上高防 IP 地址是一件极其痛苦的事情。一旦加漏、加错将直接引发故障。

五、最佳实践

(1) 虚拟机部署
  1. SRE 维护信任的 IP 池,X-Real-IP、REMOTE-ADDR、X-Forwarded-For 均统一为 realip 模块重写后的 $remote_addr 变量,开发不感知;
  2. 开发无需修改代码,因为上述三个变量读取出来的值是一致的,无任何风险。
(2) 容器化部署

a. PHP 无需改动,可以平滑切换上容器。因为 PHP 容器上层依然有 nginx.conf,平移该配置即可;

b. GO 容器化,有 2 种方案:

注:最终采用方案 2,去除了 Pod 内部的 nginx 转发,Pod 的上层使用了 nginx-ingress,做到了业务无感知容器上云。

  1. 如果保留虚拟机架构,即 Go 服务上层有 nginx,也是平移就可以了,跟 PHP 一样;

  2. 如果 Go 服务上游去除 nginx 转发:

    流量入口使用 7 层腾讯云 CLB / 阿里云 SLB 进行 HTTPS 卸载后转发到容器集群的 nginx-ingress,业务代码无感知。实现原理和虚拟机方案相似,均为配置 realip 模块和统一 X-Real-IP、REMOTE-ADDR、X-Forwarded-For 头部,详情可以参考以下资料:

    • https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#use-proxy-protocol
    • https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-real-ip-cidr
    • https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#server-snippet
    • https://help.aliyun.com/document_detail/86533.html

还有个容易忽略的点——ingress 选型。

如果使用 Pod 直连,也就是不使用 nginx-ingress:

PHP / Go 上层都需要有一层 nginx 并配置好 nginx.conf,配置 realip 模块和统一 X-Real-IP、REMOTE-ADDR、X-Forwarded-For 头部。

此时 PHP / Go 架构统一,但对 Go 容器来说多了一层 nginx,会造成资源浪费(每个 Pod 都需要部署一个 nginx,再转发到 Go)。

具体用哪个 ingress,就要看怎么取舍了。

nginx 存在的意义在于阻止业务直接感知到信任代理 IP 列表的存在,如果对于你的业务而言,各个业务线去维护这个配置列表成本极低,那 nginx 确实是没有存在的必要性。


总之,我个人认为:

  1. 业务完全不需要关心如何获取客户端的真实 IP,这是最好的选择;
  2. 千万不要封装各种函数去获取客户端真实 IP,这种问题最好交给上层 SRE 基础架构的同学负责,不然真的非常容易出问题;
  3. 理解好三大原则,获取客户端真实 IP 的问题,就跟喝水一样简单!

OK,文章终于写完了,花费了好多天的时间整理,憋出来了。感谢你读到这里,是时候吃晚饭了:)


文章来源于本人博客,发布于 2021-12-19,原文链接:https://imlht.com/archives/248/文章来源地址https://www.toymoban.com/news/detail-753849.html

到了这里,关于获取客户端真实 IP 地址的最佳实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 获取客户端真实IP的方法

    获取请求的IP很简单,可以直接使用request.getRemoteAddr()直接获取。但由于请求在转发到接口前,会经过大量的反向代理,例如流程图中,至少要经过Nginx后,请求才会转发到接口,因此需要对请求接口的IP做处理,提取客户端真实IP地址。 配置Nginx的配置文件,需要反向代理服务

    2024年02月16日
    浏览(44)
  • kubernetes获取客户端真实ip

    大部分的业务场景都需要获取客户端的ip来审计或采取措施,文章从nodeport暴露方式获取真实ip到ingress-nginx获取真实ip 初学者用k8s创建时暴露方式一般采用nodeport,这样方式暴露导致应用负载和访问者并不是同一段网络,当web服务获取客户端ip的时候会发现获取到的ip是k8s网关的

    2024年02月01日
    浏览(38)
  • nginx获取客户端真实ip

    在nginx中获取客户端真实IP的方法有多种,以下是其中两种常用的方法: 使用nginx的access_log模块记录请求日志,并在日志中包含客户端的真实IP信息。例如: 在上述配置中,通过使用http_x_forwarded_for字段来获取客户端的真实IP地址。如果该字段不存在或不合法,则使用remote_ad

    2024年02月16日
    浏览(41)
  • k8s ingress获取客户端客户端真实IP

    在Kubernetes中,获取客户端真实IP地址是一个常见需求。这是因为在负载均衡架构中,原始请求的源IP地址会被替换成负载均衡器的IP地址。 获取客户端真实IP的需求背景包括以下几点: 安全性:基于客户端IP进行访问控制和认证授权可以提高系统安全性。 日志记录与审计:记

    2024年02月13日
    浏览(49)
  • nginx如何获取真实客户端ip

    nginx作为反向代理服务器,即代理我们的服务端,下面介绍下如何配置nginx获取真实的客户端ip 1、配置nginx.con 2、在java程序中可以通过如下方式获取: 这样就可以打印出真实ip了!即request.getHeader(\\\"X-Real-IP\\\")的值 引用: 查看端口占用及释放所占用的端口_查询谷歌浏览器的端口

    2024年02月11日
    浏览(43)
  • Nginx(二十) 获取真实客户端IP

            客户端在访问互联网应用服务器时,与真实的应用服务器之间会因为有多层反向代理,而导致真实应用服务器获取的仅是最近一层的反向代理服务器 IP。为使 Nginx 后端的上游服务器可以获得真实客户端 IP,Nginx 提供了 ngx_http_realip_module 模块用以实现真实客户端

    2024年01月16日
    浏览(37)
  • 学习NodeJs之【如何获取客户端真实IP】

            产品想要增加一个操作日志的模块,重点记录增删改的操作ip。         这块业务其实需要按【是否代理】来分逻辑。首次开发时,并未考虑  测试生产环境有统一代理  ,导致传统的获取ip方式取到了容器网关ip。——显然这样是无法满足产品想要区分操作

    2024年02月03日
    浏览(38)
  • 【全方位解析】如何获取客户端/服务端真实 IP

    1.比如在投票系统开发中,为了防止刷票,我们需要限制每个 IP 地址只能投票一次 2.当网站受到诸如 DDoS(Distributed Denial of Service,分布式拒绝服务攻击)等攻击时,我们需要快速定位攻击者 IP 3.在渗透测试过程中,经常会碰到网站有 CDN(Content Distribution Network,内容交付网络

    2024年02月07日
    浏览(48)
  • k8s ingress 添加获取客户端真实ip配置

    本环境是一个互联网ip服务器上的nginx转发k8s集群内的ingress域名,实现所有服务通过域名访问, 默认配置下,在pod内获取客户端请求地址信息时,获取的是pod的ip和节点ip。要获取客户端ip,需要添加nginx配置中 除此之外还需要修改configmap ingress-nginx-controller ,增加如下配置

    2024年02月02日
    浏览(35)
  • Nginx+netty实现tcp负载均衡,获取客户端真实ip

    在nginx.conf文件中,events,http同级添加配置 启动nginx服务 启动2个服务netty服务设置nginx中8888,8889端口。 使用tcp工具连接并发送数据测试 参考博客 参考链接1 参考链接2

    2024年02月06日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包