背景

当我们的服务强依赖用户的IP时如何确保后端服务能拿到正确的用户IP就显得尤为重要,否则轻则造成部分功能失效重则会引发安全问题。如果服务是直接暴露在公网上,根据TCP的原理通过socket是可以拿到真实的用户IP的,但是为了安全性和负载均衡的需要我们很少会直接把后端服务直接放在公网上,通常会在服务前置NginxHaproxy等反向代理软件,有时为了需要还会加多层代理,这时如何能获取到用户的IP就成了问题,因为根据TCP的原理,反向代理时上一层和下一层代理建连,后层代理只能获取到上一层代理的IP,没法获取到用户的源IP。在这种情况下X-Forwarded-For就应运而生,也是本文想聊的重点。除此之外还有像Proxy ProtocolTOA等技术也能解决反向代理过程中用户源IP丢失的问题,但这不是本文的关注点。

X-Forwarded-For

X-Forwarded-For是 HTTP头的一个字段,最开始是由 Squid这个缓存代理软件引入,在客户端访问服务器的过程中如果需要经过HTTP代理或者负载均衡服务器,可以被用来获取最初发起请求的客户端的IP地址,如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用。 RFC 7239(Forwarded HTTP Extension)是这个头信息的标准化版本。

X-Forwarded-For 格式:

1
X-Forwarded-For: <client>, <proxy1>, <proxy2>
  • client:客户端的IP地址。

  • proxy1, proxy2:如果一个请求经过了多个代理服务器,那么每一个代理服务器的IP地址都会被依次记录在内。也就是说,最右端的IP地址表示最近通过的代理服务器,而最左端的IP地址表示最初发起请求的客户端的IP地址。

这里要强调的一下:X-Forwarded-For只规定了这个字段的格式,并不代表这是代理服务器的默认行为,是否增加还要是具体配置。

Nginx添加X-Forwarded-For

Nginx做反向代理时为了让后端服务能获取到真实的用户ip网上的教程大多会让增加下边两行配置:

1
2
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

$proxy_add_x_forwarded_for变量的值是什么呢?来看下官方文档中解释

the “X-Forwarded-For” client request header field with the $remote_addr variable appended to it, separated by a comma. If the “X-Forwarded-For” field is not present in the client request header, the $proxy_add_x_forwarded_for variable is equal to the $remote_addr variable.

分两种情况:

  • 如果请求中不带X-Forwarded-For头,那么取$remote_addr的值;
  • 如果请求中带X-Forwarded-For头,那么在X-Forwarded-For后追加$remote_addr,即: X-Forwarded-For,$remote_addr

很简单就两行配置,但是这么配置有没有问题呢?

接下来我们来聊聊X-Forwarded-For头的安全问题,X-Forwarded-For可以被伪造。

X-Forwarded-For 伪造

因为X-Forwarded-For只是http的请求的一个头,如果请求时带上一个一个伪造的X-Forwarded-For(使用curl -H 'X-Forwarded-For: 8.8.8.8' http://www.dianduidian.com一条命令就能实现),这时Nginx如果使用上边的配置的话由于X-Forwarded-For不为空,所以Nginx只会在现在值的基础上追加,这样后端服务在拿到头后根据约定取最左边ip话就会拿到一个伪造的IP,会有安全风险。

由于头是不可靠的我们不能信任客户端传过来头信息,那么如何解决呢?

TCP不像UDP必须经过3次握手,客户端的IP是无法伪造的,所以最外层的代理一定要取$remote_addr的值,对应配置

1
2
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;

从X-Forwarded-For中截取用户IP

实际应用中有时我们需要从X-Forwarded-For头中截取出用户的IP,Nginx可以通过正则实现

1
2
3
    if ( $http_x_forwarded_for ~ (^[^,]+) ){
      set  $x_real_ip $1;
    }

结论

为了保证后端服务能获取到真实的用户IP,无论中间有多少层代理,必须保证最外层代理能获取到真实的用户IP,这是整个信任连的基础;最外层(直接暴露给用户)的代理一定不能信任请求带过来的X-Forwarded-For,而应取TCP建连时的IP(即$remote_addr),同时必须保证中间层的代理无法被用户直接访问到,否则就不是一个完整的信任链,就有伪造的可能。

  • 最外层Nginx:

    1
    2
    
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
    
  • 其它中间层Nginx:

    1
    2
    
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    

另外还要提下使用CDN这种特殊情况,虽然它的控制权不在我们这里但也同样适用于也上边的结论,CDN属于最外层代理,我们的代理属于中间层,我们代理只能信任前置CDN传过来的X-Forwarded-For头信息,但这肯定也是没问题的,毕竟CDN厂商肯定已经考虑到了。

只要服务不是直接暴露在外网,后端服务就必须借助中间代理层的传递来获取真实的用户IP,虽然X-Forwarded-For存在伪造的可能,但只要清楚它的原理,根据具体情况使用合理的配置是完全可以避免的。

参考

http://nginx.org/en/docs/http/ngx_http_proxy_module.html

https://en.wikipedia.org/wiki/X-Forwarded-For#cite_note-RFC_7239-30

https://tools.ietf.org/html/rfc7239