Go WAF Plugin (#400)

This commit is contained in:
rinfx
2023-06-28 19:25:36 +08:00
committed by GitHub
parent fc05a3b256
commit c32e1ab69b
74 changed files with 22639 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
module github.com/corazawaf/coraza-proxy-wasm/magefiles
go 1.19
require (
fortio.org/fortio v1.38.4
github.com/magefile/mage v1.14.0
github.com/tetratelabs/wabin v0.0.0-20220927005300-3b0fbf39a46a
)
require (
github.com/google/uuid v1.3.0 // indirect
golang.org/x/exp v0.0.0-20221111204811-129d8d6c17ab // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
)

View File

@@ -0,0 +1,21 @@
fortio.org/assert v1.1.2 h1:t6WGDqPD5VFrUvx30U0+3mgXXcoPonrdKqt0vfJHn8E=
fortio.org/fortio v1.38.4 h1:HkVsu9E4emMU+i2D2HjPll1Dbu5IqCRTeKeoiYWxFWA=
fortio.org/fortio v1.38.4/go.mod h1:xZTReCI6wlPJQN+JssVO6jsqXJV2vC32dcZJrUaqmcU=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/tetratelabs/wabin v0.0.0-20220927005300-3b0fbf39a46a h1:P0R3+CTAT7daT8ig5gh9GEd/eDQ5md1xl4pkYMcwOqg=
github.com/tetratelabs/wabin v0.0.0-20220927005300-3b0fbf39a46a/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
golang.org/x/exp v0.0.0-20221111204811-129d8d6c17ab h1:1S7USr8/C0Sgk4egxq4zZ07zYt2Xh1IiFp8hUMXH/us=
golang.org/x/exp v0.0.0-20221111204811-129d8d6c17ab/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,81 @@
package main
import (
"fmt"
"net/http"
"time"
"fortio.org/fortio/fhttp"
"fortio.org/fortio/fnet"
"fortio.org/fortio/periodic"
"github.com/magefile/mage/sh"
)
// LoadTest runs load tests against the ftw deployment.
func LoadTest() error {
for _, threads := range []int{1, 2, 4} {
for _, payloadSize := range []int{0, 100, 1000, 10000} {
for _, conf := range []string{"envoy-config.yaml", "envoy-config-nowasm.yaml"} {
if err := doLoadTest(conf, payloadSize, threads); err != nil {
return err
}
}
}
}
return nil
}
func doLoadTest(conf string, payloadSize int, threads int) error {
if err := sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "build", "--pull"); err != nil {
return err
}
defer func() {
_ = sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "kill")
_ = sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "down", "-v")
}()
if err := sh.RunWithV(map[string]string{"ENVOY_CONFIG": fmt.Sprintf("/conf/%s", conf)}, "docker-compose",
"--file", "ftw/docker-compose.yml", "run", "--service-ports", "--rm", "-d", "envoy"); err != nil {
return err
}
// Wait for Envoy to start.
for i := 0; i < 1000; i++ {
if resp, err := http.Get("http://localhost:8080/anything"); err != nil {
continue
} else {
if resp.Body != nil {
resp.Body.Close()
}
if resp.StatusCode == http.StatusOK {
break
}
}
time.Sleep(50 * time.Millisecond)
}
opts := &fhttp.HTTPRunnerOptions{
RunnerOptions: periodic.RunnerOptions{
QPS: 100,
NumThreads: threads,
Duration: 10 * time.Second,
},
HTTPOptions: fhttp.HTTPOptions{
URL: "http://localhost:8080/anything",
Payload: fnet.GenerateRandomPayload(payloadSize),
},
}
fmt.Printf("Running load test with config=%s, payloadSize=%d, threads=%d\n", conf, payloadSize, threads)
res, err := fhttp.RunHTTPTest(opts)
if err != nil {
return err
}
rr := res.Result()
fmt.Printf("All done %d calls (plus %d warmup) %.3f ms avg, %.1f qps\n",
rr.DurationHistogram.Count,
0,
1000.*rr.DurationHistogram.Avg,
rr.ActualQPS)
return nil
}

View File

@@ -0,0 +1,271 @@
package main
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/tetratelabs/wabin/binary"
"github.com/tetratelabs/wabin/wasm"
)
var minGoVersion = "1.19"
var tinygoMinorVersion = "0.27"
var addLicenseVersion = "04bfe4ee9ca5764577b029acc6a1957fd1997153" // https://github.com/google/addlicense
var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases
var gosImportsVer = "v0.3.1" // https://github.com/rinchsan/gosimports/releases/tag/v0.3.1
var errCommitFormatting = errors.New("files not formatted, please commit formatting changes")
var errNoGitDir = errors.New("no .git directory found")
func init() {
for _, check := range []func() error{
checkTinygoVersion,
checkGoVersion,
} {
if err := check(); err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
}
}
// checkGoVersion checks the minimum version of Go is supported.
func checkGoVersion() error {
v, err := sh.Output("go", "version")
if err != nil {
return fmt.Errorf("unexpected go error: %v", err)
}
// Version can/cannot include patch version e.g.
// - go version go1.19 darwin/arm64
// - go version go1.19.2 darwin/amd64
versionRegex := regexp.MustCompile("go([0-9]+).([0-9]+).?([0-9]+)?")
compare := versionRegex.FindStringSubmatch(v)
if len(compare) != 4 {
return fmt.Errorf("unexpected go semver: %q", v)
}
compare = compare[1:]
if compare[2] == "" {
compare[2] = "0"
}
base := strings.SplitN(minGoVersion, ".", 3)
if len(base) == 2 {
base = append(base, "0")
}
for i := 0; i < 3; i++ {
baseN, _ := strconv.Atoi(base[i])
compareN, _ := strconv.Atoi(compare[i])
if baseN > compareN {
return fmt.Errorf("unexpected go version, minimum want %q, have %q", minGoVersion, strings.Join(compare, "."))
}
}
return nil
}
// checkTinygoVersion checks that exactly the right tinygo version is supported because
// tinygo isn't stable yet.
func checkTinygoVersion() error {
v, err := sh.Output("tinygo", "version")
if err != nil {
return fmt.Errorf("unexpected tinygo error: %v", err)
}
// Assume a dev build is valid.
if strings.Contains(v, "-dev") {
return nil
}
if !strings.HasPrefix(v, fmt.Sprintf("tinygo version %s", tinygoMinorVersion)) {
return fmt.Errorf("unexpected tinygo version, wanted %s", tinygoMinorVersion)
}
return nil
}
// Format formats code in this repository.
func Format() error {
if err := sh.RunV("go", "mod", "tidy"); err != nil {
return err
}
// addlicense strangely logs skipped files to stderr despite not being erroneous, so use the long sh.Exec form to
// discard stderr too.
if _, err := sh.Exec(map[string]string{}, io.Discard, io.Discard, "go", "run", fmt.Sprintf("github.com/google/addlicense@%s", addLicenseVersion),
"-c", "The OWASP Coraza contributors",
"-s=only",
"-y=",
"-ignore", "**/*.yml",
"-ignore", "**/*.yaml",
"-ignore", "examples/**", "."); err != nil {
return err
}
return sh.RunV("go", "run", fmt.Sprintf("github.com/rinchsan/gosimports/cmd/gosimports@%s", gosImportsVer),
"-w",
"-local",
"github.com/corazawaf/coraza-proxy-wasm",
".")
}
// Lint verifies code quality.
func Lint() error {
if err := sh.RunV("go", "run", fmt.Sprintf("github.com/golangci/golangci-lint/cmd/golangci-lint@%s", golangCILintVer), "run"); err != nil {
return err
}
mg.SerialDeps(Format)
if sh.Run("git", "diff", "--exit-code") != nil {
return errCommitFormatting
}
return nil
}
// Test runs all unit tests.
func Test() error {
return sh.RunV("go", "test", "./...")
}
// Coverage runs tests with coverage and race detector enabled.
func Coverage() error {
if err := os.MkdirAll("build", 0755); err != nil {
return err
}
if err := sh.RunV("go", "test", "-race", "-coverprofile=build/coverage.txt", "-covermode=atomic", "-coverpkg=./...", "./..."); err != nil {
return err
}
return sh.RunV("go", "tool", "cover", "-html=build/coverage.txt", "-o", "build/coverage.html")
}
// Doc runs godoc, access at http://localhost:6060
func Doc() error {
return sh.RunV("go", "run", "golang.org/x/tools/cmd/godoc@latest", "-http=:6060")
}
// Check runs lint and tests.
func Check() {
mg.SerialDeps(Lint, Test)
}
// Build builds the Coraza wasm plugin.
func Build() error {
if err := os.MkdirAll("local", 0755); err != nil {
return err
}
buildTags := []string{"custommalloc", "no_fs_access"}
if os.Getenv("TIMING") == "true" {
buildTags = append(buildTags, "timing", "proxywasm_timing")
}
if os.Getenv("MEMSTATS") == "true" {
buildTags = append(buildTags, "memstats")
}
buildTagArg := fmt.Sprintf("-tags='%s'", strings.Join(buildTags, " "))
// ~100MB initial heap
initialPages := 2100
if ipEnv := os.Getenv("INITIAL_PAGES"); ipEnv != "" {
if ip, err := strconv.Atoi(ipEnv); err != nil {
return err
} else {
initialPages = ip
}
}
if err := sh.RunV("tinygo", "build", "-gc=custom", "-opt=2", "-o", filepath.Join("local", "mainraw.wasm"), "-scheduler=none", "-target=wasi", buildTagArg); err != nil {
return err
}
if err := patchWasm(filepath.Join("local", "mainraw.wasm"), filepath.Join("local", "main.wasm"), initialPages); err != nil {
return err
}
if err := sh.RunV("rm", filepath.Join("local", "mainraw.wasm")); err != nil {
return err
}
return nil
}
// E2e runs e2e tests with a built plugin against the example deployment. Requires docker-compose.
func E2e() error {
if err := sh.RunV("docker-compose", "--file", "e2e/docker-compose.yml", "build", "--pull"); err != nil {
return err
}
return sh.RunV("docker-compose", "-f", "e2e/docker-compose.yml", "up", "--abort-on-container-exit", "tests")
}
// Ftw runs ftw tests with a built plugin and Envoy. Requires docker-compose.
func Ftw() error {
if err := sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "build", "--pull"); err != nil {
return err
}
defer func() {
_ = sh.RunV("docker-compose", "--file", "ftw/docker-compose.yml", "down", "-v")
}()
env := map[string]string{
"FTW_CLOUDMODE": os.Getenv("FTW_CLOUDMODE"),
"FTW_INCLUDE": os.Getenv("FTW_INCLUDE"),
"ENVOY_IMAGE": os.Getenv("ENVOY_IMAGE"),
}
if os.Getenv("ENVOY_NOWASM") == "true" {
env["ENVOY_CONFIG"] = "/conf/envoy-config-nowasm.yaml"
}
task := "ftw"
if os.Getenv("MEMSTATS") == "true" {
task = "ftw-memstats"
}
return sh.RunWithV(env, "docker-compose", "--file", "ftw/docker-compose.yml", "run", "--rm", task)
}
// RunExample spins up the test environment, access at http://localhost:8080. Requires docker-compose.
func RunExample() error {
return sh.RunWithV(map[string]string{"ENVOY_IMAGE": os.Getenv("ENVOY_IMAGE")}, "docker-compose", "--file", "example/docker-compose.yml", "up", "-d", "envoy-logs")
}
// TeardownExample tears down the test environment. Requires docker-compose.
func TeardownExample() error {
return sh.RunV("docker-compose", "--file", "example/docker-compose.yml", "down")
}
var Default = Build
func patchWasm(inPath, outPath string, initialPages int) error {
raw, err := os.ReadFile(inPath)
if err != nil {
return err
}
mod, err := binary.DecodeModule(raw, wasm.CoreFeaturesV2)
if err != nil {
return err
}
mod.MemorySection.Min = uint32(initialPages)
for _, imp := range mod.ImportSection {
switch {
case imp.Name == "fd_filestat_get":
imp.Name = "fd_fdstat_get"
case imp.Name == "path_filestat_get":
imp.Module = "env"
imp.Name = "proxy_get_header_map_value"
}
}
out := binary.EncodeModule(mod)
if err = os.WriteFile(outPath, out, 0644); err != nil {
return err
}
return nil
}