├── CloudflareST ├── README.md ├── cf_RE.sh ├── cf_ddns ├── cf_push ├── ip.sh ├── ip_down.sh └── workers-vless.js /CloudflareST: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ydl898898/cloudflareDDNS/4369b7c687533084bd133633bca9179c8cc75a7a/CloudflareST -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloudflareDDNS 2 | 全自动IP优选DDNS域名更新 3 | 4 | ##版本:V2.1.2 5 | #新功能,支持ip地址全自动下载,更新优选完毕后推送至TG,再也不怕脚本没有成功运行了。 6 | #使用脚本需要安装jq和timeout,新增openwrt专用cf_RE.sh文件,运行cf_RE.sh即可在openwrt安装jq和timeout两个扩展。 7 | #其他linux请自行安装jq和timeout。 8 | #主程序为ip_down.sh。 9 | 10 | ################################################################################################### 11 | -------------------------------------------------------------------------------- /cf_RE.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##用于openwrt安装jq和timeout,确保CloudflareSpeedTestDDNS运行正常。 3 | opkg update 4 | opkg install jq coreutils-timeout 5 | opkg list-installed | grep jq 6 | opkg list-installed | grep coreutils-timeout 7 | -------------------------------------------------------------------------------- /cf_ddns: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##版本:V2.1.2 3 | #新功能,支持ip地址全自动下载,更新优选完毕后推送至TG,再也不怕脚本没有成功运行了。 4 | #使用脚本需要安装jq和timeout,新增openwrt专用cf_RE.sh文件,运行cf_RE.sh即可在openwrt安装jq和timeout两个扩展。 5 | #其他linux请自行安装jq和timeout。 6 | #主程序为ip_down.sh。 7 | 8 | ################################################################################################### 9 | export LANG=zh_CN.UTF-8 10 | ################################################################################################### 11 | 12 | ipv4Regex="((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"; 13 | #默认关闭小云朵 14 | proxy="false"; 15 | #验证cf账号信息是否正确 16 | res=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/${zone_id}" -H "X-Auth-Email:$x_email" -H "X-Auth-Key:$api_key" -H "Content-Type:application/json"); 17 | resSuccess=$(echo "$res" | jq -r ".success"); 18 | if [[ $resSuccess != "true" ]]; then 19 | pushmessage="登陆错误,检查cloudflare账号信息填写是否正确!" 20 | source cf_push; 21 | exit 1; 22 | fi 23 | echo "Cloudflare账号验证成功"; 24 | #获取域名填写数量 25 | num=${#hostname[*]}; 26 | #判断优选ip数量是否大于域名数,小于则让优选数与域名数相同 27 | if [ "$CFST_DN" -le $num ] ; then 28 | CFST_DN=$num; 29 | fi 30 | CFST_P=$CFST_DN; 31 | #判断工作模式 32 | if [ "$IP_ADDR" = "ipv6" ] ; then 33 | if [ ! -f "ipv6.txt" ]; then 34 | echo "当前工作模式为ipv6,但该目录下没有【ipv6.txt】,请配置【ipv6.txt】。下载地址:https://github.com/XIU2/CloudflareSpeedTest/releases"; 35 | exit 2; 36 | else 37 | echo "当前工作模式为ipv6"; 38 | fi 39 | else 40 | echo "当前工作模式为ipv4"; 41 | fi 42 | 43 | #读取配置文件中的客户端 44 | if [ "$clien" = "6" ] ; then 45 | CLIEN=bypass; 46 | elif [ "$clien" = "5" ] ; then 47 | CLIEN=openclash; 48 | elif [ "$clien" = "4" ] ; then 49 | CLIEN=clash; 50 | elif [ "$clien" = "3" ] ; then 51 | CLIEN=shadowsocksr; 52 | elif [ "$clien" = "2" ] ; then 53 | CLIEN=passwall2; 54 | else 55 | CLIEN=passwall; 56 | fi 57 | 58 | #判断是否停止科学上网服务 59 | if [ "$pause" = "false" ] ; then 60 | echo "按要求未停止科学上网服务"; 61 | else 62 | /etc/init.d/$CLIEN stop; 63 | echo "已停止$CLIEN"; 64 | fi 65 | 66 | #判断是否配置测速地址 67 | if [[ "$CFST_URL" == http* ]] ; then 68 | CFST_URL_R="-url $CFST_URL"; 69 | else 70 | CFST_URL_R=""; 71 | fi 72 | 73 | 74 | if [ "$IP_ADDR" = "ipv6" ] ; then 75 | #开始优选IPv6 76 | ./CloudflareST $CFST_URL_R -tp $CFST_TP -n $CFST_N -dn $CFST_DN -tl $CFST_TL -tll $CFST_TLL -sl $CFST_SL -p $CFST_P -f ipv6.txt 77 | else 78 | #开始优选IPv4 79 | ./CloudflareST $CFST_URL_R -tp $CFST_TP -n $CFST_N -dn $CFST_DN -tl $CFST_TL -tll $CFST_TLL -sl $CFST_SL -p $CFST_P 80 | fi 81 | echo "测速完毕"; 82 | if [ "$pause" = "false" ] ; then 83 | echo "按要求未重启科学上网服务"; 84 | sleep 3s; 85 | else 86 | /etc/init.d/$CLIEN restart; 87 | echo "已重启$CLIEN"; 88 | echo "为保证cloudflareAPI连接正常 将在30秒后开始更新域名解析"; 89 | sleep 3s; 90 | fi 91 | #开始循环 92 | echo "正在更新域名,请稍后..."; 93 | x=0; 94 | while [[ ${x} -lt $num ]]; do 95 | CDNhostname=${hostname[$x]}; 96 | #获取优选后的ip地址 97 | 98 | ipAddr=$(sed -n "$((x + 2)),1p" result.csv | awk -F, '{print $1}'); 99 | echo "开始更新第$((x + 1))个---$ipAddr"; 100 | #开始DDNS 101 | if [[ $ipAddr =~ $ipv4Regex ]]; then 102 | recordType="A"; 103 | else 104 | recordType="AAAA"; 105 | fi 106 | 107 | 108 | listDnsApi="https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?type=${recordType}&name=${CDNhostname}"; 109 | createDnsApi="https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records"; 110 | 111 | res=$(curl -s -X GET "$listDnsApi" -H "X-Auth-Email:$x_email" -H "X-Auth-Key:$api_key" -H "Content-Type:application/json"); 112 | recordId=$(echo "$res" | jq -r ".result[0].id"); 113 | recordIp=$(echo "$res" | jq -r ".result[0].content"); 114 | 115 | if [[ $recordIp = "$ipAddr" ]]; then 116 | echo "更新失败,获取最快的IP与云端相同"; 117 | resSuccess=false; 118 | elif [[ $recordId = "null" ]]; then 119 | res=$(curl -s -X POST "$createDnsApi" -H "X-Auth-Email:$x_email" -H "X-Auth-Key:$api_key" -H "Content-Type:application/json" --data "{\"type\":\"$recordType\",\"name\":\"$CDNhostname\",\"content\":\"$ipAddr\",\"proxied\":$proxy}"); 120 | resSuccess=$(echo "$res" | jq -r ".success"); 121 | else 122 | updateDnsApi="https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${recordId}"; 123 | res=$(curl -s -X PUT "$updateDnsApi" -H "X-Auth-Email:$x_email" -H "X-Auth-Key:$api_key" -H "Content-Type:application/json" --data "{\"type\":\"$recordType\",\"name\":\"$CDNhostname\",\"content\":\"$ipAddr\",\"proxied\":$proxy}"); 124 | resSuccess=$(echo "$res" | jq -r ".success"); 125 | fi 126 | 127 | if [[ $resSuccess = "true" ]]; then 128 | echo "$CDNhostname更新成功"; 129 | else 130 | echo "$CDNhostname更新失败"; 131 | fi 132 | 133 | x=$((x + 1)); 134 | sleep 3s; 135 | #会生成一个名为informlog的临时文件作为推送的内容。 136 | done > informlog 137 | pushmessage=$(cat informlog); 138 | source cf_push; 139 | exit 0; 140 | -------------------------------------------------------------------------------- /cf_push: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##用于CloudflareSpeedTestDDNS执行情况推送。 3 | echo $pushmessage 4 | message_text=$pushmessage 5 | #解析模式,可选HTML或Markdown 6 | MODE='HTML' 7 | #api接口 8 | URL="https://api.telegram.org/bot${telegramBotToken}/sendMessage" 9 | if [[ -z ${telegramBotToken} ]]; then 10 | echo "未配置TG推送" 11 | else 12 | res=$(timeout 20s curl -s -X POST $URL -d chat_id=${telegramBotUserId} -d parse_mode=${MODE} -d text="${message_text}") 13 | if [ $? == 124 ];then 14 | echo 'TG_api请求超时,请检查网络是否重启完成并是否能够访问TG' 15 | exit 1 16 | fi 17 | resSuccess=$(echo "$res" | jq -r ".ok") 18 | if [[ $resSuccess = "true" ]]; then 19 | echo "TG推送成功"; 20 | else 21 | echo "TG推送失败,请检查TG机器人token和ID"; 22 | fi 23 | fi 24 | exit 0; 25 | -------------------------------------------------------------------------------- /ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##版本:V2.1.2 3 | #新功能,支持ip地址全自动下载,更新优选完毕后推送至TG,再也不怕脚本没有成功运行了。 4 | #使用脚本需要安装jq和timeout,新增openwrt专用cf_RE.sh文件,运行cf_RE.sh即可在openwrt安装jq和timeout两个扩展。 5 | #其他linux请自行安装jq和timeout。 6 | #主程序为ip_down.sh。 7 | 8 | ################################################################################################### 9 | export LANG=zh_CN.UTF-8 10 | ################################################################################################### 11 | ##运行模式ipv4 or ipv6 默认为:ipv4 12 | #指定工作模式为ipv4还是ipv6。如果为ipv6,请在文件夹下添加ipv6.txt 13 | #ipv6.txt在CloudflareST工具包里,下载地址:https://github.com/XIU2/CloudflareSpeedTest/releases 14 | IP_ADDR=ipv4 15 | ################################################################################################### 16 | echo 17 | echo '你的IP地址是'$(curl 4.ipw.cn)',请确认为本机未经过代理的地址' 18 | echo '在路上:https://www.youtube.com/channel/UC4g8abtv5Mi7z8TRW3YOdBA' 19 | echo 'Github:https://github.com/ydl898898/cloudflareDDNS.git' 20 | ################################################################################################### 21 | ##cloudflare配置 22 | #cloudflare账号邮箱 23 | x_email=xxxxxx@163.com 24 | #填写需要DDNS的完整域名 25 | #支持多域名:域名需要填写在括号中,每个域名之间用“空格”相隔。 26 | #例如:(cdn.test.com) 或者 (cdn1.test.com cdn2.test.com cdn3.test.com) 27 | hostname=(ddns1.xxxxxxx.link ddns2.xxxxxxx.link ddns3.xxxxxxx.link ddns4.xxxxxxx.link ddns5.xxxxxxx.link ddns6.xxxxxxx.link) 28 | #区域ID 29 | zone_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 30 | #Global API Key(获取你的API令牌,你的API密钥就是你的登陆密码) 31 | api_key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 32 | ################################################################################################### 33 | ##openwrt科学上网插件配置 34 | #优选节点时是否自动停止科学上网服务 true=自动停止 false=不停止 默认为 true 35 | pause=true 36 | #填写openwrt使用的是哪个科学上网客户端,填写对应的“数字” 默认为 1 客户端为passwall 37 | # 1=passwall 2=passwall2 3=ShadowSocksR Plus+ 4=clash 5=openclash 6=bypass 38 | clien=5 39 | ################################################################################################### 40 | ##CloudflareST配置 41 | #测速地址 42 | CFST_URL=https://cs.xxxxxxx.link 43 | #测速线程数量;越多测速越快,性能弱的设备 (如路由器) 请勿太高;(默认 200 最多 1000 ) 44 | CFST_N=200 45 | # 指定测速端口;延迟测速/下载测速时使用的端口;(默认 443 端口) 46 | CFST_TP=443 47 | #下载测速数量;延迟测速并排序后,从最低延迟起下载测速的数量;(默认 10 个) 48 | CFST_DN=10 49 | #平均延迟上限;只输出低于指定平均延迟的 IP,可与其他上限/下限搭配;(默认9999 ms 这里推荐配置250 ms) 50 | CFST_TL=250 51 | #平均延迟下限;只输出高于指定平均延迟的 IP,可与其他上限/下限搭配、过滤假墙 IP;(默认 0 ms 这里推荐配置40) 52 | CFST_TLL=40 53 | #下载速度下限;只输出高于指定下载速度的 IP,凑够指定数量 [-dn] 才会停止测速;(默认 0.00 MB/s 这里推荐5.00MB/s) 54 | CFST_SL=3 55 | ##################################################################################################### 56 | ##TG推送设置 57 | #(填写即为开启推送,未填写则为不开启) 58 | #TG机器人token 例如:123456789:ABCDEFG... 59 | telegramBotToken= 60 | #用户ID或频道、群ID 例如:123456789 61 | telegramBotUserId= 62 | ##################################################################################################### 63 | source cf_ddns 64 | -------------------------------------------------------------------------------- /ip_down.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ##版本:V2.1.2 3 | #新功能,支持ip地址全自动下载,更新优选完毕后推送至TG,再也不怕脚本没有成功运行了。 4 | #使用脚本需要安装jq和timeout,新增openwrt专用cf_RE.sh文件,运行cf_RE.sh即可在openwrt安装jq和timeout两个扩展。 5 | #其他linux请自行安装jq和timeout。 6 | #主程序为ip_down.sh。 7 | 8 | ################################################################################################### 9 | export LANG=zh_CN.UTF-8 10 | ################################################################################################### 11 | # 定义下载链接和保存路径 12 | download_url="https://zip.baipiao.eu.org/" 13 | save_path="/root/txt.zip" 14 | extracted_folder="/root/txt" # 解压后的文件夹路径 15 | ################################################################################################### 16 | 17 | # 定义最大尝试次数 18 | max_attempts=10 19 | current_attempt=1 20 | ################################################################################################### 21 | 22 | # 循环尝试下载 23 | while [ $current_attempt -le $max_attempts ] 24 | do 25 | # 下载文件 26 | wget "${download_url}" -O $save_path 27 | 28 | # 检查是否下载成功 29 | if [ $? -eq 0 ]; then 30 | break 31 | else 32 | echo "Download attempt $current_attempt failed." 33 | current_attempt=$((current_attempt+1)) 34 | fi 35 | done 36 | ################################################################################################### 37 | 38 | # 检查是否下载成功 39 | if [ $current_attempt -gt $max_attempts ]; then 40 | echo "Failed to download the file after $max_attempts attempts." 41 | else 42 | # 删除原来的txt文件夹内容 43 | rm -rf $extracted_folder/* 44 | 45 | # 解压文件 46 | unzip $save_path -d $extracted_folder 47 | 48 | # 删除压缩包 49 | rm $save_path 50 | 51 | echo "File downloaded and unzipped successfully." 52 | ################################################################################################### 53 | # 合并文件为ip.txt 54 | # 合并所有含有-1-443.txt的文本文件到一个新文件中 55 | merged_file="/root/CloudflareST/merged_ip.txt" 56 | cat $extracted_folder/*-1-443.txt > $merged_file 57 | ################################################################################################### 58 | # 移动到ip.txt到程序总目录 59 | 60 | # 将合并后的文件移动到/root/CloudflareST/ip.txt并覆盖原文件 61 | mv -f "$merged_file" "/root/ddns/ip.txt" 62 | echo "Merged text files containing '-1-443.txt' moved and renamed as 'ip.txt' in /root/ddns." 63 | fi 64 | source ip.sh 65 | -------------------------------------------------------------------------------- /workers-vless.js: -------------------------------------------------------------------------------- 1 | // version base on commit 43fad05dcdae3b723c53c226f8181fc5bd47223e, time is 2023-06-22 15:20:02 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 = '生成你自己的uuid替换'; 8 | 9 | const proxyIPs = ['cdn-all.xn--b6gac.eu.org', 'cdn.xn--b6gac.eu.org', 'cdn-b100.xn--b6gac.eu.org', 'edgetunnel.anycast.eu.org', 'cdn.anycast.eu.org']; 10 | let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; 11 | 12 | let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query 13 | 14 | // v2board api environment variables 15 | let nodeId = ''; // 1 16 | 17 | let apiToken = ''; //abcdefghijklmnopqrstuvwxyz123456 18 | 19 | let apiHost = ''; // api.v2board.com 20 | 21 | if (!isValidUUID(userID)) { 22 | throw new Error('uuid is not valid'); 23 | } 24 | 25 | export default { 26 | /** 27 | * @param {import("@cloudflare/workers-types").Request} request 28 | * @param {{UUID: string, PROXYIP: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env 29 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx 30 | * @returns {Promise} 31 | */ 32 | async fetch(request, env, ctx) { 33 | try { 34 | userID = env.UUID || userID; 35 | proxyIP = env.PROXYIP || proxyIP; 36 | dohURL = env.DNS_RESOLVER_URL || dohURL; 37 | nodeId = env.NODE_ID || nodeId; 38 | apiToken = env.API_TOKEN || apiToken; 39 | apiHost = env.API_HOST || apiHost; 40 | const upgradeHeader = request.headers.get('Upgrade'); 41 | if (!upgradeHeader || upgradeHeader !== 'websocket') { 42 | const url = new URL(request.url); 43 | switch (url.pathname) { 44 | case '/cf': 45 | return new Response(JSON.stringify(request.cf, null, 4), { 46 | status: 200, 47 | headers: { 48 | "Content-Type": "application/json;charset=utf-8", 49 | }, 50 | }); 51 | case '/connect': // for test connect to cf socket 52 | const [hostname, port] = ['cloudflare.com', '80']; 53 | console.log(`Connecting to ${hostname}:${port}...`); 54 | 55 | try { 56 | const socket = await connect({ 57 | hostname: hostname, 58 | port: parseInt(port, 10), 59 | }); 60 | 61 | const writer = socket.writable.getWriter(); 62 | 63 | try { 64 | await writer.write(new TextEncoder().encode('GET / HTTP/1.1\r\nHost: ' + hostname + '\r\n\r\n')); 65 | } catch (writeError) { 66 | writer.releaseLock(); 67 | await socket.close(); 68 | return new Response(writeError.message, { status: 500 }); 69 | } 70 | 71 | writer.releaseLock(); 72 | 73 | const reader = socket.readable.getReader(); 74 | let value; 75 | 76 | try { 77 | const result = await reader.read(); 78 | value = result.value; 79 | } catch (readError) { 80 | await reader.releaseLock(); 81 | await socket.close(); 82 | return new Response(readError.message, { status: 500 }); 83 | } 84 | 85 | await reader.releaseLock(); 86 | await socket.close(); 87 | 88 | return new Response(new TextDecoder().decode(value), { status: 200 }); 89 | } catch (connectError) { 90 | return new Response(connectError.message, { status: 500 }); 91 | } 92 | case `/${userID}`: { 93 | const vlessConfig = getVLESSConfig(userID, request.headers.get('Host')); 94 | return new Response(`${vlessConfig}`, { 95 | status: 200, 96 | headers: { 97 | "Content-Type": "text/plain;charset=utf-8", 98 | } 99 | }); 100 | } 101 | default: 102 | // return new Response('Not found', { status: 404 }); 103 | // For any other path, reverse proxy to 'www.fmprc.gov.cn' and return the original response 104 | url.hostname = 'tv.cctv.com'; 105 | url.protocol = 'https:'; 106 | request = new Request(url, request); 107 | return await fetch(request); 108 | } 109 | } else { 110 | return await vlessOverWSHandler(request); 111 | } 112 | } catch (err) { 113 | /** @type {Error} */ let e = err; 114 | return new Response(e.toString()); 115 | } 116 | }, 117 | }; 118 | 119 | 120 | 121 | 122 | /** 123 | * 124 | * @param {import("@cloudflare/workers-types").Request} request 125 | */ 126 | async function vlessOverWSHandler(request) { 127 | 128 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */ 129 | // @ts-ignore 130 | const webSocketPair = new WebSocketPair(); 131 | const [client, webSocket] = Object.values(webSocketPair); 132 | 133 | webSocket.accept(); 134 | 135 | let address = ''; 136 | let portWithRandomLog = ''; 137 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { 138 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); 139 | }; 140 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; 141 | 142 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log); 143 | 144 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ 145 | let remoteSocketWapper = { 146 | value: null, 147 | }; 148 | let udpStreamWrite = null; 149 | let isDns = false; 150 | 151 | // ws --> remote 152 | readableWebSocketStream.pipeTo(new WritableStream({ 153 | async write(chunk, controller) { 154 | if (isDns && udpStreamWrite) { 155 | return udpStreamWrite(chunk); 156 | } 157 | if (remoteSocketWapper.value) { 158 | const writer = remoteSocketWapper.value.writable.getWriter() 159 | await writer.write(chunk); 160 | writer.releaseLock(); 161 | return; 162 | } 163 | 164 | const { 165 | hasError, 166 | message, 167 | portRemote = 443, 168 | addressRemote = '', 169 | rawDataIndex, 170 | vlessVersion = new Uint8Array([0, 0]), 171 | isUDP, 172 | } = await processVlessHeader(chunk, userID); 173 | address = addressRemote; 174 | portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' 175 | } `; 176 | if (hasError) { 177 | // controller.error(message); 178 | throw new Error(message); // cf seems has bug, controller.error will not end stream 179 | // webSocket.close(1000, message); 180 | return; 181 | } 182 | // if UDP but port not DNS port, close it 183 | if (isUDP) { 184 | if (portRemote === 53) { 185 | isDns = true; 186 | } else { 187 | // controller.error('UDP proxy only enable for DNS which is port 53'); 188 | throw new Error('UDP proxy only enable for DNS which is port 53'); // cf seems has bug, controller.error will not end stream 189 | return; 190 | } 191 | } 192 | // ["version", "附加信息长度 N"] 193 | const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]); 194 | const rawClientData = chunk.slice(rawDataIndex); 195 | 196 | // TODO: support udp here when cf runtime has udp support 197 | if (isDns) { 198 | const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log); 199 | udpStreamWrite = write; 200 | udpStreamWrite(rawClientData); 201 | return; 202 | } 203 | handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log); 204 | }, 205 | close() { 206 | log(`readableWebSocketStream is close`); 207 | }, 208 | abort(reason) { 209 | log(`readableWebSocketStream is abort`, JSON.stringify(reason)); 210 | }, 211 | })).catch((err) => { 212 | log('readableWebSocketStream pipeTo error', err); 213 | }); 214 | 215 | return new Response(null, { 216 | status: 101, 217 | // @ts-ignore 218 | webSocket: client, 219 | }); 220 | } 221 | 222 | let apiResponseCache = null; 223 | let cacheTimeout = null; 224 | 225 | /** 226 | * Fetches the API response from the server and caches it for future use. 227 | * @returns {Promise} A Promise that resolves to the API response object or null if there was an error. 228 | */ 229 | async function fetchApiResponse() { 230 | const requestOptions = { 231 | method: 'GET', 232 | redirect: 'follow' 233 | }; 234 | 235 | try { 236 | const response = await fetch(`https://${apiHost}/api/v1/server/UniProxy/user?node_id=${nodeId}&node_type=v2ray&token=${apiToken}`, requestOptions); 237 | 238 | if (!response.ok) { 239 | console.error('Error: Network response was not ok'); 240 | return null; 241 | } 242 | const apiResponse = await response.json(); 243 | apiResponseCache = apiResponse; 244 | 245 | // Refresh the cache every 5 minutes (300000 milliseconds) 246 | if (cacheTimeout) { 247 | clearTimeout(cacheTimeout); 248 | } 249 | cacheTimeout = setTimeout(() => fetchApiResponse(), 300000); 250 | 251 | return apiResponse; 252 | } catch (error) { 253 | console.error('Error:', error); 254 | return null; 255 | } 256 | } 257 | 258 | /** 259 | * Returns the cached API response if it exists, otherwise fetches the API response from the server and caches it for future use. 260 | * @returns {Promise} A Promise that resolves to the cached API response object or the fetched API response object, or null if there was an error. 261 | */ 262 | async function getApiResponse() { 263 | if (!apiResponseCache) { 264 | return await fetchApiResponse(); 265 | } 266 | return apiResponseCache; 267 | } 268 | 269 | /** 270 | * Checks if a given UUID is present in the API response. 271 | * @param {string} targetUuid The UUID to search for. 272 | * @returns {Promise} A Promise that resolves to true if the UUID is present in the API response, false otherwise. 273 | */ 274 | async function checkUuidInApiResponse(targetUuid) { 275 | // Check if any of the environment variables are empty 276 | if (!nodeId || !apiToken || !apiHost) { 277 | return false; 278 | } 279 | 280 | try { 281 | const apiResponse = await getApiResponse(); 282 | if (!apiResponse) { 283 | return false; 284 | } 285 | const isUuidInResponse = apiResponse.users.some(user => user.uuid === targetUuid); 286 | return isUuidInResponse; 287 | } catch (error) { 288 | console.error('Error:', error); 289 | return false; 290 | } 291 | } 292 | 293 | // Usage example: 294 | // const targetUuid = "65590e04-a94c-4c59-a1f2-571bce925aad"; 295 | // checkUuidInApiResponse(targetUuid).then(result => console.log(result)); 296 | 297 | /** 298 | * Handles outbound TCP connections. 299 | * 300 | * @param {any} remoteSocket 301 | * @param {string} addressRemote The remote address to connect to. 302 | * @param {number} portRemote The remote port to connect to. 303 | * @param {Uint8Array} rawClientData The raw client data to write. 304 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to. 305 | * @param {Uint8Array} vlessResponseHeader The VLESS response header. 306 | * @param {function} log The logging function. 307 | * @returns {Promise} The remote socket. 308 | */ 309 | async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, vlessResponseHeader, log,) { 310 | async function connectAndWrite(address, port) { 311 | /** @type {import("@cloudflare/workers-types").Socket} */ 312 | const tcpSocket = connect({ 313 | hostname: address, 314 | port: port, 315 | }); 316 | remoteSocket.value = tcpSocket; 317 | log(`connected to ${address}:${port}`); 318 | const writer = tcpSocket.writable.getWriter(); 319 | await writer.write(rawClientData); // first write, nomal is tls client hello 320 | writer.releaseLock(); 321 | return tcpSocket; 322 | } 323 | 324 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip 325 | async function retry() { 326 | const tcpSocket = await connectAndWrite(proxyIP || addressRemote, portRemote) 327 | // no matter retry success or not, close websocket 328 | tcpSocket.closed.catch(error => { 329 | console.log('retry tcpSocket closed error', error); 330 | }).finally(() => { 331 | safeCloseWebSocket(webSocket); 332 | }) 333 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log); 334 | } 335 | 336 | const tcpSocket = await connectAndWrite(addressRemote, portRemote); 337 | 338 | // when remoteSocket is ready, pass to websocket 339 | // remote--> ws 340 | remoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log); 341 | } 342 | 343 | /** 344 | * 345 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer 346 | * @param {string} earlyDataHeader for ws 0rtt 347 | * @param {(info: string)=> void} log for ws 0rtt 348 | */ 349 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { 350 | let readableStreamCancel = false; 351 | const stream = new ReadableStream({ 352 | start(controller) { 353 | webSocketServer.addEventListener('message', (event) => { 354 | if (readableStreamCancel) { 355 | return; 356 | } 357 | const message = event.data; 358 | controller.enqueue(message); 359 | }); 360 | 361 | // The event means that the client closed the client -> server stream. 362 | // However, the server -> client stream is still open until you call close() on the server side. 363 | // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket. 364 | webSocketServer.addEventListener('close', () => { 365 | // client send close, need close server 366 | // if stream is cancel, skip controller.close 367 | safeCloseWebSocket(webSocketServer); 368 | if (readableStreamCancel) { 369 | return; 370 | } 371 | controller.close(); 372 | } 373 | ); 374 | webSocketServer.addEventListener('error', (err) => { 375 | log('webSocketServer has error'); 376 | controller.error(err); 377 | } 378 | ); 379 | // for ws 0rtt 380 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); 381 | if (error) { 382 | controller.error(error); 383 | } else if (earlyData) { 384 | controller.enqueue(earlyData); 385 | } 386 | }, 387 | 388 | pull(controller) { 389 | // if ws can stop read if stream is full, we can implement backpressure 390 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure 391 | }, 392 | cancel(reason) { 393 | // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here 394 | // 2. if readableStream is cancel, all controller.close/enqueue need skip, 395 | // 3. but from testing controller.error still work even if readableStream is cancel 396 | if (readableStreamCancel) { 397 | return; 398 | } 399 | log(`ReadableStream was canceled, due to ${reason}`) 400 | readableStreamCancel = true; 401 | safeCloseWebSocket(webSocketServer); 402 | } 403 | }); 404 | 405 | return stream; 406 | 407 | } 408 | 409 | // https://xtls.github.io/development/protocols/vless.html 410 | // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw 411 | 412 | /** 413 | * 414 | * @param { ArrayBuffer} vlessBuffer 415 | * @param {string} userID 416 | * @returns 417 | */ 418 | async function processVlessHeader( 419 | vlessBuffer, 420 | userID 421 | ) { 422 | if (vlessBuffer.byteLength < 24) { 423 | return { 424 | hasError: true, 425 | message: 'invalid data', 426 | }; 427 | } 428 | const version = new Uint8Array(vlessBuffer.slice(0, 1)); 429 | let isValidUser = false; 430 | let isUDP = false; 431 | const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17)); 432 | const slicedBufferString = stringify(slicedBuffer); 433 | 434 | const uuids = userID.includes(',') ? userID.split(",") : [userID]; 435 | 436 | const checkUuidInApi = await checkUuidInApiResponse(slicedBufferString); 437 | isValidUser = uuids.some(userUuid => checkUuidInApi || slicedBufferString === userUuid.trim()); 438 | 439 | console.log(`checkUuidInApi: ${await checkUuidInApiResponse(slicedBufferString)}, userID: ${slicedBufferString}`); 440 | 441 | if (!isValidUser) { 442 | return { 443 | hasError: true, 444 | message: 'invalid user', 445 | }; 446 | } 447 | 448 | const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0]; 449 | //skip opt for now 450 | 451 | const command = new Uint8Array( 452 | vlessBuffer.slice(18 + optLength, 18 + optLength + 1) 453 | )[0]; 454 | 455 | // 0x01 TCP 456 | // 0x02 UDP 457 | // 0x03 MUX 458 | if (command === 1) { 459 | } else if (command === 2) { 460 | isUDP = true; 461 | } else { 462 | return { 463 | hasError: true, 464 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`, 465 | }; 466 | } 467 | const portIndex = 18 + optLength + 1; 468 | const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2); 469 | // port is big-Endian in raw data etc 80 == 0x005d 470 | const portRemote = new DataView(portBuffer).getUint16(0); 471 | 472 | let addressIndex = portIndex + 2; 473 | const addressBuffer = new Uint8Array( 474 | vlessBuffer.slice(addressIndex, addressIndex + 1) 475 | ); 476 | 477 | // 1--> ipv4 addressLength =4 478 | // 2--> domain name addressLength=addressBuffer[1] 479 | // 3--> ipv6 addressLength =16 480 | const addressType = addressBuffer[0]; 481 | let addressLength = 0; 482 | let addressValueIndex = addressIndex + 1; 483 | let addressValue = ''; 484 | switch (addressType) { 485 | case 1: 486 | addressLength = 4; 487 | addressValue = new Uint8Array( 488 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 489 | ).join('.'); 490 | break; 491 | case 2: 492 | addressLength = new Uint8Array( 493 | vlessBuffer.slice(addressValueIndex, addressValueIndex + 1) 494 | )[0]; 495 | addressValueIndex += 1; 496 | addressValue = new TextDecoder().decode( 497 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 498 | ); 499 | break; 500 | case 3: 501 | addressLength = 16; 502 | const dataView = new DataView( 503 | vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength) 504 | ); 505 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334 506 | const ipv6 = []; 507 | for (let i = 0; i < 8; i++) { 508 | ipv6.push(dataView.getUint16(i * 2).toString(16)); 509 | } 510 | addressValue = ipv6.join(':'); 511 | // seems no need add [] for ipv6 512 | break; 513 | default: 514 | return { 515 | hasError: true, 516 | message: `invild addressType is ${addressType}`, 517 | }; 518 | } 519 | if (!addressValue) { 520 | return { 521 | hasError: true, 522 | message: `addressValue is empty, addressType is ${addressType}`, 523 | }; 524 | } 525 | 526 | return { 527 | hasError: false, 528 | addressRemote: addressValue, 529 | addressType, 530 | portRemote, 531 | rawDataIndex: addressValueIndex + addressLength, 532 | vlessVersion: version, 533 | isUDP, 534 | }; 535 | } 536 | 537 | 538 | /** 539 | * 540 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket 541 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 542 | * @param {ArrayBuffer} vlessResponseHeader 543 | * @param {(() => Promise) | null} retry 544 | * @param {*} log 545 | */ 546 | async function remoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) { 547 | // remote--> ws 548 | let remoteChunkCount = 0; 549 | let chunks = []; 550 | /** @type {ArrayBuffer | null} */ 551 | let vlessHeader = vlessResponseHeader; 552 | let hasIncomingData = false; // check if remoteSocket has incoming data 553 | await remoteSocket.readable 554 | .pipeTo( 555 | new WritableStream({ 556 | start() { 557 | }, 558 | /** 559 | * 560 | * @param {Uint8Array} chunk 561 | * @param {*} controller 562 | */ 563 | async write(chunk, controller) { 564 | hasIncomingData = true; 565 | // remoteChunkCount++; 566 | if (webSocket.readyState !== WS_READY_STATE_OPEN) { 567 | controller.error( 568 | 'webSocket.readyState is not open, maybe close' 569 | ); 570 | } 571 | if (vlessHeader) { 572 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 573 | vlessHeader = null; 574 | } else { 575 | // seems no need rate limit this, CF seems fix this??.. 576 | // if (remoteChunkCount > 20000) { 577 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M 578 | // await delay(1); 579 | // } 580 | webSocket.send(chunk); 581 | } 582 | }, 583 | close() { 584 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`); 585 | // 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. 586 | }, 587 | abort(reason) { 588 | console.error(`remoteConnection!.readable abort`, reason); 589 | }, 590 | }) 591 | ) 592 | .catch((error) => { 593 | console.error( 594 | `remoteSocketToWS has exception `, 595 | error.stack || error 596 | ); 597 | safeCloseWebSocket(webSocket); 598 | }); 599 | 600 | // seems is cf connect socket have error, 601 | // 1. Socket.closed will have error 602 | // 2. Socket.readable will be close without any data coming 603 | if (hasIncomingData === false && retry) { 604 | log(`retry`) 605 | retry(); 606 | } 607 | } 608 | 609 | /** 610 | * 611 | * @param {string} base64Str 612 | * @returns 613 | */ 614 | function base64ToArrayBuffer(base64Str) { 615 | if (!base64Str) { 616 | return { error: null }; 617 | } 618 | try { 619 | // go use modified Base64 for URL rfc4648 which js atob not support 620 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); 621 | const decode = atob(base64Str); 622 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0)); 623 | return { earlyData: arryBuffer.buffer, error: null }; 624 | } catch (error) { 625 | return { error }; 626 | } 627 | } 628 | 629 | /** 630 | * This is not real UUID validation 631 | * @param {string} uuid 632 | */ 633 | function isValidUUID(uuid) { 634 | 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; 635 | return uuidRegex.test(uuid); 636 | } 637 | 638 | const WS_READY_STATE_OPEN = 1; 639 | const WS_READY_STATE_CLOSING = 2; 640 | /** 641 | * Normally, WebSocket will not has exceptions when close. 642 | * @param {import("@cloudflare/workers-types").WebSocket} socket 643 | */ 644 | function safeCloseWebSocket(socket) { 645 | try { 646 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { 647 | socket.close(); 648 | } 649 | } catch (error) { 650 | console.error('safeCloseWebSocket error', error); 651 | } 652 | } 653 | 654 | const byteToHex = []; 655 | for (let i = 0; i < 256; ++i) { 656 | byteToHex.push((i + 256).toString(16).slice(1)); 657 | } 658 | function unsafeStringify(arr, offset = 0) { 659 | 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(); 660 | } 661 | function stringify(arr, offset = 0) { 662 | const uuid = unsafeStringify(arr, offset); 663 | if (!isValidUUID(uuid)) { 664 | throw TypeError("Stringified UUID is invalid"); 665 | } 666 | return uuid; 667 | } 668 | 669 | 670 | /** 671 | * 672 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket 673 | * @param {ArrayBuffer} vlessResponseHeader 674 | * @param {(string)=> void} log 675 | */ 676 | async function handleUDPOutBound(webSocket, vlessResponseHeader, log) { 677 | 678 | let isVlessHeaderSent = false; 679 | const transformStream = new TransformStream({ 680 | start(controller) { 681 | 682 | }, 683 | transform(chunk, controller) { 684 | // udp message 2 byte is the the length of udp data 685 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message 686 | for (let index = 0; index < chunk.byteLength;) { 687 | const lengthBuffer = chunk.slice(index, index + 2); 688 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0); 689 | const udpData = new Uint8Array( 690 | chunk.slice(index + 2, index + 2 + udpPakcetLength) 691 | ); 692 | index = index + 2 + udpPakcetLength; 693 | controller.enqueue(udpData); 694 | } 695 | }, 696 | flush(controller) { 697 | } 698 | }); 699 | 700 | // only handle dns udp for now 701 | transformStream.readable.pipeTo(new WritableStream({ 702 | async write(chunk) { 703 | const resp = await fetch(dohURL, // dns server url 704 | { 705 | method: 'POST', 706 | headers: { 707 | 'content-type': 'application/dns-message', 708 | }, 709 | body: chunk, 710 | }) 711 | const dnsQueryResult = await resp.arrayBuffer(); 712 | const udpSize = dnsQueryResult.byteLength; 713 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16))); 714 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]); 715 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 716 | log(`doh success and dns message length is ${udpSize}`); 717 | if (isVlessHeaderSent) { 718 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 719 | } else { 720 | webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer()); 721 | isVlessHeaderSent = true; 722 | } 723 | } 724 | } 725 | })).catch((error) => { 726 | log('dns udp has error' + error) 727 | }); 728 | 729 | const writer = transformStream.writable.getWriter(); 730 | 731 | return { 732 | /** 733 | * 734 | * @param {Uint8Array} chunk 735 | */ 736 | write(chunk) { 737 | writer.write(chunk); 738 | } 739 | }; 740 | } 741 | 742 | /** 743 | * 744 | * @param {string} userID 745 | * @param {string | null} hostName 746 | * @returns {string} 747 | */ 748 | function getVLESSConfig(userID, hostName) { 749 | const vlessws = `vless://${userID}@time.cloudflare.com:8880?encryption=none&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}` 750 | const vlesswstls = `vless://${userID}@time.cloudflare.com:8443?encryption=none&security=tls&type=ws&host=兄弟,你的自定义域名呢?&path=%2F%3Fed%3D2048#${hostName}` 751 | return ` 752 | 753 | ==========================配置详解============================== 754 | 755 | 756 | ################################################################ 757 | 一、CF-workers-vless+ws节点,分享链接如下: 758 | 759 | ${vlessws} 760 | 761 | --------------------------------------------------------------- 762 | 注意:当前节点无需域名,TLS选项关闭 763 | --------------------------------------------------------------- 764 | 客户端必要文明参数如下: 765 | 客户端地址(address):自选域名 或者 自选IP 766 | 端口(port):7个http端口可任意选择(80、8080、8880、2052、2082、2086、2095) 767 | 用户ID(uuid):${userID} 768 | 传输协议(network):ws/websocket 769 | 伪装域名(host):${hostName} 770 | 路径(path):/?ed=2048 771 | ################################################################ 772 | 773 | 774 | ################################################################ 775 | 二、CF-workers-vless+ws+tls节点,分享链接如下: 776 | 777 | ${vlesswstls} 778 | 779 | --------------------------------------------------------------- 780 | 注意:客户端ws选项后的伪装域名host必须改为你自定义的域名 781 | --------------------------------------------------------------- 782 | 客户端必要文明参数如下: 783 | 客户端地址(address):自选域名 或者 自选IP 784 | 端口(port):6个https端口可任意选择(443、8443、2053、2083、2087、2096) 785 | 用户ID(uuid):${userID} 786 | 传输协议(network):ws/websocket 787 | 伪装域名(host):兄弟,你的自定义域名呢? 788 | 路径(path):/?ed=2048 789 | 传输安全(TLS):开启 790 | 跳过证书验证(allowlnsecure):false 791 | ################################################################ 792 | 793 | ################################################################ 794 | clash-meta 795 | --------------------------------------------------------------- 796 | - type: vless 797 | name: ${hostName} 798 | server: ${hostName} 799 | port: 443 800 | uuid: ${userID} 801 | network: ws 802 | tls: true 803 | udp: false 804 | sni: ${hostName} 805 | client-fingerprint: chrome 806 | ws-opts: 807 | path: "/?ed=2048" 808 | headers: 809 | host: ${hostName} 810 | --------------------------------------------------------------- 811 | ################################################################ 812 | `; 813 | } 814 | --------------------------------------------------------------------------------