-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathScript.js
More file actions
1926 lines (1818 loc) · 172 KB
/
Script.js
File metadata and controls
1926 lines (1818 loc) · 172 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* Clash-Script 全局扩展脚本 · 基于哨兵标记的规则幂等清理与注入(Firefly 精确豁免版)v260604
*
* ══════════════════════════ ░░ 脚本自述 ░░ ══════════════════════════
*
* Script.js 路径(Windows):
* %APPDATA%\io.github.clash-verge-rev.clash-verge-rev\profiles
* C:\Users\Administrator\AppData\Roaming\io.github.clash-verge-rev.clash-verge-rev\profiles
*
* 默认模式:拦截优先 + Firefly 精确例外放行
* - ENABLE_FIREFLY = true:精确放行 Firefly AI 请求,保留其他 Adobe 遥测/激活域名的拦截行为。
* - Firefly 依赖端点必要妥协:auth / cc-api / lcs 等端点因 Firefly 功能依赖而一并放行;
* 最后防线为 AdobeGCClient.exe → REJECT-DROP(需 ENABLE_PROCESS_RULE=true + TUN 模式 + 管理员权限),另还需配置文件的 find-process-mode 参数启用获取进程信息。
* 注意:Creative Cloud.exe / CCXProcess.exe / CoreSync.exe 等进程同样访问这些端点,
* 进程规则仅覆盖 AdobeGCClient.exe,其余进程因依赖链考量予以必要豁免(原因见正文 Firefly 必要妥协,详见 adobeSharedDeps 注释及设计取舍)。
* - 适用场景:需要使用 Photoshop 生成式填充、Firefly 等 Adobe AI 功能。
*
* ══════════════════════════ ░░ 功能概览 ░░ ══════════════════════════
*
* - 智能识别代理策略组(多级降级策略:优选组(三轮:关键词→正则→类型)→ 兜底组 → 容错选取 → 全部失败则中止注入)
* - 注入拦截规则(Adobe / Corel / Autodesk 等软件的激活域名与遥测域名)
* - 代理与直连规则
* - 进程规则(需管理员权限 + TUN 模式,即 Mihomo 创建虚拟网卡接管全部流量)
* - 激进阻断模块(默认关闭,需谨慎开启)
* - Hosts 级 DNS 覆写(四种映射子模式:黑洞型与欺骗型,由 HOSTS_MODE 选择)
* - 基于哨兵标记的幂等性(即无论执行多少次,结果相同)规则清理与注入(栈重建算法,O(N) 时间 / O(N) 空间,单次遍历,防止用户多次重新加载订阅导致本脚本注入的规则块重复追加)
* - 异常降级保护,详细运行日志
*
* ══════════════════════════ ░░ 使用说明 ░░ ══════════════════════════
*
* 1. 调整顶部配置区的功能开关(true / false)
* 2. 在对应数组中增删域名即可,无需修改下方逻辑
* 3. 保存后在 Clash Verge Rev 中重新加载配置文件即可生效,无需重启
*
*/
function main(config) {
const _startTime = Date.now(); // 计时起点
// ⚙️ ══════════════════════ 配置区(按需调整) ══════════════════════
// 所有 ENABLE_* 开关语义统一:true = 启用 false = 禁用。
// 修改后在 Clash Verge Rev(CVR,即本脚本所在的 Clash 图形前端)中重新加载订阅即可生效,无需重启。
// true = 完全启用脚本功能。
// false = 禁用规则注入,但仍返回含调试标记的修改版配置(向规则头部写入一条使用 .invalid 保留域的调试标记规则,非原样返回),详见下方 ENABLE_SCRIPT 分支说明。
// 即不注入功能性规则,实际网络路由等同于未加载脚本;只是规则列表中保留一条可见的调试标记供外部识别脚本禁用状态。
const ENABLE_SCRIPT = true;
// ──── 以下开关分别对应各注入层,但声明顺序与注入顺序解耦,注入顺序由 LAYER_ORDER 唯一决定(三处例外见括号说明)
// 顺序:allow > block > process > proxy > aggressive > direct
// · ENABLE_BLOCK 与 ENABLE_FIREFLY 相邻声明便于阅读(两者分属 block/allow 层,语义紧密关联)
// · ENABLE_GLOBAL_KEYWORD_BLOCK 属 block 层子开关,因说明篇幅较长集中声明于配置区末尾
// · ENABLE_SCRIPT、ENABLE_HOSTS_OVERRIDE 独立于此六层结构之外
const ENABLE_BLOCK = true; // 拦截模块(规则优先级高,仅次于 allow 层,isFireflyActive=true 时 Firefly 放行先于拦截执行,见 LAYER_ORDER)
// 关闭拦截模块同时意味着 Firefly 放行规则也不注入。流量既无 REJECT 也无代理覆盖,被进程规则或 MATCH 兜底策略接管。
const ENABLE_FIREFLY = true; // 精确放行 Adobe Firefly AI 生成式请求。启用放行规则需(ENABLE_BLOCK=true)对应拦截层以供豁免。
// ⚠️ 注意:此开关的放行规则注入状态由 isFireflyActive 唯一决定。见下方声明
// ⚠️ Firefly 放行出口为 proxyGroupName,该值由下方智能识别逻辑自动确定;若识别失败(全部策略均告失败),
// 脚本直接 return config 中止注入,Firefly 请求将退回订阅原始策略。必要妥协:auth/cc-api 等鉴权端点同时放行。
// 最后防线为 AdobeGCClient.exe → REJECT-DROP(仅 ENABLE_PROCESS_RULE=true + TUN 模式下有效)
const ENABLE_PROCESS_RULE = true; // 进程规则模块(需 TUN 模式 + 管理员权限;另还需配置文件的 find-process-mode 参数启用获取进程信息)。
const ENABLE_PROXY = true; // 指定域名走代理模块
const ENABLE_AGGRESSIVE = false; // 激进阻断模块(⚠️ 谨慎启用,可能影响官网/插件商店访问)
// ⚠️ 受影响范围:Adobe 插件市场/字体、Autodesk 官网/登录/授权、Office 更新/模板、ActiveX/旧版 OA 系统
// aggressiveRules 必须在 directRules 之前注入,否则父域 DIRECT 规则会优先匹配,子域 REJECT-DROP 将失效。
const ENABLE_GLOBAL_KEYWORD_BLOCK = false; // 关键词全局阻断(⚠️ 极度激进,会误杀大量合法 CDN/第三方服务)
// 命名说明:该开关命名为三段式(ENABLE / GLOBAL_KEYWORD / BLOCK),与同文件其他开关声明的两段式命名不对称;
// 原因:命名中的 GLOBAL 表示该开关控制的是全局关键词匹配行为,区别于 BLOCK 控制拦截模块。两者语义层次不同。
const ENABLE_DIRECT = true; // 指定域名直连模块
const ENABLE_HOSTS_OVERRIDE = true; // Hosts DNS 覆写模块(四种映射子模式:黑洞型与欺骗型,由 HOSTS_MODE 选择)
// ❗ 生效前提:CVR › DNS 覆写,必须同时开启「启用 DNS」和「使用 Hosts」。两个开关缺一不可,脚本无法感知 UI 层开关状态;
// 未开启时本模块失效(脚本仍打印成功日志,但 Hosts 覆写不生效)。
// ℹ️ 依赖约束:ENABLE_SCRIPT=false 时此模块被跳过(脚本提前返回,Hosts 注入不执行);如需关闭规则注入同时保留 Hosts 覆写,应保持 ENABLE_SCRIPT=true 并关闭各子模块开关。
// 💡 推荐使用 "ipv4-loopback"(当前默认值):返回 127.0.0.1,产生 ECONNREFUSED(回环模拟拦截),
// 应用兼容性通常最好;ipv4-blackhole 阻断速度最快,但可能被部分应用归类为断网状态。
//
// Hosts 模式选项:ipv4-loopback(127.0.0.1) / ipv4-blackhole(0.0.0.0) / dual-loopback(127.0.0.1+::1) / dual-blackhole(0.0.0.0+::)
// 命名说明:ipv4- 前缀标识 IPv4 单栈;dual- 前缀标识 IPv4+IPv6 双栈;
// loopback 为回环模拟拦截(DNS 返回回环地址,TCP 因本地无监听而 ECONNREFUSED,更温和);
// blackhole 为黑洞拦截(DNS 返回不可路由地址,OS 地址校验即失败)。四个名称完全对称。
//
// 各模式连接失败类型(来源:Mihomo wiki + OS 网络栈行为):
// ipv4-loopback → 127.0.0.1 → 本地 TCP 栈返回 RST(无监听端口),应用层收到 ECONNREFUSED,回环模拟拦截,更温和
// ipv4-blackhole → 0.0.0.0 → 错误码取决于操作系统:无论哪种错误码,TCP SYN 都不会发出,阻断速度最快。但部分应用可能将此错误误判为断网状态
// dual-loopback → 127.0.0.1 + ::1 → 同 ipv4-loopback,IPv4/IPv6 双栈回环模拟拦截
// dual-blackhole → 0.0.0.0 + :: → 同 ipv4-blackhole,IPv4/IPv6 双栈黑洞拦截(慎用:向 :: 发起连接的 OS 级行为因平台而异,可能导致应用异常)
//
// 双栈黑洞模式(dual-blackhole)因 IPv6 的 :: 行为更激进,存在非预期的连接错误或应用异常;ipv4-blackhole 单栈版风险较低。
const HOSTS_MODE = "ipv4-loopback";
//
// ──── ✅ 节点级 client-fingerprint 注入开关,TLS 客户端指纹模拟预设 ────
// 模拟指定客户端的 TLS 握手特征,以增强抗检测能力。实际效果依赖目标站点策略,不保证绕过指纹检测或消除触发验证码。
// - true: 为所有未设置 fingerprint 的节点注入默认指纹
// - false: 完全跳过指纹注入,保留节点原始配置
// ℹ️ 两种方式可阻止指纹注入:
// 1. 设置 ENABLE_CLIENT_FINGERPRINT = false → 模块完全关闭,日志显示“已禁用”
// 2. 设置 DEFAULT_FINGERPRINT = "none" → 模块运行但跳过注入,日志显示“已启用但 none”
// 两者最终效果相同(节点无 client-fingerprint 字段),可根据需要选择。
const ENABLE_CLIENT_FINGERPRINT = true;
// 默认指纹,仅在 ENABLE_CLIENT_FINGERPRINT 为 true 时生效。可选值: chrome / firefox / safari / iOS / android / edge / 360 / qq / random / none
// 💡 random:启动时从指纹库(按 Cloudflare Radar 数据概率)生成一个浏览器指纹并固定使用,非每连接随机切换。概率:Chrome 50%,Safari 25%,iOS 16.7%,Firefox 8.3%
const DEFAULT_FINGERPRINT = "chrome";
// 指纹注入黑名单:名称中包含这些关键词的节点,即使没有指纹也不会被注入。用途:保护特殊节点(如专用 IP 节点、特定落地机)不被意外修改指纹。
const FINGERPRINT_BLACKLIST = [
// "专用", "原生", "nobook", // 例:用户自建的落地节点,不希望被修改;原生 IP 节点;特定服务商要求不修改指纹。取消注释状态即启用。
];
// ──── 典型配置组合参考(按需参照调整上方开关值;此处为说明性文字,无需操作)────
//
// 【默认推荐】拦截 + Firefly 放行 + 代理 + 直连 + Hosts DNS 覆写(激进模式关闭)
// ENABLE_BLOCK=true ENABLE_FIREFLY=true ENABLE_PROCESS_RULE=true
// ENABLE_PROXY=true ENABLE_DIRECT=true ENABLE_HOSTS_OVERRIDE=true
// ENABLE_AGGRESSIVE=false HOSTS_MODE="ipv4-loopback"
//
// 【纯拦截模式】只拦截,不注入代理/直连规则,适合规则轻量化场景。
// ENABLE_BLOCK=true ENABLE_FIREFLY=false ENABLE_PROCESS_RULE=false
// ENABLE_PROXY=false ENABLE_DIRECT=false ENABLE_HOSTS_OVERRIDE=true
// ENABLE_AGGRESSIVE=false
//
// 【激进模式】在默认推荐基础上额外开启激进阻断,彻底封堵 adobe.io / adsk.com 等。
// ⚠️ 激进模式会影响官网/插件商店访问,开启前请仔细阅读 ENABLE_AGGRESSIVE 注释
// ENABLE_AGGRESSIVE=true (其余开关与默认推荐相同)
//
// 【仅调试/禁用脚本】停用规则注入,保留调试标记,方便对比前后差异。
// ENABLE_SCRIPT=false (其余开关无效)
// ──── Firefly 派生开关:isFireflyActive 是 Firefly 放行规则是否被注入的唯一决策变量 ────
// 派生值,仅由 ENABLE_FIREFLY && ENABLE_BLOCK 决定,不可反向修改上游开关。
// 设计逻辑:只有同时开启拦截模块(ENABLE_BLOCK)和 Firefly 开关(ENABLE_FIREFLY),Firefly 放行规则才真正生效,有拦截层才有"豁免"的意义。
// 所有 Firefly 相关代码逻辑均使用此变量,而非原始 ENABLE_FIREFLY,
// 防止用户因 ENABLE_FIREFLY=true 而误以为 Firefly 已放行(ENABLE_BLOCK=false 时 isFireflyActive 自动降级为 false,放行规则不注入)
const isFireflyActive = ENABLE_FIREFLY && ENABLE_BLOCK;
// ══════════════════════ 防御性检查 ══════════════════════
if (!config || typeof config !== "object" || Array.isArray(config)) {
throw new Error("[Script] main() 收到非法 config(null / 数组 / 非对象类型),终止执行以保护内核加载安全");
}
if (!Array.isArray(config.rules)) config.rules = [];
if (!Array.isArray(config["proxy-groups"])) config["proxy-groups"] = [];
// 功能依赖检查:isFireflyActive 已将 ENABLE_FIREFLY 与 ENABLE_BLOCK 的依赖关系封装,此 warn 仅供调试,对运行逻辑无影响。
if (ENABLE_FIREFLY && !ENABLE_BLOCK) {
console.warn("⚠️ 警告:Firefly 放行需同时启用拦截模块(ENABLE_BLOCK=true),当前已自动降级(未生效)");
}
if (ENABLE_PROCESS_RULE && config["find-process-mode"] !== "strict" && config["find-process-mode"] !== "always") {
console.warn(`⚠️ 进程规则要求 find-process-mode 为 strict 或 always,当前为 [${config["find-process-mode"] ?? "(未设置)"}],进程规则将静默失效`);
}
// ══════════════════════ ENABLE_SCRIPT 分支 ══════════════════════
// 先清理上次遗留标记,再插入新标记,防止多次切换后堆叠。
// ──── 前置执行:基于哨兵标记的幂等性规则清理与注入(栈重建算法,O(N) 单次遍历,处理任意数量堆叠)────
// 哨兵标记(sentinel):成对包裹本脚本注入的规则区间,供幂等清理时精确定位注入范围。
// 此处在 ENABLE_SCRIPT 判断之前执行,即使 ENABLE_SCRIPT=false 时也清理旧哨兵,
// 确保注入操作的幂等性:无论脚本执行多少次,结果相同,防多次切换后旧规则残留堆叠。
//
// 💡【算法选型:三候选方案,实测失败用例与边界用例详见附录"哨兵清理算法"节】
//
// (1) 废弃:filter 状态机——孤儿(无匹配配对的单端哨兵) START 出现时,其后全部订阅规则被无差别删除,当次执行不可逆(规则被删除,仅可通过重载订阅恢复)。
//
// (2) 废弃:while + findIndex + splice(首个END向前配对)——孤儿 END 触发 break,
// 其后全部有效配对未处理,旧注入规则全部残留;O(P×N) 时间,每轮 splice 内存重分配。
// 变体(废弃):findIndex 取全局首个 END 而非向前最近配对,嵌套场景连带删除哨兵间有效用户规则。
//
// (3) 采用方案:单次遍历栈重建(Stack Rebuild)—— O(N) 时间 / O(N) 空间。
//
// ⚠️ 哨兵格式设计:哨兵必须是合法的 Clash 三段式规则(TYPE,VALUE,POLICY)且格式固定,
// 清理算法依赖精确等值(===)匹配;若确需修改哨兵格式,须同步更新清理逻辑。null/undefined 也不会意外匹配哨兵字符串,严格等值天然防御。
// 💡 TLD 选型:使用 RFC 6761 明确保留的 .invalid(无效域),而非 .local(RFC 6762 mDNS 保留域)。
// local 在部分系统级 mDNS 配置下可能触发 DNS 多播查询;invalid 作为保留域,标准 DNS 实现不应对其解析,产生额外 DNS 流量的风险极低,更为安全。
const _SENTINEL_START = "DOMAIN,START-rule-injection-sentinel.invalid,REJECT";
const _SENTINEL_END = "DOMAIN,END-rule-injection-sentinel.invalid,REJECT";
{
// 栈重建:单次遍历,O(N) 时间,O(N) 空间。
// START 压栈时记录 newRules.length 快照;END 匹配时回退 length 至快照值,等效删除整段旧注入区块。
//
// 崩溃恢复行为(上次执行意外中断导致孤儿哨兵残留时):
// ⚠️ 两种孤儿均不抛出异常、不中断注入,残留规则可接受,中止注入不可接受。
// 孤儿 START(无配对 END):START 本身不写入 newRules,但其 length 快照被压栈(永不弹出);
// 其后续规则正常推入 newRules(无对应 END,不发生截断),旧注入规则与本次新注入共存一个周期。
// 旧注入与新注入并存一个周期,若两者覆盖域名相同则新注入先命中(结果正确);若不同则双重生效直至下次正常清理。
// 孤儿 END(无配对 START):静默跳过,不截断任何内容。
const newRules = [];
const stack = [];
for (const rule of config.rules) {
if (rule === _SENTINEL_START) {
stack.push(newRules.length);
continue;
}
if (rule === _SENTINEL_END) {
if (stack.length > 0) {
newRules.length = stack.pop(); // O(1) 截断,等效 splice 但无线性拷贝开销
}
continue;
}
newRules.push(rule);
}
config.rules = newRules;
}
if (!ENABLE_SCRIPT) {
// ⚠️ 注意:ENABLE_SCRIPT=false 是「带调试标记的受控禁用」,不是零修改的原样返回。
// 此分支仍会执行两个操作:
// (1) 清除上次遗留的 debug-script-disabled 标记(防标记重复追加)
// (2) 在规则头部插入新的 debug-script-disabled 标记(供外部识别脚本禁用状态)
// 因此返回的 config 与订阅原始状态有微小差异(多一条标记规则)。
// 如需零修改直接返回(Passthrough 直通模式),将此 if 分支体改为 return config; 即可。
// 如需保留 Hosts DNS 覆写但关闭规则注入,请保持 ENABLE_SCRIPT=true,
// 并将 ENABLE_BLOCK / ENABLE_PROXY / ENABLE_DIRECT 等各子模块开关设为 false。
// 等值匹配策略一致;避免宽泛子串误删合法规则此处用 filter 而非栈重建,因调试标记为单条平铺,无嵌套清理需求。
config.rules = config.rules.filter(r => r !== "DOMAIN,debug-script-disabled.marker.invalid,REJECT");
config.rules.unshift("DOMAIN,debug-script-disabled.marker.invalid,REJECT");
return config;
}
// ═══════════════ client-fingerprint 注入逻辑 ═══════════════
if (ENABLE_CLIENT_FINGERPRINT && DEFAULT_FINGERPRINT !== "none" && Array.isArray(config.proxies)) {
const blacklistSet = new Set(FINGERPRINT_BLACKLIST.map(s => s.toLowerCase()));
const blacklistArr = [...blacklistSet];
let injectedCount = 0;
let skippedCount = 0;
let preExistingCount = 0;
config.proxies = config.proxies.map(p => {
// 1. 尊重节点已有配置:字段存在即保留(包括 null、false、"")
// ⚠️ 变更 DEFAULT_FINGERPRINT 不会更新已有该字段的节点;如需强制覆盖,须先从订阅 YAML 删除节点的 client-fingerprint 字段后重新加载。
if (Object.prototype.hasOwnProperty.call(p, 'client-fingerprint')) {
preExistingCount++;
return p;
}
// 2. 检查黑名单:节点名包含黑名单关键词则跳过
const nodeName = (p.name || "").toLowerCase();
const isBlacklisted = blacklistArr.some(keyword => nodeName.includes(keyword));
if (isBlacklisted) {
skippedCount++;
return p;
}
// 3. 注入默认指纹
injectedCount++;
return { ...p, 'client-fingerprint': DEFAULT_FINGERPRINT };
});
console.log(`✅ TLS 指纹注入完成: 新增注入 ${injectedCount} 个,`
+ `跳过黑名单 ${skippedCount} 个,`
+ `保留原有指纹 ${preExistingCount} 个 (默认指纹: ${DEFAULT_FINGERPRINT})`);
} else if (ENABLE_CLIENT_FINGERPRINT && DEFAULT_FINGERPRINT === "none") {
console.log("ℹ️ TLS 指纹注入已启用,但默认指纹为 'none',不执行注入。");
} else if (ENABLE_CLIENT_FINGERPRINT && !Array.isArray(config.proxies)) {
console.log("ℹ️ TLS 指纹注入已启用,但 config.proxies 不是数组(订阅可能仅含 proxy-providers),跳过注入。");
} else {
console.log("ℹ️ TLS 指纹注入已禁用 (ENABLE_CLIENT_FINGERPRINT = false)。");
}
// ────────────────────────────────────────────────
console.log("=".repeat(28));
// 当前实现手动 padStart 拼接,格式固定为 HH:MM:SS(本地时区),跨引擎跨区域设置格式一致(时间值仍为本地时区)。
const _now = new Date(); // 当前时间(用于格式化日志时间戳)
const _ts = [_now.getHours(), _now.getMinutes(), _now.getSeconds()]
.map(n => String(n).padStart(2, "0"))
.join(":");
console.log(`📊 规则注入开始 [${_ts}]`);
console.log("=".repeat(28));
// ════════════════ 1. 智能识别代理策略组 ════════════════
//
// 逻辑:多级降级,兼容大多数订阅格式。无可用组时终止注入,防止 Mihomo 内核崩溃(见容错选取策略及代理组排除断言)。
let proxyGroupName = null; // 初始为 null,强制要求下游所有赋值路径全覆盖;任何未赋值路径均会触发代理组排除断言安全拦截。
// 注意:当前路径:null 不到达断言(识别失败已 return config);防御路径(仅在未来分支遗漏赋值时触发):sanitizeName(null) →""→ 断言拦截;
// 所有真实执行路径均会在策略链结束前显式赋值(成功时为 mainGroup.name);识别失败时直接 return config。
// 若将来新增分支遗漏赋值,sanitizeName(null) 返回 "",断言 !_sanitizedProxy 为 true 并安全拦截。
// 💡 当前实现中识别成功时 proxyGroupName 必定被赋值为组名;若将来新增代码分支,须确保也对其显式赋值,否则 null 会到达代理组排除断言并触发安全兜底(中止注入)。
// 💡 出口控制说明:识别逻辑通过 EXCLUDED_NAMES 明确排除了绝大多数不适合的出口;极端情况下(全部策略均失败)代码直接 return config,使网络退回订阅原始规则,防止内核崩溃。
// 策略组分三类:
// 排除组(EXCLUDED):绝对不能用作代理出口,会导致代理规则失效(流量不经过任何代理节点)
// 兜底组(FALLBACK):可用但不优先,无更好选项时才降级使用(GLOBAL/全局 等)
// 优选组(Eligible):正常可用且优先选择的代理组。
const EXCLUDED_NAMES = new Set([
"DIRECT",
"REJECT",
"COMPATIBLE", // Clash Premium 兼容模式保留关键字,Mihomo 不使用此类型;保留以防订阅包含 Clash Premium 格式策略组导致误选
"DEFAULT", // Mihomo 内部保留词,用于 Fallback 策略默认出口表达,防御性排除
"MATCH", // Mihomo 内置动作关键字(兜底策略);正常订阅格式下极不可能出现同名代理组,保留以阻止未来误用
"PASS", // 防止将 Mihomo 的 PASS(透传)策略错误选为代理出口
]);
const FALLBACK_NAMES = new Set(["GLOBAL"]); // 兜底组:降级才选
// ❗ 运行时配置断言:FALLBACK_NAMES ∩ EXCLUDED_NAMES 必须为空集。
// 若修改 FALLBACK_NAMES 或 EXCLUDED_NAMES,务必确保两者互斥。
// 若 "REJECT" 等被误加入 FALLBACK_NAMES,_isEligibleGroupCore 中的提前 return true 会旁路 EXCLUDED_NAMES 检查,使排除词被错误视为合法兜底组。
// ⚠️ 触发则说明常量集被误修改:改用 return config 而非 throw,保证用户网络不中断(退回订阅原始规则),错误仍通过 console.error 明确输出。
{
const _overlap = [...FALLBACK_NAMES].filter(n => EXCLUDED_NAMES.has(n));
if (_overlap.length > 0) {
console.error(`❌ 配置断言失败:FALLBACK_NAMES ∩ EXCLUDED_NAMES 非空: ${_overlap.join(", ")}`);
console.error(` 此约束被违反将导致 REJECT 等排除词被误选为代理出口,脚本中止注入`);
return config;
}
}
// 中文排除组正则(两段结构,这是有意设计,请勿合并为统一锚定写法):
// 前半段:^...$ 精确匹配(加 ^$ 两端锚定),覆盖"全部/全网/全用/全球/所有/默认"等独立词。
// → 避免「所有节点」「全局代理」等合法组名被误伤
// → 「全用」:含义为"全部用途",见于部分订阅的「全用途代理」组名;此类组名语义模糊,可能配置为 DIRECT 直连出口,也可能是真实代理出口,为保守起见一律排除;
// 保留的代价极低(精确词 $ 锚定,不会误伤含「全用」的复合组名如「全用节点」)
// 后半段:无位置锚定,子串匹配,覆盖「直连国内」「全局直连」「拒绝广告」等任意位置变体。
// → 「拒绝垃圾流量」含「拒绝」,保守排除:含「拒绝」的组名通常指向 REJECT 出口,用作路由出口将导致流量被拒绝。命名模糊时仍保守排除以防路由失效。
// ⚠️ "全局"已从此正则移出,由独立的 FALLBACK_CN_RE 负责识别(原因见下方 FALLBACK_CN_RE 及 isEligibleGroup 防回归说明)。
// ⚠️ 已知局限:后半段无位置锚点,采用子串匹配。若代理组命名为「非直连节点」、「不拒绝广告」等包含否定前缀的复合词,会因包含「直连」或「拒绝」子串而被错误排除。
// ⚠️ 「默认节点」等含「默认」的复合词组名不触发(精确词加 $ 锚定为设计取舍)此类指向 DIRECT 的订阅极为罕见;此取舍可最大化保障通用订阅的兼容性。
// ⚠️ 极端边界(两字排除):组名恰为「全球」(仅两字,无修饰词)时被精确匹配排除(^全球$),无警告日志,注入将进入容错路径或触发代理组排除断言中止。
// 含「全球」的复合词(如「全球节点」)因含「节点」关键词,可进入优选策略正常被选中。
// ℹ️ 针对极端特例的避坑指南,若订阅唯一组名恰为「全球」,有两种解法:
// 1. 直接在订阅转换前端或本地 Profile 覆写层将该代理组重命名(推荐,零代码侵入);
// 2. 在 EXCLUDED_CN_RE 中移除 "全球" 的匹配项,并将 FALLBACK_CN_RE 改为 /^(?:全局|全球)$/(同时更新 FALLBACK_NAMES ∩ EXCLUDED_NAMES 断言的测试词列表)。
//
const EXCLUDED_CN_RE = /^(?:全(?:部|网|用|球)|所有|默认)$|(?:直连|拒绝)/;
// 中文兜底组:「全局」对应 FALLBACK_NAMES 中的 GLOBAL,语义与行为均对称。
// ⚠️ 防回归:"全局"必须在此独立正则中识别,不得移入 EXCLUDED_CN_RE:
// · 若移入 EXCLUDED_CN_RE → isEligibleGroup("全局")=false,"全局"被排除出所有策略;
// 而 GLOBAL 由第四轮兜底降级路径的 isFallbackGroup 直接选中(isEligibleGroup 不参与第四轮),两者应对称,故"全局"不可移入 EXCLUDED_CN_RE。
// · (假设"全局"被移入 EXCLUDED_CN_RE 的情形下)即使兜底路径选中"全局",代理组排除断言的 EXCLUDED_CN_RE.test("全局") 也会为 true,立即中止注入,
// 净效果为零,两个条件同时满足才能正常工作,缺一不可。
const FALLBACK_CN_RE = /^全局$/;
// ❗ 运行时配置断言:FALLBACK_CN_RE 与 EXCLUDED_CN_RE 对兜底关键词"全局"的覆盖必须互斥。
// 若修改 FALLBACK_CN_RE 或 EXCLUDED_CN_RE,务必确保两者互斥。若"全局"同时被两者匹配,第四轮兜底路径虽能选中它,
// 但随后代理组排除断言(EXCLUDED_CN_RE.test)会立即中止注入,净效果为零;同时 isEligibleGroupCore 也会将其排除,兜底路径失效。
// ⚠️ 注意:此时哨兵清理已经执行(旧注入规则已剥离),返回的是清理后但未注入新规则的配置;等效于"本轮脚本跳过注入",用户仍有原始订阅规则托底,下次重载时恢复正常。
{
const _testWord = "全局";
if (FALLBACK_CN_RE.test(_testWord) && EXCLUDED_CN_RE.test(_testWord)) {
console.error(`❌ 配置断言失败:"${_testWord}" 同时匹配 FALLBACK_CN_RE 和 EXCLUDED_CN_RE`);
console.error(` 互斥约束被违反:兜底选取将被排除断言拦截,净效果为零,脚本中止注入`);
return config;
}
}
// 合法代理出口类型白名单(统一引用源:关键词优选/正则优选/类型优选各轮均引用此常量,新增类型只需改此处)。
// ⚠️ 最终容错策略(第五轮)改用 _UNSUITABLE_TYPES 黑名单方式,不引用此常量;
// 两者从正反两面描述同一批被排除类型(relay / url-latency-benchmark),
// 修改此处须同步检查 _UNSUITABLE_TYPES,反之亦然,两者描述同一批被排除类型的正反两面,当前已知类型空间内互补,新增类型须同步检查两处。
// load-balance 为动态路由策略,与 url-test 同级,具备合法出口语义,纳入白名单。
// 被排除的类型(在此两个 Set 中均体现为排除):
// relay:固定节点链路转发,强制指定出口,无用户可切换的节点选择语义。
// url-latency-benchmark:测速专用工具,以延迟评测为目的,不应作为流量出口组。
// smart:Mihomo v1.18+ 正式稳定类型,自适应出口选择,具备合法出口语义,已纳入白名单。旧版对 smart 行为不保证,若遇路由异常可回退:将 smart 从本 Set 移除。
// load-balance 已纳入 VALID_PROXY_TYPES(动态路由,具备合法出口语义),不再排除。
// 注意:此处保留最低限度的类型语义过滤,而非彻底放开,彻底放开会导致 relay 等固定链路被选中,流量走预设链路而非用户期望的可切换代理,行为与预期不符。
const VALID_PROXY_TYPES = new Set(["select", "url-test", "fallback", "load-balance", "smart"]);
// ⚠️ 互补视图:与 VALID_PROXY_TYPES 正反两面描述同一批被排除类型,修改任一处须同步检查另一处。
// 当前排除:relay(固定链路)/ url-latency-benchmark(测速工具)。smart 已从本 Set 移除并纳入 VALID_PROXY_TYPES。
const _UNSUITABLE_TYPES = new Set(["relay", "url-latency-benchmark"]);
// sanitizeName:统一不可见字符与 Bidi 控制符清理逻辑(完整列表见附录特殊字符集说明),防止视觉欺骗攻击与比较失配。
// @param {string} name - 原始组名,函数内部负责清洗;调用方无需预先清洗,传入原始字符串即可。
// @returns {string} 清洗后的组名(已移除不可见控制符并 trim);非字符串输入返回空字符串。
// ⚠️ g 是功能必要条件:需要清除字符串中所有不可见字符,而非仅首次命中。
// ⚠️ u 标志(Unicode 模式):确保字符类 [] 按完整 Unicode 码点解析,避免代理对被拆分匹配,语义更严谨;
// 当前覆盖范围仅含 BMP 内字符(U+0000–U+FEFF),不加 u 在现有代码路径下无实际 bug,
// 但加 u 是最佳实践,便于将来扩展覆盖范围时无需回头补加。
const _SANITIZE_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\u00AD\u061C\u200B-\u200F\u2028-\u202E\u2060\u2066-\u2069\uFEFF]/gu;
function sanitizeName(name) {
if (typeof name !== "string") return "";
if (!name) return ""; // 短路返回,省去对空字符串的无意义正则调用
// ⚠️ 若将来改用 exec()/test() 代替 replace(),须在调用前手动重置 lastIndex=0(g 标志正则有状态);String.replace() 内部自动重置 lastIndex,此处无需显式重置。
return name.replace(_SANITIZE_RE, '').trim();
}
// _isFallbackGroupCore / _isEligibleGroupCore:_Core 后缀约定(见附录命名准则)接受已清洗字符串,轻量版公开接口委托版,跳过二次 sanitizeName。
// 公开接口 isFallbackGroup / isEligibleGroup 负责清洗,再委托此二函数;_Core 变体仅供已预计算 cleanName 的内部路径使用。
//
// ⚠️ 调用顺序约束:_isEligibleGroupCore 中 _isFallbackGroupCore 必须先于 EXCLUDED_NAMES / EXCLUDED_CN_RE 检查执行,
// 兜底组(GLOBAL/全局)须通过 isEligibleGroup 初步关卡才能在第四轮降级路径中被选中;此顺序确保在互斥约束被意外违反时兜底路径仍有额外一层保障。
// ⚠️ 集合互斥约束:FALLBACK_NAMES 中的值不应与 EXCLUDED_NAMES 重叠,若将 "REJECT" 等误加入 FALLBACK_NAMES,提前 return true 将旁路 EXCLUDED_NAMES 检查。
// 上方运行时断言(FALLBACK_NAMES ∩ EXCLUDED_NAMES)从启动时保证此约束。
function _isFallbackGroupCore(trimmed) {
if (!trimmed) return false;
if (FALLBACK_NAMES.has(trimmed.toUpperCase())) return true;
return FALLBACK_CN_RE.test(trimmed);
}
function _isEligibleGroupCore(trimmed) {
if (!trimmed) return false;
if (_isFallbackGroupCore(trimmed)) return true;
if (EXCLUDED_NAMES.has(trimmed.toUpperCase())) return false;
if (EXCLUDED_CN_RE.test(trimmed)) return false;
return true;
}
// isEligibleGroup("全局") 返回 true(兜底组有入选资格),但前三轮优选策略以 !isFallbackGroup 二次过滤,"全局"须降至第四轮才被选中。"有资格"≠"优先选中"。
function isEligibleGroup(name) {
return _isEligibleGroupCore(sanitizeName(name));
}
function isFallbackGroup(name) {
return _isFallbackGroupCore(sanitizeName(name));
}
if (config["proxy-groups"].length > 0) {
// 预编译静态关键词正则(替代原 KEYWORDS 数组 + some/includes 多次子串搜索):
// /i 标志覆盖 ASCII 大小写变体(proxy/Proxy/PROXY/auto/AUTO 等),
// 无需手动枚举多份硬编码字符串。中文字符与 emoji(🚀)在 /i 下无副作用,正常匹配。
// ⚠️ /i 修复了原实现仅能匹配大写 AUTO 的遗漏(proxy/Proxy/auto-select 等混合大小写组名均无法命中),
// 现覆盖 ASCII 大小写所有变体,为正确行为;含 auto 的组名(如 auto-select)是合法的代理出口。
// ⚠️ g 标志:_KW_RE 不含 g 标志,RegExp.test() 无 g 时不修改 lastIndex,完全无状态,
// 在 _groupsPrepped.find() 中多次调用 .test() 行为一致,无需担心状态污染。
// 🚀(U+1F680)为 BMP 外字符,无 u 标志时作代理对匹配,现代 V8 引擎行为正确;若需严格 Unicode 语义可加 u 标志(/…/iu),当前不加亦无实际问题。
// 不含 "global"(GLOBAL 由 FALLBACK_NAMES 单独处理),不含 "默认"(已被 EXCLUDED_CN_RE 覆盖,关键词层无需重复)。
const _KW_RE = /节点(?:选择)?|手动选择|选节点|proxy|auto|自动|🚀|飞机|机场|线路|订阅/i;
// 预计算所有组的 cleanName,避免各轮 find 各自对同一组名重复调用 sanitizeName。
// 对含 100+ 代理组的大型订阅,最坏情况下五轮各遍历一次,sanitizeName(_SANITIZE_RE.replace)被执行 1×N 次 sanitize + 5×N 次正则测试;
// 预计算降为 1×N,后续各轮直接引用 cleanName。
const _groupsPrepped = config["proxy-groups"].map(g => ({
g,
cleanName: sanitizeName(g?.name)
}));
// [优选·关键词] 关键词 / include-all / 多节点三路并联匹配(最优先,覆盖最广)
// 各轮 find 统一返回完整条目 { g, cleanName },由 _mainEntry 持有;不再拆解出 .g 后回头再次线性搜索 cleanName(双重 find 模式已消除)。
let _mainEntry = _groupsPrepped.find(({ g, cleanName }) => {
if (!_isEligibleGroupCore(cleanName)) return false;
if (_isFallbackGroupCore(cleanName)) return false;
const typeOk = VALID_PROXY_TYPES.has(g?.type);
const nameMatch = _KW_RE.test(cleanName);
const hasMany = Array.isArray(g?.proxies) && g.proxies.length > 3;
// length > 3(即 ≥ 4):排除 proxies 数组近乎为空的极简占位组,如:["节点1","节点2","节点3"](length=3,被排除),
// ["节点1","节点2","节点3","节点4"](length=4,通过),优先选入条目数量充足的组。
// ⚠️ 注意:proxies 数组可包含三类条目:底层节点名称、其他策略组名称、内置代理名称(DIRECT / REJECT);
// length > 3 衡量的是三类条目的总数,不等于底层节点计数。
// 已知局限:全部条目均为策略组引用(无底层节点)时,阈值仍可成立,但被选中的组仍能正常委托子组路由,实际影响极小;后续多轮兜底进一步覆盖此场景。
const includeAll = g?.["include-all"] === true || g?.["include-all"] === "true";
// includeAll 仅接受 boolean true 或字符串 "true"(严格等值);数字 1 / 其他 truthy 值不触发(有意严格,避免意外匹配)。
return typeOk && (nameMatch || includeAll || hasMany);
});
// [优选·正则] 正则宽松匹配(次选,排除兜底组)
if (!_mainEntry) {
_mainEntry = _groupsPrepped.find(({ g, cleanName }) =>
_isEligibleGroupCore(cleanName) && !_isFallbackGroupCore(cleanName) &&
/代理|节点|选择|Proxy/i.test(cleanName) &&
VALID_PROXY_TYPES.has(g?.type) &&
Array.isArray(g?.proxies) && g.proxies.length > 3
);
}
// [优选·类型] 类型约束(放宽数量,任意合法出口类型)
// 增加 Array.isArray + length > 0 约束,防止选中空 proxies 的 select 组。
// 各策略数量约束对比:关键词/正则策略要求 length > 3,本策略与兜底降级策略均要求 length > 0;
// 本策略放宽数量约束(> 0 而非 > 3)以扩大覆盖范围,避免漏选小型节点池。
if (!_mainEntry) {
_mainEntry = _groupsPrepped.find(({ g, cleanName }) =>
_isEligibleGroupCore(cleanName) && !_isFallbackGroupCore(cleanName) &&
VALID_PROXY_TYPES.has(g?.type) &&
Array.isArray(g?.proxies) && g.proxies.length > 0
);
}
// [兜底降级] 降级选取(GLOBAL/"全局" 等,优选策略全部失败时才触发)
// ⚠️ 不能直接取首个元素,订阅第一个组可能是 DIRECT,导致本脚本注入的代理规则失效(流量直连)
// 保留类型过滤(与前三轮优选策略一致),防止选中固定链路(relay)和测速专用(url-latency-benchmark);smart 已纳入 VALID_PROXY_TYPES,可被选中。
if (!_mainEntry) {
_mainEntry = _groupsPrepped.find(({ g, cleanName }) =>
_isFallbackGroupCore(cleanName) &&
VALID_PROXY_TYPES.has(g?.type) &&
Array.isArray(g?.proxies) &&
g.proxies.length > 0
);
if (_mainEntry) {
console.warn(`⚠️ 未找到优选代理组,降级使用兜底组 [${_mainEntry.g.name}]`);
}
}
// [最终容错选取] 安全兜底(全部前置策略失败时的最后屏障)────────────────
// 优选层策略(关键词/正则/类型)与兜底层降级策略全部失效时进入此分支。
// 目的:在显式 return config 中止注入之前,尽力寻找可用组。
// 策略:仅排除出口语义不适合的类型(relay / url-latency-benchmark),其他类型均允许;smart 已移入 VALID_PROXY_TYPES,此处不再排除。
// 为减少用户干预成本,在无理想出口组时优先选取次优组而非中止注入;用户若发现路由异常,可通过添加关键词或调整组名来引导识别。
// 注:load-balance 已纳入 VALID_PROXY_TYPES,不在 _UNSUITABLE_TYPES 排除列表中。
if (!_mainEntry) {
// [最终容错选取] 排除语义不适合做代理出口的类型(而非全部放开)
// relay:固定节点链路转发,无节点选择语义,用户无法在其界面切换节点。
// url-latency-benchmark:测速专用工具,以延迟评测为目的,不应作为流量出口组。smart 已移入 VALID_PROXY_TYPES,Mihomo v1.18+ 已稳定,不再列为不适用类型。
_mainEntry = _groupsPrepped.find(({ g, cleanName }) =>
_isEligibleGroupCore(cleanName) &&
!_UNSUITABLE_TYPES.has(g?.type) &&
Array.isArray(g?.proxies) && g.proxies.length > 0
);
if (_mainEntry) {
console.warn(`🚨 严重警告:关键词/正则/类型优选 + 兜底组降级全部失败,触发最终容错选取`);
console.warn(` 已排除固定链路(relay)/ 测速专用(url-latency-benchmark);smart 已纳入白名单,可被选中;`
+ `选取首个可用组 [${_mainEntry.g.name}] (type: ${_mainEntry.g.type ?? "未知"})`);
console.warn(` 建议检查订阅结构是否符合关键词列表`);
}
}
// _mainEntry 持有完整条目 { g, cleanName },无需第二次线性搜索。
const mainGroup = _mainEntry?.g;
if (mainGroup?.name) {
proxyGroupName = mainGroup.name;
// cleanName 直接取自 _mainEntry,零额外遍历。
const _selectedClean = _mainEntry.cleanName;
const groupFlag = _isFallbackGroupCore(_selectedClean) ? "⚠️" : "✅";
console.log(`${groupFlag} 代理组识别成功: [${proxyGroupName}] (type: ${mainGroup.type ?? "未知"})`); // ⚠️=兜底组,✅=优选组
} else {
// 容错选取策略也失败:订阅中无任何可注入的代理出口组(全被排除或类型不适)。
// 直接返回 config,完整降级为订阅原始规则,防止 Mihomo 内核因找不到策略组而崩溃。
console.error("❌ 致命:订阅中没有任何可用的代理组,中止规则注入");
console.error(" 网络将走订阅原始规则,不注入任何自定义规则,防止 Mihomo 内核启动失败");
console.log(` 已扫描的代理组:`);
// _groupsPrepped 已预计算 cleanName,错误路径同样零额外 sanitizeName 调用
_groupsPrepped.forEach(({ g, cleanName }, idx) => {
const eligible = _isEligibleGroupCore(cleanName);
const fallback = _isFallbackGroupCore(cleanName);
const status = !eligible ? "❌" : (fallback ? "⚠️" : "✅");
const count = g?.proxies?.length ?? 0;
console.log(` ${idx + 1}. ${status} [${g?.name}] (${g?.type ?? "未知"}, ${count} 节点)`);
});
return config;
}
} else {
// 此 else 仅在 proxy-groups 为空(length === 0)时执行。
// 注意:即使 if 块内五轮策略全部失败,也不会到达此处,if/else 的判断条件是 proxy-groups.length,而非策略是否成功。
// 直接返回 config,中止规则注入,防止 Mihomo 内核因找不到策略组而崩溃,使网络退回订阅原始规则。
console.error("❌ 致命:proxy-groups 为空,中止规则注入");
console.error(" 网络将走订阅原始规则,不注入任何自定义规则,防止 Mihomo 内核启动失败");
return config;
}
// 💡 Mihomo 规则语法中策略组名直接使用原始名称,空格 / emoji 均无需引号。引号包裹反而会让内核把引号字符视为组名的一部分,导致 proxy not found 报错。
// ❗ 代理组排除断言:防止 proxyGroupName 解析为排除出口导致拦截规则静默失效。
// 覆盖全部排除名:DIRECT / REJECT / COMPATIBLE / DEFAULT / MATCH / PASS 及中文等价排除词。
// 注:失败路径(全部策略失败 / proxy-groups 为空)均已在上方显式 return config,正常执行到此处时 proxyGroupName 必然是识别成功的合法组名;
// 此断言作为防御纵深,防止将来新增代码路径绕过显式返回,或选组逻辑被重构后假设不再成立。
// 注:兜底组已被剥离出排除正则,确保在优选降级触发时,它能顺利通过代理组排除断言而不被误杀。
{
const _sanitizedProxy = sanitizeName(proxyGroupName);
if (!_sanitizedProxy ||
EXCLUDED_NAMES.has(_sanitizedProxy.toUpperCase()) ||
EXCLUDED_CN_RE.test(_sanitizedProxy)) {
console.error(`❌ 代理组排除断言触发:proxyGroupName 解析为排除出口 [${proxyGroupName}]`);
console.error(` 注入出口目标解析为排除项,allow/proxy 层路由将失效,脚本中止注入以保护配置安全边界`);
return config;
}
}
// ❗ 规则字段注入安全断言:proxyGroupName(原始值)不得含破坏 Clash 规则语法或 YAML 结构的字符。
// 💡【设计意图:容错识别 vs 安全注入分离】
// sanitizeName 在"识别阶段"清洗组名,目的是宽容匹配,因编辑器或复制粘贴意外引入不可见控制符的代理组(如名称带 BOM 的组),
// 用户设置该组的本意是合法代理出口,不应因组名中意外混入的不可见字符导致识别阶段漏选。此断言在"注入阶段"对原始值实施一票否决,Mihomo 内核按原始名称匹配策略组,
// 注入只能使用原始名;若原始名含控制符,会破坏 Clash 规则行语法,危及整个规则文件解析。
// 两者不是冗余,是刻意的"宽进严出(识别阶段宽容,注入阶段严格)"纵深防御:识别尽量不漏选,注入绝对不破坏语法。
// proxyGroupName 存储原始值(mainGroup.name),sanitizeName 的清洗结果不用于此处。清洗结果仅用于排除词汇匹配,注入时仍使用原始值。
// 四类拒绝维度(不同攻击向量),详细字符集见附录特殊字符集说明。
// 注:_SANITIZE_RE 与此断言存在字符集重叠,但两者作用层次不同(清洗识别副本 vs 拒绝注入原始值),目的不重叠,非冗余。
// 💡 两层覆盖范围的完整差异说明见 _SANITIZE_RE 注释(权威定义源);
// 本断言为注入层权威说明:仅覆盖 Clash/YAML 语法破坏字符,不覆盖 Bidi 控制符(Bidi 视觉欺骗问题由识别层处理,注入层不需要重复防御)。
if (/[,\u0000-\u001F\u007F\u0085\u2028\u2029]/.test(proxyGroupName)) {
console.error(`❌ Token 断言触发:proxyGroupName [${JSON.stringify(proxyGroupName)}] 含非法字符`);
console.error(` 逗号截断规则语义;C0 控制字符(U+0000–U+001F,含 \t/\n/\r)及 NEL(U+0085,C1 控制字符,等效换行)——均破坏 Clash 规则语法,脚本中止注入`);
return config;
}
// ✅ 执行到此处时,proxy-groups 非空且 proxyGroupName 已赋值(空或识别失败均已 return config),此断言针对"选组逻辑重构后 proxyGroupName 与实际数组意外失配"的防御场景。
// 正常执行路径下 proxyGroupName = mainGroup.name,必然存在于数组中;此断言针对的是选组逻辑被重构或调用方变更后该假设不再成立的情形,属防御纵深而非冗余。
//
// 💡 比较策略:使用原始名称精确匹配(g?.name === proxyGroupName),而非双侧 sanitizeName。
// 理由:
// (1) Mihomo 内核按原始名称精确匹配策略组,存在性断言应当模拟 Mihomo 的匹配行为。
// (2) proxyGroupName = mainGroup.name,mainGroup 本身即从数组中取得,直接等价必然命中,双侧 sanitize 不提供任何额外防护。
const groupExists = config["proxy-groups"].some(g => g?.name === proxyGroupName);
if (!groupExists) {
console.error(`❌ 存在性断言触发:代理组 [${proxyGroupName}] 在当前配置中不存在`);
console.error(` 注入此组名会导致 Mihomo 内核启动失败,脚本中止注入`);
return config;
}
// 💡 哨兵为合法三段式规则(见 _SENTINEL_START / _SENTINEL_END 声明);纯注释字符串(如 "# START")会导致内核加载失败。
// ════════════ 2. 数据层(域名列表 + 注入辅助工具,在此维护) ════════════
//
// 规则构造辅助函数:逐项将域名 / 关键词转换为 Clash 规则字符串并追加到目标数组。
// ⚠️ 调用方须确保数组元素均为字符串;非字符串元素(null / undefined / 数字)会被模板字符串静默转换,
// 生成格式合法但语义非法的规则(如 DOMAIN-SUFFIX,null,REJECT),Mihomo 不报错但该规则永远不会命中。
// 当前所有调用方均使用字符串字面量数组,无此风险;若将来从外部数据源动态填充,须在调用前校验元素类型。
// ⚠️ 类型守卫:过滤非字符串或空元素,防止 null/undefined/数字被模板字符串静默转换为
// DOMAIN-SUFFIX,null,REJECT 等语义非法规则(Mihomo 不报错但规则永远不命中)。
// 当前所有调用方均使用字符串字面量数组,守卫仅为将来动态数据源预防。
const pushSuffix = (domains, action, pool) => domains.forEach(d => {
if (typeof d === "string" && d.length > 0) pool.push(`DOMAIN-SUFFIX,${d},${action}`);
else console.warn(`[Script] pushSuffix: 非法条目已跳过`, d);
});
const pushDomain = (domains, action, pool) => domains.forEach(d => {
if (typeof d === "string" && d.length > 0) pool.push(`DOMAIN,${d},${action}`);
else console.warn(`[Script] pushDomain: 非法条目已跳过`, d);
});
const pushKeyword = (words, action, pool) => words.forEach(w => {
if (typeof w === "string" && w.length > 0) pool.push(`DOMAIN-KEYWORD,${w},${action}`);
else console.warn(`[Script] pushKeyword: 非法条目已跳过`, w);
});
// ──── Adobe Firefly 依赖端点集(统一引用源:所有用到该集合的地方均引用此数组,修改时无需同步多处)────
// adobeFireflyOnly 独立成数组(而非并入 adobeSuffix),是因为两者路由动作不同:
// adobeFireflyOnly 在 isFireflyActive=true 时走代理(allow 层),adobeSuffix 始终走 REJECT(block 层);合并会丢失路由区分能力。
//
// 路由动作由 isFireflyActive 决定:
// isFireflyActive=true → pushSuffix(adobeSharedDeps, proxyGroupName, layerPools.allow) → 走代理
// isFireflyActive=false → pushSuffix(adobeSharedDeps, "REJECT", layerPools.block) → 走拦截
// 两条分支覆盖相同域名集合,行为对称,单一维护点,修改只需改此数组。
// adobe 相关变量命名:
// adobeFireflyOnly:Firefly AI 生成式专属(clio, firefly-api 等)
// adobeSharedDeps:共用鉴权端点,Firefly 和其他 Adobe 功能(同时被用于 CC 激活验证)共同依赖的鉴权/授权端点
//
// ⚠️【Firefly 必要妥协】auth.services.adobe.com / cc-api-cp.adobe.io 同时承载 CC 正版验证心跳。
// isFireflyActive=true 时放行后,以下进程的鉴权请求均走代理,而进程规则仅覆盖 AdobeGCClient.exe:
// AdobeGCClient.exe ← 由 processBlockRules REJECT-DROP(静默丢包,见下方说明)兜底(已覆盖)
// Creative Cloud.exe ← CC 桌面客户端含授权心跳(基于依赖链考量的必要豁免:心跳放行不触发重验证,TUN 进程规则本身不可靠)
// CCXProcess.exe ← CC 扩展宿主进程(同 Creative Cloud.exe,必要豁免)
// CoreSync.exe ← CC 同步守护进程(同上)
// 取舍依据:非官方激活环境中,补丁修改了 AdobeGCClient.exe 的本地验证逻辑(本地返回激活成功,无需真实网络应答);
// 本脚本在此基础上阻断其出站连接,作为额外网络层防线,防止激活状态回报和设备信息上传。
// 其余进程的心跳即便放行也不会触发重新验证。进程规则本身需管理员+TUN,不可靠。
//
// ⚠️【QUIC(RFC 9000;基于 UDP 的安全传输协议,内嵌 TLS 1.3)豁免机制】Firefly 相关 .adobe.io 域名在 adobeUdpBlock 之前注入(first-match),
// 其 UDP 流量先命中 allow 层走代理,adobeUdpBlock 的 adobe.io 通配不再执行。
// → 豁免效果由注入顺序自动保证(allow 层先于 adobeUdpBlock 入 pool,先命中即生效),无需额外处理。
// ⚠️ 前提:此豁免仅在 Mihomo 能识别 SNI 或存在 Fake-IP 映射时成立。
// ECH(Encrypted Client Hello,将 SNI 加密)会使 Sniffer 失效,但影响范围取决于寻址路径:
// · 路径A(Fake-IP + TUN,CVR 默认配置路径):域名已由 DNS 映射阶段记录,ECH 不影响豁免效果,
// allow 层 DOMAIN-SUFFIX 正常命中,Firefly QUIC 流量正常走代理。
// · 路径B(应用绕过 Mihomo DNS,使用 DoH / DoT 或硬编码 IP):无 Fake-IP 映射,
// Sniffer 又被 ECH 阻断,allow 层与 adobeUdpBlock 的域名规则同时失效,
// Firefly QUIC 流量不受规则层干预,滑落至 MATCH(详见 adobeUdpBlock 末尾说明)。
const adobeSharedDeps = [
// ──── 已确认条目(抓包或官方资料可支撑)────
"ims-na1.adobelogin.com", // 登录令牌刷新(已确认)
"adobeid-na1.services.adobe.com", // Adobe ID 服务(已确认)
"auth.services.adobe.com", // Adobe ID 鉴权,Firefly Token 来源(已确认)
"cc-api-cp.adobe.io", // CC 权限校验,含 Firefly 订阅验证(已确认)
"cc-api-data.adobe.io", // CC 生成结果存储(已确认)
"lcs-roaming.adobe.io", // 授权漫游,Firefly 订阅状态同步(已确认)
// ──── 待抓包确认条目(基于行为和命名推断,非官方文档支撑)────
// ⚠️ 设计取舍:优先可用性(Firefly 正常运行),而非最小权限拦截原则。以下域名尚无公开抓包资料确认其确切功能,但 Firefly 在实测中依赖这些端点,故默认放行。
// 若追求最严格的拦截策略,可手动将其移至 adobeSuffix(改为 REJECT)并重新测试 Firefly 功能是否正常,确认后再决定是否从本数组移除。
"scdown.adobe.io", // 【推断·放行风险低、漏拦截风险可接受】基于行为推断,未经抓包验证;Firefly 在实测中依赖此端点(即使功能定义不明)
// lcs-cops.adobe.io 已移出:注释原文承认"若此域实为 CC 激活验证端点,isFireflyActive=true 时将被错误放行,激活拦截防线出现缺口"。
// 为消除该缺口,改为固定注入 adobeSuffix(始终 REJECT),待抓包确认其为 Firefly 专属后可移回此数组。
];
// 🚫 ─────────────────────── Adobe 激活 / 遥测核心拦截 ───────────────────────
// 💡 关于 REJECT vs REJECT-DROP(Mihomo 的两种拒绝策略):
// REJECT 发送 TCP RST(TCP 侧)/ ICMP Port Unreachable(UDP 侧),软件快速感知失败(通常切换离线模式或放弃重试),启动无卡顿,推荐用于遥测/授权域名。
// REJECT-DROP 静默丢包,不回应任何数据包(TCP 和 UDP 均适用),
// TCP 侧:软件 Socket 陷入 SYN_SENT 直至系统 TCP 超时;
// UDP 侧:数据包被无声丢弃,软件等待响应直至应用层超时;
// 超时时长为估算值(非固定值),应用层 Socket 阻塞约 15–30s(含 TCP 重传轮次),实际取决于操作系统 TCP 重传配置(Windows 10 默认 TcpMaxSynRetransmissions=2,
// SYN 重传总时长约 21s;Windows 11 默认值已调整,实际超时可能有所不同)。仅用于非官方修改补丁后门(backdoorSuffix/backdoorKeyword)和进程规则,
// 以此拖延被拦截进程感知失败的时间(Socket 等待超时而非立即失败),阻碍恶意程序发现阻断并切换备用域名的速度。
//
// adobeSharedDeps 条目已移出(路由动作由 isFireflyActive 决定),此处为非 Firefly 依赖的拦截域名。
const adobeSuffix = [
"adobestats.io", // 统计上报主域
"activate.adobe.com", // 激活核心
"lmlicenses.wip4.adobe.com", // Adobe 许可证管理服务(wip4 疑似集群标识,功能已抓包确认)
"prod.adobegenuine.com", // Genuine Integrity Service(正版完整性验证服务)
"na1e.services.adobe.com", // Genuine 服务备用
// "adobedtm.com", // Adobe DTM 旧版遥测域(DTM 已于 2021 年停止维护,新版 CC 不再依赖此域),可能仍有旧版 CC 存量实例使用
"crs.cr.adobe.com", // License check(许可证检查)
"cclibraries-defaults-cdn.adobe.com", // CC Libraries 默认资源 CDN(内容分发网络),截后会导致跨 CC 应用的共享资源库无法加载默认资源,影响正版用户。
"adobesearch.adobe.io", // 搜索遥测
"p13n.adobe.io", // 个性化遥测(p13n = personalization 字符数缩写)
"ic.adobe.io", // Insight Collector(洞察收集器)
"lcs-mobile.adobe.io", // 新版 CC 移动端授权
"adobe-dns.adobe.com", // Adobe 自有 DNS 服务(拦截后可减少软件绕过系统 DNS、向自有解析器查询激活/遥测 IP 的可能性,降低 hosts 层拦截被旁路的概率)
"adobe-dns-2.adobe.com", // Adobe 自有 DNS 备用节点 2(同上)
"adobe-dns-3.adobe.com", // Adobe 自有 DNS 备用节点 3(同上)
"practivate.adobe.com", // 预激活服务
"lm.licenses.adobe.com", // License Manager(许可证管理器)
"genuine.adobe.com", // 正版验证
"oobesaas.adobe.com", // Adobe SaaS 授权验证服务(oobesaas 为 Adobe 内部命名,与 Windows OOBE 无关;阻断后抑制授权弹窗)
// 注:ffc-static-cdn.oobesaas.adobe.com 已被此 SUFFIX 完整覆盖,无需单独列出
"sstats.adobe.com", // 实时统计上报(新版 CC 框架)
"entitlementauthz.adobe.com", // 授权(Authorization)验证服务(authz 为 authorization 缩写,2025 年新增)
"assets.entitlement.adobe.com", // 授权资产校验(2025 年新增)
"telemetry.adobe.com", // Adobe 遥测的另一入口,在部分 CC 版本抓包中出现
"lcs-cops.adobe.io", // 云端授权策略端点(待抓包确认 Firefly 专属性)。原位于 adobeSharedDeps(isFireflyActive=true 时走代理),
// 因存在激活拦截缺口风险,改为固定 REJECT。若抓包确认其仅服务 Firefly 而非 CC 激活验证,可将其移回 adobeSharedDeps。
];
// ──── 随机子域正则(统一引用源),adobeRegex 与 adobeUdpBlock 均引用此变量,禁止各自硬编码,修改只需改此处 ────
// 注:实际遥测子域通常为小写十六进制字符(0-9a-f),正则使用全字母数字范围(A-Za-z0-9)为稳妥覆盖,不影响正确性。
// ⚠️ ^$ 锚定不可移除:Go regexp.MatchString 为子串匹配,若移除锚定,
// "abcdefgh.adobe.io.evil.com" 也会命中(子串 abcdefgh.adobe.io 满足 {8,12} 模式),
// 导致非 adobe.io 域名被错误拦截(过拦截误伤 false positive),而非 adobe.io 的流量无辜受殃。
// 注意:adobestats.io 已在 adobeSuffix 以 DOMAIN-SUFFIX 全覆盖(含所有子域),
// 本 REGEX 在实际注入顺序下被前置 SUFFIX 规则遮蔽,功能冗余但无害;保留的意义仅为正则规则集的完整性表达。
const _ADOBE_RAND_RE_STR = "^[A-Za-z0-9]{8,12}\\.adobe\\.io$"; // adobe.io 随机子域(8-12位)
const _ADOBESTATS_RAND_RE_STR = "^[A-Za-z0-9]{10}\\.adobestats\\.io$"; // adobestats.io 随机子域(社区记录为固定10位,若实测发现其他长度,请调整此正则)
// 正则:拦截随机子域(遥测特征:8-12 位随机字符)改用 REJECT(非 REJECT-DROP):
// 遥测随机子域无"拖延感知"的必要,此类域名不存在切换备用域名的自适应逻辑,
// REJECT 让软件快速感知失败(通常切换离线模式或放弃重试),避免 15–30s 超时卡顿影响 PS 启动体验。
const adobeRegex = [
`DOMAIN-REGEX,${_ADOBE_RAND_RE_STR},REJECT`,
// ⚠️ senseicore(10位)/ senseimds(9位)也满足 _ADOBE_RAND_RE_STR,但均为具名服务域名而非随机遥测子域。
// · isFireflyActive=true:adobeFireflyOnly 精确 SUFFIX 先命中 allow 层,此正则对两者无效(已豁免)。
// · isFireflyActive=false:两者将被此 REGEX 命中并 REJECT(立即返回失败),
// 用户表现为 PS Neural Filters / Select Subject 等依赖 Sensei 的 AI 功能立即报错,而非卡死 15–30s,改 REJECT 后用户可快速判断为网络拦截而非软件 bug。
// ❗【待抓包确认】若确认 senseicore/senseimds 同时服务非 Firefly 的 PS AI 功能,
// 建议将其显式加入 adobeFireflyOnly(精确放行走代理)或 adobeSuffix(以精确 DOMAIN-SUFFIX 替代当前 REGEX 覆盖,动作不变仍为 REJECT)
`DOMAIN-REGEX,${_ADOBESTATS_RAND_RE_STR},REJECT`,
];
// QUIC(RFC 9000;基于 UDP 的安全传输协议,内嵌 TLS 1.3)/ UDP 拦截:强制 Adobe 应用放弃 QUIC(HTTP/3),降级至 TCP(HTTP/1.1 或 HTTP/2)再被域名规则捕获
// ❗ 生效前提:仅 TUN 模式。UDP 拦截规则在系统代理模式下完全无效。
// ⚠️ DOMAIN-SUFFIX / DOMAIN-REGEX / DOMAIN-KEYWORD 类规则依赖 Mihomo 能获取域名信息:
// Mihomo 通过 DNS 解析映射(已走 Mihomo DNS 的流量)或 Sniffer(嗅探 QUIC 握手 SNI)识别域名;
// 纯 IP 形式的 UDP/QUIC 流量无域名信息可供匹配,DOMAIN 类规则对其无效。
// ⚠️ PROCESS-NAME 规则不依赖 SNI 嗅探(通过系统 Socket 直接获取进程信息),在路径B(应用绕过 Mihomo DNS 且开启 ECH,DOMAIN 类规则全部失效)下,
// 是唯一有效的域名无关进程级拦截手段;路径A(Fake-IP 正常)下 DOMAIN 规则已生效,PROCESS-NAME 为附加纵深而非唯一防线。
//
// 改用 REJECT(非 REJECT-DROP):UDP 阻断目的仅是强制 TCP fallback,无需"拖延感知"效果。
// REJECT 发送 ICMP Port Unreachable,应用立即感知 QUIC 不可达并 fallback 至 TCP,比 REJECT-DROP 的 15–30s 超时 fallback 快得多,用户体验更好。
//
// ⚠️【directRules 中 adobe.com 子域的 UDP 路径说明】
// fonts.adobe.com / stock.adobe.com / behance.adobe.com 等在 directRules 中配置为 DIRECT。
// 其 UDP(QUIC)流量先命中 AND,((NETWORK,UDP),(DOMAIN-SUFFIX,adobe.com)),REJECT(下方第三条),
// 收到 ICMP 后应用立即 fallback 至 TCP,TCP 连接再命中 directRules 的 DOMAIN-SUFFIX,DIRECT。
// 整体路径:UDP→REJECT(立即) → TCP fallback → DIRECT。无延迟,行为符合预期。
//
// AND 条件书写顺序按代价从低到高排列(设计意图:期望内核能够尽早排除低代价条件后跳过高代价求值):
// NETWORK(读包头)→ DST-PORT(整数比较)→ DOMAIN-*(依赖 SNI 嗅探)实际求值顺序依赖 Mihomo 内核实现,此处为书写规范而非内核行为保证。
const adobeUdpBlock = [
// ⚠️ 以下各条均依赖 Mihomo DNS 映射或 Sniffer SNI 嗅探才能识别域名;
// 纯 IP 形式 QUIC 流量或路径B(绕过 Mihomo DNS 且开启 ECH)下,DOMAIN 类规则对此无效(见末尾说明)
"AND,((NETWORK,UDP),(DOMAIN-SUFFIX,adobe.io)),REJECT", // 阻断 adobe.io 所有 UDP 流量(含 QUIC/443),强制回退 TCP
"AND,((NETWORK,UDP),(DOMAIN-SUFFIX,adobestats.io)),REJECT", // 阻断统计域所有 UDP 流量(含 QUIC/443)
"AND,((NETWORK,UDP),(DOMAIN-SUFFIX,adobe.com)),REJECT", // 阻断 adobe.com 所有 UDP 流量(含 QUIC/443)
`AND,((NETWORK,UDP),(DOMAIN-REGEX,${_ADOBE_RAND_RE_STR})),REJECT`, // 阻断随机子域 QUIC(遥测特征,8-12位,引用 _ADOBE_RAND_RE_STR 统一引用源)
// ⚠️ 转义链路:JS 字符串 "\\." → 字符串值 "\." → Mihomo Go regexp 接收 \. → 匹配字面点。
// AND 规则内嵌 DOMAIN-REGEX 的括号解析基于 Mihomo v1.15+ 实测;旧版可能静默忽略整条 AND 规则,
// 此时 adobeUdpBlock 其余精确条目(DOMAIN-SUFFIX)仍有效,此条失效不影响整体覆盖。
"AND,((NETWORK,UDP),(DST-PORT,443),(DOMAIN-KEYWORD,adobe)),REJECT", // 兜底:UDP + 443端口 + adobe 关键词,覆盖未列举子域
// ⚠️ 可靠性存疑:纯 UDP 流量无 TLS SNI 时,DOMAIN-KEYWORD 可能无域名信息可供匹配,
// Mihomo 需开启 Sniffer(dns.sniffer)解析 QUIC 握手 SNI 才能识别域名;
// 实际生效取决于 Mihomo 版本,不可作为唯一防线,上方精确规则为主要覆盖。
//
// ⚠️【ECH 架构级边界——仅适用于绕过 Mihomo DNS 的场景。详见 adobeSharedDeps 注释中的路径A/B 分析】
];
// Adobe WebSocket 遥测(2025 年新增:以 WSS(WebSocket Secure)建立持久 TCP 长连接上传遥测;
// WSS 握手阶段走标准 HTTP Upgrade 请求,升级后转为全双工 WebSocket 持久连接,
// 与常规 HTTP 轮询/REST 调用模式不同;adobeUdpBlock 仅覆盖 UDP,此 TCP 路径须单独注入 DOMAIN 规则拦截)
// ⚠️ 使用 DOMAIN 精确匹配(而非 DOMAIN-SUFFIX):WSS 走 TCP,而 adobeUdpBlock 仅覆盖 UDP,无法拦截此类流量;
// 目前仅有此一个已知端点,无多级子域的抓包证据,保守使用精确匹配,等待后续抓包资料支持后再评估是否扩展。
const adobeWsDomain = [ // 如后续抓包发现更多 WSS 端点,在此数组补充
"wss.adobe.io", // 前缀 wss 推断为 WebSocket Secure 遥测端点,待抓包确认(新版 CC 框架)。wss 仅 3 字符,不满足随机子域长度正则,必须显式列出
];
// 🔓 ─────────────── Firefly 生成式 AI 专属放行域名(不含 adobeSharedDeps)───────────────
// 原则:精确放行 Firefly AI 请求,保留其余激活/遥测域名的拦截。
//
// 【域名分类】
// Firefly 依赖端点集:已统一到 adobeSharedDeps(统一引用源),此处仅含 Firefly/Clio/Sensei 专属 AI 域名。
// 用于 Adobe AI 生成式填充,需在拦截层中优先放行,走代理以确保可用性:
// firefly.adobe.com / firefly.adobe.io / firefly-api.adobe.io /
// firefly-cliov2.adobe.com / clio.adobe.io / clio-prober.adobe.io /
// clio-assets.adobe.com / senseicore.adobe.io / senseimds.adobe.io
//
// ⚠️【必要妥协】adobeSharedDeps 同时承载 CC 正版验证心跳,
// 放行后激活拦截的最终防线为 PROCESS-NAME,AdobeGCClient.exe → REJECT-DROP(需 ENABLE_PROCESS_RULE=true + TUN 模式 + 管理员权限,进程规则本身不可靠)。
// 其余未覆盖进程详见 adobeSharedDeps 注释中的 Firefly 必要妥协。
// 关于 adobeUdpBlock 与 Firefly .adobe.io 域名的 QUIC 豁免机制:
// 最终规则池展开顺序(由 LAYER_ORDER 决定,allow → block,与 push 调用书写顺序无关):
// adobeSharedDeps+adobeFireflyOnly(allow 层)→ adobeSuffix → adobeRegex → adobeUdpBlock(block 层)isFireflyActive=true 时,
// allow 层的精确 DOMAIN-SUFFIX 规则(如 firefly-api.adobe.io / clio.adobe.io 等)已在 adobeUdpBlock 之前入 pool。
// Mihomo first-match(首条命中生效):Firefly 域名的 UDP 流量先命中 allow 层走代理,adobeUdpBlock 的 AND,((NETWORK,UDP),(DOMAIN-SUFFIX,adobe.io)) 不再执行。
// → 豁免效果由注入顺序自动保证(allow 层先于 adobeUdpBlock 入 pool,先命中即生效),无需额外处理。⚠️ 前提:此豁免仅在 Mihomo 能识别 SNI 或存在 Fake-IP 映射时成立。
// ⚠️ ECH 路径分析同上,详见 adobeSharedDeps 注释。
const adobeFireflyOnly = [
// Firefly AI 核心。
"firefly.adobe.com", // Firefly 主服务入口
"firefly.adobe.io", // Firefly API(.io 端点)
"firefly-api.adobe.io", // PS 生成式填充调用入口
"firefly-cliov2.adobe.com", // Firefly Clio v2 模型接口
// Clio 生成模型。
"clio.adobe.io", // Clio 生成模型主接口
"clio-prober.adobe.io", // Clio 功能可用性探针
"clio-assets.adobe.com", // Clio 生成结果资源 CDN(内容分发网络)
// Sensei AI 平台。
"senseicore.adobe.io", // Sensei AI 服务核心
"senseimds.adobe.io", // Sensei 模型分发服务(MDS = Model Distribution Service)
];
// ─────────────────────── CorelDRAW 全家桶激活拦截 ───────────────────────
// ⚠️ 不拦截整个 corel.com,否则官网无法访问(见 directRules)
const corelSuffix = [
"activation.corel.com", // 激活验证入口
"licensing.corel.com", // 许可证服务
"license1.corel.com", // 许可证服务器 1
"license2.corel.com", // 许可证服务器 2
"mc.corel.com", // 会员验证
"ipm.corel.com", // In-Product Messaging(产品内弹窗消息)服务
"ipm2.corel.com", // IPM 备用节点
"telemetry.corel.com", // 统计上报
"world.corel.com", // 消息推送 + 序列号黑名单检查
];
// ───────────── Autodesk (CAD / 3dsMax / Maya) 激活与遥测拦截 ─────────────
const autodeskSuffix = [
"adlm.cloud.autodesk.com", // 许可验证主域(最重要,ADLM = Autodesk Desktop Licensing Module)
"adlm-autodesk.com", // ADLM 独立许可域
"licensing-autodesk.com", // 许可证服务备用域
"api.entitlements.autodesk.com", // 授权 API 接口
"telemetry.autodesk.com", // 遥测上报
"usage.autodesk.com", // 使用统计上报
"metric.autodesk.com", // 性能指标上报
"crashreport.autodesk.com", // 崩溃报告上传
"dlm.autodesk.com", // Download Manager(下载管理器)版本检查
"adsklicensing.com", // Autodesk 许可服务独立域
"clic.autodesk.com", // 核心授权验证(CLIC 推测为 Cloud LICensing 缩写,无官方资料确认)
"genuine-software.autodesk.com", // 正版验证服务
"edge.activity.autodesk.com", // 活动/行为追踪
"developer.api.autodesk.com", // 开发者 API(含许可验证)
"autodesk.com.edgekey.net", // Akamai CDN 节点(授权验证回源;同时承载官网静态资源和更新下载,如官网访问异常,可注释此条并改用进程规则兜底)
"crp.autodesk.com", // 云渲染授权(CRP = Cloud Rendering Platform)
"autodesk.flexnetoperations.com", // Revenera FlexNet Operations 许可云平台(Autodesk 租户子域,第三方托管)
];
// Autodesk 精确域名匹配(防误伤子域,不用 SUFFIX)。
const autodeskDomain = [
"ipm-aem.autodesk.com", // 弹窗消息(精确匹配,防误伤子域)
];
// DOMAIN-KEYWORD 杀伤力较强,仅针对 Autodesk 特有模块关键词。
//
// ──────────── BLOCK vs AGGRESSIVE 重叠说明(设计意图,禁止清理)────────────
// "entitlement.autodesk" 同时出现在:
// (1) autodeskKeyword(此处)→ ENABLE_BLOCK=true 时生效,REJECT
// DOMAIN-KEYWORD 为子串匹配,覆盖域名中含 "entitlement.autodesk"(含点)的域名,如 entitlement.autodesk.com。
// ⚠️ 注意:api.entitlements.autodesk.com 因含 "entitlements"(entitlement 后跟 s 再跟点),
// 子串 "entitlement.autodesk"(entitlement 后直接跟点)在其中不存在,不被此 KEYWORD 命中。
// 简言之:匹配 entitlement.autodesk.com,但不匹配 entitlements.autodesk.com(复数形式,不同 API 端点)。
// 该域名由 autodeskSuffix 中的 DOMAIN-SUFFIX 精确条目独立覆盖,两者不可互相替代。
// (2) aggressiveRules → DOMAIN-SUFFIX,entitlement.autodesk.com,REJECT-DROP 仅在 ENABLE_AGGRESSIVE=true 时额外生效。
//
// 两种开关状态下的行为分析:
// ENABLE_BLOCK=true, ENABLE_AGGRESSIVE=false(默认):
// → autodeskKeyword REJECT 先命中,aggressiveRules 不注入,无冲突
// → "entitlement.autodesk" 是 entitlement.autodesk.com 的唯一覆盖,必须保留
// ENABLE_BLOCK=true, ENABLE_AGGRESSIVE=true:
// → autodeskKeyword REJECT(KEYWORD 规则)先于 aggressiveRules SUFFIX(SUFFIX 规则)命中(pool 注入顺序决定)
// → aggressiveRules 中的 entitlement.autodesk.com SUFFIX 规则被遮蔽,实质冗余但无害
// ENABLE_BLOCK=false, ENABLE_AGGRESSIVE=true(极少使用):autodeskKeyword 不注入,aggressiveRules SUFFIX 独立生效,此时两者各司其职,无冲突
//
// 结论:重叠为纵深覆盖,在所有开关组合下均无副作用,无需合并或删除任一条目。删除单条(如认为 SUFFIX 已覆盖可删 KEYWORD)在 ENABLE_BLOCK=true,
// ENABLE_AGGRESSIVE=false(默认配置)下会产生漏拦截 Bug,此时 KEYWORD 是唯一覆盖,SUFFIX 在 AGGRESSIVE 层未注入。
// ─────────────────────────────────────────────────────────────
const autodeskKeyword = [
"adlm", // Autodesk Desktop Licensing Module(桌面许可证模块)
// ⚠️ 因 SUFFIX 规则注入顺序在 KEYWORD 之前(pushSuffix 先调用),first-match 语义下 SUFFIX 先命中。
// KEYWORD "adlm" 额外覆盖 autodesk 体系外含 adlm 子串的第三方域名;已知此类域名不存在,属防御性冗余,可接受。
"telemetry.autodesk", // Autodesk 遥测模块关键词兜底
"entitlement.autodesk", // Autodesk 授权模块关键词兜底(见上方 BLOCK vs AGGRESSIVE 说明注释块)
];
// ─────────────── 第三方非官方修改补丁后门(高危,强烈建议保留)───────────────
// 这些域名会回传设备信息,甚至下发新的远程控制指令。
const backdoorSuffix = [
"966v26.com", // 非官方修改补丁后门主域(回传设备信息)
"vposy.com", // 知名非官方修改补丁作者域名(Adobe/Office)
"api.pzz.cn", // 国内非官方修改补丁回传接口
// "cc-cdn.com", // 【待观测】命名形似 Adobe CC CDN,无抓包证据,保守纳入;可信度低于前三条。若误命中合法 CDN,会导致启动卡顿
];
// 关键词兜底:覆盖 966v26.net / cdn.966v26.org 等非 .com TLD(顶级域名,Top-Level Domain)变种,REJECT-DROP 策略与 backdoorSuffix 一致。
// ⚠️ 误命中风险评估:"966v26" 为高特异性域名特征字符串(抓包来源),在已知合法域名中无任何同名子串,实际误命中概率极低。
// 若将来发现误命中(如某 CDN 域名恰好含此子串),可将 REJECT-DROP 改为 REJECT 降低影响面;但鉴于字符串高度随机性,此情形极不可能发生,当前策略可接受。
const backdoorKeyword = ["966v26"];
// ──────────── IDM / Bandicam / Wondershare 等其他软件激活拦截 ────────────
const idmSuffix = [
"registeridm.com", // IDM 注册验证域
// "internetdownloadmanager.com", // ⚠️ 已注释:拦截主域误伤官网,改用下方精确子域
"secure.internetdownloadmanager.com", // 序列号验证接口
"mirror.internetdownloadmanager.com", // 更新镜像服务器
"mirror2.internetdownloadmanager.com", // 更新镜像服务器
"mirror3.internetdownloadmanager.com", // 更新镜像服务器
"idm-patch.com", // IDM 非官方修改补丁域(安全风险)
"idm-update.com", // IDM 非官方更新域(安全风险)
];
const idmKeyword = [
"tonec", // IDM 开发商 Tonec Inc. 的品牌名,覆盖 tonec.com 等序列号验证相关子域。
];
const wondershareSuffix = [
"activation.wondershare.com", // Wondershare 激活验证入口
"license.wondershare.com", // 许可证验证服务
"wondershare.cc", // Wondershare 海外追踪/统计域
"wondershare.cn", // Wondershare 国内遥测/统计域
// "iskysoft.com", // ⚠️ 已注释:主域即官网,无已知专用验证子域,拦截主域将导致官网无法访问。如有抓包确认的验证子域,请替换为精确条目。
// "imyfone.com", // ⚠️ 已注释:同上,主域即官网,无已知专用验证子域。
];
// 所有注释条目均因误伤官网而改用精确 DOMAIN 匹配,已移至 miscSoftwareDomain。
const miscSoftwareSuffix = [
// "bandicam.com", // ⚠️ 已注释:主域误伤官网,改用下方精确子域
// "bandisoft.com", // ⚠️ 已注释:主域误伤官网,改用下方精确子域
// "xmind.app", // ⚠️ 已注释:主域误伤官网(含正版用户同步/分享功能),改用下方精确子域
// "xmind.net", // ⚠️ 已注释:主域误伤官网(XMind 8 下载/插件),改用下方精确子域
// "listary.com", // ⚠️ 已注释:主域误伤官网,改用下方精确子域
// ⚠️ typora.io 是官网主域,直接拦截会导致插件/主题无法下载。精确拦截授权验证子域,放行主站:typora.io / store.typora.io
];
const miscSoftwareDomain = [
// ──────────────────────── Bandisoft 家族 ────────────────────────
"cert.bandicam.com", // Bandicam 正版证书/激活验证核心
"ssl.bandisoft.com", // Bandizip/Bandicam 全家桶授权验证核心
"dl.bandisoft.com", // 更新下载/版本心跳(不影响离线使用;如需更新可临时放开)
// ───────────────────────────── XMind ─────────────────────────────
// 来源:多份抓包记录及 hosts 屏蔽教程(CSDN / 博客园 / 52pojie)
// XMind 2020+(Electron)与 XMind 8(Java)均通过以下域名验证授权:
"www.xmind.app", // XMind 2020+ 授权验证主接口(Electron 版)
"www.xmind.net", // XMind 8 授权验证接口(Java 版)/ 更新检查
"www.xmind.cn", // XMind 中文站授权验证 / 国内更新检查
"dl2.xmind.cn", // XMind 8 更新安装包下载 CDN(版本检查由 www.xmind.net 触发,此域仅承载安装包分发)
// ⚠️ 扩展提醒:如需追加其他 XMind 子域,请注意 api.xmind.net / api.xmind.app 等 API 端点可能承载功能性请求(而非仅授权验证),拦截前应抓包确认,避免影响正常使用。
// ──────────────────────────── Listary ────────────────────────────
// 来源:社区抓包记录(非官方文档),support 子域为目前唯一有记录的联网端点。其他子域名(api.listary.com 等)无公开资料,不添加以免误判。
"support.listary.com", // 激活/授权验证接口(精确匹配,防误伤主站)
// ──────────────────────── WinRAR (RARLAB) ────────────────────────
// 来源:CVE-2021-35052 安全报告;Wireshark/Burp 抓包记录;rarlab.com 官网。
"notifier.rarlab.com", // 广告弹窗 / 试用到期通知页面。CVE-2021-35052:该域曾被中间人攻击利用执行任意代码。屏蔽同时消除安全风险 + 关闭广告弹窗。
// ──────────────────────────── Typora ────────────────────────────
"license.typora.io", // Typora 授权验证接口
"verify.typora.io", // Typora 激活校验
];
// ────────────────── 微软 & Office 遥测(不影响正常使用)──────────────────
// 微软遥测改用 REJECT(立即返回 RST,避免 TCP 重传开销)。
const msTelemSuffix = [
"telemetry.microsoft.com", // Windows/Office 遥测主域
"v20.events.data.microsoft.com", // Windows 诊断数据 v2.0
"v10.events.data.microsoft.com", // Windows 诊断数据 v1.0
"nexus.officeapps.live.com", // Office 遥测上报
"officeclient.microsoft.com", // Office 客户端统计
"vortex.data.microsoft.com", // Windows 错误报告
"settings-win.data.microsoft.com", // Windows 诊断数据上报端点(非设置同步;settings-win 为历史命名,实为诊断遥测)
"watson.telemetry.microsoft.com", // Watson 崩溃报告服务
];
// ────────────────────────── 国产广告联盟 / 遥测 ──────────────────────────
const cnAdSuffix = [
// WPS
"ups.k0s.gk.kingsoft.com", // WPS 升级推送服务
"pcfg.wps.cn", // WPS 配置/广告下发
"wps.com.cn", // WPS 备用主域(.com.cn 为金山注册的备用主域)
// ⚠️ 全域 SUFFIX 拦截(含所有子域);无抓包资料确认其子域仅含遥测端点,拦截后可能影响账号类或功能类子域。
// 若发现登录异常,可改用精确子域 DOMAIN 匹配(与 360.cn 的保守策略一致:主域放行,仅拦截已确认的遥测子域)
"wpsgold.wpscdn.cn", // WPS 广告资源 CDN(内容分发网络)
// "sync.wps.cn", // ⚠️ 已注释:WPS 云文档同步,拦截后云同步失效
// 海康威视(仅精确子域,主域不拦截)
// ⚠️ 若使用海康摄像头/NVR/DVR 设备,建议注释以下三条:
// upgrade.hikvision.com 拦截后设备无法检测固件更新。
// ezdns.hikvision.com 拦截后 DDNS(Dynamic DNS,动态域名解析)功能失效,远程访问中断。
// cloudmsg.hikvision.com 拦截后萤石云/APP 推送通知失效。
"upgrade.hikvision.com", // 海康固件升级检查(可触发静默下载)
"ezdns.hikvision.com", // 海康 DDNS(动态域名解析)回传(拦截后远程访问中断)
"cloudmsg.hikvision.com", // 海康云消息推送
// 向日葵远程(仅遥测子域,oray.com 主域不宜拦截)
"sunloginlog.oray.com", // 向日葵日志上报
"report.oray.com", // 向日葵行为上报
// ToDesk 远程。
"log.todesk.com", // ToDesk 日志上报
"report.todesk.com", // ToDesk 遥测上报
// 百度输入法。
"shurufa.baidu.com", // 百度输入法云服务
"input.baidu.com", // 百度输入法联网同步
// 搜狗输入法(精确子域补充,主域 sogou.com 不拦截)
// "api.sogoucloud.com", // ⚠️ 已注释:搜狗输入法云端接口,域名拼写无公开抓包资料确认,待验证后启用
// 腾讯 Bugly 崩溃上报 SDK(Software Development Kit,软件开发工具包;大量国产软件集成,含设备指纹)
"bugly.qq.com", // 腾讯 Bugly 崩溃上报 SDK
"bugly.gtimg.com", // 腾讯 Bugly 管理后台使用的静态资源 CDN
// 字节跳动系(抖音/剪映/头条/西瓜共用)
"log.snssdk.com", // 字节系客户端日志上报(头条/西瓜等)
"i.snssdk.com", // 字节跳动国内 SDK 主接口域(⚠️ 非单纯遥测:含账号认证、功能 API 等,拦截后可能导致字节系 APP 功能性断连,非仅屏蔽上报)
"log.byteoversea.com", // 字节跳动海外日志上报(抖音/剪映共用)
// 剪映专业版(CapCut)
"metrics.capcut.com", // 剪映遥测上报
"log.capcut.com", // 剪映日志收集
// QQ音乐。
// "qqmusic.qq.com", // ⚠️ 待验证:命名无遥测特征前缀,可能是功能性主域,抓包确认前暂不拦截