refactor - blacklist, errors, greylist, letsencrypt and redis

This commit is contained in:
bunkerity 2023-04-14 17:37:30 +02:00
parent 2075a5d4c2
commit 666b7a1bac
7 changed files with 276 additions and 402 deletions

View File

@ -10,7 +10,7 @@ local blacklist = class("blacklist", plugin)
function blacklist:new()
-- Call parent new
local ok, err = plugin.new(self, "antibot")
local ok, err = plugin.new(self, "blacklist")
if not ok then
return false, err
end

View File

@ -5,11 +5,11 @@ local cachestore = require "bunkerweb.cachestore"
local cjson = require "cjson"
local resolver = require "resty.dns.resolver"
local dnsbl = class("country", plugin)
local dnsbl = class("dnsbl", plugin)
local dnsbl:new()
-- Call parent new
local ok, err = plugin.new(self, "country")
local ok, err = plugin.new(self, "dnsbl")
if not ok then
return false, err
end

View File

@ -12,7 +12,6 @@ location = {{ page }} {
{% endfor %}
{% endif %}
{% if INTERCEPTED_ERROR_CODES != "" %}
{% for intercepted_error_code in INTERCEPTED_ERROR_CODES.split(" ") %}
{% if not intercepted_error_code + "=" in ERRORS +%}
@ -27,16 +26,18 @@ location = {{ page }} {
modsecurity off;
default_type 'text/html';
content_by_lua_block {
local logger = require "logger"
local logger = require "bunkerweb.logger"
local errors = require "errors.errors"
local html, err
logger:new("errors")
errors:new()
if ngx.status == 200 then
html, err = errors.error_html(tostring(405))
html, err = errors:error_html(tostring(405))
else
html, err = errors.error_html(tostring(ngx.status))
html, err = errors:error_html(tostring(ngx.status))
end
if not html then
logger.log(ngx.ERR, "ERRORS", "Error while computing HTML error template for {{ intercepted_error_code }} : " .. err)
logger:log(ngx.ERR, "error while computing HTML error template for {{ intercepted_error_code }} : " .. err)
else
ngx.say(html)
end

View File

@ -1,17 +1,21 @@
local _M = {}
_M.__index = _M
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local cachestore = require "bunkerweb.cachestore"
local cjson = require "cjson"
local utils = require "utils"
local datastore = require "datastore"
local logger = require "logger"
local cjson = require "cjson"
local errors = class("errors", plugin)
function _M.new()
local self = setmetatable({}, _M)
return self, nil
function errors:new()
-- Call parent new
local ok, err = plugin.new(self, "errors")
if not ok then
return false, err
end
return true, "success"
end
function _M:init()
function errors:init()
-- Save default errors into datastore
local default_errors = {
["400"] = {
@ -65,12 +69,12 @@ function _M:init()
}
local ok, err = datastore:set("plugin_errors_default_errors", cjson.encode(default_errors))
if not ok then
return false, "can't save default errors to datastore : " .. err
return self:ret(false, "can't save default errors to datastore : " .. err)
end
-- Save generic template into datastore
local f, err = io.open("/usr/share/bunkerweb/core/errors/files/error.html", "r")
if not f then
return false, "can't open error.html : " .. err
return self:ret(false, "can't open error.html : " .. err)
end
local template = f:read("*all")
f:close()
@ -81,7 +85,7 @@ function _M:init()
return true, "success"
end
function _M.error_html(code)
function errors:error_html(code)
-- Load default errors texts
local default_errors, err = datastore:get("plugin_errors_default_errors")
if not default_errors then
@ -98,4 +102,4 @@ function _M.error_html(code)
default_errors[code].body2), "success"
end
return _M
return errors

View File

@ -1,365 +1,232 @@
local _M = {}
_M.__index = _M
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local cjson = require "cjson"
local ipmatcher = require "resty.ipmatcher"
local utils = require "utils"
local datastore = require "datastore"
local logger = require "logger"
local cjson = require "cjson"
local ipmatcher = require "resty.ipmatcher"
local greylist = class("dnsbl", plugin)
function _M.new()
local self = setmetatable({}, _M)
return self, nil
end
function _M:init()
function greylist:new()
-- Call parent new
local ok, err = plugin.new(self, "greylist")
if not ok then
return false, err
end
-- Check if redis is enabled
local use_redis, err = utils.get_variable("USE_REDIS", false)
if not use_redis then
return false, err
end
self.use_redis = use_redis == "yes"
-- Check if init is needed
local init_needed, err = utils.has_variable("USE_GREYLIST", "yes")
if init_needed == nil then
return false, err
end
if not init_needed then
return true, "no service uses Greylist, skipping init"
end
-- Read greylists
local greylists = {
["IP"] = {},
["RDNS"] = {},
["ASN"] = {},
["USER_AGENT"] = {},
["URI"] = {}
}
local i = 0
for kind, _ in pairs(greylists) do
local f, err = io.open("/var/cache/bunkerweb/greylist/" .. kind .. ".list", "r")
if f then
for line in f:lines() do
table.insert(greylists[kind], line)
i = i + 1
end
f:close()
if ngx.get_phase() == "init" then
local init_needed, err = utils.has_variable("USE_GREYLIST", "yes")
if init_needed == nil then
return false, err
end
end
-- Load them into datastore
local ok, err = datastore:set("plugin_greylist_list", cjson.encode(greylists))
if not ok then
return false, "can't store Greylist list into datastore : " .. err
end
return true, "successfully loaded " .. tostring(i) .. " greylisted IP/network/rDNS/ASN/User-Agent/URI"
end
function _M:access()
-- Check if access is needed
local access_needed, err = utils.get_variable("USE_GREYLIST")
if access_needed == nil then
return false, err, false, nil
end
if access_needed ~= "yes" then
return true, "Greylist not activated", false, nil
end
-- Check the cache
local cached_ip, err = self:is_in_cache("ip" .. ngx.var.remote_addr)
if cached_ip and cached_ip ~= "ok" then
return true, "IP is in greylist cache (info = " .. cached_ip .. ")", false, ngx.OK
end
local cached_uri, err = self:is_in_cache("uri" .. ngx.var.uri)
if cached_uri and cached_uri ~= "ok" then
return true, "URI is in greylist cache (info = " .. cached_uri .. ")", false, ngx.OK
end
local cached_ua = true
if ngx.var.http_user_agent then
cached_ua, err = self:is_in_cache("ua" .. ngx.var.http_user_agent)
if cached_ua and cached_ua ~= "ok" then
return true, "User-Agent is in greylist cache (info = " .. cached_ua .. ")", false, ngx.OK
self.init_needed = init_needed
-- Decode lists
else
local lists, err = datastore:get("plugin_greylist_lists")
if not lists then
return false, err
end
self.lists = cjson.decode(lists)
end
if cached_ip and cached_uri and cached_ua then
return true, "full request is in greylist cache (not greylisted)", false, nil
end
-- Get list
local data, err = datastore:get("plugin_greylist_list")
if not data then
return false, "can't get Greylist list : " .. err, false, nil
end
local ok, greylists = pcall(cjson.decode, data)
if not ok then
return false, "error while decoding greylists : " .. greylists, false, nil
end
-- Return value
local ret, ret_err = true, "success"
-- Check if IP is in IP/net greylist
local ip_net, err = utils.get_variable("GREYLIST_IP")
if ip_net and ip_net ~= "" then
for element in ip_net:gmatch("%S+") do
table.insert(greylists["IP"], element)
end
end
if not cached_ip then
local ipm, err = ipmatcher.new(greylists["IP"])
if not ipm then
ret = false
ret_err = "can't instantiate ipmatcher " .. err
else
if ipm:match(ngx.var.remote_addr) then
self:add_to_cache("ip" .. ngx.var.remote_addr, "ip/net")
return ret, "client IP " .. ngx.var.remote_addr .. " is in greylist", false, ngx.OK
end
end
end
-- Check if rDNS is in greylist
local rdns_global, err = utils.get_variable("GREYLIST_RDNS_GLOBAL")
local check = true
if not rdns_global then
logger.log(ngx.ERR, "GREYLIST", "Error while getting GREYLIST_RDNS_GLOBAL variable : " .. err)
elseif rdns_global == "yes" then
check, err = utils.ip_is_global(ngx.var.remote_addr)
if check == nil then
logger.log(ngx.ERR, "GREYLIST", "Error while getting checking if IP is global : " .. err)
end
end
if not cached_ip and check then
local rdns, err = utils.get_rdns(ngx.var.remote_addr)
if not rdns then
ret = false
ret_err = "error while trying to get reverse dns : " .. err
else
local rdns_list, err = utils.get_variable("GREYLIST_RDNS")
if rdns_list and rdns_list ~= "" then
for element in rdns_list:gmatch("%S+") do
table.insert(greylists["RDNS"], element)
end
end
for i, suffix in ipairs(greylists["RDNS"]) do
if rdns:sub(- #suffix) == suffix then
self:add_to_cache("ip" .. ngx.var.remote_addr, "rDNS " .. suffix)
return ret, "client IP " .. ngx.var.remote_addr .. " is in greylist (info = rDNS " .. suffix .. ")", false, ngx.OK
end
end
end
end
-- Check if ASN is in greylist
if not cached_ip then
if utils.ip_is_global(ngx.var.remote_addr) then
local asn, err = utils.get_asn(ngx.var.remote_addr)
if not asn then
ret = false
ret_err = "error while trying to get asn number : " .. err
else
local asn_list, err = utils.get_variable("GREYLIST_ASN")
if asn_list and asn_list ~= "" then
for element in asn_list:gmatch("%S+") do
table.insert(greylists["ASN"], element)
end
end
for i, asn_bl in ipairs(greylists["ASN"]) do
if tostring(asn) == asn_bl then
self:add_to_cache("ip" .. ngx.var.remote_addr, "ASN " .. tostring(asn))
return ret, "client IP " .. ngx.var.remote_addr .. " is in greylist (kind = ASN " .. tostring(asn) .. ")", false,
ngx.OK
end
end
end
end
end
-- IP is not greylisted
local ok, err = self:add_to_cache("ip" .. ngx.var.remote_addr, "ok")
if not ok then
ret = false
ret_err = err
end
-- Check if User-Agent is in greylist
if not cached_ua and ngx.var.http_user_agent then
local ua_list, err = utils.get_variable("GREYLIST_USER_AGENT")
if ua_list and ua_list ~= "" then
for element in ua_list:gmatch("%S+") do
table.insert(greylists["USER_AGENT"], element)
end
end
for i, ua_bl in ipairs(greylists["USER_AGENT"]) do
if ngx.var.http_user_agent:match(ua_bl) then
self:add_to_cache("ua" .. ngx.var.http_user_agent, "UA " .. ua_bl)
return ret, "client User-Agent " .. ngx.var.http_user_agent .. " is in greylist (matched " .. ua_bl .. ")", false,
ngx.OK
end
end
-- UA is not greylisted
local ok, err = self:add_to_cache("ua" .. ngx.var.http_user_agent, "ok")
if not ok then
ret = false
ret_err = err
end
end
-- Check if URI is in greylist
if not cached_uri then
local uri_list, err = utils.get_variable("GREYLIST_URI")
if uri_list and uri_list ~= "" then
for element in uri_list:gmatch("%S+") do
table.insert(greylists["URI"], element)
end
end
for i, uri_bl in ipairs(greylists["URI"]) do
if ngx.var.uri:match(uri_bl) then
self:add_to_cache("uri" .. ngx.var.uri, "URI " .. uri_bl)
return ret, "client URI " .. ngx.var.uri .. " is in greylist (matched " .. uri_bl .. ")", false, ngx.OK
end
end
end
-- URI is not greylisted
local ok, err = self:add_to_cache("uri" .. ngx.var.uri, "ok")
if not ok then
ret = false
ret_err = err
end
return ret, "IP is not in list (error = " .. ret_err .. ")", true, utils.get_deny_status()
end
function _M:preread()
-- Check if preread is needed
local preread_needed, err = utils.get_variable("USE_GREYLIST")
if preread_needed == nil then
return false, err, false, nil
end
if preread_needed ~= "yes" then
return true, "Greylist not activated", false, nil
end
-- Check the cache
local cached_ip, err = self:is_in_cache("ip" .. ngx.var.remote_addr)
if cached_ip and cached_ip ~= "ok" then
return true, "IP is in greylist cache (info = " .. cached_ip .. ")", false, ngx.OK
end
if cached_ip then
return true, "full request is in greylist cache (not greylisted)", false, nil
end
-- Get list
local data, err = datastore:get("plugin_greylist_list")
if not data then
return false, "can't get Greylist list : " .. err, false, nil
end
local ok, greylists = pcall(cjson.decode, data)
if not ok then
return false, "error while decoding greylists : " .. greylists, false, nil
end
-- Return value
local ret, ret_err = true, "success"
-- Check if IP is in IP/net greylist
local ip_net, err = utils.get_variable("GREYLIST_IP")
if ip_net and ip_net ~= "" then
for element in ip_net:gmatch("%S+") do
table.insert(greylists["IP"], element)
end
end
if not cached_ip then
local ipm, err = ipmatcher.new(greylists["IP"])
if not ipm then
ret = false
ret_err = "can't instantiate ipmatcher " .. err
else
if ipm:match(ngx.var.remote_addr) then
self:add_to_cache("ip" .. ngx.var.remote_addr, "ip/net")
return ret, "client IP " .. ngx.var.remote_addr .. " is in greylist", false, ngx.OK
end
end
end
-- Check if rDNS is in greylist
local rdns_global, err = utils.get_variable("GREYLIST_RDNS_GLOBAL")
local check = true
if not rdns_global then
logger.log(ngx.ERR, "GREYLIST", "Error while getting GREYLIST_RDNS_GLOBAL variable : " .. err)
elseif rdns_global == "yes" then
check, err = utils.ip_is_global(ngx.var.remote_addr)
if check == nil then
logger.log(ngx.ERR, "GREYLIST", "Error while getting checking if IP is global : " .. err)
end
end
if not cached_ip and check then
local rdns, err = utils.get_rdns(ngx.var.remote_addr)
if not rdns then
ret = false
ret_err = "error while trying to get reverse dns : " .. err
else
local rdns_list, err = utils.get_variable("GREYLIST_RDNS")
if rdns_list and rdns_list ~= "" then
for element in rdns_list:gmatch("%S+") do
table.insert(greylists["RDNS"], element)
end
end
for i, suffix in ipairs(greylists["RDNS"]) do
if rdns:sub(- #suffix) == suffix then
self:add_to_cache("ip" .. ngx.var.remote_addr, "rDNS " .. suffix)
return ret, "client IP " .. ngx.var.remote_addr .. " is in greylist (info = rDNS " .. suffix .. ")", false, ngx.OK
end
end
end
end
-- Check if ASN is in greylist
if not cached_ip then
if utils.ip_is_global(ngx.var.remote_addr) then
local asn, err = utils.get_asn(ngx.var.remote_addr)
if not asn then
ret = false
ret_err = "error while trying to get asn number : " .. err
else
local asn_list, err = utils.get_variable("GREYLIST_ASN")
if asn_list and asn_list ~= "" then
for element in asn_list:gmatch("%S+") do
table.insert(greylists["ASN"], element)
end
end
for i, asn_bl in ipairs(greylists["ASN"]) do
if tostring(asn) == asn_bl then
self:add_to_cache("ip" .. ngx.var.remote_addr, "ASN " .. tostring(asn))
return ret, "client IP " .. ngx.var.remote_addr .. " is in greylist (kind = ASN " .. tostring(asn) .. ")", false,
ngx.OK
end
end
end
end
end
-- IP is not greylisted
local ok, err = self:add_to_cache("ip" .. ngx.var.remote_addr, "ok")
if not ok then
ret = false
ret_err = err
end
return ret, "IP is not in list (error = " .. ret_err .. ")", true, utils.get_deny_status()
end
function _M:is_in_cache(ele)
local kind, err = datastore:get("plugin_greylist_cache_" .. ngx.var.server_name .. ele)
if not kind then
if err ~= "not found" then
logger.log(ngx.ERR, "GREYLIST", "Error while accessing cache : " .. err)
end
return false, err
end
return kind, "success"
end
function _M:add_to_cache(ele, kind)
local ok, err = datastore:set("plugin_greylist_cache_" .. ngx.var.server_name .. ele, kind, 3600)
if not ok then
logger.log(ngx.ERR, "GREYLIST", "Error while adding element to cache : " .. err)
return false, err
end
-- Instantiate cachestore
cachestore:new(use_redis)
return true, "success"
end
return _M
function greylist:init()
if self.init_needed then
-- Read blacklists
local greylists = {
["IP"] = {},
["RDNS"] = {},
["ASN"] = {},
["USER_AGENT"] = {},
["URI"] = {},
}
local i = 0
for kind, _ in pairs(greylists) do
local f, err = io.open("/var/cache/bunkerweb/greylist/" .. kind .. ".list", "r")
if f then
for line in f:lines() do
table.insert(greylists[kind], line)
i = i + 1
end
f:close()
end
end
-- Load them into datastore
local ok, err = datastore:set("plugin_greylist_lists", cjson.encode(greylists))
if not ok then
return self:ret(false, "can't store greylist list into datastore : " .. err)
end
return self:ret(true, "successfully loaded " .. tostring(i) .. " bad IP/network/rDNS/ASN/User-Agent/URI")
end
end
function greylist:access()
-- Check if access is needed
if self.variables["USE_GREYLIST"] ~= "yes" then
return self:ret(true, "greylist not activated")
end
-- Check the caches
local checks = {
["IP"] = "ip" .. ngx.var.remote_addr
}
if ngx.var.http_user_agent then
checks["UA"] = "ua" .. ngx.var.http_user_agent
end
if ngx.var.uri then
checks["URI"] = "uri" .. ngx.var.uri
end
local already_cached = {
["IP"] = false,
["URI"] = false,
["UA"] = false
}
for k, v in pairs(checks) do
local cached, err = self:is_in_cache(v)
if not cached and err ~= "success" then
self.logger:log(ngx.ERR, "error while checking cache : " .. err)
elseif cached and cached ~= "ok" then
return self:ret(true, k + " is in cached greylist", utils.get_deny_status())
end
if cached then
already_cached[k] = true
end
end
-- Check lists
if not self.lists then
return self:ret(false, "lists is nil")
end
-- Perform checks
for k, v in pairs(checks) do
if not already_cached[k] then
local greylisted, err = self:is_greylisted(k)
if greylisted == nil then
self.logger:log(ngx.ERR, "error while checking if " .. k .. " is greylisted : " .. err)
else
local ok, err = self:add_to_cache(v, greylisted or "ok")
if not ok then
self.logger:log(ngx.ERR, "error while adding element to cache : " .. err)
end
if greylisted == "ko" then
return self:ret(true, k + " is not in greylist", utils.get_deny_status())
end
end
end
end
-- Return
return self:ret(true, "greylisted")
end
function greylist:preread()
return self:access()
end
function greylist:is_greylisted(kind)
if kind == "IP" then
return self:is_greylisted_ip()
elseif kind == "URI"
return self:is_greylisted_uri()
elseif kind == "UA"
return self:is_greylisted_ua()
return false, "unknown kind " .. kind
end
function greylist:is_greylisted_ip()
-- Check if IP is in blacklist
local ipm, err = ipmatcher.new(self.lists["IP"])
if not ipm then
return nil, err
end
local match, err = ipm:match(ngx.var.remote_addr)
if err then
return nil, err
end
if match then
return true, "ip"
end
-- Check if rDNS is needed
local check_rdns = true
if self.variables["BLACKLIST_RDNS_GLOBAL"] == "yes" then
local is_global, err = utils.ip_is_global(ngx.var.remote_addr)
if is_global == nil then
return nil, err
end
if not is_global then
check_rdns = false
end
end
if check_rdns then
-- Get rDNS
local rdns_list, err = utils.get_rdns(ngx.var.remote_addr)
if not rdns_list then
return nil, err
end
-- Check if rDNS is in greylist
for i, suffix in ipairs(self.lists["RDNS"]) do
for j, rdns in ipairs(rdns_list) do
if rdns:sub(-#suffix) == suffix then
return true, "rDNS " .. suffix
end
end
end
end
-- Check if ASN is in greylist
for i, bl_asn in ipairs(self.lists["ASN"]) do
if bl_asn == tostring(asn) then
return true, "ASN " .. bl_asn
end
end
-- Not greylisted
return false, "ko"
end
function greylist:is_greylisted_uri()
-- Check if URI is in blacklist
for i, uri in ipairs(self.lists["URI"]) do
if ngx.var.uri:match(uri) then
return true, "URI " .. uri
end
end
-- URI is not greylisted
return false, "ko"
end
function greylist:is_greylisted_ua()
-- Check if UA is in greylist
for i, ua in ipairs(self.lists["USER_AGENT"]) do
if ngx.var.http_user_agent:match(ua) then
return true, "UA " .. ua
end
end
-- UA is not greylisted
return false, "ko"
end
function greylist:is_in_cache(ele)
local ok, data = cachestore:get("plugin_greylist_" .. ele)
if not ok then then
return false, data
end
return true, data
end
function greylist:add_to_cache(ele, value)
local ok, err = cachestore:set("plugin_greylist_" .. ele, value)
if not ok then then
return false, err
end
return true
end
return greylist

View File

@ -1,23 +1,27 @@
local _M = {}
_M.__index = _M
local class = require "middleclass"
local plugin = require "bunkerweb.plugin"
local utils = require "bunkerweb.utils"
local cjson = require "cjson"
local logger = require "logger"
local cjson = require "cjson"
local letsencrypt = class("letsencrypt", plugin)
function _M.new()
local self = setmetatable({}, _M)
return self, nil
end
function _M:access()
if string.sub(ngx.var.uri, 1, string.len("/.well-known/acme-challenge/")) == "/.well-known/acme-challenge/" then
logger.log(ngx.NOTICE, "LETS-ENCRYPT", "Got a visit from Let's Encrypt, let's whitelist it.")
return true, "success", true, ngx.exit(ngx.OK)
function letsencrypt:new()
-- Call parent new
local ok, err = plugin.new(self, "letsencrypt")
if not ok then
return false, err
end
return true, "success", false, nil
end
function _M:api()
function letsencrypt:access()
if string.sub(ngx.var.uri, 1, string.len("/.well-known/acme-challenge/")) == "/.well-known/acme-challenge/" then
self.logger:log(ngx.NOTICE, "got a visit from Let's Encrypt, let's whitelist it")
return self:ret(true, "visit from LE", ngx.OK)
end
return self:ret(true, "success")
end
function letsencrypt:api()
if not string.match(ngx.var.uri, "^/lets%-encrypt/challenge$") or
(ngx.var.request_method ~= "POST" and ngx.var.request_method ~= "DELETE") then
return false, nil, nil
@ -47,4 +51,4 @@ function _M:api()
return true, ngx.HTTP_NOT_FOUND, { status = "error", msg = "unknown request" }
end
return _M
return letsencrypt

View File

@ -7,19 +7,17 @@ local clusterstore = require "bunkerweb.clusterstore"
local redis = class("redis", plugin)
function redis:new()
plugin.new(self, "redis")
-- Store variable for later use
local use_redis, err = utils.get_variable("USE_REDIS", false)
if use_redis == nil then
return self:ret(false, "can't check USE_REDIS variable : " .. err)
-- Call parent new
local ok, err = plugin.new(self, "redis")
if not ok then
return false, err
end
self.use_redis = use_redis == "yes"
return self:ret(true, "success")
return true, "success"
end
function redis:init()
-- Check if init is needed
if not self.use_redis then
if self.variables["USE_REDIS"] then
return self:ret(true, "redis not used")
end
-- Check redis connection