From 1eed02c325f2a6ae58f7fd4d3adbfa538c8acbd6 Mon Sep 17 00:00:00 2001 From: win Date: Tue, 31 Mar 2026 14:08:24 +0800 Subject: [PATCH] fix: restore node-tls-proxy routing lost during rebase - Re-add NodeTLSProxyConfig struct to GatewayConfig (removed by upstream) - Re-create http_upstream_antigravity.go with proxy routing functions - Add proxy intercept hook in Do() for api.anthropic.com requests --- backend/internal/config/config.go | 19 ++++ backend/internal/repository/http_upstream.go | 10 ++ .../repository/http_upstream_antigravity.go | 100 ++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100644 backend/internal/repository/http_upstream_antigravity.go diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 0fff8cc4..8da26d70 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -381,6 +381,8 @@ type GatewayConfig struct { OpenAIWS GatewayOpenAIWSConfig `mapstructure:"openai_ws"` // AntigravityLSWorker: LS worker 容器控制平面配置 AntigravityLSWorker GatewayAntigravityLSWorkerConfig `mapstructure:"antigravity_ls_worker"` + // NodeTLSProxy: Node.js TLS 代理配置 + NodeTLSProxy NodeTLSProxyConfig `mapstructure:"node_tls_proxy"` // HTTP 上游连接池配置(性能优化:支持高并发场景调优) // MaxIdleConns: 所有主机的最大空闲连接总数 @@ -669,6 +671,23 @@ type SoraModelFiltersConfig struct { HidePromptEnhance bool `mapstructure:"hide_prompt_enhance"` } +// NodeTLSProxyConfig Node.js TLS 代理配置 +// 通过本地 Node.js 进程转发 HTTPS 请求,利用原生 TLS 栈产生真实 JA3 指纹 +type NodeTLSProxyConfig struct { + // Enabled: 全局开关 + Enabled bool `mapstructure:"enabled"` + // ListenPort: Node.js 代理监听端口 + ListenPort int `mapstructure:"listen_port"` + // ListenHost: Node.js 代理监听地址(Docker 内用服务名,裸机用 127.0.0.1) + ListenHost string `mapstructure:"listen_host"` + // HealthPath: 健康检查路径 + HealthPath string `mapstructure:"health_path"` + // UpstreamHost: 默认上游主机 + UpstreamHost string `mapstructure:"upstream_host"` + // ProxyHosts: 允许走代理的主机白名单,为空时仅代理 api.anthropic.com + ProxyHosts []string `mapstructure:"proxy_hosts"` +} + // TLSFingerprintConfig TLS指纹伪装配置 // 用于模拟 Claude CLI (Node.js) 的 TLS 握手特征,避免被识别为非官方客户端 type TLSFingerprintConfig struct { diff --git a/backend/internal/repository/http_upstream.go b/backend/internal/repository/http_upstream.go index 20e0c421..497cf56c 100644 --- a/backend/internal/repository/http_upstream.go +++ b/backend/internal/repository/http_upstream.go @@ -148,6 +148,16 @@ func NewHTTPUpstream(cfg *config.Config) service.HTTPUpstream { // - 调用方必须关闭 resp.Body,否则会导致 inFlight 计数泄漏 // - inFlight > 0 的客户端不会被淘汰,确保活跃请求不被中断 func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) { + // Node.js TLS 代理:仅 Anthropic API + // Antigravity (googleapis) 使用 Go 原生 TLS(更接近真实 BoringCrypto 指纹) + // proxyURL 通过 X-Upstream-Proxy header 传递给 node-tls-proxy 动态选择出口 + if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil && req.URL.Scheme == "https" { + host := req.URL.Hostname() + if host == "api.anthropic.com" { + return s.doViaNodeTLSProxy(req, proxyURL, accountID, accountConcurrency) + } + } + if err := s.validateRequestHost(req); err != nil { return nil, err } diff --git a/backend/internal/repository/http_upstream_antigravity.go b/backend/internal/repository/http_upstream_antigravity.go new file mode 100644 index 00000000..55c28cdb --- /dev/null +++ b/backend/internal/repository/http_upstream_antigravity.go @@ -0,0 +1,100 @@ +package repository + +// ============================================================== +// antigravity — Node.js TLS 代理扩展 +// +// 此文件包含 Antigravity fork 新增的 Node.js TLS 代理功能, +// 与 upstream 代码完全隔离,便于 upstream 更新时的合并维护。 +// +// 上游文件 http_upstream.go 中的钩子调用点: +// Do() — 直接路由到 doViaNodeTLSProxy +// DoWithTLS() — profile==nil 时回退到 Do(),触发同样的路由 +// ============================================================== + +import ( + "fmt" + "log/slog" + "net/http" +) + +// isNodeTLSProxyEnabled 检查 Node.js TLS 代理是否启用 +func (s *httpUpstreamService) isNodeTLSProxyEnabled() bool { + if s.cfg == nil { + return false + } + return s.cfg.Gateway.NodeTLSProxy.Enabled +} + +// shouldRouteViaNodeProxy 判断请求是否应该走 Node.js TLS 代理 +// 仅拦截目标主机在 proxy_hosts 白名单中的 HTTPS 请求, +// 白名单为空时默认只代理 api.anthropic.com。 +func (s *httpUpstreamService) shouldRouteViaNodeProxy(req *http.Request) bool { + if req == nil || req.URL == nil || req.URL.Scheme != "https" { + return false + } + reqHost := req.URL.Hostname() + if reqHost == "" { + return false + } + + hosts := s.cfg.Gateway.NodeTLSProxy.ProxyHosts + if len(hosts) == 0 { + // 默认只代理 Anthropic + return reqHost == "api.anthropic.com" + } + for _, h := range hosts { + if reqHost == h { + return true + } + } + return false +} + +// doViaNodeTLSProxy 通过 Node.js TLS 代理发送请求 +// 将 HTTPS 请求改为 HTTP 明文发送到本地 Node.js 代理, +// 由 Node.js 进程使用原生 TLS 栈完成到上游的 HTTPS 连接。 +// 原始目标主机通过 X-Forwarded-Host 传递给 Node.js 代理, +// 代理据此动态连接到正确的上游主机。 +func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) { + proxyCfg := s.cfg.Gateway.NodeTLSProxy + listenHost := proxyCfg.ListenHost + if listenHost == "" { + listenHost = "127.0.0.1" + } + listenPort := proxyCfg.ListenPort + if listenPort == 0 { + listenPort = 3456 + } + + // 克隆请求,避免修改原始 req(重试时需要原始 URL) + proxyReq := req.Clone(req.Context()) + // 安全复制 Body:优先用 GetBody 工厂方法 + if req.GetBody != nil { + proxyReq.Body, _ = req.GetBody() + } else { + proxyReq.Body = req.Body + } + + // 保存原始目标主机,通过自定义头传给 Node.js 代理 + originalHost := req.URL.Host + proxyReq.Header.Set("X-Forwarded-Host", originalHost) + + // 如果账号绑定了代理(落地机 GOST),通过 header 传递给 node-tls-proxy + // node-tls-proxy 会用此代理作为上游出口,实现动态路由 + if proxyURL != "" { + proxyReq.Header.Set("X-Upstream-Proxy", proxyURL) + } + + // 重写请求 URL:https://api.anthropic.com/v1/... → http://127.0.0.1:3456/v1/... + proxyReq.URL.Scheme = "http" + proxyReq.URL.Host = fmt.Sprintf("%s:%d", listenHost, listenPort) + + slog.Debug("node_tls_proxy_rewrite", + "account_id", accountID, + "original_host", originalHost, + "rewritten_to", proxyReq.URL.Host, + ) + + // 通过标准 HTTP 客户端发送(不需要 TLS,代理是本地 HTTP) + return s.Do(proxyReq, "", accountID, accountConcurrency) +}