分享 711 11

    使用Cloudflare Workers实现科学上网

    AI摘要:Cloudflare Worker是一种使用Cloudflare的服务来实现科学上网的方法。使用Cloudflare Worker,你可以将你的网站域名解析到Cloudflare,并在Worker中部署代码来实现科学上网。具体的使用方法是在Worker中创建一个Worker,并填写代码,然后部署成功后,访问https://域名/uuid即可获得vess链接信息。

    作为知名的 菩萨Cloudflare深受大家的喜爱,其中 worker 也让人摸索出了可以科学上网的方法

    准备工作

    • 一个顶级域名
    • 一个Cloudflare账号

    使用方法

    添加站点,把域名解析在cloudflare

    在worker中创建Worker

    填写名称,点击部署

    部署成功,点击编辑代码

    填入以下代码,
    注意,修改userID 代码第七行
    只需在Windows系统中 按下 "Win + R", 键入

    Powershell -NoExit -Command "[guid]::NewGuid()"

    即可生成userID

    // <!--GAMFC-->version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 UTC<!--GAMFC-END-->.
    // @ts-ignore
    import { connect } from 'cloudflare:sockets';
    
    // How to generate your own UUID:
    // [Windows] Press "Win + R", input cmd and run:  Powershell -NoExit -Command "[guid]::NewGuid()"
    let userID = '6f08a7a9-bfde-4df6-920d-877a08d63f32';
    
    let proxyIP = '103.200.112.108';
    
    
    if (!isValidUUID(userID)) {
        throw new Error('uuid is not valid');
    }
    
    export default {
        /**
         * @param {import("@cloudflare/workers-types").Request} request
         * @param {{UUID: string, PROXYIP: string}} env
         * @param {import("@cloudflare/workers-types").ExecutionContext} ctx
         * @returns {Promise<Response>}
         */
        async fetch(request, env, ctx) {
            try {
                userID = env.UUID || userID;
                proxyIP = env.PROXYIP || proxyIP;
                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 `/${userID}`: {
                            const vlessConfig = getVLESSConfig(userID, request.headers.get('Host'));
                            return new Response(`${vlessConfig}`, {
                                status: 200,
                                headers: {
                                    "Content-Type": "text/plain;charset=utf-8",
                                }
                            });
                        }
                        default:
                            return new Response('Not found', { status: 404 });
                    }
                } 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,
                } = processVlessHeader(chunk, userID);
                address = addressRemote;
                portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp '
                    } `;
                if (hasError) {
                    // controller.error(message);
                    throw new Error(message); // cf 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'); // cf 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 cf 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,
        });
    }
    
    /**
     * 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 cf 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 
     */
    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;
        if (stringify(new Uint8Array(vlessBuffer.slice(1, 17))) === userID) {
            isValidUser = true;
        }
        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, CF seems fix this??..
                            // if (remoteChunkCount > 20000) {
                            //     // cf 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 cf 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('https://1.1.1.1/dns-query',
                    {
                        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 vlessMain = `vless://${userID}@${hostName}:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`
        return `
    ################################################################
    v2ray
    ---------------------------------------------------------------
    ${vlessMain}
    ---------------------------------------------------------------
    ################################################################
    clash-meta
    ---------------------------------------------------------------
    - type: vless
      name: ${hostName}
      server: ${hostName}
      port: 443
      uuid: ${userID}
      network: ws
      tls: true
      udp: false
      sni: ${hostName}
      client-fingerprint: chrome
      ws-opts:
        path: "/?ed=2048"
        headers:
          host: ${hostName}
    ---------------------------------------------------------------
    ################################################################
    `;
    }

    获得VLESS

    部署成功之后,绑定一个域名

    访问 https://域名/uuid 即可看到vless链接信息

    老孙
    资深网民孙先生

    Notice: Undefined index: HTTP_ACCEPT_LANGUAGE in /www/wwwroot/imsun.org/usr/themes/typecho-theme-farallon/comments.php on line 4
    1. 灰常记忆
      2024-05-03 12:19

      😀这方法不错,有时间试试,就是不知道对vps有没有影响?

        1. 老孙
          2024-05-03 20:14
          @灰常记忆

          这个 就不用VPS, 只要域名解析在cloudflare就行😅 纯白嫖

    2. 王云子
      王云子
      2024-04-16 21:58

      好家伙,老孙这可不兴教学啊😃

        1. 老孙
          2024-04-17 08:52
          @王云子

          有需要的怎么都会找到,没需要的压根也不会想到. - -!

    3. 小饿
      2024-04-16 08:07

      我尝试了一下,速度太慢了,可以作为应急梯子,常用的还得vps自建

        1. 老孙
          2024-04-16 08:50
          @小饿

          毕竟用的CF这种公共的平台,也很正常,VPS很容易就被封端口了

            1. 小饿
              2024-04-16 08:52
              @老孙

              个人用,每个月走量很小,平时连视频都不怎么看,已经用了很久了,一直很稳定。

                1. 老孙
                  2024-04-16 08:54
                  @小饿

                  我就被封了好多次端口了.之前裸奔还被封过IP,后来用VLESS+WS+TLS了,就只封端口了

    4. Teacher Du
      2024-04-12 17:31

      这方法好!

        1. 老孙
          2024-04-15 10:19
          @Teacher Du

          🤣 毕竟人们还是有需求的.于是才有了这些白嫖的方法😁

            1. TeacherDu
              2024-04-15 22:42
              @老孙

              速度还挺快的!