SkyBlog

减速云搭建扶墙

使用 cloudflare 的 worker 搭建 vless

这篇文章发布于 2024年11月02日,星期六,15:42阅读 ? 次,0 条评论

准备条件:

  • 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())
    }
  }
}