1. Nginx模块
大部分的Nginx模块入门的文章,都是从Hello World入手,描述一个简单的模块编写,但在真正的业务场景中,需求会更为复杂。为了平衡复杂度,本文会介绍Nginx自带的一个简单的模块realip入手,教大家如何写一个Nginx的模块。
realip 模块默认是不会编译到Nginx中的,源码编译的时候,使用参数--with-http_realip_module配置即可。
1.1 realip模块解决了什么问题
解决的问题:用于获取真实的客户端IP。思考一个场景:如果Nginx前面还有有一层(或者多层)的方向代理,那么Nginx在TCP连接建立时候获取到的客户端IP是上一层代理的IP,并不是真实的客户端IP。有的逻辑,比如基于IP的安全组或限流等功能需要获取到真实的客户端IP。解决的方法有两种思路: 1. 在传输层上解决,比如proxy protocol的方案 2. 在应用层解决,上一层的代理在请求的Header中带上真实的客户端IP,Nginx可以在应用层上获取到真实的客户端IP。
realip支持了proxy protocol的方案和Header的方案,本文仅介绍基于Header的方案。首先看一下,realip模块的指令有三个:
- set_realip_from : CIDR或IP(或unix domain),如果客户端的IP在此CIDR里面,才认为是可信的,才信任其发过来的Header(X-Forwarded-For)是没有经过伪造的。
- real_ip_header: 真实IP的获取来源。X-Real-IP | X-Forwarded-For|proxy_protocol 或其他Header的名字
- real_ip_recursive: 是否递归获取真实客户端IP。
realip模块支持两个变量:
- $realip_remote_addr: 与Nginx建立TCP连接的客户端地址
- $realip_remote_port: 与Nginx建立TCP连接的客户端端口
1.2 请求的例子
基础的nginx.conf 配置如下:
worker_processes 1;
master_process off;
daemon off;
error_log logs/error.log;
events
{
worker_connections 1024;
}
http
{
server
{
listen 8080;
location /
{
set_real_ip_from 127.0.0.1/32;
set_real_ip_from 2.2.2.2/32;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
default_type text/html;
content_by_lua_block
{
ngx.say("Client IP: " .. ngx.var.remote_addr)
}
}
}
}
此配置中,设置了来自127.0.0.1和2.2.2.2 IP的请求都认为是可信赖的,并且使用了X-Forwarded-For Header作为真实客户端IP的来源。另外real_ip_recursive开启了递归获取真实客户端。
由于127.0.0.1的IP是Nginx信任的,因此Nginx取出了X-Forwarded-For中的IP作为真实客户端IP。
由于127.0.0.1的IP是Nginx信任的,2.2.2.2也是Nginx信任的,并且real_ip_recursive 为on,因此Nginx认为真实客户端是2.2.2.3,而不是2.2.2.2。在没有开启real_ip_recursive的时候(为off),Nginx认为的真实客户端是2.2.2.2,因为默认会取最后一个IP作为客户端IP(最后一个IP的定义是,X-Forwarded-For的value中最右边一个IP,因为X-Forwarded-For格式是:hoop1,hoop2,hoop3,...., last hoop)。作为对比,我们关闭real_ip_recursive,就可以看到Nginx认为真实的客户端就是2.2.2.2了。
另外演示一下,客户端的IP(连接Nginx的客户端)不属于Nginx信任的范围的结果:
我们可以看到,Nginx并不信任来自于10.0.2.15的请求,因此客户端IP也是认为是10.0.2.15。
PS: 此演示中,客户端和Nginx都部署在一个机器中。因此,使用不同的IP访问Nginx,那么客户端的IP也会跟着变化。机器上的网卡信息如下:
补充:
以xff的头是client,proxy1, proxy2为例子,为什么Nginx的realip模块是使用xff最右边的IP作为真实的客户端IP proxy2(在trusted的范围内的时候),但是按照xff的标准,应该是使用最左边client作为客户端IP?
这样是因为xff的头部有可能是伪造的,所以默认是使用最右边的。但是如果想要获取到最左边就需要real_ip_recursive 为on,并且proxy1和proxy2都在可信任的范围内,这样形成的信任链。
详细阅读:serverfault.com/questions/3…
2. 源码分析
realip模块的代码入口是在ngx_http_realip_module中,里面定义了模块的context和指令。context主要是定义了配置文件解析过程(解析前/解析/解析后)各个阶段需要做的动作。
nginx_http_realip_module_ctx中,在配置文件解析前会调用ngx_http_realip_add_variables注册realip模块的两个变量。ngx_http_realip_init是负责往HTTP处理的流程中注册hook,表明在哪些阶段执行回调,可以看出,在一个请求的生命周期中,ngx_http_realip_handler会被执行两次。
static ngx_int_t
ngx_http_realip_init(ngx_conf_t *cf)
{
ngx_http_handler_pt *h;
ngx_http_core_main_conf_t *cmcf;
cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
h = ngx_array_push(&cmcf->phases[NGX_HTTP_POST_READ_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
// 在请求的Post read阶段需要执行ngx_http_realip_handler
*h = ngx_http_realip_handler;
h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers);
if (h == NULL) {
return NGX_ERROR;
}
// 在pre access阶段执行ngx_http_realip_handler
*h = ngx_http_realip_handler;
return NGX_OK;
}
ngx_http_realip_commands 里面定义的realip模块支持的三个指令(set_real_ip_from/real_ip_header/real_ip_recursive)。
static ngx_command_t ngx_http_realip_commands[] = {
// set_real_ip_from的指令在nginx.conf出现的时候,执行ngx_http_realip_from配置
{ 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_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_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 },
ngx_null_command
};
可以看出,一个模块一般是有三大部分组成:
(1)指令: 用于从配置文件中配置相关的参数
(2) 变量:用于于其他模块交互,或者保留运行信息
(3)在请求阶段(任意阶段)的Hook函数,负责执行此模块的逻辑代码
除了此三大部分外,其余的就是关于配置文件的解析流程,配置文件的解析也是一个复杂的过程,本文不做展开。