AngieGuardian/angie-guardian
GitHub: AngieGuardian/angie-guardian
为 Angie Web 服务器设计的 WAF 与工作量证明 bot 防火墙 sidecar,通过自适应挑战和行为分析保护网站与 API 免受恶意流量和自动化攻击。
Stars: 2 | Forks: 1
# Angie Guardian
一个 Web 应用程序防火墙 (WAF) 和工作量证明 bot 防火墙,专为
[Angie](https://angie.software/) 编写,使用 Go 语言。
Guardian 保护托管在 Angie 上的网站和 API 免受恶意流量、
滥用 bot 和自动化攻击,同时允许合法访问者正常访问
网站。它允许受信任的请求,通过工作量证明来挑战可疑的客户端,
并明确阻断真实的威胁。
## 功能特性
Guardian 将请求时的 WAF 与工作量证明挑战相结合。策略
可按域名进行配置,因此一个实例可以保护多个 vhost。
- **热重载 WAF:** 支持针对路径、查询参数、
User-Agent、HTTP headers 和 HTTP 方法的 RE2 关键词和正则表达式规则。
- **自适应 bot 防御:** 行为评分、蜜罐、IP 封禁、
GeoIP/声誉检查以及已验证 bot 的 DNS 验证。
- **工作量证明通行证:** 自适应的 SHA-256 浏览器挑战,随后是经过
低成本验证的 Ed25519 签名 cookie;并提供可选的无 JS 降级方案。
- **抗重放攻击:** 挑战为一次性使用,且与域名和
客户端 IP 绑定。
- **异常检测:** 可选的离线训练,基于 Angie 访问日志,支持在实时请求路径中进行
快速评分。
- **高可用部署:** 持久化密钥、用于多副本的共享存储、安全
重载,以及故障开放或故障关闭集成。
## 快速开始
从 [发布页面](https://gitlab.melroy.org/melroy/angie-guardian/-/releases) 安装指定版本的 `linux-amd64` 或 `linux-arm64` 包,
然后按照发布优先的
[入门指南](https://angieguardian.org/guide/getting-started) 操作。
归档文件包含二进制文件、示例配置、WAF 启动规则、
Angie 片段以及 systemd unit;运维人员无需安装 Go 或检出代码仓库。
```
# After downloading and extracting a pinned release:
sudo install -Dm755 guardiand /usr/local/bin/guardiand
```
如需使用容器和持久化状态,请参阅
[生产环境 Docker 指南](https://angieguardian.org/guide/production#docker)。
## 文档
指南、示例以及完整的配置和 API 参考文档发布在 ** **。
## 工作原理
Angie 保持在请求路径中,并继续为站点现有的静态文件、
`try_files`、FastCGI 或反向代理 handler 提供服务。Guardian 作为 sidecar 决策
服务:Angie 向其发送无请求体的内部授权子请求,并根据
返回的允许、挑战或拒绝响应执行操作。第一张图展示了该
实时请求生命周期;第二张图则将支持性的策略、状态、
训练和运维平面与核心请求热路径区分开来。
```
@startuml
skinparam backgroundColor #F8FAFC
skinparam shadowing false
skinparam roundCorner 12
skinparam defaultFontName Sans-Serif
skinparam sequence {
ArrowColor #2563EB
LifeLineBorderColor #475569
LifeLineBackgroundColor #F8FAFC
ParticipantBorderColor #475569
ParticipantBackgroundColor #F8FAFC
ParticipantFontColor #0F172A
ActorBorderColor #475569
ActorFontColor #0F172A
GroupBorderColor #64748B
GroupFontColor #0F172A
}
hide footbox
title Angie Guardian - Live Request Path
actor "Visitor/Bot" as Client
participant "Angie vhost" as Angie
participant "guardiand" as Guardian
participant "Original site handler\n(backend)" as Backend
Client -[#2563EB]> Angie: Request
Angie -[#2563EB]> Guardian: Can this request continue?
alt ALLOW
Guardian -[#15803D]> Angie: Allow
Angie -[#15803D]> Backend: Continue to the site
Backend -[#15803D]> Client: Site response
else CHALLENGE
Guardian -[#D97706]> Angie: Challenge
Angie -[#D97706]> Client: Proof-of-work page
Client -[#D97706]> Angie: Solve challenge
Angie -[#D97706]> Guardian: Verify solution
Guardian -[#D97706]> Angie: Grant signed pass
Angie -[#D97706]> Client: Signed pass
Client -[#15803D]> Angie: Retry original request
Angie -[#15803D]> Guardian: Check signed pass
Guardian -[#15803D]> Angie: Allow
Angie -[#15803D]> Backend: Continue to the site
Backend -[#15803D]> Client: Site response
else DENY
Guardian -[#B91C1C]> Angie: Deny
Angie -[#B91C1C]> Client: Block request
else GUARDIAN UNAVAILABLE - FAIL OPEN
Angie -[#15803D]> Backend: Continue to the site
Backend -[#15803D]> Client: Site response
end
note over Angie, Backend #E8F5E9
Remove the fail-open error_page mapping to choose fail-closed.
end note
@enduml
```
同一个 sidecar 加载策略和密钥材料,拥有有状态保护,并
暴露独立的运维监听器。离线训练和可选的
集成为此支持平面提供数据,而无需介入实时请求
路径。
```
@startuml
skinparam backgroundColor transparent
skinparam shadowing false
skinparam roundCorner 12
skinparam defaultFontName Sans-Serif
skinparam ArrowFontSize 11
skinparam activity {
BorderColor #475569
FontColor #0F172A
BackgroundColor #F8FAFC
}
skinparam partition {
BorderColor #475569
FontColor #0F172A
}
partition #F5F3FF "Angie Guardian - Policy, State, Training, and Operations" {
start
if (Integration path?) then (full sidecar)
fork
-[#64748B,dashed]->
:Load policy\nConfig, WAF rules, keys, and threat data; <<#F8FAFC>>
fork again
-[#2563EB]->
:Optional offline training\nAccess logs -> anomaly model; <<#DBEAFE>>
end fork
-[#2563EB]->
:Activate a validated snapshot\nKeep the last good version if reload fails; <<#FFF7E6>>
fork
-[#64748B,dashed]->
:Stateful protection\nBlocks, challenges, and counters; <<#F1F5F9>>
fork again
-[#7C3AED]->
:Operations\nDashboard, API, health, and metrics; <<#F8FAFC>>
fork again
-[#B91C1C,dashed]->
:Optional nftables\nDrop known blocks before Angie; <<#FDECEC>>
end fork
stop
else (optional WASM alternative)
-[#9333EA,dashed]->
:Run stateless policy inside Angie\nStore-free WAF only; no PoW or shared state; <<#F3E8FF>>
stop
endif
}
@enduml
```
## 性能表现
Guardian 专为 Angie 的授权热路径而构建。在
AMD Ryzen Threadripper 7960X 上使用 64 个连接进行的本地回环测试得出了以下吞吐量
结果(每次为 3 次固定负载运行的中位数,每次运行均启动全新 daemon 和存储;
挑战发布在其**加载后的稳态**下测量,此时进程内的两个
计数器缓存均已填满并超过容量,而不是针对空
存储进行测量):
| 存储后端 | 允许请求/秒 | 回访客户端请求/秒 | 每秒发布挑战 |
|---|---:|---:|---:|
| 内存存储 (ephemeral) | 180,000 | 173,000 | 160,000 |
| Pebble (async, 默认) | 182,000 | 171,000 | 61,000 |
| Pebble (sync, 完全持久化) | 179,000 | 170,000 | 34,000 |
| BuntDB (async) | 182,000 | 170,000 | 56,000 |
| Redis/Valkey | 94,000 | 93,000 | 49,000 |
在每个后端中,读取路径始终保持在 93,000 请求/秒以上。挑战
发布属于写密集型,这解释了内存
与持久化存储之间的差异。请参阅
[负载测试指南](https://angieguardian.org/guide/load-testing)
了解场景、延迟百分位、方法论和复现命令。
## 集成路径
Guardian 提供两种运行方式,它们共享同一个决策核心:
- **Sidecar(默认,全功能)。** 一个通过
原生 `auth_request` 指令接入 Angie 的 Go daemon。这是完整的实现:
工作量证明和行为 IP 封禁使用其拥有的共享存储;
异常评分和已验证 bot 的 DNS 验证也仅限 sidecar 模式。建议从这里开始。
- **WASM 模块(可选,无状态 WAF)。** 将无存储检查(allowlist、
denylist、蜜罐、关键词/正则签名)编译为 WebAssembly 并
通过 Angie 的 WASM 支持在其进程内运行,适用于喜欢该
集成方式的运维人员。它仅限无状态的 WAF:工作量证明、行为封禁、
异常评分和已验证 bot 的 DNS 检查需要 sidecar。使用 `make wasm` 进行构建;请参阅
[USAGE.md](USAGE.md) 中的 "WASM module" 章节。
这两种路径共享相同的无存储匹配逻辑。它们的有状态结果
有所不同:在 sidecar 中,`challenge` 可以通过绑定的 PoW token 获得满足,并且
`block`/蜜罐命中会保持 IP 封禁状态;无状态的 WASM guest 对
其中任何匹配项均返回普通的拒绝。经过验证的 PoW token 永远不会使
sidecar 客户端免受 `deny` 或 `block` 签名检查,因此被盗用的 token 无法
绕过 WAF。
## 运维
- **可观测性:** Prometheus 指标、健康/就绪检查、内置
[Grafana dashboard](deploy/grafana-dashboard.json) 以及
[告警规则](deploy/alerts.yaml)。
- **管理 API 和控制面板:** 受 token 保护的封禁管理、近期
决策、异常/配置状态,以及可选的隔离网络 Web 控制面板。
- **安全维护:** 热重载保持最后已知良好的配置处于激活状态;
签名密钥可以轮换,而不会立即使现有通行证失效。
- **灵活的状态管理:** 单实例可使用 `memory`、`pebble` 或 `buntdb`;
共享多实例部署可使用 `redis`/`valkey`。
请参阅 [USAGE.md](USAGE.md) 了解端点、身份验证、控制面板设置、重载
和密钥轮换的详细信息。
## 安全性
Guardian 的 [安全模型和局限性](https://angieguardian.org/guide/threat-model)
详细说明了它能防御什么以及故意不防御什么。如需报告
漏洞,请参阅 [SECURITY.md](SECURITY.md);请勿公开提出 issue。
## 许可证
[AGPL-3.0](LICENSE),© Melroy van den Berg
## 开发
以下内容适用于构建、测试或修改
Angie Guardian 的贡献者。运维人员可以在此停止,并使用上面链接的文档。
### 测试
默认的测试套件是自包含的;仅在执行端到端测试时需要 Docker。
| 检查 | 命令 | 目的 |
|---|---|---|
| 单元测试 | `go test ./...` | 核心、存储和 HTTP 传输 |
| 竞态检测 | `go test -race ./...` | 并发回归 |
| 端到端 | `make e2e` | 真实的 Angie → guardiand → 后端技术栈 |
| 模糊测试 | `make fuzz` | 不受信任的及热重载的解析器 |
| 基准测试 | `go test -bench=. -benchmem ./core/... ./transport/http/` | 请求热路径 |
| 内存分配门控 | `make bench-regress` | 对照 `allocs-baseline.txt` 检查热路径 `allocs/op`;这也是一项 CI 作业 |
[端到端套件](test/e2e/) 涵盖了工作量证明、WAF 结果、
行为封禁、故障开放、指标和管理 API。CI 会在
`main` 和发布标签上运行它;在合并技术栈级别的更改之前,请在本地运行测试。
将 `testdata/fuzz/` 中有用的模糊测试崩溃提交为回归种子。
#### 填充控制面板
运行一个临时实例并从两个
shell 生成具有代表性的控制面板流量:
```
go run ./cmd/guardiand -config test/seed/guardian.seed.yaml
make seed # two minutes
make seed SEEDTIME=5m # optional longer run
```
打开 。所链接的
[种子配置](test/seed/guardian.seed.yaml) 仅限内存使用,旨在用于
本地开发。`make seed` 会创建真实的流量混合;在进行
吞吐量测量时,请使用 `guardian-loadtest`。
### 从源码构建
所需的 Go 工具链版本在 [go.mod](go.mod) 中指定。将三个 sidecar
二进制文件构建到 `dist/` 中,或者额外构建可选的 WASM 模块:
```
make build
make wasm
```
文档站点可以通过 `make docs-dev` 进行预览,或通过
`make docs` 进行构建。
### 性能测试
`guardian-loadtest` 像 Angie 那样通过 keepalive HTTP 驱动
热路径,并报告吞吐量和
延迟百分位。
**场景**(前三个以读取为主;`challenge` 是唯一的
写密集型路径):
| 场景 | 作用 | 每个请求的存储 I/O |
|---|---|---|
| `allow` | 普通请求,完整流水线,以“默认允许”结束 | 一旦封禁镜像完成初始化填充,嵌入式后端上无操作;`redis` 上有 1 次读取(直读模式,因此其他副本的封禁会立即生效) |
| `token` | 解决一个 PoW 挑战,然后使用 cookie 大量请求 `/auth`(生产环境中的常见路径) | 与 `allow` 相同 |
| `deny` | 位于黑名单的客户端 IP(拒绝 + 决策日志记录路径) | 无存储 I/O |
| `challenge` | 为每个请求发布一个新的 PoW 挑战 | 通常是 1 次**写入**(挑战 CAS);在 [攻击模式](https://angieguardian.org/guide/attack-mode) 下,或者当该有状态写入失败时,发布是无状态的:发布时不写入,单次消费的写入转移到兑换阶段。限流和升级计数器在进程内计算,并在后台刷入 |
**结果**(单节点,回环,64 连接,负载生成器共享相同的
CPU:AMD Ryzen Threadripper 7960X,24C/48T;Linux 6.17;Go 1.26.5;
Redis 后端使用 Valkey 9;每次运行均使用全新的 daemon 和擦除的存储;每个单元格
运行 3 次的中位数)。读取使用 `-warmup 50000 -n 500000`;挑战使用
`-warmup 150000 -n 150000`,因此其预热会将两个 131k 条目的计数器缓存
推过容量上限,并且测量窗口是加载后的稳态,而不是空
存储所提供的快速冷启动。每个单元格是**吞吐量 / p50 / p99**
(req/s 和每次请求的延迟):
| 后端 | allow | token | deny | challenge (写入) |
|---|---|---|---|---|
| `memory` (ephemeral) | 180k / 0.13ms / 1.8ms | 173k / 0.16ms / 1.7ms | 157k / 0.14ms / 2.3ms | **160k / 0.29ms / 1.6ms** |
| `pebble` (async, 默认持久化) | 182k / 0.13ms / 1.8ms | 171k / 0.15ms / 1.8ms | 154k / 0.14ms / 2.4ms | **61k / 0.90ms / 3.6ms** |
| `pebble` (sync, 完全持久化) | 179k / 0.13ms / 1.8ms | 170k / 0.15ms / 1.8ms | 154k / 0.14ms / 2.4ms | **34k / 1.5ms / 5.3ms** |
| `buntdb` (async, 单文件) | 182k / 0.13ms / 1.8ms | 170k / 0.14ms / 1.8ms | 155k / 0.13ms / 2.4ms **56k / 1.2ms / 4.8ms** |
| `redis`·`valkey` (集群) | 94k / 0.64ms / 1.3ms | 93k / 0.64ms / 1.4ms | 162k / 0.13ms / 2.3ms | **49k / 1.2ms / 2.5ms** |
(`buntdb` + `sync: true` 仅测得约 0.6k 次挑战写入/秒,因为
每次提交都进行 fsync 的单写入者存储速度就是那么慢,因此该组合在
启动时即被**拒绝**;如需同步持久性,请使用 `pebble` 配合 `sync: true`。)
每个后端在读取路径上都以极大的优势超过了 ≥50k req/s 的预算。
在**嵌入式**后端上,进程内的封禁镜像使存储成为
权威数据源,因此在初始扫描后,`allow`/`token` 根本不进行存储 I/O,
这就是为什么它们聚集在 ~154–182k 的原因。`redis`/`valkey` 是个例外:它
保持直读模式(为了跨副本的正确性,每个请求进行一次网络读取),
因此其 `allow`/`token` 结果较低(~93–94k),而 `deny`(无存储读取)保持快速。
**写入**路径是后端至关重要的部分。发布挑战会写入一条
CAS 记录,并且持久化后端吸收该操作的能力远优于同步的
单写入者存储:
- **`pebble`**(默认持久化)在异步模式下可维持约 61k 次挑战写入/秒,
即使在完全持久化的 `sync: true` 模式下,依然能达到约 34k/秒。两者都远高于
同步 fsync 的单写入者存储。它是一个 LSM 引擎,因此写入会命中
WAL 和 memtable 并在后台被刷入。
- **`buntdb`** 在异步模式下接近 Pebble(~56k/秒),并将所有内容
存储在一个文件中,这使得备份更加简单。它是单写入者的,因此 `sync: true`
会在每次提交时进行 fsync 并导致性能骤降至约 600/秒,因此 guardiand 在该配置下
**拒绝启动**,并建议您使用 Pebble 以获得同步
持久性。
- **`memory`** 完全没有写入上限(~160k/秒),但在
重启时会丢失所有状态。
- **`redis`/`valkey`** 维持约 49k 次挑战写入/秒(与
嵌入式持久化后端相当),是**多实例**选项:它是
共享存储,允许负载均衡器后面的副本看到彼此的封禁
和单次消费标记。它为此牺牲了一些读取吞吐量(每个
请求进行一次网络读取)。上述嵌入式后端仅限单节点。请参阅
[存储指南](https://angieguardian.org/guide/production#choosing-a-store-backend)。
当大量*新*客户端可能
超过持久化上限时,可通过以下两种方式进一步提升写入路径:
- 设置 `pow.mode: suspicion`,这样就没有全量捕获的挑战(只有明确的
异常/WAF/GeoIP/声誉策略会发布挑战),或者依靠
[攻击模式](https://angieguardian.org/guide/attack-mode) 的
**无状态**发布,该模式在发布时不写入任何内容。剩下的唯一
写入是兑换时的单次消费标记,除非攻击者实际解决了工作量证明,否则
无法触发该写入。
- 已验证的 token 会在进程内缓存(约 35 ns,无内存分配,而完整的 Ed25519
验证需要约 40 µs),因此回访客户端的请求会始终保持在
快速读取路径上,而不受后端影响。
- 在这些速率下,读取路径主要受 Go 的垃圾回收器限制,而不是受
存储限制:设置 `GOGC=800` 可将允许吞吐量提高约 20%。请参阅
生产环境指南中的 "GC 调优"。
`redis` 后端可与 Redis 和
[Valkey](https://valkey.io/) 协同工作(这是一种直接替代方案,使用相同的通信协议和相同的
`backend: redis` 值)。
#### 复现
```
go build ./cmd/guardiand ./cmd/guardian-loadtest
mkdir -p .guardian
sed -e 's#/etc/guardian/rules.d/common.yaml#deploy/rules-common.yaml#' \
-e 's#/etc/guardian/#.guardian/#g' \
-e 's#/var/lib/guardian/#.guardian/#g' \
guardian.example.yaml > guardian.local.yaml
# 1. Start guardiand with your store backend (pebble shown; for redis set
# store.backend: redis and store.addr in the config). PoW must be enabled
# for the token/challenge scenarios.
./guardiand -config guardian.local.yaml &
# 2. Run each scenario with FIXED WORK (-warmup/-n), 64 connections, and a
# fresh daemon + wiped store per run so results are comparable. Use a
# distinct -ip per read run so a behavioural block from one run doesn't
# bleed into the next; the challenge scenario rotates the client IP itself
# to dodge the issuance limit, and its warmup pushes the counter caches
# past capacity so the measured window is the loaded steady state. allow
# uses the PoW-off host: the scenario expects 200s, and a PoW-on host
# answers 401 (challenge) for unvouched clients.
./guardian-loadtest -scenario allow -host api.example.com -ip 198.51.100.10 -c 64 -warmup 50000 -n 500000
./guardian-loadtest -scenario token -host example.com -ip 198.51.100.11 -c 64 -warmup 50000 -n 500000
./guardian-loadtest -scenario deny -host example.com -ip 203.0.113.9 -c 64 -warmup 50000 -n 500000 # IP must be denylisted
./guardian-loadtest -scenario challenge -host example.com -c 64 -warmup 150000 -n 150000
```
输出中的 `per-second:` 行显示了运行是否达到了稳态;
下降的线条表示总体混合了多种状态,只有带有
相同标志的固定负载运行才具有可比性。`make bench-regress` 是配套的 CI
门控:对照 `allocs-baseline.txt` 中提交的基准检查热路径的 allocs/op,
这是确定性的且与机器无关。
微基准测试位于代码旁边,涵盖热路径的每一层:
`/auth` handler 及其构建的请求值、针对每个裁决的 `Evaluate`、
`ShedDecision`、PoW 验证和异常评分。
```
go test -bench=. -benchmem ./core/... ./transport/http/
```
请在阅读负载测试的同时阅读它们,而不是用它们替代负载测试。在
~180k req/s 的回环速率下,daemon 每个请求大约花费 70–80 µs 的 CPU,而
决策本身远低于一微秒:几乎所有的时间都花在了内核
和 `net/http` 上。因此,如果一项更改使决策路径的成本降低数倍,
最终对端到端请求速率的提升也仅有几个百分点,并且
最适合通过 CPU 消耗/请求来衡量。请参阅
[负载测试指南](https://angieguardian.org/guide/load-testing#micro-benchmarks)。
### 架构
所有决策逻辑都位于与传输无关的单一接缝之后:
```
core.Engine.Evaluate(ctx, RequestContext) Decision
```
HTTP `auth_request` 传输是围绕它的轻量级封装。无存储的
WAF 检查位于叶子包 `core/stateless` 中,WASM guest
直接导入该包,因此进程内模块可以重用完全相同的逻辑,而
无需引入存储、PoW 或异常依赖项。
```
core/ decision engine, pipeline, config, scoreboard, recent-decision ring
core/stateless/ store-free WAF checks + value types (shared by sidecar & WASM)
core/pow/ challenges, Ed25519 JWTs, token cache, key persistence + rotation
core/waf/ signature rules, signed IDs
core/anomaly/ statistical baseline model, online scorer, hot-swap cache
core/store/ TTL'd shared state: memory | buntdb | pebble | redis
core/intel/ GeoIP/ASN databases and IP reputation feeds
core/botverify/ rDNS + forward-confirm crawler verification
core/attackmode/ fleet attack posture: signal aggregation and state machine
core/enforce/ block mirror and the optional nftables kernel sink
core/health/ background store probe behind /readyz
core/metrics/ Prometheus instrumentation (private registry)
transport/http/ auth_request sidecar + admin/metrics/dashboard
transport/wasm/ optional http-wasm guest (stateless WAF, runs inside Angie)
internal/ small shared helpers (bounded file reads, background-work jitter)
cmd/ guardiand (sidecar), guardian-train (offline anomaly training),
guardian-loadtest (stress tool)
deploy/ Angie snippets, systemd unit, rules, Grafana dashboard, alert rules
web/ challenge/denied pages and the admin dashboard, with its
vendored chart libraries (no CDN, works air-gapped)
```
标签:AppImage, EVTX分析, Go, PoW工作量证明, Ruby工具, Web应用防火墙, 反爬虫与Bot防御, 抗DDoS, 搜索引擎查询, 日志审计, 网络安全, 自定义请求头, 请求拦截, 隐私保护