Conversation
基于 Claude Code v2.1.88 源码分析,修复多个可被 Anthropic 检测的差距: - 实现消息指纹算法(SHA256 盐值 + 字符索引),替代随机 buildHash - billing header cc_version 从设备 profile 动态取版本号,不再硬编码 - billing header cc_entrypoint 从客户端 UA 解析,支持 cli/vscode/local-agent - billing header 新增 cc_workload 支持(通过 X-CPA-Claude-Workload 头传入) - 新增 X-Claude-Code-Session-Id 头(每 apiKey 缓存 UUID,TTL=1h) - 新增 x-client-request-id 头(仅 api.anthropic.com,每请求 UUID) - 补全 4 个缺失的 beta flags(structured-outputs/fast-mode/redact-thinking/token-efficient-tools) - OAuth scope 对齐 Claude Code 2.1.88(移除 org:create_api_key,添加 sessions/mcp/file_upload) - Anthropic-Dangerous-Direct-Browser-Access 仅在 API key 模式发送 - 响应头网关指纹清洗(剥离 litellm/helicone/portkey/cloudflare/kong/braintrust 前缀头)
Claude executor 的 API 请求之前使用 Go 标准库 crypto/tls,JA3 指纹 与真实 Claude Code(Bun/BoringSSL)不匹配,可被 Cloudflare 识别。 - 新增 helps/utls_client.go,封装 utls Chrome 指纹 + HTTP/2 + 代理支持 - Claude executor 的 4 处 NewProxyAwareHTTPClient 替换为 NewUtlsHTTPClient - 其他 executor(Gemini/Codex/iFlow 等)不受影响,仍用标准 TLS - 非 HTTPS 请求自动回退到标准 transport
- computeFingerprint 使用 rune 索引替代字节索引,修复多字节字符指纹不匹配 - utls Chrome TLS 指纹仅对 Anthropic 官方域名生效,自定义 base_url 走标准 transport - IPv6 地址使用 net.JoinHostPort 正确拼接端口
There was a problem hiding this comment.
Code Review
This pull request enhances the proxy's emulation of the Claude Code CLI by implementing a utls HTTP client with Chrome fingerprinting, session ID caching, and AI gateway header stripping. It also updates system prompt injection to include deterministic fingerprints and workload metadata. Feedback identifies a critical performance issue where the HTTP client is recreated per request, defeating connection pooling, and suggests an optimization for the fingerprinting logic to handle large prompts more efficiently.
| func NewUtlsHTTPClient(cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { | ||
| var proxyURL string | ||
| if auth != nil { | ||
| proxyURL = strings.TrimSpace(auth.ProxyURL) | ||
| } | ||
| if proxyURL == "" && cfg != nil { | ||
| proxyURL = strings.TrimSpace(cfg.ProxyURL) | ||
| } | ||
|
|
||
| utlsRT := newUtlsRoundTripper(proxyURL) | ||
|
|
||
| var standardTransport http.RoundTripper = &http.Transport{ | ||
| DialContext: (&net.Dialer{ | ||
| Timeout: 30 * time.Second, | ||
| KeepAlive: 30 * time.Second, | ||
| }).DialContext, | ||
| } | ||
| if proxyURL != "" { | ||
| if transport := buildProxyTransport(proxyURL); transport != nil { | ||
| standardTransport = transport | ||
| } | ||
| } | ||
|
|
||
| client := &http.Client{ | ||
| Transport: &fallbackRoundTripper{ | ||
| utls: utlsRT, | ||
| fallback: standardTransport, | ||
| }, | ||
| } | ||
| if timeout > 0 { | ||
| client.Timeout = timeout | ||
| } | ||
| return client | ||
| } |
There was a problem hiding this comment.
The NewUtlsHTTPClient function instantiates a new utlsRoundTripper and a new http.Transport (via standardTransport) for every call. Since this function is invoked for every request in ClaudeExecutor (e.g., in Execute, ExecuteStream, and CountTokens), it completely defeats connection pooling.
This results in a new TCP connection and a full TLS handshake for every single request to Anthropic, which significantly increases latency and makes the proxy easily detectable by the upstream service due to the lack of connection reuse. Additionally, creating a new http.Transport per request can lead to resource exhaustion (file descriptors) under high load.
Recommendation: Reuse http.Client or RoundTripper instances. You should implement a cache (e.g., a sync.Map or a map protected by a mutex) to store and reuse these objects, keyed by the proxyURL and other relevant configuration parameters.
| func computeFingerprint(messageText, version string) string { | ||
| indices := [3]int{4, 7, 20} | ||
| runes := []rune(messageText) | ||
| var sb strings.Builder | ||
| for _, idx := range indices { | ||
| if idx < len(runes) { | ||
| sb.WriteRune(runes[idx]) | ||
| } else { | ||
| sb.WriteRune('0') | ||
| } | ||
| } | ||
| input := fingerprintSalt + sb.String() + version | ||
| h := sha256.Sum256([]byte(input)) | ||
| return hex.EncodeToString(h[:])[:3] | ||
| } |
There was a problem hiding this comment.
In computeFingerprint, converting the entire messageText to a rune slice ([]rune(messageText)) is inefficient if the system prompt is large, as it allocates a new slice and copies all characters. Since you only need characters at specific indices (4, 7, and 20), you can iterate through the string using range and stop once you've collected the required runes.
func computeFingerprint(messageText, version string) string {
indices := [3]int{4, 7, 20}
var sb strings.Builder
count := 0
idxPtr := 0
for _, r := range messageText {
for idxPtr < len(indices) && count == indices[idxPtr] {
sb.WriteRune(r)
idxPtr++
}
count++
if idxPtr >= len(indices) || count > 20 {
break
}
}
for idxPtr < len(indices) {
sb.WriteRune('0')
idxPtr++
}
input := fingerprintSalt + sb.String() + version
h := sha256.Sum256([]byte(input))
return hex.EncodeToString(h[:])[:3]
}There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bb44671845
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| } | ||
|
|
||
| billingText := generateBillingHeader(payload, experimentalCCHSigning, version, messageText, entrypoint, workload) | ||
| billingBlock := fmt.Sprintf(`{"type":"text","text":"%s"}`, billingText) |
There was a problem hiding this comment.
Escape billing text before building raw JSON block
This line interpolates billingText directly into a JSON string, but billingText now includes entrypoint and workload values sourced from request headers; if either contains " or \, the generated raw JSON becomes invalid. Because later sjson.SetRawBytes errors are ignored, the system block injection can silently fail for those requests, causing malformed or missing cloaking metadata. Build this block via JSON marshaling (or escape the text) instead of raw string formatting.
Useful? React with 👍 / 👎.
| var standardTransport http.RoundTripper = &http.Transport{ | ||
| DialContext: (&net.Dialer{ | ||
| Timeout: 30 * time.Second, | ||
| KeepAlive: 30 * time.Second, | ||
| }).DialContext, | ||
| } |
There was a problem hiding this comment.
Preserve proxy inheritance when no explicit proxy is set
When proxyURL is empty, this code still installs a custom http.Transport with no proxy function, which bypasses HTTP_PROXY/HTTPS_PROXY/NO_PROXY inheritance that the previous client path got from the default transport. As a result, Claude traffic that previously worked via environment-configured proxies now goes direct unless every auth/config explicitly sets ProxyURL, which can break egress-restricted deployments.
Useful? React with 👍 / 👎.
Summary
基于 Claude Code v2.1.88 源码(source map 逆向)分析,修复了多个可被 Anthropic 检测的差距:
指纹与签名:
SHA256(salt + msg[4,7,20] + version)[:3],替代随机 buildHashcc_version从设备 profile 动态取版本号cc_entrypoint从客户端 UA 解析(cli/vscode/local-agent)cc_workload支持HTTP 头部:
X-Claude-Code-Session-Id(每 apiKey 缓存 UUID,TTL=1h)x-client-request-id(仅首方 API,每请求 UUID)Anthropic-Dangerous-Direct-Browser-Access仅在 API key 模式发送TLS 指纹:
helps/utls_client.go,仅影响 Claude executor,其他 executor 不受影响安全加固:
相比 #2486 的改进
computeFingerprint使用 rune 索引替代字节索引,多字节字符(中文)指纹不再错误net.JoinHostPort正确拼接端口Test plan
go build ./...编译通过go test ./internal/runtime/executor/...全部通过go test ./internal/runtime/executor/helps/...全部通过