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
71a068c193
commit
5de1618e08
@ -124,7 +124,7 @@ func NewHTTPUpstream(cfg *config.Config) service.HTTPUpstream {
|
|||||||
// - 调用方必须关闭 resp.Body,否则会导致 inFlight 计数泄漏
|
// - 调用方必须关闭 resp.Body,否则会导致 inFlight 计数泄漏
|
||||||
// - inFlight > 0 的客户端不会被淘汰,确保活跃请求不被中断
|
// - inFlight > 0 的客户端不会被淘汰,确保活跃请求不被中断
|
||||||
func (s *httpUpstreamService) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
|
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" {
|
if s.isNodeTLSProxyEnabled() && req != nil && req.URL != nil && req.URL.Scheme == "https" {
|
||||||
return s.doViaNodeTLSProxy(req, accountID, accountConcurrency)
|
return s.doViaNodeTLSProxy(req, accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
@ -181,8 +181,7 @@ func (s *httpUpstreamService) DoWithTLS(req *http.Request, proxyURL string, acco
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 优先使用 Node.js TLS 代理模式
|
// 优先使用 Node.js TLS 代理模式
|
||||||
if s.isNodeTLSProxyEnabled() {
|
if s.isNodeTLSProxyEnabled() && s.shouldRouteViaNodeProxy(req) { return s.doViaNodeTLSProxy(req, accountID, accountConcurrency)
|
||||||
return s.doViaNodeTLSProxy(req, accountID, accountConcurrency)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLS 指纹已启用,记录调试日志
|
// TLS 指纹已启用,记录调试日志
|
||||||
@ -247,10 +246,27 @@ func (s *httpUpstreamService) isNodeTLSProxyEnabled() bool {
|
|||||||
return s.cfg.Gateway.NodeTLSProxy.Enabled
|
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 代理发送请求
|
// doViaNodeTLSProxy 通过 Node.js TLS 代理发送请求
|
||||||
// 将 HTTPS 请求改为 HTTP 明文发送到本地 Node.js 代理,
|
// 将 HTTPS 请求改为 HTTP 明文发送到本地 Node.js 代理,
|
||||||
// 由 Node.js 进程使用原生 TLS 栈完成到上游的 HTTPS 连接。
|
// 由 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) {
|
func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, accountID int64, accountConcurrency int) (*http.Response, error) {
|
||||||
proxyCfg := s.cfg.Gateway.NodeTLSProxy
|
proxyCfg := s.cfg.Gateway.NodeTLSProxy
|
||||||
listenHost := proxyCfg.ListenHost
|
listenHost := proxyCfg.ListenHost
|
||||||
@ -262,6 +278,10 @@ func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, accountID int
|
|||||||
listenPort = 3456
|
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/...
|
// 重写请求 URL:https://api.anthropic.com/v1/... → http://127.0.0.1:3456/v1/...
|
||||||
originalURL := req.URL.String()
|
originalURL := req.URL.String()
|
||||||
req.URL.Scheme = "http"
|
req.URL.Scheme = "http"
|
||||||
@ -270,11 +290,14 @@ func (s *httpUpstreamService) doViaNodeTLSProxy(req *http.Request, accountID int
|
|||||||
slog.Debug("node_tls_proxy_rewrite",
|
slog.Debug("node_tls_proxy_rewrite",
|
||||||
"account_id", accountID,
|
"account_id", accountID,
|
||||||
"original_url", originalURL,
|
"original_url", originalURL,
|
||||||
|
"original_host", originalHost,
|
||||||
"rewritten_to", req.URL.String(),
|
"rewritten_to", req.URL.String(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 递归保护:标记已经过代理重写,避免 Do() 再次进入本方法
|
||||||
|
req.URL.Scheme = "http" // Do() 只拦截 scheme=="https",http 会走正常路径
|
||||||
|
|
||||||
// 通过标准 HTTP 客户端发送(不需要 TLS,代理是本地 HTTP)
|
// 通过标准 HTTP 客户端发送(不需要 TLS,代理是本地 HTTP)
|
||||||
// proxyURL 为空(直连本地),使用标准 Do 路径
|
|
||||||
return s.Do(req, "", accountID, accountConcurrency)
|
return s.Do(req, "", accountID, accountConcurrency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -85,24 +85,28 @@ function connectViaProxy(proxyUrl, targetHost, targetPort) {
|
|||||||
|
|
||||||
// ─── 构建上游请求选项 ──────────────────────────────────────
|
// ─── 构建上游请求选项 ──────────────────────────────────────
|
||||||
function buildUpstreamOptions(req) {
|
function buildUpstreamOptions(req) {
|
||||||
// 复制头,重写 host
|
// 动态确定上游主机:优先使用 X-Forwarded-Host,回退到 UPSTREAM_HOST 配置
|
||||||
|
const targetHost = req.headers['x-forwarded-host'] || UPSTREAM_HOST;
|
||||||
|
|
||||||
|
// 复制头,重写 host 为实际目标
|
||||||
const headers = { ...req.headers };
|
const headers = { ...req.headers };
|
||||||
headers.host = UPSTREAM_HOST;
|
headers.host = targetHost;
|
||||||
// 移除 hop-by-hop 头
|
// 移除内部头和 hop-by-hop 头
|
||||||
|
delete headers['x-forwarded-host'];
|
||||||
delete headers['connection'];
|
delete headers['connection'];
|
||||||
delete headers['keep-alive'];
|
delete headers['keep-alive'];
|
||||||
delete headers['proxy-connection'];
|
delete headers['proxy-connection'];
|
||||||
delete headers['transfer-encoding'];
|
delete headers['transfer-encoding'];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostname: UPSTREAM_HOST,
|
hostname: targetHost,
|
||||||
port: 443,
|
port: 443,
|
||||||
path: req.url,
|
path: req.url,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers,
|
headers,
|
||||||
// 关键:不设置任何自定义 TLS 选项
|
// 关键:不设置任何自定义 TLS 选项
|
||||||
// 让 Node.js 使用默认 TLS stack → JA3/JA4 天然匹配
|
// 让 Node.js 使用默认 TLS stack → JA3/JA4 天然匹配
|
||||||
servername: UPSTREAM_HOST, // SNI
|
servername: targetHost, // SNI
|
||||||
timeout: CONNECT_TIMEOUT,
|
timeout: CONNECT_TIMEOUT,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user