ghprobe 0.1.3 → 0.1.6

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/CHANGELOG.md CHANGED
@@ -5,6 +5,176 @@
5
5
  格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
6
6
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
7
7
 
8
+ > **版本号说明**:npm 发布采用 `0.1.x` 版本线(`npm i -g ghprobe` 装到的就是它);
9
+ > 0.1.5 及之前的 npm 包内含仓库内部 0.2~0.6 的全部能力(对应关系:npm 0.1.5 ≈ 内部 0.6.0)。
10
+ > **自 0.1.6 起条目直接使用 npm 版本号**,内部版本号不再单独记录。
11
+
12
+ ## [0.1.6] - 2026-11-13
13
+
14
+ ### Added
15
+
16
+ - **`ghprobe off` / `ghprobe on`:一键暂停与恢复(不卸载)**。off = 剥掉 hosts 块 +
17
+ 暂停计划任务 + 落暂停标记,daemon / 健康检查见到标记直接跳过(连探测请求都不发);
18
+ on = 反向恢复并当场触发一轮健康检查,约 1 分钟内重新写好 hosts。
19
+ 适合出差、换网、临时装了代理工具的场景,不用整个 uninstall。
20
+ 手动 `probe --apply` 是显式意图不受门约束,但会提示当前处于暂停状态。
21
+ - **`ghprobe report`:报障诊断包**。版本 / 系统 / 暂停状态 / status / 配置差异 /
22
+ hosts 块 / 自愈日志尾部一次收集,自动复制到剪贴板并落盘
23
+ (`%LOCALAPPDATA%\ghprobe
24
+ - **更新检查**:交互命令(install / status / doctor / probe / report)每天最多查一次
25
+ npm registry(npmmirror 优先、官方源兜底、5s 超时、失败静默),有新版提示
26
+ `npm i -g ghprobe`。只提示不自动升级;daemon / 计划任务静默路径绝不联网查版本。
27
+
28
+ ### Fixed
29
+
30
+ - **自愈日志无限膨胀**(重要):daemon / 监督者每 15~60s 追加一行,日志此前只增不减,
31
+ 长跑机器实测可攒到几十 MB。现在追加前自动滚动:超过 2MB 只保留最后 256KB,
32
+ 老用户升级后下次写入自动截断,无需手动清理。
33
+
34
+ ### Changed
35
+
36
+ - `status` 在暂停状态下首行显示恢复指引;hosts 块被 off 剥掉时不再误导用户「先跑 install」。
37
+ - `uninstall` 顺带清掉暂停标记(重装 = 全新开始)。
38
+
39
+
40
+ ## [0.6.0] - 2026-10-31
41
+
42
+ ### Added
43
+
44
+ - **用户行为感知的切换时机算法(`src/algorithm.js`)**:换 IP 不是零成本 —— flushdns +
45
+ 连接池重建,正在浏览的页面会卡一下。daemon 现在按「打扰成本 vs 紧急度」决定何时落盘:
46
+ - **成本权重**(可配 `algorithm.weights`):前台是浏览器 100 > 浏览器在后台跑 30 >
47
+ 前台是其他软件 10 > 空闲 0;
48
+ - **P0 不吃权重**:当前主备全死、用户此刻真的断着的域名 → **立即切**。「正在用」
49
+ 和「不能用」二选一,当然选别断网;首次写入(无从打扰)同此;
50
+ - **非紧急优化类**(提上备选 / 重排优化 / 补缺失 / 兜底写入)在前台是浏览器时**推迟**,
51
+ 攒成待写块由 daemon 每轮检查,用户空闲后落地;推迟上限 `deferMaxMs`(默认 10 分钟)防饿死。
52
+ - 检测免管理员:`tasklist` 扫浏览器进程(名单可配)+ PowerShell `GetForegroundWindow`
53
+ 查前台窗口;结果短缓存(beforeApply 同轮复用,空闲落地走新鲜检测)。
54
+ - 只作用于 daemon 自动写入;手动 `health --apply` / `probe --apply` 是显式意图,不设门。
55
+ - 顺带修正:`fullUpdate` 写入时把 `hostsPath` 传给 `applyHosts`(此前健康检查的写路径
56
+ 测试注入会被忽略,默认恰好等于真实 hosts 才未暴露)。
57
+ - 单元测试 192 → **201**(tasklist 解析 / 成本权重 / 决策矩阵 / 门控与空闲落地,全注入)。
58
+
59
+ ## [0.5.0] - 2026-10-31
60
+
61
+ ### Added
62
+
63
+ - **常驻 daemon(无缝切换的核心)**:`ghprobe daemon` 以 15~45s **自适应循环**持续自愈——
64
+ 全部健康 45s、有异常/stuck/出错 15s、断网 120s。取代「每分钟起一个新进程」的固定节拍:
65
+ 故障发现不再等下一分钟,修复全程无进程启动开销。实测架构目标:抖动窗口结束后的
66
+ 恢复延迟从 ~6 分钟量级压进 ~1 分钟以内。
67
+ - 每轮写心跳文件(`daemon.heartbeat`);单实例:发现更年轻的活实例自动让位;
68
+ - 每轮重载配置:编辑 `config.yaml` 即时生效;
69
+ - 循环体异常不退出(按 15s 快档重试)。
70
+ - **监督者架构(零迁移成本)**:现有的每分钟计划任务**原地降级**为监督者——
71
+ 心跳新鲜 → 秒退(本轮交给 daemon);心跳陈旧/没有 → **自动拉起新 daemon** + 本轮照常兜底。
72
+ daemon 崩溃 → 下一分钟自动复活。旧版本用户升级 npm 包后自动获得,**无需重装、无需新 UAC**。
73
+ 手动诊断用 `ghprobe health --force` 绕过监督逻辑。
74
+ - **status 显示常驻守护状态**:`常驻守护:运行中(pid …,心跳 Ns 前)` 一眼可见。
75
+ - 单元测试 185 → **192**(心跳/单实例/自适应间隔/主循环全注入,不打网络不真睡)。
76
+
77
+ ## [0.4.0] - 2026-10-31
78
+
79
+ ### Changed
80
+
81
+ - **🔴 修正 `raw.githubusercontent.com` 探测判据路径(潜伏 bug)**:
82
+ `DOMAIN_SIG` 里的 `/torvalds/linux/README.md` 在真 GitHub 上是 **404** ——
83
+ raw 的第 2 段是分支名,无分支的路径会被当作「README.md 分支」。就算网络全通,
84
+ 该判据也会把活 IP 判死(假阴性)。修正为 `/torvalds/linux/master/README`
85
+ (实测 200 且含 "Linux kernel";linux 仓库根文件本来就叫 `README`,无 `.md`)。
86
+ - **默认兜底链按实测数据重写**(jsDelivr 官方多 CDN,同一份内容、官方运营、无中间人):
87
+
88
+ | 替身 | 2026-10-31 实测 |
89
+ | --- | --- |
90
+ | `fastly.jsdelivr.net`(新,首选) | Fastly anycast 151.101.x.229,node+双 curl 全栈全 IP **100%**,TTFB ~125ms |
91
+ | `testingcf.jsdelivr.net`(新) | Cloudflare 段,100%,与 Fastly 真正异构 |
92
+ | `gcore.jsdelivr.net`(新) | 100%,但当前与 testingcf 解析到同一对 CF IP,纯语义冗余 |
93
+ | ~~`cdn.jsdelivr.net`~~(移除) | **0/45**:TCP 全通、ClientHello 后即 RST(SNI 硬封),换 IP/换 TLS 栈均无效 |
94
+
95
+ 三个替身都以**完整写法**入配置(自带 `/gh/torvalds/linux@master/README` + mustContain 判据,
96
+ 它们不在代码 DOMAIN_SIG 里,简写会退回探根路径的弱判据)。
97
+ 老用户没改过 `fallbacks` 的自动跟随新默认;改过的用 `ghprobe config-diff` 查看。
98
+
99
+ ### Added
100
+
101
+ - **`ghprobe status` 秒级速查**:不打任何网络请求,汇总「hosts 块现状(每域名主 IP + 备选 +
102
+ 缺失/阻断标记)+ 计划任务状态 + 自愈日志尾部」。与 doctor 的分工:日常「现在什么情况」用
103
+ status(<1s),坏了要诊断再用 doctor(~1 分钟)。
104
+ - **候选池拉取自救**:`updatePool` 支持**健康 hosts 条目当备用种子**(排在静态 seed 之前)——
105
+ 它们刚通过三关 + HTTP 终审,是「当下确实能直连 GitHub」的 IP;seed 池被 SNI 整段封锁时
106
+ 这批 IP 往往是唯一还拉得到 `api.github.com/meta` 的入口,池子不再永久冻结。
107
+ `health` 与 `update-pool` 两条路径都已接入。
108
+ - 单元测试 178 → **185**。
109
+
110
+ ### Investigated(按用户要求实测,结论如实记录)
111
+
112
+ - **Cloudflare 1.1.1.1:当前网络全封死,不可用** —— UDP 53 超时、TCP 53 超时、
113
+ DoH 直连 IP 超时、`cloudflare-dns.com` 秒 RST。另勘测确认本机 System32 curl 与 Git
114
+ mingw64 curl 均为 Schannel 构建(与「mingw=OpenSSL」的既有假设不符,栈对比结论不受影响);
115
+ 系统 DNS(UDP 53 出站)全部不可用,DoH 是唯一解析通道 —— 印证了本工具「DoH 端点 IP 直连」的设计。
116
+
117
+ ## [0.3.0] - 2026-10-31
118
+
119
+ ### Added
120
+
121
+ - **`github.githubassets.com` 加入默认域名族**:github.com 网页的全部 JS/CSS/字体都从这里
122
+ 加载 —— 不写它「页面能打开但样式全裂」,是直连浏览最常见的体验杀手。
123
+ 🔴 它的 Fastly 段是 `185.199.108-111.215`,与 githubusercontent 系的 `.133` **不同段**
124
+ (实测 `.133` 对该 SNI 直接连接失败),所以单独建 `ghassets` 池。
125
+ 判据用 `GET /favicons/favicon.svg`(实测 4 个 .215 IP 全部 200 且以 `<svg` 开头)。
126
+ - **`ssh.github.com` 加入默认域名族(SSH-over-443)**:`git clone git@ssh.github.com:...`
127
+ 的入口,SNI 封锁下最稳的 SSH 通道。探测类型新增 `type:"ssh"`:服务端先发 banner,
128
+ 读到合法 `SSH-2.0-*` 即活,不走 TLS/HTTP 三关(`sshProbe`)。
129
+ - **schannel 二次重探**:Node 的 TLS ClientHello 被中间设备识别并 reset 时(README §3 实测:
130
+ 同一 IP 同一时刻 openssl 20/20、Node 5/20、浏览器正常),改用 **Windows 自带的 curl.exe
131
+ (Schannel 构建,与 Edge 同一 TLS 栈)** 重探一次。为此 probeIp 的端口改为可注入。
132
+ 重探结果的语义随之升级:
133
+ - schannel 通 → 写入 hosts(0.3.0 前这种情况只能写 `# blocked` 放弃,而浏览器明明能用);
134
+ - schannel 后 HTTP 层仍 4xx → 假活,与指纹无关,不标 blocked;
135
+ - schannel 后连接层仍被 reset → **连浏览器同款栈都过不去**,`# blocked` 从此是
136
+ 「浏览器栈验证过确实被拦」的强结论,不再可能是客户端指纹的假阴性。
137
+ 🔴 必须显式定位 System32 的 curl.exe:PATH 前面的可能是 Git 自带的 OpenSSL 构建
138
+ (实测 mingw64 curl 走 OpenSSL,指纹又变回去)。
139
+ - **排名双采样抗抖动**:对每域名前 3 名再采一枪,`mergeSamples` 按均值 + 失败罚分
140
+ (500ms/次)重排名。单采样时同一 IP 相邻两轮分差可达数倍、排名近乎随机,
141
+ 而无谓的换 IP 要刷 DNS 缓存、断连接池 —— 排名稳比单次「最优」更重要。
142
+ `probe.stableSamples: 2`(默认开),设 1 退回旧行为。
143
+ - 单元测试 166 → **178**(SSH banner 解析/探测、schannel 注入路径、双采样合并、
144
+ githubassets 判据;全部本地 socket 与注入替身,不打真网络)。
145
+
146
+ ### Live validation (2026-10-31, 真实网络)
147
+
148
+ - 首轮 probe:`ssh.github.com` 经 banner 判活(20.205.243.160,54ms);
149
+ `github.githubassets.com` 4 个 .215 全活(最优 score 36);`api.github.com` /
150
+ `github.com`(双采样后 4 个可用)正常。
151
+ - `raw/gist/avatars.githubusercontent.com`(.133 段)本轮全灭 —— schannel 重探同样被
152
+ connection reset,证实是网络层对该批 SNI 的真实封锁,**不是工具的指纹假阴性**;
153
+ 工具如实标 blocked/absent 并走冷却,这正是设计行为。
154
+
155
+ ## [0.2.0] - 2026-10-30
156
+
157
+ ### Added
158
+
159
+ - **`ghprobe doctor` 一键自检**:把「本地网络 / DoH 解析 / 逐域名探测 / hosts 写入状态 /
160
+ 计划任务」五层各查一遍,逐项给人话结论 + 下一步建议(如「跑 `ghprobe update-pool`」
161
+ 「重跑 `ghprobe install`」)。定位:GitHub 连不上时,普通人不该被要求去读日志——
162
+ 一条命令看懂哪层坏了、该做什么。有 fail 项时退出码非零,方便脚本消费;
163
+ `# blocked` 类域名只标 warn 不算失败(浏览器可能仍可用,不该吓退用户)。
164
+ 评估逻辑拆成纯函数、编排依赖可注入,单测不打真网络。
165
+ - **`api.github.com` 加入默认域名族**:gh CLI(`gh api`)与 REST API 直连提速。
166
+ DNS 同样被污染,此前 hosts 不管它。判据用 `GET /rate_limit`(未认证也返回 200,
167
+ 响应体必含 `rate` 字段,不依赖网页特征);候选走 seed/auto 池(GitHub 自租段)。
168
+ 老用户升级后经三方合并自动获得该域名(没动过 `domains` 键的话)。
169
+ - **DoH 端点增加 DNSPod 第二入口 `120.53.53.53`**(与 `1.12.12.12` 同证书 `doh.pub`):
170
+ 单端点抖动时解析不再整体失明。真实网络实测 4/4 端点可用。
171
+ - 单元测试 153 → **166**。
172
+
173
+ ### Notes
174
+
175
+ - `ghprobe serve` 的 HTTP API 保持不变(`POST /api/probe`、`GET /api/status`、
176
+ `GET /api/hosts`、`GET /api/config`),供脚本消费。
177
+
8
178
  ## [0.1.3] - 2026-10-12
9
179
 
10
180
  ### Changed
package/README.en.md CHANGED
@@ -43,7 +43,7 @@ It provides **no** proxy, tunnel, traffic forwarding, or protocol evasion, and i
43
43
 
44
44
  **① Local DoH resolution, bypassing system-DNS poisoning**
45
45
 
46
- Candidates do not come from system DNS (which may be poisoned); they come over DoH (RFC 8484 wire format, with no UDP/53 path at all). The defaults are endpoints verified reachable from mainland China: Alibaba DNS (`223.5.5.5` / `223.6.6.6`) and DNSPod (`1.12.12.12`).
46
+ Candidates do not come from system DNS (which may be poisoned); they come over DoH (RFC 8484 wire format, with no UDP/53 path at all). The defaults are endpoints verified reachable from mainland China: Alibaba DNS (`223.5.5.5` / `223.6.6.6`) and DNSPod (`1.12.12.12` / `120.53.53.53`).
47
47
 
48
48
  Key detail: **DoH endpoints are reached by IP directly, with SNI validation.** If reaching a DoH endpoint itself required resolving its hostname first, you would fall into the deadlock of "resolving the DoH hostname requires resolving first." So the config stores the endpoint's IP, and a `sni` field carries the service hostname for certificate validation (RFC 6066 forbids using an IP as SNI), for example:
49
49
 
@@ -170,6 +170,8 @@ The last layer needs its own explanation, because it is the most easily misunder
170
170
 
171
171
  `# absent` = probed, genuinely no usable IP. `# blocked` = the local probe client got fingerprinted; your browser may well work.
172
172
 
173
+ > **Since 0.3.0**: after a Node reset, ghprobe retries once with the built-in Windows `curl.exe` (Schannel build, **the same TLS stack as Edge**) — `probe.schannelRetry`, on by default. Retry succeeds → the entry is written to hosts normally. Retry also reset → only then is `# blocked` written, and it now means "**verified blocked with a browser-equivalent stack**", no longer a possible fingerprint false negative.
174
+
173
175
  **Fixing the fourth layer** requires packet-level techniques (TTL manipulation, decoy packets, TCP-layer desync), which need a kernel driver (tools like GoodbyeDPI). That directly conflicts with this project's "no driver, no system network path changes" trade-off, so it is **deliberately out of scope**. ghprobe's job is: **given the network you already have, pick the right IPs and keep them working.**
174
176
 
175
177
  ---
@@ -357,6 +359,10 @@ ghprobe hosts
357
359
  ghprobe health
358
360
  ghprobe health --apply # write to system hosts when a problem is detected (UAC prompt)
359
361
 
362
+ # One-shot self-check: network / DoH / probing / hosts / scheduled task,
363
+ # with a plain-language verdict and next-step hint for each item
364
+ ghprobe doctor
365
+
360
366
  # Refresh the candidate pool from GitHub's official api.github.com/meta (once a day is enough)
361
367
  # That API is limited to 60 requests/hour unauthenticated, so don't call it often.
362
368
  # health also does this on schedule, so you rarely need it manually.
@@ -431,11 +437,14 @@ ipPools:
431
437
 
432
438
  domainPools:
433
439
  github.com: [auto, seed, azure]
440
+ api.github.com: [auto, seed]
441
+ ssh.github.com: [auto, seed]
434
442
  codeload.github.com: [auto, seed, azure]
435
443
  objects.githubusercontent.com: [fastly]
436
444
  raw.githubusercontent.com: [fastly]
437
445
  gist.githubusercontent.com: [fastly]
438
446
  avatars.githubusercontent.com: [fastly]
447
+ github.githubassets.com: [ghassets]
439
448
 
440
449
  poolUpdate:
441
450
  enabled: true
@@ -452,10 +461,10 @@ networkCheck:
452
461
  probe: { host: 223.5.5.5, port: 443, timeoutMs: 2000 }
453
462
 
454
463
  fallbacks:
455
- raw.githubusercontent.com: [cdn.jsdelivr.net]
464
+ raw.githubusercontent.com: [fastly.jsdelivr.net]
456
465
 
457
466
  timeouts: { dohMs: 5000, tcpMs: 2500, tlsMs: 6000, httpMs: 6000 }
458
- probe: { maxCandidates: 8, maxPoolIps: 32, maxPerDomain: 4, concurrency: 10 }
467
+ probe: { maxCandidates: 8, maxPoolIps: 32, maxPerDomain: 4, concurrency: 10, schannelRetry: true, stableSamples: 2 }
459
468
  hosts: { maxEntriesPerDomain: 2 }
460
469
  serve: { host: 127.0.0.1, port: 8930 }
461
470
  schedule:
@@ -479,7 +488,7 @@ If you deployed via `install.ps1`, you can also use `.\install.ps1 -Uninstall`.
479
488
 
480
489
  ---
481
490
 
482
- ## 6. The fallback chain: why cdn.jsdelivr.net shows up in hosts
491
+ ## 6. The fallback chain: why *.jsdelivr.net shows up in hosts
483
492
 
484
493
  Besides the primary domains like `github.com`, ghprobe maintains a **fallback chain** for
485
494
  hostname-blocked domains: when every primary candidate is dead, it probes alternative entry
@@ -492,20 +501,22 @@ entry domain** will. GitHub file content has several equivalent entry points:
492
501
 
493
502
  ```text
494
503
  raw.githubusercontent.com/<user>/<repo>/<ref>/<path> <- the original (often blocked)
495
- cdn.jsdelivr.net/gh/<user>/<repo>@<ref>/<path> <- jsDelivr public CDN, same content
504
+ <mirror>.jsdelivr.net/gh/<user>/<repo>@<ref>/<path> <- jsDelivr's official multi-CDN, same content
496
505
  ```
497
506
 
498
- **Why only one by default**: fallback domains get blocked too (measured at the same moment:
499
- `cdn.jsdelivr.net` accepted TCP but had its TLS handshake cut), so the chain **supports
500
- multiple candidates**; but the default ships only the public CDN `cdn.jsdelivr.net` —
501
- **ghprobe presets no third-party proxy or mirror domains** (e.g. `ghproxy.net` /
502
- `gh-proxy.com`): with those, "your requests go through their server", which contradicts
503
- this tool's "no proxy" positioning and carries legal risk. Add your own if you need more
504
- (how-to below):
507
+ **The default chain since 0.4.0 (driven by 2026-10-31 measurements)**:
505
508
 
506
- | Fallback | Type | Note |
507
- | --- | --- | --- |
508
- | `cdn.jsdelivr.net` | jsDelivr public CDN (Cloudflare) | path mapping `/gh/<user>/<repo>@<ref>/<path>` |
509
+ | Fallback | Type | Measured | Note |
510
+ | --- | --- | --- | --- |
511
+ | `fastly.jsdelivr.net` | jsDelivr's official Fastly segment | **100%** across all stacks/IPs, TTFB ~125ms | first choice; stable resolution (151.101.x.229) |
512
+ | `testingcf.jsdelivr.net` | jsDelivr's official Cloudflare segment | 100% | genuinely heterogeneous from Fastly; second channel |
513
+ | `gcore.jsdelivr.net` | jsDelivr official (nominally GCore) | 100% | currently resolves to the same CF IPs as testingcf; semantic redundancy only |
514
+ | ~~`cdn.jsdelivr.net`~~ | jsDelivr official multi-CDN scheduler | **0/45, hard SNI-blocked** | removed in 0.4.0: no IP or TLS stack helps, it only burns probe timeouts |
515
+
516
+ The official jsDelivr subdomains are not third-party proxies: same content, officially
517
+ operated, no man-in-the-middle. Third-party proxies (`ghproxy.net` / `gh-proxy.com` and
518
+ alike) mean "your requests go through their server" — **ghprobe presets none**; add your
519
+ own if you need more (how-to below):
509
520
 
510
521
  > Any third-party fallback domain you add yourself is your own judgement and risk.
511
522
 
@@ -521,7 +532,7 @@ Behaviour and prerequisites:
521
532
  ```yaml
522
533
  fallbacks:
523
534
  raw.githubusercontent.com:
524
- - cdn.jsdelivr.net
535
+ - fastly.jsdelivr.net # full form (with path + mustContain) shown in config.default.yaml
525
536
  # Add your own: gate 3 uses path + mustContain to verify this IP really serves this content
526
537
  - { domain: my.mirror.example, path: "/gh/torvalds/linux@master/README", mustContain: ["Linux kernel"] }
527
538
  ```
@@ -585,7 +596,7 @@ The task registered by `install` uses `LogonType = Interactive`, so it won't fir
585
596
 
586
597
  ### Disclaimer
587
598
 
588
- This tool performs DNS preference selection and connectivity probing only. It implements and runs no proxy, tunnel, or traffic forwarding itself, and presets no third-party proxy or mirror domains in its default config (the fallback chain defaults to the public CDN `cdn.jsdelivr.net`; any other fallback must be added by the user, at their own risk). You are solely responsible for any consequences of using this tool. Please comply with the laws and regulations of your jurisdiction.
599
+ This tool performs DNS preference selection and connectivity probing only. It implements and runs no proxy, tunnel, or traffic forwarding itself, and presets no third-party proxy domains in its default config (the fallback chain defaults to jsDelivr's official subdomains `fastly` / `testingcf` / `gcore.jsdelivr.net` — official multi-CDN entries for the same content; any other fallback must be added by the user, at their own risk). You are solely responsible for any consequences of using this tool. Please comply with the laws and regulations of your jurisdiction.
589
600
 
590
601
  ---
591
602
 
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  ```bash
23
23
  npm install -g ghprobe
24
24
  ghprobe install # 探测一次 + 写入 hosts + 注册每分钟健康检查(全程只弹一次 UAC)
25
+ ghprobe off # 暂停:剥掉 hosts 块 + 停止自愈写入(不卸载;恢复用 ghprobe on)
25
26
  ghprobe uninstall # 停止:删除计划任务并还原 hosts
26
27
  ```
27
28
 
@@ -43,7 +44,7 @@ ghprobe uninstall # 停止:删除计划任务并还原 hosts
43
44
 
44
45
  **① 本地 DoH 解析,绕开系统 DNS 污染**
45
46
 
46
- 候选 IP 不来自系统 DNS(可能被污染),而是走 DoH(RFC 8484 wire format,全程无 UDP/53 路径)。默认使用国内实测可达的端点:阿里 DNS(`223.5.5.5` / `223.6.6.6`)和 DNSPod(`1.12.12.12`)。
47
+ 候选 IP 不来自系统 DNS(可能被污染),而是走 DoH(RFC 8484 wire format,全程无 UDP/53 路径)。默认使用国内实测可达的端点:阿里 DNS(`223.5.5.5` / `223.6.6.6`)和 DNSPod(`1.12.12.12` / `120.53.53.53`)。
47
48
 
48
49
  关键细节:**DoH 端点用 IP 直连 + SNI 校验**。如果连 DoH 端点本身都要先解析它的域名,就会掉进「解析 DoH 域名又要先解析」的死锁;所以配置里端点写的是 IP,同时用 `sni` 字段填服务域名做证书校验(RFC 6066 不允许拿 IP 当 SNI),例如:
49
50
 
@@ -52,6 +53,7 @@ doh:
52
53
  - { name: alidns1, host: 223.5.5.5, sni: dns.alidns.com, path: /dns-query }
53
54
  - { name: alidns2, host: 223.6.6.6, sni: dns.alidns.com, path: /dns-query }
54
55
  - { name: dnspod, host: 1.12.12.12, sni: doh.pub, path: /dns-query }
56
+ - { name: dnspod2, host: 120.53.53.53, sni: doh.pub, path: /dns-query }
55
57
  ```
56
58
 
57
59
  多个端点并发查询,成功者取并集去重,再用内置 IP 池补一批候选。
@@ -168,7 +170,12 @@ ghprobe 的取舍是:**只做「优选 IP + 自动维护」,不碰系统网
168
170
  # blocked raw.githubusercontent.com 2026-10-11T10:27:09.367Z
169
171
  ```
170
172
 
171
- 两种标记的区别:`# absent` = 探过、确实没有可用 IP;`# blocked` = 本机探测客户端被识别,你的浏览器很可能照样能用。
173
+ 两种标记的区别:`# absent` = 探过、确实没有可用 IP;`# blocked` = 探测客户端 TLS 被识别,你的浏览器很可能照样能用。
174
+
175
+ > **0.3.0 起**:Node 被 reset 后会用 Windows 自带的 `curl.exe`(Schannel 构建,
176
+ > **与 Edge 同一 TLS 栈**)重探一次(`probe.schannelRetry`,默认开)。重探通过 →
177
+ > 正常写进 hosts;重探仍被 reset → `# blocked` 才落笔,此时它意味着
178
+ > 「**浏览器同款栈验证过确实被拦**」,不再是客户端指纹的假阴性。
172
179
 
173
180
  **要解决第四层**需要包级手法(TTL 操纵、伪包、TCP 层 desync),那必须装内核驱动(类似 GoodbyeDPI 那类工具)。这跟本项目「不装驱动、不改系统网络路径」的取舍直接冲突,所以**刻意不做**。本工具的定位是:**在你现有的网络条件下,把 IP 选对、并持续维护**。
174
181
 
@@ -350,6 +357,19 @@ ghprobe hosts
350
357
  ghprobe health
351
358
  ghprobe health --apply # 检测到异常时写入系统 hosts(会弹 UAC)
352
359
 
360
+ # 一键自检:网络 / DoH / 探测 / hosts / 计划任务,逐项人话结论 + 下一步建议
361
+ # GitHub 连不上不知道哪坏了?先跑它。有 fail 项时退出码非零,方便脚本判断
362
+ ghprobe doctor
363
+
364
+ # 暂停 / 恢复(不卸载):off = 剥掉 hosts 块 + 暂停计划任务 + 停止自愈写入,
365
+ # 出差、换网、临时不想用时不用整个卸载;on = 恢复并当场触发一轮健康检查
366
+ ghprobe off
367
+ ghprobe on
368
+
369
+ # 报障诊断包:版本 / status / 配置差异 / hosts 块 / 日志尾部,
370
+ # 自动复制到剪贴板并落盘 —— 提 issue 时直接粘贴,报障成本最低
371
+ ghprobe report
372
+
353
373
  # 从 GitHub 官方 api.github.com/meta 更新候选池缓存(每天一次即可)
354
374
  # 该 API 未认证限 60 次/小时,所以别频繁调;health 到点也会自动调,一般不用手动
355
375
  ghprobe update-pool
@@ -423,11 +443,14 @@ ipPools:
423
443
 
424
444
  domainPools:
425
445
  github.com: [auto, seed, azure]
446
+ api.github.com: [auto, seed]
447
+ ssh.github.com: [auto, seed]
426
448
  codeload.github.com: [auto, seed, azure]
427
449
  objects.githubusercontent.com: [fastly]
428
450
  raw.githubusercontent.com: [fastly]
429
451
  gist.githubusercontent.com: [fastly]
430
452
  avatars.githubusercontent.com: [fastly]
453
+ github.githubassets.com: [ghassets]
431
454
 
432
455
  poolUpdate:
433
456
  enabled: true
@@ -444,10 +467,10 @@ networkCheck:
444
467
  probe: { host: 223.5.5.5, port: 443, timeoutMs: 2000 }
445
468
 
446
469
  fallbacks:
447
- raw.githubusercontent.com: [cdn.jsdelivr.net]
470
+ raw.githubusercontent.com: [fastly.jsdelivr.net]
448
471
 
449
472
  timeouts: { dohMs: 5000, tcpMs: 2500, tlsMs: 6000, httpMs: 6000 }
450
- probe: { maxCandidates: 8, maxPoolIps: 32, maxPerDomain: 4, concurrency: 10 }
473
+ probe: { maxCandidates: 8, maxPoolIps: 32, maxPerDomain: 4, concurrency: 10, schannelRetry: true, stableSamples: 2 }
451
474
  hosts: { maxEntriesPerDomain: 2 }
452
475
  serve: { host: 127.0.0.1, port: 8930 }
453
476
  schedule:
@@ -456,23 +479,26 @@ schedule:
456
479
  ```
457
480
 
458
481
 
459
- ### 卸载 / 回滚与日志
482
+ ### 卸载 / 暂停 / 回滚与日志
460
483
 
461
484
  ```bash
485
+ ghprobe off # 暂停:剥掉 hosts 块 + 暂停计划任务 + 停止自愈(不卸载)
486
+ ghprobe on # 恢复:清暂停标记 + 恢复计划任务 + 当场触发一轮健康检查
462
487
  ghprobe uninstall # 删除计划任务 + 还原 hosts(只删自己的行)
463
488
  ```
464
489
 
465
490
  如果你是用 `install.ps1` 部署的,也可以用 `.\install.ps1 -Uninstall`。
466
491
 
492
+ - **只是临时不想用**(出差、换网络环境、装了别的代理工具):用 `ghprobe off`,不用卸载 —— GitHub 立刻回到系统默认解析,自愈完全停手;`ghprobe on` 一键恢复(约 1 分钟内重新写好 hosts)。
467
493
  - **手动回滚 hosts**:写入前会备份到 `C:\Windows\System32\drivers\etc\hosts.ghprobe-bak`;但更推荐用 `ghprobe uninstall`(它按块剥离,不会误伤第三方后来追加的条目)。
468
- - **健康检查日志**:`%LOCALAPPDATA%\ghprobe\ghprobe.log`。计划任务静默运行、stdout/stderr 都被丢弃,所以每轮结果会追加到这里(含 `action` / `reason` / `changed`),跑挂了能查。
494
+ - **健康检查日志**:`%LOCALAPPDATA%\ghprobe\ghprobe.log`。计划任务静默运行、stdout/stderr 都被丢弃,所以每轮结果会追加到这里(含 `action` / `reason` / `changed`),跑挂了能查。日志**自动滚动**:超过 2MB 只保留最后 256KB,不会无限膨胀(0.1.6 起;此前长跑机器能攒出几十 MB,升级后下次写入自动截断)。
469
495
  - **用户配置**:`%LOCALAPPDATA%\ghprobe\config.yaml`(可编辑,见上一节)。
470
496
  - **候选池缓存**:`%LOCALAPPDATA%\ghprobe\pool.cache.json`(`update-pool` 自动维护,不用手改)。
471
497
  - **部署日志**:用 `install.ps1` 时默认写到 `%TEMP%\ghprobe-deploy.log`。
472
498
 
473
499
  ---
474
500
 
475
- ## 6. 兜底链:为什么 hosts 里会出现 cdn.jsdelivr.net
501
+ ## 6. 兜底链:为什么 hosts 里会出现 *.jsdelivr.net
476
502
 
477
503
  除了 `github.com` 这类主域名,ghprobe 还会给**被主机名封锁**的域名配一条**兜底链**:
478
504
  主候选全灭时改探测替身域名,并把**当下能通的那个**写进 hosts。
@@ -483,18 +509,21 @@ ClientHello 的形态(实测数据见 §3「边界」)。这种时候换任
483
509
 
484
510
  ```text
485
511
  raw.githubusercontent.com/<user>/<repo>/<ref>/<path> ← 原始入口(常被拦)
486
- cdn.jsdelivr.net/gh/<user>/<repo>@<ref>/<path> ← jsDelivr 公共 CDN,同一份内容
512
+ <mirror>.jsdelivr.net/gh/<user>/<repo>@<ref>/<path> ← jsDelivr 官方多 CDN,同一份内容
487
513
  ```
488
514
 
489
- **为什么只默认一个**:替身域名自己也会被封(实测同一时刻 `cdn.jsdelivr.net` 的 TCP 连得上、
490
- 但 TLS 被掐),所以兜底链**支持多候选**;但默认只留 `cdn.jsdelivr.net` 这一条公共 CDN ——
491
- **ghprobe 不预置任何第三方代理 / 镜像域名**(如 `ghproxy.net` / `gh-proxy.com`):
492
- 这类服务「请求会经过它们的服务器」,既违背本工具「无代理」的定位,也有法律风险。
493
- 需要更多兜底,自己加(写法见下文):
515
+ **0.4.0 默认链(2026-10-31 实测数据驱动)**:
494
516
 
495
- | 替身 | 类型 | 说明 |
496
- | --- | --- | --- |
497
- | `cdn.jsdelivr.net` | jsDelivr 公共 CDN(Cloudflare) | 路径映射 `/gh/<user>/<repo>@<ref>/<path>` |
517
+ | 替身 | 类型 | 实测 | 说明 |
518
+ | --- | --- | --- | --- |
519
+ | `fastly.jsdelivr.net` | jsDelivr 官方 Fastly 段 | 全栈全 IP **100%**,TTFB ~125ms | 首选;解析稳定(151.101.x.229) |
520
+ | `testingcf.jsdelivr.net` | jsDelivr 官方 Cloudflare 段 | 100% | 与 Fastly 真正异构,第二通道 |
521
+ | `gcore.jsdelivr.net` | jsDelivr 官方(名义 GCore) | 100% | 当前与 testingcf 同解析到 CF,纯语义冗余 |
522
+ | ~~`cdn.jsdelivr.net`~~ | jsDelivr 官方多 CDN 调度 | **0/45,SNI 硬封** | 0.4.0 移除:换 IP/换 TLS 栈均无效,留着白耗超时 |
523
+
524
+ jsDelivr 官方子域与第三方代理不同:同一份内容、官方运营、无中间人;第三方代理
525
+ (`ghproxy.net` / `gh-proxy.com` 一类)「请求会经过它们的服务器」,**ghprobe 不预置**,
526
+ 需要更多兜底自己加(写法见下文):
498
527
 
499
528
  > 🔴 自己添加的第三方替身域名,其可见性 / 合规性 / 法律风险由添加者自行判断和承担。
500
529
 
@@ -509,7 +538,7 @@ cdn.jsdelivr.net/gh/<user>/<repo>@<ref>/<path> ← jsDelivr 公共 CDN
509
538
  ```yaml
510
539
  fallbacks:
511
540
  raw.githubusercontent.com:
512
- - cdn.jsdelivr.net
541
+ - fastly.jsdelivr.net # 完整写法见 config.default.yaml:替身自带 path + mustContain 判据
513
542
  # 自己加一个:第三关用 path + mustContain 验证「这个 IP 真的在提供这份内容」
514
543
  - { domain: my.mirror.example, path: "/gh/torvalds/linux@master/README", mustContain: ["Linux kernel"] }
515
544
  ```
@@ -543,6 +572,12 @@ ghprobe install -c my.yaml
543
572
  **Q:为什么有时候修不好?**
544
573
  工具只能找到**当前网络、当前时刻**可用的 IP。如果遇到的是 SNI 主机名黑名单(换 IP 修不了)或 IP 黑洞,可能所有候选都不可用。此时会命中兜底链(如果配了),或者触发全灭护栏 —— **保留旧 IP、不写空块**,绝不会硬塞一个错误 IP 进去。
545
574
 
575
+ **Q:点了 GitHub 一开始没反应,过一会又自己好了?**
576
+ 这是**链路抖动 + 客户端自动兜底**在工作,不是坏了。ghprobe 每个域名写 2 条 IP(主 + 备选):首条恰好在分钟级的抖动窗口里被拦时,浏览器 / git 会自动去试备选(代价是多等一个连接超时,所以「没反应」一下);两条同时失效的话,常驻自愈 15~45s 内换 IP。「没反应」持续超过一分钟才需要管:跑 `ghprobe status` 看 hosts 与自愈日志,不行再 `ghprobe doctor` 定位。
577
+
578
+ **Q:遇到问题怎么报?**
579
+ 先跑 `ghprobe report`(生成诊断包并复制到剪贴板)和 `ghprobe doctor`,把两份输出贴到 <https://github.com/zmw-zmw/ghprobe/issues>。诊断包只含版本信息、hosts 块和自愈日志尾部,**不含任何个人数据**。
580
+
546
581
  **Q:会不会影响别的软件?**
547
582
  不会。它**只写 hosts 文件**,不装代理、不改系统代理设置、不劫持流量。而且只操作 `# === ghprobe begin/end ===` 之间属于它自己的那一块,**你原有的 hosts 条目和第三方工具写的条目都不会被改动**;卸载时也只剥掉自己的块。
548
583
 
@@ -573,7 +608,7 @@ npm install -g ghprobe@latest
573
608
 
574
609
  ### 免责声明
575
610
 
576
- 本工具仅做 DNS 优选与连通性探测:自身不实现、不运行任何代理、隧道或流量转发,默认配置也不预置任何第三方代理 / 镜像域名(兜底链默认仅含公共 CDN `cdn.jsdelivr.net`,其余替身需用户自行添加并自担风险)。使用本工具产生的一切后果由使用者自行承担。请遵守你所在地区的法律法规。
611
+ 本工具仅做 DNS 优选与连通性探测:自身不实现、不运行任何代理、隧道或流量转发,默认配置也不预置任何第三方代理域名(兜底链默认只含 jsDelivr 官方子域 `fastly` / `testingcf` / `gcore.jsdelivr.net`——同一份内容的官方多 CDN 入口,其余替身需用户自行添加并自担风险)。使用本工具产生的一切后果由使用者自行承担。请遵守你所在地区的法律法规。
577
612
 
578
613
  ---
579
614