跳转到内容

WAF 可编排规则设计

本文定义 OpenFlare WAF 从固定判定链重构为可视化有向无环图(DAG)的目标架构、数据模型、执行语义、发布模型与迁移边界。IP 组的来源与成员计算仍遵循 WAF 设计,本文只改变规则如何组合和执行。

目标与边界

用户新增 WAF 规则时只输入名称。Server 随即创建一张合法的默认图 开始 → 通过,前端进入基于 React Flow 的独立编排页面。用户通过添加处理单元、配置节点并连接分支构建策略,不再填写固定顺序的黑白名单与 PoW 表单。

支持的节点:

节点数量约束输入输出配置
开始每张图恰好一个next
通过每张图恰好一个一个或多个
阻止可创建多个一个或多个HTTP 状态码、HTML 响应体
IP 匹配可创建多个一个或多个truefalseIP、CIDR、IP 组 ID
地域匹配可创建多个一个或多个truefalse国家代码、地区代码
UA 检查可创建多个一个或多个truefalse要求携带 UA、浏览器/OS 白名单与 and/or、屏蔽爬虫/非正常 UA(不含爬虫)/自定义正则
安全防护可创建多个一个或多个truefalse基础特征检测(路径穿越/文件包含默认开;SQL/XSS/命令注入/SSRF/上传/XXE/CRLF 可开关);命中任一已启用规则为 false
PoW可创建多个一个或多个next算法、难度、会话 TTL、挑战 TTL

IP 匹配、地域匹配、UA 检查与安全防护不区分黑名单或白名单。true 只表示请求通过该节点判定,false 只表示未通过;放行或阻止的业务含义完全由连线决定。UA 检查的求值顺序为:要求携带 UA → 屏蔽爬虫/非正常 UA → 白名单匹配。安全防护在请求 Path/Query/Header/Cookie/Body(有限)上做特征匹配。PoW 验证完成后沿 next 继续,未完成时由挑战页面接管当前请求,不产生 false 分支。

不实现循环、脚本节点、任意表达式节点、子图调用和跨规则跳转。

控制面架构

规则图采用控制面编辑态和数据面运行态分离的双模型:

  1. React Flow 编辑器提交版本化图 JSON,其中包含节点 ID、节点类型、显示名称、坐标、类型化配置和连线。
  2. Server 对整张图执行权威校验,通过后以单个事务保存图并递增修订号。
  3. 配置发布时,Server 再次校验所有启用规则,将图编译为不含坐标、标签等 UI 字段的紧凑运行时 DAG,并收集被引用的 IP 组 ID。
  4. Agent 原子落盘完整发布快照并 reload OpenResty。新 Worker 启动时只加载和解析一次规则 JSON。
  5. 请求热路径只遍历 Worker 内存中的不可变运行时图,不读取文件、不计算 checksum、不解析 JSON。

编辑态 JSON 使用明确的 schema_version。节点配置使用按节点类型区分的结构,不允许用无约束键值对象绕过 Server 校验。初始安全上限为每条规则 128 个节点、256 条边和 256 KiB 编辑态 JSON;这些限制由 API 和发布编译器共同执行。

图结构约束

规则保存与发布必须满足全部约束:

  • 图是有向无环图,禁止自环和任意循环。
  • 恰好存在一个开始节点和一个通过节点;阻止节点可以存在多个。
  • 开始节点无入边且恰好有一个 next 出口;通过和阻止节点无出口。
  • IP 匹配、地域匹配、UA 检查与安全防护的 truefalse 出口必须各连接一次;PoW 的 next 必须连接一次。
  • 除终止节点外不得存在悬空出口;每个非开始节点至少有一条入边。
  • 所有节点都必须从开始节点可达,且从每个可执行节点出发都能抵达通过或阻止。
  • 边的源端口必须属于源节点类型;同一源端口不得连接多个目标。
  • 节点 ID 在图内唯一,边 ID 在图内唯一,所有边引用的节点必须存在。
  • 节点配置必须通过对应类型的字段、范围、引用存在性和体积校验。

前端提供即时校验和连线限制以改善体验,但 Server 是唯一权威校验方。删除节点时前端同步删除关联边并将规则标记为未保存;图恢复合法前禁止保存。

多规则执行语义

一个路由可以绑定多条自定义规则。绑定关系是有序列表,并遵循以下顺序:

  1. 启用的全局规则固定最先执行,不参与路由侧排序。
  2. 路由绑定的启用规则按绑定顺序依次执行。
  3. 当前规则抵达阻止节点时立即输出该节点配置的响应并终止请求。
  4. 当前规则抵达通过节点时,只表示当前规则执行完成;若仍有后续规则则继续执行。
  5. 全部规则均抵达通过节点后,请求才真正放行并进入后续 OpenResty/回源链路。

运行时图在发布前已经过完整校验。若 Lua 执行器仍遇到未知节点、未知端口、缺失目标或超过节点步数上限,则记录限频错误并阻止请求,避免损坏的安全配置意外放行。

IP 组内存刷新

规则拓扑只在发布并 reload OpenResty 时生效;IP 组成员仍可由手动、订阅或自动任务独立更新,不要求发布或 reload。

IP 组采用协调 Worker、共享快照和 Worker 本地对象的两级缓存:

  1. 请求始终读取当前 Worker 内存中的 IP 组对象,不访问文件或共享字典中的 JSON。
  2. 每 5 秒只有一个取得共享锁的 Worker 读取轻量 checksum 文件。
  3. checksum 未变化时立即结束,不读取完整 waf_ip_groups.json
  4. checksum 变化时,协调 Worker 读取并验证一次完整 JSON,再把原始快照按 checksum 写入独立的 64 MiB ngx.shared.openflare_waf_ip_groups,最后更新提交指针。
  5. 其他 Worker 发现共享版本变化后,从共享内存取得快照、解析并原子替换各自的本地对象,不重复读取磁盘。
  6. 刷新失败时继续使用上一份有效对象,限频记录错误,并在下一周期重试。

Agent 必须先原子替换 IP 组 JSON,最后原子更新 checksum,使 Worker 永远不会把半写入文件识别为新版本。Server 发布/同步和 Agent 落盘共同执行 20 MiB 聚合快照上限;共享字典使用不会强制淘汰旧键的安全写入,失败时保留当前与上一代不可变快照。

API 与编辑器

创建接口只接受规则名称,创建成功后返回带默认图的规则详情。规则元数据、图保存和路由绑定使用独立操作,避免修改启用状态或绑定时覆盖画布。

图详情包含 revision。保存请求提交 revision + graph,Server 仅在修订号匹配时更新并递增修订号;不匹配时返回冲突,前端提示重新加载,禁止静默覆盖其他页面的修改。路由绑定接口接受有序规则 ID 数组。

React Flow 编辑页采用全宽画布和固定右侧属性栏:

  • 顶部提供返回、规则名称、启用状态、校验状态和保存操作。
  • 画布使用紧凑高度和较小的首次适配缩放,支持缩放、平移、框选、删除、自动布局和 MiniMap/Controls 等必要导航能力;节点拖动由 React Flow 本地受控状态实时处理,拖动结束后才把坐标写回编辑图。
  • “添加处理单元”提供 IP 匹配、地域匹配、UA 检查、安全防护、PoW 和阻止;开始与通过由默认图提供且不可删除或重复添加。
  • 选中普通节点或连线后可使用画布删除按钮或 Delete/Backspace 删除;删除节点时同步移除关联连线。
  • 右侧属性栏默认隐藏,选中节点后才显示并用于编辑配置;点击连线或画布空白处时收起。
  • 地域匹配属性使用完整国家与 ISO 3166-2 一级行政区数据;国家选项同时显示本地化名称与代码,行政区支持按国家名、行政区名或代码搜索,避免一次渲染数千个选项。
  • 离开存在未保存变更的页面前必须提示;保存冲突和 Server 校验错误应定位到相关节点或边。

WAF 列表展示规则名称、启用状态、节点数量、应用路由数量和更新时间。新建规则的对话框只有名称字段,成功后立即导航到编排页面。

持久化与迁移

规则记录增加版本化图 JSON 与修订号;绑定记录增加执行顺序。图作为一个聚合整体保存,不拆成节点表和边表,以保证编辑操作的事务边界,并让新增节点类型不必频繁扩展数据库 Schema。

升级现有安装时:

  • 保留规则名称、全局标记、启用状态及路由绑定关系。
  • 所有规则图重置为 开始 → 通过,不迁移旧 IP/地域名单、PoW 或拦截响应配置。
  • 现有绑定按稳定顺序写入顺序字段;全局规则仍固定前置。
  • 新图和运行时稳定后移除旧规则字段、固定顺序编译逻辑和旧前端表单,不长期维护双执行器。

该迁移会让旧防护配置停止生效,升级说明必须显著提示管理员在发布下一版本前重新编排规则。

发布、失败与回滚

规则图只在配置发布时生效。发布前校验或编译失败时拒绝发布,当前活动版本保持不变。Agent 写入、OpenResty 配置检查或 reload 失败时,应用流程失败并恢复上一份有效发布版本。

新 Worker 只接受完整且可解析的规则运行态配置。旧 Worker 在 OpenResty 优雅 reload 期间继续使用旧内存图,新 Worker 使用新图,因此请求不会观察到半更新状态。

地域数据库不可用时,地域匹配返回 false 并限频告警,保持现有行为。IP 组刷新失败时保留旧内存快照。PoW 未完成由挑战模块接管请求,不视为执行错误;PoW 节点配置先以短期键写入 OpenResty 共享内存,再通过 ngx.exec 的显式参数传给内部挑战处理器,不能依赖内部重定向保留 ngx.ctx 或隐式继承请求参数。发布快照中的空规则绑定必须编码为 JSON 空数组;运行时将旧快照中的 null 可选数组按空数组处理,禁止因 cjsonngx.null userdata 中断请求。

基于 Apache License 2.0 发布