├── .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 |
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 | [](https://github.com/6Kmfi6HP/EDtunnel)
12 | [](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 | [](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 | 
267 |
268 | ### Pages.dev 设置 | Pages.dev Settings
269 | 在 Pages 设置页面配置环境变量 | Configure environment variables in Pages settings page
270 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------