CaddyGuard:一款基于 Go 的 Caddy v2 原生 WAF 插件
用 Go 原生编写 Caddy v2 中间件,12 项检测链全开仅约 1.7% 性能开销——支持全局自动生效、域名级差异化规则、IPv4/IPv6 双栈 CIDR 匹配、64 分片滑动窗口 CC 防护、POST body 关键词预过滤,以及无需重启的热加载。
项目地址:https://github.com/qist/caddyguard
为什么需要 CaddyGuard?
Caddy v2 因其自动 HTTPS、简洁配置和 Go 原生性能,越来越多的团队将其作为反向代理和 Web 服务器。但 Caddy 生态中缺少一个真正可用的 WAF 插件:
- ModSecurity 有 Caddy 的 libmodsecurity 封装,但依赖 C 绑定,部署复杂
- Cloudflare 等 SaaS WAF 是黑盒,规则不可控,且有延迟和隐私顾虑
- 自己用 Caddy 中间件写?很多人不知道从何入手
CaddyGuard 就是为填补这个空白而生的——一个纯 Go 编写的 Caddy v2 原生 WAF 插件,用标准中间件链实现,零 reflect/unsafe,部署简单到一行 xcaddy build 就搞定。
核心特性
全局自动生效
这是 CaddyGuard 最具特色的设计。通过自定义 caddyguardfile 适配器,全局配置一次 rule_dir 后,所有站点自动启用 WAF,站点块完全不需要写 caddyguard 指令:
1 | { |
适配器在解析 Caddyfile 后,自动向每个 HTTP server 的每条 route 注入 Guard handler。如果某条 route 已经有 caddyguard handler(如路径级 waf_enable off),则跳过注入,确保路径级配置优先。
12 项检测链
按检测成本从低到高排序,命中即返回,攻击请求在最低成本阶段就被短路拦截:
| 顺序 | 检测项 | 说明 |
|---|---|---|
| 1 | 白名单 IP | 命中放行,跳过所有后续检测 |
| 2 | 白名单 URL | 命中放行,跳过所有后续检测 |
| 3 | 动态黑名单 IP | CC 自动拉黑期内的 IP |
| 4 | 静态黑名单 IP | 手动配置的 IP 黑名单 |
| 5 | CC 攻击 | 64 分片滑动窗口计数 |
| 6 | User-Agent | 恶意扫描器/工具 UA |
| 7 | URL 路径 | 路径遍历、敏感文件、管理后台 |
| 8 | URL 参数 | SQL 注入、XSS、SSTI、RCE |
| 9 | Cookie | Cookie 注入 |
| 10 | Referer | 恶意来源、支付接口保护 |
| 11 | POST body | 关键词预过滤 + Content-Type 分流 |
| 12 | 文件上传 | 扩展名检测,最昂贵的检测项 |
IPv4/IPv6 双栈 IP 匹配
IP 黑白名单同时支持 IPv4 和 IPv6,三种格式全覆盖:
1 | # CIDR 表示法(推荐) |
在实现上,IPv4 CIDR 在加载阶段被预编译为 uint32 范围,排序后用二分搜索(O(log n));IPv6 CIDR 保存为 *net.IPNet 线性匹配;精确 IP 用 hash map 查找(O(1))。整个 IP 规则集按 mtime 缓存,规则文件未变时直接复用预编译结果。
CDN 代理 IP 信任链
当 CaddyGuard 部署在 CDN 后面时,需要从 X-Forwarded-For 获取真实客户端 IP。但直接信任 XFF 会让攻击者伪造该头绕过 IP 黑白名单。
解决方案是在 cdnip.rule 中配置实际使用的 CDN IP 段:
| 条件 | XFF 处理 | 说明 |
|---|---|---|
remote_addr 在 cdnip.rule 中 |
信任 XFF | 提取真实客户端 IP |
remote_addr 不在 cdnip.rule 中 |
不信任 XFF | 使用 remote_addr 防伪造 |
| cdnip.rule 不存在/为空 | 信任所有 XFF | 向后兼容 |
CC 防护:64 分片滑动窗口
CC 防护是 WAF 的核心功能之一,CaddyGuard 的实现有几个亮点:
- 64 分片:基于 FNV-1a hash 对
IP+URI分片,每个分片独立锁,消除全局锁竞争 - 8 桶滑动窗口:真正的滑动窗口语义(非 TTL 重置),窗口内 8 个时间片桶分别计数
- 内存上限:CC 计数器最大 100 万 key,超过后自动淘汰过期条目
- 自动封禁:超过
cc_rate阈值后自动拉黑 IP,持续cc_block_ttl秒 - 后台清理:每 60 秒清理过期计数器和过期封禁
Go Benchmark 微基准测试显示,CC Incr 操作仅 67 ns/op,IsBanned 仅 25 ns/op——在高并发场景下几乎零开销。
POST body 检测:关键词预过滤
POST body 是最昂贵的检测项(需要读取 body + 正则匹配),CaddyGuard 用三层优化将开销降到最低:
第一层:Content-Type 分流
application/x-www-form-urlencoded:通过ParseQuery解析 key/value 逐一匹配,完成后跳过 raw body 二次扫描application/json/application/xml:直接走 raw body 扫描multipart/form-data:跳过(由文件上传检测处理),除非开启multipart_streaming_check
第二层:关键词自动提取预过滤
加载阶段从每条正则规则中自动提取字面量关键词。例如规则 select.+(from|limit) 提取出关键词 select。请求阶段先用 bytes.Contains 检查 body 是否包含任何关键词(Go 的 bytes.Contains 有 SIMD 优化),不包含任何关键词的 body 直接跳过全部正则。
这意味着 99% 的正常 POST 请求(登录、表单提交、API 调用)不会触发任何正则匹配。
第三层:worker 级匹配缓存
对小体积输入(≤512 字节)的匹配结果做全局缓存,key = 规则集标识 + caseInsensitive + input。重复请求(如健康检查)直接复用结果,缓存上限 4096 条。
热加载:2 秒生效,零停机
规则和配置文件修改后 2 秒内自动生效,无需重启 Caddy:
- 触发方式:修改
rule-config/目录下的任何.rule或.json文件 - 实现原理:基于 mtime 检查 + 节流(2 秒间隔),避免每个请求都
os.Stat - 并发安全:使用
RWMutex+ double-check,加载期间请求不受影响 - 完全进程内:不需要
caddy reload
热加载的实现采用读锁快速路径 + 写锁 double-check 模式:
1 | // 快速路径:节流窗口内直接用缓存 |
域名级配置 + 路径级开关
域名级覆盖:通过 domain.json 为不同域名配置不同的检测策略,支持精确域名和通配符:
1 | { |
域名级配置在加载阶段预合并到全局基线中,请求阶段只需一次 map 查找(O(1)),零运行时开销。
路径级开关:利用 Caddy 原生 path matcher,对特定 URL 路径关闭 WAF:
1 | sub.example.com { |
编码绕过防护
CaddyGuard 支持递归解码,防止攻击者通过多层编码绕过规则:
- URL 编码:递归 URL-decode(最多 8 层),如
%25253C→%253C→%3C→< - JS Unicode:
\u003C→<,\u{003C}→< - JS Hex:
\x3C→< - HTML 实体:
<→<,<→<
正常流量(不含 %、\u、\x、&# 标记)走快速路径,零解码开销。
ReDoS 安全
CaddyGuard 基于 Go 的 RE2 正则引擎,无回溯爆炸风险。即使规则文件中意外引入了恶意正则(如 (a+)+$),也不会导致 CPU 100% 或进程挂起。
三种配置方式
CaddyGuard 提供三种配置方式,适应不同场景:
| caddyguardfile 适配器 | caddyfile 适配器 | JSON | |
|---|---|---|---|
| 全局配置 | ✅ 一次配置,所有站点自动生效 | ❌ 每站点单独写 | ❌ 每个 route 手动写 |
| 启动参数 | --adapter caddyguardfile |
--adapter caddyfile |
不需要 adapter |
| 推荐场景 | 多站点统一 WAF | 单站点/精细控制 | Docker / K8s |
性能测试结果
压测环境
- 物理机(4 核 CPU),本机回环
- Apache Bench (ab),50000 请求,200 并发,Keep-Alive
核心数据
| 场景 | req/s | P99 | 开销 |
|---|---|---|---|
| Caddy + reverse_proxy(无 WAF) | 6,329 | 73ms | 基准 |
| CaddyGuard 规则全关 | 6,405 | 70ms | ~0% |
| CaddyGuard 规则全开(不含 CC) | 6,223 | 73ms | ~1.7% |
| CaddyGuard 攻击 UA 拦截 | 11,027 | 21ms | — |
| CaddyGuard + POST body | 5,363 | 86ms | ~15% |
WAF 全开吞吐下降仅 1.7%(6,223 vs 6,329),几乎可以忽略不计。
优化前后对比
| 场景 | 优化前 req/s | 优化后 req/s | 提升 |
|---|---|---|---|
| WAF 全开(不含 CC) | 5,713 | 6,223 | +8.9% |
| WAF + POST body | 4,991 | 5,363 | +7.4% |
| WAF 全开 vs 无 WAF 开销 | ~11% | ~1.7% | 降低 85% |
Go Benchmark 微基准
| 组件 | 性能 | 说明 |
|---|---|---|
| CC Incr (64 分片并行) | 67 ns/op | 无锁竞争 |
| CC IsBanned (并行) | 25 ns/op | 分片读锁 |
| matchRules (大小写不敏感) | 2.2 ns/op | ToLower 一次 + worker 缓存 |
| GetEffectiveConfig | 1,474 ns/op | 预合并缓存 O(1) |
| runChecks (正常请求) | 6,431 ns/op | 12 项检测全通过 |
性能优化措施详解
| 优化项 | 说明 |
|---|---|
| 规则双引擎 | 纯字符串规则用 strings.Contains(快 10x),正则规则用预编译 *regexp.Regexp |
| 关键词预过滤 | 加载阶段从正则提取字面量关键词,请求阶段 bytes.Contains 预检跳过不匹配的正则 |
| worker 级匹配缓存 | 小输入(≤512字节)匹配结果全局缓存,上限 4096 条 |
| POST Content-Type 分流 | form-urlencoded 走 ParseQuery 解析 key/value 后跳过 raw body 二次扫描 |
| 空规则提前返回 | 各检测函数获取规则后立即判断空规则,避免无意义的 header/body 读取 |
| UA bloom-filter 预检 | 7 个 bot 标记词预过滤,99% 正常流量跳过白名单遍历 |
| URL 参数短路 | 无查询参数时直接返回,跳过 pairs 循环和正则匹配 |
| 最小输入长度检查 | 输入 < 2 字符直接跳过规则匹配 |
| 请求类型短路 | bodyless="on" 时 GET/HEAD/OPTIONS 跳过 POST 和文件上传检测 |
| Cookie 检测后移 | Cookie 检测移到 URL/Args 之后,攻击请求在 URL 阶段即短路返回 |
| IP 预编译缓存 | CIDR 排序 + 二分搜索 + 精确 IP hash 查找 |
| 配置预合并 | 域名级配置在加载阶段预合并,请求阶段 O(1) 查找 |
| 请求上下文缓存 | clientIP 和 URI 在 context 中缓存,同请求内不重复计算 |
稳定性测试
| 测试项 | 结果 | 说明 |
|---|---|---|
| 持续压测 10 分钟 | ✅ | 内存零增长 |
| 100 万随机 URL CC 攻击 | ✅ | 内存仅增长 5MB |
| 32MB+ multipart 文件 | ✅ | 未崩溃 |
| 热加载规则一致性 | ✅ | 5s 内生效,服务不中断 |
| ReDoS 恶意正则 | ✅ | Go RE2 无回溯 |
| 超长 URL (100KB) | ✅ | 未崩溃 |
| 1000 个 Cookie | ✅ | 未崩溃 |
| 回归测试 | ✅ | 156/156 全部通过 |
| 44 种攻击拦截 | ✅ | 全部正确拦截 (403) |
| IPv4/IPv6 CIDR 拦截 | ✅ | 正确拦截 |
| 日志字段截断保护 | ✅ | 10KB URL 截断到 4110 字节 |
| cc_rate 无效配置 | ✅ | CC 自动禁用 + 错误日志 |
项目结构
1 | caddyguard/ |
快速开始
编译
1 | # 安装 xcaddy |
配置
1 | { |
启动
1 | caddy run --config /etc/caddy/Caddyfile --adapter caddyguardfile |
Docker 部署
1 | FROM caddy:2-builder AS builder |
日志
攻击日志同步落盘,不丢失。每行一条 JSON,包含时间戳、客户端 IP、攻击类型、命中规则、请求 URL 等:
1 | { |
日志文件按天分文件({YYYY-MM-DD}_waf.log),单文件超过 100MB 自动轮转为 .old,字段自动截断到 4096 字节防止单条日志过大。
与 NginxGuard 的对比
CaddyGuard 与同作者的 NginxGuard(基于 Lua + OpenResty)在设计理念上一脉相承,但在实现上有几个关键差异:
| NginxGuard | CaddyGuard | |
|---|---|---|
| 语言 | Lua | Go |
| Web 服务器 | Nginx / OpenResty | Caddy v2 |
| 全局自动生效 | Nginx 配置加载时注入 | caddyguardfile 适配器自动注入 |
| 并发模型 | OpenResty 单 worker 天然串行 | sync.Mutex / sync.RWMutex |
| 正则引擎 | PCRE(有 ReDoS 风险) | RE2(无回溯,ReDoS 安全) |
| CC 存储 | Lua shared dict | 64 分片 MemoryStore |
| IP 匹配 | Lua FFI + 二分搜索 | Go net.ParseCIDR + 二分搜索 |
| 性能开销 | ~16%(GET 流量) | ~1.7%(全规则开启) |
两者共享相同的规则文件格式和检测链顺序,可以无缝切换。
总结
CaddyGuard 证明了用 Go 原生编写 Caddy 中间件可以实现企业级 WAF 防护,同时保持极低的性能开销。它的核心设计理念是:
- 全局自动生效 — 配置一次,所有站点自动防护
- 分层优化 — 从规则预编译到请求短路,每一层都减少不必要的开销
- 安全第一 — ReDoS 安全、日志不丢失、超限拦截防部分扫描误放行
- 运维友好 — 热加载、域名级配置、路径级开关、JSON 日志
如果你在使用 Caddy v2 并且需要 WAF 防护,不妨试试 CaddyGuard。