├── .github └── workflows │ ├── test.yml │ └── obfuscate.yml ├── README.md ├── فارسی.md └── snippets /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Worker Script 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' # 当任何标签被推送时触发 7 | 8 | jobs: 9 | deploy-worker: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Get tag name 14 | id: get_tag 15 | run: | 16 | TAG_NAME=${GITHUB_REF#refs/tags/} 17 | echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT 18 | echo "当前标签: $TAG_NAME" 19 | 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 # 需要完整的Git历史来获取标签信息 24 | 25 | - name: Check if source file exists 26 | run: | 27 | if [ ! -f "少年你相信光吗" ]; then 28 | echo "错误:在项目根目录未找到 '少年你相信光吗' 文件。" 29 | exit 1 30 | fi 31 | echo "成功找到源文件 '少年你相信光吗'" 32 | 33 | - name: Rename file to _worker.js 34 | run: | 35 | cp "少年你相信光吗" "_worker.js" 36 | echo "成功将 '少年你相信光吗' 复制为 '_worker.js'" 37 | echo "注意:文件不会提交到仓库,仅用于 Release" 38 | 39 | - name: Create zip file 40 | run: | 41 | zip Pages.zip _worker.js 42 | echo "成功将 '_worker.js' 压缩为 'Pages.zip'" 43 | 44 | - name: Create GitHub Release 45 | uses: softprops/action-gh-release@v1 46 | with: 47 | tag_name: ${{ steps.get_tag.outputs.tag_name }} 48 | name: Pages ${{ steps.get_tag.outputs.tag_name }} 49 | body: | 50 | ## 部署信息 51 | 52 | - **源文件**: 少年你相信光吗 53 | - **目标文件**: _worker.js 54 | - **压缩文件**: Pages.zip 55 | - **标签**: ${{ steps.get_tag.outputs.tag_name }} 56 | - **部署时间**: ${{ github.event.head_commit.timestamp }} 57 | 58 | ## 文件变更 59 | 60 | 已从 `少年你相信光吗` 生成 `_worker.js` 文件,并压缩为 `Pages.zip`。 61 | draft: false 62 | prerelease: false 63 | files: Pages.zip 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | 67 | - name: Output summary 68 | run: | 69 | echo "## 部署完成" >> $GITHUB_STEP_SUMMARY 70 | echo "- 源文件: 少年你相信光吗" >> $GITHUB_STEP_SUMMARY 71 | echo "- 目标文件: _worker.js" >> $GITHUB_STEP_SUMMARY 72 | echo "- 压缩文件: Pages.zip" >> $GITHUB_STEP_SUMMARY 73 | echo "- 标签: ${{ steps.get_tag.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY 74 | echo "- GitHub Release 已创建" >> $GITHUB_STEP_SUMMARY 75 | -------------------------------------------------------------------------------- /.github/workflows/obfuscate.yml: -------------------------------------------------------------------------------- 1 | name: Generate and Obfuscate Worker Script 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - '**' # 仅匹配分支推送,排除标签推送 8 | paths: 9 | - '明文源吗' 10 | 11 | jobs: 12 | build-and-obfuscate: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '18' 23 | 24 | - name: Install Obfuscator 25 | run: npm install javascript-obfuscator 26 | 27 | - name: Obfuscate from local source file 28 | run: | 29 | cat > obfuscate.js << 'EOF' 30 | const JavaScriptObfuscator = require('javascript-obfuscator'); 31 | const fs = require('fs'); 32 | const path = require('path'); 33 | 34 | const sourceFileName = '明文源吗'; 35 | const outputFileName = '少年你相信光吗'; 36 | const sourceFilePath = path.join(process.cwd(), sourceFileName); 37 | 38 | if (!fs.existsSync(sourceFilePath)) { 39 | console.error('错误:在路径 \'' + sourceFilePath + '\' 未找到源文件。请确保您的仓库根目录有名为 \'明文源吗\' 的文件。'); 40 | process.exit(1); 41 | } 42 | 43 | const originalCode = fs.readFileSync(sourceFilePath, 'utf8'); 44 | 45 | if (!originalCode || originalCode.trim().length === 0) { 46 | console.error('错误:源文件 ' + sourceFileName + ' 为空。'); 47 | process.exit(1); 48 | } 49 | 50 | const obfuscationOptions = { 51 | compact: true, 52 | controlFlowFlattening: false, 53 | controlFlowFlatteningThreshold: 0, 54 | deadCodeInjection: false, 55 | stringArray: true, 56 | stringArrayEncoding: ['base64'], 57 | stringArrayThreshold: 1.0, 58 | stringArrayRotate: true, 59 | stringArrayShuffle: true, 60 | stringArrayWrappersCount: 2, 61 | stringArrayWrappersChainedCalls: false, 62 | stringArrayWrappersParametersMaxCount: 3, 63 | renameGlobals: true, 64 | identifierNamesGenerator: 'mangled-shuffled', 65 | identifierNamesCache: null, 66 | identifiersPrefix: '', 67 | renameProperties: false, 68 | renamePropertiesMode: 'safe', 69 | ignoreImports: false, 70 | target: 'browser', 71 | numbersToExpressions: false, 72 | simplify: false, 73 | splitStrings: true, 74 | splitStringsChunkLength: 1, 75 | transformObjectKeys: false, 76 | unicodeEscapeSequence: true, 77 | selfDefending: false, 78 | debugProtection: false, 79 | debugProtectionInterval: 0, 80 | disableConsoleOutput: true, 81 | domainLock: [] 82 | }; 83 | 84 | const obfuscatedCode = JavaScriptObfuscator.obfuscate(originalCode, obfuscationOptions).getObfuscatedCode(); 85 | 86 | fs.writeFileSync(path.join(process.cwd(), outputFileName), obfuscatedCode, 'utf8'); 87 | console.log('成功将 \'' + sourceFileName + '\' 混淆并保存至 \'' + outputFileName + '\'。'); 88 | EOF 89 | node obfuscate.js 90 | 91 | - name: Commit and push the obfuscated file 92 | run: | 93 | git config --global user.name 'GitHub Actions Bot' 94 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 95 | git add '少年你相信光吗' 96 | if git diff --staged --quiet; then 97 | echo "No changes to commit, the obfuscated file is already up-to-date." 98 | else 99 | git commit -m "部署用这个" 100 | git push 101 | fi 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CFnew - 终端 v2.8 2 | 3 |
4 | 5 | **语言 / زبان:** [🇨🇳 中文](README.md) | [🇮🇷 فارسی](فارسی.md) 6 | 7 |
8 | 9 |
10 | 11 | **多协议支持 · 自定义路径 · 延迟测试优选** 12 | 13 | [![Telegram](https://img.shields.io/badge/Telegram-交流群-blue?logo=telegram)](https://t.me/+ft-zI76oovgwNmRh) 14 | [![Version](https://img.shields.io/badge/Version-2.8-green)]() 15 | [![License](https://img.shields.io/badge/License-MIT-orange)]() 16 | 17 |
18 | 19 | ## ✨ v2.8 核心特性 20 | 21 | - 🎭 **多协议支持** - VLESS + Trojan + xhttp,自由切换 22 | - 🛤️ **自定义路径** - 告别UUID路径,自定义访问地址,支持多级路径 23 | - ⚡ **延迟测试** - 内置IP延迟测试,自动获取机场码,多线程并发测试 24 | - 🔄 **订阅转换** - 自定义转换服务,优选类型细粒度控制 25 | - 🎯 **图形化管理** - KV存储,实时配置,无需重新部署 26 | - 🚀 **API管理** - RESTful API,批量操作,动态更新 27 | - 📱 **多客户端支持** - CLASH、SURGE、SING-BOX、LOON、QUANTUMULT X、V2RAY、Shadowrocket、STASH、NEKORAY、V2RAYNG 28 | - 🚀 **应用唤醒** - 点击按钮自动唤醒对应客户端应用 29 | - 🔍 **自动识别** - 根据User-Agent自动识别并返回对应格式 30 | - 🌐 **多语言支持** - 支持中文和波斯语(伊朗语),根据浏览器语言自动切换 31 | 32 | --- 33 | 34 | ## 🆕 v2.8 更新内容 35 | 36 | - ⚙️ **在线优选展示优化** - 延迟测试结果列表只展示测速成功的优选节点,失败/超时自动隐藏,方便一键添加优选。 37 | - 🌍 **地区/机场国旗显示** - Cloudflare 数据中心映射名称前统一加上对应国家/地区 emoji 国旗,例如 `🇺🇸 圣何塞`、`🇭🇰 香港`。 38 | - 🌐 **URL 获取增强** - “URL获取” 支持逗号分隔多个 URL,并自动解析远程内容中逗号分隔的 IP/节点,统一填入测试列表。 39 | - 🎛️ **界面特效收敛** - 默认关闭矩阵雨、光效闪烁等前端特效,界面更简洁稳定,仍保留绿色终端风格。 40 | - 🧩 **细节与文案优化** - 版本号更新为 v2.8,并对多语言文案和若干内部逻辑做小幅优化。 41 | 42 | --- 43 | 44 | ### 配套工具 45 | 46 | | 类型 | 描述 | 链接 | 47 | | :--- | :--- | :--- | 48 | | **优选工具** | 根据自己的网络环境选择最适合的IP | https://github.com/byJoey/yx-tools/releases | 49 | | **文字教程** | 详细的部署与使用说明博客文章 | [https://joeyblog.net/yuanchuang/1146.html](https://joeyblog.net/yuanchuang/1146.html) | 50 | | **Workers视频教程** | 直观的操作演示和功能讲解 | https://www.youtube.com/watch?v=aYzTr8FafN4 | 51 | | **Pages视频教程** | 直观的操作演示和功能讲解 | https://www.youtube.com/watch?v=JhVxJChDL-E | 52 | | **Snippets视频教程** | 直观的操作演示和功能讲解 | https://www.youtube.com/watch?v=xeFeH3Akcu8 | 53 | 54 | ### 部署 55 | 56 | 加入了千呼万唤的订阅每15分钟自动优选一次 57 | 58 | #### 🔧 基础配置 59 | | 变量名 | 值 | 说明 | 60 | | :--- | :--- | :--- | 61 | | `u` | `你的 UUID` | **必需**。用于访问订阅和配置管理界面 | 62 | | `p` | `proxyip` | **可选**。自定义ProxyIP地址和端口 | 63 | | `s` | `你的SOCKS5地址` | **可选**。用于将所有出站流量通过 SOCKS5 代理转发,格式为 `user:pass@host:port` 或 `host:port` | 64 | | `d` | `自定义路径` | **可选**。自定义订阅访问路径,支持多级路径,如 `/mypath` 或 `/path/to/sub`,不填则使用 UUID 路径。如果路径没有以 `/` 开头,会自动补上 | 65 | | `wk` | `地区代码` | **可选**。手动指定Worker地区,如:`SG`、`HK`、`US`、`JP`等 | 66 | 67 | #### 🎭 协议配置 68 | | 变量名 | 值 | 说明 | 69 | | :--- | :--- | :--- | 70 | | `ev` | `yes/no` | **可选**。启用VLESS协议(默认启用) | 71 | | `et` | `yes/no` | **可选**。启用Trojan协议(默认禁用) | 72 | | `ex` | `yes/no` | **可选**。启用xhttp协议(默认禁用) | 73 | | `tp` | `自定义密码` | **可选**。Trojan协议密码,留空则使用UUID | 74 | 75 | #### 🎯 图形化配置(推荐) 76 | - **KV存储配置**:在Workers中创建KV命名空间,绑定环境变量 `C` 77 | - **访问界面**:部署后访问 `/{你的UUID}` 即可使用图形化配置管理 78 | - **实时生效**:通过界面修改配置无需重新部署,立即生效 79 | 80 | #### 🔧 高级控制 81 | | 变量名 | 值 | 说明 | 82 | | :--- | :--- | :--- | 83 | | `yx` | `自定义优选IP/域名` | **可选**。支持节点命名,格式:`1.1.1.1:443#香港节点,8.8.8.8:53#Google DNS` | 84 | | `yxURL` | `优选IP来源URL` | **可选**。自定义优选IP列表来源URL,留空则使用默认地址 | 85 | | `scu` | `订阅转换地址` | **可选**。自定义订阅转换服务URL,默认:`https://url.v1.mk/sub` | 86 | | `epd` | `yes/no` | **可选**。启用优选域名(默认启用) | 87 | | `epi` | `yes/no` | **可选**。启用优选IP(默认启用) | 88 | | `egi` | `yes/no` | **可选**。启用GitHub默认优选(默认启用) | 89 | | `qj` | `no` | **可选**。降级控制,设置为`no`时启用降级模式:CF直连失败→SOCKS5连接→fallback地址 | 90 | | `dkby` | `yes` | **可选**。TLS控制,设置为`yes`时只生成TLS节点,不生成非TLS节点(如80端口) | 91 | | `yxby` | `yes` | **可选**。优选控制,设置为`yes`时关闭所有优选功能,只使用原生地址,不生成优选IP和域名节点 | 92 | | `rm` | `no` | **可选**。地区匹配控制,设置为`no`时关闭地区智能匹配 | 93 | | `ae` | `yes` | **可选**。API管理开关,设置为`yes`时允许通过API动态管理优选IP(默认关闭) | 94 | 95 | #### 📦 KV存储设置(可选但推荐) 96 | 1. 在Cloudflare Workers中创建KV命名空间 97 | 2. 在Workers设置中绑定KV命名空间,变量名设为 `C` 98 | 3. 重新部署Workers 99 | 4. 访问 `/{你的UUID}` 即可使用图形化配置管理 100 | 101 | #### 🔑 API快速开始 102 | 1. https://github.com/byJoey/yx-tools/releases 优选软件 103 | 2. **开启API功能**:访问 `/{UUID}` 或 `/{自定义路径}` → 找到"允许API管理"→ 选择"开启API管理"→ 保存 104 | 3. **添加单个IP**: 105 | ```bash 106 | # 使用UUID路径 107 | curl -X POST "https://your-worker.workers.dev/{UUID}/api/preferred-ips" \ 108 | -H "Content-Type: application/json" \ 109 | -d '{"ip": "1.2.3.4", "port": 443, "name": "香港节点"}' 110 | 111 | # 使用自定义路径(如果设置了d变量) 112 | curl -X POST "https://your-worker.workers.dev/{自定义路径}/api/preferred-ips" \ 113 | -H "Content-Type: application/json" \ 114 | -d '{"ip": "1.2.3.4", "port": 443, "name": "香港节点"}' 115 | ``` 116 | 4. **批量添加IP**: 117 | ```bash 118 | curl -X POST "https://your-worker.workers.dev/{UUID或自定义路径}/api/preferred-ips" \ 119 | -H "Content-Type: application/json" \ 120 | -d '[ 121 | {"ip": "1.2.3.4", "port": 443, "name": "节点1"}, 122 | {"ip": "5.6.7.8", "port": 8443, "name": "节点2"} 123 | ]' 124 | ``` 125 | 5. **一键清空**: 126 | ```bash 127 | curl -X DELETE "https://your-worker.workers.dev/{UUID或自定义路径}/api/preferred-ips" \ 128 | -H "Content-Type: application/json" \ 129 | -d '{"all": true}' 130 | ``` 131 | 132 | ### 新功能 133 | 134 | #### ⚡ 延迟测试功能(v2.7 起提供,v2.8 优化展示) 135 | - **内置测试工具**:无需外部软件,直接在配置页面测试IP延迟 136 | - **多种IP来源**: 137 | - 手动输入:直接输入IP或域名,支持批量(逗号分隔) 138 | - CF随机IP:从Cloudflare IP段随机生成指定数量的IP 139 | - URL获取:从远程URL获取IP列表进行测试 140 | - **多线程并发**:支持1-50线程并发测试,默认5线程 141 | - **自动获取机场码**:测试成功后自动获取CF机场码(如SJC、LAX等) 142 | - **中文机场名映射**:自动将机场码映射为中文名称(如SJC→圣何塞) 143 | - **真实延迟计算**:自动扣除DNS+TLS握手时间,显示纯网络延迟 144 | - **浏览器记忆**:所有设置(端口、线程数、IP来源、URL等)自动保存到浏览器 145 | - **一键添加优选**:选中测试结果后一键替换优选列表并自动保存到KV 146 | 147 | #### 🎭 多协议支持 148 | - **VLESS协议**:默认启用,完整的VLESS支持 149 | - **Trojan协议**:支持Trojan-WS-TLS,自定义密码或使用UUID 150 | - **xhttp协议**:基于HTTP POST的伪装协议 151 | - **协议混用**:支持同时启用多个协议,客户端自动识别 152 | - **协议切换**:图形化界面一键启用/禁用协议 153 | - **独立保存**:协议配置区域新增独立保存按钮,更方便快捷 154 | - **密码管理**:Trojan支持自定义密码,留空自动使用UUID 155 | 156 | #### 🛤️ 自定义路径(d变量) 157 | - **路径自定义**:不再使用UUID作为路径,设置你喜欢的路径名 158 | - **多级路径支持**:支持多级路径,如 `/path/to/sub`,灵活组织访问结构 159 | - **自动补全**:如果路径没有以 `/` 开头,系统会自动补上 160 | - **安全增强**:自定义路径后UUID路径自动禁用,提高隐蔽性 161 | - **灵活访问**:`https://worker.dev/mypath/sub` 或 `https://worker.dev/path/to/sub/sub` 更简洁易记 162 | - **动态切换**:通过图形界面随时修改访问路径 163 | - **状态显示**:配置页面实时显示当前使用的路径类型 164 | 165 | #### 🎯 图形化配置管理 166 | - **KV存储支持**:使用Cloudflare KV存储持久化配置 167 | - **图形化界面**:访问 `/{你的UUID}` 或 `/{自定义路径}` 即可使用配置管理界面 168 | - **实时配置**:无需重新部署,配置立即生效 169 | - **配置优先级**:KV配置 > 环境变量 > 默认值 170 | 171 | #### 🌐 多语言支持 172 | - **自动语言检测**:根据浏览器语言自动选择中文或波斯语 173 | - **手动语言切换**:页面右上角提供语言选择器,可手动切换 174 | - **持久化存储**:用户选择的语言会保存到浏览器,下次访问自动应用 175 | - **RTL支持**:波斯语版本自动启用从右到左(RTL)布局 176 | - **完整翻译**:所有界面文本、提示信息、按钮标签都已翻译 177 | 178 | #### 🔄 订阅转换控制 179 | - **自定义转换服务**:支持配置自定义订阅转换URL 180 | - **优选类型控制**:细粒度控制优选域名、优选IP、GitHub优选 181 | - **独立开关**:每种优选类型可单独启用/禁用 182 | - **默认设置**:内置优选域名、优选IP、GitHub默认优选全部启用 183 | - **实时更新**:修改配置后立即生效,无需重新部署 184 | 185 | #### 🚀 API动态管理 186 | - **API管理**:通过RESTful API动态管理优选IP,无需修改代码 187 | - **批量上报**:支持一次性批量添加多个优选IP 188 | - **一键清空**:支持清空所有优选IP,快速更新列表 189 | - **安全开关**:默认关闭,需在图形界面手动开启API功能 190 | - **自动合并**:API添加的IP与手动配置的yx变量自动合并 191 | - **实时同步**:API添加的IP立即在配置页面显示 192 | - **API端点**: 193 | - `GET /{UUID或路径}/api/preferred-ips` - 查询优选IP列表 194 | - `POST /{UUID或路径}/api/preferred-ips` - 添加优选IP(支持单个/批量) 195 | - `DELETE /{UUID或路径}/api/preferred-ips` - 删除优选IP(支持单个/全部) 196 | 197 | #### 🌍 手动指定地区 198 | - **地区选择**:支持手动指定Worker地区,覆盖自动检测 199 | - **设置方式**:`wk=SG` 或通过图形化界面选择 200 | - **支持地区**:US、SG、JP、HK、KR、DE、SE、NL、FI、GB 201 | - **智能显示**:系统状态会显示"手动指定地区"而非"自动检测" 202 | 203 | #### 🏷️ 优选节点命名 204 | - **自定义名称**:支持为优选节点设置自定义名称 205 | - **格式支持**:`IP:端口#节点名称` 或 `IP:端口`(使用默认名称) 206 | - **示例**:`1.1.1.1:443#香港节点,8.8.8.8:53#Google DNS` 207 | - **默认格式**:未设置名称时自动生成 `自定义优选-IP:端口` 208 | 209 | #### 📊 系统状态监控 210 | - **实时检测**:显示Worker地区、检测方式、ProxyIP状态 211 | - **智能匹配**:同地区 → 邻近地区 → 其他地区的选择逻辑 212 | - **状态指示**:可视化显示系统运行状态和配置信息 213 | 214 | #### 🔧 高级控制选项 215 | - **地区匹配控制**:`rm=no` 关闭地区智能匹配 216 | - **降级控制**:`qj=no` 启用降级模式(CF直连失败→SOCKS5→fallback) 217 | - **TLS控制**:`dkby=yes` 只生成TLS节点,不生成非TLS节点 218 | - **优选控制**:`yxby=yes` 关闭所有优选功能 219 | 220 | #### 🎨 多客户端支持 221 | - **10种客户端**:支持 CLASH、SURGE、SING-BOX、LOON、QUANTUMULT X、V2RAY、Shadowrocket、STASH、NEKORAY、V2RAYNG 222 | - **自动转换**:根据客户端类型自动生成对应配置 223 | - **一键获取**:图形化界面一键生成订阅链接 224 | - **应用唤醒**:点击客户端按钮自动唤醒对应应用,无需手动复制链接 225 | - **自动识别**:根据User-Agent自动识别客户端类型并返回对应格式 226 | - **协议适配**:不同客户端自动适配最佳协议组合 227 | - **ALPN支持**:所有TLS链接自动包含 `h3,h2,http/1.1` 协议协商 228 | 229 | #### ⚡ 性能优化 230 | - **智能优选**:每15分钟自动优选一次,保持最佳性能 231 | - **容错机制**:多重备用方案,确保服务稳定性 232 | - **缓存优化**:智能缓存机制,减少重复计算 233 | 234 | ### 致谢 235 | 236 | * 本项目基于 [zizifn/edgetunnel](https://github.com/zizifn/edgetunnel) 修改,感谢原作者的贡献。 237 | * 本项目内置ProxyIP部分 来自CM [[cmliu](https://github.com/cmliu)) ,感谢作者的贡献。 238 | * 本项目反代IP来着前端独苗kejiland [[qwer-search](https://github.com/qwer-search)) ,感谢作者的贡献。 239 | * 本项目在线优选接口来自 [[白嫖哥](https://t.me/bestcfipas)) ,感谢作者的贡献。 240 | 241 | 242 | ## Star History 243 | 244 | [![Star History Chart](https://api.star-history.com/svg?repos=byJoey/cfnew&type=Timeline)](https://www.star-history.com/#byJoey/cfnew&Timeline&LogScale) 245 | -------------------------------------------------------------------------------- /فارسی.md: -------------------------------------------------------------------------------- 1 | # CFnew - ترمینال v2.2 2 | 3 |
4 | 5 | **زبان / 语言:** [🇨🇳 中文](README.md) | [🇮🇷 فارسی](فارسی.md) 6 | 7 |
8 | 9 |
10 | 11 | **پشتیبانی چند پروتکل · مسیر سفارشی · بهبود اختلاط کد** 12 | 13 | [![Telegram](https://img.shields.io/badge/Telegram-گروه_تبادل_نظرات-blue?logo=telegram)](https://t.me/+ft-zI76oovgwNmRh) 14 | [![Version](https://img.shields.io/badge/Version-2.1-green)]() 15 | [![License](https://img.shields.io/badge/License-MIT-orange)]() 16 | 17 |
18 | 19 | ## ✨ ویژگی‌های اصلی v2.1 20 | 21 | - 🎭 **پشتیبانی چند پروتکل** - VLESS + Trojan + xhttp، تعویض آزاد 22 | - 🛤️ **مسیر سفارشی** - خداحافظی با مسیر UUID، آدرس دسترسی سفارشی، پشتیبانی از مسیرهای چند سطحی 23 | - 🔐 **اختلاط کد** - رمزگذاری کلمات کلیدی، پنهان‌سازی ویژگی‌ها، افزایش امنیت 24 | - 🔄 **تبدیل اشتراک** - سرویس تبدیل سفارشی، کنترل دقیق نوع ترجیحی 25 | - 🎯 **مدیریت گرافیکی** - ذخیره‌سازی KV، پیکربندی بلادرنگ، بدون نیاز به استقرار مجدد 26 | - 🚀 **مدیریت API** - RESTful API، عملیات دسته‌ای، به‌روزرسانی پویا 27 | - 📱 **پشتیبانی چند کلاینت** - CLASH، SURGE، SING-BOX، LOON، QUANTUMULT X، V2RAY، Shadowrocket، STASH، NEKORAY، V2RAYNG 28 | - 🚀 **بیدار کردن برنامه** - با کلیک روی دکمه به طور خودکار برنامه کلاینت مربوطه را بیدار می‌کند 29 | - 🔍 **تشخیص خودکار** - بر اساس User-Agent به طور خودکار تشخیص داده و قالب مربوطه را برمی‌گرداند 30 | 31 | --- 32 | 33 | ## 🆕 محتوای به‌روزرسانی v2.1 34 | 35 | - ✨ اضافه شدن پشتیبانی از کلاینت‌های Shadowrocket، STASH، NEKORAY، V2RAYNG 36 | - ✨ اضافه شدن عملکرد بیدار کردن خودکار برنامه، باز کردن کلاینت با یک کلیک 37 | - ✨ اضافه شدن عملکرد تشخیص خودکار، بازگشت هوشمند قالب کلاینت 38 | - ✨ بهینه‌سازی مسیر سفارشی، پشتیبانی از مسیرهای چند سطحی (مثل `/path/to/sub`) 39 | - ✨ تکمیل پیکربندی ALPN، تمام لینک‌های TLS به طور خودکار شامل `h3,h2,http/1.1` می‌شوند 40 | - ✨ اضافه شدن دکمه ذخیره مستقل پیکربندی پروتکل، مدیریت پیکربندی راحت‌تر 41 | - 🔧 رفع منطق پردازش مسیر، تکمیل خودکار اسلش ابتدایی 42 | 43 | --- 44 | 45 | ### ابزارهای همراه 46 | 47 | | نوع | توضیحات | لینک | 48 | | :--- | :--- | :--- | 49 | | **ابزار ترجیحی** | بر اساس محیط شبکه خود مناسب‌ترین IP را انتخاب کنید | https://github.com/byJoey/yx-tools/releases | 50 | | **آموزش متنی** | مقاله وبلاگ با توضیحات دقیق استقرار و استفاده | [https://joeyblog.net/yuanchuang/1146.html](https://joeyblog.net/yuanchuang/1146.html) | 51 | | **آموزش ویدیویی Workers** | نمایش عملی و توضیح عملکرد | https://www.youtube.com/watch?v=aYzTr8FafN4 | 52 | | **آموزش ویدیویی Pages** | نمایش عملی و توضیح عملکرد | https://www.youtube.com/watch?v=JhVxJChDL-E | 53 | | **آموزش ویدیویی Snippets** | نمایش عملی و توضیح عملکرد | https://www.youtube.com/watch?v=xeFeH3Akcu8 | 54 | 55 | ### استقرار 56 | 57 | اضافه شدن انتخاب خودکار اشتراک هر 15 دقیقه یکبار که هزاران بار درخواست شده بود 58 | 59 | #### 🔧 پیکربندی پایه 60 | | نام متغیر | مقدار | توضیحات | 61 | | :--- | :--- | :--- | 62 | | `u` | `UUID شما` | **الزامی**. برای دسترسی به اشتراک و رابط مدیریت پیکربندی | 63 | | `p` | `proxyip` | **اختیاری**. آدرس و پورت ProxyIP سفارشی | 64 | | `s` | `آدرس SOCKS5 شما` | **اختیاری**. برای ارسال تمام ترافیک خروجی از طریق پروکسی SOCKS5، فرمت: `user:pass@host:port` یا `host:port` | 65 | | `d` | `مسیر سفارشی` | **اختیاری**. مسیر دسترسی اشتراک سفارشی، پشتیبانی از مسیرهای چند سطحی، مثل `/mypath` یا `/path/to/sub`، اگر خالی بگذارید از مسیر UUID استفاده می‌شود. اگر مسیر با `/` شروع نشود، به طور خودکار اضافه می‌شود | 66 | | `wk` | `کد منطقه` | **اختیاری**. تعیین دستی منطقه Worker، مثل: `SG`، `HK`، `US`، `JP` و غیره | 67 | 68 | #### 🎭 پیکربندی پروتکل (جدید) 69 | | نام متغیر | مقدار | توضیحات | 70 | | :--- | :--- | :--- | 71 | | `ev` | `yes/no` | **اختیاری**. فعال‌سازی پروتکل VLESS (به طور پیش‌فرض فعال است) | 72 | | `et` | `yes/no` | **اختیاری**. فعال‌سازی پروتکل Trojan (به طور پیش‌فرض غیرفعال است) | 73 | | `ex` | `yes/no` | **اختیاری**. فعال‌سازی پروتکل xhttp (به طور پیش‌فرض غیرفعال است) | 74 | | `tp` | `رمز عبور سفارشی` | **اختیاری**. رمز عبور پروتکل Trojan، اگر خالی بگذارید از UUID استفاده می‌شود | 75 | 76 | #### 🎯 پیکربندی گرافیکی (توصیه می‌شود) 77 | - **پیکربندی ذخیره‌سازی KV**: در Workers یک فضای نام KV ایجاد کنید، متغیر محیطی `C` را متصل کنید 78 | - **دسترسی به رابط**: پس از استقرار به `/{UUID شما}` دسترسی پیدا کنید تا از مدیریت پیکربندی گرافیکی استفاده کنید 79 | - **اعمال بلادرنگ**: تغییر پیکربندی از طریق رابط بدون نیاز به استقرار مجدد، بلافاصله اعمال می‌شود 80 | 81 | #### 🔧 کنترل پیشرفته 82 | | نام متغیر | مقدار | توضیحات | 83 | | :--- | :--- | :--- | 84 | | `yx` | `IP/دامنه ترجیحی سفارشی` | **اختیاری**. پشتیبانی از نامگذاری گره، فرمت: `1.1.1.1:443#گره هنگ‌کنگ,8.8.8.8:53#Google DNS` | 85 | | `yxURL` | `URL منبع IP ترجیحی` | **اختیاری**. URL منبع لیست IP ترجیحی سفارشی، اگر خالی بگذارید از آدرس پیش‌فرض استفاده می‌شود | 86 | | `scu` | `آدرس تبدیل اشتراک` | **اختیاری**. URL سرویس تبدیل اشتراک سفارشی، پیش‌فرض: `https://url.v1.mk/sub` | 87 | | `epd` | `yes/no` | **اختیاری**. فعال‌سازی دامنه ترجیحی (به طور پیش‌فرض فعال است) | 88 | | `epi` | `yes/no` | **اختیاری**. فعال‌سازی IP ترجیحی (به طور پیش‌فرض فعال است) | 89 | | `egi` | `yes/no` | **اختیاری**. فعال‌سازی ترجیح پیش‌فرض GitHub (به طور پیش‌فرض فعال است) | 90 | | `qj` | `no` | **اختیاری**. کنترل کاهش سطح، وقتی `no` تنظیم شود حالت کاهش سطح فعال می‌شود: اتصال مستقیم CF ناموفق → اتصال SOCKS5 → آدرس fallback | 91 | | `dkby` | `yes` | **اختیاری**. کنترل TLS، وقتی `yes` تنظیم شود فقط گره‌های TLS تولید می‌شوند، گره‌های غیرTLS تولید نمی‌شوند (مثل پورت 80) | 92 | | `yxby` | `yes` | **اختیاری**. کنترل ترجیحی، وقتی `yes` تنظیم شود تمام عملکردهای ترجیحی خاموش می‌شوند، فقط از آدرس اصلی استفاده می‌شود، گره‌های IP و دامنه ترجیحی تولید نمی‌شوند | 93 | | `rm` | `no` | **اختیاری**. کنترل تطبیق منطقه، وقتی `no` تنظیم شود تطبیق هوشمند منطقه خاموش می‌شود | 94 | | `ae` | `yes` | **اختیاری**. کلید مدیریت API، وقتی `yes` تنظیم شود اجازه مدیریت پویای IP ترجیحی از طریق API داده می‌شود (به طور پیش‌فرض خاموش است) | 95 | 96 | #### 📦 تنظیمات ذخیره‌سازی KV (اختیاری اما توصیه می‌شود) 97 | 1. در Cloudflare Workers یک فضای نام KV ایجاد کنید 98 | 2. در تنظیمات Workers فضای نام KV را متصل کنید، نام متغیر را `C` تنظیم کنید 99 | 3. Workers را دوباره استقرار دهید 100 | 4. به `/{UUID شما}` دسترسی پیدا کنید تا از مدیریت پیکربندی گرافیکی استفاده کنید 101 | 102 | #### 🔑 شروع سریع API 103 | 1. https://github.com/byJoey/yx-tools/releases نرم‌افزار ترجیحی 104 | 2. **فعال‌سازی عملکرد API**: به `/{UUID}` یا `/{مسیر سفارشی}` دسترسی پیدا کنید → "اجازه مدیریت API" را پیدا کنید → "فعال‌سازی مدیریت API" را انتخاب کنید → ذخیره 105 | 3. **افزودن IP تک**: 106 | ```bash 107 | # استفاده از مسیر UUID 108 | curl -X POST "https://your-worker.workers.dev/{UUID}/api/preferred-ips" \ 109 | -H "Content-Type: application/json" \ 110 | -d '{"ip": "1.2.3.4", "port": 443, "name": "گره هنگ‌کنگ"}' 111 | 112 | # استفاده از مسیر سفارشی (اگر متغیر d تنظیم شده باشد) 113 | curl -X POST "https://your-worker.workers.dev/{مسیر سفارشی}/api/preferred-ips" \ 114 | -H "Content-Type: application/json" \ 115 | -d '{"ip": "1.2.3.4", "port": 443, "name": "گره هنگ‌کنگ"}' 116 | ``` 117 | 4. **افزودن دسته‌ای IP**: 118 | ```bash 119 | curl -X POST "https://your-worker.workers.dev/{UUID یا مسیر سفارشی}/api/preferred-ips" \ 120 | -H "Content-Type: application/json" \ 121 | -d '[ 122 | {"ip": "1.2.3.4", "port": 443, "name": "گره 1"}, 123 | {"ip": "5.6.7.8", "port": 8443, "name": "گره 2"} 124 | ]' 125 | ``` 126 | 5. **پاک کردن یک کلیکی**: 127 | ```bash 128 | curl -X DELETE "https://your-worker.workers.dev/{UUID یا مسیر سفارشی}/api/preferred-ips" \ 129 | -H "Content-Type: application/json" \ 130 | -d '{"all": true}' 131 | ``` 132 | 133 | ### عملکرد جدید 134 | 135 | #### 🎭 پشتیبانی چند پروتکل (تقویت شده در v2.1) 136 | - **پروتکل VLESS**: به طور پیش‌فرض فعال، پشتیبانی کامل VLESS 137 | - **پروتکل Trojan**: پشتیبانی از Trojan-WS-TLS، رمز عبور سفارشی یا استفاده از UUID 138 | - **پروتکل xhttp**: پروتکل استتار مبتنی بر HTTP POST 139 | - **استفاده ترکیبی پروتکل**: پشتیبانی از فعال‌سازی همزمان چند پروتکل، کلاینت به طور خودکار تشخیص می‌دهد 140 | - **تعویض پروتکل**: فعال/غیرفعال کردن پروتکل با یک کلیک در رابط گرافیکی 141 | - **ذخیره مستقل**: منطقه پیکربندی پروتکل دکمه ذخیره مستقل جدید اضافه شده، راحت‌تر و سریع‌تر (جدید در v2.1) 142 | - **مدیریت رمز عبور**: Trojan از رمز عبور سفارشی پشتیبانی می‌کند، اگر خالی بگذارید به طور خودکار از UUID استفاده می‌شود 143 | 144 | #### 🛤️ مسیر سفارشی (متغیر d) (تقویت شده در v2.1) 145 | - **سفارشی‌سازی مسیر**: دیگر از UUID به عنوان مسیر استفاده نمی‌شود، نام مسیر مورد علاقه خود را تنظیم کنید 146 | - **پشتیبانی از مسیرهای چند سطحی**: پشتیبانی از مسیرهای چند سطحی، مثل `/path/to/sub`، سازماندهی انعطاف‌پذیر ساختار دسترسی (جدید در v2.1) 147 | - **تکمیل خودکار**: اگر مسیر با `/` شروع نشود، سیستم به طور خودکار اضافه می‌کند (جدید در v2.1) 148 | - **افزایش امنیت**: پس از مسیر سفارشی مسیر UUID به طور خودکار غیرفعال می‌شود، افزایش پنهان‌کاری 149 | - **دسترسی انعطاف‌پذیر**: `https://worker.dev/mypath/sub` یا `https://worker.dev/path/to/sub/sub` ساده‌تر و راحت‌تر برای یادآوری 150 | - **تعویض پویا**: از طریق رابط گرافیکی هر زمان مسیر دسترسی را تغییر دهید 151 | - **نمایش وضعیت**: صفحه پیکربندی وضعیت نوع مسیر فعلی را به صورت بلادرنگ نمایش می‌دهد 152 | 153 | #### 🎯 مدیریت پیکربندی گرافیکی 154 | - **پشتیبانی ذخیره‌سازی KV**: استفاده از ذخیره‌سازی Cloudflare KV برای پیکربندی پایدار 155 | - **رابط گرافیکی**: به `/{UUID شما}` یا `/{مسیر سفارشی}` دسترسی پیدا کنید تا از رابط مدیریت پیکربندی استفاده کنید 156 | - **پیکربندی بلادرنگ**: بدون نیاز به استقرار مجدد، پیکربندی بلافاصله اعمال می‌شود 157 | - **اولویت پیکربندی**: پیکربندی KV > متغیر محیطی > مقدار پیش‌فرض 158 | 159 | #### 🔄 کنترل تبدیل اشتراک (جدید در v2.0) 160 | - **سرویس تبدیل سفارشی**: پشتیبانی از پیکربندی URL تبدیل اشتراک سفارشی 161 | - **کنترل نوع ترجیحی**: کنترل دقیق دامنه ترجیحی، IP ترجیحی، ترجیح GitHub 162 | - **کلید مستقل**: هر نوع ترجیحی می‌تواند به طور جداگانه فعال/غیرفعال شود 163 | - **تنظیمات پیش‌فرض**: دامنه ترجیحی داخلی، IP ترجیحی، ترجیح پیش‌فرض GitHub همه فعال هستند 164 | - **به‌روزرسانی بلادرنگ**: پس از تغییر پیکربندی بلافاصله اعمال می‌شود، بدون نیاز به استقرار مجدد 165 | 166 | #### 🔐 بهبود اختلاط کد (جدید در v2.0) 167 | - **رمزگذاری کلمات کلیدی**: کلمات کلیدی حساس مثل websocket، clash، v2ray با رمزگذاری Base64 168 | - **اختلاط نام متغیر**: تمام نام‌های متغیر پیکربندی به طور یکنواخت از نام‌های کوتاه استفاده می‌کنند (ev/et/ex/tp/scu و غیره) 169 | - **پاکسازی دیباگ**: حذف تمام دستورات دیباگ console 170 | - **پنهان‌سازی ویژگی‌ها**: کاهش 95%+ خطر تطبیق کلمات کلیدی 171 | - **دوست‌دار کاربر**: رابط HTML واضح و قابل خواندن باقی می‌ماند 172 | - **ابزار خودکار**: اسکریپت اختلاط ارائه می‌شود، برای نگهداری بعدی راحت است 173 | 174 | #### 🚀 مدیریت پویای API 175 | - **مدیریت API**: مدیریت پویای IP ترجیحی از طریق RESTful API، بدون نیاز به تغییر کد 176 | - **گزارش دسته‌ای**: پشتیبانی از افزودن دسته‌ای چند IP ترجیحی 177 | - **پاک کردن یک کلیکی**: پشتیبانی از پاک کردن تمام IP ترجیحی، به‌روزرسانی سریع لیست 178 | - **کلید امنیتی**: به طور پیش‌فرض خاموش است، باید عملکرد API را به صورت دستی در رابط گرافیکی فعال کنید 179 | - **ادغام خودکار**: IP های اضافه شده از طریق API با متغیر yx پیکربندی دستی به طور خودکار ادغام می‌شوند 180 | - **همگام‌سازی بلادرنگ**: IP های اضافه شده از طریق API بلافاصله در صفحه پیکربندی نمایش داده می‌شوند 181 | - **نقاط پایانی API**: 182 | - `GET /{UUID یا مسیر}/api/preferred-ips` - پرس‌وجوی لیست IP ترجیحی 183 | - `POST /{UUID یا مسیر}/api/preferred-ips` - افزودن IP ترجیحی (پشتیبانی از تک/دسته‌ای) 184 | - `DELETE /{UUID یا مسیر}/api/preferred-ips` - حذف IP ترجیحی (پشتیبانی از تک/همه) 185 | 186 | 187 | #### 🌍 تعیین دستی منطقه 188 | - **انتخاب منطقه**: پشتیبانی از تعیین دستی منطقه Worker، بازنویسی تشخیص خودکار 189 | - **روش تنظیم**: `wk=SG` یا از طریق رابط گرافیکی انتخاب کنید 190 | - **مناطق پشتیبانی شده**: US، SG، JP، HK، KR، DE، SE، NL، FI، GB 191 | - **نمایش هوشمند**: وضعیت سیستم "تعیین دستی منطقه" را نمایش می‌دهد نه "تشخیص خودکار" 192 | 193 | #### 🏷️ نامگذاری گره ترجیحی 194 | - **نام سفارشی**: پشتیبانی از تنظیم نام سفارشی برای گره ترجیحی 195 | - **پشتیبانی فرمت**: `IP:پورت#نام گره` یا `IP:پورت` (استفاده از نام پیش‌فرض) 196 | - **مثال**: `1.1.1.1:443#گره هنگ‌کنگ,8.8.8.8:53#Google DNS` 197 | - **فرمت پیش‌فرض**: وقتی نام تنظیم نشود به طور خودکار `ترجیح سفارشی-IP:پورت` تولید می‌شود 198 | 199 | #### 📊 نظارت بر وضعیت سیستم 200 | - **تشخیص بلادرنگ**: نمایش منطقه Worker، روش تشخیص، وضعیت ProxyIP 201 | - **تطبیق هوشمند**: منطق انتخاب هم‌منطقه → منطقه مجاور → سایر مناطق 202 | - **نمایش وضعیت**: نمایش بصری وضعیت اجرای سیستم و اطلاعات پیکربندی 203 | 204 | #### 🔧 گزینه‌های کنترل پیشرفته 205 | - **کنترل تطبیق منطقه**: `rm=no` خاموش کردن تطبیق هوشمند منطقه 206 | - **کنترل کاهش سطح**: `qj=no` فعال‌سازی حالت کاهش سطح (اتصال مستقیم CF ناموفق → SOCKS5 → fallback) 207 | - **کنترل TLS**: `dkby=yes` فقط تولید گره‌های TLS، تولید گره‌های غیرTLS نمی‌شود 208 | - **کنترل ترجیحی**: `yxby=yes` خاموش کردن تمام عملکردهای ترجیحی 209 | 210 | #### 🎨 پشتیبانی چند کلاینت (تقویت شده در v2.1) 211 | - **10 نوع کلاینت**: پشتیبانی از CLASH، SURGE، SING-BOX، LOON، QUANTUMULT X، V2RAY، Shadowrocket، STASH، NEKORAY، V2RAYNG (4 کلاینت جدید در v2.1) 212 | - **تبدیل خودکار**: بر اساس نوع کلاینت به طور خودکار پیکربندی مربوطه تولید می‌شود 213 | - **دریافت یک کلیکی**: تولید یک کلیکی لینک اشتراک در رابط گرافیکی 214 | - **بیدار کردن برنامه**: کلیک روی دکمه کلاینت به طور خودکار برنامه مربوطه را بیدار می‌کند، بدون نیاز به کپی دستی لینک (جدید در v2.1) 215 | - **تشخیص خودکار**: بر اساس User-Agent به طور خودکار نوع کلاینت را تشخیص داده و قالب مربوطه را برمی‌گرداند (جدید در v2.1) 216 | - **سازگاری پروتکل**: کلاینت‌های مختلف به طور خودکار با بهترین ترکیب پروتکل سازگار می‌شوند 217 | - **پشتیبانی ALPN**: تمام لینک‌های TLS به طور خودکار شامل مذاکره پروتکل `h3,h2,http/1.1` می‌شوند (جدید در v2.1) 218 | 219 | #### ⚡ بهینه‌سازی عملکرد 220 | - **انتخاب هوشمند**: هر 15 دقیقه یکبار به طور خودکار انتخاب می‌شود، حفظ بهترین عملکرد 221 | - **مکانیسم تحمل خطا**: چندین طرح پشتیبان، تضمین ثبات سرویس 222 | - **بهینه‌سازی کش**: مکانیسم کش هوشمند، کاهش محاسبات تکراری 223 | 224 | ### تشکر 225 | 226 | * این پروژه بر اساس [zizifn/edgetunnel](https://github.com/zizifn/edgetunnel) اصلاح شده است، از مشارکت نویسنده اصلی سپاسگزاریم. 227 | * ProxyIP داخلی این پروژه از CM [[cmliu](https://github.com/cmliu)) است، از مشارکت نویسنده سپاسگزاریم. 228 | * IP پروکسی معکوس این پروژه از kejiland جلوی [[qwer-search](https://github.com/qwer-search)) است، از مشارکت نویسنده سپاسگزاریم. 229 | ## تاریخچه ستاره 230 | 231 | [![Star History Chart](https://api.star-history.com/svg?repos=byJoey/cfnew&type=Timeline)](https://www.star-history.com/#byJoey/cfnew&Timeline&LogScale) 232 | 233 | 234 | -------------------------------------------------------------------------------- /snippets: -------------------------------------------------------------------------------- 1 | import { connect } from 'cloudflare:sockets'; 2 | 3 | // --- 硬编码配置 --- 4 | const authToken = 'f64bdc57-0f54-4705-bf75-cfd646d98c06'; 5 | let fallbackAddress = ''; 6 | const socks5Config = ''; 7 | // 手动指定地区(留空则自动检测,可选值:US、SG、JP、HK、KR、DE、SE、NL、FI、GB) 8 | const manualWorkerRegion = ''; 9 | // D短地址(自定义路径,留空则使用UUID路径,支持多级路径如:mypath 或 path/to/sub) 10 | const customPath = ''; 11 | // GitHub订阅URL(硬编码) 12 | const githubPreferredURL = 'https://raw.githubusercontent.com/qwer-search/bestip/refs/heads/main/kejilandbestip.txt'; 13 | // 启用GitHub优选IP(true启用,false禁用) 14 | const enableGitHubPreferred = true; 15 | // 启用其他优选(域名优选,true启用,false禁用) 16 | const enableOtherPreferred = true; 17 | // API地址配置(订阅转换服务) 18 | const apiBaseUrl = 'https://url.v1.mk/sub'; 19 | 20 | const directDomains = [ 21 | { name: "cloudflare.182682.xyz", domain: "cloudflare.182682.xyz" }, 22 | { name: "speed.marisalnc.com", domain: "speed.marisalnc.com" }, 23 | { domain: "freeyx.cloudflare88.eu.org" }, { domain: "bestcf.top" }, 24 | { domain: "cdn.2020111.xyz" }, { domain: "cfip.cfcdn.vip" }, 25 | { domain: "cf.0sm.com" }, { domain: "cf.090227.xyz" }, 26 | { domain: "cf.zhetengsha.eu.org" }, { domain: "cloudflare.9jy.cc" }, 27 | { domain: "cf.zerone-cdn.pp.ua" }, { domain: "cfip.1323123.xyz" }, 28 | { domain: "cnamefuckxxs.yuchen.icu" }, { domain: "cloudflare-ip.mofashi.ltd" }, 29 | { domain: "115155.xyz" }, { domain: "cname.xirancdn.us" }, 30 | { domain: "f3058171cad.002404.xyz" }, { domain: "8.889288.xyz" }, 31 | { domain: "cdn.tzpro.xyz" }, { domain: "cf.877771.xyz" }, 32 | { domain: "xn--b6gac.eu.org" } 33 | ]; 34 | 35 | const parsedSocks5Config = {}; 36 | const isSocksEnabled = false; 37 | 38 | let enableRegionMatching = true; 39 | let currentWorkerRegion = ''; 40 | 41 | const backupIPs = [ 42 | { domain: 'ProxyIP.US.CMLiussss.net', region: 'US', regionCode: 'US', port: 443 }, 43 | { domain: 'ProxyIP.SG.CMLiussss.net', region: 'SG', regionCode: 'SG', port: 443 }, 44 | { domain: 'ProxyIP.JP.CMLiussss.net', region: 'JP', regionCode: 'JP', port: 443 }, 45 | { domain: 'ProxyIP.HK.CMLiussss.net', region: 'HK', regionCode: 'HK', port: 443 }, 46 | { domain: 'ProxyIP.KR.CMLiussss.net', region: 'KR', regionCode: 'KR', port: 443 }, 47 | { domain: 'ProxyIP.DE.CMLiussss.net', region: 'DE', regionCode: 'DE', port: 443 }, 48 | { domain: 'ProxyIP.SE.CMLiussss.net', region: 'SE', regionCode: 'SE', port: 443 }, 49 | { domain: 'ProxyIP.NL.CMLiussss.net', region: 'NL', regionCode: 'NL', port: 443 }, 50 | { domain: 'ProxyIP.FI.CMLiussss.net', region: 'FI', regionCode: 'FI', port: 443 }, 51 | { domain: 'ProxyIP.GB.CMLiussss.net', region: 'GB', regionCode: 'GB', port: 443 } 52 | ]; 53 | 54 | const E_INVALID_DATA = atob('aW52YWxpZCBkYXRh'); 55 | const E_INVALID_USER = atob('aW52YWxpZCB1c2Vy'); 56 | const E_UNSUPPORTED_CMD = atob('Y29tbWFuZCBpcyBub3Qgc3VwcG9ydGVk'); 57 | const E_UDP_DNS_ONLY = atob('VURQIHByb3h5IG9ubHkgZW5hYmxlIGZvciBETlMgd2hpY2ggaXMgcG9ydCA1Mw=='); 58 | const E_INVALID_ADDR_TYPE = atob('aW52YWxpZCBhZGRyZXNzVHlwZQ=='); 59 | const E_EMPTY_ADDR = atob('YWRkcmVzc1ZhbHVlIGlzIGVtcHR5'); 60 | const E_WS_NOT_OPEN = atob('d2ViU29ja2V0LmVhZHlTdGF0ZSBpcyBub3Qgb3Blbg=='); 61 | const E_INVALID_ID_STR = atob('U3RyaW5naWZpZWQgaWRlbnRpZmllciBpcyBpbnZhbGlk'); 62 | const E_INVALID_SOCKS_ADDR = atob('SW52YWxpZCBTT0NLUyBhZGRyZXNzIGZvcm1hdA=='); 63 | const E_SOCKS_NO_METHOD = atob('bm8gYWNjZXB0YWJsZSBtZXRob2Rz'); 64 | const E_SOCKS_AUTH_NEEDED = atob('c29ja3Mgc2VydmVyIG5lZWRzIGF1dGg='); 65 | const E_SOCKS_AUTH_FAIL = atob('ZmFpbCB0byBhdXRoIHNvY2tzIHNlcnZlcg=='); 66 | const E_SOCKS_CONN_FAIL = atob('ZmFpbCB0byBvcGVuIHNvY2tzIGNvbm5lY3Rpb24='); 67 | 68 | const ADDRESS_TYPE_IPV4 = 1; 69 | const ADDRESS_TYPE_URL = 2; 70 | const ADDRESS_TYPE_IPV6 = 3; 71 | 72 | async function detectWorkerRegion(request) { 73 | try { 74 | const cfCountry = request.cf?.country; 75 | if (cfCountry) { 76 | const countryToRegion = { 77 | 'US': 'US', 'SG': 'SG', 'JP': 'JP', 'HK': 'HK', 'KR': 'KR', 78 | 'DE': 'DE', 'SE': 'SE', 'NL': 'NL', 'FI': 'FI', 'GB': 'GB', 79 | 'CN': 'HK', 'TW': 'HK', 'AU': 'SG', 'CA': 'US', 80 | 'FR': 'DE', 'IT': 'DE', 'ES': 'DE', 'CH': 'DE', 81 | 'AT': 'DE', 'BE': 'NL', 'DK': 'SE', 'NO': 'SE', 'IE': 'GB' 82 | }; 83 | if (countryToRegion[cfCountry]) return countryToRegion[cfCountry]; 84 | } 85 | return 'HK'; 86 | } catch (error) { 87 | return 'HK'; 88 | } 89 | } 90 | 91 | function getNearbyRegions(region) { 92 | const nearbyMap = { 93 | 'US': ['SG', 'JP', 'HK', 'KR'], 94 | 'SG': ['JP', 'HK', 'KR', 'US'], 95 | 'JP': ['SG', 'HK', 'KR', 'US'], 96 | 'HK': ['SG', 'JP', 'KR', 'US'], 97 | 'KR': ['JP', 'HK', 'SG', 'US'], 98 | 'DE': ['NL', 'GB', 'SE', 'FI'], 99 | 'SE': ['DE', 'NL', 'FI', 'GB'], 100 | 'NL': ['DE', 'GB', 'SE', 'FI'], 101 | 'FI': ['SE', 'DE', 'NL', 'GB'], 102 | 'GB': ['DE', 'NL', 'SE', 'FI'] 103 | }; 104 | return nearbyMap[region] || []; 105 | } 106 | 107 | function getAllRegionsByPriority(region) { 108 | const nearbyRegions = getNearbyRegions(region); 109 | const allRegions = ['US', 'SG', 'JP', 'HK', 'KR', 'DE', 'SE', 'NL', 'FI', 'GB']; 110 | return [region, ...nearbyRegions, ...allRegions.filter(r => r !== region && !nearbyRegions.includes(r))]; 111 | } 112 | 113 | function getSmartRegionSelection(workerRegion, availableIPs) { 114 | if (!enableRegionMatching || !workerRegion) return availableIPs; 115 | const priorityRegions = getAllRegionsByPriority(workerRegion); 116 | const sortedIPs = []; 117 | for (const region of priorityRegions) { 118 | const regionIPs = availableIPs.filter(ip => ip.regionCode === region); 119 | sortedIPs.push(...regionIPs); 120 | } 121 | return sortedIPs; 122 | } 123 | 124 | async function getBestBackupIP(workerRegion = '') { 125 | if (backupIPs.length === 0) return null; 126 | const availableIPs = backupIPs.map(ip => ({ ...ip, available: true })); 127 | if (enableRegionMatching && workerRegion) { 128 | const sortedIPs = getSmartRegionSelection(workerRegion, availableIPs); 129 | if (sortedIPs.length > 0) return sortedIPs[0]; 130 | } 131 | return availableIPs[0]; 132 | } 133 | 134 | function parseAddressAndPort(input) { 135 | if (!input) return { address: '', port: null }; 136 | if (input.includes('[') && input.includes(']')) { 137 | const match = input.match(/^\[([^\]]+)\](?::(\d+))?$/); 138 | if (match) { 139 | return { address: match[1], port: match[2] ? parseInt(match[2], 10) : null }; 140 | } 141 | } 142 | const lastColonIndex = input.lastIndexOf(':'); 143 | if (lastColonIndex > 0) { 144 | const address = input.substring(0, lastColonIndex); 145 | const portStr = input.substring(lastColonIndex + 1); 146 | const port = parseInt(portStr, 10); 147 | if (!isNaN(port) && port > 0 && port <= 65535) return { address, port }; 148 | } 149 | return { address: input, port: null }; 150 | } 151 | 152 | export default { 153 | async fetch(request, env, ctx) { 154 | try { 155 | const url = new URL(request.url); 156 | 157 | if (manualWorkerRegion && manualWorkerRegion.trim()) { 158 | currentWorkerRegion = manualWorkerRegion.trim().toUpperCase(); 159 | } else { 160 | currentWorkerRegion = await detectWorkerRegion(request); 161 | } 162 | 163 | let currentFallbackAddress = fallbackAddress; 164 | if (!currentFallbackAddress && currentWorkerRegion) { 165 | const bestBackupIP = await getBestBackupIP(currentWorkerRegion); 166 | if (bestBackupIP) currentFallbackAddress = bestBackupIP.domain + ':' + bestBackupIP.port; 167 | } 168 | 169 | if (request.headers.get('Upgrade') === 'websocket') { 170 | return await handleWsRequest(request, currentFallbackAddress); 171 | } else if (request.method === 'GET') { 172 | if (url.pathname === '/') { 173 | const successHtml = `服务正常

✅ 服务正常

请继续后面的操作。

`; 174 | return new Response(successHtml, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } }); 175 | } 176 | 177 | if (customPath && customPath.trim()) { 178 | const cleanCustomPath = customPath.trim().startsWith('/') ? customPath.trim() : '/' + customPath.trim(); 179 | const normalizedCustomPath = cleanCustomPath.endsWith('/') && cleanCustomPath.length > 1 ? cleanCustomPath.slice(0, -1) : cleanCustomPath; 180 | const normalizedPath = url.pathname.endsWith('/') && url.pathname.length > 1 ? url.pathname.slice(0, -1) : url.pathname; 181 | 182 | if (normalizedPath === normalizedCustomPath) { 183 | return await handleSubscriptionPage(request, authToken); 184 | } 185 | 186 | if (normalizedPath === normalizedCustomPath + '/sub') { 187 | return await handleSubscriptionRequest(request, authToken, url); 188 | } 189 | 190 | if (url.pathname.length > 1 && url.pathname !== '/') { 191 | const user = url.pathname.replace(/\/$/, '').replace('/sub', '').substring(1); 192 | if (isValidFormat(user)) { 193 | return new Response(JSON.stringify({ 194 | error: '访问被拒绝', 195 | message: '当前 Worker 已启用自定义路径模式,UUID 访问已禁用' 196 | }), { 197 | status: 403, 198 | headers: { 'Content-Type': 'application/json; charset=utf-8' } 199 | }); 200 | } 201 | } 202 | } else { 203 | if (url.pathname.length > 1 && url.pathname !== '/' && !url.pathname.includes('/sub')) { 204 | const uuid = url.pathname.replace(/\/$/, '').substring(1); 205 | if (isValidFormat(uuid)) { 206 | if (uuid === authToken) return await handleSubscriptionPage(request, uuid); 207 | return new Response('UUID错误', { status: 403 }); 208 | } 209 | } 210 | 211 | if (url.pathname.includes('/sub')) { 212 | const pathParts = url.pathname.split('/'); 213 | if (pathParts.length === 2 && pathParts[1] === 'sub') { 214 | const uuid = pathParts[0].substring(1); 215 | if (isValidFormat(uuid)) { 216 | if (uuid === authToken) return await handleSubscriptionRequest(request, uuid, url); 217 | return new Response('UUID错误', { status: 403 }); 218 | } 219 | } 220 | } 221 | 222 | if (url.pathname.toLowerCase().includes(`/${authToken}`)) { 223 | return await handleSubscriptionRequest(request, authToken); 224 | } 225 | } 226 | } 227 | return new Response('Not Found', { status: 404 }); 228 | } catch (err) { 229 | return new Response(err.toString(), { status: 500 }); 230 | } 231 | }, 232 | }; 233 | 234 | async function handleSubscriptionPage(request, uuid = null) { 235 | if (!uuid) uuid = authToken; 236 | 237 | const pageHtml = ` 238 | 239 | 240 | 241 | 242 | 订阅中心 243 | 268 | 269 | 270 |
271 |
272 |
代理订阅中心精简版 v2.1
273 |
274 |
275 |

代理订阅中心

276 |

多客户端支持 • 智能优选 • 一键生成

277 |
278 |
279 |

[ 选择客户端 ]

280 |
281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 |
292 |
293 |
294 |
295 |

[ 快速获取 ]

296 | 297 |
298 |
299 |
300 |

[ 相关链接 ]

301 |
302 | GitHub 项目 303 | YouTube @joeyblog 304 |
305 |
306 |
307 | 413 | 414 | `; 415 | 416 | return new Response(pageHtml, { 417 | status: 200, 418 | headers: { 'Content-Type': 'text/html; charset=utf-8' } 419 | }); 420 | } 421 | 422 | async function handleSubscriptionRequest(request, uuid, url = null) { 423 | if (!url) url = new URL(request.url); 424 | 425 | const finalLinks = []; 426 | const workerDomain = url.hostname; 427 | 428 | const nativeList = [{ ip: workerDomain, isp: '原生地址' }]; 429 | finalLinks.push(...generateLinksFromSource(nativeList, uuid, workerDomain)); 430 | 431 | if (enableOtherPreferred) { 432 | const domainList = directDomains.map(d => ({ ip: d.domain, isp: d.name || d.domain })); 433 | finalLinks.push(...generateLinksFromSource(domainList, uuid, workerDomain)); 434 | } 435 | 436 | if (enableGitHubPreferred) { 437 | const newIPList = await fetchAndParseNewIPs(); 438 | if (newIPList.length > 0) finalLinks.push(...generateLinksFromNewIPs(newIPList, uuid, workerDomain)); 439 | } 440 | 441 | if (finalLinks.length === 0) { 442 | const errorRemark = "所有节点获取失败"; 443 | const errorLink = `vless://00000000-0000-0000-0000-000000000000@127.0.0.1:80?encryption=none&security=none&type=ws&host=error.com&path=%2F#${encodeURIComponent(errorRemark)}`; 444 | finalLinks.push(errorLink); 445 | } 446 | 447 | const subscriptionContent = btoa(finalLinks.join('\n')); 448 | 449 | return new Response(subscriptionContent, { 450 | headers: { 451 | 'Content-Type': 'text/plain; charset=utf-8', 452 | 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', 453 | }, 454 | }); 455 | } 456 | 457 | function generateLinksFromSource(list, uuid, workerDomain) { 458 | const httpsPorts = [443]; 459 | const links = []; 460 | const wsPath = '/?ed=2048'; 461 | const proto = 'vless'; 462 | 463 | list.forEach(item => { 464 | const nodeNameBase = item.isp.replace(/\s/g, '_'); 465 | const safeIP = item.ip.includes(':') ? `[${item.ip}]` : item.ip; 466 | 467 | httpsPorts.forEach(port => { 468 | const wsNodeName = `${nodeNameBase}-${port}-WS-TLS`; 469 | const wsParams = new URLSearchParams({ 470 | encryption: 'none', 471 | security: 'tls', 472 | sni: workerDomain, 473 | fp: 'randomized', 474 | type: 'ws', 475 | host: workerDomain, 476 | path: wsPath 477 | }); 478 | links.push(`${proto}://${uuid}@${safeIP}:${port}?${wsParams.toString()}#${encodeURIComponent(wsNodeName)}`); 479 | }); 480 | }); 481 | 482 | return links; 483 | } 484 | 485 | async function fetchAndParseNewIPs() { 486 | const url = githubPreferredURL; 487 | try { 488 | const response = await fetch(url); 489 | if (!response.ok) return []; 490 | const text = await response.text(); 491 | const results = []; 492 | const lines = text.trim().replace(/\r/g, "").split('\n'); 493 | const regex = /^([^:]+):(\d+)#(.*)$/; 494 | 495 | for (const line of lines) { 496 | const trimmedLine = line.trim(); 497 | if (!trimmedLine) continue; 498 | const match = trimmedLine.match(regex); 499 | if (match) { 500 | results.push({ 501 | ip: match[1], 502 | port: parseInt(match[2], 10), 503 | name: match[3].trim() || match[1] 504 | }); 505 | } 506 | } 507 | return results; 508 | } catch (error) { 509 | return []; 510 | } 511 | } 512 | 513 | function generateLinksFromNewIPs(list, uuid, workerDomain) { 514 | const links = []; 515 | const wsPath = '/?ed=2048'; 516 | const proto = 'vless'; 517 | 518 | list.forEach(item => { 519 | const nodeName = item.name; 520 | const safeIP = item.ip.includes(':') ? `[${item.ip}]` : item.ip; 521 | const params = { 522 | encryption: 'none', 523 | security: 'tls', 524 | sni: workerDomain, 525 | fp: 'randomized', 526 | type: 'ws', 527 | host: workerDomain, 528 | path: wsPath 529 | }; 530 | const wsParams = new URLSearchParams(params); 531 | links.push(`${proto}://${uuid}@${safeIP}:${item.port}?${wsParams.toString()}#${encodeURIComponent(nodeName)}`); 532 | }); 533 | 534 | return links; 535 | } 536 | 537 | async function handleWsRequest(request, currentFallbackAddress = null) { 538 | const wsPair = new WebSocketPair(); 539 | const [clientSock, serverSock] = Object.values(wsPair); 540 | serverSock.accept(); 541 | 542 | let remoteConnWrapper = { socket: null }; 543 | let isDnsQuery = false; 544 | 545 | const fbAddr = currentFallbackAddress || fallbackAddress; 546 | 547 | const earlyData = request.headers.get('sec-websocket-protocol') || ''; 548 | const readable = makeReadableStream(serverSock, earlyData); 549 | 550 | readable.pipeTo(new WritableStream({ 551 | async write(chunk) { 552 | if (isDnsQuery) return await forwardUDP(chunk, serverSock, null); 553 | if (remoteConnWrapper.socket) { 554 | const writer = remoteConnWrapper.socket.writable.getWriter(); 555 | await writer.write(chunk); 556 | writer.releaseLock(); 557 | return; 558 | } 559 | const { hasError, message, addressType, port, hostname, rawIndex, version, isUDP } = parseWsPacketHeader(chunk, authToken); 560 | if (hasError) throw new Error(message); 561 | 562 | if (isUDP) { 563 | if (port === 53) isDnsQuery = true; 564 | else throw new Error(E_UDP_DNS_ONLY); 565 | } 566 | const respHeader = new Uint8Array([version[0], 0]); 567 | const rawData = chunk.slice(rawIndex); 568 | 569 | if (isDnsQuery) return forwardUDP(rawData, serverSock, respHeader); 570 | await forwardTCP(addressType, hostname, port, rawData, serverSock, respHeader, remoteConnWrapper, fbAddr); 571 | }, 572 | })).catch((err) => { console.log('WS Stream Error:', err); }); 573 | 574 | return new Response(null, { status: 101, webSocket: clientSock }); 575 | } 576 | 577 | async function forwardTCP(addrType, host, portNum, rawData, ws, respHeader, remoteConnWrapper, fbAddr = null) { 578 | async function connectAndSend(address, port) { 579 | const remoteSock = connect({ hostname: address, port: port }); 580 | const writer = remoteSock.writable.getWriter(); 581 | await writer.write(rawData); 582 | writer.releaseLock(); 583 | return remoteSock; 584 | } 585 | 586 | async function retryConnection() { 587 | let fallbackHost, fallbackPort; 588 | if (fbAddr && fbAddr.trim()) { 589 | const parsed = parseAddressAndPort(fbAddr); 590 | fallbackHost = parsed.address; 591 | fallbackPort = parsed.port || portNum; 592 | } else if (fallbackAddress && fallbackAddress.trim()) { 593 | const parsed = parseAddressAndPort(fallbackAddress); 594 | fallbackHost = parsed.address; 595 | fallbackPort = parsed.port || portNum; 596 | } else { 597 | const bestBackupIP = await getBestBackupIP(currentWorkerRegion); 598 | fallbackHost = bestBackupIP ? bestBackupIP.domain : host; 599 | fallbackPort = bestBackupIP ? bestBackupIP.port : portNum; 600 | } 601 | const newSocket = await connectAndSend(fallbackHost || host, fallbackPort); 602 | remoteConnWrapper.socket = newSocket; 603 | newSocket.closed.catch(() => { }).finally(() => closeSocketQuietly(ws)); 604 | connectStreams(newSocket, ws, respHeader, null); 605 | } 606 | 607 | try { 608 | const initialSocket = await connectAndSend(host, portNum); 609 | remoteConnWrapper.socket = initialSocket; 610 | connectStreams(initialSocket, ws, respHeader, retryConnection); 611 | } catch (err) { 612 | console.log('Initial connection failed, trying fallback:', err); 613 | retryConnection(); 614 | } 615 | } 616 | 617 | function parseWsPacketHeader(chunk, token) { 618 | if (chunk.byteLength < 24) return { hasError: true, message: E_INVALID_DATA }; 619 | const version = new Uint8Array(chunk.slice(0, 1)); 620 | if (formatIdentifier(new Uint8Array(chunk.slice(1, 17))) !== token) return { hasError: true, message: E_INVALID_USER }; 621 | const optLen = new Uint8Array(chunk.slice(17, 18))[0]; 622 | const cmd = new Uint8Array(chunk.slice(18 + optLen, 19 + optLen))[0]; 623 | let isUDP = false; 624 | if (cmd === 1) { } 625 | else if (cmd === 2) { isUDP = true; } 626 | else { return { hasError: true, message: E_UNSUPPORTED_CMD }; } 627 | 628 | const portIdx = 19 + optLen; 629 | const port = new DataView(chunk.slice(portIdx, portIdx + 2)).getUint16(0); 630 | let addrIdx = portIdx + 2, addrLen = 0, addrValIdx = addrIdx + 1, hostname = ''; 631 | const addressType = new Uint8Array(chunk.slice(addrIdx, addrValIdx))[0]; 632 | 633 | switch (addressType) { 634 | case ADDRESS_TYPE_IPV4: 635 | addrLen = 4; 636 | hostname = new Uint8Array(chunk.slice(addrValIdx, addrValIdx + addrLen)).join('.'); 637 | break; 638 | case ADDRESS_TYPE_URL: 639 | addrLen = new Uint8Array(chunk.slice(addrValIdx, addrValIdx + 1))[0]; 640 | addrValIdx += 1; 641 | hostname = new TextDecoder().decode(chunk.slice(addrValIdx, addrValIdx + addrLen)); 642 | break; 643 | case ADDRESS_TYPE_IPV6: 644 | addrLen = 16; 645 | const ipv6 = []; 646 | const ipv6View = new DataView(chunk.slice(addrValIdx, addrValIdx + addrLen)); 647 | for (let i = 0; i < 8; i++) ipv6.push(ipv6View.getUint16(i * 2).toString(16)); 648 | hostname = ipv6.join(':'); 649 | break; 650 | default: 651 | return { hasError: true, message: `${E_INVALID_ADDR_TYPE}: ${addressType}` }; 652 | } 653 | 654 | if (!hostname) return { hasError: true, message: `${E_EMPTY_ADDR}: ${addressType}` }; 655 | return { hasError: false, addressType, port, hostname, isUDP, rawIndex: addrValIdx + addrLen, version }; 656 | } 657 | 658 | function makeReadableStream(socket, earlyDataHeader) { 659 | let cancelled = false; 660 | return new ReadableStream({ 661 | start(controller) { 662 | socket.addEventListener('message', (event) => { if (!cancelled) controller.enqueue(event.data); }); 663 | socket.addEventListener('close', () => { if (!cancelled) { closeSocketQuietly(socket); controller.close(); } }); 664 | socket.addEventListener('error', (err) => controller.error(err)); 665 | const { earlyData, error } = base64ToArray(earlyDataHeader); 666 | if (error) controller.error(error); 667 | else if (earlyData) controller.enqueue(earlyData); 668 | }, 669 | cancel() { cancelled = true; closeSocketQuietly(socket); } 670 | }); 671 | } 672 | 673 | async function connectStreams(remoteSocket, webSocket, headerData, retryFunc) { 674 | let header = headerData, hasData = false; 675 | await remoteSocket.readable.pipeTo( 676 | new WritableStream({ 677 | async write(chunk, controller) { 678 | hasData = true; 679 | if (webSocket.readyState !== 1) controller.error(E_WS_NOT_OPEN); 680 | if (header) { 681 | webSocket.send(await new Blob([header, chunk]).arrayBuffer()); 682 | header = null; 683 | } else { 684 | webSocket.send(chunk); 685 | } 686 | }, 687 | abort(reason) { console.error("Readable aborted:", reason); }, 688 | }) 689 | ).catch((error) => { console.error("Stream connection error:", error); closeSocketQuietly(webSocket); }); 690 | 691 | if (!hasData && retryFunc) retryFunc(); 692 | } 693 | 694 | async function forwardUDP(udpChunk, webSocket, respHeader) { 695 | try { 696 | const tcpSocket = connect({ hostname: '8.8.4.4', port: 53 }); 697 | let vlessHeader = respHeader; 698 | const writer = tcpSocket.writable.getWriter(); 699 | await writer.write(udpChunk); 700 | writer.releaseLock(); 701 | await tcpSocket.readable.pipeTo(new WritableStream({ 702 | async write(chunk) { 703 | if (webSocket.readyState === 1) { 704 | if (vlessHeader) { 705 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 706 | vlessHeader = null; 707 | } else { 708 | webSocket.send(chunk); 709 | } 710 | } 711 | }, 712 | })); 713 | } catch (error) { 714 | console.error(`DNS forward error: ${error.message}`); 715 | } 716 | } 717 | 718 | function base64ToArray(b64Str) { 719 | if (!b64Str) return { error: null }; 720 | try { 721 | b64Str = b64Str.replace(/-/g, '+').replace(/_/g, '/'); 722 | return { earlyData: Uint8Array.from(atob(b64Str), (c) => c.charCodeAt(0)).buffer, error: null }; 723 | } catch (error) { 724 | return { error }; 725 | } 726 | } 727 | 728 | function isValidFormat(uuid) { 729 | return /^[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); 730 | } 731 | 732 | function closeSocketQuietly(socket) { 733 | try { if (socket.readyState === 1 || socket.readyState === 2) socket.close(); } 734 | catch (error) { } 735 | } 736 | 737 | const hexTable = Array.from({ length: 256 }, (v, i) => (i + 256).toString(16).slice(1)); 738 | 739 | function formatIdentifier(arr, offset = 0) { 740 | const id = ( 741 | hexTable[arr[offset]] + hexTable[arr[offset + 1]] + hexTable[arr[offset + 2]] + hexTable[arr[offset + 3]] + "-" + 742 | hexTable[arr[offset + 4]] + hexTable[arr[offset + 5]] + "-" + 743 | hexTable[arr[offset + 6]] + hexTable[arr[offset + 7]] + "-" + 744 | hexTable[arr[offset + 8]] + hexTable[arr[offset + 9]] + "-" + 745 | hexTable[arr[offset + 10]] + hexTable[arr[offset + 11]] + hexTable[arr[offset + 12]] + hexTable[arr[offset + 13]] + 746 | hexTable[arr[offset + 14]] + hexTable[arr[offset + 15]] 747 | ).toLowerCase(); 748 | if (!isValidFormat(id)) throw new TypeError(E_INVALID_ID_STR); 749 | return id; 750 | } 751 | --------------------------------------------------------------------------------