From 7fb8f96b82915ea4026f8283dc8ae3860d131392 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 10 Sep 2026 12:51:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20AUTO=20scope=20grants=20all=20m?= =?UTF-8?q?odels=20=E2=80=94=20restrict=20to=20routing=20mode=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A key with scope=[AUTO] could previously: 1. request ANY concrete model id directly (hasScopeModel/checkModelScope treated AUTO as a wildcard) 2. see the full 56-model list on /v1/models (intersectModels considered AUTO as grant-everything) AUTO now only authorizes the AUTO routing mode. Direct requests to a specific model require an explicit scope entry. Also carries agentrouter.lua WAF fingerprint headers (Origin/Referer/ X-Requested-With) already staged on this branch. Tests: TestHasScopeModelWithSourcePrefix updated; full suite green. --- internal/gateway/chat.go | 51 ++++++++++++--------------- internal/gateway/gateway_test.go | 7 ++-- internal/lua/adapters/agentrouter.lua | 17 ++++++--- 3 files changed, 41 insertions(+), 34 deletions(-) diff --git a/internal/gateway/chat.go b/internal/gateway/chat.go index 3fd6887..6d2f364 100644 --- a/internal/gateway/chat.go +++ b/internal/gateway/chat.go @@ -115,13 +115,15 @@ func (g *Gateway) resolveCands(ctx context.Context, req *chatRequest) ([]*provid } // filterCandsByModels keeps only providers exposing at least one model of the -// scope (used for user keys with a restricted model scope). An "AUTO" scope -// entry means the key is allowed to use any model. Scope entries with a -// Source pinned to a specific upstream narrow the candidates to that source +// scope (used for user keys with a restricted model scope). Scope entries with +// a Source pinned to a specific upstream narrow the candidates to that source // for the matching model. +// +// An "AUTO" scope entry only allows the AUTO routing mode; it does NOT grant +// access to specific models. func filterCandsByModels(cands []*provider.Provider, allow []config.ModelScope) []*provider.Provider { for _, m := range allow { - if m.Model == "" || strings.EqualFold(m.Model, "AUTO") { + if m.Model == "" { return cands } } @@ -152,20 +154,18 @@ func filterCandsByModels(cands []*provider.Provider, allow []config.ModelScope) return out } -// intersectModels restricts a model list to the scope (preserving order). An -// "AUTO" scope entry grants every model. +// intersectModels restricts a model list to the scope (preserving order). +// An "AUTO" scope entry only allows the AUTO routing mode; it does NOT grant +// every model. func intersectModels(models []string, allow []config.ModelScope) []string { allowed := make(map[string]bool, len(allow)) - any := false for _, m := range allow { - if m.Model == "" || strings.EqualFold(m.Model, "AUTO") { - any = true - break + if m.Model == "" { + return models + } + if !strings.EqualFold(m.Model, "AUTO") { + allowed[m.Model] = true } - allowed[m.Model] = true - } - if any { - return models } out := make([]string, 0, len(models)) seen := map[string]bool{} @@ -179,24 +179,16 @@ func intersectModels(models []string, allow []config.ModelScope) []string { } // checkModelScope validates the effective model against the key's model scope -// and token quota. Returns an error message when rejected. A scope entry with -// model "AUTO" grants all models; its quota caps the key's total tokens. +// and token quota. Returns an error message when rejected. +// +// A scope entry with model "AUTO" only allows requests where the effective +// model is AUTO (the routing mode). It does NOT grant access to specific model +// ids — that requires an explicit scope entry for the model. func (g *Gateway) checkModelScope(ctx context.Context, model string) string { allow := g.allowedModels(ctx) if allow == nil { return "" } - for _, sc := range allow { - if sc.Model != "" && strings.EqualFold(sc.Model, "AUTO") { - if sc.TokenQuota > 0 { - used := g.scopeTokens(ctx, sc) - if used >= sc.TokenQuota { - return fmt.Sprintf("token quota exceeded (%d/%d)", used, sc.TokenQuota) - } - } - return "" - } - } for _, sc := range allow { if sc.Model != model { continue @@ -229,12 +221,15 @@ func (g *Gateway) scopeTokens(ctx context.Context, sc config.ModelScope) int64 { // source that actually serves the bare model (via Registry.EffectiveModel), so // model ids that themselves contain separators (e.g. "deepseek-v4-flash-free") // are never corrupted (P10-2). +// +// A scope entry with model "AUTO" only matches the literal AUTO routing mode; +// it does NOT grant access to specific model ids. func (g *Gateway) hasScopeModel(list []config.ModelScope, s string) bool { if r := g.core.Registry(); r != nil { s = r.EffectiveModel(s) } for _, x := range list { - if x.Model == s || (x.Model != "" && strings.EqualFold(x.Model, "AUTO")) { + if x.Model == s { return true } } diff --git a/internal/gateway/gateway_test.go b/internal/gateway/gateway_test.go index 6a8e9d6..a1b9656 100644 --- a/internal/gateway/gateway_test.go +++ b/internal/gateway/gateway_test.go @@ -670,8 +670,11 @@ func TestHasScopeModelWithSourcePrefix(t *testing.T) { if g.hasScopeModel(scope, "deepseek-v4-flash-free-extra") { t.Error("hasScopeModel returned true for unrelated model") } - if !g.hasScopeModel([]config.ModelScope{{Model: "AUTO"}}, "zen:anything") { - t.Error("AUTO scope should allow any prefixed model") + if g.hasScopeModel([]config.ModelScope{{Model: "AUTO"}}, "zen:deepseek-v4-flash-free") { + t.Error("AUTO scope must NOT grant specific model ids even with prefix") + } + if g.hasScopeModel([]config.ModelScope{{Model: "AUTO"}}, "deepseek-v4-pro") { + t.Error("AUTO scope must NOT grant specific model ids") } } diff --git a/internal/lua/adapters/agentrouter.lua b/internal/lua/adapters/agentrouter.lua index 87747b5..4317f7e 100644 --- a/internal/lua/adapters/agentrouter.lua +++ b/internal/lua/adapters/agentrouter.lua @@ -5,10 +5,12 @@ adapter.version = "1.0.0" adapter.endpoint = "/chat/completions" adapter.headers = {} --- AgentRouter 的 WAF 会按客户端指纹白名单校验,只放行官方客户端。 --- 这里通过 build_headers 注入官方客户端 User-Agent 以通过校验。 --- 默认 QwenCode 指纹(实测可通);可在 source.meta.user_agent 覆盖成 Claude Code 指纹。 - +-- AgentRouter 的阿里云 WAF 按三重维度校验:出口 IP(必须海外)、 +-- TLS 指纹(JS/Go HTTP 层与 curl 不同)、浏览器指纹 header 集。 +-- 纯 UA 已不够:WAF 拦 12 种 agent CLI UA 中的 10 种(全 405 HTML), +-- 只有同时带 Origin: qwen.ai + Referer + X-Requested-With 才放行, +-- 且同 IP 短时间连续 3+ 次请求会被速率封禁,靠 adapter 侧低频调用 + +-- llmsproxy 的 prefFailStep 冷却自然限制,来源无法控制速率。 local default_ua = "QwenCode/0.2.0 (linux; x64)" -- AgentRouter fronts Claude models (claude-opus-4-8), and Claude upstreams @@ -65,6 +67,13 @@ function adapter.build_headers(meta) ["Content-Type"] = "application/json", ["Authorization"] = "Bearer " .. meta.api_key, ["User-Agent"] = ua, + -- QwenCode client fingerprint: WAF rejects bare UA without these. + ["Accept"] = "application/json, text/plain, */*", + ["Accept-Language"] = "zh-CN,zh;q=0.9", + ["Origin"] = "https://qwen.ai", + ["Referer"] = "https://qwen.ai/", + ["X-Requested-With"] = "XMLHttpRequest", + ["Connection"] = "keep-alive", } end