diff options
author | HappyLeslieAlexander <HappyLeslieAlexander@duck.com> | 2025-01-25 13:54:29 +0100 |
---|---|---|
committer | HappyLeslieAlexander <HappyLeslieAlexander@duck.com> | 2025-01-25 13:54:29 +0100 |
commit | 4580d495ed5fdda7bf48247dce50c2825094bd36 (patch) | |
tree | 01a2a5e903013e2b1a017f26dc7e29b33a93de1e /_worker.js |
Diffstat (limited to '_worker.js')
-rw-r--r-- | _worker.js | 1564 |
1 files changed, 1564 insertions, 0 deletions
diff --git a/_worker.js b/_worker.js new file mode 100644 index 0000000..7cc30d6 --- /dev/null +++ b/_worker.js @@ -0,0 +1,1564 @@ +// @ts-ignore +import { connect } from 'cloudflare:sockets'; + +// 建议修改为自己的 UUID +let userID = '7597a4e5-bee0-470d-b50d-ebf412c00b47'; + +// 生成配置文件的 Cloudflare 优选 IP (www.gov.se speed.cloudflare.com等使用Cloudflare CDN的网站也可以) +const bestCFIP = "www.visa.com.hk" + +// 用于 Cloudflare 网站的代理 IP +const proxyIPs = ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org']; // (https://github.com/HappyLeslieAlexander/Cloudflare_VLESS/blob/main/proxyip.txt 中的地址也可以) +let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; + +let dohURL = 'https://cloudflare-dns.com/dns-query'; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query + +// v2board api environment variables +let nodeId = ''; // 1 + +let apiToken = ''; //abcdefghijklmnopqrstuvwxyz123456 + +let apiHost = ''; // api.v2board.com + +if (!isValidUUID(userID)) { + throw new Error('uuid is not valid'); +} + +export default { + /** + * @param {import("@cloudflare/workers-types").Request} request + * @param {{UID: 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) { + try { + userID = env.UID || userID; + proxyIP = env.PROXYIP || proxyIP; + dohURL = env.DNS_RESOLVER_URL || dohURL; + nodeId = env.NODE_ID || nodeId; + apiToken = env.API_TOKEN || apiToken; + apiHost = env.API_HOST || apiHost; + 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 '/connect': // for test connect to Cloudflare socket + const [hostname, port] = ['cloudflare.com', '80']; + console.log(`Connecting to ${hostname}:${port}...`); + + try { + const socket = await connect({ + hostname: hostname, + port: parseInt(port, 10), + }); + + const writer = socket.writable.getWriter(); + + try { + await writer.write(new TextEncoder().encode('GET / HTTP/1.1\r\nHost: ' + hostname + '\r\n\r\n')); + } catch (writeError) { + writer.releaseLock(); + await socket.close(); + return new Response(writeError.message, { status: 500 }); + } + + writer.releaseLock(); + + const reader = socket.readable.getReader(); + let value; + + try { + const result = await reader.read(); + value = result.value; + } catch (readError) { + await reader.releaseLock(); + await socket.close(); + return new Response(readError.message, { status: 500 }); + } + + await reader.releaseLock(); + await socket.close(); + + return new Response(new TextDecoder().decode(value), { status: 200 }); + } catch (connectError) { + return new Response(connectError.message, { status: 500 }); + } + case `/${userID}`: { + const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); + return new Response(`${vlessConfig}`, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } + case `/${userID}/base64`: { + const base64Config = getBase64Config(userID, request.headers.get('Host')); + return new Response(`${base64Config}`, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } + case `/${userID}/clash`: { + const clashConfig = getClashConfig(userID, request.headers.get('Host')); + return new Response(`${clashConfig}`, { + status: 200, + headers: { + "Content-Type": "text/plain;charset=utf-8", + } + }); + } + case `/${userID}/sb`: { + const singConfig = getSingConfig(userID, request.headers.get('Host')); + return new Response(`${singConfig}`, { + status: 200, + headers: { + "Content-Type": "application/json;charset=utf-8", + } + }); + } + default: + // return new Response('Not found', { status: 404 }); + // For any other path, reverse proxy to 'www.visa.com.hk' and return the original response + url.hostname = 'www.visa.com.hk'; + url.protocol = 'https:'; + request = new Request(url, request); + return await fetch(request); + } + } 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-ignore + const 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 --> 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, + } = await processVlessHeader(chunk, userID); + address = addressRemote; + portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' + } `; + if (hasError) { + // controller.error(message); + throw new Error(message); // Cloudflare seems has bug, controller.error will not end stream + // webSocket.close(1000, message); + return; + } + // if UDP but port not DNS port, close it + if (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'); // Cloudflare seems has bug, controller.error will not end stream + return; + } + } + // ["version", "附加信息长度 N"] + const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); + const rawClientData = chunk.slice(rawDataIndex); + + // TODO: support udp here when Cloudflare runtime has udp support + 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, + // @ts-ignore + webSocket: client, + }); +} + +let apiResponseCache = null; +let cacheTimeout = null; + +/** + * Fetches the API response from the server and caches it for future use. + * @returns {Promise<object|null>} A Promise that resolves to the API response object or null if there was an error. + */ +async function fetchApiResponse() { + const requestOptions = { + method: 'GET', + redirect: 'follow' + }; + + try { + const response = await fetch(`https://${apiHost}/api/v1/server/UniProxy/user?node_id=${nodeId}&node_type=v2ray&token=${apiToken}`, requestOptions); + + if (!response.ok) { + console.error('Error: Network response was not ok'); + return null; + } + const apiResponse = await response.json(); + apiResponseCache = apiResponse; + + // Refresh the cache every 5 minutes (300000 milliseconds) + if (cacheTimeout) { + clearTimeout(cacheTimeout); + } + cacheTimeout = setTimeout(() => fetchApiResponse(), 300000); + + return apiResponse; + } catch (error) { + console.error('Error:', error); + return null; + } +} + +/** + * Returns the cached API response if it exists, otherwise fetches the API response from the server and caches it for future use. + * @returns {Promise<object|null>} A Promise that resolves to the cached API response object or the fetched API response object, or null if there was an error. + */ +async function getApiResponse() { + if (!apiResponseCache) { + return await fetchApiResponse(); + } + return apiResponseCache; +} + +/** + * Checks if a given UUID is present in the API response. + * @param {string} targetUuid The UUID to search for. + * @returns {Promise<boolean>} A Promise that resolves to true if the UUID is present in the API response, false otherwise. + */ +async function checkUuidInApiResponse(targetUuid) { + // Check if any of the environment variables are empty + if (!nodeId || !apiToken || !apiHost) { + return false; + } + + try { + const apiResponse = await getApiResponse(); + if (!apiResponse) { + return false; + } + const isUuidInResponse = apiResponse.users.some(user => user.uuid === targetUuid); + return isUuidInResponse; + } catch (error) { + console.error('Error:', error); + return false; + } +} + +// Usage example: +// const targetUuid = "65590e04-a94c-4c59-a1f2-571bce925aad"; +// checkUuidInApiResponse(targetUuid).then(result => console.log(result)); + +/** + * 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 hello + writer.releaseLock(); + return tcpSocket; + } + + // if the Cloudflare connect tcp socket have no incoming data, we retry to redirect ip + async function retry() { + const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) + // no matter retry success or not, close websocket + 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); + + // when remoteSocket is ready, pass to websocket + // remote--> ws + remoteSocketToWS(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.close + safeCloseWebSocket(webSocketServer); + if (readableStreamCancel) { + return; + } + controller.close(); + } + ); + webSocketServer.addEventListener('error', (err) => { + log('webSocketServer has error'); + controller.error(err); + } + ); + // for ws 0rtt + 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 + }, + 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 cancel + if (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 + */ +async 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; + const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); + const slicedBufferString = stringify(slicedBuffer); + + const uuids = userID.includes(',') ? userID.split(",") : [userID]; + + const checkUuidInApi = await checkUuidInApiResponse(slicedBufferString); + isValidUser = uuids.some(userUuid => checkUuidInApi || slicedBufferString === userUuid.trim()); + + console.log(`checkUuidInApi: ${await checkUuidInApiResponse(slicedBufferString)}, userID: ${slicedBufferString}`); + + if (!isValidUser) { + return { + hasError: true, + message: 'invalid user', + }; + } + + const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; + //skip opt for now + + const command = new Uint8Array( + vlessBuffer.slice(18 + optLength, 18 + optLength + 1) + )[0]; + + // 0x01 TCP + // 0x02 UDP + // 0x03 MUX + if (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 == 0x005d + const 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 =16 + const 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:7334 + const ipv6 = []; + for (let i = 0; i < 8; i++) { + ipv6.push(dataView.getUint16(i * 2).toString(16)); + } + addressValue = ipv6.join(':'); + // seems no need add [] for ipv6 + break; + 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--> ws + let remoteChunkCount = 0; + let chunks = []; + /** @type {ArrayBuffer | null} */ + let vlessHeader = vlessResponseHeader; + let hasIncomingData = false; // check if remoteSocket has incoming data + 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, maybe close' + ); + } + if (vlessHeader) { + webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); + vlessHeader = null; + } else { + // seems no need rate limit this, Cloudflare seems fix this??.. + // if (remoteChunkCount > 20000) { + // // Cloudflare 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 Cloudflare connect socket have error, + // 1. Socket.closed will have error + // 2. Socket.readable will be close without any data coming + if (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 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 { 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 message + 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 now + 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 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 vlessLink = `vless://${userID}\u0040${bestCFIP}:80?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Workers/Pages` + const vlessTlsLink = `vless://${userID}\u0040${bestCFIP}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Workers/Pages-TLS` + return ` +下面是非 TLS 端口的节点信息及节点分享链接,可使用 Cloudflare 支持的非 TLS 端口: + +地址:${hostName} 或 Cloudflare 优选 IP +端口:80 或 Cloudflare 支持的非 TLS 端口 +UUID:${userID} +传输:ws +伪装域名:${hostName} +路径:/?ed=2048 + +${vlessLink} + +下面是 TLS 端口的节点信息及节点分享链接,可使用 Cloudflare 支持的 TLS 端口: + +地址:${hostName} 或 Cloudflare 优选 IP +端口:443 或 Cloudflare 支持的 TLS 端口 +UUID:${userID} +传输:ws +传输层安全:TLS +伪装域名:${hostName} +路径:/?ed=2048 +SNI 域名:${hostName} + +${vlessTlsLink} + +Base64 通用节点订阅链接:https://${hostName}/${userID}/base64 +Clash 配置文件订阅链接:https://${hostName}/${userID}/clash +Sing-box 配置文件订阅链接:https://${hostName}/${userID}/sb + +提示:部分地区有 Cloudflare 默认域名被污染的情况,除非打开客户端的 TLS 分片功能,否则无法使用 TLS 端口的节点 +如为 Pages 部署的节点则只能使用 TLS 端口的节点 +--------------------------------------------------------------- +捐赠 +加密货币 +TRON +TY7n1xwiHCBqcQqGH1cxjTQqZTuTXbzB4S +Ethereum +0xed57e7237e88cec19d3fd12a0d26bacb1dcc247b +Polygon +0xed57e7237e88cec19d3fd12a0d26bacb1dcc247b +TON +UQC4r4gxAIbOTEEZGG-C1Ffn9inRo24J7qw3U0dFfaIfKyFr +--------------------------------------------------------------- +联系 +Telegram: https://t.me/Depressed_LeslieAlexander/ +E-mail: https://github.com/HappyLeslieAlexander/ +项目地址: https://github.com/HappyLeslieAlexander/Cloudflare_VLESS/ +By Leslie Alexander +`; +} + +function getBase64Config(userID, hostName) { + const vlessLinks = btoa(`vless://${userID}\u0040${bestCFIP}:80?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-80\nvless://${userID}\u0040${bestCFIP}:8080?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-8080\nvless://${userID}\u0040${bestCFIP}:8880?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-8880\nvless://${userID}\u0040${bestCFIP}:2052?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-2052\nvless://${userID}\u0040${bestCFIP}:2082?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-2082\nvless://${userID}\u0040${bestCFIP}:2086?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-2086\nvless://${userID}\u0040${bestCFIP}:2095?encryption=none&security=none&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-2095\nvless://${userID}\u0040${bestCFIP}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-TLS-443\nvless://${userID}\u0040${bestCFIP}:2053?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-TLS-2053\nvless://${userID}\u0040${bestCFIP}:2083?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-TLS-2083\nvless://${userID}\u0040${bestCFIP}:2087?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-TLS-2087\nvless://${userID}\u0040${bestCFIP}:2096?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-TLS-2096\nvless://${userID}\u0040${bestCFIP}:8443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2560#Leslie-Cloudflare-vless-TLS-8443`); + + return `${vlessLinks}` +} + +function getClashConfig(userID, hostName) { + return `port: 7890 +allow-lan: true +mode: rule +log-level: info +unified-delay: true +global-client-fingerprint: chrome +dns: + enable: true + listen: :53 + ipv6: true + enhanced-mode: fake-ip + fake-ip-range: 198.18.0.1/16 + default-nameserver: + - 223.5.5.5 + - 1.1.1.1 + nameserver: + - https://dns.alidns.com/dns-query + - https://cloudflare-dns.com/dns-query + fallback: + - https://cloudflare-dns.com/dns-query + fallback-filter: + geoip: true + geoip-code: CN + ipcidr: + - 240.0.0.0/4 + +proxies: +- name: Cloudflare-vless-80 + type: vless + server: ${bestCFIP} + port: 80 + uuid: ${userID} + udp: false + tls: false + network: ws + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-8080 + type: vless + server: ${bestCFIP} + port: 8080 + uuid: ${userID} + udp: false + tls: false + network: ws + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-8880 + type: vless + server: ${bestCFIP} + port: 8880 + uuid: ${userID} + udp: false + tls: false + network: ws + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-2052 + type: vless + server: ${bestCFIP} + port: 2052 + uuid: ${userID} + udp: false + tls: false + network: ws + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-2082 + type: vless + server: ${bestCFIP} + port: 2082 + uuid: ${userID} + udp: false + tls: false + network: ws + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-2086 + type: vless + server: ${bestCFIP} + port: 2086 + uuid: ${userID} + udp: false + tls: false + network: ws + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-2095 + type: vless + server: ${bestCFIP} + port: 2095 + uuid: ${userID} + udp: false + tls: false + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-tls-443 + type: vless + server: ${bestCFIP} + port: 443 + uuid: ${userID} + udp: false + tls: true + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-tls-2053 + type: vless + server: ${bestCFIP} + port: 2053 + uuid: ${userID} + udp: false + tls: true + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-tls-2083 + type: vless + server: ${bestCFIP} + port: 2083 + uuid: ${userID} + udp: false + tls: true + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-tls-2087 + type: vless + server: ${bestCFIP} + port: 2087 + uuid: ${userID} + udp: false + tls: true + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-tls-2096 + type: vless + server: ${bestCFIP} + port: 2096 + uuid: ${userID} + udp: false + tls: true + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +- name: Cloudflare-vless-tls-8443 + type: vless + server: ${bestCFIP} + port: 8443 + uuid: ${userID} + udp: false + tls: true + network: ws + servername: ${hostName} + ws-opts: + path: "/?ed=2048" + headers: + Host: ${hostName} + +proxy-groups: +- name: 负载均衡 + type: load-balance + url: http://www.gstatic.com/generate_204 + interval: 300 + proxies: + - Cloudflare-vless-80 + - Cloudflare-vless-8080 + - Cloudflare-vless-8880 + - Cloudflare-vless-2052 + - Cloudflare-vless-2082 + - Cloudflare-vless-2086 + - Cloudflare-vless-2095 + - Cloudflare-vless-tls-443 + - Cloudflare-vless-tls-2053 + - Cloudflare-vless-tls-2083 + - Cloudflare-vless-tls-2087 + - Cloudflare-vless-tls-2096 + - Cloudflare-vless-tls-8443 + +- name: 自动选择 + type: url-test + url: http://www.gstatic.com/generate_204 + interval: 300 + tolerance: 50 + proxies: + - Cloudflare-vless-80 + - Cloudflare-vless-8080 + - Cloudflare-vless-8880 + - Cloudflare-vless-2052 + - Cloudflare-vless-2082 + - Cloudflare-vless-2086 + - Cloudflare-vless-2095 + - Cloudflare-vless-tls-443 + - Cloudflare-vless-tls-2053 + - Cloudflare-vless-tls-2083 + - Cloudflare-vless-tls-2087 + - Cloudflare-vless-tls-2096 + - Cloudflare-vless-tls-8443 + +- name: 🌍选择代理 + type: select + proxies: + - 负载均衡 + - 自动选择 + - DIRECT + - Cloudflare-vless-80 + - Cloudflare-vless-8080 + - Cloudflare-vless-8880 + - Cloudflare-vless-2052 + - Cloudflare-vless-2082 + - Cloudflare-vless-2086 + - Cloudflare-vless-2095 + - Cloudflare-vless-tls-443 + - Cloudflare-vless-tls-2053 + - Cloudflare-vless-tls-2083 + - Cloudflare-vless-tls-2087 + - Cloudflare-vless-tls-2096 + - Cloudflare-vless-tls-8443 + +rules: + - GEOIP,LAN,DIRECT + - GEOIP,CN,DIRECT + - MATCH,🌍选择代理` +} + +function getSingConfig(userID, hostName) { + return `{ + "log": { + "disabled": false, + "level": "info", + "timestamp": true + }, + "experimental": { + "clash_api": { + "external_controller": "127.0.0.1:9090", + "external_ui": "ui", + "external_ui_download_url": "", + "external_ui_download_detour": "", + "secret": "", + "default_mode": "Rule" + }, + "cache_file": { + "enabled": true, + "path": "cache.db", + "store_fakeip": true + } + }, + "dns": { + "fakeip": { + "enabled": true, + "inet4_range": "198.18.0.0/15", + "inet6_range": "fc00::/18" + }, + "independent_cache": true, + "rules": [ + { + "outbound": [ + "any" + ], + "server": "local" + }, + { + "query_type": [ + "A", + "AAAA" + ], + "rewrite_ttl": 1, + "server": "fakeip" + }, + { + "clash_mode": "global", + "server": "remote" + }, + { + "clash_mode": "direct", + "server": "local" + }, + { + "rule_set": "geosite-cn", + "server": "local" + } + ], + "servers": [ + { + "address": "https://cloudflare-dns.com/dns-query", + "detour": "select", + "tag": "remote" + }, + { + "address": "https://dns.alidns.com/dns-query", + "detour": "direct", + "tag": "local" + }, + { + "address": "rcode://success", + "tag": "block" + }, + { + "address": "fakeip", + "tag": "fakeip" + } + ], + "strategy": "prefer_ipv4" + }, + "experimental": { + "cache_file": { + "enabled": true + }, + "clash_api": { + "external_controller": "127.0.0.1:9090", + "secret": "" + } + }, + "inbounds": [ + { + "auto_route": true, + "domain_strategy": "prefer_ipv4", + "endpoint_independent_nat": true, + "inet4_address": "172.19.0.1/30", + "inet6_address": "2001:0470:f9da:fdfa::1/64", + "mtu": 9000, + "sniff": true, + "strict_route": true, + "type": "tun" + }, + { + "domain_strategy": "prefer_ipv4", + "listen": "127.0.0.1", + "listen_port": 2333, + "tag": "socks-in", + "type": "socks", + "users": [] + }, + { + "domain_strategy": "prefer_ipv4", + "listen": "127.0.0.1", + "listen_port": 2334, + "tag": "mixed-in", + "type": "mixed", + "users": [] + } + ], + "log": {}, + "outbounds": [ + { + "tag": "select", + "type": "selector", + "default": "urltest", + "outbounds": [ + "urltest", + "Cloudflare-vless-80", + "Cloudflare-vless-8080", + "Cloudflare-vless-8880", + "Cloudflare-vless-2052", + "Cloudflare-vless-2082", + "Cloudflare-vless-2086", + "Cloudflare-vless-2095", + "Cloudflare-vless-tls-443", + "Cloudflare-vless-tls-2053", + "Cloudflare-vless-tls-2083", + "Cloudflare-vless-tls-2087", + "Cloudflare-vless-tls-2096", + "Cloudflare-vless-tls-8443" + ] + }, + { + "server": "www.visa.com.hk", + "server_port": 80, + "tag": "Cloudflare-vless-80", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 8080, + "tag": "Cloudflare-vless-8080", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 8880, + "tag": "Cloudflare-vless-8880", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2052, + "tag": "Cloudflare-vless-2052", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2082, + "tag": "Cloudflare-vless-2082", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2086, + "tag": "Cloudflare-vless-2086", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2095, + "tag": "Cloudflare-vless-2095", + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 443, + "tag": "Cloudflare-vless-tls-443", + "tls": { + "enabled": true, + "server_name": "${hostName}" + }, + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2053, + "tag": "Cloudflare-vless-tls-2053", + "tls": { + "enabled": true, + "server_name": "${hostName}" + }, + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2083, + "tag": "Cloudflare-vless-tls-2083", + "tls": { + "enabled": true, + "server_name": "${hostName}" + }, + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2087, + "tag": "Cloudflare-vless-tls-2087", + "tls": { + "enabled": true, + "server_name": "${hostName}" + }, + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 2096, + "tag": "Cloudflare-vless-tls-2096", + "tls": { + "enabled": true, + "server_name": "${hostName}" + }, + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "server": "www.visa.com.hk", + "server_port": 8443, + "tag": "Cloudflare-vless-tls-8443", + "tls": { + "enabled": true, + "server_name": "${hostName}" + }, + "transport": { + "headers": { + "Host": [ + "${hostName}" + ] + }, + "path": "/?ed=2048", + "type": "ws" + }, + "type": "vless", + "uuid": "${userID}", + "packet_encoding": "xudp" + }, + { + "tag": "urltest", + "type": "urltest", + "outbounds": [ + "Cloudflare-vless-80", + "Cloudflare-vless-8080", + "Cloudflare-vless-8880", + "Cloudflare-vless-2052", + "Cloudflare-vless-2082", + "Cloudflare-vless-2086", + "Cloudflare-vless-2095", + "Cloudflare-vless-tls-443", + "Cloudflare-vless-tls-2053", + "Cloudflare-vless-tls-2083", + "Cloudflare-vless-tls-2087", + "Cloudflare-vless-tls-2096", + "Cloudflare-vless-tls-8443" + ] + }, + { + "tag": "direct", + "type": "direct" + }, + { + "tag": "block", + "type": "block" + }, + { + "tag": "dns-out", + "type": "dns" + } + ], + "route": { + "auto_detect_interface": true, + "rule_set": [ + { + "format": "binary", + "tag": "geoip-cn", + "type": "remote", + "url": "https://mirror.ghproxy.com/https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs" + }, + { + "format": "binary", + "tag": "geosite-cn", + "type": "remote", + "url": "https://mirror.ghproxy.com/https://raw.githubusercontent.com/xmdhs/sing-geosite/rule-set-Loyalsoldier/geosite-geolocation-cn.srs" + } + ], + "rules": [ + { + "outbound": "dns-out", + "port": 53 + }, + { + "clash_mode": "direct", + "outbound": "direct" + }, + { + "clash_mode": "global", + "outbound": "select" + }, + { + "ip_is_private": true, + "outbound": "direct" + }, + { + "outbound": "direct", + "rule_set": "geoip-cn" + } + ] + } +}`; +} |