cfpilot-scanner 0.2.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +81 -2
  2. package/dist/cli.js +1043 -58
  3. package/package.json +41 -41
package/dist/cli.js CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/cli.ts
4
10
  import { readFileSync } from "node:fs";
@@ -89,6 +95,59 @@ var BUILTIN_IPV4_CIDRS = [
89
95
  "172.64.0.0/13",
90
96
  "131.0.72.0/22"
91
97
  ];
98
+ var IPV4_CIDR_WEIGHTS = {
99
+ "104.16.0.0/13": 1,
100
+ // 实测 8/12
101
+ "104.24.0.0/14": 0.9,
102
+ // 实测 7/12
103
+ "103.21.244.0/22": 0.3,
104
+ "103.31.4.0/22": 0.3,
105
+ "190.93.240.0/20": 0.3,
106
+ "188.114.96.0/20": 0.3,
107
+ "141.101.64.0/18": 0.2,
108
+ "162.158.0.0/16": 0.2,
109
+ // 162.158.x.x:边缘节点,部分可用
110
+ // 162.159.x.x 是 Cloudflare 的管理 / API 网段,真机实测**多数**返回
111
+ // HTTP 403(反爬风控),但**并非全部**——用户实测 162.159.237.59 可用
112
+ // (2.8 MB/s),本地复测同样通过(SJC)。
113
+ //
114
+ // 因此**不能压到 0**(会错杀可用 IP),给一个很低的权重:
115
+ // 仍能采到少量候选,但不让大量 403 IP 占据阶段二额度。
116
+ //
117
+ // ⚠️ 教训:权重粒度为 /16,但实际可用性可能细到单个 IP。
118
+ // 实测样本只有 9 个就下「完全不可用」的结论过于草率。
119
+ "162.159.0.0/16": 0.05,
120
+ "172.64.0.0/13": 0.1,
121
+ // 实测 0/12 —— 不采样
122
+ "173.245.48.0/20": 0,
123
+ "103.22.200.0/22": 0,
124
+ "108.162.192.0/18": 0,
125
+ "197.234.240.0/22": 0,
126
+ "198.41.128.0/17": 0,
127
+ "131.0.72.0/22": 0
128
+ };
129
+ var DEFAULT_CIDR_WEIGHT = 0.2;
130
+ function cidrWeight(cidr) {
131
+ const normalized = cidr.trim();
132
+ if (normalized in IPV4_CIDR_WEIGHTS) return IPV4_CIDR_WEIGHTS[normalized];
133
+ const parsed = parseCidr(normalized);
134
+ if (!parsed || parsed.family !== 4) return DEFAULT_CIDR_WEIGHT;
135
+ let bestPrefix = -1;
136
+ let bestWeight = DEFAULT_CIDR_WEIGHT;
137
+ for (const [tableCidr, weight] of Object.entries(IPV4_CIDR_WEIGHTS)) {
138
+ const tableParsed = parseCidr(tableCidr);
139
+ if (!tableParsed || tableParsed.family !== 4) continue;
140
+ if (tableParsed.prefix > parsed.prefix) continue;
141
+ const parsedNorm = parsed.base >> BigInt(32 - tableParsed.prefix);
142
+ const tableNorm = tableParsed.base >> BigInt(32 - tableParsed.prefix);
143
+ if (parsedNorm !== tableNorm) continue;
144
+ if (tableParsed.prefix > bestPrefix) {
145
+ bestPrefix = tableParsed.prefix;
146
+ bestWeight = weight;
147
+ }
148
+ }
149
+ return bestWeight;
150
+ }
92
151
  var BUILTIN_IPV6_CIDRS = [
93
152
  "2400:cb00::/32",
94
153
  "2606:4700::/32",
@@ -137,7 +196,7 @@ function parseCidr(cidr) {
137
196
  const hostBits2 = 128 - prefix;
138
197
  const size2 = 1n << BigInt(hostBits2);
139
198
  const mask2 = hostBits2 === 0 ? 0n : (1n << BigInt(hostBits2)) - 1n;
140
- return { family: 6, base: base & ~mask2, prefix, size: size2 };
199
+ return { family: 6, base: base & ~mask2, prefix, size: size2, cidr: cidr.trim() };
141
200
  }
142
201
  if (prefix < 0 || prefix > 32) return null;
143
202
  const value = ipv4ToBigInt(addr);
@@ -145,7 +204,7 @@ function parseCidr(cidr) {
145
204
  const hostBits = 32 - prefix;
146
205
  const size = 1n << BigInt(hostBits);
147
206
  const mask = hostBits === 0 ? 0n : (1n << BigInt(hostBits)) - 1n;
148
- return { family: 4, base: value & ~mask, prefix, size };
207
+ return { family: 4, base: value & ~mask, prefix, size, cidr: cidr.trim() };
149
208
  }
150
209
  function ipv4ToBigInt(addr) {
151
210
  const parts = addr.split(".");
@@ -207,30 +266,114 @@ function bigIntToIpv6(value) {
207
266
  const tail = groups.slice(bestStart + bestLen).join(":");
208
267
  return `${head}::${tail}`;
209
268
  }
269
+ function splitByWeight(cidr) {
270
+ if (cidr.family !== 4) return [cidr];
271
+ const out = [];
272
+ const stack = [{ c: cidr }];
273
+ while (stack.length > 0) {
274
+ const item = stack.pop();
275
+ const c = item.c;
276
+ const weight = item.known ?? cidrWeight(c.cidr);
277
+ if (c.prefix >= 32) {
278
+ out.push(c);
279
+ continue;
280
+ }
281
+ const half = 1n << BigInt(32 - c.prefix - 1);
282
+ const mk = (base) => {
283
+ const sub = { ...c, prefix: c.prefix + 1, size: half, base };
284
+ return { ...sub, cidr: `${bigIntToIpv4(base)}/${c.prefix + 1}` };
285
+ };
286
+ const left = mk(c.base);
287
+ const right = mk(c.base + half);
288
+ const lw = cidrWeight(left.cidr);
289
+ const rw = cidrWeight(right.cidr);
290
+ if (lw === rw) {
291
+ out.push(c);
292
+ continue;
293
+ }
294
+ stack.push({ c: left, known: lw }, { c: right, known: rw });
295
+ }
296
+ return out.sort((a, b) => a.base < b.base ? -1 : a.base > b.base ? 1 : 0);
297
+ }
210
298
  function expandCidrs(cidrs, options = {}) {
211
299
  const sample = options.sample ?? 2e4;
212
300
  const random = makeRandom(options.seed ?? 24301);
213
- const parsed = cidrs.map(parseCidr).filter((c) => c !== null);
301
+ const parsedRaw = cidrs.map(parseCidr).filter((c) => c !== null);
302
+ if (parsedRaw.length === 0) return [];
303
+ const parsed = parsedRaw.flatMap(splitByWeight);
214
304
  if (parsed.length === 0) return [];
215
- const totalSize = parsed.reduce((sum, c) => sum + c.size, 0n);
216
- const out = [];
217
- for (const cidr of parsed) {
218
- if (sample <= 0 || cidr.size <= 2000n) {
305
+ if (sample <= 0) {
306
+ const all = [];
307
+ for (const cidr of parsed) {
219
308
  for (let i = 0n; i < cidr.size; i++) {
220
309
  const value = cidr.base + i;
221
- out.push(cidr.family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
310
+ all.push(cidr.family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
222
311
  }
223
- continue;
224
312
  }
225
- const ratio = Number(totalSize === 0n ? 1n : cidr.size * BigInt(sample) / totalSize);
226
- const count = Math.max(64, Math.min(2e4, ratio));
227
- const step = Number(cidr.size / BigInt(count));
228
- const offset = Math.floor(random() * step);
229
- for (let i = 0; i < count; i++) {
230
- const offsetValue = BigInt(offset + Math.floor(i * step));
231
- if (offsetValue >= cidr.size) break;
232
- const value = cidr.base + offsetValue;
233
- out.push(cidr.family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
313
+ return all;
314
+ }
315
+ const minPerCidr = sample <= 1e3 ? 1 : 64;
316
+ const out = [];
317
+ for (const family of [4, 6]) {
318
+ const group = parsed.filter((c) => c.family === family);
319
+ if (group.length === 0) continue;
320
+ const hasOtherFamily = parsed.some((c) => c.family !== family);
321
+ const familyBudget = Math.max(1, Math.round(hasOtherFamily ? sample / 2 : sample));
322
+ const effectiveSize = (c) => {
323
+ if (c.family !== 4) return c.size;
324
+ const weight = cidrWeight(c.cidr);
325
+ if (weight <= 0) return 0n;
326
+ const scaled = Number(c.size) * weight;
327
+ return scaled >= 1 ? BigInt(Math.floor(scaled)) : 0n;
328
+ };
329
+ const rawTotal = group.reduce((sum, c) => sum + effectiveSize(c), 0n);
330
+ const allZero = rawTotal === 0n;
331
+ let familyTotal = rawTotal;
332
+ let allocOf = effectiveSize;
333
+ if (allZero) {
334
+ familyTotal = group.reduce((sum, c) => sum + c.size, 0n);
335
+ allocOf = (c) => c.size;
336
+ }
337
+ if (familyTotal === 0n) continue;
338
+ const minForCidr = (c) => {
339
+ if (c.family !== 4) return minPerCidr;
340
+ const w = cidrWeight(c.cidr);
341
+ if (w >= 1) return minPerCidr;
342
+ return Math.max(1, Math.round(minPerCidr * w));
343
+ };
344
+ const plans = group.map((cidr) => {
345
+ const ratio = Number(allocOf(cidr) * BigInt(familyBudget) / familyTotal);
346
+ const wanted = Math.max(minForCidr(cidr), Math.min(2e4, ratio));
347
+ return { cidr, count: Math.min(Number(cidr.size), wanted) };
348
+ });
349
+ const usablePlans = allZero ? plans : plans.filter((p) => effectiveSize(p.cidr) > 0n);
350
+ let familySum = usablePlans.reduce((sum, p) => sum + p.count, 0);
351
+ while (familySum > familyBudget) {
352
+ let target = null;
353
+ for (const plan of usablePlans) {
354
+ if (plan.count > 1 && (!target || plan.count > target.count)) target = plan;
355
+ }
356
+ if (!target) break;
357
+ target.count -= 1;
358
+ familySum -= 1;
359
+ }
360
+ for (const { cidr, count } of usablePlans) {
361
+ if (count <= 0) continue;
362
+ if (count >= Number(cidr.size)) {
363
+ for (let i = 0n; i < cidr.size; i++) {
364
+ const value = cidr.base + i;
365
+ out.push(family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
366
+ }
367
+ continue;
368
+ }
369
+ const step = Math.floor(Number(cidr.size) / count);
370
+ const offset = Math.floor(random() * step);
371
+ for (let i = 0; i < count; i++) {
372
+ const offsetValue = BigInt(offset + Math.floor(i * step));
373
+ if (offsetValue >= cidr.size) break;
374
+ const value = cidr.base + offsetValue;
375
+ out.push(family === 4 ? bigIntToIpv4(value) : bigIntToIpv6(value));
376
+ }
234
377
  }
235
378
  }
236
379
  return out;
@@ -266,7 +409,7 @@ function readIpFile(text) {
266
409
  // src/candidates.ts
267
410
  var DEFAULT_CANDIDATE_SAMPLE = 2e4;
268
411
  var DEFAULT_CANDIDATE_SEED = 24301;
269
- var DEFAULT_BRIDGE_SAMPLE = 300;
412
+ var DEFAULT_BRIDGE_SAMPLE = 6556;
270
413
  function normalizeCidrInput(cidr) {
271
414
  if (cidr === void 0) return [];
272
415
  const list = Array.isArray(cidr) ? cidr : [cidr];
@@ -326,11 +469,239 @@ function normalizeCandidateList(input) {
326
469
  return out;
327
470
  }
328
471
 
329
- // src/scan.ts
472
+ // src/net-env.ts
330
473
  import * as net from "node:net";
474
+ import { execFile } from "node:child_process";
475
+ var PROXY_PORTS = [
476
+ { port: 7890, name: "Clash / Mihomo\uFF08HTTP\uFF09" },
477
+ { port: 7891, name: "Clash / Mihomo\uFF08SOCKS\uFF09" },
478
+ { port: 7897, name: "Clash Verge" },
479
+ { port: 1080, name: "\u901A\u7528 SOCKS" },
480
+ { port: 10808, name: "v2rayN\uFF08SOCKS\uFF09" },
481
+ { port: 10809, name: "v2rayN\uFF08HTTP\uFF09" },
482
+ { port: 2080, name: "Nekoray / sing-box" },
483
+ { port: 2081, name: "Nekoray / sing-box" },
484
+ { port: 8889, name: "Surge" },
485
+ { port: 6152, name: "Surge\uFF08HTTP\uFF09" }
486
+ ];
487
+ function isPortListening(port, timeoutMs = 400) {
488
+ return new Promise((resolve) => {
489
+ const socket = new net.Socket();
490
+ let settled = false;
491
+ const done = (open) => {
492
+ if (settled) return;
493
+ settled = true;
494
+ try {
495
+ socket.destroy();
496
+ } catch {
497
+ }
498
+ resolve(open);
499
+ };
500
+ socket.setTimeout(timeoutMs, () => done(false));
501
+ socket.once("connect", () => done(true));
502
+ socket.once("error", () => done(false));
503
+ try {
504
+ socket.connect(port, "127.0.0.1");
505
+ } catch {
506
+ done(false);
507
+ }
508
+ });
509
+ }
510
+ async function readSystemProxy() {
511
+ if (process.platform !== "win32") return "";
512
+ const query = (key) => new Promise((resolve) => {
513
+ execFile(
514
+ "reg",
515
+ ["query", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "/v", key],
516
+ { timeout: 3e3, windowsHide: true },
517
+ (err, stdout) => resolve(err ? "" : String(stdout))
518
+ );
519
+ });
520
+ try {
521
+ const enabled = await query("ProxyEnable");
522
+ if (!/ProxyEnable\s+REG_DWORD\s+0x1/i.test(enabled)) return "";
523
+ const server = await query("ProxyServer");
524
+ const m = /ProxyServer\s+REG_SZ\s+(\S+)/i.exec(server);
525
+ return m ? m[1] : "(\u5DF2\u542F\u7528\uFF0C\u4F46\u672A\u8BFB\u5230\u5730\u5740)";
526
+ } catch {
527
+ return "";
528
+ }
529
+ }
530
+ function isFakeIp(ip) {
531
+ const parts = ip.split(".").map(Number);
532
+ const [a, b] = parts;
533
+ return a === 198 && (b === 18 || b === 19);
534
+ }
535
+ async function detectProxyInterference(host) {
536
+ const findings = [];
537
+ const listening = [];
538
+ await Promise.all(
539
+ PROXY_PORTS.map(async ({ port, name }) => {
540
+ if (await isPortListening(port)) listening.push(`${name} \u2192 127.0.0.1:${port}`);
541
+ })
542
+ );
543
+ if (listening.length > 0) {
544
+ findings.push({
545
+ message: "\u672C\u673A\u6709\u4EE3\u7406\u7A0B\u5E8F\u5728\u76D1\u542C\uFF1A\n " + listening.join("\n "),
546
+ advice: "\u5173\u95ED\u8FD9\u4E9B\u4EE3\u7406\u7A0B\u5E8F\uFF0C\u6216\u5728\u5176\u4E2D\u628A\u672C\u7A0B\u5E8F\u52A0\u5165\u76F4\u8FDE/\u7ED5\u8FC7\u540D\u5355"
547
+ });
548
+ }
549
+ const sysProxy = await readSystemProxy();
550
+ if (sysProxy) {
551
+ findings.push({
552
+ message: `\u7CFB\u7EDF\u4EE3\u7406\u5DF2\u542F\u7528\uFF1A${sysProxy}`,
553
+ advice: "\u5728\u300C\u8BBE\u7F6E \u2192 \u7F51\u7EDC\u548C Internet \u2192 \u4EE3\u7406\u300D\u4E2D\u5173\u95ED\u7CFB\u7EDF\u4EE3\u7406"
554
+ });
555
+ }
556
+ const probeHost = host?.trim() || "www.cloudflare.com";
557
+ try {
558
+ const { resolve4 } = await import("node:dns/promises");
559
+ const ips = await resolve4(probeHost).catch(() => []);
560
+ const fake = ips.find((ip) => isFakeIp(ip));
561
+ if (fake) {
562
+ findings.push({
563
+ message: `DNS \u628A ${probeHost} \u89E3\u6790\u5230 ${fake}\uFF08198.18/15 \u4FDD\u7559\u6BB5\uFF09\uFF0CTUN / fake-ip \u6B63\u5728\u751F\u6548`,
564
+ advice: "\u5173\u95ED\u4EE3\u7406\u5DE5\u5177\u7684 TUN \u6A21\u5F0F\u4E0E fake-ip DNS"
565
+ });
566
+ }
567
+ } catch {
568
+ }
569
+ return { clean: findings.length === 0, findings };
570
+ }
571
+ function isMsysPollutedPath(value) {
572
+ const looksWindows = /^[A-Za-z]:[\\/]/.test(value) || value.includes("\\");
573
+ const hasMsysHint = /node_modules|bash-win32|@dsh-ops|msys/i.test(value);
574
+ return looksWindows || hasMsysHint;
575
+ }
331
576
 
332
- // src/true-probe.ts
577
+ // src/speed-url.ts
333
578
  import * as tls from "node:tls";
579
+ var SPEED_URL_PRESETS = [
580
+ { url: "speed.cloudflare.com/__down?bytes=104857600", label: "Cloudflare \u5B98\u65B9" },
581
+ { url: "cf.090227.xyz/__down?bytes=104857600", label: "CM \u63D0\u4F9B" }
582
+ ];
583
+ var DEFAULT_SPEED_URL = SPEED_URL_PRESETS[0].url;
584
+ function parseSpeedUrl(raw, fallbackScheme = "https") {
585
+ const value = (raw ?? "").trim();
586
+ if (!value) throw new Error("\u6D4B\u901F\u5730\u5740\u4E3A\u7A7A");
587
+ const schemeMatch = /^([a-z][a-z0-9+.-]*):\/\//i.exec(value);
588
+ if (schemeMatch) {
589
+ const scheme2 = schemeMatch[1].toLowerCase();
590
+ if (scheme2 !== "http" && scheme2 !== "https") {
591
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u6D4B\u901F\u5730\u5740\u534F\u8BAE\uFF1A${scheme2}\uFF08\u53EA\u652F\u6301 http / https\uFF09`);
592
+ }
593
+ }
594
+ let normalized = value;
595
+ if (normalized.startsWith("//")) {
596
+ normalized = `${fallbackScheme}:${normalized}`;
597
+ } else if (!/^https?:\/\//i.test(normalized)) {
598
+ normalized = `${fallbackScheme}://${normalized}`;
599
+ }
600
+ let parsed;
601
+ try {
602
+ parsed = new URL(normalized);
603
+ } catch {
604
+ throw new Error(`\u6D4B\u901F\u5730\u5740\u65E0\u6CD5\u89E3\u6790\uFF1A${value}`);
605
+ }
606
+ const scheme = parsed.protocol.replace(":", "").toLowerCase();
607
+ if (scheme !== "http" && scheme !== "https") {
608
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u6D4B\u901F\u5730\u5740\u534F\u8BAE\uFF1A${scheme}`);
609
+ }
610
+ if (!parsed.hostname) throw new Error(`\u6D4B\u901F\u5730\u5740\u7F3A\u5C11\u57DF\u540D\uFF1A${value}`);
611
+ return {
612
+ raw: value,
613
+ scheme,
614
+ host: parsed.hostname,
615
+ path: `${parsed.pathname}${parsed.search}` || "/"
616
+ };
617
+ }
618
+ function schemeForPort(port) {
619
+ return port === 443 || port === 2053 || port === 2083 || port === 2087 || port === 2096 || port === 8443 ? "https" : "http";
620
+ }
621
+ function checkSpeedUrlAvailable(parsed, timeoutMs = 6e3) {
622
+ return new Promise((resolve) => {
623
+ let settled = false;
624
+ let received = "";
625
+ const finish = (available, status, error) => {
626
+ if (settled) return;
627
+ settled = true;
628
+ clearTimeout(timer);
629
+ try {
630
+ socket.destroy();
631
+ } catch {
632
+ }
633
+ resolve({ available, status, error });
634
+ };
635
+ const path = parsed.path.includes("bytes=") ? parsed.path.replace(/bytes=\d+/, "bytes=1024") : parsed.path;
636
+ const request = [
637
+ `GET ${path} HTTP/1.1`,
638
+ `Host: ${parsed.host}`,
639
+ `User-Agent: ${USER_AGENT_FOR_CHECK}`,
640
+ "Accept: */*",
641
+ "Connection: close",
642
+ "",
643
+ ""
644
+ ].join("\r\n");
645
+ const connectOptions = {
646
+ host: parsed.host,
647
+ port: parsed.scheme === "https" ? 443 : 80,
648
+ servername: parsed.host,
649
+ rejectUnauthorized: false
650
+ };
651
+ const socket = parsed.scheme === "https" ? tls.connect(connectOptions, () => socket.write(request)) : (
652
+ // http 明文:用 net 连接
653
+ (() => {
654
+ const net3 = __require("node:net");
655
+ const s = net3.connect({ host: parsed.host, port: 80 }, () => s.write(request));
656
+ return s;
657
+ })()
658
+ );
659
+ const timer = setTimeout(() => finish(false, 0, "\u9884\u68C0\u8D85\u65F6"), timeoutMs);
660
+ socket.setTimeout(timeoutMs, () => finish(false, 0, "\u9884\u68C0\u8D85\u65F6"));
661
+ socket.on("error", (err) => finish(false, 0, err.message));
662
+ socket.on("data", (chunk) => {
663
+ received += chunk.toString("utf8");
664
+ const m = /^HTTP\/1\.[01]\s+(\d{3})/.exec(received);
665
+ if (m) {
666
+ const status = Number(m[1]);
667
+ finish(status === 200 || status === 206, status);
668
+ }
669
+ });
670
+ });
671
+ }
672
+ var USER_AGENT_FOR_CHECK = "CFPilot-Scanner/1.0 (+https://github.com/xmwengxing/CFPilot)";
673
+ async function pickAvailableSpeedUrl(candidates, log) {
674
+ const list = candidates.length > 0 ? candidates : [DEFAULT_SPEED_URL];
675
+ for (const candidate of list) {
676
+ let parsed;
677
+ try {
678
+ parsed = parseSpeedUrl(candidate, "https");
679
+ } catch {
680
+ continue;
681
+ }
682
+ const result = await checkSpeedUrlAvailable(parsed);
683
+ if (result.available) {
684
+ log?.(`\u6D4B\u901F\u5730\u5740\u53EF\u7528\uFF1A${candidate}`);
685
+ return candidate;
686
+ }
687
+ log?.(`\u6D4B\u901F\u5730\u5740\u4E0D\u53EF\u7528\uFF08${result.error ?? "HTTP " + result.status}\uFF09\uFF1A${candidate}`);
688
+ }
689
+ log?.(`\u6240\u6709\u6D4B\u901F\u5730\u5740\u90FD\u4E0D\u53EF\u7528\uFF0C\u56DE\u9000\u5230\u9ED8\u8BA4\uFF1A${DEFAULT_SPEED_URL}`);
690
+ return DEFAULT_SPEED_URL;
691
+ }
692
+ function withBytes(parsed, bytes) {
693
+ if (/bytes=\d+/.test(parsed.path)) {
694
+ return parsed.path.replace(/bytes=\d+/, `bytes=${bytes}`);
695
+ }
696
+ const sep = parsed.path.includes("?") ? "&" : "?";
697
+ return `${parsed.path}${sep}bytes=${bytes}`;
698
+ }
699
+
700
+ // src/scan.ts
701
+ import * as net2 from "node:net";
702
+
703
+ // src/true-probe.ts
704
+ import * as tls2 from "node:tls";
334
705
  import * as crypto from "node:crypto";
335
706
  var USER_AGENT = "CFPilot-Scanner/1.0 (+https://github.com/xmwengxing/CFPilot)";
336
707
  function extractColo(text) {
@@ -364,7 +735,7 @@ function probeWs101(ip, port, host, timeoutMs = 3e3, path = "/__edge_probe") {
364
735
  "",
365
736
  ""
366
737
  ].join("\r\n");
367
- const socket = tls.connect(
738
+ const socket = tls2.connect(
368
739
  {
369
740
  host: ip,
370
741
  port,
@@ -405,6 +776,216 @@ function probeWs101(ip, port, host, timeoutMs = 3e3, path = "/__edge_probe") {
405
776
  });
406
777
  });
407
778
  }
779
+ function encodeWsFrame(payload, opcode = 2) {
780
+ const mask = crypto.randomBytes(4);
781
+ const masked = Buffer.alloc(payload.length);
782
+ for (let i = 0; i < payload.length; i++) {
783
+ masked[i] = payload[i] ^ mask[i % 4];
784
+ }
785
+ let header;
786
+ if (payload.length < 126) {
787
+ header = Buffer.from([128 | opcode, 128 | payload.length]);
788
+ } else if (payload.length < 65536) {
789
+ header = Buffer.alloc(4);
790
+ header[0] = 128 | opcode;
791
+ header[1] = 254;
792
+ header.writeUInt16BE(payload.length, 2);
793
+ } else {
794
+ header = Buffer.alloc(10);
795
+ header[0] = 128 | opcode;
796
+ header[1] = 255;
797
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
798
+ }
799
+ return Buffer.concat([header, mask, masked]);
800
+ }
801
+ function decodeWsFrame(buf) {
802
+ if (buf.length < 2) return null;
803
+ const opcode = buf[0] & 15;
804
+ const len7 = buf[1] & 127;
805
+ let offset;
806
+ let payloadLen;
807
+ if (len7 < 126) {
808
+ offset = 2;
809
+ payloadLen = len7;
810
+ } else if (len7 === 126) {
811
+ if (buf.length < 4) return null;
812
+ offset = 4;
813
+ payloadLen = buf.readUInt16BE(2);
814
+ } else {
815
+ if (buf.length < 10) return null;
816
+ offset = 10;
817
+ payloadLen = Number(buf.readBigUInt64BE(2));
818
+ }
819
+ if (buf.length < offset + payloadLen) return null;
820
+ const frame = {
821
+ opcode,
822
+ payload: Buffer.from(buf.subarray(offset, offset + payloadLen))
823
+ };
824
+ const rest = Buffer.from(buf.subarray(offset + payloadLen));
825
+ return { frame, rest };
826
+ }
827
+ function probeWsPing(ip, port, host, timeoutMs = 5e3, path = "/__edge_probe") {
828
+ return new Promise((resolve) => {
829
+ const started = Date.now();
830
+ const key = crypto.randomBytes(16).toString("base64");
831
+ let settled = false;
832
+ let handshakeDone = false;
833
+ let buf = Buffer.alloc(0);
834
+ let receivedText = "";
835
+ const finish = (result) => {
836
+ if (settled) return;
837
+ settled = true;
838
+ clearTimeout(timer);
839
+ try {
840
+ socket.destroy();
841
+ } catch {
842
+ }
843
+ resolve({ ...result, latencyMs: Date.now() - started });
844
+ };
845
+ const request = [
846
+ `GET ${path} HTTP/1.1`,
847
+ `Host: ${host}`,
848
+ `Upgrade: websocket`,
849
+ `Connection: Upgrade`,
850
+ `Sec-WebSocket-Key: ${key}`,
851
+ `Sec-WebSocket-Version: 13`,
852
+ `User-Agent: ${USER_AGENT}`,
853
+ "",
854
+ ""
855
+ ].join("\r\n");
856
+ const socket = tls2.connect(
857
+ {
858
+ host: ip,
859
+ port,
860
+ servername: host,
861
+ rejectUnauthorized: false,
862
+ ALPNProtocols: ["http/1.1"]
863
+ },
864
+ () => {
865
+ socket.write(request);
866
+ }
867
+ );
868
+ const timer = setTimeout(() => {
869
+ finish({
870
+ ok: false,
871
+ colo: "",
872
+ status: 0,
873
+ error: handshakeDone ? "PING \u8D85\u65F6\uFF08\u6570\u636E\u9762\u672A\u54CD\u5E94\uFF09" : "\u63E1\u624B\u8D85\u65F6",
874
+ pongVerified: handshakeDone ? false : void 0
875
+ });
876
+ }, timeoutMs);
877
+ socket.setTimeout(timeoutMs, () => {
878
+ finish({
879
+ ok: false,
880
+ colo: "",
881
+ status: 0,
882
+ error: handshakeDone ? "PING \u8D85\u65F6\uFF08\u6570\u636E\u9762\u672A\u54CD\u5E94\uFF09" : "\u8FDE\u63A5\u8D85\u65F6",
883
+ pongVerified: handshakeDone ? false : void 0
884
+ });
885
+ });
886
+ socket.on("error", (err) => {
887
+ finish({
888
+ ok: false,
889
+ colo: "",
890
+ status: 0,
891
+ error: err.message,
892
+ pongVerified: handshakeDone ? false : void 0
893
+ });
894
+ });
895
+ socket.on("data", (chunk) => {
896
+ buf = Buffer.concat([buf, chunk]);
897
+ if (!handshakeDone) {
898
+ const lineEnd = buf.indexOf("\r\n\r\n");
899
+ if (lineEnd < 0) {
900
+ if (buf.length > 8192) finish({ ok: false, colo: "", status: 0, error: "\u54CD\u5E94\u5934\u5F02\u5E38" });
901
+ return;
902
+ }
903
+ const head = buf.slice(0, lineEnd).toString("latin1");
904
+ const statusLine = head.split("\r\n")[0] ?? "";
905
+ const statusMatch = /^HTTP\/1\.[01]\s+(\d{3})/.exec(statusLine);
906
+ const status = statusMatch ? Number(statusMatch[1]) : 0;
907
+ const colo = extractColo(head);
908
+ buf = buf.subarray(lineEnd + 4);
909
+ if (status !== 101) {
910
+ finish({ ok: false, colo, status, error: `HTTP ${status}` });
911
+ return;
912
+ }
913
+ handshakeDone = true;
914
+ socket.write(encodeWsFrame(new TextEncoder().encode("PING"), 1));
915
+ }
916
+ for (; ; ) {
917
+ const decoded = decodeWsFrame(buf);
918
+ if (!decoded) break;
919
+ buf = decoded.rest;
920
+ const { opcode, payload } = decoded.frame;
921
+ if (opcode === 8) {
922
+ const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
923
+ finish({
924
+ ok: false,
925
+ colo: "",
926
+ status: 101,
927
+ error: `WS \u5173\u95ED\uFF08code ${code}\uFF09`,
928
+ pongVerified: false
929
+ });
930
+ return;
931
+ }
932
+ if (opcode === 1 || opcode === 2) {
933
+ receivedText += payload.toString("utf8");
934
+ try {
935
+ const parsed = JSON.parse(receivedText);
936
+ if (parsed.pong === true) {
937
+ finish({
938
+ ok: true,
939
+ colo: typeof parsed.colo === "string" ? parsed.colo.toUpperCase() : "",
940
+ status: 101,
941
+ pongVerified: true,
942
+ probeColo: typeof parsed.colo === "string" ? parsed.colo.toUpperCase() : void 0
943
+ });
944
+ return;
945
+ }
946
+ } catch {
947
+ }
948
+ }
949
+ }
950
+ });
951
+ });
952
+ }
953
+ function probeTlsHandshake(ip, port, host, timeoutMs = 4e3) {
954
+ return new Promise((resolve) => {
955
+ const started = Date.now();
956
+ let settled = false;
957
+ const finish = (result) => {
958
+ if (settled) return;
959
+ settled = true;
960
+ clearTimeout(timer);
961
+ try {
962
+ socket.destroy();
963
+ } catch {
964
+ }
965
+ resolve({ ip, tlsMs: result.tlsMs ?? Date.now() - started, ...result });
966
+ };
967
+ const socket = tls2.connect(
968
+ {
969
+ host: ip,
970
+ port,
971
+ servername: host,
972
+ rejectUnauthorized: false,
973
+ ALPNProtocols: ["http/1.1"]
974
+ },
975
+ () => {
976
+ const alpn = socket.alpnProtocol;
977
+ if (alpn && alpn !== "http/1.1") {
978
+ finish({ error: `ALPN=${alpn}` });
979
+ return;
980
+ }
981
+ finish({ tlsMs: Date.now() - started, alpn: alpn || void 0 });
982
+ }
983
+ );
984
+ const timer = setTimeout(() => finish({ error: "TLS \u63E1\u624B\u8D85\u65F6" }), timeoutMs);
985
+ socket.setTimeout(timeoutMs, () => finish({ error: "TLS \u8FDE\u63A5\u8D85\u65F6" }));
986
+ socket.on("error", (err) => finish({ error: err.message }));
987
+ });
988
+ }
408
989
  function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edge_probe") {
409
990
  return new Promise((resolve) => {
410
991
  const started = Date.now();
@@ -430,7 +1011,7 @@ function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edg
430
1011
  "",
431
1012
  ""
432
1013
  ].join("\r\n");
433
- const socket = tls.connect(
1014
+ const socket = tls2.connect(
434
1015
  {
435
1016
  host: ip,
436
1017
  port,
@@ -489,13 +1070,215 @@ function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edg
489
1070
  socket.on("close", settle);
490
1071
  });
491
1072
  }
1073
+ function measureThroughput(options) {
1074
+ const {
1075
+ ip,
1076
+ port,
1077
+ sniHost,
1078
+ path,
1079
+ requestHost = sniHost,
1080
+ durationMs = 6e3,
1081
+ warmupMs = 1e3,
1082
+ windowMs = 400,
1083
+ connections = 3,
1084
+ timeoutMs = 2e4
1085
+ } = options;
1086
+ const connCount = Math.max(1, Math.min(8, connections));
1087
+ return new Promise((resolve) => {
1088
+ const started = Date.now();
1089
+ let settled = false;
1090
+ let totalBytes = 0;
1091
+ let windowBytes = 0;
1092
+ let windowStart = started;
1093
+ let peak = 0;
1094
+ let openedCount = 0;
1095
+ let lastError = "";
1096
+ const sockets = [];
1097
+ const finish = () => {
1098
+ if (settled) return;
1099
+ settled = true;
1100
+ clearInterval(ticker);
1101
+ clearTimeout(timer);
1102
+ for (const s of sockets) {
1103
+ try {
1104
+ s.destroy();
1105
+ } catch {
1106
+ }
1107
+ }
1108
+ const elapsedNow = Date.now() - started;
1109
+ if (elapsedNow >= warmupMs) {
1110
+ const dt = (Date.now() - windowStart) / 1e3;
1111
+ if (dt > 0) {
1112
+ const speed = windowBytes / dt / 1024 / 1024;
1113
+ if (speed > peak) peak = speed;
1114
+ }
1115
+ }
1116
+ const elapsed = Math.max(1e-3, (Date.now() - started) / 1e3);
1117
+ resolve({
1118
+ ok: totalBytes > 0,
1119
+ bytes: totalBytes,
1120
+ ms: Date.now() - started,
1121
+ peakMBps: Number(peak.toFixed(2)),
1122
+ avgMBps: Number((totalBytes / 1024 / 1024 / elapsed).toFixed(2)),
1123
+ connections: openedCount,
1124
+ error: totalBytes > 0 ? void 0 : lastError || "\u672A\u6536\u5230\u6570\u636E"
1125
+ });
1126
+ };
1127
+ const request = [
1128
+ `GET ${path} HTTP/1.1`,
1129
+ `Host: ${requestHost}`,
1130
+ `User-Agent: ${USER_AGENT}`,
1131
+ "Accept: */*",
1132
+ "Accept-Encoding: identity",
1133
+ "Connection: close",
1134
+ "",
1135
+ ""
1136
+ ].join("\r\n");
1137
+ const openOne = () => {
1138
+ let headerDone = false;
1139
+ let headerBuffer = "";
1140
+ const socket = tls2.connect(
1141
+ {
1142
+ host: ip,
1143
+ port,
1144
+ servername: sniHost,
1145
+ rejectUnauthorized: false,
1146
+ ALPNProtocols: ["http/1.1"]
1147
+ },
1148
+ () => {
1149
+ openedCount++;
1150
+ socket.write(request);
1151
+ }
1152
+ );
1153
+ sockets.push(socket);
1154
+ socket.setTimeout(timeoutMs, () => {
1155
+ try {
1156
+ socket.destroy();
1157
+ } catch {
1158
+ }
1159
+ });
1160
+ socket.on("error", (err) => {
1161
+ lastError = err.message;
1162
+ });
1163
+ socket.on("data", (chunk) => {
1164
+ if (!headerDone) {
1165
+ headerBuffer += chunk.toString("latin1");
1166
+ const idx = headerBuffer.indexOf("\r\n\r\n");
1167
+ if (idx < 0) {
1168
+ if (headerBuffer.length > 8192) {
1169
+ lastError = "\u54CD\u5E94\u5934\u5F02\u5E38";
1170
+ socket.destroy();
1171
+ }
1172
+ return;
1173
+ }
1174
+ const head = headerBuffer.slice(0, idx);
1175
+ const status = /^HTTP\/1\.[01]\s+(\d{3})/.exec(head)?.[1];
1176
+ if (status !== "200") {
1177
+ lastError = `HTTP ${status ?? "?"}`;
1178
+ socket.destroy();
1179
+ return;
1180
+ }
1181
+ headerDone = true;
1182
+ const body = Buffer.from(headerBuffer, "latin1").subarray(idx + 4);
1183
+ totalBytes += body.byteLength;
1184
+ windowBytes += body.byteLength;
1185
+ headerBuffer = "";
1186
+ } else {
1187
+ totalBytes += chunk.byteLength;
1188
+ windowBytes += chunk.byteLength;
1189
+ }
1190
+ if (Date.now() - started > durationMs) finish();
1191
+ });
1192
+ socket.on("end", () => {
1193
+ if (sockets.every((s) => s.destroyed || s.readableEnded)) finish();
1194
+ });
1195
+ socket.on("close", () => {
1196
+ if (sockets.every((s) => s.destroyed)) finish();
1197
+ });
1198
+ };
1199
+ const ticker = setInterval(() => {
1200
+ const now = Date.now();
1201
+ const dt = (now - windowStart) / 1e3;
1202
+ if (dt <= 0) return;
1203
+ const bytes = windowBytes;
1204
+ windowBytes = 0;
1205
+ windowStart = now;
1206
+ if (now - started < warmupMs) return;
1207
+ const speed = bytes / dt / 1024 / 1024;
1208
+ if (speed > peak) peak = speed;
1209
+ }, windowMs);
1210
+ const timer = setTimeout(finish, durationMs + 4e3);
1211
+ for (let i = 0; i < connCount; i++) {
1212
+ setTimeout(openOne, i * 60);
1213
+ }
1214
+ });
1215
+ }
1216
+
1217
+ // src/countries.ts
1218
+ var COUNTRY_COLOS = {
1219
+ HK: ["HKG"],
1220
+ TW: ["TPE", "KHH"],
1221
+ JP: ["NRT", "KIX", "CTS", "FUK"],
1222
+ KR: ["ICN", "PUS"],
1223
+ SG: ["SIN"],
1224
+ MY: ["KUL"],
1225
+ TH: ["BKK"],
1226
+ VN: ["SGN", "HAN"],
1227
+ ID: ["CGK", "DPS"],
1228
+ PH: ["MNL"],
1229
+ IN: ["BOM", "MAA", "DEL"],
1230
+ AU: ["SYD", "MEL", "BNE", "PER"],
1231
+ NZ: ["AKL"],
1232
+ US: ["LAX", "SJC", "SEA", "ORD", "DFW", "IAD", "EWR", "ATL", "MIA", "DEN", "PHX", "BOS"],
1233
+ CA: ["YYZ", "YVR", "YYC"],
1234
+ GB: ["LHR", "MAN"],
1235
+ DE: ["FRA", "MUC", "BER"],
1236
+ FR: ["CDG", "MRS"],
1237
+ NL: ["AMS"],
1238
+ IT: ["MXP", "FCO"],
1239
+ ES: ["MAD", "BCN"],
1240
+ SE: ["ARN"],
1241
+ CH: ["ZRH"],
1242
+ AT: ["VIE"],
1243
+ PL: ["WAW"],
1244
+ RU: ["DME", "SVO"],
1245
+ TR: ["IST"],
1246
+ AE: ["DXB"],
1247
+ IL: ["TLV"],
1248
+ ZA: ["JNB"],
1249
+ BR: ["GRU", "GIG"],
1250
+ AR: ["EZE"],
1251
+ CL: ["SCL"],
1252
+ MX: ["MEX"]
1253
+ };
1254
+ var coloToCountry = /* @__PURE__ */ new Map();
1255
+ for (const [country, colos] of Object.entries(COUNTRY_COLOS)) {
1256
+ for (const colo of colos) coloToCountry.set(colo, country);
1257
+ }
1258
+ function countryOfColo(colo) {
1259
+ return coloToCountry.get(colo.toUpperCase()) ?? "";
1260
+ }
1261
+ function isKnownCountry(code) {
1262
+ return Object.prototype.hasOwnProperty.call(COUNTRY_COLOS, code.toUpperCase());
1263
+ }
1264
+ var SUPPORTED_COUNTRIES = Object.keys(COUNTRY_COLOS);
1265
+ function normalizeCountryFilter(input) {
1266
+ if (!Array.isArray(input)) return [];
1267
+ const out = [];
1268
+ for (const raw of input) {
1269
+ const code = String(raw ?? "").trim().toUpperCase();
1270
+ if (!code || out.includes(code)) continue;
1271
+ if (isKnownCountry(code)) out.push(code);
1272
+ }
1273
+ return out;
1274
+ }
492
1275
 
493
1276
  // src/scan.ts
494
1277
  async function tcpPing(ip, port, timeoutMs) {
495
1278
  return new Promise((resolve) => {
496
1279
  const started = Date.now();
497
1280
  let settled = false;
498
- const socket = new net.Socket();
1281
+ const socket = new net2.Socket();
499
1282
  const done = (ok) => {
500
1283
  if (settled) return;
501
1284
  settled = true;
@@ -528,6 +1311,8 @@ async function runScan(options) {
528
1311
  const started = Date.now();
529
1312
  const concurrency = options.concurrency ?? 300;
530
1313
  const tcpTimeoutMs = options.tcpTimeoutMs ?? 1200;
1314
+ const tlsConcurrency = options.tlsConcurrency ?? 64;
1315
+ const tlsTimeoutMs = options.tlsTimeoutMs ?? 4e3;
531
1316
  const maxLatencyMs = options.maxLatencyMs ?? 0;
532
1317
  const probeLimit = options.probeLimit ?? 120;
533
1318
  const probeConcurrency = options.probeConcurrency ?? 24;
@@ -558,29 +1343,115 @@ async function runScan(options) {
558
1343
  });
559
1344
  const tcpOk = tcpResults.filter((r) => r.ok);
560
1345
  const filtered = maxLatencyMs > 0 ? tcpOk.filter((r) => r.latency <= maxLatencyMs) : tcpOk;
561
- const stage1 = filtered.sort((a, b) => a.latency - b.latency).slice(0, probeLimit);
562
- report({ type: "stage", stage: 2, name: "WebSocket 101 \u771F\u8FDE\u63A5\u6821\u9A8C", total: stage1.length });
1346
+ report({ type: "stage", stage: 1, name: "TLS \u63E1\u624B\u7B5B\u9009", total: filtered.length });
1347
+ let tlsDone = 0;
1348
+ const tlsResults = await pool(filtered, tlsConcurrency, async (item) => {
1349
+ const probe = await probeTlsHandshake(item.ip, options.port, options.host, tlsTimeoutMs);
1350
+ tlsDone++;
1351
+ if (tlsDone % 200 === 0 || tlsDone === filtered.length) {
1352
+ report({
1353
+ type: "progress",
1354
+ stage: 1,
1355
+ done: tlsDone,
1356
+ total: filtered.length,
1357
+ message: `TLS \u63E1\u624B ${tlsDone}/${filtered.length}${probe.tlsMs > 0 ? ` ${probe.tlsMs}ms` : " \u2717"}`
1358
+ });
1359
+ }
1360
+ return { ...item, tls: probe };
1361
+ });
1362
+ const tlsOk = tlsResults.filter((r) => r.tls.tlsMs > 0);
1363
+ const stage1 = tlsOk.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, probeLimit);
1364
+ const strictProbe = options.verifyPayload !== false;
1365
+ const strictProbePath = "/__edge_probe";
1366
+ report({
1367
+ type: "stage",
1368
+ stage: 2,
1369
+ name: strictProbe ? "WS 101 + \u54CD\u5E94\u4F53\u6821\u9A8C\uFF08\u63A2\u9488\u7AEF\u70B9\uFF09" : "WebSocket 101 \u6821\u9A8C",
1370
+ total: stage1.length
1371
+ });
563
1372
  let probeDone = 0;
1373
+ let verifiedCount = 0;
564
1374
  const probeResults = await pool(stage1, probeConcurrency, async (item) => {
565
- const result = await probeWs101(item.ip, options.port, options.host, probeTimeoutMs, options.wsPath ?? "/__edge_probe");
1375
+ const probeFn = strictProbe ? probeWsPing : probeWs101;
1376
+ const probePath = strictProbe ? strictProbePath : options.wsPath ?? "/__edge_probe";
1377
+ const result = await probeFn(
1378
+ item.ip,
1379
+ options.port,
1380
+ options.host,
1381
+ probeTimeoutMs,
1382
+ probePath
1383
+ );
566
1384
  probeDone++;
1385
+ if (result.pongVerified) verifiedCount++;
567
1386
  report({
568
1387
  type: "progress",
569
1388
  stage: 2,
570
1389
  done: probeDone,
571
1390
  total: stage1.length,
572
- message: `101 \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : ""}`
1391
+ message: strictProbe ? `101+PING \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : result.pongVerified === false ? " \u2717 \u65E0\u54CD\u5E94" : ""}` : `101 \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : ""}`
573
1392
  });
574
1393
  return { ...item, probe: result };
575
1394
  });
576
- const wsOk = probeResults.filter((r) => r.probe.ok);
577
- const stage2 = wsOk.sort((a, b) => a.latency - b.latency).slice(0, speedLimit);
1395
+ const wsOk = strictProbe ? probeResults.filter((r) => r.probe.ok && r.probe.pongVerified === true) : probeResults.filter((r) => r.probe.ok);
1396
+ const countryFilter = normalizeCountryFilter(options.countries ?? []);
1397
+ let wsFiltered = wsOk;
1398
+ let appliedCountries = [];
1399
+ if (countryFilter.length > 0 && wsOk.length > 0) {
1400
+ const matched = wsOk.filter((r) => countryFilter.includes(countryOfColo(r.probe.colo || "")));
1401
+ if (matched.length > 0) {
1402
+ wsFiltered = matched;
1403
+ appliedCountries = countryFilter;
1404
+ report({
1405
+ type: "progress",
1406
+ stage: 2,
1407
+ done: wsOk.length,
1408
+ total: wsOk.length,
1409
+ message: `\u56FD\u5BB6\u7B5B\u9009 ${countryFilter.join("/")}\uFF1A\u4FDD\u7559 ${matched.length}/${wsOk.length}\uFF0C\u8FDB\u5165\u6D4B\u901F`
1410
+ });
1411
+ } else {
1412
+ report({
1413
+ type: "progress",
1414
+ stage: 2,
1415
+ done: wsOk.length,
1416
+ total: wsOk.length,
1417
+ message: `\u56FD\u5BB6\u7B5B\u9009 ${countryFilter.join("/")} \u540E\u65E0\u5019\u9009\uFF0C\u5DF2\u5FFD\u7565\uFF08\u91C7\u6837\u592A\u5C0F\uFF1F\u8C03\u5230 500~1000 \u518D\u8BD5\uFF09`
1418
+ });
1419
+ }
1420
+ }
1421
+ const stage2 = wsFiltered.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, speedLimit);
578
1422
  let speedResults;
1423
+ const speedMode = options.speedMode ?? "probe";
579
1424
  if (options.skipSpeed) {
580
1425
  report({ type: "stage", stage: 3, name: "\u8DF3\u8FC7\u6D4B\u901F\uFF08\u6309\u5EF6\u8FDF\u6392\u5E8F\uFF09", total: stage2.length });
581
1426
  speedResults = stage2.map((r) => ({ ...r }));
1427
+ } else if (speedMode === "url") {
1428
+ report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5\uFF08\u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF09", total: stage2.length });
1429
+ const parsedUrl = parseSpeedUrl(options.speedUrl ?? DEFAULT_SPEED_URL, schemeForPort(options.port));
1430
+ const requestPath = withBytes(parsedUrl, options.speedBytes ?? 104857600);
1431
+ let speedDone = 0;
1432
+ speedResults = await pool(stage2, speedConcurrency, async (item) => {
1433
+ const result = await measureThroughput({
1434
+ ip: item.ip,
1435
+ port: options.port,
1436
+ sniHost: parsedUrl.host,
1437
+ requestHost: parsedUrl.host,
1438
+ path: requestPath,
1439
+ connections: options.speedConnections ?? 3,
1440
+ durationMs: options.speedDurationMs ?? 6e3,
1441
+ timeoutMs: options.speedTimeoutMs ?? 2e4
1442
+ });
1443
+ speedDone++;
1444
+ report({
1445
+ type: "progress",
1446
+ stage: 3,
1447
+ done: speedDone,
1448
+ total: stage2.length,
1449
+ message: `\u6D4B\u901F ${speedDone}/${stage2.length} ${result.peakMBps.toFixed(1)} MB/s`
1450
+ });
1451
+ return { ...item, speed: result.ok ? result.peakMBps : 0 };
1452
+ });
582
1453
  } else {
583
- report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5", total: stage2.length });
1454
+ report({ type: "stage", stage: 3, name: "\u771F\u5B9E\u4E0B\u884C\u901F\u5EA6\u6D4B\u8BD5\uFF08\u9879\u76EE\u63A2\u9488\u7AEF\u70B9\uFF09", total: stage2.length });
584
1455
  let speedDone = 0;
585
1456
  speedResults = await pool(stage2, speedConcurrency, async (item) => {
586
1457
  const result = await measureDownload(
@@ -599,16 +1470,19 @@ async function runScan(options) {
599
1470
  total: stage2.length,
600
1471
  message: `\u6D4B\u901F ${speedDone}/${stage2.length} ${result.speedMBps.toFixed(1)} MB/s`
601
1472
  });
602
- return { ...item, download: result };
1473
+ return { ...item, speed: result.ok ? result.speedMBps : 0 };
603
1474
  });
604
1475
  }
605
- const entries = speedResults.filter((r) => options.skipSpeed ? true : Boolean(r.download && r.download.ok && r.download.speedMBps > 0)).map((r) => ({
1476
+ const entries = speedResults.filter((r) => options.skipSpeed ? true : Boolean(r.speed && r.speed > 0)).map((r) => ({
606
1477
  ip: r.ip,
607
1478
  port: options.port,
608
1479
  colo: r.probe.colo || "UNK",
609
1480
  latency: r.latency,
610
- speed: r.download ? Number(r.download.speedMBps.toFixed(2)) : 0
611
- })).sort((a, b) => options.skipSpeed ? a.latency - b.latency : b.speed - a.speed || a.latency - b.latency).slice(0, top);
1481
+ tlsMs: r.tls.tlsMs,
1482
+ speed: r.speed ? Number(r.speed.toFixed(2)) : 0
1483
+ })).sort(
1484
+ (a, b) => options.skipSpeed ? a.tlsMs - b.tlsMs || a.latency - b.latency : b.speed - a.speed || a.tlsMs - b.tlsMs || a.latency - b.latency
1485
+ ).slice(0, top);
612
1486
  for (const entry of entries) {
613
1487
  report({ type: "result", entry });
614
1488
  }
@@ -617,9 +1491,13 @@ async function runScan(options) {
617
1491
  stats: {
618
1492
  candidates: candidates.length,
619
1493
  tcpOk: tcpOk.length,
1494
+ tlsOk: tlsOk.length,
620
1495
  wsOk: wsOk.length,
1496
+ verified: verifiedCount,
621
1497
  speedOk: entries.length,
622
- elapsedMs: Date.now() - started
1498
+ elapsedMs: Date.now() - started,
1499
+ countries: appliedCountries,
1500
+ wsFiltered: wsFiltered.length
623
1501
  }
624
1502
  };
625
1503
  report({ type: "done", count: entries.length });
@@ -857,6 +1735,8 @@ async function handleRequest(req, res) {
857
1735
  host: body.host.trim(),
858
1736
  port: body.port ?? 443,
859
1737
  candidates: plan.candidates,
1738
+ // 国家过滤(可多选):不传 = 全量;无效代码在 normalizeCountryFilter 里被丢弃
1739
+ countries: Array.isArray(body.countries) ? body.countries : void 0,
860
1740
  top: body.top ?? 15,
861
1741
  onProgress: (event) => broadcast(job, event)
862
1742
  });
@@ -924,7 +1804,7 @@ async function handleRequest(req, res) {
924
1804
  }
925
1805
 
926
1806
  // src/version.ts
927
- var VERSION = "0.2.0";
1807
+ var VERSION = "0.4.0";
928
1808
  var NAME = "cfpilot-scanner";
929
1809
 
930
1810
  // src/cli.ts
@@ -948,14 +1828,33 @@ scan \u9009\u9879:
948
1828
  --no-callback \u53EA\u6253\u5370\u7ED3\u679C\uFF0C\u4E0D\u56DE\u586B
949
1829
  --path <path> \u9636\u6BB5\u4E8C\u6821\u9A8C\u7684 WebSocket \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 /__edge_probe
950
1830
  \uFF08\u5BF9\u63A5\u5176\u5B83\u9879\u76EE\u65F6\u586B\u5B83\u81EA\u5DF1\u7684 WS \u8DEF\u5F84\uFF0C\u4F8B\u5982 /vless \u6216 /<uuid>\uFF09
951
- --probe-path <path> \u9636\u6BB5\u4E09\u6D4B\u901F\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 /__edge_probe
952
- --no-speed \u8DF3\u8FC7\u6D4B\u901F\uFF0C\u53EA\u6309\u5EF6\u8FDF\u6392\u5E8F\uFF08\u76EE\u6807\u6CA1\u6709 CFPilot \u63A2\u9488\u65F6\u7528\uFF09
1831
+ --probe-path <path> probe \u6A21\u5F0F\u7684\u6D4B\u901F\u8DEF\u5F84\uFF0C\u9ED8\u8BA4 /__edge_probe
1832
+ --speed-mode <mode> \u6D4B\u901F\u65B9\u5F0F\uFF1Aauto\uFF08\u9ED8\u8BA4\uFF09/ probe / url
1833
+ auto = \u76EE\u6807\u6709\u63A2\u9488\u7AEF\u70B9\u5C31\u7528\u5B83\uFF0C\u5426\u5219\u81EA\u52A8\u6539\u7528\u516C\u5F00\u6D4B\u901F\u5730\u5740
1834
+ probe = \u53EA\u7528\u76EE\u6807\u9879\u76EE\u7684\u63A2\u9488\u7AEF\u70B9\uFF08\u6700\u5FE0\u5B9E\uFF0C\u8981\u6C42\u5BF9\u65B9\u6709\u8BE5\u7AEF\u70B9\uFF09
1835
+ url = \u53EA\u7528\u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF08\u4EFB\u4F55\u9879\u76EE\u90FD\u80FD\u6D4B\uFF09
1836
+ --speed-url <url> \u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF0C\u9ED8\u8BA4 ${DEFAULT_SPEED_URL}
1837
+ \u53EF\u6362\u6210\u4EFB\u610F\u8FD4\u56DE\u5927\u6587\u4EF6\u7684\u5730\u5740
1838
+ --speed-conns <n> \u6D4B\u901F\u5E76\u53D1\u8FDE\u63A5\u6570\uFF0C\u9ED8\u8BA4 3
1839
+ \u5355\u6761 TCP \u8FDE\u63A5\u53D7\u62E5\u585E\u7A97\u53E3\u9650\u5236\uFF0C\u591A\u8FDE\u63A5\u66F4\u63A5\u8FD1\u771F\u5B9E\u5E26\u5BBD
1840
+ --speed-duration <ms> \u6D4B\u901F\u65F6\u957F\uFF0C\u9ED8\u8BA4 6000
1841
+ --no-verify \u9636\u6BB5\u4E8C\u53EA\u505A 101 \u63E1\u624B\uFF0C\u8DF3\u8FC7\u54CD\u5E94\u4F53\u6821\u9A8C
1842
+ \uFF08\u9ED8\u8BA4\u6821\u9A8C\uFF1A\u53D1 PING \u5E76\u9A8C\u8BC1\u6570\u636E\u9762\u56DE {"pong":true}\uFF09
1843
+ --no-speed \u8DF3\u8FC7\u6D4B\u901F\uFF0C\u53EA\u6309\u5EF6\u8FDF\u6392\u5E8F
953
1844
  --top <n> \u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4 15
954
- --concurrency <n> \u9636\u6BB5\u4E00\u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
1845
+ --concurrency <n> \u9636\u6BB5\u4E00 TCP \u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
1846
+ --tls-concurrency <n> \u9636\u6BB5\u4E00 TLS \u63E1\u624B\u5E76\u53D1\uFF0C\u9ED8\u8BA4 64
1847
+ TLS \u63E1\u624B\u662F CPU \u5BC6\u96C6\u578B\u64CD\u4F5C\uFF0C\u8FC7\u9AD8\u4F1A\u8BA9\u672C\u673A\u6210\u4E3A\u74F6\u9888
955
1848
  --sample <n> \u5019\u9009 IP \u91C7\u6837\u6570\uFF0C\u9ED8\u8BA4 20000
1849
+ \u5EFA\u8BAE 6556 \u8D77\uFF1A\u9636\u6BB5\u4E00\u6309 TLS \u63E1\u624B\u8017\u65F6\u7B5B\u9009\uFF0C\u800C\u63E1\u624B\u5FEB\u6162
1850
+ \u4E0E TCP \u5EF6\u8FDF\u51E0\u4E4E\u65E0\u5173\uFF08\u5B9E\u6D4B TCP 9ms \u7684 IP \u53EF\u80FD\u8981 3.5 \u79D2\uFF09\uFF0C
1851
+ \u91C7\u6837\u5C0F\u4E86\u6839\u672C\u78B0\u4E0D\u5230\u63E1\u624B\u5FEB\u7684 IP
1852
+ --countries <a,b> \u56FD\u5BB6 / \u5730\u533A\u8FC7\u6EE4\uFF08\u53EF\u591A\u9009\uFF0C\u9017\u53F7\u5206\u9694\uFF0C\u5982 HK,JP,SG\uFF09
1853
+ \u4E0D\u9009 = \u5168\u91CF\u3002\u673A\u623F\u6765\u81EA 101 \u63E1\u624B\u54CD\u5E94\uFF1B\u7B5B\u5B8C\u4E3A\u7A7A\u4F1A\u81EA\u52A8\u5FFD\u7565
956
1854
  --cidr <a,b> \u81EA\u5B9A\u4E49\u7F51\u6BB5\uFF08\u53EF\u91CD\u590D\uFF0C\u7528\u9017\u53F7\u5206\u9694\uFF09
957
1855
  --ip-file <path> \u4F7F\u7528\u672C\u5730 IP \u5217\u8868\u6587\u4EF6
958
- --max-latency <ms> \u9636\u6BB5\u4E00\u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
1856
+ --max-latency <ms> \u9636\u6BB5\u4E00 TCP \u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
1857
+ --tls-timeout <ms> \u9636\u6BB5\u4E00 TLS \u63E1\u624B\u8D85\u65F6\uFF0C\u9ED8\u8BA4 4000
959
1858
  --probe-limit <n> \u8FDB\u5165 101 \u6821\u9A8C\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 120
960
1859
  --speed-limit <n> \u8FDB\u5165\u6D4B\u901F\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 30
961
1860
  --speed-bytes <n> \u6D4B\u901F\u5B57\u8282\u6570\uFF0C\u9ED8\u8BA4 5242880
@@ -972,6 +1871,35 @@ serve \u9009\u9879:
972
1871
  ${NAME} scan --host cfpilot.example.com --token abc123
973
1872
  ${NAME} serve
974
1873
  `;
1874
+ function normalizeWsPath(raw, fallback) {
1875
+ const value = (raw ?? fallback).trim();
1876
+ if (!value) return fallback;
1877
+ if (isMsysPollutedPath(value)) {
1878
+ process.stderr.write(
1879
+ `
1880
+ \u2717 --path \u88AB MSYS\uFF08Git-Bash\uFF09\u8F6C\u6362\u6210\u4E86 Windows \u8DEF\u5F84\uFF1A
1881
+ \u6536\u5230\u7684\u662F\uFF1A${value}
1882
+
1883
+ \u8FD9\u662F Git-Bash \u7684\u81EA\u52A8\u8DEF\u5F84\u8F6C\u6362\u884C\u4E3A\uFF0C\u4E0D\u662F\u672C\u5DE5\u5177\u7684\u95EE\u9898\u3002
1884
+ \u4E09\u79CD\u7ED5\u5F00\u65B9\u5F0F\uFF0C\u4EFB\u9009\u5176\u4E00\uFF1A
1885
+ 1. \u7528\u7B49\u53F7\u5199\u6CD5\uFF1A --path=/vless
1886
+ 2. \u52A0\u73AF\u5883\u53D8\u91CF\uFF1A MSYS_NO_PATHCONV=1 cfpilot-scanner scan ...
1887
+ 3. \u6362\u7528 PowerShell / CMD \u6267\u884C
1888
+ `
1889
+ );
1890
+ process.exit(1);
1891
+ }
1892
+ if (!value.startsWith("/")) {
1893
+ process.stderr.write(
1894
+ `
1895
+ \u2717 --path \u5FC5\u987B\u4EE5 / \u5F00\u5934\uFF0C\u6536\u5230\u7684\u662F\uFF1A\u300C${value}\u300D
1896
+ \u4F8B\u5982\uFF1A--path /vless \u6216 --path /\uFF08\u76EE\u6807\u9879\u76EE\u7528\u6839\u8DEF\u5F84\u65F6\uFF09
1897
+ `
1898
+ );
1899
+ process.exit(1);
1900
+ }
1901
+ return value;
1902
+ }
975
1903
  async function main() {
976
1904
  const args = parseArgs(process.argv.slice(2));
977
1905
  const command = args.command && !args.command.startsWith("-") ? args.command : "";
@@ -1067,15 +1995,16 @@ function printTable(entries) {
1067
1995
  process.stdout.write("\n\u6CA1\u6709\u901A\u8FC7\u5168\u90E8\u6821\u9A8C\u7684\u8282\u70B9\u3002\n");
1068
1996
  return;
1069
1997
  }
1070
- process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F \u5EF6\u8FDF(ms) \u4E0B\u884C(MB/s)\n");
1071
- process.stdout.write("\u2500".repeat(58) + "\n");
1998
+ process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F TLS(ms) TCP(ms) \u4E0B\u884C(MB/s)\n");
1999
+ process.stdout.write("\u2500".repeat(66) + "\n");
1072
2000
  entries.forEach((entry, index) => {
1073
2001
  const rank = String(index + 1).padStart(2, " ");
1074
2002
  const address = `${entry.ip}:${entry.port}`.padEnd(24, " ");
1075
2003
  const colo = (entry.colo || "UNK").padEnd(6, " ");
2004
+ const tls3 = String(entry.tlsMs).padStart(7, " ");
1076
2005
  const latency = String(entry.latency).padStart(7, " ");
1077
2006
  const speed = entry.speed.toFixed(1).padStart(9, " ");
1078
- process.stdout.write(`${rank} ${address} ${colo} ${latency} ${speed}
2007
+ process.stdout.write(`${rank} ${address} ${colo} ${tls3} ${latency} ${speed}
1079
2008
  `);
1080
2009
  });
1081
2010
  }
@@ -1097,9 +2026,11 @@ async function commandScan(args) {
1097
2026
  const top = flagNumber(args, "top", 15);
1098
2027
  const token = flagString(args, "token");
1099
2028
  const noCallback = flagBool(args, "no-callback");
1100
- const wsPath = flagString(args, "path", "/__edge_probe");
1101
- const probePath = flagString(args, "probe-path", "/__edge_probe");
2029
+ const wsPath = normalizeWsPath(flagString(args, "path"), "/__edge_probe");
2030
+ const probePath = normalizeWsPath(flagString(args, "probe-path"), "/__edge_probe");
1102
2031
  const noSpeed = flagBool(args, "no-speed");
2032
+ const verifyPayload = !flagBool(args, "no-verify");
2033
+ const countries = normalizeCountryFilter(flagList(args, "countries"));
1103
2034
  const callback = noCallback ? "" : flagString(args, "callback", token ? `https://${host}/api/v1/preferred-ips` : "");
1104
2035
  if (!quiet) {
1105
2036
  process.stdout.write(`
@@ -1107,8 +2038,31 @@ ${NAME} v${VERSION}
1107
2038
  `);
1108
2039
  process.stdout.write(`\u76EE\u6807\u57DF\u540D\uFF1A${host}:${port}\uFF08\u5C06\u4F5C\u4E3A TLS SNI \u4E0E HTTP Host\uFF09
1109
2040
  `);
1110
- process.stdout.write("\u6B63\u5728\u51C6\u5907 Cloudflare \u7F51\u6BB5\u2026\n");
2041
+ process.stdout.write(`WS \u8DEF\u5F84\uFF1A${wsPath}
2042
+ `);
2043
+ if (countries.length > 0) {
2044
+ process.stdout.write(`\u56FD\u5BB6\u7B5B\u9009\uFF1A${countries.join(" / ")}\uFF08\u4E0D\u586B\u5219\u5168\u91CF\uFF09
2045
+ `);
2046
+ }
2047
+ }
2048
+ if (!quiet) {
2049
+ const env = await detectProxyInterference(host);
2050
+ if (!env.clean) {
2051
+ process.stdout.write("\n\u26A0\uFE0F \u68C0\u6D4B\u5230\u53EF\u80FD\u5E72\u6270\u4F18\u9009\u7684\u56E0\u7D20\uFF1A\n");
2052
+ for (const f of env.findings) {
2053
+ process.stdout.write(` ${f.message}
2054
+ `);
2055
+ process.stdout.write(` \u2192 ${f.advice}
2056
+ `);
2057
+ }
2058
+ process.stdout.write("\n \u4F18\u9009\u6D4B\u7684\u662F\u300C\u672C\u673A\u76F4\u8FDE\u8BE5 IP\u300D\u7684\u771F\u5B9E\u8D28\u91CF\u3002\n");
2059
+ process.stdout.write(" \u6D41\u91CF\u88AB\u4EE3\u7406\u63A5\u7BA1\u65F6\uFF0C\u7ED3\u679C\u770B\u8D77\u6765\u6B63\u5E38\uFF0C\u4F46\u6240\u6709 IP \u6210\u7EE9\u8D8B\u4E8E\u4E00\u81F4\uFF0C\n");
2060
+ process.stdout.write(" \u5BFC\u5165\u5BA2\u6237\u7AEF\u540E\u5927\u91CF -1\uFF08\u56E0\u4E3A\u4ECE\u672A\u771F\u6B63\u76F4\u8FDE\u9A8C\u8BC1\u8FC7\uFF09\u3002\n");
2061
+ process.stdout.write(" \u2192 \u5EFA\u8BAE\u5173\u95ED\u4EE3\u7406\u5DE5\u5177\uFF08\u542B TUN \u6A21\u5F0F\u4E0E\u7CFB\u7EDF\u4EE3\u7406\uFF09\u540E\u91CD\u8DD1\u3002\n");
2062
+ process.stdout.write(" \u2192 \u6216\u628A\u672C\u7A0B\u5E8F\u4E0E\u5F85\u6D4B IP \u6BB5\u8BBE\u4E3A\u76F4\u8FDE\u3002\n\n");
2063
+ }
1111
2064
  }
2065
+ if (!quiet) process.stdout.write("\u6B63\u5728\u51C6\u5907 Cloudflare \u7F51\u6BB5\u2026\n");
1112
2066
  const candidates = await resolveCandidatesFromArgs(args);
1113
2067
  if (candidates.length === 0) {
1114
2068
  process.stderr.write("\u5019\u9009 IP \u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5 --ip-file / --cidr \u53C2\u6570\n");
@@ -1117,38 +2071,69 @@ ${NAME} v${VERSION}
1117
2071
  if (!quiet) process.stdout.write(`\u5019\u9009 IP\uFF1A${candidates.length} \u4E2A
1118
2072
  `);
1119
2073
  let skipSpeed = noSpeed;
2074
+ let speedMode = "probe";
2075
+ let speedUrl = flagString(args, "speed-url", DEFAULT_SPEED_URL);
1120
2076
  if (!skipSpeed) {
1121
- try {
1122
- const probe = await fetch(`https://${host}${probePath}?bytes=1024`, {
1123
- method: "GET",
1124
- signal: AbortSignal.timeout(8e3)
1125
- });
1126
- if (!probe.headers.get("x-probe-bytes")) {
1127
- skipSpeed = true;
2077
+ const forcedMode = flagString(args, "speed-mode");
2078
+ if (forcedMode === "url") {
2079
+ speedMode = "url";
2080
+ if (!quiet) process.stdout.write(`\u6D4B\u901F\u65B9\u5F0F\uFF1A\u516C\u5F00\u6D4B\u901F\u5730\u5740\uFF08${speedUrl}\uFF09
2081
+ `);
2082
+ } else if (forcedMode === "probe") {
2083
+ speedMode = "probe";
2084
+ } else {
2085
+ let hasProbe = false;
2086
+ try {
2087
+ const probe = await fetch(`https://${host}${probePath}?bytes=1024`, {
2088
+ method: "GET",
2089
+ signal: AbortSignal.timeout(8e3)
2090
+ });
2091
+ hasProbe = Boolean(probe.headers.get("x-probe-bytes"));
2092
+ } catch {
2093
+ hasProbe = false;
2094
+ }
2095
+ if (hasProbe) {
2096
+ speedMode = "probe";
2097
+ if (!quiet) process.stdout.write("\u6D4B\u901F\u65B9\u5F0F\uFF1A\u9879\u76EE\u63A2\u9488\u7AEF\u70B9\uFF08\u6700\u5FE0\u5B9E\uFF09\n");
2098
+ } else {
2099
+ speedMode = "url";
1128
2100
  if (!quiet) {
1129
- process.stdout.write("\n\u63D0\u793A\uFF1A" + probePath + " \u4E0D\u662F CFPilot \u63A2\u9488\u7AEF\u70B9\uFF0C\u5DF2\u8DF3\u8FC7\u6D4B\u901F\uFF08\u53EA\u6309\u5EF6\u8FDF\u6392\u5E8F\uFF09\n");
1130
- process.stdout.write(" \u5982\u9700\u6D4B\u901F\uFF0C\u8BF7\u7528 --probe-path \u6307\u5B9A\u76EE\u6807\u9879\u76EE\u7684\u6D4B\u901F\u8DEF\u5F84\n");
2101
+ process.stdout.write(`
2102
+ \u63D0\u793A\uFF1A${probePath} \u4E0D\u662F CFPilot \u63A2\u9488\u7AEF\u70B9\uFF0C\u6539\u7528\u516C\u5F00\u6D4B\u901F\u5730\u5740\u6D4B\u901F
2103
+ `);
2104
+ process.stdout.write(" \uFF08\u4ECD\u4F1A\u5148\u505A 101 \u6821\u9A8C\uFF0C\u56E0\u6B64\u4E0D\u4F1A\u9009\u51FA\u8FDE\u4E0D\u4E0A\u7684 IP\uFF09\n");
1131
2105
  }
2106
+ speedUrl = await pickAvailableSpeedUrl(
2107
+ [speedUrl, ...SPEED_URL_PRESETS.map((p) => p.url)],
2108
+ quiet ? void 0 : (m) => process.stdout.write(` ${m}
2109
+ `)
2110
+ );
1132
2111
  }
1133
- } catch {
1134
- skipSpeed = true;
1135
2112
  }
1136
2113
  }
1137
2114
  const summary = await runScan({
1138
2115
  host,
1139
2116
  port,
1140
2117
  candidates,
2118
+ countries,
1141
2119
  wsPath,
1142
2120
  probePath,
1143
2121
  skipSpeed,
2122
+ verifyPayload,
1144
2123
  concurrency: flagNumber(args, "concurrency", 300),
1145
2124
  tcpTimeoutMs: flagNumber(args, "timeout", 1200),
2125
+ tlsConcurrency: flagNumber(args, "tls-concurrency", 64),
2126
+ tlsTimeoutMs: flagNumber(args, "tls-timeout", 4e3),
1146
2127
  maxLatencyMs: flagNumber(args, "max-latency", 0),
1147
2128
  probeLimit: flagNumber(args, "probe-limit", 120),
1148
2129
  probeTimeoutMs: flagNumber(args, "probe-timeout", 3e3),
1149
2130
  speedLimit: flagNumber(args, "speed-limit", 30),
1150
2131
  speedTimeoutMs: flagNumber(args, "speed-timeout", 12e3),
1151
2132
  speedBytes: flagNumber(args, "speed-bytes", 5 * 1024 * 1024),
2133
+ speedMode,
2134
+ speedUrl,
2135
+ speedConnections: flagNumber(args, "speed-conns", 3),
2136
+ speedDurationMs: flagNumber(args, "speed-duration", 6e3),
1152
2137
  top,
1153
2138
  onProgress: (event) => printProgress(event, quiet)
1154
2139
  });