#!/bin/sh # Zapret Manager by StressOzz LuCI installer — самодостаточный скрипт (все файлы зашиты внутри). set -e rm -rf \ /usr/lib/zapret-manager \ /usr/libexec/rpcd/zapret-manager \ /usr/share/luci/menu.d/luci-app-zapret-manager.json \ /usr/share/rpcd/acl.d/luci-app-zapret-manager.json \ /www/luci-static/resources/view/zapret-manager \ /www/luci-static/resources/zapret-manager \ /tmp/zapret-manager \ /tmp/luci-indexcache* \ /tmp/luci-modulecache/* 2>/dev/null /etc/init.d/rpcd restart >/dev/null 2>&1 || true /etc/init.d/uhttpd restart >/dev/null 2>&1 || true mkdir -p /usr/lib/zapret-manager cat > '/usr/lib/zapret-manager/backend.sh' << 'ZM_INSTALLER_EOF' CONF="/etc/config/zapret" GH_RAW="https://raw.githubusercontent.com" GH_MAIN="https://github.com" EXCLUDE_URL="${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/zapret-hosts-user-exclude.txt" FLOWSEAL_ZIP="${GH_MAIN}/Flowseal/zapret-discord-youtube/archive/refs/heads/main.zip" FLOWSEAL_FAKE_RAW="${GH_MAIN}/Flowseal/zapret-discord-youtube/raw/refs/heads/main/bin" STR_URL="${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/files/StrYoutube" JOBS_DIR="/tmp/zapret-manager" HOSTS_FILE="/etc/hosts" EXPERT_MODE_FILE="/etc/zapret_manager_expert_mode" PORTS_UDP="88,1024-2407,2409-4499,4502-19293,19345-49999,50101-65535" PORTS_TCP="2802,2302,2502,3478-3480,3724,6000-8000,8085,8090,8100,8903,8904,25565,27015-27030,27036-27037,35500-35600,50001,60442" mkdir -p "$JOBS_DIR" if command -v opkg >/dev/null 2>&1; then PKG="opkg"; INSTALL="opkg install"; DELETE="opkg remove"; UPDATE="opkg update" TG_ARCH="$(opkg print-architecture 2>/dev/null | awk '{print $2}' | tail -n1)" RAZ="ipk" else PKG="apk"; INSTALL="apk add --allow-untrusted"; DELETE="apk del"; UPDATE="apk update" TG_ARCH="$(apk --print-arch 2>/dev/null)" RAZ="apk" fi # ---------- helpers ---------------------------------------------------- esc() { # JSON-escape a string (arg1) for embedding inside "..." — однострочные значения printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n' ' ' } esc_ml() { # то же самое, но СОХРАНЯЕТ переносы строк (как \n для JSON) — для лога printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g' } _pkg_is_installed() { if [ "$PKG" = "apk" ]; then apk info -e "$1" >/dev/null 2>&1 else opkg list-installed 2>/dev/null | grep -q "^$1 "; fi } _ensure_deps() { # curl и unzip нужны почти всем install-операциям (скачивание релизов, # распаковка Flowseal и т.д.) — ставим один раз в начале. local need="" updated=0 command -v curl >/dev/null 2>&1 || need="$need curl" command -v unzip >/dev/null 2>&1 || need="$need unzip" [ -z "$need" ] && return 0 echo "==> Устанавливаем зависимости:$need" $UPDATE >/dev/null 2>&1 updated=1 $INSTALL $need >/dev/null 2>&1 } job_start() { # job_start local name="$1"; shift local log="$JOBS_DIR/$name.log" local pid="$JOBS_DIR/$name.pid" # Если задача с таким именем уже выполняется (двойной клик, гонка на # фронтенде и т.п.) — НЕ запускаем вторую копию поверх первой: обе будут # писать в одни и те же временные файлы одновременно, что даёт задвоенный # вывод и задвоенные результаты. Вместо этого просто подключаемся к уже # идущей задаче тем же job-именем (started:true — фронтенд и так просто # продолжит опрашивать её через pollJob, ничего менять не нужно). if [ -f "$pid" ] && kill -0 "$(cat "$pid" 2>/dev/null)" 2>/dev/null; then printf '{"started":true,"job":"%s","already_running":true}\n' "$name" return 0 fi : > "$log" ( "$@" >>"$log" 2>&1; echo "__DONE__ $?" >>"$log" ) & echo $! > "$pid" printf '{"started":true,"job":"%s"}\n' "$name" } job_status() { local name="$1" local pid="$JOBS_DIR/$name.pid" local log="$JOBS_DIR/$name.log" local running="false" done="false" rc="" if [ -f "$pid" ] && kill -0 "$(cat "$pid" 2>/dev/null)" 2>/dev/null; then running="true"; fi if [ -f "$log" ] && grep -q '^__DONE__' "$log"; then done="true" rc=$(grep '^__DONE__' "$log" | tail -1 | awk '{print $2}') fi printf '{"running":%s,"done":%s,"rc":"%s"}\n' "$running" "$done" "$rc" } log_tail() { local name="$1" local log="$JOBS_DIR/$name.log" [ -f "$log" ] || { echo '{"lines":""}'; return; } printf '{"lines":"%s"}\n' "$(esc_ml "$(tail -n 300 "$log" | grep -v '^__DONE__')")" } zapret_restart() { [ -x /opt/zapret/sync_config.sh ] && /opt/zapret/sync_config.sh >/dev/null 2>&1 /etc/init.d/zapret restart >/dev/null 2>&1 } # ---------- status ------------------------------------------------------- system_info() { local model arch owrt df_out tmp_used tmp_free root_used root_free model=$(cat /tmp/sysinfo/model 2>/dev/null) arch=$(grep DISTRIB_ARCH /etc/openwrt_release 2>/dev/null | cut -d"'" -f2) owrt=$(grep '^DISTRIB_RELEASE=' /etc/openwrt_release 2>/dev/null | cut -d"'" -f2) df_out=$(df -h /tmp / 2>/dev/null) tmp_used=$(echo "$df_out" | awk 'NR==2{print $3}') tmp_free=$(echo "$df_out" | awk 'NR==2{print $4}') root_used=$(echo "$df_out" | awk 'NR==3{print $3}') root_free=$(echo "$df_out" | awk 'NR==3{print $4}') printf '{"model":"%s","arch":"%s","openwrt":"%s","tmp_used":"%s","tmp_free":"%s","root_used":"%s","root_free":"%s"}\n' \ "$(esc "$model")" "$(esc "$arch")" "$(esc "$owrt")" \ "$(esc "$tmp_used")" "$(esc "$tmp_free")" "$(esc "$root_used")" "$(esc "$root_free")" } status() { local zr="not_installed" zr_running="false" zr_ver="" if [ -f /etc/init.d/zapret ]; then zr="installed" if [ "$PKG" = "opkg" ]; then zr_ver=$(opkg list-installed zapret 2>/dev/null | awk '{sub(/-r[0-9]+$/,"",$3); print $3}') else zr_ver=$(apk info -v 2>/dev/null | grep '^zapret-' | head -n1 | cut -d- -f2 | sed 's/-r[0-9]\+$//') fi pgrep -f "/opt/zapret" >/dev/null 2>&1 && zr_running="true" fi local zr2="not_installed" zr2_running="false" if [ -f /etc/init.d/zapret2 ]; then zr2="installed" /etc/init.d/zapret2 status >/dev/null 2>&1 && zr2_running="true" fi local strat="" fs_marker if [ -f "$CONF" ]; then fs_marker=$(grep -m1 '^# ZMFS:' "$CONF" | sed 's/^# ZMFS://') strat=$(grep -oE '#v[0-9]+|#Yv[0-9]+|#Gv[1-4]|#Dv[0-9]+' "$CONF" | sed 's/^#//' | tr '\n' ' ' | sed 's/ $//') [ -n "$fs_marker" ] && strat="$fs_marker${strat:+ $strat}" fi printf '{"pkg":"%s","zapret":"%s","zapret_running":%s,"zapret_version":"%s","zapret2":"%s","zapret2_running":%s,"strategy":"%s","flowseal":"%s"}\n' \ "$PKG" "$zr" "$zr_running" "$(esc "$zr_ver")" "$zr2" "$zr2_running" "$(esc "$strat")" "$(esc "$fs_marker")" } # ---------- install / remove / start / stop ------------------------------- _zapret_latest_version() { local ver # Читаем заголовок Location из редиректа напрямую (без -L + url_effective) — # надёжнее: не все сборки curl одинаково ведут себя со связкой -w # url_effective + HEAD-запрос + редирект. ver=$(curl -fsSI --connect-timeout 4 --max-time 6 \ "https://github.com/remittor/zapret-openwrt/releases/latest" 2>/dev/null \ | tr -d '\r' | awk -F': ' 'tolower($1)=="location"{print $2}' | tail -n1 | sed 's#.*/v##') # Санити-проверка: версия zapret всегда вида NN.NNNNNNNN — если извлечь # не удалось (пусто, или всё ещё похоже на URL), лучше не показывать # ничего, чем сырую ссылку. echo "$ver" | grep -qE '^[0-9]+\.[0-9]+$' && echo "$ver" || echo "" } zapret_latest_version() { printf '{"version":"%s"}\n' "$(esc "$(_zapret_latest_version)")" } do_install_zapret() { _ensure_deps if [ -f /etc/init.d/zapret2 ] && [ ! -f "$EXPERT_MODE_FILE" ]; then echo "ОШИБКА: установлен Zapret2, он несовместим с Zapret — сначала удалите Zapret2" return 1 fi echo "==> Определяем версию Zapret" local ver arch url tmp ver="$(_zapret_latest_version)" [ -z "$ver" ] && { echo "!! Не удалось определить версию, использую фиксированную 72.20260307"; ver="72.20260307"; } arch="$(awk -F\' '/DISTRIB_ARCH/ {print $2}' /etc/openwrt_release)" url="${GH_MAIN}/remittor/zapret-openwrt/releases/download/v${ver}/zapret_v${ver}_${arch}.zip" tmp="$JOBS_DIR/install_tmp"; rm -rf "$tmp"; mkdir -p "$tmp"; cd "$tmp" || return 1 echo "==> Обновляем список пакетов" $UPDATE if [ -f /etc/init.d/zapret ]; then echo "==> Останавливаем текущий Zapret" /etc/init.d/zapret stop >/dev/null 2>&1 for p in $(pgrep -f /opt/zapret 2>/dev/null); do kill -9 "$p" 2>/dev/null; done fi echo "==> Скачиваем $url" local attempt=1 max_attempts=5 while [ "$attempt" -le "$max_attempts" ]; do rm -f zapret.zip wget -q -U "Mozilla/5.0" -O zapret.zip "$url" >/dev/null 2>&1 command -v unzip >/dev/null 2>&1 || { echo "==> Устанавливаем unzip"; $INSTALL unzip >/dev/null 2>&1; } if [ -s zapret.zip ] && unzip -tq zapret.zip >/dev/null 2>&1; then break fi attempt=$((attempt + 1)) done if [ "$attempt" -gt "$max_attempts" ]; then echo "ОШИБКА: не удалось скачать целый архив за $max_attempts попыток — проверьте соединение с GitHub" return 1 fi unzip -o zapret.zip >/dev/null 2>&1 echo "==> Устанавливаем пакеты" if [ "$PKG" = "apk" ]; then for p in apk/zapret*; do [ -f "$p" ] || continue echo "$p" | grep -q luci && continue echo " - $(basename "$p")"; $INSTALL "$p" >/dev/null 2>&1 || { echo "ОШИБКА: $p"; return 1; } done for p in apk/luci*; do [ -f "$p" ] || continue echo " - $(basename "$p")"; $INSTALL "$p" >/dev/null 2>&1 done else for p in zapret_*.ipk; do [ -f "$p" ] || continue echo " - $(basename "$p")"; $INSTALL "$p" >/dev/null 2>&1 || { echo "ОШИБКА: $p"; return 1; } done for p in luci-app-zapret_*.ipk; do [ -f "$p" ] || continue echo " - $(basename "$p")"; $INSTALL "$p" >/dev/null 2>&1 done fi echo "==> Добавляем домены в исключения" rm -f /opt/zapret/ipset/zapret-hosts-user-exclude.txt wget -q -U "Mozilla/5.0" -O /opt/zapret/ipset/zapret-hosts-user-exclude.txt "$EXCLUDE_URL" do_add_fake_flow echo "==> Включаем автозапуск и custom.d-скрипты" /etc/init.d/zapret enable >/dev/null 2>&1 sed -i "/DISABLE_CUSTOM/s/'1'/'0'/" "$CONF" 2>/dev/null cd /; rm -rf "$tmp" echo "==> Готово, Zapret установлен ($ver)" } do_remove_zapret() { echo "==> Останавливаем Zapret" /etc/init.d/zapret stop >/dev/null 2>&1 for p in $(pgrep -f /opt/zapret 2>/dev/null); do kill -9 "$p" 2>/dev/null; done echo "==> Удаляем пакеты" $DELETE luci-app-zapret >/dev/null 2>&1 $DELETE zapret >/dev/null 2>&1 echo "==> Удаляем файлы" rm -rf /opt/zapret "$CONF" /etc/init.d/zapret /etc/firewall.zapret crontab -l 2>/dev/null | grep -v -i zapret | crontab - 2>/dev/null echo "==> Готово, Zapret удалён" } zapret_action() { local action="$1" case "$action" in install|update) job_start install_zapret do_install_zapret ;; remove) job_start remove_zapret do_remove_zapret ;; start) /etc/init.d/zapret start >/dev/null 2>&1; zapret_restart; status ;; stop) /etc/init.d/zapret stop >/dev/null 2>&1 for p in $(pgrep -f /opt/zapret 2>/dev/null); do kill -9 "$p" 2>/dev/null; done status ;; *) echo '{"error":"неизвестное действие"}' ;; esac } # ---------- Zapret2 install / remove / start / stop ------------------------ # Zapret2 is a routerich.ru-packaged alternative to Zapret, only available # for aarch64_cortex-a53 and mutually exclusive with the main Zapret. do_install_zapret2() { _ensure_deps local arch arch="$(awk -F\' '/DISTRIB_ARCH/ {print $2}' /etc/openwrt_release)" if [ "$arch" != "aarch64_cortex-a53" ]; then echo "ОШИБКА: Zapret2 поддерживает только архитектуру aarch64_cortex-a53 (у вас: $arch)" return 1 fi if [ -f /etc/init.d/zapret ] && [ ! -f "$EXPERT_MODE_FILE" ]; then echo "ОШИБКА: установлен основной Zapret, он несовместим с Zapret2 — сначала удалите Zapret" return 1 fi local base_url raz if [ "$PKG" = "apk" ]; then base_url="https://packages.routerich.ru/25.12/mediatek/filogic/routerich/"; raz="apk" else base_url="https://packages.routerich.ru/24.10/mediatek/filogic/routerich/"; raz="ipk" fi local tmp="$JOBS_DIR/install_z2_tmp"; rm -rf "$tmp"; mkdir -p "$tmp"; cd "$tmp" || return 1 echo "==> Ищем актуальный пакет Zapret2 в $base_url" curl -fsS --connect-timeout 8 --max-time 15 "$base_url" -o index.html \ || { echo "ОШИБКА: не удалось получить список пакетов"; return 1; } local main_file luci_file if [ "$PKG" = "apk" ]; then main_file=$(grep -oE 'zapret2-[0-9][^"]*\.apk' index.html | head -n1) luci_file=$(grep -oE 'luci-app-zapret2-[0-9][^"]*\.apk' index.html | head -n1) else main_file=$(grep -oE 'zapret2_[0-9][^"]*_aarch64_cortex-a53\.ipk' index.html | head -n1) luci_file=$(grep -oE 'luci-app-zapret2_[0-9][^"]*_all\.ipk' index.html | head -n1) fi [ -z "$main_file" ] && { echo "ОШИБКА: пакет zapret2 не найден в репозитории"; return 1; } echo "==> Скачиваем $main_file" curl -fsSL --max-time 30 "${base_url}${main_file}" -o "$main_file" || { echo "ОШИБКА скачивания $main_file"; return 1; } if [ -n "$luci_file" ]; then echo "==> Скачиваем $luci_file" curl -fsSL --max-time 30 "${base_url}${luci_file}" -o "$luci_file" fi echo "==> Обновляем список пакетов" $UPDATE echo "==> Устанавливаем" $INSTALL ./*."$raz" || { echo "ОШИБКА установки"; return 1; } echo "==> Добавляем домены в исключения" mkdir -p /opt/zapret2/ipset wget -q -U "Mozilla/5.0" -O /opt/zapret2/ipset/zapret_hosts_user_exclude.txt "$EXCLUDE_URL" echo "==> Настраиваем стратегии" mkdir -p /opt/zapret2/init.d/openwrt/custom.d wget -q -U "Mozilla/5.0" -O /opt/zapret2/init.d/openwrt/custom.d/50-discord_media.sh \ "${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/files/Zapret2/50-discord_media.sh" \ || echo "!! Не удалось загрузить 50-discord_media.sh" wget -q -U "Mozilla/5.0" -O /etc/config/zapret2 \ "${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/files/Zapret2/zapret2" \ || echo "!! Не удалось загрузить zapret2" wget -q -U "Mozilla/5.0" -O /opt/zapret2/ipset/zapret_hosts_discord.txt \ "${GH_RAW}/StressOzz/Zapret-Manager/refs/heads/main/files/Zapret2/zapret_hosts_discord.txt" \ || echo "!! Не удалось загрузить zapret_hosts_discord.txt" echo "==> Запускаем Zapret2" /etc/init.d/zapret2 enable >/dev/null 2>&1 /etc/init.d/zapret2 restart >/dev/null 2>&1 cd /; rm -rf "$tmp" echo "==> Готово, Zapret2 установлен" } do_remove_zapret2() { echo "==> Останавливаем Zapret2" /etc/init.d/zapret2 stop >/dev/null 2>&1 echo "==> Удаляем пакеты" $DELETE luci-app-zapret2 >/dev/null 2>&1 $DELETE zapret2 >/dev/null 2>&1 echo "==> Удаляем файлы" rm -f /etc/config/zapret2 rm -rf /opt/zapret2 echo "==> Готово, Zapret2 удалён" } zapret2_action() { local action="$1" case "$action" in install|update) job_start install_zapret2 do_install_zapret2 ;; remove) job_start remove_zapret2 do_remove_zapret2 ;; start) /etc/init.d/zapret2 start >/dev/null 2>&1; status ;; stop) /etc/init.d/zapret2 stop >/dev/null 2>&1; status ;; *) echo '{"error":"неизвестное действие"}' ;; esac } # ---------- strategies v1..v10 (constants copied verbatim from the CLI) -- strategy_v1() { printf '%s\n' "#v1" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=split2" "--dpi-desync-split-seqovl=681" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/stun.bin"; } strategy_v2() { printf '%s\n' "#v2" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=fake,multisplit" "--dpi-desync-split-seqovl=681" "--dpi-desync-split-pos=1" "--dpi-desync-fooling=ts" "--dpi-desync-repeats=8" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/stun.bin" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com"; } strategy_v3() { printf '%s\n' "#v3" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=hostfakesplit" "--dpi-desync-hostfakesplit-mod=host=ozon.ru" "--dpi-desync-repeats=4" "--dpi-desync-fooling=ts,md5sig" "--dpi-desync-badseq-increment=0"; } strategy_v4() { printf '%s\n' "#v4" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=multisplit" "--dpi-desync-split-seqovl=582" "--dpi-desync-split-pos=1" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/stun.bin"; } strategy_v5() { printf '%s\n' "#v5" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=fake,fakeddisorder" "--dpi-desync-split-pos=1" "--dpi-desync-fake-tls=/opt/zapret/files/fake/stun.bin" "--dpi-desync-fake-tls-mod=none" "--dpi-desync-fakedsplit-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-fooling=badseq,badsum" "--dpi-desync-badseq-increment=0"; } strategy_v6() { printf '%s\n' "#v6" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=hostfakesplit" "--dpi-desync-hostfakesplit-mod=host=i2.photo.2gis.com" "--dpi-desync-hostfakesplit-midhost=host-2" "--dpi-desync-split-seqovl=726" "--dpi-desync-fooling=badsum,badseq" "--dpi-desync-badseq-increment=0"; } strategy_v7() { printf '%s\n' "#v7" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=fake,multisplit" "--dpi-desync-split-seqovl=654" "--dpi-desync-split-pos=1" "--dpi-desync-fooling=badseq,badsum" "--dpi-desync-repeats=8" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/stun.bin" "--dpi-desync-fake-tls=/opt/zapret/files/fake/stun.bin" "--dpi-desync-badseq-increment=0"; } strategy_v8() { printf '%s\n' "#v8" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=fake" "--dpi-desync-fooling=ts" "--dpi-desync-fake-tls=/opt/zapret/files/fake/4pda.bin" "--dpi-desync-fake-tls-mod=none"; } strategy_v9() { printf '%s\n' "#v9" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=hostfakesplit" "--dpi-desync-fooling=badseq,badsum" "--dpi-desync-hostfakesplit-mod=host=ozon.ru" "--dpi-desync-badseq-increment=0"; } strategy_v10() { printf '%s\n' "#v10" "--filter-tcp=443" "--hostlist-exclude=/opt/zapret/ipset/zapret-hosts-user-exclude.txt" "--dpi-desync=fake,split2" "--dpi-desync-split-pos=2" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-hostfakesplit-mod=host=maxcdn.bootstrapcdn.com" "--dpi-desync-fake-tls-mod=rnd,sni=maxcdn.bootstrapcdn.com" "--dpi-desync-fooling=ts"; } # ---------- shared post-apply helpers (verbatim behaviour from CLI) ------ _add_gp_domains() { local f="/opt/zapret/ipset/zapret-hosts-google.txt" mkdir -p "$(dirname "$f")"; touch "$f" printf '%s\n' "gvt1.com" "googleplay.com" "play.google.com" "beacons.gvt2.com" \ "play.googleapis.com" "play-fe.googleapis.com" "lh3.googleusercontent.com" \ "android.clients.google.com" "connectivitycheck.gstatic.com" \ "play-lh.googleusercontent.com" "play-games.googleusercontent.com" \ "prod-lt-playstoregatewayadapter-pa.googleapis.com" "youtubei.youtube.com" \ | grep -Fxv -f "$f" 2>/dev/null >> "$f" } _refresh_exclude_file() { rm -f /opt/zapret/ipset/zapret-hosts-user-exclude.txt wget -q -U "Mozilla/5.0" -O /opt/zapret/ipset/zapret-hosts-user-exclude.txt "$EXCLUDE_URL" } _add_yv_default() { # adds a basic YouTube (Yv08) block if the strategy has none and isn't a Flowseal "general" one if ! grep -q "^#Yv" "$CONF" && ! grep -q "^#general" "$CONF"; then sed -i "/^[[:space:]]*option NFQWS_OPT '/a\\#Yv08\\n--filter-tcp=443\\n--hostlist=/opt/zapret/ipset/zapret-hosts-google.txt\\n--dpi-desync=hostfakesplit\\n--dpi-desync-hostfakesplit-mod=host=google.com\\n--dpi-desync-fooling=ts\\n--new" "$CONF" fi } _discord_str_add() { # adds the base discord,stun UDP block + a default Dv1 discord.media block if missing if ! grep -q "option NFQWS_PORTS_UDP.*19294-19344,50000-50100" "$CONF"; then sed -i "/^[[:space:]]*option NFQWS_PORTS_UDP '/s/'\$/,19294-19344,50000-50100'/" "$CONF" fi if ! grep -q "option NFQWS_PORTS_TCP.*2053,2083,2087,2096,8443" "$CONF"; then sed -i "/^[[:space:]]*option NFQWS_PORTS_TCP '/s/'\$/,2053,2083,2087,2096,8443'/" "$CONF" fi if ! grep -q -- "--filter-udp=19294-19344,50000-50100" "$CONF"; then local last_line1 last_line1=$(grep -n "^'\$" "$CONF" | tail -n1 | cut -d: -f1) [ -n "$last_line1" ] && sed -i "${last_line1},\$d" "$CONF" printf "%s\n" "--new" "--filter-udp=19294-19344,50000-50100" "--filter-l7=discord,stun" \ "--dpi-desync=fake" "--dpi-desync-fake-discord=/opt/zapret/files/fake/stun.bin" \ "--dpi-desync-fake-stun=/opt/zapret/files/fake/stun.bin" "--dpi-desync-repeats=6" \ "#Dv1" "--new" "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" \ "--dpi-desync=multisplit" "--dpi-desync-split-seqovl=652" "--dpi-desync-split-pos=2" \ "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" \ "'" >> "$CONF" fi } _ts_warning() { grep -q "=ts" "$CONF" 2>/dev/null && echo true || echo false; } strategy_list_v() { printf '{"items":[%s]}\n' "$( i=1 while [ "$i" -le 10 ]; do printf '{"id":"v%s","label":"Стратегия v%s"}' "$i" "$i" [ "$i" -lt 10 ] && printf ',' i=$((i + 1)) done )" } strategy_set_v() { local version="$1" echo "$version" | grep -qE '^v([1-9]|10)$' || { echo '{"error":"некорректная версия"}'; return 1; } [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } sed -i '/^# ZMFS:/d' "$CONF" sed -i "/^[[:space:]]*option NFQWS_OPT '/,\$d" "$CONF" { echo " option NFQWS_OPT '"; strategy_"$version"; echo "'"; } >> "$CONF" _add_gp_domains _refresh_exclude_file _add_yv_default _discord_str_add zapret_restart printf '{"ok":true,"strategy":"%s","ts_warning":%s}\n' "$version" "$(_ts_warning)" } # ---------- Flowseal strategies ----------------------------------------- _flowseal_file() { echo "$JOBS_DIR/flowseal_strategies.txt"; } do_add_fake_flow() { # Порт ADD_FAKE_FLOW: докачивает дополнительные fake-файлы, которых нет # в базовом пакете Zapret, но на которые ссылаются стратегии Flowseal. [ -d /opt/zapret ] || return 0 [ -f "$CONF" ] || return 0 local f msg=0 for f in stun2.bin quic_initial_tencent_com.bin quic_initial_steamcommunity_com.bin \ tls_clienthello_sochi_park.bin quic_initial_4pda_to.bin quic_initial_5ka_ru.bin \ tls_clienthello_5ka_ru.bin quic_initial_rutube_ru.bin; do if [ ! -f "/opt/zapret/files/fake/$f" ]; then [ "$msg" = 0 ] && { echo "==> Скачиваем дополнительные fake-файлы"; msg=1; } wget -q -U "Mozilla/5.0" -O "/opt/zapret/files/fake/$f" "${FLOWSEAL_FAKE_RAW}/$f" \ || echo "!! Не удалось загрузить файл $f" fi done } do_flowseal_download() { _ensure_deps local out zip tmp attempt=1 max_attempts=5 out="$(_flowseal_file)"; zip="$JOBS_DIR/flowseal.zip"; tmp="$JOBS_DIR/flowseal_src" echo "==> Скачиваем список стратегий Flowseal" rm -rf "$tmp" "$zip"; : > "$out" command -v unzip >/dev/null 2>&1 || $INSTALL unzip >/dev/null 2>&1 while [ "$attempt" -le "$max_attempts" ]; do rm -f "$zip" wget -q -U "Mozilla/5.0" -O "$zip" "$FLOWSEAL_ZIP" >/dev/null 2>&1 if [ -s "$zip" ] && unzip -tq "$zip" >/dev/null 2>&1; then break fi attempt=$((attempt + 1)) done if [ "$attempt" -gt "$max_attempts" ]; then echo "ОШИБКА: не удалось скачать целый архив за $max_attempts попыток — проверьте соединение с GitHub" return 1 fi mkdir -p "$tmp"; unzip -oq "$zip" -d "$tmp" || { echo "ОШИБКА распаковки"; return 1; } local base="$tmp/zapret-discord-youtube-main" echo "==> Разбираем .bat-стратегии" find "$base" -type f -name 'general*.bat' ! -name 'general (ALT5).bat' | while read -r F; do MATCH=$(grep -E '^--filter-udp=19294-19344,50000-50100|^--filter-tcp=%GameFilterTCP%|^--filter-udp=%GameFilterUDP%|^--filter-tcp=2053,2083,2087,2096,8443|^--filter-tcp=443 --hostlist="%LISTS%list-google.txt"|^--filter-tcp=80,443 --hostlist="%LISTS%list-general.txt"' "$F") [ -z "$MATCH" ] && continue NAME=$(basename "$F" .bat) { echo "#$NAME"; echo "$MATCH" | sed 's/--/\n--/g' | sed '/^$/d' | sed 's/[[:space:]]*$//'; echo; } >> "$out" done echo "==> Приводим пути и плейсхолдеры к реальным" # служебные списки/ipset — заменяем на реальные пути или вычищаем sed -i '/--hostlist="%LISTS%list-general.txt"/d' "$out" sed -i '/--ipset="%LISTS%ipset-all.txt"/d' "$out" sed -i '/--hostlist="%LISTS%list-general-user.txt"/d' "$out" sed -i '/--ipset-exclude="%LISTS%ipset-exclude.txt"/d' "$out" sed -i '/--ipset-exclude="%LISTS%ipset-exclude-user.txt"/d' "$out" sed -i '/--hostlist-exclude="%LISTS%list-exclude-user.txt"/d' "$out" sed -i 's|"%LISTS%list-exclude.txt"|/opt/zapret/ipset/zapret-hosts-user-exclude.txt|g' "$out" sed -i 's|"%LISTS%list-google.txt"|/opt/zapret/ipset/zapret-hosts-google.txt|g' "$out" sed -i 's/--new[[:space:]]\^/--new/g' "$out" # fake-бинарники — каждый плейсхолдер на свой реальный путь в /opt/zapret/files/fake sed -i 's|"%BIN%tls_clienthello_www_google_com.bin"|/opt/zapret/files/fake/tls_clienthello_www_google_com.bin|g' "$out" sed -i 's|"%BIN%tls_clienthello_sochi_park.bin"|/opt/zapret/files/fake/tls_clienthello_sochi_park.bin|g' "$out" sed -i 's|"%BIN%stun.bin"|/opt/zapret/files/fake/stun.bin|g' "$out" sed -i 's|"%BIN%tls_clienthello_4pda_to.bin"|/opt/zapret/files/fake/4pda.bin|g' "$out" sed -i 's|"%BIN%quic_initial_www_google_com.bin"|/opt/zapret/files/fake/quic_initial_www_google_com.bin|g' "$out" sed -i 's|"%BIN%stun2.bin"|/opt/zapret/files/fake/stun2.bin|g' "$out" sed -i 's|"%BIN%quic_initial_tencent_com.bin"|/opt/zapret/files/fake/quic_initial_tencent_com.bin|g' "$out" sed -i 's|"%BIN%quic_initial_steamcommunity_com.bin"|/opt/zapret/files/fake/quic_initial_steamcommunity_com.bin|g' "$out" sed -i 's|"%BIN%quic_initial_4pda_to.bin"|/opt/zapret/files/fake/quic_initial_4pda_to.bin|g' "$out" sed -i 's|"%BIN%ACTIVE_DISCORD_UDP.bin"|/opt/zapret/files/fake/quic_initial_steamcommunity_com.bin|g' "$out" sed -i 's|"%BIN%ACTIVE_GAME_UDP.bin"|/opt/zapret/files/fake/quic_initial_4pda_to.bin|g' "$out" sed -i 's|"%BIN%tls_clienthello_max_ru.bin"|/opt/zapret/files/fake/tls_clienthello_www_onetrust_com.bin|g' "$out" sed -i 's|"%BIN%quic_initial_5ka_ru.bin"|/opt/zapret/files/fake/quic_initial_5ka_ru.bin|g' "$out" sed -i 's|"%BIN%quic_initial_rutube_ru.bin"|/opt/zapret/files/fake/quic_initial_rutube_ru.bin|g' "$out" sed -i 's|\^!|/opt/zapret/files/fake/tls_clienthello_www_google_com.bin|g' "$out" # игровые порты sed -i "s|%GameFilterTCP%|$PORTS_TCP|g" "$out" sed -i "s|%GameFilterUDP%|$PORTS_UDP|g" "$out" # финальная зачистка: хвостовые пробелы, пустые строки, дублирующиеся --new подряд sed -i 's/[[:space:]]\+$//' "$out" sed -i '/^[[:space:]]*$/d' "$out" sed -i '/^--new$/ { N; /^--new\n$/d; }' "$out" rm -rf "$tmp" "$zip" do_add_fake_flow echo "==> Готово, стратегий: $(grep -c '^#' "$out")" } strategy_list_flowseal() { local f; f="$(_flowseal_file)" if [ ! -s "$f" ]; then job_start flowseal_download do_flowseal_download return fi printf '{"items":[%s]}\n' "$( grep '^#' "$f" | sed 's/^#//' | awk '{ gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); printf "%s{\"id\":\"%s\",\"label\":\"%s\"}", (NR>1?",":""), $0, $0 }' )" } strategy_set_flowseal() { local name="$1" f block f="$(_flowseal_file)" [ -s "$f" ] || { echo '{"error":"список не загружен — сначала обновите"}'; return 1; } [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } block=$(awk -v n="#$name" '$0==n{flag=1; print; next} /^#/ && flag{exit} flag{print}' "$f") [ -z "$block" ] && { echo '{"error":"стратегия не найдена"}'; return 1; } sed -i '/^# ZMFS:/d' "$CONF" { printf '# ZMFS:%s\n' "$name"; cat "$CONF"; } > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" sed -i "/option NFQWS_OPT '/,\$d" "$CONF" { echo " option NFQWS_OPT '"; echo "$block"; echo "'"; } >> "$CONF" if ! grep -q "option NFQWS_PORTS_UDP.*19294-19344,50000-50100" "$CONF"; then sed -i "/^[[:space:]]*option NFQWS_PORTS_UDP '/s/'\$/,19294-19344,50000-50100'/" "$CONF" fi if ! grep -q "option NFQWS_PORTS_TCP.*2053,2083,2087,2096,8443" "$CONF"; then sed -i "/^[[:space:]]*option NFQWS_PORTS_TCP '/s/'\$/,2053,2083,2087,2096,8443'/" "$CONF" fi _add_gp_domains _refresh_exclude_file # помечаем базовую игровую секцию (Gv0) — как в оригинальном flowseal_menu sed -i '/--new/{N;/--filter-tcp=2802/{s/--new/#Gv0\n--new/;};}' "$CONF" zapret_restart printf '{"ok":true,"strategy":"%s","ts_warning":%s}\n' "$(esc "$name")" "$(_ts_warning)" } # ---------- YouTube Yv strategies (ported from choose_strategy_YOUTUBE) -- _yv_file() { echo "$JOBS_DIR/youtube_strategies.txt"; } do_yv_download() { _ensure_deps local out; out="$(_yv_file)" echo "==> Скачиваем список стратегий для YouTube" curl -fsSL --connect-timeout 8 --max-time 15 "$STR_URL" -o "$out" || { echo "ОШИБКА скачивания"; return 1; } echo "==> Готово, стратегий: $(grep -c '^#Yv' "$out")" } strategy_list_youtube() { local f; f="$(_yv_file)" if [ ! -s "$f" ]; then job_start youtube_download do_yv_download return fi printf '{"items":[%s]}\n' "$( grep -oE '^#Yv[0-9]+' "$f" | sed 's/^#//' | awk '{ printf "%s{\"id\":\"%s\"}", (NR>1?",":""), $0 }' )" } strategy_set_youtube() { local name="$1" f selected f="$(_yv_file)" [ -s "$f" ] || { echo '{"error":"список не загружен — сначала обновите"}'; return 1; } [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } selected="#$name" grep -qxF "$selected" "$f" || { echo '{"error":"стратегия не найдена"}'; return 1; } local saved="$JOBS_DIR/yv_saved" newtmp="$JOBS_DIR/yv_new" finaltmp="$JOBS_DIR/yv_final" oldtmp="$JOBS_DIR/yv_old" local awk_strip="$JOBS_DIR/yv_strip.awk" awk_insert="$JOBS_DIR/yv_insert.awk" awk_dedup="$JOBS_DIR/yv_dedup.awk" local flag=0 : > "$saved" while IFS= read -r line; do [ "$line" = "$selected" ] && flag=1 && continue case "$line" in \#Yv[0-9]*) flag=0 ;; esac [ "$flag" -eq 1 ] && printf '%s\n' "$line" >> "$saved" done < "$f" # 1) вырезаем текущий блок option NFQWS_OPT '...' в отдельный файл, чистим от старых #Yv local awk_cut="$JOBS_DIR/yv_cut.awk" cat > "$awk_cut" << 'AWK_CUT_EOF' /^[ \t]*option NFQWS_OPT '$/ { flag = 1 } flag { print } AWK_CUT_EOF awk -f "$awk_cut" "$CONF" > "$oldtmp" rm -f "$awk_cut" sed -i "/^[[:space:]]*option NFQWS_OPT '/,\$d" "$CONF" sed -i "/^[[:space:]]*#Yv[0-9]\+/d" "$oldtmp" # 2) вырезаем существующую YouTube-секцию (--filter-tcp=443 + hostlist=google), если была cat > "$awk_strip" << 'AWK_STRIP_EOF' { if (skip) { if ($0 == "--new" || $0 ~ /^[ \t]*'$/) { skip = 0; next } if ($0 ~ /^[ \t]*$/) next next } if ($0 == "--filter-tcp=443") { getline n if (n == "--hostlist=/opt/zapret/ipset/zapret-hosts-google.txt") { skip = 1; next } else { print $0; print n; next } } if ($0 == "--hostlist=/opt/zapret/ipset/zapret-hosts-google.txt") has_google = 1 if ($0 ~ /^[ \t]*#Yv/) next print } AWK_STRIP_EOF awk -f "$awk_strip" "$oldtmp" > "$newtmp" # 3) вставляем выбранную Yv-стратегию: сразу после первого "--new" (если секция уже была), # иначе сразу после открывающей строки option NFQWS_OPT ' cat > "$awk_insert" << 'AWK_INSERT_EOF' BEGIN { inserted = 0; has_google = 0 } $0 == "--hostlist=/opt/zapret/ipset/zapret-hosts-google.txt" { has_google = 1 } $0 == "--new" && !inserted { while ((getline l < savedfile) > 0) if (l !~ /^[ \t]*$/) print l print "--new" inserted = 1 next } $0 ~ /^[ \t]*option NFQWS_OPT '$/ && !has_google && !inserted { print print sel while ((getline l < savedfile) > 0) if (l !~ /^[ \t]*$/) print l print "--new" inserted = 1 next } { print } AWK_INSERT_EOF awk -v sel="$selected" -v savedfile="$saved" -f "$awk_insert" "$newtmp" > "$finaltmp" cat "$finaltmp" >> "$CONF" # 4) убираем случайные дубликаты подряд идущих "--new" cat > "$awk_dedup" << 'AWK_DEDUP_EOF' { if ($0 == "--new") { if (prev != "--new") print } else print prev = $0 } AWK_DEDUP_EOF awk -f "$awk_dedup" "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" grep -qE "^[ \t]*'[ \t]*\$" "$CONF" || echo "'" >> "$CONF" _add_gp_domains zapret_restart rm -f "$saved" "$newtmp" "$finaltmp" "$oldtmp" "$awk_strip" "$awk_insert" "$awk_dedup" printf '{"ok":true,"strategy":"%s","ts_warning":%s}\n' "$(esc "$name")" "$(_ts_warning)" } # ---------- Discord Dv strategies (all 17, verbatim from CLI) ----------- Dv1() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=multisplit" "--dpi-desync-split-seqovl=652" "--dpi-desync-split-pos=2" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv2() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,multisplit" "--dpi-desync-split-seqovl=681" "--dpi-desync-split-pos=1" "--dpi-desync-fooling=ts" "--dpi-desync-repeats=8" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com"; } Dv3() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake" "--dpi-desync-repeats=6" "--dpi-desync-fooling=ts" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-fake-tls-mod=none"; } Dv4() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=multisplit" "--dpi-desync-split-seqovl=652" "--dpi-desync-split-pos=2" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv5() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,multisplit" "--dpi-desync-repeats=6" "--dpi-desync-fooling=badseq" "--dpi-desync-badseq-increment=1000" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv6() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=multisplit" "--dpi-desync-split-seqovl=681" "--dpi-desync-split-pos=1" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv7() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=multisplit" "--dpi-desync-split-pos=2,sniext+1" "--dpi-desync-split-seqovl=679" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv8() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake" "--dpi-desync-fake-tls-mod=none" "--dpi-desync-repeats=6" "--dpi-desync-fooling=badseq" "--dpi-desync-badseq-increment=2"; } Dv9() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,fakedsplit" "--dpi-desync-split-pos=1" "--dpi-desync-fooling=badseq" "--dpi-desync-badseq-increment=2" "--dpi-desync-repeats=8" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com"; } Dv10() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,multisplit" "--dpi-desync-split-seqovl=681" "--dpi-desync-split-pos=1" "--dpi-desync-fooling=badseq" "--dpi-desync-badseq-increment=10000000" "--dpi-desync-repeats=8" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com"; } Dv11() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,multisplit" "--dpi-desync-split-seqovl=681" "--dpi-desync-split-pos=1" "--dpi-desync-fooling=ts" "--dpi-desync-repeats=8" "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com"; } Dv12() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake" "--dpi-desync-repeats=6" "--dpi-desync-fooling=badseq" "--dpi-desync-badseq-increment=2" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv13() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake" "--dpi-desync-repeats=6" "--dpi-desync-fooling=ts" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv14() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,fakedsplit" "--dpi-desync-repeats=6" "--dpi-desync-fooling=ts" "--dpi-desync-fakedsplit-pattern=0x00" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin"; } Dv15() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,multidisorder" "--dpi-desync-split-pos=1,midsld" "--dpi-desync-repeats=11" "--dpi-desync-fooling=badseq" "--dpi-desync-fake-tls=0x00000000" "--dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com"; } Dv16() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=fake,hostfakesplit" "--dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com" "--dpi-desync-hostfakesplit-mod=host=www.google.com,altorder=1" "--dpi-desync-fooling=ts"; } Dv17() { printf '%s\n' "--filter-tcp=2053,2083,2087,2096,8443" "--hostlist-domains=discord.media" "--dpi-desync=hostfakesplit" "--dpi-desync-repeats=4" "--dpi-desync-fooling=ts" "--dpi-desync-hostfakesplit-mod=host=www.google.com"; } discord_status() { local dv="" fake="" if [ -f "$CONF" ]; then dv=$(grep -oE '#Dv[0-9]+' "$CONF" | head -n1 | sed 's/#//') fake=$(grep -m1 -- '--dpi-desync-fake-discord=' "$CONF" | sed 's|.*fake/||') fi printf '{"current":"%s","current_fake":"%s","available":["Dv1","Dv2","Dv3","Dv4","Dv5","Dv6","Dv7","Dv8","Dv9","Dv10","Dv11","Dv12","Dv13","Dv14","Dv15","Dv16","Dv17"]}\n' "$(esc "$dv")" "$(esc "$fake")" } discord_set_dv() { local num="$1" strat echo "$num" | grep -qE '^(1[0-7]|[1-9])$' || { echo '{"error":"некорректный номер Dv"}'; return 1; } [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } strat="$(Dv"$num")" grep -q -E '^[[:space:]]*--filter-tcp=2053,2083,2087,2096,8443' "$CONF" || { echo '{"error":"блок discord.media отсутствует — сначала установите базовую стратегию с поддержкой Discord"}'; return 1; } local start end line start=$(grep -n -E '^[[:space:]]*--filter-tcp=2053,2083,2087,2096,8443' "$CONF" | head -n1 | cut -d: -f1) end=$(tail -n +"$start" "$CONF" | grep -n -m1 -E '^--new$|^#|^'"'"'$' | cut -d: -f1) end=$((start + end - 1)) sed -i "${start},$((end - 1))d" "$CONF" line=$start echo "$strat" | while IFS= read -r l; do sed -i "${line}i$l" "$CONF"; line=$((line + 1)); done if grep -q -E '^#[[:space:]]*Dv' "$CONF"; then sed -i "s/^#[[:space:]]*Dv[0-9]\+/#Dv$num/" "$CONF" else sed -i "${start}i#Dv$num" "$CONF" fi zapret_restart printf '{"ok":true,"dv":"Dv%s"}\n' "$num" } discord_set_fake() { local file="$1" case "$file" in stun.bin|stun2.bin|quic_initial_4pda_to.bin|quic_initial_tencent_com.bin|tls_clienthello_sochi_park.bin|quic_initial_www_google_com.bin|quic_initial_steamcommunity_com.bin|quic_initial_5ka_ru.bin|quic_initial_rutube_ru.bin) ;; *) echo '{"error":"неизвестный fake-файл"}'; return 1 ;; esac [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } grep -q -- "--filter-l7=discord,stun" "$CONF" || { echo '{"error":"блок discord,stun не найден"}'; return 1; } awk -v new="$file" '{ if ($0 == "--filter-l7=discord,stun") { print; getline; print; if ($0 == "--dpi-desync=fake") { getline a; getline b; if (a ~ /^--dpi-desync-fake-discord=/) { print "--dpi-desync-fake-discord=/opt/zapret/files/fake/" new print "--dpi-desync-fake-stun=/opt/zapret/files/fake/" new } else { print a; print b } } next } print }' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" zapret_restart printf '{"ok":true,"fake":"%s"}\n' "$(esc "$file")" } # ---------- hosts blocks (subset, verbatim constants from CLI) ----------- _hosts_block() { case "$1" in nalog) printf '%s\n' \ "#Nalog" \ "213.24.64.175 lkfl2.nalog.ru" \ "213.24.64.181 lknpd.nalog.ru" ;; rutor) printf '%s\n' \ "#rutor" \ "173.245.58.219 rutor.info d.rutor.info" ;; ntc) printf '%s\n' \ "#ntc.party" \ "130.255.77.28 ntc.party" ;; instagram) printf '%s\n' \ "#Instagram&Facebook" \ "57.144.222.34 instagram.com www.instagram.com" \ "157.240.9.174 instagram.com www.instagram.com" \ "157.240.245.174 instagram.com www.instagram.com b.i.instagram.com z-p42-chat-e2ee-ig.facebook.com help.instagram.com" \ "157.240.205.174 instagram.com www.instagram.com" \ "57.144.244.192 static.cdninstagram.com graph.instagram.com i.instagram.com api.instagram.com edge-chat.instagram.com" \ "31.13.66.63 scontent.cdninstagram.com scontent-hel3-1.cdninstagram.com" \ "57.144.244.1 facebook.com www.facebook.com fb.com fbsbx.com" \ "57.144.244.128 static.xx.fbcdn.net scontent.xx.fbcdn.net" \ "31.13.67.20 scontent-hel3-1.xx.fbcdn.net" ;; librusec) printf '%s\n' \ "#lib.rus.ec" \ "185.39.18.98 lib.rus.ec www.lib.rus.ec" ;; ai) printf '%s\n' \ "#Gemini" \ "45.155.204.190 gemini.google.com" \ "#Grok" \ "45.155.204.190 grok.com accounts.x.ai assets.grok.com" \ "#OpenAI" \ "45.155.204.190 chatgpt.com ab.chatgpt.com auth.openai.com auth0.openai.com platform.openai.com cdn.oaistatic.com" \ "45.155.204.190 tcr9i.chat.openai.com webrtc.chatgpt.com android.chat.openai.com api.openai.com operator.chatgpt.com" \ "45.155.204.190 sora.chatgpt.com sora.com videos.openai.com ios.chat.openai.com cdn.auth0.com files.oaiusercontent.com" \ "#Microsoft" \ "45.155.204.190 copilot.microsoft.com sydney.bing.com edgeservices.bing.com rewards.bing.com" \ "45.155.204.190 xsts.auth.xboxlive.com xgpuwebf2p.gssv-play-prod.xboxlive.com xgpuweb.gssv-play-prod.xboxlive.com" \ "#ElevenLabs" \ "45.155.204.190 elevenlabs.io api.us.elevenlabs.io elevenreader.io api.elevenlabs.io help.elevenlabs.io" \ "#DeepL" \ "45.155.204.190 deepl.com www.deepl.com www2.deepl.com login-wall.deepl.com w.deepl.com dict.deepl.com ita-free.www.deepl.com" \ "45.155.204.190 write-free.www.deepl.com experimentation.deepl.com experimentation-grpc.deepl.com ita-free.app.deepl.com" \ "45.155.204.190 ott.deepl.com api-free.deepl.com backend.deepl.com clearance.deepl.com errortracking.deepl.com" \ "45.155.204.190 oneshot-free.www.deepl.com checkout.www.deepl.com gtm.deepl.com auth.deepl.com shield.deepl.com" \ "#Claude" \ "45.155.204.190 claude.ai console.anthropic.com api.anthropic.com" \ "#Trae.ai" \ "45.155.204.190 trae-api-sg.mchost.guru api.trae.ai api-sg-central.trae.ai api16-normal-alisg.mchost.guru" \ "#Windsurf" \ "45.155.204.190 windsurf.com codeium.com server.codeium.com web-backend.codeium.com marketplace.windsurf.com" \ "45.155.204.190 unleash.codeium.com inference.codeium.com windsurf-stable.codeium.com" \ "144.31.14.104 windsurf-telemetry.codeium.com" \ "#Manus" \ "45.155.204.190 manus.im api.manus.im" \ "#Notion" \ "45.155.204.190 www.notion.so calendar.notion.so" \ "#AIStudio" \ "45.155.204.190 aistudio.google.com generativelanguage.googleapis.com aitestkitchen.withgoogle.com aisandbox-pa.googleapis.com xsts.auth.xboxlive.com" \ "45.155.204.190 webchannel-alkalimakersuite-pa.clients6.google.com alkalimakersuite-pa.clients6.google.com assistant-s3-pa.googleapis.com" \ "45.155.204.190 proactivebackend-pa.googleapis.com robinfrontend-pa.googleapis.com o.pki.goog labs.google labs.google.com notebooklm.google" \ "45.155.204.190 notebooklm.google.com jules.google.com stitch.withgoogle.com gemini.google.com copilot.microsoft.com edgeservices.bing.com" \ "45.155.204.190 rewards.bing.com sydney.bing.com xboxdesignlab.xbox.com xgpuweb.gssv-play-prod.xboxlive.com xgpuwebf2p.gssv-play-prod.xboxlive.com" ;; twitch) printf '%s\n' \ "#Twitch" \ "45.155.204.190 usher.ttvnw.net gql.twitch.tv" ;; telegram) printf '%s\n' \ "#TelegramWeb" \ "149.154.167.220 core.telegram.org api.telegram.org flora.web.telegram.org kws1-1.web.telegram.org kws1.web.telegram.org kws2-1.web.telegram.org kws2.web.telegram.org kws4-1.web.telegram.org" \ "149.154.167.220 kws4.web.telegram.org kws5-1.web.telegram.org kws5.web.telegram.org pluto-1.web.telegram.org pluto.web.telegram.org td.telegram.org telegram.dog" \ "149.154.167.220 telegram.me telegram.org telegram.space telesco.pe venus.web.telegram.org web.telegram.org zws1-1.web.telegram.org zws1.web.telegram.org" \ "149.154.167.220 tg.dev t.me zws2-1.web.telegram.org zws2.web.telegram.org zws4-1.web.telegram.org zws5-1.web.telegram.org zws5.web.telegram.org" ;; spotify) printf '%s\n' \ "#Spotify" \ "45.155.204.190 api.spotify.com login5.spotify.com encore.scdn.co gew1-spclient.spotify.com spclient.wg.spotify.com" \ "45.155.204.190 api-partner.spotify.com aet.spotify.com www.spotify.com accounts.spotify.com open.spotify.com" \ "45.155.204.190 accounts.scdn.co gew1-dealer.spotify.com open-exp.spotifycdn.com www-growth.scdn.co" ;; spotifyext) printf '%s\n' \ "#SpotifyEXT" \ "45.155.204.190 spotify.com www.spotify.com accounts.spotify.com login.spotify.com login5.spotify.com login.app.spotify.com auth.spotify.com account.spotify.com api.spotify.com api-partner.spotify.com spclient.wg.spotify.com gew1-spclient.spotify.com" \ "45.155.204.190 guc3-spclient.spotify.com gae2-spclient.spotify.com gue1-spclient.spotify.com gnl-spclient.spotify.com spclient.spotify.com ap-gew1.spotify.com ap-gue1.spotify.com ap-gae2.spotify.com ap-gew4.spotify.com ap-sto3.spotify.com" \ "45.155.204.190 ap-guc3.spotify.com ap.spotify.com apresolve.spotify.com aet.spotify.com gew1-dealer.g2.spotify.com guc3-dealer.g2.spotify.com gue1-dealer.g2.spotify.com dealer.spotify.com dealer-wg.spotify.com edge-web.dual-gslb.spotify.com" \ "45.155.204.190 client.spotify.com web-partner.spotify.com connect.spotify.com gce.spotify.com clienttoken.spotify.com exp.wg.spotify.com pixel.spotify.com pixel-static.spotify.com image-upload.spotify.com content.spotify.com analytics.spotify.com" \ "45.155.204.190 crashdump.spotify.com log.spotify.com logger.spotify.com metrics.spotify.com desktop.spotify.com audio-fa-tls13.spotifycdn.com heads-fa-tls13.spotifycdn.com heads4-fa-tls13.spotifycdn.com image-cdn-fa.spotifycdn.com" \ "45.155.204.190 concerts.spotifycdn.com mrkt.spotifycdn.com pickasso.spotifycdn.com podz-content.spotifycdn.com seed-mix-image.spotifycdn.com thisis-images.spotifycdn.com wap.spotifycdn.com web-sdk-assets.spotifycdn.com spotifycdn.com spotifycdn.net" \ "35.186.224.24 open.spotify.com" \ "162.159.141.124 audio4-fa-tls13.spotifycdn.com audio-cf.spotifycdn.com open-exp.spotifycdn.com" \ "23.36.163.34 audio-ak-spotify-com.akamaized.net" \ "2.16.168.44 audio4-ak-spotify-com.akamaized.net" \ "199.232.210.248 scdn.co i.scdn.co line-up.scdn.co mosaic.scdn.co daily-mix.scdn.co lineup-images.scdn.co encore.scdn.co image-cdn-fa.scdn.co accounts.scdn.co www.scdn.co www-growth.scdn.co av.scdn.co seafoam.scdn.co" \ "23.48.23.145 heads-ak-spotify-com.akamaized.net" \ "45.155.204.190 xpui.app.spotify.com" ;; scell) printf '%s\n' \ "#Supercell" \ "103.27.157.38 accounts.supercell.com cdn.id.supercell.com clashofclans.inbox.supercell.com game-assets.brawlstarsgame.com" \ "103.27.157.38 game-assets.clashofclans.com game-assets.clashroyaleapp.com security.id.supercell.com store.supercell.com" \ "31.25.239.132 accounts.supercell.com cdn.id.supercell.com clashofclans.inbox.supercell.com game-assets.brawlstarsgame.com" \ "31.25.239.132 game-assets.clashofclans.com game-assets.clashroyaleapp.com game.boombeachgame.com game.mocogame.com security.id.supercell.com store.supercell.com" \ "185.246.223.127 game.brawlstarsgame.com" \ "62.133.62.97 game.clashroyaleapp.com" \ "193.23.209.189 gamea.clashofclans.com" \ "108.61.167.26 game.squadbustersgame.com" ;; githubraw) printf '%s\n' \ "#githubusercontent.com" \ "146.75.22.132 objects.githubusercontent.com release-assets.githubusercontent.com raw.githubusercontent.com private-user-images.githubusercontent.com gist.githubusercontent.com" \ "146.75.22.132 camo.githubusercontent.com avatars.githubusercontent.com avatars0.githubusercontent.com avatars1.githubusercontent.com avatars2.githubusercontent.com avatars3.githubusercontent.com avatars4.githubusercontent.com avatars5.githubusercontent.com" ;; github) printf '%s\n' \ "#github.com" \ "140.82.114.3 github.com" \ "185.199.110.154 github.githubassets.com" \ "185.199.110.133 camo.githubassets.com" ;; ubisoft) printf '%s\n' \ "#Ubisoft" \ "52.6.7.14 ubi.com" \ "172.67.139.108 r6s.com" \ "54.155.2.87 rainbow6.com" \ "52.222.149.31 ubisoft.com" \ "54.76.54.196 uplay.ubisoft.com" \ "2.23.89.92 static3.cdn.ubi.com" \ "18.209.141.203 connect.ubisoft.com" \ "2.23.89.244 ubiservices.cdn.ubi.com" \ "99.83.188.134 public-ubiservices.ubi.com" \ "3.33.249.140 public-ws-ubiservices.ubi.com" ;; tapeop) printf '%s\n' \ "#tapeop.dev" \ "216.24.57.251 www.tapeop.dev tapeop.dev" \ "216.24.57.3 www.tapeop.dev tapeop.dev" ;; roblox) printf '%s\n' \ "#tr.rbxcdn.com" \ "108.156.22.8 tr.rbxcdn.com" \ "108.157.32.114 tr.rbxcdn.com" \ "18.65.147.108 tr.rbxcdn.com" \ "18.65.147.112 tr.rbxcdn.com" \ "13.224.181.18 tr.rbxcdn.com" \ "13.224.181.74 tr.rbxcdn.com" \ "54.230.253.22 tr.rbxcdn.com" \ "54.230.253.81 tr.rbxcdn.com" \ "54.230.253.48 tr.rbxcdn.com" \ "54.230.253.59 tr.rbxcdn.com" \ "143.204.214.34 tr.rbxcdn.com" \ "143.204.214.67 tr.rbxcdn.com" \ "143.204.214.92 tr.rbxcdn.com" \ "99.84.181.25 tr.rbxcdn.com" \ "99.84.181.63 tr.rbxcdn.com" \ "65.8.158.45 tr.rbxcdn.com" \ "65.8.158.112 tr.rbxcdn.com" ;; updatesdiscord) printf '%s\n' \ "#updates.discord.com" \ "162.159.138.232 updates.discord.com" \ "162.159.137.232 updates.discord.com" \ "162.159.128.233 updates.discord.com" \ "162.159.135.232 updates.discord.com" ;; *) return 1 ;; esac } _hosts_block_status() { local block="$1" content line content="$(_hosts_block "$block")" || return 1 while IFS= read -r line; do [ -z "$line" ] && continue grep -Fxq "$line" "$HOSTS_FILE" || { echo "false"; return; } done <<-EOF $content EOF echo "true" } hosts_status() { local blocks="nalog rutor ntc instagram librusec ai twitch telegram spotify spotifyext scell githubraw github ubisoft tapeop roblox updatesdiscord" b first=1 local geohide="" if grep -q '^### geohide.ru: hosts file' "$HOSTS_FILE" 2>/dev/null; then if grep -q '^# Регион серверов: US$' "$HOSTS_FILE" 2>/dev/null; then geohide="us" elif grep -q '^# Регион серверов: EU$' "$HOSTS_FILE" 2>/dev/null; then geohide="eu" elif grep -q '^# Регион серверов: RU$' "$HOSTS_FILE" 2>/dev/null; then geohide="ru" else geohide="unknown"; fi fi printf '{"geohide":"%s","items":[' "$(esc "$geohide")" for b in $blocks; do [ "$first" -eq 1 ] || printf ',' first=0 printf '{"id":"%s","enabled":%s}' "$b" "$(_hosts_block_status "$b")" done printf ']}\n' } hosts_toggle() { local block="$1" content line enabled content="$(_hosts_block "$block")" || { echo '{"error":"неизвестный блок"}'; return 1; } enabled="$(_hosts_block_status "$block")" if [ "$enabled" = "true" ]; then while IFS= read -r line; do [ -z "$line" ] && continue; sed -i "\\|^$line\$|d" "$HOSTS_FILE"; done <<-EOF $content EOF else while IFS= read -r line; do [ -z "$line" ] && continue; grep -Fxq "$line" "$HOSTS_FILE" || echo "$line" >> "$HOSTS_FILE"; done <<-EOF $content EOF fi /etc/init.d/dnsmasq restart >/dev/null 2>&1 printf '{"ok":true,"block":"%s","enabled":%s}\n' "$(esc "$block")" "$([ "$enabled" = "true" ] && echo false || echo true)" } hosts_replace_geohide() { local region="$1" url tmp case "$region" in ru) url="${GH_RAW}/Internet-Helper/GeoHideDNS/refs/heads/main/hosts/hosts" ;; eu) url="${GH_RAW}/Internet-Helper/GeoHideDNS/refs/heads/main/hosts/eu/hosts" ;; us) url="${GH_RAW}/Internet-Helper/GeoHideDNS/refs/heads/main/hosts/us/hosts" ;; *) echo '{"error":"неизвестный регион"}'; return 1 ;; esac tmp="$JOBS_DIR/geohide_hosts.tmp" wget -q -U "Mozilla/5.0" -O "$tmp" "$url" >/dev/null 2>&1 if [ ! -s "$tmp" ]; then rm -f "$tmp" echo '{"error":"не удалось скачать GeoHide hosts"}' return 1 fi mv "$tmp" "$HOSTS_FILE" /etc/init.d/dnsmasq restart >/dev/null 2>&1 printf '{"ok":true,"region":"%s"}\n' "$region" } hosts_reset() { printf '%s\n' "127.0.0.1 localhost" "" "::1 localhost ip6-localhost ip6-loopback" "ff02::1 ip6-allnodes" "ff02::2 ip6-allrouters" > "$HOSTS_FILE" /etc/init.d/dnsmasq restart >/dev/null 2>&1 printf '{"ok":true}\n' } # ---------- Gv game strategy (ported from fix_GAME / Gv_Xtreme / GV_FAKE) -- _remove_ports_if_present() { local option="$1" ports="$2" p for p in $(echo "$ports" | tr ',' ' '); do sed -i "\\#option $option '#s#,$p##g" "$CONF" done sed -i "\\#option $option '#s#,,#,#g; s#,\$##" "$CONF" } _add_ports_if_missing() { local option="$1" ports="$2" p for p in $(echo "$ports" | tr ',' ' '); do grep -q "option $option '.*\b$p\b" "$CONF" || sed -i "\\#option $option '#s#'\$#,$p'#" "$CONF" done } strategy_TCP_common() { printf '%s\n' "--new" "--filter-tcp=$PORTS_TCP" "--dpi-desync-any-protocol=1" "--dpi-desync-cutoff=n5" \ "--dpi-desync=multisplit" "--dpi-desync-split-seqovl=582" "--dpi-desync-split-pos=1" \ "--dpi-desync-split-seqovl-pattern=/opt/zapret/files/fake/stun.bin" } strategy_Gv1() { printf '%s\n' "#Gv1" "--new" "--filter-udp=$PORTS_UDP" "--dpi-desync=fake" "--dpi-desync-cutoff=d2" \ "--dpi-desync-any-protocol=1" "--dpi-desync-fake-unknown-udp=/opt/zapret/files/fake/stun.bin" } strategy_Gv() { local n="$1" printf '%s\n' "#Gv$n" "--new" "--filter-udp=$PORTS_UDP" "--dpi-desync=fake" "--dpi-desync-repeats=10" \ "--dpi-desync-any-protocol=1" "--dpi-desync-fake-unknown-udp=/opt/zapret/files/fake/stun.bin" "--dpi-desync-cutoff=n$n" } game_status() { local current="" xtreme="false" fake="" if [ -f "$CONF" ]; then local i for i in 1 2 3 4; do grep -q "^#Gv$i\$" "$CONF" && current="Gv$i"; done grep -q "^#Gv[0-9]\+Xtreme\$" "$CONF" && xtreme="true" fake=$(grep -m1 -- '--dpi-desync-fake-unknown-udp=' "$CONF" | sed 's|.*fake/||') fi printf '{"current":"%s","xtreme":%s,"fake":"%s"}\n' "$(esc "$current")" "$xtreme" "$(esc "$fake")" } game_set() { local choice="$1" current="" i echo "$choice" | grep -qE '^[1-4]$' || { echo '{"error":"некорректный номер Gv"}'; return 1; } [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } for i in 1 2 3 4; do grep -q "^#Gv$i\$" "$CONF" && current="Gv$i"; done local last_quote gv_line last_quote=$(grep -n "^'\$" "$CONF" | tail -n1 | cut -d: -f1) if grep -q "^#Gv" "$CONF"; then gv_line=$(grep -n "^#Gv" "$CONF" | tail -n1 | cut -d: -f1) sed -i "${gv_line},${last_quote}d" "$CONF" elif [ -n "$last_quote" ]; then sed -i "${last_quote},\$d" "$CONF" fi if [ "$current" = "Gv$choice" ]; then # повторный выбор той же стратегии = снять игровую стратегию _remove_ports_if_present NFQWS_PORTS_UDP "$PORTS_UDP" _remove_ports_if_present NFQWS_PORTS_TCP "$PORTS_TCP" echo "'" >> "$CONF" zapret_restart printf '{"ok":true,"game":"none"}\n' return fi local strat if [ "$choice" = "1" ]; then strat="$(strategy_Gv1; strategy_TCP_common)"; else strat="$(strategy_Gv "$choice"; strategy_TCP_common)"; fi echo "$strat" | sed '/^$/d' >> "$CONF" echo "'" >> "$CONF" _add_ports_if_missing NFQWS_PORTS_UDP "$PORTS_UDP" _add_ports_if_missing NFQWS_PORTS_TCP "$PORTS_TCP" zapret_restart printf '{"ok":true,"game":"Gv%s"}\n' "$choice" } game_set_fake() { local file="$1" case "$file" in stun.bin|stun2.bin|quic_initial_4pda_to.bin|quic_initial_tencent_com.bin|tls_clienthello_sochi_park.bin|quic_initial_www_google_com.bin|quic_initial_steamcommunity_com.bin|quic_initial_5ka_ru.bin|quic_initial_rutube_ru.bin) ;; *) echo '{"error":"неизвестный fake-файл"}'; return 1 ;; esac [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } grep -q -- '--dpi-desync-fake-unknown-udp=' "$CONF" || { echo '{"error":"игровая стратегия не установлена"}'; return 1; } awk -v new="$file" 'BEGIN{done=0} !done && /--dpi-desync-fake-unknown-udp=/{sub(/\/opt\/zapret\/files\/fake\/[^ ]+/, "/opt/zapret/files/fake/" new); done=1} {print}' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" zapret_restart printf '{"ok":true,"fake":"%s"}\n' "$(esc "$file")" } game_toggle_xtreme() { [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } local xfile="/opt/zapret/tmp/GvXtreme" local xports="80,88,444-65535" local xnfqws="80,88,443-65535" mkdir -p "$(dirname "$xfile")" if grep -q "^#Gv[0-9]\+Xtreme\$" "$CONF"; then [ -f "$xfile" ] || { echo '{"error":"файл восстановления отсутствует"}'; return 1; } local old_gv old_udp old_tcp old_tcp_opt old_udp_opt old_gv=$(sed -n '1p' "$xfile") old_udp=$(sed -n '2p' "$xfile") old_tcp=$(sed -n '3p' "$xfile") old_tcp_opt=$(sed -n '4p' "$xfile") old_udp_opt=$(sed -n '5p' "$xfile") awk -v gv="$old_gv" -v udp="$old_udp" -v tcp="$old_tcp" ' /^#Gv[0-9]+Xtreme$/ { print gv; restore = 1; next } restore && /^--filter-udp=/ { print udp; next } restore && /^--filter-tcp=/ { print tcp; restore = 0; next } { print } ' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" [ -n "$old_tcp_opt" ] && sed -i "s|^[[:space:]]*option NFQWS_PORTS_TCP .*|$old_tcp_opt|" "$CONF" [ -n "$old_udp_opt" ] && sed -i "s|^[[:space:]]*option NFQWS_PORTS_UDP .*|$old_udp_opt|" "$CONF" rm -f "$xfile" zapret_restart printf '{"ok":true,"xtreme":false}\n' return fi grep -q "^#Gv[0-9]\+\$" "$CONF" || { echo '{"error":"игровая стратегия не установлена"}'; return 1; } awk ' /^#Gv[0-9]+$/ { gv = $0; found = 1; next } found && /^--filter-udp=/ { udp = $0 } found && /^--filter-tcp=/ { tcp = $0 } /^[ \t]*option NFQWS_PORTS_TCP / { tcp_option = $0 } /^[ \t]*option NFQWS_PORTS_UDP / { udp_option = $0 } END { print gv; print udp; print tcp; print tcp_option; print udp_option } ' "$CONF" > "$xfile" sed -i -e "s|^[[:space:]]*option NFQWS_PORTS_TCP .*| option NFQWS_PORTS_TCP '$xnfqws'|" \ -e "s|^[[:space:]]*option NFQWS_PORTS_UDP .*| option NFQWS_PORTS_UDP '$xnfqws'|" "$CONF" sed -i "s/^#\(Gv[0-9]\+\)\$/#\1Xtreme/" "$CONF" awk -v ports="$xports" ' /^#Gv[0-9]+Xtreme$/ { gv = 1; print; next } gv && /^--filter-udp=/ { print "--filter-udp=" ports; next } gv && /^--filter-tcp=/ { print "--filter-tcp=" ports; gv = 0; next } { print } ' "$CONF" > "$CONF.tmp" && mv "$CONF.tmp" "$CONF" zapret_restart printf '{"ok":true,"xtreme":true}\n' } # ---------- System (ported from Sys_Info / sys_menu) --------------------- _quic_blocked() { uci show firewall 2>/dev/null | grep -q "name='Block_UDP_80'" \ && uci show firewall 2>/dev/null | grep -q "name='Block_UDP_443'" \ && echo true || echo false } _ipv6_enabled_in_zapret() { [ -f "$CONF" ] && grep -q "option DISABLE_IPV6 '0'" "$CONF" && echo true || echo false } _flow_offloading_fix_applied() { grep -q 'ct original packets ge 30 flow offload @ft;' /usr/share/firewall4/templates/ruleset.uc 2>/dev/null \ && echo true || echo false } _expert_mode() { [ -f "$EXPERT_MODE_FILE" ] && echo true || echo false; } system_status() { printf '{"quic_blocked":%s,"ipv6_enabled":%s,"flow_offloading_fix":%s,"expert_mode":%s,"pkg":"%s"}\n' \ "$(_quic_blocked)" "$(_ipv6_enabled_in_zapret)" "$(_flow_offloading_fix_applied)" "$(_expert_mode)" "$PKG" } system_check_connectivity() { local t4 t6 t4=$(ping -4 -c1 -W2 google.com 2>/dev/null | grep 'time=' | sed -E 's/.*time=([0-9.]+).*/\1/') t6=$(ping -6 -c1 -W2 google.com 2>/dev/null | grep 'time=' | sed -E 's/.*time=([0-9.]+).*/\1/') printf '{"ipv4_ok":%s,"ipv4_ms":"%s","ipv6_ok":%s,"ipv6_ms":"%s"}\n' \ "$([ -n "$t4" ] && echo true || echo false)" "$(esc "$t4")" \ "$([ -n "$t6" ] && echo true || echo false)" "$(esc "$t6")" } system_toggle_quic() { if [ "$(_quic_blocked)" = "true" ]; then local rule idx for rule in Block_UDP_80 Block_UDP_443; do while true; do idx=$(uci show firewall | grep "name='$rule'" | cut -d. -f2 | cut -d= -f1 | head -n1) [ -z "$idx" ] && break uci delete firewall.$idx >/dev/null 2>&1 done done uci commit firewall >/dev/null 2>&1 /etc/init.d/firewall restart >/dev/null 2>&1 printf '{"ok":true,"quic_blocked":false}\n' return fi uci add firewall rule >/dev/null 2>&1 uci set firewall.@rule[-1].name='Block_UDP_80' >/dev/null 2>&1 uci add_list firewall.@rule[-1].proto='udp' >/dev/null 2>&1 uci set firewall.@rule[-1].src='lan' >/dev/null 2>&1 uci set firewall.@rule[-1].dest='wan' >/dev/null 2>&1 uci set firewall.@rule[-1].dest_port='80' >/dev/null 2>&1 uci set firewall.@rule[-1].target='REJECT' >/dev/null 2>&1 uci add firewall rule >/dev/null 2>&1 uci set firewall.@rule[-1].name='Block_UDP_443' >/dev/null 2>&1 uci add_list firewall.@rule[-1].proto='udp' >/dev/null 2>&1 uci set firewall.@rule[-1].src='lan' >/dev/null 2>&1 uci set firewall.@rule[-1].dest='wan' >/dev/null 2>&1 uci set firewall.@rule[-1].dest_port='443' >/dev/null 2>&1 uci set firewall.@rule[-1].target='REJECT' >/dev/null 2>&1 uci commit firewall >/dev/null 2>&1 /etc/init.d/firewall restart >/dev/null 2>&1 printf '{"ok":true,"quic_blocked":true}\n' } system_toggle_ipv6() { [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; } if grep -q "option DISABLE_IPV6 '0'" "$CONF"; then sed -i "s/option DISABLE_IPV6 '0'/option DISABLE_IPV6 '1'/" "$CONF" zapret_restart printf '{"ok":true,"ipv6_enabled":false}\n' return fi local t6 t6=$(ping -6 -c1 -W2 google.com 2>/dev/null | grep 'time=') if [ -z "$t6" ]; then echo '{"error":"IPv6 недоступен на роутере — включение не рекомендуется"}' return 1 fi sed -i "s/option DISABLE_IPV6 '1'/option DISABLE_IPV6 '0'/" "$CONF" zapret_restart printf '{"ok":true,"ipv6_enabled":true}\n' } system_toggle_flow_offloading_fix() { local template="/usr/share/firewall4/templates/ruleset.uc" [ -f "$template" ] || { echo '{"error":"шаблон firewall4 не найден"}'; return 1; } if grep -q 'ct original packets ge 30 flow offload @ft;' "$template"; then sed -i 's/meta l4proto { tcp, udp } ct original packets ge 30 flow offload @ft;/meta l4proto { tcp, udp } flow offload @ft;/' "$template" fw4 restart >/dev/null 2>&1 printf '{"ok":true,"flow_offloading_fix":false}\n' return fi sed -i 's/meta l4proto { tcp, udp } flow offload @ft;/meta l4proto { tcp, udp } ct original packets ge 30 flow offload @ft;/' "$template" fw4 restart >/dev/null 2>&1 printf '{"ok":true,"flow_offloading_fix":true}\n' } system_toggle_expert_mode() { if [ -f "$EXPERT_MODE_FILE" ]; then rm -f "$EXPERT_MODE_FILE" printf '{"ok":true,"expert_mode":false}\n' else echo 1 > "$EXPERT_MODE_FILE" printf '{"ok":true,"expert_mode":true}\n' fi } system_uninstall_panel() { # Панель удаляет саму себя — включая собственный rpcd-скрипт и этот файл. # Делать rm -rf прямо здесь небезопасно (шелл ещё читает этот же файл), # поэтому копируем действие в отдельный скрипт в /tmp (вне удаляемых # путей), запускаем его отсоединённым фоновым процессом с небольшой # задержкой (чтобы успеть отдать ответ браузеру) и сразу возвращаем ok — # дальше приложение уже недоступно, поэтому опрашивать тут нечего. local script="/tmp/zm_uninstall_panel.sh" cat > "$script" << 'ZM_UNINSTALL_EOF' #!/bin/sh sleep 1 rm -rf /usr/lib/zapret-manager /usr/libexec/rpcd/zapret-manager \ /usr/share/luci/menu.d/luci-app-zapret-manager.json \ /usr/share/rpcd/acl.d/luci-app-zapret-manager.json \ /www/luci-static/resources/view/zapret-manager \ /www/luci-static/resources/zapret-manager \ /tmp/zapret-manager /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null /etc/init.d/rpcd restart >/dev/null 2>&1 /etc/init.d/uhttpd restart >/dev/null 2>&1 rm -f "$0" ZM_UNINSTALL_EOF chmod 0755 "$script" ( sh "$script" >/dev/null 2>&1 & ) printf '{"ok":true}\n' } # ---------- OpenWrt mirror selection (ported from menu_MIR / set_mirror) -- _confz() { if [ "$PKG" = "apk" ]; then echo "/etc/apk/repositories.d/distfeeds.list" else echo "/etc/opkg/distfeeds.conf"; fi } _mirror_name() { local f url f="$(_confz)" [ -f "$f" ] || { echo "файл не найден"; return; } url=$(head -n1 "$f") case "$url" in *mirror-03.infra.openwrt.org*) echo "Infra OpenWrt" ;; *c3sl.ufpr.br*) echo "Brazil" ;; *ustc.edu.cn*) echo "China" ;; *tetaneutral.net*) echo "France" ;; *garr.it*) echo "Italy" ;; *marwan.ma*) echo "Morocco" ;; *pixeldeck.net*) echo "USA" ;; *rwth-aachen.de*) echo "Germany" ;; *downloads.openwrt.org*) echo "default / OpenWrt" ;; *) echo "неизвестное" ;; esac } mirror_status() { printf '{"current":"%s"}\n' "$(esc "$(_mirror_name)")" } do_mirror_set() { local host="$1" f echo "==> Проверяем доступность $host" if ! wget -q --spider --timeout=5 "https://$host/releases/" >/dev/null 2>&1; then echo "ОШИБКА: зеркало недоступно" return 1 fi f="$(_confz)" sed -i "s|https://.*/releases/|https://$host/releases/|g" "$f" echo "==> Зеркало доступно, обновляем список пакетов" if ! $UPDATE >/dev/null 2>&1; then echo "ОШИБКА обновления пакетов — зеркало сброшено на default" sed -i "s|https://.*/releases/|https://downloads.openwrt.org/releases/|g" "$f" return 1 fi echo "==> Готово, зеркало переключено и работает" } mirror_set() { local id="$1" host case "$id" in infra) host="mirror-03.infra.openwrt.org" ;; brazil) host="openwrt.c3sl.ufpr.br" ;; china) host="mirrors.ustc.edu.cn/openwrt" ;; france) host="openwrt.tetaneutral.net" ;; italy) host="openwrt.mirror.garr.it/mirrors/openwrt" ;; morocco) host="mirror.marwan.ma/openwrt" ;; usa) host="openwrt.pixeldeck.net" ;; germany_rwth) host="ftp.halifax.rwth-aachen.de/openwrt" ;; default) host="downloads.openwrt.org" ;; *) echo '{"error":"неизвестное зеркало"}'; return 1 ;; esac job_start mirror_set do_mirror_set "$host" } # ---------- IP exclusions (ported from Exclusions_menu) ------------------ _excl_dir() { echo "/opt/zapret/init.d/openwrt/custom.d"; } _excl_file() { echo "$(_excl_dir)/20-script.sh"; } _excl_current() { local f; f="$(_excl_file)" [ -f "$f" ] || return 0 grep -oE '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' "$f" | sort -u } _excl_write() { local ips="$1" f f="$(_excl_file)" mkdir -p "$(_excl_dir)" if [ -n "$ips" ]; then local formatted formatted=$(printf '%s\n' "$ips" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g') { echo "EXCEPT_SRC='{ $formatted }'" echo "nft insert rule inet zapret postrouting_hook index 0 \\" echo " ip saddr \$EXCEPT_SRC meta mark set meta mark \\| 0x40000000" } > "$f" else : > "$f" fi } exclusions_status() { [ -f /etc/init.d/zapret ] || { echo '{"error":"Zapret не установлен"}'; return 1; } local current devjson="" first=1 ip name ts mac rest current=$(_excl_current) if [ -f /tmp/dhcp.leases ]; then while read -r ts mac ip name rest; do [ -z "$ip" ] && continue echo "$ip" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' || continue [ -z "$name" ] && name="*" [ "$name" = "*" ] && name="Неизвестное устройство" [ "$first" -eq 1 ] || devjson="$devjson," first=0 devjson="$devjson{\"ip\":\"$ip\",\"name\":\"$(esc "$name")\",\"excluded\":$(printf '%s\n' "$current" | grep -qx "$ip" && echo true || echo false)}" done < /tmp/dhcp.leases fi local ip2 for ip2 in $current; do [ -z "$ip2" ] && continue printf '%s' "$devjson" | grep -q "\"ip\":\"$ip2\"" && continue [ "$first" -eq 1 ] || devjson="$devjson," first=0 devjson="$devjson{\"ip\":\"$ip2\",\"name\":\"Устройство offline\",\"excluded\":true}" done printf '{"devices":[%s]}\n' "$devjson" } exclusions_toggle() { local ip="$1" current new_list excluded="false" echo "$ip" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' || { echo '{"error":"некорректный IP-адрес"}'; return 1; } [ -f /etc/init.d/zapret ] || { echo '{"error":"Zapret не установлен"}'; return 1; } current=$(_excl_current) if printf '%s\n' "$current" | grep -qx "$ip"; then new_list=$(printf '%s\n' "$current" | grep -vx "$ip") else new_list=$(printf '%s\n%s\n' "$current" "$ip" | grep -v '^$' | sort -u) excluded="true" fi _excl_write "$new_list" zapret_restart printf '{"ok":true,"ip":"%s","excluded":%s}\n' "$(esc "$ip")" "$excluded" } exclusions_clear() { [ -f /etc/init.d/zapret ] || { echo '{"error":"Zapret не установлен"}'; return 1; } _excl_write "" zapret_restart printf '{"ok":true}\n' } # ---------- TG WS Proxy: MTProto / SOCKS5 / Rust (ported from menu_TG) --- TG_MTPROTO_VER="0.10" TG_GO_VER="1.4.1" TG_RS_VER="2.3.3" TG_BIN_GO="/usr/bin/tg-ws-proxy-go" TG_INIT_GO="/etc/init.d/tg-ws-proxy-go" TG_BIN_RS="/usr/bin/tg-ws-proxy-rs" TG_INIT_RS="/etc/init.d/tg-ws-proxy-rs" TG_SECRET_RS_FILE="/etc/tg-ws-proxy-rs.secret" TG_SECRET_MT_FILE="/etc/tg-ws-proxy/secret.conf" _tg_arch_rs() { case "$TG_ARCH" in aarch64*) echo "tg-ws-proxy-aarch64-unknown-linux-musl" ;; x86_64) echo "tg-ws-proxy-x86_64-unknown-linux-musl" ;; arm*) echo "tg-ws-proxy-armv7-unknown-linux-musleabihf" ;; mipsel*) echo "tg-ws-proxy-mipsel-unknown-linux-musl" ;; mips*) echo "tg-ws-proxy-mips-unknown-linux-musl" ;; *) return 1 ;; esac } _tg_arch_go() { case "$TG_ARCH" in aarch64*) echo "tg-ws-proxy-openwrt-aarch64" ;; arm*) echo "tg-ws-proxy-openwrt-armv7" ;; mipsel*) echo "tg-ws-proxy-openwrt-mipsel_24kc" ;; mips*) echo "tg-ws-proxy-openwrt-mips_24kc" ;; x86_64) echo "tg-ws-proxy-openwrt-x86_64" ;; *) return 1 ;; esac } tg_status() { local mt="not_installed" mt_running="false" local go="not_installed" go_running="false" local rs="not_installed" rs_running="false" local secret_mt="" secret_rs="" lan_ip if [ -f /etc/init.d/tg-ws-proxy ]; then mt="installed"; pidof tg-ws-proxy >/dev/null 2>&1 && mt_running="true"; fi if [ -f "$TG_INIT_GO" ]; then go="installed"; pidof tg-ws-proxy-go >/dev/null 2>&1 && go_running="true"; fi if [ -f "$TG_INIT_RS" ]; then rs="installed"; pidof tg-ws-proxy-rs >/dev/null 2>&1 && rs_running="true"; fi [ -f "$TG_SECRET_MT_FILE" ] && secret_mt=$(grep '^SECRET=' "$TG_SECRET_MT_FILE" | cut -d= -f2) [ -f "$TG_SECRET_RS_FILE" ] && secret_rs=$(cat "$TG_SECRET_RS_FILE") lan_ip=$(uci -q get network.lan.ipaddr 2>/dev/null | cut -d/ -f1) printf '{"mtproto":"%s","mtproto_running":%s,"socks5":"%s","socks5_running":%s,"rust":"%s","rust_running":%s,"lan_ip":"%s","secret_mtproto":"%s","secret_rust":"%s"}\n' \ "$mt" "$mt_running" "$go" "$go_running" "$rs" "$rs_running" "$(esc "$lan_ip")" "$(esc "$secret_mt")" "$(esc "$secret_rs")" } do_tg_install_mtproto() { _ensure_deps echo "==> Устанавливаем TG WS Proxy MTProto" local go_suf raz url tmp arch_full if [ "$PKG" = "apk" ]; then go_suf="r1"; raz="apk"; else go_suf="1"; raz="ipk"; fi arch_full="$(awk -F\' '/DISTRIB_ARCH/ {print $2}' /etc/openwrt_release)" url="${GH_MAIN}/spatiumstas/tg-ws-proxy-go/releases/download/${TG_MTPROTO_VER}/tg-ws-proxy_${TG_MTPROTO_VER}-${go_suf}_openwrt_${arch_full}.${raz}" tmp="/tmp/tg-ws-proxy.$raz" rm -f /etc/tg-ws-proxy.conf /etc/tg-ws-proxy.conf-opkg $UPDATE >/dev/null 2>&1 echo "==> Скачиваем $(basename "$url")" wget -q -O "$tmp" "$url" || { echo "ОШИБКА скачивания $url"; return 1; } $INSTALL "$tmp" >/dev/null 2>&1 || { echo "ОШИБКА установки"; rm -f "$tmp"; return 1; } rm -f "$tmp" mkdir -p "$(dirname "$TG_SECRET_MT_FILE")" if ! grep -q '^SECRET=.' "$TG_SECRET_MT_FILE" 2>/dev/null; then echo "SECRET=$(head -c16 /dev/urandom | hexdump -e '16/1 "%02x"')" > "$TG_SECRET_MT_FILE" fi rm -f /etc/tg-ws-proxy.conf /etc/tg-ws-proxy.conf-opkg /etc/init.d/tg-ws-proxy enable >/dev/null 2>&1 /etc/init.d/tg-ws-proxy restart >/dev/null 2>&1 echo "==> Готово" } do_tg_remove_mtproto() { echo "==> Удаляем TG WS Proxy MTProto" /etc/init.d/tg-ws-proxy stop >/dev/null 2>&1 /etc/init.d/tg-ws-proxy disable >/dev/null 2>&1 $DELETE tg-ws-proxy >/dev/null 2>&1 rm -rf /etc/tg-ws-proxy /etc/tg-ws-proxy.conf /etc/tg-ws-proxy.conf-opkg echo "==> Готово" } do_tg_install_socks5() { _ensure_deps echo "==> Устанавливаем TG WS Proxy SOCKS5" local file url file="$(_tg_arch_go)" || { echo "ОШИБКА: архитектура не поддерживается ($TG_ARCH)"; return 1; } url="${GH_MAIN}/d0mhate/-tg-ws-proxy-Manager-go/releases/download/v${TG_GO_VER}/${file}" echo "==> Скачиваем $file" curl -fL --max-time 30 -o "$TG_BIN_GO" "$url" || { echo "ОШИБКА скачивания"; rm -f "$TG_BIN_GO"; return 1; } chmod +x "$TG_BIN_GO" if [ ! -f "$TG_INIT_GO" ]; then printf '#!/bin/sh /etc/rc.common\nSTART=99\nUSE_PROCD=1\n\nstart_service() {\n\tprocd_open_instance\n\tprocd_set_param command /usr/bin/tg-ws-proxy-go --host 0.0.0.0 --port 2080 --cf-proxy --cf-proxy-first --cf-balance\n\tprocd_set_param respawn\n\tprocd_close_instance\n}\n' > "$TG_INIT_GO" chmod +x "$TG_INIT_GO" "$TG_INIT_GO" enable >/dev/null 2>&1 fi "$TG_INIT_GO" restart >/dev/null 2>&1 echo "==> Готово" } do_tg_remove_socks5() { echo "==> Удаляем TG WS Proxy SOCKS5" [ -x "$TG_INIT_GO" ] && { "$TG_INIT_GO" stop >/dev/null 2>&1; "$TG_INIT_GO" disable >/dev/null 2>&1; } rm -f "$TG_BIN_GO" "$TG_INIT_GO" echo "==> Готово" } do_tg_install_rust() { _ensure_deps echo "==> Устанавливаем TG WS Proxy Rust" local file url tmp_archive tmp_dir secret file="$(_tg_arch_rs)" || { echo "ОШИБКА: архитектура не поддерживается ($TG_ARCH)"; return 1; } url="${GH_MAIN}/valnesfjord/tg-ws-proxy-rs/releases/download/v${TG_RS_VER}/${file}.tar.gz" tmp_archive="/tmp/tg-ws-proxy-rs.tar.gz"; tmp_dir="/tmp/tg-ws-proxy-rs" echo "==> Скачиваем $file" curl -fL --max-time 30 -o "$tmp_archive" "$url" || { echo "ОШИБКА скачивания"; return 1; } rm -rf "$tmp_dir"; mkdir -p "$tmp_dir" tar -xzf "$tmp_archive" -C "$tmp_dir" || { echo "ОШИБКА распаковки"; rm -f "$tmp_archive"; return 1; } rm -f "$TG_BIN_RS" mv "$tmp_dir"/tg-ws-proxy* "$TG_BIN_RS" || { echo "ОШИБКА установки бинарника"; rm -rf "$tmp_dir" "$tmp_archive"; return 1; } chmod +x "$TG_BIN_RS" rm -rf "$tmp_dir" "$tmp_archive" if [ ! -f "$TG_SECRET_RS_FILE" ]; then head -c16 /dev/urandom | hexdump -e '16/1 "%02x"' > "$TG_SECRET_RS_FILE" fi secret=$(cat "$TG_SECRET_RS_FILE") if [ ! -f "$TG_INIT_RS" ]; then printf '#!/bin/sh /etc/rc.common\nSTART=99\nUSE_PROCD=1\n\nstart_service() {\n\tprocd_open_instance\n\tprocd_set_param command /usr/bin/tg-ws-proxy-rs --host 0.0.0.0 --port 2443 --secret %s --default-domains --cf-balance --cf-priority\n\tprocd_set_param respawn\n\tprocd_close_instance\n}\n' "$secret" > "$TG_INIT_RS" chmod +x "$TG_INIT_RS" "$TG_INIT_RS" enable >/dev/null 2>&1 fi "$TG_INIT_RS" restart >/dev/null 2>&1 echo "==> Готово" } do_tg_remove_rust() { echo "==> Удаляем TG WS Proxy Rust" [ -x "$TG_INIT_RS" ] && { "$TG_INIT_RS" stop >/dev/null 2>&1; "$TG_INIT_RS" disable >/dev/null 2>&1; } rm -f "$TG_BIN_RS" "$TG_INIT_RS" "$TG_SECRET_RS_FILE" echo "==> Готово" } tg_action() { local variant="$1" action="$2" case "$variant:$action" in mtproto:install|mtproto:update) job_start tg_install_mtproto do_tg_install_mtproto ;; mtproto:remove) job_start tg_remove_mtproto do_tg_remove_mtproto ;; socks5:install|socks5:update) job_start tg_install_socks5 do_tg_install_socks5 ;; socks5:remove) job_start tg_remove_socks5 do_tg_remove_socks5 ;; rust:install|rust:update) job_start tg_install_rust do_tg_install_rust ;; rust:remove) job_start tg_remove_rust do_tg_remove_rust ;; *) echo '{"error":"неизвестный вариант/действие"}' ;; esac } tg_restart_all() { [ -x /etc/init.d/tg-ws-proxy ] && /etc/init.d/tg-ws-proxy restart >/dev/null 2>&1 [ -x "$TG_INIT_GO" ] && "$TG_INIT_GO" restart >/dev/null 2>&1 [ -x "$TG_INIT_RS" ] && "$TG_INIT_RS" restart >/dev/null 2>&1 printf '{"ok":true}\n' } # ---------- DNS over HTTPS (subset, verbatim constants from CLI) -------- _doh_file="/etc/config/https-dns-proxy" do_doh_install() { local installed installed=$(doh_status | grep -o '"installed":[a-z]*' | cut -d: -f2) if [ "$installed" = "true" ]; then echo "==> DNS over HTTPS уже установлен" return 0 fi _ensure_deps echo "==> Обновляем список пакетов" $UPDATE >/dev/null 2>&1 echo "==> Устанавливаем https-dns-proxy и luci-app-https-dns-proxy" $INSTALL https-dns-proxy luci-app-https-dns-proxy >/dev/null 2>&1 || { echo "ОШИБКА установки"; return 1; } echo "==> Готово, DNS over HTTPS установлен — выберите провайдера ниже" } do_doh_remove() { echo "==> Удаляем DNS over HTTPS" echo "==> Удаляем пакеты" $DELETE https-dns-proxy luci-app-https-dns-proxy >/dev/null 2>&1 echo "==> Удаляем файлы конфигурации" rm -f /etc/config/https-dns-proxy /etc/init.d/https-dns-proxy /etc/init.d/dnsmasq restart >/dev/null 2>&1 echo "==> Готово, DNS over HTTPS удалён" } doh_remove() { job_start doh_remove do_doh_remove } doh_install() { job_start doh_install do_doh_install } doh_status() { local installed="false" current="" if [ "$PKG" = "apk" ]; then apk info -e https-dns-proxy >/dev/null 2>&1 && installed="true" else opkg list-installed 2>/dev/null | grep -q '^https-dns-proxy ' && installed="true"; fi if [ -f "$_doh_file" ]; then if grep -q "eu.geohide.ru" "$_doh_file"; then current="geohide_eu" elif grep -q "us.geohide.ru" "$_doh_file"; then current="geohide_us" elif grep -q "geohide.ru" "$_doh_file"; then current="geohide_ru" elif grep -q "xbox-dns.ru" "$_doh_file"; then current="xbox" elif grep -q "cloudflare-dns.com" "$_doh_file"; then current="cloudflare" elif grep -q "dns.google" "$_doh_file"; then current="google" elif grep -q "dns.quad9.net" "$_doh_file"; then current="quad9"; fi fi printf '{"installed":%s,"current":"%s"}\n' "$installed" "$(esc "$current")" } doh_set() { local provider="$1" url bootstrap="" case "$provider" in cloudflare) url="https://cloudflare-dns.com/dns-query"; bootstrap="1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001" ;; google) url="https://dns.google/dns-query"; bootstrap="8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844" ;; quad9) url="https://dns.quad9.net/dns-query"; bootstrap="9.9.9.9,149.112.112.112,2620:fe::fe,2620:fe::9" ;; xbox) url="https://xbox-dns.ru/dns-query" ;; geohide_ru) url="https://geohide.ru/dns-query" ;; geohide_eu) url="https://eu.geohide.ru/dns-query" ;; geohide_us) url="https://us.geohide.ru/dns-query" ;; *) echo '{"error":"неизвестный провайдер"}'; return 1 ;; esac local installed; installed=$(doh_status | grep -o '"installed":[a-z]*' | cut -d: -f2) if [ "$installed" != "true" ]; then echo '{"error":"DNS over HTTPS не установлен — сначала нажмите «Установить DNS over HTTPS»"}' return 1 fi { # базовый блок config main — дословно как в оригинальном doh_set, # критично: force_dns_port (без него принудительное перенаправление # DNS-трафика на прокси может не сработать) echo "config main 'config'" echo " option canary_domains_icloud '1'" echo " option canary_domains_mozilla '1'" echo " option dnsmasq_config_update '*'" echo " option force_dns '1'" echo " option notrack_dns '1'" echo " list force_dns_port '53'" echo " list force_dns_port '853'" echo " list force_dns_src_interface 'lan'" echo " option procd_trigger_wan6 '0'" echo " option heartbeat_domain 'heartbeat.mossdef.org'" echo " option heartbeat_sleep_timeout '10'" echo " option heartbeat_wait_timeout '10'" echo " option user 'nobody'" echo " option group 'nogroup'" echo " option listen_addr '127.0.0.1'" echo " option force_ip_family 'auto'" echo "" echo "config https-dns-proxy" echo " option resolver_url '$url'" [ -n "$bootstrap" ] && echo " option bootstrap_dns '$bootstrap'" } > "$_doh_file" /etc/init.d/https-dns-proxy reload >/dev/null 2>&1 /etc/init.d/https-dns-proxy restart >/dev/null 2>&1 /etc/init.d/dnsmasq restart >/dev/null 2>&1 printf '{"ok":true,"provider":"%s"}\n' "$provider" } # ---------- dispatch ------------------------------------------------------- cmd="$1"; shift case "$cmd" in status) status ;; system_info) system_info ;; job_status) job_status "$1" ;; log_tail) log_tail "$1" ;; zapret_action) zapret_action "$1" ;; zapret_latest_version) zapret_latest_version ;; zapret2_action) zapret2_action "$1" ;; strategy_list_v) strategy_list_v ;; strategy_set_v) strategy_set_v "$1" ;; strategy_list_flowseal) strategy_list_flowseal ;; strategy_set_flowseal) strategy_set_flowseal "$1" ;; strategy_list_youtube) strategy_list_youtube ;; strategy_set_youtube) strategy_set_youtube "$1" ;; discord_status) discord_status ;; discord_set_dv) discord_set_dv "$1" ;; discord_set_fake) discord_set_fake "$1" ;; game_status) game_status ;; game_set) game_set "$1" ;; game_set_fake) game_set_fake "$1" ;; game_toggle_xtreme) game_toggle_xtreme ;; system_status) system_status ;; system_check_connectivity) system_check_connectivity ;; system_toggle_quic) system_toggle_quic ;; system_toggle_ipv6) system_toggle_ipv6 ;; system_toggle_flow_offloading_fix) system_toggle_flow_offloading_fix ;; system_toggle_expert_mode) system_toggle_expert_mode ;; system_uninstall_panel) system_uninstall_panel ;; mirror_status) mirror_status ;; mirror_set) mirror_set "$1" ;; exclusions_status) exclusions_status ;; exclusions_toggle) exclusions_toggle "$1" ;; exclusions_clear) exclusions_clear ;; tg_status) tg_status ;; tg_action) tg_action "$1" "$2" ;; tg_restart_all) tg_restart_all ;; hosts_status) hosts_status ;; hosts_toggle) hosts_toggle "$1" ;; hosts_replace_geohide) hosts_replace_geohide "$1" ;; hosts_reset) hosts_reset ;; doh_install) doh_install ;; doh_remove) doh_remove ;; doh_status) doh_status ;; doh_set) doh_set "$1" ;; *) echo '{"error":"неизвестная команда"}'; exit 1 ;; esac ZM_INSTALLER_EOF chmod 0755 '/usr/lib/zapret-manager/backend.sh' mkdir -p /usr/libexec/rpcd cat > '/usr/libexec/rpcd/zapret-manager' << 'ZM_INSTALLER_EOF' #!/bin/sh # rpcd ubus backend for luci-app-zapret-manager # Bridges LuCI JS (ubus calls) to /usr/lib/zapret-manager/backend.sh . /usr/share/libubox/jshn.sh BACKEND="/usr/lib/zapret-manager/backend.sh" list_methods() { json_init json_add_object "status"; json_close_object json_add_object "system_info"; json_close_object json_add_object "job_status"; json_add_string "job" "string"; json_close_object json_add_object "log_tail"; json_add_string "job" "string"; json_close_object json_add_object "zapret_action"; json_add_string "action" "string"; json_close_object json_add_object "zapret_latest_version"; json_close_object json_add_object "zapret2_action"; json_add_string "action" "string"; json_close_object json_add_object "strategy_list_v"; json_close_object json_add_object "strategy_set_v"; json_add_string "version" "string"; json_close_object json_add_object "strategy_list_flowseal"; json_close_object json_add_object "strategy_set_flowseal"; json_add_string "name" "string"; json_close_object json_add_object "strategy_list_youtube"; json_close_object json_add_object "strategy_set_youtube"; json_add_string "name" "string"; json_close_object json_add_object "discord_status"; json_close_object json_add_object "discord_set_dv"; json_add_string "num" "string"; json_close_object json_add_object "discord_set_fake"; json_add_string "file" "string"; json_close_object json_add_object "game_status"; json_close_object json_add_object "game_set"; json_add_string "choice" "string"; json_close_object json_add_object "game_set_fake"; json_add_string "file" "string"; json_close_object json_add_object "game_toggle_xtreme"; json_close_object json_add_object "system_status"; json_close_object json_add_object "system_check_connectivity"; json_close_object json_add_object "system_toggle_quic"; json_close_object json_add_object "system_toggle_ipv6"; json_close_object json_add_object "system_toggle_flow_offloading_fix"; json_close_object json_add_object "system_toggle_expert_mode"; json_close_object json_add_object "system_uninstall_panel"; json_close_object json_add_object "mirror_status"; json_close_object json_add_object "mirror_set"; json_add_string "id" "string"; json_close_object json_add_object "exclusions_status"; json_close_object json_add_object "exclusions_toggle"; json_add_string "ip" "string"; json_close_object json_add_object "exclusions_clear"; json_close_object json_add_object "tg_status"; json_close_object json_add_object "tg_action"; json_add_string "variant" "string"; json_add_string "action" "string"; json_close_object json_add_object "tg_restart_all"; json_close_object json_add_object "hosts_status"; json_close_object json_add_object "hosts_toggle"; json_add_string "block" "string"; json_close_object json_add_object "hosts_replace_geohide"; json_add_string "region" "string"; json_close_object json_add_object "hosts_reset"; json_close_object json_add_object "doh_status"; json_close_object json_add_object "doh_install"; json_close_object json_add_object "doh_remove"; json_close_object json_add_object "doh_set"; json_add_string "provider" "string"; json_close_object json_dump } call_method() { local method="$1" local input input="$(cat)" json_load "$input" 2>/dev/null case "$method" in status) "$BACKEND" status ;; system_info) "$BACKEND" system_info ;; job_status) json_get_var job job; "$BACKEND" job_status "$job" ;; log_tail) json_get_var job job; "$BACKEND" log_tail "$job" ;; zapret_action) json_get_var action action; "$BACKEND" zapret_action "$action" ;; zapret_latest_version) "$BACKEND" zapret_latest_version ;; zapret2_action) json_get_var action action; "$BACKEND" zapret2_action "$action" ;; strategy_list_v) "$BACKEND" strategy_list_v ;; strategy_set_v) json_get_var version version; "$BACKEND" strategy_set_v "$version" ;; strategy_list_flowseal) "$BACKEND" strategy_list_flowseal ;; strategy_set_flowseal) json_get_var name name; "$BACKEND" strategy_set_flowseal "$name" ;; strategy_list_youtube) "$BACKEND" strategy_list_youtube ;; strategy_set_youtube) json_get_var name name; "$BACKEND" strategy_set_youtube "$name" ;; discord_status) "$BACKEND" discord_status ;; discord_set_dv) json_get_var num num; "$BACKEND" discord_set_dv "$num" ;; discord_set_fake) json_get_var file file; "$BACKEND" discord_set_fake "$file" ;; game_status) "$BACKEND" game_status ;; game_set) json_get_var choice choice; "$BACKEND" game_set "$choice" ;; game_set_fake) json_get_var file file; "$BACKEND" game_set_fake "$file" ;; game_toggle_xtreme) "$BACKEND" game_toggle_xtreme ;; system_status) "$BACKEND" system_status ;; system_check_connectivity) "$BACKEND" system_check_connectivity ;; system_toggle_quic) "$BACKEND" system_toggle_quic ;; system_toggle_ipv6) "$BACKEND" system_toggle_ipv6 ;; system_toggle_flow_offloading_fix) "$BACKEND" system_toggle_flow_offloading_fix ;; system_toggle_expert_mode) "$BACKEND" system_toggle_expert_mode ;; system_uninstall_panel) "$BACKEND" system_uninstall_panel ;; mirror_status) "$BACKEND" mirror_status ;; mirror_set) json_get_var id id; "$BACKEND" mirror_set "$id" ;; exclusions_status) "$BACKEND" exclusions_status ;; exclusions_toggle) json_get_var ip ip; "$BACKEND" exclusions_toggle "$ip" ;; exclusions_clear) "$BACKEND" exclusions_clear ;; tg_status) "$BACKEND" tg_status ;; tg_action) json_get_var variant variant; json_get_var action action; "$BACKEND" tg_action "$variant" "$action" ;; tg_restart_all) "$BACKEND" tg_restart_all ;; hosts_status) "$BACKEND" hosts_status ;; hosts_toggle) json_get_var block block; "$BACKEND" hosts_toggle "$block" ;; hosts_replace_geohide) json_get_var region region; "$BACKEND" hosts_replace_geohide "$region" ;; hosts_reset) "$BACKEND" hosts_reset ;; doh_status) "$BACKEND" doh_status ;; doh_install) "$BACKEND" doh_install ;; doh_remove) "$BACKEND" doh_remove ;; doh_set) json_get_var provider provider; "$BACKEND" doh_set "$provider" ;; *) echo '{"error":"unknown method"}'; return 1 ;; esac } case "$1" in list) list_methods ;; call) call_method "$2" ;; *) echo '{"error":"usage: zapret-manager {list|call }"}'; exit 1 ;; esac ZM_INSTALLER_EOF chmod 0755 '/usr/libexec/rpcd/zapret-manager' mkdir -p /usr/share/rpcd/acl.d cat > '/usr/share/rpcd/acl.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF' { "luci-app-zapret-manager": { "description": "Grant access to the Zapret Manager backend", "read": { "ubus": { "zapret-manager": [ "status", "job_status", "log_tail", "system_info", "strategy_list_v", "strategy_list_flowseal", "strategy_list_youtube", "discord_status", "hosts_status", "doh_status", "game_status", "system_status", "mirror_status", "exclusions_status", "tg_status", "zapret_latest_version" ] } }, "write": { "ubus": { "zapret-manager": [ "zapret_action", "zapret2_action", "strategy_set_v", "strategy_set_flowseal", "strategy_set_youtube", "discord_set_dv", "discord_set_fake", "hosts_toggle", "doh_set", "hosts_replace_geohide", "hosts_reset", "doh_install", "doh_remove", "game_set", "game_set_fake", "game_toggle_xtreme", "system_check_connectivity", "system_toggle_quic", "system_toggle_ipv6", "system_toggle_flow_offloading_fix", "system_toggle_expert_mode", "system_uninstall_panel", "mirror_set", "exclusions_toggle", "exclusions_clear", "tg_action", "tg_restart_all" ] } } } } ZM_INSTALLER_EOF mkdir -p /usr/share/luci/menu.d cat > '/usr/share/luci/menu.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF' { "admin/services/zapret-manager": { "title": "Zapret Manager", "order": 60, "action": { "type": "alias", "path": "admin/services/zapret-manager/dashboard" } }, "admin/services/zapret-manager/dashboard": { "title": "Дашборд", "order": 10, "action": { "type": "view", "path": "zapret-manager/dashboard" } }, "admin/services/zapret-manager/strategy": { "title": "Стратегии", "order": 20, "action": { "type": "view", "path": "zapret-manager/strategy" } }, "admin/services/zapret-manager/youtube": { "title": "YouTube", "order": 25, "action": { "type": "view", "path": "zapret-manager/youtube" } }, "admin/services/zapret-manager/game": { "title": "Игры", "order": 27, "action": { "type": "view", "path": "zapret-manager/game" } }, "admin/services/zapret-manager/discord": { "title": "Discord", "order": 30, "action": { "type": "view", "path": "zapret-manager/discord" } }, "admin/services/zapret-manager/hosts": { "title": "Hosts", "order": 40, "action": { "type": "view", "path": "zapret-manager/hosts" } }, "admin/services/zapret-manager/doh": { "title": "DNS over HTTPS", "order": 50, "action": { "type": "view", "path": "zapret-manager/doh" } }, "admin/services/zapret-manager/tgproxy": { "title": "TG WS Proxy", "order": 55, "action": { "type": "view", "path": "zapret-manager/tgproxy" } }, "admin/services/zapret-manager/system": { "title": "Система", "order": 60, "action": { "type": "view", "path": "zapret-manager/system" } }, "admin/services/zapret-manager/exclusions": { "title": "Исключения IP", "order": 70, "action": { "type": "view", "path": "zapret-manager/exclusions" } } } ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/zapret-manager cat > '/www/luci-static/resources/zapret-manager/common.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require baseclass'; 'require rpc'; 'require ui'; var callStatus = rpc.declare({ object: 'zapret-manager', method: 'status', expect: {} }); var callSystemInfo = rpc.declare({ object: 'zapret-manager', method: 'system_info', expect: {} }); var callJobStatus = rpc.declare({ object: 'zapret-manager', method: 'job_status', params: ['job'], expect: {} }); var callLogTail = rpc.declare({ object: 'zapret-manager', method: 'log_tail', params: ['job'], expect: {} }); var callZapretAction = rpc.declare({ object: 'zapret-manager', method: 'zapret_action', params: ['action'], expect: {} }); var callZapretLatestVersion = rpc.declare({ object: 'zapret-manager', method: 'zapret_latest_version', expect: {} }); var callZapret2Action = rpc.declare({ object: 'zapret-manager', method: 'zapret2_action', params: ['action'], expect: {} }); var callStrategyListV = rpc.declare({ object: 'zapret-manager', method: 'strategy_list_v', expect: {} }); var callStrategySetV = rpc.declare({ object: 'zapret-manager', method: 'strategy_set_v', params: ['version'], expect: {} }); var callStrategyListFlowseal = rpc.declare({ object: 'zapret-manager', method: 'strategy_list_flowseal', expect: {} }); var callStrategySetFlowseal = rpc.declare({ object: 'zapret-manager', method: 'strategy_set_flowseal', params: ['name'], expect: {} }); var callStrategyListYoutube = rpc.declare({ object: 'zapret-manager', method: 'strategy_list_youtube', expect: {} }); var callStrategySetYoutube = rpc.declare({ object: 'zapret-manager', method: 'strategy_set_youtube', params: ['name'], expect: {} }); var callDiscordStatus = rpc.declare({ object: 'zapret-manager', method: 'discord_status', expect: {} }); var callDiscordSetDv = rpc.declare({ object: 'zapret-manager', method: 'discord_set_dv', params: ['num'], expect: {} }); var callDiscordSetFake = rpc.declare({ object: 'zapret-manager', method: 'discord_set_fake', params: ['file'], expect: {} }); var callGameStatus = rpc.declare({ object: 'zapret-manager', method: 'game_status', expect: {} }); var callGameSet = rpc.declare({ object: 'zapret-manager', method: 'game_set', params: ['choice'], expect: {} }); var callGameSetFake = rpc.declare({ object: 'zapret-manager', method: 'game_set_fake', params: ['file'], expect: {} }); var callGameToggleXtreme = rpc.declare({ object: 'zapret-manager', method: 'game_toggle_xtreme', expect: {} }); var callSystemStatus = rpc.declare({ object: 'zapret-manager', method: 'system_status', expect: {} }); var callSystemCheckConnectivity = rpc.declare({ object: 'zapret-manager', method: 'system_check_connectivity', expect: {} }); var callSystemToggleQuic = rpc.declare({ object: 'zapret-manager', method: 'system_toggle_quic', expect: {} }); var callSystemToggleIpv6 = rpc.declare({ object: 'zapret-manager', method: 'system_toggle_ipv6', expect: {} }); var callSystemToggleFlowOffloadingFix = rpc.declare({ object: 'zapret-manager', method: 'system_toggle_flow_offloading_fix', expect: {} }); var callSystemToggleExpertMode = rpc.declare({ object: 'zapret-manager', method: 'system_toggle_expert_mode', expect: {} }); var callSystemUninstallPanel = rpc.declare({ object: 'zapret-manager', method: 'system_uninstall_panel', expect: {} }); var callMirrorStatus = rpc.declare({ object: 'zapret-manager', method: 'mirror_status', expect: {} }); var callMirrorSet = rpc.declare({ object: 'zapret-manager', method: 'mirror_set', params: ['id'], expect: {} }); var callExclusionsStatus = rpc.declare({ object: 'zapret-manager', method: 'exclusions_status', expect: {} }); var callExclusionsToggle = rpc.declare({ object: 'zapret-manager', method: 'exclusions_toggle', params: ['ip'], expect: {} }); var callExclusionsClear = rpc.declare({ object: 'zapret-manager', method: 'exclusions_clear', expect: {} }); var callTgStatus = rpc.declare({ object: 'zapret-manager', method: 'tg_status', expect: {} }); var callTgAction = rpc.declare({ object: 'zapret-manager', method: 'tg_action', params: ['variant', 'action'], expect: {} }); var callTgRestartAll = rpc.declare({ object: 'zapret-manager', method: 'tg_restart_all', expect: {} }); var callHostsStatus = rpc.declare({ object: 'zapret-manager', method: 'hosts_status', expect: {} }); var callHostsToggle = rpc.declare({ object: 'zapret-manager', method: 'hosts_toggle', params: ['block'], expect: {} }); var callHostsReplaceGeohide = rpc.declare({ object: 'zapret-manager', method: 'hosts_replace_geohide', params: ['region'], expect: {} }); var callHostsReset = rpc.declare({ object: 'zapret-manager', method: 'hosts_reset', expect: {} }); var callDohStatus = rpc.declare({ object: 'zapret-manager', method: 'doh_status', expect: {} }); var callDohInstall = rpc.declare({ object: 'zapret-manager', method: 'doh_install', expect: {} }); var callDohRemove = rpc.declare({ object: 'zapret-manager', method: 'doh_remove', expect: {} }); var callDohSet = rpc.declare({ object: 'zapret-manager', method: 'doh_set', params: ['provider'], expect: {} }); function injectCss() { if (document.getElementById('zm-css')) return; var l = document.createElement('link'); l.id = 'zm-css'; l.rel = 'stylesheet'; l.href = L.resource('view/zapret-manager/style.css'); document.head.appendChild(l); } function badge(ok, textOk, textBad) { var cls = ok ? 'zm-ok' : 'zm-bad'; return E('span', { 'class': 'zm-badge ' + cls }, [ E('span', { 'class': 'zm-dot' }), ok ? textOk : textBad ]); } // Раскрашивает построчный вывод (==> сообщения) для читаемости: стрелки — // голубым, обычные шаги — жёлтым, успешные/готово — зелёным, ошибки — красным, // !! предупреждения — оранжевым; необработанный вывод команд — приглушённым серым. function renderLog(logEl, text) { logEl.innerHTML = ''; var lines = (text || '').split('\n'); lines.forEach(function(line) { if (line === '') return; var div = document.createElement('div'); var m = line.match(/^(==>)\s?(.*)$/); if (m) { var arrow = document.createElement('span'); arrow.className = 'zm-log-arrow'; arrow.textContent = m[1] + ' '; div.appendChild(arrow); var msgSpan = document.createElement('span'); var msg = m[2]; if (/ОШИБКА|error|не удалось/i.test(msg)) msgSpan.className = 'zm-log-msg-error'; else if (/Готово|успешно|примен[её]н|установлен|удал[её]н|сгенерирован|сохран[её]н|переключен|обновлен|завершен/i.test(msg)) msgSpan.className = 'zm-log-msg-ok'; else msgSpan.className = 'zm-log-msg-info'; msgSpan.textContent = msg; div.appendChild(msgSpan); } else if (/^!!/.test(line)) { div.className = 'zm-log-msg-warn'; div.textContent = line; } else { div.className = 'zm-log-code'; div.textContent = line; } logEl.appendChild(div); }); logEl.scrollTop = logEl.scrollHeight; } function pollJob(job, logEl, onDone, onTick) { logEl.classList.add('zm-show'); var failCount = 0; var timer = setInterval(function() { Promise.all([ callJobStatus(job), callLogTail(job) ]).then(function(res) { failCount = 0; var st = res[0], lg = res[1]; renderLog(logEl, (lg && lg.lines) || ''); if (typeof onTick === 'function') onTick(); if (st && st.done === true) { clearInterval(timer); onDone(st.rc === '0'); } }).catch(function() { // Одиночная неудача опроса (роутер занят распаковкой/установкой — // временная заминка) не должна навсегда останавливать поллинг: // сама операция на роутере продолжает идти в фоне независимо от // того, отвечает ли сейчас веб-интерфейс. Останавливаемся только // после нескольких подряд неудач и явно предупреждаем. failCount++; if (failCount >= 8) { clearInterval(timer); toast('Роутер не отвечает — операция может ещё выполняться в фоне. Обновите страницу через полминуты, чтобы проверить результат.', 'warning', 25000); } }); }, 1200); } function refreshBanner(message) { return E('div', { 'class': 'zm-refresh-banner zm-show' }, [ E('span', {}, message || 'Список меню LuCI мог измениться — выйдите и зайдите заново, чтобы увидеть изменения.'), E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { // Разрываем сессию в фоне, затем перезагружаем ИМЕННО текущую // страницу (а не отдельный URL logout) — LuCI при недействительной // сессии сама показывает форму входа прямо на этом адресе и после // успешного входа возвращает сюда же (штатное поведение при // истечении сессии, тут просто используем его напрямую). fetch(L.url('admin/logout'), { credentials: 'same-origin' }).catch(function() {}).then(function() { location.reload(); }); } }, 'Выйти из LuCI') ]); } // ---------- собственные всплывающие уведомления (не системные LuCI) ---------- function toastContainer() { var c = document.getElementById('zm-toast-container'); if (!c) { c = document.createElement('div'); c.id = 'zm-toast-container'; document.body.appendChild(c); } return c; } function toast(message, kind, duration) { var container = toastContainer(); var el = E('div', { 'class': 'zm-toast zm-toast-' + (kind || 'info') }, [ E('span', { 'class': 'zm-toast-icon' }, kind === 'error' ? '✕' : (kind === 'warning' ? '!' : '✓')), E('span', { 'class': 'zm-toast-text' }, message) ]); container.appendChild(el); requestAnimationFrame(function() { el.classList.add('zm-toast-show'); }); var hide = function() { el.classList.remove('zm-toast-show'); setTimeout(function() { el.parentNode && el.parentNode.removeChild(el); }, 250); }; el.addEventListener('click', hide); setTimeout(hide, duration || (kind === 'error' ? 14000 : 9000)); } function notifyStrategyResult(res, okLabel) { if (res.error) { toast(res.error, 'error'); return false; } toast(okLabel + ' применена', 'info'); return true; } return baseclass.extend({ injectCss: injectCss, badge: badge, pollJob: pollJob, renderLog: renderLog, refreshBanner: refreshBanner, toast: toast, notifyStrategyResult: notifyStrategyResult, status: callStatus, systemInfo: callSystemInfo, jobStatus: callJobStatus, logTail: callLogTail, zapretAction: callZapretAction, zapretLatestVersion: callZapretLatestVersion, zapret2Action: callZapret2Action, strategyListV: callStrategyListV, strategySetV: callStrategySetV, strategyListFlowseal: callStrategyListFlowseal, strategySetFlowseal: callStrategySetFlowseal, strategyListYoutube: callStrategyListYoutube, strategySetYoutube: callStrategySetYoutube, discordStatus: callDiscordStatus, discordSetDv: callDiscordSetDv, discordSetFake: callDiscordSetFake, gameStatus: callGameStatus, gameSet: callGameSet, gameSetFake: callGameSetFake, gameToggleXtreme: callGameToggleXtreme, systemStatus: callSystemStatus, systemCheckConnectivity: callSystemCheckConnectivity, systemToggleQuic: callSystemToggleQuic, systemToggleIpv6: callSystemToggleIpv6, systemToggleFlowOffloadingFix: callSystemToggleFlowOffloadingFix, systemToggleExpertMode: callSystemToggleExpertMode, systemUninstallPanel: callSystemUninstallPanel, mirrorStatus: callMirrorStatus, mirrorSet: callMirrorSet, exclusionsStatus: callExclusionsStatus, exclusionsToggle: callExclusionsToggle, exclusionsClear: callExclusionsClear, tgStatus: callTgStatus, tgAction: callTgAction, tgRestartAll: callTgRestartAll, hostsStatus: callHostsStatus, hostsToggle: callHostsToggle, hostsReplaceGeohide: callHostsReplaceGeohide, hostsReset: callHostsReset, dohStatus: callDohStatus, dohInstall: callDohInstall, dohRemove: callDohRemove, dohSet: callDohSet }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/dashboard.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require ui'; 'require zapret-manager.common as zm'; return view.extend({ load: function() { zm.injectCss(); return Promise.all([ zm.status(), zm.dohStatus().catch(function() { return {}; }), zm.hostsStatus().catch(function() { return { items: [] }; }), zm.systemStatus().catch(function() { return {}; }), zm.zapretLatestVersion().catch(function() { return {}; }), zm.systemInfo().catch(function() { return {}; }) ]); }, render: function(all) { var view = this; var data = all[0], dohData = all[1], hostsData = all[2], sysData = all[3]; var latestVersion = (all[4] && all[4].version) || ''; var sysInfo = all[5] || {}; var wrap = E('div', { 'class': 'zm-wrap' }); var overviewEl = E('div', {}); var cards = E('div', { 'class': 'zm-cards' }); var logEl = E('pre', { 'class': 'zm-log' }); var DOH_LABELS = { cloudflare: 'Cloudflare', google: 'Google', quad9: 'Quad9', xbox: 'XBOX', geohide_ru: 'GeoHide RU', geohide_eu: 'GeoHide EU', geohide_us: 'GeoHide US' }; function renderOverview(d, doh, hosts, sys) { var hostsEnabled = (hosts.items || []).filter(function(it) { return it.enabled; }).length; var hostsTotal = (hosts.items || []).length; var sysFlags = []; if (sys.quic_blocked) sysFlags.push('QUIC заблокирован'); if (sys.ipv6_enabled) sysFlags.push('IPv6 в Zapret включён'); if (sys.flow_offloading_fix) sysFlags.push('Flow Offloading fix'); return E('div', { 'class': 'zm-card', 'style': 'margin-bottom:4px' }, [ E('h3', {}, 'Обзор'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Zapret'), d.zapret === 'installed' ? zm.badge(d.zapret_running === true, 'запущен', 'остановлен') : zm.badge(false, '', 'не установлен') ]), d.zapret2 === 'installed' ? E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Zapret2'), zm.badge(d.zapret2_running === true, 'запущен', 'остановлен') ]) : E([]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'DNS over HTTPS'), doh.installed ? zm.badge(true, DOH_LABELS[doh.current] || doh.current || 'установлен, провайдер не определён', '') : zm.badge(false, '', 'не установлен') ]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Домены в hosts'), hosts.geohide ? zm.badge(true, 'GeoHide ' + hosts.geohide.toUpperCase(), '') : hostsTotal ? zm.badge(hostsEnabled > 0, hostsEnabled + ' из ' + hostsTotal + ' включено', 'ничего не включено') : E('span', {}, '—') ]), sysFlags.length ? E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Система'), E('span', { 'style': 'overflow-wrap:anywhere' }, sysFlags.join(', ')) ]) : E([]) ]); } function renderCards(d) { cards.innerHTML = ''; var zActions = []; if (d.zapret === 'installed') { zActions.push(E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(view, 'doAction', 'remove') }, 'Удалить')); if (/^[0-9]+\.[0-9]+$/.test(latestVersion) && d.zapret_version && latestVersion !== d.zapret_version) { zActions.push(E('button', { 'class': 'cbi-button', 'click': ui.createHandlerFn(view, 'doAction', 'update') }, 'Обновить до ' + latestVersion)); } zActions.push(E('button', { 'class': 'cbi-button', 'click': ui.createHandlerFn(view, 'doAction', d.zapret_running ? 'stop' : 'start') }, d.zapret_running ? 'Остановить' : 'Запустить')); } else { zActions.push(E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.createHandlerFn(view, 'doAction', 'install') }, 'Установить')); } var zCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Zapret'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Статус'), d.zapret === 'installed' ? zm.badge(d.zapret_running === true, 'запущен', 'остановлен') : zm.badge(false, '', 'не установлен') ]), d.zapret_version ? E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Версия'), E('span', {}, d.zapret_version) ]) : E([]), d.strategy ? E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Стратегия'), E('span', {}, d.strategy) ]) : E([]), E('div', { 'class': 'zm-actions' }, zActions) ]); var z2Actions = []; if (d.zapret2 === 'installed') { z2Actions.push(E('button', { 'class': 'cbi-button cbi-button-remove', 'click': ui.createHandlerFn(view, 'doAction2', 'remove') }, 'Удалить')); z2Actions.push(E('button', { 'class': 'cbi-button', 'click': ui.createHandlerFn(view, 'doAction2', d.zapret2_running ? 'stop' : 'start') }, d.zapret2_running ? 'Остановить' : 'Запустить')); } else { z2Actions.push(E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.createHandlerFn(view, 'doAction2', 'install') }, 'Установить')); } var z2Card = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Zapret2'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Статус'), d.zapret2 === 'installed' ? zm.badge(d.zapret2_running === true, 'запущен', 'остановлен') : zm.badge(false, '', 'не установлен') ]), E('p', { 'class': 'zm-hint' }, 'Только для архитектуры aarch64_cortex-a53. Несовместим с Zapret.'), E('div', { 'class': 'zm-actions' }, z2Actions) ]); var sysCard = E('div', { 'class': 'zm-card' }, [ E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Модель:'), E('span', {}, sysInfo.model || '—') ]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Архитектура:'), E('span', {}, sysInfo.arch || '—') ]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'OpenWrt:'), E('span', {}, sysInfo.openwrt || '—') ]), E('p', { 'class': 'zm-hint', 'style': 'margin-top:10px; margin-bottom:2px' }, 'Место на роутере:'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, '/tmp'), E('span', {}, sysInfo.tmp_free ? ('занято ' + sysInfo.tmp_used + ' · свободно ' + sysInfo.tmp_free) : '—') ]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, '/root'), E('span', {}, sysInfo.root_free ? ('занято ' + sysInfo.root_used + ' · свободно ' + sysInfo.root_free) : '—') ]) ]); cards.appendChild(zCard); cards.appendChild(z2Card); cards.appendChild(sysCard); } function refreshOverview() { Promise.all([ zm.status(), zm.dohStatus().catch(function() { return {}; }), zm.hostsStatus().catch(function() { return { items: [] }; }), zm.systemStatus().catch(function() { return {}; }) ]).then(function(res) { overviewEl.innerHTML = ''; overviewEl.appendChild(renderOverview(res[0], res[1], res[2], res[3])); }); } overviewEl.appendChild(renderOverview(data, dohData, hostsData, sysData)); renderCards(data); wrap.appendChild(E('div', { 'class': 'zm-header' }, [ E('h2', {}, 'Zapret Manager'), E('span', { 'class': 'zm-header-by' }, 'by StressOzz'), E('div', { 'class': 'zm-header-links' }, [ E('a', { 'href': 'http://stresskvn.lol/', 'target': '_blank', 'rel': 'noreferrer' }, 'StressKVN — обход белых списков!'), E('a', { 'href': 'https://t.me/stressozz_manager', 'target': '_blank', 'rel': 'noreferrer' }, 'Сообщество Telegram') ]) ])); wrap.appendChild(overviewEl); wrap.appendChild(cards); wrap.appendChild(logEl); var bannerEl = E('div', {}); wrap.appendChild(bannerEl); this.logEl = logEl; this.bannerEl = bannerEl; this.renderCards = renderCards; this.refreshOverview = refreshOverview; return wrap; }, doAction: function(action) { var view = this; if (view.busy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } var job = (action === 'install' || action === 'update') ? 'install_zapret' : (action === 'remove') ? 'remove_zapret' : null; var LABELS = { install: 'Устанавливаем Zapret', update: 'Обновляем Zapret', remove: 'Удаляем Zapret', start: 'Запускаем Zapret', stop: 'Останавливаем Zapret' }; zm.toast(LABELS[action] || 'Выполняем', 'warning'); if (job) view.busy = true; zm.zapretAction(action).then(function(res) { if (job && res && res.started) { zm.pollJob(job, view.logEl, function(ok) { view.busy = false; zm.toast(ok ? 'Готово' : 'Операция завершилась с ошибкой', ok ? 'info' : 'error'); zm.status().then(function(d) { view.renderCards(d); }); view.refreshOverview(); if (ok && (action === 'install' || action === 'remove')) { view.bannerEl.innerHTML = ''; view.bannerEl.appendChild(zm.refreshBanner('Пункт меню Zapret в LuCI мог измениться — выйдите и зайдите заново.')); } }); } else if (res) { view.busy = false; view.renderCards(res); view.refreshOverview(); } }).catch(function() { view.busy = false; }); }, doAction2: function(action) { var view = this; if (view.busy2) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } var job = (action === 'install' || action === 'update') ? 'install_zapret2' : (action === 'remove') ? 'remove_zapret2' : null; var LABELS = { install: 'Устанавливаем Zapret2', update: 'Обновляем Zapret2', remove: 'Удаляем Zapret2', start: 'Запускаем Zapret2', stop: 'Останавливаем Zapret2' }; zm.toast(LABELS[action] || 'Выполняем', 'warning'); if (job) view.busy2 = true; zm.zapret2Action(action).then(function(res) { if (job && res && res.started) { zm.pollJob(job, view.logEl, function(ok) { view.busy2 = false; zm.toast(ok ? 'Готово' : 'Операция завершилась с ошибкой', ok ? 'info' : 'error'); zm.status().then(function(d) { view.renderCards(d); }); view.refreshOverview(); if (ok && (action === 'install' || action === 'remove')) { view.bannerEl.innerHTML = ''; view.bannerEl.appendChild(zm.refreshBanner('Пункт меню Zapret2 в LuCI мог измениться — выйдите и зайдите заново.')); } }); } else if (res) { view.busy2 = false; view.renderCards(res); view.refreshOverview(); } }).catch(function() { view.busy2 = false; }); } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/discord.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; var FAKES = [ 'stun.bin', 'stun2.bin', 'quic_initial_4pda_to.bin', 'quic_initial_tencent_com.bin', 'tls_clienthello_sochi_park.bin', 'quic_initial_www_google_com.bin', 'quic_initial_steamcommunity_com.bin', 'quic_initial_5ka_ru.bin', 'quic_initial_rutube_ru.bin' ]; return view.extend({ load: function() { zm.injectCss(); return zm.discordStatus(); }, render: function(data) { var wrap = E('div', { 'class': 'zm-wrap' }); var dvGrid = E('div', { 'class': 'zm-grid' }); var fakeGrid = E('div', { 'class': 'zm-grid' }); function renderDv() { dvGrid.innerHTML = ''; (data.available || []).forEach(function(dv) { var num = dv.replace('Dv', ''); dvGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.current === dv ? ' zm-active' : ''), 'click': function() { zm.toast('Применяем стратегию ' + dv + '', 'warning'); zm.discordSetDv(num).then(function(res) { if (!zm.notifyStrategyResult(res, dv)) return; refreshState(); }); } }, dv)); }); } function renderFake() { fakeGrid.innerHTML = ''; FAKES.forEach(function(f) { fakeGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.current_fake === f ? ' zm-active' : ''), 'click': function() { zm.toast('Меняем fake-файл на ' + f + '', 'warning'); zm.discordSetFake(f).then(function(res) { if (!zm.notifyStrategyResult(res, f)) return; refreshState(); }); } }, f)); }); } function refreshState() { zm.discordStatus().then(function(res) { data = res; renderDv(); renderFake(); }); } renderDv(); renderFake(); var dvCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Стратегия для discord.media'), dvGrid, E('p', { 'class': 'zm-hint' }, 'Нужна базовая стратегия с блоком discord.media') ]); var fakeCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Fake-файл для discord,stun'), fakeGrid ]); wrap.appendChild(dvCard); wrap.appendChild(fakeCard); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/doh.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; var PROVIDERS = [ { id: 'google', label: 'Google' }, { id: 'cloudflare', label: 'Cloudflare' }, { id: 'quad9', label: 'Quad9' }, { id: 'xbox', label: 'XBOX' }, { id: 'geohide_ru', label: 'GeoHide RU' }, { id: 'geohide_eu', label: 'GeoHide EU' }, { id: 'geohide_us', label: 'GeoHide US' } ]; return view.extend({ load: function() { zm.injectCss(); return zm.dohStatus(); }, render: function(data) { var wrap = E('div', { 'class': 'zm-wrap' }); var logEl = E('pre', { 'class': 'zm-log' }); var bannerEl = E('div', {}); var busy = false; var grid = E('div', { 'class': 'zm-grid' }); function renderGrid() { grid.innerHTML = ''; PROVIDERS.forEach(function(p) { grid.appendChild(E('div', { 'class': 'zm-tile' + (data.current === p.id ? ' zm-active' : ''), 'click': function() { if (busy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } zm.toast('Меняем DNS на ' + p.label, 'warning'); zm.dohSet(p.id).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(p.label + ' применён', 'info'); zm.dohStatus().then(function(res2) { data = res2; renderGrid(); }); }); } }, p.label)); }); } renderGrid(); var installBtn = E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { if (busy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } zm.toast('Устанавливаем DNS over HTTPS', 'warning'); busy = true; zm.dohInstall().then(function(res) { if (res.error) { busy = false; zm.toast(res.error, 'error'); return; } if (res.started) { zm.pollJob('doh_install', logEl, function(ok) { busy = false; zm.toast(ok ? 'DNS over HTTPS установлен' : 'Ошибка установки', ok ? 'info' : 'error'); if (ok) { bannerEl.innerHTML = ''; bannerEl.appendChild(zm.refreshBanner('Пункт меню DNS over HTTPS в LuCI мог измениться — выйдите и зайдите заново.')); } }); } else { busy = false; } }).catch(function() { busy = false; }); } }, 'Установить DNS over HTTPS'); var removeBtn = E('button', { 'class': 'cbi-button cbi-button-remove', 'click': function() { if (busy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; } zm.toast('Удаляем DNS over HTTPS', 'warning'); busy = true; zm.dohRemove().then(function(res) { if (res.error) { busy = false; zm.toast(res.error, 'error'); return; } if (res.started) { zm.pollJob('doh_remove', logEl, function(ok) { busy = false; zm.toast(ok ? 'DNS over HTTPS удалён' : 'Ошибка удаления', ok ? 'info' : 'error'); if (ok) { bannerEl.innerHTML = ''; bannerEl.appendChild(zm.refreshBanner('Пункт меню DNS over HTTPS в LuCI мог измениться — выйдите и зайдите заново.')); } }); } else { busy = false; } }).catch(function() { busy = false; }); } }, 'Удалить'); var card = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'DNS over HTTPS'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Пакет'), zm.badge(data.installed === true, 'установлен', 'не установлен') ]), E('div', { 'class': 'zm-actions' }, data.installed ? [ removeBtn ] : [ installBtn ]), grid, logEl ]); wrap.appendChild(card); wrap.appendChild(bannerEl); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/exclusions.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; return view.extend({ load: function() { zm.injectCss(); return zm.exclusionsStatus(); }, render: function(data) { var wrap = E('div', { 'class': 'zm-wrap' }); if (data.error) { wrap.appendChild(E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Исключения IP'), E('p', { 'class': 'zm-hint' }, data.error) ])); return wrap; } var grid = E('div', { 'class': 'zm-grid' }); function renderGrid(devices) { grid.innerHTML = ''; (devices || []).forEach(function(d) { grid.appendChild(E('div', { 'class': 'zm-tile' + (d.excluded ? ' zm-active' : ''), 'click': function() { zm.toast('Переключаем исключение для ' + d.ip + '', 'warning'); zm.exclusionsToggle(d.ip).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(d.ip + (res.excluded ? ' исключён' : ' больше не исключён'), 'info'); zm.exclusionsStatus().then(function(r) { renderGrid(r.devices); }); }); } }, [ E('div', {}, d.ip), E('div', { 'class': 'zm-hint' }, d.name) ])); }); if (!devices || !devices.length) grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Устройства не найдены (нет записей в /tmp/dhcp.leases).')); } renderGrid(data.devices); var manualInput = E('input', { 'type': 'text', 'placeholder': '192.168.1.100', 'class': 'cbi-input-text' }); var card = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Исключения IP из Zapret'), grid, E('p', { 'class': 'zm-hint' }, 'Клик по устройству — включить/выключить исключение (трафик этого IP не проходит через Zapret).'), E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button', 'click': function() { zm.toast('Обновляем список устройств', 'warning'); zm.exclusionsStatus().then(function(res) { renderGrid(res.devices); zm.toast('Список устройств обновлён', 'info'); }); } }, 'Обновить список'), manualInput, E('button', { 'class': 'cbi-button', 'click': function() { var ip = manualInput.value.trim(); if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) { zm.toast('Некорректный IPv4 адрес', 'error'); return; } zm.toast('Добавляем ' + ip + ' в исключения', 'warning'); zm.exclusionsToggle(ip).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } manualInput.value = ''; zm.toast(ip + (res.excluded ? ' добавлен в исключения' : ' убран из исключений'), 'info'); zm.exclusionsStatus().then(function(r) { renderGrid(r.devices); }); }); } }, 'Добавить вручную'), E('button', { 'class': 'cbi-button cbi-button-remove', 'click': function() { zm.toast('Очищаем все исключения', 'warning'); zm.exclusionsClear().then(function() { zm.toast('Все исключения очищены', 'info'); zm.exclusionsStatus().then(function(res) { renderGrid(res.devices); }); }); } }, 'Очистить все') ]) ]); wrap.appendChild(card); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/game.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; var FAKES = [ 'stun.bin', 'stun2.bin', 'quic_initial_4pda_to.bin', 'quic_initial_tencent_com.bin', 'tls_clienthello_sochi_park.bin', 'quic_initial_www_google_com.bin', 'quic_initial_steamcommunity_com.bin', 'quic_initial_5ka_ru.bin', 'quic_initial_rutube_ru.bin' ]; return view.extend({ load: function() { zm.injectCss(); return zm.gameStatus(); }, render: function(data) { var wrap = E('div', { 'class': 'zm-wrap' }); var gvGrid = E('div', { 'class': 'zm-grid' }); var xtremeRow = E('div', {}); var fakeGrid = E('div', { 'class': 'zm-grid' }); function renderGv() { gvGrid.innerHTML = ''; [1, 2, 3, 4].forEach(function(n) { gvGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.current === ('Gv' + n) ? ' zm-active' : ''), 'click': function() { zm.toast('Применяем игровую стратегию Gv' + n + '', 'warning'); zm.gameSet(String(n)).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(res.game === 'none' ? 'Игровая стратегия снята' : res.game + ' применена', 'info'); refreshState(); }); } }, 'Gv' + n)); }); } function renderXtreme() { var xtreme = data.xtreme === true; xtremeRow.innerHTML = ''; xtremeRow.appendChild(E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Статус'), zm.badge(xtreme, 'включён', 'выключен') ])); xtremeRow.appendChild(E('p', { 'class': 'zm-hint' }, 'Внимание: может повлиять на работу приложений и соединений — используйте только для проверки игр.')); xtremeRow.appendChild(E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': xtreme ? 'cbi-button cbi-button-remove' : 'cbi-button cbi-button-positive', 'click': function() { zm.toast(xtreme ? 'Выключаем Xtreme' : 'Включаем Xtreme', 'warning'); zm.gameToggleXtreme().then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(res.xtreme ? 'Xtreme включён' : 'Xtreme выключен', 'info'); refreshState(); }); } }, xtreme ? 'Выключить Xtreme' : 'Включить Xtreme') ])); } function renderFake() { fakeGrid.innerHTML = ''; FAKES.forEach(function(f) { fakeGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.fake === f ? ' zm-active' : ''), 'click': function() { zm.toast('Меняем fake-файл на ' + f + '', 'warning'); zm.gameSetFake(f).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(f + ' установлен', 'info'); refreshState(); }); } }, f)); }); } function refreshState() { zm.gameStatus().then(function(res) { data = res; renderGv(); renderXtreme(); renderFake(); }); } renderGv(); renderXtreme(); renderFake(); var gvCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Игровая стратегия'), gvGrid, E('p', { 'class': 'zm-hint' }, 'Повторный клик по уже выбранной — снимает игровую стратегию.') ]); var xtremeCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Xtreme режим'), xtremeRow ]); var fakeCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Fake-файл для игровой стратегии'), fakeGrid, E('p', { 'class': 'zm-hint' }, 'Доступно только если игровая стратегия уже выбрана.') ]); wrap.appendChild(gvCard); wrap.appendChild(xtremeCard); wrap.appendChild(fakeCard); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/hosts.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; var LABELS = { nalog: 'Налог.ру', rutor: 'RUTOR', ntc: 'ntc.party', instagram: 'Instagram & Facebook', librusec: 'lib.rus.ec', ai: 'AI сервисы (ChatGPT, Claude, Gemini)', twitch: 'Twitch', telegram: 'Telegram Web', spotify: 'Spotify', spotifyext: 'Spotify (расширенный)', scell: 'Supercell (Clash, Brawl Stars)', githubraw: 'githubusercontent.com', github: 'GitHub', ubisoft: 'Ubisoft', tapeop: 'tapeop.dev', roblox: 'Roblox', updatesdiscord: 'Discord (updates.discord.com)' }; return view.extend({ load: function() { zm.injectCss(); return zm.hostsStatus(); }, render: function(data) { var wrap = E('div', { 'class': 'zm-wrap' }); var grid = E('div', { 'class': 'zm-grid' }); function renderGrid(items) { grid.innerHTML = ''; (items || []).forEach(function(it) { grid.appendChild(E('div', { 'class': 'zm-tile' + (it.enabled ? ' zm-active' : ''), 'click': function() { zm.toast('Переключаем ' + (LABELS[it.id] || it.id) + '', 'warning'); zm.hostsToggle(it.id).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast((LABELS[it.id] || it.id) + (res.enabled ? ' включён' : ' выключен'), 'info'); zm.hostsStatus().then(function(r) { renderGrid(r.items); }); }); } }, LABELS[it.id] || it.id)); }); } renderGrid(data.items); var card = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Домены в /etc/hosts'), grid, E('p', { 'class': 'zm-hint' }, 'Нажмите на блок, чтобы включить или выключить прописанные IP этого сервиса.') ]); wrap.appendChild(card); var geoLogEl = E('pre', { 'class': 'zm-log' }); var geoGrid = E('div', { 'class': 'zm-grid' }); function renderGeoGrid() { geoGrid.innerHTML = ''; geoGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.geohide === 'ru' ? ' zm-active' : ''), 'click': function() { replaceGeohide('ru'); } }, 'GeoHide RU')); geoGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.geohide === 'eu' ? ' zm-active' : ''), 'click': function() { replaceGeohide('eu'); } }, 'GeoHide EU')); geoGrid.appendChild(E('div', { 'class': 'zm-tile' + (data.geohide === 'us' ? ' zm-active' : ''), 'click': function() { replaceGeohide('us'); } }, 'GeoHide US')); } renderGeoGrid(); var resetBtn = E('button', { 'class': 'cbi-button cbi-button-remove', 'click': resetHosts }, 'Восстановить hosts'); var geoCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Заменить hosts на GeoHide'), E('p', { 'class': 'zm-hint' }, 'Внимание: это ПОЛНОСТЬЮ заменит файл /etc/hosts на список от GeoHide DNS — все блоки выше и любые ваши собственные записи будут удалены.'), geoGrid, E('p', { 'class': 'zm-hint' }, 'Или восстановить hosts к чистому виду. Уберёт и блоки выше, и GeoHide.'), E('div', { 'class': 'zm-actions' }, [ resetBtn ]), geoLogEl ]); wrap.appendChild(geoCard); function refreshAll() { zm.hostsStatus().then(function(res) { data = res; renderGrid(res.items); renderGeoGrid(); }); } function replaceGeohide(region) { zm.toast('Заменяем hosts на GeoHide ' + region.toUpperCase() + '', 'warning'); geoLogEl.classList.add('zm-show'); zm.renderLog(geoLogEl, '==> Скачиваем и заменяем /etc/hosts'); zm.hostsReplaceGeohide(region).then(function(res) { if (res.error) { zm.renderLog(geoLogEl, '==> ОШИБКА: ' + res.error); zm.toast(res.error, 'error'); return; } zm.renderLog(geoLogEl, '==> Готово — hosts заменён на GeoHide ' + region.toUpperCase() + '.'); zm.toast('hosts заменён на GeoHide ' + region.toUpperCase(), 'info'); refreshAll(); }); } function resetHosts() { zm.toast('Восстанавливаем hosts', 'warning'); geoLogEl.classList.add('zm-show'); zm.renderLog(geoLogEl, '==> Восстанавливаем hosts'); zm.hostsReset().then(function(res) { if (res.error) { zm.renderLog(geoLogEl, '==> ОШИБКА: ' + res.error); zm.toast(res.error, 'error'); return; } zm.renderLog(geoLogEl, '==> Готово — hosts восстановлен.'); zm.toast('hosts восстановлен', 'info'); refreshAll(); }); } return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/strategy.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; return view.extend({ load: function() { zm.injectCss(); return Promise.all([ zm.status(), zm.strategyListV() ]); }, render: function(data) { var status = data[0], vList = data[1]; var lastFlowseal = null; var wrap = E('div', { 'class': 'zm-wrap' }); var logEl = E('pre', { 'class': 'zm-log' }); var currentBanner = E('div', { 'class': 'zm-current-banner' }); wrap.appendChild(currentBanner); var vGrid = E('div', { 'class': 'zm-grid' }); var vCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Стратегии v1 – v10'), vGrid ]); var fGrid = E('div', { 'class': 'zm-grid' }); var fCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Стратегии Flowseal'), E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button', 'click': function() { zm.toast('Обновляем список Flowseal', 'warning'); fGrid.innerHTML = 'Загрузка списка'; zm.strategyListFlowseal().then(function(res) { if (res.started) { zm.pollJob('flowseal_download', logEl, function(ok) { zm.toast(ok ? 'Список Flowseal обновлён' : 'Не удалось обновить список', ok ? 'info' : 'error'); zm.strategyListFlowseal().then(renderFlowseal); }); } else { renderFlowseal(res); zm.toast('Список Flowseal обновлён', 'info'); } }); } }, 'Обновить список') ]), fGrid ]); function renderBanner() { currentBanner.className = 'zm-current-banner' + (status.strategy ? '' : ' zm-current-empty'); currentBanner.innerHTML = ''; if (status.strategy) { currentBanner.appendChild(E('span', {}, 'Сейчас применено: ')); currentBanner.appendChild(E('b', {}, status.strategy)); } else { currentBanner.appendChild(E('span', {}, 'Стратегия ещё не выбрана')); } } function renderV() { vGrid.innerHTML = ''; (vList.items || []).forEach(function(it) { var words = (' ' + (status.strategy || '') + ' '); vGrid.appendChild(E('div', { 'class': 'zm-tile' + (!status.flowseal && words.indexOf(' ' + it.id + ' ') !== -1 ? ' zm-active' : ''), 'click': function() { zm.toast('Применяем стратегию ' + it.id + '', 'warning'); zm.strategySetV(it.id).then(function(res) { if (!zm.notifyStrategyResult(res, it.id)) return; refreshState(); }); } }, it.id)); }); } function renderFlowseal(res) { lastFlowseal = res; fGrid.innerHTML = ''; (res.items || []).forEach(function(it) { fGrid.appendChild(E('div', { 'class': 'zm-tile' + (status.flowseal === it.id ? ' zm-active' : ''), 'click': function() { zm.toast('Применяем стратегию ' + it.label + '', 'warning'); zm.strategySetFlowseal(it.id).then(function(r2) { if (!zm.notifyStrategyResult(r2, it.id)) return; refreshState(); }); } }, it.label)); }); if (!res.items || !res.items.length) fGrid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список пуст — нажмите «Обновить список».')); } function refreshState() { zm.status().then(function(s) { status = s; renderBanner(); renderV(); if (lastFlowseal) renderFlowseal(lastFlowseal); }); } renderBanner(); renderV(); wrap.appendChild(vCard); wrap.appendChild(fCard); wrap.appendChild(logEl); zm.strategyListFlowseal().then(function(res) { if (!res.started) renderFlowseal(res); else fGrid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список ещё не загружен — нажмите «Обновить список».')); }); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/style.css' << 'ZM_INSTALLER_EOF' .zm-wrap { display: flex; flex-direction: column; gap: 16px; max-width: 1100px; } .zm-header { display: flex; align-items: baseline; gap: 10px; margin-bottom: -4px; flex-wrap: wrap; } .zm-header h2 { margin: 0; font-size: 22px; font-weight: 700; } .zm-header-by { font-size: 13px; opacity: .55; } .zm-header-links { display: flex; gap: 8px; margin-left: auto; flex-wrap: wrap; } .zm-header-links a { font-size: 12.5px; font-weight: 600; text-decoration: none; color: #229ed9; background: rgba(34,158,217,.1); border: 1px solid rgba(34,158,217,.25); border-radius: 999px; padding: 5px 13px; transition: background .15s; } .zm-header-links a:hover { background: rgba(34,158,217,.18); } .zm-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 14px; } .zm-card { background: var(--background-color-medium, #fff); border: 1px solid rgba(0,0,0,.08); border-radius: 12px; padding: 18px 20px; box-shadow: 0 1px 3px rgba(0,0,0,.05), 0 1px 2px rgba(0,0,0,.04); overflow-wrap: break-word; transition: box-shadow .15s; } .zm-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,.08); } .zm-card h3 { margin: 0 0 12px 0; font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px; } /* Строка "подпись: значение" — подпись и значение стоят РЯДОМ (умеренный отступ), а не растянуты в разные концы широкой карточки. */ .zm-row { display: flex; align-items: center; gap: 12px; margin: 7px 0; font-size: 13px; flex-wrap: wrap; } .zm-row .zm-label { opacity: .65; flex-shrink: 0; } .zm-row > span:last-child { overflow-wrap: anywhere; } .zm-badge { display: inline-flex; align-items: center; gap: 6px; padding: 3px 11px; border-radius: 999px; font-size: 12px; font-weight: 600; white-space: nowrap; } .zm-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; } .zm-ok { background: rgba(46,160,67,.12); color: #1a7f37; } .zm-ok .zm-dot { background: #1a7f37; } .zm-bad { background: rgba(207,34,46,.10); color: #cf222e; } .zm-bad .zm-dot { background: #cf222e; } .zm-warn { background: rgba(191,135,0,.12); color: #9a6700; } .zm-warn .zm-dot { background: #9a6700; } .zm-off { background: rgba(110,118,129,.12); color: #57606a; } .zm-off .zm-dot { background: #57606a; } .zm-actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin: 14px 0; } .zm-actions .cbi-button { margin: 0; } /* Плитки: flex вместо grid — компактные "чипы" по контенту, а не растянутые на всю колонку, аккуратно переносятся на новую строку. */ .zm-grid { display: flex; flex-wrap: wrap; gap: 9px; } .zm-tile { flex: 0 1 auto; min-width: 90px; max-width: 100%; border: 1px solid rgba(0,0,0,.1); border-radius: 9px; padding: 10px 16px; cursor: pointer; text-align: center; font-size: 13px; line-height: 1.35; overflow-wrap: break-word; transition: border-color .15s, background .15s, transform .1s; background: var(--background-color-low, #fafafa); } .zm-tile:hover { border-color: #1a7f37; transform: translateY(-1px); } .zm-tile.zm-active { border-color: #1a7f37; background: rgba(26,127,55,.16); font-weight: 700; color: #15803d; box-shadow: 0 0 0 2px rgba(26,127,55,.35); } .zm-tile.zm-active::before { content: "✓ "; } .zm-tile.zm-tile-pending { opacity: .55; border-style: dashed; cursor: not-allowed; } .zm-tile.zm-tile-pending:hover { border-color: rgba(0,0,0,.1); transform: none; } /* Живой вывод действий (установка/тест/и т.д.) — большая, ясно видимая консоль вместо мелкой полоски: крупнее шрифт, больше высоты, чёткая рамка. white-space: pre-wrap — длинные строки (URL и т.п.) переносятся ВНИЗ, а не вылезают вправо за пределы карточки. */ .zm-log { background: #0d1117; color: #e6edf3; font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, "Liberation Mono", monospace; font-size: 13.5px; line-height: 1.7; border: 1px solid rgba(255,255,255,.10); border-radius: 10px; padding: 16px 18px; white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; max-height: 520px; min-height: 220px; overflow-x: hidden; overflow-y: auto; margin-top: 14px; display: none; box-shadow: inset 0 0 0 1px rgba(0,0,0,.2); } .zm-log.zm-show { display: block; } .zm-log:empty::before { content: "Ожидание вывода..."; opacity: .4; } /* Раскраска построчного вывода (==> сообщения) для читаемости. */ .zm-log-arrow { color: #56d4dd; font-weight: 700; } .zm-log-msg-info { color: #e3c04a; } .zm-log-msg-ok { color: #3fb950; font-weight: 600; } .zm-log-msg-error { color: #ff7b72; font-weight: 600; } .zm-log-msg-warn { color: #ffa657; } .zm-log-code { color: #8b949e; } .zm-hint { font-size: 12px; opacity: .65; margin-top: 6px; line-height: 1.5; overflow-wrap: break-word; } /* Уведомление о необходимости обновить страницу LuCI после установки/удаления пакетов, добавляющих собственные меню (luci-app-*). */ .zm-refresh-banner { display: flex; align-items: center; justify-content: space-between; gap: 14px; background: rgba(191,135,0,.12); border: 2px solid rgba(191,135,0,.35); color: #9a6700; border-radius: 12px; padding: 16px 20px; font-size: 15px; font-weight: 500; margin-top: 12px; } .zm-refresh-banner button { flex-shrink: 0; } /* Собственные всплывающие уведомления — вместо системных баннеров LuCI. Сделаны крупными и заметными по запросу (увеличены повторно). */ #zm-toast-container { position: fixed; top: 20px; right: 20px; z-index: 10000; display: flex; flex-direction: column; gap: 14px; max-width: 520px; } .zm-toast { display: flex; align-items: flex-start; gap: 14px; background: #1c2128; color: #e6edf3; padding: 22px 26px; border-radius: 14px; box-shadow: 0 10px 40px rgba(0,0,0,.4); font-size: 17px; line-height: 1.5; font-weight: 500; cursor: pointer; opacity: 0; transform: translateX(24px); transition: opacity .22s ease, transform .22s ease; border-left: 6px solid #1a7f37; } .zm-toast-show { opacity: 1; transform: translateX(0); } .zm-toast-error { border-left-color: #cf222e; } .zm-toast-warning { border-left-color: #9a6700; } .zm-toast-icon { flex-shrink: 0; font-weight: 700; font-size: 22px; line-height: 1.3; } .zm-toast-info .zm-toast-icon { color: #3fb950; } .zm-toast-error .zm-toast-icon { color: #ff7b72; } .zm-toast-warning .zm-toast-icon { color: #e3b341; } .zm-toast-text { overflow-wrap: anywhere; } /* Крупная плитка со ссылкой для вставки в Telegram (tg://proxy / tg://socks) — отдельно от узких карточек статуса, чтобы длинная ссылка помещалась целиком. */ .zm-tg-link-card { background: rgba(26,127,55,.06); border: 1px solid rgba(26,127,55,.25); border-radius: 12px; padding: 16px 18px; margin-bottom: 12px; } .zm-tg-link-card h4 { margin: 0 0 8px 0; font-size: 14px; font-weight: 700; } .zm-tg-link-hint { font-size: 12.5px; opacity: .7; margin-bottom: 10px; } .zm-tg-link-box { background: #0d1117; color: #7ee787; font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace; font-size: 14.5px; line-height: 1.6; border-radius: 8px; padding: 12px 14px; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere; margin-bottom: 8px; } .zm-tg-link-row { display: flex; align-items: flex-start; gap: 10px; } .zm-tg-link-row .zm-tg-link-box { flex: 1 1 auto; } /* Заметный индикатор "что сейчас используется/применено" на страницах стратегий/DoH/hosts/fake. */ .zm-current-banner { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; background: rgba(26,127,55,.07); border: 1px solid rgba(26,127,55,.22); border-radius: 10px; padding: 10px 16px; font-size: 13px; margin-bottom: 4px; } .zm-current-banner b { font-weight: 700; } .zm-current-banner.zm-current-empty { background: rgba(110,118,129,.08); border-color: rgba(110,118,129,.2); } /* Наши страницы не используют UCI-формы LuCI (никакие uci.set/save не вызываются) — стандартная панель "Save & Apply / Save / Reset" здесь ничего не делает и только сбивает с толку. Скрываем её, пока наш CSS загружен (то есть только на страницах Zapret Manager — при переходе на любую другую страницу LuCI загружается заново без нашего style.css, и панель снова появится там, где она реально нужна). */ .cbi-page-actions { display: none !important; } ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/system.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; var MIRRORS = [ { id: 'infra', label: 'Infra OpenWrt' }, { id: 'brazil', label: 'Brazil' }, { id: 'china', label: 'China' }, { id: 'france', label: 'France' }, { id: 'italy', label: 'Italy' }, { id: 'morocco', label: 'Morocco' }, { id: 'usa', label: 'USA' }, { id: 'germany_rwth', label: 'Germany' }, { id: 'default', label: 'default / OpenWrt' } ]; return view.extend({ load: function() { zm.injectCss(); return Promise.all([ zm.systemStatus(), zm.mirrorStatus() ]); }, render: function(data) { var sysData = data[0], mirrorData = data[1]; var view = this; var wrap = E('div', { 'class': 'zm-wrap' }); var netEl = E('div', {}, [ E('p', { 'class': 'zm-hint' }, 'Нажмите «Проверить», чтобы протестировать IPv4/IPv6') ]); function renderStatusCard(d) { var card = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Система'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Блокировка QUIC'), zm.badge(d.quic_blocked === true, 'включена', 'выключена') ]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'IPv6 в Zapret'), zm.badge(d.ipv6_enabled === true, 'включён', 'выключен') ]), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Fix для Flow Offloading'), zm.badge(d.flow_offloading_fix === true, 'применён', 'не применён') ]), E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': d.quic_blocked ? 'cbi-button cbi-button-remove' : 'cbi-button', 'click': function() { zm.toast(d.quic_blocked ? 'Выключаем блокировку QUIC' : 'Включаем блокировку QUIC', 'warning'); zm.systemToggleQuic().then(function(res) { zm.toast(d.quic_blocked ? 'Блокировка QUIC выключена' : 'Блокировка QUIC включена', 'info'); zm.systemStatus().then(refresh); }); } }, d.quic_blocked ? 'Выключить блокировку QUIC' : 'Включить блокировку QUIC'), E('button', { 'class': 'cbi-button', 'click': function() { zm.toast(d.ipv6_enabled ? 'Выключаем IPv6 в Zapret' : 'Включаем IPv6 в Zapret', 'warning'); zm.systemToggleIpv6().then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(res.ipv6_enabled ? 'IPv6 в Zapret включён' : 'IPv6 в Zapret выключен', 'info'); zm.systemStatus().then(refresh); }); } }, d.ipv6_enabled ? 'Выключить IPv6 в Zapret' : 'Включить IPv6 в Zapret'), E('button', { 'class': 'cbi-button', 'click': function() { zm.toast(d.flow_offloading_fix ? 'Отключаем Fix Flow Offloading' : 'Применяем Fix Flow Offloading', 'warning'); zm.systemToggleFlowOffloadingFix().then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } zm.toast(res.flow_offloading_fix ? 'Fix для Flow Offloading применён' : 'Fix для Flow Offloading отключён', 'info'); zm.systemStatus().then(refresh); }); } }, d.flow_offloading_fix ? 'Отключить Fix Flow Offloading' : 'Применить Fix Flow Offloading') ]) ]); return card; } function refresh(d) { var newCard = renderStatusCard(d); wrap.replaceChild(newCard, wrap.firstChild); } wrap.appendChild(renderStatusCard(sysData)); var netCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Проверка сети'), netEl, E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button', 'click': function() { netEl.textContent = 'Проверяем'; zm.toast('Проверяем IPv4/IPv6', 'warning'); zm.systemCheckConnectivity().then(function(res) { netEl.innerHTML = ''; netEl.appendChild(E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'IPv4 (google.com)'), zm.badge(res.ipv4_ok === true, 'ok, ' + res.ipv4_ms + ' ms', 'недоступен') ])); netEl.appendChild(E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'IPv6 (google.com)'), zm.badge(res.ipv6_ok === true, 'ok, ' + res.ipv6_ms + ' ms', 'недоступен') ])); }); } }, 'Проверить IPv4 / IPv6') ]) ]); wrap.appendChild(netCard); var mirrorGrid = E('div', { 'class': 'zm-grid' }); var mirrorCurrentEl = E('span', {}, mirrorData.current); var mirrorBusy = false; MIRRORS.forEach(function(m) { mirrorGrid.appendChild(E('div', { 'class': 'zm-tile' + (mirrorData.current === m.label ? ' zm-active' : ''), 'click': function() { if (mirrorBusy) { zm.toast('Дождитесь завершения переключения зеркала', 'warning'); return; } if (mirrorData.current === m.label) { zm.toast('Это зеркало уже выбрано', 'info'); return; } mirrorBusy = true; zm.toast('Переключаем зеркало на «' + m.label + '»', 'warning'); mirrorLog.classList.add('zm-show'); zm.renderLog(mirrorLog, '==> Проверяем и переключаем'); zm.mirrorSet(m.id).then(function(res) { if (res.error) { mirrorBusy = false; zm.toast(res.error, 'error'); return; } zm.pollJob('mirror_set', mirrorLog, function(ok) { mirrorBusy = false; zm.toast(ok ? ('Зеркало переключено на «' + m.label + '»') : 'Не удалось переключить зеркало', ok ? 'info' : 'error'); if (!ok) return; zm.mirrorStatus().then(function(res2) { mirrorData.current = res2.current; mirrorCurrentEl.textContent = res2.current; Array.prototype.forEach.call(mirrorGrid.children, function(el, i) { el.classList.toggle('zm-active', MIRRORS[i].label === res2.current); }); }); }); }).catch(function() { mirrorBusy = false; }); } }, m.label)); }); var mirrorLog = E('pre', { 'class': 'zm-log' }); var mirrorCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Зеркало OpenWrt'), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Текущее'), mirrorCurrentEl ]), mirrorGrid, mirrorLog ]); wrap.appendChild(mirrorCard); var uninstallLog = E('pre', { 'class': 'zm-log' }); var uninstallCard = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Удалить Zapret Manager из LuCI'), E('p', { 'class': 'zm-hint' }, 'Уберёт только веб-интерфейс (эту панель) — саму программу-оболочку из LuCI. ' + 'Zapret, Zapret2, DNS over HTTPS и TG WS Proxy, если они были установлены через ' + 'панель, останутся на роутере без изменений и продолжат работать.' ), E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button cbi-button-remove', 'click': function() { if (!confirm('Удалить веб-интерфейс Zapret Manager из LuCI?\n\nСам Zapret и остальные установленные через панель компоненты не пострадают. Действие необратимо — панель придётся ставить заново.')) { return; } uninstallLog.classList.add('zm-show'); zm.renderLog(uninstallLog, '==> Удаляем веб-интерфейс Zapret Manager'); zm.toast('Удаляем Zapret Manager из LuCI', 'warning'); zm.systemUninstallPanel().then(function(res) { if (res.error) { zm.renderLog(uninstallLog, '==> ОШИБКА: ' + res.error); zm.toast(res.error, 'error'); return; } zm.renderLog(uninstallLog, '==> Готово. Панель удалена, страница больше не будет отвечать. Выходим из LuCI'); zm.toast('Zapret Manager удалён из LuCI', 'info'); setTimeout(function() { location.href = L.url('admin/logout'); }, 2500); }).catch(function() { zm.renderLog(uninstallLog, '==> Готово (соединение прервано — это ожидаемо, панель уже удалена). Выходим из LuCI'); setTimeout(function() { location.href = L.url('admin/logout'); }, 2500); }); } }, 'Удалить панель из LuCI') ]), uninstallLog ]); wrap.appendChild(uninstallCard); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/tgproxy.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; var VARIANTS = [ { id: 'mtproto', title: 'MTProto (пакет)', port: 1443, kind: 'proto' }, { id: 'socks5', title: 'SOCKS5', port: 2080, kind: 'socks' }, { id: 'rust', title: 'Rust (MTProto)', port: 2443, kind: 'proto' } ]; function copyToClipboard(text) { if (window.isSecureContext && navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(function() { zm.toast('Ссылка скопирована', 'info'); }).catch(function() { fallbackCopy(text); }); } else { fallbackCopy(text); } } function fallbackCopy(text) { // LuCI обычно открыт по обычному http:// (не https), а Clipboard API работает // только в защищённом контексте — используем старый, но надёжный способ через // скрытое текстовое поле + document.execCommand('copy'), он работает и по http. var ta = document.createElement('textarea'); ta.value = text; ta.setAttribute('readonly', ''); ta.style.position = 'fixed'; ta.style.top = '0'; ta.style.left = '0'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.focus(); ta.select(); ta.setSelectionRange(0, text.length); var ok = false; try { ok = document.execCommand('copy'); } catch (e) { ok = false; } document.body.removeChild(ta); zm.toast(ok ? 'Ссылка скопирована' : 'Не удалось скопировать — выделите ссылку вручную', ok ? 'info' : 'warning'); } return view.extend({ load: function() { zm.injectCss(); return zm.tgStatus(); }, render: function(data) { var view = this; var wrap = E('div', { 'class': 'zm-wrap' }); var cards = E('div', { 'class': 'zm-cards' }); var linksWrap = E('div', {}); var logEl = E('pre', { 'class': 'zm-log' }); function statusOf(d, id) { if (id === 'mtproto') return { installed: d.mtproto === 'installed', running: d.mtproto_running === true, secret: d.secret_mtproto }; if (id === 'socks5') return { installed: d.socks5 === 'installed', running: d.socks5_running === true, secret: '' }; return { installed: d.rust === 'installed', running: d.rust_running === true, secret: d.secret_rust }; } function renderLinks(d) { linksWrap.innerHTML = ''; var any = false; VARIANTS.forEach(function(v) { var st = statusOf(d, v.id); if (!st.installed || !st.running || !d.lan_ip) return; any = true; var link, extra; if (v.kind === 'socks') { link = 'tg://socks?server=' + d.lan_ip + '&port=' + v.port; extra = 'SOCKS5-адрес: ' + d.lan_ip + ':' + v.port; } else { var secretPrefixed = 'dd' + st.secret; link = 'tg://proxy?server=' + d.lan_ip + '&port=' + v.port + '&secret=' + secretPrefixed; extra = 'Хост: ' + d.lan_ip + ' Порт: ' + v.port + ' Ключ: ' + secretPrefixed; } linksWrap.appendChild(E('div', { 'class': 'zm-tg-link-card' }, [ E('h4', {}, v.title), E('p', { 'class': 'zm-tg-link-hint' }, 'Откройте эту ссылку на телефоне/компьютере с Telegram — прокси подключится автоматически. Либо скопируйте и вставьте её в браузер/Telegram вручную.'), E('div', { 'class': 'zm-tg-link-row' }, [ E('div', { 'class': 'zm-tg-link-box' }, link), E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { copyToClipboard(link); } }, 'Скопировать') ]), E('p', { 'class': 'zm-hint' }, extra) ])); }); if (!any) { linksWrap.appendChild(E('p', { 'class': 'zm-hint' }, 'Ссылки появятся здесь после установки и запуска хотя бы одного варианта прокси ниже.')); } } function renderCards(d) { cards.innerHTML = ''; VARIANTS.forEach(function(v) { var st = statusOf(d, v.id); var actions = []; if (st.installed) { actions.push(E('button', { 'class': 'cbi-button cbi-button-remove', 'click': function() { doAction(v.id, 'remove'); } }, 'Удалить')); actions.push(E('button', { 'class': 'cbi-button', 'click': function() { doAction(v.id, 'update'); } }, 'Обновить')); } else { actions.push(E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() { doAction(v.id, 'install'); } }, 'Установить')); } cards.appendChild(E('div', { 'class': 'zm-card' }, [ E('h3', {}, v.title), E('div', { 'class': 'zm-row' }, [ E('span', { 'class': 'zm-label' }, 'Статус'), st.installed ? zm.badge(st.running, 'запущен', 'остановлен') : zm.badge(false, '', 'не установлен') ]), E('div', { 'class': 'zm-actions' }, actions) ])); }); } function doAction(variantId, action) { var job = action === 'remove' ? ('tg_remove_' + (variantId === 'mtproto' ? 'mtproto' : variantId)) : ('tg_install_' + (variantId === 'mtproto' ? 'mtproto' : variantId)); zm.toast((action === 'remove' ? 'Удаляем ' : action === 'update' ? 'Обновляем ' : 'Устанавливаем ') + variantId + '', 'warning'); zm.tgAction(variantId, action).then(function(res) { if (res.error) { zm.toast(res.error, 'error'); return; } if (res.started) { zm.pollJob(job, logEl, function(ok) { zm.toast(ok ? 'Готово' : 'Ошибка', ok ? 'info' : 'error'); zm.tgStatus().then(function(d) { renderCards(d); renderLinks(d); }); }); } }); } renderLinks(data); renderCards(data); wrap.appendChild(linksWrap); wrap.appendChild(cards); wrap.appendChild(logEl); wrap.appendChild(E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Общие действия'), E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button', 'click': function() { zm.toast('Перезапускаем TG WS Proxy', 'warning'); zm.tgRestartAll().then(function() { zm.toast('Все запущенные TG WS Proxy перезапущены', 'info'); }); } }, 'Перезапустить все') ]) ])); return wrap; } }); ZM_INSTALLER_EOF mkdir -p /www/luci-static/resources/view/zapret-manager cat > '/www/luci-static/resources/view/zapret-manager/youtube.js' << 'ZM_INSTALLER_EOF' 'use strict'; 'require view'; 'require zapret-manager.common as zm'; return view.extend({ load: function() { zm.injectCss(); return Promise.all([ zm.status(), zm.strategyListYoutube() ]); }, render: function(data) { var status = data[0], initial = data[1]; var wrap = E('div', { 'class': 'zm-wrap' }); var logEl = E('pre', { 'class': 'zm-log' }); var grid = E('div', { 'class': 'zm-grid' }); var lastList = null; var current = ''; function currentYv(s) { var words = (s.strategy || '').split(' '); for (var i = 0; i < words.length; i++) { if (/^Yv[0-9]+$/.test(words[i])) return words[i]; } return ''; } function renderGrid(res) { lastList = res; grid.innerHTML = ''; (res.items || []).forEach(function(it) { grid.appendChild(E('div', { 'class': 'zm-tile' + (current === it.id ? ' zm-active' : ''), 'click': function() { zm.toast('Применяем стратегию ' + it.id + '', 'warning'); zm.strategySetYoutube(it.id).then(function(r2) { if (!zm.notifyStrategyResult(r2, it.id)) return; refreshState(); }); } }, it.id)); }); if (!res.items || !res.items.length) grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список пуст — нажмите «Обновить список».')); } function refreshState() { zm.status().then(function(s) { current = currentYv(s); if (lastList) renderGrid(lastList); }); } current = currentYv(status); var card = E('div', { 'class': 'zm-card' }, [ E('h3', {}, 'Стратегии для YouTube'), E('div', { 'class': 'zm-actions' }, [ E('button', { 'class': 'cbi-button', 'click': function() { zm.toast('Обновляем список YouTube-стратегий', 'warning'); grid.innerHTML = 'Загрузка списка'; zm.strategyListYoutube().then(function(res) { if (res.started) { zm.pollJob('youtube_download', logEl, function(ok) { zm.toast(ok ? 'Список YouTube-стратегий обновлён' : 'Не удалось обновить список', ok ? 'info' : 'error'); zm.strategyListYoutube().then(renderGrid); }); } else { renderGrid(res); zm.toast('Список YouTube-стратегий обновлён', 'info'); } }); } }, 'Обновить список') ]), grid, E('p', { 'class': 'zm-hint' }, 'Применяется поверх текущей стратегии — заменяет только YouTube-часть.') ]); wrap.appendChild(card); wrap.appendChild(logEl); if (!initial.started) { renderGrid(initial); } else { grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список ещё не загружен — нажмите «Обновить список».')); } return wrap; } }); ZM_INSTALLER_EOF rm -f /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true /etc/init.d/rpcd restart >/dev/null 2>&1 /etc/init.d/uhttpd restart >/dev/null 2>&1 if command -v apk >/dev/null 2>&1; then PM="apk"; INSTALL="apk add" else PM="opkg"; INSTALL="opkg install"; fi command -v curl >/dev/null 2>&1 || $INSTALL curl >/dev/null 2>&1 || true command -v unzip >/dev/null 2>&1 || $INSTALL unzip >/dev/null 2>&1 || true