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.
- package/dist/cli.js +514 -87
- 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
|
|
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
|
|
242
|
-
const wanted = Math.max(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1029
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
1634
|
-
process.stdout.write("\u2500".repeat(
|
|
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
|
+
"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",
|