From c9c2fa818527fa5385a3f115aaf24c40646814da Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 6 May 2026 17:36:25 +0800 Subject: [PATCH] first commit --- config/common_config.lua | 88 ++++++++++++++ config/rate_limit_config.lua | 117 ++++++++++++++++++ lib/logger.lua | 230 +++++++++++++++++++++++++++++++++++ modules/rate_limiter.lua | 151 +++++++++++++++++++++++ modules/ua_limiter.lua | 93 ++++++++++++++ waf.lua | 28 +++++ 6 files changed, 707 insertions(+) create mode 100644 config/common_config.lua create mode 100644 config/rate_limit_config.lua create mode 100644 lib/logger.lua create mode 100644 modules/rate_limiter.lua create mode 100644 modules/ua_limiter.lua create mode 100644 waf.lua diff --git a/config/common_config.lua b/config/common_config.lua new file mode 100644 index 0000000..b91675c --- /dev/null +++ b/config/common_config.lua @@ -0,0 +1,88 @@ +-- 公共配置文件 +-- 集中管理所有模块的通用配置 + +local _M = {} + +-- ============================================ +-- IP速率限制配置 +-- ============================================ +_M.rate_limit = { + -- 是否启用 + enabled = true, + + -- 共享内存字典名称(需在nginx.conf中定义) + shared_dict = "ip_rate_limit", + + -- 时间窗口(秒):统计多长时间内的请求 + time_window = 60, + + -- 最大请求次数:时间窗口内允许的最大请求数 + max_requests = 100, + + -- 处置策略:allow / rate_limit / block + action = "rate_limit", + + -- HTTP状态码(根据action自动设置,也可手动指定) + status_code = nil, + + -- 拒绝访问时的响应消息 + message = nil +} + +-- ============================================ +-- User-Agent限制配置 +-- ============================================ +_M.ua_limit = { + -- 是否启用 + enabled = false, + + -- 处置策略:allow / rate_limit / block + action = "block", + + -- HTTP状态码 + status_code = 403, + + -- 拒绝访问时的响应消息 + message = "Access denied", + + -- 黑名单User-Agent列表(匹配到的UA会被阻止) + blacklist = { + -- "BadBot", + -- "Scraper", + -- "Spider" + }, + + -- 白名单User-Agent列表(匹配到的UA总是放行) + whitelist = { + -- "Googlebot", + -- "Bingbot" + } +} + +-- ============================================ +-- 域名配置(IP速率限制模块使用) +-- ============================================ +_M.domains = { + -- 在此添加需要启用限流的域名 + -- 留空表示对所有域名生效 + -- 示例: + -- "example.com", + -- "api.example.com", +} + +-- ============================================ +-- 日志配置(所有模块共用) +-- ============================================ +_M.log_config = { + -- 是否启用日志 + enabled = true, + + -- 日志级别:DEBUG, INFO, WARN, ERROR + log_level = "INFO", + log_path = "/usr/local/openresty/nginx/logs/waf.log", + + log_request_details = true, + log_allowed = true +} + +return _M diff --git a/config/rate_limit_config.lua b/config/rate_limit_config.lua new file mode 100644 index 0000000..4103db0 --- /dev/null +++ b/config/rate_limit_config.lua @@ -0,0 +1,117 @@ +-- IP速率限制统一配置模块 +-- 从公共配置加载,提供便捷的访问接口 + +local _M = {} + +local common_config = require "config.common_config" + +-- 处置策略枚举 +_M.ACTION_ALLOW = "allow" -- 放行 +_M.ACTION_RATE_LIMIT = "rate_limit" -- 限制速率(返回429) +_M.ACTION_BLOCK = "block" -- 封禁(返回403) + +-- ============================================ +-- 预设配置模板(可选使用) +-- ============================================ +_M.presets = { + -- 宽松模式:放行 + allow_all = { + time_window = 60, + max_requests = 10000, + action = _M.ACTION_ALLOW + }, + + -- 标准模式:限制速率 + standard = { + time_window = 60, + max_requests = 100, + action = _M.ACTION_RATE_LIMIT + }, + + -- 严格模式:封禁 + strict = { + time_window = 60, + max_requests = 50, + action = _M.ACTION_BLOCK + }, + + -- API接口保护 + api_protection = { + time_window = 60, + max_requests = 30, + action = _M.ACTION_RATE_LIMIT, + status_code = 429, + message = "API rate limit exceeded" + }, + + -- 登录防暴力破解 + login_protection = { + time_window = 300, + max_requests = 10, + action = _M.ACTION_BLOCK, + message = "Too many login attempts" + } +} + +-- ============================================ +-- 配置管理函数 +-- ============================================ + +-- 获取配置(从公共配置读取) +function _M.get_config() + return common_config.rate_limit +end + +-- 更新配置 +function _M.update_config(new_config) + local cfg = common_config.rate_limit + for k, v in pairs(new_config) do + cfg[k] = v + end + + -- 如果没有手动指定状态码,根据action自动设置 + if not new_config.status_code then + if cfg.action == _M.ACTION_RATE_LIMIT then + cfg.status_code = 429 + elseif cfg.action == _M.ACTION_BLOCK then + cfg.status_code = 403 + else + cfg.status_code = 200 + end + end + + -- 如果没有手动指定消息,根据action自动设置 + if not new_config.message then + if cfg.action == _M.ACTION_RATE_LIMIT then + cfg.message = "Rate limit exceeded. Please try again later." + elseif cfg.action == _M.ACTION_BLOCK then + cfg.message = "Access denied." + else + cfg.message = "" + end + end +end + +-- 应用预设配置 +function _M.apply_preset(preset_name) + local preset = _M.presets[preset_name] + if preset then + _M.update_config(preset) + return true + end + return false +end + +-- 快速配置函数 +-- @param time_window: 时间窗口(秒) +-- @param max_requests: 最大请求次数 +-- @param action: 处置策略(allow/rate_limit/block) +function _M.configure(time_window, max_requests, action) + _M.update_config({ + time_window = time_window, + max_requests = max_requests, + action = action + }) +end + +return _M diff --git a/lib/logger.lua b/lib/logger.lua new file mode 100644 index 0000000..cf35623 --- /dev/null +++ b/lib/logger.lua @@ -0,0 +1,230 @@ +-- 日志记录模块 +-- 记录IP频率限制的详细日志信息 + +local _M = {} + +-- 日志级别 +_M.LOG_DEBUG = "DEBUG" +_M.LOG_INFO = "INFO" +_M.LOG_WARN = "WARN" +_M.LOG_ERROR = "ERROR" + +-- 从公共配置加载日志配置 +local common_config = require "config.common_config" +local config = common_config.log_config + +-- 日志文件句柄缓存 +config.log_file_handle = nil + +-- 日志级别映射 +local log_levels = { + DEBUG = ngx.DEBUG, + INFO = ngx.INFO, + WARN = ngx.WARN, + ERROR = ngx.ERR +} + +-- 检查日志级别是否应该输出 +local function should_log(level) + if not config.enabled then + return false + end + + local current_level = log_levels[config.log_level] or ngx.INFO + local msg_level = log_levels[level] or ngx.INFO + + return msg_level >= current_level +end + +-- 清理字符串,防止日志注入 +local function sanitize_string(str) + if not str then + return "" + end + + -- 去除换行符、回车符等特殊字符 + str = tostring(str) + str = str:gsub("[\r\n\t]", " ") + str = str:gsub("[%z\1-\31]", "") + + -- 限制长度 + if #str > 500 then + str = str:sub(1, 500) .. "..." + end + + return str +end + +-- 获取当前时间戳 +local function get_timestamp() + return ngx.time() +end + +-- 格式化日志消息 +local function format_message(level, message, extra_info) + local timestamp = get_timestamp() + local date_str = ngx.http_time(timestamp) + + local log_msg = string.format("%s %s [%s] %s", + "[IP_RATE_LIMIT]", + date_str, + level, + message + ) + + if extra_info and next(extra_info) then + local extras = {} + for k, v in pairs(extra_info) do + table.insert(extras, string.format("%s=%s", k, sanitize_string(v))) + end + log_msg = log_msg .. " | " .. table.concat(extras, " ") + end + + -- 添加换行符 + log_msg = log_msg .. "\n" + + return log_msg +end + +-- 打开日志文件 +local function open_log_file() + if config.log_path == "" then + return nil + end + + -- 如果已经有打开的文件句柄,直接返回 + if config.log_file_handle then + return config.log_file_handle + end + + -- 尝试打开文件(追加模式) + local file, err = io.open(config.log_path, "a") + if not file then + ngx.log(ngx.ERR, "[IP_RATE_LIMIT] Failed to open log file: ", config.log_path, " error: ", err) + return nil + end + + config.log_file_handle = file + return file +end + +-- 关闭日志文件 +local function close_log_file() + if config.log_file_handle then + config.log_file_handle:flush() + config.log_file_handle:close() + config.log_file_handle = nil + end +end + +-- 写入日志 +local function write_log(log_msg, ngx_level) + -- 如果配置了日志文件路径,写入文件 + if config.log_path ~= "" then + local file = open_log_file() + if file then + file:write(log_msg) + file:flush() + end + end + + -- 同时写入Nginx error.log(用于调试和备份) + ngx.log(ngx_level, log_msg) +end + +-- 记录日志 +local function log(level, message, extra_info) + if not should_log(level) then + return + end + + local log_msg = format_message(level, message, extra_info) + local ngx_level = log_levels[level] or ngx.INFO + + write_log(log_msg, ngx_level) +end + +-- ============================================ +-- 公共API +-- ============================================ + +-- 记录放行日志 +function _M.log_allowed(client_ip, request_count, time_window) + if not config.log_allowed then + return + end + + log(_M.LOG_DEBUG, "Request allowed", { + ip = client_ip, + count = tostring(request_count), + window = tostring(time_window) .. "s", + action = "ALLOWED" + }) +end + +-- 记录限流日志 +function _M.log_rate_limited(client_ip, request_count, max_requests, time_window) + log(_M.LOG_WARN, "Rate limit exceeded", { + ip = client_ip, + count = tostring(request_count), + max = tostring(max_requests), + window = tostring(time_window) .. "s", + action = "RATE_LIMITED", + status = "429" + }) +end + +-- 记录封禁日志 +function _M.log_blocked(client_ip, request_count, max_requests, time_window) + log(_M.LOG_WARN, "Access blocked", { + ip = client_ip, + count = tostring(request_count), + max = tostring(max_requests), + window = tostring(time_window) .. "s", + action = "BLOCKED", + status = "403" + }) +end + +-- 记录请求详情 +function _M.log_request_details(client_ip, uri, method, user_agent) + if not config.log_request_details then + return + end + + log(_M.LOG_DEBUG, "Request details", { + ip = client_ip, + uri = sanitize_string(uri), + method = method, + ua = sanitize_string(user_agent or "") + }) +end + +-- 记录错误日志 +function _M.log_error(message, error_info) + log(_M.LOG_ERROR, message, error_info) +end + +-- 记录警告日志 +function _M.log_warn(message, warn_info) + log(_M.LOG_WARN, message, warn_info) +end + +-- 记录信息日志 +function _M.log_info(message, info) + log(_M.LOG_INFO, message, info) +end + +-- 刷新日志(确保所有日志已写入) +function _M.flush() + if config.log_file_handle then + config.log_file_handle:flush() + end +end + +-- 清理资源(在worker退出时调用) +function _M.cleanup() + close_log_file() +end + +return _M diff --git a/modules/rate_limiter.lua b/modules/rate_limiter.lua new file mode 100644 index 0000000..e86977d --- /dev/null +++ b/modules/rate_limiter.lua @@ -0,0 +1,151 @@ +-- IP频率限制核心模块 +-- 功能:统计指定IP在时间段内的请求次数,根据配置执行相应处置策略 + +local _M = {} + +local config_module = require "config.rate_limit_config" +local common_config = require "config.common_config" +local logger = require "lib.logger" + +-- 获取配置 +local function get_config() + return config_module.get_config() +end + +-- 检查域名是否应该应用限流 +local function should_apply_rate_limit(host) + local cfg = get_config() + + -- 如果未启用,直接返回false + if not cfg.enabled then + return false + end + + -- 如果没有host,默认不应用 + if not host or host == "" then + return false + end + + -- 从公共配置获取域名列表 + local domains = common_config.domains + + -- 如果domains列表为空,对所有域名生效 + if #domains == 0 then + return true + end + + -- 检查域名是否在配置的列表中 + for _, domain in ipairs(domains) do + if host == domain then + return true + end + end + + return false +end + +-- 主函数:检查并限制IP请求频率 +-- 返回值: +-- - true: 允许访问 +-- - false: 拒绝访问(已执行相应处置) +function _M.check_rate_limit() + local cfg = get_config() + + -- 如果未启用,直接放行 + if not cfg.enabled then + return true + end + + -- 获取当前域名 + local host = ngx.var.host + + -- 检查是否应该对此域名应用限流 + if not should_apply_rate_limit(host) then + logger.log_info("Rate limit skipped for domain", { + domain = host, + reason = "not_in_domains_list" + }) + return true + end + + -- 获取共享内存字典 + local dict = ngx.shared[cfg.shared_dict] + if not dict then + logger.log_error("Shared dictionary not found", { + dict_name = cfg.shared_dict + }) + return true + end + + -- 获取客户端IP + local client_ip = ngx.var.remote_addr + if not client_ip then + logger.log_warn("Failed to get client IP") + return true + end + + -- 记录请求详情(可选) + logger.log_request_details( + client_ip, + ngx.var.request_uri, + ngx.var.request_method, + ngx.var.http_user_agent + ) + + local current_time = ngx.time() + local time_window = cfg.time_window + local max_requests = cfg.max_requests + + -- 构建key:使用时间窗口的起始时间戳 + local window_start = math.floor(current_time / time_window) * time_window + local key = client_ip .. ":" .. window_start + + -- 使用原子操作增加计数 + local new_count, err = dict:incr(key, 1, 0) + if not new_count then + logger.log_error("Failed to increment counter", { + ip = client_ip, + error = err or "unknown" + }) + return true + end + + -- 设置过期时间 + if new_count == 1 then + dict:expire(key, time_window + 10) + end + + -- 检查是否超过限制 + if new_count > max_requests then + -- 根据配置的处置策略执行相应操作 + if cfg.action == config_module.ACTION_ALLOW then + -- 放行:仅记录日志,不阻止 + logger.log_allowed(client_ip, new_count, time_window) + return true + + elseif cfg.action == config_module.ACTION_RATE_LIMIT then + -- 限制速率:返回429 + logger.log_rate_limited(client_ip, new_count, max_requests, time_window) + + ngx.status = cfg.status_code or 429 + ngx.header["Content-Type"] = "text/plain; charset=utf-8" + ngx.say(cfg.message or "Rate limit exceeded") + return false + + elseif cfg.action == config_module.ACTION_BLOCK then + -- 封禁:返回403 + logger.log_blocked(client_ip, new_count, max_requests, time_window) + + ngx.status = cfg.status_code or 403 + ngx.header["Content-Type"] = "text/plain; charset=utf-8" + ngx.say(cfg.message or "Access denied") + return false + end + end + + -- 正常放行 + logger.log_allowed(client_ip, new_count, time_window) + return true +end + +return _M diff --git a/modules/ua_limiter.lua b/modules/ua_limiter.lua new file mode 100644 index 0000000..33c8b4e --- /dev/null +++ b/modules/ua_limiter.lua @@ -0,0 +1,93 @@ +-- User-Agent限制模块 +-- 功能:根据User-Agent进行访问控制(黑白名单) + +local _M = {} + +local common_config = require "config.common_config" +local logger = require "lib.logger" + +-- 获取配置 +local function get_config() + return common_config.ua_limit +end + +-- 检查User-Agent是否在列表中(支持部分匹配) +local function ua_in_list(ua, list) + if not ua or #list == 0 then + return false + end + + for _, pattern in ipairs(list) do + -- 使用字符串查找,支持部分匹配 + if ua:find(pattern, 1, true) then + return true + end + end + + return false +end + +-- 主函数:检查User-Agent +-- 返回值: +-- - true: 允许访问 +-- - false: 拒绝访问 +function _M.check_ua_limit() + local cfg = get_config() + + -- 如果未启用,直接放行 + if not cfg.enabled then + return true + end + + -- 获取User-Agent + local ua = ngx.var.http_user_agent + + -- 如果没有UA,记录日志但不阻止(可根据需求调整) + if not ua or ua == "" then + logger.log_info("Empty User-Agent", { + ip = ngx.var.remote_addr, + uri = ngx.var.request_uri + }) + return true + end + + -- 检查白名单:在白名单中的UA总是放行 + if ua_in_list(ua, cfg.whitelist) then + logger.log_info("User-Agent whitelisted", { + ua = ua, + action = "ALLOWED" + }) + return true + end + + -- 检查黑名单:在黑名单中的UA会被阻止 + if ua_in_list(ua, cfg.blacklist) then + logger.log_warn("User-Agent blocked", { + ua = ua, + action = cfg.action, + status = tostring(cfg.status_code) + }) + + -- 执行处置策略 + if cfg.action == "allow" then + -- 仅记录日志,不阻止 + return true + elseif cfg.action == "rate_limit" then + ngx.status = cfg.status_code or 429 + ngx.header["Content-Type"] = "text/plain; charset=utf-8" + ngx.say(cfg.message or "Rate limit exceeded") + return false + else + -- 默认封禁 + ngx.status = cfg.status_code or 403 + ngx.header["Content-Type"] = "text/plain; charset=utf-8" + ngx.say(cfg.message or "Access denied") + return false + end + end + + -- 不在任何列表中,正常放行 + return true +end + +return _M diff --git a/waf.lua b/waf.lua new file mode 100644 index 0000000..f0ee728 --- /dev/null +++ b/waf.lua @@ -0,0 +1,28 @@ +-- WAF统一入口模块 +-- 功能:集中管理所有安全检查(IP限流、UA限制等) + +local _M = {} + +local rate_limiter = require "modules.rate_limiter" +local ua_limiter = require "modules.ua_limiter" + +-- 执行所有安全检查 +-- 返回值: +-- - true: 所有检查通过,允许访问 +-- - false: 某个检查失败,已返回错误响应 +function _M.check() + -- IP频率限制检查 + if not rate_limiter.check_rate_limit() then + return false + end + + -- User-Agent限制检查 + if not ua_limiter.check_ua_limit() then + return false + end + + -- 所有检查通过 + return true +end + +return _M