This commit is contained in:
StressOzz authored and GitHub committed 2026-10-24 16:43:28 +05:00
1 parent a62bd1e7fd
commit 4cc235e292
1 file changed
+485 -66
+485 -66
View File
@@ -1,6 +1,6 @@
#!/bin/sh
read -r _ _ ZM_NEW_VER <<'ZM_VERSION_EOF'
# Version: 4.34
# Version: 4.38
ZM_VERSION_EOF
set -e
@@ -1544,6 +1544,8 @@ zm_watch() {
_fk_mlkem_patch
_fk_serial_patch
_fk_listsvia_patch
_fk_ghdl_patch
_fk_b4_watch
fi
_st_colo_guard
_st_vpn_watch
@@ -6944,15 +6946,26 @@ _zo_doh_hosts() {
}'
}
_zo_doh_doms() { local d; d="$(_zo_doh_hosts)"; echo "${d:-$ZO_DOH_DEF}"; }
# Блок DoH по умолчанию. Прежний (маскировка под ya.ru) у тех, кто его не менял, панель сама
# заменяет на новый (под common.dot.dns.yandex.net); изменённый вручную блок не трогаем
ZO_DOH_OLD="--filter-tcp=443 --dpi-desync=hostfakesplit --dpi-desync-hostfakesplit-mod=host=ya.ru --dpi-desync-hostfakesplit-midhost=host-2 --dpi-desync-split-seqovl=726 --dpi-desync-fooling=badsum,badseq --dpi-desync-badseq-increment=0"
ZO_DOH_NEW="--filter-tcp=443 --dpi-desync=hostfakesplit --dpi-desync-hostfakesplit-mod=host=common.dot.dns.yandex.net --dpi-desync-hostfakesplit-midhost=host-2 --dpi-desync-split-seqovl=726 --dpi-desync-fooling=badsum,badseq --dpi-desync-badseq-increment=0"
_zo_doh_tpl() {
local t
t="$(grep '^--' "$ZO_DOH_FLAG" 2>/dev/null | grep -v '^--hostlist-domains=' | grep -vx -- '--new')"
if ! printf '%s\n' "$t" | grep -q '^--filter-tcp=' || ! printf '%s\n' "$t" | grep -q '^--dpi-desync='; then
t="$(printf '%s\n' --filter-tcp=443 --dpi-desync=hostfakesplit --dpi-desync-hostfakesplit-mod=host=ya.ru --dpi-desync-hostfakesplit-midhost=host-2 \
--dpi-desync-split-seqovl=726 --dpi-desync-fooling=badsum,badseq --dpi-desync-badseq-increment=0)"
if ! printf '%s\n' "$t" | grep -q '^--filter-tcp=' || ! printf '%s\n' "$t" | grep -q '^--dpi-desync=' || [ "$(echo $t)" = "$ZO_DOH_OLD" ]; then
t="$(printf '%s\n' $ZO_DOH_NEW)"
fi
printf '%s\n' "$t"
}
# В стратегии стоит прежний блок DoH по умолчанию — его пора заменить новым
_zo_doh_stale() {
[ "$(_nfq_opt_body | awk -v M="$ZO_DOH_MK" '
$0 ~ M { c++; d = (c == 1); next }
d && $0 == "--new" { d = 0 }
d && /^[ \t]*--/ && !/^[ \t]*--hostlist-domains=/ { l = $0; gsub(/^[ \t]+|[ \t\r]+$/, "", l); o = o (o == "" ? "" : " ") l }
END { print o }')" = "$ZO_DOH_OLD" ]
}
_zo_doh_scan() {
_nfq_opt_body | awk -v M="$ZO_DOH_MK" '
NR == 1 { next }
@@ -6992,7 +7005,7 @@ _zo_doh_sync() {
grep -q "^[[:space:]]*option NFQWS_OPT '[[:space:]]*\$" "$CONF" || return 1
_nochange_strategy && return 1
doms="$(_zo_doh_doms)"
[ "$(ZM_NFQ_BODY=""; _zo_doh_scan)" = "top $doms" ] && return 1
[ "$(ZM_NFQ_BODY=""; _zo_doh_scan)" = "top $doms" ] && ! (ZM_NFQ_BODY=""; _zo_doh_stale) && return 1
cap="$ZO_DOH_FLAG.cap"
rm -f "$cap"
_zo_doh_cut "$cap"
@@ -11360,6 +11373,32 @@ _doh_cur_id() {
p="$(_doh_provider_of "$u")"
echo "${p:-$u}"
}
# Переход на сервер, через который DNS на роутере так и не заработал (напрямую сервер отвечает, а через
# https-dns-proxy — нет), не повторяем каждые 2 минуты: каждая такая попытка перезапускает DNS роутера и
# рвёт его на 10–15 секунд. Пауза 10 минут, затем 20, 40… до 2 часов; ручной выбор сервера её сбрасывает.
_doh_bad_key() { printf 'bad%s' "$(printf '%s' "$1" | md5sum | cut -c1-10)"; }
_doh_bad_wait() {
local v n w=600
v="$(_doh_st_get "$(_doh_bad_key "$1")" "")"; n="${v#*:}"
case "$n" in ''|*[!0-9]*) echo 0; return ;; esac
while [ "$n" -gt 1 ] && [ "$w" -lt 7200 ]; do w=$((w * 2)); n=$((n - 1)); done
[ "$w" -gt 7200 ] && w=7200
echo "$w"
}
_doh_bad_skip() {
local v t w
v="$(_doh_st_get "$(_doh_bad_key "$1")" "")"; [ -n "$v" ] || return 1
t="${v%%:*}"; w="$(_doh_bad_wait "$1")"
case "$t" in ''|*[!0-9]*) return 1 ;; esac
[ $(( $(date +%s) - t )) -lt "$w" ]
}
_doh_bad_mark() {
local v n
v="$(_doh_st_get "$(_doh_bad_key "$1")" "")"; n="${v#*:}"
case "$n" in ''|*[!0-9]*) n=0 ;; esac
_doh_st_put "$(_doh_bad_key "$1")" "$(date +%s):$((n + 1))"
}
_doh_bad_clear() { [ -f "$DOH_FO_STATE" ] && grep -q "^$(_doh_bad_key "$1")=" "$DOH_FO_STATE" && _doh_st_put "$(_doh_bad_key "$1")" ""; return 0; }
_doh_ok() { case "$(_doh_probe "$(_doh_url_of "$1")" "$(_doh_boots_for "$1")" "/tmp/zm-dohfo.$$")" in "ok "*) return 0 ;; esac; return 1; }
_doh_cur_alive() {
local port
@@ -11368,6 +11407,8 @@ _doh_cur_alive() {
sleep 2
_doh_live_real "$port" && return 0
_doh_ok "$1" || return 1
# перезапуск уже не помог два раза подряд — не дёргаем DNS роутера снова, решает переключение
[ "${2:-0}" -lt 2 ] || return 1
_zm_run 30 /etc/init.d/https-dns-proxy restart >/dev/null 2>&1
_doh_wait_real "$port" 10 && { _doh_fo_event "$(_doh_name "$1") отвечает, а DNS на роутере завис — перезапустили https-dns-proxy"; return 0; }
return 1
@@ -11402,8 +11443,10 @@ doh_watch() {
_doh_fo_put active "$cur"
if [ "$mode" = direct ]; then
for c in "$pref" $([ "$(_doh_fo_get fo 0)" = 1 ] && _doh_fo_pool "$pref" ""); do
_doh_bad_skip "$c" && continue
_doh_ok "$c" || continue
[ "$c" = "$cur" ] || _doh_apply "$c" >/dev/null 2>&1 || continue
[ "$c" = "$cur" ] || _doh_apply "$c" >/dev/null 2>&1 || { _doh_bad_mark "$c"; continue; }
_doh_bad_clear "$c"
_doh_fo_put active "$c"
/etc/init.d/https-dns-proxy start >/dev/null 2>&1
t=0; while ! _doh_local_ok 5053 && [ "$t" -lt 8 ]; do sleep 1; t=$((t + 1)); done
@@ -11425,18 +11468,23 @@ doh_watch() {
fi
fi
fails="$(_doh_st_get fails 0)"; okc="$(_doh_st_get okc 0)"
if [ "$cur" != "$pref" ] && [ "$(_doh_fo_get fo 0)" = 1 ]; then
if [ "$cur" != "$pref" ] && [ "$(_doh_fo_get fo 0)" = 1 ] && ! _doh_bad_skip "$pref"; then
if _doh_ok "$pref"; then okc=$((okc + 1)); else okc=0; fi
if [ "$okc" -ge 2 ] && _doh_apply "$pref" >/dev/null 2>&1; then
_doh_fo_put active "$pref"
_doh_st_put mode normal fails 0 okc 0 since "$(date +%s)"
_doh_fo_event "$(_doh_name "$pref") снова отвечает — вернулись на него с $(_doh_name "$cur")"
rmdir "$lk" 2>/dev/null
return 0
if [ "$okc" -ge 2 ]; then
if _doh_apply "$pref" >/dev/null 2>&1; then
_doh_bad_clear "$pref"
_doh_fo_put active "$pref"
_doh_st_put mode normal fails 0 okc 0 since "$(date +%s)"
_doh_fo_event "$(_doh_name "$pref") снова отвечает — вернулись на него с $(_doh_name "$cur")"
rmdir "$lk" 2>/dev/null
return 0
fi
_doh_bad_mark "$pref"; okc=0
_doh_fo_event "$(_doh_name "$pref") отвечает, но DNS роутера через него не заработал — остаёмся на $(_doh_name "$cur"), снова попробуем через $(( $(_doh_bad_wait "$pref") / 60 )) мин"
fi
_doh_st_put okc "$okc"
fi
if /etc/init.d/https-dns-proxy running >/dev/null 2>&1 && _doh_cur_alive "$cur"; then
if /etc/init.d/https-dns-proxy running >/dev/null 2>&1 && _doh_cur_alive "$cur" "$fails"; then
_doh_st_put fails 0
rmdir "$lk" 2>/dev/null
return 0
@@ -11445,8 +11493,10 @@ doh_watch() {
[ "$fails" -ge 2 ] || { rmdir "$lk" 2>/dev/null; return 0; }
if [ "$(_doh_fo_get fo 0)" = 1 ]; then
for c in $([ "$cur" != "$pref" ] && echo "$pref") $(_doh_fo_pool "$pref" "$cur"); do
_doh_bad_skip "$c" && continue
_doh_ok "$c" || continue
_doh_apply "$c" >/dev/null 2>&1 || continue
_doh_apply "$c" >/dev/null 2>&1 || { _doh_bad_mark "$c"; continue; }
_doh_bad_clear "$c"
_doh_fo_put active "$c"
_doh_st_put mode "$([ "$c" = "$pref" ] && echo normal || echo backup)" fails 0 okc 0 since "$(date +%s)"
_doh_fo_event "$(_doh_name "$cur") не отвечает — DNS переключён на $(_doh_name "$c")"
@@ -11523,6 +11573,12 @@ doh_failover_set() {
/etc/init.d/https-dns-proxy start >/dev/null 2>&1
_doh_fo_event "автопереключение выключено — DNS снова идёт через DoH"
fi
# выключили, стоя на запасном: возвращаем основной, если он отвечает, — иначе запасной так и остался бы навсегда
v="$(_doh_cur_id)"; pref="$(_doh_fo_get pref "")"
if [ -n "$v" ] && [ -n "$pref" ] && [ "$v" != "$pref" ] && _doh_ok "$pref" && _doh_apply "$pref" >/dev/null 2>&1; then
_doh_fo_put active "$pref"
_doh_fo_event "автопереключение выключено — вернулись на основной $(_doh_name "$pref")"
fi
rm -f "$DOH_FO_STATE"
fi
_zo_doh_follow
@@ -12333,7 +12389,7 @@ stl_apply() {
else
rm -f "$new"
echo "ОШИБКА: ядро steer отвергло настройку"
printf '%s\n' "$err" | tail -n 5
printf '%s\n' "$err" | tail -n 5 | _st_outs_text
return 1
fi
fi
@@ -12345,7 +12401,7 @@ stl_apply() {
if ! err="$(_stl_t 300 steer apply 2>&1 >/dev/null)"; then
if [ -s "$prev" ]; then mv -f "$prev" "$STL_SPEC"; else rm -f "$STL_SPEC"; fi
echo "ОШИБКА: ядро steer не применило правила — остались прежние"
printf '%s\n' "$err" | tail -n 5
printf '%s\n' "$err" | tail -n 5 | _st_outs_text
return 1
fi
rm -f "$prev"
@@ -12410,19 +12466,170 @@ stl_diag() {
d="$(_stl_t 30 steer diag 2>/dev/null | tr '\n' ' ')"
case "$d" in '{'*'}'*) printf '%s' "$d" ;; *) echo null ;; esac
}
stl_diag_lines() {
local f="$STL_TMP/stl-diag.json" n i=0
mkdir -p "$STL_TMP"
_stl_t 30 steer diag > "$f" 2>/dev/null
n="$(jsonfilter -i "$f" -e '@.checks[*].id' 2>/dev/null | wc -l)"
_stl_diag_tsv() {
local n i=0
n="$(jsonfilter -i "$1" -e '@.checks[*].id' 2>/dev/null | wc -l)"
while [ "$i" -lt "$n" ]; do
printf '%s\t%s\t%s\n' "$(jsonfilter -i "$f" -e "@.checks[$i].verdict" 2>/dev/null)" \
"$(jsonfilter -i "$f" -e "@.checks[$i].what" 2>/dev/null)" "$(jsonfilter -i "$f" -e "@.checks[$i].why" 2>/dev/null)"
printf '%s\t%s\t%s\n' "$(jsonfilter -i "$1" -e "@.checks[$i].verdict" 2>/dev/null)" \
"$(jsonfilter -i "$1" -e "@.checks[$i].what" 2>/dev/null)" "$(jsonfilter -i "$1" -e "@.checks[$i].why" 2>/dev/null)"
i=$((i + 1))
done
}
stl_diag_lines() {
local f="$STL_TMP/stl-diag.json"
mkdir -p "$STL_TMP"
_stl_t 30 steer diag > "$f" 2>/dev/null
_stl_diag_tsv "$f"
rm -f "$f"
}
# Проверка ядра steer — понятными словами. Ядро называет выходы внутренними именами: zm_vpn — VPN
# секции «Основная», zm_vpn2 и zm_vpn3 — VPN секций 2 и 3, zmv2_vless и т. п. — узлы этих секций,
# zm_if2 — туннель роутера у секции 2, zm_warp — общий WARP. Вместо них пишем имя секции. Поломка
# секции, в которой ничего не выбрано (или которая выключена), — предупреждение: через неё ничего не
# идёт. К отказу узла — коротко, что это значит и что делать.
# Таблица секций: «id подпись состояние(work|idle|off) туннель»; подпись пустая, если секция одна.
_st_diag_map() {
local s many="" st
[ -n "$(_st_secs)" ] && many=1
for s in main $(_st_secs); do
if [ "$s" = main ]; then
if _st_sec_off main; then st=off; elif _st_sec_with main _st_work_main; then st=work; else st=idle; fi
printf '%s\t%s\t%s\t%s\n' main "${many:+Секция «$(_st_sec_name main)»}" "$st" "$(_st_main_iface)"
else
if _st_sec_off "$s"; then st=off; elif _st_sec_work "$s"; then st=work; else st=idle; fi
printf '%s\t%s\t%s\t%s\n' "$s" "Секция «$(_st_sec_name "$s")»" "$st" "$(_st_sec_iface "$s")"
fi
done
}
# stdin: «verdict what why» от ядра; stdout через \037 (не табуляция: пустые поля не должны слипаться при read):
# «verdict, what, why, что это и что делать, заметка, исходный verdict, исходный what»
ZM_ST_DIAG_AWK='
function secof(o) {
if (o == "zm_vpn" || o == "zm_if" || o ~ /^zmv_/) return "main"
if (o ~ /^zm_(vpn|if)[2-9]$/) return "s" substr(o, length(o), 1)
if (o ~ /^zmv[2-9]_/) return "s" substr(o, 4, 1)
return ""
}
function tipof(r, c) {
if (r ~ /вместо 101/) {
c = r; sub(/^[^0-9]*/, "", c); c = substr(c, 1, 3) + 0
if (c == 400 || c == 404) return "Сервер не знает такого адреса подключения — в ссылке неверный path или host"
if (c == 403) return "Сервер или CDN перед ним не пускает — проверьте host и sni в ссылке или возьмите другой узел"
if (c >= 500) return "Сервер узла (или CDN перед ним) не смог принять подключение — обычно в ссылке неверный path или host, реже сбой у поставщика"
return "Сервер не принял подключение WebSocket — проверьте path и host в ссылке"
}
if (r ~ /Upgrade|Sec-WebSocket|кадр WebSocket/) return "По ссылке отвечает не VPN-сервер, а что-то другое (сайт-заглушка, прокси) — проверьте path, host и порт"
if (r ~ /не разрешил/) return "Адрес сервера из ссылки не находится — проверьте адрес в ссылке и DNS роутера"
if (r ~ /TCP не соединился|нет сокета/) return "До сервера не достучаться: он выключен, сменил порт или его блокирует провайдер"
if (r ~ /ClientHello|таймаут|не ответил за|узел не ответил/) return "Сервер молчит — так выглядит блокировка по имени сайта (SNI) или лежащий узел; попробуйте другой узел"
if (r ~ /не признал ключ|UUID|отверг авторизацию|отказал в авторизации|авторизации негоден|pbk или sid/) return "Сервер не принял ключ или пароль из ссылки — ссылка устарела: обновите подписку или ссылку"
if (r ~ /подлинност|сертификат|pinSHA256/) return "Сертификат сервера не прошёл проверку — в ссылке неверный sni, или соединение подменяют"
if (r ~ /ECH/) return "Ключ ECH в ссылке устарел — обновите ссылку или подписку"
if (r ~ /VLESS encryption/) return "Строка encryption в ссылке не совпадает с сервером — обновите ссылку"
if (r ~ /HTTP\/2|HTTP\/1\.1|gRPC/) return "Сервер не поддерживает транспорт из ссылки (ws, grpc, xhttp) — проверьте тип транспорта"
if (r ~ /QUIC не открылся/) return "Подключение по QUIC (UDP) не открылось — проверьте порт и sni в ссылке; иногда провайдер режет UDP"
if (r ~ /соединения не открываются/) return "Узел отвечает на проверку, но настоящие соединения не держит — выберите другой узел"
if (r ~ /закрыл соединение|TLS закрыт|обрыв|поток закрыт|GOAWAY|не прислал данных|ответа нет|ответ не по протоколу|соединение закрыто/) return "Сервер обрывает соединение — узел перегружен или не пускает с этого адреса; попробуйте другой узел"
return "Узел не отвечает — выберите другой узел или проверьте ссылку"
}
BEGIN {
FS = "\t"; OFS = U
while ((getline l < M) > 0) { split(l, a, "\t"); LB[a[1]] = a[2]; ST[a[1]] = a[3]; IF[a[1]] = a[4] }
close(M)
T = "(zm_vpn[2-9]?|zmv[2-9]?_[A-Za-z0-9]+|zm_warp[0-9]*|zmwarp[0-9]*)"
}
{
v = $1; w = $2; y = $3; v0 = v; tip = ""; note = ""; s = ""; lab = ""
if (w ~ /^выход [A-Za-z0-9_.-]+/) {
o = w; sub(/^выход /, "", o); rest = o; sub(/[: ].*$/, "", o)
rest = substr(rest, length(o) + 1); sub(/^:? */, "", rest)
s = secof(o)
if (s != "") {
k = (o ~ /^zm_if/) ? "туннель" (IF[s] != "" ? " " IF[s] : "") : "VPN"
lab = (LB[s] != "" ? LB[s] " · " : "") k
} else if (o ~ /^zm_warp/) lab = "WARP"
else if (o == "zm_direct") lab = "Мимо Steer"
if (lab != "") {
sub("^устройство " T " в зоне, NAT есть$", "туннель подключён, NAT есть", rest)
sub("^устройство " T " в зоне$", "туннель подключён", rest)
sub("^устройства " T " нет$", "туннель не поднят", rest)
sub("^устройство " T " выключено$", "туннель выключен", rest)
sub("^" T " вне зоны фаервола$", "туннель вне зоны фаервола", rest)
sub("^у " T " нет masquerade", "у туннеля нет masquerade", rest)
sub("^" T " не отвечает", "туннель не отвечает", rest)
cl = ""
if (match(rest, /, трафик канала /)) {
cl = substr(rest, RSTART + RLENGTH); rest = substr(rest, 1, RSTART - 1)
if (cl == "идёт напрямую") cl = "сервисы идут напрямую"
else if (cl == "остановлен") cl = "сервисы остановлены"
else if (cl == "идёт через обход") cl = "сервисы идут через обход блокировок"
}
if (rest ~ /^узел перестал отвечать/) {
rest = "узел не отвечает"
r = y; sub(/; выход вернётся сам, когда узел ответит$/, "", r); sub(/ \(проверьте path и host\)$/, "", r)
if (r == "узел не отвечает") r = ""
y = r; tip = tipof(r)
} else if (rest ~ /^туннель не отвечает/) {
y = "проверьте туннель и его сервер"
tip = "Туннель есть, но трафик через него не проходит — обычно не отвечает сервер туннеля"
} else if (rest == "туннель не поднят") sub(/^туннель не поднят — /, "", y)
if (s != "" && ST[s] != "" && ST[s] != "work") cl = ""
w = lab ": " rest (cl != "" ? ", " cl : "")
}
} else if (w ~ /^канал s[2-9]_/) {
c = w; sub(/^канал /, "", c); s = substr(c, 1, 2); sub(/^s[2-9]_/, "", c); sub(/:.*$/, "", c)
t = w; sub(/^канал s[2-9]_[^:]*/, "", t)
if (LB[s] != "") w = LB[s] " · список «" c "»" t
s = ""
}
if (v == "fail" && s != "" && ST[s] != "" && ST[s] != "work") {
v = "warn"
note = ST[s] == "off" ? "Секция выключена — на работу это сейчас не влияет" : "В секции ничего не выбрано — на работу это сейчас не влияет"
if (LB[s] == "") note = ST[s] == "off" ? "Steer выключен — на работу это сейчас не влияет" : "Сервисы не выбраны — на работу это сейчас не влияет"
} else if (v == "fail" && tip != "" && s != "") tip = tip ". Как только он ответит, всё заработает само"
print v, w, y, tip, note, v0, $2
}'
_st_diag_human() {
local m="$ST_RUN/diag.map.$$"
mkdir -p "$ST_RUN"
_st_diag_map > "$m" 2>/dev/null
awk -v M="$m" -v U="$(printf '\037')" "$ZM_ST_DIAG_AWK"
rm -f "$m"
}
# Любой текст ядра (ошибки apply, журнал): «выход zm_vpn2» → «Секция «чайна» · VPN»
_st_outs_text() {
local m="$ST_RUN/diag.map.$$"
mkdir -p "$ST_RUN"
_st_diag_map > "$m" 2>/dev/null
awk -v M="$m" '
function secof(o) {
if (o == "zm_vpn" || o == "zm_if" || o ~ /^zmv_/) return "main"
if (o ~ /^zm_(vpn|if)[2-9]$/) return "s" substr(o, length(o), 1)
if (o ~ /^zmv[2-9]_/) return "s" substr(o, 4, 1)
return ""
}
BEGIN { while ((getline l < M) > 0) { split(l, a, "\t"); LB[a[1]] = a[2]; IF[a[1]] = a[4] } close(M) }
{
out = ""; t = $0
while (match(t, /выход (zm_vpn[2-9]?|zm_if[2-9]?|zmv[2-9]?_[A-Za-z0-9]+|zm_warp[0-9]*|zm_direct)/)) {
o = substr(t, RSTART, RLENGTH); sub(/^выход /, "", o); s = secof(o)
if (s != "") lab = (LB[s] != "" ? LB[s] " · " : "") ((o ~ /^zm_if/) ? "туннель" (IF[s] != "" ? " " IF[s] : "") : "VPN")
else lab = (o == "zm_direct") ? "Мимо Steer" : "WARP"
out = out substr(t, 1, RSTART - 1) lab; t = substr(t, RSTART + RLENGTH)
}
print out t
}'
rm -f "$m"
}
# Одна строка ядра → понятный текст (для списков «что сломано» перед починкой)
_st_diag_what() { printf 'fail\t%s\t\n' "$1" | _st_diag_human | awk -F"$(printf '\037')" '{ print $2 }'; }
# Для вкладки Steer → «Проверка»: [{"v","what","why","tip","note"}]
_st_diag_hjson() {
_st_diag_human | awk -F"$(printf '\037')" "$ZM_JESC_AWK"'
$1 ~ /^(ok|warn|fail)$/ { printf "%s{\"v\":\"%s\",\"what\":\"%s\",\"why\":\"%s\",\"tip\":\"%s\",\"note\":\"%s\"}", (n++ ? "," : ""), $1, jesc($2), jesc($3), jesc($4), jesc($5) }'
}
_stl_explain_run() {
local t="" x="$STL_ETC/.zm-explain.$$.json"
if [ -s "$STL_SPEC" ] && grep -q '"scope":"device"' "$STL_SPEC" 2>/dev/null; then
@@ -14090,6 +14297,9 @@ _st_tgws_warp() {
}
ST_LISTS_MANIFEST="https://github.com/xyzmean/splify2-lists/releases/latest/download/lists.json"
# Тот же lists.json каталог кладёт и в ветку main. Берём его оттуда (raw.githubusercontent.com): файлы релизов
# отдаёт release-assets.githubusercontent.com, а он у части провайдеров не отвечает. Не вышло — из релиза
ST_LISTS_MANIFEST_RAW="https://raw.githubusercontent.com/xyzmean/splify2-lists/main/lists.json"
ST_CAT_SKIP=" mydyson itdoginfo_meta "
ST_SRS_FALLBACK="https://github.com/itdoginfo/allow-domains/releases/latest/download"
@@ -14109,6 +14319,28 @@ _st_fetch() {
curl -fsSL --interface "$i" --connect-timeout 6 --max-time 60 -o "$2" "$1" 2>/dev/null && [ -s "$2" ]
}
# Набор b4geoip из релиза Greeg0ry/b4geoip-forkop → тот же файл из ветки main (srs/ИМЯ.srs); остальное — как есть
_st_b4_raw() {
case "$1" in
https://github.com/[Gg]reeg0ry/b4geoip-forkop/releases/*.srs) echo "https://raw.githubusercontent.com/Greeg0ry/b4geoip-forkop/main/srs/${1##*/}" ;;
*) echo "$1" ;;
esac
}
# .srs набора: b4geoip — сначала из ветки main, не вышло — из релиза, как раньше. Если в этом обновлении
# не ответили ни raw, ни релиз, следующие наборы b4geoip 20 минут пробуют только raw — без двойного ожидания
_st_srs_dl() {
local u r
u="$(_st_srs_url "$1")"; r="$(_st_b4_raw "$u")"
if [ "$r" != "$u" ]; then
_st_fetch "$r" "$2" && [ "$(head -c 3 "$2")" = SRS ] && return 0
[ -n "$(find "$ST_RUN/b4.down" -mmin -20 2>/dev/null)" ] && return 1
fi
_st_fetch "$u" "$2" && [ "$(head -c 3 "$2")" = SRS ] && return 0
[ "$r" != "$u" ] && { mkdir -p "$ST_RUN"; : > "$ST_RUN/b4.down"; }
return 1
}
_json_flat() {
awk 'BEGIN { RS = "\001" }
function emit(v) { p = ""; for (k = 1; k <= d; k++) p = p (k > 1 ? "." : "") key[k]; print p "\t" v; if (arr[d]) key[d]++ }
@@ -14212,6 +14444,17 @@ _st_cat_build() {
_st_cat_src() { [ -s "$ST_CAT_URL" ] && head -n1 "$ST_CAT_URL" || echo "$ST_LISTS_MANIFEST"; }
# Каталог по умолчанию — сначала из ветки main, затем из релиза; свой каталог — по его ссылке
_st_cat_dl() {
local u
u="$(_st_cat_src)"
if [ "$u" = "$ST_LISTS_MANIFEST" ]; then
_st_fetch "$ST_LISTS_MANIFEST_RAW" "$1" && _st_cat_build "$1" && return 0
rm -f "$1"
fi
_st_fetch "$u" "$1" && _st_cat_build "$1"
}
_st_cat_refresh() {
local m="$ST_RUN/lists.json" lk="$ST_RUN/cat.lock" rc=0 t
[ -f "$ST_CAT_OFF" ] && return 0
@@ -14230,7 +14473,7 @@ _st_cat_refresh() {
rmdir "$lk" 2>/dev/null; mkdir "$lk" 2>/dev/null || return 0
fi
fi
if _st_fetch "$(_st_cat_src)" "$m.tmp" && _st_cat_build "$m.tmp"; then
if _st_cat_dl "$m.tmp"; then
mv -f "$m.tmp" "$m"
touch "$ST_CAT_SVC"
rm -f "$ST_DIR/catalog.err"
@@ -14312,7 +14555,7 @@ _st_srs_get() {
echo " ✓ $set: скачан, доменов $(wc -l < "$d/$set.dom.tmp" | tr -d ' '), подсетей $(wc -l < "$d/$set.pfx.tmp" | tr -d ' ')" >&2
else
if [ ! -f "$ST_RUN/dl.$set" ]; then
if _st_fetch "$(_st_srs_url "$set")" "$f" && [ "$(head -c 3 "$f")" = SRS ]; then
if _st_srs_dl "$set" "$f"; then
mv -f "$f" "$d/$set.srs"
touch "$ST_RUN/dl.$set"
echo " ✓ $set.srs: скачан, $(ls -l "$d/$set.srs" | awk '{ printf "%d КБ", ($5 + 1023) / 1024 }')" >&2
@@ -14802,7 +15045,7 @@ _st_selfcheck0() {
if _st_failopen; then
if [ "$(_st_onfail)" = drop ]; then echo "[FAIL] Туннель: ядро steer не может пустить через него трафик — выбранные сервисы остановлены"
else echo "[FAIL] Туннель: ядро steer не может пустить через него трафик — сервисы идут напрямую, через провайдера"; fi
logread 2>/dev/null | grep -F 'steer[warn]' | tail -n 6 | sed 's/^[^]]*\]: / /'
logread 2>/dev/null | grep -F 'steer[warn]' | tail -n 6 | sed 's/^[^]]*\]: / /' | _st_outs_text
bad=1
else
echo "[ OK ] Ядро steer снова ведёт трафик в туннель"
@@ -14814,12 +15057,11 @@ _st_selfcheck0() {
while IFS="$(printf '\t')" read -r v what why; do
[ "$v" = fail ] && ! _st_diag_node "$what" && core=1
done < "$f"
if [ -z "$core" ] && [ -z "$moved" ] && grep -q '^fail' "$f"; then
echo " · Ядро steer исправно — не отвечает узел или сервер туннеля; перезапуск и переустановка ядра тут не помогут"
fi
local nodeonly=""
[ -z "$core" ] && [ -z "$moved" ] && _st_diag_human < "$f" | awk -F"$(printf '\037')" '$1 == "fail" { f = 1 } END { exit !f }' && nodeonly=1
if [ -n "$core" ] && [ -z "$ST_HEALING" ] && _st_work && [ ! -f "$ST_OFF" ]; then
while IFS="$(printf '\t')" read -r v what why; do
[ "$v" = fail ] && ! _st_diag_node "$what" && echo "!! $what"
[ "$v" = fail ] && ! _st_diag_node "$what" && echo "!! $(_st_diag_what "$what")"
done < "$f"
ST_HEALING=1
if _st_heal "$f"; then healed=1; fi
@@ -14827,19 +15069,29 @@ _st_selfcheck0() {
did="$ST_HEAL_DID"
_rb_say "Проверяем ещё раз"
fi
while IFS="$(printf '\t')" read -r v what why; do
local raw tip note v0 warned=""
_st_diag_human < "$f" > "$f.h"
while IFS="$(printf '\037')" read -r v what why tip note v0 raw; do
case "$v" in
ok) echo "[ OK ] $what" ;;
warn) echo "!! $what${why:+ — $why}" ;;
warn)
[ -n "$moved" ] && [ "$v0" = fail ] && _st_diag_node "$raw" && continue
echo "!! $what${why:+ — $why}"; warned=1
[ -n "$tip" ] && echo " · $tip"
[ -n "$note" ] && echo " · $note" ;;
fail)
[ -n "$moved" ] && _st_diag_node "$what" && continue
[ -n "$moved" ] && _st_diag_node "$raw" && continue
case "$why" in *'init.d/steer'*|*ерезапустите*|*ереустановите*) [ -n "$did" ] && why="$did; пришлите журнал в чат поддержки" ;; esac
echo "[FAIL] $what${why:+ — $why}"; bad=1 ;;
echo "[FAIL] $what${why:+ — $why}"; bad=1
[ -n "$tip" ] && echo " · $tip" ;;
esac
done < "$f"
rm -f "$f"
done < "$f.h"
rm -f "$f" "$f.h"
[ -n "$nodeonly" ] && echo " · Само ядро steer исправно — не отвечает узел или сервер туннеля; перезапуск и переустановка ядра тут не помогут"
[ -n "$healed" ] && [ "$bad" = 0 ] && echo " · Поломку ядра панель исправила сама"
if [ "$bad" = 0 ]; then _rb_say "Проверка пройдена"; else _rb_warn "Проверка нашла поломку — подробности выше"; fi
if [ "$bad" != 0 ]; then _rb_warn "Проверка нашла поломку — подробности выше"
elif [ -n "$warned" ]; then _rb_say "Проверка пройдена — работе ничего не мешает, замечания выше"
else _rb_say "Проверка пройдена"; fi
return $bad
}
@@ -17833,8 +18085,17 @@ steer_action() {
isep=","
done
fi
stl_present && _st_owns "steer-spec" && d="$(stl_diag)"
printf '{"warp":"%s","colo":"%s","vpn":"%s","vpn_ip":"%s","vpn_loc":"%s","tunnels":[%s],"ifs":[%s],"diag":%s}\n' "$warp" "$(esc "$colo")" "$vpn" "$(esc "$vip")" "$(esc "$vloc")" "$tun" "$itj" "${d:-null}"
local hj=""
if stl_present && _st_owns "steer-spec"; then
d="$(stl_diag)"
if [ "$d" != null ]; then
mkdir -p "$ST_RUN"
printf '%s\n' "$d" > "$ST_RUN/diag.ui.$$"
hj="$(_stl_diag_tsv "$ST_RUN/diag.ui.$$" | _st_diag_hjson)"
rm -f "$ST_RUN/diag.ui.$$"
fi
fi
printf '{"warp":"%s","colo":"%s","vpn":"%s","vpn_ip":"%s","vpn_loc":"%s","tunnels":[%s],"ifs":[%s],"diag":%s,"human":[%s]}\n' "$warp" "$(esc "$colo")" "$vpn" "$(esc "$vip")" "$(esc "$vloc")" "$tun" "$itj" "${d:-null}" "$hj"
;;
dns_fix)
do_steer_dns_fix
@@ -21405,6 +21666,65 @@ ZM_FK_FAST_EOF
return 0
}
# Секции Forkozz со списками b4geoip по ссылкам релизов (сохранены до 4.38 или вернулись из резервной копии):
# переводим их на raw-ссылки и перезапускаем Forkozz — не чаще раза в 30 минут
_fk_b4_watch() {
local f="$JOBS_DIR/fk-b4.last"
grep -qi 'greeg0ry/b4geoip-forkop/releases/' /etc/config/netshift 2>/dev/null || return 0
[ -n "$(find "$f" -mmin -30 2>/dev/null)" ] && return 0
_job_running forkop && return 0
mkdir -p "$JOBS_DIR"; : > "$f"
( _fk_device_setup apply >/dev/null 2>&1 </dev/null & )
return 0
}
_fk_ghdl_patch() {
local f=/usr/lib/netshift/helpers.sh
[ -f "$f" ] || return 0
grep -q '^# zm-ghdl-begin 1$' "$f" && return 0
if grep -q '^# zm-ghdl-begin' "$f"; then
sed -i '/^# zm-ghdl-begin/,/^# zm-ghdl-end$/d' "$f"
else
[ "$(grep -c '^download_to_file() {$' "$f")" = 1 ] && grep -q '^redact_url_for_log() {$' "$f" || return 0
sed -i 's/^download_to_file() {$/zm_download_to_file_raw() {/' "$f"
fi
grep -q '^zm_download_to_file_raw() {$' "$f" || return 0
cat >> "$f" << 'ZM_FK_GHDL_EOF'
# zm-ghdl-begin 1
# Zapret Manager: списки с GitHub. У части провайдеров файлы релизов GitHub (их отдаёт
# release-assets.githubusercontent.com) не открываются, хотя сам github.com открыт, — и каждый список
# ждал по минуте, а потом не скачивался. Если в панели «Доступ к GitHub» стоит «Авто», такой файл
# качается через зеркало. После первого провала напрямую следующие списки этого обновления сразу идут
# через зеркало, без минуты ожидания на каждом. Через VPN (http_proxy) и не с GitHub — как раньше.
download_to_file() {
local zm_url="$1" zm_fp="$2" zm_m zm_slow=/tmp/netshift.zm-ghslow zm_skip=""
case "$zm_url" in
https://github.com/*|https://raw.githubusercontent.com/*|https://objects.githubusercontent.com/*|https://release-assets.githubusercontent.com/*|https://codeload.github.com/*) ;;
*) zm_download_to_file_raw "$@"; return ;;
esac
[ -n "$3" ] && { zm_download_to_file_raw "$@"; return; }
[ -n "${ZM_GHDL_MS+x}" ] || ZM_GHDL_MS="$(/opt/zapret-manager-luci/backend.sh gh_mirrors 2>/dev/null)"
[ -n "$ZM_GHDL_MS" ] || { zm_download_to_file_raw "$@"; return; }
if [ -n "$(find "$zm_slow" -mmin -30 2>/dev/null)" ]; then zm_skip=1
else
zm_download_to_file_raw "$@" && return 0
: > "$zm_slow"
fi
for zm_m in $ZM_GHDL_MS; do
log "Downloading via GitHub mirror $zm_m: $(redact_url_for_log "$zm_url")" "warn"
if zm_download_to_file_raw "https://$zm_m/$zm_url" "$zm_fp" "" 1 1 15 && [ -s "$zm_fp" ] && ! head -c 512 "$zm_fp" | grep -qiE '<(!doctype|html)'; then
log "Downloaded via GitHub mirror $zm_m" "info"
return 0
fi
done
if [ -n "$zm_skip" ] && zm_download_to_file_raw "$zm_url" "$zm_fp" "" 1 1 10; then rm -f "$zm_slow"; return 0; fi
return 1
}
# zm-ghdl-end
ZM_FK_GHDL_EOF
return 0
}
_fk_listsvia_patch() {
local f=/usr/bin/netshift tmp
[ -f "$f" ] || return 0
@@ -21731,6 +22051,8 @@ _fk_lists_progress() {
/Importing community subnet lists for/ { print " → Адреса встроенных сервисов — заносим в файрвол"; next }
/Importing domains from URL: / { u = $0; sub(/^.*URL: /, "", u); print " → Домены из списка " base(u); next }
/Importing subnets from URL: / { u = $0; sub(/^.*URL: /, "", u); print " → Адреса из списка " base(u) " — заносим в файрвол"; next }
/^Downloading via GitHub mirror / { m = $0; sub(/^Downloading via GitHub mirror /, "", m); sub(/:.*$/, "", m); print " · GitHub не отдал файл напрямую — качаем через зеркало " m; next }
/^Downloaded via GitHub mirror / { m = $0; sub(/^Downloaded via GitHub mirror /, "", m); print " ✓ Скачано через зеркало " m; next }
/^Download .* list failed/ { u = $2; print " ✗ Не скачался список " base(u); next }
/Failed to decompile binary rule set/ { print " ✗ Набор правил не раскрылся — его адреса пропущены"; next }
/Lists update completed successfully/ { print " ✓ Все списки загружены"; next }
@@ -21845,6 +22167,12 @@ _fk_lists_errors() {
[ -n "$e" ] || return 0
echo "!! Не все списки загрузились:"
printf '%s\n' "$e" | sed 's/^/ ✗ /'
case "$e" in *"сервер не ответил вовремя"*|*"сервер недоступен"*)
if [ "$(uci -q get netshift.settings.download_lists_via_proxy)" != 1 ] && [ -z "$(gh_mirrors)" ]; then
echo " · Похоже, провайдер не пускает к файлам GitHub (их отдаёт release-assets.githubusercontent.com). Включите"
echo " «Скачивать списки сервисов через VPN» в настройках Forkozz или «Авто» в Система → Доступ к GitHub и нажмите «Перезапустить»"
fi ;;
esac
return 0
}
@@ -22046,6 +22374,7 @@ do_fk_install() {
_fk_mlkem_patch
_fk_serial_patch
_fk_listsvia_patch
_fk_ghdl_patch
rm -rf "$tmp"
if [ ! -s /etc/config/netshift ]; then
@@ -22383,6 +22712,7 @@ do_fk_service() {
_fk_mlkem_patch
_fk_serial_patch
_fk_listsvia_patch
_fk_ghdl_patch
mkdir -p /etc/sing-box
_fk_log_mark
: > "$JOBS_DIR/forkop-svc.out"
@@ -22453,6 +22783,7 @@ do_fk_lists() {
_fk_mlkem_patch
_fk_serial_patch
_fk_listsvia_patch
_fk_ghdl_patch
_fk_log_mark
/usr/bin/netshift list_update > "$out" 2>&1 &
lp=$!
@@ -22538,6 +22869,7 @@ do_fk_subs() {
_fk_mlkem_patch
_fk_serial_patch
_fk_listsvia_patch
_fk_ghdl_patch
_zm_quiet "" /usr/bin/netshift subscription_update all || { echo "ОШИБКА: подписка не обновилась — проверьте ссылку"; return 1; }
_fk_ext_auto all "$lx"
_fk_say "Готово: подписки обновлены"
@@ -25011,6 +25343,7 @@ case "$cmd" in
ui_flags_get) ui_flags_get ;;
ui_flag_set) ui_flag_set "$1" ;;
adblock_dns_sync) _ab_dns_sync ;;
zapret_doh_migrate) [ -f "$ZO_DOH_FLAG" ] && [ -f "$CONF" ] && _zo_doh_stale && { _zo_doh_follow; echo '{"changed":true}'; } || echo '{"changed":false}' ;;
multi) _zm_multi "$1" ;;
*) echo '{"error":"неизвестная команда"}'; exit 1 ;;
esac
@@ -25051,6 +25384,17 @@ const SRS_MAIN = "https://github.com/itdoginfo/allow-domains/releases/latest/dow
const ITD_SUBNETS = { cloudflare: true, cloudfront: true, digitalocean: true, discord: true, google_meet: true, hetzner: true,
meta: true, ovh: true, roblox: true, telegram: true, twitter: true };
const CAT_SKIP = { mydyson: true, itdoginfo_meta: true };
// b4geoip (Greeg0ry/b4geoip-forkop). Файлы его релизов отдаёт release-assets.githubusercontent.com — у части
// провайдеров он не отвечает, и списки не скачивались. Те же .srs каждый день кладутся в ветку main
// репозитория — берём их оттуда, с raw.githubusercontent.com (как и списки itdoginfo).
const B4_RAW = "https://raw.githubusercontent.com/Greeg0ry/b4geoip-forkop/main/srs/";
// Что внутри наборов: twitch и xbox — только домены, остальные (сборка из geoip.dat) — только подсети.
// Каждый подключается туда, где он работает, и качается один раз. Незнакомый новый набор — как раньше:
// и доменами, и подсетями, так он сработает в любом случае.
const B4_DOMAINS = { twitch: true, xbox: true };
const B4_SUBNETS = { adobe: true, anthropic: true, apple: true, blizzard: true, bungie: true, ccp: true, electronicarts: true,
epicgames: true, google: true, nintendo: true, play2go: true, riot: true, roblox: true, sony: true, taketwo: true,
ubisoft: true, valve: true, wargaming: true };
const PKG_LOCAL_DIR = "/usr/share/zm-redbtn/svc/";
const V2F_RAW = "https://raw.githubusercontent.com/v2fly/domain-list-community/master/data";
const PKG_LISTS = {
@@ -25175,6 +25519,28 @@ function latest_url(u) {
return replace(s(u), /^(https:\/\/github\.com\/[^\/]+\/[^\/]+\/releases\/)download\/[^\/]+\//, "$1latest/download/");
}
// Имя набора b4geoip по ссылке (релиз — последний или с датой — или ветка main), иначе null
function b4_name(u) {
let m = match(s(u), /^https:\/\/(github\.com\/greeg0ry\/b4geoip-forkop\/releases\/(latest\/download|download\/[^\/]+)|raw\.githubusercontent\.com\/greeg0ry\/b4geoip-forkop\/(refs\/heads\/)?main\/srs)\/([a-z0-9_-]+)\.srs$/i);
return m ? lc(m[4]) : null;
}
// Набор b4geoip → { k, u }: k — куда он подключается (s — домены, n — подсети, r — и то и другое), u — raw-ссылка
function b4_ref(n) {
return { k: B4_DOMAINS[n] ? "s" : B4_SUBNETS[n] ? "n" : "r", u: B4_RAW + n + ".srs" };
}
// Наборы правил секции (s — домены, r — домены и подсети, n — только подсети): b4geoip — по raw-ссылке и своего типа
function refs_fix(r) {
let o = { c: r.c || [], s: [], r: [], n: [], l: r.l || [] };
for (let k in [ "s", "r", "n" ]) for (let u in (r[k] || [])) {
let b = b4_name(u), x = b ? b4_ref(b) : { k, u };
push(o[x.k], x.u);
}
o.s = uniq(o.s); o.r = uniq(o.r); o.n = uniq(o.n);
return o;
}
function kind_hint(u) {
u = lc(s(u));
for (let w in [ "geosite", "domain", "adguard", "filter" ]) if (index(u, w) >= 0) return "domains";
@@ -25204,6 +25570,8 @@ function idx_tokens(e, known) {
if (n && known[n]) return [ "c:" + n ];
if (e.fmt == "lst") return [ "l:" + e.url ];
if (n) return itd_plain(n);
let b4 = b4_name(e.url);
if (b4) { let x = b4_ref(b4); return [ x.k + ":" + x.u ]; }
let u = latest_url(e.url);
if (e.kind == "d") return [ "s:" + u ];
if (kind_hint(u) == "domains") return [];
@@ -25421,7 +25789,7 @@ function url_ext(u) {
function is_rs(u) { let e = url_ext(u); return e == "srs" || e == "json"; }
function sec_refs(m) {
let r = { c: uniq(words(m.community_lists)), s: [], r: [], l: [] }, zr = rawlist(m.zm_refs);
let r = { c: uniq(words(m.community_lists)), s: [], r: [], n: [], l: [] }, zr = rawlist(m.zm_refs);
if (length(zr)) {
for (let t in zr) {
let k = substr(t, 0, 1), v = substr(t, 2);
@@ -25434,11 +25802,11 @@ function sec_refs(m) {
else if (index(sn, u) >= 0) push(r.r, u);
else push(r.s, u);
}
for (let u in sn) if (index(d, u) < 0) push(is_rs(u) ? r.r : r.l, u);
for (let u in sn) if (index(d, u) < 0) push(is_rs(u) ? r.n : r.l, u);
for (let u in [ ...words(m.local_domain_lists), ...words(m.local_subnet_lists) ]) push(r.l, u);
}
r.s = uniq(r.s); r.r = uniq(r.r); r.l = uniq(r.l);
return r;
r.l = uniq(r.l);
return refs_fix(r);
}
function sec_configured(m) {
@@ -25459,7 +25827,7 @@ function sec_summary(c, sec, main) {
return {
name: sec, main, label: sec_label(m, main), enabled: main || s(m.disabled) != "1", mode,
iface: s(m.interface), links: length(sec_links(m)),
refs: { c: rf.c, s: rf.s, r: rf.r }, lists: rf.l,
refs: { c: rf.c, s: rf.s, r: rf.r, n: rf.n }, lists: rf.l,
domains: length(domain_lines(m)), subnets: length(subnet_lines(m)), own_off: own_off(m),
full: device_ips(m), all: all_ips(m)
};
@@ -25754,8 +26122,39 @@ function patch_device_engine() {
}
// Секции, сохранённые до 4.38 (или пришедшие из резервной копии): b4geoip — по ссылкам релизов, и каждый
// набор подключён и доменами, и подсетями. Переводим на raw-ссылки и правильный тип; остальное не трогаем
function b4_migrate() {
let c = cursor(), ch = false;
for (let x in sections(c, "section")) {
let n = x[".name"], zr = rawlist(x.zm_refs), d = rawlist(x.remote_domain_lists), sn = rawlist(x.remote_subnet_lists);
let zr2 = [], d2 = [], sn2 = [];
for (let t in zr) {
let k = substr(t, 0, 1), b = substr(t, 1, 1) == ":" && index([ "s", "r", "n" ], k) >= 0 ? b4_name(substr(t, 2)) : null;
if (b) { let y = b4_ref(b); push(zr2, y.k + ":" + y.u); } else push(zr2, t);
}
for (let u in d) {
let b = b4_name(u), y = b ? b4_ref(b) : null;
if (!y) push(d2, u); else push(y.k == "n" ? sn2 : d2, y.u);
}
for (let u in sn) {
let b = b4_name(u), y = b ? b4_ref(b) : null;
if (!y) push(sn2, u); else push(y.k == "s" ? d2 : sn2, y.u);
}
zr2 = uniq(zr2); d2 = uniq(d2); sn2 = uniq(sn2);
if (join("\n", zr2) == join("\n", zr) && join("\n", d2) == join("\n", d) && join("\n", sn2) == join("\n", sn)) continue;
if (length(zr)) set_list(c, n, "zm_refs", zr2);
set_list(c, n, "remote_domain_lists", d2);
set_list(c, n, "remote_subnet_lists", sn2);
ch = true;
}
if (ch) { c.save(CFG); c.commit(CFG); }
return ch;
}
function cmd_device_setup() {
out({ ok: true, changed: patch_device_engine() });
let eng = patch_device_engine(), lists = b4_migrate();
out({ ok: true, changed: eng || lists, lists });
}
function del_keys(c, sec, keys) { for (let k in keys) c.delete(CFG, sec, k); }
@@ -25785,11 +26184,11 @@ function apply_servers(c, sec) {
function set_lists(c, sec, rf, services, domains, subnets) {
set_list(c, sec, "community_lists", services);
let zr = [];
for (let k in [ "s", "r", "l" ]) for (let v in rf[k]) push(zr, k + ":" + v);
for (let k in [ "s", "r", "n", "l" ]) for (let v in (rf[k] || [])) push(zr, k + ":" + v);
set_list(c, sec, "zm_refs", zr);
let rl = filter(rf.l, (u) => !pkg_local(u)), ll = filter(rf.l, (u) => pkg_local(u));
set_list(c, sec, "remote_domain_lists", uniq([ ...rf.s, ...rf.r, ...rl ]));
set_list(c, sec, "remote_subnet_lists", uniq([ ...rf.r, ...rl ]));
set_list(c, sec, "remote_subnet_lists", uniq([ ...rf.r, ...(rf.n || []), ...rl ]));
del_keys(c, sec, [ "local_domain_lists", "local_subnet_lists", "user_domains", "user_subnets" ]);
set_list(c, sec, "local_domain_lists", uniq(ll));
if (length(domains)) {
@@ -26210,7 +26609,7 @@ function cmd_get(want) {
exclude: s(m.zm_exclude),
hide_names: rawlist(m.zm_hide),
jsons: mode == "json" ? 1 : 0,
refs: { c: rf.c, s: rf.s, r: rf.r },
refs: { c: rf.c, s: rf.s, r: rf.r, n: rf.n },
catalog: { items: catalog_items(services_list()), meta: cat_meta() },
own_on: !own_off(m),
domains: own_view(m, rf.l).domains,
@@ -26285,10 +26684,11 @@ function cmd_set() {
let known = services_list(), refs = type(d.refs) == "object" ? d.refs : {};
let services = filter(uniq(words(refs.c)), (x) => index(known, x) >= 0);
let rsets = uniq(words(refs.s)), rsubs = uniq(words(refs.r)), rplain = uniq(words(refs.l));
let rsets = uniq(words(refs.s)), rsubs = uniq(words(refs.r)), rnets = uniq(words(refs.n)), rplain = uniq(words(refs.l));
for (let x in uniq(words(refs.c))) if (index(known, x) < 0 && EXTRA_SRS[x]) push(rsets, EXTRA_SRS[x]);
rsets = uniq(rsets);
for (let x in [ ...rsets, ...rsubs ])
let rfx = refs_fix({ s: rsets, r: rsubs, n: rnets });
rsets = rfx.s; rsubs = rfx.r; rnets = rfx.n;
for (let x in [ ...rsets, ...rsubs, ...rnets ])
if (!match(x, /^https?:\/\/[^ \t]+$/i)) fail("неверная ссылка на набор правил: " + substr(x, 0, 60));
for (let x in rplain)
if (!valid_url(x) && !pkg_local(x)) fail("неверная ссылка на список: " + substr(x, 0, 60));
@@ -26306,7 +26706,7 @@ function cmd_set() {
let all = d.all == null ? all_ips(c.get_all(CFG, sec) || {}) : uniq(tokens(d.all));
for (let x in [ ...full, ...excl, ...all ]) if (!valid_ip(x)) fail("неверный адрес устройства: " + x);
if (length(all) > 64) fail("устройств с полным трафиком через VPN можно выбрать не больше 64");
if (!length(all) && !length(services) && !length(rsets) && !length(rsubs) && !length(domains) && !length(subnets) && !length(lists) && (length(full) || created || !sec_configured(c.get_all(CFG, sec) || {})))
if (!length(all) && !length(services) && !length(rsets) && !length(rsubs) && !length(rnets) && !length(domains) && !length(subnets) && !length(lists) && (length(full) || created || !sec_configured(c.get_all(CFG, sec) || {})))
fail(!own_on && (length(keep.domains) || length(keep.subnets) || length(keep.lists))
? "«Свой список» выключен, а других сервисов в секции нет — включите его или выберите сервис"
: "выберите для секции хотя бы один сервис, домен или адрес — устройства только ограничивают её правила");
@@ -26319,7 +26719,7 @@ function cmd_set() {
if (!device_scopes_overlap(full, device_ips(x))) continue;
let other = sec_label(x, n == p0.sec), orf = sec_refs(x);
for (let v in services) if (index(words(x.community_lists), v) >= 0) fail("список «" + v + "» уже идёт через секцию «" + other + "» — один список можно включить только в одной секции");
for (let v in [ ...rsets, ...rsubs ]) if (index([ ...orf.s, ...orf.r ], v) >= 0) fail("набор правил уже идёт через секцию «" + other + "»: " + substr(v, 0, 60));
for (let v in [ ...rsets, ...rsubs, ...rnets ]) if (index([ ...orf.s, ...orf.r, ...orf.n ], v) >= 0) fail("набор правил уже идёт через секцию «" + other + "»: " + substr(v, 0, 60));
let od = domain_lines(x);
for (let v in domains) if (index(od, v) >= 0) fail("домен " + v + " уже есть в секции «" + other + "» — оставьте его в одной секции");
let os = subnet_lines(x);
@@ -26378,7 +26778,7 @@ function cmd_set() {
}
write_sec_ext(c, sec, mode, d.x);
set_lists(c, sec, { s: rsets, r: rsubs, l: lists }, services, domains, subnets);
set_lists(c, sec, { s: rsets, r: rsubs, n: rnets, l: lists }, services, domains, subnets);
if (own_on || !(length(keep.domains) || length(keep.subnets) || length(keep.lists))) {
del_keys(c, sec, [ "zm_own_off", "zm_own_domains", "zm_own_subnets", "zm_own_lists" ]);
} else {
@@ -26395,7 +26795,7 @@ function cmd_set() {
c.set(CFG, BYPASS, "zm_label", "Исключения");
c.set(CFG, BYPASS, "connection_type", "exclusion");
c.delete(CFG, BYPASS, "disabled");
set_lists(c, BYPASS, { s: [], r: [], l: [] }, [], bdom, bsub);
set_lists(c, BYPASS, { s: [], r: [], n: [], l: [] }, [], bdom, bsub);
c.delete(CFG, BYPASS, "zm_refs");
c.reorder(CFG, BYPASS, 1);
}
@@ -27643,7 +28043,7 @@ function where_scan(host, ip, win) {
if (xe != null) add(names["c:" + cn] ?? cn, cn + "_extra.lst", xe);
}
}
for (let k in [ "s", "r" ]) for (let u in rf[k]) {
for (let k in [ "s", "r", "n" ]) for (let u in rf[k]) {
used[k + ":" + u] = true;
if (srs_hit(url_rs(u), host, ip, on)) add(names[k + ":" + u] ?? ref_base(u), ref_base(u), "");
}
@@ -27663,7 +28063,7 @@ function where_scan(host, ip, win) {
if (e != null) { hit = { list: ref_base(v), entry: e }; break; }
} else if (k == "c") {
if (srs_hit({ type: "remote", tag: "zmw-" + v + "-community-ruleset", url: SRS_MAIN + "/" + v + ".srs", format: "binary" }, host, ip, false)) { hit = { list: v + ".srs", entry: "" }; break; }
} else if (k == "s" || k == "r") {
} else if (k == "s" || k == "r" || k == "n") {
if (srs_hit(url_rs(v), host, ip, false)) { hit = { list: ref_base(v), entry: "" }; break; }
}
}
@@ -33808,7 +34208,15 @@ return view.extend({
if (data.stopped) return badge('zm-off', 'Steer выключен');
if (secOff(id)) return badge('zm-off', 'секция выключена');
if (ex === 'none') return badge('zm-warn', s && s.kind === 'vpn' ? 'нет подписки' : s && s.kind === 'iface' ? 'нет туннеля' : s ? 'WARP не подключён' : 'нет подключения');
if (!secWork(id)) return badge('zm-off', 'ничего не выбрано');
if (!secWork(id)) {
var iv = s && ex === 'vpn' && data.steer_running ? (s.vpn && s.vpn.state) || {} : null;
if (iv && !iv.up && (iv.failed || iv.why || (iv.probe && iv.probe.state && iv.probe.state !== 'probing'))) {
var ib = badge('zm-warn', 'узел не отвечает');
ib.title = 'В секции ничего не выбрано, поэтому на работу это не влияет' + (iv.why ? '. Причина: ' + iv.why : '');
return ib;
}
return badge('zm-off', 'ничего не выбрано');
}
if (!data.steer_running) return badge('zm-bad', 'не работает');
if (ex === 'vpn') {
if (!s) return data.failopen ? badge('zm-bad', 'VPN не принят') : data.vpn_live === false && data.vpn_up ? badge('zm-bad', 'нет связи') : data.vpn_up ? badge('zm-ok', 'работает') : badge('zm-warn', 'подключаемся');
@@ -34816,14 +35224,18 @@ return view.extend({
else items.push([ 'fail', who + ': трафик через туннель не идёт', t.kind === 'awg' ? 'почините его на вкладке AmneziaWG' : 'проверьте подключение в Сеть → Интерфейсы' ]);
});
var d = diagRes.diag;
if (d && d.checks) d.checks.forEach(function(c) {
if (Array.isArray(diagRes.human) && diagRes.human.length) diagRes.human.forEach(function(c) { items.push([ c.v, c.what, c.why, c.tip, c.note ]); });
else if (d && d.checks) d.checks.forEach(function(c) {
if (c.verdict === 'ok' || c.verdict === 'warn' || c.verdict === 'fail') items.push([ c.verdict, c.what, c.why ]);
});
var bad = items.filter(function(i) { return i[0] === 'fail'; }).length, warn = items.filter(function(i) { return i[0] === 'warn'; }).length;
checkCard.appendChild(chkRow(E('span', { 'class': 'zm-label' }, 'Итог'), bad ? badge('zm-bad', 'есть поломка') : warn ? badge('zm-warn', 'работает, есть замечания') : badge('zm-ok', 'всё в порядке')));
var CLS = { ok: 'zm-ok', warn: 'zm-warn', fail: 'zm-bad' }, TXT = { ok: 'ок', warn: 'внимание', fail: 'ошибка' };
items.forEach(function(i) {
checkCard.appendChild(chkRow(badge(CLS[i[0]], TXT[i[0]]), E('span', {}, [].concat(i[1], i[2] ? ' — ' + i[2] : ''))));
var body = [ E('span', {}, [].concat(i[1], i[2] ? ' — ' + i[2] : '')) ];
if (i[3]) body.push(E('span', { 'class': 'zm-chk-tip' }, i[3]));
if (i[4]) body.push(E('span', { 'class': 'zm-chk-tip zm-chk-note' }, i[4]));
checkCard.appendChild(chkRow(badge(CLS[i[0]], TXT[i[0]]), body.length > 1 ? E('span', {}, body) : body[0]));
});
}
checkCard.appendChild(E('div', { 'class': 'zm-actions' }, [
@@ -40042,6 +40454,9 @@ html.zm-theme-dark .zm-card {
.zm-row.zm-chk { flex-wrap: nowrap; align-items: baseline; }
.zm-row.zm-chk > .zm-chk-h { flex: 0 0 104px; min-width: 0; }
.zm-row.zm-chk > .zm-chk-b { flex: 1 1 auto; min-width: 0; line-height: 1.5; }
.zm-chk-tip { display: block; margin-top: 3px; font-size: 12px; line-height: 1.45; opacity: .78; }
.zm-chk-tip::before { content: "→ "; opacity: .7; }
.zm-chk-note::before { content: "· "; }
.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; }
@@ -44169,7 +44584,7 @@ function pickFromCfg(c) {
c = c || {};
var items = ((c.catalog && c.catalog.items) || []).filter(function(it) { return it && it.id && (it.targets || []).length; });
var refs = c.refs || {}, toks = [], present = {}, sel = {}, covered = {}, extra = [];
[ 'c', 's', 'r' ].forEach(function(k) { (refs[k] || []).forEach(function(v) { toks.push(k + ':' + v); }); });
[ 'c', 's', 'r', 'n' ].forEach(function(k) { (refs[k] || []).forEach(function(v) { toks.push(k + ':' + v); }); });
var lists = (c.lists || []).slice();
toks.forEach(function(t) { present[refNorm(t)] = true; });
lists.forEach(function(u) { present[refNorm('l:' + u)] = true; });
@@ -44189,7 +44604,7 @@ function pickFromCfg(c) {
}
function refsOf(d) {
var out = { c: [], s: [], r: [], l: [] }, seen = {};
var out = { c: [], s: [], r: [], n: [], l: [] }, seen = {};
(d.items || []).forEach(function(it) {
if (!d.sel[it.id]) return;
it.targets.forEach(function(t) {
@@ -44485,7 +44900,7 @@ function validCfg(x) {
function sig(d) {
var srt = function(a) { return (a || []).slice().sort(); };
var tl = function(v) { return String(v || '').split(/\n/).map(function(x) { return x.trim(); }).filter(Boolean).join('\n'); };
return JSON.stringify([ extSig(d, SEC_EXT), extSig(d, SET_EXT), d.sec, d.mode, tl(d.links), tl(d.sub), d.sub_interval, d.iface, d.fastest, (function(r) { return [ srt(r.c), srt(r.s), srt(r.r), srt(r.l) ]; })(refsOf(d)), tl(d.domains), d.own_on !== false,
return JSON.stringify([ extSig(d, SEC_EXT), extSig(d, SET_EXT), d.sec, d.mode, tl(d.links), tl(d.sub), d.sub_interval, d.iface, d.fastest, (function(r) { return [ srt(r.c), srt(r.s), srt(r.r), srt(r.n), srt(r.l) ]; })(refsOf(d)), tl(d.domains), d.own_on !== false,
tl(d.subnets), tl(d.lists), srt(d.full), srt(d.all || []), srt(d.excl), d.mode === 'iface' ? '' : String(d.exclude || ''), d.dns.type, d.dns.servers, d.dns.bootstraps, d.dns.detour, d.quic_off, d.list_interval, d.lists_via,
String(d.label || '').trim(), d.isMain || d.enabled, tl(d.byDomains), tl(d.bySubnets) ]);
}
@@ -44555,7 +44970,7 @@ function newCfg(c) {
var n = ((c && c.sections) || []).length + 1;
x.sec = 'new'; x.is_main = false; x.exists = false; x.enabled = true; x.label = warp && /^zmwarp/.test(warp.name) && !((c && c.sections) || []).some(function(s) { return s.label === 'WARP'; }) ? 'WARP' : 'Секция ' + n;
x.mode = 'iface'; x.iface = warp ? warp.name : ''; x.links = []; x.sub = ''; x.sub_interval = '12h'; x.exclude = ''; x.hide_names = [];
x.refs = { c: [], s: [], r: [] }; x.own_on = false; x.domains = []; x.subnets = []; x.lists = []; x.subs = []; x.x = {};
x.refs = { c: [], s: [], r: [], n: [] }; x.own_on = false; x.domains = []; x.subnets = []; x.lists = []; x.subs = []; x.x = {};
x.full = [];
x.all = [];
return x;
@@ -44825,7 +45240,7 @@ return view.extend({
}
var rf = refsOf(draft);
var ownN = textLines(draft.domains).length + textLines(draft.subnets).length + textLines(draft.lists).length;
if (!rf.c.length && !rf.s.length && !rf.r.length && !rf.l.length && (!ownN || draft.own_on === false) && !draft.all.length && (draft.full.length || !secExists()))
if (!rf.c.length && !rf.s.length && !rf.r.length && !rf.n.length && !rf.l.length && (!ownN || draft.own_on === false) && !draft.all.length && (draft.full.length || !secExists()))
return [ 'svc', ownN ? '«Свой список» выключен, а других сервисов в секции «' + secName() + '» нет — включите его или выберите сервис'
: 'Выберите хотя бы один сервис, домен или адрес для секции «' + secName() + '» — устройства только ограничивают её правила' ];
var badOwn = tokLines(draft.domains).map(domainProblem).filter(Boolean)[0];
@@ -52016,6 +52431,10 @@ if [ -x /usr/bin/netshift ] && [ -f /usr/lib/netshift/constants.sh ] && ! _zmi_h
else _zmi_warn "Сверить не удалось — откройте вкладку Forkozz и нажмите «Перезапустить»"; fi
fi
case "$(/opt/zapret-manager-luci/backend.sh zapret_doh_migrate 2>/dev/null || true)" in
*'"changed":true'*) _zmi_ok "Блок DoH в стратегии Zapret обновлён до новой версии" ;;
esac
if [ -x /usr/bin/bytetube ]; then
case "$(/opt/zapret-manager-luci/backend.sh bytetube_sync 2>/dev/null || true)" in
*'"restarted":true'*) _zmi_ok "Скрипты ByeTube обновлены, ByeTube перезапущен" ;;