Nginx 开源免费 WAF 防火墙宝塔插件 新增PHP2Shell 防护
基于 OpenResty (ngx_lua) 的高性能 Web 应用防火墙,宝塔面板(BT Panel)插件。

Gitee 仓库:
目录
功能特性
极低磁盘 IO 运行时:
init_by_lua阶段一次性加载所有配置和规则到内存,access_by_lua阶段纯内存匹配,仅拦截日志同步直写(对齐 free_waf),性能接近原生 NginxCC 攻击防护:基于
shared_dict的滑动窗口频次统计 + CC2 挑战验证,支持自定义阈值、检测周期、自适应封禁时长、自动永久拉黑阈值、每小时请求硬上限恶意 HEAD 零容忍拦截:首次命中即封 IP(对齐 free_waf),爬虫 UA 和白名单 URI 放行
多维度攻击拦截:URL 路径、URL 黑名单、GET 参数、POST 内容、Cookie、User-Agent、扫描器特征头
IP 黑白名单:支持精确 IP 匹配及 IP 段范围匹配,独立响应状态码
海外 IP 拦截:基于国内 IP 段列表自动拦截海外访问(含 SSL 验证 IP 白名单豁免)
文件上传保护:禁止上传目录脚本执行 + 全局/站点两级扩展名黑名单
ThinkPHP 漏洞防护:内置 ThinkPHP 5.x RCE 和 3.x 日志泄露防护规则
PHP2Shell 防护:双层防御 multipart 请求滥用——POST 规则层检测空字节注入/嵌套 multipart/超长 Disposition,Content-Type 层校验 boundary 合法性(缺失/空/超长/控制字符/重复),防护 CVE-2026-109618/9
域名映射表:
domains.json构建 O(1) 精确查找,自动将绑定域名(如 www.example.com)归一化到站点主名站点级独立配置:全局 → 站点三态配置(开启/关闭/继承),每个站点可单独启停任意防护模块
站点专属防护:每个站点可配置专属 URL 白名单、禁止 PHP 路径、禁止路径访问、禁止扩展名、URI 自定义拦截规则(url_rule_ex)、URI 参数校验(url_tell)
规则精准跳过:基于 hash set 的
disable_rule机制,O(1) 时间复杂度跳过指定站点的禁用规则CDN / 反向代理支持:自定义
cdn_header从请求头获取真实客户端 IP,支持多头部按顺序回溯系统防火墙集成:支持将 IP 封禁同步写入
iptables/firewalld/ufw,实现全端口封锁分类拦截模板:GET、POST、Cookie、User-Agent 各模块独立拦截页面和 HTTP 状态码
统计与日志:分类统计每个模块和每个站点的拦截数,JSON 格式日志含完整 HTTP 请求包
安全锁机制:IP 封禁时使用
shared_dict:add实现并发安全的一次性封锁,避免重复写文件自动重载生效:Python 后端每次修改配置自动执行
nginx -t && nginx reload,零停机热更新
运行原理
┌─────────────────────────────────────────────────────────────────┐ │ Nginx 启动 / reload │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ init_by_lua_file → init.lua │ │ │ │ • 加载 config.json / site.json 到全局变量 │ │ │ │ • 加载 rule/*.json 规则文件到内存数组 │ │ │ │ • 预加载 html/*.html 拦截模板 │ │ │ │ • 加载 domains.json → 构建 domains_map (O(1) 域名归一) │ │ │ │ • 初始化 shared_dict 统计计数 + 历史恢复与校准 │ │ │ │ • 构建站点级 disable_rule hash set │ │ │ │ • 提取站点专属白名单 / PHP禁止路径 / 禁止上传扩展名 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ ↓ │ │ access_by_lua_file → waf.lua │ │ • 纯内存匹配,仅拦截日志同步直写,零 JSON 解析 │ │ • domains_map O(1) 归一化绑定域名 → 站点主名 │ │ • get_effective_cfg() 合并全局开关 + 站点三态配置 │ │ • 短路匹配:规则命中即返回,后续规则不再遍历 │ │ • shared_dict 实现 IP 封禁、CC 计数、自动永久拉黑 │ └─────────────────────────────────────────────────────────────────┘
请求过滤流程
请求进入(domains_map 归一化域名 → 加载生效配置) │ ├─► ① IP 白名单 → 放行 │ ├─► ② IP 黑名单 → 拦截 │ ├─► ③ 已封锁 IP(drop 检查)→ 拦截 │ ├─► ④ URL 白名单 → 放行 │ ├─► ⑤ 恶意 HEAD 请求拦截 → 拦截(零容忍,首次命中即封IP) │ ├─► ⑥ URL 黑名单检查 → 拦截 │ ├─► ⑦ 海外 IP 拦截(SSL验证IP豁免)→ 拦截 │ ├─► ⑧ CC 攻击检测(滑动窗口 + CC2挑战验证 + 自动永久拉黑)→ 拦截 │ ├─► ⑨ 扫描器特征检测(Cookie/Header/Args)→ 拦截 │ ├─► ⑩ UA 恶意规则 → 拦截 │ ├─► ⑪ URL 路径规则 → 拦截 │ ├─► ⑫ GET 参数规则 → 拦截 │ ├─► ⑬ Cookie 规则 → 拦截 │ ├─► ⑭ ThinkPHP 漏洞防护(5.x RCE + 3.x 日志泄露)→ 拦截 │ ├─► ⑮ PHP2Shell 防护(multipart boundary 合法性校验)→ 拦截 │ ├─► ⑯ POST 检查(内容攻击 + 文件上传扩展名黑名单 + disable_rule 跳过)→ 拦截 │ ├─► ⑰ 站点级自定义规则(PHP禁止路径 / 禁止路径 / 禁止扩展名 / url_rule / url_tell)→ 拦截 │ └─► 放行
每个环节命中即短路返回,不做多余检查,最大化性能。模块开关由 get_effective_cfg() 根据全局配置与站点三态配置计算得出。
文件结构
bt_ngx_waf/ ├── info.json # 宝塔插件元数据(版本、作者、安装入口) ├── install.sh # 安装 / 更新 / 卸载脚本 ├── migrate.py # 配置补全脚本(通用默认值补全,对齐 free_waf 模式) ├── bt_ngx_waf_main.py # Python 后端 API(面板数据读写 + 自动生成 domains.json + 系统防火墙集成) ├── index.html # 前端管理界面 ├── icon.png # 插件图标 ├── conf/ │ └── bt_ngx_waf.conf # Nginx 配置片段(shared_dict + lua hook) └── data/ ├── config.json # 全局配置模板 ├── site.json # 站点级配置存储(支持多站点 + 三态继承) ├── domains.json # 域名映射表(Python 后端自动生成) ├── total.json # 统计计数持久化(含分类计数 + 站点级计数 + 校准) ├── drop_ip.log # IP 封禁历史日志 ├── operation.log # 操作日志 ├── lua/ │ ├── init.lua # 初始化脚本(init_by_lua,仅 reload 时执行) │ ├── waf.lua # 请求过滤主逻辑(access_by_lua,每请求执行 + API 端点分流) │ └── api.lua # Lua 端 API 接口(仅 127.0.0.1 可访问,已整合进 waf.lua handle_api) ├── rule/ │ ├── args.json # GET 参数规则(SQL注入/XSS等) │ ├── cookie.json # Cookie 攻击规则 │ ├── ip_black.json # IP 黑名单(支持精确匹配和IP段) │ ├── ip_white.json # IP 白名单 │ ├── post.json # POST 内容规则 │ ├── url.json # URL 路径规则 │ ├── url_black.json # URL 黑名单规则(独立于 url 规则) │ ├── url_white.json # URL 白名单规则 │ ├── whiteurl.json # URL 白名单规则(兼容旧版命名) │ ├── user_agent.json # User-Agent 规则(恶意爬虫/扫描器) │ ├── scan_black.json # 扫描器特征规则(Cookie / Header / Args) │ ├── head_white.json # HEAD 请求白名单规则(爬虫UA自动放行) │ └── cn.json # 国内 IP 段列表(海外拦截用) └── html/ ├── get.html # GET/URL 拦截页面 ├── post.html # POST 拦截页面 ├── cookie.html # Cookie 拦截页面 └── user_agent.html # UA 拦截页面
运行时目录(安装后):
/www/server/bt_ngx_waf/ ├── lua/*.lua # Lua 脚本(来自 data/lua/) ├── rule/*.json # 规则文件(来自 data/rule/,保留用户自定义) ├── html/*.html # 拦截模板(来自 data/html/,保留用户自定义) ├── config.json # 全局配置(面板修改后自动 reload) ├── site.json # 站点配置 ├── domains.json # 域名映射表(Python 后端写入,与 site.json 同步) ├── total.json # 统计持久化 └── drop_ip.log # IP 封禁历史
规则格式
规则文件采用 JSON 数组格式,每条规则为一个子数组:
[启用状态, "正则表达式", "规则说明", 是否自定义]
| 字段 | 类型 | 说明 |
|---|---|---|
| 启用状态 | number | 1 = 启用,0 = 禁用 |
| 正则表达式 | string | PCRE 正则语法,自动添加 ijo flag(不区分大小写、UTF-8、优化) |
| 规则说明 | string | 中文描述,便于在面板管理 |
| 是否自定义 | number | 0 = 官方内置,1 = 用户自定义 |
示例:
[ [1, "(?:select|union|insert|update|delete)\\s+", "SQL注入", 0], [1, "<script.*?>", "XSS攻击", 1], [0, "已禁用的规则", "这条不会生效", 0] ]
IP 黑白名单(ip_black.json / ip_white.json)支持两种格式:
[ "192.168.1.1", "10.0.0.1", [ [173, 224, 195, 2], [173, 224, 195, 10] ], [ [203, 106, 0, 1], [203, 106, 0, 255] ] ]
配置说明
全局配置 (config.json)
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
version | number | 3 | 配置文件版本号 |
open | boolean | true | WAF 总开关 |
log | boolean | true | 攻击日志开关 |
log_dir | string | /www/wwwlogs/bt_ngx_waf/ | 日志存储目录 |
log_save | number | 30 | 日志保留天数 |
Redirect | boolean | true | 拦截后是否返回 302 跳转 |
post_max_size | number | 10485760 | POST Body 最大字节数(10MB) |
retry | number | 5 | N 秒内累计多少次拦截后封锁 IP |
retry_cycle | number | 60 | N 秒统计周期 |
retry_time | number | 600 | 基础封锁时长(秒),多次累积自适应延长 |
black_fileExt | array | ["php", "jsp"] | 全局禁止上传脚本扩展名 |
ipWhitelist | array | ["127.0.0.1"] | 全局 IP 白名单(可在 rule/ip_white.json 扩展) |
ipBlocklist | array | [] | 全局 IP 黑名单(可在 rule/ip_black.json 扩展) |
cc.open | boolean | true | CC 防护开关 |
cc.limit | number | 100 | 周期内最大请求次数 |
cc.cycle | number | 60 | 检测周期(秒) |
cc.endtime | number | 300 | 基础 CC 封锁时长(秒) |
cc.status | number | 444 | CC 拦截 HTTP 状态码 |
cc.auto_black_count | number | 4 | 被 CC 拦截累计达 N 次后升级为永久拉黑(TTL=0,0=关闭) |
cc.force_black_per_hour | number | 36000 | 每小时请求硬上限,超过直接永久拉黑(0=关闭) |
head.open | boolean | true | 恶意 HEAD 请求拦截开关(首次命中即封 IP,爬虫 UA 放行) |
head.status | number | 444 | HEAD 请求拦截状态码 |
get.open | boolean | true | GET/URL 模块总开关 |
get.status | number | 403 | GET/URL 拦截状态码 |
url.open | boolean | true | URL 路径规则开关 |
url.status | number | 403 | URL 拦截状态码 |
url.reqfile | string | get.html | URL 拦截模板 |
args.open | boolean | true | GET 参数规则开关 |
args.status | number | 403 | GET 参数拦截状态码 |
args.reqfile | string | get.html | GET 参数拦截模板 |
post.open | boolean | true | POST 规则开关 |
post.status | number | 403 | POST 拦截状态码 |
post.reqfile | string | post.html | POST 拦截模板 |
cookie.open | boolean | true | Cookie 规则开关 |
cookie.status | number | 403 | Cookie 拦截状态码 |
cookie.reqfile | string | cookie.html | Cookie 拦截模板 |
user_agent.open | boolean | true | User-Agent 规则开关 |
user_agent.status | number | 403 | UA 拦截状态码 |
user_agent.reqfile | string | user_agent.html | UA 拦截模板 |
white_module.open | boolean | true | URL 白名单开关 |
scan.open | boolean | true | 扫描器防护开关 |
scan.status | number | 444 | 扫描器拦截状态码 |
drop_abroad.open | boolean | false | 海外 IP 拦截开关 |
drop_abroad.status | number | 444 | 海外 IP 拦截状态码 |
block.status | number | 444 | IP 被封锁后的拦截状态码 |
ipBlocklist_status | number | 444 | IP 黑名单拦截状态码 |
from_data | boolean | true | 是否记录 multipart form-data 完整数据到日志 |
php2shell.open | boolean | true | PHP2Shell 防护开关(multipart boundary 合法性校验) |
php2shell.boundary_max_len | number | 70 | boundary 最大允许长度(RFC 2046 规定 70 字节) |
php2shell.status | number | 403 | PHP2Shell 拦截 HTTP 状态码 |
站点配置 (site.json)
每个站点字段支持三态配置:null = 继承全局,true = 强制开启,false = 强制关闭
{ "example.com": { "open": true, "cdn": false, "cdn_header": ["x-forwarded-for", "x-real-ip"], "cc": { "open": true, "limit": 80, "cycle": 30, "endtime": 600, "auto_black_count": 4, "force_black_per_hour": 36000 }, "disable_rule": { "args": [2, 5], "post": [1]
}, "whiteurl": ["^/api/public/", "^/upload/"], "disable_php_path": ["^/uploads/", "^/data/"], "disable_upload_ext": ["php", "jsp", "asp", "exe"], "disable_path": ["^/admin/backup/", "^/config/"], "disable_ext": ["bak", "sql", "ini"], "url_rule": [["^/api/order", "price.*=.*99999"], ["^/api/user", "role.*=.*admin"]], "url_tell": [["^/api/internal", "token", "secret123"]], "scan": true, "drop_abroad": false, "head": true, "retry": 3, "retry_time": 300, "retry_cycle": 60
}
}| 字段 | 类型 | 说明 |
|---|---|---|
open | boolean/null | 站点 WAF 总开关 |
cdn | boolean/null | 是否启用 CDN 模式(从请求头获取真实 IP) |
cdn_header | array/null | CDN 真实 IP 头名称列表,按顺序回溯 |
cc.open | boolean/null | 站点级 CC 防护开关 |
cc.limit | number/null | 周期内最大请求次数 |
cc.cycle | number/null | 检测周期(秒) |
cc.endtime | number/null | 基础 CC 封锁时长(秒) |
cc.auto_black_count | number/null | 被 CC 拦截累计达 N 次后升级为永久拉黑 |
cc.force_black_per_hour | number/null | 每小时请求硬上限,超过直接永久拉黑(0=关闭) |
cc.increase | boolean/null | 是否开启 CC2 挑战验证模式 |
disable_rule | object/null | 站点禁用的规则索引({args: [0,1], post: [2]}),O(1) hash set 跳过 |
whiteurl | array/null | 站点专属 URL 白名单正则(命中则跳过所有检查) |
disable_php_path | array/null | 禁止 PHP 解析的路径正则(如 ^/uploads/) |
disable_upload_ext | array/null | 站点专属禁止上传扩展名(覆盖全局 black_fileExt) |
disable_path | array/null | 禁止直接访问的路径正则(如备份目录、配置目录) |
disable_ext | array/null | 禁止直接访问的文件扩展名(如 .bak、.sql) |
url_rule | array/null | 站点级 URI 自定义拦截 [[uri_pattern, params_regex], ...] |
url_tell | array/null | 站点级 URI 参数校验 [[uri_pattern, param_name, expected_value], ...],实际值≠预期值则拦截 |
scan | boolean/null | 扫描器防护开关 |
drop_abroad | boolean/null | 海外 IP 拦截开关 |
head | boolean/null | 恶意 HEAD 请求拦截开关 |
retry / retry_cycle / retry_time | number/null | IP 封禁参数(覆盖全局) |
安装部署
前置条件
Nginx / OpenResty:必须编译
lua-nginx-module支持Lua 版本:5.1+(与 OpenResty 内置一致)
宝塔面板:7.x / 8.x / 正式版均可
互斥说明:不能与宝塔
free_waf插件共存
安装步骤
# 方式一:通过宝塔面板插件市场直接安装https://gitee.com/web/bt_ngx_lua_waf/releases/download/latest/bt_ngx_waf.zip# 方式二:源码部署cd /www/server/panel/plugin/bt_ngx_waf bash install.sh install# 方式三:更新现有安装bash install.sh update# 卸载bash install.sh uninstall
安装脚本做了什么
创建
/www/server/bt_ngx_waf/运行目录和/www/wwwlogs/bt_ngx_waf/日志目录拷贝 Lua 脚本、规则文件、HTML 模板(保留用户已修改的文件)
写入
bt_ngx_waf.conf到 Nginx 虚拟主机配置目录自动
nginx -t && nginx reload生效
手动生效(可选)
# 如果安装脚本自动重载失败,手动执行/etc/init.d/nginx configtest && /etc/init.d/nginx reload# 验证 shared_dict 是否加载curl -s http://127.0.0.1/bt_ngx_waf_api?action=ping# 应返回 {"status":"ok"}热更新配置
修改任何配置文件(config.json / site.json / rule/*.json)后:
/etc/init.d/nginx reload
规则和配置在 init_by_lua 阶段重新加载到内存,零停机生效。
API 接口
Lua 端 API(仅限 127.0.0.1)
仅限 127.0.0.1 内网访问,防止外部探测。在 waf.lua 的 API 端点分流中统一处理。
访问方式:http://127.0.0.1/bt_ngx_waf_api?action=xxx
| 接口 | 说明 |
|---|---|
?action=ping | 健康检查,返回 {"status":"ok"} |
?action=get_total | 获取全局拦截统计(总数 + 分类计数 + 站点级计数 + 启动时间) |
?action=get_site_total&siteName=xxx | 获取指定站点的拦截统计(含所有分类计数) |
?action=get_drop_ip | 获取当前活跃的封禁 IP 列表(从 bt_ngx_waf_drop 字典遍历) |
?action=remove_drop_ip&ip=xxx | 解除指定 IP 的封禁(同时清理 bt_ngx_waf_drop 和 bt_ngx_waf_cc) |
?action=clean_drop_ip | 清空所有封禁 IP(同时清理两个字典的封锁记录) |
Python 后端管理接口
bt_ngx_waf_main.py 提供完整的管理接口(通过宝塔面板前端调用),每次写配置自动执行 nginx -t && nginx reload:
全局配置类
| 接口 | 说明 |
|---|---|
getConfig | 读取全局配置 |
saveConfig | 保存全局配置 |
get_html_tpl | 获取拦截模板内容 |
save_html_tpl | 保存拦截模板 |
站点配置类
| 接口 | 说明 |
|---|---|
getSiteConfig | 读取所有站点配置 |
saveSiteConfig | 保存所有站点配置(自动同步生成 domains.json) |
getSiteConfig_byname&siteName=xxx | 按名称读取单个站点配置 |
getSiteDetail&siteName=xxx | 获取站点详情(含域名列表) |
getSiteRule&siteName=xxx | 获取站点专属规则 |
addSiteRule&siteName=xxx | 添加站点专属规则(url_rule_ex / url_tell / disable_path 等) |
getSiteDisableRule&siteName=xxx | 获取站点禁用的规则索引列表 |
addSiteDisableRule&siteName=xxx | 添加站点禁用规则索引 |
addSiteWhiteurl&siteName=xxx | 添加站点专属 URL 白名单 |
getSiteCdnHeader&siteName=xxx | 获取站点 CDN 真实 IP 头配置 |
addSiteCdnHeader&siteName=xxx | 保存站点 CDN 真实 IP 头配置 |
规则管理类
| 接口 | 说明 |
|---|---|
getRule&name=xxx | 读取指定类型规则文件(args/post/url/cookie/user_agent 等) |
saveRule&name=xxx | 保存规则文件 |
addRule | 添加自定义规则(指定 type + pattern + description) |
deleteRule | 删除自定义规则 |
saveScanRule | 保存扫描器特征规则(scan_black.json) |
saveHeadWhite | 保存 HEAD 请求白名单规则(head_white.json) |
addUrlWhite | 添加全局 URL 白名单规则 |
addUrlBlack | 添加全局 URL 黑名单规则 |
addIpWhite | 添加 IP 白名单 |
addIpBlack | 添加 IP 黑名单 |
addCnip | 添加国内 IP 段(扩展 cn.json) |
系统防火墙集成
| 接口 | 说明 |
|---|---|
addFirewallBlackIp&ip=xxx | 将 IP 同步写入系统防火墙(自动检测 iptables / firewalld / ufw),实现全端口封锁 |
统计与日志类
| 接口 | 说明 |
|---|---|
getTotal | 获取全局拦截统计(从 shared_dict 读取) |
getTotalAll | 获取全量拦截统计数据 |
getLogs / getGlLogs | 获取攻击日志列表(解析 {site}_{date}_sec.log) |
getLog&siteName=xxx&date=xxx | 获取指定站点指定日期的攻击日志 |
getLogsList | 获取日志文件列表 |
cleanLogs | 清理过期日志文件 |
getSafeLogs / getSafeLogsIp | 获取 IP 封锁历史(解析 drop_ip.log) |
getBlockHistory | 获取封锁历史详情 |
cleanWafDropIp | 清空所有活跃封禁 IP(同步清理 shared_dict) |
cleanDropHistory | 清空封禁历史日志(drop_ip.log) |
测试工具
项目根目录提供 test_waf.php 全功能测试工具,用于验证 WAF 各模块拦截效果和误拦截情况。
部署与使用
# 将 test_waf.php 放到任意被 WAF 保护的站点根目录cp test_waf.php /www/wwwroot/example.com/test_waf.php# 浏览器访问http://example.com/test_waf.php
打开后在页面顶部输入目标站点地址,即可开始测试。
测试模式
| 模式 | 说明 |
|---|---|
| 一键全测 | 自动运行所有测试用例,输出通过/失败汇总 |
| 分步测试 | 手动选择单项测试,逐条验证 |
| 误拦截检测 | 发送正常请求,验证 WAF 不会误拦 |
测试覆盖范围
| 分组 | 用例数 | 说明 |
|---|---|---|
| GET 参数攻击 | 8 | SQL 注入、XSS、PHP 危险函数、目录穿越、SSRF |
| URL 路径攻击 | 8 | 敏感文件(phpinfo/.svn/.git/shell.sql)、ThinkPHP、composer、phpMyAdmin |
| UA 攻击 | 5 | sqlmap/nmap/nikto/hydra 恶意 UA + 正常 UA 放行 |
| Cookie 攻击 | 2 | Cookie 中 SQL 注入、XSS |
| POST 攻击 | 2 | POST 中 SQL 注入、eval |
| 特殊请求 | 2 | 正常 HEAD 放行 + 恶意 HEAD 拦截 |
| 站点自定义规则 | 3 | url_rule_ex / url_tell 拦截 / url_tell 放行 |
| 封锁机制 | 1 | 连发恶意请求验证 IP 封锁闭环 |
| 误拦截检查 | 5 | 正常首页/参数/POST/文件上传 + 正常 UA |
| 恶意扫描 | 3 | 扫描器特征 Args / Header / Cookie |
| 禁止的扩展名 | 3 | .php/.jsp/.bak 上传或访问 |
| 恶意文件上传 | 1 | 自定义禁止扩展名上传 |
| 禁止 PHP 脚本 | 1 | 禁止 PHP 解析路径测试 |
| 禁止路径访问 | 1 | 禁止目录访问测试 |
| PHP2Shell 防护 | 10 | 第一层 post.json 规则(4) + 第二层 boundary 校验(5) + 正常 multipart 放行(1) |
拦截判定逻辑
WAF 拦截可能返回以下状态,测试工具据此判定:
| 状态 | 说明 |
|---|---|
403 | 规则命中拦截 |
503 | IP 封锁拦截(drop) |
444 | 海外 IP / 直接断开连接 |
code=0 + curl 错误 | WAF 直接 RST 断连,curl 收不到 HTTP 响应 |
技术要点
性能设计
极低磁盘 IO(请求阶段):
init_by_lua阶段完成所有配置和规则文件读取,access_by_lua阶段仅操作内存变量和shared_dict,唯一磁盘 IO 为拦截日志同步直写(对齐 free_waf,避免 worker 段错误)短路匹配:规则命中即返回,后续规则不再遍历
Hash set 跳过:
disable_rule使用 Lua table 构建 hash set,O(1) 判断站点是否跳过某规则预加载模板:HTML 拦截模板在
init_by_lua阶段一次性读入内存变量lazy init:
arrip()等字符串处理函数延迟到真正需要时才执行
shared_dict 隔离
使用四个独立的共享内存字典,避免与其他插件或应用冲突:
| 字典名 | 用途 | 默认大小 |
|---|---|---|
bt_ngx_waf | 统计计数、分类统计、站点级统计 | 20m |
bt_ngx_waf_cc | CC 攻击频次统计、CC2 挑战验证 | 10m |
bt_ngx_waf_drop | IP 封禁记录、并发安全锁 | 10m |
bt_ngx_waf_drop_sum | IP 封禁累计次数(用于自适应延长) | 10m |
安全锁机制
IP 封禁时使用 shared_dict:add(lock_key, 1, 1) 实现分布式安全锁:
只有拿到锁的那个请求执行实际封禁和写日志
其他并发请求检测到锁后只更新状态,避免重复写文件
日志写入方式
拦截日志采用同步直写(对齐 free_waf):命中规则后立即通过 io.open 追加写入日志文件,不使用异步定时器或队列缓冲,避免 worker 段错误(signal 11)。安全锁机制保证同一时刻只有一个请求执行写操作,避免并发重复写入。
access 阶段的 IO 限制
| 操作 | 是否允许 | 说明 |
|---|---|---|
io.open() / io.close() | 仅限日志写入 | 拦截日志同步直写是唯一允许的磁盘 IO,其他场景禁止 |
cjson.decode() | 禁止 | JSON 解析耗时,应在 init 阶段完成 |
socket.connect() | 禁止 | 网络 IO 阻塞 |
ngx.location.capture() | 禁止 | 可能导致请求嵌套死循环 |
与 free_waf 的关系
bt_ngx_waf 与宝塔 free_waf 是互斥的两个防火墙插件,架构上有以下区别:
| 对比项 | bt_ngx_waf | free_waf |
|---|---|---|
| 基础 | Lua 全量重写 | 传统 Nginx + Lua 混合 |
| 运行时磁盘 IO | 仅拦截日志同步直写 | 有(规则逐行读取 + 日志写入) |
| 配置格式 | JSON(结构化) | 纯文本(每行一条规则) |
| 规则加载 | init 阶段一次性载入数组 | access 阶段逐行遍历文件 |
| 站点级配置 | 完整三态继承 | 基础支持 |
| CC 防护 | 滑动窗口 + CC2 挑战 | 基础频次限制 |
| 依赖 | OpenResty + lua-cjson | OpenResty + lua-cjson |
本项目参考了 ngx_lua_waf 和 free_waf 的防护逻辑,结合现代 OpenResty 最佳实践进行了重构优化。
关于 webshell 查杀模块
bt_ngx_waf 未实现 webshell 查杀模块。如需服务器端木马检测功能,请到宝塔面板插件市场搜索 "木马检测" 插件安装使用。
许可证
本项目基于开源许可证发布,具体请查看仓库中的 LICENSE 文件。

