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

Workspace

开始检测

输入单个 IP、IPv6、域名或一整段列表。单条模式支持历史记录,批量模式适合直接粘贴多行目标。

实时解析与验证

单条模式支持历史快速回填,按 Enter 可以直接开始检测。

批量检测
Single / 单目标

Results

检测结果

结果、落地 IP 和地图会在这里按检测进度逐步展开。

Idle

等待开始检测

输入目标后,检测结果、出口信息和地图会在这里展示。

Guide

什么是 ProxyIP

用一段更接近实际部署场景的说明,快速理解 ProxyIP 的定义、作用和筛选标准。

Cloudflare Workers / TCP
概念

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。

Finder

获取更多 ProxyIP

按端口和地区从网络测绘数据库中发现候选 ProxyIP,方便继续放回本工具检测可用性。

More
`; } // ==================== 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); } }