From d46723d465b7b5804f8328ead50e380fac458a20 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Tue, 14 Jan 2025 15:33:04 +0000 Subject: [PATCH 01/16] "feat: add replay protection plugin This plugin prevents replay attacks by validating request nonce. Key features: - Nonce validation - Redis-based replay detection - Configurable TTL and validation rules" --- .../extensions/replay-protection/README.md | 223 ++++++++++++++++++ .../extensions/replay-protection/README_EN.md | 88 +++++++ .../extensions/replay-protection/VERSION | 1 + .../extensions/replay-protection/go.mod | 20 ++ .../extensions/replay-protection/go.sum | 24 ++ .../extensions/replay-protection/main.go | 159 +++++++++++++ 6 files changed, 515 insertions(+) create mode 100644 plugins/wasm-go/extensions/replay-protection/README.md create mode 100644 plugins/wasm-go/extensions/replay-protection/README_EN.md create mode 100644 plugins/wasm-go/extensions/replay-protection/VERSION create mode 100644 plugins/wasm-go/extensions/replay-protection/go.mod create mode 100644 plugins/wasm-go/extensions/replay-protection/go.sum create mode 100644 plugins/wasm-go/extensions/replay-protection/main.go diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md new file mode 100644 index 0000000000..7c32499a48 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -0,0 +1,223 @@ +--- +title: 防重放攻击 +keywords: [higress,nonce-protection] +description: 防重放攻击插件配置参考 +--- + +## 简介 + +Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机数来防止请求重放攻击。每个请求都需要携带一个唯一的 nonce 值,服务器会记录并校验这个值的唯一性,从而防止请求被恶意重放。 + +## 功能说明 + +- 强制或可选的 nonce 校验 +- 基于 Redis 的 nonce 唯一性验证 +- 可配置的 nonce 有效期 +- nonce 格式和长度校验 + +## 配置说明 + +| 配置项 | 类型 | 必填 | 默认值 | 说明 | +|-------------------|------|------|--------|-----| +| force_nonce | bool | 否 | true | 是否强制要求 nonce | +| nonce_ttl | int | 否 | 900 | nonce 有效期(单位:秒) | +| nonce_min_length | int | 否 | 8 | nonce 最小长度 | +| nonce_max_length | int | 否 | 128 | nonce 最大长度 | +| reject_code | int | 否 | 429 | 拒绝请求时的状态码 | +| reject_msg | string | 否 | "Duplicate nonce" | 拒绝请求时的错误信息 | +| redis.serviceName | string | 是 | - | Redis 服务名称 | +| redis.servicePort | int | 否 | 6379 | Redis 服务端口 | +| redis.timeout | int | 否 | 1000 | Redis 操作超时时间(毫秒) | +| redis.keyPrefix | string | 否 | "replay-protection" | Redis key 前缀 | + +## 配置示例 + +```yaml +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: replay-protection + namespace: higress-system +spec: + defaultConfig: + force_nonce: true + nonce_ttl: 900 + nonce_min_length: 8 + nonce_max_length: 128 + redis: + serviceName: "redis.higress" + servicePort: 6379 + timeout: 1000 + keyPrefix: "replay-protection" +url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 +``` + +## 使用说明 + +### 请求头要求 + +| 请求头 | 是否必须 | 说明 | +|-------|---------|------| +| x-apigw-nonce | 由 force_nonce 配置决定 | 随机生成的 nonce 值,需符合 base64 编码格式 | + + +### 1. 测试环境配置 + +```yaml +# test-ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: test-api + namespace: default +spec: + rules: + - host: test.example.com + http: + paths: + - path: /api/test + pathType: Prefix + backend: + service: + name: test-service + port: + number: 8080 +--- +# test-wasmplugin.yaml +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: replay-protection + namespace: higress-system +spec: + defaultConfig: + force_nonce: true + nonce_ttl: 60 # 测试时设置短一点,比如60秒 + nonce_min_length: 8 + nonce_max_length: 128 + redis: + serviceName: "redis.higress" # 确保这个 Redis 服务可用 + servicePort: 6379 + timeout: 1000 + keyPrefix: "test-replay-protection" + matchRules: + - ingress: + - default/test-api # 匹配我们的测试 Ingress +url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 +``` + +### 2. 测试脚本 + +```bash +#!/bin/bash + +# 测试 API 地址 +API_URL="http://test.example.com/api/test" + +# 测试用例1: 正常请求 +test_normal_request() { + echo "测试用例1: 正常请求" + nonce=$(openssl rand -base64 32) + echo "使用 nonce: $nonce" + + curl -X POST "$API_URL" \ + -H "x-apigw-nonce: $nonce" \ + -H "Host: test.example.com" \ + -d '{"test": "data"}' + echo -e "\n" +} + +# 测试用例2: 重放攻击 +test_replay_attack() { + echo "测试用例2: 重放攻击" + nonce=$(openssl rand -base64 32) + echo "使用 nonce: $nonce" + + # 第一次请求 + echo "第一次请求:" + curl -X POST "$API_URL" \ + -H "x-apigw-nonce: $nonce" \ + -H "Host: test.example.com" \ + -d '{"test": "data"}' + echo -e "\n" + + # 重放请求 + echo "重放请求:" + curl -X POST "$API_URL" \ + -H "x-apigw-nonce: $nonce" \ + -H "Host: test.example.com" \ + -d '{"test": "data"}' + echo -e "\n" +} + +# 测试用例3: 无 nonce +test_without_nonce() { + echo "测试用例3: 无 nonce" + curl -X POST "$API_URL" \ + -H "Host: test.example.com" \ + -d '{"test": "data"}' + echo -e "\n" +} + +# 测试用例4: nonce 太短 +test_short_nonce() { + echo "测试用例4: nonce 太短" + curl -X POST "$API_URL" \ + -H "x-apigw-nonce: abc" \ + -H "Host: test.example.com" \ + -d '{"test": "data"}' + echo -e "\n" +} + +# 运行所有测试 +run_all_tests() { + test_normal_request + sleep 2 + test_replay_attack + sleep 2 + test_without_nonce + sleep 2 + test_short_nonce +} + +# 执行测试 +run_all_tests +``` + + +### 3. 预期结果 + +1. **正常请求**: +```json +{ + "success": true, + "data": "..." +} +``` + +2. **重放攻击**: +```json +{ + "code": 429, + "message": "Request replay detected" +} +``` + +3. **无 nonce**: +```json +{ + "code": 400, + "message": "Missing nonce header" +} +``` + +4. **nonce 太短**: +```json +{ + "code": 400, + "message": "Invalid nonce length" +} +``` + + + diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md new file mode 100644 index 0000000000..e83ce68ac3 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -0,0 +1,88 @@ +--- +title: Nonce Replay Protection +keywords: [higress, replay-protection] +description: replay-protection config example +--- + + +## Introduction + +The Nonce (Number used ONCE) replay protection plugin prevents request replay attacks by validating a one-time random number in requests. Each request must carry a unique nonce value, which the server records and validates to prevent malicious request replay. + +## Features + +- Mandatory or optional nonce validation +- Redis-based nonce uniqueness verification +- Configurable nonce TTL +- Custom error responses +- Nonce format and length validation + +## Configuration + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| force_nonce | bool | No | true | Whether to enforce nonce requirement | +| nonce_ttl | int | No | 900 | Nonce validity period (seconds) | +| nonce_min_length | int | No | 8 | Minimum nonce length | +| nonce_max_length | int | No | 128 | Maximum nonce length | + +### Redis Configuration + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| serviceName | string | Yes | - | Redis service name | +| servicePort | int | No | 6379 | Redis service port | +| timeout | int | No | 1000 | Redis operation timeout (ms) | +| keyPrefix | string | No | "replay-protection" | Redis key prefix | + +## Configuration Example + +```yaml +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: replay-protection + namespace: higress-system +spec: + defaultConfig: + force_nonce: true + nonce_ttl: 900 + nonce_min_length: 8 + nonce_max_length: 128 + redis: + serviceName: "redis.higress" + servicePort: 6379 + timeout: 1000 + keyPrefix: "replay-protection" +url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 +``` + +## Usage + +### Required Headers + +| Header | Required | Description | +|--------|----------|-------------| +| x-apigw-nonce | Depends on force_nonce | Random generated nonce value in base64 format | + +### Usage Example + +```bash +# Generate nonce +nonce=$(openssl rand -base64 32) + +# Send request +curl -X POST 'https://api.example.com/path' \ + -H "x-apigw-nonce: $nonce" \ + -d '{"key": "value"}' +``` + +## Error Response + +```json +{ + "code": 429, + "message": "Duplicate nonce detected" +} +``` + diff --git a/plugins/wasm-go/extensions/replay-protection/VERSION b/plugins/wasm-go/extensions/replay-protection/VERSION new file mode 100644 index 0000000000..afaf360d37 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/VERSION @@ -0,0 +1 @@ +1.0.0 \ No newline at end of file diff --git a/plugins/wasm-go/extensions/replay-protection/go.mod b/plugins/wasm-go/extensions/replay-protection/go.mod new file mode 100644 index 0000000000..fc6ae8115e --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/go.mod @@ -0,0 +1,20 @@ +module github.com/alibaba/higress/plugins/wasm-go/extensions/replay-protection + +go 1.21.11 + +replace github.com/alibaba/higress/plugins/wasm-go => ../.. + +require ( + github.com/alibaba/higress/plugins/wasm-go v1.4.2 + github.com/higress-group/proxy-wasm-go-sdk v1.0.0 + github.com/tidwall/gjson v1.18.0 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect + github.com/magefile/mage v1.14.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/resp v0.1.1 // indirect +) diff --git a/plugins/wasm-go/extensions/replay-protection/go.sum b/plugins/wasm-go/extensions/replay-protection/go.sum new file mode 100644 index 0000000000..abe6578d09 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/go.sum @@ -0,0 +1,24 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA= +github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew= +github.com/higress-group/proxy-wasm-go-sdk v1.0.0 h1:BZRNf4R7jr9hwRivg/E29nkVaKEak5MWjBDhWjuHijU= +github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsefv8GU57h9CCLYGJA74/tF5/0= +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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= +github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go new file mode 100644 index 0000000000..a388703b62 --- /dev/null +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "fmt" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" + "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" + "github.com/tidwall/gjson" + "github.com/tidwall/resp" +) + +func main() { + wrapper.SetCtx( + "replay-protection", + wrapper.ParseConfigBy(parseConfig), + wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), + wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders), + ) +} + +type ReplayProtectionConfig struct { + ForceNonce bool // 是否启用强制 nonce 校验 + NonceTTL int // Nonce 的过期时间(单位:秒) + Redis RedisConfig + NonceMinLen int // nonce 最小长度 + NonceMaxLen int // nonce 最大长度 +} + +type RedisConfig struct { + client wrapper.RedisClient + keyPrefix string +} + +func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper.Log) error { + redisConfig := json.Get("redis") + if !redisConfig.Exists() { + return fmt.Errorf("missing redis config") + } + + serviceName := redisConfig.Get("serviceName").String() + if serviceName == "" { + return fmt.Errorf("redis service name is required") + } + + servicePort := redisConfig.Get("servicePort").Int() + if servicePort == 0 { + servicePort = 6379 + } + + username := redisConfig.Get("username").String() + password := redisConfig.Get("password").String() + timeout := redisConfig.Get("timeout").Int() + if timeout == 0 { + timeout = 1000 + } + + keyPrefix := redisConfig.Get("keyPrefix").String() + if keyPrefix == "" { + keyPrefix = "replay-protection" + } + config.Redis.keyPrefix = keyPrefix + + config.ForceNonce = json.Get("force_nonce").Bool() + config.NonceTTL = int(json.Get("nonce_ttl").Int()) + if config.NonceTTL < 1 || config.NonceTTL > 1800 { + config.NonceTTL = 900 + } + + config.Redis.client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{ + FQDN: serviceName, + Port: servicePort, + }) + + config.NonceMinLen = int(json.Get("nonce_min_length").Int()) + if config.NonceMinLen == 0 { + config.NonceMinLen = 8 + } + + config.NonceMaxLen = int(json.Get("nonce_max_length").Int()) + if config.NonceMaxLen == 0 { + config.NonceMaxLen = 128 + } + + return config.Redis.client.Init(username, password, timeout) +} + +func validateNonce(nonce string, config *ReplayProtectionConfig) error { + if len(nonce) < config.NonceMinLen || len(nonce) > config.NonceMaxLen { + return fmt.Errorf("invalid nonce length: must be between %d and %d", + config.NonceMinLen, config.NonceMaxLen) + } + + if !regexp.MustCompile(`^[a-zA-Z0-9+/=-]+$`).MatchString(nonce) { + return fmt.Errorf("invalid nonce format: must be base64 encoded") + } + + return nil +} + +func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig, log wrapper.Log) types.Action { + nonce, _ := proxywasm.GetHttpRequestHeader("x-apigw-nonce") + if config.ForceNonce && nonce == "" { + // 强制模式下,缺失 nonce 拒绝请求 + log.Warnf("Missing nonce header") + proxywasm.SendHttpResponse(400, nil, []byte("Missing nonce header"), -1) + return types.ActionPause + } + + // 如果没有 nonce,直接放行(非强制模式时) + if nonce == "" { + return types.ActionContinue + } + + if err := validateNonce(nonce, &config); err != nil { + log.Warnf("Invalid nonce: %v", err) + proxywasm.SendHttpResponse(429, nil, []byte("Invalid nonce"), -1) + return types.ActionPause + } + + redisKey := fmt.Sprintf("%s:%s", config.Redis.keyPrefix, nonce) + + // 校验 nonce 是否已存在 + err := config.Redis.client.Get(redisKey, func(response resp.Value) { + if response.Error() != nil { + log.Errorf("Redis error: %v", response.Error()) + proxywasm.ResumeHttpRequest() + } else if response.String() == "" { + // nonce 不存在:存储 nonce 并设置过期时间 + err := config.Redis.client.SetEx(redisKey, "1", config.NonceTTL, func(response resp.Value) { + if response.Error() != nil { + log.Errorf("Redis error: %v", response.Error()) + } + proxywasm.ResumeHttpRequest() + }) + if err != nil { + log.Errorf("Failed to set nonce in Redis: %v", err) + proxywasm.ResumeHttpRequest() + } + } else { + // nonce 已存在:拒绝请求 + log.Warnf("Duplicate nonce detected: %s", nonce) + proxywasm.SendHttpResponse(429, nil, []byte("Request replay detected"), -1) + } + }) + + if err != nil { + log.Errorf("Redis connection failed: %v", err) + proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) + return types.ActionPause + } +} + +func onHttpResponseHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig, log wrapper.Log) types.Action { + nonce, _ := proxywasm.GetHttpRequestHeader("x-apigw-nonce") + if nonce != "" { + proxywasm.AddHttpResponseHeader("x-apigw-nonce", nonce) + } + return types.ActionContinue +} From d894e3d776793857bcdd10854e240d9cca214df1 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Wed, 15 Jan 2025 13:12:46 +0000 Subject: [PATCH 02/16] update --- .../extensions/replay-protection/README.md | 226 ++++-------------- .../extensions/replay-protection/README_EN.md | 33 ++- .../extensions/replay-protection/go.mod | 2 +- .../extensions/replay-protection/main.go | 71 +++--- plugins/wasm-go/pkg/wrapper/redis_wrapper.go | 17 ++ 5 files changed, 136 insertions(+), 213 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md index 7c32499a48..b7aa15a916 100644 --- a/plugins/wasm-go/extensions/replay-protection/README.md +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -1,6 +1,6 @@ --- title: 防重放攻击 -keywords: [higress,nonce-protection] +keywords: [higress,replay-protection] description: 防重放攻击插件配置参考 --- @@ -10,28 +10,33 @@ Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机 ## 功能说明 -- 强制或可选的 nonce 校验 -- 基于 Redis 的 nonce 唯一性验证 -- 可配置的 nonce 有效期 -- nonce 格式和长度校验 +- **强制或可选的 nonce 校验**:可根据配置决定是否强制要求请求携带 nonce 值。 +- **基于 Redis 的 nonce 唯一性验证**:通过 Redis 存储和校验 nonce 值,确保其唯一性。 +- **可配置的 nonce 有效期**:支持设置 nonce 的有效期,过期后自动失效。 +- **nonce 格式和长度校验**:支持对 nonce 值的格式(Base64)和长度进行验证。 +- **自定义错误响应**:支持配置拒绝请求时的状态码和错误信息。 +- **可自定义 nonce 请求头**:可以自定义携带 nonce 的请求头名称。 ## 配置说明 -| 配置项 | 类型 | 必填 | 默认值 | 说明 | -|-------------------|------|------|--------|-----| -| force_nonce | bool | 否 | true | 是否强制要求 nonce | -| nonce_ttl | int | 否 | 900 | nonce 有效期(单位:秒) | -| nonce_min_length | int | 否 | 8 | nonce 最小长度 | -| nonce_max_length | int | 否 | 128 | nonce 最大长度 | -| reject_code | int | 否 | 429 | 拒绝请求时的状态码 | -| reject_msg | string | 否 | "Duplicate nonce" | 拒绝请求时的错误信息 | -| redis.serviceName | string | 是 | - | Redis 服务名称 | -| redis.servicePort | int | 否 | 6379 | Redis 服务端口 | -| redis.timeout | int | 否 | 1000 | Redis 操作超时时间(毫秒) | -| redis.keyPrefix | string | 否 | "replay-protection" | Redis key 前缀 | +| 配置项 | 类型 | 必填 | 默认值 | 说明 | +|-------------------|--------|------|-----------------|---------------------------------| +| `force_nonce` | bool | 否 | `true` | 是否强制要求请求携带 nonce 值。 | +| `nonce_header` | string | 否 | `X-Mse-Nonce` | 指定携带 nonce 值的请求头名称。 | +| `nonce_ttl` | int | 否 | `900` | nonce 的有效期(单位:秒)。 | +| `nonce_min_length`| int | 否 | `8` | nonce 值的最小长度。 | +| `nonce_max_length`| int | 否 | `128` | nonce 值的最大长度。 | +| `reject_code` | int | 否 | `429` | 拒绝请求时返回的状态码。 | +| `reject_msg` | string | 否 | `"Duplicate nonce"` | 拒绝请求时返回的错误信息。 | +| `redis.serviceName` | string | 是 | 无 | Redis 服务名称,用于存储 nonce 值。 | +| `redis.servicePort` | int | 否 | `6379` | Redis 服务端口。 | +| `redis.timeout` | int | 否 | `1000` | Redis 操作超时时间(单位:毫秒)。 | +| `redis.keyPrefix` | string | 否 | `"replay-protection"` | Redis 键前缀,用于区分不同的 nonce 键。| ## 配置示例 +以下是一个防重放攻击插件的完整配置示例: + ```yaml apiVersion: extensions.higress.io/v1alpha1 kind: WasmPlugin @@ -41,14 +46,17 @@ metadata: spec: defaultConfig: force_nonce: true - nonce_ttl: 900 - nonce_min_length: 8 - nonce_max_length: 128 + nonce_header: "X-Mse-Nonce" # 指定 nonce 请求头名称 + nonce_ttl: 900 # nonce 有效期设置为 900 秒 + nonce_min_length: 8 # nonce 最小长度 + nonce_max_length: 128 # nonce 最大长度 + reject_code: 429 # 拒绝请求时返回的状态码 + reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 redis: - serviceName: "redis.higress" - servicePort: 6379 - timeout: 1000 - keyPrefix: "replay-protection" + serviceName: "redis.higress" # Redis 服务名称 + servicePort: 6379 # Redis 服务端口 + timeout: 1000 # Redis 操作超时时间 + keyPrefix: "replay-protection" # Redis 键前缀 url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 ``` @@ -56,168 +64,40 @@ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 ### 请求头要求 -| 请求头 | 是否必须 | 说明 | -|-------|---------|------| -| x-apigw-nonce | 由 force_nonce 配置决定 | 随机生成的 nonce 值,需符合 base64 编码格式 | +| 请求头名称 | 是否必须 | 说明 | +|-----------------|----------------|------------------------------------------| +| `X-Mse-Nonce` | 根据 `force_nonce` 配置决定 | 请求中携带的随机生成的 nonce 值,需符合 Base64 格式。 | +> **注意**:可以通过 `nonce_header` 配置自定义请求头名称,默认值为 `X-Mse-Nonce`。 -### 1. 测试环境配置 - -```yaml -# test-ingress.yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: test-api - namespace: default -spec: - rules: - - host: test.example.com - http: - paths: - - path: /api/test - pathType: Prefix - backend: - service: - name: test-service - port: - number: 8080 ---- -# test-wasmplugin.yaml -apiVersion: extensions.higress.io/v1alpha1 -kind: WasmPlugin -metadata: - name: replay-protection - namespace: higress-system -spec: - defaultConfig: - force_nonce: true - nonce_ttl: 60 # 测试时设置短一点,比如60秒 - nonce_min_length: 8 - nonce_max_length: 128 - redis: - serviceName: "redis.higress" # 确保这个 Redis 服务可用 - servicePort: 6379 - timeout: 1000 - keyPrefix: "test-replay-protection" - matchRules: - - ingress: - - default/test-api # 匹配我们的测试 Ingress -url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 -``` - -### 2. 测试脚本 +### 使用示例 ```bash -#!/bin/bash - -# 测试 API 地址 -API_URL="http://test.example.com/api/test" - -# 测试用例1: 正常请求 -test_normal_request() { - echo "测试用例1: 正常请求" - nonce=$(openssl rand -base64 32) - echo "使用 nonce: $nonce" - - curl -X POST "$API_URL" \ - -H "x-apigw-nonce: $nonce" \ - -H "Host: test.example.com" \ - -d '{"test": "data"}' - echo -e "\n" -} - -# 测试用例2: 重放攻击 -test_replay_attack() { - echo "测试用例2: 重放攻击" - nonce=$(openssl rand -base64 32) - echo "使用 nonce: $nonce" - - # 第一次请求 - echo "第一次请求:" - curl -X POST "$API_URL" \ - -H "x-apigw-nonce: $nonce" \ - -H "Host: test.example.com" \ - -d '{"test": "data"}' - echo -e "\n" - - # 重放请求 - echo "重放请求:" - curl -X POST "$API_URL" \ - -H "x-apigw-nonce: $nonce" \ - -H "Host: test.example.com" \ - -d '{"test": "data"}' - echo -e "\n" -} +# Generate nonce +nonce=$(openssl rand -base64 32) -# 测试用例3: 无 nonce -test_without_nonce() { - echo "测试用例3: 无 nonce" - curl -X POST "$API_URL" \ - -H "Host: test.example.com" \ - -d '{"test": "data"}' - echo -e "\n" -} - -# 测试用例4: nonce 太短 -test_short_nonce() { - echo "测试用例4: nonce 太短" - curl -X POST "$API_URL" \ - -H "x-apigw-nonce: abc" \ - -H "Host: test.example.com" \ - -d '{"test": "data"}' - echo -e "\n" -} - -# 运行所有测试 -run_all_tests() { - test_normal_request - sleep 2 - test_replay_attack - sleep 2 - test_without_nonce - sleep 2 - test_short_nonce -} - -# 执行测试 -run_all_tests +# Send request +curl -X POST 'https://api.example.com/path' \ + -H "X-Mse-Nonce: $nonce" \ + -d '{"key": "value"}' ``` +## 返回结果 -### 3. 预期结果 - -1. **正常请求**: ```json { - "success": true, - "data": "..." + "code": 429, + "message": "Duplicate nonce detected" } ``` -2. **重放攻击**: -```json -{ - "code": 429, - "message": "Request replay detected" -} -``` - -3. **无 nonce**: -```json -{ - "code": 400, - "message": "Missing nonce header" -} -``` - -4. **nonce 太短**: -```json -{ - "code": 400, - "message": "Invalid nonce length" -} -``` +## 错误响应示例 +| 错误场景 | 状态码 | 错误信息 | +|------------------------|-------|--------------------| +| 缺少 nonce 请求头 | `400` | `Missing nonce header` | +| nonce 长度不符合要求 | `400` | `Invalid nonce length` | +| nonce 格式不符合 Base64 | `400` | `Invalid nonce format` | +| nonce 已被使用(重放攻击) | `429` | `Duplicate nonce` | diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md index e83ce68ac3..e5de973b72 100644 --- a/plugins/wasm-go/extensions/replay-protection/README_EN.md +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -23,17 +23,16 @@ The Nonce (Number used ONCE) replay protection plugin prevents request replay at |-----------|------|----------|---------|-------------| | force_nonce | bool | No | true | Whether to enforce nonce requirement | | nonce_ttl | int | No | 900 | Nonce validity period (seconds) | +| nonce_header | string | No | X-Mse-Nonce | Request header name for the nonce | +| nonce_ttl | int | No | 900 | Nonce validity period (seconds) | | nonce_min_length | int | No | 8 | Minimum nonce length | | nonce_max_length | int | No | 128 | Maximum nonce length | - -### Redis Configuration - -| Parameter | Type | Required | Default | Description | -|-----------|------|----------|---------|-------------| -| serviceName | string | Yes | - | Redis service name | -| servicePort | int | No | 6379 | Redis service port | -| timeout | int | No | 1000 | Redis operation timeout (ms) | -| keyPrefix | string | No | "replay-protection" | Redis key prefix | +| reject_code | int | No | 429 | error code when request rejected | +| reject_msg | string | No | "Duplicate nonce" | error massage when request rejected | +| redis.serviceName | string | Yes | - | Redis service name | +| sredis.ervicePort | int | No | 6379 | Redis service port | +| redis.timeout | int | No | 1000 | Redis operation timeout (ms) | +| redis.keyPrefix | string | No | "replay-protection" | Redis key prefix | ## Configuration Example @@ -47,8 +46,11 @@ spec: defaultConfig: force_nonce: true nonce_ttl: 900 + nonce_header:"" nonce_min_length: 8 nonce_max_length: 128 + reject_code: 429 + reject_msg: "Duplicate nonce" redis: serviceName: "redis.higress" servicePort: 6379 @@ -63,7 +65,9 @@ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 | Header | Required | Description | |--------|----------|-------------| -| x-apigw-nonce | Depends on force_nonce | Random generated nonce value in base64 format | +| `X-Mse-Nonce` | Depends on force_nonce | Random generated nonce value in base64 format | + +>Note: The default nonce header is X-Mse-Nonce. You can customize it using the nonce_header configuration. ### Usage Example @@ -85,4 +89,11 @@ curl -X POST 'https://api.example.com/path' \ "message": "Duplicate nonce detected" } ``` - +## Error Response Examples + +| Error Scenario | Status Code | Error Message | +|-----------------------------|-------------|-----------------------------| +| Missing nonce header | `400` | `Missing nonce header` | +| Nonce length not valid | `400` | `Invalid nonce length` | +| Nonce not Base64-encoded | `400` | `Invalid nonce format` | +| Duplicate nonce (replay attack) | `429` | `Duplicate nonce` | diff --git a/plugins/wasm-go/extensions/replay-protection/go.mod b/plugins/wasm-go/extensions/replay-protection/go.mod index fc6ae8115e..a7a890a76d 100644 --- a/plugins/wasm-go/extensions/replay-protection/go.mod +++ b/plugins/wasm-go/extensions/replay-protection/go.mod @@ -8,6 +8,7 @@ require ( github.com/alibaba/higress/plugins/wasm-go v1.4.2 github.com/higress-group/proxy-wasm-go-sdk v1.0.0 github.com/tidwall/gjson v1.18.0 + github.com/tidwall/resp v0.1.1 ) require ( @@ -16,5 +17,4 @@ require ( github.com/magefile/mage v1.14.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - github.com/tidwall/resp v0.1.1 // indirect ) diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index a388703b62..d2fe8dc2c9 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "regexp" + "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm" "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types" @@ -14,7 +16,6 @@ func main() { "replay-protection", wrapper.ParseConfigBy(parseConfig), wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), - wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders), ) } @@ -22,8 +23,11 @@ type ReplayProtectionConfig struct { ForceNonce bool // 是否启用强制 nonce 校验 NonceTTL int // Nonce 的过期时间(单位:秒) Redis RedisConfig - NonceMinLen int // nonce 最小长度 - NonceMaxLen int // nonce 最大长度 + NonceMinLen int // nonce 最小长度 + NonceMaxLen int // nonce 最大长度 + NonceHeader string //nonce头部 + RejectCode uint32 //状态码 + RejectMsg string //响应体 } type RedisConfig struct { @@ -37,6 +41,21 @@ func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper. return fmt.Errorf("missing redis config") } + config.NonceHeader = json.Get("nonce_header").String() + if config.NonceHeader == "" { + config.NonceHeader = "X-Mse-Nonce" + } + + config.RejectCode = uint32(json.Get("reject_code").Int()) + if config.RejectCode == 0 { + config.RejectCode = 429 + } + + config.RejectMsg = json.Get("reject_msg").String() + if config.RejectMsg == "" { + config.RejectMsg = "Duplicate nonce" + } + serviceName := redisConfig.Get("serviceName").String() if serviceName == "" { return fmt.Errorf("redis service name is required") @@ -81,7 +100,12 @@ func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper. config.NonceMaxLen = 128 } - return config.Redis.client.Init(username, password, timeout) + err := config.Redis.client.Init(username, password, timeout) + if err != nil { + log.Errorf("Failed to initialize Redis client: %v", err) + return fmt.Errorf("Redis initialization error: %w", err) + } + return nil } func validateNonce(nonce string, config *ReplayProtectionConfig) error { @@ -98,7 +122,7 @@ func validateNonce(nonce string, config *ReplayProtectionConfig) error { } func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig, log wrapper.Log) types.Action { - nonce, _ := proxywasm.GetHttpRequestHeader("x-apigw-nonce") + nonce, _ := proxywasm.GetHttpRequestHeader(config.NonceHeader) if config.ForceNonce && nonce == "" { // 强制模式下,缺失 nonce 拒绝请求 log.Warnf("Missing nonce header") @@ -113,33 +137,31 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig if err := validateNonce(nonce, &config); err != nil { log.Warnf("Invalid nonce: %v", err) - proxywasm.SendHttpResponse(429, nil, []byte("Invalid nonce"), -1) + proxywasm.SendHttpResponse(400, nil, []byte("Invalid nonce"), -1) return types.ActionPause } redisKey := fmt.Sprintf("%s:%s", config.Redis.keyPrefix, nonce) // 校验 nonce 是否已存在 - err := config.Redis.client.Get(redisKey, func(response resp.Value) { + err := config.Redis.client.SetNX(redisKey, "1", config.NonceTTL, func(response resp.Value) { if response.Error() != nil { log.Errorf("Redis error: %v", response.Error()) + proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) + return + } else if response.Integer() == 1 { + // SETNX 成功,请求通过 proxywasm.ResumeHttpRequest() - } else if response.String() == "" { - // nonce 不存在:存储 nonce 并设置过期时间 - err := config.Redis.client.SetEx(redisKey, "1", config.NonceTTL, func(response resp.Value) { - if response.Error() != nil { - log.Errorf("Redis error: %v", response.Error()) - } - proxywasm.ResumeHttpRequest() - }) - if err != nil { - log.Errorf("Failed to set nonce in Redis: %v", err) - proxywasm.ResumeHttpRequest() - } + return } else { - // nonce 已存在:拒绝请求 + // nonce 已存在,拒绝请求 log.Warnf("Duplicate nonce detected: %s", nonce) - proxywasm.SendHttpResponse(429, nil, []byte("Request replay detected"), -1) + proxywasm.SendHttpResponse( + config.RejectCode, + nil, + []byte(fmt.Sprintf("%s: %s", config.RejectMsg, nonce)), + -1, + ) } }) @@ -148,12 +170,5 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) return types.ActionPause } -} - -func onHttpResponseHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig, log wrapper.Log) types.Action { - nonce, _ := proxywasm.GetHttpRequestHeader("x-apigw-nonce") - if nonce != "" { - proxywasm.AddHttpResponseHeader("x-apigw-nonce", nonce) - } return types.ActionContinue } diff --git a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go index f4b42e67e7..9299d12d09 100644 --- a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go +++ b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go @@ -44,6 +44,7 @@ type RedisClient interface { Get(key string, callback RedisResponseCallback) error Set(key string, value interface{}, callback RedisResponseCallback) error SetEx(key string, value interface{}, ttl int, callback RedisResponseCallback) error + SetNX(key string, value interface{}, expiration int, callback func(response resp.Value)) error MGet(keys []string, callback RedisResponseCallback) error MSet(kvMap map[string]interface{}, callback RedisResponseCallback) error Incr(key string, callback RedisResponseCallback) error @@ -308,6 +309,22 @@ func (c *RedisClusterClient[C]) SetEx(key string, value interface{}, ttl int, ca return RedisCall(c.cluster, respString(args), callback) } +func (c *RedisClusterClient[C]) SetNX(key string, value interface{}, expiration int, callback func(response resp.Value)) error { + if err := c.checkReadyFunc(); err != nil { + return err + } + args := make([]interface{}, 0) + args = append(args, "set") + args = append(args, key) + args = append(args, value) + if expiration > 0 { + args = append(args, "ex") + args = append(args, expiration) + } + args = append(args, "nx") + return RedisCall(c.cluster, respString(args), callback) +} + func (c *RedisClusterClient[C]) MGet(keys []string, callback RedisResponseCallback) error { if err := c.checkReadyFunc(); err != nil { return err From d7cec7f67ba4b459a55db850b17f9b3f903af0ad Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Thu, 16 Jan 2025 05:31:43 +0000 Subject: [PATCH 03/16] update --- .../wasm-go/extensions/replay-protection/README.md | 2 ++ .../extensions/replay-protection/README_EN.md | 4 +++- plugins/wasm-go/extensions/replay-protection/main.go | 12 ++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md index b7aa15a916..88b049cb9b 100644 --- a/plugins/wasm-go/extensions/replay-protection/README.md +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -28,6 +28,7 @@ Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机 | `nonce_max_length`| int | 否 | `128` | nonce 值的最大长度。 | | `reject_code` | int | 否 | `429` | 拒绝请求时返回的状态码。 | | `reject_msg` | string | 否 | `"Duplicate nonce"` | 拒绝请求时返回的错误信息。 | +| validate_base64 | bool | 否 | false | 是否校验 nonce 的 base64 编码格式 | | `redis.serviceName` | string | 是 | 无 | Redis 服务名称,用于存储 nonce 值。 | | `redis.servicePort` | int | 否 | `6379` | Redis 服务端口。 | | `redis.timeout` | int | 否 | `1000` | Redis 操作超时时间(单位:毫秒)。 | @@ -50,6 +51,7 @@ spec: nonce_ttl: 900 # nonce 有效期设置为 900 秒 nonce_min_length: 8 # nonce 最小长度 nonce_max_length: 128 # nonce 最大长度 + base64_validate: true # 是否开启base64格式校验 reject_code: 429 # 拒绝请求时返回的状态码 reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 redis: diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md index e5de973b72..65eccbe227 100644 --- a/plugins/wasm-go/extensions/replay-protection/README_EN.md +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -29,8 +29,9 @@ The Nonce (Number used ONCE) replay protection plugin prevents request replay at | nonce_max_length | int | No | 128 | Maximum nonce length | | reject_code | int | No | 429 | error code when request rejected | | reject_msg | string | No | "Duplicate nonce" | error massage when request rejected | +| validate_base64 | bool | No | false | Whether to validate the base64 encoding format of the nonce. | | redis.serviceName | string | Yes | - | Redis service name | -| sredis.ervicePort | int | No | 6379 | Redis service port | +| redis.servicePort | int | No | 6379 | Redis service port | | redis.timeout | int | No | 1000 | Redis operation timeout (ms) | | redis.keyPrefix | string | No | "replay-protection" | Redis key prefix | @@ -49,6 +50,7 @@ spec: nonce_header:"" nonce_min_length: 8 nonce_max_length: 128 + base64_validate: true reject_code: 429 reject_msg: "Duplicate nonce" redis: diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index d2fe8dc2c9..2a6e5d2540 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -26,6 +26,7 @@ type ReplayProtectionConfig struct { NonceMinLen int // nonce 最小长度 NonceMaxLen int // nonce 最大长度 NonceHeader string //nonce头部 + ValidateBase64 bool // 是否校验 base64 编码格式 RejectCode uint32 //状态码 RejectMsg string //响应体 } @@ -45,6 +46,8 @@ func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper. if config.NonceHeader == "" { config.NonceHeader = "X-Mse-Nonce" } + + config.ValidateBase64 = json.Get("validate_base64").Bool() config.RejectCode = uint32(json.Get("reject_code").Int()) if config.RejectCode == 0 { @@ -113,10 +116,11 @@ func validateNonce(nonce string, config *ReplayProtectionConfig) error { return fmt.Errorf("invalid nonce length: must be between %d and %d", config.NonceMinLen, config.NonceMaxLen) } - - if !regexp.MustCompile(`^[a-zA-Z0-9+/=-]+$`).MatchString(nonce) { - return fmt.Errorf("invalid nonce format: must be base64 encoded") - } + if config.ValidateBase64 { + if !regexp.MustCompile(`^[a-zA-Z0-9+/=-]+$`).MatchString(nonce) { + return fmt.Errorf("invalid nonce format: must be base64 encoded") + } + } return nil } From 2762111e72e1ed64c5745d610ab0d9adb657f346 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Thu, 16 Jan 2025 05:43:45 +0000 Subject: [PATCH 04/16] update --- .../extensions/replay-protection/README.md | 4 +- .../extensions/replay-protection/README_EN.md | 2 +- .../extensions/replay-protection/main.go | 28 +-- .../conformance/tests/replay-protection.go | 169 ++++++++++++++++++ .../conformance/tests/replay-protection.yaml | 89 +++++++++ 5 files changed, 275 insertions(+), 17 deletions(-) create mode 100644 test/e2e/conformance/tests/replay-protection.go create mode 100644 test/e2e/conformance/tests/replay-protection.yaml diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md index 88b049cb9b..8c97eee301 100644 --- a/plugins/wasm-go/extensions/replay-protection/README.md +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -28,7 +28,7 @@ Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机 | `nonce_max_length`| int | 否 | `128` | nonce 值的最大长度。 | | `reject_code` | int | 否 | `429` | 拒绝请求时返回的状态码。 | | `reject_msg` | string | 否 | `"Duplicate nonce"` | 拒绝请求时返回的错误信息。 | -| validate_base64 | bool | 否 | false | 是否校验 nonce 的 base64 编码格式 | +| `validate_base64` | bool | 否 | `false` | 是否校验 nonce 的 base64 编码格式 | | `redis.serviceName` | string | 是 | 无 | Redis 服务名称,用于存储 nonce 值。 | | `redis.servicePort` | int | 否 | `6379` | Redis 服务端口。 | | `redis.timeout` | int | 否 | `1000` | Redis 操作超时时间(单位:毫秒)。 | @@ -51,7 +51,7 @@ spec: nonce_ttl: 900 # nonce 有效期设置为 900 秒 nonce_min_length: 8 # nonce 最小长度 nonce_max_length: 128 # nonce 最大长度 - base64_validate: true # 是否开启base64格式校验 + validate_base64: true # 是否开启base64格式校验 reject_code: 429 # 拒绝请求时返回的状态码 reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 redis: diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md index 65eccbe227..2d4d51e205 100644 --- a/plugins/wasm-go/extensions/replay-protection/README_EN.md +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -50,7 +50,7 @@ spec: nonce_header:"" nonce_min_length: 8 nonce_max_length: 128 - base64_validate: true + validate_base64: true reject_code: 429 reject_msg: "Duplicate nonce" redis: diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index 2a6e5d2540..f18e31002b 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -20,15 +20,15 @@ func main() { } type ReplayProtectionConfig struct { - ForceNonce bool // 是否启用强制 nonce 校验 - NonceTTL int // Nonce 的过期时间(单位:秒) - Redis RedisConfig - NonceMinLen int // nonce 最小长度 - NonceMaxLen int // nonce 最大长度 - NonceHeader string //nonce头部 + ForceNonce bool // 是否启用强制 nonce 校验 + NonceTTL int // Nonce 的过期时间(单位:秒) + Redis RedisConfig + NonceMinLen int // nonce 最小长度 + NonceMaxLen int // nonce 最大长度 + NonceHeader string //nonce头部 ValidateBase64 bool // 是否校验 base64 编码格式 - RejectCode uint32 //状态码 - RejectMsg string //响应体 + RejectCode uint32 //状态码 + RejectMsg string //响应体 } type RedisConfig struct { @@ -46,8 +46,8 @@ func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper. if config.NonceHeader == "" { config.NonceHeader = "X-Mse-Nonce" } - - config.ValidateBase64 = json.Get("validate_base64").Bool() + + config.ValidateBase64 = json.Get("validate_base64").Bool() config.RejectCode = uint32(json.Get("reject_code").Int()) if config.RejectCode == 0 { @@ -117,10 +117,10 @@ func validateNonce(nonce string, config *ReplayProtectionConfig) error { config.NonceMinLen, config.NonceMaxLen) } if config.ValidateBase64 { - if !regexp.MustCompile(`^[a-zA-Z0-9+/=-]+$`).MatchString(nonce) { - return fmt.Errorf("invalid nonce format: must be base64 encoded") - } - } + if !regexp.MustCompile(`^[a-zA-Z0-9+/=-]+$`).MatchString(nonce) { + return fmt.Errorf("invalid nonce format: must be base64 encoded") + } + } return nil } diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go new file mode 100644 index 0000000000..ff1fb21c5f --- /dev/null +++ b/test/e2e/conformance/tests/replay-protection.go @@ -0,0 +1,169 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "crypto/rand" + "encoding/base64" + "strings" + "testing" + "time" + + "github.com/alibaba/higress/test/e2e/conformance/utils/http" + "github.com/alibaba/higress/test/e2e/conformance/utils/suite" +) + +func init() { + Register(WasmPluginsReplayProtection) +} + +func generateBase64Nonce(length int) string { + bytes := make([]byte, length) + rand.Read(bytes) + return base64.StdEncoding.EncodeToString(bytes) +} + +var WasmPluginsReplayProtection = suite.ConformanceTest{ + ShortName: "WasmPluginsReplayProtection", + Description: "The replay protection wasm plugin prevents replay attacks by validating request nonce.", + Manifests: []string{"tests/replay-protection.yaml"}, // Path to your YAML + Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + replayNonce := generateBase64Nonce(32) + testcases := []http.Assertion{ + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", // or the correct backend name + TargetNamespace: "higress-conformance-infra", // or the correct namespace + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", // Or your test host + Path: "/get", // Or your test path + UnfollowRedirect: true, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/get", + Host: "foo.com", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, // Missing nonce should return 400 + Body: []byte("Missing nonce header"), + }, + ExpectedResponseNoRequest: false, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/get", + UnfollowRedirect: true, + Headers: map[string]string{ + "X-Mse-Nonce": "invalid-nonce", + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/get", + Host: "foo.com", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 400, // Invalid nonce format should return 400 + Body: []byte("Invalid nonce"), + }, + ExpectedResponseNoRequest: false, + }, + }, + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/get", + UnfollowRedirect: true, + Headers: map[string]string{ + "X-Mse-Nonce": generateBase64Nonce(32), + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/get", + Host: "foo.com", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: false, + }, + }, + + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/get", + UnfollowRedirect: true, + Headers: map[string]string{ + "X-Mse-Nonce": replayNonce, + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/get", + Host: "foo.com", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 429, + Body: []byte("Duplicate nonce"), + }, + ExpectedResponseNoRequest: false, + }, + }, + } + t.Run("WasmPlugins replay-protection", func(t *testing.T) { + for _, testcase := range testcases { + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) + if strings.Contains(string(testcase.Response.ExpectedResponse.Body), "Duplicate nonce") { + time.Sleep(time.Second) + } + } + }) + }, +} diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml new file mode 100644 index 0000000000..19e799a6d2 --- /dev/null +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -0,0 +1,89 @@ +# Copyright (c) 2022 Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Deploy Redis service +apiVersion: apps/v1 +kind: Deployment +metadata: + name: redis + namespace: higress-conformance-infra +spec: + replicas: 1 + selector: + matchLabels: + app: redis + template: + metadata: + labels: + app: redis + spec: + containers: + - name: redis + image: redis:6.2 + ports: + - containerPort: 6379 +--- +apiVersion: v1 +kind: Service +metadata: + name: redis + namespace: higress-conformance-infra +spec: + ports: + - port: 6379 + targetPort: 6379 + selector: + app: redis +--- +# Configure WasmPlugin +apiVersion: extensions.higress.io/v1alpha1 +kind: WasmPlugin +metadata: + name: replay-protection + namespace: higress-conformance-infra +spec: + defaultConfig: + force_nonce: true + nonce_ttl: 10 + nonce_header: "X-Mse-Nonce" + nonce_min_length: 8 + nonce_max_length: 128 + validate_base64: true + reject_code: 429 + reject_msg: "Duplicate nonce" + redis: + serviceName: "redis.higress-conformance-infra" + servicePort: 6379 + timeout: 1000 + keyPrefix: "replay-protection" + url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/replay-protection:1.0.0 +--- +# Configure HTTPRoute +apiVersion: networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: replay-protection + namespace: higress-conformance-infra +spec: + parentRefs: + - name: higress-gateway + namespace: higress-system + rules: + - matches: + - path: + type: PathPrefix + value: /get + backendRefs: + - name: infra-backend-v1 + port: 8080 \ No newline at end of file From 935d1c95061cb1954130eb9301bf63a88547dd99 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Mon, 20 Jan 2025 06:40:16 +0000 Subject: [PATCH 05/16] update --- .../extensions/replay-protection/README.md | 12 ++++----- .../extensions/replay-protection/README_EN.md | 10 +++---- .../extensions/replay-protection/VERSION | 2 +- .../extensions/replay-protection/main.go | 26 +++++++++---------- .../conformance/tests/replay-protection.go | 6 ++--- .../conformance/tests/replay-protection.yaml | 7 +++-- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md index 8c97eee301..c5753f0e7a 100644 --- a/plugins/wasm-go/extensions/replay-protection/README.md +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -22,7 +22,7 @@ Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机 | 配置项 | 类型 | 必填 | 默认值 | 说明 | |-------------------|--------|------|-----------------|---------------------------------| | `force_nonce` | bool | 否 | `true` | 是否强制要求请求携带 nonce 值。 | -| `nonce_header` | string | 否 | `X-Mse-Nonce` | 指定携带 nonce 值的请求头名称。 | +| `nonce_header` | string | 否 | `X-Higress-Nonce` | 指定携带 nonce 值的请求头名称。 | | `nonce_ttl` | int | 否 | `900` | nonce 的有效期(单位:秒)。 | | `nonce_min_length`| int | 否 | `8` | nonce 值的最小长度。 | | `nonce_max_length`| int | 否 | `128` | nonce 值的最大长度。 | @@ -47,7 +47,7 @@ metadata: spec: defaultConfig: force_nonce: true - nonce_header: "X-Mse-Nonce" # 指定 nonce 请求头名称 + nonce_header: "X-Higress-Nonce" # 指定 nonce 请求头名称 nonce_ttl: 900 # nonce 有效期设置为 900 秒 nonce_min_length: 8 # nonce 最小长度 nonce_max_length: 128 # nonce 最大长度 @@ -55,11 +55,11 @@ spec: reject_code: 429 # 拒绝请求时返回的状态码 reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 redis: - serviceName: "redis.higress" # Redis 服务名称 + serviceName: "redis.dns" # Redis 服务名称 servicePort: 6379 # Redis 服务端口 timeout: 1000 # Redis 操作超时时间 keyPrefix: "replay-protection" # Redis 键前缀 -url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 + url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm ``` ## 使用说明 @@ -68,7 +68,7 @@ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 | 请求头名称 | 是否必须 | 说明 | |-----------------|----------------|------------------------------------------| -| `X-Mse-Nonce` | 根据 `force_nonce` 配置决定 | 请求中携带的随机生成的 nonce 值,需符合 Base64 格式。 | +| `X-Higress-Nonce` | 根据 `force_nonce` 配置决定 | 请求中携带的随机生成的 nonce 值,需符合 Base64 格式。 | > **注意**:可以通过 `nonce_header` 配置自定义请求头名称,默认值为 `X-Mse-Nonce`。 @@ -80,7 +80,7 @@ nonce=$(openssl rand -base64 32) # Send request curl -X POST 'https://api.example.com/path' \ - -H "X-Mse-Nonce: $nonce" \ + -H "X-Higress-Nonce: $nonce" \ -d '{"key": "value"}' ``` diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md index 2d4d51e205..83bfa6639d 100644 --- a/plugins/wasm-go/extensions/replay-protection/README_EN.md +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -23,7 +23,7 @@ The Nonce (Number used ONCE) replay protection plugin prevents request replay at |-----------|------|----------|---------|-------------| | force_nonce | bool | No | true | Whether to enforce nonce requirement | | nonce_ttl | int | No | 900 | Nonce validity period (seconds) | -| nonce_header | string | No | X-Mse-Nonce | Request header name for the nonce | +| nonce_header | string | No | X-Higress-Nonce | Request header name for the nonce | | nonce_ttl | int | No | 900 | Nonce validity period (seconds) | | nonce_min_length | int | No | 8 | Minimum nonce length | | nonce_max_length | int | No | 128 | Maximum nonce length | @@ -54,11 +54,11 @@ spec: reject_code: 429 reject_msg: "Duplicate nonce" redis: - serviceName: "redis.higress" + serviceName: "redis.dns" servicePort: 6379 timeout: 1000 keyPrefix: "replay-protection" -url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 + url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm ``` ## Usage @@ -67,7 +67,7 @@ url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/replay-protection:v1.0.0 | Header | Required | Description | |--------|----------|-------------| -| `X-Mse-Nonce` | Depends on force_nonce | Random generated nonce value in base64 format | +| `X-Higress-Nonce` | Depends on force_nonce | Random generated nonce value in base64 format | >Note: The default nonce header is X-Mse-Nonce. You can customize it using the nonce_header configuration. @@ -79,7 +79,7 @@ nonce=$(openssl rand -base64 32) # Send request curl -X POST 'https://api.example.com/path' \ - -H "x-apigw-nonce: $nonce" \ + -H "x-Higress-nonce: $nonce" \ -d '{"key": "value"}' ``` diff --git a/plugins/wasm-go/extensions/replay-protection/VERSION b/plugins/wasm-go/extensions/replay-protection/VERSION index afaf360d37..be0aef5602 100644 --- a/plugins/wasm-go/extensions/replay-protection/VERSION +++ b/plugins/wasm-go/extensions/replay-protection/VERSION @@ -1 +1 @@ -1.0.0 \ No newline at end of file +1.0.0-alpha \ No newline at end of file diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index f18e31002b..26ac21b08b 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -20,15 +20,15 @@ func main() { } type ReplayProtectionConfig struct { - ForceNonce bool // 是否启用强制 nonce 校验 - NonceTTL int // Nonce 的过期时间(单位:秒) + ForceNonce bool // Whether to enforce nonce verification + NonceTTL int // Expiration time of the nonce (in seconds) Redis RedisConfig - NonceMinLen int // nonce 最小长度 - NonceMaxLen int // nonce 最大长度 - NonceHeader string //nonce头部 - ValidateBase64 bool // 是否校验 base64 编码格式 - RejectCode uint32 //状态码 - RejectMsg string //响应体 + NonceMinLen int // Minimum length of the nonce + NonceMaxLen int // Maximum length of the nonce + NonceHeader string // Name of the nonce heade + ValidateBase64 bool // Whether to validate base64 encoding format + RejectCode uint32 // Response code + RejectMsg string // Response body } type RedisConfig struct { @@ -128,13 +128,13 @@ func validateNonce(nonce string, config *ReplayProtectionConfig) error { func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig, log wrapper.Log) types.Action { nonce, _ := proxywasm.GetHttpRequestHeader(config.NonceHeader) if config.ForceNonce && nonce == "" { - // 强制模式下,缺失 nonce 拒绝请求 + // In force mode, reject the request if the nonce header is missing log.Warnf("Missing nonce header") proxywasm.SendHttpResponse(400, nil, []byte("Missing nonce header"), -1) return types.ActionPause } - // 如果没有 nonce,直接放行(非强制模式时) + // If there is no nonce, pass through directly (when not in force mode) if nonce == "" { return types.ActionContinue } @@ -147,18 +147,18 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig redisKey := fmt.Sprintf("%s:%s", config.Redis.keyPrefix, nonce) - // 校验 nonce 是否已存在 + // Check if the nonce already exists err := config.Redis.client.SetNX(redisKey, "1", config.NonceTTL, func(response resp.Value) { if response.Error() != nil { log.Errorf("Redis error: %v", response.Error()) proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) return } else if response.Integer() == 1 { - // SETNX 成功,请求通过 + // SETNX successful, pass the request proxywasm.ResumeHttpRequest() return } else { - // nonce 已存在,拒绝请求 + // Nonce already exists, reject the request log.Warnf("Duplicate nonce detected: %s", nonce) proxywasm.SendHttpResponse( config.RejectCode, diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go index ff1fb21c5f..59ffbb307c 100644 --- a/test/e2e/conformance/tests/replay-protection.go +++ b/test/e2e/conformance/tests/replay-protection.go @@ -80,7 +80,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Path: "/get", UnfollowRedirect: true, Headers: map[string]string{ - "X-Mse-Nonce": "invalid-nonce", + "X-Higress-Nonce": "invalid-nonce", }, }, ExpectedRequest: &http.ExpectedRequest{ @@ -109,7 +109,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Path: "/get", UnfollowRedirect: true, Headers: map[string]string{ - "X-Mse-Nonce": generateBase64Nonce(32), + "X-Higress-Nonce": generateBase64Nonce(32), }, }, ExpectedRequest: &http.ExpectedRequest{ @@ -138,7 +138,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Path: "/get", UnfollowRedirect: true, Headers: map[string]string{ - "X-Mse-Nonce": replayNonce, + "X-Higress-Nonce": replayNonce, }, }, ExpectedRequest: &http.ExpectedRequest{ diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml index 19e799a6d2..ae474b39fc 100644 --- a/test/e2e/conformance/tests/replay-protection.yaml +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -56,7 +56,7 @@ spec: defaultConfig: force_nonce: true nonce_ttl: 10 - nonce_header: "X-Mse-Nonce" + nonce_header: "X-Higress-Nonce" nonce_min_length: 8 nonce_max_length: 128 validate_base64: true @@ -67,7 +67,7 @@ spec: servicePort: 6379 timeout: 1000 keyPrefix: "replay-protection" - url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/replay-protection:1.0.0 + url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm --- # Configure HTTPRoute apiVersion: networking.k8s.io/v1 @@ -81,6 +81,9 @@ spec: namespace: higress-system rules: - matches: + - headers: + - name: host + exact: foo.com - path: type: PathPrefix value: /get From 1cfd691f7d4e108e3cc45f43be18df1c0bb01efd Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Tue, 21 Jan 2025 15:14:39 +0000 Subject: [PATCH 06/16] update --- .../extensions/replay-protection/go.mod | 2 +- .../conformance/tests/replay-protection.go | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/go.mod b/plugins/wasm-go/extensions/replay-protection/go.mod index a7a890a76d..f96c0ec763 100644 --- a/plugins/wasm-go/extensions/replay-protection/go.mod +++ b/plugins/wasm-go/extensions/replay-protection/go.mod @@ -1,6 +1,6 @@ module github.com/alibaba/higress/plugins/wasm-go/extensions/replay-protection -go 1.21.11 +go 1.23 replace github.com/alibaba/higress/plugins/wasm-go => ../.. diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go index 59ffbb307c..1323d7df6b 100644 --- a/test/e2e/conformance/tests/replay-protection.go +++ b/test/e2e/conformance/tests/replay-protection.go @@ -126,7 +126,34 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ ExpectedResponseNoRequest: false, }, }, - + { + Meta: http.AssertionMeta{ + TargetBackend: "infra-backend-v1", + TargetNamespace: "higress-conformance-infra", + }, + Request: http.AssertionRequest{ + ActualRequest: http.Request{ + Host: "foo.com", + Path: "/get", + UnfollowRedirect: true, + Headers: map[string]string{ + "X-Higress-Nonce": replayNonce, + }, + }, + ExpectedRequest: &http.ExpectedRequest{ + Request: http.Request{ + Path: "/get", + Host: "foo.com", + }, + }, + }, + Response: http.AssertionResponse{ + ExpectedResponse: http.Response{ + StatusCode: 200, + }, + ExpectedResponseNoRequest: false, + }, + }, { Meta: http.AssertionMeta{ TargetBackend: "infra-backend-v1", From b694c480d6cb4f3a629ab7a5982f9a3ee5507436 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Tue, 21 Jan 2025 15:43:55 +0000 Subject: [PATCH 07/16] update --- plugins/wasm-go/extensions/replay-protection/go.mod | 2 +- plugins/wasm-go/extensions/replay-protection/go.sum | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/go.mod b/plugins/wasm-go/extensions/replay-protection/go.mod index f96c0ec763..5cac2ed497 100644 --- a/plugins/wasm-go/extensions/replay-protection/go.mod +++ b/plugins/wasm-go/extensions/replay-protection/go.mod @@ -1,6 +1,6 @@ module github.com/alibaba/higress/plugins/wasm-go/extensions/replay-protection -go 1.23 +go 1.19 replace github.com/alibaba/higress/plugins/wasm-go => ../.. diff --git a/plugins/wasm-go/extensions/replay-protection/go.sum b/plugins/wasm-go/extensions/replay-protection/go.sum index abe6578d09..ac4aef5888 100644 --- a/plugins/wasm-go/extensions/replay-protection/go.sum +++ b/plugins/wasm-go/extensions/replay-protection/go.sum @@ -1,5 +1,4 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA= @@ -9,9 +8,7 @@ github.com/higress-group/proxy-wasm-go-sdk v1.0.0/go.mod h1:iiSyFbo+rAtbtGt/bsef 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= @@ -21,4 +18,3 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE= github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 50c088dcc45949300752f2a1c1105fb6b695b6af Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Fri, 24 Jan 2025 08:24:06 +0000 Subject: [PATCH 08/16] fix --- test/e2e/conformance/tests/replay-protection.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml index ae474b39fc..a7059cd4cb 100644 --- a/test/e2e/conformance/tests/replay-protection.yaml +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -81,7 +81,7 @@ spec: namespace: higress-system rules: - matches: - - headers: + - headers: - name: host exact: foo.com - path: From ed33204d6ef4deac450ad46474e259e389f75fa0 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Fri, 24 Jan 2025 10:25:04 +0000 Subject: [PATCH 09/16] fix --- .../conformance/tests/replay-protection.yaml | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml index a7059cd4cb..fea1c45c20 100644 --- a/test/e2e/conformance/tests/replay-protection.yaml +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -69,24 +69,21 @@ spec: keyPrefix: "replay-protection" url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm --- -# Configure HTTPRoute apiVersion: networking.k8s.io/v1 -kind: HTTPRoute +kind: Ingress metadata: name: replay-protection namespace: higress-conformance-infra + annotations: spec: - parentRefs: - - name: higress-gateway - namespace: higress-system rules: - - matches: - - headers: - - name: host - exact: foo.com - - path: - type: PathPrefix - value: /get - backendRefs: - - name: infra-backend-v1 - port: 8080 \ No newline at end of file + - host: foo.com + http: + paths: + - path: /get + pathType: Prefix + backend: + service: + name: infra-backend-v1 + port: + number: 8080 \ No newline at end of file From 88504b094b44031371cb31b66a71161cf9349b82 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Thu, 30 Jan 2025 07:34:45 +0000 Subject: [PATCH 10/16] fix --- .../extensions/replay-protection/README_EN.md | 2 +- .../extensions/replay-protection/main.go | 2 +- .../conformance/tests/replay-protection.go | 97 ++++--------------- 3 files changed, 19 insertions(+), 82 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md index 83bfa6639d..3e36d12fc2 100644 --- a/plugins/wasm-go/extensions/replay-protection/README_EN.md +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -69,7 +69,7 @@ spec: |--------|----------|-------------| | `X-Higress-Nonce` | Depends on force_nonce | Random generated nonce value in base64 format | ->Note: The default nonce header is X-Mse-Nonce. You can customize it using the nonce_header configuration. +>Note: The default nonce header is X-Higress-Nonce. You can customize it using the nonce_header configuration. ### Usage Example diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index 26ac21b08b..6c9f3584ca 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -44,7 +44,7 @@ func parseConfig(json gjson.Result, config *ReplayProtectionConfig, log wrapper. config.NonceHeader = json.Get("nonce_header").String() if config.NonceHeader == "" { - config.NonceHeader = "X-Mse-Nonce" + config.NonceHeader = "X-Higress-Nonce" } config.ValidateBase64 = json.Get("validate_base64").Bool() diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go index 1323d7df6b..aae97d6e17 100644 --- a/test/e2e/conformance/tests/replay-protection.go +++ b/test/e2e/conformance/tests/replay-protection.go @@ -17,9 +17,7 @@ package tests import ( "crypto/rand" "encoding/base64" - "strings" "testing" - "time" "github.com/alibaba/higress/test/e2e/conformance/utils/http" "github.com/alibaba/higress/test/e2e/conformance/utils/suite" @@ -38,37 +36,11 @@ func generateBase64Nonce(length int) string { var WasmPluginsReplayProtection = suite.ConformanceTest{ ShortName: "WasmPluginsReplayProtection", Description: "The replay protection wasm plugin prevents replay attacks by validating request nonce.", - Manifests: []string{"tests/replay-protection.yaml"}, // Path to your YAML + Manifests: []string{"tests/replay-protection.yaml"}, Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { replayNonce := generateBase64Nonce(32) testcases := []http.Assertion{ - { - Meta: http.AssertionMeta{ - TargetBackend: "infra-backend-v1", // or the correct backend name - TargetNamespace: "higress-conformance-infra", // or the correct namespace - }, - Request: http.AssertionRequest{ - ActualRequest: http.Request{ - Host: "foo.com", // Or your test host - Path: "/get", // Or your test path - UnfollowRedirect: true, - }, - ExpectedRequest: &http.ExpectedRequest{ - Request: http.Request{ - Path: "/get", - Host: "foo.com", - }, - }, - }, - Response: http.AssertionResponse{ - ExpectedResponse: http.Response{ - StatusCode: 400, // Missing nonce should return 400 - Body: []byte("Missing nonce header"), - }, - ExpectedResponseNoRequest: false, - }, - }, { Meta: http.AssertionMeta{ TargetBackend: "infra-backend-v1", @@ -76,26 +48,15 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, Request: http.AssertionRequest{ ActualRequest: http.Request{ - Host: "foo.com", - Path: "/get", - UnfollowRedirect: true, - Headers: map[string]string{ - "X-Higress-Nonce": "invalid-nonce", - }, - }, - ExpectedRequest: &http.ExpectedRequest{ - Request: http.Request{ - Path: "/get", - Host: "foo.com", - }, + Host: "foo.com", + Path: "/get", + Method: "GET", }, }, Response: http.AssertionResponse{ ExpectedResponse: http.Response{ - StatusCode: 400, // Invalid nonce format should return 400 - Body: []byte("Invalid nonce"), + StatusCode: 400, }, - ExpectedResponseNoRequest: false, }, }, { @@ -105,25 +66,18 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, Request: http.AssertionRequest{ ActualRequest: http.Request{ - Host: "foo.com", - Path: "/get", - UnfollowRedirect: true, + Host: "foo.com", + Path: "/get", + Method: "GET", Headers: map[string]string{ - "X-Higress-Nonce": generateBase64Nonce(32), - }, - }, - ExpectedRequest: &http.ExpectedRequest{ - Request: http.Request{ - Path: "/get", - Host: "foo.com", + "X-Higress-Nonce": "invalid-nonce", }, }, }, Response: http.AssertionResponse{ ExpectedResponse: http.Response{ - StatusCode: 200, + StatusCode: 400, }, - ExpectedResponseNoRequest: false, }, }, { @@ -133,25 +87,18 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, Request: http.AssertionRequest{ ActualRequest: http.Request{ - Host: "foo.com", - Path: "/get", - UnfollowRedirect: true, + Host: "foo.com", + Path: "/get", + Method: "GET", Headers: map[string]string{ "X-Higress-Nonce": replayNonce, }, }, - ExpectedRequest: &http.ExpectedRequest{ - Request: http.Request{ - Path: "/get", - Host: "foo.com", - }, - }, }, Response: http.AssertionResponse{ ExpectedResponse: http.Response{ StatusCode: 200, }, - ExpectedResponseNoRequest: false, }, }, { @@ -161,35 +108,25 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, Request: http.AssertionRequest{ ActualRequest: http.Request{ - Host: "foo.com", - Path: "/get", - UnfollowRedirect: true, + Host: "foo.com", + Path: "/get", + Method: "GET", Headers: map[string]string{ "X-Higress-Nonce": replayNonce, }, }, - ExpectedRequest: &http.ExpectedRequest{ - Request: http.Request{ - Path: "/get", - Host: "foo.com", - }, - }, }, Response: http.AssertionResponse{ ExpectedResponse: http.Response{ StatusCode: 429, - Body: []byte("Duplicate nonce"), }, - ExpectedResponseNoRequest: false, }, }, } + t.Run("WasmPlugins replay-protection", func(t *testing.T) { for _, testcase := range testcases { http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase) - if strings.Contains(string(testcase.Response.ExpectedResponse.Body), "Duplicate nonce") { - time.Sleep(time.Second) - } } }) }, From 6ee8499039f03818f5d19626e3ab4e00430fb1ba Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Fri, 31 Jan 2025 16:15:43 +0000 Subject: [PATCH 11/16] fix --- test/e2e/conformance/tests/replay-protection.go | 8 ++++---- test/e2e/conformance/tests/replay-protection.yaml | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go index aae97d6e17..72f7db2143 100644 --- a/test/e2e/conformance/tests/replay-protection.go +++ b/test/e2e/conformance/tests/replay-protection.go @@ -49,7 +49,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Request: http.AssertionRequest{ ActualRequest: http.Request{ Host: "foo.com", - Path: "/get", + Path: "/", Method: "GET", }, }, @@ -67,7 +67,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Request: http.AssertionRequest{ ActualRequest: http.Request{ Host: "foo.com", - Path: "/get", + Path: "/", Method: "GET", Headers: map[string]string{ "X-Higress-Nonce": "invalid-nonce", @@ -88,7 +88,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Request: http.AssertionRequest{ ActualRequest: http.Request{ Host: "foo.com", - Path: "/get", + Path: "/", Method: "GET", Headers: map[string]string{ "X-Higress-Nonce": replayNonce, @@ -109,7 +109,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ Request: http.AssertionRequest{ ActualRequest: http.Request{ Host: "foo.com", - Path: "/get", + Path: "/", Method: "GET", Headers: map[string]string{ "X-Higress-Nonce": replayNonce, diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml index fea1c45c20..08c982025d 100644 --- a/test/e2e/conformance/tests/replay-protection.yaml +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -76,12 +76,13 @@ metadata: namespace: higress-conformance-infra annotations: spec: + ingressClassName: higress rules: - - host: foo.com + - host: "foo.com" http: paths: - - path: /get - pathType: Prefix + - pathType: Prefix + path: "/" backend: service: name: infra-backend-v1 From 57072243af64041280a1e36648518be031f7cbe7 Mon Sep 17 00:00:00 2001 From: hanxiantao <601803023@qq.com> Date: Sun, 9 Feb 2025 13:43:29 +0800 Subject: [PATCH 12/16] fix e2e test --- .../conformance/tests/replay-protection.yaml | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml index 08c982025d..3a4bac9336 100644 --- a/test/e2e/conformance/tests/replay-protection.yaml +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -46,12 +46,32 @@ spec: selector: app: redis --- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: replay-protection + namespace: higress-conformance-infra + annotations: +spec: + ingressClassName: higress + rules: + - host: "foo.com" + http: + paths: + - pathType: Prefix + path: "/" + backend: + service: + name: infra-backend-v1 + port: + number: 8080 +--- # Configure WasmPlugin apiVersion: extensions.higress.io/v1alpha1 kind: WasmPlugin metadata: name: replay-protection - namespace: higress-conformance-infra + namespace: higress-system spec: defaultConfig: force_nonce: true @@ -67,24 +87,4 @@ spec: servicePort: 6379 timeout: 1000 keyPrefix: "replay-protection" - url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: replay-protection - namespace: higress-conformance-infra - annotations: -spec: - ingressClassName: higress - rules: - - host: "foo.com" - http: - paths: - - pathType: Prefix - path: "/" - backend: - service: - name: infra-backend-v1 - port: - number: 8080 \ No newline at end of file + url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm \ No newline at end of file From d1775a1ea6530fa4e1cf5e2ff8b0e69f5a2af9fb Mon Sep 17 00:00:00 2001 From: hanxiantao <601803023@qq.com> Date: Sun, 9 Feb 2025 14:06:40 +0800 Subject: [PATCH 13/16] fix e2e test --- test/e2e/conformance/tests/replay-protection.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/conformance/tests/replay-protection.yaml b/test/e2e/conformance/tests/replay-protection.yaml index 3a4bac9336..4f5fcf2fc7 100644 --- a/test/e2e/conformance/tests/replay-protection.yaml +++ b/test/e2e/conformance/tests/replay-protection.yaml @@ -75,7 +75,7 @@ metadata: spec: defaultConfig: force_nonce: true - nonce_ttl: 10 + nonce_ttl: 86400 nonce_header: "X-Higress-Nonce" nonce_min_length: 8 nonce_max_length: 128 @@ -83,7 +83,7 @@ spec: reject_code: 429 reject_msg: "Duplicate nonce" redis: - serviceName: "redis.higress-conformance-infra" + serviceName: "redis.higress-conformance-infra.svc.cluster.local" servicePort: 6379 timeout: 1000 keyPrefix: "replay-protection" From 8792448a2bcac7c27cba3ddba86cd8fdd9c36223 Mon Sep 17 00:00:00 2001 From: hanxiantao <601803023@qq.com> Date: Sun, 9 Feb 2025 14:54:20 +0800 Subject: [PATCH 14/16] add TestCaseName --- .../conformance/tests/replay-protection.go | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go index 72f7db2143..4daa0963ab 100644 --- a/test/e2e/conformance/tests/replay-protection.go +++ b/test/e2e/conformance/tests/replay-protection.go @@ -1,17 +1,3 @@ -// Copyright (c) 2022 Alibaba Group Holding Ltd. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package tests import ( @@ -43,14 +29,15 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ testcases := []http.Assertion{ { Meta: http.AssertionMeta{ + TestCaseName: "Missing nonce header", TargetBackend: "infra-backend-v1", TargetNamespace: "higress-conformance-infra", }, Request: http.AssertionRequest{ ActualRequest: http.Request{ - Host: "foo.com", - Path: "/", - Method: "GET", + Host: "foo.com", + Path: "/", + Method: "GET", }, }, Response: http.AssertionResponse{ @@ -61,6 +48,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, { Meta: http.AssertionMeta{ + TestCaseName: "Invalid nonce not base64 encoded", TargetBackend: "infra-backend-v1", TargetNamespace: "higress-conformance-infra", }, @@ -82,6 +70,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, { Meta: http.AssertionMeta{ + TestCaseName: "First request with unique nonce returns 200", TargetBackend: "infra-backend-v1", TargetNamespace: "higress-conformance-infra", }, @@ -103,6 +92,7 @@ var WasmPluginsReplayProtection = suite.ConformanceTest{ }, { Meta: http.AssertionMeta{ + TestCaseName: "Second request with repeated nonce returns 429", TargetBackend: "infra-backend-v1", TargetNamespace: "higress-conformance-infra", }, From 67d50ea428dee49fceabf159581d236b6efba78a Mon Sep 17 00:00:00 2001 From: hanxiantao <601803023@qq.com> Date: Sun, 9 Feb 2025 14:58:10 +0800 Subject: [PATCH 15/16] add TestCaseName --- test/e2e/conformance/tests/replay-protection.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/e2e/conformance/tests/replay-protection.go b/test/e2e/conformance/tests/replay-protection.go index 4daa0963ab..ff9ac77429 100644 --- a/test/e2e/conformance/tests/replay-protection.go +++ b/test/e2e/conformance/tests/replay-protection.go @@ -1,3 +1,17 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package tests import ( From ab6a397ad6d31bd6502c55ac8fc6cc0e43ec3080 Mon Sep 17 00:00:00 2001 From: yunmaoQu <2643354262@qq.com> Date: Sun, 9 Feb 2025 07:27:06 +0000 Subject: [PATCH 16/16] fix e2e test --- .../extensions/replay-protection/README.md | 34 +++++++----------- .../extensions/replay-protection/README_EN.md | 35 ++++++++----------- .../extensions/replay-protection/main.go | 9 ++--- plugins/wasm-go/pkg/wrapper/redis_wrapper.go | 4 +-- 4 files changed, 32 insertions(+), 50 deletions(-) diff --git a/plugins/wasm-go/extensions/replay-protection/README.md b/plugins/wasm-go/extensions/replay-protection/README.md index c5753f0e7a..65ce4220c7 100644 --- a/plugins/wasm-go/extensions/replay-protection/README.md +++ b/plugins/wasm-go/extensions/replay-protection/README.md @@ -39,27 +39,19 @@ Nonce (Number used ONCE) 防重放插件通过验证请求中的一次性随机 以下是一个防重放攻击插件的完整配置示例: ```yaml -apiVersion: extensions.higress.io/v1alpha1 -kind: WasmPlugin -metadata: - name: replay-protection - namespace: higress-system -spec: - defaultConfig: - force_nonce: true - nonce_header: "X-Higress-Nonce" # 指定 nonce 请求头名称 - nonce_ttl: 900 # nonce 有效期设置为 900 秒 - nonce_min_length: 8 # nonce 最小长度 - nonce_max_length: 128 # nonce 最大长度 - validate_base64: true # 是否开启base64格式校验 - reject_code: 429 # 拒绝请求时返回的状态码 - reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 - redis: - serviceName: "redis.dns" # Redis 服务名称 - servicePort: 6379 # Redis 服务端口 - timeout: 1000 # Redis 操作超时时间 - keyPrefix: "replay-protection" # Redis 键前缀 - url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm +force_nonce: true +nonce_header: "X-Higress-Nonce" # 指定 nonce 请求头名称 +nonce_ttl: 900 # nonce 有效期设置为 900 秒 +nonce_min_length: 8 # nonce 最小长度 +nonce_max_length: 128 # nonce 最大长度 +validate_base64: true # 是否开启base64格式校验 +reject_code: 429 # 拒绝请求时返回的状态码 +reject_msg: "Duplicate nonce" # 拒绝请求时返回的错误信息 +redis: + serviceName: "redis.dns" # Redis 服务名称 + servicePort: 6379 # Redis 服务端口 + timeout: 1000 # Redis 操作超时时间 + keyPrefix: "replay-protection" # Redis 键前缀 ``` ## 使用说明 diff --git a/plugins/wasm-go/extensions/replay-protection/README_EN.md b/plugins/wasm-go/extensions/replay-protection/README_EN.md index 3e36d12fc2..25dc7339e0 100644 --- a/plugins/wasm-go/extensions/replay-protection/README_EN.md +++ b/plugins/wasm-go/extensions/replay-protection/README_EN.md @@ -38,27 +38,20 @@ The Nonce (Number used ONCE) replay protection plugin prevents request replay at ## Configuration Example ```yaml -apiVersion: extensions.higress.io/v1alpha1 -kind: WasmPlugin -metadata: - name: replay-protection - namespace: higress-system -spec: - defaultConfig: - force_nonce: true - nonce_ttl: 900 - nonce_header:"" - nonce_min_length: 8 - nonce_max_length: 128 - validate_base64: true - reject_code: 429 - reject_msg: "Duplicate nonce" - redis: - serviceName: "redis.dns" - servicePort: 6379 - timeout: 1000 - keyPrefix: "replay-protection" - url: file:///opt/plugins/wasm-go/extensions/replay-protection/plugin.wasm + +force_nonce: true +nonce_ttl: 900 +nonce_header:"" +nonce_min_length: 8 +nonce_max_length: 128 +validate_base64: true +reject_code: 429 +reject_msg: "Duplicate nonce" +redis: + serviceName: "redis.dns" + servicePort: 6379 + timeout: 1000 + keyPrefix: "replay-protection" ``` ## Usage diff --git a/plugins/wasm-go/extensions/replay-protection/main.go b/plugins/wasm-go/extensions/replay-protection/main.go index 6c9f3584ca..bbe7760052 100644 --- a/plugins/wasm-go/extensions/replay-protection/main.go +++ b/plugins/wasm-go/extensions/replay-protection/main.go @@ -153,12 +153,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig log.Errorf("Redis error: %v", response.Error()) proxywasm.SendHttpResponse(500, nil, []byte("Internal Server Error"), -1) return - } else if response.Integer() == 1 { - // SETNX successful, pass the request - proxywasm.ResumeHttpRequest() - return - } else { - // Nonce already exists, reject the request + } else if len(response.String()) == 0 { log.Warnf("Duplicate nonce detected: %s", nonce) proxywasm.SendHttpResponse( config.RejectCode, @@ -166,6 +161,8 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, config ReplayProtectionConfig []byte(fmt.Sprintf("%s: %s", config.RejectMsg, nonce)), -1, ) + } else { + proxywasm.ResumeHttpRequest() } }) diff --git a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go index 9299d12d09..789ff2696e 100644 --- a/plugins/wasm-go/pkg/wrapper/redis_wrapper.go +++ b/plugins/wasm-go/pkg/wrapper/redis_wrapper.go @@ -317,11 +317,11 @@ func (c *RedisClusterClient[C]) SetNX(key string, value interface{}, expiration args = append(args, "set") args = append(args, key) args = append(args, value) + args = append(args, "NX") if expiration > 0 { - args = append(args, "ex") + args = append(args, "EX") args = append(args, expiration) } - args = append(args, "nx") return RedisCall(c.cluster, respString(args), callback) }