diff --git a/ZapretManager_LuCI.sh b/ZapretManager_LuCI.sh
index 2e86347..2c6110f 100644
--- a/ZapretManager_LuCI.sh
+++ b/ZapretManager_LuCI.sh
@@ -1,5 +1,5 @@
#!/bin/sh
-# Version: 1.46
+# Version: 1.54
set -e
GREEN="\033[1;32m"; CYAN="\033[1;36m"; YELLOW="\033[1;33m"; MAGENTA="\033[1;35m"; BLUE="\033[0;34m"; NC="\033[0m"; DGRAY="\033[38;5;244m"
@@ -70,7 +70,7 @@ cat > '/opt/zapret-manager-luci/backend.sh' << 'ZM_INSTALLER_EOF'
umask 022
CONF="/etc/config/zapret"
-ZM_VERSION="1.46"
+ZM_VERSION="1.54"
ZM_SCRIPT_URL="https://raw.githubusercontent.com/StressOzz/Zapret-Manager/refs/heads/main/ZapretManager_LuCI.sh"
GH_RAW="https://raw.githubusercontent.com"
GH_MAIN="https://github.com"
@@ -185,6 +185,29 @@ zapret_restart() {
}
+_cpu_stat() { awk '/^cpu / { t = 0; for (i = 2; i <= NF; i++) t += $i; print t, $5 + $6; exit }' /proc/stat 2>/dev/null; }
+
+_cpu_load() {
+ local f="$JOBS_DIR/cpu.stat" now prev t1 i1 t2 i2
+ mkdir -p "$JOBS_DIR"
+ now="$(_cpu_stat)"
+ [ -n "$now" ] || return 0
+ if [ -s "$f" ] && [ -z "$(find "$f" -mmin +1 2>/dev/null)" ]; then
+ prev="$(cat "$f")"
+ else
+ prev="$now"
+ sleep 1
+ now="$(_cpu_stat)"
+ fi
+ set -- $prev $now
+ t1=$1; i1=$2; t2=$3; i2=$4
+ # Замер слишком короткий (панель и боковое меню спросили почти одновременно) — отдаём прошлое значение
+ if [ $((t2 - t1)) -lt 50 ] 2>/dev/null && [ -s "$f.pct" ]; then cat "$f.pct"; return 0; fi
+ [ "$t2" -gt "$t1" ] 2>/dev/null || { echo 0; return 0; }
+ echo "$now" > "$f"
+ echo $(( (100 * ((t2 - t1) - (i2 - i1)) + (t2 - t1) / 2) / (t2 - t1) )) | tee "$f.pct"
+}
+
_cpu_temp() {
local z t typ best="" v
for z in /sys/class/thermal/thermal_zone*; do
@@ -208,41 +231,50 @@ _cpu_temp() {
else echo "$best"; fi
}
+# Есть ли интернет: пинг 1.1.1.1, затем 8.8.8.8 и 77.88.8.8. Пинг идёт в фоне,
+# результат («время ok|fail мс») живёт 10 секунд — панель не ждёт сеть.
+_inet_ping() {
+ local h out ms
+ for h in 1.1.1.1 8.8.8.8 77.88.8.8; do
+ out=$(ping -c 1 -W 2 "$h" 2>/dev/null)
+ ms=$(echo "$out" | sed -n 's/.*time=\([0-9.]*\).*/\1/p' | head -n1)
+ [ -n "$ms" ] && { echo "$(date +%s) ok ${ms%%.*}"; return 0; }
+ done
+ echo "$(date +%s) fail"
+}
+_inet_state() { # печатает «ok|fail мс» или пусто, пока не проверяли
+ local f="$ZM_STATE_DIR/inet" t r ms age
+ read -r t r ms 2>/dev/null < "$f"
+ age=$(( $(date +%s) - ${t:-0} ))
+ if [ "$age" -ge 10 ] || [ "$age" -lt 0 ]; then
+ mkdir -p "$ZM_STATE_DIR"
+ if mkdir "$f.lock" 2>/dev/null; then
+ ( _inet_ping > "$f.new" 2>/dev/null && mv -f "$f.new" "$f"; rmdir "$f.lock" ) >/dev/null 2>&1 &
+ elif [ -n "$(find "$f.lock" -mmin +1 2>/dev/null)" ]; then
+ rmdir "$f.lock" 2>/dev/null
+ fi
+ fi
+ [ -n "$r" ] && [ "$age" -lt 120 ] && echo "$r $ms"
+}
+
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)
+ local target
+ target=$(grep '^DISTRIB_TARGET=' /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","cpu_temp":"%s"}\n' \
- "$(esc "$model")" "$(esc "$arch")" "$(esc "$owrt")" \
- "$(esc "$tmp_used")" "$(esc "$tmp_free")" "$(esc "$root_used")" "$(esc "$root_free")" "$(_cpu_temp)"
-}
-
-_v_modified() {
- local want have
- want=$(strategy_"$1" | sed '1d')
- have=$(sed -n "/^[[:space:]]*option NFQWS_OPT '\$/,/^[[:space:]]*'\$/p" "$CONF" | tr -d '\r' | awk -v m="#$1" '
- { l = $0; sub(/[ \t]+$/, "", l) }
- l == m { f = 1; next }
- f && (l == "--new" || l ~ /^[ \t]*#/ || l ~ /^[ \t]*\047/) { exit }
- f { print l }')
- [ "$want" != "$have" ]
-}
-
-_strat_verify() {
- local w out=""
- for w in $1; do
- case "$w" in
- v[1-9]|v10) _v_modified "$w" && w="$w (изменена)" ;;
- esac
- out="$out${out:+ }$w"
- done
- printf '%s' "$out"
+ local inet inet_ms
+ set -- $(_inet_state)
+ inet="$1"; inet_ms="$2"
+ printf '{"model":"%s","arch":"%s","openwrt":"%s","target":"%s","hostname":"%s","kernel":"%s","tmp_used":"%s","tmp_free":"%s","root_used":"%s","root_free":"%s","cpu_temp":"%s","cpu_load":"%s","inet":"%s","inet_ms":"%s"}\n' \
+ "$(esc "$model")" "$(esc "$arch")" "$(esc "$owrt")" "$(esc "$target")" "$(esc "$(cat /proc/sys/kernel/hostname 2>/dev/null)")" "$(esc "$(uname -r 2>/dev/null)")" \
+ "$(esc "$tmp_used")" "$(esc "$tmp_free")" "$(esc "$root_used")" "$(esc "$root_free")" "$(_cpu_temp)" "$(_cpu_load)" "$inet" "$inet_ms"
}
status() {
@@ -265,17 +297,16 @@ status() {
local strat="" fs_marker=""
if [ -f "$CONF" ]; then
- strat=$(sed -n "/^[[:space:]]*option NFQWS_OPT '\$/,/^[[:space:]]*'\$/p" "$CONF" | grep '^#' | sed 's/^#//; s/[[:space:]]*$//' | tr '\n' ' ' | sed 's/ $//')
+ strat=$(sed -n "/^[[:space:]]*option NFQWS_OPT '\$/,/^[[:space:]]*'\$/p" "$CONF" | grep '^#' | sed 's/^#[[:space:]]*//; s/[[:space:]]*$//; /^udp443$/d' | tr '\n' ' ' | sed 's/ $//')
+ _udp443_on && strat="${strat:+$strat }QUIC"
fs_marker=$(grep -m1 '^# ZMFS:' "$CONF" | sed 's/^# ZMFS://')
- strat=$(_strat_verify "$strat")
- if [ -z "$strat" ] && [ -z "$fs_marker" ] && [ -n "$(sed -n "/^[[:space:]]*option NFQWS_OPT '\$/,/^[[:space:]]*'\$/p" "$CONF" | sed '1d;$d' | grep -v '^[[:space:]]*$')" ]; then
- strat="своя (без метки)"
- fi
+ sed -n "/^[[:space:]]*option NFQWS_OPT '\$/,/^[[:space:]]*'\$/p" "$CONF" | grep -qi '^#[[:space:]]*customstart' && strat="Custom"
fi
- printf '{"pkg":"%s","zapret":"%s","zapret_running":%s,"zapret_version":"%s","zapret2":"%s","zapret2_running":%s,"strategy":"%s","flowseal":"%s","yv_off":%s}\n' \
+ printf '{"pkg":"%s","zapret":"%s","zapret_running":%s,"zapret_version":"%s","zapret2":"%s","zapret2_running":%s,"strategy":"%s","flowseal":"%s","yv_off":%s,"quic_yt":%s}\n' \
"$PKG" "$zr" "$zr_running" "$(esc "$zr_ver")" "$zr2" "$zr2_running" "$(esc "$strat")" "$(esc "$fs_marker")" \
- "$([ -f /opt/zapret-manager-luci/yv_off ] && echo true || echo false)"
+ "$([ -f /opt/zapret-manager-luci/yv_off ] && echo true || echo false)" \
+ "$([ -f "$CONF" ] && _udp443_on && echo true || echo false)"
}
@@ -557,26 +588,35 @@ DV_OFF_FLAG="/opt/zapret-manager-luci/dv_off"
DISCORD_RX='^--filter-l7=discord,stun$|^--filter-udp=19294-19344,50000-50100$|^--filter-tcp=2053,2083,2087,2096,8443$|^--hostlist-domains=discord[.]media$|^#[ \t]*Dv[0-9]+$'
YV_RX='^#[ \t]*Yv[0-9]+$|^--hostlist=/opt/zapret/ipset/zapret-hosts-google[.]txt$'
-NFQ_MARK_RX='^[ \t]*#[ \t]*(Yv|Dv|Gv)[0-9]'
+NFQ_MARK_RX='^[ \t]*#[ \t]*((Yv|Dv|Gv)[0-9]|udp443[ \t]*$)'
+UDP443_MARK_RX='^[ \t]*#[ \t]*udp443[ \t]*$'
_nfq_drop_profiles() {
- local rx="$1" force="$2" old="$JOBS_DIR/nfq_drop_old" new="$JOBS_DIR/nfq_drop_new" rc
+ local rx="$1" force="$2" keep="$3" old="$JOBS_DIR/nfq_drop_old" new="$JOBS_DIR/nfq_drop_new" rc
mkdir -p "$JOBS_DIR"
awk "/^[[:space:]]*option NFQWS_OPT '\$/ { f = 1 } f" "$CONF" > "$old"
[ -s "$old" ] || { rm -f "$old"; return 1; }
grep -q "^[[:space:]]*'[[:space:]]*\$" "$old" || { rm -f "$old"; return 1; }
- awk -v RX="$rx" -v MK="$NFQ_MARK_RX" -v q="'" '
+ awk -v RX="$rx" -v KP="$keep" -v MK="$NFQ_MARK_RX" -v UQ="$UDP443_MARK_RX" -v q="'" '
function emit(l) { o[++no] = l }
- function flush( i, drop) {
+ function flush( i, drop, real, udp) {
if (n == 0 && np == 0) return
- drop = 0
+ drop = 0; real = 0; udp = 0
for (i = 1; i <= np; i++) if (pc[i] ~ RX) drop = 1
- for (i = 1; i <= n; i++) if (b[i] ~ RX) drop = 1
+ for (i = 1; i <= n; i++) {
+ if (b[i] ~ RX) drop = 1
+ if (b[i] !~ /^[ \t]*#/) real++
+ if (b[i] == "--filter-udp=443") udp = 1
+ }
+ if (drop && KP != "") {
+ for (i = 1; i <= np; i++) if (pc[i] ~ KP) drop = 0
+ for (i = 1; i <= n; i++) if (b[i] ~ KP) drop = 0
+ }
if (drop) removed = 1
- else if (n > 0) {
- for (i = 1; i <= np; i++) emit(pc[i])
+ else if (real > 0) {
+ for (i = 1; i <= np; i++) if (udp || pc[i] !~ UQ) emit(pc[i])
if (printed) emit("--new")
- for (i = 1; i <= n; i++) emit(b[i])
+ for (i = 1; i <= n; i++) if (udp || b[i] !~ UQ) emit(b[i])
printed = 1
}
n = 0; np = 0
@@ -616,6 +656,36 @@ _nfq_drop_profiles() {
_nfq_normalize() { _nfq_drop_profiles '^#ZM_NEVER_MATCHES$' force; return 0; }
+# Блок QUIC (UDP 443) для YouTube — тот же, что в консольном Zapret Manager («#udp443»)
+_nfq_opt_body() { sed -n "/^[[:space:]]*option NFQWS_OPT '\$/,/^[[:space:]]*'\$/p" "$CONF" 2>/dev/null; }
+_udp443_on() { _nfq_opt_body | grep -qx -- '--filter-udp=443'; }
+_udp443_add() {
+ _udp443_on && return 0
+ sed -i '/^[[:space:]]*#[[:space:]]*udp443[[:space:]]*$/d' "$CONF"
+ sed -i "/^[[:space:]]*option NFQWS_OPT '/a\\#udp443\\n--filter-udp=443\\n--hostlist=/opt/zapret/ipset/zapret-hosts-google.txt\\n--dpi-desync=fake\\n--dpi-desync-repeats=11\\n--dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin\\n--new" "$CONF"
+ _add_ports_if_missing NFQWS_PORTS_UDP 443
+}
+
+youtube_quic_set() {
+ local mode="$1"
+ [ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; }
+ case "$mode" in
+ on)
+ _udp443_on && { printf '{"ok":true,"quic":true}\n'; return 0; }
+ _add_gp_domains
+ _udp443_add
+ _nfq_normalize ;;
+ off)
+ _udp443_on || { sed -i '/^[[:space:]]*#[[:space:]]*udp443[[:space:]]*$/d' "$CONF"; printf '{"ok":true,"quic":false}\n'; return 0; }
+ _nfq_drop_profiles '^--filter-udp=443$'
+ case $? in 0|3) ;; *) echo '{"error":"блок стратегий Zapret (NFQWS_OPT) записан не так, как его пишет панель, — блок QUIC не тронут"}'; return 1 ;; esac
+ sed -i '/^[[:space:]]*#[[:space:]]*udp443[[:space:]]*$/d' "$CONF" ;;
+ *) echo '{"error":"неизвестное действие"}'; return 1 ;;
+ esac
+ zapret_restart
+ printf '{"ok":true,"quic":%s}\n' "$(_udp443_on && echo true || echo false)"
+}
+
_add_yv_default() {
[ -f "$YV_OFF_FLAG" ] && return 0
if ! grep -q "^#Yv" "$CONF" && ! grep -q "^#general" "$CONF"; then
@@ -659,9 +729,10 @@ strategy_list_v() {
}
strategy_set_v() {
- local version="$1" yv dv gv xt
+ local version="$1" yv dv gv xt q
echo "$version" | grep -qE '^v([1-9]|10)$' || { echo '{"error":"некорректная версия"}'; return 1; }
[ -f "$CONF" ] || { echo '{"error":"Zapret не установлен"}'; return 1; }
+ _udp443_on && q=1
yv=$(grep -oE '^#[[:space:]]*Yv[0-9]+$' "$CONF" | head -n1 | sed 's/^#[[:space:]]*//')
dv=$(grep -oE '^#[[:space:]]*Dv[0-9]+$' "$CONF" | head -n1 | sed 's/^#[[:space:]]*Dv//')
gv=$(grep -oE '^#Gv[1-4](Xtreme)?$' "$CONF" | head -n1 | sed 's/^#Gv//; s/Xtreme$//')
@@ -677,6 +748,9 @@ strategy_set_v() {
_add_yv_default
_discord_str_add
ZM_NO_RESTART=1
+ if [ -n "$yv" ] && [ "$yv" != Yv08 ] && [ ! -f "$YV_OFF_FLAG" ] && [ ! -s "$(_yv_file)" ]; then
+ do_yv_download >/dev/null 2>&1
+ fi
if [ -n "$yv" ] && [ "$yv" != Yv08 ] && [ ! -f "$YV_OFF_FLAG" ] && [ -s "$(_yv_file)" ] && grep -qxF "#$yv" "$(_yv_file)"; then
strategy_set_youtube "$yv" >/dev/null
fi
@@ -685,6 +759,7 @@ strategy_set_v() {
game_set "$gv" >/dev/null
[ -n "$xt" ] && game_toggle_xtreme >/dev/null
fi
+ [ -n "$q" ] && _udp443_add
ZM_NO_RESTART=""
_nfq_normalize
zapret_restart
@@ -785,7 +860,7 @@ do_flowseal_download() {
strategy_list_flowseal() {
local f; f="$(_flowseal_file)"
- if [ ! -s "$f" ]; then
+ if [ ! -s "$f" ] || [ "$1" = refresh ]; then
job_start flowseal_download do_flowseal_download
return
fi
@@ -798,12 +873,13 @@ strategy_list_flowseal() {
}
strategy_set_flowseal() {
- local name="$1" f block
+ local name="$1" f block q
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; }
+ _udp443_on && q=1
rm -f "$YV_OFF_FLAG" "$DV_OFF_FLAG"
_game_xtreme_undo_opts
_remove_ports_if_present NFQWS_PORTS_UDP "$PORTS_UDP"
@@ -825,6 +901,7 @@ strategy_set_flowseal() {
_add_ports_if_missing NFQWS_PORTS_UDP "$PORTS_UDP"
_add_ports_if_missing NFQWS_PORTS_TCP "$PORTS_TCP"
fi
+ [ -n "$q" ] && _udp443_add
zapret_restart
printf '{"ok":true,"strategy":"%s","ts_warning":%s}\n' "$(esc "$name")" "$(_ts_warning)"
}
@@ -842,7 +919,7 @@ do_yv_download() {
strategy_list_youtube() {
local f; f="$(_yv_file)"
- if [ ! -s "$f" ]; then
+ if [ ! -s "$f" ] || [ "$1" = refresh ]; then
job_start youtube_download do_yv_download
return
fi
@@ -853,7 +930,7 @@ strategy_list_youtube() {
)"
}
-_yv_block_remove() { _nfq_drop_profiles "$YV_RX"; }
+_yv_block_remove() { _nfq_drop_profiles "$YV_RX" "" '^--filter-udp=443$'; }
strategy_set_youtube() {
local name="$1" f selected
@@ -3355,7 +3432,8 @@ health() {
sr=2
if /etc/init.d/steer running >/dev/null 2>&1; then
if [ "$(_st_exit)" = vpn ]; then
- [ -d "/sys/class/net/$ST_VPN_OUT" ] && sr=1
+ # интерфейс поднят, но трафик не идёт — это поломка
+ [ -d "/sys/class/net/$ST_VPN_OUT" ] && { _st_vpn_live; [ $? -ne 1 ] && sr=1; }
else
for w in $(awk '{print $1}' /etc/zm-steer/warp.up 2>/dev/null) zmwarp; do
[ -d "/sys/class/net/$w" ] && { sr=1; break; }
@@ -3441,8 +3519,10 @@ do_versions_refresh() {
versions_status() { # [refresh]
local busy=false
- if [ "$1" = refresh ] || [ ! -s "$VERSIONS_CACHE" ] || [ -n "$(find "$VERSIONS_CACHE" -mmin +180 2>/dev/null)" ]; then
- _job_running versions || job_start versions do_versions_refresh >/dev/null
+ local age=180
+ [ "$1" = auto ] && age=2
+ if [ "$1" = refresh ] || [ ! -s "$VERSIONS_CACHE" ] || [ -n "$(find "$VERSIONS_CACHE" -mmin +$age 2>/dev/null)" ]; then
+ _job_running versions || ZM_VER_FORCE=1 job_start versions do_versions_refresh >/dev/null
fi
_job_running versions && busy=true
if [ -s "$VERSIONS_CACHE" ]; then
@@ -5095,6 +5175,10 @@ ST_AWG_MIRROR_FLAT="https://gitlab.com/xyzmean/brb/-/raw/main/deps/awg"
ST_CRON_TAG="# zm-steer"
ST_CRON_CMD="/etc/init.d/steer enabled && { for i in zmwarp zmwarp2 zmwarp3; do ifup \$i; done; sleep 15; /etc/init.d/steer restart; }"
ST_WARP_N=3
+ST_WARP_MAX=3 # столько туннелей бывает в автоматическом режиме
+ST_WARP_MODE="$ST_DIR/warp.mode" # own — «Свой конфиг»: один туннель zmwarp по конфигу пользователя
+ST_WARP_OWN="$ST_DIR/warp.own.conf" # копия своего конфига — вернуться к нему можно в один клик
+[ "$(cat "$ST_WARP_MODE" 2>/dev/null)" = own ] && ST_WARP_N=1
ST_WARP_UP="$ST_DIR/warp.up" # поднятые туннели «интерфейс колония», лучший первым (_st_warp_order)
ST_RU_COLOS="DME SVX LED KJA REN OVB KZN AER VVO"
ST_DEFAULT_SEL=""
@@ -5106,6 +5190,7 @@ _st_owns() { grep -qxF "$1" "$ST_OWNED" 2>/dev/null; }
_st_running() { _job_alive steer; }
_st_installed() { command -v steer >/dev/null 2>&1 && { _st_owns "engine" || _st_owns "pkg steer" || _st_owns "net $ST_WARP_IF"; }; }
_st_warp_on() { _st_owns "net $ST_WARP_IF" && [ -s "$ST_WARP_CONF" ]; }
+_st_warp_own() { [ "$(cat "$ST_WARP_MODE" 2>/dev/null)" = own ]; }
_st_ready() { _st_installed && [ ! -f "$ST_OFF" ] && [ -z "$(_st_blocker)" ]; }
_st_sel() { [ -s "$ST_SEL" ] || return 0; local id; for id in $(cat "$ST_SEL"); do _rb_routable "$id" || continue; [ "$id" = custom ] && [ ! -s "$ST_USER_DIR/custom.lst" ] && continue; echo "$id"; done; }
@@ -5181,7 +5266,7 @@ _st_ver_lt() { # A B
_st_latest_ver() {
local c="$ZM_STATE_DIR/steer.latest" v="" m
- if [ -s "$c" ] && [ -z "$(find "$c" -mmin +360 2>/dev/null)" ]; then cat "$c"; return 0; fi
+ if [ -z "$ZM_VER_FORCE" ] && [ -s "$c" ] && [ -z "$(find "$c" -mmin +360 2>/dev/null)" ]; then cat "$c"; return 0; fi
v="$(curl -Ls --connect-timeout 5 --max-time 12 -o /dev/null -w '%{url_effective}' https://github.com/xyzmean/steer/releases/latest 2>/dev/null |
grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | tail -n1)"
if ! echo "$v" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
@@ -5507,6 +5592,7 @@ _st_with() { # N КОМАНДА... — выполнить команду в ко
return $rc
}
_st_wifs_all() { local n=1; while [ "$n" -le "$ST_WARP_N" ]; do _st_wif "$n"; n=$((n + 1)); done; }
+_st_wifs_max() { local n=1; while [ "$n" -le "$ST_WARP_MAX" ]; do _st_wif "$n"; n=$((n + 1)); done; }
_st_warp_first() {
local i
[ -s "$ST_WARP_UP" ] && while read -r i _; do [ -d "/sys/class/net/$i" ] && { echo "$i"; return 0; }; done < "$ST_WARP_UP"
@@ -5642,8 +5728,108 @@ _st_warp_order() { # ФАЙЛ «интерфейс колония ключ|-» -
rm -f "$f.key"
}
+# ── «Свой конфиг»: один туннель zmwarp ровно по конфигу пользователя ──
+
+_st_warp_own_iface() { # ФАЙЛ — интерфейс zmwarp из конфига: ключи, адреса, маскировка и точка входа как есть
+ local f="$1" kv="$ST_RUN/own.kv" priv pub psk addr a ep host port keep mtu k v i="$ST_WARP_IF"
+ mkdir -p "$ST_RUN"
+ _awg_conf_kv "$f" > "$kv"
+ priv="$(_awg_cv "$kv" interface privatekey)"; pub="$(_awg_cv "$kv" peer publickey)"
+ addr="$(_awg_cv "$kv" interface address)"; ep="$(_awg_cv "$kv" peer endpoint)"
+ psk="$(_awg_cv "$kv" peer presharedkey)"; keep="$(_awg_cv "$kv" peer persistentkeepalive)"; mtu="$(_awg_cv "$kv" interface mtu)"
+ case "$ep" in
+ \[*\]:*) host="${ep%]:*}"; host="${host#[}"; port="${ep##*]:}" ;;
+ ?*:*) host="${ep%:*}"; port="${ep##*:}" ;;
+ esac
+ if [ -z "$priv" ] || [ -z "$pub" ] || [ -z "$addr" ] || [ -z "$host" ] || [ -z "$port" ]; then
+ rm -f "$kv"
+ echo "ОШИБКА: в конфиге нет PrivateKey, Address, PublicKey или Endpoint (адрес:порт)"
+ return 1
+ fi
+ uci -q delete "network.$i"
+ uci -q delete "network.${i}_peer"
+ uci set "network.$i=interface"
+ uci set "network.$i.proto=amneziawg"
+ uci set "network.$i.private_key=$priv"
+ for a in $(echo "$addr" | tr ',' ' '); do
+ case "$a" in */*) ;; *:*) a="$a/128" ;; *) a="$a/32" ;; esac
+ uci add_list "network.$i.addresses=$a"
+ done
+ uci set "network.$i.mtu=${mtu:-1280}"
+ for k in jc jmin jmax s1 s2 s3 s4 h1 h2 h3 h4 i1 i2 i3 i4 i5 j1 j2 j3 itime; do
+ v="$(_awg_cv "$kv" interface "$k")"
+ [ -n "$v" ] && uci set "network.$i.awg_$k=$v"
+ done
+ uci set "network.${i}_peer=amneziawg_$i"
+ uci set "network.${i}_peer.description=Свой WARP"
+ uci set "network.${i}_peer.public_key=$pub"
+ [ -n "$psk" ] && uci set "network.${i}_peer.preshared_key=$psk"
+ uci add_list "network.${i}_peer.allowed_ips=0.0.0.0/0"
+ case "$addr" in *:*) uci add_list "network.${i}_peer.allowed_ips=::/0" ;; esac
+ uci set "network.${i}_peer.route_allowed_ips=0"
+ uci set "network.${i}_peer.persistent_keepalive=${keep:-25}"
+ uci set "network.${i}_peer.endpoint_host=$host"
+ uci set "network.${i}_peer.endpoint_port=$port"
+ uci commit network
+ _st_own "net $i"
+ rm -f "$kv"
+}
+
+_st_warp_drop_extra() { # убрать туннели zmwarp2…: в режиме «Свой конфиг» туннель один
+ local n=2 i changed=0
+ while [ "$n" -le "$ST_WARP_MAX" ]; do
+ i="$(_st_wif "$n")"
+ if _st_owns "net $i"; then
+ ifdown "$i" >/dev/null 2>&1
+ uci -q delete "network.$i"
+ uci -q delete "network.${i}_peer"
+ sed -i "/^net $i\$/d" "$ST_OWNED"
+ changed=1
+ fi
+ rm -f "$(_st_wconf "$n")"
+ n=$((n + 1))
+ done
+ [ "$changed" = 1 ] && uci commit network
+ return 0
+}
+
+_st_warp_own_up() { # поднять свой туннель и дождаться связи — без разведки и без новых ключей
+ local i="$ST_WARP_IF" w=0 col
+ _st_install_awg || return 1
+ _st_awg_loaded || modprobe amneziawg >/dev/null 2>&1
+ if [ "$(uci -q get "network.$i.proto")" != amneziawg ]; then
+ [ -s "$ST_WARP_CONF" ] || { echo "ОШИБКА: своего конфига WARP нет — вставьте его на вкладке WARP"; return 1; }
+ _st_warp_own_iface "$ST_WARP_CONF" || return 1
+ ubus call network reload >/dev/null 2>&1
+ sleep 2
+ fi
+ _st_warp_zone
+ uci -q get "network.$i.auto" >/dev/null && { uci -q delete "network.$i.auto"; uci -q commit network; }
+ _rb_say "Поднимаем свой туннель WARP"
+ ubus call "network.interface.$i" up >/dev/null 2>&1
+ while [ "$w" -lt 25 ]; do
+ _st_stopped && return 1
+ _st_warp_alive_if "$i" && break
+ ping -I "$i" -c 1 -W 1 1.1.1.1 >/dev/null 2>&1
+ w=$((w + 1))
+ done
+ if ! _st_warp_alive_if "$i"; then
+ rm -f "$ST_WARP_UP" "$ST_DIR/warp.colo"
+ echo "ОШИБКА: сервер из вашего конфига не отвечает — проверьте ключи и Endpoint"
+ return 1
+ fi
+ col="$(_st_colo_of "$i")"
+ echo "$i ${col:-?}" > "$ST_WARP_UP"
+ if [ -n "$col" ]; then echo "$col" > "$ST_DIR/warp.colo"; else rm -f "$ST_DIR/warp.colo"; fi
+ _st_tgws_warp
+ _rb_say "Свой WARP работает: $(uci -q get "network.${i}_peer.endpoint_host"):$(uci -q get "network.${i}_peer.endpoint_port")${col:+, колония $col}"
+ [ -n "$col" ] && _st_warp_is_ru "$col" && _rb_warn "Туннель идёт через российскую колонию $col: заблокированное через неё не откроется"
+ return 0
+}
+
_st_warp_up() { # [repick]
local repick="$1" n i peer got busy="" busyip="" ru=0 c ready="" w need=0 colos="" alt p
+ _st_warp_own && { _st_warp_own_up; return; }
_st_install_awg || return 1
[ "$repick" = repick ] && rm -f "$ST_DIR/warp.ports"
_rb_say "Поднимаем туннели WARP в разных колониях"
@@ -6055,6 +6241,10 @@ _st_selfcheck() {
trace="$(curl -s --interface "$(_st_warp_first)" --connect-timeout 5 --max-time 10 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null)"
case "$trace" in
*warp=on*|*warp=plus*) echo "[ OK ] Туннель WARP: трафик идёт через Cloudflare ($(echo "$trace" | sed -n 's/^colo=//p'))" ;;
+ *ip=*)
+ # свой конфиг может вести не в WARP, а на свой сервер — тогда важно лишь, что трафик идёт
+ if _st_warp_own; then echo "[ OK ] Свой туннель: трафик идёт (выход $(echo "$trace" | sed -n 's/^ip=//p'))"
+ else echo "[FAIL] Туннель WARP: трафик идёт мимо Cloudflare WARP"; bad=1; fi ;;
*) echo "[FAIL] Туннель WARP: трафик через него не идёт"; bad=1 ;;
esac
/etc/init.d/steer running >/dev/null 2>&1 && echo "[ OK ] Служба Steer запущена" || { echo "[FAIL] Служба Steer не запущена"; bad=1; }
@@ -6134,8 +6324,74 @@ do_steer_warp_setup() {
_rb_say "Готово, WARP подключён"
}
+do_steer_warp_own() { # подключить свой конфиг (из warp.pending или сохранённый)
+ _st_phase awg
+ rm -f "$ST_STOP_FLAG"
+ _st_installed || { echo "ОШИБКА: сначала установите Steer"; return 1; }
+ [ -n "$(_st_blocker)" ] && { echo "ОШИБКА: движок Steer сейчас настраивает не Zapret Manager"; return 1; }
+ if [ -s "$ST_DIR/warp.pending" ]; then mv -f "$ST_DIR/warp.pending" "$ST_WARP_OWN"; fi
+ [ -s "$ST_WARP_OWN" ] || { echo "ОШИБКА: вставьте конфиг WARP"; return 1; }
+ chmod 600 "$ST_WARP_OWN"
+ _ensure_deps
+ _st_install_awg || return 1
+ _st_phase tunnel
+ _rb_say "Свой WARP: один туннель по вашему конфигу"
+ _st_owns "net $(_st_wif 2)" && _rb_say "Убираем автоматические туннели WARP 2 и 3"
+ echo own > "$ST_WARP_MODE"
+ ST_WARP_N=1
+ _st_warp_drop_extra
+ cp "$ST_WARP_OWN" "$ST_WARP_CONF"
+ chmod 600 "$ST_WARP_CONF"
+ rm -f "$ST_DIR/warp.ports"
+ _st_awg_loaded || modprobe amneziawg >/dev/null 2>&1
+ _st_warp_own_iface "$ST_WARP_CONF" || return 1
+ _st_warp_zone
+ ubus call network reload >/dev/null 2>&1
+ sleep 2
+ _st_warp_own_up || return 1
+ [ "$(cat "$ST_EXIT" 2>/dev/null)" = vpn ] && [ -s "$ST_SUB" ] || echo warp > "$ST_EXIT"
+ if [ ! -f "$ST_OFF" ] && [ -n "$(_st_sel)" ]; then
+ _st_phase rules
+ _st_apply tunnel_ready || return 1
+ _st_phase check
+ sleep 2
+ _st_selfcheck
+ fi
+ _rb_say "Готово, свой WARP подключён"
+}
+
+do_steer_warp_auto() { # вернуться к автоматическому режиму: ключи от Cloudflare, три туннеля
+ _st_phase awg
+ rm -f "$ST_STOP_FLAG"
+ _st_installed || { echo "ОШИБКА: сначала установите Steer"; return 1; }
+ [ -n "$(_st_blocker)" ] && { echo "ОШИБКА: движок Steer сейчас настраивает не Zapret Manager"; return 1; }
+ _ensure_deps
+ _st_install_awg || return 1
+ _st_phase tunnel
+ _rb_say "Автоматический WARP: роутер получит ключи у Cloudflare и подберёт три туннеля"
+ [ -s "$ST_WARP_OWN" ] && _rb_say "Ваш конфиг сохранён — вернуться к нему можно в один клик"
+ rm -f "$ST_WARP_MODE"
+ ST_WARP_N="$ST_WARP_MAX"
+ ifdown "$ST_WARP_IF" >/dev/null 2>&1
+ uci -q delete "network.$ST_WARP_IF"
+ uci -q delete "network.${ST_WARP_IF}_peer"
+ uci commit network
+ rm -f "$ST_WARP_CONF" "$ST_DIR/warp.ports" "$ST_WARP_UP" "$ST_DIR/warp.colo"
+ _st_warp_up || return 1
+ [ "$(cat "$ST_EXIT" 2>/dev/null)" = vpn ] && [ -s "$ST_SUB" ] || echo warp > "$ST_EXIT"
+ if [ ! -f "$ST_OFF" ] && [ -n "$(_st_sel)" ]; then
+ _st_phase rules
+ _st_apply tunnel_ready || return 1
+ _st_phase check
+ sleep 2
+ _st_selfcheck
+ fi
+ _rb_say "Готово, автоматический WARP подключён"
+}
+
_st_warp_fix() { # N [keys]
local n="$1" i c peer got busy="" busyip="" w=0 host port x col
+ _st_warp_own && { echo "ОШИБКА: у Steer свой конфиг WARP — замените его на вкладке WARP страницы Steer"; return 1; }
i="$(_st_wif "$n")"; c="$(_st_wconf "$n")"
_st_awg_loaded || modprobe amneziawg >/dev/null 2>&1
if [ "$2" = keys ] || [ ! -s "$c" ] || [ "$(uci -q get "network.$i.proto")" != amneziawg ]; then
@@ -6286,7 +6542,7 @@ do_steer_remove() {
/etc/init.d/steer stop >/dev/null 2>&1
fi
local wi netrl=0
- for wi in $(_st_wifs_all); do
+ for wi in $(_st_wifs_max); do
_st_owns "net $wi" || continue
ifdown "$wi" >/dev/null 2>&1
uci -q delete "network.$wi"
@@ -6374,10 +6630,14 @@ steer_status() {
elif command -v steer >/dev/null 2>&1; then ( _st_latest_ver >/dev/null 2>&1 & ); fi
[ -d "/sys/class/net/$ST_VPN_OUT" ] && vup=true
[ -s "$ST_SUB" ] && vsub=true
- printf '{"running":%s,"phase":"%s","blocker":"%s","installed":%s,"stopped":%s,"version":"%s","steer_running":%s,"channels":%s,"warp_up":%s,"warp_colo":"%s","warp_host":"%s","warp_port":"%s","warp_hs_age":"%s","warp_rx":%s,"warp_tx":%s,"autorestart":"%s","dns_conflict":%s,"exit":"%s","vpn_up":%s,"has_sub":%s,"sub_label":"%s","latest":"%s","ext":%s,"warp_on":%s,"tunnels":%s,"services":[%s]}\n' \
+ local vlive=null
+ if [ "$vup" = true ] && [ "$vexit" = vpn ] && [ "$off" = false ]; then
+ _st_vpn_live; case $? in 0) vlive=true ;; 1) vlive=false ;; esac
+ fi
+ printf '{"running":%s,"phase":"%s","blocker":"%s","installed":%s,"stopped":%s,"version":"%s","steer_running":%s,"channels":%s,"warp_up":%s,"warp_colo":"%s","warp_host":"%s","warp_port":"%s","warp_hs_age":"%s","warp_rx":%s,"warp_tx":%s,"autorestart":"%s","dns_conflict":%s,"exit":"%s","vpn_up":%s,"vpn_live":%s,"has_sub":%s,"sub_label":"%s","latest":"%s","ext":%s,"warp_on":%s,"warp_mode":"%s","warp_own_saved":%s,"tunnels":%s,"services":[%s]}\n' \
"$running" "$(esc "$phase")" "$blk" "$installed" "$off" "$(esc "$ver")" "$run" "${chans:-0}" "$warp_up" "$(esc "$colo")" \
"$(esc "$host")" "$(esc "$port")" "$age" "${rx:-0}" "${tx:-0}" "$(_st_cron_get)" "$dns" \
- "$vexit" "$vup" "$vsub" "$(esc "$(_st_sub_label)")" "$(esc "$latest")" "$ext" "$won" "$(_st_tunnels_json)" "$svc"
+ "$vexit" "$vup" "$vlive" "$vsub" "$(esc "$(_st_sub_label)")" "$(esc "$latest")" "$ext" "$won" "$(_st_warp_own && echo own || echo auto)" "$([ -s "$ST_WARP_OWN" ] && echo true || echo false)" "$(_st_tunnels_json)" "$svc"
}
_st_tunnels_json() {
@@ -6511,6 +6771,36 @@ _st_exit() {
}
_st_use_vpn() { [ "$(_st_exit)" = vpn ]; }
+# Живой ли VPN на деле: интерфейс может быть поднят, а трафик не идти.
+# Проверка (запрос к Cloudflare через туннель) идёт в фоне и кешируется на 2 минуты.
+ST_VPN_PROBE="$ZM_STATE_DIR/steer.vpnprobe"
+_st_vpn_probe() { # синхронно: пишет «время ok|fail ip loc»
+ local trace ip="" loc="" r=fail
+ if [ -d "/sys/class/net/$ST_VPN_OUT" ]; then
+ trace="$(curl -s --interface "$ST_VPN_OUT" --connect-timeout 5 --max-time 10 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null)"
+ ip="$(echo "$trace" | sed -n 's/^ip=//p')"; loc="$(echo "$trace" | sed -n 's/^loc=//p')"
+ [ -n "$ip" ] && r=ok
+ fi
+ mkdir -p "$ZM_STATE_DIR"
+ echo "$(date +%s) $r $ip $loc" > "$ST_VPN_PROBE"
+ [ "$r" = ok ]
+}
+_st_vpn_live() { # 0 — трафик идёт, 1 — не идёт, 2 — ещё не проверяли
+ local t r age
+ read -r t r _ 2>/dev/null < "$ST_VPN_PROBE"
+ age=$(( $(date +%s) - ${t:-0} ))
+ if [ "$age" -gt 120 ] || [ "$age" -lt 0 ]; then
+ if mkdir "$ST_VPN_PROBE.lock" 2>/dev/null; then
+ ( _st_vpn_probe >/dev/null 2>&1; rmdir "$ST_VPN_PROBE.lock" ) >/dev/null 2>&1 &
+ elif [ -n "$(find "$ST_VPN_PROBE.lock" -mmin +1 2>/dev/null)" ]; then
+ rmdir "$ST_VPN_PROBE.lock" 2>/dev/null
+ fi
+ fi
+ [ -z "$r" ] && return 2
+ [ "$age" -gt 600 ] && return 2
+ [ "$r" = ok ]
+}
+
_st_sub_label() {
[ -s "$ST_SUB" ] || return 0
if [ -s "$ST_SUB_TITLE" ]; then
@@ -6700,6 +6990,7 @@ do_steer_sub_remove() {
}
steer_sub_action() { # ДЕЙСТВИЕ ЗНАЧЕНИЕ
+ rm -f "$ST_VPN_PROBE"
local action="$1" mode="$2"
case "$action" in
sub_set)
@@ -6836,10 +7127,30 @@ steer_sub_probe() { # НОМЕР
steer_action() {
local action="$1" mode="$2"
+ [ "$action" = diag ] || rm -f "$ST_VPN_PROBE"
case "$action" in
- install|apply|start|stop|remove|warp_restart|warp_endpoint|warp_recreate|lists|engine|warp_setup|warp_fix|warp_fixkeys)
+ install|apply|start|stop|remove|warp_restart|warp_endpoint|warp_recreate|lists|engine|warp_setup|warp_fix|warp_fixkeys|warp_own|warp_mode)
_st_running && { echo '{"error":"дождитесь окончания текущей операции"}'; return 1; }
case "$action" in
+ warp_endpoint|warp_recreate|warp_fix|warp_fixkeys)
+ _st_warp_own && { echo '{"error":"в режиме «Свой конфиг» ключи и точку входа задаёт ваш конфиг — замените его или вернитесь к автоматическому WARP"}'; return 1; } ;;
+ esac
+ case "$action" in
+ warp_own)
+ if [ -n "$mode" ]; then
+ printf '%s\n' "$mode" | grep -qi '^[[:space:]]*\[interface\]' || { echo '{"error":"в конфиге нет секции [Interface]"}'; return 1; }
+ printf '%s\n' "$mode" | grep -qi '^[[:space:]]*\[peer\]' || { echo '{"error":"в конфиге нет секции [Peer]"}'; return 1; }
+ printf '%s\n' "$mode" | grep -qi '^[[:space:]]*endpoint[[:space:]]*=' || { echo '{"error":"в конфиге нет Endpoint — адреса сервера"}'; return 1; }
+ mkdir -p "$ST_DIR"
+ printf '%s\n' "$mode" | tr -d '\r' > "$ST_DIR/warp.pending"
+ chmod 600 "$ST_DIR/warp.pending"
+ else
+ [ -s "$ST_WARP_OWN" ] || { echo '{"error":"вставьте конфиг WARP"}'; return 1; }
+ fi
+ job_start steer do_steer_warp_own ;;
+ warp_mode)
+ [ "$mode" = auto ] || { echo '{"error":"неизвестный режим"}'; return 1; }
+ job_start steer do_steer_warp_auto ;;
install) job_start steer do_steer_install ;;
apply|start) job_start steer do_steer_apply ;;
stop) job_start steer do_steer_stop ;;
@@ -6896,11 +7207,8 @@ steer_action() {
local trace warp="none" colo="" tun="" tsep="" wi wn wv wc vpn="none" vip="" vloc=""
if _st_installed && [ -n "$(_st_sel)" ] && [ ! -f "$ST_OFF" ] && _st_use_vpn; then
vpn=off
- if [ -d "/sys/class/net/$ST_VPN_OUT" ]; then
- trace="$(curl -s --interface "$ST_VPN_OUT" --connect-timeout 5 --max-time 10 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null)"
- vip="$(echo "$trace" | sed -n 's/^ip=//p')"; vloc="$(echo "$trace" | sed -n 's/^loc=//p')"
- [ -n "$vip" ] && vpn=on
- fi
+ _st_vpn_probe && vpn=on
+ read -r _ _ vip vloc 2>/dev/null < "$ST_VPN_PROBE"
elif _st_installed && _st_warp_on && [ -n "$(_st_sel)" ] && [ ! -f "$ST_OFF" ]; then
warp=off
wn=1
@@ -6909,10 +7217,13 @@ steer_action() {
if _st_owns "net $wi" && [ -d "/sys/class/net/$wi" ]; then
trace="$(curl -s --interface "$wi" --connect-timeout 4 --max-time 8 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null)"
wc="$(echo "$trace" | sed -n 's/^colo=//p')"
+ wv=""
case "$trace" in
- *warp=on*|*warp=plus*) wv=on; warp=on; [ -n "$colo" ] || colo="$wc" ;;
- *) wc="$(_st_colo_of "$wi")"; [ -n "$wc" ] && wv=notls || wv=off ;;
+ *warp=on*|*warp=plus*) wv=on ;;
+ *ip=*) _st_warp_own && wv=on ;;
esac
+ if [ "$wv" = on ]; then warp=on; [ -n "$colo" ] || colo="$wc"
+ else wc="$(_st_colo_of "$wi")"; [ -n "$wc" ] && wv=notls || wv=off; fi
tun="$tun$tsep{\"n\":$wn,\"warp\":\"$wv\",\"colo\":\"$(esc "$wc")\"}"
tsep=","
fi
@@ -7482,6 +7793,13 @@ _awg_steer_n() { case "$1" in zmwarp) echo 1 ;; *) echo "${1#zmwarp}" ;; esac; }
do_awg_steer_replace() { # ИНТЕРФЕЙС
local i="$1" n c f="$AWG_DIR/pending.conf" kv="$AWG_RUN/kv" ep host port k v w=0 col
echo replace > "$AWG_RUN/phase"
+ if _st_warp_own; then
+ # у Steer «Свой конфиг» — новый конфиг становится своим конфигом Steer
+ mkdir -p "$ST_DIR" "$ST_RUN"
+ mv -f "$f" "$ST_DIR/warp.pending"
+ do_steer_warp_own
+ return
+ fi
n="$(_awg_steer_n "$i")"; c="$(_st_wconf "$n")"
_awg_conf_kv "$f" > "$kv"
[ -n "$(_awg_cv "$kv" interface privatekey)" ] && [ -n "$(_awg_cv "$kv" peer publickey)" ] && [ -n "$(_awg_cv "$kv" interface address)" ] ||
@@ -7755,10 +8073,11 @@ case "$cmd" in
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_list_flowseal) strategy_list_flowseal "$1" ;;
strategy_set_flowseal) strategy_set_flowseal "$1" ;;
- strategy_list_youtube) strategy_list_youtube ;;
+ strategy_list_youtube) strategy_list_youtube "$1" ;;
strategy_set_youtube) strategy_set_youtube "$1" ;;
+ youtube_quic_set) youtube_quic_set "$1" ;;
discord_status) discord_status ;;
discord_set_dv) discord_set_dv "$1" ;;
discord_set_fake) discord_set_fake "$1" ;;
@@ -7849,10 +8168,11 @@ list_methods() {
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_list_flowseal"; json_add_string "action" "string"; 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_list_youtube"; json_add_string "action" "string"; json_close_object
json_add_object "strategy_set_youtube"; json_add_string "name" "string"; json_close_object
+ json_add_object "youtube_quic_set"; json_add_string "mode" "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
@@ -7936,10 +8256,11 @@ call_method() {
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_list_flowseal) json_get_var action action; "$BACKEND" strategy_list_flowseal "$action" ;;
strategy_set_flowseal) json_get_var name name; "$BACKEND" strategy_set_flowseal "$name" ;;
- strategy_list_youtube) "$BACKEND" strategy_list_youtube ;;
+ strategy_list_youtube) json_get_var action action; "$BACKEND" strategy_list_youtube "$action" ;;
strategy_set_youtube) json_get_var name name; "$BACKEND" strategy_set_youtube "$name" ;;
+ youtube_quic_set) json_get_var mode mode; "$BACKEND" youtube_quic_set "$mode" ;;
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" ;;
@@ -8045,7 +8366,7 @@ cat > '/usr/share/rpcd/acl.d/luci-app-zapret-manager.json' << 'ZM_INSTALLER_EOF'
"ubus": {
"zapret-manager": [
"zapret_action", "zapret2_action",
- "strategy_set_v", "strategy_set_flowseal", "strategy_set_youtube",
+ "strategy_set_v", "strategy_set_flowseal", "strategy_set_youtube", "youtube_quic_set",
"discord_set_dv", "discord_set_fake",
"hosts_toggle", "doh_set", "hosts_replace_geohide", "hosts_reset", "hosts_file_set", "doh_install", "doh_remove",
"game_set", "game_set_fake", "game_toggle_xtreme",
@@ -8159,10 +8480,11 @@ var callZapretLatestVersion = rpc.declare({ object: 'zapret-manager', method: 'z
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 callStrategyListFlowseal = rpc.declare({ object: 'zapret-manager', method: 'strategy_list_flowseal', params: ['action'], 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 callStrategyListYoutube = rpc.declare({ object: 'zapret-manager', method: 'strategy_list_youtube', params: ['action'], expect: {} });
var callStrategySetYoutube = rpc.declare({ object: 'zapret-manager', method: 'strategy_set_youtube', params: ['name'], expect: {} });
+var callYoutubeQuicSet = rpc.declare({ object: 'zapret-manager', method: 'youtube_quic_set', params: ['mode'], 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: {} });
@@ -8463,6 +8785,7 @@ return baseclass.extend({
strategySetFlowseal: callStrategySetFlowseal,
strategyListYoutube: callStrategyListYoutube,
strategySetYoutube: callStrategySetYoutube,
+ youtubeQuicSet: callYoutubeQuicSet,
discordStatus: callDiscordStatus,
discordSetDv: callDiscordSetDv,
discordSetFake: callDiscordSetFake,
@@ -8656,22 +8979,51 @@ return view.extend({
var rows = [
row('Модель', E('span', {}, sysInfo.model || '—')),
row('Архитектура', E('span', {}, sysInfo.arch || '—')),
+ row('Платформа', E('span', {}, sysInfo.target || '—')),
row('OpenWrt', E('span', {}, sysInfo.openwrt || '—'))
];
+ // Шкалы — как в боковой панели Web UI: подпись и значение сверху, полоска снизу;
+ // жёлтая и красная — по тем же порогам. Классы zmw-mem-* дают в Web UI тот же вид, что и в меню.
+ function meter(label, val, pct, mid, hi) {
+ pct = Math.max(0, Math.min(100, pct || 0));
+ return E('div', { 'class': 'zm-meter zmw-mem-row' + (pct >= hi ? ' zm-meter-hi zmw-mem-hi' : pct >= mid ? ' zm-meter-mid zmw-mem-mid' : '') }, [
+ E('div', { 'class': 'zm-meter-head zmw-mem-head' }, [
+ E('span', { 'class': 'zm-meter-label zmw-mem-label' }, label),
+ E('span', { 'class': 'zm-meter-val zmw-mem-val' }, val)
+ ]),
+ E('div', { 'class': 'zm-meter-bar zmw-mem-bar' }, [ E('i', { 'style': 'width:' + pct.toFixed(0) + '%' }) ])
+ ]);
+ }
+ function usage(label, used, total) {
+ return total > 0 ? meter(label, zm.fmtSize(used) + ' из ' + zm.fmtSize(total), used / total * 100, 65, 85) : null;
+ }
var memRows = [];
var t = parseFloat(sysInfo.cpu_temp);
- if (!isNaN(t)) memRows.push(row('Температура ЦП', E('span', { 'class': 'zm-badge ' + (t >= 80 ? 'zm-bad' : t >= 65 ? 'zm-warn' : 'zm-ok') }, [
- E('span', { 'class': 'zm-dot' }), sysInfo.cpu_temp + ' °C'
- ])));
- if (mem.total) memRows.push(row('ОЗУ', E('span', {}, zm.usageText(mem.total - ramAvail, mem.total))));
- memRows.push(row('Флеш', E('span', {}, zm.usageText(ru, ru + rf))));
- memRows.push(row('/tmp', E('span', {}, zm.usageText(tu, tu + tf))));
+ if (!isNaN(t)) memRows.push(meter('Температура ЦП', sysInfo.cpu_temp + ' °C', t, 65, 80));
+ var cl = parseInt(sysInfo.cpu_load, 10);
+ if (!isNaN(cl)) memRows.push(meter('Нагрузка ЦП', cl + '%', cl, 70, 90));
+ memRows.push(usage('ОЗУ', mem.total - ramAvail, mem.total));
+ memRows.push(usage('Флеш', ru, ru + rf));
+ memRows.push(usage('/tmp', tu, tu + tf));
+ memRows = memRows.filter(Boolean);
+ var up = parseInt(boardInfo.uptime, 10), extras = [];
+ if (up > 0) {
+ var dd = Math.floor(up / 86400), hh = Math.floor(up % 86400 / 3600), mm = Math.floor(up % 3600 / 60);
+ extras.push(row('Время работы', E('span', {}, (dd ? dd + ' дн ' : '') + (dd || hh ? hh + ' ч ' : '') + mm + ' мин')));
+ }
+ var ims = parseInt(sysInfo.inet_ms, 10);
+ extras.push(row('Интернет', sysInfo.inet === 'ok'
+ ? E('span', { 'class': 'zm-badge ' + (ims >= 200 ? 'zm-warn' : 'zm-ok') }, [ E('span', { 'class': 'zm-dot' }), isNaN(ims) ? 'есть' : 'есть · ' + ims + ' мс' ])
+ : sysInfo.inet === 'fail'
+ ? E('span', { 'class': 'zm-badge zm-bad' }, [ E('span', { 'class': 'zm-dot' }), 'нет' ])
+ : E('span', { 'class': 'zm-badge zm-off' }, [ E('span', { 'class': 'zm-dot' }), 'проверяем…' ])));
+ rows = rows.concat(extras);
cards.appendChild(E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Система'),
E('div', { 'class': 'bt-cols' }, [
E('div', { 'class': 'bt-col' }, rows),
- E('div', { 'class': 'bt-col' }, memRows)
+ E('div', { 'class': 'bt-col zm-meters' }, memRows)
])
]));
}
@@ -8753,10 +9105,28 @@ return view.extend({
wrap.appendChild(cards);
wrap.appendChild(verEl);
renderVersions();
- loadVersions('');
+ loadVersions('auto');
this.refreshOverview = refreshOverview;
+ // Температура, нагрузка и память обновляются сами; состояние компонентов — реже
+ var sysTick = 0, sysBusy = false;
+ var sysTimer = setInterval(function() {
+ if (!document.body.contains(cards)) { if (sysTick > 2) clearInterval(sysTimer); sysTick++; return; }
+ if (document.hidden || sysBusy) return;
+ sysBusy = true;
+ sysTick++;
+ Promise.all([
+ zm.systemInfo().catch(function() { return null; }),
+ zm.boardInfo().catch(function() { return null; })
+ ]).then(function(r) {
+ if (r[0] && !r[0].error) sysInfo = r[0];
+ if (r[1] && r[1].memory) boardInfo = r[1];
+ renderCards();
+ if (sysTick % 3 === 0) refreshOverview();
+ }).catch(function() {}).then(function() { sysBusy = false; });
+ }, 5000);
+
var zmUpdateBusy = false;
function waitForServerAndReload() {
setTimeout(function() { location.href = L.url('admin/logout'); }, 4000);
@@ -9442,6 +9812,9 @@ return view.extend({
data = data || {};
var wrap = E('div', { 'class': 'zm-wrap' });
var busy = false, lastAction = '', pick = null, autoBusy = false, diagRes = null, diagBusy = false;
+ // Режим WARP: warpPick — что выбрано переключателем (null — как на роутере); ownTa — поле своего конфига,
+ // один и тот же элемент между перерисовками, чтобы вставленный текст не пропадал; ownOpen — «Заменить конфиг».
+ var warpPick = null, ownTa = null, ownOpen = false;
var mainCard = E('div', { 'class': 'zm-card' });
var logEl = E('pre', { 'class': 'zm-log' });
@@ -9473,6 +9846,8 @@ return view.extend({
if (!ok) msg = 'Не получилось — подробности в журнале';
else if (lastAction === 'install') msg = 'Steer установлен — подключите WARP или VPN';
else if (lastAction === 'warp_setup') msg = 'WARP подключён';
+ else if (lastAction === 'warp_own') msg = 'Свой WARP подключён';
+ else if (lastAction === 'warp_mode') msg = 'Автоматический WARP подключён';
else if (lastAction === 'warp_fix' || lastAction === 'warp_fixkeys') msg = 'Туннель переделан';
else if (lastAction === 'remove') msg = 'Steer удалён';
else if (lastAction === 'stop') msg = 'Steer выключен — всё идёт напрямую';
@@ -9483,6 +9858,7 @@ return view.extend({
zm.toast(msg, ok ? 'info' : 'error');
var done = lastAction;
lastAction = '';
+ if (ok && (done === 'warp_own' || done === 'warp_mode')) { warpPick = null; ownOpen = false; if (ownTa) ownTa.value = ''; }
diagRes = null;
renderAll();
loadSub();
@@ -9550,6 +9926,13 @@ return view.extend({
return [ 'нет связи', 'zm-warn' ];
}
+ // Туннель VPN: поднят ли он и идёт ли через него трафик на деле.
+ function vpnBadge() {
+ if (!data.vpn_up) return badge('zm-warn', 'подключаемся');
+ if (data.vpn_live === false) return badge('zm-bad', 'нет связи');
+ return badge('zm-ok', 'подключено');
+ }
+
function statusBadge() {
var n = selectedCount();
if (busy || data.running) return badge('zm-warn', PHASE_TEXT[data.phase] || 'работаем');
@@ -9560,6 +9943,7 @@ return view.extend({
// Работает, если служба запущена и жив хоть один туннель: упавший один из трёх —
// штатная работа, Steer уже ведёт трафик через живые.
if (data.exit === 'vpn') {
+ if (data.steer_running && data.vpn_up && data.vpn_live === false) return badge('zm-bad', 'трафик не идёт');
if (data.steer_running && data.vpn_up) return badge('zm-ok', 'работает');
if (data.steer_running) return badge('zm-warn', 'подключаемся к узлу');
return badge('zm-bad', 'не работает');
@@ -9592,9 +9976,9 @@ return view.extend({
E('div', { 'class': 'zm-seg-item' + (!isVpn ? ' zm-active' : ''), 'click': function() { if (isVpn && !busy) act('sub_exit', 'warp', 'Переключаем на WARP'); } }, 'WARP'),
E('div', { 'class': 'zm-seg-item' + (isVpn ? ' zm-active' : ''), 'click': function() { if (!isVpn && !busy) act('sub_exit', 'vpn', 'Переключаем на VPN'); } }, data.sub_label || 'VPN')
]),
- isVpn ? badge(data.vpn_up ? 'zm-ok' : 'zm-warn', data.vpn_up ? 'подключено' : 'подключаемся') : badge(ts[1], ts[0])
+ isVpn ? vpnBadge() : badge(ts[1], ts[0])
]));
- } else if (data.exit === 'vpn') mainCard.appendChild(row('Сервисы идут через', badge(data.vpn_up ? 'zm-ok' : 'zm-warn', 'VPN' + (data.sub_label ? ' · ' + data.sub_label : ''))));
+ } else if (data.exit === 'vpn') mainCard.appendChild(row('Сервисы идут через', E('span', {}, [ 'VPN' + (data.sub_label ? ' · ' + data.sub_label : '') + ' ', vpnBadge() ])));
else if (data.exit === 'warp') mainCard.appendChild(row('Сервисы идут через', badge(ts[1], 'WARP · ' + ts[0])));
else mainCard.appendChild(row('Сервисы идут через', badge('zm-warn', 'туннель не подключён — выберите вкладку WARP или VPN')));
mainCard.appendChild(row('Через туннель', E('span', {}, n ? n + ' ' + plural(n, 'сервис', 'сервиса', 'сервисов') : 'ничего не выбрано')));
@@ -9771,6 +10155,8 @@ return view.extend({
diagBusy = false;
diagRes = res || {};
renderCheck();
+ // проверка обновила кеш «идёт ли трафик» — подтягиваем состояние
+ zm.steerStatus().then(function(r) { if (r) { data = r; renderMain(); } }).catch(function() {});
if (!quiet) zm.toast('Проверка закончена', 'info');
}).catch(function() { diagBusy = false; renderCheck(); });
}
@@ -9821,8 +10207,53 @@ return view.extend({
warpCard.style.display = data.blocker ? 'none' : '';
if (data.blocker) return;
var tl = data.tunnels || [];
- warpCard.appendChild(E('h3', {}, tl.length > 1 ? 'Туннели WARP' : 'Туннель WARP'));
+ var own = data.warp_on && data.warp_mode === 'own';
+ warpCard.appendChild(E('h3', {}, own ? 'Свой WARP' : tl.length > 1 ? 'Туннели WARP' : 'Туннель WARP'));
if (!data.installed) { warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Сначала установите Steer.')); return; }
+
+ // Переключатель режима: «Автоматически» — ключи от Cloudflare и три туннеля; «Свой конфиг» — один туннель по конфигу.
+ var sel = warpPick || (data.warp_on ? (own ? 'own' : 'auto') : 'auto');
+ warpCard.appendChild(E('div', { 'class': 'zm-row' }, [
+ E('span', { 'class': 'zm-label' }, 'Режим'),
+ E('div', { 'class': 'zm-seg' }, [
+ E('div', { 'class': 'zm-seg-item' + (sel === 'auto' ? ' zm-active' : ''), 'click': function() {
+ if (busy || sel === 'auto') return;
+ if (own) {
+ if (!confirm('Перейти на автоматический WARP?\n\nРоутер получит ключи у Cloudflare и подберёт три туннеля — это займёт несколько минут. Ваш конфиг сохранится, вернуться к нему можно в один клик.')) return;
+ act('warp_mode', 'auto', 'Переходим на автоматический WARP — это займёт несколько минут');
+ return;
+ }
+ warpPick = null; renderWarp();
+ } }, 'Автоматически'),
+ E('div', { 'class': 'zm-seg-item' + (sel === 'own' ? ' zm-active' : ''), 'click': function() {
+ if (busy || sel === 'own') return;
+ warpPick = own ? null : 'own'; renderWarp();
+ } }, 'Свой конфиг')
+ ])
+ ]));
+
+ function ownEditor(btnText, toastText) {
+ if (!ownTa) ownTa = E('textarea', { 'class': 'zm-config-editor', 'spellcheck': 'false', 'style': 'min-height:220px',
+ 'placeholder': '[Interface]\nPrivateKey = …\nAddress = 172.16.0.2/32\n\n[Peer]\nPublicKey = …\nEndpoint = 162.159.192.1:2408' });
+ warpCard.appendChild(ownTa);
+ var btns = [ E('button', { 'class': 'cbi-button cbi-button-positive', 'disabled': busy ? '' : null, 'click': function() {
+ var v = ownTa.value.trim();
+ if (!v && !data.warp_own_saved) { zm.toast('Вставьте конфиг WARP', 'warning'); ownTa.focus(); return; }
+ if (v && (!/^\s*\[interface\]/im.test(v) || !/^\s*\[peer\]/im.test(v))) { zm.toast('Это не конфиг WARP: нужны секции [Interface] и [Peer]', 'error'); return; }
+ act('warp_own', v, toastText);
+ } }, btnText) ];
+ if (warpPick === 'own' || ownOpen) btns.push(E('button', { 'class': 'cbi-button', 'click': function() { warpPick = null; ownOpen = false; renderWarp(); } }, 'Отмена'));
+ warpCard.appendChild(E('div', { 'class': 'zm-actions' }, btns));
+ warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Подойдёт конфиг WireGuard или AmneziaWG: ключи, маскировка и точка входа берутся из него как есть.' +
+ (data.warp_own_saved ? ' Оставьте поле пустым — подключится ваш прошлый конфиг.' : '')));
+ }
+
+ if (sel === 'own' && !own) {
+ warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Один туннель по вашему конфигу. Роутер не будет получать ключи и искать точки входа сам.' +
+ (data.warp_on ? ' Три автоматических туннеля будут удалены.' : '')));
+ ownEditor(data.warp_on ? 'Перейти на свой конфиг' : 'Подключить', 'Подключаем свой WARP');
+ return;
+ }
if (!data.warp_on) {
warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Бесплатный туннель Cloudflare WARP с маскировкой AmneziaWG. Поставится AmneziaWG, роутер получит ключи и подберёт три туннеля в разных колониях — это займёт несколько минут.'));
warpCard.appendChild(E('div', { 'class': 'zm-actions' }, [
@@ -9830,6 +10261,26 @@ return view.extend({
]));
return;
}
+ if (own) {
+ if (data.exit === 'vpn') warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Сейчас сервисы идут через подписку — свой WARP не используется.'));
+ var t0 = tl[0] || {}, live0 = tunnelLive(t0), info0 = [];
+ if (t0.host) info0.push(t0.host + ':' + t0.port);
+ if (t0.colo && t0.colo !== '?') info0.push('сервер ' + t0.colo);
+ if (t0.up) info0.push('связь ' + fmtAge(t0.hs_age));
+ if (t0.up) info0.push('↓ ' + zm.fmtSize(+t0.rx || 0) + ' / ↑ ' + zm.fmtSize(+t0.tx || 0));
+ warpCard.appendChild(E('div', { 'class': 'zm-row' }, [
+ E('span', { 'class': 'zm-label' }, 'Туннель'),
+ badge(live0 ? 'zm-ok' : t0.up ? 'zm-warn' : 'zm-bad', live0 ? 'работает' : t0.up ? 'нет связи' : 'не поднят'),
+ E('span', {}, info0.join(' · '))
+ ]));
+ if (ownOpen) { ownEditor('Сохранить и подключить', 'Меняем конфиг WARP'); return; }
+ warpCard.appendChild(E('div', { 'class': 'zm-actions' }, [
+ E('button', { 'class': 'cbi-button', 'disabled': busy ? '' : null, 'click': function() { act('warp_restart', '', 'Перезапускаем туннель'); } }, 'Перезапустить туннель'),
+ E('button', { 'class': 'cbi-button', 'disabled': busy ? '' : null, 'click': function() { ownOpen = true; renderWarp(); if (ownTa) ownTa.focus(); } }, 'Заменить конфиг')
+ ]));
+ warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Не работает — «Перезапустить туннель». Не помогло — замените конфиг.'));
+ return;
+ }
if (data.exit === 'vpn') warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Сейчас сервисы идут через подписку — туннели WARP не используются.'));
if (tl.length > 1) warpCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Трафик идёт через самый быстрый живой туннель; упал один — Steer сам переключится на другой.'));
var deadN = tl.filter(function(t) { return !tunnelLive(t); }).length;
@@ -11135,9 +11586,20 @@ return view.extend({
]);
}
+ function applyInstalled(installed) {
+ tabBar.style.display = installed ? '' : 'none';
+ [ panelCard, subCard, autoCard ].forEach(function(c) { c.style.display = installed ? '' : 'none'; });
+ if (!installed && activeTab !== 'main') {
+ activeTab = 'main';
+ TABS.forEach(function(t2) { panels[t2.id].style.display = t2.id === activeTab ? '' : 'none'; });
+ renderTabBar();
+ }
+ }
+
function renderStatus(d) {
statusCard.innerHTML = '';
var installed = d.mihomo === 'installed';
+ applyInstalled(installed);
var allRunning = d.mihomo_running === true && d.hev_running === true && d.magitrickle_running === true;
var hasUpdate = (d.mihomo_version && d.mihomo_latest && d.mihomo_version !== d.mihomo_latest) ||
(d.magitrickle_version && d.magitrickle_latest && d.magitrickle_version !== d.magitrickle_latest);
@@ -11809,7 +12271,7 @@ return view.extend({
busy = true;
zm.toast('Обновляем список Flowseal', 'warning');
fGrid.innerHTML = 'Загрузка списка';
- zm.strategyListFlowseal().then(function(res) {
+ zm.strategyListFlowseal('refresh').then(function(res) {
if (res.started) {
zm.pollJob('flowseal_download', logEl, function(ok) {
busy = false;
@@ -11900,8 +12362,14 @@ return view.extend({
panels.strategy.appendChild(logEl);
zm.strategyListFlowseal().then(function(res) {
- if (!res.started) renderFlowseal(res);
- else fGrid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список ещё не загружен — нажмите «Обновить список».'));
+ if (!res.started) { renderFlowseal(res); return; }
+ busy = true;
+ fGrid.appendChild(E('p', { 'class': 'zm-hint' }, 'Загружаем список…'));
+ zm.pollJob('flowseal_download', logEl, function(ok) {
+ busy = false;
+ if (!ok) { fGrid.innerHTML = ''; fGrid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список не загрузился — нажмите «Обновить список».')); return; }
+ zm.strategyListFlowseal().then(renderFlowseal);
+ });
});
var nfqwsCard = E('div', { 'class': 'zm-card' });
@@ -11920,7 +12388,7 @@ return view.extend({
nfqwsCard.innerHTML = '';
nfqwsCard.appendChild(E('h3', {}, 'Редактировать текущую стратегию'));
if (!nfqwsOpen) {
- nfqwsCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Прямое редактирование параметров NFQWS_OPT в /etc/config/zapret — для опытных пользователей.'));
+ nfqwsCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Ручная правка блока NFQWS_OPT — для опытных пользователей.'));
nfqwsCard.appendChild(E('div', { 'class': 'zm-actions' }, [
E('button', {
'class': 'cbi-button',
@@ -11933,7 +12401,7 @@ return view.extend({
]));
return;
}
- nfqwsCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Содержимое в /etc/config/zapret. Сохранение применяет изменения и перезапускает Zapret.'));
+ nfqwsCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Блок NFQWS_OPT из /etc/config/zapret. «Сохранить и применить» перезапустит Zapret.'));
nfqwsEl.style.display = '';
nfqwsCard.appendChild(nfqwsEl);
nfqwsCard.appendChild(E('div', { 'class': 'zm-actions' }, [
@@ -11984,7 +12452,7 @@ return view.extend({
function renderMain() {
mainCard.innerHTML = '';
mainCard.appendChild(E('h3', {}, 'Тест стратегий v / Flowseal'));
- mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Тест идёт в фоне — переключение вкладок или закрытие LuCI его не прервёт. По окончании конфигурация всегда возвращается к тому, что было до начала теста — стратегии только тестируются, лучшую нужно применить вручную на странице «Стратегии».'));
+ mainCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Тест идёт в фоне — его не прервут ни смена вкладки, ни закрытие страницы. После теста настройки возвращаются как были. Лучшую стратегию примените сами на вкладке «Стратегии».'));
if (busy && TEST_MAIN_MODES.indexOf(curMode) !== -1) {
mainCard.appendChild(E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Статус'),
@@ -12006,7 +12474,7 @@ return view.extend({
function renderYt() {
ytCard.innerHTML = '';
ytCard.appendChild(E('h3', {}, 'Тест стратегий YouTube (Yv)'));
- ytCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Всегда тестируется отдельно от v/Flowseal.'));
+ ytCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Проверяет только YouTube-стратегии (Yv).'));
if (busy && curMode === 'youtube') {
ytCard.appendChild(E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Статус'),
@@ -12028,7 +12496,7 @@ return view.extend({
function renderCur() {
curCard.innerHTML = '';
curCard.appendChild(E('h3', {}, 'Тест текущей стратегии'));
- curCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Проверяет уже применённую сейчас стратегию (конфигурация не переключается) по доменам DPI и отдельно по доменам YouTube.'));
+ curCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Проверяет стратегию, которая стоит сейчас, ничего не меняя: отдельно по заблокированным сайтам и по YouTube.'));
if (busy && curMode === 'current') {
curCard.appendChild(E('div', { 'class': 'zm-row' }, [
E('span', { 'class': 'zm-label' }, 'Статус'),
@@ -12195,7 +12663,7 @@ return view.extend({
zm.strategySetYoutube('off').then(function(r2) {
busy = false;
if (r2.error) { zm.toast(r2.error, 'error'); return; }
- zm.toast(r2.removed ? 'YouTube-стратегия выключена' : 'YouTube-стратегии в Zapret и так нет — она больше не добавится', 'info');
+ zm.toast(r2.removed ? 'YouTube-стратегия выключена' : 'YouTube-стратегии и так не было — теперь она не будет добавляться', 'info');
refreshAll();
}).catch(function() { busy = false; });
}
@@ -12220,18 +12688,58 @@ return view.extend({
grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список пуст — нажмите «Обновить список».'));
}
+ // Блок QUIC (UDP 443): YouTube отдаёт видео и по QUIC, а не только по TCP
+ var quicOn = !!status.quic_yt, quicBlocked = false;
+ var quicCard = E('div', { 'class': 'zm-card' });
+
+ function renderQuic() {
+ quicCard.innerHTML = '';
+ quicCard.appendChild(E('h3', {}, 'QUIC для YouTube'));
+ quicCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Браузеры и приложение YouTube часто грузят видео по QUIC (UDP 443), а не по TCP. Этот блок обходит блокировку и для QUIC. Работает вместе с выбранной стратегией.'));
+ quicCard.appendChild(E('div', { 'class': 'zm-row' }, [
+ E('span', { 'class': 'zm-label' }, 'Статус'),
+ zm.badge(quicOn, 'включён', 'выключен')
+ ]));
+ if (quicBlocked) quicCard.appendChild(E('p', { 'class': 'zm-hint' }, quicOn
+ ? 'QUIC сейчас заблокирован на странице «Система» — блок не работает, пока блокировка включена.'
+ : 'QUIC сейчас заблокирован на странице «Система» — YouTube и так идёт по TCP, блок не нужен.'));
+ quicCard.appendChild(E('div', { 'class': 'zm-actions' }, [
+ E('button', {
+ 'class': quicOn ? 'cbi-button cbi-button-remove' : 'cbi-button cbi-button-positive',
+ 'click': function() {
+ if (busy) { zm.toast('Дождитесь завершения текущей операции', 'warning'); return; }
+ busy = true;
+ zm.toast(quicOn ? 'Выключаем QUIC для YouTube' : 'Включаем QUIC для YouTube', 'warning');
+ zm.youtubeQuicSet(quicOn ? 'off' : 'on').then(function(res) {
+ busy = false;
+ if (res.error) { zm.toast(res.error, 'error'); return; }
+ zm.toast(res.quic ? 'QUIC для YouTube включён' : 'QUIC для YouTube выключен', 'info');
+ refreshAll();
+ }).catch(function() { busy = false; });
+ }
+ }, quicOn ? 'Выключить' : 'Включить')
+ ]));
+ }
+
function refreshState() {
zm.status().then(function(s) {
current = currentYv(s);
yvOff = !!s.yv_off;
+ quicOn = !!s.quic_yt;
if (lastList) renderGrid(lastList);
+ renderQuic();
});
+ zm.systemStatus().then(function(ss) {
+ quicBlocked = !!(ss && ss.quic_blocked);
+ renderQuic();
+ }).catch(function() {});
}
refreshers.youtube = refreshState;
tabHooks.youtube = refreshState;
var yvOff = !!status.yv_off;
current = currentYv(status);
+ renderQuic();
var card = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Стратегии для YouTube'),
@@ -12243,7 +12751,7 @@ return view.extend({
busy = true;
zm.toast('Обновляем список YouTube-стратегий', 'warning');
grid.innerHTML = 'Загрузка списка';
- zm.strategyListYoutube().then(function(res) {
+ zm.strategyListYoutube('refresh').then(function(res) {
if (res.started) {
zm.pollJob('youtube_download', logEl, function(ok) {
busy = false;
@@ -12260,16 +12768,24 @@ return view.extend({
}, 'Обновить список')
]),
grid,
- E('p', { 'class': 'zm-hint' }, 'Применяется поверх текущей стратегии — заменяет только YouTube-часть. «Выключить» убирает YouTube-часть из Zapret совсем — например, если YouTube идёт через ByeTube или Steer.')
+ E('p', { 'class': 'zm-hint' }, 'Меняет только YouTube-часть текущей стратегии. «Выключить» убирает её совсем — например, если YouTube идёт через ByeTube или Steer.')
]);
panels.youtube.appendChild(card);
panels.youtube.appendChild(logEl);
+ panels.youtube.appendChild(quicCard);
+ refreshState();
if (!initial.started) {
renderGrid(initial);
} else {
- grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список ещё не загружен — нажмите «Обновить список».'));
+ busy = true;
+ grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Загружаем список…'));
+ zm.pollJob('youtube_download', logEl, function(ok) {
+ busy = false;
+ if (!ok) { grid.innerHTML = ''; grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Список не загрузился — нажмите «Обновить список».')); return; }
+ zm.strategyListYoutube().then(renderGrid);
+ });
}
})();
@@ -12324,7 +12840,7 @@ return view.extend({
E('span', { 'class': 'zm-label' }, 'Статус'),
zm.badge(xtreme, 'включён', 'выключен')
]));
- xtremeRow.appendChild(E('p', { 'class': 'zm-hint' }, 'Внимание: может повлиять на работу приложений и соединений — используйте только для проверки игр.'));
+ 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',
@@ -12392,7 +12908,7 @@ return view.extend({
var fakeCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Fake-файл для игровой стратегии'),
fakeGrid,
- E('p', { 'class': 'zm-hint' }, 'Доступно только если игровая стратегия уже выбрана.')
+ E('p', { 'class': 'zm-hint' }, 'Работает, только когда выбрана игровая стратегия.')
]);
panels.game.appendChild(gvCard);
@@ -12419,7 +12935,7 @@ return view.extend({
zm.discordSetDv('off').then(function(res) {
busy = false;
if (res.error) { zm.toast(res.error, 'error'); return; }
- zm.toast(res.removed ? 'Стратегия Discord выключена' : 'Стратегии Discord в Zapret и так нет — она больше не добавится', 'info');
+ zm.toast(res.removed ? 'Стратегия Discord выключена' : 'Стратегии Discord и так не было — теперь она не будет добавляться', 'info');
refreshAll();
}).catch(function() { busy = false; });
}
@@ -12477,13 +12993,13 @@ return view.extend({
var dvCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Стратегия для Discord'),
dvGrid,
- E('p', { 'class': 'zm-hint' }, 'Стратегия Discord состоит из двух блоков: голос (discord,stun) и discord.media. «Выключить» убирает оба блока из Zapret, и при смене основной стратегии они не вернутся. Выбор любой Dv включает их снова.')
+ E('p', { 'class': 'zm-hint' }, 'Стратегия Discord — это два блока: голос (discord,stun) и discord.media. «Выключить» убирает оба, и при смене основной стратегии они не вернутся. Любая Dv включает их снова.')
]);
var fakeCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Fake-файл для discord,stun'),
fakeGrid,
- E('p', { 'class': 'zm-hint' }, 'Доступно только если стратегия Discord включена.')
+ E('p', { 'class': 'zm-hint' }, 'Работает, только когда включена стратегия Discord.')
]);
panels.discord.appendChild(dvCard);
@@ -12503,7 +13019,7 @@ return view.extend({
}
editorCard.appendChild(E('h3', {}, 'Домены исключения'));
- editorCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Редактирование /opt/zapret/ipset/zapret-hosts-user-exclude.txt — домены, для которых Zapret не применяется. Сохранение перезапускает Zapret.'));
+ editorCard.appendChild(E('p', { 'class': 'zm-hint' }, 'Сайты из этого списка Zapret не трогает — по одному домену в строке. Сохранение перезапускает Zapret.'));
editorCard.appendChild(contentEl);
editorCard.appendChild(E('div', { 'class': 'zm-actions' }, [
E('button', {
@@ -12572,7 +13088,7 @@ return view.extend({
}, [ 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).'));
+ grid.appendChild(E('p', { 'class': 'zm-hint' }, 'Устройства не найдены — нажмите «Обновить список».'));
}
renderGrid(data.devices);
@@ -12582,7 +13098,7 @@ return view.extend({
var card = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Исключение устройств из Zapret'),
grid,
- E('p', { 'class': 'zm-hint' }, 'Клик по устройству — включить/выключить исключение (трафик этого IP не будет проходить через Zapret).'),
+ E('p', { 'class': 'zm-hint' }, 'Нажмите на устройство, чтобы исключить его из Zapret или вернуть обратно. Красным отмечены исключённые.'),
E('div', { 'class': 'zm-actions' }, [
E('button', {
'class': 'cbi-button',
@@ -12802,6 +13318,17 @@ html.zm-theme-dark .zm-card {
.zm-off .zm-dot { background: #57606a; }
.zm-actions { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; margin: 14px 0; }
+
+/* Шкалы в «Системе» (как в боковой панели Web UI) */
+.zm-meters { display: flex; flex-direction: column; gap: 13px; padding-top: 7px; }
+body:not(.zmw-body) .zm-meter-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; font-size: 13px; margin-bottom: 6px; }
+body:not(.zmw-body) .zm-meter-label { opacity: .65; }
+body:not(.zmw-body) .zm-meter-val { font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
+body:not(.zmw-body) .zm-meter-bar { height: 6px; border-radius: 6px; background: rgba(110,118,129,.18); overflow: hidden; }
+body:not(.zmw-body) .zm-meter-bar i { display: block; height: 100%; border-radius: 6px; background: linear-gradient(90deg, #0969da, #3b8eea); transition: width .6s cubic-bezier(.2,.8,.2,1); }
+body:not(.zmw-body) .zm-meter-mid .zm-meter-bar i { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
+body:not(.zmw-body) .zm-meter-hi .zm-meter-bar i { background: linear-gradient(90deg, #ef4444, #f87171); }
+html.zm-theme-dark body:not(.zmw-body) .zm-meter-bar { background: rgba(255,255,255,.1); }
.zm-actions .cbi-button { margin: 0; }
.zm-grid { display: flex; flex-wrap: wrap; gap: 9px; }
@@ -12825,6 +13352,8 @@ html.zm-theme-dark .zm-credit-tile { border-color: rgba(255,255,255,.12); backgr
.zm-credit-product { font-weight: 700; font-size: 13.5px; margin-bottom: 2px; overflow-wrap: anywhere; }
.zm-credit-author { font-size: 12px; opacity: .75; margin-bottom: 6px; }
.zm-credit-tile a { font-size: 12px; word-break: break-all; overflow-wrap: anywhere; }
+.zm-credit-tile.zm-credit-all { grid-column: 1 / -1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; }
+.zm-credit-tile.zm-credit-all .zm-credit-author { margin-bottom: 0; }
.zm-tile {
flex: 0 1 auto;
@@ -13361,30 +13890,37 @@ return view.extend({
wrap.appendChild(uninstallCard);
var CREDITS = [
- { product: 'Zapret Manager LuCI и ByeTube', author: 'StressOzz', url: 'https://github.com/StressOzz', self: true },
+ { product: 'Zapret Manager и ByeTube', author: 'StressOzz', url: 'https://github.com/StressOzz', self: true },
+ { product: 'zapret, zapret2', author: 'bol-van', url: 'https://github.com/bol-van' },
{ product: 'zapret-openwrt', author: 'remittor', url: 'https://github.com/remittor/zapret-openwrt' },
+ { product: 'Zapret2', author: 'routerich', url: 'https://github.com/routerich' },
{ product: 'стратегии Flowseal', author: 'Flowseal', url: 'https://github.com/Flowseal/zapret-discord-youtube' },
{ product: 'ByeDPI-OpenWrt', author: 'DPITrickster', url: 'https://github.com/DPITrickster/ByeDPI-OpenWrt' },
- { product: 'mihomo', author: 'MetaCubeX', url: 'https://github.com/MetaCubeX/mihomo' },
+ { product: 'mihomo, metacubexd', author: 'MetaCubeX', url: 'https://github.com/MetaCubeX' },
+ { product: 'zashboard', author: 'Zephyruso', url: 'https://github.com/Zephyruso/zashboard' },
+ { product: 'hev-socks5-tunnel', author: 'heiher', url: 'https://github.com/heiher/hev-socks5-tunnel' },
{ product: 'MagiTrickle', author: 'MagiTrickle', url: 'https://github.com/MagiTrickle/MagiTrickle' },
{ product: 'sTGWS, Steer', author: 'xyzmean', url: 'https://github.com/xyzmean' },
{ product: 'tg-ws-proxy-go (MTProto)', author: 'spatiumstas', url: 'https://github.com/spatiumstas/tg-ws-proxy-go' },
{ product: 'tg-ws-proxy-go (SOCKS5)', author: 'd0mhate', url: 'https://github.com/d0mhate/-tg-ws-proxy-Manager-go' },
{ product: 'tg-ws-proxy-rs (Rust)', author: 'valnesfjord', url: 'https://github.com/valnesfjord/tg-ws-proxy-rs' },
- { product: 'GeoHideDNS', author: 'Internet-Helper', url: 'https://github.com/Internet-Helper/GeoHideDNS' },
- { product: 'dpi-checkers', author: 'hyperion-cs', url: 'https://github.com/hyperion-cs/dpi-checkers' }
+ { product: 'Mixomo, GeoHideDNS', author: 'Internet-Helper', url: 'https://github.com/Internet-Helper' },
+ { product: 'allow-domains', author: 'itdoginfo', url: 'https://github.com/itdoginfo/allow-domains' },
+ { product: 'dpi-checkers', author: 'hyperion-cs', url: 'https://github.com/hyperion-cs/dpi-checkers' },
+ { product: 'awg-openwrt (AmneziaWG)', author: '2Grey', url: 'https://github.com/2Grey/awg-openwrt' },
+ { product: 'Всем пользователям', author: 'кто помогает, тестирует и поддерживает проект ❤', self: true, all: true }
];
var creditsGrid = E('div', { 'class': 'zm-credits-grid' });
CREDITS.forEach(function(c) {
- creditsGrid.appendChild(E('div', { 'class': 'zm-credit-tile' + (c.self ? ' zm-credit-self' : '') }, [
+ creditsGrid.appendChild(E('div', { 'class': 'zm-credit-tile' + (c.self ? ' zm-credit-self' : '') + (c.all ? ' zm-credit-all' : '') }, [
E('div', { 'class': 'zm-credit-product' }, c.product),
E('div', { 'class': 'zm-credit-author' }, c.author),
- E('a', { 'href': c.url, 'target': '_blank', 'rel': 'noreferrer' }, c.url.replace(/^https?:\/\//, ''))
+ c.url ? E('a', { 'href': c.url, 'target': '_blank', 'rel': 'noreferrer' }, c.url.replace(/^https?:\/\//, '')) : ''
]));
});
var creditsCard = E('div', { 'class': 'zm-card' }, [
E('h3', {}, 'Спасибо'),
- E('p', { 'class': 'zm-hint' }, 'Zapret Manager LuCI объединяет и упаковывает в один интерфейс наработки этих проектов и их авторов.'),
+ E('p', { 'class': 'zm-hint' }, 'Zapret Manager собирает в одном месте работу этих проектов и их авторов.'),
creditsGrid
]);
wrap.appendChild(creditsCard);
@@ -15159,8 +15695,12 @@ var UBUS_ERR = [ 'OK', 'неверная команда', 'неверный ар
var rpcSeq = 0;
function ubus(object, method, params, useSid) {
+ // Запрос к роутеру не должен висеть вечно: через 60 с — ошибка вместо вечного «крутится».
+ var ac = window.AbortController ? new AbortController() : null;
+ var tm = ac ? setTimeout(function () { ac.abort(); }, 60000) : 0;
return fetch('/ubus', {
method: 'POST',
+ signal: ac ? ac.signal : undefined,
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
credentials: 'omit',
@@ -15169,6 +15709,7 @@ function ubus(object, method, params, useSid) {
params: [ useSid || sid || NULL_SID, object, method, params || {} ]
})
}).then(function (r) {
+ clearTimeout(tm);
if (r.status === 404) {
var e404 = new Error('На роутере не отвечает /ubus — нужен пакет uhttpd-mod-ubus.');
e404.noUbus = true;
@@ -15184,6 +15725,10 @@ function ubus(object, method, params, useSid) {
}
if (!msg || !Array.isArray(msg.result)) throw new Error('Некорректный ответ ubus');
return msg.result;
+ }, function (err) {
+ clearTimeout(tm);
+ if (err && err.name === 'AbortError') throw new Error('роутер не ответил за 60 секунд');
+ throw err;
});
}
@@ -15496,6 +16041,12 @@ var ICONS = {
logout: '',
sun: '',
moon: '',
+ ink: '',
+ bento: '',
+ minimal: '',
+ retro: '',
+ depth: '',
+ palette: '',
refresh: '',
menu: '',
close: '',
@@ -15531,13 +16082,13 @@ var logoSeq = 0;
function logo(cls) {
var id = 'zmwz' + (++logoSeq);
return E('div', { 'class': 'zmw-logo' + (cls ? ' ' + cls : '') }, [
- E('