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/LICENSE +21 -0
- package/README.md +254 -0
- package/cac +1193 -0
- package/package.json +42 -0
- package/scripts/postinstall.js +25 -0
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
|
+
|