├── README.md └── worker-vless.js /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | zizifn 大佬的一个开源项目 [edgetunnel](https://github.com/zizifn/edgetunnel) ,使得我们可以免费的在 Cloudflare 上面通过部署 Worker ,来创建一个免费 VLESS 节点! 4 | 5 | ### 何为 Cloudflare Worker? 6 | 7 | Cloudflare Worker 是 Cloudflare 提供的一种服务,它允许开发者在全球分布的边缘服务器上运行自定义的 JavaScript 代码。 8 | 9 | Cloudflare Worker 可以用来处理 HTTP 请求,从而允许开发者通过编写 JavaScript 代码来实现各种功能,例如路由请求、修改请求和响应、执行身份验证、实现缓存策略等。 10 | 11 | ## 准备工作 12 | 13 | 1. 注册 Cloudflare 账号,[注册地址](https://dash.cloudflare.com/sign-up) 14 | 15 | 2. 购买注册域名一个 16 | 17 | 推荐在 Namesilo 进行购买,因为他的 WHOIS 隐私 是免费的,可以适当的进行一下隐私保护,而且域名还都挺便宜的。 18 | 19 | 购买地址:[点击访问](https://www.namesilo.com/?rid=6254266mw) (1.88刀/年 起) 20 | 21 | 3. 托管域名到 Cloudflare 22 | 23 | ## 详细教程请访问 24 | 25 | V2raySSR综合网,[详情地址](https://v2rayssr.com/worker-vless.html) 26 | 27 | -------------------------------------------------------------------------------- /worker-vless.js: -------------------------------------------------------------------------------- 1 | // version base on commit 841ed4e9ff121dde0ed6a56ae800c2e6c4f66056, time is 2024-04-16 18:02:37 UTC. 2 | // @ts-ignore 3 | import { connect } from 'cloudflare:sockets'; 4 | 5 | // How to generate your own UUID: 6 | // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" 7 | let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; 8 | 9 | let proxyIP = 'cdn.xn--b6gac.eu.org'; // workers.cloudflare.cyou bestproxy.onecf.eu.org cdn-all.xn--b6gac.eu.org cdn.xn--b6gac.eu.org 10 | 11 | if (!isValidUUID(userID)) { 12 | throw new Error('uuid is not valid'); 13 | } 14 | 15 | export default { 16 | /** 17 | * @param {import("@cloudflare/workers-types").Request} request 18 | * @param {{UUID: string, PROXYIP: string}} env 19 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 20 | * @returns {Promise} 21 | */ 22 | async fetch(request, env, ctx) { 23 | try { 24 | userID = env.UUID || userID; 25 | proxyIP = env.PROXYIP || proxyIP; 26 | const upgradeHeader = request.headers.get('Upgrade'); 27 | if (!upgradeHeader || upgradeHeader !== 'websocket') { 28 | const url = new URL(request.url); 29 | switch (url.pathname) { 30 | case '/': 31 | return new Response(JSON.stringify(request.cf), { status: 200 }); 32 | case `/${userID}`: { 33 | const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); 34 | return new Response(`${vlessConfig}`, { 35 | status: 200, 36 | headers: { 37 | "Content-Type": "text/plain;charset=utf-8", 38 | } 39 | }); 40 | } 41 | default: 42 | return new Response('Not found', { status: 404 }); 43 | } 44 | } else { 45 | return await vlessOverWSHandler(request); 46 | } 47 | } catch (err) { 48 | /** @type {Error} */ let e = err; 49 | return new Response(e.toString()); 50 | } 51 | }, 52 | }; 53 | 54 | 55 | 56 | 57 | /** 58 | * 59 | * @param {import("@cloudflare/workers-types").Request} request 60 | */ 61 | async function vlessOverWSHandler(request) { 62 | 63 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */ 64 | // @ts-ignore 65 | const webSocketPair = new WebSocketPair(); 66 | const [client, webSocket] = Object.values(webSocketPair); 67 | 68 | webSocket.accept(); 69 | 70 | let address = ''; 71 | let portWithRandomLog = ''; 72 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { 73 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); 74 | }; 75 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; 76 | 77 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); 78 | 79 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ 80 | let remoteSocketWapper = { 81 | value: null, 82 | }; 83 | let udpStreamWrite = null; 84 | let isDns = false; 85 | 86 | // ws --> remote 87 | readableWebSocketStream.pipeTo(new WritableStream({ 88 | async write(chunk, controller) { 89 | if (isDns && udpStreamWrite) { 90 | return udpStreamWrite(chunk); 91 | } 92 | if (remoteSocketWapper.value) { 93 | const writer = remoteSocketWapper.value.writable.getWriter() 94 | await writer.write(chunk); 95 | writer.releaseLock(); 96 | return; 97 | } 98 | 99 | const { 100 | hasError, 101 | message, 102 | portRemote = 443, 103 | addressRemote = '', 104 | rawDataIndex, 105 | vlessVersion = new Uint8Array([0, 0]), 106 | isUDP, 107 | } = processVlessHeader(chunk, userID); 108 | address = addressRemote; 109 | portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' 110 | } `; 111 | if (hasError) { 112 | // controller.error(message); 113 | throw new Error(message); // cf seems has bug, controller.error will not end stream 114 | // webSocket.close(1000, message); 115 | return; 116 | } 117 | // if UDP but port not DNS port, close it 118 | if (isUDP) { 119 | if (portRemote === 53) { 120 | isDns = true; 121 | } else { 122 | // controller.error('UDP proxy only enable for DNS which is port 53'); 123 | throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream 124 | return; 125 | } 126 | } 127 | // ["version", "附加信息长度 N"] 128 | const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); 129 | const rawClientData = chunk.slice(rawDataIndex); 130 | 131 | // TODO: support udp here when cf runtime has udp support 132 | if (isDns) { 133 | const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); 134 | udpStreamWrite = write; 135 | udpStreamWrite(rawClientData); 136 | return; 137 | } 138 | handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); 139 | }, 140 | close() { 141 | log(`readableWebSocketStream is close`); 142 | }, 143 | abort(reason) { 144 | log(`readableWebSocketStream is abort`, JSON.stringify(reason)); 145 | }, 146 | })).catch((err) => { 147 | log('readableWebSocketStream pipeTo error', err); 148 | }); 149 | 150 | return new Response(null, { 151 | status: 101, 152 | // @ts-ignore 153 | webSocket: client, 154 | }); 155 | } 156 | 157 | /** 158 | * Handles outbound TCP connections. 159 | * 160 | * @param {any} remoteSocket 161 | * @param {string} addressRemote The remote address to connect to. 162 | * @param {number} portRemote The remote port to connect to. 163 | * @param {Uint8Array} rawClientData The raw client data to write. 164 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. 165 | * @param {Uint8Array} vlessResponseHeader The VLESS response header. 166 | * @param {function} log The logging function. 167 | * @returns {Promise} The remote socket. 168 | */ 169 | async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { 170 | async function connectAndWrite(address, port) { 171 | /** @type {import("@cloudflare/workers-types").Socket} */ 172 | const tcpSocket = connect({ 173 | hostname: address, 174 | port: port, 175 | }); 176 | remoteSocket.value = tcpSocket; 177 | log(`connected to ${address}:${port}`); 178 | const writer = tcpSocket.writable.getWriter(); 179 | await writer.write(rawClientData); // first write, nomal is tls client hello 180 | writer.releaseLock(); 181 | return tcpSocket; 182 | } 183 | 184 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip 185 | async function retry() { 186 | const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) 187 | // no matter retry success or not, close websocket 188 | tcpSocket.closed.catch(error => { 189 | console.log('retry tcpSocket closed error', error); 190 | }).finally(() => { 191 | safeCloseWebSocket(webSocket); 192 | }) 193 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); 194 | } 195 | 196 | const tcpSocket = await connectAndWrite(addressRemote, portRemote); 197 | 198 | // when remoteSocket is ready, pass to websocket 199 | // remote--> ws 200 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); 201 | } 202 | 203 | /** 204 | * 205 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer 206 | * @param {string} earlyDataHeader for ws 0rtt 207 | * @param {(info: string)=> void} log for ws 0rtt 208 | */ 209 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { 210 | let readableStreamCancel = false; 211 | const stream = new ReadableStream({ 212 | start(controller) { 213 | webSocketServer.addEventListener('message', (event) => { 214 | if (readableStreamCancel) { 215 | return; 216 | } 217 | const message = event.data; 218 | controller.enqueue(message); 219 | }); 220 | 221 | // The event means that the client closed the client -> server stream. 222 | // However, the server -> client stream is still open until you call close() on the server side. 223 | // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. 224 | webSocketServer.addEventListener('close', () => { 225 | // client send close, need close server 226 | // if stream is cancel, skip controller.close 227 | safeCloseWebSocket(webSocketServer); 228 | if (readableStreamCancel) { 229 | return; 230 | } 231 | controller.close(); 232 | } 233 | ); 234 | webSocketServer.addEventListener('error', (err) => { 235 | log('webSocketServer has error'); 236 | controller.error(err); 237 | } 238 | ); 239 | // for ws 0rtt 240 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); 241 | if (error) { 242 | controller.error(error); 243 | } else if (earlyData) { 244 | controller.enqueue(earlyData); 245 | } 246 | }, 247 | 248 | pull(controller) { 249 | // if ws can stop read if stream is full, we can implement backpressure 250 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure 251 | }, 252 | cancel(reason) { 253 | // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here 254 | // 2. if readableStream is cancel, all controller.close/enqueue need skip, 255 | // 3. but from testing controller.error still work even if readableStream is cancel 256 | if (readableStreamCancel) { 257 | return; 258 | } 259 | log(`ReadableStream was canceled, due to ${reason}`) 260 | readableStreamCancel = true; 261 | safeCloseWebSocket(webSocketServer); 262 | } 263 | }); 264 | 265 | return stream; 266 | 267 | } 268 | 269 | // https://xtls.github.io/development/protocols/vless.html 270 | // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw 271 | 272 | /** 273 | * 274 | * @param { ArrayBuffer} vlessBuffer 275 | * @param {string} userID 276 | * @returns 277 | */ 278 | function processVlessHeader( 279 | vlessBuffer, 280 | userID 281 | ) { 282 | if (vlessBuffer.byteLength < 24) { 283 | return { 284 | hasError: true, 285 | message: 'invalid data', 286 | }; 287 | } 288 | const version = new Uint8Array(vlessBuffer.slice(0, 1)); 289 | let isValidUser = false; 290 | let isUDP = false; 291 | if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) { 292 | isValidUser = true; 293 | } 294 | if (!isValidUser) { 295 | return { 296 | hasError: true, 297 | message: 'invalid user', 298 | }; 299 | } 300 | 301 | const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; 302 | //skip opt for now 303 | 304 | const command = new Uint8Array( 305 | vlessBuffer.slice(18 + optLength, 18 + optLength + 1) 306 | )[0]; 307 | 308 | // 0x01 TCP 309 | // 0x02 UDP 310 | // 0x03 MUX 311 | if (command === 1) { 312 | } else if (command === 2) { 313 | isUDP = true; 314 | } else { 315 | return { 316 | hasError: true, 317 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, 318 | }; 319 | } 320 | const portIndex = 18 + optLength + 1; 321 | const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); 322 | // port is big-Endian in raw data etc 80 == 0x005d 323 | const portRemote = new DataView(portBuffer).getUint16(0); 324 | 325 | let addressIndex = portIndex + 2; 326 | const addressBuffer = new Uint8Array( 327 | vlessBuffer.slice(addressIndex, addressIndex + 1) 328 | ); 329 | 330 | // 1--> ipv4 addressLength =4 331 | // 2--> domain name addressLength=addressBuffer[1] 332 | // 3--> ipv6 addressLength =16 333 | const addressType = addressBuffer[0]; 334 | let addressLength = 0; 335 | let addressValueIndex = addressIndex + 1; 336 | let addressValue = ''; 337 | switch (addressType) { 338 | case 1: 339 | addressLength = 4; 340 | addressValue = new Uint8Array( 341 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 342 | ).join('.'); 343 | break; 344 | case 2: 345 | addressLength = new Uint8Array( 346 | vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) 347 | )[0]; 348 | addressValueIndex += 1; 349 | addressValue = new TextDecoder().decode( 350 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 351 | ); 352 | break; 353 | case 3: 354 | addressLength = 16; 355 | const dataView = new DataView( 356 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 357 | ); 358 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 359 | const ipv6 = []; 360 | for (let i = 0; i < 8; i++) { 361 | ipv6.push(dataView.getUint16(i * 2).toString(16)); 362 | } 363 | addressValue = ipv6.join(':'); 364 | // seems no need add [] for ipv6 365 | break; 366 | default: 367 | return { 368 | hasError: true, 369 | message: `invild addressType is ${addressType}`, 370 | }; 371 | } 372 | if (!addressValue) { 373 | return { 374 | hasError: true, 375 | message: `addressValue is empty, addressType is ${addressType}`, 376 | }; 377 | } 378 | 379 | return { 380 | hasError: false, 381 | addressRemote: addressValue, 382 | addressType, 383 | portRemote, 384 | rawDataIndex: addressValueIndex + addressLength, 385 | vlessVersion: version, 386 | isUDP, 387 | }; 388 | } 389 | 390 | 391 | /** 392 | * 393 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket 394 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 395 | * @param {ArrayBuffer} vlessResponseHeader 396 | * @param {(() => Promise) | null} retry 397 | * @param {*} log 398 | */ 399 | async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { 400 | // remote--> ws 401 | let remoteChunkCount = 0; 402 | let chunks = []; 403 | /** @type {ArrayBuffer | null} */ 404 | let vlessHeader = vlessResponseHeader; 405 | let hasIncomingData = false; // check if remoteSocket has incoming data 406 | await remoteSocket.readable 407 | .pipeTo( 408 | new WritableStream({ 409 | start() { 410 | }, 411 | /** 412 | * 413 | * @param {Uint8Array} chunk 414 | * @param {*} controller 415 | */ 416 | async write(chunk, controller) { 417 | hasIncomingData = true; 418 | // remoteChunkCount++; 419 | if (webSocket.readyState !== WS_READY_STATE_OPEN) { 420 | controller.error( 421 | 'webSocket.readyState is not open, maybe close' 422 | ); 423 | } 424 | if (vlessHeader) { 425 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 426 | vlessHeader = null; 427 | } else { 428 | // seems no need rate limit this, CF seems fix this??.. 429 | // if (remoteChunkCount > 20000) { 430 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M 431 | // await delay(1); 432 | // } 433 | webSocket.send(chunk); 434 | } 435 | }, 436 | close() { 437 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); 438 | // 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. 439 | }, 440 | abort(reason) { 441 | console.error(`remoteConnection!.readable abort`, reason); 442 | }, 443 | }) 444 | ) 445 | .catch((error) => { 446 | console.error( 447 | `remoteSocketToWS has exception `, 448 | error.stack || error 449 | ); 450 | safeCloseWebSocket(webSocket); 451 | }); 452 | 453 | // seems is cf connect socket have error, 454 | // 1. Socket.closed will have error 455 | // 2. Socket.readable will be close without any data coming 456 | if (hasIncomingData === false && retry) { 457 | log(`retry`) 458 | retry(); 459 | } 460 | } 461 | 462 | /** 463 | * 464 | * @param {string} base64Str 465 | * @returns 466 | */ 467 | function base64ToArrayBuffer(base64Str) { 468 | if (!base64Str) { 469 | return { error: null }; 470 | } 471 | try { 472 | // go use modified Base64 for URL rfc4648 which js atob not support 473 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); 474 | const decode = atob(base64Str); 475 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); 476 | return { earlyData: arryBuffer.buffer, error: null }; 477 | } catch (error) { 478 | return { error }; 479 | } 480 | } 481 | 482 | /** 483 | * This is not real UUID validation 484 | * @param {string} uuid 485 | */ 486 | function isValidUUID(uuid) { 487 | 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; 488 | return uuidRegex.test(uuid); 489 | } 490 | 491 | const WS_READY_STATE_OPEN = 1; 492 | const WS_READY_STATE_CLOSING = 2; 493 | /** 494 | * Normally, WebSocket will not has exceptions when close. 495 | * @param {import("@cloudflare/workers-types").WebSocket} socket 496 | */ 497 | function safeCloseWebSocket(socket) { 498 | try { 499 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { 500 | socket.close(); 501 | } 502 | } catch (error) { 503 | console.error('safeCloseWebSocket error', error); 504 | } 505 | } 506 | 507 | const byteToHex = []; 508 | for (let i = 0; i < 256; ++i) { 509 | byteToHex.push((i + 256).toString(16).slice(1)); 510 | } 511 | function unsafeStringify(arr, offset = 0) { 512 | 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(); 513 | } 514 | function stringify(arr, offset = 0) { 515 | const uuid = unsafeStringify(arr, offset); 516 | if (!isValidUUID(uuid)) { 517 | throw TypeError("Stringified UUID is invalid"); 518 | } 519 | return uuid; 520 | } 521 | 522 | 523 | /** 524 | * 525 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 526 | * @param {ArrayBuffer} vlessResponseHeader 527 | * @param {(string)=> void} log 528 | */ 529 | async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { 530 | 531 | let isVlessHeaderSent = false; 532 | const transformStream = new TransformStream({ 533 | start(controller) { 534 | 535 | }, 536 | transform(chunk, controller) { 537 | // udp message 2 byte is the the length of udp data 538 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message 539 | for (let index = 0; index < chunk.byteLength;) { 540 | const lengthBuffer = chunk.slice(index, index + 2); 541 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); 542 | const udpData = new Uint8Array( 543 | chunk.slice(index + 2, index + 2 + udpPakcetLength) 544 | ); 545 | index = index + 2 + udpPakcetLength; 546 | controller.enqueue(udpData); 547 | } 548 | }, 549 | flush(controller) { 550 | } 551 | }); 552 | 553 | // only handle dns udp for now 554 | transformStream.readable.pipeTo(new WritableStream({ 555 | async write(chunk) { 556 | const resp = await fetch('https://1.1.1.1/dns-query', 557 | { 558 | method: 'POST', 559 | headers: { 560 | 'content-type': 'application/dns-message', 561 | }, 562 | body: chunk, 563 | }) 564 | const dnsQueryResult = await resp.arrayBuffer(); 565 | const udpSize = dnsQueryResult.byteLength; 566 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); 567 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); 568 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 569 | log(`doh success and dns message length is ${udpSize}`); 570 | if (isVlessHeaderSent) { 571 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 572 | } else { 573 | webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 574 | isVlessHeaderSent = true; 575 | } 576 | } 577 | } 578 | })).catch((error) => { 579 | log('dns udp has error' + error) 580 | }); 581 | 582 | const writer = transformStream.writable.getWriter(); 583 | 584 | return { 585 | /** 586 | * 587 | * @param {Uint8Array} chunk 588 | */ 589 | write(chunk) { 590 | writer.write(chunk); 591 | } 592 | }; 593 | } 594 | 595 | /** 596 | * 597 | * @param {string} userID 598 | * @param {string | null} hostName 599 | * @returns {string} 600 | */ 601 | function getVLESSConfig(userID, hostName) { 602 | const vlessMain = `vless://${userID}\u0040${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` 603 | return ` 604 | ################################################################ 605 | v2ray 606 | --------------------------------------------------------------- 607 | ${vlessMain} 608 | --------------------------------------------------------------- 609 | ################################################################ 610 | clash-meta 611 | --------------------------------------------------------------- 612 | - type: vless 613 | name: ${hostName} 614 | server: ${hostName} 615 | port: 443 616 | uuid: ${userID} 617 | network: ws 618 | tls: true 619 | udp: false 620 | sni: ${hostName} 621 | client-fingerprint: chrome 622 | ws-opts: 623 | path: "/?ed=2048" 624 | headers: 625 | host: ${hostName} 626 | --------------------------------------------------------------- 627 | ################################################################ 628 | `; 629 | } 630 | --------------------------------------------------------------------------------