win 0fefedf9cd feat(claude-mimic): upgrade Claude Code mimicry to 2.1.145 via bundle abstraction
反编译本地 Claude Code 2.1.145 二进制 (Bun 1.3.14 打包,
@anthropic-ai/sdk@0.94.0 嵌入) 提取真实指纹,系统性升级 mimicry。

核心改动:
- 新增 ClaudeCodeBundle struct 作为单一事实源,DefaultBundle 描述当前
  伪装目标的完整快照 (CLIVersion/SDKVersion/RuntimeVersion/OS/Arch)
- DefaultCLIVersion/DefaultStainlessPackageVersion/CLICurrentVersion/
  DefaultHeaders 全部派生自 DefaultBundle,消除三处 (2.1.92, 2.1.104,
  0.70.0, 0.81.0) 版本分裂
- CLI 版本 2.1.92/2.1.104 -> 2.1.145
- SDK 版本 0.70.0/0.81.0 -> 0.94.0
- 新增 12 个 2.1.145 反编译确认的 anthropic-beta token:
  advanced-tool-use, tool-search-tool, mcp-servers, mcp-client,
  mid-conversation-system, afk-mode, cache-diagnosis, context-hint,
  environments, managed-agents, skills, compact
- FullClaudeCodeMimicryBetas() 从 7 个 token 升级到 21 个 ordered list
- 修正 BetaTokenEfficientTools 错日期 (2026-03-28 -> 2025-02-19)
- 从默认 beta header 移除已 GA 的 BetaFineGrainedToolStreaming /
  BetaTokenEfficientTools (常量保留供客户端显式 merge)
- claudemask.RequiredNodeHeaders 加 X-Claude-Code-Session-Id 强制

新增 ensureClaudeCodeSessionID helper (claude_code_session_id.go):
- 真实 CLI 在 SDK 内强制 X-Claude-Code-Session-Id:y_(),缺失被判第三方
- OAuth mimic 路径: metadata.user_id 派生 -> canonical UUID 写入
  -> 兜底 uuid.NewString()
- API key passthrough 路径: 不从 body 派生,保护客户端原始语义
- 所有路径均对客户端传入的非法 UUID 执行删除 (避免恶意值上游透传)
- 所有写入 header 的 session-id 都通过 uuid.Parse 校验

测试:
- 新增 14 个 ensureClaudeCodeSessionID 单元测试,含恶意 UUID 注入拒绝
  + API key 路径隔离 + canonical 形式校验
- 新增 3 个 bundle 派生一致性测试
- mask_test 加 session-id 缺失校验 case
- 老 UA 断言 2.1.104 -> 2.1.145

不在范围:
- TLS 指纹 (utls 已处理)
- Bun.hash vs xxHash64 算法验证 (需 golden vectors,独立项目)

References:
- VERSION:2.1.145 BUILD_TIME:2026-05-19T01:36:35Z
  GIT_SHA:daa4c3755d45ab0cf97bb41db8c03bd2dfd2ff5f
2026-05-20 17:18:47 +08:00

115 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package claudemask provides Node.js client emulation for Claude API requests
package claudemask
import (
"net/http"
"strings"
"github.com/Wei-Shaw/sub2api/internal/pkg/claude"
)
// GoClientIndicators 是可能暴露 Go 客户端身份的 HTTP 头列表
// 这些头需要被清除或伪装
var GoClientIndicators = []string{
"Go-Http-Client/",
"User-Agent",
// Go 默认在 User-Agent 中会包含 "Go-http-client"
}
// SuspiciousHeaders 是在 Claude API 请求中不应出现的头
// 或应被移除的头(非 Node.js 客户端会发送)
var SuspiciousHeaders = []string{
"Accept-Encoding", // Go http.Client 自动添加,但应由 utls 处理
"Content-Length", // 应由 http.Transport 自动管理
}
// RequiredNodeHeaders 是 Node.js Claude Code 客户端必须有的头
//
// 来源:反编译 Claude Code 2.1.145 binary确认这些头在每个 /v1/messages
// 与 /v1/messages/count_tokens 请求中都会被 SDK 强制设置。
var RequiredNodeHeaders = map[string]bool{
"User-Agent": true,
"X-Stainless-Lang": true,
"X-Stainless-Runtime": true,
"X-Stainless-Runtime-Version": true,
"X-Stainless-Package-Version": true,
"X-Stainless-OS": true,
"X-Stainless-Arch": true,
"anthropic-version": true,
// 2.1.145 强制SDK 内 `"X-Claude-Code-Session-Id":y_()`
// 上游用其识别会话;缺失会被判第三方。
"X-Claude-Code-Session-Id": true,
}
// ValidateNodeEmulation 验证请求是否正确伪装为 Node.js 客户端
// 返回 (isValid, errorMessages)
func ValidateNodeEmulation(req *http.Request) (bool, []string) {
if req == nil {
return false, []string{"request is nil"}
}
var errors []string
// 检查必要的 Node.js 指纹头
for header := range RequiredNodeHeaders {
if req.Header.Get(header) == "" {
errors = append(errors, "missing required header: "+header)
}
}
// 检查是否包含 Go 客户端指示
ua := req.Header.Get("User-Agent")
if ua == "" {
errors = append(errors, "User-Agent is empty")
} else if strings.Contains(ua, "Go-http-client") {
errors = append(errors, "User-Agent contains Go-http-client indicator")
} else if !strings.Contains(ua, "claude-cli") && !strings.Contains(ua, "node") {
errors = append(errors, "User-Agent does not contain Node.js indicators")
}
// 验证 X-Stainless-Runtime 应为 "node"
runtime := req.Header.Get("X-Stainless-Runtime")
if runtime != "node" {
errors = append(errors, "X-Stainless-Runtime should be 'node', got: "+runtime)
}
// 验证 X-Stainless-Lang 应为 "js"
lang := req.Header.Get("X-Stainless-Lang")
if lang != "js" {
errors = append(errors, "X-Stainless-Lang should be 'js', got: "+lang)
}
return len(errors) == 0, errors
}
// CleanRequest 清除或修复任何会暴露 Go 客户端身份的请求头
// 这是一个"猴子补丁",用于修复任何遗漏的伪装
func CleanRequest(req *http.Request) {
if req == nil {
return
}
// 检查并修复可疑的 User-Agent
ua := req.Header.Get("User-Agent")
if ua == "" || strings.Contains(ua, "Go-http-client") {
// 如果 User-Agent 缺失或包含 Go 指示,设置为 Node.js 格式
req.Header.Set("User-Agent", claude.DefaultUserAgent())
}
// 确保 Accept-Encoding 由 utls 而非 http.Client 设置
// (通常 utls 会接管,但为保险起见检查)
if req.Header.Get("Accept-Encoding") != "" {
// 这通常是安全的,但某些情况下可能需要调整
}
// 移除可能暴露 Go 版本的头
req.Header.Del("Go-Version") // 不标准,但为安全起见
}
// ValidateAndClean 组合验证和清理
// 返回验证结果和清理后的请求
func ValidateAndClean(req *http.Request) (bool, []string) {
CleanRequest(req)
return ValidateNodeEmulation(req)
}