准备条件: cloudflare 账号 一个绑定到 cloudflare 的域名。由于平台提供的默认域名 workers.dev 被墙 我的域名是在腾讯云购买的,其 DNSPod 面板不准修改 NS 解析记录,但是可以在域名注册面板修改域名的 DNS 部署: 到 Cloudflare | Web Performance & Security 创建 Worker;创建好后点击编辑代码,粘贴下面的代码(修改第三行内容)并部署 到此 Worker 设置里,添加一个自定义域 访问 /:UUID 可得到节点信息 Worker 存在一些限制,如每日最多10w次请求,单次请求128M等,详情 import { connect } from 'cloudflare:sockets' const UUID = '38fb8867-4062-4c41-a5bb-dd6ce1d1a4db' const PROXY_IP = 'proxyip.aliyun.fxxk.dedyn.io' const isValidUUID = uuid => /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid) if (!isValidUUID(UUID)) throw new Error('uuid is not valid') const base64ToArrayBuffer = base64Str => { if (!base64Str) return { error: null } try { base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/') const earlyData = Uint8Array.from(atob(base64Str), c => c.charCodeAt(0)).buffer return { earlyData, error: null } } catch (error) { return { error } } } const safeCloseWebSocket = socket => { try { if (socket.readyState == 1 || socket.readyState == 2) { socket.close() } } catch (error) { console.error('safeCloseWebSocket error', error) } } const makeReadableWebSocketStream = (webSocketServer, earlyDataHeader, log) => { let readableStreamCancel = false return new ReadableStream({ cancel: reason => { if (readableStreamCancel) return log(`ReadableStream was canceled, due to ${reason}`) readableStreamCancel = true safeCloseWebSocket(webSocketServer) }, start: controller => { webSocketServer.addEventListener('message', event => { if (readableStreamCancel) return controller.enqueue(event.data) }) webSocketServer.addEventListener('close', () => { safeCloseWebSocket(webSocketServer) if (readableStreamCancel) return controller.close() }) webSocketServer.addEventListener('error', error => { log('webSocketServer has error') controller.error(error) }) const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader) if (error) { controller.error(error) } else if (earlyData) { controller.enqueue(earlyData) } } }) } const byteToHex = Array.from({ length: 256 }, (v, k) => (k + 256).toString(16).slice(1)) const stringify = (arr, offset = 0) => { const uuid = ( 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() if (!isValidUUID(uuid)) throw new TypeError('Stringified UUID is invalid') return uuid } const processVlessHeadler = (vlessBuffer, uuid) => { if (vlessBuffer.byteLength < 24) return { hasError: true, message: 'invalid data' } if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) != uuid) return { hasError: true, message: 'invalid user' } const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0] const command = new Uint8Array(vlessBuffer.slice(18 + optLength, 18 + optLength + 1))[0] let isUDP = false switch (command) { case 1: break case 2: isUDP = true break default: return { hasError: true, message: `command ${command} is not support, command 01-tcp,02-udp,03-mux` } } const portIndex = 18 + optLength + 1 const addressIndex = portIndex + 2 const addressType = new Uint8Array(vlessBuffer.slice(addressIndex, addressIndex + 1))[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)) const ipv6 = [] for (let i = 0; i < 8; i++) { ipv6.push(dataView.getUint16(i * 2).toString(16)) } addressValue = ipv6.join(':') break default: return { hasError: true, message: `invild addressType is ${addressType}` } } if (!addressValue) return { hasError: true, message: `addressValue is empty, addressType is ${addressType}` } return { addressType, isUDP, addressRemote: addressValue, hasError: false, portRemote: new DataView(vlessBuffer.slice(portIndex, portIndex + 2)).getUint16(0), rawDataIndex: addressValueIndex + addressLength, vlessVersion: new Uint8Array(vlessBuffer.slice(0, 1)) } } const handleUDPOutBound = async (webSocket, vlessResponseHeader, log) => { const transformStream = new TransformStream({ transform: (chunk, controller) => { 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) } } }) let isVlessHeaderSent = false const writableStream = new WritableStream({ write: async body => { const res = await fetch('https://1.1.1.1/dns-query', { body, headers: { 'content-type': 'application/dns-message' }, method: 'POST' }) const dnsQueryResult = await res.arrayBuffer() const udpSize = dnsQueryResult.byteLength const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 255, udpSize & 255]) if (webSocket.readyState == 1) { 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 } } } }) transformStream.readable.pipeTo(writableStream).catch(error => { log('dns udp has error' + error) }) return { write: chunk => { transformStream.writable.getWriter().write(chunk) } } } const remoteSocketToWS = async (remoteSocket, webSocket, vlessResponseHeader, retry, log) => { let hasIncomingData = false let vlessHeader = vlessResponseHeader const writableStream = new WritableStream({ abort: reason => { console.error('remoteConnection!.readable abort', reason) }, close: () => { log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`) }, write: async (chunk, controller) => { hasIncomingData = true if (webSocket.readyState != 1) { controller.error('webSocket.readyState is not open, maybe close') } if (vlessHeader) { webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()) vlessHeader = null } else { webSocket.send(chunk) } } }) await remoteSocket.readable.pipeTo(writableStream).catch(error => { console.error('remoteSocketToWS has exception ', error.stack || error) safeCloseWebSocket(webSocket) }) if (!hasIncomingData && retry) { log('retry') retry() } } const handleTCPOutBound = async (remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log) => { const connectAndWrite = async (hostname, port) => { const tcpSocket = connect({ hostname, port }) remoteSocket.value = tcpSocket log(`connected to ${hostname}:${port}`) const writer = tcpSocket.writable.getWriter() await writer.write(rawClientData) writer.releaseLock() return tcpSocket } const retry = async () => { const tcpSocket = await connectAndWrite(PROXY_IP || 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) remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log) } const vlessOverWSHandler = async request => { const [client, webSocket] = Object.values(new WebSocketPair()) webSocket.accept() let address = '' let portWithRandomLog = '' const Log = (info, event) => { console.log(`[${address}:${portWithRandomLog}] ${info}`, event || '') } const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '' const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, Log) const remoteSocketWapper = { value: null } let isDns = false let udpStreamWrite = null const writableStream = new WritableStream({ abort: reason => { Log('readableWebSocketStream is abort', JSON.stringify(reason)) }, close: () => { Log('readableWebSocketStream is close') }, write: async chunk => { 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 } = processVlessHeadler(chunk, UUID) address = addressRemote portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp' : 'tcp'}` if (hasError) throw new Error(message) if (isUDP) { if (portRemote == 53) { isDns = true } else { throw new Error('UDP proxy only enable for DNS which is port 53') } } const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]) const rawClientData = chunk.slice(rawDataIndex) if (isDns) { const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, Log) udpStreamWrite = write udpStreamWrite(rawClientData) return } handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, Log) } }) readableWebSocketStream.pipeTo(writableStream).catch(error => { Log('readableWebSocketStream pipeTo error', error) }) return new Response(null, { status: 101, webSocket: client }) } const getVLESSConfig = (uuid, hostname) => ` # v2ray ${`vless://${uuid}@${hostname}:443?encryption=none&security=tls&sni=${hostname}&fp=randomized&type=ws&host=${hostname}&path=%2F%3Fed%3D2048#${hostname}`} # clash - name: 减速云 type: vless server: ${hostname} port: 443 uuid: ${uuid} network: ws tls: true udp: false sni: ${hostname} client-fingerprint: chrome ws-opts: path: "/?ed=2048" headers: host: ${hostname} ` export default { fetch: async request => { try { 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 `/${UUID}`: { return new Response(getVLESSConfig(UUID, request.headers.get('Host')), { headers: { 'Content-Type': 'text/plain;charset=utf-8' }, status: 200 }) } default: return new Response('Not found', { status: 404 }) } } else { return await vlessOverWSHandler(request) } } catch (error) { return new Response(error.toString()) } } }