From 621ce6723a9066c1fcd538a3a17296e7c43851ee Mon Sep 17 00:00:00 2001 From: grendervill Date: Fri, 19 Jun 2026 15:23:17 +0300 Subject: [PATCH] =?UTF-8?q?version0.4=20=D0=9E=D0=B1=D1=89=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 6148 -> 6148 bytes app.py | 67 +++++++-- install-panel.sh | 2 +- install.sh | 89 ++++++++++-- mita-ctl.sh | 343 ++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 422 insertions(+), 79 deletions(-) mode change 100644 => 100755 mita-ctl.sh diff --git a/.DS_Store b/.DS_Store index 0a2e5ac49fea0a6d8927065230370ca46fda79d2..524330d086169b3a89cb364f06e1333c64eab4a6 100644 GIT binary patch delta 201 zcmZoMXfc=|#>B)qu~2NHo}wr-0|Nsi1A_nqgDyidLq0<;Ljgn1#=_-{@*o*rAU~6# zgdq`GmLU}=k~&#}MP3@8yaP)EhoPalj)IY)$>b9(_Ud>PmjxH)<>cq3gB-J2kVA}R iV#EB+>>T_YK!*bP-fgc5S_IRu?rliA5`0xc$go4xxaX@Lq0V0GFk*IRYM`Sx2cj;!=tlfn6 z5)S|dsWuu^2k2HgIbhQ@fH@b%%?TOOyssaAv_l1 z%Kl60`Iy#Ng(nH`rK71`z*$}3)XF6ir?+v+zvVm2a5kPCgPT)ef-?mGhINc)897Jf zD8o(W_3+RzoQ?X7(_s}h;VEpvE*!uJKENmV0$<@f{DQwIhgQ%Hw2Icy15`$jQ59{Y z0PP@xUQhLk+=nc#%/dev/null +"$PANEL_DIR/venv/bin/pip" install -q flask gunicorn pyyaml 2>/dev/null ok "Python venv и Flask установлены" # ── копирование файлов ───────────────────────────────────────────── diff --git a/install.sh b/install.sh index 1e10dfa..aaf5f86 100755 --- a/install.sh +++ b/install.sh @@ -88,8 +88,9 @@ apt-get install -y -qq \ iptables ipset ufw \ fail2ban openssl \ net-tools \ + python3 python3-yaml \ 2>/dev/null -ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset)" +ok "Базовые пакеты установлены (включая fail2ban, ufw, ipset, python3-yaml)" # ============================================================================= # УСТАНОВКА MITA @@ -264,7 +265,8 @@ WARP_PORT=$WARP_PORT MITA_PORT_RANGE="$MITA_PORT_RANGE" MITA_PROTOCOL="$MITA_PROTOCOL" MITA_LOG_LEVEL="$MITA_LOG_LEVEL" -GEOSITE_BASE="https://raw.githubusercontent.com/v2fly/domain-list-community/release" +GEOSITE_YML_URL="https://github.com/v2fly/domain-list-community/releases/latest/download/dlc.dat_plain.yml" +GEOSITE_CACHE="/var/cache/mita-geosite.yml" GEOIP_BASE="https://raw.githubusercontent.com/herrbischoff/country-ip-blocks/master/ipv4" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] \$*" | tee -a /var/log/mita-update.log; } @@ -276,6 +278,31 @@ TMP_DOMAINS=\$(mktemp) # Пользовательские домены grep -v '^\s*#' "\$LISTS_DIR/custom_domains.txt" | grep -v '^\s*$' >> "\$TMP_DOMAINS" || true +# Проверяем, нужен ли geosite YAML (есть хотя бы одна строка geosite: в auto_domains.txt) +NEED_GEOSITE=false +grep -q '^geosite:' "\$LISTS_DIR/auto_domains.txt" 2>/dev/null && NEED_GEOSITE=true + +if \$NEED_GEOSITE; then + mkdir -p "\$(dirname "\$GEOSITE_CACHE")" + CACHE_AGE_DAYS=999 + if [[ -f "\$GEOSITE_CACHE" ]]; then + CACHE_AGE_SEC=\$(( \$(date +%s) - \$(stat -c %Y "\$GEOSITE_CACHE" 2>/dev/null || echo 0) )) + CACHE_AGE_DAYS=\$(( CACHE_AGE_SEC / 86400 )) + fi + if [[ \$CACHE_AGE_DAYS -ge 7 ]]; then + log "Загрузка базы geosite (dlc.dat_plain.yml, ~10-15 МБ, кэш обновляется раз в 7 дней)..." + if curl -sfL --max-time 60 "\$GEOSITE_YML_URL" -o "\$GEOSITE_CACHE.tmp" 2>/dev/null; then + mv "\$GEOSITE_CACHE.tmp" "\$GEOSITE_CACHE" + log "База geosite обновлена" + else + log "WARN: не удалось загрузить базу geosite — используется старый кэш (если есть)" + rm -f "\$GEOSITE_CACHE.tmp" + fi + else + log "База geosite в кэше актуальна (обновлена \$CACHE_AGE_DAYS дн. назад)" + fi +fi + # Автоматические источники while IFS= read -r line; do line="\$(echo "\$line" | xargs)" @@ -284,10 +311,40 @@ while IFS= read -r line; do if [[ "\$line" == geosite:* ]]; then category="\${line#geosite:}" log " geosite:\$category" - curl -sf "\$GEOSITE_BASE/\${category}.txt" 2>/dev/null \ - | grep -v '^\s*#' | grep -v '^\s*$' | grep -v 'include:' | grep -v 'regexp:' \ - | sed 's/^full://' \ - >> "\$TMP_DOMAINS" || log " WARN: не удалось загрузить geosite:\$category" + if [[ -f "\$GEOSITE_CACHE" ]]; then + python3 -c " +import yaml, sys +try: + with open('\$GEOSITE_CACHE') as f: + data = yaml.safe_load(f) + found = False + for entry in data.get('lists', []): + if entry.get('name','').lower() == '\$category'.lower(): + found = True + for rule in entry.get('rules', []): + d = rule + for prefix in ('domain:', 'full:'): + if d.startswith(prefix): + d = d[len(prefix):] + print(d) + break + else: + # regexp: и include: пропускаем — не доменные правила + pass + if not found: + sys.stderr.write('NOTFOUND\\n') +except Exception as e: + sys.stderr.write(f'ERROR:{e}\\n') +" >> "\$TMP_DOMAINS" 2>/tmp/geosite_err.log + if grep -q "NOTFOUND" /tmp/geosite_err.log 2>/dev/null; then + log " WARN: категория geosite:\$category не найдена в базе" + elif grep -q "ERROR" /tmp/geosite_err.log 2>/dev/null; then + log " WARN: ошибка парсинга geosite:\$category — $(cat /tmp/geosite_err.log)" + fi + rm -f /tmp/geosite_err.log + else + log " WARN: база geosite недоступна, пропускаю geosite:\$category" + fi elif [[ "\$line" == http* ]]; then log " URL: \$line" @@ -598,12 +655,24 @@ echo "" # ============================================================================= section "Веб-панель управления" -PANEL_INSTALLER="$SCRIPT_DIR/mita-panel-pkg/install-panel.sh" +# Ищем install-panel.sh в нескольких возможных местах: +# 1) Прямо рядом с install.sh (актуальная структура архива) +# 2) В подпапке mita-panel-pkg/ (старая структура архива) +PANEL_INSTALLER="" +for candidate in \ + "$SCRIPT_DIR/install-panel.sh" \ + "$SCRIPT_DIR/mita-panel-pkg/install-panel.sh" +do + if [[ -f "$candidate" ]]; then + PANEL_INSTALLER="$candidate" + break + fi +done -if [[ ! -f "$PANEL_INSTALLER" ]]; then +if [[ -z "$PANEL_INSTALLER" ]]; then warn "Файл install-panel.sh не найден рядом со скриптом." - warn "Ожидаемый путь: $PANEL_INSTALLER" - warn "Скачайте архив mita-panel.zip, распакуйте рядом с install.sh и запустите install-panel.sh вручную." + warn "Искал в: $SCRIPT_DIR/ и $SCRIPT_DIR/mita-panel-pkg/" + warn "Убедитесь что install-panel.sh, app.py и templates/ распакованы рядом с install.sh." else echo -e "Установить веб-панель управления mita? (логин/пароль и секретный путь генерируются автоматически)" echo -e " ${GREEN}y${NC} — да, установить сейчас" diff --git a/mita-ctl.sh b/mita-ctl.sh old mode 100644 new mode 100755 index b89c627..83c18c5 --- a/mita-ctl.sh +++ b/mita-ctl.sh @@ -75,6 +75,7 @@ main_menu() { echo -e " ${BOLD}7.${NC} Настройка fail2ban" echo -e " ${BOLD}8.${NC} Рекомендации по безопасности VPS" echo -e " ${RED}${BOLD}9.${NC} ${RED}Полное удаление mita, панели и WARP${NC}" + echo -e " ${BOLD}10.${NC} Показать текущую конфигурацию" echo -e " ${BOLD}0.${NC} Выход" echo "" read -r -p " Выберите пункт: " choice @@ -88,6 +89,7 @@ main_menu() { 7) menu_fail2ban ;; 8) menu_security ;; 9) menu_uninstall ;; + 10) menu_show_config ;; 0) exit 0 ;; *) warn "Неверный выбор" ;; esac @@ -411,6 +413,76 @@ _rebuild_start_sh() { # ════════════════════════════════════════════════════════════════════════════ # 4. ПОЛЬЗОВАТЕЛИ # ════════════════════════════════════════════════════════════════════════════ +# ── Общая функция: безопасное применение конфига mita ─────────────────────── +# Запускает mita если он не активен (нужно для apply config), +# применяет конфиг, возвращает демон в исходное состояние. +# Возвращает 0 при успехе, 1 при ошибке. Текст ошибки — в stdout. +_apply_mita_config() { + local MITA_WAS_STOPPED=false + local MITA_BG_PID="" + + if ! systemctl is-active --quiet mita 2>/dev/null; then + systemctl reset-failed mita 2>/dev/null || true + rm -f /etc/mita/server.conf.pb 2>/dev/null + /usr/bin/mita run & + MITA_BG_PID=$! + sleep 2 + MITA_WAS_STOPPED=true + fi + + local apply_out apply_rc + apply_out=$(mita apply config "$MITA_CONFIG" 2>&1) + apply_rc=$? + + if $MITA_WAS_STOPPED; then + kill "$MITA_BG_PID" 2>/dev/null || true + wait "$MITA_BG_PID" 2>/dev/null || true + sleep 1 + fi + + if [[ $apply_rc -ne 0 ]]; then + echo "$apply_out" + return 1 + fi + + [[ -f /etc/mita/server.conf.pb ]] && chown mita:mita /etc/mita/server.conf.pb 2>/dev/null + + systemctl reset-failed mita 2>/dev/null || true + systemctl restart mita + sleep 1 + return 0 +} + +_delete_user() { + local uname="$1" + local result + result=$(python3 << PYEOF +import json +with open('$MITA_CONFIG') as f: d=json.load(f) +before=len(d.get('users',[])) +d['users']=[u for u in d.get('users',[]) if u['name']!='$uname'] +after=len(d['users']) +with open('$MITA_CONFIG','w') as f: json.dump(d,f,indent=2) +print("DELETED" if before>after else "NOTFOUND") +PYEOF +) + + if [[ "$result" == "NOTFOUND" ]]; then + warn "Пользователь '$uname' не найден" + return 1 + fi + + if _apply_mita_config; then + if systemctl is-active --quiet mita; then + ok "Пользователь '$uname' удалён, mita работает" + else + warn "Пользователь удалён, но mita не запустился — проверьте: journalctl -u mita -n 20" + fi + else + error "Ошибка применения конфига после удаления пользователя" + fi +} + menu_users() { section "Управление пользователями mita" @@ -452,19 +524,7 @@ for u in users: print(f' - {u[\"name\"]}') pause ;; 3) read -r -p "Имя пользователя для удаления: " uname - python3 << PYEOF -import json, subprocess -with open('$MITA_CONFIG') as f: d=json.load(f) -before=len(d.get('users',[])) -d['users']=[u for u in d.get('users',[]) if u['name']!='$uname'] -after=len(d['users']) -with open('$MITA_CONFIG','w') as f: json.dump(d,f,indent=2) -if before>after: - subprocess.run(['mita','apply','config','$MITA_CONFIG'],capture_output=True) - print(f"Пользователь '$uname' удалён") -else: - print(f"Пользователь '$uname' не найден") -PYEOF + _delete_user "$uname" pause ;; 4) read -r -p "Имя пользователя: " uname @@ -506,38 +566,80 @@ _create_user() { local pass pass=$(openssl rand -base64 48 | tr -d '/+=' | head -c 48) + # Записываем пользователя в JSON и проверяем результат явно + local py_result + py_result=$(python3 << PYEOF +import json, sys +try: + with open('$MITA_CONFIG') as f: d=json.load(f) +except Exception as e: + print(f"ERROR_READ:{e}") + sys.exit(1) + +if any(u['name']=='$uname' for u in d.get('users',[])): + print("EXISTS") + sys.exit(0) + +d.setdefault('users',[]).append({'name':'$uname','password':'$pass'}) +try: + with open('$MITA_CONFIG','w') as f: json.dump(d,f,indent=2) +except Exception as e: + print(f"ERROR_WRITE:{e}") + sys.exit(1) + +print("WRITTEN") +PYEOF +) + + if [[ "$py_result" == "EXISTS" ]]; then + warn "Пользователь '$uname' уже существует" + return 1 + fi + if [[ "$py_result" != "WRITTEN" ]]; then + error "Не удалось записать конфиг: $py_result" + return 1 + fi + + local apply_err + apply_err=$(_apply_mita_config) + if [[ $? -ne 0 ]]; then + error "mita apply config завершился с ошибкой:" + echo "$apply_err" + return 1 + fi + + if systemctl is-active --quiet mita; then + ok "Создан: $uname" + else + warn "Пользователь создан, но mita не запустился — проверьте: journalctl -u mita -n 20" + fi + + echo " Пароль: $pass" + + # Показать клиентский конфиг python3 << PYEOF import json, subprocess with open('$MITA_CONFIG') as f: d=json.load(f) -if any(u['name']=='$uname' for u in d.get('users',[])): - print(f"Пользователь '$uname' уже существует") -else: - d.setdefault('users',[]).append({'name':'$uname','password':'$pass'}) - with open('$MITA_CONFIG','w') as f: json.dump(d,f,indent=2) - r=subprocess.run(['mita','apply','config','$MITA_CONFIG'],capture_output=True,text=True) - print(f"✓ Создан: $uname") - print(f" Пароль: $pass") - # Показать клиентский конфиг - server_ip=subprocess.run(['curl','-s','--max-time','5','ifconfig.me'],capture_output=True,text=True).stdout.strip() - cfg=d.get('portBindings',[{}])[0] - port=cfg.get('portRange',str(cfg.get('port','2100-2110'))) - proto=cfg.get('protocol','TCP') - client={ - 'profiles':[{'profileName':'default','user':{'name':'$uname','password':'$pass'}, - 'servers':[{'ipAddress':server_ip,'portBindings':[{'portRange':port,'protocol':proto}]}]}], - 'activeProfile':'default','rpcPort':8964,'socks5Port':1080 - } - print(f"\n Клиентский конфиг (mieru CLI):") - print(json.dumps(client,indent=2,ensure_ascii=False)) +server_ip=subprocess.run(['curl','-s','--max-time','5','ifconfig.me'],capture_output=True,text=True).stdout.strip() +cfg=d.get('portBindings',[{}])[0] +port=cfg.get('portRange',str(cfg.get('port','2100-2110'))) +proto=cfg.get('protocol','TCP') +client={ + 'profiles':[{'profileName':'default','user':{'name':'$uname','password':'$pass'}, + 'servers':[{'ipAddress':server_ip,'portBindings':[{'portRange':port,'protocol':proto}]}]}], + 'activeProfile':'default','rpcPort':8964,'socks5Port':1080 +} +print(f"\n Клиентский конфиг (mieru CLI):") +print(json.dumps(client,indent=2,ensure_ascii=False)) - first_port = port.split('-')[0] if '-' in port else port - print(f"\n Параметры для ручного ввода в Karing/sing-box клиентах:") - print(f" server: {server_ip}") - print(f" server port: {first_port}") - print(f" username: $uname") - print(f" password: $pass") - print(f" transport: TCP") - print(f" multiplexing: multiplexing_low") +first_port = port.split('-')[0] if '-' in port else port +print(f"\n Параметры для ручного ввода в Karing/sing-box клиентах:") +print(f" server: {server_ip}") +print(f" server port: {first_port}") +print(f" username: $uname") +print(f" password: $pass") +print(f" transport: TCP") +print(f" multiplexing: multiplexing_low") PYEOF echo "" @@ -583,8 +685,8 @@ KARINGEOF # ════════════════════════════════════════════════════════════════════════════ # 5. ПОКАЗАТЬ ДАННЫЕ АДМИНА # ════════════════════════════════════════════════════════════════════════════ -menu_show_admin() { - section "Данные администратора панели" +# ── Общая функция: вывод логина/пароля/URL панели с учётом access_mode ────── +_print_panel_access_info() { local user pass secret panel_port access_mode user=$(panel_value "admin_user") pass=$(panel_value "admin_pass") @@ -600,21 +702,134 @@ menu_show_admin() { echo -e " Секретный путь: ${BOLD}/${secret}/${NC}" echo "" - if [[ "$access_mode" == "ssh" ]]; then - local ssh_port - ssh_port=$(panel_value "ssh_port"); ssh_port=${ssh_port:-22} - print_ssh_instruction "$panel_port" "$secret" "$ssh_port" - else - local proto="http" - [[ -n "$(panel_value ssl_cert)" ]] && proto="https" - local domain - domain=$(panel_value "ssl_domain") - if [[ -n "$domain" ]]; then - echo -e " Адрес: ${BOLD}${GREEN}${proto}://${domain}:${panel_port}/${secret}/${NC}" - else + case "$access_mode" in + ssh) + local ssh_port + ssh_port=$(panel_value "ssh_port"); ssh_port=${ssh_port:-22} + echo -e " Способ доступа: ${YELLOW}SSH-туннель${NC} (панель не торчит наружу)" + print_ssh_instruction "$panel_port" "$secret" "$ssh_port" + ;; + domain) + local proto="http" domain + [[ -n "$(panel_value ssl_cert)" ]] && proto="https" + domain=$(panel_value "ssl_domain") + echo -e " Способ доступа: ${YELLOW}Доменное имя${NC}" + if [[ -n "$domain" ]]; then + echo -e " Адрес: ${BOLD}${GREEN}${proto}://${domain}:${panel_port}/${secret}/${NC}" + else + warn "Домен не настроен в panel.json, показываю по IP" + echo -e " Адрес: ${BOLD}${GREEN}${proto}://${server_ip}:${panel_port}/${secret}/${NC}" + fi + ;; + *) + local proto="http" + [[ -n "$(panel_value ssl_cert)" ]] && proto="https" + echo -e " Способ доступа: ${YELLOW}По IP-адресу${NC}" echo -e " Адрес: ${BOLD}${GREEN}${proto}://${server_ip}:${panel_port}/${secret}/${NC}" - fi + ;; + esac +} + +menu_show_admin() { + section "Данные администратора панели" + _print_panel_access_info + pause +} + +menu_show_config() { + section "Текущая конфигурация сервера" + + # ── Доступ к панели ─────────────────────────────────────────── + echo -e "${CYAN}── Веб-панель ──────────────────────────────────────────${NC}" + if [[ -f "$PANEL_CONFIG" ]]; then + _print_panel_access_info + else + warn "Панель не установлена (panel.json не найден)" fi + + echo "" + echo -e "${CYAN}── Статус сервисов ─────────────────────────────────────${NC}" + + local mita_active panel_active warp_active f2b_active ufw_active + systemctl is-active --quiet mita 2>/dev/null && mita_active=true || mita_active=false + systemctl is-active --quiet mita-panel 2>/dev/null && panel_active=true || panel_active=false + systemctl is-active --quiet fail2ban 2>/dev/null && f2b_active=true || f2b_active=false + command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active" && ufw_active=true || ufw_active=false + + warp_active=false + if command -v docker &>/dev/null && docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^cloudflare-warp$"; then + warp_active=true + fi + + _status_line() { + local label="$1" active="$2" + if [[ "$active" == "true" ]]; then + echo -e " ${label}: ${GREEN}● работает${NC}" + else + echo -e " ${label}: ${RED}● остановлен/не установлен${NC}" + fi + } + + _status_line "mita сервер " "$mita_active" + _status_line "Веб-панель " "$panel_active" + _status_line "Cloudflare WARP " "$warp_active" + _status_line "fail2ban " "$f2b_active" + _status_line "UFW " "$ufw_active" + + # ── mita: пользователи и порты ───────────────────────────────── + echo "" + echo -e "${CYAN}── mita сервер ─────────────────────────────────────────${NC}" + if [[ -f "$MITA_CONFIG" ]]; then + python3 -c " +import json +with open('$MITA_CONFIG') as f: d=json.load(f) +bindings = d.get('portBindings', [{}]) +b = bindings[0] if bindings else {} +port = b.get('portRange', str(b.get('port','?'))) +proto = b.get('protocol','?') +users = d.get('users', []) +print(f' Порты: {port} ({proto})') +print(f' Пользователей: {len(users)}') +for u in users: + print(f' - {u[\"name\"]}') +egress = d.get('egress', {}) +if egress.get('rules'): + print(f' WARP egress: настроен ({len(egress[\"rules\"])} правил)') +else: + print(f' WARP egress: не настроен') +" + else + warn "Конфиг mita не найден ($MITA_CONFIG)" + fi + + # ── fail2ban лимиты ─────────────────────────────────────────── + echo "" + echo -e "${CYAN}── fail2ban (лимит входа в панель) ─────────────────────${NC}" + local max_retry ban_time + max_retry=$(panel_value "login_max_attempts"); max_retry=${max_retry:-"не настроено"} + ban_time=$(panel_value "login_ban_seconds"); ban_time=${ban_time:-"не настроено"} + echo -e " Попыток до блокировки: ${YELLOW}${max_retry}${NC}" + echo -e " Время блокировки: ${YELLOW}${ban_time}${NC} сек" + + # ── SSL ─────────────────────────────────────────────────────── + echo "" + echo -e "${CYAN}── SSL-сертификат панели ────────────────────────────────${NC}" + local ssl_type + ssl_type=$(panel_value "ssl_type") + if [[ -z "$ssl_type" || "$ssl_type" == "none" ]]; then + echo -e " Тип: ${RED}не настроен${NC} (панель работает по HTTP)" + else + echo -e " Тип: ${YELLOW}${ssl_type}${NC}" + [[ -n "$(panel_value ssl_domain)" ]] && echo -e " Домен: ${YELLOW}$(panel_value ssl_domain)${NC}" + echo -e " Файл: $(panel_value ssl_cert)" + fi + + # ── Версии ──────────────────────────────────────────────────── + echo "" + echo -e "${CYAN}── Версии ──────────────────────────────────────────────${NC}" + command -v mita &>/dev/null && echo -e " mita: $(mita version 2>/dev/null || echo '?')" + command -v docker &>/dev/null && echo -e " docker: $(docker --version 2>/dev/null | cut -d, -f1)" + pause } @@ -885,13 +1100,21 @@ menu_uninstall() { info "Остановка mita..." systemctl stop mita 2>/dev/null || true systemctl disable mita 2>/dev/null || true + + # Сначала пробуем штатное удаление через dpkg/apt — пока unit-файл + # ещё на месте, иначе pre/post-removal скрипты пакета падают + DEB_PKG=$(dpkg -l 2>/dev/null | grep -i "^ii.*mita" | awk '{print $2}' | head -1) + if [[ -n "$DEB_PKG" ]]; then + apt-get remove -y -qq "$DEB_PKG" 2>/dev/null \ + || dpkg --remove --force-remove-reinstreq "$DEB_PKG" 2>/dev/null \ + || true + fi + + # Затем подчищаем то, что могло остаться вручную rm -f /usr/lib/systemd/system/mita.service rm -f /etc/systemd/system/mita.service - command -v mita &>/dev/null && { - DEB_PKG=$(dpkg -l 2>/dev/null | grep -i mita | awk '{print $2}' | head -1) - [[ -n "$DEB_PKG" ]] && apt-get remove -y -qq "$DEB_PKG" 2>/dev/null - } rm -f /usr/bin/mita + dpkg --configure -a 2>/dev/null || true ok "mita сервер удалён" # ── 3. Удалить WARP (Docker) ───────────────────────────────────