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

2018-05-31 11:42:12 +08:00
 knownsec200

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 保——攻击打不死,专接防不住;
3407 次点击
所在节点    Linux
9 条回复
AlexaZhou
2018-05-31 12:26:03 +08:00
VeryNginx 欢迎了解下
0312birdzhang
2018-05-31 12:28:28 +08:00
HttpGuard 欢迎了解一下(
xiaoz
2018-05-31 12:29:17 +08:00
@AlexaZhou 老哥,屏蔽 ip 段的功能出来没有?
ryd994
2018-05-31 12:48:33 +08:00
1. limit_req
2. 你这样获取 IP 根本就是错的。除非明确前面有反代,而且确认请求确实来自反代,而且反代会清理非法 header,否则不能相信 header 里的 IP
ryd994
2018-05-31 12:50:11 +08:00
最后一行才是重点
这是个抗 D 宝的广告
@Livid
oovveeaarr
2018-05-31 12:54:18 +08:00
我觉得这种帖还好,就最后一句小广告,其他是技术分享也不错。
rrfeng
2018-05-31 12:59:14 +08:00
这个写的很详细但是...能吐槽的地方太多了,大家不要用。
a7a2
2018-05-31 13:10:47 +08:00
网上 n 年前已经一大堆解决方案。。。

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

还没有出来 😂  。。。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/459173

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX