From 4bccdf2986063578588c8774cbf45ce5aac85b7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A6=8F=E7=8B=BC?= Date: Thu, 11 Jun 2026 23:27:19 +0800 Subject: [PATCH 1/2] test(coraza): add middleware performance benchmarks --- v3/coraza/coraza_bench_test.go | 406 +++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 v3/coraza/coraza_bench_test.go diff --git a/v3/coraza/coraza_bench_test.go b/v3/coraza/coraza_bench_test.go new file mode 100644 index 000000000..d5fa21af9 --- /dev/null +++ b/v3/coraza/coraza_bench_test.go @@ -0,0 +1,406 @@ +package coraza + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strconv" + "testing" + "time" + + corazawaf "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/types" + "github.com/gofiber/fiber/v3" + fiberlog "github.com/gofiber/fiber/v3/log" + "github.com/valyala/fasthttp" +) + +const benchQueryRules = `SecRuleEngine On +SecRequestBodyAccess On +SecRule ARGS:attack "@streq 1" "id:1001,phase:2,deny,status:403,msg:'attack detected'"` + +const benchBodyRules = `SecRuleEngine On +SecRequestBodyAccess On +SecRule REQUEST_BODY "@contains attack" "id:1002,phase:2,deny,status:403,msg:'body attack detected'"` + +const benchBodyLimit = 4 * 1024 * 1024 + +var ( + benchBody1KBAllow = bytes.Repeat([]byte("payload=safe&"), 78) + benchBody1KBBlock = append(bytes.Repeat([]byte("payload=safe&"), 77), []byte("payload=attack")...) + benchBody64KBAllow = bytes.Repeat([]byte("payload=safe&"), 5041) + benchObserveLatency = 2 * time.Millisecond +) + +func BenchmarkFiberBaseline_GET(b *testing.B) { + b.ReportAllocs() + + app := newBenchmarkApp(b, nil) + req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) + benchmarkAppRequest(b, app, func() *http.Request { + return req + }) +} + +func BenchmarkCoraza_NoRules_GET(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, "") + app := newBenchmarkApp(b, engine) + req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) + benchmarkAppRequest(b, app, func() *http.Request { + return req + }) +} + +func BenchmarkCoraza_QueryRule_GET_Allow(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, benchQueryRules) + app := newBenchmarkApp(b, engine) + req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) + benchmarkAppRequest(b, app, func() *http.Request { + return req + }) +} + +func BenchmarkCoraza_QueryRule_GET_Block(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, benchQueryRules) + app := newBenchmarkApp(b, engine) + req := httptest.NewRequest(http.MethodGet, "/?attack=1", nil) + benchmarkAppRequest(b, app, func() *http.Request { + return req + }) +} + +func BenchmarkCoraza_BodyRule_POST_1KB_Allow(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, benchBodyRules) + app := newBenchmarkApp(b, engine) + benchmarkAppRequest(b, app, func() *http.Request { + return newBenchmarkPostRequest(benchBody1KBAllow) + }) +} + +func BenchmarkCoraza_BodyRule_POST_1KB_Block(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, benchBodyRules) + app := newBenchmarkApp(b, engine) + benchmarkAppRequest(b, app, func() *http.Request { + return newBenchmarkPostRequest(benchBody1KBBlock) + }) +} + +func BenchmarkCoraza_BodyRule_POST_64KB_Allow(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, benchBodyRules) + app := newBenchmarkApp(b, engine) + benchmarkAppRequest(b, app, func() *http.Request { + return newBenchmarkPostRequest(benchBody64KBAllow) + }) +} + +func BenchmarkCoraza_ManyHeaders_GET(b *testing.B) { + b.ReportAllocs() + + engine := newBenchmarkEngine(b, benchQueryRules) + app := newBenchmarkApp(b, engine) + req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) + addBenchmarkHeaders(req.Header, 64) + + benchmarkAppRequest(b, app, func() *http.Request { + return req + }) +} + +func BenchmarkConvertFiberToStdRequest_GET(b *testing.B) { + b.ReportAllocs() + + app, ctx := newBenchmarkCtx(b, http.MethodGet, "/?name=safe", nil) + defer app.ReleaseCtx(ctx) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + req, err := convertFiberToStdRequest(ctx) + if err != nil { + b.Fatal(err) + } + closeRequestBody(b, req) + } +} + +func BenchmarkConvertFiberToStdRequest_POST_1KB(b *testing.B) { + b.ReportAllocs() + + app, ctx := newBenchmarkCtx(b, http.MethodPost, "/", benchBody1KBAllow) + defer app.ReleaseCtx(ctx) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + req, err := convertFiberToStdRequest(ctx) + if err != nil { + b.Fatal(err) + } + closeRequestBody(b, req) + } +} + +func BenchmarkProcessRequest_FromStdRequest_GET(b *testing.B) { + b.ReportAllocs() + + waf := newBenchmarkWAF(b, "") + req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) + req.RemoteAddr = "127.0.0.1:1234" + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tx := waf.NewTransaction() + if _, err := processRequest(tx, req, benchBodyLimit); err != nil { + b.Fatal(err) + } + closeTransaction(b, tx) + } +} + +func BenchmarkProcessRequest_FromStdRequest_POST_1KB(b *testing.B) { + b.ReportAllocs() + + waf := newBenchmarkWAF(b, benchBodyRules) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tx := waf.NewTransaction() + req := newBenchmarkPostRequest(benchBody1KBAllow) + req.RemoteAddr = "127.0.0.1:1234" + if _, err := processRequest(tx, req, benchBodyLimit); err != nil { + b.Fatal(err) + } + closeTransaction(b, tx) + closeRequestBody(b, req) + } +} + +func BenchmarkCorazaTransaction_Direct_GET(b *testing.B) { + b.ReportAllocs() + + waf := newBenchmarkWAF(b, "") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tx := waf.NewTransaction() + processDirectRequest(b, tx, http.MethodGet, "/?name=safe", nil, 1) + closeTransaction(b, tx) + } +} + +func BenchmarkCorazaTransaction_Direct_POST_1KB(b *testing.B) { + b.ReportAllocs() + + waf := newBenchmarkWAF(b, benchBodyRules) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + tx := waf.NewTransaction() + processDirectRequest(b, tx, http.MethodPost, "/", benchBody1KBAllow, 1) + closeTransaction(b, tx) + } +} + +func BenchmarkDefaultMetricsCollector_ObserveRequest(b *testing.B) { + b.ReportAllocs() + + collector := NewDefaultMetricsCollector() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + collector.ObserveRequest(benchObserveLatency, i%16 == 0) + } +} + +func BenchmarkDefaultMetricsCollector_ObserveRequestParallel(b *testing.B) { + b.ReportAllocs() + + collector := NewDefaultMetricsCollector() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + collector.ObserveRequest(benchObserveLatency, i%16 == 0) + i++ + } + }) +} + +func newBenchmarkEngine(b *testing.B, rules string) *Engine { + b.Helper() + + cfg := Config{ + LogLevel: fiberlog.LevelError, + RequestBodyAccess: true, + } + if rules != "" { + cfg.DirectivesFile = []string{writeBenchmarkRuleFile(b, rules)} + } + + engine, err := NewEngine(cfg) + if err != nil { + b.Fatal(err) + } + + return engine +} + +func newBenchmarkWAF(b *testing.B, rules string) corazawaf.WAF { + b.Helper() + + cfg := corazawaf.NewWAFConfig().WithRequestBodyAccess() + if rules != "" { + cfg = cfg.WithDirectivesFromFile(writeBenchmarkRuleFile(b, rules)) + } + + waf, err := corazawaf.NewWAF(cfg) + if err != nil { + b.Fatal(err) + } + + return waf +} + +func writeBenchmarkRuleFile(b *testing.B, rules string) string { + b.Helper() + + path := filepath.Join(b.TempDir(), "bench.conf") + if err := os.WriteFile(path, []byte(rules), 0o600); err != nil { + b.Fatal(err) + } + + return path +} + +func newBenchmarkApp(b *testing.B, engine *Engine) *fiber.App { + b.Helper() + + app := fiber.New() + if engine != nil { + app.Use(engine.Middleware()) + } + app.All("/", func(c fiber.Ctx) error { + return c.SendString("ok") + }) + + return app +} + +func benchmarkAppRequest(b *testing.B, app *fiber.App, newReq func() *http.Request) { + b.Helper() + + resp, err := app.Test(newReq()) + if err != nil { + b.Fatal(err) + } + closeResponseBody(b, resp) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + resp, err := app.Test(newReq()) + if err != nil { + b.Fatal(err) + } + closeResponseBody(b, resp) + } +} + +func newBenchmarkPostRequest(body []byte) *http.Request { + req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req +} + +func addBenchmarkHeaders(header http.Header, count int) { + for i := 0; i < count; i++ { + header.Set("X-Bench-"+strconv.Itoa(i), "value-"+strconv.Itoa(i)) + } +} + +func newBenchmarkCtx(b *testing.B, method, uri string, body []byte) (*fiber.App, fiber.Ctx) { + b.Helper() + + app := fiber.New() + fctx := &fasthttp.RequestCtx{} + fctx.Request.Header.SetMethod(method) + fctx.Request.SetRequestURI(uri) + fctx.Request.Header.SetHost("example.com") + if len(body) > 0 { + fctx.Request.Header.SetContentType("application/x-www-form-urlencoded") + fctx.Request.SetBody(body) + } + + return app, app.AcquireCtx(fctx) +} + +func processDirectRequest(b *testing.B, tx types.Transaction, method, uri string, body []byte, headerCount int) { + b.Helper() + + tx.ProcessConnection("127.0.0.1", 1234, "", 0) + tx.ProcessURI(uri, method, "HTTP/1.1") + tx.AddRequestHeader("Host", "example.com") + tx.SetServerName("example.com") + for i := 0; i < headerCount; i++ { + tx.AddRequestHeader("X-Bench-"+strconv.Itoa(i), "value-"+strconv.Itoa(i)) + } + if it := tx.ProcessRequestHeaders(); it != nil { + return + } + if len(body) > 0 { + if it, _, err := tx.ReadRequestBodyFrom(bytes.NewReader(body)); err != nil { + b.Fatal(err) + } else if it != nil { + return + } + } + if _, err := tx.ProcessRequestBody(); err != nil { + b.Fatal(err) + } +} + +func closeResponseBody(b *testing.B, resp *http.Response) { + b.Helper() + + if resp.Body == nil { + return + } + if err := resp.Body.Close(); err != nil { + b.Fatal(err) + } +} + +func closeRequestBody(b *testing.B, req *http.Request) { + b.Helper() + + if req.Body == nil || req.Body == http.NoBody { + return + } + if err := req.Body.Close(); err != nil { + b.Fatal(err) + } +} + +func closeTransaction(b *testing.B, tx interface { + ProcessLogging() + Close() error +}) { + b.Helper() + + tx.ProcessLogging() + if err := tx.Close(); err != nil { + b.Fatal(err) + } +} From 4511dd814b401aa1cbfd63b7172129cf61f9bc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9?= Date: Thu, 18 Jun 2026 09:58:46 +0200 Subject: [PATCH 2/2] test(coraza): assert benchmark request paths and tidy helpers - assert the warm-up status in app-level benchmarks so "_Block" cases fail loudly if a rule stops blocking instead of silently measuring the allow path - drop the unused headerCount parameter from processDirectRequest - document body-size constants and the baseline-subtraction intent Co-Authored-By: Claude Opus 4.8 (1M context) --- v3/coraza/coraza_bench_test.go | 44 +++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/v3/coraza/coraza_bench_test.go b/v3/coraza/coraza_bench_test.go index d5fa21af9..70320c923 100644 --- a/v3/coraza/coraza_bench_test.go +++ b/v3/coraza/coraza_bench_test.go @@ -28,18 +28,27 @@ SecRule REQUEST_BODY "@contains attack" "id:1002,phase:2,deny,status:403,msg:'bo const benchBodyLimit = 4 * 1024 * 1024 var ( - benchBody1KBAllow = bytes.Repeat([]byte("payload=safe&"), 78) + // "payload=safe&" is 13 bytes; the repeat counts size each body to the + // labelled order of magnitude (78*13 ~= 1KB, 5041*13 ~= 64KB). + benchBody1KBAllow = bytes.Repeat([]byte("payload=safe&"), 78) + // benchBody1KBBlock ends in "payload=attack" so it triggers benchBodyRules. benchBody1KBBlock = append(bytes.Repeat([]byte("payload=safe&"), 77), []byte("payload=attack")...) benchBody64KBAllow = bytes.Repeat([]byte("payload=safe&"), 5041) benchObserveLatency = 2 * time.Millisecond ) +// BenchmarkFiberBaseline_GET measures a plain Fiber app driven through the +// app.Test harness with no Coraza middleware attached. The Coraza_* app-level +// benchmarks share that same harness, so this baseline is the reference to +// subtract when reading their numbers as middleware overhead. The lower-level +// BenchmarkProcessRequest_* / BenchmarkCorazaTransaction_* benchmarks isolate +// the middleware cost without the harness. func BenchmarkFiberBaseline_GET(b *testing.B) { b.ReportAllocs() app := newBenchmarkApp(b, nil) req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusOK, func() *http.Request { return req }) } @@ -50,7 +59,7 @@ func BenchmarkCoraza_NoRules_GET(b *testing.B) { engine := newBenchmarkEngine(b, "") app := newBenchmarkApp(b, engine) req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusOK, func() *http.Request { return req }) } @@ -61,7 +70,7 @@ func BenchmarkCoraza_QueryRule_GET_Allow(b *testing.B) { engine := newBenchmarkEngine(b, benchQueryRules) app := newBenchmarkApp(b, engine) req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusOK, func() *http.Request { return req }) } @@ -72,7 +81,7 @@ func BenchmarkCoraza_QueryRule_GET_Block(b *testing.B) { engine := newBenchmarkEngine(b, benchQueryRules) app := newBenchmarkApp(b, engine) req := httptest.NewRequest(http.MethodGet, "/?attack=1", nil) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusForbidden, func() *http.Request { return req }) } @@ -82,7 +91,7 @@ func BenchmarkCoraza_BodyRule_POST_1KB_Allow(b *testing.B) { engine := newBenchmarkEngine(b, benchBodyRules) app := newBenchmarkApp(b, engine) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusOK, func() *http.Request { return newBenchmarkPostRequest(benchBody1KBAllow) }) } @@ -92,7 +101,7 @@ func BenchmarkCoraza_BodyRule_POST_1KB_Block(b *testing.B) { engine := newBenchmarkEngine(b, benchBodyRules) app := newBenchmarkApp(b, engine) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusForbidden, func() *http.Request { return newBenchmarkPostRequest(benchBody1KBBlock) }) } @@ -102,7 +111,7 @@ func BenchmarkCoraza_BodyRule_POST_64KB_Allow(b *testing.B) { engine := newBenchmarkEngine(b, benchBodyRules) app := newBenchmarkApp(b, engine) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusOK, func() *http.Request { return newBenchmarkPostRequest(benchBody64KBAllow) }) } @@ -115,7 +124,7 @@ func BenchmarkCoraza_ManyHeaders_GET(b *testing.B) { req := httptest.NewRequest(http.MethodGet, "/?name=safe", nil) addBenchmarkHeaders(req.Header, 64) - benchmarkAppRequest(b, app, func() *http.Request { + benchmarkAppRequest(b, app, http.StatusOK, func() *http.Request { return req }) } @@ -195,7 +204,7 @@ func BenchmarkCorazaTransaction_Direct_GET(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { tx := waf.NewTransaction() - processDirectRequest(b, tx, http.MethodGet, "/?name=safe", nil, 1) + processDirectRequest(b, tx, http.MethodGet, "/?name=safe", nil) closeTransaction(b, tx) } } @@ -208,7 +217,7 @@ func BenchmarkCorazaTransaction_Direct_POST_1KB(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { tx := waf.NewTransaction() - processDirectRequest(b, tx, http.MethodPost, "/", benchBody1KBAllow, 1) + processDirectRequest(b, tx, http.MethodPost, "/", benchBody1KBAllow) closeTransaction(b, tx) } } @@ -299,13 +308,19 @@ func newBenchmarkApp(b *testing.B, engine *Engine) *fiber.App { return app } -func benchmarkAppRequest(b *testing.B, app *fiber.App, newReq func() *http.Request) { +func benchmarkAppRequest(b *testing.B, app *fiber.App, wantStatus int, newReq func() *http.Request) { b.Helper() + // Warm-up request, asserting the path actually exercised matches the + // benchmark label (e.g. a "_Block" case must really return 403, otherwise + // a broken rule would silently turn it into an allow-path measurement). resp, err := app.Test(newReq()) if err != nil { b.Fatal(err) } + if resp.StatusCode != wantStatus { + b.Fatalf("unexpected status code: got %d, want %d", resp.StatusCode, wantStatus) + } closeResponseBody(b, resp) b.ResetTimer() @@ -346,16 +361,13 @@ func newBenchmarkCtx(b *testing.B, method, uri string, body []byte) (*fiber.App, return app, app.AcquireCtx(fctx) } -func processDirectRequest(b *testing.B, tx types.Transaction, method, uri string, body []byte, headerCount int) { +func processDirectRequest(b *testing.B, tx types.Transaction, method, uri string, body []byte) { b.Helper() tx.ProcessConnection("127.0.0.1", 1234, "", 0) tx.ProcessURI(uri, method, "HTTP/1.1") tx.AddRequestHeader("Host", "example.com") tx.SetServerName("example.com") - for i := 0; i < headerCount; i++ { - tx.AddRequestHeader("X-Bench-"+strconv.Itoa(i), "value-"+strconv.Itoa(i)) - } if it := tx.ProcessRequestHeaders(); it != nil { return }