V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
knownsec200
V2EX  ›  Linux

基于 Nginx && Lua 的简易 CC 防护方案

  •  
  •   knownsec200 · 2018-05-31 11:42:12 +08:00 · 3251 次点击
    这是一个创建于 2129 天前的主题,其中的信息可能已经有所发展或是发生改变。

    0×00 前言

    1.CC 攻击简述
    	CC 攻击(Challenge Collapsar)是常见网站应用层攻击的一种,目的是消耗服务器资源,降低业务响应效率;极端情况会让站点无法正常提供服务;
    2.本文要点
    	旨在描述,通过 ngx_lua 模块开发并集成基于令牌桶算法的简易 IP 限速功能,实现 CC 攻击的防护;
    3.本文面向的人群
    	有一定的运维、开发基础的人群;
    	
    

    0×01 服务部署

    0.环境
    	a.系统
    		CentOS Linux release 7.5.1804 (Core);
    		
    	b.资源存放目录
    		mkdir /root/ngx_lua
    		
    	c.要求
    		各种编译安装相关依赖和报错请 google 解决;
    		
    	d.NGX_LUA 官文
    		https://github.com/openresty/lua-nginx-module#installation
    		
    	e.准备
    		cd /root/ngx_lua
    	
    1.Lua
    	wget http://www.lua.org/ftp/lua-5.3.4.tar.gz
    	tar zxf lua-5.3.4.tar.gz
    	cd lua-5.3.4
    	make linux test
    	cd ..
    	
    2.LuaJIT 2.1
    	wget http://luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz
    	tar zxvf LuaJIT-2.1.0-beta3.tar.gz
    	cd LuaJIT-2.1.0-beta3
    	
    	#指定安装目录
    	make PREFIX=/usr/local/luajit2
    	make install PREFIX=/usr/local/luajit2
    	cd ..
    	
    3.NDK
    	wget https://github.com/simplresty/ngx_devel_kit/archive/v0.3.1rc1.tar.gz
    	tar zxvf v0.3.1rc1.tar.gz
    	
    4.LUA_NGX
    	wget https://github.com/openresty/lua-nginx-module/archive/v0.10.13.tar.gz
    	tar zxvf v0.10.13.tar.gz
    	
    5.LUA_RESTY_REDIS
    	wget -O "lua-resty-redis-master.zip" https://codeload.github.com/openresty/lua-resty-redis/zip/master
    	unzip lua-resty-redis-master.zip
    	cd lua-resty-redis-master
    	make install PREFIX=/usr/local/lua-redis
    	cd ..
    	
    5.REDIS
    	wget http://download.redis.io/releases/redis-4.0.9.tar.gz
    	tar zxvf redis-4.0.9.tar.gz
    	cd redis-4.0.9
    	
    	#复制配置文件模板
    	cp redis.conf /etc/
    	
    	#编译安装
    	make PREFIX=/usr/local/redis
    	make install PREFIX=/usr/local/redis
    	
    	#尝试运行,可以考虑打包为后台服务或托管给 supervisor,本文略;
    	cd /usr/local/redis/bin
    	./redis-server /etc/redis.conf
    	
    6.Nginx
    	#添加 NGINX 用户
    	useradd -s /sbin/nologin www
    	
    	#下载、解压并进入目录
    	wget http://nginx.org/download/nginx-1.13.12.tar.gz
    	tar zxvf nginx-1.13.12.tar.gz
    	cd nginx-1.13.12
    	
    	# 增加环境变量
    	export LUAJIT_LIB=/usr/local/luajit2/lib
    	export LUAJIT_INC=/usr/local/luajit2/include/luajit-2.1
    	
    	# 编译安装
    	./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_gzip_static_module --with-http_sub_module  --with-ld-opt="-Wl,-rpath,/usr/local/luajit2/lib" --add-dynamic-module=/root/ngx_lua/ngx_devel_kit-0.3.1rc1 --add-dynamic-module=/root/ngx_lua/lua-nginx-module-0.10.13
    	make && make test
    	
    	# 编辑主配置文件使其支持 NGX_LUA
    	vim /usr/local/nginx/conf/nginx.conf
    		
    		# 指定为其创建的用户
    		user www www;
    		
    		# 指定进程数及将进程绑定至 CPU 核心;
    		worker_processes  auto;
    		worker_cpu_affinity auto;
    
    		pid        logs/nginx.pid;
    		
    		# 打开文件数
    		worker_rlimit_nofile    65535;
    		
    		# 此处加载 LUA 相关模块
    		load_module modules/ndk_http_module.so;
    		load_module modules/ngx_http_lua_module.so;
    		
    		events {
    			use epoll;
    			worker_connections  65535;
    			accept_mutex off;
    			multi_accept on;
    		}
    
    
    		http {
    			include       mime.types;
    			default_type  application/octet-stream;
    
    			server_names_hash_bucket_size       128;
    			client_header_buffer_size   64k;
    			large_client_header_buffers 4       32k;
    			client_max_body_size        512m;
    			
    			# lua redis 依赖包
    			lua_package_path "/usr/local/lua-redis/lib/lua/?.lua;;";
    			
    			sendfile  on;
    			keepalive_timeout  60;
    
    			server_tokens       off;
    			log_format access '$remote_addr - $remote_user [$time_local] "$request" '
    							  '$status $body_bytes_sent "$http_referer" '
    							  '"$http_user_agent" "$http_x_forwarded_for"';
    
    			include conf.d/*.conf;
    		}
    	:wq
    	nginx -t && nginx
    

    0×02 开发 LUA 响应体及建立 VHOST

    1.建立 lua 脚本存放目录
    	mkdir /usr/local/nginx/conf/lua
    	
    2.开发用于响应内容的 lua 脚本
    	vim /usr/local/nginx/conf/lua/content.lua
    		--获取请求的 HEADER
    		local headers = ngx.req.get_headers()
    		
    		--依次通过 x_real_ip,x_forwarded_for,remote_addr 获取客户端 IP
    		local clientip = headers["X-Real-IP"]
    		if clientip == nil then
    		   clientip = headers["x_forwarded_for"]
    		end
    		if clientip == nil then
    		   clientip = ngx.var.remote_addr
    		end
    		
    		--指定响应内容
    		ngx.say("Your Ip Adress is ",clientip,", WelCome!")
    	:wq
    	
    3.搭建用于测试的 VHOST
    	a.新建配置文件
    		vim /usr/local/nginx/conf/conf.d/luatest.conf
    			server
    			{
    				#指定监听端口及主机名
    				listen 80;
    				server_name www.knownsec.com;
    				
    				#建立测试地址
    				location /lua_test
    				{
    						# 指定响应的默认 MIME 类型
    						default_type "text/html";
    
    						# 通过 lua 响应内容
    						content_by_lua_file conf/lua/index.lua;
    				}
    
    				error_log  /home/log/ngx/error.log;
    				access_log  /home/log/ngx/access access;
    			}
    		:wq
    		
    	b.测试并重载配置
    	nginx -t
    	nginx -s reload
    	
    4.测试访问
    	curl --resolve www.knownsec.com:80:192.168.0.196 http://www.knownsec.com/lua_test
    	Your Ip Adress is 192.168.0.196, WelCome!
    

    0×03 IP 限速实现原理

    1.请求处理过程
    	a.NGINX 的请求处理过程一共划分为 11 个阶段,分别是:post-read、server-rewrite、find-config、rewrite、post-rewrite、preaccess、access、post-access、try-files、content、log.(参考: https://github.com/nginx/nginx/blob/master/src/http/ngx_http_core_module.h )
    	b.在 nginx 官方文档(参考: https://www.nginx.com/resources/wiki/modules/lua/)中,可处理阶段均有相应的 lua 指令;就本文的目的而言,访问限速控制处于 access 阶段,所以需要使用的指令为 access_by_lua ;
    	c.为了方便调试和管理,可以使用 access_by_lua_file 指令直接加载指定路径下的 lua 文件来对 access 过程进行处理;
    
    2.基于令牌桶算法的逻辑
    	a.令牌桶算法可控制请求的数量,并允许突发大量请求的情况。
    	b.当用户请求 Nginx 时,判断该 location 是否需要限制流量;
    	c.若需要,则检查当前 IP 是否已有令牌桶,若无则使用 setex 往 redis 中放入令牌桶,并指定的令牌数量及“桶”过期时间;设定单位时间内允许访问次数,比如 1 分钟允许 10 次,则令牌数量为 9(当前请求算作首次消耗),过期时间 60s
    	d.若已有令牌桶,并且令牌数量大于 0,则使用 decr 使其值减 1(消耗令牌)并放行;
    	e.若令牌数量为 0,则拦截;
    	
    3.代码实现
    	vim /usr/local/lua-redis/lib/lua/LimitRate.lua
    	
    		--加载 REDIS 模块
    		local r_md = require "resty.redis"
    		
    		--获取当前请求的 HEADER
    		local headers = ngx.req.get_headers()
    		
    		--指定 REDIS 的地址和端口
    		local redis_ip = "127.0.0.1"
    		local redis_port = "9600"
    		
    		--建立 redis 实例
    		local redis = r_md:new()
    		
    		--指定单位时间
    		local qtrange = 60
    		
    		--允许访问的次数
    		local qcount = 10
    
    		--尝试根据 HTTP 头遂级获取 IP
    		local clientip = headers["X-Real-IP"]
    		if clientip == nil then
    		   clientip = headers["x_forwarded_for"]
    		end
    		if clientip == nil then
    		   clientip = ngx.var.remote_addr
    		end
    
    		--释放 redis 连接的函数
    		local function redis_close(red)
    
    			--释放连接,使用 set_keepalive 指令将当前连接放入当前进程的连接池中待用,需要指定连接池的大小及其中各个连接的空闲超时时间;
    			local pool_max_idle_time = 10000 --毫秒
    			local pool_size = 100 --连接池大小
    			local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    
    			if not ok then
    				ngx_log(ngx_ERR, "set redis keepalive error : ", err)
    			end
    		end
    		
    		--指定所有 REDIS 操作的超时时间,其中包含了连接超时
    		redis:set_timeout(1000)
    		
    		建立连接,若异常则释放连接;
    		local ok, wrong = redis:connect(redis_ip,redis_port)
    		if not ok then
    				redis_close(redis)
    		end
    
    		--建立限速类
    		LimitIpRate = {}
    
    		--限速方法,令牌桶限速逻辑实现
    		function LimitIpRate:is_limited()
    		
    				--尝试获取当前 IP 的令牌桶,令牌桶命名为“ x.x.x.x|pool ”
    				local res, err = redis:get(clientip.."|pool")
    				if not res then
    						ngx.log(ngx.ERR,"lua error: ",err)
    				end
    
    				--如果 res 不为空并且类型为 string,则代表获取到了对应的令牌桶
    				if type(res) == "string" then
    				
    						--可用令牌数量为 0 则拦截
    						if tonumber(res) == 0 then
    								ngx.exit(ngx.HTTP_FORBIDDEN)
    								
    						--反之放行并让令牌数量减少 1
    						else
    								add,err = redis:decr(clientip.."|pool")
    								if not add then
    										ngx.log(ngx.ERR,"lua error: ",err)
    								end
    						end
    				
    				--如果 res 不为空并且类型为 userdata,则代表 redis 中没有对应令牌桶
    				elseif type(res) == "userdata" then
    						
    						--往 redis 中放入指定名称、过期时间、值的键值对
    						ini, err = redis:setex(clientip.."|pool", qtrange, (qcount-1))
    						if not ini then
    								ngx.log(ngx.ERR,"lua error: ",err)
    						end
    				end
    		end
    		
    		--调用限速方法
    		LimitIpRate.is_limited()
    			
    	:wq
    	
    4.将 LimitRate.lua 集成进 NGINX 配置文件
    	a.编辑配置文件,加入指令
    		vim /usr/local/nginx/conf/conf.d/luatest.conf
    			server
    			{
    				#指定监听端口及主机名
    				listen 80;
    				server_name www.knownsec.com;
    				
    				#建立测试地址
    				location /lua_test
    				{
    						# 指定响应的默认 MIME 类型
    						default_type "text/html";
    						
    						# 通过 lua 对 access 进行过滤
    						access_by_lua_file "conf/lua/LimitRate.lua";
    						
    						# 通过 lua 返回响应内容
    						content_by_lua_file conf/lua/index.lua;
    				}
    
    				error_log  /home/log/ngx/error.log;
    				access_log  /home/log/ngx/access access;
    			}
    		:wq
    	b.测试并重载配置
    		nginx -t
    		nginx -s reload
    	
    5.限速测试
    	for i in {1..12}; do curl -s --resolve www.knownsec.com:80:192.168.0.196 http://www.knownsec.com/lua_test -o /dev/null -w %{http_code};echo ;done
    	200
    	200
    	200
    	200
    	200
    	200
    	200
    	200
    	200
    	200
    	403
    	403
    

    0×04 总结

    a.在需要的 location 使用 ngx_lua 指定加载 LimitRate.lua ,并指定单位时间和单位时间中允许的请求次数;则可实现对该 location 请求的 IP 限速;
    b.截止目前,仅是简单地描述了如何实现 IP 限速控制,还需要结合更多的业务环境来开发不同的需求,才能逐渐构建相对成熟的 CC 防护体系;
    c.攻防之根本即为攻防双方对成本的投入;
    d.若需成熟解决方案,可选择抗 D 保——攻击打不死,专接防不住;
    
    9 条回复    2018-05-31 19:27:37 +08:00
    AlexaZhou
        1
    AlexaZhou  
       2018-05-31 12:26:03 +08:00
    VeryNginx 欢迎了解下
    0312birdzhang
        2
    0312birdzhang  
       2018-05-31 12:28:28 +08:00
    HttpGuard 欢迎了解一下(
    xiaoz
        3
    xiaoz  
       2018-05-31 12:29:17 +08:00 via Android
    @AlexaZhou 老哥,屏蔽 ip 段的功能出来没有?
    ryd994
        4
    ryd994  
       2018-05-31 12:48:33 +08:00 via Android
    1. limit_req
    2. 你这样获取 IP 根本就是错的。除非明确前面有反代,而且确认请求确实来自反代,而且反代会清理非法 header,否则不能相信 header 里的 IP
    ryd994
        5
    ryd994  
       2018-05-31 12:50:11 +08:00 via Android
    最后一行才是重点
    这是个抗 D 宝的广告
    @Livid
    oovveeaarr
        6
    oovveeaarr  
       2018-05-31 12:54:18 +08:00
    我觉得这种帖还好,就最后一句小广告,其他是技术分享也不错。
    rrfeng
        7
    rrfeng  
       2018-05-31 12:59:14 +08:00 via Android
    这个写的很详细但是...能吐槽的地方太多了,大家不要用。
    a7a2
        8
    a7a2  
       2018-05-31 13:10:47 +08:00
    网上 n 年前已经一大堆解决方案。。。

    无论怎么优秀的反 ddos 代码都有误封几率。所以一定要使用触发反 cc 机制,例如连接数大于多少或者系统某资源占用多于多少才触发。。
    AlexaZhou
        9
    AlexaZhou  
       2018-05-31 19:27:37 +08:00
    @xiaoz

    还没有出来 😂  。。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2946 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 14:10 · PVG 22:10 · LAX 07:10 · JFK 10:10
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.