├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ └── config.yml
└── workflows
│ └── build.yml
├── .gitignore
├── LICENSE
├── README.md
├── README_fa.md
├── _worker.js
├── docs
├── assets
│ └── images
│ │ ├── Bind_kv.jpg
│ │ ├── Clean_ip_domain.jpg
│ │ ├── Connect_to_git.jpg
│ │ ├── Fork_repo.jpg
│ │ ├── Fragment-Configs.jpg
│ │ ├── Fragment-Settings.jpg
│ │ ├── Fragment-Sub.jpg
│ │ ├── Full-Normal-Configs.jpg
│ │ ├── Hiddify_fragment.jpg
│ │ ├── My-IP.jpg
│ │ ├── Nav_dash_kv.jpg
│ │ ├── Navigate_worker_dash.jpg
│ │ ├── Normal-Configs.jpg
│ │ ├── Pages_add_variables.jpg
│ │ ├── Pages_application.jpg
│ │ ├── Pages_bind_kv.jpg
│ │ ├── Pages_env_vars.jpg
│ │ ├── Pages_production_details.jpg
│ │ ├── Pages_retry_deployment.jpg
│ │ ├── Pages_variables.jpg
│ │ ├── Panel.jpg
│ │ ├── Proxy_ips.jpg
│ │ ├── Routing_rules.jpg
│ │ ├── Settings_functions.jpg
│ │ ├── Sync_fork.jpg
│ │ ├── VLESS_Trojan_settings.jpg
│ │ ├── Warp-Configs.jpg
│ │ ├── Warp-Pro-Configs.jpg
│ │ ├── Warp-Pro-Settings.jpg
│ │ ├── Warp-Settings.jpg
│ │ ├── Worker_mobile_upload.jpg
│ │ ├── Workers_add_variables.jpg
│ │ └── Workers_variables.jpg
├── configuration_fa.md
├── faq.md
├── pages_installation_fa.md
└── worker_installation_fa.md
├── locations.json
├── package.json
└── src
├── authentication
└── auth.js
├── cores-configs
├── clash.js
├── helpers.js
├── normalConfigs.js
├── sing-box.js
└── xray.js
├── helpers
├── helpers.js
└── init.js
├── kv
└── handlers.js
├── pages
├── error.js
├── home.js
└── login.js
├── protocols
├── trojan.js
├── vless.js
└── warp.js
└── worker.js
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report | گزارش مشکل
2 | description: Report a bug encountered while using BPB Panel
3 | body:
4 | - type: markdown
5 | attributes:
6 | value: |
7 | Please make sure to provide a descriptive report. It saves time for both the developer and users who are looking for solutions. Providing as much information as possible, including screenshots and logs, is highly appreciated. This will help us to better understand the issue and respond more effectively.
8 | لطفا مطمئن شوید که گزارشی تشریحی ارائه دهید. این باعث میشود که توسعهدهنده و کاربرانی که در جستجوی راهکارها هستند، زمان کمتری صرف کنند. ارائه اطلاعات بیشتر از جمله اسکرینشات و لاگ توصیه میشود. این باعث میشود که ما بهتر بتوانیم مشکل را درک کرده و بهصورت بهتر پاسخ دهیم.
9 | - type: checkboxes
10 | id: confirm-search
11 | attributes:
12 | label: Attention | توجه
13 | description: Please search [existing issues](https://github.com/bia-pain-bache/BPB-Worker-Panel/issues) before reporting | لطفا قبل از ارسال گزارش [ایشوهای موجود](https://github.com/bia-pain-bache/BPB-Worker-Panel/issues) را بررسی کنید.
14 | options:
15 | - label: I searched and no similar issues were found | جستجو کردم و هیچ گزارش مشابهی پیدا نشد
16 | required: true
17 | - type: textarea
18 | id: problem
19 | attributes:
20 | label: What Happened? | چه اتفاقی افتاده؟
21 | description: |
22 | Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner.
23 | لطفاً اطلاعات کاملی را ارائه دهید. اگر این کار را انجام ندهید، ممکن است مشکل شما به صورت تمام شده تلقی شده یا در زمان مناسبی بررسی نشود.
24 | validations:
25 | required: true
26 | - type: textarea
27 | id: reproduce
28 | attributes:
29 | label: Minimal Reproducible Example | چه پروسهای برای مشاهده این مشکل طی کردهاید؟
30 | placeholder: |
31 | 1. Go to '...'
32 | 2. Click on '....'
33 | 3. Scroll down to '....'
34 | 4. See error
35 | - type: textarea
36 | id: logs
37 | attributes:
38 | label: Relevant log output | لاگ برنامه یا پنل
39 | description: Refer to the program menu and copy the log and send it to us.
40 | render: shell
41 | validations:
42 | required: true
43 | - type: textarea
44 | id: panel-version
45 | attributes:
46 | label: Panel version | نسخهی پنل
47 | validations:
48 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: FAQ | سوالات متداول
4 | url: https://github.com/bia-pain-bache/BPB-Worker-Panel/blob/main/docs/faq.md
5 | about: Before asking a question or presenting a problem, read through the frequently asked questions.
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Worker
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Check out the code
17 | uses: actions/checkout@v4
18 |
19 | - name: Set up Node.js
20 | uses: actions/setup-node@v4
21 | with:
22 | node-version: "latest"
23 |
24 | - name: Install dependencies
25 | run: npm install
26 |
27 | - name: Build project
28 | run: |
29 | npx wrangler deploy src/worker.js --name bpb-worker-panel --compatibility-flag [nodejs_compat] --compatibility-date 2024-10-26 --dry-run --outdir=dist
30 | npx javascript-obfuscator dist/worker.js --output _worker.js
31 |
32 | - name: Commit and push built worker
33 | run: |
34 | git config --global user.name "github-actions[bot]"
35 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
36 | git add _worker.js
37 | git commit -m "Automated build: update _worker.js"
38 | git push
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !README.md
3 | !README_fa.md
4 | !location.json
5 | !docs/
6 | !docs/**
7 | !.github/
8 | !.github/**
9 | !src/
10 | !src/**
11 | !package.json
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
💦 BPB Panel
2 |
3 | ### 🌏 Readme in [Farsi](README_fa.md)
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Introduction
11 | This project is dedicated to developing a user panel for the [Cloudflare-workers/pages proxy script](https://github.com/yonggekkk/Cloudflare-workers-pages-vless) created by [yonggekkk](https://github.com/yonggekkk). The panel offers two deployment options:
12 | - **Worker** deployment
13 | - **Pages** deployment
14 |
15 |
16 | 🌟 If you found **BPB Panel** valuable, Your donations make all the difference 🌟
17 | - **USDT (BEP20):** `0x111EFF917E7cf4b0BfC99Edffd8F1AbC2b23d158`
18 |
19 | ## Features
20 |
21 | 1. **Free**: No cost involved.
22 | 2. **User-Friendly Panel:** Designed for easy navigation, configuration and usage.
23 | 3. **Protocols:** Provides VLESS, Trojan and Wireguard (Warp) protocols.
24 | 4. **Warp Pro configs:** Optimized Warp for crucial circumstances.
25 | 5. **Support Fragment:** Supports Fragment functionality for crucial network situations.
26 | 6. **Full routing rules:** Bypassing Iran/China/Russia and LAN, Blocking QUIC, Porn, Ads, Malwares, Phishing...
27 | 7. **Chain Proxy:** Capable of adding a chain proxy to fix IP.
28 | 8. **Supports Wide Range of Clients:** Offers subscription links for Xray, Sing-box and Clash core clients.
29 | 9. **Password-Protected Panel:** Secure your panel with password protection.
30 | 10. **Fully customizable:** Ability to use online scanner and setting up clean IP-domains, Proxy IP, setting DNS servers, choosing ports and protocols, Warp endpoints...
31 |
32 |
33 | ## How to use:
34 | - [Installation (Pages)](docs/pages_installation_fa.md)
35 |
36 | - [Installation (Worker)](docs/worker_installation_fa.md)
37 |
38 | - [How to use](docs/configuration_fa.md)
39 |
40 | - [FAQ](docs/faq.md)
41 |
42 |
43 | ## Supported Clients
44 | | Client | Version | Fragment | Warp Pro |
45 | | :-------------: | :-------------: | :-------------: | :-------------: |
46 | | **v2rayNG** | 1.8.19 or higher | :heavy_check_mark: | :x: |
47 | | **v2rayN** | 6.42 or higher | :heavy_check_mark: | :x: |
48 | | **v2rayN-PRO** | 1.4 or higher | :heavy_check_mark: | :heavy_check_mark: |
49 | | **Nekobox** | | :x: | :x: |
50 | | **Sing-box** | 1.8.10 or higher | :x: | :x: |
51 | | **Streisand** | | :heavy_check_mark: | :x: |
52 | | **V2Box** | | :x: | :x: |
53 | | **Shadowrocket** | | :x: | :x: |
54 | | **Nekoray** | | :heavy_check_mark: | :x: |
55 | | **Hiddify** | 2.0.5 or higher | :x: | :heavy_check_mark: |
56 | | **NikaNG** | | :heavy_check_mark: | :heavy_check_mark: |
57 | | **Clash Meta** | | :x: | :x: |
58 | | **Clash Verge Rev** | | :x: | :x: |
59 | | **FLClash** | | :x: | :x: |
60 |
61 |
62 | ---
63 |
64 | ## Stargazers Over Time
65 | [](https://starchart.cc/bia-pain-bache/BPB-Worker-Panel)
66 |
67 | ---
68 |
69 | ### Special Thanks
70 | - CF-vless code author [3Kmfi6HP](https://github.com/3Kmfi6HP/EDtunnel)
71 | - CF preferred IP program author [badafans](https://github.com/badafans/Cloudflare-IP-SpeedTest), [XIU2](https://github.com/XIU2/CloudflareSpeedTest)
72 |
73 | ---
74 |
75 | For a detailed tutorial on the core script, please refer to [Yongge’s blog and video tutorials](https://ygkkk.blogspot.com/2023/07/cfworkers-vless.html).
76 |
--------------------------------------------------------------------------------
/README_fa.md:
--------------------------------------------------------------------------------
1 | 💦 پنل BPB
2 |
3 | #### 🌏 [English](README.md)
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## معرفی
11 |
12 | این پروژه توسعهی یک پنل کاربری برای اسکریپت پروکسی Cloudflare-workers/pages ایجاد شده توسط yonggekkk میباشد.
13 |
14 | ### این پنل به دو روش راهاندازی میشود:
15 |
16 | - راهاندازی با **Cloudflare Worker**
17 | - راهاندازی با **Cloudflare Worker**
18 |
19 |
20 | 🌟 اگر پروژهی **BPB Panel** براتون مفید بوده، حمایت شما مایهی دلگرمی من هست 🌟
21 |
22 | 0x111EFF917E7cf4b0BfC99Edffd8F1AbC2b23d158
:USDT (BEP20)
23 |
24 |
25 | ## ویژگیها
26 |
27 |
28 | - رایگان
29 | - پنل کاربری راحت: قابلیت آسان تنظیمات و دریافت کانفیگ ها و لینک های اشتراک.
30 | - پروتکلهای متنوع: ارائه کانفیگهای VLESS، Trojan و Warp.
31 | - سابسکریپشن Warp Pro: ارائهی کانفیگهای وارپ بهینه شده برای شرایط همیشه خاص ایران
32 | - پشتیبانی از فرگمنت: قابل استفاده حتی در صورت فیلتر شدن دامنه.
33 | - قوانین مسیریابی کامل: شامل دور زدن سایتهای ایرانی و چینی، روسی و دسترسی مستقیم به LAN، مسدودسازی تبلیغات ایرانی و خارجی و پورن و پروتکل QUIC
34 | - زنجیرهی Proxy: قابلیت اضافه کردن Proxy خروجی جهت تثبیت IP.
35 | - پشتیبانی از طیف وسیعی از برنامهها: لینکهای اشتراک را برای انواع نرم افزار ها با هستههای Xray و Sing-box و Clash ارائه میدهد.
36 | - پنل با رمز عبور محافظت شده: ایمنسازی پنل با استفاده از رمز عبور.
37 | - سفارشیسازی کامل تنظیمات: قابلیت اسکن و تنظیم IP تمیز، Proxy IP، DNS سرورها، پورتها، پروتکلها و Warp endpoint و ...
38 |
39 |
40 |
41 | ## نحوهی راهاندازی، تنظیمات و استفاده
42 | - [نصب به صورت Pages](docs/pages_installation_fa.md)
43 | - [نصب به صورت Worker](docs/worker_installation_fa.md)
44 | - [نحوه استفاده از پنل](docs/configuration_fa.md)
45 | - [پرسشهای متداول (FAQ)](docs/faq.md)
46 |
47 |
48 | ## برنامههای پشتیبانی شده
49 |
50 |
51 |
52 | برنامه |
53 | نسخه |
54 | Fragment |
55 | Warp Pro |
56 |
57 |
58 |
59 | v2rayNG |
60 | 1.8.19 و بالاتر |
61 | ✔️ |
62 | ❌ |
63 |
64 |
65 | v2rayN |
66 | 6.42 و بالاتر |
67 | ✔️ |
68 | ❌ |
69 |
70 |
71 | v2rayN-Pro |
72 | 1.4 و بالاتر |
73 | ✔️ |
74 | ✔️ |
75 |
76 |
77 | Nekobox |
78 | |
79 | ❌ |
80 | ❌ |
81 |
82 |
83 | Sing-box |
84 | 1.8.10 و بالاتر |
85 | ❌ |
86 | ❌ |
87 |
88 |
89 | Streisand |
90 | |
91 | ✔️ |
92 | ❌ |
93 |
94 |
95 | V2Box |
96 | |
97 | ❌ |
98 | ❌ |
99 |
100 |
101 | Shadowrocket |
102 | |
103 | ❌ |
104 | ❌ |
105 |
106 |
107 | Nekoray |
108 | |
109 | ✔️ |
110 | ❌ |
111 |
112 |
113 | Hiddify |
114 | 2.0.5 و بالاتر |
115 | ❌ |
116 | ✔️ |
117 |
118 |
119 | NikaNG |
120 | |
121 | ✔️ |
122 | ✔️ |
123 |
124 |
125 | Clash Meta |
126 | |
127 | ❌ |
128 | ❌ |
129 |
130 |
131 | Clash Verg Rev |
132 | |
133 | ❌ |
134 | ❌ |
135 |
136 |
137 | FLClash |
138 | |
139 | ❌ |
140 | ❌ |
141 |
142 |
143 |
144 |
145 |
146 | ---
147 | ## تعداد ستارهها به مرور زمان
148 |
149 | [](https://starchart.cc/bia-pain-bache/BPB-Worker-Panel)
150 |
151 | ---
152 | ### تشکر ویژه
153 |
154 | - نویسنده کد CF-vless 3Kmfi6HP
155 | - نویسنده برنامه IP ترجیحی CF badafans، XIU2
156 |
157 | ---
158 | برای آموزش جزئیات اسکریپت اصلی، لطفاً به وبلاگ و آموزشهای ویدیویی Yongge مراجعه کنید.
159 |
--------------------------------------------------------------------------------
/docs/assets/images/Bind_kv.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Bind_kv.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Clean_ip_domain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Clean_ip_domain.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Connect_to_git.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Connect_to_git.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Fork_repo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Fork_repo.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Fragment-Configs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Fragment-Configs.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Fragment-Settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Fragment-Settings.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Fragment-Sub.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Fragment-Sub.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Full-Normal-Configs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Full-Normal-Configs.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Hiddify_fragment.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Hiddify_fragment.jpg
--------------------------------------------------------------------------------
/docs/assets/images/My-IP.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/My-IP.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Nav_dash_kv.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Nav_dash_kv.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Navigate_worker_dash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Navigate_worker_dash.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Normal-Configs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Normal-Configs.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_add_variables.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_add_variables.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_application.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_application.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_bind_kv.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_bind_kv.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_env_vars.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_env_vars.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_production_details.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_production_details.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_retry_deployment.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_retry_deployment.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Pages_variables.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Pages_variables.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Panel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Panel.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Proxy_ips.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Proxy_ips.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Routing_rules.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Routing_rules.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Settings_functions.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Settings_functions.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Sync_fork.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Sync_fork.jpg
--------------------------------------------------------------------------------
/docs/assets/images/VLESS_Trojan_settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/VLESS_Trojan_settings.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Warp-Configs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Warp-Configs.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Warp-Pro-Configs.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Warp-Pro-Configs.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Warp-Pro-Settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Warp-Pro-Settings.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Warp-Settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Warp-Settings.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Worker_mobile_upload.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Worker_mobile_upload.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Workers_add_variables.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Workers_add_variables.jpg
--------------------------------------------------------------------------------
/docs/assets/images/Workers_variables.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fusurus/BPB-Worker-Panel/2cb307bb0e8a118d5165e73c4c89d5fb96284ab2/docs/assets/images/Workers_variables.jpg
--------------------------------------------------------------------------------
/docs/configuration_fa.md:
--------------------------------------------------------------------------------
1 | نحوهی استفاده و تنظیمات
2 |
3 | فرض کنید اسم worker یا pages شما هست `worker-polished-leaf-d022`:
4 |
5 |
6 | شما میتونید Panel رو با اضافه کردن `panel/` انتهاش مثل زیر ببینید:
7 |
8 | >`https://worker-polished-leaf-d022.workers.dev/panel`
9 |
10 |
11 | ازتون میخواد پسورد جدید بذارید و لاگین کنید و تمام.
12 | > [!IMPORTANT]
13 | > پسورد باید حداقل **8 کاراکتر** باشه و حداقل یه **حرف بزرگ** و یه **عدد** هم توش باشه. برای دفعات بعدی از پایین پنل هم میتونید پسورد رو عوض کنید.
14 | حالا بریم بخشای مختلف پنل رو بررسی کنیم:
15 |
16 |
17 | # 1 - سابسکریپشن Normal
18 |
19 |
20 |
21 |
22 |
23 | از این بخش شروع کردم چون خیلیا دوست دارن بدون فرگمنت یا تنظیمات پنل استفاده کنن، ولی حواستون باشه باید خودتون بلد باشید روی اپلیکیشنها تنظیم کنید وگرنه امکان داره به مشکل بخورید. توصیه میکنم از سابهای `Full Normal` استفاده کنید چون تمام تنظیمات پنل روشون اعمال شده و نیازی نیست کار خاصی بکنید. توجه داشته باشید Routing Rules یا Chain Proxy و تنظیمات DNS پنل روی این ساب اعمال نمیشه (باید دستی در برنامه تنظیم کنید).
24 | این لینک 6 تا کانفیگ بهتون میده. (از بخش تنظیمات IP تمیز و Port و Protocol میتونید تعداد کانفیگها رو زیاد کنید) حالا فرق این 6 تا کانفیگ چیه؟
25 |
26 | - **مسیر کانفیگ Websocket Path:** هر کانفیگ یه مسیر متفاوت داره.
27 | - **آدرس کانفیگ:** از این 6 تا کانفیگ اولیش دامنهی خود ورکرتونه، دومیش آدرس www.speedtest.net هست که روی اکثر اپراتورا تمیزه، ، و 3 تا 6 هم IP های دامنهی خودتون که اینام معمولا تمیزن. دوتا IPv4 و دوتا IPv6.
28 |
29 | حالا چطوری میشه تعدادشون رو اضافه کرد؟ توضیحات و تنظیمات بیشتر رو در بخشهای [اضافه کردن IP تمیز](#1-4--تنظیمات-ip-تمیز) و [اضافه کردن پورت](#1-9--انتخاب-port) , [انتخاب پروتکلها](#1-8--انتخاب-protocol) و [اضافه کردن Custom CDN](#1-6--تنظیمات-custom-cdn) توضیح دادیم.
30 | > [!CAUTION]
31 | >برای استفاده از این ساب Mux رو از تنظیمات هر اپلیکیشنی که استفاده میکنید خاموش کنید.
32 |
33 | > [!WARNING]
34 | > با استفاده از این Worker مرتب IP دیوایس شما تغییر میکنه، بنابراین برای کارهایی مثل ترید و PayPal و حتی سایتهایی مثل Hetzner که حساس هستن ازش استفاده نکنید، امکان Ban شدن هست. در مورد فیکس کردن IP دو تا راهحل دادیم، یکی [تنظیم Proxy IP](#1-2--تنظیمات-proxy-ip) موقع راهاندازی و دومی [استفاده از Chain Proxy](#1-3--تنظیمات-chain-proxy).
35 |
36 |
37 | # 2 - سابسکریپشن Full Normal
38 |
39 |
40 |
41 |
42 |
43 | این ساب کانفیگای بالایی رو میده با این تفاوت که تمام تنظیمات قست VLESS / Trojan پنل روش اعمال میشه که میتونید [اینجا](#1--تنظیمات-vlesstrojan) یاد بگیرید، همینطور این سابها کانفیگ **Best Ping** دارن (توضیح در ادامه). با اعمال تنظیمات routing تا حدود 90% تبلیغات ایرانی و خارجی رو بلاک میکنه، سایتهای ایرانی و چینی رو دور میزنه (نیاز به خاموش کردن VPN برای درگاه پرداخت و ... نیست)، LAN رو دور میزنه، پورن و QUIC رو مسدود میکنه و همینطور ساب Sing-box محتواهای Phishing و Malware و ... رو تا حد خوبی مسدود میکنه.
44 | > [!TIP]
45 | > کانفیگ **Best Ping** چیه؟ این کانفیگ اومده همهی کانفیگای پنل رو ادغام کرده و هر 30 ثانیه چک میکنه کدوم کانفیگ سرعت بهتری داره به اون وصل میشه! اگر ip تمیز وارد کرده باشید یا پروتکل تروجان فعال کنید یا پورتای دیگه انتخاب کنید، اونم داخل Best Ping اضافه میشه. این نوع کانفیگ رو توی سابهای Fragment و Warp هم در ادامه داریم.
46 |
47 |
48 | # 3 - سابسکریپشن Fragment
49 |
50 |
51 |
52 |
53 |
54 | > [!NOTE]
55 | > **خواص کانفیگای Fragment**
56 | >
57 | > 1- اتصال حتی در صورت فیلتر شدن دامنهی شخصی یا ورکر
58 | >
59 | > 2- بهبود کیفیت و سرعت روی همهی اپراتورا مخصوصا اونایی که اختلال دارن روی کلادفلر
60 |
61 |
62 |
63 | ## 3-1 - سابسکریپشن Fragment برای Xray
64 |
65 | منظور برنامههایی هستن که از هستهی Xray استفاده میکنن، در اصل ردیف اول جدول FRAGMENT SUB در پنل، وارد کردنش داخل اپ هم مثل ساب معمولیه. کانفیگهای این قسمت یه `F` تو اسمشون دارن.
66 |
67 | این ساب همون تعداد کانفیگ که تو ساب Full Normal داشتید رو با فرگمنت بهتون میده (با تنظیمات فرگمنتی که توی پنل اعمال کردید) به اضافهی کانفیگ **Best Fragment** و **Workerless**. شما هر تنظیماتی که داخل پنل انجام بدید وقتی ساب رو آپدیت کنید روی همهی کانفیگا اعمال میشه.
68 |
69 | > [!TIP]
70 | > کانفیگ WorkerLess بدون ورکر خیلی از سایتها و اپلیکیشنهای فیلتر شده رو باز میکنه! مثل یوتیوب، توییتر، Google Play و سایتهای فیلتر شدهی دیگه که اینجا جا نمیشه. حواستون باشه که این کانفیگ چون از ورکر استفاده نمیکنه IP شما رو تغییر نمیده بنابراین برای کارهای امنیتی مناسب نیست. تغییراتی که برای فرگمنت در پنل اعمال میکنید روی این کانفیگ هم اعمال میشه بجز Chain Proxy.
71 |
72 | > [!TIP]
73 | > کانفیگ Best Fragment میاد 18 مقدار مختلف فرگمنت رو اعمال میکنه و هر کدوم سرعت بیشتری داشته باشن رو بر اساس اپراتور شما انتخاب مبکنه! این 18 حالت جوری انتخاب شده که هیچ بازهای از قلم نیافته و کانفیگ تمام بازههای کوچیک و بزرگ رو هر 1 دقیقه تست کنه و به بهترینش وصل بشه.
74 | تنظیمات پیشرفته مربوط به فرگمنت هم [اینجا](#4-2--تنظیمات-fragment) توضیح داده شده.
75 |
76 |
77 | ## 3-2- اسابسکریپشن Fragment برای Hiddify
78 |
79 | ردیف دوم جدول FRAGMENT SUB برای استفادهی فرگمنت روی برنامهی Hiddify هست، با یک تفاوت. بخاطر محدودیتهای این برنامه خیلی از تنظیمات پنل روی این ساب اعمال نمیشه، در اصل خود برنامه بازنویسی میکنه این تنظیمات رو. کانفیگهای این قسمت یه `F` تو اسمشون دارن. تنظیمات زیر رو باید دستی در برنامه Hiddify اعمال کنید و در حال حاضر از پنل اعمال نمیشن:
80 |
81 | 1. Remote DNS
82 | 2. Local DNS
83 | 3. Fragment Packets
84 | 4. Routing
85 |
86 | > [!CAUTION]
87 | > 1- حتما Remote DNS رو از تنظیمات برنامه تغییر بدید به یک DOH مثل:
88 | > https://8.8.8.8/dns-query یا https://94.140.14.14/dns-query
89 | > در صورتی که از ...//:udp یا IP خالی این قسمت استفاده کنید ورکرتون کار نمیکنه.
90 | >
91 | > 2- اگر در برنامه دستی Fragment رو روشن کرده باشید تنظیمات فرگمنت پنل اعمال نمیشه.
92 |
93 | البته یه راه دیگه هم هست و اونم اینکه ساب نرمال رو وارد برنامه Hiddify بکنید و مثل عکس زیر فرگمنت رو خودتون فعال کنید:
94 |
95 |
96 |
97 |
98 |
99 | # 4- سابسکریپشن Warp
100 |
101 |
102 |
103 |
104 |
105 | این ساب یه کانفیگ Warp میده که IP کلادفلر ایران هست و یه کانفیگ Warp on Warp (به اختضار WoW) که IP خارجه (در حال حاضر بدلیل تغییرات کلادفلر بعضی وقتا IP ایران میده) و یه کافیگ Warp Best Ping که میاد به سریعترین کانفیگ Warp وصل میشه و همیشه IP ایران داره و یه کانفیگ WoW Best Ping که به سریعترین کانفیگ WoW وصل میشه که IP خارجه. بصورت پبشفرض یه کانفیگ Warp و WoW بیشتر نیست ولی اگر قسمت Endpoints رو ویرایش کنید به تعداد Endpoint های ورودی کانفیگ Warp و WoW اضافه میشه.
106 |
107 | در نظر داشته باشید حتما از اسکنر استفاده کنید برای پیدا کردن Endpoint مناسب روی اپراتور خودتون. اسکریپت اسکنر داخل پنل هست، کپی کنید و داخل Termux روی اندروید اجراش کنید. برای قرار دادن داخل پنل قسمت تنظیمات پیشرفته 7-4 رو بخونید. ساب وارپ معمولی ممکنه روی یه سری اپراتور مثل ایرانسل خوب کار کنه ولی برای بقیه از ساب Warp Pro استفاده کنید.
108 |
109 |
110 | # 5- سابسکریپشن Warp PRO
111 |
112 |
113 |
114 |
115 |
116 | توسعههای جدیدی رو هستههای Xray و Singbox توسط GFW-Knocker و تیم Hiddify برای وارپ صورت گرفته که خروجیش شده برنامههای MahsaNG، NikaNG، v2rayN-PRO و Hiddify که این امکان رو به ما داده برای شرایط ایران اتصال به وارپ رو بهینه کنیم، مشابه کاری که بچهها روی Oblivion انجام میدن. بنابراین WARP PRO SUB رو به پنل اضافه کردم که از قسمت تنظیمات WARP PRO SETTINGS میشه شخصیسازی کرد. مقادیر بهینه برای هر اپراتور بصرت تجربی بدست میاد که مثل تنظیمات فرگمنت ممکنه در زمانهای مختلف هم فرق داشته باشن، اما مقادیر پیشفرض تست شدن و در حال حاضر خوب کار مکنن، صرفا باید Endpoint مناسب بذارید.
117 |
118 | > [!CAUTION]
119 | > اپ Hiddify هم باید حداقل ورژنش 2.0.5 باشه.
120 |
121 |
122 | جدول My IP
123 |
124 |
125 |
126 |
127 |
128 | بعد از وصل شدن به پروکسی میتونید صفحه رو رفرش کنید و به این جدول مراجعه کنید و ببینید IP هاتون چیه. این جدول دو ردیف داره، ردیف اول IP شما رو برای آدرسهاس Cloudflare نشون میده، اگر Proxy IP داشته باشید IP شما برای آدرسهای Cloudflare همین Proxy IP خواهد بود و برای مابقی آدرسها یک IP تصادفی کلادفلر. بنابراین از این طریق میتونید چک کنید Proxy IP که انتخاب کردید اعمال شده یا نه. اگر با کانفیگهای Warp وصل بشید هم قاعدتا هر دو ردیف جدول باید یک IP رو نشون بدن. دقت کنید برای عملکرد درست این قسمت اگر از افزونهی uBlock Origin استفاده میکنید باید غیرفعالش کنید.
129 |
130 |
131 | تنظیمات پیشرفته
132 |
133 | اول بگم اگر هر تغییر اشتباهی دادید نگران نباشید، بغل دکمه APPLY SETTINGS یه دکمه Reset گذاشتم که پنل رو به تنظیمات پیشفرض برمیگردونه.
134 |
135 |
136 | ## 1- تنظیمات VLESS/TROJAN
137 |
138 |
139 |
140 |
141 |
142 | این قسمت برای تنظیمات کانفیگای Fragment و سابهای Clash و Singbox هست و تاثیری روی کانفیگهای بخش نرمال v2ray و همچنین سابهای وارپ نداره.
143 |
144 | ### 1-1- سرورهای DNS
145 |
146 | بطور پیشفرض من Google DOH رو گذاشتم برای Remote DNS و Google DNS رو گذاشتم برای Local DNS. یعنی تو کانفیگ پیشفرض اینه:
147 |
148 | >`Remote DNS: https://8.8.8.8/dns-query`
149 | >
150 | >`Local DNS: 8.8.8.8`
151 |
152 | > [!CAUTION]
153 | > به هیچ عنوان از `https://1.1.1.1/dns-query` یا `https://cloudflare-dns.com/dns-query` برای remote DNS استفاده نکنید چون پینگ رو بالا میبره و کانکشن ناپایدار میشه.
154 |
155 | > [!TIP]
156 | > از ورژن 2.5.5 به بعد میتونید از DOH یا DOT های رسمی استفاده کنید و مطمئن باشید که بهترین عملکرد رو دارن، برای مثال چندتا رو اینجا میذارم:
157 | >
158 | > `https://dns.google/dns-query`
159 | >
160 | > `https://dns.adguard-dns.com/dns-query`
161 | >
162 | > `https://dns.quad9.net/dns-query`
163 | >
164 | > `tls://dns.google`
165 |
166 | میتونید Fake DNS رو هم فعال کنید، به سرعت DNS کمک میکنه ولی حواستون باشه ممکنه با بعضی برنامهها سازگار نباشه و یا DNS سیستم رو درگیر کنه، بنابراین اگر نمیدونید دقیقا چیه ترجیحا فعالش نکنید.
167 |
168 |
169 | ### 1-2- تنظیمات Proxy IP
170 |
171 | برای تغییر Proxy IP از ورژن 2.3.5 به بعد میتونید از طریق خود پنل انجام بدید، به این ترتیب که اعمال میکنید و ساب رو آپدیت میکنید و تمام. اما توصیه میکنم از روش قدیمی داشبورد کلادفلر انجام بدید چون:
172 |
173 | > [!CAUTION]
174 | > اگر از طریق پنل Proxy IP رو اعمال کنید و اون IP از کار بیافته، باید یه IP جایگزین کنید و ساب رو آپدیت کنید. معنیش اینه که اگر کانفیگ اهدا کرده باشید و Proxy IP رو تغییر بدید دیگه فایدهای نداره چون یوزر ساب نداره که کانفیگ رو آپدیت کنه. بنابراین توصیه میشه از این روش فقط برای مصرف شخصی استفاده کنید. اما خوبی روش قدیمی اینه که نیازی به آپدیت کردن کانفیگها نداره.
175 |
176 | برای مثال میتونید از لینک زیر Proxy IP انتخاب کنید، یه تعدادی IP نشون میده که میتونید کشورشون رو هم چک کنید و یک یا چندتا انتخاب کنید:
177 |
178 | >[Proxy IP](https://www.nslookup.io/domains/bpb.yousef.isegaro.com/dns-records/)
179 |
180 | یا میتونید از [این آموزش](https://github.com/bia-pain-bache/BPB-Worker-Panel/blob/main/docs/proxy-ip-scanner.md) برای خودتون اسکن کنید. البته در حال حاضر ممکنه اسکنر خوب جواب نده، میتونید امتحان کنید.
181 |
182 | > [!TIP]
183 | > اگر خواستید چند Proxy IP داشته باشید میتونید با ویرگول وارد کنید، مثل `151.213.181.145`,`5.163.51.41`,`bpb.yousef.isegaro.com`
184 |
185 |
186 |
187 | ### 1-3- تنظیمات Chain Proxy
188 |
189 | قبلا گفتیم که میشه یه دونه Proxy IP گذاشت و IP رو برای سایتهای پشت کلادفلر ثابت کرد، اما کماکان وقتی سایتای معمولی رو باز میکنیم IP ما متعلق به ورکر هست که اینم هر چند وقت یک بار عوض میشه. برای اینکه کلا برای همهی سایتها IP رو فیکس کنیم این قسمت اضافه شده. میتونیم یه کانفیگ VLESS یا Socks یا Http رایگان که حتی فیلتر هم شده باشه (بشرطی که فقط تو ایران فیلتر شده باشه ولی کار کنه) این قسمت بذاریم و IP ما برای همیشه ثابت میشه به IP این کانفیگ.
190 |
191 | > [!CAUTION]
192 | > 1- این کانفیگ نباید خودش ورکر باشه وگرنه بازم IP نهاییتون تغییر میکنه.
193 | >
194 | > 2- برای دریافت کانفیگ رایگان منبع زیاده، من سایت [racevpn.com](https://racevpn.com) رو توصیه میکنم که البته محدودیت زمانی داره، بر اساس کشور میتونید کانفیگ بگیرید. از کانفیگای [IRCF](https://ircfspace.github.io/tconfig/) و یا بات تلگرامی [ی ب خ](https://t.me/TheTVCbot) هم میتونید استفاده کنید ولی ممکنه بعضی از کانفیگاشون از کار افتاده باشه.
195 | >
196 | > 3- کانفیگ VLESS میتونه یکی از انواع زیر باشه:
197 | >
198 | > `Reality TCP`
199 | >
200 | > `Reality GRPC`
201 | >
202 | > `Reality WS`
203 | >
204 | > `Reality TCP Header`
205 | >
206 | > `WS TLS`
207 | >
208 | > `GRPC TLS`
209 | >
210 | > `TCP TLS`
211 | >
212 | > `WS`
213 | >
214 | > `GRPC`
215 | >
216 | > `TCP`
217 | >
218 | > `TCP Header`
219 | >
220 | > 5- کانفیگ Socks میتونه یکی از اشکال زیر باشه:
221 | >
222 | > socks://`address`:`port`
223 | >
224 | > socks://`user`:`pass`@`address`:`port`
225 | >
226 | > 6- کانفیگ Http میتونه یکی از اشکال زیر باشه:
227 | >
228 | > http://`address`:`port`
229 | >
230 | > http://`user`:`pass`@`address`:`port`
231 | >
232 | > 7- این بخش فقط روی تمام سابها به جز ردیف اول جدول Normal و سابهای Warp اعمال میشه، بعد از اعمال حتما ساب رو آپدیت کنید. ولی ساب نرمال اون کانفیگ رو بصورت تکی بهتون میده. مثلا میتونید تو برنامهی Nekobox یا Husi در قسمت Group ساب خودتون رو ویرایش کنید و این کانفیگ رو بعنوان Landing Proxy قرار بدید، اینجوری ساب Chain میشه. جدیدا برنامه v2rayNG هم از ورژن 1.9.1 این قابلیت رو اضافه کرده، باید اسم کانفیگ رو کپی کنید، برید Subscription group setting سابتون رو ویرایش کنید و اسم رو در قسمت `Next proxy remarks` جاگذاری کنید.
233 |
234 | > [!IMPORTANT]
235 | > 1- گر از کانفیگ VLESS TLS برای Chain استفاده میکنید حتما باید پورتش 443 باشه وگرنه پنل بهتون اجازه نمیده.
236 | >
237 | > 2- کانفیگهای VLESS که برای alpn مقدار randomized دارن روی Clash کار نمیکن چون ساپورت نمیکنه.
238 | >
239 | > 3- کانفیگ VLESS WS برای Chain کردن روی Sing-box مناسب نیست، باگ داره.
240 |
241 |
242 | ### 1-4- تنظیمات IP تمیز
243 |
244 | لینک های اشتراک نرمال (بدون فرگمنت) 6 تا کانفیگ بهتون میده. اینجا میتونید تعداد کانفیگها رو زیاد کنید. یه اسکنر هم هست که میتونید بر اساس سیستمعامل خودتون فایل zip رو دانلود کنید و بعد از extract کردن فایل CloudflareScanner رو اجرا کنید، بعد از تکمیل تست خروجی رو در فایل result.csv میریزه که میتونید بر اساس Delay و Download speed انتخاب کنید، توصیه میکنم روی ویندوز انجام بدید و موقع تست حتما VPN قطع باشه. در حالت عادی IP های خوبی میده ولی برای اسکن پیشرفته راهنما رو [اینجا](https://github.com/bia-pain-bache/Cloudflare-Clean-IP-Scanner/blob/master/README.md) بخونید.
245 |
246 | > [!TIP]
247 | > روی اپراتورهایی که از IPv6 پشتیبانی میکنن (مثل رایتل، ایرانسل و آسیاتک) اول IPv6 رو روی سیمکارت فعال کنید، بعد داخل تنظیمات V2RayNG گزینهی Prefer IPv6 رو فعال کنید و از بین این 6 تا کانفیگ هم از اون دو تا آخری یا اونی که آدرسش دامنه خودتون هست استفاده کنید. به طور کلی همیشه یه بار Real delay all configuration بزنید و با هر کدوم بهتر بود وصل بشید.
248 |
249 |
250 |
251 |
252 | اون 6 تا کانفیگ پیشفرضی که پنل میده همشون IP تمیز هستن، در ضمن اگر از کانفیگای Fragment استفاده کنید دیگه خیلی IP تمیز اهمیتی نداره، اما بعضی اپراتورا مثل مخابرات روی کانفیگای معمولی هنوز IP تمیز میخوان.
253 |
254 | خب حالا اگر خواستید علاوه بر اون 6 تا کانفیگ دیگه ای اضافه کنید که با IP تمیز خودتون باشه، مطابق عکس IP یا دامینهای تمیز خودتون رو با ویرگول وارد کنید و Apply بزنید:
255 |
256 |
257 |
258 |
259 |
260 | الان اگر توی اپلیکیشن Update subscription بزنید میبینید که کانفیگای جدید اضافه شدن.
261 |
262 | در ضمن این کانفیگای جدید همزمان به قسمت فرگمنت هم اضافه میشن.
263 |
264 |
265 |
266 | > [!CAUTION]
267 | > حتما بعد از اعمال ساب رو آپدیت کنید.
268 |
269 |
270 | ### 1-5- فعال کردن IPv6
271 |
272 | پنل بطور پیشفرض کانفیگهای IPv6 هم میده، ولی اگر اپراتورتون پشتیبانی نمیکنه، برای خلوت شدن کانفیگها و تنظیم بهینهی DNS میتونید غیرفعالش کنید.
273 |
274 |
275 | ### 1-6- تنظیمات Custom CDN
276 |
277 | 3 تا فیلد داریم به اسم Custom CDN، برای مواقعی استفاده میشه که شما میاید دامنهی Worker خودتون رو میبرید پشت یه CDN دیگه، مثلا Fastly یا Gcore یا هر CDN دیگهای. این 3 قسمت به ترتیب عبارتند از:
278 |
279 | 1- بخش `Custom Addr` که در اصل حکم همون IP ها یا IP تمیزهای کلادفلر رو داره. ولی از هر CDN که اینجا استفاده میکنید باید IP های خودشو بذارید، نمیشه IP کلادفلرو بذارید برای Fastly یا Gcore. اینجام مثل IP تمیز که داشتیم میتونید دامنه، IP ورژن 4 یا 6 با ویرگول بذارید، حواستون باشه IPv6 باید بین [ ] باشه مثل:
280 | > speedtest.net , [2a04:4e42:200::731] , 151.101.66.219
281 |
282 | 2- بخش `Custom Host` باید host که توی اون CDN تعریف کردید و اشاره میکنه به ورکر خودتون رو بذارید. مثلا توی Fastly میشه یه آدرس دامنه فیک تعریف کرد.
283 |
284 | 3- بخش `Custom SNI` هم میتونید اون دامنهی فیک رو بذارید هم یه سایتی که روی همون CDN باشه. مثلا سایت speedtest.net (بدون wwww) روی CDN Fastly هست.
285 |
286 | حالا بعد از تنظیم کردن این بخش، کانفیگاش به سابهای Normal اضافه میشه، همهی سابهای Sing-box و Clash و v2ray و .... اسم این کانفیگها یه `C` داره توش که با بقیه قاطی نشه.
287 |
288 | > [!IMPORTANT]
289 | > در حال حاضر فقط کانفیگهای پورت 443 و 80 با این متد متصل میشن.
290 |
291 | > [!TIP]
292 | > این کانفیگها روی ساب Normal و Full Normal میان. ولی اگر از ساب Normal استفاده میکنید باید دستی از تنظیمات کانفیگ Allow Insecure رو فعال کنید. Full Normal خودش اعمال میکنه.
293 |
294 |
295 | ### 1-7- زمان چک کردن Best Ping
296 |
297 | تو همهی سابهای Fragment یا Sing-box و Clash ما Best Ping رو داریم. بصورت پیشفرض هر 30 ثانیه میومد بهترین کانفیگ یا مقدار Fragment رو پیدا میکرد و بهش وصل میشد، اما ممکنه اگه سرعت نت خوب نباشه و شما در حال تماشای ویدئو یا بازی کردن باشید این 30 ثانیه دردسرساز بشه و تجربهی Lag داشته باشید. از اینجا میتونید زمان رو تنظیم کنید، حداقل میتونه 10 ثانیه باشه و حداکثر 90.
298 |
299 |
300 | ### 1-8- انتخاب Protocol
301 |
302 | میتونید یکی یا هر دو پروتکل VLESS و Trojan رو فعال کنید.
303 | > [!CAUTION]
304 | > این دو پروتکل بر بستر کلادفلر اتصال UDP رو به خوبی پشتیبانی نمیکنن، بنابراین برای مثال تماس صوتی تلگرام کار نمیکنه. همچنین نمیتونید از DNS های UDP بعنوان remote DNS استفاده کنید، اگر در برنامهای دیدید remote DNS یه IP مثل 1.1.1.1 هست یا یه همچین چیزی udp://1.1.1.1 به مشکل بر میخورید. حتما از فرمتهای زیر استفاده کنید:
305 | >
306 | > `https://IP/dns-query` مثل `https://8.8.8.8/dns-query` , `https://94.140.14.14/dns-query` ....
307 | >
308 | > `https://doh/dns-query` مثل `https://dns.google/dns-query` , `https://cloudflare-dns.com/dns-query` ....
309 | >
310 | > `tcp://IP` مثل `tcp://8.8.8.8` , `tcp://94.140.14.14` ....
311 | >
312 | > `tls://IP` مثل `tls://dns.google` , `tls://cloudflare-dns.com` ....
313 |
314 |
315 | ### 1-9- انتخاب Port
316 |
317 | از این بخش میتونید پورتهای مورد نیازتون رو انتخاب کنید. یه نعدادیشون بهتون کانفیگ TLS میدن که امنتره ولی وقتایی که روی TLS و Fragment اختلال ایجاد میشه این کانفیگا وصل میشن.
318 | > [!CAUTION]
319 | > دقت کنید برای استفاده از کانفیگهای non TLS باید از روش Workers دپلوی کرده باشید. در غیر این صورت Port های http در پنل نمایش داده نمیشن چون با روش Pages کار نمیکنن.
320 |
321 | > [!TIP]
322 | > کانفیگهای non TLS فقط به ساب نرمال اضافه میشن.
323 |
324 |
325 | ## 2- تنظیمات Fragment
326 |
327 |
328 |
329 |
330 |
331 | بصورت پیشفرض:
332 |
333 |
334 | >`Length: 100-200`
335 | >
336 | >`Interval: 1-1`
337 | >
338 | >`Packets: tlshello`
339 |
340 | خب حالا میتونید پارامترا رو تنظیم کنید و Apply رو بزنید. اینجوری کانفیگای فرگمنت با تنظیمات شما ارائه میشن.
341 |
342 |
343 | > [!NOTE]
344 | > میتونید یکی از پارامترا رو عوض کنید یا همشو با هم. هر تغییری که بدید ذخیره میشه و دفعه بعد نیازی به تنظیم از اول نیست.
345 |
346 | > [!IMPORTANT]
347 | > مقادیر فرگمنت حداکثر دارن، Length بیشتر از 500 نمیتونه باشه، Interval بیشتر از 30.
348 |
349 |
350 | ## 3- تنظیمات WARP GENERAL
351 |
352 |
353 |
354 |
355 |
356 | بین هر دو ساب Warp و Warp Pro مشترکه و روی جفتشون اعمال میشه که دو تا قسمت اصلی داره:
357 |
358 | 1. یه Endpoints داریم که اینا برای Warp حکم IP تمیز برای VLESS رو دارن. هم برای کانفیگای Warp اعمال میشه هم WoW. یه اسکریپت اسکنر هم گذاشتم که روی Termux اندروید یا روی لینوکس میتونید اجراش کنید و بذارید توی پنل، البته 100 درصدی نیست و باید تست کنید دیگه.
359 |
360 | > [!CAUTION]
361 | > دقت کنید وارد کردن Endpoint به صورت IP:Port یا Domain:port هست و باید بینشون ویرگول باشه.
362 | >
363 | > برای وارد کردن IPv6 باید داخل [] قرارش بدید. به مثال زیر دقت کنید:
364 | >
365 | > 123.45.8.6:1701 , engage.cloudflareclient:2408 , [2a06:98c1:3120::3]:939
366 |
367 | 2. میتونید Fake DNS رو برای کانفیگای Warp هم جداگونه فعال کنید، به سرعت DNS کمک میکنه ولی حواستون باشه ممکنه با بعضی برنامهها سازگار نباشه و یا DNS سیستم رو درگیر کنه، بنابراین اگر نمیدونید دقیقا چیه ترجیحا فعالش نکنید.
368 | 3. اگر اپراتورتون از IPv6 پشتیبانی نمیکنه برای عملکرد بهینهی DNS و پروکسی میتونید غیرفعالش کنید.
369 | 4. قسمت Warp+ License شما میتونید یه لایسنس وارپ پلاس اعمال کنید و کانفیگاتون رو به پلاس ارتقا بدید که سرعت بهتری دارن. وقتی وارد کردید اول باید Apply کنید تا ذخیره بشه بعد از قسمت تنظیمات وارپ Update رو بزنید. از [اینجا](https://ircfspace.github.io/warpplus/) یا از این [بات تلگرامی](https://t.me/generatewarpplusbot) یا از این [کانال تلگرامی](https://t.me/warpplus) یه لایسنس Warp Plus بگیرید. اما حواستون باشه که هر لایسنس فقط برای 5 تا کانقیگ وارپ استفاده میشه و هر بار که شما ازش توی پنل استفاده کنید 2 بارش مصرف میشه. بعبارت دیگه اگر لایسنس شما مصرف نشده باشه و بذارید داخل پنل، دو بار میتونید Update رو بزنید و کانفیگاتون تبدیل به پلاس میشه، بعدش ارور میده که Too many connected device.
370 |
371 | > [!CAUTION]
372 | > اگر از کانال تلگرامی یا سایت اولی متعلق به IRCF لایسنس رو بگیرید، چون عمومی هستن ممکنه همون اول بگه Too many connected devices. ولی اون بات تلگرامی هرچی بده جدیده فقط یه سری مراحلی داره که بتونید از بات استفاده کنید.
373 |
374 | > [!TIP]
375 | > اگر لایسنس Warp+ نداشته باشید و Update کنید میاد همون کانفیگای معمولی جدیدو بروزرسانی میکنه اما اگر داشته باشید میاد تبدیل به کانفیگای Warp Plus میکنه.
376 |
377 | > [!TIP]
378 | > بعد از اینکه لایسنس رو Apply کردید و کانفیگای وارپ رو Update کردید و ساب وارپ رو Update کردید و وصل شدید برای اینکه چک کنید واقعا Plus شده یا نه [این لینک](https://cloudflare.com/cdn-cgi/trace) رو باز کنید، اون آخراش باید نوشته باشه warp=plus.
379 |
380 | 5. قسمت Warp Configs اینجوریه که اگه Update کنید میره کانفیگای وارپ جدید از کلادفلر میگیره و ذخیره میکنه، اگه سابها رو آپدیت کنید میبینید که تغییر کردن. ولی این قسمت اصلا ربطی به سرعت اتصال نداره.
381 |
382 |
383 | 6. زمان چک کردن Best Ping. تو سابهای Warp و Warp PRO ما Best Ping رو داریم. بصورت پیشفرض هر 30 ثانیه میومد بهترین کانفیگ یا Endpoint رو پیدا میکرد و بهش وصل میشد، اما ممکنه اگه سرعت نت خوب نباشه و شما در حال تماشای ویدئو یا بازی کردن باشید این 30 ثانیه دردسرساز بشه و تجربهی Lag داشته باشید. از اینجا میتونید زمان رو تنظیم کنید، حداقل میتونه 10 ثانیه باشه و حداکثر 90.
384 |
385 |
386 | ## 4- تنظیمات WARP PRO
387 |
388 |
389 |
390 |
391 |
392 | فقط برای ساب WARP PRO هست که بالاتر توضیح دادم چیه. چند قسمت داره:
393 |
394 | 1. اولی Hiddify Noise Mode هست که تعیین میکنه روی چه مودی نویزها (پکتهای فیک) تولید بشن. هستهی Singbox تیم Hiddify این حالتا رو پشتیبانی میکنه:
395 |
396 | - مودهای m1 تا m6
397 | - مود h_HEX که اون HEX میتونه بین 00 تا FF باشه، مثلا h_0a , h_f9 و h_9c و ...
398 | - مود g_HEX_HEX_HEX که بازم اون HEX مثل بالاس مثلا g_0a_ff_9c
399 |
400 | 2. دومی NikaNG Noise Mode هست که این حالتا رو داره:
401 |
402 | - مود none یعنی هیچ نویزی اعمال نشه، در اصل میشه همون کانفیگای نرمال وارپ.
403 | - مود quic که خود تیم سازنده برای شرایط ایران توصیه کرده.
404 | - مود random که میاد بصورت تصادفی Noise تولید میکنه.
405 | - و مود آخر که میتونید خودتون یه رشتهی HEX دلبخواه استفاده کنید، مثلا fe09ad5600bc...
406 |
407 | 3. بخش Noise Count تعداد این پکتهای فیک یا نویز هست که ارسال میشه. مثلا پیشفرض پنل میگه بین 10 الی 15 تا بفرست.
408 | 4. بعدی Noise size هست که از اسمش مشخصه طول این پکتهاست.
409 | 5. آخری Noise Delay هم فاصلهی بین ارسال این نویزها هست.
410 |
411 | این تنظیمات به مرور برای هر اپراتور با آزمون و خطاها مشخص میشه.
412 |
413 |
414 | ## 5- تنظیمات Routing Rules
415 |
416 |
417 |
418 |
419 |
420 | این قسمت برای اینه که کانفیگها (به جز اونایی که ساب Normal میده) بتونن:
421 | 1. اتصال مستقیم LAN داشته باشن. مثلا دسترسی به 127.0.0.1 یا 192.168.1.1 مستقیم میشه.
422 | 2. به سایتهای ایرانی مستقیم بدون VPN وصل بشن (برای بازدید از بعضی سایتها مخصوصا درگاه پرداخت نیازی به قطع کردن نباشه)
423 | 3. دسترسی مستقیم به سایتهای چینی داشته باشن.
424 | 4. به سایتهای روسی دسترسی مستقیم داشته باشن.
425 | 5. تبلیغات ایرانی و خارجی تا حدود 80 درصد مسدود کنن.
426 | 6. سایتهای پورن رو مسدود کنن.
427 | 7. اتصالات QUIC رو مسدود کنن (بخاطر ناپایدار بودن شبکه)
428 |
429 | در حالت عادی این قسمت غیر فعال شده، چون باید اول مطمئن بشید Geo asset برنامهتون آپدیته.
430 | > [!CAUTION]
431 | > اگر فعال کردید و VPN متصل نشد تنها دلیلش آپدیت نبودن Geo asset هست. از منوی برنامهی v2rayNG وارد قسمت Geo asset files بشید و اون علامت ابر یا دانلود رو بزنید تا آپدیت بشن، اگر آپدیت ناموفق باشه وصل نمیشید. اگر هر کاری کردید آپدیت نشد دو تا فایل از دو تا لینک زیر دانلود کنید و بجای آپدیت زدن، دکمه اضافه کردن رو بزنید و این دوتا فایل رو وارد کنید:
432 | >
433 | >[geoip.dat](https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat)
434 | >
435 | >[geosite.dat](https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat)
436 |
437 | ### 5-1- تنظیمات Custom Rules
438 |
439 | اگر تنظیماتی نیاز دارید که در قسمت بالا موجود نیست میتونید از این قسمت استفاده کنید، برای مثال فرض کنید پورن رو بلاک کردید ولی یه سایت پورن خاص توی لیست نبوده و بلاک نمیکنه، میتونید از اینجا استفاده کنید.
440 | > [!TIP]
441 | > از سه فرمت مختلف در این قسمت میتونید استفاده کنید:
442 | >
443 | > 1- دامنه مثل `google.com`، فقط حواستون باشه اگر google.com رو وارد کنید همهی زیردامنههاش هم مسدود یا مستقیم میشه، مثل drive.google.com یا mail.google.com
444 | >
445 | > 2- میتونید تک IPv4 یا IPv6 بذارید، دقت کنید IPv6 باید مثل قسمتای دیگه پنل به این شکل وارد بشه: `[2606:4700::6810:85e5]`
446 | >
447 | > 3- میتونید یه IP Range رو بذارید مثل `192.168.1.1`/32 یا 128/`[2606:4700::6810:85e5]`
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ⁉️ سوالات متداول
2 |
3 | 1- چرا کانفیگ Fragment وصل نمیشه؟
4 | - اگر `Routing` فعال کردید و VPN متصل نشد تنها دلیلش آپدیت نبودن Geo asset هست. از منوی برنامهی v2rayNG وارد قسمت `Geo asset files` بشید و اون علامت ابر یا دانلود رو بزنید تا آپدیت بشن، اگر آپدیت ناموفق باشه وصل نمیشید. اگر هر کاری کردید آپدیت نشد دو تا فایل از دو تا لینک زیر دانلود کنید و بجای آپدیت زدن، دکمه اضافه کردن رو بزنید و این دوتا فایل رو وارد کنید:
5 | >
6 | >[geoip.dat](https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat)
7 | >
8 | >[geosite.dat](https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat)
9 |
10 |
11 | 2- چرا کانفیگ نرمال وصل نمیشه؟
12 | - برای استفاده از این کانفیگ ها `Mux` رو از تنظیمات هر اپلیکیشنی که استفاده میکنید خاموش کنید.
13 |
14 |
15 | 3- چرا برنامههای Nekobox یا Hiddify Next هیچ سایتی رو باز نمیکنن؟
16 | - باید داخل تنظیمات اپلیکیشن `remote DNS` رو اینجوری بذارید:
17 | > `https://8.8.8.8/dns-query`
18 |
19 |
20 | 4- چرا کانفیگ فرگمنت روی اپراتور من سرعتش کمه؟
21 | - هر اپراتوری تنظیمات فرگمنت مخصوص خودش رو داره. اکثرا با پیشفرض پنل اوکی هستن ولی ممکنه روی اپراتور شما این مقادیر بهتر باشه، باید تست کنید:
22 | > `Length: 10-100`
23 | >
24 | > `Length: 10-20`
25 |
26 |
27 | 5- چرا Ping من انقدر بالاست؟
28 | - به هیچ عنوان از `https://1.1.1.1/dns-query` برای remote DNS استفاده نکنید چون پینگ رو بالا میبره.
29 |
30 |
31 | 6- من از اون دو تا لینک آموزش Proxy IP گذاشتم ولی سایتا رو باز نمیکنه!
32 | - تعداد این IP ها زیاده و ممکنه تعداد زیادیشون از کار افتاده باشن. باید تست کنید تا یه خوبشو سوا کنید.
33 |
34 |
35 | 7- وقتی proxy IP گذاشتم کار میکرد ولی الان از کار افتاده!
36 | - اگر از تک IP استفاده کنید احتمالا بعد یه مدت دوباره از کار میافته و خیلی سایتا باز نمیشن. باید از اول این مراحلو برید. ترجیحا اگر کار خاصی انجام نمیدید که نیاز به IP ثابت داشته باشه بذارید پیشفرض پنل بمونه، Proxy IP تکی نذارید.
37 |
38 |
39 | 8- چرا وقتی میرم به آدرس `panel/` ارور میده؟
40 | - طبق آموزش راهاندازی کنید، KV درست تنظیم نشده.
41 |
42 |
43 | 9- دپلوی کردم ولی ارور 1101 میده کلادفلر!
44 | - اگر ورکر بوده از روش Pages بسازید و اگر اونم ارور داد اکانت کلادفلر شما قبلا شناسایی شده، با یک ایمیل رسمی مثل Gmail یه اکانت جدید گیتهاب و کلادفلر بسازید و ترجیحا از روش Pages، در ضمن اسم پروژه رو حتما عوض کنید که کلمهی bpb داخلش نباشه.
45 |
46 |
47 | 10- آیا میتونم ازش برای ترید استفاده کنم؟
48 | - اگر IP کلادفلر شما آلمان هست (که معمولا همینطوره) از Proxy IP تکی آلمان استفاده کنید احتمالا مشکلی نداشته باشه ولی ترجیحا از روش Chain Proxy استفاده کنید برای فیکس کردن IP.
49 |
50 |
51 | 11- از روش Pages دپلوی کردم ولی وقتی توی گیتهاب برای ورژن جدید Sync fork میزنم ورژن پنل عوض نمیشه!
52 | - کلادفلر هر دفعه که آپدیت میکنید یه لینک تست جدید برای اون ورژن میسازه، بخاطر همین وقتی پروژه رو باز میکنید در قسمت Deployment چندتا لینک میبینید که با هم فرق دارن. هیچکدوم از اینا لینک اصلی پنل شما نیست، باید از بالای صفحه قسمت Production اون Visit Site رو بزنید و از اونجا وارد پنل بشید.
53 |
54 |
55 | 12- پورتهای non TLS رو فعال کردم ولی وصل نمیشن!
56 | - دقت کنید برای استفاده از کانفیگهای non TLS باید فقط از طریق Workers بدون دامنه شخصی یا Custom Domain دپلوی کرده باشید.
57 |
58 |
59 | 13- کانفیگ Best Fragment چرا وصل نمیشه یا پینگ میده کار نمیکنه؟
60 | - از تنظیمات `Prefer IPv6` رو خاموش کنید.
61 |
62 |
63 | 14- چرا تماس صوتی تلگرام یا clubhouse کار نمیکنه؟
64 | - کلادفلر نمیتونه درست پروتکل UPD رو برقرار کنه، در حال حاضر راه حل موثری براش نیست.
65 |
66 |
67 | 15- کانفیگ نرمال Trojan چرا وصل نمیشه؟
68 | - در صورتی که با ساب نرمال میخواید وصل بشید، از هر برنامهای که استفاده میکنید باید Remote DNS رو چک کنید که مثل پنل باشه، فرمتهای udp://1.1.1.1 یا 1.1.1.1 با تروجان کار نمیکنن. فرمتهای زیر مناسبن:
69 | - `https://8.8.8.8/dns-query`
70 | - `tcp://8.8.8.8`
71 | - `tls://8.8.8.8`
72 | - توصیه میکنم از ساب Full Normal یا Fragment استفاده کنید که همهی تنظیمات رو خودشون دارن.
73 |
74 |
75 | 16- چرا ChatGPT باز نمیشه؟
76 | - بخاطر اینکه Proxy IP های پیشفرض پنل عمومی هستن و ممکنه خیلیاشون برای ChatGPT مشکوک باشن. از لینک زیر باید بگردید تست کنید و یه IP مناسب برای خودتون بردارید:
77 | > https://www.nslookup.io/domains/bpb.yousef.isegaro.com/dns-records/
78 |
79 |
80 | 17- پسورد پنل یادم نمیاد چیکار کنم؟
81 | - به داشبورد کلادفلر برید، از قسمت KV اون KV که برای Worker یا Pages ساختید رو پیدا کنید و view رو بزنید، به قسمت KV Pairs برید، توی جدول یه pwd میبینید، مقدار روبروش پسوردتونه.
82 |
83 |
84 | 18- اگر UUID و پسورد Trojan رو عوض نکنم چی میشه؟
85 | - دیگران میتونن به پروکسی شما دسترسی داشته باشن و خطر امنیتی داره، بنابراین توصیه میکنم طبق آموزش خیلی راحت عوضشون کنید.
--------------------------------------------------------------------------------
/docs/pages_installation_fa.md:
--------------------------------------------------------------------------------
1 | نصب از طریق Cloudflare Pages
2 |
3 | ## مقدمه
4 | احتمالاً میدونید که دو روش استفاده Worker و Pages برای ساخت پروکسی روی کلادفلر مورد استفاده قرار میگیره، نکته جالب توجه اینه که روش Worker که مرسومتره یه محدودیت داره که روزانه اجازه ارسال بیشتر از صدهزار Request به شما نمیده. البته این محدودیت برای مصرف ۲-۳ نفر کافیه . برای دور زدن این محدودیت در روش worker یه دامنه به ورکر وصل میکردیم و اینجوری نامحدود میشد (که ظاهرا باگ کلادفلر هست). اما Pages این محدودیت رو نداره. البته چون ما در این روش از امکانی به اسم Pages functions استفاده میکنیم کماکان مشابه Worker شما ایمیلی دریافت خواهید کرد که پر شدن ظرفیت مصرف 100k رو به شما اطلاع میده، در این روش حتی اگر از دامنه شخصی هم استفاده کنید باز این ایمیل رو دریافت می کنید. **ولی در نهایت تجربه نشون داده که سرویس شما قطع نخواهد شد.**
5 |
6 | مزیت مهم دیگهش سهولت در یروزرسانی هست. وقتی کد پروژه به روز بشه شما هم به راحتی و بدون نیاز به طی مجدد مراحل میتونید پنلتون رو به روزرسانی کنید. توضیحات بیشتر در بخش [بروزرسانی](#بروز-رسانی) اومده.
7 |
8 | ضمنا مراحل استفاده از Pages بسیار سادهتر است و به راحتی روی گوشی موبایلتون میتونید این کارها رو انجام بدید.
9 |
10 | ## قدم اول - Github
11 | در سایت [Github](https://github.com/signup) یه اکانت میسازید ( برای ثبت نام فقط یک ایمیل لازم دارید، توصیه میکنم از ایمیلهای Fake یا موقت استفاده نکنید). با مشخصات کاربریتون در گیتهاب لاگین میکنید.
12 |
13 | حالا به آدرس گیتهاب [BPB-Worker-Panel](https://github.com/bia-pain-bache/BPB-Worker-Panel) میرید و از اون بالا دکمهی Fork رو میزنید.
14 |
15 |
16 |
17 |
18 |
19 | تو صفحهی بعدی به هیچی دست نزنید و Create Fork رو بزنید. خب کار ما با گیتهاب تموم شد.
20 |
21 |
22 | ## قدم دوم - Cloudflare Pages
23 | اگر اکانت کلود فلر ندارید از [اینجا](https://dash.cloudflare.com/sign-up) یک اکانت بسازید (اینجا هم فقط یک ایمیل برای ثبت نام لازم دارید).
24 |
25 | حالا در اکانت کلادفلرتون از منوی سمت چپ وارد قسمت `Workers and Pages` بشید (همونجا که ورکر میساختیم) و `Create Application` رو بزنید. با این تفاوت این دفعه `Pages` رو انتخاب میکنیم:
26 |
27 |
28 |
29 |
30 |
31 | اینجا `Connect to Git` رو میزنید و میرید مرحله بعد:
32 |
33 |
34 |
35 |
36 |
37 | اینجا روی `BPB-Worker-Panel` کلیک کنید تا فعال بشه و `Begin Setup` رو بزنید. مرحلهی بعد یه `Project Name` داره که میشه دامین پنل شما، اونو حتما عوض کنید و یه اسم دلخواه بذارید وگرنه ممکنه اکانتتون توسط کلادفلر شناسایی بشه. حالا اینجا یه فرقی با Workers داره، یعنی اگر بخواید UUID یا Proxy IP رو عوض کنید دیکه نمیتونید برید توی کد تغییر بدید، اگر خواستید از پیشفرض پنل استفاده کنید که هیچ، اگرنه همین الان برید بخش [تنظیمات پیشرفته](#تنظیمات-پیشرفته-اختیاری) رو بخونید و بعد ادامه بدید.
38 |
39 | دیگه الان میتونید `Save and Deploy` رو بزنید.
40 | یه چند ثانیه زمان میخواد تا پروژه نصب بشه، صبر کنید تا دکمهی `Continue to Project` ظاهر بشه و بزنید و برید تو صفحهی پروژه.
41 |
42 | ## قدم سوم - ساخت Cloudflare KV
43 | از منوی سمت چپ میریم به قسمت KV:
44 |
45 |
46 |
47 |
48 |
49 | روی `Create a namespace` کلیک میکنیم و یه اسم دلخواه بهش میدیم و Add میکنیم.
50 |
51 |
52 | برگردید به قسمت `Workers and Pages` و وارد اون پروژهی Pages بشید که ساختید، با توجه به عکس زیر برید قسمت `Settings`:
53 |
54 |
55 |
56 |
57 |
58 |
59 | اینجا مثل ورکر تو صفحه قسمت `Bindings` رو پیدا کنید، `Add` بزنید و `KV Namespace` رو انتخاب کنید، `Variable name` باید حتما `bpb` باشه (همینجوری که نوشتم) و `KV namespace` اون KV رو انتخاب میکنید که مرحله دو ساختید و `save` میکنید.
60 |
61 |
62 |
63 |
64 |
65 | خب کارمون با KV تموم شد، حالا فقط باید دوباره Deploy کنیم که تغییرات KV اعمال بشه.
66 |
67 | از نوار بالا به قسمت `Deployment` برگردید و از بخش `Production` برید به `view details`:
68 |
69 |
70 |
71 |
72 |
73 | حالا تو قسمت `Deployment detail` دکمهی `Manage Deployment` رو بزنید و `Retry deployment`:
74 |
75 |
76 |
77 |
78 |
79 | چند ثانیه صبر میکنید تا مراحلش تموم شه و کار ما تموم شد!
80 |
81 | یه Back بزنید و از قسمت `Production` روی `visit site` بزنید، بعد یه `panel/` تهش اضافه کنید و وارد پنل بشید.
82 | آموزشهای تنظیمات و نکات هم که تو [آموزش اصلی](configuration_fa.md) هست.
83 | نصب به پایان رسیده و توضیحاتی که در ادامه اومده شاید برای عموم لازم نباشه!
84 |
85 | تنظیمات پیشرفته (اختیاری)
86 |
87 | شاید تا الان متوجه شده باشید که در مورد تغییر UUID و Proxy IP و پسورد Trojan چیزی نگفتیم، چون شما میتونید بدون انجام این مرحله از تنظیمات پیشفرض پنل استفاده کنید. ولی توصیه میکنم حداقل UUID و پسورد Trojan رو عوض کنید.
88 |
89 |
90 | ## 1- تغییر UUID:
91 |
92 | همونطور که میدونید UUID مثل اسم رمزی میمونه که داخل لینکهای اشتراک و کانفیگ ها قرار میگیره و شما در صورت نیاز میتونید تغییر بدید. در صورت تغییر این پارامتر اتصال کاربرهای شما قطع میشه و لازم هست لینک اشتراک و یا کانفیگ ها رو مجددا در اختیارشون قرار بدید. در صورتی که این UUID رو در این مرحله تعریف نکنید هم کد از یک UUID پیشفرض استفاده خواهد کرد.
93 |
94 |
95 | ## 2- ثابت کردن Proxy IP:
96 |
97 | ما یه مشکلی داریم که این کد به صورت پیشفرض از تعداد زیادی IP Proxy استفاده میکنه که برای هر بار اتصال به سایتای پشت کلادفلر ( شامل بخش وسیعی از وب میشه) به صورت رندوم IP جدیدی انتخاب میکنه و در نتیجه به صورت متناوب IP شما تغییر پیدا میکنه. این تغییر IP شاید برای برخی مشکل ساز باشه (مخصوصا تریدرها). برای تغییر Proxy IP از ورژن 2.3.5 به بعد میتونید از طریق خود پنل انجام بدید، به این ترتیب که اعمال میکنید و ساب رو آپدیت میکنید و تمام. اما توصیه میکنم از روشی که در ادامه توضیح دادم استفاده کنید چون:
98 |
99 | > [!CAUTION]
100 | > اگر از طریق پنل Proxy IP رو اعمال کنید و اون IP از کار بیافته، باید یه IP جایگزین کنید و ساب رو آپدیت کنید. معنیش اینه که اگر کانفیگ اهدا کرده باشید و Proxy IP رو تغییر بدید دیگه فایدهای نداره چون یوزر ساب نداره که کانفیگ رو آپدیت کنه. بنابراین توصیه میشه از این روش فقط برای مصرف شخصی استفاده کنید. اما خوبی روش دوم که در ادامه میگم اینه که از طریق داشبورد کلادفلر انجام میشه و نیازی به آپدیت کردن کانفیگها نداره.
101 |
102 |
103 | ## 3- تغییر پسورد Trojan:
104 |
105 | پروتکل Trojan با پسورد پیشفرض خودش کار میکنه ولی با توجه به اینکه تعداد زیادی از این پنل استفاده میکنن، توصیه میکنم حتما عوضش کنید.
106 |
107 |
108 |
109 | برای تغییر UUID و Proxy IP و پسورد Trojan توی همین صفحه (قدم سوم، جایی که BPB-Worker-Panel رو انتخاب میکنید) میاید پایین و قسمت `Environment variables` رو باز میکنید:
110 |
111 |
112 |
113 |
114 |
115 | اینجا باید مقادیر رو مشخص کنید. هر بار `Add` میزنید و یه کدوم رو وارد میکنید و `Save` میکنید:
116 |
117 |
118 |
119 |
120 |
121 | یه بار `Add variable` بزنید و خونه اول رو بنویسید `UUID` با حروف بزرگ، بعد از [اینجا](https://www.uuidgenerator.net/) یه UUID بگیرید و بذارید خونه دوم.
122 |
123 | حالا یه بار دیگه `Add variable` بزنید خونه اول `PROXYIP` با حروف بزرگ، IP رو هم میتونید از لینک زیر بگیرید، اینا رو باز کنید یه تعدادی IP نشون میده که میتونید کشورشون رو هم چک کنید و یک یا چندتا انتخاب کنید:
124 |
125 | >[Proxy IP](https://www.nslookup.io/domains/bpb.yousef.isegaro.com/dns-records/)
126 |
127 |
128 |
129 |
130 |
131 | > [!TIP]
132 | > اگر خواستید چند Proxy IP داشته باشید میتونید با ویرگول وارد کنید، مثل `151.213.181.145`,`5.163.51.41`,`bpb.yousef.isegaro.com`
133 |
134 |
135 | یه بار دیگه `Add variable` بزنید خونه اول `TROJAN_PASS` با حروف بزرگ، یه پسورد دلخواه بذارید.
136 |
137 | > [!TIP]
138 | > اگر موقع دپلوی کردن این تنظیمات رو انجام ندادید بعدا هم میتونید از قسمت Settings این مقادیر رو اضافه کنید. از قسمت `Workers and Pages` وارد اون پروژهی Pages بشید که ساختید، با توجه به عکس زیر برید قسمت `Settings` و پایینتر `Variables and Secrets`:
139 |
140 |
141 |
142 |
143 |
144 | و در نهایت از نوار بالا به قسمت `Deployment` برگردید و از بخش `Production` برید به `view details` و تو قسمت `Deployment detail` دکمهی `Manage Deployment` رو بزنید و `Retry deployment`.
145 |
146 |
147 | ## 4- اتصال دامنه به Pages:
148 |
149 | برای این کار به داشبورد کلادفلر میرید و از قسمت `Workers and Pages` پنل خودتون رو انتخاب میکنید. به قسمت `Custom domains` میرید و `set up a custom domain` رو میزنید. اینجا ازتون میخواد یه Domain وارد کنید (دقت کنید قبلا باید یه دامنه خریداری کرده باشید و روی همین اکانت فعال کرده باشید که اینجا جای آموزشش نیست). حالا فرض کنید یه دامنه دارید به اسم bpb.com، در قسمت Domain میتونید خود دامنه یا یک زیردامنه دلخواه بزنید. مثلا xyz.bpb.com، بعد هم `Continue` رو میزنید و در صفحهی بعد هم `Activate domain`. کلادفلر خودش میره Pages رو به دامنهی شما متصل میکنه (یه مدت طول میکشه تا این اتفاق بیافته، خود کلادفلر میگه ممکنه تا 48 ساعت طول بکشه). خب بعد از این مدت میتونید از آدرس `https://xyz.bpb.com/panel` وارد پنلتون بشید و سابهای جدید رو دریافت کنید.
150 |
151 |
152 | بروزرسانی پنل
153 |
154 | یکی از مزیتهای Pages نسبت به Worker اینه که وقتی آپدیتی برای کد منتشر میشه دیگه نیازی نیست برید ورژن جدید worker.js رو دانلود کنید و روز از نو روزی از نو! اصلا برای آپدیت دیگه کاری به کلادفلر ندارید. کافیه به گیتهاب خودتون برید وارد ریپازیتوری `BPB-Worker-Panel` بشید و از اینجا `Sync fork` رو بزنید:
155 |
156 |
157 |
158 |
159 |
160 | بعد `Update branch` رو میزنید و تمام. خوبیش اینه که با این کار Cloudflare Pages خودش متوجه میشه و در حدود ۱ دقیقه بعد خودبخود آپدیت میکنه براتون.
161 |
--------------------------------------------------------------------------------
/docs/worker_installation_fa.md:
--------------------------------------------------------------------------------
1 | نصب از طریق Cloudflare Workers
2 |
3 | خب اول کد Worker رو از [اینجا](https://github.com/bia-pain-bache/BPB-Worker-Panel/releases/latest/download/worker.js) دانلود کنید، بعد ورکری که ساختید رو Edit code بزنید و از سایدبار سمت چپ فایل worker.js رو delete کنید و فایل جدید رو آپلود کنید، اگر ارور داد فایل package-lock.json رو هم پاک کنید. چون کد خیلی زیاد شده Copy Paste کردن با گوشی خیلی سخت شده، با توجه به عکس زیر آپلود کنید. توی مویایل منوی کناری رو باز کنید و تاح کنید روش نگه دارید و آپلود کنید.
4 |
5 |
6 |
7 |
8 |
9 | پنل با UUID و Proxy IP و پسورد Trojan پیشفرض خودش کار میکنه و میتونید ادامه بدید، ولی اگر خواستید تغییر بدید به بخش [تنظیمات پیشرفته](#تنظیمات-پیشرفته-اختیاری) برید و برگردید اینجا.
10 |
11 | در نهایت ورکر رو `Save and Deploy` کنید.
12 | حالا از اینجا به داشبورد ورکر برگردید و این مراحل را دنبال کنید:
13 |
14 |
15 |
16 |
17 |
18 | از این قسمت وارد صفحه `KV` بشید:
19 |
20 |
21 |
22 |
23 |
24 | تو قسمت KV بزنید `Create a namespace` و یه اسم دلخواه وارد کنید مثلا Test و `Add` کنید.
25 |
26 | دوباره از منوی سمت چپ به قسمت `Workers & Pages` برید، ورکری که ساختید رو باز کنید، برید به قسمت `Settings` و `Bindings` رو پیدا کنید. `Add` بزنید و `KV Namespace` رو انتخاب کنید، مطابق تصویر زیر از کشویی پایینی اون KV که ساخته بودید انتخاب کنید (در مثال Test بود). چیزی که مهمه کشویی بالاییه، حتما باید مقدارش رو بذارید `bpb` و `Save` و تمام.
27 |
28 |
29 |
30 |
31 |
32 | برای مثال، فرض کنید دامنهی ورکر شما هست worker-polished-leaf-d022.workers.dev، یه `panel/` تهش اضافه کنید و وارد پنل بشید. مثال:
33 |
34 | >`https://worker-polished-leaf-d022.workers.dev/panel`
35 |
36 | ازتون میخواد پسورد جدید بذارید و لاگین کنید و تمام.
37 | نصب به پایان رسیده و توضیحاتی که در ادامه اومده شاید برای عموم لازم نباشه.
38 | آموزشهای تنظیمات و نکات هم که توی [آموزش اصلی](configuration_fa.md) هست.
39 |
40 | تنظیمات پیشرفته (اختیاری)
41 |
42 | شاید تا الان متوجه شده باشید که در مورد تغییر UUID و Proxy IP و پسورد Trojan چیزی نگفتیم، چون شما میتونید بدون انجام این مرحله از تنظیمات پیشفرض پنل استفاده کنید. ولی توصیه میکنم حداقل UUID و پسورد Trojan رو عوض کنید.
43 |
44 |
45 | ## 1- تغییر UUID:
46 |
47 | همونطور که میدونید UUID مثل اسم رمزی میمونه که داخل لینکهای اشتراک و کانفیگ ها قرار میگیره و شما در صورت نیاز میتونید تغییر بدید. در صورت تغییر این پارامتر اتصال کاربرهای شما قطع میشه و لازم هست لینک اشتراک و یا کانفیگ ها رو مجددا در اختیارشون قرار بدید. در صورتی که این UUID رو در این مرحله تعریف نکنید هم کد از یک UUID پیشفرض استفاده خواهد کرد.
48 |
49 |
50 | ## 2- ثابت کردن Proxy IP:
51 |
52 | ما یه مشکلی داریم که این کد به صورت پیشفرض از تعداد زیادی IP Proxy استفاده میکنه که برای هر بار اتصال به سایتای پشت کلادفلر ( شامل بخش وسیعی از وب میشه) به صورت رندوم IP جدیدی انتخاب میکنه و در نتیجه به صورت متناوب IP شما تغییر پیدا میکنه. این تغییر IP شاید برای برخی مشکل ساز باشه (مخصوصا تریدرها). برای تغییر Proxy IP از ورژن 2.3.5 به بعد میتونید از طریق خود پنل انجام بدید، به این ترتیب که اعمال میکنید و ساب رو آپدیت میکنید و تمام. اما توصیه میکنم از روشی که در ادامه توضیح دادم استفاده کنید چون:
53 |
54 | > [!CAUTION]
55 | > اگر از طریق پنل Proxy IP رو اعمال کنید و اون IP از کار بیافته، باید یه IP جایگزین کنید و ساب رو آپدیت کنید. معنیش اینه که اگر کانفیگ اهدا کرده باشید و Proxy IP رو تغییر بدید دیگه فایدهای نداره چون یوزر ساب نداره که کانفیگ رو آپدیت کنه. بنابراین توصیه میشه از این روش فقط برای مصرف شخصی استفاده کنید. اما خوبی روش دوم که در ادامه میگم اینه که از طریق داشبورد کلادفلر انجام میشه و نیازی به آپدیت کردن کانفیگها نداره.
56 |
57 |
58 | ## 3- تغییر پسورد Trojan:
59 |
60 | پروتکل Trojan با پسورد پیشفرض خودش کار میکنه ولی با توجه به اینکه تعداد زیادی از این پنل استفاده میکنن، توصیه میکنم حتما این پسورد عوض بشه.
61 |
62 |
63 |
64 | برای تغییر UUID و Proxy IP و پسورد Trojan از منوی سمت چپ به قسمت `Workers & Pages` برید، ورکری که ساختید رو باز کنید، برید به قسمت `Settings` و `Variables and Secrets` رو پیدا کنید:
65 |
66 |
67 |
68 |
69 |
70 | اینجا باید مقادیر رو مشخص کنید. هر بار `Add` میزنید و یه کدوم رو وارد میکنید و `Deploy` میکنید:
71 |
72 |
73 |
74 |
75 |
76 | یه بار `Add variable` بزنید و خونه اول رو بنویسید `UUID` با حروف بزرگ، بعد از [اینجا](https://www.uuidgenerator.net/) یه UUID بگیرید و بذارید خونه دوم.
77 |
78 | حالا یه بار دیگه `Add variable` بزنید خونه اول `PROXYIP` با حروف بزرگ، IP رو هم میتونید از لینک زیر بگیرید، اینا رو باز کنید یه تعدادی IP نشون میده که میتونید کشورشون رو هم چک کنید و یک یا چندتا انتخاب کنید:
79 |
80 | >[Proxy IP](https://www.nslookup.io/domains/bpb.yousef.isegaro.com/dns-records/)
81 |
82 |
83 |
84 |
85 |
86 | > [!TIP]
87 | > اگر خواستید چند Proxy IP داشته باشید میتونید با ویرگول وارد کنید، مثل `151.213.181.145`,`5.163.51.41`,`bpb.yousef.isegaro.com`
88 |
89 |
90 | یه بار دیگه `Add variable` بزنید خونه اول `TROJAN_PASS` با حروف بزرگ، یه پسورد دلخواه بذارید.
91 |
92 |
93 | ## 4- اتصال دامنه به Workers:
94 |
95 | برای این کار به داشبورد کلادفلر میرید و از قسمت `Workers and Pages` ورکر خودتون رو انتخاب میکنید. به قسمت `Settings` میرید و همون اول `Domains & Routes` رو میبینید، `Add +` رو میزنید و `Custom domain` رو انتخاب میکنید. اینجا ازتون میخواد یه Domain وارد کنید (دقت کنید قبلا باید یه دامنه خریداری کرده باشید و روی همین اکانت فعال کرده باشید که اینجا جای آموزشش نیست). حالا فرض کنید یه دامنه دارید به اسم bpb.com، در قسمت Domain میتونید خود دامنه یا یک زیردامنه دلخواه بزنید. مثلا xyz.bpb.com . بعد هم `Add domain` رو میزنید. کلادفلر خودش میره ورکر رو به دامنهی شما متصل میکنه (یه مدت طول میکشه تا این اتفاق بیافته، خود کلادفلر میگه ممکنه تا 24 ساعت طول بکشه).
96 | بعد باید دوباره `Add +` رو بزنید و این بار `Route` رو بزنید، قسمت Zone که دامنه خودتون رو انتخاب میکنید و در قسمت Route باید اینجوری دامنه جدید رو وارد کنید:
97 | > `*bpb.com/*`
98 |
99 | خب بعد از این میتونید از آدرس `https://xyz.bpb.com/panel` وارد پنلتون بشید و سابهای جدید رو دریافت کنید.
100 |
101 | > [!TIP]
102 | > 1- اگر به ورکر دامنه وصل کنید مثل Pages ترافیکش نامحدود میشه.
103 | >
104 | > 2- خود ورکر پورتهای nonTLS مثل 80 و 8080 و ... رو ساپورت میکنه و توی پنل نشون میده، اما اگر دامنه متصل بشه دیگه این پورتها کار نمیکنن و پنل هم نشون نمیده.
--------------------------------------------------------------------------------
/locations.json:
--------------------------------------------------------------------------------
1 | [{"iata":"TIA","lat":41.4146995544,"lon":19.7206001282,"cca2":"AL","region":"Europe","city":"Tirana"},{"iata":"ALG","lat":36.6910018921,"lon":3.2154099941,"cca2":"DZ","region":"Africa","city":"Algiers"},{"iata":"AAE","lat":36.85596,"lon":7.79207,"cca2":"DZ","region":"Africa","city":"Annaba"},{"iata":"ORN","lat":35.6911,"lon":-0.6416,"cca2":"DZ","region":"Africa","city":"Oran"},{"iata":"LAD","lat":-8.8583698273,"lon":13.2312002182,"cca2":"AO","region":"Africa","city":"Luanda"},{"iata":"EZE","lat":-34.8222,"lon":-58.5358,"cca2":"AR","region":"South America","city":"Buenos Aires"},{"iata":"COR","lat":-31.31,"lon":-64.208333,"cca2":"AR","region":"South America","city":"Córdoba"},{"iata":"NQN","lat":-38.9490013123,"lon":-68.1557006836,"cca2":"AR","region":"South America","city":"Neuquen"},{"iata":"EVN","lat":40.1473007202,"lon":44.3959007263,"cca2":"AM","region":"Middle East","city":"Yerevan"},{"iata":"ADL","lat":-34.9431729,"lon":138.5335637,"cca2":"AU","region":"Oceania","city":"Adelaide"},{"iata":"BNE","lat":-27.3841991425,"lon":153.117004394,"cca2":"AU","region":"Oceania","city":"Brisbane"},{"iata":"CBR","lat":-35.3069000244,"lon":149.1950073242,"cca2":"AU","region":"Oceania","city":"Canberra"},{"iata":"HBA","lat":-42.883209,"lon":147.331665,"cca2":"AU","region":"Oceania","city":"Hobart"},{"iata":"MEL","lat":-37.6733016968,"lon":144.843002319,"cca2":"AU","region":"Oceania","city":"Melbourne"},{"iata":"PER","lat":-31.9402999878,"lon":115.967002869,"cca2":"AU","region":"Oceania","city":"Perth"},{"iata":"SYD","lat":-33.9460983276,"lon":151.177001953,"cca2":"AU","region":"Oceania","city":"Sydney"},{"iata":"VIE","lat":48.1102981567,"lon":16.5697002411,"cca2":"AT","region":"Europe","city":"Vienna"},{"iata":"LLK","lat":38.7463989258,"lon":48.8180007935,"cca2":"AZ","region":"Middle East","city":"Astara"},{"iata":"GYD","lat":40.4674987793,"lon":50.0466995239,"cca2":"AZ","region":"Middle East","city":"Baku"},{"iata":"BAH","lat":26.2707996368,"lon":50.6335983276,"cca2":"BH","region":"Middle East","city":"Manama"},{"iata":"CGP","lat":22.2495995,"lon":91.8133011,"cca2":"BD","region":"Asia Pacific","city":"Chittagong"},{"iata":"DAC","lat":23.843347,"lon":90.397783,"cca2":"BD","region":"Asia Pacific","city":"Dhaka"},{"iata":"JSR","lat":23.1837997437,"lon":89.1607971191,"cca2":"BD","region":"Asia Pacific","city":"Jashore"},{"iata":"BGI","lat":13.103562,"lon":-59.603226,"cca2":"BB","region":"North America","city":"Bridgetown"},{"iata":"MSQ","lat":53.9006,"lon":27.599,"cca2":"BY","region":"Europe","city":"Minsk"},{"iata":"BRU","lat":50.9014015198,"lon":4.4844398499,"cca2":"BE","region":"Europe","city":"Brussels"},{"iata":"PBH","lat":27.4712,"lon":89.6339,"cca2":"BT","region":"Asia Pacific","city":"Thimphu"},{"iata":"GBE","lat":-24.6282,"lon":25.9231,"cca2":"BW","region":"Africa","city":"Gaborone"},{"iata":"QWJ","lat":-22.738,"lon":-47.334,"cca2":"BR","region":"South America","city":"Americana"},{"iata":"BEL","lat":-1.4563,"lon":-48.5013,"cca2":"BR","region":"South America","city":"Belém"},{"iata":"CNF","lat":-19.624444,"lon":-43.971944,"cca2":"BR","region":"South America","city":"Belo Horizonte"},{"iata":"BNU","lat":-26.89245,"lon":-49.07696,"cca2":"BR","region":"South America","city":"Blumenau"},{"iata":"BSB","lat":-15.79824,"lon":-47.90859,"cca2":"BR","region":"South America","city":"Brasilia"},{"iata":"CFC","lat":-26.7762,"lon":-51.0125,"cca2":"BR","region":"South America","city":"Cacador"},{"iata":"VCP","lat":-22.90662,"lon":-47.08576,"cca2":"BR","region":"South America","city":"Campinas"},{"iata":"CAW","lat":-21.698299408,"lon":-41.301700592,"cca2":"BR","region":"South America","city":"Campos dos Goytacazes"},{"iata":"XAP","lat":-27.1341991425,"lon":-52.6566009521,"cca2":"BR","region":"South America","city":"Chapeco"},{"iata":"CGB","lat":-15.59611,"lon":-56.09667,"cca2":"BR","region":"South America","city":"Cuiaba"},{"iata":"CWB","lat":-25.5284996033,"lon":-49.1758003235,"cca2":"BR","region":"South America","city":"Curitiba"},{"iata":"FLN","lat":-27.6702785492,"lon":-48.5525016785,"cca2":"BR","region":"South America","city":"Florianopolis"},{"iata":"FOR","lat":-3.7762799263,"lon":-38.5326004028,"cca2":"BR","region":"South America","city":"Fortaleza"},{"iata":"GYN","lat":-16.69727,"lon":-49.26851,"cca2":"BR","region":"South America","city":"Goiania"},{"iata":"ITJ","lat":-27.6116676331,"lon":-48.6727790833,"cca2":"BR","region":"South America","city":"Itajai"},{"iata":"JOI","lat":-26.304408,"lon":-48.846383,"cca2":"BR","region":"South America","city":"Joinville"},{"iata":"JDO","lat":-7.2242,"lon":-39.313,"cca2":"BR","region":"South America","city":"Juazeiro do Norte"},{"iata":"MAO","lat":-3.11286,"lon":-60.01949,"cca2":"BR","region":"South America","city":"Manaus"},{"iata":"POA","lat":-29.9944000244,"lon":-51.1713981628,"cca2":"BR","region":"South America","city":"Porto Alegre"},{"iata":"REC","lat":-8.1264896393,"lon":-34.9235992432,"cca2":"BR","region":"South America","city":"Recife"},{"iata":"RAO","lat":-21.1363887787,"lon":-47.7766685486,"cca2":"BR","region":"South America","city":"Ribeirao Preto"},{"iata":"GIG","lat":-22.8099994659,"lon":-43.2505569458,"cca2":"BR","region":"South America","city":"Rio de Janeiro"},{"iata":"SSA","lat":-12.9086112976,"lon":-38.3224983215,"cca2":"BR","region":"South America","city":"Salvador"},{"iata":"SJP","lat":-20.807157,"lon":-49.378994,"cca2":"BR","region":"South America","city":"São José do Rio Preto"},{"iata":"SJK","lat":-23.1791,"lon":-45.8872,"cca2":"BR","region":"South America","city":"São José dos Campos"},{"iata":"GRU","lat":-23.4355564117,"lon":-46.4730567932,"cca2":"BR","region":"South America","city":"São Paulo"},{"iata":"SOD","lat":-23.54389,"lon":-46.63445,"cca2":"BR","region":"South America","city":"Sorocaba"},{"iata":"NVT","lat":-26.8251,"lon":-49.2695,"cca2":"BR","region":"South America","city":"Timbo"},{"iata":"UDI","lat":-18.8836116791,"lon":-48.225276947,"cca2":"BR","region":"South America","city":"Uberlandia"},{"iata":"VIX","lat":-20.64871,"lon":-41.90857,"cca2":"BR","region":"South America","city":"Vitoria"},{"iata":"BWN","lat":4.903052,"lon":114.939819,"cca2":"BN","region":"Asia Pacific","city":"Bandar Seri Begawan"},{"iata":"SOF","lat":42.6966934204,"lon":23.4114360809,"cca2":"BG","region":"Europe","city":"Sofia"},{"iata":"OUA","lat":12.3531999588,"lon":-1.5124200583,"cca2":"BF","region":"Africa","city":"Ouagadougou"},{"iata":"PNH","lat":11.5466003418,"lon":104.84400177,"cca2":"KH","region":"Asia Pacific","city":"Phnom Penh"},{"iata":"YYC","lat":51.113899231,"lon":-114.019996643,"cca2":"CA","region":"North America","city":"Calgary"},{"iata":"YVR","lat":49.193901062,"lon":-123.183998108,"cca2":"CA","region":"North America","city":"Vancouver"},{"iata":"YWG","lat":49.9099998474,"lon":-97.2398986816,"cca2":"CA","region":"North America","city":"Winnipeg"},{"iata":"YOW","lat":45.3224983215,"lon":-75.6691970825,"cca2":"CA","region":"North America","city":"Ottawa"},{"iata":"YYZ","lat":43.6772003174,"lon":-79.6305999756,"cca2":"CA","region":"North America","city":"Toronto"},{"iata":"YUL","lat":45.4706001282,"lon":-73.7407989502,"cca2":"CA","region":"North America","city":"Montréal"},{"iata":"YXE","lat":52.1707992554,"lon":-106.699996948,"cca2":"CA","region":"North America","city":"Saskatoon"},{"iata":"ARI","lat":-18.348611,"lon":-70.338889,"cca2":"CL","region":"South America","city":"Arica"},{"iata":"SCL","lat":-33.3930015564,"lon":-70.7857971191,"cca2":"CL","region":"South America","city":"Santiago"},{"iata":"BOG","lat":4.70159,"lon":-74.1469,"cca2":"CO","region":"South America","city":"Bogotá"},{"iata":"MDE","lat":6.16454,"lon":-75.4231,"cca2":"CO","region":"South America","city":"Medellín"},{"iata":"FIH","lat":-4.3857498169,"lon":15.4446001053,"cca2":"CD","region":"Africa","city":"Kinshasa"},{"iata":"SJO","lat":9.9938602448,"lon":-84.2088012695,"cca2":"CR","region":"South America","city":"San José"},{"iata":"ZAG","lat":45.7429008484,"lon":16.0687999725,"cca2":"HR","region":"Europe","city":"Zagreb"},{"iata":"CUR","lat":12.1888999939,"lon":-68.9598007202,"cca2":"CW","region":"North America","city":"Willemstad"},{"iata":"LCA","lat":34.8750991821,"lon":33.6249008179,"cca2":"CY","region":"Europe","city":"Nicosia"},{"iata":"PRG","lat":50.1007995605,"lon":14.2600002289,"cca2":"CZ","region":"Europe","city":"Prague"},{"iata":"CPH","lat":55.6179008484,"lon":12.6560001373,"cca2":"DK","region":"Europe","city":"Copenhagen"},{"iata":"JIB","lat":11.5473003387,"lon":43.1595001221,"cca2":"DJ","region":"Africa","city":"Djibouti"},{"iata":"SDQ","lat":18.4297008514,"lon":-69.6688995361,"cca2":"DO","region":"North America","city":"Santo Domingo"},{"iata":"GYE","lat":-2.1894,"lon":-79.8891,"cca2":"EC","region":"South America","city":"Guayaquil"},{"iata":"UIO","lat":-0.1291666667,"lon":-78.3575,"cca2":"EC","region":"South America","city":"Quito"},{"iata":"CAI","lat":30.1219005585,"lon":31.4055995941,"cca2":"EG","region":"Africa","city":"Cairo"},{"iata":"TLL","lat":59.4132995605,"lon":24.8327999115,"cca2":"EE","region":"Europe","city":"Tallinn"},{"iata":"HEL","lat":60.317199707,"lon":24.963300705,"cca2":"FI","region":"Europe","city":"Helsinki"},{"iata":"BOD","lat":44.82946,"lon":-0.58355,"cca2":"FR","region":"Europe","city":"Bordeaux"},{"iata":"LYS","lat":45.7263,"lon":5.0908,"cca2":"FR","region":"Europe","city":"Lyon"},{"iata":"MRS","lat":43.439271922,"lon":5.2214241028,"cca2":"FR","region":"Europe","city":"Marseille"},{"iata":"CDG","lat":49.0127983093,"lon":2.5499999523,"cca2":"FR","region":"Europe","city":"Paris"},{"iata":"PPT","lat":-17.5536994934,"lon":-149.606994629,"cca2":"PF","region":"Oceania","city":"Tahiti"},{"iata":"TBS","lat":41.6692008972,"lon":44.95470047,"cca2":"GE","region":"Europe","city":"Tbilisi"},{"iata":"TXL","lat":52.5597000122,"lon":13.2876996994,"cca2":"DE","region":"Europe","city":"Berlin"},{"iata":"DUS","lat":51.2895011902,"lon":6.7667798996,"cca2":"DE","region":"Europe","city":"Düsseldorf"},{"iata":"FRA","lat":50.0264015198,"lon":8.543129921,"cca2":"DE","region":"Europe","city":"Frankfurt"},{"iata":"HAM","lat":53.6304016113,"lon":9.9882297516,"cca2":"DE","region":"Europe","city":"Hamburg"},{"iata":"MUC","lat":48.3538017273,"lon":11.7861003876,"cca2":"DE","region":"Europe","city":"Munich"},{"iata":"STR","lat":48.783333,"lon":9.183333,"cca2":"DE","region":"Europe","city":"Stuttgart"},{"iata":"ACC","lat":5.614818,"lon":-0.205874,"cca2":"GH","region":"Africa","city":"Accra"},{"iata":"ATH","lat":37.9364013672,"lon":23.9444999695,"cca2":"GR","region":"Europe","city":"Athens"},{"iata":"SKG","lat":40.5196990967,"lon":22.9708995819,"cca2":"GR","region":"Europe","city":"Thessaloniki"},{"iata":"GND","lat":12.007116,"lon":-61.7882288,"cca2":"GD","region":"South America","city":"St. George's"},{"iata":"GUM","lat":13.4834003448,"lon":144.796005249,"cca2":"GU","region":"Asia Pacific","city":"Hagatna"},{"iata":"GUA","lat":14.5832996368,"lon":-90.5274963379,"cca2":"GT","region":"North America","city":"Guatemala City"},{"iata":"GEO","lat":6.825648,"lon":-58.163756,"cca2":"GY","region":"South America","city":"Georgetown"},{"iata":"TGU","lat":14.0608,"lon":-87.2172,"cca2":"HN","region":"South America","city":"Tegucigalpa"},{"iata":"HKG","lat":22.3089008331,"lon":113.915000916,"cca2":"HK","region":"Asia Pacific","city":"Hong Kong"},{"iata":"BUD","lat":47.4369010925,"lon":19.2555999756,"cca2":"HU","region":"Europe","city":"Budapest"},{"iata":"KEF","lat":63.9850006104,"lon":-22.6056003571,"cca2":"IS","region":"Europe","city":"Reykjavík"},{"iata":"AMD","lat":23.0225,"lon":72.5714,"cca2":"IN","region":"Asia Pacific","city":"Ahmedabad"},{"iata":"BLR","lat":13.7835719,"lon":76.6165937,"cca2":"IN","region":"Asia Pacific","city":"Bangalore"},{"iata":"BBI","lat":20.2961,"lon":85.8245,"cca2":"IN","region":"Asia Pacific","city":"Bhubaneswar"},{"iata":"IXC","lat":30.673500061,"lon":76.7884979248,"cca2":"IN","region":"Asia Pacific","city":"Chandigarh"},{"iata":"MAA","lat":12.9900054932,"lon":80.1692962646,"cca2":"IN","region":"Asia Pacific","city":"Chennai"},{"iata":"HYD","lat":17.2313175201,"lon":78.4298553467,"cca2":"IN","region":"Asia Pacific","city":"Hyderabad"},{"iata":"CNN","lat":11.915858,"lon":75.55094,"cca2":"IN","region":"Asia Pacific","city":"Kannur"},{"iata":"KNU","lat":26.4499,"lon":80.3319,"cca2":"IN","region":"Asia Pacific","city":"Kanpur"},{"iata":"COK","lat":9.9312,"lon":76.2673,"cca2":"IN","region":"Asia Pacific","city":"Kochi"},{"iata":"CCU","lat":22.6476933,"lon":88.4349249,"cca2":"IN","region":"Asia Pacific","city":"Kolkata"},{"iata":"BOM","lat":19.0886993408,"lon":72.8678970337,"cca2":"IN","region":"Asia Pacific","city":"Mumbai"},{"iata":"NAG","lat":21.1610714,"lon":79.0024702,"cca2":"IN","region":"Asia Pacific","city":"Nagpur"},{"iata":"DEL","lat":28.5664997101,"lon":77.1031036377,"cca2":"IN","region":"Asia Pacific","city":"New Delhi"},{"iata":"PAT","lat":25.591299057,"lon":85.0879974365,"cca2":"IN","region":"Asia Pacific","city":"Patna"},{"iata":"DPS","lat":-8.748169899,"lon":115.1669998169,"cca2":"ID","region":"Asia Pacific","city":"Denpasar"},{"iata":"CGK","lat":-6.1275229,"lon":106.6515118,"cca2":"ID","region":"Asia Pacific","city":"Jakarta"},{"iata":"JOG","lat":-7.7881798744,"lon":110.4319992065,"cca2":"ID","region":"Asia Pacific","city":"Yogyakarta"},{"iata":"BGW","lat":33.2625007629,"lon":44.2346000671,"cca2":"IQ","region":"Middle East","city":"Baghdad"},{"iata":"BSR","lat":30.5491008759,"lon":47.6621017456,"cca2":"IQ","region":"Middle East","city":"Basra"},{"iata":"EBL","lat":36.1901,"lon":43.993,"cca2":"IQ","region":"Middle East","city":"Erbil"},{"iata":"NJF","lat":31.989722,"lon":44.404167,"cca2":"IQ","region":"Middle East","city":"Najaf"},{"iata":"XNH","lat":30.9358005524,"lon":46.0900993347,"cca2":"IQ","region":"Middle East","city":"Nasiriyah"},{"iata":"ISU","lat":35.5668,"lon":45.4161,"cca2":"IQ","region":"Middle East","city":"Sulaymaniyah"},{"iata":"ORK","lat":51.8413009644,"lon":-8.491109848,"cca2":"IE","region":"Europe","city":"Cork"},{"iata":"DUB","lat":53.4212989807,"lon":-6.270070076,"cca2":"IE","region":"Europe","city":"Dublin"},{"iata":"HFA","lat":32.78492,"lon":34.96069,"cca2":"IL","region":"Middle East","city":"Haifa"},{"iata":"TLV","lat":32.0113983154,"lon":34.8866996765,"cca2":"IL","region":"Middle East","city":"Tel Aviv"},{"iata":"MXP","lat":45.6305999756,"lon":8.7281103134,"cca2":"IT","region":"Europe","city":"Milan"},{"iata":"PMO","lat":38.16114,"lon":13.31546,"cca2":"IT","region":"Europe","city":"Palermo"},{"iata":"FCO","lat":41.8045005798,"lon":12.2508001328,"cca2":"IT","region":"Europe","city":"Rome"},{"iata":"KIN","lat":17.9951,"lon":-76.7846,"cca2":"JM","region":"North America","city":"Kingston"},{"iata":"FUK","lat":33.5902,"lon":130.4017,"cca2":"JP","region":"Asia Pacific","city":"Fukuoka"},{"iata":"OKA","lat":26.1958,"lon":127.646,"cca2":"JP","region":"Asia Pacific","city":"Naha"},{"iata":"KIX","lat":34.4272994995,"lon":135.244003296,"cca2":"JP","region":"Asia Pacific","city":"Osaka"},{"iata":"NRT","lat":35.7647018433,"lon":140.386001587,"cca2":"JP","region":"Asia Pacific","city":"Tokyo"},{"iata":"AMM","lat":31.7226009369,"lon":35.9931983948,"cca2":"JO","region":"Middle East","city":"Amman"},{"iata":"ALA","lat":43.3521003723,"lon":77.0404968262,"cca2":"KZ","region":"Asia Pacific","city":"Almaty"},{"iata":"MBA","lat":-4.0348300934,"lon":39.5942001343,"cca2":"KE","region":"Africa","city":"Mombasa"},{"iata":"NBO","lat":-1.319239974,"lon":36.9277992249,"cca2":"KE","region":"Africa","city":"Nairobi"},{"iata":"ICN","lat":37.4691009521,"lon":126.450996399,"cca2":"KR","region":"Asia Pacific","city":"Seoul"},{"iata":"KWI","lat":29.226600647,"lon":47.9688987732,"cca2":"KW","region":"Middle East","city":"Kuwait City"},{"iata":"VTE","lat":17.9757,"lon":102.5683,"cca2":"LA","region":"Asia Pacific","city":"Vientiane"},{"iata":"RIX","lat":56.9235992432,"lon":23.9710998535,"cca2":"LV","region":"Europe","city":"Riga"},{"iata":"BEY","lat":33.8208999634,"lon":35.4883995056,"cca2":"LB","region":"Middle East","city":"Beirut"},{"iata":"VNO","lat":54.6341018677,"lon":25.2858009338,"cca2":"LT","region":"Europe","city":"Vilnius"},{"iata":"LUX","lat":49.6265983582,"lon":6.211520195,"cca2":"LU","region":"Europe","city":"Luxembourg City"},{"iata":"MFM","lat":22.1495990753,"lon":113.592002869,"cca2":"MO","region":"Asia Pacific","city":"Macau"},{"iata":"TNR","lat":-18.91368,"lon":47.53613,"cca2":"MG","region":"Africa","city":"Antananarivo"},{"iata":"JHB","lat":1.635848,"lon":103.665943,"cca2":"MY","region":"Asia Pacific","city":"Johor Bahru"},{"iata":"KUL","lat":2.745579958,"lon":101.709999084,"cca2":"MY","region":"Asia Pacific","city":"Kuala Lumpur"},{"iata":"MLE","lat":4.1748,"lon":73.50888,"cca2":"MV","region":"Asia Pacific","city":"Male"},{"iata":"MRU","lat":-20.4302005768,"lon":57.6836013794,"cca2":"MU","region":"Africa","city":"Port Louis"},{"iata":"GDL","lat":20.5217990875,"lon":-103.3109970093,"cca2":"MX","region":"North America","city":"Guadalajara"},{"iata":"MEX","lat":19.4363002777,"lon":-99.0720977783,"cca2":"MX","region":"North America","city":"Mexico City"},{"iata":"QRO","lat":20.6173000336,"lon":-100.185997009,"cca2":"MX","region":"North America","city":"Queretaro"},{"iata":"KIV","lat":46.9277000427,"lon":28.9309997559,"cca2":"MD","region":"Europe","city":"Chișinău"},{"iata":"ULN","lat":47.8431015015,"lon":106.766998291,"cca2":"MN","region":"Asia Pacific","city":"Ulaanbaatar"},{"iata":"CMN","lat":33.3675003052,"lon":-7.5899701118,"cca2":"MA","region":"Africa","city":"Casablanca"},{"iata":"MPM","lat":-25.9207992554,"lon":32.5726013184,"cca2":"MZ","region":"Africa","city":"Maputo"},{"iata":"MDL","lat":21.7051697,"lon":95.9695206,"cca2":"MM","region":"Asia Pacific","city":"Mandalay"},{"iata":"RGN","lat":16.9073009491,"lon":96.1332015991,"cca2":"MM","region":"Asia Pacific","city":"Yangon"},{"iata":"KTM","lat":27.6965999603,"lon":85.3591003418,"cca2":"NP","region":"Asia Pacific","city":"Kathmandu"},{"iata":"AMS","lat":52.3086013794,"lon":4.7638897896,"cca2":"NL","region":"Europe","city":"Amsterdam"},{"iata":"NOU","lat":-22.0146007538,"lon":166.212997436,"cca2":"NC","region":"Oceania","city":"Noumea"},{"iata":"AKL","lat":-37.0080986023,"lon":174.792007446,"cca2":"NZ","region":"Oceania","city":"Auckland"},{"iata":"CHC","lat":-43.4893989563,"lon":172.5319976807,"cca2":"NZ","region":"Oceania","city":"Christchurch"},{"iata":"LOS","lat":6.5773701668,"lon":3.321160078,"cca2":"NG","region":"Africa","city":"Lagos"},{"iata":"OSL","lat":60.193901062,"lon":11.100399971,"cca2":"NO","region":"Europe","city":"Oslo"},{"iata":"MCT","lat":23.5932998657,"lon":58.2844009399,"cca2":"OM","region":"Middle East","city":"Muscat"},{"iata":"ISB","lat":33.6166992188,"lon":73.0991973877,"cca2":"PK","region":"Asia Pacific","city":"Islamabad"},{"iata":"KHI","lat":24.9064998627,"lon":67.1607971191,"cca2":"PK","region":"Asia Pacific","city":"Karachi"},{"iata":"LHE","lat":31.5216007233,"lon":74.4036026001,"cca2":"PK","region":"Asia Pacific","city":"Lahore"},{"iata":"ZDM","lat":32.2719,"lon":35.0194,"cca2":"PS","region":"Middle East","city":"Ramallah"},{"iata":"PTY","lat":9.0713596344,"lon":-79.3834991455,"cca2":"PA","region":"South America","city":"Panama City"},{"iata":"ASU","lat":-25.2399997711,"lon":-57.5200004578,"cca2":"PY","region":"South America","city":"Asunción"},{"iata":"LIM","lat":-12.021900177,"lon":-77.1143035889,"cca2":"PE","region":"South America","city":"Lima"},{"iata":"CGY","lat":8.4156198502,"lon":124.611000061,"cca2":"PH","region":"Asia Pacific","city":"Cagayan de Oro"},{"iata":"CEB","lat":10.3074998856,"lon":123.978996277,"cca2":"PH","region":"Asia Pacific","city":"Cebu"},{"iata":"MNL","lat":14.508600235,"lon":121.019996643,"cca2":"PH","region":"Asia Pacific","city":"Manila"},{"iata":"WAW","lat":52.1656990051,"lon":20.9671001434,"cca2":"PL","region":"Europe","city":"Warsaw"},{"iata":"LIS","lat":38.7812995911,"lon":-9.1359195709,"cca2":"PT","region":"Europe","city":"Lisbon"},{"iata":"DOH","lat":25.2605946,"lon":51.6137665,"cca2":"QA","region":"Middle East","city":"Doha"},{"iata":"RUN","lat":-20.8871002197,"lon":55.5102996826,"cca2":"RE","region":"Africa","city":"Saint-Denis"},{"iata":"OTP","lat":44.5722007751,"lon":26.1021995544,"cca2":"RO","region":"Europe","city":"Bucharest"},{"iata":"KHV","lat":48.5279998779,"lon":135.18800354,"cca2":"RU","region":"Asia Pacific","city":"Khabarovsk"},{"iata":"KJA","lat":56.0153,"lon":92.8932,"cca2":"RU","region":"Asia Pacific","city":"Krasnoyarsk"},{"iata":"DME","lat":55.4087982178,"lon":37.9062995911,"cca2":"RU","region":"Europe","city":"Moscow"},{"iata":"LED","lat":59.8003005981,"lon":30.2625007629,"cca2":"RU","region":"Europe","city":"Saint Petersburg"},{"iata":"KLD","lat":56.8587,"lon":35.9176,"cca2":"RU","region":"Europe","city":"Tver"},{"iata":"SVX","lat":56.8431,"lon":60.6454,"cca2":"RU","region":"Asia Pacific","city":"Yekaterinburg"},{"iata":"KGL","lat":-1.9686299563,"lon":30.1394996643,"cca2":"RW","region":"Africa","city":"Kigali"},{"iata":"DMM","lat":26.471200943,"lon":49.7979011536,"cca2":"SA","region":"Middle East","city":"Dammam"},{"iata":"JED","lat":21.679599762,"lon":39.15650177,"cca2":"SA","region":"Middle East","city":"Jeddah"},{"iata":"RUH","lat":24.9575996399,"lon":46.6987991333,"cca2":"SA","region":"Middle East","city":"Riyadh"},{"iata":"DKR","lat":14.7412099,"lon":-17.4889771,"cca2":"SN","region":"Africa","city":"Dakar"},{"iata":"BEG","lat":44.8184013367,"lon":20.3090991974,"cca2":"RS","region":"Europe","city":"Belgrade"},{"iata":"SIN","lat":1.3501900434,"lon":103.994003296,"cca2":"SG","region":"Asia Pacific","city":"Singapore"},{"iata":"BTS","lat":48.1486,"lon":17.1077,"cca2":"SK","region":"Europe","city":"Bratislava"},{"iata":"CPT","lat":-33.9648017883,"lon":18.6016998291,"cca2":"ZA","region":"Africa","city":"Cape Town"},{"iata":"DUR","lat":-29.6144444444,"lon":31.1197222222,"cca2":"ZA","region":"Africa","city":"Durban"},{"iata":"JNB","lat":-26.133333,"lon":28.25,"cca2":"ZA","region":"Africa","city":"Johannesburg"},{"iata":"BCN","lat":41.2971000671,"lon":2.0784599781,"cca2":"ES","region":"Europe","city":"Barcelona"},{"iata":"MAD","lat":40.4936,"lon":-3.56676,"cca2":"ES","region":"Europe","city":"Madrid"},{"iata":"CMB","lat":7.1807599068,"lon":79.8841018677,"cca2":"LK","region":"Asia Pacific","city":"Colombo"},{"iata":"PBM","lat":5.452831,"lon":-55.187783,"cca2":"SR","region":"South America","city":"Paramaribo"},{"iata":"GOT","lat":57.6627998352,"lon":12.279800415,"cca2":"SE","region":"Europe","city":"Gothenburg"},{"iata":"ARN","lat":59.6519012451,"lon":17.9186000824,"cca2":"SE","region":"Europe","city":"Stockholm"},{"iata":"GVA","lat":46.2380981445,"lon":6.1089501381,"cca2":"CH","region":"Europe","city":"Geneva"},{"iata":"ZRH","lat":47.4646987915,"lon":8.5491695404,"cca2":"CH","region":"Europe","city":"Zurich"},{"iata":"KHH","lat":22.5771007538,"lon":120.3499984741,"cca2":"TW","region":"Asia Pacific","city":"Kaohsiung City"},{"iata":"TPE","lat":25.0776996613,"lon":121.233001709,"cca2":"TW","region":"Asia Pacific","city":"Taipei"},{"iata":"DAR","lat":-6.8781099319,"lon":39.2025985718,"cca2":"TZ","region":"Africa","city":"Dar es Salaam"},{"iata":"BKK","lat":13.6810998917,"lon":100.747001648,"cca2":"TH","region":"Asia Pacific","city":"Bangkok"},{"iata":"CNX","lat":18.7667999268,"lon":98.962600708,"cca2":"TH","region":"Asia Pacific","city":"Chiang Mai"},{"iata":"URT","lat":9.1325998306,"lon":99.135597229,"cca2":"TH","region":"Asia Pacific","city":"Surat Thani"},{"iata":"TUN","lat":36.8510017395,"lon":10.2271995544,"cca2":"TN","region":"Africa","city":"Tunis"},{"iata":"IST","lat":40.9768981934,"lon":28.8145999908,"cca2":"TR","region":"Europe","city":"Istanbul"},{"iata":"ADB","lat":38.32377,"lon":27.14317,"cca2":"TR","region":"Europe","city":"Izmir"},{"iata":"KBP","lat":50.3450012207,"lon":30.8946990967,"cca2":"UA","region":"Europe","city":"Kyiv"},{"iata":"DXB","lat":25.2527999878,"lon":55.3643989563,"cca2":"AE","region":"Middle East","city":"Dubai"},{"iata":"EDI","lat":55.9500007629,"lon":-3.3724999428,"cca2":"GB","region":"Europe","city":"Edinburgh"},{"iata":"LHR","lat":51.4706001282,"lon":-0.4619410038,"cca2":"GB","region":"Europe","city":"London"},{"iata":"MAN","lat":53.3536987305,"lon":-2.2749500275,"cca2":"GB","region":"Europe","city":"Manchester"},{"iata":"MGM","lat":32.30059814,"lon":-86.39399719,"cca2":"US","region":"North America","city":"Montgomery"},{"iata":"PHX","lat":33.434299469,"lon":-112.012001038,"cca2":"US","region":"North America","city":"Phoenix"},{"iata":"LAX","lat":33.94250107,"lon":-118.4079971,"cca2":"US","region":"North America","city":"Los Angeles"},{"iata":"SMF","lat":38.695400238,"lon":-121.591003418,"cca2":"US","region":"North America","city":"Sacramento"},{"iata":"SAN","lat":32.7336006165,"lon":-117.190002441,"cca2":"US","region":"North America","city":"San Diego"},{"iata":"SFO","lat":37.6189994812,"lon":-122.375,"cca2":"US","region":"North America","city":"San Francisco"},{"iata":"SJC","lat":37.3625984192,"lon":-121.929000855,"cca2":"US","region":"North America","city":"San Jose"},{"iata":"DEN","lat":39.8616981506,"lon":-104.672996521,"cca2":"US","region":"North America","city":"Denver"},{"iata":"JAX","lat":30.4941005707,"lon":-81.6878967285,"cca2":"US","region":"North America","city":"Jacksonville"},{"iata":"MIA","lat":25.7931995392,"lon":-80.2906036377,"cca2":"US","region":"North America","city":"Miami"},{"iata":"TLH","lat":30.3964996338,"lon":-84.3503036499,"cca2":"US","region":"North America","city":"Tallahassee"},{"iata":"TPA","lat":27.9755001068,"lon":-82.533203125,"cca2":"US","region":"North America","city":"Tampa"},{"iata":"ATL","lat":33.6366996765,"lon":-84.4281005859,"cca2":"US","region":"North America","city":"Atlanta"},{"iata":"HNL","lat":21.3187007904,"lon":-157.9219970703,"cca2":"US","region":"North America","city":"Honolulu"},{"iata":"ORD","lat":41.97859955,"lon":-87.90480042,"cca2":"US","region":"North America","city":"Chicago"},{"iata":"IND","lat":39.717300415,"lon":-86.2944030762,"cca2":"US","region":"North America","city":"Indianapolis"},{"iata":"BGR","lat":44.8081,"lon":-68.795,"cca2":"US","region":"North America","city":"Bangor"},{"iata":"BOS","lat":42.36429977,"lon":-71.00520325,"cca2":"US","region":"North America","city":"Boston"},{"iata":"DTW","lat":42.2123985291,"lon":-83.3534011841,"cca2":"US","region":"North America","city":"Detroit"},{"iata":"MSP","lat":44.8819999695,"lon":-93.2218017578,"cca2":"US","region":"North America","city":"Minneapolis"},{"iata":"MCI","lat":39.2975997925,"lon":-94.7138977051,"cca2":"US","region":"North America","city":"Kansas City"},{"iata":"STL","lat":38.7486991882,"lon":-90.3700027466,"cca2":"US","region":"North America","city":"St. Louis"},{"iata":"OMA","lat":41.3031997681,"lon":-95.8940963745,"cca2":"US","region":"North America","city":"Omaha"},{"iata":"LAS","lat":36.08010101,"lon":-115.1520004,"cca2":"US","region":"North America","city":"Las Vegas"},{"iata":"EWR","lat":40.6925010681,"lon":-74.1687011719,"cca2":"US","region":"North America","city":"Newark"},{"iata":"ABQ","lat":35.0844,"lon":-106.6504,"cca2":"US","region":"North America","city":"Albuquerque"},{"iata":"BUF","lat":42.94049835,"lon":-78.73220062,"cca2":"US","region":"North America","city":"Buffalo"},{"iata":"CLT","lat":35.2140007019,"lon":-80.9430999756,"cca2":"US","region":"North America","city":"Charlotte"},{"iata":"RDU","lat":35.93543,"lon":-78.88075,"cca2":"US","region":"North America","city":"Durham"},{"iata":"CLE","lat":41.50069,"lon":-81.68412,"cca2":"US","region":"North America","city":"Cleveland"},{"iata":"CMH","lat":39.9980010986,"lon":-82.8918991089,"cca2":"US","region":"North America","city":"Columbus"},{"iata":"OKC","lat":35.46655,"lon":-97.65373,"cca2":"US","region":"North America","city":"Oklahoma City"},{"iata":"PDX","lat":45.58869934,"lon":-122.5979996,"cca2":"US","region":"North America","city":"Portland"},{"iata":"PHL","lat":39.8718986511,"lon":-75.2410964966,"cca2":"US","region":"North America","city":"Philadelphia"},{"iata":"PIT","lat":40.49150085,"lon":-80.23290253,"cca2":"US","region":"North America","city":"Pittsburgh"},{"iata":"FSD","lat":43.540819819502,"lon":-96.65511577730963,"cca2":"US","region":"North America","city":"Sioux Falls"},{"iata":"MEM","lat":35.0424003601,"lon":-89.9766998291,"cca2":"US","region":"North America","city":"Memphis"},{"iata":"BNA","lat":36.1245002747,"lon":-86.6781997681,"cca2":"US","region":"North America","city":"Nashville"},{"iata":"AUS","lat":30.1975,"lon":-97.6664,"cca2":"US","region":"North America","city":"Austin"},{"iata":"DFW","lat":32.8968009949,"lon":-97.0380020142,"cca2":"US","region":"North America","city":"Dallas"},{"iata":"IAH","lat":29.9843997955,"lon":-95.3414001465,"cca2":"US","region":"North America","city":"Houston"},{"iata":"MFE","lat":26.17580032,"lon":-98.23860168,"cca2":"US","region":"North America","city":"McAllen"},{"iata":"SAT","lat":29.429461,"lon":-98.487061,"cca2":"US","region":"North America","city":"San Antonio"},{"iata":"SLC","lat":40.7883987427,"lon":-111.977996826,"cca2":"US","region":"North America","city":"Salt Lake City"},{"iata":"IAD","lat":38.94449997,"lon":-77.45580292,"cca2":"US","region":"North America","city":"Ashburn"},{"iata":"ORF","lat":36.8945999146,"lon":-76.2012023926,"cca2":"US","region":"North America","city":"Norfolk"},{"iata":"RIC","lat":37.5051994324,"lon":-77.3197021484,"cca2":"US","region":"North America","city":"Richmond"},{"iata":"SEA","lat":47.4490013123,"lon":-122.308998108,"cca2":"US","region":"North America","city":"Seattle"},{"iata":"TAS","lat":41.257900238,"lon":69.2811965942,"cca2":"UZ","region":"Asia Pacific","city":"Tashkent"},{"iata":"HAN","lat":21.221200943,"lon":105.806999206,"cca2":"VN","region":"Asia Pacific","city":"Hanoi"},{"iata":"SGN","lat":10.8187999725,"lon":106.652000427,"cca2":"VN","region":"Asia Pacific","city":"Ho Chi Minh City"},{"iata":"HRE","lat":-17.9318008423,"lon":31.0928001404,"cca2":"ZW","region":"Africa","city":"Harare"}]
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bpb-worker-panel",
3 | "version": "2.7.2",
4 | "author": {
5 | "name": "Bia Pain Bache"
6 | },
7 | "devDependencies": {
8 | "wrangler": "latest",
9 | "javascript-obfuscator": "latest"
10 | },
11 | "dependencies": {
12 | "jose": "^5.9.4",
13 | "js-sha256": "^0.11.0",
14 | "tweetnacl": "^1.0.3"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/authentication/auth.js:
--------------------------------------------------------------------------------
1 | import { SignJWT, jwtVerify } from 'jose';
2 | import nacl from 'tweetnacl';
3 | import { initializeParams, userID, origin } from "../helpers/init";
4 | import { renderLoginPage } from '../pages/login';
5 | import { renderErrorPage } from '../pages/error';
6 |
7 | async function generateJWTToken (request, env) {
8 | await initializeParams(request, env);
9 | const password = await request.text();
10 | const savedPass = await env.bpb.get('pwd');
11 | if (password !== savedPass) return new Response('Method Not Allowed', { status: 405 });
12 | let secretKey = await env.bpb.get('secretKey');
13 | if (!secretKey) {
14 | secretKey = generateSecretKey();
15 | await env.bpb.put('secretKey', secretKey);
16 | }
17 | const secret = new TextEncoder().encode(secretKey);
18 | const jwtToken = await new SignJWT({ userID })
19 | .setProtectedHeader({ alg: 'HS256' })
20 | .setIssuedAt()
21 | .setExpirationTime('24h')
22 | .sign(secret);
23 |
24 | return new Response('Success', {
25 | status: 200,
26 | headers: {
27 | 'Set-Cookie': `jwtToken=${jwtToken}; HttpOnly; Secure; Max-Age=${7 * 24 * 60 * 60}; Path=/; SameSite=Strict`,
28 | 'Content-Type': 'text/plain',
29 | }
30 | });
31 | }
32 |
33 | function generateSecretKey () {
34 | const key = nacl.randomBytes(32);
35 | return Array.from(key, byte => byte.toString(16).padStart(2, '0')).join('');
36 | }
37 |
38 | export async function Authenticate (request, env) {
39 | try {
40 | const secretKey = await env.bpb.get('secretKey');
41 | const secret = new TextEncoder().encode(secretKey);
42 | const cookie = request.headers.get('Cookie')?.match(/(^|;\s*)jwtToken=([^;]*)/);
43 | const token = cookie ? cookie[2] : null;
44 |
45 | if (!token) {
46 | console.log('Unauthorized: Token not available!');
47 | return false;
48 | }
49 |
50 | const { payload } = await jwtVerify(token, secret);
51 | console.log(`Successfully authenticated, User ID: ${payload.userID}`);
52 | return true;
53 | } catch (error) {
54 | console.log(error);
55 | return false;
56 | }
57 | }
58 |
59 | export function logout() {
60 | return new Response('Success', {
61 | status: 200,
62 | headers: {
63 | 'Set-Cookie': 'jwtToken=; Secure; SameSite=None; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
64 | 'Content-Type': 'text/plain'
65 | }
66 | });
67 | }
68 |
69 | export async function resetPassword(request, env) {
70 | let auth = await Authenticate(request, env);
71 | const oldPwd = await env.bpb.get('pwd');
72 | if (oldPwd && !auth) return new Response('Unauthorized!', { status: 401 });
73 | const newPwd = await request.text();
74 | if (newPwd === oldPwd) return new Response('Please enter a new Password!', { status: 400 });
75 | await env.bpb.put('pwd', newPwd);
76 | return new Response('Success', {
77 | status: 200,
78 | headers: {
79 | 'Set-Cookie': 'jwtToken=; Path=/; Secure; SameSite=None; Expires=Thu, 01 Jan 1970 00:00:00 GMT',
80 | 'Content-Type': 'text/plain',
81 | }
82 | });
83 | }
84 |
85 | export async function login(request, env) {
86 | await initializeParams(request, env);
87 | if (typeof env.bpb !== 'object') return await renderErrorPage(request, env, 'KV Dataset is not properly set!', null, true);
88 | const auth = await Authenticate(request, env);
89 | if (auth) return Response.redirect(`${origin}/panel`, 302);
90 | if (request.method === 'POST') return await generateJWTToken(request, env);
91 | return await renderLoginPage(request, env);
92 | }
--------------------------------------------------------------------------------
/src/cores-configs/clash.js:
--------------------------------------------------------------------------------
1 | import { getConfigAddresses, extractWireguardParams, generateRemark, randomUpperCase, getRandomPath, isIPv6, isIPv4 } from './helpers';
2 | import { initializeParams, userID, trojanPassword, hostName, defaultHttpsPorts } from "../helpers/init";
3 | import { getDataset } from '../kv/handlers';
4 | import { renderErrorPage } from '../pages/error';
5 | import { isDomain } from '../helpers/helpers';
6 |
7 | async function buildClashDNS (proxySettings, isChain, isWarp) {
8 | const {
9 | remoteDNS,
10 | localDNS,
11 | vlessTrojanFakeDNS,
12 | outProxyParams,
13 | enableIPv6,
14 | warpFakeDNS,
15 | warpEnableIPv6,
16 | bypassIran,
17 | bypassChina,
18 | bypassRussia,
19 | customBypassRules,
20 | customBlockRules
21 | } = proxySettings;
22 |
23 | const warpRemoteDNS = warpEnableIPv6
24 | ? ["1.1.1.1", "1.0.0.1", "[2606:4700:4700::1111]", "[2606:4700:4700::1001]"]
25 | : ["1.1.1.1", "1.0.0.1"];
26 | const isFakeDNS = (vlessTrojanFakeDNS && !isWarp) || (warpFakeDNS && isWarp);
27 | const isIPv6 = (enableIPv6 && !isWarp) || (warpEnableIPv6 && isWarp);
28 | const customBypassRulesDomains = customBypassRules.split(',').filter(address => isDomain(address));
29 | const isBypass = bypassIran || bypassChina || bypassRussia;
30 | const bypassRules = [
31 | { rule: bypassIran, geosite: "ir" },
32 | { rule: bypassChina, geosite: "cn" },
33 | { rule: bypassRussia, geosite: "ru" }
34 | ];
35 |
36 | const dns = {
37 | "enable": true,
38 | "listen": "0.0.0.0:1053",
39 | "ipv6": isIPv6,
40 | "respect-rules": true,
41 | "use-hosts": true,
42 | "use-system-hosts": false,
43 | "nameserver": isWarp
44 | ? warpRemoteDNS.map(dns => isChain ? `${dns}#💦 Warp - Best Ping 🚀` : `${dns}#✅ Selector`)
45 | : [isChain ? `${remoteDNS}#proxy-1` : `${remoteDNS}#✅ Selector`],
46 | "proxy-server-nameserver": [`${localDNS}#DIRECT`]
47 | };
48 |
49 | if (isChain && !isWarp) {
50 | const chainOutboundServer = JSON.parse(outProxyParams).server;
51 | if (isDomain(chainOutboundServer)) dns["nameserver-policy"] = {
52 | [chainOutboundServer]: isChain ? `${remoteDNS}#proxy-1` : `${remoteDNS}#✅ Selector`
53 | };
54 | }
55 |
56 | if (isBypass) {
57 | const geosites = [];
58 | bypassRules.forEach(({ rule, geosite }) => {
59 | rule && geosites.push(geosite)
60 | });
61 |
62 | dns["nameserver-policy"] = {
63 | ...dns["nameserver-policy"],
64 | [`rule-set:${geosites.join(',')}`]: [`${localDNS}#DIRECT`]
65 | };
66 | }
67 |
68 | customBypassRulesDomains.forEach( domain => {
69 | dns["nameserver-policy"] = {
70 | ...dns["nameserver-policy"],
71 | [`+.${domain}`]: [`${localDNS}#DIRECT`]
72 | };
73 | });
74 |
75 | if (isFakeDNS) Object.assign(dns, {
76 | "enhanced-mode": "fake-ip",
77 | "fake-ip-range": "198.18.0.1/16",
78 | "fake-ip-filter": ["geosite:private"]
79 | });
80 |
81 | return dns;
82 | }
83 |
84 | function buildClashRoutingRules (proxySettings) {
85 | const {
86 | bypassLAN,
87 | bypassIran,
88 | bypassChina,
89 | bypassRussia,
90 | blockAds,
91 | blockPorn,
92 | blockUDP443,
93 | customBypassRules,
94 | customBlockRules
95 | } = proxySettings;
96 |
97 | const customBypassRulesTotal = customBypassRules ? customBypassRules.split(',') : [];
98 | const customBlockRulesTotal = customBlockRules ? customBlockRules.split(',') : [];
99 | const geoRules = [
100 | {
101 | rule: bypassLAN,
102 | type: 'direct',
103 | noResolve: true,
104 | ruleProvider: {
105 | format: "yaml",
106 | geosite: "private",
107 | geoip: "private-cidr",
108 | geositeURL: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/private.yaml",
109 | geoipURL: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/private.yaml"
110 | }
111 | },
112 | {
113 | rule: bypassIran,
114 | type: 'direct',
115 | ruleProvider: {
116 | format: "text",
117 | geosite: "ir",
118 | geoip: "ir-cidr",
119 | geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/ir.txt",
120 | geoipURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/ircidr.txt"
121 | }
122 | },
123 | {
124 | rule: bypassChina,
125 | type: 'direct',
126 | ruleProvider: {
127 | format: "yaml",
128 | geosite: "cn",
129 | geoip: "cn-cidr",
130 | geositeURL: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/cn.yaml",
131 | geoipURL: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/cn.yaml"
132 | }
133 | },
134 | {
135 | rule: bypassRussia,
136 | type: 'direct',
137 | ruleProvider: {
138 | format: "yaml",
139 | geosite: "ru",
140 | geoip: "ru-cidr",
141 | geositeURL: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/category-ru.yaml",
142 | geoipURL: "https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geoip/ru.yaml"
143 | }
144 | },
145 | {
146 | rule: true,
147 | type: 'block',
148 | ruleProvider: {
149 | format: "text",
150 | geosite: "malware",
151 | geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/malware.txt"
152 | }
153 | },
154 | {
155 | rule: true,
156 | type: 'block',
157 | ruleProvider: {
158 | format: "text",
159 | geosite: "phishing",
160 | geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/phishing.txt"
161 | }
162 | },
163 | {
164 | rule: true,
165 | type: 'block',
166 | ruleProvider: {
167 | format: "text",
168 | geosite: "cryptominers",
169 | geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/cryptominers.txt"
170 | }
171 | },
172 | {
173 | rule: blockAds,
174 | type: 'block',
175 | ruleProvider: {
176 | format: "text",
177 | geosite: "ads",
178 | geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/ads.txt"
179 | }
180 | },
181 | {
182 | rule: blockPorn,
183 | type: 'block',
184 | ruleProvider: {
185 | format: "text",
186 | geosite: "nsfw",
187 | geositeURL: "https://raw.githubusercontent.com/Chocolate4U/Iran-clash-rules/release/nsfw.txt",
188 | }
189 | },
190 | ];
191 |
192 | function buildRuleProvider (tag, format, behavior, url) {
193 | const fileExtension = format === 'text' ? 'txt' : format;
194 | return {
195 | [tag]: {
196 | type: "http",
197 | format,
198 | behavior,
199 | url,
200 | path: `./ruleset/${tag}.${fileExtension}`,
201 | interval: 86400
202 | }
203 | }
204 | }
205 |
206 | const directDomainRules = [], directIPRules = [], blockDomainRules = [], blockIPRules = [], ruleProviders = {};
207 | geoRules.forEach( ({ rule, type, ruleProvider, noResolve }) => {
208 | const { geosite, geoip, geositeURL, geoipURL, format } = ruleProvider;
209 | if (rule) {
210 | if (geosite) {
211 | const targetRules = type === 'direct' ? directDomainRules : blockDomainRules;
212 | targetRules.push(`RULE-SET,${geosite},${type === 'direct' ? 'DIRECT' : 'REJECT'}`);
213 | const ruleProvider = buildRuleProvider(geosite, format, 'domain', geositeURL);
214 | Object.assign(ruleProviders, ruleProvider);
215 | }
216 |
217 | if (geoip) {
218 | const targetRules = type === 'direct' ? directIPRules : blockIPRules;
219 | targetRules.push(`RULE-SET,${geoip},${type === 'direct' ? 'DIRECT' : 'REJECT'}${noResolve ? ',no-resolve' : ''}`);
220 | const ruleProvider = buildRuleProvider(geoip, format, 'ipcidr', geoipURL);
221 | Object.assign(ruleProviders, ruleProvider);
222 | }
223 | }
224 | });
225 |
226 | const generateRule = (address, action) => {
227 | if (isDomain(address)) {
228 | return `DOMAIN-SUFFIX,${address},${action}`;
229 | } else {
230 | const type = isIPv4(address) ? 'IP-CIDR' : 'IP-CIDR6';
231 | const ip = isIPv6(address) ? address.replace(/\[|\]/g, '') : address;
232 | const cidr = address.includes('/') ? '' : isIPv4(address) ? '/32' : '/128';
233 | return `${type},${ip}${cidr},${action},no-resolve`;
234 | }
235 | };
236 |
237 | [...customBypassRulesTotal, ...customBlockRulesTotal].forEach((address, index) => {
238 | const isDirectRule = index < customBypassRulesTotal.length;
239 | const action = isDirectRule ? 'DIRECT' : 'REJECT';
240 | const targetRules = isDirectRule
241 | ? isDomain(address) ? directDomainRules : directIPRules
242 | : isDomain(address) ? blockDomainRules : blockIPRules;
243 |
244 | targetRules.push(generateRule(address, action));
245 | });
246 |
247 | const rules = [...directDomainRules, ...directIPRules, ...blockDomainRules, ...blockIPRules];
248 | blockUDP443 && rules.push("AND,((NETWORK,udp),(DST-PORT,443)),REJECT");
249 | rules.push("MATCH,✅ Selector");
250 | return { rules, ruleProviders };
251 | }
252 |
253 | function buildClashVLESSOutbound (remark, address, port, host, sni, path, allowInsecure) {
254 | const tls = defaultHttpsPorts.includes(port) ? true : false;
255 | const addr = isIPv6(address) ? address.replace(/\[|\]/g, '') : address;
256 | const outbound = {
257 | "name": remark,
258 | "type": "vless",
259 | "server": addr,
260 | "port": +port,
261 | "uuid": userID,
262 | "tls": tls,
263 | "network": "ws",
264 | "udp": true,
265 | "ws-opts": {
266 | "path": path,
267 | "headers": { "host": host },
268 | "max-early-data": 2560,
269 | "early-data-header-name": "Sec-WebSocket-Protocol"
270 | }
271 | };
272 |
273 | if (tls) {
274 | Object.assign(outbound, {
275 | "servername": sni,
276 | "alpn": ["h2", "http/1.1"],
277 | "client-fingerprint": "random",
278 | "skip-cert-verify": allowInsecure
279 | });
280 | }
281 |
282 | return outbound;
283 | }
284 |
285 | function buildClashTrojanOutbound (remark, address, port, host, sni, path, allowInsecure) {
286 | const addr = isIPv6(address) ? address.replace(/\[|\]/g, '') : address;
287 | return {
288 | "name": remark,
289 | "type": "trojan",
290 | "server": addr,
291 | "port": +port,
292 | "password": trojanPassword,
293 | "network": "ws",
294 | "udp": true,
295 | "ws-opts": {
296 | "path": path,
297 | "headers": { "host": host },
298 | "max-early-data": 2560,
299 | "early-data-header-name": "Sec-WebSocket-Protocol"
300 | },
301 | "sni": sni,
302 | "alpn": ["h2", "http/1.1"],
303 | "client-fingerprint": "random",
304 | "skip-cert-verify": allowInsecure
305 | };
306 | }
307 |
308 | function buildClashWarpOutbound (warpConfigs, remark, endpoint, chain) {
309 | const ipv6Regex = /\[(.*?)\]/;
310 | const portRegex = /[^:]*$/;
311 | const endpointServer = endpoint.includes('[') ? endpoint.match(ipv6Regex)[1] : endpoint.split(':')[0];
312 | const endpointPort = endpoint.includes('[') ? +endpoint.match(portRegex)[0] : +endpoint.split(':')[1];
313 | const {
314 | warpIPv6,
315 | reserved,
316 | publicKey,
317 | privateKey
318 | } = extractWireguardParams(warpConfigs, chain);
319 |
320 | return {
321 | "name": remark,
322 | "type": "wireguard",
323 | "ip": "172.16.0.2/32",
324 | "ipv6": warpIPv6,
325 | "private-key": privateKey,
326 | "server": endpointServer,
327 | "port": endpointPort,
328 | "public-key": publicKey,
329 | "allowed-ips": ["0.0.0.0/0", "::/0"],
330 | "reserved": reserved,
331 | "udp": true,
332 | "mtu": 1280,
333 | "dialer-proxy": chain
334 | };
335 | }
336 |
337 | function buildClashChainOutbound(chainProxyParams) {
338 | if (["socks", "http"].includes(chainProxyParams.protocol)) {
339 | const { protocol, server, port, user, pass } = chainProxyParams;
340 | const proxyType = protocol === 'socks' ? 'socks5' : protocol;
341 | return {
342 | "name": "",
343 | "type": proxyType,
344 | "server": server,
345 | "port": +port,
346 | "dialer-proxy": "",
347 | "username": user,
348 | "password": pass
349 | };
350 | }
351 |
352 | const { server, port, uuid, flow, security, type, sni, fp, alpn, pbk, sid, headerType, host, path, serviceName } = chainProxyParams;
353 | const chainOutbound = {
354 | "name": "💦 Chain Best Ping 💥",
355 | "type": "vless",
356 | "server": server,
357 | "port": +port,
358 | "udp": true,
359 | "uuid": uuid,
360 | "flow": flow,
361 | "network": type,
362 | "dialer-proxy": "💦 Best Ping 💥"
363 | };
364 |
365 | if (security === 'tls') {
366 | const tlsAlpns = alpn ? alpn?.split(',') : [];
367 | Object.assign(chainOutbound, {
368 | "tls": true,
369 | "servername": sni,
370 | "alpn": tlsAlpns,
371 | "client-fingerprint": fp
372 | });
373 | }
374 |
375 | if (security === 'reality') Object.assign(chainOutbound, {
376 | "tls": true,
377 | "servername": sni,
378 | "client-fingerprint": fp,
379 | "reality-opts": {
380 | "public-key": pbk,
381 | "short-id": sid
382 | }
383 | });
384 |
385 | if (headerType === 'http') {
386 | const httpPaths = path?.split(',');
387 | chainOutbound["http-opts"] = {
388 | "method": "GET",
389 | "path": httpPaths,
390 | "headers": {
391 | "Connection": ["keep-alive"],
392 | "Content-Type": ["application/octet-stream"]
393 | }
394 | };
395 | }
396 |
397 | if (type === 'ws') {
398 | const wsPath = path?.split('?ed=')[0];
399 | const earlyData = +path?.split('?ed=')[1];
400 | chainOutbound["ws-opts"] = {
401 | "path": wsPath,
402 | "headers": {
403 | "Host": host
404 | },
405 | "max-early-data": earlyData,
406 | "early-data-header-name": "Sec-WebSocket-Protocol"
407 | };
408 | }
409 |
410 | if (type === 'grpc') chainOutbound["grpc-opts"] = {
411 | "grpc-service-name": serviceName
412 | };
413 |
414 | return chainOutbound;
415 | }
416 |
417 | export async function getClashWarpConfig(request, env) {
418 | const { kvNotFound, proxySettings, warpConfigs } = await getDataset(request, env);
419 | if (kvNotFound) return await renderErrorPage(request, env, 'KV Dataset is not properly set!', null, true);
420 | const { warpEndpoints } = proxySettings;
421 | const config = structuredClone(clashConfigTemp);
422 | config.dns = await buildClashDNS(proxySettings, true, true);
423 | const { rules, ruleProviders } = buildClashRoutingRules(proxySettings);
424 | config.rules = rules;
425 | config['rule-providers'] = ruleProviders;
426 | const selector = config['proxy-groups'][0];
427 | const warpUrlTest = config['proxy-groups'][1];
428 | selector.proxies = ['💦 Warp - Best Ping 🚀', '💦 WoW - Best Ping 🚀'];
429 | warpUrlTest.name = '💦 Warp - Best Ping 🚀';
430 | warpUrlTest.interval = +proxySettings.bestWarpInterval;
431 | config['proxy-groups'].push(structuredClone(warpUrlTest));
432 | const WoWUrlTest = config['proxy-groups'][2];
433 | WoWUrlTest.name = '💦 WoW - Best Ping 🚀';
434 | let warpRemarks = [], WoWRemarks = [];
435 |
436 | warpEndpoints.split(',').forEach( (endpoint, index) => {
437 | const warpRemark = `💦 ${index + 1} - Warp 🇮🇷`;
438 | const WoWRemark = `💦 ${index + 1} - WoW 🌍`;
439 | const warpOutbound = buildClashWarpOutbound(warpConfigs, warpRemark, endpoint, '');
440 | const WoWOutbound = buildClashWarpOutbound(warpConfigs, WoWRemark, endpoint, warpRemark);
441 | config.proxies.push(WoWOutbound, warpOutbound);
442 | warpRemarks.push(warpRemark);
443 | WoWRemarks.push(WoWRemark);
444 | warpUrlTest.proxies.push(warpRemark);
445 | WoWUrlTest.proxies.push(WoWRemark);
446 | });
447 |
448 | selector.proxies.push(...warpRemarks, ...WoWRemarks);
449 | return new Response(JSON.stringify(config, null, 4), {
450 | status: 200,
451 | headers: {
452 | 'Content-Type': 'text/plain;charset=utf-8',
453 | 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
454 | 'CDN-Cache-Control': 'no-store'
455 | }
456 | });
457 | }
458 |
459 | export async function getClashNormalConfig (request, env) {
460 | await initializeParams(request, env);
461 | const { kvNotFound, proxySettings } = await getDataset(request, env);
462 | if (kvNotFound) return await renderErrorPage(request, env, 'KV Dataset is not properly set!', null, true);
463 | let chainProxy;
464 | const {
465 | resolvedRemoteDNS,
466 | cleanIPs,
467 | proxyIP,
468 | ports,
469 | vlessConfigs,
470 | trojanConfigs,
471 | outProxy,
472 | outProxyParams,
473 | customCdnAddrs,
474 | customCdnHost,
475 | customCdnSni,
476 | bestVLESSTrojanInterval,
477 | enableIPv6
478 | } = proxySettings;
479 |
480 | if (outProxy) {
481 | const proxyParams = JSON.parse(outProxyParams);
482 | try {
483 | chainProxy = buildClashChainOutbound(proxyParams);
484 | } catch (error) {
485 | console.log('An error occured while parsing chain proxy: ', error);
486 | chainProxy = undefined;
487 | await env.bpb.put("proxySettings", JSON.stringify({
488 | ...proxySettings,
489 | outProxy: '',
490 | outProxyParams: {}
491 | }));
492 | }
493 | }
494 |
495 | const config = structuredClone(clashConfigTemp);
496 | if (resolvedRemoteDNS.server) {
497 | config.hosts = {
498 | [resolvedRemoteDNS.server]: resolvedRemoteDNS.staticIPs
499 | }
500 | } else {
501 | delete config.hosts;
502 | }
503 | const { rules, ruleProviders } = buildClashRoutingRules(proxySettings);
504 | config.dns = await buildClashDNS(proxySettings, chainProxy, false);
505 | config.rules = rules;
506 | config['rule-providers'] = ruleProviders;
507 | const selector = config['proxy-groups'][0];
508 | const urlTest = config['proxy-groups'][1];
509 | selector.proxies = ['💦 Best Ping 💥'];
510 | urlTest.name = '💦 Best Ping 💥';
511 | urlTest.interval = +bestVLESSTrojanInterval;
512 | const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6);
513 | const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : [];
514 | const totalAddresses = [...Addresses, ...customCdnAddresses];
515 | let proxyIndex = 1, path;
516 | const protocols = [
517 | ...(vlessConfigs ? ['VLESS'] : []),
518 | ...(trojanConfigs ? ['Trojan'] : [])
519 | ];
520 |
521 | protocols.forEach ( protocol => {
522 | let protocolIndex = 1;
523 | ports.forEach ( port => {
524 | totalAddresses.forEach( addr => {
525 | let VLESSOutbound, TrojanOutbound;
526 | const isCustomAddr = customCdnAddresses.includes(addr);
527 | const configType = isCustomAddr ? 'C' : '';
528 | const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName);
529 | const host = isCustomAddr ? customCdnHost : hostName;
530 | const remark = generateRemark(protocolIndex, port, addr, cleanIPs, protocol, configType).replace(' : ', ' - ');
531 |
532 | if (protocol === 'VLESS') {
533 | path = `/${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`;
534 | VLESSOutbound = buildClashVLESSOutbound(
535 | chainProxy ? `proxy-${proxyIndex}` : remark,
536 | addr,
537 | port,
538 | host,
539 | sni,
540 | path,
541 | isCustomAddr
542 | );
543 | config.proxies.push(VLESSOutbound);
544 | selector.proxies.push(remark);
545 | urlTest.proxies.push(remark);
546 | }
547 |
548 | if (protocol === 'Trojan' && defaultHttpsPorts.includes(port)) {
549 | path = `/tr${getRandomPath(16)}${proxyIP ? `/${btoa(proxyIP)}` : ''}`;
550 | TrojanOutbound = buildClashTrojanOutbound(
551 | chainProxy ? `proxy-${proxyIndex}` : remark,
552 | addr,
553 | port,
554 | host,
555 | sni,
556 | path,
557 | isCustomAddr
558 | );
559 | config.proxies.push(TrojanOutbound);
560 | selector.proxies.push(remark);
561 | urlTest.proxies.push(remark);
562 | }
563 |
564 | if (chainProxy) {
565 | let chain = structuredClone(chainProxy);
566 | chain['name'] = remark;
567 | chain['dialer-proxy'] = `proxy-${proxyIndex}`;
568 | config.proxies.push(chain);
569 | }
570 |
571 | proxyIndex++;
572 | protocolIndex++;
573 | });
574 | });
575 | });
576 |
577 | return new Response(JSON.stringify(config, null, 4), {
578 | status: 200,
579 | headers: {
580 | 'Content-Type': 'text/plain;charset=utf-8',
581 | 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
582 | 'CDN-Cache-Control': 'no-store'
583 | }
584 | });
585 | }
586 |
587 | const clashConfigTemp = {
588 | "mixed-port": 7890,
589 | "ipv6": true,
590 | "allow-lan": true,
591 | "mode": "rule",
592 | "log-level": "warning",
593 | "disable-keep-alive": false,
594 | "keep-alive-idle": 30,
595 | "keep-alive-interval": 30,
596 | "unified-delay": false,
597 | "geo-auto-update": true,
598 | "geo-update-interval": 168,
599 | "external-controller": "127.0.0.1:9090",
600 | "external-ui-url": "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip",
601 | "external-ui": "ui",
602 | "external-controller-cors": {
603 | "allow-origins": [ "*" ],
604 | "allow-private-network": true
605 | },
606 | "profile": {
607 | "store-selected": true,
608 | "store-fake-ip": true
609 | },
610 | "hosts": {},
611 | "dns": {},
612 | "tun": {
613 | "enable": true,
614 | "stack": "mixed",
615 | "auto-route": true,
616 | "strict-route": true,
617 | "auto-detect-interface": true,
618 | "dns-hijack": ["any:53"],
619 | "mtu": 9000
620 | },
621 | "sniffer": {
622 | "enable": true,
623 | "force-dns-mapping": true,
624 | "parse-pure-ip": true,
625 | "override-destination": false,
626 | "sniff": {
627 | "HTTP": {
628 | "ports": [80, 8080, 8880, 2052, 2082, 2086, 2095]
629 | },
630 | "TLS": {
631 | "ports": [443, 8443, 2053, 2083, 2087, 2096]
632 | }
633 | }
634 | },
635 | "proxies": [],
636 | "proxy-groups": [
637 | {
638 | "name": "✅ Selector",
639 | "type": "select",
640 | "proxies": []
641 | },
642 | {
643 | "name": "",
644 | "type": "url-test",
645 | "url": "https://www.gstatic.com/generate_204",
646 | "interval": 30,
647 | "tolerance": 50,
648 | "proxies": []
649 | }
650 | ],
651 | "rule-providers": {},
652 | "rules": [],
653 | "ntp": {
654 | "enable": true,
655 | "server": "time.apple.com",
656 | "port": 123,
657 | "interval": 30
658 | }
659 | };
--------------------------------------------------------------------------------
/src/cores-configs/helpers.js:
--------------------------------------------------------------------------------
1 | import { resolveDNS, isDomain } from '../helpers/helpers';
2 |
3 | export async function getConfigAddresses(hostName, cleanIPs, enableIPv6) {
4 | const resolved = await resolveDNS(hostName);
5 | const defaultIPv6 = enableIPv6 ? resolved.ipv6.map((ip) => `[${ip}]`) : []
6 | return [
7 | hostName,
8 | 'www.speedtest.net',
9 | ...resolved.ipv4,
10 | ...defaultIPv6,
11 | ...(cleanIPs ? cleanIPs.split(',') : [])
12 | ];
13 | }
14 |
15 | export function extractWireguardParams(warpConfigs, isWoW) {
16 | const index = isWoW ? 1 : 0;
17 | const warpConfig = warpConfigs[index].account.config;
18 | return {
19 | warpIPv6: `${warpConfig.interface.addresses.v6}/128`,
20 | reserved: warpConfig.client_id,
21 | publicKey: warpConfig.peers[0].public_key,
22 | privateKey: warpConfigs[index].privateKey,
23 | };
24 | }
25 |
26 | export function generateRemark(index, port, address, cleanIPs, protocol, configType) {
27 | let addressType;
28 | const type = configType ? ` ${configType}` : '';
29 |
30 | cleanIPs.includes(address)
31 | ? addressType = 'Clean IP'
32 | : addressType = isDomain(address) ? 'Domain': isIPv4(address) ? 'IPv4' : isIPv6(address) ? 'IPv6' : '';
33 |
34 | return `💦 ${index} - ${protocol}${type} - ${addressType} : ${port}`;
35 | }
36 |
37 | export function randomUpperCase (str) {
38 | let result = '';
39 | for (let i = 0; i < str.length; i++) {
40 | result += Math.random() < 0.5 ? str[i].toUpperCase() : str[i];
41 | }
42 | return result;
43 | }
44 |
45 | export function getRandomPath (length) {
46 | let result = '';
47 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
48 | const charactersLength = characters.length;
49 | for (let i = 0; i < length; i++) {
50 | result += characters.charAt(Math.floor(Math.random() * charactersLength));
51 | }
52 | return result;
53 | }
54 |
55 | export function base64ToDecimal (base64) {
56 | const binaryString = atob(base64);
57 | const hexString = Array.from(binaryString).map(char => char.charCodeAt(0).toString(16).padStart(2, '0')).join('');
58 | const decimalArray = hexString.match(/.{2}/g).map(hex => parseInt(hex, 16));
59 | return decimalArray;
60 | }
61 |
62 | export function isIPv4(address) {
63 | const ipv4Pattern = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
64 | return ipv4Pattern.test(address);
65 | }
66 |
67 | export function isIPv6(address) {
68 | const ipv6Pattern = /^\[(?:(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,7}:|::(?:[a-fA-F0-9]{1,4}:){0,7}|(?:[a-fA-F0-9]{1,4}:){1,6}:[a-fA-F0-9]{1,4}|(?:[a-fA-F0-9]{1,4}:){1,5}(?::[a-fA-F0-9]{1,4}){1,2}|(?:[a-fA-F0-9]{1,4}:){1,4}(?::[a-fA-F0-9]{1,4}){1,3}|(?:[a-fA-F0-9]{1,4}:){1,3}(?::[a-fA-F0-9]{1,4}){1,4}|(?:[a-fA-F0-9]{1,4}:){1,2}(?::[a-fA-F0-9]{1,4}){1,5}|[a-fA-F0-9]{1,4}:(?::[a-fA-F0-9]{1,4}){1,6})\](?:\/(1[0-1][0-9]|12[0-8]|[0-9]?[0-9]))?$/;
69 | return ipv6Pattern.test(address);
70 | }
--------------------------------------------------------------------------------
/src/cores-configs/normalConfigs.js:
--------------------------------------------------------------------------------
1 | import { getConfigAddresses, generateRemark, randomUpperCase, getRandomPath } from './helpers';
2 | import { initializeParams, userID, trojanPassword, hostName, client, defaultHttpsPorts } from "../helpers/init";
3 | import { getDataset } from '../kv/handlers';
4 | import { renderErrorPage } from '../pages/error';
5 |
6 | export async function getNormalConfigs(request, env) {
7 | await initializeParams(request, env);
8 | const { kvNotFound, proxySettings } = await getDataset(request, env);
9 | if (kvNotFound) return await renderErrorPage(request, env, 'KV Dataset is not properly set!', null, true);
10 | const {
11 | cleanIPs,
12 | proxyIP,
13 | ports,
14 | vlessConfigs,
15 | trojanConfigs ,
16 | outProxy,
17 | customCdnAddrs,
18 | customCdnHost,
19 | customCdnSni,
20 | enableIPv6
21 | } = proxySettings;
22 |
23 | let vlessConfs = '', trojanConfs = '', chainProxy = '';
24 | let proxyIndex = 1;
25 | const Addresses = await getConfigAddresses(hostName, cleanIPs, enableIPv6);
26 | const customCdnAddresses = customCdnAddrs ? customCdnAddrs.split(',') : [];
27 | const totalAddresses = [...Addresses, ...customCdnAddresses];
28 | const alpn = client === 'singbox' ? 'http/1.1' : 'h2,http/1.1';
29 | const trojanPass = encodeURIComponent(trojanPassword);
30 | const earlyData = client === 'singbox'
31 | ? '&eh=Sec-WebSocket-Protocol&ed=2560'
32 | : encodeURIComponent('?ed=2560');
33 |
34 | ports.forEach(port => {
35 | totalAddresses.forEach((addr, index) => {
36 | const isCustomAddr = index > Addresses.length - 1;
37 | const configType = isCustomAddr ? 'C' : '';
38 | const sni = isCustomAddr ? customCdnSni : randomUpperCase(hostName);
39 | const host = isCustomAddr ? customCdnHost : hostName;
40 | const path = `${getRandomPath(16)}${proxyIP ? `/${encodeURIComponent(btoa(proxyIP))}` : ''}${earlyData}`;
41 | const vlessRemark = encodeURIComponent(generateRemark(proxyIndex, port, addr, cleanIPs, 'VLESS', configType));
42 | const trojanRemark = encodeURIComponent(generateRemark(proxyIndex, port, addr, cleanIPs, 'Trojan', configType));
43 | const tlsFields = defaultHttpsPorts.includes(port)
44 | ? `&security=tls&sni=${sni}&fp=randomized&alpn=${alpn}`
45 | : '&security=none';
46 |
47 | if (vlessConfigs) {
48 | vlessConfs += `${atob('dmxlc3M6Ly8=')}${userID}@${addr}:${port}?path=/${path}&encryption=none&host=${host}&type=ws${tlsFields}#${vlessRemark}\n`;
49 | }
50 |
51 | if (trojanConfigs) {
52 | trojanConfs += `${atob('dHJvamFuOi8v')}${trojanPass}@${addr}:${port}?path=/tr${path}&host=${host}&type=ws${tlsFields}#${trojanRemark}\n`;
53 | }
54 |
55 | proxyIndex++;
56 | });
57 | });
58 |
59 | if (outProxy) {
60 | let chainRemark = `#${encodeURIComponent('💦 Chain proxy 🔗')}`;
61 | if (outProxy.startsWith('socks') || outProxy.startsWith('http')) {
62 | const regex = /^(?:socks|http):\/\/([^@]+)@/;
63 | const isUserPass = outProxy.match(regex);
64 | const userPass = isUserPass ? isUserPass[1] : false;
65 | chainProxy = userPass
66 | ? outProxy.replace(userPass, btoa(userPass)) + chainRemark
67 | : outProxy + chainRemark;
68 | } else {
69 | chainProxy = outProxy.split('#')[0] + chainRemark;
70 | }
71 | }
72 |
73 | const configs = btoa(vlessConfs + trojanConfs + chainProxy);
74 | return new Response(configs, {
75 | status: 200,
76 | headers: {
77 | 'Content-Type': 'text/plain;charset=utf-8',
78 | 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
79 | 'CDN-Cache-Control': 'no-store'
80 | }
81 | });
82 | }
--------------------------------------------------------------------------------
/src/helpers/helpers.js:
--------------------------------------------------------------------------------
1 | import { Authenticate } from "../authentication/auth";
2 | import { getDataset, updateDataset } from "../kv/handlers";
3 | import { renderErrorPage } from "../pages/error";
4 | import { renderHomePage } from "../pages/home";
5 | import { initializeParams, origin } from "./init";
6 |
7 | export function isValidUUID(uuid) {
8 | 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;
9 | return uuidRegex.test(uuid);
10 | }
11 |
12 | export async function resolveDNS (domain) {
13 | const dohURL = 'https://cloudflare-dns.com/dns-query';
14 | const dohURLv4 = `${dohURL}?name=${encodeURIComponent(domain)}&type=A`;
15 | const dohURLv6 = `${dohURL}?name=${encodeURIComponent(domain)}&type=AAAA`;
16 |
17 | try {
18 | const [ipv4Response, ipv6Response] = await Promise.all([
19 | fetch(dohURLv4, { headers: { accept: 'application/dns-json' } }),
20 | fetch(dohURLv6, { headers: { accept: 'application/dns-json' } })
21 | ]);
22 |
23 | const ipv4Addresses = await ipv4Response.json();
24 | const ipv6Addresses = await ipv6Response.json();
25 |
26 | const ipv4 = ipv4Addresses.Answer
27 | ? ipv4Addresses.Answer.map((record) => record.data)
28 | : [];
29 | const ipv6 = ipv6Addresses.Answer
30 | ? ipv6Addresses.Answer.map((record) => record.data)
31 | : [];
32 |
33 | return { ipv4, ipv6 };
34 | } catch (error) {
35 | console.error('Error resolving DNS:', error);
36 | throw new Error(`An error occurred while resolving DNS - ${error}`);
37 | }
38 | }
39 |
40 | export function isDomain(address) {
41 | const domainPattern = /^(?!\-)(?:[A-Za-z0-9\-]{1,63}\.)+[A-Za-z]{2,}$/;
42 | return domainPattern.test(address);
43 | }
44 |
45 | export async function handlePanel(request, env) {
46 | await initializeParams(request, env);
47 | const auth = await Authenticate(request, env);
48 | if (request.method === 'POST') {
49 | if (!auth) return new Response('Unauthorized or expired session!', { status: 401 });
50 | await updateDataset(request, env);
51 | return new Response('Success', { status: 200 });
52 | }
53 |
54 | const { kvNotFound, proxySettings } = await getDataset(request, env);
55 | if (kvNotFound) return await renderErrorPage(request, env, 'KV Dataset is not properly set!', null, true);
56 | const pwd = await env.bpb.get('pwd');
57 | if (pwd && !auth) return Response.redirect(`${origin}/login`, 302);
58 | const isPassSet = pwd?.length >= 8;
59 | return await renderHomePage(request, env, proxySettings, isPassSet);
60 | }
61 |
62 | export async function fallback(request) {
63 | const url = new URL(request.url);
64 | url.hostname = 'www.speedtest.net';
65 | url.protocol = 'https:';
66 | request = new Request(url, request);
67 | return await fetch(request);
68 | }
69 |
70 | export async function getMyIP(request) {
71 | const ip = await request.text();
72 | try {
73 | const response = await fetch(`http://ip-api.com/json/${ip}?nocache=${Date.now()}`);
74 | const geoLocation = await response.json();
75 | return new Response(JSON.stringify(geoLocation), {
76 | status: 200,
77 | headers: {
78 | 'Content-Type': 'text/plain;charset=utf-8'
79 | }
80 | });
81 | } catch (error) {
82 | console.error('Error fetching IP address:', error);
83 | }
84 | }
--------------------------------------------------------------------------------
/src/helpers/init.js:
--------------------------------------------------------------------------------
1 | import { isValidUUID } from './helpers';
2 | const defaultProxyIP = 'bpb.yousef.isegaro.com';
3 | let userID, dohURL, proxyIP, trojanPassword, defaultHttpPorts, defaultHttpsPorts, panelVersion, hostName, origin, client, pathName;
4 |
5 | function initParams(request, env) {
6 | const proxyIPs = env.PROXYIP?.split(',').map(proxyIP => proxyIP.trim());
7 | userID = env.UUID || '89b3cbba-e6ac-485a-9481-976a0415eab9';
8 | if (!isValidUUID(userID)) throw new Error(`Invalid UUID: ${userID}`);
9 | dohURL = env.DOH_URL || 'https://cloudflare-dns.com/dns-query';
10 | proxyIP = proxyIPs ? proxyIPs[Math.floor(Math.random() * proxyIPs.length)] : defaultProxyIP;
11 | trojanPassword = env.TROJAN_PASS || 'bpb-trojan';
12 | defaultHttpPorts = ['80', '8080', '2052', '2082', '2086', '2095', '8880'];
13 | defaultHttpsPorts = ['443', '8443', '2053', '2083', '2087', '2096'];
14 | panelVersion = '2.7.6';
15 | hostName = request.headers.get('Host');
16 | const url = new URL(request.url);
17 | const searchParams = new URLSearchParams(url.search);
18 | client = searchParams.get('app');
19 | origin = url.origin;
20 | pathName = url.pathname;
21 | }
22 |
23 | export function initializeParams(request, env) {
24 | initParams(request, env);
25 | return Promise.resolve();
26 | }
27 |
28 | export { userID, dohURL, proxyIP, trojanPassword, hostName, origin, client, pathName, defaultHttpPorts, defaultHttpsPorts, panelVersion };
29 |
--------------------------------------------------------------------------------
/src/kv/handlers.js:
--------------------------------------------------------------------------------
1 | import { fetchWarpConfigs } from '../protocols/warp';
2 | import { isDomain, resolveDNS } from '../helpers/helpers';
3 | import { initializeParams, panelVersion } from '../helpers/init';
4 | import { Authenticate } from '../authentication/auth';
5 | import { renderErrorPage } from '../pages/error';
6 |
7 | export async function getDataset(request, env) {
8 | await initializeParams(request, env);
9 | let proxySettings, warpConfigs;
10 | if (typeof env.bpb !== 'object') {
11 | return {kvNotFound: true, proxySettings: null, warpConfigs: null}
12 | }
13 |
14 | try {
15 | proxySettings = await env.bpb.get("proxySettings", {type: 'json'});
16 | warpConfigs = await env.bpb.get('warpConfigs', {type: 'json'});
17 | } catch (error) {
18 | console.log(error);
19 | throw new Error(`An error occurred while getting KV - ${error}`);
20 | }
21 |
22 | if (!proxySettings) {
23 | proxySettings = await updateDataset(request, env);
24 | const { error, configs } = await fetchWarpConfigs(env, proxySettings);
25 | if (error) throw new Error(`An error occurred while getting Warp configs - ${error}`);
26 | warpConfigs = configs;
27 | }
28 |
29 | if (panelVersion !== proxySettings.panelVersion) proxySettings = await updateDataset(request, env);
30 | return {kvNotFound: false, proxySettings, warpConfigs}
31 | }
32 |
33 | export async function updateDataset (request, env) {
34 | await initializeParams(request, env);
35 | let newSettings = request.method === 'POST' ? await request.formData() : null;
36 | const isReset = newSettings?.get('resetSettings') === 'true';
37 | let currentSettings;
38 | if (!isReset) {
39 | try {
40 | currentSettings = await env.bpb.get("proxySettings", {type: 'json'});
41 | } catch (error) {
42 | console.log(error);
43 | throw new Error(`An error occurred while getting current KV settings - ${error}`);
44 | }
45 | } else {
46 | await env.bpb.delete('warpConfigs');
47 | newSettings = null;
48 | }
49 |
50 | const validateField = (field) => {
51 | const fieldValue = newSettings?.get(field);
52 | if (fieldValue === undefined) return null;
53 | if (fieldValue === 'true') return true;
54 | if (fieldValue === 'false') return false;
55 | return fieldValue;
56 | }
57 |
58 | const remoteDNS = validateField('remoteDNS') ?? currentSettings?.remoteDNS ?? 'https://8.8.8.8/dns-query';
59 | const enableIPv6 = validateField('enableIPv6') ?? currentSettings?.enableIPv6 ?? true;
60 | const url = new URL(remoteDNS);
61 | const remoteDNSServer = url.hostname;
62 | const isServerDomain = isDomain(remoteDNSServer);
63 | let resolvedRemoteDNS = {};
64 | if (isServerDomain) {
65 | try {
66 | const resolvedDomain = await resolveDNS(remoteDNSServer);
67 | resolvedRemoteDNS = {
68 | server: remoteDNSServer,
69 | staticIPs: enableIPv6 ? [...resolvedDomain.ipv4, ...resolvedDomain.ipv6] : resolvedDomain.ipv4
70 | };
71 | } catch (error) {
72 | console.log(error);
73 | throw new Error(`An error occurred while resolving remote DNS server, please try agian! - ${error}`);
74 | }
75 | }
76 |
77 | const proxySettings = {
78 | remoteDNS: remoteDNS,
79 | resolvedRemoteDNS: resolvedRemoteDNS,
80 | localDNS: validateField('localDNS') ?? currentSettings?.localDNS ?? '8.8.8.8',
81 | vlessTrojanFakeDNS: validateField('vlessTrojanFakeDNS') ?? currentSettings?.vlessTrojanFakeDNS ?? false,
82 | proxyIP: validateField('proxyIP')?.replaceAll(' ', '') ?? currentSettings?.proxyIP ?? '',
83 | outProxy: validateField('outProxy') ?? currentSettings?.outProxy ?? '',
84 | outProxyParams: extractChainProxyParams(validateField('outProxy')) ?? currentSettings?.outProxyParams ?? {},
85 | cleanIPs: validateField('cleanIPs')?.replaceAll(' ', '') ?? currentSettings?.cleanIPs ?? '',
86 | enableIPv6: enableIPv6,
87 | customCdnAddrs: validateField('customCdnAddrs')?.replaceAll(' ', '') ?? currentSettings?.customCdnAddrs ?? '',
88 | customCdnHost: validateField('customCdnHost')?.trim() ?? currentSettings?.customCdnHost ?? '',
89 | customCdnSni: validateField('customCdnSni')?.trim() ?? currentSettings?.customCdnSni ?? '',
90 | bestVLESSTrojanInterval: validateField('bestVLESSTrojanInterval') ?? currentSettings?.bestVLESSTrojanInterval ?? '30',
91 | vlessConfigs: validateField('vlessConfigs') ?? currentSettings?.vlessConfigs ?? true,
92 | trojanConfigs: validateField('trojanConfigs') ?? currentSettings?.trojanConfigs ?? false,
93 | ports: validateField('ports')?.split(',') ?? currentSettings?.ports ?? ['443'],
94 | lengthMin: validateField('fragmentLengthMin') ?? currentSettings?.lengthMin ?? '100',
95 | lengthMax: validateField('fragmentLengthMax') ?? currentSettings?.lengthMax ?? '200',
96 | intervalMin: validateField('fragmentIntervalMin') ?? currentSettings?.intervalMin ?? '1',
97 | intervalMax: validateField('fragmentIntervalMax') ?? currentSettings?.intervalMax ?? '1',
98 | fragmentPackets: validateField('fragmentPackets') ?? currentSettings?.fragmentPackets ?? 'tlshello',
99 | bypassLAN: validateField('bypass-lan') ?? currentSettings?.bypassLAN ?? false,
100 | bypassIran: validateField('bypass-iran') ?? currentSettings?.bypassIran ?? false,
101 | bypassChina: validateField('bypass-china') ?? currentSettings?.bypassChina ?? false,
102 | bypassRussia: validateField('bypass-russia') ?? currentSettings?.bypassRussia ?? false,
103 | blockAds: validateField('block-ads') ?? currentSettings?.blockAds ?? false,
104 | blockPorn: validateField('block-porn') ?? currentSettings?.blockPorn ?? false,
105 | blockUDP443: validateField('block-udp-443') ?? currentSettings?.blockUDP443 ?? false,
106 | customBypassRules: validateField('customBypassRules')?.replaceAll(' ', '') ?? currentSettings?.customBypassRules ?? '',
107 | customBlockRules: validateField('customBlockRules')?.replaceAll(' ', '') ?? currentSettings?.customBlockRules ?? '',
108 | warpEndpoints: validateField('warpEndpoints')?.replaceAll(' ', '') ?? currentSettings?.warpEndpoints ?? 'engage.cloudflareclient.com:2408',
109 | warpFakeDNS: validateField('warpFakeDNS') ?? currentSettings?.warpFakeDNS ?? false,
110 | warpEnableIPv6: validateField('warpEnableIPv6') ?? currentSettings?.warpEnableIPv6 ?? true,
111 | warpPlusLicense: validateField('warpPlusLicense') ?? currentSettings?.warpPlusLicense ?? '',
112 | bestWarpInterval: validateField('bestWarpInterval') ?? currentSettings?.bestWarpInterval ?? '30',
113 | hiddifyNoiseMode: validateField('hiddifyNoiseMode') ?? currentSettings?.hiddifyNoiseMode ?? 'm4',
114 | nikaNGNoiseMode: validateField('nikaNGNoiseMode') ?? currentSettings?.nikaNGNoiseMode ?? 'quic',
115 | noiseCountMin: validateField('noiseCountMin') ?? currentSettings?.noiseCountMin ?? '10',
116 | noiseCountMax: validateField('noiseCountMax') ?? currentSettings?.noiseCountMax ?? '15',
117 | noiseSizeMin: validateField('noiseSizeMin') ?? currentSettings?.noiseSizeMin ?? '5',
118 | noiseSizeMax: validateField('noiseSizeMax') ?? currentSettings?.noiseSizeMax ?? '10',
119 | noiseDelayMin: validateField('noiseDelayMin') ?? currentSettings?.noiseDelayMin ?? '1',
120 | noiseDelayMax: validateField('noiseDelayMax') ?? currentSettings?.noiseDelayMax ?? '1',
121 | panelVersion: panelVersion
122 | };
123 |
124 | try {
125 | await env.bpb.put("proxySettings", JSON.stringify(proxySettings));
126 | } catch (error) {
127 | console.log(error);
128 | throw new Error(`An error occurred while updating KV - ${error}`);
129 | }
130 |
131 | return proxySettings;
132 | }
133 |
134 | function extractChainProxyParams(chainProxy) {
135 | let configParams = {};
136 | if (!chainProxy) return {};
137 | const url = new URL(chainProxy);
138 | const protocol = url.protocol.slice(0, -1);
139 | if (protocol === 'vless') {
140 | const params = new URLSearchParams(url.search);
141 | configParams = {
142 | protocol: protocol,
143 | uuid : url.username,
144 | server : url.hostname,
145 | port : url.port
146 | };
147 |
148 | params.forEach( (value, key) => {
149 | configParams[key] = value;
150 | });
151 | } else {
152 | configParams = {
153 | protocol: protocol,
154 | user : url.username,
155 | pass : url.password,
156 | server : url.host,
157 | port : url.port
158 | };
159 | }
160 |
161 | return JSON.stringify(configParams);
162 | }
163 |
164 | export async function updateWarpConfigs(request, env) {
165 | const auth = await Authenticate(request, env);
166 | if (!auth) return new Response('Unauthorized', { status: 401 });
167 | if (request.method === 'POST') {
168 | try {
169 | const { kvNotFound, proxySettings } = await getDataset(request, env);
170 | if (kvNotFound) return await renderErrorPage(request, env, 'KV Dataset is not properly set!', null, true);
171 | const { error: warpPlusError } = await fetchWarpConfigs(env, proxySettings);
172 | if (warpPlusError) return new Response(warpPlusError, { status: 400 });
173 | return new Response('Warp configs updated successfully', { status: 200 });
174 | } catch (error) {
175 | console.log(error);
176 | return new Response(`An error occurred while updating Warp configs! - ${error}`, { status: 500 });
177 | }
178 | } else {
179 | return new Response('Unsupported request', { status: 405 });
180 | }
181 | }
--------------------------------------------------------------------------------
/src/pages/error.js:
--------------------------------------------------------------------------------
1 | import { initializeParams, panelVersion } from "../helpers/init";
2 |
3 | export async function renderErrorPage (request, env, message, error, refer) {
4 | await initializeParams(request, env);
5 | const errorPage = `
6 |
7 |
8 |
9 |
10 |
11 | Error Page
12 |
40 |
41 |
42 |
43 |
BPB Panel ${panelVersion} 💦
44 |
45 |
${message} ${refer
46 | ? 'Please try again or refer to documents'
47 | : ''}
48 |
49 |
${error ? `⚠️ ${error.stack.toString()}` : ''}
50 |
51 |
52 |
55 |
56 | `;
57 |
58 | return new Response(errorPage, { status: 200, headers: {'Content-Type': 'text/html'}});
59 | }
--------------------------------------------------------------------------------
/src/pages/login.js:
--------------------------------------------------------------------------------
1 | import { initializeParams, origin, panelVersion } from "../helpers/init";
2 |
3 | export async function renderLoginPage (request, env) {
4 | await initializeParams(request, env);
5 | const loginPage = `
6 |
7 |
8 |
9 |
10 |
11 | User Login
12 |
104 |
105 |
106 |
107 |
BPB Panel ${panelVersion} 💦
108 |
119 |
120 |
147 |
148 | `;
149 |
150 | return new Response(loginPage, {
151 | status: 200,
152 | headers: {
153 | 'Content-Type': 'text/html;charset=utf-8',
154 | 'Access-Control-Allow-Origin': origin,
155 | 'Access-Control-Allow-Methods': 'GET, POST',
156 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
157 | 'X-Content-Type-Options': 'nosniff',
158 | 'X-Frame-Options': 'DENY',
159 | 'Referrer-Policy': 'strict-origin-when-cross-origin',
160 | 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate, no-transform',
161 | 'CDN-Cache-Control': 'no-store'
162 | }
163 | });
164 | }
--------------------------------------------------------------------------------
/src/protocols/trojan.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'cloudflare:sockets';
2 | import sha256 from 'js-sha256';
3 | import { initializeParams, trojanPassword, proxyIP, pathName } from "../helpers/init";
4 |
5 | export async function trojanOverWSHandler(request, env) {
6 | await initializeParams(request, env);
7 | const webSocketPair = new WebSocketPair();
8 | const [client, webSocket] = Object.values(webSocketPair);
9 | webSocket.accept();
10 | let address = "";
11 | let portWithRandomLog = "";
12 | const log = (info, event) => {
13 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || "");
14 | };
15 | const earlyDataHeader = request.headers.get("sec-websocket-protocol") || "";
16 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);
17 | let remoteSocketWapper = {
18 | value: null,
19 | };
20 | let udpStreamWrite = null;
21 |
22 | readableWebSocketStream
23 | .pipeTo(
24 | new WritableStream({
25 | async write(chunk, controller) {
26 | if (udpStreamWrite) {
27 | return udpStreamWrite(chunk);
28 | }
29 |
30 | if (remoteSocketWapper.value) {
31 | const writer = remoteSocketWapper.value.writable.getWriter();
32 | await writer.write(chunk);
33 | writer.releaseLock();
34 | return;
35 | }
36 |
37 | const {
38 | hasError,
39 | message,
40 | portRemote = 443,
41 | addressRemote = "",
42 | rawClientData,
43 | } = await parseTrojanHeader(chunk);
44 |
45 | address = addressRemote;
46 | portWithRandomLog = `${portRemote}--${Math.random()} tcp`;
47 |
48 | if (hasError) {
49 | throw new Error(message);
50 | return;
51 | }
52 |
53 | handleTCPOutBound(request, remoteSocketWapper, addressRemote, portRemote, rawClientData, webSocket, log);
54 | },
55 | close() {
56 | log(`readableWebSocketStream is closed`);
57 | },
58 | abort(reason) {
59 | log(`readableWebSocketStream is aborted`, JSON.stringify(reason));
60 | },
61 | })
62 | )
63 | .catch((err) => {
64 | log("readableWebSocketStream pipeTo error", err);
65 | });
66 |
67 | return new Response(null, {
68 | status: 101,
69 | // @ts-ignore
70 | webSocket: client,
71 | });
72 | }
73 |
74 | async function parseTrojanHeader(buffer) {
75 | if (buffer.byteLength < 56) {
76 | return {
77 | hasError: true,
78 | message: "invalid data",
79 | };
80 | }
81 |
82 | let crLfIndex = 56;
83 | if (new Uint8Array(buffer.slice(56, 57))[0] !== 0x0d || new Uint8Array(buffer.slice(57, 58))[0] !== 0x0a) {
84 | return {
85 | hasError: true,
86 | message: "invalid header format (missing CR LF)",
87 | };
88 | }
89 |
90 | const password = new TextDecoder().decode(buffer.slice(0, crLfIndex));
91 | if (password !== sha256.sha224(trojanPassword)) {
92 | return {
93 | hasError: true,
94 | message: "invalid password",
95 | };
96 | }
97 |
98 | const socks5DataBuffer = buffer.slice(crLfIndex + 2);
99 | if (socks5DataBuffer.byteLength < 6) {
100 | return {
101 | hasError: true,
102 | message: "invalid SOCKS5 request data",
103 | };
104 | }
105 |
106 | const view = new DataView(socks5DataBuffer);
107 | const cmd = view.getUint8(0);
108 | if (cmd !== 1) {
109 | return {
110 | hasError: true,
111 | message: "unsupported command, only TCP (CONNECT) is allowed",
112 | };
113 | }
114 |
115 | const atype = view.getUint8(1);
116 | // 0x01: IPv4 address
117 | // 0x03: Domain name
118 | // 0x04: IPv6 address
119 | let addressLength = 0;
120 | let addressIndex = 2;
121 | let address = "";
122 | switch (atype) {
123 | case 1:
124 | addressLength = 4;
125 | address = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength)).join(".");
126 | break;
127 | case 3:
128 | addressLength = new Uint8Array(socks5DataBuffer.slice(addressIndex, addressIndex + 1))[0];
129 | addressIndex += 1;
130 | address = new TextDecoder().decode(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength));
131 | break;
132 | case 4:
133 | addressLength = 16;
134 | const dataView = new DataView(socks5DataBuffer.slice(addressIndex, addressIndex + addressLength));
135 | const ipv6 = [];
136 | for (let i = 0; i < 8; i++) {
137 | ipv6.push(dataView.getUint16(i * 2).toString(16));
138 | }
139 | address = ipv6.join(":");
140 | break;
141 | default:
142 | return {
143 | hasError: true,
144 | message: `invalid addressType is ${atype}`,
145 | };
146 | }
147 |
148 | if (!address) {
149 | return {
150 | hasError: true,
151 | message: `address is empty, addressType is ${atype}`,
152 | };
153 | }
154 |
155 | const portIndex = addressIndex + addressLength;
156 | const portBuffer = socks5DataBuffer.slice(portIndex, portIndex + 2);
157 | const portRemote = new DataView(portBuffer).getUint16(0);
158 | return {
159 | hasError: false,
160 | addressRemote: address,
161 | portRemote,
162 | rawClientData: socks5DataBuffer.slice(portIndex + 4),
163 | };
164 | }
165 |
166 | /**
167 | * Handles outbound TCP connections.
168 | *
169 | * @param {any} remoteSocket
170 | * @param {string} addressRemote The remote address to connect to.
171 | * @param {number} portRemote The remote port to connect to.
172 | * @param {Uint8Array} rawClientData The raw client data to write.
173 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
174 | * @param {function} log The logging function.
175 | * @returns {Promise} The remote socket.
176 | */
177 | async function handleTCPOutBound(
178 | request,
179 | remoteSocket,
180 | addressRemote,
181 | portRemote,
182 | rawClientData,
183 | webSocket,
184 | log
185 | ) {
186 | async function connectAndWrite(address, port) {
187 | if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) address = `${atob('d3d3Lg==')}${address}${atob('LnNzbGlwLmlv')}`;
188 | /** @type {import("@cloudflare/workers-types").Socket} */
189 | const tcpSocket = connect({
190 | hostname: address,
191 | port: port,
192 | });
193 | remoteSocket.value = tcpSocket;
194 | log(`connected to ${address}:${port}`);
195 | const writer = tcpSocket.writable.getWriter();
196 | await writer.write(rawClientData); // first write, nomal is tls client hello
197 | writer.releaseLock();
198 | return tcpSocket;
199 | }
200 |
201 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip
202 | async function retry() {
203 | const panelProxyIP = pathName.split('/')[2];
204 | const panelProxyIPs = panelProxyIP ? atob(panelProxyIP).split(',') : undefined;
205 | const finalProxyIP = panelProxyIPs ? panelProxyIPs[Math.floor(Math.random() * panelProxyIPs.length)] : proxyIP || addressRemote;
206 | const tcpSocket = await connectAndWrite(finalProxyIP, portRemote);
207 | // no matter retry success or not, close websocket
208 | tcpSocket.closed
209 | .catch((error) => {
210 | console.log("retry tcpSocket closed error", error);
211 | })
212 | .finally(() => {
213 | safeCloseWebSocket(webSocket);
214 | });
215 |
216 | trojanRemoteSocketToWS(tcpSocket, webSocket, null, log);
217 | }
218 |
219 | const tcpSocket = await connectAndWrite(addressRemote, portRemote);
220 |
221 | // when remoteSocket is ready, pass to websocket
222 | // remote--> ws
223 | trojanRemoteSocketToWS(tcpSocket, webSocket, retry, log);
224 | }
225 |
226 | /**
227 | * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket.
228 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from.
229 | * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT.
230 | * @param {(info: string)=> void} log The logging function.
231 | * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket.
232 | */
233 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
234 | let readableStreamCancel = false;
235 | const stream = new ReadableStream({
236 | start(controller) {
237 | webSocketServer.addEventListener("message", (event) => {
238 | if (readableStreamCancel) {
239 | return;
240 | }
241 | const message = event.data;
242 | controller.enqueue(message);
243 | });
244 |
245 | // The event means that the client closed the client -> server stream.
246 | // However, the server -> client stream is still open until you call close() on the server side.
247 | // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.
248 | webSocketServer.addEventListener("close", () => {
249 | // client send close, need close server
250 | // if stream is cancel, skip controller.close
251 | safeCloseWebSocket(webSocketServer);
252 | if (readableStreamCancel) {
253 | return;
254 | }
255 | controller.close();
256 | });
257 | webSocketServer.addEventListener("error", (err) => {
258 | log("webSocketServer has error");
259 | controller.error(err);
260 | });
261 | // for ws 0rtt
262 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
263 | if (error) {
264 | controller.error(error);
265 | } else if (earlyData) {
266 | controller.enqueue(earlyData);
267 | }
268 | },
269 | pull(controller) {
270 | // if ws can stop read if stream is full, we can implement backpressure
271 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure
272 | },
273 | cancel(reason) {
274 | // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here
275 | // 2. if readableStream is cancel, all controller.close/enqueue need skip,
276 | // 3. but from testing controller.error still work even if readableStream is cancel
277 | if (readableStreamCancel) {
278 | return;
279 | }
280 | log(`ReadableStream was canceled, due to ${reason}`);
281 | readableStreamCancel = true;
282 | safeCloseWebSocket(webSocketServer);
283 | },
284 | });
285 |
286 | return stream;
287 | }
288 |
289 | async function trojanRemoteSocketToWS(remoteSocket, webSocket, retry, log) {
290 | let hasIncomingData = false;
291 | await remoteSocket.readable
292 | .pipeTo(
293 | new WritableStream({
294 | start() {},
295 | /**
296 | *
297 | * @param {Uint8Array} chunk
298 | * @param {*} controller
299 | */
300 | async write(chunk, controller) {
301 | hasIncomingData = true;
302 | if (webSocket.readyState !== WS_READY_STATE_OPEN) {
303 | controller.error("webSocket connection is not open");
304 | }
305 | webSocket.send(chunk);
306 | },
307 | close() {
308 | log(`remoteSocket.readable is closed, hasIncomingData: ${hasIncomingData}`);
309 | },
310 | abort(reason) {
311 | console.error("remoteSocket.readable abort", reason);
312 | },
313 | })
314 | )
315 | .catch((error) => {
316 | console.error(`trojanRemoteSocketToWS error:`, error.stack || error);
317 | safeCloseWebSocket(webSocket);
318 | });
319 |
320 | if (hasIncomingData === false && retry) {
321 | log(`retry`);
322 | retry();
323 | }
324 | }
325 |
326 | /**
327 | * Decodes a base64 string into an ArrayBuffer.
328 | * @param {string} base64Str The base64 string to decode.
329 | * @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.
330 | */
331 | function base64ToArrayBuffer(base64Str) {
332 | if (!base64Str) {
333 | return { earlyData: null, error: null };
334 | }
335 | try {
336 | // go use modified Base64 for URL rfc4648 which js atob not support
337 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
338 | const decode = atob(base64Str);
339 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
340 | return { earlyData: arryBuffer.buffer, error: null };
341 | } catch (error) {
342 | return { earlyData: null, error };
343 | }
344 | }
345 |
346 | const WS_READY_STATE_OPEN = 1;
347 | const WS_READY_STATE_CLOSING = 2;
348 | /**
349 | * Closes a WebSocket connection safely without throwing exceptions.
350 | * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close.
351 | */
352 | function safeCloseWebSocket(socket) {
353 | try {
354 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
355 | socket.close();
356 | }
357 | } catch (error) {
358 | console.error('safeCloseWebSocket error', error);
359 | }
360 | }
--------------------------------------------------------------------------------
/src/protocols/vless.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'cloudflare:sockets';
2 | import { isValidUUID } from '../helpers/helpers';
3 | import { initializeParams, userID, dohURL, proxyIP, pathName } from "../helpers/init";
4 |
5 | /**
6 | * Handles VLESS over WebSocket requests by creating a WebSocket pair, accepting the WebSocket connection, and processing the VLESS header.
7 | * @param {import("@cloudflare/workers-types").Request} request The incoming request object.
8 | * @returns {Promise} A Promise that resolves to a WebSocket response object.
9 | */
10 | export async function vlessOverWSHandler(request, env) {
11 | /** @type {import("@cloudflare/workers-types").WebSocket[]} */
12 | // @ts-ignore
13 | await initializeParams(request, env);
14 | const webSocketPair = new WebSocketPair();
15 | const [client, webSocket] = Object.values(webSocketPair);
16 |
17 | webSocket.accept();
18 |
19 | let address = "";
20 | let portWithRandomLog = "";
21 | const log = (/** @type {string} */ info, /** @type {string | undefined} */ event) => {
22 | console.log(`[${address}:${portWithRandomLog}] ${info}`, event || "");
23 | };
24 | const earlyDataHeader = request.headers.get("sec-websocket-protocol") || "";
25 |
26 | const readableWebSocketStream = makeReadableWebSocketStream(webSocket, earlyDataHeader, log);
27 |
28 | /** @type {{ value: import("@cloudflare/workers-types").Socket | null}}*/
29 | let remoteSocketWapper = {
30 | value: null,
31 | };
32 | let udpStreamWrite = null;
33 | let isDns = false;
34 |
35 | // ws --> remote
36 | readableWebSocketStream
37 | .pipeTo(
38 | new WritableStream({
39 | async write(chunk, controller) {
40 | if (isDns && udpStreamWrite) {
41 | return udpStreamWrite(chunk);
42 | }
43 | if (remoteSocketWapper.value) {
44 | const writer = remoteSocketWapper.value.writable.getWriter();
45 | await writer.write(chunk);
46 | writer.releaseLock();
47 | return;
48 | }
49 |
50 | const {
51 | hasError,
52 | message,
53 | portRemote = 443,
54 | addressRemote = "",
55 | rawDataIndex,
56 | vlessVersion = new Uint8Array([0, 0]),
57 | isUDP,
58 | } = await processVlessHeader(chunk, userID);
59 | address = addressRemote;
60 | portWithRandomLog = `${portRemote}--${Math.random()} ${isUDP ? "udp " : "tcp "} `;
61 | if (hasError) {
62 | // controller.error(message);
63 | throw new Error(message); // cf seems has bug, controller.error will not end stream
64 | // webSocket.close(1000, message);
65 | return;
66 | }
67 | // if UDP but port not DNS port, close it
68 | if (isUDP) {
69 | if (portRemote === 53) {
70 | isDns = true;
71 | } else {
72 | // controller.error('UDP proxy only enable for DNS which is port 53');
73 | throw new Error("UDP proxy only enable for DNS which is port 53"); // cf seems has bug, controller.error will not end stream
74 | return;
75 | }
76 | }
77 | // ["version", "附加信息长度 N"]
78 | const vlessResponseHeader = new Uint8Array([vlessVersion[0], 0]);
79 | const rawClientData = chunk.slice(rawDataIndex);
80 |
81 | // TODO: support udp here when cf runtime has udp support
82 | if (isDns) {
83 | const { write } = await handleUDPOutBound(webSocket, vlessResponseHeader, log);
84 | udpStreamWrite = write;
85 | udpStreamWrite(rawClientData);
86 | return;
87 | }
88 |
89 | handleTCPOutBound(
90 | request,
91 | remoteSocketWapper,
92 | addressRemote,
93 | portRemote,
94 | rawClientData,
95 | webSocket,
96 | vlessResponseHeader,
97 | log
98 | );
99 | },
100 | close() {
101 | log(`readableWebSocketStream is close`);
102 | },
103 | abort(reason) {
104 | log(`readableWebSocketStream is abort`, JSON.stringify(reason));
105 | },
106 | })
107 | )
108 | .catch((err) => {
109 | log("readableWebSocketStream pipeTo error", err);
110 | });
111 |
112 | return new Response(null, {
113 | status: 101,
114 | // @ts-ignore
115 | webSocket: client,
116 | });
117 | }
118 |
119 | /**
120 | * Checks if a given UUID is present in the API response.
121 | * @param {string} targetUuid The UUID to search for.
122 | * @returns {Promise} A Promise that resolves to true if the UUID is present in the API response, false otherwise.
123 | */
124 | async function checkUuidInApiResponse(targetUuid) {
125 | // Check if any of the environment variables are empty
126 |
127 | try {
128 | const apiResponse = await getApiResponse();
129 | if (!apiResponse) {
130 | return false;
131 | }
132 | const isUuidInResponse = apiResponse.users.some((user) => user.uuid === targetUuid);
133 | return isUuidInResponse;
134 | } catch (error) {
135 | console.error("Error:", error);
136 | return false;
137 | }
138 | }
139 |
140 | /**
141 | * Handles outbound TCP connections.
142 | *
143 | * @param {any} remoteSocket
144 | * @param {string} addressRemote The remote address to connect to.
145 | * @param {number} portRemote The remote port to connect to.
146 | * @param {Uint8Array} rawClientData The raw client data to write.
147 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to pass the remote socket to.
148 | * @param {Uint8Array} vlessResponseHeader The VLESS response header.
149 | * @param {function} log The logging function.
150 | * @returns {Promise} The remote socket.
151 | */
152 | async function handleTCPOutBound(
153 | request,
154 | remoteSocket,
155 | addressRemote,
156 | portRemote,
157 | rawClientData,
158 | webSocket,
159 | vlessResponseHeader,
160 | log
161 | ) {
162 | async function connectAndWrite(address, port) {
163 | if (/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(address)) address = `${atob('d3d3Lg==')}${address}${atob('LnNzbGlwLmlv')}`;
164 | /** @type {import("@cloudflare/workers-types").Socket} */
165 | const tcpSocket = connect({
166 | hostname: address,
167 | port: port,
168 | });
169 | remoteSocket.value = tcpSocket;
170 | log(`connected to ${address}:${port}`);
171 | const writer = tcpSocket.writable.getWriter();
172 | await writer.write(rawClientData); // first write, nomal is tls client hello
173 | writer.releaseLock();
174 | return tcpSocket;
175 | }
176 |
177 | // if the cf connect tcp socket have no incoming data, we retry to redirect ip
178 | async function retry() {
179 | const panelProxyIP = pathName.split('/')[2];
180 | const panelProxyIPs = panelProxyIP ? atob(panelProxyIP).split(',') : undefined;
181 | const finalProxyIP = panelProxyIPs ? panelProxyIPs[Math.floor(Math.random() * panelProxyIPs.length)] : proxyIP || addressRemote;
182 | const tcpSocket = await connectAndWrite(finalProxyIP, portRemote);
183 | // no matter retry success or not, close websocket
184 | tcpSocket.closed
185 | .catch((error) => {
186 | console.log("retry tcpSocket closed error", error);
187 | })
188 | .finally(() => {
189 | safeCloseWebSocket(webSocket);
190 | });
191 |
192 | vlessRemoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, null, log);
193 | }
194 |
195 | const tcpSocket = await connectAndWrite(addressRemote, portRemote);
196 |
197 | // when remoteSocket is ready, pass to websocket
198 | // remote--> ws
199 | vlessRemoteSocketToWS(tcpSocket, webSocket, vlessResponseHeader, retry, log);
200 | }
201 |
202 | /**
203 | * Creates a readable stream from a WebSocket server, allowing for data to be read from the WebSocket.
204 | * @param {import("@cloudflare/workers-types").WebSocket} webSocketServer The WebSocket server to create the readable stream from.
205 | * @param {string} earlyDataHeader The header containing early data for WebSocket 0-RTT.
206 | * @param {(info: string)=> void} log The logging function.
207 | * @returns {ReadableStream} A readable stream that can be used to read data from the WebSocket.
208 | */
209 | function makeReadableWebSocketStream(webSocketServer, earlyDataHeader, log) {
210 | let readableStreamCancel = false;
211 | const stream = new ReadableStream({
212 | start(controller) {
213 | webSocketServer.addEventListener("message", (event) => {
214 | if (readableStreamCancel) {
215 | return;
216 | }
217 | const message = event.data;
218 | controller.enqueue(message);
219 | });
220 |
221 | // The event means that the client closed the client -> server stream.
222 | // However, the server -> client stream is still open until you call close() on the server side.
223 | // The WebSocket protocol says that a separate close message must be sent in each direction to fully close the socket.
224 | webSocketServer.addEventListener("close", () => {
225 | // client send close, need close server
226 | // if stream is cancel, skip controller.close
227 | safeCloseWebSocket(webSocketServer);
228 | if (readableStreamCancel) {
229 | return;
230 | }
231 | controller.close();
232 | });
233 | webSocketServer.addEventListener("error", (err) => {
234 | log("webSocketServer has error");
235 | controller.error(err);
236 | });
237 | // for ws 0rtt
238 | const { earlyData, error } = base64ToArrayBuffer(earlyDataHeader);
239 | if (error) {
240 | controller.error(error);
241 | } else if (earlyData) {
242 | controller.enqueue(earlyData);
243 | }
244 | },
245 | pull(controller) {
246 | // if ws can stop read if stream is full, we can implement backpressure
247 | // https://streams.spec.whatwg.org/#example-rs-push-backpressure
248 | },
249 | cancel(reason) {
250 | // 1. pipe WritableStream has error, this cancel will called, so ws handle server close into here
251 | // 2. if readableStream is cancel, all controller.close/enqueue need skip,
252 | // 3. but from testing controller.error still work even if readableStream is cancel
253 | if (readableStreamCancel) {
254 | return;
255 | }
256 | log(`ReadableStream was canceled, due to ${reason}`);
257 | readableStreamCancel = true;
258 | safeCloseWebSocket(webSocketServer);
259 | },
260 | });
261 |
262 | return stream;
263 | }
264 |
265 | // https://xtls.github.io/development/protocols/vless.html
266 | // https://github.com/zizifn/excalidraw-backup/blob/main/v2ray-protocol.excalidraw
267 |
268 | /**
269 | * Processes the VLESS header buffer and returns an object with the relevant information.
270 | * @param {ArrayBuffer} vlessBuffer The VLESS header buffer to process.
271 | * @param {string} userID The user ID to validate against the UUID in the VLESS header.
272 | * @returns {{
273 | * hasError: boolean,
274 | * message?: string,
275 | * addressRemote?: string,
276 | * addressType?: number,
277 | * portRemote?: number,
278 | * rawDataIndex?: number,
279 | * vlessVersion?: Uint8Array,
280 | * isUDP?: boolean
281 | * }} An object with the relevant information extracted from the VLESS header buffer.
282 | */
283 | async function processVlessHeader(vlessBuffer, userID) {
284 | if (vlessBuffer.byteLength < 24) {
285 | return {
286 | hasError: true,
287 | message: "invalid data",
288 | };
289 | }
290 | const version = new Uint8Array(vlessBuffer.slice(0, 1));
291 | let isValidUser = false;
292 | let isUDP = false;
293 | const slicedBuffer = new Uint8Array(vlessBuffer.slice(1, 17));
294 | const slicedBufferString = stringify(slicedBuffer);
295 |
296 | const uuids = userID.includes(",") ? userID.split(",") : [userID];
297 |
298 | const checkUuidInApi = await checkUuidInApiResponse(slicedBufferString);
299 | isValidUser = uuids.some((userUuid) => checkUuidInApi || slicedBufferString === userUuid.trim());
300 |
301 | console.log(`checkUuidInApi: ${await checkUuidInApiResponse(slicedBufferString)}, userID: ${slicedBufferString}`);
302 |
303 | if (!isValidUser) {
304 | return {
305 | hasError: true,
306 | message: "invalid user",
307 | };
308 | }
309 |
310 | const optLength = new Uint8Array(vlessBuffer.slice(17, 18))[0];
311 | //skip opt for now
312 |
313 | const command = new Uint8Array(vlessBuffer.slice(18 + optLength, 18 + optLength + 1))[0];
314 |
315 | // 0x01 TCP
316 | // 0x02 UDP
317 | // 0x03 MUX
318 | if (command === 1) {
319 | } else if (command === 2) {
320 | isUDP = true;
321 | } else {
322 | return {
323 | hasError: true,
324 | message: `command ${command} is not support, command 01-tcp,02-udp,03-mux`,
325 | };
326 | }
327 | const portIndex = 18 + optLength + 1;
328 | const portBuffer = vlessBuffer.slice(portIndex, portIndex + 2);
329 | // port is big-Endian in raw data etc 80 == 0x005d
330 | const portRemote = new DataView(portBuffer).getUint16(0);
331 |
332 | let addressIndex = portIndex + 2;
333 | const addressBuffer = new Uint8Array(vlessBuffer.slice(addressIndex, addressIndex + 1));
334 |
335 | // 1--> ipv4 addressLength =4
336 | // 2--> domain name addressLength=addressBuffer[1]
337 | // 3--> ipv6 addressLength =16
338 | const addressType = addressBuffer[0];
339 | let addressLength = 0;
340 | let addressValueIndex = addressIndex + 1;
341 | let addressValue = "";
342 | switch (addressType) {
343 | case 1:
344 | addressLength = 4;
345 | addressValue = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength)).join(".");
346 | break;
347 | case 2:
348 | addressLength = new Uint8Array(vlessBuffer.slice(addressValueIndex, addressValueIndex + 1))[0];
349 | addressValueIndex += 1;
350 | addressValue = new TextDecoder().decode(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength));
351 | break;
352 | case 3:
353 | addressLength = 16;
354 | const dataView = new DataView(vlessBuffer.slice(addressValueIndex, addressValueIndex + addressLength));
355 | // 2001:0db8:85a3:0000:0000:8a2e:0370:7334
356 | const ipv6 = [];
357 | for (let i = 0; i < 8; i++) {
358 | ipv6.push(dataView.getUint16(i * 2).toString(16));
359 | }
360 | addressValue = ipv6.join(":");
361 | // seems no need add [] for ipv6
362 | break;
363 | default:
364 | return {
365 | hasError: true,
366 | message: `invild addressType is ${addressType}`,
367 | };
368 | }
369 | if (!addressValue) {
370 | return {
371 | hasError: true,
372 | message: `addressValue is empty, addressType is ${addressType}`,
373 | };
374 | }
375 |
376 | return {
377 | hasError: false,
378 | addressRemote: addressValue,
379 | addressType,
380 | portRemote,
381 | rawDataIndex: addressValueIndex + addressLength,
382 | vlessVersion: version,
383 | isUDP,
384 | };
385 | }
386 |
387 | /**
388 | * Converts a remote socket to a WebSocket connection.
389 | * @param {import("@cloudflare/workers-types").Socket} remoteSocket The remote socket to convert.
390 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket to connect to.
391 | * @param {ArrayBuffer | null} vlessResponseHeader The VLESS response header.
392 | * @param {(() => Promise) | null} retry The function to retry the connection if it fails.
393 | * @param {(info: string) => void} log The logging function.
394 | * @returns {Promise} A Promise that resolves when the conversion is complete.
395 | */
396 | async function vlessRemoteSocketToWS(remoteSocket, webSocket, vlessResponseHeader, retry, log) {
397 | // remote--> ws
398 | let remoteChunkCount = 0;
399 | let chunks = [];
400 | /** @type {ArrayBuffer | null} */
401 | let vlessHeader = vlessResponseHeader;
402 | let hasIncomingData = false; // check if remoteSocket has incoming data
403 | await remoteSocket.readable
404 | .pipeTo(
405 | new WritableStream({
406 | start() {},
407 | /**
408 | *
409 | * @param {Uint8Array} chunk
410 | * @param {*} controller
411 | */
412 | async write(chunk, controller) {
413 | hasIncomingData = true;
414 | // remoteChunkCount++;
415 | if (webSocket.readyState !== WS_READY_STATE_OPEN) {
416 | controller.error("webSocket.readyState is not open, maybe close");
417 | }
418 | if (vlessHeader) {
419 | webSocket.send(await new Blob([vlessHeader, chunk]).arrayBuffer());
420 | vlessHeader = null;
421 | } else {
422 | // seems no need rate limit this, CF seems fix this??..
423 | // if (remoteChunkCount > 20000) {
424 | // // cf one package is 4096 byte(4kb), 4096 * 20000 = 80M
425 | // await delay(1);
426 | // }
427 | webSocket.send(chunk);
428 | }
429 | },
430 | close() {
431 | log(`remoteConnection!.readable is close with hasIncomingData is ${hasIncomingData}`);
432 | // 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.
433 | },
434 | abort(reason) {
435 | console.error(`remoteConnection!.readable abort`, reason);
436 | },
437 | })
438 | )
439 | .catch((error) => {
440 | console.error(`vlessRemoteSocketToWS has exception `, error.stack || error);
441 | safeCloseWebSocket(webSocket);
442 | });
443 |
444 | // seems is cf connect socket have error,
445 | // 1. Socket.closed will have error
446 | // 2. Socket.readable will be close without any data coming
447 | if (hasIncomingData === false && retry) {
448 | log(`retry`);
449 | retry();
450 | }
451 | }
452 |
453 | /**
454 | * Decodes a base64 string into an ArrayBuffer.
455 | * @param {string} base64Str The base64 string to decode.
456 | * @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.
457 | */
458 | function base64ToArrayBuffer(base64Str) {
459 | if (!base64Str) {
460 | return { earlyData: null, error: null };
461 | }
462 | try {
463 | // go use modified Base64 for URL rfc4648 which js atob not support
464 | base64Str = base64Str.replace(/-/g, '+').replace(/_/g, '/');
465 | const decode = atob(base64Str);
466 | const arryBuffer = Uint8Array.from(decode, (c) => c.charCodeAt(0));
467 | return { earlyData: arryBuffer.buffer, error: null };
468 | } catch (error) {
469 | return { earlyData: null, error };
470 | }
471 | }
472 |
473 | const WS_READY_STATE_OPEN = 1;
474 | const WS_READY_STATE_CLOSING = 2;
475 | /**
476 | * Closes a WebSocket connection safely without throwing exceptions.
477 | * @param {import("@cloudflare/workers-types").WebSocket} socket The WebSocket connection to close.
478 | */
479 | function safeCloseWebSocket(socket) {
480 | try {
481 | if (socket.readyState === WS_READY_STATE_OPEN || socket.readyState === WS_READY_STATE_CLOSING) {
482 | socket.close();
483 | }
484 | } catch (error) {
485 | console.error('safeCloseWebSocket error', error);
486 | }
487 | }
488 |
489 | const byteToHex = [];
490 |
491 | for (let i = 0; i < 256; ++i) {
492 | byteToHex.push((i + 256).toString(16).slice(1));
493 | }
494 |
495 | function unsafeStringify(arr, offset = 0) {
496 | return (
497 | byteToHex[arr[offset + 0]] +
498 | byteToHex[arr[offset + 1]] +
499 | byteToHex[arr[offset + 2]] +
500 | byteToHex[arr[offset + 3]] +
501 | "-" +
502 | byteToHex[arr[offset + 4]] +
503 | byteToHex[arr[offset + 5]] +
504 | "-" +
505 | byteToHex[arr[offset + 6]] +
506 | byteToHex[arr[offset + 7]] +
507 | "-" +
508 | byteToHex[arr[offset + 8]] +
509 | byteToHex[arr[offset + 9]] +
510 | "-" +
511 | byteToHex[arr[offset + 10]] +
512 | byteToHex[arr[offset + 11]] +
513 | byteToHex[arr[offset + 12]] +
514 | byteToHex[arr[offset + 13]] +
515 | byteToHex[arr[offset + 14]] +
516 | byteToHex[arr[offset + 15]]
517 | ).toLowerCase();
518 | }
519 |
520 | function stringify(arr, offset = 0) {
521 | const uuid = unsafeStringify(arr, offset);
522 | if (!isValidUUID(uuid)) {
523 | throw TypeError("Stringified UUID is invalid");
524 | }
525 | return uuid;
526 | }
527 |
528 | /**
529 | * Handles outbound UDP traffic by transforming the data into DNS queries and sending them over a WebSocket connection.
530 | * @param {import("@cloudflare/workers-types").WebSocket} webSocket The WebSocket connection to send the DNS queries over.
531 | * @param {ArrayBuffer} vlessResponseHeader The VLESS response header.
532 | * @param {(string) => void} log The logging function.
533 | * @returns {{write: (chunk: Uint8Array) => void}} An object with a write method that accepts a Uint8Array chunk to write to the transform stream.
534 | */
535 | async function handleUDPOutBound(webSocket, vlessResponseHeader, log) {
536 | let isVlessHeaderSent = false;
537 | const transformStream = new TransformStream({
538 | start(controller) {},
539 | transform(chunk, controller) {
540 | // udp message 2 byte is the the length of udp data
541 | // TODO: this should have bug, beacsue maybe udp chunk can be in two websocket message
542 | for (let index = 0; index < chunk.byteLength; ) {
543 | const lengthBuffer = chunk.slice(index, index + 2);
544 | const udpPakcetLength = new DataView(lengthBuffer).getUint16(0);
545 | const udpData = new Uint8Array(chunk.slice(index + 2, index + 2 + udpPakcetLength));
546 | index = index + 2 + udpPakcetLength;
547 | controller.enqueue(udpData);
548 | }
549 | },
550 | flush(controller) {},
551 | });
552 |
553 | // only handle dns udp for now
554 | transformStream.readable
555 | .pipeTo(
556 | new WritableStream({
557 | async write(chunk) {
558 | const resp = await fetch(
559 | dohURL, // dns server url
560 | {
561 | method: "POST",
562 | headers: {
563 | "content-type": "application/dns-message",
564 | },
565 | body: chunk,
566 | }
567 | );
568 | const dnsQueryResult = await resp.arrayBuffer();
569 | const udpSize = dnsQueryResult.byteLength;
570 | // console.log([...new Uint8Array(dnsQueryResult)].map((x) => x.toString(16)));
571 | const udpSizeBuffer = new Uint8Array([(udpSize >> 8) & 0xff, udpSize & 0xff]);
572 | if (webSocket.readyState === WS_READY_STATE_OPEN) {
573 | log(`doh success and dns message length is ${udpSize}`);
574 | if (isVlessHeaderSent) {
575 | webSocket.send(await new Blob([udpSizeBuffer, dnsQueryResult]).arrayBuffer());
576 | } else {
577 | webSocket.send(await new Blob([vlessResponseHeader, udpSizeBuffer, dnsQueryResult]).arrayBuffer());
578 | isVlessHeaderSent = true;
579 | }
580 | }
581 | },
582 | })
583 | )
584 | .catch((error) => {
585 | log("dns udp has error" + error);
586 | });
587 |
588 | const writer = transformStream.writable.getWriter();
589 |
590 | return {
591 | /**
592 | *
593 | * @param {Uint8Array} chunk
594 | */
595 | write(chunk) {
596 | writer.write(chunk);
597 | },
598 | };
599 | }
--------------------------------------------------------------------------------
/src/protocols/warp.js:
--------------------------------------------------------------------------------
1 | import nacl from 'tweetnacl';
2 |
3 | export async function fetchWarpConfigs (env, proxySettings) {
4 | let warpConfigs = [];
5 | const apiBaseUrl = 'https://api.cloudflareclient.com/v0a4005/reg';
6 | const { warpPlusLicense } = proxySettings;
7 | const warpKeys = [ generateKeyPair(), generateKeyPair() ];
8 | const commonPayload = {
9 | install_id: "",
10 | fcm_token: "",
11 | tos: new Date().toISOString(),
12 | type: "Android",
13 | model: 'PC',
14 | locale: 'en_US',
15 | warp_enabled: true
16 | };
17 |
18 | const fetchAccount = async (key) => {
19 | const response = await fetch(apiBaseUrl, {
20 | method: 'POST',
21 | headers: {
22 | 'User-Agent': 'insomnia/8.6.1',
23 | 'Content-Type': 'application/json'
24 | },
25 | body: JSON.stringify({ ...commonPayload, key: key.publicKey })
26 | });
27 | return await response.json();
28 | };
29 |
30 | const updateAccount = async (accountData, key) => {
31 | const response = await fetch(`${apiBaseUrl}/${accountData.id}/account`, {
32 | method: 'PUT',
33 | headers: {
34 | 'User-Agent': 'insomnia/8.6.1',
35 | 'Content-Type': 'application/json',
36 | 'Authorization': `Bearer ${accountData.token}`
37 | },
38 | body: JSON.stringify({ ...commonPayload, key: key.publicKey, license: warpPlusLicense })
39 | });
40 | return {
41 | status: response.status,
42 | data: await response.json()
43 | };
44 | };
45 |
46 | for (const key of warpKeys) {
47 | const accountData = await fetchAccount(key);
48 | warpConfigs.push({
49 | privateKey: key.privateKey,
50 | account: accountData
51 | });
52 |
53 | if (warpPlusLicense) {
54 | const { status, data: responseData } = await updateAccount(accountData, key);
55 | if (status !== 200 && !responseData.success) {
56 | return { error: responseData.errors[0]?.message, configs: null };
57 | }
58 | }
59 | }
60 |
61 | const configs = JSON.stringify(warpConfigs)
62 | await env.bpb.put('warpConfigs', configs);
63 | return { error: null, configs };
64 | }
65 |
66 | const generateKeyPair = () => {
67 | const base64Encode = (array) => btoa(String.fromCharCode.apply(null, array));
68 | let privateKey = nacl.randomBytes(32);
69 | privateKey[0] &= 248;
70 | privateKey[31] &= 127;
71 | privateKey[31] |= 64;
72 | let publicKey = nacl.scalarMult.base(privateKey);
73 | const publicKeyBase64 = base64Encode(publicKey);
74 | const privateKeyBase64 = base64Encode(privateKey);
75 | return { publicKey: publicKeyBase64, privateKey: privateKeyBase64 };
76 | };
--------------------------------------------------------------------------------
/src/worker.js:
--------------------------------------------------------------------------------
1 | // https://github.com/bia-pain-bache/BPB-Worker-Panel
2 | import { vlessOverWSHandler } from './protocols/vless';
3 | import { trojanOverWSHandler } from './protocols/trojan';
4 | import { updateWarpConfigs } from './kv/handlers';
5 | import { logout, resetPassword, login } from './authentication/auth';
6 | import { renderErrorPage } from './pages/error';
7 | import { getXrayCustomConfigs, getXrayWarpConfigs } from './cores-configs/xray';
8 | import { getSingBoxCustomConfig, getSingBoxWarpConfig } from './cores-configs/sing-box';
9 | import { getClashNormalConfig, getClashWarpConfig } from './cores-configs/clash';
10 | import { getNormalConfigs } from './cores-configs/normalConfigs';
11 | import { initializeParams, userID, client, pathName } from './helpers/init';
12 | import { fallback, getMyIP, handlePanel } from './helpers/helpers';
13 |
14 | export default {
15 | async fetch(request, env) {
16 | try {
17 | const upgradeHeader = request.headers.get('Upgrade');
18 | await initializeParams(request, env);
19 | if (!upgradeHeader || upgradeHeader !== 'websocket') {
20 | switch (pathName) {
21 | case '/update-warp':
22 | return await updateWarpConfigs(request, env);
23 |
24 | case `/sub/${userID}`:
25 | if (client === 'sfa') return await getSingBoxCustomConfig(request, env, false);
26 | if (client === 'clash') return await getClashNormalConfig(request, env);
27 | if (client === 'xray') return await getXrayCustomConfigs(request, env, false);
28 | return await getNormalConfigs(request, env);
29 |
30 | case `/fragsub/${userID}`:
31 | return client === 'hiddify'
32 | ? await getSingBoxCustomConfig(request, env, true)
33 | : await getXrayCustomConfigs(request, env, true);
34 |
35 | case `/warpsub/${userID}`:
36 | if (client === 'clash') return await getClashWarpConfig(request, env);
37 | if (client === 'singbox' || client === 'hiddify') return await getSingBoxWarpConfig(request, env, client);
38 | return await getXrayWarpConfigs(request, env, client);
39 |
40 | case '/panel':
41 | return await handlePanel(request, env);
42 |
43 | case '/login':
44 | return await login(request, env);
45 |
46 | case '/logout':
47 | return logout();
48 |
49 | case '/panel/password':
50 | return await resetPassword(request, env);
51 |
52 | case '/my-ip':
53 | return await getMyIP(request);
54 |
55 | default:
56 | return await fallback(request);
57 | }
58 | } else {
59 | return pathName.startsWith('/tr')
60 | ? await trojanOverWSHandler(request, env)
61 | : await vlessOverWSHandler(request, env);
62 | }
63 | } catch (err) {
64 | return await renderErrorPage(request, env, 'Something went wrong!', err, false);
65 | }
66 | }
67 | };
--------------------------------------------------------------------------------