const DEFAULT_BEIAN_CONTENT = `© 2025 - 2026 Check ProxyIP · 基于 Cloudflare Workers 构建与运行 · 今日访问人数:··· · 站点维护:CMLiussss
`;
const RESOLVE_BATCH_LIMIT = 15;
export default {
async fetch(request, env) {
const 备案内容 = env.BEIAN ?? DEFAULT_BEIAN_CONTENT;
const url = new URL(request.url);
if (url.pathname === '/check') {
return handleCheckProxyRequest(request);
} else if (url.pathname === '/resolve') {
const proxyip = url.searchParams.get('proxyip');
if (!proxyip) {
return new Response('Missing proxyip', { status: 400 });
}
try {
const targets = await handleResolve(proxyip);
return new Response(JSON.stringify(targets), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
} else if (url.pathname === '/resolve-batch') {
return handleResolveBatchRequest(request);
} else if (url.pathname === '/locations') return fetch(new Request('https://speed.cloudflare.com/locations', { headers: { 'Referer': 'https://speed.cloudflare.com/' } }));
return new Response(generateHTML(备案内容), {
headers: { 'Content-Type': 'text/html; charset=UTF-8' }
});
}
};
async function handleResolveBatchRequest(request) {
const headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
};
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers
});
}
if (request.method !== 'POST') {
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
status: 405,
headers: {
...headers,
'Allow': 'POST, OPTIONS'
}
});
}
let payload;
try {
payload = await request.json();
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid JSON body' }), {
status: 400,
headers
});
}
const inputs = getResolveBatchInputs(payload);
if (!inputs.length) {
return new Response(JSON.stringify({ error: 'Missing targets' }), {
status: 400,
headers
});
}
if (inputs.length > RESOLVE_BATCH_LIMIT) {
return new Response(JSON.stringify({ error: `Resolve batch limit is ${RESOLVE_BATCH_LIMIT}` }), {
status: 400,
headers
});
}
const results = await Promise.all(inputs.map(async input => {
try {
return {
input,
targets: await handleResolve(input)
};
} catch (error) {
return {
input,
targets: [],
error: error.message
};
}
}));
return new Response(JSON.stringify({ results }), {
headers
});
}
function getResolveBatchInputs(payload) {
const values = Array.isArray(payload?.targets)
? payload.targets
: (Array.isArray(payload?.proxyips) ? payload.proxyips : []);
return uniqueStrings(values
.map(value => String(value || '').trim())
.filter(Boolean));
}
function uniqueStrings(values) {
const seenValues = new Set();
return values.filter(value => {
if (seenValues.has(value)) return false;
seenValues.add(value);
return true;
});
}
async function handleResolve(input) {
let { host, port } = parseTarget(input);
const tpPortMatch = host.toLowerCase().match(/\.tp(\d{1,5})\./);
if (tpPortMatch) {
const tpPort = Number(tpPortMatch[1]);
if (tpPort >= 1 && tpPort <= 65535) {
port = tpPort;
}
}
const bracketedIPv6 = host.startsWith('[') && host.endsWith(']');
const rawIPv6 = /^[0-9a-fA-F:]+$/.test(host);
if (isIPv4(host) || bracketedIPv6 || rawIPv6) {
const finalHost = rawIPv6 && !bracketedIPv6 ? `[${host}]` : host;
return [`${finalHost}:${port}`];
}
const [txtRecords, aRecords, aaaaRecords] = await Promise.all([
dohQuery(host, 'TXT'),
dohQuery(host, 'A'),
dohQuery(host, 'AAAA')
]);
const results = [];
for (const record of txtRecords.filter(item => item.type === 16 && item.data)) {
const value = normalizeTxtValue(record.data);
for (const part of value.split(',')) {
const candidate = part.trim();
if (candidate) results.push(candidate);
}
}
for (const record of aRecords.filter(item => item.type === 1 && item.data)) {
results.push(`${record.data}:${port}`);
}
for (const record of aaaaRecords.filter(item => item.type === 28 && item.data)) {
results.push(`[${record.data}]:${port}`);
}
if (!results.length) {
throw new Error('Could not resolve domain');
}
return uniqueStrings(results);
}
function parseTarget(input) {
let host = String(input || '').split('#')[0].trim();
let port = 443;
if (host.startsWith('[')) {
const ipv6PortIndex = host.lastIndexOf(']:');
if (ipv6PortIndex !== -1) {
const maybePort = Number(host.slice(ipv6PortIndex + 2));
if (Number.isInteger(maybePort) && maybePort >= 1 && maybePort <= 65535) {
port = maybePort;
host = host.slice(0, ipv6PortIndex + 1);
}
}
return { host, port };
}
const colonMatches = host.match(/:/g) || [];
if (colonMatches.length === 1) {
const separatorIndex = host.lastIndexOf(':');
const maybePort = Number(host.slice(separatorIndex + 1));
if (Number.isInteger(maybePort) && maybePort >= 1 && maybePort <= 65535) {
port = maybePort;
host = host.slice(0, separatorIndex);
}
}
return { host, port };
}
function isIPv4(value) {
const parts = value.split('.');
return parts.length === 4 && parts.every(part => {
if (!/^\d{1,3}$/.test(part)) return false;
const num = Number(part);
return num >= 0 && num <= 255;
});
}
function normalizeTxtValue(value) {
const text = String(value ?? '').trim();
if (text.startsWith('"') && text.endsWith('"')) {
return text.slice(1, -1).replace(/\\"/g, '"');
}
return text.replace(/\\"/g, '"');
}
async function dohQuery(name, type, endpoint = 'https://cloudflare-dns.com/dns-query') {
const startedAt = performance.now();
try {
const response = await fetch(
`${endpoint}?name=${encodeURIComponent(name)}&type=${encodeURIComponent(type)}`,
{
headers: {
accept: 'application/dns-json'
}
}
);
if (!response.ok) {
console.warn(`[DoH] ${name} ${type} failed with status ${response.status}`);
return [];
}
const payload = await response.json();
if (!Array.isArray(payload.Answer)) {
console.log(`[DoH] ${name} ${type} returned 0 answers in ${(performance.now() - startedAt).toFixed(2)}ms`);
return [];
}
const answers = payload.Answer.map(answer => ({
name: answer.name || name,
type: answer.type,
TTL: answer.TTL,
data: answer.type === 16 ? normalizeTxtValue(answer.data) : answer.data
}));
console.log(`[DoH] ${name} ${type} returned ${answers.length} answers in ${(performance.now() - startedAt).toFixed(2)}ms`);
return answers;
} catch (error) {
console.error(`[DoH] ${name} ${type} error after ${(performance.now() - startedAt).toFixed(2)}ms`, error);
return [];
}
}
function generateHTML(备案内容) {
return `
Check ProxyIP
等待开始检测
输入目标后,检测结果、出口信息和地图会在这里展示。
当前筛选没有匹配的检测结果。
概念
ProxyIP 是一个可被验证的中转入口
在 Cloudflare Workers 的使用语境里,ProxyIP 通常指那些能够成功代理到 Cloudflare 服务的第三方 IP。它不是 Cloudflare 官方分配给你的接入地址,而是一个可以替你完成转发的外部节点。
限制来源
为什么很多场景会专门去找 ProxyIP
Cloudflare Workers 的 TCP sockets 文档 明确提到,指向 Cloudflare IP ranges 的出站 TCP 连接会被阻止。也就是说,某些依赖直连 Cloudflare IP 的链路,不能在 Workers 里直接打通。
Outbound TCP sockets to Cloudflare IP ranges are blocked.
Cloudflare Workers
发起请求,尝试访问目标服务
→
ProxyIP 节点
位于第三方网络,负责中转和反向代理
→
Cloudflare 服务
最终被访问的站点、边缘服务或 CDN 目标
实际作用可以理解为:让 Workers 先连到第三方节点,再由该节点替你把流量送到 Cloudflare 侧,绕开直连 Cloudflare IP 的限制。
应用场景
为什么像 edgetunnel、epeius 这类项目会用到它
当目标网站本身走的是 Cloudflare CDN 或 Cloudflare 边缘网络时,项目如果需要直接建立到目标地址的 TCP 连接,就可能因为上述限制而失败。配置可用的 ProxyIP 后,这类项目就能借助中转节点继续完成访问。
有效特征
有效的 ProxyIP,通常至少满足这些条件
- 能够成功建立代理到指定端口(通常为 443)的 TCP 连接
- 具备反向代理 Cloudflare IP 段的 HTTPS 服务能力
这页检测的意义:本工具不是只做静态解析,而是尽量模拟真实链路去验证目标是否真的可用,帮助你更快筛掉“看起来在线、实际不可做代理”的候选 IP。
`;
}
// ==================== local /check implementation ====================
/**
* merged worker
* TLS + target worker
*/
const e = 769, t = 771, n = 772, r = 20, i = 21, s = 22, a = 23, h = 1, c = 2, o = 4, l = 8, f = 11, u = 12, y = 13, p = 14, w = 15, d = 16, g = 20, k = 24, v = 0, A = 10, S = 11, m = 13, b = 16, C = 43, H = 45, T = 51, E = 0, L = new TextEncoder, K = new TextDecoder, P = new Uint8Array(0), U = new Map(Object.entries({ TLS_AES_128_GCM_SHA256: { id: 4865, keyLen: 16, ivLen: 12, hash: "SHA-256", tls13: !0 }, TLS_AES_256_GCM_SHA384: { id: 4866, keyLen: 32, ivLen: 12, hash: "SHA-384", tls13: !0 }, TLS_CHACHA20_POLY1305_SHA256: { id: 4867, keyLen: 32, ivLen: 12, hash: "SHA-256", tls13: !0, chacha: !0 }, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: { id: 49199, keyLen: 16, ivLen: 4, hash: "SHA-256", kex: "ECDHE" }, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: { id: 49200, keyLen: 32, ivLen: 4, hash: "SHA-384", kex: "ECDHE" }, TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: { id: 52392, keyLen: 32, ivLen: 12, hash: "SHA-256", kex: "ECDHE", chacha: !0 }, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: { id: 49195, keyLen: 16, ivLen: 4, hash: "SHA-256", kex: "ECDHE" }, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: { id: 49196, keyLen: 32, ivLen: 4, hash: "SHA-384", kex: "ECDHE" }, TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: { id: 52393, keyLen: 32, ivLen: 12, hash: "SHA-256", kex: "ECDHE", chacha: !0 } }).map((([, e]) => [e.id, e]))), I = new Map([[29, "X25519"], [23, "P-256"]]), x = [2052, 2053, 2054, 1025, 1281, 1537, 1027, 1283, 1539], _ = (...e) => { const t = e => { const n = []; for (const r of e) r instanceof Uint8Array ? n.push(...r) : Array.isArray(r) ? n.push(...t(r)) : "number" == typeof r && n.push(r); return n }; return new Uint8Array(t(e)) }, B = e => [e >> 8 & 255, 255 & e], R = (e, t) => e[t] << 8 | e[t + 1], M = (e, t) => e[t] << 16 | e[t + 1] << 8 | e[t + 2], W = (...e) => { const t = e.filter((e => e && e.length > 0)), n = t.reduce(((e, t) => e + t.length), 0), r = new Uint8Array(n); let i = 0; for (const e of t) r.set(e, i), i += e.length; return r }, D = e => crypto.getRandomValues(new Uint8Array(e)), N = (e, t) => { if (!e || !t || e.length !== t.length) return !1; let n = 0; for (let r = 0; r < e.length; r++)n |= e[r] ^ t[r]; return 0 === n }, q = e => "SHA-512" === e ? 64 : "SHA-384" === e ? 48 : 32; async function $(e, t, n) { const r = await crypto.subtle.importKey("raw", t, { name: "HMAC", hash: e }, !1, ["sign"]); return new Uint8Array(await crypto.subtle.sign("HMAC", r, n)) } async function G(e, t) { return new Uint8Array(await crypto.subtle.digest(e, t)) } async function V(e, t, n, r, i = "SHA-256") { const s = W(L.encode(t), n); let a = new Uint8Array(0), h = s; for (; a.length < r;) { h = await $(i, e, h); const t = await $(i, e, W(h, s)); a = W(a, t) } return a.slice(0, r) } async function X(e, t, n) { return t && t.length || (t = new Uint8Array(q(e))), $(e, t, n) } async function O(e, t, n, r, i) { const s = L.encode("tls13 " + n); return async function (e, t, n, r) { const i = q(e), s = Math.ceil(r / i); let a = new Uint8Array(0), h = new Uint8Array(0); for (let r = 1; r <= s; r++)h = await $(e, t, W(h, n, [r])), a = W(a, h); return a.slice(0, r) }(e, t, _(B(i), s.length, s, r.length, r), i) } async function F(e = "P-256") { if ("X25519" === e) { const e = await crypto.subtle.generateKey({ name: "X25519" }, !0, ["deriveBits"]); return { keyPair: e, publicKeyRaw: new Uint8Array(await crypto.subtle.exportKey("raw", e.publicKey)) } } const t = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: e }, !0, ["deriveBits"]); return { keyPair: t, publicKeyRaw: new Uint8Array(await crypto.subtle.exportKey("raw", t.publicKey)) } } async function Y(e, t, n = "P-256") { if ("X25519" === n) { const n = await crypto.subtle.importKey("raw", t, { name: "X25519" }, !1, []); return new Uint8Array(await crypto.subtle.deriveBits({ name: "X25519", public: n }, e, 256)) } const r = await crypto.subtle.importKey("raw", t, { name: "ECDH", namedCurve: n }, !1, []), i = "P-384" === n ? 384 : "P-521" === n ? 528 : 256; return new Uint8Array(await crypto.subtle.deriveBits({ name: "ECDH", public: r }, e, i)) } async function j(e, t, n, r) { const i = await crypto.subtle.importKey("raw", e, { name: "AES-GCM" }, !1, ["encrypt"]); return new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: t, additionalData: r, tagLength: 128 }, i, n)) } async function z(e, t, n, r) { const i = await crypto.subtle.importKey("raw", e, { name: "AES-GCM" }, !1, ["decrypt"]); return new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: t, additionalData: r, tagLength: 128 }, i, n)) } function J(e, t) { return (e << t | e >>> 32 - t) >>> 0 } function Q(e, t, n, r, i) { e[t] = e[t] + e[n] >>> 0, e[i] = J(e[i] ^ e[t], 16), e[r] = e[r] + e[i] >>> 0, e[n] = J(e[n] ^ e[r], 12), e[t] = e[t] + e[n] >>> 0, e[i] = J(e[i] ^ e[t], 8), e[r] = e[r] + e[i] >>> 0, e[n] = J(e[n] ^ e[r], 7) } function Z(e, t, n) { const r = new Uint32Array(16); r[0] = 1634760805, r[1] = 857760878, r[2] = 2036477234, r[3] = 1797285236; const i = new DataView(e.buffer, e.byteOffset, e.byteLength); for (let e = 0; e < 8; e++)r[4 + e] = i.getUint32(4 * e, !0); r[12] = t; const s = new DataView(n.buffer, n.byteOffset, n.byteLength); r[13] = s.getUint32(0, !0), r[14] = s.getUint32(4, !0), r[15] = s.getUint32(8, !0); const a = new Uint32Array(r); for (let e = 0; e < 10; e++)Q(a, 0, 4, 8, 12), Q(a, 1, 5, 9, 13), Q(a, 2, 6, 10, 14), Q(a, 3, 7, 11, 15), Q(a, 0, 5, 10, 15), Q(a, 1, 6, 11, 12), Q(a, 2, 7, 8, 13), Q(a, 3, 4, 9, 14); for (let e = 0; e < 16; e++)a[e] = a[e] + r[e] >>> 0; return new Uint8Array(a.buffer.slice(0)) } function ee(e, t, n) { const r = new Uint8Array(n.length); let i = 1; for (let s = 0; s < n.length; s += 64) { const a = Z(e, i++, t), h = Math.min(64, n.length - s); for (let e = 0; e < h; e++)r[s + e] = n[s + e] ^ a[e] } return r } function te(e, t) { const n = function (e) { const t = new Uint8Array(e); return t[3] &= 15, t[7] &= 15, t[11] &= 15, t[15] &= 15, t[4] &= 252, t[8] &= 252, t[12] &= 252, t }(e.slice(0, 16)), r = e.slice(16, 32); let i = [0n, 0n, 0n, 0n, 0n]; const s = [0x3ffffffn & BigInt(n[0] | n[1] << 8 | n[2] << 16 | n[3] << 24), 0x3ffffffn & BigInt(n[3] >> 2 | n[4] << 6 | n[5] << 14 | n[6] << 22), 0x3ffffffn & BigInt(n[6] >> 4 | n[7] << 4 | n[8] << 12 | n[9] << 20), 0x3ffffffn & BigInt(n[9] >> 6 | n[10] << 2 | n[11] << 10 | n[12] << 18), 0x3ffffffn & BigInt(n[13] | n[14] << 8 | n[15] << 16)]; for (let e = 0; e < t.length; e += 16) { const n = t.slice(e, e + 16), r = new Uint8Array(17); r.set(n), r[n.length] = 1, i[0] += BigInt(r[0] | r[1] << 8 | r[2] << 16 | (3 & r[3]) << 24), i[1] += BigInt(r[3] >> 2 | r[4] << 6 | r[5] << 14 | (15 & r[6]) << 22), i[2] += BigInt(r[6] >> 4 | r[7] << 4 | r[8] << 12 | (63 & r[9]) << 20), i[3] += BigInt(r[9] >> 6 | r[10] << 2 | r[11] << 10 | r[12] << 18), i[4] += BigInt(r[13] | r[14] << 8 | r[15] << 16 | r[16] << 24); const a = [0n, 0n, 0n, 0n, 0n]; for (let e = 0; e < 5; e++)for (let t = 0; t < 5; t++) { const n = e + t; n < 5 ? a[n] += i[e] * s[t] : a[n - 5] += i[e] * s[t] * 5n } let h = 0n; for (let e = 0; e < 5; e++)a[e] += h, i[e] = 0x3ffffffn & a[e], h = a[e] >> 26n; i[0] += 5n * h, h = i[0] >> 26n, i[0] &= 0x3ffffffn, i[1] += h } let a = i[0] | i[1] << 26n | i[2] << 52n | i[3] << 78n | i[4] << 104n; a = a + r.reduce(((e, t, n) => e + (BigInt(t) << BigInt(8 * n))), 0n) & (1n << 128n) - 1n; const h = new Uint8Array(16); for (let e = 0; e < 16; e++)h[e] = Number(a >> BigInt(8 * e) & 0xffn); return h } function ne(e, t, n, r) { const i = Z(e, 0, t).slice(0, 32), s = ee(e, t, n), a = (16 - r.length % 16) % 16, h = (16 - s.length % 16) % 16, c = new Uint8Array(r.length + a + s.length + h + 16); c.set(r, 0), c.set(s, r.length + a); const o = new DataView(c.buffer, r.length + a + s.length + h); o.setBigUint64(0, BigInt(r.length), !0), o.setBigUint64(8, BigInt(s.length), !0); const l = te(i, c); return W(s, l) } function re(e, t, n, r) { if (n.length < 16) throw new Error("Ciphertext too short"); const i = n.slice(-16), s = n.slice(0, -16), a = Z(e, 0, t).slice(0, 32), h = (16 - r.length % 16) % 16, c = (16 - s.length % 16) % 16, o = new Uint8Array(r.length + h + s.length + c + 16); o.set(r, 0), o.set(s, r.length + h); const l = new DataView(o.buffer, r.length + h + s.length + c); l.setBigUint64(0, BigInt(r.length), !0), l.setBigUint64(8, BigInt(s.length), !0); const f = te(a, o); let u = 0; for (let e = 0; e < 16; e++)u |= i[e] ^ f[e]; if (0 !== u) throw new Error("ChaCha20-Poly1305 authentication failed"); return ee(e, t, s) } function ie(e, n, r = t) { return _(e, B(r), B(n.length), n) } function se(e, t) { return _(e, (e => [e >> 16 & 255, e >> 8 & 255, 255 & e])(t.length), t) } class ae { constructor() { this.buffer = new Uint8Array(0) } feed(e) { this.buffer = W(this.buffer, e) } next() { if (this.buffer.length < 5) return null; const e = this.buffer[0], t = R(this.buffer, 1), n = R(this.buffer, 3); if (this.buffer.length < 5 + n) return null; const r = this.buffer.slice(5, 5 + n); return this.buffer = this.buffer.slice(5 + n), { type: e, version: t, length: n, fragment: r } } } class he { constructor() { this.buffer = new Uint8Array(0) } feed(e) { this.buffer = W(this.buffer, e) } next() { if (this.buffer.length < 4) return null; const e = this.buffer[0], t = M(this.buffer, 1); if (this.buffer.length < 4 + t) return null; const n = this.buffer.slice(4, 4 + t), r = this.buffer.slice(0, 4 + t); return this.buffer = this.buffer.slice(4 + t), { type: e, length: t, body: n, raw: r } } } function ce(e) { let t = 0; const r = R(e, t); t += 2; const i = e.slice(t, t + 32); t += 32; const s = e[t++], a = e.slice(t, t + s); t += s; const h = R(e, t); t += 2; const c = e[t++]; let o = r, l = null, f = null; if (t < e.length) { const n = R(e, t); t += 2; const r = t + n; for (; t + 4 <= r;) { const n = R(e, t); t += 2; const r = R(e, t); t += 2; const i = e.slice(t, t + r); if (t += r, n === C && r >= 2) o = R(i, 0); else if (n === T && r >= 4) { const e = R(i, 0), t = R(i, 2); l = { group: e, key: i.slice(4, 4 + t) } } else n === b && r >= 3 && (f = K.decode(i.slice(3, 3 + i[2]))) } } const u = new Uint8Array([207, 33, 173, 116, 229, 154, 97, 17, 190, 29, 140, 2, 30, 101, 184, 145, 194, 162, 17, 22, 122, 187, 140, 94, 7, 158, 9, 226, 200, 168, 51, 156]); return { version: r, serverRandom: i, sessionId: a, cipherSuite: h, compression: c, selectedVersion: o, keyShare: l, alpn: f, isHRR: N(i, u), isTls13: o === n } } function oe(e) { let t = 0; t++; const n = R(e, t); t += 2; const r = e[t++]; return { namedCurve: n, serverPublicKey: e.slice(t, t + r) } } function le(e, t = 0) { let n = 0; if (t) { const t = e[n++]; n += t } if (n + 3 > e.length) return null; const r = M(e, n); if (n += 3, !r || n + 3 > e.length) return null; const i = M(e, n); return n += 3, i ? e.slice(n, n + i) : null } function fe(e) { const t = { alpn: null }; let n = 2; const r = 2 + R(e, 0); for (; n + 4 <= r;) { const r = R(e, n); n += 2; const i = R(e, n); if (n += 2, r === b && i >= 3) { const r = e[n + 2]; r > 0 && n + 3 + r <= n + i && (t.alpn = K.decode(e.slice(n + 3, n + 3 + r))) } n += i } return t } const F0 = e => { if (e = String(e ?? "").trim(), "[" === e[0] && "]" === e[e.length - 1] && (e = e.slice(1, -1)), !e || e.includes(":")) return ""; const t = e.split("."); if (4 !== t.length) return e; for (const n of t) { if ("" === n || n.length > 3) return e; let t = 0; for (let r = 0; r < n.length; r++) { const i = n.charCodeAt(r) - 48; if (i < 0 || i > 9) return e; t = 10 * t + i } if (t > 255) return e } return "" }, Z0 = e => e && 1 === e[0] && 112 === e[1]; function ue(e, n, r, { tls13: i = !0, tls12: s = !0, alpn: a = null } = {}) { n = F0(n); const c = []; i && c.push(4865, 4866, 4867), s && c.push(49199, 49200, 52392, 49195, 49196, 52393); const o = _(...c.flatMap(B)), l = [_(255, 1, 0, 1, 0)]; if (n) { const e = L.encode(n), t = _(0, B(e.length), e); l.push(_(B(v), B(t.length + 2), B(t.length), t)) } l.push(_(B(S), 0, 2, 1, 0)), l.push(_(B(A), 0, 6, 0, 4, 0, 29, 0, 23)); const f = _(...x.flatMap(B)); l.push(_(B(m), B(f.length + 2), B(f.length), f)); const u = Array.isArray(a) ? a.filter(Boolean) : a ? [a] : []; if (u.length) { const e = W(...u.map((e => { const t = L.encode(e); return _(t.length, t) }))); l.push(_(B(b), B(e.length + 2), B(e.length), e)) } if (i && r) { let e; if (l.push(s ? _(B(C), 0, 5, 4, 3, 4, 3, 3) : _(B(C), 0, 3, 2, 3, 4)), l.push(_(B(H), 0, 2, 1, 1)), r?.x25519 && r?.p256) e = W(_(0, 29, B(r.x25519.length), r.x25519), _(0, 23, B(r.p256.length), r.p256)); else if (r?.x25519) e = _(0, 29, B(r.x25519.length), r.x25519); else if (r?.p256) e = _(0, 23, B(r.p256.length), r.p256); else { if (!(r instanceof Uint8Array)) throw new Error("Invalid keyShares"); e = _(0, 23, B(r.length), r) } l.push(_(B(T), B(e.length + 2), B(e.length), e)) } const y = W(...l); return se(h, _(B(t), e, 0, B(o.length), o, 1, 0, B(y.length), y)) } const ye = e => { const t = new Uint8Array(8); return new DataView(t.buffer).setBigUint64(0, e, !1), t }, pe = (e, t) => { const n = e.slice(), r = ye(t); for (let e = 0; e < 8; e++)n[n.length - 8 + e] ^= r[e]; return n }, we = (e, t, n, r) => Promise.all([O(e, t, "key", P, n), O(e, t, "iv", P, r)]); class TlsClient { constructor(e, t = {}) { if (this.socket = e, this.serverName = t.serverName || "", this.supportTls13 = !1 !== t.tls13, this.supportTls12 = !1 !== t.tls12, !this.supportTls13 && !this.supportTls12) throw new Error("At least one TLS version must be enabled"); this.alpnProtocols = Array.isArray(t.alpn) ? t.alpn : t.alpn ? [t.alpn] : null, this.timeout = t.timeout ?? 3e4, this.clientRandom = D(32), this.serverRandom = null, this.handshakeChunks = [], this.handshakeComplete = !1, this.negotiatedAlpn = null, this.cipherSuite = null, this.cipherConfig = null, this.isTls13 = !1, this.masterSecret = null, this.handshakeSecret = null, this.clientWriteKey = null, this.serverWriteKey = null, this.clientWriteIv = null, this.serverWriteIv = null, this.clientHandshakeKey = null, this.serverHandshakeKey = null, this.clientHandshakeIv = null, this.serverHandshakeIv = null, this.clientAppKey = null, this.serverAppKey = null, this.clientAppIv = null, this.serverAppIv = null, this.clientSeqNum = 0n, this.serverSeqNum = 0n, this.recordParser = new ae, this.handshakeParser = new he, this.keyPairs = new Map, this.ecdhKeyPair = null, this.sawCert = !1 } recordHandshake(e) { this.handshakeChunks.push(e) } transcript() { return 1 === this.handshakeChunks.length ? this.handshakeChunks[0] : W(...this.handshakeChunks) } getCipherConfig(e) { return U.get(e) || null } async readChunk(e) { if (!this.timeout) return e.read(); let t; const n = e.read(), r = await Promise.race([n, new Promise(e => t = setTimeout(e, this.timeout, 0))]).finally(() => clearTimeout(t)); if (r) return r; try { await e.cancel("TLS read timeout") } catch { } try { await n } catch { } throw new Error("TLS read timeout") } async pr(e, t, n) { for (; ;) { let r; for (; r = this.recordParser.next();)if (await t(r)) return; const { value: i, done: s } = await this.readChunk(e); if (s) throw new Error(n); this.recordParser.feed(i) } } async ph(e, t, n) { for (let e; e = this.handshakeParser.next();)if (await t(e)) return; return this.pr(e, (async e => { if (e.type === i) { if (Z0(e.fragment)) return; throw new Error(`TLS Alert: ${e.fragment[1]}`) } if (e.type === s) { this.handshakeParser.feed(e.fragment); for (let e; e = this.handshakeParser.next();)if (await t(e)) return 1 } }), n) } async acceptCertificate(e) { if (!e?.length) throw new Error("Empty certificate"); this.sawCert = !0 } async handshake() { const [t, n] = await Promise.all([F("P-256"), F("X25519")]); this.keyPairs = new Map([[23, t], [29, n]]), this.ecdhKeyPair = t.keyPair; const r = this.socket.readable.getReader(), i = this.socket.writable.getWriter(); try { const a = ue(this.clientRandom, this.serverName, { x25519: n.publicKeyRaw, p256: t.publicKeyRaw }, { tls13: this.supportTls13, tls12: this.supportTls12, alpn: this.alpnProtocols }); this.recordHandshake(a), await i.write(ie(s, a, e)); const h = await this.receiveServerHello(r); if (h.isHRR) throw new Error("HelloRetryRequest is not supported by TLSClientMini"); if (h.keyShare?.group && this.keyPairs.has(h.keyShare.group)) { const e = this.keyPairs.get(h.keyShare.group); this.ecdhKeyPair = e.keyPair } h.isTls13 ? await this.handshakeTls13(r, i, h) : await this.handshakeTls12(r, i), this.handshakeComplete = !0 } finally { r.releaseLock(), i.releaseLock() } } async receiveServerHello(e) { for (; ;) { const { value: t, done: n } = await this.readChunk(e); if (n) throw new Error("Connection closed waiting for ServerHello"); let r; for (this.recordParser.feed(t); r = this.recordParser.next();) { if (r.type === i) { if (Z0(r.fragment)) continue; throw new Error(`TLS Alert: level=${r.fragment[0]}, desc=${r.fragment[1]}`) } if (r.type !== s) continue; let e; for (this.handshakeParser.feed(r.fragment); e = this.handshakeParser.next();) { if (e.type !== c) continue; this.recordHandshake(e.raw); const t = ce(e.body); if (this.serverRandom = t.serverRandom, this.cipherSuite = t.cipherSuite, this.cipherConfig = this.getCipherConfig(t.cipherSuite), this.isTls13 = t.isTls13, this.negotiatedAlpn = t.alpn || null, !this.cipherConfig) throw new Error(`Unsupported cipher suite: 0x${t.cipherSuite.toString(16)}`); return t } } } } async handshakeTls12(e, t) { let n = null, a = !1; if (await this.ph(e, (async e => { switch (e.type) { case f: { this.recordHandshake(e.raw); const t = le(e.body, 1); if (!t) throw new Error("Missing TLS 1.2 certificate"); await this.acceptCertificate(t); break } case u: this.recordHandshake(e.raw), n = oe(e.body); break; case p: return this.recordHandshake(e.raw), a = !0, 1; case y: throw new Error("Client certificate is not supported"); default: this.recordHandshake(e.raw) } }), "Connection closed during TLS 1.2 handshake"), !this.sawCert) throw new Error("Missing TLS 1.2 leaf certificate"); if (!n) throw new Error("Missing TLS 1.2 ServerKeyExchange"); const h = I.get(n.namedCurve); if (!h) throw new Error(`Unsupported named curve: 0x${n.namedCurve.toString(16)}`); const c = this.keyPairs.get(n.namedCurve); if (!c) throw new Error(`Missing key pair for curve: 0x${n.namedCurve.toString(16)}`); const o = await Y(c.keyPair.privateKey, n.serverPublicKey, h), l = se(d, _(c.publicKeyRaw.length, c.publicKeyRaw)); this.recordHandshake(l); const w = this.cipherConfig.hash; this.masterSecret = await V(o, "master secret", W(this.clientRandom, this.serverRandom), 48, w); const k = this.cipherConfig.keyLen, v = this.cipherConfig.ivLen, A = await V(this.masterSecret, "key expansion", W(this.serverRandom, this.clientRandom), 2 * k + 2 * v, w); this.clientWriteKey = A.slice(0, k), this.serverWriteKey = A.slice(k, 2 * k), this.clientWriteIv = A.slice(2 * k, 2 * k + v), this.serverWriteIv = A.slice(2 * k + v, 2 * k + 2 * v), await t.write(ie(s, l)), await t.write(ie(r, _(1))); const S = await V(this.masterSecret, "client finished", await G(w, this.transcript()), 12, w), m = se(g, S); this.recordHandshake(m), await t.write(ie(s, await this.encryptTls12(m, s))); let b = !1; await this.pr(e, (async e => { if (e.type === i) { if (Z0(e.fragment)) return; throw new Error(`TLS Alert: ${e.fragment[1]}`) } if (e.type === r) return void (b = !0); if (e.type !== s || !b) return; const t = await this.decryptTls12(e.fragment, s); if (t[0] !== g) return; const n = M(t, 1), a = t.slice(4, 4 + n), h = await V(this.masterSecret, "server finished", await G(w, this.transcript()), 12, w); if (!N(a, h)) throw new Error("TLS 1.2 server Finished verify failed"); return 1 }), "Connection closed waiting for TLS 1.2 Finished") } async handshakeTls13(e, t, n) { const h = I.get(n.keyShare?.group); if (!h || !n.keyShare?.key?.length) throw new Error("Missing TLS 1.3 key_share"); const c = this.cipherConfig.hash, o = q(c), u = this.cipherConfig.keyLen, p = this.cipherConfig.ivLen, d = await Y(this.ecdhKeyPair.privateKey, n.keyShare.key, h), k = await X(c, null, new Uint8Array(o)), v = await O(c, k, "derived", await G(c, P), o); this.handshakeSecret = await X(c, v, d); const A = await G(c, this.transcript()), S = await O(c, this.handshakeSecret, "c hs traffic", A, o), m = await O(c, this.handshakeSecret, "s hs traffic", A, o);[this.clientHandshakeKey, this.clientHandshakeIv] = await we(c, S, u, p), [this.serverHandshakeKey, this.serverHandshakeIv] = await we(c, m, u, p); const b = await O(c, m, "finished", P, o); let C = !1; const H = async e => { switch (e.type) { case l: { const t = fe(e.body); t.alpn && (this.negotiatedAlpn = t.alpn), this.recordHandshake(e.raw); break } case f: { const t = le(e.body); if (!t) throw new Error("Missing TLS 1.3 certificate"); await this.acceptCertificate(t), this.recordHandshake(e.raw); break } case y: throw new Error("Client certificate is not supported"); case w: this.recordHandshake(e.raw); break; case g: { const t = await $(c, b, await G(c, this.transcript())); if (!N(t, e.body)) throw new Error("TLS 1.3 server Finished verify failed"); this.recordHandshake(e.raw), C = !0; break } default: this.recordHandshake(e.raw) } }; await this.pr(e, (async e => { if (e.type === r || e.type === s) return; if (e.type === i) { if (Z0(e.fragment)) return; throw new Error(`TLS Alert: ${e.fragment[1]}`) } if (e.type !== a) return; const t = await this.decryptTls13Handshake(e.fragment), n = t[t.length - 1], h = t.slice(0, -1); if (n === s) { this.handshakeParser.feed(h); for (let e; e = this.handshakeParser.next();)if (await H(e), C) return 1 } }), "Connection closed during TLS 1.3 handshake"); const T = await G(c, this.transcript()), E = await O(c, this.handshakeSecret, "derived", await G(c, P), o), L = await X(c, E, new Uint8Array(o)), K = await O(c, L, "c ap traffic", T, o), U = await O(c, L, "s ap traffic", T, o);[this.clientAppKey, this.clientAppIv] = await we(c, K, u, p), [this.serverAppKey, this.serverAppIv] = await we(c, U, u, p); const x = await O(c, S, "finished", P, o), _ = await $(c, x, await G(c, this.transcript())), B = se(g, _); this.recordHandshake(B), await t.write(ie(a, await this.encryptTls13Handshake(W(B, [s])))), this.clientSeqNum = 0n, this.serverSeqNum = 0n } async encryptTls12(e, n) { const r = this.clientSeqNum++, i = ye(r), s = W(i, [n], B(t), B(e.length)); if (this.cipherConfig.chacha) { const t = pe(this.clientWriteIv, r); return ne(this.clientWriteKey, t, e, s) } const a = D(8); return W(a, await j(this.clientWriteKey, W(this.clientWriteIv, a), e, s)) } async decryptTls12(e, n) { const r = this.serverSeqNum++, i = ye(r); if (this.cipherConfig.chacha) { const s = pe(this.serverWriteIv, r); return re(this.serverWriteKey, s, e, W(i, [n], B(t), B(e.length - 16))) } const s = e.slice(0, 8), a = e.slice(8); return z(this.serverWriteKey, W(this.serverWriteIv, s), a, W(i, [n], B(t), B(a.length - 16))) } async encryptTls13Handshake(e) { const t = pe(this.clientHandshakeIv, this.clientSeqNum++), n = _(a, 3, 3, B(e.length + 16)); return this.cipherConfig.chacha ? ne(this.clientHandshakeKey, t, e, n) : j(this.clientHandshakeKey, t, e, n) } async decryptTls13Handshake(e) { const t = pe(this.serverHandshakeIv, this.serverSeqNum++), n = _(a, 3, 3, B(e.length)), r = await (this.cipherConfig.chacha ? re(this.serverHandshakeKey, t, e, n) : z(this.serverHandshakeKey, t, e, n)); let i = r.length - 1; for (; i >= 0 && !r[i];)i--; return i < 0 ? P : r.slice(0, i + 1) } async encryptTls13(e) { const t = W(e, [a]), n = pe(this.clientAppIv, this.clientSeqNum++), r = _(a, 3, 3, B(t.length + 16)); return this.cipherConfig.chacha ? ne(this.clientAppKey, n, t, r) : j(this.clientAppKey, n, t, r) } async decryptTls13(e) { const t = pe(this.serverAppIv, this.serverSeqNum++), n = _(a, 3, 3, B(e.length)), r = this.cipherConfig.chacha ? await re(this.serverAppKey, t, e, n) : await z(this.serverAppKey, t, e, n); let i = r.length - 1; for (; i >= 0 && !r[i];)i--; return i < 0 ? { data: P, type: 0 } : { data: r.slice(0, i), type: r[i] } } async write(e) { if (!this.handshakeComplete) throw new Error("Handshake not complete"); const t = this.socket.writable.getWriter(); try { this.isTls13 ? await t.write(ie(a, await this.encryptTls13(e))) : await t.write(ie(a, await this.encryptTls12(e, a))) } finally { t.releaseLock() } } async read() { for (; ;) { let e; for (; e = this.recordParser.next();) { if (e.type === i) { if (e.fragment[1] === E) return null; throw new Error(`TLS Alert: ${e.fragment[1]}`) } if (e.type !== a) continue; if (!this.isTls13) return this.decryptTls12(e.fragment, a); const { data: t, type: n } = await this.decryptTls13(e.fragment); if (n === a) return t; if (n === i) { if (t[1] === E) return null; throw new Error(`TLS Alert: ${t[1]}`) } if (n !== s) continue; let r; for (this.handshakeParser.feed(t); r = this.handshakeParser.next();)if (r.type !== o && r.type === k) throw new Error("TLS 1.3 KeyUpdate is not supported by TLSClientMini") } const t = this.socket.readable.getReader(); try { const { value: e, done: n } = await this.readChunk(t); if (n) return null; this.recordParser.feed(e) } finally { t.releaseLock() } } } close() { this.socket.close() } }
// ==================== merged target ====================
import { connect } from 'cloudflare:sockets';
const enc = L;
const dec = K;
const DEFAULT_TIMEOUT_MS = 9999;
const DEFAULT_READ_LIMIT = 65536;
const CRLF = Uint8Array.of(13, 10);
const HEADER_BODY_SEPARATOR = Uint8Array.of(13, 10, 13, 10);
const HTTP_STATUS_RE = /^HTTP\/\d(?:\.\d)?\s+(\d{3})/;
const CHUNKED_TRANSFER_RE = /\r\ntransfer-encoding:\s*chunked\r\n/i;
const USAGE_EXAMPLES = [
'GET /check?proxyip=118.34.215.56:34042',
"POST JSON {'proxyip':'118.34.215.56:34042'}",
];
const CHECK_RESPONSE_HEADERS = {
'content-type': 'application/json; charset=utf-8',
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET, POST, OPTIONS',
'access-control-allow-headers': 'Content-Type',
};
// 用两个固定探针分别测试出口:
// - 访问 ipv4.090227.xyz 看返回的出口 IP
// - 访问 ipv6.090227.xyz 看返回的出口 IP
// 最终是否“支持 IPv4/IPv6”取决于探针返回的出口 IP 文本,不是候选代理地址本身。
const PROBE_TARGETS = [
['ipv4', 'ipv4.090227.xyz', '/'],
['ipv6', 'ipv6.090227.xyz', '/'],
].map(([name, host, path]) => ({
name,
host,
request: enc.encode(
`GET ${path} HTTP/1.1\r\nHost: ${host}\r\nAccept: application/json, text/plain, */*\r\nAccept-Language: en-US,en;q=0.9\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\nAccept-Encoding: identity\r\nConnection: close\r\n\r\n`
),
}));
function parsePositiveInt(value, fallback) {
const parsed = Number.parseInt(`${value ?? ''}`, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function concatBytes(chunks) {
const merged = new Uint8Array(chunks.reduce((sum, { length }) => sum + length, 0));
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
return merged;
}
function withTimeout(promise, timeoutMs, label) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error(`${label} timeout after ${timeoutMs}ms`)),
timeoutMs
);
}),
]).finally(() => clearTimeout(timer));
}
function indexOfBytes(haystack, needle, start = 0) {
outer: for (let i = start; i <= haystack.length - needle.length; i++) {
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
continue outer;
}
}
return i;
}
return -1;
}
function pickExitIp(payload) {
// 探针 JSON 里优先取 ip,兼容部分接口用 ipAddress 命名。
const ip = payload?.ip ?? payload?.ipAddress;
return typeof ip === 'string' && ip ? ip : null;
}
function checkJsonResponse(data, status = 200) {
return new Response(JSON.stringify(data, null, 2), {
status,
headers: CHECK_RESPONSE_HEADERS,
});
}
// 优先使用探针返回的 ipType;若没有 ipType,再回退到根据出口 IP 字符串判定。
function getExitFamily(result) {
if (!result?.ok) return null;
const ipType = `${result?.exit?.ipType ?? ''}`.toLowerCase();
if (ipType === 'ipv4' || ipType === 'ipv6') return ipType;
const exitIp = pickExitIp(result.exit) ?? '';
// IPv4:必须匹配 x.x.x.x 且每段 0-255。
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(exitIp) && exitIp.split('.').every((part) => Number(part) <= 255)) return 'ipv4';
// IPv6:当前逻辑只要包含 ":" 就视为 IPv6(未做完整 RFC 严格校验)。
if (exitIp.includes(':')) return 'ipv6';
return null;
}
async function handleCheckProxyRequest(req) {
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: CHECK_RESPONSE_HEADERS,
});
}
if (req.method !== 'GET' && req.method !== 'POST') {
return checkJsonResponse(
{ success: false, error: 'method not allowed', usage: USAGE_EXAMPLES },
405
);
}
try {
const url = new URL(req.url);
const pathname = url.pathname.replace(/\/+$/, '') || '/';
if (pathname !== '/check') {
return checkJsonResponse(
{ success: false, error: 'not found', usage: USAGE_EXAMPLES },
404
);
}
const { searchParams } = url;
const bodyText = req.method === 'POST' ? await req.text() : '';
const body = bodyText ? JSON.parse(bodyText) : {};
const rawCandidates = body.proxyips ?? body.proxyip ?? searchParams.get('proxyip');
const rawList = Array.isArray(rawCandidates) ? rawCandidates : `${rawCandidates ?? ''}`.split(/[\s,]+/);
const candidates = [...new Set(rawList.map((item) => `${item ?? ''}`.trim()).filter(Boolean))];
const timeoutMs = parsePositiveInt(body.timeoutMs ?? searchParams.get('timeoutMs'), DEFAULT_TIMEOUT_MS);
const readLimit = parsePositiveInt(body.readLimit ?? searchParams.get('readLimit'), DEFAULT_READ_LIMIT);
if (!candidates.length) {
return checkJsonResponse(
{ success: false, error: 'missing proxyip', usage: USAGE_EXAMPLES },
400
);
}
const results = await Promise.all(
candidates.map(async (rawCandidate) => {
const defaultPort = 443;
let candidate;
if (rawCandidate.startsWith('[')) {
const match = rawCandidate.match(/^\[([^\]]+)\](?::(\d+))?$/);
if (!match) throw new Error(`invalid IPv6 candidate: ${rawCandidate}`);
candidate = { raw: rawCandidate, hostname: match[1], port: Number(match[2]) || defaultPort };
} else {
const [, hostname = rawCandidate, port] = rawCandidate.match(/^([^:]+):(\d+)$/) ?? [];
candidate = { raw: rawCandidate, hostname, port: Number(port) || defaultPort };
}
const probeResults = await Promise.all(
PROBE_TARGETS.map(async (target) => {
let socket = null;
let tlsClient = null;
let connectMs = null;
let tlsMs = null;
let httpMs = null;
let statusCode = null;
const buildResult = (
ok,
resultStatusCode,
error,
{ exit = null } = {}
) => ({
candidate: candidate.raw,
connect_ms: connectMs,
tls_ms: tlsMs,
http_ms: httpMs,
status_code: resultStatusCode,
ok,
error,
exit,
});
try {
let startedAt = Date.now();
const hostAddr = candidate.hostname.includes(':') ? `[${candidate.hostname}]` : candidate.hostname;
socket = connect({ hostname: hostAddr, port: candidate.port });
await withTimeout(socket.opened, timeoutMs, 'tcp connect');
connectMs = Date.now() - startedAt;
startedAt = Date.now();
tlsClient = new TlsClient(socket, { serverName: target.host, timeout: timeoutMs });
await withTimeout(tlsClient.handshake(), timeoutMs, 'tls handshake');
tlsMs = Date.now() - startedAt;
startedAt = Date.now();
await withTimeout(tlsClient.write(target.request), timeoutMs, 'http write');
const chunks = [];
for (let read = 0; read < readLimit;) {
const chunk = (await withTimeout(tlsClient.read(), timeoutMs, 'http read'))?.subarray(0, readLimit - read);
if (!chunk?.length) {
break;
}
chunks.push(chunk);
read += chunk.length;
}
const rawResponse = concatBytes(chunks);
httpMs = Date.now() - startedAt;
if (!rawResponse.length) return buildResult(false, null, 'empty response');
const splitIndex = indexOfBytes(rawResponse, HEADER_BODY_SEPARATOR);
const [headerBytes, bodyBytes] = splitIndex < 0
? [rawResponse, new Uint8Array(0)]
: [rawResponse.subarray(0, splitIndex), rawResponse.subarray(splitIndex + HEADER_BODY_SEPARATOR.length)];
const headerText = dec.decode(headerBytes);
statusCode = Number(headerText.match(HTTP_STATUS_RE)?.[1] ?? 0) || null;
let responseBodyBytes;
if (splitIndex < 0) {
responseBodyBytes = rawResponse;
} else if (CHUNKED_TRANSFER_RE.test(`\r\n${headerText}\r\n`)) {
const decodedChunks = [];
for (let offset = 0; offset < bodyBytes.length;) {
const lineEnd = indexOfBytes(bodyBytes, CRLF, offset);
if (lineEnd < 0) throw new Error('missing chunk size line terminator');
const sizeHex = dec.decode(bodyBytes.slice(offset, lineEnd)).split(';', 1)[0].trim();
const size = Number.parseInt(sizeHex, 16);
if (!Number.isFinite(size)) throw new Error(`invalid chunk size: ${sizeHex}`);
const bodyStart = lineEnd + CRLF.length;
const bodyEnd = bodyStart + size;
if (!size) break;
if (bodyEnd + CRLF.length > bodyBytes.length) throw new Error('truncated chunk body');
decodedChunks.push(bodyBytes.slice(bodyStart, bodyEnd));
offset = bodyEnd + CRLF.length;
}
responseBodyBytes = concatBytes(decodedChunks);
} else {
responseBodyBytes = bodyBytes;
}
const responseText = dec.decode(responseBodyBytes);
if (statusCode !== 200) {
const bodyPreview = responseText ? ` body: ${responseText.slice(0, 120)}` : '';
return buildResult(false, statusCode, `unexpected status: ${statusCode ?? 'unknown'}${bodyPreview}`);
}
let payload;
try {
payload = JSON.parse(responseText);
} catch (error) {
return buildResult(false, statusCode, `invalid json response: ${String(error?.message || error)}`);
}
if (!pickExitIp(payload)) return buildResult(false, statusCode, 'probe json missing exit ip');
return buildResult(true, statusCode, null, { exit: payload });
} catch (error) {
return buildResult(false, statusCode, String(error?.message || error));
} finally {
try { tlsClient?.close(); } catch { }
try { if (!tlsClient) socket?.close(); } catch { }
}
})
);
// probeResults 顺序与 PROBE_TARGETS 一致:[0] => ipv4 探针结果, [1] => ipv6 探针结果
const [ipv4, ipv6] = probeResults;
const hasIPv4 = getExitFamily(ipv4) === 'ipv4';
const hasIPv6 = getExitFamily(ipv6) === 'ipv6';
// probe_results 展示规则:
// - 默认:supports_ipv4/supports_ipv6 为 true 才展示对应探针结果
// - 兜底:当两者都为 false 时,同时展示 ipv4/ipv6 探针结果,便于排错
const displayedProbeResults = {};
if (!hasIPv4 && !hasIPv6) {
if (ipv4) displayedProbeResults.ipv4 = ipv4;
if (ipv6) displayedProbeResults.ipv6 = ipv6;
} else {
if (hasIPv4 && ipv4) displayedProbeResults.ipv4 = ipv4;
if (hasIPv6 && ipv6) displayedProbeResults.ipv6 = ipv6;
}
const inferredStack = hasIPv4 && hasIPv6 ? 'dual_stack' : hasIPv4 ? 'ipv4_only' : hasIPv6 ? 'ipv6_only' : 'unknown';
const responseTime = probeResults.length
? Math.ceil(probeResults.reduce((s, r) => s + (Number.isFinite(r?.connect_ms) ? r.connect_ms : 0), 0) / probeResults.length)
: 0;
return {
candidate: rawCandidate,
success: probeResults.some((result) => result.ok),
proxyIP: candidate.hostname,
portRemote: candidate.port,
inferred_stack: inferredStack,
supports_ipv4: hasIPv4,
supports_ipv6: hasIPv6,
dual_stack: inferredStack === 'dual_stack',
responseTime,
colo: req.cf?.colo || 'CF',
timeStamp: new Date().toISOString(),
probe_results: displayedProbeResults,
};
})
);
return checkJsonResponse(results.length === 1 ? results[0] : results);
} catch (error) {
return checkJsonResponse({ ok: false, error: String(error?.message || error) }, 500);
}
}