├── .gitignore
├── package.json
├── wrangler.toml
├── LICENSE
├── .github
└── workflows
│ ├── sync.yml
│ ├── create.yml
│ └── cf.yml
├── README.md
└── _worker.js
/.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/
--------------------------------------------------------------------------------
/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 | },
11 | "author": "",
12 | "license": "ISC",
13 | "devDependencies": {
14 | "@cloudflare/workers-types": "^4.20230710.1",
15 | "wrangler": "^3.2.0"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "cf-worker-ws-dev" # todo
2 | #name = "cf-worker-connect-test" # todo
3 | #main = "test/worker/cf-cdn-cgi-trace2.js"
4 | #main = "test/worker/worker-connect-test.js"
5 | main = "_worker.js"
6 | compatibility_date = "2023-05-26"
7 |
8 | [vars]
9 | # UUID = "d342d11e-d424-4583-b36e-524ab1f0afa4"
10 | # PROXYIP = "1.2.3.4"
11 | # DNS_RESOLVER_URL = "https://cloudflare-dns.com/dns-query"
12 | # NODE_ID = "1"
13 | # API_TOKEN = "example_dev_token"
14 | # API_HOST = "api.v2board.com"
15 | UUID = "1b6c1745-992e-4aac-8685-266c090e50ea,89b64978-6244-4bf3-bf64-67ade4ce5c8f,d342d11e-d424-4583-b36e-524ab1f0afa4"
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EDtunnel
2 |
3 |
4 |
5 |
6 |
7 | GitHub Repository for [https://github.com/zizifn/edgetunnel](https://github.com/zizifn/edgetunnel)
8 |
9 | ask question and cloudflare ips: [https://t.me/edtunnel](https://t.me/edtunnel)
10 |
11 | [](https://github.com/zizifn/edgetunnel)
12 |
13 | ## available branches and explain
14 |
15 | | Branch Name | Description |
16 | | ------------- | ------------------------------------------------------------- |
17 | | remote-socks5 | Branch for remote SOCKS5 proxy pool used implementation |
18 | | socks5 | Branch for SOCKS5 proxyIP implementation |
19 | | vless | Branch for outbound VLESS protocol implementation |
20 | | vless2 | Branch for alternative outbound VLESS protocol implementation |
21 | | code1 | Branch for code1 feature development |
22 | | code2 | Branch for code2 alternative feature development |
23 | | dns | Branch for DNS alternative related development |
24 | | main | Main branch for the project |
25 | | pages | New version for deployment on Cloudflare Pages |
26 |
27 | ## Deploy in pages.dev
28 |
29 | 1. See YouTube Video:
30 |
31 | [https://www.youtube.com/watch?v=8I-yTNHB0aw](https://www.youtube.com/watch?v=8I-yTNHB0aw)
32 |
33 | 2. Clone this repository deploy in cloudflare pages.
34 |
35 | ## Deploy in worker.dev
36 |
37 | 1. Copy `_worker.js` code from [here](https://github.com/3Kmfi6HP/EDtunnel/blob/main/_worker.js).
38 |
39 | 2. Alternatively, you can click the button below to deploy directly.
40 |
41 | [](https://deploy.workers.cloudflare.com/?url=https://github.com/3Kmfi6HP/EDtunnel)
42 |
43 | ## Lazy to deploy
44 |
45 | `aHR0cHM6Ly9vc3MudjJyYXlzZS5jb20vcHJveGllcy9kYXRhLzIwMjMtMDctMzAvRnJFS1lvQS50eHQ=` (free clash.meta subscribe config)
46 |
47 | ## UUID Setting (Optional)
48 |
49 | 1. When deploy in cloudflare pages, you can set uuid in `wrangler.toml` file. variable name is `UUID`. `wrangler.toml` file is also supported. (recommended) in case deploy in webpages, you can not set uuid in `wrangler.toml` file.
50 |
51 | 2. When deploy in worker.dev, you can set uuid in `_worker.js` file. variable name is `userID`. `wrangler.toml` file is also supported. (recommended) in case deploy in webpages, you can not set uuid in `wrangler.toml` file. in this case, you can also set uuid in `UUID` enviroment variable.
52 |
53 | Note: `UUID` is the uuid you want to set. pages.dev and worker.dev all of them method supported, but depend on your deploy method.
54 |
55 | ### UUID Setting Example
56 |
57 | 1. single uuid environment variable
58 |
59 | ```.environment
60 | UUID = "uuid here your want to set"
61 | ```
62 |
63 | 2. multiple uuid environment variable
64 |
65 | ```.environment
66 | UUID = "uuid1,uuid2,uuid3"
67 | ```
68 |
69 | note: uuid1, uuid2, uuid3 are separated by commas`,`.
70 | when you set multiple uuid, you can use `https://edtunnel.pages.dev/uuid1` to get the clash config and vless:// link.
71 |
72 | ## subscribe vless:// link (Optional)
73 |
74 | 1. visit `https://edtunnel.pages.dev/uuid your set` to get the subscribe link.
75 |
76 | 2. visit `https://edtunnel.pages.dev/sub/uuid your set` to get the subscribe content with `uuid your set` path.
77 |
78 | note: `uuid your set` is the uuid you set in UUID enviroment or `wrangler.toml`, `_worker.js` file.
79 | when you set multiple uuid, you can use `https://edtunnel.pages.dev/sub/uuid1` to get the subscribe content with `uuid1` path.(only support first uuid in multiple uuid set)
80 |
81 | 3. visit `https://edtunnel.pages.dev/sub/uuid your set/?format=clash` to get the subscribe content with `uuid your set` path and `clash` format. content will return with base64 encode.
82 |
83 | note: `uuid your set` is the uuid you set in UUID enviroment or `wrangler.toml`, `_worker.js` file.
84 | when you set multiple uuid, you can will use `https://edtunnel.pages.dev/sub/uuid1/?format=clash` to get the subscribe content with `uuid1` path and `clash` format.(only support first uuid in multiple uuid set)
85 |
86 | ## subscribe Cloudflare bestip(pure ip) link
87 |
88 | 1. visit `https://edtunnel.pages.dev/bestip/uuid your set` to get subscribe info.
89 |
90 | 2. cpoy subscribe url link `https://edtunnel.pages.dev/bestip/uuid your set` to any clients(clash/v2rayN/v2rayNG) you want to use.
91 |
92 | 3. done. if have any questions please join [@edtunnel](https://t.me/edtunnel)
93 |
94 | ## multiple port support (Optional)
95 |
96 |
98 |
99 | For a list of Cloudflare supported ports, please refer to the [official documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/ports).
100 |
101 | By default, the port is 80 and 443. If you want to add more ports, you can use the following ports:
102 |
103 | ```text
104 | 80, 8080, 8880, 2052, 2086, 2095, 443, 8443, 2053, 2096, 2087, 2083
105 | http port: 80, 8080, 8880, 2052, 2086, 2095
106 | https port: 443, 8443, 2053, 2096, 2087, 2083
107 | ```
108 |
109 | if you deploy in cloudflare pages, https port is not supported. Simply add multiple ports node drictly use subscribe link, subscribe content will return all Cloudflare supported ports.
110 |
111 | ## proxyIP (Optional)
112 |
113 | 1. When deploy in cloudflare pages, you can set proxyIP in `wrangler.toml` file. variable name is `PROXYIP`.
114 |
115 | 2. When deploy in worker.dev, you can set proxyIP in `_worker.js` file. variable name is `proxyIP`.
116 |
117 | note: `proxyIP` is the ip or domain you want to set. this means that the proxyIP is used to route traffic through a proxy rather than directly to a website that is using Cloudflare's (CDN). if you don't set this variable, connection to the Cloudflare IP will be cancelled (or blocked)...
118 |
119 | resons: Outbound TCP sockets to Cloudflare IP ranges are temporarily blocked, please refer to the [tcp-sockets documentation](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#considerations)
120 |
121 | ## Usage
122 |
123 | frist, open your pages.dev domain `https://edtunnel.pages.dev/` in your browser, then you can see the following page:
124 | The path `/uuid your seetting` to get the clash config and vless:// link.
125 |
126 | ## Star History
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/_worker.js:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { connect } from 'cloudflare:sockets';
3 |
4 | // How to generate your own UUID:
5 | // [Windows] Press "Win + R", input cmd and run: Powershell -NoExit -Command "[guid]::NewGuid()"
6 | let userID = 'd342d11e-d424-4583-b36e-524ab1f0afa4';
7 |
8 | const พร็อกซีไอพีs = ['cdn.xn--b6gac.eu.org', 'cdn-all.xn--b6gac.eu.org', 'workers.cloudflare.cyou'];
9 |
10 | // if you want to use ipv6 or single พร็อกซีไอพี, please add comment at this line and remove comment at the next line
11 | let พร็อกซีไอพี = พร็อกซีไอพีs[Math.floor(Math.random() * พร็อกซีไอพีs.length)];
12 | // use single พร็อกซีไอพี instead of random
13 | // let พร็อกซีไอพี = 'cdn.xn--b6gac.eu.org';
14 | // ipv6 พร็อกซีไอพี example remove comment to use
15 | // let พร็อกซีไอพี = "[2a01:4f8:c2c:123f:64:5:6810:c55a]"
16 |
17 | let dohURL = 'https://sky.rethinkdns.com/1:-Pf_____9_8A_AMAIgE8kMABVDDmKOHTAKg='; // https://cloudflare-dns.com/dns-query or https://dns.google/dns-query
18 |
19 | if (!isValidUUID(userID)) {
20 | throw new Error('uuid is invalid');
21 | }
22 |
23 | export default {
24 | /**
25 | * @param {import("@cloudflare/workers-types").Request} request
26 | * @param {{UUID: string, พร็อกซีไอพี: string, DNS_RESOLVER_URL: string, NODE_ID: int, API_HOST: string, API_TOKEN: string}} env
27 | * @param {import("@cloudflare/workers-types").ExecutionContext} ctx
28 | * @returns {Promise}
29 | */
30 | async fetch(request, env, ctx) {
31 | // uuid_validator(request);
32 | try {
33 | userID = env.UUID || userID;
34 | พร็อกซีไอพี = env.PROXYIP || พร็อกซีไอพี;
35 | dohURL = env.DNS_RESOLVER_URL || dohURL;
36 | let userID_Path = userID;
37 | if (userID.includes(',')) {
38 | userID_Path = userID.split(',')[0];
39 | }
40 | const upgradeHeader = request.headers.get('Upgrade');
41 | if (!upgradeHeader || upgradeHeader !== 'websocket') {
42 | const url = new URL(request.url);
43 | switch (url.pathname) {
44 | case `/cf`: {
45 | return new Response(JSON.stringify(request.cf, null, 4), {
46 | status: 200,
47 | headers: {
48 | "Content-Type": "application/json;charset=utf-8",
49 | },
50 | });
51 | }
52 | case `/${userID_Path}`: {
53 | const วเลสConfig = getวเลสConfig(userID, request.headers.get('Host'));
54 | return new Response(`${วเลสConfig}`, {
55 | status: 200,
56 | headers: {
57 | "Content-Type": "text/html; charset=utf-8",
58 | }
59 | });
60 | };
61 | case `/sub/${userID_Path}`: {
62 | const url = new URL(request.url);
63 | const searchParams = url.searchParams;
64 | const วเลสSubConfig = สร้างวเลสSub(userID, request.headers.get('Host'));
65 | // Construct and return response object
66 | return new Response(btoa(วเลสSubConfig), {
67 | status: 200,
68 | headers: {
69 | "Content-Type": "text/plain;charset=utf-8",
70 | }
71 | });
72 | };
73 | case `/bestip/${userID_Path}`: {
74 | const headers = request.headers;
75 | const url = `https://sub.xf.free.hr/auto?host=${request.headers.get('Host')}&uuid=${userID}&path=/`;
76 | const bestSubConfig = await fetch(url, { headers: headers });
77 | return bestSubConfig;
78 | };
79 | default:
80 | // return new Response('Not found', { status: 404 });
81 | // For any other path, reverse proxy to 'ramdom website' and return the original response, caching it in the process
82 | const randomHostname = cn_hostnames[Math.floor(Math.random() * cn_hostnames.length)];
83 | const newHeaders = new Headers(request.headers);
84 | newHeaders.set('cf-connecting-ip', '1.2.3.4');
85 | newHeaders.set('x-forwarded-for', '1.2.3.4');
86 | newHeaders.set('x-real-ip', '1.2.3.4');
87 | newHeaders.set('referer', 'https://www.google.com/search?q=edtunnel');
88 | // Use fetch to proxy the request to 15 different domains
89 | const proxyUrl = 'https://' + randomHostname + url.pathname + url.search;
90 | let modifiedRequest = new Request(proxyUrl, {
91 | method: request.method,
92 | headers: newHeaders,
93 | body: request.body,
94 | redirect: 'manual',
95 | });
96 | const proxyResponse = await fetch(modifiedRequest, { redirect: 'manual' });
97 | // Check for 302 or 301 redirect status and return an error response
98 | if ([301, 302].includes(proxyResponse.status)) {
99 | return new Response(`Redirects to ${randomHostname} are not allowed.`, {
100 | status: 403,
101 | statusText: 'Forbidden',
102 | });
103 | }
104 | // Return the response from the proxy server
105 | return proxyResponse;
106 | }
107 | } else {
108 | return await วเลสOverWSHandler(request);
109 | }
110 | } catch (err) {
111 | /** @type {Error} */ let e = err;
112 | return new Response(e.toString());
113 | }
114 | },
115 | };
116 |
117 | export async function uuid_validator(request) {
118 | const hostname = request.headers.get('Host');
119 | const currentDate = new Date();
120 |
121 | const subdomain = hostname.split('.')[0];
122 | const year = currentDate.getFullYear();
123 | const month = String(currentDate.getMonth() + 1).padStart(2, '0');
124 | const day = String(currentDate.getDate()).padStart(2, '0');
125 |
126 | const formattedDate = `${year}-${month}-${day}`;
127 |
128 | // const daliy_sub = formattedDate + subdomain
129 | const hashHex = await hashHex_f(subdomain);
130 | // subdomain string contains timestamps utc and uuid string TODO.
131 | console.log(hashHex, subdomain, formattedDate);
132 | }
133 |
134 | export async function hashHex_f(string) {
135 | const encoder = new TextEncoder();
136 | const data = encoder.encode(string);
137 | const hashBuffer = await crypto.subtle.digest('SHA-256', data);
138 | const hashArray = Array.from(new Uint8Array(hashBuffer));
139 | const hashHex = hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
140 | return hashHex;
141 | }
142 |
143 | /**
144 | * Handles วเลส over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the วเลส header.
145 | * @param {import("@cloudflare/workers-types").Request} request The incoming request object.
146 | * @returns {Promise} A Promise that resolves to a WebSocket response object.
147 | */
148 | async function วเลสOverWSHandler(request) {
149 | const webSocketPair = new WebSocketPair();
150 | const [client, webSocket] = Object.values(webSocketPair);
151 | webSocket.accept();
152 |
153 | let address = '';
154 | let portWithRandomLog = '';
155 | let currentDate = new Date();
156 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
157 | console.log(`[${currentDate} ${address}:${portWithRandomLog}] ${info}`, event || '');
158 | };
159 | const earlyDataHeader = request.headers.get('sec-websocket-protocol') || '';
160 |
161 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);
162 |
163 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
164 | let remoteSocketWapper = {
165 | value: null,
166 | };
167 | let udpStreamWrite = null;
168 | let isDns = false;
169 |
170 | // ws --> remote
171 | readableWebSocketStream.pipeTo(new WritableStream({
172 | async write(chunk, controller) {
173 | if (isDns && udpStreamWrite) {
174 | return udpStreamWrite(chunk);
175 | }
176 | if (remoteSocketWapper.value) {
177 | const writer = remoteSocketWapper.value.writable.getWriter()
178 | await writer.write(chunk);
179 | writer.releaseLock();
180 | return;
181 | }
182 |
183 | const {
184 | hasError,
185 | message,
186 | portRemote = 443,
187 | addressRemote = '',
188 | rawDataIndex,
189 | วเลสVersion = new Uint8Array([0, 0]),
190 | isUDP,
191 | } = processวเลสHeader(chunk, userID);
192 | address = addressRemote;
193 | portWithRandomLog = `${portRemote} ${isUDP ? 'udp' : 'tcp'} `;
194 | if (hasError) {
195 | // controller.error(message);
196 | throw new Error(message); // cf seems has bug, controller.error will not end stream
197 | }
198 |
199 | // If UDP and not DNS port, close it
200 | if (isUDP && portRemote !== 53) {
201 | throw new Error('UDP proxy only enabled for DNS which is port 53');
202 | // cf seems has bug, controller.error will not end stream
203 | }
204 |
205 | if (isUDP && portRemote === 53) {
206 | isDns = true;
207 | }
208 |
209 | // ["version", "附加信息长度 N"]
210 | const วเลสResponseHeader = new Uint8Array([วเลสVersion[0], 0]);
211 | const rawClientData = chunk.slice(rawDataIndex);
212 |
213 | // TODO: support udp here when cf runtime has udp support
214 | if (isDns) {
215 | const { write } = await handleUDPOutBound(webSocket, วเลสResponseHeader, log);
216 | udpStreamWrite = write;
217 | udpStreamWrite(rawClientData);
218 | return;
219 | }
220 | handleTCPOutBound(remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, วเลสResponseHeader, log);
221 | },
222 | close() {
223 | log(`readableWebSocketStream is close`);
224 | },
225 | abort(reason) {
226 | log(`readableWebSocketStream is abort`, JSON.stringify(reason));
227 | },
228 | })).catch((err) => {
229 | log('readableWebSocketStream pipeTo error', err);
230 | });
231 |
232 | return new Response(null, {
233 | status: 101,
234 | webSocket: client,
235 | });
236 | }
237 |
238 | /**
239 | * Handles outbound TCP connections.
240 | *
241 | * @param {any} remoteSocket
242 | * @param {string} addressRemote The remote address to connect to.
243 | * @param {number} portRemote The remote port to connect to.
244 | * @param {Uint8Array} rawClientData The raw client data to write.
245 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
246 | * @param {Uint8Array} วเลสResponseHeader The วเลส response header.
247 | * @param {function} log The logging function.
248 | * @returns {Promise} The remote socket.
249 | */
250 | async function handleTCPOutBound(remoteSocket, addressRemote, portRemote, rawClientData, webSocket, วเลสResponseHeader, log,) {
251 |
252 | /**
253 | * Connects to a given address and port and writes data to the socket.
254 | * @param {string} address The address to connect to.
255 | * @param {number} port The port to connect to.
256 | * @returns {Promise} A Promise that resolves to the connected socket.
257 | */
258 | async function connectAndWrite(address, port) {
259 | /** @type {import("@cloudflare/workers-types").Socket} */
260 | const tcpSocket = connect({
261 | hostname: address,
262 | port: port,
263 | });
264 | remoteSocket.value = tcpSocket;
265 | log(`connected to ${address}:${port}`);
266 | const writer = tcpSocket.writable.getWriter();
267 | await writer.write(rawClientData); // first write, nomal is tls client hello
268 | writer.releaseLock();
269 | return tcpSocket;
270 | }
271 |
272 | /**
273 | * Retries connecting to the remote address and port if the Cloudflare socket has no incoming data.
274 | * @returns {Promise} A Promise that resolves when the retry is complete.
275 | */
276 | async function retry() {
277 | const tcpSocket = await connectAndWrite(พร็อกซีไอพี || addressRemote, portRemote)
278 | tcpSocket.closed.catch(error => {
279 | console.log('retry tcpSocket closed error', error);
280 | }).finally(() => {
281 | safeCloseWebSocket(webSocket);
282 | })
283 | remoteSocketToWS(tcpSocket, webSocket, วเลสResponseHeader, null, log);
284 | }
285 |
286 | const tcpSocket = await connectAndWrite(addressRemote, portRemote);
287 |
288 | // when remoteSocket is ready, pass to websocket
289 | // remote--> ws
290 | remoteSocketToWS(tcpSocket, webSocket, วเลสResponseHeader, retry, log);
291 | }
292 |
293 | /**
294 | * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket.
295 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from.
296 | * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT.
297 | * @param {(info: string)=> void} log The logging function.
298 | * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket.
299 | */
300 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
301 | let readableStreamCancel = false;
302 | const stream = new ReadableStream({
303 | start(controller) {
304 | webSocketServer.addEventListener('message', (event) => {
305 | const message = event.data;
306 | controller.enqueue(message);
307 | });
308 |
309 | webSocketServer.addEventListener('close', () => {
310 | safeCloseWebSocket(webSocketServer);
311 | controller.close();
312 | });
313 |
314 | webSocketServer.addEventListener('error', (err) => {
315 | log('webSocketServer has error');
316 | controller.error(err);
317 | });
318 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
319 | if (error) {
320 | controller.error(error);
321 | } else if (earlyData) {
322 | controller.enqueue(earlyData);
323 | }
324 | },
325 |
326 | pull(controller) {
327 | // if ws can stop read if stream is full, we can implement backpressure
328 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure
329 | },
330 |
331 | cancel(reason) {
332 | log(`ReadableStream was canceled, due to ${reason}`)
333 | readableStreamCancel = true;
334 | safeCloseWebSocket(webSocketServer);
335 | }
336 | });
337 |
338 | return stream;
339 | }
340 |
341 | // https://xtls.github.io/development/protocols/วเลส.html
342 | // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw
343 |
344 | /**
345 | * Processes the วเลส header buffer and returns an object with the relevant information.
346 | * @param {ArrayBuffer} วเลสBuffer The วเลส header buffer to process.
347 | * @param {string} userID The user ID to validate against the UUID in the วเลส header.
348 | * @returns {{
349 | * hasError: boolean,
350 | * message?: string,
351 | * addressRemote?: string,
352 | * addressType?: number,
353 | * portRemote?: number,
354 | * rawDataIndex?: number,
355 | * วเลสVersion?: Uint8Array,
356 | * isUDP?: boolean
357 | * }} An object with the relevant information extracted from the วเลส header buffer.
358 | */
359 | function processวเลสHeader(วเลสBuffer, userID) {
360 | if (วเลสBuffer.byteLength < 24) {
361 | return {
362 | hasError: true,
363 | message: 'invalid data',
364 | };
365 | }
366 |
367 | const version = new Uint8Array(วเลสBuffer.slice(0, 1));
368 | let isValidUser = false;
369 | let isUDP = false;
370 | const slicedBuffer = new Uint8Array(วเลสBuffer.slice(1, 17));
371 | const slicedBufferString = stringify(slicedBuffer);
372 | // check if userID is valid uuid or uuids split by , and contains userID in it otherwise return error message to console
373 | const uuids = userID.includes(',') ? userID.split(",") : [userID];
374 | // uuid_validator(hostName, slicedBufferString);
375 |
376 |
377 | // isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim());
378 | isValidUser = uuids.some(userUuid => slicedBufferString === userUuid.trim()) || uuids.length === 1 && slicedBufferString === uuids[0].trim();
379 |
380 | console.log(`userID: ${slicedBufferString}`);
381 |
382 | if (!isValidUser) {
383 | return {
384 | hasError: true,
385 | message: 'invalid user',
386 | };
387 | }
388 |
389 | const optLength = new Uint8Array(วเลสBuffer.slice(17, 18))[0];
390 | //skip opt for now
391 |
392 | const command = new Uint8Array(
393 | วเลสBuffer.slice(18 + optLength, 18 + optLength + 1)
394 | )[0];
395 |
396 | // 0x01 TCP
397 | // 0x02 UDP
398 | // 0x03 MUX
399 | if (command === 1) {
400 | isUDP = false;
401 | } else if (command === 2) {
402 | isUDP = true;
403 | } else {
404 | return {
405 | hasError: true,
406 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
407 | };
408 | }
409 | const portIndex = 18 + optLength + 1;
410 | const portBuffer = วเลสBuffer.slice(portIndex, portIndex + 2);
411 | // port is big-Endian in raw data etc 80 == 0x005d
412 | const portRemote = new DataView(portBuffer).getUint16(0);
413 |
414 | let addressIndex = portIndex + 2;
415 | const addressBuffer = new Uint8Array(
416 | วเลสBuffer.slice(addressIndex, addressIndex + 1)
417 | );
418 |
419 | // 1--> ipv4 addressLength =4
420 | // 2--> domain name addressLength=addressBuffer[1]
421 | // 3--> ipv6 addressLength =16
422 | const addressType = addressBuffer[0];
423 | let addressLength = 0;
424 | let addressValueIndex = addressIndex + 1;
425 | let addressValue = '';
426 | switch (addressType) {
427 | case 1:
428 | addressLength = 4;
429 | addressValue = new Uint8Array(
430 | วเลสBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
431 | ).join('.');
432 | break;
433 | case 2:
434 | addressLength = new Uint8Array(
435 | วเลสBuffer.slice(addressValueIndex, addressValueIndex + 1)
436 | )[0];
437 | addressValueIndex += 1;
438 | addressValue = new TextDecoder().decode(
439 | วเลสBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
440 | );
441 | break;
442 | case 3:
443 | addressLength = 16;
444 | const dataView = new DataView(
445 | วเลสBuffer.slice(addressValueIndex, addressValueIndex + addressLength)
446 | );
447 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334
448 | const ipv6 = [];
449 | for (let i = 0; i < 8; i++) {
450 | ipv6.push(dataView.getUint16(i * 2).toString(16));
451 | }
452 | addressValue = ipv6.join(':');
453 | // seems no need add [] for ipv6
454 | break;
455 | default:
456 | return {
457 | hasError: true,
458 | message: `invild addressType is ${addressType}`,
459 | };
460 | }
461 | if (!addressValue) {
462 | return {
463 | hasError: true,
464 | message: `addressValue is empty, addressType is ${addressType}`,
465 | };
466 | }
467 |
468 | return {
469 | hasError: false,
470 | addressRemote: addressValue,
471 | addressType,
472 | portRemote,
473 | rawDataIndex: addressValueIndex + addressLength,
474 | วเลสVersion: version,
475 | isUDP,
476 | };
477 | }
478 |
479 |
480 | /**
481 | * Converts a remote socket to a WebSocket connection.
482 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert.
483 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to.
484 | * @param {ArrayBuffer | null} วเลสResponseHeader The วเลส response header.
485 | * @param {(() => Promise) | null} retry The function to retry the connection if it fails.
486 | * @param {(info: string) => void} log The logging function.
487 | * @returns {Promise} A Promise that resolves when the conversion is complete.
488 | */
489 | async function remoteSocketToWS(remoteSocket, webSocket, วเลสResponseHeader, retry, log) {
490 | // remote--> ws
491 | let remoteChunkCount = 0;
492 | let chunks = [];
493 | /** @type {ArrayBuffer | null} */
494 | let วเลสHeader = วเลสResponseHeader;
495 | let hasIncomingData = false; // check if remoteSocket has incoming data
496 | await remoteSocket.readable
497 | .pipeTo(
498 | new WritableStream({
499 | start() {
500 | },
501 | /**
502 | *
503 | * @param {Uint8Array} chunk
504 | * @param {*} controller
505 | */
506 | async write(chunk, controller) {
507 | hasIncomingData = true;
508 | remoteChunkCount++;
509 | if (webSocket.readyState !== WS_READY_STATE_OPEN) {
510 | controller.error(
511 | 'webSocket.readyState is not open, maybe close'
512 | );
513 | }
514 | if (วเลสHeader) {
515 | webSocket.send(await new Blob([วเลสHeader, chunk]).arrayBuffer());
516 | วเลสHeader = null;
517 | } else {
518 | // console.log(`remoteSocketToWS send chunk ${chunk.byteLength}`);
519 | // seems no need rate limit this, CF seems fix this??..
520 | // if (remoteChunkCount > 20000) {
521 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M
522 | // await delay(1);
523 | // }
524 | webSocket.send(chunk);
525 | }
526 | },
527 | close() {
528 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
529 | // safeCloseWebSocket(webSocket); // no need server close websocket frist for some case will casue HTTP ERR_CONTENT_LENGTH_MISMATCH issue, client will send close event anyway.
530 | },
531 | abort(reason) {
532 | console.error(`remoteConnection!.readable abort`, reason);
533 | },
534 | })
535 | )
536 | .catch((error) => {
537 | console.error(
538 | `remoteSocketToWS has exception `,
539 | error.stack || error
540 | );
541 | safeCloseWebSocket(webSocket);
542 | });
543 |
544 | // seems is cf connect socket have error,
545 | // 1. Socket.closed will have error
546 | // 2. Socket.readable will be close without any data coming
547 | if (hasIncomingData === false && retry) {
548 | log(`retry`)
549 | retry();
550 | }
551 | }
552 |
553 | /**
554 | * Decodes a base64 string into an ArrayBuffer.
555 | * @param {string} base64Str The base64 string to decode.
556 | * @returns {{earlyData: ArrayBuffer|null, error: Error|null}} An object containing the decoded ArrayBuffer or null if there was an error, and any error that occurred during decoding or null if there was no error.
557 | */
558 | function base64ToArrayBuffer(base64Str) {
559 | if (!base64Str) {
560 | return { earlyData: null, error: null };
561 | }
562 | try {
563 | // go use modified Base64 for URL rfc4648 which js atob not support
564 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
565 | const decode = atob(base64Str);
566 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
567 | return { earlyData: arryBuffer.buffer, error: null };
568 | } catch (error) {
569 | return { earlyData: null, error };
570 | }
571 | }
572 |
573 | /**
574 | * Checks if a given string is a valid UUID.
575 | * Note: This is not a real UUID validation.
576 | * @param {string} uuid The string to validate as a UUID.
577 | * @returns {boolean} True if the string is a valid UUID, false otherwise.
578 | */
579 | function isValidUUID(uuid) {
580 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
581 | return uuidRegex.test(uuid);
582 | }
583 |
584 | const WS_READY_STATE_OPEN = 1;
585 | const WS_READY_STATE_CLOSING = 2;
586 | /**
587 | * Closes a WebSocket connection safely without throwing exceptions.
588 | * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close.
589 | */
590 | function safeCloseWebSocket(socket) {
591 | try {
592 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
593 | socket.close();
594 | }
595 | } catch (error) {
596 | console.error('safeCloseWebSocket error', error);
597 | }
598 | }
599 |
600 | const byteToHex = [];
601 |
602 | for (let i = 0; i < 256; ++i) {
603 | byteToHex.push((i + 256).toString(16).slice(1));
604 | }
605 |
606 | function unsafeStringify(arr, offset = 0) {
607 | return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
608 | }
609 |
610 | function stringify(arr, offset = 0) {
611 | const uuid = unsafeStringify(arr, offset);
612 | if (!isValidUUID(uuid)) {
613 | throw TypeError("Stringified UUID is invalid");
614 | }
615 | return uuid;
616 | }
617 |
618 |
619 | /**
620 | * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection.
621 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over.
622 | * @param {ArrayBuffer} วเลสResponseHeader The วเลส response header.
623 | * @param {(string) => void} log The logging function.
624 | * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream.
625 | */
626 | async function handleUDPOutBound(webSocket, วเลสResponseHeader, log) {
627 |
628 | let isวเลสHeaderSent = false;
629 | const transformStream = new TransformStream({
630 | start(controller) {
631 |
632 | },
633 | transform(chunk, controller) {
634 | // udp message 2 byte is the the length of udp data
635 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message
636 | for (let index = 0; index < chunk.byteLength;) {
637 | const lengthBuffer = chunk.slice(index, index + 2);
638 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
639 | const udpData = new Uint8Array(
640 | chunk.slice(index + 2, index + 2 + udpPakcetLength)
641 | );
642 | index = index + 2 + udpPakcetLength;
643 | controller.enqueue(udpData);
644 | }
645 | },
646 | flush(controller) {
647 | }
648 | });
649 |
650 | // only handle dns udp for now
651 | transformStream.readable.pipeTo(new WritableStream({
652 | async write(chunk) {
653 | const resp = await fetch(dohURL, // dns server url
654 | {
655 | method: 'POST',
656 | headers: {
657 | 'content-type': 'application/dns-message',
658 | },
659 | body: chunk,
660 | })
661 | const dnsQueryResult = await resp.arrayBuffer();
662 | const udpSize = dnsQueryResult.byteLength;
663 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));
664 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
665 | if (webSocket.readyState === WS_READY_STATE_OPEN) {
666 | log(`doh success and dns message length is ${udpSize}`);
667 | if (isวเลสHeaderSent) {
668 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
669 | } else {
670 | webSocket.send(await new Blob([วเลสResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
671 | isวเลสHeaderSent = true;
672 | }
673 | }
674 | }
675 | })).catch((error) => {
676 | log('dns udp has error' + error)
677 | });
678 |
679 | const writer = transformStream.writable.getWriter();
680 |
681 | return {
682 | /**
683 | *
684 | * @param {Uint8Array} chunk
685 | */
686 | write(chunk) {
687 | writer.write(chunk);
688 | }
689 | };
690 | }
691 |
692 | const at = 'QA==';
693 | const pt = 'dmxlc3M=';
694 | const ed = 'RUR0dW5uZWw=';
695 | /**
696 | *
697 | * @param {string} userID - single or comma separated userIDs
698 | * @param {string | null} hostName
699 | * @returns {string}
700 | */
701 | function getวเลสConfig(userIDs, hostName) {
702 | const commonUrlPart = `:443?encryption=none&security=tls&sni=${hostName}&fp=randomized&type=ws&host=${hostName}&path=%2F%3Fed%3D2048#${hostName}`;
703 | const hashSeparator = "################################################################";
704 |
705 | // Split the userIDs into an array
706 | const userIDArray = userIDs.split(",");
707 |
708 | // Prepare output string for each userID
709 | const output = userIDArray.map((userID) => {
710 | const วเลสMain = atob(pt) + '://' + userID + atob(at) + hostName + commonUrlPart;
711 | const วเลสSec = atob(pt) + '://' + userID + atob(at) + พร็อกซีไอพี + commonUrlPart;
712 | return `UUID: ${userID} ${hashSeparator}\nv2ray default ip
713 | ---------------------------------------------------------------
714 | ${วเลสMain}
715 | Copy วเลสMain
716 | ---------------------------------------------------------------
717 | v2ray with bestip
718 | ---------------------------------------------------------------
719 | ${วเลสSec}
720 | Copy วเลสSec
721 | ---------------------------------------------------------------`;
722 | }).join('\n');
723 | const sublink = `https://${hostName}/sub/${userIDArray[0]}?format=clash`
724 | const subbestip = `https://${hostName}/bestip/${userIDArray[0]}`;
725 | const clash_link = `https://api.v1.mk/sub?target=clash&url=${encodeURIComponent(sublink)}&insert=false&emoji=true&list=false&tfo=false&scv=true&fdn=false&sort=false&new_name=true`;
726 | // Prepare header string
727 | const header = `
728 |
729 | Welcome! This function generates configuration for วเลส protocol. If you found this useful, please check our GitHub project for more:
730 | 欢迎!这是生成 วเลส 协议的配置。如果您发现这个项目很好用,请查看我们的 GitHub 项目给我一个star:
731 | EDtunnel - https://github.com/3Kmfi6HP/EDtunnel
732 |
733 | วเลส 节点订阅连接
734 | Clash for Windows 节点订阅连接
735 | Clash 节点订阅连接
736 | 优选IP自动节点订阅
737 | Clash优选IP自动
738 | singbox优选IP自动
739 | nekobox优选IP自动
740 | v2rayNG优选IP自动
`;
741 |
742 | // HTML Head with CSS and FontAwesome library
743 | const htmlHead = `
744 |
745 | EDtunnel: วเลส configuration
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 |
754 |
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 |
805 |
806 |
807 |
808 |
809 | `;
810 |
811 | // Join output with newlines, wrap inside and
812 | return `
813 |
814 | ${htmlHead}
815 |
816 | ${header}
817 | ${output}
818 |
819 |
830 | `;
831 | }
832 |
833 | const เซ็ตพอร์ตHttp = new Set([80, 8080, 8880, 2052, 2086, 2095, 2082]);
834 | const เซ็ตพอร์ตHttps = new Set([443, 8443, 2053, 2096, 2087, 2083]);
835 |
836 | function สร้างวเลสSub(ไอดีผู้ใช้_เส้นทาง, ชื่อโฮสต์) {
837 | const อาร์เรย์ไอดีผู้ใช้ = ไอดีผู้ใช้_เส้นทาง.includes(',') ? ไอดีผู้ใช้_เส้นทาง.split(',') : [ไอดีผู้ใช้_เส้นทาง];
838 | const ส่วนUrlทั่วไปHttp = `?encryption=none&security=none&fp=random&type=ws&host=${ชื่อโฮสต์}&path=%2F%3Fed%3D2048#`;
839 | const ส่วนUrlทั่วไปHttps = `?encryption=none&security=tls&sni=${ชื่อโฮสต์}&fp=random&type=ws&host=${ชื่อโฮสต์}&path=%2F%3Fed%3D2048#`;
840 |
841 | const ผลลัพธ์ = อาร์เรย์ไอดีผู้ใช้.flatMap((ไอดีผู้ใช้) => {
842 | const การกำหนดค่าHttp = Array.from(เซ็ตพอร์ตHttp).flatMap((พอร์ต) => {
843 | if (!ชื่อโฮสต์.includes('pages.dev')) {
844 | const ส่วนUrl = `${ชื่อโฮสต์}-HTTP-${พอร์ต}`;
845 | const วเลสหลักHttp = atob(pt) + '://' + ไอดีผู้ใช้ + atob(at) + ชื่อโฮสต์ + ':' + พอร์ต + ส่วนUrlทั่วไปHttp + ส่วนUrl;
846 | return พร็อกซีไอพีs.flatMap((พร็อกซีไอพี) => {
847 | const วเลสรองHttp = atob(pt) + '://' + ไอดีผู้ใช้ + atob(at) + พร็อกซีไอพี + ':' + พอร์ต + ส่วนUrlทั่วไปHttp + ส่วนUrl + '-' + พร็อกซีไอพี + '-' + atob(ed);
848 | return [วเลสหลักHttp, วเลสรองHttp];
849 | });
850 | }
851 | return [];
852 | });
853 |
854 | const การกำหนดค่าHttps = Array.from(เซ็ตพอร์ตHttps).flatMap((พอร์ต) => {
855 | const ส่วนUrl = `${ชื่อโฮสต์}-HTTPS-${พอร์ต}`;
856 | const วเลสหลักHttps = atob(pt) + '://' + ไอดีผู้ใช้ + atob(at) + ชื่อโฮสต์ + ':' + พอร์ต + ส่วนUrlทั่วไปHttps + ส่วนUrl;
857 | return พร็อกซีไอพีs.flatMap((พร็อกซีไอพี) => {
858 | const วเลสรองHttps = atob(pt) + '://' + ไอดีผู้ใช้ + atob(at) + พร็อกซีไอพี + ':' + พอร์ต + ส่วนUrlทั่วไปHttps + ส่วนUrl + '-' + พร็อกซีไอพี + '-' + atob(ed);
859 | return [วเลสหลักHttps, วเลสรองHttps];
860 | });
861 | });
862 |
863 | return [...การกำหนดค่าHttp, ...การกำหนดค่าHttps];
864 | });
865 |
866 | return ผลลัพธ์.join('\n');
867 | }
868 |
869 | const cn_hostnames = [
870 | 'weibo.com', // Weibo - A popular social media platform
871 | 'www.baidu.com', // Baidu - The largest search engine in China
872 | 'www.qq.com', // QQ - A widely used instant messaging platform
873 | 'www.taobao.com', // Taobao - An e-commerce website owned by Alibaba Group
874 | 'www.jd.com', // JD.com - One of the largest online retailers in China
875 | 'www.sina.com.cn', // Sina - A Chinese online media company
876 | 'www.sohu.com', // Sohu - A Chinese internet service provider
877 | 'www.tmall.com', // Tmall - An online retail platform owned by Alibaba Group
878 | 'www.163.com', // NetEase Mail - One of the major email providers in China
879 | 'www.zhihu.com', // Zhihu - A popular question-and-answer website
880 | 'www.youku.com', // Youku - A Chinese video sharing platform
881 | 'www.xinhuanet.com', // Xinhua News Agency - Official news agency of China
882 | 'www.douban.com', // Douban - A Chinese social networking service
883 | 'www.meituan.com', // Meituan - A Chinese group buying website for local services
884 | 'www.toutiao.com', // Toutiao - A news and information content platform
885 | 'www.ifeng.com', // iFeng - A popular news website in China
886 | 'www.autohome.com.cn', // Autohome - A leading Chinese automobile online platform
887 | 'www.360.cn', // 360 - A Chinese internet security company
888 | 'www.douyin.com', // Douyin - A Chinese short video platform
889 | 'www.kuaidi100.com', // Kuaidi100 - A Chinese express delivery tracking service
890 | 'www.wechat.com', // WeChat - A popular messaging and social media app
891 | 'www.csdn.net', // CSDN - A Chinese technology community website
892 | 'www.imgo.tv', // ImgoTV - A Chinese live streaming platform
893 | 'www.aliyun.com', // Alibaba Cloud - A Chinese cloud computing company
894 | 'www.eyny.com', // Eyny - A Chinese multimedia resource-sharing website
895 | 'www.mgtv.com', // MGTV - A Chinese online video platform
896 | 'www.xunlei.com', // Xunlei - A Chinese download manager and torrent client
897 | 'www.hao123.com', // Hao123 - A Chinese web directory service
898 | 'www.bilibili.com', // Bilibili - A Chinese video sharing and streaming platform
899 | 'www.youth.cn', // Youth.cn - A China Youth Daily news portal
900 | 'www.hupu.com', // Hupu - A Chinese sports community and forum
901 | 'www.youzu.com', // Youzu Interactive - A Chinese game developer and publisher
902 | 'www.panda.tv', // Panda TV - A Chinese live streaming platform
903 | 'www.tudou.com', // Tudou - A Chinese video-sharing website
904 | 'www.zol.com.cn', // ZOL - A Chinese electronics and gadgets website
905 | 'www.toutiao.io', // Toutiao - A news and information app
906 | 'www.tiktok.com', // TikTok - A Chinese short-form video app
907 | 'www.netease.com', // NetEase - A Chinese internet technology company
908 | 'www.cnki.net', // CNKI - China National Knowledge Infrastructure, an information aggregator
909 | 'www.zhibo8.cc', // Zhibo8 - A website providing live sports streams
910 | 'www.zhangzishi.cc', // Zhangzishi - Personal website of Zhang Zishi, a public intellectual in China
911 | 'www.xueqiu.com', // Xueqiu - A Chinese online social platform for investors and traders
912 | 'www.qqgongyi.com', // QQ Gongyi - Tencent's charitable foundation platform
913 | 'www.ximalaya.com', // Ximalaya - A Chinese online audio platform
914 | 'www.dianping.com', // Dianping - A Chinese online platform for finding and reviewing local businesses
915 | 'www.suning.com', // Suning - A leading Chinese online retailer
916 | 'www.zhaopin.com', // Zhaopin - A Chinese job recruitment platform
917 | 'www.jianshu.com', // Jianshu - A Chinese online writing platform
918 | 'www.mafengwo.cn', // Mafengwo - A Chinese travel information sharing platform
919 | 'www.51cto.com', // 51CTO - A Chinese IT technical community website
920 | 'www.qidian.com', // Qidian - A Chinese web novel platform
921 | 'www.ctrip.com', // Ctrip - A Chinese travel services provider
922 | 'www.pconline.com.cn', // PConline - A Chinese technology news and review website
923 | 'www.cnzz.com', // CNZZ - A Chinese web analytics service provider
924 | 'www.telegraph.co.uk', // The Telegraph - A British newspaper website
925 | 'www.ynet.com', // Ynet - A Chinese news portal
926 | 'www.ted.com', // TED - A platform for ideas worth spreading
927 | 'www.renren.com', // Renren - A Chinese social networking service
928 | 'www.pptv.com', // PPTV - A Chinese online video streaming platform
929 | 'www.liepin.com', // Liepin - A Chinese online recruitment website
930 | 'www.881903.com', // 881903 - A Hong Kong radio station website
931 | 'www.aipai.com', // Aipai - A Chinese online video sharing platform
932 | 'www.ttpaihang.com', // Ttpaihang - A Chinese celebrity popularity ranking website
933 | 'www.quyaoya.com', // Quyaoya - A Chinese online ticketing platform
934 | 'www.91.com', // 91.com - A Chinese software download website
935 | 'www.dianyou.cn', // Dianyou - A Chinese game information website
936 | 'www.tmtpost.com', // TMTPost - A Chinese technology media platform
937 | 'www.douban.com', // Douban - A Chinese social networking service
938 | 'www.guancha.cn', // Guancha - A Chinese news and commentary website
939 | 'www.so.com', // So.com - A Chinese search engine
940 | 'www.58.com', // 58.com - A Chinese classified advertising website
941 | 'www.cnblogs.com', // Cnblogs - A Chinese technology blog community
942 | 'www.cntv.cn', // CCTV - China Central Television official website
943 | 'www.secoo.com', // Secoo - A Chinese luxury e-commerce platform
944 | ];
945 |
--------------------------------------------------------------------------------