Compare commits

..

18 Commits
v2.2.3 ... main

Author SHA1 Message Date
cyberslack_lee
c8b82797c5 fix: harden cluster key rate limit cookie parsing (#4091) 2026-07-23 10:25:37 +08:00
EndlessSeeker
b272f3a818 docs: add CNCF incubation review artifacts (#4177)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-07-22 18:09:49 +08:00
Xijun Dai
762324c376 fix(ai-proxy): fix cache token double-counting in Claude-to-OpenAI protocol conversion (#4149)
Signed-off-by: Xijun Dai <daixijun1990@gmail.com>
Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
Co-authored-by: Kent Dong <ch3cho@qq.com>
2026-07-18 00:14:24 +08:00
geekspeng
165f2974db feat(ai-token,cluster-key-ratelimit): multi-rule OR overlay (breaking change) (#4011)
Signed-off-by: geekspeng <geekspeng@icloud.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-17 21:11:33 +08:00
ljbddy
2682c926fc feat(ai-statistics): add llm_failure_count metric for error response tracking (#3975)
Signed-off-by: ljb <1327941766@qq.com>
2026-07-17 19:16:16 +08:00
Anas Khan
e01db53e9f fix(cache-control): emit standard max-age Cache-Control directive (#4118)
Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
2026-07-17 18:42:30 +08:00
EndlessSeeker
45942d90b6 fix(jwt-auth): harden cookie token parsing (#4153)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-07-17 14:23:19 +08:00
Kent Dong
58773cb98e feat(ci): allow custom wasm plugin image name via .buildrc (#4053)
Signed-off-by: CH3CHO <ch3cho@qq.com>
2026-07-16 14:41:15 +08:00
EndlessSeeker
afb72b81cb test: add Gateway API v1.4 conformance CI (#4135)
Signed-off-by: EndlessSeeker <1766508902@qq.com>
2026-07-14 19:49:58 +08:00
澄潭
152f14b171 fix(golang-filter): align Envoy Go bindings with the envoy/envoy submodule (#4138)
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-07-14 17:53:39 +08:00
澄潭
e169ccbe5f feat: require HTTP Basic auth for higress-ops MCP server (#4139)
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-07-14 17:51:28 +08:00
cyberslack_lee
154782660c fix: fix rand.init(0) panic in failover.go (#4112) 2026-07-09 14:41:33 +08:00
澄潭
e8b9989265 fix: align cors plugin behavior with cors semantics (#4036)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
Signed-off-by: zty98751 <zty98751@alibaba-inc.com>
2026-07-09 14:36:08 +08:00
澄潭
c6fa1f2ad0 fix: bump envoy submodule to skip Redis async client teardown on unchanged config (#4104)
Signed-off-by: 澄潭 <zty98751@alibaba-inc.com>
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-07-08 10:05:14 +08:00
澄潭
bd9c4c5104 Add Envoy gateway update skill (#4105)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-07-07 23:26:38 +08:00
澄潭
4e4ca39b42 fix: bump envoy submodule for doAfterVmCallActions drain-to-local (#4034) (#4080)
Signed-off-by: 澄潭 <zty98751@alibaba-inc.com>
2026-07-07 09:36:25 +08:00
澄潭
d5f2b624fb feat: initialize issue-spec workflow for Higress (#4059)
Signed-off-by: johnlanni <johnlanni@users.noreply.github.com>
Co-authored-by: johnlanni <johnlanni@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 21:55:53 +08:00
澄潭
6c98ea74e0 fix: avoid waiting for missing request bodies in transformer (#4024)
Signed-off-by: johnlanni <zty98751@alibaba-inc.com>
2026-06-25 17:36:01 +08:00
129 changed files with 6259 additions and 520 deletions

View File

@@ -0,0 +1,94 @@
---
name: higress-update-envoy-gateway
description: Update Higress Envoy binary and gateway image dependencies for e2e validation. Use when Codex needs to build Envoy packages from the current Higress branch, upload those packages to higress-group/proxy releases, update Makefile.core.mk ENVOY_PACKAGE_URL_PATTERN and ENVOY_LATEST_IMAGE_TAG, run make build-gateway-local, retag the generated proxy/proxyv2 image as gateway, push the gateway image, or prepare a signed-off PR that lets Higress e2e tests consume a new Envoy build.
---
# Higress Envoy Gateway Dependency Update
## Core Workflow
Run from the Higress repo root unless a step explicitly says otherwise.
1. Verify context:
- Check `git status --short --branch`.
- Do not remove unrelated user changes or generated artifacts.
- Use `gh` for GitHub release/PR checks and always pass `--repo` for non-current repos.
2. Build Envoy packages from the current branch:
```bash
git submodule update --init
make build-envoy
```
Expected artifacts land in `external/package/`, typically:
- `envoy-alpha-<proxy-sha>.tar.gz`
- `envoy-symbol-<proxy-sha>.tar.gz`
- matching `.sha256` and `.dwp` files
3. Publish Envoy package release in `higress-group/proxy`:
- Create a new release tag by incrementing the requested RC/test tag, for example `v2.2.4-rc.2-test-cpp-host`.
- Match the reference release asset names, even if local filenames include SHAs:
- local `envoy-alpha-<sha>.tar.gz` uploads as `envoy-amd64.tar.gz`
- local `envoy-symbol-<sha>.tar.gz` uploads as `envoy-symbol-amd64.tar.gz`
- Use temporary renamed copies rather than renaming source artifacts:
```bash
cp external/package/envoy-alpha-<sha>.tar.gz /tmp/envoy-amd64.tar.gz
cp external/package/envoy-symbol-<sha>.tar.gz /tmp/envoy-symbol-amd64.tar.gz
gh release create <release-tag> /tmp/envoy-amd64.tar.gz /tmp/envoy-symbol-amd64.tar.gz \
--repo higress-group/proxy --target <target-branch> --title <release-tag> --generate-notes
gh release view <release-tag> --repo higress-group/proxy --json tagName,targetCommitish,assets,url
```
- If following an existing reference release, inspect it first with `gh release view`.
4. Update Higress Makefile dependencies:
- In `Makefile.core.mk`, set:
```make
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/<release-tag>/envoy-symbol-ARCH.tar.gz
```
- After building and pushing the gateway image, set:
```make
ENVOY_LATEST_IMAGE_TAG ?= <gateway-image-tag>
```
5. Build the local gateway image:
```bash
make build-gateway-local
```
Watch the log to confirm Envoy downloads from the new release URL. The build target may emit an image under a proxy-style repository/name, commonly:
- `higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2:<tag>`
6. Retag proxy image as gateway:
```bash
docker tag higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/proxyv2:<tag> \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:<tag>
docker images --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedSince}}' \
higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway
```
Verify the `gateway:<tag>` and `proxyv2:<tag>` image IDs match.
7. Push the gateway image when requested:
```bash
docker push higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:<tag>
```
Record the pushed digest in the final response.
8. Commit and push Makefile changes:
- Commit only intended tracked files. Do not add `plugins/golang-filter/golang-filter_amd64.so` or other build outputs unless explicitly requested.
- Use DCO sign-off:
```bash
git add Makefile.core.mk
git commit -s -m "Update gateway envoy dependencies"
git push origin <branch>
```
- If DCO fails after a previous unsigned commit:
```bash
git commit --amend --no-edit --signoff
git push --force-with-lease origin <branch>
gh pr checks <pr-number> --repo higress-group/higress
```
## Practical Notes
- `make build-gateway-local` may need Docker daemon access, network access, and write access to repo/submodule state.
- If sandboxed commands fail with read-only filesystem errors, Docker socket permission errors, or network failures, rerun the same important command with elevated permissions and a concrete justification.
- The default local gateway tag usually comes from the current Git revision shown in the build log as `TAG=<sha>`.
- For e2e validation, the point of this workflow is to make `install-dev`, `install-dev-wasmplugin`, and local image update targets use the newly pushed gateway image through `ENVOY_LATEST_IMAGE_TAG`.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Higress Envoy Gateway"
short_description: "Update Envoy packages and gateway tag"
default_prompt: "Use $higress-update-envoy-gateway to update Higress Makefile Envoy dependencies and build a gateway image for e2e validation."

View File

@@ -0,0 +1,35 @@
---
name: issue-spec-apply
description: Implement PROCESS comments for an issue-spec change and keep PR traceability synchronized.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Apply
Use when the user asks for /issue-spec:apply, issue-spec apply, or implementing PROCESS/TASK scopes from an issue-spec change.
## Steps
1. Read proposal/design/implement issue context and list typed comments with issue-spec comment list --json.
2. Confirm issue-spec auth status --json includes the expected GitHub backend. Local gh-authenticated sessions can use the native gh backend; keep ISSUE_SPEC_TOKEN="$(gh auth token)" only as an older-version or forced-rest compatibility path.
3. Create or update PROCESS comments with owner agent, scope, dependencies, write ownership, and status.
4. Split non-trivial work into independent worker PROCESS nodes when file/module ownership does not overlap; execute independent workers in parallel when available.
5. Add dedicated review PROCESS nodes for non-trivial changes. Review PROCESS nodes should own review scopes such as CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
6. Link each PROCESS to its TASK comments with issue-spec link.
7. Implement the code changes for one PROCESS scope at a time, or integrate completed worker outputs by dependency order.
8. Link every worker and review PROCESS to the PR with issue-spec pr link-process.
9. Add PR rationale comments on key changed lines with issue-spec pr rationale, each linked to a SPEC comment.
10. Mark PROCESS comments done only after implementation/review work and focused verification evidence exist.
## Coordinator DAG Execution
1. Build the ready set from PROCESS nodes whose dependencies are done.
2. Keep immediate blocking work local when the next step depends on it.
3. Spawn or assign independent worker agents only when their write ownership is disjoint.
4. Spawn or assign independent review agents only when their review scopes are disjoint.
5. Integrate completed outputs by dependency order and update PROCESS evidence before marking done.

View File

@@ -0,0 +1,24 @@
---
name: issue-spec-archive
description: Create the post-merge durable spec archive PR for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Archive
Use when the user asks for /issue-spec:archive, issue-spec archive, or creating the post-merge durable spec PR.
## Steps
1. Confirm the implementation PR is merged.
2. Create the durable spec PR:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --create-pr --branch issue-spec/durable-spec-<capability> --json
3. Review the durable spec PR for long-lived behavior only. Do not copy process records, review findings, or verification logs into durable specs.
4. After durable spec PR merge, keep proposal/design/implement issues as audit history unless the project policy says to close them.

View File

@@ -0,0 +1,58 @@
---
name: issue-spec-github
description: Use GitHub CLI for GitHub issues, pull requests, CI runs, and API queries that issue-spec does not wrap.
license: MIT
compatibility: Requires GitHub CLI (gh).
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# GitHub CLI
Use the `gh` CLI to interact with GitHub repositories, issues, pull requests, CI, and API endpoints.
## When To Use
- Checking PR status, reviews, mergeability, or CI checks.
- Creating, viewing, updating, closing, or commenting on GitHub issues.
- Listing or inspecting pull requests, workflow runs, releases, labels, or repository metadata.
- Calling GitHub API endpoints with `gh api` when issue-spec does not provide a dedicated command.
## When Not To Use
- Local git operations such as commit, branch, fetch, merge, or push. Use `git` directly.
- Non-GitHub repositories. Use the matching provider CLI instead.
- Complex code review across local diffs. Read the repository files directly and use issue-spec review commands for traceable findings.
## Setup
```bash
gh auth login
gh auth status
```
## Common Commands
```bash
gh issue list --repo owner/repo --state open
gh issue view 42 --repo owner/repo --json number,title,state,url,body
gh issue comment 42 --repo owner/repo --body "Comment body"
gh pr list --repo owner/repo
gh pr view 17 --repo owner/repo --json number,title,state,headRefName,baseRefName,url
gh pr checks 17 --repo owner/repo
gh run list --repo owner/repo --limit 10
gh run view <run-id> --repo owner/repo --log-failed
gh api repos/owner/repo/labels --jq '.[].name'
```
## Notes
- Always pass `--repo owner/repo` when the current directory is not definitely inside the target repository.
- Use GitHub URLs directly when convenient, for example `gh pr view https://github.com/owner/repo/pull/17`.
- Prefer structured output with `--json` and `--jq` when another command or agent step consumes the result.
- issue-spec owns the proposal, design, implement, typed comment, review, verify, and archive workflow state. Use `gh` for adjacent GitHub operations that are outside issue-spec's command surface.

View File

@@ -0,0 +1,37 @@
---
name: issue-spec-propose
description: Create or continue proposal, SPEC, QUESTION, design, and TASK artifacts for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Propose
Use when the user asks for /issue-spec:propose, issue-spec propose, creating a change proposal, drafting SPEC comments, or preparing design/tasks after questions converge.
## Steps
1. Create the proposal issue:
issue-spec issue create proposal --repo higress-group/higress --change <change-name> --body-file <proposal.md>
2. If the proposal body needs revision after discussion, update it in place:
issue-spec issue update --repo higress-group/higress --issue <proposal-issue> --body-file <proposal.md> --summary "<what changed>"
3. Add SPEC comments with issue-spec comment upsert --type SPEC. SPEC comments must use MUST/SHALL and WHEN/THEN scenarios.
4. Add QUESTION comments for unresolved behavior with issue-spec question create and resolve blocking questions before design.
5. Create the design issue after SPEC/QUESTION convergence:
issue-spec issue create design --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --body-file <design.md>
6. Add TASK comments with issue-spec comment upsert --type TASK and link every TASK to covered SPEC comments with issue-spec link.
7. Create the implement issue once tasks are ready:
issue-spec issue create implement --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --design <design-issue-or-url> --body-file <implement.md>
8. Run issue-spec verify-links and fix missing backlinks before implementation.

View File

@@ -0,0 +1,32 @@
---
name: issue-spec-review
description: Review an issue-spec implementation PR, create PR line findings, reply after fixes, and sync REVIEW comments.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Review
Use when the user asks for /issue-spec:review, issue-spec review, or a PR review gate for an issue-spec implementation.
## Steps
1. Run issue-spec review sync --repo higress-group/higress --pr <number> --implement <issue> --id REVIEW-<n> --json to capture current rationale comments, findings, and checks.
2. For non-trivial PRs, spawn or assign dedicated review agents as review PROCESS owners. Multiple review agents can run in parallel when their review scopes are independent.
3. Give each review agent a concrete scope and expected output: actionable findings only, severity, file/line, linked SPEC, owner PROCESS, and suggested fix.
4. Create actionable PR line findings with issue-spec review finding. Use P0/P1 for blockers and P2 for non-blocking follow-up.
5. Assign every finding to a PROCESS owner. If no findings are found, record that result in REVIEW or VERIFY evidence.
6. After the worker fixes a finding, reply to the original thread with issue-spec review reply --status resolved.
7. Re-run review sync. P0/P1 findings must be resolved before final verify/archive.
## Review DAG Policy
1. Every non-trivial PR should have at least one dedicated review PROCESS node before final verify.
2. Use multiple review agents in parallel when scopes are independent, for example CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
3. A review agent reports findings only; the coordinator converts actionable line findings into issue-spec review finding comments.
4. P0/P1 findings block final verify until the owner PROCESS fixes them and issue-spec review reply records the resolution on the original thread.
5. If a review agent finds no issues, record that result in REVIEW or VERIFY evidence before marking the review PROCESS done.

View File

@@ -0,0 +1,28 @@
---
name: issue-spec-verify
description: Run final issue-spec verification across traceability, questions, review findings, PR rationale, PR checks, and durable spec draft.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Verify
Use when the user asks for /issue-spec:verify, issue-spec verify, or final readiness evidence before merge/archive.
## Steps
1. Run focused project tests and record evidence in VERIFY comments.
2. Run issue-spec verify-links --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json.
3. Render a durable spec draft:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --output /tmp/<capability>-spec.md --json
4. Run final verify:
issue-spec verify --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --pr <pr> --durable-spec /tmp/<capability>-spec.md --json
5. Final verify must fail if blocking questions, missing links, missing PROCESS rationale, open P0/P1 findings, failed or pending PR checks, or durable spec omissions exist.

View File

@@ -0,0 +1,53 @@
---
name: issue-spec-workflow
description: Use issue-spec to run an issue-native OpenSpec-style workflow with GitHub issues, typed comments, PR review comments, final verification, and durable spec archive PRs.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Workflow
Use this skill for issue-native OpenSpec work. Active change artifacts live in GitHub issues and issue comments; durable specs are repository files created after implementation merge.
## Start
1. Run issue-spec auth status --json and confirm the active auth source and GitHub backend.
2. Run issue-spec status --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json when issues already exist.
3. For new work, create proposal, design, and implement issues with issue-spec issue create and pass --body-file with concrete markdown content.
4. When an issue body changes, update it in place with issue-spec issue update --body-file and include --summary for the human-readable audit trail.
5. Store requirements, tasks, process ownership, review, and verify evidence as typed comments.
## GitHub Backend
- Local agents may rely on native GitHub CLI support: when no ISSUE_SPEC_TOKEN, GH_TOKEN, GITHUB_TOKEN, keyring token, or issue-spec config token is present and gh auth status --active succeeds for the target host, issue-spec auto-selects the gh backend.
- Explicit env or stored issue-spec tokens keep the rest backend under auto selection. Set ISSUE_SPEC_GITHUB_BACKEND=rest or ISSUE_SPEC_GITHUB_BACKEND=gh only when a workflow needs deterministic backend selection.
- The gh backend proxies GitHub API operations through gh api and uses gh --hostname for Enterprise hosts. It does not replace local git commands.
- ISSUE_SPEC_API_URL applies to the rest backend. Forced gh mode should be used only with hosts that gh can address.
- Use ISSUE_SPEC_TOKEN="$(gh auth token)" only for older issue-spec versions or when deliberately forcing rest while sourcing the token from gh.
## Rules
- Create SPEC comments before design; each SPEC must be testable and include WHEN/THEN scenarios.
- Do not leave active proposal/design/implement issue bodies as TBD placeholders.
- Resolve blocking QUESTION comments before design/tasks, or explicitly record accepted assumptions.
- Link SPEC <-> TASK and TASK <-> PROCESS with issue-spec link.
- Link every PROCESS to the implementation PR with issue-spec pr link-process.
- For non-trivial changes, include review PROCESS nodes in the DAG; review agents are scheduled like worker agents and can run in parallel when their review scopes are independent.
- Small changes may stay coordinator-only, but record the serial execution decision in the implement or VERIFY evidence.
- Before human review, add PR rationale comments with issue-spec pr rationale for every active PROCESS.
- Use issue-spec review finding for PR line findings and issue-spec review reply to close the original thread.
- Run issue-spec review sync and issue-spec verify before declaring ready.
- After the implementation PR merges, create the separate durable spec PR with issue-spec archive durable-spec --create-pr.
## Coordinator DAG Execution
1. Treat PROCESS comments as DAG nodes with explicit owner, dependencies, write or review scope, PR link, and evidence.
2. Select ready PROCESS nodes whose dependencies are done and whose scopes do not overlap.
3. Dispatch independent worker PROCESS nodes in parallel when their file/module ownership is disjoint.
4. Dispatch independent review PROCESS nodes in parallel for non-trivial PRs after PR rationale exists.
5. Integrate completed worker outputs by dependency order; route P0/P1 review findings back to the owner PROCESS.
6. Mark PROCESS nodes done only after their implementation or review evidence is recorded and blocking findings are resolved.

View File

@@ -0,0 +1,31 @@
---
name: "Issue Spec: Apply"
description: "Implement PROCESS comments for an issue-spec change and keep PR traceability synchronized."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Apply
Use when the user asks for /issue-spec:apply, issue-spec apply, or implementing PROCESS/TASK scopes from an issue-spec change.
## Steps
1. Read proposal/design/implement issue context and list typed comments with issue-spec comment list --json.
2. Confirm issue-spec auth status --json includes the expected GitHub backend. Local gh-authenticated sessions can use the native gh backend; keep ISSUE_SPEC_TOKEN="$(gh auth token)" only as an older-version or forced-rest compatibility path.
3. Create or update PROCESS comments with owner agent, scope, dependencies, write ownership, and status.
4. Split non-trivial work into independent worker PROCESS nodes when file/module ownership does not overlap; execute independent workers in parallel when available.
5. Add dedicated review PROCESS nodes for non-trivial changes. Review PROCESS nodes should own review scopes such as CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
6. Link each PROCESS to its TASK comments with issue-spec link.
7. Implement the code changes for one PROCESS scope at a time, or integrate completed worker outputs by dependency order.
8. Link every worker and review PROCESS to the PR with issue-spec pr link-process.
9. Add PR rationale comments on key changed lines with issue-spec pr rationale, each linked to a SPEC comment.
10. Mark PROCESS comments done only after implementation/review work and focused verification evidence exist.
## Coordinator DAG Execution
1. Build the ready set from PROCESS nodes whose dependencies are done.
2. Keep immediate blocking work local when the next step depends on it.
3. Spawn or assign independent worker agents only when their write ownership is disjoint.
4. Spawn or assign independent review agents only when their review scopes are disjoint.
5. Integrate completed outputs by dependency order and update PROCESS evidence before marking done.

View File

@@ -0,0 +1,20 @@
---
name: "Issue Spec: Archive"
description: "Create the post-merge durable spec archive PR for an issue-spec change."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Archive
Use when the user asks for /issue-spec:archive, issue-spec archive, or creating the post-merge durable spec PR.
## Steps
1. Confirm the implementation PR is merged.
2. Create the durable spec PR:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --create-pr --branch issue-spec/durable-spec-<capability> --json
3. Review the durable spec PR for long-lived behavior only. Do not copy process records, review findings, or verification logs into durable specs.
4. After durable spec PR merge, keep proposal/design/implement issues as audit history unless the project policy says to close them.

View File

@@ -0,0 +1,33 @@
---
name: "Issue Spec: Propose"
description: "Create or continue proposal, SPEC, QUESTION, design, and TASK artifacts for an issue-spec change."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Propose
Use when the user asks for /issue-spec:propose, issue-spec propose, creating a change proposal, drafting SPEC comments, or preparing design/tasks after questions converge.
## Steps
1. Create the proposal issue:
issue-spec issue create proposal --repo higress-group/higress --change <change-name> --body-file <proposal.md>
2. If the proposal body needs revision after discussion, update it in place:
issue-spec issue update --repo higress-group/higress --issue <proposal-issue> --body-file <proposal.md> --summary "<what changed>"
3. Add SPEC comments with issue-spec comment upsert --type SPEC. SPEC comments must use MUST/SHALL and WHEN/THEN scenarios.
4. Add QUESTION comments for unresolved behavior with issue-spec question create and resolve blocking questions before design.
5. Create the design issue after SPEC/QUESTION convergence:
issue-spec issue create design --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --body-file <design.md>
6. Add TASK comments with issue-spec comment upsert --type TASK and link every TASK to covered SPEC comments with issue-spec link.
7. Create the implement issue once tasks are ready:
issue-spec issue create implement --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --design <design-issue-or-url> --body-file <implement.md>
8. Run issue-spec verify-links and fix missing backlinks before implementation.

View File

@@ -0,0 +1,28 @@
---
name: "Issue Spec: Review"
description: "Review an issue-spec implementation PR, create PR line findings, reply after fixes, and sync REVIEW comments."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Review
Use when the user asks for /issue-spec:review, issue-spec review, or a PR review gate for an issue-spec implementation.
## Steps
1. Run issue-spec review sync --repo higress-group/higress --pr <number> --implement <issue> --id REVIEW-<n> --json to capture current rationale comments, findings, and checks.
2. For non-trivial PRs, spawn or assign dedicated review agents as review PROCESS owners. Multiple review agents can run in parallel when their review scopes are independent.
3. Give each review agent a concrete scope and expected output: actionable findings only, severity, file/line, linked SPEC, owner PROCESS, and suggested fix.
4. Create actionable PR line findings with issue-spec review finding. Use P0/P1 for blockers and P2 for non-blocking follow-up.
5. Assign every finding to a PROCESS owner. If no findings are found, record that result in REVIEW or VERIFY evidence.
6. After the worker fixes a finding, reply to the original thread with issue-spec review reply --status resolved.
7. Re-run review sync. P0/P1 findings must be resolved before final verify/archive.
## Review DAG Policy
1. Every non-trivial PR should have at least one dedicated review PROCESS node before final verify.
2. Use multiple review agents in parallel when scopes are independent, for example CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
3. A review agent reports findings only; the coordinator converts actionable line findings into issue-spec review finding comments.
4. P0/P1 findings block final verify until the owner PROCESS fixes them and issue-spec review reply records the resolution on the original thread.
5. If a review agent finds no issues, record that result in REVIEW or VERIFY evidence before marking the review PROCESS done.

View File

@@ -0,0 +1,24 @@
---
name: "Issue Spec: Verify"
description: "Run final issue-spec verification across traceability, questions, review findings, PR rationale, PR checks, and durable spec draft."
category: "Workflow"
tags: ["workflow", "issue-spec"]
---
# Issue Spec Verify
Use when the user asks for /issue-spec:verify, issue-spec verify, or final readiness evidence before merge/archive.
## Steps
1. Run focused project tests and record evidence in VERIFY comments.
2. Run issue-spec verify-links --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json.
3. Render a durable spec draft:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --output /tmp/<capability>-spec.md --json
4. Run final verify:
issue-spec verify --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --pr <pr> --durable-spec /tmp/<capability>-spec.md --json
5. Final verify must fail if blocking questions, missing links, missing PROCESS rationale, open P0/P1 findings, failed or pending PR checks, or durable spec omissions exist.

View File

@@ -0,0 +1 @@
../../.agents/skills/higress-update-envoy-gateway

View File

@@ -0,0 +1,35 @@
---
name: issue-spec-apply
description: Implement PROCESS comments for an issue-spec change and keep PR traceability synchronized.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Apply
Use when the user asks for /issue-spec:apply, issue-spec apply, or implementing PROCESS/TASK scopes from an issue-spec change.
## Steps
1. Read proposal/design/implement issue context and list typed comments with issue-spec comment list --json.
2. Confirm issue-spec auth status --json includes the expected GitHub backend. Local gh-authenticated sessions can use the native gh backend; keep ISSUE_SPEC_TOKEN="$(gh auth token)" only as an older-version or forced-rest compatibility path.
3. Create or update PROCESS comments with owner agent, scope, dependencies, write ownership, and status.
4. Split non-trivial work into independent worker PROCESS nodes when file/module ownership does not overlap; execute independent workers in parallel when available.
5. Add dedicated review PROCESS nodes for non-trivial changes. Review PROCESS nodes should own review scopes such as CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
6. Link each PROCESS to its TASK comments with issue-spec link.
7. Implement the code changes for one PROCESS scope at a time, or integrate completed worker outputs by dependency order.
8. Link every worker and review PROCESS to the PR with issue-spec pr link-process.
9. Add PR rationale comments on key changed lines with issue-spec pr rationale, each linked to a SPEC comment.
10. Mark PROCESS comments done only after implementation/review work and focused verification evidence exist.
## Coordinator DAG Execution
1. Build the ready set from PROCESS nodes whose dependencies are done.
2. Keep immediate blocking work local when the next step depends on it.
3. Spawn or assign independent worker agents only when their write ownership is disjoint.
4. Spawn or assign independent review agents only when their review scopes are disjoint.
5. Integrate completed outputs by dependency order and update PROCESS evidence before marking done.

View File

@@ -0,0 +1,24 @@
---
name: issue-spec-archive
description: Create the post-merge durable spec archive PR for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Archive
Use when the user asks for /issue-spec:archive, issue-spec archive, or creating the post-merge durable spec PR.
## Steps
1. Confirm the implementation PR is merged.
2. Create the durable spec PR:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --create-pr --branch issue-spec/durable-spec-<capability> --json
3. Review the durable spec PR for long-lived behavior only. Do not copy process records, review findings, or verification logs into durable specs.
4. After durable spec PR merge, keep proposal/design/implement issues as audit history unless the project policy says to close them.

View File

@@ -0,0 +1,58 @@
---
name: issue-spec-github
description: Use GitHub CLI for GitHub issues, pull requests, CI runs, and API queries that issue-spec does not wrap.
license: MIT
compatibility: Requires GitHub CLI (gh).
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# GitHub CLI
Use the `gh` CLI to interact with GitHub repositories, issues, pull requests, CI, and API endpoints.
## When To Use
- Checking PR status, reviews, mergeability, or CI checks.
- Creating, viewing, updating, closing, or commenting on GitHub issues.
- Listing or inspecting pull requests, workflow runs, releases, labels, or repository metadata.
- Calling GitHub API endpoints with `gh api` when issue-spec does not provide a dedicated command.
## When Not To Use
- Local git operations such as commit, branch, fetch, merge, or push. Use `git` directly.
- Non-GitHub repositories. Use the matching provider CLI instead.
- Complex code review across local diffs. Read the repository files directly and use issue-spec review commands for traceable findings.
## Setup
```bash
gh auth login
gh auth status
```
## Common Commands
```bash
gh issue list --repo owner/repo --state open
gh issue view 42 --repo owner/repo --json number,title,state,url,body
gh issue comment 42 --repo owner/repo --body "Comment body"
gh pr list --repo owner/repo
gh pr view 17 --repo owner/repo --json number,title,state,headRefName,baseRefName,url
gh pr checks 17 --repo owner/repo
gh run list --repo owner/repo --limit 10
gh run view <run-id> --repo owner/repo --log-failed
gh api repos/owner/repo/labels --jq '.[].name'
```
## Notes
- Always pass `--repo owner/repo` when the current directory is not definitely inside the target repository.
- Use GitHub URLs directly when convenient, for example `gh pr view https://github.com/owner/repo/pull/17`.
- Prefer structured output with `--json` and `--jq` when another command or agent step consumes the result.
- issue-spec owns the proposal, design, implement, typed comment, review, verify, and archive workflow state. Use `gh` for adjacent GitHub operations that are outside issue-spec's command surface.

View File

@@ -0,0 +1,37 @@
---
name: issue-spec-propose
description: Create or continue proposal, SPEC, QUESTION, design, and TASK artifacts for an issue-spec change.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Propose
Use when the user asks for /issue-spec:propose, issue-spec propose, creating a change proposal, drafting SPEC comments, or preparing design/tasks after questions converge.
## Steps
1. Create the proposal issue:
issue-spec issue create proposal --repo higress-group/higress --change <change-name> --body-file <proposal.md>
2. If the proposal body needs revision after discussion, update it in place:
issue-spec issue update --repo higress-group/higress --issue <proposal-issue> --body-file <proposal.md> --summary "<what changed>"
3. Add SPEC comments with issue-spec comment upsert --type SPEC. SPEC comments must use MUST/SHALL and WHEN/THEN scenarios.
4. Add QUESTION comments for unresolved behavior with issue-spec question create and resolve blocking questions before design.
5. Create the design issue after SPEC/QUESTION convergence:
issue-spec issue create design --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --body-file <design.md>
6. Add TASK comments with issue-spec comment upsert --type TASK and link every TASK to covered SPEC comments with issue-spec link.
7. Create the implement issue once tasks are ready:
issue-spec issue create implement --repo higress-group/higress --change <change-name> --proposal <proposal-issue-or-url> --design <design-issue-or-url> --body-file <implement.md>
8. Run issue-spec verify-links and fix missing backlinks before implementation.

View File

@@ -0,0 +1,32 @@
---
name: issue-spec-review
description: Review an issue-spec implementation PR, create PR line findings, reply after fixes, and sync REVIEW comments.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Review
Use when the user asks for /issue-spec:review, issue-spec review, or a PR review gate for an issue-spec implementation.
## Steps
1. Run issue-spec review sync --repo higress-group/higress --pr <number> --implement <issue> --id REVIEW-<n> --json to capture current rationale comments, findings, and checks.
2. For non-trivial PRs, spawn or assign dedicated review agents as review PROCESS owners. Multiple review agents can run in parallel when their review scopes are independent.
3. Give each review agent a concrete scope and expected output: actionable findings only, severity, file/line, linked SPEC, owner PROCESS, and suggested fix.
4. Create actionable PR line findings with issue-spec review finding. Use P0/P1 for blockers and P2 for non-blocking follow-up.
5. Assign every finding to a PROCESS owner. If no findings are found, record that result in REVIEW or VERIFY evidence.
6. After the worker fixes a finding, reply to the original thread with issue-spec review reply --status resolved.
7. Re-run review sync. P0/P1 findings must be resolved before final verify/archive.
## Review DAG Policy
1. Every non-trivial PR should have at least one dedicated review PROCESS node before final verify.
2. Use multiple review agents in parallel when scopes are independent, for example CLI/API behavior, workflow docs, tests, compatibility, or security-sensitive surfaces.
3. A review agent reports findings only; the coordinator converts actionable line findings into issue-spec review finding comments.
4. P0/P1 findings block final verify until the owner PROCESS fixes them and issue-spec review reply records the resolution on the original thread.
5. If a review agent finds no issues, record that result in REVIEW or VERIFY evidence before marking the review PROCESS done.

View File

@@ -0,0 +1,28 @@
---
name: issue-spec-verify
description: Run final issue-spec verification across traceability, questions, review findings, PR rationale, PR checks, and durable spec draft.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Verify
Use when the user asks for /issue-spec:verify, issue-spec verify, or final readiness evidence before merge/archive.
## Steps
1. Run focused project tests and record evidence in VERIFY comments.
2. Run issue-spec verify-links --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json.
3. Render a durable spec draft:
issue-spec archive durable-spec --repo higress-group/higress --proposal <issue> --capability <capability> --output /tmp/<capability>-spec.md --json
4. Run final verify:
issue-spec verify --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --pr <pr> --durable-spec /tmp/<capability>-spec.md --json
5. Final verify must fail if blocking questions, missing links, missing PROCESS rationale, open P0/P1 findings, failed or pending PR checks, or durable spec omissions exist.

View File

@@ -0,0 +1,53 @@
---
name: issue-spec-workflow
description: Use issue-spec to run an issue-native OpenSpec-style workflow with GitHub issues, typed comments, PR review comments, final verification, and durable spec archive PRs.
license: MIT
compatibility: Requires issue-spec CLI.
metadata:
author: issue-spec
version: "1.0"
generatedBy: "issue-spec"
---
# Issue Spec Workflow
Use this skill for issue-native OpenSpec work. Active change artifacts live in GitHub issues and issue comments; durable specs are repository files created after implementation merge.
## Start
1. Run issue-spec auth status --json and confirm the active auth source and GitHub backend.
2. Run issue-spec status --repo higress-group/higress --proposal <issue> --design <issue> --implement <issue> --json when issues already exist.
3. For new work, create proposal, design, and implement issues with issue-spec issue create and pass --body-file with concrete markdown content.
4. When an issue body changes, update it in place with issue-spec issue update --body-file and include --summary for the human-readable audit trail.
5. Store requirements, tasks, process ownership, review, and verify evidence as typed comments.
## GitHub Backend
- Local agents may rely on native GitHub CLI support: when no ISSUE_SPEC_TOKEN, GH_TOKEN, GITHUB_TOKEN, keyring token, or issue-spec config token is present and gh auth status --active succeeds for the target host, issue-spec auto-selects the gh backend.
- Explicit env or stored issue-spec tokens keep the rest backend under auto selection. Set ISSUE_SPEC_GITHUB_BACKEND=rest or ISSUE_SPEC_GITHUB_BACKEND=gh only when a workflow needs deterministic backend selection.
- The gh backend proxies GitHub API operations through gh api and uses gh --hostname for Enterprise hosts. It does not replace local git commands.
- ISSUE_SPEC_API_URL applies to the rest backend. Forced gh mode should be used only with hosts that gh can address.
- Use ISSUE_SPEC_TOKEN="$(gh auth token)" only for older issue-spec versions or when deliberately forcing rest while sourcing the token from gh.
## Rules
- Create SPEC comments before design; each SPEC must be testable and include WHEN/THEN scenarios.
- Do not leave active proposal/design/implement issue bodies as TBD placeholders.
- Resolve blocking QUESTION comments before design/tasks, or explicitly record accepted assumptions.
- Link SPEC <-> TASK and TASK <-> PROCESS with issue-spec link.
- Link every PROCESS to the implementation PR with issue-spec pr link-process.
- For non-trivial changes, include review PROCESS nodes in the DAG; review agents are scheduled like worker agents and can run in parallel when their review scopes are independent.
- Small changes may stay coordinator-only, but record the serial execution decision in the implement or VERIFY evidence.
- Before human review, add PR rationale comments with issue-spec pr rationale for every active PROCESS.
- Use issue-spec review finding for PR line findings and issue-spec review reply to close the original thread.
- Run issue-spec review sync and issue-spec verify before declaring ready.
- After the implementation PR merges, create the separate durable spec PR with issue-spec archive durable-spec --create-pr.
## Coordinator DAG Execution
1. Treat PROCESS comments as DAG nodes with explicit owner, dependencies, write or review scope, PR link, and evidence.
2. Select ready PROCESS nodes whose dependencies are done and whose scopes do not overlap.
3. Dispatch independent worker PROCESS nodes in parallel when their file/module ownership is disjoint.
4. Dispatch independent review PROCESS nodes in parallel for non-trivial PRs after PR rationale exists.
5. Integrate completed worker outputs by dependency order; route P0/P1 review findings back to the owner PROCESS.
6. Mark PROCESS nodes done only after their implementation or review evidence is recorded and blocking findings are resolved.

View File

@@ -7,10 +7,14 @@ assignees: ''
---
**If you are reporting *any* crash or *any* potential security issue, *do not*
open an issue in this repo. Please report the issue via [ASRC](https://security.alibaba.com/)(Alibaba Security Response Center) where the issue will be triaged appropriately.**
**If you are reporting a potential security issue, do not open a public issue.
You must submit the same substantive report through both
[GitHub Private Security Advisories](https://github.com/higress-group/higress/security/advisories/new)
and the [Alibaba Security Response Center](https://security.alibaba.com/), as
required by the project's [security policy](https://github.com/higress-group/higress/blob/main/SECURITY.md).
A crash with no suspected security impact may be reported with this template.**
- [ ] I have searched the [issues](https://github.com/alibaba/higress/issues) of this repository and believe that this is not a duplicate.
- [ ] I have searched the [issues](https://github.com/higress-group/higress/issues) of this repository and believe that this is not a duplicate.
### . Issue Description

View File

@@ -104,11 +104,6 @@ jobs:
push_command=${push_command#\"}
push_command=${push_command%\"} # 删除PUSH_COMMAND中的双引号确保oras push正常解析
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:${{ env.VERSION }}"
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${{ env.PLUGIN_NAME }}:latest"
echo "TargetImage=${target_image}"
echo "TargetImageLatest=${target_image_latest}"
cd ${{ github.workspace }}/plugins/wasm-${PLUGIN_TYPE}/extensions/${PLUGIN_NAME}
if [ -f ./.buildrc ]; then
echo 'Found .buildrc file, sourcing it...'
@@ -116,6 +111,21 @@ jobs:
else
echo '.buildrc file not found'
fi
# Resolve custom image short name from .buildrc, fallback to plugin directory name.
# .buildrc may define IMAGE_NAME=xxx to override the output image tag.
IMAGE_NAME=${IMAGE_NAME:-$PLUGIN_NAME}
if ! [[ "$IMAGE_NAME" =~ ^[a-z0-9._-]+$ ]]; then
echo "::error::Invalid IMAGE_NAME '$IMAGE_NAME' in .buildrc — must match [a-z0-9._-]+"
exit 1
fi
echo "IMAGE_NAME=${IMAGE_NAME}"
target_image="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${IMAGE_NAME}:${{ env.VERSION }}"
target_image_latest="${{ env.IMAGE_REGISTRY_SERVICE }}/${{ env.IMAGE_REPOSITORY}}/${IMAGE_NAME}:latest"
echo "TargetImage=${target_image}"
echo "TargetImageLatest=${target_image_latest}"
echo "EXTRA_TAGS=${EXTRA_TAGS}"
if [ "${PLUGIN_TYPE}" == "go" ]; then
command="

View File

@@ -93,8 +93,77 @@ jobs:
gateway-conformance-test:
runs-on: ubuntu-22.04
needs: [build]
env:
GATEWAY_API_TEST_NAMESPACE: gateway-conformance-infra
HIGRESS_TEST_IMAGE_TAG: ${{ github.event.pull_request.head.sha || github.sha }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ env.HIGRESS_TEST_IMAGE_TAG }}
- name: Disable containerd image store
run: |
sudo bash -c 'cat > /etc/docker/daemon.json << EOF
{
"features": {
"containerd-snapshotter": false
}
}
EOF'
sudo systemctl restart docker
docker info -f '{{ .DriverStatus }}'
- name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
swap-storage: true
- name: "Setup Go"
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Setup Golang Caches
uses: actions/cache@v4
with:
path: |-
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ github.run_id }}
restore-keys: ${{ runner.os }}-go
- name: Run Higress Gateway API Tests
run: |-
GOPROXY="https://proxy.golang.org,direct" make gateway-conformance-test \
TAG="${HIGRESS_TEST_IMAGE_TAG}" \
HIGRESS_CONFORMANCE_VERSION="${HIGRESS_TEST_IMAGE_TAG}"
- name: Collect Gateway API Test Diagnostics
if: always()
run: |-
mkdir -p out/gateway-api-conformance/diagnostics
kubectl get gatewayclass,gateway,httproute,referencegrant -A -o yaml > out/gateway-api-conformance/diagnostics/resources.yaml 2>&1 || true
kubectl get events -A --sort-by=.lastTimestamp > out/gateway-api-conformance/diagnostics/events.txt 2>&1 || true
kubectl logs -n "${GATEWAY_API_TEST_NAMESPACE}" deployment/higress-controller --all-containers > out/gateway-api-conformance/diagnostics/controller.log 2>&1 || true
kubectl logs -n "${GATEWAY_API_TEST_NAMESPACE}" deployment/higress-gateway --all-containers > out/gateway-api-conformance/diagnostics/gateway.log 2>&1 || true
tools/bin/kind-gateway-api export logs out/gateway-api-conformance/diagnostics/kind --name higress || true
- name: Upload Gateway API Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: gateway-conformance-test
path: out/gateway-api-conformance/
if-no-files-found: warn
- name: Clean Gateway API Test Environment
if: always()
run: make gateway-conformance-test-clean
higress-conformance-test:
runs-on: ubuntu-22.04

View File

@@ -0,0 +1,32 @@
name: "Check golang-filter Envoy Sync"
on:
push:
branches: [main]
paths:
- "plugins/golang-filter/go.mod"
- "envoy/envoy"
- ".gitmodules"
- "tools/hack/check-golang-filter-envoy-sync.sh"
- ".github/workflows/check-golang-filter-envoy-sync.yaml"
pull_request:
branches: ["*"]
paths:
- "plugins/golang-filter/go.mod"
- "envoy/envoy"
- ".gitmodules"
- "tools/hack/check-golang-filter-envoy-sync.sh"
- ".github/workflows/check-golang-filter-envoy-sync.yaml"
workflow_dispatch: ~
jobs:
check-envoy-sync:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
# The check reads the submodule commit from the tree, so submodules
# do not need to be fetched.
submodules: false
- name: "Verify golang-filter Envoy pin matches the envoy/envoy submodule"
run: bash tools/hack/check-golang-filter-envoy-sync.sh

3
.gitignore vendored
View File

@@ -17,3 +17,6 @@ target/
tools/hack/cluster.conf
envoy/1.20
istio/1.12
# Local working notes (design specs, implementation plans) — not for upstream PRs
docs/superpowers/

4
.issue-spec/config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"hostname": "github.com",
"repo": "higress-group/higress"
}

View File

@@ -36,7 +36,9 @@ header:
- 'pkg/ingress/kube/gateway/istio/testdata'
- 'release-notes/**'
- '.cursor/**'
- '.claude/**'
- '.claude/**'
- '.agents/**'
- '.issue-spec/**'
comment: on-failure
dependency:

156
AGENTS.md Normal file
View File

@@ -0,0 +1,156 @@
# AGENTS.md
Guidance for AI agents working in this repository.
Higress is a cloud-native API gateway built on Istio and Envoy. The control
plane extends Istio/pilot (Go); the data plane is Envoy extended with WASM
plugins (Go/Rust/C++/AssemblyScript) and a Go-based `golang-filter`. It supports
Ingress/Gateway API and ships a rich plugin ecosystem (including AI gateway
plugins).
## Repository layout
Top-level directories (all paths relative to repo root):
- `cmd/higress/` — main entrypoint (`main.go`) for the Higress controller binary.
- `pkg/` — core Go control-plane packages: `bootstrap/`, `cert/`, `cmd/`,
`common/`, `config/`, `ingress/` (Ingress/Gateway config translation),
`kube/`.
- `api/` — protobuf/CRD API definitions; Higress CRDs live in
`api/extensions/v1alpha1` (e.g. the `WasmPlugin` type). Generated with
`make gen-api` / `make gen-client` (see `api/gen.sh`, `buf.*`).
- `client/` — generated Go clientset for Higress CRDs.
- `istio/` — git submodules of higress-group forks of Istio (`api`, `istio`,
`client-go`, `pkg`, `proxy`); see `.gitmodules`. Pulled via `make submodule`
(part of `prebuild`).
- `envoy/` — Envoy + `go-control-plane` submodules (higress-group forks).
- `external/` — vendored/external mirror dirs used during build (istio, envoy,
proxy, etc.).
- `plugins/` — all data-plane plugins (see "Plugins" below).
- `registry/` — service-discovery registry integrations (nacos, consul, eureka,
zookeeper, direct, mcp, ...).
- `hgctl/` — the `hgctl` CLI (separate Go module) for managing Higress.
- `helm/` — Helm charts: `helm/core` (the dev/install chart) and `helm/higress`.
- `test/``test/e2e/` (conformance/e2e, see "Build & test") and
`test/gateway/`.
- `tools/` — build/CI scripting: `tools/hack/` (build scripts), `tools/bin/`,
`tools/linter/`, `*.mk`.
- `samples/` — example manifests (gateway-api, hello-world, wasmplugin, ...).
- `docker/`, `docs/`, `release-notes/` — packaging, docs, and release notes.
- `Makefile` — istio common-files wrapper (supports `BUILD_WITH_CONTAINER`);
real targets live in `Makefile.core.mk` (+ `Makefile.overrides.mk`).
## Plugins
All plugins live under `plugins/`. See `plugins/README.md` for the contributor
overview. Prebuilt plugin images are published to
`higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins`.
### plugins/wasm-go/ (primary WASM plugin framework, Go)
- `extensions/<name>/` — one directory per plugin (~59 plugins, many `ai-*`).
Each plugin is its own Go module: `main.go`, `go.mod`/`go.sum`, `VERSION`,
`README.md`(+`README_EN.md`), often `config/`, `util/`, `main_test.go`.
Optional `.buildrc` sets `EXTRA_TAGS`; optional `prepare.sh`/`prepare.sh`.
`plugin.wasm` is a build artifact and is **not** committed.
- Shared SDK: plugins depend on external modules
`github.com/higress-group/wasm-go` and
`github.com/higress-group/proxy-wasm-go-sdk` (NOT an in-repo SDK dir).
In-repo, `plugins/wasm-go/pkg/mcp/` provides MCP helpers and
`plugins/wasm-go/mcp-servers/` holds MCP server plugins.
- `examples/` — minimal reference plugins (custom-log, custom-span-attribute,
test-foreign-function).
- Build: `plugins/wasm-go/Makefile`. `PLUGIN_NAME=<name> make build` builds a
wasm file (output to `extensions/<name>/plugin.wasm`) + image via
`Dockerfile`/`DockerfileBuilder` (uses a `wasm-go-builder` image, Go 1.24,
TinyGo optional). `make build-push` pushes the image; `make local-build`
builds locally with `GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared`.
- Conventions: `VERSION` is the image tag; the CI/e2e batch builder
(`tools/hack/build-wasm-plugins.sh`) only compiles a wasm-go plugin whose
`VERSION` ends in `-alpha` (see the section at the bottom of this file).
### plugins/wasm-rust/ (Rust WASM plugins)
- Workspace-style: root `Cargo.toml`/`Cargo.lock`, shared `src/`,
`extensions/<name>/` per plugin (e.g. `ai-data-masking`, `ai-intent`,
`request-block`, `say-hello`, `demo-wasm`), `example/`.
- Build via `plugins/wasm-rust/Makefile` (`PLUGIN_NAME=<name> make build`, plus
`lint`/`test`); the batch builder runs it when `PLUGIN_TYPE=RUST`.
### plugins/wasm-cpp/ (C++ WASM plugins, Bazel)
- Bazel project: `WORKSPACE`, `BUILD`, `bazel/`, `common/`, `scripts/`,
`extensions/<name>/` (e.g. `basic_auth`, `jwt_auth`, `key_rate_limit`,
`model_router`, ...). Build via `plugins/wasm-cpp/Makefile`
(`PLUGIN_NAME=<name> make build`), invoked with `PLUGIN_TYPE=CPP`.
### plugins/wasm-assemblyscript/ (AssemblyScript WASM plugins)
- Node/AssemblyScript project: `asconfig.json`, `package.json`, `assembly/`,
`extensions/`.
### plugins/golang-filter/ (Envoy Go HTTP filter, NOT WASM)
- A native Envoy Golang HTTP filter (`main.go`, `mcp-server/`, `mcp-session/`);
compiled as a shared object (`.so`) independent of Envoy — no Envoy rebuild
needed. Requires Higress >= 2.1.0. Plugins register in `main.go`'s `init()`
via `RegisterHttpFilterFactoryAndConfigParser`. See
`plugins/golang-filter/README.md`.
- Build: `plugins/golang-filter/Makefile` (docker build, outputs
`golang-filter_<arch>.so`). Wired into the gateway image build via
`Makefile.core.mk` targets `build-golang-filter[-amd64|-arm64]`.
### How plugins are loaded
`WasmPlugin` CRDs (`extensions.higress.io/v1alpha1`) reference a plugin by
`url:` — either `oci://.../plugins/<name>:<version>` (image) or
`file:///opt/plugins/.../plugin.wasm` (local mount used in e2e). The dev install
`make install-dev-wasmplugin` sets Helm `global.volumeWasmPlugins=true` to mount
locally built wasm files into the gateway.
## Build & test
Run targets from the repo root; `Makefile` delegates to `Makefile.core.mk`.
Common ones:
- `make build` / `make build-linux` — build the Higress controller binary
(`prebuild` first fetches submodules).
- `make build-hgctl` — build the `hgctl` CLI.
- `make build-gateway` / `make build-istio` / `make build-envoy` — data-plane
and control-plane images (gateway pulls in the golang-filter).
- `make build-wasmplugins` — runs `tools/hack/build-wasm-plugins.sh` to batch
build WASM plugins (respects `PLUGIN_TYPE` / `PLUGIN_NAME`; Go plugins require
a `-alpha` VERSION).
- `make gen-api` / `make gen-client` — regenerate API/client code.
### Conformance / e2e tests (`test/e2e/`)
- Entrypoint `test/e2e/e2e_test.go`, run with build tag `conformance` and
`--test-area` / `--execute-tests` flags.
- Cases live in `test/e2e/conformance/tests/` as **paired `<name>.go` +
`<name>.yaml`** files (~68 cases; WASM cases are prefixed by language, e.g.
`go-wasm-*`, `cpp-wasm-*`). Support code: `conformance/base/`,
`conformance/utils/`, `conformance/embed.go`.
- Key Make targets (each spins up a kind cluster):
- `make higress-conformance-test` — Ingress/Gateway conformance.
- `make higress-wasmplugin-test` — WASM plugin e2e (uses
`install-dev-wasmplugin`, which builds plugins and mounts them).
- `*-prepare` / `*-skip-docker-build` / `*-clean` variants exist for
iterating; `run-higress-e2e-test[-wasmplugin]` runs `go test` against an
already-prepared cluster (filter with `TEST_SHORTNAME`).
- For the specifics of authoring a wasm-go e2e test, see the section below.
## Writing e2e conformance tests with wasm-go plugins
When adding an e2e conformance test that ships its own wasm-go plugin under
`plugins/wasm-go/extensions/<name>/`:
- The plugin's `VERSION` file **must end in `-alpha`** (e.g. `1.0.0-alpha`).
CI's `tools/hack/build-wasm-plugins.sh` only compiles a wasm-go plugin when
its version ends in `-alpha`; otherwise it silently skips it.
- `plugin.wasm` is a build artifact and is **not** committed. If the plugin
isn't built, the `file:///opt/plugins/.../plugin.wasm` URL in the test's
`WasmPlugin` manifest resolves to a missing file, envoy rejects the wasm
config and fails closed, and every request on that route returns HTTP 500.
Locally this can be masked because a previously built `plugin.wasm` still
exists on disk — so a test can pass locally yet 500 in CI.

79
COMMUNITY.md Normal file
View File

@@ -0,0 +1,79 @@
# Higress Community
This document is the authoritative inventory of official Higress project
communication channels, including channels used by its subprojects. Official
technical and governance decisions are recorded in public GitHub issues,
discussions, pull requests, or published meeting notes.
## Public channels
| Channel | Scope and purpose |
| --- | --- |
| [GitHub Issues](https://github.com/higress-group/higress/issues) | Bug reports, feature requests, and work tracking for the primary repository |
| [GitHub Pull Requests](https://github.com/higress-group/higress/pulls) | Public change proposals, reviews, and decision records |
| [GitHub Discussions](https://github.com/higress-group/higress/discussions) | User questions, ideas, announcements, and longer-form community discussion |
| [Discord](https://discord.gg/tSbww9VDaM) | Public real-time user and contributor chat; decisions arising there must be recorded on GitHub |
| [Higress mailing list](mailto:higress@googlegroups.com) | Community questions and contributor contact by email |
| [Chinese-language community group](./README_ZH.md#%E4%BA%A4%E6%B5%81%E7%BE%A4) | Publicly advertised Chinese-language user and contributor chat |
| [Higress WeChat Official Account](./README_ZH.md#%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB) | Chinese-language technical articles and project announcements; broadcast rather than a decision channel |
| [Higress website and documentation](https://higress.cn/en/) | Published user and contributor documentation and project announcements |
Each subproject uses its own public GitHub issues and pull requests for work
specific to that repository:
| Subproject | Issues | Pull requests |
| --- | --- | --- |
| `higress-console` | [Issues](https://github.com/higress-group/higress-console/issues) | [Pull requests](https://github.com/higress-group/higress-console/pulls) |
| `higress-standalone` | [Issues](https://github.com/higress-group/higress-standalone/issues) | [Pull requests](https://github.com/higress-group/higress-standalone/pulls) |
| `plugin-server` | [Issues](https://github.com/higress-group/plugin-server/issues) | [Pull requests](https://github.com/higress-group/plugin-server/pulls) |
| `wasm-go` | [Issues](https://github.com/higress-group/wasm-go/issues) | [Pull requests](https://github.com/higress-group/wasm-go/pulls) |
Cross-subproject and project-governance decisions are recorded in the primary
Higress repository.
## Non-public channels
Non-public channels are limited to reports whose confidentiality protects
reporters or users:
| Channel | Special purpose |
| --- | --- |
| [GitHub Private Security Advisories](https://github.com/higress-group/higress/security/advisories/new) | One of the two required confidential vulnerability-reporting channels; used for project triage, remediation, and disclosure coordination under [`SECURITY.md`](./SECURITY.md) |
| [Alibaba Security Response Center](https://security.alibaba.com/) | The second required confidential vulnerability-reporting channel; the same substantive report must be submitted here and correlated with the GitHub advisory by the Security Response Team |
| [CNCF Code of Conduct reporting](mailto:conduct@cncf.io) | Confidential Code of Conduct incident reporting under [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) |
| [CNCF TOC private mailing list](mailto:cncf-private-toc@lists.cncf.io) | Confidential escalation when project leadership cannot resolve a security or governance conflict without conflicted participants |
Personal messages, employer-internal systems, and informal maintainer chats are
not official project decision channels. If they inform project work, the
non-sensitive decision and rationale must be recorded publicly.
## Community meetings
Higress does not currently run a recurring public community meeting and does
not yet have a CNCF calendar entry. Establishing an up-to-date public meeting
scheduler and/or CNCF calendar integration is tracked as an Incubation
readiness item in
[`docs/cncf/governance-review.md`](./docs/cncf/governance-review.md).
When a recurring meeting is established, its schedule and joining information
will be published here and on the CNCF calendar. Agendas, notes, recordings,
and decisions will be public, with confidential security and Code of Conduct
matters excluded.
## Contributor activity and recruitment
Public, continuously updated evidence is available through:
- [GitHub contributor activity](https://github.com/higress-group/higress/graphs/contributors)
- [Recent repository activity](https://github.com/higress-group/higress/pulse)
- [CNCF DevStats for Higress](https://higress.devstats.cncf.io/)
- [`help wanted` issues](https://github.com/higress-group/higress/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22)
- [`good first issue` issues](https://github.com/higress-group/higress/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- the contributor path in [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md)
- the active-maintainer evidence and annual roster review in
[`MAINTAINERS.md`](./MAINTAINERS.md)
Contributors can start through an issue, discussion, documentation update, bug
fix, test, plugin, or other pull request. Maintainers and code owners recruit
and mentor contributors through the public channels above and identify
approachable work with contribution labels.

View File

@@ -19,22 +19,109 @@ Higress governance is guided by the following values:
## Roles
Higress role definitions are documented in:
Higress has the following project-wide roles:
- **Contributor**: anyone who participates through issues, discussions,
documentation, code, testing, or other community work. Contributors do not
need a formal appointment.
- **Code owner**: a contributor delegated to review changes in one or more
paths in [`CODEOWNERS`](./CODEOWNERS). Code owners assess technical quality,
request changes, and help maintain their assigned area. Code ownership does
not grant project-wide governance authority or a permanent seat. A code
owner is added or removed by a public pull request updating `CODEOWNERS`,
using the decision process below. Maintainers remain accountable for the
resulting ownership coverage and may merge only in accordance with the
repository's configured permissions and review rules.
- **Maintainer**: a project-wide leadership role responsible for technical
direction, releases, governance, community health, and the delegation of
code ownership. Maintainers are not limited to the paths where they are
named as code owners.
The current roster, responsibilities, and lifecycle are documented in:
- [`MAINTAINERS.md`](./MAINTAINERS.md)
- [`CODEOWNERS`](./CODEOWNERS)
- [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md)
## Project Scope and Subprojects
The following repositories comprise the CNCF Higress project. Unless a
subproject documents an additional local rule, this governance and the
project-wide maintainer roster apply to it.
| Repository | Responsibility | Status |
| --- | --- | --- |
| [`higress-group/higress`](https://github.com/higress-group/higress) | Core control plane, data plane integration, APIs, Helm charts, CLI, plugins, documentation, and releases | Active; primary repository |
| [`higress-group/higress-console`](https://github.com/higress-group/higress-console) | Web management console | Active subproject |
| [`higress-group/higress-standalone`](https://github.com/higress-group/higress-standalone) | Standalone and local deployment packaging | Active subproject |
| [`higress-group/plugin-server`](https://github.com/higress-group/plugin-server) | Plugin distribution service used by Higress | Active subproject |
| [`higress-group/wasm-go`](https://github.com/higress-group/wasm-go) | Go SDK for Higress Wasm plugins | Active subproject |
Git submodules declared in [`.gitmodules`](./.gitmodules), including the
Higress-hosted forks of Istio, Envoy, and their related API and client
libraries, are pinned source dependencies used to build Higress; they are not
separately governed Higress subprojects. Other upstream dependency forks,
integrations, examples, experiments, websites, and repositories in the
`higress-group` organization are also not Higress subprojects unless they are
added to this table.
A proposal to add, remove, transfer, or archive a subproject must be made in a
public issue or pull request. It must identify the repository, purpose,
maintainers or delegated code owners, contribution path, communication
channels, and lifecycle status. The proposal follows the project-direction
decision process below. An archived or removed subproject remains listed here
with its final status and successor, if any.
## Decision Making
Higress uses **lazy consensus** by default.
When consensus cannot be reached, maintainers may start a vote on a public
issue or pull request. A simple majority of votes cast decides the outcome.
Technical proposals, contribution acceptance, project goals, leadership and
role assignments, subproject changes, requests made on behalf of Higress to
the CNCF, and governance changes are discussed in public issues or pull
requests. Maintainers are responsible for ensuring that significant decisions
record their rationale and outcome in the relevant public thread.
When consensus cannot be reached, a maintainer may start a vote on the public
issue or pull request. A simple majority of non-conflicted maintainer votes
cast decides the outcome. A maintainer with a material personal or employer
conflict must disclose it and abstain from the decision. If all maintainers are
conflicted, the project will request guidance from the CNCF TOC.
For governance or project direction changes, maintainers should allow adequate
time for public discussion before finalizing decisions.
## Vendor Neutrality
Higress project direction and governance are independent of any single
company. Maintainer and code-owner roles are held by individuals, not reserved
for employers. No employer has a guaranteed seat, veto, or preferred decision
weight. Contributions, integrations, defaults, and roadmap priorities are
evaluated on community and technical merit. Company-specific products may be
supported, but they do not receive privileged governance treatment.
Affiliation changes do not by themselves add or remove a role. Maintainers
disclose affiliations in `MAINTAINERS.md` and disclose material conflicts in
the public decision record.
## Function-Based Teams
Maintainers may create a function-based team, such as a release or security
team, through a public governance pull request. The change must document the
team's purpose, authority, membership or selection method, onboarding,
conflict handling, removal, and retirement. Sensitive operational details may
remain private, but the team's mandate and decision authority must be public.
The current Security Response Team and its private report-handling process are
defined in [`SECURITY.md`](./SECURITY.md).
## Community and Meetings
The authoritative inventory of public and non-public communication channels,
subproject channels, contribution activity, and meeting information is in
[`COMMUNITY.md`](./COMMUNITY.md). Project decisions must be recorded in a
public issue, discussion, pull request, or published meeting notes even when
preliminary conversation occurred elsewhere.
## Governance Updates
Changes to this document are made through pull requests and approved by

View File

@@ -2,24 +2,49 @@
This file lists the current maintainers of the Higress project.
A maintainer is a contributor who is actively responsible for guiding the
direction of the project, reviewing and merging contributions, and
participating in project governance.
A maintainer is a contributor who is actively responsible for project-wide
technical direction, releases, governance, community health, and the
delegation of code ownership across the Higress project and its subprojects.
Maintainer authority is held by individuals and is not tied to an employer.
For the day-to-day code-review ownership of individual subdirectories,
see [`CODEOWNERS`](./CODEOWNERS).
## Maintainers
| Name | GitHub | Affiliation |
| -------------- | ------------------------------------------------------- | ----------------- |
| Yiquan Dong | [@CH3CHO](https://github.com/CH3CHO) | Trip.com |
| Yuanxiao Zhao | [@EndlessSeeker](https://github.com/EndlessSeeker) | Alibaba Cloud |
| Leilei Geng | [@gengleilei](https://github.com/gengleilei) | Alibaba Cloud |
| Xiantao Han | [@hanxiantao](https://github.com/hanxiantao) | XinYe Technology |
| Zhiwei Cheng | [@cr7258](https://github.com/cr7258) | NVIDIA |
| Tianyi Zhang | [@johnlanni](https://github.com/johnlanni) | Alibaba Cloud |
| Jingfeng Xu | [@lexburner](https://github.com/lexburner) | Alibaba Cloud |
| Name | GitHub contact | Domain of responsibility | Affiliation |
| --- | --- | --- | --- |
| Yiquan Dong | [@CH3CHO](https://github.com/CH3CHO) | Project-wide governance, technical direction, releases, and community stewardship | Trip.com |
| Yuanxiao Zhao | [@EndlessSeeker](https://github.com/EndlessSeeker) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
| Leilei Geng | [@gengleilei](https://github.com/gengleilei) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
| Xiantao Han | [@hanxiantao](https://github.com/hanxiantao) | Project-wide governance, technical direction, releases, and community stewardship | XinYe Technology |
| Zhiwei Cheng | [@cr7258](https://github.com/cr7258) | Project-wide governance, technical direction, releases, and community stewardship | NVIDIA |
| Tianyi Zhang | [@johnlanni](https://github.com/johnlanni) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
| Jingfeng Xu | [@lexburner](https://github.com/lexburner) | Project-wide governance, technical direction, releases, and community stewardship | Alibaba Cloud |
More specific day-to-day review responsibilities are delegated through
[`CODEOWNERS`](./CODEOWNERS); they do not narrow a maintainer's project-wide
responsibility.
## Activity and roster review
A maintainer is active when they have participated in project review,
development, releases, issue triage, community support, or governance during
the preceding 12 months and remain willing to perform the role. The roster is
reviewed in a public pull request at least annually. The review checks public
activity, current affiliation and contact information, and asks maintainers
whose status is unclear to confirm their availability.
The 2026 roster review used public GitHub issue and pull-request activity for
each maintainer:
- [Yiquan Dong](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3ACH3CHO)
- [Yuanxiao Zhao](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3AEndlessSeeker)
- [Leilei Geng](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Agengleilei)
- [Xiantao Han](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Ahanxiantao)
- [Zhiwei Cheng](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Acr7258)
- [Tianyi Zhang](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Ajohnlanni)
- [Jingfeng Xu](https://github.com/higress-group/higress/issues?q=updated%3A%3E%3D2025-07-21+involves%3Alexburner)
## Becoming a maintainer
@@ -28,7 +53,29 @@ Higress follows the contribution and graduation paths described in
contributors who consistently demonstrate good technical judgement and
community stewardship can be nominated as maintainers by an existing
maintainer; nominations are accepted by lazy consensus among the current
maintainers.
maintainers. The nomination pull request must update this roster and state the
candidate's public contribution history, domain of responsibility, contact,
and affiliation.
## Leaving or changing maintainer status
- A maintainer may step down by opening or approving a pull request that moves
them to the emeritus list or removes them from the roster.
- A maintainer who has no qualifying activity for 12 months will be contacted
publicly where practical and given at least 30 days to confirm whether they
want to resume the role, move to emeritus, or step down. If there is no
response, another maintainer may propose the change through lazy consensus.
- A maintainer may be removed for sustained failure to perform the role or for
a Code of Conduct violation. The proposal follows the governance decision
process, excludes conflicted maintainers, and preserves confidential details
where required by the Code of Conduct process.
- Emeritus maintainers retain recognition but no maintainer authority or
required repository access. An emeritus or former maintainer may return
through the same public nomination process used for a new maintainer.
## Emeritus maintainers
There are currently no emeritus maintainers.
## Reporting issues

View File

@@ -21,6 +21,21 @@ GO ?= go
export GOPROXY ?= https://proxy.golang.org,direct
GATEWAY_API_VERSION ?= v1.4.0
GATEWAY_CONFORMANCE_PROFILE ?= GATEWAY-HTTP
GATEWAY_CONFORMANCE_SUPPORTED_FEATURES ?= Gateway,HTTPRoute,ReferenceGrant
GATEWAY_CONFORMANCE_REPORT ?= out/gateway-api-conformance/report.yaml
GATEWAY_CONFORMANCE_CONTACT ?= https://github.com/alibaba/higress/issues
GATEWAY_CONFORMANCE_RUN_TEST ?=
GATEWAY_CONFORMANCE_ALLOW_CRDS_MISMATCH ?= false
GATEWAY_API_TEST_NAMESPACE ?= gateway-conformance-infra
GATEWAY_API_GATEWAY_SERVICE_TYPE ?= ClusterIP
GATEWAY_API_DIAL_LOCALHOST ?= true
GATEWAY_API_LOCAL_HTTP_PORT ?= 80
GATEWAY_API_LOCAL_HTTPS_PORT ?= 443
GATEWAY_API_KIND_NODE_TAG ?= v1.34.0@sha256:7416a61b42b1662ca6ca89f02028ac133a309a2a30ba309614e8ec94d976dc5a
HIGRESS_CONFORMANCE_VERSION ?= $(shell git rev-parse HEAD)
TARGET_ARCH ?= amd64
VALID_ARCHS := amd64 arm64
@@ -151,7 +166,7 @@ docker-buildx-push: clean-env docker.higress-buildx
export PARENT_GIT_TAG:=$(shell cat VERSION)
export PARENT_GIT_REVISION:=$(TAG)
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.2.3/envoy-symbol-ARCH.tar.gz
export ENVOY_PACKAGE_URL_PATTERN?=https://github.com/higress-group/proxy/releases/download/v2.2.4-rc.2-test-cpp-host/envoy-symbol-ARCH.tar.gz
build-envoy: prebuild
./tools/hack/build-envoy.sh
@@ -205,11 +220,15 @@ install: pre-install
helm install higress helm/higress -n higress-system --create-namespace --set 'global.local=true'
HIGRESS_LATEST_IMAGE_TAG ?= latest
ENVOY_LATEST_IMAGE_TAG ?= fcc50202f47e27f6b8391a4bd9bbc0a9127d89d7
ENVOY_LATEST_IMAGE_TAG ?= 481184afc44176eb23d64e0011dc3ea1ae6a410c
ISTIO_LATEST_IMAGE_TAG ?= de2c9628294f51b13c4a70b3a862b4372890797a
install-dev: pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true'
install-dev-gateway-api: pre-install
helm install higress helm/core -n $(GATEWAY_API_TEST_NAMESPACE) --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'gateway.service.type=$(GATEWAY_API_GATEWAY_SERVICE_TYPE)'
install-dev-wasmplugin: build-wasmplugins pre-install
helm install higress helm/core -n higress-system --create-namespace --set 'controller.tag=$(TAG)' --set 'gateway.replicas=1' --set 'pilot.tag=$(ISTIO_LATEST_IMAGE_TAG)' --set 'gateway.tag=$(ENVOY_LATEST_IMAGE_TAG)' --set 'global.local=true' --set 'global.volumeWasmPlugins=true' --set 'global.onlyPushRouteCluster=false'
@@ -262,9 +281,71 @@ clean: clean-higress clean-gateway clean-istio clean-env clean-tool
include tools/tools.mk
include tools/lint.mk
# gateway-conformance-test runs gateway api conformance tests.
# install-gateway-api-crds installs the Gateway API CRDs used by the conformance suite.
.PHONY: install-gateway-api-crds
install-gateway-api-crds:
kubectl apply --server-side=true -f https://github.com/kubernetes-sigs/gateway-api/releases/download/$(GATEWAY_API_VERSION)/standard-install.yaml
kubectl wait --for=condition=Established crd/gatewayclasses.gateway.networking.k8s.io --timeout=120s
kubectl wait --for=condition=Established crd/gateways.gateway.networking.k8s.io --timeout=120s
kubectl wait --for=condition=Established crd/httproutes.gateway.networking.k8s.io --timeout=120s
kubectl wait --for=condition=Established crd/referencegrants.gateway.networking.k8s.io --timeout=120s
# create-gateway-api-cluster creates the Kubernetes version used by Gateway API v1.4.0 tests.
.PHONY: create-gateway-api-cluster
create-gateway-api-cluster: $(tools/kind-gateway-api)
KIND=$(tools/kind-gateway-api) KIND_NODE_TAG=$(GATEWAY_API_KIND_NODE_TAG) tools/hack/create-cluster.sh
# delete-gateway-api-cluster deletes the Gateway API test cluster.
.PHONY: delete-gateway-api-cluster
delete-gateway-api-cluster: $(tools/kind-gateway-api)
$(tools/kind-gateway-api) delete cluster --name higress
# kube-load-gateway-api-images loads only the images required by the Gateway API tests.
.PHONY: kube-load-gateway-api-images
kube-load-gateway-api-images: $(tools/kind-gateway-api)
KIND=$(tools/kind-gateway-api) tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress $(TAG)
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
tools/hack/docker-pull-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)
KIND=$(tools/kind-gateway-api) tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/pilot $(ISTIO_LATEST_IMAGE_TAG)
KIND=$(tools/kind-gateway-api) tools/hack/kind-load-image.sh higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway $(ENVOY_LATEST_IMAGE_TAG)
# gateway-conformance-test-prepare prepares a kind cluster for Gateway API tests.
.PHONY: gateway-conformance-test-prepare
gateway-conformance-test-prepare: delete-gateway-api-cluster create-gateway-api-cluster install-gateway-api-crds docker-build kube-load-gateway-api-images install-dev-gateway-api
kubectl wait --timeout=10m -n $(GATEWAY_API_TEST_NAMESPACE) deployment/higress-controller --for=condition=Available
kubectl wait --timeout=10m -n $(GATEWAY_API_TEST_NAMESPACE) deployment/higress-gateway --for=condition=Available
kubectl wait --timeout=10m gatewayclass/higress --for=condition=Accepted
# run-gateway-conformance-test runs the upstream Gateway API Conformance Suite.
.PHONY: run-gateway-conformance-test
run-gateway-conformance-test:
mkdir -p $(dir $(GATEWAY_CONFORMANCE_REPORT))
HIGRESS_GATEWAY_API_TEST_DIAL_LOCALHOST=$(GATEWAY_API_DIAL_LOCALHOST) \
HIGRESS_GATEWAY_API_TEST_LOCAL_HTTP_PORT=$(GATEWAY_API_LOCAL_HTTP_PORT) \
HIGRESS_GATEWAY_API_TEST_LOCAL_HTTPS_PORT=$(GATEWAY_API_LOCAL_HTTPS_PORT) go test -v ./test/gateway \
-run '^TestGatewayAPIConformance$$' \
-args \
--gateway-class=higress \
--supported-features=$(GATEWAY_CONFORMANCE_SUPPORTED_FEATURES) \
--conformance-profiles=$(GATEWAY_CONFORMANCE_PROFILE) \
--organization=alibaba \
--project=higress \
--url=https://github.com/alibaba/higress \
--version=$(HIGRESS_CONFORMANCE_VERSION) \
--contact=$(GATEWAY_CONFORMANCE_CONTACT) \
--mode=default \
--cleanup-base-resources=false \
--allow-crds-mismatch=$(GATEWAY_CONFORMANCE_ALLOW_CRDS_MISMATCH) \
$(if $(GATEWAY_CONFORMANCE_RUN_TEST),--run-test=$(GATEWAY_CONFORMANCE_RUN_TEST),) \
--report-output=$(abspath $(GATEWAY_CONFORMANCE_REPORT))
# gateway-conformance-test runs Gateway API tests as a standard Higress integration test.
.PHONY: gateway-conformance-test
gateway-conformance-test:
gateway-conformance-test: gateway-conformance-test-prepare run-gateway-conformance-test
# gateway-conformance-test-clean deletes the Gateway API test cluster.
.PHONY: gateway-conformance-test-clean
gateway-conformance-test-clean: delete-gateway-api-cluster
# higress-conformance-test-prepare prepares the environment for higress conformance tests.
.PHONY: higress-conformance-test-prepare

View File

@@ -8,8 +8,8 @@
<div align="center">
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![Build Status](https://github.com/higress-group/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/higress-group/higress/actions)
[![license](https://img.shields.io/github/license/higress-group/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![discord](https://img.shields.io/discord/1364956090566971515?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=flat-square)](https://discord.gg/tSbww9VDaM)
[![CNCF Sandbox](https://img.shields.io/badge/CNCF-Sandbox-30638E?logo=linuxfoundation&logoColor=white)](https://www.cncf.io/projects/)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12667/badge)](https://www.bestpractices.dev/projects/12667)
@@ -21,6 +21,7 @@
[**Official Site**](https://higress.ai/en/) &nbsp; |
&nbsp; [**Docs**](https://higress.cn/en/docs/latest/overview/what-is-higress/) &nbsp; |
&nbsp; [**Blog**](https://higress.cn/en/blog/) &nbsp; |
&nbsp; [**Roadmap**](./ROADMAP.md) &nbsp; |
&nbsp; [**MCP Server QuickStart**](https://higress.cn/en/ai/mcp-quick-start/) &nbsp; |
&nbsp; [**Developer Guide**](https://higress.cn/en/docs/latest/dev/architecture/) &nbsp; |
&nbsp; [**Wasm Plugin Hub**](https://higress.cn/en/plugin/) &nbsp; |
@@ -35,19 +36,19 @@ Higress is a cloud-native API gateway based on Istio and Envoy, which can be ext
### Core Use Cases
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
Higress's AI gateway capabilities support all [mainstream model providers](https://github.com/higress-group/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider) both domestic and international. It also supports hosting MCP (Model Context Protocol) Servers through its plugin mechanism, enabling AI Agents to easily call various tools and services. With the [openapi-to-mcp tool](https://github.com/higress-group/openapi-to-mcpserver), you can quickly convert OpenAPI specifications into remote MCP servers for hosting. Higress provides unified management for both LLM API and MCP API.
**🌟 Try it now at [https://mcp.higress.ai/](https://mcp.higress.ai/)** to experience Higress-hosted Remote MCP Servers firsthand:
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### Enterprise Adoption
### Production Adoption
Higress was born within Alibaba to solve the issues of Tengine reload affecting long-connection services and insufficient load balancing capabilities for gRPC/Dubbo. Within Alibaba Cloud, Higress's AI gateway capabilities support core AI applications such as Tongyi Bailian model studio, machine learning PAI platform, and other critical AI services. Alibaba Cloud has built its cloud-native API gateway product based on Higress, providing 99.99% gateway high availability guarantee service capabilities for a large number of enterprise customers.
You can click the button below to install the enterprise version of Higress:
[![Deploy on AlibabaCloud](https://img.alicdn.com/imgextra/i1/O1CN01e6vwe71EWTHoZEcpK_!!6000000000359-55-tps-170-40.svg)](https://www.aliyun.com/product/api-gateway?spm=higress-github.topbar.0.0.0)
Higress originated at Alibaba to address long-connection disruption during
gateway reloads and improve gRPC/Dubbo load balancing. It is now developed as
a vendor-neutral CNCF project and is used by organizations across multiple
industries. Public adopters and their use cases are listed in
[`ADOPTERS.md`](./ADOPTERS.md).
## Summary
@@ -77,8 +78,9 @@ Port descriptions:
- Port 8080: Gateway HTTP protocol entry
- Port 8443: Gateway HTTPS protocol entry
> All Higress Docker images use Higress's own image repository and are not affected by Docker Hub rate limits.
> In addition, the submission and updates of the images are protected by a security scanning mechanism (powered by Alibaba Cloud ACR), making them very secure for use in production environments.
> Higress publishes project images through dedicated regional registry
> endpoints. Operators may mirror the images to a registry they control and
> configure the Helm `global.hub` value accordingly.
>
> If you experience a timeout when pulling image from `higress-registry.cn-hangzhou.cr.aliyuncs.com`, you can try replacing it with the following docker registry mirror source:
>
@@ -100,9 +102,6 @@ Port descriptions:
For other installation methods such as Helm deployment under K8s, please refer to the official [Quick Start documentation](https://higress.ai/en/docs/latest/user/quickstart/).
If you are deploying on the cloud, it is recommended to use the [Enterprise Edition](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
## Use Cases
- **MCP Server Hosting**:
@@ -185,6 +184,10 @@ Join our Discord community! This is where you can connect with developers and ot
[![discord](https://img.shields.io/discord/1364956090566971515?color=5865F2&label=discord&labelColor=black&logo=discord&logoColor=white&style=for-the-badge)](https://discord.gg/tSbww9VDaM)
The complete inventory of public and private communication channels,
subproject channels, meeting information, and contributor activity is in
[`COMMUNITY.md`](./COMMUNITY.md).
### Code of Conduct
The Higress community follows the
@@ -198,7 +201,9 @@ participating in the community.
Project governance, the maintainer roster, and the contribution model are
described in [`GOVERNANCE.md`](./GOVERNANCE.md) and
[`MAINTAINERS.md`](./MAINTAINERS.md). New contributors are encouraged to start
with [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md).
with [`CONTRIBUTING_EN.md`](./CONTRIBUTING_EN.md). Forward planning and release
procedures are documented in [`ROADMAP.md`](./ROADMAP.md) and
[`RELEASE.md`](./RELEASE.md).
### Security
@@ -218,13 +223,13 @@ Higress would not be possible without the valuable open-source work of projects
### Contributors
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
<a href="https://github.com/higress-group/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=higress-group/higress"/>
</a>
### Star History
[![Star History Chart](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![Star History Chart](https://api.star-history.com/svg?repos=higress-group/higress&type=Date)](https://star-history.com/#higress-group/higress&Date)
---

View File

@@ -6,8 +6,8 @@
</h1>
<h4 align="center"> AIネイティブAPIゲートウェイ </h4>
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![Build Status](https://github.com/higress-group/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/higress-group/higress/actions)
[![license](https://img.shields.io/github/license/higress-group/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![CNCF Sandbox](https://img.shields.io/badge/CNCF-Sandbox-30638E?logo=linuxfoundation&logoColor=white)](https://www.cncf.io/projects/)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12667/badge)](https://www.bestpractices.dev/projects/12667)
@@ -30,15 +30,19 @@ Higressは、IstioとEnvoyをベースにしたクラウドネイティブAPIゲ
### 主な使用シナリオ
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCPModel Context Protocolサーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
HigressのAIゲートウェイ機能は、国内外のすべての[主要モデルプロバイダー](https://github.com/higress-group/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)をサポートし、vllm/ollamaなどに基づく自己構築DeepSeekモデルにも対応しています。また、プラグインメカニズムを通じてMCPModel Context Protocolサーバーをホストすることもでき、AI Agentが様々なツールやサービスを簡単に呼び出せるようにします。[openapi-to-mcpツール](https://github.com/higress-group/openapi-to-mcpserver)を使用すると、OpenAPI仕様を迅速にリモートMCPサーバーに変換してホスティングできます。HigressはLLM APIとMCP APIの統一管理を提供します。
**🌟 今すぐ[https://mcp.higress.ai/](https://mcp.higress.ai/)で体験**してください。HigressがホストするリモートMCPサーバーを直接体験できます:
![Higress MCP Server Platform](https://img.alicdn.com/imgextra/i2/O1CN01nmVa0a1aChgpyyWOX_!!6000000003294-0-tps-3430-1742.jpg)
### 企業での採用
### 本番環境での採用
Higressは、Tengineのリロード長時間接続のビジネスに影響を与える問題や、gRPC/Dubboの負荷分散能力の不足を解決するために、Alibaba内部で誕生しました。Alibaba Cloud内では、HigressのAIゲートウェイ機能がTongyi Qianwen APP、Tongyi Bailian Model Studio、機械学習PAIプラットフォームなどの中核的なAIアプリケーションをサポートしています。また、国内の主要なAIGC企業ZeroOneやAI製品FastGPTにもサービスを提供しています。Alibaba Cloudは、Higressを基盤にクラウドネイティブAPIゲートウェイ製品を構築し、多くの企業顧客に99.99%のゲートウェイ高可用性保証サービスを提供しています。
Higressは、ゲートウェイのリロードによる長時間接続への影響と
gRPC/Dubboの負荷分散上の課題を解決するため、Alibabaで誕生しました。
現在はベンダーニュートラルなCNCFプロジェクトとしてコミュニティにより
開発され、複数業界の組織で利用されています。公開されている採用組織と
ユースケースは[`ADOPTERS.md`](./ADOPTERS.md)を参照してください。
## 目次
@@ -232,13 +236,13 @@ WeChat公式アカウント
### 貢献者
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
<a href="https://github.com/higress-group/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=higress-group/higress"/>
</a>
### スターの歴史
[![スターの歴史チャート](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![スターの歴史チャート](https://api.star-history.com/svg?repos=higress-group/higress&type=Date)](https://star-history.com/#higress-group/higress&Date)
---

View File

@@ -8,8 +8,8 @@
<div align="center">
[![Build Status](https://github.com/alibaba/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/alibaba/higress/actions)
[![license](https://img.shields.io/github/license/alibaba/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![Build Status](https://github.com/higress-group/higress/actions/workflows/build-and-test.yaml/badge.svg?branch=main)](https://github.com/higress-group/higress/actions)
[![license](https://img.shields.io/github/license/higress-group/higress.svg)](https://www.apache.org/licenses/LICENSE-2.0.html)
[![CNCF Sandbox](https://img.shields.io/badge/CNCF-Sandbox-30638E?logo=linuxfoundation&logoColor=white)](https://www.cncf.io/projects/)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12667/badge)](https://www.bestpractices.dev/projects/12667)
@@ -37,7 +37,7 @@ Higress 是一款云原生 API 网关,内核基于 Istio 和 Envoy可以用
### 核心使用场景
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/alibaba/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型。同时Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理。
Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://github.com/higress-group/higress/tree/main/plugins/wasm-go/extensions/ai-proxy/provider)和基于 vllm/ollama 等自建的 DeepSeek 模型。同时Higress 支持通过插件方式托管 MCP (Model Context Protocol) 服务器,使 AI Agent 能够更容易地调用各种工具和服务。借助 [openapi-to-mcp 工具](https://github.com/higress-group/openapi-to-mcpserver),您可以快速将 OpenAPI 规范转换为远程 MCP 服务器进行托管。Higress 提供了对 LLM API 和 MCP API 的统一管理。
**🌟 立即体验 [https://mcp.higress.ai/](https://mcp.higress.ai/)** 基于 Higress 托管的远程 MCP 服务器:
@@ -45,13 +45,10 @@ Higress 的 AI 网关能力支持国内外所有[主流模型供应商](https://
### 生产环境采用
Higress 在阿里内部为解决 Tengine reload 对长连接业务有损,以及 gRPC/Dubbo 负载均衡能力不足而诞生。在阿里云内部Higress 的 AI 网关能力支撑了通义千问 APP、通义百炼模型工作室、机器学习 PAI 平台等核心 AI 应用。同时服务国内头部的 AIGC 企业(如零一万物),以及 AI 产品(如 FastGPT。阿里云基于 Higress 构建了云原生 API 网关产品,为大量企业客户提供 99.99% 的网关高可用保障服务能力。
可以点下方按钮安装企业版 Higress:
[![Deploy on AlibabaCloud](https://img.alicdn.com/imgextra/i4/O1CN01tHRaNm22hflDqxKV5_!!6000000007152-55-tps-170-40.svg)](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
如果您使用开源的Higress并希望获得企业级支持可以联系johnlanni的邮箱zty98751@alibaba-inc.com或社交媒体账号微信号nomadao钉钉号chengtanzty。添加好友时请备注Higress :
Higress 最初在阿里内部为解决网关 reload 对长连接业务有损,以及
gRPC/Dubbo 负载均衡能力不足而诞生。Higress 目前作为厂商中立的 CNCF
项目由社区共同开发,并已被多个行业的组织采用。公开采用者和使用场景见
[`ADOPTERS.md`](./ADOPTERS.md)。
## Summary
@@ -80,7 +77,8 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
- 8080 端口:网关 HTTP 协议入口
- 8443 端口:网关 HTTPS 协议入口
**Higress 的所有 Docker 镜像都一直使用自己独享的仓库,不受 Docker Hub 境内访问受限的影响**
**Higress 通过多个区域的项目镜像仓库发布镜像。使用者也可以将镜像同步到
自己控制的仓库,并通过 Helm 的 `global.hub` 参数进行配置。**
> 如果从 `higress-registry.cn-hangzhou.cr.aliyuncs.com` 拉取镜像超时,可以尝试使用以下镜像加速源:
>
@@ -102,8 +100,6 @@ docker run -d --rm --name higress-ai -v ${PWD}:/data \
K8s 下使用 Helm 部署等其他安装方式可以参考官网 [Quick Start 文档](https://higress.cn/docs/latest/user/quickstart/)。
如果您是在云上部署,推荐使用[企业版](https://www.aliyun.com/product/apigateway?spm=higress-github.topbar.0.0.0)
## 使用场景
- **AI 网关**:
@@ -261,13 +257,13 @@ Higress 社区遵循 [**CNCF Code of Conduct**](https://github.com/cncf/foundati
### 贡献者
<a href="https://github.com/alibaba/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=alibaba/higress"/>
<a href="https://github.com/higress-group/higress/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=higress-group/higress"/>
</a>
### Star History
[![Star History](https://api.star-history.com/svg?repos=alibaba/higress&type=Date)](https://star-history.com/#alibaba/higress&Date)
[![Star History](https://api.star-history.com/svg?repos=higress-group/higress&type=Date)](https://star-history.com/#higress-group/higress&Date)
---

112
RELEASE.md Normal file
View File

@@ -0,0 +1,112 @@
# Higress Release Process
This document defines the project-wide process for producing a Higress core
release from the primary repository. Subprojects may add repository-specific
steps, but must use the same public approval and security principles.
## Versioning and support
Higress uses [Semantic Versioning](https://semver.org/) with Git tags in the
form `vMAJOR.MINOR.PATCH`. A release-candidate tag may add an `-rc.N` suffix.
- A **major** release may contain incompatible changes and requires migration
guidance.
- A **minor** release adds backward-compatible features and may contain
announced deprecations.
- A **patch** release contains backward-compatible fixes, including security
fixes when appropriate.
The supported major-version lines are listed in [`SECURITY.md`](./SECURITY.md).
A proposal to end support for a major line is made publicly through a pull
request updating `SECURITY.md`, normally at least 90 days before the stated end
of support. Already unsupported versions do not receive routine fixes.
## Roles and authorization
A maintainer acts as **release manager** for each release. The release manager
coordinates the tracking issue, version changes, validation, tag, automation,
and announcement. At least one other unconflicted maintainer reviews and
approves the release pull request. Only maintainers with the required GitHub
and protected-environment access may create release tags or approve publishing
jobs.
Security releases additionally follow [`SECURITY.md`](./SECURITY.md). Embargoed
details stay in the private advisory until the Security Response Team approves
disclosure.
## 1. Plan the release
The release manager opens or identifies a public tracking issue for a normal
release. It records:
- the target version and release type;
- intended scope and linked changes;
- the release manager and expected timing;
- known compatibility, migration, deprecation, and security considerations;
- validation status and unresolved blockers.
An embargoed security release uses a private advisory for planning until public
disclosure is safe.
## 2. Prepare the release pull request
Create a release pull request against `main` that keeps the version in
`VERSION`, `helm/core/Chart.yaml`, `helm/higress/Chart.yaml`, dependency
metadata, and release notes consistent. Update documentation, migration or
deprecation guidance, and supported-version information when they change.
The pull request must pass the repository's required build, unit, race,
conformance, plugin, license, and other configured checks relevant to its
changes. The release manager records any intentionally inapplicable check or
accepted exception in the pull request. A known release-blocking regression,
unresolved critical vulnerability, inconsistent version, or missing migration
guidance blocks the release.
The approving maintainer verifies that the release scope matches the public
plan, required checks passed, versioned dependencies are intentional, and the
release notes accurately describe user-visible changes.
## 3. Tag and publish
After the release pull request is merged, the release manager creates the
corresponding immutable `vMAJOR.MINOR.PATCH` or release-candidate tag from the
reviewed commit. Published tags are not moved or reused.
The tag triggers GitHub Actions that publish, as applicable:
- controller, pilot, and gateway container images;
- Helm charts and chart indexes;
- `hgctl` archives for supported platforms;
- generated CRDs;
- standalone distribution artifacts;
- GitHub release notes and repository release notes.
Manual workflow dispatch is for recovery or a documented exceptional case; it
does not bypass release approval or change the commit being released.
## 4. Verify and announce
The release manager verifies that publishing workflows succeeded and that the
release page, checksums or archives, image tags, Helm charts, CRDs, and release
notes refer to the intended version. Installation and representative gateway
traffic are smoke-tested with the published artifacts before the release is
announced as ready.
Release announcements link the GitHub release and call out breaking changes,
deprecations, migrations, known issues, and security impact. Confirmed security
issues are disclosed through a GitHub Security Advisory and CVE when
appropriate.
## Failure, rollback, and correction
If validation or publishing fails, stop promotion and document the failure in
the tracking issue or release pull request. Do not move or overwrite an
existing public tag. Fix the problem through normal review and publish a new
release-candidate or patch version.
An operator rollback uses the previously pinned Helm chart, configuration, and
images, normally through `helm rollback`. Because CRD schemas and stored
resources may not be reversible, release notes must identify migrations that
need special rollback handling. The project may withdraw a compromised
artifact from distribution, but it preserves a public advisory and audit trail
explaining the affected version and safe replacement.

30
ROADMAP.md Normal file
View File

@@ -0,0 +1,30 @@
# Higress Roadmap
The maintained public Higress roadmap is published on the
[Higress website](https://higress.ai/en/docs/latest/overview/roadmap/). Its
source is version controlled in the
[`higress-group/higress-group.github.io`](https://github.com/higress-group/higress-group.github.io/blob/main/src/content/docs/latest/en/overview/roadmap.md)
repository.
Roadmap entries describe intended direction and target timing, not a guarantee
that a feature or release will ship on a particular date. Security, quality,
compatibility, and contributor availability can change priorities.
## Changing the roadmap
Anyone may propose a roadmap item or priority change through a public
[Higress issue](https://github.com/higress-group/higress/issues) or
[discussion](https://github.com/higress-group/higress/discussions). A proposal
should describe the user problem, expected outcome, project scope, major
dependencies, compatibility or security impact, and a responsible contributor
or maintainer when known.
Maintainers decide roadmap changes through the public lazy-consensus process in
[`GOVERNANCE.md`](./GOVERNANCE.md). Accepted changes are reflected in the
version-controlled website roadmap. Significant changes should link back to
the public issue, discussion, or pull request that records their rationale.
The roadmap is reviewed when planning a minor or major release and whenever a
material project-direction change is accepted. Completed work is documented in
[GitHub releases](https://github.com/higress-group/higress/releases) and the
[`release-notes`](./release-notes) directory.

View File

@@ -17,11 +17,15 @@ your contributions.
**Please do NOT report security vulnerabilities through public GitHub issues,
discussions, or pull requests.**
Instead, please report them through **both** of the following private channels:
Every vulnerability report must be submitted through both of the following
private channels. Please provide the same substantive report to each channel
and, when available, include the case identifier from the other channel so the
Security Response Team can correlate the records. Do not wait for one channel
to acknowledge the report before submitting it to the other.
1. **GitHub Private Security Advisory**:
1. **GitHub Private Security Advisory (required)**:
<https://github.com/higress-group/higress/security/advisories/new>
2. **Alibaba Security Response Center (ASRC)**:
2. **Alibaba Security Response Center (required)**:
<https://security.alibaba.com/>
Please include as much of the following information as possible to help us
@@ -53,9 +57,58 @@ keep you informed of our progress throughout the process.
## Security Response Team
The Higress security response is handled by the project maintainers listed in
[`MAINTAINERS.md`](./MAINTAINERS.md). Security reports submitted via GitHub
Private Security Advisory are visible to all current maintainers.
The Security Response Team (SRT) is composed of the current project
maintainers:
- Yiquan Dong ([@CH3CHO](https://github.com/CH3CHO))
- Yuanxiao Zhao ([@EndlessSeeker](https://github.com/EndlessSeeker))
- Leilei Geng ([@gengleilei](https://github.com/gengleilei))
- Xiantao Han ([@hanxiantao](https://github.com/hanxiantao))
- Zhiwei Cheng ([@cr7258](https://github.com/cr7258))
- Tianyi Zhang ([@johnlanni](https://github.com/johnlanni))
- Jingfeng Xu ([@lexburner](https://github.com/lexburner))
[`MAINTAINERS.md`](./MAINTAINERS.md) is authoritative for membership. A merged
change to that roster onboards or offboards the same person from the SRT and
their private security access must be updated promptly.
For each report, the SRT assigns the following responsibilities in the private
advisory or equivalent confidential case record:
- **Triage coordinator**: acknowledges the report, maintains contact with the
reporter, assigns severity, tracks deadlines, and coordinates the team.
- **Fix lead**: reproduces the issue and develops or coordinates remediation.
- **Reviewer and release lead**: independently reviews the fix, prepares the
supported-version releases, and verifies that artifacts are available.
- **Disclosure lead**: prepares the advisory, CVE request when appropriate,
credits, and coordinated public communication.
One person may perform more than one role, but every confirmed vulnerability
must involve at least two unconflicted SRT members so that remediation receives
independent review.
### Report handling, conflicts, and escalation
1. The SRT correlates the required GitHub Private Security Advisory and Alibaba
Security Response Center submissions. The GitHub advisory or an equivalent
access-controlled project record contains the report, assignments,
decisions, timeline, fix, and disclosure plan; material status and
disclosure updates are reflected in both required reporting records.
2. An SRT member with a personal, employer, or product conflict must disclose
it privately and recuse from severity, release, or disclosure decisions for
that case. The triage coordinator assigns an unconflicted replacement.
3. If acknowledgement, triage, remediation, or disclosure is at risk of
missing the timelines in this policy, the triage coordinator escalates the
case to the full unconflicted SRT and records a revised plan. Critical
vulnerabilities are escalated immediately.
4. If a reporter receives no acknowledgement within three business days, they
should follow up through both private reporting channels and reference both
case identifiers when available. No vulnerability details should be posted
publicly.
5. If fewer than two SRT members are unconflicted, the unconflicted member
escalates confidentially to the CNCF TOC private mailing list at
[cncf-private-toc@lists.cncf.io](mailto:cncf-private-toc@lists.cncf.io)
before a release or disclosure decision is made.
## Disclosure Policy

View File

@@ -0,0 +1,409 @@
# General Technical Review — Higress / Incubation
- **Project:** Higress
- **Project version:** v2.2.3
- **Website:** <https://higress.ai/en/>
- **Date updated:** 2026-07-21
- **Template version:** CNCF General Technical Review v1.0
- **Review state:** Project working draft; maintainer approval and CNCF Project
Reviews verification pending
- **Evidence branch:**
[`higress-group/higress@main`](https://github.com/higress-group/higress/tree/main)
- **Intended TOC snapshot:** `projects/higress/tech-review/YYYY-MM-DD.md`
- **Description:** Higress is a cloud-native API gateway built on Envoy and
Istio. It supports Kubernetes Ingress and Gateway API and is extensible with
Wasm plugins and native Go filters.
This document answers all Day 0 and Day 1 questions required for an Incubation
review. It follows the current CNCF TOC
[General Technical Review questionnaire](https://github.com/cncf/toc/blob/main/toc_subprojects/project-reviews-subproject/general-technical-questions.md).
Statements that are not yet backed by a project guarantee are identified as
gaps rather than treated as completed controls.
This is the project-maintained working copy. For formal Due Diligence, a CNCF
reviewer or associate verifies it and archives a dated snapshot in `cncf/toc`.
The reviewer should freeze evidence links to the reviewed revision when that
snapshot is archived.
## Project Self-Assessment Summary
Higress has substantive evidence for every Day 0 and Day 1 question, but this
document does not assign the project an external technical-review rating. The
strongest Day 1 gaps are the absence of an automated
upgrade→downgrade→upgrade matrix, a published compatibility-review cadence,
and complete release supply-chain attestations. Broad control-plane RBAC also
requires minimization and documented justification. These findings should be
tracked during Due Diligence rather than hidden by marking the questionnaire
complete.
Day 2 questions are not required for an Incubation application and are not
answered in this snapshot. Operational evidence that already exists may be
added during CNCF review, but it is not needed to make the Day 0 and Day 1
scope complete.
## Day 0 — Planning Phase
### Scope
**How are roadmap scope and mid- to long-term features determined, and how does
that map to contributions and the maintainer ladder?**
Forward-looking goals and target versions are maintained in the public
[`ROADMAP.md`](https://github.com/higress-group/higress/blob/main/ROADMAP.md),
which links the version-controlled website roadmap. Feature scope and roadmap
changes are discussed through public GitHub issues, discussions, and pull
requests. Maintainers use lazy consensus, as documented in
[`GOVERNANCE.md`](https://github.com/higress-group/higress/blob/main/GOVERNANCE.md),
to accept project-direction changes.
Contributors implement accepted work through pull requests, may receive
delegated path-review responsibility as code owners, and may be nominated as
maintainers after sustained contribution under
[`MAINTAINERS.md`](https://github.com/higress-group/higress/blob/main/MAINTAINERS.md).
Objective progression expectations for the intermediate code-owner role remain
a project-maturity gap.
**Who are the target personas?**
- Platform and gateway engineers operating shared application entry points.
- Application teams publishing APIs through Ingress or Gateway API.
- AI platform teams governing access to LLM and MCP services.
- Plugin developers extending request and response processing.
- Security and SRE teams configuring policy, telemetry, and reliability.
**What are the primary, additional, and unsupported use cases?**
The primary use case is routing and governing north-south API traffic. Other
supported use cases include Kubernetes Ingress and Gateway API, service
registry integration, AI/LLM proxying, MCP server exposure, authentication,
rate limiting, observability, and standalone development installations.
Higress is not a transparent east-west service mesh, identity provider, model
provider, general-purpose application runtime, secrets manager, or substitute
for an organization's PKI, security governance, and incident response.
**Which organizations benefit from adoption?**
Organizations operating Kubernetes platforms, microservices, public or
partner APIs, multi-provider AI platforms, or centralized platform-engineering
services can benefit. Public examples are listed in
[`ADOPTERS.md`](https://github.com/higress-group/higress/blob/main/ADOPTERS.md).
**What end-user research has been completed?**
The project records adopter names and use cases in `ADOPTERS.md` and receives
feedback through GitHub and Discord. No independent end-user research report or
published structured interview set is currently available. Adopter interviews
for CNCF Incubation remain to be completed through the official TOC process.
### Usability
**How do target personas interact with the project?**
Cluster administrators install Higress with Helm. Platform teams configure it
with Kubernetes Ingress, Gateway API, Higress and Istio custom resources, the
`hgctl` CLI, or the optional console. Application teams normally submit routes
and policies rather than operate the data plane directly. Plugin developers use
the Wasm SDKs or native filter interfaces.
**What are the UX and UI?**
The primary interface is declarative Kubernetes YAML and Helm values. The
optional Higress Console provides browser-based management of routes, services,
certificates, and plugins. `hgctl` provides command-line operations. Runtime
behavior is observed through logs, metrics, traces, Kubernetes status, and
Envoy administration data where enabled.
**How does Higress integrate with production environments?**
Higress integrates with Kubernetes, Envoy, Istio APIs, Gateway API,
Prometheus-compatible monitoring, OpenTelemetry tracing, OCI registries,
certificate issuers, and service registries such as Nacos, Consul, Eureka, and
ZooKeeper. AI deployments may integrate with identity services, Redis, and
model providers. Except for Kubernetes in the standard Helm deployment, these
integrations are optional and selected by the adopter.
### Design
**What design principles and practices are followed?**
- Declarative, versioned APIs and reconciliation.
- Separation of control plane and Envoy-based data plane.
- xDS dynamic updates without gateway configuration reloads.
- Standard Kubernetes Ingress and Gateway API where possible.
- Independently versioned extensions, with Wasm used as an isolation boundary.
- Compatibility and regression coverage through unit, race, conformance, and
end-to-end tests.
The component architecture and data flow are documented in
[`docs/architecture.md`](https://github.com/higress-group/higress/blob/main/docs/architecture.md).
**What differs between proof-of-concept, development, test, and production?**
The all-in-one image or a minimal single-cluster Helm installation is suitable
for evaluation. CI uses ephemeral kind clusters. Production operators should
pin versions, use multiple gateway replicas, define resource sizing, configure
PodDisruptionBudgets and topology placement, use production PKI, and monitor
traffic and xDS health. The chart defaults to one controller replica and does
not constitute a production high-availability guarantee.
**Which services are required in the cluster?**
The Kubernetes deployment requires the Kubernetes API and DNS. Higress
controller/discovery supplies xDS to the gateway. The console, external service
registries, Redis, certificate issuers, observability backends, OCI registries,
and AI providers are optional.
**How is identity and access management implemented?**
Kubernetes ServiceAccounts and RBAC authorize control-plane access to cluster
resources. TokenReview and SubjectAccessReview APIs are used by inherited Istio
control-plane functions. Gateway-facing identity and policy are configured with
TLS and plugins such as JWT, OIDC, key-auth, HMAC, and basic-auth. Higress does
not operate an identity provider.
**How is sovereignty addressed?**
Higress does not transmit project usage telemetry by default. Operators choose
their image/plugin registries, model providers, observability destinations,
certificate authorities, and data locations. Request data is nevertheless
processed by every configured gateway plugin and upstream provider; operators
are responsible for selecting integrations that meet residency requirements.
**Which compliance requirements are addressed?**
The open-source project does not claim PCI-DSS, SOC 2, ISO 27001, GDPR, or other
regulatory certification. It is Apache-2.0 licensed and CI includes source and
dependency license checks. Deployers must assess their full environment and
configuration.
**What are the high-availability requirements?**
Gateway availability requires multiple replicas or nodes, adequate capacity,
health probes, and failure-domain placement. Controller unavailability stops
new configuration but existing Envoy processes may continue serving their last
accepted configuration. Production control-plane availability requires
multiple appropriately configured replicas and Kubernetes leader-election
behavior. The default single controller is a gap operators must address.
**What are the CPU, network, memory, and storage requirements?**
Current Helm defaults request 500m CPU and 2 GiB memory for the controller and
2 CPU and 2 GiB memory per gateway. Actual requirements depend on route count,
plugin set, configuration churn, request rate, payload sizes, connections, and
telemetry. Operators must size from load tests. Traffic bandwidth and
connection count drive network and socket consumption.
Configuration and certificates are stored as Kubernetes API objects. Gateway
and controller runtime files are ephemeral. The optional console and configured
integrations may introduce separate persistence requirements.
**What is the API design?**
- **Topology and conventions:** Kubernetes Ingress, Gateway API, Istio
networking APIs, and versioned Higress CRDs under `networking.higress.io` and
`extensions.higress.io` are reconciled into xDS resources.
- **Defaults:** Helm defaults create controller/discovery and gateway
deployments, ServiceAccounts, RBAC, Services, and CRDs. Gateway service type
defaults to `LoadBalancer`; automatic HTTPS is enabled in chart values.
- **Additional configuration:** Production use normally requires explicit
resource sizing, exposure, TLS, replicas, credentials, monitoring, and route
policy.
- **New/changed calls:** Enabling registry, certificate, identity, OCI, Redis,
or AI integrations causes calls to the endpoints explicitly configured by
the operator. Enabling Gateway API or inference support causes the controller
to watch and reconcile those API groups.
- **Kubernetes compatibility:** Compatibility is tested with kind and Gateway
API conformance. CRD and Kubernetes-version requirements are carried in Helm
packaging and release dependencies.
- **Versioning and breaking changes:** CRDs have explicit API versions. Stable
breaking changes require migration guidance, deprecation notice, and release
notes. Alpha Gateway API and inference capabilities are explicitly enabled
and do not have the same stability promise as stable APIs.
**What is the release process?**
Higress uses semantic version tags, including release-candidate suffixes when
needed. A `v*.*.*` tag triggers workflows that build controller, pilot and
gateway images, attach generated CRDs, build multi-platform `hgctl` archives,
and generate release notes. Versions are recorded in `VERSION`, Helm chart
metadata, dependency metadata, and `release-notes/`. The normative
[`RELEASE.md`](https://github.com/higress-group/higress/blob/main/RELEASE.md)
defines planning, two-person approval, validation, tagging, publishing,
verification, rollback/correction, security releases, and supported-version
changes.
### Installation
**How is the project installed and initialized?**
```console
helm repo add higress.io https://higress.io/helm-charts
helm repo update
helm install higress -n higress-system higress.io/higress --create-namespace
```
The chart installs CRDs, controller/discovery, gateway, Services,
ServiceAccounts, and RBAC. Operators then apply routes and policies or use the
optional console.
**How does an adopter validate installation?**
Check the Helm release and Ready state of controller and gateway pods, apply a
sample backend and route, then send an HTTP request through the gateway and
inspect status, logs, and metrics. CI performs the same class of installation
in ephemeral clusters and runs Gateway API and Higress conformance traffic.
### Security
**Where is the cloud-native security self-assessment?**
[`docs/cncf/security-self-assessment.md`](./security-self-assessment.md)
**How does Higress address the Cloud Native Security Tenets?**
- **Secure by default:** gateway containers use non-root and restricted
settings on supported Kubernetes platforms, TLS and private Secret delivery
are supported, and optional integrations require explicit configuration.
- **Least privilege:** separate ServiceAccounts and RBAC exist for controller
and gateway. The controller's broad ClusterRole and empty default container
security context are known exceptions requiring improvement.
- **Defense in depth:** Kubernetes RBAC, TLS/SDS, Envoy validation, Wasm
isolation, authentication/policy plugins, and network controls provide
distinct layers.
- **Explicit trust and boundaries:** Kubernetes administrators, xDS,
certificate sources, plugins, registries, identity providers, and upstreams
are separate trust boundaries described in the security self-assessment.
- **Secure lifecycle:** public review, automated tests, license checks, weekly
CodeQL, private advisories, and coordinated disclosure are in place, with
SBOM/signing/dynamic-analysis gaps disclosed.
Operators can loosen security by overriding pod/container security contexts,
using privileged or host networking modes, expanding RBAC, exposing admin
ports, disabling TLS/policy, or installing third-party/native plugins. These
settings should be treated as risk acceptances and tested in the adopter's
threat model.
**What security hygiene is maintained, and which features are high risk if not
maintained?**
The project uses required review, build and unit tests with Go race detection,
Gateway API/Higress conformance, plugin tests, license checks, weekly CodeQL,
versioned dependencies, Private Security Advisories, and coordinated
disclosure. Certificate handling, RBAC reconciliation, xDS generation and
validation, parsers/routing, authentication and authorization plugins, plugin
loading, and release artifacts are treated as security-critical boundaries.
**What privileges are required?**
The gateway reads Kubernetes Secrets for SDS. The controller watches and
reconciles ingress, Gateway API, Higress/Istio, discovery, certificate,
admission, and leader-election resources and performs TokenReview and
SubjectAccessReview calls. Several controller rules are cluster-wide and use
broad resource or verb sets. These are functional requirements of the current
implementation, but the project has not demonstrated that every permission is
minimal.
**How are certificates rotated?**
Gateway certificates are delivered through SDS. Automatic HTTPS can request
and renew ACME certificates, and Kubernetes Secrets can be updated without
gateway configuration reload. Operators remain responsible for issuer trust,
renewal monitoring, emergency rotation, and external certificate systems.
**How is the software supply chain secured?**
Source and dependency licenses are checked; dependencies and submodule commits
are versioned; changes are reviewed and tested; release artifacts are produced
by GitHub Actions from version tags. Some workflow actions are commit-pinned.
Project-generated release SBOMs, universal immutable action pinning, artifact
signatures, and verifiable build provenance are not currently present and are
recorded as gaps in the security self-assessment.
## Day 1 — Installation and Deployment Phase
### Project Installation and Configuration
**What do installation and configuration look like?**
Helm installs the project resources. Values select watched namespaces, replica
counts, resources, autoscaling, Service exposure, security contexts,
observability, Gateway API features, image/plugin registries, and optional
integrations. Operators should pin a chart/application version and keep reviewed
values in version control. Routes, services, credentials, and plugin policy are
then applied as Kubernetes resources or through the console.
### Project Enablement and Rollback
**How is Higress enabled or disabled in a live cluster, and is downtime
required?**
Install the chart and direct selected IngressClasses, GatewayClasses, routes,
DNS, or load-balancer traffic to Higress. Workloads do not require sidecars.
With sufficient replicas, ordinary controller and gateway rolling updates are
designed not to require application downtime. Capacity loss, incompatible
configuration, or a single-replica deployment can cause interruption.
Disable Higress by first moving traffic and route ownership to another entry
point, then uninstalling the Helm release. Existing workload behavior changes
only when traffic or resources select Higress.
**How are enablement and disablement tested?**
Pull-request CI installs Higress into kind and executes conformance and E2E
traffic. There is no dedicated automated migration test that moves live traffic
from another gateway to Higress and back; this is a gap.
**How are created resources cleaned up, including CRDs?**
`helm uninstall` removes chart-owned namespaced and cluster-scoped resources
managed by the release. CRDs and user-authored custom resources must be
inventoried and removed deliberately because automatic deletion could destroy
configuration data.
### Rollout, Upgrade and Rollback Planning
**How is compatibility with Kubernetes and orchestration tools maintained?**
The project updates Kubernetes, Gateway API, Istio, and Envoy dependencies and
uses build, kind, conformance, and E2E workflows on ongoing changes. Supported
versions are communicated through chart/dependency metadata, documentation,
and releases. No fixed public compatibility-review cadence is documented.
**How are rollbacks performed, and how can they fail?**
Operators retain the previous Helm revision, values, configuration, and pinned
images, then use `helm rollback <release> <revision>`. CRD storage/schema
changes, incompatible user configuration, unavailable old images, RBAC changes,
certificate changes, or external dependency changes may prevent a complete
rollback. Control-plane failure may leave existing gateways serving their last
accepted xDS state; data-plane rollout, plugin load failure, or invalid
fail-closed policy can affect live traffic.
**Which metrics should trigger rollback?**
Readiness failures, xDS rejection/NACKs, HTTP 4xx/5xx changes, latency and
timeout regression, gateway/controller restart rate, certificate errors,
plugin load failures, connection failures, and CPU/memory/socket saturation
should be compared with the pre-rollout baseline.
**How are upgrade→downgrade→upgrade paths tested?**
The repository tests clean installation and current-version conformance but
does not contain a dedicated automated upgrade→downgrade→upgrade version
matrix. This is a known Day 1 gap; operators must validate their exact source
and target versions in a non-production cluster.
**How are deprecations and removals communicated?**
They are communicated through GitHub releases, versioned release notes,
documentation, and API/version changes. `RELEASE.md` requires migration and
deprecation guidance in the release pull request and announcement. The project
does not currently define a universal minimum deprecation period for all API
types, which remains a process improvement.
**How are alpha and beta capabilities used during rollout?**
Alpha Gateway API and inference capabilities are selected through explicit
chart values and API versions. Operators should enable them first in a test or
canary environment, monitor the rollback signals above, and avoid relying on
their stability as if they were stable APIs.

View File

@@ -0,0 +1,274 @@
# Higress Governance Review
This is the Higress project's self-assessed Governance Review for its CNCF
Incubation application. It follows the current CNCF TOC
[Governance Review Template](https://github.com/cncf/toc/blob/main/toc_subprojects/project-reviews-subproject/governance-review-template.md).
A CNCF Project Reviews reviewer may amend the assessment and findings during
the public review.
- **Review state:** Project working draft; maintainer approval and CNCF Project
Reviews verification pending
- **Evidence branch:**
[`higress-group/higress@main`](https://github.com/higress-group/higress/tree/main)
- **Template verified:** 2026-07-21 against `cncf/toc` `main`
- **Intended TOC snapshot:**
`projects/higress/governance-review/YYYY-MM-DD.md`
This is the project-maintained working copy. CNCF reviewers may revise the
status and findings before a dated snapshot is archived in `cncf/toc`.
The reviewer should freeze evidence links to the reviewed revision when that
snapshot is archived.
## Summary and Assessment
**Status: Mostly Satisfactory**
Higress has discoverable governance, maintainer, code-owner, contribution,
community, security, and Code of Conduct documents. The project defines
project-wide maintainer responsibility, delegated code ownership, subproject
scope, vendor-neutral decisions, maintainer lifecycle, communication channels,
and security-response roles. The maintainer list includes seven publicly active
people affiliated with four organizations.
The documentation and public evidence satisfy all Governance Review criteria
except one Incubation-required item: Higress does not currently publish an
up-to-date public meeting scheduler or integrate its meetings with the CNCF
calendar. The project should establish that real public meeting infrastructure
before asserting that the Governance Review has no remaining Must-Fix item.
### Executing the Assessment
The self-assessment reviewed the following repository evidence as it exists on
2026-07-21:
- [`GOVERNANCE.md`](https://github.com/higress-group/higress/blob/main/GOVERNANCE.md)
- [`MAINTAINERS.md`](https://github.com/higress-group/higress/blob/main/MAINTAINERS.md)
- [`CODEOWNERS`](https://github.com/higress-group/higress/blob/main/CODEOWNERS)
- [`CONTRIBUTING_EN.md`](https://github.com/higress-group/higress/blob/main/CONTRIBUTING_EN.md)
- [`CODE_OF_CONDUCT.md`](https://github.com/higress-group/higress/blob/main/CODE_OF_CONDUCT.md)
- [`COMMUNITY.md`](https://github.com/higress-group/higress/blob/main/COMMUNITY.md)
- [`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md)
- [`README.md`](https://github.com/higress-group/higress/blob/main/README.md)
- Git history for the governance, maintainer, and ownership files
The review distinguishes documented policy from observed repository evidence.
It does not infer private processes or settings that are not publicly recorded.
### Must-Fix Items
The following issues need to be resolved before the Higress Incubation
application asserts completion of its Governance Review:
1. Publish an up-to-date public meeting scheduler and/or integrate the project
meetings with the CNCF calendar.
### Points of Excellence
- The maintainer roster is public and shows affiliation diversity across
Alibaba Cloud, Trip.com, XinYe Technology, and NVIDIA, with linked public
activity evidence for every listed maintainer.
- Governance distinguishes delegated code ownership from project-wide
maintainer authority and defines role and subproject lifecycle processes.
- The project publishes an authoritative channel inventory and separates
public project decisions from narrowly scoped confidential reporting.
- The named Security Response Team has defined incident roles, independent
review, conflict handling, and escalation.
- Governance explicitly protects vendor-neutral direction through individual
seats, equal voting weight, and conflict disclosure.
### Areas for Improvement
The following are non-blocking at Incubation but would improve long-term
governance maturity:
- Record governance evolution and link decisions to issues or pull requests.
- Publish examples demonstrating the maintainer lifecycle in practice.
- Add objective progression expectations for appointment to the code-owner
role.
- Extend `CODEOWNERS` or equivalent ownership records to each active
subproject and periodically audit coverage.
- Track contributor growth and recruitment trends using the linked CNCF
DevStats metrics, not only point-in-time activity.
---
## Review
This review audits the project's current governance evidence. The project's
Incubation application has not yet been opened; this file is the project's
self-assessment to accompany that application.
### Governance Summary
Higress uses maintainer-led lazy consensus. Significant decisions are recorded
in public issues or pull requests. When consensus cannot be reached,
non-conflicted maintainers may vote publicly and a simple majority of votes
cast decides. Governance defines contributor, code-owner, and maintainer
authority; the maintainer and subproject lifecycles; vendor neutrality; and
function-based teams. Confidential security work is handled by a named
Security Response Team under a public mandate.
### Governance Evolution
**Incubating: Suggested — Partially satisfied.**
`GOVERNANCE.md` and `MAINTAINERS.md` were introduced in April 2026, and
`CODEOWNERS` has changed repeatedly since 2022. This demonstrates change over
time, but the repository does not explain why governance evolved or connect
those changes to project experience and outcomes.
### Discoverability
**Incubating: Suggested — Satisfied.**
The README links the governance, maintainer, contribution, Code of Conduct, and
security documents from its Community section.
### Accuracy and Clarity
**Governance reflects actual activities — Incubating: Suggested — Satisfied.**
Public issue/PR collaboration and lazy consensus are consistent with the
repository workflow. Code-owner authority, security-team operation, and the
absence of a current recurring public meeting are documented without claiming
unobserved processes.
**Vendor-neutral direction — Incubating: Suggested — Satisfied.**
Governance states that roles are held by individuals, prohibits guaranteed
employer seats, vetoes, or preferred decision weight, requires material
conflict disclosure, and evaluates contributions and integrations on community
and technical merit.
### Decisions and Role Assignments
**Leadership, contribution, CNCF, governance, and goal decisions — Incubating:
Suggested — Satisfied.**
Governance applies public lazy consensus and non-conflicted maintainer voting
to leadership, contribution acceptance, goals, requests to CNCF, subproject
scope, and governance changes. The contribution guide and `CODEOWNERS` provide
the day-to-day change-review path.
**Function-based teams — Incubating: Suggested — Satisfied.**
Governance defines the creation and retirement requirements for function-based
teams. `SECURITY.md` names the Security Response Team and defines onboarding,
offboarding, incident assignments, conflicts, independent review, and
escalation.
### Maintainers and Maintainer Lifecycle
**Complete lifecycle — Incubating: Suggested — Satisfied.**
The project documents nomination, public activity review, voluntary departure,
inactivity, removal, emeritus status, and return.
**Lifecycle demonstrated — Incubating: Suggested — Not demonstrated.**
The maintainer file has only one introducing commit in the available history.
No public example of adding, replacing, or moving a maintainer to emeritus is
linked.
**Names, contact, responsibility, affiliation — Incubating: Required —
Satisfied.**
The roster provides names, GitHub contacts, project-wide responsibility
domains, and affiliations for all maintainers.
**Appropriate number of active maintainers — Incubating: Required —
Satisfied.**
Seven maintainers cover the project and its four subprojects. The roster
defines active status and annual public review, and its 2026 review links
public issue and pull-request activity for every listed maintainer during the
preceding 12 months.
**Maintainers from at least two organizations — Incubating: N/A, but met.**
The roster names four affiliations.
### Ownership
**Code and documentation ownership matches governance roles — Incubating:
Required — Satisfied.**
Governance defines code owners as delegated path reviewers, documents their
authority and public appointment/removal process, and makes clear that code
ownership neither grants project-wide governance authority nor limits a
maintainer's project-wide responsibility. This matches `CODEOWNERS`, which
includes maintainers and additional area reviewers.
### Code of Conduct
**Adoption and adherence — Incubating: Required — Satisfied as documented.**
The project adopts the CNCF Code of Conduct in `CODE_OF_CONDUCT.md` and links it
from the README and governance.
**Cross-link from governance — Incubating: Required — Satisfied.**
`GOVERNANCE.md` links both the CNCF Code of Conduct and the project copy.
### Subprojects
**All subprojects listed — Incubating: Required — Satisfied.**
Governance identifies the primary repository and lists `higress-console`,
`higress-standalone`, `plugin-server`, and `wasm-go` as active subprojects. It
also distinguishes upstream forks, integrations, examples, websites, and
other organization repositories from the CNCF project scope.
**Subproject lifecycle — Incubating: Suggested — Satisfied.**
Governance applies the project-wide maintainer roster and public decision
process to subprojects, records each repository's responsibility and status,
and requires leadership, contribution, communication, and lifecycle details
when a subproject is added, removed, transferred, or archived.
### Contributors and Community
**Contributor ladder — Incubating: Suggested — Partially satisfied.**
Governance defines contributor, delegated code-owner, and project-wide
maintainer roles. The contribution and maintainer documents define how
contributors participate and how sustained contributors progress to
maintainership; code-owner assignments are changed publicly through
`CODEOWNERS`. Objective progression expectations for the intermediate
code-owner role can be made more explicit.
**Issue and change submission — Incubating: Required — Satisfied.**
`CONTRIBUTING_EN.md`, issue templates, and the pull-request template document
the process.
**At least one public communication channel — Incubating: Required —
Satisfied.**
GitHub Issues, pull requests, and Discord are publicly documented.
**All public/private channels documented — Incubating: Required — Satisfied.**
`COMMUNITY.md` is the authoritative inventory for project and subproject GitHub
channels, Discord, mailing and localized community channels, documentation,
and narrowly scoped private security and Code of Conduct reporting. It states
that informal or employer-internal conversations are not project decision
channels and requires public decision records.
**Public meeting scheduler/CNCF calendar — Incubating: Required — Not
satisfied.**
No current public meeting scheduler or CNCF calendar integration is linked from
the reviewed repository documents.
**Contribution documentation — Incubating: Required — Satisfied.**
The contribution guide covers issues, pull requests, branches, commits, tests,
style, and AI-assisted contribution requirements.
**Contributor activity and recruitment — Incubating: Required — Satisfied.**
`COMMUNITY.md` links continuously updated GitHub contributor and repository
activity, CNCF DevStats, active-maintainer evidence, `help wanted` and `good
first issue` recruitment queues, the contribution path, and public mentoring
channels.

View File

@@ -0,0 +1,218 @@
# Higress CNCF Incubation Application — Working Draft
This working draft follows the CNCF TOC
[Project Incubation Application v1.6](https://github.com/cncf/toc/blob/main/.github/ISSUE_TEMPLATE/template-incubation-application.md),
verified on 2026-07-21. It is not ready to file while any item labelled
**BLOCKED** below remains unresolved.
## Review Project Moving Level Evaluation
- [ ] I have reviewed the TOC moving-level readiness triage guide, ensured the
criteria are met before opening the issue, and understand that unmet criteria
will result in closure.
This box remains unchecked because the public meeting, access-control evidence,
OpenSSF Passing badge, adopter interviews/verification, and remaining
vendor-neutral resource work are incomplete.
## Project information
- **Project repositories:**
[`higress`](https://github.com/higress-group/higress),
[`higress-console`](https://github.com/higress-group/higress-console),
[`higress-standalone`](https://github.com/higress-group/higress-standalone),
[`plugin-server`](https://github.com/higress-group/plugin-server), and
[`wasm-go`](https://github.com/higress-group/wasm-go)
- **Project site:** <https://higress.ai/en/>
- **Subprojects:** `higress-console`, `higress-standalone`, `plugin-server`,
and `wasm-go`; authoritative scope is in
[`GOVERNANCE.md`](https://github.com/higress-group/higress/blob/main/GOVERNANCE.md)
- **Related projects:** confirm before submission whether any project point of
contact operates a technically related project in another foundation
- **Communication:**
[`COMMUNITY.md`](https://github.com/higress-group/higress/blob/main/COMMUNITY.md)
- **Project points of contact:** Yuanxiao Zhao and Yiquan Dong; **TODO:** confirm
direct contact emails before filing (public community contact:
[higress@googlegroups.com](mailto:higress@googlegroups.com))
## Incubation Criteria Summary
### Application Level Assertion
- [x] Higress is currently Sandbox, accepted on 2026-03-15, and is applying to
Incubation.
- [ ] Higress is applying to join CNCF directly at Incubation level. Not
applicable; remove this option from the filed issue.
### Adoption Assertion
Public production adopters in
[`ADOPTERS.md`](https://github.com/higress-group/higress/blob/main/ADOPTERS.md)
include Ant Digital, Kuaishou, Trip.com, Vipshop, and Labring.
- [ ] **BLOCKED — Adopter interviews:** submit 57 willing adopters through the
official CNCF Adopter Interview Questionnaire before DD assignment. This
requires adopter consent and current contact details; a repository edit
cannot complete it.
## Application Process Principles
### Suggested
- [ ] Engage with the appropriate domain-specific TAG(s) to present the
technical architecture. This is suggested, not a filing prerequisite in
v1.6. A future presentation should be linked here if completed.
### Required
- [x] Complete a General Technical Review. The project self-assessment is in
[`general-technical-review.md`](https://github.com/higress-group/higress/blob/main/docs/cncf/general-technical-review.md);
maintainer approval and CNCF Project Reviews verification are requested.
- [ ] Complete a Governance Review. The self-assessment is in
[`governance-review.md`](https://github.com/higress-group/higress/blob/main/docs/cncf/governance-review.md),
but its public-meeting Must-Fix remains open.
- [ ] **BLOCKED — All project metadata and resources are vendor-neutral.**
Governance and the primary README now document vendor-neutral direction and
no longer promote a single commercial product, but release images remain
hosted only on Alibaba Cloud registry domains, the historical Go module path
contains `github.com/alibaba`, the website roadmap still includes a
single-vendor enterprise mapping, and the security policy requires duplicate
submission to the vendor-operated Alibaba Security Response Center. The
application needs a maintainer/CNCF-reviewed compatibility and infrastructure
plan.
- [x] Review and acknowledgement of Sandbox expectations and maturity-level
requirements. Higress was accepted as a Sandbox project on 2026-03-15 and
this application explicitly acknowledges the current requirements.
- [ ] Due Diligence Review. This is completed through CNCF review, resolution
of concerns, and public comment after a ready application is filed.
- [x] Appropriate installation, end-user, reference, and sample documentation
is linked from the project README and website.
## Governance and Maintainers
### Suggested
- [ ] Complete the Governance Review with the CNCF Project Reviews subproject;
external verification is pending.
- [x] Governance is discoverable and version controlled.
- [x] Governance documents actual public decision, leadership, role, and
security-team processes without claiming a recurring meeting that does not
exist.
- [x] Governance documents vendor-neutral project direction and conflicts.
- [x] Leadership, contribution, CNCF request, governance, goal, and subproject
decisions use a public process.
- [x] Function-based team assignment, onboarding, removal, conflicts, and
retirement are documented.
- [x] The complete maintainer lifecycle is documented.
- [ ] Demonstrate a completed maintainer lifecycle event. Suggested; no public
addition, replacement, or emeritus transition is yet linked.
- [x] Subproject scope, leadership model, contribution, status, and lifecycle
are documented.
### Required
- [x] Current maintainers include names, GitHub contact, responsibility domain,
and affiliation in
[`MAINTAINERS.md`](https://github.com/higress-group/higress/blob/main/MAINTAINERS.md).
- [x] Seven active maintainers are appropriate to the primary repository and
four subprojects; the annual activity review links public evidence for each.
- [x] Code and documentation ownership matches the documented code-owner and
maintainer roles.
- [x] The CNCF Code of Conduct is adopted and linked.
- [x] The Code of Conduct is cross-linked from governance.
- [x] All current subprojects are listed in governance.
## Contributors and Community
### Suggested
- [x] Contributor, code-owner, and maintainer roles form a contributor ladder;
objective code-owner progression criteria remain an improvement item.
### Required
- [x] Issue and change submission are documented.
- [x] Public GitHub and Discord communication channels are documented.
- [x] All official project, subproject, and narrowly scoped non-public channels
are inventoried in `COMMUNITY.md`.
- [ ] **BLOCKED — Public meeting scheduler/CNCF calendar.** Higress does not
currently publish an up-to-date public meeting scheduler or integrate its
meetings with the CNCF calendar. A real schedule or calendar integration must
be established rather than represented by documentation alone.
- [x] Contribution documentation is maintained.
- [x] Contributor activity and recruitment are demonstrated through GitHub,
DevStats, contribution labels, and maintainer activity links.
## Engineering Principles
### Suggested
- [x] The roadmap change process is documented in
[`ROADMAP.md`](https://github.com/higress-group/higress/blob/main/ROADMAP.md).
- [x] Regular release history is public in GitHub Releases and versioned
release notes.
### Required
- [x] Project goals, differentiation, need, and cloud-native use cases are
documented in the README and GTR.
- [x] What Higress does and why it exists are documented in the README and GTR.
- [x] A maintained public roadmap and change process are linked from
`ROADMAP.md`.
- [x] Architecture and software design are documented in
[`docs/architecture.md`](https://github.com/higress-group/higress/blob/main/docs/architecture.md)
and the GTR.
- [x] The release process is documented in
[`RELEASE.md`](https://github.com/higress-group/higress/blob/main/RELEASE.md).
## Security
### Suggested
- [ ] Complete a joint assessment with CNCF TAG Security and Compliance. This
is suggested, not a v1.6 filing prerequisite.
### Required
- [x] Vulnerability reporting is documented in
[`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md).
- [ ] **BLOCKED — Enforced repository access-control evidence.** Public files
do not prove organization 2FA, least-privilege team access, protected default
branch/rulesets, required non-author review, or protected release
environments. An organization owner must verify/configure these settings and
publish reviewable evidence appropriate for CNCF DD.
- [x] Named security-response membership, incident roles, two-person review,
conflicts, report handling, and escalation are documented.
- [x] The Security Self-Assessment is documented in
[`security-self-assessment.md`](https://github.com/higress-group/higress/blob/main/docs/cncf/security-self-assessment.md).
- [ ] **BLOCKED — OpenSSF Best Practices Passing badge.** The public entry is
currently 96%. Warning enforcement/remediation, static-analysis frequency,
confirmed CodeQL alert disposition, and project-level dynamic analysis remain
before the badge can be certified as Passing.
## Ecosystem
### Required
- [x] The public adopter list records organizations, contacts, environment, and
use cases.
- [x] At least three independent adopters report production use; five are
publicly listed.
- [ ] **BLOCKED — TOC adopter verification.** Complete the official interviews
and allow CNCF/TOC reviewers to verify the adopter evidence.
- [x] Integrations and compatibility with Kubernetes, Gateway API, Envoy,
Istio, Prometheus/OpenTelemetry, OCI, service registries, and model providers
are documented in the GTR, architecture, README, and user documentation.
## Current filing decision
**Do not file yet.** The top-level readiness attestation would be false while
the following required items remain open:
1. a real public meeting scheduler and/or CNCF calendar integration;
2. enforced repository access-control evidence;
3. OpenSSF Best Practices Passing, including warning enforcement, analysis
frequency, alert disposition, and dynamic analysis;
4. vendor-neutral resource/website remediation or an accepted migration plan;
5. 57 adopter interview submissions and later TOC verification; and
6. direct project point-of-contact emails confirmed for the issue.

View File

@@ -0,0 +1,282 @@
# Higress Security Self-Assessment
## Metadata
| | |
| --- | --- |
| Assessment stage | Complete working draft, pending maintainer approval; updated 2026-07-21 |
| Software | <https://github.com/higress-group/higress> |
| Review state | Prepared from public project evidence; no independent security review or audit |
| Evidence branch | [`higress-group/higress@main`](https://github.com/higress-group/higress/tree/main) |
| Assessment outline | [CNCF TAG Security Self-Assessment](https://tag-security.cncf.io/community/assessments/guide/self-assessment/) |
| Intended TOC snapshot | `projects/higress/security-assessment/self-assessment.md` |
| Security provider | No. Higress provides security features, but its primary function is API gateway traffic management. |
| Languages | Go, C++, Rust, AssemblyScript, shell, and Helm/YAML |
| SBOM | Not generated for release artifacts. Go module files, Cargo lock data, and container build files provide dependency inputs. |
### Security Links
| Document | Location |
| --- | --- |
| Vulnerability reporting and response | [`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md) |
| Architecture | [`docs/architecture.md`](https://github.com/higress-group/higress/blob/main/docs/architecture.md) |
| Helm defaults | [`helm/core/values.yaml`](https://github.com/higress-group/higress/blob/main/helm/core/values.yaml) |
| OpenSSF Best Practices | <https://www.bestpractices.dev/projects/12667> |
This is the project-maintained working copy. For formal Due Diligence, a vetted
snapshot must be archived at the path above in `cncf/toc`. The reviewer should
freeze evidence links to the reviewed revision when that snapshot is archived.
## Overview
Higress translates declarative ingress, Gateway API, service discovery, and
plugin configuration into xDS consumed by an Envoy-based gateway. It accepts
untrusted downstream traffic, selects upstream services, applies traffic and
security policy, and proxies requests and responses.
### Actors and Actions
- **Cluster administrator:** installs/upgrades Higress, grants RBAC, configures
exposure, certificates, registries, and security contexts.
- **Gateway operator/platform engineer:** creates routes, services, policies,
plugins, credentials, and observability configuration.
- **Application owner:** requests routes/policy and operates upstream services.
- **End user/client:** sends potentially hostile network requests.
- **Plugin author/provider:** supplies code that executes in the gateway's Wasm
sandbox or native filter boundary.
- **External provider/registry:** supplies service discovery data, plugins,
identity metadata, certificates, or AI/model APIs when configured.
- **Project maintainer/release manager:** reviews changes, responds to reports,
and publishes releases.
### Background
Higress combines the Envoy data plane and Istio-derived control plane with
Ingress/Gateway API translation and an extensible plugin ecosystem. The
control plane watches configuration and discovery sources and generates xDS;
the data plane accepts untrusted network traffic and applies that configuration.
### Architecture and Data Flow
The security-relevant flow is:
1. An authenticated Kubernetes user or automation writes Ingress, Gateway API,
Higress/Istio, Secret, and policy resources to the Kubernetes API.
2. Higress controller and discovery components watch authorized resources,
optional service registries, and certificate sources, then translate the
desired state into xDS and SDS configuration.
3. Gateway pilot agents and Envoy processes receive configuration and secret
material. Existing gateways may retain their last accepted configuration if
the control plane becomes unavailable.
4. Untrusted clients connect to the gateway. Envoy and configured filters parse,
authenticate, authorize, transform, and route requests to upstream services
or explicitly configured AI/model providers.
5. The gateway emits access logs, metrics, and traces to destinations selected
by the operator. Depending on configuration, those signals may include
sensitive request metadata.
### Data Assets and Trust Boundaries
| Asset or boundary | Security relevance |
| --- | --- |
| Kubernetes configuration and status | Unauthorized mutation can redirect traffic, weaken policy, load code, or disrupt availability. |
| TLS keys, certificates, tokens, and upstream credentials | Disclosure or replacement can enable impersonation, traffic decryption, or unauthorized upstream access. |
| Request/response bodies, AI prompts, and model outputs | May contain application secrets, personal data, proprietary data, or attacker-controlled content. |
| xDS/SDS control-plane boundary | Integrity and availability determine what code, routes, clusters, policy, and secrets the data plane uses. |
| Gateway process boundary | Native Go/C++ filters execute inside the proxy trust boundary; a defect can affect the full gateway process. |
| Wasm plugin boundary | Wasm provides stronger isolation than a native filter, but plugins can still observe or modify traffic granted to them. |
| External integration boundary | Registries, identity providers, certificate issuers, Redis, observability systems, OCI registries, and model providers have separate operators and trust. |
| Source and release boundary | Compromise of source control, CI identities, dependencies, images, or plugin artifacts can affect every adopter. |
### Goals
- Preserve the integrity and availability of configuration delivery between
control and data planes, reject invalid xDS updates, and make transport trust
assumptions explicit for operators.
- Terminate and originate TLS as configured and distribute private key material
through Kubernetes Secrets and SDS.
- Isolate Wasm plugin execution from the gateway process to the extent provided
by the Envoy/Wasm runtime.
- Enforce configured routing, authentication, authorization, traffic, and data
policies consistently.
- Contact only the external registries, identity systems, observability
backends, AI providers, and upstreams explicitly configured by the operator.
- Provide a private vulnerability reporting and coordinated disclosure path.
### Non-goals
Higress does not secure a compromised Kubernetes control plane, node, cluster
administrator, upstream service, identity provider, model provider, registry,
or native plugin. It does not guarantee that user-authored policy is correct,
provide regulatory certification, or replace network segmentation, secrets
management, PKI governance, application security, or incident response.
## Self-Assessment Use
This document is an internal analysis by the Higress project. It is not an
independent audit, certification, or attestation. It gives adopters and CNCF
reviewers an initial view of security boundaries, practices, and known gaps.
## Security Functions and Features
### Critical
- xDS configuration generation, transport, validation, and last-known-good
behavior between controller/discovery and gateways.
- TLS termination/origination, SDS secret delivery, certificate issuance and
rotation paths, and private-key access.
- Kubernetes RBAC, ServiceAccounts, token reviews, subject-access reviews, and
admission/configuration validation.
- HTTP/TCP parsing, routing, upstream selection, and request/response mutation.
- Plugin loading and Wasm sandbox boundary; native Go/C++ filters share the
gateway process trust boundary.
- Release workflows, container/plugin registries, dependency inputs, and
published artifacts.
### Security Relevant
- Authentication and authorization plugins (JWT, OIDC, key, HMAC, basic auth),
WAF, rate limiting, request blocking, and data masking.
- Pod/container security contexts, host networking, privileged mode, RBAC
toggles, network exposure, and admin/debug endpoints.
- Access, audit-style, metrics, and trace output, which may contain sensitive
request metadata depending on operator configuration.
- External service registries, Redis, certificate issuers, identity providers,
OCI registries, and AI/model providers.
### Threat Model
This project-authored model uses the actors, assets, flows, and boundaries above.
Priority reflects generic impact for a shared production gateway; adopters must
adjust it for their tenancy model and data classification.
| ID | Priority | Threat scenario | Current mitigation and residual risk |
| --- | --- | --- | --- |
| HG-TM-01 | High | A principal with excessive Kubernetes permissions creates or changes routes, policies, Secrets, or plugins to hijack traffic or bypass controls. | Kubernetes authentication, RBAC, API validation, namespace scoping where configured, and review/GitOps practices reduce exposure. Cluster administrators remain trusted, and the controller currently has broad cluster-wide permissions. |
| HG-TM-02 | High | A compromised controller, gateway ServiceAccount, or ClusterRoleBinding exposes TLS keys or upstream credentials. | Secrets are delivered through Kubernetes APIs and SDS rather than embedded in images. Gateway and controller roles are separate, but secret access and several controller rules remain broad and require minimization. |
| HG-TM-03 | High | A malicious or compromised plugin/image executes code, exfiltrates traffic, or alters policy. | Plugin installation is explicit and Wasm supplies a sandbox boundary. Native filters share the gateway process; release/plugin artifacts lack project-wide signature, SBOM, and provenance guarantees. |
| HG-TM-04 | High | Control-plane or xDS/SDS channel compromise injects malicious configuration or secret material, or prevents updates. | Envoy validates delivered resources and can retain last accepted configuration. Operators must protect control-plane identities, endpoints, network paths, and Kubernetes access; transport and deployment assumptions require a dedicated hardening guide. |
| HG-TM-05 | High | Hostile downstream traffic exploits an Envoy/filter parser defect or exhausts connections, memory, CPU, sockets, or upstream capacity. | Envoy validation, resource limits, timeouts, rate-limit/circuit-breaker features, probes, and multiple replicas can limit impact. Safe values are deployment-specific and timely Envoy/Higress patching remains essential. |
| HG-TM-06 | High | A tenant accidentally or deliberately attaches a route or policy to another tenant's gateway and exposes or disrupts traffic. | Kubernetes RBAC, namespace separation, Gateway API attachment controls, and review can restrict authors. Higress does not make a hostile multi-tenant cluster safe when administrators grant overlapping write privileges. |
| HG-TM-07 | High | Requests, AI prompts, credentials, or responses are disclosed to an external registry, plugin, observability backend, identity service, or model provider. | Integrations require operator configuration and can be restricted through credentials and network policy. Operators remain responsible for egress allowlists, provider contracts, log redaction, residency, and data-retention controls. |
| HG-TM-08 | Medium | Logs, metrics, traces, admin/debug endpoints, or configuration dumps expose credentials or sensitive request metadata. | Admin interfaces are not intended as public endpoints and telemetry is configurable. Access control, redaction, retention, and exposure are deployment responsibilities; unsafe logging or endpoint exposure remains possible. |
| HG-TM-09 | High | Certificate expiration, issuer compromise, or unauthorized Secret replacement causes outage or impersonation. | SDS and automatic HTTPS support dynamic certificate updates and renewal. Operators must secure issuers, monitor expiry/renewal, test emergency rotation, and control Secret writers. |
| HG-TM-10 | High | A source-control, dependency, GitHub Actions, registry, or maintainer-account compromise produces malicious release artifacts. | Public review, CI tests, license checks, CodeQL, pinned dependency/submodule inputs, and GitHub release workflows provide layers. Missing universal immutable action pinning, release SBOMs, signatures, provenance, and documented repository access controls leave material residual risk. |
The model should be reviewed after material architecture, privilege, plugin,
release-pipeline, or trust-boundary changes. The project does not yet enforce a
review cadence or require a named security reviewer, which is itself a process
gap.
## Project Compliance
The open-source project does not claim PCI-DSS, SOC 2, ISO 27001, GDPR, or
other regulatory certification. Deployers are responsible for assessing their
configuration and operational environment. Source is Apache-2.0 and pull
requests run license header and dependency-license checks.
## Secure Development Practices
Pull requests are publicly reviewed and run build/unit tests with Go race
detection, Gateway API and Higress conformance tests, plugin tests, and license
checks. CodeQL is scheduled weekly; it is not currently a pull-request gate.
The configured `golangci-lint` execution is commented out because of existing
findings. Release tags trigger image and CLI/CRD artifact builds. Dependency
inputs are versioned, but release artifacts do not currently have a
project-generated SBOM, signature, or SLSA provenance. Not all workflow actions
are pinned to immutable commits. The public repository does not prove a
required reviewer count, signed-commit requirement, organization-wide 2FA, or
branch-protection configuration; those controls require separate repository-
settings evidence.
Ordinary project-team communication uses GitHub issues, pull requests,
discussions, mailing, localized community channels, and Discord. Inbound users
use the same public channels. Every vulnerability report must be submitted to
both GitHub Private Security Advisories and the Alibaba Security Response
Center, as documented in `SECURITY.md`. The Security Response Team correlates
the two private records.
Releases, the project website, and the WeChat Official Account are outbound
channels. [`COMMUNITY.md`](https://github.com/higress-group/higress/blob/main/COMMUNITY.md)
is the authoritative inventory of public, subproject, and narrowly scoped
non-public channels.
Higress operates in the Kubernetes networking and cloud-native gateway
ecosystem. It implements Ingress and Gateway API and builds on Envoy, Istio,
OCI, Prometheus/OpenTelemetry conventions, and optional service registries.
## Security Issue Resolution
[`SECURITY.md`](https://github.com/higress-group/higress/blob/main/SECURITY.md)
prohibits public vulnerability reports and
requires reporters to submit the same substantive report through both GitHub
Private Security Advisories and the Alibaba Security Response Center. The named
Security Response Team is the current maintainer list.
For each case it assigns a triage coordinator, fix lead, independent reviewer
and release lead, and disclosure lead, with at least two unconflicted members.
The policy documents conflicts and escalation. Its targets are acknowledgement
within three business days and triage within 14 days, followed by private fix
development, coordinated disclosure (typically within 90 days), a GitHub
Security Advisory, and a CVE request where appropriate.
An operational security incident in an adopter environment remains the
adopter's responsibility. The project handles defects in project code and
artifacts. For confirmed project vulnerabilities, maintainers triage impact,
develop and release a fix, notify affected users through a GitHub Security
Advisory and release information, and coordinate timing with the reporter.
## Appendix
### Known Gaps
- OpenSSF Passing is not complete. Outstanding areas include compiler warning
enforcement, static-analysis alert remediation, and dynamic analysis.
- There are unresolved critical/high CodeQL alerts requiring maintainer access,
triage, and either fixes or documented upstream/vendor dispositions.
- Release SBOMs, signatures, and verifiable build provenance are absent.
- The controller's ClusterRole is broad and its default container security
context is empty. Gateway non-root defaults depend on Kubernetes/platform
capability; legacy fallback adds `NET_BIND_SERVICE` and allows escalation.
- No dedicated fuzzing/DAST program or automated upgrade/downgrade matrix is
documented.
- This threat model is project-authored, has not been independently validated,
and is not yet backed by a published data-flow diagram with explicit trust
boundaries or an independent security audit.
- Requiring every reporter to duplicate the report in the vendor-operated
Alibaba Security Response Center creates a vendor-neutrality concern that
requires CNCF review or a future neutral replacement plan.
### Known Issues Over Time
Published project advisories are available from the repository's
[Security Advisories](https://github.com/higress-group/higress/security/advisories)
page. This assessment does not claim that the absence of a public advisory for
a period means no vulnerability existed. The project has not published an
aggregate vulnerability history or mean-time-to-remediation report.
### OpenSSF Best Practices
The [Higress OpenSSF entry](https://www.bestpractices.dev/projects/12667) is at
96% of the Passing badge as of this assessment. Seven criteria remain
unanswered or unmet: compiler-warning enforcement, strict-warning enforcement,
warning remediation, static-analysis remediation, static-analysis frequency,
dynamic analysis, and enabling assertions or equivalent dynamic-analysis
checks. Passing requires evidence and implementation for all seven, not merely
updating the questionnaire.
### Example Use Cases
1. A platform team exposes Kubernetes services through Gateway API with TLS,
JWT authentication, rate limiting, and Prometheus metrics.
2. An AI platform routes requests across model providers while applying token
quotas, content policy, and request/response observability.
3. A microservice platform discovers Nacos/Consul services and exposes them
through stable API routes without reloading the data plane.
### Related Projects and Vendors
Envoy supplies the proxy foundation; Istio supplies xDS/control-plane building
blocks; Kubernetes Gateway API and Ingress supply standard configuration APIs.
Kong, Apache APISIX, Envoy Gateway, Traefik, and ingress controllers address
overlapping gateway use cases with different APIs, extension models, and
operational tradeoffs. Commercial products may distribute or manage Higress,
but they are outside this open-source security assessment.

1
go.mod
View File

@@ -128,6 +128,7 @@ require (
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect

4
go.sum
View File

@@ -5345,7 +5345,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250219182151-9fdb1cabc7b2/go.
google.golang.org/genproto/googleapis/api v0.0.0-20250227231956-55c901821b1e/go.mod h1:Xsh8gBVxGCcbV8ZeTB9wI5XPyZ5RvC6V3CTeeplHbiA=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
@@ -5475,8 +5474,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250227231956-55c901821b1e/go.
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
@@ -5564,7 +5561,6 @@ google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=

View File

@@ -21,6 +21,7 @@ import (
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -31,6 +32,7 @@ import (
networking "istio.io/api/networking/v1alpha3"
"istio.io/istio/pilot/pkg/networking/core"
"istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
"istio.io/istio/pilot/pkg/status"
"istio.io/istio/pkg/config"
"istio.io/istio/pkg/config/constants"
"istio.io/istio/pkg/config/schema/gvk"
@@ -192,6 +194,90 @@ func TestListGatewayResourceType(t *testing.T) {
}
}
func TestListGatewayResourceTypeWithAlternateGatewayClassName(t *testing.T) {
alternateGateway := gatewaySpec.DeepCopy()
alternateGateway.GatewayClassName = "alternate"
controller := setupController(t,
&k8sbeta.GatewayClass{
ObjectMeta: metav1.ObjectMeta{Name: "alternate"},
Spec: *gatewayClassSpec,
},
&k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "alternate-gw", Namespace: "ns1"},
Spec: *alternateGateway,
})
cfg := controller.List(gvk.Gateway, "ns1")
assert.Equal(t, len(cfg), 1)
assert.Equal(t, cfg[0].Name, "alternate-gw-"+constants.KubernetesGatewayName+"-default")
}
func TestHTTPRouteBackendServiceLifecycle(t *testing.T) {
backendPort := k8s.PortNumber(80)
route := httpRouteSpec.DeepCopy()
route.Rules = []k8s.HTTPRouteRule{{
BackendRefs: []k8s.HTTPBackendRef{{
BackendRef: k8s.BackendRef{
BackendObjectReference: k8s.BackendObjectReference{
Name: "backend",
Port: &backendPort,
},
},
}},
}}
controller := setupController(t,
&k8sbeta.GatewayClass{
ObjectMeta: metav1.ObjectMeta{Name: "higress"},
Spec: *gatewayClassSpec,
},
&k8sbeta.Gateway{
ObjectMeta: metav1.ObjectMeta{Name: "gwspec", Namespace: "ns1"},
Spec: *gatewaySpec,
},
&k8sbeta.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{Name: "http-route", Namespace: "ns1"},
Spec: *route,
},
)
statusQueue := &TestStatusQueue{state: map[status.Resource]any{}}
setAndWait(t, controller, statusQueue)
resolvedRefs := func() string {
for _, rawStatus := range statusQueue.Statuses() {
routeStatus, ok := rawStatus.(*k8s.HTTPRouteStatus)
if !ok {
continue
}
for _, parent := range routeStatus.Parents {
for _, condition := range parent.Conditions {
if condition.Type == string(k8s.RouteConditionResolvedRefs) {
return string(condition.Status) + "/" + condition.Reason
}
}
}
}
return ""
}
assert.EventuallyEqual(t, resolvedRefs, "False/BackendNotFound")
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "backend", Namespace: "ns1"},
Spec: corev1.ServiceSpec{Ports: []corev1.ServicePort{{
Name: "http",
Port: int32(backendPort),
}}},
}
if _, err := controller.client.Kube().CoreV1().Services("ns1").Create(t.Context(), service, metav1.CreateOptions{}); err != nil {
t.Fatal(err)
}
assert.EventuallyEqual(t, resolvedRefs, "True/ResolvedRefs")
if err := controller.client.Kube().CoreV1().Services("ns1").Delete(t.Context(), service.Name, metav1.DeleteOptions{}); err != nil {
t.Fatal(err)
}
assert.EventuallyEqual(t, resolvedRefs, "False/BackendNotFound")
}
func TestListGatewayResourceTypeWithCustomGatewayClass(t *testing.T) {
if runInGatewayClassSubprocess(t) {
return

View File

@@ -1064,12 +1064,12 @@ func buildDestination(ctx RouteContext, to k8s.BackendRef, ns string,
return nil, nil, &ConfigError{Reason: InvalidDestination, Message: "service name invalid; the name of the Service must be used, not the hostname."}
}
hostname = fmt.Sprintf("%s.%s.svc.%s", to.Name, namespace, ctx.DomainSuffix)
// Start - Updated by Higress
//key := namespace + "/" + string(to.Name)
//svc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
svc := ctx.LookupHostname(hostname, namespace, "Service")
// End - Updated by Higress
if svc == nil {
// Keep Higress service resolution as the primary lookup so its service naming and
// plugin binding semantics remain unchanged. Always fetch from the informer to
// retain the dependency needed to recompute the Route when the Service is deleted.
key := namespace + "/" + string(to.Name)
kubeSvc := ptr.Flatten(krt.FetchOne(ctx.Krt, ctx.Services, krt.FilterKey(key)))
if ctx.LookupHostname(hostname, namespace, "Service") == nil && kubeSvc == nil {
invalidBackendErr = &ConfigError{Reason: InvalidDestinationNotFound, Message: fmt.Sprintf("backend(%s) not found", hostname)}
}
case config.GroupVersionKind{Group: gvk.ServiceEntry.Group, Kind: "Hostname"}:
@@ -1592,6 +1592,21 @@ func (r routeParentReference) IsMesh() bool {
return r.InternalName == "mesh"
}
func (r routeParentReference) hostnameIntersection(rawRouteHost string) (string, bool) {
routeHost := host.Name(rawRouteHost)
listenerHost := host.Name(r.Hostname)
if len(listenerHost) == 0 || listenerHost == "*" {
return rawRouteHost, true
}
if routeHost.SubsetOf(listenerHost) {
return rawRouteHost, true
}
if listenerHost.SubsetOf(routeHost) {
return r.Hostname, true
}
return "", false
}
func (r routeParentReference) hostnameAllowedByIsolation(rawRouteHost string) bool {
routeHost := host.Name(rawRouteHost)
ourListener := host.Name(r.Hostname)

View File

@@ -1522,6 +1522,36 @@ func TestGatewayReferenceAllowedParentHostnameParsing(t *testing.T) {
}
}
func TestRouteParentReferenceHostnameIntersection(t *testing.T) {
tests := []struct {
name string
listenerHost string
routeHost string
wantHost string
wantMatch bool
}{
{name: "empty listener", listenerHost: "", routeHost: "example.com", wantHost: "example.com", wantMatch: true},
{name: "empty route", listenerHost: "example.com", routeHost: "*", wantHost: "example.com", wantMatch: true},
{name: "exact match", listenerHost: "example.com", routeHost: "example.com", wantHost: "example.com", wantMatch: true},
{name: "exact listener and wildcard route", listenerHost: "foo.example.com", routeHost: "*.example.com", wantHost: "foo.example.com", wantMatch: true},
{name: "wildcard listener and exact route", listenerHost: "*.example.com", routeHost: "foo.example.com", wantHost: "foo.example.com", wantMatch: true},
{name: "narrower route wildcard", listenerHost: "*.example.com", routeHost: "*.foo.example.com", wantHost: "*.foo.example.com", wantMatch: true},
{name: "narrower listener wildcard", listenerHost: "*.foo.example.com", routeHost: "*.example.com", wantHost: "*.foo.example.com", wantMatch: true},
{name: "disjoint exact hosts", listenerHost: "foo.example.com", routeHost: "bar.example.com", wantMatch: false},
{name: "disjoint wildcard hosts", listenerHost: "*.example.com", routeHost: "*.example.net", wantMatch: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parent := routeParentReference{Hostname: tt.listenerHost}
gotHost, gotMatch := parent.hostnameIntersection(tt.routeHost)
if gotHost != tt.wantHost || gotMatch != tt.wantMatch {
t.Fatalf("hostnameIntersection() = (%q, %v), want (%q, %v)", gotHost, gotMatch, tt.wantHost, tt.wantMatch)
}
})
}
}
func TestReferencePolicy(t *testing.T) {
validator := crdvalidation.NewIstioValidator(t)
type res struct {

View File

@@ -38,8 +38,7 @@ func GatewayClassesCollection(
krt.Collection[GatewayClass],
) {
return krt.NewStatusCollection(gatewayClasses, func(ctx krt.HandlerContext, obj *gateway.GatewayClass) (*gateway.GatewayClassStatus, *GatewayClass) {
if gatewayv1.ObjectName(obj.Name) != gatewayv1.ObjectName(gatewayClassName) ||
obj.Spec.ControllerName != managedGatewayController {
if obj.Spec.ControllerName != managedGatewayController {
return nil, nil
}
if _, known := classInfos[obj.Spec.ControllerName]; !known {

View File

@@ -148,7 +148,12 @@ func HTTPRouteCollection(
}
// Create one VS per hostname with a single hostname.
// This ensures we can treat each hostname independently, as the spec requires
for _, h := range vsHosts {
processedHosts := sets.New[string]()
for _, routeHost := range vsHosts {
h, ok := parent.hostnameIntersection(routeHost)
if !ok || processedHosts.InsertContains(h) {
continue
}
if !parent.hostnameAllowedByIsolation(h) {
// TODO: standardize a status message for this upstream and report
continue

View File

@@ -23,13 +23,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/http.higress-system
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: higress-system
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- name: http
route:

View File

@@ -42,13 +42,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/http.apple
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-apple~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-apple~apple.example
namespace: apple
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-apple
hosts:
- '*'
- apple.example
http:
- name: apple/http
route:
@@ -63,13 +63,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/http.banana
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-banana~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-banana~banana.example
namespace: banana
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-banana
hosts:
- '*'
- banana.example
http:
- name: banana/http
route:

View File

@@ -23,13 +23,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/mirror.default,HTTPRoute/redirect.default,HTTPRoute/rewrite.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- match:
- uri:

View File

@@ -142,13 +142,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/invalid-mirror.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- name: default/invalid-mirror
route:

View File

@@ -305,13 +305,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-default~*.domain.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-default
hosts:
- '*'
- '*.domain.example'
http:
- name: default/bind-all
route:
@@ -326,13 +326,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/same-namespace-valid.istio-system
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-foobar~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-foobar~*.foobar.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-foobar
hosts:
- '*'
- '*.foobar.example'
http:
- name: default/bind-all
route:
@@ -374,13 +374,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-no~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-no~*.slctr-expr-dne-no.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-no
hosts:
- '*'
- '*.slctr-expr-dne-no.example'
http:
- name: default/bind-all
route:
@@ -401,13 +401,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-yes~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-yes~*.slctr-expr-dne-yes.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-dne-yes
hosts:
- '*'
- '*.slctr-expr-dne-yes.example'
http:
- name: default/bind-all
route:
@@ -428,13 +428,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-no~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-no~*.slctr-expr-notin-no.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-no
hosts:
- '*'
- '*.slctr-expr-notin-no.example'
http:
- name: default/bind-all
route:
@@ -455,13 +455,13 @@ metadata:
annotations:
internal.istio.io/parents: HTTPRoute/bind-all.default,HTTPRoute/bind-cross-namespace.group-namespace1
internal.istio.io/route-semantics: gateway
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-yes~*
name: higress-system~gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-yes~*.slctr-expr-notin-yes.example
namespace: default
spec:
gateways:
- higress-system/gateway-istio-autogenerated-k8s-gateway-slctr-expr-notin-yes
hosts:
- '*'
- '*.slctr-expr-notin-yes.example'
http:
- name: default/bind-all
route:

View File

@@ -1,8 +1,8 @@
module github.com/alibaba/higress/plugins/golang-filter
go 1.22
go 1.23
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c
replace github.com/envoyproxy/envoy => github.com/higress-group/envoy v0.0.0-20260707144248-4491e6783b3d
replace github.com/mark3labs/mcp-go => github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30
@@ -17,7 +17,7 @@ require (
github.com/openai/openai-go/v2 v2.7.0
github.com/pkoukk/tiktoken-go v0.1.8
github.com/stretchr/testify v1.9.0
google.golang.org/protobuf v1.36.5
google.golang.org/protobuf v1.36.10
gorm.io/driver/clickhouse v0.6.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.11
@@ -53,6 +53,7 @@ require (
github.com/cockroachdb/errors v1.9.1 // indirect
github.com/cockroachdb/logtags v0.0.0-20211118104740-dabe8e521a4f // indirect
github.com/cockroachdb/redact v1.1.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/getsentry/sentry-go v0.12.0 // indirect

View File

@@ -185,7 +185,10 @@ github.com/getsentry/sentry-go v0.12.0/go.mod h1:NSap0JBYWzHND8oMbyi0+XZhUalc1TB
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-faker/faker/v4 v4.1.0 h1:ffuWmpDrducIUOO0QSKSF5Q2dxAht+dhsT9FvVHhPEI=
github.com/go-faker/faker/v4 v4.1.0/go.mod h1:uuNc0PSRxF8nMgjGrrrU4Nw5cF30Jc6Kd0/FUTTYbhg=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
@@ -264,8 +267,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
@@ -294,8 +297,8 @@ github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c h1:chAOZk/qEXFhLILWoNucj3X6r9xYnRR+SWFvhsOa2oo=
github.com/higress-group/envoy v0.0.0-20250430151331-2c556780b65c/go.mod h1:SU+IJUAfh1kkZtH+u0E1dnwho8AhbGeYMgp5vvjU+Gc=
github.com/higress-group/envoy v0.0.0-20260707144248-4491e6783b3d h1:JHeCakX2pS4OxpAdBNqvkUfLSrSVCNNtPkSjKe8lkfk=
github.com/higress-group/envoy v0.0.0-20260707144248-4491e6783b3d/go.mod h1:mgMEye9tOlNiUTG+iYhQYgCzQcX46MMS0Jo6bVwRt1U=
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30 h1:N4NMq8M1nZyyChPyzn+EUUdHi5asig2uLR5hOyRmsXI=
github.com/higress-group/mcp-go v0.0.0-20250428145706-792ce64b4b30/go.mod h1:O9gri9UOzthw728vusc2oNu99lVh8cKCajpxNfC90gE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
@@ -426,6 +429,7 @@ github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKf
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -934,8 +938,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -24,6 +24,10 @@ const (
type SSEServerWrapper struct {
BaseServer *common.SSEServer
HostMatchers []common.HostMatcher // Pre-parsed host matchers for efficient matching
// AuthUsername/AuthPassword hold the HTTP Basic credentials required to
// reach this server. An empty AuthUsername means authentication is disabled.
AuthUsername string
AuthPassword string
}
type config struct {
@@ -110,12 +114,20 @@ func (p *Parser) Parse(any *anypb.Any, callbacks api.ConfigCallbackHandler) (int
return nil, fmt.Errorf("failed to initialize MCP Server: %w", err)
}
conf.servers = append(conf.servers, &SSEServerWrapper{
wrapper := &SSEServerWrapper{
BaseServer: common.NewSSEServer(serverInstance,
common.WithSSEEndpoint(fmt.Sprintf("%s%s", serverPath, mcp_session.GlobalSSEPathSuffix)),
common.WithMessageEndpoint(serverPath)),
HostMatchers: hostMatchers,
})
}
// Servers that require HTTP Basic auth expose their credentials via the
// BasicAuthProvider interface; the filter enforces them per request.
if authProvider, ok := server.(common.BasicAuthProvider); ok {
wrapper.AuthUsername, wrapper.AuthPassword = authProvider.GetBasicAuthCredentials()
}
conf.servers = append(conf.servers, wrapper)
api.LogDebug(fmt.Sprintf("Registered MCP Server: %s", serverType))
}

View File

@@ -31,6 +31,19 @@ func (f *filter) DecodeHeaders(header api.RequestHeaderMap, endStream bool) api.
for _, server := range f.config.servers {
if common.MatchDomainWithMatchers(f.host, server.HostMatchers) && strings.HasPrefix(f.path, server.BaseServer.GetMessageEndpoint()) {
// Enforce HTTP Basic auth for servers that require it, before any
// further processing of the request.
if server.AuthUsername != "" {
authHeader, _ := header.Get("authorization")
if !common.CheckBasicAuth(authHeader, server.AuthUsername, server.AuthPassword) {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(
http.StatusUnauthorized,
"Unauthorized",
map[string][]string{"WWW-Authenticate": {`Basic realm="MCP Server"`}},
0, "")
return api.LocalReply
}
}
if url.Method != http.MethodPost {
f.callbacks.DecoderFilterCallbacks().SendLocalReply(http.StatusMethodNotAllowed, "Method not allowed", nil, 0, "")
return api.LocalReply

View File

@@ -19,9 +19,18 @@ type HigressOpsConfig struct {
envoyAdminURL string
namespace string
istiodToken string
username string
password string
description string
}
// GetBasicAuthCredentials implements common.BasicAuthProvider. The higress-ops
// server exposes Istio/Envoy debug interfaces, so it always requires callers to
// present HTTP Basic credentials.
func (c *HigressOpsConfig) GetBasicAuthCredentials() (string, string) {
return c.username, c.password
}
func (c *HigressOpsConfig) ParseConfig(config map[string]interface{}) error {
istiodURL, ok := config["istiodURL"].(string)
if !ok {
@@ -41,6 +50,20 @@ func (c *HigressOpsConfig) ParseConfig(config map[string]interface{}) error {
c.namespace = "higress-system"
}
// Basic auth credentials are mandatory: the higress-ops server exposes
// Istio/Envoy debug interfaces and must not be reachable unauthenticated.
username, ok := config["username"].(string)
if !ok || username == "" {
return errors.New("missing username: higress-ops requires basic auth credentials (username and password)")
}
c.username = username
password, ok := config["password"].(string)
if !ok || password == "" {
return errors.New("missing password: higress-ops requires basic auth credentials (username and password)")
}
c.password = password
// Optional: Istiod authentication token (required for cross-pod access)
if istiodToken, ok := config["istiodToken"].(string); ok {
c.istiodToken = istiodToken

View File

@@ -0,0 +1,45 @@
package common
import (
"crypto/subtle"
"encoding/base64"
"strings"
)
// BasicAuthProvider is implemented by server configs that require HTTP Basic
// authentication. When a server exposes non-empty credentials, the MCP server
// filter enforces Basic auth on every request routed to that server.
type BasicAuthProvider interface {
// GetBasicAuthCredentials returns the username and password required to
// access the server. An empty username means authentication is disabled.
GetBasicAuthCredentials() (username string, password string)
}
// CheckBasicAuth reports whether the given Authorization header value carries
// HTTP Basic credentials matching the expected username and password. The
// comparison is constant-time to avoid leaking credential length or content
// through timing. It always returns false when the expected username is empty.
func CheckBasicAuth(authHeader, expectedUsername, expectedPassword string) bool {
if expectedUsername == "" {
return false
}
const prefix = "Basic "
if len(authHeader) < len(prefix) || !strings.EqualFold(authHeader[:len(prefix)], prefix) {
return false
}
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(authHeader[len(prefix):]))
if err != nil {
return false
}
username, password, ok := strings.Cut(string(decoded), ":")
if !ok {
return false
}
userMatch := subtle.ConstantTimeCompare([]byte(username), []byte(expectedUsername)) == 1
passMatch := subtle.ConstantTimeCompare([]byte(password), []byte(expectedPassword)) == 1
return userMatch && passMatch
}

View File

@@ -0,0 +1,39 @@
package common
import (
"encoding/base64"
"testing"
)
func basicHeader(user, pass string) string {
return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+pass))
}
func TestCheckBasicAuth(t *testing.T) {
tests := []struct {
name string
authHeader string
wantUser string
wantPass string
want bool
}{
{"valid", basicHeader("admin", "s3cret"), "admin", "s3cret", true},
{"wrong password", basicHeader("admin", "nope"), "admin", "s3cret", false},
{"wrong username", basicHeader("root", "s3cret"), "admin", "s3cret", false},
{"empty expected username disables auth", basicHeader("admin", "s3cret"), "", "s3cret", false},
{"missing header", "", "admin", "s3cret", false},
{"non-basic scheme", "Bearer sometoken", "admin", "s3cret", false},
{"malformed base64", "Basic !!!notbase64!!!", "admin", "s3cret", false},
{"missing colon", "Basic " + base64.StdEncoding.EncodeToString([]byte("adminonly")), "admin", "s3cret", false},
{"case-insensitive scheme", "basic " + base64.StdEncoding.EncodeToString([]byte("admin:s3cret")), "admin", "s3cret", true},
{"password containing colon", basicHeader("admin", "a:b:c"), "admin", "a:b:c", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CheckBasicAuth(tt.authHeader, tt.wantUser, tt.wantPass); got != tt.want {
t.Fatalf("CheckBasicAuth(%q, %q, %q) = %v, want %v", tt.authHeader, tt.wantUser, tt.wantPass, got, tt.want)
}
})
}
}

View File

@@ -11,6 +11,10 @@
```bash
# NOTE: 如果你想在构建插件的时候设置额外的构建参数 EXTRA_TAGS
# 请更新 extensions/${PLUGIN_NAME} 插件目录对应的 .buildrc 文件
# NOTE: 如果你想自定义最终推送的镜像短名(覆盖默认的插件目录名),
# 可以在 .buildrc 中添加 IMAGE_NAME=<your-image-name>。
# 镜像短名只能包含小写字母、数字、`.`、`_`、`-`,否则构建会失败。
# 本设置仅影响推送的镜像 tag不影响源码路径。
$ PLUGIN_NAME=request-block make build
```

View File

@@ -9,6 +9,10 @@ The wasm-go plugin can be built quickly with the following command:
```bash
# NOTE: if you want to set EXTRA_TAGS for the wasm plugin
# please set them in the .buildrc file under extensions/${PLUGIN_NAME} directory
# NOTE: to override the output image short name (default: plugin directory name),
# add `IMAGE_NAME=<your-image-name>` to .buildrc.
# The image name must contain only [a-z0-9._-], or the build will fail.
# This affects the pushed image tag only, not the source code path.
$ PLUGIN_NAME=request-block make build
```

View File

@@ -353,6 +353,26 @@ func (c *ClaudeToOpenAIConverter) ConvertClaudeRequestToOpenAIWithOptions(body [
return result, nil
}
// computeClaudeInputTokens computes Claude-compatible input_tokens from OpenAI usage.
//
// In OpenAI's API, prompt_tokens includes cached_tokens (subset relationship).
// In Claude's API, input_tokens should NOT include cache tokens.
// We detect the OpenAI-standard semantics by checking if total_tokens == prompt_tokens + completion_tokens.
// For providers like Bedrock where prompt_tokens does NOT include cache tokens
// (total_tokens != prompt_tokens + completion_tokens), we return prompt_tokens as-is.
func computeClaudeInputTokens(u *usage) int {
if u == nil {
return 0
}
promptTokens := u.PromptTokens
if u.PromptTokensDetails != nil && u.PromptTokensDetails.CachedTokens > 0 {
if u.TotalTokens > 0 && u.TotalTokens == promptTokens+u.CompletionTokens {
return promptTokens - u.PromptTokensDetails.CachedTokens
}
}
return promptTokens
}
// ConvertOpenAIResponseToClaude converts an OpenAI response back to Claude format
func (c *ClaudeToOpenAIConverter) ConvertOpenAIResponseToClaude(ctx wrapper.HttpContext, body []byte) ([]byte, error) {
log.Debugf("[OpenAI->Claude] Original OpenAI response body: %s", string(body))
@@ -373,7 +393,7 @@ func (c *ClaudeToOpenAIConverter) ConvertOpenAIResponseToClaude(ctx wrapper.Http
// Only include usage if it's available
if openaiResponse.Usage != nil {
claudeResponse.Usage = claudeTextGenUsage{
InputTokens: openaiResponse.Usage.PromptTokens,
InputTokens: computeClaudeInputTokens(openaiResponse.Usage),
OutputTokens: openaiResponse.Usage.CompletionTokens,
}
if openaiResponse.Usage.PromptTokensDetails != nil {
@@ -582,7 +602,7 @@ func (c *ClaudeToOpenAIConverter) buildClaudeStreamResponse(ctx wrapper.HttpCont
// Only include usage if it's available
if openaiResponse.Usage != nil {
message.Usage = claudeTextGenUsage{
InputTokens: openaiResponse.Usage.PromptTokens,
InputTokens: computeClaudeInputTokens(openaiResponse.Usage),
OutputTokens: 0,
}
}
@@ -828,7 +848,7 @@ func (c *ClaudeToOpenAIConverter) buildClaudeStreamResponse(ctx wrapper.HttpCont
}
if openaiResponse.Usage != nil {
message.Usage = claudeTextGenUsage{
InputTokens: openaiResponse.Usage.PromptTokens,
InputTokens: computeClaudeInputTokens(openaiResponse.Usage),
OutputTokens: 0,
}
}
@@ -985,7 +1005,7 @@ func (c *ClaudeToOpenAIConverter) buildClaudeStreamResponse(ctx wrapper.HttpCont
openaiResponse.Usage.PromptTokens, openaiResponse.Usage.CompletionTokens)
usage := &claudeTextGenUsage{
InputTokens: openaiResponse.Usage.PromptTokens,
InputTokens: computeClaudeInputTokens(openaiResponse.Usage),
OutputTokens: openaiResponse.Usage.CompletionTokens,
}
if openaiResponse.Usage.PromptTokensDetails != nil {

View File

@@ -1099,6 +1099,72 @@ func TestClaudeToOpenAIConverter_ConvertOpenAIResponseToClaude(t *testing.T) {
require.NotNil(t, toolContent.Input)
assert.Equal(t, "/Users/zhangty/git/higress/README.md", (*toolContent.Input)["file_path"])
})
t.Run("openai_standard_usage_with_cached_tokens", func(t *testing.T) {
// OpenAI standard: prompt_tokens includes cached_tokens.
// total_tokens == prompt_tokens + completion_tokens -> subtract cached_tokens.
openaiResponse := `{
"id": "chatcmpl-test",
"model": "gpt-4o",
"object": "chat.completion",
"choices": [{
"index": 0,
"finish_reason": "stop",
"message": {"role": "assistant", "content": "Hello!"}
}],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 20,
"total_tokens": 120,
"prompt_tokens_details": {"cached_tokens": 60}
}
}`
result, err := converter.ConvertOpenAIResponseToClaude(nil, []byte(openaiResponse))
require.NoError(t, err)
var claudeResp claudeTextGenResponse
err = json.Unmarshal(result, &claudeResp)
require.NoError(t, err)
// input_tokens should exclude cache: 100 - 60 = 40
assert.Equal(t, 40, claudeResp.Usage.InputTokens)
assert.Equal(t, 20, claudeResp.Usage.OutputTokens)
assert.Equal(t, 60, claudeResp.Usage.CacheReadInputTokens)
})
t.Run("bedrock_style_usage_with_cached_tokens", func(t *testing.T) {
// Bedrock-style: prompt_tokens does NOT include cached_tokens.
// total_tokens (180) != prompt_tokens (100) + completion_tokens (20) -> do NOT subtract.
openaiResponse := `{
"id": "chatcmpl-test",
"model": "gpt-4o",
"object": "chat.completion",
"choices": [{
"index": 0,
"finish_reason": "stop",
"message": {"role": "assistant", "content": "Hello!"}
}],
"usage": {
"prompt_tokens": 100,
"completion_tokens": 20,
"total_tokens": 180,
"prompt_tokens_details": {"cached_tokens": 60}
}
}`
result, err := converter.ConvertOpenAIResponseToClaude(nil, []byte(openaiResponse))
require.NoError(t, err)
var claudeResp claudeTextGenResponse
err = json.Unmarshal(result, &claudeResp)
require.NoError(t, err)
// input_tokens should NOT be adjusted: 100 (prompt_tokens already excludes cache)
assert.Equal(t, 100, claudeResp.Usage.InputTokens)
assert.Equal(t, 20, claudeResp.Usage.OutputTokens)
assert.Equal(t, 60, claudeResp.Usage.CacheReadInputTokens)
})
}
func TestProviderConfigSupportsMessageReasoningContent(t *testing.T) {
@@ -1354,6 +1420,28 @@ func TestClaudeToOpenAIConverter_ConvertOpenAIStreamResponseToClaude_WithCachedT
resultStr := string(result)
assert.Contains(t, resultStr, "\"type\":\"message_delta\"")
// OpenAI standard: prompt_tokens (100) includes cached_tokens (60).
// Claude semantics: input_tokens should exclude cache tokens, so 100 - 60 = 40.
assert.Contains(t, resultStr, "\"input_tokens\":40")
assert.Contains(t, resultStr, "\"output_tokens\":20")
assert.Contains(t, resultStr, "\"cache_read_input_tokens\":60")
}
func TestClaudeToOpenAIConverter_ConvertOpenAIStreamResponseToClaude_BedrockStyleUsage(t *testing.T) {
converter := &ClaudeToOpenAIConverter{}
// Bedrock-style usage: prompt_tokens does NOT include cached_tokens.
// total_tokens (180) != prompt_tokens (100) + completion_tokens (20),
// so computeClaudeInputTokens should NOT subtract cached_tokens.
streamChunk := "data: {\"id\":\"chatcmpl-test\",\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}]}\n\n" +
"data: {\"id\":\"chatcmpl-test\",\"model\":\"gpt-4o\",\"choices\":[{\"index\":0,\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":100,\"completion_tokens\":20,\"total_tokens\":180,\"prompt_tokens_details\":{\"cached_tokens\":60}}}\n\n"
result, err := converter.ConvertOpenAIStreamResponseToClaude(nil, []byte(streamChunk))
require.NoError(t, err)
resultStr := string(result)
assert.Contains(t, resultStr, "\"type\":\"message_delta\"")
// Bedrock: prompt_tokens already excludes cache, so input_tokens = prompt_tokens = 100.
assert.Contains(t, resultStr, "\"input_tokens\":100")
assert.Contains(t, resultStr, "\"output_tokens\":20")
assert.Contains(t, resultStr, "\"cache_read_input_tokens\":60")

View File

@@ -649,6 +649,10 @@ func (c *ProviderConfig) GetGlobalRandomToken() string {
count := len(apiTokens)
switch count {
case 0:
if len(unavailableApiTokens) == 0 {
log.Warn("all tokens are unavailable and no unavailable tokens recorded")
return ""
}
log.Warn("all tokens are unavailable, will use random one of the unavailable tokens")
return unavailableApiTokens[rand.Intn(len(unavailableApiTokens))]
case 1:

View File

@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
@@ -82,6 +83,7 @@ const (
LLMServiceDuration = "llm_service_duration"
LLMDurationCount = "llm_duration_count"
LLMStreamDurationCount = "llm_stream_duration_count"
LLMFailureCount = "llm_failure_count"
ResponseType = "response_type"
ChatID = "chat_id"
ChatRound = "chat_round"
@@ -820,6 +822,13 @@ func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, dat
ctx.SetUserAttribute(ChatID, chatID.String())
}
// Track streaming errors across chunks — SSE failures often appear as
// data: {"error":{...}} before data: [DONE], so the last chunk alone is
// insufficient for error detection.
if !ctx.GetBoolContext("hasStreamError", false) && isErrorResponse(data) {
ctx.SetContext("hasStreamError", true)
}
// Get requestStartTime from http context
requestStartTime, ok := ctx.GetContext(StatisticsRequestStartTime).(int64)
if !ok {
@@ -873,8 +882,13 @@ func onHttpStreamingBody(ctx wrapper.HttpContext, config AIStatisticsConfig, dat
debugLogAiLog(ctx)
_ = ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
// Write metrics
writeMetric(ctx, config)
// Write metrics — prefer the accumulated buffer for error detection
// so that errors split across multiple SSE chunks are not missed.
bodyForMetric := data
if config.shouldBufferStreamingBody && len(streamingBodyBuffer) > 0 {
bodyForMetric = streamingBodyBuffer
}
writeMetric(ctx, config, bodyForMetric)
}
return data
}
@@ -928,7 +942,7 @@ func onHttpResponseBody(ctx wrapper.HttpContext, config AIStatisticsConfig, body
_ = ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
// Write metrics
writeMetric(ctx, config)
writeMetric(ctx, config, body)
return types.ActionContinue
}
@@ -1341,7 +1355,59 @@ func setSpanAttribute(key string, value interface{}) {
}
}
func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig) {
// isErrorResponse checks whether the LLM response indicates an error.
// Detects errors by:
// 1. Response body contains non-null "error" field at root level (OpenAI/Anthropic format).
// Handles both raw JSON and SSE "data: " prefixed chunks, including multi-event
// streaming buffers.
// 2. HTTP status code >= 400 as fallback when body is empty.
//
// Note: some providers (e.g. Anthropic streaming responses) emit {"error":""}
// even on success; an empty-string error is treated as not-an-error to avoid
// false positives.
func isErrorResponse(body []byte) bool {
if len(body) > 0 {
// SSE chunks are prefixed with "data: "; accumulated buffers contain
// multiple SSE events separated by \n\n. Split and check each event.
trimmed := bytes.TrimSpace(body)
if bytes.HasPrefix(trimmed, []byte("data: ")) {
for _, event := range bytes.Split(trimmed, []byte("\n\n")) {
jsonBody := bytes.TrimSpace(event)
if bytes.HasPrefix(jsonBody, []byte("data: ")) {
jsonBody = jsonBody[len("data: "):]
}
if hasErrorField(jsonBody) {
return true
}
}
return false
}
return hasErrorField(body)
}
// Fallback: check HTTP status code for errors with empty body (connection reset, timeout, etc.)
if len(body) == 0 {
if statusCode, err := proxywasm.GetHttpResponseHeader(":status"); err == nil {
if code, err := strconv.Atoi(statusCode); err == nil && code >= 400 {
return true
}
}
}
return false
}
// hasErrorField checks whether a JSON body contains a non-null, non-empty-string "error" field.
func hasErrorField(jsonBody []byte) bool {
errorVal := gjson.GetBytes(jsonBody, "error")
if errorVal.Exists() && errorVal.Value() != nil {
if errorVal.Type == gjson.String && errorVal.String() == "" {
return false
}
return true
}
return false
}
func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig, body []byte) {
// Generate usage metrics
var ok bool
var route, cluster, model string
@@ -1357,6 +1423,29 @@ func writeMetric(ctx wrapper.HttpContext, config AIStatisticsConfig) {
return
}
// Get model for metric label (may be empty for error responses)
modelStr := "-"
if m := ctx.GetUserAttribute(tokenusage.CtxKeyModel); m != nil {
if ms, ok := m.(string); ok {
modelStr = ms
}
}
// Fallback to request model for error responses where usage info is unavailable
if modelStr == "-" {
if rm, ok := ctx.GetContext(tokenusage.CtxKeyRequestModel).(string); ok && rm != "" {
modelStr = rm
}
}
// Count failure before usage check, so error responses without usage info are still counted.
// For streaming, also check the hasStreamError flag set during onHttpStreamingBody.
// llm_failure_count is intentionally incremented regardless of disableOpenaiUsage,
// because error responses carry no usage info and operators still need the failure
// signal even when usage tracking is off.
if isErrorResponse(body) || ctx.GetBoolContext("hasStreamError", false) {
config.incrementCounter(generateMetricName(route, cluster, modelStr, consumer, LLMFailureCount), 1)
}
if config.disableOpenaiUsage {
return
}

View File

@@ -2167,3 +2167,222 @@ func TestConfigWithDefaultAttributes(t *testing.T) {
})
})
}
func TestIsErrorResponse(t *testing.T) {
// Test error body detection (OpenAI/Anthropic format)
t.Run("error object in body", func(t *testing.T) {
body := []byte(`{"error": {"type": "api_error", "message": "Unauthorized"}}`)
require.True(t, isErrorResponse(body))
})
t.Run("error object with only code", func(t *testing.T) {
body := []byte(`{"error": {"code": "invalid_api_key"}}`)
require.True(t, isErrorResponse(body))
})
t.Run("error string in body", func(t *testing.T) {
body := []byte(`{"error": "Something went wrong"}`)
require.True(t, isErrorResponse(body))
})
t.Run("empty error string is not counted", func(t *testing.T) {
body := []byte(`{"error": ""}`)
require.False(t, isErrorResponse(body))
})
t.Run("null error is not counted", func(t *testing.T) {
body := []byte(`{"error": null}`)
require.False(t, isErrorResponse(body))
})
t.Run("no error field in body", func(t *testing.T) {
body := []byte(`{"choices": [{"message": {"content": "Hi"}}], "model": "gpt-4"}`)
require.False(t, isErrorResponse(body))
})
t.Run("nested error field not at root", func(t *testing.T) {
body := []byte(`{"choices": [{"error": "x"}]}`)
require.False(t, isErrorResponse(body))
})
// Empty body with HTTP status fallback is tested in TestIsErrorResponseWithHTTPStatus
// (requires wasm test environment for proxywasm.GetHttpResponseHeader)
}
func TestIsErrorResponseWithHTTPStatus(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
t.Run("empty body with 401 status returns true", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/chat"},
{":method", "POST"},
})
host.CallOnHttpResponseHeaders([][2]string{
{":status", "401"},
{"content-type", "application/json"},
})
require.True(t, isErrorResponse([]byte{}))
host.CompleteHttp()
})
t.Run("empty body with 200 status returns false", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/chat"},
{":method", "POST"},
})
host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"content-type", "application/json"},
})
require.False(t, isErrorResponse([]byte{}))
host.CompleteHttp()
})
})
}
func TestFailureCountMetric(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
t.Run("error response body increments failure count only", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.SetRouteName("api-v1")
host.SetClusterName("cluster-1")
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/chat"},
{":method", "POST"},
{"x-mse-consumer", "user1"},
})
requestBody := []byte(`{"model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": "Hello"}]}`)
host.CallOnHttpRequestBody(requestBody)
time.Sleep(10 * time.Millisecond)
host.CallOnHttpResponseHeaders([][2]string{
{":status", "401"},
{"content-type", "application/json"},
})
errorBody := []byte(`{"error": {"type": "authentication_error", "message": "Invalid API key"}}`)
host.CallOnHttpResponseBody(errorBody)
host.CompleteHttp()
// Verify llm_failure_count is incremented
failureMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.llm_failure_count"
failureValue, err := host.GetCounterMetric(failureMetric)
require.NoError(t, err)
require.Equal(t, uint64(1), failureValue)
// Verify success metrics are NOT present (usage info unavailable in error response)
durationCountMetric := "route.api-v1.upstream.cluster-1.model.gpt-3.5-turbo.consumer.user1.metric.llm_duration_count"
_, err = host.GetCounterMetric(durationCountMetric)
require.Error(t, err, "llm_duration_count should not exist for error response without usage")
})
t.Run("success response does not increment failure count", func(t *testing.T) {
host, status := test.NewTestHost(basicConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.SetRouteName("api-v1")
host.SetClusterName("cluster-1")
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/chat"},
{":method", "POST"},
{"x-mse-consumer", "user1"},
})
requestBody := []byte(`{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}`)
host.CallOnHttpRequestBody(requestBody)
time.Sleep(10 * time.Millisecond)
host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"content-type", "application/json"},
})
responseBody := []byte(`{
"choices": [{"message": {"content": "Hello!"}}],
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
"model": "gpt-4"
}`)
host.CallOnHttpResponseBody(responseBody)
host.CompleteHttp()
// Verify llm_failure_count is NOT present
failureMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user1.metric.llm_failure_count"
_, err := host.GetCounterMetric(failureMetric)
require.Error(t, err, "llm_failure_count should not exist for successful response")
// Verify success metrics exist
durationCountMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user1.metric.llm_duration_count"
durationCountValue, err := host.GetCounterMetric(durationCountMetric)
require.NoError(t, err)
require.Equal(t, uint64(1), durationCountValue)
})
})
}
func TestStreamingFailureCountMetric(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
t.Run("sse error in middle chunk before done", func(t *testing.T) {
host, status := test.NewTestHost(streamingBodyConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.SetRouteName("api-v1")
host.SetClusterName("cluster-1")
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"x-mse-consumer", "user1"},
})
requestBody := []byte(`{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}`)
host.CallOnHttpRequestBody(requestBody)
host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"content-type", "text/event-stream"},
})
// Simulate SSE stream: error appears in middle chunk, last chunk is [DONE]
chunk1 := []byte(`data: {"choices":[{"delta":{"content":"Hello"}}]}`)
host.CallOnHttpStreamingResponseBody(chunk1, false)
chunk2 := []byte(`data: {"error":{"type":"api_error","message":"Something went wrong"}}`)
host.CallOnHttpStreamingResponseBody(chunk2, false)
chunk3 := []byte(`data: [DONE]`)
host.CallOnHttpStreamingResponseBody(chunk3, true)
host.CompleteHttp()
// Verify llm_failure_count is incremented despite [DONE] being the last chunk
failureMetric := "route.api-v1.upstream.cluster-1.model.gpt-4.consumer.user1.metric.llm_failure_count"
failureValue, err := host.GetCounterMetric(failureMetric)
require.NoError(t, err)
require.Equal(t, uint64(1), failureValue)
})
})
}

View File

@@ -4,12 +4,23 @@ keywords: [ AI网关, AI token限流 ]
description: AI Token限流插件配置参考
---
> ⚠️ **行为变更提示(无版本号变化)**
>
> 自本次更新起,`rule_items` 的匹配语义从 **first-match-wins**(命中第一条即返回)改为 **all-match OR 叠加**(所有命中规则都评估,任一触发即拒绝)。同时解除 `global_threshold` 与 `rule_items` 的互斥约束,支持混合配置。
>
> - 老配置(单条 `rule_items` 或仅 `global_threshold`):行为不变
> - 老配置(多条 `rule_items` 期望短路匹配):**行为会变** —— 所有命中规则都会评估
> - Redis key 格式新增 `{rule_name}` hash tag 以兼容 Redis Cluster老计数器数据不兼容
>
> 详见下方"功能说明"与"配置示例"。
## 功能说明
`ai-token-ratelimit`插件基于 Redis 实现了 AI Token 限流功能,支持以下种限流模式:
`ai-token-ratelimit`插件基于 Redis 实现了 AI Token 限流功能,支持以下种限流模式:
- **规则级全局限流**:依据相同的`rule_name``global_threshold`配置,为自定义规则组设置全局 token 限流阈值
- **Key 级动态限流**:根据请求中的动态 Key包括 URL 参数、请求头、客户端 IP、Consumer 名称或 Cookie 字段等)进行分组 token 限流
- **混合限流**:同时配置 `global_threshold`(全局兜底)和 `rule_items`(按维度细分),所有命中的规则**叠加生效**,任一触发即拒绝。
## 运行属性
@@ -21,8 +32,8 @@ description: AI Token限流插件配置参考
| 配置项 | 类型 | 必填 | 默认值 | 说明 |
| ----------------------- | ------ | ---- | ------ |---------------------------------------------------------------------------|
| rule_name | string | 是 | - | 限流规则名称,根据限流规则名称+限流类型+限流key名称+限流key对应的实际值来拼装redis key |
| global_threshold | Object | 否,`global_threshold``rule_items` 选填一项 | - | 对整个自定义规则组进行限流 |
| rule_items | array of object | 否,`global_threshold``rule_items` 选填一项 | - | 限流规则项按照rule_items下的排列顺序匹配第一个rule_item后命中限流规则,后续规则将被忽略 |
| global_threshold | Object | 否,至少一项;可同时配置 | - | 对整个自定义规则组进行限流 |
| rule_items | array of object | 否,至少一项;可同时配置 | - | 限流规则项,最多支持 **10 条**。所有满足匹配条件的 `rule_item` 都会参与限流规则之间是"或"关系,任一触发即拒绝;规则的执行顺序不影响最终结果。详见下方"配置说明"展开。 |
| rejected_code | int | 否 | 429 | 请求被限流时返回的HTTP状态码 |
| rejected_msg | string | 否 | Too many requests | 请求被限流时,返回的响应体 |
| redis | object | 是 | - | redis相关配置 |
@@ -51,6 +62,14 @@ description: AI Token限流插件配置参考
| limit_by_per_ip | string | 否,`limit_by_*`中选填一项 | - | 按规则匹配特定 IP并对每个 IP 分别计算限流,配置获取限流键值的来源 IP 参数名称,从请求头获取,以`from-header-对应的header名`,示例:`from-header-x-forwarded-for`直接获取对端socket ip配置为`from-remote-addr` |
| limit_keys | array of object | 是 | - | 配置匹配键值后的限流次数 |
#### `rule_items` 多规则匹配语义
`rule_items` 是一个数组,**所有满足匹配条件**的 `rule_item` 都会被评估(均发起 Redis 计数器检查),规则之间是"或"关系,**任一触发即拒绝**。
> **注意:** 拒绝时按 `global_threshold`(如有)→ `rule_items`(数组顺序)的优先级**取最先触发的规则**报告响应头(`X-RateLimit-Reset` 等),而非"所有触发规则叠加报告"。因此,若同时配置了 `global_threshold` 和 `rule_items``global_threshold` 触发时总会优先报告,不会被 `rule_items` 覆盖。
> `rule_items` 数组最多支持 **10 条**规则。每条 `rule_item` 会按命中的 `limit_keys` 产生独立的 Redis 计数器。
`limit_keys`中每一项的配置字段说明。
| 配置项 | 类型 | 必填 | 默认值 | 说明 |

View File

@@ -4,12 +4,23 @@ keywords: [ AI Gateway, AI Token Rate Limiting ]
description: AI Token Rate Limiting Plugin Configuration Reference
---
> ⚠️ **Behavior Change Notice (no version bump)**
>
> As of this update, `rule_items` matching semantics changed from **first-match-wins** (returns on first hit) to **all-match OR overlay** (all matched rules are evaluated, any trigger rejects). The mutual exclusion between `global_threshold` and `rule_items` is also removed to support hybrid configuration.
>
> - Existing config (single `rule_items` or only `global_threshold`): behavior unchanged
> - Existing config (multiple `rule_items` expecting short-circuit match): **behavior will change** — all matched rules are evaluated
> - Redis key format adds `{rule_name}` hash tag for Redis Cluster compatibility; old counter data is incompatible
>
> See "Features" and "Configuration Examples" below for details.
## Function Description
The `ai-token-ratelimit` plugin implements AI Token rate limiting based on Redis, supporting the following two rate limiting modes:
The `ai-token-ratelimit` plugin implements AI Token rate limiting based on Redis, supporting the following three rate limiting modes:
- **Rule-level Global Rate Limiting**: Sets a global token rate limit threshold for custom rule groups based on the same `rule_name` and `global_threshold` configurations.
- **Key-level Dynamic Rate Limiting**: Performs grouped token rate limiting based on dynamic keys in requests (including URL parameters, request headers, client IP, Consumer name, or Cookie fields, etc.).
- **Hybrid rate limiting**: Configure `global_threshold` (global fallback) and `rule_items` (per-dimension) simultaneously. All matched rules take effect together; any trigger rejects the request.
## Runtime Properties
@@ -23,8 +34,8 @@ Plugin execution priority: `600`
| Configuration Item | Type | Required | Default Value | Description |
|--------------------------|----------------|----------|---------------|-------------------------------------------------------------------------------------------------|
| rule_name | string | Yes | - | Name of the rate limiting rule. The Redis key is assembled based on the rate limiting rule name + rate limiting type + rate limiting key name + actual value corresponding to the rate limiting key. |
| global_threshold | Object | No, either `global_threshold` or `rule_items` is required | - | Rate limits the entire custom rule group |
| rule_items | array of object| No, either `global_threshold` or `rule_items` is required | - | Rate limiting rule items. The first matching `rule_item` in the order of `rule_items` triggers the rate limiting rule, and subsequent rules are ignored. |
| global_threshold | Object | No, at least one of them is required; can be configured simultaneously | - | Rate limits the entire custom rule group |
| rule_items | array of object| No, at least one of them is required; can be configured simultaneously | - | Rate limiting rule items. Supports up to **10** rules. All matched `rule_item`s are evaluated and combined with an OR relationship; any trigger rejects the request. The execution order of rules does not affect the final result. See the expanded `rule_items` notes below for details. |
| rejected_code | int | No | 429 | HTTP status code returned when a request is rate-limited |
| rejected_msg | string | No | Too many requests | Response body returned when a request is rate-limited |
| redis | object | Yes | - | Redis-related configurations |
@@ -55,6 +66,14 @@ Plugin execution priority: `600`
| limit_by_per_ip | string | No, one of `limit_by_*` is required | - | Matches specific IPs by rule and calculates rate limits for each IP separately. Configures the source of the rate limiting key value as the IP parameter name, obtained from the request header in the format `from-header-corresponding_header_name` (e.g., `from-header-x-forwarded-for`), or directly obtains the peer socket IP by configuring `from-remote-addr`. |
| limit_keys | array of object | Yes | - | Configures the rate limiting count after matching the key value |
#### `rule_items` Multi-Rule Matching Semantics
`rule_items` is an array. **All** `rule_item`s whose match conditions are satisfied are evaluated (each triggers an independent Redis counter check), and the rules are combined with an OR relationship: **any trigger rejects the request**.
> **Note:** On rejection, the response headers (`X-RateLimit-Reset`, etc.) are reported from the **first triggered rule** in priority order: `global_threshold` (if present) → `rule_items` (in array order). This is **not** an "all triggered rules combined" report. Therefore, when both `global_threshold` and `rule_items` are configured, a triggered `global_threshold` always takes reporting priority over `rule_items`.
> The `rule_items` array supports up to **10** rules. Each `rule_item` produces independent Redis counters for each matched `limit_keys`.
### Description of Configuration Fields in `limit_keys`

View File

@@ -46,6 +46,9 @@ const (
SecondsPerMinute = 60 * Second
SecondsPerHour = 60 * SecondsPerMinute
SecondsPerDay = 24 * SecondsPerHour
// MaxRuleItems 限制 rule_items 数组最大长度,对齐商业版"最多同时命中 10 条规则"。
MaxRuleItems = 10
)
var timeWindows = map[string]int64{
@@ -181,8 +184,6 @@ func initLimitRule(json gjson.Result, config *AiTokenRateLimitConfig) error {
hasRule := ruleItemsResult.Exists()
if !hasGlobal && !hasRule {
return errors.New("at least one of 'global_threshold' or 'rule_items' must be set")
} else if hasGlobal && hasRule {
return errors.New("'global_threshold' and 'rule_items' cannot be set at the same time")
}
// 处理全局限流配置
@@ -192,14 +193,19 @@ func initLimitRule(json gjson.Result, config *AiTokenRateLimitConfig) error {
return fmt.Errorf("failed to parse global_threshold: %w", err)
}
config.GlobalThreshold = threshold
return nil
}
// 处理条件限流规则
if !hasRule {
return nil
}
items := ruleItemsResult.Array()
if len(items) == 0 {
return errors.New("config rule_items cannot be empty")
}
if len(items) > MaxRuleItems {
return fmt.Errorf("rule_items length %d exceeds maximum %d", len(items), MaxRuleItems)
}
var ruleItems []LimitRuleItem
// 用于记录已出现的LimitType和Key的组合
@@ -298,7 +304,7 @@ func parseLimitRuleItem(item gjson.Result) (*LimitRuleItem, error) {
}
if limitType == "" {
return nil, errors.New("only one of 'limit_by_header' and 'limit_by_param' and 'limit_by_consumer' and 'limit_by_cookie' and 'limit_by_per_header' and 'limit_by_per_param' and 'limit_by_per_consumer' and 'limit_by_per_cookie' and 'limit_by_per_ip' can be set")
return nil, errors.New("at least one of 'limit_by_header', 'limit_by_param', 'limit_by_consumer', 'limit_by_cookie', 'limit_by_per_header', 'limit_by_per_param', 'limit_by_per_consumer', 'limit_by_per_cookie', 'limit_by_per_ip' must be set")
}
ruleItem.LimitType = limitType

View File

@@ -2,6 +2,8 @@ package config
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -166,13 +168,40 @@ func TestParseAiTokenRateLimitConfig(t *testing.T) {
},
},
{
name: "Conflict_GlobalThresholdAndRuleItems",
name: "Both_ShouldSucceed",
json: `{
"rule_name": "test-conflict",
"rule_name": "test-both",
"global_threshold": {"token_per_second": 100},
"rule_items": [{"limit_by_header": "x-test"}]
"rule_items": [
{
"limit_by_header": "x-test",
"limit_keys": [{"key": "k1", "token_per_minute": 10}]
}
]
}`,
expectedErr: errors.New("'global_threshold' and 'rule_items' cannot be set at the same time"),
expected: AiTokenRateLimitConfig{
RuleName: "test-both",
GlobalThreshold: &GlobalThreshold{
Count: 100,
TimeWindow: Second,
},
RuleItems: []LimitRuleItem{
{
LimitType: LimitByHeaderType,
Key: "x-test",
ConfigItems: []LimitConfigItem{
{
ConfigType: ExactType,
Key: "k1",
Count: 10,
TimeWindow: SecondsPerMinute,
},
},
},
},
RejectedCode: DefaultRejectedCode,
RejectedMsg: DefaultRejectedMsg,
},
},
{
name: "Missing_GlobalThresholdAndRuleItems",
@@ -199,6 +228,16 @@ func TestParseAiTokenRateLimitConfig(t *testing.T) {
RejectedMsg: "Forbidden",
},
},
{
name: "LimitByType_AtLeastOne_Error",
json: `{
"rule_name": "no-limit-by",
"rule_items": [
{"limit_keys": [{"key": "k", "token_per_second": 1}]}
]
}`,
expectedErr: errors.New("failed to parse rule_item in rule_items: at least one of 'limit_by_header', 'limit_by_param', 'limit_by_consumer', 'limit_by_cookie', 'limit_by_per_header', 'limit_by_per_param', 'limit_by_per_consumer', 'limit_by_per_cookie', 'limit_by_per_ip' must be set"),
},
}
for _, tt := range tests {
@@ -216,3 +255,32 @@ func TestParseAiTokenRateLimitConfig(t *testing.T) {
})
}
}
func TestParseAiTokenRateLimitConfig_RuleItemsLimit(t *testing.T) {
tests := []struct {
name string
itemCnt int
expectErr string
}{
{"Exactly10", 10, ""},
{"Over10", 11, "rule_items length 11 exceeds maximum 10"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
items := make([]string, 0, tt.itemCnt)
for i := 0; i < tt.itemCnt; i++ {
items = append(items,
fmt.Sprintf(`{"limit_by_header":"h%d","limit_keys":[{"key":"k","token_per_second":1}]}`, i+1))
}
jsonStr := fmt.Sprintf(`{"rule_name":"t","rule_items":[%s]}`, strings.Join(items, ","))
var config AiTokenRateLimitConfig
err := ParseAiTokenRateLimitConfig(gjson.Parse(jsonStr), &config)
if tt.expectErr == "" {
assert.NoError(t, err)
assert.Len(t, config.RuleItems, tt.itemCnt)
} else {
assert.EqualError(t, err, tt.expectErr)
}
})
}
}

View File

@@ -6,8 +6,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0 h1:YGdj8KBzVjabU3STUfwMZghB+VlX6YLfJtLbrsWaOD0=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20250822030947-8345453fddd0/go.mod h1:tRI2LfMudSkKHhyv1uex3BWzcice2s/l8Ah8axporfA=
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8 h1:rs+AH1wfZy4swzuAyiRXT7xPUm8gycXt9Gwy0tqOq0o=
github.com/higress-group/wasm-go v1.0.2-0.20250821081215-b573359becf8/go.mod h1:9k7L730huS/q4V5iH9WLDgf5ZUHEtfhM/uXcegKDG/M=
github.com/higress-group/wasm-go v1.0.6-0.20251103065747-41d65dbb2f9e h1:wYW/DXjyQniQLaB26c+J9NQk3+AhqByzS1r18NShvB4=
github.com/higress-group/wasm-go v1.0.6-0.20251103065747-41d65dbb2f9e/go.mod h1:B8C6+OlpnyYyZUBEdUXA7tYZYD+uwZTNjfkE5FywA+A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

View File

@@ -23,6 +23,7 @@ import (
"ai-token-ratelimit/config"
"ai-token-ratelimit/util"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/log"
@@ -45,55 +46,59 @@ func init() {
const (
RedisKeyPrefix string = "higress-token-ratelimit"
// AiTokenGlobalRateLimitFormat 全局限流模式 redis key 为 RedisKeyPrefix:限流规则名称:global_threshold:时间窗口
AiTokenGlobalRateLimitFormat = RedisKeyPrefix + ":%s:global_threshold:%d"
// AiTokenRateLimitFormat 规则限流模式 redis key 为 RedisKeyPrefix:限流规则名称:限流类型:时间窗口:限流key名称:限流key对应的实际值
AiTokenRateLimitFormat = RedisKeyPrefix + ":%s:%s:%d:%s:%s"
RequestPhaseFixedWindowScript = `
local current = redis.call('get', KEYS[1])
local ttl = redis.call('ttl', KEYS[1])
local threshold = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
-- 键不存在时返回初始状态计数0窗口时间为过期时间
if not current then
return {threshold, 0, window}
end
// 使用 {rule_name} hash tag 让多规则多键操作在 Redis Cluster 下落到同一 slot
// AiTokenGlobalRateLimitFormat 全局限流模式 redis key 为 RedisKeyPrefix:{限流规则名称}:global_threshold:时间窗口
AiTokenGlobalRateLimitFormat = RedisKeyPrefix + ":{%s}:global_threshold:%d"
// AiTokenRateLimitFormat 规则限流模式 redis key 为 RedisKeyPrefix:{限流规则名称}:限流类型:时间窗口:限流key名称:限流key对应的实际值
AiTokenRateLimitFormat = RedisKeyPrefix + ":{%s}:%s:%d:%s:%s"
// MultiKeyRequestPhaseScript 多规则请求阶段只读检查脚本
// KEYS = [key1, ..., keyN]
// ARGV = [threshold1, window1, threshold2, window2, ..., thresholdN, windowN]
// 返回嵌套数组 {{threshold_i, current_i, ttl_i}, ...}
MultiKeyRequestPhaseScript = `
local results = {}
for i = 1, #KEYS do
local threshold = tonumber(ARGV[2*i - 1])
local window = tonumber(ARGV[2*i])
local current = redis.call('get', KEYS[i])
local ttl = redis.call('ttl', KEYS[i])
-- 修复异常过期时间(确保窗口有效
if ttl < 0 then
ttl = window
-- 键不存在时返回初始状态计数0窗口时间为过期时间
if not current then
table.insert(results, {threshold, 0, window})
else
-- 修复异常过期时间(确保窗口有效)
if ttl < 0 then
ttl = window
end
-- 返回窗口状态:阈值、当前计数、剩余时间
table.insert(results, {threshold, tonumber(current), ttl})
end
end
-- 返回窗口状态:阈值、当前计数、剩余时间
return {threshold, tonumber(current), ttl}
return results
`
// MultiKeyResponsePhaseScript 多规则响应阶段累加脚本(仅 ai-token-ratelimit 使用)
// KEYS = [key1, ..., keyN]
// ARGV = [threshold1, window1, count1, ..., thresholdN, windowN, countN]
// 每条规则独立判断 current <= threshold 才累加;返回 KEYS 数量
MultiKeyResponsePhaseScript = `
for i = 1, #KEYS do
local threshold = tonumber(ARGV[3*i - 2])
local window = tonumber(ARGV[3*i - 1])
local added = tonumber(ARGV[3*i])
local current = tonumber(redis.call('get', KEYS[i]) or "0")
if current <= threshold then
current = redis.call('incrby', KEYS[i], added)
if current == added then
redis.call('expire', KEYS[i], window)
else
local ttl = redis.call('ttl', KEYS[i])
if ttl < 0 then redis.call('expire', KEYS[i], window) end
end
end
end
return #KEYS
`
ResponsePhaseFixedWindowScript = `
local key = KEYS[1]
local threshold = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local added = tonumber(ARGV[3]) -- 需要累加的token数量
local current = tonumber(redis.call('get', key) or "0")
-- 只有当前计数未超过阈值时才执行累加
if current <= threshold then
current = redis.call('incrby', key, added)
-- 第一次设置值时初始化过期时间
if current == added then
redis.call('expire', key, window)
else
-- 非首次设置时检查过期时间,确保窗口有效性
local ttl = redis.call('ttl', key)
if ttl < 0 then
redis.call('expire', key, window)
end
end
end
-- 返回当前窗口状态:阈值、当前计数、剩余时间
return {threshold, current, redis.call('ttl', key)}
`
LimitRedisContextKey = "LimitRedisContext"
@@ -110,10 +115,16 @@ type LimitContext struct {
reset int
}
// MatchedRule 表示请求阶段命中的单条限流规则global 或 rule_item
type MatchedRule struct {
key string // 完整 Redis key
count int64 // 时间窗口内的限额(与 LimitConfigItem.Count / GlobalThreshold.Count 同义)
window int64 // 时间窗口大小(秒)
}
// LimitRedisContext 暂存请求阶段命中的全部规则,供响应阶段多键 INCRBY 使用
type LimitRedisContext struct {
key string
count int64
window int64
rules []MatchedRule
}
func parseConfig(json gjson.Result, cfg *config.AiTokenRateLimitConfig) error {
@@ -132,59 +143,73 @@ func parseConfig(json gjson.Result, cfg *config.AiTokenRateLimitConfig) error {
func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.AiTokenRateLimitConfig) types.Action {
ctx.DisableReroute()
limitKey, count, timeWindow := "", int64(0), int64(0)
if cfg.GlobalThreshold != nil {
// 全局限流模式
limitKey = fmt.Sprintf(AiTokenGlobalRateLimitFormat, cfg.RuleName, cfg.GlobalThreshold.TimeWindow)
count = cfg.GlobalThreshold.Count
timeWindow = cfg.GlobalThreshold.TimeWindow
} else {
// 规则限流模式
val, ruleItem, configItem := checkRequestAgainstLimitRule(ctx, cfg.RuleItems)
if ruleItem == nil || configItem == nil {
// 没有匹配到限流规则直接返回
return types.ActionContinue
}
limitKey = fmt.Sprintf(AiTokenRateLimitFormat, cfg.RuleName, ruleItem.LimitType, configItem.TimeWindow, ruleItem.Key, val)
count = configItem.Count
timeWindow = configItem.TimeWindow
matched := collectMatchedRules(ctx, cfg)
if len(matched) == 0 {
// 无任何规则命中:直接放行,不发起 Redis 调用
log.Debugf("ai-token-ratelimit: no rule matched, path=%s host=%s", ctx.Path(), ctx.Host())
return types.ActionContinue
}
ctx.SetContext(LimitRedisContextKey, LimitRedisContext{
key: limitKey,
count: count,
window: timeWindow,
})
log.Debugf("ai-token-ratelimit: request phase matched %d rule(s), path=%s host=%s",
len(matched), ctx.Path(), ctx.Host())
for i, m := range matched {
log.Debugf("ai-token-ratelimit: rule[%d] key=%s threshold=%d window=%ds",
i, m.key, m.count, m.window)
}
// 执行限流逻辑
keys := []interface{}{limitKey}
args := []interface{}{count, timeWindow}
err := cfg.RedisClient.Eval(RequestPhaseFixedWindowScript, 1, keys, args, func(response resp.Value) {
resultArray := response.Array()
if len(resultArray) != 3 {
log.Errorf("redis response parse error, response: %v", response)
proxywasm.ResumeHttpRequest()
n := len(matched)
keys := make([]interface{}, n)
args := make([]interface{}, 0, n*2)
for i, m := range matched {
keys[i] = m.key
args = append(args, m.count, m.window)
}
// 暂存命中规则,供响应阶段多键 INCRBY 使用
ctx.SetContext(LimitRedisContextKey, LimitRedisContext{rules: matched})
err := cfg.RedisClient.Eval(MultiKeyRequestPhaseScript, n, keys, args, func(response resp.Value) {
arr := response.Array()
if len(arr) != n {
log.Errorf("redis response length mismatch: got %d, want %d", len(arr), n)
_ = proxywasm.ResumeHttpRequest()
return
}
// 获取限流结果
threshold, current, ttl := resultArray[0].Integer(), resultArray[1].Integer(), resultArray[2].Integer()
context := LimitContext{
count: threshold,
remaining: threshold - current,
reset: ttl,
}
if current > threshold {
// 触发限流
ctx.SetUserAttribute("token_ratelimit_status", "limited")
ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
rejected(cfg, context)
} else {
proxywasm.ResumeHttpRequest()
// 单次遍历:触发即 return未触发则放行。
// ai-token-ratelimit 不对外暴露 LimitContext与 cluster-key-ratelimit 不同,
// 后者通过 X-RateLimit-* 头可观测 tightest 选择),因此此处不再写入 Context
for i, ruleResult := range arr {
ruleState := ruleResult.Array()
if len(ruleState) != 3 {
log.Errorf("redis sub-array length mismatch: got %d, want 3", len(ruleState))
_ = proxywasm.ResumeHttpRequest()
return
}
threshold, current, ttl := ruleState[0].Integer(), ruleState[1].Integer(), ruleState[2].Integer()
log.Debugf("ai-token-ratelimit: eval rule[%d] key=%s threshold=%d current=%d ttl=%ds",
i, matched[i].key, threshold, current, ttl)
if current > threshold {
// 命中触发的第一条规则(按 collectMatchedRules 顺序global 优先)
log.Debugf("ai-token-ratelimit: rule[%d] key=%s triggered (current=%d > threshold=%d), rejecting with code %d",
i, matched[i].key, current, threshold, cfg.RejectedCode)
ctx.SetUserAttribute("token_ratelimit_status", "limited")
_ = ctx.WriteUserAttributeToLogWithKey(wrapper.AILogKey)
rejected(cfg, LimitContext{
count: threshold,
remaining: threshold - current,
reset: ttl,
})
return
}
}
log.Debugf("ai-token-ratelimit: all %d rule(s) within threshold, allowing request to proceed", n)
_ = proxywasm.ResumeHttpRequest()
})
if err != nil {
log.Errorf("redis call failed: %v", err)
return types.ActionContinue
@@ -194,39 +219,90 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.AiTokenRateLimitCo
func onHttpStreamingBody(ctx wrapper.HttpContext, cfg config.AiTokenRateLimitConfig, data []byte, endOfStream bool) []byte {
if usage := tokenusage.GetTokenUsage(ctx, data); usage.TotalToken > 0 {
log.Debugf("ai-token-ratelimit: token usage detected input=%d output=%d total=%d",
usage.InputToken, usage.OutputToken, usage.TotalToken)
ctx.SetContext(tokenusage.CtxKeyInputToken, usage.InputToken)
ctx.SetContext(tokenusage.CtxKeyOutputToken, usage.OutputToken)
}
if endOfStream {
if ctx.GetContext(tokenusage.CtxKeyInputToken) == nil || ctx.GetContext(tokenusage.CtxKeyOutputToken) == nil {
return data
}
inputToken := ctx.GetContext(tokenusage.CtxKeyInputToken).(int64)
outputToken := ctx.GetContext(tokenusage.CtxKeyOutputToken).(int64)
limitRedisContext, ok := ctx.GetContext(LimitRedisContextKey).(LimitRedisContext)
if !ok {
return data
}
keys := []interface{}{limitRedisContext.key}
args := []interface{}{limitRedisContext.count, limitRedisContext.window, inputToken + outputToken}
err := cfg.RedisClient.Eval(ResponsePhaseFixedWindowScript, 1, keys, args, nil)
if err != nil {
log.Errorf("redis call failed: %v", err)
}
if !endOfStream {
return data
}
inputTokenRaw := ctx.GetContext(tokenusage.CtxKeyInputToken)
outputTokenRaw := ctx.GetContext(tokenusage.CtxKeyOutputToken)
if inputTokenRaw == nil || outputTokenRaw == nil {
log.Debugf("ai-token-ratelimit: response phase end-of-stream reached but no token usage recorded, skipping")
return data
}
inputToken, ok1 := inputTokenRaw.(int64)
outputToken, ok2 := outputTokenRaw.(int64)
if !ok1 || !ok2 {
log.Errorf("ai-token-ratelimit: response phase token usage context has unexpected types: input=%T output=%T",
inputTokenRaw, outputTokenRaw)
return data
}
limitRedisContextRaw := ctx.GetContext(LimitRedisContextKey)
if limitRedisContextRaw == nil {
log.Debugf("ai-token-ratelimit: response phase reached with no LimitRedisContext, skipping accumulation")
return data
}
limitRedisContext, ok := limitRedisContextRaw.(LimitRedisContext)
if !ok || len(limitRedisContext.rules) == 0 {
log.Debugf("ai-token-ratelimit: response phase LimitRedisContext is empty, skipping accumulation")
return data
}
// 多键 INCRBY每条规则一组 (threshold, window, added)
n := len(limitRedisContext.rules)
keys := make([]interface{}, n)
args := make([]interface{}, 0, n*3)
added := inputToken + outputToken
log.Debugf("ai-token-ratelimit: response phase accumulating tokens input=%d output=%d total=%d across %d rule(s)",
inputToken, outputToken, added, n)
for i, r := range limitRedisContext.rules {
keys[i] = r.key
args = append(args, r.count, r.window, added)
log.Debugf("ai-token-ratelimit: rule[%d] key=%s threshold=%d window=%ds added=%d",
i, r.key, r.count, r.window, added)
}
err := cfg.RedisClient.Eval(MultiKeyResponsePhaseScript, n, keys, args, func(response resp.Value) {
incremented := response.Integer()
log.Debugf("ai-token-ratelimit: response phase INCRBY done, %d/%d rule(s) updated", incremented, n)
})
if err != nil {
log.Errorf("redis call failed: %v", err)
}
return data
}
func checkRequestAgainstLimitRule(ctx wrapper.HttpContext, ruleItems []config.LimitRuleItem) (string, *config.LimitRuleItem, *config.LimitConfigItem) {
if len(ruleItems) > 0 {
for _, rule := range ruleItems {
val, ruleItem, configItem := hitRateRuleItem(ctx, rule)
if ruleItem != nil && configItem != nil {
return val, ruleItem, configItem
}
// collectMatchedRules 遍历 global_threshold 和 rule_items返回所有命中规则。
// 顺序global_threshold如有rule_items 中所有命中项(按数组顺序追加)。
// 该顺序决定了"触发时优先报告哪条规则"以及"未触发时 tightest 的选择范围"。
func collectMatchedRules(ctx wrapper.HttpContext, cfg config.AiTokenRateLimitConfig) []MatchedRule {
var matched []MatchedRule
if cfg.GlobalThreshold != nil {
matched = append(matched, MatchedRule{
key: fmt.Sprintf(AiTokenGlobalRateLimitFormat, cfg.RuleName, cfg.GlobalThreshold.TimeWindow),
count: cfg.GlobalThreshold.Count,
window: cfg.GlobalThreshold.TimeWindow,
})
}
for _, ruleItem := range cfg.RuleItems {
val, hitRule, hitItem := hitRateRuleItem(ctx, ruleItem)
if hitRule != nil && hitItem != nil {
matched = append(matched, MatchedRule{
key: fmt.Sprintf(AiTokenRateLimitFormat, cfg.RuleName, hitRule.LimitType, hitItem.TimeWindow, hitRule.Key, val),
count: hitItem.Count,
window: hitItem.TimeWindow,
})
}
}
return "", nil, nil
return matched
}
func hitRateRuleItem(ctx wrapper.HttpContext, rule config.LimitRuleItem) (string, *config.LimitRuleItem, *config.LimitConfigItem) {

View File

@@ -16,11 +16,14 @@ package main
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/test"
"github.com/stretchr/testify/require"
"github.com/tidwall/resp"
)
// 测试配置:全局限流配置
@@ -191,6 +194,58 @@ var regexpLimitConfig = func() json.RawMessage {
return data
}()
// 测试配置混合限流global_threshold + rule_items 同时配置)
var hybridLimitConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"rule_name": "ai-token-hybrid",
"global_threshold": map[string]interface{}{
"token_per_minute": 10000,
},
"rule_items": []map[string]interface{}{
{
"limit_by_header": "x-api-key",
"limit_keys": []map[string]interface{}{
{
"key": "vip-key",
"token_per_minute": 100,
},
},
},
},
"redis": map[string]interface{}{
"service_name": "redis.static",
"service_port": 6379,
},
})
return data
}()
// 测试配置:多条 rule_items 同时命中
var multiRuleItemsConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"rule_name": "ai-token-multi-items",
"rule_items": []map[string]interface{}{
{
"limit_by_header": "x-api-key",
"limit_keys": []map[string]interface{}{
{"key": "k1", "token_per_minute": 100},
},
},
{
"limit_by_param": "apikey",
"limit_keys": []map[string]interface{}{
{"key": "k2", "token_per_minute": 50},
},
},
},
"redis": map[string]interface{}{
"service_name": "redis.static",
"service_port": 6379,
},
})
return data
}()
func TestParseConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
// 测试全局限流配置解析
@@ -291,8 +346,8 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
// 返回 [threshold, current, ttl] 格式
resp := test.CreateRedisRespArray([]interface{}{1000, 1, 60})
// 返回 [[threshold, current, ttl]] 嵌套格式
resp := multiRuleResp([3]int{1000, 1, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -316,7 +371,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{100, 1, 60})
resp := multiRuleResp([3]int{100, 1, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -339,7 +394,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{50, 1, 60})
resp := multiRuleResp([3]int{50, 1, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -363,7 +418,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{200, 1, 60})
resp := multiRuleResp([3]int{200, 1, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -387,7 +442,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{75, 1, 60})
resp := multiRuleResp([3]int{75, 1, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -410,8 +465,8 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(触发限流)
// 返回 [threshold, current, ttl] 格式current > threshold 表示触发限流
resp := test.CreateRedisRespArray([]interface{}{1000, 1001, 60})
// 返回 [[threshold, current, ttl]] 嵌套格式current > threshold 表示触发限流
resp := multiRuleResp([3]int{1000, 1001, 60})
host.CallOnRedisCall(0, resp)
// 检查是否发送了限流响应
@@ -440,6 +495,213 @@ func TestOnHttpRequestHeaders(t *testing.T) {
// 应该返回 ActionContinue因为没有匹配到限流规则
require.Equal(t, types.ActionContinue, action)
})
// 混合限流global + rule_item 同时命中,返回 2-key Eval 响应
t.Run("hybrid limit both match", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{10000, 1, 60},
[3]int{100, 1, 60},
)
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
})
// 多条 rule_items 同时命中
t.Run("multi rule_items all match", func(t *testing.T) {
host, status := test.NewTestHost(multiRuleItemsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test?apikey=k2"},
{":method", "POST"},
{"x-api-key", "k1"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{100, 1, 60},
[3]int{50, 1, 60},
)
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
})
// 混合限流global 触发current > thresholdrule_item 未触发
t.Run("hybrid limit global triggered", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{10000, 10001, 60},
[3]int{100, 1, 60},
)
host.CallOnRedisCall(0, resp)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(429), localResponse.StatusCode)
host.CompleteHttp()
})
// Redis 响应数组长度与规则数不匹配
t.Run("redis response length mismatch", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp([3]int{10000, 1, 60})
host.CallOnRedisCall(0, resp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
// Redis 子数组长度异常(少于 3
t.Run("redis sub-array length mismatch", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
badResp, err := resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.IntegerValue(10000),
resp.IntegerValue(1),
}),
}).MarshalRESP()
require.NoError(t, err)
host.CallOnRedisCall(0, badResp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
// 混合配置global 命中rule_items 不命中(请求头不含 x-api-key
t.Run("hybrid limit rule_items no match", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp([3]int{10000, 1, 60})
host.CallOnRedisCall(0, resp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
// 多规则同时触发,验证 rejected 报告 globalreset=60 而非 rule_item 的 30
t.Run("hybrid limit both triggered reports global first", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{10000, 10001, 60},
[3]int{100, 101, 30},
)
host.CallOnRedisCall(0, resp)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(429), localResponse.StatusCode)
// LocalHttpResponse.Headers 类型为 [][2]string按切片迭代查找目标头
var resetHeader string
for _, h := range localResponse.Headers {
if strings.EqualFold(h[0], RateLimitResetHeader) && h[1] != "" {
resetHeader = h[1]
break
}
}
require.Equal(t, "60", resetHeader, "应报告 global 规则的 reset 时间")
host.CompleteHttp()
})
// 多规则未触发ai-token-ratelimit 不对外暴露 tightest 选择(无 X-RateLimit-* 头),
// 此处仅验证不触发拒绝tightest 行为由 cluster-key-ratelimit 的测试覆盖。
t.Run("multi-rule no trigger does not reject", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{10000, 9000, 60},
[3]int{100, 10, 60},
)
host.CallOnRedisCall(0, resp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
})
}
@@ -459,7 +721,7 @@ func TestOnHttpStreamingBody(t *testing.T) {
})
// 模拟 Redis 调用响应
resp := test.CreateRedisRespArray([]interface{}{1000, 1, 60})
resp := multiRuleResp([3]int{1000, 1, 60})
host.CallOnRedisCall(0, resp)
// 处理流式响应体
@@ -499,7 +761,7 @@ func TestOnHttpStreamingBody(t *testing.T) {
})
// 模拟 Redis 调用响应
resp := test.CreateRedisRespArray([]interface{}{1000, 1, 60})
resp := multiRuleResp([3]int{1000, 1, 60})
host.CallOnRedisCall(0, resp)
// 处理流式响应体
@@ -514,6 +776,72 @@ func TestOnHttpStreamingBody(t *testing.T) {
host.CompleteHttp()
})
// 多规则下响应阶段应触发 2-key INCRBY。
// Strengthened: 用 GetRedisCalloutAttributes() 断言响应阶段确实发起了一次 Eval。
// Callout 计数模式0 → 1 (请求阶段) → 0 (CallOnRedisCall 消费) → 1 (响应阶段)
// 如果 response 阶段被改回 1-key 或不调用 Redis最终计数会停在 0 而非 1。
t.Run("streaming body multi-rule incrby", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
require.Equal(t, 0, len(host.GetRedisCalloutAttributes()),
"请求前应无 Redis 调用")
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "POST"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
require.Equal(t, 1, len(host.GetRedisCalloutAttributes()),
"请求阶段应发起 1 次多键 Eval")
resp := multiRuleResp(
[3]int{10000, 1, 60},
[3]int{100, 1, 60},
)
host.CallOnRedisCall(0, resp)
body := []byte(`{"choices":[{"message":{"content":"hi"}}],"usage":{"prompt_tokens":10,"completion_tokens":15,"total_tokens":25}}`)
// 注意ai-token 的 onHttpStreamingBody 注册为 ProcessStreamingResponseBody
// 对应测试方法 CallOnHttpStreamingResponseBody不是 CallOnHttpStreamingRequestBody
// 这是因为 ai-token 处理的是 LLM 的流式响应体usage 字段在响应里)。
streamAction := host.CallOnHttpStreamingResponseBody(body, true)
require.Equal(t, types.ActionContinue, streamAction)
// 响应阶段应再发起 1 次多键 INCRBYcallout 数 0 → 1
// 如果 response 阶段被改回 1-key 或不调用 Redis此断言会失败。
require.Equal(t, 1, len(host.GetRedisCalloutAttributes()),
"响应阶段应再发起 1 次多键 INCRBYcallout 数 0 → 1")
host.CompleteHttp()
})
// 流式响应体处理时无前置 context覆盖 line 216-225, 227-234
// 使用 headerLimitConfig仅 x-api-key 规则ensureContextInitialized 自动触发的
// onHttpRequestHeaders 因为缺少 x-api-key 头而 matched 为空,返回 ActionContinue
// 此时 token context / LimitRedisContext 均未被设置。后续 streaming body 应通过
// GetContext 判空分支安全返回 data不 panic、不调用 Redis。
t.Run("streaming body without prior context", func(t *testing.T) {
host, status := test.NewTestHost(headerLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 直接处理流式响应体(缺少 x-api-keymatched 为空SetContext 未调用)
body := []byte(`{"choices":[{"message":{"content":"hi"}}],"usage":{"prompt_tokens":10,"completion_tokens":15,"total_tokens":25}}`)
streamAction := host.CallOnHttpStreamingResponseBody(body, true)
// 应安全返回 ActionContinue
require.Equal(t, types.ActionContinue, streamAction)
// 不应触发任何 Redis callout前置 context 缺失时跳过响应阶段)
require.Equal(t, 0, len(host.GetRedisCalloutAttributes()),
"missing token context should skip response-phase INCRBY")
host.CompleteHttp()
})
})
}
@@ -537,7 +865,7 @@ func TestCompleteFlow(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 2. 模拟 Redis 调用响应
resp := test.CreateRedisRespArray([]interface{}{100, 1, 60})
resp := multiRuleResp([3]int{100, 1, 60})
host.CallOnRedisCall(0, resp)
// 3. 处理流式响应体
@@ -555,3 +883,24 @@ func TestCompleteFlow(t *testing.T) {
})
})
}
// multiRuleResp 构建多规则嵌套 Redis 响应RESP wire format
// test.CreateRedisRespArray 不支持嵌套数组,因此直接用 resp.Writer 构造。
// 每个 [3]int 元组为 {threshold, current, ttl}。
// 注意:不能使用 resp.Value.Bytes()——它返回的是显示用字符串,不是 RESP wire 格式。
// 必须用 resp.Writer.WriteArray 或 Value.MarshalRESP() 输出真正的 RESP 字节流。
func multiRuleResp(items ...[3]int) []byte {
values := make([]resp.Value, len(items))
for i, it := range items {
values[i] = resp.ArrayValue([]resp.Value{
resp.IntegerValue(it[0]),
resp.IntegerValue(it[1]),
resp.IntegerValue(it[2]),
})
}
b, err := resp.ArrayValue(values).MarshalRESP()
if err != nil {
panic(fmt.Sprintf("failed to marshal multiRuleResp: %v", err))
}
return b
}

View File

@@ -53,8 +53,8 @@ func ExtractCookieValueByKey(cookie string, key string) (value string) {
pairs := strings.Split(cookie, ";")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
kv := strings.Split(pair, "=")
if kv[0] == key {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 && kv[0] == key {
value = kv[1]
break
}

View File

@@ -0,0 +1,51 @@
package util
import "testing"
func TestExtractCookieValueByKey(t *testing.T) {
tests := []struct {
name string
cookie string
key string
want string
}{
{
name: "extracts matching cookie value",
cookie: "user=alice; other=value",
key: "user",
want: "alice",
},
{
name: "skips segment without equals sign",
cookie: "user; other=value",
key: "user",
want: "",
},
{
name: "keeps equals signs in cookie value",
cookie: "user=alice=admin; other=value",
key: "user",
want: "alice=admin",
},
{
name: "empty cookie returns empty",
cookie: "",
key: "user",
want: "",
},
{
name: "key not present returns empty",
cookie: "user=alice; other=value",
key: "missing",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ExtractCookieValueByKey(tt.cookie, tt.key); got != tt.want {
t.Fatalf("ExtractCookieValueByKey() = %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1 @@
IMAGE_NAME=go-basic-auth

View File

@@ -0,0 +1 @@
IMAGE_NAME=go-bot-detect

View File

@@ -1 +1 @@
1.0.0
1.0.0-alpha

View File

@@ -74,7 +74,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config CacheControlConfig, l
if hit {
if config.expires == "max" {
proxywasm.AddHttpResponseHeader("Expires", "Thu, 31 Dec 2037 23:55:55 GMT")
proxywasm.AddHttpResponseHeader("Cache-Control", "maxAge=315360000")
proxywasm.AddHttpResponseHeader("Cache-Control", "max-age=315360000")
} else if config.expires == "epoch" {
proxywasm.AddHttpResponseHeader("Expires", "Thu, 01 Jan 1970 00:00:01 GMT")
proxywasm.AddHttpResponseHeader("Cache-Control", "no-cache")
@@ -83,7 +83,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, config CacheControlConfig, l
currentTime := time.Now()
expireTime := currentTime.Add(time.Duration(maxAge) * time.Second)
proxywasm.AddHttpResponseHeader("Expires", expireTime.UTC().Format(http.TimeFormat))
proxywasm.AddHttpResponseHeader("Cache-Control", "maxAge="+strconv.FormatInt(maxAge, 10))
proxywasm.AddHttpResponseHeader("Cache-Control", "max-age="+strconv.FormatInt(maxAge, 10))
}
}
return types.ActionContinue

View File

@@ -234,7 +234,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
// 验证是否添加了缓存控制头
responseHeaders := host.GetResponseHeaders()
require.True(t, test.HasHeader(responseHeaders, "expires"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=3600"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "max-age=3600"))
host.CompleteHttp()
})
@@ -264,7 +264,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
// 验证是否添加了缓存控制头
responseHeaders := host.GetResponseHeaders()
require.True(t, test.HasHeader(responseHeaders, "expires"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=315360000"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "max-age=315360000"))
host.CompleteHttp()
})
@@ -354,7 +354,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
// 验证是否添加了缓存控制头
responseHeaders := host.GetResponseHeaders()
require.True(t, test.HasHeader(responseHeaders, "expires"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=7200"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "max-age=7200"))
host.CompleteHttp()
})
@@ -384,7 +384,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
// 验证是否添加了缓存控制头
responseHeaders := host.GetResponseHeaders()
require.True(t, test.HasHeader(responseHeaders, "expires"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=1800"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "max-age=1800"))
host.CompleteHttp()
})
@@ -414,7 +414,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
// 验证是否添加了缓存控制头
responseHeaders := host.GetResponseHeaders()
require.True(t, test.HasHeader(responseHeaders, "expires"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=3600"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "max-age=3600"))
host.CompleteHttp()
})
@@ -452,7 +452,7 @@ func TestCompleteFlow(t *testing.T) {
// 验证是否添加了必要的缓存控制响应头
require.True(t, test.HasHeader(responseHeaders, "expires"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "maxAge=3600"))
require.True(t, test.HasHeaderWithValue(responseHeaders, "cache-control", "max-age=3600"))
host.CompleteHttp()
})

View File

@@ -4,14 +4,25 @@ keywords: [higress, rate-limit]
description: 基于 Key 集群限流插件配置参考
---
> ⚠️ **行为变更提示(无版本号变化)**
>
> 自本次更新起,`rule_items` 的匹配语义从 **first-match-wins**(命中第一条即返回)改为 **all-match OR 叠加**(所有命中规则都评估,任一触发即拒绝)。同时解除 `global_threshold` 与 `rule_items` 的互斥约束,支持混合配置。
>
> - 老配置(单条 `rule_items` 或仅 `global_threshold`):行为不变
> - 老配置(多条 `rule_items` 期望短路匹配):**行为会变** —— 所有命中规则都会评估
> - Redis key 格式新增 `{rule_name}` hash tag 以兼容 Redis Cluster老计数器数据不兼容
>
> 详见下方"功能说明"与"配置示例"。
## 功能说明
`cluster-key-rate-limit` 插件基于 Redis 实现**集群级限流**,适用于需要跨多个 Higress Gateway 实例进行**全局一致速率限制**的场景。
支持种限流模式:
支持种限流模式:
- **规则级全局限流**:基于相同的 `rule_name``global_threshold` 配置,对自定义规则组设置全局限流阈值
- **Key 级动态限流**:根据请求中的动态 Key如 URL 参数、请求头、客户端 IP、Consumer 名称或 Cookie 字段)进行分组限流
- **混合限流**:同时配置 `global_threshold`(全局兜底)和 `rule_items`(按维度细分),所有命中的规则**叠加生效**,任一触发即拒绝。
## 运行属性
@@ -23,8 +34,8 @@ description: 基于 Key 集群限流插件配置参考
| 配置项 | 类型 | 必填 | 默认值 | 说明 |
| ----------------------- | ------ | ---- | ------ |-----------------------------------------------------------------------------|
| rule_name | string | 是 | - | 限流规则名称,根据限流规则名称 + 限流类型 + 限流 key 名称 + 限流 key 对应的实际值来拼装 redis key |
| global_threshold | Object | 否,`global_threshold``rule_items` 选填一项 | - | 对整个自定义规则组进行限流 |
| rule_items | array of object | 否,`global_threshold``rule_items` 选填一项 | - | 限流规则项,按照 rule_items 下的排列顺序,匹配第一个 rule_item 后命中限流规则,后续规则将被忽略 |
| global_threshold | Object | 否,至少一项;可同时配置 | - | 对整个自定义规则组进行限流 |
| rule_items | array of object | 否,至少一项;可同时配置 | - | 限流规则项,最多支持 **10 条**。所有满足匹配条件的 `rule_item` 都会参与限流规则之间是"或"关系,任一触发即拒绝;规则的执行顺序不影响最终结果。详见下方"配置说明"展开。 |
| show_limit_quota_header | bool | 否 | false | 响应头中是否显示 `X-RateLimit-Limit`(限制的总请求数)和 `X-RateLimit-Remaining`(剩余还可以发送的请求数) |
| rejected_code | int | 否 | 429 | 请求被限流时,返回的 HTTP 状态码 |
| rejected_msg | string | 否 | Too many requests | 请求被限流时,返回的响应体 |
@@ -54,6 +65,18 @@ description: 基于 Key 集群限流插件配置参考
| limit_by_per_ip | string | 否,`limit_by_*` 中选填一项 | - | 按规则匹配特定 IP并对每个 IP 分别计算限流,配置获取限流键值的来源 IP 参数名称,从请求头获取,以 `from-header-对应的header名`,示例:`from-header-x-forwarded-for`,直接获取对端 socket ip配置为 `from-remote-addr` |
| limit_keys | array of object | 是 | - | 配置匹配键值后的限流次数 |
#### `rule_items` 多规则匹配语义
`rule_items` 是一个数组,**所有满足匹配条件**的 `rule_item` 都会被评估,规则之间是"或"关系,**任一触发即拒绝**。规则的执行顺序不影响最终结果。
> `rule_items` 数组最多支持 **10 条**规则。每条 `rule_item` 会按命中的 `limit_keys` 产生独立的 Redis 计数器。
##### 多规则场景下的 X-RateLimit-* 头
当多条规则同时未触发、配置 `show_limit_quota_header: true` 时:
- `X-RateLimit-Limit` / `X-RateLimit-Remaining`:取剩余比例最小(最紧约束)的命中规则
- `X-RateLimit-Reset`(触发限流时返回):取第一条触发的规则(按 `rule_items` 数组顺序,全局优先)
`limit_keys` 中每一项的配置字段说明。
| 配置项 | 类型 | 必填 | 默认值 | 说明 |

View File

@@ -5,15 +5,26 @@ description: Configuration reference for the Key-based cluster rate limiting plu
---
> ⚠️ **Behavior Change Notice (no version bump)**
>
> As of this update, `rule_items` matching semantics changed from **first-match-wins** (returns on first hit) to **all-match OR overlay** (all matched rules are evaluated, any trigger rejects). The mutual exclusion between `global_threshold` and `rule_items` is also removed to support hybrid configuration.
>
> - Existing config (single `rule_items` or only `global_threshold`): behavior unchanged
> - Existing config (multiple `rule_items` expecting short-circuit match): **behavior will change** — all matched rules are evaluated
> - Redis key format adds `{rule_name}` hash tag for Redis Cluster compatibility; old counter data is incompatible
>
> See "Function Description" and "Configuration Examples" below for details.
## Function Description
The `cluster-key-rate-limit` plugin implements **cluster-level rate limiting** based on Redis, suitable for scenarios
requiring **globally consistent rate limiting across multiple Higress Gateway instances**.
It supports two rate limiting modes:
It supports three rate limiting modes:
- **Rule-Level Global Rate Limiting**: Applies a unified rate limit threshold to custom rule groups based on identical `rule_name` and `global_threshold` configurations.
- **Key-Level Dynamic Rate Limiting**: Groups and limits requests by dynamic keys extracted from requests, such as URL parameters, request headers, client IPs, consumer names, or cookie fields.
- **Hybrid rate limiting**: Configure `global_threshold` (global fallback) and `rule_items` (per-dimension) simultaneously. All matched rules take effect together; any trigger rejects the request.
## Operational Attributes
@@ -25,8 +36,8 @@ It supports two rate limiting modes:
| Configuration Item | Type | Required | Default Value | Description |
|--------------------------|---------------|-------------------------------------------|---------------------|----------------------------------------------------------------------------|
| rule_name | string | Yes | - | Name of the rate limiting rule. Used to construct the Redis key in the format: `rule_name:rate_limit_type:key_name:key_value`. |
| global_threshold | Object | No (choose either `global_threshold` or `rule_items`) | - | Apply rate limiting to the entire custom rule group.|
| rule_items | array of object | No (choose either `global_threshold` or `rule_items`) | - | Rate limiting rule items. Rules are matched in the order of the array; once the first matching rule is hit, subsequent rules are ignored. |
| global_threshold | Object | No, at least one of them is required; can be configured simultaneously | - | Apply rate limiting to the entire custom rule group.|
| rule_items | array of object | No, at least one of them is required; can be configured simultaneously | - | Rate limiting rule items. Supports up to **10** rules. All matched `rule_item`s are evaluated and combined with an OR relationship; any trigger rejects the request. The execution order of rules does not affect the final result. See the expanded `rule_items` notes below for details. |
| show_limit_quota_header | bool | No | false | Whether to display `X-RateLimit-Limit` (total allowed requests) and `X-RateLimit-Remaining` (remaining allowed requests) in the response header. |
| rejected_code | int | No | 429 | HTTP status code returned when a request is rate-limited. |
| rejected_msg | string | No | Too many requests | Response body returned when a request is rate-limited. |
@@ -56,6 +67,18 @@ It supports two rate limiting modes:
| limit_by_per_ip | string | No (choose one of `limit_by_*` fields) | - | Matches specific IPs by rule and calculates rate limits for each IP. The IP can be extracted from a request header (formatted as `from-header-<header_name>`, e.g., `from-header-x-forwarded-for`) or directly from the peer socket IP (configured as `from-remote-addr`). |
| limit_keys | array of object | Yes | - | Configures the rate limits for matched key values. |
#### `rule_items` Multi-Rule Matching Semantics
`rule_items` is an array. **All** `rule_item`s whose match conditions are satisfied are evaluated, and the rules are combined with an OR relationship: **any trigger rejects the request**. The execution order of rules does not affect the final result.
> The `rule_items` array supports up to **10** rules. Each `rule_item` produces independent Redis counters for each matched `limit_keys`.
##### X-RateLimit-* headers in multi-rule scenarios
When multiple rules are matched and none trigger (with `show_limit_quota_header: true`):
- `X-RateLimit-Limit` / `X-RateLimit-Remaining`: from the matched rule with the smallest remaining ratio (tightest constraint)
- `X-RateLimit-Reset` (returned when triggered): from the first triggered rule (in `rule_items` array order, global first)
### Configuration Fields for `limit_keys`
| Configuration Item | Type | Required | Default Value | Description |

View File

@@ -47,6 +47,9 @@ const (
SecondsPerMinute = 60 * Second
SecondsPerHour = 60 * SecondsPerMinute
SecondsPerDay = 24 * SecondsPerHour
// MaxRuleItems 限制 rule_items 数组最大长度
MaxRuleItems = 10
)
var timeWindows = map[string]int64{
@@ -169,8 +172,6 @@ func initLimitRule(json gjson.Result, config *ClusterKeyRateLimitConfig) error {
hasRule := ruleItemsResult.Exists()
if !hasGlobal && !hasRule {
return errors.New("at least one of 'global_threshold' or 'rule_items' must be set")
} else if hasGlobal && hasRule {
return errors.New("'global_threshold' and 'rule_items' cannot be set at the same time")
}
// 处理全局限流配置
@@ -180,14 +181,19 @@ func initLimitRule(json gjson.Result, config *ClusterKeyRateLimitConfig) error {
return fmt.Errorf("failed to parse global_threshold: %w", err)
}
config.GlobalThreshold = threshold
return nil
}
// 处理条件限流规则
if !hasRule {
return nil
}
items := ruleItemsResult.Array()
if len(items) == 0 {
return errors.New("config rule_items cannot be empty")
}
if len(items) > MaxRuleItems {
return fmt.Errorf("rule_items length %d exceeds maximum %d", len(items), MaxRuleItems)
}
var ruleItems []LimitRuleItem
// 用于记录已出现的LimitType和Key的组合
@@ -286,7 +292,7 @@ func parseLimitRuleItem(item gjson.Result) (*LimitRuleItem, error) {
}
if limitType == "" {
return nil, errors.New("only one of 'limit_by_header' and 'limit_by_param' and 'limit_by_consumer' and 'limit_by_cookie' and 'limit_by_per_header' and 'limit_by_per_param' and 'limit_by_per_consumer' and 'limit_by_per_cookie' and 'limit_by_per_ip' can be set")
return nil, errors.New("at least one of 'limit_by_header', 'limit_by_param', 'limit_by_consumer', 'limit_by_cookie', 'limit_by_per_header', 'limit_by_per_param', 'limit_by_per_consumer', 'limit_by_per_cookie', 'limit_by_per_ip' must be set")
}
ruleItem.LimitType = limitType

View File

@@ -2,6 +2,8 @@ package config
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@@ -166,13 +168,41 @@ func TestParseClusterKeyRateLimitConfig(t *testing.T) {
},
},
{
name: "Conflict_GlobalThresholdAndRuleItems",
name: "Both_ShouldSucceed",
json: `{
"rule_name": "test-conflict",
"rule_name": "test-both",
"global_threshold": {"query_per_second": 100},
"rule_items": [{"limit_by_header": "x-test"}]
"rule_items": [
{
"limit_by_header": "x-test",
"limit_keys": [{"key": "k1", "query_per_minute": 10}]
}
]
}`,
expectedErr: errors.New("'global_threshold' and 'rule_items' cannot be set at the same time"),
expected: ClusterKeyRateLimitConfig{
RuleName: "test-both",
GlobalThreshold: &GlobalThreshold{
Count: 100,
TimeWindow: Second,
},
RuleItems: []LimitRuleItem{
{
LimitType: LimitByHeaderType,
Key: "x-test",
ConfigItems: []LimitConfigItem{
{
ConfigType: ExactType,
Key: "k1",
Count: 10,
TimeWindow: SecondsPerMinute,
},
},
},
},
ShowLimitQuotaHeader: false,
RejectedCode: DefaultRejectedCode,
RejectedMsg: DefaultRejectedMsg,
},
},
{
name: "Missing_GlobalThresholdAndRuleItems",
@@ -217,6 +247,16 @@ func TestParseClusterKeyRateLimitConfig(t *testing.T) {
RejectedMsg: DefaultRejectedMsg,
},
},
{
name: "LimitByType_AtLeastOne_Error",
json: `{
"rule_name": "no-limit-by",
"rule_items": [
{"limit_keys": [{"key": "k", "query_per_second": 1}]}
]
}`,
expectedErr: errors.New("failed to parse rule_item in rule_items: at least one of 'limit_by_header', 'limit_by_param', 'limit_by_consumer', 'limit_by_cookie', 'limit_by_per_header', 'limit_by_per_param', 'limit_by_per_consumer', 'limit_by_per_cookie', 'limit_by_per_ip' must be set"),
},
}
for _, tt := range tests {
@@ -234,3 +274,32 @@ func TestParseClusterKeyRateLimitConfig(t *testing.T) {
})
}
}
func TestParseClusterKeyRateLimitConfig_RuleItemsLimit(t *testing.T) {
tests := []struct {
name string
itemCnt int
expectErr string
}{
{"Exactly10", 10, ""},
{"Over10", 11, "rule_items length 11 exceeds maximum 10"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
items := make([]string, 0, tt.itemCnt)
for i := 0; i < tt.itemCnt; i++ {
items = append(items,
fmt.Sprintf(`{"limit_by_header":"h%d","limit_keys":[{"key":"k","query_per_second":1}]}`, i+1))
}
jsonStr := fmt.Sprintf(`{"rule_name":"t","rule_items":[%s]}`, strings.Join(items, ","))
var config ClusterKeyRateLimitConfig
err := ParseClusterKeyRateLimitConfig(gjson.Parse(jsonStr), &config)
if tt.expectErr == "" {
assert.NoError(t, err)
assert.Len(t, config.RuleItems, tt.itemCnt)
} else {
assert.EqualError(t, err, tt.expectErr)
}
})
}
}

View File

@@ -16,6 +16,7 @@ package main
import (
"fmt"
"math"
"net"
"net/url"
"strconv"
@@ -46,30 +47,37 @@ func init() {
const (
// RedisKeyPrefix 集群限流插件在 Redis 中 key 的统一前缀
RedisKeyPrefix = "higress-cluster-key-rate-limit"
// 使用 {rule_name} hash tag 让多规则多键操作在 Redis Cluster 下落到同一 slot
// ClusterGlobalRateLimitFormat 全局限流模式 redis key 为 RedisKeyPrefix:限流规则名称:global_threshold:时间窗口
ClusterGlobalRateLimitFormat = RedisKeyPrefix + ":%s:global_threshold:%d"
ClusterGlobalRateLimitFormat = RedisKeyPrefix + ":{%s}:global_threshold:%d"
// ClusterRateLimitFormat 规则限流模式 redis key 为 RedisKeyPrefix:限流规则名称:限流类型:时间窗口:限流key名称:限流key对应的实际值
ClusterRateLimitFormat = RedisKeyPrefix + ":%s:%s:%d:%s:%s"
FixedWindowScript = `
local key = KEYS[1]
local threshold = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('get', key) or "0")
-- 只有超过阈值时才停止累加,达到阈值时仍允许(此时是最后一次允许)
if current > threshold then
return {threshold, current, redis.call('ttl', key)}
ClusterRateLimitFormat = RedisKeyPrefix + ":{%s}:%s:%d:%s:%s"
// MultiKeyFixedWindowScript 多规则请求阶段 check + incr 合一脚本cluster-key-rate-limit 使用)
// KEYS = [key1, ..., keyN]
// ARGV = [threshold1, window1, ..., thresholdN, windowN]
// 返回嵌套数组 {{threshold_i, current_i, ttl_i}, ...}(每个 key 独立判断是否 incr
MultiKeyFixedWindowScript = `
local results = {}
for i = 1, #KEYS do
local threshold = tonumber(ARGV[2*i - 1])
local window = tonumber(ARGV[2*i])
local current = tonumber(redis.call('get', KEYS[i]) or "0")
local ttl = redis.call('ttl', KEYS[i])
if ttl < 0 then ttl = window end
if current > threshold then
-- 已超阈值,不再 incr
table.insert(results, {threshold, current, ttl})
else
-- 未超阈值,原子 incr
current = redis.call('incr', KEYS[i])
if current == 1 then
redis.call('expire', KEYS[i], window)
end
table.insert(results, {threshold, current, redis.call('ttl', KEYS[i])})
end
end
-- 计数未超过阈值,执行累加
current = redis.call('incr', key)
-- 第一次累加时设置过期时间
if current == 1 then
redis.call('expire', key, window)
end
return {threshold, current, redis.call('ttl', key)}
return results
`
LimitContextKey = "LimitContext" // 限流上下文信息
@@ -87,6 +95,13 @@ type LimitContext struct {
reset int
}
// MatchedRule 表示请求阶段命中的单条限流规则global 或 rule_item
type MatchedRule struct {
key string // 完整 Redis key
count int64 // 时间窗口内的限额
window int64 // 时间窗口大小(秒)
}
func parseConfig(json gjson.Result, cfg *config.ClusterKeyRateLimitConfig) error {
err := config.InitRedisClusterClient(json, cfg)
if err != nil {
@@ -101,51 +116,82 @@ func parseConfig(json gjson.Result, cfg *config.ClusterKeyRateLimitConfig) error
func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.ClusterKeyRateLimitConfig) types.Action {
ctx.DisableReroute()
limitKey, count, timeWindow := "", int64(0), int64(0)
if cfg.GlobalThreshold != nil {
// 全局限流模式
limitKey = fmt.Sprintf(ClusterGlobalRateLimitFormat, cfg.RuleName, cfg.GlobalThreshold.TimeWindow)
count = cfg.GlobalThreshold.Count
timeWindow = cfg.GlobalThreshold.TimeWindow
} else {
// 规则限流模式
val, ruleItem, configItem := checkRequestAgainstLimitRule(ctx, cfg.RuleItems)
if ruleItem == nil || configItem == nil {
// 没有匹配到限流规则直接返回
return types.ActionContinue
}
limitKey = fmt.Sprintf(ClusterRateLimitFormat, cfg.RuleName, ruleItem.LimitType, configItem.TimeWindow, ruleItem.Key, val)
count = configItem.Count
timeWindow = configItem.TimeWindow
matched := collectMatchedRules(ctx, cfg)
if len(matched) == 0 {
// 无任何规则命中:直接放行,不发起 Redis 调用
log.Debugf("cluster-key-rate-limit: no rule matched, path=%s host=%s", ctx.Path(), ctx.Host())
return types.ActionContinue
}
// 执行限流逻辑
keys := []interface{}{limitKey}
args := []interface{}{count, timeWindow}
err := cfg.RedisClient.Eval(FixedWindowScript, 1, keys, args, func(response resp.Value) {
resultArray := response.Array()
if len(resultArray) != 3 {
log.Errorf("redis response parse error, response: %v", response)
proxywasm.ResumeHttpRequest()
log.Debugf("cluster-key-rate-limit: request phase matched %d rule(s), path=%s host=%s",
len(matched), ctx.Path(), ctx.Host())
for i, m := range matched {
log.Debugf("cluster-key-rate-limit: rule[%d] key=%s threshold=%d window=%ds",
i, m.key, m.count, m.window)
}
n := len(matched)
keys := make([]interface{}, n)
args := make([]interface{}, 0, n*2)
for i, m := range matched {
keys[i] = m.key
args = append(args, m.count, m.window)
}
err := cfg.RedisClient.Eval(MultiKeyFixedWindowScript, n, keys, args, func(response resp.Value) {
arr := response.Array()
if len(arr) != n {
log.Errorf("redis response length mismatch: got %d, want %d", len(arr), n)
_ = proxywasm.ResumeHttpRequest()
return
}
// 获取限流结果
threshold, current, ttl := resultArray[0].Integer(), resultArray[1].Integer(), resultArray[2].Integer()
context := LimitContext{
count: threshold,
remaining: threshold - current,
reset: ttl,
}
if current > threshold {
// 触发限流
rejected(cfg, context)
} else {
ctx.SetContext(LimitContextKey, context)
proxywasm.ResumeHttpRequest()
// 使用 math.MaxFloat64 初始化,避免在 arr[0] 校验前预先读取
tightestIdx := 0
tightestRatio := math.MaxFloat64
for i, sub := range arr {
a := sub.Array()
if len(a) != 3 {
log.Errorf("redis sub-array length mismatch: got %d, want 3", len(a))
_ = proxywasm.ResumeHttpRequest()
return
}
threshold, current, ttl := a[0].Integer(), a[1].Integer(), a[2].Integer()
log.Debugf("cluster-key-rate-limit: eval rule[%d] key=%s threshold=%d current=%d ttl=%ds",
i, matched[i].key, threshold, current, ttl)
if current > threshold {
// 命中触发的第一条规则(按 collectMatchedRules 顺序global 优先)
log.Debugf("cluster-key-rate-limit: rule[%d] key=%s triggered (current=%d > threshold=%d), rejecting with code %d",
i, matched[i].key, current, threshold, cfg.RejectedCode)
rejected(cfg, LimitContext{
count: threshold,
remaining: threshold - current,
reset: ttl,
})
return
}
if ratio := float64(threshold-current) / float64(threshold); ratio < tightestRatio {
tightestIdx = i
tightestRatio = ratio
}
}
// 未触发:写入 tightest 规则到 LimitContext供 onHttpResponseHeaders 读取 X-RateLimit-* 头
tightSub := arr[tightestIdx].Array()
tightThreshold, tightCurrent, tightTtl := tightSub[0].Integer(), tightSub[1].Integer(), tightSub[2].Integer()
log.Debugf("cluster-key-rate-limit: all %d rule(s) within threshold, tightest rule[%d] key=%s (count=%d remaining=%d reset=%ds)",
n, tightestIdx, matched[tightestIdx].key, tightThreshold, tightThreshold-tightCurrent, tightTtl)
ctx.SetContext(LimitContextKey, LimitContext{
count: tightThreshold,
remaining: tightThreshold - tightCurrent,
reset: tightTtl,
})
_ = proxywasm.ResumeHttpRequest()
})
if err != nil {
@@ -158,25 +204,43 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, cfg config.ClusterKeyRateLimi
func onHttpResponseHeaders(ctx wrapper.HttpContext, config config.ClusterKeyRateLimitConfig) types.Action {
limitContext, ok := ctx.GetContext(LimitContextKey).(LimitContext)
if !ok {
log.Debugf("cluster-key-rate-limit: response phase reached with no LimitContext, skipping X-RateLimit-* headers")
return types.ActionContinue
}
if config.ShowLimitQuotaHeader {
_ = proxywasm.ReplaceHttpResponseHeader(RateLimitLimitHeader, strconv.Itoa(limitContext.count))
_ = proxywasm.ReplaceHttpResponseHeader(RateLimitRemainingHeader, strconv.Itoa(limitContext.remaining))
log.Debugf("cluster-key-rate-limit: response phase wrote X-RateLimit-Limit=%d X-RateLimit-Remaining=%d",
limitContext.count, limitContext.remaining)
}
return types.ActionContinue
}
func checkRequestAgainstLimitRule(ctx wrapper.HttpContext, ruleItems []config.LimitRuleItem) (string, *config.LimitRuleItem, *config.LimitConfigItem) {
if len(ruleItems) > 0 {
for _, rule := range ruleItems {
val, ruleItem, configItem := hitRateRuleItem(ctx, rule)
if ruleItem != nil && configItem != nil {
return val, ruleItem, configItem
}
// collectMatchedRules 遍历 global_threshold 和 rule_items返回所有命中规则。
// 顺序global_threshold如有rule_items 中所有命中项(按数组顺序追加)。
func collectMatchedRules(ctx wrapper.HttpContext, cfg config.ClusterKeyRateLimitConfig) []MatchedRule {
var matched []MatchedRule
if cfg.GlobalThreshold != nil {
matched = append(matched, MatchedRule{
key: fmt.Sprintf(ClusterGlobalRateLimitFormat, cfg.RuleName, cfg.GlobalThreshold.TimeWindow),
count: cfg.GlobalThreshold.Count,
window: cfg.GlobalThreshold.TimeWindow,
})
}
for _, ruleItem := range cfg.RuleItems {
val, hitRule, hitItem := hitRateRuleItem(ctx, ruleItem)
if hitRule != nil && hitItem != nil {
matched = append(matched, MatchedRule{
key: fmt.Sprintf(ClusterRateLimitFormat, cfg.RuleName, hitRule.LimitType, hitItem.TimeWindow, hitRule.Key, val),
count: hitItem.Count,
window: hitItem.TimeWindow,
})
}
}
return "", nil, nil
return matched
}
func hitRateRuleItem(ctx wrapper.HttpContext, rule config.LimitRuleItem) (string, *config.LimitRuleItem, *config.LimitConfigItem) {

View File

@@ -16,6 +16,8 @@ package main
import (
"encoding/json"
"fmt"
"strings"
"testing"
"cluster-key-rate-limit/config"
@@ -23,6 +25,7 @@ import (
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/test"
"github.com/stretchr/testify/require"
"github.com/tidwall/resp"
)
// 测试配置:全局限流配置
@@ -222,6 +225,56 @@ var regexpLimitConfig = func() json.RawMessage {
return data
}()
// 测试配置混合限流global + rule_items 同时生效)
var hybridLimitConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"rule_name": "cluster-hybrid",
"global_threshold": map[string]interface{}{
"query_per_minute": 10000,
},
"rule_items": []map[string]interface{}{
{
"limit_by_header": "x-api-key",
"limit_keys": []map[string]interface{}{
{"key": "vip-key", "query_per_minute": 100},
},
},
},
"redis": map[string]interface{}{
"service_name": "redis.static",
"service_port": 6379,
},
"show_limit_quota_header": true,
})
return data
}()
// 测试配置:多条 rule_items 同时命中
var multiRuleItemsConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"rule_name": "cluster-multi-items",
"rule_items": []map[string]interface{}{
{
"limit_by_header": "x-api-key",
"limit_keys": []map[string]interface{}{
{"key": "k1", "query_per_minute": 100},
},
},
{
"limit_by_param": "apikey",
"limit_keys": []map[string]interface{}{
{"key": "k2", "query_per_minute": 50},
},
},
},
"redis": map[string]interface{}{
"service_name": "redis.static",
"service_port": 6379,
},
})
return data
}()
func TestParseConfig(t *testing.T) {
test.RunGoTest(t, func(t *testing.T) {
// 测试全局限流配置解析
@@ -385,7 +438,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
// 由于需要调用 Redis应该返回 HeaderStopAllIterationAndWatermark
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
resp := multiRuleResp([3]int{1000, 999, 60})
// 模拟 Redis 调用响应(允许请求)
host.CallOnRedisCall(0, resp)
@@ -409,7 +462,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{10, 9, 60})
resp := multiRuleResp([3]int{10, 9, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -433,7 +486,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{10, 9, 60})
resp := multiRuleResp([3]int{10, 9, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -457,7 +510,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{10, 9, 1})
resp := multiRuleResp([3]int{10, 9, 1})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -481,7 +534,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{10, 9, 60})
resp := multiRuleResp([3]int{10, 9, 60})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -505,7 +558,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 模拟 Redis 调用响应(允许请求)
resp := test.CreateRedisRespArray([]interface{}{10, 9, 86400})
resp := multiRuleResp([3]int{10, 9, 86400})
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
@@ -529,7 +582,7 @@ func TestOnHttpRequestHeaders(t *testing.T) {
// 模拟 Redis 调用响应(触发限流)
// 当前请求数(1001)超过阈值(1000),触发限流
resp := test.CreateRedisRespArray([]interface{}{1000, 1001, 60})
resp := multiRuleResp([3]int{1000, 1001, 60})
host.CallOnRedisCall(0, resp)
// 检查是否发送了限流响应
@@ -540,6 +593,224 @@ func TestOnHttpRequestHeaders(t *testing.T) {
host.CompleteHttp()
})
// 混合限流global + rule_item 同时命中
t.Run("hybrid limit both match", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{10000, 1, 60},
[3]int{100, 1, 60},
)
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
})
// 多条 rule_items 同时命中
t.Run("multi rule_items all match", func(t *testing.T) {
host, status := test.NewTestHost(multiRuleItemsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test?apikey=k2"},
{":method", "GET"},
{"x-api-key", "k1"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{100, 1, 60},
[3]int{50, 1, 60},
)
host.CallOnRedisCall(0, resp)
host.CompleteHttp()
})
// 混合限流global 触发
t.Run("hybrid limit global triggered", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"x-api-key", "vip-key"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp(
[3]int{10000, 10001, 60},
[3]int{100, 1, 60},
)
host.CallOnRedisCall(0, resp)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(429), localResponse.StatusCode)
host.CompleteHttp()
})
// Redis 响应数组长度与规则数不匹配
t.Run("redis response length mismatch", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"x-api-key", "vip-key"},
})
resp := multiRuleResp([3]int{10000, 1, 60}) // 期望 2 条,返回 1 条
host.CallOnRedisCall(0, resp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
// Redis 子数组长度异常(少于 3
t.Run("redis sub-array length mismatch", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"x-api-key", "vip-key"},
})
badResp, err := resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.IntegerValue(10000),
resp.IntegerValue(1),
}),
}).MarshalRESP() // 关键:用 MarshalRESP() 而非 Bytes()
require.NoError(t, err)
host.CallOnRedisCall(0, badResp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
// 混合配置global 命中rule_items 不命中
t.Run("hybrid limit rule_items no match", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
resp := multiRuleResp([3]int{10000, 1, 60})
host.CallOnRedisCall(0, resp)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
// 多规则同时触发,验证 rejected 报告 globalreset=60 而非 rule_item 的 30
t.Run("hybrid limit both triggered reports global first", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"x-api-key", "vip-key"},
})
resp := multiRuleResp(
[3]int{10000, 10001, 60},
[3]int{100, 101, 30},
)
host.CallOnRedisCall(0, resp)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(429), localResponse.StatusCode)
// LocalHttpResponse.Headers 类型为 [][2]string按切片迭代查找目标头
var resetHeader string
for _, h := range localResponse.Headers {
if strings.EqualFold(h[0], "X-RateLimit-Reset") {
resetHeader = h[1]
break
}
}
require.Equal(t, "60", resetHeader, "应报告 global 规则的 reset 时间")
host.CompleteHttp()
})
// 多规则未触发,剩余比例不同 → LimitContext 取 tightest
// cluster-key 可以通过 X-RateLimit-* 响应头验证ai-token 不能)
t.Run("multi-rule no trigger takes tightest", func(t *testing.T) {
host, status := test.NewTestHost(hybridLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"x-api-key", "vip-key"},
})
resp := multiRuleResp(
[3]int{10000, 9000, 60}, // global: 剩余 10%
[3]int{100, 10, 60}, // rule_item: 剩余 90%
)
host.CallOnRedisCall(0, resp)
require.Nil(t, host.GetLocalResponse())
host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"content-type", "application/json"},
})
responseHeaders := host.GetResponseHeaders()
require.True(t, test.HasHeader(responseHeaders, "x-ratelimit-limit"))
require.True(t, test.HasHeader(responseHeaders, "x-ratelimit-remaining"))
for _, h := range responseHeaders {
if strings.EqualFold(h[0], "x-ratelimit-limit") {
require.Equal(t, "10000", h[1], "X-RateLimit-Limit 应为 tightest(global) 的 threshold")
}
if strings.EqualFold(h[0], "x-ratelimit-remaining") {
require.Equal(t, "1000", h[1], "X-RateLimit-Remaining 应为 tightest(global) 的 threshold-current")
}
}
host.CompleteHttp()
})
})
}
@@ -559,7 +830,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
})
// 模拟 Redis 调用响应
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
resp := multiRuleResp([3]int{1000, 999, 60})
host.CallOnRedisCall(0, resp)
// 处理响应头
@@ -609,7 +880,7 @@ func TestOnHttpResponseHeaders(t *testing.T) {
})
// 模拟 Redis 调用响应
resp := test.CreateRedisRespArray([]interface{}{1000, 999, 60})
resp := multiRuleResp([3]int{1000, 999, 60})
host.CallOnRedisCall(0, resp)
// 处理响应头
@@ -628,6 +899,88 @@ func TestOnHttpResponseHeaders(t *testing.T) {
host.CompleteHttp()
})
// 测试响应头处理时 context 中无 LimitContext覆盖 GetContext 返回 nil 的分支)。
// 场景:使用 headerLimitConfig仅 x-ca-key 规则ensureContextInitialized 调用的
// onHttpRequestHeaders 因为缺少 x-ca-key 头而 matched 为空,返回 ActionContinue
// 此时 SetContext 未被调用。后续 onHttpResponseHeaders 应通过类型断言失败分支安全返回。
t.Run("no prior context", func(t *testing.T) {
host, status := test.NewTestHost(headerLimitConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
// 直接处理响应头:测试框架会通过 ensureContextInitialized 触发默认请求头处理,
// 但缺少 x-ca-key因此 collectMatchedRules 返回空SetContext 未调用。
action := host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{"content-type", "application/json"},
})
// 应该返回 ActionContinue且不 panic
require.Equal(t, types.ActionContinue, action)
// 不应写入限流配额响应头
responseHeaders := host.GetResponseHeaders()
require.False(t, test.HasHeader(responseHeaders, "x-ratelimit-limit"))
require.False(t, test.HasHeader(responseHeaders, "x-ratelimit-remaining"))
host.CompleteHttp()
})
})
}
// TestRejectedHidesQuotaHeadersWhenDisabled 验证 rejected() 在 show_limit_quota_header=false
// 时只设置 X-RateLimit-Reset不设置 X-RateLimit-Limit / X-RateLimit-Remaining覆盖 line 339-341
func TestRejectedHidesQuotaHeadersWhenDisabled(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
hideQuotaConfig := func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"rule_name": "routeA-rejected-hide-quota",
"global_threshold": map[string]interface{}{
"query_per_minute": 1,
},
"redis": map[string]interface{}{
"service_name": "redis.static",
"service_port": 6379,
},
"show_limit_quota_header": false,
"rejected_code": 429,
"rejected_msg": "Too many requests",
})
return data
}()
host, status := test.NewTestHost(hideQuotaConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
})
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 触发限流current (2) > threshold (1)
resp := multiRuleResp([3]int{1, 2, 60})
host.CallOnRedisCall(0, resp)
// 应返回 429 本地响应
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(429), localResponse.StatusCode)
// X-RateLimit-Reset 应保留(始终设置)
require.True(t, test.HasHeader(localResponse.Headers, "x-ratelimit-reset"),
"X-RateLimit-Reset should always be present on rejection")
// X-RateLimit-Limit / X-RateLimit-Remaining 应缺失show_limit_quota_header=false
require.False(t, test.HasHeader(localResponse.Headers, "x-ratelimit-limit"),
"X-RateLimit-Limit should be hidden when show_limit_quota_header=false")
require.False(t, test.HasHeader(localResponse.Headers, "x-ratelimit-remaining"),
"X-RateLimit-Remaining should be hidden when show_limit_quota_header=false")
host.CompleteHttp()
})
}
@@ -649,7 +1002,7 @@ func TestCompleteFlow(t *testing.T) {
require.Equal(t, types.HeaderStopAllIterationAndWatermark, action)
// 2. 模拟 Redis 调用响应
resp := test.CreateRedisRespArray([]interface{}{1000, 1, 60})
resp := multiRuleResp([3]int{1000, 1, 60})
host.CallOnRedisCall(0, resp)
// 3. 处理响应头
@@ -672,3 +1025,24 @@ func TestCompleteFlow(t *testing.T) {
})
})
}
// multiRuleResp 构建多规则嵌套 Redis 响应RESP wire format
// test.CreateRedisRespArray 不支持嵌套数组,因此直接用 resp 包构造。
// 每个 [3]int 元组为 {threshold, current, ttl}。
// 注意:不能使用 resp.Value.Bytes()——它返回的是显示用字符串,不是 RESP wire 格式。
// 必须用 resp.Writer.WriteArray 或 Value.MarshalRESP() 输出真正的 RESP 字节流。
func multiRuleResp(items ...[3]int) []byte {
values := make([]resp.Value, len(items))
for i, it := range items {
values[i] = resp.ArrayValue([]resp.Value{
resp.IntegerValue(it[0]),
resp.IntegerValue(it[1]),
resp.IntegerValue(it[2]),
})
}
b, err := resp.ArrayValue(values).MarshalRESP()
if err != nil {
panic(fmt.Sprintf("failed to marshal multiRuleResp: %v", err))
}
return b
}

View File

@@ -10,8 +10,25 @@ description: 跨域资源共享插件配置参考
## 运行属性
插件执行阶段:`授权阶段`
插件执行优先级:`340`
插件执行阶段:`认证阶段`
插件执行优先级:`2000`
## 版本说明
### 2.0.1
相比 `2.0.0`,本版本对齐浏览器 CORS 语义,并将插件执行优先级调整为 `2000`
具体变更和升级注意事项:
* 实际 CORS 请求的 Origin 或 Method 不匹配时,插件不再直接返回 `403`,而是继续转发到后端;插件不会添加 CORS 允许响应头,并会移除后端返回的 CORS policy 响应头,由浏览器按 CORS 规则拦截。若已有监控或客户端逻辑依赖网关直接返回 `403`,升级后需要相应调整。
* CORS 预检请求统一由插件直接返回 `204 No Content`。非法预检请求不会返回 `Access-Control-Allow-*``Access-Control-Expose-Headers``Access-Control-Allow-Credentials``Access-Control-Max-Age`,浏览器会判定预检失败;若已有逻辑依赖非法预检返回 `403`,升级后需要相应调整。
* 同源 `OPTIONS` 请求即使携带类似预检的请求头,也会继续转发到后端,避免被 CORS 插件误拦截。
* `allow_methods: ["*"]` 会在预检响应中回显本次请求的 `Access-Control-Request-Method``allow_headers: ["*"]` 会回显规范化后的 `Access-Control-Request-Headers`,没有请求头时不返回 `Access-Control-Allow-Headers`
* 默认 Method/Header 解析会按逗号拆分并去除空格,避免默认值被当作单个不可匹配的条目。
* Origin 模式匹配会锚定完整 Origin避免类似 `http://api.example.com.evil.com` 误匹配 `http://*.example.com`;如果历史配置依赖非完整匹配,需要调整为明确的 Origin 模式。
*`Access-Control-Allow-Origin` 返回具体 Origin 而不是 `*` 时,插件会合并返回 `Vary: Origin`,避免缓存复用错误的跨域响应。
* `expose_headers: ["*"]` 在携带凭据的请求中仍按兼容方式接受配置,但浏览器会把 `Access-Control-Expose-Headers: *` 当作字面量 Header 名称,而不是暴露所有 Header。
## 配置字段
@@ -19,16 +36,18 @@ description: 跨域资源共享插件配置参考
|-----------------------|-----------------|----------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| allow_origins | array of string | 选填 | * | 允许跨域访问的 Origin格式为 scheme://host:port示例如 http://example.com:8081。当 allow_credentials 为 false 时,可以使用 * 来表示允许所有 Origin 通过 |
| allow_origin_patterns | array of string | 选填 | - | 允许跨域访问的 Origin 模式匹配, 用 * 匹配域名或者端口, <br/>比如 http://*.example.com -- 匹配域名, http://*.example.com:[8080,9090] -- 匹配域名和指定端口, http://*.example.com:[*] -- 匹配域名和所有端口。单独 * 表示匹配所有域名和端口 |
| allow_methods | array of string | 选填 | GET, PUT, POST, DELETE, PATCH, OPTIONS | 允许跨域访问的 Method比如GETPOST 等。可以使用 * 来表示允许所有 Method |
| allow_headers | array of string | 选填 | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | 允许跨域访问时请求方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header |
| expose_headers | array of string | 选填 | - | 允许跨域访问时响应方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header。 |
| allow_methods | array of string | 选填 | GET, PUT, POST, DELETE, PATCH, OPTIONS | 允许跨域访问的 Method比如GETPOST 等。可以使用 * 来表示允许所有 Method;预检响应会回显本次请求的 `Access-Control-Request-Method` |
| allow_headers | array of string | 选填 | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | 允许跨域访问时请求方携带哪些非 CORS 规范以外的 Header。可以使用 * 来表示允许任意 Header;预检响应会回显规范化后的 `Access-Control-Request-Headers`,没有请求头时不返回 `Access-Control-Allow-Headers` |
| expose_headers | array of string | 选填 | - | 允许跨域访问时响应方携带哪些非 CORS 规范以外的 Header。可以使用 *;但当请求携带凭据时,浏览器会把 `Access-Control-Expose-Headers: *` 当作字面量 Header 名称,而不是暴露所有 Header。 |
| allow_credentials | bool | 选填 | false | 是否允许跨域访问的请求方携带凭据(如 Cookie 等)。根据 CORS 规范,如果设置该选项为 true在 allow_origins 不能使用 * 替换成使用 allow_origin_patterns * |
| max_age | number | 选填 | 86400秒 | 浏览器缓存 CORS 结果的最大时间,单位为秒。<br/>在这个时间范围内,浏览器会复用上一次的检查结果 |
> 注意
> * allow_credentials 是一个很敏感的选项请谨慎开启。开启之后allow_credentials 和 allow_origins 为 * 不能同时使用,同时设置时, allow_origins 值为 "*" 生效。
> * allow_origins 和 allow_origin_patterns 可以同时设置, 先检查 allow_origins 是否匹配,然后再检查 allow_origin_patterns 是否匹配
> * 非法 CORS 请求, HTTP 状态码返回是 403 返回体内容为 "Invalid CORS request"
> * 对于实际 CORS 请求,如果 Origin 或 Method 不匹配配置,插件会继续转发到后端,但不会添加 CORS 响应头,并会移除后端返回的 CORS policy 响应头,由浏览器根据 CORS 规则拦截结果。
> * 对于 CORS 预检请求,合法请求由插件直接返回 `204 No Content` 和配置对应的 CORS 响应头;非法请求由插件直接返回 `204 No Content`,但不返回 `Access-Control-Allow-*`、`Access-Control-Expose-Headers`、`Access-Control-Allow-Credentials` 或 `Access-Control-Max-Age`,浏览器会判定预检失败。
> * 当 `Access-Control-Allow-Origin` 返回具体 Origin 而不是 `*` 时,插件会合并返回 `Vary: Origin`,避免缓存复用错误的跨域响应。
## 配置示例
@@ -150,7 +169,7 @@ spec:
configDisable: false
ingress:
- ingress-cors-httpbin
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:1.0.0
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:2.0.1
imagePullPolicy: Always
```
@@ -161,10 +180,11 @@ spec:
curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 200 OK
> x-cors-version: 1.0.0
> x-cors-version: 2.0.1
> access-control-allow-origin: http://httpbin2.example.org:9090
> access-control-expose-headers: X-Custom-Header,X-Env-UTM
> access-control-allow-credentials: true
> vary: Origin
```
#### 预检请求
@@ -172,14 +192,15 @@ curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, Token" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 200 OK
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< access-control-allow-origin: http://httpbin2.example.org:9090
< access-control-allow-methods: GET,POST,PATCH
< access-control-allow-headers: Content-Type,Token,Authorization
< access-control-expose-headers: X-Custom-Header,X-Env-UTM
< access-control-allow-credentials: true
< access-control-max-age: 3600
< vary: Origin
< date: Tue, 23 May 2023 11:41:28 GMT
< server: istio-envoy
< content-length: 0
@@ -190,18 +211,18 @@ curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: http
#### 非法 CORS Origin 预检请求
非法预检请求会返回 `204 No Content`,但不包含 `access-control-allow-*` 等 CORS policy 响应头,浏览器会判定预检失败。
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" http://127.0.0.1/anything/get\?foo\=1
HTTP/1.1 403 Forbidden
< content-length: 70
< content-type: text/plain
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< date: Tue, 23 May 2023 11:27:01 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
#### 非法 CORS Method 预检请求
@@ -209,15 +230,13 @@ Invalid CORS request
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: DELETE" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 403 Forbidden
< content-length: 49
< content-type: text/plain
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< date: Tue, 23 May 2023 11:28:51 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
#### 非法 CORS Header 预检请求
@@ -225,15 +244,13 @@ Invalid CORS request
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: TokenView" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 403 Forbidden
< content-length: 52
< content-type: text/plain
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< date: Tue, 23 May 2023 11:31:03 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
## 参考文档

View File

@@ -7,23 +7,42 @@ description: Cross-Origin Resource Sharing plugin configuration reference
The `cors` plugin can enable CORS (Cross-Origin Resource Sharing) HTTP response headers for the server.
## Execution Attributes
Plugin execution phase: `Authorization Phase`
Plugin execution priority: `340`
Plugin execution phase: `AUTHN`
Plugin execution priority: `2000`
## Release Notes
### 2.0.1
Compared with `2.0.0`, this version aligns the plugin behavior with browser CORS semantics and changes the plugin execution priority to `2000`.
Changes and upgrade notes:
* When the Origin or Method of an actual CORS request does not match the configuration, the plugin no longer returns a direct `403`. The request continues upstream; the plugin does not add CORS allow response headers and removes upstream CORS policy response headers, so browsers block the result according to CORS rules. If monitoring or client logic depends on a gateway-level `403`, adjust it before upgrading.
* CORS preflight requests are answered directly with `204 No Content`. Invalid preflight responses omit `Access-Control-Allow-*`, `Access-Control-Expose-Headers`, `Access-Control-Allow-Credentials`, and `Access-Control-Max-Age`, so browsers treat the preflight as failed. If existing logic depends on invalid preflights returning `403`, adjust it before upgrading.
* Same-origin `OPTIONS` requests continue upstream even when they carry preflight-like request headers, preventing the CORS plugin from intercepting them incorrectly.
* `allow_methods: ["*"]` echoes the current `Access-Control-Request-Method` in preflight responses; `allow_headers: ["*"]` echoes normalized `Access-Control-Request-Headers` and omits `Access-Control-Allow-Headers` when no request headers were requested.
* Default Method/Header values are split by comma and trimmed, preventing defaults from being treated as one unmatchable item.
* Origin pattern matching is anchored to the full Origin value, avoiding accidental matches such as `http://api.example.com.evil.com` for `http://*.example.com`. If an existing configuration relied on partial matching, update it to an explicit Origin pattern.
* When `Access-Control-Allow-Origin` is a specific Origin instead of `*`, the plugin merges `Vary: Origin` into the response to prevent cache reuse with the wrong CORS headers.
* `expose_headers: ["*"]` remains accepted for compatibility when credentials are allowed, but browsers treat `Access-Control-Expose-Headers: *` as a literal header name for credentialed requests instead of exposing all headers.
## Configuration Fields
| Name | Data Type | Required | Default Value | Description |
|-----------------------|------------------|----------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| allow_origins | array of string | Optional | * | Allowed Origins for cross-origin access, formatted as scheme://host:port, for example, http://example.com:8081. When allow_credentials is false, * can be used to allow all Origins through. |
| allow_origin_patterns | array of string | Optional | - | Patterns for matching allowed Origins for cross-origin access, using * to match domain or port, <br/>for example http://*.example.com -- matches domain, http://*.example.com:[8080,9090] -- matches domain and specified ports, http://*.example.com:[*] -- matches domain and all ports. A single * indicates matching all domains and ports. |
| allow_methods | array of string | Optional | GET, PUT, POST, DELETE, PATCH, OPTIONS | Allowed Methods for cross-origin access, for example: GET, POST, etc. * can be used to indicate all Methods are allowed. |
| allow_headers | array of string | Optional | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | Allowed Headers for the requester to carry that are not part of CORS specifications during cross-origin access. * can be used to indicate any Header is allowed. |
| expose_headers | array of string | Optional | - | Allowed Headers for the responder to carry that are not part of CORS specifications during cross-origin access. * can be used to indicate any Header is allowed. |
| allow_methods | array of string | Optional | GET, PUT, POST, DELETE, PATCH, OPTIONS | Allowed Methods for cross-origin access, for example: GET, POST, etc. `*` can be used to indicate all Methods are allowed; preflight responses echo the current `Access-Control-Request-Method`. |
| allow_headers | array of string | Optional | DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,<br/>If-Modified-Since,Cache-Control,Content-Type,Authorization | Allowed Headers for the requester to carry that are not part of CORS specifications during cross-origin access. `*` can be used to indicate any Header is allowed; preflight responses echo normalized `Access-Control-Request-Headers`, and omit `Access-Control-Allow-Headers` when no request headers were requested. |
| expose_headers | array of string | Optional | - | Allowed Headers for the responder to expose during cross-origin access. `*` can be used, but for credentialed requests browsers treat `Access-Control-Expose-Headers: *` as a literal header name instead of exposing all headers. |
| allow_credentials | bool | Optional | false | Whether to allow the requester to carry credentials (e.g. Cookies) during cross-origin access. According to CORS specifications, if this option is set to true, * cannot be used for allow_origins, replace it with allow_origin_patterns. |
| max_age | number | Optional | 86400 seconds | Maximum time for browsers to cache CORS results, in seconds. <br/>Within this time frame, browsers will reuse the previous inspection results. |
> Note
> * allow_credentials is a very sensitive option, please enable it with caution. Once enabled, allow_credentials and allow_origins cannot both be *, if both are set, the allow_origins value of "*" takes effect.
> * allow_origins and allow_origin_patterns can be set simultaneously. First, check if allow_origins matches, then check if allow_origin_patterns matches.
> * Illegal CORS requests will return HTTP status code 403, with the response body content as "Invalid CORS request".
> * For actual CORS requests, if the Origin or Method does not match the configuration, the plugin continues the request upstream, does not add CORS response headers, and removes upstream CORS policy response headers. Browsers then block the result according to CORS rules.
> * For CORS preflight requests, valid requests are answered directly with `204 No Content` and the configured CORS response headers. Invalid requests are answered directly with `204 No Content`, but without `Access-Control-Allow-*`, `Access-Control-Expose-Headers`, `Access-Control-Allow-Credentials`, or `Access-Control-Max-Age`; browsers treat the preflight as failed.
> * When `Access-Control-Allow-Origin` is a specific Origin instead of `*`, the plugin merges `Vary: Origin` into the response to prevent cache reuse with the wrong CORS headers.
## Configuration Examples
### Allow all cross-origin access, without allowing the requester to carry credentials
@@ -142,7 +161,7 @@ spec:
configDisable: false
ingress:
- ingress-cors-httpbin
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:1.0.0
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:2.0.1
imagePullPolicy: Always
```
@@ -151,23 +170,25 @@ spec:
```shell
curl -v -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 200 OK
> x-cors-version: 1.0.0
> x-cors-version: 2.0.1
> access-control-allow-origin: http://httpbin2.example.org:9090
> access-control-expose-headers: X-Custom-Header,X-Env-UTM
> access-control-allow-credentials: true
> vary: Origin
```
#### Preflight Request
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: Content-Type, Token" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 200 OK
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< access-control-allow-origin: http://httpbin2.example.org:9090
< access-control-allow-methods: GET,POST,PATCH
< access-control-allow-headers: Content-Type,Token,Authorization
< access-control-expose-headers: X-Custom-Header,X-Env-UTM
< access-control-allow-credentials: true
< access-control-max-age: 3600
< vary: Origin
< date: Tue, 23 May 2023 11:41:28 GMT
< server: istio-envoy
< content-length: 0
@@ -177,45 +198,41 @@ curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpb
```
#### Illegal CORS Origin Preflight Request
Invalid preflight requests return `204 No Content` without `access-control-allow-*` CORS policy response headers, so browsers treat the preflight as failed.
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" http://127.0.0.1/anything/get\?foo\=1
HTTP/1.1 403 Forbidden
< content-length: 70
< content-type: text/plain
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< date: Tue, 23 May 2023 11:27:01 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
#### Illegal CORS Method Preflight Request
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: DELETE" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 403 Forbidden
< content-length: 49
< content-type: text/plain
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< date: Tue, 23 May 2023 11:28:51 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
#### Illegal CORS Header Preflight Request
```shell
curl -v -X OPTIONS -H "Origin: http://httpbin2.example.org:9090" -H "Host: httpbin.example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: TokenView" http://127.0.0.1/anything/get\?foo\=1
< HTTP/1.1 403 Forbidden
< content-length: 52
< content-type: text/plain
< x-cors-version: 1.0.0
< HTTP/1.1 204 No Content
< x-cors-trace: trace
< date: Tue, 23 May 2023 11:31:03 GMT
< server: istio-envoy
< content-length: 0
<
* Connection #0 to host 127.0.0.1 left intact
Invalid CORS request
```
## Reference Documents

View File

@@ -1 +1 @@
1.0.0
2.0.1

View File

@@ -24,10 +24,9 @@ import (
)
const (
defaultMatchAll = "*"
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultAllAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS, HEAD, TRACE, CONNECT"
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
defaultMatchAll = "*"
defaultAllowMethods = "GET, PUT, POST, DELETE, PATCH, OPTIONS"
defaultAllowHeaders = "DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With," +
"If-Modified-Since,Cache-Control,Content-Type,Authorization"
defaultMaxAge = 86400
protocolHttpName = "http"
@@ -80,6 +79,7 @@ func newOriginPatternFromString(declaredPattern string) OriginPattern {
patternValue += ":(" + strings.ReplaceAll(portList, ",", "|") + ")"
}
}
patternValue = "^" + patternValue + "$"
return OriginPattern{
declaredPattern: declaredPattern,
@@ -143,7 +143,7 @@ type HttpCorsContext struct {
}
func (c *CorsConfig) GetVersion() string {
return "1.0.0"
return "2.0.1"
}
func (c *CorsConfig) FillDefaultValues() {
@@ -151,10 +151,10 @@ func (c *CorsConfig) FillDefaultValues() {
c.allowOrigins = []string{defaultMatchAll}
}
if len(c.allowHeaders) == 0 {
c.allowHeaders = []string{defaultAllowHeaders}
c.allowHeaders = splitCommaSeparatedValues(defaultAllowHeaders)
}
if len(c.allowMethods) == 0 {
c.allowMethods = strings.Split(defaultAllowMethods, "")
c.allowMethods = splitCommaSeparatedValues(defaultAllowMethods)
}
if c.maxAge == 0 {
c.maxAge = defaultMaxAge
@@ -360,21 +360,29 @@ func (c *CorsConfig) checkHeaders(requestHeaders string) (string, bool) {
return "", false
}
allowHeaders := splitCommaSeparatedValues(strings.Join(c.allowHeaders, ","))
if len(allowHeaders) == 0 {
return "", false
}
// Return normalized request headers when allowHeaders contains *.
if containsMatchAll(allowHeaders) {
return strings.Join(splitCommaSeparatedValues(requestHeaders), ","), true
}
if len(requestHeaders) == 0 {
return strings.Join(c.allowHeaders, ","), true
return strings.Join(allowHeaders, ","), true
}
// Return all request headers when allowHeaders contains *
if c.allowHeaders[0] == defaultMatchAll {
return requestHeaders, true
checkHeaders := splitCommaSeparatedValues(requestHeaders)
if len(checkHeaders) == 0 {
return strings.Join(allowHeaders, ","), true
}
checkHeaders := strings.Split(requestHeaders, ",")
// Each request header should be existed in allowHeaders configuration
for _, h := range checkHeaders {
isExist := false
for _, allowHeader := range c.allowHeaders {
if strings.ToLower(h) == strings.ToLower(allowHeader) {
for _, allowHeader := range allowHeaders {
if strings.EqualFold(h, allowHeader) {
isExist = true
break
}
@@ -391,22 +399,48 @@ func (c *CorsConfig) checkMethods(requestMethod string) (string, bool) {
if len(requestMethod) == 0 {
return "", false
}
requestMethod = strings.TrimSpace(requestMethod)
if len(requestMethod) == 0 {
return "", false
}
// Find method existed in allowMethods configuration
for _, method := range c.allowMethods {
allowMethods := splitCommaSeparatedValues(strings.Join(c.allowMethods, ","))
for _, method := range allowMethods {
if method == defaultMatchAll {
return defaultAllAllowMethods, true
return requestMethod, true
}
if strings.ToLower(method) == strings.ToLower(requestMethod) {
return strings.Join(c.allowMethods, ","), true
if strings.EqualFold(method, requestMethod) {
return strings.Join(allowMethods, ","), true
}
}
return "", false
}
func (c *CorsConfig) isPreFlight(origin, method, controllerRequestMethod string) bool {
return len(origin) > 0 && strings.ToLower(method) == strings.ToLower(HttpMethodOptions) && len(controllerRequestMethod) > 0
func splitCommaSeparatedValues(values string) []string {
parts := strings.Split(values, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if len(part) > 0 {
result = append(result, part)
}
}
return result
}
func containsMatchAll(values []string) bool {
for _, value := range values {
if value == defaultMatchAll {
return true
}
}
return false
}
func (c *CorsConfig) isPreFlight(origin, method, controlRequestMethod string) bool {
return len(origin) > 0 && strings.ToLower(method) == strings.ToLower(HttpMethodOptions) && len(controlRequestMethod) > 0
}
func (c *CorsConfig) isCorsRequest(scheme, host, origin string) bool {

View File

@@ -0,0 +1,167 @@
// 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 config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCorsConfigIssue1743FillDefaultValuesSplitsMethodsAndHeaders(t *testing.T) {
c := &CorsConfig{}
c.FillDefaultValues()
assert.Equal(t, []string{"GET", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"}, c.allowMethods)
assert.Contains(t, c.allowHeaders, "Content-Type")
assert.Contains(t, c.allowHeaders, "Authorization")
assert.NotContains(t, c.allowHeaders, defaultAllowHeaders)
allowMethods, methodOk := c.checkMethods("POST")
assert.True(t, methodOk)
assert.Equal(t, "GET,PUT,POST,DELETE,PATCH,OPTIONS", allowMethods)
allowHeaders, headerOk := c.checkHeaders("Content-Type, Authorization")
assert.True(t, headerOk)
assert.Contains(t, allowHeaders, "Content-Type")
assert.Contains(t, allowHeaders, "Authorization")
}
func TestCorsConfigIssue1743CheckHeadersTrimsRequestTokens(t *testing.T) {
c := &CorsConfig{
allowHeaders: []string{"Content-Type", "Authorization"},
}
allowHeaders, ok := c.checkHeaders("Content-Type, Authorization")
assert.True(t, ok)
assert.Equal(t, "Content-Type,Authorization", allowHeaders)
}
func TestCorsConfigIssue1743WildcardMethodsEchoRequestedMethod(t *testing.T) {
c := &CorsConfig{
allowMethods: []string{"*"},
}
allowMethods, ok := c.checkMethods("PROPFIND")
assert.True(t, ok)
assert.Equal(t, "PROPFIND", allowMethods)
}
func TestCorsConfigIssue1743WildcardHeadersEchoTrimmedRequestHeaders(t *testing.T) {
c := &CorsConfig{
allowHeaders: []string{"*"},
}
allowHeaders, ok := c.checkHeaders(" Content-Type, Authorization, ")
assert.True(t, ok)
assert.Equal(t, "Content-Type,Authorization", allowHeaders)
}
func TestCorsConfigIssue1743WildcardHeadersOmitAllowHeadersWhenNoRequestHeaders(t *testing.T) {
c := &CorsConfig{
allowHeaders: []string{"*"},
}
allowHeaders, ok := c.checkHeaders("")
assert.True(t, ok)
assert.Empty(t, allowHeaders)
}
func TestCorsConfigIssue1743ProcessWildcardPreflight(t *testing.T) {
c := &CorsConfig{
allowOrigins: []string{"*"},
allowMethods: []string{"*"},
allowHeaders: []string{"*"},
}
httpCorsContext, err := c.Process("https", "api.example.com", "OPTIONS", [][2]string{
{HeaderOrigin, "https://client.example.com"},
{HeaderControlRequestMethod, "PROPFIND"},
{HeaderControlRequestHeaders, " Content-Type, Authorization, "},
})
require.NoError(t, err)
assert.True(t, httpCorsContext.IsValid)
assert.True(t, httpCorsContext.IsPreFlight)
assert.True(t, httpCorsContext.IsCorsRequest)
assert.Equal(t, "PROPFIND", httpCorsContext.AllowMethods)
assert.Equal(t, "Content-Type,Authorization", httpCorsContext.AllowHeaders)
}
func TestCorsConfigIssue1743ProcessWildcardPreflightOmitsAllowHeadersWithoutRequestHeaders(t *testing.T) {
c := &CorsConfig{
allowOrigins: []string{"*"},
allowMethods: []string{"*"},
allowHeaders: []string{"*"},
}
httpCorsContext, err := c.Process("https", "api.example.com", "OPTIONS", [][2]string{
{HeaderOrigin, "https://client.example.com"},
{HeaderControlRequestMethod, "PROPFIND"},
})
require.NoError(t, err)
assert.True(t, httpCorsContext.IsValid)
assert.Equal(t, "PROPFIND", httpCorsContext.AllowMethods)
assert.Empty(t, httpCorsContext.AllowHeaders)
}
func TestCorsConfigIssue1743OriginPatternAnchored(t *testing.T) {
c := &CorsConfig{
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com"),
},
}
allowOrigin, ok := c.checkOrigin("http://api.example.com")
assert.True(t, ok)
assert.Equal(t, "http://api.example.com", allowOrigin)
allowOrigin, ok = c.checkOrigin("http://api.example.com.evil.com")
assert.False(t, ok)
assert.Empty(t, allowOrigin)
}
func TestCorsConfigIssue1743OriginPatternWithPortsAnchored(t *testing.T) {
c := &CorsConfig{
allowOriginPatterns: []OriginPattern{
newOriginPatternFromString("http://*.example.com:[8080,9090]"),
},
}
allowOrigin, ok := c.checkOrigin("http://api.example.com:8080")
assert.True(t, ok)
assert.Equal(t, "http://api.example.com:8080", allowOrigin)
allowOrigin, ok = c.checkOrigin("http://api.example.com:8080.evil.com")
assert.False(t, ok)
assert.Empty(t, allowOrigin)
}
func TestCorsConfigIssue1743ExposeHeadersWildcardWithCredentialsAccepted(t *testing.T) {
c := &CorsConfig{}
c.AddExposeHeader("*")
err := c.SetAllowCredentials(true)
require.NoError(t, err)
assert.Equal(t, []string{"*"}, c.exposeHeaders)
assert.True(t, c.allowCredentials)
}

View File

@@ -143,35 +143,35 @@ func TestCorsConfig_isPreFlight(t *testing.T) {
name string
origin string
method string
controllerRequestMethod string
controlRequestMethod string
want bool
}{
{
name: "blank case",
origin: "",
method: "",
controllerRequestMethod: "",
controlRequestMethod:"",
want: false,
},
{
name: "normal case",
origin: "http://httpbin.example.com",
method: "Options",
controllerRequestMethod: "PUT",
controlRequestMethod:"PUT",
want: true,
},
{
name: "bad case with diff method",
origin: "http://httpbin.example.com",
method: "GET",
controllerRequestMethod: "PUT",
controlRequestMethod:"PUT",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &CorsConfig{}
assert.Equalf(t, tt.want, c.isPreFlight(tt.origin, tt.method, tt.controllerRequestMethod), "isPreFlight(%v, %v, %v)", tt.origin, tt.method, tt.controllerRequestMethod)
assert.Equalf(t, tt.want, c.isPreFlight(tt.origin, tt.method, tt.controlRequestMethod), "isPreFlight(%v, %v, %v)", tt.origin, tt.method, tt.controlRequestMethod)
})
}
}
@@ -188,7 +188,7 @@ func TestCorsConfig_checkMethods(t *testing.T) {
name: "default *",
allowMethods: []string{"*"},
requestMethod: "GET",
wantMethods: defaultAllAllowMethods,
wantMethods: "GET",
wantOk: true,
},
{

View File

@@ -63,5 +63,5 @@ spec:
configDisable: false
ingress:
- ingress-cors-httpbin
url: oci://docker.io/2456868764/cors:1.0.0
imagePullPolicy: Always
url: oci://higress-registry.cn-hangzhou.cr.aliyuncs.com/plugins/cors:2.0.1
imagePullPolicy: Always

View File

@@ -18,6 +18,7 @@ import (
"cors/config"
"fmt"
"net/http"
"strings"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
@@ -28,6 +29,11 @@ import (
func main() {}
const (
headerVary = "Vary"
varyOrigin = "Origin"
)
func init() {
wrapper.SetCtx(
"cors",
@@ -94,19 +100,17 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig,
// Set HttpContext
ctx.SetContext(config.HttpContextKey, httpCorsContext)
// Response forbidden when it is not valid cors request
if !httpCorsContext.IsValid {
headers := make([][2]string, 0)
headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"})
proxywasm.SendHttpResponseWithDetail(http.StatusForbidden, "cors.forbidden", headers, []byte("Invalid CORS request"), -1)
return types.ActionPause
}
// Response directly when it is cors preflight request
if httpCorsContext.IsPreFlight {
if httpCorsContext.IsCorsRequest && httpCorsContext.IsPreFlight {
headers := make([][2]string, 0)
headers = append(headers, [2]string{config.HeaderPluginTrace, "trace"})
proxywasm.SendHttpResponseWithDetail(http.StatusOK, "cores.preflight", headers, nil, -1)
if !httpCorsContext.IsValid {
proxywasm.SendHttpResponseWithDetail(http.StatusNoContent, "cors.preflight.invalid", headers, nil, -1)
return types.ActionPause
}
headers = append(headers, buildCorsHeaders(httpCorsContext)...)
headers = appendVaryOriginHeader(headers, httpCorsContext.AllowOrigin)
proxywasm.SendHttpResponseWithDetail(http.StatusNoContent, "cors.preflight", headers, nil, -1)
return types.ActionPause
}
@@ -120,6 +124,7 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig
// Remove upstream cors response headers if existed
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowOrigin)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowMethods)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowHeaders)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlExposeHeaders)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlAllowCredentials)
proxywasm.RemoveHttpResponseHeader(config.HeaderAccessControlMaxAge)
@@ -140,24 +145,52 @@ func onHttpResponseHeaders(ctx wrapper.HttpContext, corsConfig config.CorsConfig
}
// Add Cors headers when it is cors and valid request
if len(httpCorsContext.AllowOrigin) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowOrigin, httpCorsContext.AllowOrigin)
}
if len(httpCorsContext.AllowMethods) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowMethods, httpCorsContext.AllowMethods)
}
if len(httpCorsContext.AllowHeaders) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowHeaders, httpCorsContext.AllowHeaders)
}
if len(httpCorsContext.ExposeHeaders) > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlExposeHeaders, httpCorsContext.ExposeHeaders)
}
if httpCorsContext.AllowCredentials {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlAllowCredentials, "true")
}
if httpCorsContext.MaxAge > 0 {
proxywasm.AddHttpResponseHeader(config.HeaderAccessControlMaxAge, fmt.Sprintf("%d", httpCorsContext.MaxAge))
for _, header := range buildCorsHeaders(httpCorsContext) {
proxywasm.AddHttpResponseHeader(header[0], header[1])
}
addVaryOriginHeader(httpCorsContext.AllowOrigin)
return types.ActionContinue
}
func buildCorsHeaders(httpCorsContext config.HttpCorsContext) [][2]string {
headers := make([][2]string, 0, 6)
if len(httpCorsContext.AllowOrigin) > 0 {
headers = append(headers, [2]string{config.HeaderAccessControlAllowOrigin, httpCorsContext.AllowOrigin})
}
if len(httpCorsContext.AllowMethods) > 0 {
headers = append(headers, [2]string{config.HeaderAccessControlAllowMethods, httpCorsContext.AllowMethods})
}
if len(httpCorsContext.AllowHeaders) > 0 {
headers = append(headers, [2]string{config.HeaderAccessControlAllowHeaders, httpCorsContext.AllowHeaders})
}
if len(httpCorsContext.ExposeHeaders) > 0 {
headers = append(headers, [2]string{config.HeaderAccessControlExposeHeaders, httpCorsContext.ExposeHeaders})
}
if httpCorsContext.AllowCredentials {
headers = append(headers, [2]string{config.HeaderAccessControlAllowCredentials, "true"})
}
if httpCorsContext.MaxAge > 0 {
headers = append(headers, [2]string{config.HeaderAccessControlMaxAge, fmt.Sprintf("%d", httpCorsContext.MaxAge)})
}
return headers
}
func appendVaryOriginHeader(headers [][2]string, allowOrigin string) [][2]string {
if !shouldVaryOrigin(allowOrigin) {
return headers
}
return append(headers, [2]string{headerVary, varyOrigin})
}
func addVaryOriginHeader(allowOrigin string) {
if !shouldVaryOrigin(allowOrigin) {
return
}
proxywasm.AddHttpResponseHeader(headerVary, varyOrigin)
}
func shouldVaryOrigin(allowOrigin string) bool {
allowOrigin = strings.TrimSpace(allowOrigin)
return len(allowOrigin) > 0 && allowOrigin != "*"
}

View File

@@ -0,0 +1,154 @@
// 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 main
import (
"net/http"
"testing"
"cors/config"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/higress-group/wasm-go/pkg/test"
"github.com/stretchr/testify/require"
)
func TestIssue1743InvalidActualCorsRequestContinuesUpstream(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicCorsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":scheme", "http"},
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "GET"},
{"origin", "http://invalid.com"},
})
require.Equal(t, types.ActionContinue, action)
require.Nil(t, host.GetLocalResponse())
action = host.CallOnHttpResponseHeaders([][2]string{
{":status", "200"},
{config.HeaderAccessControlAllowOrigin, "http://invalid.com"},
{config.HeaderAccessControlAllowMethods, "GET,POST"},
{config.HeaderAccessControlAllowHeaders, "X-Upstream"},
{config.HeaderAccessControlExposeHeaders, "X-Upstream-Expose"},
{config.HeaderAccessControlAllowCredentials, "true"},
{config.HeaderAccessControlMaxAge, "600"},
})
require.Equal(t, types.ActionContinue, action)
responseHeaders := host.GetResponseHeaders()
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowOrigin))
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowMethods))
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowHeaders))
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlExposeHeaders))
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlAllowCredentials))
require.False(t, test.HasHeader(responseHeaders, config.HeaderAccessControlMaxAge))
host.CompleteHttp()
})
}
func TestIssue1743SameOriginOptionsWithPreflightHeadersContinuesUpstream(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicCorsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":scheme", "http"},
{":authority", "example.com"},
{":path", "/api/test"},
{":method", "OPTIONS"},
{"origin", "http://example.com"},
{"access-control-request-method", "POST"},
})
require.Equal(t, types.ActionContinue, action)
require.Nil(t, host.GetLocalResponse())
host.CompleteHttp()
})
}
func TestIssue1743ValidPreflightReturnsNoContentWithCorsHeaders(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicCorsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":scheme", "http"},
{":authority", "api.example.com"},
{":path", "/api/test"},
{":method", "OPTIONS"},
{"origin", "http://example.com"},
{"access-control-request-method", "POST"},
{"access-control-request-headers", "Content-Type"},
})
require.Equal(t, types.ActionPause, action)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(http.StatusNoContent), localResponse.StatusCode)
require.Equal(t, "cors.preflight", localResponse.StatusCodeDetail)
require.Empty(t, localResponse.Data)
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderPluginTrace, "trace"))
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowOrigin, "http://example.com"))
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowMethods, "GET,POST,OPTIONS"))
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlAllowHeaders, "Content-Type,Authorization"))
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlExposeHeaders, "X-Custom-Header"))
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderAccessControlMaxAge, "3600"))
host.CompleteHttp()
})
}
func TestIssue1743InvalidPreflightReturnsNoContentWithoutCorsHeaders(t *testing.T) {
test.RunTest(t, func(t *testing.T) {
host, status := test.NewTestHost(basicCorsConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":scheme", "http"},
{":authority", "api.example.com"},
{":path", "/api/test"},
{":method", "OPTIONS"},
{"origin", "http://invalid.com"},
{"access-control-request-method", "POST"},
})
require.Equal(t, types.ActionPause, action)
localResponse := host.GetLocalResponse()
require.NotNil(t, localResponse)
require.Equal(t, uint32(http.StatusNoContent), localResponse.StatusCode)
require.Equal(t, "cors.preflight.invalid", localResponse.StatusCodeDetail)
require.Empty(t, localResponse.Data)
require.True(t, test.HasHeaderWithValue(localResponse.Headers, config.HeaderPluginTrace, "trace"))
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowOrigin))
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowMethods))
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowHeaders))
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlExposeHeaders))
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlAllowCredentials))
require.False(t, test.HasHeader(localResponse.Headers, config.HeaderAccessControlMaxAge))
host.CompleteHttp()
})
}

Some files were not shown because too many files have changed in this diff Show More