nginx 的 Real IP 模块用于解决代理服务器转发请求到nginx上时可能出现的 IP 地址问题。因为当 PROXY收到客户端的请求时,它会通过自己的IP与nginx服务器连接并转发请求。这会导致在nginx应用程序中记录的 IP 地址是代理服务器的地址,而不是实际客户端的地址。
举例如下图:
当nginx收到来自客户端的HTTP请求,由于经过了中间的代理PROXY服务器,NGINX默认只能知道请求来自PROXY的内网IP 192.168.0.1,而不是客户端的真实IP 111.22.33.44。因此,需要一种机制能够让NGINX获取到客户端的真实IP,好在HTTP协议可以通过X-Forwarded-For头或者X-Real-IP头将客户端的真实IP透传到后端,在这个例子中,当PROXY收到请求后,它会在客户端的请求头中增加一个带有客户端IP的X-Forwarded-For头或者X-Real-IP头,然后转发给后端的NGINX服务器,NGINX服务器要根据约定从对应的HTTP请求头中获取客户端的真实IP。
Real IP 模块的使命就是将代理服务器传递的真实客户端 IP 地址还原为实际客户端的 IP 地址,以便nginx中的应用模块可以获取到真实的客户端IP。
本文首先介绍Real IP 模块的使用和配置,然后通过对Real IP 模块的源码分析来深入理解其实现的机理。
ngx_http_realip_module默认是没有enable的,因此,需要在configure的时候将这个模块enable,如下:
./configure --with-http_realip_module
1. real_ip_header配置指令
语 法:
real_ip_header field | X-Real-IP | X-Forwarded-For | proxy_protocol;
默认值:
real_ip_header X-Real-IP;
上下文:
http, server, location
本指令用来定义从哪个地方获取客户端的真实IP,以便让NGINX能够获取到,并将获取到的客户端真实IP替换代理服务器的IP。
其中的选项包括 X-Real-IP 头, X-Forwarded-For 头,或者自定义HTTP 头都可以。另外,还支持通过proxy_protocol协议来获取客户端的真实IP,当然,要使用这个选项,首先需要在nginx上开启proxy_protocol的功能。
2. real_ip_recursive配置指令
语 法:
real_ip_recursive on | off;
默认值:
real_ip_recursive off;
上下文:
http, server, location
本指令用来开启或者关闭通过HTTP相关头获取客户端真实IP的时候是否允许多级代理的情况。
如果禁用递归搜索,与受信任地址之一匹配的原始客户端地址将被请求头字段中由 real_ip_header 指令定义的最后一个地址替换。如果启用递归搜索,与受信任地址之一匹配的原始客户端地址将被请求头字段中最后一个非受信任地址替换。
通过开启递归选项,nginx可以处理客户端和nginx之间经历了多次代理的情况,nginx能够通过设置的 PROXY列表将所有的 PROXY IP剥离掉,而找到第一个不是PROXY的IP作为真实客户端IP。
3. set_real_ip_from配置指令
定义一个或者多个受信任的PROXY服务器的地址,格式如下:
语 法:
set_real_ip_from address | CIDR | unix:;
默认值:
—
上下文:
http, server, location
set_real_ip_from指令可以定义多次,定义的PROXY服务器地址可以用ip/mask的方式指定(即CIDR),也可以用域名的方式指定,还可以指定为”unix:“用来表示信任所有以unix socket形式建立的连接。
举个例子,如下:
set_real_ip_from 192.168.0.0/24;
set_real_ip_from unix:
set_real_ip_from www.test_proxy.com;
先举一个例子:
http {
# ...
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# ...
}
以上例子开启了Real IP功能,nginx将查找X-Forwarded-For头来获取客户端真实IP。
本模块在获取到客户端的真实IP后,它会将与它连接的IP地址替换为客户端真实IP,当然有时候我们还是需要得到PROXY的IP和端口,那么nginx也提供相应的机制来获得,即通过变量的方式来提供这个信息。包括两个变量如下:
{ ngx_string("set_real_ip_from"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_realip_from,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
在以上配置指定引用的回调函数ngx_http_realip_from中,会将解析到的受信任的proxy地址存储到 rlcf->from数组里面。譬如以下代码:
if (ngx_strcmp(value[1].data, "unix:") == 0) {
cidr = ngx_array_push(rlcf->from);
if (cidr == NULL) {
return NGX_CONF_ERROR;
}
cidr->family = AF_UNIX;
return NGX_CONF_OK;
}
将在解析到unix:的时候,会在数组里面添加一个family类型为 AF_UNIX的条目。 对于IP/MASK表示的CDIR,则会解析cdir(同时支持ipv4和ipv6类型的地址)。 对于域名,则nginx会调用ngx_inet_resolve_host来解析域名,需要注意的是,这个解析域名的操作是同步请求,如果解析域名的操作比较慢,有可能导致nginx启动的时候会卡住。解析出来的若干个IP地址将逐个被添加到rlcf->from数组中。
{ ngx_string("real_ip_header"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_http_realip,
NGX_HTTP_LOC_CONF_OFFSET,
0,
NULL },
在以上配置指定引用的回调函数ngx_http_realip中,会解析客户端真实IP的来源类型,包括名字叫X-Real-IP、X-Forwarded-For标准HTTP头或者是自定义HTTP头,也可以指定proxy_protocol,利用proxy_protocol协议透传过来的客户端真实IP。
源码如下:
static char *
ngx_http_realip(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
ngx_http_realip_loc_conf_t *rlcf = conf;
ngx_str_t *value;
if (rlcf->type != NGX_CONF_UNSET_UINT) {
return "is duplicate";
}
value = cf->args->elts;
if (ngx_strcmp(value[1].data, "X-Real-IP") == 0) {
rlcf->type = NGX_HTTP_REALIP_XREALIP;
return NGX_CONF_OK;
}
if (ngx_strcmp(value[1].data, "X-Forwarded-For") == 0) {
rlcf->type = NGX_HTTP_REALIP_XFWD;
return NGX_CONF_OK;
}
if (ngx_strcmp(value[1].data, "proxy_protocol") == 0) {
rlcf->type = NGX_HTTP_REALIP_PROXY;
return NGX_CONF_OK;
}
/* 这里预先计算在http请求头中进行查找的自定义HTTP头名字的哈希值
对于 X-Real-IP和 X-Forwarded-For头,nginx在解析请求头的时候
已经自动设置到r->headers_in对应的字段中了,所以不需要进行查找就可以直接提取到,
所以只有自定义HTTP头才需要计算哈希值
*/
rlcf->type = NGX_HTTP_REALIP_HEADER;
rlcf->hash = ngx_hash_strlow(value[1].data, value[1].data, value[1].len);
rlcf->header = value[1];
return NGX_CONF_OK;
}
{ ngx_string("real_ip_recursive"),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
NGX_HTTP_LOC_CONF_OFFSET,
offsetof(ngx_http_realip_loc_conf_t, recursive),
NULL },
real_ip_recursive配置指令只设置一个标记recursive。
模块的初始化是由ngx_http_realip_init函数来完成的,它在postconfiguration阶段执行。本函数在NGX_HTTP_POST_READ_PHASE阶段挂载了一个回调函数,当nginx读取完请求头的时候,就会回调ngx_http_realip_handler;同时本函数在NGX_HTTP_PREACCESS_PHASE阶段也挂载了一个回调函数,同样是ngx_http_realip_handler函数。
为什么要在两个阶段执行同一个回调函数呢?我想,在一般情况下只要在NGX_HTTP_POST_READ_PHASE阶段执行就可以了。然而,对于配置为自定义的HTTP HEADER头的情况,本模块允许在NGX_HTTP_PREACCESS_PHASE阶段前由其他用户自定义模块来添加这个自定义HTTP HEADER请求头,并由ngx_http_realip_handler在NGX_HTTP_PREACCESS_PHASE阶段进行解析来获取客户端真实IP,这样子实现上就更加灵活了,如何获取客户端真实IP完全可以由用户自行定制,甚至可以放在请求URL或者HTTP BODY中。
当nginx收到进来的请求后,在NGX_HTTP_POST_READ_PHASE阶段会回调ngx_http_realip_handler函数,下面来分析ngx_http_realip_handler函数。
首先会判断是否启用了real ip功能,这个是通过rlcf->from是否为NULL来判断的,如下代码:
rlcf = ngx_http_get_module_loc_conf(r, ngx_http_realip_module);
if (rlcf->from == NULL) {
return NGX_DECLINED;
}
如果是NULL,则返回NGX_DECLINED,跳过本模块的其他逻辑,由nginx执行后续的处理。
接下来是获取上下文信息,如果上下文信息已经存在了,说明当前请求已经由本模块处理过了,直接跳过本模块的逻辑,如下代码:
ctx = ngx_http_realip_get_module_ctx(r);
if (ctx) {
return NGX_DECLINED;
}
下面是对设置的不同的客户端真实IP获取的来源类型来进行处理,分别是X-Real-IP头、X-Forwarded-For头、proxy_protocol协议、其他自定义头,源码如下:
switch (rlcf->type) {
case NGX_HTTP_REALIP_XREALIP:
if (r->headers_in.x_real_ip == NULL) {
return NGX_DECLINED;
}
value = &r->headers_in.x_real_ip->value;
xfwd = NULL;
break;
case NGX_HTTP_REALIP_XFWD:
xfwd = r->headers_in.x_forwarded_for;
if (xfwd == NULL) {
return NGX_DECLINED;
}
value = NULL;
break;
case NGX_HTTP_REALIP_PROXY:
if (r->connection->proxy_protocol == NULL) {
return NGX_DECLINED;
}
value = &r->connection->proxy_protocol->src_addr;
xfwd = NULL;
break;
default: /* NGX_HTTP_REALIP_HEADER */
part = &r->headers_in.headers.part;
header = part->elts;
hash = rlcf->hash;
len = rlcf->header.len;
p = rlcf->header.data;
/* 遍历在http请求头列表中查找自定义头 */
for (i = 0; /* void */ ; i++) {
if (i >= part->nelts) {
if (part->next == NULL) {
break;
}
part = part->next;
header = part->elts;
i = 0;
}
if (hash == header[i].hash
&& len == header[i].key.len
&& ngx_strncmp(p, header[i].lowcase_key, len) == 0)
{
value = &header[i].value;
xfwd = NULL;
goto found;
}
}
/* 如果找不到自定义头,就跳过本模块的其他逻辑 */
return NGX_DECLINED;
}
最后是将获取到的真实客户端IP设置到connection对象c中的sockaddr中,源码如下:
found:
c = r->connection;
addr.sockaddr = c->sockaddr;
addr.socklen = c->socklen;
/* addr.name = c->addr_text; */
if (ngx_http_get_forwarded_addr(r, &addr, xfwd, value, rlcf->from,
rlcf->recursive)
!= NGX_DECLINED)
{
if (rlcf->type == NGX_HTTP_REALIP_PROXY) {
ngx_inet_set_port(addr.sockaddr, c->proxy_protocol->src_port);
}
/* 设置真实客户端IP */
return ngx_http_realip_set_addr(r, &addr);
}
return NGX_DECLINED;
ngx_http_realip_handler函数根据设置的不同类别,获取了可以用来获取客户端真实IP地址所对应的字段以后,就调用ngx_http_get_forwarded_addr来获取IP地址。
需要了解的是,本模块对于X-Real-IP和自定义头,与X-Forwarded-For采用了两种处理策略。对于前者,调用ngx_http_get_forwarded_addr前只是设置value字段,而后者则会设置xfwd字段用于保存所有的X-Forwarded-For头(可能不止一个,而是有多个)。
对于非X-Forwarded-For的请求,在ngx_http_get_forwarded_addr函数中,执行如下逻辑:
if (headers == NULL) {
return ngx_http_get_forwarded_addr_internal(r, addr, value->data,
value->len, proxies,
recursive);
}
而对于X-Forwarded-For的请求,在ngx_http_get_forwarded_addr函数中,这种情况在开启了递归的情况下需要遍历所有同名的X-Forwarded-For头,执行如下逻辑:
/* 将X-Forwarded-For头按照HTTP请求头中列出的顺序倒排序 */
for (h = headers, headers = NULL; h; h = next) {
next = h->next;
h->next = headers;
headers = h;
}
/* iterate over all headers in reverse order */
rc = NGX_DECLINED;
found = 0;
/* 遍历所有X-Forwarded-For头,提取客户端IP地址 */
for (h = headers; h; h = h->next) {
rc = ngx_http_get_forwarded_addr_internal(r, addr, h->value.data,
h->value.len, proxies,
recursive);
if (!recursive) { /* 如果没有开启递归模式,只处理一个X-Forwarded-For头即可 */
break;
}
if (rc == NGX_DECLINED && found) {
rc = NGX_DONE; /* 在开启递归的情况下,返回从后往前数第一个非受信IP地址 */
break;
}
if (rc != NGX_OK) {
break;
}
/* 开启了递归模式的情况下,将继续查找下一个X-Forwarded-For头 */
found = 1;
}
/* 将所有X-Forwarded-For头的顺序恢复原样 */
for (h = headers, headers = NULL; h; h = next) {
next = h->next;
h->next = headers;
headers = h;
}
return rc;
以下是ngx_http_get_forwarded_addr_internal的实现源码:
static ngx_int_t
ngx_http_get_forwarded_addr_internal(ngx_http_request_t *r, ngx_addr_t *addr,
u_char *xff, size_t xfflen, ngx_array_t *proxies, int recursive)
{
u_char *p;
ngx_addr_t paddr;
ngx_uint_t found;
found = 0;
do {
/* ngx_cidr_match函数在匹配到设置的proxy列表中的IP段时返回NGX_OK
否则返回NGX_DECLINED */
if (ngx_cidr_match(addr->sockaddr, proxies) != NGX_OK) {
/* 如果之前没有匹配到,则返回NGX_DECLINED
如果之前有匹配到,则返回NGX_DONE,
表示在开启递归的情况下,返回从后往前数第一个非受信IP地址 */
*/
return found ? NGX_DONE : NGX_DECLINED;
}
/* 反向查找下一个IP地址,多个IP地址之间用空格或者都好隔开 */
for (p = xff + xfflen - 1; p > xff; p--, xfflen--) {
if (*p != ' ' && *p != ',') {
break;
}
}
for ( /* void */ ; p > xff; p--) {
if (*p == ' ' || *p == ',') {
p++;
break;
}
}
/* 根据以上提取到的文本解析出客户端真实IP地址 */
if (ngx_parse_addr_port(r->pool, &paddr, p, xfflen - (p - xff))
!= NGX_OK)
{
return found ? NGX_DONE : NGX_DECLINED;
}
*addr = paddr;
found = 1;
xfflen = p - 1 - xff;
} while (recursive && p > xff); /* 开启递归的情况下就继续扫描 */
return NGX_OK;
}
总结一下:以上代码的逻辑就是在关闭递归的情况下,当和nginx连接的IP在PROXY列表中,则返回HTTP HEADER头中设置的最后一个IP地址,否则就不进行客户端真实IP的设置动作;而在递归开启的情况下,当和nginx连接的IP在PROXY列表中,则在HTTP请求头中设置的从后往前排列的IP地址中,一直找到第一个不在PROXY列表中的IP地址作为客户端真实IP地址,否则不进行客户端真实IP的设置动作。
因此,通过开启递归选项,nginx可以处理客户端和nginx之间经历了多次代理的情况,nginx能够通过设置的 PROXY列表将所有的 PROXY IP剥离掉,而找到第一个不是PROXY的IP作为真实客户端IP。但是无论怎样,都需要确保与nginx发生真实TCP连接的PROXY的地址必须在PROXY列表中,nginx才有可能获取到真实客户端IP地址后再设置真实客户端IP地址。
在获取了客户端真实IP后,本模块调用ngx_http_realip_set_addr函数来设置真实客户端地址。
首先,它需要将IP地址转变为文本,源码如下:
len = ngx_sock_ntop(addr->sockaddr, addr->socklen, text,
NGX_SOCKADDR_STRLEN, 0);
if (len == 0) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
经过上面操作,IP地址的文本信息放在text字符串数组中。 然后是从内存池中分配一个内存空间,用来保存text字符串数组中的内容。源码如下:
p = ngx_pnalloc(c->pool, len);
if (p == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
ngx_memcpy(p, text, len);
设置模块上下文信息,源码如下:
ngx_http_set_ctx(r, ctx, ngx_http_realip_module);
ctx->connection = c;
ctx->sockaddr = c->sockaddr;
ctx->socklen = c->socklen;
ctx->addr_text = c->addr_text;
一旦设置了模块上下文,也就是表明了本次请求本模块已经处理过并设置了客户端真实IP,后续重新进入ngx_http_realip_handler函数就不需要再处理了。
最后将IP地址文本信息设置到连接的地址信息字段中,源码如下:
c->sockaddr = addr->sockaddr;
c->socklen = addr->socklen;
c->addr_text.len = len;
c->addr_text.data = p;
在执行上面源码的操作后,后续在nginx的其他模块中获取客户端IP时就会得到新设置的客户端真实IP地址。
在以上分析过程中,故意跳过了请求结束后的收尾处理工作的处理机制。下面来进行说明:
收尾处理是利用nginx的内存池自身的机制来实现的,源码如下:
cln = ngx_pool_cleanup_add(r->pool, sizeof(ngx_http_realip_ctx_t));
if (cln == NULL) {
return NGX_HTTP_INTERNAL_SERVER_ERROR;
}
......
/* 设置回调函数 */
cln->handler = ngx_http_realip_cleanup;
......
在当前request的内存池中挂载一个回收对象,挂载的回收对象中设置的回调函数将在请求结束的时候,在释放内存池的时候自动被调用。在这里就是ngx_http_realip_cleanup函数。
ngx_http_realip_cleanup将在当前的请求结束的时候被回调,其主要的工作就是恢复在上面设置的客户端真实IP地址。由于一个客户端TCP连接会话可以发起多次HTTP请求,在一次HTTP请求结束后,nginx就需要恢复其连接上下文。
那么为什么要恢复连接上下文呢?将当前连接的客户端真实IP地址一直沿用到下一次请求不可以吗?答案是不可以的。因为PROXY和nginx之间的连接可能会被PROXY复用,而下次请求进来的时候,完全有可能已经不是原来的客户端了,如果按照这个逻辑处理,PROXY复用之前与nginx的连接,就很可能"串台”了。
其源码如下:
static void
ngx_http_realip_cleanup(void *data)
{
ngx_http_realip_ctx_t *ctx = data;
ngx_connection_t *c;
c = ctx->connection;
c->sockaddr = ctx->sockaddr;
c->socklen = ctx->socklen;
c->addr_text = ctx->addr_text;
}
从ngx_http_realip_cleanup函数的源码看到,是没有回收内存的逻辑的,因为内存回收的逻辑是完全由nginx的内存池自动来处理的,我们不需要关心。