cfpilot-scanner 0.3.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 (2) hide show
  1. package/dist/cli.js +394 -24
  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;
@@ -303,7 +409,7 @@ function readIpFile(text) {
303
409
  // src/candidates.ts
304
410
  var DEFAULT_CANDIDATE_SAMPLE = 2e4;
305
411
  var DEFAULT_CANDIDATE_SEED = 24301;
306
- var DEFAULT_BRIDGE_SAMPLE = 300;
412
+ var DEFAULT_BRIDGE_SAMPLE = 6556;
307
413
  function normalizeCidrInput(cidr) {
308
414
  if (cidr === void 0) return [];
309
415
  const list = Array.isArray(cidr) ? cidr : [cidr];
@@ -670,6 +776,216 @@ function probeWs101(ip, port, host, timeoutMs = 3e3, path = "/__edge_probe") {
670
776
  });
671
777
  });
672
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
+ }
673
989
  function measureDownload(ip, port, host, bytes, timeoutMs = 12e3, path = "/__edge_probe") {
674
990
  return new Promise((resolve) => {
675
991
  const started = Date.now();
@@ -995,6 +1311,8 @@ async function runScan(options) {
995
1311
  const started = Date.now();
996
1312
  const concurrency = options.concurrency ?? 300;
997
1313
  const tcpTimeoutMs = options.tcpTimeoutMs ?? 1200;
1314
+ const tlsConcurrency = options.tlsConcurrency ?? 64;
1315
+ const tlsTimeoutMs = options.tlsTimeoutMs ?? 4e3;
998
1316
  const maxLatencyMs = options.maxLatencyMs ?? 0;
999
1317
  const probeLimit = options.probeLimit ?? 120;
1000
1318
  const probeConcurrency = options.probeConcurrency ?? 24;
@@ -1025,22 +1343,56 @@ async function runScan(options) {
1025
1343
  });
1026
1344
  const tcpOk = tcpResults.filter((r) => r.ok);
1027
1345
  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 });
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
+ });
1030
1372
  let probeDone = 0;
1373
+ let verifiedCount = 0;
1031
1374
  const probeResults = await pool(stage1, probeConcurrency, async (item) => {
1032
- 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
+ );
1033
1384
  probeDone++;
1385
+ if (result.pongVerified) verifiedCount++;
1034
1386
  report({
1035
1387
  type: "progress",
1036
1388
  stage: 2,
1037
1389
  done: probeDone,
1038
1390
  total: stage1.length,
1039
- 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}` : ""}`
1040
1392
  });
1041
1393
  return { ...item, probe: result };
1042
1394
  });
1043
- const wsOk = probeResults.filter((r) => r.probe.ok);
1395
+ const wsOk = strictProbe ? probeResults.filter((r) => r.probe.ok && r.probe.pongVerified === true) : probeResults.filter((r) => r.probe.ok);
1044
1396
  const countryFilter = normalizeCountryFilter(options.countries ?? []);
1045
1397
  let wsFiltered = wsOk;
1046
1398
  let appliedCountries = [];
@@ -1066,7 +1418,7 @@ async function runScan(options) {
1066
1418
  });
1067
1419
  }
1068
1420
  }
1069
- const stage2 = wsFiltered.sort((a, b) => a.latency - b.latency).slice(0, speedLimit);
1421
+ const stage2 = wsFiltered.sort((a, b) => a.tls.tlsMs - b.tls.tlsMs || a.latency - b.latency).slice(0, speedLimit);
1070
1422
  let speedResults;
1071
1423
  const speedMode = options.speedMode ?? "probe";
1072
1424
  if (options.skipSpeed) {
@@ -1126,8 +1478,11 @@ async function runScan(options) {
1126
1478
  port: options.port,
1127
1479
  colo: r.probe.colo || "UNK",
1128
1480
  latency: r.latency,
1481
+ tlsMs: r.tls.tlsMs,
1129
1482
  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);
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);
1131
1486
  for (const entry of entries) {
1132
1487
  report({ type: "result", entry });
1133
1488
  }
@@ -1136,7 +1491,9 @@ async function runScan(options) {
1136
1491
  stats: {
1137
1492
  candidates: candidates.length,
1138
1493
  tcpOk: tcpOk.length,
1494
+ tlsOk: tlsOk.length,
1139
1495
  wsOk: wsOk.length,
1496
+ verified: verifiedCount,
1140
1497
  speedOk: entries.length,
1141
1498
  elapsedMs: Date.now() - started,
1142
1499
  countries: appliedCountries,
@@ -1447,7 +1804,7 @@ async function handleRequest(req, res) {
1447
1804
  }
1448
1805
 
1449
1806
  // src/version.ts
1450
- var VERSION = "0.2.0";
1807
+ var VERSION = "0.4.0";
1451
1808
  var NAME = "cfpilot-scanner";
1452
1809
 
1453
1810
  // src/cli.ts
@@ -1481,15 +1838,23 @@ scan \u9009\u9879:
1481
1838
  --speed-conns <n> \u6D4B\u901F\u5E76\u53D1\u8FDE\u63A5\u6570\uFF0C\u9ED8\u8BA4 3
1482
1839
  \u5355\u6761 TCP \u8FDE\u63A5\u53D7\u62E5\u585E\u7A97\u53E3\u9650\u5236\uFF0C\u591A\u8FDE\u63A5\u66F4\u63A5\u8FD1\u771F\u5B9E\u5E26\u5BBD
1483
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
1484
1843
  --no-speed \u8DF3\u8FC7\u6D4B\u901F\uFF0C\u53EA\u6309\u5EF6\u8FDF\u6392\u5E8F
1485
1844
  --top <n> \u8FD4\u56DE\u6761\u6570\uFF0C\u9ED8\u8BA4 15
1486
- --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
1487
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
1488
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
1489
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
1490
1854
  --cidr <a,b> \u81EA\u5B9A\u4E49\u7F51\u6BB5\uFF08\u53EF\u91CD\u590D\uFF0C\u7528\u9017\u53F7\u5206\u9694\uFF09
1491
1855
  --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
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
1493
1858
  --probe-limit <n> \u8FDB\u5165 101 \u6821\u9A8C\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 120
1494
1859
  --speed-limit <n> \u8FDB\u5165\u6D4B\u901F\u7684\u5019\u9009\u6570\uFF0C\u9ED8\u8BA4 30
1495
1860
  --speed-bytes <n> \u6D4B\u901F\u5B57\u8282\u6570\uFF0C\u9ED8\u8BA4 5242880
@@ -1630,15 +1995,16 @@ function printTable(entries) {
1630
1995
  process.stdout.write("\n\u6CA1\u6709\u901A\u8FC7\u5168\u90E8\u6821\u9A8C\u7684\u8282\u70B9\u3002\n");
1631
1996
  return;
1632
1997
  }
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");
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");
1635
2000
  entries.forEach((entry, index) => {
1636
2001
  const rank = String(index + 1).padStart(2, " ");
1637
2002
  const address = `${entry.ip}:${entry.port}`.padEnd(24, " ");
1638
2003
  const colo = (entry.colo || "UNK").padEnd(6, " ");
2004
+ const tls3 = String(entry.tlsMs).padStart(7, " ");
1639
2005
  const latency = String(entry.latency).padStart(7, " ");
1640
2006
  const speed = entry.speed.toFixed(1).padStart(9, " ");
1641
- process.stdout.write(`${rank} ${address} ${colo} ${latency} ${speed}
2007
+ process.stdout.write(`${rank} ${address} ${colo} ${tls3} ${latency} ${speed}
1642
2008
  `);
1643
2009
  });
1644
2010
  }
@@ -1663,6 +2029,7 @@ async function commandScan(args) {
1663
2029
  const wsPath = normalizeWsPath(flagString(args, "path"), "/__edge_probe");
1664
2030
  const probePath = normalizeWsPath(flagString(args, "probe-path"), "/__edge_probe");
1665
2031
  const noSpeed = flagBool(args, "no-speed");
2032
+ const verifyPayload = !flagBool(args, "no-verify");
1666
2033
  const countries = normalizeCountryFilter(flagList(args, "countries"));
1667
2034
  const callback = noCallback ? "" : flagString(args, "callback", token ? `https://${host}/api/v1/preferred-ips` : "");
1668
2035
  if (!quiet) {
@@ -1752,8 +2119,11 @@ ${NAME} v${VERSION}
1752
2119
  wsPath,
1753
2120
  probePath,
1754
2121
  skipSpeed,
2122
+ verifyPayload,
1755
2123
  concurrency: flagNumber(args, "concurrency", 300),
1756
2124
  tcpTimeoutMs: flagNumber(args, "timeout", 1200),
2125
+ tlsConcurrency: flagNumber(args, "tls-concurrency", 64),
2126
+ tlsTimeoutMs: flagNumber(args, "tls-timeout", 4e3),
1757
2127
  maxLatencyMs: flagNumber(args, "max-latency", 0),
1758
2128
  probeLimit: flagNumber(args, "probe-limit", 120),
1759
2129
  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.0",
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",