feat(tls): 添加 Go uTLS sidecar 以绕过 Cloudflare TLS 指纹检测

- 新增 Go 语言编写的 TLS sidecar 服务,使用 uTLS 库模拟 Chrome 指纹
- 在 Dockerfile 中添加多阶段构建以编译 sidecar 二进制文件
- 扩展配置系统,支持启用/禁用 sidecar 及自定义端口
- 修改 Grok 提供商,使其请求可通过 sidecar 转发
- 在前端界面添加 TLS sidecar 配置选项和国际化支持
- 服务启动时自动启动 sidecar,关闭时优雅停止
This commit is contained in:
hex2077 2026-02-28 00:02:12 +08:00
parent 4f9189e96e
commit 81dd6a3f86
13 changed files with 810 additions and 3 deletions

View file

@ -1,3 +1,16 @@
# ── Stage 1: 编译 Go TLS sidecar ──
FROM golang:1.22-alpine AS sidecar-builder
RUN apk add --no-cache git
WORKDIR /build
COPY tls-sidecar/go.mod tls-sidecar/go.sum* ./
RUN go mod download
COPY tls-sidecar/ ./
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o tls-sidecar .
# ── Stage 2: Node.js 应用 ──
# 使用官方Node.js运行时作为基础镜像
# 选择20-alpine版本以满足undici包的要求需要Node.js >=20.18.1
FROM node:20-alpine
@ -9,6 +22,10 @@ LABEL description="Docker image for AIClient2API server"
# 安装必要的系统工具tar 用于更新功能git 用于版本检查)
RUN apk add --no-cache tar git
# 从 sidecar 构建阶段复制二进制
COPY --from=sidecar-builder /build/tls-sidecar /app/tls-sidecar/tls-sidecar
RUN chmod +x /app/tls-sidecar/tls-sidecar
# 设置工作目录
WORKDIR /app

View file

@ -1 +1 @@
2.9.9.2
2.9.9.31

View file

@ -57,5 +57,7 @@
"LOG_INCLUDE_REQUEST_ID": true,
"LOG_INCLUDE_TIMESTAMP": true,
"LOG_MAX_FILE_SIZE": 10485760,
"LOG_MAX_FILES": 10
"LOG_MAX_FILES": 10,
"TLS_SIDECAR_ENABLED": false,
"TLS_SIDECAR_PORT": 9090
}

View file

@ -90,7 +90,10 @@ export async function initializeConfig(args = process.argv.slice(2), configFileP
LOG_INCLUDE_REQUEST_ID: true,
LOG_INCLUDE_TIMESTAMP: true,
LOG_MAX_FILE_SIZE: 10485760,
LOG_MAX_FILES: 10
LOG_MAX_FILES: 10,
TLS_SIDECAR_ENABLED: false, // 启用 Go uTLS sidecar需要编译 tls-sidecar 二进制)
TLS_SIDECAR_PORT: 9090, // sidecar 监听端口
TLS_SIDECAR_BINARY_PATH: null // 自定义二进制路径(默认自动搜索)
};
logger.info('[Config] Using default configuration.');
}

View file

@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import { API_ACTIONS, isRetryableNetworkError } from '../../utils/common.js';
import { getProviderModels } from '../provider-models.js';
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
import { getTLSSidecar } from '../../utils/tls-sidecar.js';
import { MODEL_PROVIDER } from '../../utils/common.js';
import { GrokConverter } from '../../converters/strategies/GrokConverter.js';
import { ConverterFactory } from '../../converters/ConverterFactory.js';
@ -110,6 +111,23 @@ export class GrokApiService {
this.lastSyncAt = null;
}
/**
* 如果 TLS sidecar 可用 axios 请求改为通过 sidecar 转发
* sidecar 不可用时保持原有 https.Agent TLS 配置
*/
_applySidecar(axiosConfig) {
const sidecar = getTLSSidecar();
if (sidecar.isReady()) {
// 获取上游代理 URL如果有
const proxyUrl = this.config.PROXY_URL &&
this.config.PROXY_ENABLED_PROVIDERS?.includes(MODEL_PROVIDER.GROK_CUSTOM)
? this.config.PROXY_URL : null;
sidecar.wrapAxiosConfig(axiosConfig, proxyUrl);
logger.debug('[Grok] Request routed through TLS sidecar');
}
return axiosConfig;
}
async initialize() {
if (this.isInitialized) return;
logger.info('[Grok] Initializing Grok API Service...');
@ -165,6 +183,7 @@ export class GrokApiService {
};
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const response = await axios(axiosConfig);
@ -542,6 +561,7 @@ export class GrokApiService {
};
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const response = await axios(axiosConfig);
@ -604,6 +624,7 @@ export class GrokApiService {
};
configureAxiosProxy(axiosConfig, this.config, MODEL_PROVIDER.GROK_CUSTOM);
this._applySidecar(axiosConfig);
try {
const response = await axios(axiosConfig);

View file

@ -6,6 +6,7 @@ import { initializeUIManagement } from './ui-manager.js';
import { initializeAPIManagement } from './api-manager.js';
import { createRequestHandler } from '../handlers/request-handler.js';
import { discoverPlugins, getPluginManager } from '../core/plugin-manager.js';
import { getTLSSidecar } from '../utils/tls-sidecar.js';
/**
* @license
@ -178,6 +179,11 @@ function setupWorkerCommunication() {
async function gracefulShutdown() {
logger.info('[Server] Initiating graceful shutdown...');
// 停止 TLS sidecar
try {
await getTLSSidecar().stop();
} catch { /* ignore */ }
if (serverInstance) {
serverInstance.close(() => {
logger.info('[Server] HTTP server closed');
@ -242,6 +248,20 @@ async function startServer() {
// logger.info('[Initialization] Checking for unlinked provider configs...');
// await autoLinkProviderConfigs(CONFIG);
// Start TLS sidecar if enabled
if (CONFIG.TLS_SIDECAR_ENABLED) {
const sidecar = getTLSSidecar();
const started = await sidecar.start({
port: CONFIG.TLS_SIDECAR_PORT,
binaryPath: CONFIG.TLS_SIDECAR_BINARY_PATH || undefined,
});
if (started) {
logger.info('[Initialization] TLS sidecar started successfully');
} else {
logger.warn('[Initialization] TLS sidecar failed to start, falling back to Node.js TLS');
}
}
// Initialize plugin system
logger.info('[Initialization] Discovering and initializing plugins...');
await discoverPlugins();

288
src/utils/tls-sidecar.js Normal file
View file

@ -0,0 +1,288 @@
/**
* TLS Sidecar Manager
*
* 管理 Go uTLS sidecar 进程的生命周期
* - 启动/停止 sidecar 二进制
* - 健康检查 & 自动重启
* - axios 提供 sidecar 代理配置
*/
import { spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import logger from './logger.js';
import http from 'http';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const DEFAULT_PORT = 9090;
const HEALTH_CHECK_INTERVAL = 30000; // 30s
const HEALTH_CHECK_TIMEOUT = 3000; // 3s
const MAX_RESTART_ATTEMPTS = 5;
const RESTART_DELAY = 2000; // 2s
class TLSSidecar {
constructor() {
this.process = null;
this.port = DEFAULT_PORT;
this.baseUrl = null;
this.healthCheckTimer = null;
this.restartCount = 0;
this.isShuttingDown = false;
this.ready = false;
}
/**
* 启动 sidecar 进程
* @param {Object} options
* @param {number} [options.port] - 监听端口
* @param {string} [options.binaryPath] - 自定义二进制路径
* @returns {Promise<boolean>}
*/
async start(options = {}) {
if (this.process) {
logger.info('[TLS-Sidecar] Already running');
return true;
}
this.port = options.port || parseInt(process.env.TLS_SIDECAR_PORT) || DEFAULT_PORT;
this.baseUrl = `http://127.0.0.1:${this.port}`;
// 查找二进制文件
const binaryPath = options.binaryPath || this._findBinary();
if (!binaryPath) {
logger.error('[TLS-Sidecar] Binary not found. Build it with: cd tls-sidecar && go build -o tls-sidecar');
return false;
}
logger.info(`[TLS-Sidecar] Starting: ${binaryPath} on port ${this.port}`);
try {
this.process = spawn(binaryPath, [], {
env: {
...process.env,
TLS_SIDECAR_PORT: String(this.port),
},
stdio: ['ignore', 'pipe', 'pipe'],
});
// 转发 sidecar 日志
this.process.stdout.on('data', (data) => {
const msg = data.toString().trim();
if (msg) logger.info(`[TLS-Sidecar] ${msg}`);
});
this.process.stderr.on('data', (data) => {
const msg = data.toString().trim();
if (msg) logger.error(`[TLS-Sidecar] ${msg}`);
});
this.process.on('exit', (code, signal) => {
logger.warn(`[TLS-Sidecar] Process exited (code=${code}, signal=${signal})`);
this.process = null;
this.ready = false;
if (!this.isShuttingDown && this.restartCount < MAX_RESTART_ATTEMPTS) {
this.restartCount++;
logger.info(`[TLS-Sidecar] Auto-restart attempt ${this.restartCount}/${MAX_RESTART_ATTEMPTS}`);
setTimeout(() => this.start(options), RESTART_DELAY);
}
});
this.process.on('error', (err) => {
logger.error(`[TLS-Sidecar] Spawn error: ${err.message}`);
this.process = null;
this.ready = false;
});
// 等待 sidecar 就绪
const ok = await this._waitForReady();
if (ok) {
this.ready = true;
this.restartCount = 0;
this._startHealthCheck();
logger.info(`[TLS-Sidecar] Ready at ${this.baseUrl}`);
}
return ok;
} catch (err) {
logger.error(`[TLS-Sidecar] Failed to start: ${err.message}`);
return false;
}
}
/**
* 停止 sidecar 进程
*/
async stop() {
this.isShuttingDown = true;
this._stopHealthCheck();
if (this.process) {
logger.info('[TLS-Sidecar] Stopping...');
return new Promise((resolve) => {
const timeout = setTimeout(() => {
if (this.process) {
logger.warn('[TLS-Sidecar] Force killing');
this.process.kill('SIGKILL');
}
resolve();
}, 5000);
this.process.once('exit', () => {
clearTimeout(timeout);
this.process = null;
this.ready = false;
logger.info('[TLS-Sidecar] Stopped');
resolve();
});
this.process.kill('SIGTERM');
});
}
}
/**
* 检查 sidecar 是否正在运行且健康
* @returns {boolean}
*/
isReady() {
return this.ready && this.process !== null;
}
/**
* 获取 sidecar base URL
* @returns {string|null}
*/
getBaseUrl() {
return this.isReady() ? this.baseUrl : null;
}
/**
* axios 配置 sidecar 代理
* 将目标 URL 改为 sidecar 地址原始目标通过 header 传递
*
* @param {Object} axiosConfig - axios 配置对象
* @param {string} [proxyUrl] - 上游代理 URL可选
* @returns {Object} 修改后的 axios 配置
*/
wrapAxiosConfig(axiosConfig, proxyUrl) {
if (!this.isReady()) {
return axiosConfig; // sidecar 不可用,原样返回
}
const targetUrl = axiosConfig.url;
// 将请求指向 sidecar
axiosConfig.url = this.baseUrl;
// 通过 header 传递目标和代理信息
axiosConfig.headers = axiosConfig.headers || {};
axiosConfig.headers['X-Target-Url'] = targetUrl;
if (proxyUrl) {
axiosConfig.headers['X-Proxy-Url'] = proxyUrl;
}
// 走 sidecar 不需要 Node.js 侧的 TLS agent
delete axiosConfig.httpAgent;
delete axiosConfig.httpsAgent;
// 确保 axios 不使用自己的代理
axiosConfig.proxy = false;
return axiosConfig;
}
// ──── 内部方法 ────
_findBinary() {
const projectRoot = path.resolve(__dirname, '..', '..');
const isWin = process.platform === 'win32';
const ext = isWin ? '.exe' : '';
const candidates = [
path.join(projectRoot, 'tls-sidecar', `tls-sidecar${ext}`),
path.join(projectRoot, `tls-sidecar${ext}`),
path.join('/usr', 'local', 'bin', `tls-sidecar${ext}`),
path.join('/app', 'tls-sidecar', `tls-sidecar${ext}`),
path.join('/app', `tls-sidecar${ext}`),
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) {
return p;
}
} catch { /* ignore */ }
}
return null;
}
async _waitForReady(timeoutMs = 10000) {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
const ok = await this._healthCheck();
if (ok) return true;
} catch { /* retry */ }
await sleep(500);
}
logger.error('[TLS-Sidecar] Timed out waiting for sidecar to become ready');
return false;
}
_healthCheck() {
return new Promise((resolve) => {
const req = http.get(`${this.baseUrl}/health`, { timeout: HEALTH_CHECK_TIMEOUT }, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
resolve(res.statusCode === 200);
});
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
_startHealthCheck() {
this._stopHealthCheck();
this.healthCheckTimer = setInterval(async () => {
const ok = await this._healthCheck();
if (!ok && this.ready) {
logger.warn('[TLS-Sidecar] Health check failed');
this.ready = false;
} else if (ok && !this.ready) {
logger.info('[TLS-Sidecar] Recovered');
this.ready = true;
}
}, HEALTH_CHECK_INTERVAL);
}
_stopHealthCheck() {
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = null;
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 单例
let instance = null;
export function getTLSSidecar() {
if (!instance) {
instance = new TLSSidecar();
}
return instance;
}
export default TLSSidecar;

View file

@ -177,6 +177,12 @@ async function loadConfiguration() {
if (logMaxFileSizeEl) logMaxFileSizeEl.value = data.LOG_MAX_FILE_SIZE || 10485760;
if (logMaxFilesEl) logMaxFilesEl.value = data.LOG_MAX_FILES || 10;
// TLS Sidecar 配置
const tlsSidecarEnabledEl = document.getElementById('tlsSidecarEnabled');
const tlsSidecarPortEl = document.getElementById('tlsSidecarPort');
if (tlsSidecarEnabledEl) tlsSidecarEnabledEl.checked = data.TLS_SIDECAR_ENABLED || false;
if (tlsSidecarPortEl) tlsSidecarPortEl.value = data.TLS_SIDECAR_PORT || 9090;
} catch (error) {
console.error('Failed to load configuration:', error);
}
@ -274,6 +280,10 @@ async function saveConfiguration() {
config.LOG_INCLUDE_TIMESTAMP = document.getElementById('logIncludeTimestamp')?.checked !== false;
config.LOG_MAX_FILE_SIZE = parseInt(document.getElementById('logMaxFileSize')?.value || 10485760);
config.LOG_MAX_FILES = parseInt(document.getElementById('logMaxFiles')?.value || 10);
// TLS Sidecar 配置
config.TLS_SIDECAR_ENABLED = document.getElementById('tlsSidecarEnabled')?.checked || false;
config.TLS_SIDECAR_PORT = parseInt(document.getElementById('tlsSidecarPort')?.value || 9090);
try {
await window.apiClient.post('/config', config);

View file

@ -319,6 +319,9 @@ const translations = {
'config.proxy.urlNote': '支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理',
'config.proxy.enabledProviders': '启用代理的提供商',
'config.proxy.enabledProvidersNote': '选择需要通过代理访问的提供商,未选中的提供商将直接连接',
'config.proxy.tlsSidecarEnabled': 'TLS 指纹伪装 (uTLS Sidecar)',
'config.proxy.tlsSidecarPort': 'Sidecar 端口',
'config.proxy.tlsSidecarNote': '启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare需重启服务',
'config.log.title': '日志设置',
'config.log.enabled': '启用日志',
'config.log.outputMode': '日志输出模式',
@ -1127,6 +1130,9 @@ const translations = {
'config.proxy.urlNote': 'Supports HTTP, HTTPS and SOCKS5 proxies. Leave empty to disable proxy',
'config.proxy.enabledProviders': 'Providers Using Proxy',
'config.proxy.enabledProvidersNote': 'Select providers that should use the proxy. Unselected providers will connect directly',
'config.proxy.tlsSidecarEnabled': 'TLS Fingerprint Spoofing (uTLS Sidecar)',
'config.proxy.tlsSidecarPort': 'Sidecar Port',
'config.proxy.tlsSidecarNote': 'When enabled, Grok requests are routed through Go uTLS sidecar for perfect Chrome TLS/H2 fingerprint to bypass Cloudflare (requires restart)',
'config.log.title': 'Log Settings',
'config.log.enabled': 'Enable Logging',
'config.log.outputMode': 'Log Output Mode',

View file

@ -128,6 +128,21 @@
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
</div>
<hr>
<div class="config-row">
<div class="form-group">
<label data-i18n="config.proxy.tlsSidecarEnabled">TLS 指纹伪装 (uTLS Sidecar)</label>
<label class="toggle-switch">
<input type="checkbox" id="tlsSidecarEnabled">
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<label for="tlsSidecarPort" data-i18n="config.proxy.tlsSidecarPort">Sidecar 端口</label>
<input type="number" id="tlsSidecarPort" class="form-control" min="1024" max="65535" value="9090">
</div>
</div>
<small class="form-text" data-i18n="config.proxy.tlsSidecarNote">启用后 Grok 请求将通过 Go uTLS sidecar 转发,完美模拟 Chrome TLS/H2 指纹绕过 Cloudflare需重启服务</small>
</div>
<!-- 服务治理与高可用 -->

2
tls-sidecar/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
tls-sidecar
tls-sidecar.exe

17
tls-sidecar/go.mod Normal file
View file

@ -0,0 +1,17 @@
module tls-sidecar
go 1.22
require (
github.com/refraction-networking/utls v1.6.7
golang.org/x/net v0.33.0
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
)

406
tls-sidecar/main.go Normal file
View file

@ -0,0 +1,406 @@
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)
}
}