diff --git a/backend/internal/repository/http_upstream.go b/backend/internal/repository/http_upstream.go index 5b882f92..e7127788 100644 --- a/backend/internal/repository/http_upstream.go +++ b/backend/internal/repository/http_upstream.go @@ -124,7 +124,7 @@ 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 代理模式(全局生效,不依赖账号级 TLS 指纹开关) + // 优先使用 Node.js TLS 代理模式:拦截所有 HTTPS 上游请求 if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil && req.URL.Scheme == "https" { return s.doViaNodeTLSProxy(req, accountID, accountConcurrency) } @@ -181,8 +181,7 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco } // 优先使用 Node.js TLS 代理模式 - if s.isNodeTLSProxyEnabled() { - return s.doViaNodeTLSProxy(req, accountID, accountConcurrency) + if s.isNodeTLSProxyEnabled() && s.shouldRouteViaNodeProxy(req) { return s.doViaNodeTLSProxy(req, accountID, accountConcurrency) } // TLS 指纹已启用,记录调试日志 @@ -247,10 +246,27 @@ func (s *httpUpstreamService) isNodeTLSProxyEnabled() bool { return s.cfg.Gateway.NodeTLSProxy.Enabled } +// shouldRouteViaNodeProxy 判断请求是否应该走 Node.js TLS 代理 +// 仅拦截发往配置的上游主机(默认 api.anthropic.com)的 HTTPS 请求, +// 其他请求(如 Gemini、Sora)走原有路径。 +func (s *httpUpstreamService) shouldRouteViaNodeProxy(req *http.Request) bool { + if req == nil || req.URL == nil || req.URL.Scheme != "https" { + return false + } + upstreamHost := s.cfg.Gateway.NodeTLSProxy.UpstreamHost + if upstreamHost == "" { + upstreamHost = "api.anthropic.com" + } + // 比较请求的目标主机(去掉端口) + reqHost := req.URL.Hostname() + return reqHost == upstreamHost +} + // doViaNodeTLSProxy 通过 Node.js TLS 代理发送请求 // 将 HTTPS 请求改为 HTTP 明文发送到本地 Node.js 代理, // 由 Node.js 进程使用原生 TLS 栈完成到上游的 HTTPS 连接。 -// 这样 JA3/JA4 指纹天然匹配 Node.js (Claude CLI)。 +// 原始目标主机通过 X-Forwarded-Host 传递给 Node.js 代理, +// 代理据此动态连接到正确的上游主机。 func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, accountID int64, accountConcurrency int) (*http.Response, error) { proxyCfg := s.cfg.Gateway.NodeTLSProxy listenHost := proxyCfg.ListenHost @@ -262,6 +278,10 @@ func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, accountID int listenPort = 3456 } + // 保存原始目标主机,通过自定义头传给 Node.js 代理 + originalHost := req.URL.Host + req.Header.Set("X-Forwarded-Host", originalHost) + // 重写请求 URL:https://api.anthropic.com/v1/... → http://127.0.0.1:3456/v1/... originalURL := req.URL.String() req.URL.Scheme = "http" @@ -270,11 +290,14 @@ func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, accountID int slog.Debug("node_tls_proxy_rewrite", "account_id", accountID, "original_url", originalURL, + "original_host", originalHost, "rewritten_to", req.URL.String(), ) + // 递归保护:标记已经过代理重写,避免 Do() 再次进入本方法 + req.URL.Scheme = "http" // Do() 只拦截 scheme=="https",http 会走正常路径 + // 通过标准 HTTP 客户端发送(不需要 TLS,代理是本地 HTTP) - // proxyURL 为空(直连本地),使用标准 Do 路径 return s.Do(req, "", accountID, accountConcurrency) } diff --git a/tools/node-tls-proxy/proxy.js b/tools/node-tls-proxy/proxy.js index 19d8befe..341a9e8d 100644 --- a/tools/node-tls-proxy/proxy.js +++ b/tools/node-tls-proxy/proxy.js @@ -85,24 +85,28 @@ function connectViaProxy(proxyUrl, targetHost, targetPort) { // ─── 构建上游请求选项 ────────────────────────────────────── function buildUpstreamOptions(req) { - // 复制头,重写 host + // 动态确定上游主机:优先使用 X-Forwarded-Host,回退到 UPSTREAM_HOST 配置 + const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST; + + // 复制头,重写 host 为实际目标 const headers = { ...req.headers }; - headers.host = UPSTREAM_HOST; - // 移除 hop-by-hop 头 + headers.host = targetHost; + // 移除内部头和 hop-by-hop 头 + delete headers['x-forwarded-host']; delete headers['connection']; delete headers['keep-alive']; delete headers['proxy-connection']; delete headers['transfer-encoding']; return { - hostname: UPSTREAM_HOST, + hostname: targetHost, port: 443, path: req.url, method: req.method, headers, // 关键:不设置任何自定义 TLS 选项 // 让 Node.js 使用默认 TLS stack → JA3/JA4 天然匹配 - servername: UPSTREAM_HOST, // SNI + servername: targetHost, // SNI timeout: CONNECT_TIMEOUT, }; }