X-Forwarded-For是怎么来的

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 方法可以获取到请求客户端的
	IPfunc (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}

分析

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

$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

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...

源自
链接: X-Forwarded-For 是一个 HTTP 扩展头
http://www.manongjc.com/detail/60-jolcbnqfzwhlqwq.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值