claude-cac 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cac ADDED
@@ -0,0 +1,1193 @@
1
+ #!/usr/bin/env bash
2
+ # cac — Claude Anti-fingerprint Cloak
3
+ # 由 build.sh 从 src/ 构建,勿直接编辑本文件
4
+ set -euo pipefail
5
+
6
+ CAC_DIR="$HOME/.cac"
7
+ ENVS_DIR="$CAC_DIR/envs"
8
+
9
+ # ━━━ utils.sh ━━━
10
+ # ── utils: 颜色、读写、UUID、proxy 解析 ───────────────────────
11
+
12
+ _read() { [[ -f "$1" ]] && tr -d '[:space:]' < "$1" || echo "${2:-}"; }
13
+ _bold() { printf '\033[1m%s\033[0m' "$*"; }
14
+ _green() { printf '\033[32m%s\033[0m' "$*"; }
15
+ _red() { printf '\033[31m%s\033[0m' "$*"; }
16
+ _yellow() { printf '\033[33m%s\033[0m' "$*"; }
17
+
18
+ _detect_os() {
19
+ case "$(uname -s)" in
20
+ Darwin) echo "macos" ;;
21
+ Linux) echo "linux" ;;
22
+ MINGW*|MSYS*|CYGWIN*) echo "windows" ;;
23
+ *) echo "unknown" ;;
24
+ esac
25
+ }
26
+
27
+ _new_uuid() { uuidgen | tr '[:lower:]' '[:upper:]'; }
28
+ _new_sid() { uuidgen | tr '[:upper:]' '[:lower:]'; }
29
+ _new_user_id() { python3 -c "import os; print(os.urandom(32).hex())"; }
30
+ _new_machine_id() { uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]'; }
31
+ _new_hostname() { echo "host-$(uuidgen | cut -d- -f1 | tr '[:upper:]' '[:lower:]')"; }
32
+ _new_mac() { od -An -tx1 -N5 /dev/urandom | awk '{printf "02:%s:%s:%s:%s:%s",$1,$2,$3,$4,$5}'; }
33
+
34
+ # host:port:user:pass → http://user:pass@host:port
35
+ # 或直接传入完整 URL(http://、https://、socks5://)
36
+ _parse_proxy() {
37
+ local raw="$1"
38
+ # 如果已经是完整 URL,直接返回
39
+ if [[ "$raw" =~ ^(http|https|socks5):// ]]; then
40
+ echo "$raw"
41
+ return
42
+ fi
43
+ # 否则解析 host:port:user:pass 格式
44
+ local host port user pass
45
+ host=$(echo "$raw" | cut -d: -f1)
46
+ port=$(echo "$raw" | cut -d: -f2)
47
+ user=$(echo "$raw" | cut -d: -f3)
48
+ pass=$(echo "$raw" | cut -d: -f4)
49
+ if [[ -z "$user" ]]; then
50
+ echo "http://${host}:${port}"
51
+ else
52
+ echo "http://${user}:${pass}@${host}:${port}"
53
+ fi
54
+ }
55
+
56
+ # socks5://user:pass@host:port → host:port
57
+ _proxy_host_port() {
58
+ echo "$1" | sed 's|.*@||' | sed 's|.*://||'
59
+ }
60
+
61
+ _proxy_reachable() {
62
+ local hp host port
63
+ hp=$(_proxy_host_port "$1")
64
+ host=$(echo "$hp" | cut -d: -f1)
65
+ port=$(echo "$hp" | cut -d: -f2)
66
+ (echo >/dev/tcp/"$host"/"$port") 2>/dev/null
67
+ }
68
+
69
+ _current_env() { _read "$CAC_DIR/current"; }
70
+ _env_dir() { echo "$ENVS_DIR/$1"; }
71
+
72
+ _require_setup() {
73
+ [[ -f "$CAC_DIR/real_claude" ]] || {
74
+ echo "错误:请先运行 'cac setup'" >&2; exit 1
75
+ }
76
+ }
77
+
78
+ _require_env() {
79
+ [[ -d "$ENVS_DIR/$1" ]] || {
80
+ echo "错误:环境 '$1' 不存在,用 'cac ls' 查看" >&2; exit 1
81
+ }
82
+ }
83
+
84
+ _find_real_claude() {
85
+ PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/bin" | tr '\n' ':') \
86
+ command -v claude 2>/dev/null || true
87
+ }
88
+
89
+ _update_statsig() {
90
+ local statsig="$HOME/.claude/statsig"
91
+ [[ -d "$statsig" ]] || return 0
92
+ for f in "$statsig"/statsig.stable_id.*; do
93
+ [[ -f "$f" ]] && printf '"%s"' "$1" > "$f"
94
+ done
95
+ }
96
+
97
+ _update_claude_json_user_id() {
98
+ local user_id="$1"
99
+ local claude_json="$HOME/.claude.json"
100
+ [[ -f "$claude_json" ]] || return 0
101
+ python3 - "$claude_json" "$user_id" << 'PYEOF'
102
+ import json, sys
103
+ fpath, uid = sys.argv[1], sys.argv[2]
104
+ with open(fpath) as f:
105
+ d = json.load(f)
106
+ d['userID'] = uid
107
+ with open(fpath, 'w') as f:
108
+ json.dump(d, f, indent=2, ensure_ascii=False)
109
+ PYEOF
110
+ [[ $? -eq 0 ]] || echo "警告:更新 ~/.claude.json userID 失败" >&2
111
+ }
112
+
113
+ # ━━━ dns_block.sh ━━━
114
+ # ── DNS 拦截 & 遥测域名屏蔽 ─────────────────────────────────────
115
+
116
+ # 需要拦截的遥测域名
117
+ TELEMETRY_DOMAINS=(
118
+ "statsig.anthropic.com"
119
+ "sentry.io"
120
+ "o1137031.ingest.sentry.io"
121
+ )
122
+
123
+ # 写入 HOSTALIASES 文件(备用层:gethostbyname 级别拦截)
124
+ # HOSTALIASES 格式为 hostname-to-hostname 映射(非 IP)
125
+ _write_blocked_hosts() {
126
+ local hosts_file="$CAC_DIR/blocked_hosts"
127
+ {
128
+ echo "# cac — blocked telemetry domains"
129
+ echo "# Generated by cac setup, used via HOSTALIASES"
130
+ for domain in "${TELEMETRY_DOMAINS[@]}"; do
131
+ printf '%s\tlocalhost\n' "$domain"
132
+ done
133
+ } > "$hosts_file"
134
+ }
135
+
136
+ # 写入 Node.js DNS guard 模块(核心层:dns.lookup / dns.resolve 级别拦截 + mTLS)
137
+ _write_dns_guard_js() {
138
+ local guard_file="$CAC_DIR/cac-dns-guard.js"
139
+ cat > "$guard_file" << 'DNSGUARD_EOF'
140
+ // ═══════════════════════════════════════════════════════════════
141
+ // cac-dns-guard.js
142
+ // NS 层级遥测域名拦截 | mTLS 证书注入 | fetch 防泄露
143
+ // 通过 NODE_OPTIONS="--require <this>" 注入到 Claude Code 进程
144
+ // ═══════════════════════════════════════════════════════════════
145
+ 'use strict';
146
+
147
+ var dns = require('dns');
148
+ var net = require('net');
149
+ var tls = require('tls');
150
+ var http = require('http');
151
+ var https = require('https');
152
+ var fs = require('fs');
153
+
154
+ // ─── 1. NS 层级遥测域名拦截 ──────────────────────────────────
155
+
156
+ var BLOCKED_DOMAINS = new Set([
157
+ 'statsig.anthropic.com',
158
+ 'sentry.io',
159
+ 'o1137031.ingest.sentry.io',
160
+ ]);
161
+
162
+ /**
163
+ * 检查域名是否在拦截名单中(含子域匹配)
164
+ * e.g. "foo.sentry.io" 会匹配 "sentry.io"
165
+ */
166
+ function isDomainBlocked(hostname) {
167
+ if (!hostname) return false;
168
+ var h = hostname.toLowerCase().replace(/\.$/,'');
169
+ if (BLOCKED_DOMAINS.has(h)) return true;
170
+ var parts = h.split('.');
171
+ for (var i = 1; i < parts.length - 1; i++) {
172
+ if (BLOCKED_DOMAINS.has(parts.slice(i).join('.'))) return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ function makeBlockedError(hostname, syscall) {
178
+ var msg = 'connect ECONNREFUSED (blocked by cac): ' + hostname;
179
+ var err = new Error(msg);
180
+ err.code = 'ECONNREFUSED';
181
+ err.errno = -111;
182
+ err.hostname = hostname;
183
+ err.syscall = syscall || 'connect';
184
+ return err;
185
+ }
186
+
187
+ // ── 1a. 拦截 dns.lookup ──
188
+ var _origLookup = dns.lookup;
189
+ dns.lookup = function cacLookup(hostname, options, callback) {
190
+ if (typeof options === 'function') { callback = options; options = {}; }
191
+ if (isDomainBlocked(hostname)) {
192
+ var err = makeBlockedError(hostname, 'getaddrinfo');
193
+ if (typeof callback === 'function') process.nextTick(function() { callback(err); });
194
+ return {};
195
+ }
196
+ return _origLookup.call(dns, hostname, options, callback);
197
+ };
198
+
199
+ // ── 1b. 拦截 dns.resolve / resolve4 / resolve6 ──
200
+ ['resolve','resolve4','resolve6'].forEach(function(method) {
201
+ var orig = dns[method];
202
+ if (!orig) return;
203
+ dns[method] = function(hostname) {
204
+ var args = Array.prototype.slice.call(arguments);
205
+ var cb = args[args.length - 1];
206
+ if (isDomainBlocked(hostname)) {
207
+ var err = makeBlockedError(hostname, 'query');
208
+ if (typeof cb === 'function') process.nextTick(function() { cb(err); });
209
+ return;
210
+ }
211
+ return orig.apply(dns, args);
212
+ };
213
+ });
214
+
215
+ // ── 1c. 拦截 dns.promises ──
216
+ if (dns.promises) {
217
+ var _origPLookup = dns.promises.lookup;
218
+ if (_origPLookup) {
219
+ dns.promises.lookup = function cacPromiseLookup(hostname, options) {
220
+ if (isDomainBlocked(hostname)) return Promise.reject(makeBlockedError(hostname, 'getaddrinfo'));
221
+ return _origPLookup.call(dns.promises, hostname, options);
222
+ };
223
+ }
224
+ ['resolve','resolve4','resolve6'].forEach(function(method) {
225
+ var orig = dns.promises[method];
226
+ if (!orig) return;
227
+ dns.promises[method] = function(hostname) {
228
+ if (isDomainBlocked(hostname)) return Promise.reject(makeBlockedError(hostname, 'query'));
229
+ return orig.apply(dns.promises, arguments);
230
+ };
231
+ });
232
+ }
233
+
234
+ // ── 1d. 网络层安全网:拦截 net.connect 到遥测域名 ──
235
+ var _origNetConnect = net.connect;
236
+ var _origNetCreateConn = net.createConnection;
237
+
238
+ function getHostFromArgs(args) {
239
+ if (typeof args[0] === 'object') return args[0].host || args[0].hostname || '';
240
+ return '';
241
+ }
242
+
243
+ function makeBlockedSocket(host) {
244
+ var sock = new net.Socket();
245
+ var err = makeBlockedError(host, 'connect');
246
+ process.nextTick(function() { sock.destroy(err); });
247
+ return sock;
248
+ }
249
+
250
+ net.connect = function cacNetConnect() {
251
+ var host = getHostFromArgs(arguments);
252
+ if (isDomainBlocked(host)) return makeBlockedSocket(host);
253
+ return _origNetConnect.apply(net, arguments);
254
+ };
255
+ net.createConnection = function cacNetCreateConnection() {
256
+ var host = getHostFromArgs(arguments);
257
+ if (isDomainBlocked(host)) return makeBlockedSocket(host);
258
+ return _origNetCreateConn.apply(net, arguments);
259
+ };
260
+
261
+
262
+ // ─── 2. mTLS 客户端证书注入 ──────────────────────────────────
263
+
264
+ var mtlsCert = process.env.CAC_MTLS_CERT;
265
+ var mtlsKey = process.env.CAC_MTLS_KEY;
266
+ var mtlsCa = process.env.CAC_MTLS_CA;
267
+ var proxyHostPort = process.env.CAC_PROXY_HOST || '';
268
+
269
+ if (mtlsCert && mtlsKey) {
270
+ var certData, keyData, caData;
271
+ try {
272
+ certData = fs.readFileSync(mtlsCert);
273
+ keyData = fs.readFileSync(mtlsKey);
274
+ if (mtlsCa) caData = fs.readFileSync(mtlsCa);
275
+ } catch(e) {
276
+ certData = null; keyData = null;
277
+ }
278
+
279
+ if (certData && keyData) {
280
+ // 仅对代理服务器连接注入 mTLS 证书
281
+ var proxyHost = proxyHostPort.split(':')[0];
282
+ var proxyPort = parseInt(proxyHostPort.split(':')[1], 10) || 0;
283
+
284
+ // 2a. 拦截 tls.connect,在代理连接时注入客户端证书
285
+ var _origTlsConnect = tls.connect;
286
+ tls.connect = function cacTlsConnect() {
287
+ // 规范化参数:tls.connect(options[, cb]) 或 tls.connect(port[, host][, options][, cb])
288
+ var args = Array.prototype.slice.call(arguments);
289
+ var options, callback;
290
+
291
+ if (typeof args[0] === 'object') {
292
+ options = args[0];
293
+ callback = (typeof args[1] === 'function') ? args[1] : undefined;
294
+ } else {
295
+ // tls.connect(port, host, options, cb) 形式
296
+ var port = args[0];
297
+ var host = (typeof args[1] === 'string') ? args[1] : 'localhost';
298
+ var optIdx = (typeof args[1] === 'string') ? 2 : 1;
299
+ options = (typeof args[optIdx] === 'object') ? args[optIdx] : {};
300
+ options.port = port;
301
+ options.host = host;
302
+ callback = args[args.length - 1];
303
+ if (typeof callback !== 'function') callback = undefined;
304
+ }
305
+
306
+ // 仅在连接代理时注入(精确匹配 host:port)
307
+ var targetHost = options.host || options.hostname || '';
308
+ var targetPort = options.port || 0;
309
+ if (proxyHost && targetHost === proxyHost &&
310
+ (proxyPort === 0 || targetPort === proxyPort)) {
311
+ if (!options.cert) {
312
+ options.cert = certData;
313
+ options.key = keyData;
314
+ if (caData) {
315
+ options.ca = options.ca
316
+ ? [].concat(options.ca, caData)
317
+ : [caData];
318
+ }
319
+ }
320
+ }
321
+
322
+ if (callback) return _origTlsConnect.call(tls, options, callback);
323
+ return _origTlsConnect.call(tls, options);
324
+ };
325
+
326
+ // 2b. 注入 CA 到 https.globalAgent(仅信任 CA,不注入客户端私钥)
327
+ if (caData && https.globalAgent && https.globalAgent.options) {
328
+ https.globalAgent.options.ca = https.globalAgent.options.ca
329
+ ? [].concat(https.globalAgent.options.ca, caData)
330
+ : [caData];
331
+ }
332
+ }
333
+ }
334
+
335
+
336
+ // ─── 3. fetch 遥测拦截补丁 ──────────────────────────────────
337
+ // Node.js 原生 fetch (undici) 不经过 dns.lookup,会绕过 DNS 拦截
338
+ // 策略:优先用 node-fetch(走 http/https 模块 → dns.lookup)
339
+ // 否则 wrap 原生 fetch 拦截遥测域名
340
+
341
+ (function patchFetch() {
342
+ if (typeof globalThis === 'undefined') return;
343
+
344
+ // 优先加载 node-fetch(基于 http/https 模块,天然走 dns.lookup 拦截链)
345
+ try {
346
+ var nodeFetch = require('node-fetch');
347
+ if (nodeFetch && typeof nodeFetch === 'function') {
348
+ globalThis.fetch = nodeFetch;
349
+ if (nodeFetch.Headers) globalThis.Headers = nodeFetch.Headers;
350
+ if (nodeFetch.Request) globalThis.Request = nodeFetch.Request;
351
+ if (nodeFetch.Response) globalThis.Response = nodeFetch.Response;
352
+ return;
353
+ }
354
+ } catch(e) {
355
+ // node-fetch 不可用
356
+ }
357
+
358
+ // 兜底:包装原生 fetch,至少确保遥测域名被拦截
359
+ if (typeof globalThis.fetch === 'function') {
360
+ var _origFetch = globalThis.fetch;
361
+ globalThis.fetch = function cacFetch(input, init) {
362
+ var hostname;
363
+ try {
364
+ var urlStr = typeof input === 'string' ? input :
365
+ (input && input.url) ? input.url : '';
366
+ if (urlStr) hostname = new URL(urlStr).hostname;
367
+ } catch(e) { /* ignore */ }
368
+
369
+ if (hostname && isDomainBlocked(hostname)) {
370
+ return Promise.reject(makeBlockedError(hostname, 'fetch'));
371
+ }
372
+ return _origFetch(input, init);
373
+ };
374
+ }
375
+ })();
376
+ DNSGUARD_EOF
377
+ chmod 644 "$guard_file"
378
+ }
379
+
380
+ # 验证 DNS 拦截是否生效
381
+ _check_dns_block() {
382
+ local domain="${1:-statsig.anthropic.com}"
383
+ local guard_file="$CAC_DIR/cac-dns-guard.js"
384
+
385
+ if [[ ! -f "$guard_file" ]]; then
386
+ echo "$(_red "✗") DNS guard 模块不存在"
387
+ return 1
388
+ fi
389
+
390
+ local result
391
+ result=$(node -e '
392
+ require(process.argv[1]);
393
+ var dns = require("dns");
394
+ dns.lookup(process.argv[2], function(err) {
395
+ process.stdout.write(err && (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") ? "BLOCKED" : "OPEN");
396
+ });
397
+ ' "$guard_file" "$domain" 2>/dev/null || echo "ERROR")
398
+
399
+ if [[ "$result" == "BLOCKED" ]]; then
400
+ echo "$(_green "✓") $domain 已拦截"
401
+ return 0
402
+ else
403
+ echo "$(_red "✗") $domain 未被拦截 ($result)"
404
+ return 1
405
+ fi
406
+ }
407
+
408
+ # ━━━ mtls.sh ━━━
409
+ # ── mTLS 客户端证书管理 ─────────────────────────────────────────
410
+
411
+ # 生成自签 CA(setup 时调用,仅生成一次)
412
+ _generate_ca_cert() {
413
+ local ca_dir="$CAC_DIR/ca"
414
+ local ca_key="$ca_dir/ca_key.pem"
415
+ local ca_cert="$ca_dir/ca_cert.pem"
416
+
417
+ if [[ -f "$ca_cert" ]] && [[ -f "$ca_key" ]]; then
418
+ echo " CA 证书已存在,跳过生成"
419
+ return 0
420
+ fi
421
+
422
+ mkdir -p "$ca_dir"
423
+
424
+ # 生成 CA 私钥(4096 位 RSA)
425
+ openssl genrsa -out "$ca_key" 4096 2>/dev/null || {
426
+ echo "错误:生成 CA 私钥失败" >&2; return 1
427
+ }
428
+ chmod 600 "$ca_key"
429
+
430
+ # 生成自签 CA 证书(有效期 10 年)
431
+ openssl req -new -x509 \
432
+ -key "$ca_key" \
433
+ -out "$ca_cert" \
434
+ -days 3650 \
435
+ -subj "/CN=cac-privacy-ca/O=cac/OU=mtls" \
436
+ -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
437
+ -addext "keyUsage=critical,keyCertSign,cRLSign" \
438
+ 2>/dev/null || {
439
+ echo "错误:生成 CA 证书失败" >&2; return 1
440
+ }
441
+ chmod 644 "$ca_cert"
442
+ }
443
+
444
+ # 为指定环境生成客户端证书(cac add 时调用)
445
+ _generate_client_cert() {
446
+ local name="$1"
447
+ local env_dir="$ENVS_DIR/$name"
448
+ local ca_key="$CAC_DIR/ca/ca_key.pem"
449
+ local ca_cert="$CAC_DIR/ca/ca_cert.pem"
450
+
451
+ if [[ ! -f "$ca_key" ]] || [[ ! -f "$ca_cert" ]]; then
452
+ echo " 警告:CA 证书不存在,跳过客户端证书生成" >&2
453
+ return 1
454
+ fi
455
+
456
+ local client_key="$env_dir/client_key.pem"
457
+ local client_csr="$env_dir/client_csr.pem"
458
+ local client_cert="$env_dir/client_cert.pem"
459
+
460
+ # 生成客户端私钥(2048 位 RSA)
461
+ openssl genrsa -out "$client_key" 2048 2>/dev/null || {
462
+ echo "错误:生成客户端私钥失败" >&2; return 1
463
+ }
464
+ chmod 600 "$client_key"
465
+
466
+ # 生成 CSR
467
+ openssl req -new \
468
+ -key "$client_key" \
469
+ -out "$client_csr" \
470
+ -subj "/CN=cac-client-${name}/O=cac/OU=env-${name}" \
471
+ 2>/dev/null || {
472
+ echo "错误:生成 CSR 失败" >&2; return 1
473
+ }
474
+
475
+ # 用 CA 签发客户端证书(有效期 1 年)
476
+ openssl x509 -req \
477
+ -in "$client_csr" \
478
+ -CA "$ca_cert" \
479
+ -CAkey "$ca_key" \
480
+ -CAcreateserial \
481
+ -out "$client_cert" \
482
+ -days 365 \
483
+ -extfile <(printf "keyUsage=critical,digitalSignature\nextendedKeyUsage=clientAuth") \
484
+ 2>/dev/null || {
485
+ echo "错误:签发客户端证书失败" >&2; return 1
486
+ }
487
+ chmod 644 "$client_cert"
488
+
489
+ # 清理 CSR(不再需要)
490
+ rm -f "$client_csr"
491
+ }
492
+
493
+ # 验证 mTLS 证书状态
494
+ _check_mtls() {
495
+ local env_dir="$1"
496
+ local ca_cert="$CAC_DIR/ca/ca_cert.pem"
497
+ local client_cert="$env_dir/client_cert.pem"
498
+ local client_key="$env_dir/client_key.pem"
499
+
500
+ # 检查 CA
501
+ if [[ ! -f "$ca_cert" ]]; then
502
+ echo "$(_red "✗") CA 证书不存在"
503
+ return 1
504
+ fi
505
+
506
+ # 检查客户端证书
507
+ if [[ ! -f "$client_cert" ]] || [[ ! -f "$client_key" ]]; then
508
+ echo "$(_yellow "⚠") 客户端证书不存在"
509
+ return 1
510
+ fi
511
+
512
+ # 验证证书链
513
+ if openssl verify -CAfile "$ca_cert" "$client_cert" >/dev/null 2>&1; then
514
+ # 检查证书有效期
515
+ local expiry
516
+ expiry=$(openssl x509 -in "$client_cert" -noout -enddate 2>/dev/null | cut -d= -f2)
517
+ local cn
518
+ cn=$(openssl x509 -in "$client_cert" -noout -subject 2>/dev/null | sed 's/.*CN *= *//')
519
+ echo "$(_green "✓") mTLS 证书有效 (CN=$cn, 到期: $expiry)"
520
+ return 0
521
+ else
522
+ echo "$(_red "✗") 证书链验证失败"
523
+ return 1
524
+ fi
525
+ }
526
+
527
+ # ━━━ templates.sh ━━━
528
+ # ── templates: 写入 wrapper 和 ioreg shim ──────────────────────
529
+
530
+ _write_wrapper() {
531
+ mkdir -p "$CAC_DIR/bin"
532
+ cat > "$CAC_DIR/bin/claude" << 'WRAPPER_EOF'
533
+ #!/usr/bin/env bash
534
+ set -euo pipefail
535
+
536
+ CAC_DIR="$HOME/.cac"
537
+ ENVS_DIR="$CAC_DIR/envs"
538
+
539
+ # cacstop 状态:直接透传
540
+ if [[ -f "$CAC_DIR/stopped" ]]; then
541
+ _real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude" 2>/dev/null || true)
542
+ [[ -x "$_real" ]] && exec "$_real" "$@"
543
+ echo "[cac] 错误:找不到真实 claude,运行 'cac setup'" >&2; exit 1
544
+ fi
545
+
546
+ # 读取当前环境
547
+ if [[ ! -f "$CAC_DIR/current" ]]; then
548
+ echo "[cac] 错误:未激活任何环境,运行 'cac <name>'" >&2; exit 1
549
+ fi
550
+ _name=$(tr -d '[:space:]' < "$CAC_DIR/current")
551
+ _env_dir="$ENVS_DIR/$_name"
552
+ [[ -d "$_env_dir" ]] || { echo "[cac] 错误:环境 '$_name' 不存在" >&2; exit 1; }
553
+
554
+ PROXY=$(tr -d '[:space:]' < "$_env_dir/proxy")
555
+
556
+ # pre-flight:代理连通性(纯 bash,无 fork)
557
+ _hp="${PROXY##*@}"; _hp="${_hp##*://}"
558
+ _host="${_hp%%:*}"
559
+ _port="${_hp##*:}"
560
+ if ! (echo >/dev/tcp/"$_host"/"$_port") 2>/dev/null; then
561
+ echo "[cac] 错误:[$_name] 代理 $_hp 不通,拒绝启动。" >&2
562
+ echo "[cac] 提示:运行 'cac check' 排查,或 'cacstop' 临时停用" >&2
563
+ exit 1
564
+ fi
565
+
566
+ # 注入 statsig stable_id
567
+ if [[ -f "$_env_dir/stable_id" ]]; then
568
+ _sid=$(tr -d '[:space:]' < "$_env_dir/stable_id")
569
+ for _f in "$HOME/.claude/statsig"/statsig.stable_id.*; do
570
+ [[ -f "$_f" ]] && printf '"%s"' "$_sid" > "$_f"
571
+ done
572
+ fi
573
+
574
+ # 注入环境变量 —— 代理
575
+ export HTTPS_PROXY="$PROXY" HTTP_PROXY="$PROXY" ALL_PROXY="$PROXY"
576
+ export NO_PROXY="localhost,127.0.0.1"
577
+ export PATH="$CAC_DIR/shim-bin:$PATH"
578
+
579
+ # ── 多层环境变量遥测保护 ──
580
+ # Layer 1: Claude Code 原生开关
581
+ export CLAUDE_CODE_ENABLE_TELEMETRY=
582
+ # Layer 2: 通用遥测标准 (https://consoledonottrack.com)
583
+ export DO_NOT_TRACK=1
584
+ # Layer 3: OpenTelemetry SDK 全面禁用
585
+ export OTEL_SDK_DISABLED=true
586
+ export OTEL_TRACES_EXPORTER=none
587
+ export OTEL_METRICS_EXPORTER=none
588
+ export OTEL_LOGS_EXPORTER=none
589
+ # Layer 4: Sentry DSN 置空,阻止错误上报
590
+ export SENTRY_DSN=
591
+ # Layer 5: Claude Code 特有开关
592
+ export DISABLE_ERROR_REPORTING=1
593
+ export DISABLE_BUG_COMMAND=1
594
+ export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
595
+ # Layer 6: 其他已知遥测标志
596
+ export TELEMETRY_DISABLED=1
597
+ export DISABLE_TELEMETRY=1
598
+
599
+ # 清除第三方 API 配置,强制走 OAuth 官方端点
600
+ unset ANTHROPIC_BASE_URL
601
+ unset ANTHROPIC_AUTH_TOKEN
602
+ unset ANTHROPIC_API_KEY
603
+
604
+ # ── NS 层级 DNS 拦截 ──
605
+ if [[ -f "$CAC_DIR/cac-dns-guard.js" ]]; then
606
+ case "${NODE_OPTIONS:-}" in
607
+ *cac-dns-guard.js*) ;; # 已注入,跳过
608
+ *) export NODE_OPTIONS="${NODE_OPTIONS:-} --require $CAC_DIR/cac-dns-guard.js" ;;
609
+ esac
610
+ fi
611
+ # 备用层:HOSTALIASES(gethostbyname 级别)
612
+ [[ -f "$CAC_DIR/blocked_hosts" ]] && export HOSTALIASES="$CAC_DIR/blocked_hosts"
613
+
614
+ # ── mTLS 客户端证书 ──
615
+ if [[ -f "$_env_dir/client_cert.pem" ]] && [[ -f "$_env_dir/client_key.pem" ]]; then
616
+ export CAC_MTLS_CERT="$_env_dir/client_cert.pem"
617
+ export CAC_MTLS_KEY="$_env_dir/client_key.pem"
618
+ [[ -f "$CAC_DIR/ca/ca_cert.pem" ]] && {
619
+ export CAC_MTLS_CA="$CAC_DIR/ca/ca_cert.pem"
620
+ export NODE_EXTRA_CA_CERTS="$CAC_DIR/ca/ca_cert.pem"
621
+ }
622
+ export CAC_PROXY_HOST="$_hp"
623
+ fi
624
+
625
+ [[ -f "$_env_dir/tz" ]] && export TZ=$(tr -d '[:space:]' < "$_env_dir/tz")
626
+ [[ -f "$_env_dir/lang" ]] && export LANG=$(tr -d '[:space:]' < "$_env_dir/lang")
627
+ [[ -f "$_env_dir/hostname" ]] && export HOSTNAME=$(tr -d '[:space:]' < "$_env_dir/hostname")
628
+
629
+ # 执行真实 claude
630
+ _real=$(tr -d '[:space:]' < "$CAC_DIR/real_claude")
631
+ [[ -x "$_real" ]] || { echo "[cac] 错误:$_real 不可执行,运行 'cac setup'" >&2; exit 1; }
632
+ exec "$_real" "$@"
633
+ WRAPPER_EOF
634
+ chmod +x "$CAC_DIR/bin/claude"
635
+ }
636
+
637
+ _write_ioreg_shim() {
638
+ mkdir -p "$CAC_DIR/shim-bin"
639
+ cat > "$CAC_DIR/shim-bin/ioreg" << 'IOREG_EOF'
640
+ #!/usr/bin/env bash
641
+ CAC_DIR="$HOME/.cac"
642
+
643
+ # 非目标调用:透传真实 ioreg
644
+ if ! echo "$*" | grep -q "IOPlatformExpertDevice"; then
645
+ _real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
646
+ command -v ioreg 2>/dev/null || true)
647
+ [[ -n "$_real" ]] && exec "$_real" "$@"
648
+ exit 0
649
+ fi
650
+
651
+ # 读取当前环境的 UUID
652
+ _uuid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/uuid"
653
+ if [[ ! -f "$_uuid_file" ]]; then
654
+ _real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') \
655
+ command -v ioreg 2>/dev/null || true)
656
+ [[ -n "$_real" ]] && exec "$_real" "$@"
657
+ exit 0
658
+ fi
659
+ FAKE_UUID=$(tr -d '[:space:]' < "$_uuid_file")
660
+
661
+ cat <<EOF
662
+ +-o Root <class IORegistryEntry, id 0x100000100, retain 11>
663
+ +-o J314sAP <class IOPlatformExpertDevice, id 0x100000101, registered, matched, active, busy 0 (0 ms), retain 28>
664
+ {
665
+ "IOPlatformUUID" = "$FAKE_UUID"
666
+ "IOPlatformSerialNumber" = "C02FAKE000001"
667
+ "manufacturer" = "Apple Inc."
668
+ "model" = "Mac14,5"
669
+ }
670
+ EOF
671
+ IOREG_EOF
672
+ chmod +x "$CAC_DIR/shim-bin/ioreg"
673
+ }
674
+
675
+ _write_machine_id_shim() {
676
+ mkdir -p "$CAC_DIR/shim-bin"
677
+ cat > "$CAC_DIR/shim-bin/cat" << 'CAT_EOF'
678
+ #!/usr/bin/env bash
679
+ CAC_DIR="$HOME/.cac"
680
+
681
+ # 先获取真实 cat 路径(避免递归调用自身)
682
+ _real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v cat 2>/dev/null || true)
683
+
684
+ # 拦截 /etc/machine-id 和 /var/lib/dbus/machine-id
685
+ if [[ "$1" == "/etc/machine-id" ]] || [[ "$1" == "/var/lib/dbus/machine-id" ]]; then
686
+ _mid_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/machine_id"
687
+ if [[ -f "$_mid_file" ]] && [[ -n "$_real" ]]; then
688
+ exec "$_real" "$_mid_file"
689
+ fi
690
+ fi
691
+
692
+ # 非目标调用:透传真实 cat
693
+ [[ -n "$_real" ]] && exec "$_real" "$@"
694
+ exit 1
695
+ CAT_EOF
696
+ chmod +x "$CAC_DIR/shim-bin/cat"
697
+ }
698
+
699
+ _write_hostname_shim() {
700
+ mkdir -p "$CAC_DIR/shim-bin"
701
+ cat > "$CAC_DIR/shim-bin/hostname" << 'HOSTNAME_EOF'
702
+ #!/usr/bin/env bash
703
+ CAC_DIR="$HOME/.cac"
704
+
705
+ # 读取伪造的 hostname
706
+ _hn_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/hostname"
707
+ if [[ -f "$_hn_file" ]]; then
708
+ cat "$_hn_file"
709
+ exit 0
710
+ fi
711
+
712
+ # 透传真实 hostname
713
+ _real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v hostname 2>/dev/null || true)
714
+ [[ -n "$_real" ]] && exec "$_real" "$@"
715
+ exit 1
716
+ HOSTNAME_EOF
717
+ chmod +x "$CAC_DIR/shim-bin/hostname"
718
+ }
719
+
720
+ _write_ifconfig_shim() {
721
+ mkdir -p "$CAC_DIR/shim-bin"
722
+ cat > "$CAC_DIR/shim-bin/ifconfig" << 'IFCONFIG_EOF'
723
+ #!/usr/bin/env bash
724
+ CAC_DIR="$HOME/.cac"
725
+
726
+ _real=$(PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "$CAC_DIR/shim-bin" | tr '\n' ':') command -v ifconfig 2>/dev/null || true)
727
+
728
+ _mac_file="$CAC_DIR/envs/$(tr -d '[:space:]' < "$CAC_DIR/current" 2>/dev/null)/mac_address"
729
+ if [[ -f "$_mac_file" ]] && [[ -n "$_real" ]]; then
730
+ FAKE_MAC=$(tr -d '[:space:]' < "$_mac_file")
731
+ "$_real" "$@" | sed "s/ether [0-9a-f:]\{17\}/ether $FAKE_MAC/g" && exit 0
732
+ fi
733
+
734
+ [[ -n "$_real" ]] && exec "$_real" "$@"
735
+ exit 1
736
+ IFCONFIG_EOF
737
+ chmod +x "$CAC_DIR/shim-bin/ifconfig"
738
+ }
739
+
740
+ # ━━━ cmd_setup.sh ━━━
741
+ # ── cmd: setup ─────────────────────────────────────────────────
742
+
743
+ cmd_setup() {
744
+ echo "=== cac setup ==="
745
+
746
+ local real_claude
747
+ real_claude=$(_find_real_claude)
748
+ if [[ -z "$real_claude" ]]; then
749
+ echo "错误:找不到 claude 命令,请先安装 Claude CLI" >&2
750
+ echo " npm install -g @anthropic-ai/claude-code" >&2
751
+ exit 1
752
+ fi
753
+ echo " 真实 claude:$real_claude"
754
+
755
+ mkdir -p "$ENVS_DIR"
756
+ echo "$real_claude" > "$CAC_DIR/real_claude"
757
+
758
+ local os; os=$(_detect_os)
759
+ _write_wrapper
760
+ _write_hostname_shim
761
+ _write_ifconfig_shim
762
+
763
+ if [[ "$os" == "macos" ]]; then
764
+ _write_ioreg_shim
765
+ echo " ✓ ioreg shim → $CAC_DIR/shim-bin/ioreg"
766
+ elif [[ "$os" == "linux" ]]; then
767
+ _write_machine_id_shim
768
+ echo " ✓ machine-id shim → $CAC_DIR/shim-bin/cat"
769
+ fi
770
+
771
+ echo " ✓ wrapper → $CAC_DIR/bin/claude"
772
+ echo " ✓ hostname shim → $CAC_DIR/shim-bin/hostname"
773
+ echo " ✓ ifconfig shim → $CAC_DIR/shim-bin/ifconfig"
774
+
775
+ # DNS guard (NS 层级遥测拦截 + DoH)
776
+ _write_dns_guard_js
777
+ _write_blocked_hosts
778
+ echo " ✓ DNS guard → $CAC_DIR/cac-dns-guard.js"
779
+ echo " ✓ blocked hosts → $CAC_DIR/blocked_hosts"
780
+
781
+ # mTLS CA 证书
782
+ _generate_ca_cert
783
+ echo " ✓ mTLS CA → $CAC_DIR/ca/ca_cert.pem"
784
+
785
+ echo
786
+ echo "── 下一步 ──────────────────────────────────────────────"
787
+ echo "1. 将以下两行加到 ~/.zshrc 最前面:"
788
+ echo
789
+ echo " export PATH=\"\$HOME/bin:\$PATH\" # cac 命令"
790
+ echo " export PATH=\"$CAC_DIR/bin:\$PATH\" # claude wrapper"
791
+ echo
792
+ echo "2. source ~/.zshrc"
793
+ echo
794
+ echo "3. 添加第一个代理环境:"
795
+ echo " cac add <名字> <host:port:user:pass>"
796
+ }
797
+
798
+ # ━━━ cmd_env.sh ━━━
799
+ # ── cmd: add / switch / ls ─────────────────────────────────────
800
+
801
+ cmd_add() {
802
+ _require_setup
803
+ if [[ $# -lt 2 ]]; then
804
+ echo "用法:cac add <名字> <host:port:user:pass>" >&2
805
+ echo "示例:cac add us1 1.2.3.4:1080:username:password" >&2
806
+ exit 1
807
+ fi
808
+
809
+ local name="$1" raw_proxy="$2"
810
+ local env_dir="$ENVS_DIR/$name"
811
+
812
+ if [[ -d "$env_dir" ]]; then
813
+ echo "错误:环境 '$name' 已存在,用 'cac ls' 查看" >&2
814
+ exit 1
815
+ fi
816
+
817
+ local proxy
818
+ proxy=$(_parse_proxy "$raw_proxy")
819
+
820
+ echo "即将创建环境:$(_bold "$name")"
821
+ echo " 代理:$proxy"
822
+ echo
823
+
824
+ printf " 检测代理 ... "
825
+ if _proxy_reachable "$proxy"; then
826
+ echo "$(_green "✓ 可达")"
827
+ else
828
+ echo "$(_red "✗ 不通")"
829
+ echo " 警告:代理当前不可达(代理客户端可能未启动)"
830
+ fi
831
+
832
+ # 自动检测出口 IP 的时区和语言
833
+ printf " 检测时区 ... "
834
+ local exit_ip tz lang
835
+ exit_ip=$(curl -s --proxy "$proxy" --connect-timeout 8 https://api.ipify.org 2>/dev/null || true)
836
+ if [[ -n "$exit_ip" ]]; then
837
+ local ip_info
838
+ ip_info=$(curl -s --connect-timeout 8 "http://ip-api.com/json/${exit_ip}?fields=timezone,countryCode" 2>/dev/null || true)
839
+ tz=$(echo "$ip_info" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('timezone',''))" 2>/dev/null || true)
840
+ country=$(echo "$ip_info" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('countryCode',''))" 2>/dev/null || true)
841
+ [[ -z "$tz" ]] && tz="America/New_York"
842
+ [[ "$country" == "US" || "$country" == "GB" || "$country" == "AU" || "$country" == "CA" ]] && lang="en_US.UTF-8" || lang="en_US.UTF-8"
843
+ echo "$(_green "✓ $tz")"
844
+ else
845
+ tz="America/New_York"
846
+ lang="en_US.UTF-8"
847
+ echo "$(_yellow "⚠ 获取失败,默认 $tz")"
848
+ fi
849
+ echo
850
+
851
+ printf "确认创建?[yes/N] "
852
+ read -r confirm
853
+ [[ "$confirm" == "yes" ]] || { echo "已取消。"; exit 0; }
854
+
855
+ mkdir -p "$env_dir"
856
+ echo "$proxy" > "$env_dir/proxy"
857
+ echo "$(_new_uuid)" > "$env_dir/uuid"
858
+ echo "$(_new_sid)" > "$env_dir/stable_id"
859
+ echo "$(_new_user_id)" > "$env_dir/user_id"
860
+ echo "$(_new_machine_id)" > "$env_dir/machine_id"
861
+ echo "$(_new_hostname)" > "$env_dir/hostname"
862
+ echo "$(_new_mac)" > "$env_dir/mac_address"
863
+ echo "$tz" > "$env_dir/tz"
864
+ echo "$lang" > "$env_dir/lang"
865
+
866
+ # 生成 mTLS 客户端证书
867
+ printf " 生成 mTLS 证书 ... "
868
+ if _generate_client_cert "$name"; then
869
+ echo "$(_green "✓")"
870
+ else
871
+ echo "$(_yellow "⚠ 跳过")"
872
+ fi
873
+
874
+ echo
875
+ echo "$(_green "✓") 环境 '$(_bold "$name")' 已创建"
876
+ echo " UUID :$(cat "$env_dir/uuid")"
877
+ echo " stable_id:$(cat "$env_dir/stable_id")"
878
+ echo " mTLS :$([ -f "$env_dir/client_cert.pem" ] && echo "已配置" || echo "未配置")"
879
+ echo " TZ :$tz"
880
+ echo " LANG :$lang"
881
+ echo
882
+ echo "切换到该环境:cac $name"
883
+ }
884
+
885
+ cmd_switch() {
886
+ _require_setup
887
+ local name="$1"
888
+ _require_env "$name"
889
+
890
+ local proxy; proxy=$(_read "$ENVS_DIR/$name/proxy")
891
+
892
+ printf "检测 [%s] 代理 ... " "$name"
893
+ if _proxy_reachable "$proxy"; then
894
+ echo "$(_green "✓ 可达")"
895
+ else
896
+ echo "$(_yellow "⚠ 不通")"
897
+ echo "警告:代理不可达,仍切换(启动 claude 时会拦截)"
898
+ fi
899
+
900
+ echo "$name" > "$CAC_DIR/current"
901
+ rm -f "$CAC_DIR/stopped"
902
+
903
+ _update_statsig "$(_read "$ENVS_DIR/$name/stable_id")"
904
+ _update_claude_json_user_id "$(_read "$ENVS_DIR/$name/user_id")"
905
+
906
+ echo "$(_green "✓") 已切换到 $(_bold "$name")"
907
+ }
908
+
909
+ cmd_ls() {
910
+ _require_setup
911
+
912
+ if [[ ! -d "$ENVS_DIR" ]] || [[ -z "$(ls -A "$ENVS_DIR" 2>/dev/null)" ]]; then
913
+ echo "(暂无环境,用 'cac add <名字> <proxy>' 添加)"
914
+ return
915
+ fi
916
+
917
+ local current; current=$(_current_env)
918
+ local stopped_tag=""
919
+ [[ -f "$CAC_DIR/stopped" ]] && stopped_tag=" $(_red "[stopped]")"
920
+
921
+ for env_dir in "$ENVS_DIR"/*/; do
922
+ local name; name=$(basename "$env_dir")
923
+ local proxy; proxy=$(_read "$env_dir/proxy" "(未配置)")
924
+ if [[ "$name" == "$current" ]]; then
925
+ printf " %s %s%s\n" "$(_green "▶")" "$(_bold "$name")" "$stopped_tag"
926
+ printf " %s\n" "$proxy"
927
+ else
928
+ printf " %s\n" "$name"
929
+ printf " %s\n" "$proxy"
930
+ fi
931
+ done
932
+ }
933
+
934
+ # ━━━ cmd_check.sh ━━━
935
+ # ── cmd: check ─────────────────────────────────────────────────
936
+
937
+ # 检测本地代理软件冲突(Clash / Surge / Shadowrocket 等)
938
+ _check_proxy_conflict() {
939
+ local proxy="$1" proxy_ip="$2"
940
+ local proxy_hp; proxy_hp=$(_proxy_host_port "$proxy")
941
+ local proxy_host="${proxy_hp%%:*}"
942
+
943
+ local os; os=$(_detect_os)
944
+ local conflicts=()
945
+
946
+ # ── 1. 检测 TUN 模式进程 ──
947
+ local tun_procs="clash|mihomo|sing-box|surge|shadowrocket|v2ray|xray|hysteria|tuic|nekoray"
948
+ local running
949
+ if [[ "$os" == "macos" ]]; then
950
+ running=$(ps aux 2>/dev/null | grep -iE "$tun_procs" | grep -v grep || true)
951
+ else
952
+ running=$(ps -eo comm 2>/dev/null | grep -iE "$tun_procs" || true)
953
+ fi
954
+
955
+ if [[ -n "$running" ]]; then
956
+ local proc_names
957
+ proc_names=$(echo "$running" | grep -ioE "$tun_procs" | sort -u | head -3)
958
+ if [[ -n "$proc_names" ]]; then
959
+ conflicts+=("检测到本地代理进程: $(echo "$proc_names" | tr '\n' ' ')")
960
+ fi
961
+ fi
962
+
963
+ # ── 2. 检测 TUN 网卡(utun / tun)──
964
+ if [[ "$os" == "macos" ]]; then
965
+ local tun_count
966
+ tun_count=$(ifconfig 2>/dev/null | grep -cE '^utun[0-9]+' || echo 0)
967
+ if [[ "$tun_count" -gt 3 ]]; then
968
+ conflicts+=("检测到多个 TUN 网卡 (${tun_count} 个),可能有代理软件启用了 TUN 模式")
969
+ fi
970
+ elif [[ "$os" == "linux" ]]; then
971
+ if ip link show tun0 >/dev/null 2>&1; then
972
+ conflicts+=("检测到 tun0 网卡,可能有代理软件启用了 TUN 模式")
973
+ fi
974
+ fi
975
+
976
+ # ── 3. 检测系统代理是否指向本机(macOS)──
977
+ if [[ "$os" == "macos" ]]; then
978
+ local net_service
979
+ net_service=$(networksetup -listallnetworkservices 2>/dev/null | grep -iE 'Wi-Fi|Ethernet|以太网' | head -1 || true)
980
+ if [[ -n "$net_service" ]]; then
981
+ local sys_http_proxy
982
+ sys_http_proxy=$(networksetup -getwebproxy "$net_service" 2>/dev/null || true)
983
+ if echo "$sys_http_proxy" | grep -qi "Enabled: Yes"; then
984
+ local sys_host sys_port
985
+ sys_host=$(echo "$sys_http_proxy" | awk '/^Server:/{print $2}')
986
+ sys_port=$(echo "$sys_http_proxy" | awk '/^Port:/{print $2}')
987
+ [[ -n "$sys_host" ]] && conflicts+=("系统 HTTP 代理已开启: ${sys_host}:${sys_port}")
988
+ fi
989
+ fi
990
+ fi
991
+
992
+ # ── 4. 检测代理流量是否被二次转发(复用已获取的 proxy_ip)──
993
+ local direct_ip
994
+ direct_ip=$(curl -s --noproxy '*' --connect-timeout 5 https://api.ipify.org 2>/dev/null || true)
995
+ if [[ -n "$direct_ip" ]] && [[ -n "$proxy_ip" ]] && [[ "$direct_ip" == "$proxy_ip" ]]; then
996
+ conflicts+=("代理出口 IP ($proxy_ip) 与直连出口 IP 相同,代理流量可能被本地软件拦截")
997
+ fi
998
+
999
+ # ── 输出结果 ──
1000
+ if [[ ${#conflicts[@]} -eq 0 ]]; then
1001
+ echo "$(_green "✓") 未检测到本地代理软件冲突"
1002
+ return 0
1003
+ fi
1004
+
1005
+ echo "$(_yellow "⚠ 检测到可能的代理冲突"):"
1006
+ for msg in "${conflicts[@]}"; do
1007
+ echo " $(_yellow "•") $msg"
1008
+ done
1009
+ echo
1010
+ echo " $(_bold "解决方法"):在本地代理软件中为 cac 代理服务器 IP 添加 DIRECT 规则"
1011
+ echo " 代理服务器:$(_bold "$proxy_host")"
1012
+ echo
1013
+ if [[ "$os" == "macos" ]]; then
1014
+ echo " Clash 示例(添加到规则列表最前面):"
1015
+ echo " - IP-CIDR,${proxy_host}/32,DIRECT"
1016
+ fi
1017
+ return 1
1018
+ }
1019
+
1020
+ cmd_check() {
1021
+ _require_setup
1022
+
1023
+ local current; current=$(_current_env)
1024
+
1025
+ if [[ -f "$CAC_DIR/stopped" ]]; then
1026
+ echo "$(_yellow "⚠ cac 已停用(cac stop)") — claude 裸跑中"
1027
+ echo " 恢复:cac ${current:-<name>}"
1028
+ return
1029
+ fi
1030
+
1031
+ if [[ -z "$current" ]]; then
1032
+ echo "错误:未激活任何环境,运行 'cac <name>'" >&2; exit 1
1033
+ fi
1034
+
1035
+ local env_dir="$ENVS_DIR/$current"
1036
+ local proxy; proxy=$(_read "$env_dir/proxy")
1037
+
1038
+ echo "当前环境:$(_bold "$current")"
1039
+ echo " 代理 :$proxy"
1040
+ echo " UUID :$(_read "$env_dir/uuid")"
1041
+ echo " stable_id :$(_read "$env_dir/stable_id")"
1042
+ echo " user_id :$(_read "$env_dir/user_id" "(旧环境,无此字段)")"
1043
+ echo " TZ :$(_read "$env_dir/tz" "(未设置)")"
1044
+ echo " LANG :$(_read "$env_dir/lang" "(未设置)")"
1045
+ echo
1046
+
1047
+ # ── 网络连通性 ──
1048
+ printf " TCP 连通 ... "
1049
+ if ! _proxy_reachable "$proxy"; then
1050
+ echo "$(_red "✗ 不通")"; return
1051
+ fi
1052
+ echo "$(_green "✓")"
1053
+
1054
+ printf " 出口 IP ... "
1055
+ local proxy_ip
1056
+ proxy_ip=$(curl -s --proxy "$proxy" \
1057
+ --connect-timeout 8 https://api.ipify.org 2>/dev/null || true)
1058
+ if [[ -n "$proxy_ip" ]]; then
1059
+ echo "$(_green "$proxy_ip")"
1060
+ else
1061
+ echo "$(_yellow "获取失败")"
1062
+ fi
1063
+
1064
+ # ── 本地代理冲突检测(复用已获取的 proxy_ip)──
1065
+ echo
1066
+ echo "── 冲突检测 ────────────────────────────────────────────"
1067
+ printf " 代理冲突 ... "
1068
+ _check_proxy_conflict "$proxy" "$proxy_ip"
1069
+
1070
+ echo
1071
+ echo "── 安全防护状态 ──────────────────────────────────────"
1072
+
1073
+ # ── NS 层级 DNS 拦截 ──
1074
+ printf " DNS 拦截 ... "
1075
+ _check_dns_block "statsig.anthropic.com"
1076
+
1077
+ # ── 多层环境变量保护(单次读取 wrapper 文件)──
1078
+ echo " 环境变量保护:"
1079
+ local wrapper_file="$CAC_DIR/bin/claude"
1080
+ local wrapper_content=""
1081
+ [[ -f "$wrapper_file" ]] && wrapper_content=$(<"$wrapper_file")
1082
+ local env_vars=(
1083
+ "CLAUDE_CODE_ENABLE_TELEMETRY"
1084
+ "DO_NOT_TRACK"
1085
+ "OTEL_SDK_DISABLED"
1086
+ "OTEL_TRACES_EXPORTER"
1087
+ "OTEL_METRICS_EXPORTER"
1088
+ "OTEL_LOGS_EXPORTER"
1089
+ "SENTRY_DSN"
1090
+ "DISABLE_ERROR_REPORTING"
1091
+ "DISABLE_BUG_COMMAND"
1092
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
1093
+ "TELEMETRY_DISABLED"
1094
+ "DISABLE_TELEMETRY"
1095
+ )
1096
+ for var in "${env_vars[@]}"; do
1097
+ printf " %-32s" "$var"
1098
+ if [[ "$wrapper_content" == *"$var"* ]]; then
1099
+ echo "$(_green "✓ 已配置")"
1100
+ else
1101
+ echo "$(_red "✗ 未找到")"
1102
+ fi
1103
+ done
1104
+
1105
+ # ── mTLS 证书 ──
1106
+ printf " mTLS 认证 ... "
1107
+ _check_mtls "$env_dir"
1108
+ }
1109
+
1110
+ # ━━━ cmd_stop.sh ━━━
1111
+ # ── cmd: stop / continue ───────────────────────────────────────
1112
+
1113
+ cmd_stop() {
1114
+ touch "$CAC_DIR/stopped"
1115
+ local current; current=$(_current_env)
1116
+ echo "$(_yellow "⚠ cac 已停用") — claude 将裸跑(无代理、无伪装)"
1117
+ echo " 恢复:cac -c"
1118
+ }
1119
+
1120
+ cmd_continue() {
1121
+ if [[ ! -f "$CAC_DIR/stopped" ]]; then
1122
+ echo "cac 当前未停用,无需恢复"
1123
+ return
1124
+ fi
1125
+
1126
+ local current; current=$(_current_env)
1127
+ if [[ -z "$current" ]]; then
1128
+ echo "错误:没有已激活的环境,运行 'cac <name>'" >&2; exit 1
1129
+ fi
1130
+
1131
+ rm -f "$CAC_DIR/stopped"
1132
+ echo "$(_green "✓") cac 已恢复 — 当前环境:$(_bold "$current")"
1133
+ }
1134
+
1135
+ # ━━━ cmd_help.sh ━━━
1136
+ # ── cmd: help ──────────────────────────────────────────────────
1137
+
1138
+ cmd_help() {
1139
+ cat <<EOF
1140
+ $(_bold "cac") — Claude Anti-fingerprint Cloak
1141
+
1142
+ $(_bold "用法:")
1143
+ cac setup 首次安装
1144
+ cac add <名字> <host:port:u:p> 添加新环境(需要 yes 确认)
1145
+ cac <名字> 切换到指定环境
1146
+ cac ls 列出所有环境
1147
+ cac check 核查当前环境(代理 + 安全防护)
1148
+ cac stop 临时停用,claude 裸跑
1149
+ cac -c 恢复停用
1150
+
1151
+ $(_bold "代理格式:")
1152
+ host:port:user:pass 带认证的 SOCKS5
1153
+ host:port 无认证的 SOCKS5
1154
+
1155
+ $(_bold "安全防护:")
1156
+ NS 层级 DNS 拦截 拦截 statsig.anthropic.com 等遥测域名
1157
+ fetch 遥测拦截 替换原生 fetch,防止绕过 DNS 拦截
1158
+ 多层环境变量保护 DO_NOT_TRACK / OTEL_SDK_DISABLED 等 12 层遥测阻断
1159
+ mTLS 客户端证书 自签 CA + 客户端证书 + https.globalAgent 注入
1160
+
1161
+ $(_bold "示例:")
1162
+ cac add us1 1.2.3.4:1080:username:password
1163
+ cac us1
1164
+ cac check
1165
+ cac stop
1166
+
1167
+ $(_bold "文件目录:")
1168
+ ~/.cac/bin/claude wrapper(拦截所有 claude 调用)
1169
+ ~/.cac/shim-bin/ ioreg / hostname / ifconfig shim
1170
+ ~/.cac/cac-dns-guard.js NS 层级 DNS 拦截 + DoH + mTLS 注入模块
1171
+ ~/.cac/blocked_hosts HOSTALIASES 遥测域名拦截
1172
+ ~/.cac/ca/ mTLS 自签 CA 证书
1173
+ ~/.cac/current 当前激活的环境名
1174
+ ~/.cac/envs/<name>/ 各环境:proxy / uuid / stable_id / client_cert
1175
+ EOF
1176
+ }
1177
+
1178
+ # ━━━ main.sh ━━━
1179
+ # ── 入口:分发命令 ──────────────────────────────────────────────
1180
+
1181
+ [[ $# -eq 0 ]] && { cmd_help; exit 0; }
1182
+
1183
+ case "$1" in
1184
+ setup) cmd_setup ;;
1185
+ add) cmd_add "${@:2}" ;;
1186
+ ls|list) cmd_ls ;;
1187
+ check) cmd_check ;;
1188
+ stop) cmd_stop ;;
1189
+ -c) cmd_continue ;;
1190
+ help|--help|-h) cmd_help ;;
1191
+ *) cmd_switch "$1" ;;
1192
+ esac
1193
+