mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
[优化] 更新文档
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# Agent 设计文档
|
||||
|
||||
你会学到:Agent 的设计原则、核心功能模块、与 Server 的交互链路,以及如何通过不可变版本模型与三阶段容灾机制来保证配置应用的安全性和可靠性。
|
||||
|
||||
---
|
||||
|
||||
## 需求分析
|
||||
|
||||
在分布式反向代理与边缘安全网关场景中,Agent 扮演着打通控制面(Server)与数据面(OpenResty)的核心角色。由于 Agent 运行在用户实际的节点服务器上,其设计必须遵循以下核心安全与高可用需求:
|
||||
|
||||
1. **主动拉取(Pull 模型)而非被动接收**:Server 不直接持有节点的 SSH 秘钥,也不主动发起向节点的入向连接。所有控制指令与配置更新均由 Agent 主动通过心跳(Heartbeat)或长连接(WebSocket)向上拉取。这消除了节点侧的入向防火墙安全隐患,防止了控制通道被劫持。
|
||||
2. **极低侵入性**:Agent 作为一个独立的 Go 二进制进程运行,只与本地 OpenResty 进程进行基于文件的配置重写与信号通知交互,不干涉节点上的其他系统服务。
|
||||
3. **极强容灾与自愈能力**:由于网络抖动、磁盘写满或异常配置等因素极易导致配置同步失败,Agent 必须具备零依赖的本地回滚自愈能力,严防因单次配置失误导致整机服务彻底瘫痪。
|
||||
4. **纯粹的数据与状态落地**:Agent 仅负责承载 Server 渲染好的文件与控制意图落地,不包含复杂的业务逻辑校验、多端租户鉴权等控制面职责,确保了节点侧的高效与轻量。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
Agent 主要由以下核心子模块组成,共同配合完成其完整的生命周期管理:
|
||||
|
||||
| 模块名称 | 对应目录 | 功能职责 |
|
||||
| :--- | :--- | :--- |
|
||||
| **配置同步** | `sync/` | 负责拉取完整配置包,写入文件,触发重载,记录并回报同步状态。 |
|
||||
| **心跳管理** | `heartbeat/` | 定期向 Server 上报节点健康状态、资源指标,并获取最新激活版本摘要。 |
|
||||
| **WebSocket** | `wsclient/` | 保持与 Server 的长连接,提供秒级实时的配置推送与控制面指令响应。 |
|
||||
| **OpenResty 管控** | `nginx/` | 执行 Nginx 配置校验 (`openresty -t`)、重写、平滑重载 (`reload`) 及进程自启动。 |
|
||||
| **本地状态库** | `state/` | 持久化记录本地应用版本、错误日志及未成功上报的可观测性指标缓冲。 |
|
||||
| **自更新服务** | `updater/` | 监听 Server 自更新指令,安全拉取新版本二进制并完成原地热升级。 |
|
||||
| **可观测性** | `observability/` | 采集系统宿主机 CPU/内存/磁盘及 Nginx 性能指标,处理访问日志并上报。 |
|
||||
| **GeoIP 维护** | `geoipdata/` `geoipupdate/` | 维护并定期更新本地 GeoIP 数据库,为 WAF 地域过滤提供支撑。 |
|
||||
|
||||
---
|
||||
|
||||
## 与 Server 的交互链路
|
||||
|
||||
Agent 在生命周期中主要通过 **基于 Token 的自动注册** 和 **心跳/WebSocket 双通道** 与控制面通信。
|
||||
|
||||
### 1. 自动注册流程
|
||||
若 Agent 启动时本地 `agent.json` 的 `access_token` 为空,但配置了 `discovery_token`,将触发自动注册流程:
|
||||
1. Agent 向控制面 `/api/agent/register` 发送注册请求,携带本地硬件摘要、IP 及主机名。
|
||||
2. Server 校验 `discovery_token` 有效后,在数据库生成唯一的 `NodeID` 与专属 `AccessToken`(即 `agent_token`)并返回。
|
||||
3. Agent 将获取的专用 Token 写入本地配置文件,擦除一次性 `discovery_token`,后续所有的通信均基于专属 `AccessToken` 进行鉴权认证。
|
||||
|
||||
### 2. 双通道心跳与同步机制
|
||||
* **HTTP 轮询通道(兜底与探测)**:Agent 默认按设定的 `heartbeat_interval` 间隔发送 POST 心跳包。上报指标的同时获取当前激活版本的摘要信息(Version & Checksum)。
|
||||
* **WebSocket 通道(实时通信)**:在 HTTP 心跳成功后,Agent 自动尝试将连接升级为 WebSocket (`/api/agent/ws`)。
|
||||
* WS 连接建立后,心跳与指标上报全面转移到 WS 管道,降低网络开销。
|
||||
* Server 发布或激活新版本时,通过 WS 广播通知 Agent。Agent 收到变更事件后,**立即触发同步流程**,实现秒级配置生效。
|
||||
* 若 WS 链路因网络问题断开,Agent 自动降级为 HTTP 轮询,并采用指数退避机制尝试重建 WS。
|
||||
|
||||
### 3. 交互时序图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Agent as OpenFlare Agent
|
||||
participant OR as 本地 OpenResty
|
||||
participant Server as OpenFlare Server
|
||||
|
||||
Note over Agent: 首次启动 (无 AccessToken)
|
||||
Agent->>Server: 1. 自动注册请求 (携带 discovery_token)
|
||||
Server-->>Agent: 2. 颁发 NodeID 与专属 AccessToken (agent_token)
|
||||
Note over Agent: 存储 Token 至本地配置文件
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
Note over Agent, Server: HTTP 兜底与 WebSocket 升级
|
||||
Agent->>Server: 3. 发送 HTTP Heartbeat (上报系统状态与健康度)
|
||||
Server-->>Agent: 4. 返回 ActiveConfig 摘要及 AgentSettings
|
||||
Agent->>Server: 5. 发起 WebSocket 升级请求 (/api/agent/ws)
|
||||
Server-->>Agent: 6. 升级成功 (建立双向持久实时通道)
|
||||
end
|
||||
|
||||
rect rgb(245, 245, 245)
|
||||
Note over Agent, Server: 实时配置发布应用链路
|
||||
Note over Server: 管理员在 UI 点击发布配置
|
||||
Server->>Agent: 7. 通过 WS 广播新配置摘要 (WSMessageTypeActiveConfig)
|
||||
Agent->>Server: 8. 请求拉取完整配置详情 (携带目标 Version/Checksum)
|
||||
Server-->>Agent: 9. 返回完整配置快照 (Nginx配置、证书、WAF规则等)
|
||||
Note over Agent: 备份旧文件,写入新配置至本地临时路径
|
||||
Agent->>OR: 10. 执行配置语法校验 (openresty -t)
|
||||
OR-->>Agent: 11. 返回语法校验结果 (OK)
|
||||
Agent->>OR: 12. 平滑重载信号 (openresty -s reload)
|
||||
Agent->>Server: 13. 上报应用成功状态 (Apply Log & ActiveVersion)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OpenResty 的管控
|
||||
|
||||
Agent 对数据面 OpenResty 的管控实现了端到端的闭环,包含配置落地、语法验证、平滑重载和异常状态捕获:
|
||||
|
||||
### 1. 配置文件的落地组织
|
||||
同步成功后,Agent 会将配置按照特定的物理结构写入到本地 `/etc/nginx/openflare-lua/` 目录下(或配置指定的 `LuaDir`):
|
||||
* `nginx.conf`:主配置文件(替换相关占位符,配置性能参数、Shared Dictionaries 及全局 Server)。
|
||||
* `routes.conf`:路由配置文件(由 Agent 生成,包含所有代理网站的 Server 块、证书路径、缓存及速率限制指令)。
|
||||
* `certs/`:证书存放目录(文件命名为 `{cert_id}.crt` 和 `{cert_id}.key`)。
|
||||
* `waf/` 与 `pow/`:WAF 及防 CC 挑战所需的专用 Lua 运行时脚本。
|
||||
* `waf_config.json` 与 `waf_ip_groups.json`:WAF 过滤引擎所需的结构化规则配置文件。
|
||||
|
||||
### 2. 精细化的重载动作
|
||||
1. **备份当前配置**:在写入新文件之前,Agent 会将现有的配置文件复制到 `.backup` 临时目录下,保留完整的现场快照。
|
||||
2. **写入并替换占位符**:将最新拉取的模板写入,自动将模板中的绝对路径占位符(如 `__OPENFLARE_LUA_DIR__`)替换为本地实际运行路径。
|
||||
3. **语法校验**:调用 `openresty -t -c <temp_nginx.conf>` 进行严格的语法测试。
|
||||
4. **平滑重载**:若校验通过,将新配置移至正式路径,执行 `openresty -s reload`。若 OpenResty 处于未启动状态,则使用当前配置拉起进程。
|
||||
5. **捕获异常**:校验或重载失败时,Agent 会截获标准错误输出(stderr),提取前 2000 个字符的详细报错信息。
|
||||
|
||||
---
|
||||
|
||||
## 发布与配置应用模型
|
||||
|
||||
OpenFlare 摒弃了动态 Patch 节点配置的落后方式,采用 **不可变配置版本发布模型**。
|
||||
|
||||
```text
|
||||
修改规则 -> 预览 / 查看 diff -> 发布 -> 生成完整配置版本 -> 激活版本 -> Agent 拉取 -> 本地应用 -> 上报结果
|
||||
```
|
||||
|
||||
### 1. 核心设计原则
|
||||
* **完整发布**:每次发布均是对当前控制面所有启用路由、证书、全局与局部 WAF 规则进行一次性全量编译,生成带唯一 `checksum` 的完整版本。
|
||||
* **版本格式**:采用 `YYYYMMDD-NNN` 递增格式,确保版本历史直观、具备单调递增性。
|
||||
* **全局单激活版本**:系统同时只有一个处于 `active` 状态的全局配置版本。回滚时无需逆向打补丁,只需将历史某个健康版本的状态改为 `active`,Agent 重新拉取应用即可。
|
||||
|
||||
### 2. 三阶段容灾回滚机制
|
||||
当 Agent 发现配置应用(或平滑重载)失败时,将自动激活以下三阶段容灾防瘫痪链路:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[配置应用失败] --> B[第一阶段: 尝试本地备份恢复]
|
||||
B -- 备份文件存在 --> C[写入本地备份文件]
|
||||
C --> D[执行 openresty -t 校验]
|
||||
D -- 校验成功 --> E[reload 恢复旧版本运行]
|
||||
D -- 校验失败 --> F[进入第二阶段]
|
||||
B -- 无备份 --> F[第二阶段: 写入内置安全兜底配置]
|
||||
F --> G[写入兜底 nginx.conf: 仅监听 80 端口]
|
||||
G --> H[启用 stub_status 健康检查]
|
||||
G --> I[其他路由统一返回 503 且拦截异常配置]
|
||||
G --> J[尝试拉起 OpenResty 维持基础存活]
|
||||
J --> K[进入第三阶段]
|
||||
E --> L[上报 Apply Warning]
|
||||
K --> M[本地阻断该异常版本重复应用]
|
||||
M --> N[上报 Apply Error 并保留详细报错]
|
||||
```
|
||||
|
||||
1. **第一阶段:本地备份回退**
|
||||
* Agent 尝试从前一步保存的 `.backup` 目录恢复主配置、路由及证书。
|
||||
* 写入备份文件后,重新执行 `openresty -t` 校验。若成功,重载回退并向 Server 上报 `Warning`(警告:应用新版本失败,已自动退回历史健康版本)。
|
||||
2. **第二阶段:内置安全兜底运行**
|
||||
* 若本地不存在备份配置(如首次部署即配置错误),或者回退备份配置依然校验失败,Agent 将激活最终自愈机制——写入**内置安全兜底配置**。
|
||||
* **安全兜底配置规范**:
|
||||
* 仅监听 `80` 端口,不包含任何用户的真实反代路由。
|
||||
* 除 `/openflare/stub_status` 健康监测路由返回正常外,其他一切访问请求统一返回状态码 `503 Service Unavailable`,响应体固定为 `OpenFlare: No Valid Configuration`。
|
||||
* 尝试以此极简配置拉起 OpenResty。这能够确保 Nginx 进程自身不瘫痪,保留了底层的健康检查与探针通道,防止容器/Pod 因健康检查失败而被调度系统不断销毁重启,同时保护了敏感路由的安全性。
|
||||
3. **第三阶段:本地配置阻断**
|
||||
* Agent 会将当前导致崩溃的配置 `version + checksum` 记录在本地状态库的阻断名单中。
|
||||
* 在控制面未激活新的配置(`checksum` 发生变化)之前,Agent 心跳将阻断对此异常版本的重复同步拉取,防止节点陷入“心跳 -> 拉取崩溃配置 -> 崩溃回滚”的死循环。
|
||||
|
||||
### 3. WAF IP 组运行时异步同步
|
||||
为了避免高频变动的恶意 IP 黑名单频繁触发主配置的全量发布与 reload(平滑重载对 Nginx 依然有微小的 CPU 与连接开销),IP 组成员采用了与发布版解耦的**异步差分同步设计**:
|
||||
|
||||
* **静态发布快照**:发布生成的 `waf_config.json` 中仅包含规则组对 IP 组的引用关系(即 `ip_whitelist_group_ids` / `ip_blacklist_group_ids`),不包含具体的 IP 成员列表。
|
||||
* **心跳差分对比**:Agent 在心跳包中上报本地已缓存 IP 组的 MD5 Checksum 映射表。
|
||||
* **差分下发**:Server 比对当前激活版本引用的 IP 组哈希,仅向 Agent 下发缺失或发生变更的 IP 组成员,写入本地 `waf_ip_groups.json`,实现极速差分同步。
|
||||
* **WebSocket 实时通知**:当 Server 手动更新 IP 组、订阅源自动同步成功、或安全规则自动触发临时封禁时,Server 会立即通过 WebSocket 广播受影响的 IP 组更新包,Agent 接收落地并即时生效,全程**无须 reload Nginx**。
|
||||
|
||||
---
|
||||
|
||||
## 设计约束
|
||||
|
||||
为保证数据与控制链路的安全边界,Agent 代码编写与二次开发必须严格遵守以下工程约束:
|
||||
|
||||
1. **零特权指令通道**:Server 绝对禁止向 Agent 传递任何任意 shell 命令或远程执行脚本(如 exec/eval 等)。所有系统控制原语(如启动、停止、重载、更新)必须硬编码在 Agent 二进制内部。
|
||||
2. **严格的 Token 过滤与前缀验证**:Agent 侧向 Server 请求资源时,接口端点固定以 `/api/agent/` 为前缀,并强制携带 `X-Agent-Token` 进行签名或令牌核验。
|
||||
3. **节点自治原则**:Agent 须具备完备的离线工作能力。在与 Server 失去连接期间,本地 OpenResty 必须依靠本地已落地的配置保持反向代理服务的绝对正常运行。
|
||||
@@ -218,6 +218,6 @@ WAF IP 组由 Server 管理。手动 IP 组直接保存 IP/IP 段列表;自动
|
||||
如果要修改架构相关代码,先阅读:
|
||||
|
||||
1. [产品边界](./index.md)
|
||||
2. [发布模型](./release-model.md)
|
||||
2. [Agent 与发布模型](./agent-design.md)
|
||||
3. [开发约束](../guildline/development-constraints.md)
|
||||
4. [仓库结构](./repository.md)
|
||||
|
||||
@@ -101,7 +101,7 @@ export LOG_LEVEL='debug'
|
||||
go run ./cmd/agent -config ./agent.json
|
||||
```
|
||||
|
||||
未配置 `openresty_path` 时,Agent 默认调用 `openresty`。调试时可显式配置 `openresty_path`、`main_config_path`、`route_config_path`、`access_log_path`、`cert_dir`、`lua_dir` 和 `runtime_config_dir`。
|
||||
未配置 `openresty_path` 时,Agent 默认调用 `openresty`。调试时可显式配置 `openresty_path`、`main_config_path`、`route_config_path` , `access_log_path`、`cert_dir`、`lua_dir` 和 `runtime_config_dir`。
|
||||
|
||||
## 测试
|
||||
|
||||
|
||||
+31
-56
@@ -56,34 +56,28 @@ OpenFlare 当前不定位为通用日志平台、服务网格、Kubernetes Ingre
|
||||
|
||||
## 网站配置约束
|
||||
|
||||
`proxy_routes` 从“单域名规则”升级为“网站配置”聚合对象。一条记录对应一个网站,可绑定一个或多个域名,并共享一组站点级配置。
|
||||
`proxy_routes` 是“网站配置”的聚合对象。一条记录对应一个网站,可绑定一个或多个域名,并共享一组站点级配置。
|
||||
|
||||
约束:
|
||||
|
||||
* `proxy_routes.site_name` 是网站的业务唯一标识。
|
||||
* `proxy_routes.domains` 至少包含一个域名,且 `domains[0]` 作为主域名。
|
||||
* 任一域名全局只能属于一个 `proxy_routes`。
|
||||
* 迁移期可保留 `proxy_routes.domain` 作为 `domains[0]` 的镜像字段,但业务读写与后续扩展必须以 `site_name` + `domains` 为准。
|
||||
* 网站级流量限制、反向代理与缓存配置当前按站点共享,不在同一网站内做域名级差异化配置。
|
||||
* 网站级流量限制、反向代理与缓存配置均按站点共享,不在同一网站内做域名级差异化配置。
|
||||
* HTTPS 允许在同一站点内按域名绑定证书。
|
||||
|
||||
## 源站约束
|
||||
## 源站与上游约束
|
||||
|
||||
`origins` 只保存源站地址、展示名与备注,不承载协议、端口、路径、权重或健康检查策略。
|
||||
|
||||
`proxy_routes` 可选关联一个 `origins` 记录,用于复用源站地址;规则仍保存完整 `origin_url` 快照以参与渲染与版本快照。
|
||||
`origins` 服务于源站目录复用,仅保存源站地址、展示名与备注,不承载协议、端口、路径、权重或健康检查策略。`proxy_routes` 可选关联一个 `origins`,但规则内部仍保存完整上游快照以参与渲染。
|
||||
|
||||
上游约束:
|
||||
|
||||
* `proxy_routes` 至少包含一个上游地址(直连类型),或关联一个 Tunnel(内网穿透类型)。
|
||||
* `proxy_routes.upstream_type` 区分上游类型:`direct`(默认,直连)或 `tunnel`(内网穿透)。
|
||||
* 为兼容历史数据保留 `origin_url` 主上游字段,也允许在同一规则内补充多个上游做负载均衡。
|
||||
* 上游统一渲染为带 keepalive 的 named `upstream`。
|
||||
* 单上游可附带 base path 或 query 并在 `proxy_pass` 中追加。
|
||||
* 多上游限定为纯 `scheme://host[:port]`。
|
||||
* `proxy_routes` 至少包含一个上游地址(直连类型 `direct`),或关联一个 Tunnel(内网穿透类型 `tunnel`)。
|
||||
* 多上游负载均衡统一渲染为带 keepalive 的 named `upstream`。
|
||||
* 单上游允许附带 base path 或 query,并在 `proxy_pass` 中追加。多上游限定为纯 `scheme://host[:port]` 结构,且同一规则内的协议必须一致。
|
||||
* `proxy_routes.origin_host` 为可选字段,用于回源时覆盖 `Host` 请求头。
|
||||
* 所有直连类型上游地址都必须为合法 `http://` 或 `https://`。
|
||||
* 内网穿透类型上游必须关联 `tunnel_id`,并指定内网目标地址与协议。
|
||||
* 所有直连类型上游地址都必须为合法的 `http://` 或 `https://`。
|
||||
* 内网穿透类型上游必须关联有效 `tunnel_id`,并指定内网目标地址与协议。
|
||||
|
||||
## 内网穿透约束
|
||||
|
||||
@@ -147,12 +141,11 @@ OpenFlare 通过 TunnelRelay 节点与 OpenFlared 客户端实现内网穿透,
|
||||
* **Tunnel 侧配置**:Relay 列表 + frpc 代理定义。随发布流程版本化,变更时优先使用 `frpc reload` 热重载。
|
||||
* **Relay 配置**:通过心跳响应下发,相对静态,不纳入版本化流程。
|
||||
|
||||
### 当前阶段约束
|
||||
### 隧道设计约束
|
||||
|
||||
* 仅支持 HTTP 协议隧道流量,保留未来 TCP/UDP 隧道扩展性。
|
||||
* Tunnel 类型上游的域名 DNS 应仅解析到 TunnelRelay 节点;EdgeNode 上对应请求会因 frps 不可达返回 502。
|
||||
* frp 版本使用 v0.61+(或更新稳定版),frp 二进制由部署脚本或 Docker 镜像提供。
|
||||
* 暂不支持 TCP/UDP 端口分配;HTTP 单端口复用已满足 MVP 需求。
|
||||
* 仅支持 HTTP 协议隧道流量(保留 TCP/UDP 隧道的可扩展性),暂不支持单独的 TCP/UDP 端口分配。
|
||||
* Tunnel 类型上游的域名 DNS 应当解析到指定的 TunnelRelay 中继节点。
|
||||
* frp 二进制(v0.61+)由系统部署脚本或容器镜像统一打包提供。
|
||||
|
||||
|
||||
## HTTPS 约束
|
||||
@@ -167,48 +160,30 @@ OpenFlare 通过 TunnelRelay 节点与 OpenFlared 客户端实现内网穿透,
|
||||
|
||||
## WAF 约束
|
||||
|
||||
WAF 以规则组为配置边界。系统固定一个全局规则组,默认应用到所有网站;网站可叠加多个自定义规则组。
|
||||
WAF 以规则组为核心配置边界。系统提供唯一的全局规则组(默认应用至所有站点),网站可在此基础上叠加多个自定义规则组。
|
||||
|
||||
一期支持:
|
||||
核心能力:
|
||||
|
||||
* IP / IP 段白名单与黑名单。
|
||||
* IP 组引用,支持手动、自动、订阅三类 IP 组。
|
||||
* 国家级地域白名单与黑名单。
|
||||
* 规则组级拦截状态码与响应页面,默认 `418` 与空页面。
|
||||
* 支持单个 IP / CIDR 网段黑白名单。
|
||||
* 支持 IP 组引用(包括手动、自动Expr计算、URL订阅三类 IP 组)。
|
||||
* 支持基于 GeoIP 的国家/地区级地域准入过滤。
|
||||
* 支持规则组自定义拦截响应(支持自定义状态码与拦截 HTML 页面,默认返回 `418`)。
|
||||
|
||||
IP 组约束:
|
||||
IP 组与判定约束:
|
||||
|
||||
* 手动 IP 组由管理端直接维护 IP/IP 段列表。
|
||||
* 自动 IP 组使用 Expr 语法保存自定义规则,由 Server 定时按单个 IP 聚合请求日志并更新 IP 列表。
|
||||
* 订阅 IP 组由 Server 定时从 HTTP/HTTPS URL 同步,支持文本列表和 JSON 映射。
|
||||
* WAF 运行时不访问数据库;发布版本只保存规则组引用的 IP 组 ID,不把 IP 组成员展开进版本快照。
|
||||
* Agent 通过心跳上报本地 IP 组 checksum,Server 仅返回 checksum 不一致的 IP 组;Server 侧 IP 组更新时会通过 Agent WebSocket 主动广播变更组,使节点可在不重新发布配置版本的情况下更新 WAF IP 组内容。
|
||||
|
||||
自动 IP 组首批内置预设规则:
|
||||
|
||||
* 单个 IP 请求数大于 100,且 404 状态码占比不低于 80%:`request_count > 100 && status_404_ratio >= 0.8`
|
||||
* 单个 IP 通过 IP 地址访问次数大于 50,且通过 IP 地址访问占比大于 50%:`ip_host_count > 50 && ip_host_ratio > 0.5`
|
||||
|
||||
判定顺序:
|
||||
|
||||
* 白名单是放行例外,任意启用规则组命中白名单即放行。
|
||||
* 未命中白名单时继续判断黑名单。
|
||||
* 多个黑名单命中时,全局规则组优先,其后按自定义规则组 ID 升序。
|
||||
|
||||
地域识别由 Agent 维护节点本地 MaxMind mmdb,OpenResty Lua 在请求路径中读取本地库。GeoIP 依赖不可用时只能跳过地域规则,不得影响 IP 规则与反向代理主链路。
|
||||
* **运行时解耦**:WAF 运行时只读取本地 JSON,不访问 Server 数据库;配置版本仅保存引用的 IP 组 ID。IP 组成员通过哈希 Checksum 差分心跳及 WebSocket 异步推送,实现无需平滑重载 Nginx 的热生效。
|
||||
* **内置预设 Expr 规则**:
|
||||
* 高频 404 扫描封禁:`request_count > 100 && status_404_ratio >= 0.8`
|
||||
* 恶意 IP 直连探测:`ip_host_count > 50 && ip_host_ratio > 0.5`
|
||||
* **判决优先级**:白名单拥有绝对优先权。若未命中白名单,则触发黑名单漏斗匹配(全局规则组优先,自定义组按 ID 升序匹配)。
|
||||
* 地域解析依赖节点本地 MaxMind 库;当 GeoIP 异常时自动忽略地域规则,不得破坏 IP 规则与反代主链路的可用性。
|
||||
|
||||
## 认证源约束
|
||||
|
||||
`auth_sources` 是管理端第三方登录入口的配置对象,当前仅支持 `github` 与 `oidc` 两类。启用后的认证源会显示在登录页。
|
||||
`auth_sources` 统一支持 `github` 与 `oidc` 登录配置入口。`external_accounts` 存储第三方与本地用户的绑定关系。第三方账号首次接入逻辑:
|
||||
|
||||
`external_accounts` 保存认证源外部账号与本地用户的绑定关系。第三方账号首次登录时:
|
||||
|
||||
* 已绑定本地用户则直接登录。
|
||||
* 当前已有本地登录 Session 时,绑定到当前用户。
|
||||
* 未绑定且允许注册时,自动创建普通用户并绑定。
|
||||
* 未绑定且关闭注册时,只允许用户输入已有本地账号密码完成绑定。
|
||||
|
||||
旧 `users.github_id` 仅作为升级迁移来源,新的第三方账号登录与绑定关系必须以 `external_accounts` 为准。
|
||||
* 已绑定时直接授权登录;若已有本地会话则自动建立绑定。
|
||||
* 未绑定且允许注册时自动创建本地账号;若关闭注册,则要求用户提供已有本地账号密码以建立关联。
|
||||
|
||||
## 版本与观测约束
|
||||
|
||||
@@ -223,9 +198,9 @@ IP 组约束:
|
||||
|
||||
* 产品范围或系统边界变化时更新本文档。
|
||||
* 系统结构或模块职责变化时更新 [系统架构](./architecture.md)。
|
||||
* 发布、同步、回滚模型变化时更新 [发布模型](./release-model.md)。
|
||||
* 发布、同步、回滚与 Agent 模型变化时更新 [Agent 与发布模型](./agent-design.md)。
|
||||
* 开发约束、代码规范、接口约定变化时更新 [开发约束](../guildline/development-constraints.md)。
|
||||
* 部署方式变化时更新 [部署说明](../reference/deployment.md) 与 README。
|
||||
* 部署方式变化时更新 [部署说明](../deployment/deployment.md) 与 README.
|
||||
* 配置项变化时更新 [配置项参考](../reference/configuration.md)。
|
||||
* 已完成阶段不再以“版本计划”形式回填。
|
||||
* 新阶段开始前,先补设计,再进入实现。
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# 发布模型
|
||||
|
||||
你会学到:OpenFlare 为什么以完整配置版本为发布单位,发布、激活、Agent 应用和回滚分别如何工作。
|
||||
|
||||
OpenFlare 的发布模型以完整配置版本为中心,而不是在线修改节点配置。
|
||||
|
||||
标准链路:
|
||||
|
||||
```text
|
||||
修改规则 -> 预览 / 查看 diff -> 发布 -> 生成完整配置版本 -> 激活版本 -> Agent 拉取 -> 本地应用 -> 上报结果
|
||||
```
|
||||
|
||||
## 发布规则
|
||||
|
||||
Server 发布时必须:
|
||||
|
||||
1. 读取全部启用的 `proxy_routes`。
|
||||
2. 读取 Server 侧 OpenResty 主配置、性能参数、缓存参数和必要 Lua 资源。
|
||||
3. 读取域名与证书绑定关系。
|
||||
4. 读取 WAF 全局规则组、自定义规则组、IP 组引用与网站绑定关系。
|
||||
5. 保留 WAF 规则组引用的 IP 组 ID,渲染完整 OpenResty 配置与 WAF 运行时配置;IP 组成员不进入发布版本。
|
||||
6. 计算 `checksum`。
|
||||
7. 写入 `config_versions`。
|
||||
8. 切换激活版本。
|
||||
9. 让 Agent 在后续 heartbeat 中发现并应用。
|
||||
|
||||
版本号格式固定为 `YYYYMMDD-NNN`。
|
||||
|
||||
## 预览与发布
|
||||
|
||||
预览和 diff 是只读能力,不产生发布记录。
|
||||
|
||||
发布会生成新的完整配置版本。版本必须包含足够信息,让未来回滚时可以基于历史快照重新应用,而不依赖当前可变配置。
|
||||
|
||||
## 激活版本
|
||||
|
||||
全局同时只能有一个激活版本。当前不做按节点分组的差异化版本。
|
||||
|
||||
Agent 通过 heartbeat 获取激活版本摘要;当远端版本或 checksum 与本地状态不一致时,Agent 才进入同步流程。当 Agent WS 连接升级开启且连接可用时,Server 在发布或激活版本成功后会广播最新激活版本摘要,Agent 收到后复用普通同步流程立即拉取并应用配置。WS 不可用时仍按 HTTP heartbeat 间隔发现变更。
|
||||
|
||||
## 不可变历史
|
||||
|
||||
历史版本不可变。回滚不是修改旧版本,而是重新激活旧版本。
|
||||
|
||||
这样做的结果是:
|
||||
|
||||
* 每个版本都可以追溯。
|
||||
* 回滚链路与普通发布应用链路一致。
|
||||
* Agent 不需要理解“反向 patch”,只需要应用一个目标版本。
|
||||
|
||||
## Agent 应用策略
|
||||
|
||||
Agent 发现新版本后会:
|
||||
|
||||
1. 拉取目标版本详情。
|
||||
2. 备份旧文件。
|
||||
3. 写入主配置、路由配置、证书、必要 Lua 资源与 WAF/PoW 运行时配置。
|
||||
4. 执行 OpenResty 配置校验。
|
||||
5. reload;如果运行时未启动,则尝试用当前配置启动 OpenResty。
|
||||
6. 上报成功、警告或失败。
|
||||
|
||||
如果新配置激活失败,Agent 必须尝试恢复运行;回滚成功时上报警告。若本地没有历史主配置可回滚,Agent 会写入内置安全兜底配置并尝试拉起 OpenResty:该配置对外只监听 `80` 端口,不包含任何用户路由,统一返回 `503 Service Unavailable` 与 `OpenFlare: No Valid Configuration`,同时保留本地 `stub_status` 健康检查入口。兜底启动成功时仍阻断失败目标版本并上报警告;存在历史主配置但回滚后仍无法恢复运行时上报失败。
|
||||
|
||||
某个目标 `version + checksum` 一旦应用失败并回退,Agent 会在本地状态中阻断该目标重复应用。只有远端激活版本或 checksum 发生变化,才允许再次尝试。
|
||||
|
||||
## 设计约束
|
||||
|
||||
* 发布必须读取全部启用的网站配置,而不是只渲染本次修改对象。
|
||||
* 回滚通过重新激活旧版本实现,不修改历史版本。
|
||||
* Agent API 固定使用节点专属 `agent_token`,首次接入可使用 `discovery_token`。
|
||||
* Server 不提供远程 shell 或任意命令执行入口。
|
||||
* 配置版本必须保存完整快照、渲染结果和 `checksum`。
|
||||
* WAF 规则组、IP 组引用 ID 和网站绑定关系必须随完整配置版本进入快照与 checksum;IP 组成员由 Agent 独立按 checksum 差异同步,不受版本回滚影响。
|
||||
|
||||
## WAF IP 组运行时同步
|
||||
|
||||
WAF IP 组成员不纳入配置版本。发布版本只包含规则组直接 IP 与 `ip_whitelist_group_ids` / `ip_blacklist_group_ids`。Agent 应用版本后会从渲染出的 `waf_config.json` 中提取引用 ID,并向 Server 请求缺失或 checksum 不一致的 IP 组数据。
|
||||
|
||||
Agent 后续心跳会携带本地 IP 组 checksum。Server 根据当前激活版本引用的 IP 组 ID 对比 checksum,只返回差异组,避免每次心跳传输全部 IP 组。Server 在手动更新、订阅同步或自动规则执行后,会通过 Agent WebSocket 广播发生变化的 IP 组;WS 不可用时,下一次 HTTP heartbeat 仍会按 checksum 差异补齐。
|
||||
@@ -7,7 +7,9 @@
|
||||
| `openflare_server` | Gin + GORM + SQLite/PostgreSQL 单体控制面 |
|
||||
| `openflare_server/web` | Next.js 15 App Router 管理端前端,由 Go Server 托管 |
|
||||
| `openflare_agent` | Go 单体 Agent,运行在节点侧 |
|
||||
| `scripts` | Agent 安装、卸载等辅助脚本 |
|
||||
| `openflare_relay` | Tunnel 中继代理,运行在公网边缘管理 frps 进程 |
|
||||
| `openflared` | Tunnel 客户端,运行在内网服务器侧管理 frpc 进程 |
|
||||
| `scripts` | 安装、自更新等系统辅助脚本 |
|
||||
| `docs` | VitePress 文档站、设计基线、开发规范、部署与配置文档 |
|
||||
| `docs/en` | 英文版文档 |
|
||||
|
||||
@@ -60,3 +62,33 @@
|
||||
| `tests/` | 前端单元测试与集成测试(Vitest、Playwright) |
|
||||
| `scripts/` | 构建和部署相关脚本 |
|
||||
| `public/` | 静态资源 |
|
||||
|
||||
## Relay 模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| `cmd/` | Relay 命令行启动入口及初始化主函数 |
|
||||
| `internal/config/`| 本地配置文件解析与默认参数初始化 |
|
||||
| `internal/frps/` | 管理 frps 进程生命周期、端口与 Token 并监控运行 |
|
||||
| `internal/heartbeat/`| 周期性 HTTP 心跳通信、上报状态并获取更新请求 |
|
||||
| `internal/httpclient/`| Server 的通用 API 客户端调用工具类 |
|
||||
| `internal/observability/`| 采集本地宿主机、frps 的基础运行指标并进行预聚合 |
|
||||
| `internal/relay/` | 协调中继的核心生命周期、初始化与清理 |
|
||||
| `internal/state/` | 本地运行时状态、错误记录与持久化缓存 |
|
||||
| `internal/updater/`| Relay 升级检查、下载安装与重启机制 |
|
||||
| `internal/wsclient/`| 与 Server 保持的长连接 WebSocket 双向通信管道 |
|
||||
|
||||
## OpenFlared (Client) 模块
|
||||
|
||||
| 模块 | 职责 |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| `cmd/` | Client 命令行启动入口及初始化主函数 |
|
||||
| `internal/config/`| 本地客户端配置加载与解析 |
|
||||
| `internal/flared/`| 内网穿透客户端的核心调度与状态管理机制 |
|
||||
| `internal/frpc/` | 热重载/动态生成多 Relay 的 `frpc.toml` 并监控 frpc |
|
||||
| `internal/heartbeat/`| 与控制面进行的心跳通信,包含 Token 校验机制 |
|
||||
| `internal/httpclient/`| 客户端通用 API 通信客户端 |
|
||||
| `internal/sync/` | 增量拉取最新 Tunnel 路由绑定关系、生成快照并应用 |
|
||||
| `internal/updater/`| 客户端自更新、新版检查与更新落地逻辑 |
|
||||
| `internal/wsclient/`| 用于实时监听 Server 端隧道配置变更推送的 WS 信道 |
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
# 内网穿透隧道设计文档
|
||||
|
||||
你会学到:OpenFlare 内网穿透隧道的架构设计、双端管控组件(Relay 与 Client)的内部原理、交互逻辑以及数据面与控制面的通信流程。
|
||||
|
||||
---
|
||||
|
||||
## 需求分析
|
||||
|
||||
在典型的 Web 应用托管场景中,许多源站(Origin Server)部署在内网环境(如本地开发机、局域网服务器或受防火墙限制的内网集群)。这些服务器通常:
|
||||
1. **无公网 IP**:无法直接被公网流量访问。
|
||||
2. **安全合规限制**:不允许随意在边界路由器上配置端口映射(NAT)。
|
||||
3. **动态 IP 变动**:传统的 DDNS 方案延迟高且极不稳定。
|
||||
|
||||
为了让内网源站能够无缝接入 OpenFlare 全局数据网关并享受 WAF 地域防护、TLS 证书托管等增值服务,OpenFlare 设计了基于 **反向中继穿透隧道** 的整体解决方案。在该架构中,公网边缘节点作为反代入口和流量中继,内网侧仅需发起安全出向连接,即可实现公网流量安全、稳定地反向穿透到内网源站。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
内网穿透隧道子系统包含以下核心能力:
|
||||
|
||||
* **Relay 节点动态管理**:由控制面动态派发中继服务(frps),动态分发服务端口与认证令牌(Token)。
|
||||
* **多隧道反向代理映射**:支持在单个内网客户端上映射多个内网 Web 端口,并将多域名路由绑定至对应的中继节点。
|
||||
* **独立进程生命周期管控**:中继与客户端均为 Go 编写的独立二进制守护进程,内部负责拉起、监控、自愈及热升级底层的 frp 引擎。
|
||||
* **基于 Token 的独立认证隔离**:中继端使用 `agent_token`,内网客户端使用专属 `tunnel_token`,权限与路由边界隔离。
|
||||
* **配置校验与增量热重载**:仅在隧道绑定关系、证书或 Relay 拓扑发生实际变化时,才重写配置文件并平滑重载进程,降低运行开销。
|
||||
|
||||
---
|
||||
|
||||
## 内网穿透与隧道架构
|
||||
|
||||
内网穿透子系统基于成熟的 `frp` 高性能隧道协议进行整合,分为 **控制面 (Control Plane)** 与 **数据面 (Data Plane)**。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% 数据流
|
||||
Browser[1. 浏览器 / 访客] -->|HTTPS 请求| Agent[2. OpenResty / Agent]
|
||||
Agent -->|本机转发 proxy_pass| RelayFrps[3. OpenFlare Relay / frps]
|
||||
RelayFrps -->|加密隧道协议| FlaredFrpc[4. OpenFlared / frpc]
|
||||
FlaredFrpc -->|转发本地请求| LocalOrigin[5. 内网源站 192.168.x.x]
|
||||
|
||||
%% 控制流与心跳
|
||||
Server[OpenFlare Server 控制面] <-->|Relay API / Heartbeat| RelayManager[openflare_relay 进程]
|
||||
Server <-->|Client API / Heartbeat| ClientManager[openflared 进程]
|
||||
|
||||
RelayManager -.->|管控进程及配置| RelayFrps
|
||||
ClientManager -.->|管控多 Relay 进程| FlaredFrpc
|
||||
|
||||
style Browser fill:#f9f,stroke:#333,stroke-width:2px
|
||||
style LocalOrigin fill:#9f9,stroke:#333,stroke-width:2px
|
||||
style Server fill:#f96,stroke:#333,stroke-width:2px
|
||||
```
|
||||
|
||||
* **控制面(Control Plane)**:Server 维护数据库状态;中继节点上的 `openflare_relay` 进程与内网服务器上的 `openflared` 进程通过 HTTP 心跳与 WebSocket 长通道同步隧道配置。
|
||||
* **数据面(Data Plane)**:公网流量首先进入公网边缘的 Agent (OpenResty),在此完成 HTTPS 握手、TLS 终止和 WAF 过滤,接着通过 `proxy_pass` 转发到同机部署的 `openflare_relay (frps)`。`frps` 再将请求封包通过与内网 `openflared (frpc)` 建立的持久隧道传输过去,最后由 `frpc` 拆包并分发给内网实际的源站服务。
|
||||
|
||||
---
|
||||
|
||||
## Relay (中继端) 设计
|
||||
|
||||
`openflare_relay` 是部署在公网边缘的中继管理器,运行在 `tunnel_relay` 类型的节点上。
|
||||
|
||||
### 1. 核心架构与逻辑
|
||||
* **进程守护**:Relay 进程内部持有 `frps` 二进制,通过 `exec.Command` 拉起 `frps -c frps.toml` 子进程,并启动 goroutine 异步监听其退出状态。如果发现 `frps` 异常退出,会结合退避机制自动拉起。
|
||||
* **动态配置渲染**:通过 HTTP 心跳向控制面同步状态,获取当前的 `RelayConfig`,主要参数包括:
|
||||
* `bindPort`:frps 用于监听内网 frpc 客户端连接的公网控制端口。
|
||||
* `vhostHTTPPort`:虚拟主机(Virtual Host)HTTP 流量监听端口,Agent 的 proxy_pass 会指向此端口。
|
||||
* `authToken`:客户端连接时进行握手校验的安全凭证。
|
||||
* `webServer`:开启 frps 的仪表盘 API,Relay 基于此接口或管理控制端口收集实时的活跃隧道数和流量指标。
|
||||
* **状态上报**:Relay 每周期心跳会向控制面上报底层 `frps` 的活跃连接数、注册客户端数、各个代理通道的实时状态以及 Relay 版本。
|
||||
|
||||
---
|
||||
|
||||
## Openflared (客户端) 设计
|
||||
|
||||
`openflared` 是运行在用户内网服务器侧的客户端管理器,使用独立的 `tunnel_token` 进行鉴权。
|
||||
|
||||
### 1. 核心设计机制
|
||||
* **多 Relay 支持(多路复用)**:
|
||||
为保障高可用或就近接入,控制面可能会将客户端连接调度到多个公网 Relay。`openflared` 会读取 `TunnelConfig` 中下发的 Relays 列表,在本地为每一个 Relay 节点独立生成一个专用的配置文件(命名为 `frpc_<relay_node_id>.toml`),并分别为每个 Relay 进程分配独立的 cancelable context。
|
||||
* **子进程独立监控**:
|
||||
`openflared` 内部维护一个 `processes` 映射表,对每个 `frpc` 子进程进行独立的生命周期管控。当控制面增加或移除 Relay 时,客户端会增量拉起新进程或优雅注销老进程,避免影响其他正常工作的隧道。
|
||||
* **动态 TOML 生成**:
|
||||
为每个 Relay 渲染 TOML 时,客户端会遍历 Proxies 列表,将每个内网服务的 `LocalAddr`、`LocalPort`、绑定的 `CustomDomains` 写入到 `[[proxies]]` 块中。
|
||||
|
||||
---
|
||||
|
||||
## 交互逻辑与流量模型
|
||||
|
||||
内网穿透子系统实现了一致性版本控制和状态反馈。
|
||||
|
||||
### 1. 控制面发布与同步流程
|
||||
|
||||
```text
|
||||
管理员修改隧道/内网端口映射 -> 提交发布 -> 生成新 Tunnel 版本与 Checksum
|
||||
|
|
||||
v (推送或心跳拉取)
|
||||
+-------------------------------------------+-------------------------------------------+
|
||||
| |
|
||||
v (中继端) v (内网客户端)
|
||||
openflare_relay 心跳检测到 frps 端口/Token 变化 openflared 心跳检测到 tunnel_version 发生变更
|
||||
重新渲染本地 frps.toml 请求拉取最新代理映射包
|
||||
Kill 并重新拉起 frps 进程 重新渲染 frpc_<relay_id>.toml
|
||||
上报健康状态为 healthy 对有变更的 Relay 进程执行重启与配置热重载
|
||||
上报应用结果 (Apply Success/Error)
|
||||
```
|
||||
|
||||
1. **版本化控制**:所有内网隧道的路由和映射关系与主路由系统类似,也经过版本化控制,下发 `version` 与 `checksum`,确保客户端不重复写入和频繁重载进程。
|
||||
2. **应用结果闭环**:客户端应用新配置后,会在心跳中携带应用结果上报控制面。若因内网端口不可达或证书配置有误导致 frpc 无法建连,客户端会截获进程输出将 `LastError` 上报,管理员在 Server 即可直观查看穿透失败原因。
|
||||
|
||||
### 2. 数据面流量模型
|
||||
1. **公网入口 (Agent)**:
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name intranet.example.com;
|
||||
# ... TLS 证书与 WAF 过滤逻辑 ...
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:18080; # 指向本地 frps 的虚拟主机端口
|
||||
proxy_set_header Host $host; # 必须保留原 Host,因为 frps 依靠 Host 进行内部路由分发
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
2. **中继节点 (frps)**:
|
||||
`frps` 监听到 `18080` 端口有 HTTP 请求进来,读取 HTTP 请求头中的 `Host: intranet.example.com`,在其已注册的活跃隧道表中检索该域名对应的加密 TCP 连接(由内网 frpc 建立)。
|
||||
3. **加密隧道传输 (TCP)**:
|
||||
`frps` 将 HTTP 请求封装进内部 TCP 隧道协议,发送给内网的 `frpc` 客户端。
|
||||
4. **内网客户端分发 (frpc)**:
|
||||
`openflared` 管理的 `frpc` 收到封包,根据本地配置(`localIP = "127.0.0.1"`, `localPort = 8080`)将请求建立本地 TCP 连接转发给内网 Web 服务,并将 Web 服务的响应原路打包返回,最终呈现给公网用户。
|
||||
@@ -0,0 +1,130 @@
|
||||
# WAF 设计文档
|
||||
|
||||
你会学到:OpenFlare 边缘 Web 应用防火墙(WAF)的核心架构、动态 IP 组异步差分同步模型、OpenResty Lua 高性能缓存方案以及完整的请求过滤与判定逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 需求分析
|
||||
|
||||
在互联网公开环境中,Web 应用程序面临着各种各样的安全威胁(如扫描器踩点、刷接口、针对特定地域的恶意网络爬虫、勒索攻击及 CC 攻击等)。如果直接把恶意请求放行给源站(Origin Server),会导致:
|
||||
1. **源站负载飙升**:高频的数据库查询与 CPU 运算极易耗尽服务器资源。
|
||||
2. **敏感接口被刷**:登录、注册、短信验证码接口容易被恶意滥用导致财产损失。
|
||||
3. **数据泄露风险**:恶意的通用漏洞探测行为无法被提前拦截。
|
||||
|
||||
因此,OpenFlare 需要在最前端的数据面(OpenResty)构建一套 **高性能、可弹性伸缩的 WAF 过滤引擎**。该引擎能够在最接近用户的边缘层以毫秒级的极低开销对恶意请求进行深度过滤,减轻源站压力,并提供防 CC(PoW 挑战)、IP 黑白名单与地域级别拦截等核心安全防护能力。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
|
||||
OpenFlare WAF 包含以下核心防护维度:
|
||||
|
||||
* **IP 级拦截(IP 黑白名单)**:支持单 IP、CIDR 网段过滤,支持将上万 IP 聚合为 IP 组进行高效比对。
|
||||
* **地域黑白名单(GeoIP 限制)**:集成 MaxMind 数据库,支持针对国家(Country)和省份/地区(Region)执行精准准入控制。
|
||||
* **自定义拦截响应**:支持针对不同的过滤规则自定义阻断状态码(如 403, 418)以及个性化的 HTML 拦截页面。
|
||||
* **人机挑战(PoW CC 防护)**:支持无感人机挑战,通过计算 Hash 碰撞防止自动化脚本和僵尸网络(Botnet)对接口进行并发冲击。
|
||||
|
||||
---
|
||||
|
||||
## IP 组设计与动态异步同步
|
||||
|
||||
IP 组是 WAF 进行高效黑白名单管控的核心容器。OpenFlare 将 IP 组根据更新频率与产生渠道分为三类:
|
||||
|
||||
### 1. IP 组类型
|
||||
* **手动 IP 组(Manual)**:由管理员在控制面板上手动输入 IP 或 CIDR 列表。主要用于静态的信任 IP 或长期的封禁。
|
||||
* **订阅 IP 组(Subscription)**:配置远程文本(按行分隔)或标准的 JSON 订阅地址。Server 侧的定时任务会周期性抓取远程订阅源并自动解析导入。主要用于集成开源的威胁情报库、云厂商的 IP 范围等。
|
||||
* **自动 IP 组(Automatic)**:**最具弹性的动态防护通道**。控制面的定时扫描任务会读取所有节点的访问日志,按照设定的 Expr 规则(例如:“5分钟内请求 `/api/login` 接口触发 401 超过 50 次”)进行聚合分析,一旦匹配,自动将该恶意源 IP 写入封禁组,并指定封禁时长。
|
||||
|
||||
### 2. 异步差分同步设计 (不触发 Nginx Reload)
|
||||
在传统的 Nginx WAF 设计中,IP 黑名单的更新通常需要重写配置并 reload。如果恶意 IP 封禁以秒级或分钟级高频触发,频繁 reload 会导致 Nginx 频繁新建 Worker 进程并销毁老进程,导致性能骤降。
|
||||
|
||||
OpenFlare 采用 **动态 IP 组异步差分同步设计**:
|
||||
|
||||
```text
|
||||
WAF IP 成员更新 (手动/订阅/自动自动触发)
|
||||
|
|
||||
v
|
||||
Server 更新数据库并计算该 IP 组的全新 MD5 Checksum
|
||||
|
|
||||
+----------------------------------------+
|
||||
| (WebSocket 实时广播) | (心跳兜底比对)
|
||||
v v
|
||||
Server 立即向所有 Agent 推送变更组的完整成员 Agent 心跳上报本地所有 IP 组的 Checksum 映射表
|
||||
| |
|
||||
| v
|
||||
| Server 发现 Checksum 不一致,下发变更的 IP 组成员
|
||||
v |
|
||||
Agent 接收成员数据,将其以 JSON 形式写入本地磁盘路径:waf_ip_groups.json
|
||||
|
|
||||
v (Lua 内存感知)
|
||||
OpenResty Lua 引擎通过 MD5 校验和秒级感知文件变化并热更新内存,无需 reload 进程
|
||||
```
|
||||
|
||||
通过这一架构,上万个高频变动的动态黑名单 IP 的落地和生效,**全程无需 reload 任何 Nginx 进程**,极大地保护了网关的高并发性能。
|
||||
|
||||
---
|
||||
|
||||
## 规则组与网站绑定
|
||||
|
||||
* **WAF 规则组(Rule Group)**:WAF 过滤政策的最小逻辑集合。一条规则组内可以包含 IP 黑白名单、IP 组引用、地域限制及防 CC 挑战配置。
|
||||
* **全局规则组(Global)**:当规则组被标记为 `is_global = true` 时,该规则组对节点上托管的**所有网站路由**默认生效。
|
||||
* **网站绑定绑定(Site Binding)**:网站路由(Proxy Route)可以绑定一个或多个非全局规则组。判定时,会执行 `全局规则组 + 绑定规则组` 的并集逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 实现方案与高性能缓存
|
||||
|
||||
WAF 在 OpenResty 的 `access_by_lua` 阶段被触发,核心由 Lua 文件与本地落地的 JSON 配置构成。
|
||||
|
||||
### 1. 物理结构
|
||||
* `waf_config.json`:包含所有规则组的元数据、国家地域限制、以及网站(Site)与规则组的关联映射。
|
||||
* `waf_ip_groups.json`:包含所有同步下来的 IP 组与对应的 IP 列表。
|
||||
* `waf/runtime.lua`:WAF 规则比对的实际运行时引擎。
|
||||
* `waf/check.lua`:接入层入口,负责包引入与 check() 触发。
|
||||
|
||||
### 2. 共享内存字典 (ngx.shared) 高性能缓存设计
|
||||
在每次 Web 请求进来时都读取磁盘上的 JSON 文件并进行解码,会导致磁盘 I/O 成为严重的性能瓶颈。
|
||||
|
||||
OpenFlare 利用 **OpenResty 共享内存字典 (ngx.shared.openflare_waf_config)** 设计了二级缓存机制:
|
||||
|
||||
1. **零文件 I/O 路径**:
|
||||
在 Lua 中,每次执行 `check()` 时,首先利用 `ngx.md5` 瞬间计算本地磁盘 JSON 文件的 MD5 哈希(这一操作几乎为零耗时,因为文件已被操作系统 Page Cache 缓存)。
|
||||
2. **哈希比对与热加载**:
|
||||
比对共享内存中存储的缓存哈希键(`_config_hash`)。
|
||||
* **若哈希未发生变化**:直接从共享内存字典中读取已解码、存在内存中的 Lua Table 配置,整个校验过程完全基于**共享内存操作**,耗时在 **微秒级** 级别。
|
||||
* **若哈希不一致**:说明 Agent 刚刚落地了新的 WAF 规则或 IP 组,Lua 自动读取磁盘文件并使用 `cjson.decode` 解码,解码后的数据及全新的 MD5 写入共享内存,供后续 Worker 进程无缝读取。
|
||||
|
||||
---
|
||||
|
||||
## 应用流程与判定判定控制逻辑
|
||||
|
||||
当一个 HTTP/HTTPS 请求到达 OpenResty 后,WAF 会在 `access` 阶段按下图所示的漏斗判决链进行逐步匹配拦截:
|
||||
|
||||
### 1. WAF 判定流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[请求进入 access 阶段] --> B[获取当前请求的 Site Name]
|
||||
B --> C[在共享内存中加载与此 Site 绑定的所有活跃规则组]
|
||||
C --> D{匹配到 IP 白名单 / 白名单 IP 组?}
|
||||
D -- 是 (匹配成功) --> E[放行请求 - ALLOW]
|
||||
D -- 否 --> F{匹配到国家/地区地域白名单?}
|
||||
F -- 是 (匹配成功) --> E
|
||||
F -- 否 --> G{匹配到 IP 黑名单 / 黑名单 IP 组?}
|
||||
G -- 是 (匹配成功) --> H[阻断请求 - BLOCK]
|
||||
G -- 否 --> I{匹配到国家/地区地域黑名单?}
|
||||
I -- 是 (匹配成功) --> H
|
||||
I -- 否 --> J{是否启用了防 CC PoW 验证?}
|
||||
J -- 是 --> K[转交防 CC 模块处理]
|
||||
J -- 否 --> L[无安全风险,正常放行]
|
||||
|
||||
H --> M[退出并返回规则组配置的自定义状态码与拦截响应体]
|
||||
```
|
||||
|
||||
### 2. 判决步骤细则
|
||||
1. **白名单前置**:
|
||||
为了防止误杀以及保障核心回源流量(如搜索引擎蜘蛛、CDN 回源 IP、办公区出口)的顺畅,WAF **优先匹配 IP 白名单与地域白名单**。一旦白名单匹配成功,直接绕过后续的所有黑名单检测和 CC 挑战,立刻放行。
|
||||
2. **黑名单强力阻断**:
|
||||
如果在白名单判定中未被捕获,请求将进入黑名单漏斗。一旦请求源 IP 命中 IP 黑名单、命中引用的黑名单 IP 组、或是处于被禁止的国家/地区范围内,Lua 引擎立即将 `ngx.ctx.openflare_waf_blocked` 标记设为 `true`。
|
||||
3. **输出响应**:
|
||||
命中黑名单后,Lua 提取匹配到规则组的 `block_status_code`(默认返回 418 / 403)和 `block_response_body`(拦截页面 HTML),通过 `ngx.say()` 输出响应体并执行 `ngx.exit(status)` 平滑退出请求,防止请求继续向后透传。
|
||||
Reference in New Issue
Block a user