Ubuntu 下 Nginx+Lua 实现动态高频IP封禁

背景

前阵子遇上了类似ddos的攻击, 为了防止某恶意用户多次对服务器端口进行攻击,我们需要建立一个动态的 IP 黑名单。对于黑名单之内的 IP ,拒绝提供服务。

实现 IP 黑名单的功能有很多途径:

在操作系统层面,配置 iptables,拒绝指定 IP 的网络请求;
在 Web Server 层面,通过 Nginx 自身的 deny 选项 或者 lua 插件 配置 IP 黑名单;
在应用层面,在请求服务之前检查一遍客户端 IP 是否在黑名单。
为了方便管理和共享,我们通过 Nginx+Lua+Redis 的架构实现 IP 黑名单和 IP 白名单的功能。

本方法不用openresty方式, 如果用的openresty会更简便, 但是由于本机已经安装了nginx, 则选择后加lua和nginx-lua-module的方式搭配使用

Nginx和Lua的环境安装教程

在已经安装好nginx、lua 和nginx的lua模块后, 还需要下载一个openresty的lua-resty-redis模块, 实现操作redis功能的一些库

安装lua-resty-redis模块

方法1:

#下载包
wget https://github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz
#解压包
tar -zxvf v0.29.tar.gz
#将解压出来的包中的lib/resty解压到nginx文件夹下的lua文件夹
cp -r lua-resty-redis-0.29/lib/resty $NGINX_INSTALL_PATH/lua/

方法2:

#git clone
git clone https://github.com/openresty/lua-resty-redis.git
#将包中的lib/resty解压到nginx文件夹下的lua文件夹
cp -r lib/resty $NGINX_INSTALL_PATH/lua/
$NGINX_INSTALL_PATH 为nginx的目录, 复制到这个目录后续nginx的配置文件需要引入这个目录的.lua文件

配置nginx.conf

这里用一个默认的nginx配置default.conf举例。 需要指明lua库的路径, 加在server{}外部

lua_package_path "/etc/nginx/lua/?.lua;;";

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }
        location /lua {
            set $business "lua";  #加这个的目的是为了知道存在redis的数据是哪个location的,也可以不加
            access_by_lua_file /etc/nginx/ip.lua;  #lua脚本位置
            default_type 'text/html';
            content_by_lua 'ngx.say("hello, lua!~~~~~~~~")';
        }

 }

/etc/nginx/lua/?.lua是引入lua的拓展包, 其中的/etc/nginx/lua是lua的拓展包目录

set $business "lua" 是接下来的lua脚本传入的类似标识区分的字符, 这样做可以给redis缓存加一个标识, 有效区分不同项目或功能

access_by_lua_file /etc/nginx/ip.lua; 值的是执行的lua脚本

lua脚本

下面是lua脚本ip.lua, 文件位置位于/etc/nginx/

-- 封禁IP时间(秒)
local ip_block_time=300


-- 指定ip访问频率时间段(秒)
local ip_time_out=10


-- 指定ip访问频率计数最大值(秒)
local ip_max_count=1


-- nginx的location中定义的业务标识符,也可以不加,不过加了后方便区分
local BUSINESS = ngx.var.business


-- ip白名单(不需要拦截)
local whiteList = {"127.111.111.111"}


-- 连接redis
local redis = require "resty.redis"  
local conn = redis:new()  
ok, err = conn:connect("127.0.0.1", 6379)
-- 超时时间2秒
conn:set_timeout(2000)
-- 如果连接失败,跳转到脚本结尾
if not ok then
    goto FLAG
end


-- 判断ip是否在白名单内, 如果在则不做记录和拦截
-- 方法1 直接往whiteList添加元素, 键值对的形式, 比如whiteList["127.0.0.1"] = 1
-- if whiteList[ngx.var.remote_addr] ~= nil then
--    goto FLAG
-- end
-- 方法二 利用循环判断一维数组值是否存在, whiteList的定义为{"127.111.111.111"}
for k,v in ipairs(whiteList) do
  if v == ngx.var.remote_addr then
      goto FLAG
  end
end


-- 查询ip是否被禁止访问,如果存在则返回403错误代码
is_block, err = conn:get(BUSINESS.."-BLOCK-"..ngx.var.remote_addr)  
if is_block == '1' then
    ngx.exit(403)
    goto FLAG
end
-- 查询redis中保存的ip的计数器
ip_count, err = conn:get(BUSINESS.."-COUNT-"..ngx.var.remote_addr)
-- 如果不存在,则将该IP存入redis,并将计数器设置为1、该KEY的超时时间为ip_time_out
if ip_count == ngx.null then 
    res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr, 1)
    res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
else
-- 存在则将单位时间内的访问次数加1
    ip_count = ip_count + 1
-- 如果超过单位时间限制的访问次数,则添加限制访问标识,限制时间为ip_block_time
    if ip_count >= ip_max_count then
        res, err = conn:set(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, 1)
        res, err = conn:expire(BUSINESS.."-BLOCK-"..ngx.var.remote_addr, ip_block_time)
    else
        res, err = conn:set(BUSINESS.."-COUNT-"..ngx.var.remote_addr,ip_count)
        res, err = conn:expire(BUSINESS.."-COUNT-"..ngx.var.remote_addr, ip_time_out)
    end
end
-- 结束标记
::FLAG::
local ok, err = conn:close()

按照脚本规定, 十秒内访问一次, 第二次则直接返回403, 测试效果如预期; 5分钟后重新访问则又可访问一次

403.png

也可以通过服务端的redis客户端, 查看当前的被记录的ip
redis.png
添加新评论