cfpilot-scanner 0.3.0 → 0.4.1

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 (2) hide show
  1. package/dist/cli.js +514 -87
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -95,6 +95,59 @@ var BUILTIN_IPV4_CIDRS = [
95
95
  "172.64.0.0/13",
96
96
  "131.0.72.0/22"
97
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
+ }
98
151
  var BUILTIN_IPV6_CIDRS = [
99
152
  "2400:cb00::/32",
100
153
  "2606:4700::/32",
@@ -143,7 +196,7 @@ function parseCidr(cidr) {
143
196
  const hostBits2 = 128 - prefix;
144
197
  const size2 = 1n << BigInt(hostBits2);
145
198
  const mask2 = hostBits2 === 0 ? 0n : (1n << BigInt(hostBits2)) - 1n;
146
- return { family: 6, base: base & ~mask2, prefix, size: size2 };
199
+ return { family: 6, base: base & ~mask2, prefix, size: size2, cidr: cidr.trim() };
147
200
  }
148
201
  if (prefix < 0 || prefix > 32) return null;
149
202
  const value = ipv4ToBigInt(addr);
@@ -151,7 +204,7 @@ function parseCidr(cidr) {
151
204
  const hostBits = 32 - prefix;
152
205
  const size = 1n << BigInt(hostBits);
153
206
  const mask = hostBits === 0 ? 0n : (1n << BigInt(hostBits)) - 1n;
154
- return { family: 4, base: value & ~mask, prefix, size };
207
+ return { family: 4, base: value & ~mask, prefix, size, cidr: cidr.trim() };
155
208
  }
156
209
  function ipv4ToBigInt(addr) {
157
210
  const parts = addr.split(".");
@@ -213,10 +266,41 @@ function bigIntToIpv6(value) {
213
266
  const tail = groups.slice(bestStart + bestLen).join(":");
214
267
  return `${head}::${tail}`;
215
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
+ }
216
298
  function expandCidrs(cidrs, options = {}) {
217
299
  const sample = options.sample ?? 2e4;
218
300
  const random = makeRandom(options.seed ?? 24301);
219
- 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);
220
304
  if (parsed.length === 0) return [];
221
305
  if (sample <= 0) {
222
306
  const all = [];
@@ -233,26 +317,48 @@ function expandCidrs(cidrs, options = {}) {
233
317
  for (const family of [4, 6]) {
234
318
  const group = parsed.filter((c) => c.family === family);
235
319
  if (group.length === 0) continue;
236
- const familyTotal = group.reduce((sum, c) => sum + c.size, 0n);
237
- if (familyTotal === 0n) continue;
238
320
  const hasOtherFamily = parsed.some((c) => c.family !== family);
239
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
+ };
240
344
  const plans = group.map((cidr) => {
241
- const ratio = Number(cidr.size * BigInt(familyBudget) / familyTotal);
242
- const wanted = Math.max(minPerCidr, Math.min(2e4, ratio));
345
+ const ratio = Number(allocOf(cidr) * BigInt(familyBudget) / familyTotal);
346
+ const wanted = Math.max(minForCidr(cidr), Math.min(2e4, ratio));
243
347
  return { cidr, count: Math.min(Number(cidr.size), wanted) };
244
348
  });
245
- let familySum = plans.reduce((sum, p) => sum + p.count, 0);
349
+ const usablePlans = allZero ? plans : plans.filter((p) => effectiveSize(p.cidr) > 0n);
350
+ let familySum = usablePlans.reduce((sum, p) => sum + p.count, 0);
246
351
  while (familySum > familyBudget) {
247
352
  let target = null;
248
- for (const plan of plans) {
353
+ for (const plan of usablePlans) {
249
354
  if (plan.count > 1 && (!target || plan.count > target.count)) target = plan;
250
355
  }
251
356
  if (!target) break;
252
357
  target.count -= 1;
253
358
  familySum -= 1;
254
359
  }
255
- for (const { cidr, count } of plans) {
360
+ for (const { cidr, count } of usablePlans) {
361
+ if (count <= 0) continue;
256
362
  if (count >= Number(cidr.size)) {
257
363
  for (let i = 0n; i < cidr.size; i++) {
258
364
  const value = cidr.base + i;
@@ -300,69 +406,6 @@ function readIpFile(text) {
300
406
  return out;
301
407
  }
302
408
 
303
- // src/candidates.ts
304
- var DEFAULT_CANDIDATE_SAMPLE = 2e4;
305
- var DEFAULT_CANDIDATE_SEED = 24301;
306
- var DEFAULT_BRIDGE_SAMPLE = 300;
307
- function normalizeCidrInput(cidr) {
308
- if (cidr === void 0) return [];
309
- const list = Array.isArray(cidr) ? cidr : [cidr];
310
- const out = [];
311
- for (const item of list) {
312
- if (typeof item !== "string") continue;
313
- for (const part of item.split(/[\n,]+/)) {
314
- const value = part.trim();
315
- if (value) out.push(value);
316
- }
317
- }
318
- return out;
319
- }
320
- function isIpv6Cidr(cidr) {
321
- return cidr.includes(":");
322
- }
323
- function filterByFamily(cidrs, family) {
324
- if (family === "both") return cidrs;
325
- const wantIpv6 = family === "v6";
326
- return cidrs.filter((cidr) => isIpv6Cidr(cidr) === wantIpv6);
327
- }
328
- async function loadCloudflareCidrs(family) {
329
- let ipv4 = BUILTIN_IPV4_CIDRS;
330
- let ipv6 = BUILTIN_IPV6_CIDRS;
331
- try {
332
- const ranges = await fetchCloudflareCidrs();
333
- if (ranges.ipv4.length > 0) ipv4 = ranges.ipv4;
334
- if (ranges.ipv6.length > 0) ipv6 = ranges.ipv6;
335
- } catch {
336
- }
337
- if (family === "v4") return [...ipv4];
338
- if (family === "v6") return [...ipv6];
339
- return [...ipv4, ...ipv6];
340
- }
341
- async function resolveCandidates(opts = {}) {
342
- const sample = opts.sample ?? DEFAULT_CANDIDATE_SAMPLE;
343
- const seed = opts.seed ?? DEFAULT_CANDIDATE_SEED;
344
- const family = opts.family ?? "both";
345
- const customCidrs = normalizeCidrInput(opts.cidr);
346
- if (customCidrs.length > 0) {
347
- return expandCidrs(filterByFamily(customCidrs, family), { sample, seed });
348
- }
349
- const cidrs = await loadCloudflareCidrs(family);
350
- return expandCidrs(cidrs, { sample, seed });
351
- }
352
- function normalizeCandidateList(input) {
353
- if (!Array.isArray(input) || input.length === 0) return [];
354
- const lines = input.filter((item) => typeof item === "string");
355
- if (lines.length === 0) return [];
356
- const seen = /* @__PURE__ */ new Set();
357
- const out = [];
358
- for (const ip of readIpFile(lines.join("\n"))) {
359
- if (seen.has(ip)) continue;
360
- seen.add(ip);
361
- out.push(ip);
362
- }
363
- return out;
364
- }
365
-
366
409
  // src/net-env.ts
367
410
  import * as net from "node:net";
368
411
  import { execFile } from "node:child_process";
@@ -426,6 +469,44 @@ function isFakeIp(ip) {
426
469
  const [a, b] = parts;
427
470
  return a === 198 && (b === 18 || b === 19);
428
471
  }
472
+ function isPublicAddress(ip) {
473
+ const value = ip.trim();
474
+ if (!value) return false;
475
+ if (!value.includes(":")) {
476
+ const parts = value.split(".");
477
+ if (parts.length !== 4) return false;
478
+ const nums = [];
479
+ for (const p of parts) {
480
+ if (!/^\d{1,3}$/.test(p)) return false;
481
+ const n = Number(p);
482
+ if (n > 255) return false;
483
+ nums.push(n);
484
+ }
485
+ const [a, b] = nums;
486
+ if (a === 0) return false;
487
+ if (a === 10) return false;
488
+ if (a === 127) return false;
489
+ if (a >= 224) return false;
490
+ if (a === 169 && b === 254) return false;
491
+ if (a === 172 && b >= 16 && b <= 31) return false;
492
+ if (a === 192 && b === 168) return false;
493
+ if (a === 100 && b >= 64 && b <= 127) return false;
494
+ if (a === 198 && (b === 18 || b === 19)) return false;
495
+ return true;
496
+ }
497
+ const lower = value.toLowerCase();
498
+ if (lower.startsWith("::ffff:") && lower.includes(".")) {
499
+ return isPublicAddress(lower.slice(7));
500
+ }
501
+ const groups = lower.split(":");
502
+ if (groups.length < 3) return false;
503
+ const first = groups[0];
504
+ if (!/^[0-9a-f]{1,4}$/.test(first)) return false;
505
+ const firstByte = Number.parseInt(first.slice(0, 2).padEnd(2, "0"), 16);
506
+ if (firstByte < 32 || firstByte > 63) return false;
507
+ if (lower.startsWith("2001:db8:")) return false;
508
+ return true;
509
+ }
429
510
  async function detectProxyInterference(host) {
430
511
  const findings = [];
431
512
  const listening = [];
@@ -468,6 +549,74 @@ function isMsysPollutedPath(value) {
468
549
  return looksWindows || hasMsysHint;
469
550
  }
470
551
 
552
+ // src/candidates.ts
553
+ var DEFAULT_CANDIDATE_SAMPLE = 2e4;
554
+ var DEFAULT_CANDIDATE_SEED = 24301;
555
+ var DEFAULT_BRIDGE_SAMPLE = 6556;
556
+ function normalizeCidrInput(cidr) {
557
+ if (cidr === void 0) return [];
558
+ const list = Array.isArray(cidr) ? cidr : [cidr];
559
+ const out = [];
560
+ for (const item of list) {
561
+ if (typeof item !== "string") continue;
562
+ for (const part of item.split(/[\n,]+/)) {
563
+ const value = part.trim();
564
+ if (value) out.push(value);
565
+ }
566
+ }
567
+ return out;
568
+ }
569
+ function isIpv6Cidr(cidr) {
570
+ return cidr.includes(":");
571
+ }
572
+ function filterByFamily(cidrs, family) {
573
+ if (family === "both") return cidrs;
574
+ const wantIpv6 = family === "v6";
575
+ return cidrs.filter((cidr) => isIpv6Cidr(cidr) === wantIpv6);
576
+ }
577
+ async function loadCloudflareCidrs(family) {
578
+ let ipv4 = BUILTIN_IPV4_CIDRS;
579
+ let ipv6 = BUILTIN_IPV6_CIDRS;
580
+ try {
581
+ const ranges = await fetchCloudflareCidrs();
582
+ if (ranges.ipv4.length > 0) ipv4 = ranges.ipv4;
583
+ if (ranges.ipv6.length > 0) ipv6 = ranges.ipv6;
584
+ } catch {
585
+ }
586
+ if (family === "v4") return [...ipv4];
587
+ if (family === "v6") return [...ipv6];
588
+ return [...ipv4, ...ipv6];
589
+ }
590
+ async function resolveCandidates(opts = {}) {
591
+ const sample = opts.sample ?? DEFAULT_CANDIDATE_SAMPLE;
592
+ const seed = opts.seed ?? DEFAULT_CANDIDATE_SEED;
593
+ const family = opts.family ?? "both";
594
+ const customCidrs = normalizeCidrInput(opts.cidr);
595
+ if (customCidrs.length > 0) {
596
+ return expandCidrs(filterByFamily(customCidrs, family), { sample, seed });
597
+ }
598
+ const cidrs = await loadCloudflareCidrs(family);
599
+ return expandCidrs(cidrs, { sample, seed });
600
+ }
601
+ function normalizeCandidateList(input) {
602
+ if (!Array.isArray(input) || input.length === 0) return [];
603
+ const lines = input.filter((item) => typeof item === "string");
604
+ if (lines.length === 0) return [];
605
+ const seen = /* @__PURE__ */ new Set();
606
+ const out = [];
607
+ for (const ip of readIpFile(lines.join("\n"))) {
608
+ if (seen.has(ip)) continue;
609
+ seen.add(ip);
610
+ const isIpv4Literal = /^\d{1,3}(\.\d{1,3}){3}$/.test(ip);
611
+ const isIpv6Literal = ip.includes(":");
612
+ if (isIpv4Literal || isIpv6Literal) {
613
+ if (!isPublicAddress(ip)) continue;
614
+ }
615
+ out.push(ip);
616
+ }
617
+ return out;
618
+ }
619
+
471
620
  // src/speed-url.ts
472
621
  import * as tls from "node:tls";
473
622
  var SPEED_URL_PRESETS = [
@@ -670,6 +819,216 @@ function probeWs101(ip, port, host, timeoutMs = 3e3, path = "/__edge_probe") {
670
819
  });
671
820
  });
672
821
  }
822
+ function encodeWsFrame(payload, opcode = 2) {
823
+ const mask = crypto.randomBytes(4);
824
+ const masked = Buffer.alloc(payload.length);
825
+ for (let i = 0; i < payload.length; i++) {
826
+ masked[i] = payload[i] ^ mask[i % 4];
827
+ }
828
+ let header;
829
+ if (payload.length < 126) {
830
+ header = Buffer.from([128 | opcode, 128 | payload.length]);
831
+ } else if (payload.length < 65536) {
832
+ header = Buffer.alloc(4);
833
+ header[0] = 128 | opcode;
834
+ header[1] = 254;
835
+ header.writeUInt16BE(payload.length, 2);
836
+ } else {
837
+ header = Buffer.alloc(10);
838
+ header[0] = 128 | opcode;
839
+ header[1] = 255;
840
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
841
+ }
842
+ return Buffer.concat([header, mask, masked]);
843
+ }
844
+ function decodeWsFrame(buf) {
845
+ if (buf.length < 2) return null;
846
+ const opcode = buf[0] & 15;
847
+ const len7 = buf[1] & 127;
848
+ let offset;
849
+ let payloadLen;
850
+ if (len7 < 126) {
851
+ offset = 2;
852
+ payloadLen = len7;
853
+ } else if (len7 === 126) {
854
+ if (buf.length < 4) return null;
855
+ offset = 4;
856
+ payloadLen = buf.readUInt16BE(2);
857
+ } else {
858
+ if (buf.length < 10) return null;
859
+ offset = 10;
860
+ payloadLen = Number(buf.readBigUInt64BE(2));
861
+ }
862
+ if (buf.length < offset + payloadLen) return null;
863
+ const frame = {
864
+ opcode,
865
+ payload: Buffer.from(buf.subarray(offset, offset + payloadLen))
866
+ };
867
+ const rest = Buffer.from(buf.subarray(offset + payloadLen));
868
+ return { frame, rest };
869
+ }
870
+ function probeWsPing(ip, port, host, timeoutMs = 5e3, path = "/__edge_probe") {
871
+ return new Promise((resolve) => {
872
+ const started = Date.now();
873
+ const key = crypto.randomBytes(16).toString("base64");
874
+ let settled = false;
875
+ let handshakeDone = false;
876
+ let buf = Buffer.alloc(0);
877
+ let receivedText = "";
878
+ const finish = (result) => {
879
+ if (settled) return;
880
+ settled = true;
881
+ clearTimeout(timer);
882
+ try {
883
+ socket.destroy();
884
+ } catch {
885
+ }
886
+ resolve({ ...result, latencyMs: Date.now() - started });
887
+ };
888
+ const request = [
889
+ `GET ${path} HTTP/1.1`,
890
+ `Host: ${host}`,
891
+ `Upgrade: websocket`,
892
+ `Connection: Upgrade`,
893
+ `Sec-WebSocket-Key: ${key}`,
894
+ `Sec-WebSocket-Version: 13`,
895
+ `User-Agent: ${USER_AGENT}`,
896
+ "",
897
+ ""
898
+ ].join("\r\n");
899
+ const socket = tls2.connect(
900
+ {
901
+ host: ip,
902
+ port,
903
+ servername: host,
904
+ rejectUnauthorized: false,
905
+ ALPNProtocols: ["http/1.1"]
906
+ },
907
+ () => {
908
+ socket.write(request);
909
+ }
910
+ );
911
+ const timer = setTimeout(() => {
912
+ finish({
913
+ ok: false,
914
+ colo: "",
915
+ status: 0,
916
+ error: handshakeDone ? "PING \u8D85\u65F6\uFF08\u6570\u636E\u9762\u672A\u54CD\u5E94\uFF09" : "\u63E1\u624B\u8D85\u65F6",
917
+ pongVerified: handshakeDone ? false : void 0
918
+ });
919
+ }, timeoutMs);
920
+ socket.setTimeout(timeoutMs, () => {
921
+ finish({
922
+ ok: false,
923
+ colo: "",
924
+ status: 0,
925
+ error: handshakeDone ? "PING \u8D85\u65F6\uFF08\u6570\u636E\u9762\u672A\u54CD\u5E94\uFF09" : "\u8FDE\u63A5\u8D85\u65F6",
926
+ pongVerified: handshakeDone ? false : void 0
927
+ });
928
+ });
929
+ socket.on("error", (err) => {
930
+ finish({
931
+ ok: false,
932
+ colo: "",
933
+ status: 0,
934
+ error: err.message,
935
+ pongVerified: handshakeDone ? false : void 0
936
+ });
937
+ });
938
+ socket.on("data", (chunk) => {
939
+ buf = Buffer.concat([buf, chunk]);
940
+ if (!handshakeDone) {
941
+ const lineEnd = buf.indexOf("\r\n\r\n");
942
+ if (lineEnd < 0) {
943
+ if (buf.length > 8192) finish({ ok: false, colo: "", status: 0, error: "\u54CD\u5E94\u5934\u5F02\u5E38" });
944
+ return;
945
+ }
946
+ const head = buf.slice(0, lineEnd).toString("latin1");
947
+ const statusLine = head.split("\r\n")[0] ?? "";
948
+ const statusMatch = /^HTTP\/1\.[01]\s+(\d{3})/.exec(statusLine);
949
+ const status = statusMatch ? Number(statusMatch[1]) : 0;
950
+ const colo = extractColo(head);
951
+ buf = buf.subarray(lineEnd + 4);
952
+ if (status !== 101) {
953
+ finish({ ok: false, colo, status, error: `HTTP ${status}` });
954
+ return;
955
+ }
956
+ handshakeDone = true;
957
+ socket.write(encodeWsFrame(new TextEncoder().encode("PING"), 1));
958
+ }
959
+ for (; ; ) {
960
+ const decoded = decodeWsFrame(buf);
961
+ if (!decoded) break;
962
+ buf = decoded.rest;
963
+ const { opcode, payload } = decoded.frame;
964
+ if (opcode === 8) {
965
+ const code = payload.length >= 2 ? payload.readUInt16BE(0) : 1005;
966
+ finish({
967
+ ok: false,
968
+ colo: "",
969
+ status: 101,
970
+ error: `WS \u5173\u95ED\uFF08code ${code}\uFF09`,
971
+ pongVerified: false
972
+ });
973
+ return;
974
+ }
975
+ if (opcode === 1 || opcode === 2) {
976
+ receivedText += payload.toString("utf8");
977
+ try {
978
+ const parsed = JSON.parse(receivedText);
979
+ if (parsed.pong === true) {
980
+ finish({
981
+ ok: true,
982
+ colo: typeof parsed.colo === "string" ? parsed.colo.toUpperCase() : "",
983
+ status: 101,
984
+ pongVerified: true,
985
+ probeColo: typeof parsed.colo === "string" ? parsed.colo.toUpperCase() : void 0
986
+ });
987
+ return;
988
+ }
989
+ } catch {
990
+ }
991
+ }
992
+ }
993
+ });
994
+ });
995
+ }
996
+ function probeTlsHandshake(ip, port, host, timeoutMs = 4e3) {
997
+ return new Promise((resolve) => {
998
+ const started = Date.now();
999
+ let settled = false;
1000
+ const finish = (result) => {
1001
+ if (settled) return;
1002
+ settled = true;
1003
+ clearTimeout(timer);
1004
+ try {
1005
+ socket.destroy();
1006
+ } catch {
1007
+ }
1008
+ resolve({ ip, tlsMs: result.tlsMs ?? Date.now() - started, ...result });
1009
+ };
1010
+ const socket = tls2.connect(
1011
+ {
1012
+ host: ip,
1013
+ port,
1014
+ servername: host,
1015
+ rejectUnauthorized: false,
1016
+ ALPNProtocols: ["http/1.1"]
1017
+ },
1018
+ () => {
1019
+ const alpn = socket.alpnProtocol;
1020
+ if (alpn && alpn !== "http/1.1") {
1021
+ finish({ error: `ALPN=${alpn}` });
1022
+ return;
1023
+ }
1024
+ finish({ tlsMs: Date.now() - started, alpn: alpn || void 0 });
1025
+ }
1026
+ );
1027
+ const timer = setTimeout(() => finish({ error: "TLS \u63E1\u624B\u8D85\u65F6" }), timeoutMs);
1028
+ socket.setTimeout(timeoutMs, () => finish({ error: "TLS \u8FDE\u63A5\u8D85\u65F6" }));
1029
+ socket.on("error", (err) => finish({ error: err.message }));
1030
+ });
1031
+ }
673
1032
  function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edge_probe") {
674
1033
  return new Promise((resolve) => {
675
1034
  const started = Date.now();
@@ -991,10 +1350,22 @@ async function pool(items, concurrency, worker) {
991
1350
  await Promise.all(runners);
992
1351
  return out;
993
1352
  }
1353
+ async function withRetry(attempts, fn, isOk = (r) => r.ok === true) {
1354
+ const total = Math.max(1, Math.floor(attempts));
1355
+ let last;
1356
+ for (let i = 0; i < total; i++) {
1357
+ last = await fn();
1358
+ if (isOk(last)) return last;
1359
+ }
1360
+ return last;
1361
+ }
994
1362
  async function runScan(options) {
995
1363
  const started = Date.now();
996
1364
  const concurrency = options.concurrency ?? 300;
997
1365
  const tcpTimeoutMs = options.tcpTimeoutMs ?? 1200;
1366
+ const retries = options.retries ?? 2;
1367
+ const tlsConcurrency = options.tlsConcurrency ?? 64;
1368
+ const tlsTimeoutMs = options.tlsTimeoutMs ?? 4e3;
998
1369
  const maxLatencyMs = options.maxLatencyMs ?? 0;
999
1370
  const probeLimit = options.probeLimit ?? 120;
1000
1371
  const probeConcurrency = options.probeConcurrency ?? 24;
@@ -1010,7 +1381,7 @@ async function runScan(options) {
1010
1381
  report({ type: "stage", stage: 1, name: "TCP \u63E1\u624B\u521D\u7B5B", total: candidates.length });
1011
1382
  let done = 0;
1012
1383
  const tcpResults = await pool(candidates, concurrency, async (ip) => {
1013
- const result = await tcpPing(ip, options.port, tcpTimeoutMs);
1384
+ const result = await withRetry(retries, () => tcpPing(ip, options.port, tcpTimeoutMs));
1014
1385
  done++;
1015
1386
  if (done % 500 === 0 || done === candidates.length) {
1016
1387
  report({
@@ -1025,22 +1396,57 @@ async function runScan(options) {
1025
1396
  });
1026
1397
  const tcpOk = tcpResults.filter((r) => r.ok);
1027
1398
  const filtered = maxLatencyMs > 0 ? tcpOk.filter((r) => r.latency <= maxLatencyMs) : tcpOk;
1028
- const stage1 = filtered.sort((a, b) => a.latency - b.latency).slice(0, probeLimit);
1029
- report({ type: "stage", stage: 2, name: "WebSocket 101 \u771F\u8FDE\u63A5\u6821\u9A8C", total: stage1.length });
1399
+ report({ type: "stage", stage: 1, name: "TLS \u63E1\u624B\u7B5B\u9009", total: filtered.length });
1400
+ let tlsDone = 0;
1401
+ const tlsResults = await pool(filtered, tlsConcurrency, async (item) => {
1402
+ const probe = await withRetry(
1403
+ retries,
1404
+ () => probeTlsHandshake(item.ip, options.port, options.host, tlsTimeoutMs),
1405
+ (r) => r.tlsMs > 0
1406
+ );
1407
+ tlsDone++;
1408
+ if (tlsDone % 200 === 0 || tlsDone === filtered.length) {
1409
+ report({
1410
+ type: "progress",
1411
+ stage: 1,
1412
+ done: tlsDone,
1413
+ total: filtered.length,
1414
+ message: `TLS \u63E1\u624B ${tlsDone}/${filtered.length}${probe.tlsMs > 0 ? ` ${probe.tlsMs}ms` : " \u2717"}`
1415
+ });
1416
+ }
1417
+ return { ...item, tls: probe };
1418
+ });
1419
+ const tlsOk = tlsResults.filter((r) => r.tls.tlsMs > 0);
1420
+ const stage1 = tlsOk.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, probeLimit);
1421
+ const strictProbe = options.verifyPayload !== false;
1422
+ const strictProbePath = "/__edge_probe";
1423
+ report({
1424
+ type: "stage",
1425
+ stage: 2,
1426
+ name: strictProbe ? "WS 101 + \u54CD\u5E94\u4F53\u6821\u9A8C\uFF08\u63A2\u9488\u7AEF\u70B9\uFF09" : "WebSocket 101 \u6821\u9A8C",
1427
+ total: stage1.length
1428
+ });
1030
1429
  let probeDone = 0;
1430
+ let verifiedCount = 0;
1031
1431
  const probeResults = await pool(stage1, probeConcurrency, async (item) => {
1032
- const result = await probeWs101(item.ip, options.port, options.host, probeTimeoutMs, options.wsPath ?? "/__edge_probe");
1432
+ const probeFn = strictProbe ? probeWsPing : probeWs101;
1433
+ const probePath = strictProbe ? strictProbePath : options.wsPath ?? "/__edge_probe";
1434
+ const result = await withRetry(
1435
+ retries,
1436
+ () => probeFn(item.ip, options.port, options.host, probeTimeoutMs, probePath)
1437
+ );
1033
1438
  probeDone++;
1439
+ if (result.pongVerified) verifiedCount++;
1034
1440
  report({
1035
1441
  type: "progress",
1036
1442
  stage: 2,
1037
1443
  done: probeDone,
1038
1444
  total: stage1.length,
1039
- message: `101 \u6821\u9A8C ${probeDone}/${stage1.length}${result.ok ? ` \u2713 ${result.colo}` : ""}`
1445
+ 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}` : ""}`
1040
1446
  });
1041
1447
  return { ...item, probe: result };
1042
1448
  });
1043
- const wsOk = probeResults.filter((r) => r.probe.ok);
1449
+ const wsOk = strictProbe ? probeResults.filter((r) => r.probe.ok && r.probe.pongVerified === true) : probeResults.filter((r) => r.probe.ok);
1044
1450
  const countryFilter = normalizeCountryFilter(options.countries ?? []);
1045
1451
  let wsFiltered = wsOk;
1046
1452
  let appliedCountries = [];
@@ -1066,7 +1472,7 @@ async function runScan(options) {
1066
1472
  });
1067
1473
  }
1068
1474
  }
1069
- const stage2 = wsFiltered.sort((a, b) => a.latency - b.latency).slice(0, speedLimit);
1475
+ const stage2 = wsFiltered.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, speedLimit);
1070
1476
  let speedResults;
1071
1477
  const speedMode = options.speedMode ?? "probe";
1072
1478
  if (options.skipSpeed) {
@@ -1126,8 +1532,11 @@ async function runScan(options) {
1126
1532
  port: options.port,
1127
1533
  colo: r.probe.colo || "UNK",
1128
1534
  latency: r.latency,
1535
+ tlsMs: r.tls.tlsMs,
1129
1536
  speed: r.speed ? Number(r.speed.toFixed(2)) : 0
1130
- })).sort((a, b) => options.skipSpeed ? a.latency - b.latency : b.speed - a.speed || a.latency - b.latency).slice(0, top);
1537
+ })).sort(
1538
+ (a, b) => options.skipSpeed ? a.tlsMs - b.tlsMs || a.latency - b.latency : b.speed - a.speed || a.tlsMs - b.tlsMs || a.latency - b.latency
1539
+ ).slice(0, top);
1131
1540
  for (const entry of entries) {
1132
1541
  report({ type: "result", entry });
1133
1542
  }
@@ -1136,7 +1545,9 @@ async function runScan(options) {
1136
1545
  stats: {
1137
1546
  candidates: candidates.length,
1138
1547
  tcpOk: tcpOk.length,
1548
+ tlsOk: tlsOk.length,
1139
1549
  wsOk: wsOk.length,
1550
+ verified: verifiedCount,
1140
1551
  speedOk: entries.length,
1141
1552
  elapsedMs: Date.now() - started,
1142
1553
  countries: appliedCountries,
@@ -1447,7 +1858,7 @@ async function handleRequest(req, res) {
1447
1858
  }
1448
1859
 
1449
1860
  // src/version.ts
1450
- var VERSION = "0.2.0";
1861
+ var VERSION = "0.4.1";
1451
1862
  var NAME = "cfpilot-scanner";
1452
1863
 
1453
1864
  // src/cli.ts
@@ -1481,15 +1892,25 @@ scan \u9009\u9879:
1481
1892
  --speed-conns <n> \u6D4B\u901F\u5E76\u53D1\u8FDE\u63A5\u6570\uFF0C\u9ED8\u8BA4 3
1482
1893
  \u5355\u6761 TCP \u8FDE\u63A5\u53D7\u62E5\u585E\u7A97\u53E3\u9650\u5236\uFF0C\u591A\u8FDE\u63A5\u66F4\u63A5\u8FD1\u771F\u5B9E\u5E26\u5BBD
1483
1894
  --speed-duration <ms> \u6D4B\u901F\u65F6\u957F\uFF0C\u9ED8\u8BA4 6000
1895
+ --retries <n> \u6BCF\u4E2A\u63A2\u6D4B\u7684\u5C1D\u8BD5\u6B21\u6570\uFF08\u542B\u9996\u6B21\uFF09\uFF0C\u9ED8\u8BA4 2
1896
+ \u771F\u673A\u5076\u53D1\u5931\u8D25\u7387 3~5%\uFF0C\u91CD\u8BD5\u53D6\u7B2C\u4E00\u6B21\u6210\u529F\u7684\u7ED3\u679C
1897
+ --no-verify \u9636\u6BB5\u4E8C\u53EA\u505A 101 \u63E1\u624B\uFF0C\u8DF3\u8FC7\u54CD\u5E94\u4F53\u6821\u9A8C
1898
+ \uFF08\u9ED8\u8BA4\u6821\u9A8C\uFF1A\u53D1 PING \u5E76\u9A8C\u8BC1\u6570\u636E\u9762\u56DE {"pong":true}\uFF09
1484
1899
  --no-speed \u8DF3\u8FC7\u6D4B\u901F\uFF0C\u53EA\u6309\u5EF6\u8FDF\u6392\u5E8F
1485
1900
  --top <n> \u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4 15
1486
- --concurrency <n> \u9636\u6BB5\u4E00\u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
1901
+ --concurrency <n> \u9636\u6BB5\u4E00 TCP \u5E76\u53D1\uFF0C\u9ED8\u8BA4 300
1902
+ --tls-concurrency <n> \u9636\u6BB5\u4E00 TLS \u63E1\u624B\u5E76\u53D1\uFF0C\u9ED8\u8BA4 64
1903
+ TLS \u63E1\u624B\u662F CPU \u5BC6\u96C6\u578B\u64CD\u4F5C\uFF0C\u8FC7\u9AD8\u4F1A\u8BA9\u672C\u673A\u6210\u4E3A\u74F6\u9888
1487
1904
  --sample <n> \u5019\u9009 IP \u91C7\u6837\u6570\uFF0C\u9ED8\u8BA4 20000
1905
+ \u5EFA\u8BAE 6556 \u8D77\uFF1A\u9636\u6BB5\u4E00\u6309 TLS \u63E1\u624B\u8017\u65F6\u7B5B\u9009\uFF0C\u800C\u63E1\u624B\u5FEB\u6162
1906
+ \u4E0E TCP \u5EF6\u8FDF\u51E0\u4E4E\u65E0\u5173\uFF08\u5B9E\u6D4B TCP 9ms \u7684 IP \u53EF\u80FD\u8981 3.5 \u79D2\uFF09\uFF0C
1907
+ \u91C7\u6837\u5C0F\u4E86\u6839\u672C\u78B0\u4E0D\u5230\u63E1\u624B\u5FEB\u7684 IP
1488
1908
  --countries <a,b> \u56FD\u5BB6 / \u5730\u533A\u8FC7\u6EE4\uFF08\u53EF\u591A\u9009\uFF0C\u9017\u53F7\u5206\u9694\uFF0C\u5982 HK,JP,SG\uFF09
1489
1909
  \u4E0D\u9009 = \u5168\u91CF\u3002\u673A\u623F\u6765\u81EA 101 \u63E1\u624B\u54CD\u5E94\uFF1B\u7B5B\u5B8C\u4E3A\u7A7A\u4F1A\u81EA\u52A8\u5FFD\u7565
1490
1910
  --cidr <a,b> \u81EA\u5B9A\u4E49\u7F51\u6BB5\uFF08\u53EF\u91CD\u590D\uFF0C\u7528\u9017\u53F7\u5206\u9694\uFF09
1491
1911
  --ip-file <path> \u4F7F\u7528\u672C\u5730 IP \u5217\u8868\u6587\u4EF6
1492
- --max-latency <ms> \u9636\u6BB5\u4E00\u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
1912
+ --max-latency <ms> \u9636\u6BB5\u4E00 TCP \u5EF6\u8FDF\u4E0A\u9650\uFF0C\u9ED8\u8BA4 0\uFF08\u4E0D\u9650\uFF09
1913
+ --tls-timeout <ms> \u9636\u6BB5\u4E00 TLS \u63E1\u624B\u8D85\u65F6\uFF0C\u9ED8\u8BA4 4000
1493
1914
  --probe-limit <n> \u8FDB\u5165 101 \u6821\u9A8C\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 120
1494
1915
  --speed-limit <n> \u8FDB\u5165\u6D4B\u901F\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 30
1495
1916
  --speed-bytes <n> \u6D4B\u901F\u5B57\u8282\u6570\uFF0C\u9ED8\u8BA4 5242880
@@ -1630,15 +2051,16 @@ function printTable(entries) {
1630
2051
  process.stdout.write("\n\u6CA1\u6709\u901A\u8FC7\u5168\u90E8\u6821\u9A8C\u7684\u8282\u70B9\u3002\n");
1631
2052
  return;
1632
2053
  }
1633
- process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F \u5EF6\u8FDF(ms) \u4E0B\u884C(MB/s)\n");
1634
- process.stdout.write("\u2500".repeat(58) + "\n");
2054
+ process.stdout.write("\n\u6392\u540D \u5730\u5740 \u673A\u623F TLS(ms) TCP(ms) \u4E0B\u884C(MB/s)\n");
2055
+ process.stdout.write("\u2500".repeat(66) + "\n");
1635
2056
  entries.forEach((entry, index) => {
1636
2057
  const rank = String(index + 1).padStart(2, " ");
1637
2058
  const address = `${entry.ip}:${entry.port}`.padEnd(24, " ");
1638
2059
  const colo = (entry.colo || "UNK").padEnd(6, " ");
2060
+ const tls3 = String(entry.tlsMs).padStart(7, " ");
1639
2061
  const latency = String(entry.latency).padStart(7, " ");
1640
2062
  const speed = entry.speed.toFixed(1).padStart(9, " ");
1641
- process.stdout.write(`${rank} ${address} ${colo} ${latency} ${speed}
2063
+ process.stdout.write(`${rank} ${address} ${colo} ${tls3} ${latency} ${speed}
1642
2064
  `);
1643
2065
  });
1644
2066
  }
@@ -1663,6 +2085,7 @@ async function commandScan(args) {
1663
2085
  const wsPath = normalizeWsPath(flagString(args, "path"), "/__edge_probe");
1664
2086
  const probePath = normalizeWsPath(flagString(args, "probe-path"), "/__edge_probe");
1665
2087
  const noSpeed = flagBool(args, "no-speed");
2088
+ const verifyPayload = !flagBool(args, "no-verify");
1666
2089
  const countries = normalizeCountryFilter(flagList(args, "countries"));
1667
2090
  const callback = noCallback ? "" : flagString(args, "callback", token ? `https://${host}/api/v1/preferred-ips` : "");
1668
2091
  if (!quiet) {
@@ -1752,8 +2175,12 @@ ${NAME} v${VERSION}
1752
2175
  wsPath,
1753
2176
  probePath,
1754
2177
  skipSpeed,
2178
+ verifyPayload,
1755
2179
  concurrency: flagNumber(args, "concurrency", 300),
2180
+ retries: flagNumber(args, "retries", 2),
1756
2181
  tcpTimeoutMs: flagNumber(args, "timeout", 1200),
2182
+ tlsConcurrency: flagNumber(args, "tls-concurrency", 64),
2183
+ tlsTimeoutMs: flagNumber(args, "tls-timeout", 4e3),
1757
2184
  maxLatencyMs: flagNumber(args, "max-latency", 0),
1758
2185
  probeLimit: flagNumber(args, "probe-limit", 120),
1759
2186
  probeTimeoutMs: flagNumber(args, "probe-timeout", 3e3),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cfpilot-scanner",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "CFPilot 优选器:用你自己的域名做 WebSocket 101 真连接校验,只保留真正可用的 Cloudflare IP;兼容任意 VLESS-WS 项目",
5
5
  "license": "MIT",
6
6
  "author": "CFPilot Contributors",
@@ -31,7 +31,7 @@
31
31
  "scripts": {
32
32
  "build": "node scripts/build.mjs",
33
33
  "typecheck": "tsc --noEmit -p tsconfig.json",
34
- "prepublishOnly": "npm run build"
34
+ "prepublishOnly": "node ../../scripts/sync-version.mjs packages/scanner-cli && npm run build"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26.6.5",