fix: restore node-tls-proxy routing lost during rebase
Some checks failed
CI / test (push) Failing after 1m32s
CI / golangci-lint (push) Failing after 1m32s
Security Scan / backend-security (push) Failing after 32s
Security Scan / frontend-security (push) Failing after 32s

- 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
This commit is contained in:
win 2026-03-31 14:08:24 +08:00
parent d3d885cf75
commit 1eed02c325
3 changed files with 129 additions and 0 deletions

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
// 重写请求 URLhttps://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)
}