Files
blackbird/waf/main.go
T
qing dd9f8e51ff fix(waf): 修复P1/P2安全审计缺陷
P1-1: IPv6 地址解析改用 net.SplitHostPort,修复 XFF 带方括号致限流失效
P1-2: 捕获 ProcessResponseHeaders 返回值,命中记录 WARNING 告警
P1-4: 新增 SIGINT/SIGTERM 优雅关闭 + logclient.Flush()
P1-5: 标准日志输出到 stderr,保留 panic 堆栈
P2-1: 规则文件加载前校验大小(1 MiB 上限)防 OOM
P2-2: logclient db 地址支持 LOG_DB_ADDR 环境变量
P2-5: 日志失败输出到 stderr 而非 stdout

修复率 35% -> 76%(P1/P2 全部清零)
2026-08-08 02:36:22 +08:00

175 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Command waf 是 micro_servers 的 WAF 入口网关。
//
// 部署形态:客户端 ──HTTPS──▶ waf:443 ──明文 HTTP──▶ gateway:内部端口
// WAF 终止 TLS、运行 coraza 规则匹配,未命中则转发到 gateway。
//
// 极客操作:
//
// kill -HUP <pid> # 热加载规则
// waf -list # 列出所有规则
// waf -test rules/local/x.conf # 校验单文件语法
// waf -check "/?id=1' OR '1'='1" # 用当前规则测试输入
package main
import (
"context"
"crypto/tls"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"gopkg.in/yaml.v3"
"github.com/micro_servers/waf/internal/engine"
"github.com/micro_servers/waf/internal/logclient"
"github.com/micro_servers/waf/internal/proxy"
)
// Config WAF 配置文件结构。
type Config struct {
Listen string `yaml:"listen"` // 监听地址,默认 :443
TLSCert string `yaml:"tls_cert"` // 证书文件
TLSKey string `yaml:"tls_key"` // 私钥文件
Upstream string `yaml:"upstream"` // gateway 地址,如 http://127.0.0.1:8443
RulesDir string `yaml:"rules_dir"` // 规则根目录
}
func main() {
var (
configFile = flag.String("c", "config.yaml", "配置文件路径")
doList = flag.Bool("list", false, "列出所有规则后退出")
doTest = flag.String("test", "", "校验单个规则文件语法后退出")
doCheck = flag.String("check", "", "用当前规则测试输入字符串(URI/path)后退出")
)
flag.Parse()
cfg := loadConfig(*configFile)
// 子命令:极客 CLI 工具
if *doTest != "" {
if err := engine.TestFile(*doTest); err != nil {
fmt.Fprintln(os.Stderr, "FAIL:", err)
os.Exit(1)
}
fmt.Println("OK")
return
}
eng, err := engine.New(&engine.Config{
RulesDir: cfg.RulesDir,
ErrorCallback: engine.LogError,
})
if err != nil {
log.Fatalf("引擎初始化失败: %v", err)
}
if *doList {
rules, err := eng.ListRules()
if err != nil {
log.Fatal(err)
}
out, _ := json.MarshalIndent(rules, "", " ")
fmt.Println(string(out))
return
}
if *doCheck != "" {
ids := eng.CheckInput(*doCheck)
fmt.Printf("input=%q hit_rules=%v\n", *doCheck, ids)
return
}
// 服务模式
logclient.Init()
// 标准日志输出到 stderr(systemd/journalctl 可捕获),保留 net/http 的 panic 堆栈与 TLS 错误
log.SetOutput(os.Stderr)
p, err := proxy.New(cfg.Upstream, eng)
if err != nil {
logclient.Error("waf", fmt.Sprintf("代理初始化失败: %v", err), nil)
logclient.Flush()
os.Exit(1)
}
// SIGHUP 热加载
hupCh := make(chan os.Signal, 1)
signal.Notify(hupCh, syscall.SIGHUP)
go func() {
for range hupCh {
if err := eng.Reload(); err != nil {
logclient.Error("waf", fmt.Sprintf("热加载失败,保留旧规则: %v", err), nil)
} else {
logclient.Info("waf", "规则热加载成功", nil)
}
}
}()
server := &http.Server{
Addr: cfg.Listen,
Handler: p,
ReadTimeout: 15 * time.Second,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
// SIGINT/SIGTERM 优雅关闭:等待进行中的请求完成后再退出,并 flush 队列日志
termCh := make(chan os.Signal, 1)
signal.Notify(termCh, syscall.SIGINT, syscall.SIGTERM)
logclient.Info("waf", fmt.Sprintf("服务启动 listen=%s upstream=%s rules=%s pid=%d",
cfg.Listen, cfg.Upstream, cfg.RulesDir, os.Getpid()), nil)
errCh := make(chan error, 1)
go func() {
errCh <- server.ListenAndServeTLS(cfg.TLSCert, cfg.TLSKey)
}()
select {
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
logclient.Error("waf", fmt.Sprintf("TLS 监听失败: %v", err), nil)
logclient.Flush()
os.Exit(1)
}
case <-termCh:
logclient.Info("waf", "收到终止信号,开始优雅关闭", nil)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logclient.Error("waf", fmt.Sprintf("优雅关闭超时: %v", err), nil)
}
logclient.Flush()
}
}
func loadConfig(path string) *Config {
data, err := os.ReadFile(path)
if err != nil {
log.Fatalf("读取配置失败: %v", err)
}
var c Config
if err := yaml.Unmarshal(data, &c); err != nil {
log.Fatalf("解析配置失败: %v", err)
}
if c.Listen == "" {
c.Listen = ":443"
}
if c.RulesDir == "" {
c.RulesDir = "./rules"
}
if c.Upstream == "" {
log.Fatal("upstream 不能为空")
}
return &c
}