Skip to content

fix: 增强 Claude 反代检测对抗能力#2489

Merged
luispater merged 3 commits intorouter-for-me:devfrom
Soein:upstream-pr
Apr 2, 2026
Merged

fix: 增强 Claude 反代检测对抗能力#2489
luispater merged 3 commits intorouter-for-me:devfrom
Soein:upstream-pr

Conversation

@Soein
Copy link
Copy Markdown
Contributor

@Soein Soein commented Apr 2, 2026

Summary

基于 Claude Code v2.1.88 源码(source map 逆向)分析,修复了多个可被 Anthropic 检测的差距:

指纹与签名:

  • 实现消息指纹算法 SHA256(salt + msg[4,7,20] + version)[:3],替代随机 buildHash
  • billing header cc_version 从设备 profile 动态取版本号
  • billing header cc_entrypoint 从客户端 UA 解析(cli/vscode/local-agent)
  • billing header 新增 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 模式发送
  • 补全 4 个缺失 beta flags(structured-outputs/fast-mode/redact-thinking/token-efficient-tools)

TLS 指纹:

  • Claude API 请求使用 utls Chrome TLS 指纹,替代 Go 标准库 TLS
  • 仅对 Anthropic 官方域名(api.anthropic.com)启用 utls,自定义 base_url 走标准 transport
  • 新增 helps/utls_client.go,仅影响 Claude executor,其他 executor 不受影响

安全加固:

  • OAuth scope 对齐 Claude Code 2.1.88(5 个 scope)
  • 响应头网关指纹清洗(剥离 litellm/helicone/portkey/cloudflare/kong/braintrust 前缀头)

相比 #2486 的改进

  • 移除 fork 专用的 CI workflow(sync-upstream / build-and-push),不应提交上游
  • 修复 computeFingerprint 使用 rune 索引替代字节索引,多字节字符(中文)指纹不再错误
  • 修复 utls 仅对 Anthropic 官方域名生效,自定义 base_url 的 HTTP/1.1 反代不再失败
  • 修复 IPv6 地址使用 net.JoinHostPort 正确拼接端口

Test plan

  • go build ./... 编译通过
  • go test ./internal/runtime/executor/... 全部通过
  • go test ./internal/runtime/executor/helps/... 全部通过

Soein added 3 commits April 2, 2026 15:55
基于 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 正确拼接端口
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +155 to +188
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment on lines +1201 to +1215
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]
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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]
}

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +166 to +171
var standardTransport http.RoundTripper = &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@luispater luispater changed the base branch from main to dev April 2, 2026 12:55
@luispater luispater merged commit e3eb048 into router-for-me:dev Apr 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants