AIClient-2-API/tls-sidecar/main.go
hex2077 81dd6a3f86 feat(tls): 添加 Go uTLS sidecar 以绕过 Cloudflare TLS 指纹检测
- 新增 Go 语言编写的 TLS sidecar 服务,使用 uTLS 库模拟 Chrome 指纹
- 在 Dockerfile 中添加多阶段构建以编译 sidecar 二进制文件
- 扩展配置系统,支持启用/禁用 sidecar 及自定义端口
- 修改 Grok 提供商,使其请求可通过 sidecar 转发
- 在前端界面添加 TLS sidecar 配置选项和国际化支持
- 服务启动时自动启动 sidecar,关闭时优雅停止
2026-02-28 00:02:12 +08:00

406 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
"golang.org/x/net/proxy"
)
// ──────────────────────────────────────────────
// TLS Sidecar — Go uTLS reverse proxy
//
// Node.js 发请求到 http://127.0.0.1:<port>
// 通过以下自定义 Header 传递目标信息:
// X-Target-Url: 实际目标 URL必填
// X-Proxy-Url: 上游代理(可选,支持 http/socks5
//
// 所有其他 Header 原样转发给目标服务器。
// 响应(包括 SSE 流式)透传回 Node.js。
//
// uTLS 使用 Chrome 最新指纹ALPN 协商 h2/http1.1
// 根据服务器返回的 ALPN 自动选择 HTTP/2 或 HTTP/1.1 传输。
// ──────────────────────────────────────────────
const (
defaultPort = 9090
headerTarget = "X-Target-Url"
headerProxy = "X-Proxy-Url"
readTimeout = 30 * time.Second
writeTimeout = 0 // SSE 流式响应不设写超时(仅监听 localhost安全
idleTimeout = 120 * time.Second
)
// 全局 RoundTripper 缓存(按 proxyURL 分组,复用 H2 连接)
var (
rtCacheMu sync.Mutex
rtCache = make(map[string]*utlsRoundTripper)
)
func getOrCreateRT(proxyURL string) *utlsRoundTripper {
rtCacheMu.Lock()
defer rtCacheMu.Unlock()
if rt, ok := rtCache[proxyURL]; ok {
return rt
}
rt := newUTLSRoundTripper(proxyURL)
rtCache[proxyURL] = rt
return rt
}
// ──────────────── uTLS RoundTripper ────────────────
// 根据 ALPN 协商结果自动选择 H2 或 H1 传输
type utlsRoundTripper struct {
proxyURL string
mu sync.Mutex
h2Conns map[string]*http2.ClientConn // H2 连接缓存 (per host)
}
func newUTLSRoundTripper(proxyURL string) *utlsRoundTripper {
return &utlsRoundTripper{
proxyURL: proxyURL,
h2Conns: make(map[string]*http2.ClientConn),
}
}
func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
addr := req.URL.Host
if !strings.Contains(addr, ":") {
if req.URL.Scheme == "https" {
addr += ":443"
} else {
addr += ":80"
}
}
// 尝试复用已有的 H2 连接
rt.mu.Lock()
if cc, ok := rt.h2Conns[addr]; ok {
rt.mu.Unlock()
if cc.CanTakeNewRequest() {
resp, err := cc.RoundTrip(req)
if err == nil {
return resp, nil
}
// H2 连接已失效,清除缓存重建
log.Printf("[TLS-Sidecar] Cached H2 conn failed for %s: %v, reconnecting", addr, err)
}
rt.mu.Lock()
delete(rt.h2Conns, addr)
rt.mu.Unlock()
} else {
rt.mu.Unlock()
}
// 建立新的 uTLS 连接
conn, err := dialUTLS(req.Context(), "tcp", addr, rt.proxyURL)
if err != nil {
return nil, err
}
// 根据 ALPN 协商结果决定走 H2 还是 H1
alpn := conn.ConnectionState().NegotiatedProtocol
log.Printf("[TLS-Sidecar] Connected to %s, ALPN: %q", addr, alpn)
if alpn == "h2" {
// HTTP/2: 创建 H2 ClientConn
cc, err := (&http2.Transport{}).NewClientConn(conn)
if err != nil {
conn.Close()
return nil, fmt.Errorf("h2 client conn: %w", err)
}
rt.mu.Lock()
rt.h2Conns[addr] = cc
rt.mu.Unlock()
return cc.RoundTrip(req)
}
// HTTP/1.1: 通过一次性 Transport 使用已建立的 TLS 连接
// DialTLSContext 返回已完成 TLS 握手的 connhttp.Transport 不会重复握手
used := false
t1 := &http.Transport{
DialTLSContext: func(ctx context.Context, network, a string) (net.Conn, error) {
if !used {
used = true
return conn, nil
}
// 后续连接走正常 uTLS dial
return dialUTLS(ctx, network, a, rt.proxyURL)
},
MaxIdleConnsPerHost: 1,
IdleConnTimeout: 90 * time.Second,
}
resp, err := t1.RoundTrip(req)
if err != nil {
conn.Close()
t1.CloseIdleConnections()
}
return resp, err
}
func (rt *utlsRoundTripper) CloseIdleConnections() {
rt.mu.Lock()
defer rt.mu.Unlock()
for k, cc := range rt.h2Conns {
cc.Close()
delete(rt.h2Conns, k)
}
}
// ──────────────── Main ────────────────
func main() {
port := defaultPort
if p := os.Getenv("TLS_SIDECAR_PORT"); p != "" {
if v, err := strconv.Atoi(p); err == nil {
port = v
}
}
mux := http.NewServeMux()
mux.HandleFunc("/health", handleHealth)
mux.HandleFunc("/", handleProxy)
srv := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", port),
Handler: mux,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("[TLS-Sidecar] Shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
}()
log.Printf("[TLS-Sidecar] Listening on 127.0.0.1:%d (Chrome uTLS, H2+H1 auto)\n", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("[TLS-Sidecar] Fatal: %v", err)
}
}
// ──────────────── Health ────────────────
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
fmt.Fprintf(w, `{"status":"ok","tls":"utls-chrome-auto","protocols":"h2,http/1.1"}`)
}
// ──────────────── Proxy Handler ────────────────
func handleProxy(w http.ResponseWriter, r *http.Request) {
targetURL := r.Header.Get(headerTarget)
if targetURL == "" {
http.Error(w, `{"error":"missing X-Target-Url header"}`, http.StatusBadRequest)
return
}
proxyURL := r.Header.Get(headerProxy)
// Parse target
parsed, err := url.Parse(targetURL)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error":"invalid target url: %s"}`, err), http.StatusBadRequest)
return
}
// Build outgoing request
outReq, err := http.NewRequestWithContext(r.Context(), r.Method, targetURL, r.Body)
if err != nil {
http.Error(w, fmt.Sprintf(`{"error":"failed to create request: %s"}`, err), http.StatusInternalServerError)
return
}
// Copy headers (skip internal + hop-by-hop)
for key, vals := range r.Header {
lk := strings.ToLower(key)
if lk == strings.ToLower(headerTarget) || lk == strings.ToLower(headerProxy) {
continue
}
if lk == "connection" || lk == "keep-alive" || lk == "transfer-encoding" ||
lk == "te" || lk == "trailer" || lk == "upgrade" || lk == "host" {
continue
}
for _, v := range vals {
outReq.Header.Add(key, v)
}
}
outReq.Host = parsed.Host
// Execute via uTLS RoundTripper
rt := getOrCreateRT(proxyURL)
resp, err := rt.RoundTrip(outReq)
if err != nil {
log.Printf("[TLS-Sidecar] RoundTrip error → %s: %v", parsed.Host, err)
http.Error(w, fmt.Sprintf(`{"error":"upstream request failed: %s"}`, err), http.StatusBadGateway)
return
}
defer resp.Body.Close()
// Copy response headers
for key, vals := range resp.Header {
for _, v := range vals {
w.Header().Add(key, v)
}
}
w.WriteHeader(resp.StatusCode)
// Stream body (SSE-friendly: flush after every read)
flusher, canFlush := w.(http.Flusher)
buf := make([]byte, 32*1024)
for {
n, readErr := resp.Body.Read(buf)
if n > 0 {
if _, writeErr := w.Write(buf[:n]); writeErr != nil {
log.Printf("[TLS-Sidecar] Write error: %v", writeErr)
return
}
if canFlush {
flusher.Flush()
}
}
if readErr != nil {
if readErr != io.EOF {
log.Printf("[TLS-Sidecar] Read error: %v", readErr)
}
return
}
}
}
// ──────────────── uTLS Dial ────────────────
func dialUTLS(ctx context.Context, network, addr string, proxyURL string) (*utls.UConn, error) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}
// TCP 连接(可能经过代理)
var rawConn net.Conn
if proxyURL != "" {
rawConn, err = dialViaProxy(ctx, network, addr, proxyURL)
} else {
var d net.Dialer
rawConn, err = d.DialContext(ctx, network, addr)
}
if err != nil {
return nil, fmt.Errorf("tcp dial failed: %w", err)
}
// uTLS 握手 — Chrome 最新指纹 (HelloChrome_Auto 跟随 utls 库更新)
tlsConn := utls.UClient(rawConn, &utls.Config{
ServerName: host,
NextProtos: []string{"h2", "http/1.1"},
}, utls.HelloChrome_Auto)
// 握手超时
if deadline, ok := ctx.Deadline(); ok {
tlsConn.SetDeadline(deadline)
} else {
tlsConn.SetDeadline(time.Now().Add(15 * time.Second))
}
if err := tlsConn.Handshake(); err != nil {
rawConn.Close()
return nil, fmt.Errorf("utls handshake failed: %w", err)
}
// 握手完成,清除超时
tlsConn.SetDeadline(time.Time{})
return tlsConn, nil
}
// ──────────────── Proxy Dialer ────────────────
func dialViaProxy(ctx context.Context, network, addr string, proxyURL string) (net.Conn, error) {
parsed, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("invalid proxy url: %w", err)
}
switch strings.ToLower(parsed.Scheme) {
case "socks5", "socks5h", "socks4", "socks":
var auth *proxy.Auth
if parsed.User != nil {
auth = &proxy.Auth{
User: parsed.User.Username(),
}
auth.Password, _ = parsed.User.Password()
}
dialer, err := proxy.SOCKS5("tcp", parsed.Host, auth, &net.Dialer{
Timeout: 15 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("socks5 dialer: %w", err)
}
if ctxDialer, ok := dialer.(proxy.ContextDialer); ok {
return ctxDialer.DialContext(ctx, network, addr)
}
return dialer.Dial(network, addr)
case "http", "https":
proxyConn, err := net.DialTimeout("tcp", parsed.Host, 15*time.Second)
if err != nil {
return nil, fmt.Errorf("connect to http proxy: %w", err)
}
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", addr, addr)
if parsed.User != nil {
username := parsed.User.Username()
password, _ := parsed.User.Password()
cred := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", cred)
}
connectReq += "\r\n"
if _, err = proxyConn.Write([]byte(connectReq)); err != nil {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT write: %w", err)
}
buf := make([]byte, 4096)
n, err := proxyConn.Read(buf)
if err != nil {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT read: %w", err)
}
if !strings.Contains(string(buf[:n]), "200") {
proxyConn.Close()
return nil, fmt.Errorf("proxy CONNECT rejected: %s", strings.TrimSpace(string(buf[:n])))
}
return proxyConn, nil
default:
return nil, fmt.Errorf("unsupported proxy scheme: %s", parsed.Scheme)
}
}