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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
# 全局 WAF 配置 — 只写一次
caddyguard {
rule_dir /etc/caddyguard/rule-config
}
}

# 站点不需要写 caddyguard,自动生效
example.com {
reverse_proxy 127.0.0.1:8080
}

another.com {
reverse_proxy 127.0.0.1:9090
}

适配器在解析 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
2
3
4
5
6
7
8
9
10
11
# CIDR 表示法(推荐)
192.168.1.0/24 # IPv4 CIDR
2001:db8::/32 # IPv6 CIDR

# glob 通配符
192.168.1.* # IPv4 通配符
2001:db8::* # IPv6 通配符

# 精确匹配
8.8.8.8 # IPv4 精确
::1 # IPv6 loopback

在实现上,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
2
3
4
5
6
7
8
9
10
11
// 快速路径:节流窗口内直接用缓存
if entry != nil && !shouldCheck(atomic.LoadInt64(&entry.lastCheckNano)) {
return entry.config
}
// 需要检查 mtime
mtime, exists := getFileMtime(path)
// mtime 未变 → 缓存命中
if entry != nil && entry.modTime == mtime {
return entry.config
}
// 缓存未命中 → 写锁 + double check + 重新加载

域名级配置 + 路径级开关

域名级覆盖:通过 domain.json 为不同域名配置不同的检测策略,支持精确域名和通配符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"www.example.com": {
"url_check": "off",
"cc_rate": "100/60",
"rule_dir": "domains/www.example.com"
},
"api.example.com": {
"waf_enable": "off"
},
"*.test.com": {
"post_check": "off",
"cookie_check": "off"
}
}

域名级配置在加载阶段预合并到全局基线中,请求阶段只需一次 map 查找(O(1)),零运行时开销。

路径级开关:利用 Caddy 原生 path matcher,对特定 URL 路径关闭 WAF:

1
2
3
4
5
6
7
sub.example.com {
@webhook path /api/webhook/*
route @webhook {
caddyguard waf_enable off
reverse_proxy 127.0.0.1:8080
}
}

编码绕过防护

CaddyGuard 支持递归解码,防止攻击者通过多层编码绕过规则:

  • URL 编码:递归 URL-decode(最多 8 层),如 %25253C%253C%3C<
  • JS Unicode\u003C<\u{003C}<
  • JS Hex\x3C<
  • HTML 实体&#x3C;<&#60;<

正常流量(不含 %\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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
caddyguard/
├── module.go # Caddy 模块注册 + Caddyfile 解析 + ServeHTTP
├── handler.go # 检测链编排 + WAF 响应输出
├── config.go # Config 结构体定义
├── domain.go # 域名级配置查找(预合并缓存)
├── rules.go # 规则缓存 + 热加载 + 关键词自动提取
├── adapter.go # caddyguardfile 适配器(全局自动注入)
├── matcher.go # 正则匹配引擎(预编译 + 关键词预过滤 + 缓存)
├── storage.go # CC 存储(64 分片 + 滑动窗口)
├── logger.go # 同步日志(Mutex + 100MB 轮转)
├── decode.go # 递归解码(URL/JS Unicode/HTML 实体)
├── request_context.go # 请求上下文缓存
├── ip_cache.go # IP 规则预编译缓存(CIDR 二分搜索)
├── detector_ip.go # IP 黑白名单检测
├── detector_url.go # URL 路径 + 参数检测
├── detector_ua.go # User-Agent 检测
├── detector_cookie.go # Cookie 检测
├── detector_post.go # POST body 检测
├── detector_cc.go # CC 攻击检测
├── detector_referer.go # Referer 检测
├── detector_fileupload.go # 文件上传检测
├── rule-config/ # 规则配置文件
│ ├── config.json # 全局 WAF 配置
│ ├── domain.json # 域名级配置覆盖
│ ├── *.rule # 规则文件
│ └── domains/ # 域名级独立规则目录
└── test-config/ # 测试 Caddyfile + 压测脚本

快速开始

编译

1
2
3
4
5
6
7
8
9
10
11
12
# 安装 xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

# 从 GitHub 编译
xcaddy build --with github.com/qist/caddyguard --output ./caddy

# 验证
./caddy list-modules | grep caddyguard
# 输出:
# caddy.adapters.caddyguardfile
# caddyguard
# http.handlers.caddyguard

配置

1
2
3
4
5
6
7
8
9
{
caddyguard {
rule_dir /etc/caddyguard/rule-config
}
}

example.com {
reverse_proxy 127.0.0.1:8080
}

启动

1
caddy run --config /etc/caddy/Caddyfile --adapter caddyguardfile

Docker 部署

1
2
3
4
5
6
7
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/qist/caddyguard

FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
COPY rule-config /etc/caddyguard/rule-config
COPY Caddyfile /etc/caddy/Caddyfile

日志

攻击日志同步落盘,不丢失。每行一条 JSON,包含时间戳、客户端 IP、攻击类型、命中规则、请求 URL 等:

1
2
3
4
5
6
7
8
9
10
{
"@timestamp": "2026-08-13T04:30:00Z",
"client_ip": "192.168.2.50",
"local_time": "2026-08-13 12:30:00",
"server_name": "www.example.com",
"user_agent": "sqlmap/1.0",
"attack_method": "UserAgent",
"req_url": "/?id=1+union+select",
"rule_tag": "(sqlmap|nmap|...)"
}

日志文件按天分文件({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 防护,同时保持极低的性能开销。它的核心设计理念是:

  1. 全局自动生效 — 配置一次,所有站点自动防护
  2. 分层优化 — 从规则预编译到请求短路,每一层都减少不必要的开销
  3. 安全第一 — ReDoS 安全、日志不丢失、超限拦截防部分扫描误放行
  4. 运维友好 — 热加载、域名级配置、路径级开关、JSON 日志

如果你在使用 Caddy v2 并且需要 WAF 防护,不妨试试 CaddyGuard。

项目地址https://github.com/qist/caddyguard