├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------