From 902fe6ad078af8e3d4c1a0fc95bba96611dfe8c0 Mon Sep 17 00:00:00 2001 From: bunkerity Date: Thu, 16 Feb 2023 16:42:38 +0100 Subject: [PATCH] bw - init work on redis --- src/bw/lua/clusterstore.lua | 56 ++++++ src/common/confs/server-http/access-lua.conf | 28 +++ src/common/core/badbehavior/badbehavior.lua | 185 ++++++++++++++----- src/common/core/redis/plugin.json | 72 ++++++++ src/common/core/redis/redis.lua | 28 +++ 5 files changed, 326 insertions(+), 43 deletions(-) create mode 100644 src/bw/lua/clusterstore.lua create mode 100644 src/common/core/redis/plugin.json create mode 100644 src/common/core/redis/redis.lua diff --git a/src/bw/lua/clusterstore.lua b/src/bw/lua/clusterstore.lua new file mode 100644 index 000000000..eaf0bc327 --- /dev/null +++ b/src/bw/lua/clusterstore.lua @@ -0,0 +1,56 @@ +local M = {} +local redis = require "resty.redis" +local utils = require "utils" + +function M:connect() + -- Instantiate object + local redis_client, err = redis:new() + if redis_client == nil then + return false, err + end + -- Get variables + local variables = { + ["REDIS_HOST"] = "", + ["REDIS_PORT"] = "", + ["REDIS_SSL"] = "", + ["REDIS_TIMEOUT"] = "", + ["REDIS_KEEPALIVE_IDLE"] = "", + ["REDIS_KEEPALIVE_POOL"] = "" + } + for k, v in pairs(variables) do + local value, err = utils.get_variable(k) + if value == nil then + return false, err + end + variables[k] = value + end + -- Set timeouts + redis_client:set_timeouts(tonumber(variables["REDIS_TIMEOUT"]), tonumber(variables["REDIS_TIMEOUT"]), tonumber(variables["REDIS_TIMEOUT"])) + -- Connect + local options = { + ["ssl"] = false + } + if variables["REDIS_SSL"] == "yes" then + options["ssl"] = true + end + return redis.connect(variables["REDIS_HOST"], tonumber(variables["REDIS_PORT"]), options) +end + +function M:close(redis_client) + -- Get variables + local variables = { + ["REDIS_KEEPALIVE_IDLE"] = "", + ["REDIS_KEEPALIVE_POOL"] = "" + } + for k, v in pairs(variables) do + local value, err = utils.get_variable(k) + if value == nil then + return false, err + end + variables[k] = value + end + -- Equivalent to close but keep a pool of connections + return redis_client:set_keepalive(tonumber(variables["REDIS_KEEPALIVE_IDLE"]), tonumber(variables["REDIS_KEEPALIVE_POOL"])) +end + +return M \ No newline at end of file diff --git a/src/common/confs/server-http/access-lua.conf b/src/common/confs/server-http/access-lua.conf index cb0fcb417..fd6e84868 100644 --- a/src/common/confs/server-http/access-lua.conf +++ b/src/common/confs/server-http/access-lua.conf @@ -19,6 +19,34 @@ if banned then logger.log(ngx.WARN, "ACCESS", "IP " .. ngx.var.remote_addr .. " is banned with reason : " .. banned) ngx.exit(utils.get_deny_status()) end +-- Redis case +local use_redis = utils.get_variable("USE_REDIS") +if use_redis == "yes" then + -- Connect + local redis_client, err = clusterstore:connect() + if not redis_client then + logger.log(ngx.ERR, "ACCESS", "can't connect to redis server : " .. err) + else + -- Get ban + local ban, err = redis_client:get("ban_" .. ngx.var.remote_addr) + if err then + logger.log(ngx.ERR, "ACCESS", "GET failed : " .. err) + elseif ban then + -- Get TTL + local ttl, err = redis_client:ttl("ban_" .. ngx.var.remote_addr) + if not ttl then + logger.log(ngx.ERR, "ACCESS", "TTL failed : " .. err) + else + local ok, err = datastore:set("bans_ip_" .. ip, ban, ttl) + if not ok then + logger.log(ngx.ERR, "ACCESS", "can't save ban to the datastore : " .. err) + return + end + end + end + redis_client:close() + end +end -- List all plugins local list, err = plugins:list() diff --git a/src/common/core/badbehavior/badbehavior.lua b/src/common/core/badbehavior/badbehavior.lua index 457e9539d..108f528fc 100644 --- a/src/common/core/badbehavior/badbehavior.lua +++ b/src/common/core/badbehavior/badbehavior.lua @@ -1,74 +1,173 @@ local _M = {} _M.__index = _M -local utils = require "utils" -local datastore = require "datastore" -local logger = require "logger" -local cjson = require "cjson" +local utils = require "utils" +local datastore = require "datastore" +local logger = require "logger" +local cjson = require "cjson" +local clusterstore = require "clusterstore" function _M.new() local self = setmetatable({}, _M) return self, nil end +function _M.increase(premature, use_redis, ip, count_time, ban_time, threshold) + -- Local case + local counter, err = datastore:get("plugin_badbehavior_count_" .. ip) + if not counter and err ~= "not found" then + return false, "can't get counts from the datastore : " .. err + end + if counter == nil then + counter = 0 + end + counter = counter + 1 + -- Redis case + if use_redis then + -- Connect to server + local redis_client, err = clusterstore:connect() + if not redis_client then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) Can't connect to redis server : " .. err) + return + end + -- Start transaction + local ok, err = redis_client:multi() + if not ok then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) Can't start transaction : " .. err) + redis_client:close() + return + end + -- Increment counter + counter, err = redis_client:incr("bad_behavior_" .. ip) + if not counter then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) INCR failed : " .. err) + redis_client:close() + return + end + -- Expires counter + local expire, err = redis_client:expire("bad_behavior_" .. ip, count_time) + if not counter then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) EXPIRE failed : " .. err) + redis_client:close() + return + end + -- Add IP to redis bans if needed + if counter > threshold then + local ban, err = redis_client:set("ban_" .. ip, "bad behavior", "EX", ban_time) + if not ban then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) SET failed : " .. err) + redis_client:close() + return + end + end + -- Exec transaction + local exec, err = redis_client:exec() + if not exec then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) EXEC failed : " .. err) + redis_client:close() + return + end + for i, v in ipairs(exec) do + if v[1] == false then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) Transaction failed : " .. v[2]) + redis_client:close() + return + end + end + -- End connection + redis_client:close() + end + -- Store local counter + local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, counter) + if not ok then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) can't save counts to the datastore : " .. err) + return + end + -- Store local ban + if counter > threshold then + local ok, err = datastore:set("bans_ip_" .. ip, "bad behavior", ban_time) + if not ok then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) can't save ban to the datastore : " .. err) + return + end + logger.log(ngx.WARN, "BAD-BEHAVIOR", "IP " .. ip .. " is banned for " .. ban_time .. "s (" .. tostring(counter) .. "/" .. tostring(threshold) .. ")") + end + -- Call decrease later + local ok, err = ngx.timer.at(count_time, _M.decrease, use_redis, ip) + if not ok then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(increase) can't create decrease timer : " .. err) + end +end + +function _M.decrease(premature, use_redis, ip) + -- Decrease from local store + local count, err = datastore:get("plugin_badbehavior_count_" .. ip) + if err then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease) Can't get counts from the datastore : " .. err) + return + end + if not count then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease) Count is null") + return + end + local new_count = count - 1 + if new_count <= 0 then + datastore:delete("plugin_badbehavior_count_" .. ip) + return + end + local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, new_count) + if not ok then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease) Can't save counts to the datastore : " .. err) + return + end + -- Decrease from redis + if use_redis then + -- Connect to server + local redis_client, err = clusterstore:connect() + if not redis_client then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease) Can't connect to redis server : " .. err) + return + end + -- Decrement counter + local counter, err = redis_client:decr("bad_behavior_" .. ip) + if not counter then + logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease) DECR failed : " .. err) + redis_client:close() + return + end + redis_client:close() + end +end + function _M:log() + -- Get vars self.use = utils.get_variable("USE_BAD_BEHAVIOR") self.ban_time = utils.get_variable("BAD_BEHAVIOR_BAN_TIME") self.status_codes = utils.get_variable("BAD_BEHAVIOR_STATUS_CODES") self.threshold = utils.get_variable("BAD_BEHAVIOR_THRESHOLD") self.count_time = utils.get_variable("BAD_BEHAVIOR_COUNT_TIME") + self.use_redis = utils.get_variable("USE_REDIS") + -- Check if bad behavior is activated if self.use ~= "yes" then return true, "bad behavior not activated" end + -- Check if we have a bad status code if not self.status_codes:match(tostring(ngx.status)) then return true, "not increasing counter" end + -- Check if we are whitelisted if ngx.var.is_whitelisted == "yes" then return true, "client is whitelisted" end - local count, err = datastore:get("plugin_badbehavior_count_" .. ngx.var.remote_addr) - if not count and err ~= "not found" then - return false, "can't get counts from the datastore : " .. err + -- Call increase function later and with cosocket enabled + local use_redis = false + if self.use_redis == "yes" then + use_redis = true end - local new_count = 1 - if count ~= nil then - new_count = count + 1 - end - local ok, err = datastore:set("plugin_badbehavior_count_" .. ngx.var.remote_addr, new_count) - if not ok then - return false, "can't save counts to the datastore : " .. err - end - local function decrease_callback(premature, ip) - local count, err = datastore:get("plugin_badbehavior_count_" .. ip) - if err then - logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease_callback) Can't get counts from the datastore : " .. err) - return - end - if not count then - logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease_callback) Count is null") - return - end - local new_count = count - 1 - if new_count <= 0 then - datastore:delete("plugin_badbehavior_count_" .. ip) - return - end - local ok, err = datastore:set("plugin_badbehavior_count_" .. ip, new_count) - if not ok then - logger.log(ngx.ERR, "BAD-BEHAVIOR", "(decrease_callback) Can't save counts to the datastore : " .. err) - end - end - local hdr, err = ngx.timer.at(tonumber(self.count_time), decrease_callback, ngx.var.remote_addr) + local ok, err = ngx.timer.at(0, _M.increase, use_redis, ngx.var.remote_addr, tonumber(self.count_time), tonumber(self.ban_time), tonumber(self.threshold)) if not ok then return false, "can't create decrease timer : " .. err end - if new_count > tonumber(self.threshold) then - local ok, err = datastore:set("bans_ip_" .. ngx.var.remote_addr, "bad behavior", tonumber(self.ban_time)) - if not ok then - return false, "can't save ban to the datastore : " .. err - end - logger.log(ngx.WARN, "BAD-BEHAVIOR", "IP " .. ngx.var.remote_addr .. " is banned for " .. tostring(self.ban_time) .. "s (" .. tostring(new_count) .. "/" .. tostring(self.threshold) .. ")") - end return true, "success" end diff --git a/src/common/core/redis/plugin.json b/src/common/core/redis/plugin.json new file mode 100644 index 000000000..ba2c88425 --- /dev/null +++ b/src/common/core/redis/plugin.json @@ -0,0 +1,72 @@ +{ + "id": "redis", + "order": 999, + "name": "Redis", + "description": "Redis server configuration when using BunkerWeb in cluster mode.", + "version": "0.1", + "settings": { + "USE_REDIS": { + "context": "global", + "default": "no", + "help": "Activate Redis.", + "id": "use-redis", + "label": "Activate Redis", + "regex": "^(yes|no)$", + "type": "check" + }, + "REDIS_HOST": { + "context": "global", + "default": "", + "help": "Redis server IP or hostname.", + "id": "redis-host", + "label": "Redis server", + "regex": "^.*$", + "type": "text" + }, + "REDIS_PORT": { + "context": "global", + "default": "6379", + "help": "Redis server port.", + "id": "redis-port", + "label": "Redis port", + "regex": "^[0-9]+$", + "type": "text" + }, + "REDIS_SSL": { + "context": "global", + "default": "no", + "help": "Use SSL/TLS connection with Redis server.", + "id": "redis-ssl", + "label": "Redis SSL/TLS", + "regex": "^(yes|no)$", + "type": "check" + }, + "REDIS_TIMEOUT": { + "context": "global", + "default": "1000", + "help": "Redis server timeout (in ms) for connect, read and write.", + "id": "redis-timeout", + "label": "Redis timeout (ms)", + "regex": "^[0-9]+$", + "type": "text" + }, + "REDIS_KEEPALIVE_IDLE": { + "context": "global", + "default": "30000", + "help": "Max idle time (in ms) before closing redis connection in the pool.", + "id": "redis-keepalive-idle", + "label": "Redis keepalive idle (ms)", + "regex": "^[0-9]+$", + "type": "text" + }, + "REDIS_KEEPALIVE_POOL": { + "context": "global", + "default": "10", + "help": "Max number of redis connection(s) kept in the pool.", + "id": "redis-keepalive-pool", + "label": "Redis keepalive pool", + "regex": "^[0-9]+$", + "type": "text" + } + } +} diff --git a/src/common/core/redis/redis.lua b/src/common/core/redis/redis.lua new file mode 100644 index 000000000..b3750da7a --- /dev/null +++ b/src/common/core/redis/redis.lua @@ -0,0 +1,28 @@ +local _M = {} +_M.__index = _M + +local utils = require "utils" +local datastore = require "datastore" +local logger = require "logger" +local cjson = require "cjson" +local resolver = require "resty.dns.resolver" + +function _M.new() + local self = setmetatable({}, _M) + return self, nil +end + +function _M:init() + -- Check if init is needed + local init_needed, err = utils.get_variable("USE_REDIS", "yes") + if init_needed == nil then + return false, "can't check USE_REDIS variable : " .. err + end + if not init_needed then + return true, "redis not used" + end + -- TODO : check redis connectivity + return true, "redis ping successful" +end + +return _M