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.
- package/dist/cli.js +394 -24
- 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;
|
|
@@ -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 =
|
|
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
|
-
|
|
1029
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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
|
|
1634
|
-
process.stdout.write("\u2500".repeat(
|
|
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
|
+
"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",
|