fix: Node.js TLS 代理动态识别上游主机
- Go: 通过 X-Forwarded-Host 传递原始目标主机给 Node.js 代理 - Node.js: 读取 X-Forwarded-Host 动态连接到正确的上游主机 - 所有 HTTPS 上游请求统一走代理,不再固定绑定 api.anthropic.com - Gemini/Sora 等不同上游自动识别,无需手动配置
This commit is contained in:
parent
2fff535bcd
commit
5c587c1095
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user