diff --git a/tools/node-tls-proxy/proxy.js b/tools/node-tls-proxy/proxy.js index 2e990d23..cb5c6c3f 100644 --- a/tools/node-tls-proxy/proxy.js +++ b/tools/node-tls-proxy/proxy.js @@ -5,6 +5,7 @@ const https = require('https'); const http2 = require('http2'); const net = require('net'); const crypto = require('crypto'); +// os 模块不引用 — 避免暴露真实主机信息 // ─── 配置 ─────────────────────────────────────────────── const UPSTREAM_HOST = process.env.UPSTREAM_HOST || 'api.anthropic.com'; @@ -17,6 +18,8 @@ const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED !== 'false'; // 默认 const DD_API_KEY = process.env.DD_API_KEY || 'pubbbf48e6d78dae54bceaa4acf463299bf'; const CLI_VERSION = process.env.CLI_VERSION || '2.1.81'; const BUILD_TIME = process.env.BUILD_TIME || '2026-03-20T21:26:18Z'; +// 伪装的 Node 版本(CLI 2.1.81 打包的 Node 版本) +const FAKE_NODE_VERSION = process.env.FAKE_NODE_VERSION || 'v22.14.0'; const log = (level, msg, extra = {}) => { const entry = { time: new Date().toISOString(), level, msg, ...extra }; @@ -27,6 +30,121 @@ const HEALTH_PATH = '/__health'; const h2Hosts = new Set(); const h2Sessions = new Map(); +// ─── 虚拟主机身份生成 ───────────────────────────────────── +// 每个账号基于 seed 生成全局唯一的主机身份,看起来像一台真实的个人开发机 +// 匹配 CLI 的 OTEL detectResources: hostDetector + processDetector + serviceInstanceIdDetector +// +// 设计原则: +// 1. 同一账号(seed)永远产出同一台"机器"的特征 +// 2. 不同账号的特征互不相同(无共享池、无碰撞) +// 3. 每个字段都像人手动设置的,不是程序生成的 + +// hostname 构造词表 — 组合后空间 > 100万,基本不碰撞 +const HN_PREFIX = ['dev','code','work','build','my','home','lab','eng','hack','prog','desk','box','main','personal','linux']; +const HN_MIDDLE = ['','station','machine','server','node','pc','setup','rig','env','hub']; +const HN_STYLE = ['dash','dot','bare']; // 连接风格 + +// 用户名词表 — 真实开发者常用,组合后也是高基数 +const UN_FIRST = ['alex','sam','chris','jordan','max','lee','kai','pat','jamie','taylor','morgan','casey','drew','avery','riley','blake','quinn','reese','cameron','skyler','dev','coder','user','admin','ubuntu','runner']; +const UN_SUFFIX = ['','dev','eng','42','_dev','01','x','z','_','99','007']; + +function generateHostIdentity(seed) { + // 确定性哈希工具:同一 seed+suffix 永远返回同一结果 + const h = (suffix) => crypto.createHash('sha256').update(seed + ':' + suffix).digest(); + + // ── hostname: 组合生成,如 "alex-devstation", "work-box-7f3a" ── + const hb = h('hostname'); + const prefix = HN_PREFIX[hb.readUInt8(0) % HN_PREFIX.length]; + const middle = HN_MIDDLE[hb.readUInt8(1) % HN_MIDDLE.length]; + const style = HN_STYLE[hb.readUInt8(2) % HN_STYLE.length]; + const tail = hb.slice(3, 5).toString('hex'); // 4 hex chars 保证唯一 + let hostname; + if (middle) { + const sep = style === 'dot' ? '.' : style === 'dash' ? '-' : ''; + hostname = `${prefix}${sep}${middle}`; + } else { + // 无中间词时必须加 hex 尾缀,避免 hostname 太短(如裸 "my"、"dev") + hostname = `${prefix}-${tail}`; + } + // 有中间词时 50% 概率加 hex 尾缀(真实场景很多人用 hostname 如 "dev-box-a3f2") + if (middle && hb.readUInt8(5) % 2 === 0) hostname += `-${tail}`; + + // ── username: 组合生成,如 "alexdev", "sam42", "chris_dev" ── + const ub = h('username'); + const first = UN_FIRST[ub.readUInt8(0) % UN_FIRST.length]; + const suffix = UN_SUFFIX[ub.readUInt8(1) % UN_SUFFIX.length]; + const username = `${first}${suffix}`; + + // ── terminal & shell: 按权重分布(xterm-256color 占大多数) ── + const termRoll = h('terminal').readUInt8(0) % 100; + const terminal = termRoll < 60 ? 'xterm-256color' : + termRoll < 75 ? 'screen-256color' : + termRoll < 88 ? 'tmux-256color' : + termRoll < 95 ? 'alacritty' : 'rxvt-unicode-256color'; + + const shellRoll = h('shell').readUInt8(0) % 100; + const shell = shellRoll < 55 ? '/bin/bash' : + shellRoll < 85 ? '/bin/zsh' : + shellRoll < 95 ? '/usr/bin/bash' : '/usr/bin/zsh'; + + // ── host.id: /etc/machine-id (32 hex chars, Linux 标准) ── + const machineId = h('machine-id').slice(0, 16).toString('hex'); + + // ── PID: 每个 session 随机生成,模拟每次启动新进程 ── + // 不用 seed 确定性生成,因为真实 CLI 每次启动都是新 PID + const pid = 1000 + Math.floor(Math.random() * 64000); + + // ── kernel version: 模拟真实 Linux 发行版 ── + const kb = h('kernel'); + const kernelMajor = 5 + (kb.readUInt8(0) % 2); // 5 or 6 + const kernelMinor = kb.readUInt8(1) % 20; + const kernelPatch = kb.readUInt8(2) % 200; + const ubuntuBuild = 50 + (kb.readUInt8(3) % 150); + const osVersion = `#${ubuntuBuild}-Ubuntu SMP`; + + // ── 可执行文件路径: 按安装方式分布 ── + const pathRoll = h('execpath').readUInt8(0) % 100; + const executablePath = pathRoll < 40 ? `/home/${username}/.claude/local/claude` : + pathRoll < 70 ? '/usr/local/bin/claude' : + pathRoll < 90 ? `/home/${username}/.local/bin/claude` : + '/usr/bin/claude'; + + return { + hostname, + username, + terminal, + shell, + machineId, + pid, + arch: 'x64', + osType: 'Linux', + osVersion, + kernelRelease: `${kernelMajor}.${kernelMinor}.${kernelPatch}-generic`, + // service.instance.id: 每个 session 唯一(CLI 用 randomUUID) + serviceInstanceId: crypto.randomUUID(), + executablePath, + executableName: 'claude', + command: 'claude', + commandArgs: [], + runtimeName: 'nodejs', + runtimeVersion: FAKE_NODE_VERSION.replace('v', ''), + // ripgrep 信息也按 seed 生成,不同账号不一样 + ripgrepVersion: (() => { + const rv = h('ripgrep'); + const versions = ['14.1.1','14.1.0','14.0.2','13.0.0','13.0.1','14.0.1','14.0.0']; + return versions[rv.readUInt8(0) % versions.length]; + })(), + ripgrepPath: (() => { + const rp = h('rgpath'); + const paths = ['/usr/bin/rg','/usr/local/bin/rg','/home/'+username+'/.cargo/bin/rg','/snap/bin/rg','/usr/bin/rg','/usr/bin/rg']; + return paths[rp.readUInt8(0) % paths.length]; + })(), + // MCP server 数量(真实用户 0~6 个,影响启动事件序列) + mcpServerCount: 1 + (h('mcp').readUInt8(0) % 5), // 1~5 + mcpFailCount: h('mcp').readUInt8(1) % 3, // 0~2 个失败 + }; +} + // ─── 遥测模拟 ──────────────────────────────────────────── // 每个 device_id 的会话状态 @@ -34,11 +152,15 @@ const sessionStates = new Map(); function getOrCreateSession(deviceId) { if (sessionStates.has(deviceId)) return sessionStates.get(deviceId); + const hostId = generateHostIdentity(deviceId); const state = { sessionId: crypto.randomUUID(), deviceId, + hostId, startTime: Date.now(), requestCount: 0, + // 追踪 ripgrep 是否已上报 + ripgrepReported: false, }; sessionStates.set(deviceId, state); return state; @@ -48,11 +170,13 @@ function generateDeviceId(accountSeed) { return crypto.createHash('sha256').update(`device:${accountSeed}`).digest('hex'); } -function buildEnvBlock() { +// ─── OTEL Resource Attributes (匹配 CLI 的 detectResources) ─── + +function buildEnvBlock(hostId) { return { platform: 'linux', - node_version: process.version, - terminal: 'xterm-256color', + node_version: FAKE_NODE_VERSION, + terminal: hostId.terminal, package_managers: 'npm', runtimes: 'node', is_running_with_bun: false, @@ -62,7 +186,7 @@ function buildEnvBlock() { is_claude_code_action: false, is_claude_ai_auth: false, version: CLI_VERSION, - arch: 'x64', + arch: hostId.arch, is_claude_code_remote: false, deployment_environment: 'unknown-linux', is_conductor: false, @@ -75,59 +199,82 @@ function buildEnvBlock() { } function buildProcessMetrics(uptime) { - const rss = 200_000_000 + Math.floor(Math.random() * 100_000_000); + // 模拟真实 CLI 的内存曲线:RSS 随 uptime 缓慢增长 + const baseRss = 180_000_000 + Math.min(uptime * 50_000, 200_000_000); + const rss = Math.floor(baseRss + Math.random() * 80_000_000); + const heapTotal = Math.floor(rss * 0.6 + Math.random() * 10_000_000); + const heapUsed = Math.floor(heapTotal * 0.5 + Math.random() * heapTotal * 0.3); return Buffer.from(JSON.stringify({ uptime, rss, - heapTotal: 30_000_000 + Math.floor(Math.random() * 5_000_000), - heapUsed: 40_000_000 + Math.floor(Math.random() * 20_000_000), + heapTotal, + heapUsed, external: 14_000_000 + Math.floor(Math.random() * 2_000_000), arrayBuffers: Math.floor(Math.random() * 10_000), constrainedMemory: 0, - cpuUsage: { user: 100_000 + Math.floor(Math.random() * 300_000), system: 20_000 + Math.floor(Math.random() * 80_000) }, + cpuUsage: { + user: Math.floor(uptime * 10_000 + Math.random() * 300_000), + system: Math.floor(uptime * 2_000 + Math.random() * 80_000), + }, cpuPercent: Math.random() * 200, })).toString('base64'); } -function buildEvent(eventName, session, model, betas) { +function buildEvent(eventName, session, model, betas, extraData, timestampOverride) { const uptime = (Date.now() - session.startTime) / 1000; + const processMetrics = buildProcessMetrics(uptime); + // 缓存最近一次的 process metrics,供 DataDog 日志复用(保持两边一致) + session._lastProcessMetrics = { uptime, raw: processMetrics }; + const eventData = { + event_name: eventName, + client_timestamp: timestampOverride || new Date().toISOString(), + model: model || 'claude-sonnet-4-6', + session_id: session.sessionId, + user_type: 'external', + betas: betas || 'claude-code-20250219,interleaved-thinking-2025-05-14', + env: buildEnvBlock(session.hostId), + entrypoint: 'cli', + is_interactive: true, + client_type: 'cli', + process: processMetrics, + event_id: crypto.randomUUID(), + device_id: session.deviceId, + // 注意:不加 resource 字段 — event_logging/batch 是自定义端点, + // OTEL resource attributes 由 CLI 通过单独的 OTLP exporter 发送,不在这里 + }; + // 合并额外字段(用于特定事件的附加数据) + if (extraData) Object.assign(eventData, extraData); return { event_type: 'ClaudeCodeInternalEvent', - event_data: { - event_name: eventName, - client_timestamp: new Date().toISOString(), - model: model || 'claude-sonnet-4-6', - session_id: session.sessionId, - user_type: 'external', - betas: betas || 'claude-code-20250219,interleaved-thinking-2025-05-14', - env: buildEnvBlock(), - entrypoint: 'cli', - is_interactive: true, - client_type: 'cli', - process: buildProcessMetrics(uptime), - event_id: crypto.randomUUID(), - device_id: session.deviceId, - }, + event_data: eventData, }; } // 发送遥测到 api.anthropic.com/api/event_logging/batch -function sendTelemetryEvents(events) { +function sendTelemetryEvents(events, session) { if (!TELEMETRY_ENABLED || events.length === 0) return; const body = JSON.stringify({ events }); + const headers = { + 'Accept': 'application/json, text/plain, */*', + 'Content-Type': 'application/json', + 'User-Agent': `claude-code/${CLI_VERSION}`, + 'x-service-name': 'claude-code', + 'Content-Length': Buffer.byteLength(body), + }; + // 如果有 session,注入 OTEL trace headers(匹配 CLI 的 W3C Trace Context) + if (session) { + const traceId = crypto.randomBytes(16).toString('hex'); + const spanId = crypto.randomBytes(8).toString('hex'); + headers['traceparent'] = `00-${traceId}-${spanId}-01`; + } + const opts = { hostname: 'api.anthropic.com', port: 443, path: '/api/event_logging/batch', method: 'POST', - headers: { - 'Accept': 'application/json, text/plain, */*', - 'Content-Type': 'application/json', - 'User-Agent': `claude-code/${CLI_VERSION}`, - 'x-service-name': 'claude-code', - 'Content-Length': Buffer.byteLength(body), - }, + headers, timeout: 10000, }; @@ -146,13 +293,40 @@ function sendTelemetryEvents(events) { function sendDatadogLog(eventName, session, model) { if (!TELEMETRY_ENABLED) return; + const hostId = session.hostId; const uptime = (Date.now() - session.startTime) / 1000; + + // 复用 Anthropic 事件侧缓存的 process metrics(保持两边数值一致) + // 如果没有缓存(首次调用),现场生成 + let pm; + if (session._lastProcessMetrics && Math.abs(session._lastProcessMetrics.uptime - uptime) < 2) { + pm = JSON.parse(Buffer.from(session._lastProcessMetrics.raw, 'base64').toString()); + } else { + const baseRss = 180_000_000 + Math.min(uptime * 50_000, 200_000_000); + const rss = Math.floor(baseRss + Math.random() * 80_000_000); + const heapTotal = Math.floor(rss * 0.6 + Math.random() * 10_000_000); + const heapUsed = Math.floor(heapTotal * 0.5 + Math.random() * heapTotal * 0.3); + pm = { + uptime, + rss, + heapTotal, + heapUsed, + external: 14_000_000 + Math.floor(Math.random() * 2_000_000), + arrayBuffers: Math.floor(Math.random() * 10_000), + constrainedMemory: 0, + cpuUsage: { + user: Math.floor(uptime * 10_000 + Math.random() * 300_000), + system: Math.floor(uptime * 2_000 + Math.random() * 80_000), + }, + }; + } + const entry = { ddsource: 'nodejs', - ddtags: `event:${eventName},arch:x64,client_type:cli,model:${model || 'claude-sonnet-4-6'},platform:linux,user_type:external,version:${CLI_VERSION},version_base:${CLI_VERSION}`, + ddtags: `event:${eventName},arch:${hostId.arch},client_type:cli,model:${model || 'claude-sonnet-4-6'},platform:linux,user_type:external,version:${CLI_VERSION},version_base:${CLI_VERSION}`, message: eventName, service: 'claude-code', - hostname: 'claude-code', + hostname: hostId.hostname, env: 'external', model: model || 'claude-sonnet-4-6', session_id: session.sessionId, @@ -160,20 +334,11 @@ function sendDatadogLog(eventName, session, model) { entrypoint: 'cli', is_interactive: 'true', client_type: 'cli', - process_metrics: { - uptime, - rss: 200_000_000 + Math.floor(Math.random() * 100_000_000), - heapTotal: 30_000_000 + Math.floor(Math.random() * 5_000_000), - heapUsed: 40_000_000 + Math.floor(Math.random() * 20_000_000), - external: 14_000_000 + Math.floor(Math.random() * 2_000_000), - arrayBuffers: Math.floor(Math.random() * 10_000), - constrainedMemory: 0, - cpuUsage: { user: 100_000 + Math.floor(Math.random() * 300_000), system: 20_000 + Math.floor(Math.random() * 80_000) }, - }, + process_metrics: pm, platform: 'linux', platform_raw: 'linux', - arch: 'x64', - node_version: process.version, + arch: hostId.arch, + node_version: FAKE_NODE_VERSION, version: CLI_VERSION, version_base: CLI_VERSION, build_time: BUILD_TIME, @@ -219,18 +384,38 @@ function emitPreRequestTelemetry(reqHeaders, body) { const betas = reqHeaders['anthropic-beta'] || 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,redact-thinking-2026-02-12,context-management-2025-06-27,prompt-caching-scope-2026-01-05,effort-2025-11-24'; - // 首次请求:发完整启动事件序列(匹配真实 CLI 抓包:4-6 个事件) + // 首次请求:发完整启动事件序列(匹配真实 CLI 的时序) if (session.requestCount === 1) { - // 第一批:MCP 连接事件(真实 CLI 有多个 MCP server) + const hostId = session.hostId; + // 生成递增的时间戳,模拟真实 CLI 启动流程的时间差 + const baseTime = Date.now(); + const ts = (offsetMs) => new Date(baseTime + offsetMs).toISOString(); + + // 第一批:启动 + 工具检测 + MCP 连接事件 const batch1 = [ - buildEvent('tengu_started', session, model, betas), - buildEvent('tengu_init', session, model, betas), - buildEvent('tengu_mcp_server_connection_failed', session, model, betas), - buildEvent('tengu_mcp_server_connection_failed', session, model, betas), - buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas), - buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas), + buildEvent('tengu_started', session, model, betas, null, ts(0)), + buildEvent('tengu_init', session, model, betas, null, ts(80 + Math.floor(Math.random() * 120))), + // tengu_ripgrep_availability — CLI 必发的工具检测事件,版本/路径按账号不同 + buildEvent('tengu_ripgrep_availability', session, model, betas, { + ripgrep_available: true, + ripgrep_version: hostId.ripgrepVersion, + ripgrep_path: hostId.ripgrepPath, + }, ts(200 + Math.floor(Math.random() * 150))), ]; - sendTelemetryEvents(batch1); + // MCP 连接事件:数量按账号不同(真实用户配置的 MCP server 数量差异很大) + let mcpOffset = 400; + const mcpSuccessCount = hostId.mcpServerCount - hostId.mcpFailCount; + for (let i = 0; i < hostId.mcpFailCount; i++) { + mcpOffset += 100 + Math.floor(Math.random() * 300); + batch1.push(buildEvent('tengu_mcp_server_connection_failed', session, model, betas, null, ts(mcpOffset))); + } + for (let i = 0; i < mcpSuccessCount; i++) { + mcpOffset += 200 + Math.floor(Math.random() * 500); + batch1.push(buildEvent('tengu_mcp_server_connection_succeeded', session, model, betas, null, ts(mcpOffset))); + } + + session.ripgrepReported = true; + sendTelemetryEvents(batch1, session); sendDatadogLog('tengu_started', session, model); sendDatadogLog('tengu_init', session, model); @@ -240,7 +425,7 @@ function emitPreRequestTelemetry(reqHeaders, body) { buildEvent('tengu_session_init', session, model, betas), buildEvent('tengu_context_loaded', session, model, betas), ]; - sendTelemetryEvents(batch2); + sendTelemetryEvents(batch2, session); }, 25000 + Math.floor(Math.random() * 10000)); } @@ -248,7 +433,7 @@ function emitPreRequestTelemetry(reqHeaders, body) { const events = [ buildEvent('tengu_api_request_started', session, model, betas), ]; - sendTelemetryEvents(events); + sendTelemetryEvents(events, session); } // 请求后发遥测 @@ -270,16 +455,28 @@ function emitPostRequestTelemetry(reqHeaders, statusCode, body) { buildEvent('tengu_api_request_completed', session, model, betas), buildEvent('tengu_conversation_turn_completed', session, model, betas), ]; - sendTelemetryEvents(events); + sendTelemetryEvents(events, session); sendDatadogLog('tengu_api_request_completed', session, model); - // 随机发额外事件(模拟用户行为:打开文件、查看搜索等) + // 模拟错误遥测(低概率,匹配 TelemetrySafeError) + if (statusCode >= 400 && Math.random() < 0.5) { + const errorEvent = buildEvent('tengu_api_request_error', session, model, betas, { + error_type: 'TelemetrySafeError', + error_code: statusCode, + error_message: statusCode === 429 ? 'rate_limit_exceeded' : + statusCode === 529 ? 'overloaded' : + statusCode >= 500 ? 'server_error' : 'client_error', + }); + sendTelemetryEvents([errorEvent], session); + } + + // 随机发额外事件(仅使用已知的真实 CLI 事件名) if (Math.random() < 0.3) { setTimeout(() => { const extra = [ buildEvent('tengu_tool_use_completed', session, model, betas), ]; - sendTelemetryEvents(extra); + sendTelemetryEvents(extra, session); }, 2000 + Math.floor(Math.random() * 5000)); } }