前言
本实验将会为大家解析cloud flare的反向解析代理服务如何搭建,works如何创建等等。本文中教学创建的实例已在文章编写结束后释放,该项技术不可用于违法用途!违者自行承担后果!!
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
原理拓扑图
一、知识链条
1、Cloud flare简介
Cloudflare 是一家全球性的云服务提供商,成立于2009年。它提供了一系列网络基础设施和安全性服务,帮助网站加速、保护、以及优化其在线内容。
Cloudflare 提供的主要服务包括:
1. CDN(内容分发网络):通过将内容分发到全球性的数据中心,加速网站加载速度,提高用户体验。
2. DDoS 攻击防护:保护网站免受分布式拒绝服务(DDoS)攻击,确保服务的可用性和稳定性。
3. WAF(网络应用防火墙):监控和过滤网站流量,阻止恶意攻击、SQL 注入和跨站脚本等网络攻击。
4. DNS(域名解析服务):提供快速、可靠的域名解析服务,帮助网站管理和优化其网络流量。
5. SSL/TLS 加密:提供免费的 SSL/TLS 加密证书,加密网站与访问者之间的通信,保护数据安全和隐私。
6. Workers:允许开发者在 Cloudflare 的边缘网络上运行自定义的代码,以实现更高级的功能和定制化的处理。
Cloudflare 的服务被广泛应用于各种规模的网站和在线应用程序中,包括企业网站、电子商务平台、博客、游戏、移动应用等。其目标是通过提供简单易用、高性能和高安全性的服务,帮助客户在互联网上取得成功。同时,Cloud flare持有全球顶级DNS1.1.1.1,可见其实力不凡。
=========================================================================
2、VLess协议
vless 协议是一个相对于 V2Ray 的一种新的协议,它是 V2Ray 的一个轻量级替代品。vless 的设计目标是减少传输数据的开销,提高性能,并且简化配置。
vless 协议的原理基本上与 V2Ray 中的 vmess 协议类似,都是在传输层上实现的一个加密隧道,用于在客户端和服务器之间传输数据。但是 vless 相对于 vmess 有一些不同之处:
1. **减少传输数据的开销**:vless 协议在传输数据时采用了更加精简的数据格式,以减少传输数据的开销,提高传输效率。
2. **简化配置**:相对于 vmess,vless 的配置更加简单,主要是因为它省略了一些在某些情况下不必要的选项和字段。
3. **增强安全性**:vless 在安全性方面与 vmess 相当,都是采用了 TLS 加密和传输,保障了数据的安全性和隐私。
4. **性能提升**:由于 vless 减少了数据传输的开销,并且在设计上更加精简,因此在一些场景下可能会提供更好的性能表现。
总的来说,vless 协议是为了简化配置、提高性能、减少传输开销而设计的一种协议,它保留了 vmess 协议的安全性和灵活性,同时简化了配置和提高了性能。
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3、Cloud flare-works实例
Cloudflare的"Works"是Cloudflare推出的一系列整合服务,旨在为企业提供更全面的网络安全和性能优化解决方案。Works整合了Cloudflare的不同产品,包括:
1. **Access:** 提供零信任访问控制,确保用户可以安全地访问企业网络和应用程序,无论他们身在何处。
2. **Gateway:** 提供安全的互联网访问,包括网络内容过滤、恶意软件防护、广告拦截和隐私保护。
3. **Spectrum:** 扩展Cloudflare的边缘网络保护到任何TCP/UDP流量,使得所有的网络流量都能受到Cloudflare的保护。
4. **Workers:** 允许开发者在Cloudflare的边缘网络上运行轻量级的JavaScript代码,从而实现定制化的网络功能和逻辑。
5. **Orbit:** 提供实时的API和事件监控,使得开发者可以实时地查看和分析其应用程序的性能和行为。
通过将这些服务整合在一起,Cloudflare Works可以为企业提供更强大、更全面的网络安全和性能优化解决方案,帮助他们保护应用程序、数据和用户,并提高网络性能和可用性。
4、UUID
UUID(Universally Unique Identifier)是一种标识符,用于在计算系统中唯一地标识信息或实体。它是由一个128位的数字组成,通常以32个十六进制数字的形式表示,中间用连字符分隔。UUID的生成算法保证了在理论上具有非常低的重复概率。
UUID广泛应用于各种场景,例如:
1. 数据库系统:UUID可以用作数据库表格的主键,确保每个记录在整个数据库中具有唯一标识。
2. 分布式系统:在分布式系统中,UUID可以用于唯一标识各个节点、任务或事件,以确保全局唯一性。
3. 软件开发:开发人员可以使用UUID作为临时文件名、会话标识符或唯一的标识符来跟踪和管理数据。
4. 网络通信:UUID可以用于标识网络协议中的消息、数据包或会话,以确保唯一性和识别性。
总之,UUID是一种在计算系统中用于唯一标识信息或实体的标识符,具有广泛的应用领域。
接下来我们开始创建workers实例
二、创建workers
1、访问cloud flare
先进入官网>>>Cloud flare<<<
请记住,访问cloud flare时,会有人机验证,照常过就行,如果无法通过,请开启霍格沃兹力量或者切换IP即可。
这里已经进入首页,可以在右上角调整语言设置:
首先,你要有非CN的账号,(推荐使用Google)我这里使用Google/US账户进行注册登录:
进入cloud flare首页,在左侧找到workers和pages,进入。
2、创建workers
进入创建应用程序,开始创建一个workers实例。
给你的workers创建一个名称,我这里就使用ceshi,然后点击部署。
部署完成后,点击编辑代码。
将默认原来的代码全部清除
3、实例代码部署
edgetunnel/src/worker-vless.js at main · leilei223/edgetunnel · GitHub
实例代码1、(本章节使用)
清除后导入workers代码
// <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->.
// @ts-ignore
import { connect } from 'cloudflare:sockets';// How to generate your own UUID:
// [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()"
let userID = 'UUID';let proxyIP = 'CDN-IP';if (!isValidUUID(userID)) {throw new Error('uuid is not valid');
}export default {/*** @param {import("@cloudflare/workers-types").Request} request* @param {{UUID: string, PROXYIP: string}} env* @param {import("@cloudflare/workers-types").ExecutionContext} ctx* @returns {Promise<Response>}*/async fetch(request, env, ctx) {try {userID = env.UUID || userID;proxyIP = env.PROXYIP || proxyIP;const upgradeHeader = request.headers.get('Upgrade');if (!upgradeHeader || upgradeHeader !== 'websocket') {const url = new URL(request.url);switch (url.pathname) {case '/':return new Response(JSON.stringify(request.cf), { status: 200 });case `/${userID}`: {const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));return new Response(`${vlessConfig}`, {status: 200,headers: {"Content-Type": "text/plain;charset=utf-8",}});}default:return new Response('Not found', { status: 404 });}} else {return await vlessOverWSHandler(request);}} catch (err) {/** @type {Error} */ let e = err;return new Response(e.toString());}},
};/*** * @param {import("@cloudflare/workers-types").Request} request*/
async function vlessOverWSHandler(request) {/** @type {import("@cloudflare/workers-types").WebSocket[]} */// @ts-ignoreconst webSocketPair = new WebSocketPair();const [client, webSocket] = Object.values(webSocketPair);webSocket.accept();let address = '';let portWithRandomLog = '';const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {console.log(`[${address}:${portWithRandomLog}] ${info}`, event || '');};const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);/** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/let remoteSocketWapper = {value: null,};let udpStreamWrite = null;let isDns = false;// ws --> remotereadableWebSocketStream.pipeTo(new WritableStream({async write(chunk, controller) {if (isDns && udpStreamWrite) {return udpStreamWrite(chunk);}if (remoteSocketWapper.value) {const writer = remoteSocketWapper.value.writable.getWriter()await writer.write(chunk);writer.releaseLock();return;}const {hasError,message,portRemote = 443,addressRemote = '',rawDataIndex,vlessVersion = new Uint8Array([0, 0]),isUDP,} = processVlessHeader(chunk, userID);address = addressRemote;portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp '} `;if (hasError) {// controller.error(message);throw new Error(message); // cf seems has bug, controller.error will not end stream// webSocket.close(1000, message);return;}// if UDP but port not DNS port, close itif (isUDP) {if (portRemote === 53) {isDns = true;} else {// controller.error('UDP proxy only enable for DNS which is port 53');throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end streamreturn;}}// ["version", "附加信息长度 N"]const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);const rawClientData = chunk.slice(rawDataIndex);// TODO: support udp here when cf runtime has udp supportif (isDns) {const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log);udpStreamWrite = write;udpStreamWrite(rawClientData);return;}handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);},close() {log(`readableWebSocketStream is close`);},abort(reason) {log(`readableWebSocketStream is abort`, JSON.stringify(reason));},})).catch((err) => {log('readableWebSocketStream pipeTo error', err);});return new Response(null, {status: 101,// @ts-ignorewebSocket: client,});
}/*** Handles outbound TCP connections.** @param {any} remoteSocket * @param {string} addressRemote The remote address to connect to.* @param {number} portRemote The remote port to connect to.* @param {Uint8Array} rawClientData The raw client data to write.* @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.* @param {Uint8Array} vlessResponseHeader The VLESS response header.* @param {function} log The logging function.* @returns {Promise<void>} The remote socket.*/
async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {async function connectAndWrite(address, port) {/** @type {import("@cloudflare/workers-types").Socket} */const tcpSocket = connect({hostname: address,port: port,});remoteSocket.value = tcpSocket;log(`connected to ${address}:${port}`);const writer = tcpSocket.writable.getWriter();await writer.write(rawClientData); // first write, nomal is tls client hellowriter.releaseLock();return tcpSocket;}// if the cf connect tcp socket have no incoming data, we retry to redirect ipasync function retry() {const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote)// no matter retry success or not, close websockettcpSocket.closed.catch(error => {console.log('retry tcpSocket closed error', error);}).finally(() => {safeCloseWebSocket(webSocket);})remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);}const tcpSocket = await connectAndWrite(addressRemote, portRemote);// when remoteSocket is ready, pass to websocket// remote--> wsremoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
}/*** * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer* @param {string} earlyDataHeader for ws 0rtt* @param {(info: string)=> void} log for ws 0rtt*/
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {let readableStreamCancel = false;const stream = new ReadableStream({start(controller) {webSocketServer.addEventListener('message', (event) => {if (readableStreamCancel) {return;}const message = event.data;controller.enqueue(message);});// The event means that the client closed the client -> server stream.// However, the server -> client stream is still open until you call close() on the server side.// The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.webSocketServer.addEventListener('close', () => {// client send close, need close server// if stream is cancel, skip controller.closesafeCloseWebSocket(webSocketServer);if (readableStreamCancel) {return;}controller.close();});webSocketServer.addEventListener('error', (err) => {log('webSocketServer has error');controller.error(err);});// for ws 0rttconst { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);if (error) {controller.error(error);} else if (earlyData) {controller.enqueue(earlyData);}},pull(controller) {// if ws can stop read if stream is full, we can implement backpressure// https://streams.spec.whatwg.org/#example-rs-push-backpressure},cancel(reason) {// 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here// 2. if readableStream is cancel, all controller.close/enqueue need skip,// 3. but from testing controller.error still work even if readableStream is cancelif (readableStreamCancel) {return;}log(`ReadableStream was canceled, due to ${reason}`)readableStreamCancel = true;safeCloseWebSocket(webSocketServer);}});return stream;}// https://xtls.github.io/development/protocols/vless.html
// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw/*** * @param { ArrayBuffer} vlessBuffer * @param {string} userID * @returns */
function processVlessHeader( vlessBuffer,userID) {if (vlessBuffer.byteLength < 24) {return {hasError: true,message: 'invalid data',};}const version = new Uint8Array(vlessBuffer.slice(0, 1));let isValidUser = false;let isUDP = false;if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) {isValidUser = true;}if (!isValidUser) {return {hasError: true,message: 'invalid user',};}const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];//skip opt for nowconst command = new Uint8Array(vlessBuffer.slice(18 + optLength, 18 + optLength + 1))[0];// 0x01 TCP// 0x02 UDP// 0x03 MUXif (command === 1) {} else if (command === 2) {isUDP = true;} else {return {hasError: true,message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,};}const portIndex = 18 + optLength + 1;const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);// port is big-Endian in raw data etc 80 == 0x005dconst portRemote = new DataView(portBuffer).getUint16(0);let addressIndex = portIndex + 2;const addressBuffer = new Uint8Array(vlessBuffer.slice(addressIndex, addressIndex + 1));// 1--> ipv4 addressLength =4// 2--> domain name addressLength=addressBuffer[1]// 3--> ipv6 addressLength =16const addressType = addressBuffer[0];let addressLength = 0;let addressValueIndex = addressIndex + 1;let addressValue = '';switch (addressType) {case 1:addressLength = 4;addressValue = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join('.');break;case 2:addressLength = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + 1))[0];addressValueIndex += 1;addressValue = new TextDecoder().decode(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength));break;case 3:addressLength = 16;const dataView = new DataView(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength));// 2001:0db8:85a3:0000:0000:8a2e:0370:7334const ipv6 = [];for (let i = 0; i < 8; i++) {ipv6.push(dataView.getUint16(i * 2).toString(16));}addressValue = ipv6.join(':');// seems no need add [] for ipv6break;default:return {hasError: true,message: `invild addressType is ${addressType}`,};}if (!addressValue) {return {hasError: true,message: `addressValue is empty, addressType is ${addressType}`,};}return {hasError: false,addressRemote: addressValue,addressType,portRemote,rawDataIndex: addressValueIndex + addressLength,vlessVersion: version,isUDP,};
}/*** * @param {import("@cloudflare/workers-types").Socket} remoteSocket * @param {import("@cloudflare/workers-types").WebSocket} webSocket * @param {ArrayBuffer} vlessResponseHeader * @param {(() => Promise<void>) | null} retry* @param {*} log */
async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {// remote--> wslet remoteChunkCount = 0;let chunks = [];/** @type {ArrayBuffer | null} */let vlessHeader = vlessResponseHeader;let hasIncomingData = false; // check if remoteSocket has incoming dataawait remoteSocket.readable.pipeTo(new WritableStream({start() {},/*** * @param {Uint8Array} chunk * @param {*} controller */async write(chunk, controller) {hasIncomingData = true;// remoteChunkCount++;if (webSocket.readyState !== WS_READY_STATE_OPEN) {controller.error('webSocket.readyState is not open, maybe close');}if (vlessHeader) {webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());vlessHeader = null;} else {// seems no need rate limit this, CF seems fix this??..// if (remoteChunkCount > 20000) {// // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M// await delay(1);// }webSocket.send(chunk);}},close() {log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);// safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway.},abort(reason) {console.error(`remoteConnection!.readable abort`, reason);},})).catch((error) => {console.error(`remoteSocketToWS has exception `,error.stack || error);safeCloseWebSocket(webSocket);});// seems is cf connect socket have error,// 1. Socket.closed will have error// 2. Socket.readable will be close without any data comingif (hasIncomingData === false && retry) {log(`retry`)retry();}
}/*** * @param {string} base64Str * @returns */
function base64ToArrayBuffer(base64Str) {if (!base64Str) {return { error: null };}try {// go use modified Base64 for URL rfc4648 which js atob not supportbase64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');const decode = atob(base64Str);const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));return { earlyData: arryBuffer.buffer, error: null };} catch (error) {return { error };}
}/*** This is not real UUID validation* @param {string} uuid */
function isValidUUID(uuid) {const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return uuidRegex.test(uuid);
}const WS_READY_STATE_OPEN = 1;
const WS_READY_STATE_CLOSING = 2;
/*** Normally, WebSocket will not has exceptions when close.* @param {import("@cloudflare/workers-types").WebSocket} socket*/
function safeCloseWebSocket(socket) {try {if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {socket.close();}} catch (error) {console.error('safeCloseWebSocket error', error);}
}const byteToHex = [];
for (let i = 0; i < 256; ++i) {byteToHex.push((i + 256).toString(16).slice(1));
}
function unsafeStringify(arr, offset = 0) {return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
function stringify(arr, offset = 0) {const uuid = unsafeStringify(arr, offset);if (!isValidUUID(uuid)) {throw TypeError("Stringified UUID is invalid");}return uuid;
}/*** * @param {import("@cloudflare/workers-types").WebSocket} webSocket * @param {ArrayBuffer} vlessResponseHeader * @param {(string)=> void} log */
async function handleUDPOutBound(webSocket, vlessResponseHeader, log) {let isVlessHeaderSent = false;const transformStream = new TransformStream({start(controller) {},transform(chunk, controller) {// udp message 2 byte is the the length of udp data// TODO: this should have bug, beacsue maybe udp chunk can be in two websocket messagefor (let index = 0; index < chunk.byteLength;) {const lengthBuffer = chunk.slice(index, index + 2);const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);const udpData = new Uint8Array(chunk.slice(index + 2, index + 2 + udpPakcetLength));index = index + 2 + udpPakcetLength;controller.enqueue(udpData);}},flush(controller) {}});// only handle dns udp for nowtransformStream.readable.pipeTo(new WritableStream({async write(chunk) {const resp = await fetch('https://1.1.1.1/dns-query',{method: 'POST',headers: {'content-type': 'application/dns-message',},body: chunk,})const dnsQueryResult = await resp.arrayBuffer();const udpSize = dnsQueryResult.byteLength;// console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);if (webSocket.readyState === WS_READY_STATE_OPEN) {log(`doh success and dns message length is ${udpSize}`);if (isVlessHeaderSent) {webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());} else {webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());isVlessHeaderSent = true;}}}})).catch((error) => {log('dns udp has error' + error)});const writer = transformStream.writable.getWriter();return {/*** * @param {Uint8Array} chunk */write(chunk) {writer.write(chunk);}};
}/*** * @param {string} userID * @param {string | null} hostName* @returns {string}*/
function getVLESSConfig(userID, hostName) {const vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`return `
################################################################
v2ray
---------------------------------------------------------------
${vlessMain}
---------------------------------------------------------------
################################################################
clash-meta
---------------------------------------------------------------
- type: vlessname: ${hostName}server: ${hostName}port: 443uuid: ${userID}network: wstls: trueudp: falsesni: ${hostName}client-fingerprint: chromews-opts:path: "/?ed=2048"headers:host: ${hostName}
---------------------------------------------------------------
################################################################
`;
}
代码文件workers.js同时存于GitHub
实例代码2、(进阶玩家使用)
提供给有技术能力的极客玩家和开发者们自行开发,支持伪装域名等等,借鉴:E家之长,零度解说。
// @ts-ignore
import { connect } from 'cloudflare:sockets';
@ts-ignore import { connect } from 'cloudflare:sockets';
// How to generate your own UUID:
// [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()"
let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4';
如何生成自己的 UUID: // [Windows] 按“Win + R”,输入 cmd 并运行: Powershell -NoExit -Command “[guid]::NewGuid()” let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4';
const proxyIPs = ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org', 'workers.cloudflare.cyou'];
const proxyIPs = ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org', 'workers.cloudflare.cyou'];
// if you want to use ipv6 or single proxyIP, please add comment at this line and remove comment at the next line
let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)];
// use single proxyIP instead of random
// let proxyIP = 'cdn.xn--b6gac.eu.org';
// ipv6 proxyIP example remove comment to use
// let proxyIP = "[2a01:4f8:c2c:123f:64:5:6810:c55a]"
如果要使用 ipv6 或单个 proxyIP,请在此行添加注释,并在下一行删除注释 let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)];use single proxyIP instead of random // let proxyIP = 'cdn.xn--b6gac.eu.org';ipv6 proxyIP 示例删除要使用的注释 // let proxyIP = “[2a01:4f8:c2c:123f:64:5:6810:c55a]”
let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query
让 dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg=';https://cloudflare-dns.com/dns-query 或 https://dns.google/dns-query
if (!isValidUUID(userID)) {throw new Error('uuid is invalid');
}
if (!isValidUUID(userID)) { throw new Error('uuid is invalid');
export default {/*** @param {import("@cloudflare/workers-types").Request} request* @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env* @param {import("@cloudflare/workers-types").ExecutionContext} ctx* @returns {Promise<Response>}*/async fetch(request, env, ctx) {// uuid_validator(request);try {userID = env.UUID || userID;proxyIP = env.PROXYIP || proxyIP;dohURL = env.DNS_RESOLVER_URL || dohURL;let userID_Path = userID;if (userID.includes(',')) {userID_Path = userID.split(',')[0];}const upgradeHeader = request.headers.get('Upgrade');if (!upgradeHeader || upgradeHeader !== 'websocket') {const url = new URL(request.url);switch (url.pathname) {case `/cf`: {return new Response(JSON.stringify(request.cf, null, 4), {status: 200,headers: {"Content-Type": "application/json;charset=utf-8",},});}case `/${userID_Path}`: {const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));return new Response(`${vlessConfig}`, {status: 200,headers: {"Content-Type": "text/html; charset=utf-8",}});};case `/sub/${userID_Path}`: {const url = new URL(request.url);const searchParams = url.searchParams;const vlessSubConfig = createVLESSSub(userID, request.headers.get('Host'));// Construct and return response objectreturn new Response(btoa(vlessSubConfig), {status: 200,headers: {"Content-Type": "text/plain;charset=utf-8",}});};case `/bestip/${userID_Path}`: {const headers = request.headers;const url = `https://sub.xf.free.hr/auto?host=${request.headers.get('Host')}&uuid=${userID}&path=/`;const bestSubConfig = await fetch(url, { headers: headers });return bestSubConfig;};default:// return new Response('Not found', { status: 404 });// For any other path, reverse proxy to 'ramdom website' and return the original response, caching it in the processconst randomHostname = cn_hostnames[Math.floor(Math.random() * cn_hostnames.length)];const newHeaders = new Headers(request.headers);newHeaders.set('cf-connecting-ip', '1.2.3.4');newHeaders.set('x-forwarded-for', '1.2.3.4');newHeaders.set('x-real-ip', '1.2.3.4');newHeaders.set('referer', 'https://www.google.com/search?q=edtunnel');// Use fetch to proxy the request to 15 different domainsconst proxyUrl = 'https://' + randomHostname + url.pathname + url.search;let modifiedRequest = new Request(proxyUrl, {method: request.method,headers: newHeaders,body: request.body,redirect: 'manual',});const proxyResponse = await fetch(modifiedRequest, { redirect: 'manual' });// Check for 302 or 301 redirect status and return an error responseif ([301, 302].includes(proxyResponse.status)) {return new Response(`Redirects to ${randomHostname} are not allowed.`, {status: 403,statusText: 'Forbidden',});}// Return the response from the proxy serverreturn proxyResponse;}} else {return await vlessOverWSHandler(request);}} catch (err) {/** @type {Error} */ let e = err;return new Response(e.toString());}},
};
导出默认值 { /** * @param {import(“@cloudflare/workers-types”)。request} request * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env * @param {import(“@cloudflare/workers-types”)。ExecutionContext} ctx * @returns {Promise<Response>} */ async fetch(request, env, ctx) { // uuid_validator(request); try { userID = env.UUID ||用户 ID;proxyIP = 环境。代理IP ||代理IP;dohURL = 环境。DNS_RESOLVER_URL ||dohURL;let userID_Path = userID;if (userID.includes(',')) { userID_Path = userID.split(',')[0]; } const upgradeHeader = request.headers.get('升级');if (!upgradeHeader || upgradeHeader !== 'websocket') { const url = new URL(request.url); switch (url.pathname) { case '/cf': { return new Response(JSON.stringify(request.cf, null, 4), { status: 200, headers: { “Content-Type”: “application/json;charset=utf-8“, }, });} case '/${userID_Path}': { const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); return new Response('${vlessConfig}', { status: 200, headers: { “Content-Type”: “text/html;charset=utf-8“, } });};case '/sub/${userID_Path}': { const url = new URL(request.url); const searchParams = url.searchParams; const vlessSubConfig = createVLESSSub(userID, request.headers.get('Host')); // 构造并返回响应对象 return new Response(btoa(vlessSubConfig), { status: 200, headers: { “Content-Type”: “text/plain;charset=utf-8“, } });};case '/bestip/${userID_Path}': { const headers = request.headers; const url = 'https://sub.xf.free.hr/auto?host=${request.headers.get('主机')}&uuid=${userID}&path=/';const bestSubConfig = await fetch(url, { headers: headers });返回 bestSubConfig;};default: // 返回 new Response('Not found', { status: 404 });对于任何其他路径,反向代理到“ramdom website”并返回原始响应,将其缓存在进程中 const randomHostname = cn_hostnames[Math.floor(Math.random() * cn_hostnames.length)];const newHeaders = 新标头(request.headers);newHeaders.set('cf-connecting-ip', '1.2.3.4');newHeaders.set('x-转发-for', '1.2.3.4');newHeaders.set('x-real-ip', '1.2.3.4');newHeaders.set('referer', 'https://www.google.com/search?q=edtunnel');使用 fetch 将请求代理到 15 个不同的域 const proxyUrl = 'https://' + randomHostname + url.pathname + url.search;let modifiedRequest = new Request(proxyUrl, { method: request.method, headers: newHeaders, body: request.body, redirect: 'manual', });const proxyResponse = await fetch(modifiedRequest, { redirect: 'manual' });检查 302 或 301 重定向状态并返回错误响应 if ([301, 302].includes(proxyResponse.status)) { return new Response('Redirects to ${randomHostname} are not allowed.', { status: 403, statusText: 'Forbidden', }); } // 从代理服务器返回响应 return proxyResponse;} } else { return await vlessOverWSHandler(request);catch (err) { /** @type {Error} */ let e = err; return new Response(e.toString());} }, };
export async function uuid_validator(request) {const hostname = request.headers.get('Host');const currentDate = new Date();
export async function uuid_validator(request) { const hostname = request.headers.get('Host'); const currentDate = new Date();const subdomain = hostname.split('.')[0];const year = currentDate.getFullYear();const month = String(currentDate.getMonth() + 1).padStart(2, '0');const day = String(currentDate.getDate()).padStart(2, '0');
常量子域 = hostname.split('.')[0];常量年份 = currentDate.getFullYear();常量月 = String(currentDate.getMonth() + 1).padStart(2, '0');const day = String(currentDate.getDate()).padStart(2, '0');const formattedDate = `${year}-${month}-${day}`;
const formattedDate = '${year}-${month}-${day}';// const daliy_sub = formattedDate + subdomainconst hashHex = await hashHex_f(subdomain);// subdomain string contains timestamps utc and uuid string TODO.console.log(hashHex, subdomain, formattedDate);
}
const daliy_sub = formattedDate + 子域 const hashHex = await hashHex_f(子域);子域字符串包含时间戳 UTC 和 UUID 字符串 TODO。console.log(hashHex, subdomain, formattedDate);}
export async function hashHex_f(string) {const encoder = new TextEncoder();const data = encoder.encode(string);const hashBuffer = await crypto.subtle.digest('SHA-256', data);const hashArray = Array.from(new Uint8Array(hashBuffer));const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');return hashHex;
}
export async function hashHex_f(string) { const encoder = new TextEncoder(); const data = encoder.encode(string); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join(''); return hashHex; }
/*** Handles VLESS over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the VLESS header.* @param {import("@cloudflare/workers-types").Request} request The incoming request object.* @returns {Promise<Response>} A Promise that resolves to a WebSocket response object.*/
async function vlessOverWSHandler(request) {const webSocketPair = new WebSocketPair();const [client, webSocket] = Object.values(webSocketPair);webSocket.accept();
/** * 通过创建 WebSocket 对、接受 WebSocket 连接并处理 VLESS 标头来处理 VLESS over WebSocket 请求。* @param {import(“@cloudflare/workers-types”)。Request} request 传入的请求对象。* @returns {Promise} 解<Response>析为 WebSocket 响应对象的 Promise。 */ 异步函数 vlessOverWSHandler(request) { const webSocketPair = new WebSocketPair(); const [client, webSocket] = Object.values(webSocketPair); webSocket.accept();let address = '';let portWithRandomLog = '';let currentDate = new Date();const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || '');};const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
let address = '';easy portWithRandomLog = '';let currentDate = 新日期();const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { console.log('[${currentDate} ${address}:${portWithRandomLog}] ${info}', event ||'');};const earlyDataHeader = request.headers.get('sec-websocket-protocol') ||'';const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);
const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);/** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/let remoteSocketWapper = {value: null,};let udpStreamWrite = null;let isDns = false;
/** @type {{ value: import(“@cloudflare/workers-types”)。插座 |null}}*/ let remoteSocketWapper = { value: null, };让 udpStreamWrite = null;let isDns = false;// ws --> remotereadableWebSocketStream.pipeTo(new WritableStream({async write(chunk, controller) {if (isDns && udpStreamWrite) {return udpStreamWrite(chunk);}if (remoteSocketWapper.value) {const writer = remoteSocketWapper.value.writable.getWriter()await writer.write(chunk);writer.releaseLock();return;}
ws --> remote readableWebSocketStream.pipeTo(new WritableStream({ async write(chunk, controller) { if (isDns &&; udpStreamWrite) { return udpStreamWrite(chunk); } if (remoteSocketWapper.value) { const writer = remoteSocketWapper.value.writable.getWriter() await writer.write(chunk); writer.releaseLock(); return; }const {hasError,message,portRemote = 443,addressRemote = '',rawDataIndex,vlessVersion = new Uint8Array([0, 0]),isUDP,} = processVlessHeader(chunk, userID);address = addressRemote;portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `;if (hasError) {// controller.error(message);throw new Error(message); // cf seems has bug, controller.error will not end stream}
const { hasError, message, portRemote = 443, addressRemote = '', rawDataIndex, vlessVersion = new Uint8Array([0, 0]), isUDP, } = processVlessHeader(chunk, userID);地址 = addressRemote;portWithRandomLog = '${portRemote} ${isUDP ?'udp' : 'tcp'} ';if (hasError) { // controller.error(message); throw new Error(message); // cf 似乎有 bug,controller.error 不会结束流 }// If UDP and not DNS port, close itif (isUDP && portRemote !== 53) {throw new Error('UDP proxy only enabled for DNS which is port 53');// cf seems has bug, controller.error will not end stream}
如果 UDP 而不是 DNS 端口,请关闭它 if (isUDP && portRemote !== 53) { throw new Error('UDP proxy only enabled for DNS which is port 53'); // cf 似乎有 bug,controller.error 不会结束流 }if (isUDP && portRemote === 53) {isDns = true;}
if (isUDP && portRemote === 53) { isDns = true;// ["version", "附加信息长度 N"]const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);const rawClientData = chunk.slice(rawDataIndex);
[“version”, “附加信息长度 N”] const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);const rawClientData = chunk.slice(rawDataIndex);// TODO: support udp here when cf runtime has udp supportif (isDns) {const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log);udpStreamWrite = write;udpStreamWrite(rawClientData);return;}handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);},close() {log(`readableWebSocketStream is close`);},abort(reason) {log(`readableWebSocketStream is abort`, JSON.stringify(reason));},})).catch((err) => {log('readableWebSocketStream pipeTo error', err);});
TODO:当 cf 运行时支持 udp 时,这里支持 udp if (isDns) { const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); udpStreamWrite = write; udpStreamWrite(rawClientData); return; } handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log);}, close() { log('readableWebSocketStream is close'); }, abort(reason) { log('readableWebSocketStream is abort', JSON.stringify(reason)); }, })).catch((err) => { log('readableWebSocketStream pipeTo error', err);return new Response(null, {status: 101,webSocket: client,});
}
return new Response(null, { status: 101, webSocket: client, });}
/*** Handles outbound TCP connections.** @param {any} remoteSocket * @param {string} addressRemote The remote address to connect to.* @param {number} portRemote The remote port to connect to.* @param {Uint8Array} rawClientData The raw client data to write.* @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.* @param {Uint8Array} vlessResponseHeader The VLESS response header.* @param {function} log The logging function.* @returns {Promise<void>} The remote socket.*/
async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {
/** * 处理出站 TCP 连接。* * @param {any} remoteSocket * @param {string} addressRemote 要连接到的远程地址。* @param {number} portRemote 要连接到的远程端口。* @param {Uint8Array} rawClientData 要写入的原始客户端数据。* @param {import(“@cloudflare/workers-types”)。WebSocket} webSocket 要将远程套接字传递到的 WebSocket。* @param {Uint8Array} vlessResponseHeader VLESS 响应标头。* @param {function} log 日志记录函数。* @returns {Promise<void>} 远程套接字。 */ 异步函数 handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) {/*** Connects to a given address and port and writes data to the socket.* @param {string} address The address to connect to.* @param {number} port The port to connect to.* @returns {Promise<import("@cloudflare/workers-types").Socket>} A Promise that resolves to the connected socket.*/async function connectAndWrite(address, port) {/** @type {import("@cloudflare/workers-types").Socket} */const tcpSocket = connect({hostname: address,port: port,});remoteSocket.value = tcpSocket;log(`connected to ${address}:${port}`);const writer = tcpSocket.writable.getWriter();await writer.write(rawClientData); // first write, nomal is tls client hellowriter.releaseLock();return tcpSocket;}
/** * 连接到给定的地址和端口,并将数据写入套接字。* @param {string} address 要连接到的地址。* @param {number} port 要连接到的端口。* @returns {Promise<import(“@cloudflare/workers-types”)。Socket>} 解析为连接的套接字的 Promise。*/ 异步函数 connectAndWrite(address, port) { /** @type {import(“@cloudflare/workers-types”)。套接字} */ const tcpSocket = connect({ hostname: address, port: port, });remoteSocket.value = tcpSocket;log('连接到${address}:${port}');常量写入器 = tcpSocket.writable.getWriter();等待 writer.write(rawClientData);首先写入,nomal 是 tls 客户端 hello writer.releaseLock();返回 tcpSocket;}/*** Retries connecting to the remote address and port if the Cloudflare socket has no incoming data.* @returns {Promise<void>} A Promise that resolves when the retry is complete.*/async function retry() {const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote)tcpSocket.closed.catch(error => {console.log('retry tcpSocket closed error', error);}).finally(() => {safeCloseWebSocket(webSocket);})remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);}
/** * 如果 Cloudflare 套接字没有传入数据,则重试连接到远程地址和端口。* @returns {Promise<void>} 重试完成后解析的 Promise。 */ 异步函数 retry() { const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) tcpSocket.closed.catch(error => { console.log('retry tcpSocket closed error', error);finally(() => { safeCloseWebSocket(webSocket); remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);}const tcpSocket = await connectAndWrite(addressRemote, portRemote);
const tcpSocket = await connectAndWrite(addressRemote, portRemote);// when remoteSocket is ready, pass to websocket// remote--> wsremoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
}
当 remoteSocket 准备就绪时,传递到 websocket // remote--> ws remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);}
/*** Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket.* @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from.* @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT.* @param {(info: string)=> void} log The logging function.* @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket.*/
function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {let readableStreamCancel = false;const stream = new ReadableStream({start(controller) {webSocketServer.addEventListener('message', (event) => {const message = event.data;controller.enqueue(message);});
/** * 从 WebSocket 服务器创建可读流,允许从 WebSocket 读取数据。* @param {import(“@cloudflare/workers-types”)。WebSocket} webSocketServer 要从中创建可读流的 WebSocket 服务器。* @param {string} earlyDataHeader 包含 WebSocket 0-RTT 早期数据的标头。* @param {(info: string)=> void} log 日志函数。* @returns {ReadableStream} 可用于从 WebSocket 读取数据的可读流。*/ function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { let readableStreamCancel = false; const stream = new ReadableStream({ start(controller) { webSocketServer.addEventListener('message', (event) => { const message = event.data; controller.enqueue(message);webSocketServer.addEventListener('close', () => {safeCloseWebSocket(webSocketServer);controller.close();});
webSocketServer.addEventListener('close', () => { safeCloseWebSocket(webSocketServer);webSocketServer.addEventListener('error', (err) => {log('webSocketServer has error');controller.error(err);});const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);if (error) {controller.error(error);} else if (earlyData) {controller.enqueue(earlyData);}},
webSocketServer.addEventListener('error', (err) => { log('webSocketServer has error'); controller.error(err);const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);if (error) { controller.error(error); } else if (earlyData) { controller.enqueue(earlyData); } },pull(controller) {// if ws can stop read if stream is full, we can implement backpressure// https://streams.spec.whatwg.org/#example-rs-push-backpressure},
pull(controller) { // 如果 ws 可以在流已满时停止读取,我们可以实现背压 // https://streams.spec.whatwg.org/#example-rs-push-backpressure },cancel(reason) {log(`ReadableStream was canceled, due to ${reason}`)readableStreamCancel = true;safeCloseWebSocket(webSocketServer);}});
cancel(reason) { log('由于 ${reason}',ReadableStream 已取消 = true; safeCloseWebSocket(webSocketServer);return stream;
} 回流;}
// https://xtls.github.io/development/protocols/vless.html
// https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw
https://xtls.github.io/development/protocols/vless.html // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw
/*** Processes the VLESS header buffer and returns an object with the relevant information.* @param {ArrayBuffer} vlessBuffer The VLESS header buffer to process.* @param {string} userID The user ID to validate against the UUID in the VLESS header.* @returns {{* hasError: boolean,* message?: string,* addressRemote?: string,* addressType?: number,* portRemote?: number,* rawDataIndex?: number,* vlessVersion?: Uint8Array,* isUDP?: boolean* }} An object with the relevant information extracted from the VLESS header buffer.*/
function processVlessHeader(vlessBuffer, userID) {if (vlessBuffer.byteLength < 24) {return {hasError: true,message: 'invalid data',};}
/** * 处理 VLESS 标头缓冲区并返回包含相关信息的对象。* @param {ArrayBuffer} vlessBuffer 要处理的 VLESS 标头缓冲区。* @param {string} userID 要根据 VLESS 标头中的 UUID 进行验证的用户 ID。* @returns {{ * hasError: boolean, * message?: string, * addressRemote?: string, * addressType?: number, * portRemote?: number, * rawDataIndex?: number, * vlessVersion?: Uint8Array, * isUDP?: boolean * }} 包含从 VLESS 标头缓冲区提取的相关信息的对象。*/ 函数 processVlessHeader(vlessBuffer, userID) { if (vlessBuffer.byteLength < 24) { return { hasError: true, message: 'invalid data', }; }const version = new Uint8Array(vlessBuffer.slice(0, 1));let isValidUser = false;let isUDP = false;const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17));const slicedBufferString = stringify(slicedBuffer);// check if userID is valid uuid or uuids split by , and contains userID in it otherwise return error message to consoleconst uuids = userID.includes(',') ? userID.split(",") : [userID];// uuid_validator(hostName, slicedBufferString);
const version = new Uint8Array(vlessBuffer.slice(0, 1));let isValidUser = false;let isUDP = false;const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17));const slicedBufferString = stringify(slicedBuffer);检查 userID 是否有效 uuid 或 uuids 拆分为 ,并且其中包含 userID,否则返回错误消息到控制台 const uuids = userID.includes(',') ?userID.split(“,”) : [用户ID];uuid_validator(主机名,slicedBufferString);// isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim());isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim();
isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim());isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) ||uuids.length === 1 && slicedBufferString === uuids[0].trim();console.log(`userID: ${slicedBufferString}`);
console.log('用户 ID: ${slicedBufferString}');if (!isValidUser) {return {hasError: true,message: 'invalid user',};}
if (!isValidUser) { return { hasError: true, message: 'invalid user', };const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];//skip opt for now
const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];立即跳过选择const command = new Uint8Array(vlessBuffer.slice(18 + optLength, 18 + optLength + 1))[0];
const 命令 = new Uint8Array( vlessBuffer.slice(18 + optLength, 18 + optLength + 1) )[0];// 0x01 TCP// 0x02 UDP// 0x03 MUXif (command === 1) {isUDP = false;} else if (command === 2) {isUDP = true;} else {return {hasError: true,message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,};}const portIndex = 18 + optLength + 1;const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);// port is big-Endian in raw data etc 80 == 0x005dconst portRemote = new DataView(portBuffer).getUint16(0);
0x01 TCP // 0x02 UDP // 0x03 MUX if (command === 1) { isUDP = false; } else if (command === 2) { isUDP = true; } else { return { hasError: true, message: 'command ${command} is not support, command 01-tcp,02-udp,03-mux', }; } const portIndex = 18 + optLength + 1;const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);port 在原始数据中是 big-Endian 等 80 == 0x005d const portRemote = new DataView(portBuffer).getUint16(0);let addressIndex = portIndex + 2;const addressBuffer = new Uint8Array(vlessBuffer.slice(addressIndex, addressIndex + 1));
let addressIndex = 端口索引 + 2;const addressBuffer = new Uint8Array( vlessBuffer.slice(addressIndex, addressIndex + 1) );// 1--> ipv4 addressLength =4// 2--> domain name addressLength=addressBuffer[1]// 3--> ipv6 addressLength =16const addressType = addressBuffer[0];let addressLength = 0;let addressValueIndex = addressIndex + 1;let addressValue = '';switch (addressType) {case 1:addressLength = 4;addressValue = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join('.');break;case 2:addressLength = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + 1))[0];addressValueIndex += 1;addressValue = new TextDecoder().decode(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength));break;case 3:addressLength = 16;const dataView = new DataView(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength));// 2001:0db8:85a3:0000:0000:8a2e:0370:7334const ipv6 = [];for (let i = 0; i < 8; i++) {ipv6.push(dataView.getUint16(i * 2).toString(16));}addressValue = ipv6.join(':');// seems no need add [] for ipv6break;default:return {hasError: true,message: `invild addressType is ${addressType}`,};}if (!addressValue) {return {hasError: true,message: `addressValue is empty, addressType is ${addressType}`,};}
1--> ipv4 addressLength =4 // 2--> 域名 addressLength=addressBuffer[1] // 3--> ipv6 addressLength =16 const addressType = addressBuffer[0];let addressLength = 0;让 addressValueIndex = addressIndex + 1;let addressValue = '';switch (addressType) { case 1: addressLength = 4; addressValue = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ).join('.'); break; case 2: addressLength = new Uint8Array( vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) )[0]; addressValueIndex += 1; addressValue = new TextDecoder().decode( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) ); break; case 3: addressLength = 16; const dataView = new DataView( vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) );2001:0db8:85a3:0000:0000:8a2e:0370:7334 const IPv6 = [];for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)); } addressValue = ipv6.join(':');似乎不需要为 IPv6 中断添加 [];默认值: return { hasError: true, message: 'invild addressType is ${addressType}', };} if (!addressValue) { return { hasError: true, message: 'addressValue is empty, addressType is ${addressType}', }; }return {hasError: false,addressRemote: addressValue,addressType,portRemote,rawDataIndex: addressValueIndex + addressLength,vlessVersion: version,isUDP,};
}
return { hasError: false, addressRemote: addressValue, addressType, portRemote, rawDataIndex: addressValueIndex + addressLength, vlessVersion: version, isUDP, };}/*** Converts a remote socket to a WebSocket connection.* @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert.* @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to.* @param {ArrayBuffer | null} vlessResponseHeader The VLESS response header.* @param {(() => Promise<void>) | null} retry The function to retry the connection if it fails.* @param {(info: string) => void} log The logging function.* @returns {Promise<void>} A Promise that resolves when the conversion is complete.*/
async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {// remote--> wslet remoteChunkCount = 0;let chunks = [];/** @type {ArrayBuffer | null} */let vlessHeader = vlessResponseHeader;let hasIncomingData = false; // check if remoteSocket has incoming dataawait remoteSocket.readable.pipeTo(new WritableStream({start() {},/*** * @param {Uint8Array} chunk * @param {*} controller */async write(chunk, controller) {hasIncomingData = true;remoteChunkCount++;if (webSocket.readyState !== WS_READY_STATE_OPEN) {controller.error('webSocket.readyState is not open, maybe close');}if (vlessHeader) {webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());vlessHeader = null;} else {// console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`);// seems no need rate limit this, CF seems fix this??..// if (remoteChunkCount > 20000) {// // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M// await delay(1);// }webSocket.send(chunk);}},close() {log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);// safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway.},abort(reason) {console.error(`remoteConnection!.readable abort`, reason);},})).catch((error) => {console.error(`remoteSocketToWS has exception `,error.stack || error);safeCloseWebSocket(webSocket);});/** * 将远程套接字转换为 WebSocket 连接。* @param {import(“@cloudflare/workers-types”)。Socket} remoteSocket 要转换的远程套接字。* @param {import(“@cloudflare/workers-types”)。WebSocket} webSocket 要连接到的 WebSocket。* @param {ArrayBuffer | null} vlessResponseHeader VLESS 响应标头。* @param {(() => Promise<void>) | null} retry 如果连接失败,则重试连接的函数。 * @param {(info: string) => void} log 日志记录函数。 * @returns {Promise<void>} 转换完成后解析的 Promise。 */ 异步函数 remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { // remote--> ws let remoteChunkCount = 0; let chunks = []; /** @type {ArrayBuffer | null} */ let vlessHeader = vlessResponseHeader; let hasIncomingData = false; // 检查 remoteSocket 是否有传入数据 await remoteSocket.readable .pipeTo( new WritableStream({ start() { }, /** * * @param {Uint8Array} chunk * @param {*} controller */ async write(chunk, controller) { hasIncomingData = true; remoteChunkCount++; if (webSocket.readyState !== WS_READY_STATE_OPEN) { controller.error( 'webSocket.readyState is not open, may close' ); } if (vlessHeader) { webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); vlessHeader = null; } else { // 控制台。log('remoteSocketToWS 发送块 ${chunk.byteLength}');似乎不需要速率限制这个,CF似乎解决了这个问题??..if (remoteChunkCount > 20000) { // // cf 一个包是 4096 byte(4kb), 4096 * 20000 = 80M // await delay(1); // } webSocket.send(chunk);} }, close() { log('remoteConnection!.readable 接近,hasIncomingData 为 ${hasIncomingData}');safeCloseWebSocket(webSocket);在某些情况下,不需要服务器关闭 websocket 首先会导致 HTTP ERR_CONTENT_LENGTH_MISMATCH问题,客户端无论如何都会发送关闭事件。}, abort(reason) { console.error('remoteConnection!.可读中止',原因);}, }) ) .catch((error) => { console.error( 'remoteSocketToWS has exception ', error.堆栈 ||错误);safeCloseWebSocket(webSocket);});// seems is cf connect socket have error,// 1. Socket.closed will have error// 2. Socket.readable will be close without any data comingif (hasIncomingData === false && retry) {log(`retry`)retry();}
}
似乎是 cf 连接套接字有错误, // 1.Socket.closed 将出现错误 // 2.Socket.readable 将关闭,没有任何数据传入,如果 (hasIncomingData === false &&; retry) { log('retry') retry();
/*** Decodes a base64 string into an ArrayBuffer.* @param {string} base64Str The base64 string to decode.* @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error.*/
function base64ToArrayBuffer(base64Str) {if (!base64Str) {return { earlyData: null, error: null };}try {// go use modified Base64 for URL rfc4648 which js atob not supportbase64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');const decode = atob(base64Str);const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));return { earlyData: arryBuffer.buffer, error: null };} catch (error) {return { earlyData: null, error };}
}
/** * 将 base64 字符串解码为 ArrayBuffer。* @param {string} base64Str 要解码的 base64 字符串。* @returns {{earlyData: ArrayBuffer|null, error: Error|null}} 包含解码的 ArrayBuffer 或 null(如果有错误)的对象,以及解码期间发生的任何错误,如果没有错误,则包含 null。*/ function base64ToArrayBuffer(base64Str) { if (!base64Str) { return { earlyData: null, error: null }; } try { // go use modified Base64 for URL rfc4648 which js atob not support base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); const decode = atob(base64Str); const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); return { earlyData: arryBuffer.buffer, error: null }; } catch (error) { return { earlyData: null, error }; } }
/*** Checks if a given string is a valid UUID.* Note: This is not a real UUID validation.* @param {string} uuid The string to validate as a UUID.* @returns {boolean} True if the string is a valid UUID, false otherwise.*/
function isValidUUID(uuid) {const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;return uuidRegex.test(uuid);
}
/** * 检查给定字符串是否为有效的 UUID。* 注意:这不是真正的 UUID 验证。* @param {string} uuid 要验证为 UUID 的字符串。* @returns {boolean} 如果字符串是有效的 UUID,则为 true,否则为 false。*/ function isValidUUID(uuid) { const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; return uuidRegex.test(uuid); }
const WS_READY_STATE_OPEN = 1;
const WS_READY_STATE_CLOSING = 2;
/*** Closes a WebSocket connection safely without throwing exceptions.* @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close.*/
function safeCloseWebSocket(socket) {try {if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {socket.close();}} catch (error) {console.error('safeCloseWebSocket error', error);}
}
常量 WS_READY_STATE_OPEN = 1;常量 WS_READY_STATE_CLOSING = 2;/** * 安全地关闭 WebSocket 连接,而不会引发异常。* @param {import(“@cloudflare/workers-types”)。WebSocket} socket 要关闭的 WebSocket 连接。*/ function safeCloseWebSocket(socket) { try { if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { socket.close(); } } catch (error) { console.error('safeCloseWebSocket error', error);
const byteToHex = []; 常量字节ToHex = [];
for (let i = 0; i < 256; ++i) {byteToHex.push((i + 256).toString(16).slice(1));
}
for (let i = 0; i < 256; ++i) { byteToHex.push((i + 256).toString(16).slice(1));
function unsafeStringify(arr, offset = 0) {return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
}
函数 unsafeStringify(arr, offset = 0) { return(byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + “-” + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + “-” + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + “-” + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + “-” + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] +byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();}
function stringify(arr, offset = 0) {const uuid = unsafeStringify(arr, offset);if (!isValidUUID(uuid)) {throw TypeError("Stringified UUID is invalid");}return uuid;
}
function stringify(arr, offset = 0) { const uuid = unsafeStringify(arr, offset); if (!isValidUUID(uuid)) { throw TypeError(“Stringified UUID is invalid”); } return uuid; }/*** Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection.* @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over.* @param {ArrayBuffer} vlessResponseHeader The VLESS response header.* @param {(string) => void} log The logging function.* @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream.*/
async function handleUDPOutBound(webSocket, vlessResponseHeader, log) {
/** * 通过将数据转换为 DNS 查询并通过 WebSocket 连接发送来处理出站 UDP 流量。* @param {import(“@cloudflare/workers-types”)。WebSocket} webSocket 用于发送 DNS 查询的 WebSocket 连接。* @param {ArrayBuffer} vlessResponseHeader VLESS 响应标头。* @param {(string) => void} log 日志函数。* @returns {{write: (chunk: Uint8Array) => void}} 一个对象,其写入方法接受 Uint8Array 块以写入转换流。*/ 异步函数 handleUDPOutBound(webSocket, vlessResponseHeader, log) {let isVlessHeaderSent = false;const transformStream = new TransformStream({start(controller) {
let isVlessHeaderSent = false;const transformStream = new TransformStream({ start(controller) {},transform(chunk, controller) {// udp message 2 byte is the the length of udp data// TODO: this should have bug, beacsue maybe udp chunk can be in two websocket messagefor (let index = 0; index < chunk.byteLength;) {const lengthBuffer = chunk.slice(index, index + 2);const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);const udpData = new Uint8Array(chunk.slice(index + 2, index + 2 + udpPakcetLength));index = index + 2 + udpPakcetLength;controller.enqueue(udpData);}},flush(controller) {}});
}, transform(chunk, controller) { // udp message 2 byte 是 udp 数据的长度 // TODO: 这应该有 bug, beacsue 也许 udp 块可以在两个 websocket 消息中 for (let index = 0; index < chunk.byteLength;){ const lengthBuffer = chunk.slice(index, index + 2); const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); const udpData = new Uint8Array( chunk.slice(index + 2, index + 2 + udpPakcetLength) ); index = index + 2 + udpPakcetLength; controller.enqueue(udpData); }}, flush(controller) { } });// only handle dns udp for nowtransformStream.readable.pipeTo(new WritableStream({async write(chunk) {const resp = await fetch(dohURL, // dns server url{method: 'POST',headers: {'content-type': 'application/dns-message',},body: chunk,})const dnsQueryResult = await resp.arrayBuffer();const udpSize = dnsQueryResult.byteLength;// console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);if (webSocket.readyState === WS_READY_STATE_OPEN) {log(`doh success and dns message length is ${udpSize}`);if (isVlessHeaderSent) {webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());} else {webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());isVlessHeaderSent = true;}}}})).catch((error) => {log('dns udp has error' + error)});
现在只处理 dns udp transformStream.readable.pipeTo(new WritableStream({ async write(chunk) { const resp = await fetch(dohURL, // dns server url { method: 'POST', headers: { 'content-type': 'application/dns-message', }, body: chunk, }) const dnsQueryResult = await resp.arrayBuffer(); const udpSize = dnsQueryResult.byteLength; // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);if (webSocket.readyState === WS_READY_STATE_OPEN) { log('doh 成功且 dns 消息长度为 ${udpSize}'); if (isVlessHeaderSent) { webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); } else { webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); isVlessHeaderSent = true;})).catch((error) => { log('dns udp has error' + error) });const writer = transformStream.writable.getWriter();
常量写入器 = transformStream.writable.getWriter();return {/*** * @param {Uint8Array} chunk */write(chunk) {writer.write(chunk);}};
}
return { /** * * @param {Uint8Array} 块 */ write(chunk) { writer.write(chunk);}
/**** @param {string} userID - single or comma separated userIDs* @param {string | null} hostName* @returns {string}*/
function getVLESSConfig(userIDs, hostName) {const commonUrlPart = `:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`;const hashSeparator = "################################################################";
/** * * @param {string} userID - 单个或逗号分隔的 userID * @param {string | null} hostName * @returns {string} */ function getVLESSConfig(userIDs, hostName) { const commonUrlPart = ':443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}'; const hashSeparator = “################################################################”;// Split the userIDs into an arrayconst userIDArray = userIDs.split(",");
将 userID 拆分为一个数组 const userIDArray = userIDs.split(“,”);// Prepare output string for each userIDconst output = userIDArray.map((userID) => {const vlessMain = `vless://${userID}@${hostName}${commonUrlPart}`;const vlessSec = `vless://${userID}@${proxyIP}${commonUrlPart}`;return `<h2>UUID: ${userID}</h2>${hashSeparator}\nv2ray default ip
---------------------------------------------------------------
${vlessMain}
<button onclick='copyToClipboard("${vlessMain}")'><i class="fa fa-clipboard"></i> Copy vlessMain</button>
---------------------------------------------------------------
v2ray with bestip
---------------------------------------------------------------
${vlessSec}
<button onclick='copyToClipboard("${vlessSec}")'><i class="fa fa-clipboard"></i> Copy vlessSec</button>
---------------------------------------------------------------`;}).join('\n');const sublink = `https://${hostName}/sub/${userIDArray[0]}?format=clash`const subbestip = `https://${hostName}/bestip/${userIDArray[0]}`;const clash_link = `https://api.v1.mk/sub?target=clash&url=${encodeURIComponent(sublink)}&insert=false&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;// Prepare header stringconst header = `
<p align='center'><img src='https://cloudflare-ipfs.com/ipfs/bafybeigd6i5aavwpr6wvnwuyayklq3omonggta4x2q7kpmgafj357nkcky' alt='图片描述' style='margin-bottom: -50px;'>
<b style='font-size: 15px;'>Welcome! This function generates configuration for VLESS protocol. If you found this useful, please check our GitHub project for more:</b>
<b style='font-size: 15px;'>欢迎!这是生成 VLESS 协议的配置。如果您发现这个项目很好用,请查看我们的 GitHub 项目给我一个star:</b>
<a href='https://github.com/3Kmfi6HP/EDtunnel' target='_blank'>EDtunnel - https://github.com/3Kmfi6HP/EDtunnel</a>
<iframe src='https://ghbtns.com/github-btn.html?user=USERNAME&repo=REPOSITORY&type=star&count=true&size=large' frameborder='0' scrolling='0' width='170' height='30' title='GitHub'></iframe>
<a href='//${hostName}/sub/${userIDArray[0]}' target='_blank'>VLESS 节点订阅连接</a>
<a href='clash://install-config?url=${encodeURIComponent(`https://${hostName}/sub/${userIDArray[0]}?format=clash`)}}' target='_blank'>Clash for Windows 节点订阅连接</a>
<a href='${clash_link}' target='_blank'>Clash 节点订阅连接</a>
<a href='${subbestip}' target='_blank'>优选IP自动节点订阅</a>
<a href='clash://install-config?url=${encodeURIComponent(subbestip)}' target='_blank'>Clash优选IP自动</a>
<a href='sing-box://import-remote-profile?url=${encodeURIComponent(subbestip)}' target='_blank'>singbox优选IP自动</a>
<a href='sn://subscription?url=${encodeURIComponent(subbestip)}' target='_blank'>nekobox优选IP自动</a>
<a href='v2rayng://install-config?url=${encodeURIComponent(subbestip)}' target='_blank'>v2rayNG优选IP自动</a></p>`;为每个用户 ID 准备输出字符串 const output = userIDArray.map((userID) => { const vlessMain = 'vless://${userID}@${hostName}${commonUrlPart}'; const vlessSec = 'vless://${userID}@${proxyIP}${commonUrlPart}'; return '<h2>UUID: ${userID}</h2>${hashSeparator}\nv2ray default ip --------------------------------------------------------------- ${vlessMain} <button onclick='copyToClipboard(“${vlessMain}”)'><i class=“fa fa-clipboard”></i> 复制 vlessMain</button>--------------------------------------------------------------- v2ray with bestip --------------------------------------------------------------- ${vlessSec} <button onclick='copyToClipboard(“${vlessSec}”)'><i class=“fa fa-clipboard”></i> Copy vlessSec</button> ---------------------------------------------------------------'; })。联接('\n');const sublink = 'https://${hostName}/sub/${userIDArray[0]}?format=clash' const subbestip = 'https://${hostName}/bestip/${userIDArray[0]}';const clash_link = 'https://api.v1.mk/sub?target=clash&url=${encodeURIComponent(sublink)}&insert=false&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true';准备头文件字符串 const header = ' <p align='center'><img src='https://cloudflare-ipfs.com/ipfs/bafybeigd6i5aavwpr6wvnwuyayklq3omonggta4x2q7kpmgafj357nkcky' alt='图片描述' style='margin-bottom: -50px;'> <b style='font-size: 15px;'>欢迎!此函数生成 VLESS 协议的配置。如果您觉得这很有用,请查看我们的 GitHub 项目了解更多信息:</b> <b style='font-size: 15px;'>欢迎!这是生成 VLESS 协议的配置。如果您发现这个项目很好用,请查看我们的 GitHub 项目给我一个星:</b> <a href='https://github.com/3Kmfi6HP/EDtunnel' target='_blank'>EDtunnel - https://github.com/3Kmfi6HP/EDtunnel</a> <iframe src='https://ghbtns.com/github-btn.html?user=USERNAME&repo=REPOSITORY&type=star&count=true&size=large' frameborder='0' scrolling='0' width='170' height='30' title='GitHub'></iframe> <a href='//${hostName}/sub/${userIDArray[0]}' target='_blank'>VLESS 节点订阅连接</a> <a href='clash://install-config?url=${encodeURIComponent('https://${hostName}/sub/${userIDArray[0]}?format=clash')}}' target='_blank'>Clash for Windows 节点订阅连接</a> <a href='${clash_link}' target='_blank'>Clash 节点订阅连接</a><a href='${subbestip}' target='_blank'>优选IP自动节点订阅</a> <a href='clash://install-config?url=${encodeURIComponent(subbestip)}' target='_blank'>Clash优选IP自动</a> <a href='sing-box://import-remote-profile?url=${encodeURIComponent(subbestip)}' target='_blank'>singbox优选IP自动</a> <a href='sn://subscription?url=${encodeURIComponent(subbestip)}' target='_blank'>nekobox优选IP自动</a> <a href='v2rayng://install-config?url=${encodeURIComponent(subbestip)}' target='_blank'>v2rayNG优选IP自动</a></p>';// HTML Head with CSS and FontAwesome libraryconst htmlHead = `<head><title>EDtunnel: VLESS configuration</title><meta name='description' content='This is a tool for generating VLESS protocol configurations. Give us a star on GitHub https://github.com/3Kmfi6HP/EDtunnel if you found it useful!'><meta name='keywords' content='EDtunnel, cloudflare pages, cloudflare worker, severless'><meta name='viewport' content='width=device-width, initial-scale=1'><meta property='og:site_name' content='EDtunnel: VLESS configuration' /><meta property='og:type' content='website' /><meta property='og:title' content='EDtunnel - VLESS configuration and subscribe output' /><meta property='og:description' content='Use cloudflare pages and worker severless to implement vless protocol' /><meta property='og:url' content='https://${hostName}/' /><meta property='og:image' content='https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${encodeURIComponent(`vless://${userIDs.split(",")[0]}@${hostName}${commonUrlPart}`)}' /><meta name='twitter:card' content='summary_large_image' /><meta name='twitter:title' content='EDtunnel - VLESS configuration and subscribe output' /><meta name='twitter:description' content='Use cloudflare pages and worker severless to implement vless protocol' /><meta name='twitter:url' content='https://${hostName}/' /><meta name='twitter:image' content='https://cloudflare-ipfs.com/ipfs/bafybeigd6i5aavwpr6wvnwuyayklq3omonggta4x2q7kpmgafj357nkcky' /><meta property='og:image:width' content='1500' /><meta property='og:image:height' content='1500' />
带有 CSS 和 FontAwesome 库的 HTML Head const htmlHead = ' <head> <title>EDtunnel: VLESS 配置</title> <meta name='description' content='这是一个用于生成 VLESS 协议配置的工具。 如果您觉得有用,请在 GitHub 上给我们一颗星 https://github.com/3Kmfi6HP/EDtunnel!'> <meta name='keywords' content='EDtunnel, cloudflare pages, cloudflare worker, severless'> <meta name='viewport' content='width=device-width, initial-scale=1'><meta property='og:site_name' content='EDtunnel: VLESS 配置' /> <meta property='og:type' content='website' /> <meta property='og:title' content='EDtunnel - VLESS 配置和订阅输出' /> <meta property='og:description' content='使用 cloudflare pages 和 worker severless 实现 vless 协议' /> <meta property='og:url' content='https://${hostName}/' /><meta property='og:image' content='https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${encodeURIComponent('vless://${userIDs.split(“,”)[0]}@${hostName}${commonUrlPart}')}' /> <meta name='twitter:card' content='summary_large_image' /> <meta name='twitter:title' content='EDtunnel - VLESS 配置和订阅输出' /> <meta name='twitter:description' content='使用 cloudflare pages 和 worker severless 实现 vless 协议' /><meta name='twitter:url' content='https://${hostName}/' /> <meta name='twitter:image' content='https://cloudflare-ipfs.com/ipfs/bafybeigd6i5aavwpr6wvnwuyayklq3omonggta4x2q7kpmgafj357nkcky' /> <meta property='og:image:width' content='1500' /> <meta property='og:image:height' content='1500' /><style>body {font-family: Arial, sans-serif;background-color: #f0f0f0;color: #333;padding: 10px;}
<style> body { font-family: Arial, sans-serif; background-color: #f0f0f0; color: #333; padding: 10px; }a {color: #1a0dab;text-decoration: none;}img {max-width: 100%;height: auto;}
a { color: #1a0dab; text-decoration: none; } img { max-width: 100%; height: auto; }pre {white-space: pre-wrap;word-wrap: break-word;background-color: #fff;border: 1px solid #ddd;padding: 15px;margin: 10px 0;}/* Dark mode */@media (prefers-color-scheme: dark) {body {background-color: #333;color: #f0f0f0;}
pre { white-space: pre-wrap; word-wrap: break-word; background-color: #fff; border: 1px solid #ddd; padding: 15px; margin: 10px 0; } /* 深色模式 */ @media (prefers-color-scheme: dark) { body { background-color: #333; color: #f0f0f0; }a {color: #9db4ff;}
a { 颜色: #9db4ff;pre {background-color: #282a36;border-color: #6272a4;}}</style><!-- Add FontAwesome library --><link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'></head>`;
<!-- 添加 FontAwesome 库 --> <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'> </head> ';// Join output with newlines, wrap inside <html> and <body>return `<html>${htmlHead}<body><pre style='background-color: transparent; border: none;'>${header}</pre><pre>${output}</pre></body><script>function copyToClipboard(text) {navigator.clipboard.writeText(text).then(() => {alert("Copied to clipboard");}).catch((err) => {console.error("Failed to copy to clipboard:", err);});}</script></html>`;
}
用换行符连接输出,在里面<html>换行并<body>返回 ' <html> ${htmlHead} <body> <pre style='background-color: transparent; border: none;'>${header}</pre> <pre>${output}</pre> </body> <script> function copyToClipboard(text) { navigator.clipboard.writeText(text) .then(() => { alert(“复制到剪贴板”); }) .catch((err) => { console.error(“无法复制到剪贴板:”, err); </script> </html> });';}
const portSet_http = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]);
const portSet_https = new Set([443, 8443, 2053, 2096, 2087, 2083]);
const portSet_http = 新集([80, 8080, 8880, 2052, 2086, 2095, 2082]);常量 portSet_https = 新集([443, 8443, 2053, 2096, 2087, 2083]);
function createVLESSSub(userID_Path, hostName) {const userIDArray = userID_Path.includes(',') ? userID_Path.split(',') : [userID_Path];const commonUrlPart_http = `?encryption=none&security=none&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`;const commonUrlPart_https = `?encryption=none&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#`;
函数 createVLESSSub(userID_Path, hostName) { const userIDArray = userID_Path.includes(',') ? userID_Path.split(',') : [userID_Path]; const commonUrlPart_http = '?encryption=none&security=none&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#'; const commonUrlPart_https = '?encryption=none&security=tls&sni=${hostName}&fp=random&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#';const output = userIDArray.flatMap((userID) => {const httpConfigurations = Array.from(portSet_http).flatMap((port) => {if (!hostName.includes('pages.dev')) {const urlPart = `${hostName}-HTTP-${port}`;const vlessMainHttp = `vless://${userID}@${hostName}:${port}${commonUrlPart_http}${urlPart}`;return proxyIPs.flatMap((proxyIP) => {const vlessSecHttp = `vless://${userID}@${proxyIP}:${port}${commonUrlPart_http}${urlPart}-${proxyIP}-EDtunnel`;return [vlessMainHttp, vlessSecHttp];});}return [];});
const output = userIDArray.flatMap((userID) => { const httpConfigurations = Array.from(portSet_http).flatMap((port) => { if (!hostName.includes('pages.dev')) { const urlPart = '${hostName}-HTTP-${port}'; const vlessMainHttp = 'vless://${userID}@${hostName}:${port}${commonUrlPart_http}${urlPart}'; return proxyIPs.flatMap((proxyIP) => { const vlessSecHttp = 'vless://${userID}@${proxyIP}:${port}${commonUrlPart_http}${urlPart}-${proxyIP}-EDtunnel'; 返回 [vlessMainHttp,vlessSecHttp];});} 返回 [];});const httpsConfigurations = Array.from(portSet_https).flatMap((port) => {const urlPart = `${hostName}-HTTPS-${port}`;const vlessMainHttps = `vless://${userID}@${hostName}:${port}${commonUrlPart_https}${urlPart}`;return proxyIPs.flatMap((proxyIP) => {const vlessSecHttps = `vless://${userID}@${proxyIP}:${port}${commonUrlPart_https}${urlPart}-${proxyIP}-EDtunnel`;return [vlessMainHttps, vlessSecHttps];});});
const httpsConfigurations = Array.from(portSet_https).flatMap((port) => { const urlPart = '${hostName}-HTTPS-${port}'; const vlessMainHttps = 'vless://${userID}@${hostName}:${port}${commonUrlPart_https}${urlPart}'; return proxyIPs.flatMap((proxyIP) => { const vlessSecHttps = 'vless://${userID}@${proxyIP}:${port}${commonUrlPart_https}${urlPart}-${proxyIP}-EDtunnel'; return [vlessMainHttps, vlessSecHttps]; });return [...httpConfigurations, ...httpsConfigurations];});
返回 [...httpConfigurations、...https配置];});return output.join('\n');
}
返回 output.join('\n');}
const cn_hostnames = ['weibo.com', // Weibo - A popular social media platform'www.baidu.com', // Baidu - The largest search engine in China'www.qq.com', // QQ - A widely used instant messaging platform'www.taobao.com', // Taobao - An e-commerce website owned by Alibaba Group'www.jd.com', // JD.com - One of the largest online retailers in China'www.sina.com.cn', // Sina - A Chinese online media company'www.sohu.com', // Sohu - A Chinese internet service provider'www.tmall.com', // Tmall - An online retail platform owned by Alibaba Group'www.163.com', // NetEase Mail - One of the major email providers in China'www.zhihu.com', // Zhihu - A popular question-and-answer website'www.youku.com', // Youku - A Chinese video sharing platform'www.xinhuanet.com', // Xinhua News Agency - Official news agency of China'www.douban.com', // Douban - A Chinese social networking service'www.meituan.com', // Meituan - A Chinese group buying website for local services'www.toutiao.com', // Toutiao - A news and information content platform'www.ifeng.com', // iFeng - A popular news website in China'www.autohome.com.cn', // Autohome - A leading Chinese automobile online platform'www.360.cn', // 360 - A Chinese internet security company'www.douyin.com', // Douyin - A Chinese short video platform'www.kuaidi100.com', // Kuaidi100 - A Chinese express delivery tracking service'www.wechat.com', // WeChat - A popular messaging and social media app'www.csdn.net', // CSDN - A Chinese technology community website'www.imgo.tv', // ImgoTV - A Chinese live streaming platform'www.aliyun.com', // Alibaba Cloud - A Chinese cloud computing company'www.eyny.com', // Eyny - A Chinese multimedia resource-sharing website'www.mgtv.com', // MGTV - A Chinese online video platform'www.xunlei.com', // Xunlei - A Chinese download manager and torrent client'www.hao123.com', // Hao123 - A Chinese web directory service'www.bilibili.com', // Bilibili - A Chinese video sharing and streaming platform'www.youth.cn', // Youth.cn - A China Youth Daily news portal'www.hupu.com', // Hupu - A Chinese sports community and forum'www.youzu.com', // Youzu Interactive - A Chinese game developer and publisher'www.panda.tv', // Panda TV - A Chinese live streaming platform'www.tudou.com', // Tudou - A Chinese video-sharing website'www.zol.com.cn', // ZOL - A Chinese electronics and gadgets website'www.toutiao.io', // Toutiao - A news and information app'www.tiktok.com', // TikTok - A Chinese short-form video app'www.netease.com', // NetEase - A Chinese internet technology company'www.cnki.net', // CNKI - China National Knowledge Infrastructure, an information aggregator'www.zhibo8.cc', // Zhibo8 - A website providing live sports streams'www.zhangzishi.cc', // Zhangzishi - Personal website of Zhang Zishi, a public intellectual in China'www.xueqiu.com', // Xueqiu - A Chinese online social platform for investors and traders'www.qqgongyi.com', // QQ Gongyi - Tencent's charitable foundation platform'www.ximalaya.com', // Ximalaya - A Chinese online audio platform'www.dianping.com', // Dianping - A Chinese online platform for finding and reviewing local businesses'www.suning.com', // Suning - A leading Chinese online retailer'www.zhaopin.com', // Zhaopin - A Chinese job recruitment platform'www.jianshu.com', // Jianshu - A Chinese online writing platform'www.mafengwo.cn', // Mafengwo - A Chinese travel information sharing platform'www.51cto.com', // 51CTO - A Chinese IT technical community website'www.qidian.com', // Qidian - A Chinese web novel platform'www.ctrip.com', // Ctrip - A Chinese travel services provider'www.pconline.com.cn', // PConline - A Chinese technology news and review website'www.cnzz.com', // CNZZ - A Chinese web analytics service provider'www.telegraph.co.uk', // The Telegraph - A British newspaper website 'www.ynet.com', // Ynet - A Chinese news portal'www.ted.com', // TED - A platform for ideas worth spreading'www.renren.com', // Renren - A Chinese social networking service'www.pptv.com', // PPTV - A Chinese online video streaming platform'www.liepin.com', // Liepin - A Chinese online recruitment website'www.881903.com', // 881903 - A Hong Kong radio station website'www.aipai.com', // Aipai - A Chinese online video sharing platform'www.ttpaihang.com', // Ttpaihang - A Chinese celebrity popularity ranking website'www.quyaoya.com', // Quyaoya - A Chinese online ticketing platform'www.91.com', // 91.com - A Chinese software download website'www.dianyou.cn', // Dianyou - A Chinese game information website'www.tmtpost.com', // TMTPost - A Chinese technology media platform'www.douban.com', // Douban - A Chinese social networking service'www.guancha.cn', // Guancha - A Chinese news and commentary website'www.so.com', // So.com - A Chinese search engine'www.58.com', // 58.com - A Chinese classified advertising website'www.cnblogs.com', // Cnblogs - A Chinese technology blog community'www.cntv.cn', // CCTV - China Central Television official website'www.secoo.com', // Secoo - A Chinese luxury e-commerce platform
];
const cn_hostnames = [ 'weibo.com', // 微博 - 流行的社交媒体平台 'www.baidu.com', // 百度 - 中国最大的搜索引擎 'www.qq.com', // QQ - 广泛使用的即时通讯平台 'www.taobao.com', // 淘宝 - 阿里巴巴集团旗下的电子商务网站 'www.jd.com', // JD.com - 中国最大的在线零售商之一 'www.sina.com.cn', // 新浪 - 中国在线媒体公司 'www.sohu.com', // 搜狐 - 中国互联网服务提供商 'www.tmall.com', 天猫 - 阿里巴巴集团旗下的在线零售平台 “www.163.com”, // 网易邮箱 - 中国主要的电子邮件提供商之一 “www.zhihu.com”, // 知乎 - 一个受欢迎的问答网站 “www.youku.com”, // 优酷 - 中国视频分享平台“www.xinhuanet.com”, // 新华社 - 中国官方通讯社“www.douban.com”, // 豆瓣 - 中国社交网络服务“www.meituan.com”, // 美团 - 一个中国的本地服务团购网站'www.toutiao.com', // 今日头条 - 新闻和信息内容平台 'www.ifeng.com', // 爱风 - 中国流行的新闻网站 'www.autohome.com.cn', // 汽车之家 - 中国领先的汽车在线平台'www.360.cn', // 360 - 中国互联网安全公司 'www.douyin.com', // 抖音 - 中国短视频平台'www.kuaidi100.com', // 快的100 - 中国快递跟踪服务'www.wechat.com', // 微信 - 流行的消息传递和社交媒体应用程序'www.csdn.net', // CSDN - 中国科技社区网站 'www.imgo.tv', // ImgoTV - 中国直播平台'www.aliyun.com', // 阿里云 - 中国云计算公司 'www.eyny.com', // Eyny - 中国多媒体资源共享网站'www.mgtv.com', // MGTV - 中国在线视频平台'www.xunlei.com', // 迅雷 - 中国下载管理器和种子客户端 'www.hao123.com', // Hao123 - 中国网络目录服务 'www.bilibili.com', // 哔哩哔哩 - 中国视频分享和流媒体平台'www.youth.cn', // Youth.cn - 中国青年报新闻门户网站'www.hupu.com', // 虎浦 - 中国体育社区和论坛 'www.youzu.com', // 游豆互动 - 中国游戏开发商和发行商 'www.panda.tv', // 熊猫电视 - 中国直播平台'www.tudou.com', 土豆 - 中国视频分享网站“www.zol.com.cn”, // ZOL - 中国电子和小工具网站“www.toutiao.io”, // 今日头条 - 新闻和信息应用程序“www.tiktok.com”, // TikTok - 中国短视频应用程序“www.netease.com”, // 网易 - 中国互联网科技公司“www.cnki.net”, // CNKI - 中国国家知识基础设施,信息聚合器“www.zhibo8.cc”, // Zhibo8 - 提供体育直播的网站 'www.zhangzishi.cc', // 张子石 - 中国公共知识分子张子石的个人网站 “www.xueqiu.com”, // 雪球 - 面向投资者和交易者的中国在线社交平台 “www.qqgongyi.com”, // QQ公益 - 腾讯慈善基金会平台“www.ximalaya.com”, // 喜马拉雅 - 中国在线音频平台“www.dianping.com”, // 大众点评 - 寻找和评论本地企业的中国在线平台 “www.suning.com”, // 苏宁 - 中国领先的在线零售商 “www.zhaopin.com”, // 智联招聘 - A中国招聘平台'www.jianshu.com', // 建书 - 中国在线写作平台'www.mafengwo.cn', // 马峰窝 - 中国旅游信息共享平台'www.51cto.com', // 51CTO - 中国IT技术社区网站'www.qidian.com', // 启电 - 中国网络小说平台'www.ctrip.com', // 携程 - 中国旅游服务提供商 'www.pconline.com.cn', // PConline - 中国科技新闻和评论网站 'www.cnzz.com', // CNZZ - 中国网络分析服务提供商 'www.telegraph.co.uk', // The Telegraph - 英国报纸网站 'www.ynet.com', // Ynet - 中国新闻门户网站 'www.ted.com', // TED - 一个值得传播思想的平台 “www.renren.com”, // 人人网 - 中国社交网络服务 'www.pptv.com', // PPTV - 一个中文在线视频流媒体平台“www.liepin.com”, // 列品 - 中国在线招聘网站“www.881903.com”, // 881903 - 香港广播电台网站“www.aipai.com”, // 爱拍 - 中国在线视频分享平台“www.ttpaihang.com”, // Ttpaihang - 中国名人人气排行榜网站“www.quyaoya.com”, // 曲耀雅 - 中国在线票务平台“www.91.com”, // 91.com - 中国软件下载网站“www.dianyou.cn”, // 点游 - 中国游戏信息网站'www.tmtpost.com', // TMTPost - 中国科技媒体平台'www.douban.com', // 豆瓣 - 中国社交网络服务'www.guancha.cn', // 关查 - 中国新闻评论网站'www.so.com', // So.com - 中国搜索引擎'www.58.com', // 58.com - 中国分类广告网站'www.cnblogs.com', // Cnblogs - 中国科技博客社区'www.cntv.cn', // 央视 - 中国中央电视台官方网站'www.secoo.com', // 寺库 - 中国奢侈品电商平台 ];
仓库:https://github.com/Li468446/workers.js/tree/main
---------------------------------------------------------------------------------------------------------------------------------
导入完成后,我们需要修改两个地方,分别是UUID和CDN-IP。CDN-IP必须是反代IP,不能是官方IP!!!
4、UUID生成
UUID的大概解释在本文开头的知识链条中有比较标准的解释,大家可以作为参考。UUID因其属性,随机生成且基本不会重合,我们现在去生成它,将它放入代码中即可。这里给大家推荐一个UUID在线随机生成的网站,希望能帮到各位开发者和极客玩家>>>open<<<,网址:
https://1024tools.com/uuid
进入后点击生成即可,然后随便选择一个就行,UUID非常重要,请把你使用的UUID保存一份副本,避免后续的操作无法进行!!!
这里我则第二个,复制出来后回到实例代码中,修改UUID为生成的这个。
5、cloud flare-proxy-IP 、proxy域的选择
优质域名推荐
cdn-all.xn--b6gac.eu.org
cdn.xn--b6gac.eu.org
cdn-b100.xn--b6gac.eu.org
edgetunnel.anycast.eu.org
cdn.anycast.eu.org(亚洲地区)
jp.cloudflarest.link
achk.cloudflarest.link(阿里香港优选)
这里给大家两种优选cloud flare优质IP的方案:
2、个人推荐
113.64.186.198
103.200.112.108
199.15.76.35edgetunnel.anycast.eu.org(美国的加速CDN)
cdn.anycast.eu.org(香港日本新加坡加速CDN)香港优选IP:20.187.89.16
日本高速优选IP:146.56.149.205
美国优选IP:172.64.135.146)
韩国高速优选IP:129.154.199.251
日本高速优选IP:146.56.149.205cdn-all.xn--b6gac.eu.org
cdn.xn--b6gac.eu.org
cdn-b100.xn--b6gac.eu.org
edgetunnel.anycast.eu.org
cdn.anycast.eu.org
同时也可以选出运营商的最优反代IP,放入实例代码中的CDN-IP位置即可。这里我就直接使用这个IP。
113.64.186.198
填入后点击部署即可。
我已经保存部署过了,所以这里这个按钮是灰色的
到这里,cloud flare的实例部署就已经完成了。
三、使用方法(无域名)
生成的实例流量转发的协议为vless,所以Vtwo(2)ray是个不错的选择.
1、V222222222222rayn
复制刚刚创建的代码地址,格式为:https://vless.xxx.workers.dev/
,其中 vless
为创建的脚本名称,xxx
为 CloudFlare 用户名。
复制出来后,后面加个/+UUID,如下
https://ceshi.xiaorantang68.workers.dev/37959939-2926-4e4c-a0d2-6468a1b9f7f7
这样我们就获取到了vless服务器的两个节点信息
复制红框内的内容后粘贴进v222222222222rayn
添加完成,但是现在这个服务器还不能使用,需要对节点进行进一步的修改。
因为不使用域名,所以加密方式不需要选择,将TLS改为空
如下图:
端口呢,也不能使用443端口,这里我切换为80端口(不使用TLS可以将端口改为80或2052)
2、修改地址
现在就可以进行IP的优选了,脚本可以在我的仓库下载:https://github.com/Li468446/workers.js
填入v222222222rayn的vless服务器连接信息中即可。
lanzou:https://pan.lanpw.com/b0742hkxehttps://pan.lanpw.com/b0742hkxe
下载下来后运行jp.bat批处理程序即可自动优选。优选前请关闭所有的系统代理,避免优选出现误差!!!
优选结果会写入rest.csv文件内,填入vless服务器IP中即可,这里就不多做解答
优选出来的IP直接填入vless服务器的地址栏即可,确定后切换为活动服务器,访问谷歌尝试:
到这里,v2222222222222rayn的配置就完成了,可以进行测试
四、实例测试
我这里优选出来的本地IP是104.19.170.**,填入后访问谷歌,测试成功。
查看cloud flare的实例,有正常的请求,正常工作。
实验成功
五、总结
需要有一定的web基础和网络代理的基础,过程繁琐,在这个过程中,最容易出错的地方就在于实例中的workers的proxy IP和v222222222222rayn的客户端连接的CF优选IP容易搞混淆,Proxy-IP必须是反向代理IP,否则实例的反向代理会出现问题。
=========================================================================
最后,建议各位开发者们仔细观看,认真学习,我在文中如有表达不够清晰,存在疑问的,可以在下方评论区提出,我会一一为大家解答,有错误的地方也请大佬指点一二,共同学习,一起进步。
本章节就到这里,感谢大家的支持!