From 6958b0dedb375f47b922b86b09d474dd95b43717 Mon Sep 17 00:00:00 2001 From: win Date: Wed, 25 Mar 2026 11:50:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20macOS=20=E6=8C=87=E7=BA=B9=E4=BC=AA?= =?UTF-8?q?=E8=A3=85=20=E2=80=94=20TCP=20TTL/=E6=97=B6=E9=97=B4=E6=88=B3/?= =?UTF-8?q?=E6=97=B6=E5=8C=BA=20+=20H2=E4=BC=98=E5=85=88=20+=20Jitter?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit proxy.js: - 主机身份全面改为 macOS (hostname: alex-MBP, osType: Darwin) - macOS 版本号 (Ventura/Sonoma/Sequoia), Darwin 内核 22/23/24.x - machineId 改为 IOPlatformUUID 格式(大写 UUID) - arch: 70% arm64 / 30% x64(Apple Silicon 主流) - 遥测 platform/ddtags 改为 darwin,路径改为 /Users/ - Jitter: 指数分布,80% 快(80-300ms) / 20% 慢(400-1200ms) - H2 优先: api.anthropic.com/cloudaicompanion/generativelanguage 直接走 H2 setup-firewall.sh: - 新增 TCP TTL 强制 = 64 (iptables mangle TTL) - 新增 TCP 时间戳禁用 (net.ipv4.tcp_timestamps=0 + 持久化) - 新增系统时区设置 America/Los_Angeles - 新增 timezone 子命令、完整 status 输出 --- antigravity/firewall/setup-firewall.sh | 115 +++++++++++++--- antigravity/node-tls-proxy/proxy.js | 175 +++++++++++++------------ 2 files changed, 190 insertions(+), 100 deletions(-) diff --git a/antigravity/firewall/setup-firewall.sh b/antigravity/firewall/setup-firewall.sh index 14041d88..d22a8a43 100755 --- a/antigravity/firewall/setup-firewall.sh +++ b/antigravity/firewall/setup-firewall.sh @@ -1,10 +1,16 @@ #!/bin/bash -# sub2api 指纹防泄露 iptables 规则 -# 确保只有 Node.js TLS Proxy 能直连上游 HTTPS, -# sub2api Go 进程即使有 bug 也无法绕过。 +# sub2api Antigravity — 指纹防泄露 + macOS 特征伪装规则 +# +# 功能: +# 1. QUIC/UDP 阻断 — 强制走 TCP/TLS +# 2. 出站 TCP 443 限制 — 只有 nodeproxy 用户能直连 +# 3. IPv6 阻断 — 消除 IPv6 泄露 +# 4. TCP TTL 伪装 — 改为 64,匹配 macOS/Linux(对抗 OS 识别) +# 5. TCP 时间戳重写 — 禁用内核时间戳,防止通过 TCP TS 推算 uptime/系统时间 +# 6. 系统时区设置 — 设为 America/Los_Angeles(加州时区,匹配目标用户群) # # 用法: -# sudo bash setup-firewall.sh [apply|remove|status] +# sudo bash setup-firewall.sh [apply|remove|status|timezone] # # 前置条件: # - Node.js proxy 以专用用户 "nodeproxy" 运行 @@ -14,9 +20,50 @@ set -euo pipefail NODE_PROXY_USER="${MG_NODE_PROXY_USER:-nodeproxy}" CHAIN_NAME="MG_FINGERPRINT" +TARGET_TZ="America/Los_Angeles" log() { echo "[$(date '+%H:%M:%S')] $*"; } +# ─── 时区设置 ──────────────────────────────────────────────────────── +set_timezone() { + log "Setting system timezone to $TARGET_TZ ..." + if command -v timedatectl &>/dev/null; then + timedatectl set-timezone "$TARGET_TZ" + log " timedatectl: timezone set to $(timedatectl show -p Timezone --value)" + elif [ -f "/usr/share/zoneinfo/$TARGET_TZ" ]; then + ln -sf "/usr/share/zoneinfo/$TARGET_TZ" /etc/localtime + echo "$TARGET_TZ" > /etc/timezone + log " /etc/localtime -> $TARGET_TZ" + else + log " WARNING: Cannot set timezone — timedatectl not found and zoneinfo missing" + fi +} + +# ─── TCP 时间戳禁用 ────────────────────────────────────────────────── +# Linux TCP 时间戳会随系统 uptime 线性增长,对方可通过测量 TS 差值 +# 推算服务器启动时间,识破"全天候在线的服务器"特征。 +# 禁用后 TCP TS 选项不再发送,无法通过 TS 推断 uptime。 +disable_tcp_timestamps() { + log "Disabling TCP timestamps (anti-uptime fingerprinting)..." + sysctl -w net.ipv4.tcp_timestamps=0 > /dev/null + # 持久化(防止重启后恢复) + if ! grep -q "net.ipv4.tcp_timestamps" /etc/sysctl.conf 2>/dev/null; then + echo "net.ipv4.tcp_timestamps=0" >> /etc/sysctl.conf + log " Written to /etc/sysctl.conf" + else + sed -i 's/net.ipv4.tcp_timestamps=.*/net.ipv4.tcp_timestamps=0/' /etc/sysctl.conf + log " Updated in /etc/sysctl.conf" + fi + log " TCP timestamps: DISABLED" +} + +enable_tcp_timestamps() { + sysctl -w net.ipv4.tcp_timestamps=1 > /dev/null + sed -i 's/net.ipv4.tcp_timestamps=.*/net.ipv4.tcp_timestamps=1/' /etc/sysctl.conf 2>/dev/null || true + log " TCP timestamps: ENABLED (restored)" +} + +# ─── iptables 规则 ─────────────────────────────────────────────────── apply_rules() { log "Applying fingerprint firewall rules..." @@ -30,13 +77,13 @@ apply_rules() { # 创建自定义链(幂等) iptables -N "$CHAIN_NAME" 2>/dev/null || iptables -F "$CHAIN_NAME" - # === Rule 1: QUIC 阻断 — 丢弃所有出站 UDP 443/4433 === + # === Rule 1: QUIC 阻断 === iptables -A "$CHAIN_NAME" -p udp --dport 443 -j DROP \ -m comment --comment "MG: block QUIC/HTTP3 UDP 443" iptables -A "$CHAIN_NAME" -p udp --dport 4433 -j DROP \ -m comment --comment "MG: block QUIC alt UDP 4433" - # === Rule 2: 允许 Node.js proxy 出站 TCP 443 === + # === Rule 2: 允许 nodeproxy 出站 TCP 443 === iptables -A "$CHAIN_NAME" -p tcp --dport 443 \ -m owner --uid-owner "$NODE_PROXY_USER" -j ACCEPT \ -m comment --comment "MG: allow nodeproxy TCP 443" @@ -45,43 +92,66 @@ apply_rules() { iptables -A "$CHAIN_NAME" -p tcp --dport 443 -j REJECT --reject-with tcp-reset \ -m comment --comment "MG: block non-proxy TCP 443" - # 将自定义链挂载到 OUTPUT(幂等) + # 挂载到 OUTPUT(幂等) if ! iptables -C OUTPUT -j "$CHAIN_NAME" 2>/dev/null; then iptables -A OUTPUT -j "$CHAIN_NAME" fi # === Rule 4: IPv6 全面阻断 === ip6tables -N "${CHAIN_NAME}_V6" 2>/dev/null || ip6tables -F "${CHAIN_NAME}_V6" - # 允许回环 ip6tables -A "${CHAIN_NAME}_V6" -o lo -j ACCEPT \ -m comment --comment "MG: allow IPv6 loopback" - # 阻断其他 IPv6 出站 ip6tables -A "${CHAIN_NAME}_V6" -j DROP \ -m comment --comment "MG: block all IPv6 outbound" - if ! ip6tables -C OUTPUT -j "${CHAIN_NAME}_V6" 2>/dev/null; then ip6tables -A OUTPUT -j "${CHAIN_NAME}_V6" fi + # === Rule 5: TCP TTL 伪装 (macOS TTL = 64) === + # macOS 和 Linux 默认 TTL 都是 64,但数据中心 Linux 有些发行版是 128。 + # 强制设为 64 确保一致,并防止"服务器离对方 0 跳"露馅。 + iptables -t mangle -N "${CHAIN_NAME}_TTL" 2>/dev/null || iptables -t mangle -F "${CHAIN_NAME}_TTL" + iptables -t mangle -A "${CHAIN_NAME}_TTL" -p tcp --dport 443 \ + -j TTL --ttl-set 64 \ + -m comment --comment "MG: spoof TTL=64 (macOS)" + if ! iptables -t mangle -C OUTPUT -j "${CHAIN_NAME}_TTL" 2>/dev/null; then + iptables -t mangle -A OUTPUT -j "${CHAIN_NAME}_TTL" + fi + log "Firewall rules applied successfully." log " - UDP 443/4433: BLOCKED (QUIC)" log " - TCP 443: ONLY '$NODE_PROXY_USER' allowed" log " - IPv6 outbound: BLOCKED" + log " - TCP TTL: FORCED to 64 (macOS spoof)" + + # === TCP 时间戳禁用 === + disable_tcp_timestamps + + # === 时区设置 === + set_timezone + + log "" + log "=== All anti-fingerprint measures applied ===" + log " OS Fingerprint: TTL=64 (macOS/Linux)" + log " TCP Timestamps: Disabled (anti-uptime leak)" + log " Timezone: $TARGET_TZ" } remove_rules() { log "Removing fingerprint firewall rules..." - # 从 OUTPUT 移除引用 iptables -D OUTPUT -j "$CHAIN_NAME" 2>/dev/null || true ip6tables -D OUTPUT -j "${CHAIN_NAME}_V6" 2>/dev/null || true + iptables -t mangle -D OUTPUT -j "${CHAIN_NAME}_TTL" 2>/dev/null || true - # 清空并删除自定义链 iptables -F "$CHAIN_NAME" 2>/dev/null || true iptables -X "$CHAIN_NAME" 2>/dev/null || true ip6tables -F "${CHAIN_NAME}_V6" 2>/dev/null || true ip6tables -X "${CHAIN_NAME}_V6" 2>/dev/null || true + iptables -t mangle -F "${CHAIN_NAME}_TTL" 2>/dev/null || true + iptables -t mangle -X "${CHAIN_NAME}_TTL" 2>/dev/null || true + enable_tcp_timestamps log "Firewall rules removed." } @@ -89,16 +159,29 @@ show_status() { log "=== IPv4 MG_FINGERPRINT chain ===" iptables -L "$CHAIN_NAME" -n -v 2>/dev/null || echo "(not found)" echo + log "=== IPv4 mangle TTL chain ===" + iptables -t mangle -L "${CHAIN_NAME}_TTL" -n -v 2>/dev/null || echo "(not found)" + echo log "=== IPv6 MG_FINGERPRINT_V6 chain ===" ip6tables -L "${CHAIN_NAME}_V6" -n -v 2>/dev/null || echo "(not found)" + echo + log "=== TCP Timestamps ===" + sysctl net.ipv4.tcp_timestamps + echo + log "=== System Timezone ===" + timedatectl show -p Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo "(unknown)" + echo + log "=== Current TTL (outbound) ===" + sysctl net.ipv4.ip_default_ttl } case "${1:-apply}" in - apply) apply_rules ;; - remove) remove_rules ;; - status) show_status ;; + apply) apply_rules ;; + remove) remove_rules ;; + status) show_status ;; + timezone) set_timezone ;; *) - echo "Usage: $0 [apply|remove|status]" + echo "Usage: $0 [apply|remove|status|timezone]" exit 1 ;; esac diff --git a/antigravity/node-tls-proxy/proxy.js b/antigravity/node-tls-proxy/proxy.js index cb5c6c3f..3fad7d4c 100644 --- a/antigravity/node-tls-proxy/proxy.js +++ b/antigravity/node-tls-proxy/proxy.js @@ -39,88 +39,75 @@ const h2Sessions = new Map(); // 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']; +// ─── macOS 主机身份词表 ────────────────────────────────────────── +// macOS 用户 hostname 习惯: "alex-MBP", "sam-MacBook-Pro" 等 +const MBP_NAMES = ['alex','sam','chris','max','lee','kai','jamie','taylor','morgan','casey', + 'drew','avery','riley','blake','jordan','ryan','parker','quinn','reese','cameron']; +const MBP_SUFFIX = ['-MBP','-MacBook','-MacBook-Pro','-MacBook-Air',"s-MBP","s-MacBook","s-MacBook-Pro"]; function generateHostIdentity(seed) { - // 确定性哈希工具:同一 seed+suffix 永远返回同一结果 - const h = (suffix) => crypto.createHash('sha256').update(seed + ':' + suffix).digest(); + const h = (s) => crypto.createHash('sha256').update(seed + ':' + s).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}`; + // ── hostname: macOS 风格 ── + const hb = h('hostname'); + const name = MBP_NAMES[hb.readUInt8(0) % MBP_NAMES.length]; + const sfx = MBP_SUFFIX[hb.readUInt8(1) % MBP_SUFFIX.length]; + const hostname = `${name}${sfx}`; - // ── 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}`; + // ── username: 取自 hostname 名字(真实 Mac 行为) ── + const username = name; - // ── terminal & shell: 按权重分布(xterm-256color 占大多数) ── + // ── terminal: macOS 常见终端分布 ── 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 terminal = termRoll < 75 ? 'xterm-256color' : + termRoll < 88 ? 'screen-256color' : + termRoll < 96 ? 'alacritty' : 'kitty'; + // ── shell: macOS 默认 zsh(Catalina+);部分用 bash/fish ── 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'; + const shell = shellRoll < 65 ? '/bin/zsh' : + shellRoll < 82 ? '/usr/local/bin/zsh' : + shellRoll < 93 ? '/bin/bash' : '/opt/homebrew/bin/fish'; - // ── host.id: /etc/machine-id (32 hex chars, Linux 标准) ── - const machineId = h('machine-id').slice(0, 16).toString('hex'); + // ── host.id: macOS IOPlatformUUID 格式(大写 UUID) ── + const mid = h('machine-id'); + const machineId = [ + mid.slice(0,4).toString('hex').toUpperCase(), + mid.slice(4,6).toString('hex').toUpperCase(), + mid.slice(6,8).toString('hex').toUpperCase(), + mid.slice(8,10).toString('hex').toUpperCase(), + mid.slice(10,16).toString('hex').toUpperCase(), + ].join('-'); - // ── PID: 每个 session 随机生成,模拟每次启动新进程 ── - // 不用 seed 确定性生成,因为真实 CLI 每次启动都是新 PID - const pid = 1000 + Math.floor(Math.random() * 64000); + // ── PID: macOS GUI 应用 PID 通常较小 ── + const pid = 500 + Math.floor(Math.random() * 8000); - // ── 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`; + // ── macOS 版本: 13(Ventura)/14(Sonoma)/15(Sequoia) ── + const kb = h('kernel'); + const macosMajor = 13 + (kb.readUInt8(0) % 3); + const macosMinor = kb.readUInt8(1) % 8; + const macosPatch = kb.readUInt8(2) % 5; + // Darwin 内核: macOS 13=22.x, 14=23.x, 15=24.x + const darwinMajor = 22 + (macosMajor - 13); + const darwinMinor = kb.readUInt8(3) % 7; + const darwinPatch = kb.readUInt8(4) % 5; + const osVersion = `${macosMajor}.${macosMinor}.${macosPatch}`; - // ── 可执行文件路径: 按安装方式分布 ── + // ── arch: Apple Silicon arm64 占 70%,Intel x64 占 30% ── + const arch = h('arch').readUInt8(0) % 100 < 70 ? 'arm64' : 'x64'; + + // ── 可执行文件路径: macOS 常见安装位置 ── 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'; + const executablePath = pathRoll < 50 ? `/Users/${username}/.claude/local/claude` : + pathRoll < 80 ? '/usr/local/bin/claude' : + pathRoll < 95 ? `/Users/${username}/.local/bin/claude` : + '/opt/homebrew/bin/claude'; return { - hostname, - username, - terminal, - shell, - machineId, - pid, - arch: 'x64', - osType: 'Linux', + hostname, username, terminal, shell, machineId, pid, arch, + osType: 'Darwin', osVersion, - kernelRelease: `${kernelMajor}.${kernelMinor}.${kernelPatch}-generic`, - // service.instance.id: 每个 session 唯一(CLI 用 randomUUID) + kernelRelease: `${darwinMajor}.${darwinMinor}.${darwinPatch}`, serviceInstanceId: crypto.randomUUID(), executablePath, executableName: 'claude', @@ -128,20 +115,21 @@ function generateHostIdentity(seed) { 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]; + return ['14.1.1','14.1.0','14.0.2','13.0.0','13.0.1','14.0.1','14.0.0'][rv.readUInt8(0) % 7]; })(), 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]; + return [ + '/opt/homebrew/bin/rg', + '/usr/local/bin/rg', + `/Users/${username}/.cargo/bin/rg`, + '/usr/local/opt/ripgrep/bin/rg', + ][rp.readUInt8(0) % 4]; })(), - // MCP server 数量(真实用户 0~6 个,影响启动事件序列) - mcpServerCount: 1 + (h('mcp').readUInt8(0) % 5), // 1~5 - mcpFailCount: h('mcp').readUInt8(1) % 3, // 0~2 个失败 + mcpServerCount: 1 + (h('mcp').readUInt8(0) % 5), + mcpFailCount: h('mcp').readUInt8(1) % 3, }; } @@ -173,8 +161,9 @@ function generateDeviceId(accountSeed) { // ─── OTEL Resource Attributes (匹配 CLI 的 detectResources) ─── function buildEnvBlock(hostId) { + const platformStr = 'darwin'; return { - platform: 'linux', + platform: platformStr, node_version: FAKE_NODE_VERSION, terminal: hostId.terminal, package_managers: 'npm', @@ -188,13 +177,13 @@ function buildEnvBlock(hostId) { version: CLI_VERSION, arch: hostId.arch, is_claude_code_remote: false, - deployment_environment: 'unknown-linux', + deployment_environment: `unknown-${platformStr}`, is_conductor: false, version_base: CLI_VERSION, build_time: BUILD_TIME, is_local_agent_mode: false, vcs: 'git', - platform_raw: 'linux', + platform_raw: platformStr, }; } @@ -323,7 +312,7 @@ function sendDatadogLog(eventName, session, model) { const entry = { ddsource: 'nodejs', - 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}`, + ddtags: `event:${eventName},arch:${hostId.arch},client_type:cli,model:${model || 'claude-sonnet-4-6'},platform:darwin,user_type:external,version:${CLI_VERSION},version_base:${CLI_VERSION}`, message: eventName, service: 'claude-code', hostname: hostId.hostname, @@ -335,14 +324,14 @@ function sendDatadogLog(eventName, session, model) { is_interactive: 'true', client_type: 'cli', process_metrics: pm, - platform: 'linux', - platform_raw: 'linux', + platform: 'darwin', + platform_raw: 'darwin', arch: hostId.arch, node_version: FAKE_NODE_VERSION, version: CLI_VERSION, version_base: CLI_VERSION, build_time: BUILD_TIME, - deployment_environment: 'unknown-linux', + deployment_environment: 'unknown-darwin', vcs: 'git', }; @@ -667,11 +656,29 @@ async function proxyRequest(req, res) { // 请求前发遥测(仅 /v1/messages 请求) if (req.url.includes('/v1/messages') && TELEMETRY_ENABLED) { emitPreRequestTelemetry(savedHeaders, body); - // 随机延迟 50-200ms 模拟真实 CLI 行为 - await new Promise(r => setTimeout(r, 50 + Math.floor(Math.random() * 150))); } - if (h2Hosts.has(targetHost)) { + // ── Jitter 注入 ────────────────────────────────────────────────── + // 模拟人类编码间歇:80% 快速响应(80-300ms),20% 慢速思考(400-1200ms) + // 使用 -log(rand) 指数衰减使延迟尾部更接近真实键盘输入节奏 + const jitterMs = (() => { + if (Math.random() < 0.80) { + return Math.floor(80 + (-Math.log(Math.random()) * 90)); // 快:~80-300ms + } + return Math.floor(400 + Math.random() * 800); // 慢:400-1200ms + })(); + await new Promise(r => setTimeout(r, jitterMs)); + + // ── H2 优先策略 ────────────────────────────────────────────────── + // Anthropic/Google API 均支持 HTTP/2。 + // 直接走 H2 = Node.js 原生帧顺序,与真实 CLI 完全一致。 + // 其他 host 维持原有 H1→H2 自动切换逻辑。 + const H2_PREFER_HOSTS = new Set([ + 'api.anthropic.com', + 'cloudaicompanion.googleapis.com', + 'generativelanguage.googleapis.com', + ]); + if (H2_PREFER_HOSTS.has(targetHost) || h2Hosts.has(targetHost)) { await sendViaH2(targetHost, req.method, req.url, req.headers, body, res, savedHeaders); } else { await sendViaH1(targetHost, req.method, req.url, req.headers, body, res, savedHeaders);