#!/bin/sh
# Искра Роутер — установщик VPN-панели на Keenetic (Entware).
# Разворачивает: sing-box (opkg) + панель singpanel + сторож зависаний + автозапуск.
# Чистый шаблон: серверы/устройства добавляются потом в панели.
set -e

BASE="${UPDATE_BASE:-https://213.134.2.226:8443}"
INFRA_PIN="${UPDATE_SPKI_PIN:-sXGIgyS00TvT+w2g942GWFEeYDnUPXxLTm7LswwVuXk=}"
OPT=/opt
BIN=$OPT/usr/bin/singpanel
CFGDIR=$OPT/etc/singpanel
SBDIR=$OPT/etc/sing-box

say(){ echo "[Искра] $*"; }
die(){ echo "[Искра][ОШИБКА] $*" >&2; exit 1; }
curl_infra(){ curl -k --pinnedpubkey "sha256//$INFRA_PIN" "$@"; }
json_escape(){
  # Имена вводятся человеком и могут содержать кавычки/обратный слеш.
  # Убираем переводы строк и экранируем JSON без зависимости от jq/python.
  printf '%s' "$1" | tr '\r\n\t' '   ' | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# 0) offline/local-режим «с флешки»: если рядом со скриптом лежат manifest.txt и
# singpanel-<arch>, панель ставится из них (без скачивания бинаря), а SHA-256
# сверяется с локальным manifest так же fail-closed. Сетевые шаги (opkg,
# регистрация в хабе, каталог серверов) остаются сетевыми. Если локальных файлов
# нет — поведение прежнее (скачивание). BusyBox sh может не иметь `readlink -f`,
# поэтому каталог скрипта вычисляем из $0 максимально просто.
script_dir(){
  sd=$1
  case "$sd" in
    */*) sd=${sd%/*} ;;
    *)   sd=. ;;
  esac
  (CDPATH= cd -- "$sd" 2>/dev/null && pwd) || printf '%s' "$sd"
}
SELF_DIR=$(script_dir "$0")
# Явный FLEET_LOCAL_DIR перекрывает автоопределение по $0.
LOCAL_DIR=""
if [ -n "$FLEET_LOCAL_DIR" ]; then
  LOCAL_DIR=$FLEET_LOCAL_DIR
elif [ -n "$SELF_DIR" ] && [ -f "$SELF_DIR/manifest.txt" ]; then
  LOCAL_DIR=$SELF_DIR
fi

# 1) Entware
[ -x "$OPT/bin/opkg" ] || die "Entware не найден (/opt/bin/opkg). Сначала установи Entware."

# 2) архитектура
M=$(uname -m)
case "$M" in
  aarch64) ARCH=arm64 ;;
  armv7l|armv7) ARCH=armv7 ;;
  mips)
    # На Keenetic `uname -m` одинаков для big- и little-endian MIPS.
    # BusyBox od не поддерживает GNU-флаги -A/-t: прежняя проверка молча
    # возвращала пустую строку и всегда выбирала mipsle. Читаем байт EI_DATA
    # (offset 5) у нативного /bin/sh: 002 = MSB, 001 = LSB.
    if dd if=/bin/sh bs=1 skip=5 count=1 2>/dev/null | od -b 2>/dev/null | grep -q ' 002'; then
      ARCH=mips
    else
      ARCH=mipsle
    fi
    ;;
  mipsel|mips64el) ARCH=mipsle ;;
  *) die "неизвестная архитектура '$M' — напиши разработчику" ;;
esac
say "архитектура: $M → singpanel-$ARCH"

# 3) зависимости из Entware
say "ставлю зависимости (sing-box, ip-full, curl, iptables, ipset, conntrack, openssh-client, autossh)…"
$OPT/bin/opkg update >/dev/null 2>&1 || say "opkg update не удался — продолжаю с имеющимися фидами"
for p in sing-box-go ip-full curl iptables ipset conntrack openssh-client autossh; do
  $OPT/bin/opkg install "$p" >/dev/null 2>&1 || say "не смог поставить $p (возможно уже есть)"
done
command -v sing-box >/dev/null 2>&1 || [ -x $OPT/bin/sing-box ] || die "sing-box не установился"

# 3a) освободить TCP/443 для чистого TPROXY. Это ограничение KeeneticOS:
# встроенная HTTPS-служба на 443 ломает TCP-перехват независимо от архитектуры
# и аппаратного NAT. Повторная установка идемпотентна: уже перенесённый порт
# не трогаем. KEEP_KEENETIC_SSL_PORT=1 — явный opt-out для Hybrid/TUN-сценария.
keenetic_ssl_port(){
  curl -fsS http://127.0.0.1:79/rci/ip/http/ssl 2>/dev/null \
    | sed -n 's/.*"port"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9][0-9]*\)"\{0,1\}.*/\1/p' \
    | head -1
}
keenetic_http_port(){
  curl -fsS http://127.0.0.1:79/rci/ip/http 2>/dev/null \
    | sed -n '/"ssl"[[:space:]]*:/q; s/.*"port"[[:space:]]*:[[:space:]]*"\{0,1\}\([0-9][0-9]*\)"\{0,1\}.*/\1/p' \
    | head -1
}
KSSL_OLD=$(keenetic_ssl_port)
KHTTP_OLD=$(keenetic_http_port)
if [ "$KHTTP_OLD" = "443" ]; then
  say "обычный HTTP Keenetic занимает TCP/443; перенеси его в веб-интерфейсе роутера — до этого TPROXY в авто-режиме останется выключен"
elif [ "$KSSL_OLD" = "443" ]; then
  KSSL_NEW=${KEENETIC_SSL_PORT:-8443}
  case "$KSSL_NEW" in
    ''|*[!0-9]*) say "HTTPS Keenetic остаётся на 443: некорректный KEENETIC_SSL_PORT='$KSSL_NEW'" ;;
    7893|8090|8091|10890) say "HTTPS Keenetic остаётся на 443: порт $KSSL_NEW занят службой VPN-панели" ;;
    *)
      if [ "$KSSL_NEW" -lt 1024 ] || [ "$KSSL_NEW" -gt 65535 ]; then
        say "HTTPS Keenetic остаётся на 443: порт $KSSL_NEW вне безопасного диапазона 1024–65535"
      elif [ "$KEEP_KEENETIC_SSL_PORT" = "1" ]; then
        say "HTTPS Keenetic оставлен на 443 по KEEP_KEENETIC_SSL_PORT=1; TPROXY в авто-режиме будет выключен"
      elif ndmc -c "ip http ssl port $KSSL_NEW" >/dev/null 2>&1 \
        && ndmc -c "system configuration save" >/dev/null 2>&1; then
        sleep 1
        KSSL_GOT=$(keenetic_ssl_port)
        if [ "$KSSL_GOT" = "$KSSL_NEW" ]; then
          say "HTTPS Keenetic перенесён 443 → $KSSL_NEW; прямой HTTPS/KeenDNS/SSTP теперь используют :$KSSL_NEW"
        else
          say "команда переноса HTTPS выполнена, но RCI сообщает порт ${KSSL_GOT:-неизвестен}; проверь в настройках панели"
        fi
      else
        say "не удалось перенести HTTPS Keenetic с 443 на $KSSL_NEW; панель установится, но TPROXY в авто-режиме останется выключен"
      fi
      ;;
  esac
elif [ -n "$KSSL_OLD" ]; then
  say "HTTPS Keenetic уже на порту $KSSL_OLD — TCP/443 свободен для TPROXY"
else
  say "не удалось прочитать HTTPS-порт Keenetic; после установки проверь карточку «Перехват трафика»"
fi

# 4) панель — из локального каталога (флешка/offline) или скачиванием
mkdir -p "$OPT/usr/bin" "$CFGDIR" "$SBDIR" "$OPT/var/log" "$OPT/var/lib/sing-box" "$OPT/etc/ndm/netfilter.d"

# TLS сервера обновлений проверяется по закреплённому SHA-256 публичного ключа:
# самоподписанный сертификат не зависит от внешнего CA, а MITM не сможет
# подменить одновременно install.sh, manifest.txt и бинарь. SHA-256 артефакта
# остаётся второй независимой проверкой целостности и работает fail-closed.
sha256_of() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" 2>/dev/null | awk '{print $1}'
  elif command -v openssl >/dev/null 2>&1; then
    openssl dgst -sha256 "$1" 2>/dev/null | awk '{print $NF}'
  fi
}

# Local-режим включается только когда рядом есть И manifest.txt, И бинарь под
# нашу архитектуру. Иначе (даже при заданном FLEET_LOCAL_DIR) откатываемся на
# скачивание — местный manifest сам по себе бинарь не заменит.
LOCAL_BIN=""
LOCAL_MANIFEST=""
if [ -n "$LOCAL_DIR" ] && [ -f "$LOCAL_DIR/manifest.txt" ] && [ -f "$LOCAL_DIR/singpanel-$ARCH" ]; then
  LOCAL_MANIFEST="$LOCAL_DIR/manifest.txt"
  LOCAL_BIN="$LOCAL_DIR/singpanel-$ARCH"
elif [ -n "$FLEET_LOCAL_DIR" ]; then
  say "local-режим запрошен ($FLEET_LOCAL_DIR), но рядом нет manifest.txt+singpanel-$ARCH — скачиваю из сети"
fi

if [ -n "$LOCAL_BIN" ]; then
  say "local-режим: ставлю панель из $LOCAL_DIR (без скачивания бинаря)"
  MANIFEST=$(cat "$LOCAL_MANIFEST" 2>/dev/null) \
    || die "не читается локальный manifest.txt ($LOCAL_MANIFEST) — установка остановлена"
else
  say "скачиваю панель…"
  MANIFEST=$(curl_infra -fsSL "$BASE/manifest.txt" 2>/dev/null) \
    || die "не скачался manifest.txt релиза ($BASE/manifest.txt) — установка остановлена"
fi
WANT_SUM=$(printf '%s\n' "$MANIFEST" | sed -n "s/^singpanel-$ARCH=\([0-9a-fA-F]\{64\}\)\$/\1/p" | head -1)
[ -n "$WANT_SUM" ] || die "в манифесте нет SHA-256 для singpanel-$ARCH — установка остановлена"
MAN_VERSION=$(printf '%s\n' "$MANIFEST" | sed -n 's/^version=\(.*\)$/\1/p' | head -1)

if [ -n "$LOCAL_BIN" ]; then
  cp "$LOCAL_BIN" "$BIN.new" || die "не скопировался локальный бинарь ($LOCAL_BIN) — установка остановлена"
else
  curl_infra -fsSL "$BASE/singpanel-$ARCH" -o "$BIN.new" || die "не скачался бинарь ($BASE/singpanel-$ARCH)"
fi
GOT_SUM=$(sha256_of "$BIN.new")
if [ -z "$GOT_SUM" ]; then
  rm -f "$BIN.new"
  die "нечем проверить SHA-256 (нет sha256sum и openssl) — установка остановлена"
fi
if [ "$GOT_SUM" != "$(printf '%s' "$WANT_SUM" | tr 'A-F' 'a-f')" ]; then
  rm -f "$BIN.new"
  die "SHA-256 скачанного singpanel-$ARCH не совпал с манифестом — установка остановлена"
fi
say "SHA-256 совпал с манифестом релиза"
chmod 755 "$BIN.new"
if ! BIN_VERSION=$("$BIN.new" version 2>&1); then
  rm -f "$BIN.new"
  die "скачанный бинарь singpanel-$ARCH не запускается: $BIN_VERSION"
fi
if [ -n "$MAN_VERSION" ] && [ "$BIN_VERSION" != "$MAN_VERSION" ]; then
  rm -f "$BIN.new"
  die "бинарь сообщает версию $BIN_VERSION, а манифест описывает $MAN_VERSION — установка остановлена"
fi
mv "$BIN.new" "$BIN"
say "версия панели: $BIN_VERSION"

# 5) чистый конфиг sing-box (минимальный, панель его перезапишет при добавлении серверов)
[ -f "$SBDIR/config.json" ] || cat > "$SBDIR/config.json" <<'CFG'
{
  "log": { "level": "warn", "timestamp": true },
  "inbounds": [ { "type": "tun", "tag": "singtun", "interface_name": "singtun",
    "address": ["172.20.0.1/30"], "mtu": 1500, "auto_route": false, "stack": "gvisor" } ],
  "outbounds": [ { "type": "direct" } ],
  "route": { "final": "direct", "auto_detect_interface": true }
}
CFG

# 6) имя роутера: из env или спросить интерактивно
if [ -z "$RTUN_NAME" ]; then
  DEF=$(hostname 2>/dev/null || echo router)
  printf "[Искра] Имя этого роутера (дом/дача/офис) [%s]: " "$DEF"
  read RTUN_NAME </dev/tty 2>/dev/null || RTUN_NAME=""
  [ -z "$RTUN_NAME" ] && RTUN_NAME="$DEF"
fi

# токен парка: из env или спросить интерактивно (Enter — пропустить,
# тогда без удалённого доступа и авто-серверов, всё добавишь в панели позже)
if [ -z "$RTUN_TOKEN" ]; then
  printf "[Искра] Токен парка (Enter — пропустить): "
  read RTUN_TOKEN </dev/tty 2>/dev/null || RTUN_TOKEN=""
fi

# 6a) авто-регистрация в хабе удалённого доступа (если задан токен)
HUB_BASE="${RTUN_HUB_BASE:-$BASE/hub}"
# Порт текущей SSH-сессии — это Entware SSH, который нужно пробросить через
# хаб. На разных Keenetic он бывает 22, 222 и пользовательским. При установке
# не по SSH можно явно передать RTUN_LOCAL_SSH_PORT; старый дефолт — 222.
if [ -z "$RTUN_LOCAL_SSH_PORT" ] && [ -n "$SSH_CONNECTION" ]; then
  RTUN_LOCAL_SSH_PORT=${SSH_CONNECTION##* }
fi
case "$RTUN_LOCAL_SSH_PORT" in
  ''|*[!0-9]*) RTUN_LOCAL_SSH_PORT=222 ;;
esac
RTUN_FIELDS=""
if [ -n "$RTUN_TOKEN" ]; then
  say "регистрирую роутер '$RTUN_NAME' в хабе удалённого доступа…"
  RTUN_NAME_JSON=$(json_escape "$RTUN_NAME")
  RTUN_TOKEN_JSON=$(json_escape "$RTUN_TOKEN")
  RPORT=$(curl_infra -fsS -X POST "$HUB_BASE/register" \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"$RTUN_NAME_JSON\",\"token\":\"$RTUN_TOKEN_JSON\"}" 2>/dev/null \
    | sed -n 's/.*"port":\([0-9]*\).*/\1/p')
  if [ -n "$RPORT" ]; then
    curl_infra -fsS --get --data-urlencode "token=$RTUN_TOKEN" "$HUB_BASE/key" \
      -o "$OPT/etc/rtun_key" 2>/dev/null && chmod 600 "$OPT/etc/rtun_key"
    ON=false; [ "$RTUN_ON" = "1" ] && ON=true
    RTUN_FIELDS=",\"remoteOn\":$ON,\"remotePort\":$RPORT,\"remoteName\":\"$RTUN_NAME_JSON\",\"remoteLocalPort\":$RTUN_LOCAL_SSH_PORT,\"catalogToken\":\"$RTUN_TOKEN_JSON\""
    say "зарегистрирован: порт $RPORT на хабе → локальный SSH $RTUN_LOCAL_SSH_PORT (удалённый мониторинг включён=$ON)"
  else
    say "регистрация не удалась (проверь RTUN_TOKEN) — доступ и серверы можно добавить в панели позже"
  fi
fi

# 6b) чистый state панели (с полями удалённого доступа/каталога, если зарегистрирован)
[ -f "$CFGDIR/state.json" ] || echo "{\"servers\":[],\"devices\":[],\"lists\":[],\"defaults\":{\"mode\":\"off\",\"server\":\"\"}$RTUN_FIELDS}" > "$CFGDIR/state.json"
chmod 600 "$CFGDIR/state.json"

# 7) сторож TX watchdog
cat > "$OPT/bin/txwd-guard.sh" <<'GUARD'
#!/bin/sh
LOG=/opt/keenlog/keenetic.log
GLOG=/opt/var/log/txwd-guard.log
PORT="GigabitEthernet0/1"
mkdir -p /opt/var/log
last=$(grep -c "suspended by TX watchdog" "$LOG" 2>/dev/null || echo 0)
while true; do
  n=$(grep -c "suspended by TX watchdog" "$LOG" 2>/dev/null || echo 0)
  if [ "$n" -lt "$last" ]; then last=$n; fi
  if [ "$n" -gt "$last" ]; then
    ndmc -c "interface $PORT down" >/dev/null 2>&1; sleep 2; ndmc -c "interface $PORT up" >/dev/null 2>&1
    echo "$(date +%Y-%m-%dT%H:%M:%S) RECOVERED $PORT after TX watchdog (event #$n)" >> "$GLOG"
    last=$n
  fi
  sleep 4
done
GUARD
chmod 755 "$OPT/bin/txwd-guard.sh"

# 8) netfilter.d-хук: восстановление правил при перестройке firewall
# NDM зовёт хук на каждую перестроенную таблицу и для каждого семейства.
# Семейства разделены, а сами прогоны схлопываются внутри панели, чтобы одна
# перестройка firewall не давала подряд несколько полных netapply.
cat > "$OPT/etc/ndm/netfilter.d/50-singpanel.sh" <<'HOOK'
#!/bin/sh
# Восстановление IPv4/IPv6 правил панели после перестройки firewall Keenetic.
case "$type" in
  ip6tables) /opt/usr/bin/singpanel netapply v6 >/dev/null 2>&1 & ;;
  *)         /opt/usr/bin/singpanel netapply v4 >/dev/null 2>&1 & ;;
esac
exit 0
HOOK
chmod 755 "$OPT/etc/ndm/netfilter.d/50-singpanel.sh"

# 8b) schedule.d-хук: deny-guard при срабатывании расписаний Keenetic
# (родительский контроль). Firewall при этом НЕ перестраивается, поэтому
# netfilter.d молчит — а состав запрещённых клиентов мог измениться. deny-sync
# дёшев: один RCI-запрос и ipset swap, без пересборки правил. Панель также сама
# пишет и мигрирует этот хук при старте (ensureScheduleHook).
mkdir -p "$OPT/etc/ndm/schedule.d"
cat > "$OPT/etc/ndm/schedule.d/55-singpanel.sh" <<'HOOK'
#!/bin/sh
# Синхронизация deny-guard панели при срабатывании расписаний Keenetic
# (родительский контроль). Дёшево: RCI + ipset swap, без пересборки правил.
/opt/usr/bin/singpanel deny-sync >/dev/null 2>&1 &
exit 0
HOOK
chmod 755 "$OPT/etc/ndm/schedule.d/55-singpanel.sh"

# 9) init-скрипты автозапуска
cat > "$OPT/etc/init.d/S95singpanel" <<'INIT'
#!/bin/sh
ENABLED=yes
PROCS=singpanel
ARGS="serve"
PREARGS=""
DESC=$PROCS
PATH=/opt/usr/bin:/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
. /opt/etc/init.d/rc.func
INIT
chmod 755 "$OPT/etc/init.d/S95singpanel"

cat > "$OPT/etc/init.d/S97txwd-guard" <<'INIT'
#!/bin/sh
PIDF=/var/run/txwd-guard.pid
GBIN=/opt/bin/txwd-guard.sh
STATE=/opt/etc/singpanel/state.json
running(){ for d in /proc/[0-9]*; do tr "\0" " " < $d/cmdline 2>/dev/null | grep -q "txwd-guard.sh" && echo $(basename $d); done; }
enabled(){ grep -Eq '"portHangGuard"[[:space:]]*:[[:space:]]*true' "$STATE" 2>/dev/null; }
case "$1" in
  start|"") enabled || exit 0; [ -n "$(running)" ] && exit 0; start-stop-daemon -S -b -m -p "$PIDF" -x "$GBIN" 2>/dev/null ;;
  stop) for p in $(running); do kill -9 "$p" 2>/dev/null; done; rm -f "$PIDF" ;;
  restart) $0 stop; sleep 2; $0 start ;;
esac
INIT
chmod 755 "$OPT/etc/init.d/S97txwd-guard"

# sing-box init (если пакет не положил свой)
[ -f "$OPT/etc/init.d/S99sing-box" ] || cat > "$OPT/etc/init.d/S99sing-box" <<'INIT'
#!/bin/sh
ENABLED=yes
PROCS=sing-box
ARGS="run -D /opt/var/lib/sing-box -C /opt/etc/sing-box"
PREARGS=""
DESC=$PROCS
PATH=/opt/sbin:/opt/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
. /opt/etc/init.d/rc.func
INIT
chmod 755 "$OPT/etc/init.d/S99sing-box"

# 10) старт
say "запускаю сервисы…"
"$OPT/etc/init.d/S99sing-box" restart >/dev/null 2>&1 || "$OPT/etc/init.d/S99sing-box" start >/dev/null 2>&1
sleep 2
# До 1.6.71 TX-watchdog сторож запускался установщиком безусловно. Сначала
# гасим прежний процесс; новая панель сама немедленно поднимет его обратно,
# только если в state.json человек явно включил соответствующий тумблер.
"$OPT/etc/init.d/S97txwd-guard" stop >/dev/null 2>&1 || true
"$OPT/etc/init.d/S95singpanel" restart >/dev/null 2>&1 || "$OPT/etc/init.d/S95singpanel" start >/dev/null 2>&1
sleep 2

# 10a) авто-синк VLESS-серверов из каталога (если есть токен)
if [ -n "$RTUN_TOKEN" ]; then
	# Повторная установка не перезаписывает state.json. Доливаем токен через
	# проверяемый хабом bootstrap, чтобы старый/частичный state тоже ожил.
	# Имя и порты доливаются ТОЛЬКО в пустые поля — пользовательские настройки
	# повторная установка не перезаписывает.
	curl -fsS -H "Content-Type: application/json" \
	  -d "{\"token\":\"$(json_escape "$RTUN_TOKEN")\",\"name\":\"$(json_escape "$RTUN_NAME")\",\"port\":${RPORT:-0},\"localPort\":$RTUN_LOCAL_SSH_PORT}" \
	  http://127.0.0.1:8090/api/catalog/bootstrap >/dev/null 2>&1 || true

	# X-Fleet-Auth действует только во fleet-канале (loopback 8091) и только с
	# персональным секретом роутера — общий токен парка на основном 8090 больше
	# не даёт ничего. Секрет панель создаёт при старте; ждём его появления.
	FLEET_SECRET=""
	i=0
	while [ $i -lt 20 ]; do
	  [ -f "$CFGDIR/fleetauth" ] && FLEET_SECRET=$(cat "$CFGDIR/fleetauth" 2>/dev/null | tr -d ' \t\r\n')
	  [ -n "$FLEET_SECRET" ] && break
	  i=$((i+1)); sleep 1
	done
	if [ -z "$FLEET_SECRET" ]; then
	  say "панель не создала fleet-секрет — синк каталога и финальный apply сделай кнопками в панели"
	else
	  FLEET_URL="http://127.0.0.1:8091"
	  say "тяну VLESS-серверы из каталога…"
	  SADD=$(curl -fsS -H "X-Fleet-Auth: $FLEET_SECRET" -X POST "$FLEET_URL/api/servers/sync" 2>/dev/null | sed -n 's/.*"added":\([0-9]*\).*/\1/p')
	  [ -n "$SADD" ] && say "серверов добавлено из каталога: $SADD" || say "каталог серверов подтянуть не удалось (можно кнопкой в панели позже)"
	  # Даже если каталог уже был актуален, финальный apply гарантирует, что
	  # singtun и сетевые правила подняты после любой повторной установки.
	  curl -fsS -H "X-Fleet-Auth: $FLEET_SECRET" -X POST "$FLEET_URL/api/apply" >/dev/null 2>&1 || true
	fi
fi
sleep 1

LANIP=$(ip -4 addr show br0 2>/dev/null | sed -n 's/.*inet \([0-9.]*\).*/\1/p' | head -1)
[ -z "$LANIP" ] && LANIP=$(ndmc -c "show interface Home" 2>/dev/null | sed -n 's/.*address: \([0-9.]*\).*/\1/p' | head -1)
[ -z "$LANIP" ] && LANIP="<IP роутера>"
echo ""
say "ГОТОВО ✅"
echo "  Панель: http://$LANIP:8090"
echo "  Пароль из локальной сети не спрашивается."
echo "  Дальше: открой панель → вкладка «Серверы» → добавь vless:// ссылки."
echo "  Обновление позже: singpanel update  (или кнопка в панели)."
echo "  Настройки/статус/версии/KeenDNS/удалённый мониторинг — во вкладке «Настройки»."
if [ -n "$LOCAL_BIN" ]; then
  echo "  Local-режим: бинарь взят из $LOCAL_DIR и проверен по локальному manifest.txt"
  echo "    (fail-closed). Зависимости opkg, регистрация в хабе и каталог серверов"
  echo "    всё равно тянулись из сети — роутеру нужен интернет."
fi
if [ -n "$RPORT" ]; then
  echo "  Удалённый мониторинг: зарегистрирован в хабе как «$RTUN_NAME» (порт $RPORT)."
else
  echo "  Удалённый мониторинг: тумблер во вкладке «Настройки»; авто-регистрация —"
  echo "    RTUN_NAME=имя RTUN_TOKEN=токен [RTUN_ON=1] sh -c \"\$(curl -kfsSL --pinnedpubkey 'sha256//$INFRA_PIN' $BASE/install.sh)\""
fi
