##1.背景知识
1.1. 前提知识点:
还有nginx中的几个变量:
- remote_addr
代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP,除非代理将你的IP附在请求header中一起转交给web服务器。
-
X-Forwarded-For(简称XFF)
X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
XFF的格式为:
X-Forwarded-For: client, proxy1, proxy2
XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成,最开始的是离服务端最远的设备 IP,然后是每一级代理设备的 IP。(注意:如果未经严格处理,可以被伪造)
如果一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1、Proxy2、Proxy3,IP 分别为 IP1、IP2、IP3,用户真实 IP 为 IP0,那么按照 XFF 标准,服务端最终会收到以下信息:
X-Forwarded-For: IP0, IP1, IP2
Proxy3 直连服务器,它会给 XFF 追加 IP2,表示它是在帮 Proxy2 转发请求。列表中并没有 IP3,IP3 可以在服务端通过 Remote Address 字段获得。我们知道 HTTP 连接基于 TCP 连接,HTTP 协议中没有 IP 的概念,Remote Address 来自 TCP 连接,表示与服务端建立 TCP 连接的设备 IP,在这个例子里就是 IP3。Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。但是在正常情况下,web服务器获取Remote Address只会获取到上一级的IP,本例里则是proxy3 的 IP3,这里先埋个伏笔。
-
X-Real-IP
这又是一个自定义头部字段,通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端,这个要看经过代理的层级次数或是是否始终将真实IP一路传下来。(注意:如果未经严格处理,可以被伪造)
1.2.前提与铁律
铁律:当多层代理或使用CDN时,如果代理服务器不把用户的真实IP传递下去,那么业务服务器将永远不可能获取到用户的真实IP。
1.3.用户真实IP的来源和现实情况
首先说用户真实的IP也会存在很多人共用一个IP的情况。用户的请求到达业务服务器会经过以下几种情形:
####1.3.1.宽带供应商提供独立IP
比如家里电信宽带上网,电信给分配了公网ip,那么一个请求经过的ip路径如下:
192.168.0.101(用户电脑ip)–>192.168.0.1/116.1.2.3(路由器的局域网ip及路由器得到的电信公网ip)–>119.147.19.234(业务的前端负载均衡服务器)–>192.168.126.127(业务处理服务器)。
这种情况下,119.147.19.234会把得到的116.1.2.3附加到头信息中传给192.168.126.127,因此这种情况下,我们取得的用户ip则为:116.1.2.3。如果119.147.19.234没有把116.1.2.3附加到头信息中传给业务服务器,业务服务器就只能取上上一级的110.147.19.234.
1.3.2.宽带供应商不能提供独立IP
宽带提供商没有足够的公网ip,分配的是个内网ip,比如长宽等小的isp。请求路径则可能为:
192.168.0.123(用户电脑ip)–>192.168.0.1/10.0.1.2(路由器的局域网ip及路由器得到的运营商内网ip)–>211.162.78.1(网络运营商长城宽带的公网ip)–>119.147.19.234(业务的前端负载均衡服务器)–>192.168.126.127(业务处理服务器)。
这种情况下得到的用户ip,就是211.162.78.1。 这种情况下,就可能出现一个ip对应有数十上百个用户的情况了(受运营商提供的代理规模决定,比如可能同时有几千或上万的宽带用户都是从211.162.78.1这个ip对外请求)。
####1.3.3.手机2g上网
网络提供商没法直接提供ip给单个用户终端,以中国移动cmwap上网为例,因此请求路径可能为:
手机(手机上没法查看到ip)–> 10.0.0.172(cmwap代理服务器ip)–>10.0.1.2(移动运营商内网ip)–>202.96.75.1(移动运营商的公网ip)–>119.147.19.234(业务的前端负载均衡服务器)–>192.168.126.127(业务处理服务器)。
这种情况下得到的用户ip,就是202.96.75.1。2008年的时候整个广东联通就三个手机上网的公网ip,因此这种情况下,同一ip出现数十万用户也是正常的。
####1.3.4.大厂,有几万或数十万员工,但是出口上网ip就一个
这种也会出现来自同一ip的超多用户,比如腾讯、百度等某一个办公区,可能达到几万人,但出口IP可能就那么几个。
2.如何获取用户真实IP
2.1. 当业务服务器直接暴露在公网上,并且未使用CDN和反向代理服务器时:
可以直接使用remote_addr。如 PHP 可以直接使用
$_SERVER['REMOTE_ADDR']
这时候,HTTP_X_FORWARDED_FOR 和 HTTP_X_REAL_IP 都是可以被伪造的,但REMOTE_ADDR是客户端和服务器的握手IP,即client的出口IP,伪造不了。
比如用下面这条命令来请求一个php文件,并且输出$_SERVER信息
curl http://10.200.21.32/test.php -H 'X-Forwarded-For: unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1' -H 'X-Real-IP: 2.2.2.2, <a>'
结果是(只取部分信息,10.100.11.25是我电脑的IP,服务器是内网服务器,所以不会有公网IP)
[HTTP_X_FORWARDED_FOR] => unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1
[REMOTE_ADDR] => 10.100.11.25
[HTTP_X_REAL_IP] => 2.2.2.2, <a>
可以看到,HTTP_X_FORWARDED_FOR 和 HTTP_X_REAL_IP 是万万不可直接拿来用的。使用$remote_addr
是明智的选择。
比如我们伪造一下来源IP发给著名的 ip138.com
curl http://1212.ip138.com/ic.asp -H 'X-Forwarded-For: unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1'
它原样输出了我们伪造的XFF。
2.2.在代理服务器或CDN之后的业务服务器
前提:上面的每一层代理或CDN,都将原始请求的 remote_addr 一路传递下去。我们先来看其中一种方案。
如果web服务器上层也是使用nginx做代理或负载均衡,则需要在代理层的nginx配置中明确XFF参数,累加传递上一个请求方的IP到header请求中。以下是代理层的nginx配置参数。
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
如果web服务器前面使用了HAProxy,则需要增加以下配置来将用户的真实IP转发到web服务器。
option forwardfor
如果想在业务服务器获取完整的链路信息,还是通过XFF获取,需要在nginx的配置中加一条配置,加上此配置可以让我们获取整个链路信息:
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
实测此参数最好加在被 include 的fastcgi.conf
中,就是有一堆fastcgi_param
配置的那个文件否则就写入location
段。这个配置可能会影响你的nginx日志,这个后续会详细说明。如果不配置此项,则我们在WEB SERVER 上直接获取到的XFF信息则是上一个代理层的IP。当然,也不影响获取用户真实IP。不过如果你是在调试配置的情况下,就不方便查看整个链路了。
2.2.1 在只有一层代理的情况下
我们按上面的配置发起一个伪造请求, 10.100.11.25 是我电脑的IP,链路为:
10.100.11.25(client)->10.200.21.33(Proxy)->10.200.21.32(Web Server)
curl 请求:
curl http://10.200.21.33:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'
结果如下:
[HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 10.100.11.25
[REMOTE_ADDR] => 10.200.21.33
[HTTP_X_REAL_IP] => 10.100.11.25
我们可以看到,XFF被附加上了我的IP,但前面的一系列伪造内容,可以轻易骗过很多规则,而HTTP_X_REAL_IP
则传递了我电脑的IP。因为在上面的配置中,X-Real-IP
已经被设置为握手 IP。 但多层代理之后,以上面的规则,显然 HTTP_X_REAL_IP
也不会是真实的用户IP了。而 HTTP_X_FORWARDED_FOR
则在原有信息(我们伪造的信息)之后附上了握手 IP 一起传递过来了。
2.2.2 在两层或更多代理的情况下
我们这里只测试两层,实际链路为:
10.100.11.25(client)->10.200.21.34(Proxy)->10.200.21.33(Proxy)->10.200.21.32(Web Server)
Curl 命令:
curl http://10.200.21.34:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2'
两层代理的情况下结果为:
[HTTP_X_FORWARDED_FOR] => unkonw, <8.8.8.8> 1.1.1.1, 10.100.11.25, 10.200.21.34
[REMOTE_ADDR] => 10.200.21.33
[HTTP_X_REAL_IP] => 10.200.21.34
根据上面的情况,怎么挑出真正的用户IP呢?设想三种方案:
1. 第一层代理将用户的真实 IP 放在 X-Real-IP 中传递下去,后面的每一层都使用 X-Real-IP 继续往下传递。配置为:
proxy_set_header X-Real-IP $remote_addr; # 针对首层代理,拿到真实IP
proxy_set_header X-Real-IP $http_x_real_ip; # 针对非首层代理,一直传下去
2. 从首层开始,