Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions deploy/docker/managed-user/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
nano \
less \
unzip \
tzdata \
&& rm -rf /var/lib/apt/lists/*

RUN sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf \
Expand Down Expand Up @@ -224,6 +225,15 @@ COPY deploy/docker/managed-user/launch-chromium.sh /usr/local/bin/launch-chromiu
COPY deploy/docker/managed-user/restart-vnc.sh /usr/local/bin/restart-vnc
COPY deploy/docker/managed-user/vnc-status.sh /usr/local/bin/vnc-status

# v4.3: 进程内环境伪装模块(经 BUN_OPTIONS=--preload 注入 claude 进程)。
# entrypoint 写 /etc/environment,profile.d 做非 login shell 双保险。
COPY tools/spoof-fingerprint.js /usr/local/lib/cloud-cli-proxy/spoof-fingerprint.js
# v4.3: OAuth 浏览器入口 — claude /login 等认证流程在 VNC 桌面 Chromium 打开,
# 浏览器流量与 API 同走 sing-box 出口,OAuth 回调走容器内 localhost。
COPY deploy/docker/managed-user/browser-open.sh /usr/local/bin/cc-browser-open
RUN chmod 0644 /usr/local/lib/cloud-cli-proxy/spoof-fingerprint.js \
&& chmod 0755 /usr/local/bin/cc-browser-open

# v4.0: 容器内 nft default-deny 规则集(D-V4-3 / NET-01 / NET-02)
COPY deploy/docker/managed-user/default-deny.nft /etc/cloud-claude/default-deny.nft
RUN chmod 0644 /etc/cloud-claude/default-deny.nft
Expand Down
57 changes: 57 additions & 0 deletions deploy/docker/managed-user/browser-open.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# OAuth 浏览器入口 — claude /login 等"需要打开浏览器"的认证流程统一走这里。
#
# 背景:claude 的 OAuth 授权会打印 https://claude.ai/oauth/authorize?...&redirect_uri=http://localhost:PORT
# 并经 $BROWSER 打开浏览器。若用户用本机浏览器完成认证:
# 1. 认证流量走用户本地网络(出口 IP 与容器 API 会话不一致,触发风控关联)
# 2. redirect_uri 指向容器内 localhost,本机浏览器根本回调不到
# 因此浏览器必须与 claude 同容器:本脚本在 VNC 桌面(DISPLAY=:99)的 Chromium
# 中打开授权页,浏览器全流量经 sing-box tun 出口,回调走容器内 localhost 直达。
#
# 用户通过产品的 VNC 入口(websocket 6080)看到并操作该浏览器。

set -euo pipefail

URL="${1:-}"
DISPLAY="${DISPLAY:-:99}"
export DISPLAY

if [[ -z "${URL}" ]]; then
echo "[browser-open] no URL given" >&2
exit 1
fi

if ! xdpyinfo >/dev/null 2>&1; then
# 桌面未就绪(MODE=local 或桌面栈故障):不阻塞 claude,给出人工路径
echo "[browser-open] desktop not ready — connect via VNC (ws 6080) and open:" >&2
echo "${URL}" >&2
exit 0
fi

browser_cmd=""
for candidate in chromium chromium-browser google-chrome; do
if command -v "${candidate}" >/dev/null 2>&1; then
browser_cmd="${candidate}"
break
fi
done
if [[ -z "${browser_cmd}" ]]; then
echo "[browser-open] no chromium binary found" >&2
exit 1
fi

# 与桌面 Chromium 共用同一 user-data-dir:已有实例时自动开新标签并复用会话。
# WebRTC 防泄漏 flags 与 launch-chromium.sh 保持一致(UDP 不走非代理路径)。
nohup "${browser_cmd}" \
--no-sandbox \
--disable-dev-shm-usage \
--user-data-dir=/workspace/.chrome-data \
--no-first-run \
--disable-gpu \
--disable-features=WebRtcHideLocalIpsWithMdns \
--enforce-webrtc-ip-permission-check \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp \
"${URL}" >/dev/null 2>&1 &

# 立即返回,避免调用方(claude 的 open 调用)等待浏览器退出
exit 0
85 changes: 77 additions & 8 deletions deploy/docker/managed-user/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -128,19 +128,54 @@ prepare_persistent_state() {
}

prepare_container_disguise() {
# Per-container unique machine-id(基于 hostname + uptime,保证唯一性)
local h; h="$(hostname)"
local t; t="$(cat /proc/uptime 2>/dev/null | tr -d ' .')"
local mid; mid="$(echo -n "${h}-${t}" | sha256sum | cut -c1-32)"
# Per-container unique machine-id。持久化到 /var/lib/claude-persist:
# 容器重启后保持稳定,避免遥测 device_id 漂移引发"频繁新设备"异常。
local persist_mid=/var/lib/claude-persist/machine-id
local mid
if [ -s "$persist_mid" ]; then
mid="$(cat "$persist_mid")"
else
mid="$(echo -n "$(hostname)-$(date +%s)" | sha256sum | cut -c1-32)"
echo "$mid" > "$persist_mid"
chown "${RUN_USER:-workspace}:${RUN_USER:-workspace}" "$persist_mid" 2>/dev/null || true
chmod 444 "$persist_mid"
fi
echo "$mid" > /etc/machine-id
echo "$mid" > /var/lib/dbus/machine-id 2>/dev/null || true
chmod 444 /etc/machine-id

# 时区伪装:与出口 IP 地理位置配套(由控制面经 DISGUISE_TZ 注入,缺省美东)。
# 同时作用于 Date/Intl(TZ 变量)与 shell 工具(/etc/localtime),三层一致。
local tz="${DISGUISE_TZ:-America/New_York}"
if [ -f "/usr/share/zoneinfo/${tz}" ]; then
echo "$tz" > /etc/timezone
ln -sfn "/usr/share/zoneinfo/${tz}" /etc/localtime
else
echo "[entrypoint] WARN unknown DISGUISE_TZ=${tz}, fallback America/New_York" >&2
tz=America/New_York
echo "$tz" > /etc/timezone
ln -sfn "/usr/share/zoneinfo/${tz}" /etc/localtime
fi

# 内存配额透传给 spoof 模块:os.totalmem() 返回 /proc 物理内存(宿主值),
# 按 cgroup 限额伪装,避免泄漏宿主机内存容量。未限额时不注入(spoof 用 8G 缺省)。
local mem_bytes=""
if [ -r /sys/fs/cgroup/memory.max ]; then
local v; v="$(cat /sys/fs/cgroup/memory.max 2>/dev/null || echo max)"
case "$v" in max|'') ;; *[!0-9]*) ;; *) mem_bytes="$v" ;; esac
fi
if [ -z "$mem_bytes" ] && [ -r /sys/fs/cgroup/memory/memory.limit_in_bytes ]; then
local v1; v1="$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes 2>/dev/null || echo '')"
case "$v1" in ''|*[!0-9]*) ;; 9223372036854771712) ;; *) mem_bytes="$v1" ;; esac
fi

# 删除容器检测标志
rm -f /.dockerenv

# 遥测阻断环境变量。覆盖写入(非追加)防止容器 restart 重复追加。
cat > /etc/environment <<'ENVTELEM'
# 遥测阻断 + 伪装模块注入。覆盖写入(非追加)防止容器 restart 重复追加。
# BUN_OPTIONS 是 claude(bun 编译二进制)唯一有效的 preload 注入通道;
# 不注入 NODE_OPTIONS,避免干扰用户自己的 node 程序。
cat > /etc/environment <<ENVTELEM
DISABLE_TELEMETRY=1
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=
Expand All @@ -152,9 +187,39 @@ OTEL_LOGS_EXPORTER=none
SENTRY_DSN=
DISABLE_ERROR_REPORTING=1
TELEMETRY_DISABLED=1
TZ=${tz}
SPOOF_TIMEZONE=${tz}
BUN_OPTIONS=--preload /usr/local/lib/cloud-cli-proxy/spoof-fingerprint.js
BROWSER=/usr/local/bin/cc-browser-open
${mem_bytes:+SPOOF_TOTAL_MEM=${mem_bytes}}
ENVTELEM

echo "[entrypoint] container disguise: machine-id generated, /.dockerenv removed, telemetry blocked"
# UsePAM=no 时 /etc/environment 不进 login shell,SPOOF_* 需经 profile.d 传递。
# 此文件由 entrypoint 按实际配额动态生成,与上方静态 /etc/environment 双通道冗余。
{
echo "export TZ=\"${tz}\""
echo "export SPOOF_TIMEZONE=\"${tz}\""
echo "export BROWSER=/usr/local/bin/cc-browser-open"
if [ -n "$mem_bytes" ]; then
echo "export SPOOF_TOTAL_MEM=${mem_bytes}"
fi
} > /etc/profile.d/zz-disguise.sh
chmod 0644 /etc/profile.d/zz-disguise.sh

echo "[entrypoint] container disguise: machine-id=${mid:0:8}…(persistent) /.dockerenv removed TZ=${tz} mem=${mem_bytes:-default8G} telemetry blocked"

# 导出到 entrypoint(PID 1)进程环境:sshd 会话与 VNC 桌面终端均为其
# 子进程,统一继承;覆盖非 login shell 不读 /etc/environment 的场景。
# 必须在 start_sshd_or_die / start_desktop_stack 之前完成(见 main 流程)。
export TZ="${tz}"
export SPOOF_TIMEZONE="${tz}"
export BUN_OPTIONS="--preload /usr/local/lib/cloud-cli-proxy/spoof-fingerprint.js"
# OAuth 浏览器入口:claude /login 经 $BROWSER 在 VNC 桌面 Chromium 打开,
# 认证流量与 API 同出口,回调走容器内 localhost。
export BROWSER=/usr/local/bin/cc-browser-open
if [ -n "$mem_bytes" ]; then
export SPOOF_TOTAL_MEM="${mem_bytes}"
fi
}

prepare_mergerfs_check() {
Expand Down Expand Up @@ -522,7 +587,6 @@ YAML

prepare_workspace_dirs
prepare_persistent_state
prepare_container_disguise
prepare_mergerfs_check
assert_tmux_version

Expand Down Expand Up @@ -623,6 +687,11 @@ start_vnc_watchdog_async() {

# SSH setup
mkdir -p /var/run/sshd

# 容器伪装在 sshd / 桌面栈启动前完成:machine-id、TZ、BUN_OPTIONS 注入
# 需先进入 PID 1 环境,sshd 会话与 VNC 桌面才能继承。
prepare_container_disguise

if ! ls /etc/ssh/ssh_host_*_key >/dev/null 2>&1; then
ssh-keygen -A
fi
Expand Down
15 changes: 14 additions & 1 deletion deploy/docker/managed-user/launch-chromium.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,21 @@ if [[ -z "${browser_cmd}" ]]; then
-e bash -lc "echo Chromium is not installed.; exec bash"
fi

# 带参数(URL / flags)时同样附加防泄漏与固定 profile 参数,保证
# OAuth 等场景新开的标签与桌面会话一致(chromium 自动复用已有实例)。
if [[ $# -gt 0 ]]; then
exec "${browser_cmd}" "$@"
exec "${browser_cmd}" \
--no-sandbox \
--disable-dev-shm-usage \
--user-data-dir=/workspace/.chrome-data \
--lang="${CHROMIUM_LANG}" \
--accept-lang="${CHROMIUM_ACCEPT_LANG}" \
--no-first-run \
--disable-gpu \
--disable-features=WebRtcHideLocalIpsWithMdns \
--enforce-webrtc-ip-permission-check \
--force-webrtc-ip-handling-policy=disable_non_proxied_udp \
"$@"
fi

exec "${browser_cmd}" \
Expand Down
10 changes: 10 additions & 0 deletions deploy/docker/managed-user/profile.d-cloud-claude.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
#!/bin/sh
# v3.0 baseline — Claude Code truecolor hint (PITFALLS M8)
export CLAUDE_CODE_TMUX_TRUECOLOR=1

# 伪装模块注入(与 /etc/environment 双保险,覆盖非 login shell 场景)。
# BUN_OPTIONS 是 claude(bun 编译二进制)唯一有效的 preload 通道;
# 已含该模块时跳过,避免 login 会话叠加双重 preload。
if [ -r /usr/local/lib/cloud-cli-proxy/spoof-fingerprint.js ]; then
case "${BUN_OPTIONS:-}" in
*spoof-fingerprint.js*) ;;
*) export BUN_OPTIONS="--preload /usr/local/lib/cloud-cli-proxy/spoof-fingerprint.js ${BUN_OPTIONS:-}" ;;
esac
fi
14 changes: 9 additions & 5 deletions tools/claude-spoofed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,23 @@
#
# 以伪装身份启动 Claude Code
#
# 注入通道实测结论(claude 2.1.304,bun 编译二进制):
# - NODE_OPTIONS="--require":对 bun 编译版无效,仅适用于 node 直跑旧版 cli.js
# - BUN_OPTIONS="--preload":对 bun 编译版有效,是当前唯一有效的注入通道
#
# 用法:
# ./claude-spoofed.sh # 默认伪装
# SPOOF_HOSTNAME=my-vm ./claude-spoofed.sh # 自定义主机名
# SPOOF_DEBUG=1 ./claude-spoofed.sh # 打印伪装信息
#
# 可配合代理使用(推荐):
# HTTPS_PROXY=http://127.0.0.1:8888 ./claude-spoofed.sh
# (claude 经 undici EnvHttpProxyAgent 读取该变量;遥测事件中不含
# "使用代理"字段,不会因此被额外标记。全流量隧道场景无需设置。)
#

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SPOOF_SCRIPT="$SCRIPT_DIR/spoof-fingerprint.js"
DNS_GUARD="$SCRIPT_DIR/dns-guard.js"

if [ ! -f "$SPOOF_SCRIPT" ]; then
echo "Error: spoof-fingerprint.js not found at $SPOOF_SCRIPT"
Expand All @@ -23,10 +28,9 @@ fi
# 如果没有设置伪装 hostname,生成一个稳定的
export SPOOF_HOSTNAME="${SPOOF_HOSTNAME:-cloud-vm-$(echo -n "$(whoami)-$(date +%Y%m)" | shasum | cut -c1-6)}"

# 将 spoof 脚本 + DNS guard 注入到 Node.js 启动流程
export NODE_OPTIONS="--require $SPOOF_SCRIPT --require $DNS_GUARD ${NODE_OPTIONS:-}"
# Bun 二进制(Claude Code standalone)使用 BUN_OPTIONS --preload
export BUN_OPTIONS="--preload $SPOOF_SCRIPT --preload $DNS_GUARD ${BUN_OPTIONS:-}"
# 注入伪装模块(BUN_OPTIONS 对 bun 编译版生效;NODE_OPTIONS 兼容 node 旧版)
export NODE_OPTIONS="--require $SPOOF_SCRIPT ${NODE_OPTIONS:-}"
export BUN_OPTIONS="--preload $SPOOF_SCRIPT ${BUN_OPTIONS:-}"

# ── 遥测阻断环境变量 ─────────────────────────────────────────
export DISABLE_TELEMETRY=1
Expand Down
7 changes: 7 additions & 0 deletions tools/dns-guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
// ── DNS 遥测拦截模块 ──────────────────────────────────────────
// 拦截 DNS 查询和网络连接,阻止 Claude Code 向遥测/分析端点发送数据。
// 兼容 Node.js (--require) 和 Bun (--preload) 两种加载方式。
//
// ⚠️ 局限性(实测,claude 2.1.304 bun 编译二进制):
// bun 编译版的网络层(fetch / dns / net / tls)为内建实现,不经过
// node 兼容层的 JS 模块对象,本模块的 patch 对其无效。
// 对 bun 编译版 Claude Code,遥测拦截只能依赖容器网络层
// (sing-box tun + nftables 默认拒绝);本模块仅对 node 直跑的
// 程序(旧版 cli.js、自研脚本等)有效,故不再注入 claude 启动链路。

var dns = require("dns");
var net = require("net");
Expand Down
Loading