准备条件:
- 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())
}
}
}