504 lines
17 KiB
Bash
Executable File
504 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||
# shellcheck source=../lib/common.sh
|
||
source "${PROJECT_ROOT}/lib/common.sh"
|
||
|
||
LOG_FILE="/var/log/wireguard-server-install.log"
|
||
NON_INTERACTIVE=0
|
||
|
||
WG_INTERFACE="wg0"
|
||
WG_PORT="51820"
|
||
WG_NETWORK="10.66.66.0/24"
|
||
WG_ADDRESS="10.66.66.1/24"
|
||
SERVER_PUBLIC_IP=""
|
||
SERVER_DNS="1.1.1.1"
|
||
DEFAULT_IFACE=""
|
||
|
||
GUI_ENABLE="yes"
|
||
GUI_HOST=""
|
||
GUI_PORT="5000"
|
||
GUI_USER="admin"
|
||
GUI_PASSWORD=""
|
||
GUI_PASSWORD_GENERATED=0
|
||
GUI_SESSION_SECRET=""
|
||
GUI_RESET_DB="no"
|
||
|
||
usage() {
|
||
cat <<'USAGE'
|
||
Установка WireGuard-сервера и GUI (Debian/Ubuntu).
|
||
Каждый запуск выполняет полный reset прошлой инсталляции и поднимает все с нуля.
|
||
|
||
Использование:
|
||
install_server.sh [опции]
|
||
|
||
Опции:
|
||
--non-interactive Режим без вопросов (используются аргументы/дефолты)
|
||
--wg-interface <name> Имя интерфейса WireGuard (по умолчанию: wg0)
|
||
--wg-port <port> Порт WireGuard (по умолчанию: 51820)
|
||
--wg-network <cidr> Подсеть VPN (по умолчанию: 10.66.66.0/24)
|
||
--wg-address <cidr> Адрес сервера в VPN (по умолчанию: 10.66.66.1/24)
|
||
--server-public-ip <ip> Публичный IP сервера
|
||
--server-dns <ip> DNS для клиентов (по умолчанию: 1.1.1.1)
|
||
--default-iface <iface> Внешний интерфейс для NAT
|
||
|
||
--gui-enable <yes|no> Включить GUI wireguard-ui (по умолчанию: yes)
|
||
--gui-host <host> Домен/IP для открытия GUI
|
||
--gui-port <port> Порт GUI (по умолчанию: 5000)
|
||
--gui-user <user> Логин GUI (по умолчанию: admin)
|
||
--gui-password <pass> Пароль GUI (если не указан, будет запрос)
|
||
--gui-reset-db <yes|no> Устарело: теперь reset GUI выполняется автоматически
|
||
|
||
-h, --help Показать помощь
|
||
USAGE
|
||
}
|
||
|
||
on_error() {
|
||
local code=$?
|
||
log_error "Установка прервана с ошибкой (код: ${code}). Подробности в ${LOG_FILE}"
|
||
exit "$code"
|
||
}
|
||
trap on_error ERR
|
||
|
||
parse_args() {
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--non-interactive)
|
||
NON_INTERACTIVE=1; shift ;;
|
||
--wg-interface)
|
||
WG_INTERFACE="$2"; shift 2 ;;
|
||
--wg-port)
|
||
WG_PORT="$2"; shift 2 ;;
|
||
--wg-network)
|
||
WG_NETWORK="$2"; shift 2 ;;
|
||
--wg-address)
|
||
WG_ADDRESS="$2"; shift 2 ;;
|
||
--server-public-ip)
|
||
SERVER_PUBLIC_IP="$2"; shift 2 ;;
|
||
--server-dns)
|
||
SERVER_DNS="$2"; shift 2 ;;
|
||
--default-iface)
|
||
DEFAULT_IFACE="$2"; shift 2 ;;
|
||
--gui-enable)
|
||
GUI_ENABLE="$2"; shift 2 ;;
|
||
--gui-host)
|
||
GUI_HOST="$2"; shift 2 ;;
|
||
--gui-port)
|
||
GUI_PORT="$2"; shift 2 ;;
|
||
--gui-user)
|
||
GUI_USER="$2"; shift 2 ;;
|
||
--gui-password)
|
||
GUI_PASSWORD="$2"; shift 2 ;;
|
||
--gui-reset-db)
|
||
GUI_RESET_DB="$2"; shift 2 ;;
|
||
-h|--help)
|
||
usage; exit 0 ;;
|
||
*)
|
||
die "Неизвестный аргумент: $1"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
validate_inputs() {
|
||
is_valid_port "$WG_PORT" || die "Некорректный порт WireGuard: $WG_PORT"
|
||
is_valid_port "$GUI_PORT" || die "Некорректный порт GUI: $GUI_PORT"
|
||
[[ "$GUI_ENABLE" == "yes" || "$GUI_ENABLE" == "no" ]] || die "--gui-enable должен быть yes или no"
|
||
|
||
if [[ ! "$WG_NETWORK" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[12][0-9]|3[0-2])$ ]]; then
|
||
die "Некорректная сеть WG: $WG_NETWORK"
|
||
fi
|
||
|
||
if [[ ! "$WG_ADDRESS" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[12][0-9]|3[0-2])$ ]]; then
|
||
die "Некорректный адрес WG сервера: $WG_ADDRESS"
|
||
fi
|
||
}
|
||
|
||
reset_existing_install() {
|
||
log_warn "Выполняю полный reset предыдущей инсталляции WireGuard/GUI"
|
||
|
||
if [[ -d /etc/wireguard ]]; then
|
||
local conf iface
|
||
shopt -s nullglob
|
||
for conf in /etc/wireguard/*.conf; do
|
||
iface="$(basename "$conf" .conf)"
|
||
systemctl disable --now "wg-syncconf@${iface}.path" >/dev/null 2>&1 || true
|
||
systemctl stop "wg-syncconf@${iface}.service" >/dev/null 2>&1 || true
|
||
systemctl disable --now "wg-quick@${iface}.service" >/dev/null 2>&1 || true
|
||
wg-quick down "$iface" >/dev/null 2>&1 || true
|
||
done
|
||
shopt -u nullglob
|
||
|
||
rm -f /etc/wireguard/*.conf /etc/wireguard/*.key /etc/wireguard/wg-meta.env
|
||
log_info "Очищены конфиги/ключи WireGuard в /etc/wireguard"
|
||
fi
|
||
|
||
if command -v docker >/dev/null 2>&1; then
|
||
if [[ -f /opt/wireguard-ui/docker-compose.yml ]]; then
|
||
(cd /opt/wireguard-ui && docker compose down --remove-orphans >/dev/null 2>&1) || true
|
||
(cd /opt/wireguard-ui && docker-compose down --remove-orphans >/dev/null 2>&1) || true
|
||
fi
|
||
docker rm -f wireguard-ui >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
rm -rf /opt/wireguard-ui/db/* /opt/wireguard-ui/data/* /opt/wireguard-ui/docker-compose.yml
|
||
log_info "Очищено состояние GUI в /opt/wireguard-ui"
|
||
}
|
||
|
||
collect_inputs() {
|
||
if [[ -z "$DEFAULT_IFACE" ]]; then
|
||
DEFAULT_IFACE="$(detect_default_iface || true)"
|
||
fi
|
||
if [[ -z "$DEFAULT_IFACE" ]]; then
|
||
if ((NON_INTERACTIVE)); then
|
||
die "Не удалось определить внешний интерфейс. Передайте --default-iface"
|
||
fi
|
||
read -r -p "Введите внешний интерфейс (например eth0): " DEFAULT_IFACE
|
||
fi
|
||
|
||
if [[ -z "$SERVER_PUBLIC_IP" ]]; then
|
||
SERVER_PUBLIC_IP="$(detect_public_ip || true)"
|
||
fi
|
||
if [[ -z "$SERVER_PUBLIC_IP" ]]; then
|
||
if ((NON_INTERACTIVE)); then
|
||
die "Не удалось определить публичный IP. Передайте --server-public-ip"
|
||
fi
|
||
read -r -p "Введите публичный IP сервера: " SERVER_PUBLIC_IP
|
||
fi
|
||
|
||
if [[ -z "$GUI_HOST" ]]; then
|
||
GUI_HOST="$SERVER_PUBLIC_IP"
|
||
fi
|
||
|
||
if [[ "$GUI_ENABLE" == "yes" ]]; then
|
||
if [[ -z "$GUI_PASSWORD" ]]; then
|
||
GUI_PASSWORD="$(random_alnum 8)"
|
||
GUI_PASSWORD_GENERATED=1
|
||
log_warn "Пароль GUI не задан. Сгенерирован пароль (8 символов): ${GUI_PASSWORD}"
|
||
|
||
if (( ! NON_INTERACTIVE )); then
|
||
local replace_or_password=""
|
||
read -r -p "Хотите заменить сгенерированный пароль GUI? [y/N] (или введите пароль сразу): " replace_or_password
|
||
if [[ "$replace_or_password" =~ ^([yY][eE][sS]|[yY])$ ]]; then
|
||
local custom_gui_password=""
|
||
ask_secret "Введите новый пароль GUI (${GUI_USER})" custom_gui_password
|
||
if [[ -n "$custom_gui_password" ]]; then
|
||
GUI_PASSWORD="$custom_gui_password"
|
||
GUI_PASSWORD_GENERATED=0
|
||
else
|
||
log_warn "Пустой пароль не принят. Остается сгенерированный пароль."
|
||
fi
|
||
elif [[ -n "$replace_or_password" && ! "$replace_or_password" =~ ^([nN][oO]?|[nN])$ ]]; then
|
||
GUI_PASSWORD="$replace_or_password"
|
||
GUI_PASSWORD_GENERATED=0
|
||
fi
|
||
fi
|
||
fi
|
||
GUI_SESSION_SECRET="$(random_alnum 32)"
|
||
[[ "$GUI_RESET_DB" == "yes" || "$GUI_RESET_DB" == "no" ]] || die "--gui-reset-db должен быть yes или no"
|
||
if [[ "$GUI_RESET_DB" == "yes" ]]; then
|
||
log_warn "--gui-reset-db устарел: очистка GUI теперь выполняется автоматически на каждом запуске."
|
||
fi
|
||
fi
|
||
|
||
validate_inputs
|
||
}
|
||
|
||
install_packages() {
|
||
apt_install_if_missing \
|
||
wireguard wireguard-tools iproute2 iptables curl ca-certificates openssl \
|
||
qrencode docker.io
|
||
|
||
if apt-cache show docker-compose-plugin >/dev/null 2>&1; then
|
||
apt_install_if_missing docker-compose-plugin
|
||
elif apt-cache show docker-compose >/dev/null 2>&1; then
|
||
apt_install_if_missing docker-compose
|
||
else
|
||
log_warn "Не найден пакет docker-compose-plugin/docker-compose. Проверьте репозитории APT."
|
||
fi
|
||
}
|
||
|
||
setup_sysctl() {
|
||
local f="/etc/sysctl.d/99-wireguard-forwarding.conf"
|
||
cat > "$f" <<EOF_SYSCTL
|
||
net.ipv4.ip_forward=1
|
||
net.ipv6.conf.all.forwarding=1
|
||
EOF_SYSCTL
|
||
sysctl --system >/dev/null
|
||
log_success "IP forwarding включен"
|
||
}
|
||
|
||
setup_keys() {
|
||
mkdir -p /etc/wireguard
|
||
chmod 700 /etc/wireguard
|
||
|
||
local priv="/etc/wireguard/server_private.key"
|
||
local pub="/etc/wireguard/server_public.key"
|
||
|
||
if [[ ! -f "$priv" ]]; then
|
||
umask 077
|
||
wg genkey | tee "$priv" | wg pubkey > "$pub"
|
||
log_success "Сгенерированы ключи сервера"
|
||
else
|
||
if [[ ! -f "$pub" ]]; then
|
||
wg pubkey < "$priv" > "$pub"
|
||
fi
|
||
log_info "Ключи сервера уже существуют, переиспользую"
|
||
fi
|
||
|
||
safe_chmod_600 "$priv"
|
||
safe_chmod_600 "$pub"
|
||
}
|
||
|
||
setup_wg_config() {
|
||
local conf="/etc/wireguard/${WG_INTERFACE}.conf"
|
||
local server_priv
|
||
server_priv="$(cat /etc/wireguard/server_private.key)"
|
||
|
||
sed -e "s|__WG_ADDRESS__|${WG_ADDRESS}|g" \
|
||
-e "s|__WG_PORT__|${WG_PORT}|g" \
|
||
-e "s|__SERVER_PRIVATE_KEY__|${server_priv}|g" \
|
||
-e "s|__WG_INTERFACE__|${WG_INTERFACE}|g" \
|
||
-e "s|__WG_NETWORK__|${WG_NETWORK}|g" \
|
||
-e "s|__DEFAULT_IFACE__|${DEFAULT_IFACE}|g" \
|
||
"${PROJECT_ROOT}/templates/wg0.conf.template" > "$conf"
|
||
|
||
safe_chmod_600 "$conf"
|
||
log_success "Создан базовый конфиг: $conf"
|
||
}
|
||
|
||
setup_meta() {
|
||
local meta="/etc/wireguard/wg-meta.env"
|
||
touch "$meta"
|
||
safe_chmod_600 "$meta"
|
||
|
||
set_kv_in_file "WG_INTERFACE" "$WG_INTERFACE" "$meta"
|
||
set_kv_in_file "WG_PORT" "$WG_PORT" "$meta"
|
||
set_kv_in_file "WG_NETWORK" "$WG_NETWORK" "$meta"
|
||
set_kv_in_file "WG_ADDRESS" "$WG_ADDRESS" "$meta"
|
||
set_kv_in_file "SERVER_PUBLIC_IP" "$SERVER_PUBLIC_IP" "$meta"
|
||
set_kv_in_file "SERVER_DNS" "$SERVER_DNS" "$meta"
|
||
set_kv_in_file "DEFAULT_IFACE" "$DEFAULT_IFACE" "$meta"
|
||
}
|
||
|
||
setup_wg_service() {
|
||
systemd_enable_now "wg-quick@${WG_INTERFACE}.service"
|
||
log_success "WireGuard сервис запущен и включен в автозапуск"
|
||
}
|
||
|
||
setup_ufw_if_active() {
|
||
if command -v ufw >/dev/null 2>&1; then
|
||
if ufw status 2>/dev/null | grep -q "Status: active"; then
|
||
ufw allow "${WG_PORT}/udp" || true
|
||
if [[ "$GUI_ENABLE" == "yes" ]]; then
|
||
ufw allow "${GUI_PORT}/tcp" || true
|
||
fi
|
||
log_info "UFW активен: правила для WireGuard/GUI добавлены"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
install_peer_helper() {
|
||
mkdir -p /usr/local/lib/wireguard-automation/{lib,server}
|
||
install -m 640 "${PROJECT_ROOT}/lib/common.sh" /usr/local/lib/wireguard-automation/lib/common.sh
|
||
install -m 750 "${PROJECT_ROOT}/server/wg-peerctl.sh" /usr/local/lib/wireguard-automation/server/wg-peerctl.sh
|
||
|
||
cat > /usr/local/sbin/wg-peerctl <<'EOF_HELPER'
|
||
#!/usr/bin/env bash
|
||
exec /usr/local/lib/wireguard-automation/server/wg-peerctl.sh "$@"
|
||
EOF_HELPER
|
||
chmod 750 /usr/local/sbin/wg-peerctl
|
||
|
||
log_success "Установлен helper: /usr/local/sbin/wg-peerctl"
|
||
}
|
||
|
||
install_wg_sync_watcher() {
|
||
cat > /usr/local/sbin/wg-syncconf-apply <<'EOF_SYNC_APPLY'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
|
||
iface="${1:-wg0}"
|
||
conf="/etc/wireguard/${iface}.conf"
|
||
|
||
[[ -f "$conf" ]] || exit 0
|
||
|
||
if systemctl is-active --quiet "wg-quick@${iface}.service"; then
|
||
wg syncconf "$iface" <(wg-quick strip "$conf")
|
||
else
|
||
systemctl start "wg-quick@${iface}.service"
|
||
fi
|
||
EOF_SYNC_APPLY
|
||
chmod 750 /usr/local/sbin/wg-syncconf-apply
|
||
|
||
cat > /etc/systemd/system/wg-syncconf@.service <<'EOF_SYNC_SERVICE'
|
||
[Unit]
|
||
Description=Apply WireGuard config changes for %i
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
ExecStart=/usr/local/sbin/wg-syncconf-apply %i
|
||
EOF_SYNC_SERVICE
|
||
|
||
cat > /etc/systemd/system/wg-syncconf@.path <<'EOF_SYNC_PATH'
|
||
[Unit]
|
||
Description=Watch WireGuard config changes for %i
|
||
|
||
[Path]
|
||
PathExists=/etc/wireguard/%i.conf
|
||
PathModified=/etc/wireguard/%i.conf
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF_SYNC_PATH
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable --now "wg-syncconf@${WG_INTERFACE}.path"
|
||
log_success "Включено авто-применение изменений /etc/wireguard/${WG_INTERFACE}.conf -> ${WG_INTERFACE}"
|
||
}
|
||
|
||
setup_gui() {
|
||
[[ "$GUI_ENABLE" == "yes" ]] || { log_warn "GUI отключен (GUI_ENABLE=no)"; return; }
|
||
|
||
mkdir -p /opt/wireguard-ui/{db,data}
|
||
safe_chmod_700 /opt/wireguard-ui
|
||
|
||
cat > /opt/wireguard-ui/docker-compose.yml <<EOF_COMPOSE
|
||
services:
|
||
wireguard-ui:
|
||
image: ngoduykhanh/wireguard-ui:latest
|
||
container_name: wireguard-ui
|
||
restart: unless-stopped
|
||
ports:
|
||
- "${GUI_PORT}:5000"
|
||
environment:
|
||
- WGUI_USERNAME=${GUI_USER}
|
||
- WGUI_PASSWORD=${GUI_PASSWORD}
|
||
- SESSION_SECRET=${GUI_SESSION_SECRET}
|
||
- WGUI_ENDPOINT_ADDRESS=${SERVER_PUBLIC_IP}:${WG_PORT}
|
||
- WGUI_DNS=${SERVER_DNS}
|
||
- WGUI_CONFIG_FILE_PATH=/etc/wireguard/${WG_INTERFACE}.conf
|
||
- WGUI_SERVER_INTERFACE_ADDRESSES=${WG_ADDRESS}
|
||
- WGUI_SERVER_LISTEN_PORT=${WG_PORT}
|
||
- WGUI_DEFAULT_CLIENT_ALLOWED_IPS=0.0.0.0/0
|
||
- WGUI_MANAGE_START=true
|
||
- WGUI_MANAGE_RESTART=true
|
||
volumes:
|
||
- /etc/wireguard:/etc/wireguard
|
||
- /opt/wireguard-ui/db:/app/db
|
||
- /opt/wireguard-ui/data:/app/data
|
||
cap_add:
|
||
- NET_ADMIN
|
||
EOF_COMPOSE
|
||
|
||
systemd_enable_now docker.service
|
||
|
||
local compose_cmd=()
|
||
local compose_mode=""
|
||
if docker compose version >/dev/null 2>&1; then
|
||
compose_cmd=(docker compose)
|
||
compose_mode="plugin"
|
||
elif command -v docker-compose >/dev/null 2>&1; then
|
||
compose_cmd=(docker-compose)
|
||
compose_mode="legacy"
|
||
else
|
||
die "Не найден docker compose. Установите docker-compose-plugin или docker-compose."
|
||
fi
|
||
|
||
# На некоторых системах с legacy docker-compose (v1) при recreate может возникать
|
||
# KeyError: 'ContainerConfig'. Предварительно удаляем старый контейнер по имени.
|
||
if [[ "$compose_mode" == "legacy" ]]; then
|
||
docker rm -f wireguard-ui >/dev/null 2>&1 || true
|
||
|
||
# Удаляем возможные старые контейнеры вида <project>_wireguard-ui
|
||
# и контейнеры сервиса wireguard-ui по compose-label.
|
||
local legacy_ids legacy_names
|
||
legacy_ids="$(docker ps -aq --filter 'label=com.docker.compose.service=wireguard-ui' || true)"
|
||
if [[ -n "$legacy_ids" ]]; then
|
||
docker rm -f $legacy_ids >/dev/null 2>&1 || true
|
||
fi
|
||
|
||
legacy_names="$(docker ps -a --format '{{.Names}}' | grep -E '(^|[_-])wireguard-ui($|[_-])' || true)"
|
||
if [[ -n "$legacy_names" ]]; then
|
||
while IFS= read -r cname; do
|
||
[[ -n "$cname" ]] || continue
|
||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||
done <<< "$legacy_names"
|
||
fi
|
||
fi
|
||
|
||
(cd /opt/wireguard-ui && "${compose_cmd[@]}" up -d --remove-orphans)
|
||
log_success "GUI wireguard-ui запущен"
|
||
}
|
||
|
||
print_summary() {
|
||
local service_status gui_status
|
||
service_status="$(systemctl is-active "wg-quick@${WG_INTERFACE}" 2>/dev/null || true)"
|
||
gui_status="disabled"
|
||
|
||
if [[ "$GUI_ENABLE" == "yes" ]]; then
|
||
gui_status="$(docker ps --filter name=wireguard-ui --format '{{.Status}}' || true)"
|
||
[[ -n "$gui_status" ]] || gui_status="not running"
|
||
fi
|
||
|
||
cat <<EOF_SUMMARY
|
||
|
||
================ ИТОГОВАЯ СВОДКА ================
|
||
WireGuard сервис: ${service_status}
|
||
Интерфейс: ${WG_INTERFACE}
|
||
Порт WireGuard: ${WG_PORT}/udp
|
||
Конфиг сервера: /etc/wireguard/${WG_INTERFACE}.conf
|
||
Ключи сервера: /etc/wireguard/server_private.key, /etc/wireguard/server_public.key
|
||
Подсеть VPN: ${WG_NETWORK}
|
||
Маршрутизация NAT: через интерфейс ${DEFAULT_IFACE}
|
||
|
||
GUI: ${GUI_ENABLE}
|
||
GUI адрес: http://${GUI_HOST}:${GUI_PORT}
|
||
GUI логин: ${GUI_USER}
|
||
GUI статус: ${gui_status}
|
||
$(if [[ "$GUI_ENABLE" == "yes" && "$GUI_PASSWORD_GENERATED" -eq 1 ]]; then echo "GUI пароль: ${GUI_PASSWORD} (сгенерирован, рекомендуется заменить)"; fi)
|
||
|
||
Helper для peer: /usr/local/sbin/wg-peerctl
|
||
Auto-apply GUI->WG: enabled (wg-syncconf@${WG_INTERFACE}.path)
|
||
Лог установки: ${LOG_FILE}
|
||
=================================================
|
||
EOF_SUMMARY
|
||
|
||
if [[ "$GUI_ENABLE" == "yes" ]]; then
|
||
echo "Ссылка для входа в GUI: http://${GUI_HOST}:${GUI_PORT}"
|
||
fi
|
||
}
|
||
|
||
main() {
|
||
parse_args "$@"
|
||
|
||
require_root
|
||
check_os_supported
|
||
require_cmd ip
|
||
require_cmd awk
|
||
require_cmd sed
|
||
|
||
collect_inputs
|
||
|
||
log_info "Начинаю установку WireGuard-сервера (чистый запуск)"
|
||
reset_existing_install
|
||
install_packages
|
||
setup_sysctl
|
||
setup_keys
|
||
setup_wg_config
|
||
setup_meta
|
||
setup_wg_service
|
||
setup_ufw_if_active
|
||
install_peer_helper
|
||
install_wg_sync_watcher
|
||
setup_gui
|
||
print_summary
|
||
|
||
log_success "Установка завершена"
|
||
}
|
||
|
||
main "$@"
|