├── tgbot.py ├── LICENSE ├── README.md └── reality-ezpz.sh /tgbot.py: -------------------------------------------------------------------------------- 1 | import os 2 | from telegram import InlineKeyboardButton, InlineKeyboardMarkup 3 | from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, MessageHandler, Filters, ConversationHandler 4 | import re 5 | import subprocess 6 | import io 7 | import qrcode 8 | 9 | token = os.environ['BOT_TOKEN'] 10 | admin = os.environ['BOT_ADMIN'] 11 | updater = Updater(token) 12 | username_regex = re.compile("^[a-zA-Z0-9]+$") 13 | command = 'bash <(curl -sL https://raw.githubusercontent.com/aleskxyz/reality-ezpz/master/reality-ezpz.sh) ' 14 | def get_users_ezpz(): 15 | local_command = command + '--list-users' 16 | return run_command(local_command).split('\n')[:-1] 17 | def get_config_ezpz(username): 18 | local_command = command + f"--show-user {username} | grep -E '://|^\\{{\"dns\"'" 19 | return run_command(local_command).split('\n')[:-1] 20 | def delete_user_ezpz(username): 21 | local_command = command + f'--delete-user {username}' 22 | run_command(local_command) 23 | return 24 | def add_user_ezpz(username): 25 | local_command = command + f'--add-user {username}' 26 | run_command(local_command) 27 | return 28 | 29 | def run_command(command): 30 | process = subprocess.Popen(['/bin/bash', '-c', command], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 31 | output, _ = process.communicate() 32 | return output.decode() 33 | 34 | def restricted(func): 35 | def wrapped(update, context, *args, **kwargs): 36 | username = None 37 | if update.message: 38 | username = update.message.chat.username 39 | elif update.callback_query and update.callback_query.message: 40 | username = update.callback_query.message.chat.username 41 | admin_list = admin.split(',') 42 | if username in admin_list: 43 | return func(update, context, *args, **kwargs) 44 | else: 45 | context.bot.send_message(chat_id=update.effective_chat.id, text='You are not authorized to use this bot.') 46 | return wrapped 47 | 48 | @restricted 49 | def start(update, context): 50 | commands_text = "Reality-EZPZ User Management Bot\n\nChoose an option:" 51 | keyboard = [ 52 | [InlineKeyboardButton('Show User', callback_data='show_user')], 53 | [InlineKeyboardButton('Add User', callback_data='add_user')], 54 | [InlineKeyboardButton('Delete User', callback_data='delete_user')], 55 | ] 56 | reply_markup = InlineKeyboardMarkup(keyboard) 57 | context.bot.send_message(chat_id=update.effective_chat.id, text=commands_text, reply_markup=reply_markup) 58 | 59 | @restricted 60 | def users_list(update, context, text, callback): 61 | keyboard = [] 62 | for user in get_users_ezpz(): 63 | keyboard.append([InlineKeyboardButton(user, callback_data=f'{callback}!{user}')]) 64 | keyboard.append([InlineKeyboardButton('Back', callback_data='start')]) 65 | reply_markup = InlineKeyboardMarkup(keyboard) 66 | context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup) 67 | 68 | @restricted 69 | def show_user(update, context, username): 70 | keyboard = [[InlineKeyboardButton('Back', callback_data='show_user')]] 71 | reply_markup = InlineKeyboardMarkup(keyboard) 72 | context.bot.send_message(chat_id=update.effective_chat.id, text=f'Config for "{username}":', parse_mode='HTML') 73 | config_list = get_config_ezpz(username) 74 | ipv6_pattern = r'"server":"[0-9a-fA-F:]+"' 75 | 76 | for config in config_list: 77 | if config.endswith("-ipv6") or re.search(ipv6_pattern, config): 78 | config_text = f"IPv6 Config:\n
{config}
" 79 | else: 80 | config_text = f"
{config}
" 81 | 82 | qr_img = qrcode.make(config) 83 | bio = io.BytesIO() 84 | qr_img.save(bio, 'PNG') 85 | bio.seek(0) 86 | 87 | context.bot.send_photo(chat_id=update.effective_chat.id, photo=bio, caption=config_text, parse_mode='HTML', reply_markup=reply_markup) 88 | 89 | @restricted 90 | def delete_user(update, context, username): 91 | keyboard = [] 92 | if len(get_users_ezpz()) == 1: 93 | text = 'You cannot delete the only user.\nAt least one user is needed.\nCreate a new user, then delete this one.' 94 | keyboard.append([InlineKeyboardButton('Back', callback_data='start')]) 95 | reply_markup = InlineKeyboardMarkup(keyboard) 96 | context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup) 97 | return 98 | text = f'Are you sure to delete "{username}"?' 99 | keyboard.append([InlineKeyboardButton('Delete', callback_data=f'approve_delete!{username}')]) 100 | keyboard.append([InlineKeyboardButton('Cancel', callback_data='delete_user')]) 101 | reply_markup = InlineKeyboardMarkup(keyboard) 102 | context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup) 103 | 104 | @restricted 105 | def add_user(update, context): 106 | text = 'Enter the username:' 107 | keyboard = [] 108 | keyboard.append([InlineKeyboardButton('Cancel', callback_data='cancel')]) 109 | reply_markup = InlineKeyboardMarkup(keyboard) 110 | context.user_data['expected_input'] = 'username' 111 | context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup) 112 | 113 | @restricted 114 | def approve_delete(update, context, username): 115 | delete_user_ezpz(username) 116 | text = f'User {username} has been deleted.' 117 | keyboard = [] 118 | keyboard.append([InlineKeyboardButton('Back', callback_data='start')]) 119 | reply_markup = InlineKeyboardMarkup(keyboard) 120 | context.bot.send_message(chat_id=update.effective_chat.id, text=text, reply_markup=reply_markup) 121 | 122 | @restricted 123 | def cancel(update, context): 124 | if 'expected_input' in context.user_data: 125 | del context.user_data['expected_input'] 126 | start(update, context) 127 | 128 | @restricted 129 | def button(update, context): 130 | query = update.callback_query 131 | query.answer() 132 | response = query.data.split('!') 133 | if len(response) == 1: 134 | if response[0] == 'start': 135 | start(update, context) 136 | elif response[0] == 'cancel': 137 | cancel(update, context) 138 | elif response[0] == 'show_user': 139 | users_list(update, context, 'Select user to view config:', 'show_user') 140 | elif response[0] == 'delete_user': 141 | users_list(update, context, 'Select user to delete:', 'delete_user') 142 | elif response[0] == 'add_user': 143 | add_user(update, context) 144 | else: 145 | context.bot.send_message(chat_id=update.effective_chat.id, text='Button pressed: {}'.format(response[0])) 146 | if len(response) > 1: 147 | if response[0] == 'show_user': 148 | show_user(update, context, response[1]) 149 | if response[0] == 'delete_user': 150 | delete_user(update, context, response[1]) 151 | if response[0] == 'approve_delete': 152 | approve_delete(update, context, response[1]) 153 | 154 | @restricted 155 | def user_input(update, context): 156 | if 'expected_input' in context.user_data: 157 | expected_input = context.user_data['expected_input'] 158 | del context.user_data['expected_input'] 159 | if expected_input == 'username': 160 | username = update.message.text 161 | if username in get_users_ezpz(): 162 | update.message.reply_text(f'User "{username}" exists, try another username.') 163 | add_user(update, context) 164 | return 165 | if not username_regex.match(username): 166 | update.message.reply_text('Username can only contains A-Z, a-z and 0-9, try another username.') 167 | add_user(update, context) 168 | return 169 | add_user_ezpz(username) 170 | update.message.reply_text(f'User "{username}" is created.') 171 | show_user(update, context, username) 172 | 173 | start_handler = CommandHandler('start', start) 174 | button_handler = CallbackQueryHandler(button) 175 | 176 | updater.dispatcher.add_handler(start_handler) 177 | updater.dispatcher.add_handler(button_handler) 178 | updater.dispatcher.add_handler(MessageHandler(Filters.text & ~Filters.command, user_input)) 179 | 180 | updater.start_polling() 181 | updater.idle() 182 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reality-ezpz 2 | Install and configure vless with reality or TLS on your linux server by executing a single command! 3 | 4 | TUIC and hysteria2 on sing-box is also supported! 5 | 6 | This script: 7 | * Installs docker with compose plugin in your server 8 | * Generates docker-compose.yml and sing-box/xray configuration for vless protocol for reality and tls 9 | * Generates docker-compose.yml and sing-box configuration for TUIC protocol with tls 10 | * Generates docker-compose.yml and sing-box configuration for hysteria2 protocol with tls 11 | * Create Cloudflare warp account and configure warp as outbound 12 | * Generates client configuration string and QRcode 13 | * Gets and renews valid certificate from Letsencrypt for TLS encryption 14 | * Fine-tunes kernel tunables 15 | * Is designed by taking security considerations into account to make the server undetectable 16 | * Provides a Telegram bot to manage users from Telegram 17 | 18 | Features: 19 | * Generates client configuration string 20 | * Generates client configuration QRcode 21 | * You can choose between xray or sing-box core 22 | * You can choose between reality or TLS security protocol 23 | * You can use a Text-based user interface (TUI) 24 | * You can create multiple user accounts 25 | * You can regenerate configuration and keys 26 | * You can change SNI domain 27 | * You can change transport protocol (tcp, http, grpc, ws) 28 | * You can change tunneling protocol (vless, TUIC, hysteria2, shadowtls) 29 | * You can get valid TLS certificate with Letsencrypt 30 | * You can block malware and adult contents 31 | * Merges your custom advanced configuration 32 | * Use Cloudflare WARP to hide your outbound traffic 33 | * Supports Cloudflare warp+ 34 | * Install with a single command 35 | * Telegram bot for user management 36 | * Create backup from users and configuration 37 | * Restore users and configuration from backup 38 | 39 | Supported OS: 40 | * Ubuntu 22.04 41 | * Ubuntu 20.04 42 | * Ubuntu 18.04 43 | * Debian 11 44 | * Debian 10 45 | * CentOS Stream 9 46 | * CentOS Stream 8 47 | * CentOS 7 48 | * Fedora 37 49 | 50 | ## Quick Start 51 | You can start using this script with default configuration by copy and paste the line below in terminal. 52 | 53 | This command will configure `sing-box` with `reality` security protocol over `tcp` transport protocol on port `443` for `www.google.com` SNI domain by default: 54 | ``` 55 | bash <(curl -sL https://bit.ly/realityez) 56 | ``` 57 | or (if the above command dosen't work): 58 | ``` 59 | bash <(curl -sL https://raw.githubusercontent.com/aleskxyz/reality-ezpz/master/reality-ezpz.sh) 60 | ``` 61 | After a while you will get configuration string and QR code: 62 | ![image](https://user-images.githubusercontent.com/39186039/232563871-0140e10a-22b4-4653-9bc9-cdba519a8b41.png) 63 | 64 | You can run TUI with `-m` or `--menu` option: 65 | ``` 66 | bash <(curl -sL https://bit.ly/realityez) -m 67 | ``` 68 | And then you will see management menu in your terminal: 69 | ![image](https://github.com/aleskxyz/reality-ezpz/assets/39186039/a727148c-1a11-4702-80f3-ab8b46d916af) 70 | 71 | You can also enable Telegram bot with `--enable-tgbot` option and manage users from with your Telegram bot ([More Info](#telegram-bot)) 72 |

73 | 74 |

75 | 76 | Help message of the script: 77 | ``` 78 | 79 | Usage: reality-ezpz.sh [-t|--transport=tcp|http|grpc|ws|tuic|hysteria2|shadowtls] [-d|--domain=] [--server=] 80 | [--regenerate] [--default] [-r|--restart] [--enable-safenet=true|false] [--port=] [-c|--core=xray|sing-box] 81 | [--enable-warp=true|false] [--warp-license=] [--security=reality|letsencrypt|selfsigned] [-m|--menu] 82 | [--show-server-config] [--add-user=] [--lists-users] [--show-user=] [--delete-user=] 83 | [--backup] [--restore=] [--backup-password=] [-u|--uninstall] 84 | 85 | -t, --transport Transport protocol (tcp, http, grpc, ws, tuic, hysteria2, shadowtls default: tcp) 86 | -d, --domain Domain to use as SNI (default: www.google.com) 87 | --server IP address or domain name of server (Must be a valid domain if using letsencrypt security) 88 | --regenerate Regenerate public and private keys 89 | --default Restore default configuration 90 | -r --restart Restart services 91 | -u, --uninstall Uninstall reality 92 | --enable-safenet Enable or disable safenet (blocking malware and adult content) 93 | --port Server port (default: 443) 94 | --enable-warp Enable or disable Cloudflare warp 95 | --warp-license Add Cloudflare warp+ license 96 | -c --core Select core (xray, sing-box, default: sing-box) 97 | --security Select type of TLS encryption (reality, letsencrypt, selfsigned, default: reality) 98 | -m --menu Show menu 99 | --enable-tgbot Enable Telegram bot for user management 100 | --tgbot-token Token of Telegram bot 101 | --tgbot-admins Usernames of telegram bot admins (Comma separated list of usernames without leading '@') 102 | --show-server-config Print server configuration 103 | --add-user Add new user 104 | --list-users List all users 105 | --show-user Shows the config and QR code of the user 106 | --delete-user Delete the user 107 | --backup Backup users and configuration and upload it to temp.sh 108 | --restore Restore backup from URL or file 109 | --backup-password Create/Restore password protected backup file 110 | -h, --help Display this help message 111 | ``` 112 | 113 | ## Clients 114 | - Android 115 | - [v2rayNG](https://github.com/2dust/v2rayNg/releases) 116 | - [NekoBox](https://github.com/MatsuriDayo/NekoBoxForAndroid/releases) 117 | - iOS 118 | - [FoXray](https://apps.apple.com/app/foxray/id6448898396) 119 | - [Shadowrocket](https://apps.apple.com/app/shadowrocket/id932747118) 120 | - [Stash](https://apps.apple.com/app/stash/id1596063349) 121 | - Windows 122 | - [v2rayN](https://github.com/2dust/v2rayN/releases) 123 | - Windows, Linux, macOS 124 | - [NekoRay](https://github.com/MatsuriDayo/nekoray/releases) 125 | - [Furious](https://github.com/LorenEteval/Furious/releases) 126 | 127 | ## Security Options 128 | This script can configure the service with 3 types of security options: 129 | - reality 130 | - letsencrypt 131 | - selfsigned 132 | 133 | By default `reality` is configured but you can change the security protocol with `--security` option. 134 | 135 | The `letsencrypt` option will use Letsencrypt to get a valid certificate for you server. So you have to assign a valid domain or subdomain to your server with `--server ` option. 136 | 137 | The `selfsigned` option is same as `letsencrypt` but the certificates are self-signed and you don't need to assign a domain or subdomain to your server. 138 | 139 | ## Compatibility and recommendation 140 | CDN compatibility table: 141 | 142 | | | Cloudflare | ArvanCloud | 143 | | ------------ | ------------ | ------------ | 144 | | reality | :x: | :x: | 145 | | selfsigned | :heavy_check_mark: | :heavy_check_mark: | 146 | | letsencrypt | :heavy_check_mark: | :heavy_check_mark: | 147 | | tcp | :x: | :x: | 148 | | http | :x: | :heavy_check_mark: | 149 | | grpc | :heavy_check_mark: | :heavy_check_mark: | 150 | | ws | :heavy_check_mark: | :heavy_check_mark: | 151 | | tuic | :x: | :x: | 152 | | hysteria2 | :x: | :x: | 153 | | shadowtls | :x: | :x: | 154 | 155 | - You need to enable `grpc` or `websocket` in Cloudflare if you want to use the corresponding transport protocols. 156 | - You have to configure CDN provider to use HTTPS for connecting to your server. 157 | - The `ws` transport protocol is not compatible with `reality` security option. 158 | - The `tuic` tunneling protocol is not compatible with `reality` security option. 159 | - The `tuic` tunneling protocol is only compatible with `sing-box` core option. 160 | - The `hysteria2` tunneling protocol is not compatible with `reality` security option. 161 | - The `hysteria2` tunneling protocol is only compatible with `sing-box` core option. 162 | - The `shadowtls` tunneling protocol is only compatible with `sing-box` core option. 163 | - Avoid using `tcp` transport protocol with `letsencrypt` or `selfsigned` security options. 164 | - Avoid using `selfsigned` security option. Get a domain and use `letsencrypt` option. 165 | - Do not change the port to something other than `443`. 166 | - The `sing-box` core has better performance. 167 | - Using [NekoBox](https://github.com/MatsuriDayo/NekoBoxForAndroid/releases) for Android is recommended. 168 | 169 | ## User Management 170 | You can add, view and delete multiple user account with this script easily! 171 | 172 | ### Add User 173 | You can add additional user by using `--add-user` option: 174 | ``` 175 | bash <(curl -sL https://bit.ly/realityez) --add-user user1 176 | ``` 177 | This command will create `test1` as a new user. 178 | 179 | Notice: Username can only contains A-Z, a-z and 0-9 180 | 181 | ### List Users 182 | You can view a list of all users by using `--list-users` option: 183 | ``` 184 | bash <(curl -sL https://bit.ly/realityez) --list-users 185 | ``` 186 | 187 | ### Show User Configuration 188 | You can get config string and QR code of the user for importing by using `--show-user` option: 189 | ``` 190 | bash <(curl -sL https://bit.ly/realityez) --show-user user1 191 | ``` 192 | This command will print config string and QR code of `user1` 193 | 194 | ### Delete User 195 | You can delete a user by using `--delete-user` option: 196 | ``` 197 | bash <(curl -sL https://bit.ly/realityez) --delete-user user1 198 | ``` 199 | This command will delete `user1` 200 | 201 | ## Advanced Configuration 202 | You can change script defaults by using different arguments. 203 | 204 | Your configuration will be saved and restored in each execution. So You can run the script multiple time with out any problem. 205 | 206 | ### Change SNI domain 207 | Reality protocol will use the public certificate of SNI domain. 208 | 209 | Default SNI domain is `www.google.com`. 210 | 211 | You can change it by using `--domain` or `-d` options: 212 | ``` 213 | bash <(curl -sL https://bit.ly/realityez) -d yahoo.com 214 | ``` 215 | ### Change transport protocol 216 | Default transport protocol is `tcp`. 217 | 218 | You can change it by using `--transport` or `-t` options: 219 | ``` 220 | bash <(curl -sL https://bit.ly/realityez) -t http 221 | ``` 222 | Valid options are `tcp`,`http`, `grpc`, `ws`, `tuic`, `hysteria2` and `shadowtls`. 223 | 224 | `ws` is not compatible with reality protocol. You have to use `letsencrypt` or `selfsigned` with it. 225 | 226 | `tuic` is not compatible with reality protocol. You have to use `letsencrypt` or `selfsigned` with it. 227 | 228 | `tuic` is compatible with sing-box core only. 229 | 230 | `hysteria2` is not compatible with reality protocol. You have to use `letsencrypt` or `selfsigned` with it. 231 | 232 | `hysteria2` is compatible with sing-box core only. 233 | 234 | `shadowtls` is compatible with sing-box core only. 235 | 236 | #### ShadowTLS 237 | ShadowTLS is a TLS disguise proxy that can use someone else's trusted certificate. It is similar to "Reality," but in transport, it uses Shadowsocks. So you need to assign a working SNI to it. 238 | 239 | When you enable ShadowTLS, you will configure two proxies: ShadowTLS and Shadowsocks. 240 | 241 | You need to configure your client to use both proxies in chain mode. 242 | 243 | First, ShadowTLS will establish a secure connection with the server, then Shadowsocks will use the connection created by ShadowTLS. 244 | 245 | ### Block malware and adult contents 246 | You can block malware and adult contents by using `--enable-safenet` option: 247 | ``` 248 | bash <(curl -sL https://bit.ly/realityez) --enable-safenet true 249 | ``` 250 | You can disable this feature with `--enable-safenet false` option. 251 | 252 | ### Get runnig configuration 253 | You can get the running configuration with `--show-server-config` option: 254 | ``` 255 | bash <(curl -sL https://bit.ly/realityez) --show-server-config 256 | ``` 257 | 258 | ### Regenerate configuration keys 259 | You can regenerate keys by using `--regenerate` option: 260 | ``` 261 | bash <(curl -sL https://bit.ly/realityez) --regenerate 262 | ``` 263 | All other configuration will be same as before. 264 | 265 | ### Restart services 266 | You can restart the service by using `-r` or `--restart` options: 267 | ``` 268 | bash <(curl -sL https://bit.ly/realityez) -r 269 | ``` 270 | 271 | ### Restore default configuration 272 | You can restore default configuration by using `--default` option. 273 | ``` 274 | bash <(curl -sL https://bit.ly/realityez) --default 275 | ``` 276 | User account will not change with this option. 277 | 278 | ### Uninstall 279 | You can delete configuration and services by using `--uninstall` or `-u` options: 280 | ``` 281 | bash <(curl -sL https://bit.ly/realityez) -u 282 | ``` 283 | 284 | ### Change port 285 | Notice: Do not change default port. This may block your IP! 286 | 287 | Default port is `443`. 288 | 289 | In case of using `letsencrypt` security option, port `80` has to be available for Letsencrypt challenge. 290 | 291 | You can change it by using `--port` option: 292 | ``` 293 | bash <(curl -sL https://bit.ly/realityez) --port 8443 294 | ``` 295 | 296 | ### Change engine core 297 | Default engine core is sing-box but you can also switch to xray by using `--core` or `-c` options: 298 | ``` 299 | bash <(curl -sL https://bit.ly/realityez) -c xray 300 | ``` 301 | Valid options are `xray` and `sing-box`. 302 | 303 | ### Create backup 304 | You can create a backup from users and configuration and upload it to https://temp.sh/ by using `--backup` option. 305 | 306 | The `--backup-password` option allows you to protect the backup zip file with the specified password. (Optional) 307 | ``` 308 | bash <(curl -sL https://bit.ly/realityez) --backup --backup-password "P@ssw0rd" 309 | ``` 310 | This command will give you a URL to download you backup file. The URL is only valid for 3 days. 311 | 312 | ### Restore backup 313 | You can restore a previously created backup file with `--restore` option. 314 | 315 | You need to give the path or URL of the backup file to restore. 316 | 317 | The `--backup-password` option allows you to restore the password protected backup zip file. 318 | ``` 319 | bash <(curl -sL https://bit.ly/realityez) --restore /path/to/backup.zip --backup-password "P@ssw0rd" 320 | ``` 321 | or 322 | ``` 323 | bash <(curl -sL https://bit.ly/realityez) --restore "https://www.example.com/backup.zip" --backup-password "P@ssw0rd" 324 | ``` 325 | 326 | You can migrate users and configuration from one server to another by: 327 | 328 | 1. Create backup in the old server and copy the URL of backup file 329 | 1. Restore the URL of backup file in the new server 330 | 331 | ### Text-based user interface (TUI) 332 | You can also use the TUI for changing the configuration of the service. 333 | 334 | To access to TUI you can use `-m` or `--menu` options: 335 | ``` 336 | bash <(curl -sL https://bit.ly/realityez) -m 337 | ``` 338 | 339 | ## Telegram Bot 340 | You can manage users with Telegram Bot. 341 | 342 | You should get a Telegram bot token from `@BotFather` Telegram account. 343 | 344 | Then you can enable Telegram bot by using this command as an example: 345 | ``` 346 | bash <(curl -sL https://bit.ly/realityez) --enable-tgbot true --tgbot-token --tgbot-admins= 347 | ``` 348 | In the command above you have to provide a comma separated list of Telegram usernames (without leading '@') which are authorized to use Telegram bot. 349 | 350 | You can disable Telegram bot with this command: 351 | ``` 352 | bash <(curl -sL https://bit.ly/realityez) --enable-tgbot false 353 | ``` 354 | 355 | ## Cloudflare WARP 356 | This script uses official Cloudflare WARP client for connecting to Cloudflare network and send all outbound traffic to Cloudflare server. So your servers address will be masked by Cloudflare IPs. This gives you a better web surffing experience due to less captcha challenges and also resolves some websites limitations on your servers IP. 357 | 358 | You can enable Cloudflare WARP by using `--enable-warp true` option. This script will create and register a free WAPR account and use it. 359 | ``` 360 | bash <(curl -sL https://bit.ly/realityez) --enable-warp true 361 | ``` 362 | Free account has traffic limitation and lower performance in comparison with WARP+ account which needs license. 363 | 364 | You can either buy an WARP+ Unlimited license or get a free WARP+ license from this telegram bot: https://t.me/generatewarpplusbot 365 | 366 | After getting a license from that telegram bot, you can use the license for your server with `--warp-license` option: 367 | ``` 368 | bash <(curl -sL https://bit.ly/realityez) --warp-license aaaaaaaa-bbbbbbbb-cccccccc 369 | ``` 370 | You can use each warp+ license on 4 devices only. 371 | 372 | You can disable Cloudflare WARP with `--enable-warp false`: 373 | ``` 374 | bash <(curl -sL https://bit.ly/realityez) --enable-warp false 375 | ``` 376 | 377 | ## Example 378 | You can combine different options together. 379 | 380 | We want to setup a server with these configurations: 381 | * `grpc` transport protocol 382 | * `www.wikipedia.org` as SNI domain 383 | * Block adult contents 384 | * Enable Cloudflare WARP 385 | * Set Cloudflare WARP+ license 386 | 387 | So we need to execute this command: 388 | ``` 389 | bash <(curl -sL https://bit.ly/realityez) --transport=grpc --domain=www.wikipedia.com --enable-safenet=true --enable-warp=true --warp-license=26z9i0ld-WG0wy324-rA703nZ2 390 | ``` 391 | ## Custom Configuration 392 | Use this feature only if you know exactly what you are doing! 393 | 394 | You can override the configuration generated by the script and add your own custom configuration to it. 395 | 396 | Write your custom configuration in one of these files based on the engine that you are using: 397 | 398 | ``` 399 | /opt/reality-ezpz/sing-box.patch 400 | ``` 401 | or 402 | ``` 403 | /opt/reality-ezpz/xray.patch 404 | ``` 405 | And run script to apply your changes. 406 | 407 | For example if you want to increase the debug level of sing-box engine, you can create `/opt/reality-ezpz/sing-box.patch` with this content: 408 | ``` 409 | { 410 | "log": { 411 | "level": "debug", 412 | "timestamp": true 413 | } 414 | } 415 | ``` -------------------------------------------------------------------------------- /reality-ezpz.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | 20 | set -e 21 | declare -A defaults 22 | declare -A config_file 23 | declare -A args 24 | declare -A config 25 | declare -A users 26 | declare -A path 27 | declare -A service 28 | declare -A md5 29 | declare -A regex 30 | declare -A image 31 | 32 | config_path="/opt/reality-ezpz" 33 | compose_project='reality-ezpz' 34 | tgbot_project='tgbot' 35 | BACKTITLE=RealityEZPZ 36 | MENU="Select an option:" 37 | HEIGHT=30 38 | WIDTH=60 39 | CHOICE_HEIGHT=20 40 | 41 | image[xray]="teddysun/xray:1.8.4" 42 | image[sing-box]="gzxhwq/sing-box:1.8.14" 43 | image[nginx]="nginx:1.24.0" 44 | image[certbot]="certbot/certbot:v2.6.0" 45 | image[haproxy]="haproxy:2.8.0" 46 | image[python]="python:3.11-alpine" 47 | image[wgcf]="virb3/wgcf:2.2.29" 48 | 49 | defaults[transport]=tcp 50 | defaults[domain]=www.google.com 51 | defaults[port]=443 52 | defaults[safenet]=OFF 53 | defaults[warp]=OFF 54 | defaults[warp_license]="" 55 | defaults[warp_private_key]="" 56 | defaults[warp_token]="" 57 | defaults[warp_id]="" 58 | defaults[warp_client_id]="" 59 | defaults[warp_interface_ipv4]="" 60 | defaults[warp_interface_ipv6]="" 61 | defaults[core]=sing-box 62 | defaults[security]=reality 63 | defaults[server]=$(curl -fsSL --ipv4 https://cloudflare.com/cdn-cgi/trace | grep ip | cut -d '=' -f2) 64 | defaults[tgbot]=OFF 65 | defaults[tgbot_token]="" 66 | defaults[tgbot_admins]="" 67 | 68 | config_items=( 69 | "core" 70 | "security" 71 | "service_path" 72 | "public_key" 73 | "private_key" 74 | "short_id" 75 | "transport" 76 | "domain" 77 | "server" 78 | "port" 79 | "safenet" 80 | "warp" 81 | "warp_license" 82 | "warp_private_key" 83 | "warp_token" 84 | "warp_id" 85 | "warp_client_id" 86 | "warp_interface_ipv4" 87 | "warp_interface_ipv6" 88 | "tgbot" 89 | "tgbot_token" 90 | "tgbot_admins" 91 | ) 92 | 93 | regex[domain]="^[a-zA-Z0-9]+([-.][a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$" 94 | regex[port]="^[1-9][0-9]*$" 95 | regex[warp_license]="^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{8}-[a-zA-Z0-9]{8}$" 96 | regex[username]="^[a-zA-Z0-9]+$" 97 | regex[ip]="^([0-9]{1,3}\.){3}[0-9]{1,3}$" 98 | regex[tgbot_token]="^[0-9]{8,10}:[a-zA-Z0-9_-]{35}$" 99 | regex[tgbot_admins]="^[a-zA-Z][a-zA-Z0-9_]{4,31}(,[a-zA-Z][a-zA-Z0-9_]{4,31})*$" 100 | regex[domain_port]="^[a-zA-Z0-9]+([-.][a-zA-Z0-9]+)*\.[a-zA-Z]{2,}(:[1-9][0-9]*)?$" 101 | regex[file_path]="^[a-zA-Z0-9_/.-]+$" 102 | regex[url]="^(http|https)://([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|[0-9]{1,3}(\.[0-9]{1,3}){3})(:[0-9]{1,5})?(/.*)?$" 103 | 104 | function show_help { 105 | echo "" 106 | echo "Usage: reality-ezpz.sh [-t|--transport=tcp|http|grpc|ws|tuic|hysteria2|shadowtls] [-d|--domain=] [--server=] [--regenerate] [--default] 107 | [-r|--restart] [--enable-safenet=true|false] [--port=] [-c|--core=xray|sing-box] [--enable-warp=true|false] 108 | [--warp-license=] [--security=reality|letsencrypt|selfsigned] [-m|--menu] [--show-server-config] [--add-user=] [--lists-users] 109 | [--show-user=] [--delete-user=] [--backup] [--restore=] [--backup-password=] [-u|--uninstall]" 110 | echo "" 111 | echo " -t, --transport Transport protocol (tcp, http, grpc, ws, tuic, hysteria2, shadowtls, default: ${defaults[transport]})" 112 | echo " -d, --domain Domain to use as SNI (default: ${defaults[domain]})" 113 | echo " --server IP address or domain name of server (Must be a valid domain if using letsencrypt security)" 114 | echo " --regenerate Regenerate public and private keys" 115 | echo " --default Restore default configuration" 116 | echo " -r --restart Restart services" 117 | echo " -u, --uninstall Uninstall reality" 118 | echo " --enable-safenet Enable or disable safenet (blocking malware and adult content)" 119 | echo " --port Server port (default: ${defaults[port]})" 120 | echo " --enable-warp Enable or disable Cloudflare warp" 121 | echo " --warp-license Add Cloudflare warp+ license" 122 | echo " -c --core Select core (xray, sing-box, default: ${defaults[core]})" 123 | echo " --security Select type of TLS encryption (reality, letsencrypt, selfsigned, default: ${defaults[security]})" 124 | echo " -m --menu Show menu" 125 | echo " --enable-tgbot Enable Telegram bot for user management" 126 | echo " --tgbot-token Token of Telegram bot" 127 | echo " --tgbot-admins Usernames of telegram bot admins (Comma separated list of usernames without leading '@')" 128 | echo " --show-server-config Print server configuration" 129 | echo " --add-user Add new user" 130 | echo " --list-users List all users" 131 | echo " --show-user Shows the config and QR code of the user" 132 | echo " --delete-user Delete the user" 133 | echo " --backup Backup users and configuration and upload it to temp.sh" 134 | echo " --restore Restore backup from URL or file" 135 | echo " --backup-password Create/Restore password protected backup file" 136 | echo " -h, --help Display this help message" 137 | return 1 138 | } 139 | 140 | function parse_args { 141 | local opts 142 | opts=$(getopt -o t:d:ruc:mh --long transport:,domain:,server:,regenerate,default,restart,uninstall,enable-safenet:,port:,warp-license:,enable-warp:,core:,security:,menu,show-server-config,add-user:,list-users,show-user:,delete-user:,backup,restore:,backup-password:,enable-tgbot:,tgbot-token:,tgbot-admins:,help -- "$@") 143 | if [[ $? -ne 0 ]]; then 144 | return 1 145 | fi 146 | eval set -- "$opts" 147 | while true; do 148 | case $1 in 149 | -t|--transport) 150 | args[transport]="$2" 151 | case ${args[transport]} in 152 | tcp|http|grpc|ws|tuic|hysteria2|shadowtls) 153 | shift 2 154 | ;; 155 | *) 156 | echo "Invalid transport protocol: ${args[transport]}" 157 | return 1 158 | ;; 159 | esac 160 | ;; 161 | -d|--domain) 162 | args[domain]="$2" 163 | if ! [[ ${args[domain]} =~ ${regex[domain_port]} ]]; then 164 | echo "Invalid domain: ${args[domain]}" 165 | return 1 166 | fi 167 | shift 2 168 | ;; 169 | --server) 170 | args[server]="$2" 171 | if ! [[ ${args[server]} =~ ${regex[domain]} || ${args[server]} =~ ${regex[ip]} ]]; then 172 | echo "Invalid server: ${args[domain]}" 173 | return 1 174 | fi 175 | shift 2 176 | ;; 177 | --regenerate) 178 | args[regenerate]=true 179 | shift 180 | ;; 181 | --default) 182 | args[default]=true 183 | shift 184 | ;; 185 | -r|--restart) 186 | args[restart]=true 187 | shift 188 | ;; 189 | -u|--uninstall) 190 | args[uninstall]=true 191 | shift 192 | ;; 193 | --enable-safenet) 194 | case "$2" in 195 | true|false) 196 | $2 && args[safenet]=ON || args[safenet]=OFF 197 | shift 2 198 | ;; 199 | *) 200 | echo "Invalid safenet option: $2" 201 | return 1 202 | ;; 203 | esac 204 | ;; 205 | --enable-warp) 206 | case "$2" in 207 | true|false) 208 | $2 && args[warp]=ON || args[warp]=OFF 209 | shift 2 210 | ;; 211 | *) 212 | echo "Invalid warp option: $2" 213 | return 1 214 | ;; 215 | esac 216 | ;; 217 | --port) 218 | args[port]="$2" 219 | if ! [[ ${args[port]} =~ ${regex[port]} ]]; then 220 | echo "Invalid port number: ${args[port]}" 221 | return 1 222 | elif ((args[port] < 1 || args[port] > 65535)); then 223 | echo "Port number out of range: ${args[port]}" 224 | return 1 225 | fi 226 | shift 2 227 | ;; 228 | --warp-license) 229 | args[warp_license]="$2" 230 | if ! [[ ${args[warp_license]} =~ ${regex[warp_license]} ]]; then 231 | echo "Invalid warp license: ${args[warp_license]}" 232 | return 1 233 | fi 234 | shift 2 235 | ;; 236 | -c|--core) 237 | args[core]="$2" 238 | case ${args[core]} in 239 | xray|sing-box) 240 | shift 2 241 | ;; 242 | *) 243 | echo "Invalid core: ${args[core]}" 244 | return 1 245 | ;; 246 | esac 247 | ;; 248 | --security) 249 | args[security]="$2" 250 | case ${args[security]} in 251 | reality|letsencrypt|selfsigned) 252 | shift 2 253 | ;; 254 | *) 255 | echo "Invalid TLS security option: ${args[security]}" 256 | return 1 257 | ;; 258 | esac 259 | ;; 260 | -m|--menu) 261 | args[menu]=true 262 | shift 263 | ;; 264 | --enable-tgbot) 265 | case "$2" in 266 | true|false) 267 | $2 && args[tgbot]=ON || args[tgbot]=OFF 268 | shift 2 269 | ;; 270 | *) 271 | echo "Invalid enable-tgbot option: $2" 272 | return 1 273 | ;; 274 | esac 275 | ;; 276 | --tgbot-token) 277 | args[tgbot_token]="$2" 278 | if [[ ! ${args[tgbot_token]} =~ ${regex[tgbot_token]} ]]; then 279 | echo "Invalid Telegram Bot Token: ${args[tgbot_token]}" 280 | return 1 281 | fi 282 | if ! curl -sSfL -m 3 "https://api.telegram.org/bot${args[tgbot_token]}/getMe" >/dev/null 2>&1; then 283 | echo "Invalid Telegram Bot Token: Telegram Bot Token is incorrect. Check it again." 284 | return 1 285 | fi 286 | shift 2 287 | ;; 288 | --tgbot-admins) 289 | args[tgbot_admins]="$2" 290 | if [[ ! ${args[tgbot_admins]} =~ ${regex[tgbot_admins]} || $tgbot_admins =~ .+_$ || $tgbot_admins =~ .+_,.+ ]]; then 291 | echo "Invalid Telegram Bot Admins Username: ${args[tgbot_admins]}\nThe usernames must separated by ',' without leading '@' character or any extra space." 292 | return 1 293 | fi 294 | shift 2 295 | ;; 296 | --show-server-config) 297 | args[server-config]=true 298 | shift 299 | ;; 300 | --add-user) 301 | args[add_user]="$2" 302 | if ! [[ ${args[add_user]} =~ ${regex[username]} ]]; then 303 | echo "Invalid username: ${args[add_user]}\nUsername can only contains A-Z, a-z and 0-9" 304 | return 1 305 | fi 306 | shift 2 307 | ;; 308 | --list-users) 309 | args[list_users]=true 310 | shift 311 | ;; 312 | --show-user) 313 | args[show_config]="$2" 314 | shift 2 315 | ;; 316 | --delete-user) 317 | args[delete_user]="$2" 318 | shift 2 319 | ;; 320 | --backup) 321 | args[backup]=true 322 | shift 323 | ;; 324 | --restore) 325 | args[restore]="$2" 326 | if [[ ! ${args[restore]} =~ ${regex[file_path]} ]] && [[ ! ${args[restore]} =~ ${regex[url]} ]]; then 327 | echo "Invalid: Backup file path or URL is not valid." 328 | return 1 329 | fi 330 | shift 2 331 | ;; 332 | --backup-password) 333 | args[backup_password]="$2" 334 | shift 2 335 | ;; 336 | -h|--help) 337 | return 1 338 | ;; 339 | --) 340 | shift 341 | break 342 | ;; 343 | *) 344 | echo "Unknown option: $1" 345 | return 1 346 | ;; 347 | esac 348 | done 349 | 350 | if [[ ${args[uninstall]} == true ]]; then 351 | uninstall 352 | fi 353 | 354 | if [[ -n ${args[warp_license]} ]]; then 355 | args[warp]=ON 356 | fi 357 | } 358 | 359 | function backup { 360 | local backup_name 361 | local backup_password="$1" 362 | local backup_file_url 363 | local exit_code 364 | backup_name="reality-ezpz-backup-$(date +%Y-%m-%d_%H-%M-%S).zip" 365 | cd "${config_path}" 366 | if [ -z "${backup_password}" ]; then 367 | zip -r "/tmp/${backup_name}" . > /dev/null 368 | else 369 | zip -P "${backup_password}" -r "/tmp/${backup_name}" . > /dev/null 370 | fi 371 | if ! backup_file_url=$(curl -fsS -m 30 -F "file=@/tmp/${backup_name}" "https://temp.sh/upload"); then 372 | rm -f "/tmp/${backup_name}" 373 | echo "Error in uploading backup file" >&2 374 | return 1 375 | fi 376 | rm -f "/tmp/${backup_name}" 377 | echo "${backup_file_url}" 378 | } 379 | 380 | function restore { 381 | local backup_file="$1" 382 | local backup_password="$2" 383 | local temp_file 384 | local unzip_output 385 | local unzip_exit_code 386 | local current_state 387 | if [[ ! -r ${backup_file} ]]; then 388 | temp_file=$(mktemp -u) 389 | if [[ "${backup_file}" =~ ^https?://temp\.sh/ ]]; then 390 | if ! curl -fSsL -m 30 -X POST "${backup_file}" -o "${temp_file}"; then 391 | echo "Cannot download or find backup file" >&2 392 | return 1 393 | fi 394 | else 395 | if ! curl -fSsL -m 30 "${backup_file}" -o "${temp_file}"; then 396 | echo "Cannot download or find backup file" >&2 397 | return 1 398 | fi 399 | fi 400 | backup_file="${temp_file}" 401 | fi 402 | current_state=$(set +o) 403 | set +e 404 | if [[ -z "${backup_password}" ]]; then 405 | unzip_output=$(unzip -P "" -t "${backup_file}" 2>&1) 406 | else 407 | unzip_output=$(unzip -P "${backup_password}" -t "${backup_file}" 2>&1) 408 | fi 409 | unzip_exit_code=$? 410 | eval "$current_state" 411 | if [[ ${unzip_exit_code} -eq 0 ]]; then 412 | if ! echo "${unzip_output}" | grep -q 'config'; then 413 | echo "The provided file is not a reality-ezpz backup file." >&2 414 | rm -f "${temp_file}" 415 | return 1 416 | fi 417 | else 418 | if echo "${unzip_output}" | grep -q 'incorrect password'; then 419 | echo "The provided password for backup file is incorrect." >&2 420 | else 421 | echo "An error occurred during zip file verification: ${unzip_output}" >&2 422 | fi 423 | rm -f "${temp_file}" 424 | return 1 425 | fi 426 | rm -rf "${config_path}" 427 | mkdir -p "${config_path}" 428 | set +e 429 | if [[ -z "${backup_password}" ]]; then 430 | unzip_output=$(unzip -d "${config_path}" "${backup_file}" 2>&1) 431 | else 432 | unzip_output=$(unzip -P "${backup_password}" -d "${config_path}" "${backup_file}" 2>&1) 433 | fi 434 | unzip_exit_code=$? 435 | eval "$current_state" 436 | if [[ ${unzip_exit_code} -ne 0 ]]; then 437 | echo "Error in backup restore: ${unzip_output}" >&2 438 | rm -f "${temp_file}" 439 | return 1 440 | fi 441 | rm -f "${temp_file}" 442 | return 443 | } 444 | 445 | function dict_expander { 446 | local -n dict=$1 447 | for key in "${!dict[@]}"; do 448 | echo "${key} ${dict[$key]}" 449 | done 450 | } 451 | 452 | function parse_config_file { 453 | if [[ ! -r "${path[config]}" ]]; then 454 | generate_keys 455 | return 0 456 | fi 457 | while IFS= read -r line; do 458 | if [[ "${line}" =~ ^\s*# ]] || [[ "${line}" =~ ^\s*$ ]]; then 459 | continue 460 | fi 461 | key=$(echo "$line" | cut -d "=" -f 1) 462 | value=$(echo "$line" | cut -d "=" -f 2-) 463 | config_file["${key}"]="${value}" 464 | done < "${path[config]}" 465 | if [[ -z "${config_file[public_key]}" || \ 466 | -z "${config_file[private_key]}" || \ 467 | -z "${config_file[short_id]}" || \ 468 | -z "${config_file[service_path]}" ]]; then 469 | generate_keys 470 | fi 471 | return 0 472 | } 473 | 474 | function parse_users_file { 475 | mkdir -p "$config_path" 476 | touch "${path[users]}" 477 | while read -r line; do 478 | if [[ "${line}" =~ ^\s*# ]] || [[ "${line}" =~ ^\s*$ ]]; then 479 | continue 480 | fi 481 | IFS="=" read -r key value <<< "${line}" 482 | users["${key}"]="${value}" 483 | done < "${path[users]}" 484 | if [[ -n ${args[add_user]} ]]; then 485 | if [[ -z "${users["${args[add_user]}"]}" ]]; then 486 | users["${args[add_user]}"]=$(cat /proc/sys/kernel/random/uuid) 487 | else 488 | echo 'User "'"${args[add_user]}"'" already exists.' 489 | fi 490 | fi 491 | if [[ -n ${args[delete_user]} ]]; then 492 | if [[ -n "${users["${args[delete_user]}"]}" ]]; then 493 | if [[ ${#users[@]} -eq 1 ]]; then 494 | echo -e "You cannot delete the only user.\nAt least one user is needed.\nCreate a new user, then delete this one." 495 | exit 1 496 | fi 497 | unset users["${args[delete_user]}"] 498 | else 499 | echo "User "${args[delete_user]}" does not exists." 500 | exit 1 501 | fi 502 | fi 503 | if [[ ${#users[@]} -eq 0 ]]; then 504 | users[RealityEZPZ]=$(cat /proc/sys/kernel/random/uuid) 505 | echo "RealityEZPZ=${users[RealityEZPZ]}" >> "${path[users]}" 506 | return 0 507 | fi 508 | return 0 509 | } 510 | 511 | function restore_defaults { 512 | local defaults_items=("${!defaults[@]}") 513 | local keep=false 514 | local exclude_list=( 515 | "warp_license" 516 | "tgbot_token" 517 | ) 518 | if [[ -n ${config[warp_id]} && -n ${config[warp_token]} ]]; then 519 | warp_delete_account "${config[warp_id]}" "${config[warp_token]}" 520 | fi 521 | for item in "${defaults_items[@]}"; do 522 | keep=false 523 | for i in "${exclude_list[@]}"; do 524 | if [[ "${i}" == "${item}" ]]; then 525 | keep=true 526 | break 527 | fi 528 | done 529 | if [[ ${keep} == true ]]; then 530 | continue 531 | fi 532 | config["${item}"]="${defaults[${item}]}" 533 | done 534 | } 535 | 536 | function build_config { 537 | local free_80=true 538 | if [[ ${args[regenerate]} == true ]]; then 539 | generate_keys 540 | fi 541 | for item in "${config_items[@]}"; do 542 | if [[ -n ${args["${item}"]} ]]; then 543 | config["${item}"]="${args[${item}]}" 544 | elif [[ -n ${config_file["${item}"]} ]]; then 545 | config["${item}"]="${config_file[${item}]}" 546 | else 547 | config["${item}"]="${defaults[${item}]}" 548 | fi 549 | done 550 | if [[ ${args[default]} == true ]]; then 551 | restore_defaults 552 | return 0 553 | fi 554 | if [[ ${config[tgbot]} == 'ON' && -z ${config[tgbot_token]} ]]; then 555 | echo 'To enable Telegram bot, you have to give the token of bot with --tgbot-token option.' 556 | exit 1 557 | fi 558 | if [[ ${config[tgbot]} == 'ON' && -z ${config[tgbot_admins]} ]]; then 559 | echo 'To enable Telegram bot, you have to give the list of authorized Telegram admins username with --tgbot-admins option.' 560 | exit 1 561 | fi 562 | if [[ ! ${config[server]} =~ ${regex[domain]} && ${config[security]} == 'letsencrypt' ]]; then 563 | echo 'You have to assign a domain to server with "--server " option if you want to use "letsencrypt" as TLS certifcate.' 564 | exit 1 565 | fi 566 | if [[ ${config[transport]} == 'ws' && ${config[security]} == 'reality' ]]; then 567 | echo 'You cannot use "ws" transport with "reality" TLS certificate. Use other transports or change TLS certifcate to letsencrypt or selfsigned' 568 | exit 1 569 | fi 570 | if [[ ${config[transport]} == 'tuic' && ${config[security]} == 'reality' ]]; then 571 | echo 'You cannot use "tuic" transport with "reality" TLS certificate. Use other transports or change TLS certifcate to letsencrypt or selfsigned' 572 | exit 1 573 | fi 574 | if [[ ${config[transport]} == 'tuic' && ${config[core]} == 'xray' ]]; then 575 | echo 'You cannot use "tuic" transport with "xray" core. Use other transports or change core to sing-box' 576 | exit 1 577 | fi 578 | if [[ ${config[transport]} == 'hysteria2' && ${config[security]} == 'reality' ]]; then 579 | echo 'You cannot use "hysteria2" transport with "reality" TLS certificate. Use other transports or change TLS certifcate to letsencrypt or selfsigned' 580 | exit 1 581 | fi 582 | if [[ ${config[transport]} == 'hysteria2' && ${config[core]} == 'xray' ]]; then 583 | echo 'You cannot use "hysteria2" transport with "xray" core. Use other transports or change core to sing-box' 584 | exit 1 585 | fi 586 | if [[ ${config[transport]} == 'shadowtls' && ${config[core]} == 'xray' ]]; then 587 | echo 'You cannot use "shadowtls" transport with "xray" core. Use other transports or change core to sing-box' 588 | exit 1 589 | fi 590 | if [[ ${config[security]} == 'letsencrypt' && ${config[port]} -ne 443 ]]; then 591 | if lsof -i :80 >/dev/null 2>&1; then 592 | free_80=false 593 | for container in $(${docker_cmd} -p ${compose_project} ps -q); do 594 | if docker port "${container}"| grep '0.0.0.0:80' >/dev/null 2>&1; then 595 | free_80=true 596 | break 597 | fi 598 | done 599 | fi 600 | if [[ ${free_80} != 'true' ]]; then 601 | echo 'Port 80 must be free if you want to use "letsencrypt" as the security option.' 602 | exit 1 603 | fi 604 | fi 605 | if [[ (-n "${args[security]}" || -n "${args[transport]}") && ("${args[security]}" == 'reality' || "${args[transport]}" == 'shadowtls') && ("${config_file[security]}" != 'reality' && "${config_file[transport]}" != 'shadowtls') ]]; then 606 | config[domain]="${defaults[domain]}" 607 | fi 608 | if [[ (-n "${args[security]}" || -n "${args[transport]}") && ("${args[security]}" != 'reality' && "${args[transport]}" != 'shadowtls') && ("${config_file[security]}" == 'reality' || "${config_file[transport]}" == 'shadowtls') ]]; then 609 | config[domain]="${config[server]}" 610 | fi 611 | if [[ -n "${args[server]}" && ("${config[security]}" != 'reality' && "${config[transport]}" != 'shadowtls') ]]; then 612 | config[domain]="${config[server]}" 613 | fi 614 | if [[ -n "${args[warp]}" && "${args[warp]}" == 'OFF' && "${config_file[warp]}" == 'ON' ]]; then 615 | if [[ -n ${config[warp_id]} && -n ${config[warp_token]} ]]; then 616 | warp_delete_account "${config[warp_id]}" "${config[warp_token]}" 617 | fi 618 | fi 619 | if { [[ -n "${args[warp]}" && "${args[warp]}" == 'ON' && "${config_file[warp]}" == 'OFF' ]] || \ 620 | [[ "${config[warp]}" == 'ON' && ( -z ${config[warp_private_key]} || \ 621 | -z ${config[warp_token]} || \ 622 | -z ${config[warp_id]} || \ 623 | -z ${config[warp_client_id]} || \ 624 | -z ${config[warp_interface_ipv4]} || \ 625 | -z ${config[warp_interface_ipv6]} ) ]]; }; then 626 | config[warp]='OFF' 627 | warp_create_account || exit 1 628 | config[warp]='ON' 629 | fi 630 | if [[ -n "${args[warp_license]}" && ( -z "${config_file[warp_license]}" || "${args[warp_license]}" != "${config_file[warp_license]}" ) ]]; then 631 | if ! warp_add_license "${config[warp_id]}" "${config[warp_token]}" "${args[warp_license]}"; then 632 | config[warp_license]="" 633 | echo "WARP+ license error! Please check your license and try again." 634 | exit 1 635 | fi 636 | fi 637 | } 638 | 639 | function update_config_file { 640 | mkdir -p "${config_path}" 641 | touch "${path[config]}" 642 | for item in "${config_items[@]}"; do 643 | if grep -q "^${item}=" "${path[config]}"; then 644 | sed -i "s|^${item}=.*|${item}=${config[${item}]}|" "${path[config]}" 645 | else 646 | echo "${item}=${config[${item}]}" >> "${path[config]}" 647 | fi 648 | done 649 | check_reload 650 | } 651 | 652 | function update_users_file { 653 | rm -f "${path[users]}" 654 | for user in "${!users[@]}"; do 655 | echo "${user}=${users[${user}]}" >> "${path[users]}" 656 | done 657 | check_reload 658 | } 659 | 660 | function generate_keys { 661 | local key_pair 662 | key_pair=$(docker run --rm ${image[xray]} xray x25519) 663 | config_file[public_key]=$(echo "${key_pair}" | grep 'Public key:' | awk '{print $3}') 664 | config_file[private_key]=$(echo "${key_pair}" | grep 'Private key:' | awk '{print $3}') 665 | config_file[short_id]=$(openssl rand -hex 8) 666 | config_file[service_path]=$(openssl rand -hex 4) 667 | } 668 | 669 | function uninstall { 670 | if docker compose >/dev/null 2>&1; then 671 | docker compose --project-directory "${config_path}" down --timeout 2 || true 672 | docker compose --project-directory "${config_path}" -p ${compose_project} down --timeout 2 || true 673 | docker compose --project-directory "${config_path}/tgbot" -p ${tgbot_project} down --timeout 2 || true 674 | elif which docker-compose >/dev/null 2>&1; then 675 | docker-compose --project-directory "${config_path}" down --timeout 2 || true 676 | docker-compose --project-directory "${config_path}" -p ${compose_project} down --timeout 2 || true 677 | docker-compose --project-directory "${config_path}/tgbot" -p ${tgbot_project} down --timeout 2 || true 678 | fi 679 | rm -rf "${config_path}" 680 | echo "Reality-EZPZ uninstalled successfully." 681 | exit 0 682 | } 683 | 684 | function install_packages { 685 | if [[ -n $BOT_TOKEN ]]; then 686 | return 0 687 | fi 688 | if ! which qrencode whiptail jq xxd zip unzip >/dev/null 2>&1; then 689 | if which apt >/dev/null 2>&1; then 690 | apt update 691 | DEBIAN_FRONTEND=noninteractive apt install qrencode whiptail jq xxd zip unzip -y 692 | return 0 693 | fi 694 | if which yum >/dev/null 2>&1; then 695 | yum makecache 696 | yum install epel-release -y || true 697 | yum install qrencode newt jq vim-common zip unzip -y 698 | return 0 699 | fi 700 | echo "OS is not supported!" 701 | return 1 702 | fi 703 | } 704 | 705 | function install_docker { 706 | if ! which docker >/dev/null 2>&1; then 707 | curl -fsSL -m 5 https://get.docker.com | bash 708 | systemctl enable --now docker 709 | docker_cmd="docker compose" 710 | return 0 711 | fi 712 | if docker compose >/dev/null 2>&1; then 713 | docker_cmd="docker compose" 714 | return 0 715 | fi 716 | if which docker-compose >/dev/null 2>&1; then 717 | docker_cmd="docker-compose" 718 | return 0 719 | fi 720 | curl -fsSL -m 30 https://github.com/docker/compose/releases/download/v2.28.0/docker-compose-linux-$(uname -m) -o /usr/local/bin/docker-compose 721 | chmod +x /usr/local/bin/docker-compose 722 | docker_cmd="docker-compose" 723 | return 0 724 | } 725 | 726 | function generate_docker_compose { 727 | cat >"${path[compose]}" <"${path[tgbot_compose]}" < "${path[haproxy]}" 894 | } 895 | 896 | function generate_certbot_script { 897 | cat >"${path[certbot_startup]}" << EOF 898 | #!/bin/sh 899 | trap exit TERM 900 | fullchain_path=/etc/letsencrypt/live/${config[server]}/fullchain.pem 901 | if [[ -r "\${fullchain_path}" ]]; then 902 | fullchain_fingerprint=\$(openssl x509 -noout -fingerprint -sha256 -in "\${fullchain_path}" 2>/dev/null |\ 903 | awk -F= '{print \$2}' | tr -d : | tr '[:upper:]' '[:lower:]') 904 | installed_fingerprint=\$(openssl x509 -noout -fingerprint -sha256 -in /certificate/server.pem 2>/dev/null |\ 905 | awk -F= '{print \$2}' | tr -d : | tr '[:upper:]' '[:lower:]') 906 | if [[ \$fullchain_fingerprint != \$installed_fingerprint ]]; then 907 | /deployhook.sh /certificate ${compose_project} ${config[server]} ${service[server_crt]} $([[ ${config[transport]} != 'tcp' ]] && echo "${service[server_pem]}" || true) 908 | fi 909 | fi 910 | while true; do 911 | ls -d /website/* | grep -E '^/website/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\$'|xargs rm -f 912 | uuid=\$(uuidgen) 913 | echo "\$uuid" > "/website/\$uuid" 914 | response=\$(curl -skL --max-time 3 http://${config[server]}/\$uuid) 915 | if echo "\$response" | grep \$uuid >/dev/null; then 916 | break 917 | fi 918 | echo "Domain ${config[server]} is not pointing to the server" 919 | sleep 5 920 | done 921 | ls -d /website/* | grep -E '^/website/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\$'|xargs rm -f 922 | while true; do 923 | certbot certonly -n \\ 924 | --standalone \\ 925 | --key-type ecdsa \\ 926 | --elliptic-curve secp256r1 \\ 927 | --agree-tos \\ 928 | --register-unsafely-without-email \\ 929 | -d ${config[server]} \\ 930 | --deploy-hook "/deployhook.sh /certificate ${compose_project} ${config[server]} ${service[server_crt]} $([[ ${config[transport]} != 'tcp' ]] && echo "${service[server_pem]}" || true)" 931 | sleep 1h & 932 | wait \${!} 933 | done 934 | EOF 935 | } 936 | 937 | function generate_certbot_deployhook { 938 | cat >"${path[certbot_deployhook]}" << EOF 939 | #!/bin/sh 940 | cert_path=\$1 941 | compose_project=\$2 942 | domain=\$3 943 | renewed_path=/etc/letsencrypt/live/\$domain 944 | cat "\$renewed_path/fullchain.pem" > "\$cert_path/server.crt" 945 | cat "\$renewed_path/privkey.pem" > "\$cert_path/server.key" 946 | cat "\$renewed_path/fullchain.pem" "\$renewed_path/privkey.pem" > "\$cert_path/server.pem" 947 | i=4 948 | while [ \$i -le \$# ]; do 949 | eval service=\\\${\$i} 950 | docker compose -p "${compose_project}" restart --timeout 2 "\$service" 951 | i=\$((i+1)) 952 | done 953 | EOF 954 | chmod +x "${path[certbot_deployhook]}" 955 | } 956 | 957 | function generate_certbot_dockerfile { 958 | cat >"${path[certbot_dockerfile]}" << EOF 959 | FROM ${image[certbot]} 960 | RUN apk add --no-cache docker-cli-compose curl uuidgen 961 | EOF 962 | } 963 | 964 | function generate_tgbot_dockerfile { 965 | cat >"${path[tgbot_dockerfile]}" << EOF 966 | FROM ${image[python]} 967 | WORKDIR ${config_path}/tgbot 968 | RUN apk add --no-cache docker-cli-compose curl bash newt libqrencode-tools sudo openssl jq zip unzip 969 | RUN pip install --no-cache-dir python-telegram-bot==13.5 qrcode[pil]==7.4.2 970 | CMD [ "python", "./tgbot.py" ] 971 | EOF 972 | } 973 | 974 | function download_tgbot_script { 975 | curl -fsSL -m 3 https://raw.githubusercontent.com/aleskxyz/reality-ezpz/master/tgbot.py -o "${path[tgbot_script]}" 976 | } 977 | 978 | function generate_selfsigned_certificate { 979 | openssl ecparam -name prime256v1 -genkey -out "${path[server_key]}" 980 | openssl req -new -key "${path[server_key]}" -out /tmp/server.csr -subj "/CN=${config[server]}" 981 | openssl x509 -req -days 365 -in /tmp/server.csr -signkey "${path[server_key]}" -out "${path[server_crt]}" 982 | cat "${path[server_key]}" "${path[server_crt]}" > "${path[server_pem]}" 983 | rm -f /tmp/server.csr 984 | } 985 | 986 | function generate_engine_config { 987 | local type="vless" 988 | local users_object="" 989 | local reality_object="" 990 | local tls_object="" 991 | local warp_object="" 992 | local reality_port=443 993 | local temp_file 994 | if [[ ${config[transport]} == 'tuic' ]]; then 995 | type='tuic' 996 | elif [[ ${config[transport]} == 'hysteria2' ]]; then 997 | type='hysteria2' 998 | elif [[ ${config[transport]} == 'shadowtls' ]]; then 999 | type='shadowtls' 1000 | else 1001 | type='vless' 1002 | fi 1003 | if [[ (${config[security]} == 'reality' || ${config[transport]} == 'shadowtls') && ${config[domain]} =~ ":" ]]; then 1004 | reality_port="${config[domain]#*:}" 1005 | fi 1006 | if [[ ${config[core]} == 'sing-box' ]]; then 1007 | reality_object='"tls": { 1008 | "enabled": true, 1009 | "server_name": "'"${config[domain]%%:*}"'", 1010 | "alpn": [], 1011 | "reality": { 1012 | "enabled": true, 1013 | "handshake": { 1014 | "server": "'"${config[domain]%%:*}"'", 1015 | "server_port": '"${reality_port}"' 1016 | }, 1017 | "private_key": "'"${config[private_key]}"'", 1018 | "short_id": ["'"${config[short_id]}"'"], 1019 | "max_time_difference": "1m" 1020 | } 1021 | }' 1022 | tls_object='"tls": { 1023 | "enabled": true, 1024 | "certificate_path": "/etc/sing-box/server.crt", 1025 | "key_path": "/etc/sing-box/server.key" 1026 | }' 1027 | if [[ ${config[warp]} == 'ON' ]]; then 1028 | warp_object='{ 1029 | "type": "wireguard", 1030 | "tag": "warp", 1031 | "server": "engage.cloudflareclient.com", 1032 | "server_port": 2408, 1033 | "system_interface": false, 1034 | "local_address": [ 1035 | "'"${config[warp_interface_ipv4]}"'/32", 1036 | "'"${config[warp_interface_ipv6]}"'/128" 1037 | ], 1038 | "private_key": "'"${config[warp_private_key]}"'", 1039 | "peer_public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", 1040 | "reserved": '"$(warp_decode_reserved "${config[warp_client_id]}")"', 1041 | "mtu": 1280 1042 | },' 1043 | fi 1044 | for user in "${!users[@]}"; do 1045 | if [ -n "$users_object" ]; then 1046 | users_object="${users_object},"$'\n' 1047 | fi 1048 | if [[ ${config[transport]} == 'tuic' ]]; then 1049 | users_object=${users_object}'{"uuid": "'"${users[${user}]}"'", "password": "'"$(echo -n "${user}${users[${user}]}" | sha256sum | cut -d ' ' -f 1 | head -c 16)"'", "name": "'"${user}"'"}' 1050 | elif [[ ${config[transport]} == 'hysteria2' ]]; then 1051 | users_object=${users_object}'{"password": "'"$(echo -n "${user}${users[${user}]}" | sha256sum | cut -d ' ' -f 1 | head -c 16)"'", "name": "'"${user}"'"}' 1052 | elif [[ ${config[transport]} == 'shadowtls' ]]; then 1053 | users_object=${users_object}'{"password": "'"${users[${user}]}"'", "name": "'"${user}"'"}' 1054 | else 1055 | users_object=${users_object}'{"uuid": "'"${users[${user}]}"'", "flow": "'"$([[ ${config[transport]} == 'tcp' ]] && echo 'xtls-rprx-vision' || true)"'", "name": "'"${user}"'"}' 1056 | fi 1057 | done 1058 | cat >"${path[engine]}" <"${path[engine]}" < ${temp_file} 1391 | mv ${temp_file} ${path[engine]} 1392 | fi 1393 | } 1394 | 1395 | function generate_config { 1396 | generate_docker_compose 1397 | generate_engine_config 1398 | if [[ ${config[security]} != "reality" && ${config[transport]} != 'shadowtls' ]]; then 1399 | mkdir -p "${config_path}/certificate" 1400 | generate_haproxy_config 1401 | if [[ ! -r "${path[server_pem]}" || ! -r "${path[server_crt]}" || ! -r "${path[server_key]}" ]]; then 1402 | generate_selfsigned_certificate 1403 | fi 1404 | fi 1405 | if [[ ${config[security]} == "letsencrypt" && ${config[transport]} != 'shadowtls' ]]; then 1406 | mkdir -p "${config_path}/certbot" 1407 | generate_certbot_deployhook 1408 | generate_certbot_dockerfile 1409 | generate_certbot_script 1410 | fi 1411 | if [[ ${config[tgbot]} == "ON" ]]; then 1412 | mkdir -p "${config_path}/tgbot" 1413 | generate_tgbot_compose 1414 | generate_tgbot_dockerfile 1415 | download_tgbot_script 1416 | fi 1417 | } 1418 | 1419 | function get_ipv6 { 1420 | curl -fsSL -m 3 --ipv6 https://cloudflare.com/cdn-cgi/trace 2> /dev/null | grep ip | cut -d '=' -f2 1421 | } 1422 | 1423 | function print_client_configuration { 1424 | local username=$1 1425 | local client_config 1426 | local ipv6 1427 | local client_config_ipv6 1428 | if [[ ${config[transport]} == 'tuic' ]]; then 1429 | client_config="tuic://" 1430 | client_config="${client_config}${users[${username}]}" 1431 | client_config="${client_config}:$(echo -n "${username}${users[${username}]}" | sha256sum | cut -d ' ' -f 1 | head -c 16)" 1432 | client_config="${client_config}@${config[server]}" 1433 | client_config="${client_config}:${config[port]}" 1434 | client_config="${client_config}/?congestion_control=bbr&udp_relay_mode=quic" 1435 | client_config="${client_config}$([[ ${config[security]} == 'selfsigned' ]] && echo "&allow_insecure=1" || true)" 1436 | client_config="${client_config}#${username}" 1437 | elif [[ ${config[transport]} == 'hysteria2' ]]; then 1438 | client_config="hy2://" 1439 | client_config="${client_config}$(echo -n "${username}${users[${username}]}" | sha256sum | cut -d ' ' -f 1 | head -c 16)" 1440 | client_config="${client_config}@${config[server]}" 1441 | client_config="${client_config}:${config[port]}" 1442 | client_config="${client_config}/?obfs=salamander&obfs-password=${config[service_path]}" 1443 | client_config="${client_config}$([[ ${config[security]} == 'selfsigned' ]] && echo "&insecure=1" || true)" 1444 | client_config="${client_config}#${username}" 1445 | elif [[ ${config[transport]} == 'shadowtls' ]]; then 1446 | client_config='{"dns":{"independent_cache":true,"rules":[{"domain":["dns.google"],"server":"dns-direct"}],"servers":[{"address":"https://dns.google/dns-query","address_resolver":"dns-direct","strategy":"ipv4_only","tag":"dns-remote"},{"address":"local","address_resolver":"dns-local","detour":"direct","strategy":"ipv4_only","tag":"dns-direct"},{"address":"local","detour":"direct","tag":"dns-local"},{"address":"rcode://success","tag":"dns-block"}]},"inbounds":[{"listen":"127.0.0.1","listen_port":6450,"override_address":"8.8.8.8","override_port":53,"tag":"dns-in","type":"direct"},{"domain_strategy":"","endpoint_independent_nat":true,"inet4_address":["172.19.0.1/28"],"mtu":9000,"sniff":true,"sniff_override_destination":false,"stack":"mixed","tag":"tun-in","auto_route":true,"type":"tun"},{"domain_strategy":"","listen":"127.0.0.1","listen_port":2080,"sniff":true,"sniff_override_destination":false,"tag":"mixed-in","type":"mixed"}],"log":{"level":"warning"},"outbounds":[{"method":"chacha20-ietf-poly1305","password":"'"${users[${username}]}"'","server":"127.0.0.1","server_port":1080,"type":"shadowsocks","udp_over_tcp":true,"domain_strategy":"","tag":"proxy","detour":"shadowtls"},{"password":"'"${users[${username}]}"'","server":"'"${config[server]}"'","server_port":'"${config[port]}"',"tls":{"enabled":true,"insecure":false,"server_name":"'"${config[domain]%%:*}"'","utls":{"enabled":true,"fingerprint":"chrome"}},"version":3,"type":"shadowtls","domain_strategy":"","tag":"shadowtls"},{"tag":"direct","type":"direct"},{"tag":"bypass","type":"direct"},{"tag":"block","type":"block"},{"tag":"dns-out","type":"dns"}],"route":{"auto_detect_interface":true,"rule_set":[],"rules":[{"outbound":"dns-out","port":[53]},{"inbound":["dns-in"],"outbound":"dns-out"},{"ip_cidr":["224.0.0.0/3","ff00::/8"],"outbound":"block","source_ip_cidr":["224.0.0.0/3","ff00::/8"]}]}}' 1447 | else 1448 | client_config="vless://" 1449 | client_config="${client_config}${users[${username}]}" 1450 | client_config="${client_config}@${config[server]}" 1451 | client_config="${client_config}:${config[port]}" 1452 | client_config="${client_config}?security=$([[ ${config[security]} == 'reality' ]] && echo reality || echo tls)" 1453 | client_config="${client_config}&encryption=none" 1454 | client_config="${client_config}&alpn=$([[ ${config[transport]} == 'ws' ]] && echo 'http/1.1' || echo 'h2,http/1.1')" 1455 | client_config="${client_config}&headerType=none" 1456 | client_config="${client_config}&fp=chrome" 1457 | client_config="${client_config}&type=${config[transport]}" 1458 | client_config="${client_config}&flow=$([[ ${config[transport]} == 'tcp' ]] && echo 'xtls-rprx-vision' || true)" 1459 | client_config="${client_config}&sni=${config[domain]%%:*}" 1460 | client_config="${client_config}$([[ ${config[transport]} == 'ws' || ${config[transport]} == 'http' ]] && echo "&host=${config[server]}" || true)" 1461 | client_config="${client_config}$([[ ${config[security]} == 'reality' ]] && echo "&pbk=${config[public_key]}" || true)" 1462 | client_config="${client_config}$([[ ${config[security]} == 'reality' ]] && echo "&sid=${config[short_id]}" || true)" 1463 | client_config="${client_config}$([[ ${config[transport]} == 'ws' || ${config[transport]} == 'http' ]] && echo "&path=%2F${config[service_path]}" || true)" 1464 | client_config="${client_config}$([[ ${config[transport]} == 'grpc' ]] && echo '&mode=gun' || true)" 1465 | client_config="${client_config}$([[ ${config[transport]} == 'grpc' ]] && echo "&serviceName=${config[service_path]}" || true)" 1466 | client_config="${client_config}#${username}" 1467 | fi 1468 | echo "" 1469 | echo "==================================================" 1470 | echo "Client configuration:" 1471 | echo "" 1472 | echo "$client_config" 1473 | echo "" 1474 | echo "Or you can scan the QR code:" 1475 | echo "" 1476 | qrencode -t ansiutf8 "${client_config}" 1477 | ipv6=$(get_ipv6) 1478 | if [[ -n $ipv6 ]]; then 1479 | if [[ ${config[transport]} != 'shadowtls' ]]; then 1480 | client_config_ipv6=$(echo "$client_config" | sed "s/@${config[server]}:/@[${ipv6}]:/" | sed "s/#${username}/#${username}-ipv6/") 1481 | else 1482 | client_config_ipv6=$(echo "$client_config" | sed "s/\"server\":\"${config[server]}\"/\"server\":\"${ipv6}\"/") 1483 | fi 1484 | echo "" 1485 | echo "==================IPv6 Config======================" 1486 | echo "Client configuration:" 1487 | echo "" 1488 | echo "$client_config_ipv6" 1489 | echo "" 1490 | echo "Or you can scan the QR code:" 1491 | echo "" 1492 | qrencode -t ansiutf8 "${client_config_ipv6}" 1493 | fi 1494 | } 1495 | 1496 | function upgrade { 1497 | local uuid 1498 | local warp_token 1499 | local warp_id 1500 | if [[ -e "${HOME}/reality/config" ]]; then 1501 | ${docker_cmd} --project-directory "${HOME}/reality" down --remove-orphans --timeout 2 1502 | mv -f "${HOME}/reality" ${config_path} 1503 | fi 1504 | uuid=$(grep '^uuid=' "${path[config]}" 2>/dev/null | cut -d= -f2 || true) 1505 | if [[ -n $uuid ]]; then 1506 | sed -i '/^uuid=/d' "${path[users]}" 1507 | echo "RealityEZPZ=${uuid}" >> "${path[users]}" 1508 | sed -i 's|=true|=ON|g; s|=false|=OFF|g' "${path[users]}" 1509 | fi 1510 | rm -f "${config_path}/xray.conf" 1511 | rm -f "${config_path}/singbox.conf" 1512 | if ! ${docker_cmd} ls | grep ${compose_project} >/dev/null && [[ -r ${path[compose]} ]]; then 1513 | ${docker_cmd} --project-directory ${config_path} down --remove-orphans --timeout 2 1514 | fi 1515 | if [[ -r ${path[config]} ]]; then 1516 | sed -i 's|transport=h2|transport=http|g' "${path[config]}" 1517 | sed -i 's|core=singbox|core=sing-box|g' "${path[config]}" 1518 | sed -i 's|security=tls-invalid|security=selfsigned|g' "${path[config]}" 1519 | sed -i 's|security=tls-valid|security=letsencrypt|g' "${path[config]}" 1520 | fi 1521 | for key in "${!path[@]}"; do 1522 | if [[ -d "${path[$key]}" ]]; then 1523 | rm -rf "${path[$key]}" 1524 | fi 1525 | done 1526 | if [[ -d "${config_path}/warp" ]]; then 1527 | ${docker_cmd} --project-directory ${config_path} -p ${compose_project} down --remove-orphans --timeout 2 || true 1528 | warp_token=$(cat ${config_path}/warp/reg.json | jq -r '.api_token') 1529 | warp_id=$(cat ${config_path}/warp/reg.json | jq -r '.registration_id') 1530 | warp_api "DELETE" "/reg/${warp_id}" "" "${warp_token}" >/dev/null 2>&1 || true 1531 | rm -rf "${config_path}/warp" 1532 | fi 1533 | } 1534 | 1535 | function main_menu { 1536 | local selection 1537 | while true; do 1538 | selection=$(whiptail --clear --backtitle "$BACKTITLE" --title "Server Management" \ 1539 | --menu "$MENU" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 1540 | --ok-button "Select" \ 1541 | --cancel-button "Exit" \ 1542 | "1" "Add New User" \ 1543 | "2" "Delete User" \ 1544 | "3" "View User" \ 1545 | "4" "View Server Config" \ 1546 | "5" "Configuration" \ 1547 | 3>&1 1>&2 2>&3) 1548 | if [[ $? -ne 0 ]]; then 1549 | break 1550 | fi 1551 | case $selection in 1552 | 1 ) 1553 | add_user_menu 1554 | ;; 1555 | 2 ) 1556 | delete_user_menu 1557 | ;; 1558 | 3 ) 1559 | view_user_menu 1560 | ;; 1561 | 4 ) 1562 | view_config_menu 1563 | ;; 1564 | 5 ) 1565 | configuration_menu 1566 | ;; 1567 | esac 1568 | done 1569 | } 1570 | 1571 | function add_user_menu { 1572 | local username 1573 | local message 1574 | while true; do 1575 | username=$(whiptail \ 1576 | --clear \ 1577 | --backtitle "$BACKTITLE" \ 1578 | --title "Add New User" \ 1579 | --inputbox "Enter username:" \ 1580 | $HEIGHT $WIDTH \ 1581 | 3>&1 1>&2 2>&3) 1582 | if [[ $? -ne 0 ]]; then 1583 | break 1584 | fi 1585 | if [[ ! $username =~ ${regex[username]} ]]; then 1586 | message_box "Invalid Username" "Username can only contains A-Z, a-z and 0-9" 1587 | continue 1588 | fi 1589 | if [[ -n ${users[$username]} ]]; then 1590 | message_box "Invalid Username" '"'"${username}"'" already exists.' 1591 | continue 1592 | fi 1593 | users[$username]=$(cat /proc/sys/kernel/random/uuid) 1594 | update_users_file 1595 | whiptail \ 1596 | --clear \ 1597 | --backtitle "$BACKTITLE" \ 1598 | --title "Add New User" \ 1599 | --yes-button "View User" \ 1600 | --no-button "Return" \ 1601 | --yesno 'User "'"${username}"'" has been created.' \ 1602 | $HEIGHT $WIDTH \ 1603 | 3>&1 1>&2 2>&3 1604 | if [[ $? -ne 0 ]]; then 1605 | break 1606 | fi 1607 | view_user_menu "${username}" 1608 | done 1609 | } 1610 | 1611 | function delete_user_menu { 1612 | local username 1613 | while true; do 1614 | username=$(list_users_menu "Delete User") 1615 | if [[ $? -ne 0 ]]; then 1616 | return 0 1617 | fi 1618 | if [[ ${#users[@]} -eq 1 ]]; then 1619 | message_box "Delete User" "You cannot delete the only user.\nAt least one user is needed.\nCreate a new user, then delete this one." 1620 | continue 1621 | fi 1622 | whiptail \ 1623 | --clear \ 1624 | --backtitle "$BACKTITLE" \ 1625 | --title "Delete User" \ 1626 | --yesno "Are you sure you want to delete $username?" \ 1627 | $HEIGHT $WIDTH \ 1628 | 3>&1 1>&2 2>&3 1629 | if [[ $? -ne 0 ]]; then 1630 | continue 1631 | fi 1632 | unset users["${username}"] 1633 | update_users_file 1634 | message_box "Delete User" 'User "'"${username}"'" has been deleted.' 1635 | done 1636 | } 1637 | 1638 | function view_user_menu { 1639 | local username 1640 | local user_config 1641 | while true; do 1642 | if [[ $# -gt 0 ]]; then 1643 | username=$1 1644 | else 1645 | username=$(list_users_menu "View User") 1646 | if [[ $? -ne 0 ]]; then 1647 | return 0 1648 | fi 1649 | fi 1650 | if [[ ${config[transport]} == 'tuic' ]]; then 1651 | user_config=$(echo " 1652 | Protocol: tuic 1653 | Remarks: ${username} 1654 | Address: ${config[server]} 1655 | Port: ${config[port]} 1656 | UUID: ${users[$username]} 1657 | Password: $(echo -n "${username}${users[${username}]}" | sha256sum | cut -d ' ' -f 1 | head -c 16) 1658 | UDP Relay Mode: quic 1659 | Congestion Control: bbr 1660 | " | tr -s '\n') 1661 | elif [[ ${config[transport]} == 'hysteria2' ]]; then 1662 | user_config=$(echo " 1663 | Protocol: hysteria2 1664 | Remarks: ${username} 1665 | Address: ${config[server]} 1666 | Port: ${config[port]} 1667 | Password: $(echo -n "${username}${users[${username}]}" | sha256sum | cut -d ' ' -f 1 | head -c 16) 1668 | OBFS Type: salamander 1669 | OBFS Password: ${config[service_path]} 1670 | " | tr -s '\n') 1671 | elif [[ ${config[transport]} == 'shadowtls' ]]; then 1672 | user_config=$(echo " 1673 | === First item of the chain proxy === 1674 | Protocol: shadowtls 1675 | Remarks: ${username}-shadowtls 1676 | Address: ${config[server]} 1677 | Port: ${config[port]} 1678 | Password: ${users[$username]} 1679 | Protocol Version: 3 1680 | SNI: ${config[domain]%%:*} 1681 | Fingerprint: chrome 1682 | === Second item of the chain proxy === 1683 | Protocol: shadowsocks 1684 | Remarks: ${username}-shadowsocks 1685 | Address: 127.0.0.1 1686 | Port: 1080 1687 | Password: ${users[$username]} 1688 | Encryption Method: chacha20-ietf-poly1305 1689 | UDP over TCP: true 1690 | 1691 | " | tr -s '\n') 1692 | else 1693 | user_config=$(echo " 1694 | Protocol: vless 1695 | Remarks: ${username} 1696 | Address: ${config[server]} 1697 | Port: ${config[port]} 1698 | ID: ${users[$username]} 1699 | Flow: $([[ ${config[transport]} == 'tcp' ]] && echo 'xtls-rprx-vision' || true) 1700 | Network: ${config[transport]} 1701 | $([[ ${config[transport]} == 'ws' || ${config[transport]} == 'http' ]] && echo "Host Header: ${config[server]}" || true) 1702 | $([[ ${config[transport]} == 'ws' || ${config[transport]} == 'http' ]] && echo "Path: /${config[service_path]}" || true) 1703 | $([[ ${config[transport]} == 'grpc' ]] && echo 'gRPC mode: gun' || true) 1704 | $([[ ${config[transport]} == 'grpc' ]] && echo 'gRPC serviceName: '"${config[service_path]}" || true) 1705 | TLS: $([[ ${config[security]} == 'reality' ]] && echo 'reality' || echo 'tls') 1706 | SNI: ${config[domain]%%:*} 1707 | ALPN: $([[ ${config[transport]} == 'ws' ]] && echo 'http/1.1' || echo 'h2,http/1.1') 1708 | Fingerprint: chrome 1709 | $([[ ${config[security]} == 'reality' ]] && echo "PublicKey: ${config[public_key]}" || true) 1710 | $([[ ${config[security]} == 'reality' ]] && echo "ShortId: ${config[short_id]}" || true) 1711 | " | tr -s '\n') 1712 | fi 1713 | whiptail \ 1714 | --clear \ 1715 | --backtitle "$BACKTITLE" \ 1716 | --title "${username} details" \ 1717 | --yes-button "View QR" \ 1718 | --no-button "Return" \ 1719 | --yesno "${user_config}" \ 1720 | $HEIGHT $WIDTH \ 1721 | 3>&1 1>&2 2>&3 1722 | if [[ $? -eq 0 ]]; then 1723 | clear 1724 | print_client_configuration "${username}" 1725 | echo 1726 | echo "Press Enter to return ..." 1727 | read 1728 | clear 1729 | fi 1730 | if [[ $# -gt 0 ]]; then 1731 | return 0 1732 | fi 1733 | done 1734 | } 1735 | 1736 | function list_users_menu { 1737 | local title=$1 1738 | local options 1739 | local selection 1740 | options=$(dict_expander users) 1741 | selection=$(whiptail --clear --noitem --backtitle "$BACKTITLE" --title "$title" \ 1742 | --menu "Select the user" $HEIGHT $WIDTH $CHOICE_HEIGHT $options \ 1743 | 3>&1 1>&2 2>&3) 1744 | if [[ $? -ne 0 ]]; then 1745 | return 1 1746 | fi 1747 | echo "${selection}" 1748 | } 1749 | 1750 | function show_server_config { 1751 | local server_config 1752 | server_config="Core: ${config[core]}" 1753 | server_config=$server_config$'\n'"Server Address: ${config[server]}" 1754 | server_config=$server_config$'\n'"Domain SNI: ${config[domain]}" 1755 | server_config=$server_config$'\n'"Port: ${config[port]}" 1756 | server_config=$server_config$'\n'"Transport: ${config[transport]}" 1757 | server_config=$server_config$'\n'"Security: ${config[security]}" 1758 | server_config=$server_config$'\n'"Safenet: ${config[safenet]}" 1759 | server_config=$server_config$'\n'"WARP: ${config[warp]}" 1760 | server_config=$server_config$'\n'"WARP License: ${config[warp_license]}" 1761 | server_config=$server_config$'\n'"Telegram Bot: ${config[tgbot]}" 1762 | server_config=$server_config$'\n'"Telegram Bot Token: ${config[tgbot_token]}" 1763 | server_config=$server_config$'\n'"Telegram Bot Admins: ${config[tgbot_admins]}" 1764 | echo "${server_config}" 1765 | } 1766 | 1767 | function view_config_menu { 1768 | local server_config 1769 | server_config=$(show_server_config) 1770 | message_box "Server Configuration" "${server_config}" 1771 | } 1772 | 1773 | function restart_menu { 1774 | whiptail \ 1775 | --clear \ 1776 | --backtitle "$BACKTITLE" \ 1777 | --title "Restart Services" \ 1778 | --yesno "Are you sure to restart services?" \ 1779 | $HEIGHT $WIDTH \ 1780 | 3>&1 1>&2 2>&3 1781 | if [[ $? -ne 0 ]]; then 1782 | return 1783 | fi 1784 | restart_docker_compose 1785 | if [[ ${config[tgbot]} == 'ON' ]]; then 1786 | restart_tgbot_compose 1787 | fi 1788 | } 1789 | 1790 | function regenerate_menu { 1791 | whiptail \ 1792 | --clear \ 1793 | --backtitle "$BACKTITLE" \ 1794 | --title "Regenrate keys" \ 1795 | --yesno "Are you sure to regenerate keys?" \ 1796 | $HEIGHT $WIDTH \ 1797 | 3>&1 1>&2 2>&3 1798 | if [[ $? -ne 0 ]]; then 1799 | return 1800 | fi 1801 | generate_keys 1802 | config[public_key]=${config_file[public_key]} 1803 | config[private_key]=${config_file[private_key]} 1804 | config[short_id]=${config_file[short_id]} 1805 | update_config_file 1806 | message_box "Regenerate keys" "All keys has been regenerated." 1807 | } 1808 | 1809 | function restore_defaults_menu { 1810 | whiptail \ 1811 | --clear \ 1812 | --backtitle "$BACKTITLE" \ 1813 | --title "Restore Default Config" \ 1814 | --yesno "Are you sure to restore default configuration?" \ 1815 | $HEIGHT $WIDTH \ 1816 | 3>&1 1>&2 2>&3 1817 | if [[ $? -ne 0 ]]; then 1818 | return 1819 | fi 1820 | restore_defaults 1821 | update_config_file 1822 | message_box "Restore Default Config" "All configurations has been restored to their defaults." 1823 | } 1824 | 1825 | function configuration_menu { 1826 | local selection 1827 | while true; do 1828 | selection=$(whiptail --clear --backtitle "$BACKTITLE" --title "Configuration" \ 1829 | --menu "Select an option:" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 1830 | "1" "Core" \ 1831 | "2" "Server Address" \ 1832 | "3" "Transport" \ 1833 | "4" "SNI Domain" \ 1834 | "5" "Security" \ 1835 | "6" "Port" \ 1836 | "7" "Safe Internet" \ 1837 | "8" "WARP" \ 1838 | "9" "Telegram Bot" \ 1839 | "10" "Restart Services" \ 1840 | "11" "Regenerate Keys" \ 1841 | "12" "Restore Defaults" \ 1842 | "13" "Create Backup" \ 1843 | "14" "Restore Backup" \ 1844 | 3>&1 1>&2 2>&3) 1845 | if [[ $? -ne 0 ]]; then 1846 | break 1847 | fi 1848 | case $selection in 1849 | 1 ) 1850 | config_core_menu 1851 | ;; 1852 | 2 ) 1853 | config_server_menu 1854 | ;; 1855 | 3 ) 1856 | config_transport_menu 1857 | ;; 1858 | 4 ) 1859 | config_sni_domain_menu 1860 | ;; 1861 | 5 ) 1862 | config_security_menu 1863 | ;; 1864 | 6 ) 1865 | config_port_menu 1866 | ;; 1867 | 7 ) 1868 | config_safenet_menu 1869 | ;; 1870 | 8 ) 1871 | config_warp_menu 1872 | ;; 1873 | 9 ) 1874 | config_tgbot_menu 1875 | ;; 1876 | 10 ) 1877 | restart_menu 1878 | ;; 1879 | 11 ) 1880 | regenerate_menu 1881 | ;; 1882 | 12 ) 1883 | restore_defaults_menu 1884 | ;; 1885 | 13 ) 1886 | backup_menu 1887 | ;; 1888 | 14 ) 1889 | restore_backup_menu 1890 | ;; 1891 | esac 1892 | done 1893 | } 1894 | 1895 | function config_core_menu { 1896 | local core 1897 | while true; do 1898 | core=$(whiptail --clear --backtitle "$BACKTITLE" --title "Core" \ 1899 | --radiolist --noitem "Select a core engine:" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 1900 | "xray" "$([[ "${config[core]}" == 'xray' ]] && echo 'on' || echo 'off')" \ 1901 | "sing-box" "$([[ "${config[core]}" == 'sing-box' ]] && echo 'on' || echo 'off')" \ 1902 | 3>&1 1>&2 2>&3) 1903 | if [[ $? -ne 0 ]]; then 1904 | break 1905 | fi 1906 | if [[ ${core} == 'xray' && ${config[transport]} == 'tuic' ]]; then 1907 | message_box 'Invalid Configuration' 'You cannot use "xray" core with "tuic" transport. Change core to "sing-box" or use other transports' 1908 | continue 1909 | fi 1910 | if [[ ${core} == 'xray' && ${config[transport]} == 'hysteria2' ]]; then 1911 | message_box 'Invalid Configuration' 'You cannot use "xray" core with "hysteria2" transport. Change core to "sing-box" or use other transports' 1912 | continue 1913 | fi 1914 | if [[ ${core} == 'xray' && ${config[transport]} == 'shadowtls' ]]; then 1915 | message_box 'Invalid Configuration' 'You cannot use "xray" core with "shadowtls" transport. Change core to "sing-box" or use other transports' 1916 | continue 1917 | fi 1918 | config[core]=$core 1919 | update_config_file 1920 | break 1921 | done 1922 | } 1923 | 1924 | function config_server_menu { 1925 | local server 1926 | while true; do 1927 | server=$(whiptail --clear --backtitle "$BACKTITLE" --title "Server Address" \ 1928 | --inputbox "Enter Server IP or Domain:" $HEIGHT $WIDTH "${config["server"]}" \ 1929 | 3>&1 1>&2 2>&3) 1930 | if [[ $? -ne 0 ]]; then 1931 | break 1932 | fi 1933 | if [[ ! ${server} =~ ${regex[domain]} && ${config[security]} == 'letsencrypt' ]]; then 1934 | message_box 'Invalid Configuration' 'You have to assign a valid domain to server if you want to use "letsencrypt" certificate.' 1935 | continue 1936 | fi 1937 | if [[ -z ${server} ]]; then 1938 | server="${defaults[server]}" 1939 | fi 1940 | config[server]="${server}" 1941 | if [[ ${config[security]} != 'reality' && ${config[transport]} != 'shadowtls' ]]; then 1942 | config[domain]="${server}" 1943 | fi 1944 | update_config_file 1945 | break 1946 | done 1947 | } 1948 | 1949 | function config_transport_menu { 1950 | local transport 1951 | while true; do 1952 | transport=$(whiptail --clear --backtitle "$BACKTITLE" --title "Transport" \ 1953 | --radiolist --noitem "Select a transport protocol:" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 1954 | "tcp" "$([[ "${config[transport]}" == 'tcp' ]] && echo 'on' || echo 'off')" \ 1955 | "http" "$([[ "${config[transport]}" == 'http' ]] && echo 'on' || echo 'off')" \ 1956 | "grpc" "$([[ "${config[transport]}" == 'grpc' ]] && echo 'on' || echo 'off')" \ 1957 | "ws" "$([[ "${config[transport]}" == 'ws' ]] && echo 'on' || echo 'off')" \ 1958 | "tuic" "$([[ "${config[transport]}" == 'tuic' ]] && echo 'on' || echo 'off')" \ 1959 | "hysteria2" "$([[ "${config[transport]}" == 'hysteria2' ]] && echo 'on' || echo 'off')" \ 1960 | "shadowtls" "$([[ "${config[transport]}" == 'shadowtls' ]] && echo 'on' || echo 'off')" \ 1961 | 3>&1 1>&2 2>&3) 1962 | if [[ $? -ne 0 ]]; then 1963 | break 1964 | fi 1965 | if [[ ${transport} == 'ws' && ${config[security]} == 'reality' ]]; then 1966 | message_box 'Invalid Configuration' 'You cannot use "ws" transport with "reality" TLS certificate. Use other transports or change TLS certifcate to "letsencrypt" or "selfsigned"' 1967 | continue 1968 | fi 1969 | if [[ ${transport} == 'tuic' && ${config[security]} == 'reality' ]]; then 1970 | message_box 'Invalid Configuration' 'You cannot use "tuic" transport with "reality" TLS certificate. Use other transports or change TLS certifcate to "letsencrypt" or "selfsigned"' 1971 | continue 1972 | fi 1973 | if [[ ${transport} == 'tuic' && ${config[core]} == 'xray' ]]; then 1974 | message_box 'Invalid Configuration' 'You cannot use "tuic" transport with "xray" core. Use other transports or change core to "sing-box"' 1975 | continue 1976 | fi 1977 | if [[ ${transport} == 'hysteria2' && ${config[security]} == 'reality' ]]; then 1978 | message_box 'Invalid Configuration' 'You cannot use "hysteria2" transport with "reality" TLS certificate. Use other transports or change TLS certifcate to "letsencrypt" or "selfsigned"' 1979 | continue 1980 | fi 1981 | if [[ ${transport} == 'hysteria2' && ${config[core]} == 'xray' ]]; then 1982 | message_box 'Invalid Configuration' 'You cannot use "hysteria2" transport with "xray" core. Use other transports or change core to "sing-box"' 1983 | continue 1984 | fi 1985 | if [[ ${transport} == 'shadowtls' && ${config[core]} == 'xray' ]]; then 1986 | message_box 'Invalid Configuration' 'You cannot use "shadowtls" transport with "xray" core. Use other transports or change core to "sing-box"' 1987 | continue 1988 | fi 1989 | config[transport]=$transport 1990 | update_config_file 1991 | break 1992 | done 1993 | } 1994 | 1995 | function config_sni_domain_menu { 1996 | local sni_domain 1997 | while true; do 1998 | sni_domain=$(whiptail --clear --backtitle "$BACKTITLE" --title "SNI Domain" \ 1999 | --inputbox "Enter SNI domain:" $HEIGHT $WIDTH "${config[domain]}" \ 2000 | 3>&1 1>&2 2>&3) 2001 | if [[ $? -ne 0 ]]; then 2002 | break 2003 | fi 2004 | if [[ ! $sni_domain =~ ${regex[domain_port]} ]]; then 2005 | message_box "Invalid Domain" '"'"${sni_domain}"'" in not a valid domain.' 2006 | continue 2007 | fi 2008 | config[domain]=$sni_domain 2009 | update_config_file 2010 | break 2011 | done 2012 | } 2013 | 2014 | function config_security_menu { 2015 | local security 2016 | local free_80=true 2017 | while true; do 2018 | security=$(whiptail --clear --backtitle "$BACKTITLE" --title "Security Type" \ 2019 | --radiolist --noitem "Select a security type:" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 2020 | "reality" "$([[ "${config[security]}" == 'reality' ]] && echo 'on' || echo 'off')" \ 2021 | "letsencrypt" "$([[ "${config[security]}" == 'letsencrypt' ]] && echo 'on' || echo 'off')" \ 2022 | "selfsigned" "$([[ "${config[security]}" == 'selfsigned' ]] && echo 'on' || echo 'off')" \ 2023 | 3>&1 1>&2 2>&3) 2024 | if [[ $? -ne 0 ]]; then 2025 | break 2026 | fi 2027 | if [[ ! ${config[server]} =~ ${regex[domain]} && ${security} == 'letsencrypt' ]]; then 2028 | message_box 'Invalid Configuration' 'You have to assign a valid domain to server if you want to use "letsencrypt" as security type' 2029 | continue 2030 | fi 2031 | if [[ ${config[transport]} == 'ws' && ${security} == 'reality' ]]; then 2032 | message_box 'Invalid Configuration' 'You cannot use "reality" TLS certificate with "ws" transport protocol. Change TLS certifcate to "letsencrypt" or "selfsigned" or use other transport protocols' 2033 | continue 2034 | fi 2035 | if [[ ${config[transport]} == 'tuic' && ${security} == 'reality' ]]; then 2036 | message_box 'Invalid Configuration' 'You cannot use "reality" TLS certificate with "tuic" transport. Change TLS certifcate to "letsencrypt" or "selfsigned" or use other transports' 2037 | continue 2038 | fi 2039 | if [[ ${config[transport]} == 'hysteria2' && ${security} == 'reality' ]]; then 2040 | message_box 'Invalid Configuration' 'You cannot use "reality" TLS certificate with "hysteria2" transport. Change TLS certifcate to "letsencrypt" or "selfsigned" or use other transports' 2041 | continue 2042 | fi 2043 | if [[ ${security} == 'letsencrypt' && ${config[port]} -ne 443 ]]; then 2044 | if lsof -i :80 >/dev/null 2>&1; then 2045 | free_80=false 2046 | for container in $(${docker_cmd} -p ${compose_project} ps -q); do 2047 | if docker port "${container}" | grep '0.0.0.0:80' >/dev/null 2>&1; then 2048 | free_80=true 2049 | break 2050 | fi 2051 | done 2052 | fi 2053 | if [[ ${free_80} != 'true' ]]; then 2054 | message_box 'Port 80 must be free if you want to use "letsencrypt" as the security option.' 2055 | continue 2056 | fi 2057 | fi 2058 | if [[ ${security} != 'reality' && ${config[transport]} != 'shadowtls' ]]; then 2059 | config[domain]="${config[server]}" 2060 | fi 2061 | if [[ ${security} == 'reality' || ${config[transport]} == 'shadowtls' ]]; then 2062 | config[domain]="${defaults[domain]}" 2063 | fi 2064 | config[security]="${security}" 2065 | update_config_file 2066 | break 2067 | done 2068 | } 2069 | 2070 | function config_port_menu { 2071 | local port 2072 | while true; do 2073 | port=$(whiptail --clear --backtitle "$BACKTITLE" --title "Port" \ 2074 | --inputbox "Enter port number:" $HEIGHT $WIDTH "${config[port]}" \ 2075 | 3>&1 1>&2 2>&3) 2076 | if [[ $? -ne 0 ]]; then 2077 | break 2078 | fi 2079 | if [[ ! $port =~ ${regex[port]} ]]; then 2080 | message_box "Invalid Port" "Port must be an integer" 2081 | continue 2082 | fi 2083 | if ((port < 1 || port > 65535)); then 2084 | message_box "Invalid Port" "Port must be between 1 to 65535" 2085 | continue 2086 | fi 2087 | config[port]=$port 2088 | update_config_file 2089 | break 2090 | done 2091 | } 2092 | 2093 | function config_safenet_menu { 2094 | local safenet 2095 | safenet=$(whiptail --clear --backtitle "$BACKTITLE" --title "Safe Internet" \ 2096 | --radiolist --noitem "Enable blocking malware and adult content" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 2097 | "Enable" "$([[ "${config[safenet]}" == 'ON' ]] && echo 'on' || echo 'off')" \ 2098 | "Disable" "$([[ "${config[safenet]}" == 'OFF' ]] && echo 'on' || echo 'off')" \ 2099 | 3>&1 1>&2 2>&3) 2100 | if [[ $? -ne 0 ]]; then 2101 | return 2102 | fi 2103 | config[safenet]=$([[ $safenet == 'Enable' ]] && echo ON || echo OFF) 2104 | update_config_file 2105 | } 2106 | 2107 | function config_warp_menu { 2108 | local warp 2109 | local warp_license 2110 | local error 2111 | local temp_file 2112 | local exit_code 2113 | local old_warp=${config[warp]} 2114 | local old_warp_license=${config[warp_license]} 2115 | while true; do 2116 | warp=$(whiptail --clear --backtitle "$BACKTITLE" --title "WARP" \ 2117 | --radiolist --noitem "Enable WARP:" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 2118 | "Enable" "$([[ "${config[warp]}" == 'ON' ]] && echo 'on' || echo 'off')" \ 2119 | "Disable" "$([[ "${config[warp]}" == 'OFF' ]] && echo 'on' || echo 'off')" \ 2120 | 3>&1 1>&2 2>&3) 2121 | if [[ $? -ne 0 ]]; then 2122 | break 2123 | fi 2124 | if [[ $warp == 'Disable' ]]; then 2125 | config[warp]=OFF 2126 | if [[ -n ${config[warp_id]} && -n ${config[warp_token]} ]]; then 2127 | warp_delete_account "${config[warp_id]}" "${config[warp_token]}" 2128 | fi 2129 | return 2130 | fi 2131 | if [[ -z ${config[warp_private_key]} || \ 2132 | -z ${config[warp_token]} || \ 2133 | -z ${config[warp_id]} || \ 2134 | -z ${config[warp_client_id]} || \ 2135 | -z ${config[warp_interface_ipv4]} || \ 2136 | -z ${config[warp_interface_ipv6]} ]]; then 2137 | temp_file=$(mktemp) 2138 | warp_create_account > "${temp_file}" 2139 | exit_code=$? 2140 | error=$(< "${temp_file}") 2141 | rm -f "${temp_file}" 2142 | if [[ ${exit_code} -ne 0 ]]; then 2143 | message_box "WARP account creation error" "${error}" 2144 | continue 2145 | fi 2146 | fi 2147 | config[warp]=ON 2148 | while true; do 2149 | warp_license=$(whiptail --clear --backtitle "$BACKTITLE" --title "WARP+ License" \ 2150 | --inputbox "Enter WARP+ License:\nLeave blank if you only want to use free WARP account" $HEIGHT $WIDTH "${config[warp_license]}" \ 2151 | 3>&1 1>&2 2>&3) 2152 | if [[ $? -ne 0 ]]; then 2153 | break 2154 | fi 2155 | if [[ -n "${warp_license}" && ! $warp_license =~ ${regex[warp_license]} ]]; then 2156 | message_box "Invalid Input" "Invalid WARP+ License" 2157 | continue 2158 | fi 2159 | if [[ -n "${warp_license}" ]]; then 2160 | temp_file=$(mktemp) 2161 | warp_add_license "${config[warp_id]}" "${config[warp_token]}" "${warp_license}" > "${temp_file}" 2162 | exit_code=$? 2163 | error=$(< "${temp_file}") 2164 | rm -f "${temp_file}" 2165 | if [[ ${exit_code} -ne 0 ]]; then 2166 | message_box "WARP license error" "${error}" 2167 | continue 2168 | fi 2169 | fi 2170 | update_config_file 2171 | return 2172 | done 2173 | done 2174 | config[warp]=$old_warp 2175 | config[warp_license]=$old_warp_license 2176 | } 2177 | 2178 | function config_tgbot_menu { 2179 | local tgbot 2180 | local tgbot_token 2181 | local tgbot_admins 2182 | local old_tgbot=${config[tgbot]} 2183 | local old_tgbot_token=${config[tgbot_token]} 2184 | local old_tgbot_admins=${config[tgbot_admins]} 2185 | while true; do 2186 | tgbot=$(whiptail --clear --backtitle "$BACKTITLE" --title "Enable Telegram Bot" \ 2187 | --radiolist --noitem "Enable Telegram Bot:" $HEIGHT $WIDTH $CHOICE_HEIGHT \ 2188 | "Enable" "$([[ "${config[tgbot]}" == 'ON' ]] && echo 'on' || echo 'off')" \ 2189 | "Disable" "$([[ "${config[tgbot]}" == 'OFF' ]] && echo 'on' || echo 'off')" \ 2190 | 3>&1 1>&2 2>&3) 2191 | if [[ $? -ne 0 ]]; then 2192 | break 2193 | fi 2194 | if [[ $tgbot == 'Disable' ]]; then 2195 | config[tgbot]=OFF 2196 | update_config_file 2197 | return 2198 | fi 2199 | config[tgbot]=ON 2200 | while true; do 2201 | tgbot_token=$(whiptail --clear --backtitle "$BACKTITLE" --title "Telegram Bot Token" \ 2202 | --inputbox "Enter Telegram Bot Token:" $HEIGHT $WIDTH "${config[tgbot_token]}" \ 2203 | 3>&1 1>&2 2>&3) 2204 | if [[ $? -ne 0 ]]; then 2205 | break 2206 | fi 2207 | if [[ ! $tgbot_token =~ ${regex[tgbot_token]} ]]; then 2208 | message_box "Invalid Input" "Invalid Telegram Bot Token" 2209 | continue 2210 | fi 2211 | if ! curl -sSfL -m 3 "https://api.telegram.org/bot${tgbot_token}/getMe" >/dev/null 2>&1; then 2212 | message_box "Invalid Input" "Telegram Bot Token is incorrect. Check it again." 2213 | continue 2214 | fi 2215 | config[tgbot_token]=$tgbot_token 2216 | while true; do 2217 | tgbot_admins=$(whiptail --clear --backtitle "$BACKTITLE" --title "Telegram Bot Admins" \ 2218 | --inputbox "Enter Telegram Bot Admins (Seperate multiple admins by comma ',' without leading '@'):" $HEIGHT $WIDTH "${config[tgbot_admins]}" \ 2219 | 3>&1 1>&2 2>&3) 2220 | if [[ $? -ne 0 ]]; then 2221 | break 2222 | fi 2223 | if [[ ! $tgbot_admins =~ ${regex[tgbot_admins]} || $tgbot_admins =~ .+_$ || $tgbot_admins =~ .+_,.+ ]]; then 2224 | message_box "Invalid Input" "Invalid Username\nThe usernames must separated by ',' without leading '@' character or any extra space." 2225 | continue 2226 | fi 2227 | config[tgbot_admins]=$tgbot_admins 2228 | update_config_file 2229 | return 2230 | done 2231 | done 2232 | done 2233 | config[tgbot]=$old_tgbot 2234 | config[tgbot_token]=$old_tgbot_token 2235 | config[tgbot_admins]=$old_tgbot_admins 2236 | } 2237 | 2238 | function backup_menu { 2239 | local backup_password 2240 | local result 2241 | backup_password=$(whiptail \ 2242 | --clear \ 2243 | --backtitle "$BACKTITLE" \ 2244 | --title "Backup" \ 2245 | --inputbox "Choose a password for the backup file.\nLeave blank if you do not wish to set a password for the backup file." \ 2246 | $HEIGHT $WIDTH \ 2247 | 3>&1 1>&2 2>&3) 2248 | if [[ $? -ne 0 ]]; then 2249 | return 2250 | fi 2251 | if result=$(backup "${backup_password}" 2>&1); then 2252 | clear 2253 | echo "Backup has been create and uploaded successfully." 2254 | echo "You can download the backup file from here:" 2255 | echo "" 2256 | echo "${result}" 2257 | echo "" 2258 | echo "The URL is valid for 3 days." 2259 | echo 2260 | echo "Press Enter to return ..." 2261 | read 2262 | clear 2263 | else 2264 | message_box "Backup Failed" "${result}" 2265 | fi 2266 | } 2267 | 2268 | function restore_backup_menu { 2269 | local backup_file 2270 | local backup_password 2271 | local result 2272 | while true; do 2273 | backup_file=$(whiptail \ 2274 | --clear \ 2275 | --backtitle "$BACKTITLE" \ 2276 | --title "Restore Backup" \ 2277 | --inputbox "Enter backup file path or URL" \ 2278 | $HEIGHT $WIDTH \ 2279 | 3>&1 1>&2 2>&3) 2280 | if [[ $? -ne 0 ]]; then 2281 | break 2282 | fi 2283 | if [[ ! $backup_file =~ ${regex[file_path]} ]] && [[ ! $backup_file =~ ${regex[url]} ]]; then 2284 | message_box "Invalid Backup path of URL" "Backup file path or URL is not valid." 2285 | continue 2286 | fi 2287 | backup_password=$(whiptail \ 2288 | --clear \ 2289 | --backtitle "$BACKTITLE" \ 2290 | --title "Restore Backup" \ 2291 | --inputbox "Enter backup file password.\nLeave blank if there is no password." \ 2292 | $HEIGHT $WIDTH \ 2293 | 3>&1 1>&2 2>&3) 2294 | if [[ $? -ne 0 ]]; then 2295 | continue 2296 | fi 2297 | if result=$(restore "${backup_file}" "${backup_password}" 2>&1); then 2298 | parse_config_file 2299 | parse_users_file 2300 | build_config 2301 | update_config_file 2302 | update_users_file 2303 | message_box "Backup Restore Successful" "Backup has been restored successfully." 2304 | args[restart]=true 2305 | break 2306 | else 2307 | message_box "Backup Restore Failed" "${result}" 2308 | fi 2309 | done 2310 | } 2311 | 2312 | function restart_docker_compose { 2313 | ${docker_cmd} --project-directory ${config_path} -p ${compose_project} down --remove-orphans --timeout 2 || true 2314 | ${docker_cmd} --project-directory ${config_path} -p ${compose_project} up --build -d --remove-orphans --build 2315 | } 2316 | 2317 | function restart_tgbot_compose { 2318 | ${docker_cmd} --project-directory ${config_path}/tgbot -p ${tgbot_project} down --remove-orphans --timeout 2 || true 2319 | ${docker_cmd} --project-directory ${config_path}/tgbot -p ${tgbot_project} up --build -d --remove-orphans --build 2320 | } 2321 | 2322 | function restart_container { 2323 | if [[ -z "$(${docker_cmd} ls | grep "${path[compose]}" | grep running || true)" ]]; then 2324 | restart_docker_compose 2325 | return 2326 | fi 2327 | if ${docker_cmd} --project-directory ${config_path} -p ${compose_project} ps --services "$1" | grep "$1"; then 2328 | ${docker_cmd} --project-directory ${config_path} -p ${compose_project} restart --timeout 2 "$1" 2329 | fi 2330 | } 2331 | 2332 | function warp_api { 2333 | local verb=$1 2334 | local resource=$2 2335 | local data=$3 2336 | local token=$4 2337 | local team_token=$5 2338 | local endpoint=https://api.cloudflareclient.com/v0a1922 2339 | local temp_file 2340 | local error 2341 | local command 2342 | local headers=( 2343 | "User-Agent: okhttp/3.12.1" 2344 | "CF-Client-Version: a-6.3-1922" 2345 | "Content-Type: application/json" 2346 | ) 2347 | temp_file=$(mktemp) 2348 | if [[ -n ${token} ]]; then 2349 | headers+=("Authorization: Bearer ${token}") 2350 | fi 2351 | if [[ -n ${team_token} ]]; then 2352 | headers+=("Cf-Access-Jwt-Assertion: ${team_token}") 2353 | fi 2354 | command="curl -sLX ${verb} -m 3 -w '%{http_code}' -o ${temp_file} ${endpoint}${resource}" 2355 | for header in "${headers[@]}"; do 2356 | command+=" -H '${header}'" 2357 | done 2358 | if [[ -n ${data} ]]; then 2359 | command+=" -d '${data}'" 2360 | fi 2361 | response_code=$(( $(eval "${command}" || true) )) 2362 | response_body=$(cat "${temp_file}") 2363 | rm -f "${temp_file}" 2364 | if [[ response_code -eq 0 ]]; then 2365 | return 1 2366 | fi 2367 | if [[ response_code -gt 399 ]]; then 2368 | error=$(echo "${response_body}" | jq -r '.errors[0].message' 2> /dev/null || true) 2369 | if [[ ${error} != 'null' ]]; then 2370 | echo "${error}" 2371 | fi 2372 | return 2 2373 | fi 2374 | echo "${response_body}" 2375 | } 2376 | 2377 | function warp_create_account { 2378 | local response 2379 | docker run --rm -it -v "${config_path}":/data "${image[wgcf]}" register --config /data/wgcf-account.toml --accept-tos 2380 | if [[ $? -ne 0 || ! -r ${config_path}/wgcf-account.toml ]]; then 2381 | echo "WARP account creation has been failed!" 2382 | return 1 2383 | fi 2384 | config[warp_token]=$(cat ${config_path}/wgcf-account.toml | grep 'access_token' | cut -d "'" -f2) 2385 | config[warp_id]=$(cat ${config_path}/wgcf-account.toml | grep 'device_id' | cut -d "'" -f2) 2386 | config[warp_private_key]=$(cat ${config_path}/wgcf-account.toml | grep 'private_key' | cut -d "'" -f2) 2387 | rm -f ${config_path}/wgcf-account.toml 2388 | response=$(warp_api "GET" "/reg/${config[warp_id]}" "" "${config[warp_token]}") 2389 | if [[ $? -ne 0 ]]; then 2390 | if [[ -n ${response} ]]; then 2391 | echo "${response}" 2392 | fi 2393 | return 1 2394 | fi 2395 | config[warp_client_id]=$(echo "${response}" | jq -r '.config.client_id') 2396 | config[warp_interface_ipv4]=$(echo "${response}" | jq -r '.config.interface.addresses.v4') 2397 | config[warp_interface_ipv6]=$(echo "${response}" | jq -r '.config.interface.addresses.v6') 2398 | update_config_file 2399 | } 2400 | 2401 | function warp_add_license { 2402 | local id=$1 2403 | local token=$2 2404 | local license=$3 2405 | local data 2406 | local response 2407 | data='{"license": "'$license'"}' 2408 | response=$(warp_api "PUT" "/reg/${id}/account" "${data}" "${token}") 2409 | if [[ $? -ne 0 ]]; then 2410 | if [[ -n ${response} ]]; then 2411 | echo "${response}" 2412 | fi 2413 | return 1 2414 | fi 2415 | config[warp_license]=${license} 2416 | update_config_file 2417 | } 2418 | 2419 | function warp_delete_account { 2420 | local id=$1 2421 | local token=$2 2422 | warp_api "DELETE" "/reg/${id}" "" "${token}" >/dev/null 2>&1 || true 2423 | config[warp_private_key]="" 2424 | config[warp_token]="" 2425 | config[warp_id]="" 2426 | config[warp_client_id]="" 2427 | config[warp_interface_ipv4]="" 2428 | config[warp_interface_ipv6]="" 2429 | update_config_file 2430 | } 2431 | 2432 | function warp_decode_reserved { 2433 | client_id=$1 2434 | reserved=$(echo "${client_id}" | base64 -d | xxd -p | fold -w2 | while read HEX; do printf '%d ' "0x${HEX}"; done | awk '{print "["$1", "$2", "$3"]"}') 2435 | echo "${reserved}" 2436 | } 2437 | 2438 | function check_reload { 2439 | declare -A restart 2440 | generate_config 2441 | for key in "${!path[@]}"; do 2442 | if [[ "${md5["$key"]}" != $(get_md5 "${path[$key]}") ]]; then 2443 | restart["${service["$key"]}"]='true' 2444 | md5["$key"]=$(get_md5 "${path[$key]}") 2445 | fi 2446 | done 2447 | if [[ "${restart[tgbot]}" == 'true' && "${config[tgbot]}" == 'ON' ]]; then 2448 | restart_tgbot_compose 2449 | fi 2450 | if [[ "${config[tgbot]}" == 'OFF' ]]; then 2451 | ${docker_cmd} --project-directory ${config_path}/tgbot -p ${tgbot_project} down --remove-orphans --timeout 2 >/dev/null 2>&1 || true 2452 | fi 2453 | if [[ "${restart[compose]}" == 'true' ]]; then 2454 | restart_docker_compose 2455 | return 2456 | fi 2457 | for key in "${!restart[@]}"; do 2458 | if [[ $key != 'none' && $key != 'tgbot' ]]; then 2459 | restart_container "${key}" 2460 | fi 2461 | done 2462 | } 2463 | 2464 | function message_box { 2465 | local title=$1 2466 | local message=$2 2467 | whiptail \ 2468 | --clear \ 2469 | --backtitle "$BACKTITLE" \ 2470 | --title "$title" \ 2471 | --msgbox "$message" \ 2472 | $HEIGHT $WIDTH \ 2473 | 3>&1 1>&2 2>&3 2474 | } 2475 | 2476 | function get_md5 { 2477 | local file_path 2478 | file_path=$1 2479 | md5sum "${file_path}" 2>/dev/null | cut -f1 -d' ' || true 2480 | } 2481 | 2482 | function generate_file_list { 2483 | path[config]="${config_path}/config" 2484 | path[users]="${config_path}/users" 2485 | path[compose]="${config_path}/docker-compose.yml" 2486 | path[engine]="${config_path}/engine.conf" 2487 | path[haproxy]="${config_path}/haproxy.cfg" 2488 | path[certbot_deployhook]="${config_path}/certbot/deployhook.sh" 2489 | path[certbot_dockerfile]="${config_path}/certbot/Dockerfile" 2490 | path[certbot_startup]="${config_path}/certbot/startup.sh" 2491 | path[server_pem]="${config_path}/certificate/server.pem" 2492 | path[server_key]="${config_path}/certificate/server.key" 2493 | path[server_crt]="${config_path}/certificate/server.crt" 2494 | path[tgbot_script]="${config_path}/tgbot/tgbot.py" 2495 | path[tgbot_dockerfile]="${config_path}/tgbot/Dockerfile" 2496 | path[tgbot_compose]="${config_path}/tgbot/docker-compose.yml" 2497 | 2498 | service[config]='none' 2499 | service[users]='none' 2500 | service[compose]='compose' 2501 | service[engine]='engine' 2502 | service[haproxy]='haproxy' 2503 | service[certbot_deployhook]='certbot' 2504 | service[certbot_dockerfile]='compose' 2505 | service[certbot_startup]='certbot' 2506 | service[server_pem]='haproxy' 2507 | service[server_key]='engine' 2508 | service[server_crt]='engine' 2509 | service[tgbot_script]='tgbot' 2510 | service[tgbot_dockerfile]='compose' 2511 | service[tgbot_compose]='tgbot' 2512 | 2513 | for key in "${!path[@]}"; do 2514 | md5["$key"]=$(get_md5 "${path[$key]}") 2515 | done 2516 | } 2517 | 2518 | function tune_kernel { 2519 | cat >/etc/sysctl.d/99-reality-ezpz.conf </dev/null 2>&1 || true 2542 | } 2543 | 2544 | function configure_docker { 2545 | local docker_config="/etc/docker/daemon.json" 2546 | local config_modified=false 2547 | local temp_file 2548 | temp_file=$(mktemp) 2549 | if [[ ! -f "${docker_config}" ]] || [[ ! -s "${docker_config}" ]]; then 2550 | echo '{"experimental": true, "ip6tables": true}' | jq . > "${docker_config}" 2551 | config_modified=true 2552 | else 2553 | if ! jq . "${docker_config}" &> /dev/null; then 2554 | echo '{"experimental": true, "ip6tables": true}' | jq . > "${docker_config}" 2555 | config_modified=true 2556 | else 2557 | if jq 'if .experimental != true or .ip6tables != true then .experimental = true | .ip6tables = true else . end' "${docker_config}" | jq . > "${temp_file}"; then 2558 | if ! cmp --silent "${docker_config}" "${temp_file}"; then 2559 | mv "${temp_file}" "${docker_config}" 2560 | config_modified=true 2561 | fi 2562 | fi 2563 | fi 2564 | fi 2565 | rm -f "${temp_file}" 2566 | if [[ "${config_modified}" = true ]] || ! systemctl is-active --quiet docker; then 2567 | sudo systemctl restart docker || true 2568 | fi 2569 | } 2570 | 2571 | parse_args "$@" || show_help 2572 | if [[ $EUID -ne 0 ]]; then 2573 | echo "This script must be run as root." 2574 | exit 1 2575 | fi 2576 | if [[ ${args[backup]} == true ]]; then 2577 | if [[ -n ${args[backup_password]} ]]; then 2578 | backup_url=$(backup "${args[backup_password]}") 2579 | else 2580 | backup_url=$(backup) 2581 | fi 2582 | if [[ $? -eq 0 ]]; then 2583 | echo "Backup created successfully. You can download the backup file from this address:" 2584 | echo "${backup_url}" 2585 | echo "The URL is valid for 3 days." 2586 | exit 0 2587 | fi 2588 | fi 2589 | if [[ -n ${args[restore]} ]]; then 2590 | if [[ -n ${args[backup_password]} ]]; then 2591 | restore "${args[restore]}" "${args[backup_password]}" 2592 | else 2593 | restore "${args[restore]}" 2594 | fi 2595 | if [[ $? -eq 0 ]]; then 2596 | args[restart]=true 2597 | echo "Backup has been restored successfully." 2598 | fi 2599 | echo "Press Enter to continue ..." 2600 | read 2601 | clear 2602 | fi 2603 | generate_file_list 2604 | install_packages 2605 | install_docker 2606 | configure_docker 2607 | upgrade 2608 | parse_config_file 2609 | parse_users_file 2610 | build_config 2611 | update_config_file 2612 | update_users_file 2613 | tune_kernel 2614 | 2615 | if [[ ${args[menu]} == 'true' ]]; then 2616 | set +e 2617 | main_menu 2618 | set -e 2619 | fi 2620 | if [[ ${args[restart]} == 'true' ]]; then 2621 | restart_docker_compose 2622 | if [[ ${config[tgbot]} == 'ON' ]]; then 2623 | restart_tgbot_compose 2624 | fi 2625 | fi 2626 | if [[ -z "$(${docker_cmd} ls | grep "${path[compose]}" | grep running || true)" ]]; then 2627 | restart_docker_compose 2628 | fi 2629 | if [[ -z "$(${docker_cmd} ls | grep "${path[tgbot_compose]}" | grep running || true)" && ${config[tgbot]} == 'ON' ]]; then 2630 | restart_tgbot_compose 2631 | fi 2632 | if [[ ${args[server-config]} == true ]]; then 2633 | show_server_config 2634 | exit 0 2635 | fi 2636 | if [[ -n ${args[list_users]} ]]; then 2637 | for user in "${!users[@]}"; do 2638 | echo "${user}" 2639 | done 2640 | exit 0 2641 | fi 2642 | if [[ ${#users[@]} -eq 1 ]]; then 2643 | username="${!users[@]}" 2644 | fi 2645 | if [[ -n ${args[show_config]} ]]; then 2646 | username="${args[show_config]}" 2647 | if [[ -z "${users["${username}"]}" ]]; then 2648 | echo 'User "'"$username"'" does not exists.' 2649 | exit 1 2650 | fi 2651 | fi 2652 | if [[ -n ${args[add_user]} ]]; then 2653 | username="${args[add_user]}" 2654 | fi 2655 | if [[ -n $username ]]; then 2656 | print_client_configuration "${username}" 2657 | fi 2658 | echo "Command has been executed successfully!" 2659 | exit 0 2660 | --------------------------------------------------------------------------------