win 8cac4269aa feat: Node.js TLS 指纹代理 + 网络隔离防泄露
- 新增 Node.js TLS Forward Proxy (tools/node-tls-proxy/)
  原生 Node.js TLS 栈发起上游 HTTPS,JA3/JA4 天然匹配 Claude CLI
  SSE 流式透传,支持上游 HTTP CONNECT 代理
  零依赖,Node.js 24.13.0 锁定版本

- Go 集成 (config.go + http_upstream.go)
  新增 NodeTLSProxyConfig 配置
  DoWithTLS 优先走 Node.js 代理模式,URL 重写 https→http://localhost:3456

- Docker 网络隔离 (docker-compose.tls-proxy.yml)
  sub2api 容器仅 internal 网络,物理隔离外网
  node-tls-proxy 唯一出站通道,IPv6 内核级禁用

- iptables 防泄露脚本 (tools/firewall/)
  QUIC/UDP 443 全局 DROP,仅 nodeproxy 用户可出站 TCP 443

- 镜像切换为 zfc931912343/ 仓库
2026-03-25 11:37:26 +08:00

246 lines
8.1 KiB
JavaScript
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.

'use strict';
const http = require('http');
const https = require('https');
const net = require('net');
// ─── 配置 ───────────────────────────────────────────────
const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'api.anthropic.com';
const LISTEN_PORT = parseInt(process.env.PROXY_PORT || '3456', 10);
const LISTEN_HOST = process.env.PROXY_HOST || '127.0.0.1';
// 可选:上游 HTTPS 代理(仅支持 HTTP CONNECT 隧道)
const UPSTREAM_PROXY = process.env.UPSTREAM_PROXY || '';
// 连接超时ms
const CONNECT_TIMEOUT = parseInt(process.env.CONNECT_TIMEOUT || '30000', 10);
// 空闲超时ms— 对 SSE 长连接要足够大
const IDLE_TIMEOUT = parseInt(process.env.IDLE_TIMEOUT || '600000', 10);
// ─── 日志 ───────────────────────────────────────────────
const log = (level, msg, extra = {}) => {
const entry = {
time: new Date().toISOString(),
level,
msg,
...extra,
};
process.stderr.write(JSON.stringify(entry) + '\n');
};
// ─── 健康检查路径 ─────────────────────────────────────────
const HEALTH_PATH = '/__health';
// ─── 通过 HTTP 代理建立 CONNECT 隧道 ──────────────────────
function connectViaProxy(proxyUrl, targetHost, targetPort) {
return new Promise((resolve, reject) => {
const proxy = new URL(proxyUrl);
const proxyPort = parseInt(proxy.port || '80', 10);
const conn = net.connect(proxyPort, proxy.hostname, () => {
const connectReq =
`CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\n` +
`Host: ${targetHost}:${targetPort}\r\n`;
// 代理认证
const auth = proxy.username
? `Proxy-Authorization: Basic ${Buffer.from(
`${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password || '')}`
).toString('base64')}\r\n`
: '';
conn.write(connectReq + auth + '\r\n');
});
conn.once('error', reject);
conn.setTimeout(CONNECT_TIMEOUT, () => {
conn.destroy(new Error('proxy CONNECT timeout'));
});
// 读取代理响应
let buf = '';
const onData = (chunk) => {
buf += chunk.toString();
const idx = buf.indexOf('\r\n\r\n');
if (idx === -1) return; // 头还没完整
conn.removeListener('data', onData);
const statusLine = buf.slice(0, buf.indexOf('\r\n'));
const statusCode = parseInt(statusLine.split(' ')[1], 10);
if (statusCode === 200) {
conn.setTimeout(0); // 清除超时
// 如果头之后有残余数据,先 unshift 回去
const remainder = buf.slice(idx + 4);
if (remainder.length > 0) {
conn.unshift(Buffer.from(remainder));
}
resolve(conn);
} else {
conn.destroy();
reject(new Error(`proxy CONNECT failed: ${statusLine}`));
}
};
conn.on('data', onData);
});
}
// ─── 构建上游请求选项 ──────────────────────────────────────
function buildUpstreamOptions(req) {
// 复制头,重写 host
const headers = { ...req.headers };
headers.host = UPSTREAM_HOST;
// 移除 hop-by-hop 头
delete headers['connection'];
delete headers['keep-alive'];
delete headers['proxy-connection'];
delete headers['transfer-encoding'];
return {
hostname: UPSTREAM_HOST,
port: 443,
path: req.url,
method: req.method,
headers,
// 关键:不设置任何自定义 TLS 选项
// 让 Node.js 使用默认 TLS stack → JA3/JA4 天然匹配
servername: UPSTREAM_HOST, // SNI
timeout: CONNECT_TIMEOUT,
};
}
// ─── 代理请求 ───────────────────────────────────────────
async function proxyRequest(req, res) {
const opts = buildUpstreamOptions(req);
let proxyReq;
if (UPSTREAM_PROXY) {
// 通过代理建立隧道,然后 TLS 握手
try {
const socket = await connectViaProxy(UPSTREAM_PROXY, UPSTREAM_HOST, 443);
opts.socket = socket;
opts.agent = false; // 使用自定义 socket不走连接池
proxyReq = https.request(opts);
} catch (err) {
log('error', 'proxy tunnel failed', { error: err.message });
if (!res.headersSent) {
res.writeHead(502, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'proxy_tunnel_error', message: err.message }));
}
return;
}
} else {
proxyReq = https.request(opts);
}
// 上游响应
proxyReq.on('response', (proxyRes) => {
// 过滤 hop-by-hop 头
const responseHeaders = { ...proxyRes.headers };
delete responseHeaders['connection'];
delete responseHeaders['keep-alive'];
delete responseHeaders['transfer-encoding']; // Node.js 会自动处理
res.writeHead(proxyRes.statusCode, responseHeaders);
// SSE / 流式透传:逐块 pipe
proxyRes.pipe(res, { end: true });
proxyRes.on('error', (err) => {
log('error', 'upstream response error', { error: err.message });
res.end();
});
});
// 上游连接错误
proxyReq.on('error', (err) => {
log('error', 'upstream request error', {
error: err.message,
path: req.url,
method: req.method,
});
if (!res.headersSent) {
res.writeHead(502, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'upstream_error', message: err.message }));
}
});
// 超时
proxyReq.on('timeout', () => {
log('warn', 'upstream request timeout', { path: req.url });
proxyReq.destroy(new Error('upstream timeout'));
});
// 客户端断开时中止上游请求
req.on('close', () => {
if (!proxyReq.destroyed) {
proxyReq.destroy();
}
});
// 将客户端请求体 pipe 到上游
req.pipe(proxyReq, { end: true });
}
// ─── HTTP 服务器 ─────────────────────────────────────────
const server = http.createServer((req, res) => {
// 健康检查
if (req.url === HEALTH_PATH) {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
upstream: UPSTREAM_HOST,
node: process.version,
openssl: process.versions.openssl,
uptime: process.uptime(),
}));
return;
}
proxyRequest(req, res).catch((err) => {
log('error', 'unhandled proxy error', { error: err.message });
if (!res.headersSent) {
res.writeHead(500, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'internal_error' }));
}
});
});
// SSE 长连接:禁用 server 级超时(由上游控制)
server.timeout = 0;
server.keepAliveTimeout = IDLE_TIMEOUT;
server.headersTimeout = 60000;
server.listen(LISTEN_PORT, LISTEN_HOST, () => {
log('info', 'node-tls-proxy started', {
listen: `${LISTEN_HOST}:${LISTEN_PORT}`,
upstream: `${UPSTREAM_HOST}:443`,
proxy: UPSTREAM_PROXY || '(direct)',
node: process.version,
openssl: process.versions.openssl,
});
});
// ─── 优雅关闭 ─────────────────────────────────────────────
let shuttingDown = false;
function shutdown(signal) {
if (shuttingDown) return;
shuttingDown = true;
log('info', `received ${signal}, shutting down`);
server.close(() => {
log('info', 'server closed');
process.exit(0);
});
// 强制退出兜底
setTimeout(() => process.exit(1), 5000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// 未捕获异常不要崩进程
process.on('uncaughtException', (err) => {
log('error', 'uncaught exception', { error: err.message, stack: err.stack });
});
process.on('unhandledRejection', (reason) => {
log('error', 'unhandled rejection', { error: String(reason) });
});