From 5c587c109500eafac6ccd26045a694afb3e18342 Mon Sep 17 00:00:00 2001 From: win Date: Sun, 22 Mar 2026 01:09:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Node.js=20TLS=20=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=AF=86=E5=88=AB=E4=B8=8A=E6=B8=B8=E4=B8=BB?= =?UTF-8?q?=E6=9C=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Go: 通过 X-Forwarded-Host 传递原始目标主机给 Node.js 代理 - Node.js: 读取 X-Forwarded-Host 动态连接到正确的上游主机 - 所有 HTTPS 上游请求统一走代理,不再固定绑定 api.anthropic.com - Gemini/Sora 等不同上游自动识别,无需手动配置 --- backend/internal/repository/http_upstream.go | 33 +++++++++++++++++--- tools/node-tls-proxy/proxy.js | 14 ++++++--- 2 files changed, 37 insertions(+), 10 deletions(-) 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, }; }