- Implement http.Hijacker on responseRecorder to enable WebSocket upgrade (delegates Hijack() to underlying ResponseWriter, marks hijacked state) - After WebSocket upgrade (101 Switching Protocols), skip HTTP response-phase WAF checks since connection becomes a binary frame stream - Log WebSocket upgrades at Info level for observability - Fix P2-3: use r.Proto instead of hardcoded "HTTP/1.1" in ProcessResponseHeaders for correct HTTP/2 protocol version reporting - Add Write() method to responseRecorder to ensure WriteHeader is called before first Write (complies with http.ResponseWriter contract) - Add bufio/net imports required by Hijacker interface - Update README with WebSocket support section and v1.2 changelog - Update SECURITY_AUDIT to mark P2-3 and P2-4 as fixed
waf — Web 应用防火墙网关
基于 Go + coraza 的 WAF 网关,部署在 gateway 之前,终止 TLS 并按 SecLang 规则库匹配请求,未命中则转发到 gateway。
客户端 ──HTTPS──▶ waf:443 (终止 TLS + coraza 规则匹配) ──明文 HTTP──▶ gateway:8443 (鉴权 + 反代)
为什么是 Go
coraza-waf/coraza是 ModSecurity SecLang 的 Go 实现,100% 兼容 OWASP CRS v4net/http/httputil.ReverseProxy标准库反向代理,零第三方依赖- goroutine + net/http 吞吐量比 Python 高一个数量级
go build出单二进制,core/init.txt直接拉起
极客友好的规则系统
| 操作 | 命令 |
|---|---|
| 编辑规则 | vim rules/local/custom.conf |
| 校验语法 | ./waf.bin -test rules/local/custom.conf |
| 列出所有规则 | ./waf.bin -list |
| 测试输入 | ./waf.bin -check "/?id=1' OR '1'='1" |
| 热加载(不重启) | kill -HUP $(pgrep waf) |
| 临时禁用某文件 | mv rules/local/x.conf x.conf.bak && kill -HUP $(pgrep waf) (引擎仅加载 .conf 后缀) |
规则文件组织
rules/
├── waf.conf # 主入口:基础配置(SecRuleEngine / SecRequestBodyLimit 等)
├── local/ # 本地自定义规则(ID 段 100000-199999)
│ ├── custom.conf # 示例:SQL注入/XSS/路径遍历/命令注入/扫描器UA
│ └── exceptions.conf # 误报白名单(SecRuleRemoveById / ctl:ruleRemoveById)
└── crs/ # OWASP CRS(不入库,由 _fetch_crs.sh 拉取,可选)
├── coraza.conf # CRS 入口(请求体处理器初始化 + tx.crs_setup_version)
├── LICENSE # Apache-2.0
└── rules/*.conf # CRS 规则文件
注意:engine 不使用 coraza 的
Include指令(其路径相对 cwd 解析易出错),改为在 engine.Reload 中自行扫描目录、逐个WithDirectivesFromFile加载。
规则加载顺序
由 engine.go:50-89 Reload 控制,固定顺序:
waf.conf— 基础配置local/*.conf— 本地自定义规则(按文件名字母序)crs/coraza.conf— OWASP CRS 入口(仅当文件存在时加载)crs/rules/*.conf— OWASP CRS 规则(按文件名字母序)
规则 ID 分段
| ID 段 | 用途 |
|---|---|
| 100000-199999 | 本地自定义规则 |
| 200000-200006 | CRS 请求体处理器初始化(XML/JSON,由 crs/coraza.conf 定义,仅当启用 CRS 时存在) |
| 900000-999999 | OWASP CRS 规则(由 crs/rules/ 提供,不手改,仅当启用 CRS 时存在) |
规则语法(SecLang)
SecRule REQUEST_URI|ARGS "@rx (?i)union[\s/*]+select" \
"id:100001,phase:1,deny,status:403,log,msg:'SQL injection: UNION SELECT'"
字段说明:
REQUEST_URI|ARGS匹配目标(URI / 参数 / Header / Cookie 等)@rx ...正则匹配(也支持@pm、@contains、@beginsWith等)phase:1请求头阶段 /phase:2请求体阶段 /phase:3响应头 /phase:4响应体deny,status:403命中后动作msg命中日志描述
部署形态
WAF 必须终止 TLS 才能看到明文 body,因此证书需从 gateway 迁到 WAF:
- 把
gateway的full_chain.crt/server.key复制到waf/certs/ gateway改为监听127.0.0.1:8443,关闭 TLS(仅本机)waf监听:443,转发到http://127.0.0.1:8443
配置
config.yaml:
listen: ":443"
tls_cert: "./certs/full_chain.crt"
tls_key: "./certs/server.key"
# gateway 内部 HTTP 端口(gateway 需改为监听 127.0.0.1:8443 不启用 TLS)
upstream: "http://127.0.0.1:8443"
# 规则根目录,主入口为 <rules_dir>/waf.conf
rules_dir: "./rules"
| 字段 | 默认值 | 说明 |
|---|---|---|
listen |
:443 |
监听地址 |
tls_cert / tls_key |
— | TLS 证书与私钥路径 |
upstream |
— | gateway 地址,必填(启动时硬性校验,缺失将导致进程退出) |
rules_dir |
./rules |
规则根目录 |
目录结构
waf/
├── main.go # 入口:服务模式 + CLI 子命令
├── config.yaml # 配置
├── go.mod
├── internal/
│ ├── engine/
│ │ └── engine.go # coraza 封装:加载/热加载/列举/单测/输入检测
│ ├── logclient/
│ │ └── logclient.go # Go 版 pymsg 日志客户端(异步批量推送)
│ └── proxy/
│ └── proxy.go # ReverseProxy + WAF 事务接入 + 安全响应头注入
├── rules/
│ ├── waf.conf
│ └── local/
│ ├── custom.conf
│ └── exceptions.conf
├── _set.sh # 编译(含 go mod tidy + download + build)
├── _run.sh # 运行
├── _goproxy.sh # 设置 GOPROXY 国内镜像(由 _set.sh 调用)
├── _fetch_crs.sh # 拉取 OWASP CRS(可选)
├── .gitignore
└── README.md
安装
# 1. 安装 Go 1.21+(实际要求 1.22+,见 go.mod)
brew install go # macOS
# 或 sudo apt install golang-go # Debian/Ubuntu
# 2. 编译(_set.sh 会自动设置 GOPROXY 国内镜像 + GOTOOLCHAIN=local)
bash _set.sh
# 3. 准备证书
mkdir certs && cp /path/to/full_chain.crt /path/to/server.key certs/
# 4. 运行
bash _run.sh
国内环境:_set.sh 会调用 _goproxy.sh 自动设置
GOPROXY=https://goproxy.cn,direct与GOSUMDB=sum.golang.google.cn;并强制GOTOOLCHAIN=local避免 Go 1.21+ 自动从proxy.golang.org下载 toolchain 超时。用户已显式配置非默认代理时尊重不覆盖。
接入 core 自启动
在 core/init.txt 增加:
bash /opt/micro_servers/waf/_run.sh
并把 gateway 改为内部 HTTP 端口(不再监听 443)。
安全响应头
WAF 在边缘层统一注入安全响应头(proxy.go:54-77 setSecurityHeaders),覆盖所有出站响应(包括正常转发、命中拦截、上游 502)。下游服务(static/dnfs 等)的同名头会被 WAF 覆盖,避免 CSP 绕过。
| Header | 值 | 说明 |
|---|---|---|
| X-Frame-Options | SAMEORIGIN | 允许本站 iframe 嵌套(登录页条例弹窗),防跨站点击劫持 |
| X-Content-Type-Options | nosniff | — |
| X-XSS-Protection | 1; mode=block | 现代浏览器已废弃,仅兼容旧浏览器 |
| Referrer-Policy | strict-origin-when-cross-origin | — |
| Permissions-Policy | camera=(), microphone=(), geolocation=() | — |
| Strict-Transport-Security | max-age=15552000 | HSTS 半年期,不含子域 |
| Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; ... | 暂保留 'unsafe-inline' 以兼容 forgot.html 与 games/*.html 的内联脚本 |
拦截日志
命中规则时 block() 经 logclient(Go 版 pymsg 客户端,见 internal/logclient/)异步批量推送到 db:8005 的 Logs 表(与其他服务的集中日志同链路;标准日志已静默,stdout/journalctl 中看不到拦截记录):
- 规则命中拦截(block 触发):
service=waf、level=WARNING - 规则加载/解析错误(
LogError回调触发,引擎初始化/热加载时的语义错误):service=waf、level=ERROR
block() 推送的日志 extra 字段(JSON 字符串):
| 字段 | 类型 | 说明 |
|---|---|---|
rule_id |
int | 命中规则 ID |
ip |
string | RemoteAddr 原始值,含端口号(如 1.2.3.4:54321) |
method |
string | 请求方法 |
path |
string | 请求路径 |
query |
string | 查询字符串 |
ua |
string | User-Agent |
cost_ms |
int64 | 处理耗时(毫秒) |
fail-safe:db 不可用时日志静默丢弃,不影响拦截动作本身。
查询最近的拦截记录:
sqlite3 /opt/micro_servers/db/database.db \
"SELECT ts, message, extra FROM Logs WHERE service='waf' AND level='WARNING' ORDER BY id DESC LIMIT 20;"
聚合统计命中规则:
sqlite3 /opt/micro_servers/db/database.db \
"SELECT json_extract(extra,'\$.rule_id') AS rule, COUNT(*) FROM Logs \
WHERE service='waf' AND level='WARNING' GROUP BY rule ORDER BY 2 DESC;"
查看规则加载/解析错误:
sqlite3 /opt/micro_servers/db/database.db \
"SELECT ts, message, extra FROM Logs WHERE service='waf' AND level='ERROR' ORDER BY id DESC LIMIT 20;"
启用 OWASP CRS(可选)
通过 Go module proxy 拉取(不依赖 GitHub 直连,国内可用):
bash _fetch_crs.sh # 拉取最新版
# 或
bash _fetch_crs.sh v4.25.0 # 拉取指定版本
脚本会:
- 通过
GOPROXY=https://goproxy.cn,direct下载github.com/corazawaf/coraza-coreruleset/v4到模块缓存 - 复制规则文件(
*.conf+*.data)到rules/crs/rules/ - 自动生成
rules/crs/coraza.conf(含请求体处理器初始化规则 200000-200006 与tx.crs_setup_version) - 复制 Apache-2.0 LICENSE
完成后 rules/crs/coraza.conf 存在,engine.Reload 会自动检测并加载,无需改 waf.conf。直接热加载即可生效:
kill -HUP $(pgrep waf)
校验加载结果:
./waf.bin -list | head
rules/crs/整个目录已在.gitignore中,不入库。如需升级 CRS,重跑_fetch_crs.sh即可。
请求处理流程
proxy.go:81-172 ServeHTTP:
ProcessConnection+ProcessURI+ 添加请求头(含 Host)ProcessRequestHeaders→ 命中即block返回 403- 读取请求体(10MB 硬限制,超限返回 413)→
ReadRequestBodyFrom→ProcessRequestBody→ 命中即 block httputil.ReverseProxy.ServeHTTP转发到 gateway- 捕获响应头 →
ProcessResponseHeaders(仅审计日志,不拦截) ModifyResponse统一注入安全响应头(覆盖下游同名头)- defer 中确保
r.Body耗尽并关闭,保证 TCP 连接复用
请求体大小限制
WAF 在代码层面对请求体实施 10MB 硬限制(proxy.go#L19-L22 maxRequestBodySize),与 waf.conf 中的 SecRequestBodyLimit 10485760 保持一致:
- 使用
io.LimitReader+ 多读 1 字节检测超限 - 超限直接返回
413 Request Entity Too Large,不经过 Coraza 规则引擎 - 读取错误(客户端断开)记录日志后直接返回,不继续处理
- 从根本上防止超大请求体导致 OOM 崩溃
HTTP 服务器超时防护
main.go#L111-L122 为 http.Server 配置了多层超时,防御 Slowloris 等慢连接 DoS 攻击:
| 参数 | 值 | 作用 |
|---|---|---|
ReadHeaderTimeout |
10s | 读取请求头超时,直接防御 Slowloris |
ReadTimeout |
15s | 读取整个请求(含 body)超时,防慢 POST |
WriteTimeout |
30s | 写入响应超时 |
IdleTimeout |
120s | keep-alive 空闲连接超时 |
MaxHeaderBytes |
1MB | 请求头大小限制 |
Director 改写(proxy.go:36-46):剥离 RemoteAddr 端口,设置 X-Forwarded-For 与 X-Forwarded-Proto: https,供 gateway 的 RateLimiter 取真实客户端 IP。
ErrorHandler(proxy.go:48-53):upstream 不可达时返回 502,同时注入安全响应头,不暴露内部堆栈,仅服务端 log。
WebSocket 代理支持
WAF 支持透明代理 WebSocket 连接(如 aibox 的 WebSocket 接口)。实现要点:
responseRecorder实现了http.Hijacker接口,将 Hijack 调用委托给底层http.ResponseWriter- WebSocket 握手阶段(HTTP Upgrade 请求/响应)仍然经过 WAF 规则检测(请求头 phase:1)
- 握手成功(101 Switching Protocols)后,连接被 Hijack,由
httputil.ReverseProxy自动进行双向字节流转发 - 升级后连接转为二进制帧协议,跳过 HTTP 响应阶段的 WAF 检测,记录 Info 日志
- WebSocket 帧内容不做 WAF 检测(帧是二进制双向流,Coraza 规则引擎基于 HTTP 请求/响应模型)
http.Server的超时设置不影响 Hijack 后的 WebSocket 长连接(Hijack 后连接脱离 Server 超时管理)- 安全响应头(CSP/HSTS 等)不注入 WebSocket 升级响应(101 响应由协议协商决定,注入非标准头可能导致客户端兼容问题)
配置要求:
- 无特殊配置,WebSocket 连接(
Upgrade: websocket)自动识别并代理 - 需要上游服务正确处理 WebSocket 升级(如 aibox 监听 ws:// 端口)
已知限制
- 响应头检测(
ProcessResponseHeaders)当前仅做审计日志,命中后不执行拦截(响应头已发送无法撤销状态码)。如需响应头拦截需引入 buffered response writer。 - 响应体检测未启用(未
ProcessResponseBody,缓冲整个响应延迟成本高)。SQL 报错回显等响应体泄漏检测缺失。 - WebSocket 帧内容不检测:升级后二进制帧直接转发,不经过 WAF 规则引擎(适用于内部可信服务如 aibox)。
- 限流/黑白名单未实现,可后续作为 coraza 规则或独立中间件加。
- TLS 仅设
MinVersion: TLS1.2,未限定 cipher suites,允许弱加密套件。 X-Forwarded-For直接覆盖原值(Header.Set),未按 RFC 7239 追加;当前 gateway 也只取第一个 IP,无实际影响,但语义不严格。注意:IPv6 地址在 X-Forwarded-For 中可能带方括号(待修复,使用LastIndexByte无法正确处理 IPv6 格式)。- CSP 仍含
'unsafe-inline'(为兼容 forgot.html 与 games/*.html 的内联脚本),待前端清理后可收紧。 - HSTS 已启用(半年期,不含子域),站点全量 HTTPS 覆盖后可按需收紧。
- logclient 日志推送每次新建 TCP 短连接,高并发拦截场景可能产生较多 TIME_WAIT;db 地址硬编码
127.0.0.1:8005,不支持环境变量配置。 - 默认规则全部注释:
rules/local/custom.conf中自定义规则默认注释,需手动取消注释或拉取 OWASP CRS 后才有实际拦截效果。启动时无 warning 提示。 - 缺少 SIGINT/SIGTERM 优雅关闭,进程终止时进行中的请求被暴力中断,logclient 队列中未发送的日志可能丢失。
- 标准日志(
log.SetOutput)被静默到io.Discard,net/http 的 panic 堆栈和 TLS 错误不可见。
版本日志
v1.2(2026-08-01)— WebSocket 支持 + 修复
- feat: WebSocket 透明代理支持,
responseRecorder实现http.Hijacker接口 - WebSocket 握手阶段仍经过 WAF 请求头检测,升级后自动双向转发
- WebSocket 升级成功记录 Info 日志,便于追踪
- fix(P2-3):
ProcessResponseHeaders协议版本从硬编码"HTTP/1.1"改为r.Proto,支持 HTTP/2 - fix(P2-4): responseRecorder 添加 Write 方法确保 WriteHeader 在首次 Write 前调用
- 补充
bufio/netimport 用于 Hijacker 实现
v1.1(2026-08-01)— 安全加固
- P0 修复:请求体 OOM 防护:添加 10MB 硬限制,
io.LimitReader+ 多读1字节检测超限,超限返回 413 - P0 修复:Slowloris DoS 防护:
http.Server添加 ReadHeaderTimeout(10s)/ReadTimeout(15s)/WriteTimeout(30s)/IdleTimeout(120s)/MaxHeaderBytes(1MB) - P1 修复:请求体读取错误处理:
io.ReadAll失败时记录日志并返回,不再传递出错 body 给 ReverseProxy;ReadRequestBodyFrom/ProcessRequestBody错误均记录日志 - P2 修复:r.Body 连接复用:defer 中耗尽并关闭 r.Body,确保 TCP 连接可复用
- 413 响应同样注入安全响应头
v1.0
- 初始版本:TLS 终止 + Coraza WAF 规则匹配 + 反向代理 + SIGHUP 热加载 + 异步日志推送 + 安全响应头注入
依赖
- Go 1.22+(见 go.mod,锁
go 1.22.0) github.com/corazawaf/coraza/v3v3.3.0(锁版本,避免 v3.7.0 要求 Go 1.25+)gopkg.in/yaml.v3v3.0.1
许可证
MIT License