├── .github └── workflows │ ├── cf.yml │ ├── create.yml │ ├── obfuscator.yml │ └── sync.yml ├── .gitignore ├── LICENSE ├── README.md ├── _worker.js ├── image ├── image-1.png ├── image-2.png ├── image-3.png ├── image.png └── logo.png ├── index.js ├── package.json └── wrangler.toml /.github/workflows/cf.yml: -------------------------------------------------------------------------------- 1 | name: ⛅ CF Worker 2 | on: 3 | # docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow 4 | # docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#workflow_dispatch 5 | # docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#onworkflow_dispatchinputs 6 | workflow_dispatch: 7 | # github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/ 8 | # docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs 9 | inputs: 10 | # docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onworkflow_dispatchinputs 11 | environment: 12 | description: 'wrangler env to deploy to' 13 | required: true 14 | default: 'dev' 15 | type: choice 16 | options: 17 | - dev 18 | - prod 19 | - one 20 | commit: 21 | description: 'git tip commit to deploy' 22 | default: 'main' 23 | required: true 24 | 25 | push: 26 | # TODO: inputs.environment and inputs.commit 27 | branches: 28 | - "main" 29 | tags: 30 | - "v*" 31 | paths-ignore: 32 | - ".github/**" 33 | - "!.github/workflows/cf.yml" 34 | - ".env.example" 35 | - ".eslintrc.cjs" 36 | - ".prettierignore" 37 | - "fly.toml" 38 | - "README.md" 39 | - "node.Dockerfile" 40 | - "deno.Dockerfile" 41 | - "import_map.json" 42 | - ".vscode/*" 43 | - ".husky/*" 44 | - ".prettierrc.json" 45 | - "LICENSE" 46 | - "run" 47 | repository_dispatch: 48 | 49 | env: 50 | GIT_REF: ${{ github.event.inputs.commit || github.ref }} 51 | # default is 'dev' which is really empty/no env 52 | WORKERS_ENV: '' 53 | 54 | jobs: 55 | deploy: 56 | name: 🚀 Deploy worker 57 | runs-on: ubuntu-latest 58 | timeout-minutes: 60 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v3.3.0 62 | with: 63 | ref: ${{ env.GIT_REF }} 64 | fetch-depth: 0 65 | 66 | - name: 🛸 Env? 67 | # 'dev' env deploys to default WORKERS_ENV, which is, '' (an empty string) 68 | if: github.event.inputs.environment == 'prod' || github.event.inputs.environment == 'one' 69 | run: | 70 | echo "WORKERS_ENV=${WENV}" >> $GITHUB_ENV 71 | echo "COMMIT_SHA=${COMMIT_SHA}" >> $GITHUB_ENV 72 | shell: bash 73 | env: 74 | WENV: ${{ github.event.inputs.environment }} 75 | COMMIT_SHA: ${{ github.sha }} 76 | 77 | - name: 🎱 Tag? 78 | # docs.github.com/en/actions/learn-github-actions/contexts#github-context 79 | if: github.ref_type == 'tag' 80 | run: | 81 | echo "WORKERS_ENV=${WENV}" >> $GITHUB_ENV 82 | echo "COMMIT_SHA=${COMMIT_SHA}" >> $GITHUB_ENV 83 | shell: bash 84 | env: 85 | # tagged deploys always deploy to prod 86 | WENV: 'prod' 87 | COMMIT_SHA: ${{ github.sha }} 88 | 89 | # npm (and node16) are installed by wrangler-action in a pre-job setup 90 | - name: 🏗 Get dependencies 91 | run: npm i 92 | 93 | - name: 📚 Wrangler publish 94 | # github.com/cloudflare/wrangler-action 95 | uses: cloudflare/wrangler-action@2.0.0 96 | with: 97 | apiToken: ${{ secrets.CF_API_TOKEN }} 98 | # input overrides env-defaults, regardless 99 | environment: ${{ env.WORKERS_ENV }} 100 | env: 101 | CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }} 102 | GIT_COMMIT_ID: ${{ env.GIT_REF }} 103 | 104 | - name: 🎤 Notice 105 | run: | 106 | echo "::notice::Deployed to ${WORKERS_ENV} / ${GIT_REF} @ ${COMMIT_SHA}" 107 | -------------------------------------------------------------------------------- /.github/workflows/create.yml: -------------------------------------------------------------------------------- 1 | name: "🌲 Create many branches" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | create_branches: 8 | runs-on: ubuntu-latest 9 | name: "🌲 Create and push branches" 10 | env: 11 | EMAIL: ${{ secrets.GH_EMAIL }} 12 | NAME: ${{ secrets.GH_USERNAME }} 13 | TOKEN: ${{ secrets.TOKEN }} 14 | REMOTE_NAME: origin 15 | BRANCH_BASE_NAME: proxyip 16 | BRANCH_COUNT: 300 17 | 18 | steps: 19 | - name: ⚙️ Set up Git config 20 | run: | 21 | git config --global user.email "${{ env.EMAIL }}" 22 | git config --global user.name "${{ env.NAME }}" 23 | 24 | - name: 📦 Clone repository 25 | run: | 26 | git clone https://${{ env.TOKEN }}@github.com/${{github.repository}}.git 27 | cd $(echo ${{github.repository}} | awk -F'/' '{print $2}') 28 | 29 | - name: 🌿 Create and push branches 30 | run: | 31 | cd $(echo ${{github.repository}} | awk -F'/' '{print $2}') 32 | for ((i=1; i<=${{ env.BRANCH_COUNT }}; i++)); do 33 | branch_name="${{ env.BRANCH_BASE_NAME }}${i}" 34 | git branch $branch_name 35 | git checkout $branch_name 36 | git commit --allow-empty -m "Create branch ${branch_name}" 37 | git push ${{ env.REMOTE_NAME }} $branch_name 38 | git checkout main 39 | done 40 | -------------------------------------------------------------------------------- /.github/workflows/obfuscator.yml: -------------------------------------------------------------------------------- 1 | name: Obfuscate and Commit 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | obfuscate: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Use Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: "16" 23 | 24 | - name: Install dependencies 25 | run: npm install -g javascript-obfuscator 26 | 27 | - name: Obfuscate code 28 | run: | 29 | javascript-obfuscator index.js --output _worker.js \ 30 | --compact true \ 31 | --control-flow-flattening true \ 32 | --control-flow-flattening-threshold 1 \ 33 | --dead-code-injection true \ 34 | --dead-code-injection-threshold 1 \ 35 | --identifier-names-generator hexadecimal \ 36 | --rename-globals true \ 37 | --string-array true \ 38 | --string-array-encoding 'rc4' \ 39 | --string-array-threshold 1 \ 40 | --transform-object-keys true \ 41 | --unicode-escape-sequence true 42 | 43 | - name: Commit changes 44 | run: | 45 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 46 | git config --local user.name "github-actions[bot]" 47 | git add _worker.js 48 | git commit -m "Obfuscate _worker.js" || echo "No changes to commit" 49 | 50 | - name: Push changes 51 | uses: ad-m/github-push-action@master 52 | with: 53 | github_token: ${{ secrets.GITHUB_TOKEN }} 54 | branch: ${{ github.ref }} 55 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: "🔄 Sync all branches with main" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | sync_branches: 8 | runs-on: ubuntu-latest 9 | name: "🔄 Sync branches with main" 10 | env: 11 | EMAIL: ${{ secrets.GH_EMAIL }} 12 | NAME: ${{ secrets.GH_USERNAME }} 13 | TOKEN: ${{ secrets.TOKEN }} 14 | REMOTE_NAME: origin 15 | BRANCH_BASE_NAME: proxyip 16 | BRANCH_COUNT: 200 17 | 18 | steps: 19 | - name: 🛠 Set up Git config 20 | run: | 21 | git config --global user.email "${{ env.EMAIL }}" 22 | git config --global user.name "${{ env.NAME }}" 23 | 24 | - name: 📥 Clone repository 25 | run: | 26 | git clone https:///${{ env.TOKEN }}@github.com/${{github.repository}}.git 27 | cd $(echo ${{github.repository}} | awk -F'/' '{print $2}') 28 | 29 | - name: 🔄 Sync and push branches 30 | run: | 31 | cd $(echo ${{github.repository}} | awk -F'/' '{print $2}') 32 | git checkout main 33 | git pull ${REMOTE_NAME} main 34 | for ((i=1; i<=${{ env.BRANCH_COUNT }}; i++)); do 35 | branch_name="${{ env.BRANCH_BASE_NAME }}${i}" 36 | git checkout $branch_name 37 | git merge main --no-edit 38 | git push ${REMOTE_NAME} $branch_name 39 | git checkout main 40 | done 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .wrangler 5 | # Dependency directories 6 | node_modules/ 7 | 8 | # Environment variables 9 | .env 10 | 11 | # Build output 12 | build/ 13 | dist/ 14 | 15 | # IDE files 16 | .vscode/ 17 | .idea/ 18 | *.sublime-project 19 | *.sublime-workspace 20 | 21 | # Miscellaneous 22 | .DS_Store 23 | Thumbs.db 24 | .env 25 | node_modules/ 26 | package-lock.json 27 | socks5.js 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 3Kmfi6HP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EDtunnel 2 | 3 |

4 | edgetunnel 5 |

6 | 7 | EDtunnel 是一个基于 Cloudflare Workers 和 Pages 的代理工具,支持多种协议和配置选项。 8 | 9 | EDtunnel is a proxy tool based on Cloudflare Workers and Pages, supporting multiple protocols and configuration options. 10 | 11 | [![Repository](https://img.shields.io/badge/View%20on-GitHub-blue.svg)](https://github.com/6Kmfi6HP/EDtunnel) 12 | [![Telegram](https://img.shields.io/badge/Discuss-Telegram-blue.svg)](https://t.me/edtunnel) 13 | 14 | ## ✨ 特性 | Features 15 | 16 | - 支持 Cloudflare Workers 和 Pages 部署 17 | - 支持多 UUID 配置 18 | - 支持自定义代理 IP 和端口 19 | - 支持 SOCKS5 代理 20 | - 提供自动配置订阅链接 21 | - 支持 URL 查询参数覆盖配置 22 | - 简单易用的部署流程 23 | 24 | - Support for Cloudflare Workers and Pages deployment 25 | - Multiple UUID configuration support 26 | - Custom proxy IP and port support 27 | - SOCKS5 proxy support 28 | - Automatic configuration subscription link 29 | - URL query parameter configuration override support 30 | - Simple and easy deployment process 31 | 32 | ## 🚀 快速部署 | Quick Deployment 33 | 34 | ### 在 Pages.dev 部署 | Deploy on Pages.dev 35 | 36 | 1. 观看部署教程视频 | Watch deployment tutorial video: 37 | [YouTube Tutorial](https://www.youtube.com/watch?v=8I-yTNHB0aw) 38 | 39 | 2. 克隆此仓库并在 Cloudflare Pages 中部署 | Clone this repository and deploy in Cloudflare Pages 40 | 41 | ### 在 Worker.dev 部署 | Deploy on Worker.dev 42 | 43 | 1. 从[这里](https://github.com/6Kmfi6HP/EDtunnel/blob/main/_worker.js)复制 `_worker.js` 代码 | Copy `_worker.js` code from [here](https://github.com/6Kmfi6HP/EDtunnel/blob/main/_worker.js) 44 | 45 | 2. 或者点击下方按钮一键部署 | Or click the button below to deploy directly: 46 | 47 | [![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/6Kmfi6HP/EDtunnel) 48 | 49 | ## ⚙️ 配置说明 | Configuration Guide 50 | 51 | ### 环境变量配置 | Environment Variables 52 | 53 | | 变量名 (Variable) | 是否必需 (Required) | 示例 (Example) | 说明 (Description) | 54 | |------------------|-------------------|---------------|-------------------| 55 | | `UUID` | 否 (No) | 单个 (Single): `12345678-1234-1234-1234-123456789012`
多个 (Multiple): `uuid1,uuid2,uuid3` | 用户识别码 / User identification | 56 | | `PROXYIP` | 否 (No) | `1.1.1.1` 或 (or) `example.com`
多个 (Multiple): `1.1.1.1:9443,2.2.2.2:8443` | 自定义代理IP和端口 / Custom proxy IP and port | 57 | | `SOCKS5` | 否 (No) | `user:pass@host:port`
多个 (Multiple): `user1:pass1@host1:port1,user2:pass2@host2:port2` | SOCKS5代理配置 / SOCKS5 proxy configuration | 58 | | `SOCKS5_RELAY` | 否 (No) | `true` 或 (or) `false` | 启用SOCKS5流量转发 / Enable SOCKS5 traffic relay | 59 | 60 | ### URL查询参数配置 | URL Query Parameter Configuration 61 | 62 | 您可以使用URL查询参数直接覆盖环境变量配置,这些参数的优先级高于环境变量。出于安全考虑,UUID 不能通过 URL 查询参数设置。 63 | 64 | You can use URL query parameters to directly override environment variable configurations. These parameters have higher priority than environment variables. For security reasons, UUID cannot be set via URL query parameters. 65 | 66 | | 查询参数 (Query Parameter) | 对应环境变量 (Corresponding ENV) | 示例 (Example) | 说明 (Description) | 67 | |--------------------------|--------------------------------|---------------|-------------------| 68 | | `proxyip` | `PROXYIP` | `?proxyip=1.1.1.1:443` | 覆盖代理IP和端口 / Override proxy IP and port | 69 | | `socks5` | `SOCKS5` | `?socks5=user:pass@host:port` | 覆盖SOCKS5代理配置 / Override SOCKS5 proxy configuration | 70 | | `socks5_relay` | `SOCKS5_RELAY` | `?socks5_relay=true` | 覆盖SOCKS5转发设置 / Override SOCKS5 relay setting | 71 | 72 | > **安全说明**: UUID 必须通过环境变量或配置文件设置,不能通过 URL 参数设置,以防止未授权修改用户身份。 73 | > **Security Note**: UUID must be set via environment variables or configuration files, not through URL parameters, to prevent unauthorized identity modifications. 74 | 75 | #### 使用示例 | Usage Examples: 76 | 77 | 1. 临时更改代理IP | Temporarily change proxy IP: 78 | ``` 79 | https://your-domain.workers.dev/?proxyip=another-proxy-ip:port 80 | ``` 81 | 82 | 2. 组合多个参数 | Combine multiple parameters: 83 | ``` 84 | https://your-domain.workers.dev/?proxyip=1.1.1.1:443&socks5_relay=true 85 | ``` 86 | 87 | 3. 应用于特定路径 | Apply to specific paths: 88 | ``` 89 | https://your-domain.workers.dev/sub/your-uuid?proxyip=1.1.1.1:443 90 | ``` 91 | 92 | #### 特性说明 | Feature Notes: 93 | 94 | - 优先级:URL参数 > 环境变量 > 默认值 95 | - 临时性:这些更改仅对当前请求有效,不会永久修改配置 96 | - 可组合:可以组合多个参数实现复杂配置调整 97 | - 适用场景:快速测试、临时切换配置、第三方系统动态调用 98 | 99 | - Priority: URL parameters > Environment Variables > Default Values 100 | - Temporary: These changes only apply to the current request and do not permanently modify configurations 101 | - Combinable: Multiple parameters can be combined for complex configuration adjustments 102 | - Use cases: Quick testing, temporary configuration switching, dynamic calls from third-party systems 103 | 104 | #### URL格式注意事项 | URL Format Notes: 105 | 106 | - 确保查询参数使用正确的格式: `?参数名=值`。问号 `?` 不应被URL编码(`%3F`)。 107 | - 如果您看到像 `/%3Fproxyip=value` 这样的URL,这不会正确工作,应改为 `/?proxyip=value`。 108 | - 本项目现已支持处理编码在路径中的查询参数,但建议使用标准格式以确保最佳兼容性。 109 | 110 | - Ensure query parameters use the correct format: `?parameter=value`. The question mark `?` should not be URL encoded (`%3F`). 111 | - If you see URLs like `/%3Fproxyip=value`, this won't work correctly. Use `/?proxyip=value` instead. 112 | - This project now supports handling query parameters encoded in the path, but using the standard format is recommended for best compatibility. 113 | 114 | ### 非443端口配置 | Non-443 Port Configuration 115 | 116 | 1. 访问 (Visit) `https://proxyip.edtunnel.best/` 117 | 2. 输入 (Enter) `ProxyIP:proxyport` 并点击检查 (and click Check) 118 | 3. 当显示 (When showing) `Proxy IP: true` 时可用 (it's available) 119 | 4. 在 Worker 中配置 (Configure in Worker): `PROXYIP=211.230.110.231:50008` 120 | 121 | 注意:带端口的代理IP可能在某些仅支持HTTP的Cloudflare站点上无效。 122 | Note: Proxy IPs with ports may not work on HTTP-only Cloudflare sites. 123 | 124 | ### UUID 配置方法 | UUID Configuration 125 | 126 | #### 方法一 | Method 1 127 | 在 `wrangler.toml` 文件中设置(不推荐在公共仓库中使用) 128 | Set in `wrangler.toml` file (not recommended for public repositories) 129 | 130 | ```toml 131 | [vars] 132 | UUID = "your-uuid-here" 133 | ``` 134 | 135 | #### 方法二 | Method 2 136 | 在 Cloudflare Dashboard 的环境变量中设置(推荐方式) 137 | Set in Cloudflare Dashboard environment variables (recommended method) 138 | 139 | ## ⚠️ 重要提示:多项配置分隔符 | Important Note: Multiple Configuration Separator 140 | 141 | 所有多项配置必须使用英文逗号(,)分隔,不能使用中文逗号(,) 142 | All multiple configurations MUST use English comma(,) as separator, NOT Chinese comma(,) 143 | 144 | ✅ 正确示例 | Correct Examples: 145 | ```bash 146 | # UUID多个配置 | Multiple UUID 147 | UUID=uuid1,uuid2,uuid3 148 | 149 | # SOCKS5多个代理 | Multiple SOCKS5 proxies 150 | SOCKS5=192.168.1.1:1080,192.168.1.2:1080 151 | 152 | # PROXYIP多个地址 | Multiple PROXYIP 153 | PROXYIP=1.1.1.1:443,2.2.2.2:443 154 | ``` 155 | 156 | ❌ 错误示例 | Wrong Examples: 157 | ```bash 158 | # 错误:使用中文逗号 | Wrong: Using Chinese comma 159 | UUID=uuid1,uuid2,uuid3 160 | 161 | # 错误:使用中文逗号 | Wrong: Using Chinese comma 162 | SOCKS5=192.168.1.1:1080,192.168.1.2:1080 163 | ``` 164 | 165 | ## 📱 快速使用 | Quick Start 166 | 167 | ### 自动配置订阅 | Auto Configuration Subscribe 168 | 169 | 使用以下链接获取自动配置 | Use the following link for auto configuration: 170 | ``` 171 | https://sub.xf.free.hr/auto 172 | ``` 173 | 174 | ### 查看配置 | View Configuration 175 | 176 | - 访问您的域名 | Visit your domain: `https://your-domain.pages.dev` 177 | - 使用特定UUID | Use specific UUID: `/sub/[uuid]` 178 | - 查看完整配置 | View full configuration: 直接访问域名根路径 (visit domain root path) 179 | - 获取订阅内容 | Get subscription content: 访问 `/sub/[uuid]` (visit `/sub/[uuid]`) 180 | 181 | ## 🔧 高级配置 | Advanced Configuration 182 | 183 | ### 多UUID支持 | Multiple UUID Support 184 | 185 | 您可以通过以下方式配置多个UUID | You can configure multiple UUIDs in these ways: 186 | 187 | 1. 环境变量方式 | Via environment variables: 188 | ``` 189 | UUID=uuid1,uuid2,uuid3 190 | ``` 191 | 192 | 2. 配置文件方式 | Via configuration file: 193 | ```toml 194 | [vars] 195 | UUID = "uuid1,uuid2,uuid3" 196 | ``` 197 | 198 | ### SOCKS5代理配置 | SOCKS5 Proxy Configuration 199 | 200 | 支持以下格式 | Supports the following formats: 201 | - 基础格式 | Basic format: `host:port` 202 | - 认证格式 | Authentication format: `username:password@host:port` 203 | - 多代理格式(使用英文逗号分隔)| Multiple proxies (separated by English comma): `proxy1,proxy2,proxy3` 204 | 205 | #### 配置示例 | Configuration Examples: 206 | 207 | 1. 单个代理 | Single Proxy: 208 | ```bash 209 | # 基础格式 | Basic format 210 | SOCKS5=192.168.1.1:1080 211 | 212 | # 带认证格式 | With authentication 213 | SOCKS5=user:pass@192.168.1.1:1080 214 | ``` 215 | 216 | 2. 多个代理(使用英文逗号分隔)| Multiple Proxies (separated by English comma): 217 | ```bash 218 | # 多个基础代理 | Multiple basic proxies 219 | SOCKS5=192.168.1.1:1080,192.168.1.2:1080,192.168.1.3:1080 220 | 221 | # 多个带认证代理 | Multiple proxies with authentication 222 | SOCKS5=user1:pass1@host1:port1,user2:pass2@host2:port2 223 | 224 | # 混合格式 | Mixed format 225 | SOCKS5=192.168.1.1:1080,user:pass@192.168.1.2:1080,192.168.1.3:1080 226 | ``` 227 | 228 | #### SOCKS5 代理负载均衡 | SOCKS5 Proxy Load Balancing 229 | 230 | 当配置多个代理时,系统会自动进行负载均衡: 231 | When multiple proxies are configured, the system will automatically perform load balancing: 232 | 233 | - 随机选择 | Random selection 234 | - 自动故障转移 | Automatic failover 235 | - 支持混合认证方式 | Support mixed authentication methods 236 | 237 | #### SOCKS5_RELAY 设置 | SOCKS5_RELAY Settings 238 | 239 | 启用 SOCKS5 全局转发 | Enable SOCKS5 global relay: 240 | ```bash 241 | SOCKS5_RELAY=true 242 | ``` 243 | 244 | 注意事项 | Notes: 245 | - 确保代理服务器稳定可用 | Ensure proxy servers are stable and available 246 | - 建议使用私有代理以提高安全性 | Recommend using private proxies for better security 247 | - 多代理配置时使用英文逗号分隔 | Use commas to separate multiple proxies 248 | - 支持动态添加和移除代理 | Support dynamic proxy addition and removal 249 | 250 | ## 🚨 注意事项 | Notes 251 | 252 | - 带端口的代理IP可能在某些仅HTTP的Cloudflare站点上无效 253 | - 多UUID配置时使用英文逗号分隔 254 | - 建议通过环境变量设置敏感信息 255 | - 定期更新以获取最新功能和安全修复 256 | 257 | - Proxy IPs with ports may not work on HTTP-only Cloudflare sites 258 | - Use commas to separate multiple UUIDs 259 | - Recommend setting sensitive information via environment variables 260 | - Update regularly for latest features and security fixes 261 | 262 | ## 🔧 环境变量设置 | Environment Variable Settings 263 | 264 | ### Workers.dev 设置 | Workers.dev Settings 265 | 在 Workers 设置页面配置环境变量 | Configure environment variables in Workers settings page 266 | ![workers](image/image-1.png) 267 | 268 | ### Pages.dev 设置 | Pages.dev Settings 269 | 在 Pages 设置页面配置环境变量 | Configure environment variables in Pages settings page 270 | ![pages](image/image-2.png) 271 | 272 | ## 💬 获取帮助 | Get Help 273 | 274 | - Telegram 群组 | Telegram Group: [EDtunnel Group](https://t.me/edtunnel) 275 | - GitHub 仓库 | Repository: [EDtunnel](https://github.com/6Kmfi6HP/EDtunnel) 276 | - 问题反馈 | Issue Report: [创建新问题 | Create New Issue](https://github.com/6Kmfi6HP/EDtunnel/issues) 277 | - 功能建议 | Feature Request: [提交建议 | Submit Request](https://github.com/6Kmfi6HP/EDtunnel/discussions) 278 | 279 | ## 📝 贡献指南 | Contributing 280 | 281 | 欢迎提交 Pull Request 来改进项目!请确保: 282 | Welcome Pull Requests to improve the project! Please ensure: 283 | 284 | 1. 代码符合项目规范 | Code follows project standards 285 | 2. 添加必要的测试 | Add necessary tests 286 | 3. 更新相关文档 | Update relevant documentation 287 | 4. 描述清楚改动原因 | Clearly describe the reasons for changes 288 | 289 | ## 📜 许可证 | License 290 | 291 | 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 292 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 293 | 294 | ## Star History 295 | 296 | 297 | 298 | 299 | 300 | Star History Chart 301 | 302 | 303 | -------------------------------------------------------------------------------- /image/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6Kmfi6HP/EDtunnel/dc72e4fbdc771e1de05f1f37a35ab4e8c99fcec0/image/image-1.png -------------------------------------------------------------------------------- /image/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6Kmfi6HP/EDtunnel/dc72e4fbdc771e1de05f1f37a35ab4e8c99fcec0/image/image-2.png -------------------------------------------------------------------------------- /image/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6Kmfi6HP/EDtunnel/dc72e4fbdc771e1de05f1f37a35ab4e8c99fcec0/image/image-3.png -------------------------------------------------------------------------------- /image/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6Kmfi6HP/EDtunnel/dc72e4fbdc771e1de05f1f37a35ab4e8c99fcec0/image/image.png -------------------------------------------------------------------------------- /image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/6Kmfi6HP/EDtunnel/dc72e4fbdc771e1de05f1f37a35ab4e8c99fcec0/image/logo.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // EDtunnel - A Cloudflare Worker-based VLESS Proxy with WebSocket Transport 2 | // @ts-ignore 3 | import { connect } from 'cloudflare:sockets'; 4 | 5 | // ====================================== 6 | // Configuration 7 | // ====================================== 8 | 9 | /** 10 | * User configuration and settings 11 | * Generate UUID: [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()" 12 | */ 13 | let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4'; 14 | 15 | /** 16 | * Array of proxy server addresses with ports 17 | * Format: ['hostname:port', 'hostname:port'] 18 | */ 19 | const proxyIPs = ['cdn.xn--b6gac.eu.org:443', 'cdn-all.xn--b6gac.eu.org:443']; 20 | 21 | // Randomly select a proxy server from the pool 22 | let proxyIP = proxyIPs[Math.floor(Math.random() * proxyIPs.length)]; 23 | let proxyPort = proxyIP.includes(':') ? proxyIP.split(':')[1] : '443'; 24 | 25 | // Alternative configurations: 26 | // Single proxy IP: let proxyIP = 'cdn.xn--b6gac.eu.org'; 27 | // IPv6 example: let proxyIP = "[2a01:4f8:c2c:123f:64:5:6810:c55a]" 28 | 29 | /** 30 | * SOCKS5 proxy configuration 31 | * Format: 'username:password@host:port' or 'host:port' 32 | */ 33 | let socks5Address = ''; 34 | 35 | /** 36 | * SOCKS5 relay mode 37 | * When true: All traffic is proxied through SOCKS5 38 | * When false: Only Cloudflare IPs use SOCKS5 39 | */ 40 | let socks5Relay = false; 41 | 42 | if (!isValidUUID(userID)) { 43 | throw new Error('uuid is not valid'); 44 | } 45 | 46 | let parsedSocks5Address = {}; 47 | let enableSocks = false; 48 | 49 | /** 50 | * Main handler for the Cloudflare Worker. Processes incoming requests and routes them appropriately. 51 | * @param {import("@cloudflare/workers-types").Request} request - The incoming request object 52 | * @param {Object} env - Environment variables containing configuration 53 | * @param {string} env.UUID - User ID for authentication 54 | * @param {string} env.PROXYIP - Proxy server IP address 55 | * @param {string} env.SOCKS5 - SOCKS5 proxy configuration 56 | * @param {string} env.SOCKS5_RELAY - SOCKS5 relay mode flag 57 | * @returns {Promise} Response object 58 | */ 59 | export default { 60 | /** 61 | * @param {import("@cloudflare/workers-types").Request} request 62 | * @param {{UUID: string, PROXYIP: string, SOCKS5: string, SOCKS5_RELAY: string}} env 63 | * @param {import("@cloudflare/workers-types").ExecutionContext} _ctx 64 | * @returns {Promise} 65 | */ 66 | async fetch(request, env, _ctx) { 67 | try { 68 | const { UUID, PROXYIP, SOCKS5, SOCKS5_RELAY } = env; 69 | const url = new URL(request.url); 70 | 71 | // 为当前请求创建配置副本,避免修改全局变量 72 | const requestConfig = { 73 | userID: UUID || userID, 74 | socks5Address: SOCKS5 || socks5Address, 75 | socks5Relay: SOCKS5_RELAY === 'true' || socks5Relay, 76 | proxyIP: null, 77 | proxyPort: null, 78 | enableSocks: false, 79 | parsedSocks5Address: {} 80 | }; 81 | 82 | // 获取正常URL参数 83 | let urlPROXYIP = url.searchParams.get('proxyip'); 84 | let urlSOCKS5 = url.searchParams.get('socks5'); 85 | let urlSOCKS5_RELAY = url.searchParams.get('socks5_relay'); 86 | 87 | // 检查编码在路径中的参数 88 | if (!urlPROXYIP && !urlSOCKS5 && !urlSOCKS5_RELAY) { 89 | const encodedParams = parseEncodedQueryParams(url.pathname); 90 | urlPROXYIP = urlPROXYIP || encodedParams.proxyip; 91 | urlSOCKS5 = urlSOCKS5 || encodedParams.socks5; 92 | urlSOCKS5_RELAY = urlSOCKS5_RELAY || encodedParams.socks5_relay; 93 | } 94 | 95 | // 验证proxyip格式 96 | if (urlPROXYIP) { 97 | // 验证格式: domain:port 或 ip:port,支持逗号分隔 98 | const proxyPattern = /^([a-zA-Z0-9][-a-zA-Z0-9.]*(\.[a-zA-Z0-9][-a-zA-Z0-9.]*)+|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[0-9a-fA-F:]+\]):\d{1,5}$/; 99 | const proxyAddresses = urlPROXYIP.split(',').map(addr => addr.trim()); 100 | const isValid = proxyAddresses.every(addr => proxyPattern.test(addr)); 101 | if (!isValid) { 102 | console.warn('无效的proxyip格式:', urlPROXYIP); 103 | urlPROXYIP = null; 104 | } 105 | } 106 | 107 | // 验证socks5格式 108 | if (urlSOCKS5) { 109 | // 基本验证 - 支持逗号分隔的多个地址 110 | const socks5Pattern = /^(([^:@]+:[^:@]+@)?[a-zA-Z0-9][-a-zA-Z0-9.]*(\.[a-zA-Z0-9][-a-zA-Z0-9.]*)+|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d{1,5}$/; 111 | const socks5Addresses = urlSOCKS5.split(',').map(addr => addr.trim()); 112 | const isValid = socks5Addresses.every(addr => socks5Pattern.test(addr)); 113 | if (!isValid) { 114 | console.warn('无效的socks5格式:', urlSOCKS5); 115 | urlSOCKS5 = null; 116 | } 117 | } 118 | 119 | // 应用URL参数到当前请求的配置 120 | requestConfig.socks5Address = urlSOCKS5 || requestConfig.socks5Address; 121 | requestConfig.socks5Relay = urlSOCKS5_RELAY === 'true' || requestConfig.socks5Relay; 122 | 123 | // 记录参数值,用于调试 124 | console.log('配置参数:', requestConfig.userID, requestConfig.socks5Address, requestConfig.socks5Relay, urlPROXYIP); 125 | 126 | // Handle proxy configuration for the current request 127 | const proxyConfig = handleProxyConfig(urlPROXYIP || PROXYIP); 128 | requestConfig.proxyIP = proxyConfig.ip; 129 | requestConfig.proxyPort = proxyConfig.port; 130 | 131 | // 记录最终使用的代理设置 132 | console.log('使用代理:', requestConfig.proxyIP, requestConfig.proxyPort); 133 | 134 | if (requestConfig.socks5Address) { 135 | try { 136 | const selectedSocks5 = selectRandomAddress(requestConfig.socks5Address); 137 | requestConfig.parsedSocks5Address = socks5AddressParser(selectedSocks5); 138 | requestConfig.enableSocks = true; 139 | } catch (err) { 140 | console.log(err.toString()); 141 | requestConfig.enableSocks = false; 142 | } 143 | } 144 | 145 | const userIDs = requestConfig.userID.includes(',') ? requestConfig.userID.split(',').map(id => id.trim()) : [requestConfig.userID]; 146 | const host = request.headers.get('Host'); 147 | const requestedPath = url.pathname.substring(1); // Remove leading slash 148 | const matchingUserID = userIDs.length === 1 ? 149 | (requestedPath === userIDs[0] || 150 | requestedPath === `sub/${userIDs[0]}` || 151 | requestedPath === `bestip/${userIDs[0]}` ? userIDs[0] : null) : 152 | userIDs.find(id => { 153 | const patterns = [id, `sub/${id}`, `bestip/${id}`]; 154 | return patterns.some(pattern => requestedPath.startsWith(pattern)); 155 | }); 156 | 157 | if (request.headers.get('Upgrade') !== 'websocket') { 158 | if (url.pathname === '/cf') { 159 | return new Response(JSON.stringify(request.cf, null, 4), { 160 | status: 200, 161 | headers: { "Content-Type": "application/json;charset=utf-8" }, 162 | }); 163 | } 164 | 165 | if (matchingUserID) { 166 | if (url.pathname === `/${matchingUserID}` || url.pathname === `/sub/${matchingUserID}`) { 167 | const isSubscription = url.pathname.startsWith('/sub/'); 168 | const proxyAddresses = PROXYIP ? PROXYIP.split(',').map(addr => addr.trim()) : requestConfig.proxyIP; 169 | const content = isSubscription ? 170 | GenSub(matchingUserID, host, proxyAddresses) : 171 | getConfig(matchingUserID, host, proxyAddresses); 172 | 173 | return new Response(content, { 174 | status: 200, 175 | headers: { 176 | "Content-Type": isSubscription ? 177 | "text/plain;charset=utf-8" : 178 | "text/html; charset=utf-8" 179 | }, 180 | }); 181 | } else if (url.pathname === `/bestip/${matchingUserID}`) { 182 | return fetch(`https://bestip.06151953.xyz/auto?host=${host}&uuid=${matchingUserID}&path=/`, { headers: request.headers }); 183 | } 184 | } 185 | return handleDefaultPath(url, request); 186 | } else { 187 | return await ProtocolOverWSHandler(request, requestConfig); 188 | } 189 | } catch (err) { 190 | return new Response(err.toString()); 191 | } 192 | }, 193 | }; 194 | 195 | /** 196 | * Handles default path requests when no specific route matches. 197 | * Generates and returns a cloud drive interface HTML page. 198 | * @param {URL} url - The URL object of the request 199 | * @param {Request} request - The incoming request object 200 | * @returns {Response} HTML response with cloud drive interface 201 | */ 202 | async function handleDefaultPath(url, request) { 203 | const host = request.headers.get('Host'); 204 | const DrivePage = ` 205 | 206 | 207 | 208 | 209 | 210 | ${host} - Cloud Drive 211 | 319 | 320 | 321 |
322 |

Cloud Drive

323 |

Welcome to your personal cloud storage. Here are your uploaded files:

324 | 325 |
    326 |
327 |
328 |
📁
329 |

Upload a File

330 |

Drag and drop a file here or click to select

331 | 332 |
333 |
334 |
335 | 438 | 439 | 440 | `; 441 | 442 | // 返回伪装的网盘页面 443 | return new Response(DrivePage, { 444 | headers: { 445 | "content-type": "text/html;charset=UTF-8", 446 | }, 447 | }); 448 | } 449 | 450 | /** 451 | * Handles protocol over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the protocol header. 452 | * @param {import("@cloudflare/workers-types").Request} request - The incoming request object 453 | * @param {Object} config - The configuration for this request 454 | * @returns {Promise} WebSocket response 455 | */ 456 | async function ProtocolOverWSHandler(request, config = null) { 457 | // 如果没有传入配置,使用全局配置 458 | if (!config) { 459 | config = { 460 | userID, 461 | socks5Address, 462 | socks5Relay, 463 | proxyIP, 464 | proxyPort, 465 | enableSocks, 466 | parsedSocks5Address 467 | }; 468 | } 469 | 470 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */ 471 | // @ts-ignore 472 | const webSocketPair = new WebSocketPair(); 473 | const [client, webSocket] = Object.values(webSocketPair); 474 | 475 | webSocket.accept(); 476 | 477 | let address = ''; 478 | let portWithRandomLog = ''; 479 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => { 480 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || ''); 481 | }; 482 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || ''; 483 | 484 | const readableWebSocketStream = MakeReadableWebSocketStream(webSocket, earlyDataHeader, log); 485 | 486 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/ 487 | let remoteSocketWapper = { 488 | value: null, 489 | }; 490 | let isDns = false; 491 | 492 | // ws --> remote 493 | readableWebSocketStream.pipeTo(new WritableStream({ 494 | async write(chunk, controller) { 495 | if (isDns) { 496 | return await handleDNSQuery(chunk, webSocket, null, log); 497 | } 498 | if (remoteSocketWapper.value) { 499 | const writer = remoteSocketWapper.value.writable.getWriter() 500 | await writer.write(chunk); 501 | writer.releaseLock(); 502 | return; 503 | } 504 | 505 | const { 506 | hasError, 507 | message, 508 | addressType, 509 | portRemote = 443, 510 | addressRemote = '', 511 | rawDataIndex, 512 | ProtocolVersion = new Uint8Array([0, 0]), 513 | isUDP, 514 | } = ProcessProtocolHeader(chunk, config.userID); 515 | address = addressRemote; 516 | portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? 'udp ' : 'tcp ' 517 | } `; 518 | if (hasError) { 519 | // controller.error(message); 520 | throw new Error(message); // cf seems has bug, controller.error will not end stream 521 | } 522 | // Handle UDP connections for DNS (port 53) only 523 | if (isUDP) { 524 | if (portRemote === 53) { 525 | isDns = true; 526 | } else { 527 | throw new Error('UDP proxy is only enabled for DNS (port 53)'); 528 | } 529 | return; // Early return after setting isDns or throwing error 530 | } 531 | // ["version", "附加信息长度 N"] 532 | const ProtocolResponseHeader = new Uint8Array([ProtocolVersion[0], 0]); 533 | const rawClientData = chunk.slice(rawDataIndex); 534 | 535 | if (isDns) { 536 | return handleDNSQuery(rawClientData, webSocket, ProtocolResponseHeader, log); 537 | } 538 | HandleTCPOutBound(remoteSocketWapper, addressType, addressRemote, portRemote, rawClientData, webSocket, ProtocolResponseHeader, log, config); 539 | }, 540 | close() { 541 | log(`readableWebSocketStream is close`); 542 | }, 543 | abort(reason) { 544 | log(`readableWebSocketStream is abort`, JSON.stringify(reason)); 545 | }, 546 | })).catch((err) => { 547 | log('readableWebSocketStream pipeTo error', err); 548 | }); 549 | 550 | return new Response(null, { 551 | status: 101, 552 | // @ts-ignore 553 | webSocket: client, 554 | }); 555 | } 556 | 557 | /** 558 | * Handles outbound TCP connections for the proxy. 559 | * Establishes connection to remote server and manages data flow. 560 | * @param {Socket} remoteSocket - Remote socket connection 561 | * @param {string} addressType - Type of address (IPv4/IPv6) 562 | * @param {string} addressRemote - Remote server address 563 | * @param {number} portRemote - Remote server port 564 | * @param {Uint8Array} rawClientData - Raw data from client 565 | * @param {WebSocket} webSocket - WebSocket connection 566 | * @param {Uint8Array} protocolResponseHeader - Protocol response header 567 | * @param {Function} log - Logging function 568 | * @param {Object} config - The configuration for this request 569 | */ 570 | async function HandleTCPOutBound(remoteSocket, addressType, addressRemote, portRemote, rawClientData, webSocket, protocolResponseHeader, log, config = null) { 571 | // 如果没有传入配置,使用全局配置 572 | if (!config) { 573 | config = { 574 | userID, 575 | socks5Address, 576 | socks5Relay, 577 | proxyIP, 578 | proxyPort, 579 | enableSocks, 580 | parsedSocks5Address 581 | }; 582 | } 583 | 584 | async function connectAndWrite(address, port, socks = false) { 585 | /** @type {import("@cloudflare/workers-types").Socket} */ 586 | let tcpSocket; 587 | if (config.socks5Relay) { 588 | tcpSocket = await socks5Connect(addressType, address, port, log, config.parsedSocks5Address) 589 | } else { 590 | tcpSocket = socks ? await socks5Connect(addressType, address, port, log, config.parsedSocks5Address) 591 | : connect({ 592 | hostname: address, 593 | port: port, 594 | }); 595 | } 596 | remoteSocket.value = tcpSocket; 597 | log(`connected to ${address}:${port}`); 598 | const writer = tcpSocket.writable.getWriter(); 599 | await writer.write(rawClientData); // first write, normal is tls client hello 600 | writer.releaseLock(); 601 | return tcpSocket; 602 | } 603 | 604 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip 605 | async function retry() { 606 | let tcpSocket; 607 | if (config.enableSocks) { 608 | tcpSocket = await connectAndWrite(addressRemote, portRemote, true); 609 | } else { 610 | tcpSocket = await connectAndWrite(config.proxyIP || addressRemote, config.proxyPort || portRemote, false); 611 | } 612 | // no matter retry success or not, close websocket 613 | tcpSocket.closed.catch(error => { 614 | console.log('retry tcpSocket closed error', error); 615 | }).finally(() => { 616 | safeCloseWebSocket(webSocket); 617 | }) 618 | RemoteSocketToWS(tcpSocket, webSocket, protocolResponseHeader, null, log); 619 | } 620 | 621 | let tcpSocket = await connectAndWrite(addressRemote, portRemote); 622 | 623 | // when remoteSocket is ready, pass to websocket 624 | // remote--> ws 625 | RemoteSocketToWS(tcpSocket, webSocket, protocolResponseHeader, retry, log); 626 | } 627 | 628 | /** 629 | * Creates a readable stream from WebSocket server. 630 | * Handles early data and WebSocket messages. 631 | * @param {WebSocket} webSocketServer - WebSocket server instance 632 | * @param {string} earlyDataHeader - Header for early data (0-RTT) 633 | * @param {Function} log - Logging function 634 | * @returns {ReadableStream} Stream of WebSocket data 635 | */ 636 | function MakeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) { 637 | let readableStreamCancel = false; 638 | const stream = new ReadableStream({ 639 | start(controller) { 640 | webSocketServer.addEventListener('message', (event) => { 641 | const message = event.data; 642 | controller.enqueue(message); 643 | }); 644 | 645 | webSocketServer.addEventListener('close', () => { 646 | safeCloseWebSocket(webSocketServer); 647 | controller.close(); 648 | }); 649 | 650 | webSocketServer.addEventListener('error', (err) => { 651 | log('webSocketServer has error'); 652 | controller.error(err); 653 | }); 654 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader); 655 | if (error) { 656 | controller.error(error); 657 | } else if (earlyData) { 658 | controller.enqueue(earlyData); 659 | } 660 | }, 661 | 662 | pull(_controller) { 663 | // if ws can stop read if stream is full, we can implement backpressure 664 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure 665 | }, 666 | 667 | cancel(reason) { 668 | log(`ReadableStream was canceled, due to ${reason}`) 669 | readableStreamCancel = true; 670 | safeCloseWebSocket(webSocketServer); 671 | } 672 | }); 673 | 674 | return stream; 675 | } 676 | 677 | /** 678 | * Processes VLESS protocol header. 679 | * Extracts and validates protocol information from buffer. 680 | * @param {ArrayBuffer} protocolBuffer - Buffer containing protocol header 681 | * @param {string} userID - User ID for validation 682 | * @returns {Object} Processed header information 683 | */ 684 | function ProcessProtocolHeader(protocolBuffer, userID) { 685 | if (protocolBuffer.byteLength < 24) { 686 | return { hasError: true, message: 'invalid data' }; 687 | } 688 | 689 | const dataView = new DataView(protocolBuffer); 690 | const version = dataView.getUint8(0); 691 | const slicedBufferString = stringify(new Uint8Array(protocolBuffer.slice(1, 17))); 692 | 693 | const uuids = userID.includes(',') ? userID.split(",") : [userID]; 694 | const isValidUser = uuids.some(uuid => slicedBufferString === uuid.trim()) || 695 | (uuids.length === 1 && slicedBufferString === uuids[0].trim()); 696 | 697 | console.log(`userID: ${slicedBufferString}`); 698 | 699 | if (!isValidUser) { 700 | return { hasError: true, message: 'invalid user' }; 701 | } 702 | 703 | const optLength = dataView.getUint8(17); 704 | const command = dataView.getUint8(18 + optLength); 705 | 706 | if (command !== 1 && command !== 2) { 707 | return { hasError: true, message: `command ${command} is not supported, command 01-tcp,02-udp,03-mux` }; 708 | } 709 | 710 | const portIndex = 18 + optLength + 1; 711 | const portRemote = dataView.getUint16(portIndex); 712 | const addressType = dataView.getUint8(portIndex + 2); 713 | let addressValue, addressLength, addressValueIndex; 714 | 715 | switch (addressType) { 716 | case 1: 717 | addressLength = 4; 718 | addressValueIndex = portIndex + 3; 719 | addressValue = new Uint8Array(protocolBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join('.'); 720 | break; 721 | case 2: 722 | addressLength = dataView.getUint8(portIndex + 3); 723 | addressValueIndex = portIndex + 4; 724 | addressValue = new TextDecoder().decode(protocolBuffer.slice(addressValueIndex, addressValueIndex + addressLength)); 725 | break; 726 | case 3: 727 | addressLength = 16; 728 | addressValueIndex = portIndex + 3; 729 | addressValue = Array.from({ length: 8 }, (_, i) => dataView.getUint16(addressValueIndex + i * 2).toString(16)).join(':'); 730 | break; 731 | default: 732 | return { hasError: true, message: `invalid addressType: ${addressType}` }; 733 | } 734 | 735 | if (!addressValue) { 736 | return { hasError: true, message: `addressValue is empty, addressType is ${addressType}` }; 737 | } 738 | 739 | return { 740 | hasError: false, 741 | addressRemote: addressValue, 742 | addressType, 743 | portRemote, 744 | rawDataIndex: addressValueIndex + addressLength, 745 | protocolVersion: new Uint8Array([version]), 746 | isUDP: command === 2 747 | }; 748 | } 749 | 750 | /** 751 | * Converts remote socket connection to WebSocket. 752 | * Handles data transfer between socket and WebSocket. 753 | * @param {Socket} remoteSocket - Remote socket connection 754 | * @param {WebSocket} webSocket - WebSocket connection 755 | * @param {ArrayBuffer} protocolResponseHeader - Protocol response header 756 | * @param {Function} retry - Retry function for failed connections 757 | * @param {Function} log - Logging function 758 | */ 759 | async function RemoteSocketToWS(remoteSocket, webSocket, protocolResponseHeader, retry, log) { 760 | let hasIncomingData = false; 761 | 762 | try { 763 | await remoteSocket.readable.pipeTo( 764 | new WritableStream({ 765 | async write(chunk) { 766 | if (webSocket.readyState !== WS_READY_STATE_OPEN) { 767 | throw new Error('WebSocket is not open'); 768 | } 769 | 770 | hasIncomingData = true; 771 | 772 | if (protocolResponseHeader) { 773 | webSocket.send(await new Blob([protocolResponseHeader, chunk]).arrayBuffer()); 774 | protocolResponseHeader = null; 775 | } else { 776 | webSocket.send(chunk); 777 | } 778 | }, 779 | close() { 780 | log(`Remote connection readable closed. Had incoming data: ${hasIncomingData}`); 781 | }, 782 | abort(reason) { 783 | console.error(`Remote connection readable aborted:`, reason); 784 | }, 785 | }) 786 | ); 787 | } catch (error) { 788 | console.error(`RemoteSocketToWS error:`, error.stack || error); 789 | safeCloseWebSocket(webSocket); 790 | } 791 | 792 | if (!hasIncomingData && retry) { 793 | log(`No incoming data, retrying`); 794 | await retry(); 795 | } 796 | } 797 | 798 | /** 799 | * Converts base64 string to ArrayBuffer. 800 | * @param {string} base64Str - Base64 encoded string 801 | * @returns {Object} Object containing decoded data or error 802 | */ 803 | function base64ToArrayBuffer(base64Str) { 804 | if (!base64Str) { 805 | return { earlyData: null, error: null }; 806 | } 807 | try { 808 | // Convert modified Base64 for URL (RFC 4648) to standard Base64 809 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/'); 810 | // Decode Base64 string 811 | const binaryStr = atob(base64Str); 812 | // Convert binary string to ArrayBuffer 813 | const buffer = new ArrayBuffer(binaryStr.length); 814 | const view = new Uint8Array(buffer); 815 | for (let i = 0; i < binaryStr.length; i++) { 816 | view[i] = binaryStr.charCodeAt(i); 817 | } 818 | return { earlyData: buffer, error: null }; 819 | } catch (error) { 820 | return { earlyData: null, error }; 821 | } 822 | } 823 | 824 | /** 825 | * Validates UUID format. 826 | * @param {string} uuid - UUID string to validate 827 | * @returns {boolean} True if valid UUID 828 | */ 829 | function isValidUUID(uuid) { 830 | // More precise UUID regex pattern 831 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; 832 | return uuidRegex.test(uuid); 833 | } 834 | 835 | const WS_READY_STATE_OPEN = 1; 836 | const WS_READY_STATE_CLOSING = 2; 837 | 838 | /** 839 | * Safely closes WebSocket connection. 840 | * Prevents exceptions during WebSocket closure. 841 | * @param {WebSocket} socket - WebSocket to close 842 | */ 843 | function safeCloseWebSocket(socket) { 844 | try { 845 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) { 846 | socket.close(); 847 | } 848 | } catch (error) { 849 | console.error('safeCloseWebSocket error:', error); 850 | } 851 | } 852 | 853 | const byteToHex = Array.from({ length: 256 }, (_, i) => (i + 0x100).toString(16).slice(1)); 854 | 855 | /** 856 | * Converts byte array to hex string without validation. 857 | * @param {Uint8Array} arr - Byte array to convert 858 | * @param {number} offset - Starting offset 859 | * @returns {string} Hex string 860 | */ 861 | function unsafeStringify(arr, offset = 0) { 862 | return [ 863 | byteToHex[arr[offset]], 864 | byteToHex[arr[offset + 1]], 865 | byteToHex[arr[offset + 2]], 866 | byteToHex[arr[offset + 3]], 867 | '-', 868 | byteToHex[arr[offset + 4]], 869 | byteToHex[arr[offset + 5]], 870 | '-', 871 | byteToHex[arr[offset + 6]], 872 | byteToHex[arr[offset + 7]], 873 | '-', 874 | byteToHex[arr[offset + 8]], 875 | byteToHex[arr[offset + 9]], 876 | '-', 877 | byteToHex[arr[offset + 10]], 878 | byteToHex[arr[offset + 11]], 879 | byteToHex[arr[offset + 12]], 880 | byteToHex[arr[offset + 13]], 881 | byteToHex[arr[offset + 14]], 882 | byteToHex[arr[offset + 15]] 883 | ].join('').toLowerCase(); 884 | } 885 | 886 | /** 887 | * Safely converts byte array to hex string with validation. 888 | * @param {Uint8Array} arr - Byte array to convert 889 | * @param {number} offset - Starting offset 890 | * @returns {string} Hex string 891 | */ 892 | function stringify(arr, offset = 0) { 893 | const uuid = unsafeStringify(arr, offset); 894 | if (!isValidUUID(uuid)) { 895 | throw new TypeError("Stringified UUID is invalid"); 896 | } 897 | return uuid; 898 | } 899 | 900 | /** 901 | * Handles DNS query through UDP. 902 | * Processes DNS requests and forwards them. 903 | * @param {ArrayBuffer} udpChunk - UDP data chunk 904 | * @param {WebSocket} webSocket - WebSocket connection 905 | * @param {ArrayBuffer} protocolResponseHeader - Protocol response header 906 | * @param {Function} log - Logging function 907 | */ 908 | async function handleDNSQuery(udpChunk, webSocket, protocolResponseHeader, log) { 909 | // no matter which DNS server client send, we alwasy use hard code one. 910 | // beacsue someof DNS server is not support DNS over TCP 911 | try { 912 | const dnsServer = '8.8.4.4'; // change to 1.1.1.1 after cf fix connect own ip bug 913 | const dnsPort = 53; 914 | /** @type {ArrayBuffer | null} */ 915 | let vlessHeader = protocolResponseHeader; 916 | /** @type {import("@cloudflare/workers-types").Socket} */ 917 | const tcpSocket = connect({ 918 | hostname: dnsServer, 919 | port: dnsPort, 920 | }); 921 | 922 | log(`connected to ${dnsServer}:${dnsPort}`); 923 | const writer = tcpSocket.writable.getWriter(); 924 | await writer.write(udpChunk); 925 | writer.releaseLock(); 926 | await tcpSocket.readable.pipeTo(new WritableStream({ 927 | async write(chunk) { 928 | if (webSocket.readyState === WS_READY_STATE_OPEN) { 929 | if (vlessHeader) { 930 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer()); 931 | vlessHeader = null; 932 | } else { 933 | webSocket.send(chunk); 934 | } 935 | } 936 | }, 937 | close() { 938 | log(`dns server(${dnsServer}) tcp is close`); 939 | }, 940 | abort(reason) { 941 | console.error(`dns server(${dnsServer}) tcp is abort`, reason); 942 | }, 943 | })); 944 | } catch (error) { 945 | console.error( 946 | `handleDNSQuery have exception, error: ${error.message}` 947 | ); 948 | } 949 | } 950 | 951 | /** 952 | * Establishes SOCKS5 proxy connection. 953 | * @param {number} addressType - Type of address 954 | * @param {string} addressRemote - Remote address 955 | * @param {number} portRemote - Remote port 956 | * @param {Function} log - Logging function 957 | * @param {Object} parsedSocks5Addr - Parsed SOCKS5 address information 958 | * @returns {Promise} Connected socket 959 | */ 960 | async function socks5Connect(addressType, addressRemote, portRemote, log, parsedSocks5Addr = null) { 961 | // 如果没有传入解析的SOCKS5地址,使用全局的 962 | const { username, password, hostname, port } = parsedSocks5Addr || parsedSocks5Address; 963 | 964 | // Connect to the SOCKS server 965 | const socket = connect({ 966 | hostname, 967 | port, 968 | }); 969 | 970 | // Request head format (Worker -> Socks Server): 971 | // +----+----------+----------+ 972 | // |VER | NMETHODS | METHODS | 973 | // +----+----------+----------+ 974 | // | 1 | 1 | 1 to 255 | 975 | // +----+----------+----------+ 976 | 977 | // https://en.wikipedia.org/wiki/SOCKS#SOCKS5 978 | // For METHODS: 979 | // 0x00 NO AUTHENTICATION REQUIRED 980 | // 0x02 USERNAME/PASSWORD https://datatracker.ietf.org/doc/html/rfc1929 981 | const socksGreeting = new Uint8Array([5, 2, 0, 2]); 982 | 983 | const writer = socket.writable.getWriter(); 984 | 985 | await writer.write(socksGreeting); 986 | log('sent socks greeting'); 987 | 988 | const reader = socket.readable.getReader(); 989 | const encoder = new TextEncoder(); 990 | let res = (await reader.read()).value; 991 | // Response format (Socks Server -> Worker): 992 | // +----+--------+ 993 | // |VER | METHOD | 994 | // +----+--------+ 995 | // | 1 | 1 | 996 | // +----+--------+ 997 | if (res[0] !== 0x05) { 998 | log(`socks server version error: ${res[0]} expected: 5`); 999 | return; 1000 | } 1001 | if (res[1] === 0xff) { 1002 | log("no acceptable methods"); 1003 | return; 1004 | } 1005 | 1006 | // if return 0x0502 1007 | if (res[1] === 0x02) { 1008 | log("socks server needs auth"); 1009 | if (!username || !password) { 1010 | log("please provide username/password"); 1011 | return; 1012 | } 1013 | // +----+------+----------+------+----------+ 1014 | // |VER | ULEN | UNAME | PLEN | PASSWD | 1015 | // +----+------+----------+------+----------+ 1016 | // | 1 | 1 | 1 to 255 | 1 | 1 to 255 | 1017 | // +----+------+----------+------+----------+ 1018 | const authRequest = new Uint8Array([ 1019 | 1, 1020 | username.length, 1021 | ...encoder.encode(username), 1022 | password.length, 1023 | ...encoder.encode(password) 1024 | ]); 1025 | await writer.write(authRequest); 1026 | res = (await reader.read()).value; 1027 | // expected 0x0100 1028 | if (res[0] !== 0x01 || res[1] !== 0x00) { 1029 | log("fail to auth socks server"); 1030 | return; 1031 | } 1032 | } 1033 | 1034 | // Request data format (Worker -> Socks Server): 1035 | // +----+-----+-------+------+----------+----------+ 1036 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 1037 | // +----+-----+-------+------+----------+----------+ 1038 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 1039 | // +----+-----+-------+------+----------+----------+ 1040 | // ATYP: address type of following address 1041 | // 0x01: IPv4 address 1042 | // 0x03: Domain name 1043 | // 0x04: IPv6 address 1044 | // DST.ADDR: desired destination address 1045 | // DST.PORT: desired destination port in network octet order 1046 | 1047 | // addressType 1048 | // 1--> ipv4 addressLength =4 1049 | // 2--> domain name 1050 | // 3--> ipv6 addressLength =16 1051 | let DSTADDR; // DSTADDR = ATYP + DST.ADDR 1052 | switch (addressType) { 1053 | case 1: 1054 | DSTADDR = new Uint8Array( 1055 | [1, ...addressRemote.split('.').map(Number)] 1056 | ); 1057 | break; 1058 | case 2: 1059 | DSTADDR = new Uint8Array( 1060 | [3, addressRemote.length, ...encoder.encode(addressRemote)] 1061 | ); 1062 | break; 1063 | case 3: 1064 | DSTADDR = new Uint8Array( 1065 | [4, ...addressRemote.split(':').flatMap(x => [parseInt(x.slice(0, 2), 16), parseInt(x.slice(2), 16)])] 1066 | ); 1067 | break; 1068 | default: 1069 | log(`invild addressType is ${addressType}`); 1070 | return; 1071 | } 1072 | const socksRequest = new Uint8Array([5, 1, 0, ...DSTADDR, portRemote >> 8, portRemote & 0xff]); 1073 | await writer.write(socksRequest); 1074 | log('sent socks request'); 1075 | 1076 | res = (await reader.read()).value; 1077 | // Response format (Socks Server -> Worker): 1078 | // +----+-----+-------+------+----------+----------+ 1079 | // |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT | 1080 | // +----+-----+-------+------+----------+----------+ 1081 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 1082 | // +----+-----+-------+------+----------+----------+ 1083 | if (res[1] === 0x00) { 1084 | log("socks connection opened"); 1085 | } else { 1086 | log("fail to open socks connection"); 1087 | return; 1088 | } 1089 | writer.releaseLock(); 1090 | reader.releaseLock(); 1091 | return socket; 1092 | } 1093 | 1094 | /** 1095 | * Parses SOCKS5 address string. 1096 | * @param {string} address - SOCKS5 address string 1097 | * @returns {Object} Parsed address information 1098 | */ 1099 | function socks5AddressParser(address) { 1100 | let [latter, former] = address.split("@").reverse(); 1101 | let username, password, hostname, port; 1102 | if (former) { 1103 | const formers = former.split(":"); 1104 | if (formers.length !== 2) { 1105 | throw new Error('Invalid SOCKS address format'); 1106 | } 1107 | [username, password] = formers; 1108 | } 1109 | const latters = latter.split(":"); 1110 | port = Number(latters.pop()); 1111 | if (isNaN(port)) { 1112 | throw new Error('Invalid SOCKS address format'); 1113 | } 1114 | hostname = latters.join(":"); 1115 | const regex = /^\[.*\]$/; 1116 | if (hostname.includes(":") && !regex.test(hostname)) { 1117 | throw new Error('Invalid SOCKS address format'); 1118 | } 1119 | return { 1120 | username, 1121 | password, 1122 | hostname, 1123 | port, 1124 | } 1125 | } 1126 | 1127 | const at = 'QA=='; 1128 | const pt = 'dmxlc3M='; 1129 | const ed = 'RUR0dW5uZWw='; 1130 | 1131 | /** 1132 | * Generates configuration for VLESS client. 1133 | * @param {string} userIDs - Single or comma-separated user IDs 1134 | * @param {string} hostName - Host name for configuration 1135 | * @param {string|string[]} proxyIP - Proxy IP address or array of addresses 1136 | * @returns {string} Configuration HTML 1137 | */ 1138 | function getConfig(userIDs, hostName, proxyIP) { 1139 | const commonUrlPart = `?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`; 1140 | 1141 | // Split the userIDs into an array 1142 | const userIDArray = userIDs.split(","); 1143 | 1144 | // Prepare output string for each userID 1145 | const sublink = `https://${hostName}/sub/${userIDArray[0]}?format=clash` 1146 | const subbestip = `https://${hostName}/bestip/${userIDArray[0]}`; 1147 | const clash_link = `https://url.v1.mk/sub?target=clash&url=${encodeURIComponent(`https://${hostName}/sub/${userIDArray[0]}?format=clash`)}&insert=false&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`; 1148 | // HTML Head with CSS and FontAwesome library 1149 | const htmlHead = ` 1150 | 1151 | EDtunnel: Configuration 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1281 | 1282 | 1283 | `; 1284 | 1285 | const header = ` 1286 |
1287 |

EDtunnel: Protocol Configuration

1288 | 1289 |

Welcome! This function generates configuration for the vless protocol. If you found this useful, please check our GitHub project:

1290 |

EDtunnel - https://github.com/6Kmfi6HP/EDtunnel

1291 |
1292 | 1298 |
1299 |

Options Explained:

1300 |
    1301 |
  • VLESS Subscription: Direct link for VLESS protocol configuration. Suitable for clients supporting VLESS.
  • 1302 |
  • Clash Subscription: Opens the Clash client with pre-configured settings. Best for Clash users on mobile devices.
  • 1303 |
  • Clash Link: A web link to convert the VLESS config to Clash format. Useful for manual import or troubleshooting.
  • 1304 |
  • Best IP Subscription: Provides a curated list of optimal server IPs for many different countries.
  • 1305 |
1306 |

Choose the option that best fits your client and needs. For most users, the VLESS or Clash Subscription will be the easiest to use.

1307 |
1308 |
1309 | `; 1310 | 1311 | const configOutput = userIDArray.map((userID) => { 1312 | const protocolMain = atob(pt) + '://' + userID + atob(at) + hostName + ":443" + commonUrlPart; 1313 | const protocolSec = atob(pt) + '://' + userID + atob(at) + proxyIP[0].split(':')[0] + ":" + proxyPort + commonUrlPart; 1314 | return ` 1315 |
1316 |

UUID: ${userID}

1317 |

Default IP Configuration

1318 |
1319 |
${protocolMain}
1320 | 1321 |
1322 | 1323 |

Best IP Configuration

1324 |
1325 | 1330 |
1331 |
1332 |
1333 |
${protocolSec}
1334 | 1335 |
1336 |
1337 | `; 1338 | }).join(''); 1339 | 1340 | return ` 1341 | 1342 | ${htmlHead} 1343 | 1344 | ${header} 1345 | ${configOutput} 1346 | 1370 | 1371 | `; 1372 | } 1373 | 1374 | const HttpPort = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]); 1375 | const HttpsPort = new Set([443, 8443, 2053, 2096, 2087, 2083]); 1376 | 1377 | /** 1378 | * Generates subscription content. 1379 | * @param {string} userID_path - User ID path 1380 | * @param {string} hostname - Host name 1381 | * @param {string|string[]} proxyIP - Proxy IP address or array of addresses 1382 | * @returns {string} Subscription content 1383 | */ 1384 | function GenSub(userID_path, hostname, proxyIP) { 1385 | // Add all CloudFlare public CNAME domains 1386 | const mainDomains = new Set([ 1387 | hostname, 1388 | // public domains 1389 | 'icook.hk', 1390 | 'japan.com', 1391 | 'malaysia.com', 1392 | 'russia.com', 1393 | 'singapore.com', 1394 | 'www.visa.com', 1395 | 'www.csgo.com', 1396 | 'www.shopify.com', 1397 | 'www.whatismyip.com', 1398 | 'www.ipget.net', 1399 | // 高频率更新 1400 | // 'speed.marisalnc.com', // 1000ip/3min 1401 | 'freeyx.cloudflare88.eu.org', // 1000ip/3min 1402 | 'cloudflare.182682.xyz', // 15ip/15min 1403 | // '115155.xyz', // 18ip/1小时 1404 | // 'cdn.2020111.xyz', // 15ip/10min 1405 | 'cfip.cfcdn.vip', // 6ip/1天 1406 | proxyIPs, 1407 | // 手动更新和未知频率 1408 | 'cf.0sm.com', // 手动更新 1409 | 'cloudflare-ip.mofashi.ltd', // 未知频率 1410 | 'cf.090227.xyz', // 未知频率 1411 | // 'cname.xirancdn.us', // 未知频率 1412 | // 'f3058171cad.002404.xyz', // 未知频率 1413 | 'cf.zhetengsha.eu.org', // 未知频率 1414 | 'cloudflare.9jy.cc', // 未知频率 1415 | // '8.889288.xyz', // 未知频率 1416 | 'cf.zerone-cdn.pp.ua', // 未知频率 1417 | 'cfip.1323123.xyz', // 未知频率 1418 | 'cdn.tzpro.xyz', // 未知频率 1419 | 'cf.877771.xyz', // 未知频率 1420 | 'cnamefuckxxs.yuchen.icu', // 未知频率 1421 | 'cfip.xxxxxxxx.tk', // OTC大佬提供维护 1422 | ]); 1423 | 1424 | const userIDArray = userID_path.includes(',') ? userID_path.split(",") : [userID_path]; 1425 | const proxyIPArray = Array.isArray(proxyIP) ? proxyIP : (proxyIP ? (proxyIP.includes(',') ? proxyIP.split(',') : [proxyIP]) : proxyIPs); 1426 | const randomPath = () => '/' + Math.random().toString(36).substring(2, 15) + '?ed=2048'; 1427 | const commonUrlPartHttp = `?encryption=none&security=none&fp=random&type=ws&host=${hostname}&path=${encodeURIComponent(randomPath())}#`; 1428 | const commonUrlPartHttps = `?encryption=none&security=tls&sni=${hostname}&fp=random&type=ws&host=${hostname}&path=%2F%3Fed%3D2048#`; 1429 | 1430 | const result = userIDArray.flatMap((userID) => { 1431 | let allUrls = []; 1432 | // Generate main HTTP URLs first for all domains 1433 | if (!hostname.includes('pages.dev')) { 1434 | mainDomains.forEach(domain => { 1435 | Array.from(HttpPort).forEach((port) => { 1436 | const urlPart = `${hostname.split('.')[0]}-${domain}-HTTP-${port}`; 1437 | const mainProtocolHttp = atob(pt) + '://' + userID + atob(at) + domain + ':' + port + commonUrlPartHttp + urlPart; 1438 | allUrls.push(mainProtocolHttp); 1439 | }); 1440 | }); 1441 | } 1442 | 1443 | // Generate main HTTPS URLs for all domains 1444 | mainDomains.forEach(domain => { 1445 | Array.from(HttpsPort).forEach((port) => { 1446 | const urlPart = `${hostname.split('.')[0]}-${domain}-HTTPS-${port}`; 1447 | const mainProtocolHttps = atob(pt) + '://' + userID + atob(at) + domain + ':' + port + commonUrlPartHttps + urlPart; 1448 | allUrls.push(mainProtocolHttps); 1449 | }); 1450 | }); 1451 | 1452 | // Generate proxy HTTPS URLs 1453 | proxyIPArray.forEach((proxyAddr) => { 1454 | const [proxyHost, proxyPort = '443'] = proxyAddr.split(':'); 1455 | const urlPart = `${hostname.split('.')[0]}-${proxyHost}-HTTPS-${proxyPort}`; 1456 | const secondaryProtocolHttps = atob(pt) + '://' + userID + atob(at) + proxyHost + ':' + proxyPort + commonUrlPartHttps + urlPart + '-' + atob(ed); 1457 | allUrls.push(secondaryProtocolHttps); 1458 | }); 1459 | 1460 | return allUrls; 1461 | }); 1462 | 1463 | return btoa(result.join('\n')); 1464 | // return result.join('\n'); 1465 | } 1466 | 1467 | /** 1468 | * Handles proxy configuration and returns standardized proxy settings 1469 | * @param {string} PROXYIP - Proxy IP configuration from environment 1470 | * @returns {{ip: string, port: string}} Standardized proxy configuration 1471 | */ 1472 | function handleProxyConfig(PROXYIP) { 1473 | if (PROXYIP) { 1474 | const proxyAddresses = PROXYIP.split(',').map(addr => addr.trim()); 1475 | const selectedProxy = selectRandomAddress(proxyAddresses); 1476 | const [ip, port = '443'] = selectedProxy.split(':'); 1477 | return { ip, port }; 1478 | } else { 1479 | const port = proxyIP.includes(':') ? proxyIP.split(':')[1] : '443'; 1480 | const ip = proxyIP.split(':')[0]; 1481 | return { ip, port }; 1482 | } 1483 | } 1484 | 1485 | /** 1486 | * Selects a random address from a comma-separated string or array of addresses 1487 | * @param {string|string[]} addresses - Comma-separated string or array of addresses 1488 | * @returns {string} Selected address 1489 | */ 1490 | function selectRandomAddress(addresses) { 1491 | const addressArray = typeof addresses === 'string' ? 1492 | addresses.split(',').map(addr => addr.trim()) : 1493 | addresses; 1494 | return addressArray[Math.floor(Math.random() * addressArray.length)]; 1495 | } 1496 | 1497 | /** 1498 | * 合并路径查询参数解析为通用函数 1499 | * @param {string} pathname - URL路径 1500 | * @returns {Object} 解析的参数对象 1501 | */ 1502 | function parseEncodedQueryParams(pathname) { 1503 | const params = {}; 1504 | if (pathname.includes('%3F')) { 1505 | const encodedParamsMatch = pathname.match(/%3F(.+)$/); 1506 | if (encodedParamsMatch) { 1507 | const encodedParams = encodedParamsMatch[1]; 1508 | const paramPairs = encodedParams.split('&'); 1509 | 1510 | for (const pair of paramPairs) { 1511 | const [key, value] = pair.split('='); 1512 | if (value) params[key] = decodeURIComponent(value); 1513 | } 1514 | } 1515 | } 1516 | return params; 1517 | } 1518 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edtunnel", 3 | "version": "1.0.0", 4 | "description": "A tunnel for the edgetunnel project to allow deployed applications to cloudflare pages and workers to be accessed via a custom domain.", 5 | "main": "_worker.js", 6 | "scripts": { 7 | "deploy": "wrangler deploy", 8 | "build": "wrangler deploy --dry-run", 9 | "dev": "wrangler dev --remote", 10 | "dev-local": "wrangler dev index.js --remote", 11 | "obfuscate": "javascript-obfuscator index.js --output _worker.js --compact true --control-flow-flattening true --control-flow-flattening-threshold 1 --dead-code-injection true --dead-code-injection-threshold 1 --string-array true --string-array-encoding 'rc4' --string-array-threshold 1 --transform-object-keys true --unicode-escape-sequence true" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@cloudflare/workers-types": "^4.20230710.1", 17 | "javascript-obfuscator": "^4.1.1", 18 | "wrangler": "^3.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "cf-worker-ws-dev" 2 | main = "_worker.js" 3 | compatibility_date = "2023-05-26" 4 | # node_compat = true 5 | workers_dev = true 6 | 7 | [vars] 8 | # PROXYIP = "211.230.110.231:50008" 9 | UUID = "1b6c1745-992e-4aac-8685-266c090e50ea" 10 | # SOCKS5 = "127.0.0.1:1080" 11 | # SOCKS5_RELAY = "true" 12 | --------------------------------------------------------------------------------